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}