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