risingwave_meta/stream/source_manager/
worker.rs1use 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
31const DEBUG_SPLITS_KEY: &str = "debug_splits";
36
37#[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
65pub struct ConnectorSourceWorker {
68 source_id: SourceId,
69 source_name: String,
70 current_splits: SharedSplits,
71 enumerator: Box<dyn AnySplitEnumerator>,
73 period: Duration,
74 metrics: Arc<MetaMetrics>,
75 connector_properties: ConnectorProperties,
76 fail_cnt: u32,
77 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
116pub 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 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
176pub 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
244const TICK_INFLIGHT_WARN_INTERVAL: Duration = Duration::from_secs(30);
246
247async 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 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 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 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 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 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 async fn run_tick(&mut self) -> MetaResult<()> {
435 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 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 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
497pub 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 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 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 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 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 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 Tick(#[educe(Debug(ignore))] oneshot::Sender<MetaResult<()>>),
592 DropFragments(Vec<FragmentId>),
594 FinishBackfill(Vec<FragmentId>),
596 Terminate,
598 UpdateProps(ConnectorProperties),
600}