Skip to main content

risingwave_meta/manager/sink_coordination/
manager.rs

1// Copyright 2023 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::collections::{HashMap, HashSet};
16use std::pin::pin;
17use std::sync::Arc;
18
19use anyhow::anyhow;
20use futures::future::{BoxFuture, Either, select};
21use futures::stream::FuturesUnordered;
22use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
23use risingwave_common::bitmap::Bitmap;
24use risingwave_common::id::{DatabaseId, JobId};
25use risingwave_connector::connector_common::IcebergSinkCompactionUpdate;
26use risingwave_connector::sink::catalog::SinkId;
27use risingwave_connector::sink::{SinkCommittedEpochSubscriber, SinkError, SinkParam};
28use risingwave_pb::connector_service::coordinate_request::Msg;
29use risingwave_pb::connector_service::{CoordinateRequest, CoordinateResponse, coordinate_request};
30use rw_futures_util::pending_on_none;
31use sea_orm::DatabaseConnection;
32use thiserror_ext::AsReport;
33use tokio::sync::mpsc;
34use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
35use tokio::sync::oneshot::{Receiver, Sender, channel};
36use tokio::task::{JoinError, JoinHandle};
37use tokio_stream::wrappers::UnboundedReceiverStream;
38use tonic::Status;
39use tracing::{error, info, warn};
40
41use crate::hummock::HummockManagerRef;
42use crate::manager::MetadataManager;
43use crate::manager::sink_coordination::SinkWriterRequestStream;
44use crate::manager::sink_coordination::coordinator_worker::CoordinatorWorker;
45use crate::manager::sink_coordination::handle::SinkWriterCoordinationHandle;
46use crate::notification::{
47    CollectionNotifier, Notifier, NotifierStarter, StartReceiver, wait_collection,
48};
49
50macro_rules! send_with_err_check {
51    ($tx:expr, $msg:expr) => {
52        if $tx.send($msg).is_err() {
53            error!("unable to send msg");
54        }
55    };
56}
57
58const BOUNDED_CHANNEL_SIZE: usize = 16;
59
60enum ManagerRequest {
61    NewSinkWriter(SinkWriterCoordinationHandle),
62    StopCoordinator {
63        notifier: Notifier,
64        /// Streaming jobs whose sink coordinators should be stopped.
65        job_ids: Vec<JobId>,
66    },
67    RecoveryStart {
68        recovery: RecoveryStart,
69        notifier: Notifier,
70    },
71    RecoverySucceeded {
72        recovery: RecoverySucceeded,
73        notifier: Notifier,
74    },
75}
76
77#[derive(Debug)]
78pub enum RecoveryStart {
79    Global,
80    Database {
81        database_id: DatabaseId,
82        job_ids: HashSet<JobId>,
83    },
84}
85
86#[derive(Debug)]
87pub enum RecoverySucceeded {
88    Global {
89        failed_databases: HashMap<DatabaseId, HashSet<JobId>>,
90    },
91    Database(DatabaseId),
92}
93
94#[derive(Debug)]
95enum RecoveryFence {
96    Global,
97    Databases(HashMap<DatabaseId, HashSet<JobId>>),
98}
99
100#[derive(Clone)]
101pub struct SinkCoordinatorManager {
102    request_tx: mpsc::Sender<ManagerRequest>,
103}
104fn new_committed_epoch_subscriber(
105    hummock_manager: HummockManagerRef,
106    metadata_manager: MetadataManager,
107) -> SinkCommittedEpochSubscriber {
108    Arc::new(move |sink_id| {
109        let hummock_manager = hummock_manager.clone();
110        let metadata_manager = metadata_manager.clone();
111        async move {
112            let state_table_ids = metadata_manager
113                .get_sink_state_table_ids(sink_id)
114                .await
115                .map_err(SinkError::from)?;
116            let Some(table_id) = state_table_ids.first() else {
117                return Err(anyhow!("no state table id in sink: {}", sink_id).into());
118            };
119            hummock_manager
120                .subscribe_table_committed_epoch(*table_id)
121                .await
122                .map_err(SinkError::from)
123        }
124        .boxed()
125    })
126}
127
128impl SinkCoordinatorManager {
129    pub fn start_worker(
130        db: DatabaseConnection,
131        hummock_manager: HummockManagerRef,
132        metadata_manager: MetadataManager,
133        iceberg_compact_stat_sender: UnboundedSender<IcebergSinkCompactionUpdate>,
134        await_tree_reg: await_tree::Registry,
135    ) -> (Self, (JoinHandle<()>, Sender<()>)) {
136        let subscriber = new_committed_epoch_subscriber(hummock_manager, metadata_manager);
137        Self::start_worker_with_spawn_worker({
138            move |param, manager_request_stream| {
139                let sink_id = param.sink_id;
140                let fut = CoordinatorWorker::run(
141                    param,
142                    manager_request_stream,
143                    db.clone(),
144                    subscriber.clone(),
145                    iceberg_compact_stat_sender.clone(),
146                );
147                tokio::spawn(
148                    await_tree_reg
149                        .register_derived_root(format!("Sink Coordinator {sink_id}"))
150                        .instrument(fut),
151                )
152            }
153        })
154    }
155
156    fn start_worker_with_spawn_worker(
157        spawn_coordinator_worker: impl SpawnCoordinatorFn,
158    ) -> (Self, (JoinHandle<()>, Sender<()>)) {
159        let (request_tx, request_rx) = mpsc::channel(BOUNDED_CHANNEL_SIZE);
160        let (shutdown_tx, shutdown_rx) = channel();
161        let worker = ManagerWorker::new(request_rx, shutdown_rx);
162        let join_handle = tokio::spawn(worker.execute(spawn_coordinator_worker));
163        (
164            SinkCoordinatorManager { request_tx },
165            (join_handle, shutdown_tx),
166        )
167    }
168
169    pub async fn handle_new_request(
170        &self,
171        mut request_stream: SinkWriterRequestStream,
172    ) -> Result<impl Stream<Item = Result<CoordinateResponse, Status>> + use<>, Status> {
173        let (param, vnode_bitmap) = match request_stream.try_next().await? {
174            Some(CoordinateRequest {
175                msg:
176                    Some(Msg::StartRequest(coordinate_request::StartCoordinationRequest {
177                        param: Some(param),
178                        vnode_bitmap: Some(vnode_bitmap),
179                    })),
180            }) => (SinkParam::from_proto(param), Bitmap::from(&vnode_bitmap)),
181            msg => {
182                return Err(Status::invalid_argument(format!(
183                    "expected CoordinateRequest::StartRequest in the first request, get {:?}",
184                    msg
185                )));
186            }
187        };
188        let (response_tx, response_rx) = mpsc::unbounded_channel();
189        self.request_tx
190            .send(ManagerRequest::NewSinkWriter(
191                SinkWriterCoordinationHandle::new(request_stream, response_tx, param, vnode_bitmap),
192            ))
193            .await
194            .map_err(|_| {
195                Status::unavailable(
196                    "unable to send to sink manager worker. The worker may have stopped",
197                )
198            })?;
199
200        Ok(UnboundedReceiverStream::new(response_rx))
201    }
202
203    pub async fn stop_sink_coordinators_for_jobs(&self, job_ids: Vec<JobId>) {
204        let (notifier, started_rx) = Notifier::new();
205        if self
206            .request_tx
207            .send(ManagerRequest::StopCoordinator { notifier, job_ids })
208            .await
209            .is_err()
210        {
211            error!("unable to send sink coordinator stop request");
212            return;
213        }
214        if let Err(err) = Self::wait_for_completion(started_rx).await {
215            error!(error = %err.as_report(), "failed to wait for sink coordinators to stop");
216        }
217    }
218
219    pub async fn start_recovery(&self, recovery: RecoveryStart) -> anyhow::Result<()> {
220        let (notifier, started_rx) = Notifier::new();
221        self.request_tx
222            .send(ManagerRequest::RecoveryStart { recovery, notifier })
223            .await
224            .map_err(|_| anyhow!("sink coordinator manager worker has stopped"))?;
225        Self::wait_for_completion(started_rx).await
226    }
227
228    pub async fn recovery_succeeded(&self, recovery: RecoverySucceeded) -> anyhow::Result<()> {
229        let (notifier, started_rx) = Notifier::new();
230        self.request_tx
231            .send(ManagerRequest::RecoverySucceeded { recovery, notifier })
232            .await
233            .map_err(|_| anyhow!("sink coordinator manager worker has stopped"))?;
234        Self::wait_for_completion(started_rx).await
235    }
236
237    async fn wait_for_completion(started_rx: StartReceiver) -> anyhow::Result<()> {
238        let receivers = started_rx
239            .await
240            .map_err(|_| anyhow!("sink coordinator manager dropped start notifier"))??;
241        wait_collection(receivers).await?;
242        Ok(())
243    }
244}
245
246struct CoordinatorWorkerHandle {
247    /// Sender to coordinator worker. Drop the sender as a stop signal
248    request_sender: Option<UnboundedSender<SinkWriterCoordinationHandle>>,
249    /// Notify when the coordinator worker stops
250    finish_notifiers: Vec<CollectionNotifier>,
251}
252
253struct ManagerWorker {
254    request_rx: mpsc::Receiver<ManagerRequest>,
255    // Make it option so that it can be polled with &mut SinkManagerWorker
256    shutdown_rx: Receiver<()>,
257
258    running_coordinator_worker_join_handles:
259        FuturesUnordered<BoxFuture<'static, (SinkId, Result<(), JoinError>)>>,
260    running_coordinator_worker: HashMap<SinkId, CoordinatorWorkerHandle>,
261    recovery_fence: Option<RecoveryFence>,
262}
263
264enum ManagerEvent {
265    NewRequest(ManagerRequest),
266    CoordinatorWorkerFinished {
267        sink_id: SinkId,
268        join_result: Result<(), JoinError>,
269    },
270}
271
272trait SpawnCoordinatorFn = FnMut(SinkParam, UnboundedReceiver<SinkWriterCoordinationHandle>) -> JoinHandle<()>
273    + Send
274    + 'static;
275
276impl ManagerWorker {
277    fn new(request_rx: mpsc::Receiver<ManagerRequest>, shutdown_rx: Receiver<()>) -> Self {
278        ManagerWorker {
279            request_rx,
280            shutdown_rx,
281            running_coordinator_worker_join_handles: Default::default(),
282            running_coordinator_worker: Default::default(),
283            recovery_fence: None,
284        }
285    }
286
287    async fn execute(mut self, mut spawn_coordinator_worker: impl SpawnCoordinatorFn) {
288        while let Some(event) = self.next_event().await {
289            match event {
290                ManagerEvent::NewRequest(request) => match request {
291                    ManagerRequest::NewSinkWriter(request) => {
292                        self.handle_new_sink_writer(request, &mut spawn_coordinator_worker)
293                    }
294                    ManagerRequest::StopCoordinator { notifier, job_ids } => {
295                        let mut notifier = notifier.start();
296                        for job_id in job_ids {
297                            if let Some(worker_handle) = self
298                                .running_coordinator_worker
299                                .get_mut(&job_id.as_sink_id())
300                            {
301                                Self::drain_coordinator(worker_handle, &mut notifier);
302                            }
303                        }
304                        notifier.started();
305                    }
306                    ManagerRequest::RecoveryStart { recovery, notifier } => {
307                        self.apply_recovery_start(recovery, notifier);
308                    }
309                    ManagerRequest::RecoverySucceeded { recovery, notifier } => {
310                        let notifier = notifier.start();
311                        self.apply_recovery_succeeded(recovery);
312                        notifier.started();
313                    }
314                },
315                ManagerEvent::CoordinatorWorkerFinished {
316                    sink_id,
317                    join_result,
318                } => self.handle_coordinator_finished(sink_id, join_result),
319            }
320        }
321        self.clean_up().await;
322        info!("sink manager worker exited");
323    }
324
325    fn apply_recovery_start(&mut self, recovery: RecoveryStart, notifier: Notifier) {
326        let job_ids_to_stop = match recovery {
327            RecoveryStart::Global => {
328                self.recovery_fence = Some(RecoveryFence::Global);
329                None
330            }
331            RecoveryStart::Database {
332                database_id,
333                job_ids,
334            } => {
335                let job_ids_to_stop = job_ids.clone();
336                match &mut self.recovery_fence {
337                    Some(RecoveryFence::Global) => {}
338                    Some(RecoveryFence::Databases(databases)) => {
339                        databases.entry(database_id).or_default().extend(job_ids);
340                    }
341                    None => {
342                        self.recovery_fence = Some(RecoveryFence::Databases(HashMap::from([(
343                            database_id,
344                            job_ids,
345                        )])));
346                    }
347                }
348                Some(job_ids_to_stop)
349            }
350        };
351
352        let mut notifier = notifier.start();
353        if let Some(job_ids) = job_ids_to_stop {
354            for job_id in job_ids {
355                if let Some(worker_handle) = self
356                    .running_coordinator_worker
357                    .get_mut(&job_id.as_sink_id())
358                {
359                    Self::drain_coordinator(worker_handle, &mut notifier);
360                }
361            }
362        } else {
363            for worker_handle in self.running_coordinator_worker.values_mut() {
364                Self::drain_coordinator(worker_handle, &mut notifier);
365            }
366        }
367        notifier.started();
368    }
369
370    fn drain_coordinator(
371        worker_handle: &mut CoordinatorWorkerHandle,
372        notifier: &mut NotifierStarter,
373    ) {
374        worker_handle.finish_notifiers.push(notifier.add_notify());
375        if let Some(sender) = worker_handle.request_sender.take() {
376            // Drop the sender as a signal to notify the coordinator worker to stop.
377            drop(sender);
378        }
379    }
380
381    fn apply_recovery_succeeded(&mut self, recovery: RecoverySucceeded) {
382        match recovery {
383            RecoverySucceeded::Global { failed_databases } => {
384                self.recovery_fence = if failed_databases.is_empty() {
385                    None
386                } else {
387                    Some(RecoveryFence::Databases(failed_databases))
388                };
389            }
390            RecoverySucceeded::Database(database_id) => {
391                let Some(RecoveryFence::Databases(databases)) = &mut self.recovery_fence else {
392                    return;
393                };
394                databases.remove(&database_id);
395                if databases.is_empty() {
396                    self.recovery_fence = None;
397                }
398            }
399        }
400    }
401
402    fn is_sink_fenced(&self, sink_id: SinkId) -> bool {
403        match &self.recovery_fence {
404            Some(RecoveryFence::Global) => true,
405            Some(RecoveryFence::Databases(databases)) => databases
406                .values()
407                .any(|job_ids| job_ids.contains(&sink_id.as_job_id())),
408            None => false,
409        }
410    }
411
412    async fn next_event(&mut self) -> Option<ManagerEvent> {
413        match select(
414            select(
415                pin!(self.request_rx.recv()),
416                pin!(pending_on_none(
417                    self.running_coordinator_worker_join_handles.next()
418                )),
419            ),
420            &mut self.shutdown_rx,
421        )
422        .await
423        {
424            Either::Left((either, _)) => match either {
425                Either::Left((Some(request), _)) => Some(ManagerEvent::NewRequest(request)),
426                Either::Left((None, _)) => None,
427                Either::Right(((sink_id, join_result), _)) => {
428                    Some(ManagerEvent::CoordinatorWorkerFinished {
429                        sink_id,
430                        join_result,
431                    })
432                }
433            },
434            Either::Right(_) => None,
435        }
436    }
437
438    async fn clean_up(&mut self) {
439        info!("sink manager worker start cleaning up");
440        for worker_handle in self.running_coordinator_worker.values_mut() {
441            if let Some(sender) = worker_handle.request_sender.take() {
442                // drop the sender to notify the coordinator worker to stop
443                drop(sender);
444            }
445        }
446        while let Some((sink_id, join_result)) =
447            self.running_coordinator_worker_join_handles.next().await
448        {
449            self.handle_coordinator_finished(sink_id, join_result);
450        }
451        info!("sink manager worker finished cleaning up");
452    }
453
454    fn handle_coordinator_finished(&mut self, sink_id: SinkId, join_result: Result<(), JoinError>) {
455        let worker_handle = self
456            .running_coordinator_worker
457            .remove(&sink_id)
458            .expect("finished coordinator should have an associated worker handle");
459        for finish_notifier in worker_handle.finish_notifiers {
460            finish_notifier.notify_collected();
461        }
462        match join_result {
463            Ok(()) => {
464                info!(
465                    id = %sink_id,
466                    "sink coordinator has gracefully finished",
467                );
468            }
469            Err(err) => {
470                error!(
471                    id = %sink_id,
472                    error = %err.as_report(),
473                    "sink coordinator finished with error",
474                );
475            }
476        }
477    }
478
479    fn handle_new_sink_writer(
480        &mut self,
481        new_writer: SinkWriterCoordinationHandle,
482        spawn_coordinator_worker: &mut impl SpawnCoordinatorFn,
483    ) {
484        let param = new_writer.param();
485        let sink_id = param.sink_id;
486
487        if self.is_sink_fenced(sink_id) {
488            new_writer.abort(Status::unavailable(format!(
489                "sink coordinator for sink {} is unavailable during recovery",
490                sink_id
491            )));
492            return;
493        }
494
495        let handle = self
496            .running_coordinator_worker
497            .entry(param.sink_id)
498            .or_insert_with(|| {
499                // Launch the coordinator worker task if it is the first
500                let (request_tx, request_rx) = unbounded_channel();
501                let join_handle = spawn_coordinator_worker(param.clone(), request_rx);
502                self.running_coordinator_worker_join_handles.push(
503                    join_handle
504                        .map(move |join_result| (sink_id, join_result))
505                        .boxed(),
506                );
507                CoordinatorWorkerHandle {
508                    request_sender: Some(request_tx),
509                    finish_notifiers: Vec::new(),
510                }
511            });
512
513        if let Some(sender) = handle.request_sender.as_mut() {
514            send_with_err_check!(sender, new_writer);
515        } else {
516            warn!(
517                "handle a new request while the sink coordinator is being stopped: {:?}",
518                param
519            );
520            new_writer.abort(Status::internal("the sink is being stopped"));
521        }
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use std::collections::{HashMap, HashSet};
528    use std::future::{Future, poll_fn};
529    use std::pin::pin;
530    use std::sync::Arc;
531    use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
532    use std::task::Poll;
533
534    use anyhow::anyhow;
535    use async_trait::async_trait;
536    use futures::future::{join, try_join};
537    use futures::{FutureExt, StreamExt, TryFutureExt};
538    use itertools::Itertools;
539    use rand::seq::SliceRandom;
540    use risingwave_common::bitmap::{Bitmap, BitmapBuilder};
541    use risingwave_common::hash::VirtualNode;
542    use risingwave_common::id::{DatabaseId, JobId};
543    use risingwave_connector::sink::catalog::{SinkId, SinkType};
544    use risingwave_connector::sink::{
545        SinglePhaseCommitCoordinator, SinkCommitCoordinator, SinkError, SinkParam,
546        TwoPhaseCommitCoordinator,
547    };
548    use risingwave_meta_model::SinkSchemachange;
549    use risingwave_pb::connector_service::sink_metadata::{Metadata, SerializedMetadata};
550    use risingwave_pb::connector_service::{CoordinateResponse, SinkMetadata};
551    use risingwave_pb::data::PbDataType;
552    use risingwave_pb::data::data_type::PbTypeName;
553    use risingwave_pb::plan_common::PbField;
554    use risingwave_pb::stream_plan::sink_schema_change::Op as SinkSchemachangeOp;
555    use risingwave_pb::stream_plan::{PbSinkAddColumnsOp, PbSinkSchemaChange};
556    use risingwave_rpc_client::CoordinatorStreamHandle;
557    use sea_orm::{ConnectionTrait, Database, DatabaseConnection};
558    use tokio::sync::Notify;
559    use tokio::sync::mpsc::unbounded_channel;
560    use tokio_stream::wrappers::ReceiverStream;
561    use tonic::Status;
562
563    use super::{
564        ManagerRequest, ManagerWorker, RecoveryFence, RecoveryStart, RecoverySucceeded,
565        SinkWriterCoordinationHandle,
566    };
567    use crate::manager::sink_coordination::SinkCoordinatorManager;
568    use crate::manager::sink_coordination::coordinator_worker::CoordinatorWorker;
569    use crate::manager::sink_coordination::manager::SinkCommittedEpochSubscriber;
570
571    fn test_writer_handle(
572        param: SinkParam,
573    ) -> (
574        SinkWriterCoordinationHandle,
575        tokio::sync::mpsc::UnboundedReceiver<Result<CoordinateResponse, Status>>,
576    ) {
577        let (response_tx, response_rx) = unbounded_channel();
578        (
579            SinkWriterCoordinationHandle::new(
580                futures::stream::pending().boxed(),
581                response_tx,
582                param,
583                Bitmap::ones(1),
584            ),
585            response_rx,
586        )
587    }
588
589    #[test]
590    fn test_recovery_fence_transitions() {
591        let (_request_tx, request_rx) = tokio::sync::mpsc::channel(1);
592        let (_shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
593        let mut worker = ManagerWorker::new(request_rx, shutdown_rx);
594        let database_1 = DatabaseId::new(1);
595        let database_2 = DatabaseId::new(2);
596        let sink_1 = SinkId::new(11);
597        let sink_2 = SinkId::new(12);
598        let sink_3 = SinkId::new(21);
599        let job_1 = sink_1.as_job_id();
600        let job_2 = sink_2.as_job_id();
601        let job_3 = sink_3.as_job_id();
602
603        assert!(!worker.is_sink_fenced(sink_1));
604
605        worker.apply_recovery_start(
606            RecoveryStart::Database {
607                database_id: database_1,
608                job_ids: HashSet::from([job_1]),
609            },
610            crate::notification::Notifier::new().0,
611        );
612        assert!(worker.is_sink_fenced(sink_1));
613        assert!(!worker.is_sink_fenced(sink_2));
614
615        worker.apply_recovery_start(
616            RecoveryStart::Database {
617                database_id: database_1,
618                job_ids: HashSet::from([job_2]),
619            },
620            crate::notification::Notifier::new().0,
621        );
622        worker.apply_recovery_start(
623            RecoveryStart::Database {
624                database_id: database_2,
625                job_ids: HashSet::from([job_3]),
626            },
627            crate::notification::Notifier::new().0,
628        );
629        assert!(worker.is_sink_fenced(sink_1));
630        assert!(worker.is_sink_fenced(sink_2));
631        assert!(worker.is_sink_fenced(sink_3));
632
633        worker.apply_recovery_start(
634            RecoveryStart::Global,
635            crate::notification::Notifier::new().0,
636        );
637        worker.apply_recovery_start(
638            RecoveryStart::Global,
639            crate::notification::Notifier::new().0,
640        );
641        assert!(matches!(worker.recovery_fence, Some(RecoveryFence::Global)));
642        assert!(worker.is_sink_fenced(SinkId::new(999)));
643
644        worker.apply_recovery_start(
645            RecoveryStart::Database {
646                database_id: database_1,
647                job_ids: HashSet::from([job_1]),
648            },
649            crate::notification::Notifier::new().0,
650        );
651        assert!(matches!(worker.recovery_fence, Some(RecoveryFence::Global)));
652        worker.apply_recovery_succeeded(RecoverySucceeded::Database(database_1));
653        assert!(matches!(worker.recovery_fence, Some(RecoveryFence::Global)));
654
655        worker.apply_recovery_succeeded(RecoverySucceeded::Global {
656            failed_databases: HashMap::from([
657                (database_1, HashSet::from([job_1, job_2])),
658                (database_2, HashSet::from([job_3])),
659            ]),
660        });
661        assert!(worker.is_sink_fenced(sink_1));
662        assert!(worker.is_sink_fenced(sink_3));
663        assert!(!worker.is_sink_fenced(SinkId::new(999)));
664
665        worker.apply_recovery_succeeded(RecoverySucceeded::Database(database_1));
666        assert!(!worker.is_sink_fenced(sink_1));
667        assert!(worker.is_sink_fenced(sink_3));
668
669        worker.apply_recovery_succeeded(RecoverySucceeded::Database(database_2));
670        assert!(worker.recovery_fence.is_none());
671
672        worker.apply_recovery_start(
673            RecoveryStart::Global,
674            crate::notification::Notifier::new().0,
675        );
676        worker.apply_recovery_succeeded(RecoverySucceeded::Global {
677            failed_databases: HashMap::new(),
678        });
679        assert!(worker.recovery_fence.is_none());
680    }
681
682    #[tokio::test]
683    async fn test_recovery_fence_blocks_spawn_and_waits_for_coordinator_exit() {
684        let database_id = DatabaseId::new(1);
685        let sink_id = SinkId::new(11);
686        let param = SinkParam {
687            sink_id,
688            sink_name: "test".into(),
689            properties: Default::default(),
690            columns: vec![],
691            downstream_pk: None,
692            sink_type: SinkType::AppendOnly,
693            ignore_delete: false,
694            format_desc: None,
695            db_name: "test".into(),
696            sink_from_name: "test".into(),
697        };
698        let spawn_count = Arc::new(AtomicUsize::new(0));
699        let spawned = Arc::new(Notify::new());
700        let stopping = Arc::new(Notify::new());
701        let allow_exit = Arc::new(Notify::new());
702        let (manager, (_join_handle, _shutdown_tx)) =
703            SinkCoordinatorManager::start_worker_with_spawn_worker({
704                let spawn_count = spawn_count.clone();
705                let spawned = spawned.clone();
706                let stopping = stopping.clone();
707                let allow_exit = allow_exit.clone();
708                move |_param, mut request_rx| {
709                    spawn_count.fetch_add(1, Ordering::SeqCst);
710                    spawned.notify_one();
711                    let stopping = stopping.clone();
712                    let allow_exit = allow_exit.clone();
713                    tokio::spawn(async move {
714                        while request_rx.recv().await.is_some() {}
715                        stopping.notify_one();
716                        allow_exit.notified().await;
717                    })
718                }
719            });
720
721        manager
722            .start_recovery(RecoveryStart::Database {
723                database_id,
724                job_ids: HashSet::from([sink_id.as_job_id()]),
725            })
726            .await
727            .unwrap();
728
729        let (writer, mut response_rx) = test_writer_handle(param.clone());
730        manager
731            .request_tx
732            .send(ManagerRequest::NewSinkWriter(writer))
733            .await
734            .unwrap();
735        let status = response_rx.recv().await.unwrap().unwrap_err();
736        assert_eq!(status.code(), tonic::Code::Unavailable);
737        assert_eq!(spawn_count.load(Ordering::SeqCst), 0);
738
739        manager
740            .recovery_succeeded(RecoverySucceeded::Database(database_id))
741            .await
742            .unwrap();
743        let (writer, _response_rx) = test_writer_handle(param.clone());
744        manager
745            .request_tx
746            .send(ManagerRequest::NewSinkWriter(writer))
747            .await
748            .unwrap();
749        spawned.notified().await;
750        assert_eq!(spawn_count.load(Ordering::SeqCst), 1);
751
752        let stop_task = tokio::spawn({
753            let manager = manager.clone();
754            async move {
755                manager
756                    .stop_sink_coordinators_for_jobs(vec![sink_id.as_job_id()])
757                    .await;
758            }
759        });
760        stopping.notified().await;
761        assert!(!stop_task.is_finished());
762        allow_exit.notify_one();
763        stop_task.await.unwrap();
764
765        let (writer, _response_rx) = test_writer_handle(param.clone());
766        manager
767            .request_tx
768            .send(ManagerRequest::NewSinkWriter(writer))
769            .await
770            .unwrap();
771        spawned.notified().await;
772        assert_eq!(spawn_count.load(Ordering::SeqCst), 2);
773
774        let recovery_task = tokio::spawn({
775            let manager = manager.clone();
776            async move {
777                manager
778                    .start_recovery(RecoveryStart::Database {
779                        database_id,
780                        job_ids: HashSet::from([sink_id.as_job_id()]),
781                    })
782                    .await
783                    .unwrap();
784            }
785        });
786        stopping.notified().await;
787        assert!(!recovery_task.is_finished());
788
789        let (writer, mut response_rx) = test_writer_handle(param);
790        manager
791            .request_tx
792            .send(ManagerRequest::NewSinkWriter(writer))
793            .await
794            .unwrap();
795        let status = response_rx.recv().await.unwrap().unwrap_err();
796        assert_eq!(status.code(), tonic::Code::Unavailable);
797        assert_eq!(spawn_count.load(Ordering::SeqCst), 2);
798
799        allow_exit.notify_one();
800        recovery_task.await.unwrap();
801    }
802
803    struct MockSinglePhaseCoordinator<
804        C,
805        F: FnMut(u64, Vec<SinkMetadata>, &mut C) -> Result<(), SinkError>,
806    > {
807        context: C,
808        f: F,
809    }
810
811    impl<
812        C: Send + 'static,
813        F: FnMut(u64, Vec<SinkMetadata>, &mut C) -> Result<(), SinkError> + Send + 'static,
814    > MockSinglePhaseCoordinator<C, F>
815    {
816        fn new_coordinator(context: C, f: F) -> SinkCommitCoordinator {
817            SinkCommitCoordinator::SinglePhase(Box::new(MockSinglePhaseCoordinator { context, f }))
818        }
819    }
820
821    #[async_trait]
822    impl<C: Send, F: FnMut(u64, Vec<SinkMetadata>, &mut C) -> Result<(), SinkError> + Send>
823        SinglePhaseCommitCoordinator for MockSinglePhaseCoordinator<C, F>
824    {
825        async fn init(&mut self) -> risingwave_connector::sink::Result<()> {
826            Ok(())
827        }
828
829        async fn commit_data(
830            &mut self,
831            epoch: u64,
832            metadata: Vec<SinkMetadata>,
833        ) -> risingwave_connector::sink::Result<()> {
834            (self.f)(epoch, metadata, &mut self.context)
835        }
836
837        async fn commit_schema_change(
838            &mut self,
839            _epoch: u64,
840            _schema_change: PbSinkSchemaChange,
841        ) -> risingwave_connector::sink::Result<()> {
842            unreachable!()
843        }
844    }
845
846    #[tokio::test]
847    async fn test_basic() {
848        let db = prepare_db_backend().await;
849
850        let param = SinkParam {
851            sink_id: SinkId::from(1),
852            sink_name: "test".into(),
853            properties: Default::default(),
854            columns: vec![],
855            downstream_pk: None,
856            sink_type: SinkType::AppendOnly,
857            ignore_delete: false,
858            format_desc: None,
859            db_name: "test".into(),
860            sink_from_name: "test".into(),
861        };
862
863        let epoch0 = 232;
864        let epoch1 = 233;
865        let epoch2 = 234;
866
867        let mut all_vnode = (0..VirtualNode::COUNT_FOR_TEST).collect_vec();
868        all_vnode.shuffle(&mut rand::rng());
869        let (first, second) = all_vnode.split_at(VirtualNode::COUNT_FOR_TEST / 2);
870        let build_bitmap = |indexes: &[usize]| {
871            let mut builder = BitmapBuilder::zeroed(VirtualNode::COUNT_FOR_TEST);
872            for i in indexes {
873                builder.set(*i, true);
874            }
875            builder.finish()
876        };
877        let vnode1 = build_bitmap(first);
878        let vnode2 = build_bitmap(second);
879
880        let metadata = [
881            [vec![1u8, 2u8], vec![3u8, 4u8]],
882            [vec![5u8, 6u8], vec![7u8, 8u8]],
883        ];
884        let sender = Arc::new(tokio::sync::Mutex::new(None));
885        let mock_subscriber: SinkCommittedEpochSubscriber = {
886            let captured_sender = sender.clone();
887            Arc::new(move |_sink_id: SinkId| {
888                let (sender, receiver) = unbounded_channel();
889                let captured_sender = captured_sender.clone();
890                async move {
891                    let mut guard = captured_sender.lock().await;
892                    *guard = Some(sender);
893                    Ok((1, receiver))
894                }
895                .boxed()
896            })
897        };
898
899        let (manager, (_join_handle, _stop_tx)) =
900            SinkCoordinatorManager::start_worker_with_spawn_worker({
901                let expected_param = param.clone();
902                let metadata = metadata.clone();
903                let db = db.clone();
904                move |param, new_writer_rx| {
905                    let metadata = metadata.clone();
906                    let expected_param = expected_param.clone();
907                    let db = db.clone();
908                    tokio::spawn({
909                        let subscriber = mock_subscriber.clone();
910                        async move {
911                            // validate the start request
912                            assert_eq!(param, expected_param);
913                            CoordinatorWorker::execute_coordinator(
914                                db,
915                                param.clone(),
916                                new_writer_rx,
917                                MockSinglePhaseCoordinator::new_coordinator(
918                                    0,
919                                    move |epoch, metadata_list, count: &mut usize| {
920                                        *count += 1;
921                                        let mut metadata_list =
922                                            metadata_list
923                                                .into_iter()
924                                                .map(|metadata| match metadata {
925                                                    SinkMetadata {
926                                                        metadata:
927                                                            Some(Metadata::Serialized(
928                                                                SerializedMetadata { metadata },
929                                                            )),
930                                                    } => metadata,
931                                                    _ => unreachable!(),
932                                                })
933                                                .collect_vec();
934                                        metadata_list.sort();
935                                        match *count {
936                                            1 => {
937                                                assert_eq!(epoch, epoch1);
938                                                assert_eq!(2, metadata_list.len());
939                                                assert_eq!(metadata[0][0], metadata_list[0]);
940                                                assert_eq!(metadata[0][1], metadata_list[1]);
941                                            }
942                                            2 => {
943                                                assert_eq!(epoch, epoch2);
944                                                assert_eq!(2, metadata_list.len());
945                                                assert_eq!(metadata[1][0], metadata_list[0]);
946                                                assert_eq!(metadata[1][1], metadata_list[1]);
947                                            }
948                                            _ => unreachable!(),
949                                        }
950                                        Ok(())
951                                    },
952                                ),
953                                subscriber.clone(),
954                            )
955                            .await;
956                        }
957                    })
958                }
959            });
960
961        let build_client = |vnode| async {
962            CoordinatorStreamHandle::new_with_init_stream(param.to_proto(), vnode, |rx| async {
963                Ok(tonic::Response::new(
964                    manager
965                        .handle_new_request(ReceiverStream::new(rx).map(Ok).boxed())
966                        .await
967                        .unwrap()
968                        .boxed(),
969                ))
970            })
971            .await
972            .unwrap()
973            .0
974        };
975
976        let (mut client1, mut client2) =
977            join(build_client(vnode1), pin!(build_client(vnode2))).await;
978
979        let (aligned_epoch1, aligned_epoch2) = try_join(
980            client1.align_initial_epoch(epoch0),
981            client2.align_initial_epoch(epoch1),
982        )
983        .await
984        .unwrap();
985        assert_eq!(aligned_epoch1, epoch1);
986        assert_eq!(aligned_epoch2, epoch1);
987
988        {
989            // commit epoch1
990            let mut commit_future = pin!(
991                client2
992                    .commit(
993                        epoch1,
994                        SinkMetadata {
995                            metadata: Some(Metadata::Serialized(SerializedMetadata {
996                                metadata: metadata[0][1].clone(),
997                            })),
998                        },
999                        None,
1000                    )
1001                    .map(|result| result.unwrap())
1002            );
1003            assert!(
1004                poll_fn(|cx| Poll::Ready(commit_future.as_mut().poll(cx)))
1005                    .await
1006                    .is_pending()
1007            );
1008            join(
1009                commit_future,
1010                client1
1011                    .commit(
1012                        epoch1,
1013                        SinkMetadata {
1014                            metadata: Some(Metadata::Serialized(SerializedMetadata {
1015                                metadata: metadata[0][0].clone(),
1016                            })),
1017                        },
1018                        None,
1019                    )
1020                    .map(|result| result.unwrap()),
1021            )
1022            .await;
1023        }
1024
1025        // commit epoch2
1026        let mut commit_future = pin!(
1027            client1
1028                .commit(
1029                    epoch2,
1030                    SinkMetadata {
1031                        metadata: Some(Metadata::Serialized(SerializedMetadata {
1032                            metadata: metadata[1][0].clone(),
1033                        })),
1034                    },
1035                    None,
1036                )
1037                .map(|result| result.unwrap())
1038        );
1039        assert!(
1040            poll_fn(|cx| Poll::Ready(commit_future.as_mut().poll(cx)))
1041                .await
1042                .is_pending()
1043        );
1044        join(
1045            commit_future,
1046            client2
1047                .commit(
1048                    epoch2,
1049                    SinkMetadata {
1050                        metadata: Some(Metadata::Serialized(SerializedMetadata {
1051                            metadata: metadata[1][1].clone(),
1052                        })),
1053                    },
1054                    None,
1055                )
1056                .map(|result| result.unwrap()),
1057        )
1058        .await;
1059    }
1060
1061    #[tokio::test]
1062    async fn test_single_writer() {
1063        let db = prepare_db_backend().await;
1064        let param = SinkParam {
1065            sink_id: SinkId::from(1),
1066            sink_name: "test".into(),
1067            properties: Default::default(),
1068            columns: vec![],
1069            downstream_pk: None,
1070            sink_type: SinkType::AppendOnly,
1071            ignore_delete: false,
1072            format_desc: None,
1073            db_name: "test".into(),
1074            sink_from_name: "test".into(),
1075        };
1076
1077        let epoch1 = 233;
1078        let epoch2 = 234;
1079
1080        let all_vnode = (0..VirtualNode::COUNT_FOR_TEST).collect_vec();
1081        let build_bitmap = |indexes: &[usize]| {
1082            let mut builder = BitmapBuilder::zeroed(VirtualNode::COUNT_FOR_TEST);
1083            for i in indexes {
1084                builder.set(*i, true);
1085            }
1086            builder.finish()
1087        };
1088        let vnode = build_bitmap(&all_vnode);
1089
1090        let metadata = [vec![1u8, 2u8], vec![3u8, 4u8]];
1091        let sender = Arc::new(tokio::sync::Mutex::new(None));
1092        let mock_subscriber: SinkCommittedEpochSubscriber = {
1093            let captured_sender = sender.clone();
1094            Arc::new(move |_sink_id: SinkId| {
1095                let (sender, receiver) = unbounded_channel();
1096                let captured_sender = captured_sender.clone();
1097                async move {
1098                    let mut guard = captured_sender.lock().await;
1099                    *guard = Some(sender);
1100                    Ok((1, receiver))
1101                }
1102                .boxed()
1103            })
1104        };
1105        let (manager, (_join_handle, _stop_tx)) =
1106            SinkCoordinatorManager::start_worker_with_spawn_worker({
1107                let expected_param = param.clone();
1108                let metadata = metadata.clone();
1109                let db = db.clone();
1110                move |param, new_writer_rx| {
1111                    let metadata = metadata.clone();
1112                    let expected_param = expected_param.clone();
1113                    let db = db.clone();
1114                    tokio::spawn({
1115                        let subscriber = mock_subscriber.clone();
1116                        async move {
1117                            // validate the start request
1118                            assert_eq!(param, expected_param);
1119                            CoordinatorWorker::execute_coordinator(
1120                                db,
1121                                param.clone(),
1122                                new_writer_rx,
1123                                MockSinglePhaseCoordinator::new_coordinator(
1124                                    0,
1125                                    move |epoch, metadata_list, count: &mut usize| {
1126                                        *count += 1;
1127                                        let mut metadata_list =
1128                                            metadata_list
1129                                                .into_iter()
1130                                                .map(|metadata| match metadata {
1131                                                    SinkMetadata {
1132                                                        metadata:
1133                                                            Some(Metadata::Serialized(
1134                                                                SerializedMetadata { metadata },
1135                                                            )),
1136                                                    } => metadata,
1137                                                    _ => unreachable!(),
1138                                                })
1139                                                .collect_vec();
1140                                        metadata_list.sort();
1141                                        match *count {
1142                                            1 => {
1143                                                assert_eq!(epoch, epoch1);
1144                                                assert_eq!(1, metadata_list.len());
1145                                                assert_eq!(metadata[0], metadata_list[0]);
1146                                            }
1147                                            2 => {
1148                                                assert_eq!(epoch, epoch2);
1149                                                assert_eq!(1, metadata_list.len());
1150                                                assert_eq!(metadata[1], metadata_list[0]);
1151                                            }
1152                                            _ => unreachable!(),
1153                                        }
1154                                        Ok(())
1155                                    },
1156                                ),
1157                                subscriber.clone(),
1158                            )
1159                            .await;
1160                        }
1161                    })
1162                }
1163            });
1164
1165        let build_client = |vnode| async {
1166            CoordinatorStreamHandle::new_with_init_stream(param.to_proto(), vnode, |rx| async {
1167                Ok(tonic::Response::new(
1168                    manager
1169                        .handle_new_request(ReceiverStream::new(rx).map(Ok).boxed())
1170                        .await
1171                        .unwrap()
1172                        .boxed(),
1173                ))
1174            })
1175            .await
1176            .unwrap()
1177            .0
1178        };
1179
1180        let mut client = build_client(vnode).await;
1181
1182        let aligned_epoch = client.align_initial_epoch(epoch1).await.unwrap();
1183        assert_eq!(aligned_epoch, epoch1);
1184
1185        client
1186            .commit(
1187                epoch1,
1188                SinkMetadata {
1189                    metadata: Some(Metadata::Serialized(SerializedMetadata {
1190                        metadata: metadata[0].clone(),
1191                    })),
1192                },
1193                None,
1194            )
1195            .await
1196            .unwrap();
1197
1198        client
1199            .commit(
1200                epoch2,
1201                SinkMetadata {
1202                    metadata: Some(Metadata::Serialized(SerializedMetadata {
1203                        metadata: metadata[1].clone(),
1204                    })),
1205                },
1206                None,
1207            )
1208            .await
1209            .unwrap();
1210    }
1211
1212    #[tokio::test]
1213    async fn test_partial_commit() {
1214        let db = prepare_db_backend().await;
1215        let param = SinkParam {
1216            sink_id: SinkId::from(1),
1217            sink_name: "test".into(),
1218            properties: Default::default(),
1219            columns: vec![],
1220            downstream_pk: None,
1221            sink_type: SinkType::AppendOnly,
1222            ignore_delete: false,
1223            format_desc: None,
1224            db_name: "test".into(),
1225            sink_from_name: "test".into(),
1226        };
1227
1228        let epoch = 233;
1229
1230        let mut all_vnode = (0..VirtualNode::COUNT_FOR_TEST).collect_vec();
1231        all_vnode.shuffle(&mut rand::rng());
1232        let (first, second) = all_vnode.split_at(VirtualNode::COUNT_FOR_TEST / 2);
1233        let build_bitmap = |indexes: &[usize]| {
1234            let mut builder = BitmapBuilder::zeroed(VirtualNode::COUNT_FOR_TEST);
1235            for i in indexes {
1236                builder.set(*i, true);
1237            }
1238            builder.finish()
1239        };
1240        let vnode1 = build_bitmap(first);
1241        let vnode2 = build_bitmap(second);
1242
1243        let sender = Arc::new(tokio::sync::Mutex::new(None));
1244        let mock_subscriber: SinkCommittedEpochSubscriber = {
1245            let captured_sender = sender.clone();
1246            Arc::new(move |_sink_id: SinkId| {
1247                let (sender, receiver) = unbounded_channel();
1248                let captured_sender = captured_sender.clone();
1249                async move {
1250                    let mut guard = captured_sender.lock().await;
1251                    *guard = Some(sender);
1252                    Ok((1, receiver))
1253                }
1254                .boxed()
1255            })
1256        };
1257        let (manager, (_join_handle, _stop_tx)) =
1258            SinkCoordinatorManager::start_worker_with_spawn_worker({
1259                let expected_param = param.clone();
1260                let db = db.clone();
1261                move |param, new_writer_rx| {
1262                    let expected_param = expected_param.clone();
1263                    let db = db.clone();
1264                    tokio::spawn({
1265                        let subscriber = mock_subscriber.clone();
1266                        async move {
1267                            // validate the start request
1268                            assert_eq!(param, expected_param);
1269                            CoordinatorWorker::execute_coordinator(
1270                                db,
1271                                param,
1272                                new_writer_rx,
1273                                MockSinglePhaseCoordinator::new_coordinator(
1274                                    (),
1275                                    |_, _, _| unreachable!(),
1276                                ),
1277                                subscriber.clone(),
1278                            )
1279                            .await;
1280                        }
1281                    })
1282                }
1283            });
1284
1285        let build_client = |vnode| async {
1286            CoordinatorStreamHandle::new_with_init_stream(param.to_proto(), vnode, |rx| async {
1287                Ok(tonic::Response::new(
1288                    manager
1289                        .handle_new_request(ReceiverStream::new(rx).map(Ok).boxed())
1290                        .await
1291                        .unwrap()
1292                        .boxed(),
1293                ))
1294            })
1295            .await
1296            .unwrap()
1297            .0
1298        };
1299
1300        let (mut client1, client2) = join(build_client(vnode1), build_client(vnode2)).await;
1301
1302        // commit epoch
1303        let mut commit_future = pin!(client1.commit(
1304            epoch,
1305            SinkMetadata {
1306                metadata: Some(Metadata::Serialized(SerializedMetadata {
1307                    metadata: vec![],
1308                })),
1309            },
1310            None,
1311        ));
1312        assert!(
1313            poll_fn(|cx| Poll::Ready(commit_future.as_mut().poll(cx)))
1314                .await
1315                .is_pending()
1316        );
1317        drop(client2);
1318        assert!(commit_future.await.is_err());
1319    }
1320
1321    #[tokio::test]
1322    async fn test_fail_commit() {
1323        let db = prepare_db_backend().await;
1324        let param = SinkParam {
1325            sink_id: SinkId::from(1),
1326            sink_name: "test".into(),
1327            properties: Default::default(),
1328            columns: vec![],
1329            downstream_pk: None,
1330            sink_type: SinkType::AppendOnly,
1331            ignore_delete: false,
1332            format_desc: None,
1333            db_name: "test".into(),
1334            sink_from_name: "test".into(),
1335        };
1336
1337        let epoch = 233;
1338
1339        let mut all_vnode = (0..VirtualNode::COUNT_FOR_TEST).collect_vec();
1340        all_vnode.shuffle(&mut rand::rng());
1341        let (first, second) = all_vnode.split_at(VirtualNode::COUNT_FOR_TEST / 2);
1342        let build_bitmap = |indexes: &[usize]| {
1343            let mut builder = BitmapBuilder::zeroed(VirtualNode::COUNT_FOR_TEST);
1344            for i in indexes {
1345                builder.set(*i, true);
1346            }
1347            builder.finish()
1348        };
1349        let vnode1 = build_bitmap(first);
1350        let vnode2 = build_bitmap(second);
1351        let sender = Arc::new(tokio::sync::Mutex::new(None));
1352        let mock_subscriber: SinkCommittedEpochSubscriber = {
1353            let captured_sender = sender.clone();
1354            Arc::new(move |_sink_id: SinkId| {
1355                let (sender, receiver) = unbounded_channel();
1356                let captured_sender = captured_sender.clone();
1357                async move {
1358                    let mut guard = captured_sender.lock().await;
1359                    *guard = Some(sender);
1360                    Ok((1, receiver))
1361                }
1362                .boxed()
1363            })
1364        };
1365        let (manager, (_join_handle, _stop_tx)) =
1366            SinkCoordinatorManager::start_worker_with_spawn_worker({
1367                let expected_param = param.clone();
1368                let db = db.clone();
1369                move |param, new_writer_rx| {
1370                    let expected_param = expected_param.clone();
1371                    let db = db.clone();
1372                    tokio::spawn({
1373                        let subscriber = mock_subscriber.clone();
1374                        {
1375                            async move {
1376                                // validate the start request
1377                                assert_eq!(param, expected_param);
1378                                CoordinatorWorker::execute_coordinator(
1379                                    db,
1380                                    param,
1381                                    new_writer_rx,
1382                                    MockSinglePhaseCoordinator::new_coordinator((), |_, _, _| {
1383                                        Err(SinkError::Coordinator(anyhow!("failed to commit")))
1384                                    }),
1385                                    subscriber.clone(),
1386                                )
1387                                .await;
1388                            }
1389                        }
1390                    })
1391                }
1392            });
1393
1394        let build_client = |vnode| async {
1395            CoordinatorStreamHandle::new_with_init_stream(param.to_proto(), vnode, |rx| async {
1396                Ok(tonic::Response::new(
1397                    manager
1398                        .handle_new_request(ReceiverStream::new(rx).map(Ok).boxed())
1399                        .await
1400                        .unwrap()
1401                        .boxed(),
1402                ))
1403            })
1404            .await
1405            .unwrap()
1406            .0
1407        };
1408
1409        let (mut client1, mut client2) = join(build_client(vnode1), build_client(vnode2)).await;
1410
1411        // commit epoch
1412        let mut commit_future = pin!(client1.commit(
1413            epoch,
1414            SinkMetadata {
1415                metadata: Some(Metadata::Serialized(SerializedMetadata {
1416                    metadata: vec![],
1417                })),
1418            },
1419            None,
1420        ));
1421        assert!(
1422            poll_fn(|cx| Poll::Ready(commit_future.as_mut().poll(cx)))
1423                .await
1424                .is_pending()
1425        );
1426        let (result1, result2) = join(
1427            commit_future,
1428            client2.commit(
1429                epoch,
1430                SinkMetadata {
1431                    metadata: Some(Metadata::Serialized(SerializedMetadata {
1432                        metadata: vec![],
1433                    })),
1434                },
1435                None,
1436            ),
1437        )
1438        .await;
1439        assert!(result1.is_err());
1440        assert!(result2.is_err());
1441    }
1442
1443    #[tokio::test]
1444    async fn test_update_vnode_bitmap() {
1445        let db = prepare_db_backend().await;
1446        let param = SinkParam {
1447            sink_id: SinkId::from(1),
1448            sink_name: "test".into(),
1449            properties: Default::default(),
1450            columns: vec![],
1451            downstream_pk: None,
1452            sink_type: SinkType::AppendOnly,
1453            ignore_delete: false,
1454            format_desc: None,
1455            db_name: "test".into(),
1456            sink_from_name: "test".into(),
1457        };
1458
1459        let epoch1 = 233;
1460        let epoch2 = 234;
1461        let epoch3 = 235;
1462        let epoch4 = 236;
1463
1464        let mut all_vnode = (0..VirtualNode::COUNT_FOR_TEST).collect_vec();
1465        all_vnode.shuffle(&mut rand::rng());
1466        let (first, second) = all_vnode.split_at(VirtualNode::COUNT_FOR_TEST / 2);
1467        let build_bitmap = |indexes: &[usize]| {
1468            let mut builder = BitmapBuilder::zeroed(VirtualNode::COUNT_FOR_TEST);
1469            for i in indexes {
1470                builder.set(*i, true);
1471            }
1472            builder.finish()
1473        };
1474        let vnode1 = build_bitmap(first);
1475        let vnode2 = build_bitmap(second);
1476
1477        let metadata = [
1478            [vec![1u8, 2u8], vec![3u8, 4u8]],
1479            [vec![5u8, 6u8], vec![7u8, 8u8]],
1480        ];
1481
1482        let metadata_scale_out = [vec![9u8, 10u8], vec![11u8, 12u8], vec![13u8, 14u8]];
1483        let metadata_scale_in = [vec![13u8, 14u8], vec![15u8, 16u8]];
1484        let sender = Arc::new(tokio::sync::Mutex::new(None));
1485        let mock_subscriber: SinkCommittedEpochSubscriber = {
1486            let captured_sender = sender.clone();
1487            Arc::new(move |_sink_id: SinkId| {
1488                let (sender, receiver) = unbounded_channel();
1489                let captured_sender = captured_sender.clone();
1490                async move {
1491                    let mut guard = captured_sender.lock().await;
1492                    *guard = Some(sender);
1493                    Ok((1, receiver))
1494                }
1495                .boxed()
1496            })
1497        };
1498        let (manager, (_join_handle, _stop_tx)) =
1499            SinkCoordinatorManager::start_worker_with_spawn_worker({
1500                let expected_param = param.clone();
1501                let metadata = metadata.clone();
1502                let metadata_scale_out = metadata_scale_out.clone();
1503                let metadata_scale_in = metadata_scale_in.clone();
1504                let db = db.clone();
1505                move |param, new_writer_rx| {
1506                    let metadata = metadata.clone();
1507                    let metadata_scale_out = metadata_scale_out.clone();
1508                    let metadata_scale_in = metadata_scale_in.clone();
1509                    let expected_param = expected_param.clone();
1510                    let db = db.clone();
1511                    tokio::spawn({
1512                        let subscriber = mock_subscriber.clone();
1513                        async move {
1514                            // validate the start request
1515                            assert_eq!(param, expected_param);
1516                            CoordinatorWorker::execute_coordinator(
1517                                db,
1518                                param.clone(),
1519                                new_writer_rx,
1520                                MockSinglePhaseCoordinator::new_coordinator(
1521                                    0,
1522                                    move |epoch, metadata_list, count: &mut usize| {
1523                                        *count += 1;
1524                                        let mut metadata_list =
1525                                            metadata_list
1526                                                .into_iter()
1527                                                .map(|metadata| match metadata {
1528                                                    SinkMetadata {
1529                                                        metadata:
1530                                                            Some(Metadata::Serialized(
1531                                                                SerializedMetadata { metadata },
1532                                                            )),
1533                                                    } => metadata,
1534                                                    _ => unreachable!(),
1535                                                })
1536                                                .collect_vec();
1537                                        metadata_list.sort();
1538                                        let (expected_epoch, expected_metadata_list) = match *count
1539                                        {
1540                                            1 => (epoch1, metadata[0].as_slice()),
1541                                            2 => (epoch2, metadata[1].as_slice()),
1542                                            3 => (epoch3, metadata_scale_out.as_slice()),
1543                                            4 => (epoch4, metadata_scale_in.as_slice()),
1544                                            _ => unreachable!(),
1545                                        };
1546                                        assert_eq!(expected_epoch, epoch);
1547                                        assert_eq!(expected_metadata_list, &metadata_list);
1548                                        Ok(())
1549                                    },
1550                                ),
1551                                subscriber.clone(),
1552                            )
1553                            .await;
1554                        }
1555                    })
1556                }
1557            });
1558
1559        let build_client = |vnode| async {
1560            CoordinatorStreamHandle::new_with_init_stream(param.to_proto(), vnode, |rx| async {
1561                Ok(tonic::Response::new(
1562                    manager
1563                        .handle_new_request(ReceiverStream::new(rx).map(Ok).boxed())
1564                        .await
1565                        .unwrap()
1566                        .boxed(),
1567                ))
1568            })
1569            .await
1570        };
1571
1572        let ((mut client1, _), (mut client2, _)) =
1573            try_join(build_client(vnode1), pin!(build_client(vnode2)))
1574                .await
1575                .unwrap();
1576
1577        let (aligned_epoch1, aligned_epoch2) = try_join(
1578            client1.align_initial_epoch(epoch1),
1579            client2.align_initial_epoch(epoch1),
1580        )
1581        .await
1582        .unwrap();
1583        assert_eq!(aligned_epoch1, epoch1);
1584        assert_eq!(aligned_epoch2, epoch1);
1585
1586        {
1587            // commit epoch1
1588            let mut commit_future = pin!(
1589                client2
1590                    .commit(
1591                        epoch1,
1592                        SinkMetadata {
1593                            metadata: Some(Metadata::Serialized(SerializedMetadata {
1594                                metadata: metadata[0][1].clone(),
1595                            })),
1596                        },
1597                        None,
1598                    )
1599                    .map(|result| result.unwrap())
1600            );
1601            assert!(
1602                poll_fn(|cx| Poll::Ready(commit_future.as_mut().poll(cx)))
1603                    .await
1604                    .is_pending()
1605            );
1606            join(
1607                commit_future,
1608                client1
1609                    .commit(
1610                        epoch1,
1611                        SinkMetadata {
1612                            metadata: Some(Metadata::Serialized(SerializedMetadata {
1613                                metadata: metadata[0][0].clone(),
1614                            })),
1615                        },
1616                        None,
1617                    )
1618                    .map(|result| result.unwrap()),
1619            )
1620            .await;
1621        }
1622
1623        let (vnode1, vnode2, vnode3) = {
1624            let (first, second) = all_vnode.split_at(VirtualNode::COUNT_FOR_TEST / 3);
1625            let (second, third) = second.split_at(VirtualNode::COUNT_FOR_TEST / 3);
1626            (
1627                build_bitmap(first),
1628                build_bitmap(second),
1629                build_bitmap(third),
1630            )
1631        };
1632
1633        let mut build_client3_future = pin!(build_client(vnode3));
1634        assert!(
1635            poll_fn(|cx| Poll::Ready(build_client3_future.as_mut().poll(cx)))
1636                .await
1637                .is_pending()
1638        );
1639        let mut client3;
1640        {
1641            {
1642                // commit epoch2
1643                let mut commit_future = pin!(
1644                    client1
1645                        .commit(
1646                            epoch2,
1647                            SinkMetadata {
1648                                metadata: Some(Metadata::Serialized(SerializedMetadata {
1649                                    metadata: metadata[1][0].clone(),
1650                                })),
1651                            },
1652                            None,
1653                        )
1654                        .map_err(Into::into)
1655                );
1656                assert!(
1657                    poll_fn(|cx| Poll::Ready(commit_future.as_mut().poll(cx)))
1658                        .await
1659                        .is_pending()
1660                );
1661                try_join(
1662                    commit_future,
1663                    client2.commit(
1664                        epoch2,
1665                        SinkMetadata {
1666                            metadata: Some(Metadata::Serialized(SerializedMetadata {
1667                                metadata: metadata[1][1].clone(),
1668                            })),
1669                        },
1670                        None,
1671                    ),
1672                )
1673                .await
1674                .unwrap();
1675            }
1676
1677            client3 = {
1678                let (
1679                    (client3, init_epoch),
1680                    (update_vnode_bitmap_epoch1, update_vnode_bitmap_epoch2),
1681                ) = try_join(
1682                    build_client3_future,
1683                    try_join(
1684                        client1.update_vnode_bitmap(&vnode1),
1685                        client2.update_vnode_bitmap(&vnode2),
1686                    )
1687                    .map_err(Into::into),
1688                )
1689                .await
1690                .unwrap();
1691                assert_eq!(init_epoch, Some(epoch2));
1692                assert_eq!(update_vnode_bitmap_epoch1, epoch2);
1693                assert_eq!(update_vnode_bitmap_epoch2, epoch2);
1694                client3
1695            };
1696            let mut commit_future3 = pin!(client3.commit(
1697                epoch3,
1698                SinkMetadata {
1699                    metadata: Some(Metadata::Serialized(SerializedMetadata {
1700                        metadata: metadata_scale_out[2].clone(),
1701                    })),
1702                },
1703                None,
1704            ));
1705            assert!(
1706                poll_fn(|cx| Poll::Ready(commit_future3.as_mut().poll(cx)))
1707                    .await
1708                    .is_pending()
1709            );
1710            let mut commit_future1 = pin!(client1.commit(
1711                epoch3,
1712                SinkMetadata {
1713                    metadata: Some(Metadata::Serialized(SerializedMetadata {
1714                        metadata: metadata_scale_out[0].clone(),
1715                    })),
1716                },
1717                None,
1718            ));
1719            assert!(
1720                poll_fn(|cx| Poll::Ready(commit_future1.as_mut().poll(cx)))
1721                    .await
1722                    .is_pending()
1723            );
1724            assert!(
1725                poll_fn(|cx| Poll::Ready(commit_future3.as_mut().poll(cx)))
1726                    .await
1727                    .is_pending()
1728            );
1729            try_join(
1730                client2.commit(
1731                    epoch3,
1732                    SinkMetadata {
1733                        metadata: Some(Metadata::Serialized(SerializedMetadata {
1734                            metadata: metadata_scale_out[1].clone(),
1735                        })),
1736                    },
1737                    None,
1738                ),
1739                try_join(commit_future1, commit_future3),
1740            )
1741            .await
1742            .unwrap();
1743        }
1744
1745        let (vnode2, vnode3) = {
1746            let (first, second) = all_vnode.split_at(VirtualNode::COUNT_FOR_TEST / 3);
1747            (build_bitmap(first), build_bitmap(second))
1748        };
1749
1750        {
1751            let (_, (update_vnode_bitmap_epoch2, update_vnode_bitmap_epoch3)) = try_join(
1752                client1.stop(),
1753                try_join(
1754                    client2.update_vnode_bitmap(&vnode2),
1755                    client3.update_vnode_bitmap(&vnode3),
1756                ),
1757            )
1758            .await
1759            .unwrap();
1760            assert_eq!(update_vnode_bitmap_epoch2, epoch3);
1761            assert_eq!(update_vnode_bitmap_epoch3, epoch3);
1762        }
1763
1764        {
1765            let mut commit_future = pin!(
1766                client2
1767                    .commit(
1768                        epoch4,
1769                        SinkMetadata {
1770                            metadata: Some(Metadata::Serialized(SerializedMetadata {
1771                                metadata: metadata_scale_in[0].clone(),
1772                            })),
1773                        },
1774                        None,
1775                    )
1776                    .map(|result| result.unwrap())
1777            );
1778            assert!(
1779                poll_fn(|cx| Poll::Ready(commit_future.as_mut().poll(cx)))
1780                    .await
1781                    .is_pending()
1782            );
1783            join(
1784                commit_future,
1785                client3
1786                    .commit(
1787                        epoch4,
1788                        SinkMetadata {
1789                            metadata: Some(Metadata::Serialized(SerializedMetadata {
1790                                metadata: metadata_scale_in[1].clone(),
1791                            })),
1792                        },
1793                        None,
1794                    )
1795                    .map(|result| result.unwrap()),
1796            )
1797            .await;
1798        }
1799    }
1800
1801    struct MockTwoPhaseCoordinator<
1802        P: FnMut(
1803            u64,
1804            Vec<SinkMetadata>,
1805            Option<PbSinkSchemaChange>,
1806        ) -> Result<Option<Vec<u8>>, SinkError>,
1807        CD: FnMut(u64, Vec<u8>) -> Result<(), SinkError>,
1808        CS: FnMut(u64, PbSinkSchemaChange) -> Result<(), SinkError>,
1809    > {
1810        pre_commit: P,
1811        commit_data: CD,
1812        commit_schema_change: CS,
1813    }
1814
1815    impl<
1816        P: FnMut(
1817                u64,
1818                Vec<SinkMetadata>,
1819                Option<PbSinkSchemaChange>,
1820            ) -> Result<Option<Vec<u8>>, SinkError>
1821            + Send
1822            + 'static,
1823        CD: FnMut(u64, Vec<u8>) -> Result<(), SinkError> + Send + 'static,
1824        CS: FnMut(u64, PbSinkSchemaChange) -> Result<(), SinkError> + Send + 'static,
1825    > MockTwoPhaseCoordinator<P, CD, CS>
1826    {
1827        fn new_coordinator(
1828            pre_commit: P,
1829            commit_data: CD,
1830            commit_schema_change: CS,
1831        ) -> SinkCommitCoordinator {
1832            SinkCommitCoordinator::TwoPhase(Box::new(MockTwoPhaseCoordinator {
1833                pre_commit,
1834                commit_data,
1835                commit_schema_change,
1836            }))
1837        }
1838    }
1839
1840    #[async_trait]
1841    impl<
1842        P: FnMut(
1843                u64,
1844                Vec<SinkMetadata>,
1845                Option<PbSinkSchemaChange>,
1846            ) -> Result<Option<Vec<u8>>, SinkError>
1847            + Send
1848            + 'static,
1849        CD: FnMut(u64, Vec<u8>) -> Result<(), SinkError> + Send + 'static,
1850        CS: FnMut(u64, PbSinkSchemaChange) -> Result<(), SinkError> + Send + 'static,
1851    > TwoPhaseCommitCoordinator for MockTwoPhaseCoordinator<P, CD, CS>
1852    {
1853        async fn init(&mut self) -> risingwave_connector::sink::Result<()> {
1854            Ok(())
1855        }
1856
1857        async fn pre_commit(
1858            &mut self,
1859            epoch: u64,
1860            metadata: Vec<SinkMetadata>,
1861            schema_change: Option<PbSinkSchemaChange>,
1862        ) -> risingwave_connector::sink::Result<Option<Vec<u8>>> {
1863            (self.pre_commit)(epoch, metadata, schema_change)
1864        }
1865
1866        async fn commit_data(
1867            &mut self,
1868            epoch: u64,
1869            commit_metadata: Vec<u8>,
1870        ) -> risingwave_connector::sink::Result<()> {
1871            (self.commit_data)(epoch, commit_metadata)
1872        }
1873
1874        async fn commit_schema_change(
1875            &mut self,
1876            epoch: u64,
1877            schema_change: PbSinkSchemaChange,
1878        ) -> risingwave_connector::sink::Result<()> {
1879            (self.commit_schema_change)(epoch, schema_change)
1880        }
1881
1882        async fn abort(&mut self, _epoch: u64, _commit_metadata: Vec<u8>) {
1883            tracing::debug!("abort called");
1884        }
1885    }
1886
1887    async fn prepare_db_backend() -> DatabaseConnection {
1888        let db: DatabaseConnection = Database::connect("sqlite::memory:").await.unwrap();
1889        let ddl = "
1890            CREATE TABLE IF NOT EXISTS pending_sink_state (
1891                sink_id i32 NOT NULL,
1892                epoch i64 NOT NULL,
1893                sink_state STRING NOT NULL,
1894                metadata BLOB,
1895                schema_change BLOB,
1896                PRIMARY KEY (sink_id, epoch)
1897            )
1898        ";
1899        db.execute(sea_orm::Statement::from_string(
1900            db.get_database_backend(),
1901            ddl.to_owned(),
1902        ))
1903        .await
1904        .unwrap();
1905        db
1906    }
1907
1908    async fn list_rows(
1909        db: &DatabaseConnection,
1910    ) -> Vec<(
1911        i32,
1912        i64,
1913        String,
1914        Option<Vec<u8>>,
1915        Option<PbSinkSchemaChange>,
1916    )> {
1917        let sql =
1918            "SELECT sink_id, epoch, sink_state, metadata, schema_change FROM pending_sink_state";
1919        let rows = db
1920            .query_all(sea_orm::Statement::from_string(
1921                db.get_database_backend(),
1922                sql.to_owned(),
1923            ))
1924            .await
1925            .unwrap();
1926        rows.into_iter()
1927            .map(|row| {
1928                (
1929                    row.try_get("", "sink_id").unwrap(),
1930                    row.try_get("", "epoch").unwrap(),
1931                    row.try_get("", "sink_state").unwrap(),
1932                    row.try_get("", "metadata").unwrap(),
1933                    row.try_get::<Option<SinkSchemachange>>("", "schema_change")
1934                        .unwrap()
1935                        .map(|v| v.to_protobuf()),
1936                )
1937            })
1938            .collect()
1939    }
1940
1941    async fn set_epoch_aborted(db: &DatabaseConnection, sink_id: SinkId, epoch: u64) {
1942        let sql = format!(
1943            "UPDATE pending_sink_state SET sink_state = 'ABORTED' WHERE sink_id = {} AND epoch = {}",
1944            sink_id, epoch as i64
1945        );
1946        db.execute(sea_orm::Statement::from_string(
1947            db.get_database_backend(),
1948            sql,
1949        ))
1950        .await
1951        .unwrap();
1952    }
1953
1954    #[tokio::test]
1955    async fn test_init_response_skips_recovered_empty_pending_epoch() {
1956        let db = prepare_db_backend().await;
1957
1958        let param = SinkParam {
1959            sink_id: SinkId::from(1),
1960            sink_name: "test".into(),
1961            properties: Default::default(),
1962            columns: vec![],
1963            downstream_pk: None,
1964            sink_type: SinkType::AppendOnly,
1965            ignore_delete: false,
1966            format_desc: None,
1967            db_name: "test".into(),
1968            sink_from_name: "test".into(),
1969        };
1970
1971        let epoch1 = 233;
1972
1973        let sql = format!(
1974            "INSERT INTO pending_sink_state (sink_id, epoch, sink_state) VALUES ({}, {}, 'PENDING')",
1975            param.sink_id, epoch1
1976        );
1977        db.execute(sea_orm::Statement::from_string(
1978            db.get_database_backend(),
1979            sql,
1980        ))
1981        .await
1982        .unwrap();
1983
1984        let all_vnode = (0..VirtualNode::COUNT_FOR_TEST).collect_vec();
1985        let build_bitmap = |indexes: &[usize]| {
1986            let mut builder = BitmapBuilder::zeroed(VirtualNode::COUNT_FOR_TEST);
1987            for i in indexes {
1988                builder.set(*i, true);
1989            }
1990            builder.finish()
1991        };
1992        let vnode = build_bitmap(&all_vnode);
1993
1994        let sender = Arc::new(tokio::sync::Mutex::new(None));
1995        let mock_subscriber: SinkCommittedEpochSubscriber = {
1996            let captured_sender = sender.clone();
1997            Arc::new(move |_sink_id: SinkId| {
1998                let (epoch_sender, receiver) = unbounded_channel();
1999                let captured_sender = captured_sender.clone();
2000                async move {
2001                    let mut guard = captured_sender.lock().await;
2002                    *guard = Some(epoch_sender);
2003                    Ok((epoch1, receiver))
2004                }
2005                .boxed()
2006            })
2007        };
2008
2009        let pre_commit_attempt = Arc::new(AtomicI32::new(0));
2010        let commit_attempt = Arc::new(AtomicI32::new(0));
2011        let (manager, (_join_handle, _stop_tx)) =
2012            SinkCoordinatorManager::start_worker_with_spawn_worker({
2013                let expected_param = param.clone();
2014                let db = db.clone();
2015                let pre_commit_attempt = pre_commit_attempt.clone();
2016                let commit_attempt = commit_attempt.clone();
2017                move |param, new_writer_rx| {
2018                    let expected_param = expected_param.clone();
2019                    let db = db.clone();
2020                    let pre_commit_attempt = pre_commit_attempt.clone();
2021                    let commit_attempt = commit_attempt.clone();
2022                    tokio::spawn({
2023                        let subscriber = mock_subscriber.clone();
2024                        async move {
2025                            assert_eq!(param, expected_param);
2026                            CoordinatorWorker::execute_coordinator(
2027                                db,
2028                                param.clone(),
2029                                new_writer_rx,
2030                                MockTwoPhaseCoordinator::new_coordinator(
2031                                    move |_epoch, _metadata_list, _schema_change| {
2032                                        pre_commit_attempt.fetch_add(
2033                                            1,
2034                                            std::sync::atomic::Ordering::SeqCst,
2035                                        );
2036                                        Err(SinkError::Coordinator(anyhow!(
2037                                            "pre_commit should not be called for a known pending epoch"
2038                                        )))
2039                                    },
2040                                    move |_epoch, _commit_metadata| {
2041                                        commit_attempt
2042                                            .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2043                                        Ok(())
2044                                    },
2045                                    move |_epoch, _schema_change| unreachable!(),
2046                                ),
2047                                subscriber.clone(),
2048                            )
2049                            .await;
2050                        }
2051                    })
2052                }
2053            });
2054
2055        let (_client, log_store_rewind_start_epoch) =
2056            CoordinatorStreamHandle::new_with_init_stream(param.to_proto(), vnode, |rx| async {
2057                Ok(tonic::Response::new(
2058                    manager
2059                        .handle_new_request(ReceiverStream::new(rx).map(Ok).boxed())
2060                        .await
2061                        .unwrap()
2062                        .boxed(),
2063                ))
2064            })
2065            .await
2066            .unwrap();
2067        assert_eq!(log_store_rewind_start_epoch, Some(epoch1));
2068
2069        for _ in 0..50 {
2070            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2071            let rows = list_rows(&db).await;
2072            if rows[0].2 == "COMMITTED" {
2073                break;
2074            }
2075        }
2076
2077        assert_eq!(
2078            pre_commit_attempt.load(std::sync::atomic::Ordering::SeqCst),
2079            0
2080        );
2081        assert_eq!(commit_attempt.load(std::sync::atomic::Ordering::SeqCst), 0);
2082        let rows = list_rows(&db).await;
2083        assert_eq!(rows.len(), 1);
2084        assert_eq!(rows[0].1, epoch1 as i64);
2085        assert_eq!(rows[0].2, "COMMITTED");
2086        assert_eq!(rows[0].3, None);
2087    }
2088
2089    #[tokio::test]
2090    async fn test_pre_commit_failed() {
2091        let db = prepare_db_backend().await;
2092
2093        let param = SinkParam {
2094            sink_id: SinkId::from(1),
2095            sink_name: "test".into(),
2096            properties: Default::default(),
2097            columns: vec![],
2098            downstream_pk: None,
2099            sink_type: SinkType::AppendOnly,
2100            ignore_delete: false,
2101            format_desc: None,
2102            db_name: "test".into(),
2103            sink_from_name: "test".into(),
2104        };
2105
2106        let epoch1 = 233;
2107
2108        let all_vnode = (0..VirtualNode::COUNT_FOR_TEST).collect_vec();
2109        let build_bitmap = |indexes: &[usize]| {
2110            let mut builder = BitmapBuilder::zeroed(VirtualNode::COUNT_FOR_TEST);
2111            for i in indexes {
2112                builder.set(*i, true);
2113            }
2114            builder.finish()
2115        };
2116        let vnode = build_bitmap(&all_vnode);
2117
2118        let metadata = vec![1u8, 2u8];
2119        let sender = Arc::new(tokio::sync::Mutex::new(None));
2120        let mock_subscriber: SinkCommittedEpochSubscriber = {
2121            let captured_sender = sender.clone();
2122            Arc::new(move |_sink_id: SinkId| {
2123                let (sender, receiver) = unbounded_channel();
2124                let captured_sender = captured_sender.clone();
2125                async move {
2126                    let mut guard = captured_sender.lock().await;
2127                    *guard = Some(sender);
2128                    Ok((epoch1, receiver))
2129                }
2130                .boxed()
2131            })
2132        };
2133
2134        let (manager, (_join_handle, _stop_tx)) =
2135            SinkCoordinatorManager::start_worker_with_spawn_worker({
2136                let expected_param = param.clone();
2137                let db = db.clone();
2138                move |param, new_writer_rx| {
2139                    let expected_param = expected_param.clone();
2140                    let db = db.clone();
2141                    tokio::spawn({
2142                        let subscriber = mock_subscriber.clone();
2143                        async move {
2144                            // validate the start request
2145                            assert_eq!(param, expected_param);
2146                            CoordinatorWorker::execute_coordinator(
2147                                db,
2148                                param.clone(),
2149                                new_writer_rx,
2150                                MockTwoPhaseCoordinator::new_coordinator(
2151                                    move |_epoch, _metadata_list, _schema_change| {
2152                                        Err(SinkError::Coordinator(anyhow!("failed to pre commit")))
2153                                    },
2154                                    move |_epoch, _commit_metadata| unreachable!(),
2155                                    move |_epoch, _schema_change| unreachable!(),
2156                                ),
2157                                subscriber.clone(),
2158                            )
2159                            .await;
2160                        }
2161                    })
2162                }
2163            });
2164
2165        let build_client = |vnode| async {
2166            CoordinatorStreamHandle::new_with_init_stream(param.to_proto(), vnode, |rx| async {
2167                Ok(tonic::Response::new(
2168                    manager
2169                        .handle_new_request(ReceiverStream::new(rx).map(Ok).boxed())
2170                        .await
2171                        .unwrap()
2172                        .boxed(),
2173                ))
2174            })
2175            .await
2176            .unwrap()
2177            .0
2178        };
2179
2180        let mut client = build_client(vnode).await;
2181
2182        let aligned_epoch = client.align_initial_epoch(1).await.unwrap();
2183        assert_eq!(aligned_epoch, 1);
2184
2185        let commit_result = client
2186            .commit(
2187                epoch1,
2188                SinkMetadata {
2189                    metadata: Some(Metadata::Serialized(SerializedMetadata {
2190                        metadata: metadata.clone(),
2191                    })),
2192                },
2193                None,
2194            )
2195            .await;
2196        assert!(commit_result.is_err());
2197
2198        let rows = list_rows(&db).await;
2199        assert!(rows.is_empty());
2200    }
2201
2202    #[tokio::test]
2203    async fn test_empty_pre_commit_waiting_on_checkpoint() {
2204        let db = prepare_db_backend().await;
2205
2206        let param = SinkParam {
2207            sink_id: SinkId::from(1),
2208            sink_name: "test".into(),
2209            properties: Default::default(),
2210            columns: vec![],
2211            downstream_pk: None,
2212            sink_type: SinkType::AppendOnly,
2213            ignore_delete: false,
2214            format_desc: None,
2215            db_name: "test".into(),
2216            sink_from_name: "test".into(),
2217        };
2218
2219        let epoch0 = 232;
2220        let epoch1 = 233;
2221
2222        let all_vnode = (0..VirtualNode::COUNT_FOR_TEST).collect_vec();
2223        let build_bitmap = |indexes: &[usize]| {
2224            let mut builder = BitmapBuilder::zeroed(VirtualNode::COUNT_FOR_TEST);
2225            for i in indexes {
2226                builder.set(*i, true);
2227            }
2228            builder.finish()
2229        };
2230        let vnode = build_bitmap(&all_vnode);
2231
2232        let metadata = vec![1u8, 2u8];
2233
2234        let sender = Arc::new(tokio::sync::Mutex::new(None));
2235        let mock_subscriber: SinkCommittedEpochSubscriber = {
2236            let captured_sender = sender.clone();
2237            Arc::new(move |_sink_id: SinkId| {
2238                let (sender, receiver) = unbounded_channel();
2239                let captured_sender = captured_sender.clone();
2240                async move {
2241                    let mut guard = captured_sender.lock().await;
2242                    *guard = Some(sender);
2243                    Ok((epoch0, receiver))
2244                }
2245                .boxed()
2246            })
2247        };
2248
2249        let (manager, (_join_handle, _stop_tx)) =
2250            SinkCoordinatorManager::start_worker_with_spawn_worker({
2251                let expected_param = param.clone();
2252                let db = db.clone();
2253                move |param, new_writer_rx| {
2254                    let expected_param = expected_param.clone();
2255                    let db = db.clone();
2256                    tokio::spawn({
2257                        let subscriber = mock_subscriber.clone();
2258                        async move {
2259                            // validate the start request
2260                            assert_eq!(param, expected_param);
2261                            CoordinatorWorker::execute_coordinator(
2262                                db,
2263                                param.clone(),
2264                                new_writer_rx,
2265                                MockTwoPhaseCoordinator::new_coordinator(
2266                                    move |_epoch, metadata_list, _schema_change| {
2267                                        assert_eq!(metadata_list.len(), 1);
2268                                        Ok(None)
2269                                    },
2270                                    move |_epoch, _commit_metadata| unreachable!(),
2271                                    move |_epoch, _schema_change| unreachable!(),
2272                                ),
2273                                subscriber.clone(),
2274                            )
2275                            .await;
2276                        }
2277                    })
2278                }
2279            });
2280
2281        let build_client = |vnode| async {
2282            CoordinatorStreamHandle::new_with_init_stream(param.to_proto(), vnode, |rx| async {
2283                Ok(tonic::Response::new(
2284                    manager
2285                        .handle_new_request(ReceiverStream::new(rx).map(Ok).boxed())
2286                        .await
2287                        .unwrap()
2288                        .boxed(),
2289                ))
2290            })
2291            .await
2292            .unwrap()
2293            .0
2294        };
2295
2296        let mut client = build_client(vnode).await;
2297
2298        let aligned_epoch = client.align_initial_epoch(1).await.unwrap();
2299        assert_eq!(aligned_epoch, 1);
2300
2301        client
2302            .commit(
2303                epoch1,
2304                SinkMetadata {
2305                    metadata: Some(Metadata::Serialized(SerializedMetadata {
2306                        metadata: metadata.clone(),
2307                    })),
2308                },
2309                None,
2310            )
2311            .await
2312            .unwrap();
2313
2314        {
2315            let rows = list_rows(&db).await;
2316            assert_eq!(rows.len(), 1);
2317            assert_eq!(rows[0].1, epoch1 as i64);
2318            assert_eq!(rows[0].2, "PENDING");
2319            assert_eq!(rows[0].3, None);
2320
2321            let guard = sender.lock().await;
2322            let sender = guard.as_ref().unwrap().clone();
2323            sender.send(epoch1).unwrap();
2324        }
2325
2326        // wait max 5 seconds for the commit to be processed
2327        for _ in 0..50 {
2328            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2329            let rows = list_rows(&db).await;
2330            if rows[0].2 == "COMMITTED" {
2331                break;
2332            }
2333        }
2334
2335        {
2336            let rows = list_rows(&db).await;
2337            assert_eq!(rows.len(), 1);
2338            assert_eq!(rows[0].1, epoch1 as i64);
2339            assert_eq!(rows[0].2, "COMMITTED");
2340            assert_eq!(rows[0].3, None);
2341        }
2342    }
2343
2344    #[tokio::test]
2345    async fn test_commit_retry_loop() {
2346        let db = prepare_db_backend().await;
2347
2348        let param = SinkParam {
2349            sink_id: SinkId::from(1),
2350            sink_name: "test".into(),
2351            properties: Default::default(),
2352            columns: vec![],
2353            downstream_pk: None,
2354            sink_type: SinkType::AppendOnly,
2355            ignore_delete: false,
2356            format_desc: None,
2357            db_name: "test".into(),
2358            sink_from_name: "test".into(),
2359        };
2360
2361        let epoch1 = 233;
2362
2363        let all_vnode = (0..VirtualNode::COUNT_FOR_TEST).collect_vec();
2364        let build_bitmap = |indexes: &[usize]| {
2365            let mut builder = BitmapBuilder::zeroed(VirtualNode::COUNT_FOR_TEST);
2366            for i in indexes {
2367                builder.set(*i, true);
2368            }
2369            builder.finish()
2370        };
2371        let vnode = build_bitmap(&all_vnode);
2372
2373        let metadata = vec![1u8, 2u8];
2374        let sender = Arc::new(tokio::sync::Mutex::new(None));
2375        let mock_subscriber: SinkCommittedEpochSubscriber = {
2376            let captured_sender = sender.clone();
2377            Arc::new(move |_sink_id: SinkId| {
2378                let (sender, receiver) = unbounded_channel();
2379                let captured_sender = captured_sender.clone();
2380                async move {
2381                    let mut guard = captured_sender.lock().await;
2382                    *guard = Some(sender);
2383                    Ok((epoch1, receiver))
2384                }
2385                .boxed()
2386            })
2387        };
2388
2389        let commit_attempt = Arc::new(AtomicI32::new(0));
2390
2391        let (manager, (_join_handle, _stop_tx)) =
2392            SinkCoordinatorManager::start_worker_with_spawn_worker({
2393                let expected_param = param.clone();
2394                let metadata = metadata.clone();
2395                let db = db.clone();
2396                let commit_attempt = commit_attempt.clone();
2397                move |param, new_writer_rx| {
2398                    let metadata = metadata.clone();
2399                    let expected_param = expected_param.clone();
2400                    let db = db.clone();
2401                    let commit_attempt = commit_attempt.clone();
2402                    tokio::spawn({
2403                        let subscriber = mock_subscriber.clone();
2404                        async move {
2405                            // validate the start request
2406                            assert_eq!(param, expected_param);
2407                            CoordinatorWorker::execute_coordinator(
2408                                db,
2409                                param.clone(),
2410                                new_writer_rx,
2411                                MockTwoPhaseCoordinator::new_coordinator(
2412                                    move |_epoch, metadata_list, _schema_change| {
2413                                        let metadata =
2414                                            Itertools::exactly_one(metadata_list.into_iter())
2415                                                .unwrap();
2416                                        Ok(match metadata.metadata {
2417                                            Some(Metadata::Serialized(SerializedMetadata {
2418                                                metadata,
2419                                            })) => Some(metadata),
2420                                            _ => unreachable!(),
2421                                        })
2422                                    },
2423                                    move |_epoch, commit_metadata| {
2424                                        assert_eq!(commit_metadata, metadata);
2425                                        if commit_attempt
2426                                            .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
2427                                            < 2
2428                                        {
2429                                            Err(SinkError::Coordinator(anyhow!("failed to commit")))
2430                                        } else {
2431                                            Ok(())
2432                                        }
2433                                    },
2434                                    move |_epoch, _schema_change| unreachable!(),
2435                                ),
2436                                subscriber.clone(),
2437                            )
2438                            .await;
2439                        }
2440                    })
2441                }
2442            });
2443
2444        let build_client = |vnode| async {
2445            CoordinatorStreamHandle::new_with_init_stream(param.to_proto(), vnode, |rx| async {
2446                Ok(tonic::Response::new(
2447                    manager
2448                        .handle_new_request(ReceiverStream::new(rx).map(Ok).boxed())
2449                        .await
2450                        .unwrap()
2451                        .boxed(),
2452                ))
2453            })
2454            .await
2455            .unwrap()
2456            .0
2457        };
2458
2459        let mut client = build_client(vnode).await;
2460
2461        let aligned_epoch = client.align_initial_epoch(1).await.unwrap();
2462        assert_eq!(aligned_epoch, 1);
2463
2464        client
2465            .commit(
2466                epoch1,
2467                SinkMetadata {
2468                    metadata: Some(Metadata::Serialized(SerializedMetadata {
2469                        metadata: metadata.clone(),
2470                    })),
2471                },
2472                None,
2473            )
2474            .await
2475            .unwrap();
2476
2477        // wait max 10 seconds for the commit to be processed
2478        for _ in 0..100 {
2479            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2480            let rows = list_rows(&db).await;
2481            if rows[0].2 == "COMMITTED" {
2482                break;
2483            }
2484        }
2485
2486        assert_eq!(commit_attempt.load(std::sync::atomic::Ordering::SeqCst), 3);
2487
2488        {
2489            let rows = list_rows(&db).await;
2490            assert_eq!(rows.len(), 1);
2491            assert_eq!(rows[0].1, epoch1 as i64);
2492            assert_eq!(rows[0].2, "COMMITTED");
2493        }
2494    }
2495
2496    #[tokio::test]
2497    async fn test_aborted() {
2498        let db = prepare_db_backend().await;
2499
2500        let param = SinkParam {
2501            sink_id: SinkId::from(1),
2502            sink_name: "test".into(),
2503            properties: Default::default(),
2504            columns: vec![],
2505            downstream_pk: None,
2506            sink_type: SinkType::AppendOnly,
2507            ignore_delete: false,
2508            format_desc: None,
2509            db_name: "test".into(),
2510            sink_from_name: "test".into(),
2511        };
2512
2513        let epoch0 = 232;
2514        let epoch1 = 233;
2515
2516        let all_vnode = (0..VirtualNode::COUNT_FOR_TEST).collect_vec();
2517        let build_bitmap = |indexes: &[usize]| {
2518            let mut builder = BitmapBuilder::zeroed(VirtualNode::COUNT_FOR_TEST);
2519            for i in indexes {
2520                builder.set(*i, true);
2521            }
2522            builder.finish()
2523        };
2524        let vnode = build_bitmap(&all_vnode);
2525
2526        let metadata = vec![1u8, 2u8];
2527
2528        let sender = Arc::new(tokio::sync::Mutex::new(None));
2529        let mock_subscriber: SinkCommittedEpochSubscriber = {
2530            let captured_sender = sender.clone();
2531            Arc::new(move |_sink_id: SinkId| {
2532                let (sender, receiver) = unbounded_channel();
2533                let captured_sender = captured_sender.clone();
2534                async move {
2535                    let mut guard = captured_sender.lock().await;
2536                    *guard = Some(sender);
2537                    Ok((epoch0, receiver))
2538                }
2539                .boxed()
2540            })
2541        };
2542
2543        let (manager, (_join_handle, _stop_tx)) =
2544            SinkCoordinatorManager::start_worker_with_spawn_worker({
2545                let expected_param = param.clone();
2546                let metadata = metadata.clone();
2547                let db = db.clone();
2548                move |param, new_writer_rx| {
2549                    let metadata = metadata.clone();
2550                    let expected_param = expected_param.clone();
2551                    let db = db.clone();
2552                    tokio::spawn({
2553                        let subscriber = mock_subscriber.clone();
2554                        async move {
2555                            // validate the start request
2556                            assert_eq!(param, expected_param);
2557                            CoordinatorWorker::execute_coordinator(
2558                                db,
2559                                param.clone(),
2560                                new_writer_rx,
2561                                MockTwoPhaseCoordinator::new_coordinator(
2562                                    move |_epoch, metadata_list, _schema_change| {
2563                                        let metadata =
2564                                            Itertools::exactly_one(metadata_list.into_iter())
2565                                                .unwrap();
2566                                        Ok(match metadata.metadata {
2567                                            Some(Metadata::Serialized(SerializedMetadata {
2568                                                metadata,
2569                                            })) => Some(metadata),
2570                                            _ => unreachable!(),
2571                                        })
2572                                    },
2573                                    move |_epoch, commit_metadata| {
2574                                        assert_eq!(commit_metadata, metadata);
2575                                        Ok(())
2576                                    },
2577                                    move |_epoch, _schema_change| unreachable!(),
2578                                ),
2579                                subscriber.clone(),
2580                            )
2581                            .await;
2582                        }
2583                    })
2584                }
2585            });
2586
2587        let build_client = |vnode| async {
2588            CoordinatorStreamHandle::new_with_init_stream(param.to_proto(), vnode, |rx| async {
2589                Ok(tonic::Response::new(
2590                    manager
2591                        .handle_new_request(ReceiverStream::new(rx).map(Ok).boxed())
2592                        .await
2593                        .unwrap()
2594                        .boxed(),
2595                ))
2596            })
2597            .await
2598            .unwrap()
2599            .0
2600        };
2601
2602        let mut client = build_client(vnode.clone()).await;
2603
2604        let aligned_epoch = client.align_initial_epoch(1).await.unwrap();
2605        assert_eq!(aligned_epoch, 1);
2606
2607        client
2608            .commit(
2609                epoch1,
2610                SinkMetadata {
2611                    metadata: Some(Metadata::Serialized(SerializedMetadata {
2612                        metadata: metadata.clone(),
2613                    })),
2614                },
2615                None,
2616            )
2617            .await
2618            .unwrap();
2619
2620        manager
2621            .stop_sink_coordinators_for_jobs(vec![JobId::from(1)])
2622            .await;
2623
2624        {
2625            let rows = list_rows(&db).await;
2626            assert_eq!(rows.len(), 1);
2627            assert_eq!(rows[0].1, epoch1 as i64);
2628            assert_eq!(rows[0].2, "PENDING");
2629
2630            set_epoch_aborted(&db, SinkId::from(1), epoch1).await;
2631            let rows = list_rows(&db).await;
2632            assert_eq!(rows.len(), 1);
2633            assert_eq!(rows[0].1, epoch1 as i64);
2634            assert_eq!(rows[0].2, "ABORTED");
2635        }
2636
2637        let mut client = build_client(vnode).await;
2638
2639        let aligned_epoch = client.align_initial_epoch(1).await.unwrap();
2640        assert_eq!(aligned_epoch, 1);
2641
2642        {
2643            let rows = list_rows(&db).await;
2644            assert!(rows.is_empty());
2645        }
2646    }
2647
2648    #[tokio::test]
2649    async fn test_flush_when_reschedule() {
2650        let db = prepare_db_backend().await;
2651
2652        let param = SinkParam {
2653            sink_id: SinkId::from(1),
2654            sink_name: "test".into(),
2655            properties: Default::default(),
2656            columns: vec![],
2657            downstream_pk: None,
2658            sink_type: SinkType::AppendOnly,
2659            ignore_delete: false,
2660            format_desc: None,
2661            db_name: "test".into(),
2662            sink_from_name: "test".into(),
2663        };
2664
2665        let epoch0 = 232;
2666        let epoch1 = 233;
2667
2668        let all_vnode = (0..VirtualNode::COUNT_FOR_TEST).collect_vec();
2669        let build_bitmap = |indexes: &[usize]| {
2670            let mut builder = BitmapBuilder::zeroed(VirtualNode::COUNT_FOR_TEST);
2671            for i in indexes {
2672                builder.set(*i, true);
2673            }
2674            builder.finish()
2675        };
2676        let vnode = build_bitmap(&all_vnode);
2677
2678        let metadata = vec![1u8, 2u8];
2679        let schema_change = PbSinkSchemaChange {
2680            original_schema: vec![PbField {
2681                data_type: Some(PbDataType {
2682                    type_name: PbTypeName::Int32 as i32,
2683                    ..Default::default()
2684                }),
2685                name: "col_v1".into(),
2686            }],
2687            op: Some(SinkSchemachangeOp::AddColumns(PbSinkAddColumnsOp {
2688                fields: vec![PbField {
2689                    data_type: Some(PbDataType {
2690                        type_name: PbTypeName::Varchar as i32,
2691                        ..Default::default()
2692                    }),
2693                    name: "new_col".into(),
2694                }],
2695            })),
2696        };
2697
2698        let sender = Arc::new(tokio::sync::Mutex::new(None));
2699        let mock_subscriber: SinkCommittedEpochSubscriber = {
2700            let captured_sender = sender.clone();
2701            Arc::new(move |_sink_id: SinkId| {
2702                let (sender, receiver) = unbounded_channel();
2703                let captured_sender = captured_sender.clone();
2704                async move {
2705                    let mut guard = captured_sender.lock().await;
2706                    *guard = Some(sender);
2707                    Ok((epoch0, receiver))
2708                }
2709                .boxed()
2710            })
2711        };
2712
2713        let (manager, (_join_handle, _stop_tx)) =
2714            SinkCoordinatorManager::start_worker_with_spawn_worker({
2715                let expected_param = param.clone();
2716                let metadata = metadata.clone();
2717                let schema_change = schema_change.clone();
2718                let db = db.clone();
2719                move |param, new_writer_rx| {
2720                    let metadata = metadata.clone();
2721                    let schema_change_for_pre_commit = schema_change.clone();
2722                    let schema_change_for_commit = schema_change.clone();
2723                    let expected_param = expected_param.clone();
2724                    let db = db.clone();
2725                    tokio::spawn({
2726                        let subscriber = mock_subscriber.clone();
2727                        async move {
2728                            assert_eq!(param, expected_param);
2729                            CoordinatorWorker::execute_coordinator(
2730                                db,
2731                                param.clone(),
2732                                new_writer_rx,
2733                                MockTwoPhaseCoordinator::new_coordinator(
2734                                    move |_epoch, metadata_list, schema_change| {
2735                                        assert_eq!(
2736                                            schema_change,
2737                                            Some(schema_change_for_pre_commit.clone())
2738                                        );
2739                                        let metadata =
2740                                            Itertools::exactly_one(metadata_list.into_iter())
2741                                                .unwrap();
2742                                        Ok(match metadata.metadata {
2743                                            Some(Metadata::Serialized(SerializedMetadata {
2744                                                metadata,
2745                                            })) => Some(metadata),
2746                                            _ => unreachable!(),
2747                                        })
2748                                    },
2749                                    move |_epoch, commit_metadata| {
2750                                        assert_eq!(commit_metadata, metadata);
2751                                        Ok(())
2752                                    },
2753                                    move |_epoch, schema_change| {
2754                                        assert_eq!(schema_change, schema_change_for_commit.clone());
2755                                        Ok(())
2756                                    },
2757                                ),
2758                                subscriber.clone(),
2759                            )
2760                            .await;
2761                        }
2762                    })
2763                }
2764            });
2765
2766        let build_client = |vnode| async {
2767            CoordinatorStreamHandle::new_with_init_stream(param.to_proto(), vnode, |rx| async {
2768                Ok(tonic::Response::new(
2769                    manager
2770                        .handle_new_request(ReceiverStream::new(rx).map(Ok).boxed())
2771                        .await
2772                        .unwrap()
2773                        .boxed(),
2774                ))
2775            })
2776            .await
2777        };
2778
2779        let (mut client1, _) = build_client(vnode.clone()).await.unwrap();
2780
2781        let aligned_epoch = client1.align_initial_epoch(1).await.unwrap();
2782        assert_eq!(aligned_epoch, 1);
2783
2784        client1
2785            .commit(
2786                epoch1,
2787                SinkMetadata {
2788                    metadata: Some(Metadata::Serialized(SerializedMetadata {
2789                        metadata: metadata.clone(),
2790                    })),
2791                },
2792                Some(schema_change.clone()),
2793            )
2794            .await
2795            .unwrap();
2796
2797        {
2798            let rows = list_rows(&db).await;
2799            assert_eq!(rows.len(), 1);
2800            assert_eq!(rows[0].1, epoch1 as i64);
2801            assert_eq!(rows[0].2, "PENDING");
2802            assert_eq!(rows[0].4, Some(schema_change.clone()));
2803        }
2804
2805        let mut build_client2_future = pin!(build_client(vnode.clone()));
2806        assert!(
2807            poll_fn(|cx| Poll::Ready(build_client2_future.as_mut().poll(cx)))
2808                .await
2809                .is_pending()
2810        );
2811
2812        client1.stop().await.unwrap();
2813
2814        assert!(
2815            poll_fn(|cx| Poll::Ready(build_client2_future.as_mut().poll(cx)))
2816                .await
2817                .is_pending()
2818        );
2819
2820        {
2821            let guard = sender.lock().await;
2822            let sender = guard.as_ref().unwrap().clone();
2823            sender.send(epoch1).unwrap();
2824        }
2825
2826        let (_, init_epoch) = build_client2_future.await.unwrap();
2827        assert_eq!(init_epoch, Some(epoch1));
2828
2829        {
2830            let rows = list_rows(&db).await;
2831            assert_eq!(rows.len(), 1);
2832            assert_eq!(rows[0].1, epoch1 as i64);
2833            assert_eq!(rows[0].2, "COMMITTED");
2834            assert_eq!(rows[0].4, Some(schema_change.clone()));
2835        }
2836    }
2837}