1use std::collections::HashMap;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::Arc;
12
13use futures::StreamExt;
14use photon_backend::{
15 consumer_group::instance_id_from_env,
16 consumer_group::static_assigned_shards,
17 consumer_group::{ConsumerGroupCoordinator, GroupMember, StaticGroupCoordinator},
18 delivery::DlqRecordParams,
19 delivery_mode::ShardConfig,
20 instrumentation::FailureReason,
21 HandlerDescriptor, HandlerRegistry, PhotonError, Result,
22};
23use photon_core::IdentityFactory;
24use tokio::task::{JoinHandle, JoinSet};
25use tokio_util::sync::CancellationToken;
26
27use crate::Photon;
28
29pub struct ExecutorController {
31 started: AtomicBool,
32 cancel: CancellationToken,
33 tasks: std::sync::Mutex<Vec<JoinHandle<()>>>,
34}
35
36impl Default for ExecutorController {
37 fn default() -> Self {
38 Self {
39 started: AtomicBool::new(false),
40 cancel: CancellationToken::new(),
41 tasks: std::sync::Mutex::new(Vec::new()),
42 }
43 }
44}
45
46impl ExecutorController {
47 #[tracing::instrument(name = "photon.executor.start", skip_all)]
59 pub fn start(&self, photon: &Photon, identity: &Arc<dyn IdentityFactory>) -> Result<()> {
60 if self
61 .started
62 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
63 .is_err()
64 {
65 return Err(PhotonError::Internal(
66 "Photon executor already started".into(),
67 ));
68 }
69
70 let registry = HandlerRegistry::auto_discover();
71 if registry.is_empty() {
72 return Ok(());
73 }
74
75 let mut tasks = Vec::new();
76 for handler in registry.iter() {
77 let cancel = self.cancel.child_token();
78 let task = if handler.is_consumer_group() {
79 spawn_group_handler_loop(photon.clone(), Arc::clone(identity), handler, cancel)
80 } else {
81 spawn_handler_loop(photon.clone(), Arc::clone(identity), handler, cancel)
82 };
83 tasks.push(task);
84 }
85
86 *self
87 .tasks
88 .lock()
89 .unwrap_or_else(std::sync::PoisonError::into_inner) = tasks;
90 Ok(())
91 }
92
93 pub fn shutdown(&self) {
100 self.cancel.cancel();
101 }
102
103 pub async fn join(&self) {
112 let tasks = std::mem::take(
113 &mut *self
114 .tasks
115 .lock()
116 .unwrap_or_else(std::sync::PoisonError::into_inner),
117 );
118 for task in tasks {
119 let _ = task.await;
120 }
121 }
122}
123
124const fn failure_reason(err: &PhotonError) -> FailureReason {
125 match err {
126 PhotonError::Identity(_) => FailureReason::IdentityBuild,
127 _ => FailureReason::HandlerError,
128 }
129}
130
131fn shard_count_for_handler(photon: &Photon, handler: &'static HandlerDescriptor) -> u32 {
132 handler
133 .group_shard_count
134 .or_else(|| {
135 photon
136 .registry()
137 .get(handler.topic_name)
138 .and_then(|d| d.shard_config)
139 .map(|c| c.shard_count)
140 })
141 .unwrap_or(ShardConfig::DEFAULT_SHARD_COUNT)
142}
143
144fn spawn_group_handler_loop(
145 photon: Photon,
146 identity: Arc<dyn IdentityFactory>,
147 handler: &'static HandlerDescriptor,
148 cancel: CancellationToken,
149) -> JoinHandle<()> {
150 let topic = handler.topic_name;
151 let group_id = handler.consumer_group.unwrap_or("");
152 let invoke = handler.invoke;
153 let services = Arc::clone(&photon.runtime().executor_services);
154 let coordinator = StaticGroupCoordinator;
155
156 tokio::spawn(async move {
157 let shard_count = shard_count_for_handler(&photon, handler);
158 let instance_id = instance_id_from_env().unwrap_or_else(|| "0".into());
159 let assigned = coordinator
160 .register(GroupMember {
161 group_id: group_id.to_string(),
162 instance_id: instance_id.clone(),
163 topic_name: topic.to_string(),
164 shard_count,
165 })
166 .await
167 .unwrap_or_else(|_| static_assigned_shards(shard_count));
168
169 let mut after_seq_by_shard = HashMap::new();
170 for shard_id in &assigned {
171 let shard_key = photon_backend::shard_storage_key(*shard_id);
172 let seq = photon
173 .get_checkpoint_seq(group_id, topic, Some(&shard_key))
174 .await
175 .ok()
176 .flatten()
177 .unwrap_or(0);
178 after_seq_by_shard.insert(*shard_id, Some(seq));
179 }
180
181 let mut stream = photon.subscribe_consumer_group(topic, &assigned, after_seq_by_shard);
182 let mut inflight = JoinSet::new();
183
184 loop {
185 tokio::select! {
186 biased;
187 () = cancel.cancelled() => break,
188 event_result = stream.next() => {
189 let Some(event_result) = event_result else { break };
190 match event_result {
191 Ok(event) => {
192 let permit = match services.worker_pool.acquire().await {
193 Ok(p) => p,
194 Err(e) => {
195 tracing::warn!(
196 topic = topic,
197 group = group_id,
198 error = %e,
199 "worker pool acquire failed"
200 );
201 continue;
202 }
203 };
204 let identity = Arc::clone(&identity);
205 let services = Arc::clone(&services);
206 let group = group_id.to_string();
207
208 inflight.spawn(async move {
209 let _permit = permit;
210 let event_id = event.event_id.clone();
211 let topic_name = event.topic_name.clone();
212 let topic_key = event.topic_key.clone();
213 let seq = event.seq;
214 let dispatch = invoke(identity.as_ref(), &event);
215 match dispatch.await {
216 Ok(()) => {
217 if let Err(e) = services
218 .checkpoint_coalescer
219 .record(&group, &topic_name, topic_key.as_deref(), seq)
220 .await
221 {
222 let _ = services.dlq.record(&DlqRecordParams {
223 event_id: &event_id,
224 topic_name: &topic_name,
225 topic_key: topic_key.as_deref(),
226 seq,
227 subscription_name: Some(&group),
228 reason: FailureReason::CheckpointError,
229 error: e.to_string(),
230 });
231 }
232 }
233 Err(e) => {
234 let _ = services.dlq.record(&DlqRecordParams {
235 event_id: &event_id,
236 topic_name: &topic_name,
237 topic_key: topic_key.as_deref(),
238 seq,
239 subscription_name: Some(&group),
240 reason: failure_reason(&e),
241 error: e.to_string(),
242 });
243 }
244 }
245 });
246 }
247 Err(e) => {
248 tracing::warn!(
249 topic = topic,
250 group = group_id,
251 error = %e,
252 "consumer group subscription stream error"
253 );
254 }
255 }
256 }
257 }
258 }
259
260 while inflight.join_next().await.is_some() {}
261 })
262}
263
264fn spawn_handler_loop(
265 photon: Photon,
266 identity: Arc<dyn IdentityFactory>,
267 handler: &'static HandlerDescriptor,
268 cancel: CancellationToken,
269) -> JoinHandle<()> {
270 let topic = handler.topic_name;
271 let subscription_name = handler.subscription_name;
272 let invoke = handler.invoke;
273 let services = Arc::clone(&photon.runtime().executor_services);
274
275 tokio::spawn(async move {
276 let after_seq = Some(
280 photon
281 .get_checkpoint_seq(subscription_name, topic, None)
282 .await
283 .ok()
284 .flatten()
285 .unwrap_or(0),
286 );
287
288 let mut stream = photon.subscribe(topic, None, after_seq);
289 let mut inflight = JoinSet::new();
290
291 loop {
292 tokio::select! {
293 biased;
294 () = cancel.cancelled() => break,
295 event_result = stream.next() => {
296 let Some(event_result) = event_result else { break };
297 match event_result {
298 Ok(event) => {
299 let permit = match services.worker_pool.acquire().await {
300 Ok(p) => p,
301 Err(e) => {
302 tracing::warn!(
303 topic = topic,
304 subscription = subscription_name,
305 error = %e,
306 "worker pool acquire failed"
307 );
308 continue;
309 }
310 };
311 let identity = Arc::clone(&identity);
312 let services = Arc::clone(&services);
313 let sub_name = subscription_name.to_string();
314
315 inflight.spawn(async move {
316 let _permit = permit;
317 let event_id = event.event_id.clone();
318 let topic_name = event.topic_name.clone();
319 let topic_key = event.topic_key.clone();
320 let seq = event.seq;
321 let dispatch = invoke(identity.as_ref(), &event);
322 match dispatch.await {
323 Ok(()) => {
324 if let Err(e) = services
325 .checkpoint_coalescer
326 .record(&sub_name, &topic_name, topic_key.as_deref(), seq)
327 .await
328 {
329 let _ = services.dlq.record(&DlqRecordParams {
330 event_id: &event_id,
331 topic_name: &topic_name,
332 topic_key: topic_key.as_deref(),
333 seq,
334 subscription_name: Some(&sub_name),
335 reason: FailureReason::CheckpointError,
336 error: e.to_string(),
337 });
338 }
339 }
340 Err(e) => {
341 let _ = services.dlq.record(&DlqRecordParams {
342 event_id: &event_id,
343 topic_name: &topic_name,
344 topic_key: topic_key.as_deref(),
345 seq,
346 subscription_name: Some(&sub_name),
347 reason: failure_reason(&e),
348 error: e.to_string(),
349 });
350 }
351 }
352 });
353 }
354 Err(e) => {
355 tracing::warn!(
356 topic = topic,
357 subscription = subscription_name,
358 error = %e,
359 "handler subscription stream error"
360 );
361 }
362 }
363 }
364 }
365 }
366
367 while inflight.join_next().await.is_some() {}
368 })
369}