1use std::sync::Arc;
13use std::time::{Duration, Instant};
14
15use differential_dataflow::operators::arrange::{Arranged, TraceAgent};
16use differential_dataflow::trace::SpanOf;
17use differential_dataflow::trace::implementations::merge_batcher::MergeBatcher;
18use differential_dataflow::{AsCollection, Hashable, VecCollection};
19use mz_persist_client::operators::shard_source::SnapshotMode;
20use mz_repr::{Datum, Diff, GlobalId, Row, Timestamp};
21use mz_row_spine::{ArcOrdValBatcher, ArcOrdValSpine};
22use mz_storage_operators::persist_source;
23use mz_storage_types::controller::CollectionMetadata;
24use mz_storage_types::errors::DataflowError;
25use mz_storage_types::sinks::{StorageSinkConnection, StorageSinkDesc};
26use mz_timely_util::builder_async::PressOnDropButton;
27use timely::dataflow::operators::Leave;
28use timely::dataflow::{Scope, StreamVec};
29use tracing::warn;
30
31use crate::healthcheck::HealthStatusMessage;
32use crate::storage_state::StorageState;
33
34pub(crate) type SinkTrace = TraceAgent<ArcOrdValSpine<Option<Row>, Row, Timestamp, Diff>>;
38
39pub(crate) type SinkBatchStream<'scope> = StreamVec<'scope, Timestamp, SpanOf<SinkTrace>>;
47
48pub(crate) fn render_sink<'scope>(
52 scope: Scope<'scope, ()>,
53 storage_state: &mut StorageState,
54 sink_id: GlobalId,
55 sink: &StorageSinkDesc<CollectionMetadata, mz_repr::Timestamp>,
56) -> (
57 StreamVec<'scope, (), HealthStatusMessage>,
58 Vec<PressOnDropButton>,
59) {
60 let snapshot_mode = if sink.with_snapshot {
61 SnapshotMode::Include
62 } else {
63 SnapshotMode::Exclude
64 };
65
66 let error_handler = storage_state.error_handler("storage_sink", sink_id);
67
68 let name = format!("{sink_id}-sinks");
69 let outer_scope = scope.clone();
70
71 scope.scoped(&name, |scope| {
72 let mut tokens = vec![];
73 let sink_render = get_sink_render_for(&sink.connection);
74
75 let (ok_collection, err_collection, persist_tokens) =
76 persist_source::persist_source::<_, persist_source::RowVecBuilder<Timestamp>>(
77 scope,
78 sink.from,
79 Arc::clone(&storage_state.persist_clients),
80 &storage_state.txns_ctx,
81 sink.from_storage_metadata.clone(),
82 None,
83 Some(sink.as_of.clone()),
84 snapshot_mode,
85 timely::progress::Antichain::new(),
86 None,
87 None,
88 async {},
89 error_handler,
90 );
91 tokens.extend(persist_tokens);
92
93 let batches = arrange_sink_input(&*sink_render, ok_collection.as_collection());
94 let key_is_synthetic = sink_render.get_key_indices().is_none()
95 && sink_render.get_relation_key_indices().is_none();
96
97 let (health, sink_tokens) = sink_render.render_sink(
98 storage_state,
99 sink,
100 sink_id,
101 batches,
102 key_is_synthetic,
103 err_collection.as_collection(),
104 );
105 tokens.extend(sink_tokens);
106 (health.leave(outer_scope), tokens)
107 })
108}
109
110fn arrange_sink_input<'scope>(
123 sink_render: &dyn SinkRender<'scope>,
124 collection: VecCollection<'scope, Timestamp, Row, Diff>,
125) -> SinkBatchStream<'scope> {
126 let key_indices = sink_render
127 .get_key_indices()
128 .or_else(|| sink_render.get_relation_key_indices())
129 .map(|k| k.to_vec());
130
131 let keyed = match key_indices {
132 None => collection.map(|row| (Some(Row::pack(Some(Datum::UInt64(row.hashed())))), row)),
133 Some(key_indices) => {
134 let mut datum_vec = mz_repr::DatumVec::new();
135 collection.map(move |row| {
136 let key = {
139 let datums = datum_vec.borrow_with(&row);
140 Row::pack(key_indices.iter().map(|&idx| datums[idx].clone()))
141 };
142 (Some(key), row)
143 })
144 }
145 };
146
147 #[allow(clippy::disallowed_methods)]
150 let Arranged { stream, trace: _ } = keyed
151 .arrange_named::<ArcOrdValBatcher<_, _, _, _>, ArcOrdValSpine<_, _, _, _>>(
152 "Arrange Sink",
153 MergeBatcher::new,
154 );
155 stream
156}
157
158pub(crate) struct PkViolationWarner {
170 sink_id: GlobalId,
171 from_id: GlobalId,
172 last_warning: Instant,
173 current: Option<(u64, Timestamp)>,
174 count: usize,
175}
176
177impl PkViolationWarner {
178 pub fn new(sink_id: GlobalId, from_id: GlobalId) -> Self {
179 Self {
180 sink_id,
181 from_id,
182 last_warning: Instant::now(),
183 current: None,
184 count: 0,
185 }
186 }
187
188 pub fn observe(&mut self, key: &Option<Row>, time: Timestamp) {
192 let hash = key.as_ref().map(|k| k.hashed()).unwrap_or(u64::MAX);
196 let same = self.current == Some((hash, time));
197 if !same {
198 self.flush();
199 self.current = Some((hash, time));
200 }
201 self.count += 1;
202 }
203
204 pub fn flush(&mut self) {
207 if self.count > 1 {
208 let now = Instant::now();
209 if now.duration_since(self.last_warning) >= Duration::from_secs(10) {
210 self.last_warning = now;
211 warn!(
212 sink_id = ?self.sink_id,
213 from_id = ?self.from_id,
214 "primary key error: expected at most one update per key and timestamp; \
215 this can happen when the configured sink key is not a primary key of \
216 the sinked relation"
217 );
218 }
219 }
220 self.current = None;
221 self.count = 0;
222 }
223}
224
225pub(crate) trait SinkRender<'scope> {
227 fn get_key_indices(&self) -> Option<&[usize]>;
230
231 fn get_relation_key_indices(&self) -> Option<&[usize]>;
234
235 fn render_sink(
245 &self,
246 storage_state: &mut StorageState,
247 sink: &StorageSinkDesc<CollectionMetadata, Timestamp>,
248 sink_id: GlobalId,
249 batches: SinkBatchStream<'scope>,
250 key_is_synthetic: bool,
251 err_collection: VecCollection<'scope, Timestamp, DataflowError, Diff>,
252 ) -> (
253 StreamVec<'scope, Timestamp, HealthStatusMessage>,
254 Vec<PressOnDropButton>,
255 );
256}
257
258fn get_sink_render_for<'scope>(connection: &StorageSinkConnection) -> Box<dyn SinkRender<'scope>> {
259 match connection {
260 StorageSinkConnection::Kafka(connection) => Box::new(connection.clone()),
261 StorageSinkConnection::Iceberg(connection) => Box::new(connection.clone()),
262 }
263}