Skip to main content

mocra_core/engine/chain/
task_model_chain.rs

1#![allow(unused)]
2use crate::common::status_tracker::ErrorDecision;
3use crate::engine::events::{
4    EventBus, EventEnvelope, EventPhase, EventType, ModuleGenerateEvent, ParserTaskModelEvent,
5    RequestEvent, TaskModelEvent,
6};
7use crate::engine::processors::event_processor::{EventAwareTypedChain, EventProcessorTrait};
8use log::info;
9
10use crate::common::context::PipelineContext;
11use crate::common::model::chain_key;
12use crate::common::model::message::{TaskErrorEvent, TaskEvent, TaskParserEvent, UnifiedTaskInput};
13use crate::common::model::{ModuleConfig, Request};
14use crate::errors::{Error, ModuleError, Result};
15use async_trait::async_trait;
16
17use crate::common::processors::processor::{
18    ProcessorContext, ProcessorResult, ProcessorTrait, RetryPolicy,
19};
20use crate::common::processors::processor_chain::ErrorStrategy;
21use crate::queue::{QueueManager, QueuedItem};
22use futures::stream::StreamExt;
23use log::{debug, error, warn};
24use metrics::counter;
25use serde_json::json;
26use std::marker::PhantomData;
27use std::sync::Arc;
28use std::time::{SystemTime, UNIX_EPOCH};
29use uuid::Uuid;
30
31use crate::engine::chain::backpressure::{BackpressureSendState, send_with_backpressure};
32
33/// High-level action produced by threshold checks in ingress chains.
34#[derive(Debug, Clone)]
35pub enum ChainDecision {
36    /// Continue processing without side effects.
37    Continue,
38    /// Retry current path after delay.
39    RetryAfter {
40        delay: std::time::Duration,
41        action_applied: bool,
42    },
43    /// Stop processing for the current module scope.
44    TerminateModule {
45        reason: String,
46        action_applied: bool,
47    },
48    /// Stop processing for the current task scope.
49    TerminateTask {
50        reason: String,
51        action_applied: bool,
52    },
53}
54
55impl ChainDecision {
56    fn action_applied(&self) -> bool {
57        match self {
58            ChainDecision::Continue => false,
59            ChainDecision::RetryAfter { action_applied, .. } => *action_applied,
60            ChainDecision::TerminateModule { action_applied, .. } => *action_applied,
61            ChainDecision::TerminateTask { action_applied, .. } => *action_applied,
62        }
63    }
64}
65
66#[cfg(test)]
67mod decision_tests {
68    use super::ChainDecision;
69
70    #[test]
71    fn chain_decision_action_applied_flags_are_stable() {
72        let continue_decision = ChainDecision::Continue;
73        assert!(!continue_decision.action_applied());
74
75        let retry_lua = ChainDecision::RetryAfter {
76            delay: std::time::Duration::from_millis(1000),
77            action_applied: true,
78        };
79        let retry_fallback = ChainDecision::RetryAfter {
80            delay: std::time::Duration::from_millis(1000),
81            action_applied: false,
82        };
83        assert!(retry_lua.action_applied());
84        assert!(!retry_fallback.action_applied());
85
86        let terminate_module_lua = ChainDecision::TerminateModule {
87            reason: "module limit".to_string(),
88            action_applied: true,
89        };
90        let terminate_module_fallback = ChainDecision::TerminateModule {
91            reason: "module limit".to_string(),
92            action_applied: false,
93        };
94        assert!(terminate_module_lua.action_applied());
95        assert!(!terminate_module_fallback.action_applied());
96
97        let terminate_task_lua = ChainDecision::TerminateTask {
98            reason: "task limit".to_string(),
99            action_applied: true,
100        };
101        let terminate_task_fallback = ChainDecision::TerminateTask {
102            reason: "task limit".to_string(),
103            action_applied: false,
104        };
105        assert!(terminate_task_lua.action_applied());
106        assert!(!terminate_task_fallback.action_applied());
107    }
108}
109
110#[async_trait]
111/// Abstraction for threshold and termination decisions.
112pub trait ThresholdDecisionService: Send + Sync {
113    /// Performs preflight validation before task expansion.
114    async fn task_precheck(&self, task_id: &str) -> ChainDecision;
115    /// Decides how to handle an `ErrorTaskModel` path.
116    async fn error_task_decide(&self, input: &TaskErrorEvent) -> ChainDecision;
117}
118
119/// Default threshold decision service backed by `StatusTracker`.
120#[derive(Clone)]
121pub struct StatusTrackerThresholdDecisionService {
122    state: Arc<PipelineContext>,
123}
124
125impl StatusTrackerThresholdDecisionService {
126    /// Creates a threshold decision service backed by `StatusTracker`.
127    pub fn new(state: Arc<PipelineContext>) -> Self {
128        Self { state }
129    }
130}
131
132#[async_trait]
133impl ThresholdDecisionService for StatusTrackerThresholdDecisionService {
134    async fn task_precheck(&self, task_id: &str) -> ChainDecision {
135        match self
136            .state
137            .status_tracker
138            .should_task_continue(task_id)
139            .await
140        {
141            Ok(ErrorDecision::Continue) => ChainDecision::Continue,
142            Ok(ErrorDecision::Terminate(reason)) => ChainDecision::TerminateTask {
143                reason,
144                action_applied: false,
145            },
146            Err(err) => {
147                warn!(
148                    "[ThresholdDecisionService] task precheck failed: task_id={} error={}, fallback=continue",
149                    task_id, err
150                );
151                ChainDecision::Continue
152            }
153            _ => ChainDecision::Continue,
154        }
155    }
156
157    async fn error_task_decide(&self, input: &TaskErrorEvent) -> ChainDecision {
158        let task_id = chain_key::task_runtime_id(
159            &input.account_task.platform,
160            &input.account_task.account,
161            input.run_id,
162        );
163
164        let parse_error: Error = ModuleError::ModuleNotFound(input.error_msg.clone().into()).into();
165        let mut retry_after: Option<std::time::Duration> = None;
166
167        if let Some(modules) = &input.account_task.module {
168            for module_name in modules {
169                let module_id = chain_key::module_runtime_id(
170                    &input.account_task.account,
171                    &input.account_task.platform,
172                    module_name,
173                );
174                // Run-scoped module ID for StatusTracker error isolation across runs
175                let module_id_scoped = format!("{}-{}", module_id, input.run_id);
176
177                if input.prefix_request != Uuid::nil() {
178                    match self
179                        .state
180                        .status_tracker
181                        .record_parse_error(
182                            &task_id,
183                            &module_id_scoped,
184                            &input.prefix_request.to_string(),
185                            &parse_error,
186                        )
187                        .await
188                    {
189                        Ok(ErrorDecision::RetryAfter(delay)) => {
190                            retry_after = Some(delay);
191                        }
192                        Ok(_) => {}
193                        Err(err) => {
194                            warn!(
195                                "[ThresholdDecisionService] record_parse_error failed: task_id={} module_id={} error={}",
196                                task_id, module_id_scoped, err
197                            );
198                        }
199                    }
200                }
201
202                match self
203                    .state
204                    .status_tracker
205                    .should_module_continue(&module_id_scoped)
206                    .await
207                {
208                    Ok(ErrorDecision::Terminate(reason)) => {
209                        return ChainDecision::TerminateModule {
210                            reason,
211                            action_applied: false,
212                        };
213                    }
214                    Ok(_) => {}
215                    Err(err) => {
216                        warn!(
217                            "[ThresholdDecisionService] module precheck failed: module_id={} error={}",
218                            module_id, err
219                        );
220                    }
221                }
222            }
223        }
224
225        match self.task_precheck(&task_id).await {
226            ChainDecision::TerminateTask { reason, .. } => ChainDecision::TerminateTask {
227                reason,
228                action_applied: false,
229            },
230            _ => retry_after
231                .map(|delay| ChainDecision::RetryAfter {
232                    delay,
233                    action_applied: false,
234                })
235                .unwrap_or(ChainDecision::Continue),
236        }
237    }
238}
239
240/// First ingress processor for `TaskModel` and `ParserTaskModel` variants.
241///
242/// Responsibilities:
243/// - pre-check task thresholds,
244/// - load/construct `Task`,
245/// - handle error-task retry/terminate decisions,
246/// - emit semantic events for observability.
247pub struct TaskModelProcessor {
248    task_manager: Arc<TaskManager>,
249    state: Arc<PipelineContext>,
250    queue_manager: Arc<QueueManager>,
251    event_bus: Option<Arc<EventBus>>,
252    threshold_decision_service: Arc<dyn ThresholdDecisionService>,
253}
254
255impl TaskModelProcessor {
256    async fn task_precheck(&self, task_id: &str) -> ChainDecision {
257        self.threshold_decision_service.task_precheck(task_id).await
258    }
259
260    async fn error_task_decide(&self, input: &TaskErrorEvent) -> ChainDecision {
261        self.threshold_decision_service
262            .error_task_decide(input)
263            .await
264    }
265
266    async fn persist_error_retry_schedule(
267        &self,
268        input: &TaskErrorEvent,
269        delay: std::time::Duration,
270    ) {
271        let task_id = chain_key::task_runtime_id(
272            &input.account_task.platform,
273            &input.account_task.account,
274            input.run_id,
275        );
276        let module_id = input
277            .context
278            .module_id
279            .clone()
280            .or_else(|| {
281                input.account_task.module.as_ref().and_then(|m| {
282                    m.first().map(|name| {
283                        chain_key::module_runtime_id(
284                            &input.account_task.account,
285                            &input.account_task.platform,
286                            name,
287                        )
288                    })
289                })
290            })
291            .unwrap_or_else(|| {
292                chain_key::module_runtime_id(
293                    &input.account_task.account,
294                    &input.account_task.platform,
295                    "unknown",
296                )
297            });
298        let retry_key = chain_key::error_retry_schedule_key(&task_id);
299        let now_ms = SystemTime::now()
300            .duration_since(UNIX_EPOCH)
301            .unwrap_or_default()
302            .as_millis() as f64;
303        let score = now_ms + delay.as_millis() as f64;
304        let member = format!("{}:{}:{}", task_id, module_id, input.prefix_request);
305
306        if let Err(err) = self
307            .state
308            .cache_service
309            .zadd(&retry_key, score, member.as_bytes())
310            .await
311        {
312            warn!(
313                "[TaskModelProcessor<ErrorTaskModel>] failed to persist retry schedule: key={} error={}",
314                retry_key, err
315            );
316        }
317    }
318
319    async fn persist_terminate_mark(
320        &self,
321        input: &TaskErrorEvent,
322        terminate_module: Option<&str>,
323        reason: &str,
324    ) {
325        let task_id = chain_key::task_runtime_id(
326            &input.account_task.platform,
327            &input.account_task.account,
328            input.run_id,
329        );
330        let terminate_key = if let Some(module_id) = terminate_module {
331            chain_key::terminate_module_key(&task_id, module_id)
332        } else {
333            chain_key::terminate_task_key(&task_id)
334        };
335
336        let ttl_secs = {
337            let cfg = self.state.config.read().await;
338            cfg.cache.ttl
339        };
340        let payload = json!({
341            "reason": reason,
342            "task_id": task_id,
343            "module_id": terminate_module,
344            "prefix_request": input.prefix_request.to_string(),
345        })
346        .to_string();
347        if let Err(err) = self
348            .state
349            .cache_service
350            .set_nx(
351                &terminate_key,
352                payload.as_bytes(),
353                Some(std::time::Duration::from_secs(ttl_secs)),
354            )
355            .await
356        {
357            warn!(
358                "[TaskModelProcessor<ErrorTaskModel>] failed to persist terminate mark: key={} error={}",
359                terminate_key, err
360            );
361        }
362    }
363
364    async fn emit_threshold_terminated_event(
365        &self,
366        input: &TaskErrorEvent,
367        decision: &str,
368        reason: &str,
369    ) {
370        let Some(event_bus) = &self.event_bus else {
371            return;
372        };
373
374        let payload = json!({
375            "run_id": input.run_id.to_string(),
376            "account": input.account_task.account,
377            "platform": input.account_task.platform,
378            "module_id": input.context.module_id,
379            "step_idx": input.context.step_idx,
380            "prefix_request": input.prefix_request.to_string(),
381            "decision": decision,
382            "reason": reason,
383        });
384
385        if let Err(err) = event_bus
386            .publish(EventEnvelope::engine(
387                EventType::TaskTerminatedByThreshold,
388                EventPhase::Completed,
389                payload,
390            ))
391            .await
392        {
393            warn!(
394                "[TaskModelProcessor<ErrorTaskModel>] failed to publish threshold termination event: {}",
395                err
396            );
397        }
398    }
399}
400#[async_trait]
401impl ProcessorTrait<TaskEvent, Task> for TaskModelProcessor {
402    fn name(&self) -> &'static str {
403        "TaskModelProcessor"
404    }
405
406    async fn process(&self, input: TaskEvent, context: ProcessorContext) -> ProcessorResult<Task> {
407        debug!(
408            "[TaskModelProcessor] Processing Task: account={} platform={} modules={:?} retry={}",
409            input.account,
410            input.platform,
411            input.module,
412            context
413                .retry_policy
414                .as_ref()
415                .map(|r| r.current_retry)
416                .unwrap_or(0)
417        );
418
419        let task_id = chain_key::task_runtime_id(&input.platform, &input.account, input.run_id);
420        match self.task_precheck(&task_id).await {
421            ChainDecision::Continue => {}
422            ChainDecision::TerminateTask { reason, .. } => {
423                error!(
424                    "[TaskModelProcessor<TaskModel>] task terminated (pre-check): task_id={} reason={}",
425                    task_id, reason
426                );
427                if let Err(e) = self
428                    .queue_manager
429                    .send_to_dlq("task", &input, &reason)
430                    .await
431                {
432                    error!(
433                        "[TaskModelProcessor<TaskModel>] failed to send to DLQ: {}",
434                        e
435                    );
436                }
437                return ProcessorResult::FatalFailure(
438                    ModuleError::TaskMaxError(reason.into()).into(),
439                );
440            }
441            _ => {}
442        }
443
444        let task = self.task_manager.load_with_model(&input).await;
445        debug!(
446            "[TaskModelProcessor] load_with_model result: {:?}",
447            task.as_ref().map(|t| t.id())
448        );
449
450        match task {
451            Ok(task) => {
452                if task.is_empty() {
453                    return ProcessorResult::FatalFailure(
454                        ModuleError::ModuleNotFound(
455                            format!(
456                                "No modules found for the given TaskModel, task_model: {input:?}"
457                            )
458                            .into(),
459                        )
460                        .into(),
461                    );
462                }
463                let default_locker_ttl = self.state.config.read().await.crawler.module_locker_ttl;
464                let mut all_locked = true;
465                for m in task.modules.iter() {
466                    if !self
467                        .state
468                        .status_tracker
469                        .is_module_locker(m.id().as_ref(), default_locker_ttl)
470                        .await
471                    {
472                        all_locked = false;
473                        break;
474                    }
475                }
476                if all_locked {
477                    warn!(
478                        "[TaskModelProcessor<TaskModel>] all target modules locked, defer via retry policy: account={} platform={} run_id={}",
479                        input.account, input.platform, input.run_id
480                    );
481                    // Avoid duplicate amplification: this branch used to both
482                    // push back into `task-normal` and return RetryableFailure,
483                    // which could multiply the same TaskModel exponentially.
484                    // Keep a single retry path (executor-managed retry only).
485                    return ProcessorResult::RetryableFailure(
486                        context
487                            .retry_policy
488                            .unwrap_or_default()
489                            .with_reason("all target modules locked".to_string()),
490                    );
491                }
492
493                info!(
494                    "[TaskModelProcessor] Successfully converted TaskModel to Task. Task ID: {}, Modules count: {}",
495                    task.id(),
496                    task.modules.len()
497                );
498
499                ProcessorResult::Success(task)
500            }
501            Err(e) => {
502                debug!(
503                    "[TaskModelProcessor] load_with_model failed: account={} platform={} run_id={} error={e}",
504                    input.account, input.platform, input.run_id
505                );
506                warn!(
507                    "[TaskModelProcessor<TaskModel>] load_with_model failed, will retry: account={} platform={} run_id={} error={e}",
508                    input.account, input.platform, input.run_id
509                );
510                ProcessorResult::RetryableFailure(
511                    context
512                        .retry_policy
513                        .unwrap_or(RetryPolicy::default().with_reason(e.to_string())),
514                )
515            }
516        }
517    }
518    async fn pre_process(&self, _input: &TaskEvent, _context: &ProcessorContext) -> Result<()> {
519        Ok(())
520    }
521    async fn handle_error(
522        &self,
523        input: &TaskEvent,
524        error: Error,
525        _context: &ProcessorContext,
526    ) -> ProcessorResult<Task> {
527        let sender = self.queue_manager.get_error_push_channel();
528        let error_msg = TaskErrorEvent {
529            id: Default::default(),
530            account_task: input.clone(),
531            error_msg: error.to_string(),
532            timestamp: chrono::Utc::now().timestamp_millis() as u64,
533            metadata: Default::default(),
534            context: Default::default(),
535            run_id: input.run_id,
536            prefix_request: Uuid::nil(),
537        };
538        if let Err(e) = sender.send(QueuedItem::new(error_msg)).await {
539            error!("[TaskModelProcessor<TaskModel>] failed to enqueue ErrorTaskModel: {e}");
540        }
541        ProcessorResult::FatalFailure(
542            ModuleError::ModuleNotFound("Failed to load task, re-queued".into()).into(),
543        )
544    }
545}
546#[async_trait]
547impl EventProcessorTrait<TaskEvent, Task> for TaskModelProcessor {
548    fn pre_status(&self, input: &TaskEvent) -> Option<EventEnvelope> {
549        Some(EventEnvelope::engine(
550            EventType::TaskModel,
551            EventPhase::Started,
552            TaskModelEvent::from(input),
553        ))
554    }
555
556    fn finish_status(&self, input: &TaskEvent, output: &Task) -> Option<EventEnvelope> {
557        let mut task_model_event: TaskModelEvent = input.into();
558        task_model_event.modules = Some(output.get_module_names());
559        Some(EventEnvelope::engine(
560            EventType::TaskModel,
561            EventPhase::Completed,
562            task_model_event,
563        ))
564    }
565
566    fn working_status(&self, input: &TaskEvent) -> Option<EventEnvelope> {
567        Some(EventEnvelope::engine(
568            EventType::TaskModel,
569            EventPhase::Started,
570            TaskModelEvent::from(input),
571        ))
572    }
573
574    fn error_status(&self, input: &TaskEvent, error: &Error) -> Option<EventEnvelope> {
575        Some(EventEnvelope::engine_error(
576            EventType::TaskModel,
577            EventPhase::Failed,
578            TaskModelEvent::from(input),
579            error,
580        ))
581    }
582
583    fn retry_status(&self, input: &TaskEvent, retry_policy: &RetryPolicy) -> Option<EventEnvelope> {
584        Some(EventEnvelope::engine(
585            EventType::TaskModel,
586            EventPhase::Retry,
587            json!({
588                "data": TaskModelEvent::from(input),
589                "retry_count": retry_policy.current_retry,
590                "reason": retry_policy.reason.clone().unwrap_or_default(),
591            }),
592        ))
593    }
594}
595#[async_trait]
596impl ProcessorTrait<TaskParserEvent, Task> for TaskModelProcessor {
597    fn name(&self) -> &'static str {
598        "TaskModelProcessor"
599    }
600
601    async fn process(
602        &self,
603        input: TaskParserEvent,
604        context: ProcessorContext,
605    ) -> ProcessorResult<Task> {
606        debug!(
607            "[TaskModelProcessor<ParserTaskModel>] start: account={} platform={} crawler={:?} retry_count={}",
608            input.account_task.account,
609            input.account_task.platform,
610            input.account_task.module,
611            context
612                .retry_policy
613                .as_ref()
614                .map(|r| r.current_retry)
615                .unwrap_or(0)
616        );
617
618        // Check whether this task scope has already been terminated.
619        let task_id = chain_key::task_runtime_id(
620            &input.account_task.platform,
621            &input.account_task.account,
622            input.run_id,
623        );
624        match self.task_precheck(&task_id).await {
625            ChainDecision::Continue => {
626                debug!(
627                    "[TaskModelProcessor<ParserTaskModel>] task can continue: task_id={}",
628                    task_id
629                );
630            }
631            ChainDecision::TerminateTask { reason, .. } => {
632                error!(
633                    "[TaskModelProcessor<ParserTaskModel>] task terminated (pre-check): task_id={} reason={}",
634                    task_id, reason
635                );
636                return ProcessorResult::FatalFailure(
637                    ModuleError::TaskMaxError(reason.into()).into(),
638                );
639            }
640            _ => {}
641        }
642
643        let task = self.task_manager.load_parser(&input).await;
644        match task {
645            Ok(task) => ProcessorResult::Success(task),
646            Err(e) => {
647                warn!(
648                    "[TaskModelProcessor<ParserTaskModel>] load_parser failed, will retry: account={} platform={} run_id={} error={e}",
649                    input.account_task.account, input.account_task.platform, input.run_id
650                );
651                ProcessorResult::RetryableFailure(
652                    context
653                        .retry_policy
654                        .unwrap_or(RetryPolicy::default().with_reason(e.to_string())),
655                )
656            }
657        }
658    }
659    async fn pre_process(
660        &self,
661        _input: &TaskParserEvent,
662        _context: &ProcessorContext,
663    ) -> Result<()> {
664        Ok(())
665    }
666    async fn handle_error(
667        &self,
668        input: &TaskParserEvent,
669        error: Error,
670        _context: &ProcessorContext,
671    ) -> ProcessorResult<Task> {
672        let sender = self.queue_manager.get_error_push_channel();
673        let error_msg = TaskErrorEvent {
674            id: Default::default(),
675            account_task: input.account_task.clone(),
676            error_msg: error.to_string(),
677            timestamp: chrono::Utc::now().timestamp_millis() as u64,
678            metadata: Default::default(),
679            context: Default::default(),
680            run_id: input.run_id,
681            prefix_request: input.prefix_request,
682        };
683        if let Err(e) = sender.send(QueuedItem::new(error_msg)).await {
684            error!("[TaskModelProcessor<ParserTaskModel>] failed to enqueue ErrorTaskModel: {e}");
685        }
686        ProcessorResult::FatalFailure(
687            ModuleError::ModuleNotFound("Failed to load task, re-queued".into()).into(),
688        )
689    }
690}
691#[async_trait]
692impl EventProcessorTrait<TaskParserEvent, Task> for TaskModelProcessor {
693    fn pre_status(&self, input: &TaskParserEvent) -> Option<EventEnvelope> {
694        Some(EventEnvelope::engine(
695            EventType::ParserTaskModel,
696            EventPhase::Started,
697            ParserTaskModelEvent::from(input),
698        ))
699    }
700
701    fn finish_status(&self, input: &TaskParserEvent, output: &Task) -> Option<EventEnvelope> {
702        let mut evt: ParserTaskModelEvent = input.into();
703        evt.modules = Some(output.get_module_names());
704        Some(EventEnvelope::engine(
705            EventType::ParserTaskModel,
706            EventPhase::Completed,
707            evt,
708        ))
709    }
710
711    fn working_status(&self, input: &TaskParserEvent) -> Option<EventEnvelope> {
712        Some(EventEnvelope::engine(
713            EventType::ParserTaskModel,
714            EventPhase::Started,
715            ParserTaskModelEvent::from(input),
716        ))
717    }
718
719    fn error_status(&self, input: &TaskParserEvent, err: &Error) -> Option<EventEnvelope> {
720        Some(EventEnvelope::engine_error(
721            EventType::ParserTaskModel,
722            EventPhase::Failed,
723            ParserTaskModelEvent::from(input),
724            err,
725        ))
726    }
727
728    fn retry_status(
729        &self,
730        input: &TaskParserEvent,
731        retry_policy: &RetryPolicy,
732    ) -> Option<EventEnvelope> {
733        Some(EventEnvelope::engine(
734            EventType::ParserTaskModel,
735            EventPhase::Retry,
736            json!({
737                "data": ParserTaskModelEvent::from(input),
738                "retry_count": retry_policy.current_retry,
739                "reason": retry_policy.reason.clone().unwrap_or_default(),
740            }),
741        ))
742    }
743}
744#[async_trait]
745impl ProcessorTrait<TaskErrorEvent, Task> for TaskModelProcessor {
746    fn name(&self) -> &'static str {
747        "TaskModelProcessor"
748    }
749
750    async fn process(
751        &self,
752        input: TaskErrorEvent,
753        context: ProcessorContext,
754    ) -> ProcessorResult<Task> {
755        debug!(
756            "[TaskModelProcessor<ErrorTaskModel>] start: account={} platform={} crawler={:?} retry_count={}",
757            input.account_task.account,
758            input.account_task.platform,
759            input.account_task.module,
760            context
761                .retry_policy
762                .as_ref()
763                .map(|r| r.current_retry)
764                .unwrap_or(0)
765        );
766
767        let task_id = chain_key::task_runtime_id(
768            &input.account_task.platform,
769            &input.account_task.account,
770            input.run_id,
771        );
772        match self.error_task_decide(&input).await {
773            ChainDecision::Continue => {
774                counter!("mocra_error_task_threshold_decision_total", "decision" => "continue")
775                    .increment(1);
776                debug!(
777                    "[TaskModelProcessor<ErrorTaskModel>] task can continue: task_id={}",
778                    task_id
779                );
780            }
781            ChainDecision::RetryAfter {
782                delay,
783                action_applied,
784            } => {
785                counter!("mocra_error_task_threshold_decision_total", "decision" => "retry_after")
786                    .increment(1);
787                if !(ChainDecision::RetryAfter {
788                    delay,
789                    action_applied,
790                })
791                .action_applied()
792                {
793                    self.persist_error_retry_schedule(&input, delay).await;
794                }
795                let mut retry_policy = context.retry_policy.unwrap_or_default();
796                retry_policy.retry_delay = std::cmp::max(delay.as_millis() as u64, 1);
797                retry_policy.reason = Some(format!(
798                    "error task retry after {}ms",
799                    retry_policy.retry_delay
800                ));
801                return ProcessorResult::RetryableFailure(retry_policy);
802            }
803            ChainDecision::TerminateModule {
804                reason,
805                action_applied,
806            } => {
807                counter!("mocra_error_task_threshold_decision_total", "decision" => "terminate_module").increment(1);
808                let module_id = input
809                    .context
810                    .module_id
811                    .clone()
812                    .or_else(|| {
813                        input.account_task.module.as_ref().and_then(|m| {
814                            m.first().map(|name| {
815                                chain_key::module_runtime_id(
816                                    &input.account_task.account,
817                                    &input.account_task.platform,
818                                    name,
819                                )
820                            })
821                        })
822                    })
823                    .unwrap_or_else(|| {
824                        chain_key::module_runtime_id(
825                            &input.account_task.account,
826                            &input.account_task.platform,
827                            "unknown",
828                        )
829                    });
830                if !(ChainDecision::TerminateModule {
831                    reason: reason.clone(),
832                    action_applied,
833                })
834                .action_applied()
835                {
836                    self.persist_terminate_mark(&input, Some(&module_id), &reason)
837                        .await;
838                }
839                self.emit_threshold_terminated_event(&input, "terminate_module", &reason)
840                    .await;
841                warn!(
842                    "[TaskModelProcessor<ErrorTaskModel>] module terminated by threshold: task_id={} reason={}",
843                    task_id, reason
844                );
845                return ProcessorResult::FatalFailure(
846                    ModuleError::ModuleMaxError(reason.into()).into(),
847                );
848            }
849            ChainDecision::TerminateTask {
850                reason,
851                action_applied,
852            } => {
853                counter!("mocra_error_task_threshold_decision_total", "decision" => "terminate_task").increment(1);
854                if !(ChainDecision::TerminateTask {
855                    reason: reason.clone(),
856                    action_applied,
857                })
858                .action_applied()
859                {
860                    self.persist_terminate_mark(&input, None, &reason).await;
861                }
862                self.emit_threshold_terminated_event(&input, "terminate_task", &reason)
863                    .await;
864                error!(
865                    "[TaskModelProcessor<ErrorTaskModel>] task terminated (pre-check): task_id={} reason={}",
866                    task_id, reason
867                );
868                if let Err(e) = self
869                    .queue_manager
870                    .send_to_dlq("error_task", &input, &reason)
871                    .await
872                {
873                    error!(
874                        "[TaskModelProcessor<ErrorTaskModel>] failed to send to DLQ: {}",
875                        e
876                    );
877                }
878                return ProcessorResult::FatalFailure(
879                    ModuleError::TaskMaxError(reason.into()).into(),
880                );
881            }
882        }
883
884        let task = self.task_manager.load_error(&input).await;
885        match task {
886            Ok(task) => ProcessorResult::Success(task),
887            Err(e) => {
888                warn!(
889                    "[TaskModelProcessor<ErrorTaskModel>] load_error failed, will retry: account={} platform={} run_id={} error={e}",
890                    input.account_task.account, input.account_task.platform, input.run_id
891                );
892                ProcessorResult::RetryableFailure(
893                    context
894                        .retry_policy
895                        .unwrap_or(RetryPolicy::default().with_reason(e.to_string())),
896                )
897            }
898        }
899    }
900    async fn pre_process(
901        &self,
902        _input: &TaskErrorEvent,
903        _context: &ProcessorContext,
904    ) -> Result<()> {
905        Ok(())
906    }
907}
908#[async_trait]
909impl EventProcessorTrait<TaskErrorEvent, Task> for TaskModelProcessor {
910    fn pre_status(&self, input: &TaskErrorEvent) -> Option<EventEnvelope> {
911        Some(EventEnvelope::engine(
912            EventType::ParserTaskModel,
913            EventPhase::Started,
914            ParserTaskModelEvent::from(input),
915        ))
916    }
917
918    fn finish_status(&self, input: &TaskErrorEvent, output: &Task) -> Option<EventEnvelope> {
919        let mut evt: ParserTaskModelEvent = input.into();
920        evt.modules = Some(output.get_module_names());
921        Some(EventEnvelope::engine(
922            EventType::ParserTaskModel,
923            EventPhase::Completed,
924            evt,
925        ))
926    }
927
928    fn working_status(&self, input: &TaskErrorEvent) -> Option<EventEnvelope> {
929        Some(EventEnvelope::engine(
930            EventType::ParserTaskModel,
931            EventPhase::Started,
932            ParserTaskModelEvent::from(input),
933        ))
934    }
935
936    fn error_status(&self, input: &TaskErrorEvent, err: &Error) -> Option<EventEnvelope> {
937        Some(EventEnvelope::engine_error(
938            EventType::ParserTaskModel,
939            EventPhase::Failed,
940            ParserTaskModelEvent::from(input),
941            err,
942        ))
943    }
944
945    fn retry_status(
946        &self,
947        input: &TaskErrorEvent,
948        retry_policy: &RetryPolicy,
949    ) -> Option<EventEnvelope> {
950        Some(EventEnvelope::engine(
951            EventType::ParserTaskModel,
952            EventPhase::Retry,
953            json!({
954                "data": ParserTaskModelEvent::from(input),
955                "retry_count": retry_policy.current_retry,
956                "reason": retry_policy.reason.clone().unwrap_or_default(),
957            }),
958        ))
959    }
960}
961
962pub struct TaskModuleProcessor {
963    state: Arc<PipelineContext>,
964}
965#[async_trait]
966impl ProcessorTrait<Task, Vec<Module>> for TaskModuleProcessor {
967    fn name(&self) -> &'static str {
968        "TaskModuleProcessor"
969    }
970
971    async fn process(
972        &self,
973        input: Task,
974        _context: ProcessorContext,
975    ) -> ProcessorResult<Vec<Module>> {
976        debug!(
977            "[TaskModuleProcessor] start: task_id={} module_count={}",
978            input.id(),
979            input.modules.len()
980        );
981        let metadata = input.metadata.clone();
982        let login_info = input.login_info.clone();
983        let mut modules: Vec<Module> = Vec::new();
984        let default_locker_ttl = self.state.config.read().await.crawler.module_locker_ttl;
985        for mut module in input.modules {
986            // Check module-level threshold before generating requests.
987            match self
988                .state
989                .status_tracker
990                .should_module_continue(&module.id())
991                .await
992            {
993                Ok(ErrorDecision::Continue) => {
994                    debug!(
995                        "[TaskModuleProcessor] module can continue: module_id={}",
996                        module.id()
997                    );
998                }
999                Ok(ErrorDecision::Terminate(reason)) => {
1000                    // Module reached threshold; skip it and continue with others.
1001                    error!(
1002                        "[TaskModuleProcessor] skip terminated module: module_id={} reason={}",
1003                        module.id(),
1004                        reason
1005                    );
1006                    // Skip only this module; keep pipeline alive.
1007                    continue;
1008                }
1009                Err(e) => {
1010                    warn!(
1011                        "[TaskModuleProcessor] error tracker check failed for module: module_id={} error={}, continue anyway",
1012                        module.id(),
1013                        e
1014                    );
1015                }
1016                _ => {}
1017            }
1018
1019            module.locker_ttl = module
1020                .config
1021                .get_config_value("module_locker_ttl")
1022                .and_then(|v| v.as_u64())
1023                .unwrap_or(default_locker_ttl);
1024            module.bind_task_context(metadata.clone(), login_info.clone());
1025            modules.push(module);
1026        }
1027
1028        let mut filtered_modules: Vec<Module> = Vec::new();
1029        for x in modules.into_iter() {
1030            filtered_modules.push(x);
1031        }
1032        let modules = filtered_modules;
1033        ProcessorResult::Success(modules)
1034    }
1035}
1036
1037#[async_trait]
1038impl EventProcessorTrait<Task, Vec<Module>> for TaskModuleProcessor {
1039    fn pre_status(&self, _input: &Task) -> Option<EventEnvelope> {
1040        None
1041    }
1042
1043    fn finish_status(&self, _input: &Task, _output: &Vec<Module>) -> Option<EventEnvelope> {
1044        None
1045    }
1046
1047    fn working_status(&self, _input: &Task) -> Option<EventEnvelope> {
1048        None
1049    }
1050
1051    fn error_status(&self, _input: &Task, _err: &Error) -> Option<EventEnvelope> {
1052        None
1053    }
1054
1055    fn retry_status(&self, _input: &Task, _retry_policy: &RetryPolicy) -> Option<EventEnvelope> {
1056        None
1057    }
1058}
1059
1060pub struct TaskProcessor {
1061    task_manager: Arc<TaskManager>,
1062}
1063#[async_trait]
1064impl ProcessorTrait<Module, SyncBoxStream<'static, Request>> for TaskProcessor {
1065    fn name(&self) -> &'static str {
1066        "TaskProcessor"
1067    }
1068
1069    async fn process(
1070        &self,
1071        input: Module,
1072        context: ProcessorContext,
1073    ) -> ProcessorResult<SyncBoxStream<'static, Request>> {
1074        debug!(
1075            "[TaskProcessor] start generate: module_id={} module_name={}",
1076            input.id(),
1077            input.module.name()
1078        );
1079
1080        let (meta, login_info) = input.runtime_task_context();
1081
1082        // Context propagation path:
1083        // `TaskParserEvent.meta -> Task.metadata -> Module.bind_task_context -> Module.generate`.
1084        let requests: SyncBoxStream<'static, Request> = match input.generate(meta, login_info).await
1085        {
1086            Ok(stream) => stream,
1087            Err(e) => {
1088                warn!("[TaskProcessor] generate error, will retry: {e}");
1089                return ProcessorResult::RetryableFailure(
1090                    context
1091                        .retry_policy
1092                        .unwrap_or(RetryPolicy::default().with_reason(e.to_string())),
1093                );
1094            }
1095        };
1096        ProcessorResult::Success(requests)
1097    }
1098    async fn post_process(
1099        &self,
1100        _input: &Module,
1101        _output: &SyncBoxStream<'static, Request>,
1102        _context: &ProcessorContext,
1103    ) -> Result<()> {
1104        // Stream-based output cannot be checked for emptiness here without consumption.
1105        Ok(())
1106    }
1107}
1108#[async_trait]
1109impl EventProcessorTrait<Module, SyncBoxStream<'static, Request>> for TaskProcessor {
1110    fn pre_status(&self, input: &Module) -> Option<EventEnvelope> {
1111        Some(EventEnvelope::engine(
1112            EventType::ModuleGenerate,
1113            EventPhase::Started,
1114            ModuleGenerateEvent::from(input),
1115        ))
1116    }
1117
1118    fn finish_status(
1119        &self,
1120        input: &Module,
1121        _output: &SyncBoxStream<'static, Request>,
1122    ) -> Option<EventEnvelope> {
1123        Some(EventEnvelope::engine(
1124            EventType::ModuleGenerate,
1125            EventPhase::Completed,
1126            ModuleGenerateEvent::from(input),
1127        ))
1128    }
1129
1130    fn working_status(&self, input: &Module) -> Option<EventEnvelope> {
1131        Some(EventEnvelope::engine(
1132            EventType::ModuleGenerate,
1133            EventPhase::Started,
1134            ModuleGenerateEvent::from(input),
1135        ))
1136    }
1137
1138    fn error_status(&self, input: &Module, err: &Error) -> Option<EventEnvelope> {
1139        Some(EventEnvelope::engine_error(
1140            EventType::ModuleGenerate,
1141            EventPhase::Failed,
1142            ModuleGenerateEvent::from(input),
1143            err,
1144        ))
1145    }
1146
1147    fn retry_status(&self, input: &Module, retry_policy: &RetryPolicy) -> Option<EventEnvelope> {
1148        Some(EventEnvelope::engine(
1149            EventType::ModuleGenerate,
1150            EventPhase::Retry,
1151            json!({
1152                "data": ModuleGenerateEvent::from(input),
1153                "retry_count": retry_policy.current_retry,
1154                "reason": retry_policy.reason.clone().unwrap_or_default(),
1155            }),
1156        ))
1157    }
1158}
1159pub struct RequestPublish {
1160    queue_manager: Arc<QueueManager>,
1161    state: Arc<PipelineContext>,
1162}
1163
1164#[async_trait]
1165impl ProcessorTrait<Request, ()> for RequestPublish {
1166    fn name(&self) -> &'static str {
1167        "RequestPublish"
1168    }
1169
1170    async fn process(&self, input: Request, context: ProcessorContext) -> ProcessorResult<()> {
1171        let request_id = input.id;
1172        let module_id = input.module_id();
1173        let backpressure_retry_delay_ms = {
1174            let cfg = self.state.config.read().await;
1175            cfg.crawler.backpressure_retry_delay_ms
1176        };
1177
1178        info!(
1179            "[RequestPublish] publish request: request_id={} module_id={}",
1180            request_id, module_id
1181        );
1182
1183        // Persist request for downstream fallback/recovery.
1184        let id = input.id.to_string();
1185
1186        // Fire-and-forget cache persistence to avoid blocking hot stream path.
1187        let cache_service = self.state.cache_service.clone();
1188        let request_clone = input.clone();
1189
1190        tokio::spawn(async move {
1191            if let Err(e) = request_clone.send(&id, &cache_service).await {
1192                // Log at debug level to avoid spamming warns if cache is just busy
1193                debug!("[RequestPublish] persist failed (background): {e}");
1194            }
1195        });
1196
1197        let tx = self.queue_manager.get_request_push_channel();
1198        match send_with_backpressure(&tx, QueuedItem::new(input)).await {
1199            Ok(BackpressureSendState::Direct) => {}
1200            Ok(BackpressureSendState::RecoveredFromFull) => {
1201                counter!("mocra_request_publish_backpressure_total", "reason" => "queue_full")
1202                    .increment(1);
1203                warn!(
1204                    "[RequestPublish] queue full, falling back to awaited send: request_id={} module_id={} remaining_capacity={}",
1205                    request_id,
1206                    module_id,
1207                    tx.capacity()
1208                );
1209            }
1210            Err(err) => {
1211                if err.after_full {
1212                    counter!("mocra_request_publish_backpressure_total", "reason" => "queue_full")
1213                        .increment(1);
1214                    warn!(
1215                        "[RequestPublish] queue full before close: request_id={} module_id={} remaining_capacity={}",
1216                        request_id,
1217                        module_id,
1218                        tx.capacity()
1219                    );
1220                }
1221                counter!("mocra_request_publish_backpressure_total", "reason" => "queue_closed")
1222                    .increment(1);
1223                let retry_reason = format!(
1224                    "request queue closed: request_id={} module_id={}",
1225                    err.item.inner.id,
1226                    err.item.inner.module_id()
1227                );
1228                error!("[RequestPublish] {retry_reason}");
1229                let mut retry_policy = context.retry_policy.unwrap_or_default();
1230                if let Some(delay_ms) = backpressure_retry_delay_ms {
1231                    retry_policy.retry_delay = delay_ms.max(1);
1232                }
1233                retry_policy.reason = Some(retry_reason);
1234                return ProcessorResult::RetryableFailure(retry_policy);
1235            }
1236        }
1237        ProcessorResult::Success(())
1238    }
1239    async fn handle_error(
1240        &self,
1241        input: &Request,
1242        error: Error,
1243        _context: &ProcessorContext,
1244    ) -> ProcessorResult<()> {
1245        // If we can't publish the request after retries, release the module lock
1246        // to avoid locking out future runs of this module.
1247        self.state
1248            .status_tracker
1249            .release_module_locker(&input.module_runtime_id())
1250            .await;
1251        ProcessorResult::FatalFailure(error)
1252    }
1253}
1254#[async_trait]
1255impl EventProcessorTrait<Request, ()> for RequestPublish {
1256    fn pre_status(&self, input: &Request) -> Option<EventEnvelope> {
1257        Some(EventEnvelope::engine(
1258            EventType::RequestPublish,
1259            EventPhase::Started,
1260            RequestEvent::from(input),
1261        ))
1262    }
1263
1264    fn finish_status(&self, input: &Request, _output: &()) -> Option<EventEnvelope> {
1265        Some(EventEnvelope::engine(
1266            EventType::RequestPublish,
1267            EventPhase::Completed,
1268            RequestEvent::from(input),
1269        ))
1270    }
1271
1272    fn working_status(&self, input: &Request) -> Option<EventEnvelope> {
1273        Some(EventEnvelope::engine(
1274            EventType::RequestPublish,
1275            EventPhase::Started,
1276            RequestEvent::from(input),
1277        ))
1278    }
1279
1280    fn error_status(&self, input: &Request, err: &Error) -> Option<EventEnvelope> {
1281        Some(EventEnvelope::engine_error(
1282            EventType::RequestPublish,
1283            EventPhase::Failed,
1284            RequestEvent::from(input),
1285            err,
1286        ))
1287    }
1288
1289    fn retry_status(&self, input: &Request, retry_policy: &RetryPolicy) -> Option<EventEnvelope> {
1290        Some(EventEnvelope::engine(
1291            EventType::RequestPublish,
1292            EventPhase::Retry,
1293            json!({
1294                "data": RequestEvent::from(input),
1295                "retry_count": retry_policy.current_retry,
1296                "reason": retry_policy.reason.clone().unwrap_or_default(),
1297            }),
1298        ))
1299    }
1300}
1301pub struct ConfigProcessor {
1302    pub state: Arc<PipelineContext>,
1303}
1304#[async_trait]
1305impl ProcessorTrait<Request, (Request, Option<ModuleConfig>)> for ConfigProcessor {
1306    fn name(&self) -> &'static str {
1307        "ConfigProcessor"
1308    }
1309
1310    async fn process(
1311        &self,
1312        input: Request,
1313        context: ProcessorContext,
1314    ) -> ProcessorResult<(Request, Option<ModuleConfig>)> {
1315        // ModuleConfig is persisted by loader flow keyed by `module_id`; read-through here.
1316        match ModuleConfig::sync(&input.module_id(), &self.state.cache_service).await {
1317            Ok(Some(config)) => ProcessorResult::Success((input, Some(config))),
1318            Ok(None) => ProcessorResult::Success((input, None)),
1319            Err(e) => {
1320                error!(
1321                    "Failed to fetch config for module {}: {}",
1322                    input.task_id(),
1323                    e
1324                );
1325                ProcessorResult::RetryableFailure(
1326                    context
1327                        .retry_policy
1328                        .unwrap_or(RetryPolicy::default().with_reason(e.to_string())),
1329                )
1330            }
1331        }
1332    }
1333    async fn pre_process(&self, _input: &Request, _context: &ProcessorContext) -> Result<()> {
1334        self.state
1335            .status_tracker
1336            .lock_module(&_input.module_runtime_id())
1337            .await;
1338        Ok(())
1339    }
1340    async fn handle_error(
1341        &self,
1342        _input: &Request,
1343        _error: Error,
1344        _context: &ProcessorContext,
1345    ) -> ProcessorResult<(Request, Option<ModuleConfig>)> {
1346        ProcessorResult::Success((_input.clone(), None))
1347    }
1348}
1349#[async_trait]
1350impl EventProcessorTrait<Request, (Request, Option<ModuleConfig>)> for ConfigProcessor {
1351    fn pre_status(&self, _input: &Request) -> Option<EventEnvelope> {
1352        None
1353    }
1354
1355    fn finish_status(
1356        &self,
1357        _input: &Request,
1358        _out: &(Request, Option<ModuleConfig>),
1359    ) -> Option<EventEnvelope> {
1360        None
1361    }
1362
1363    fn working_status(&self, _input: &Request) -> Option<EventEnvelope> {
1364        None
1365    }
1366
1367    fn error_status(&self, _input: &Request, _err: &Error) -> Option<EventEnvelope> {
1368        None
1369    }
1370
1371    fn retry_status(&self, _input: &Request, _retry_policy: &RetryPolicy) -> Option<EventEnvelope> {
1372        None
1373    }
1374}
1375
1376use crate::cacheable::CacheAble;
1377use crate::engine::task::module::Module;
1378use crate::engine::task::{Task, TaskManager};
1379use futures::stream::Stream;
1380use std::pin::Pin;
1381
1382pub type SyncBoxStream<'a, T> = Pin<Box<dyn Stream<Item = T> + Send + Sync + 'a>>;
1383
1384/// Converts a vector into a boxed stream for stream-native chain stages.
1385pub struct VecToStreamProcessor<T> {
1386    _marker: PhantomData<T>,
1387}
1388
1389impl<T> Default for VecToStreamProcessor<T> {
1390    fn default() -> Self {
1391        Self::new()
1392    }
1393}
1394
1395impl<T> VecToStreamProcessor<T> {
1396    pub fn new() -> Self {
1397        Self {
1398            _marker: PhantomData,
1399        }
1400    }
1401}
1402
1403#[async_trait]
1404impl<T: Send + Sync + 'static> ProcessorTrait<Vec<T>, SyncBoxStream<'static, T>>
1405    for VecToStreamProcessor<T>
1406{
1407    fn name(&self) -> &'static str {
1408        "VecToStreamProcessor"
1409    }
1410
1411    async fn process(
1412        &self,
1413        input: Vec<T>,
1414        _context: ProcessorContext,
1415    ) -> ProcessorResult<SyncBoxStream<'static, T>> {
1416        let stream = futures::stream::iter(input);
1417        let boxed: SyncBoxStream<'static, T> = Box::pin(stream);
1418        ProcessorResult::Success(boxed)
1419    }
1420}
1421
1422#[async_trait]
1423impl<T: Send + Sync + 'static> EventProcessorTrait<Vec<T>, SyncBoxStream<'static, T>>
1424    for VecToStreamProcessor<T>
1425{
1426    fn pre_status(&self, _input: &Vec<T>) -> Option<EventEnvelope> {
1427        None
1428    }
1429    fn finish_status(
1430        &self,
1431        _input: &Vec<T>,
1432        _output: &SyncBoxStream<'static, T>,
1433    ) -> Option<EventEnvelope> {
1434        None
1435    }
1436    fn working_status(&self, _input: &Vec<T>) -> Option<EventEnvelope> {
1437        None
1438    }
1439    fn error_status(&self, _input: &Vec<T>, _err: &Error) -> Option<EventEnvelope> {
1440        None
1441    }
1442    fn retry_status(&self, _input: &Vec<T>, _retry_policy: &RetryPolicy) -> Option<EventEnvelope> {
1443        None
1444    }
1445}
1446
1447pub struct FlattenStreamVecProcessor<T> {
1448    _marker: PhantomData<T>,
1449    #[allow(clippy::type_complexity)]
1450    logger: Option<Arc<dyn Fn(&T) + Send + Sync>>,
1451}
1452
1453impl<T> Default for FlattenStreamVecProcessor<T> {
1454    fn default() -> Self {
1455        Self::new()
1456    }
1457}
1458
1459impl<T> FlattenStreamVecProcessor<T> {
1460    pub fn new() -> Self {
1461        Self {
1462            _marker: PhantomData,
1463            logger: None,
1464        }
1465    }
1466    pub fn with_logger<F>(mut self, logger: F) -> Self
1467    where
1468        F: Fn(&T) + Send + Sync + 'static,
1469    {
1470        self.logger = Some(Arc::new(logger));
1471        self
1472    }
1473}
1474
1475#[async_trait]
1476impl<T: Send + Sync + 'static>
1477    ProcessorTrait<SyncBoxStream<'static, Vec<T>>, SyncBoxStream<'static, T>>
1478    for FlattenStreamVecProcessor<T>
1479{
1480    fn name(&self) -> &'static str {
1481        "FlattenStreamVecProcessor"
1482    }
1483
1484    async fn process(
1485        &self,
1486        input: SyncBoxStream<'static, Vec<T>>,
1487        _context: ProcessorContext,
1488    ) -> ProcessorResult<SyncBoxStream<'static, T>> {
1489        let logger = self.logger.clone();
1490        let stream = input.flat_map(move |v| {
1491            let logger = logger.clone();
1492            let iter = v.into_iter().inspect(move |item| {
1493                if let Some(l) = &logger {
1494                    l(item);
1495                }
1496            });
1497            futures::stream::iter(iter)
1498        });
1499        let boxed: SyncBoxStream<'static, T> = Box::pin(stream);
1500        ProcessorResult::Success(boxed)
1501    }
1502}
1503
1504#[async_trait]
1505impl<T: Send + Sync + 'static>
1506    EventProcessorTrait<SyncBoxStream<'static, Vec<T>>, SyncBoxStream<'static, T>>
1507    for FlattenStreamVecProcessor<T>
1508{
1509    fn pre_status(&self, _input: &SyncBoxStream<'static, Vec<T>>) -> Option<EventEnvelope> {
1510        None
1511    }
1512    fn finish_status(
1513        &self,
1514        _input: &SyncBoxStream<'static, Vec<T>>,
1515        _output: &SyncBoxStream<'static, T>,
1516    ) -> Option<EventEnvelope> {
1517        None
1518    }
1519    fn working_status(&self, _input: &SyncBoxStream<'static, Vec<T>>) -> Option<EventEnvelope> {
1520        None
1521    }
1522    fn error_status(
1523        &self,
1524        _input: &SyncBoxStream<'static, Vec<T>>,
1525        _err: &Error,
1526    ) -> Option<EventEnvelope> {
1527        None
1528    }
1529    fn retry_status(
1530        &self,
1531        _input: &SyncBoxStream<'static, Vec<T>>,
1532        _retry_policy: &RetryPolicy,
1533    ) -> Option<EventEnvelope> {
1534        None
1535    }
1536}
1537
1538pub struct StreamLoggerProcessor<T> {
1539    _marker: PhantomData<T>,
1540    name: String,
1541    #[allow(clippy::type_complexity)]
1542    logger: Option<Arc<dyn Fn(&T) + Send + Sync>>,
1543}
1544
1545impl<T> StreamLoggerProcessor<T> {
1546    pub fn new(name: &str) -> Self {
1547        Self {
1548            _marker: PhantomData,
1549            name: name.to_string(),
1550            logger: None,
1551        }
1552    }
1553    pub fn with_logger<F>(mut self, logger: F) -> Self
1554    where
1555        F: Fn(&T) + Send + Sync + 'static,
1556    {
1557        self.logger = Some(Arc::new(logger));
1558        self
1559    }
1560}
1561
1562#[async_trait]
1563impl<T: Send + Sync + 'static> ProcessorTrait<SyncBoxStream<'static, T>, SyncBoxStream<'static, T>>
1564    for StreamLoggerProcessor<T>
1565{
1566    fn name(&self) -> &'static str {
1567        "StreamLoggerProcessor"
1568    }
1569
1570    async fn process(
1571        &self,
1572        input: SyncBoxStream<'static, T>,
1573        _context: ProcessorContext,
1574    ) -> ProcessorResult<SyncBoxStream<'static, T>> {
1575        let logger = self.logger.clone();
1576        let stream = input.map(move |item| {
1577            if let Some(l) = &logger {
1578                l(&item);
1579            }
1580            item
1581        });
1582        let boxed: SyncBoxStream<'static, T> = Box::pin(stream);
1583        ProcessorResult::Success(boxed)
1584    }
1585}
1586
1587#[async_trait]
1588impl<T: Send + Sync + 'static>
1589    EventProcessorTrait<SyncBoxStream<'static, T>, SyncBoxStream<'static, T>>
1590    for StreamLoggerProcessor<T>
1591{
1592    fn pre_status(&self, _input: &SyncBoxStream<'static, T>) -> Option<EventEnvelope> {
1593        None
1594    }
1595    fn finish_status(
1596        &self,
1597        _input: &SyncBoxStream<'static, T>,
1598        _output: &SyncBoxStream<'static, T>,
1599    ) -> Option<EventEnvelope> {
1600        None
1601    }
1602    fn working_status(&self, _input: &SyncBoxStream<'static, T>) -> Option<EventEnvelope> {
1603        None
1604    }
1605    fn error_status(
1606        &self,
1607        _input: &SyncBoxStream<'static, T>,
1608        _err: &Error,
1609    ) -> Option<EventEnvelope> {
1610        None
1611    }
1612    fn retry_status(
1613        &self,
1614        _input: &SyncBoxStream<'static, T>,
1615        _retry_policy: &RetryPolicy,
1616    ) -> Option<EventEnvelope> {
1617        None
1618    }
1619}
1620
1621pub struct FlattenStreamProcessor<T> {
1622    _marker: PhantomData<T>,
1623}
1624
1625impl<T> Default for FlattenStreamProcessor<T> {
1626    fn default() -> Self {
1627        Self::new()
1628    }
1629}
1630
1631impl<T> FlattenStreamProcessor<T> {
1632    pub fn new() -> Self {
1633        Self {
1634            _marker: PhantomData,
1635        }
1636    }
1637}
1638
1639#[async_trait]
1640impl<T: Send + Sync + 'static>
1641    ProcessorTrait<SyncBoxStream<'static, SyncBoxStream<'static, T>>, SyncBoxStream<'static, T>>
1642    for FlattenStreamProcessor<T>
1643{
1644    fn name(&self) -> &'static str {
1645        "FlattenStreamProcessor"
1646    }
1647
1648    async fn process(
1649        &self,
1650        input: SyncBoxStream<'static, SyncBoxStream<'static, T>>,
1651        _context: ProcessorContext,
1652    ) -> ProcessorResult<SyncBoxStream<'static, T>> {
1653        let stream = input.flatten();
1654        let boxed: SyncBoxStream<'static, T> = Box::pin(stream);
1655        ProcessorResult::Success(boxed)
1656    }
1657}
1658
1659#[async_trait]
1660impl<T: Send + Sync + 'static>
1661    EventProcessorTrait<
1662        SyncBoxStream<'static, SyncBoxStream<'static, T>>,
1663        SyncBoxStream<'static, T>,
1664    > for FlattenStreamProcessor<T>
1665{
1666    fn pre_status(
1667        &self,
1668        _input: &SyncBoxStream<'static, SyncBoxStream<'static, T>>,
1669    ) -> Option<EventEnvelope> {
1670        None
1671    }
1672    fn finish_status(
1673        &self,
1674        _input: &SyncBoxStream<'static, SyncBoxStream<'static, T>>,
1675        _output: &SyncBoxStream<'static, T>,
1676    ) -> Option<EventEnvelope> {
1677        None
1678    }
1679    fn working_status(
1680        &self,
1681        _input: &SyncBoxStream<'static, SyncBoxStream<'static, T>>,
1682    ) -> Option<EventEnvelope> {
1683        None
1684    }
1685    fn error_status(
1686        &self,
1687        _input: &SyncBoxStream<'static, SyncBoxStream<'static, T>>,
1688        _err: &Error,
1689    ) -> Option<EventEnvelope> {
1690        None
1691    }
1692    fn retry_status(
1693        &self,
1694        _input: &SyncBoxStream<'static, SyncBoxStream<'static, T>>,
1695        _retry_policy: &RetryPolicy,
1696    ) -> Option<EventEnvelope> {
1697        None
1698    }
1699}
1700
1701pub struct UnifiedTaskIngressChain {
1702    /// Ingress chain for standard task creation path.
1703    task_chain: Arc<EventAwareTypedChain<TaskEvent, SyncBoxStream<'static, ()>>>,
1704    /// Ingress chain for parser-produced follow-up tasks.
1705    parser_chain: Arc<EventAwareTypedChain<TaskParserEvent, SyncBoxStream<'static, ()>>>,
1706    /// Ingress chain for error-task retries/termination path.
1707    error_chain: Arc<EventAwareTypedChain<TaskErrorEvent, SyncBoxStream<'static, ()>>>,
1708}
1709
1710impl UnifiedTaskIngressChain {
1711    /// Constructs a multiplexer over three ingress typed chains.
1712    pub fn new(
1713        task_chain: Arc<EventAwareTypedChain<TaskEvent, SyncBoxStream<'static, ()>>>,
1714        parser_chain: Arc<EventAwareTypedChain<TaskParserEvent, SyncBoxStream<'static, ()>>>,
1715        error_chain: Arc<EventAwareTypedChain<TaskErrorEvent, SyncBoxStream<'static, ()>>>,
1716    ) -> Self {
1717        Self {
1718            task_chain,
1719            parser_chain,
1720            error_chain,
1721        }
1722    }
1723
1724    pub async fn execute(
1725        &self,
1726        input: UnifiedTaskInput,
1727        context: ProcessorContext,
1728    ) -> ProcessorResult<SyncBoxStream<'static, ()>> {
1729        match input {
1730            UnifiedTaskInput::Task(task) => self.task_chain.execute(task, context).await,
1731            UnifiedTaskInput::ParserTask(task) => self.parser_chain.execute(task, context).await,
1732            UnifiedTaskInput::ErrorTask(task) => self.error_chain.execute(task, context).await,
1733        }
1734    }
1735}
1736
1737/// Builds the unified ingress chain graph used by task workers.
1738///
1739/// The resulting chain set handles three inputs:
1740/// - `TaskModel` (initial tasks),
1741/// - `ParserTaskModel` (parser-produced tasks),
1742/// - `ErrorTaskModel` (retry/threshold-controlled tasks).
1743pub async fn create_unified_task_ingress_chain(
1744    task_manager: Arc<TaskManager>,
1745    queue_manager: Arc<QueueManager>,
1746    event_bus: Option<Arc<EventBus>>,
1747    state: Arc<PipelineContext>,
1748) -> UnifiedTaskIngressChain {
1749    let task_concurrency = state
1750        .config
1751        .read()
1752        .await
1753        .crawler
1754        .task_concurrency
1755        .unwrap_or(1024);
1756    let parser_concurrency = {
1757        let cfg = state.config.read().await;
1758        cfg.crawler
1759            .parser_concurrency
1760            .or(cfg.crawler.task_concurrency)
1761            .unwrap_or(256)
1762    };
1763    let error_task_concurrency = {
1764        let cfg = state.config.read().await;
1765        cfg.crawler
1766            .error_task_concurrency
1767            .or(cfg.crawler.parser_concurrency)
1768            .or(cfg.crawler.task_concurrency)
1769            .unwrap_or(4)
1770    };
1771    let task_publish_concurrency = state
1772        .config
1773        .read()
1774        .await
1775        .crawler
1776        .publish_concurrency
1777        .unwrap_or(1024);
1778    let parser_publish_concurrency = state
1779        .config
1780        .read()
1781        .await
1782        .crawler
1783        .publish_concurrency
1784        .unwrap_or(256);
1785    let error_publish_concurrency = state
1786        .config
1787        .read()
1788        .await
1789        .crawler
1790        .publish_concurrency
1791        .unwrap_or(32);
1792
1793    let task_chain = Arc::new(
1794        EventAwareTypedChain::<TaskEvent, TaskEvent>::new(event_bus.clone())
1795            .then::<Task, _>(TaskModelProcessor {
1796                task_manager: task_manager.clone(),
1797                state: state.clone(),
1798                queue_manager: queue_manager.clone(),
1799                event_bus: event_bus.clone(),
1800                threshold_decision_service: Arc::new(StatusTrackerThresholdDecisionService::new(
1801                    state.clone(),
1802                )),
1803            })
1804            .then::<Vec<Module>, _>(TaskModuleProcessor {
1805                state: state.clone(),
1806            })
1807            .then(VecToStreamProcessor::new())
1808            .then_map_stream_in_with_strategy::<SyncBoxStream<'static, Request>, _>(
1809                TaskProcessor {
1810                    task_manager: task_manager.clone(),
1811                },
1812                task_concurrency,
1813                ErrorStrategy::Skip,
1814            )
1815            .then_one_shot(FlattenStreamProcessor::new())
1816            .then_one_shot(StreamLoggerProcessor::new("AfterFlatten").with_logger(
1817                |req: &Request| {
1818                    info!(
1819                        "[FlattenStreamProcessor] yielding request: request_id={}",
1820                        req.id
1821                    );
1822                },
1823            ))
1824            .then_map_stream_in_with_strategy::<(), _>(
1825                RequestPublish {
1826                    queue_manager: queue_manager.clone(),
1827                    state: state.clone(),
1828                },
1829                task_publish_concurrency,
1830                ErrorStrategy::Skip,
1831            ),
1832    );
1833
1834    let parser_chain = Arc::new(
1835        EventAwareTypedChain::<TaskParserEvent, TaskParserEvent>::new(event_bus.clone())
1836            .then::<Task, _>(TaskModelProcessor {
1837                task_manager: task_manager.clone(),
1838                state: state.clone(),
1839                queue_manager: queue_manager.clone(),
1840                event_bus: event_bus.clone(),
1841                threshold_decision_service: Arc::new(StatusTrackerThresholdDecisionService::new(
1842                    state.clone(),
1843                )),
1844            })
1845            .then::<Vec<Module>, _>(TaskModuleProcessor {
1846                state: state.clone(),
1847            })
1848            .then(VecToStreamProcessor::new())
1849            .then_map_stream_in_with_strategy::<SyncBoxStream<'static, Request>, _>(
1850                TaskProcessor {
1851                    task_manager: task_manager.clone(),
1852                },
1853                parser_concurrency,
1854                ErrorStrategy::Skip,
1855            )
1856            .then_one_shot(FlattenStreamProcessor::new())
1857            .then_map_stream_in_with_strategy::<(), _>(
1858                RequestPublish {
1859                    queue_manager: queue_manager.clone(),
1860                    state: state.clone(),
1861                },
1862                parser_publish_concurrency,
1863                ErrorStrategy::Skip,
1864            ),
1865    );
1866
1867    let error_chain = Arc::new(
1868        EventAwareTypedChain::<TaskErrorEvent, TaskErrorEvent>::new(event_bus)
1869            .then::<Task, _>(TaskModelProcessor {
1870                task_manager: task_manager.clone(),
1871                state: state.clone(),
1872                queue_manager: queue_manager.clone(),
1873                event_bus: None,
1874                threshold_decision_service: Arc::new(StatusTrackerThresholdDecisionService::new(
1875                    state.clone(),
1876                )),
1877            })
1878            .then::<Vec<Module>, _>(TaskModuleProcessor {
1879                state: state.clone(),
1880            })
1881            .then(VecToStreamProcessor::new())
1882            .then_map_stream_in_with_strategy::<SyncBoxStream<'static, Request>, _>(
1883                TaskProcessor {
1884                    task_manager: task_manager.clone(),
1885                },
1886                error_task_concurrency,
1887                ErrorStrategy::Skip,
1888            )
1889            .then_one_shot(FlattenStreamProcessor::new())
1890            .then_map_stream_in_with_strategy::<(), _>(
1891                RequestPublish {
1892                    queue_manager,
1893                    state,
1894                },
1895                error_publish_concurrency,
1896                ErrorStrategy::Skip,
1897            ),
1898    );
1899
1900    UnifiedTaskIngressChain::new(task_chain, parser_chain, error_chain)
1901}