risingwave_meta/manager/iceberg_pk_index_sink/
manager.rs1use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17use std::sync::atomic::{AtomicBool, Ordering};
18
19use anyhow::anyhow;
20use arc_swap::ArcSwap;
21use parking_lot::RwLock;
22use risingwave_common::id::{JobId, PartialGraphId};
23use risingwave_connector::connector_common::{
24 IcebergCommittedSnapshot, IcebergSinkCompactionUpdate,
25};
26use risingwave_connector::sink::catalog::SinkId;
27use risingwave_connector::sink::iceberg::IcebergConfig;
28use tokio::sync::Mutex;
29use tokio::sync::mpsc::UnboundedSender;
30use tracing::warn;
31
32use super::committed_epoch::PartialGraphCommittedEpochs;
33use super::coordinator::IcebergPkIndexSinkCoordinator;
34use super::{IcebergPkIndexPreCommitMetadata, build_iceberg_config, is_iceberg_pk_index_sink};
35use crate::controller::catalog::CatalogControllerRef;
36use crate::manager::iceberg_compaction::IcebergCompactionManagerRef;
37
38type CoordinatorRef = Arc<Mutex<IcebergPkIndexSinkCoordinator>>;
39
40pub(crate) struct IcebergPkIndexSinkRegistration {
42 ready: AtomicBool,
43 config: ArcSwap<IcebergConfig>,
44}
45
46impl std::fmt::Debug for IcebergPkIndexSinkRegistration {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 f.debug_struct("IcebergPkIndexSinkRegistration")
49 .field("ready", &self.is_ready())
50 .finish_non_exhaustive()
51 }
52}
53
54impl IcebergPkIndexSinkRegistration {
55 pub(crate) fn is_ready(&self) -> bool {
56 self.ready.load(Ordering::Acquire)
57 }
58
59 pub(crate) fn config(&self) -> Arc<IcebergConfig> {
60 self.config.load_full()
61 }
62
63 #[cfg(test)]
64 pub(crate) fn for_test(config: IcebergConfig) -> Arc<Self> {
65 Arc::new(Self {
66 ready: AtomicBool::new(true),
67 config: ArcSwap::from_pointee(config),
68 })
69 }
70
71 #[cfg(test)]
72 pub(crate) fn retire_for_test(&self) {
73 self.ready.store(false, Ordering::Release);
74 }
75}
76
77struct RegisteredSink {
78 partial_graph_id: PartialGraphId,
79 registration: Arc<IcebergPkIndexSinkRegistration>,
80 coordinator: Option<CoordinatorRef>,
81}
82
83#[derive(Clone)]
85pub struct IcebergPkIndexSinkManager {
86 inner: Arc<ManagerInner>,
87}
88
89struct ManagerInner {
90 catalog_controller: CatalogControllerRef,
91 compaction_manager: IcebergCompactionManagerRef,
92 coordinators: RwLock<HashMap<SinkId, RegisteredSink>>,
96 committed_epochs: PartialGraphCommittedEpochs,
102 compaction_stat_tx: UnboundedSender<IcebergSinkCompactionUpdate>,
103}
104
105impl IcebergPkIndexSinkManager {
106 pub fn new(
107 catalog_controller: CatalogControllerRef,
108 compaction_manager: IcebergCompactionManagerRef,
109 compaction_stat_tx: UnboundedSender<IcebergSinkCompactionUpdate>,
110 ) -> Self {
111 IcebergPkIndexSinkManager {
112 inner: Arc::new(ManagerInner {
113 catalog_controller,
114 compaction_manager,
115 coordinators: RwLock::new(HashMap::new()),
116 committed_epochs: PartialGraphCommittedEpochs::default(),
117 compaction_stat_tx,
118 }),
119 }
120 }
121
122 pub async fn register_sink(
126 &self,
127 sink_id: SinkId,
128 partial_graph_id: PartialGraphId,
129 ) -> anyhow::Result<()> {
130 self.register_sink_with(
131 sink_id,
132 partial_graph_id,
133 IcebergPkIndexSinkCoordinator::init,
134 )
135 .await
136 }
137
138 async fn register_sink_with<F, Fut>(
139 &self,
140 sink_id: SinkId,
141 partial_graph_id: PartialGraphId,
142 initialize: F,
143 ) -> anyhow::Result<()>
144 where
145 F: FnOnce(SinkId, IcebergConfig, sea_orm::DatabaseConnection) -> Fut + Send,
146 Fut: Future<Output = anyhow::Result<IcebergPkIndexSinkCoordinator>> + Send,
147 {
148 let (registration, config, db) = {
149 let catalog = self.inner.catalog_controller.get_inner_read_guard().await;
152 let sink = catalog
153 .get_sink_by_id(sink_id)
154 .await?
155 .ok_or_else(|| anyhow!("iceberg pk-index sink {} no longer exists", sink_id))?;
156 anyhow::ensure!(
157 is_iceberg_pk_index_sink(&sink.properties),
158 "sink {} is not an iceberg pk-index sink",
159 sink_id
160 );
161 let config = build_iceberg_config(&sink)?;
162 let registration = Arc::new(IcebergPkIndexSinkRegistration {
163 ready: AtomicBool::new(false),
164 config: ArcSwap::from_pointee(config.clone()),
165 });
166 let mut coordinators = self.inner.coordinators.write();
167 Self::remove_sink(&mut coordinators, sink_id, &self.inner.committed_epochs);
168 coordinators.insert(
169 sink_id,
170 RegisteredSink {
171 partial_graph_id,
172 registration: registration.clone(),
173 coordinator: None,
174 },
175 );
176 self.inner
177 .compaction_manager
178 .on_recovery_start(Some(&[sink_id]));
179 self.inner.committed_epochs.ensure(partial_graph_id);
180 (registration, config, catalog.db.clone())
181 };
182 let cleanup = scopeguard::guard(registration, |registration| {
183 let mut coordinators = self.inner.coordinators.write();
184 if coordinators
185 .get(&sink_id)
186 .is_some_and(|sink| Arc::ptr_eq(&sink.registration, ®istration))
187 {
188 Self::remove_sink(&mut coordinators, sink_id, &self.inner.committed_epochs);
189 }
190 });
191 let coordinator = initialize(sink_id, config, db).await?;
192 {
193 let mut coordinators = self.inner.coordinators.write();
194 let sink = coordinators
195 .get_mut(&sink_id)
196 .filter(|sink| Arc::ptr_eq(&sink.registration, &cleanup))
197 .ok_or_else(|| {
198 anyhow!("iceberg pk-index sink {} registration was retired", sink_id)
199 })?;
200 sink.coordinator = Some(Arc::new(Mutex::new(coordinator)));
201 }
202 scopeguard::ScopeGuard::into_inner(cleanup);
203 Ok(())
204 }
205
206 pub(crate) async fn on_jobs_ready(&self, job_ids: &HashSet<JobId>) {
209 let sinks = {
210 let coordinators = self.inner.coordinators.read();
211 coordinators
212 .iter()
213 .filter(|(sink_id, sink)| {
214 job_ids.contains(&sink_id.as_job_id()) && !sink.registration.is_ready()
215 })
216 .filter_map(|(&sink_id, sink)| {
217 sink.coordinator.as_ref().map(|coordinator| {
218 (sink_id, sink.registration.clone(), coordinator.clone())
219 })
220 })
221 .collect::<Vec<_>>()
222 };
223 for (sink_id, registration, coordinator) in sinks {
224 let coordinator = coordinator.lock().await;
227 let coordinators = self.inner.coordinators.write();
228 if !coordinators
229 .get(&sink_id)
230 .is_some_and(|sink| Arc::ptr_eq(&sink.registration, ®istration))
231 || registration.ready.swap(true, Ordering::AcqRel)
232 {
233 continue;
234 }
235 self.inner.compaction_manager.on_sink_ready(
236 sink_id,
237 registration,
238 coordinator.latest_observed_snapshot(),
239 );
240 }
241 }
242
243 pub(crate) fn on_sink_config_changed(&self, sink_id: SinkId, config: IcebergConfig) {
244 let mut coordinators = self.inner.coordinators.write();
245 if let Some(sink) = coordinators.get_mut(&sink_id) {
246 sink.registration.config.store(Arc::new(config));
247 if sink.registration.is_ready() {
248 self.inner
249 .compaction_manager
250 .on_sink_config_changed(sink_id, (*sink.registration.config()).clone());
251 }
252 }
253 }
254
255 pub(crate) async fn pre_commit(
258 &self,
259 input: IcebergPkIndexPreCommitMetadata,
260 ) -> anyhow::Result<()> {
261 let coordinator = self.coordinator(input.sink_id)?;
262 coordinator
263 .lock()
264 .await
265 .pre_commit(input.prev_epoch, input.reports, input.compaction)
266 .await
267 }
268
269 pub async fn commit_epoch(&self, sink_id: SinkId) -> anyhow::Result<()> {
272 let coordinator = self.coordinator(sink_id)?;
273 let mut coordinator_guard = coordinator.lock().await;
275 let result = coordinator_guard.commit().await?;
276 if let Some((observed_snapshot, has_new_data)) = result {
277 let coordinators = self.inner.coordinators.read();
278 if let Some(sink) = coordinators.get(&sink_id)
279 && sink
280 .coordinator
281 .as_ref()
282 .is_some_and(|current| Arc::ptr_eq(current, &coordinator))
283 && sink.registration.is_ready()
284 {
285 self.notify_compaction_scheduler(sink_id, Some(observed_snapshot), has_new_data);
286 }
287 }
288 drop(coordinator_guard);
289 Ok(())
290 }
291
292 fn notify_compaction_scheduler(
293 &self,
294 sink_id: SinkId,
295 observed_snapshot: Option<IcebergCommittedSnapshot>,
296 has_new_data: bool,
297 ) {
298 let Some(observed_snapshot) = observed_snapshot else {
299 return;
300 };
301 if self
302 .inner
303 .compaction_stat_tx
304 .send(IcebergSinkCompactionUpdate {
305 sink_id,
306 force_compaction: false,
307 has_new_data,
308 observed_snapshot,
309 })
310 .is_err()
311 {
312 warn!(%sink_id, "failed to notify iceberg compaction scheduler");
313 }
314 }
315
316 pub fn advance_committed_epochs(
318 &self,
319 epochs: impl IntoIterator<Item = (PartialGraphId, u64)>,
320 ) {
321 self.inner.committed_epochs.advance_all(epochs);
322 }
323
324 pub async fn wait_epoch(
328 &self,
329 sink_id: SinkId,
330 target_epoch: u64,
331 ) -> anyhow::Result<Option<i64>> {
332 let partial_graph_id = self.partial_graph_of(sink_id)?;
333 self.inner
334 .committed_epochs
335 .wait(partial_graph_id, target_epoch)
336 .await;
337 let coordinator = self.coordinator(sink_id)?;
338 let snapshot_id = coordinator.lock().await.current_snapshot_id();
339 Ok(snapshot_id)
340 }
341
342 pub fn retire_sinks(&self, sink_ids: impl IntoIterator<Item = SinkId>) {
345 let mut coordinators = self.inner.coordinators.write();
346 for sink_id in sink_ids {
347 Self::remove_sink(&mut coordinators, sink_id, &self.inner.committed_epochs);
348 self.inner
349 .compaction_manager
350 .clear_iceberg_maintenance_by_sink_id(sink_id);
351 }
352 }
353
354 pub(crate) fn start_recovery(&self, job_ids: Option<&HashSet<JobId>>) {
355 let mut coordinators = self.inner.coordinators.write();
356 if let Some(job_ids) = job_ids {
357 for job_id in job_ids {
358 Self::remove_sink(
359 &mut coordinators,
360 job_id.as_sink_id(),
361 &self.inner.committed_epochs,
362 );
363 }
364 } else {
365 for sink in coordinators.values() {
366 sink.registration.ready.store(false, Ordering::Release);
367 }
368 coordinators.clear();
369 self.inner.committed_epochs.clear();
370 }
371 let scope = job_ids.map(|ids| ids.iter().map(|id| id.as_sink_id()).collect::<Vec<_>>());
372 self.inner
373 .compaction_manager
374 .on_recovery_start(scope.as_deref());
375 }
376
377 fn remove_sink(
378 coordinators: &mut HashMap<SinkId, RegisteredSink>,
379 sink_id: SinkId,
380 committed_epochs: &PartialGraphCommittedEpochs,
381 ) {
382 if let Some(sink) = coordinators.remove(&sink_id) {
383 sink.registration.ready.store(false, Ordering::Release);
384 if !coordinators
385 .values()
386 .any(|other| other.partial_graph_id == sink.partial_graph_id)
387 {
388 committed_epochs.remove(sink.partial_graph_id);
389 }
390 }
391 }
392
393 fn coordinator(&self, sink_id: SinkId) -> anyhow::Result<CoordinatorRef> {
394 self.inner
395 .coordinators
396 .read()
397 .get(&sink_id)
398 .and_then(|sink| sink.coordinator.clone())
399 .ok_or_else(|| {
400 anyhow!(
401 "iceberg pk-index sink coordinator for sink {} is not registered",
402 sink_id
403 )
404 })
405 }
406
407 fn partial_graph_of(&self, sink_id: SinkId) -> anyhow::Result<PartialGraphId> {
408 self.inner
409 .coordinators
410 .read()
411 .get(&sink_id)
412 .map(|sink| sink.partial_graph_id)
413 .ok_or_else(|| {
414 anyhow!(
415 "iceberg pk-index sink coordinator for sink {} is not registered",
416 sink_id
417 )
418 })
419 }
420}
421
422#[cfg(test)]
423#[path = "manager_test.rs"]
424mod tests;