risingwave_connector/sink/iceberg/
commit_retry.rs1use std::future::Future;
21use std::sync::Arc;
22use std::time::Duration;
23
24use anyhow::{Result, anyhow, bail};
25use iceberg::table::Table;
26use iceberg::{Catalog, TableIdent};
27use risingwave_common::util::retry::exponential_backoff;
28use thiserror_ext::AsReport;
29use tokio_retry::RetryIf;
30use tokio_retry::strategy::jitter;
31
32#[derive(Clone, Debug)]
33pub struct CommitRetryLogContext {
34 pub iceberg_component: &'static str,
35 pub iceberg_operation: &'static str,
36 pub table: String,
37 pub branch: String,
38 pub sink_id: Option<String>,
39 pub epoch: Option<u64>,
40 pub snapshot_id: Option<i64>,
41}
42
43impl CommitRetryLogContext {
44 pub fn new(
45 iceberg_component: &'static str,
46 iceberg_operation: &'static str,
47 table: impl Into<String>,
48 branch: impl Into<String>,
49 ) -> Self {
50 Self {
51 iceberg_component,
52 iceberg_operation,
53 table: table.into(),
54 branch: branch.into(),
55 sink_id: None,
56 epoch: None,
57 snapshot_id: None,
58 }
59 }
60
61 pub fn with_sink_id(mut self, sink_id: impl ToString) -> Self {
62 self.sink_id = Some(sink_id.to_string());
63 self
64 }
65
66 pub fn with_epoch(mut self, epoch: u64) -> Self {
67 self.epoch = Some(epoch);
68 self
69 }
70
71 pub fn with_snapshot_id(mut self, snapshot_id: i64) -> Self {
72 self.snapshot_id = Some(snapshot_id);
73 self
74 }
75}
76
77pub enum CommitError {
79 ReloadTable(anyhow::Error),
82 Commit(anyhow::Error),
85}
86
87pub async fn reload_table(
92 catalog: &dyn Catalog,
93 table_ident: &TableIdent,
94 schema_id: i32,
95 partition_spec_id: i32,
96) -> Result<Table> {
97 let table = catalog
98 .load_table(table_ident)
99 .await
100 .map_err(|e| anyhow!(e).context("reload iceberg table"))?;
101 if table.metadata().current_schema_id() != schema_id {
102 bail!(
103 "iceberg sink: schema evolution not supported; expect schema id {}, got {}",
104 schema_id,
105 table.metadata().current_schema_id(),
106 );
107 }
108 if table.metadata().default_partition_spec_id() != partition_spec_id {
109 bail!(
110 "iceberg sink: partition evolution not supported; expect partition spec id {}, got {}",
111 partition_spec_id,
112 table.metadata().default_partition_spec_id(),
113 );
114 }
115 Ok(table)
116}
117
118pub async fn run_with_retry<F, Fut, Out>(
125 catalog: Arc<dyn Catalog>,
126 table_ident: TableIdent,
127 schema_id: i32,
128 partition_spec_id: i32,
129 retry_num: usize,
130 log_context: CommitRetryLogContext,
131 commit_action: F,
132) -> Result<Out>
133where
134 F: Fn(Table) -> Fut + Send + Sync,
135 Fut: Future<Output = Result<Out, CommitError>> + Send,
136{
137 let retry_strategy =
138 exponential_backoff(Duration::from_millis(10), 10, Duration::from_secs(60))
139 .map(jitter)
140 .take(retry_num);
141
142 RetryIf::spawn(
143 retry_strategy,
144 || {
145 let catalog = catalog.clone();
146 let table_ident = table_ident.clone();
147 let commit_action = &commit_action;
148 async move {
149 let table =
150 reload_table(catalog.as_ref(), &table_ident, schema_id, partition_spec_id)
151 .await
152 .map_err(CommitError::ReloadTable)?;
153 commit_action(table).await
154 }
155 },
156 |err: &CommitError| match err {
157 CommitError::Commit(e) => {
158 tracing::warn!(
159 iceberg_component = log_context.iceberg_component,
160 iceberg_operation = log_context.iceberg_operation,
161 sink_id = log_context.sink_id.as_deref().unwrap_or("unknown"),
162 epoch = ?log_context.epoch,
163 snapshot_id = ?log_context.snapshot_id,
164 table = %log_context.table,
165 branch = %log_context.branch,
166 schema_id,
167 partition_spec_id,
168 error = %e.as_report(),
169 "iceberg_commit_retryable_error",
170 );
171 true
172 }
173 CommitError::ReloadTable(e) => {
174 tracing::error!(
175 iceberg_component = log_context.iceberg_component,
176 iceberg_operation = log_context.iceberg_operation,
177 sink_id = log_context.sink_id.as_deref().unwrap_or("unknown"),
178 epoch = ?log_context.epoch,
179 snapshot_id = ?log_context.snapshot_id,
180 table = %log_context.table,
181 branch = %log_context.branch,
182 schema_id,
183 partition_spec_id,
184 error = %e.as_report(),
185 "iceberg_commit_reload_table_non_retryable_error",
186 );
187 false
188 }
189 },
190 )
191 .await
192 .map_err(|e| match e {
193 CommitError::ReloadTable(e) | CommitError::Commit(e) => e,
194 })
195}