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 pub fn start(
63 &self,
64 photon: &Photon,
65 identity: &Arc<dyn IdentityFactory>,
66 ) -> Result<()> {
67 if self
68 .started
69 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
70 .is_err()
71 {
72 return Err(PhotonError::Internal(
73 "Photon executor already started".into(),
74 ));
75 }
76
77 let registry = HandlerRegistry::auto_discover();
78 if registry.is_empty() {
79 return Ok(());
80 }
81
82 let mut tasks = Vec::new();
83 for handler in registry.iter() {
84 let cancel = self.cancel.child_token();
85 let task = if handler.is_consumer_group() {
86 spawn_group_handler_loop(photon.clone(), Arc::clone(identity), handler, cancel)
87 } else {
88 spawn_handler_loop(photon.clone(), Arc::clone(identity), handler, cancel)
89 };
90 tasks.push(task);
91 }
92
93 *self.tasks.lock().unwrap() = tasks;
94 Ok(())
95 }
96
97 pub fn shutdown(&self) {
104 self.cancel.cancel();
105 }
106
107 pub async fn join(&self) {
120 let tasks = std::mem::take(&mut *self.tasks.lock().unwrap());
121 for task in tasks {
122 let _ = task.await;
123 }
124 }
125}
126
127const fn failure_reason(err: &PhotonError) -> FailureReason {
128 match err {
129 PhotonError::Identity(_) => FailureReason::IdentityBuild,
130 _ => FailureReason::HandlerError,
131 }
132}
133
134fn shard_count_for_handler(photon: &Photon, handler: &'static HandlerDescriptor) -> u32 {
135 handler
136 .group_shard_count
137 .or_else(|| {
138 photon
139 .registry()
140 .get(handler.topic_name)
141 .and_then(|d| d.shard_config)
142 .map(|c| c.shard_count)
143 })
144 .unwrap_or(ShardConfig::DEFAULT_SHARD_COUNT)
145}
146
147fn spawn_group_handler_loop(
148 photon: Photon,
149 identity: Arc<dyn IdentityFactory>,
150 handler: &'static HandlerDescriptor,
151 cancel: CancellationToken,
152) -> JoinHandle<()> {
153 let topic = handler.topic_name;
154 let group_id = handler.consumer_group.unwrap_or("");
155 let invoke = handler.invoke;
156 let services = Arc::clone(&photon.runtime().executor_services);
157 let coordinator = StaticGroupCoordinator;
158
159 tokio::spawn(async move {
160 let shard_count = shard_count_for_handler(&photon, handler);
161 let instance_id = instance_id_from_env().unwrap_or_else(|| "0".into());
162 let assigned = coordinator
163 .register(GroupMember {
164 group_id: group_id.to_string(),
165 instance_id: instance_id.clone(),
166 topic_name: topic.to_string(),
167 shard_count,
168 })
169 .await
170 .unwrap_or_else(|_| static_assigned_shards(shard_count));
171
172 let mut after_seq_by_shard = HashMap::new();
173 for shard_id in &assigned {
174 let shard_key = photon_backend::shard_storage_key(*shard_id);
175 let seq = photon
176 .get_checkpoint_seq(group_id, topic, Some(&shard_key))
177 .await
178 .ok()
179 .flatten()
180 .unwrap_or(0);
181 after_seq_by_shard.insert(*shard_id, Some(seq));
182 }
183
184 let mut stream =
185 photon.subscribe_consumer_group(topic, &assigned, after_seq_by_shard);
186 let mut inflight = JoinSet::new();
187
188 loop {
189 tokio::select! {
190 biased;
191 () = cancel.cancelled() => break,
192 event_result = stream.next() => {
193 let Some(event_result) = event_result else { break };
194 match event_result {
195 Ok(event) => {
196 let permit = services.worker_pool.acquire().await;
197 let identity = Arc::clone(&identity);
198 let services = Arc::clone(&services);
199 let group = group_id.to_string();
200
201 inflight.spawn(async move {
202 let _permit = permit;
203 let event_id = event.event_id.clone();
204 let topic_name = event.topic_name.clone();
205 let topic_key = event.topic_key.clone();
206 let seq = event.seq;
207 let dispatch = invoke(identity.as_ref(), &event);
208 match dispatch.await {
209 Ok(()) => {
210 if let Err(e) = services
211 .checkpoint_coalescer
212 .record(&group, &topic_name, topic_key.as_deref(), seq)
213 .await
214 {
215 let _ = services.dlq.record(&DlqRecordParams {
216 event_id: &event_id,
217 topic_name: &topic_name,
218 topic_key: topic_key.as_deref(),
219 seq,
220 subscription_name: Some(&group),
221 reason: FailureReason::CheckpointError,
222 error: e.to_string(),
223 });
224 }
225 }
226 Err(e) => {
227 let _ = services.dlq.record(&DlqRecordParams {
228 event_id: &event_id,
229 topic_name: &topic_name,
230 topic_key: topic_key.as_deref(),
231 seq,
232 subscription_name: Some(&group),
233 reason: failure_reason(&e),
234 error: e.to_string(),
235 });
236 }
237 }
238 });
239 }
240 Err(e) => {
241 tracing::warn!(
242 topic = topic,
243 group = group_id,
244 error = %e,
245 "consumer group subscription stream error"
246 );
247 }
248 }
249 }
250 }
251 }
252
253 while inflight.join_next().await.is_some() {}
254 })
255}
256
257fn spawn_handler_loop(
258 photon: Photon,
259 identity: Arc<dyn IdentityFactory>,
260 handler: &'static HandlerDescriptor,
261 cancel: CancellationToken,
262) -> JoinHandle<()> {
263 let topic = handler.topic_name;
264 let subscription_name = handler.subscription_name;
265 let invoke = handler.invoke;
266 let services = Arc::clone(&photon.runtime().executor_services);
267
268 tokio::spawn(async move {
269 let after_seq = Some(
273 photon
274 .get_checkpoint_seq(subscription_name, topic, None)
275 .await
276 .ok()
277 .flatten()
278 .unwrap_or(0),
279 );
280
281 let mut stream = photon.subscribe(topic, None, after_seq);
282 let mut inflight = JoinSet::new();
283
284 loop {
285 tokio::select! {
286 biased;
287 () = cancel.cancelled() => break,
288 event_result = stream.next() => {
289 let Some(event_result) = event_result else { break };
290 match event_result {
291 Ok(event) => {
292 let permit = services.worker_pool.acquire().await;
293 let identity = Arc::clone(&identity);
294 let services = Arc::clone(&services);
295 let sub_name = subscription_name.to_string();
296
297 inflight.spawn(async move {
298 let _permit = permit;
299 let event_id = event.event_id.clone();
300 let topic_name = event.topic_name.clone();
301 let topic_key = event.topic_key.clone();
302 let seq = event.seq;
303 let dispatch = invoke(identity.as_ref(), &event);
304 match dispatch.await {
305 Ok(()) => {
306 if let Err(e) = services
307 .checkpoint_coalescer
308 .record(&sub_name, &topic_name, topic_key.as_deref(), seq)
309 .await
310 {
311 let _ = services.dlq.record(&DlqRecordParams {
312 event_id: &event_id,
313 topic_name: &topic_name,
314 topic_key: topic_key.as_deref(),
315 seq,
316 subscription_name: Some(&sub_name),
317 reason: FailureReason::CheckpointError,
318 error: e.to_string(),
319 });
320 }
321 }
322 Err(e) => {
323 let _ = services.dlq.record(&DlqRecordParams {
324 event_id: &event_id,
325 topic_name: &topic_name,
326 topic_key: topic_key.as_deref(),
327 seq,
328 subscription_name: Some(&sub_name),
329 reason: failure_reason(&e),
330 error: e.to_string(),
331 });
332 }
333 }
334 });
335 }
336 Err(e) => {
337 tracing::warn!(
338 topic = topic,
339 subscription = subscription_name,
340 error = %e,
341 "handler subscription stream error"
342 );
343 }
344 }
345 }
346 }
347 }
348
349 while inflight.join_next().await.is_some() {}
350 })
351}