Skip to main content

photon_runtime/
photon.rs

1//! Main Photon runtime handle — publish, subscribe, and executor control.
2//!
3//! See the crate [Getting started](https://docs.rs/uf-photon/latest/photon/#getting-started)
4//! for Mode 1 (embedded) and Mode 2 (brokered publisher / worker) walkthroughs.
5
6use std::sync::Arc;
7
8use futures::stream::Stream;
9use photon_core::IdentityFactory;
10
11use photon_backend::{Event, PhotonBackend, ReclaimReport, Result, StoragePort, TopicRegistry, ExecutorServices, BackendCapabilities};
12
13use crate::admin::collect_admin_snapshot;
14use crate::admin::AdminSnapshot;
15
16use crate::executor::ExecutorController;
17
18/// Shared storage port, executor services, and handler dispatch controller.
19#[derive(Clone)]
20pub struct PhotonRuntimeState {
21    /// Storage port used by executor checkpoint/retention services.
22    pub storage_port: Arc<dyn StoragePort>,
23    /// Services used by durable handler executors.
24    pub executor_services: Arc<ExecutorServices>,
25    /// Handler dispatch controller.
26    pub executor: Arc<ExecutorController>,
27}
28
29/// Main Photon runtime handle.
30///
31/// Keep this value alive for the lifetime of the process that publishes or runs handlers.
32/// Build it once with [`Photon::builder`], pass it to `publish_on` / `subscribe_on`, and call
33/// [`start_executor`](Self::start_executor) on Mode 1 hosts and Mode 2 **worker** binaries.
34///
35/// | Role | What to call |
36/// |------|----------------|
37/// | Publisher (Mode 2) | `publish_on(&photon)` — usually **no** executor |
38/// | Worker (Mode 2) | [`start_executor`](Self::start_executor) + `#[subscribe]` |
39/// | Embedded (Mode 1) | both publish and [`start_executor`](Self::start_executor) |
40///
41/// Getting started: [Mode 1](https://docs.rs/uf-photon/latest/photon/#mode-1--embedded-one-binary),
42/// [Mode 2](https://docs.rs/uf-photon/latest/photon/#mode-2--brokered-publisher--worker-binaries).
43///
44/// # Example
45///
46/// ```rust,no_run
47/// use std::sync::Arc;
48///
49/// use photon_core::JsonIdentityFactory;
50/// use photon_runtime::Photon;
51///
52/// # fn main() -> photon_backend::Result<()> {
53/// let photon = Photon::builder().auto_registry().build()?;
54/// photon.start_executor(Arc::new(JsonIdentityFactory))?;
55/// # let _ = photon;
56/// # Ok(())
57/// # }
58/// ```
59#[derive(Clone)]
60pub struct Photon {
61    backend: Arc<dyn PhotonBackend>,
62    runtime: PhotonRuntimeState,
63}
64
65static DEFAULT_PHOTON: std::sync::RwLock<Option<Photon>> = std::sync::RwLock::new(None);
66
67/// Configure the default Photon instance used by macro-generated convenience helpers
68/// (`Type::publish()` / `Type::subscribe()`).
69///
70/// Prefer passing an explicit [`Photon`] handle via `publish_on` / `subscribe_on` or
71/// [`Photon::publish`]. This process-wide shim is optional sugar for simple hosts.
72///
73/// # Example
74///
75/// ```rust,no_run
76/// use std::sync::Arc;
77///
78/// use photon_core::JsonIdentityFactory;
79/// use photon_runtime::{configure, Photon};
80///
81/// # fn main() -> photon_backend::Result<()> {
82/// let photon = Photon::builder().auto_registry().build()?;
83/// photon.start_executor(Arc::new(JsonIdentityFactory))?;
84/// configure(photon);
85/// # Ok(())
86/// # }
87/// ```
88///
89/// # Panics
90///
91/// Panics if an internal lock is poisoned.
92pub fn configure(photon: Photon) {
93    let mut guard = DEFAULT_PHOTON.write().unwrap();
94    *guard = Some(photon);
95}
96
97/// Clone of the process-wide Photon set by [`configure`], if any.
98///
99/// Prefer an explicit [`Photon`] handle. This exists for macro convenience helpers.
100///
101/// # Panics
102///
103/// Panics if an internal lock is poisoned.
104pub fn default() -> Option<Photon> {
105    let guard = DEFAULT_PHOTON.read().unwrap();
106    guard.clone()
107}
108
109impl Photon {
110    pub(crate) fn new(backend: Arc<dyn PhotonBackend>, runtime: PhotonRuntimeState) -> Self {
111        Self { backend, runtime }
112    }
113
114    /// Start building a Photon runtime instance.
115    ///
116    /// See [`crate::builder::PhotonBuilder`] for Mode 1 / Mode 2 wiring.
117    #[must_use]
118    pub fn builder() -> crate::builder::PhotonBuilder {
119        crate::builder::PhotonBuilder::default()
120    }
121
122    /// Telemetry label for the installed backend.
123    #[must_use]
124    pub fn backend_label(&self) -> &'static str {
125        self.backend.telemetry_label()
126    }
127
128    pub(crate) fn backend_capabilities(&self) -> BackendCapabilities {
129        PhotonBackend::capabilities(self.backend.as_ref())
130    }
131
132    /// Compose a read-only ops introspection snapshot for host admin UIs.
133    ///
134    /// Aggregates the topic catalog, handler inventory, backend capabilities, and checkpoint
135    /// cursors for inventory-registered handlers. Does not touch publish/subscribe hot paths.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if a checkpoint load fails.
140    pub async fn admin_snapshot(&self) -> Result<AdminSnapshot> {
141        collect_admin_snapshot(self).await
142    }
143
144    /// Publish a single event to a topic by name (low-level).
145    ///
146    /// Prefer the typed API generated by [`topic`](https://docs.rs/uf-photon/latest/photon/attr.topic.html):
147    /// `EventType { … }.publish_on(&photon).await`.
148    ///
149    /// # Example
150    ///
151    /// ```rust,ignore
152    /// // After #[topic(name = "orders.created")] on OrderCreated:
153    /// OrderCreated {
154    ///     order_id: "ord-1".into(),
155    ///     amount_cents: 9900,
156    /// }
157    /// .publish_on(&photon)
158    /// .await?;
159    /// ```
160    ///
161    /// # Errors
162    ///
163    /// Returns an error if the storage adapter rejects the append.
164    pub async fn publish(
165        &self,
166        topic_name: &str,
167        topic_key: Option<&str>,
168        actor_json: serde_json::Value,
169        payload_json: serde_json::Value,
170    ) -> Result<String> {
171        PhotonBackend::publish(
172            self.backend.as_ref(),
173            topic_name,
174            topic_key,
175            actor_json,
176            payload_json,
177        )
178        .await
179    }
180
181    /// Subscribe to topic events as a raw JSON stream (low-level).
182    ///
183    /// Prefer the typed API from [`topic`](https://docs.rs/uf-photon/latest/photon/attr.topic.html):
184    /// `EventType::subscribe_on(&photon, opts)`, or inventory handlers via `#[subscribe]` +
185    /// [`start_executor`](Self::start_executor).
186    ///
187    /// Runnable typed stream: `cargo run -p uf-photon --example keyed_topic --features runtime,mem`.
188    /// Runnable raw stream: `cargo run -p uf-photon --example manual_subscribe --features runtime,mem`.
189    ///
190    /// # Example (typed — preferred)
191    ///
192    /// ```rust,ignore
193    /// use futures::StreamExt;
194    /// use photon::{SubscribeOpts, topic};
195    ///
196    /// #[topic(name = "orders.created")]
197    /// struct OrderCreated { order_id: String }
198    ///
199    /// # async fn demo(photon: &photon::Photon) -> photon::Result<()> {
200    /// let mut stream = OrderCreated::subscribe_on(
201    ///     photon,
202    ///     SubscribeOpts::default_ephemeral(),
203    /// )
204    /// .await?;
205    /// if let Some(Ok(envelope)) = stream.next().await {
206    ///     let _ = envelope.payload.order_id;
207    /// }
208    /// # Ok(())
209    /// # }
210    /// ```
211    #[must_use]
212    pub fn subscribe(
213        &self,
214        topic_name: &str,
215        topic_key_filter: Option<&str>,
216        after_seq: Option<i64>,
217    ) -> std::pin::Pin<Box<dyn Stream<Item = Result<Event>> + Send>> {
218        PhotonBackend::subscribe(
219            self.backend.as_ref(),
220            topic_name.to_string(),
221            topic_key_filter.map(std::string::ToString::to_string),
222            after_seq,
223        )
224    }
225
226    /// Subscribe to assigned virtual shards for a consumer group (multiplexed stream).
227    #[must_use]
228    pub fn subscribe_consumer_group(
229        &self,
230        topic_name: &str,
231        shard_ids: &[u32],
232        after_seq_by_shard: std::collections::HashMap<u32, Option<i64>>,
233    ) -> std::pin::Pin<Box<dyn Stream<Item = Result<Event>> + Send>> {
234        photon_backend::merge_shard_streams(
235            Arc::clone(&self.backend),
236            topic_name.to_string(),
237            shard_ids,
238            after_seq_by_shard,
239        )
240    }
241
242    /// Load a specific event by ID.
243    ///
244    /// # Errors
245    ///
246    /// Returns an error if the operation fails.
247    pub async fn get_event(&self, event_id: &str) -> Result<Option<Event>> {
248        PhotonBackend::get_event(self.backend.as_ref(), event_id).await
249    }
250
251    /// Return the registered topic catalog.
252    #[must_use]
253    pub fn registry(&self) -> &TopicRegistry {
254        PhotonBackend::registry(self.backend.as_ref())
255    }
256
257    /// Read the last checkpoint sequence for a subscription/topic pair.
258    ///
259    /// # Errors
260    ///
261    /// Returns an error if the operation fails.
262    pub async fn get_checkpoint_seq(
263        &self,
264        subscription_name: &str,
265        topic_name: &str,
266        topic_key: Option<&str>,
267    ) -> Result<Option<i64>> {
268        PhotonBackend::get_checkpoint_seq(
269            self.backend.as_ref(),
270            subscription_name,
271            topic_name,
272            topic_key,
273        )
274        .await
275    }
276
277    /// Persist an updated checkpoint sequence for a subscription/topic pair.
278    ///
279    /// # Errors
280    ///
281    /// Returns an error if the operation fails.
282    pub async fn set_checkpoint(
283        &self,
284        subscription_name: &str,
285        topic_name: &str,
286        topic_key: Option<&str>,
287        last_seq: i64,
288    ) -> Result<()> {
289        PhotonBackend::set_checkpoint(
290            self.backend.as_ref(),
291            subscription_name,
292            topic_name,
293            topic_key,
294            last_seq,
295        )
296        .await
297    }
298
299    /// Shared tailer / executor services.
300    #[must_use]
301    pub const fn runtime(&self) -> &PhotonRuntimeState {
302        &self.runtime
303    }
304
305    /// Reclaim transport log rows past the safe watermark (ops / retention entry point).
306    ///
307    /// Call periodically (or from a headless ops job) after durable subscribers have advanced
308    /// checkpoints. Retention knobs: crate [`config`](https://docs.rs/uf-photon/latest/photon/config/)
309    /// (`PHOTON_TRANSPORT_*` / builder [`retention_policy`](crate::builder::PhotonBuilder::retention_policy)).
310    ///
311    /// # Errors
312    ///
313    /// Returns an error if a storage reclaim operation fails.
314    pub async fn reclaim_transport(&self) -> Result<Vec<ReclaimReport>> {
315        self.runtime
316            .executor_services
317            .retention_reclaimer
318            .sweep_all()
319            .await
320    }
321
322    /// Start inventory-registered `#[photon::subscribe]` handlers.
323    ///
324    /// Required on **Mode 1** hosts and **Mode 2 worker** binaries. Publisher-only Mode 2
325    /// processes typically skip this. Requires an [`IdentityFactory`] (e.g.
326    /// [`photon_core::JsonIdentityFactory`] for examples/tests) for actor reconstruction.
327    ///
328    /// See [Getting started → Mode 2](https://docs.rs/uf-photon/latest/photon/#mode-2--brokered-publisher--worker-binaries).
329    ///
330    /// # Example
331    ///
332    /// ```rust,no_run
333    /// use std::sync::Arc;
334    ///
335    /// use photon_core::JsonIdentityFactory;
336    /// use photon_runtime::Photon;
337    ///
338    /// # async fn boot() -> photon_backend::Result<()> {
339    /// let photon = Photon::builder().auto_registry().build()?;
340    /// photon.start_executor(Arc::new(JsonIdentityFactory))?;
341    /// photon.shutdown_executor();
342    /// photon.join_executor().await;
343    /// # Ok(())
344    /// # }
345    /// ```
346    ///
347    /// # Errors
348    ///
349    /// Returns an error if the executor was already started on this runtime.
350    #[allow(clippy::needless_pass_by_value)] // Arc-by-value is the public ownership API
351    pub fn start_executor(&self, identity: Arc<dyn IdentityFactory>) -> Result<()> {
352        self.runtime.executor.start(self, &identity)
353    }
354
355    /// Signal handler loops to stop accepting new events.
356    ///
357    /// # Contract
358    ///
359    /// Idempotent. Pair with [`Self::join_executor`] to await in-flight work.
360    pub fn shutdown_executor(&self) {
361        self.runtime.executor.shutdown();
362    }
363
364    /// Await handler loops and in-flight dispatches after [`Self::shutdown_executor`].
365    ///
366    /// # Contract
367    ///
368    /// Safe when the executor was never started. Restart requires a new [`Photon`] build.
369    pub async fn join_executor(&self) {
370        self.runtime.executor.join().await;
371    }
372}