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 /// Return the registered topic catalog.
255 #[must_use]
256 pub fn registry(&self) -> &TopicRegistry {
257 PhotonBackend::registry(self.backend.as_ref())
258 }
259
260 /// Read the last checkpoint sequence for a subscription/topic pair.
261 ///
262 /// # Errors
263 ///
264 /// Returns an error if the operation fails.
265 pub async fn get_checkpoint_seq(
266 &self,
267 subscription_name: &str,
268 topic_name: &str,
269 topic_key: Option<&str>,
270 ) -> Result<Option<i64>> {
271 PhotonBackend::get_checkpoint_seq(
272 self.backend.as_ref(),
273 subscription_name,
274 topic_name,
275 topic_key,
276 )
277 .await
278 }
279
280 /// Persist an updated checkpoint sequence for a subscription/topic pair.
281 ///
282 /// # Errors
283 ///
284 /// Returns an error if the operation fails.
285 pub async fn set_checkpoint(
286 &self,
287 subscription_name: &str,
288 topic_name: &str,
289 topic_key: Option<&str>,
290 last_seq: i64,
291 ) -> Result<()> {
292 PhotonBackend::set_checkpoint(
293 self.backend.as_ref(),
294 subscription_name,
295 topic_name,
296 topic_key,
297 last_seq,
298 )
299 .await
300 }
301
302 /// Shared tailer / executor services.
303 #[must_use]
304 pub const fn runtime(&self) -> &PhotonRuntimeState {
305 &self.runtime
306 }
307
308 /// Reclaim transport log rows past the safe watermark (ops / retention entry point).
309 ///
310 /// Call periodically (or from a headless ops job) after durable subscribers have advanced
311 /// checkpoints. Retention knobs: crate [`config`](https://docs.rs/uf-photon/latest/photon/config/)
312 /// (`PHOTON_TRANSPORT_*` / builder [`retention_policy`](crate::builder::PhotonBuilder::retention_policy)).
313 ///
314 /// # Errors
315 ///
316 /// Returns an error if a storage reclaim operation fails.
317 pub async fn reclaim_transport(&self) -> Result<Vec<ReclaimReport>> {
318 self.runtime
319 .executor_services
320 .retention_reclaimer
321 .sweep_all()
322 .await
323 }
324
325 /// Start inventory-registered `#[photon::subscribe]` handlers.
326 ///
327 /// Required on **Embedded** hosts and **Brokered worker** binaries. Publisher-only Brokered
328 /// processes typically skip this. Requires an [`IdentityFactory`] (e.g.
329 /// [`photon_core::JsonIdentityFactory`] for examples/tests) for actor reconstruction.
330 ///
331 /// See [Getting started → Brokered](https://docs.rs/uf-photon/latest/photon/#brokered-publisher--worker-binaries).
332 ///
333 /// # Example
334 ///
335 /// ```rust,no_run
336 /// use std::sync::Arc;
337 ///
338 /// use photon_core::JsonIdentityFactory;
339 /// use photon_runtime::Photon;
340 ///
341 /// # async fn boot() -> photon_backend::Result<()> {
342 /// let photon = Photon::builder().auto_registry().build()?;
343 /// photon.start_executor(Arc::new(JsonIdentityFactory))?;
344 /// photon.shutdown_executor();
345 /// photon.join_executor().await;
346 /// # Ok(())
347 /// # }
348 /// ```
349 ///
350 /// # Errors
351 ///
352 /// Returns an error if the executor was already started on this runtime.
353 #[allow(clippy::needless_pass_by_value)] // Arc-by-value is the public ownership API
354 pub fn start_executor(&self, identity: Arc<dyn IdentityFactory>) -> Result<()> {
355 self.runtime.executor.start(self, &identity)
356 }
357
358 /// Signal handler loops to stop accepting new events.
359 ///
360 /// # Contract
361 ///
362 /// Idempotent. Pair with [`Self::join_executor`] to await in-flight work.
363 pub fn shutdown_executor(&self) {
364 self.runtime.executor.shutdown();
365 }
366
367 /// Await handler loops and in-flight dispatches after [`Self::shutdown_executor`].
368 ///
369 /// # Contract
370 ///
371 /// Safe when the executor was never started. Restart requires a new [`Photon`] build.
372 pub async fn join_executor(&self) {
373 self.runtime.executor.join().await;
374 }
375}