Skip to main content

risingwave_meta/stream/source_manager/
worker.rs

1// Copyright 2025 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::future::Future;
16use std::time::Instant;
17
18use anyhow::Context;
19use await_tree::InstrumentAwait;
20use parking_lot::Mutex;
21use risingwave_connector::WithPropertiesExt;
22#[cfg(not(debug_assertions))]
23use risingwave_connector::error::ConnectorError;
24use risingwave_connector::source::AnySplitEnumerator;
25use risingwave_connector::source::base::ConnectorProperties;
26
27use super::*;
28
29const MAX_FAIL_CNT: u32 = 10;
30
31// The key used to load `SplitImpl` directly from source properties.
32// When this key is present, the enumerator will only return the given ones
33// instead of fetching them from the external source.
34// Only valid in debug builds - will return an error in release builds.
35const DEBUG_SPLITS_KEY: &str = "debug_splits";
36
37/// Latest splits discovered by a source worker, shared between the worker and its handle.
38/// The internal lock never escapes, so no holder can keep it across an await point.
39#[derive(Clone, Default)]
40pub struct SharedSplits(Arc<Mutex<Option<BTreeMap<SplitId, SplitImpl>>>>);
41
42impl SharedSplits {
43    fn publish(&self, splits: impl IntoIterator<Item = SplitImpl>) {
44        *self.0.lock() = Some(
45            splits
46                .into_iter()
47                .map(|split| (split.id(), split))
48                .collect(),
49        );
50    }
51
52    fn snapshot(&self) -> Option<BTreeMap<SplitId, SplitImpl>> {
53        self.0.lock().clone()
54    }
55
56    pub(super) fn take(&self) -> Option<BTreeMap<SplitId, SplitImpl>> {
57        self.0.lock().take()
58    }
59
60    pub(super) fn is_unset(&self) -> bool {
61        self.0.lock().is_none()
62    }
63}
64
65/// `ConnectorSourceWorker` keeps fetching the latest split metadata from the external source service ([`Self::tick`]),
66/// and maintains it in `current_splits`.
67pub struct ConnectorSourceWorker {
68    source_id: SourceId,
69    source_name: String,
70    current_splits: SharedSplits,
71    // XXX: box or arc?
72    enumerator: Box<dyn AnySplitEnumerator>,
73    period: Duration,
74    metrics: Arc<MetaMetrics>,
75    connector_properties: ConnectorProperties,
76    fail_cnt: u32,
77    // Held for the worker's lifetime: dropping a guarded handle resets its series on the next scrape.
78    source_is_up: LabelGuardedIntGauge,
79    tick_duration: LabelGuardedHistogram,
80    monitor_error_count: LabelGuardedIntCounter,
81
82    debug_splits: Option<Vec<SplitImpl>>,
83}
84
85fn extract_prop_from_existing_source(source: &Source) -> ConnectorResult<ConnectorProperties> {
86    let options_with_secret =
87        WithOptionsSecResolved::new(source.with_properties.clone(), source.secret_refs.clone());
88    let mut properties = ConnectorProperties::extract(options_with_secret, false)?;
89    properties.init_from_pb_source(source);
90    Ok(properties)
91}
92fn extract_prop_from_new_source(source: &Source) -> ConnectorResult<ConnectorProperties> {
93    let options_with_secret = WithOptionsSecResolved::new(
94        {
95            let mut with_properties = source.with_properties.clone();
96            let _removed = with_properties.remove(DEBUG_SPLITS_KEY);
97
98            #[cfg(not(debug_assertions))]
99            {
100                if _removed.is_some() {
101                    return Err(ConnectorError::from(anyhow::anyhow!(
102                        "`debug_splits` is not allowed in release mode"
103                    )));
104                }
105            }
106
107            with_properties
108        },
109        source.secret_refs.clone(),
110    );
111    let mut properties = ConnectorProperties::extract(options_with_secret, true)?;
112    properties.init_from_pb_source(source);
113    Ok(properties)
114}
115
116/// Used to create a new [`ConnectorSourceWorkerHandle`] for a new source.
117///
118/// It will call [`ConnectorSourceWorker::tick()`] to fetch split metadata once before returning.
119pub async fn create_source_worker(
120    source: &Source,
121    metrics: Arc<MetaMetrics>,
122    await_tree_reg: await_tree::Registry,
123) -> MetaResult<ConnectorSourceWorkerHandle> {
124    tracing::info!("spawning new watcher for source {}", source.id);
125
126    let splits = SharedSplits::default();
127    let current_splits_ref = splits.clone();
128
129    let connector_properties = extract_prop_from_new_source(source)?;
130    let enable_scale_in = connector_properties.enable_drop_split();
131    let enable_adaptive_splits = connector_properties.enable_adaptive_splits();
132    let (command_tx, command_rx) = tokio::sync::mpsc::unbounded_channel();
133    let sync_call_timeout = source
134        .with_properties
135        .get_sync_call_timeout()
136        .unwrap_or(DEFAULT_SOURCE_TICK_TIMEOUT);
137    let handle = {
138        let mut worker = ConnectorSourceWorker::create(
139            source,
140            connector_properties,
141            DEFAULT_SOURCE_WORKER_TICK_INTERVAL,
142            current_splits_ref.clone(),
143            metrics,
144        )
145        .await?;
146
147        // if fail to fetch meta info, will refuse to create source
148        tokio::time::timeout(sync_call_timeout, worker.tick())
149            .await
150            .with_context(|| {
151                format!(
152                    "failed to fetch meta info for source {}, timeout {:?}",
153                    source.id, DEFAULT_SOURCE_TICK_TIMEOUT
154                )
155            })??;
156
157        let root = format!(
158            "ConnectorSourceWorker(source_id={}, name={})",
159            source.id, source.name
160        );
161        tokio::spawn(
162            await_tree_reg
163                .register_derived_root(root)
164                .instrument(async move { worker.run(command_rx).await }),
165        )
166    };
167    Ok(ConnectorSourceWorkerHandle {
168        handle,
169        command_tx,
170        splits,
171        enable_drop_split: enable_scale_in,
172        enable_adaptive_splits,
173    })
174}
175
176/// Used on startup ([`SourceManager::new`]). Failed sources will not block meta startup.
177pub fn create_source_worker_async(
178    source: Source,
179    managed_sources: &mut HashMap<SourceId, ConnectorSourceWorkerHandle>,
180    metrics: Arc<MetaMetrics>,
181    await_tree_reg: await_tree::Registry,
182) -> MetaResult<()> {
183    tracing::info!("spawning new watcher for source {}", source.id);
184
185    let splits = SharedSplits::default();
186    let current_splits_ref = splits.clone();
187    let source_id = source.id;
188    let source_name = source.name.clone();
189
190    let connector_properties = extract_prop_from_existing_source(&source)?;
191
192    let enable_drop_split = connector_properties.enable_drop_split();
193    let enable_adaptive_splits = connector_properties.enable_adaptive_splits();
194    let (command_tx, command_rx) = tokio::sync::mpsc::unbounded_channel();
195    let run_fut = async move {
196        let mut ticker = time::interval(DEFAULT_SOURCE_WORKER_TICK_INTERVAL);
197        ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
198
199        let mut worker = loop {
200            ticker.tick().await;
201
202            match ConnectorSourceWorker::create(
203                &source,
204                connector_properties.clone(),
205                DEFAULT_SOURCE_WORKER_TICK_INTERVAL,
206                current_splits_ref.clone(),
207                metrics.clone(),
208            )
209            .await
210            {
211                Ok(worker) => {
212                    break worker;
213                }
214                Err(e) => {
215                    tracing::warn!(error = %e.as_report(), "failed to create source worker");
216                }
217            }
218        };
219
220        worker.run(command_rx).await
221    };
222    let root = format!("ConnectorSourceWorker(source_id={source_id}, name={source_name})");
223    let handle = tokio::spawn(
224        await_tree_reg
225            .register_derived_root(root)
226            .instrument(run_fut),
227    );
228
229    managed_sources.insert(
230        source_id,
231        ConnectorSourceWorkerHandle {
232            handle,
233            command_tx,
234            splits,
235            enable_drop_split,
236            enable_adaptive_splits,
237        },
238    );
239    Ok(())
240}
241
242const DEFAULT_SOURCE_WORKER_TICK_INTERVAL: Duration = Duration::from_secs(30);
243
244/// Interval at which a still-running (in-flight) source worker tick emits a progressive warning.
245const TICK_INFLIGHT_WARN_INTERVAL: Duration = Duration::from_secs(30);
246
247/// Drive a source worker `tick` future to completion, recording its duration in `tick_duration`
248/// and emitting a `warn!` every [`TICK_INFLIGHT_WARN_INTERVAL`] while it is still pending.
249/// The future is never cancelled or timed out, so `tick()` semantics are unchanged.
250async fn run_tick_with_progressive_warn<F>(
251    source_id: SourceId,
252    source_name: String,
253    tick_duration: LabelGuardedHistogram,
254    fut: F,
255) -> F::Output
256where
257    F: Future,
258{
259    let start = Instant::now();
260    let mut fut = std::pin::pin!(fut);
261    let mut warn_interval = time::interval(TICK_INFLIGHT_WARN_INTERVAL);
262    // Consume the immediate first tick so the first warning fires only after one full interval.
263    warn_interval.tick().await;
264    loop {
265        select! {
266            biased;
267            output = &mut fut => {
268                tick_duration.observe(start.elapsed().as_secs_f64());
269                return output;
270            }
271            _ = warn_interval.tick() => {
272                let elapsed_secs = start.elapsed().as_secs();
273                tracing::warn!(
274                    source_id = %source_id,
275                    source_name = %source_name,
276                    "source worker tick has been running for {elapsed_secs}s; \
277                     source split state and SourceManager may be blocked",
278                );
279            }
280        }
281    }
282}
283
284impl ConnectorSourceWorker {
285    /// Recreate the `SplitEnumerator` to establish a new connection to the external source service.
286    async fn refresh(&mut self) -> MetaResult<()> {
287        let enumerator = self
288            .connector_properties
289            .clone()
290            .create_split_enumerator(Arc::new(SourceEnumeratorContext {
291                metrics: self.metrics.source_enumerator_metrics.clone(),
292                info: SourceEnumeratorInfo {
293                    source_id: self.source_id,
294                },
295            }))
296            .await
297            .context("failed to create SplitEnumerator")?;
298        self.enumerator = enumerator;
299        self.fail_cnt = 0;
300        tracing::info!("refreshed source enumerator: {}", self.source_name);
301        Ok(())
302    }
303
304    /// On creation, connection to the external source service will be established, but `splits`
305    /// will not be updated until `tick` is called.
306    pub async fn create(
307        source: &Source,
308        connector_properties: ConnectorProperties,
309        period: Duration,
310        splits: SharedSplits,
311        metrics: Arc<MetaMetrics>,
312    ) -> MetaResult<Self> {
313        let enumerator = connector_properties
314            .clone()
315            .create_split_enumerator(Arc::new(SourceEnumeratorContext {
316                metrics: metrics.source_enumerator_metrics.clone(),
317                info: SourceEnumeratorInfo {
318                    source_id: source.id,
319                },
320            }))
321            .await
322            .context("failed to create SplitEnumerator")?;
323
324        let source_id_str = source.id.to_string();
325        let metric_labels = [source_id_str.as_str(), source.name.as_str()];
326        let source_is_up = metrics
327            .source_is_up
328            .with_guarded_label_values(&metric_labels);
329        let tick_duration = metrics
330            .source_worker_tick_duration_seconds
331            .with_guarded_label_values(&metric_labels);
332        let monitor_error_count = metrics
333            .source_enumerator_monitor_error_count
334            .with_guarded_label_values(&metric_labels);
335
336        Ok(Self {
337            source_id: source.id,
338            source_name: source.name.clone(),
339            current_splits: splits,
340            enumerator,
341            period,
342            metrics,
343            connector_properties,
344            fail_cnt: 0,
345            source_is_up,
346            tick_duration,
347            monitor_error_count,
348            debug_splits: {
349                let debug_splits = source.with_properties.get(DEBUG_SPLITS_KEY);
350                #[cfg(not(debug_assertions))]
351                {
352                    if debug_splits.is_some() {
353                        return Err(ConnectorError::from(anyhow::anyhow!(
354                            "`debug_splits` is not allowed in release mode"
355                        ))
356                        .into());
357                    }
358                    None
359                }
360
361                #[cfg(debug_assertions)]
362                {
363                    use risingwave_common::types::JsonbVal;
364                    if let Some(debug_splits) = debug_splits {
365                        let mut splits = Vec::new();
366                        let debug_splits_value =
367                            jsonbb::serde_json::from_str::<serde_json::Value>(debug_splits)
368                                .context("failed to parse split impl")?;
369                        for split_impl_value in debug_splits_value.as_array().unwrap() {
370                            splits.push(SplitImpl::restore_from_json(JsonbVal::from(
371                                split_impl_value.clone(),
372                            ))?);
373                        }
374                        Some(splits)
375                    } else {
376                        None
377                    }
378                }
379            },
380        })
381    }
382
383    pub async fn run(&mut self, mut command_rx: UnboundedReceiver<SourceWorkerCommand>) {
384        let mut interval = time::interval(self.period);
385        interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
386        loop {
387            select! {
388                biased;
389                cmd = command_rx.borrow_mut().recv() => {
390                    if let Some(cmd) = cmd {
391                        match cmd {
392                            SourceWorkerCommand::Tick(tx) => {
393                                let _ = tx.send(self.run_tick().await);
394                            }
395                            SourceWorkerCommand::DropFragments(fragment_ids) => {
396                                if let Err(e) = self.drop_fragments(fragment_ids).await {
397                                    // when error happens, we just log it and ignore
398                                    tracing::warn!(error = %e.as_report(), "error happened when drop fragment");
399                                }
400                            }
401                            SourceWorkerCommand::FinishBackfill(fragment_ids) => {
402                                if let Err(e) = self.finish_backfill(fragment_ids).await {
403                                    // when error happens, we just log it and ignore
404                                    tracing::warn!(error = %e.as_report(), "error happened when finish backfill");
405                                }
406                            }
407                            SourceWorkerCommand::UpdateProps(new_props) => {
408                                self.connector_properties = new_props;
409                                if let Err(e) = self.refresh().await {
410                                    tracing::error!(error = %e.as_report(), "error happened when refresh from connector source worker");
411                                }
412                                tracing::debug!("source {} worker properties updated", self.source_name);
413                            }
414                            SourceWorkerCommand::Terminate => {
415                                return;
416                            }
417                        }
418                    }
419                }
420                _ = interval.tick() => {
421                    if self.fail_cnt > MAX_FAIL_CNT
422                        && let Err(e) = self.refresh().await {
423                            tracing::error!(error = %e.as_report(), "error happened when refresh from connector source worker");
424                        }
425                    if let Err(e) = self.run_tick().await {
426                        tracing::error!(error = %e.as_report(), "error happened when tick from connector source worker");
427                    }
428                }
429            }
430        }
431    }
432
433    /// Run one [`Self::tick`] with await-tree instrumentation, duration metric, and in-flight warnings.
434    async fn run_tick(&mut self) -> MetaResult<()> {
435        // Cloning the guarded histogram keeps its label guard alive.
436        let source_id = self.source_id;
437        let source_name = self.source_name.clone();
438        let tick_duration = self.tick_duration.clone();
439        let tick_fut = self.tick().instrument_await("tick");
440        run_tick_with_progressive_warn(source_id, source_name, tick_duration, tick_fut).await
441    }
442
443    /// Uses [`risingwave_connector::source::SplitEnumerator`] to fetch the latest split metadata from the external source service.
444    async fn tick(&mut self) -> MetaResult<()> {
445        let source_is_up = |res: i64| {
446            self.source_is_up.set(res);
447        };
448
449        let splits = {
450            if let Some(debug_splits) = &self.debug_splits {
451                debug_splits.clone()
452            } else {
453                self.enumerator
454                    .list_splits()
455                    .instrument_await("list_splits")
456                    .await
457                    .inspect_err(|_| {
458                        source_is_up(0);
459                        self.fail_cnt += 1;
460                    })?
461            }
462        };
463
464        self.fail_cnt = 0;
465        self.current_splits.publish(splits);
466
467        // Call enumerator's `on_tick` method for monitoring tasks. `on_tick` may do unbounded
468        // network I/O, so it must not run under `current_splits` (#26275). For CDC, `list_splits`
469        // never fails, so `on_tick` is the sole driver of `source_is_up` (Ok -> 1, Err -> 0).
470        match self.enumerator.on_tick().instrument_await("on_tick").await {
471            Ok(()) => source_is_up(1),
472            Err(e) => {
473                tracing::error!(
474                    "Failed to execute enumerator `on_tick` for source {}: {}",
475                    self.source_id,
476                    e.as_report()
477                );
478                source_is_up(0);
479                self.monitor_error_count.inc();
480            }
481        }
482
483        Ok(())
484    }
485
486    async fn drop_fragments(&mut self, fragment_ids: Vec<FragmentId>) -> MetaResult<()> {
487        self.enumerator.on_drop_fragments(fragment_ids).await?;
488        Ok(())
489    }
490
491    async fn finish_backfill(&mut self, fragment_ids: Vec<FragmentId>) -> MetaResult<()> {
492        self.enumerator.on_finish_backfill(fragment_ids).await?;
493        Ok(())
494    }
495}
496
497/// Handle for a running [`ConnectorSourceWorker`].
498pub struct ConnectorSourceWorkerHandle {
499    #[expect(dead_code)]
500    handle: JoinHandle<()>,
501    command_tx: UnboundedSender<SourceWorkerCommand>,
502    pub splits: SharedSplits,
503    pub enable_drop_split: bool,
504    pub enable_adaptive_splits: bool,
505}
506
507impl ConnectorSourceWorkerHandle {
508    pub fn get_enable_adaptive_splits(&self) -> bool {
509        self.enable_adaptive_splits
510    }
511
512    pub fn discovered_splits(&self, source_id: SourceId) -> MetaResult<Option<DiscoveredSplits>> {
513        let Some(discovered_splits) = self.splits.snapshot() else {
514            tracing::info!(
515                "The discover loop for source {} is not ready yet; we'll wait for the next run",
516                source_id
517            );
518            return Ok(None);
519        };
520        if discovered_splits.is_empty() {
521            tracing::warn!("Empty splits discovered for source {}", source_id);
522        }
523
524        if self.enable_adaptive_splits {
525            debug_assert!(self.enable_drop_split);
526            debug_assert!(discovered_splits.len() == 1);
527            let template = discovered_splits.into_values().next().unwrap();
528            Ok(Some(DiscoveredSplits::Adaptive(template)))
529        } else {
530            Ok(Some(DiscoveredSplits::Fixed(discovered_splits)))
531        }
532    }
533
534    fn send_command(&self, command: SourceWorkerCommand) -> MetaResult<()> {
535        let cmd_str = format!("{:?}", command);
536        self.command_tx
537            .send(command)
538            .with_context(|| format!("failed to send {cmd_str} command to source worker"))?;
539        Ok(())
540    }
541
542    /// Force [`ConnectorSourceWorker::tick()`] to be called.
543    pub async fn force_tick(&self) -> MetaResult<()> {
544        let (tx, rx) = oneshot::channel();
545        self.send_command(SourceWorkerCommand::Tick(tx))?;
546        rx.await
547            .context("failed to receive tick command response from source worker")?
548            .context("source worker tick failed")?;
549        Ok(())
550    }
551
552    pub fn drop_fragments(&self, fragment_ids: Vec<FragmentId>) {
553        tracing::debug!("drop_fragments: {:?}", fragment_ids);
554        if let Err(e) = self.send_command(SourceWorkerCommand::DropFragments(fragment_ids)) {
555            // ignore drop fragment error, just log it
556            tracing::warn!(error = %e.as_report(), "failed to drop fragments");
557        }
558    }
559
560    pub fn finish_backfill(&self, fragment_ids: Vec<FragmentId>) {
561        tracing::debug!("finish_backfill: {:?}", fragment_ids);
562        if let Err(e) = self.send_command(SourceWorkerCommand::FinishBackfill(fragment_ids)) {
563            // ignore error, just log it
564            tracing::warn!(error = %e.as_report(), "failed to finish backfill");
565        }
566    }
567
568    pub fn update_props(&self, new_props: ConnectorProperties) {
569        if let Err(e) = self.send_command(SourceWorkerCommand::UpdateProps(new_props)) {
570            // ignore update props error, just log it
571            tracing::warn!(error = %e.as_report(), "failed to update source worker properties");
572        }
573    }
574
575    pub fn terminate(&self, dropped_fragments: Option<BTreeSet<FragmentId>>) {
576        tracing::debug!("terminate: {:?}", dropped_fragments);
577        if let Some(dropped_fragments) = dropped_fragments {
578            self.drop_fragments(dropped_fragments.into_iter().collect());
579        }
580        if let Err(e) = self.send_command(SourceWorkerCommand::Terminate) {
581            // ignore terminate error, just log it
582            tracing::warn!(error = %e.as_report(), "failed to terminate source worker");
583        }
584    }
585}
586
587#[derive(educe::Educe)]
588#[educe(Debug)]
589pub enum SourceWorkerCommand {
590    /// Sync command to force [`ConnectorSourceWorker::tick()`] to be called.
591    Tick(#[educe(Debug(ignore))] oneshot::Sender<MetaResult<()>>),
592    /// Async command to drop a fragment.
593    DropFragments(Vec<FragmentId>),
594    /// Async command to finish backfill.
595    FinishBackfill(Vec<FragmentId>),
596    /// Terminate the worker task.
597    Terminate,
598    /// Update the properties of the source worker.
599    UpdateProps(ConnectorProperties),
600}