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