1use std::sync::Arc;
22
23use mz_dyncfg::ConfigValHandle;
24use mz_persist_client::batch::{BatchBuilder, ProtoBatch};
25use mz_persist_client::write::WriteHandle;
26use mz_persist_client::{PersistClient, Schemas};
27use mz_persist_types::ShardId;
28use mz_persist_types::codec_impls::UnitSchema;
29use mz_persist_types::part::PartBuilder;
30use mz_repr::{RelationDesc, Timestamp};
31use mz_storage_operators::persist::{SharedBatchBuilder, SharedBatches};
32use mz_storage_types::StorageDiff;
33use mz_storage_types::sources::SourceData;
34
35use crate::sink::materialized_view::{BatchDescription, PersistApi};
36
37pub(super) struct SharedBatchWriter {
39 persist_client: PersistClient,
40 shard_id: ShardId,
41 shard_name: String,
42 schemas: Schemas<SourceData, ()>,
43 shared_batches: SharedBatches,
44 max_bytes: ConfigValHandle<usize>,
45}
46
47impl SharedBatchWriter {
48 pub(super) async fn new(
51 persist_api: &PersistApi,
52 writer: &WriteHandle<SourceData, (), Timestamp, StorageDiff>,
53 max_bytes: ConfigValHandle<usize>,
54 ) -> Self {
55 Self {
56 persist_client: persist_api.open_client().await,
57 shard_id: persist_api.collection.data_shard,
58 shard_name: persist_api.shard_name.clone(),
59 schemas: Schemas {
61 id: writer.schema_id(),
62 key: Arc::new(persist_api.collection.relation_desc.clone()),
63 val: Arc::new(UnitSchema),
64 },
65 shared_batches: persist_api.persist_batches.clone(),
66 max_bytes,
67 }
68 }
69
70 pub(super) fn start(&self, desc: &BatchDescription) -> SharedBatchWrite<'_> {
72 let max_bytes = self.max_bytes.get();
77 let shared = (max_bytes > 0).then(|| {
78 self.shared_batches.builder(
79 desc.shared_id,
80 self.persist_client.clone(),
81 self.shard_id,
82 self.shard_name.clone(),
83 self.schemas.clone(),
84 desc.lower.clone(),
85 desc.upper.clone(),
86 )
87 });
88 SharedBatchWrite {
89 writer: self,
90 desc: desc.clone(),
91 max_bytes,
92 staged: PartBuilder::new(&*self.schemas.key, &*self.schemas.val),
93 own: None,
94 shared,
95 }
96 }
97
98 fn own_builder(
99 &self,
100 desc: &BatchDescription,
101 ) -> BatchBuilder<SourceData, (), Timestamp, StorageDiff> {
102 self.persist_client.batch_builder(
103 self.shard_id,
104 &self.shard_name,
105 self.schemas.clone(),
106 desc.lower.clone(),
107 None,
108 )
109 }
110}
111
112pub(super) struct SharedBatchWrite<'a> {
119 writer: &'a SharedBatchWriter,
120 desc: BatchDescription,
121 max_bytes: usize,
122 staged: PartBuilder<SourceData, RelationDesc, (), UnitSchema>,
125 own: Option<BatchBuilder<SourceData, (), Timestamp, StorageDiff>>,
128 shared: Option<SharedBatchBuilder>,
129}
130
131impl SharedBatchWrite<'_> {
132 pub(super) async fn push(&mut self, (k, v): &(SourceData, ()), t: Timestamp, d: StorageDiff) {
134 if self.own.is_none() {
135 if self.shared.is_some() {
136 self.staged.push(k, v, t, d);
137 if self.staged.goodbytes() < self.max_bytes {
138 return;
139 }
140 let writer = self.writer;
141 let staged = self
142 .staged
143 .finish_and_replace(&*writer.schemas.key, &*writer.schemas.val);
144 let mut own = writer.own_builder(&self.desc);
145 own.add_part(staged).await.expect("valid usage");
146 self.own = Some(own);
147 return;
148 }
149 self.own = Some(self.writer.own_builder(&self.desc));
150 }
151 let own = self.own.as_mut().expect("created above");
152 own.add(k, v, &t, &d).await.expect("valid usage");
153 }
154
155 pub(super) async fn finish(self) -> Vec<ProtoBatch> {
158 let own = match self.own {
159 Some(own) => Some(
160 own.finish(self.desc.upper.clone())
161 .await
162 .expect("valid usage"),
163 ),
164 None => None,
165 };
166 let shared = match self.shared {
167 Some(shared) => {
168 shared.push(self.staged.finish()).await;
169 shared.finish().await
170 }
171 None => None,
172 };
173 own.into_iter()
174 .chain(shared)
175 .map(|batch| batch.into_transmittable_batch())
176 .collect()
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use mz_dyncfg::ConfigValHandle;
183 use mz_persist_client::Diagnostics;
184 use mz_repr::{Datum, Row, SqlScalarType};
185 use timely::progress::Antichain;
186
187 use super::*;
188
189 fn desc() -> RelationDesc {
190 RelationDesc::builder()
191 .with_column("a", SqlScalarType::Int64.nullable(false))
192 .finish()
193 }
194
195 fn writer(
196 client: &PersistClient,
197 shared_batches: &SharedBatches,
198 shard_id: ShardId,
199 max_bytes: usize,
200 ) -> SharedBatchWriter {
201 SharedBatchWriter {
202 persist_client: client.clone(),
203 shard_id,
204 shard_name: "test".to_string(),
205 schemas: Schemas {
206 id: None,
207 key: Arc::new(desc()),
208 val: Arc::new(UnitSchema),
209 },
210 shared_batches: shared_batches.clone(),
211 max_bytes: ConfigValHandle::disconnected(max_bytes),
212 }
213 }
214
215 fn update(i: i64) -> (SourceData, ()) {
216 (SourceData(Ok(Row::pack_slice(&[Datum::Int64(i)]))), ())
217 }
218
219 async fn update_counts(
220 client: &PersistClient,
221 shard_id: ShardId,
222 batches: Vec<ProtoBatch>,
223 ) -> Vec<usize> {
224 let write = client
225 .open_writer::<SourceData, (), Timestamp, StorageDiff>(
226 shard_id,
227 Arc::new(desc()),
228 Arc::new(UnitSchema),
229 Diagnostics::for_tests(),
230 )
231 .await
232 .expect("valid usage");
233 batches
234 .into_iter()
235 .map(|proto| {
236 write
237 .batch_from_transmittable_batch(proto)
238 .into_hollow_batch()
239 .len
240 })
241 .collect()
242 }
243
244 #[mz_ore::test(tokio::test)]
245 async fn test_shared_batch_write_crosses_threshold() {
246 let client = PersistClient::new_for_tests().await;
247 let shared_batches = SharedBatches::new();
248 let shard_id = ShardId::new();
249 let small = writer(&client, &shared_batches, shard_id, usize::MAX);
250 let large = writer(&client, &shared_batches, shard_id, 1);
251 let desc = BatchDescription::new(
252 Antichain::from_elem(Timestamp::from(0)),
253 Antichain::from_elem(Timestamp::from(1)),
254 0,
255 );
256
257 let mut small_write = small.start(&desc);
258 let mut large_write = large.start(&desc);
259 small_write.push(&update(0), Timestamp::from(0), 1).await;
260 large_write.push(&update(1), Timestamp::from(0), 1).await;
263 large_write.push(&update(2), Timestamp::from(0), 1).await;
264
265 let small_batches = small_write.finish().await;
266 assert!(
267 small_batches.is_empty(),
268 "a worker below the threshold that finishes first delivers nothing"
269 );
270 let large_batches = large_write.finish().await;
271 assert_eq!(
272 update_counts(&client, shard_id, large_batches).await,
273 vec![2, 1],
274 "the last worker delivers its own batch and the shared batch"
275 );
276 }
277
278 #[mz_ore::test(tokio::test)]
279 async fn test_shared_batch_write_zero_threshold() {
280 let client = PersistClient::new_for_tests().await;
281 let shared_batches = SharedBatches::new();
282 let shard_id = ShardId::new();
283 let desc = BatchDescription::new(
284 Antichain::from_elem(Timestamp::from(0)),
285 Antichain::from_elem(Timestamp::from(1)),
286 0,
287 );
288 let writer = writer(&client, &shared_batches, shard_id, 0);
289 let mut write = writer.start(&desc);
290 assert!(
291 write.shared.is_none(),
292 "a zero threshold joins no shared batch"
293 );
294 write.push(&update(0), Timestamp::from(0), 1).await;
295 assert_eq!(
296 update_counts(&client, shard_id, write.finish().await).await,
297 vec![1],
298 "a zero threshold writes one own batch"
299 );
300 }
301}