1use std::borrow::Borrow;
13use std::fmt::Debug;
14use std::sync::Arc;
15
16use differential_dataflow::difference::Monoid;
17use differential_dataflow::lattice::Lattice;
18use differential_dataflow::trace::Description;
19use futures::StreamExt;
20use futures::stream::FuturesUnordered;
21use mz_dyncfg::{Config, ParameterScope};
22use mz_ore::task::RuntimeExt;
23use mz_ore::{instrument, soft_panic_or_log};
24use mz_persist::location::Blob;
25use mz_persist_types::schema::SchemaId;
26use mz_persist_types::{Codec, Codec64};
27use mz_proto::{IntoRustIfSome, ProtoType};
28use proptest_derive::Arbitrary;
29use semver::Version;
30use serde::{Deserialize, Serialize};
31use timely::PartialOrder;
32use timely::order::TotalOrder;
33use timely::progress::{Antichain, Timestamp};
34use tokio::runtime::Handle;
35use tracing::{Instrument, debug_span, error, info, warn};
36use uuid::Uuid;
37
38use crate::batch::{
39 Added, BATCH_DELETE_ENABLED, Batch, BatchBuilder, BatchBuilderConfig, BatchBuilderInternal,
40 BatchParts, ProtoBatch, validate_truncate_batch,
41};
42use crate::error::{InvalidUsage, UpperMismatch};
43use crate::fetch::{
44 EncodedPart, FetchBatchFilter, FetchedPart, PartDecodeFormat, PartSource,
45 VALIDATE_PART_BOUNDS_ON_READ,
46};
47use crate::internal::compact::{CompactConfig, Compactor};
48use crate::internal::encoding::{Schemas, assert_code_can_read_data};
49use crate::internal::machine::{
50 CompareAndAppendRes, ExpireFn, Machine, next_listen_batch_retry_params,
51};
52use crate::internal::metrics::{BatchWriteMetrics, Metrics, ShardMetrics};
53use crate::internal::state::{BatchPart, HandleDebugState, HollowBatch, RunOrder, RunPart};
54use crate::read::ReadHandle;
55use crate::schema::PartMigration;
56use crate::{GarbageCollector, IsolatedRuntime, PersistConfig, ShardId, parse_id};
57
58pub(crate) const COMBINE_INLINE_WRITES: Config<bool> = Config::new(
59 "persist_write_combine_inline_writes",
60 true,
61 "If set, re-encode inline writes if they don't fit into the batch metadata limits.",
62 ParameterScope::Environment,
63);
64
65pub(crate) const VALIDATE_PART_BOUNDS_ON_WRITE: Config<bool> = Config::new(
66 "persist_validate_part_bounds_on_write",
67 false,
68 "Validate the part lower <= the batch lower and the part upper <= batch upper,\
69 for the batch being appended.",
70 ParameterScope::Environment,
71);
72
73#[derive(
75 Arbitrary,
76 Clone,
77 PartialEq,
78 Eq,
79 PartialOrd,
80 Ord,
81 Hash,
82 Serialize,
83 Deserialize
84)]
85#[serde(try_from = "String", into = "String")]
86pub struct WriterId(pub(crate) [u8; 16]);
87
88impl std::fmt::Display for WriterId {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 write!(f, "w{}", Uuid::from_bytes(self.0))
91 }
92}
93
94impl std::fmt::Debug for WriterId {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 write!(f, "WriterId({})", Uuid::from_bytes(self.0))
97 }
98}
99
100impl std::str::FromStr for WriterId {
101 type Err = String;
102
103 fn from_str(s: &str) -> Result<Self, Self::Err> {
104 parse_id("w", "WriterId", s).map(WriterId)
105 }
106}
107
108impl From<WriterId> for String {
109 fn from(writer_id: WriterId) -> Self {
110 writer_id.to_string()
111 }
112}
113
114impl TryFrom<String> for WriterId {
115 type Error = String;
116
117 fn try_from(s: String) -> Result<Self, Self::Error> {
118 s.parse()
119 }
120}
121
122impl WriterId {
123 pub(crate) fn new() -> Self {
124 WriterId(*Uuid::new_v4().as_bytes())
125 }
126}
127
128#[derive(Debug)]
144pub struct WriteHandle<K: Codec, V: Codec, T, D> {
145 pub(crate) cfg: PersistConfig,
146 pub(crate) metrics: Arc<Metrics>,
147 pub(crate) machine: Machine<K, V, T, D>,
148 pub(crate) gc: GarbageCollector<K, V, T, D>,
149 pub(crate) compact: Option<Compactor<K, V, T, D>>,
150 pub(crate) blob: Arc<dyn Blob>,
151 pub(crate) isolated_runtime: Arc<IsolatedRuntime>,
152 pub(crate) writer_id: WriterId,
153 pub(crate) debug_state: HandleDebugState,
154 pub(crate) write_schemas: Schemas<K, V>,
155
156 pub(crate) upper: Antichain<T>,
157 expire_fn: Option<ExpireFn>,
158}
159
160impl<K, V, T, D> WriteHandle<K, V, T, D>
161where
162 K: Debug + Codec,
163 V: Debug + Codec,
164 T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
165 D: Monoid + Ord + Codec64 + Send + Sync,
166{
167 pub(crate) fn new(
168 cfg: PersistConfig,
169 metrics: Arc<Metrics>,
170 machine: Machine<K, V, T, D>,
171 gc: GarbageCollector<K, V, T, D>,
172 blob: Arc<dyn Blob>,
173 writer_id: WriterId,
174 purpose: &str,
175 write_schemas: Schemas<K, V>,
176 ) -> Self {
177 let isolated_runtime = Arc::clone(&machine.isolated_runtime);
178 let compact = cfg
179 .compaction_enabled
180 .then(|| Compactor::new(cfg.clone(), Arc::clone(&metrics), gc.clone()));
181 let debug_state = HandleDebugState {
182 hostname: cfg.hostname.to_owned(),
183 purpose: purpose.to_owned(),
184 };
185 let upper = machine.applier.clone_upper();
186 let expire_fn = Self::expire_fn(machine.clone(), gc.clone(), writer_id.clone());
187 WriteHandle {
188 cfg,
189 metrics,
190 machine,
191 gc,
192 compact,
193 blob,
194 isolated_runtime,
195 writer_id,
196 debug_state,
197 write_schemas,
198 upper,
199 expire_fn: Some(expire_fn),
200 }
201 }
202
203 pub fn from_read(read: &ReadHandle<K, V, T, D>, purpose: &str) -> Self {
206 Self::new(
207 read.cfg.clone(),
208 Arc::clone(&read.metrics),
209 read.machine.clone(),
210 read.gc.clone(),
211 Arc::clone(&read.blob),
212 WriterId::new(),
213 purpose,
214 read.read_schemas.clone(),
215 )
216 }
217
218 pub fn validate_part_bounds_on_write(&self) -> bool {
221 VALIDATE_PART_BOUNDS_ON_WRITE.get(&self.cfg) || VALIDATE_PART_BOUNDS_ON_READ.get(&self.cfg)
224 }
225
226 pub fn shard_id(&self) -> ShardId {
228 self.machine.shard_id()
229 }
230
231 pub fn schema_id(&self) -> Option<SchemaId> {
233 self.write_schemas.id
234 }
235
236 pub async fn try_register_schema(&mut self) -> Option<SchemaId> {
242 let Schemas { id, key, val } = &self.write_schemas;
243
244 if let Some(id) = id {
245 return Some(*id);
246 }
247
248 let (schema_id, maintenance) = self.machine.register_schema(key, val).await;
249 maintenance.start_performing(&self.machine, &self.gc);
250
251 self.write_schemas.id = schema_id;
252 schema_id
253 }
254
255 pub fn upper(&self) -> &Antichain<T> {
262 &self.upper
263 }
264
265 pub fn shared_upper(&self) -> Antichain<T> {
271 self.machine.applier.clone_upper()
272 }
273
274 #[instrument(level = "debug", fields(shard = %self.machine.shard_id()))]
280 pub async fn fetch_recent_upper(&mut self) -> &Antichain<T> {
281 self.machine
284 .applier
285 .fetch_upper(|current_upper| self.upper.clone_from(current_upper))
286 .await;
287 &self.upper
288 }
289
290 pub async fn advance_upper(&mut self, target: &Antichain<T>) {
298 let mut lower = self.shared_upper().clone();
301
302 while !PartialOrder::less_equal(target, &lower) {
303 let since = Antichain::from_elem(T::minimum());
304 let desc = Description::new(lower.clone(), target.clone(), since);
305 let batch = HollowBatch::empty(desc);
306
307 let res = self
308 .machine
309 .compare_and_append(&batch, &self.writer_id, &self.debug_state)
310 .await;
311
312 use CompareAndAppendRes::*;
313 let new_upper = match res {
314 Success(_seq_no, maintenance) => {
315 maintenance.start_performing(&self.machine, &self.gc, self.compact.as_ref());
316 batch.desc.upper().clone()
317 }
318 UpperMismatch(_seq_no, actual_upper) => actual_upper,
319 InvalidUsage(_invalid_usage) => unreachable!("batch bounds checked above"),
320 InlineBackpressure => unreachable!("batch was empty"),
321 };
322
323 self.upper.clone_from(&new_upper);
324 lower = new_upper;
325 }
326 }
327
328 #[instrument(level = "trace", fields(shard = %self.machine.shard_id()))]
358 pub async fn append<SB, KB, VB, TB, DB, I>(
359 &mut self,
360 updates: I,
361 lower: Antichain<T>,
362 upper: Antichain<T>,
363 ) -> Result<Result<(), UpperMismatch<T>>, InvalidUsage<T>>
364 where
365 SB: Borrow<((KB, VB), TB, DB)>,
366 KB: Borrow<K>,
367 VB: Borrow<V>,
368 TB: Borrow<T>,
369 DB: Borrow<D>,
370 I: IntoIterator<Item = SB>,
371 D: Send + Sync,
372 {
373 let batch = self.batch(updates, lower.clone(), upper.clone()).await?;
374 self.append_batch(batch, lower, upper).await
375 }
376
377 #[instrument(level = "trace", fields(shard = %self.machine.shard_id()))]
406 pub async fn compare_and_append<SB, KB, VB, TB, DB, I>(
407 &mut self,
408 updates: I,
409 expected_upper: Antichain<T>,
410 new_upper: Antichain<T>,
411 ) -> Result<Result<(), UpperMismatch<T>>, InvalidUsage<T>>
412 where
413 SB: Borrow<((KB, VB), TB, DB)>,
414 KB: Borrow<K>,
415 VB: Borrow<V>,
416 TB: Borrow<T>,
417 DB: Borrow<D>,
418 I: IntoIterator<Item = SB>,
419 D: Send + Sync,
420 {
421 let mut batch = self
422 .batch(updates, expected_upper.clone(), new_upper.clone())
423 .await?;
424 match self
425 .compare_and_append_batch(&mut [&mut batch], expected_upper, new_upper, true)
426 .await
427 {
428 ok @ Ok(Ok(())) => ok,
429 err => {
430 batch.delete().await;
435 err
436 }
437 }
438 }
439
440 #[instrument(level = "trace", fields(shard = %self.machine.shard_id()))]
466 pub async fn append_batch(
467 &mut self,
468 mut batch: Batch<K, V, T, D>,
469 mut lower: Antichain<T>,
470 upper: Antichain<T>,
471 ) -> Result<Result<(), UpperMismatch<T>>, InvalidUsage<T>>
472 where
473 D: Send + Sync,
474 {
475 loop {
476 let res = self
477 .compare_and_append_batch(&mut [&mut batch], lower.clone(), upper.clone(), true)
478 .await?;
479 match res {
480 Ok(()) => {
481 self.upper = upper;
482 return Ok(Ok(()));
483 }
484 Err(mismatch) => {
485 if PartialOrder::less_than(&mismatch.current, &lower) {
487 self.upper.clone_from(&mismatch.current);
488
489 batch.delete().await;
490
491 return Ok(Err(mismatch));
492 } else if PartialOrder::less_than(&mismatch.current, &upper) {
493 lower = mismatch.current;
500 } else {
501 self.upper = mismatch.current;
503
504 batch.delete().await;
508
509 return Ok(Ok(()));
510 }
511 }
512 }
513 }
514 }
515
516 #[instrument(level = "debug", fields(shard = %self.machine.shard_id()))]
550 pub async fn compare_and_append_batch(
551 &mut self,
552 batches: &mut [&mut Batch<K, V, T, D>],
553 expected_upper: Antichain<T>,
554 new_upper: Antichain<T>,
555 validate_part_bounds_on_write: bool,
556 ) -> Result<Result<(), UpperMismatch<T>>, InvalidUsage<T>>
557 where
558 D: Send + Sync,
559 {
560 let schema_id = self.try_register_schema().await;
564
565 for batch in batches.iter() {
566 if self.machine.shard_id() != batch.shard_id() {
567 return Err(InvalidUsage::BatchNotFromThisShard {
568 batch_shard: batch.shard_id(),
569 handle_shard: self.machine.shard_id(),
570 });
571 }
572 assert_code_can_read_data(&self.cfg.build_version, &batch.version);
573 if self.cfg.build_version > batch.version {
574 info!(
575 shard_id =? self.machine.shard_id(),
576 batch_version =? batch.version,
577 writer_version =? self.cfg.build_version,
578 "Appending batch from the past. This is fine but should be rare. \
579 TODO: Error on very old versions once the leaked blob detector exists."
580 )
581 }
582 fn assert_schema<A: Codec>(writer_schema: &A::Schema, batch_schema: &bytes::Bytes) {
583 if batch_schema.is_empty() {
584 return;
586 }
587 let batch_schema: A::Schema = A::decode_schema(batch_schema);
588 if *writer_schema != batch_schema {
589 error!(
590 ?writer_schema,
591 ?batch_schema,
592 "writer and batch schemas should be identical"
593 );
594 soft_panic_or_log!("writer and batch schemas should be identical");
595 }
596 }
597 assert_schema::<K>(&*self.write_schemas.key, &batch.schemas.0);
598 assert_schema::<V>(&*self.write_schemas.val, &batch.schemas.1);
599 }
600
601 let lower = expected_upper.clone();
602 let upper = new_upper;
603 let since = Antichain::from_elem(T::minimum());
604 let desc = Description::new(lower, upper, since);
605
606 let mut received_inline_backpressure = false;
607 let mut inline_batch_builder: Option<(_, BatchBuilder<K, V, T, D>)> = None;
614 let maintenance = loop {
615 let any_batch_rewrite = batches
616 .iter()
617 .any(|x| x.batch.parts.iter().any(|x| x.ts_rewrite().is_some()));
618 let (mut parts, mut num_updates, mut run_splits, mut run_metas) =
619 (vec![], 0, vec![], vec![]);
620 let mut key_storage = None;
621 let mut val_storage = None;
622 for batch in batches.iter() {
623 let bounds_truncated = validate_truncate_batch(
624 &batch.batch,
625 &desc,
626 any_batch_rewrite,
627 validate_part_bounds_on_write,
628 )?;
629 for (run_meta, run) in batch.batch.runs() {
630 let start_index = parts.len();
631 for part in run {
632 if let (
633 RunPart::Single(
634 batch_part @ BatchPart::Inline {
635 updates,
636 ts_rewrite,
637 schema_id: _,
638 deprecated_schema_id: _,
639 },
640 ),
641 Some((schema_cache, builder)),
642 ) = (part, &mut inline_batch_builder)
643 {
644 let schema_migration = PartMigration::new(
645 batch_part,
646 self.write_schemas.clone(),
647 schema_cache,
648 )
649 .await
650 .expect("schemas for inline user part");
651
652 let encoded_part = EncodedPart::from_inline(
653 &crate::fetch::FetchConfig::from_persist_config(&self.cfg),
654 &*self.metrics,
655 self.metrics.read.compaction.clone(),
656 desc.clone(),
657 updates,
658 ts_rewrite.as_ref(),
659 );
660 let mut fetched_part = FetchedPart::new(
661 Arc::clone(&self.metrics),
662 PartSource::Whole(encoded_part),
663 schema_migration,
664 FetchBatchFilter::Compaction {
665 since: desc.since().clone(),
666 },
667 false,
668 PartDecodeFormat::Arrow,
669 None,
670 );
671
672 while let Some(((k, v), t, d)) =
673 fetched_part.next_with_storage(&mut key_storage, &mut val_storage)
674 {
675 builder
676 .add(&k, &v, &t, &d)
677 .await
678 .expect("re-encoding just-decoded data");
679 }
680 } else {
681 parts.push(part.clone())
682 }
683 }
684
685 let end_index = parts.len();
686
687 if start_index == end_index {
688 continue;
689 }
690
691 if start_index != 0 {
693 run_splits.push(start_index);
694 }
695 let mut run_meta = run_meta.clone();
696 if bounds_truncated {
697 run_meta.set_bounds_truncated();
698 }
699 run_metas.push(run_meta);
700 }
701 num_updates += batch.batch.len;
702 }
703
704 let mut flushed_inline_batch = if let Some((_, builder)) = inline_batch_builder.take() {
705 let mut finished = builder
706 .finish(desc.upper().clone())
707 .await
708 .expect("invalid usage");
709 let cfg = BatchBuilderConfig::new(&self.cfg, self.shard_id());
710 finished
711 .flush_to_blob(
712 &cfg,
713 &self.metrics.inline.backpressure,
714 &self.isolated_runtime,
715 &self.write_schemas,
716 )
717 .await;
718 Some(finished)
719 } else {
720 None
721 };
722
723 if let Some(batch) = &flushed_inline_batch {
724 for (run_meta, run) in batch.batch.runs() {
725 assert!(run.len() > 0);
726 let start_index = parts.len();
727 if start_index != 0 {
728 run_splits.push(start_index);
729 }
730 run_metas.push(run_meta.clone());
731 parts.extend(run.iter().cloned())
732 }
733 }
734
735 let mut combined_batch =
736 HollowBatch::new(desc.clone(), parts, num_updates, run_metas, run_splits);
737
738 match schema_id {
742 Some(schema_id) => {
743 ensure_batch_schema(&mut combined_batch, self.shard_id(), schema_id);
744 }
745 None => {
746 assert!(
747 self.fetch_recent_upper().await.is_empty(),
748 "fetching a schema id should only fail when the shard is tombstoned"
749 )
750 }
751 }
752
753 let res = self
754 .machine
755 .compare_and_append(&combined_batch, &self.writer_id, &self.debug_state)
756 .await;
757
758 match res {
759 CompareAndAppendRes::Success(_seqno, maintenance) => {
760 self.upper.clone_from(desc.upper());
761 for batch in batches.iter_mut() {
762 batch.mark_consumed();
763 }
764 if let Some(batch) = &mut flushed_inline_batch {
765 batch.mark_consumed();
766 }
767 break maintenance;
768 }
769 CompareAndAppendRes::InvalidUsage(invalid_usage) => {
770 if let Some(batch) = flushed_inline_batch.take() {
771 batch.delete().await;
772 }
773 return Err(invalid_usage);
774 }
775 CompareAndAppendRes::UpperMismatch(_seqno, current_upper) => {
776 if let Some(batch) = flushed_inline_batch.take() {
777 batch.delete().await;
778 }
779 self.upper.clone_from(¤t_upper);
782 return Ok(Err(UpperMismatch {
783 current: current_upper,
784 expected: expected_upper,
785 }));
786 }
787 CompareAndAppendRes::InlineBackpressure => {
788 assert_eq!(received_inline_backpressure, false);
791 received_inline_backpressure = true;
792 if COMBINE_INLINE_WRITES.get(&self.cfg) {
793 inline_batch_builder = Some((
794 self.machine.applier.schema_cache(),
795 self.builder(desc.lower().clone()),
796 ));
797 continue;
798 }
799
800 let cfg = BatchBuilderConfig::new(&self.cfg, self.shard_id());
801 let flush_batches = batches
804 .iter_mut()
805 .map(|batch| async {
806 batch
807 .flush_to_blob(
808 &cfg,
809 &self.metrics.inline.backpressure,
810 &self.isolated_runtime,
811 &self.write_schemas,
812 )
813 .await
814 })
815 .collect::<FuturesUnordered<_>>();
816 let () = flush_batches.collect::<()>().await;
817
818 for batch in batches.iter() {
819 assert_eq!(batch.batch.inline_bytes(), 0);
820 }
821
822 continue;
823 }
824 }
825 };
826
827 maintenance.start_performing(&self.machine, &self.gc, self.compact.as_ref());
828
829 Ok(Ok(()))
830 }
831
832 pub fn batch_from_transmittable_batch(&self, batch: ProtoBatch) -> Batch<K, V, T, D> {
835 let shard_id: ShardId = batch
836 .shard_id
837 .into_rust()
838 .expect("valid transmittable batch");
839 assert_eq!(shard_id, self.machine.shard_id());
840
841 let ret = Batch {
842 batch_delete_enabled: BATCH_DELETE_ENABLED.get(&self.cfg),
843 metrics: Arc::clone(&self.metrics),
844 shard_metrics: Arc::clone(&self.machine.applier.shard_metrics),
845 version: Version::parse(&batch.version).expect("valid transmittable batch"),
846 schemas: (batch.key_schema, batch.val_schema),
847 batch: batch
848 .batch
849 .into_rust_if_some("ProtoBatch::batch")
850 .expect("valid transmittable batch"),
851 blob: Arc::clone(&self.blob),
852 _phantom: std::marker::PhantomData,
853 };
854 assert_eq!(ret.shard_id(), self.machine.shard_id());
855 ret
856 }
857
858 pub fn builder(&self, lower: Antichain<T>) -> BatchBuilder<K, V, T, D> {
871 Self::builder_inner(
872 &self.cfg,
873 CompactConfig::new(&self.cfg, self.shard_id()),
874 Arc::clone(&self.metrics),
875 Arc::clone(&self.machine.applier.shard_metrics),
876 &self.metrics.user,
877 Arc::clone(&self.isolated_runtime),
878 Arc::clone(&self.blob),
879 self.shard_id(),
880 self.write_schemas.clone(),
881 lower,
882 )
883 }
884
885 pub(crate) fn builder_inner(
888 persist_cfg: &PersistConfig,
889 compact_cfg: CompactConfig,
890 metrics: Arc<Metrics>,
891 shard_metrics: Arc<ShardMetrics>,
892 user_batch_metrics: &BatchWriteMetrics,
893 isolated_runtime: Arc<IsolatedRuntime>,
894 blob: Arc<dyn Blob>,
895 shard_id: ShardId,
896 schemas: Schemas<K, V>,
897 lower: Antichain<T>,
898 ) -> BatchBuilder<K, V, T, D> {
899 let parts = if let Some(max_runs) = compact_cfg.batch.max_runs {
900 BatchParts::new_compacting::<K, V, D>(
901 compact_cfg,
902 Description::new(
903 lower.clone(),
904 Antichain::new(),
905 Antichain::from_elem(T::minimum()),
906 ),
907 max_runs,
908 Arc::clone(&metrics),
909 shard_metrics,
910 shard_id,
911 Arc::clone(&blob),
912 isolated_runtime,
913 user_batch_metrics,
914 schemas.clone(),
915 )
916 } else {
917 BatchParts::new_ordered::<D>(
918 compact_cfg.batch,
919 RunOrder::Unordered,
920 Arc::clone(&metrics),
921 shard_metrics,
922 shard_id,
923 Arc::clone(&blob),
924 isolated_runtime,
925 user_batch_metrics,
926 )
927 };
928 let builder = BatchBuilderInternal::new(
929 BatchBuilderConfig::new(persist_cfg, shard_id),
930 parts,
931 metrics,
932 schemas,
933 blob,
934 shard_id,
935 persist_cfg.build_version.clone(),
936 );
937 BatchBuilder::new(
938 builder,
939 Description::new(lower, Antichain::new(), Antichain::from_elem(T::minimum())),
940 )
941 }
942
943 #[instrument(level = "trace", fields(shard = %self.machine.shard_id()))]
949 pub async fn batch<SB, KB, VB, TB, DB, I>(
950 &mut self,
951 updates: I,
952 lower: Antichain<T>,
953 upper: Antichain<T>,
954 ) -> Result<Batch<K, V, T, D>, InvalidUsage<T>>
955 where
956 SB: Borrow<((KB, VB), TB, DB)>,
957 KB: Borrow<K>,
958 VB: Borrow<V>,
959 TB: Borrow<T>,
960 DB: Borrow<D>,
961 I: IntoIterator<Item = SB>,
962 {
963 let iter = updates.into_iter();
964
965 let mut builder = self.builder(lower.clone());
966
967 for update in iter {
968 let ((k, v), t, d) = update.borrow();
969 let (k, v, t, d) = (k.borrow(), v.borrow(), t.borrow(), d.borrow());
970 match builder.add(k, v, t, d).await {
971 Ok(Added::Record | Added::RecordAndParts) => (),
972 Err(invalid_usage) => return Err(invalid_usage),
973 }
974 }
975
976 builder.finish(upper.clone()).await
977 }
978
979 pub async fn wait_for_upper_past(&mut self, frontier: &Antichain<T>) {
981 let mut watch = self.machine.applier.watch();
982 self.machine
983 .wait_for_upper_past(
984 frontier,
985 &mut watch,
986 None,
987 &self.metrics.retries.next_listen_batch, next_listen_batch_retry_params(&self.cfg),
989 )
990 .await;
991 let upper = self.machine.applier.clone_upper();
992 if PartialOrder::less_than(&self.upper, &upper) {
993 self.upper.clone_from(&upper);
994 }
995 assert!(PartialOrder::less_than(frontier, &self.upper));
996 }
997
998 #[instrument(level = "debug", fields(shard = %self.machine.shard_id()))]
1007 pub async fn expire(mut self) {
1008 let Some(expire_fn) = self.expire_fn.take() else {
1009 return;
1010 };
1011 expire_fn.0().await;
1012 }
1013
1014 fn expire_fn(
1015 machine: Machine<K, V, T, D>,
1016 gc: GarbageCollector<K, V, T, D>,
1017 writer_id: WriterId,
1018 ) -> ExpireFn {
1019 ExpireFn(Box::new(move || {
1020 Box::pin(async move {
1021 let (_, maintenance) = machine.expire_writer(&writer_id).await;
1022 maintenance.start_performing(&machine, &gc);
1023 })
1024 }))
1025 }
1026
1027 #[cfg(test)]
1029 #[track_caller]
1030 pub async fn expect_append<L, U>(&mut self, updates: &[((K, V), T, D)], lower: L, new_upper: U)
1031 where
1032 L: Into<Antichain<T>>,
1033 U: Into<Antichain<T>>,
1034 D: Send + Sync,
1035 {
1036 self.append(updates.iter(), lower.into(), new_upper.into())
1037 .await
1038 .expect("invalid usage")
1039 .expect("unexpected upper");
1040 }
1041
1042 #[cfg(test)]
1045 #[track_caller]
1046 pub async fn expect_compare_and_append(
1047 &mut self,
1048 updates: &[((K, V), T, D)],
1049 expected_upper: T,
1050 new_upper: T,
1051 ) where
1052 D: Send + Sync,
1053 {
1054 self.compare_and_append(
1055 updates.iter().map(|((k, v), t, d)| ((k, v), t, d)),
1056 Antichain::from_elem(expected_upper),
1057 Antichain::from_elem(new_upper),
1058 )
1059 .await
1060 .expect("invalid usage")
1061 .expect("unexpected upper")
1062 }
1063
1064 #[cfg(test)]
1067 #[track_caller]
1068 pub async fn expect_compare_and_append_batch(
1069 &mut self,
1070 batches: &mut [&mut Batch<K, V, T, D>],
1071 expected_upper: T,
1072 new_upper: T,
1073 ) {
1074 self.compare_and_append_batch(
1075 batches,
1076 Antichain::from_elem(expected_upper),
1077 Antichain::from_elem(new_upper),
1078 true,
1079 )
1080 .await
1081 .expect("invalid usage")
1082 .expect("unexpected upper")
1083 }
1084
1085 #[cfg(test)]
1087 #[track_caller]
1088 pub async fn expect_batch(
1089 &mut self,
1090 updates: &[((K, V), T, D)],
1091 lower: T,
1092 upper: T,
1093 ) -> Batch<K, V, T, D> {
1094 self.batch(
1095 updates.iter(),
1096 Antichain::from_elem(lower),
1097 Antichain::from_elem(upper),
1098 )
1099 .await
1100 .expect("invalid usage")
1101 }
1102}
1103
1104impl<K: Codec, V: Codec, T, D> Drop for WriteHandle<K, V, T, D> {
1105 fn drop(&mut self) {
1106 let Some(expire_fn) = self.expire_fn.take() else {
1107 return;
1108 };
1109 let handle = match Handle::try_current() {
1110 Ok(x) => x,
1111 Err(_) => {
1112 warn!(
1113 "WriteHandle {} dropped without being explicitly expired, falling back to lease timeout",
1114 self.writer_id
1115 );
1116 return;
1117 }
1118 };
1119 let expire_span = debug_span!("drop::expire");
1125 handle.spawn_named(
1126 || format!("WriteHandle::expire ({})", self.writer_id),
1127 expire_fn.0().instrument(expire_span),
1128 );
1129 }
1130}
1131
1132fn ensure_batch_schema<T>(batch: &mut HollowBatch<T>, shard_id: ShardId, schema_id: SchemaId)
1137where
1138 T: Timestamp + Lattice + Codec64,
1139{
1140 let ensure = |id: &mut Option<SchemaId>| match id {
1141 Some(id) => assert_eq!(*id, schema_id, "schema ID mismatch; shard={shard_id}"),
1142 None => *id = Some(schema_id),
1143 };
1144
1145 for run_meta in &mut batch.run_meta {
1146 ensure(&mut run_meta.schema);
1147 }
1148 for part in &mut batch.parts {
1149 match part {
1150 RunPart::Single(BatchPart::Hollow(part)) => ensure(&mut part.schema_id),
1151 RunPart::Single(BatchPart::Inline { schema_id, .. }) => ensure(schema_id),
1152 RunPart::Many(_hollow_run_ref) => {
1153 }
1157 }
1158 }
1159}
1160
1161#[cfg(test)]
1162mod tests {
1163 use std::str::FromStr;
1164 use std::sync::mpsc;
1165
1166 use differential_dataflow::consolidation::consolidate_updates;
1167 use futures_util::FutureExt;
1168 use mz_dyncfg::ConfigUpdates;
1169 use mz_ore::collections::CollectionExt;
1170 use mz_ore::task;
1171 use serde_json::json;
1172
1173 use crate::cache::PersistClientCache;
1174 use crate::tests::{all_ok, new_test_client};
1175 use crate::{PersistLocation, ShardId};
1176
1177 use super::*;
1178
1179 #[mz_persist_proc::test(tokio::test)]
1180 #[cfg_attr(miri, ignore)] async fn empty_batches(dyncfgs: ConfigUpdates) {
1182 let data = [
1183 (("1".to_owned(), "one".to_owned()), 1, 1),
1184 (("2".to_owned(), "two".to_owned()), 2, 1),
1185 (("3".to_owned(), "three".to_owned()), 3, 1),
1186 ];
1187
1188 let (mut write, _) = new_test_client(&dyncfgs)
1189 .await
1190 .expect_open::<String, String, u64, i64>(ShardId::new())
1191 .await;
1192 let blob = Arc::clone(&write.blob);
1193
1194 let mut upper = 3;
1196 write.expect_append(&data[..2], vec![0], vec![upper]).await;
1197
1198 let mut count_before = 0;
1200 blob.list_keys_and_metadata("", &mut |_| {
1201 count_before += 1;
1202 })
1203 .await
1204 .expect("list_keys failed");
1205 for _ in 0..5 {
1206 let new_upper = upper + 1;
1207 write.expect_compare_and_append(&[], upper, new_upper).await;
1208 upper = new_upper;
1209 }
1210 let mut count_after = 0;
1211 blob.list_keys_and_metadata("", &mut |_| {
1212 count_after += 1;
1213 })
1214 .await
1215 .expect("list_keys failed");
1216 assert_eq!(count_after, count_before);
1217 }
1218
1219 #[mz_persist_proc::test(tokio::test)]
1220 #[cfg_attr(miri, ignore)] async fn compare_and_append_batch_multi(dyncfgs: ConfigUpdates) {
1222 let data0 = vec![
1223 (("1".to_owned(), "one".to_owned()), 1, 1),
1224 (("2".to_owned(), "two".to_owned()), 2, 1),
1225 (("4".to_owned(), "four".to_owned()), 4, 1),
1226 ];
1227 let data1 = vec![
1228 (("1".to_owned(), "one".to_owned()), 1, 1),
1229 (("2".to_owned(), "two".to_owned()), 2, 1),
1230 (("3".to_owned(), "three".to_owned()), 3, 1),
1231 ];
1232
1233 let (mut write, mut read) = new_test_client(&dyncfgs)
1234 .await
1235 .expect_open::<String, String, u64, i64>(ShardId::new())
1236 .await;
1237
1238 let mut batch0 = write.expect_batch(&data0, 0, 5).await;
1239 let mut batch1 = write.expect_batch(&data1, 0, 4).await;
1240
1241 write
1242 .expect_compare_and_append_batch(&mut [&mut batch0, &mut batch1], 0, 4)
1243 .await;
1244
1245 let batch = write
1246 .machine
1247 .unleased_snapshot(&Antichain::from_elem(3))
1248 .await
1249 .expect("just wrote this")
1250 .into_element();
1251
1252 assert!(batch.runs().count() >= 2);
1253
1254 let expected = vec![
1255 (("1".to_owned(), "one".to_owned()), 1, 2),
1256 (("2".to_owned(), "two".to_owned()), 2, 2),
1257 (("3".to_owned(), "three".to_owned()), 3, 1),
1258 ];
1259 let mut actual = read.expect_snapshot_and_fetch(3).await;
1260 consolidate_updates(&mut actual);
1261 assert_eq!(actual, all_ok(&expected, 3));
1262 }
1263
1264 #[mz_ore::test]
1265 fn writer_id_human_readable_serde() {
1266 #[derive(Debug, Serialize, Deserialize)]
1267 struct Container {
1268 writer_id: WriterId,
1269 }
1270
1271 let id = WriterId::from_str("w00000000-1234-5678-0000-000000000000").expect("valid id");
1273 assert_eq!(
1274 id,
1275 serde_json::from_value(serde_json::to_value(id.clone()).expect("serializable"))
1276 .expect("deserializable")
1277 );
1278
1279 assert_eq!(
1281 id,
1282 serde_json::from_str("\"w00000000-1234-5678-0000-000000000000\"")
1283 .expect("deserializable")
1284 );
1285
1286 let json = json!({ "writer_id": id });
1288 assert_eq!(
1289 "{\"writer_id\":\"w00000000-1234-5678-0000-000000000000\"}",
1290 &json.to_string()
1291 );
1292 let container: Container = serde_json::from_value(json).expect("deserializable");
1293 assert_eq!(container.writer_id, id);
1294 }
1295
1296 #[mz_persist_proc::test(tokio::test)]
1297 #[cfg_attr(miri, ignore)] async fn hollow_batch_roundtrip(dyncfgs: ConfigUpdates) {
1299 let data = vec![
1300 (("1".to_owned(), "one".to_owned()), 1, 1),
1301 (("2".to_owned(), "two".to_owned()), 2, 1),
1302 (("3".to_owned(), "three".to_owned()), 3, 1),
1303 ];
1304
1305 let (mut write, mut read) = new_test_client(&dyncfgs)
1306 .await
1307 .expect_open::<String, String, u64, i64>(ShardId::new())
1308 .await;
1309
1310 let batch = write.expect_batch(&data, 0, 4).await;
1315 let hollow_batch = batch.into_transmittable_batch();
1316 let mut rehydrated_batch = write.batch_from_transmittable_batch(hollow_batch);
1317
1318 write
1319 .expect_compare_and_append_batch(&mut [&mut rehydrated_batch], 0, 4)
1320 .await;
1321
1322 let expected = vec![
1323 (("1".to_owned(), "one".to_owned()), 1, 1),
1324 (("2".to_owned(), "two".to_owned()), 2, 1),
1325 (("3".to_owned(), "three".to_owned()), 3, 1),
1326 ];
1327 let mut actual = read.expect_snapshot_and_fetch(3).await;
1328 consolidate_updates(&mut actual);
1329 assert_eq!(actual, all_ok(&expected, 3));
1330 }
1331
1332 #[mz_persist_proc::test(tokio::test)]
1333 #[cfg_attr(miri, ignore)] async fn wait_for_upper_past(dyncfgs: ConfigUpdates) {
1335 let client = new_test_client(&dyncfgs).await;
1336 let (mut write, _) = client.expect_open::<(), (), u64, i64>(ShardId::new()).await;
1337 let five = Antichain::from_elem(5);
1338
1339 assert_eq!(write.wait_for_upper_past(&five).now_or_never(), None);
1341
1342 write
1344 .expect_compare_and_append(&[(((), ()), 1, 1)], 0, 5)
1345 .await;
1346 assert_eq!(write.wait_for_upper_past(&five).now_or_never(), None);
1347
1348 write
1350 .expect_compare_and_append(&[(((), ()), 5, 1)], 5, 7)
1351 .await;
1352 assert_eq!(write.wait_for_upper_past(&five).now_or_never(), Some(()));
1353 assert_eq!(write.upper(), &Antichain::from_elem(7));
1354
1355 assert_eq!(
1358 write
1359 .wait_for_upper_past(&Antichain::from_elem(2))
1360 .now_or_never(),
1361 Some(())
1362 );
1363 assert_eq!(write.upper(), &Antichain::from_elem(7));
1364 }
1365
1366 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1367 #[cfg_attr(miri, ignore)] async fn fetch_recent_upper_linearized() {
1369 type Timestamp = u64;
1370 let max_upper = 1000;
1371
1372 let shard_id = ShardId::new();
1373 let mut clients = PersistClientCache::new_no_metrics();
1374 let upper_writer_client = clients.open(PersistLocation::new_in_mem()).await.unwrap();
1375 let (mut upper_writer, _) = upper_writer_client
1376 .expect_open::<(), (), Timestamp, i64>(shard_id)
1377 .await;
1378 clients.clear_state_cache();
1381 let upper_reader_client = clients.open(PersistLocation::new_in_mem()).await.unwrap();
1382 let (mut upper_reader, _) = upper_reader_client
1383 .expect_open::<(), (), Timestamp, i64>(shard_id)
1384 .await;
1385 let (tx, rx) = mpsc::channel();
1386
1387 let task = task::spawn(|| "upper-reader", async move {
1388 let mut upper = Timestamp::MIN;
1389
1390 while upper < max_upper {
1391 while let Ok(new_upper) = rx.try_recv() {
1392 upper = new_upper;
1393 }
1394
1395 let recent_upper = upper_reader
1396 .fetch_recent_upper()
1397 .await
1398 .as_option()
1399 .cloned()
1400 .expect("u64 is totally ordered and the shard is not finalized");
1401 assert!(
1402 recent_upper >= upper,
1403 "recent upper {recent_upper:?} is less than known upper {upper:?}"
1404 );
1405 }
1406 });
1407
1408 for upper in Timestamp::MIN..max_upper {
1409 let next_upper = upper + 1;
1410 upper_writer
1411 .expect_compare_and_append(&[], upper, next_upper)
1412 .await;
1413 tx.send(next_upper).expect("send failed");
1414 }
1415
1416 task.await;
1417 }
1418}