indexflow-core 0.1.0

Open Core Search Index Infrastructure
use crate::application::{PipelineManager, SubmissionService};
use crate::config::AppConfig;
use crate::domain::{PipelineStage, ProviderKind};
use crate::infrastructure::{SiteRepo, SubmissionLogRepo, UrlRepo};
use std::sync::Arc;
use std::time::Duration;
use tracing::{error, info};

#[derive(Clone)]
pub struct BingSubmitWorker {
    urls: UrlRepo,
    sites: SiteRepo,
    logs: SubmissionLogRepo,
    submission: SubmissionService,
    pipeline: PipelineManager,
    config: AppConfig,
}

impl BingSubmitWorker {
    pub fn new(
        urls: UrlRepo,
        sites: SiteRepo,
        logs: SubmissionLogRepo,
        submission: SubmissionService,
        pipeline: PipelineManager,
        config: AppConfig,
    ) -> Self {
        Self {
            urls,
            sites,
            logs,
            submission,
            pipeline,
            config,
        }
    }

    pub fn start(self: Arc<Self>) {
        let interval = Duration::from_secs(self.config.submit_worker_interval_secs);
        tokio::spawn(async move {
            info!("Bing Submit Worker 待机就绪");
            loop {
                if let Err(e) = self.tick().await {
                    error!(error = %e, "Bing submit worker tick failed");
                }
                tokio::time::sleep(interval).await;
            }
        });
    }

    async fn tick(&self) -> anyhow::Result<()> {
        let running_sites = self
            .pipeline
            .running_sites_for_stage(PipelineStage::PushSubmit);
        if running_sites.is_empty() {
            return Ok(());
        }

        for site_id in running_sites {
            if !self.pipeline.is_running(site_id, PipelineStage::PushSubmit) {
                continue;
            }

            let pending = self
                .urls
                .fetch_pending_bing(site_id, self.config.submit_worker_batch)
                .await?;
            if pending.is_empty() {
                let google_left = self.urls.fetch_pending_google(site_id, 1).await?;
                if google_left.is_empty() {
                    self.pipeline.stop(site_id, PipelineStage::PushSubmit);
                    info!(
                        site_id,
                        "🎉 该站点全引擎提交队列已全部处理完毕,Worker 回到待机"
                    );
                }
                continue;
            }

            for url in pending {
                if !self.pipeline.is_running(site_id, PipelineStage::PushSubmit) {
                    break;
                }
                let site = match self.sites.find_by_id(url.site_id).await {
                    Ok(Some(site)) => site,
                    Ok(None) => {
                        let message = "Bing submit failed: site not found";
                        error!(site_id, url_id = url.id, url = %url.url, "{message}");
                        if let Err(e) = self
                            .urls
                            .apply_submit_outcome(url.id, Some("FAILED"), Some(message), None, None)
                            .await
                        {
                            error!(error = %e, site_id, url_id = url.id, url = %url.url, "Bing failed outcome persistence failed");
                        }
                        continue;
                    }
                    Err(e) => {
                        let message = format!("Bing submit failed: failed to load site: {e}");
                        error!(error = %e, site_id, url_id = url.id, url = %url.url, "Bing site lookup failed");
                        if let Err(persist_error) = self
                            .urls
                            .apply_submit_outcome(
                                url.id,
                                Some("FAILED"),
                                Some(&message),
                                None,
                                None,
                            )
                            .await
                        {
                            error!(error = %persist_error, site_id, url_id = url.id, url = %url.url, "Bing failed outcome persistence failed");
                        }
                        continue;
                    }
                };

                if !site.bing_ready() {
                    info!(site_id, url_id = url.id, url = %url.url, "Bing submit skipped: Bing credentials are not configured");
                    if let Err(e) = self
                        .urls
                        .apply_submit_outcome(
                            url.id,
                            Some("FAILED"),
                            Some("Bing credentials are not configured"),
                            None,
                            None,
                        )
                        .await
                    {
                        error!(error = %e, site_id, url_id = url.id, "Bing missing credentials failure persistence failed");
                    }
                    continue;
                }

                let key = site.bing_indexnow_key.as_deref().unwrap_or("");
                let results = match self
                    .submission
                    .submit_url_batch_bing(&site.domain, key, &[url.url.clone()])
                    .await
                {
                    Ok(results) => results,
                    Err(e) => {
                        error!(error = %e, site_id, url_id = url.id, url = %url.url, "Bing submission request failed");
                        let message = e.to_string();
                        if let Err(persist_error) = self
                            .urls
                            .apply_submit_outcome(
                                url.id,
                                Some("FAILED"),
                                Some(&message),
                                None,
                                None,
                            )
                            .await
                        {
                            error!(error = %persist_error, site_id, url_id = url.id, url = %url.url, "Bing failed outcome persistence failed");
                        }
                        continue;
                    }
                };

                let Some(res) = results.first() else {
                    let message = "Bing submission returned an empty result";
                    error!(site_id, url_id = url.id, url = %url.url, "{message}");
                    if let Err(e) = self
                        .urls
                        .apply_submit_outcome(url.id, Some("FAILED"), Some(message), None, None)
                        .await
                    {
                        error!(error = %e, site_id, url_id = url.id, url = %url.url, "Bing failed outcome persistence failed");
                    }
                    continue;
                };

                if let Err(e) = self
                    .logs
                    .insert(
                        url.id,
                        ProviderKind::Bing,
                        res.is_success,
                        res.status_code.map(|c| c as i32),
                        res.response_msg.as_deref(),
                    )
                    .await
                {
                    error!(error = %e, site_id, url_id = url.id, url = %url.url, "Bing submission log persistence failed");
                }

                let st = if res.is_success {
                    "SUBMITTED"
                } else {
                    "FAILED"
                };
                if let Err(e) = self
                    .urls
                    .apply_submit_outcome(url.id, Some(st), res.response_msg.as_deref(), None, None)
                    .await
                {
                    error!(error = %e, site_id, url_id = url.id, url = %url.url, "Bing URL outcome persistence failed");
                }

                if res.is_quota_exceeded {
                    info!(
                        site_id,
                        url_id = url.id,
                        "Bing quota exceeded; stopping this site's remaining submissions"
                    );
                    break;
                }
            }
        }
        Ok(())
    }
}