diff --git a/AGENTS.md b/AGENTS.md index 6bffa109536..c6f2bf6069b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,7 @@ go build -tags libsqlite3 ./go/bindings # Regenerate checked-in protobuf (required after .proto changes) mise run build:go-protobufs mise run build:rust-protobufs +cargo fmt --all # remove format-only churn from codegen # Run pgTAP SQL Tests mise run ci:sql-tap diff --git a/Cargo.lock b/Cargo.lock index bc6e4d8d78e..df9be242ddf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8003,6 +8003,7 @@ dependencies = [ "anyhow", "build", "bytes", + "chrono", "doc", "e2e-support", "flow-client-next", diff --git a/crates/dekaf/src/read.rs b/crates/dekaf/src/read.rs index c00e35bf03b..c2fba786222 100644 --- a/crates/dekaf/src/read.rs +++ b/crates/dekaf/src/read.rs @@ -342,8 +342,9 @@ impl Read { }; let (producer, clock, flags) = gazette::uuid::parse_str(uuid.as_str())?; - // Is this a non-content control document, such as a transaction ACK? - let is_control = flags.is_ack(); + // Is this a non-content control document, such as a transaction ACK + // or a backfill control message? + let is_control = flags.is_ack() || flags.is_control(); let should_skip = match (self.not_before, self.not_after) { (Some(not_before), Some(not_after)) => clock < not_before || clock > not_after, diff --git a/crates/dekaf/src/topology.rs b/crates/dekaf/src/topology.rs index c15881cb600..85610459af3 100644 --- a/crates/dekaf/src/topology.rs +++ b/crates/dekaf/src/topology.rs @@ -261,7 +261,7 @@ impl Collection { let key_schema = avro::key_to_avro(&key_ptr, collection_schema_shape); - let (not_before, not_after) = ( + let (mut not_before, not_after) = ( binding.not_before.map(|b| { uuid::Clock::from_unix(b.seconds.try_into().unwrap(), b.nanos.try_into().unwrap()) }), @@ -270,6 +270,23 @@ impl Collection { }), ); + // Honor truncated-at journal labels: if any partition carries a + // truncated-at label, use the max across all partitions and the + // binding's not_before as the effective not_before. + for partition in &partitions { + if let Some(truncated_at_str) = partition + .spec + .labels + .as_ref() + .and_then(|ls| ls.labels.iter().find(|l| l.name == ::labels::TRUNCATED_AT)) + .map(|l| &l.value) + { + let truncated_clock = + uuid::Clock::from_u64(::labels::parse_truncated_at(truncated_at_str)?); + not_before = Some(not_before.map_or(truncated_clock, |nb| nb.max(truncated_clock))); + } + } + tracing::debug!( collection_name, partitions = partitions.len(), diff --git a/crates/derive/src/extract_api.rs b/crates/derive/src/extract_api.rs index daf048ae442..376afe78f28 100644 --- a/crates/derive/src/extract_api.rs +++ b/crates/derive/src/extract_api.rs @@ -135,8 +135,11 @@ impl cgo::Service for API { } })?; - if proto_gazette::message_flags::ACK_TXN & uuid.node != 0 { - // Transaction acknowledgements aren't expected to validate. + if proto_gazette::message_flags::ACK_TXN & uuid.node != 0 + || proto_gazette::message_flags::CONTROL & uuid.node != 0 + { + // Transaction acknowledgements and application control + // messages aren't expected to validate. } else if let Some(validator) = &mut state.validator { validator .validate(&doc, |_| None) @@ -167,7 +170,7 @@ impl cgo::Service for API { #[cfg(test)] mod test { - use super::{API, Code, extract_uuid_parts}; + use super::{API, Code, Error, extract_uuid_parts}; use cgo::Service; use proto_flow::flow; use serde_json::{Value, json}; @@ -282,4 +285,48 @@ mod test { insta::assert_debug_snapshot!((String::from_utf8_lossy(&arena), out)); } + + #[test] + fn ack_and_control_docs_skip_validation() { + let extract_validating = |uuid_doc: &str| -> Result<(), Error> { + let mut svc = API::create(); + let (mut arena, mut out) = (Vec::new(), Vec::new()); + svc.invoke_message( + Code::Configure as u32, + flow::extract_api::Config { + uuid_ptr: "/0".to_string(), + schema_json: bytes::Bytes::from_static(br#"{"type":"object"}"#), + ..Default::default() + }, + &mut arena, + &mut out, + ) + .unwrap(); + svc.invoke( + Code::Extract as u32, + uuid_doc.as_bytes(), + &mut arena, + &mut out, + ) + }; + + // A plain (OUTSIDE_TXN, flags 0x0) doc is validated and fails the schema. + assert!( + matches!( + extract_validating(r#"["9f2952f3-c6a3-11ea-8800-080607050309", 0, 0]"#), + Err(Error::FailedValidation(_)), + ), + "a plain doc is validated and fails the object schema", + ); + // ACK_TXN (0x2) and CONTROL (0x4) docs — e.g. backfill begin/complete — + // skip validation, so the same array-shaped doc is accepted. + assert!( + extract_validating(r#"["9f2952f3-c6a3-11ea-8802-080607050309", 0, 0]"#).is_ok(), + "an ACK_TXN doc skips validation", + ); + assert!( + extract_validating(r#"["9f2952f3-c6a3-11ea-8804-080607050309", 0, 0]"#).is_ok(), + "a CONTROL doc skips validation", + ); + } } diff --git a/crates/doc/src/combine/memtable.rs b/crates/doc/src/combine/memtable.rs index 797332645b4..8088da9332f 100644 --- a/crates/doc/src/combine/memtable.rs +++ b/crates/doc/src/combine/memtable.rs @@ -66,7 +66,7 @@ impl Entries { } fn compact(&mut self, alloc: &'static Bump) -> Result<(), Error> { - // `sort_ord` orders over (binding, key, !front): + // `sort_ord` orders over (binding, key, !prior_gen, !front): // For each (binding, key), we take front() entries first, and further // rely on sort preserving the order in which entries were added. // This maintains the left-to-right associative ordering of reductions. @@ -80,6 +80,7 @@ impl Entries { // Cold path: Meta prefix was equal, so compare the full key. compare_root_keys(&self.spec.keys[l.meta.binding()], &l.root, &r.root) }) + .then_with(|| l.meta.prior_gen().cmp(&r.meta.prior_gen()).reverse()) .then_with(|| l.meta.front().cmp(&r.meta.front()).reverse()) }; let validators = &mut self.spec.validators; @@ -234,6 +235,25 @@ impl MemTable { /// Add the document to the MemTable. pub fn add<'s>(&'s self, binding: u16, root: HeapNode<'s>, front: bool) -> Result<(), Error> { + self.add_with_flags(binding, root, front, false, false) + } + + /// Add a document from a prior generation to the MemTable. + /// A prior-generation document is not reduced with current-generation + /// documents and is never drained: it transfers front() onto a following + /// current-generation document of its same key, and is otherwise discarded. + pub fn add_prior_gen<'s>(&'s self, binding: u16, root: HeapNode<'s>) -> Result<(), Error> { + self.add_with_flags(binding, root, true, true, true) + } + + fn add_with_flags<'s>( + &'s self, + binding: u16, + root: HeapNode<'s>, + front: bool, + prior_gen: bool, + known_valid: bool, + ) -> Result<(), Error> { // Safety: mutable borrow does not escape this function. let entries = unsafe { &mut *self.entries.get() }; let root = unsafe { std::mem::transmute::, HeapNode<'static>>(root) }; @@ -245,12 +265,7 @@ impl MemTable { &mut entries.scratch, None, ); - let meta = Meta::new( - binding, - &entries.scratch, - front, - false, // `known_valid` - ); + let meta = Meta::new(binding, &entries.scratch, front, known_valid, prior_gen); entries.queued.push(HeapEntry { meta, @@ -303,7 +318,13 @@ impl MemTable { let raw = embedded.as_u64le_slice(); let root = HeapRoot::Embedded(raw.as_ptr(), raw.len() as u32); - let meta = Meta::from_packed_prefix(binding, packed_key_prefix, front, known_valid); + let meta = Meta::from_packed_prefix( + binding, + packed_key_prefix, + front, + known_valid, + false, // prior generation documents are suppressed by the shuffle reader + ); entries.queued.push(HeapEntry { meta, root }); if entries.should_compact() { @@ -432,6 +453,33 @@ impl MemDrainer { let Some(HeapEntry { mut meta, mut root }) = self.it.next() else { return Ok(None); }; + + // Prior-generation entries are never yielded: their content is + // superseded. One followed by its same-(binding, key) + // current-generation entry transfers front() onto it — the key was + // loaded, though its stale content is dropped — while an orphan + // (no such successor) is discarded outright. + while meta.prior_gen() { + let Some(next) = self.it.next() else { + return Ok(None); // Trailing orphan: drain is complete. + }; + + // Does `next` pair with the discarded prior-generation entry, + // as its current-generation sibling of the same (binding, key)? + // Consecutive prior-generation entries don't pair (and don't + // compare keys): each defers to the following iteration. + let paired = !next.meta.prior_gen() + && meta.0 == next.meta.0 + && compare_root_keys(&self.spec.keys[meta.binding()], &root, &next.root).is_eq(); + + HeapEntry { meta, root } = next; + self.in_group = false; + + if paired { + meta.set_front(); + } + } + let is_full = self.spec.is_full[meta.binding()]; let keys = self.spec.keys[meta.binding()].as_ref(); let name = &self.spec.names[meta.binding()]; @@ -645,6 +693,111 @@ mod test { .unwrap(); } + #[test] + fn test_prior_gen_partition() { + // Prior-generation rows never reduce with, and are never drained + // alongside, current-generation rows: a paired row transfers front() + // onto its current-generation successor, while an orphan is discarded. + // Behavior must be identical whether drained in memory or through a + // spill file. + let make = || { + let schema = build_schema( + &url::Url::parse("http://example/schema").unwrap(), + &json!({ + "properties": { "v": { "type": "array", "reduce": { "strategy": "append" } } }, + "reduce": { "strategy": "merge" } + }), + ) + .unwrap(); + let memtable = MemTable::new(Spec::with_one_binding( + true, + vec![Extractor::with_default( + "/key", + &SerPolicy::noop(), + json!("def"), + )], + "test", + Vec::new(), + Validator::new(schema).unwrap(), + )); + + let add = |doc: Value, front: bool| { + memtable + .add(0, HeapNode::from_node(&doc, memtable.alloc()), front) + .unwrap() + }; + let add_prior = |doc: Value| { + memtable + .add_prior_gen(0, HeapNode::from_node(&doc, memtable.alloc())) + .unwrap() + }; + + // Same key, a prior-generation run + its current generation: + // drained as one doc ["fresh"] with front() transferred (the run + // is held back from compaction into a single entry, so the drain + // skips through multiple prior-generation entries). + add(json!({"key": "k", "v": ["fresh"]}), false); + add_prior(json!({"key": "k", "v": ["stale"]})); + add_prior(json!({"key": "k", "v": ["stale2"]})); + // A schema-invalid orphan prior-generation row is discarded without + // FailedValidation: add_prior_gen marks it `known_valid` and it's + // dropped before drain-time validation. + add_prior(json!({"key": "bad", "v": 42})); + + memtable + }; + + // Project a drained doc to (v, prior_gen, front). A `fn` (not a closure) + // so it can be reused across both drainers. + fn project(doc: crate::combine::DrainedDoc) -> (serde_json::Value, bool, bool) { + let root = serde_json::to_value(SerPolicy::noop().on_owned(&doc.root)).unwrap(); + (root["v"].clone(), doc.meta.prior_gen(), doc.meta.front()) + } + let expected = vec![ + (json!(["fresh"]), false, true), // "k": stale content dropped, front() gained + ]; + + // In-memory drain. + let in_memory = make() + .try_into_drainer() + .unwrap() + .map_ok(project) + .collect::, _>>() + .unwrap(); + assert_eq!(in_memory, expected, "in-memory drain"); + + // Spill drain must partition identically — including across segments: + // a second segment holds the current-generation sibling of "bad", + // pairing with the first segment's (invalid, never-validated) row. + let mut spill = SpillWriter::new(io::Cursor::new(Vec::new())).unwrap(); + let spec = make().spill(&mut spill, CHUNK_TARGET_SIZE).unwrap(); + + let memtable = MemTable::new(spec); + memtable + .add( + 0, + HeapNode::from_node(&json!({"key": "bad", "v": ["fixed"]}), memtable.alloc()), + false, + ) + .unwrap(); + let spec = memtable.spill(&mut spill, CHUNK_TARGET_SIZE).unwrap(); + + let (spill, ranges) = spill.into_parts(); + let spilled = crate::combine::SpillDrainer::new(spec, spill, &ranges) + .unwrap() + .map_ok(project) + .collect::, _>>() + .unwrap(); + assert_eq!( + spilled, + vec![ + (json!(["fixed"]), false, true), // "bad": paired across segments + (json!(["fresh"]), false, true), + ], + "spill drain" + ); + } + #[test] fn test_memtable_combine_reduce_sequence() { let key = vec![Extractor::with_default( diff --git a/crates/doc/src/combine/mod.rs b/crates/doc/src/combine/mod.rs index 9b187eb9b1f..ddda9f4ea0d 100644 --- a/crates/doc/src/combine/mod.rs +++ b/crates/doc/src/combine/mod.rs @@ -270,7 +270,7 @@ impl Combiner { impl Meta { #[inline] - fn new(binding: u16, key: &[u8], front: bool, known_valid: bool) -> Self { + fn new(binding: u16, key: &[u8], front: bool, known_valid: bool, prior_gen: bool) -> Self { let b = binding.to_be_bytes(); let mut packed = [b[0], b[1], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // Copy up to 13 bytes of `key` into the packed representation. @@ -286,6 +286,9 @@ impl Meta { if known_valid { flags |= META_FLAG_KNOWN_VALID; } + if prior_gen { + flags |= META_FLAG_PRIOR_GEN; + } Self(packed, flags) } @@ -297,6 +300,7 @@ impl Meta { packed_key_prefix: &[u8; 16], front: bool, known_valid: bool, + prior_gen: bool, ) -> Self { let b = binding.to_be_bytes(); let p = packed_key_prefix; @@ -313,6 +317,9 @@ impl Meta { if known_valid { flags |= META_FLAG_KNOWN_VALID; } + if prior_gen { + flags |= META_FLAG_PRIOR_GEN; + } Self(packed, flags) } @@ -330,6 +337,16 @@ impl Meta { self.1 & META_FLAG_FRONT != 0 } + /// Was this entry from a prior generation? + /// Prior generation documents are not combined with the current generation + /// and are never drained: one followed by a same-(binding, key) + /// current-generation entry transfers front() onto that entry, and is + /// otherwise discarded. + #[inline] + pub fn prior_gen(&self) -> bool { + self.1 & META_FLAG_PRIOR_GEN != 0 + } + /// Is this entry known to be valid? Known-valid entries skip validation /// during spill/drain, and are assumed to not need redaction (validation /// drives redaction). @@ -377,6 +394,11 @@ impl Meta { fn set_not_associative(&mut self) { self.1 |= META_FLAG_NOT_ASSOCIATIVE; } + + #[inline] + fn set_front(&mut self) { + self.1 |= META_FLAG_FRONT; + } } impl std::fmt::Debug for Meta { @@ -396,6 +418,9 @@ impl std::fmt::Debug for Meta { if self.known_valid() { s.field(&"V"); } + if self.prior_gen() { + s.field(&"PG"); + } s.finish() } } @@ -408,6 +433,8 @@ const META_FLAG_NOT_ASSOCIATIVE: u8 = 0x02; const META_FLAG_DELETED: u8 = 0x04; // Flag marking this entry is known to be valid against its schema. const META_FLAG_KNOWN_VALID: u8 = 0x08; +// Flag marking a prior generation entry. +const META_FLAG_PRIOR_GEN: u8 = 0x10; // The number of used bytes within a Bump allocator. fn bump_mem_used(alloc: &bumpalo::Bump) -> usize { diff --git a/crates/doc/src/combine/spill.rs b/crates/doc/src/combine/spill.rs index e207058834a..fd9413e5177 100644 --- a/crates/doc/src/combine/spill.rs +++ b/crates/doc/src/combine/spill.rs @@ -297,7 +297,7 @@ impl Ord for Segment { fn cmp(&self, other: &Self) -> cmp::Ordering { let (l, r) = (&self.head, &other.head); - // Order entries on (binding, key, !front, spill-order): + // Order entries on (binding, key, !prior_gen, !front, spill-order): // For each (binding, key), we take front() entries first, and then // take the Segment which was produced into the spill file first. // This maintains the left-to-right associative ordering of reductions. @@ -309,6 +309,7 @@ impl Ord for Segment { .then_with(|| { Extractor::compare_key(&self.keys[l.meta.binding()], l.root.get(), r.root.get()) }) + .then_with(|| l.meta.prior_gen().cmp(&r.meta.prior_gen()).reverse()) .then_with(|| l.meta.front().cmp(&r.meta.front()).reverse()) .then_with(|| self.next.start.cmp(&other.next.start)) } @@ -351,7 +352,43 @@ impl SpillDrainer { self.heap.push(cmp::Reverse(segment)); } - let Entry { mut meta, root } = entry; + let Entry { mut meta, mut root } = entry; + + // Prior-generation entries are never yielded: their content is + // superseded. One followed by its same-(binding, key) + // current-generation entry transfers front() onto it — the key was + // loaded, though its stale content is dropped — while an orphan + // (no such successor) is discarded outright. + while meta.prior_gen() { + let Some(cmp::Reverse(segment)) = self.heap.pop() else { + return Ok(None); // Trailing orphan: drain is complete. + }; + let (next, segment) = segment.pop_head(&mut self.spill)?; + if let Some(segment) = segment { + self.heap.push(cmp::Reverse(segment)); + } + + // Does `next` pair with the discarded prior-generation entry, + // as its current-generation sibling of the same (binding, key)? + // Consecutive prior-generation entries don't pair (and don't + // compare keys): each defers to the following iteration. + let paired = !next.meta.prior_gen() + && meta.0 == next.meta.0 + && Extractor::compare_key( + &self.spec.keys[meta.binding()], + root.get(), + next.root.get(), + ) + .is_eq(); + + Entry { meta, root } = next; + self.in_group = false; + + if paired { + meta.set_front(); + } + } + let is_full = self.spec.is_full[meta.binding()]; let key = self.spec.keys[meta.binding()].as_ref(); let validator = &mut self.spec.validators[meta.binding()]; @@ -1094,7 +1131,7 @@ mod test { .map(|(binding, value, front)| HeapEntry { // !front() entries are marked known_valid, simulating having // been validated during memtable::spill(). - meta: Meta::new(*binding, &[], *front, !front), + meta: Meta::new(*binding, &[], *front, !front, false), root: HeapRoot::from_heap_node(HeapNode::from_node(value, &alloc)), }) .collect() @@ -1126,7 +1163,7 @@ mod test { let raw = buf as &[crate::embedded::U64Le]; HeapEntry { - meta: Meta::new(*binding, &[], *front, !front), + meta: Meta::new(*binding, &[], *front, !front, false), root: HeapRoot::Embedded(raw.as_ptr(), raw.len() as u32), } }) diff --git a/crates/flowctl/src/shuffle_read.rs b/crates/flowctl/src/shuffle_read.rs index aeb2411ad1c..e5fb077da4f 100644 --- a/crates/flowctl/src/shuffle_read.rs +++ b/crates/flowctl/src/shuffle_read.rs @@ -221,8 +221,9 @@ async fn drain_loop( } for entry in scan.block_iter() { - // Skip transaction acknowledgements: they carry no user content. - if uuid::Flags(entry.meta.flags.to_native()).is_ack() { + // Skip ACKs and backfill CONTROL docs: neither carries user content. + let flags = uuid::Flags(entry.meta.flags.to_native()); + if flags.is_ack() || flags.is_control() { continue; } on_entry(entry.meta.binding.to_native(), &entry)?; diff --git a/crates/labels/src/lib.rs b/crates/labels/src/lib.rs index 970aab140c6..60cb672249e 100644 --- a/crates/labels/src/lib.rs +++ b/crates/labels/src/lib.rs @@ -18,6 +18,7 @@ pub const KEY_BEGIN_MIN: &str = "00000000"; pub const KEY_END: &str = "estuary.dev/key-end"; pub const KEY_END_MAX: &str = "ffffffff"; pub const MANAGED_BY_FLOW: &str = "estuary.dev/flow"; +pub const TRUNCATED_AT: &str = "estuary.dev/truncated-at"; // ShardSpec labels. pub const TASK_NAME: &str = "estuary.dev/task-name"; @@ -187,10 +188,12 @@ pub fn is_data_plane_label(label: &str) -> bool { return true; } match label { - // Key and R-Clock splits are performed within the data-plane. - CORDON | KEY_BEGIN | KEY_END | RCLOCK_BEGIN | RCLOCK_END | SPLIT_SOURCE | SPLIT_TARGET => { - true - } + // Labels the data-plane runtime applies to live journals/shards — key, + // r-clock, and shard splits, cordoning, and the backfill truncation + // boundary — which activation and partition splits must preserve rather + // than rebuild away. + CORDON | KEY_BEGIN | KEY_END | RCLOCK_BEGIN | RCLOCK_END | SPLIT_SOURCE | SPLIT_TARGET + | TRUNCATED_AT => true, _ => false, } } @@ -218,6 +221,30 @@ pub fn expect_one_u32(set: &LabelSet, name: &str) -> Result { Ok(parsed) } +/// Format a Gazette message clock (`u64`) as a [`TRUNCATED_AT`] label value: a +/// fixed-width, 16-character lowercase hex string. +pub fn truncated_at_value(clock: u64) -> String { + format!("{clock:016x}") +} + +/// Parse a [`TRUNCATED_AT`] label value produced by [`truncated_at_value`] back +/// into its `u64` clock. +pub fn parse_truncated_at(value: &str) -> Result { + // Require the canonical encoding produced by `truncated_at_value` (16 + // lowercase hex chars): a bare `from_str_radix` would also accept a leading + // `+` or uppercase hex, yielding a value that no longer round-trips or sorts + // lexically. Confirm by round-trip so only canonical labels parse. + if let Ok(clock) = u64::from_str_radix(value, 16) + && truncated_at_value(clock) == value + { + return Ok(clock); + } + Err(Error::InvalidValue { + name: TRUNCATED_AT.to_string(), + value: value.to_string(), + }) +} + pub fn expect_one<'s>(set: &'s LabelSet, name: &str) -> Result<&'s str, Error> { let labels = values(set, name); @@ -634,4 +661,29 @@ mod test { .unwrap_err(); assert!(matches!(err, Error::NotSorted(..))); } + + #[test] + fn truncated_at_value_roundtrips_and_sorts() { + // Always 16 lowercase hex chars, and round-trips. + for clock in [0u64, 1, 0xdead_beef, u64::MAX, 1_750_000_000 << 4] { + let v = super::truncated_at_value(clock); + assert_eq!(v.len(), 16, "value {v:?} is not fixed-width"); + assert_eq!(super::parse_truncated_at(&v).unwrap(), clock); + } + // Lexical (string) order matches clock (numeric) order. + let mut by_value = [3u64, 1 << 40, 2, u64::MAX, 1 << 4]; + let mut numeric = by_value; + by_value.sort_by_key(|c| super::truncated_at_value(*c)); + numeric.sort(); + assert_eq!(by_value, numeric); + + // Malformed values error (a stale 19-digit decimal, non-hex, empty). + assert!(super::parse_truncated_at("1234605616436508552").is_err()); + assert!(super::parse_truncated_at("zzzzzzzzzzzzzzzz").is_err()); + assert!(super::parse_truncated_at("").is_err()); + // Non-canonical 16-char forms `from_str_radix` would otherwise accept: + // a leading sign, and uppercase hex (neither round-trips). + assert!(super::parse_truncated_at("+000000000000001").is_err()); + assert!(super::parse_truncated_at("DEADBEEF00000001").is_err()); + } } diff --git a/crates/proto-flow/src/capture.rs b/crates/proto-flow/src/capture.rs index cfc24c54add..333cf4e62ad 100644 --- a/crates/proto-flow/src/capture.rs +++ b/crates/proto-flow/src/capture.rs @@ -181,6 +181,10 @@ pub struct Response { pub sourced_schema: ::core::option::Option, #[prost(message, optional, tag = "7")] pub checkpoint: ::core::option::Option, + #[prost(message, optional, tag = "9")] + pub backfill_begin: ::core::option::Option, + #[prost(message, optional, tag = "10")] + pub backfill_complete: ::core::option::Option, /// Reserved for internal use. #[prost(bytes = "bytes", tag = "100")] pub internal: ::prost::bytes::Bytes, @@ -371,4 +375,24 @@ pub mod response { #[prost(message, optional, tag = "1")] pub state: ::core::option::Option, } + /// Signals the start of a backfill for a binding. + /// + /// A control message (BackfillBegin or BackfillComplete) must stand alone + /// in its connector checkpoint: the checkpoint must contain only the + /// control message followed by the terminating Checkpoint response, with + /// no Captured, SourcedSchema, or other control messages. The runtime + /// enforces this rule and will fail the session on violation. + #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] + pub struct BackfillBegin { + #[prost(uint32, tag = "1")] + pub binding: u32, + } + /// Signals the end of a backfill for a binding. + /// + /// See BackfillBegin for the "stands alone in its checkpoint" rule. + #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] + pub struct BackfillComplete { + #[prost(uint32, tag = "1")] + pub binding: u32, + } } diff --git a/crates/proto-flow/src/capture.serde.rs b/crates/proto-flow/src/capture.serde.rs index 4b5d3f18a6e..50365487d5d 100644 --- a/crates/proto-flow/src/capture.serde.rs +++ b/crates/proto-flow/src/capture.serde.rs @@ -1233,6 +1233,12 @@ impl serde::Serialize for Response { if self.checkpoint.is_some() { len += 1; } + if self.backfill_begin.is_some() { + len += 1; + } + if self.backfill_complete.is_some() { + len += 1; + } if !self.internal.is_empty() { len += 1; } @@ -1261,6 +1267,12 @@ impl serde::Serialize for Response { if let Some(v) = self.checkpoint.as_ref() { struct_ser.serialize_field("checkpoint", v)?; } + if let Some(v) = self.backfill_begin.as_ref() { + struct_ser.serialize_field("backfillBegin", v)?; + } + if let Some(v) = self.backfill_complete.as_ref() { + struct_ser.serialize_field("backfillComplete", v)?; + } if !self.internal.is_empty() { #[allow(clippy::needless_borrow)] #[allow(clippy::needless_borrows_for_generic_args)] @@ -1285,6 +1297,10 @@ impl<'de> serde::Deserialize<'de> for Response { "sourced_schema", "sourcedSchema", "checkpoint", + "backfill_begin", + "backfillBegin", + "backfill_complete", + "backfillComplete", "internal", "$internal", ]; @@ -1299,6 +1315,8 @@ impl<'de> serde::Deserialize<'de> for Response { Captured, SourcedSchema, Checkpoint, + BackfillBegin, + BackfillComplete, Internal, __SkipField__, } @@ -1330,6 +1348,8 @@ impl<'de> serde::Deserialize<'de> for Response { "captured" => Ok(GeneratedField::Captured), "sourcedSchema" | "sourced_schema" => Ok(GeneratedField::SourcedSchema), "checkpoint" => Ok(GeneratedField::Checkpoint), + "backfillBegin" | "backfill_begin" => Ok(GeneratedField::BackfillBegin), + "backfillComplete" | "backfill_complete" => Ok(GeneratedField::BackfillComplete), "$internal" | "internal" => Ok(GeneratedField::Internal), _ => Ok(GeneratedField::__SkipField__), } @@ -1358,6 +1378,8 @@ impl<'de> serde::Deserialize<'de> for Response { let mut captured__ = None; let mut sourced_schema__ = None; let mut checkpoint__ = None; + let mut backfill_begin__ = None; + let mut backfill_complete__ = None; let mut internal__ = None; while let Some(k) = map_.next_key()? { match k { @@ -1409,6 +1431,18 @@ impl<'de> serde::Deserialize<'de> for Response { } checkpoint__ = map_.next_value()?; } + GeneratedField::BackfillBegin => { + if backfill_begin__.is_some() { + return Err(serde::de::Error::duplicate_field("backfillBegin")); + } + backfill_begin__ = map_.next_value()?; + } + GeneratedField::BackfillComplete => { + if backfill_complete__.is_some() { + return Err(serde::de::Error::duplicate_field("backfillComplete")); + } + backfill_complete__ = map_.next_value()?; + } GeneratedField::Internal => { if internal__.is_some() { return Err(serde::de::Error::duplicate_field("$internal")); @@ -1431,6 +1465,8 @@ impl<'de> serde::Deserialize<'de> for Response { captured: captured__, sourced_schema: sourced_schema__, checkpoint: checkpoint__, + backfill_begin: backfill_begin__, + backfill_complete: backfill_complete__, internal: internal__.unwrap_or_default(), }) } @@ -1551,6 +1587,200 @@ impl<'de> serde::Deserialize<'de> for response::Applied { deserializer.deserialize_struct("capture.Response.Applied", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for response::BackfillBegin { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.binding != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("capture.Response.BackfillBegin", len)?; + if self.binding != 0 { + struct_ser.serialize_field("binding", &self.binding)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for response::BackfillBegin { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "binding", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Binding, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "binding" => Ok(GeneratedField::Binding), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = response::BackfillBegin; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct capture.Response.BackfillBegin") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut binding__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Binding => { + if binding__.is_some() { + return Err(serde::de::Error::duplicate_field("binding")); + } + binding__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(response::BackfillBegin { + binding: binding__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("capture.Response.BackfillBegin", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for response::BackfillComplete { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.binding != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("capture.Response.BackfillComplete", len)?; + if self.binding != 0 { + struct_ser.serialize_field("binding", &self.binding)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for response::BackfillComplete { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "binding", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Binding, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "binding" => Ok(GeneratedField::Binding), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = response::BackfillComplete; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct capture.Response.BackfillComplete") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut binding__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Binding => { + if binding__.is_some() { + return Err(serde::de::Error::duplicate_field("binding")); + } + binding__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(response::BackfillComplete { + binding: binding__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("capture.Response.BackfillComplete", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for response::Captured { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto-flow/src/materialize.rs b/crates/proto-flow/src/materialize.rs index b3be5608d6e..ac508cd7d96 100644 --- a/crates/proto-flow/src/materialize.rs +++ b/crates/proto-flow/src/materialize.rs @@ -184,7 +184,7 @@ pub mod request { /// Flush loads. No further Loads will be sent in this transaction, /// and the runtime will await the connectors's remaining Loaded /// responses followed by one Flushed response. - #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] + #[derive(Clone, PartialEq, ::prost::Message)] pub struct Flush { /// Aggregated state patches from all shards' prior-transaction Acknowledged /// responses, as a tab-delimited JSON array. Includes this shard's own @@ -193,6 +193,39 @@ pub mod request { /// cooperative multi-shard strategies use this to observe peers' state. #[prost(bytes = "bytes", tag = "1")] pub state_patches_json: ::prost::bytes::Bytes, + #[prost(message, repeated, tag = "2")] + pub backfill_begins: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "3")] + pub backfill_completes: ::prost::alloc::vec::Vec, + } + /// Nested message and enum types in `Flush`. + pub mod flush { + /// Backfill-begin signals observed during this transaction. Populated only on + /// shard zero. + #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] + pub struct BackfillBegin { + #[prost(uint32, tag = "1")] + pub binding: u32, + /// Truncation boundary of the binding's backfill: documents published at or + /// after this time are current, while earlier ones were superseded by the + /// backfill. It equals the begin's own publication time (flow_published_at), + /// and is carried on both begin and complete so connectors need not track it + /// across transactions. + #[prost(message, optional, tag = "2")] + pub timestamp: ::core::option::Option<::pbjson_types::Timestamp>, + } + /// Backfill-complete signals observed during this transaction. Populated only + /// on shard zero. + #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] + pub struct BackfillComplete { + #[prost(uint32, tag = "1")] + pub binding: u32, + /// Truncation boundary of the completed backfill (see BackfillBegin.timestamp): + /// the connector may delete destination rows whose flow_published_at predates + /// this time, as they were superseded by the backfill. + #[prost(message, optional, tag = "2")] + pub timestamp: ::core::option::Option<::pbjson_types::Timestamp>, + } } /// Store documents updated by the current transaction. /// diff --git a/crates/proto-flow/src/materialize.serde.rs b/crates/proto-flow/src/materialize.serde.rs index 24be109e786..c17b56fddc1 100644 --- a/crates/proto-flow/src/materialize.serde.rs +++ b/crates/proto-flow/src/materialize.serde.rs @@ -833,12 +833,24 @@ impl serde::Serialize for request::Flush { if !self.state_patches_json.is_empty() { len += 1; } + if !self.backfill_begins.is_empty() { + len += 1; + } + if !self.backfill_completes.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("materialize.Request.Flush", len)?; if !self.state_patches_json.is_empty() { #[allow(clippy::needless_borrow)] #[allow(clippy::needless_borrows_for_generic_args)] struct_ser.serialize_field("statePatches", &crate::as_raw_json(&self.state_patches_json)?)?; } + if !self.backfill_begins.is_empty() { + struct_ser.serialize_field("backfillBegins", &self.backfill_begins)?; + } + if !self.backfill_completes.is_empty() { + struct_ser.serialize_field("backfillCompletes", &self.backfill_completes)?; + } struct_ser.end() } } @@ -851,11 +863,17 @@ impl<'de> serde::Deserialize<'de> for request::Flush { const FIELDS: &[&str] = &[ "state_patches_json", "statePatches", + "backfill_begins", + "backfillBegins", + "backfill_completes", + "backfillCompletes", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { StatePatchesJson, + BackfillBegins, + BackfillCompletes, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -879,6 +897,8 @@ impl<'de> serde::Deserialize<'de> for request::Flush { { match value { "statePatches" | "state_patches_json" => Ok(GeneratedField::StatePatchesJson), + "backfillBegins" | "backfill_begins" => Ok(GeneratedField::BackfillBegins), + "backfillCompletes" | "backfill_completes" => Ok(GeneratedField::BackfillCompletes), _ => Ok(GeneratedField::__SkipField__), } } @@ -899,6 +919,8 @@ impl<'de> serde::Deserialize<'de> for request::Flush { V: serde::de::MapAccess<'de>, { let mut state_patches_json__ = None; + let mut backfill_begins__ = None; + let mut backfill_completes__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::StatePatchesJson => { @@ -909,6 +931,18 @@ impl<'de> serde::Deserialize<'de> for request::Flush { Some(map_.next_value::()?.0) ; } + GeneratedField::BackfillBegins => { + if backfill_begins__.is_some() { + return Err(serde::de::Error::duplicate_field("backfillBegins")); + } + backfill_begins__ = Some(map_.next_value()?); + } + GeneratedField::BackfillCompletes => { + if backfill_completes__.is_some() { + return Err(serde::de::Error::duplicate_field("backfillCompletes")); + } + backfill_completes__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -916,12 +950,242 @@ impl<'de> serde::Deserialize<'de> for request::Flush { } Ok(request::Flush { state_patches_json: state_patches_json__.unwrap_or_default(), + backfill_begins: backfill_begins__.unwrap_or_default(), + backfill_completes: backfill_completes__.unwrap_or_default(), }) } } deserializer.deserialize_struct("materialize.Request.Flush", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for request::flush::BackfillBegin { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.binding != 0 { + len += 1; + } + if self.timestamp.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("materialize.Request.Flush.BackfillBegin", len)?; + if self.binding != 0 { + struct_ser.serialize_field("binding", &self.binding)?; + } + if let Some(v) = self.timestamp.as_ref() { + struct_ser.serialize_field("timestamp", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for request::flush::BackfillBegin { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "binding", + "timestamp", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Binding, + Timestamp, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "binding" => Ok(GeneratedField::Binding), + "timestamp" => Ok(GeneratedField::Timestamp), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = request::flush::BackfillBegin; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct materialize.Request.Flush.BackfillBegin") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut binding__ = None; + let mut timestamp__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Binding => { + if binding__.is_some() { + return Err(serde::de::Error::duplicate_field("binding")); + } + binding__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Timestamp => { + if timestamp__.is_some() { + return Err(serde::de::Error::duplicate_field("timestamp")); + } + timestamp__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(request::flush::BackfillBegin { + binding: binding__.unwrap_or_default(), + timestamp: timestamp__, + }) + } + } + deserializer.deserialize_struct("materialize.Request.Flush.BackfillBegin", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for request::flush::BackfillComplete { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.binding != 0 { + len += 1; + } + if self.timestamp.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("materialize.Request.Flush.BackfillComplete", len)?; + if self.binding != 0 { + struct_ser.serialize_field("binding", &self.binding)?; + } + if let Some(v) = self.timestamp.as_ref() { + struct_ser.serialize_field("timestamp", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for request::flush::BackfillComplete { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "binding", + "timestamp", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Binding, + Timestamp, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "binding" => Ok(GeneratedField::Binding), + "timestamp" => Ok(GeneratedField::Timestamp), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = request::flush::BackfillComplete; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct materialize.Request.Flush.BackfillComplete") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut binding__ = None; + let mut timestamp__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Binding => { + if binding__.is_some() { + return Err(serde::de::Error::duplicate_field("binding")); + } + binding__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Timestamp => { + if timestamp__.is_some() { + return Err(serde::de::Error::duplicate_field("timestamp")); + } + timestamp__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(request::flush::BackfillComplete { + binding: binding__.unwrap_or_default(), + timestamp: timestamp__, + }) + } + } + deserializer.deserialize_struct("materialize.Request.Flush.BackfillComplete", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for request::Load { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto-flow/src/runtime.rs b/crates/proto-flow/src/runtime.rs index ccb0cad67a3..0ab92d93f33 100644 --- a/crates/proto-flow/src/runtime.rs +++ b/crates/proto-flow/src/runtime.rs @@ -580,6 +580,12 @@ pub struct Recover { /// Persisted trigger parameters (materialize only), or empty. #[prost(bytes = "bytes", tag = "10")] pub trigger_params_json: ::prost::bytes::Bytes, + /// Active-backfill begin clocks, keyed by binding index. Restored so the + /// capture runtime can re-apply truncated-at journal labels on startup and + /// resolve a BackfillComplete's truncated_at. Resolved from "AB:{state_key}" + /// keys by the scan. + #[prost(btree_map = "uint32, fixed64", tag = "11")] + pub active_backfills: ::prost::alloc::collections::BTreeMap, } /// Persist is sent by the leader to shard zero when state must be durably /// written. Each field maps to a contractual WriteBatch effect on shard @@ -661,6 +667,35 @@ pub struct Persist { /// Effect: Put under "trigger-params" key. #[prost(bytes = "bytes", tag = "16")] pub trigger_params_json: ::prost::bytes::Bytes, + /// The active-backfill change this transaction observed, if any. At most one + /// per commit — a backfill control signal stands alone in its transaction. + #[prost(oneof = "persist::ActiveBackfillChange", tags = "17, 18")] + pub active_backfill_change: ::core::option::Option, +} +/// Nested message and enum types in `Persist`. +pub mod persist { + /// The active-backfill change this transaction observed, if any. At most one + /// per commit — a backfill control signal stands alone in its transaction. + #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum ActiveBackfillChange { + /// BackfillBegin: record the binding's begin clock. + /// Effect: Put fixed64-LE under "AB:{state_key}" (state_key resolved by the encoder). + #[prost(message, tag = "17")] + Begin(super::ActiveBackfillBegin), + /// BackfillComplete: clear the binding's active-backfill entry. + /// Effect: Delete "AB:{state_key}" (state_key resolved by the encoder). + #[prost(uint32, tag = "18")] + CompleteBinding(u32), + } +} +/// ActiveBackfillBegin records a binding's backfill begin clock — its +/// authoritative truncated_at — staged by a committing Persist. +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ActiveBackfillBegin { + #[prost(uint32, tag = "1")] + pub binding: u32, + #[prost(fixed64, tag = "2")] + pub truncated_at: u64, } /// Persisted is sent by shard zero to the leader after the state is durable /// in the recovery log. diff --git a/crates/proto-flow/src/runtime.serde.rs b/crates/proto-flow/src/runtime.serde.rs index b2c3f226bb2..2443011d25c 100644 --- a/crates/proto-flow/src/runtime.serde.rs +++ b/crates/proto-flow/src/runtime.serde.rs @@ -1,3 +1,122 @@ +impl serde::Serialize for ActiveBackfillBegin { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.binding != 0 { + len += 1; + } + if self.truncated_at != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("runtime.ActiveBackfillBegin", len)?; + if self.binding != 0 { + struct_ser.serialize_field("binding", &self.binding)?; + } + if self.truncated_at != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("truncatedAt", ToString::to_string(&self.truncated_at).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ActiveBackfillBegin { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "binding", + "truncated_at", + "truncatedAt", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Binding, + TruncatedAt, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "binding" => Ok(GeneratedField::Binding), + "truncatedAt" | "truncated_at" => Ok(GeneratedField::TruncatedAt), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ActiveBackfillBegin; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct runtime.ActiveBackfillBegin") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut binding__ = None; + let mut truncated_at__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Binding => { + if binding__.is_some() { + return Err(serde::de::Error::duplicate_field("binding")); + } + binding__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::TruncatedAt => { + if truncated_at__.is_some() { + return Err(serde::de::Error::duplicate_field("truncatedAt")); + } + truncated_at__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(ActiveBackfillBegin { + binding: binding__.unwrap_or_default(), + truncated_at: truncated_at__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("runtime.ActiveBackfillBegin", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for Applied { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -8393,6 +8512,9 @@ impl serde::Serialize for Persist { if !self.trigger_params_json.is_empty() { len += 1; } + if self.active_backfill_change.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("runtime.Persist", len)?; if self.seq_no != 0 { #[allow(clippy::needless_borrow)] @@ -8458,6 +8580,16 @@ impl serde::Serialize for Persist { #[allow(clippy::needless_borrows_for_generic_args)] struct_ser.serialize_field("triggerParams", &crate::as_raw_json(&self.trigger_params_json)?)?; } + if let Some(v) = self.active_backfill_change.as_ref() { + match v { + persist::ActiveBackfillChange::Begin(v) => { + struct_ser.serialize_field("begin", v)?; + } + persist::ActiveBackfillChange::CompleteBinding(v) => { + struct_ser.serialize_field("completeBinding", v)?; + } + } + } struct_ser.end() } } @@ -8500,6 +8632,9 @@ impl<'de> serde::Deserialize<'de> for Persist { "deleteTriggerParams", "trigger_params_json", "triggerParams", + "begin", + "complete_binding", + "completeBinding", ]; #[allow(clippy::enum_variant_names)] @@ -8520,6 +8655,8 @@ impl<'de> serde::Deserialize<'de> for Persist { MaxKeys, DeleteTriggerParams, TriggerParamsJson, + Begin, + CompleteBinding, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -8558,6 +8695,8 @@ impl<'de> serde::Deserialize<'de> for Persist { "maxKeys" | "max_keys" => Ok(GeneratedField::MaxKeys), "deleteTriggerParams" | "delete_trigger_params" => Ok(GeneratedField::DeleteTriggerParams), "triggerParams" | "trigger_params_json" => Ok(GeneratedField::TriggerParamsJson), + "begin" => Ok(GeneratedField::Begin), + "completeBinding" | "complete_binding" => Ok(GeneratedField::CompleteBinding), _ => Ok(GeneratedField::__SkipField__), } } @@ -8593,6 +8732,7 @@ impl<'de> serde::Deserialize<'de> for Persist { let mut max_keys__ = None; let mut delete_trigger_params__ = None; let mut trigger_params_json__ = None; + let mut active_backfill_change__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::SeqNo => { @@ -8709,6 +8849,19 @@ impl<'de> serde::Deserialize<'de> for Persist { Some(map_.next_value::()?.0) ; } + GeneratedField::Begin => { + if active_backfill_change__.is_some() { + return Err(serde::de::Error::duplicate_field("begin")); + } + active_backfill_change__ = map_.next_value::<::std::option::Option<_>>()?.map(persist::ActiveBackfillChange::Begin) +; + } + GeneratedField::CompleteBinding => { + if active_backfill_change__.is_some() { + return Err(serde::de::Error::duplicate_field("completeBinding")); + } + active_backfill_change__ = map_.next_value::<::std::option::Option<::pbjson::private::NumberDeserialize<_>>>()?.map(|x| persist::ActiveBackfillChange::CompleteBinding(x.0)); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -8731,6 +8884,7 @@ impl<'de> serde::Deserialize<'de> for Persist { max_keys: max_keys__.unwrap_or_default(), delete_trigger_params: delete_trigger_params__.unwrap_or_default(), trigger_params_json: trigger_params_json__.unwrap_or_default(), + active_backfill_change: active_backfill_change__, }) } } @@ -8949,6 +9103,9 @@ impl serde::Serialize for Recover { if !self.trigger_params_json.is_empty() { len += 1; } + if !self.active_backfills.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("runtime.Recover", len)?; if !self.ack_intents.is_empty() { let v: std::collections::HashMap<_, _> = self.ack_intents.iter() @@ -8994,6 +9151,11 @@ impl serde::Serialize for Recover { #[allow(clippy::needless_borrows_for_generic_args)] struct_ser.serialize_field("triggerParams", &crate::as_raw_json(&self.trigger_params_json)?)?; } + if !self.active_backfills.is_empty() { + let v: std::collections::HashMap<_, _> = self.active_backfills.iter() + .map(|(k, v)| (k, v.to_string())).collect(); + struct_ser.serialize_field("activeBackfills", &v)?; + } struct_ser.end() } } @@ -9024,6 +9186,8 @@ impl<'de> serde::Deserialize<'de> for Recover { "maxKeys", "trigger_params_json", "triggerParams", + "active_backfills", + "activeBackfills", ]; #[allow(clippy::enum_variant_names)] @@ -9038,6 +9202,7 @@ impl<'de> serde::Deserialize<'de> for Recover { LegacyCheckpoint, MaxKeys, TriggerParamsJson, + ActiveBackfills, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -9070,6 +9235,7 @@ impl<'de> serde::Deserialize<'de> for Recover { "legacyCheckpoint" | "legacy_checkpoint" => Ok(GeneratedField::LegacyCheckpoint), "maxKeys" | "max_keys" => Ok(GeneratedField::MaxKeys), "triggerParams" | "trigger_params_json" => Ok(GeneratedField::TriggerParamsJson), + "activeBackfills" | "active_backfills" => Ok(GeneratedField::ActiveBackfills), _ => Ok(GeneratedField::__SkipField__), } } @@ -9099,6 +9265,7 @@ impl<'de> serde::Deserialize<'de> for Recover { let mut legacy_checkpoint__ = None; let mut max_keys__ = None; let mut trigger_params_json__ = None; + let mut active_backfills__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::AckIntents => { @@ -9177,6 +9344,15 @@ impl<'de> serde::Deserialize<'de> for Recover { Some(map_.next_value::()?.0) ; } + GeneratedField::ActiveBackfills => { + if active_backfills__.is_some() { + return Err(serde::de::Error::duplicate_field("activeBackfills")); + } + active_backfills__ = Some( + map_.next_value::, ::pbjson::private::NumberDeserialize>>()? + .into_iter().map(|(k,v)| (k.0, v.0)).collect() + ); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -9193,6 +9369,7 @@ impl<'de> serde::Deserialize<'de> for Recover { legacy_checkpoint: legacy_checkpoint__, max_keys: max_keys__.unwrap_or_default(), trigger_params_json: trigger_params_json__.unwrap_or_default(), + active_backfills: active_backfills__.unwrap_or_default(), }) } } diff --git a/crates/proto-flow/src/shuffle.rs b/crates/proto-flow/src/shuffle.rs index c7400d91440..d8d2b560a36 100644 --- a/crates/proto-flow/src/shuffle.rs +++ b/crates/proto-flow/src/shuffle.rs @@ -128,6 +128,41 @@ pub struct Frontier { /// Per-shard flushed LSN, indexed by shard_index. #[prost(uint64, repeated, tag = "2")] pub flushed_lsn: ::prost::alloc::vec::Vec, + /// Latest backfill-begin clock for each binding in the checkpoint delta. + /// Populated only on a terminal (empty-journals) frontier of a Progressed + /// or NextCheckpoint sequence. Empty otherwise. + #[prost(message, repeated, tag = "3")] + pub latest_backfill_begin: ::prost::alloc::vec::Vec, + /// Latest backfill-complete clock for each binding in the checkpoint delta. + /// Populated only on a terminal (empty-journals) frontier of a Progressed + /// or NextCheckpoint sequence. Empty otherwise. + #[prost(message, repeated, tag = "4")] + pub latest_backfill_complete: ::prost::alloc::vec::Vec, +} +/// Nested message and enum types in `Frontier`. +pub mod frontier { + /// BackfillBegin is a binding's latest backfill-begin clock, keyed by the + /// binding's stable journal_read_suffix. + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] + pub struct BackfillBegin { + /// Binding's journal read suffix, stable across task versions. + #[prost(string, tag = "1")] + pub journal_read_suffix: ::prost::alloc::string::String, + /// Clock of the binding's most recent backfill-begin. + #[prost(fixed64, tag = "2")] + pub clock: u64, + } + /// BackfillComplete reports a binding's most recent backfill-complete event, + /// keyed by the binding's stable journal_read_suffix. + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] + pub struct BackfillComplete { + /// Binding's journal read suffix, stable across task versions. + #[prost(string, tag = "1")] + pub journal_read_suffix: ::prost::alloc::string::String, + /// Truncation boundary of the completed backfill: the backfill's begin clock. + #[prost(fixed64, tag = "2")] + pub clock: u64, + } } /// SessionRequest is sent by the Coordinator to manage the shuffle session. #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/crates/proto-flow/src/shuffle.serde.rs b/crates/proto-flow/src/shuffle.serde.rs index b52f9b873c1..7b4354ca6da 100644 --- a/crates/proto-flow/src/shuffle.serde.rs +++ b/crates/proto-flow/src/shuffle.serde.rs @@ -161,6 +161,12 @@ impl serde::Serialize for Frontier { if !self.flushed_lsn.is_empty() { len += 1; } + if !self.latest_backfill_begin.is_empty() { + len += 1; + } + if !self.latest_backfill_complete.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("shuffle.Frontier", len)?; if !self.journals.is_empty() { struct_ser.serialize_field("journals", &self.journals)?; @@ -168,6 +174,12 @@ impl serde::Serialize for Frontier { if !self.flushed_lsn.is_empty() { struct_ser.serialize_field("flushedLsn", &self.flushed_lsn.iter().map(ToString::to_string).collect::>())?; } + if !self.latest_backfill_begin.is_empty() { + struct_ser.serialize_field("latestBackfillBegin", &self.latest_backfill_begin)?; + } + if !self.latest_backfill_complete.is_empty() { + struct_ser.serialize_field("latestBackfillComplete", &self.latest_backfill_complete)?; + } struct_ser.end() } } @@ -181,12 +193,18 @@ impl<'de> serde::Deserialize<'de> for Frontier { "journals", "flushed_lsn", "flushedLsn", + "latest_backfill_begin", + "latestBackfillBegin", + "latest_backfill_complete", + "latestBackfillComplete", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { Journals, FlushedLsn, + LatestBackfillBegin, + LatestBackfillComplete, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -211,6 +229,8 @@ impl<'de> serde::Deserialize<'de> for Frontier { match value { "journals" => Ok(GeneratedField::Journals), "flushedLsn" | "flushed_lsn" => Ok(GeneratedField::FlushedLsn), + "latestBackfillBegin" | "latest_backfill_begin" => Ok(GeneratedField::LatestBackfillBegin), + "latestBackfillComplete" | "latest_backfill_complete" => Ok(GeneratedField::LatestBackfillComplete), _ => Ok(GeneratedField::__SkipField__), } } @@ -232,6 +252,8 @@ impl<'de> serde::Deserialize<'de> for Frontier { { let mut journals__ = None; let mut flushed_lsn__ = None; + let mut latest_backfill_begin__ = None; + let mut latest_backfill_complete__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Journals => { @@ -249,6 +271,18 @@ impl<'de> serde::Deserialize<'de> for Frontier { .into_iter().map(|x| x.0).collect()) ; } + GeneratedField::LatestBackfillBegin => { + if latest_backfill_begin__.is_some() { + return Err(serde::de::Error::duplicate_field("latestBackfillBegin")); + } + latest_backfill_begin__ = Some(map_.next_value()?); + } + GeneratedField::LatestBackfillComplete => { + if latest_backfill_complete__.is_some() { + return Err(serde::de::Error::duplicate_field("latestBackfillComplete")); + } + latest_backfill_complete__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -257,12 +291,248 @@ impl<'de> serde::Deserialize<'de> for Frontier { Ok(Frontier { journals: journals__.unwrap_or_default(), flushed_lsn: flushed_lsn__.unwrap_or_default(), + latest_backfill_begin: latest_backfill_begin__.unwrap_or_default(), + latest_backfill_complete: latest_backfill_complete__.unwrap_or_default(), }) } } deserializer.deserialize_struct("shuffle.Frontier", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for frontier::BackfillBegin { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.journal_read_suffix.is_empty() { + len += 1; + } + if self.clock != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("shuffle.Frontier.BackfillBegin", len)?; + if !self.journal_read_suffix.is_empty() { + struct_ser.serialize_field("journalReadSuffix", &self.journal_read_suffix)?; + } + if self.clock != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("clock", ToString::to_string(&self.clock).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for frontier::BackfillBegin { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "journal_read_suffix", + "journalReadSuffix", + "clock", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + JournalReadSuffix, + Clock, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "journalReadSuffix" | "journal_read_suffix" => Ok(GeneratedField::JournalReadSuffix), + "clock" => Ok(GeneratedField::Clock), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = frontier::BackfillBegin; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct shuffle.Frontier.BackfillBegin") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut journal_read_suffix__ = None; + let mut clock__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::JournalReadSuffix => { + if journal_read_suffix__.is_some() { + return Err(serde::de::Error::duplicate_field("journalReadSuffix")); + } + journal_read_suffix__ = Some(map_.next_value()?); + } + GeneratedField::Clock => { + if clock__.is_some() { + return Err(serde::de::Error::duplicate_field("clock")); + } + clock__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(frontier::BackfillBegin { + journal_read_suffix: journal_read_suffix__.unwrap_or_default(), + clock: clock__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("shuffle.Frontier.BackfillBegin", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for frontier::BackfillComplete { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.journal_read_suffix.is_empty() { + len += 1; + } + if self.clock != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("shuffle.Frontier.BackfillComplete", len)?; + if !self.journal_read_suffix.is_empty() { + struct_ser.serialize_field("journalReadSuffix", &self.journal_read_suffix)?; + } + if self.clock != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("clock", ToString::to_string(&self.clock).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for frontier::BackfillComplete { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "journal_read_suffix", + "journalReadSuffix", + "clock", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + JournalReadSuffix, + Clock, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "journalReadSuffix" | "journal_read_suffix" => Ok(GeneratedField::JournalReadSuffix), + "clock" => Ok(GeneratedField::Clock), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = frontier::BackfillComplete; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct shuffle.Frontier.BackfillComplete") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut journal_read_suffix__ = None; + let mut clock__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::JournalReadSuffix => { + if journal_read_suffix__.is_some() { + return Err(serde::de::Error::duplicate_field("journalReadSuffix")); + } + journal_read_suffix__ = Some(map_.next_value()?); + } + GeneratedField::Clock => { + if clock__.is_some() { + return Err(serde::de::Error::duplicate_field("clock")); + } + clock__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(frontier::BackfillComplete { + journal_read_suffix: journal_read_suffix__.unwrap_or_default(), + clock: clock__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("shuffle.Frontier.BackfillComplete", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for JournalFrontier { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto-flow/tests/regression.rs b/crates/proto-flow/tests/regression.rs index 1ebfb97100b..1e2a681d7c1 100644 --- a/crates/proto-flow/tests/regression.rs +++ b/crates/proto-flow/tests/regression.rs @@ -492,6 +492,8 @@ fn ex_capture_response() -> capture::Response { checkpoint: Some(capture::response::Checkpoint { state: Some(ex_connector_state()), }), + backfill_begin: Some(capture::response::BackfillBegin { binding: 4 }), + backfill_complete: Some(capture::response::BackfillComplete { binding: 5 }), internal: ex_internal(), } } @@ -636,6 +638,20 @@ fn ex_materialize_request() -> materialize::Request { }), flush: Some(materialize::request::Flush { state_patches_json: json!([{"flushed": 1}]).to_string().into(), + backfill_begins: vec![materialize::request::flush::BackfillBegin { + binding: 2, + timestamp: Some(pbjson_types::Timestamp { + seconds: 1700000000, + nanos: 0, + }), + }], + backfill_completes: vec![materialize::request::flush::BackfillComplete { + binding: 3, + timestamp: Some(pbjson_types::Timestamp { + seconds: 1700000500, + nanos: 123000000, + }), + }], }), store: Some(materialize::request::Store { binding: 3, diff --git a/crates/proto-flow/tests/snapshots/regression__capture_response_json.snap b/crates/proto-flow/tests/snapshots/regression__capture_response_json.snap index 8f107d3a2ca..07de71d375b 100644 --- a/crates/proto-flow/tests/snapshots/regression__capture_response_json.snap +++ b/crates/proto-flow/tests/snapshots/regression__capture_response_json.snap @@ -86,5 +86,11 @@ expression: json_test(msg) "mergePatch": true } }, + "backfillBegin": { + "binding": 4 + }, + "backfillComplete": { + "binding": 5 + }, "$internal": "EgJIaRgB" } diff --git a/crates/proto-flow/tests/snapshots/regression__capture_response_proto.snap b/crates/proto-flow/tests/snapshots/regression__capture_response_proto.snap index 0e7c084d579..1f295e4410a 100644 --- a/crates/proto-flow/tests/snapshots/regression__capture_response_proto.snap +++ b/crates/proto-flow/tests/snapshots/regression__capture_response_proto.snap @@ -36,5 +36,6 @@ expression: proto_test(msg) |22757064 61746522 7d100142 2a080312| "update"}..B*... 000001f0 |267b2266 6f726d61 74223a22 64617465| &{"format":"date 00000200 |2d74696d 65222c22 74797065 223a2273| -time","type":"s 00000210 -|7472696e 67227da2 06061202 48691801| tring"}.....Hi.. 00000220 - 00000230 +|7472696e 67227d4a 02080452 020805a2| tring"}J...R.... 00000220 +|06061202 48691801| ....Hi.. 00000230 + 00000238 diff --git a/crates/proto-flow/tests/snapshots/regression__materialize_request_json.snap b/crates/proto-flow/tests/snapshots/regression__materialize_request_json.snap index 98421a66640..c9bd093bb25 100644 --- a/crates/proto-flow/tests/snapshots/regression__materialize_request_json.snap +++ b/crates/proto-flow/tests/snapshots/regression__materialize_request_json.snap @@ -523,7 +523,19 @@ expression: json_test(msg) "keyPacked": "VkseCQ==" }, "flush": { - "statePatches": [{"flushed":1}] + "statePatches": [{"flushed":1}], + "backfillBegins": [ + { + "binding": 2, + "timestamp": "2023-11-14T22:13:20+00:00" + } + ], + "backfillCompletes": [ + { + "binding": 3, + "timestamp": "2023-11-14T22:21:40.123+00:00" + } + ] }, "store": { "binding": 3, diff --git a/crates/proto-flow/tests/snapshots/regression__materialize_request_proto.snap b/crates/proto-flow/tests/snapshots/regression__materialize_request_proto.snap index c60d34153c1..9922ed0149e 100644 --- a/crates/proto-flow/tests/snapshots/regression__materialize_request_proto.snap +++ b/crates/proto-flow/tests/snapshots/regression__materialize_request_proto.snap @@ -196,20 +196,22 @@ expression: proto_test(msg) |7b22636f 6e6e6563 746f7222 3a7b2273| {"connector":{"s 00000bf0 |74617465 223a3432 7d7d2a13 080c1209| tate":42}}*..... 00000c00 |5b34322c 22686922 5d1a0456 4b1e0932| [42,"hi"]..VK..2 00000c10 -|110a0f5b 7b22666c 75736865 64223a31| ...[{"flushed":1 00000c20 -|7d5d3a45 0803120b 5b747275 652c6e75| }]:E....[true,nu 00000c30 -|6c6c5d1a 035a1500 22125b33 2e313431| ll]..Z..".[3.141 00000c40 -|35392c22 6669656c 6421225d 2a023c5b| 59,"field!"]*.<[ 00000c50 -|32137b22 66756c6c 223a2264 6f63756d| 2.{"full":"docum 00000c60 -|656e7422 7d380140 01427e0a 640a4a0a| ent"}8.@.B~.d.J. 00000c70 -|15612f72 6561642f 6a6f7572 6e616c3b| .a/read/journal; 00000c80 -|73756666 69781231 08b96012 150a0503| suffix.1..`..... 00000c90 -|09080507 120c09e3 21000000 00000010| ........!....... 00000ca0 -|d7081215 0a05070c 662b1d12 0c093501| ........f+....5. 00000cb0 -|00000000 000010ae 1112160a 0e616e2f| .............an/ 00000cc0 -|61636b2f 6a6f7572 6e616c12 04030402| ack/journal..... 00000cd0 -|0512165b 7b227374 61727465 64223a22| ...[{"started":" 00000ce0 -|636f6d6d 6974227d 5d4a120a 105b7b22| commit"}]J...[{" 00000cf0 -|61636b65 64223a74 7275657d 5da20606| acked":true}]... 00000d00 -|12024869 1801| ..Hi.. 00000d10 - 00000d16 +|2e0a0f5b 7b22666c 75736865 64223a31| ...[{"flushed":1 00000c20 +|7d5d120a 08021206 0880e2cf aa061a0f| }].............. 00000c30 +|0803120b 08f4e5cf aa0610c0 a9d33a3a| ..............:: 00000c40 +|45080312 0b5b7472 75652c6e 756c6c5d| E....[true,null] 00000c50 +|1a035a15 0022125b 332e3134 3135392c| ..Z..".[3.14159, 00000c60 +|22666965 6c642122 5d2a023c 5b32137b| "field!"]*.<[2.{ 00000c70 +|2266756c 6c223a22 646f6375 6d656e74| "full":"document 00000c80 +|227d3801 4001427e 0a640a4a 0a15612f| "}8.@.B~.d.J..a/ 00000c90 +|72656164 2f6a6f75 726e616c 3b737566| read/journal;suf 00000ca0 +|66697812 3108b960 12150a05 03090805| fix.1..`........ 00000cb0 +|07120c09 e3210000 00000000 10d70812| .....!.......... 00000cc0 +|150a0507 0c662b1d 120c0935 01000000| .....f+....5.... 00000cd0 +|00000010 ae111216 0a0e616e 2f61636b| ..........an/ack 00000ce0 +|2f6a6f75 726e616c 12040304 02051216| /journal........ 00000cf0 +|5b7b2273 74617274 6564223a 22636f6d| [{"started":"com 00000d00 +|6d697422 7d5d4a12 0a105b7b 2261636b| mit"}]J...[{"ack 00000d10 +|6564223a 74727565 7d5da206 06120248| ed":true}].....H 00000d20 +|691801| i.. 00000d30 + 00000d33 diff --git a/crates/proto-gazette/src/lib.rs b/crates/proto-gazette/src/lib.rs index d88cb427ce0..052d70e67e9 100644 --- a/crates/proto-gazette/src/lib.rs +++ b/crates/proto-gazette/src/lib.rs @@ -38,6 +38,9 @@ pub mod message_flags { /// On reading a ACK, the reader may process previous CONTINUE_TXN messages /// which are now considered to have committed. pub const ACK_TXN: u64 = 0x2; + /// CONTROL marks the message as an application control message. + /// It is orthogonal to transaction semantics in the low two bits. + pub const CONTROL: u64 = 0x4; } /// Capability bit-mask values defined by Gazette, which scope broker operations. diff --git a/crates/proto-gazette/src/uuid.rs b/crates/proto-gazette/src/uuid.rs index 0d1ffe642e8..9c63fac26d1 100644 --- a/crates/proto-gazette/src/uuid.rs +++ b/crates/proto-gazette/src/uuid.rs @@ -192,6 +192,7 @@ impl Flags { pub const ACK_TXN: Self = Self(crate::message_flags::ACK_TXN as u16); pub const CONTINUE_TXN: Self = Self(crate::message_flags::CONTINUE_TXN as u16); pub const OUTSIDE_TXN: Self = Self(crate::message_flags::OUTSIDE_TXN as u16); + pub const CONTROL: Self = Self(crate::message_flags::CONTROL as u16); #[inline] pub fn is_ack(&self) -> bool { @@ -208,6 +209,11 @@ impl Flags { // OUTSIDE_TXN is zero, so detect absence of CONTINUE_TXN (0x1) or ACK_TXN (0x2). self.0 & 0x3 == crate::message_flags::OUTSIDE_TXN as u16 } + + #[inline] + pub fn is_control(&self) -> bool { + self.0 & (crate::message_flags::CONTROL as u16) != 0 + } } impl std::fmt::Debug for Flags { @@ -222,6 +228,9 @@ impl std::fmt::Debug for Flags { if self.is_ack() { flags.push("ACK_TXN"); } + if self.is_control() { + flags.push("CONTROL"); + } write!(f, "Flags({:x} ({}))", self.0, flags.join("|")) } } diff --git a/crates/publisher/Cargo.toml b/crates/publisher/Cargo.toml index 5aea18d3310..e6c0b05e33f 100644 --- a/crates/publisher/Cargo.toml +++ b/crates/publisher/Cargo.toml @@ -39,4 +39,5 @@ proto-grpc = { path = "../proto-grpc" } tables = { path = "../tables" } insta = { workspace = true } +tokio = { workspace = true, features = ["test-util"] } url = { workspace = true } diff --git a/crates/publisher/src/publisher.rs b/crates/publisher/src/publisher.rs index cf060ac9f4a..d03d72d5b9a 100644 --- a/crates/publisher/src/publisher.rs +++ b/crates/publisher/src/publisher.rs @@ -1,5 +1,5 @@ use bytes::BufMut; -use proto_gazette::uuid; +use proto_gazette::{broker, uuid}; /// Publisher is responsible for transactional publishing of documents to /// journal partitions, creating partitions on-demand and as needed. @@ -210,6 +210,59 @@ impl Publisher { Ok((appender, bytes_written)) } + /// Enqueue a control document to *every* physical partition journal of a + /// Mapped collection binding, so that a reader observes it regardless of + /// its partition selector. + pub async fn enqueue_control_all_partitions( + &mut self, + binding_idx: usize, + mut doc: impl FnMut(uuid::Uuid) -> tonic::Result, + flags: uuid::Flags, + ) -> tonic::Result { + // Snapshot the journals to broadcast to, releasing the partitions watch + // borrow before we begin appending. + let journals: Vec = match &self.bindings[binding_idx] { + super::Binding::Mapped(_) => { + let super::LazyBindingClient::Mapped(lazy) = &self.binding_clients[binding_idx] + else { + unreachable!("Mapped binding has Mapped lazy client"); + }; + let (_client, partitions) = &(**lazy); + let partitions = partitions.ready().await; + let refresh = partitions.token(); + refresh + .result()? + .iter() + .map(|split| split.name.to_string()) + .collect() + } + super::Binding::Fixed(_) => { + unreachable!("control documents are only published to Mapped collection bindings") + } + }; + + // Use the same clock for every control document: they're written to + // separate journals but represent the single shared "truncated at" + // value, so the clock is ticked once outside the loop. + let clock = self.clock.tick(); + for journal in journals.iter() { + let uuid = proto_gazette::uuid::build(self.producer, clock, flags); + let doc = doc(uuid)?; + + let client = self.binding_clients[binding_idx].client(); + let appender = self.appenders.activate(journal, client); + + let mut writer = std::mem::take(&mut appender.buffer).writer(); + serde_json::to_writer(&mut writer, &doc::SerPolicy::noop().on(&doc)) + .expect("serialization of json::AsNode cannot fail"); + appender.buffer = writer.into_inner(); + appender.buffer.put_u8(b'\n'); + appender.checkpoint().await?; + } + + Ok(clock) + } + /// Flush all active Appenders, ensuring every buffered byte is durably appended. /// /// Starts concurrent background Append RPCs for any Appenders with remaining @@ -359,6 +412,66 @@ impl Publisher { ) } + /// Apply the `estuary.dev/truncated-at` journal label to the partitions of + /// each active backfill. Journals already at the target value are skipped. + pub async fn apply_truncated_at_labels( + &mut self, + active_backfills: &std::collections::BTreeMap, + ) -> tonic::Result<()> { + for (&index, &clock) in active_backfills { + let target = labels::truncated_at_value(clock); + + let super::Binding::Mapped(binding) = &self.bindings[index] else { + return Err(tonic::Status::internal(format!( + "binding {index} has an active backfill but is not a Mapped collection binding" + ))); + }; + let client = self.binding_clients[index].client(); + + // Re-list and retry until a full pass applies without losing a CAS + // race. Partitions already at the target are skipped on the retry + // and the loop converges once any racing writer (a concurrent split + // or activation) settles. Bounded at 10 attempts so a persistently- + // racing writer (or a bug that keeps losing the CAS) fails the + // transaction rather than spinning forever and silently wedging the + // caller. + for cas_retries in 0..10 { + let listing = retry_transient("list partitions", || { + client.list(broker::ListRequest { + selector: Some(broker::LabelSelector { + include: Some(labels::build_set([( + "name:prefix", + binding.partitions_prefix.as_str(), + )])), + exclude: None, + }), + ..Default::default() + }) + }) + .await + .map_err(status_from_gazette)?; + + if advance_truncated_at_labels(client, listing, &target).await? { + break; + } + + if cas_retries == 9 { + return Err(tonic::Status::aborted(format!( + "applying truncated-at label to {} lost race 10 times in a row", + binding.collection, + ))); + } + tracing::warn!( + binding = index, + collection = %binding.collection, + cas_retries, + "truncated-at label apply lost race; will retry", + ); + } + } + Ok(()) + } + /// Access the lazy Client and partitions watch for the Mapped binding at /// `index`. Panics if the binding is Fixed. Primarily used by tests. pub fn mapped_binding_client( @@ -376,3 +489,250 @@ impl Publisher { } } } + +async fn advance_truncated_at_labels( + client: &gazette::journal::Client, + listing: broker::ListResponse, + target: &str, +) -> tonic::Result { + for journal in listing.journals { + let Some(change) = truncated_at_label_change(journal, target)? else { + continue; + }; + + match retry_transient("apply truncated-at label", || { + client.apply(broker::ApplyRequest { + changes: vec![change.clone()], + }) + }) + .await + { + Ok(_) => {} + Err(gazette::Error::BrokerStatus(broker::Status::EtcdTransactionFailed)) => { + return Ok(false); + } + Err(err) => return Err(status_from_gazette(err)), + } + } + Ok(true) +} + +/// Convert a gazette client Error into a tonic::Status, preserving a gRPC status +/// when present and otherwise wrapping the error as Internal. +fn status_from_gazette(err: gazette::Error) -> tonic::Status { + match err { + gazette::Error::Grpc(status) => status, + other => tonic::Status::internal(other.to_string()), + } +} + +async fn retry_transient(what: &'static str, mut op: F) -> gazette::Result +where + F: FnMut() -> Fut, + Fut: std::future::Future>, +{ + let mut attempt: u32 = 0; + loop { + let err = match op().await { + ok @ Ok(_) => return ok, + Err(err) => err, + }; + attempt += 1; + + if !err.is_transient() || attempt == 8 { + return Err(err); + } + + // Exponential backoff from a 100ms base, capped at 10 seconds. + let backoff = std::time::Duration::from_millis(100) + .saturating_mul(2u32.saturating_pow(attempt - 1)) + .min(std::time::Duration::from_secs(10)); + tracing::warn!(what, attempt, %err, "gazette RPC failed; retrying after backoff"); + tokio::time::sleep(backoff).await; + } +} + +/// Build the apply Change that advances `journal`'s `estuary.dev/truncated-at` +/// label to `target`, or `None` if the journal is already at or beyond it. The +/// fixed-width hex encoding sorts lexically by clock, so the skip covers both an +/// equal label (idempotent) and a newer one (a later backfill already applied, +/// or clock skew across a restart) -- the label only ever advances. +fn truncated_at_label_change( + journal: broker::list_response::Journal, + target: &str, +) -> tonic::Result> { + let Some(mut spec) = journal.spec else { + return Err(tonic::Status::internal( + "list response journal is missing its spec", + )); + }; + let current = spec + .labels + .as_ref() + .and_then(|set| labels::maybe_one(set, labels::TRUNCATED_AT).ok()) + .unwrap_or(""); + + if current >= target { + return Ok(None); + } + + spec.labels = Some(labels::set_value( + spec.labels.take().unwrap_or_default(), + labels::TRUNCATED_AT, + target, + )); + + Ok(Some(broker::apply_request::Change { + expect_mod_revision: journal.mod_revision, + upsert: Some(spec), + delete: String::new(), + })) +} + +#[cfg(test)] +mod test { + use super::*; + + // A list-response journal carrying `truncated_at` (when Some) plus an + // unrelated label, used to check the advance decision and that other labels + // survive the upsert. + fn journal(mod_revision: i64, truncated_at: Option<&str>) -> broker::list_response::Journal { + let mut set = labels::build_set([("estuary.dev/collection", "the/collection")]); + if let Some(value) = truncated_at { + set = labels::set_value(set, labels::TRUNCATED_AT, value); + } + broker::list_response::Journal { + spec: Some(broker::JournalSpec { + name: "the/collection/pivot=00".to_string(), + labels: Some(set), + ..Default::default() + }), + mod_revision, + ..Default::default() + } + } + + #[test] + fn test_truncated_at_label_change_advances_only() { + let target = labels::truncated_at_value(0x20); + + // No label yet, an older label, and an equal-then-newer label: the + // decision is "advance only" -- skip unless strictly older than target. + let absent = truncated_at_label_change(journal(1, None), &target).unwrap(); + assert!(absent.is_some(), "absent label advances"); + + let older = labels::truncated_at_value(0x10); + let older = truncated_at_label_change(journal(1, Some(&older)), &target).unwrap(); + assert!(older.is_some(), "older label advances"); + + let equal = truncated_at_label_change(journal(1, Some(&target)), &target).unwrap(); + assert!(equal.is_none(), "equal label is skipped (idempotent)"); + + let newer = labels::truncated_at_value(0x30); + let newer = truncated_at_label_change(journal(1, Some(&newer)), &target).unwrap(); + assert!(newer.is_none(), "newer label is never regressed"); + } + + #[test] + fn test_truncated_at_label_change_builds_upsert() { + let target = labels::truncated_at_value(0x20); + let prior = labels::truncated_at_value(0x10); + + let change = truncated_at_label_change(journal(42, Some(&prior)), &target) + .unwrap() + .expect("older label advances"); + + // The CAS guard carries the listed revision, and the upsert sets the + // target label while preserving the journal's unrelated labels. + assert_eq!(change.expect_mod_revision, 42); + let set = change.upsert.unwrap().labels.unwrap(); + assert_eq!( + labels::maybe_one(&set, labels::TRUNCATED_AT).unwrap(), + target + ); + assert_eq!( + labels::maybe_one(&set, "estuary.dev/collection").unwrap(), + "the/collection" + ); + } + + #[test] + fn test_truncated_at_label_change_requires_spec() { + let target = labels::truncated_at_value(0x20); + let no_spec = broker::list_response::Journal { + spec: None, + mod_revision: 1, + ..Default::default() + }; + assert!(truncated_at_label_change(no_spec, &target).is_err()); + } + + #[tokio::test(start_paused = true)] + async fn test_retry_transient_budget_and_passthrough() { + use std::sync::atomic::{AtomicU32, Ordering}; + + struct Case { + name: &'static str, + fail_times: u32, + transient: bool, + expect_ok: bool, + expect_calls: u32, + } + let cases = [ + Case { + name: "immediate success", + fail_times: 0, + transient: true, + expect_ok: true, + expect_calls: 1, + }, + Case { + name: "transient then success", + fail_times: 3, + transient: true, + expect_ok: true, + expect_calls: 4, + }, + Case { + name: "terminal surfaces at once", + fail_times: 99, + transient: false, + expect_ok: false, + expect_calls: 1, + }, + Case { + name: "transient exhausts budget", + fail_times: 99, + transient: true, + expect_ok: false, + expect_calls: 8, // mirrors retry_transient's attempt budget + }, + ]; + + for case in cases { + let calls = AtomicU32::new(0); + let result = retry_transient("test", || { + let n = calls.fetch_add(1, Ordering::SeqCst); + let out: gazette::Result = if n < case.fail_times { + Err(if case.transient { + gazette::Error::Grpc(tonic::Status::unavailable("transient")) + } else { + gazette::Error::BrokerStatus(broker::Status::EtcdTransactionFailed) + }) + } else { + Ok(n) + }; + std::future::ready(out) + }) + .await; + + assert_eq!(result.is_ok(), case.expect_ok, "{}", case.name); + assert_eq!( + calls.load(Ordering::SeqCst), + case.expect_calls, + "{}", + case.name + ); + } + } +} diff --git a/crates/runtime-next/src/leader/capture/fsm.rs b/crates/runtime-next/src/leader/capture/fsm.rs index 0f28252dd82..a726d168ac1 100644 --- a/crates/runtime-next/src/leader/capture/fsm.rs +++ b/crates/runtime-next/src/leader/capture/fsm.rs @@ -43,6 +43,15 @@ use proto_gazette::uuid; use std::collections::BTreeMap; use std::time::Duration; +/// A control message observed within an isolated connector checkpoint. +#[derive(Debug, Clone, Copy)] +pub enum ControlMessage { + /// A backfill is beginning for `binding`. + BackfillBegin { binding: u32 }, + /// A backfill has completed for `binding`. + BackfillComplete { binding: u32 }, +} + /// Per-transaction aggregated state threaded through Head/Tail FSMs. #[derive(Debug, Default, Clone)] pub struct Extents { @@ -63,6 +72,8 @@ pub struct Extents { sourced_schemas: BTreeMap, // Was a synthetic checkpoint injected due to hard-bound violation? synthetic_checkpoint: bool, + // A control message observed in this transaction. + control: Option, } #[derive(Debug, Default, Clone)] @@ -85,6 +96,8 @@ pub enum ConnectorRx { Checkpoint(capture::response::Checkpoint), /// Connector emitted a SourcedSchema. SourcedSchema { binding: u32, shape: doc::Shape }, + /// Connector signaled a backfill control message (begin or complete). + Control(ControlMessage), /// Connector closed its output stream. Eof, } @@ -96,6 +109,8 @@ impl ConnectorRx { Self::Captured(_) => "Captured", Self::Checkpoint(_) => "Checkpoint", Self::SourcedSchema { .. } => "SourcedSchema", + Self::Control(ControlMessage::BackfillBegin { .. }) => "BackfillBegin", + Self::Control(ControlMessage::BackfillComplete { .. }) => "BackfillComplete", Self::Eof => "Eof", } } @@ -117,6 +132,7 @@ pub enum Tail { Recover(TailRecover), Acknowledge(TailAcknowledge), WriteIntents(TailWriteIntents), + ApplyLabels(TailApplyLabels), Done(TailDone), } @@ -141,6 +157,8 @@ pub enum Action { Drain { // Per-binding shapes folded from transaction SourcedSchema messages. sourced_schemas: BTreeMap, + // A control message to publish during this drain, if any. + control: Option, }, /// Publish a stats document as CONTINUE_TXN to the ops stats journal. WriteStats { stats: ops::proto::Stats }, @@ -153,6 +171,8 @@ pub enum Action { WriteIntents { ack_intents: BTreeMap, }, + /// Apply journal truncation labels for the shard's active backfills. + ApplyTruncatedLabels, /// Rotate accumulating and draining combiners. Rotate { extents: Extents }, /// Emit an error. @@ -172,6 +192,7 @@ impl Action { Self::Persist { .. } => "Persist", Self::Acknowledge { .. } => "Acknowledge", Self::WriteIntents { .. } => "WriteIntents", + Self::ApplyTruncatedLabels => "ApplyTruncatedLabels", Self::Rotate { .. } => "Rotate", Self::Error(_) => "Error", } @@ -219,6 +240,7 @@ impl Tail { acknowledge_done: bool, drain_finished: &mut Option, intents_write_idle: bool, + labels_apply_idle: bool, now: uuid::Clock, persist_done: bool, task: &Task, @@ -232,6 +254,7 @@ impl Tail { Self::Recover(s) => s.step(task), Self::Acknowledge(s) => s.step(acknowledge_done), Self::WriteIntents(s) => s.step(intents_write_idle), + Self::ApplyLabels(s) => s.step(labels_apply_idle), Self::Done(_) => (Action::Idle, self), } } @@ -245,6 +268,7 @@ impl Tail { Self::Recover(_) => "Recover", Self::Acknowledge(_) => "Acknowledge", Self::WriteIntents(_) => "WriteIntents", + Self::ApplyLabels(_) => "ApplyLabels", Self::Done(_) => "Done", } } @@ -300,6 +324,15 @@ impl HeadIdle { Duration::ZERO }; + let ready_is_control = matches!(ready, ConnectorRx::Control(_)); + // Force a prompt close to keep a control message isolated: flush an open + // data transaction ahead of an incoming control message, then close the + // control transaction itself (it never reaches the size trigger, so it + // would otherwise idle to the age timeout). + if (ready_is_control && is_open) || self.extents.control.is_some() { + *close_requested = true; + } + let close_policy::Decision { may_close, may_extend, @@ -318,8 +351,9 @@ impl HeadIdle { }); // Should we extend with a ready next connector checkpoint sequence? - if self.extents.synthetic_checkpoint { - // Don't extend transactions after a synthetic checkpoint. + if self.extents.synthetic_checkpoint || self.extents.control.is_some() { + // Don't extend once a synthetic checkpoint was injected or a control + // signal isolated this transaction as a hard boundary. } else if may_extend && matches!( ready, @@ -340,6 +374,21 @@ impl HeadIdle { ); } + // A pending control message with no open transaction opens a fresh one + // (it stands alone). Unlike the data-extend path above this is not gated + // on `may_extend`: a control message is always admitted into its own + // isolated transaction immediately. + if ready_is_control && !is_open { + self.extents.open = now; + return ( + Action::PollAgain, + Head::Extend(HeadExtend { + inner: self, + sequence_bytes: 0, + }), + ); + } + // Should we begin to close the transaction? if !is_open { return (Action::Idle, Head::Idle(self)); @@ -409,6 +458,16 @@ impl HeadExtend { match std::mem::take(ready) { ConnectorRx::Pending => (Action::Idle, Head::Extend(self)), ConnectorRx::Captured(captured) => { + if self.inner.extents.control.is_some() { + return ( + Action::Error(anyhow::anyhow!( + "capture connector emitted a document after a backfill \ + control message in the same checkpoint; control \ + messages must stand alone" + )), + Head::Stop, + ); + } let extents = &mut self.inner.extents; let extent = extents.bindings.entry(captured.binding).or_default(); @@ -422,6 +481,16 @@ impl HeadExtend { (Action::Captured { captured }, Head::Extend(self)) } ConnectorRx::SourcedSchema { binding, shape } => { + if self.inner.extents.control.is_some() { + return ( + Action::Error(anyhow::anyhow!( + "capture connector emitted a SourcedSchema after a backfill \ + control message in the same checkpoint; control messages \ + must stand alone" + )), + Head::Stop, + ); + } let extents = &mut self.inner.extents; let entry = extents @@ -432,6 +501,24 @@ impl HeadExtend { (Action::Idle, Head::Extend(self)) } + ConnectorRx::Control(ctrl) => { + let extents = &mut self.inner.extents; + if extents.captured_docs != 0 + || !extents.sourced_schemas.is_empty() + || extents.control.is_some() + { + return ( + Action::Error(anyhow::anyhow!( + "capture connector backfill control message must stand \ + alone in its checkpoint, with no other documents" + )), + Head::Stop, + ); + } + extents.control = Some(ctrl); + // Await the terminating Checkpoint of this isolated sequence. + (Action::Idle, Head::Extend(self)) + } ConnectorRx::Checkpoint(checkpoint) => { let Self { mut inner, @@ -457,6 +544,7 @@ impl HeadExtend { pub struct DrainedCapture { pub connector_patches: bytes::Bytes, pub bindings: BTreeMap, + pub control: Option, } #[derive(Debug)] @@ -467,11 +555,16 @@ pub struct TailBegin { impl TailBegin { pub fn step(self) -> (Action, Tail) { let Self { mut extents } = self; - // Sourced shapes belong to the drain, not to stats; lift them out of - // Extents into the Drain action and let the rest of Extents flow on. + // Sourced shapes and any control message belong to the drain, not to + // stats; lift them out of Extents into the Drain action and let the + // rest of Extents flow on. let sourced_schemas = std::mem::take(&mut extents.sourced_schemas); + let control = extents.control.take(); ( - Action::Drain { sourced_schemas }, + Action::Drain { + sourced_schemas, + control, + }, Tail::Drain(TailDrain { extents }), ) } @@ -492,6 +585,7 @@ impl TailDrain { let DrainedCapture { connector_patches, bindings, + control, } = drained; // Fold per-binding drained measures into the transaction extents. @@ -507,6 +601,7 @@ impl TailDrain { Tail::WriteStats(TailWriteStats { connector_patches, extents, + control, }), ) } @@ -516,6 +611,7 @@ impl TailDrain { pub struct TailWriteStats { pub connector_patches: bytes::Bytes, pub extents: Extents, + pub control: Option, } impl TailWriteStats { @@ -533,6 +629,7 @@ impl TailWriteStats { let Self { connector_patches, extents, + control, } = self; let ack_intents = std::mem::take(ack_intents); let seq_no = now.as_u64(); @@ -541,12 +638,14 @@ impl TailWriteStats { // intents (so a crash before WriteIntents is recoverable) and the // connector-state patches. `delete_ack_intents` first clears the prior // transaction's per-journal intents — a journal written last transaction - // but not this one would otherwise leave a stale entry. + // but not this one would otherwise leave a stale entry. `control` stages + // the binding's active-backfill change atomically with the commit. let persist = proto::Persist { seq_no, ack_intents: ack_intents.clone(), connector_patches_json: connector_patches, delete_ack_intents: true, + active_backfill_change: control, ..Default::default() }; @@ -661,6 +760,27 @@ impl TailWriteIntents { // follow-up Persist clears them from RocksDB: the next transaction's // commit Persist overwrites them, and an idle capture simply re-writes // the same idempotent intents on its next recovery. + // + // Post-commit, re-apply truncated-at journal labels for the shard's + // active backfills. The actor skips the IO when no labels are dirty, + // so this hop is cheap for ordinary transactions. + ( + Action::ApplyTruncatedLabels, + Tail::ApplyLabels(TailApplyLabels {}), + ) + } +} + +/// TailApplyLabels awaits the post-commit truncated-at journal-label apply, +/// then completes the transaction. +#[derive(Debug)] +pub struct TailApplyLabels {} + +impl TailApplyLabels { + pub fn step(self, labels_apply_idle: bool) -> (Action, Tail) { + if !labels_apply_idle { + return (Action::Idle, Tail::ApplyLabels(self)); + } (Action::Idle, Tail::Done(TailDone {})) } } @@ -722,6 +842,7 @@ mod tests { combiner_bytes: u64, drain_finished: Option, intents_idle: bool, + labels_idle: bool, now: uuid::Clock, pending_ack_intents: BTreeMap, persist_done: bool, @@ -751,6 +872,7 @@ mod tests { self.acknowledge_done, &mut self.drain_finished, self.intents_idle, + self.labels_idle, self.now, self.persist_done, &self.task, @@ -767,6 +889,7 @@ mod tests { combiner_bytes: 0, drain_finished: None, intents_idle: true, + labels_idle: true, now: uuid::Clock::from_unix(1_700_000_000, 0), pending_ack_intents: BTreeMap::new(), persist_done: true, @@ -873,6 +996,11 @@ mod tests { ctx.intents_idle = true; let (action, t) = ctx.step_tail(tail); tail = t; + // Post-commit: a no-op ApplyLabels hop (no active backfills), then Done. + assert!(matches!(action, Action::ApplyTruncatedLabels)); + assert!(matches!(tail, Tail::ApplyLabels(_))); + let (action, t) = ctx.step_tail(tail); + tail = t; assert!(matches!(action, Action::Idle)); assert!(matches!(tail, Tail::Done(_))); @@ -951,7 +1079,9 @@ mod tests { let (action, t) = ctx.step_tail(tail); tail = t; match action { - Action::Drain { sourced_schemas } => assert!(sourced_schemas.contains_key(&0)), + Action::Drain { + sourced_schemas, .. + } => assert!(sourced_schemas.contains_key(&0)), other => panic!("expected Drain, got {other:?}"), } assert!(matches!(tail, Tail::Drain(_))); @@ -989,6 +1119,7 @@ mod tests { // Resume txn 1's Tail. The staged drain output drives WriteStats. ctx.drain_finished = Some(DrainedCapture { connector_patches: Bytes::from_static(b"[{\"cursor\":\"lsn-1\"}\t]"), + control: None, bindings: BTreeMap::from([ ( 0, @@ -1017,7 +1148,7 @@ mod tests { { "_meta": {}, "shard": {}, - "ts": "2023-11-14T22:13:20.000004+00:00", + "ts": "2023-11-14T22:13:20.000005+00:00", "openSecondsTotal": 0.000008, "txnCount": 1, "capture": { @@ -1030,7 +1161,7 @@ mod tests { "docsTotal": 2, "bytesTotal": 50 }, - "lastPublishedAt": "2023-11-14T22:13:20.000012+00:00" + "lastPublishedAt": "2023-11-14T22:13:20.000013+00:00" }, "test/collectionB": { "right": { @@ -1041,7 +1172,7 @@ mod tests { "docsTotal": 1, "bytesTotal": 25 }, - "lastPublishedAt": "2023-11-14T22:13:20.000012+00:00" + "lastPublishedAt": "2023-11-14T22:13:20.000013+00:00" } } } @@ -1133,6 +1264,11 @@ mod tests { ctx.intents_idle = true; let (action, t) = ctx.step_tail(tail); tail = t; + // Post-commit ApplyLabels hop (no active backfills), then Done. + assert!(matches!(action, Action::ApplyTruncatedLabels)); + assert!(matches!(tail, Tail::ApplyLabels(_))); + let (action, t) = ctx.step_tail(tail); + tail = t; assert!(matches!(action, Action::Idle)); assert!(matches!(tail, Tail::Done(_))); @@ -1175,6 +1311,11 @@ mod tests { assert!(matches!(action, Action::WriteIntents { .. })); let (action, t) = ctx.step_tail(tail); tail = t; + // Post-commit ApplyLabels hop (no active backfills), then Done. + assert!(matches!(action, Action::ApplyTruncatedLabels)); + assert!(matches!(tail, Tail::ApplyLabels(_))); + let (action, t) = ctx.step_tail(tail); + tail = t; assert!(matches!(action, Action::Idle)); assert!(matches!(tail, Tail::Done(_))); @@ -1310,13 +1451,19 @@ mod tests { // the connector already being Pending) and only rarely `Eof`, so traces // spend their time accumulating and committing rather than stopping early. fn random_connector_rx(rng: &mut SmallRng) -> ConnectorRx { - match rng.random_range(0..12) { + match rng.random_range(0..13) { 0..=4 => captured(rng.random_range(0..3), b"{\"v\":1}"), 5..=8 => checkpoint(), 9..=10 => ConnectorRx::SourcedSchema { binding: rng.random_range(0..3), shape: doc::Shape::nothing(), }, + 11 if rng.random_bool(0.5) => ConnectorRx::Control(ControlMessage::BackfillBegin { + binding: rng.random_range(0..3), + }), + 11 => ConnectorRx::Control(ControlMessage::BackfillComplete { + binding: rng.random_range(0..3), + }), _ => ConnectorRx::Eof, } } @@ -1359,6 +1506,21 @@ mod tests { if rng.random_bool(0.30) { ctx.drain_finished = Some(DrainedCapture { connector_patches: Bytes::from_static(b"[{\"c\":1}\t]"), + // Independently of the trace's actual control flow, sometimes + // stage a control change so it threads Drain → WriteStats → + // Persist.active_backfill_change. + control: match rng.random_range(0..3) { + 0 => Some(proto::persist::ActiveBackfillChange::Begin( + proto::ActiveBackfillBegin { + binding: rng.random_range(0..3), + truncated_at: rng.random_range(1..1_000_000), + }, + )), + 1 => Some(proto::persist::ActiveBackfillChange::CompleteBinding( + rng.random_range(0..3), + )), + _ => None, + }, bindings: BTreeMap::from([( rng.random_range(0..3), ops::proto::stats::DocsAndBytes { @@ -1428,4 +1590,258 @@ mod tests { .max_tests(400) .quickcheck(prop as fn(u64) -> bool); } + + /// A backfill control transaction, end to end. + #[test] + fn control_transaction_commits_active_backfill() { + let mut ctx = mk_ctx(mk_task(false)); + let mut head = Head::Idle(HeadIdle::default()); + // Tail is Done, so Head may rotate the moment the control txn seals. + let tail = Tail::Done(TailDone::default()); + + // A BackfillBegin with no open transaction opens its own isolated one. + ctx.ready = ConnectorRx::Control(ControlMessage::BackfillBegin { binding: 0 }); + let (action, h) = ctx.step_head(head, &tail); + head = h; + assert!(matches!(action, Action::PollAgain)); + assert!(matches!(head, Head::Extend(_))); + + // HeadExtend folds the control message into `extents.control`. + let (action, h) = ctx.step_head(head, &tail); + head = h; + assert!(matches!(action, Action::Idle)); + let Head::Extend(extend) = &head else { + panic!("expected Extend, got {}", head.kind()); + }; + assert!(matches!( + extend.inner.extents.control, + Some(ControlMessage::BackfillBegin { binding: 0 }), + )); + + // The terminating Checkpoint completes the isolated sequence. + ctx.ready = checkpoint(); + let (action, h) = ctx.step_head(head, &tail); + head = h; + assert!(matches!(action, Action::Checkpoint { .. })); + assert!(matches!(head, Head::Idle(_))); + + // A document now waits behind the sealed control transaction. It can + // neither extend the (refuse-extend) control txn nor open its own. + ctx.ready = captured(0, b"{}"); + let (action, _head) = ctx.step_head(head, &tail); + assert!( + ctx.close_requested, + "the sealed control transaction requests a prompt close", + ); + let extents = match action { + Action::Rotate { extents } => extents, + other => panic!("expected Rotate, got {other:?}"), + }; + + // Begin lifts the control message into the Drain action. + let mut tail = Tail::Begin(TailBegin { extents }); + let (action, t) = ctx.step_tail(tail); + tail = t; + match action { + Action::Drain { control, .. } => assert!(matches!( + control, + Some(ControlMessage::BackfillBegin { binding: 0 }), + )), + other => panic!("expected Drain, got {other:?}"), + } + + // The drain published the CONTROL document, assigned it a clock, and + // returns the resulting active-backfill change to be committed. + ctx.drain_finished = Some(DrainedCapture { + connector_patches: Bytes::new(), + bindings: BTreeMap::new(), + control: Some(proto::persist::ActiveBackfillChange::Begin( + proto::ActiveBackfillBegin { + binding: 0, + truncated_at: 0xABCD, + }, + )), + }); + let (action, t) = ctx.step_tail(tail); + tail = t; + assert!(matches!(action, Action::WriteStats { .. })); + + // WriteStats yields the Persist, which carries the control change verbatim. + let (action, t) = ctx.step_tail(tail); + tail = t; + let persist = match action { + Action::Persist { persist } => persist, + other => panic!("expected Persist, got {other:?}"), + }; + assert_eq!( + persist.active_backfill_change, + Some(proto::persist::ActiveBackfillChange::Begin( + proto::ActiveBackfillBegin { + binding: 0, + truncated_at: 0xABCD, + }, + )), + ); + + // The remaining commit drains to Done: Persist → Recover → WriteIntents + // (no explicit acks) → ApplyLabels → Done. + let (action, t) = ctx.step_tail(tail); + tail = t; + assert!(matches!(action, Action::PollAgain)); + let (action, t) = ctx.step_tail(tail); + tail = t; + assert!(matches!(action, Action::WriteIntents { .. })); + let (action, t) = ctx.step_tail(tail); + tail = t; + assert!(matches!(action, Action::ApplyTruncatedLabels)); + assert!(matches!(tail, Tail::ApplyLabels(_))); + + // A control transaction makes the active-backfill set dirty, so the + // post-commit label apply actually runs: hold in ApplyLabels while that + // IO is in flight, then complete to Done once it lands. + ctx.labels_idle = false; + let (action, t) = ctx.step_tail(tail); + tail = t; + assert!(matches!(action, Action::Idle)); + assert!(matches!(tail, Tail::ApplyLabels(_))); + + ctx.labels_idle = true; + let (action, t) = ctx.step_tail(tail); + tail = t; + assert!(matches!(action, Action::Idle)); + assert!(matches!(tail, Tail::Done(_))); + } + + /// A control message opens its own transaction immediately, ungated by the + /// close policy: even when `may_extend` is false (so a data message would not + /// open one), a BackfillBegin is admitted into its own isolated transaction. + #[test] + fn control_opens_transaction_ignoring_close_policy() { + let mut ctx = mk_ctx(mk_task(false)); + // Narrow the policy and load the combiner so `may_extend` is false. + ctx.task.close_policy.combiner_usage_bytes = 0..10_000; + ctx.combiner_bytes = 1_000_000; + let tail = Tail::Done(TailDone::default()); + + // A Captured into a closed Head does NOT open a transaction. + ctx.ready = captured(0, b"{}"); + let (action, head) = ctx.step_head(Head::Idle(HeadIdle::default()), &tail); + assert!( + !matches!(action, Action::PollAgain) && matches!(head, Head::Idle(_)), + "a data message must not open while may_extend is false, got {action:?}", + ); + + // A BackfillBegin DOES open its own isolated transaction. + ctx.ready = ConnectorRx::Control(ControlMessage::BackfillBegin { binding: 1 }); + let (action, head) = ctx.step_head(Head::Idle(HeadIdle::default()), &tail); + assert!(matches!(action, Action::PollAgain)); + assert!(matches!(head, Head::Extend(_))); + } + + /// A control message arriving while a *data* transaction is open forces that + /// transaction to close first, so the control lands in its own isolated + /// transaction rather than mixing into the open one. + #[test] + fn incoming_control_closes_open_data_transaction() { + let mut ctx = mk_ctx(mk_task(false)); + let head = Head::Idle(HeadIdle { + extents: Extents { + checkpoints: 1, // is_open + captured_docs: 3, + ..Default::default() + }, + last_close: ctx.now, + }); + // A BackfillBegin waits behind the open data transaction. + ctx.ready = ConnectorRx::Control(ControlMessage::BackfillBegin { binding: 0 }); + + let (action, head) = ctx.step_head(head, &Tail::Done(TailDone::default())); + assert!( + ctx.close_requested, + "an incoming control message requests the open transaction's close", + ); + let extents = match action { + Action::Rotate { extents } => extents, + other => panic!("expected Rotate, got {other:?}"), + }; + // The rotated transaction carries the data; the control is still queued in + // `ready`, to stand alone in the next transaction. + assert!(extents.control.is_none()); + assert!(matches!(head, Head::Idle(_))); + } + + /// The "a control message stands alone in its checkpoint" invariant: mixing a + /// control message with documents or schemas in the same checkpoint sequence, + /// in either order, fails the task. + #[test] + fn control_must_stand_alone() { + // (label, pre-accrued extents, the violating ready message) + let cases: Vec<(&str, Extents, ConnectorRx)> = vec![ + ( + "captured after control", + Extents { + control: Some(ControlMessage::BackfillBegin { binding: 0 }), + ..Default::default() + }, + captured(0, b"{}"), + ), + ( + "sourced_schema after control", + Extents { + control: Some(ControlMessage::BackfillBegin { binding: 0 }), + ..Default::default() + }, + ConnectorRx::SourcedSchema { + binding: 0, + shape: doc::Shape::nothing(), + }, + ), + ( + "control after captured", + Extents { + captured_docs: 1, + ..Default::default() + }, + ConnectorRx::Control(ControlMessage::BackfillBegin { binding: 0 }), + ), + ( + "control after sourced_schema", + Extents { + sourced_schemas: BTreeMap::from([(0, doc::Shape::nothing())]), + ..Default::default() + }, + ConnectorRx::Control(ControlMessage::BackfillBegin { binding: 0 }), + ), + ( + "control after control", + Extents { + control: Some(ControlMessage::BackfillBegin { binding: 0 }), + ..Default::default() + }, + ConnectorRx::Control(ControlMessage::BackfillComplete { binding: 1 }), + ), + ]; + + for (label, extents, ready) in cases { + let mut ctx = mk_ctx(mk_task(false)); + ctx.ready = ready; + let head = Head::Extend(HeadExtend { + inner: HeadIdle { + extents, + last_close: ctx.now, + }, + sequence_bytes: 0, + }); + + let (action, head) = ctx.step_head(head, &Tail::Done(TailDone::default())); + let Action::Error(error) = action else { + panic!("{label}: expected Error, got {action:?}"); + }; + assert!( + format!("{error:?}").contains("stand alone"), + "{label}: {error:?}", + ); + assert!(matches!(head, Head::Stop), "{label}"); + } + } } diff --git a/crates/runtime-next/src/leader/derive/startup.rs b/crates/runtime-next/src/leader/derive/startup.rs index f8af236a29a..b297382c730 100644 --- a/crates/runtime-next/src/leader/derive/startup.rs +++ b/crates/runtime-next/src/leader/derive/startup.rs @@ -115,6 +115,7 @@ pub(super) async fn run( legacy_checkpoint, max_keys, trigger_params_json: _, + active_backfills: _, // capture-only state } = recv_recovers(shard_rx, &task.peers) .await .context("receiving Recover fan-in")?; diff --git a/crates/runtime-next/src/leader/materialize/actor.rs b/crates/runtime-next/src/leader/materialize/actor.rs index 0775c5f3b6f..27e10e64ccf 100644 --- a/crates/runtime-next/src/leader/materialize/actor.rs +++ b/crates/runtime-next/src/leader/materialize/actor.rs @@ -11,6 +11,12 @@ use tokio::sync::mpsc; /// Actor leads transactions of an established materialization task session. pub struct Actor { + // Cumulative per-`journal_read_suffix` backfill-begin clock, accumulated + // across all transactions of the session. + backfill_begin: BTreeMap, + // Cumulative per-`journal_read_suffix` backfill-complete clock, accumulated + // across all transactions of the session. + backfill_complete: BTreeMap, // Client used for trigger dispatch. http_client: reqwest::Client, // Future for an in-flight ACK intents write, if any. @@ -40,6 +46,8 @@ pub struct Actor { impl Actor { pub fn new( + backfill_begin: BTreeMap, + backfill_complete: BTreeMap, http_client: reqwest::Client, legacy_checkpoint: Option, metrics: super::Metrics, @@ -48,6 +56,8 @@ impl Actor { task: Task, ) -> Self { Self { + backfill_begin, + backfill_complete, http_client, intents_write_fut: None, legacy_checkpoint: legacy_checkpoint.map(|f| (f, consumer::Checkpoint::default())), @@ -124,7 +134,7 @@ impl Actor { "leader materialize Actor::serve iteration" ); - let action: fsm::Action; + let mut action: fsm::Action; let prev_kind = tail.kind(); (action, tail) = tail.step( &self.trigger_debounce, @@ -145,6 +155,7 @@ impl Actor { "transition", ); } + self.merge_backfill_clocks(&mut action); let tail_wake_after = self.dispatch(action)?; let action: fsm::Action; @@ -196,7 +207,10 @@ impl Actor { Duration::ZERO } - action => self.dispatch(action)?, + mut action => { + self.merge_backfill_clocks(&mut action); + self.dispatch(action)? + } }; let wake_after = std::cmp::min(head_wake_after, tail_wake_after); @@ -330,6 +344,52 @@ impl Actor { Ok(()) } + /// Stamp session-cumulative backfill clocks onto an outgoing `Load` or + /// `Persist` frontier (folding a `Load` delta into the cumulative maps first), + /// so each carries the full truncation boundary. + fn merge_backfill_clocks(&mut self, action: &mut fsm::Action) { + match action { + fsm::Action::Load { frontier } => { + for (suffix, clock) in &frontier.latest_backfill_begin { + let entry = self.backfill_begin.entry(suffix.clone()).or_insert(*clock); + *entry = (*entry).max(*clock); + } + for (suffix, clock) in &frontier.latest_backfill_complete { + let entry = self + .backfill_complete + .entry(suffix.clone()) + .or_insert(*clock); + *entry = (*entry).max(*clock); + } + frontier.latest_backfill_begin = self.backfill_begin.clone(); + frontier.latest_backfill_complete = self.backfill_complete.clone(); + } + fsm::Action::Persist { persist } => { + if let Some(frontier) = &mut persist.committed_frontier { + frontier.latest_backfill_begin = self + .backfill_begin + .iter() + .map(|(suffix, clock)| shuffle::proto::frontier::BackfillBegin { + journal_read_suffix: suffix.clone(), + clock: clock.as_u64(), + }) + .collect(); + frontier.latest_backfill_complete = self + .backfill_complete + .iter() + .map( + |(suffix, clock)| shuffle::proto::frontier::BackfillComplete { + journal_read_suffix: suffix.clone(), + clock: clock.as_u64(), + }, + ) + .collect(); + } + } + _ => {} + } + } + /// Execute the outgoing-IO primitive for an Action. #[tracing::instrument(level = "trace", fields(action = ?action), skip_all)] fn dispatch(&mut self, action: fsm::Action) -> anyhow::Result { @@ -602,6 +662,8 @@ mod tests { triggers: None, }; let actor = Actor::new( + BTreeMap::new(), + BTreeMap::new(), reqwest::Client::new(), None, super::super::Metrics::new("test/task/shard"), @@ -722,4 +784,104 @@ mod tests { } } } + + #[test] + fn merge_backfill_clocks_load_advances_and_stamps() { + let (mut actor, _rxs) = mk_actor(1); + actor.backfill_begin = BTreeMap::from([("a".to_string(), uuid::Clock::from_u64(5))]); + actor.backfill_complete = BTreeMap::from([("a".to_string(), uuid::Clock::from_u64(4))]); + + // Incoming Load delta: an older "a" (must not regress the cumulative) and + // a fresh "b". + let mut action = fsm::Action::Load { + frontier: shuffle::Frontier { + latest_backfill_begin: BTreeMap::from([ + ("a".to_string(), uuid::Clock::from_u64(3)), + ("b".to_string(), uuid::Clock::from_u64(7)), + ]), + latest_backfill_complete: BTreeMap::from([( + "b".to_string(), + uuid::Clock::from_u64(6), + )]), + ..Default::default() + }, + }; + actor.merge_backfill_clocks(&mut action); + + let want_begin = BTreeMap::from([ + ("a".to_string(), uuid::Clock::from_u64(5)), // kept 5, not regressed to 3 + ("b".to_string(), uuid::Clock::from_u64(7)), + ]); + let want_complete = BTreeMap::from([ + ("a".to_string(), uuid::Clock::from_u64(4)), + ("b".to_string(), uuid::Clock::from_u64(6)), + ]); + + // The cumulative maps advance (max-fold), never regress. + assert_eq!(actor.backfill_begin, want_begin); + assert_eq!(actor.backfill_complete, want_complete); + + // The outgoing frontier carries the full cumulative set ("a"=5 was never + // in the delta), not just the delta. + let fsm::Action::Load { frontier } = &action else { + panic!("expected Load"); + }; + assert_eq!(frontier.latest_backfill_begin, want_begin); + assert_eq!(frontier.latest_backfill_complete, want_complete); + } + + #[test] + fn merge_backfill_clocks_persist_stamps() { + let (mut actor, _rxs) = mk_actor(1); + actor.backfill_begin = BTreeMap::from([("a".to_string(), uuid::Clock::from_u64(5))]); + actor.backfill_complete = BTreeMap::from([("a".to_string(), uuid::Clock::from_u64(4))]); + + let mut action = fsm::Action::Persist { + persist: proto::Persist { + committed_frontier: Some(shuffle::proto::Frontier::default()), + ..Default::default() + }, + }; + actor.merge_backfill_clocks(&mut action); + + let fsm::Action::Persist { persist } = &action else { + panic!("expected Persist"); + }; + let frontier = persist.committed_frontier.as_ref().unwrap(); + assert_eq!( + frontier.latest_backfill_begin, + vec![shuffle::proto::frontier::BackfillBegin { + journal_read_suffix: "a".to_string(), + clock: 5, + }], + ); + assert_eq!( + frontier.latest_backfill_complete, + vec![shuffle::proto::frontier::BackfillComplete { + journal_read_suffix: "a".to_string(), + clock: 4, + }], + ); + } + + #[test] + fn merge_backfill_clocks_noop_cases() { + let (mut actor, _rxs) = mk_actor(1); + actor.backfill_begin = BTreeMap::from([("a".to_string(), uuid::Clock::from_u64(5))]); + + // A Persist without a committed frontier has nothing to stamp... + let mut persist = fsm::Action::Persist { + persist: proto::Persist::default(), + }; + actor.merge_backfill_clocks(&mut persist); + + // ...and a non-Load/Persist action is ignored. + let mut store = fsm::Action::Store; + actor.merge_backfill_clocks(&mut store); + + assert_eq!( + actor.backfill_begin, + BTreeMap::from([("a".to_string(), uuid::Clock::from_u64(5))]), + ); + } } diff --git a/crates/runtime-next/src/leader/materialize/handler.rs b/crates/runtime-next/src/leader/materialize/handler.rs index 34c49e2e5e9..d84a5531601 100644 --- a/crates/runtime-next/src/leader/materialize/handler.rs +++ b/crates/runtime-next/src/leader/materialize/handler.rs @@ -205,6 +205,9 @@ where ) .await?; + let backfill_begin = committed_frontier.latest_backfill_begin.clone(); + let backfill_complete = committed_frontier.latest_backfill_complete.clone(); + let head = fsm::Head::Idle(fsm::HeadIdle { last_close: committed_close, idempotent_replay, @@ -227,6 +230,8 @@ where }; let mut actor = actor::Actor::new( + backfill_begin, + backfill_complete, service.http_client.clone(), legacy_checkpoint, metrics, diff --git a/crates/runtime-next/src/leader/materialize/startup.rs b/crates/runtime-next/src/leader/materialize/startup.rs index 2bad6c7ef25..4380730010f 100644 --- a/crates/runtime-next/src/leader/materialize/startup.rs +++ b/crates/runtime-next/src/leader/materialize/startup.rs @@ -119,6 +119,7 @@ pub(super) async fn run( legacy_checkpoint, max_keys, trigger_params_json: pending_trigger_params, + active_backfills: _, // capture-only state } = recv_recovers(shard_rx, &task.peers) .await .context("receiving Recover fan-in")?; diff --git a/crates/runtime-next/src/publish.rs b/crates/runtime-next/src/publish.rs index c921f5e578b..0e70dc4e33c 100644 --- a/crates/runtime-next/src/publish.rs +++ b/crates/runtime-next/src/publish.rs @@ -222,6 +222,35 @@ impl Publisher { } } + /// Publish a synthesized backfill CONTROL document to *every* partition + /// journal of `binding_index`'s collection, and return the UUID clock of the + /// first copy. For a `BackfillControl::Begin` that clock is the authoritative + /// `truncated_at`. + /// + /// Broadcasting to all partitions (rather than mapping by partition fields, + /// which a `_meta`-only control body lacks) ensures readers observe the + /// signal regardless of their partition selector. + pub async fn publish_control( + &mut self, + binding_index: usize, + control: BackfillControl, + ) -> tonic::Result { + match self { + Self::Real(p) => { + p.enqueue_control_all_partitions( + binding_index + 1, + |uuid| Ok(build_control_body(uuid, control)), + uuid::Flags::CONTROL, + ) + .await + } + // Preview does no journal IO, so no UUID/clock is assigned. The + // zero boundary makes suppression inert, so backfill truncation must + // be exercised on the full stack, not via preview-next. + Self::Preview { .. } => Ok(uuid::Clock::zero()), + } + } + /// Flush all currently buffered documents. pub async fn flush(&mut self) -> tonic::Result<()> { match self { @@ -271,6 +300,28 @@ impl Publisher { } } + /// Apply (or re-apply) the `estuary.dev/truncated-at` journal label for the + /// shard's active backfills. `active_backfills` is keyed by task-binding + /// index (the actor's own indexing); the value is the backfill's begin-clock. + /// + /// Task-binding index `i` maps to publisher binding `i + 1` (binding 0 is + /// the fixed ops-stats journal), mirroring `publish_doc` / `publish_control`. + pub async fn apply_truncated_at_labels( + &mut self, + active_backfills: &BTreeMap, + ) -> tonic::Result<()> { + match self { + Self::Real(p) => { + let mapped: BTreeMap = active_backfills + .iter() + .map(|(&binding, &clock)| (binding as usize + 1, clock)) + .collect(); + p.apply_truncated_at_labels(&mapped).await + } + Self::Preview { .. } => Ok(()), + } + } + /// Write per-journal ACK intent documents to their journals. /// No-op in preview mode (intents are necessarily empty). pub async fn write_intents( @@ -309,6 +360,71 @@ pub fn producer_from_bytes(publisher_id: &[u8]) -> anyhow::Result serde_json::Value { + let mut meta = serde_json::Map::new(); + meta.insert( + "uuid".to_string(), + serde_json::Value::String(uuid.to_string()), + ); + match control { + BackfillControl::Begin => { + meta.insert("backfillBegin".to_string(), serde_json::Value::Bool(true)); + } + BackfillControl::Complete { truncated_at } => { + meta.insert( + "backfillComplete".to_string(), + serde_json::Value::Bool(true), + ); + // `truncatedAt` is an RFC3339 UTC rendering of the begin clock. The + // round-trip through RFC3339 is exact *because* begin clocks are + // tick-aligned: `Clock::tick` advances by whole microseconds and + // never sets the 4-bit sub-100ns counter, so the reader's `from_unix` + // (which floors to 100ns) reconstructs the identical clock. A clock + // carrying a non-zero counter would lose those bits — begin clocks + // never do. (Wall-clock-derived, so also always in chrono's range.) + let (seconds, nanos) = uuid::Clock::from_u64(truncated_at).to_unix(); + let dt = chrono::DateTime::from_timestamp(seconds as i64, nanos) + .expect("wall-clock-derived gazette clock is within chrono's range"); + meta.insert( + "truncatedAt".to_string(), + serde_json::Value::String(dt.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true)), + ); + } + } + // Single-shard, full-key-range captures: the control message covers the + // entire key space. (Multi-shard synchronized backfills are future work.) + meta.insert( + "keyBegin".to_string(), + serde_json::Value::String("00000000".to_string()), + ); + meta.insert( + "keyEnd".to_string(), + serde_json::Value::String("ffffffff".to_string()), + ); + serde_json::json!({ "_meta": meta }) +} + /// Patch a document UUID placeholder in-place after the publisher has assigned /// the transaction UUID. fn patch_document_uuid( @@ -396,4 +512,52 @@ mod test { assert!(producer_from_bytes(b"\x01\x02\x03\x04\x05").is_err()); assert!(producer_from_bytes(b"\x01\x02\x03\x04\x05\x06\x07").is_err()); } + + fn control_uuid() -> uuid::Uuid { + uuid::build( + new_producer(), + uuid::Clock::from_unix(1_600_000_000, 0), + uuid::Flags::CONTROL, + ) + } + + #[test] + fn build_control_body_begin_omits_truncated_at() { + let uuid = control_uuid(); + let body = build_control_body(uuid, BackfillControl::Begin); + let meta = &body["_meta"]; + + // The reader recovers the clock and CONTROL flags from `_meta.uuid`. + assert_eq!(meta["uuid"].as_str(), Some(uuid.to_string().as_str())); + assert_eq!(meta["backfillBegin"].as_bool(), Some(true)); + // A begin carries no completion marker and no `truncatedAt`: its own + // assigned clock is the boundary. + assert!(meta.get("backfillComplete").is_none()); + assert!(meta.get("truncatedAt").is_none()); + // The single-shard control covers the full key range. + assert_eq!(meta["keyBegin"].as_str(), Some("00000000")); + assert_eq!(meta["keyEnd"].as_str(), Some("ffffffff")); + } + + #[test] + fn build_control_body_complete_renders_truncated_at() { + let uuid = control_uuid(); + // A fixed unix time pins the RFC3339 rendering: UTC `Z`, and no + // fractional seconds at a whole-second clock. + let clock = uuid::Clock::from_unix(1_700_000_000, 0); + let body = build_control_body( + uuid, + BackfillControl::Complete { + truncated_at: clock.as_u64(), + }, + ); + let meta = &body["_meta"]; + + assert_eq!(meta["uuid"].as_str(), Some(uuid.to_string().as_str())); + assert_eq!(meta["backfillComplete"].as_bool(), Some(true)); + assert!(meta.get("backfillBegin").is_none()); + assert_eq!(meta["truncatedAt"].as_str(), Some("2023-11-14T22:13:20Z")); + assert_eq!(meta["keyBegin"].as_str(), Some("00000000")); + assert_eq!(meta["keyEnd"].as_str(), Some("ffffffff")); + } } diff --git a/crates/runtime-next/src/shard/capture/actor.rs b/crates/runtime-next/src/shard/capture/actor.rs index 9ca243174ea..218cd707312 100644 --- a/crates/runtime-next/src/shard/capture/actor.rs +++ b/crates/runtime-next/src/shard/capture/actor.rs @@ -23,7 +23,7 @@ use tokio::sync::mpsc; /// resource (`db`, `publisher`, ...) is `None` exactly while its future runs, /// and is restored when that future completes — the "parking" pattern shared /// with the materialize leader actor. -pub(super) struct Actor { +pub(crate) struct Actor { // --- Task and IO endpoints, fixed for the session. --- // `task` is shared (Arc) so the drain future can hold its own handle. task: std::sync::Arc, @@ -32,6 +32,7 @@ pub(super) struct Actor { metrics: super::Metrics, // When Some, a deadline at which we begin a graceful session stop. token_restart_at: Option, + is_control_producer: bool, // --- Parked resources: `Some` unless borrowed by an in-flight future. --- // RocksDB is parked with its per-binding state keys. @@ -50,11 +51,28 @@ pub(super) struct Actor { acknowledge_fut: Option>>, drain_fut: Option>>, intents_write_fut: Option>>, - persist_fut: Option)>>>, + labels_apply_fut: + Option, tonic::Result<()>)>>, + persist_fut: Option< + BoxFuture< + 'static, + anyhow::Result<( + (crate::shard::RocksDB, Vec), + Option, + )>, + >, + >, split_fut: Option, stats_write_fut: Option)>>>, + // `truncated_at` clock of each binding with an in-progress backfill — + // present from its BackfillBegin until its BackfillComplete commits. + active_backfills: BTreeMap, + // True when `active_backfills` differs from what's reflected in journal + // labels. + labels_dirty: bool, + // --- Hand-offs staged between FSM steps. --- // Drain output, staged for `TailDrain`. drain_finished: Option, @@ -71,9 +89,11 @@ struct DrainInput { impl Actor { pub fn new( + active_backfills: BTreeMap, binding_state_keys: Vec, connector_tx: mpsc::Sender, db: crate::shard::RocksDB, + is_control_producer: bool, metrics: super::Metrics, publisher: crate::Publisher, shapes: Vec, @@ -88,11 +108,13 @@ impl Actor { tokio::time::Instant::now() + delay }); + let labels_dirty = !active_backfills.is_empty(); Self { task, connector_tx, metrics, token_restart_at, + is_control_producer, db: Some((db, binding_state_keys)), publisher: Some(publisher), shapes: Some(shapes), @@ -101,9 +123,12 @@ impl Actor { acknowledge_fut: None, drain_fut: None, intents_write_fut: None, + labels_apply_fut: None, persist_fut: None, split_fut: None, stats_write_fut: None, + active_backfills, + labels_dirty, drain_finished: None, pending_ack_intents: BTreeMap::new(), } @@ -165,6 +190,7 @@ impl Actor { self.acknowledge_fut.is_none(), &mut self.drain_finished, self.intents_write_fut.is_none(), + self.labels_apply_fut.is_none(), now, self.persist_fut.is_none(), &self.task, @@ -262,8 +288,23 @@ impl Actor { self.observe_throttle(); } Some(result) = maybe_fut(&mut self.persist_fut) => { - self.db = Some(result?); + let (db, change) = result?; + self.db = Some(db); self.persist_fut = None; + match change { + Some(proto::persist::ActiveBackfillChange::Begin(begin)) => { + self.active_backfills.insert(begin.binding, begin.truncated_at); + self.labels_dirty = true; + } + Some(proto::persist::ActiveBackfillChange::CompleteBinding(binding)) => { + self.active_backfills.remove(&binding); + // Re-apply remaining backfills' labels; if this was the + // last one, an empty map has nothing to apply, so don't + // strand `labels_dirty` at true. + self.labels_dirty = !self.active_backfills.is_empty(); + } + None => {} + } } Some(result) = maybe_fut(&mut self.acknowledge_fut) => { result?; @@ -284,6 +325,13 @@ impl Actor { ); self.split_fut = None; } + Some((publisher, active_backfills, result)) = maybe_fut(&mut self.labels_apply_fut) => { + result.context("applying truncated-at journal labels")?; + self.publisher = Some(publisher); + self.active_backfills = active_backfills; + self.labels_apply_fut = None; + self.labels_dirty = false; + } // Process controller messages next. msg = controller_rx.next() => { Self::on_controller_rx(msg, &mut close_requested, &mut stopping)?; @@ -412,7 +460,10 @@ impl Actor { false // Re-poll to allow for close on connector idle-ness. } - fsm::Action::Drain { sourced_schemas } => { + fsm::Action::Drain { + sourced_schemas, + control, + } => { let DrainInput { drainer, parser } = self .drain_input .take() @@ -421,6 +472,12 @@ impl Actor { let shapes = self.shapes.take().context("missing capture shape state")?; let task = std::sync::Arc::clone(&self.task); let metrics = self.metrics.clone(); + let active_backfill_begin = match &control { + Some(fsm::ControlMessage::BackfillComplete { binding }) => { + self.active_backfills.get(binding).copied() + } + _ => None, + }; self.drain_fut = Some( async move { drain::drain_and_publish( @@ -429,6 +486,7 @@ impl Actor { publisher, task, sourced_schemas, + (control, active_backfill_begin), shapes, metrics, ) @@ -471,7 +529,7 @@ impl Actor { .persist(&persist, &binding_state_keys) .await .context("Persisting capture state")?; - Ok((db, binding_state_keys)) + Ok(((db, binding_state_keys), persist.active_backfill_change)) } .boxed(), ); @@ -507,6 +565,32 @@ impl Actor { true } + fsm::Action::ApplyTruncatedLabels => { + // Only the control producer manages truncated-at labels. A + // sub-range shard that inherited `active_backfills` (e.g. a split + // mid-backfill) must not re-apply labels it can never clear — it + // never receives the BackfillComplete that removes them. + if !self.is_control_producer + || !self.labels_dirty + || self.active_backfills.is_empty() + { + false + } else { + let mut publisher = + self.publisher.take().context("missing capture publisher")?; + let active_backfills = std::mem::take(&mut self.active_backfills); + self.labels_apply_fut = Some( + async move { + let result = + publisher.apply_truncated_at_labels(&active_backfills).await; + (publisher, active_backfills, result) + } + .boxed(), + ); + true + } + } + fsm::Action::Error(error) => return Err(error), }; @@ -552,7 +636,7 @@ impl Actor { ) -> anyhow::Result<()> { let verify = crate::verify( "Capture", - "Captured, SourcedSchema, or Checkpoint", + "Captured, SourcedSchema, Checkpoint, BackfillBegin, or BackfillComplete", "connector", ); let Some(response) = msg else { @@ -580,6 +664,42 @@ impl Actor { "received Checkpoint from connector", ); fsm::ConnectorRx::Checkpoint(checkpoint) + } else if let Some(response::BackfillBegin { binding }) = response.backfill_begin { + if !self.is_control_producer { + anyhow::bail!( + "connector emitted BackfillBegin for binding {binding}, but this \ + capture shard is not the backfill control producer" + ); + } + if binding as usize >= self.task.bindings.len() { + anyhow::bail!("connector emitted BackfillBegin for out-of-range binding {binding}"); + } + service_kit::event!( + tracing::Level::INFO, + "connector", + binding, + "received BackfillBegin from connector", + ); + fsm::ConnectorRx::Control(fsm::ControlMessage::BackfillBegin { binding }) + } else if let Some(response::BackfillComplete { binding }) = response.backfill_complete { + if !self.is_control_producer { + anyhow::bail!( + "connector emitted BackfillComplete for binding {binding}, but this \ + capture shard is not the backfill control producer" + ); + } + if binding as usize >= self.task.bindings.len() { + anyhow::bail!( + "connector emitted BackfillComplete for out-of-range binding {binding}" + ); + } + service_kit::event!( + tracing::Level::INFO, + "connector", + binding, + "received BackfillComplete from connector", + ); + fsm::ConnectorRx::Control(fsm::ControlMessage::BackfillComplete { binding }) } else { return Err(verify.fail_msg(response)); }; @@ -742,9 +862,11 @@ mod tests { let shapes = task.binding_shapes_by_index(Default::default()); let actor = Actor::new( + BTreeMap::new(), vec!["stateA".to_string(), "stateB".to_string()], connector_tx, crate::shard::RocksDB::open(None).await.unwrap(), + true, super::super::Metrics::new("test/shard"), publisher, shapes, @@ -827,9 +949,11 @@ mod tests { let shapes = task.binding_shapes_by_index(Default::default()); let mut actor = Actor::new( + BTreeMap::new(), vec!["stateA".to_string()], connector_tx, crate::shard::RocksDB::open(None).await.unwrap(), + true, super::super::Metrics::new("test/shard"), publisher, shapes, @@ -888,6 +1012,270 @@ mod tests { assert!(actor.split_fut.is_none()); } + /// Backfill lifecycle across a restart: a Begin persists the active backfill, a + /// fresh session recovers it, a Complete removes it, and an orphaned Complete + /// (never-begun binding) is a no-op. `truncated_at` is 0 — the preview + /// publisher's no-op control clock. Each control message is sealed by its own + /// terminating Checkpoint. + #[tokio::test] + async fn backfill_control_rejects_out_of_range_binding() { + // `mk_task(true)` has two bindings (indices 0 and 1); index 2 is out of + // range. An out-of-range binding from the connector must surface as a + // clean error rather than panicking downstream in publisher indexing. + let (connector_tx, _conn_rx) = mpsc::channel::(crate::CHANNEL_BUFFER); + let db = crate::shard::RocksDB::open(None).await.unwrap(); + let task = std::sync::Arc::new(mk_task(true)); + let collection_specs: Vec = task + .bindings + .iter() + .map(|b| flow::CollectionSpec { + name: b.collection_name.clone(), + ..Default::default() + }) + .collect(); + let publisher = crate::Publisher::new_preview(collection_specs.iter()); + let shapes = task.binding_shapes_by_index(Default::default()); + + let actor = Actor::new( + BTreeMap::new(), + vec!["stateA".to_string(), "stateB".to_string()], + connector_tx, + db, + true, // is_control_producer + super::super::Metrics::new("test/shard"), + publisher, + shapes, + task, + None, // token_restart_at + ); + + let mut ready = fsm::ConnectorRx::Eof; + for response in [ + Response { + backfill_begin: Some(response::BackfillBegin { binding: 2 }), + ..Default::default() + }, + Response { + backfill_complete: Some(response::BackfillComplete { binding: 2 }), + ..Default::default() + }, + ] { + let err = actor + .on_connector_rx(&mut ready, Some(Ok(response))) + .unwrap_err(); + assert!( + err.to_string().contains("out-of-range binding 2"), + "unexpected error: {err}", + ); + } + } + + #[tokio::test] + async fn serve_backfill_lifecycle() { + // One capture session over `db`: feed `responses`, drain `expect_acks` + // Acknowledges (one per committed control transaction, so each commits + // before Stop), then Stop and return the db. + async fn run_capture_session( + db: crate::shard::RocksDB, + active_backfills: BTreeMap, + responses: Vec>, + expect_acks: usize, + ) -> crate::shard::RocksDB { + let (connector_tx, mut actor_to_conn_rx) = + mpsc::channel::(crate::CHANNEL_BUFFER); + let (conn_resp_tx, conn_resp_rx) = + mpsc::channel::>(crate::CHANNEL_BUFFER); + let (controller_tx, controller_rx) = + mpsc::unbounded_channel::>(); + + let task = std::sync::Arc::new(mk_task(true)); + let collection_specs: Vec = task + .bindings + .iter() + .map(|b| flow::CollectionSpec { + name: b.collection_name.clone(), + ..Default::default() + }) + .collect(); + let publisher = crate::Publisher::new_preview(collection_specs.iter()); + let shapes = task.binding_shapes_by_index(Default::default()); + + let actor = Actor::new( + active_backfills, + vec!["stateA".to_string(), "stateB".to_string()], + connector_tx, + db, + true, + super::super::Metrics::new("test/shard"), + publisher, + shapes, + task, + None, // token_restart_at + ); + + let serve = tokio::spawn(async move { + let mut controller_rx = UnboundedReceiverStream::new(controller_rx); + actor + .serve( + ReceiverStream::new(conn_resp_rx), + &mut controller_rx, + fsm::Head::Idle(fsm::HeadIdle::default()), + fsm::Tail::Recover(fsm::TailRecover { + checkpoints: 0, + ack_intents: BTreeMap::new(), + }), + ) + .await + }); + + for response in responses { + conn_resp_tx.send(response).await.unwrap(); + } + for _ in 0..expect_acks { + assert!(actor_to_conn_rx.recv().await.unwrap().acknowledge.is_some()); + } + + controller_tx + .send(Ok(proto::Capture { + stop: Some(proto::Stop {}), + ..Default::default() + })) + .unwrap(); + let (db, _shapes) = serve.await.unwrap().unwrap(); + db + } + + let state_keys = || vec![("stateA".to_string(), 0u32), ("stateB".to_string(), 1u32)]; + + let db = run_capture_session( + crate::shard::RocksDB::open(None).await.unwrap(), + BTreeMap::new(), + vec![ + Ok(Response { + backfill_begin: Some(response::BackfillBegin { binding: 0 }), + ..Default::default() + }), + checkpoint(br#"{"cursor":"1"}"#), + ], + 1, + ) + .await; + let (db, recover) = db.scan(state_keys()).await.unwrap(); + assert_eq!( + recover.active_backfills, + BTreeMap::from([(0u32, 0u64)]), + "begin persisted the active backfill", + ); + + let db = run_capture_session( + db, + recover.active_backfills, + vec![ + Ok(Response { + backfill_complete: Some(response::BackfillComplete { binding: 0 }), + ..Default::default() + }), + checkpoint(br#"{"cursor":"2"}"#), + Ok(Response { + backfill_complete: Some(response::BackfillComplete { binding: 1 }), + ..Default::default() + }), + checkpoint(br#"{"cursor":"3"}"#), + ], + 2, + ) + .await; + let (_db, recover) = db.scan(state_keys()).await.unwrap(); + assert_eq!( + recover.active_backfills, + BTreeMap::new(), + "complete removed binding 0; orphaned complete for binding 1 was a no-op", + ); + } + + /// A shard recovered mid-backfill (non-empty `active_backfills`) re-applies its + /// truncated-at labels on the first `ApplyTruncatedLabels` rather than skipping + /// — the restart case a false `labels_dirty` seed would silently break. + #[tokio::test] + async fn recovered_active_backfills_reapply_labels() { + let (connector_tx, _connector_rx) = mpsc::channel::(crate::CHANNEL_BUFFER); + let task = std::sync::Arc::new(mk_task(true)); + let collection_specs: Vec = task + .bindings + .iter() + .map(|b| flow::CollectionSpec { + name: b.collection_name.clone(), + ..Default::default() + }) + .collect(); + let publisher = crate::Publisher::new_preview(collection_specs.iter()); + let shapes = task.binding_shapes_by_index(Default::default()); + + let mut actor = Actor::new( + BTreeMap::from([(0u32, 5u64)]), // recovered mid-backfill + vec!["stateA".to_string(), "stateB".to_string()], + connector_tx, + crate::shard::RocksDB::open(None).await.unwrap(), + true, + super::super::Metrics::new("test/shard"), + publisher, + shapes, + task.clone(), + None, // token_restart_at + ); + + let mut accumulator = crate::Accumulator::new(task.combine_spec().unwrap()).unwrap(); + actor + .dispatch(fsm::Action::ApplyTruncatedLabels, &mut accumulator) + .unwrap(); + assert!( + actor.labels_apply_fut.is_some(), + "recovered active backfills must re-apply labels, not skip", + ); + } + + /// A sub-range shard (not the control producer) that inherited + /// `active_backfills` via a mid-backfill split must NOT apply truncated-at + /// labels — only the control producer manages them, and a sub-range shard + /// never receives the BackfillComplete that would clear them. + #[tokio::test] + async fn non_control_producer_skips_label_apply() { + let (connector_tx, _connector_rx) = mpsc::channel::(crate::CHANNEL_BUFFER); + let task = std::sync::Arc::new(mk_task(true)); + let collection_specs: Vec = task + .bindings + .iter() + .map(|b| flow::CollectionSpec { + name: b.collection_name.clone(), + ..Default::default() + }) + .collect(); + let publisher = crate::Publisher::new_preview(collection_specs.iter()); + let shapes = task.binding_shapes_by_index(Default::default()); + + let mut actor = Actor::new( + BTreeMap::from([(0u32, 5u64)]), // inherited mid-backfill via a split + vec!["stateA".to_string(), "stateB".to_string()], + connector_tx, + crate::shard::RocksDB::open(None).await.unwrap(), + false, // NOT the control producer + super::super::Metrics::new("test/shard"), + publisher, + shapes, + task.clone(), + None, // token_restart_at + ); + + let mut accumulator = crate::Accumulator::new(task.combine_spec().unwrap()).unwrap(); + actor + .dispatch(fsm::Action::ApplyTruncatedLabels, &mut accumulator) + .unwrap(); + assert!( + actor.labels_apply_fut.is_none(), + "a non-control-producer shard must not apply truncated-at labels", + ); + } + /// `parse_sourced_schema` resolves a valid closed schema to its binding and /// inferred shape, and rejects an out-of-range binding index. #[test] diff --git a/crates/runtime-next/src/shard/capture/drain.rs b/crates/runtime-next/src/shard/capture/drain.rs index 070971bf4e4..d005b6a2dd2 100644 --- a/crates/runtime-next/src/shard/capture/drain.rs +++ b/crates/runtime-next/src/shard/capture/drain.rs @@ -11,6 +11,8 @@ //! it owns the publisher for its duration and hands it back via [`Output`]. use crate::leader::capture::{Task, fsm}; +use crate::proto; +use crate::publish; use anyhow::Context; use bytes::Bytes; use std::collections::{BTreeMap, BTreeSet}; @@ -43,6 +45,7 @@ pub(super) async fn drain_and_publish( mut publisher: crate::Publisher, task: std::sync::Arc, sourced_schemas: BTreeMap, + (control, active_backfill_begin): (Option, Option), mut shapes: Vec, metrics: super::Metrics, ) -> anyhow::Result { @@ -59,6 +62,47 @@ pub(super) async fn drain_and_publish( apply_sourced_schemas(&mut shapes, &task, sourced_schemas, &mut updated_inferences)?; + // A control message stands alone in its (isolated) transaction, so no ordinary + // captured documents are drained in the same pass; its CONTROL document is + // published first, taking the lowest clock — the authoritative `truncated_at`. + let drained_control = match control { + Some(fsm::ControlMessage::BackfillBegin { binding }) => { + let clock = publisher + .publish_control(binding as usize, publish::BackfillControl::Begin) + .await + .map_err(crate::status_to_anyhow) + .context("publishing BackfillBegin control document")?; + Some(proto::persist::ActiveBackfillChange::Begin( + proto::ActiveBackfillBegin { + binding, + truncated_at: clock.as_u64(), + }, + )) + } + Some(fsm::ControlMessage::BackfillComplete { binding }) => { + if let Some(begin_clock) = active_backfill_begin { + _ = publisher + .publish_control( + binding as usize, + publish::BackfillControl::Complete { + truncated_at: begin_clock, + }, + ) + .await + .map_err(crate::status_to_anyhow) + .context("publishing BackfillComplete control document")?; + Some(proto::persist::ActiveBackfillChange::CompleteBinding( + binding, + )) + } else { + // Orphaned complete (no active backfill, e.g. a begin was never + // observed): publish nothing, change nothing. + None + } + } + None => None, + }; + // State-Update-Wire-Format stream of this transaction's connector patches: // a `[`, then `,`-separated compact-JSON patches each terminated by `\t`, // and a closing `]` appended once the drain completes. @@ -140,6 +184,7 @@ pub(super) async fn drain_and_publish( drained: fsm::DrainedCapture { connector_patches: Bytes::from(connector_patches), bindings: drained, + control: drained_control, }, publisher, shapes, diff --git a/crates/runtime-next/src/shard/capture/handler.rs b/crates/runtime-next/src/shard/capture/handler.rs index cbbf0655124..870725630b4 100644 --- a/crates/runtime-next/src/shard/capture/handler.rs +++ b/crates/runtime-next/src/shard/capture/handler.rs @@ -302,6 +302,7 @@ where db = db.seed_connector_state(&mut recover).await?; let proto::Recover { ack_intents, + active_backfills, connector_state_json, last_applied, .. @@ -390,10 +391,18 @@ where // binding layout, and stow the session's final shapes back when it ends. let shapes = task.binding_shapes_by_index(std::mem::take(shapes_by_key)); + // Only single shard captures may produce backfill control messages. + let is_control_producer = range.key_begin == 0 + && range.key_end == u32::MAX + && range.r_clock_begin == 0 + && range.r_clock_end == u32::MAX; + let (db, shapes) = super::actor::Actor::new( + active_backfills, binding_state_keys, connector_tx, db, + is_control_producer, metrics, publisher, shapes, diff --git a/crates/runtime-next/src/shard/materialize/actor.rs b/crates/runtime-next/src/shard/materialize/actor.rs index 49ba46eca44..496e416cd04 100644 --- a/crates/runtime-next/src/shard/materialize/actor.rs +++ b/crates/runtime-next/src/shard/materialize/actor.rs @@ -21,9 +21,24 @@ pub(super) enum Phase { } /// Shard-side materialization reactor for one joined leader session. -pub(super) struct Actor { +pub(crate) struct Actor { // Task binding specifications. bindings: Vec, + // Cumulative backfill-begin clock per binding. + backfill_begin: Vec, + // Truncation boundary (begin clock) of the latest completed backfill per + // binding. + backfill_complete: Vec, + // Backfill-begin clock the connector has been notified of per binding. + notified_backfill_begin: Vec, + // Backfill-complete clock the connector has been notified of per binding. + notified_backfill_complete: Vec, + // True if this is shard zero, the only shard that emits backfill + // notifications to its connector. Shard zero alone receives L:Persist and so + // is the only shard with a durable, crash-safe `notified_*` baseline; the + // backfill boundary is a destination-global operation, so a single notifier + // suffices. See `backfill_flush_notifications`. + is_shard_zero: bool, // FIFO of outbound connector requests, drained head-first into // `connector_tx` as channel capacity permits. connector_pending: Vec, @@ -71,6 +86,9 @@ impl Actor { codec: connector_init::Codec, leader_tx: mpsc::UnboundedSender, max_keys: Vec<(Bytes, Bytes)>, + notified_backfill_begin: Vec, + notified_backfill_complete: Vec, + is_shard_zero: bool, metrics: super::Metrics, token_restart_at: Option, ) -> Self { @@ -82,8 +100,14 @@ impl Actor { tokio::time::Instant::now() + delay }); + let l = bindings.len(); Self { bindings, + backfill_begin: vec![0; l], + backfill_complete: vec![0; l], + notified_backfill_begin, + notified_backfill_complete, + is_shard_zero, connector_pending: Vec::new(), connector_tx, db: Some((db, binding_state_keys)), @@ -361,6 +385,21 @@ impl Actor { let frontier = shuffle::Frontier::decode(proto).context("invalid Frontier on L:Load")?; + // Resolve cumulative per-binding backfill-begin (truncated_at) clocks + // from the Frontier, keyed by each binding's journal_read_suffix. + for (begin, binding) in self.backfill_begin.iter_mut().zip(self.bindings.iter()) { + *begin = frontier + .latest_backfill_begin + .get(&binding.journal_read_suffix) + .map_or(0, |clock| clock.as_u64()); + } + for (complete, binding) in self.backfill_complete.iter_mut().zip(self.bindings.iter()) { + *complete = frontier + .latest_backfill_complete + .get(&binding.journal_read_suffix) + .map_or(0, |clock| clock.as_u64()); + } + let Phase::Idle { accumulator, shuffle_reader, @@ -370,16 +409,25 @@ impl Actor { anyhow::bail!("L:Load received while actor is not idle"); }; - let scanner = - scan::Scanner::new(accumulator, frontier, shuffle_reader, shuffle_remainders)?; + let scanner = scan::Scanner::new( + accumulator, + frontier, + shuffle_reader, + shuffle_remainders, + self.backfill_begin.clone(), + )?; return Ok((Phase::Scanning(scanner), false)); } else if let Some(proto::materialize::Flush { connector_patches_json, }) = msg.flush { + let (backfill_begins, backfill_completes) = self.backfill_flush_notifications(); self.connector_pending.push(materialize::Request { flush: Some(materialize::request::Flush { state_patches_json: connector_patches_json, + backfill_begins, + backfill_completes, + ..Default::default() }), ..Default::default() }); @@ -432,6 +480,13 @@ impl Actor { } else if let Some(persist) = msg.persist { let seq_no = persist.seq_no; + if self.notified_backfill_begin != self.backfill_begin + || self.notified_backfill_complete != self.backfill_complete + { + self.notified_backfill_begin = self.backfill_begin.clone(); + self.notified_backfill_complete = self.backfill_complete.clone(); + } + let (db, binding_state_keys) = self .db .take() @@ -451,6 +506,50 @@ impl Actor { Ok((phase, false)) } + /// Compute connector backfill notifications for bindings whose in-flight + /// begin/complete clock is ahead of the notified baseline. The `timestamp` + /// is always the begin clock — the truncation boundary — as a wall-clock + /// Timestamp. The baseline advances at commit, so each notification fires + /// once and is not re-sent across a clean restart. + /// + /// Only shard zero emits these. It is the sole shard that receives + /// L:Persist (the durable runtime checkpoint is persisted to shard zero + /// only), so it alone advances and recovers the `notified_*` baseline + /// crash-safely. + fn backfill_flush_notifications( + &self, + ) -> ( + Vec, + Vec, + ) { + if !self.is_shard_zero { + return (Vec::new(), Vec::new()); + } + + let mut begins = Vec::new(); + let mut completes = Vec::new(); + + for binding in 0..self.backfill_begin.len() { + let begin = self.backfill_begin[binding]; + if begin > self.notified_backfill_begin[binding] { + begins.push(materialize::request::flush::BackfillBegin { + binding: binding as u32, + timestamp: proto_gazette::uuid::Clock::from_u64(begin).to_pb_json_timestamp(), + }); + } + + let complete = self.backfill_complete[binding]; + if complete > self.notified_backfill_complete[binding] { + completes.push(materialize::request::flush::BackfillComplete { + binding: binding as u32, + timestamp: proto_gazette::uuid::Clock::from_u64(complete) + .to_pb_json_timestamp(), + }); + } + } + (begins, completes) + } + fn on_connector_response( &mut self, phase: &mut Phase, @@ -501,7 +600,35 @@ impl Actor { accumulator.parse_json_doc(&doc_json).with_context(|| { format!("parsing loaded doc for {}", binding_spec.collection_name) })?; - memtable.add(binding_index as u16, doc, true)?; + + // A loaded row is prior-generation when it predates the binding's + // backfill `truncated_at`: the combiner then won't reduce it with + // same-key current-generation source documents, and the drain path + // discards it. Skip the lookup entirely when no backfill is active. + let begin = self.backfill_begin[binding_index]; + let prior_gen = if begin == 0 { + false + } else if let Some(doc::HeapNode::String(uuid)) = + binding_spec.document_uuid_ptr.query(&doc) + { + let (_, clock, _) = proto_gazette::uuid::parse_str(uuid).with_context(|| { + format!( + "loaded doc for {} has an unparseable document UUID {uuid:?}", + binding_spec.collection_name, + ) + })?; + clock.as_u64() < begin + } else { + // No resolvable document UUID, so the row's publication time is + // unknown and it can't be classified. + false + }; + + if prior_gen { + memtable.add_prior_gen(binding_index as u16, doc)?; + } else { + memtable.add(binding_index as u16, doc, true)?; + } } else if let Some(materialize::response::Flushed { state }) = resp.flushed { let bindings = std::mem::take(&mut self.flushed).into_values().collect(); _ = self.leader_tx.send(proto::Materialize { @@ -603,6 +730,11 @@ mod tests { ( Actor { bindings: Vec::new(), + backfill_begin: Vec::new(), + backfill_complete: Vec::new(), + notified_backfill_begin: Vec::new(), + notified_backfill_complete: Vec::new(), + is_shard_zero: true, connector_pending: Vec::new(), connector_tx, db: None, @@ -701,6 +833,11 @@ mod tests { let actor = Actor { bindings: Vec::new(), + backfill_begin: Vec::new(), + backfill_complete: Vec::new(), + notified_backfill_begin: Vec::new(), + notified_backfill_complete: Vec::new(), + is_shard_zero: true, connector_pending: Vec::new(), connector_tx: actor_to_conn_tx, db: Some((db, Vec::new())), @@ -894,4 +1031,317 @@ mod tests { let (_db, recover) = db.scan(Vec::new()).await.unwrap(); assert_eq!(recover.last_applied.as_ref(), b"persisted-spec-bytes"); } + + #[tokio::test] + async fn backfill_notifications_fire_once_and_survive_restart() { + let (mut actor, _leader_rx, mut connector_rx) = make_actor(); + actor.backfill_begin = vec![0]; + actor.backfill_complete = vec![0]; + actor.notified_backfill_begin = vec![0]; + actor.notified_backfill_complete = vec![0]; + + // Backfill 1 begins. The real L:Flush handler stages a C:Flush carrying + // the begin notification to the connector. + actor.backfill_begin[0] = 10; + let (phase, _stop) = actor + .on_leader_message( + make_idle_phase(), + Some(Ok(proto::Materialize { + flush: Some(proto::materialize::Flush { + connector_patches_json: Bytes::new(), + }), + ..Default::default() + })), + ) + .unwrap(); + _ = actor.try_connector_tx(); + let flush = connector_rx.recv().await.unwrap().flush.unwrap(); + assert_eq!( + flush.backfill_begins.len(), + 1, + "begin1 forwarded to connector" + ); + assert_eq!(flush.backfill_begins[0].binding, 0); + assert!(flush.backfill_completes.is_empty()); + + // Flush forwards but does not commit; the baseline holds until Persist. + assert_eq!(actor.notified_backfill_begin, vec![0]); + + // The real L:Persist handler commits, advancing the durable baseline to + // the in-flight clocks so begin1 isn't re-sent. + let db = crate::shard::RocksDB::open(None).await.unwrap(); + actor.db = Some((db, Vec::new())); + _ = actor + .on_leader_message( + phase, + Some(Ok(proto::Materialize { + persist: Some(proto::Persist { + seq_no: 1, + ..Default::default() + }), + ..Default::default() + })), + ) + .unwrap(); + assert_eq!(actor.notified_backfill_begin, vec![10], "begin1 committed"); + + // The remaining rounds poke the trackers directly to exercise the edge + // detection compactly. `backfill_flush_notifications` is exactly what the + // L:Flush handler above calls; committing is `notified_* := backfill_*`. + actor.backfill_complete[0] = 20; // complete1 > begin1 + let (begins, completes) = actor.backfill_flush_notifications(); + assert_eq!((begins.len(), completes.len()), (0, 1), "complete1 fires"); + actor.notified_backfill_complete[0] = 20; // commit + + // Backfill 2 begins (begin2 > complete1); its completion hasn't arrived, + // so the only complete clock is still complete1 (< begin2) — no completion. + actor.backfill_begin[0] = 30; + let (begins, completes) = actor.backfill_flush_notifications(); + assert_eq!( + (begins.len(), completes.len()), + (1, 0), + "begin2 surfaces; no completion while it's in flight", + ); + actor.notified_backfill_begin[0] = 30; // commit + + // Restart mid-backfill-2: the notified baseline reseeds from the DURABLE + // committed frontier (begin2=30, complete1=20), NOT to 0; the in-flight + // clocks reload to the same committed values from the first Load. + actor.notified_backfill_begin = vec![30]; + actor.notified_backfill_complete = vec![20]; + actor.backfill_begin = vec![30]; + actor.backfill_complete = vec![20]; + let (begins, completes) = actor.backfill_flush_notifications(); + assert!( + begins.is_empty() && completes.is_empty(), + "a clean restart re-fires nothing: the notified baseline already \ + covers begin2 and complete1", + ); + + // Backfill 2's own completion (complete2 > begin2) finally fires once. + actor.backfill_complete[0] = 40; + let (_begins, completes) = actor.backfill_flush_notifications(); + assert_eq!(completes.len(), 1, "begin2's real completion fires"); + } + + #[tokio::test] + async fn non_zero_shard_never_notifies() { + // A non-zero shard never receives L:Persist, so its `notified_*` baseline + // can neither advance nor recover; it must suppress backfill + // notifications entirely or it would re-notify on every transaction. + // Shard zero alone drives the connector's (destination-global) delete. + let (mut actor, _leader_rx, _connector_rx) = make_actor(); + actor.is_shard_zero = false; + actor.backfill_begin = vec![10]; + actor.backfill_complete = vec![20]; + actor.notified_backfill_begin = vec![0]; + actor.notified_backfill_complete = vec![0]; + + let (begins, completes) = actor.backfill_flush_notifications(); + assert!( + begins.is_empty() && completes.is_empty(), + "a non-zero shard emits no backfill notifications regardless of clocks", + ); + } + + // A full-reduction binding storing the root document, keyed on /key, whose + // `v` array reduces by append. `document_uuid_ptr` lets the shard read each + // loaded row's UUID to classify it against the backfill boundary. + fn backfill_binding() -> Binding { + Binding { + collection_name: "test/collection".to_string(), + delta_updates: false, + document_uuid_ptr: json::Pointer::from("/_meta/uuid"), + journal_read_suffix: "test/collection/pivot=00".to_string(), + key_extractors: vec![doc::Extractor::with_default( + "/key", + &doc::SerPolicy::noop(), + serde_json::json!(""), + )], + read_schema_json: bytes::Bytes::from_static( + br#"{ + "type": "object", + "properties": { + "key": { "type": "string" }, + "v": { "type": "array", "reduce": { "strategy": "append" } } + }, + "reduce": { "strategy": "merge" } + }"#, + ), + ser_policy: doc::SerPolicy::noop(), + state_key: "test/collection".to_string(), + store_document: true, + value_plan: doc::ExtractorPlan::new(&[]), + } + } + + #[tokio::test] + async fn backfill_load_classifies_loaded_docs_through_drain() { + let producer = proto_gazette::uuid::Producer::from_bytes([0x01, 0, 0, 0, 0, 0]); + let flags = proto_gazette::uuid::Flags(0); + let mk_uuid = |clock| proto_gazette::uuid::build(producer, clock, flags).to_string(); + // The boundary, plus a row clock below it (stale) and above it (fresh). + let truncated_at = proto_gazette::uuid::Clock::from_unix(1_700_000_000, 0); + let stale = mk_uuid(proto_gazette::uuid::Clock::from_unix(1_699_999_999, 0)); + let fresh = mk_uuid(proto_gazette::uuid::Clock::from_unix(1_700_000_001, 0)); + + let (mut actor, _leader_rx, _connector_rx) = make_actor(); + actor.bindings = vec![backfill_binding()]; + actor.backfill_begin = vec![0]; + actor.backfill_complete = vec![0]; + actor.notified_backfill_begin = vec![0]; + actor.notified_backfill_complete = vec![0]; + + let accumulator = crate::Accumulator::new( + super::super::task::combine_spec(&[backfill_binding()]).unwrap(), + ) + .unwrap(); + let shuffle_dir = tempfile::tempdir().unwrap(); + let shuffle_reader = shuffle::log::Reader::new(shuffle_dir.path(), 0); + let idle = Phase::Idle { + accumulator, + shuffle_reader, + shuffle_remainders: VecDeque::new(), + }; + + // L:Load — the Frontier carries the binding's `truncated_at`, which the + // handler densifies into `backfill_begin` before entering the scan. + let mut frontier = shuffle::Frontier::new(Vec::new(), vec![0u64]).unwrap(); + frontier + .latest_backfill_begin + .insert(actor.bindings[0].journal_read_suffix.clone(), truncated_at); + + let (mut phase, _stop) = actor + .on_leader_message( + idle, + Some(Ok(proto::Materialize { + load: Some(proto::materialize::Load { + frontier: Some(frontier.encode()), + }), + ..Default::default() + })), + ) + .unwrap(); + + assert_eq!( + actor.backfill_begin, + vec![truncated_at.as_u64()], + "begin clock densified from Frontier" + ); + assert_eq!( + actor.backfill_complete, + vec![0], + "no completion present in Frontier" + ); + assert!( + matches!(phase, Phase::Scanning(_)), + "L:Load enters the scan" + ); + + // Three C:Loaded rows, classified against the boundary as they arrive. + let loaded = |key: &str, v: &str, uuid: Option<&str>| { + let doc = match uuid { + Some(u) => serde_json::json!({"key": key, "v": [v], "_meta": {"uuid": u}}), + None => serde_json::json!({"key": key, "v": [v]}), + }; + materialize::Response { + loaded: Some(materialize::response::Loaded { + binding: 0, + doc_json: Bytes::from(serde_json::to_vec(&doc).unwrap()), + }), + ..Default::default() + } + }; + for resp in [ + loaded("straddle", "stale", Some(&stale)), // older than the boundary + loaded("normal", "loaded", Some(&fresh)), // newer than the boundary + loaded("nouuid", "kept", None), // no UUID to compare + ] { + actor + .on_connector_response(&mut phase, Some(Ok(resp))) + .unwrap(); + } + + // Inject the current-generation source documents the scan would surface, + // pairing one against each loaded row. + let Phase::Scanning(mut scanner) = phase else { + panic!("expected Scanning phase after L:Load"); + }; + { + let memtable = scanner.accumulator().memtable().unwrap(); + for (key, v) in [("straddle", "fresh"), ("normal", "src"), ("nouuid", "add")] { + let doc = serde_json::json!({"key": key, "v": [v]}); + let node = doc::HeapNode::from_node(&doc, memtable.alloc()); + memtable.add(0, node, false).unwrap(); + } + } + + // Drain and collect each stored (key, v, exists). + let (accumulator, shuffle_reader, shuffle_remainders, _active) = scanner.into_parts(); + let mut drainer = + drain::Drainer::new(accumulator, shuffle_reader, shuffle_remainders).unwrap(); + + let mut stores = Vec::new(); + while let Some(req) = drainer + .step(&actor.bindings, connector_init::Codec::Json) + .unwrap() + { + let store = req.store.expect("drained request is a Store"); + let doc: serde_json::Value = serde_json::from_slice(&store.doc_json).unwrap(); + stores.push(( + doc.get("key").and_then(|k| k.as_str()).unwrap().to_string(), + doc.get("v").cloned().unwrap(), + store.exists, + )); + } + + // Drained in key order. "straddle" is prior-generation: its stale ["stale"] + // is dropped (NOT reduced) and only the current-gen source ["fresh"] stores, + // with exists=true. "normal" (newer than the boundary) and "nouuid" (no UUID) + // load normally, reducing their loaded value forward. + assert_eq!( + stores, + vec![ + ( + "normal".to_string(), + serde_json::json!(["loaded", "src"]), + true + ), + ( + "nouuid".to_string(), + serde_json::json!(["kept", "add"]), + true + ), + ("straddle".to_string(), serde_json::json!(["fresh"]), true), + ], + ); + } + + #[tokio::test] + async fn loaded_doc_with_corrupt_uuid_errors() { + let (mut actor, _leader_rx, _connector_rx) = make_actor(); + actor.bindings = vec![backfill_binding()]; + actor.backfill_begin = vec![10]; // active backfill + actor.backfill_complete = vec![0]; + actor.notified_backfill_begin = vec![0]; + actor.notified_backfill_complete = vec![0]; + + // /_meta/uuid is present and a string, but not a valid v1 UUID. + let doc = serde_json::json!({"key": "k", "v": ["x"], "_meta": {"uuid": "not-a-uuid"}}); + let mut phase = make_idle_phase(); + let result = actor.on_connector_response( + &mut phase, + Some(Ok(materialize::Response { + loaded: Some(materialize::response::Loaded { + binding: 0, + doc_json: Bytes::from(serde_json::to_vec(&doc).unwrap()), + }), + ..Default::default() + })), + ); + assert!( + result.is_err(), + "a corrupt document UUID fails the transaction" + ); + } } diff --git a/crates/runtime-next/src/shard/materialize/handler.rs b/crates/runtime-next/src/shard/materialize/handler.rs index 84be2983ddb..cd8dcf1d7b6 100644 --- a/crates/runtime-next/src/shard/materialize/handler.rs +++ b/crates/runtime-next/src/shard/materialize/handler.rs @@ -271,6 +271,8 @@ where mut leader_rx, leader_tx, max_keys, + notified_backfill_begin, + notified_backfill_complete, shuffle_reader, token_restart_at, } = startup::run( @@ -299,6 +301,9 @@ where codec, leader_tx, max_keys, + notified_backfill_begin, + notified_backfill_complete, + shard_index == 0, metrics, token_restart_at, ) diff --git a/crates/runtime-next/src/shard/materialize/mod.rs b/crates/runtime-next/src/shard/materialize/mod.rs index c52371becc6..ea1d85fd7ac 100644 --- a/crates/runtime-next/src/shard/materialize/mod.rs +++ b/crates/runtime-next/src/shard/materialize/mod.rs @@ -83,6 +83,8 @@ impl Metrics { struct Binding { collection_name: String, // Source collection. delta_updates: bool, // Delta updates, or standard? + document_uuid_ptr: json::Pointer, // Document UUID pointer (often /_meta/uuid). + journal_read_suffix: String, // Keys this binding's backfill clocks in the Frontier. key_extractors: Vec, // Key extractors for this collection. read_schema_json: bytes::Bytes, // Read JSON-Schema of collection documents. ser_policy: doc::SerPolicy, // Serialization policy for this source. diff --git a/crates/runtime-next/src/shard/materialize/scan.rs b/crates/runtime-next/src/shard/materialize/scan.rs index 0f056700f1c..f07be0c7b4f 100644 --- a/crates/runtime-next/src/shard/materialize/scan.rs +++ b/crates/runtime-next/src/shard/materialize/scan.rs @@ -19,6 +19,8 @@ pub(super) struct Scanner { buf: bytes::BytesMut, // Active bindings of this scan, indexed on binding index. active: HashMap, + // Cumulative per-binding backfill-begin clocks. + backfill_begin: Vec, } impl Scanner { @@ -27,6 +29,7 @@ impl Scanner { frontier: shuffle::Frontier, shuffle_reader: shuffle::log::Reader, shuffle_remainders: VecDeque, + backfill_begin: Vec, ) -> anyhow::Result { let scan = shuffle::log::FrontierScan::new(frontier, shuffle_reader, shuffle_remainders) .context("failed to begin a FrontierScan")?; @@ -36,6 +39,7 @@ impl Scanner { scan, buf: bytes::BytesMut::new(), active: HashMap::new(), + backfill_begin, }) } @@ -84,6 +88,14 @@ impl Scanner { .get(meta.binding.to_native() as usize) .context("scan entry has invalid meta.binding")?; + // Suppress source documents published before this binding's + // backfill `truncated_at`: a later full-refresh superseded them, so + // they must not be materialized. + let begin = self.backfill_begin[binding_index as usize]; + if meta.clock.to_native() < begin { + continue; + } + memtable .add_embedded( meta.binding.to_native(), diff --git a/crates/runtime-next/src/shard/materialize/startup.rs b/crates/runtime-next/src/shard/materialize/startup.rs index f67cd9c9a25..33968f17088 100644 --- a/crates/runtime-next/src/shard/materialize/startup.rs +++ b/crates/runtime-next/src/shard/materialize/startup.rs @@ -75,6 +75,8 @@ pub(super) struct Startup { pub leader_rx: tonic::Streaming, pub leader_tx: mpsc::UnboundedSender, pub max_keys: Vec<(bytes::Bytes, bytes::Bytes)>, + pub notified_backfill_begin: Vec, + pub notified_backfill_complete: Vec, pub shuffle_reader: shuffle::log::Reader, pub token_restart_at: Option, } @@ -148,6 +150,9 @@ where db = db.seed_connector_state(&mut recover).await?; } + let (notified_backfill_begin, notified_backfill_complete) = + recovered_backfill_baselines(&recover, &bindings); + _ = leader_tx.send(proto::Materialize { recover: Some(recover), ..Default::default() @@ -306,7 +311,110 @@ where leader_rx, leader_tx, max_keys, + notified_backfill_begin, + notified_backfill_complete, shuffle_reader, token_restart_at, }) } + +fn recovered_backfill_baselines( + recover: &proto::Recover, + bindings: &[Binding], +) -> (Vec, Vec) { + let frontier = recover.committed_frontier.as_ref(); + + let begin = bindings + .iter() + .map(|b| { + frontier + .and_then(|f| { + f.latest_backfill_begin + .iter() + .find(|e| e.journal_read_suffix == b.journal_read_suffix) + }) + .map_or(0, |e| e.clock) + }) + .collect(); + let complete = bindings + .iter() + .map(|b| { + frontier + .and_then(|f| { + f.latest_backfill_complete + .iter() + .find(|e| e.journal_read_suffix == b.journal_read_suffix) + }) + .map_or(0, |e| e.clock) + }) + .collect(); + + (begin, complete) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn binding(suffix: &str) -> Binding { + Binding { + collection_name: String::new(), + delta_updates: false, + document_uuid_ptr: json::Pointer::from(""), + journal_read_suffix: suffix.to_string(), + key_extractors: Vec::new(), + read_schema_json: bytes::Bytes::new(), + ser_policy: doc::SerPolicy::noop(), + state_key: String::new(), + store_document: false, + value_plan: doc::ExtractorPlan::new(&[]), + } + } + + #[test] + fn recovered_baselines_densify_by_suffix() { + use proto_flow::shuffle::frontier::{BackfillBegin, BackfillComplete}; + + let recover = proto::Recover { + committed_frontier: Some(proto_flow::shuffle::Frontier { + // Ordered differently than `bindings`, and "x/9" has no binding. + latest_backfill_begin: vec![ + BackfillBegin { + journal_read_suffix: "c/2".to_string(), + clock: 30, + }, + BackfillBegin { + journal_read_suffix: "a/0".to_string(), + clock: 10, + }, + BackfillBegin { + journal_read_suffix: "x/9".to_string(), + clock: 99, + }, + ], + // "a/0" began but hasn't completed; "c/2" has both. + latest_backfill_complete: vec![BackfillComplete { + journal_read_suffix: "c/2".to_string(), + clock: 25, + }], + ..Default::default() + }), + ..Default::default() + }; + + // "b/1" has no recovered clocks at all. + let bindings = vec![binding("a/0"), binding("b/1"), binding("c/2")]; + + let (begin, complete) = recovered_backfill_baselines(&recover, &bindings); + assert_eq!( + begin, + vec![10, 0, 30], + "matched by suffix, not position; missing -> 0", + ); + assert_eq!( + complete, + vec![0, 0, 25], + "complete resolves independently of begin", + ); + } +} diff --git a/crates/runtime-next/src/shard/materialize/task.rs b/crates/runtime-next/src/shard/materialize/task.rs index 745ee08988b..643f1945173 100644 --- a/crates/runtime-next/src/shard/materialize/task.rs +++ b/crates/runtime-next/src/shard/materialize/task.rs @@ -89,7 +89,7 @@ fn build_binding( delta_updates, deprecated_shuffle: _, field_selection, - journal_read_suffix: _, + journal_read_suffix, not_after: _, not_before: _, partition_selector: _, @@ -118,7 +118,7 @@ fn build_binding( partition_template: _, projections, read_schema_json, - uuid_ptr: _, + uuid_ptr, write_schema_json, } = collection.as_ref().context("missing collection")?; @@ -165,6 +165,8 @@ fn build_binding( Ok(Binding { collection_name: collection_name.clone(), delta_updates: *delta_updates, + document_uuid_ptr: json::Pointer::from(uuid_ptr.as_str()), + journal_read_suffix: journal_read_suffix.clone(), key_extractors, read_schema_json, ser_policy, diff --git a/crates/runtime-next/src/shard/recovery.rs b/crates/runtime-next/src/shard/recovery.rs index 82739f1ac62..22f5a871aee 100644 --- a/crates/runtime-next/src/shard/recovery.rs +++ b/crates/runtime-next/src/shard/recovery.rs @@ -18,12 +18,21 @@ //! between the binding indices used in the leader protocol and the //! `state_key` strings used in RocksDB keys. //! +//! Three keys carry backfill-truncation state, split by task type: `AB:` is +//! capture state — the in-progress active backfills that drive the +//! `estuary.dev/truncated-at` journal label — while `BB:`/`BC:` are +//! materialization state — the cumulative begin/complete clocks of the durable +//! truncation boundary used for stale source/loaded handling. +//! //! | Prefix | Key tail | Value | //! |--------------|------------------------------------------|----------------------------------| //! | `FH:` | `{journal}\0{state_key}\0{producer[6]}` | proto `shuffle.ProducerFrontier` | //! | `FC:` | `{journal}\0{state_key}\0{producer[6]}` | proto `shuffle.ProducerFrontier` | //! | `AI:` | `{journal}` | raw ACK intent bytes | //! | `MK-v2:` | `{state_key}` | `tuple::pack` packed key | +//! | `AB:` | `{state_key}` | fixed64 little-endian clock | +//! | `BB:` | `{journal_read_suffix}` | fixed64 little-endian clock | +//! | `BC:` | `{journal_read_suffix}` | fixed64 little-endian clock | //! | (singleton) | `checkpoint` | legacy `consumer.Checkpoint` | //! | (singleton) | `committed-close` | fixed64 little-endian clock | //! | (singleton) | `connector-state` | reduced JSON merge-patch | @@ -52,6 +61,12 @@ pub const PREFIX_ACK_INTENT: &[u8] = b"AI:"; pub const PREFIX_ACK_INTENT_END: &[u8] = b"AI;"; /// Key prefix for per-binding max-key entries: `MK-v2:{state_key}`. pub const PREFIX_MAX_KEY: &[u8] = b"MK-v2:"; +/// Capture state. Per-binding active-backfill begin clock. +pub const PREFIX_ACTIVE_BACKFILL: &[u8] = b"AB:"; +/// Materialization state. Per-binding cumulative backfill-begin clock. +pub const PREFIX_BACKFILL_BEGIN: &[u8] = b"BB:"; +/// Materialization state. Per-binding cumulative backfill-complete clock. +pub const PREFIX_BACKFILL_COMPLETE: &[u8] = b"BC:"; /// Legacy checkpoint. pub const KEY_LEGACY_CHECKPOINT: &[u8] = b"checkpoint"; /// Clock at which the last-committed transaction closed. @@ -189,6 +204,13 @@ pub fn encode_persist>( &mut emit, &mut buf, )?; + + // Cumulative backfill clocks ride the committed Frontier (keyed by + // journal_read_suffix), persisting the durable truncation boundary so + // it survives transaction rotation and leader restart. Kept out of + // encode_frontier, which also encodes the hinted frontier, because these + // clocks are committed-only. + encode_backfill_clocks(frontier, &mut emit, &mut buf); } for patch in crate::patches::split_state_patches(&persist.connector_patches_json)? { @@ -260,6 +282,36 @@ pub fn encode_persist>( }); } + // Active-backfill change: at most one per commit. Begin records the + // binding's truncated_at clock; Complete clears it. + if let Some(change) = &persist.active_backfill_change { + let (binding, truncated_at) = match change { + proto::persist::ActiveBackfillChange::Begin(begin) => { + (begin.binding, Some(begin.truncated_at)) + } + proto::persist::ActiveBackfillChange::CompleteBinding(binding) => (*binding, None), + }; + let state_key = binding_state_keys + .get(binding as usize) + .ok_or(EncodeError::UnknownBinding { + binding, + num_bindings: binding_state_keys.len(), + })? + .as_ref(); + + buf.extend_from_slice(PREFIX_ACTIVE_BACKFILL); + buf.extend_from_slice(state_key.as_bytes()); + let key = buf.split().freeze(); + + emit(match truncated_at { + Some(clock) => KeyOp::Put { + key, + value: Bytes::copy_from_slice(&clock.to_le_bytes()), + }, + None => KeyOp::Delete { key }, + }); + } + if persist.delete_trigger_params { emit(KeyOp::Delete { key: Bytes::from_static(KEY_TRIGGER_PARAMS), @@ -322,6 +374,29 @@ fn encode_frontier>( Ok(()) } +fn encode_backfill_clocks( + frontier: &shuffle::proto::Frontier, + emit: &mut impl FnMut(KeyOp), + buf: &mut BytesMut, +) { + for entry in &frontier.latest_backfill_begin { + buf.extend_from_slice(PREFIX_BACKFILL_BEGIN); + buf.extend_from_slice(entry.journal_read_suffix.as_bytes()); + emit(KeyOp::Put { + key: buf.split().freeze(), + value: Bytes::copy_from_slice(&entry.clock.to_le_bytes()), + }); + } + for entry in &frontier.latest_backfill_complete { + buf.extend_from_slice(PREFIX_BACKFILL_COMPLETE); + buf.extend_from_slice(entry.journal_read_suffix.as_bytes()); + emit(KeyOp::Put { + key: buf.split().freeze(), + value: Bytes::copy_from_slice(&entry.clock.to_le_bytes()), + }); + } +} + fn append_frontier_key( out: &mut BytesMut, prefix: &[u8], @@ -367,6 +442,8 @@ pub fn decode_recover_key_value( recover: &mut proto::Recover, committed_frontier: &mut Vec, hinted_frontier: &mut Vec, + committed_backfill_begin: &mut std::collections::BTreeMap, + committed_backfill_complete: &mut std::collections::BTreeMap, key: &[u8], value: &[u8], binding_state_keys: &[(String, u32)], @@ -389,6 +466,23 @@ pub fn decode_recover_key_value( .insert(binding, Bytes::copy_from_slice(value)); } Ok(()) + } else if let Some(rest) = key.strip_prefix(PREFIX_ACTIVE_BACKFILL) { + let state_key = std::str::from_utf8(rest).map_err(DecodeError::InvalidUtf8)?; + if let Some(binding) = lookup_binding(binding_state_keys, state_key) { + recover + .active_backfills + .insert(binding, decode_clock(value, "active-backfill")?); + } + Ok(()) + } else if let Some(rest) = key.strip_prefix(PREFIX_BACKFILL_BEGIN) { + let suffix = std::str::from_utf8(rest).map_err(DecodeError::InvalidUtf8)?; + committed_backfill_begin.insert(suffix.to_owned(), decode_clock(value, "backfill-begin")?); + Ok(()) + } else if let Some(rest) = key.strip_prefix(PREFIX_BACKFILL_COMPLETE) { + let suffix = std::str::from_utf8(rest).map_err(DecodeError::InvalidUtf8)?; + committed_backfill_complete + .insert(suffix.to_owned(), decode_clock(value, "backfill-complete")?); + Ok(()) } else if key == KEY_COMMITTED_CLOSE { recover.committed_close_clock = decode_clock(value, "committed-close-clock")?; Ok(()) @@ -408,6 +502,37 @@ pub fn decode_recover_key_value( } } +/// Fold the cumulative backfill clocks recovered from `BB:`/`BC:` keys onto the +/// recovered committed Frontier, advancing the durable truncation boundary. +pub fn fold_backfill_clocks( + recover: &mut proto::Recover, + committed_backfill_begin: std::collections::BTreeMap, + committed_backfill_complete: std::collections::BTreeMap, +) { + if committed_backfill_begin.is_empty() && committed_backfill_complete.is_empty() { + return; + } + let frontier = recover.committed_frontier.get_or_insert_default(); + frontier.latest_backfill_begin = committed_backfill_begin + .into_iter() + .map( + |(journal_read_suffix, clock)| shuffle::proto::frontier::BackfillBegin { + journal_read_suffix, + clock, + }, + ) + .collect(); + frontier.latest_backfill_complete = committed_backfill_complete + .into_iter() + .map( + |(journal_read_suffix, clock)| shuffle::proto::frontier::BackfillComplete { + journal_read_suffix, + clock, + }, + ) + .collect(); +} + fn decode_clock(value: &[u8], kind: &'static str) -> Result { let bytes: [u8; 8] = value .try_into() @@ -693,16 +818,25 @@ mod test { let mut recover = proto::Recover::default(); let mut committed_frontier = Vec::new(); let mut hinted_frontier = Vec::new(); + let mut committed_backfill_begin = std::collections::BTreeMap::new(); + let mut committed_backfill_complete = std::collections::BTreeMap::new(); for (k, v) in pairs { decode_recover_key_value( &mut recover, &mut committed_frontier, &mut hinted_frontier, + &mut committed_backfill_begin, + &mut committed_backfill_complete, &k, &v, binding_state_keys, )?; } + fold_backfill_clocks( + &mut recover, + committed_backfill_begin, + committed_backfill_complete, + ); Ok(DecodedRecover { recover, committed_frontier, @@ -868,6 +1002,158 @@ mod test { insta::assert_debug_snapshot!(decode_pairs(store, &mapping).unwrap()); } + #[test] + fn committed_backfill_clocks_roundtrip() { + let persist = proto::Persist { + committed_frontier: Some(shuffle::proto::Frontier { + latest_backfill_begin: vec![ + shuffle::proto::frontier::BackfillBegin { + journal_read_suffix: "mat%2Ft1.v1".into(), + clock: 111, + }, + shuffle::proto::frontier::BackfillBegin { + journal_read_suffix: "mat%2Ft2.v1".into(), + clock: 222, + }, + ], + latest_backfill_complete: vec![shuffle::proto::frontier::BackfillComplete { + journal_read_suffix: "mat%2Ft1.v1".into(), + clock: 110, + }], + ..Default::default() + }), + ..Default::default() + }; + + let binding_state_keys = &["materialize/mat/t1", "materialize/mat/t2"]; + let mut store: Vec<(Bytes, Bytes)> = Vec::new(); + encode_persist(&persist, binding_state_keys, |op| apply_op(&mut store, op)).unwrap(); + store.sort_by(|a, b| a.0.cmp(&b.0)); + + let mapping = state_key_index(&[("materialize/mat/t1", 0), ("materialize/mat/t2", 1)]); + let recovered = decode_pairs(store, &mapping) + .unwrap() + .recover + .committed_frontier + .expect("committed frontier recovered"); + + assert_eq!(recovered.latest_backfill_begin, persist_frontier_begin()); + assert_eq!( + recovered.latest_backfill_complete, + vec![shuffle::proto::frontier::BackfillComplete { + journal_read_suffix: "mat%2Ft1.v1".into(), + clock: 110, + }], + ); + } + + #[test] + fn active_backfill_change_roundtrip() { + // Capture-side AB: keys. A BackfillBegin records the binding's + // truncated_at clock; a later BackfillComplete deletes it. Bindings are + // keyed by stable state_key, so binding 1 resolves to "cap/c/t1". + let binding_state_keys = &["cap/c/t0", "cap/c/t1"]; + let mapping = state_key_index(&[("cap/c/t0", 0), ("cap/c/t1", 1)]); + + let begin = proto::Persist { + active_backfill_change: Some(proto::persist::ActiveBackfillChange::Begin( + proto::ActiveBackfillBegin { + binding: 1, + truncated_at: 0xABCD, + }, + )), + ..Default::default() + }; + let mut store: Vec<(Bytes, Bytes)> = Vec::new(); + encode_persist(&begin, binding_state_keys, |op| apply_op(&mut store, op)).unwrap(); + + let recovered = decode_pairs(store.clone(), &mapping).unwrap().recover; + assert_eq!( + recovered.active_backfills, + std::collections::BTreeMap::from([(1, 0xABCD)]), + "begin records binding 1's clock under its state_key", + ); + + // Completing binding 1 deletes the AB: key seeded above. + let complete = proto::Persist { + active_backfill_change: Some(proto::persist::ActiveBackfillChange::CompleteBinding(1)), + ..Default::default() + }; + encode_persist(&complete, binding_state_keys, |op| apply_op(&mut store, op)).unwrap(); + + let recovered = decode_pairs(store, &mapping).unwrap().recover; + assert!( + recovered.active_backfills.is_empty(), + "complete clears the binding's active backfill", + ); + } + + fn persist_frontier_begin() -> Vec { + vec![ + shuffle::proto::frontier::BackfillBegin { + journal_read_suffix: "mat%2Ft1.v1".into(), + clock: 111, + }, + shuffle::proto::frontier::BackfillBegin { + journal_read_suffix: "mat%2Ft2.v1".into(), + clock: 222, + }, + ] + } + + #[test] + fn fold_backfill_clocks_preserves_existing_journals() { + // The common case: a materialization with committed read offsets that has + // also observed a backfill. The clocks attach to the journal-bearing + // committed Frontier rather than replacing it. + let mut recover = proto::Recover { + committed_frontier: Some(frontier_fixture()), + ..Default::default() + }; + let journals = recover + .committed_frontier + .as_ref() + .unwrap() + .journals + .clone(); + + fold_backfill_clocks( + &mut recover, + std::collections::BTreeMap::from([("mat%2Ft1.v1".to_string(), 111u64)]), + std::collections::BTreeMap::from([("mat%2Ft1.v1".to_string(), 110u64)]), + ); + + let frontier = recover.committed_frontier.unwrap(); + assert_eq!(frontier.journals, journals, "journals are untouched"); + assert_eq!( + frontier.latest_backfill_begin, + vec![shuffle::proto::frontier::BackfillBegin { + journal_read_suffix: "mat%2Ft1.v1".into(), + clock: 111, + }], + ); + assert_eq!( + frontier.latest_backfill_complete, + vec![shuffle::proto::frontier::BackfillComplete { + journal_read_suffix: "mat%2Ft1.v1".into(), + clock: 110, + }], + ); + } + + #[test] + fn fold_backfill_clocks_without_clocks_is_noop() { + // No clocks must not materialize a committed Frontier: it stays `None`, + // matching the hinted-frontier "None when empty" invariant. + let mut recover = proto::Recover::default(); + fold_backfill_clocks( + &mut recover, + std::collections::BTreeMap::new(), + std::collections::BTreeMap::new(), + ); + assert!(recover.committed_frontier.is_none()); + } + #[test] fn delete_committed_frontier_clears_stale_keys() { // A prior session left a stale/partial committed Frontier in `FC:`. diff --git a/crates/runtime-next/src/shard/rocksdb.rs b/crates/runtime-next/src/shard/rocksdb.rs index ef129de9625..6d32a73a0fb 100644 --- a/crates/runtime-next/src/shard/rocksdb.rs +++ b/crates/runtime-next/src/shard/rocksdb.rs @@ -133,6 +133,8 @@ impl RocksDB { let mut recover = proto::Recover::default(); let mut committed_frontier: Vec = Vec::new(); let mut hinted_frontier: Vec = Vec::new(); + let mut committed_backfill_begin = std::collections::BTreeMap::new(); + let mut committed_backfill_complete = std::collections::BTreeMap::new(); let mut it = self.db.raw_iterator(); it.seek_to_first(); @@ -142,6 +144,8 @@ impl RocksDB { &mut recover, &mut committed_frontier, &mut hinted_frontier, + &mut committed_backfill_begin, + &mut committed_backfill_complete, key, value, &binding_state_keys, @@ -195,6 +199,14 @@ impl RocksDB { (!frontier.is_empty()).then(|| shuffle::JournalFrontier::encode(&frontier)); } + // Fold the persisted cumulative backfill clocks onto the + // recovered committed Frontier (keyed by journal_read_suffix). + recovery::fold_backfill_clocks( + &mut recover, + committed_backfill_begin, + committed_backfill_complete, + ); + Ok((self, recover)) }) .await diff --git a/crates/runtime-next/src/shard/snapshots/runtime_next__shard__recovery__test__decode_recover_classifies_ranges.snap b/crates/runtime-next/src/shard/snapshots/runtime_next__shard__recovery__test__decode_recover_classifies_ranges.snap index cca32411692..48d5c07c41f 100644 --- a/crates/runtime-next/src/shard/snapshots/runtime_next__shard__recovery__test__decode_recover_classifies_ranges.snap +++ b/crates/runtime-next/src/shard/snapshots/runtime_next__shard__recovery__test__decode_recover_classifies_ranges.snap @@ -1,6 +1,5 @@ --- source: crates/runtime-next/src/shard/recovery.rs -assertion_line: 947 expression: "decode_pairs(pairs, &mapping).unwrap()" --- DecodedRecover { @@ -26,6 +25,7 @@ DecodedRecover { 0: b"pk", }, trigger_params_json: b"{\"run_id\":\"r\"}", + active_backfills: {}, }, committed_frontier: [ JournalFrontier { diff --git a/crates/runtime-next/src/shard/snapshots/runtime_next__shard__recovery__test__encode_persist_hinted_then_committed_roundtrip.snap b/crates/runtime-next/src/shard/snapshots/runtime_next__shard__recovery__test__encode_persist_hinted_then_committed_roundtrip.snap index 04127c05bd1..8e709b339a4 100644 --- a/crates/runtime-next/src/shard/snapshots/runtime_next__shard__recovery__test__encode_persist_hinted_then_committed_roundtrip.snap +++ b/crates/runtime-next/src/shard/snapshots/runtime_next__shard__recovery__test__encode_persist_hinted_then_committed_roundtrip.snap @@ -1,6 +1,5 @@ --- source: crates/runtime-next/src/shard/recovery.rs -assertion_line: 797 expression: "decode_pairs(store, &mapping).unwrap()" --- DecodedRecover { @@ -20,6 +19,7 @@ DecodedRecover { 0: b"mk-v1", }, trigger_params_json: b"", + active_backfills: {}, }, committed_frontier: [ JournalFrontier { diff --git a/crates/runtime/src/combine/protocol.rs b/crates/runtime/src/combine/protocol.rs index dd90eee7039..c222982ec8a 100644 --- a/crates/runtime/src/combine/protocol.rs +++ b/crates/runtime/src/combine/protocol.rs @@ -81,7 +81,8 @@ pub fn recv_client_add( if let Some(uuid_ptr) = &binding.uuid_ptr { if let Some(uuid) = uuid_ptr.query(&doc) { - // Skip this document if its UUID is marked as an ACK. + // Skip this document if its UUID is marked as an ACK or an + // application control message. // TODO(johnny): Reconsider whether we need this after shuffle refactors. let skip = (|| { let HeapNode::String(uuid) = uuid else { @@ -91,7 +92,7 @@ pub fn recv_client_add( let (_producer, _clock, flags) = crate::uuid::parse(uuid)?; - Ok(flags.is_ack()) + Ok(flags.is_ack() || flags.is_control()) })() .with_context(|| { format!( diff --git a/crates/runtime/src/derive/protocol.rs b/crates/runtime/src/derive/protocol.rs index 81b13a51704..00f989a40a6 100644 --- a/crates/runtime/src/derive/protocol.rs +++ b/crates/runtime/src/derive/protocol.rs @@ -136,8 +136,11 @@ pub fn recv_client_read_or_flush( } if let Some(flow::UuidParts { clock, node }) = &read.uuid { - // Filter out message acknowledgements. - if proto_gazette::message_flags::ACK_TXN & node != 0 { + // Filter out transaction acknowledgements and application control + // messages. + if proto_gazette::message_flags::ACK_TXN & node != 0 + || proto_gazette::message_flags::CONTROL & node != 0 + { return Ok(None); } // Track the largest document clock that we've observed. diff --git a/crates/runtime/src/harness/materialize.rs b/crates/runtime/src/harness/materialize.rs index bf7a6f6b3e7..924dff6883d 100644 --- a/crates/runtime/src/harness/materialize.rs +++ b/crates/runtime/src/harness/materialize.rs @@ -237,6 +237,7 @@ async fn run_session( let flush = Request { flush: Some(request::Flush { state_patches_json: bytes::Bytes::new(), // Not implemented. + ..Default::default() }), ..Default::default() }; diff --git a/crates/shuffle/Cargo.toml b/crates/shuffle/Cargo.toml index 8e595ce1051..7d357244827 100644 --- a/crates/shuffle/Cargo.toml +++ b/crates/shuffle/Cargo.toml @@ -31,6 +31,7 @@ tuple = { path = "../tuple" } anyhow = { workspace = true } bytes = { workspace = true } +chrono = { workspace = true } futures = { workspace = true } highway = { workspace = true } libc = { workspace = true } diff --git a/crates/shuffle/src/frontier.rs b/crates/shuffle/src/frontier.rs index 8b92811d3e1..b61606fdd11 100644 --- a/crates/shuffle/src/frontier.rs +++ b/crates/shuffle/src/frontier.rs @@ -1,6 +1,7 @@ use crate::log; use proto_flow::shuffle; use proto_gazette::uuid::{Clock, Producer}; +use std::collections::BTreeMap; /// Frontier state of a single producer within a journal. #[derive(Debug, Clone)] @@ -230,6 +231,7 @@ impl JournalFrontier { shuffle::Frontier { journals, flushed_lsn: vec![], + ..Default::default() } } } @@ -252,6 +254,13 @@ pub struct Frontier { /// Per-shard flushed LSN (log read-through barrier), indexed by shard_index. /// Empty when not applicable (e.g. resume checkpoints). pub flushed_lsn: Vec, + /// Latest committed backfill-begin clock of the checkpoint delta, keyed by + /// `journal_read_suffix`. Folded from immediately-committed CONTROL + /// documents; does not participate in causal-hint sequencing. + pub latest_backfill_begin: BTreeMap, + /// Latest committed backfill-complete clock of the checkpoint delta, keyed + /// by `journal_read_suffix`. See `latest_backfill_begin`. + pub latest_backfill_complete: BTreeMap, /// Count of `ProducerFrontier` entries with `hinted_commit > last_commit`. /// A Frontier with a non-zero count is "partial": readable for processing /// (e.g. log scanning), but NOT a transactional boundary. @@ -359,10 +368,24 @@ impl Frontier { Ok(Self { journals, flushed_lsn, + latest_backfill_begin: BTreeMap::new(), + latest_backfill_complete: BTreeMap::new(), unresolved_hints, }) } + fn merge_backfill_clocks( + mut a: BTreeMap, + b: BTreeMap, + ) -> BTreeMap { + for (suffix, clock) in b { + a.entry(suffix) + .and_modify(|current: &mut Clock| *current = (*current).max(clock)) + .or_insert(clock); + } + a + } + /// Element-wise max of two per-shard `flushed_lsn` vectors. /// Extends the shorter vector with zeros. pub fn merge_flushed_lsn(a: Vec, b: Vec) -> Vec { @@ -389,15 +412,25 @@ impl Frontier { /// Both inputs may contain non-unique keys, which are reduced to single entries. pub fn reduce(self, other: Self) -> Self { let flushed_lsn = Self::merge_flushed_lsn(self.flushed_lsn, other.flushed_lsn); + let latest_backfill_begin = + Self::merge_backfill_clocks(self.latest_backfill_begin, other.latest_backfill_begin); + let latest_backfill_complete = Self::merge_backfill_clocks( + self.latest_backfill_complete, + other.latest_backfill_complete, + ); if self.journals.is_empty() { return Self { flushed_lsn, + latest_backfill_begin, + latest_backfill_complete, ..other }; } else if other.journals.is_empty() { return Self { flushed_lsn, + latest_backfill_begin, + latest_backfill_complete, ..self }; } @@ -440,6 +473,8 @@ impl Frontier { Self { journals: merged, flushed_lsn, + latest_backfill_begin, + latest_backfill_complete, unresolved_hints, } } @@ -511,14 +546,42 @@ impl Frontier { pub fn encode(&self) -> shuffle::Frontier { let mut proto = JournalFrontier::encode(&self.journals); proto.flushed_lsn = self.flushed_lsn.iter().map(|lsn| lsn.as_u64()).collect(); + proto.latest_backfill_begin = self + .latest_backfill_begin + .iter() + .map(|(suffix, clock)| shuffle::frontier::BackfillBegin { + journal_read_suffix: suffix.clone(), + clock: clock.as_u64(), + }) + .collect(); + proto.latest_backfill_complete = self + .latest_backfill_complete + .iter() + .map(|(suffix, clock)| shuffle::frontier::BackfillComplete { + journal_read_suffix: suffix.clone(), + clock: clock.as_u64(), + }) + .collect(); proto } /// Decode a proto `shuffle::Frontier` into a validated `Frontier`. pub fn decode(mut proto: shuffle::Frontier) -> Result { let flushed_lsn = std::mem::take(&mut proto.flushed_lsn); + let latest_backfill_begin = std::mem::take(&mut proto.latest_backfill_begin) + .into_iter() + .map(|e| (e.journal_read_suffix, Clock::from_u64(e.clock))) + .collect(); + let latest_backfill_complete = std::mem::take(&mut proto.latest_backfill_complete) + .into_iter() + .map(|e| (e.journal_read_suffix, Clock::from_u64(e.clock))) + .collect(); + let journals: Vec = JournalFrontier::decode(proto).collect(); - Self::new(journals, flushed_lsn) + let mut frontier = Self::new(journals, flushed_lsn)?; + frontier.latest_backfill_begin = latest_backfill_begin; + frontier.latest_backfill_complete = latest_backfill_complete; + Ok(frontier) } /// Extract producers with unresolved causal hints (`hinted_commit > last_commit`) @@ -554,6 +617,8 @@ impl Frontier { Frontier { journals, flushed_lsn: vec![], + latest_backfill_begin: BTreeMap::new(), + latest_backfill_complete: BTreeMap::new(), unresolved_hints, } } @@ -594,6 +659,7 @@ mod test { use super::*; use crate::testing::{jf, jf_with_bytes, pf, pf_tuple}; use log::Lsn; + use std::collections::BTreeMap; #[test] fn test_producer_frontier_reduce() { @@ -635,6 +701,8 @@ mod test { ), ], flushed_lsn: vec![Lsn::from_u64(10), Lsn::from_u64(50), Lsn::from_u64(3)], + latest_backfill_begin: BTreeMap::from([("suffix/0".into(), Clock::from_u64(100))]), + latest_backfill_complete: BTreeMap::from([("suffix/1".into(), Clock::from_u64(140))]), unresolved_hints: 0, }; let hints = Frontier { @@ -643,6 +711,8 @@ mod test { jf("journal/C", 1, vec![pf(0x03, 0, 300, 0)]), ], flushed_lsn: vec![Lsn::from_u64(40), Lsn::from_u64(20), Lsn::from_u64(30)], + latest_backfill_begin: BTreeMap::from([("suffix/0".into(), Clock::from_u64(120))]), + latest_backfill_complete: BTreeMap::from([("suffix/0".into(), Clock::from_u64(130))]), unresolved_hints: 2, }; let r = reads.reduce(hints); @@ -708,19 +778,44 @@ mod test { vec![Lsn::from_u64(40), Lsn::from_u64(50), Lsn::from_u64(30)], "element-wise max of flushed_lsn" ); + // Per-suffix max of backfill clocks across both inputs. + assert_eq!( + r.latest_backfill_begin.get("suffix/0"), + Some(&Clock::from_u64(120)) + ); + assert_eq!( + r.latest_backfill_complete.get("suffix/0"), + Some(&Clock::from_u64(130)) + ); + assert_eq!( + r.latest_backfill_complete.get("suffix/1"), + Some(&Clock::from_u64(140)) + ); - // Identity: empty reduces are no-ops and preserve flushed_lsn. + // Identity: reducing with an empty frontier preserves all fields. let f = Frontier { journals: vec![jf("j", 0, vec![pf(0x01, 1, 0, -1)])], flushed_lsn: vec![Lsn::from_u64(10), Lsn::from_u64(20)], + latest_backfill_begin: BTreeMap::from([("suffix/0".into(), Clock::from_u64(100))]), + latest_backfill_complete: BTreeMap::from([("suffix/0".into(), Clock::from_u64(200))]), unresolved_hints: 0, }; let r = f.clone().reduce(Frontier::default()); assert_eq!(r.journals.len(), 1); assert_eq!(r.flushed_lsn, vec![Lsn::from_u64(10), Lsn::from_u64(20)]); + assert_eq!(r.latest_backfill_begin, f.latest_backfill_begin); + assert_eq!(r.latest_backfill_complete, f.latest_backfill_complete); let r = Frontier::default().reduce(f); assert_eq!(r.journals.len(), 1); assert_eq!(r.flushed_lsn, vec![Lsn::from_u64(10), Lsn::from_u64(20)]); + assert_eq!( + r.latest_backfill_begin.get("suffix/0"), + Some(&Clock::from_u64(100)) + ); + assert_eq!( + r.latest_backfill_complete.get("suffix/0"), + Some(&Clock::from_u64(200)) + ); assert!( Frontier::default() .reduce(Frontier::default()) @@ -916,6 +1011,8 @@ mod test { ), ], flushed_lsn: vec![], + latest_backfill_begin: BTreeMap::new(), + latest_backfill_complete: BTreeMap::new(), unresolved_hints: 2, }; @@ -927,6 +1024,8 @@ mod test { jf("journal/B", 0, vec![pf(0x03, 250, 0, -600)]), ], flushed_lsn: vec![], + latest_backfill_begin: BTreeMap::new(), + latest_backfill_complete: BTreeMap::new(), unresolved_hints: 0, }; @@ -957,6 +1056,8 @@ mod test { let progressed2 = Frontier { journals: vec![jf("journal/B", 0, vec![pf(0x03, 400, 0, -900)])], flushed_lsn: vec![], + latest_backfill_begin: BTreeMap::new(), + latest_backfill_complete: BTreeMap::new(), unresolved_hints: 0, }; let (advanced2, resolved2) = pending.resolve_hints(&progressed2); @@ -980,11 +1081,15 @@ mod test { let mut pending = Frontier { journals: vec![jf("journal/X", 1, vec![pf(0x01, 0, 100, 0)])], flushed_lsn: vec![], + latest_backfill_begin: BTreeMap::new(), + latest_backfill_complete: BTreeMap::new(), unresolved_hints: 1, }; let progressed = Frontier { journals: vec![jf("journal/X", 0, vec![pf(0x01, 200, 0, -500)])], flushed_lsn: vec![], + latest_backfill_begin: BTreeMap::new(), + latest_backfill_complete: BTreeMap::new(), unresolved_hints: 0, }; assert_eq!(pending.resolve_hints(&progressed), (0, 0)); @@ -1034,6 +1139,8 @@ mod test { jf("journal/C", 1, vec![pf(0x07, 0, 300, 0)]), // unresolved ], flushed_lsn: vec![], + latest_backfill_begin: BTreeMap::new(), + latest_backfill_complete: BTreeMap::new(), unresolved_hints: 2, }; @@ -1075,6 +1182,8 @@ mod test { let no_hints = Frontier { journals: vec![jf("journal/A", 0, vec![pf(0x01, 100, 0, -200)])], flushed_lsn: vec![], + latest_backfill_begin: BTreeMap::new(), + latest_backfill_complete: BTreeMap::new(), unresolved_hints: 0, }; assert!(no_hints.project_unresolved_hints().journals.is_empty()); diff --git a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_hint_resolved.snap b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_hint_resolved.snap index b616281392a..e8b8d69608d 100644 --- a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_hint_resolved.snap +++ b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_hint_resolved.snap @@ -24,11 +24,15 @@ PipelineSnapshot { flushed_lsn: [ 0/1000, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved_count: 0, @@ -66,6 +70,8 @@ PipelineSnapshot { flushed_lsn: [ 0/1000, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, } diff --git a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_normal_resumes.snap b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_normal_resumes.snap index 94c82c2d0d9..b8c6c901f03 100644 --- a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_normal_resumes.snap +++ b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_normal_resumes.snap @@ -66,17 +66,23 @@ PipelineSnapshot { flushed_lsn: [ 0/3000, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved_count: 0, progressed: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, } diff --git a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_second_progressed.snap b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_second_progressed.snap index 1e31e21427b..f68eca95878 100644 --- a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_second_progressed.snap +++ b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_second_progressed.snap @@ -24,11 +24,15 @@ PipelineSnapshot { flushed_lsn: [ 0/1000, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved_count: 0, @@ -80,6 +84,8 @@ PipelineSnapshot { flushed_lsn: [ 0/2000, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, } diff --git a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_take.snap b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_take.snap index 7d1c940fc28..d23da601dbe 100644 --- a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_take.snap +++ b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__after_take.snap @@ -52,17 +52,23 @@ PipelineSnapshot { flushed_lsn: [ 0/2000, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved_count: 0, progressed: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, } diff --git a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__checkpoint_progression.snap b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__checkpoint_progression.snap index c52296646c6..9f8742cf87c 100644 --- a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__checkpoint_progression.snap +++ b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__checkpoint_progression.snap @@ -34,5 +34,7 @@ Frontier { }, ], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, } diff --git a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__cross_cohort_hint_preserved.snap b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__cross_cohort_hint_preserved.snap index 8170eb5ef92..2f669dbd77d 100644 --- a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__cross_cohort_hint_preserved.snap +++ b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__cross_cohort_hint_preserved.snap @@ -7,6 +7,8 @@ PipelineSnapshot { ready: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved: Frontier { @@ -27,12 +29,16 @@ PipelineSnapshot { }, ], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 1, }, unresolved_count: 1, progressed: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, } diff --git a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__frontier_hint_not_filtered.snap b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__frontier_hint_not_filtered.snap index 5a7be161947..14b2944f47e 100644 --- a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__frontier_hint_not_filtered.snap +++ b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__frontier_hint_not_filtered.snap @@ -42,6 +42,8 @@ PipelineSnapshot { }, ], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved: Frontier { @@ -62,12 +64,16 @@ PipelineSnapshot { }, ], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 1, }, unresolved_count: 1, progressed: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, } diff --git a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__no_recovery.snap b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__no_recovery.snap index 8f6e1c8d530..75b102871f7 100644 --- a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__no_recovery.snap +++ b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__no_recovery.snap @@ -22,17 +22,23 @@ PipelineSnapshot { }, ], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved_count: 0, progressed: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, } diff --git a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__recovery_hint_survives_completed_clocks.snap b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__recovery_hint_survives_completed_clocks.snap index 96dbd7b184c..e4e48f11e9b 100644 --- a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__recovery_hint_survives_completed_clocks.snap +++ b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__recovery_hint_survives_completed_clocks.snap @@ -7,6 +7,8 @@ PipelineSnapshot { ready: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved: Frontier { @@ -27,12 +29,16 @@ PipelineSnapshot { }, ], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 1, }, unresolved_count: 1, progressed: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, } diff --git a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__stale_hint_filtered.snap b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__stale_hint_filtered.snap index 9af81710fbc..67495e40c92 100644 --- a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__stale_hint_filtered.snap +++ b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__stale_hint_filtered.snap @@ -42,17 +42,23 @@ PipelineSnapshot { }, ], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, unresolved_count: 0, progressed: Frontier { journals: [], flushed_lsn: [], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, } diff --git a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__taken_recovery.snap b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__taken_recovery.snap index f70e7423843..bbb16481bad 100644 --- a/crates/shuffle/src/session/snapshots/shuffle__session__state__test__taken_recovery.snap +++ b/crates/shuffle/src/session/snapshots/shuffle__session__state__test__taken_recovery.snap @@ -22,5 +22,7 @@ Frontier { flushed_lsn: [ 0/1000, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, } diff --git a/crates/shuffle/src/session/state.rs b/crates/shuffle/src/session/state.rs index 98186d0f1f3..ab1affa3a13 100644 --- a/crates/shuffle/src/session/state.rs +++ b/crates/shuffle/src/session/state.rs @@ -368,6 +368,10 @@ impl CheckpointPipeline { journals: peek_journals, flushed_lsn: self.unresolved.flushed_lsn.clone(), unresolved_hints: self.unresolved.unresolved_hints, + // Backfill metadata is checkpoint-delta state, not part of an + // unresolved peek used only for partial-progress log scanning. + latest_backfill_begin: Default::default(), + latest_backfill_complete: Default::default(), }; self.floor_flushed_lsn(&mut peek); @@ -1236,6 +1240,8 @@ mod test { journals: vec![jf("journal/A", 0, vec![pf(0x01, 10, 100, -50)])], flushed_lsn: vec![], unresolved_hints: 1, + latest_backfill_begin: Default::default(), + latest_backfill_complete: Default::default(), }, vec![0], ); @@ -1357,6 +1363,8 @@ mod test { journals: vec![jf("journal/A", 0, vec![pf(0x01, 50, 200, -100)])], flushed_lsn: vec![], unresolved_hints: 0, + latest_backfill_begin: Default::default(), + latest_backfill_complete: Default::default(), }, vec![0], ); @@ -1561,6 +1569,8 @@ mod test { journals: vec![jf("test/journal/F", 0, vec![pf(0x01, 100, 200, -500)])], flushed_lsn: vec![], unresolved_hints: 0, + latest_backfill_begin: Default::default(), + latest_backfill_complete: Default::default(), }; // Checkpoint found in resume_checkpoint. @@ -1853,6 +1863,8 @@ mod test { ], flushed_lsn: vec![], unresolved_hints: 0, + latest_backfill_begin: Default::default(), + latest_backfill_complete: Default::default(), }; let mut pipeline = CheckpointPipeline::new(&resume, vec![0, 0]); diff --git a/crates/shuffle/src/slice/actor.rs b/crates/shuffle/src/slice/actor.rs index 3c49981368a..e97970ba02d 100644 --- a/crates/shuffle/src/slice/actor.rs +++ b/crates/shuffle/src/slice/actor.rs @@ -303,6 +303,17 @@ impl SliceActor { let binding_state_key = binding.state_key().to_string(); let client = (*self.topology.journal_clients[binding.index as usize]).clone(); let spec = spec.context("StartRead missing spec")?; + + let truncated_at = match spec + .labels + .as_ref() + .map_or(Ok(""), |set| labels::maybe_one(set, labels::TRUNCATED_AT))? + { + "" => uuid::Clock::default(), + value => uuid::Clock::from_u64(labels::parse_truncated_at(value)?), + }; + let effective_not_before = binding.not_before.max(truncated_at); + let journal = spec.name.into_boxed_str(); let read_id = self.reads.len() as u32; @@ -314,7 +325,7 @@ impl SliceActor { // This helps identify the sources of reads from the perspective of a gazette broker. journal: format!("{journal};{}", binding.journal_read_suffix), - begin_mod_time: binding.not_before.to_unix().0 as i64, + begin_mod_time: effective_not_before.to_unix().0 as i64, block: true, do_not_proxy: true, end_offset: 0, // No end offset. @@ -346,6 +357,7 @@ impl SliceActor { self.reads.push(ReadState::recovered( binding_index as u16, journal, + truncated_at, producers, )); @@ -748,7 +760,7 @@ impl SliceActor { read_state.read_offset = end_offset; if sequenced.is_commit { - if flags == uuid::Flags::ACK_TXN { + if flags.is_ack() { // This ACK is (binding, journal)-scoped: it commits only // this producer's documents in this binding's read of this journal. // But, it may contain causal hints of *other* journals which @@ -769,7 +781,12 @@ impl SliceActor { self.flush.set_ready(); } - // Step producer state forward to reflect the append. + // Fold any committed backfill control clock into this journal's + // per-flush backfill state, then step producer state forward. + read_state.backfill_begin = read_state.backfill_begin.max(sequenced.backfill_begin); + read_state.backfill_complete = read_state + .backfill_complete + .max(sequenced.backfill_complete); _ = read_state .pending .insert(producer, sequenced.producer_state); @@ -826,6 +843,7 @@ impl SliceActor { // draining pending→settled and resetting byte accumulators. let frontier = super::producer::build_flush_frontier( &mut self.reads, + &self.topology.bindings, self.causal_hints.drain(), self.topology.shards.len(), ); diff --git a/crates/shuffle/src/slice/producer.rs b/crates/shuffle/src/slice/producer.rs index 4cdc0c63229..270a046a28e 100644 --- a/crates/shuffle/src/slice/producer.rs +++ b/crates/shuffle/src/slice/producer.rs @@ -1,4 +1,5 @@ use proto_gazette::uuid::{Clock, Producer}; +use std::collections::BTreeMap; /// Per-producer sequencing state. /// @@ -43,17 +44,21 @@ const _: () = assert!(std::mem::size_of::() == 24); /// causal hints. /// /// `reads` provides the journal name, binding index, and pending producers -/// for each active read. `hints` yields owned `((journal, binding), +/// for each active read. `bindings` maps binding indices to `journal_read_suffix`, +/// used to key backfill metadata. `hints` yields owned `((journal, binding), /// Vec<(producer, hinted_clock)>)` entries, typically from a HashMap drain. /// /// Both inputs may arrive in arbitrary order; outputs are sorted. pub fn build_flush_frontier( reads: &mut [super::read::ReadState], + bindings: &[crate::Binding], hints: impl Iterator, u16), Vec<(Producer, Clock)>)>, shard_count: usize, ) -> crate::Frontier { // Walk all journal reads to build their JournalFrontier. let mut journals: Vec = Vec::new(); + let mut latest_backfill_begin = BTreeMap::::new(); + let mut latest_backfill_complete = BTreeMap::::new(); for read_state in reads.iter_mut() { if read_state.pending.is_empty() { @@ -64,16 +69,35 @@ pub fn build_flush_frontier( // even if offsets advanced meanwhile. continue; } - let mut producers: Vec<_> = read_state - .pending - .iter() - .map(|(producer, ps)| crate::ProducerFrontier { - producer: *producer, + + // Backfill clocks are per-journal metadata, keyed by + // `journal_read_suffix`. Drain this journal's clocks into the + // checkpoint maps; a non-zero clock implies the read has pending work. + let suffix = &bindings[read_state.binding_index as usize].journal_read_suffix; + let backfill_begin = std::mem::take(&mut read_state.backfill_begin); + if backfill_begin != Clock::zero() { + latest_backfill_begin + .entry(suffix.clone()) + .and_modify(|c| *c = (*c).max(backfill_begin)) + .or_insert(backfill_begin); + } + let backfill_complete = std::mem::take(&mut read_state.backfill_complete); + if backfill_complete != Clock::zero() { + latest_backfill_complete + .entry(suffix.clone()) + .and_modify(|c| *c = (*c).max(backfill_complete)) + .or_insert(backfill_complete); + } + + let mut producers = Vec::with_capacity(read_state.pending.len()); + for (&producer, ps) in read_state.pending.iter() { + producers.push(crate::ProducerFrontier { + producer, last_commit: ps.last_commit, hinted_commit: Clock::zero(), offset: ps.offset, - }) - .collect(); + }); + } producers.sort_by(|a, b| a.producer.cmp(&b.producer)); let bytes_read_delta = read_state.read_offset - read_state.prev_read_offset; @@ -100,6 +124,8 @@ pub fn build_flush_frontier( unresolved_hints: 0, // By construction: only `last_commit` set. journals, flushed_lsn: vec![crate::log::Lsn::ZERO; shard_count], + latest_backfill_begin, + latest_backfill_complete, }; // Build a Frontier from causal hints via single-pass iteration. @@ -143,6 +169,8 @@ pub fn build_flush_frontier( unresolved_hints, journals: hint_journals, flushed_lsn: vec![], + latest_backfill_begin: Default::default(), + latest_backfill_complete: Default::default(), }) } @@ -186,6 +214,9 @@ mod test { super::super::read::ReadState { binding_index: binding, journal: journal.into(), + truncated_at: Clock::zero(), + backfill_begin: Clock::zero(), + backfill_complete: Clock::zero(), settled: ProducerMap::default(), pending: map, read_offset, @@ -326,14 +357,55 @@ mod test { ), ]; + let bindings = vec![ + crate::testing::test_binding(0, true, None, "suffix/0"), + crate::testing::test_binding(1, true, None, "suffix/1"), + crate::testing::test_binding(2, true, None, "suffix/2"), + ]; + let snap = cases .into_iter() .map(|(name, mut reads, hints)| { - let f = build_flush_frontier(&mut reads, hints.into_iter(), 3); + let f = build_flush_frontier(&mut reads, &bindings, hints.into_iter(), 3); (name, f, reads) }) .collect::>(); insta::assert_debug_snapshot!(snap); } + + #[test] + fn test_build_flush_frontier_drains_backfill_clocks_from_reads() { + // Two journals of one binding (shared `suffix/0`). The drain folds both + // journals' clocks per-suffix via `max` and clears each read's fields. + // journal/A is processed first with the smaller begin; journal/B carries + // the larger begin, so the fold must `max`-upgrade to it rather than keep + // the first-seen value. Only journal/A carries a complete. + let mut has_both = read_state("journal/A", 0, &[(0x01, 100, -500)]); + has_both.backfill_begin = Clock::from_u64(80); + has_both.backfill_complete = Clock::from_u64(100); + + let mut begin_only = read_state("journal/B", 0, &[(0x03, 0, 700)]); + begin_only.backfill_begin = Clock::from_u64(90); + + let bindings = vec![crate::testing::test_binding(0, true, None, "suffix/0")]; + let mut reads = vec![has_both, begin_only]; + + let frontier = build_flush_frontier(&mut reads, &bindings, std::iter::empty(), 2); + + // Per-suffix max across both journals. + assert_eq!( + frontier.latest_backfill_begin.get("suffix/0"), + Some(&Clock::from_u64(90)) + ); + assert_eq!( + frontier.latest_backfill_complete.get("suffix/0"), + Some(&Clock::from_u64(100)) + ); + + // The per-journal clocks are drained (reset to zero). + assert_eq!(reads[0].backfill_begin, Clock::zero()); + assert_eq!(reads[0].backfill_complete, Clock::zero()); + assert_eq!(reads[1].backfill_begin, Clock::zero()); + } } diff --git a/crates/shuffle/src/slice/read.rs b/crates/shuffle/src/slice/read.rs index 81107746e30..dc0299ac992 100644 --- a/crates/shuffle/src/slice/read.rs +++ b/crates/shuffle/src/slice/read.rs @@ -2,6 +2,20 @@ use super::producer::ProducerState; use crate::ProducerMap; use proto_gazette::{broker, uuid}; +/// A control event parsed from a CONTROL document body. +/// +/// CONTROL documents carry backfill metadata that is folded into the checkpoint +/// frontier rather than appended to shuffle log segments. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ControlEvent { + BackfillBegin, + /// Carries the `truncated_at` (begin clock) the complete reports, parsed from + /// its `_meta.truncatedAt` RFC3339 body field. + BackfillComplete { + truncated_at: uuid::Clock, + }, +} + /// State about an active read, indexed by its `read.id()`. /// /// Each ReadState represents one (journal, binding) pair and is the @@ -14,6 +28,16 @@ pub struct ReadState { pub binding_index: u16, /// The journal name (canonical, without the `;suffix` read metadata). pub journal: Box, + /// Truncation boundary for this journal: the `estuary.dev/truncated-at` + /// label clock, or zero when absent. Documents published before it were + /// superseded by a backfill, so `sequence_document` suppresses their append. + pub truncated_at: uuid::Clock, + /// Latest `BackfillBegin` control-doc clock for this journal, awaiting the + /// next flush (zero = none). + pub backfill_begin: uuid::Clock, + /// Latest `BackfillComplete` control-doc clock for this journal, awaiting + /// the next flush (zero = none). + pub backfill_complete: uuid::Clock, /// Producers whose state is settled: either from the initial checkpoint /// or drained from `pending` at the start of a flush cycle. pub settled: ProducerMap, @@ -36,11 +60,15 @@ impl ReadState { pub fn recovered( binding_index: u16, journal: Box, + truncated_at: uuid::Clock, settled: ProducerMap, ) -> Self { Self { binding_index, journal, + truncated_at, + backfill_begin: uuid::Clock::zero(), + backfill_complete: uuid::Clock::zero(), settled, pending: Default::default(), read_offset: 0, @@ -84,6 +112,8 @@ pub struct Meta { pub flags: uuid::Flags, /// Publication Producer of `doc` (extracted from its UUID). pub producer: uuid::Producer, + /// Parsed control event, when this document has the CONTROL flag set. + pub control: Option, } /// ReadyRead is a ReadLines which has one or more parsed documents. @@ -127,7 +157,38 @@ pub fn extract_metas( ) })?; - let flags = if flags != uuid::Flags::ACK_TXN && validator.is_valid(archived) { + let has = |path: &str| json::Pointer::from_str(path).query(archived).is_some(); + let control = if !flags.is_control() { + None + } else if has("/_meta/backfillBegin") { + Some(ControlEvent::BackfillBegin) + } else if has("/_meta/backfillComplete") { + // The complete reports the boundary it completed as an RFC3339 + // `_meta.truncatedAt`; recover the begin clock from it so we don't + // depend on having read the matching BackfillBegin. + let truncated_at = json::Pointer::from_str("/_meta/truncatedAt") + .query(archived) + .and_then(|node| match node { + doc::ArchivedNode::String(s) => chrono::DateTime::parse_from_rfc3339(s).ok(), + _ => None, + }) + .map(|dt| { + uuid::Clock::from_unix(dt.timestamp() as u64, dt.timestamp_subsec_nanos()) + }) + .ok_or_else(|| { + anyhow::anyhow!( + "journal {journal} offset {begin_offset}: \ + BackfillComplete is missing a valid /_meta/truncatedAt" + ) + })?; + Some(ControlEvent::BackfillComplete { truncated_at }) + } else { + anyhow::bail!( + "journal {journal} offset {begin_offset}: control document not recognized" + ); + }; + + let flags = if !flags.is_ack() && control.is_none() && validator.is_valid(archived) { uuid::Flags(flags.0 | crate::FLAGS_SCHEMA_VALID) } else { flags @@ -139,6 +200,7 @@ pub fn extract_metas( clock, flags, producer, + control, }; begin_offset = end_offset; @@ -279,7 +341,8 @@ mod test { #[test] fn test_extract_metas() { - // Schema requires "required_field", exercising both valid and invalid paths. + // Schema requires "required_field", exercising valid, invalid, ACK bypass, + // and control doc paths. let schema = br#"{"type":"object","required":["required_field"]}"#; let bundle = doc::validation::build_bundle(schema).unwrap(); let mut validator = doc::Validator::new(bundle).unwrap(); @@ -289,22 +352,40 @@ mod test { let c1 = clock.tick(); let c2 = clock.tick(); let c3 = clock.tick(); + let c4 = clock.tick(); + let c5 = clock.tick(); - // Three docs exercise: non-zero base offset, offset chaining, - // OUTSIDE_TXN/CONTINUE_TXN/ACK_TXN flags, valid + invalid schema, ACK bypass. let json = [ + // Valid schema, OUTSIDE_TXN. format!( r#"{{"_meta":{{"uuid":"{}"}},"required_field":"present"}}"#, make_uuid_str(p1, c1, uuid::Flags::OUTSIDE_TXN), ), + // Invalid schema, CONTINUE_TXN. format!( r#"{{"_meta":{{"uuid":"{}"}},"other":"value"}}"#, make_uuid_str(p1, c2, uuid::Flags::CONTINUE_TXN), ), + // ACK_TXN (skips validation). format!( r#"{{"_meta":{{"uuid":"{}"}}}}"#, make_uuid_str(p1, c3, uuid::Flags::ACK_TXN), ), + // Control: backfillBegin (skips validation, parsed as a control event). + // CONTROL docs always carry `Flag_CONTROL` alone (0x4); the low + // transaction-semantics bits are implicitly OUTSIDE_TXN, so these + // documents are immediately committed and never participate in a + // CONTINUE_TXN / ACK_TXN span. + format!( + r#"{{"_meta":{{"uuid":"{}","backfillBegin":true}}}}"#, + make_uuid_str(p1, c4, uuid::Flags::CONTROL), + ), + // Control: backfillComplete, carrying the begin boundary it completed + // as an RFC3339 `truncatedAt`. + format!( + r#"{{"_meta":{{"uuid":"{}","backfillComplete":true,"truncatedAt":"2023-11-14T22:13:20Z"}}}}"#, + make_uuid_str(p1, c5, uuid::Flags::CONTROL), + ), ] .join("\n") + "\n"; diff --git a/crates/shuffle/src/slice/snapshots/shuffle__slice__producer__test__build_flush_frontier.snap b/crates/shuffle/src/slice/snapshots/shuffle__slice__producer__test__build_flush_frontier.snap index c2f51801926..cc27996210c 100644 --- a/crates/shuffle/src/slice/snapshots/shuffle__slice__producer__test__build_flush_frontier.snap +++ b/crates/shuffle/src/slice/snapshots/shuffle__slice__producer__test__build_flush_frontier.snap @@ -12,6 +12,8 @@ expression: snap 0/0, 0/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, [], @@ -54,12 +56,17 @@ expression: snap 0/0, 0/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, [ ReadState { binding_index: 0, journal: "journal/B", + truncated_at: Clock(0s 0ns), + backfill_begin: Clock(0s 0ns), + backfill_complete: Clock(0s 0ns), settled: { Producer(03:00:00:00:00:00): ProducerState { last_commit: Clock(0s 32ns), @@ -76,6 +83,9 @@ expression: snap ReadState { binding_index: 0, journal: "journal/A", + truncated_at: Clock(0s 0ns), + backfill_begin: Clock(0s 0ns), + backfill_complete: Clock(0s 0ns), settled: { Producer(01:00:00:00:00:00): ProducerState { last_commit: Clock(0s 16ns), @@ -92,6 +102,9 @@ expression: snap ReadState { binding_index: 0, journal: "journal/C", + truncated_at: Clock(0s 0ns), + backfill_begin: Clock(0s 0ns), + backfill_complete: Clock(0s 0ns), settled: {}, pending: {}, read_offset: 123, @@ -139,6 +152,8 @@ expression: snap 0/0, 0/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 2, }, [], @@ -167,12 +182,17 @@ expression: snap 0/0, 0/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, [ ReadState { binding_index: 0, journal: "journal/A", + truncated_at: Clock(0s 0ns), + backfill_begin: Clock(0s 0ns), + backfill_complete: Clock(0s 0ns), settled: { Producer(01:00:00:00:00:00): ProducerState { last_commit: Clock(0s 16ns), @@ -189,6 +209,9 @@ expression: snap ReadState { binding_index: 0, journal: "journal/B", + truncated_at: Clock(0s 0ns), + backfill_begin: Clock(0s 0ns), + backfill_complete: Clock(0s 0ns), settled: {}, pending: {}, read_offset: 0, @@ -256,12 +279,17 @@ expression: snap 0/0, 0/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 2, }, [ ReadState { binding_index: 0, journal: "journal/A", + truncated_at: Clock(0s 0ns), + backfill_begin: Clock(0s 0ns), + backfill_complete: Clock(0s 0ns), settled: { Producer(01:00:00:00:00:00): ProducerState { last_commit: Clock(0s 16ns), @@ -278,6 +306,9 @@ expression: snap ReadState { binding_index: 0, journal: "journal/B", + truncated_at: Clock(0s 0ns), + backfill_begin: Clock(0s 0ns), + backfill_complete: Clock(0s 0ns), settled: { Producer(03:00:00:00:00:00): ProducerState { last_commit: Clock(0s 32ns), @@ -350,12 +381,17 @@ expression: snap 0/0, 0/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 1, }, [ ReadState { binding_index: 2, journal: "journal/X", + truncated_at: Clock(0s 0ns), + backfill_begin: Clock(0s 0ns), + backfill_complete: Clock(0s 0ns), settled: { Producer(01:00:00:00:00:00): ProducerState { last_commit: Clock(0s 16ns), @@ -372,6 +408,9 @@ expression: snap ReadState { binding_index: 0, journal: "journal/X", + truncated_at: Clock(0s 0ns), + backfill_begin: Clock(0s 0ns), + backfill_complete: Clock(0s 0ns), settled: { Producer(03:00:00:00:00:00): ProducerState { last_commit: Clock(0s 8ns), @@ -411,6 +450,8 @@ expression: snap 0/0, 0/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 1, }, [], @@ -439,12 +480,17 @@ expression: snap 0/0, 0/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 1, }, [ ReadState { binding_index: 0, journal: "journal/A", + truncated_at: Clock(0s 0ns), + backfill_begin: Clock(0s 0ns), + backfill_complete: Clock(0s 0ns), settled: { Producer(01:00:00:00:00:00): ProducerState { last_commit: Clock(0s 8ns), diff --git a/crates/shuffle/src/slice/snapshots/shuffle__slice__read__test__extract_metas.snap b/crates/shuffle/src/slice/snapshots/shuffle__slice__read__test__extract_metas.snap index 3a0c885fa03..3186bceed35 100644 --- a/crates/shuffle/src/slice/snapshots/shuffle__slice__read__test__extract_metas.snap +++ b/crates/shuffle/src/slice/snapshots/shuffle__slice__read__test__extract_metas.snap @@ -1,5 +1,6 @@ --- source: crates/shuffle/src/slice/read.rs +assertion_line: 397 expression: metas --- [ @@ -9,6 +10,7 @@ expression: metas clock: Clock(1000s 1000ns), flags: Flags(8000 (OUTSIDE_TXN)), producer: Producer(01:00:00:00:00:00), + control: None, }, Meta { begin_offset: 12430, @@ -16,6 +18,7 @@ expression: metas clock: Clock(1000s 2000ns), flags: Flags(1 (CONTINUE_TXN)), producer: Producer(01:00:00:00:00:00), + control: None, }, Meta { begin_offset: 12504, @@ -23,5 +26,28 @@ expression: metas clock: Clock(1000s 3000ns), flags: Flags(2 (ACK_TXN)), producer: Producer(01:00:00:00:00:00), + control: None, + }, + Meta { + begin_offset: 12562, + end_offset: 12641, + clock: Clock(1000s 4000ns), + flags: Flags(4 (OUTSIDE_TXN|CONTROL)), + producer: Producer(01:00:00:00:00:00), + control: Some( + BackfillBegin, + ), + }, + Meta { + begin_offset: 12641, + end_offset: 12760, + clock: Clock(1000s 5000ns), + flags: Flags(4 (OUTSIDE_TXN|CONTROL)), + producer: Producer(01:00:00:00:00:00), + control: Some( + BackfillComplete { + truncated_at: Clock(1700000000s 0ns), + }, + ), }, ] diff --git a/crates/shuffle/src/slice/snapshots/shuffle__slice__state__test__flush_state_machine.snap b/crates/shuffle/src/slice/snapshots/shuffle__slice__state__test__flush_state_machine.snap index b234fb561c9..ef8de17ecbd 100644 --- a/crates/shuffle/src/slice/snapshots/shuffle__slice__state__test__flush_state_machine.snap +++ b/crates/shuffle/src/slice/snapshots/shuffle__slice__state__test__flush_state_machine.snap @@ -30,5 +30,7 @@ Frontier { 0/200, 0/300, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, } diff --git a/crates/shuffle/src/slice/state.rs b/crates/shuffle/src/slice/state.rs index ba9d362b28d..97f4ca90d5c 100644 --- a/crates/shuffle/src/slice/state.rs +++ b/crates/shuffle/src/slice/state.rs @@ -216,6 +216,10 @@ pub struct SequencedDoc { pub is_commit: bool, /// Updated producer state to commit after processing this document. pub producer_state: ProducerState, + /// Backfill-begin control-doc clock this document committed (zero = none). + pub backfill_begin: uuid::Clock, + /// Backfill-complete control-doc clock this document committed (zero = none). + pub backfill_complete: uuid::Clock, } /// Gate on `adjusted_clock` relative to `now`: if the clock is in the future, @@ -309,6 +313,7 @@ pub fn sequence_document( producer, clock, flags, + control, begin_offset, end_offset, } = meta; @@ -338,11 +343,21 @@ pub fn sequence_document( ) })?; + let mut backfill_begin = uuid::Clock::zero(); + let mut backfill_complete = uuid::Clock::zero(); + // Match over `outcome` to update `producer_state` and determine append/commit. let (is_append, is_commit) = match outcome { uuid::SequenceOutcome::OutsideCommit => { producer_state.offset = -*end_offset; - (true, true) + match control { + Some(super::read::ControlEvent::BackfillBegin) => backfill_begin = *clock, + Some(super::read::ControlEvent::BackfillComplete { truncated_at }) => { + backfill_complete = *truncated_at + } + None => {} + } + (control.is_none(), true) } uuid::SequenceOutcome::OutsideDuplicate => (false, false), uuid::SequenceOutcome::ContinueBeginSpan => { @@ -376,9 +391,13 @@ pub fn sequence_document( uuid::SequenceOutcome::AckDuplicate => (false, false), }; - // A `notBefore` or `notAfter` suppresses document append, but doesn't impact - // the propagation of flush and progress reporting. - let is_append = is_append && *clock >= binding.not_before && *clock < binding.not_after; + // A `notBefore`/`notAfter` window and the journal's `truncated_at` + // truncation boundary all suppress document append, but don't impact the + // propagation of flush and progress reporting. + let is_append = is_append + && *clock >= binding.not_before + && *clock >= read_state.truncated_at + && *clock < binding.not_after; tracing::trace!( journal = %read_state.journal, @@ -396,6 +415,8 @@ pub fn sequence_document( is_append, is_commit, producer_state, + backfill_begin, + backfill_complete, }) } @@ -594,6 +615,7 @@ mod test { flags, begin_offset, end_offset, + control: None, } } @@ -630,9 +652,10 @@ mod test { impl TestState { fn commit(&mut self, read_id: usize, producer: Producer, seq: SequencedDoc) { - _ = self.reads[read_id] - .pending - .insert(producer, seq.producer_state); + let read = &mut self.reads[read_id]; + read.backfill_begin = read.backfill_begin.max(seq.backfill_begin); + read.backfill_complete = read.backfill_complete.max(seq.backfill_complete); + _ = read.pending.insert(producer, seq.producer_state); if seq.is_commit { self.flush.set_ready(); } @@ -725,6 +748,9 @@ mod test { s.reads.push(ReadState { binding_index: 0, journal: "test/journal/A".into(), + truncated_at: Clock::zero(), + backfill_begin: Clock::zero(), + backfill_complete: Clock::zero(), settled: producers, pending: Default::default(), read_offset: 0, @@ -779,6 +805,9 @@ mod test { s.reads.push(ReadState { binding_index: 0, journal: "test/journal/A".into(), + truncated_at: Clock::zero(), + backfill_begin: Clock::zero(), + backfill_complete: Clock::zero(), settled: producers, pending: Default::default(), read_offset: 0, @@ -812,8 +841,12 @@ mod test { s.commit(0, p3, seq); // Build frontier and start flush with 3 shards. - let frontier = - super::super::producer::build_flush_frontier(&mut s.reads, std::iter::empty(), 3); + let frontier = super::super::producer::build_flush_frontier( + &mut s.reads, + &s.bindings, + std::iter::empty(), + 3, + ); assert!(!frontier.journals.is_empty(), "flushing frontier built"); let flush_cycle = s.flush.start(3, frontier); @@ -883,6 +916,7 @@ mod test { }], flushed_lsn: vec![], unresolved_hints: 0, + ..Default::default() }; // Request + flushed → has_progressed gates, take_progressed consumes. @@ -939,6 +973,9 @@ mod test { s.reads.push(ReadState { binding_index: 0, journal: "test/journal/A".into(), + truncated_at: Clock::zero(), + backfill_begin: Clock::zero(), + backfill_complete: Clock::zero(), settled: producers, pending: Default::default(), read_offset: 0, @@ -1034,6 +1071,131 @@ mod test { ); } + #[test] + fn test_sequence_outside_txn_control_docs_commit_immediately() { + let bindings = vec![test_binding(0, true, None, "/suffix")]; + let mut s = test_state(bindings); + + let p1 = producer(0x01); + + let (_offset, producers) = resolve_checkpoint(vec![checkpoint_entry(&p1, 0)]); + s.reads.push(ReadState { + binding_index: 0, + journal: "test/journal/A".into(), + truncated_at: Clock::zero(), + backfill_begin: Clock::zero(), + backfill_complete: Clock::zero(), + settled: producers, + pending: Default::default(), + read_offset: 0, + prev_read_offset: 0, + write_head: 0, + prev_write_head: 0, + }); + + // BackfillBegin as a CONTROL (implicitly OUTSIDE_TXN) doc commits the + // control clock immediately into backfill_begin; the doc itself is not + // appended (control docs never append). + let seq = sequence_document( + &s.reads[0], + &s.bindings[0], + &Meta { + producer: p1, + clock: Clock::from_unix(10, 0), + flags: OUTSIDE, + begin_offset: 100, + end_offset: 150, + control: Some(super::super::read::ControlEvent::BackfillBegin), + }, + ) + .unwrap(); + assert!(!seq.is_append, "control docs never append"); + assert!(seq.is_commit, "OUTSIDE_TXN drives an immediate commit"); + assert_eq!( + seq.backfill_begin, + Clock::from_unix(10, 0), + "reports its clock" + ); + assert_eq!(seq.backfill_complete, Clock::zero()); + s.commit(0, p1, seq); + assert_eq!(s.reads[0].backfill_begin, Clock::from_unix(10, 0)); + + let seq = sequence_document( + &s.reads[0], + &s.bindings[0], + &Meta { + producer: p1, + clock: Clock::from_unix(20, 0), + flags: OUTSIDE, + begin_offset: 150, + end_offset: 200, + control: Some(super::super::read::ControlEvent::BackfillComplete { + truncated_at: Clock::from_unix(10, 0), + }), + }, + ) + .unwrap(); + assert!(!seq.is_append); + assert!(seq.is_commit); + assert_eq!(seq.backfill_begin, Clock::zero()); + assert_eq!(seq.backfill_complete, Clock::from_unix(10, 0)); + s.commit(0, p1, seq); + assert_eq!(s.reads[0].backfill_begin, Clock::from_unix(10, 0)); + assert_eq!(s.reads[0].backfill_complete, Clock::from_unix(10, 0)); + + // A duplicate OUTSIDE_TXN control doc (same clock) is a no-op: not a + // commit, reports no clock, and leaves the read's state unchanged. + let seq = sequence_document( + &s.reads[0], + &s.bindings[0], + &Meta { + producer: p1, + clock: Clock::from_unix(20, 0), + flags: OUTSIDE, + begin_offset: 200, + end_offset: 250, + control: Some(super::super::read::ControlEvent::BackfillComplete { + truncated_at: Clock::from_unix(10, 0), + }), + }, + ) + .unwrap(); + assert!(!seq.is_append); + assert!(!seq.is_commit, "OutsideDuplicate suppresses flush"); + assert_eq!( + seq.backfill_complete, + Clock::zero(), + "duplicate reports no clock" + ); + s.commit(0, p1, seq); + assert_eq!( + s.reads[0].backfill_complete, + Clock::from_unix(10, 0), + "unchanged on duplicate" + ); + + // A fresh BackfillBegin with a later clock supersedes the prior begin via + // the commit-time max fold into the read. + let seq = sequence_document( + &s.reads[0], + &s.bindings[0], + &Meta { + producer: p1, + clock: Clock::from_unix(30, 0), + flags: OUTSIDE, + begin_offset: 250, + end_offset: 300, + control: Some(super::super::read::ControlEvent::BackfillBegin), + }, + ) + .unwrap(); + assert!(seq.is_commit); + assert_eq!(seq.backfill_begin, Clock::from_unix(30, 0)); + s.commit(0, p1, seq); + assert_eq!(s.reads[0].backfill_begin, Clock::from_unix(30, 0)); + assert_eq!(s.reads[0].backfill_complete, Clock::from_unix(10, 0)); + } + /// Build a passthrough PartitionFilter that accepts any value for the given fields. /// The field count must match the partition field segments in the journal suffix. fn passthrough_filter(fields: &[&str]) -> PartitionFilter { diff --git a/crates/shuffle/tests/scenario_fixtures.rs b/crates/shuffle/tests/scenario_fixtures.rs index f1aaecff5fa..09bd72d6dd4 100644 --- a/crates/shuffle/tests/scenario_fixtures.rs +++ b/crates/shuffle/tests/scenario_fixtures.rs @@ -260,6 +260,16 @@ async fn shuffle_scenarios() { .await; data_plane.reset().await.expect("reset"); + control_docs_are_metadata_only( + &materialization_spec, + &capture_spec, + &data_plane.journal_client, + &service, + log_dir.path(), + ) + .await; + data_plane.reset().await.expect("reset"); + multi_shard_routing( &materialization_spec, &capture_spec, @@ -310,6 +320,16 @@ async fn shuffle_scenarios() { .await; data_plane.reset().await.expect("reset"); + control_docs_reach_all_partitions( + &materialization_spec, + &capture_spec, + &data_plane.journal_client, + &service, + log_dir.path(), + ) + .await; + data_plane.reset().await.expect("reset"); + clock_window_filtering( &materialization_spec, &capture_spec, @@ -328,6 +348,16 @@ async fn shuffle_scenarios() { log_dir.path(), ) .await; + data_plane.reset().await.expect("reset"); + + resume_with_backfill_metadata( + &materialization_spec, + &capture_spec, + &data_plane.journal_client, + &service, + log_dir.path(), + ) + .await; server_handle.abort(); data_plane @@ -464,6 +494,144 @@ async fn continue_then_ack( session.close().await.expect("close"); } +/// Publish control docs and regular documents. Verify control docs update +/// checkpoint metadata but do not appear in shuffle logs. +async fn control_docs_are_metadata_only( + materialization_spec: &flow::MaterializationSpec, + capture_spec: &flow::CaptureSpec, + journal_client: &gazette::journal::Client, + service: &shuffle::Service, + log_dir: &std::path::Path, +) { + let scenario_dir = log_dir.join("control_docs_are_metadata_only"); + std::fs::create_dir_all(&scenario_dir).unwrap(); + + let producer = uuid::Producer::from_bytes([0x01, 0x00, 0x00, 0x00, 0x00, 0x11]); + let mut pub_ = make_publisher(capture_spec, journal_client, producer); + let mut expected_clock = uuid::Clock::UNIX_EPOCH; + + // BackfillBegin (OUTSIDE_TXN) is published before any ordinary data of the + // isolated checkpoint, while no CONTINUE_TXN span is open. + let begin_clock = expected_clock.tick(); + pub_.enqueue( + |uuid| { + Ok(( + 0, + serde_json::json!({ + "_meta": {"uuid": uuid.to_string(), "backfillBegin": true}, + "id": "control", + "category": "alpha", + "value": 0, + }), + )) + }, + uuid::Flags::CONTROL, + ) + .await + .unwrap(); + + _ = expected_clock.tick(); + pub_.enqueue( + |uuid| { + Ok(( + 0, + serde_json::json!({ + "_meta": {"uuid": uuid.to_string()}, + "id": "data", + "category": "alpha", + "value": 7, + }), + )) + }, + uuid::Flags::CONTINUE_TXN, + ) + .await + .unwrap(); + + // Commit the CONTINUE_TXN span first — BackfillComplete cannot be written + // while a CONTINUE_TXN span is still open, because OUTSIDE_TXN sequencing + // disallows a non-zero `max_continue`. + let (producer, commit_clock, journals) = pub_.commit_intents(); + assert_eq!(commit_clock, expected_clock.tick()); + let journal_acks = + publisher::intents::build_transaction_intents(&[(producer, commit_clock, journals)]); + pub_.write_intents(journal_acks).await.unwrap(); + + // Now that the ACK has committed the data span, BackfillComplete can be + // published as OUTSIDE_TXN and is self-committing. + _ = expected_clock.tick(); + pub_.enqueue( + |uuid| { + // The Complete carries the begin clock as its RFC3339 `truncatedAt` + // boundary, rendered exactly as the runtime publisher does. + let (seconds, nanos) = begin_clock.to_unix(); + let truncated_at = chrono::DateTime::from_timestamp(seconds as i64, nanos) + .unwrap() + .to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true); + Ok(( + 0, + serde_json::json!({ + "_meta": { + "uuid": uuid.to_string(), + "backfillComplete": true, + "truncatedAt": truncated_at, + }, + "id": "control", + "category": "alpha", + "value": 0, + }), + )) + }, + uuid::Flags::CONTROL, + ) + .await + .unwrap(); + pub_.flush().await.unwrap(); + + let mut session = shuffle::SessionClient::open( + service, + build_task(materialization_spec), + build_shards(1, service.peer_endpoint(), &scenario_dir), + Default::default(), + ) + .await + .expect("SessionClient::open"); + + // Because BackfillBegin is OUTSIDE_TXN and self-committing while the + // intervening CONTINUE_TXN span is only committed by its ACK, the begin + // and complete clocks may land in separate checkpoint flushes. Aggregate + // across checkpoints until both clocks are visible. + let suffix_0: &str = &materialization_spec.bindings[0].journal_read_suffix; + let mut frontier = session.next_checkpoint().await.expect("next_checkpoint"); + let mut shard_state: ShardState = (0..1).map(|_| None).collect(); + let mut read = collect_read_entries(&frontier, &scenario_dir, &mut shard_state); + while frontier.latest_backfill_begin.get(suffix_0) != Some(&begin_clock) + || frontier.latest_backfill_complete.get(suffix_0) != Some(&begin_clock) + { + let next = session.next_checkpoint().await.expect("next_checkpoint"); + read.extend(collect_read_entries(&next, &scenario_dir, &mut shard_state)); + frontier = frontier.reduce(next); + } + assert_eq!( + frontier.latest_backfill_begin.get(suffix_0), + Some(&begin_clock) + ); + assert_eq!( + frontier.latest_backfill_complete.get(suffix_0), + Some(&begin_clock) + ); + + insta::assert_debug_snapshot!( + "control_docs_are_metadata_only", + Checkpoint { + frontier: &frontier, + read, + } + ); + + session.close().await.expect("close"); +} + /// Open a session with 3 shards (split key space). Publish documents with /// varied /id values so they hash to different key ranges. Verify the /// frontier covers all journals and all documents are readable across shards. @@ -1006,6 +1174,93 @@ async fn partition_filtered_hints( session.close().await.expect("close"); } +/// A backfill CONTROL document carries only `_meta` (no partition fields), so +/// ordinary mapping would route it to a single default partition that a reader +/// with a constrained selector might never read. `publish_control` instead +/// broadcasts it to every partition journal. +/// +/// Seed the alpha, beta, and gamma partitions, then broadcast a BackfillBegin +/// to all three. The reader's selector excludes beta, yet it must still observe +/// the backfill begin via the included alpha/gamma partitions — which only +/// holds because the control doc was written to those journals too. +async fn control_docs_reach_all_partitions( + materialization_spec: &flow::MaterializationSpec, + capture_spec: &flow::CaptureSpec, + journal_client: &gazette::journal::Client, + service: &shuffle::Service, + log_dir: &std::path::Path, +) { + let scenario_dir = log_dir.join("control_docs_reach_all_partitions"); + std::fs::create_dir_all(&scenario_dir).unwrap(); + + let producer = uuid::Producer::from_bytes([0x01, 0x00, 0x00, 0x00, 0x00, 0x08]); + let mut pub_ = make_publisher(capture_spec, journal_client, producer); + + // Seed one document into each of alpha/beta/gamma so all three physical + // partitions exist and are listable, then commit them. + for category in ["alpha", "beta", "gamma"] { + pub_.enqueue( + |uuid| { + Ok(( + 0, + serde_json::json!({ + "_meta": {"uuid": uuid.to_string()}, + "id": format!("seed-{category}"), + "category": category, + "value": 0, + }), + )) + }, + uuid::Flags::CONTINUE_TXN, + ) + .await + .unwrap(); + } + let (producer_id, commit_clock, journals) = pub_.commit_intents(); + let journal_acks = + publisher::intents::build_transaction_intents(&[(producer_id, commit_clock, journals)]); + pub_.write_intents(journal_acks).await.unwrap(); + + // Broadcast BackfillBegin to every partition (including the excluded beta). + pub_.enqueue_control_all_partitions( + 0, + |uuid| { + Ok(serde_json::json!({ + "_meta": {"uuid": uuid.to_string(), "backfillBegin": true}, + })) + }, + uuid::Flags::CONTROL, + ) + .await + .unwrap(); + pub_.flush().await.unwrap(); + + let mut session = shuffle::SessionClient::open( + service, + build_task(materialization_spec), + build_shards(1, service.peer_endpoint(), &scenario_dir), + Default::default(), + ) + .await + .expect("SessionClient::open"); + + // Aggregate checkpoints until the (terminal) frontier carries the backfill + // begin. The reader excludes beta, so observing it at all proves the control + // doc reached the included alpha/gamma partitions. + let suffix_0: &str = &materialization_spec.bindings[0].journal_read_suffix; + let mut frontier = next_resolved_checkpoint(&mut session, "next_checkpoint").await; + while frontier.latest_backfill_begin.get(suffix_0).is_none() { + let next = next_resolved_checkpoint(&mut session, "next_checkpoint").await; + frontier = frontier.reduce(next); + } + assert!( + frontier.latest_backfill_begin.get(suffix_0).is_some(), + "a selector that excludes beta still observed the broadcast backfill begin", + ); + + session.close().await.expect("close"); +} + /// Clock-window filtering: set `not_before` on binding 0 (apples) to a /// future clock so all documents are filtered (the Publisher starts from /// Clock::UNIX_EPOCH, so document clocks are near epoch). Binding 1 (bananas) @@ -1358,3 +1613,196 @@ async fn rollback( session.close().await.expect("close"); } + +/// Verify that `latest_backfill_begin` and `latest_backfill_complete` survive +/// the checkpoint→wire→resume round-trip through the Session handler. +/// +/// Phase 1: Publish control docs + data in a committed transaction. Capture +/// a checkpoint whose frontier carries non-empty backfill maps. +/// Phase 2: Reopen a new session using that frontier as the resume +/// checkpoint. Write additional data and poll a checkpoint. The resumed +/// session must have accepted the backfill metadata from the resume +/// frontier — the session would error if the resume checkpoint were +/// malformed, and new progress comes back correctly layered on top. +async fn resume_with_backfill_metadata( + materialization_spec: &flow::MaterializationSpec, + capture_spec: &flow::CaptureSpec, + journal_client: &gazette::journal::Client, + service: &shuffle::Service, + log_dir: &std::path::Path, +) { + let phase1_dir = log_dir.join("resume_backfill_p1"); + let phase2_dir = log_dir.join("resume_backfill_p2"); + std::fs::create_dir_all(&phase1_dir).unwrap(); + std::fs::create_dir_all(&phase2_dir).unwrap(); + + let producer = uuid::Producer::from_bytes([0x01, 0x00, 0x00, 0x00, 0x00, 0x21]); + let mut pub_ = make_publisher(capture_spec, journal_client, producer); + let mut expected_clock = uuid::Clock::UNIX_EPOCH; + + // ---- Phase 1: Commit control docs + data, capture a checkpoint. ---- + + // BackfillBegin (OUTSIDE_TXN) is published before the ordinary data span. + let begin_clock = expected_clock.tick(); + pub_.enqueue( + |uuid| { + Ok(( + 0, + serde_json::json!({ + "_meta": {"uuid": uuid.to_string(), "backfillBegin": true}, + "id": "rb-ctrl", + "category": "alpha", + "value": 0, + }), + )) + }, + uuid::Flags::CONTROL, + ) + .await + .unwrap(); + + _ = expected_clock.tick(); + pub_.enqueue( + |uuid| { + Ok(( + 0, + serde_json::json!({ + "_meta": {"uuid": uuid.to_string()}, + "id": "rb-data", + "category": "alpha", + "value": 42, + }), + )) + }, + uuid::Flags::CONTINUE_TXN, + ) + .await + .unwrap(); + + // Commit the CONTINUE_TXN span before publishing BackfillComplete. + let (producer_id, commit_clock, journals) = pub_.commit_intents(); + assert_eq!(commit_clock, expected_clock.tick()); + let journal_acks = + publisher::intents::build_transaction_intents(&[(producer_id, commit_clock, journals)]); + pub_.write_intents(journal_acks).await.unwrap(); + + // BackfillComplete as OUTSIDE_TXN is self-committing and published after + // the data-span's ACK. + _ = expected_clock.tick(); + pub_.enqueue( + |uuid| { + // The Complete carries the begin clock as its RFC3339 `truncatedAt` + // boundary, rendered exactly as the runtime publisher does. + let (seconds, nanos) = begin_clock.to_unix(); + let truncated_at = chrono::DateTime::from_timestamp(seconds as i64, nanos) + .unwrap() + .to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true); + Ok(( + 0, + serde_json::json!({ + "_meta": { + "uuid": uuid.to_string(), + "backfillComplete": true, + "truncatedAt": truncated_at, + }, + "id": "rb-ctrl", + "category": "alpha", + "value": 0, + }), + )) + }, + uuid::Flags::CONTROL, + ) + .await + .unwrap(); + pub_.flush().await.unwrap(); + + let mut session = shuffle::SessionClient::open( + service, + build_task(materialization_spec), + build_shards(1, service.peer_endpoint(), &phase1_dir), + Default::default(), + ) + .await + .expect("SessionClient::open phase 1"); + + // Aggregate checkpoint deltas until both the backfill begin clock (from + // the OUTSIDE_TXN BackfillBegin commit) and the backfill complete clock + // (from the OUTSIDE_TXN BackfillComplete commit after the span's ACK) are + // visible. The two control docs commit in separate flush cycles. + let suffix_0: &str = &materialization_spec.bindings[0].journal_read_suffix; + let mut phase1_frontier = session.next_checkpoint().await.expect("phase 1 checkpoint"); + while phase1_frontier.latest_backfill_begin.get(suffix_0) != Some(&begin_clock) + || phase1_frontier.latest_backfill_complete.get(suffix_0) != Some(&begin_clock) + { + let next = session + .next_checkpoint() + .await + .expect("phase 1 next checkpoint"); + phase1_frontier = phase1_frontier.reduce(next); + } + assert_eq!( + phase1_frontier.latest_backfill_begin.get(suffix_0), + Some(&begin_clock), + "phase 1 frontier should carry backfill begin" + ); + assert_eq!( + phase1_frontier.latest_backfill_complete.get(suffix_0), + Some(&begin_clock), + "phase 1 frontier should carry the backfill begin clock as its truncation boundary" + ); + session.close().await.expect("close phase 1"); + + // ---- Phase 2: Resume from phase1_frontier, write more data. ---- + + pub_.enqueue( + |uuid| { + Ok(( + 0, + serde_json::json!({ + "_meta": {"uuid": uuid.to_string()}, + "id": "rb-new", + "category": "alpha", + "value": 99, + }), + )) + }, + uuid::Flags::OUTSIDE_TXN, + ) + .await + .unwrap(); + pub_.flush().await.unwrap(); + + let mut session = shuffle::SessionClient::open( + service, + build_task(materialization_spec), + build_shards(1, service.peer_endpoint(), &phase2_dir), + phase1_frontier.clone(), + ) + .await + .expect("SessionClient::open phase 2 (resume with backfill metadata)"); + + let phase2_frontier = next_resolved_checkpoint(&mut session, "phase 2 checkpoint").await; + + let mut phase2_shard_state: ShardState = (0..1).map(|_| None).collect(); + let phase2_read = collect_read_entries(&phase2_frontier, &phase2_dir, &mut phase2_shard_state); + insta::assert_debug_snapshot!( + "resume_with_backfill_metadata", + Checkpoint { + frontier: &phase2_frontier, + read: phase2_read, + } + ); + + session.close().await.expect("close phase 2"); +} + +// NOTE: An earlier design tested that CONTINUE_TXN control docs were correctly +// discarded on rollback via staged-then-committed sequencing. Under the +// current design, control docs carry `Flag_CONTROL` alone (implying +// OUTSIDE_TXN) and are immediately committed, so transactional rollback does +// not apply to them; interleaving OUTSIDE_TXN with a still-open CONTINUE_TXN +// span is a protocol violation. Duplicate handling of immediately-committed +// control docs is covered by the unit test +// `test_sequence_outside_txn_control_docs_commit_immediately` in +// `crates/shuffle/src/slice/state.rs`. diff --git a/crates/shuffle/tests/scenario_fuzz.rs b/crates/shuffle/tests/scenario_fuzz.rs index 5094cfea796..78435ca38a1 100644 --- a/crates/shuffle/tests/scenario_fuzz.rs +++ b/crates/shuffle/tests/scenario_fuzz.rs @@ -675,6 +675,7 @@ fn project_hints(round_frontier: &shuffle::Frontier) -> shuffle::Frontier { unresolved_hints, journals, flushed_lsn: vec![], + ..Default::default() } } @@ -861,6 +862,7 @@ async fn run_test_case_inner( journals: vec![], flushed_lsn: recovery.flushed_lsn.clone(), unresolved_hints: 0, + ..Default::default() }; // STEP 3: POLL CHECKPOINTS. @@ -930,6 +932,7 @@ async fn run_test_case_inner( journals: vec![], flushed_lsn: recovery.flushed_lsn.clone(), unresolved_hints: 0, + ..Default::default() }; if recovery.unresolved_hints != 0 { diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__clock_window_filtering.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__clock_window_filtering.snap index 262f6eb29b5..cdb60f9b150 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__clock_window_filtering.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__clock_window_filtering.snap @@ -37,6 +37,8 @@ Checkpoint { flushed_lsn: [ 1/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [ diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__continue_then_ack.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__continue_then_ack.snap index da2035582b5..e51feec44e2 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__continue_then_ack.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__continue_then_ack.snap @@ -23,6 +23,8 @@ Checkpoint { flushed_lsn: [ 1/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [ diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__control_docs_are_metadata_only.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__control_docs_are_metadata_only.snap new file mode 100644 index 00000000000..038eadadc06 --- /dev/null +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__control_docs_are_metadata_only.snap @@ -0,0 +1,48 @@ +--- +source: crates/shuffle/tests/scenario_fixtures.rs +expression: "Checkpoint { frontier: &frontier, read, }" +--- +Checkpoint { + frontier: Frontier { + journals: [ + JournalFrontier { + journal: "testing/apples/2020202020202020/category=alpha/pivot=00", + binding: 0, + producers: [ + ProducerFrontier { + producer: Producer(01:00:00:00:00:11), + last_commit: Clock(0s 4000ns), + hinted_commit: Clock(0s 0ns), + offset: -475, + }, + ], + bytes_read_delta: 475, + bytes_behind_delta: 0, + }, + ], + flushed_lsn: [ + 1/0, + ], + latest_backfill_begin: { + "materialize/testing/materialization/binding-0": Clock(0s 1000ns), + }, + latest_backfill_complete: { + "materialize/testing/materialization/binding-0": Clock(0s 1000ns), + }, + unresolved_hints: 0, + }, + read: [ + ReadEntry { + binding: 0, + journal: "testing/apples/2020202020202020/category=alpha/pivot=00", + doc: Object { + "_meta": Object { + "uuid": String("13814014-1dd2-11b2-8001-010000000011"), + }, + "category": String("alpha"), + "id": String("data"), + "value": Number(7), + }, + }, + ], +} diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__multi_partition_transaction.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__multi_partition_transaction.snap index 23ba62716d5..067cabb1f4d 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__multi_partition_transaction.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__multi_partition_transaction.snap @@ -37,6 +37,8 @@ Checkpoint { flushed_lsn: [ 1/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [ diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__multi_shard_routing.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__multi_shard_routing.snap index eac11e31fd2..f2151fbc29c 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__multi_shard_routing.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__multi_shard_routing.snap @@ -25,6 +25,8 @@ Checkpoint { 1/0, 0/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [ diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__multiple_producers_checkpoint1.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__multiple_producers_checkpoint1.snap index 48b1580d5c7..2baf410084f 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__multiple_producers_checkpoint1.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__multiple_producers_checkpoint1.snap @@ -29,6 +29,8 @@ Checkpoint { flushed_lsn: [ 1/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [ diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__multiple_producers_checkpoint2.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__multiple_producers_checkpoint2.snap index 3ea5c9e2b5d..057eacb50d4 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__multiple_producers_checkpoint2.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__multiple_producers_checkpoint2.snap @@ -23,6 +23,8 @@ Checkpoint { flushed_lsn: [ 1/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [ diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__multiple_producers_resumed.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__multiple_producers_resumed.snap index 0e549ecf855..a7d101d632d 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__multiple_producers_resumed.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__multiple_producers_resumed.snap @@ -29,6 +29,8 @@ Checkpoint { flushed_lsn: [ 1/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [ diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__partition_filtered_hints.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__partition_filtered_hints.snap index 52889a25480..0501e729535 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__partition_filtered_hints.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__partition_filtered_hints.snap @@ -37,6 +37,8 @@ Checkpoint { flushed_lsn: [ 1/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [ diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__resume_from_checkpoint_progress.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__resume_from_checkpoint_progress.snap index 0d37b29f9cb..92974fe3355 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__resume_from_checkpoint_progress.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__resume_from_checkpoint_progress.snap @@ -37,6 +37,8 @@ Checkpoint { flushed_lsn: [ 1/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [ diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__resume_from_checkpoint_recovery.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__resume_from_checkpoint_recovery.snap index 5d1b7d54303..68aea96e1d6 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__resume_from_checkpoint_recovery.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__resume_from_checkpoint_recovery.snap @@ -37,6 +37,8 @@ Checkpoint { flushed_lsn: [ 1/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [ diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__resume_with_backfill_metadata.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__resume_with_backfill_metadata.snap new file mode 100644 index 00000000000..5df83180eae --- /dev/null +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__resume_with_backfill_metadata.snap @@ -0,0 +1,44 @@ +--- +source: crates/shuffle/tests/scenario_fixtures.rs +expression: "Checkpoint { frontier: &phase2_frontier, read: phase2_read, }" +--- +Checkpoint { + frontier: Frontier { + journals: [ + JournalFrontier { + journal: "testing/apples/2020202020202020/category=alpha/pivot=00", + binding: 0, + producers: [ + ProducerFrontier { + producer: Producer(01:00:00:00:00:21), + last_commit: Clock(0s 5000ns), + hinted_commit: Clock(0s 0ns), + offset: -581, + }, + ], + bytes_read_delta: 102, + bytes_behind_delta: 0, + }, + ], + flushed_lsn: [ + 1/0, + ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, + unresolved_hints: 0, + }, + read: [ + ReadEntry { + binding: 0, + journal: "testing/apples/2020202020202020/category=alpha/pivot=00", + doc: Object { + "_meta": Object { + "uuid": String("13814032-1dd2-11b2-8000-010000000021"), + }, + "category": String("alpha"), + "id": String("rb-new"), + "value": Number(99), + }, + }, + ], +} diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase1_committed.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase1_committed.snap index 9cfe9f52df1..0dff3e4c092 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase1_committed.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase1_committed.snap @@ -29,6 +29,8 @@ Checkpoint { flushed_lsn: [ 1/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [ diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase2_clean_rollback.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase2_clean_rollback.snap index 6ae9dfa411e..b4447a0891e 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase2_clean_rollback.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase2_clean_rollback.snap @@ -23,6 +23,8 @@ Checkpoint { flushed_lsn: [ 1/1, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [], diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase3_p1_continues.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase3_p1_continues.snap index 41ec7867f23..e9b83deecd0 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase3_p1_continues.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase3_p1_continues.snap @@ -23,6 +23,8 @@ Checkpoint { flushed_lsn: [ 1/2, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [ diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase4_deep_rollback.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase4_deep_rollback.snap index 289d4c75f09..962511ec208 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase4_deep_rollback.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__rollback_phase4_deep_rollback.snap @@ -23,6 +23,8 @@ Checkpoint { flushed_lsn: [ 1/3, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [], diff --git a/crates/shuffle/tests/snapshots/scenario_fixtures__single_producer_outside_txn.snap b/crates/shuffle/tests/snapshots/scenario_fixtures__single_producer_outside_txn.snap index e4957eb6325..25ecbb31504 100644 --- a/crates/shuffle/tests/snapshots/scenario_fixtures__single_producer_outside_txn.snap +++ b/crates/shuffle/tests/snapshots/scenario_fixtures__single_producer_outside_txn.snap @@ -23,6 +23,8 @@ Checkpoint { flushed_lsn: [ 1/0, ], + latest_backfill_begin: {}, + latest_backfill_complete: {}, unresolved_hints: 0, }, read: [ diff --git a/go/labels/labels.go b/go/labels/labels.go index 8120a0f2275..cacdee2c864 100644 --- a/go/labels/labels.go +++ b/go/labels/labels.go @@ -36,8 +36,20 @@ const ( KeyEndMax = "ffffffff" // ManagedByFlow is a value for the Gazette labels.ManagedBy label. ManagedByFlow = "estuary.dev/flow" + // TruncatedAt is the publication timestamp after which journal data is current, + // determined by the last backfill-begin timestamp of the collection's capture. + // + // Its value is a fixed-width, 16-character hex encoding of a uint64 Gazette + // message.Clock. A reader raises its effective not_before to this clock to + // skip the stale pre-backfill prefix. + TruncatedAt = "estuary.dev/truncated-at" ) +// FlagControl is the CONTROL message flag (bit 2) marking application control +// messages. Control messages are immediately committed metadata events and +// never participate in a CONTINUE_TXN / ACK_TXN span. +const FlagControl uint16 = 0x4 + // ShardSpec labels. const ( // TaskName of this shard within the catalog. @@ -112,7 +124,10 @@ func IsRuntimeLabel(label string) bool { // R-Clock splits are performed dynamically by the runtime. RClockBegin, RClockEnd, // Shard splits are performed dynamically by the runtime. - SplitTarget, SplitSource: + SplitTarget, SplitSource, + // The backfill truncation boundary is applied to live journals by the + // capture runtime, so convergence must preserve it (not rebuild it away). + TruncatedAt: return true default: return false diff --git a/go/labels/labels_test.go b/go/labels/labels_test.go index a431a600ba1..508f924a690 100644 --- a/go/labels/labels_test.go +++ b/go/labels/labels_test.go @@ -24,6 +24,7 @@ func TestRuntimeLabels(t *testing.T) { {RClockEnd, true}, {SplitSource, true}, {SplitTarget, true}, + {TruncatedAt, true}, {TaskName, false}, {TaskType, false}, {labels.ContentType, false}, diff --git a/go/protocols/capture/capture.pb.go b/go/protocols/capture/capture.pb.go index d8258d87bb7..1a53d773ce5 100644 --- a/go/protocols/capture/capture.pb.go +++ b/go/protocols/capture/capture.pb.go @@ -435,14 +435,16 @@ func (m *Request_Acknowledge) XXX_DiscardUnknown() { var xxx_messageInfo_Request_Acknowledge proto.InternalMessageInfo type Response struct { - Spec *Response_Spec `protobuf:"bytes,1,opt,name=spec,proto3" json:"spec,omitempty"` - Discovered *Response_Discovered `protobuf:"bytes,2,opt,name=discovered,proto3" json:"discovered,omitempty"` - Validated *Response_Validated `protobuf:"bytes,3,opt,name=validated,proto3" json:"validated,omitempty"` - Applied *Response_Applied `protobuf:"bytes,4,opt,name=applied,proto3" json:"applied,omitempty"` - Opened *Response_Opened `protobuf:"bytes,5,opt,name=opened,proto3" json:"opened,omitempty"` - Captured *Response_Captured `protobuf:"bytes,6,opt,name=captured,proto3" json:"captured,omitempty"` - SourcedSchema *Response_SourcedSchema `protobuf:"bytes,8,opt,name=sourced_schema,json=sourcedSchema,proto3" json:"sourced_schema,omitempty"` - Checkpoint *Response_Checkpoint `protobuf:"bytes,7,opt,name=checkpoint,proto3" json:"checkpoint,omitempty"` + Spec *Response_Spec `protobuf:"bytes,1,opt,name=spec,proto3" json:"spec,omitempty"` + Discovered *Response_Discovered `protobuf:"bytes,2,opt,name=discovered,proto3" json:"discovered,omitempty"` + Validated *Response_Validated `protobuf:"bytes,3,opt,name=validated,proto3" json:"validated,omitempty"` + Applied *Response_Applied `protobuf:"bytes,4,opt,name=applied,proto3" json:"applied,omitempty"` + Opened *Response_Opened `protobuf:"bytes,5,opt,name=opened,proto3" json:"opened,omitempty"` + Captured *Response_Captured `protobuf:"bytes,6,opt,name=captured,proto3" json:"captured,omitempty"` + SourcedSchema *Response_SourcedSchema `protobuf:"bytes,8,opt,name=sourced_schema,json=sourcedSchema,proto3" json:"sourced_schema,omitempty"` + Checkpoint *Response_Checkpoint `protobuf:"bytes,7,opt,name=checkpoint,proto3" json:"checkpoint,omitempty"` + BackfillBegin *Response_BackfillBegin `protobuf:"bytes,9,opt,name=backfill_begin,json=backfillBegin,proto3" json:"backfill_begin,omitempty"` + BackfillComplete *Response_BackfillComplete `protobuf:"bytes,10,opt,name=backfill_complete,json=backfillComplete,proto3" json:"backfill_complete,omitempty"` // Reserved for internal use. Internal []byte `protobuf:"bytes,100,opt,name=internal,json=$internal,proto3" json:"internal,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` @@ -993,6 +995,96 @@ func (m *Response_Checkpoint) XXX_DiscardUnknown() { var xxx_messageInfo_Response_Checkpoint proto.InternalMessageInfo +// Signals the start of a backfill for a binding. +// +// A control message (BackfillBegin or BackfillComplete) must stand alone +// in its connector checkpoint: the checkpoint must contain only the +// control message followed by the terminating Checkpoint response, with +// no Captured, SourcedSchema, or other control messages. The runtime +// enforces this rule and will fail the session on violation. +type Response_BackfillBegin struct { + Binding uint32 `protobuf:"varint,1,opt,name=binding,proto3" json:"binding,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Response_BackfillBegin) Reset() { *m = Response_BackfillBegin{} } +func (m *Response_BackfillBegin) String() string { return proto.CompactTextString(m) } +func (*Response_BackfillBegin) ProtoMessage() {} +func (*Response_BackfillBegin) Descriptor() ([]byte, []int) { + return fileDescriptor_841a70e6e6288f13, []int{1, 8} +} +func (m *Response_BackfillBegin) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Response_BackfillBegin) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Response_BackfillBegin.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Response_BackfillBegin) XXX_Merge(src proto.Message) { + xxx_messageInfo_Response_BackfillBegin.Merge(m, src) +} +func (m *Response_BackfillBegin) XXX_Size() int { + return m.ProtoSize() +} +func (m *Response_BackfillBegin) XXX_DiscardUnknown() { + xxx_messageInfo_Response_BackfillBegin.DiscardUnknown(m) +} + +var xxx_messageInfo_Response_BackfillBegin proto.InternalMessageInfo + +// Signals the end of a backfill for a binding. +// +// See BackfillBegin for the "stands alone in its checkpoint" rule. +type Response_BackfillComplete struct { + Binding uint32 `protobuf:"varint,1,opt,name=binding,proto3" json:"binding,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Response_BackfillComplete) Reset() { *m = Response_BackfillComplete{} } +func (m *Response_BackfillComplete) String() string { return proto.CompactTextString(m) } +func (*Response_BackfillComplete) ProtoMessage() {} +func (*Response_BackfillComplete) Descriptor() ([]byte, []int) { + return fileDescriptor_841a70e6e6288f13, []int{1, 9} +} +func (m *Response_BackfillComplete) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Response_BackfillComplete) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Response_BackfillComplete.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Response_BackfillComplete) XXX_Merge(src proto.Message) { + xxx_messageInfo_Response_BackfillComplete.Merge(m, src) +} +func (m *Response_BackfillComplete) XXX_Size() int { + return m.ProtoSize() +} +func (m *Response_BackfillComplete) XXX_DiscardUnknown() { + xxx_messageInfo_Response_BackfillComplete.DiscardUnknown(m) +} + +var xxx_messageInfo_Response_BackfillComplete proto.InternalMessageInfo + func init() { proto.RegisterType((*Request)(nil), "capture.Request") proto.RegisterType((*Request_Spec)(nil), "capture.Request.Spec") @@ -1013,6 +1105,8 @@ func init() { proto.RegisterType((*Response_Captured)(nil), "capture.Response.Captured") proto.RegisterType((*Response_SourcedSchema)(nil), "capture.Response.SourcedSchema") proto.RegisterType((*Response_Checkpoint)(nil), "capture.Response.Checkpoint") + proto.RegisterType((*Response_BackfillBegin)(nil), "capture.Response.BackfillBegin") + proto.RegisterType((*Response_BackfillComplete)(nil), "capture.Response.BackfillComplete") } func init() { @@ -1020,86 +1114,90 @@ func init() { } var fileDescriptor_841a70e6e6288f13 = []byte{ - // 1251 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x56, 0x4d, 0x6f, 0x1b, 0xc5, - 0x1b, 0xef, 0xfa, 0x75, 0xfd, 0xd8, 0x4e, 0x93, 0x51, 0xfe, 0xd5, 0x76, 0x1b, 0x25, 0xe9, 0xcb, - 0x1f, 0xa5, 0x2f, 0xd8, 0xc5, 0x6d, 0x81, 0xf2, 0xd2, 0x92, 0xa4, 0x54, 0xa2, 0x08, 0x5a, 0x4d, - 0xa1, 0x08, 0x2e, 0xab, 0xcd, 0xec, 0xc4, 0x5e, 0xb2, 0xde, 0x59, 0x76, 0xd7, 0x2d, 0x96, 0xb8, - 0x23, 0xbe, 0x00, 0xe7, 0x9e, 0xb8, 0xf1, 0x31, 0x90, 0x72, 0xe0, 0xc0, 0x91, 0x53, 0x04, 0xe5, - 0x5b, 0xf4, 0x84, 0xe6, 0x6d, 0xbd, 0xf6, 0x26, 0xc1, 0xad, 0x8a, 0xc4, 0x25, 0xf1, 0xcc, 0xf3, - 0x7b, 0x66, 0x9f, 0xb7, 0xf9, 0xfd, 0x06, 0xce, 0xf5, 0x59, 0x37, 0x8a, 0x59, 0xca, 0x08, 0x0b, - 0x92, 0x2e, 0x71, 0xa3, 0x74, 0x14, 0x53, 0xfd, 0xbf, 0x23, 0x2c, 0xa8, 0xae, 0x96, 0xf6, 0xca, - 0x14, 0x78, 0x37, 0x60, 0x4f, 0xc4, 0x1f, 0x09, 0xb3, 0x97, 0xfb, 0xac, 0xcf, 0xc4, 0xcf, 0x2e, - 0xff, 0x25, 0x77, 0xcf, 0xfd, 0xd4, 0x86, 0x3a, 0xa6, 0xdf, 0x8c, 0x68, 0x92, 0xa2, 0x8b, 0x50, - 0x49, 0x22, 0x4a, 0x2c, 0x63, 0xdd, 0xd8, 0x68, 0xf6, 0xfe, 0xd7, 0xd1, 0x9f, 0x51, 0xf6, 0xce, - 0xc3, 0x88, 0x12, 0x2c, 0x20, 0xe8, 0x06, 0x98, 0x9e, 0x9f, 0x10, 0xf6, 0x98, 0xc6, 0x56, 0x49, - 0xc0, 0x4f, 0x17, 0xe0, 0x77, 0x14, 0x00, 0x67, 0x50, 0xee, 0xf6, 0xd8, 0x0d, 0x7c, 0xcf, 0x4d, - 0xa9, 0x55, 0x3e, 0xc2, 0xed, 0x91, 0x02, 0xe0, 0x0c, 0x8a, 0xae, 0x40, 0xd5, 0x8d, 0xa2, 0x60, - 0x6c, 0x55, 0x84, 0xcf, 0xa9, 0x82, 0xcf, 0x26, 0xb7, 0x62, 0x09, 0xe2, 0x69, 0xb0, 0x88, 0x86, - 0x56, 0xf5, 0x88, 0x34, 0xee, 0x47, 0x34, 0xc4, 0x02, 0x82, 0x6e, 0x41, 0xd3, 0x25, 0x7b, 0x21, - 0x7b, 0x12, 0x50, 0xaf, 0x4f, 0xad, 0x9a, 0xf0, 0x58, 0x29, 0x1e, 0x3f, 0xc1, 0xe0, 0xbc, 0x03, - 0x3a, 0x03, 0xa6, 0x1f, 0xa6, 0x34, 0x0e, 0xdd, 0xc0, 0xf2, 0xd6, 0x8d, 0x8d, 0x16, 0x6e, 0x5c, - 0xd0, 0x1b, 0xf6, 0x0f, 0x06, 0x54, 0x78, 0xc9, 0xd0, 0x5d, 0x58, 0x20, 0x2c, 0x0c, 0x29, 0x49, - 0x59, 0xec, 0xa4, 0xe3, 0x88, 0x8a, 0x0a, 0x2f, 0xf4, 0xd6, 0x3a, 0xa2, 0x3d, 0xdb, 0xf2, 0x6b, - 0x1c, 0xda, 0xd9, 0xd6, 0xb8, 0xcf, 0xc6, 0x11, 0xc5, 0x6d, 0x92, 0x5f, 0xa2, 0x9b, 0xd0, 0x24, - 0x2c, 0xdc, 0xf5, 0xfb, 0xce, 0xd7, 0x09, 0x0b, 0x45, 0xdd, 0x5b, 0x5b, 0x2b, 0xcf, 0x0f, 0xd6, - 0x2c, 0x1a, 0x12, 0xe6, 0xf9, 0x61, 0xbf, 0xcb, 0x0d, 0x1d, 0xec, 0x3e, 0xf9, 0x84, 0x26, 0x89, - 0xdb, 0xa7, 0xb8, 0x26, 0x1d, 0xec, 0xdf, 0x0d, 0x30, 0x75, 0x3f, 0xd0, 0x47, 0x50, 0x09, 0xdd, - 0xa1, 0xec, 0x40, 0x63, 0xeb, 0xc6, 0xf3, 0x83, 0xb5, 0x37, 0xfa, 0x7e, 0x3a, 0x18, 0xed, 0x74, - 0x08, 0x1b, 0x76, 0x69, 0x92, 0x8e, 0xdc, 0x78, 0x2c, 0xe7, 0xa7, 0x30, 0x51, 0x3a, 0x5a, 0x2c, - 0x8e, 0xf8, 0x2f, 0xa4, 0xf6, 0xb4, 0x02, 0xa6, 0x9e, 0x99, 0x2c, 0x35, 0xe3, 0xdf, 0x48, 0xad, - 0xf4, 0x2a, 0x52, 0x2b, 0xcf, 0x9f, 0x1a, 0x7a, 0x1f, 0xcc, 0x1d, 0x3f, 0xe4, 0x90, 0xc4, 0xaa, - 0xac, 0x97, 0x37, 0x9a, 0xbd, 0xb3, 0x47, 0x5e, 0x97, 0xce, 0x96, 0x44, 0xe2, 0xcc, 0x05, 0x5d, - 0x87, 0x56, 0xe0, 0x26, 0xa9, 0xa3, 0x5c, 0xd4, 0x85, 0x58, 0x2a, 0xc4, 0x8f, 0x9b, 0x1c, 0xa6, - 0x36, 0xd0, 0x59, 0xe5, 0xf5, 0x98, 0xc6, 0x89, 0xcf, 0x42, 0x71, 0x29, 0x1a, 0x12, 0xf2, 0x48, - 0x6e, 0xd9, 0x3f, 0x1b, 0x50, 0x57, 0x9f, 0x43, 0xf7, 0x60, 0x39, 0xa6, 0x09, 0x1b, 0xc5, 0x84, - 0x3a, 0xf9, 0x3c, 0x8d, 0x39, 0xf2, 0x5c, 0xd0, 0x9e, 0xdb, 0x32, 0xdf, 0x77, 0x00, 0x08, 0x0b, - 0x02, 0x4a, 0x52, 0x5f, 0x0d, 0x41, 0xb3, 0xb7, 0xac, 0xc2, 0xcd, 0xf6, 0x79, 0xc4, 0x5b, 0x95, - 0xfd, 0x83, 0xb5, 0x13, 0x38, 0x87, 0x46, 0x36, 0x98, 0x3b, 0x2e, 0xd9, 0xdb, 0xf5, 0x83, 0x40, - 0xd4, 0xb8, 0x8d, 0xb3, 0xb5, 0xfd, 0x87, 0x01, 0x55, 0x41, 0x11, 0xe8, 0x32, 0x68, 0xb6, 0x54, - 0x2c, 0x77, 0x48, 0x35, 0x34, 0x02, 0x59, 0x50, 0xd7, 0x45, 0x28, 0x89, 0x22, 0xe8, 0x65, 0xa1, - 0xb2, 0x95, 0x97, 0xaa, 0x6c, 0xb5, 0x50, 0x59, 0xf4, 0x16, 0x40, 0x92, 0xba, 0x29, 0x95, 0x35, - 0xac, 0xcd, 0x51, 0xc3, 0xaa, 0xc0, 0xf3, 0x96, 0x54, 0x38, 0xb1, 0xbd, 0xaa, 0x0c, 0xff, 0x0f, - 0xd5, 0xd8, 0x0d, 0xfb, 0x9a, 0xa6, 0x4f, 0xca, 0x43, 0x30, 0xdf, 0x12, 0x47, 0x48, 0xeb, 0x4c, - 0xbc, 0x95, 0xf9, 0xe3, 0xed, 0x42, 0x33, 0xc7, 0xaa, 0x68, 0x1d, 0x9a, 0x64, 0x40, 0xc9, 0x5e, - 0xc4, 0xfc, 0x30, 0x4d, 0x44, 0xe4, 0x6d, 0x9c, 0xdf, 0x3a, 0xf7, 0xfd, 0x02, 0x98, 0x98, 0x26, - 0x11, 0x0b, 0x13, 0x8a, 0x2e, 0x4d, 0x29, 0x55, 0x5e, 0x0f, 0x24, 0x20, 0x2f, 0x55, 0xef, 0x01, - 0x68, 0xfd, 0xa1, 0x9e, 0x1a, 0xaa, 0x95, 0xa2, 0xc7, 0x9d, 0x0c, 0x83, 0x73, 0x78, 0x74, 0x13, - 0x1a, 0x5a, 0x86, 0x3c, 0x55, 0x8b, 0x33, 0x45, 0x67, 0x7d, 0x09, 0x3d, 0x3c, 0x41, 0xa3, 0x6b, - 0x50, 0xe7, 0x82, 0xe4, 0x53, 0x4f, 0xcd, 0xc7, 0xe9, 0xa2, 0xe3, 0xa6, 0x04, 0x60, 0x8d, 0x44, - 0x57, 0xa1, 0xc6, 0x95, 0x89, 0x7a, 0xea, 0xb6, 0x5a, 0x45, 0x9f, 0xfb, 0xc2, 0x8e, 0x15, 0x0e, - 0xbd, 0x09, 0xa6, 0x82, 0x78, 0x4a, 0xc0, 0xec, 0xa2, 0x8f, 0xea, 0xbe, 0x87, 0x33, 0x2c, 0xe7, - 0x37, 0x79, 0xf9, 0x3c, 0x27, 0x21, 0x03, 0x3a, 0x74, 0x2d, 0x53, 0x78, 0xaf, 0x1d, 0x52, 0x4d, - 0x89, 0x7b, 0x28, 0x60, 0xb8, 0x9d, 0xe4, 0x97, 0xbc, 0xbe, 0x93, 0x3e, 0x59, 0xf5, 0xa3, 0xea, - 0xbb, 0x9d, 0x61, 0x70, 0x0e, 0x7f, 0xbc, 0x82, 0xfe, 0x5a, 0x52, 0x0a, 0x6a, 0x83, 0xa9, 0xb9, - 0x5a, 0xcd, 0x46, 0xb6, 0x46, 0x77, 0x01, 0x29, 0xde, 0x91, 0x69, 0xcc, 0xaf, 0x20, 0x2d, 0xe9, - 0xa7, 0xf2, 0xf8, 0x02, 0xce, 0xcc, 0x12, 0x59, 0xfe, 0xc0, 0x79, 0x78, 0x7b, 0x79, 0x9a, 0xcf, - 0xd4, 0xc1, 0x97, 0x61, 0xc9, 0x63, 0x64, 0x34, 0xa4, 0x61, 0xea, 0x72, 0xaa, 0x72, 0x46, 0x71, - 0x20, 0x26, 0xa2, 0x81, 0x17, 0xa7, 0x0c, 0x9f, 0xc7, 0x01, 0xba, 0x00, 0x35, 0xe6, 0x8e, 0xd2, - 0x41, 0x4f, 0xf5, 0xbf, 0x25, 0x2f, 0xde, 0xfd, 0x4d, 0xbe, 0x87, 0x95, 0x0d, 0x5d, 0x87, 0x53, - 0x59, 0xac, 0x91, 0x9b, 0x0e, 0x1c, 0x51, 0x4c, 0x1a, 0x27, 0x56, 0x6d, 0xbd, 0xbc, 0xd1, 0x98, - 0x04, 0xf2, 0xc0, 0x4d, 0x07, 0x0f, 0x94, 0xcd, 0xfe, 0xb1, 0x0c, 0x30, 0x19, 0x73, 0xf4, 0x41, - 0x4e, 0x5d, 0x0c, 0xa1, 0x2e, 0x17, 0x8e, 0xbb, 0x16, 0x45, 0x81, 0xb1, 0x7f, 0x29, 0x4d, 0x74, - 0xe0, 0x22, 0x2c, 0xc6, 0x94, 0xb0, 0xe1, 0x90, 0x86, 0x1e, 0xf5, 0x9c, 0x89, 0x0a, 0xe3, 0x93, - 0xb9, 0xfd, 0x4f, 0xb9, 0xb2, 0x1e, 0x25, 0x19, 0xa5, 0x97, 0x90, 0x8c, 0x7b, 0xb0, 0xac, 0x6b, - 0xf8, 0xc2, 0xed, 0x5a, 0xd0, 0x9e, 0xaa, 0x51, 0x8b, 0x50, 0xde, 0xa3, 0x63, 0xa1, 0xb4, 0x0d, - 0xcc, 0x7f, 0x72, 0x7e, 0xf4, 0xfc, 0xc4, 0xdd, 0x09, 0xa4, 0x78, 0x9a, 0x58, 0x2f, 0xd1, 0x79, - 0x68, 0x4f, 0x75, 0x40, 0x15, 0xbe, 0x95, 0x2f, 0x3c, 0x7a, 0x0d, 0x4e, 0xfa, 0x89, 0xb3, 0xeb, - 0x06, 0x01, 0x97, 0x22, 0x87, 0x1f, 0x5e, 0x17, 0xc7, 0xb4, 0xfd, 0xe4, 0xae, 0xda, 0xfd, 0x98, - 0x8e, 0xed, 0xef, 0xa0, 0x91, 0x31, 0x08, 0xba, 0x5d, 0x68, 0xcb, 0xf9, 0x63, 0x08, 0xe7, 0x90, - 0xae, 0x74, 0x26, 0x4d, 0x29, 0x44, 0x69, 0x14, 0xa3, 0xb4, 0x3d, 0xa8, 0x2b, 0x1a, 0x42, 0xaf, - 0x03, 0x72, 0x85, 0x9c, 0x3a, 0x1e, 0x4d, 0x48, 0xec, 0x47, 0x42, 0x88, 0x65, 0x1b, 0x97, 0xa4, - 0xe5, 0xce, 0xc4, 0x80, 0x2e, 0x81, 0x64, 0xf3, 0x59, 0xa9, 0x56, 0xcf, 0x9f, 0x87, 0xdc, 0xa6, - 0x09, 0xff, 0x43, 0xa8, 0x49, 0xe2, 0x42, 0xef, 0xc2, 0x69, 0xfa, 0x6d, 0x14, 0xf8, 0xc4, 0x4f, - 0x9d, 0xdc, 0x63, 0x9a, 0x37, 0x42, 0x32, 0xbf, 0x89, 0x2d, 0x0d, 0xd8, 0x9c, 0xb1, 0xdb, 0x5f, - 0x82, 0xa9, 0xb9, 0x8c, 0x77, 0x47, 0x25, 0xad, 0x48, 0x41, 0x2f, 0xd1, 0x35, 0x30, 0x3d, 0x46, - 0xe6, 0x9f, 0xaa, 0xb2, 0xc7, 0x88, 0x1d, 0x40, 0x7b, 0x8a, 0xe8, 0x8e, 0x39, 0x7f, 0x13, 0x9a, - 0x2f, 0x4a, 0x36, 0x33, 0xc3, 0x66, 0xbf, 0x0d, 0x30, 0xa1, 0xc4, 0x49, 0x25, 0x8d, 0x7f, 0xac, - 0x64, 0xef, 0x36, 0x34, 0x32, 0x03, 0xea, 0x41, 0x5d, 0x3f, 0x2f, 0x16, 0x67, 0xdf, 0x86, 0xf6, - 0x52, 0x61, 0x70, 0x36, 0x8c, 0xab, 0xc6, 0xd6, 0xad, 0xfd, 0x3f, 0x57, 0x4f, 0xec, 0x3f, 0x5b, - 0x35, 0x7e, 0x7b, 0xb6, 0x6a, 0x3c, 0xfd, 0x6b, 0xd5, 0xf8, 0xea, 0xca, 0x5c, 0x8f, 0x64, 0x75, - 0xd8, 0x4e, 0x4d, 0x6c, 0x5d, 0xfb, 0x3b, 0x00, 0x00, 0xff, 0xff, 0xb3, 0xb5, 0xd3, 0xc9, 0x9d, - 0x0e, 0x00, 0x00, + // 1319 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x57, 0x5d, 0x6f, 0x1b, 0x45, + 0x17, 0xee, 0xc6, 0x8e, 0xbd, 0x3e, 0x8e, 0xf3, 0x31, 0xca, 0x5b, 0x6d, 0xb7, 0x51, 0x92, 0xa6, + 0x7d, 0x51, 0xfa, 0x81, 0x5d, 0xdc, 0x16, 0x28, 0x1f, 0x2d, 0x71, 0x4a, 0x25, 0x8a, 0x20, 0xd5, + 0x14, 0x8a, 0xe0, 0x66, 0xb5, 0x9e, 0x9d, 0xd8, 0x4b, 0xd6, 0x3b, 0xcb, 0xee, 0xba, 0xc5, 0x12, + 0x7f, 0x80, 0x3f, 0xc0, 0x75, 0xaf, 0xb8, 0xe3, 0x67, 0x20, 0xf5, 0x82, 0x0b, 0x2e, 0xb9, 0xaa, + 0xa0, 0xfc, 0x0a, 0x7a, 0x85, 0xe6, 0x6b, 0xbd, 0xf6, 0xd6, 0xc1, 0xad, 0x8a, 0xc4, 0x4d, 0xeb, + 0x99, 0xf3, 0x9c, 0xb3, 0x67, 0xce, 0x39, 0xf3, 0x3c, 0x13, 0xd8, 0xe9, 0xb1, 0x56, 0x14, 0xb3, + 0x94, 0x11, 0x16, 0x24, 0x2d, 0xe2, 0x46, 0xe9, 0x30, 0xa6, 0xfa, 0xff, 0xa6, 0xb0, 0xa0, 0xaa, + 0x5a, 0xda, 0x1b, 0x13, 0xe0, 0xc3, 0x80, 0x3d, 0x14, 0xff, 0x48, 0x98, 0xbd, 0xde, 0x63, 0x3d, + 0x26, 0x7e, 0xb6, 0xf8, 0x2f, 0xb9, 0xbb, 0xf3, 0x63, 0x03, 0xaa, 0x98, 0x7e, 0x33, 0xa4, 0x49, + 0x8a, 0xce, 0x43, 0x39, 0x89, 0x28, 0xb1, 0x8c, 0x6d, 0x63, 0xb7, 0xde, 0xfe, 0x5f, 0x53, 0x7f, + 0x46, 0xd9, 0x9b, 0xf7, 0x22, 0x4a, 0xb0, 0x80, 0xa0, 0x6b, 0x60, 0x7a, 0x7e, 0x42, 0xd8, 0x03, + 0x1a, 0x5b, 0x0b, 0x02, 0x7e, 0xaa, 0x00, 0xbf, 0xa5, 0x00, 0x38, 0x83, 0x72, 0xb7, 0x07, 0x6e, + 0xe0, 0x7b, 0x6e, 0x4a, 0xad, 0xd2, 0x0c, 0xb7, 0xfb, 0x0a, 0x80, 0x33, 0x28, 0xba, 0x04, 0x8b, + 0x6e, 0x14, 0x05, 0x23, 0xab, 0x2c, 0x7c, 0x4e, 0x16, 0x7c, 0xf6, 0xb8, 0x15, 0x4b, 0x10, 0x3f, + 0x06, 0x8b, 0x68, 0x68, 0x2d, 0xce, 0x38, 0xc6, 0x41, 0x44, 0x43, 0x2c, 0x20, 0xe8, 0x06, 0xd4, + 0x5d, 0x72, 0x14, 0xb2, 0x87, 0x01, 0xf5, 0x7a, 0xd4, 0xaa, 0x08, 0x8f, 0x8d, 0x62, 0xf8, 0x31, + 0x06, 0xe7, 0x1d, 0xd0, 0x69, 0x30, 0xfd, 0x30, 0xa5, 0x71, 0xe8, 0x06, 0x96, 0xb7, 0x6d, 0xec, + 0x2e, 0xe1, 0xda, 0x39, 0xbd, 0x61, 0x7f, 0x6f, 0x40, 0x99, 0x97, 0x0c, 0xdd, 0x86, 0x65, 0xc2, + 0xc2, 0x90, 0x92, 0x94, 0xc5, 0x4e, 0x3a, 0x8a, 0xa8, 0xa8, 0xf0, 0x72, 0x7b, 0xab, 0x29, 0xda, + 0xb3, 0x2f, 0xbf, 0xc6, 0xa1, 0xcd, 0x7d, 0x8d, 0xfb, 0x6c, 0x14, 0x51, 0xdc, 0x20, 0xf9, 0x25, + 0xba, 0x0e, 0x75, 0xc2, 0xc2, 0x43, 0xbf, 0xe7, 0x7c, 0x9d, 0xb0, 0x50, 0xd4, 0x7d, 0xa9, 0xb3, + 0xf1, 0xec, 0xc9, 0x96, 0x45, 0x43, 0xc2, 0x3c, 0x3f, 0xec, 0xb5, 0xb8, 0xa1, 0x89, 0xdd, 0x87, + 0x9f, 0xd0, 0x24, 0x71, 0x7b, 0x14, 0x57, 0xa4, 0x83, 0xfd, 0x9b, 0x01, 0xa6, 0xee, 0x07, 0xfa, + 0x08, 0xca, 0xa1, 0x3b, 0x90, 0x1d, 0xa8, 0x75, 0xae, 0x3d, 0x7b, 0xb2, 0xf5, 0x46, 0xcf, 0x4f, + 0xfb, 0xc3, 0x6e, 0x93, 0xb0, 0x41, 0x8b, 0x26, 0xe9, 0xd0, 0x8d, 0x47, 0x72, 0x7e, 0x0a, 0x13, + 0xa5, 0xb3, 0xc5, 0x22, 0xc4, 0x7f, 0xe1, 0x68, 0x8f, 0xca, 0x60, 0xea, 0x99, 0xc9, 0x8e, 0x66, + 0xfc, 0x1b, 0x47, 0x5b, 0x78, 0x15, 0x47, 0x2b, 0xcd, 0x7f, 0x34, 0xf4, 0x3e, 0x98, 0x5d, 0x3f, + 0xe4, 0x90, 0xc4, 0x2a, 0x6f, 0x97, 0x76, 0xeb, 0xed, 0x33, 0x33, 0xaf, 0x4b, 0xb3, 0x23, 0x91, + 0x38, 0x73, 0x41, 0x57, 0x61, 0x29, 0x70, 0x93, 0xd4, 0x51, 0x2e, 0xea, 0x42, 0xac, 0x15, 0xf2, + 0xc7, 0x75, 0x0e, 0x53, 0x1b, 0xe8, 0x8c, 0xf2, 0x7a, 0x40, 0xe3, 0xc4, 0x67, 0xa1, 0xb8, 0x14, + 0x35, 0x09, 0xb9, 0x2f, 0xb7, 0xec, 0x9f, 0x0c, 0xa8, 0xaa, 0xcf, 0xa1, 0x3b, 0xb0, 0x1e, 0xd3, + 0x84, 0x0d, 0x63, 0x42, 0x9d, 0xfc, 0x39, 0x8d, 0x39, 0xce, 0xb9, 0xac, 0x3d, 0xf7, 0xe5, 0x79, + 0xdf, 0x01, 0x20, 0x2c, 0x08, 0x28, 0x49, 0x7d, 0x35, 0x04, 0xf5, 0xf6, 0xba, 0x4a, 0x37, 0xdb, + 0xe7, 0x19, 0x77, 0xca, 0x8f, 0x9f, 0x6c, 0x9d, 0xc0, 0x39, 0x34, 0xb2, 0xc1, 0xec, 0xba, 0xe4, + 0xe8, 0xd0, 0x0f, 0x02, 0x51, 0xe3, 0x06, 0xce, 0xd6, 0xf6, 0xef, 0x06, 0x2c, 0x0a, 0x8a, 0x40, + 0x17, 0x41, 0xb3, 0xa5, 0x62, 0xb9, 0xe7, 0x54, 0x43, 0x23, 0x90, 0x05, 0x55, 0x5d, 0x84, 0x05, + 0x51, 0x04, 0xbd, 0x2c, 0x54, 0xb6, 0xfc, 0x52, 0x95, 0x5d, 0x2c, 0x54, 0x16, 0xbd, 0x05, 0x90, + 0xa4, 0x6e, 0x4a, 0x65, 0x0d, 0x2b, 0x73, 0xd4, 0x70, 0x51, 0xe0, 0x79, 0x4b, 0xca, 0x9c, 0xd8, + 0x5e, 0xd5, 0x09, 0xff, 0x0f, 0x8b, 0xb1, 0x1b, 0xf6, 0x34, 0x4d, 0xaf, 0xc8, 0x20, 0x98, 0x6f, + 0x89, 0x10, 0xd2, 0x3a, 0x95, 0x6f, 0x79, 0xfe, 0x7c, 0x5b, 0x50, 0xcf, 0xb1, 0x2a, 0xda, 0x86, + 0x3a, 0xe9, 0x53, 0x72, 0x14, 0x31, 0x3f, 0x4c, 0x13, 0x91, 0x79, 0x03, 0xe7, 0xb7, 0x76, 0xfe, + 0x5a, 0x01, 0x13, 0xd3, 0x24, 0x62, 0x61, 0x42, 0xd1, 0x85, 0x09, 0xa5, 0xca, 0xeb, 0x81, 0x04, + 0xe4, 0xa5, 0xea, 0x3d, 0x00, 0xad, 0x3f, 0xd4, 0x53, 0x43, 0xb5, 0x51, 0xf4, 0xb8, 0x95, 0x61, + 0x70, 0x0e, 0x8f, 0xae, 0x43, 0x4d, 0xcb, 0x90, 0xa7, 0x6a, 0x71, 0xba, 0xe8, 0xac, 0x2f, 0xa1, + 0x87, 0xc7, 0x68, 0x74, 0x05, 0xaa, 0x5c, 0x90, 0x7c, 0xea, 0xa9, 0xf9, 0x38, 0x55, 0x74, 0xdc, + 0x93, 0x00, 0xac, 0x91, 0xe8, 0x32, 0x54, 0xb8, 0x32, 0x51, 0x4f, 0xdd, 0x56, 0xab, 0xe8, 0x73, + 0x20, 0xec, 0x58, 0xe1, 0xd0, 0x9b, 0x60, 0x2a, 0x88, 0xa7, 0x04, 0xcc, 0x2e, 0xfa, 0xa8, 0xee, + 0x7b, 0x38, 0xc3, 0x72, 0x7e, 0x93, 0x97, 0xcf, 0x73, 0x12, 0xd2, 0xa7, 0x03, 0xd7, 0x32, 0x85, + 0xf7, 0xd6, 0x73, 0xaa, 0x29, 0x71, 0xf7, 0x04, 0x0c, 0x37, 0x92, 0xfc, 0x92, 0xd7, 0x77, 0xdc, + 0x27, 0xab, 0x3a, 0xab, 0xbe, 0xfb, 0x19, 0x06, 0xe7, 0xf0, 0x3c, 0x0b, 0x7d, 0x4d, 0x9d, 0x2e, + 0xed, 0xf9, 0xa1, 0x55, 0x9b, 0x95, 0x45, 0x47, 0xe1, 0x3a, 0x1c, 0x86, 0x1b, 0xdd, 0xfc, 0x12, + 0x1d, 0xc0, 0x5a, 0x16, 0x87, 0xb0, 0x41, 0x14, 0xd0, 0x94, 0x5a, 0x20, 0x42, 0xed, 0xcc, 0x0e, + 0xb5, 0xaf, 0x90, 0x78, 0xb5, 0x3b, 0xb5, 0x73, 0xbc, 0xb4, 0xff, 0xb2, 0xa0, 0xa4, 0xdd, 0x06, + 0x53, 0x8b, 0x88, 0x1a, 0xda, 0x6c, 0x8d, 0x6e, 0x03, 0x52, 0x84, 0x28, 0xeb, 0x3b, 0xbf, 0xb4, + 0x2d, 0x49, 0x3f, 0x55, 0xe0, 0x2f, 0xe0, 0xf4, 0x34, 0xc3, 0xe6, 0x03, 0xce, 0x23, 0x28, 0xeb, + 0x93, 0x44, 0xab, 0x02, 0x5f, 0x84, 0x35, 0x8f, 0x91, 0xe1, 0x80, 0x86, 0xa9, 0xcb, 0x39, 0xd4, + 0x19, 0xc6, 0x81, 0x18, 0xd5, 0x1a, 0x5e, 0x9d, 0x30, 0x7c, 0x1e, 0x07, 0xe8, 0x1c, 0x54, 0x98, + 0x3b, 0x4c, 0xfb, 0x6d, 0x35, 0x98, 0x4b, 0x92, 0x11, 0x0e, 0xf6, 0xf8, 0x1e, 0x56, 0x36, 0x74, + 0x15, 0x4e, 0x66, 0xb9, 0x46, 0x6e, 0xda, 0x77, 0x44, 0x97, 0x69, 0x9c, 0x58, 0x95, 0xed, 0xd2, + 0x6e, 0x6d, 0x9c, 0xc8, 0x5d, 0x37, 0xed, 0xdf, 0x55, 0x36, 0xfb, 0x87, 0x12, 0xc0, 0xf8, 0xfe, + 0xa1, 0x0f, 0x72, 0xb2, 0x67, 0x08, 0xd9, 0x3b, 0x77, 0xdc, 0x7d, 0x2d, 0x2a, 0x9f, 0xfd, 0xf3, + 0xc2, 0x58, 0xa0, 0xce, 0xc3, 0x6a, 0x4c, 0x09, 0x1b, 0x0c, 0x68, 0xe8, 0x51, 0xcf, 0x19, 0x3f, + 0x0f, 0xf0, 0x4a, 0x6e, 0xff, 0x53, 0x2e, 0xf9, 0xb3, 0xb4, 0x6c, 0xe1, 0x25, 0xb4, 0xec, 0x0e, + 0xac, 0xeb, 0x1a, 0xbe, 0x70, 0xbb, 0x96, 0xb5, 0xa7, 0x6a, 0xd4, 0x2a, 0x94, 0x8e, 0xe8, 0x48, + 0x3c, 0x01, 0x6a, 0x98, 0xff, 0xe4, 0xc4, 0xed, 0xf9, 0x89, 0xdb, 0x0d, 0xa4, 0xaa, 0x9b, 0x58, + 0x2f, 0xd1, 0x59, 0x68, 0x4c, 0x74, 0x40, 0x15, 0x7e, 0x29, 0x5f, 0x78, 0xf4, 0x1a, 0xac, 0xf8, + 0x89, 0x73, 0xe8, 0x06, 0x01, 0x9f, 0x7b, 0x87, 0x07, 0xaf, 0x8a, 0x30, 0x0d, 0x3f, 0xb9, 0xad, + 0x76, 0x3f, 0xa6, 0x23, 0xfb, 0x3b, 0xa8, 0x65, 0xd4, 0x86, 0x6e, 0x16, 0xda, 0x72, 0xf6, 0x18, + 0x26, 0x7c, 0x4e, 0x57, 0x9a, 0xe3, 0xa6, 0x14, 0xb2, 0x34, 0x8a, 0x59, 0xda, 0x1e, 0x54, 0x15, + 0x3f, 0xa2, 0xd7, 0x01, 0xb9, 0x42, 0xe7, 0x1d, 0x8f, 0x26, 0x24, 0xf6, 0x23, 0xf1, 0x42, 0x90, + 0x6d, 0x5c, 0x93, 0x96, 0x5b, 0x63, 0x03, 0xba, 0x00, 0x52, 0x66, 0xa6, 0xdf, 0x10, 0xea, 0x5d, + 0x76, 0x8f, 0xdb, 0xb4, 0x12, 0x7d, 0x08, 0x15, 0xc9, 0xa8, 0xe8, 0x5d, 0x38, 0x45, 0xbf, 0x8d, + 0x02, 0x9f, 0xf8, 0xa9, 0x93, 0x7b, 0xe5, 0xf3, 0x46, 0x48, 0x49, 0x32, 0xb1, 0xa5, 0x01, 0x7b, + 0x53, 0x76, 0xfb, 0x4b, 0x30, 0x35, 0xc9, 0xf2, 0xee, 0xa8, 0x43, 0x2b, 0x52, 0xd0, 0x4b, 0x74, + 0x05, 0x4c, 0x8f, 0x91, 0xf9, 0xa7, 0xaa, 0xe4, 0x31, 0x62, 0x07, 0xd0, 0x98, 0x60, 0xe0, 0x63, + 0xe2, 0xef, 0x41, 0xfd, 0x45, 0xc9, 0x66, 0x6a, 0xd8, 0xec, 0xb7, 0x01, 0xc6, 0x5c, 0x3d, 0xae, + 0xa4, 0xf1, 0xcf, 0x95, 0x3c, 0x0f, 0x8d, 0x09, 0x8e, 0x9e, 0x9d, 0xa7, 0x7d, 0x09, 0x56, 0xa7, + 0x39, 0x78, 0x36, 0xba, 0x7d, 0x13, 0x6a, 0xd9, 0x17, 0x51, 0x1b, 0xaa, 0xfa, 0x41, 0xb5, 0x3a, + 0xfd, 0x1a, 0xb6, 0xd7, 0x0a, 0x13, 0xb9, 0x6b, 0x5c, 0x36, 0x3a, 0x37, 0x1e, 0xff, 0xb1, 0x79, + 0xe2, 0xf1, 0xd3, 0x4d, 0xe3, 0xd7, 0xa7, 0x9b, 0xc6, 0xa3, 0x3f, 0x37, 0x8d, 0xaf, 0x2e, 0xcd, + 0xf5, 0x67, 0x81, 0x0a, 0xd6, 0xad, 0x88, 0xad, 0x2b, 0x7f, 0x07, 0x00, 0x00, 0xff, 0xff, 0x05, + 0x6b, 0xaa, 0x26, 0x8f, 0x0f, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -1737,6 +1835,30 @@ func (m *Response) MarshalToSizedBuffer(dAtA []byte) (int, error) { i-- dAtA[i] = 0xa2 } + if m.BackfillComplete != nil { + { + size, err := m.BackfillComplete.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintCapture(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x52 + } + if m.BackfillBegin != nil { + { + size, err := m.BackfillBegin.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintCapture(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x4a + } if m.SourcedSchema != nil { { size, err := m.SourcedSchema.MarshalToSizedBuffer(dAtA[:i]) @@ -2314,6 +2436,70 @@ func (m *Response_Checkpoint) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *Response_BackfillBegin) Marshal() (dAtA []byte, err error) { + size := m.ProtoSize() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Response_BackfillBegin) MarshalTo(dAtA []byte) (int, error) { + size := m.ProtoSize() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Response_BackfillBegin) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.Binding != 0 { + i = encodeVarintCapture(dAtA, i, uint64(m.Binding)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *Response_BackfillComplete) Marshal() (dAtA []byte, err error) { + size := m.ProtoSize() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Response_BackfillComplete) MarshalTo(dAtA []byte) (int, error) { + size := m.ProtoSize() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Response_BackfillComplete) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.Binding != 0 { + i = encodeVarintCapture(dAtA, i, uint64(m.Binding)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + func encodeVarintCapture(dAtA []byte, offset int, v uint64) int { offset -= sovCapture(v) base := offset @@ -2578,6 +2764,14 @@ func (m *Response) ProtoSize() (n int) { l = m.SourcedSchema.ProtoSize() n += 1 + l + sovCapture(uint64(l)) } + if m.BackfillBegin != nil { + l = m.BackfillBegin.ProtoSize() + n += 1 + l + sovCapture(uint64(l)) + } + if m.BackfillComplete != nil { + l = m.BackfillComplete.ProtoSize() + n += 1 + l + sovCapture(uint64(l)) + } l = len(m.Internal) if l > 0 { n += 2 + l + sovCapture(uint64(l)) @@ -2810,6 +3004,36 @@ func (m *Response_Checkpoint) ProtoSize() (n int) { return n } +func (m *Response_BackfillBegin) ProtoSize() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Binding != 0 { + n += 1 + sovCapture(uint64(m.Binding)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *Response_BackfillComplete) ProtoSize() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Binding != 0 { + n += 1 + sovCapture(uint64(m.Binding)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + func sovCapture(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -4529,6 +4753,78 @@ func (m *Response) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 9: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field BackfillBegin", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCapture + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthCapture + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthCapture + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.BackfillBegin == nil { + m.BackfillBegin = &Response_BackfillBegin{} + } + if err := m.BackfillBegin.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 10: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field BackfillComplete", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCapture + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthCapture + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthCapture + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.BackfillComplete == nil { + m.BackfillComplete = &Response_BackfillComplete{} + } + if err := m.BackfillComplete.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex case 100: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Internal", wireType) @@ -5816,6 +6112,146 @@ func (m *Response_Checkpoint) Unmarshal(dAtA []byte) error { } return nil } +func (m *Response_BackfillBegin) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCapture + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: BackfillBegin: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: BackfillBegin: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Binding", wireType) + } + m.Binding = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCapture + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Binding |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipCapture(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthCapture + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Response_BackfillComplete) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCapture + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: BackfillComplete: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: BackfillComplete: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Binding", wireType) + } + m.Binding = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCapture + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Binding |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipCapture(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthCapture + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipCapture(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/go/protocols/capture/capture.proto b/go/protocols/capture/capture.proto index 1275dcb280c..043dbc35f8a 100644 --- a/go/protocols/capture/capture.proto +++ b/go/protocols/capture/capture.proto @@ -379,6 +379,26 @@ message Response { } Checkpoint checkpoint = 7; + // Signals the start of a backfill for a binding. + // + // A control message (BackfillBegin or BackfillComplete) must stand alone + // in its connector checkpoint: the checkpoint must contain only the + // control message followed by the terminating Checkpoint response, with + // no Captured, SourcedSchema, or other control messages. The runtime + // enforces this rule and will fail the session on violation. + message BackfillBegin { + uint32 binding = 1; + } + BackfillBegin backfill_begin = 9; + + // Signals the end of a backfill for a binding. + // + // See BackfillBegin for the "stands alone in its checkpoint" rule. + message BackfillComplete { + uint32 binding = 1; + } + BackfillComplete backfill_complete = 10; + // Reserved for internal use. bytes internal = 100 [ json_name = "$internal" ]; } diff --git a/go/protocols/materialize/materialize.pb.go b/go/protocols/materialize/materialize.pb.go index 6bbed513a80..5f5cfafc87e 100644 --- a/go/protocols/materialize/materialize.pb.go +++ b/go/protocols/materialize/materialize.pb.go @@ -11,6 +11,7 @@ import ( github_com_estuary_flow_go_protocols_flow "github.com/estuary/flow/go/protocols/flow" _ "github.com/gogo/protobuf/gogoproto" proto "github.com/gogo/protobuf/proto" + types "github.com/gogo/protobuf/types" protocol "go.gazette.dev/core/consumer/protocol" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" @@ -507,10 +508,12 @@ type Request_Flush struct { // patches (the runtime feeds the shard's contribution back to it for // symmetry with the scaled-out case). Connectors participating in // cooperative multi-shard strategies use this to observe peers' state. - StatePatchesJson encoding_json.RawMessage `protobuf:"bytes,1,opt,name=state_patches_json,json=statePatches,proto3,casttype=encoding/json.RawMessage" json:"state_patches_json,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + StatePatchesJson encoding_json.RawMessage `protobuf:"bytes,1,opt,name=state_patches_json,json=statePatches,proto3,casttype=encoding/json.RawMessage" json:"state_patches_json,omitempty"` + BackfillBegins []*Request_Flush_BackfillBegin `protobuf:"bytes,2,rep,name=backfill_begins,json=backfillBegins,proto3" json:"backfill_begins,omitempty"` + BackfillCompletes []*Request_Flush_BackfillComplete `protobuf:"bytes,3,rep,name=backfill_completes,json=backfillCompletes,proto3" json:"backfill_completes,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *Request_Flush) Reset() { *m = Request_Flush{} } @@ -546,6 +549,100 @@ func (m *Request_Flush) XXX_DiscardUnknown() { var xxx_messageInfo_Request_Flush proto.InternalMessageInfo +// Backfill-begin signals observed during this transaction. Populated only on +// shard zero. +type Request_Flush_BackfillBegin struct { + Binding uint32 `protobuf:"varint,1,opt,name=binding,proto3" json:"binding,omitempty"` + // Truncation boundary of the binding's backfill: documents published at or + // after this time are current, while earlier ones were superseded by the + // backfill. It equals the begin's own publication time (flow_published_at), + // and is carried on both begin and complete so connectors need not track it + // across transactions. + Timestamp *types.Timestamp `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Request_Flush_BackfillBegin) Reset() { *m = Request_Flush_BackfillBegin{} } +func (m *Request_Flush_BackfillBegin) String() string { return proto.CompactTextString(m) } +func (*Request_Flush_BackfillBegin) ProtoMessage() {} +func (*Request_Flush_BackfillBegin) Descriptor() ([]byte, []int) { + return fileDescriptor_3e8b62b327f34bc6, []int{0, 5, 0} +} +func (m *Request_Flush_BackfillBegin) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Request_Flush_BackfillBegin) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Request_Flush_BackfillBegin.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Request_Flush_BackfillBegin) XXX_Merge(src proto.Message) { + xxx_messageInfo_Request_Flush_BackfillBegin.Merge(m, src) +} +func (m *Request_Flush_BackfillBegin) XXX_Size() int { + return m.ProtoSize() +} +func (m *Request_Flush_BackfillBegin) XXX_DiscardUnknown() { + xxx_messageInfo_Request_Flush_BackfillBegin.DiscardUnknown(m) +} + +var xxx_messageInfo_Request_Flush_BackfillBegin proto.InternalMessageInfo + +// Backfill-complete signals observed during this transaction. Populated only +// on shard zero. +type Request_Flush_BackfillComplete struct { + Binding uint32 `protobuf:"varint,1,opt,name=binding,proto3" json:"binding,omitempty"` + // Truncation boundary of the completed backfill (see BackfillBegin.timestamp): + // the connector may delete destination rows whose flow_published_at predates + // this time, as they were superseded by the backfill. + Timestamp *types.Timestamp `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Request_Flush_BackfillComplete) Reset() { *m = Request_Flush_BackfillComplete{} } +func (m *Request_Flush_BackfillComplete) String() string { return proto.CompactTextString(m) } +func (*Request_Flush_BackfillComplete) ProtoMessage() {} +func (*Request_Flush_BackfillComplete) Descriptor() ([]byte, []int) { + return fileDescriptor_3e8b62b327f34bc6, []int{0, 5, 1} +} +func (m *Request_Flush_BackfillComplete) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Request_Flush_BackfillComplete) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Request_Flush_BackfillComplete.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Request_Flush_BackfillComplete) XXX_Merge(src proto.Message) { + xxx_messageInfo_Request_Flush_BackfillComplete.Merge(m, src) +} +func (m *Request_Flush_BackfillComplete) XXX_Size() int { + return m.ProtoSize() +} +func (m *Request_Flush_BackfillComplete) XXX_DiscardUnknown() { + xxx_messageInfo_Request_Flush_BackfillComplete.DiscardUnknown(m) +} + +var xxx_messageInfo_Request_Flush_BackfillComplete proto.InternalMessageInfo + // Store documents updated by the current transaction. // // The runtime populates exactly one of the JSON encodings (`key_json`, @@ -1479,6 +1576,8 @@ func init() { proto.RegisterType((*Request_Open)(nil), "materialize.Request.Open") proto.RegisterType((*Request_Load)(nil), "materialize.Request.Load") proto.RegisterType((*Request_Flush)(nil), "materialize.Request.Flush") + proto.RegisterType((*Request_Flush_BackfillBegin)(nil), "materialize.Request.Flush.BackfillBegin") + proto.RegisterType((*Request_Flush_BackfillComplete)(nil), "materialize.Request.Flush.BackfillComplete") proto.RegisterType((*Request_Store)(nil), "materialize.Request.Store") proto.RegisterType((*Request_StartCommit)(nil), "materialize.Request.StartCommit") proto.RegisterType((*Request_Acknowledge)(nil), "materialize.Request.Acknowledge") @@ -1506,128 +1605,135 @@ func init() { } var fileDescriptor_3e8b62b327f34bc6 = []byte{ - // 1921 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0xcf, 0x6f, 0x1b, 0xc7, - 0x15, 0xf6, 0x52, 0xfc, 0xf9, 0x48, 0x59, 0xd4, 0x84, 0x8a, 0xe9, 0x8d, 0x63, 0xcb, 0x4a, 0x82, - 0x08, 0x2e, 0x42, 0x19, 0x72, 0xda, 0xc4, 0x0e, 0x5c, 0x94, 0xa4, 0x28, 0x80, 0xaa, 0x24, 0x2a, - 0x23, 0xcb, 0x01, 0x72, 0x21, 0x46, 0xbb, 0x23, 0x6a, 0xad, 0xe5, 0xce, 0x76, 0x67, 0x69, 0x9b, - 0xbd, 0x17, 0x45, 0x7b, 0x6a, 0x8f, 0x0d, 0x7a, 0xe8, 0xa9, 0xd7, 0x9e, 0x0a, 0x14, 0xbd, 0x16, - 0x05, 0x7c, 0xec, 0x5f, 0xe0, 0xa2, 0xe9, 0x3f, 0xd0, 0x4b, 0x2f, 0x39, 0x15, 0xf3, 0x63, 0x97, - 0x4b, 0x9a, 0xa4, 0x28, 0xc0, 0xcd, 0x85, 0xd8, 0x79, 0xf3, 0x7d, 0x6f, 0xdf, 0xbc, 0x79, 0x33, - 0xef, 0xe3, 0xc2, 0xbd, 0x1e, 0xdb, 0xf2, 0x03, 0x16, 0x32, 0x8b, 0xb9, 0x7c, 0xab, 0x4f, 0x42, - 0x1a, 0x38, 0xc4, 0x75, 0x7e, 0x4e, 0x93, 0xcf, 0x35, 0x89, 0x40, 0xc5, 0x84, 0xc9, 0x5c, 0xb7, - 0x98, 0xc7, 0x07, 0x7d, 0x1a, 0xc4, 0xf4, 0xf8, 0x41, 0xc1, 0xcd, 0x5b, 0x63, 0xae, 0xcf, 0x5c, - 0xf6, 0x42, 0xfe, 0xe8, 0xd9, 0x4a, 0x8f, 0xf5, 0x98, 0x7c, 0xdc, 0x12, 0x4f, 0xca, 0xba, 0xf1, - 0xa7, 0x35, 0xc8, 0x61, 0xfa, 0xb3, 0x01, 0xe5, 0x21, 0xfa, 0x04, 0xd2, 0xdc, 0xa7, 0x56, 0xd5, - 0x58, 0x37, 0x36, 0x8b, 0xdb, 0x37, 0x6b, 0xc9, 0x80, 0x34, 0xa6, 0x76, 0xec, 0x53, 0x0b, 0x4b, - 0x18, 0x7a, 0x08, 0xf9, 0xe7, 0xc4, 0x75, 0x6c, 0x12, 0xd2, 0x6a, 0x4a, 0x52, 0xde, 0x9f, 0x4a, - 0x79, 0xaa, 0x41, 0x38, 0x86, 0xa3, 0xfb, 0x90, 0x21, 0xbe, 0xef, 0x0e, 0xab, 0x4b, 0x92, 0x67, - 0x4e, 0xe5, 0xd5, 0x05, 0x02, 0x2b, 0xa0, 0x88, 0x8d, 0xf9, 0xd4, 0xab, 0xa6, 0xe7, 0xc4, 0xd6, - 0xf1, 0xa9, 0x87, 0x25, 0x4c, 0xc0, 0x5d, 0x46, 0xec, 0x6a, 0x66, 0x0e, 0x7c, 0x9f, 0x11, 0x1b, - 0x4b, 0x98, 0x88, 0xe7, 0xcc, 0x1d, 0xf0, 0xf3, 0x6a, 0x76, 0x4e, 0x3c, 0xbb, 0x02, 0x81, 0x15, - 0x50, 0x30, 0x78, 0xc8, 0x02, 0x5a, 0xcd, 0xcd, 0x61, 0x1c, 0x0b, 0x04, 0x56, 0x40, 0xd4, 0x84, - 0x12, 0x0f, 0x49, 0x10, 0x76, 0x2d, 0xd6, 0xef, 0x3b, 0x61, 0x35, 0x2f, 0x89, 0xeb, 0x33, 0x88, - 0x24, 0x08, 0x9b, 0x12, 0x87, 0x8b, 0x7c, 0x34, 0x40, 0x0d, 0x28, 0x12, 0xeb, 0xc2, 0x63, 0x2f, - 0x5c, 0x6a, 0xf7, 0x68, 0xb5, 0x30, 0xc7, 0x47, 0x7d, 0x84, 0xc3, 0x49, 0x12, 0x7a, 0x0f, 0xf2, - 0x8e, 0x17, 0xd2, 0xc0, 0x23, 0x6e, 0xd5, 0x5e, 0x37, 0x36, 0x4b, 0xb8, 0xf0, 0x61, 0x64, 0x30, - 0x7f, 0x6b, 0x40, 0x5a, 0xec, 0x31, 0x3a, 0x84, 0xeb, 0x16, 0xf3, 0x3c, 0x6a, 0x85, 0x2c, 0xe8, - 0x86, 0x43, 0x9f, 0xca, 0xb2, 0xb8, 0xbe, 0xfd, 0x71, 0x4d, 0xd6, 0xd4, 0x41, 0xfc, 0x46, 0x12, - 0x3a, 0xcc, 0x13, 0x94, 0x5a, 0x33, 0xc2, 0x3f, 0x19, 0xfa, 0x14, 0x2f, 0x5b, 0xc9, 0x21, 0x7a, - 0x08, 0x45, 0x8b, 0x79, 0x67, 0x4e, 0xaf, 0xfb, 0x8c, 0x33, 0x4f, 0x16, 0x4c, 0xa9, 0x71, 0xeb, - 0xbb, 0xd7, 0x77, 0xaa, 0xd4, 0xb3, 0x98, 0xed, 0x78, 0xbd, 0x2d, 0x31, 0x51, 0xc3, 0xe4, 0xc5, - 0x01, 0xe5, 0x9c, 0xf4, 0x28, 0xce, 0x2a, 0x82, 0xf9, 0x97, 0x2c, 0xe4, 0xa3, 0x22, 0x42, 0x5f, - 0x42, 0xda, 0x23, 0x7d, 0x15, 0x4d, 0xa1, 0xf1, 0xf8, 0xbb, 0xd7, 0x77, 0x1e, 0xf6, 0x9c, 0xf0, - 0x7c, 0x70, 0x5a, 0xb3, 0x58, 0x7f, 0x8b, 0xf2, 0x70, 0x40, 0x82, 0xa1, 0x2a, 0xfe, 0x37, 0x8e, - 0xc3, 0x64, 0xd4, 0x58, 0xba, 0x9a, 0xb2, 0xd4, 0xd4, 0xdb, 0x5c, 0xea, 0xd2, 0xe2, 0x4b, 0x45, - 0x75, 0xc8, 0x9f, 0x3a, 0x9e, 0x80, 0xf0, 0x6a, 0x7a, 0x7d, 0x69, 0xb3, 0xb8, 0xfd, 0xd1, 0xdc, - 0x33, 0x55, 0x6b, 0x28, 0x34, 0x8e, 0x69, 0x68, 0x1f, 0x2a, 0x2e, 0xe1, 0x61, 0xb7, 0x3f, 0x1e, - 0x76, 0x7c, 0x14, 0x66, 0xad, 0x09, 0xbf, 0x23, 0x68, 0x13, 0x13, 0xe8, 0x2e, 0x94, 0xa4, 0xb7, - 0xe7, 0x34, 0xe0, 0xc2, 0x8b, 0x38, 0x20, 0x05, 0x5c, 0x14, 0xb6, 0xa7, 0xca, 0x64, 0xfe, 0x6e, - 0x09, 0x72, 0x3a, 0x0c, 0xb4, 0x07, 0x95, 0x80, 0x72, 0x36, 0x08, 0x2c, 0xda, 0x4d, 0xe6, 0xc0, - 0x58, 0x20, 0x07, 0xd7, 0x23, 0x66, 0x53, 0xe5, 0xe2, 0x11, 0x80, 0xc5, 0x5c, 0x97, 0x5a, 0x32, - 0x7c, 0x75, 0xc3, 0x54, 0x54, 0xf8, 0xcd, 0xd8, 0x2e, 0x22, 0x6f, 0xa4, 0x5f, 0xbd, 0xbe, 0x73, - 0x0d, 0x27, 0xd0, 0xe8, 0x97, 0x06, 0xac, 0x9d, 0x39, 0xd4, 0xb5, 0x93, 0x51, 0x74, 0xfb, 0xc4, - 0xaf, 0x2e, 0xc9, 0xac, 0x3e, 0x5e, 0x28, 0xab, 0xb5, 0x5d, 0xe1, 0x42, 0x85, 0xb3, 0xc7, 0x99, - 0x77, 0x40, 0xfc, 0x96, 0x17, 0x06, 0xc3, 0xc6, 0xad, 0x5f, 0xff, 0x73, 0xce, 0x42, 0x8a, 0x67, - 0x23, 0x1a, 0x32, 0x21, 0x7f, 0x4a, 0xac, 0x8b, 0x33, 0xc7, 0x75, 0xe5, 0xe5, 0xb5, 0x8c, 0xe3, - 0x31, 0xba, 0x09, 0xf9, 0x5e, 0xc0, 0x06, 0x7e, 0xf7, 0x74, 0x58, 0xcd, 0xac, 0x2f, 0x6d, 0x16, - 0x70, 0x4e, 0x8e, 0x1b, 0x43, 0xb3, 0x05, 0x37, 0x66, 0xbc, 0x1c, 0x95, 0x61, 0xe9, 0x82, 0x0e, - 0xd5, 0x01, 0xc0, 0xe2, 0x11, 0x55, 0x20, 0xf3, 0x9c, 0xb8, 0x03, 0x55, 0xb7, 0x25, 0xac, 0x06, - 0x8f, 0x52, 0x9f, 0x1b, 0xe6, 0x6f, 0x52, 0x90, 0x91, 0xf7, 0x28, 0x6a, 0xc2, 0xca, 0x64, 0x45, - 0x18, 0x97, 0x55, 0xc4, 0x24, 0x03, 0x55, 0x21, 0x17, 0x15, 0x42, 0x4a, 0xbe, 0x3e, 0x1a, 0xce, - 0xac, 0xba, 0xf4, 0x5b, 0xa9, 0xba, 0xcc, 0x1b, 0x55, 0x87, 0x3e, 0x03, 0xe0, 0x21, 0x09, 0xa9, - 0xaa, 0xaf, 0xec, 0x02, 0xf5, 0x95, 0x91, 0x78, 0xf3, 0xef, 0x06, 0xa4, 0x45, 0xa7, 0xf8, 0x7f, - 0x67, 0xe4, 0x23, 0xc8, 0x04, 0xc4, 0xeb, 0x51, 0xdd, 0xe3, 0x56, 0x94, 0x53, 0x2c, 0x4c, 0xd2, - 0x95, 0x9a, 0x9d, 0x58, 0x47, 0x7a, 0xf1, 0x75, 0x84, 0x90, 0x16, 0x1d, 0x4c, 0x44, 0xa0, 0xcf, - 0xbe, 0x0c, 0x7f, 0x19, 0x47, 0x43, 0xf4, 0x00, 0xf2, 0x17, 0x74, 0xb8, 0xf8, 0x7d, 0x2b, 0x6b, - 0xe9, 0x7d, 0x00, 0x41, 0xf2, 0x89, 0x75, 0x41, 0x6d, 0x75, 0x77, 0xe1, 0xc2, 0x05, 0x1d, 0x1e, - 0x49, 0x83, 0xd9, 0x81, 0x8c, 0xec, 0x83, 0x68, 0x17, 0x90, 0x8a, 0xdb, 0x27, 0xa1, 0x75, 0x4e, - 0xf9, 0xe2, 0xe7, 0xbc, 0x24, 0x79, 0x47, 0x8a, 0x66, 0xfe, 0x35, 0x05, 0x19, 0xd9, 0x27, 0xbf, - 0xdf, 0x85, 0x88, 0x4b, 0x5a, 0x1e, 0x13, 0xbe, 0x78, 0xe2, 0xb3, 0x8a, 0x80, 0x3e, 0x80, 0x65, - 0x4d, 0xd5, 0xce, 0x33, 0xd2, 0x79, 0x49, 0x19, 0xb5, 0xff, 0x07, 0x90, 0xb7, 0x99, 0xb5, 0x78, - 0x75, 0x2e, 0xd9, 0xcc, 0x42, 0xef, 0x42, 0x96, 0xbe, 0x74, 0x78, 0xc8, 0xa5, 0xac, 0xc8, 0x63, - 0x3d, 0x12, 0x76, 0x9b, 0xba, 0x34, 0xa4, 0x52, 0x35, 0xe4, 0xb1, 0x1e, 0x99, 0xdf, 0x18, 0x50, - 0x4c, 0x68, 0x05, 0xd4, 0x04, 0x14, 0x0c, 0xbc, 0xd0, 0xe9, 0xd3, 0xae, 0x75, 0x4e, 0xad, 0x0b, - 0x9f, 0x39, 0x5e, 0xa8, 0xab, 0xba, 0x52, 0x8b, 0x04, 0x64, 0xad, 0x19, 0xcf, 0xe1, 0x55, 0x8d, - 0x1f, 0x99, 0x66, 0xec, 0x6c, 0xea, 0xca, 0x3b, 0x7b, 0x02, 0xc5, 0x84, 0x06, 0x79, 0x5b, 0x05, - 0xb3, 0xf1, 0x0d, 0x82, 0x3c, 0xa6, 0xdc, 0x67, 0x1e, 0xa7, 0xa8, 0x36, 0x26, 0x59, 0x27, 0x55, - 0x98, 0x02, 0x25, 0x35, 0xeb, 0x63, 0x28, 0x44, 0x22, 0xd4, 0xd6, 0x2d, 0xe5, 0xce, 0x74, 0x52, - 0xd4, 0x0b, 0x6c, 0x3c, 0x62, 0xa0, 0xcf, 0x20, 0x27, 0xe4, 0xa8, 0xa3, 0x0b, 0xea, 0x4d, 0xc5, - 0xab, 0xc9, 0x75, 0x05, 0xc2, 0x11, 0x1a, 0x7d, 0x0a, 0x59, 0xa1, 0x4b, 0xa9, 0xad, 0x2f, 0xc4, - 0x5b, 0xd3, 0x79, 0x1d, 0x89, 0xc1, 0x1a, 0x2b, 0x58, 0x42, 0x9e, 0xd2, 0x48, 0xc7, 0xce, 0x60, - 0xed, 0x4b, 0x0c, 0xd6, 0x58, 0x11, 0xa4, 0xd4, 0xa8, 0xd4, 0xd6, 0x72, 0x76, 0x46, 0x90, 0xbb, - 0x0a, 0x84, 0x23, 0x34, 0xda, 0x83, 0xeb, 0x52, 0x6b, 0x52, 0x3b, 0xd2, 0xa8, 0x4a, 0xdc, 0x7e, - 0x30, 0x23, 0xad, 0x0a, 0xab, 0x65, 0xea, 0x32, 0x4f, 0x0e, 0xd1, 0x2e, 0x94, 0x12, 0x9a, 0xd3, - 0xd6, 0x6a, 0x77, 0x63, 0x46, 0xba, 0x12, 0x48, 0x3c, 0xc6, 0x9b, 0x2f, 0x56, 0x7f, 0x9f, 0xd2, - 0x62, 0xd5, 0x84, 0x7c, 0xa4, 0xf4, 0xf4, 0xdd, 0x11, 0x8f, 0x45, 0xdd, 0x69, 0x0d, 0xc0, 0xad, - 0x73, 0xda, 0x27, 0x57, 0x28, 0x67, 0xc5, 0x3b, 0x96, 0x34, 0xf4, 0x15, 0xbc, 0x37, 0x29, 0x6d, - 0x92, 0x0e, 0x17, 0x51, 0x79, 0x95, 0x71, 0x85, 0xa3, 0x1d, 0xff, 0x00, 0x56, 0x6d, 0x66, 0x0d, - 0xfa, 0xd4, 0x0b, 0x65, 0x4f, 0xe9, 0x0e, 0x02, 0x25, 0x15, 0x0a, 0xb8, 0x3c, 0x36, 0x71, 0x12, - 0xb8, 0xe8, 0x43, 0xc8, 0x32, 0x32, 0x08, 0xcf, 0xb7, 0x75, 0x49, 0x94, 0x54, 0x5b, 0xe9, 0xd4, - 0x85, 0x0d, 0xeb, 0xb9, 0xbd, 0x74, 0x3e, 0x5b, 0xce, 0x99, 0xff, 0xcd, 0x41, 0x21, 0x2e, 0x63, - 0xd4, 0x4c, 0x48, 0x4b, 0x43, 0x8a, 0xa0, 0x8f, 0x2f, 0xa9, 0xfc, 0x37, 0xc5, 0xa5, 0xf9, 0x12, - 0x2a, 0x47, 0x01, 0x7b, 0xa6, 0x54, 0x56, 0x93, 0x79, 0x3c, 0x0c, 0x88, 0xb8, 0x33, 0x2a, 0x90, - 0x91, 0xa2, 0x47, 0xab, 0x12, 0x35, 0x40, 0x7b, 0x42, 0xc1, 0x45, 0x18, 0x7d, 0xdc, 0xee, 0x5d, - 0xf6, 0xd2, 0x91, 0x57, 0x9c, 0x60, 0x9b, 0x7f, 0x4e, 0x01, 0x24, 0x5e, 0xd8, 0x84, 0x74, 0x42, - 0xa9, 0x6f, 0x2d, 0xee, 0xb4, 0x26, 0x15, 0xbb, 0x24, 0x8b, 0x6b, 0x35, 0xa0, 0x24, 0xda, 0xbd, - 0x02, 0xd6, 0x23, 0x21, 0x3f, 0xce, 0x98, 0x6b, 0x53, 0xbb, 0xab, 0x16, 0xa5, 0x36, 0xa3, 0xa8, - 0x6c, 0x52, 0x96, 0x6d, 0xfc, 0xd1, 0x80, 0xb4, 0x14, 0xfb, 0x45, 0xc8, 0xb5, 0x0f, 0x9f, 0xd6, - 0xf7, 0xdb, 0x3b, 0xe5, 0x6b, 0x08, 0xc1, 0xf5, 0xdd, 0x76, 0x6b, 0x7f, 0xa7, 0x8b, 0x5b, 0x5f, - 0x9e, 0xb4, 0x71, 0x6b, 0xa7, 0x6c, 0xa0, 0x35, 0x58, 0xdd, 0xef, 0x34, 0xeb, 0x4f, 0xda, 0x9d, - 0xc3, 0x91, 0x39, 0x85, 0xaa, 0x50, 0x49, 0x98, 0x9b, 0x9d, 0x83, 0x83, 0xd6, 0xe1, 0x4e, 0x6b, - 0xa7, 0xbc, 0x34, 0x72, 0xd2, 0x39, 0x12, 0xb3, 0xf5, 0xfd, 0x72, 0x1a, 0xbd, 0x03, 0x2b, 0xca, - 0xb6, 0xdb, 0xc1, 0x8d, 0xf6, 0xce, 0x4e, 0xeb, 0xb0, 0x9c, 0x41, 0x65, 0x28, 0xb5, 0x0f, 0x9b, - 0x9d, 0x83, 0xa3, 0xfa, 0x93, 0x76, 0x63, 0xbf, 0x55, 0xce, 0xa2, 0x55, 0x58, 0x3e, 0x39, 0x3c, - 0xae, 0x3f, 0x69, 0x1f, 0xef, 0xb6, 0xeb, 0xc2, 0x94, 0x33, 0xff, 0x93, 0x50, 0xe7, 0x3f, 0x82, - 0x1b, 0x16, 0xe1, 0xb4, 0xeb, 0x78, 0x9c, 0x7a, 0xdc, 0x09, 0x9d, 0xe7, 0x54, 0xad, 0x90, 0xcb, - 0x6a, 0xca, 0xe3, 0x35, 0x31, 0xdd, 0x1e, 0xcd, 0xca, 0xb5, 0x72, 0xf4, 0xb5, 0xfc, 0x43, 0xa3, - 0x13, 0x18, 0x55, 0xcf, 0xe7, 0x0b, 0x56, 0x4f, 0x22, 0xf7, 0x5c, 0x0a, 0x58, 0x9c, 0x74, 0x26, - 0x9a, 0x69, 0x7c, 0xac, 0x7c, 0x12, 0x9e, 0x57, 0x53, 0x52, 0x08, 0x97, 0x22, 0xe3, 0x11, 0x09, - 0xcf, 0x05, 0xc8, 0xa6, 0x6e, 0x48, 0xba, 0x03, 0x5f, 0xf8, 0xe6, 0x72, 0xbf, 0xf2, 0xb8, 0x24, - 0x8d, 0x27, 0xca, 0x86, 0x6a, 0x00, 0x9c, 0x06, 0x5d, 0x9f, 0xb9, 0x8e, 0x35, 0xd4, 0xf7, 0xac, - 0x56, 0x5d, 0xc7, 0x34, 0x38, 0x92, 0x66, 0x5c, 0xe0, 0xd1, 0x23, 0xba, 0x80, 0x77, 0xfd, 0xb8, - 0x96, 0xbb, 0xc9, 0x05, 0x66, 0xe5, 0x02, 0x3f, 0xbd, 0x6c, 0x81, 0xd3, 0x4e, 0x02, 0x5e, 0xf3, - 0xa7, 0x58, 0xb9, 0xf9, 0x0c, 0xca, 0x93, 0x79, 0x98, 0x22, 0xe4, 0x7f, 0x92, 0x14, 0xf2, 0x57, - 0x3b, 0x2b, 0x09, 0xd1, 0x6f, 0x43, 0x4e, 0x37, 0x20, 0xf4, 0x09, 0x20, 0xa2, 0xd6, 0x67, 0x53, - 0x6e, 0x05, 0x8e, 0x1f, 0xcb, 0xdc, 0x02, 0x5e, 0x55, 0x33, 0x3b, 0xa3, 0x09, 0x74, 0x0f, 0x94, - 0xb8, 0x9c, 0xfc, 0xb7, 0xa5, 0xff, 0xdd, 0x1e, 0x8b, 0xb9, 0x48, 0x7f, 0xfe, 0xca, 0x80, 0xac, - 0xea, 0x57, 0x6f, 0x47, 0x76, 0x3c, 0x82, 0x9b, 0xb6, 0xc3, 0xc9, 0xa9, 0x4b, 0xbb, 0xa2, 0x91, - 0x75, 0x99, 0x1f, 0x3a, 0xfd, 0x48, 0x98, 0xa7, 0xe4, 0x7e, 0xdf, 0xd0, 0x00, 0xd1, 0xf0, 0x3a, - 0x89, 0x69, 0xf3, 0x2b, 0xc8, 0xaa, 0x26, 0x38, 0x5f, 0x44, 0xc6, 0x82, 0x2c, 0xb5, 0xa0, 0x20, - 0x33, 0x7f, 0x08, 0x39, 0xdd, 0x26, 0x47, 0xb9, 0x31, 0x2e, 0xcf, 0xcd, 0x17, 0xb0, 0x3c, 0xd6, - 0x1d, 0xaf, 0x44, 0x7e, 0x04, 0xa5, 0x64, 0x43, 0xbc, 0x0a, 0x77, 0xe3, 0x17, 0x69, 0xc8, 0xb4, - 0x5e, 0x86, 0x01, 0x31, 0xff, 0x66, 0xc0, 0xdd, 0xa8, 0x50, 0x5a, 0x42, 0x45, 0x3a, 0x5e, 0x6f, - 0x54, 0xb0, 0xd1, 0x27, 0xbf, 0x7d, 0x28, 0x53, 0x3d, 0xd9, 0x4d, 0xe6, 0xad, 0xb8, 0x7d, 0x77, - 0xf6, 0xc7, 0x8f, 0xa8, 0x2d, 0xac, 0x44, 0xd4, 0xe8, 0x7e, 0x39, 0x82, 0xb2, 0x1f, 0x30, 0x9f, - 0x71, 0x6a, 0xc7, 0xde, 0x54, 0x25, 0x2d, 0xf8, 0x15, 0x63, 0x25, 0xa2, 0x6b, 0x83, 0xb8, 0xf5, - 0xe3, 0x55, 0x68, 0x5b, 0xbd, 0x47, 0x1c, 0x8f, 0x87, 0x89, 0xd3, 0x84, 0xbe, 0x18, 0xdf, 0xf4, - 0x85, 0x82, 0x8f, 0xeb, 0xa2, 0x37, 0x7e, 0xb9, 0xa5, 0xe4, 0xd9, 0x6f, 0x8d, 0xc5, 0x2b, 0x33, - 0x5a, 0xbb, 0x34, 0x8e, 0xf9, 0x37, 0xdd, 0xf7, 0x79, 0x05, 0x6c, 0xff, 0x14, 0x0a, 0x71, 0x81, - 0xa0, 0x1f, 0x43, 0x71, 0x94, 0x09, 0x8a, 0x2a, 0xd3, 0xf6, 0xc2, 0x5c, 0x9b, 0xfa, 0xa2, 0x4d, - 0xe3, 0xbe, 0xd1, 0x68, 0xbc, 0xfa, 0xd7, 0xed, 0x6b, 0xaf, 0xbe, 0xbd, 0x6d, 0xfc, 0xe3, 0xdb, - 0xdb, 0xc6, 0x1f, 0xfe, 0x7d, 0xdb, 0xf8, 0xfa, 0xfe, 0x42, 0x9f, 0xdc, 0x12, 0x0e, 0x4f, 0xb3, - 0xd2, 0xfc, 0xe0, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x3c, 0xcd, 0x0e, 0x34, 0xff, 0x16, 0x00, - 0x00, + // 2034 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0xcd, 0x6f, 0x1b, 0xc7, + 0x15, 0xf7, 0xf2, 0x9b, 0x8f, 0x94, 0x44, 0x4d, 0xe8, 0x98, 0xde, 0x38, 0x96, 0xac, 0x24, 0x88, + 0xe0, 0x20, 0x94, 0x21, 0xa7, 0x8d, 0xed, 0xc0, 0x45, 0x49, 0x8a, 0x02, 0xa8, 0x4a, 0xa2, 0x3c, + 0x92, 0x1c, 0x20, 0x17, 0x62, 0xb9, 0x3b, 0xa2, 0xd6, 0x5a, 0xee, 0x6c, 0x77, 0x96, 0xb6, 0xd9, + 0x7b, 0x51, 0xb4, 0xa7, 0xf6, 0xd8, 0xa0, 0x87, 0x9e, 0x7a, 0xea, 0xb5, 0x40, 0xd1, 0x6b, 0x51, + 0xc0, 0x40, 0x2f, 0xfd, 0x0b, 0x5c, 0x34, 0xfd, 0x07, 0x7a, 0xe9, 0x25, 0xa7, 0x60, 0x3e, 0x76, + 0xb9, 0xa4, 0x49, 0x8a, 0x02, 0x9c, 0x5c, 0x88, 0x9d, 0x37, 0xbf, 0xdf, 0xdb, 0x37, 0xb3, 0xef, + 0x93, 0x70, 0xb7, 0x47, 0xb7, 0x3c, 0x9f, 0x06, 0xd4, 0xa4, 0x0e, 0xdb, 0xea, 0x1b, 0x01, 0xf1, + 0x6d, 0xc3, 0xb1, 0x7f, 0x41, 0xe2, 0xcf, 0x55, 0x81, 0x40, 0x85, 0x98, 0x48, 0x5f, 0x37, 0xa9, + 0xcb, 0x06, 0x7d, 0xe2, 0x47, 0xf4, 0xe8, 0x41, 0xc2, 0xf5, 0x5b, 0x63, 0xaa, 0xcf, 0x1c, 0xfa, + 0x42, 0xfc, 0xa8, 0xdd, 0x72, 0x8f, 0xf6, 0xa8, 0x78, 0xdc, 0xe2, 0x4f, 0x4a, 0xba, 0xd6, 0xa3, + 0xb4, 0xe7, 0x10, 0xc9, 0xeb, 0x0e, 0xce, 0xb6, 0x02, 0xbb, 0x4f, 0x58, 0x60, 0xf4, 0x3d, 0x09, + 0xd8, 0xf8, 0x73, 0x05, 0xb2, 0x98, 0xfc, 0x7c, 0x40, 0x58, 0x80, 0x3e, 0x85, 0x14, 0xf3, 0x88, + 0x59, 0xd1, 0xd6, 0xb5, 0xcd, 0xc2, 0xf6, 0xcd, 0x6a, 0xdc, 0x62, 0x85, 0xa9, 0x1e, 0x7b, 0xc4, + 0xc4, 0x02, 0x86, 0x1e, 0x42, 0xee, 0xb9, 0xe1, 0xd8, 0x96, 0x11, 0x90, 0x4a, 0x42, 0x50, 0xde, + 0x9f, 0x4a, 0x79, 0xaa, 0x40, 0x38, 0x82, 0xa3, 0x7b, 0x90, 0x36, 0x3c, 0xcf, 0x19, 0x56, 0x92, + 0x82, 0xa7, 0x4f, 0xe5, 0xd5, 0x38, 0x02, 0x4b, 0x20, 0xb7, 0x8d, 0x7a, 0xc4, 0xad, 0xa4, 0xe6, + 0xd8, 0xd6, 0xf6, 0x88, 0x8b, 0x05, 0x8c, 0xc3, 0x1d, 0x6a, 0x58, 0x95, 0xf4, 0x1c, 0xf8, 0x3e, + 0x35, 0x2c, 0x2c, 0x60, 0xdc, 0x9e, 0x33, 0x67, 0xc0, 0xce, 0x2b, 0x99, 0x39, 0xf6, 0xec, 0x72, + 0x04, 0x96, 0x40, 0xce, 0x60, 0x01, 0xf5, 0x49, 0x25, 0x3b, 0x87, 0x71, 0xcc, 0x11, 0x58, 0x02, + 0x51, 0x03, 0x8a, 0x2c, 0x30, 0xfc, 0xa0, 0x63, 0xd2, 0x7e, 0xdf, 0x0e, 0x2a, 0x39, 0x41, 0x5c, + 0x9f, 0x41, 0x34, 0xfc, 0xa0, 0x21, 0x70, 0xb8, 0xc0, 0x46, 0x0b, 0x54, 0x87, 0x82, 0x61, 0x5e, + 0xb8, 0xf4, 0x85, 0x43, 0xac, 0x1e, 0xa9, 0xe4, 0xe7, 0xe8, 0xa8, 0x8d, 0x70, 0x38, 0x4e, 0x42, + 0xef, 0x41, 0xce, 0x76, 0x03, 0xe2, 0xbb, 0x86, 0x53, 0xb1, 0xd6, 0xb5, 0xcd, 0x22, 0xce, 0x7f, + 0x18, 0x0a, 0xf4, 0xdf, 0x69, 0x90, 0xe2, 0xdf, 0x18, 0x1d, 0xc2, 0xb2, 0x49, 0x5d, 0x97, 0x98, + 0x01, 0xf5, 0x3b, 0xc1, 0xd0, 0x23, 0xc2, 0x2d, 0x96, 0xb7, 0x3f, 0xae, 0x0a, 0xa7, 0x3b, 0x88, + 0xde, 0x68, 0x04, 0x36, 0x75, 0x39, 0xa5, 0xda, 0x08, 0xf1, 0x27, 0x43, 0x8f, 0xe0, 0x25, 0x33, + 0xbe, 0x44, 0x0f, 0xa1, 0x60, 0x52, 0xf7, 0xcc, 0xee, 0x75, 0x9e, 0x31, 0xea, 0x0a, 0x87, 0x29, + 0xd6, 0x6f, 0x7d, 0xfb, 0x7a, 0xad, 0x42, 0x5c, 0x93, 0x5a, 0xb6, 0xdb, 0xdb, 0xe2, 0x1b, 0x55, + 0x6c, 0xbc, 0x38, 0x20, 0x8c, 0x19, 0x3d, 0x82, 0x33, 0x92, 0xa0, 0xff, 0x35, 0x03, 0xb9, 0xd0, + 0x89, 0xd0, 0x13, 0x48, 0xb9, 0x46, 0x5f, 0x5a, 0x93, 0xaf, 0x3f, 0xfe, 0xf6, 0xf5, 0xda, 0xc3, + 0x9e, 0x1d, 0x9c, 0x0f, 0xba, 0x55, 0x93, 0xf6, 0xb7, 0x08, 0x0b, 0x06, 0x86, 0x3f, 0x94, 0xd1, + 0xf1, 0x46, 0xbc, 0x4c, 0x5a, 0x8d, 0x85, 0xaa, 0x29, 0x47, 0x4d, 0xbc, 0xcd, 0xa3, 0x26, 0x17, + 0x3f, 0x2a, 0xaa, 0x41, 0xae, 0x6b, 0xbb, 0x1c, 0xc2, 0x2a, 0xa9, 0xf5, 0xe4, 0x66, 0x61, 0xfb, + 0xa3, 0xb9, 0x31, 0x55, 0xad, 0x4b, 0x34, 0x8e, 0x68, 0x68, 0x1f, 0xca, 0x8e, 0xc1, 0x82, 0x4e, + 0x7f, 0xdc, 0xec, 0x28, 0x14, 0x66, 0x9d, 0x09, 0xbf, 0xc3, 0x69, 0x13, 0x1b, 0xe8, 0x0e, 0x14, + 0x85, 0xb6, 0xe7, 0xc4, 0x67, 0x5c, 0x0b, 0x0f, 0x90, 0x3c, 0x2e, 0x70, 0xd9, 0x53, 0x29, 0xd2, + 0x7f, 0x9f, 0x84, 0xac, 0x32, 0x03, 0xed, 0x41, 0xd9, 0x27, 0x8c, 0x0e, 0x7c, 0x93, 0x74, 0xe2, + 0x77, 0xa0, 0x2d, 0x70, 0x07, 0xcb, 0x21, 0xb3, 0x21, 0xef, 0xe2, 0x11, 0x80, 0x49, 0x1d, 0x87, + 0x98, 0xc2, 0x7c, 0x99, 0x61, 0xca, 0xd2, 0xfc, 0x46, 0x24, 0xe7, 0x96, 0xd7, 0x53, 0xaf, 0x5e, + 0xaf, 0x5d, 0xc3, 0x31, 0x34, 0xfa, 0x95, 0x06, 0xd7, 0xcf, 0x6c, 0xe2, 0x58, 0x71, 0x2b, 0x3a, + 0x7d, 0xc3, 0xab, 0x24, 0xc5, 0xad, 0x3e, 0x5e, 0xe8, 0x56, 0xab, 0xbb, 0x5c, 0x85, 0x34, 0x67, + 0x8f, 0x51, 0xf7, 0xc0, 0xf0, 0x9a, 0x6e, 0xe0, 0x0f, 0xeb, 0xb7, 0x7e, 0xf3, 0xef, 0x39, 0x07, + 0x29, 0x9c, 0x8d, 0x68, 0x48, 0x87, 0x5c, 0xd7, 0x30, 0x2f, 0xce, 0x6c, 0xc7, 0x11, 0xc9, 0x6b, + 0x09, 0x47, 0x6b, 0x74, 0x13, 0x72, 0x3d, 0x9f, 0x0e, 0xbc, 0x4e, 0x77, 0x58, 0x49, 0xaf, 0x27, + 0x37, 0xf3, 0x38, 0x2b, 0xd6, 0xf5, 0xa1, 0xde, 0x84, 0x1b, 0x33, 0x5e, 0x8e, 0x4a, 0x90, 0xbc, + 0x20, 0x43, 0x19, 0x00, 0x98, 0x3f, 0xa2, 0x32, 0xa4, 0x9f, 0x1b, 0xce, 0x40, 0xfa, 0x6d, 0x11, + 0xcb, 0xc5, 0xa3, 0xc4, 0x03, 0x4d, 0xff, 0x6d, 0x02, 0xd2, 0x22, 0x8f, 0xa2, 0x06, 0xac, 0x4c, + 0x7a, 0x84, 0x76, 0x99, 0x47, 0x4c, 0x32, 0x50, 0x05, 0xb2, 0xa1, 0x23, 0x24, 0xc4, 0xeb, 0xc3, + 0xe5, 0x4c, 0xaf, 0x4b, 0xbd, 0x15, 0xaf, 0x4b, 0xbf, 0xe1, 0x75, 0xe8, 0x73, 0x00, 0x16, 0x18, + 0x01, 0x91, 0xfe, 0x95, 0x59, 0xc0, 0xbf, 0xd2, 0x02, 0xaf, 0xff, 0x43, 0x83, 0x14, 0xaf, 0x14, + 0xdf, 0xf7, 0x8d, 0x7c, 0x04, 0x69, 0xdf, 0x70, 0x7b, 0x44, 0xd5, 0xb8, 0x15, 0xa9, 0x14, 0x73, + 0x91, 0x50, 0x25, 0x77, 0x27, 0xce, 0x91, 0x5a, 0xfc, 0x1c, 0x01, 0xa4, 0x78, 0x05, 0xe3, 0x16, + 0xa8, 0xd8, 0x17, 0xe6, 0x2f, 0xe1, 0x70, 0x89, 0xee, 0x43, 0xee, 0x82, 0x0c, 0x17, 0xcf, 0xb7, + 0xc2, 0x97, 0xde, 0x07, 0xe0, 0x24, 0xcf, 0x30, 0x2f, 0x88, 0x25, 0x73, 0x17, 0xce, 0x5f, 0x90, + 0xe1, 0x91, 0x10, 0xe8, 0xff, 0x4c, 0x42, 0x5a, 0x14, 0x42, 0xb4, 0x0b, 0x48, 0x1a, 0xee, 0x19, + 0x81, 0x79, 0x4e, 0xd8, 0xe2, 0x81, 0x5e, 0x14, 0xbc, 0x23, 0x49, 0x43, 0x4f, 0x60, 0x25, 0x0c, + 0x88, 0x4e, 0x97, 0xf4, 0x6c, 0x97, 0x55, 0x12, 0x22, 0x46, 0x37, 0x67, 0x57, 0xe1, 0x6a, 0x5d, + 0x31, 0xea, 0x9c, 0x80, 0x97, 0xbb, 0xf1, 0x25, 0x43, 0x5f, 0x01, 0x8a, 0x54, 0x9a, 0xb4, 0xef, + 0x39, 0x24, 0x20, 0x4c, 0x45, 0xfe, 0x27, 0x0b, 0x68, 0x6d, 0x28, 0x0e, 0x5e, 0xed, 0x4e, 0x48, + 0x98, 0x6e, 0xc2, 0xd2, 0xd8, 0xcb, 0xe7, 0xdc, 0xff, 0x03, 0xc8, 0x47, 0xed, 0x96, 0xca, 0x5f, + 0x7a, 0x55, 0x36, 0x64, 0xd5, 0xb0, 0x21, 0xab, 0x9e, 0x84, 0x08, 0x3c, 0x02, 0xeb, 0x67, 0x50, + 0x9a, 0xb4, 0xe5, 0x7b, 0x79, 0xcf, 0xdf, 0x12, 0x90, 0x16, 0x4d, 0xca, 0x0f, 0xeb, 0x45, 0xbc, + 0x42, 0x8a, 0x1c, 0xc5, 0x16, 0xf7, 0xfa, 0x8c, 0x24, 0xa0, 0x0f, 0x60, 0x49, 0x51, 0x95, 0xf2, + 0xb4, 0x50, 0x5e, 0x94, 0x42, 0xa5, 0xff, 0x3e, 0xe4, 0x2c, 0x6a, 0x2e, 0x9e, 0x1a, 0x92, 0x16, + 0x35, 0xd1, 0xbb, 0x90, 0x21, 0x2f, 0x6d, 0x16, 0x30, 0xd1, 0xd3, 0xe5, 0xb0, 0x5a, 0x71, 0xb9, + 0x45, 0xf8, 0x27, 0x10, 0x2d, 0x5b, 0x0e, 0xab, 0x95, 0xfe, 0xb5, 0x06, 0x85, 0x58, 0xa3, 0x86, + 0x1a, 0x80, 0xfc, 0x81, 0xcb, 0x2f, 0xb7, 0x63, 0x9e, 0x13, 0xf3, 0xc2, 0xa3, 0xb6, 0x1b, 0xa8, + 0x94, 0x52, 0xae, 0x86, 0xed, 0x7d, 0xb5, 0x11, 0xed, 0xe1, 0x55, 0x85, 0x1f, 0x89, 0x66, 0x44, + 0x55, 0xe2, 0xaa, 0x51, 0xa5, 0x9f, 0x42, 0x21, 0xd6, 0x00, 0xbe, 0xad, 0x60, 0xdd, 0xf8, 0x1a, + 0x41, 0x0e, 0x13, 0xe6, 0x51, 0x97, 0x11, 0x54, 0x1d, 0x9b, 0x17, 0x26, 0x5b, 0x60, 0x09, 0x8a, + 0x0f, 0x0c, 0x8f, 0x21, 0x1f, 0x4e, 0x00, 0x96, 0xf2, 0xd3, 0xb5, 0xe9, 0xa4, 0xb0, 0x10, 0x5b, + 0x78, 0xc4, 0x40, 0x9f, 0x43, 0x96, 0xcf, 0x02, 0xb6, 0x72, 0xa8, 0x37, 0xc7, 0x0d, 0x45, 0xae, + 0x49, 0x10, 0x0e, 0xd1, 0xe8, 0x33, 0xc8, 0xf0, 0xa1, 0x80, 0x58, 0xaa, 0x1a, 0xdd, 0x9a, 0xce, + 0x6b, 0x0b, 0x0c, 0x56, 0x58, 0xce, 0xe2, 0xb3, 0x01, 0x09, 0x87, 0x88, 0x19, 0xac, 0x7d, 0x81, + 0xc1, 0x0a, 0xcb, 0x8d, 0x14, 0x03, 0x02, 0xb1, 0xd4, 0x2c, 0x31, 0xc3, 0xc8, 0x5d, 0x09, 0xc2, + 0x21, 0x1a, 0xed, 0xc1, 0xb2, 0x68, 0xf4, 0x89, 0x15, 0x0e, 0x08, 0x72, 0xb2, 0xf8, 0x60, 0xc6, + 0xb5, 0x4a, 0xac, 0x9a, 0x11, 0x96, 0x58, 0x7c, 0x89, 0x76, 0xa1, 0x18, 0x6b, 0xf8, 0x2d, 0x35, + 0x6a, 0x6c, 0xcc, 0xb8, 0xae, 0x18, 0x12, 0x8f, 0xf1, 0xe6, 0x4f, 0x0a, 0x7f, 0x48, 0xa8, 0x49, + 0x41, 0x87, 0x5c, 0xd8, 0x66, 0xab, 0xdc, 0x11, 0xad, 0xb9, 0xdf, 0xa9, 0x06, 0x8c, 0x99, 0xe7, + 0xa4, 0x6f, 0x5c, 0xc1, 0x9d, 0x25, 0xef, 0x58, 0xd0, 0xd0, 0x97, 0xf0, 0xde, 0x64, 0x5f, 0x19, + 0x57, 0xb8, 0x48, 0x8b, 0x5d, 0x1e, 0x6f, 0x2f, 0x95, 0xe2, 0x4f, 0x60, 0xd5, 0xa2, 0xe6, 0xa0, + 0x4f, 0xdc, 0x40, 0x14, 0xf4, 0xce, 0xc0, 0x97, 0x7d, 0x5a, 0x1e, 0x97, 0xc6, 0x36, 0x4e, 0x7d, + 0x07, 0x7d, 0x08, 0x19, 0x6a, 0x0c, 0x82, 0xf3, 0x6d, 0xe5, 0x12, 0x45, 0x59, 0xd3, 0xdb, 0x35, + 0x2e, 0xc3, 0x6a, 0x6f, 0x2f, 0x95, 0xcb, 0x94, 0xb2, 0xfa, 0xff, 0xb3, 0x90, 0x8f, 0xdc, 0x18, + 0x35, 0x62, 0x7d, 0xbd, 0x26, 0xea, 0xd0, 0xc7, 0x97, 0x78, 0xfe, 0x9b, 0x9d, 0xbd, 0xfe, 0x12, + 0xca, 0x47, 0x3e, 0x7d, 0x26, 0x5b, 0xdc, 0x06, 0x75, 0x59, 0xe0, 0x1b, 0x3c, 0x67, 0x94, 0x21, + 0x2d, 0x3a, 0x4e, 0xd5, 0x12, 0xca, 0x05, 0xda, 0xe3, 0xed, 0x73, 0x88, 0x51, 0xe1, 0x76, 0xf7, + 0xb2, 0x97, 0x8e, 0xb4, 0xe2, 0x18, 0x5b, 0xff, 0x4b, 0x02, 0x20, 0xf6, 0xc2, 0x06, 0xa4, 0x62, + 0x63, 0xd2, 0xd6, 0xe2, 0x4a, 0xab, 0x62, 0x5c, 0x12, 0x64, 0x9e, 0x56, 0x7d, 0x62, 0x84, 0x5f, + 0x2f, 0x8f, 0xd5, 0x8a, 0xf7, 0x7e, 0x67, 0xd4, 0xb1, 0x88, 0xd5, 0x91, 0x87, 0x92, 0x1f, 0xa3, + 0x20, 0x65, 0xa2, 0x27, 0xde, 0xf8, 0x93, 0x06, 0x29, 0x31, 0x69, 0x15, 0x20, 0xdb, 0x3a, 0x7c, + 0x5a, 0xdb, 0x6f, 0xed, 0x94, 0xae, 0x21, 0x04, 0xcb, 0xbb, 0xad, 0xe6, 0xfe, 0x4e, 0x07, 0x37, + 0x9f, 0x9c, 0xb6, 0x70, 0x73, 0xa7, 0xa4, 0xa1, 0xeb, 0xb0, 0xba, 0xdf, 0x6e, 0xd4, 0x4e, 0x5a, + 0xed, 0xc3, 0x91, 0x38, 0x81, 0x2a, 0x50, 0x8e, 0x89, 0x1b, 0xed, 0x83, 0x83, 0xe6, 0xe1, 0x4e, + 0x73, 0xa7, 0x94, 0x1c, 0x29, 0x69, 0x1f, 0xf1, 0xdd, 0xda, 0x7e, 0x29, 0x85, 0xde, 0x81, 0x15, + 0x29, 0xdb, 0x6d, 0xe3, 0x7a, 0x6b, 0x67, 0xa7, 0x79, 0x58, 0x4a, 0xa3, 0x12, 0x14, 0x5b, 0x87, + 0x8d, 0xf6, 0xc1, 0x51, 0xed, 0xa4, 0x55, 0xdf, 0x6f, 0x96, 0x32, 0x68, 0x15, 0x96, 0x4e, 0x0f, + 0x8f, 0x6b, 0x27, 0xad, 0xe3, 0xdd, 0x56, 0x8d, 0x8b, 0xb2, 0xfa, 0xff, 0x62, 0xa3, 0xd1, 0x8f, + 0xe1, 0x86, 0x69, 0x30, 0xd2, 0xb1, 0x5d, 0x46, 0x5c, 0x66, 0x07, 0xf6, 0x73, 0x22, 0x4f, 0xc8, + 0x84, 0x37, 0xe5, 0xf0, 0x75, 0xbe, 0xdd, 0x1a, 0xed, 0x8a, 0xb3, 0xf2, 0x66, 0xa6, 0x30, 0xfa, + 0x12, 0xa1, 0xf7, 0x3c, 0x58, 0xd0, 0x7b, 0x62, 0x77, 0xcf, 0xc4, 0xf4, 0x80, 0xe3, 0xca, 0x78, + 0x31, 0x8d, 0xc2, 0xca, 0x33, 0x82, 0x73, 0xd1, 0x79, 0xe5, 0x71, 0x31, 0x14, 0x1e, 0x19, 0xc1, + 0x39, 0x07, 0x59, 0xc4, 0x09, 0x8c, 0xce, 0xc0, 0xe3, 0xba, 0x99, 0xf8, 0x5e, 0x39, 0x5c, 0x14, + 0xc2, 0x53, 0x29, 0x43, 0x55, 0x00, 0x46, 0xfc, 0x8e, 0x47, 0x1d, 0xdb, 0x1c, 0xaa, 0x3c, 0xab, + 0x5a, 0xde, 0x63, 0xe2, 0x1f, 0x09, 0x31, 0xce, 0xb3, 0xf0, 0x11, 0x5d, 0xc0, 0xbb, 0x5e, 0xe4, + 0xcb, 0x9d, 0xf8, 0x01, 0x33, 0xe2, 0x80, 0x9f, 0x5d, 0x76, 0xc0, 0x69, 0x91, 0x80, 0xaf, 0x7b, + 0x53, 0xa4, 0x4c, 0x7f, 0x06, 0xa5, 0xc9, 0x7b, 0x98, 0x32, 0x45, 0xfd, 0x34, 0x3e, 0x45, 0x5d, + 0x2d, 0x56, 0x62, 0x13, 0x97, 0x05, 0x59, 0x55, 0x80, 0xd0, 0xa7, 0x80, 0x0c, 0x79, 0x3e, 0x8b, + 0x30, 0xd3, 0xb7, 0xbd, 0x68, 0xc6, 0xc8, 0xe3, 0x55, 0xb9, 0xb3, 0x33, 0xda, 0x40, 0x77, 0x41, + 0x76, 0xf6, 0x93, 0xa3, 0xae, 0xfa, 0x6b, 0xe1, 0x98, 0xef, 0x85, 0xcd, 0xff, 0xaf, 0x35, 0xc8, + 0xc8, 0x7a, 0xf5, 0x76, 0xda, 0x8e, 0x47, 0x70, 0xd3, 0xb2, 0x99, 0xd1, 0x75, 0x48, 0x87, 0x17, + 0xb2, 0x0e, 0xf5, 0x02, 0xbb, 0x1f, 0x4e, 0x45, 0x09, 0xf1, 0xbd, 0x6f, 0x28, 0x00, 0x2f, 0x78, + 0xed, 0xd8, 0xb6, 0xfe, 0x25, 0x64, 0x64, 0x11, 0x9c, 0xdf, 0x44, 0x46, 0x0d, 0x59, 0x62, 0xc1, + 0x86, 0x4c, 0xff, 0x11, 0x64, 0x55, 0x99, 0x1c, 0xdd, 0x8d, 0x76, 0xf9, 0xdd, 0x7c, 0x01, 0x4b, + 0x63, 0xd5, 0xf1, 0x4a, 0xe4, 0x47, 0x50, 0x8c, 0x17, 0xc4, 0xab, 0x70, 0x37, 0x7e, 0x99, 0x82, + 0x74, 0xf3, 0x65, 0xe0, 0x1b, 0xfa, 0xdf, 0x35, 0xb8, 0x13, 0x3a, 0x4a, 0x93, 0x77, 0x91, 0xb6, + 0xdb, 0x1b, 0x39, 0x6c, 0xf8, 0x7f, 0xeb, 0x3e, 0x94, 0x88, 0xda, 0xec, 0xc4, 0xef, 0xad, 0xb0, + 0x7d, 0x67, 0xf6, 0x3f, 0x4f, 0x61, 0x59, 0x58, 0x09, 0xa9, 0x61, 0x7e, 0x39, 0x82, 0x92, 0xe7, + 0x53, 0x8f, 0x32, 0x62, 0x45, 0xda, 0xa4, 0x27, 0x2d, 0xf8, 0x17, 0xd2, 0x4a, 0x48, 0x57, 0x02, + 0x9e, 0xf5, 0xa3, 0x53, 0x28, 0x59, 0xad, 0x67, 0xd8, 0x2e, 0x0b, 0x62, 0xd1, 0x84, 0xbe, 0x18, + 0xff, 0xe8, 0x0b, 0x19, 0x1f, 0xf9, 0x45, 0x6f, 0x3c, 0xb9, 0xc9, 0xc1, 0xaf, 0x39, 0x66, 0xaf, + 0xb8, 0xd1, 0xea, 0xa5, 0x76, 0xcc, 0xcf, 0x74, 0x3f, 0x64, 0x0a, 0xd8, 0xfe, 0x19, 0xe4, 0x23, + 0x07, 0x41, 0x3f, 0x81, 0xc2, 0xe8, 0x26, 0x08, 0x2a, 0x4f, 0xfb, 0x16, 0xfa, 0xf5, 0xa9, 0x2f, + 0xda, 0xd4, 0xee, 0x69, 0xf5, 0xfa, 0xab, 0xff, 0xdc, 0xbe, 0xf6, 0xea, 0x9b, 0xdb, 0xda, 0xbf, + 0xbe, 0xb9, 0xad, 0xfd, 0xf1, 0xbf, 0xb7, 0xb5, 0xaf, 0xee, 0x2d, 0xf4, 0x7f, 0x67, 0x4c, 0x61, + 0x37, 0x23, 0xc4, 0xf7, 0xbf, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xd8, 0x41, 0xdd, 0x92, 0x9d, 0x18, + 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -2290,6 +2396,34 @@ func (m *Request_Flush) MarshalToSizedBuffer(dAtA []byte) (int, error) { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } + if len(m.BackfillCompletes) > 0 { + for iNdEx := len(m.BackfillCompletes) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.BackfillCompletes[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMaterialize(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if len(m.BackfillBegins) > 0 { + for iNdEx := len(m.BackfillBegins) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.BackfillBegins[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMaterialize(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } if len(m.StatePatchesJson) > 0 { i -= len(m.StatePatchesJson) copy(dAtA[i:], m.StatePatchesJson) @@ -2300,6 +2434,94 @@ func (m *Request_Flush) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *Request_Flush_BackfillBegin) Marshal() (dAtA []byte, err error) { + size := m.ProtoSize() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Request_Flush_BackfillBegin) MarshalTo(dAtA []byte) (int, error) { + size := m.ProtoSize() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Request_Flush_BackfillBegin) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.Timestamp != nil { + { + size, err := m.Timestamp.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMaterialize(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + if m.Binding != 0 { + i = encodeVarintMaterialize(dAtA, i, uint64(m.Binding)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *Request_Flush_BackfillComplete) Marshal() (dAtA []byte, err error) { + size := m.ProtoSize() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Request_Flush_BackfillComplete) MarshalTo(dAtA []byte) (int, error) { + size := m.ProtoSize() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Request_Flush_BackfillComplete) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.Timestamp != nil { + { + size, err := m.Timestamp.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMaterialize(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + if m.Binding != 0 { + i = encodeVarintMaterialize(dAtA, i, uint64(m.Binding)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + func (m *Request_Store) Marshal() (dAtA []byte, err error) { size := m.ProtoSize() dAtA = make([]byte, size) @@ -3550,6 +3772,56 @@ func (m *Request_Flush) ProtoSize() (n int) { if l > 0 { n += 1 + l + sovMaterialize(uint64(l)) } + if len(m.BackfillBegins) > 0 { + for _, e := range m.BackfillBegins { + l = e.ProtoSize() + n += 1 + l + sovMaterialize(uint64(l)) + } + } + if len(m.BackfillCompletes) > 0 { + for _, e := range m.BackfillCompletes { + l = e.ProtoSize() + n += 1 + l + sovMaterialize(uint64(l)) + } + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *Request_Flush_BackfillBegin) ProtoSize() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Binding != 0 { + n += 1 + sovMaterialize(uint64(m.Binding)) + } + if m.Timestamp != nil { + l = m.Timestamp.ProtoSize() + n += 1 + l + sovMaterialize(uint64(l)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *Request_Flush_BackfillComplete) ProtoSize() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Binding != 0 { + n += 1 + sovMaterialize(uint64(m.Binding)) + } + if m.Timestamp != nil { + l = m.Timestamp.ProtoSize() + n += 1 + l + sovMaterialize(uint64(l)) + } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } @@ -5652,6 +5924,286 @@ func (m *Request_Flush) Unmarshal(dAtA []byte) error { m.StatePatchesJson = []byte{} } iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field BackfillBegins", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMaterialize + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMaterialize + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMaterialize + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.BackfillBegins = append(m.BackfillBegins, &Request_Flush_BackfillBegin{}) + if err := m.BackfillBegins[len(m.BackfillBegins)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field BackfillCompletes", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMaterialize + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMaterialize + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMaterialize + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.BackfillCompletes = append(m.BackfillCompletes, &Request_Flush_BackfillComplete{}) + if err := m.BackfillCompletes[len(m.BackfillCompletes)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipMaterialize(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMaterialize + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Request_Flush_BackfillBegin) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMaterialize + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: BackfillBegin: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: BackfillBegin: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Binding", wireType) + } + m.Binding = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMaterialize + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Binding |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMaterialize + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMaterialize + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMaterialize + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Timestamp == nil { + m.Timestamp = &types.Timestamp{} + } + if err := m.Timestamp.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipMaterialize(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMaterialize + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Request_Flush_BackfillComplete) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMaterialize + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: BackfillComplete: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: BackfillComplete: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Binding", wireType) + } + m.Binding = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMaterialize + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Binding |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMaterialize + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMaterialize + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMaterialize + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Timestamp == nil { + m.Timestamp = &types.Timestamp{} + } + if err := m.Timestamp.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipMaterialize(dAtA[iNdEx:]) diff --git a/go/protocols/materialize/materialize.proto b/go/protocols/materialize/materialize.proto index 0b53e04f0c7..cadc94bbebc 100644 --- a/go/protocols/materialize/materialize.proto +++ b/go/protocols/materialize/materialize.proto @@ -6,6 +6,7 @@ option go_package = "github.com/estuary/flow/go/protocols/materialize"; import "consumer/protocol/protocol.proto"; import "go/protocols/flow/flow.proto"; import "gogoproto/gogo.proto"; +import "google/protobuf/timestamp.proto"; option (gogoproto.marshaler_all) = true; option (gogoproto.protosizer_all) = true; @@ -176,6 +177,30 @@ message Request { (gogoproto.casttype) = "encoding/json.RawMessage", json_name = "statePatches" ]; + + // Backfill-begin signals observed during this transaction. Populated only on + // shard zero. + message BackfillBegin { + uint32 binding = 1; + // Truncation boundary of the binding's backfill: documents published at or + // after this time are current, while earlier ones were superseded by the + // backfill. It equals the begin's own publication time (flow_published_at), + // and is carried on both begin and complete so connectors need not track it + // across transactions. + google.protobuf.Timestamp timestamp = 2; + } + repeated BackfillBegin backfill_begins = 2; + + // Backfill-complete signals observed during this transaction. Populated only + // on shard zero. + message BackfillComplete { + uint32 binding = 1; + // Truncation boundary of the completed backfill (see BackfillBegin.timestamp): + // the connector may delete destination rows whose flow_published_at predates + // this time, as they were superseded by the backfill. + google.protobuf.Timestamp timestamp = 2; + } + repeated BackfillComplete backfill_completes = 3; } Flush flush = 6; diff --git a/go/protocols/runtime/runtime.pb.go b/go/protocols/runtime/runtime.pb.go index 570ec17ac51..07b2be2f1e5 100644 --- a/go/protocols/runtime/runtime.pb.go +++ b/go/protocols/runtime/runtime.pb.go @@ -1623,10 +1623,15 @@ type Recover struct { // Key: binding index; Value: packed composite key tuple. MaxKeys map[uint32][]byte `protobuf:"bytes,9,rep,name=max_keys,json=maxKeys,proto3" json:"max_keys,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // Persisted trigger parameters (materialize only), or empty. - TriggerParamsJson []byte `protobuf:"bytes,10,opt,name=trigger_params_json,json=triggerParams,proto3" json:"trigger_params_json,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + TriggerParamsJson []byte `protobuf:"bytes,10,opt,name=trigger_params_json,json=triggerParams,proto3" json:"trigger_params_json,omitempty"` + // Active-backfill begin clocks, keyed by binding index. Restored so the + // capture runtime can re-apply truncated-at journal labels on startup and + // resolve a BackfillComplete's truncated_at. Resolved from "AB:{state_key}" + // keys by the scan. + ActiveBackfills map[uint32]uint64 `protobuf:"bytes,11,rep,name=active_backfills,json=activeBackfills,proto3" json:"active_backfills,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"fixed64,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *Recover) Reset() { *m = Recover{} } @@ -1721,10 +1726,17 @@ type Persist struct { DeleteTriggerParams bool `protobuf:"varint,15,opt,name=delete_trigger_params,json=deleteTriggerParams,proto3" json:"delete_trigger_params,omitempty"` // Materialization trigger parameters. // Effect: Put under "trigger-params" key. - TriggerParamsJson []byte `protobuf:"bytes,16,opt,name=trigger_params_json,json=triggerParams,proto3" json:"trigger_params_json,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + TriggerParamsJson []byte `protobuf:"bytes,16,opt,name=trigger_params_json,json=triggerParams,proto3" json:"trigger_params_json,omitempty"` + // The active-backfill change this transaction observed, if any. At most one + // per commit — a backfill control signal stands alone in its transaction. + // + // Types that are valid to be assigned to ActiveBackfillChange: + // *Persist_Begin + // *Persist_CompleteBinding + ActiveBackfillChange isPersist_ActiveBackfillChange `protobuf_oneof:"active_backfill_change"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *Persist) Reset() { *m = Persist{} } @@ -1760,6 +1772,94 @@ func (m *Persist) XXX_DiscardUnknown() { var xxx_messageInfo_Persist proto.InternalMessageInfo +type isPersist_ActiveBackfillChange interface { + isPersist_ActiveBackfillChange() + MarshalTo([]byte) (int, error) + ProtoSize() int +} + +type Persist_Begin struct { + Begin *ActiveBackfillBegin `protobuf:"bytes,17,opt,name=begin,proto3,oneof" json:"begin,omitempty"` +} +type Persist_CompleteBinding struct { + CompleteBinding uint32 `protobuf:"varint,18,opt,name=complete_binding,json=completeBinding,proto3,oneof" json:"complete_binding,omitempty"` +} + +func (*Persist_Begin) isPersist_ActiveBackfillChange() {} +func (*Persist_CompleteBinding) isPersist_ActiveBackfillChange() {} + +func (m *Persist) GetActiveBackfillChange() isPersist_ActiveBackfillChange { + if m != nil { + return m.ActiveBackfillChange + } + return nil +} + +func (m *Persist) GetBegin() *ActiveBackfillBegin { + if x, ok := m.GetActiveBackfillChange().(*Persist_Begin); ok { + return x.Begin + } + return nil +} + +func (m *Persist) GetCompleteBinding() uint32 { + if x, ok := m.GetActiveBackfillChange().(*Persist_CompleteBinding); ok { + return x.CompleteBinding + } + return 0 +} + +// XXX_OneofWrappers is for the internal use of the proto package. +func (*Persist) XXX_OneofWrappers() []interface{} { + return []interface{}{ + (*Persist_Begin)(nil), + (*Persist_CompleteBinding)(nil), + } +} + +// ActiveBackfillBegin records a binding's backfill begin clock — its +// authoritative truncated_at — staged by a committing Persist. +type ActiveBackfillBegin struct { + Binding uint32 `protobuf:"varint,1,opt,name=binding,proto3" json:"binding,omitempty"` + TruncatedAt uint64 `protobuf:"fixed64,2,opt,name=truncated_at,json=truncatedAt,proto3" json:"truncated_at,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ActiveBackfillBegin) Reset() { *m = ActiveBackfillBegin{} } +func (m *ActiveBackfillBegin) String() string { return proto.CompactTextString(m) } +func (*ActiveBackfillBegin) ProtoMessage() {} +func (*ActiveBackfillBegin) Descriptor() ([]byte, []int) { + return fileDescriptor_73af6e0737ce390c, []int{20} +} +func (m *ActiveBackfillBegin) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ActiveBackfillBegin) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ActiveBackfillBegin.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *ActiveBackfillBegin) XXX_Merge(src proto.Message) { + xxx_messageInfo_ActiveBackfillBegin.Merge(m, src) +} +func (m *ActiveBackfillBegin) XXX_Size() int { + return m.ProtoSize() +} +func (m *ActiveBackfillBegin) XXX_DiscardUnknown() { + xxx_messageInfo_ActiveBackfillBegin.DiscardUnknown(m) +} + +var xxx_messageInfo_ActiveBackfillBegin proto.InternalMessageInfo + // Persisted is sent by shard zero to the leader after the state is durable // in the recovery log. type Persisted struct { @@ -1774,7 +1874,7 @@ func (m *Persisted) Reset() { *m = Persisted{} } func (m *Persisted) String() string { return proto.CompactTextString(m) } func (*Persisted) ProtoMessage() {} func (*Persisted) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{20} + return fileDescriptor_73af6e0737ce390c, []int{21} } func (m *Persisted) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1831,7 +1931,7 @@ func (m *Apply) Reset() { *m = Apply{} } func (m *Apply) String() string { return proto.CompactTextString(m) } func (*Apply) ProtoMessage() {} func (*Apply) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{21} + return fileDescriptor_73af6e0737ce390c, []int{22} } func (m *Apply) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1875,7 +1975,7 @@ func (m *Applied) Reset() { *m = Applied{} } func (m *Applied) String() string { return proto.CompactTextString(m) } func (*Applied) ProtoMessage() {} func (*Applied) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{22} + return fileDescriptor_73af6e0737ce390c, []int{23} } func (m *Applied) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1925,7 +2025,7 @@ func (m *Open) Reset() { *m = Open{} } func (m *Open) String() string { return proto.CompactTextString(m) } func (*Open) ProtoMessage() {} func (*Open) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{23} + return fileDescriptor_73af6e0737ce390c, []int{24} } func (m *Open) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1966,7 +2066,7 @@ func (m *CloseNow) Reset() { *m = CloseNow{} } func (m *CloseNow) String() string { return proto.CompactTextString(m) } func (*CloseNow) ProtoMessage() {} func (*CloseNow) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{24} + return fileDescriptor_73af6e0737ce390c, []int{25} } func (m *CloseNow) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2006,7 +2106,7 @@ func (m *Stop) Reset() { *m = Stop{} } func (m *Stop) String() string { return proto.CompactTextString(m) } func (*Stop) ProtoMessage() {} func (*Stop) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{25} + return fileDescriptor_73af6e0737ce390c, []int{26} } func (m *Stop) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2048,7 +2148,7 @@ func (m *Stopped) Reset() { *m = Stopped{} } func (m *Stopped) String() string { return proto.CompactTextString(m) } func (*Stopped) ProtoMessage() {} func (*Stopped) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{26} + return fileDescriptor_73af6e0737ce390c, []int{27} } func (m *Stopped) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2095,7 +2195,7 @@ func (m *SessionLoop) Reset() { *m = SessionLoop{} } func (m *SessionLoop) String() string { return proto.CompactTextString(m) } func (*SessionLoop) ProtoMessage() {} func (*SessionLoop) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{27} + return fileDescriptor_73af6e0737ce390c, []int{28} } func (m *SessionLoop) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2172,7 +2272,7 @@ func (m *Capture) Reset() { *m = Capture{} } func (m *Capture) String() string { return proto.CompactTextString(m) } func (*Capture) ProtoMessage() {} func (*Capture) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{28} + return fileDescriptor_73af6e0737ce390c, []int{29} } func (m *Capture) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2214,7 +2314,7 @@ func (m *Capture_Opened) Reset() { *m = Capture_Opened{} } func (m *Capture_Opened) String() string { return proto.CompactTextString(m) } func (*Capture_Opened) ProtoMessage() {} func (*Capture_Opened) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{28, 0} + return fileDescriptor_73af6e0737ce390c, []int{29, 0} } func (m *Capture_Opened) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2313,7 +2413,7 @@ func (m *Materialize) Reset() { *m = Materialize{} } func (m *Materialize) String() string { return proto.CompactTextString(m) } func (*Materialize) ProtoMessage() {} func (*Materialize) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29} + return fileDescriptor_73af6e0737ce390c, []int{30} } func (m *Materialize) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2379,7 +2479,7 @@ func (m *Materialize_Opened) Reset() { *m = Materialize_Opened{} } func (m *Materialize_Opened) String() string { return proto.CompactTextString(m) } func (*Materialize_Opened) ProtoMessage() {} func (*Materialize_Opened) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29, 0} + return fileDescriptor_73af6e0737ce390c, []int{30, 0} } func (m *Materialize_Opened) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2423,7 +2523,7 @@ func (m *Materialize_Load) Reset() { *m = Materialize_Load{} } func (m *Materialize_Load) String() string { return proto.CompactTextString(m) } func (*Materialize_Load) ProtoMessage() {} func (*Materialize_Load) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29, 1} + return fileDescriptor_73af6e0737ce390c, []int{30, 1} } func (m *Materialize_Load) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2467,7 +2567,7 @@ func (m *Materialize_Loaded) Reset() { *m = Materialize_Loaded{} } func (m *Materialize_Loaded) String() string { return proto.CompactTextString(m) } func (*Materialize_Loaded) ProtoMessage() {} func (*Materialize_Loaded) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29, 2} + return fileDescriptor_73af6e0737ce390c, []int{30, 2} } func (m *Materialize_Loaded) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2519,7 +2619,7 @@ func (m *Materialize_Loaded_Binding) Reset() { *m = Materialize_Loaded_B func (m *Materialize_Loaded_Binding) String() string { return proto.CompactTextString(m) } func (*Materialize_Loaded_Binding) ProtoMessage() {} func (*Materialize_Loaded_Binding) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29, 2, 0} + return fileDescriptor_73af6e0737ce390c, []int{30, 2, 0} } func (m *Materialize_Loaded_Binding) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2562,7 +2662,7 @@ func (m *Materialize_Flush) Reset() { *m = Materialize_Flush{} } func (m *Materialize_Flush) String() string { return proto.CompactTextString(m) } func (*Materialize_Flush) ProtoMessage() {} func (*Materialize_Flush) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29, 3} + return fileDescriptor_73af6e0737ce390c, []int{30, 3} } func (m *Materialize_Flush) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2607,7 +2707,7 @@ func (m *Materialize_Flushed) Reset() { *m = Materialize_Flushed{} } func (m *Materialize_Flushed) String() string { return proto.CompactTextString(m) } func (*Materialize_Flushed) ProtoMessage() {} func (*Materialize_Flushed) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29, 4} + return fileDescriptor_73af6e0737ce390c, []int{30, 4} } func (m *Materialize_Flushed) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2655,7 +2755,7 @@ func (m *Materialize_Flushed_Binding) Reset() { *m = Materialize_Flushed func (m *Materialize_Flushed_Binding) String() string { return proto.CompactTextString(m) } func (*Materialize_Flushed_Binding) ProtoMessage() {} func (*Materialize_Flushed_Binding) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29, 4, 0} + return fileDescriptor_73af6e0737ce390c, []int{30, 4, 0} } func (m *Materialize_Flushed_Binding) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2696,7 +2796,7 @@ func (m *Materialize_Store) Reset() { *m = Materialize_Store{} } func (m *Materialize_Store) String() string { return proto.CompactTextString(m) } func (*Materialize_Store) ProtoMessage() {} func (*Materialize_Store) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29, 5} + return fileDescriptor_73af6e0737ce390c, []int{30, 5} } func (m *Materialize_Store) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2738,7 +2838,7 @@ func (m *Materialize_Stored) Reset() { *m = Materialize_Stored{} } func (m *Materialize_Stored) String() string { return proto.CompactTextString(m) } func (*Materialize_Stored) ProtoMessage() {} func (*Materialize_Stored) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29, 6} + return fileDescriptor_73af6e0737ce390c, []int{30, 6} } func (m *Materialize_Stored) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2784,7 +2884,7 @@ func (m *Materialize_Stored_Binding) Reset() { *m = Materialize_Stored_B func (m *Materialize_Stored_Binding) String() string { return proto.CompactTextString(m) } func (*Materialize_Stored_Binding) ProtoMessage() {} func (*Materialize_Stored_Binding) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29, 6, 0} + return fileDescriptor_73af6e0737ce390c, []int{30, 6, 0} } func (m *Materialize_Stored_Binding) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2832,7 +2932,7 @@ func (m *Materialize_StartCommit) Reset() { *m = Materialize_StartCommit func (m *Materialize_StartCommit) String() string { return proto.CompactTextString(m) } func (*Materialize_StartCommit) ProtoMessage() {} func (*Materialize_StartCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29, 7} + return fileDescriptor_73af6e0737ce390c, []int{30, 7} } func (m *Materialize_StartCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2876,7 +2976,7 @@ func (m *Materialize_StartedCommit) Reset() { *m = Materialize_StartedCo func (m *Materialize_StartedCommit) String() string { return proto.CompactTextString(m) } func (*Materialize_StartedCommit) ProtoMessage() {} func (*Materialize_StartedCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29, 8} + return fileDescriptor_73af6e0737ce390c, []int{30, 8} } func (m *Materialize_StartedCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2919,7 +3019,7 @@ func (m *Materialize_Acknowledge) Reset() { *m = Materialize_Acknowledge func (m *Materialize_Acknowledge) String() string { return proto.CompactTextString(m) } func (*Materialize_Acknowledge) ProtoMessage() {} func (*Materialize_Acknowledge) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29, 9} + return fileDescriptor_73af6e0737ce390c, []int{30, 9} } func (m *Materialize_Acknowledge) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2963,7 +3063,7 @@ func (m *Materialize_Acknowledged) Reset() { *m = Materialize_Acknowledg func (m *Materialize_Acknowledged) String() string { return proto.CompactTextString(m) } func (*Materialize_Acknowledged) ProtoMessage() {} func (*Materialize_Acknowledged) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{29, 10} + return fileDescriptor_73af6e0737ce390c, []int{30, 10} } func (m *Materialize_Acknowledged) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3051,7 +3151,7 @@ func (m *Derive) Reset() { *m = Derive{} } func (m *Derive) String() string { return proto.CompactTextString(m) } func (*Derive) ProtoMessage() {} func (*Derive) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{30} + return fileDescriptor_73af6e0737ce390c, []int{31} } func (m *Derive) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3106,7 +3206,7 @@ func (m *Derive_Opened) Reset() { *m = Derive_Opened{} } func (m *Derive_Opened) String() string { return proto.CompactTextString(m) } func (*Derive_Opened) ProtoMessage() {} func (*Derive_Opened) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{30, 0} + return fileDescriptor_73af6e0737ce390c, []int{31, 0} } func (m *Derive_Opened) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3150,7 +3250,7 @@ func (m *Derive_Load) Reset() { *m = Derive_Load{} } func (m *Derive_Load) String() string { return proto.CompactTextString(m) } func (*Derive_Load) ProtoMessage() {} func (*Derive_Load) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{30, 1} + return fileDescriptor_73af6e0737ce390c, []int{31, 1} } func (m *Derive_Load) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3195,7 +3295,7 @@ func (m *Derive_Loaded) Reset() { *m = Derive_Loaded{} } func (m *Derive_Loaded) String() string { return proto.CompactTextString(m) } func (*Derive_Loaded) ProtoMessage() {} func (*Derive_Loaded) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{30, 2} + return fileDescriptor_73af6e0737ce390c, []int{31, 2} } func (m *Derive_Loaded) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3245,7 +3345,7 @@ func (m *Derive_Loaded_Binding) Reset() { *m = Derive_Loaded_Binding{} } func (m *Derive_Loaded_Binding) String() string { return proto.CompactTextString(m) } func (*Derive_Loaded_Binding) ProtoMessage() {} func (*Derive_Loaded_Binding) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{30, 2, 0} + return fileDescriptor_73af6e0737ce390c, []int{31, 2, 0} } func (m *Derive_Loaded_Binding) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3293,7 +3393,7 @@ func (m *Derive_Flush) Reset() { *m = Derive_Flush{} } func (m *Derive_Flush) String() string { return proto.CompactTextString(m) } func (*Derive_Flush) ProtoMessage() {} func (*Derive_Flush) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{30, 3} + return fileDescriptor_73af6e0737ce390c, []int{31, 3} } func (m *Derive_Flush) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3343,7 +3443,7 @@ func (m *Derive_Flushed) Reset() { *m = Derive_Flushed{} } func (m *Derive_Flushed) String() string { return proto.CompactTextString(m) } func (*Derive_Flushed) ProtoMessage() {} func (*Derive_Flushed) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{30, 4} + return fileDescriptor_73af6e0737ce390c, []int{31, 4} } func (m *Derive_Flushed) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3384,7 +3484,7 @@ func (m *Derive_Store) Reset() { *m = Derive_Store{} } func (m *Derive_Store) String() string { return proto.CompactTextString(m) } func (*Derive_Store) ProtoMessage() {} func (*Derive_Store) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{30, 5} + return fileDescriptor_73af6e0737ce390c, []int{31, 5} } func (m *Derive_Store) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3439,7 +3539,7 @@ func (m *Derive_Stored) Reset() { *m = Derive_Stored{} } func (m *Derive_Stored) String() string { return proto.CompactTextString(m) } func (*Derive_Stored) ProtoMessage() {} func (*Derive_Stored) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{30, 6} + return fileDescriptor_73af6e0737ce390c, []int{31, 6} } func (m *Derive_Stored) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3489,7 +3589,7 @@ func (m *Derive_Stored_PublisherCommit) Reset() { *m = Derive_Stored_Pub func (m *Derive_Stored_PublisherCommit) String() string { return proto.CompactTextString(m) } func (*Derive_Stored_PublisherCommit) ProtoMessage() {} func (*Derive_Stored_PublisherCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{30, 6, 0} + return fileDescriptor_73af6e0737ce390c, []int{31, 6, 0} } func (m *Derive_Stored_PublisherCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3534,7 +3634,7 @@ func (m *Derive_StartCommit) Reset() { *m = Derive_StartCommit{} } func (m *Derive_StartCommit) String() string { return proto.CompactTextString(m) } func (*Derive_StartCommit) ProtoMessage() {} func (*Derive_StartCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{30, 7} + return fileDescriptor_73af6e0737ce390c, []int{31, 7} } func (m *Derive_StartCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3576,7 +3676,7 @@ func (m *Derive_StartedCommit) Reset() { *m = Derive_StartedCommit{} } func (m *Derive_StartedCommit) String() string { return proto.CompactTextString(m) } func (*Derive_StartedCommit) ProtoMessage() {} func (*Derive_StartedCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_73af6e0737ce390c, []int{30, 8} + return fileDescriptor_73af6e0737ce390c, []int{31, 8} } func (m *Derive_StartedCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3642,10 +3742,12 @@ func init() { proto.RegisterType((*Task)(nil), "runtime.Task") proto.RegisterType((*Recover)(nil), "runtime.Recover") proto.RegisterMapType((map[string][]byte)(nil), "runtime.Recover.AckIntentsEntry") + proto.RegisterMapType((map[uint32]uint64)(nil), "runtime.Recover.ActiveBackfillsEntry") proto.RegisterMapType((map[uint32][]byte)(nil), "runtime.Recover.MaxKeysEntry") proto.RegisterType((*Persist)(nil), "runtime.Persist") proto.RegisterMapType((map[string][]byte)(nil), "runtime.Persist.AckIntentsEntry") proto.RegisterMapType((map[uint32][]byte)(nil), "runtime.Persist.MaxKeysEntry") + proto.RegisterType((*ActiveBackfillBegin)(nil), "runtime.ActiveBackfillBegin") proto.RegisterType((*Persisted)(nil), "runtime.Persisted") proto.RegisterType((*Apply)(nil), "runtime.Apply") proto.RegisterType((*Applied)(nil), "runtime.Applied") @@ -3691,276 +3793,284 @@ func init() { } var fileDescriptor_73af6e0737ce390c = []byte{ - // 4303 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x5b, 0x4d, 0x6c, 0x1c, 0x47, - 0x76, 0xf6, 0xfc, 0xcf, 0xbc, 0x19, 0x92, 0xc3, 0x12, 0x49, 0xb5, 0x46, 0xb6, 0x48, 0x8f, 0xed, - 0x98, 0x16, 0xa9, 0x21, 0x4d, 0xdb, 0xbb, 0xb6, 0xe2, 0x3f, 0xfe, 0x69, 0x4d, 0x2d, 0x25, 0x71, - 0x8b, 0x94, 0x90, 0xe4, 0xd2, 0x68, 0x76, 0x15, 0x87, 0x2d, 0xf6, 0x74, 0xb5, 0xbb, 0x7b, 0x48, - 0x71, 0x4f, 0x01, 0x72, 0x09, 0x90, 0x43, 0x2e, 0xb9, 0x04, 0xb9, 0x24, 0xa7, 0xfc, 0x00, 0x39, - 0xe4, 0x14, 0x60, 0x0f, 0x39, 0xe5, 0x60, 0xec, 0x29, 0xc9, 0x21, 0x48, 0x2e, 0x02, 0xb2, 0xb9, - 0xe6, 0xb6, 0x09, 0x90, 0x08, 0x39, 0x04, 0xf5, 0xd3, 0xbf, 0xd3, 0x43, 0x51, 0xb4, 0x11, 0x18, - 0x8b, 0x3d, 0x48, 0xd3, 0xf5, 0xde, 0xf7, 0xaa, 0x5e, 0x55, 0xbd, 0x57, 0xf5, 0x55, 0x75, 0x13, - 0xba, 0x7d, 0xb6, 0xe2, 0x7a, 0x2c, 0x60, 0x26, 0xb3, 0xfd, 0x15, 0x6f, 0xe8, 0x04, 0xd6, 0x80, - 0x86, 0xbf, 0x3d, 0xa1, 0x41, 0x35, 0x55, 0xec, 0xdc, 0x3a, 0xf4, 0xd8, 0x09, 0xf5, 0x22, 0x83, - 0xe8, 0x41, 0x02, 0x3b, 0x0b, 0x26, 0x73, 0xfc, 0xe1, 0xe0, 0x02, 0x44, 0xba, 0x39, 0xd3, 0x70, - 0x83, 0xa1, 0x47, 0xc3, 0xdf, 0xb0, 0x96, 0x14, 0x86, 0x50, 0xcf, 0x3a, 0xa5, 0xea, 0x47, 0x21, - 0x5e, 0x4f, 0x21, 0x8e, 0x6c, 0x76, 0x26, 0xfe, 0x53, 0xda, 0xdb, 0x29, 0xed, 0xc0, 0x08, 0xa8, - 0x67, 0x19, 0xb6, 0xf5, 0x53, 0x9a, 0x7c, 0x56, 0xd8, 0x4e, 0x0a, 0xcb, 0x5c, 0xf1, 0x2f, 0xd7, - 0x57, 0xff, 0x78, 0x78, 0x74, 0x64, 0xd3, 0xf0, 0x57, 0x61, 0x66, 0xfa, 0xac, 0xcf, 0xc4, 0xe3, - 0x0a, 0x7f, 0x92, 0xd2, 0xee, 0xdf, 0x15, 0x60, 0xfa, 0xc0, 0xf0, 0x4f, 0xf6, 0xa9, 0x77, 0x6a, - 0x99, 0x74, 0x93, 0x39, 0x47, 0x56, 0x1f, 0xdd, 0x82, 0xa6, 0xcd, 0xfa, 0xfa, 0x91, 0x65, 0x53, - 0xfd, 0x88, 0x68, 0x85, 0x85, 0xc2, 0x62, 0x05, 0x37, 0x6c, 0xd6, 0xbf, 0x67, 0xd9, 0xf4, 0x1e, - 0x41, 0x37, 0xa1, 0x11, 0x18, 0xfe, 0x89, 0xee, 0x18, 0x03, 0xaa, 0x15, 0x17, 0x0a, 0x8b, 0x0d, - 0x5c, 0xe7, 0x82, 0x87, 0xc6, 0x80, 0xa2, 0x1b, 0x50, 0x1f, 0x12, 0x5f, 0x77, 0x8d, 0xe0, 0x58, - 0x2b, 0x09, 0x5d, 0x6d, 0x48, 0xfc, 0x3d, 0x23, 0x38, 0x46, 0x4b, 0x30, 0x6d, 0x32, 0x27, 0x30, - 0x2c, 0x87, 0x7a, 0xba, 0x43, 0x83, 0x33, 0xe6, 0x9d, 0x68, 0x65, 0x81, 0x69, 0x47, 0x8a, 0x87, - 0x52, 0x8e, 0xde, 0x86, 0x8a, 0x6b, 0x1b, 0x0e, 0xd5, 0xaa, 0x0b, 0x85, 0xc5, 0xc9, 0xb5, 0xc9, - 0x5e, 0x38, 0xd5, 0x7b, 0x5c, 0x8a, 0xa5, 0xb2, 0xfb, 0x3f, 0x65, 0x98, 0xdc, 0x97, 0x1d, 0xc5, - 0xf4, 0xeb, 0x21, 0xf5, 0x03, 0xb4, 0x03, 0xb5, 0xa7, 0x6c, 0xe8, 0x39, 0x86, 0x2d, 0x3c, 0x6f, - 0x6c, 0xac, 0xbc, 0x78, 0x3e, 0xbf, 0xd4, 0x67, 0xbd, 0xbe, 0xf1, 0x53, 0x1a, 0x04, 0xb4, 0x47, - 0xe8, 0xe9, 0x8a, 0xc9, 0x3c, 0xba, 0x92, 0x09, 0x92, 0xde, 0x7d, 0x69, 0x86, 0x43, 0x7b, 0x34, - 0x07, 0x55, 0x8f, 0xba, 0xb6, 0x71, 0x2e, 0x7a, 0x59, 0xc7, 0xaa, 0xc4, 0xfb, 0x78, 0x38, 0xb4, - 0x6c, 0xa2, 0x5b, 0x24, 0xec, 0xa3, 0x28, 0xef, 0x10, 0x74, 0x0f, 0xaa, 0xec, 0xe8, 0xc8, 0xa7, - 0x81, 0xe8, 0x58, 0x69, 0xa3, 0xf7, 0xe2, 0xf9, 0xfc, 0xed, 0xcb, 0x34, 0xfe, 0x48, 0x58, 0x61, - 0x65, 0x8d, 0x1e, 0x00, 0x50, 0x87, 0xe8, 0xaa, 0xae, 0xca, 0x95, 0xea, 0x6a, 0x50, 0x87, 0xc8, - 0x47, 0xb4, 0x04, 0x15, 0xcf, 0x70, 0xfa, 0x72, 0x34, 0x9b, 0x6b, 0x53, 0x3d, 0x11, 0x86, 0x98, - 0x8b, 0xf6, 0x5d, 0x6a, 0x6e, 0x94, 0xbf, 0x79, 0x3e, 0xff, 0x1a, 0x96, 0x18, 0xb4, 0x0f, 0x4d, - 0x93, 0x31, 0x8f, 0x58, 0x8e, 0x11, 0x30, 0x4f, 0xab, 0x89, 0x51, 0x7c, 0xff, 0xc5, 0xf3, 0xf9, - 0x3b, 0x79, 0x8d, 0x8f, 0xa4, 0x52, 0x6f, 0xff, 0xd8, 0xf0, 0xc8, 0xce, 0x16, 0x4e, 0xd6, 0x82, - 0x56, 0x01, 0x3c, 0xea, 0x33, 0x7b, 0x18, 0x58, 0xcc, 0xd1, 0xea, 0xc2, 0x8d, 0x76, 0x2f, 0xb2, - 0xf9, 0x8a, 0x1a, 0x84, 0x7a, 0x38, 0x81, 0x41, 0x6f, 0xc1, 0x84, 0x8a, 0x61, 0xdd, 0x72, 0x08, - 0x7d, 0xa6, 0x35, 0x16, 0x0a, 0x8b, 0x13, 0xb8, 0xa5, 0x84, 0x3b, 0x5c, 0x86, 0x3e, 0x04, 0x10, - 0x19, 0x67, 0x88, 0x6a, 0x41, 0x54, 0x3b, 0x23, 0x7b, 0xb7, 0xc9, 0x6c, 0x9b, 0x9a, 0x5c, 0xce, - 0xbb, 0x88, 0x13, 0x38, 0xb4, 0x09, 0x53, 0x71, 0x8a, 0x49, 0xd3, 0xa6, 0x30, 0xbd, 0x21, 0x4d, - 0x1f, 0xa4, 0x95, 0xc2, 0x3e, 0x6b, 0xd1, 0xfd, 0xa7, 0x32, 0x4c, 0x45, 0xb1, 0xe7, 0xbb, 0xcc, - 0xf1, 0x29, 0x5a, 0x84, 0xaa, 0x1f, 0x18, 0xc1, 0xd0, 0x17, 0xb1, 0x37, 0xb9, 0xd6, 0xee, 0x85, - 0xc3, 0xd3, 0xdb, 0x17, 0x72, 0xac, 0xf4, 0x1c, 0x79, 0x2c, 0xfa, 0x2c, 0x62, 0x2b, 0x6f, 0x2c, - 0x94, 0x1e, 0xbd, 0x03, 0x93, 0x01, 0xf5, 0x06, 0x96, 0x63, 0xd8, 0x3a, 0xf5, 0x3c, 0xe6, 0xa9, - 0x98, 0x9b, 0x08, 0xa5, 0xdb, 0x5c, 0x88, 0x7e, 0x02, 0x2d, 0x8f, 0x1a, 0x44, 0x0f, 0x8e, 0x3d, - 0x36, 0xec, 0x1f, 0x5f, 0x31, 0xfe, 0x9a, 0xbc, 0x8e, 0x03, 0x59, 0x05, 0x0f, 0xc2, 0x33, 0xcf, - 0x0a, 0xa8, 0xce, 0x3d, 0xb9, 0x6a, 0x10, 0x8a, 0x1a, 0x78, 0x97, 0xd0, 0x0e, 0x54, 0x0c, 0x8f, - 0x3a, 0x86, 0x08, 0xc2, 0xd6, 0xc6, 0x07, 0x2f, 0x9e, 0xcf, 0xaf, 0xf4, 0xad, 0xe0, 0x78, 0x78, - 0xd8, 0x33, 0xd9, 0x60, 0x85, 0xfa, 0xc1, 0xd0, 0xf0, 0xce, 0xe5, 0x32, 0x39, 0xb2, 0x70, 0xf6, - 0xd6, 0xb9, 0x29, 0x96, 0x35, 0xa0, 0x77, 0xa0, 0x4c, 0x98, 0xe9, 0x6b, 0xb5, 0x85, 0xd2, 0x62, - 0x73, 0xad, 0x29, 0x67, 0x6d, 0xdf, 0xb6, 0x4c, 0xaa, 0x42, 0x59, 0xa8, 0xd1, 0x57, 0x50, 0x93, - 0x19, 0xe4, 0x6b, 0xf5, 0x85, 0xd2, 0x15, 0xbc, 0x0f, 0xcd, 0x79, 0x9c, 0x0d, 0x87, 0x16, 0xd1, - 0x5d, 0xc3, 0x0b, 0x7c, 0xad, 0x21, 0x9a, 0x55, 0x59, 0xf4, 0xf8, 0xf1, 0xce, 0xd6, 0x1e, 0x17, - 0xab, 0xa6, 0x1b, 0x1c, 0x28, 0x04, 0x3c, 0xe8, 0x5d, 0xc3, 0x3c, 0xa1, 0x44, 0x3f, 0xa1, 0xe7, - 0x1a, 0x8c, 0x73, 0xb6, 0x21, 0x41, 0x3f, 0xa6, 0xe7, 0x5d, 0x02, 0xd3, 0x98, 0x99, 0x27, 0xfe, - 0xd6, 0xc6, 0x16, 0xf5, 0x4d, 0xcf, 0x72, 0x79, 0xee, 0x2c, 0x03, 0xf2, 0xb8, 0x90, 0x1c, 0xea, - 0xd4, 0x39, 0xd5, 0x07, 0x74, 0xe0, 0x06, 0x9e, 0x88, 0xb0, 0x2a, 0x6e, 0x2b, 0xcd, 0xb6, 0x73, - 0xfa, 0x40, 0xc8, 0xd1, 0x9b, 0xd0, 0x0a, 0xd1, 0x62, 0x15, 0x96, 0x2b, 0x74, 0x53, 0xc9, 0xf8, - 0x4a, 0xdc, 0xfd, 0xa3, 0x22, 0x34, 0x36, 0xc3, 0x15, 0x17, 0x5d, 0x87, 0x9a, 0xe5, 0xea, 0x06, - 0x21, 0xb2, 0xce, 0x06, 0xae, 0x5a, 0xee, 0x3a, 0x21, 0x1e, 0xfa, 0x01, 0x4c, 0xa8, 0x65, 0x5a, - 0x77, 0x19, 0xef, 0x77, 0x51, 0xf4, 0x60, 0x5a, 0xf6, 0x40, 0xad, 0xd4, 0x7b, 0xcc, 0x0b, 0x70, - 0xcb, 0x89, 0x0b, 0x3e, 0xda, 0x87, 0xe9, 0x81, 0xe1, 0xba, 0x94, 0xe8, 0xc7, 0xcc, 0x0f, 0x94, - 0x6d, 0x49, 0xd8, 0xbe, 0x1b, 0xad, 0xe3, 0x51, 0xfb, 0xbd, 0x07, 0x02, 0xfb, 0x15, 0xf3, 0x03, - 0x61, 0xbe, 0xed, 0x04, 0xde, 0x39, 0x4f, 0xb7, 0x94, 0x14, 0xbd, 0x01, 0x30, 0xf4, 0x8d, 0x3e, - 0xd5, 0x3d, 0x23, 0xa0, 0x22, 0xba, 0x8b, 0xb8, 0x21, 0x24, 0xd8, 0x08, 0x68, 0x67, 0x03, 0x66, - 0xf2, 0xea, 0x41, 0x6d, 0x28, 0xf1, 0xb1, 0x2f, 0x88, 0xb5, 0x83, 0x3f, 0xa2, 0x19, 0xa8, 0x9c, - 0x1a, 0xf6, 0x30, 0xdc, 0xba, 0x64, 0xe1, 0x6e, 0xf1, 0xe3, 0x42, 0xf7, 0xaf, 0x8a, 0x30, 0xbd, - 0x29, 0xb7, 0x78, 0xb5, 0x9b, 0x6c, 0x3f, 0xe3, 0x6b, 0x27, 0xdf, 0xfb, 0x74, 0x9b, 0x9e, 0x52, - 0x5b, 0xa5, 0xf5, 0x64, 0x8f, 0xef, 0xbe, 0xbb, 0xac, 0xdf, 0xdb, 0xe5, 0x52, 0x5c, 0xb7, 0x59, - 0x5f, 0x3c, 0xa1, 0x9d, 0x78, 0xaa, 0x48, 0x34, 0x81, 0x2a, 0xc5, 0x3b, 0x51, 0xdf, 0x47, 0xa6, - 0x18, 0x4f, 0x2b, 0xab, 0xc4, 0xac, 0xef, 0x40, 0xcb, 0x0f, 0x0c, 0x2f, 0xd0, 0x4d, 0x36, 0x18, - 0x58, 0x81, 0xc8, 0xfa, 0xe6, 0xda, 0x6f, 0xc4, 0x03, 0x98, 0xf5, 0x94, 0x2f, 0x31, 0x5e, 0xb0, - 0x29, 0xd0, 0xb8, 0xe9, 0xc7, 0x85, 0x0e, 0x86, 0x66, 0x42, 0x87, 0x36, 0x01, 0xa9, 0x4a, 0x74, - 0xf3, 0x98, 0x9a, 0x27, 0x2e, 0xb3, 0x9c, 0x40, 0x74, 0x8d, 0x2f, 0x9e, 0xd1, 0x8a, 0xb5, 0x19, - 0xe9, 0xf0, 0xb4, 0xc2, 0xc7, 0xa2, 0xee, 0xff, 0x96, 0x01, 0x45, 0x2e, 0xc8, 0xe5, 0x8f, 0x8f, - 0xd6, 0x2a, 0x34, 0xa2, 0xbd, 0x5c, 0x55, 0x89, 0x46, 0xe7, 0x1c, 0xc7, 0x20, 0x74, 0x17, 0xaa, - 0xcc, 0xa5, 0x0e, 0x25, 0x6a, 0x98, 0xba, 0xa3, 0x3d, 0x8c, 0xaa, 0xef, 0x3d, 0x12, 0x48, 0xac, - 0x2c, 0xd0, 0x97, 0x50, 0x57, 0x9c, 0x8c, 0xa8, 0xf1, 0x79, 0xfb, 0x22, 0x6b, 0x25, 0x22, 0x38, - 0xb2, 0x42, 0xf7, 0x00, 0x12, 0x63, 0x50, 0x1e, 0x37, 0xc6, 0x89, 0x3a, 0xe2, 0x51, 0x49, 0x58, - 0x76, 0x1e, 0x40, 0x55, 0xfa, 0xf6, 0x9d, 0x8c, 0x6e, 0xe7, 0x09, 0xd4, 0x43, 0x67, 0x79, 0xe4, - 0x9f, 0xd0, 0x73, 0x5d, 0x2e, 0x12, 0xa2, 0xa2, 0x16, 0x6e, 0x9c, 0xd0, 0xf3, 0x3d, 0x21, 0xe0, - 0xb4, 0x8a, 0xaf, 0x4a, 0x16, 0xdf, 0x94, 0xfc, 0x10, 0x55, 0x14, 0xa8, 0x76, 0xac, 0x90, 0xe0, - 0xce, 0x19, 0x40, 0xdc, 0x0a, 0x5a, 0x80, 0x0a, 0xdf, 0x8e, 0x7c, 0xe5, 0x1d, 0x88, 0xb0, 0xe6, - 0x1b, 0x95, 0x8f, 0xa5, 0x02, 0xfd, 0x08, 0x9a, 0x2e, 0xb3, 0x6d, 0xdd, 0xa3, 0xfe, 0xd0, 0x0e, - 0x44, 0xb5, 0x93, 0x17, 0x8f, 0xcf, 0x1e, 0xb3, 0x6d, 0x2c, 0xd0, 0x18, 0xdc, 0xe8, 0xb9, 0xfb, - 0x10, 0x20, 0xd6, 0xa0, 0x26, 0xd4, 0x76, 0x1e, 0x3e, 0x59, 0xdf, 0xdd, 0xd9, 0x6a, 0xbf, 0x86, - 0x1a, 0x50, 0xc1, 0xdb, 0xeb, 0x5b, 0xbf, 0xdd, 0x2e, 0xa0, 0x09, 0x68, 0x3c, 0x7c, 0x74, 0xa0, - 0xcb, 0x62, 0x11, 0xb5, 0xa0, 0xbe, 0xf9, 0xe8, 0xd1, 0xae, 0xfe, 0xe8, 0xde, 0xbd, 0x76, 0x89, - 0x1b, 0xe1, 0xed, 0xfd, 0x83, 0x75, 0x7c, 0xd0, 0x2e, 0x77, 0xff, 0xa3, 0x00, 0xed, 0x2d, 0xc1, - 0xb5, 0xbf, 0x07, 0xa9, 0xba, 0x06, 0x65, 0x1e, 0x90, 0x2a, 0x04, 0x6f, 0x45, 0xc6, 0x59, 0x07, - 0x45, 0xf8, 0x62, 0x81, 0xed, 0x2c, 0x43, 0x99, 0x97, 0xd0, 0xdb, 0x30, 0xe9, 0x7f, 0x6d, 0xf3, - 0x5d, 0xf6, 0xf4, 0xc8, 0xd7, 0x87, 0x9e, 0xa5, 0x16, 0xe1, 0x96, 0x94, 0x3e, 0x39, 0xf2, 0x1f, - 0x7b, 0x56, 0xf7, 0x3f, 0x4b, 0x30, 0x1d, 0xd6, 0xf6, 0x6d, 0x92, 0xed, 0x93, 0x4c, 0xb2, 0xbd, - 0x39, 0xe2, 0xeb, 0xd8, 0x5c, 0xdb, 0x80, 0x86, 0x3b, 0x3c, 0xb4, 0x2d, 0xff, 0x38, 0x27, 0xd9, - 0x46, 0xad, 0xf7, 0x42, 0x2c, 0x8e, 0xcd, 0xd0, 0xa7, 0x50, 0x3b, 0xb2, 0x87, 0xa2, 0x86, 0x72, - 0x26, 0xd9, 0x47, 0x6b, 0xb8, 0x27, 0x91, 0x38, 0x34, 0xf9, 0xae, 0x73, 0x2c, 0x80, 0x46, 0xe4, - 0x24, 0x3f, 0xd4, 0x0c, 0x8c, 0x67, 0xba, 0x69, 0x33, 0xf3, 0x44, 0x6d, 0xad, 0xf5, 0x81, 0xf1, - 0x6c, 0x93, 0x97, 0x33, 0x19, 0x58, 0xbc, 0x54, 0x06, 0x96, 0xc6, 0x64, 0xe0, 0x12, 0xd4, 0x54, - 0xc7, 0x5e, 0x9e, 0x7e, 0xdd, 0x3f, 0x2c, 0xc0, 0x6c, 0x4c, 0x46, 0xbf, 0x07, 0xa1, 0xde, 0xfd, - 0x59, 0x01, 0xe6, 0x52, 0x1e, 0x7d, 0x9b, 0x68, 0x5c, 0x8f, 0xc3, 0x41, 0x3a, 0x13, 0xd3, 0x83, - 0xfc, 0x36, 0x46, 0x63, 0xe2, 0x95, 0x86, 0xf3, 0x67, 0x65, 0x98, 0xdc, 0x64, 0x83, 0x43, 0xcb, - 0x89, 0x8e, 0x8b, 0xab, 0x2a, 0x75, 0xa5, 0xcd, 0xeb, 0x09, 0x7f, 0x93, 0xb0, 0x44, 0xe2, 0xa2, - 0x3b, 0x50, 0x32, 0x48, 0xe8, 0xf0, 0xcd, 0x71, 0x06, 0xeb, 0x84, 0x60, 0x8e, 0xeb, 0xfc, 0x73, - 0x51, 0x25, 0xfa, 0x97, 0x50, 0x3f, 0xb4, 0x1c, 0x62, 0x39, 0x7d, 0xee, 0x61, 0x29, 0xbd, 0x57, - 0x8d, 0xb6, 0xd6, 0xdb, 0x90, 0x60, 0x1c, 0x59, 0x75, 0xfe, 0xa0, 0x08, 0x35, 0x25, 0x45, 0x08, - 0xca, 0x47, 0x43, 0x5b, 0x4e, 0x7d, 0x1d, 0x8b, 0xe7, 0x90, 0xeb, 0x70, 0x96, 0xd6, 0x90, 0x5c, - 0xe7, 0x63, 0x68, 0xba, 0x1e, 0x7b, 0x2a, 0x8f, 0x41, 0x21, 0x07, 0x6b, 0x4b, 0xfe, 0xb6, 0x17, - 0x29, 0x14, 0x0d, 0x4d, 0x42, 0xd1, 0x67, 0xd0, 0xf4, 0xcd, 0x63, 0x3a, 0x30, 0xf4, 0xa7, 0x3e, - 0x73, 0x44, 0xb6, 0xb6, 0x36, 0x5e, 0x7f, 0xf1, 0x7c, 0x5e, 0xa3, 0x8e, 0xc9, 0xb8, 0x0b, 0x2b, - 0x5c, 0xd1, 0xc3, 0xc6, 0xd9, 0x03, 0xea, 0x0b, 0x1a, 0x06, 0xd2, 0xe0, 0xbe, 0xcf, 0x1c, 0xd4, - 0x03, 0xf0, 0xa9, 0xa7, 0xbb, 0xcc, 0xb6, 0xcc, 0x73, 0x71, 0x74, 0x88, 0xf8, 0xf2, 0x3e, 0xf5, - 0xf6, 0x84, 0x18, 0x37, 0xfc, 0xf0, 0x51, 0x5c, 0x1b, 0x08, 0x7e, 0x1d, 0x78, 0xe2, 0x78, 0xd0, - 0xc0, 0x35, 0x41, 0xa3, 0x03, 0x8f, 0x9f, 0xc2, 0x05, 0x45, 0x93, 0x6c, 0xbf, 0x81, 0x55, 0xa9, - 0xe3, 0x40, 0x69, 0x9d, 0x10, 0xa4, 0x41, 0x4d, 0x0d, 0x90, 0x22, 0x79, 0x61, 0x11, 0xfd, 0x10, - 0xea, 0x84, 0x99, 0xd2, 0xff, 0xe2, 0x25, 0xfc, 0xaf, 0x11, 0x66, 0x0a, 0xe7, 0x67, 0xa0, 0x72, - 0xe4, 0x31, 0x47, 0x52, 0xae, 0x3a, 0x96, 0x85, 0xee, 0xbf, 0x14, 0x60, 0x2a, 0x9a, 0x27, 0x75, - 0xde, 0x1b, 0xdf, 0xb8, 0x06, 0x35, 0x42, 0x6d, 0x1a, 0xa8, 0xd0, 0xae, 0xe3, 0xb0, 0x98, 0x72, - 0xab, 0x74, 0x25, 0xb7, 0xca, 0x09, 0xb7, 0x32, 0x6b, 0x53, 0x25, 0xbb, 0x36, 0xbd, 0x05, 0x13, - 0x72, 0xbc, 0x42, 0x84, 0x38, 0x7c, 0xe1, 0x96, 0x14, 0x4a, 0x50, 0xf7, 0x3a, 0xcc, 0x6e, 0x32, - 0xc7, 0xa1, 0x66, 0xc0, 0xbc, 0x3d, 0x8f, 0x3d, 0x3b, 0x57, 0x81, 0xd8, 0xfd, 0x93, 0x02, 0xcc, - 0x65, 0x35, 0xaa, 0xeb, 0xf7, 0xa1, 0xc6, 0x8f, 0x0c, 0xd4, 0xf7, 0xd5, 0x3d, 0xcb, 0xea, 0x8b, - 0xe7, 0xf3, 0xcb, 0x97, 0x39, 0x5b, 0x6d, 0x3b, 0x44, 0xae, 0xc9, 0x61, 0x05, 0x7c, 0xf6, 0x5d, - 0x5e, 0xb9, 0x6e, 0x11, 0xc5, 0xca, 0x6b, 0xa2, 0xbc, 0x43, 0x50, 0x07, 0x4a, 0x36, 0xeb, 0xab, - 0xfd, 0xa6, 0x1e, 0xae, 0x70, 0x98, 0x0b, 0xbb, 0x7f, 0x53, 0x82, 0xf2, 0x7d, 0x66, 0x39, 0xe8, - 0x36, 0x4c, 0xd3, 0xc0, 0x24, 0xfa, 0x80, 0x11, 0xdd, 0xa3, 0xa7, 0x96, 0xcf, 0x4f, 0xf4, 0xdc, - 0xab, 0x12, 0x9e, 0xe2, 0x8a, 0x07, 0x8c, 0x60, 0x25, 0x46, 0x4b, 0x50, 0xf5, 0x8f, 0x0d, 0x8f, - 0x84, 0xa7, 0x99, 0x6b, 0x51, 0x12, 0xf2, 0xaa, 0xe4, 0xe5, 0x05, 0x56, 0x10, 0x34, 0x0f, 0x4d, - 0xf1, 0xa4, 0x6e, 0x20, 0x4a, 0x62, 0x8e, 0x41, 0x88, 0xe4, 0xfd, 0xc3, 0x12, 0x4c, 0x87, 0x97, - 0x14, 0xc4, 0xf2, 0xc4, 0x30, 0x9d, 0x87, 0x77, 0x5a, 0x4a, 0xb1, 0x15, 0xca, 0xd1, 0x7b, 0x10, - 0xca, 0x74, 0xaa, 0xc6, 0x40, 0x4c, 0x58, 0x03, 0x4f, 0x29, 0x79, 0x38, 0x34, 0xe8, 0x5d, 0x98, - 0xb2, 0xc5, 0xf1, 0x3f, 0x46, 0xca, 0xb4, 0x98, 0x94, 0xe2, 0x10, 0xd8, 0xf9, 0xeb, 0x02, 0x54, - 0x84, 0xcf, 0x68, 0x12, 0x8a, 0x16, 0x51, 0xe4, 0xa1, 0x68, 0x11, 0xd4, 0x83, 0xba, 0x6d, 0x1c, - 0x52, 0x9b, 0x07, 0x67, 0x51, 0xad, 0xc6, 0x62, 0x45, 0xe4, 0xe8, 0x5d, 0xa5, 0xc1, 0x11, 0x06, - 0xad, 0x41, 0xcd, 0xa3, 0x06, 0xf7, 0x54, 0x8d, 0xb6, 0x16, 0x5f, 0x49, 0xec, 0x79, 0xcc, 0xa4, - 0xbe, 0xbf, 0xef, 0x52, 0xb3, 0xb7, 0xb3, 0x85, 0x43, 0x20, 0x5a, 0x85, 0x19, 0x31, 0xf0, 0xa6, - 0x47, 0x8d, 0x80, 0xc6, 0x63, 0x2f, 0x2e, 0x1f, 0x30, 0xe2, 0xba, 0x4d, 0xa1, 0x0a, 0x87, 0xbf, - 0xfb, 0x21, 0x54, 0xf9, 0x38, 0x53, 0xc2, 0x27, 0x8d, 0xef, 0xb8, 0xc2, 0x3e, 0x3b, 0x69, 0x03, - 0xe3, 0xd9, 0x76, 0x60, 0x46, 0x93, 0xd6, 0xfd, 0x8b, 0x02, 0x94, 0x0f, 0x0c, 0xff, 0x84, 0x2f, - 0x7b, 0xbe, 0x4b, 0x4d, 0xc5, 0x82, 0xc5, 0x33, 0x4f, 0x35, 0x97, 0x57, 0x40, 0xcf, 0xc2, 0x54, - 0x53, 0x45, 0x3e, 0xe0, 0xbc, 0x89, 0xc0, 0x33, 0x1c, 0xdf, 0x88, 0xd6, 0x40, 0x3e, 0x87, 0xbc, - 0x85, 0x83, 0x84, 0x38, 0x87, 0x86, 0x95, 0x47, 0x69, 0x18, 0x3f, 0x5b, 0x87, 0x64, 0xc6, 0xe3, - 0xc1, 0x2a, 0xd3, 0xad, 0x19, 0xc9, 0x76, 0x48, 0xf7, 0xcf, 0x2a, 0x50, 0xc3, 0xd4, 0x64, 0xa7, - 0x62, 0x7f, 0x6b, 0x1a, 0xe6, 0x89, 0x6e, 0x39, 0x01, 0x75, 0x82, 0x70, 0xd5, 0x5f, 0x88, 0x37, - 0x5c, 0x09, 0xeb, 0xad, 0x9b, 0x27, 0x3b, 0x12, 0x22, 0xcf, 0xbe, 0x60, 0x44, 0x02, 0xb4, 0x06, - 0xb3, 0xf2, 0xfc, 0x17, 0x50, 0xc2, 0xd9, 0x89, 0x4f, 0x15, 0x47, 0x29, 0x0a, 0x8e, 0x72, 0x2d, - 0x52, 0x6e, 0x72, 0x9d, 0xa4, 0x2b, 0x5f, 0x02, 0x8a, 0x6d, 0xc4, 0x2a, 0x61, 0xd1, 0x70, 0x52, - 0xa7, 0x7b, 0xe1, 0xc5, 0xf0, 0x3d, 0xa5, 0xc0, 0xd3, 0x11, 0x38, 0x14, 0xa1, 0x65, 0x98, 0x31, - 0xc3, 0xb4, 0xd7, 0xf9, 0xde, 0x49, 0x13, 0xdb, 0x00, 0x9e, 0x8c, 0x74, 0x7c, 0x77, 0xa5, 0x68, - 0x19, 0xd0, 0x31, 0xef, 0x63, 0xda, 0xc1, 0x8a, 0xbc, 0x9f, 0x90, 0x9a, 0x84, 0x77, 0x77, 0x61, - 0x4a, 0xa1, 0x23, 0xd7, 0xaa, 0xe3, 0x5c, 0x9b, 0x94, 0xc8, 0xc8, 0xaf, 0x37, 0xa1, 0x65, 0x1b, - 0x7e, 0xa0, 0x1b, 0xae, 0x6b, 0x5b, 0x94, 0x88, 0xbb, 0xc9, 0x16, 0x6e, 0x72, 0xd9, 0xba, 0x14, - 0xa1, 0x75, 0x98, 0xb6, 0x69, 0xdf, 0x30, 0xcf, 0x93, 0xcc, 0xb0, 0x7e, 0x01, 0x33, 0x6c, 0x4b, - 0x78, 0xe2, 0x58, 0xf4, 0x31, 0x70, 0xea, 0xa7, 0x9f, 0xd0, 0xf3, 0xf0, 0xaa, 0xe7, 0x8d, 0x91, - 0x39, 0x7b, 0x60, 0x3c, 0xfb, 0x31, 0x3d, 0x57, 0x13, 0x56, 0x1b, 0xc8, 0x12, 0xba, 0x0d, 0xd7, - 0x02, 0xcf, 0xea, 0xf7, 0xf9, 0xd6, 0x67, 0x78, 0xc6, 0xc0, 0x97, 0xc3, 0x06, 0xc2, 0xcd, 0x09, - 0xa5, 0xda, 0x13, 0x9a, 0xce, 0x67, 0x30, 0x95, 0x99, 0xf8, 0xe4, 0x65, 0x45, 0x23, 0xe7, 0xb2, - 0xa2, 0x95, 0xb8, 0xac, 0xe8, 0xdc, 0x85, 0x56, 0xd2, 0x87, 0x97, 0x5d, 0x74, 0x24, 0x6d, 0xbb, - 0x3f, 0xaf, 0x41, 0x6d, 0x8f, 0x7a, 0xbe, 0xe5, 0x07, 0x68, 0x16, 0xaa, 0x3e, 0xfd, 0x5a, 0x77, - 0x98, 0x30, 0x2d, 0xe3, 0x8a, 0x4f, 0xbf, 0x7e, 0xc8, 0xf8, 0x9c, 0xca, 0x0d, 0x4b, 0x4f, 0x46, - 0xb0, 0xcc, 0xaf, 0xb6, 0xd4, 0xc4, 0xde, 0x67, 0x03, 0xbd, 0x94, 0x09, 0x74, 0xd5, 0xd6, 0xd5, - 0x02, 0xbd, 0x3c, 0x3e, 0xd0, 0xef, 0xc2, 0x0d, 0xe5, 0x64, 0x4e, 0xbc, 0x57, 0x84, 0xaf, 0xd7, - 0x25, 0x60, 0x73, 0x24, 0xc4, 0xf3, 0x93, 0xa4, 0xfa, 0x0a, 0x49, 0xb2, 0x0a, 0x73, 0x71, 0x92, - 0xb8, 0x46, 0x60, 0x1e, 0x53, 0x35, 0xdf, 0x32, 0x2c, 0xdb, 0x91, 0x76, 0x4f, 0x2a, 0xc7, 0x24, - 0x4a, 0x7d, 0x4c, 0xa2, 0x7c, 0x08, 0x73, 0xaa, 0x77, 0xd9, 0x7c, 0x69, 0x88, 0xae, 0xcd, 0x48, - 0xed, 0x57, 0xe9, 0x14, 0xc9, 0x49, 0x2f, 0xb8, 0x6a, 0x7a, 0x35, 0x47, 0xd3, 0xeb, 0x63, 0xd0, - 0x94, 0x53, 0xa3, 0x59, 0xd6, 0x12, 0x6e, 0x29, 0xa7, 0x77, 0xb3, 0x59, 0x95, 0x9b, 0x98, 0x13, - 0x57, 0x4e, 0xcc, 0xc9, 0x4c, 0x62, 0x86, 0x31, 0x96, 0x9f, 0x98, 0x6b, 0x30, 0xab, 0xdc, 0x4e, - 0xe7, 0xa7, 0x36, 0x25, 0x7c, 0xbe, 0x26, 0x95, 0x07, 0xc9, 0x04, 0x1d, 0x97, 0xcc, 0xed, 0xef, - 0x59, 0x32, 0x77, 0xa1, 0xa1, 0xfa, 0x4e, 0xc9, 0x98, 0x6c, 0xee, 0xfe, 0x79, 0x01, 0x2a, 0x7c, - 0x06, 0xcf, 0xc7, 0x6d, 0xa0, 0xa7, 0xbc, 0x06, 0xc5, 0x93, 0x1b, 0x38, 0x2c, 0xf2, 0x53, 0xb1, - 0x08, 0x08, 0x61, 0x22, 0x17, 0xff, 0x3a, 0x17, 0x70, 0x22, 0x10, 0x45, 0x4b, 0x68, 0x2b, 0xa9, - 0x8c, 0x88, 0x96, 0x27, 0xca, 0x7e, 0x75, 0xcc, 0x3e, 0x22, 0x49, 0x28, 0x4a, 0xef, 0x23, 0x9c, - 0xe4, 0x76, 0x9f, 0x42, 0x2d, 0x0c, 0xb5, 0x3b, 0x80, 0xe4, 0xee, 0x1c, 0x1d, 0x5a, 0x43, 0x86, - 0xd0, 0xc0, 0xd3, 0x52, 0xb3, 0x15, 0x2b, 0x2e, 0x48, 0xc7, 0x62, 0x7e, 0x3a, 0x76, 0x7f, 0x59, - 0x50, 0x47, 0xb3, 0x57, 0x1b, 0x94, 0x77, 0xc2, 0x97, 0x69, 0xa5, 0xdc, 0x97, 0x69, 0xe1, 0x6b, - 0xb4, 0xb7, 0x2e, 0xdc, 0x43, 0xc5, 0x89, 0x94, 0xa2, 0x8f, 0x12, 0x11, 0x5d, 0x11, 0x11, 0x1d, - 0x9f, 0xc7, 0xc5, 0x29, 0x30, 0x37, 0x9c, 0xbf, 0x55, 0xbc, 0x00, 0xd4, 0xc5, 0x22, 0xf3, 0x90, - 0x9d, 0x75, 0xab, 0x50, 0xde, 0x0f, 0x98, 0xdb, 0x6d, 0x40, 0x8d, 0xff, 0xba, 0x94, 0x74, 0x7f, - 0x0b, 0x9a, 0xfb, 0xd4, 0xe7, 0x1d, 0xdd, 0x65, 0xcc, 0x1d, 0x73, 0x75, 0x50, 0xb8, 0xca, 0xd5, - 0xc1, 0x1f, 0x57, 0xa1, 0xa6, 0x2e, 0x0c, 0xd1, 0x7b, 0x89, 0x11, 0x6f, 0xae, 0xcd, 0xf6, 0xc2, - 0x37, 0xeb, 0xe1, 0x09, 0x58, 0x0c, 0xa4, 0x9c, 0x88, 0xdf, 0x84, 0x09, 0xfe, 0xab, 0x7b, 0xea, - 0xe4, 0xa1, 0xc8, 0xec, 0x5c, 0xc2, 0x46, 0x2a, 0xa4, 0x51, 0x8b, 0x83, 0xa3, 0x53, 0xca, 0x47, - 0x50, 0x27, 0x96, 0x2f, 0xb6, 0x6c, 0x35, 0x5d, 0x37, 0x46, 0xda, 0xda, 0x52, 0x00, 0x1c, 0x41, - 0xd1, 0xa7, 0x00, 0xe1, 0x73, 0x74, 0x55, 0xf5, 0xfa, 0x68, 0x83, 0x5b, 0x11, 0x06, 0x27, 0xf0, - 0xbc, 0xd1, 0x53, 0xc3, 0xb6, 0x88, 0x11, 0x50, 0x75, 0xf4, 0x1d, 0x6d, 0xf4, 0x89, 0x02, 0xe0, - 0x08, 0x8a, 0x3e, 0x81, 0x46, 0xf8, 0x4c, 0xd4, 0x46, 0x74, 0x73, 0xb4, 0xcd, 0xd0, 0x90, 0xe0, - 0x18, 0x9d, 0xbe, 0x0d, 0x6a, 0xbc, 0xe4, 0x36, 0xe8, 0x87, 0xd0, 0xf2, 0xe5, 0x0c, 0xeb, 0x36, - 0x63, 0xae, 0x36, 0xa3, 0xd6, 0xe0, 0x70, 0x32, 0x13, 0xd3, 0x8f, 0x9b, 0x7e, 0x22, 0x16, 0xde, - 0x84, 0xf2, 0x53, 0x66, 0x39, 0xda, 0xac, 0x30, 0x98, 0x48, 0x1d, 0x9c, 0xb0, 0x50, 0xa1, 0x77, - 0xa1, 0xfa, 0x54, 0xd0, 0x7b, 0x6d, 0x4e, 0x25, 0x47, 0x12, 0x44, 0x09, 0x56, 0x6a, 0x5e, 0x57, - 0x60, 0xf8, 0x27, 0xda, 0xf5, 0x4c, 0x5d, 0x9c, 0xe5, 0x63, 0xa1, 0x42, 0x2b, 0xd1, 0x5d, 0xa5, - 0x26, 0x40, 0xd7, 0xb3, 0xd7, 0xce, 0xd9, 0x1b, 0xca, 0x1e, 0x34, 0xe4, 0xbe, 0xea, 0xb0, 0x33, - 0xed, 0x86, 0xda, 0xf4, 0x22, 0x1b, 0x15, 0xf3, 0xb8, 0x6e, 0xaa, 0x27, 0xee, 0x83, 0x1f, 0x30, - 0x57, 0xeb, 0x64, 0x7c, 0xe0, 0xa9, 0x80, 0x85, 0x0a, 0xdd, 0x86, 0x9a, 0x2f, 0x13, 0x43, 0xbb, - 0xa9, 0xde, 0xd3, 0x26, 0x51, 0x2e, 0x25, 0x38, 0x04, 0x74, 0xee, 0x46, 0xd7, 0x93, 0xaf, 0x7c, - 0x13, 0xd6, 0xfd, 0xd7, 0x39, 0x68, 0x26, 0xae, 0xbc, 0xd0, 0x9d, 0x54, 0x7e, 0xdc, 0xe8, 0x25, - 0xbf, 0x08, 0xc9, 0xc9, 0x91, 0x2f, 0xf2, 0x73, 0xa4, 0x93, 0xb1, 0x1b, 0x9f, 0x27, 0x9f, 0x24, - 0x42, 0x56, 0xe6, 0xc9, 0x1b, 0xb9, 0x6d, 0xe6, 0x84, 0xed, 0x67, 0xc9, 0xb0, 0x95, 0xa9, 0x32, - 0x9f, 0xdf, 0xee, 0xcb, 0x43, 0xb7, 0xf2, 0x2b, 0x12, 0xba, 0xb7, 0xf9, 0x59, 0x5a, 0xae, 0x3a, - 0x5a, 0x26, 0x6c, 0xd4, 0x01, 0x02, 0x87, 0x00, 0xf4, 0x36, 0x54, 0x38, 0xdf, 0x3a, 0x57, 0x11, - 0x1b, 0x7f, 0xe9, 0x22, 0x36, 0x6c, 0x2c, 0x95, 0xbc, 0xc6, 0x90, 0x95, 0x75, 0x32, 0x35, 0xaa, - 0xfd, 0x12, 0x87, 0x00, 0xee, 0xa0, 0xb8, 0xd3, 0xbc, 0x99, 0x71, 0x30, 0x71, 0x89, 0xf9, 0x41, - 0x94, 0x5b, 0xaf, 0x67, 0xee, 0x31, 0x13, 0x51, 0x98, 0xcd, 0xaf, 0x3b, 0x50, 0xb6, 0x99, 0x41, - 0xb4, 0x45, 0x15, 0x94, 0x79, 0x26, 0xbb, 0xcc, 0x20, 0x58, 0xc0, 0x78, 0x1b, 0xfc, 0x97, 0x12, - 0xed, 0xbd, 0x0b, 0xda, 0xd8, 0x15, 0x10, 0xac, 0xa0, 0x68, 0x15, 0x2a, 0xe2, 0x6a, 0x57, 0xbb, - 0x9d, 0xd9, 0x62, 0x92, 0x36, 0xe2, 0xc6, 0x17, 0x4b, 0x20, 0xfa, 0x41, 0x7c, 0x89, 0xbc, 0x94, - 0xb9, 0xc4, 0x1d, 0xb1, 0x49, 0xdc, 0x1c, 0xf3, 0x96, 0xfc, 0x80, 0x79, 0x54, 0x5b, 0xbe, 0xa0, - 0xa5, 0x7d, 0x8e, 0xc0, 0x12, 0xc8, 0x3b, 0x24, 0x1e, 0x88, 0x76, 0xe7, 0x82, 0x0e, 0x09, 0x13, - 0x82, 0x15, 0x14, 0x6d, 0x66, 0x5e, 0xe3, 0xf6, 0x84, 0xe9, 0xc2, 0x18, 0xd3, 0xfc, 0x17, 0xb8, - 0x68, 0x07, 0x26, 0x45, 0x91, 0x9f, 0x1c, 0x64, 0x35, 0x2b, 0x99, 0xd7, 0x27, 0x23, 0xd5, 0x50, - 0xa2, 0x2a, 0x9a, 0xf0, 0x93, 0x45, 0xb4, 0x21, 0x8e, 0x6a, 0x0e, 0x3b, 0xb3, 0x29, 0xe9, 0x53, - 0x6d, 0xf5, 0x02, 0x77, 0xd6, 0x63, 0x1c, 0x4e, 0x1a, 0xa1, 0x6d, 0x68, 0x25, 0x8a, 0x44, 0x7b, - 0x3f, 0xf3, 0x2e, 0x69, 0x4c, 0x25, 0x04, 0xa7, 0xcc, 0x78, 0x4c, 0xbb, 0x92, 0xb9, 0x6a, 0x6b, - 0x99, 0x98, 0x56, 0x8c, 0x16, 0x87, 0x00, 0xbe, 0xa4, 0xba, 0x21, 0xcb, 0xd5, 0x3e, 0xc8, 0x2c, - 0xa9, 0x11, 0xff, 0xc5, 0x31, 0x28, 0xbd, 0x1b, 0x7c, 0x78, 0xf9, 0xdd, 0xe0, 0xd3, 0x4b, 0xed, - 0x06, 0x9f, 0xbd, 0x6c, 0x37, 0xf8, 0xbd, 0xc2, 0xd5, 0xb7, 0x03, 0xf4, 0xa3, 0x24, 0x77, 0x4c, - 0x1c, 0x97, 0x8a, 0x17, 0x1c, 0x97, 0xae, 0x45, 0x16, 0x89, 0x77, 0x5c, 0x1f, 0x41, 0x99, 0x27, - 0x18, 0xba, 0x03, 0xf5, 0xe8, 0x38, 0x58, 0x18, 0x77, 0x1c, 0x8c, 0x20, 0x9d, 0x5f, 0x16, 0xa1, - 0x2a, 0x13, 0x13, 0x7d, 0x31, 0xf2, 0xda, 0xe2, 0xad, 0x0b, 0xf2, 0x78, 0xf4, 0xad, 0x85, 0x3c, - 0x03, 0x88, 0x6b, 0x73, 0x4f, 0x97, 0x5f, 0x70, 0x1c, 0x9e, 0x07, 0x54, 0xde, 0x25, 0x94, 0xf9, - 0x19, 0x40, 0xea, 0x1e, 0x73, 0xd5, 0x06, 0xd7, 0x74, 0xfe, 0xab, 0x10, 0xbf, 0xe7, 0x98, 0x81, - 0x8a, 0xbc, 0x7b, 0x95, 0xdc, 0x56, 0x16, 0xd0, 0x22, 0xb4, 0x07, 0x96, 0xa3, 0xfb, 0x6c, 0xe8, - 0x99, 0xe9, 0x0b, 0xb1, 0xc9, 0x81, 0xe5, 0xec, 0x0b, 0xb1, 0x3c, 0x44, 0x2f, 0xca, 0x2b, 0xc0, - 0x14, 0xb2, 0xa4, 0x90, 0xc6, 0xb3, 0x24, 0x72, 0x19, 0x90, 0x44, 0x11, 0x9d, 0x30, 0xd3, 0xd7, - 0x03, 0x16, 0x18, 0xb6, 0xd8, 0xd0, 0xca, 0xb8, 0xad, 0x34, 0x5b, 0xcc, 0xf4, 0x0f, 0xb8, 0x1c, - 0xf5, 0xe0, 0x5a, 0x88, 0x16, 0xdd, 0x51, 0xf0, 0x8a, 0x80, 0x4f, 0x2b, 0x95, 0xe8, 0x8e, 0xc4, - 0x77, 0x61, 0x42, 0x11, 0x7d, 0x9d, 0x50, 0x3b, 0x50, 0x1f, 0x41, 0xe1, 0xa6, 0x64, 0xf4, 0x5b, - 0x5c, 0xd4, 0xf9, 0x04, 0x2a, 0x62, 0x95, 0xba, 0xe0, 0x28, 0x53, 0xc8, 0x3f, 0xca, 0x74, 0xfe, - 0xbb, 0x10, 0xbf, 0x07, 0xbb, 0xe8, 0x45, 0x53, 0xce, 0x8a, 0x98, 0x3b, 0x65, 0xaf, 0x78, 0x94, - 0xea, 0x9c, 0xbf, 0x6c, 0xc6, 0x6e, 0xc3, 0xb4, 0x5c, 0xe1, 0x93, 0x83, 0x2b, 0x43, 0x60, 0x4a, - 0x2a, 0xe2, 0xb1, 0x5d, 0x06, 0xa4, 0xb0, 0xc9, 0xa1, 0x2d, 0xc9, 0x99, 0x90, 0x9a, 0x78, 0x64, - 0x3b, 0x35, 0xa8, 0x88, 0x25, 0xb7, 0xf3, 0xf7, 0x05, 0xa8, 0xca, 0xc5, 0xf7, 0xd2, 0x41, 0x2b, - 0xe1, 0x39, 0xaf, 0xda, 0x2e, 0xd3, 0x1f, 0xb9, 0xc0, 0xe7, 0xf4, 0x47, 0x2a, 0x52, 0xfd, 0x51, - 0xd8, 0x9c, 0xfe, 0x48, 0x4d, 0xa2, 0x3f, 0xbf, 0x5f, 0x48, 0x7f, 0xad, 0xf3, 0xca, 0xc1, 0xf0, - 0xdd, 0xad, 0x1e, 0xeb, 0x30, 0x91, 0xda, 0x4b, 0xae, 0x10, 0x98, 0x5f, 0x40, 0x33, 0xb1, 0x03, - 0x5c, 0xa1, 0x82, 0x2f, 0xa1, 0x95, 0xdc, 0x42, 0x5e, 0xbd, 0x86, 0xee, 0xdf, 0x22, 0xa8, 0xca, - 0xaf, 0x0b, 0xd0, 0x62, 0x8a, 0x56, 0xcf, 0xf4, 0xd4, 0xd7, 0xda, 0x39, 0x8c, 0xfa, 0x6e, 0x3e, - 0xa3, 0x9e, 0x8d, 0x4d, 0xc6, 0x93, 0xe9, 0x0f, 0x47, 0xc8, 0xb4, 0x96, 0x6d, 0x29, 0x87, 0x47, - 0x7f, 0x3c, 0xca, 0xa3, 0x3b, 0x23, 0xad, 0xfd, 0x9a, 0x42, 0xe7, 0x51, 0xe8, 0x4b, 0x10, 0xde, - 0x5e, 0x86, 0xf0, 0xce, 0x65, 0x3e, 0x3c, 0xc9, 0x72, 0xdd, 0xc5, 0x14, 0xd7, 0x9d, 0xc9, 0xa2, - 0x13, 0x34, 0xb7, 0x97, 0xa1, 0xb9, 0x73, 0x79, 0xd8, 0x04, 0xc3, 0x5d, 0x4a, 0x33, 0xdc, 0xd9, - 0x2c, 0x3c, 0x45, 0x6e, 0xdf, 0xcf, 0x92, 0xdb, 0xeb, 0xb9, 0xf0, 0x24, 0xaf, 0x5d, 0x4a, 0xf3, - 0xda, 0x91, 0xfa, 0x53, 0x94, 0xb6, 0x97, 0xa1, 0xb4, 0x73, 0xb9, 0xe8, 0x98, 0xcd, 0x7e, 0x9e, - 0xcb, 0x66, 0x6f, 0x8e, 0x5a, 0x8d, 0x21, 0xb2, 0x5b, 0x63, 0x88, 0xec, 0x1b, 0xb9, 0x35, 0x8c, - 0xe3, 0xb0, 0xbf, 0x26, 0x8e, 0xdf, 0x57, 0xe2, 0xf8, 0xf3, 0x98, 0x38, 0xde, 0x1d, 0xd9, 0x83, - 0x6f, 0xe5, 0x67, 0xc6, 0x77, 0xc2, 0x19, 0xff, 0xf1, 0x57, 0x8f, 0x33, 0x7e, 0x1b, 0x3e, 0xf8, - 0x28, 0xa6, 0x83, 0xaf, 0xce, 0x1f, 0x10, 0x94, 0x07, 0x7c, 0x01, 0x91, 0x6f, 0xfb, 0xc4, 0x73, - 0xcc, 0xb2, 0x7e, 0xb7, 0x14, 0xb1, 0xac, 0x55, 0x98, 0x89, 0x3e, 0xed, 0x4b, 0xf6, 0x5f, 0xbe, - 0x7a, 0x40, 0x91, 0x2e, 0x1e, 0x81, 0x35, 0x98, 0x8d, 0x2d, 0x92, 0x63, 0x20, 0x27, 0xf6, 0x5a, - 0xa4, 0x4c, 0x30, 0xe7, 0x65, 0x40, 0xc4, 0xe3, 0xd1, 0x9d, 0x6a, 0x43, 0xb1, 0x27, 0xa5, 0x49, - 0x8d, 0x71, 0x88, 0x4e, 0xd6, 0x2f, 0xa7, 0x64, 0x5a, 0xa9, 0x12, 0xb5, 0xff, 0x04, 0xda, 0xf1, - 0x1b, 0x7d, 0xb5, 0x24, 0x55, 0x32, 0x5f, 0x01, 0xa7, 0x96, 0xc2, 0xe8, 0xc3, 0x46, 0x4f, 0xad, - 0x4d, 0x53, 0x6e, 0x5a, 0xd0, 0xd1, 0x61, 0x2a, 0x83, 0x41, 0x1d, 0xf1, 0x81, 0x0b, 0x19, 0x9a, - 0x2a, 0x8b, 0x5a, 0x38, 0x2a, 0xf3, 0x68, 0x4d, 0x06, 0xa3, 0x2c, 0x70, 0x0b, 0xf5, 0x67, 0x48, - 0xf2, 0x75, 0x6a, 0x03, 0x47, 0xe5, 0xce, 0x93, 0x34, 0x41, 0x1c, 0x97, 0xf3, 0x85, 0x57, 0xcd, - 0xf9, 0xa9, 0x0c, 0xdd, 0xbb, 0xbd, 0x04, 0x15, 0xf1, 0xe7, 0x56, 0x08, 0xa0, 0xba, 0xf7, 0x78, - 0x63, 0x77, 0x67, 0xb3, 0xfd, 0x1a, 0x6a, 0x42, 0x6d, 0x0f, 0xef, 0x3c, 0x59, 0x3f, 0xd8, 0x6e, - 0x17, 0x50, 0x03, 0x2a, 0xbb, 0x8f, 0x36, 0xd7, 0x77, 0xdb, 0xc5, 0xb5, 0xfb, 0x50, 0x57, 0x7f, - 0x0e, 0xe3, 0xa1, 0xcf, 0xa1, 0xa6, 0x9e, 0x51, 0xbc, 0x61, 0xa5, 0xff, 0x50, 0xab, 0xa3, 0x8d, - 0x2a, 0x24, 0xc9, 0x59, 0x2d, 0xac, 0xed, 0x42, 0x5d, 0x7d, 0x6a, 0xe5, 0xa1, 0x2f, 0xa1, 0xa6, - 0x9e, 0x13, 0x75, 0xa5, 0x3f, 0x98, 0x4b, 0xd4, 0x95, 0xf9, 0x42, 0x6b, 0xb1, 0xb0, 0x5a, 0x58, - 0x3b, 0x86, 0xc9, 0xf4, 0x47, 0x4c, 0xe8, 0x09, 0x4c, 0x89, 0x87, 0x48, 0xec, 0xa3, 0x5b, 0xc9, - 0x85, 0x75, 0xf4, 0x53, 0xa8, 0xce, 0xfc, 0x58, 0x7d, 0xa2, 0xa5, 0x33, 0xa8, 0xee, 0xca, 0xbf, - 0xda, 0xe9, 0x45, 0x9c, 0x73, 0x2a, 0x13, 0x47, 0x9d, 0xac, 0x80, 0x5b, 0xa2, 0xcf, 0xd2, 0xf7, - 0xbf, 0x33, 0x79, 0xc7, 0x95, 0x4e, 0xae, 0x54, 0x34, 0xfc, 0x97, 0xd1, 0x67, 0x40, 0xef, 0xc7, - 0x2f, 0x59, 0xda, 0xd9, 0x0b, 0xf3, 0xce, 0x88, 0x44, 0xb4, 0xfd, 0xff, 0xeb, 0xeb, 0xc6, 0xe7, - 0xdf, 0xfc, 0xdb, 0xad, 0xd7, 0xbe, 0xf9, 0xc5, 0xad, 0xc2, 0x3f, 0xfc, 0xe2, 0x56, 0xe1, 0x4f, - 0xff, 0xfd, 0x56, 0xe1, 0x77, 0x96, 0x2f, 0xf5, 0x67, 0x40, 0xaa, 0xbe, 0xc3, 0xaa, 0x10, 0x7d, - 0xf0, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xc0, 0xdd, 0xc1, 0x81, 0x13, 0x3a, 0x00, 0x00, + // 4432 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x7b, 0x3d, 0x6c, 0x1c, 0x49, + 0x76, 0xbf, 0xe6, 0x7b, 0xe6, 0xcd, 0x90, 0x1c, 0x96, 0x48, 0xaa, 0xd5, 0xd2, 0x8a, 0xd4, 0xec, + 0xee, 0x7f, 0xb9, 0x22, 0x35, 0xe4, 0x72, 0xb5, 0x77, 0x5a, 0xfd, 0x57, 0xbb, 0xe2, 0x97, 0x4e, + 0xd4, 0x51, 0x12, 0xaf, 0x48, 0x09, 0xb6, 0x93, 0x46, 0xb3, 0xbb, 0x38, 0x6c, 0xb1, 0xa7, 0xab, + 0xb7, 0xbb, 0x87, 0x14, 0x2f, 0x32, 0xe0, 0xc4, 0x80, 0x03, 0x27, 0x4e, 0x0c, 0x27, 0xce, 0xfc, + 0x01, 0x38, 0x70, 0x74, 0xc0, 0x05, 0x8e, 0x0c, 0x78, 0xe1, 0xc8, 0x76, 0x60, 0xd8, 0x89, 0x00, + 0x9f, 0x53, 0x67, 0x67, 0x03, 0xb6, 0xe0, 0xc0, 0xa8, 0x8f, 0xfe, 0x9c, 0x1e, 0x8a, 0xe2, 0x2e, + 0xec, 0xc5, 0xe1, 0x02, 0x69, 0xba, 0xde, 0x47, 0xd5, 0xab, 0xaa, 0xf7, 0x5e, 0xfd, 0xea, 0x75, + 0x13, 0x3a, 0x3d, 0xba, 0xe4, 0x7a, 0x34, 0xa0, 0x06, 0xb5, 0xfd, 0x25, 0x6f, 0xe0, 0x04, 0x56, + 0x9f, 0x84, 0xbf, 0x5d, 0xce, 0x41, 0x35, 0xd9, 0x54, 0x6f, 0xec, 0x7b, 0xf4, 0x88, 0x78, 0x91, + 0x42, 0xf4, 0x20, 0x04, 0xd5, 0x39, 0x83, 0x3a, 0xfe, 0xa0, 0x7f, 0x86, 0x44, 0x7a, 0x38, 0x43, + 0x77, 0x83, 0x81, 0x47, 0xc2, 0xdf, 0xb0, 0x97, 0x94, 0x8c, 0x49, 0x3c, 0xeb, 0x98, 0xc8, 0x1f, + 0x29, 0x71, 0x3d, 0x25, 0x71, 0x60, 0xd3, 0x13, 0xfe, 0x9f, 0xe4, 0xde, 0x4a, 0x71, 0xfb, 0x7a, + 0x40, 0x3c, 0x4b, 0xb7, 0xad, 0x9f, 0x92, 0xe4, 0xb3, 0x94, 0x55, 0x53, 0xb2, 0xd4, 0xe5, 0xff, + 0x72, 0x6d, 0xf5, 0x0f, 0x07, 0x07, 0x07, 0x36, 0x09, 0x7f, 0xa5, 0xcc, 0x54, 0x8f, 0xf6, 0x28, + 0x7f, 0x5c, 0x62, 0x4f, 0x82, 0xda, 0xf9, 0xab, 0x02, 0x4c, 0xee, 0xe9, 0xfe, 0xd1, 0x2e, 0xf1, + 0x8e, 0x2d, 0x83, 0xac, 0x53, 0xe7, 0xc0, 0xea, 0xa1, 0x1b, 0xd0, 0xb4, 0x69, 0x4f, 0x3b, 0xb0, + 0x6c, 0xa2, 0x1d, 0x98, 0x4a, 0x61, 0xae, 0x30, 0x5f, 0xc1, 0x0d, 0x9b, 0xf6, 0x1e, 0x5a, 0x36, + 0x79, 0x68, 0xa2, 0x6b, 0xd0, 0x08, 0x74, 0xff, 0x48, 0x73, 0xf4, 0x3e, 0x51, 0x8a, 0x73, 0x85, + 0xf9, 0x06, 0xae, 0x33, 0xc2, 0x53, 0xbd, 0x4f, 0xd0, 0x55, 0xa8, 0x0f, 0x4c, 0x5f, 0x73, 0xf5, + 0xe0, 0x50, 0x29, 0x71, 0x5e, 0x6d, 0x60, 0xfa, 0x3b, 0x7a, 0x70, 0x88, 0x16, 0x60, 0xd2, 0xa0, + 0x4e, 0xa0, 0x5b, 0x0e, 0xf1, 0x34, 0x87, 0x04, 0x27, 0xd4, 0x3b, 0x52, 0xca, 0x5c, 0xa6, 0x1d, + 0x31, 0x9e, 0x0a, 0x3a, 0xfa, 0x00, 0x2a, 0xae, 0xad, 0x3b, 0x44, 0xa9, 0xce, 0x15, 0xe6, 0xc7, + 0x57, 0xc6, 0xbb, 0xe1, 0x56, 0xef, 0x30, 0x2a, 0x16, 0xcc, 0xce, 0x7f, 0x95, 0x61, 0x7c, 0x57, + 0x4c, 0x14, 0x93, 0xaf, 0x07, 0xc4, 0x0f, 0xd0, 0x16, 0xd4, 0x5e, 0xd2, 0x81, 0xe7, 0xe8, 0x36, + 0xb7, 0xbc, 0xb1, 0xb6, 0xf4, 0xe6, 0xf5, 0xec, 0x42, 0x8f, 0x76, 0x7b, 0xfa, 0x4f, 0x49, 0x10, + 0x90, 0xae, 0x49, 0x8e, 0x97, 0x0c, 0xea, 0x91, 0xa5, 0x8c, 0x93, 0x74, 0x1f, 0x0b, 0x35, 0x1c, + 0xea, 0xa3, 0x19, 0xa8, 0x7a, 0xc4, 0xb5, 0xf5, 0x53, 0x3e, 0xcb, 0x3a, 0x96, 0x2d, 0x36, 0xc7, + 0xfd, 0x81, 0x65, 0x9b, 0x9a, 0x65, 0x86, 0x73, 0xe4, 0xed, 0x2d, 0x13, 0x3d, 0x84, 0x2a, 0x3d, + 0x38, 0xf0, 0x49, 0xc0, 0x27, 0x56, 0x5a, 0xeb, 0xbe, 0x79, 0x3d, 0x7b, 0xeb, 0x3c, 0x83, 0x3f, + 0xe3, 0x5a, 0x58, 0x6a, 0xa3, 0x27, 0x00, 0xc4, 0x31, 0x35, 0xd9, 0x57, 0xe5, 0x42, 0x7d, 0x35, + 0x88, 0x63, 0x8a, 0x47, 0xb4, 0x00, 0x15, 0x4f, 0x77, 0x7a, 0x62, 0x35, 0x9b, 0x2b, 0x13, 0x5d, + 0xee, 0x86, 0x98, 0x91, 0x76, 0x5d, 0x62, 0xac, 0x95, 0xbf, 0x79, 0x3d, 0x7b, 0x09, 0x0b, 0x19, + 0xb4, 0x0b, 0x4d, 0x83, 0x52, 0xcf, 0xb4, 0x1c, 0x3d, 0xa0, 0x9e, 0x52, 0xe3, 0xab, 0xf8, 0xc9, + 0x9b, 0xd7, 0xb3, 0xb7, 0xf3, 0x06, 0x1f, 0x0a, 0xa5, 0xee, 0xee, 0xa1, 0xee, 0x99, 0x5b, 0x1b, + 0x38, 0xd9, 0x0b, 0x5a, 0x06, 0xf0, 0x88, 0x4f, 0xed, 0x41, 0x60, 0x51, 0x47, 0xa9, 0x73, 0x33, + 0xda, 0xdd, 0x48, 0xe7, 0x11, 0xd1, 0x4d, 0xe2, 0xe1, 0x84, 0x0c, 0x7a, 0x1f, 0xc6, 0xa4, 0x0f, + 0x6b, 0x96, 0x63, 0x92, 0x57, 0x4a, 0x63, 0xae, 0x30, 0x3f, 0x86, 0x5b, 0x92, 0xb8, 0xc5, 0x68, + 0xe8, 0x0e, 0x00, 0x8f, 0x38, 0x9d, 0x77, 0x0b, 0xbc, 0xdb, 0x29, 0x31, 0xbb, 0x75, 0x6a, 0xdb, + 0xc4, 0x60, 0x74, 0x36, 0x45, 0x9c, 0x90, 0x43, 0xeb, 0x30, 0x11, 0x87, 0x98, 0x50, 0x6d, 0x72, + 0xd5, 0xab, 0x42, 0xf5, 0x49, 0x9a, 0xc9, 0xf5, 0xb3, 0x1a, 0x9d, 0x7f, 0x28, 0xc3, 0x44, 0xe4, + 0x7b, 0xbe, 0x4b, 0x1d, 0x9f, 0xa0, 0x79, 0xa8, 0xfa, 0x81, 0x1e, 0x0c, 0x7c, 0xee, 0x7b, 0xe3, + 0x2b, 0xed, 0x6e, 0xb8, 0x3c, 0xdd, 0x5d, 0x4e, 0xc7, 0x92, 0xcf, 0x24, 0x0f, 0xf9, 0x9c, 0xb9, + 0x6f, 0xe5, 0xad, 0x85, 0xe4, 0xa3, 0x0f, 0x61, 0x3c, 0x20, 0x5e, 0xdf, 0x72, 0x74, 0x5b, 0x23, + 0x9e, 0x47, 0x3d, 0xe9, 0x73, 0x63, 0x21, 0x75, 0x93, 0x11, 0xd1, 0x4f, 0xa0, 0xe5, 0x11, 0xdd, + 0xd4, 0x82, 0x43, 0x8f, 0x0e, 0x7a, 0x87, 0x17, 0xf4, 0xbf, 0x26, 0xeb, 0x63, 0x4f, 0x74, 0xc1, + 0x9c, 0xf0, 0xc4, 0xb3, 0x02, 0xa2, 0x31, 0x4b, 0x2e, 0xea, 0x84, 0xbc, 0x07, 0x36, 0x25, 0xb4, + 0x05, 0x15, 0xdd, 0x23, 0x8e, 0xce, 0x9d, 0xb0, 0xb5, 0xf6, 0xe9, 0x9b, 0xd7, 0xb3, 0x4b, 0x3d, + 0x2b, 0x38, 0x1c, 0xec, 0x77, 0x0d, 0xda, 0x5f, 0x22, 0x7e, 0x30, 0xd0, 0xbd, 0x53, 0x91, 0x26, + 0x87, 0x12, 0x67, 0x77, 0x95, 0xa9, 0x62, 0xd1, 0x03, 0xfa, 0x10, 0xca, 0x26, 0x35, 0x7c, 0xa5, + 0x36, 0x57, 0x9a, 0x6f, 0xae, 0x34, 0xc5, 0xae, 0xed, 0xda, 0x96, 0x41, 0xa4, 0x2b, 0x73, 0x36, + 0x7a, 0x04, 0x35, 0x11, 0x41, 0xbe, 0x52, 0x9f, 0x2b, 0x5d, 0xc0, 0xfa, 0x50, 0x9d, 0xf9, 0xd9, + 0x60, 0x60, 0x99, 0x9a, 0xab, 0x7b, 0x81, 0xaf, 0x34, 0xf8, 0xb0, 0x32, 0x8a, 0x9e, 0x3f, 0xdf, + 0xda, 0xd8, 0x61, 0x64, 0x39, 0x74, 0x83, 0x09, 0x72, 0x02, 0x73, 0x7a, 0x57, 0x37, 0x8e, 0x88, + 0xa9, 0x1d, 0x91, 0x53, 0x05, 0x46, 0x19, 0xdb, 0x10, 0x42, 0x3f, 0x26, 0xa7, 0x1d, 0x13, 0x26, + 0x31, 0x35, 0x8e, 0xfc, 0x8d, 0xb5, 0x0d, 0xe2, 0x1b, 0x9e, 0xe5, 0xb2, 0xd8, 0x59, 0x04, 0xe4, + 0x31, 0xa2, 0xb9, 0xaf, 0x11, 0xe7, 0x58, 0xeb, 0x93, 0xbe, 0x1b, 0x78, 0xdc, 0xc3, 0xaa, 0xb8, + 0x2d, 0x39, 0x9b, 0xce, 0xf1, 0x13, 0x4e, 0x47, 0x37, 0xa1, 0x15, 0x4a, 0xf3, 0x2c, 0x2c, 0x32, + 0x74, 0x53, 0xd2, 0x58, 0x26, 0xee, 0xfc, 0x41, 0x11, 0x1a, 0xeb, 0x61, 0xc6, 0x45, 0x57, 0xa0, + 0x66, 0xb9, 0x9a, 0x6e, 0x9a, 0xa2, 0xcf, 0x06, 0xae, 0x5a, 0xee, 0xaa, 0x69, 0x7a, 0xe8, 0x07, + 0x30, 0x26, 0xd3, 0xb4, 0xe6, 0x52, 0x36, 0xef, 0x22, 0x9f, 0xc1, 0xa4, 0x98, 0x81, 0xcc, 0xd4, + 0x3b, 0xd4, 0x0b, 0x70, 0xcb, 0x89, 0x1b, 0x3e, 0xda, 0x85, 0xc9, 0xbe, 0xee, 0xba, 0xc4, 0xd4, + 0x0e, 0xa9, 0x1f, 0x48, 0xdd, 0x12, 0xd7, 0xfd, 0x28, 0xca, 0xe3, 0xd1, 0xf8, 0xdd, 0x27, 0x5c, + 0xf6, 0x11, 0xf5, 0x03, 0xae, 0xbe, 0xe9, 0x04, 0xde, 0x29, 0x0b, 0xb7, 0x14, 0x15, 0xbd, 0x07, + 0x30, 0xf0, 0xf5, 0x1e, 0xd1, 0x3c, 0x3d, 0x20, 0xdc, 0xbb, 0x8b, 0xb8, 0xc1, 0x29, 0x58, 0x0f, + 0x88, 0xba, 0x06, 0x53, 0x79, 0xfd, 0xa0, 0x36, 0x94, 0xd8, 0xda, 0x17, 0x78, 0xee, 0x60, 0x8f, + 0x68, 0x0a, 0x2a, 0xc7, 0xba, 0x3d, 0x08, 0x8f, 0x2e, 0xd1, 0xb8, 0x57, 0xbc, 0x5b, 0xe8, 0xfc, + 0x79, 0x11, 0x26, 0xd7, 0xc5, 0x11, 0x2f, 0x4f, 0x93, 0xcd, 0x57, 0x2c, 0x77, 0xb2, 0xb3, 0x4f, + 0xb3, 0xc9, 0x31, 0xb1, 0x65, 0x58, 0x8f, 0x77, 0xd9, 0xe9, 0xbb, 0x4d, 0x7b, 0xdd, 0x6d, 0x46, + 0xc5, 0x75, 0x9b, 0xf6, 0xf8, 0x13, 0xda, 0x8a, 0xb7, 0xca, 0x8c, 0x36, 0x50, 0x86, 0xb8, 0x1a, + 0xcd, 0x7d, 0x68, 0x8b, 0xf1, 0xa4, 0xd4, 0x4a, 0xec, 0xfa, 0x16, 0xb4, 0xfc, 0x40, 0xf7, 0x02, + 0xcd, 0xa0, 0xfd, 0xbe, 0x15, 0xf0, 0xa8, 0x6f, 0xae, 0xfc, 0xbf, 0x78, 0x01, 0xb3, 0x96, 0xb2, + 0x14, 0xe3, 0x05, 0xeb, 0x5c, 0x1a, 0x37, 0xfd, 0xb8, 0xa1, 0x62, 0x68, 0x26, 0x78, 0x68, 0x1d, + 0x90, 0xec, 0x44, 0x33, 0x0e, 0x89, 0x71, 0xe4, 0x52, 0xcb, 0x09, 0xf8, 0xd4, 0x58, 0xf2, 0x8c, + 0x32, 0xd6, 0x7a, 0xc4, 0xc3, 0x93, 0x52, 0x3e, 0x26, 0x75, 0xfe, 0xbb, 0x0c, 0x28, 0x32, 0x41, + 0xa4, 0x3f, 0xb6, 0x5a, 0xcb, 0xd0, 0x88, 0xce, 0x72, 0xd9, 0x25, 0x1a, 0xde, 0x73, 0x1c, 0x0b, + 0xa1, 0x7b, 0x50, 0xa5, 0x2e, 0x71, 0x88, 0x29, 0x97, 0xa9, 0x33, 0x3c, 0xc3, 0xa8, 0xfb, 0xee, + 0x33, 0x2e, 0x89, 0xa5, 0x06, 0x7a, 0x00, 0x75, 0x89, 0xc9, 0x4c, 0xb9, 0x3e, 0x1f, 0x9c, 0xa5, + 0x2d, 0x49, 0x26, 0x8e, 0xb4, 0xd0, 0x43, 0x80, 0xc4, 0x1a, 0x94, 0x47, 0xad, 0x71, 0xa2, 0x8f, + 0x78, 0x55, 0x12, 0x9a, 0xea, 0x13, 0xa8, 0x0a, 0xdb, 0xbe, 0x93, 0xd5, 0x55, 0x5f, 0x40, 0x3d, + 0x34, 0x96, 0x79, 0xfe, 0x11, 0x39, 0xd5, 0x44, 0x92, 0xe0, 0x1d, 0xb5, 0x70, 0xe3, 0x88, 0x9c, + 0xee, 0x70, 0x02, 0x83, 0x55, 0x2c, 0x2b, 0x59, 0xec, 0x50, 0xf2, 0x43, 0xa9, 0x22, 0x97, 0x6a, + 0xc7, 0x0c, 0x21, 0xac, 0x9e, 0x00, 0xc4, 0xa3, 0xa0, 0x39, 0xa8, 0xb0, 0xe3, 0xc8, 0x97, 0xd6, + 0x01, 0x77, 0x6b, 0x76, 0x50, 0xf9, 0x58, 0x30, 0xd0, 0x8f, 0xa0, 0xe9, 0x52, 0xdb, 0xd6, 0x3c, + 0xe2, 0x0f, 0xec, 0x80, 0x77, 0x3b, 0x7e, 0xf6, 0xfa, 0xec, 0x50, 0xdb, 0xc6, 0x5c, 0x1a, 0x83, + 0x1b, 0x3d, 0x77, 0x9e, 0x02, 0xc4, 0x1c, 0xd4, 0x84, 0xda, 0xd6, 0xd3, 0x17, 0xab, 0xdb, 0x5b, + 0x1b, 0xed, 0x4b, 0xa8, 0x01, 0x15, 0xbc, 0xb9, 0xba, 0xf1, 0x9b, 0xed, 0x02, 0x1a, 0x83, 0xc6, + 0xd3, 0x67, 0x7b, 0x9a, 0x68, 0x16, 0x51, 0x0b, 0xea, 0xeb, 0xcf, 0x9e, 0x6d, 0x6b, 0xcf, 0x1e, + 0x3e, 0x6c, 0x97, 0x98, 0x12, 0xde, 0xdc, 0xdd, 0x5b, 0xc5, 0x7b, 0xed, 0x72, 0xe7, 0xdf, 0x0a, + 0xd0, 0xde, 0xe0, 0x58, 0xfb, 0x7b, 0x10, 0xaa, 0x2b, 0x50, 0x66, 0x0e, 0x29, 0x5d, 0xf0, 0x46, + 0xa4, 0x9c, 0x35, 0x90, 0xbb, 0x2f, 0xe6, 0xb2, 0xea, 0x22, 0x94, 0x59, 0x0b, 0x7d, 0x00, 0xe3, + 0xfe, 0xd7, 0x36, 0x3b, 0x65, 0x8f, 0x0f, 0x7c, 0x6d, 0xe0, 0x59, 0x32, 0x09, 0xb7, 0x04, 0xf5, + 0xc5, 0x81, 0xff, 0xdc, 0xb3, 0x3a, 0xff, 0x5e, 0x82, 0xc9, 0xb0, 0xb7, 0x6f, 0x13, 0x6c, 0x9f, + 0x67, 0x82, 0xed, 0xe6, 0x90, 0xad, 0x23, 0x63, 0x6d, 0x0d, 0x1a, 0xee, 0x60, 0xdf, 0xb6, 0xfc, + 0xc3, 0x9c, 0x60, 0x1b, 0xd6, 0xde, 0x09, 0x65, 0x71, 0xac, 0x86, 0xbe, 0x80, 0xda, 0x81, 0x3d, + 0xe0, 0x3d, 0x94, 0x33, 0xc1, 0x3e, 0xdc, 0xc3, 0x43, 0x21, 0x89, 0x43, 0x95, 0xef, 0x3a, 0xc6, + 0x02, 0x68, 0x44, 0x46, 0xb2, 0x4b, 0x4d, 0x5f, 0x7f, 0xa5, 0x19, 0x36, 0x35, 0x8e, 0xe4, 0xd1, + 0x5a, 0xef, 0xeb, 0xaf, 0xd6, 0x59, 0x3b, 0x13, 0x81, 0xc5, 0x73, 0x45, 0x60, 0x69, 0x44, 0x04, + 0x2e, 0x40, 0x4d, 0x4e, 0xec, 0xed, 0xe1, 0xd7, 0xf9, 0xfd, 0x02, 0x4c, 0xc7, 0x60, 0xf4, 0x7b, + 0xe0, 0xea, 0x9d, 0x9f, 0x17, 0x60, 0x26, 0x65, 0xd1, 0xb7, 0xf1, 0xc6, 0xd5, 0xd8, 0x1d, 0x84, + 0x31, 0x31, 0x3c, 0xc8, 0x1f, 0x63, 0xd8, 0x27, 0xde, 0x69, 0x39, 0x7f, 0x5e, 0x86, 0xf1, 0x75, + 0xda, 0xdf, 0xb7, 0x9c, 0xe8, 0xba, 0xb8, 0x2c, 0x43, 0x57, 0xe8, 0x5c, 0x4f, 0xd8, 0x9b, 0x14, + 0x4b, 0x04, 0x2e, 0xba, 0x0d, 0x25, 0xdd, 0x0c, 0x0d, 0xbe, 0x36, 0x4a, 0x61, 0xd5, 0x34, 0x31, + 0x93, 0x53, 0xff, 0xb1, 0x28, 0x03, 0xfd, 0x01, 0xd4, 0xf7, 0x2d, 0xc7, 0xb4, 0x9c, 0x1e, 0xb3, + 0xb0, 0x94, 0x3e, 0xab, 0x86, 0x47, 0xeb, 0xae, 0x09, 0x61, 0x1c, 0x69, 0xa9, 0xbf, 0x57, 0x84, + 0x9a, 0xa4, 0x22, 0x04, 0xe5, 0x83, 0x81, 0x2d, 0xb6, 0xbe, 0x8e, 0xf9, 0x73, 0x88, 0x75, 0x18, + 0x4a, 0x6b, 0x08, 0xac, 0x73, 0x17, 0x9a, 0xae, 0x47, 0x5f, 0x8a, 0x6b, 0x50, 0x88, 0xc1, 0xda, + 0x02, 0xbf, 0xed, 0x44, 0x0c, 0x09, 0x43, 0x93, 0xa2, 0xe8, 0x3e, 0x34, 0x7d, 0xe3, 0x90, 0xf4, + 0x75, 0xed, 0xa5, 0x4f, 0x1d, 0x1e, 0xad, 0xad, 0xb5, 0xeb, 0x6f, 0x5e, 0xcf, 0x2a, 0xc4, 0x31, + 0x28, 0x33, 0x61, 0x89, 0x31, 0xba, 0x58, 0x3f, 0x79, 0x42, 0x7c, 0x0e, 0xc3, 0x40, 0x28, 0x3c, + 0xf6, 0xa9, 0x83, 0xba, 0x00, 0x3e, 0xf1, 0x34, 0x97, 0xda, 0x96, 0x71, 0xca, 0xaf, 0x0e, 0x11, + 0x5e, 0xde, 0x25, 0xde, 0x0e, 0x27, 0xe3, 0x86, 0x1f, 0x3e, 0xf2, 0xb2, 0x01, 0xc7, 0xd7, 0x81, + 0xc7, 0xaf, 0x07, 0x0d, 0x5c, 0xe3, 0x30, 0x3a, 0xf0, 0xd8, 0x2d, 0x9c, 0x43, 0x34, 0x81, 0xf6, + 0x1b, 0x58, 0xb6, 0x54, 0x07, 0x4a, 0xab, 0xa6, 0x89, 0x14, 0xa8, 0xc9, 0x05, 0x92, 0x20, 0x2f, + 0x6c, 0xa2, 0x1f, 0x42, 0xdd, 0xa4, 0x86, 0xb0, 0xbf, 0x78, 0x0e, 0xfb, 0x6b, 0x26, 0x35, 0xb8, + 0xf1, 0x53, 0x50, 0x39, 0xf0, 0xa8, 0x23, 0x20, 0x57, 0x1d, 0x8b, 0x46, 0xe7, 0x9f, 0x0a, 0x30, + 0x11, 0xed, 0x93, 0xbc, 0xef, 0x8d, 0x1e, 0x5c, 0x81, 0x9a, 0x49, 0x6c, 0x12, 0x48, 0xd7, 0xae, + 0xe3, 0xb0, 0x99, 0x32, 0xab, 0x74, 0x21, 0xb3, 0xca, 0x09, 0xb3, 0x32, 0xb9, 0xa9, 0x92, 0xcd, + 0x4d, 0xef, 0xc3, 0x98, 0x58, 0xaf, 0x50, 0x82, 0x5f, 0xbe, 0x70, 0x4b, 0x10, 0x85, 0x50, 0xe7, + 0x0a, 0x4c, 0xaf, 0x53, 0xc7, 0x21, 0x46, 0x40, 0xbd, 0x1d, 0x8f, 0xbe, 0x3a, 0x95, 0x8e, 0xd8, + 0xf9, 0xa3, 0x02, 0xcc, 0x64, 0x39, 0x72, 0xea, 0x8f, 0xa1, 0xc6, 0xae, 0x0c, 0xc4, 0xf7, 0x65, + 0x9d, 0x65, 0xf9, 0xcd, 0xeb, 0xd9, 0xc5, 0xf3, 0xdc, 0xad, 0x36, 0x1d, 0x53, 0xe4, 0xe4, 0xb0, + 0x03, 0xb6, 0xfb, 0x2e, 0xeb, 0x5c, 0xb3, 0x4c, 0x89, 0xca, 0x6b, 0xbc, 0xbd, 0x65, 0x22, 0x15, + 0x4a, 0x36, 0xed, 0xc9, 0xf3, 0xa6, 0x1e, 0x66, 0x38, 0xcc, 0x88, 0x9d, 0xbf, 0x2c, 0x41, 0xf9, + 0x31, 0xb5, 0x1c, 0x74, 0x0b, 0x26, 0x49, 0x60, 0x98, 0x5a, 0x9f, 0x9a, 0x9a, 0x47, 0x8e, 0x2d, + 0x9f, 0xdd, 0xe8, 0x99, 0x55, 0x25, 0x3c, 0xc1, 0x18, 0x4f, 0xa8, 0x89, 0x25, 0x19, 0x2d, 0x40, + 0xd5, 0x3f, 0xd4, 0x3d, 0x33, 0xbc, 0xcd, 0x5c, 0x8e, 0x82, 0x90, 0x75, 0x25, 0x8a, 0x17, 0x58, + 0x8a, 0xa0, 0x59, 0x68, 0xf2, 0x27, 0x59, 0x81, 0x28, 0xf1, 0x3d, 0x06, 0x4e, 0x12, 0xf5, 0x87, + 0x05, 0x98, 0x0c, 0x8b, 0x14, 0xa6, 0xe5, 0xf1, 0x65, 0x3a, 0x0d, 0x6b, 0x5a, 0x92, 0xb1, 0x11, + 0xd2, 0xd1, 0xc7, 0x10, 0xd2, 0x34, 0x22, 0xd7, 0x80, 0x6f, 0x58, 0x03, 0x4f, 0x48, 0x7a, 0xb8, + 0x34, 0xe8, 0x23, 0x98, 0xb0, 0xf9, 0xf5, 0x3f, 0x96, 0x14, 0x61, 0x31, 0x2e, 0xc8, 0xa1, 0xa0, + 0xfa, 0x17, 0x05, 0xa8, 0x70, 0x9b, 0xd1, 0x38, 0x14, 0x2d, 0x53, 0x82, 0x87, 0xa2, 0x65, 0xa2, + 0x2e, 0xd4, 0x6d, 0x7d, 0x9f, 0xd8, 0xcc, 0x39, 0x8b, 0x32, 0x1b, 0xf3, 0x8c, 0xc8, 0xa4, 0xb7, + 0x25, 0x07, 0x47, 0x32, 0x68, 0x05, 0x6a, 0x1e, 0xd1, 0x99, 0xa5, 0x72, 0xb5, 0x95, 0xb8, 0x24, + 0xb1, 0xe3, 0x51, 0x83, 0xf8, 0xfe, 0xae, 0x4b, 0x8c, 0xee, 0xd6, 0x06, 0x0e, 0x05, 0xd1, 0x32, + 0x4c, 0xf1, 0x85, 0x37, 0x3c, 0xa2, 0x07, 0x24, 0x5e, 0x7b, 0x5e, 0x7c, 0xc0, 0x88, 0xf1, 0xd6, + 0x39, 0x2b, 0x5c, 0xfe, 0xce, 0x1d, 0xa8, 0xb2, 0x75, 0x26, 0x26, 0xdb, 0x34, 0x76, 0xe2, 0x72, + 0xfd, 0xec, 0xa6, 0xf5, 0xf5, 0x57, 0x9b, 0x81, 0x11, 0x6d, 0x5a, 0xe7, 0x4f, 0x0b, 0x50, 0xde, + 0xd3, 0xfd, 0x23, 0x96, 0xf6, 0x7c, 0x97, 0x18, 0x12, 0x05, 0xf3, 0x67, 0x16, 0x6a, 0x2e, 0xeb, + 0x80, 0x9c, 0x84, 0xa1, 0x26, 0x9b, 0x6c, 0xc1, 0xd9, 0x10, 0x81, 0xa7, 0x3b, 0xbe, 0x1e, 0xe5, + 0x40, 0xb6, 0x87, 0x6c, 0x84, 0xbd, 0x04, 0x39, 0x07, 0x86, 0x95, 0x87, 0x61, 0x18, 0xbb, 0x5b, + 0x87, 0x60, 0xc6, 0x63, 0xce, 0x2a, 0xc2, 0xad, 0x19, 0xd1, 0xb6, 0xcc, 0xce, 0xcf, 0xaa, 0x50, + 0xc3, 0xc4, 0xa0, 0xc7, 0xfc, 0x7c, 0x6b, 0xea, 0xc6, 0x91, 0x66, 0x39, 0x01, 0x71, 0x82, 0x30, + 0xeb, 0xcf, 0xc5, 0x07, 0xae, 0x10, 0xeb, 0xae, 0x1a, 0x47, 0x5b, 0x42, 0x44, 0xdc, 0x7d, 0x41, + 0x8f, 0x08, 0x68, 0x05, 0xa6, 0xc5, 0xfd, 0x2f, 0x20, 0x26, 0x43, 0x27, 0x3e, 0x91, 0x18, 0xa5, + 0xc8, 0x31, 0xca, 0xe5, 0x88, 0xb9, 0xce, 0x78, 0x02, 0xae, 0x3c, 0x00, 0x14, 0xeb, 0xf0, 0x2c, + 0x61, 0x91, 0x70, 0x53, 0x27, 0xbb, 0x61, 0x61, 0xf8, 0xa1, 0x64, 0xe0, 0xc9, 0x48, 0x38, 0x24, + 0xa1, 0x45, 0x98, 0x32, 0xc2, 0xb0, 0xd7, 0xd8, 0xd9, 0x49, 0x12, 0xc7, 0x00, 0x1e, 0x8f, 0x78, + 0xec, 0x74, 0x25, 0x68, 0x11, 0xd0, 0x21, 0x9b, 0x63, 0xda, 0xc0, 0x8a, 0xa8, 0x4f, 0x08, 0x4e, + 0xc2, 0xba, 0x7b, 0x30, 0x21, 0xa5, 0x23, 0xd3, 0xaa, 0xa3, 0x4c, 0x1b, 0x17, 0x92, 0x91, 0x5d, + 0x37, 0xa1, 0x65, 0xeb, 0x7e, 0xa0, 0xe9, 0xae, 0x6b, 0x5b, 0xc4, 0xe4, 0xb5, 0xc9, 0x16, 0x6e, + 0x32, 0xda, 0xaa, 0x20, 0xa1, 0x55, 0x98, 0xb4, 0x49, 0x4f, 0x37, 0x4e, 0x93, 0xc8, 0xb0, 0x7e, + 0x06, 0x32, 0x6c, 0x0b, 0xf1, 0xc4, 0xb5, 0xe8, 0x2e, 0x30, 0xe8, 0xa7, 0x1d, 0x91, 0xd3, 0xb0, + 0xd4, 0xf3, 0xde, 0xd0, 0x9e, 0x3d, 0xd1, 0x5f, 0xfd, 0x98, 0x9c, 0xca, 0x0d, 0xab, 0xf5, 0x45, + 0x0b, 0xdd, 0x82, 0xcb, 0x81, 0x67, 0xf5, 0x7a, 0xec, 0xe8, 0xd3, 0x3d, 0xbd, 0xef, 0x8b, 0x65, + 0x03, 0x6e, 0xe6, 0x98, 0x64, 0xed, 0x70, 0x0e, 0xda, 0x81, 0x36, 0x73, 0xbe, 0x63, 0xa2, 0xed, + 0xeb, 0xc6, 0xd1, 0x81, 0x65, 0xdb, 0xbe, 0xd2, 0xe4, 0xa3, 0x7d, 0x98, 0xe3, 0x21, 0x4c, 0x70, + 0x2d, 0x94, 0x93, 0x25, 0x12, 0x3d, 0x4d, 0x55, 0xef, 0xc3, 0x44, 0xc6, 0x95, 0x92, 0xe5, 0x8f, + 0x46, 0x4e, 0xf9, 0xa3, 0x95, 0x28, 0x7f, 0xa8, 0xf7, 0xa0, 0x95, 0x9c, 0xd5, 0xdb, 0x4a, 0x27, + 0x29, 0xdd, 0x35, 0x98, 0xca, 0xb3, 0xf1, 0x6d, 0x7d, 0x54, 0x93, 0xe5, 0x97, 0xbf, 0xa9, 0x43, + 0x6d, 0x87, 0x78, 0xbe, 0xe5, 0x07, 0x68, 0x1a, 0xaa, 0x3e, 0xf9, 0x5a, 0x73, 0x28, 0x57, 0x2d, + 0xe3, 0x8a, 0x4f, 0xbe, 0x7e, 0x4a, 0x99, 0xa7, 0x89, 0x63, 0x54, 0x4b, 0xc6, 0x95, 0x88, 0xfa, + 0xb6, 0xe0, 0xc4, 0x2b, 0x90, 0x0d, 0xbf, 0x52, 0x26, 0xfc, 0xe4, 0x58, 0x17, 0x0b, 0xbf, 0xf2, + 0xe8, 0xf0, 0xbb, 0x07, 0x57, 0xa5, 0x91, 0x39, 0x51, 0x58, 0xe1, 0xb6, 0x5e, 0x11, 0x02, 0xeb, + 0x43, 0x81, 0x97, 0x1f, 0xba, 0xd5, 0x77, 0x08, 0xdd, 0x65, 0x98, 0x89, 0x43, 0xd7, 0xd5, 0x03, + 0xe3, 0x90, 0x48, 0x2f, 0x14, 0xc1, 0xd2, 0x8e, 0xb8, 0x3b, 0x82, 0x39, 0x22, 0x7c, 0xeb, 0x23, + 0xc2, 0xf7, 0x0e, 0xcc, 0xc8, 0xd9, 0x65, 0xa3, 0xb8, 0xc1, 0xa7, 0x36, 0x25, 0xb8, 0x8f, 0xd2, + 0x81, 0x9b, 0x13, 0xf4, 0x70, 0xd1, 0xa0, 0x6f, 0x0e, 0x07, 0xfd, 0x5d, 0x50, 0xa4, 0x51, 0xc3, + 0xb1, 0xdf, 0xe2, 0x66, 0x49, 0xa3, 0xb7, 0xb3, 0xb1, 0x9e, 0x9b, 0x2e, 0xc6, 0x2e, 0x9c, 0x2e, + 0xc6, 0x33, 0xe9, 0x22, 0xf4, 0xb1, 0xfc, 0x74, 0xb1, 0x02, 0xd3, 0xd2, 0xec, 0x74, 0xd6, 0x50, + 0x26, 0xb8, 0xcd, 0x97, 0x05, 0x73, 0x2f, 0x95, 0x36, 0x46, 0xa4, 0x98, 0x76, 0x5e, 0x8a, 0xb9, + 0x03, 0x95, 0x7d, 0xd2, 0xb3, 0x1c, 0x65, 0x32, 0x73, 0xbb, 0x49, 0xc7, 0xea, 0x1a, 0x93, 0x79, + 0x74, 0x09, 0x0b, 0x61, 0xb4, 0x00, 0x6d, 0x83, 0xf6, 0x5d, 0x6e, 0x57, 0x88, 0x6e, 0x11, 0x0b, + 0xe0, 0x47, 0x97, 0xf0, 0x44, 0xc8, 0x91, 0xf7, 0x90, 0xff, 0xc3, 0x9c, 0xb3, 0xa6, 0xc0, 0x4c, + 0x26, 0x81, 0x6a, 0xc6, 0xa1, 0xee, 0xf4, 0x48, 0x07, 0xc3, 0xe5, 0x9c, 0x19, 0x9e, 0x81, 0xd6, + 0x6f, 0x42, 0x2b, 0xf0, 0x06, 0x8e, 0xa1, 0x33, 0x0f, 0xd5, 0x03, 0x99, 0x9b, 0x9a, 0x11, 0x6d, + 0x35, 0xe8, 0x74, 0xa0, 0x21, 0x37, 0x93, 0x98, 0x23, 0xd2, 0x53, 0xe7, 0x4f, 0x0a, 0x50, 0x61, + 0x2e, 0x79, 0x3a, 0x0a, 0xa7, 0x1c, 0xb3, 0x1e, 0xe4, 0x75, 0xa4, 0x81, 0xc3, 0x26, 0xba, 0x06, + 0x0d, 0xee, 0xe1, 0x5c, 0x45, 0x9c, 0xb1, 0x75, 0x46, 0x60, 0x78, 0x2b, 0x72, 0xff, 0x50, 0x57, + 0x20, 0x46, 0xee, 0xfe, 0x2f, 0xa4, 0xfe, 0xf2, 0x88, 0xe3, 0x5a, 0x60, 0x7d, 0x94, 0x3e, 0xae, + 0xd9, 0x5d, 0xa2, 0xf3, 0x12, 0x6a, 0x61, 0xec, 0xdc, 0x06, 0x24, 0x40, 0x50, 0x54, 0x1b, 0x08, + 0x81, 0x58, 0x03, 0x4f, 0x0a, 0xce, 0x46, 0xcc, 0x38, 0x23, 0xbf, 0x14, 0xf3, 0xf3, 0x4b, 0xe7, + 0x97, 0x05, 0x79, 0x03, 0x7e, 0xb7, 0x45, 0xf9, 0x30, 0x7c, 0x67, 0x59, 0xca, 0x7d, 0x67, 0x19, + 0xbe, 0xad, 0x7c, 0xff, 0x4c, 0xa8, 0xc2, 0x2f, 0xfe, 0x04, 0x7d, 0x96, 0x08, 0xd1, 0x0a, 0x0f, + 0xd1, 0xb8, 0xec, 0xc1, 0x2f, 0xdb, 0xb9, 0xf1, 0xf9, 0x6d, 0xbc, 0xb3, 0x03, 0x50, 0xe7, 0x59, + 0xf3, 0x29, 0x3d, 0xe9, 0x54, 0xa1, 0xbc, 0x1b, 0x50, 0xb7, 0xd3, 0x80, 0x1a, 0xfb, 0x75, 0x89, + 0xd9, 0xf9, 0x0d, 0x68, 0xee, 0x12, 0x9f, 0x4d, 0x74, 0x9b, 0x52, 0x77, 0x44, 0x85, 0xa6, 0x70, + 0x91, 0x0a, 0xcd, 0x1f, 0x56, 0xa1, 0x26, 0xeb, 0xb2, 0xe8, 0xe3, 0xc4, 0x8a, 0x37, 0x57, 0xa6, + 0xbb, 0xe1, 0x07, 0x0c, 0x61, 0xa1, 0x81, 0x2f, 0xa4, 0xd8, 0x88, 0xff, 0x0f, 0x63, 0xec, 0x57, + 0xf3, 0xe4, 0x05, 0x4f, 0xde, 0x19, 0x66, 0x12, 0x3a, 0x82, 0x21, 0x94, 0x5a, 0x4c, 0x38, 0xba, + 0x0c, 0x7e, 0x06, 0x75, 0xd3, 0xf2, 0x39, 0x56, 0x91, 0xdb, 0x75, 0x75, 0x68, 0xac, 0x0d, 0x29, + 0x80, 0x23, 0x51, 0xf4, 0x05, 0x40, 0xf8, 0x1c, 0x55, 0x04, 0xaf, 0x0f, 0x0f, 0xb8, 0x11, 0xc9, + 0xe0, 0x84, 0x3c, 0x1b, 0xf4, 0x58, 0xb7, 0x2d, 0x53, 0x0f, 0x88, 0xac, 0x30, 0x0c, 0x0f, 0xfa, + 0x42, 0x0a, 0xe0, 0x48, 0x14, 0x7d, 0x0e, 0x8d, 0xf0, 0xd9, 0x94, 0x27, 0xeb, 0xb5, 0xe1, 0x31, + 0x43, 0x45, 0x13, 0xc7, 0xd2, 0xe9, 0xa2, 0x5b, 0xe3, 0x2d, 0x45, 0xb7, 0x1f, 0x42, 0xcb, 0x17, + 0x3b, 0xac, 0xd9, 0x94, 0xba, 0xca, 0x94, 0x3c, 0x54, 0xc2, 0xcd, 0x4c, 0x6c, 0x3f, 0x6e, 0xfa, + 0x09, 0x5f, 0xb8, 0x09, 0xe5, 0x97, 0xd4, 0x72, 0x94, 0x69, 0xae, 0x30, 0x96, 0xba, 0x9f, 0x62, + 0xce, 0x42, 0x1f, 0x41, 0xf5, 0x25, 0xbf, 0x45, 0x29, 0x33, 0x32, 0x38, 0x92, 0x42, 0xc4, 0xc4, + 0x92, 0xcd, 0xfa, 0x0a, 0x74, 0xff, 0x48, 0xb9, 0x92, 0xe9, 0x8b, 0x5d, 0xa6, 0x30, 0x67, 0xa1, + 0xa5, 0xa8, 0x24, 0xac, 0x70, 0xa1, 0x2b, 0xd9, 0xea, 0x7e, 0xb6, 0x10, 0xdc, 0x85, 0x86, 0x00, + 0x0a, 0x0e, 0x3d, 0x51, 0xae, 0xca, 0x53, 0x3c, 0xd2, 0x91, 0x3e, 0x8f, 0xeb, 0x86, 0x7c, 0x62, + 0x36, 0xf8, 0x01, 0x75, 0x15, 0x35, 0x63, 0x03, 0x0b, 0x05, 0xcc, 0x59, 0xe8, 0x16, 0xd4, 0x7c, + 0x11, 0x18, 0xca, 0x35, 0xf9, 0x3a, 0x3c, 0x29, 0xe5, 0x12, 0x13, 0x87, 0x02, 0xea, 0xbd, 0xa8, + 0x0a, 0xfc, 0xce, 0x05, 0xc7, 0xce, 0x3f, 0xcf, 0x40, 0x33, 0x51, 0x59, 0x44, 0xb7, 0x53, 0xf1, + 0x71, 0xb5, 0x9b, 0xfc, 0xf0, 0x26, 0x27, 0x46, 0xbe, 0xca, 0x8f, 0x11, 0x35, 0xa3, 0x37, 0x3a, + 0x4e, 0x3e, 0x4f, 0xb8, 0xac, 0x88, 0x93, 0xf7, 0x72, 0xc7, 0xcc, 0x71, 0xdb, 0xfb, 0x49, 0xb7, + 0x15, 0xa1, 0x32, 0x9b, 0x3f, 0xee, 0xdb, 0x5d, 0xb7, 0xf2, 0x2b, 0xe2, 0xba, 0xb7, 0xa0, 0xe6, + 0x89, 0x1b, 0x92, 0xf4, 0xdd, 0x76, 0xf6, 0xe6, 0x84, 0x43, 0x01, 0xf4, 0x01, 0x54, 0x18, 0x80, + 0x3c, 0x95, 0x1e, 0x1b, 0x7f, 0x50, 0xc4, 0x0f, 0x6c, 0x2c, 0x98, 0xac, 0xc7, 0x10, 0x66, 0xaa, + 0x99, 0x1e, 0xe5, 0x79, 0x89, 0x43, 0x01, 0x66, 0x20, 0x2f, 0x1d, 0x5f, 0xcb, 0x18, 0x98, 0xa8, + 0x15, 0x7f, 0x1a, 0xc5, 0xd6, 0xf5, 0x4c, 0xb9, 0x38, 0xe1, 0x85, 0xd9, 0xf8, 0xba, 0x0d, 0x65, + 0x9b, 0xea, 0xa6, 0x32, 0x2f, 0x9d, 0x32, 0x4f, 0x65, 0x9b, 0xea, 0x26, 0xe6, 0x62, 0x6c, 0x0c, + 0xf6, 0x4b, 0x4c, 0xe5, 0xe3, 0x33, 0xc6, 0xd8, 0xe6, 0x22, 0x58, 0x8a, 0xa2, 0x65, 0xa8, 0xf0, + 0x0a, 0xba, 0x72, 0x2b, 0x73, 0xc4, 0x24, 0x75, 0x78, 0x61, 0x1d, 0x0b, 0x41, 0xf4, 0x83, 0xb8, + 0x56, 0xbf, 0x90, 0x41, 0x93, 0x43, 0x3a, 0x89, 0x02, 0x3d, 0x1b, 0xc9, 0x0f, 0xa8, 0x47, 0x94, + 0xc5, 0x33, 0x46, 0xda, 0x65, 0x12, 0x58, 0x08, 0xb2, 0x09, 0xf1, 0x07, 0x53, 0xb9, 0x7d, 0xc6, + 0x84, 0xb8, 0x8a, 0x89, 0xa5, 0x28, 0x5a, 0xcf, 0xbc, 0x2d, 0xef, 0x72, 0xd5, 0xb9, 0x11, 0xaa, + 0xf9, 0xef, 0xc9, 0xd1, 0x16, 0x8c, 0xf3, 0x26, 0xbb, 0x0a, 0x89, 0x6e, 0x96, 0x32, 0x6f, 0xa9, + 0x86, 0xba, 0x21, 0xa6, 0xec, 0x68, 0xcc, 0x4f, 0x36, 0xd1, 0x1a, 0xbf, 0x7b, 0x3a, 0xf4, 0xc4, + 0x26, 0x66, 0x8f, 0x28, 0xcb, 0x67, 0x98, 0xb3, 0x1a, 0xcb, 0xe1, 0xa4, 0x12, 0xda, 0x84, 0x56, + 0xa2, 0x69, 0x2a, 0x9f, 0x64, 0x5e, 0xd9, 0x8d, 0xe8, 0xc4, 0xc4, 0x29, 0x35, 0xe6, 0xd3, 0xae, + 0x40, 0xae, 0xca, 0x4a, 0xc6, 0xa7, 0x25, 0xa2, 0xc5, 0xa1, 0x00, 0x4b, 0xa9, 0x6e, 0x88, 0x72, + 0x95, 0x4f, 0x33, 0x29, 0x35, 0xc2, 0xbf, 0x38, 0x16, 0x4a, 0x9f, 0x06, 0x77, 0xce, 0x7f, 0x1a, + 0x7c, 0x71, 0xae, 0xd3, 0xe0, 0xfe, 0xdb, 0x4e, 0x83, 0xdf, 0x29, 0x5c, 0xfc, 0x38, 0x40, 0x3f, + 0x4a, 0x62, 0xc7, 0xc4, 0xfd, 0xaf, 0x78, 0xc6, 0xfd, 0xef, 0x72, 0xa4, 0x91, 0x78, 0x95, 0xf8, + 0x19, 0x94, 0x59, 0x80, 0xa1, 0xdb, 0x50, 0x8f, 0xee, 0xb7, 0x85, 0x51, 0xf7, 0xdb, 0x48, 0x44, + 0xfd, 0x65, 0x11, 0xaa, 0x22, 0x30, 0xd1, 0x57, 0x43, 0x6f, 0x87, 0xde, 0x3f, 0x23, 0x8e, 0x87, + 0x5f, 0x0e, 0x89, 0x3b, 0x00, 0x7f, 0x3b, 0xe1, 0x69, 0xe2, 0x43, 0x99, 0xfd, 0xd3, 0x80, 0x88, + 0xe2, 0x48, 0x99, 0xdd, 0x01, 0x04, 0xef, 0x39, 0x63, 0xad, 0x31, 0x8e, 0xfa, 0x1f, 0x85, 0xf8, + 0x75, 0xd2, 0x14, 0x54, 0x44, 0x89, 0x5b, 0x60, 0x5b, 0xd1, 0x40, 0xf3, 0xd0, 0xee, 0x5b, 0x8e, + 0xe6, 0xd3, 0x81, 0x67, 0xa4, 0xeb, 0x8e, 0xe3, 0x7d, 0xcb, 0xd9, 0xe5, 0x64, 0x51, 0x15, 0x98, + 0x17, 0x95, 0xd6, 0x94, 0x64, 0x49, 0x4a, 0xea, 0xaf, 0x92, 0x92, 0x8b, 0x80, 0x84, 0x94, 0xa9, + 0x99, 0xd4, 0xf0, 0xb5, 0x80, 0x06, 0xba, 0xcd, 0x0f, 0xb4, 0x32, 0x6e, 0x4b, 0xce, 0x06, 0x35, + 0xfc, 0x3d, 0x46, 0x47, 0x5d, 0xb8, 0x1c, 0x4a, 0xf3, 0xe9, 0x48, 0xf1, 0x0a, 0x17, 0x9f, 0x94, + 0x2c, 0x3e, 0x1d, 0x21, 0xdf, 0x81, 0x31, 0x09, 0xf4, 0x35, 0x93, 0xd8, 0x81, 0xfc, 0xd6, 0x0c, + 0x37, 0x05, 0xa2, 0xdf, 0x60, 0x24, 0xf5, 0x73, 0xa8, 0xf0, 0x2c, 0x75, 0xc6, 0x55, 0xa6, 0x90, + 0x7f, 0x95, 0x51, 0xff, 0xb3, 0x10, 0xbf, 0x6e, 0x3c, 0xeb, 0x7d, 0x5e, 0x4e, 0x46, 0xcc, 0xdd, + 0xb2, 0x77, 0xbc, 0x4a, 0xa9, 0xa7, 0x6f, 0xdb, 0xb1, 0x5b, 0x30, 0x29, 0x32, 0x7c, 0x72, 0x71, + 0x85, 0x0b, 0x4c, 0x08, 0x46, 0xbc, 0xb6, 0x8b, 0x80, 0xa4, 0x6c, 0x72, 0x69, 0x4b, 0x62, 0x27, + 0x04, 0x27, 0x5e, 0x59, 0xb5, 0x06, 0x15, 0x9e, 0x72, 0xd5, 0xbf, 0x2e, 0x40, 0x55, 0x24, 0xdf, + 0x73, 0x3b, 0xad, 0x10, 0xcf, 0x79, 0xa3, 0x79, 0x9e, 0xf9, 0x88, 0x04, 0x9f, 0x33, 0x1f, 0xc1, + 0x48, 0xcd, 0x47, 0xca, 0xe6, 0xcc, 0x47, 0x70, 0x12, 0xf3, 0xf9, 0xdd, 0x42, 0xfa, 0xa3, 0xa8, + 0x77, 0x76, 0x86, 0xef, 0x2e, 0x7b, 0xac, 0xc2, 0x58, 0xea, 0x2c, 0xb9, 0x80, 0x63, 0x7e, 0x05, + 0xcd, 0xc4, 0x09, 0x70, 0x81, 0x0e, 0x1e, 0x40, 0x2b, 0x79, 0x84, 0xbc, 0x7b, 0x0f, 0x9d, 0x9f, + 0x21, 0xa8, 0x8a, 0x8f, 0x38, 0xd0, 0x7c, 0x0a, 0x56, 0x4f, 0x75, 0xe5, 0x47, 0xf1, 0x39, 0x88, + 0xfa, 0x5e, 0x3e, 0xa2, 0x9e, 0x8e, 0x55, 0x46, 0x83, 0xe9, 0x3b, 0x43, 0x60, 0x5a, 0xc9, 0x8e, + 0x94, 0x83, 0xa3, 0xef, 0x0e, 0xe3, 0x68, 0x75, 0x68, 0xb4, 0x5f, 0x43, 0xe8, 0x3c, 0x08, 0x7d, + 0x0e, 0xc0, 0xdb, 0xcd, 0x00, 0xde, 0x99, 0xcc, 0xf7, 0x3d, 0x59, 0xac, 0x3b, 0x9f, 0xc2, 0xba, + 0x53, 0x59, 0xe9, 0x04, 0xcc, 0xed, 0x66, 0x60, 0xee, 0x4c, 0x9e, 0x6c, 0x02, 0xe1, 0x2e, 0xa4, + 0x11, 0xee, 0x74, 0x56, 0x3c, 0x05, 0x6e, 0x3f, 0xc9, 0x82, 0xdb, 0x2b, 0xb9, 0xe2, 0x49, 0x5c, + 0xbb, 0x90, 0xc6, 0xb5, 0x43, 0xfd, 0xa7, 0x20, 0x6d, 0x37, 0x03, 0x69, 0x67, 0x72, 0xa5, 0x63, + 0x34, 0xfb, 0x65, 0x2e, 0x9a, 0xbd, 0x36, 0xac, 0x35, 0x02, 0xc8, 0x6e, 0x8c, 0x00, 0xb2, 0xef, + 0xe5, 0xf6, 0x30, 0x0a, 0xc3, 0xfe, 0x1a, 0x38, 0x7e, 0x5f, 0x81, 0xe3, 0xdf, 0xc6, 0xc0, 0xf1, + 0xde, 0xd0, 0x19, 0x7c, 0x23, 0x3f, 0x32, 0xbe, 0x13, 0xcc, 0xf8, 0xf7, 0xbf, 0x7a, 0x98, 0xf1, + 0xdb, 0xe0, 0xc1, 0x67, 0x31, 0x1c, 0x7c, 0x77, 0xfc, 0x80, 0xa0, 0xdc, 0x67, 0x09, 0x44, 0xbc, + 0xbe, 0xe4, 0xcf, 0x31, 0xca, 0xfa, 0xed, 0x52, 0x84, 0xb2, 0x96, 0x61, 0x2a, 0xfa, 0x82, 0x32, + 0x39, 0x7f, 0xf1, 0xea, 0x01, 0x45, 0xbc, 0x78, 0x05, 0x56, 0x60, 0x3a, 0xd6, 0x48, 0xae, 0x81, + 0xd8, 0xd8, 0xcb, 0x11, 0x33, 0x81, 0x9c, 0x17, 0x01, 0x99, 0x1e, 0xf3, 0xee, 0xd4, 0x18, 0x12, + 0x3d, 0x49, 0x4e, 0x6a, 0x8d, 0x43, 0xe9, 0x64, 0xff, 0x62, 0x4b, 0x26, 0x25, 0x2b, 0xd1, 0xfb, + 0x4f, 0xa0, 0x1d, 0x7f, 0x38, 0x21, 0x53, 0x52, 0x25, 0xf3, 0xb1, 0x75, 0x2a, 0x15, 0x46, 0xdf, + 0x8f, 0x7a, 0x32, 0x37, 0x4d, 0xb8, 0x69, 0x82, 0xaa, 0xc1, 0x44, 0x46, 0x06, 0xa9, 0xfc, 0x3b, + 0x22, 0x73, 0x60, 0xc8, 0x28, 0x6a, 0xe1, 0xa8, 0xcd, 0xbc, 0x35, 0xe9, 0x8c, 0xa2, 0xc1, 0x34, + 0xe4, 0x5f, 0x7b, 0x89, 0xf7, 0xc3, 0x0d, 0x1c, 0xb5, 0xd5, 0x17, 0x69, 0x80, 0x38, 0x2a, 0xe6, + 0x0b, 0xef, 0x1a, 0xf3, 0x13, 0x19, 0xb8, 0x77, 0x6b, 0x01, 0x2a, 0xfc, 0xaf, 0xda, 0x10, 0x40, + 0x75, 0xe7, 0xf9, 0xda, 0xf6, 0xd6, 0x7a, 0xfb, 0x12, 0x6a, 0x42, 0x6d, 0x07, 0x6f, 0xbd, 0x58, + 0xdd, 0xdb, 0x6c, 0x17, 0x50, 0x03, 0x2a, 0xdb, 0xcf, 0xd6, 0x57, 0xb7, 0xdb, 0xc5, 0x95, 0xc7, + 0x50, 0x97, 0x7f, 0x75, 0xe4, 0xa1, 0x2f, 0xa1, 0x26, 0x9f, 0x51, 0x7c, 0x60, 0xa5, 0xff, 0x1e, + 0x4e, 0x55, 0x86, 0x19, 0x02, 0xe4, 0x2c, 0x17, 0x56, 0xb6, 0xa1, 0x2e, 0xbf, 0x68, 0xf3, 0xd0, + 0x03, 0xa8, 0xc9, 0xe7, 0x44, 0x5f, 0xe9, 0xef, 0x12, 0x13, 0x7d, 0x65, 0x3e, 0x84, 0x9b, 0x2f, + 0x2c, 0x17, 0x56, 0x0e, 0x61, 0x3c, 0xfd, 0xad, 0x18, 0x7a, 0x01, 0x13, 0xfc, 0x21, 0x22, 0xfb, + 0xe8, 0x46, 0x32, 0xb1, 0x0e, 0x7f, 0x71, 0xa6, 0xce, 0x8e, 0xe4, 0x27, 0x46, 0x3a, 0x81, 0xea, + 0xb6, 0xf8, 0xe3, 0xa8, 0x6e, 0x84, 0x39, 0x27, 0x32, 0x7e, 0xa4, 0x66, 0x09, 0x4c, 0x13, 0xdd, + 0x4f, 0xd7, 0x7f, 0xa7, 0xf2, 0xae, 0x2b, 0x6a, 0x2e, 0x95, 0x0f, 0xfc, 0x67, 0xd1, 0xd7, 0x56, + 0x9f, 0xc4, 0x2f, 0x59, 0xda, 0xd9, 0x82, 0xb9, 0x3a, 0x44, 0xe1, 0x63, 0xff, 0xef, 0xda, 0xba, + 0xf6, 0xe5, 0x37, 0xff, 0x72, 0xe3, 0xd2, 0x37, 0xbf, 0xb8, 0x51, 0xf8, 0xbb, 0x5f, 0xdc, 0x28, + 0xfc, 0xf1, 0xbf, 0xde, 0x28, 0xfc, 0xd6, 0xe2, 0xb9, 0xfe, 0xda, 0x4a, 0xf6, 0xb7, 0x5f, 0xe5, + 0xa4, 0x4f, 0xff, 0x27, 0x00, 0x00, 0xff, 0xff, 0x80, 0x96, 0x44, 0xb8, 0x7a, 0x3b, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -6468,6 +6578,22 @@ func (m *Recover) MarshalToSizedBuffer(dAtA []byte) (int, error) { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } + if len(m.ActiveBackfills) > 0 { + for k := range m.ActiveBackfills { + v := m.ActiveBackfills[k] + baseI := i + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(v)) + i-- + dAtA[i] = 0x11 + i = encodeVarintRuntime(dAtA, i, uint64(k)) + i-- + dAtA[i] = 0x8 + i = encodeVarintRuntime(dAtA, i, uint64(baseI-i)) + i-- + dAtA[i] = 0x5a + } + } if len(m.TriggerParamsJson) > 0 { i -= len(m.TriggerParamsJson) copy(dAtA[i:], m.TriggerParamsJson) @@ -6604,6 +6730,15 @@ func (m *Persist) MarshalToSizedBuffer(dAtA []byte) (int, error) { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } + if m.ActiveBackfillChange != nil { + { + size := m.ActiveBackfillChange.ProtoSize() + i -= size + if _, err := m.ActiveBackfillChange.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + } + } if len(m.TriggerParamsJson) > 0 { i -= len(m.TriggerParamsJson) copy(dAtA[i:], m.TriggerParamsJson) @@ -6773,6 +6908,81 @@ func (m *Persist) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *Persist_Begin) MarshalTo(dAtA []byte) (int, error) { + size := m.ProtoSize() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Persist_Begin) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + if m.Begin != nil { + { + size, err := m.Begin.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintRuntime(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x8a + } + return len(dAtA) - i, nil +} +func (m *Persist_CompleteBinding) MarshalTo(dAtA []byte) (int, error) { + size := m.ProtoSize() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Persist_CompleteBinding) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + i = encodeVarintRuntime(dAtA, i, uint64(m.CompleteBinding)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x90 + return len(dAtA) - i, nil +} +func (m *ActiveBackfillBegin) Marshal() (dAtA []byte, err error) { + size := m.ProtoSize() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ActiveBackfillBegin) MarshalTo(dAtA []byte) (int, error) { + size := m.ProtoSize() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ActiveBackfillBegin) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.TruncatedAt != 0 { + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.TruncatedAt)) + i-- + dAtA[i] = 0x11 + } + if m.Binding != 0 { + i = encodeVarintRuntime(dAtA, i, uint64(m.Binding)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + func (m *Persisted) Marshal() (dAtA []byte, err error) { size := m.ProtoSize() dAtA = make([]byte, size) @@ -10028,6 +10238,14 @@ func (m *Recover) ProtoSize() (n int) { if l > 0 { n += 1 + l + sovRuntime(uint64(l)) } + if len(m.ActiveBackfills) > 0 { + for k, v := range m.ActiveBackfills { + _ = k + _ = v + mapEntrySize := 1 + sovRuntime(uint64(k)) + 1 + 8 + n += mapEntrySize + 1 + sovRuntime(uint64(mapEntrySize)) + } + } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } @@ -10112,6 +10330,48 @@ func (m *Persist) ProtoSize() (n int) { if l > 0 { n += 2 + l + sovRuntime(uint64(l)) } + if m.ActiveBackfillChange != nil { + n += m.ActiveBackfillChange.ProtoSize() + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *Persist_Begin) ProtoSize() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Begin != nil { + l = m.Begin.ProtoSize() + n += 2 + l + sovRuntime(uint64(l)) + } + return n +} +func (m *Persist_CompleteBinding) ProtoSize() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + n += 2 + sovRuntime(uint64(m.CompleteBinding)) + return n +} +func (m *ActiveBackfillBegin) ProtoSize() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Binding != 0 { + n += 1 + sovRuntime(uint64(m.Binding)) + } + if m.TruncatedAt != 0 { + n += 9 + } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } @@ -16304,6 +16564,96 @@ func (m *Recover) Unmarshal(dAtA []byte) error { m.TriggerParamsJson = []byte{} } iNdEx = postIndex + case 11: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ActiveBackfills", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRuntime + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthRuntime + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthRuntime + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.ActiveBackfills == nil { + m.ActiveBackfills = make(map[uint32]uint64) + } + var mapkey uint32 + var mapvalue uint64 + for iNdEx < postIndex { + entryPreIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRuntime + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + if fieldNum == 1 { + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRuntime + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + mapkey |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + } else if fieldNum == 2 { + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + mapvalue = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + } else { + iNdEx = entryPreIndex + skippy, err := skipRuntime(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthRuntime + } + if (iNdEx + skippy) > postIndex { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + m.ActiveBackfills[mapkey] = mapvalue + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipRuntime(dAtA[iNdEx:]) @@ -16946,6 +17296,141 @@ func (m *Persist) Unmarshal(dAtA []byte) error { m.TriggerParamsJson = []byte{} } iNdEx = postIndex + case 17: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Begin", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRuntime + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthRuntime + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthRuntime + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + v := &ActiveBackfillBegin{} + if err := v.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + m.ActiveBackfillChange = &Persist_Begin{v} + iNdEx = postIndex + case 18: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CompleteBinding", wireType) + } + var v uint32 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRuntime + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.ActiveBackfillChange = &Persist_CompleteBinding{v} + default: + iNdEx = preIndex + skippy, err := skipRuntime(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthRuntime + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *ActiveBackfillBegin) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRuntime + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ActiveBackfillBegin: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ActiveBackfillBegin: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Binding", wireType) + } + m.Binding = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRuntime + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Binding |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field TruncatedAt", wireType) + } + m.TruncatedAt = 0 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + m.TruncatedAt = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 default: iNdEx = preIndex skippy, err := skipRuntime(dAtA[iNdEx:]) diff --git a/go/protocols/runtime/runtime.proto b/go/protocols/runtime/runtime.proto index 7758224ff75..fbaf7fe2245 100644 --- a/go/protocols/runtime/runtime.proto +++ b/go/protocols/runtime/runtime.proto @@ -545,6 +545,11 @@ message Recover { map max_keys = 9; // Persisted trigger parameters (materialize only), or empty. bytes trigger_params_json = 10 [json_name = "triggerParams"]; + // Active-backfill begin clocks, keyed by binding index. Restored so the + // capture runtime can re-apply truncated-at journal labels on startup and + // resolve a BackfillComplete's truncated_at. Resolved from "AB:{state_key}" + // keys by the scan. + map active_backfills = 11; } // Persist is sent by the leader to shard zero when state must be durably @@ -607,6 +612,23 @@ message Persist { // Materialization trigger parameters. // Effect: Put under "trigger-params" key. bytes trigger_params_json = 16 [ json_name = "triggerParams" ]; + // The active-backfill change this transaction observed, if any. At most one + // per commit — a backfill control signal stands alone in its transaction. + oneof active_backfill_change { + // BackfillBegin: record the binding's begin clock. + // Effect: Put fixed64-LE under "AB:{state_key}" (state_key resolved by the encoder). + ActiveBackfillBegin begin = 17; + // BackfillComplete: clear the binding's active-backfill entry. + // Effect: Delete "AB:{state_key}" (state_key resolved by the encoder). + uint32 complete_binding = 18; + } +} + +// ActiveBackfillBegin records a binding's backfill begin clock — its +// authoritative truncated_at — staged by a committing Persist. +message ActiveBackfillBegin { + uint32 binding = 1; + fixed64 truncated_at = 2; } // Persisted is sent by shard zero to the leader after the state is durable diff --git a/go/protocols/shuffle/shuffle.pb.go b/go/protocols/shuffle/shuffle.pb.go index b0c7cbac6f0..42a21faf9be 100644 --- a/go/protocols/shuffle/shuffle.pb.go +++ b/go/protocols/shuffle/shuffle.pb.go @@ -360,10 +360,18 @@ var xxx_messageInfo_JournalFrontier proto.InternalMessageInfo type Frontier struct { Journals []*JournalFrontier `protobuf:"bytes,1,rep,name=journals,proto3" json:"journals,omitempty"` // Per-shard flushed LSN, indexed by shard_index. - FlushedLsn []uint64 `protobuf:"varint,2,rep,packed,name=flushed_lsn,json=flushedLsn,proto3" json:"flushed_lsn,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + FlushedLsn []uint64 `protobuf:"varint,2,rep,packed,name=flushed_lsn,json=flushedLsn,proto3" json:"flushed_lsn,omitempty"` + // Latest backfill-begin clock for each binding in the checkpoint delta. + // Populated only on a terminal (empty-journals) frontier of a Progressed + // or NextCheckpoint sequence. Empty otherwise. + LatestBackfillBegin []*Frontier_BackfillBegin `protobuf:"bytes,3,rep,name=latest_backfill_begin,json=latestBackfillBegin,proto3" json:"latest_backfill_begin,omitempty"` + // Latest backfill-complete clock for each binding in the checkpoint delta. + // Populated only on a terminal (empty-journals) frontier of a Progressed + // or NextCheckpoint sequence. Empty otherwise. + LatestBackfillComplete []*Frontier_BackfillComplete `protobuf:"bytes,4,rep,name=latest_backfill_complete,json=latestBackfillComplete,proto3" json:"latest_backfill_complete,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *Frontier) Reset() { *m = Frontier{} } @@ -399,6 +407,96 @@ func (m *Frontier) XXX_DiscardUnknown() { var xxx_messageInfo_Frontier proto.InternalMessageInfo +// BackfillBegin is a binding's latest backfill-begin clock, keyed by the +// binding's stable journal_read_suffix. +type Frontier_BackfillBegin struct { + // Binding's journal read suffix, stable across task versions. + JournalReadSuffix string `protobuf:"bytes,1,opt,name=journal_read_suffix,json=journalReadSuffix,proto3" json:"journal_read_suffix,omitempty"` + // Clock of the binding's most recent backfill-begin. + Clock uint64 `protobuf:"fixed64,2,opt,name=clock,proto3" json:"clock,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Frontier_BackfillBegin) Reset() { *m = Frontier_BackfillBegin{} } +func (m *Frontier_BackfillBegin) String() string { return proto.CompactTextString(m) } +func (*Frontier_BackfillBegin) ProtoMessage() {} +func (*Frontier_BackfillBegin) Descriptor() ([]byte, []int) { + return fileDescriptor_8851eb1ddb7aa19d, []int{5, 0} +} +func (m *Frontier_BackfillBegin) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Frontier_BackfillBegin) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Frontier_BackfillBegin.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Frontier_BackfillBegin) XXX_Merge(src proto.Message) { + xxx_messageInfo_Frontier_BackfillBegin.Merge(m, src) +} +func (m *Frontier_BackfillBegin) XXX_Size() int { + return m.ProtoSize() +} +func (m *Frontier_BackfillBegin) XXX_DiscardUnknown() { + xxx_messageInfo_Frontier_BackfillBegin.DiscardUnknown(m) +} + +var xxx_messageInfo_Frontier_BackfillBegin proto.InternalMessageInfo + +// BackfillComplete reports a binding's most recent backfill-complete event, +// keyed by the binding's stable journal_read_suffix. +type Frontier_BackfillComplete struct { + // Binding's journal read suffix, stable across task versions. + JournalReadSuffix string `protobuf:"bytes,1,opt,name=journal_read_suffix,json=journalReadSuffix,proto3" json:"journal_read_suffix,omitempty"` + // Truncation boundary of the completed backfill: the backfill's begin clock. + Clock uint64 `protobuf:"fixed64,2,opt,name=clock,proto3" json:"clock,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Frontier_BackfillComplete) Reset() { *m = Frontier_BackfillComplete{} } +func (m *Frontier_BackfillComplete) String() string { return proto.CompactTextString(m) } +func (*Frontier_BackfillComplete) ProtoMessage() {} +func (*Frontier_BackfillComplete) Descriptor() ([]byte, []int) { + return fileDescriptor_8851eb1ddb7aa19d, []int{5, 1} +} +func (m *Frontier_BackfillComplete) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Frontier_BackfillComplete) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Frontier_BackfillComplete.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Frontier_BackfillComplete) XXX_Merge(src proto.Message) { + xxx_messageInfo_Frontier_BackfillComplete.Merge(m, src) +} +func (m *Frontier_BackfillComplete) XXX_Size() int { + return m.ProtoSize() +} +func (m *Frontier_BackfillComplete) XXX_DiscardUnknown() { + xxx_messageInfo_Frontier_BackfillComplete.DiscardUnknown(m) +} + +var xxx_messageInfo_Frontier_BackfillComplete proto.InternalMessageInfo + // SessionRequest is sent by the Coordinator to manage the shuffle session. type SessionRequest struct { Open *SessionRequest_Open `protobuf:"bytes,1,opt,name=open,proto3" json:"open,omitempty"` @@ -1375,6 +1473,8 @@ func init() { proto.RegisterType((*ProducerFrontier)(nil), "shuffle.ProducerFrontier") proto.RegisterType((*JournalFrontier)(nil), "shuffle.JournalFrontier") proto.RegisterType((*Frontier)(nil), "shuffle.Frontier") + proto.RegisterType((*Frontier_BackfillBegin)(nil), "shuffle.Frontier.BackfillBegin") + proto.RegisterType((*Frontier_BackfillComplete)(nil), "shuffle.Frontier.BackfillComplete") proto.RegisterType((*SessionRequest)(nil), "shuffle.SessionRequest") proto.RegisterType((*SessionRequest_Open)(nil), "shuffle.SessionRequest.Open") proto.RegisterType((*SessionRequest_NextCheckpoint)(nil), "shuffle.SessionRequest.NextCheckpoint") @@ -1402,105 +1502,112 @@ func init() { } var fileDescriptor_8851eb1ddb7aa19d = []byte{ - // 1564 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x58, 0x3b, 0x73, 0x1b, 0x47, - 0x12, 0xd6, 0xe2, 0x49, 0x34, 0x48, 0x82, 0x1c, 0x91, 0xd2, 0xde, 0x8a, 0x2f, 0x41, 0x75, 0x3a, - 0xde, 0x95, 0x0a, 0x94, 0x28, 0x9d, 0x54, 0xa7, 0xaa, 0x53, 0x89, 0x94, 0x8e, 0x25, 0xdd, 0xf1, - 0x24, 0xdd, 0x80, 0xd1, 0x25, 0x5b, 0x8b, 0xdd, 0xc1, 0x62, 0x8c, 0xc5, 0x0e, 0xbc, 0x33, 0x90, - 0x09, 0xc7, 0x0e, 0x1c, 0x38, 0xb0, 0x13, 0x3b, 0xb5, 0x63, 0x47, 0xce, 0x9c, 0x38, 0x55, 0x29, - 0xf4, 0x5f, 0xb0, 0x14, 0xf9, 0x4f, 0xb8, 0x5c, 0xf3, 0xd8, 0xc5, 0x83, 0x4b, 0xd9, 0x81, 0x03, - 0x3b, 0x21, 0x67, 0xba, 0xbf, 0x9e, 0xee, 0xe9, 0xd7, 0xf4, 0x02, 0x9a, 0x21, 0xdb, 0x1b, 0x26, - 0x4c, 0x30, 0x9f, 0x45, 0x7c, 0x8f, 0xf7, 0x46, 0xdd, 0x6e, 0x44, 0xd2, 0xff, 0x2d, 0xc5, 0x41, - 0x55, 0xb3, 0x75, 0xb6, 0x3a, 0x09, 0xeb, 0x93, 0x24, 0x13, 0xc8, 0x16, 0x1a, 0xe8, 0x6c, 0xcc, - 0x1c, 0xd6, 0x8d, 0xd8, 0x07, 0xea, 0x8f, 0xe1, 0xae, 0x85, 0x2c, 0x64, 0x6a, 0xb9, 0x27, 0x57, - 0x86, 0xba, 0x1d, 0x32, 0x16, 0x46, 0x44, 0xcb, 0x75, 0x46, 0xdd, 0x3d, 0x41, 0x07, 0x84, 0x0b, - 0x6f, 0x30, 0xd4, 0x80, 0xe6, 0x37, 0x16, 0x94, 0xdb, 0x3d, 0x2f, 0x09, 0xd0, 0x32, 0x14, 0x68, - 0x60, 0x5b, 0x3b, 0xd6, 0x6e, 0x0d, 0x17, 0x68, 0x80, 0xfe, 0x0c, 0xe5, 0xc4, 0x8b, 0x43, 0x62, - 0x17, 0x76, 0xac, 0xdd, 0xfa, 0x7e, 0xa3, 0xa5, 0x94, 0x61, 0x49, 0x6a, 0x0f, 0x89, 0x8f, 0x35, - 0x17, 0x39, 0xb0, 0x40, 0xe2, 0x60, 0xc8, 0x68, 0x2c, 0xec, 0xa2, 0x12, 0xce, 0xf6, 0x68, 0x03, - 0x6a, 0x01, 0x4d, 0x88, 0x2f, 0x58, 0x32, 0xb6, 0x4b, 0x8a, 0x39, 0x21, 0xa0, 0x7b, 0x60, 0x9b, - 0xab, 0xbb, 0x01, 0xe5, 0x7d, 0x37, 0xa2, 0x03, 0x2a, 0xdc, 0xce, 0x58, 0x10, 0x6e, 0x97, 0x77, - 0xac, 0xdd, 0x12, 0x5e, 0x37, 0xfc, 0xc7, 0x94, 0xf7, 0x8f, 0x25, 0xf7, 0x50, 0x32, 0x9b, 0x1f, - 0x17, 0x60, 0xed, 0x11, 0x8b, 0x22, 0xe2, 0x0b, 0xca, 0xe2, 0x17, 0x5e, 0x22, 0xa8, 0x5c, 0x70, - 0x74, 0x07, 0xc0, 0xcf, 0xe8, 0xea, 0x2a, 0xf5, 0xfd, 0x35, 0x6d, 0xf7, 0x04, 0xaf, 0x8c, 0x9f, - 0xc2, 0xa1, 0x23, 0x40, 0xc3, 0xf4, 0x0c, 0x97, 0x93, 0x48, 0x99, 0x67, 0x6e, 0x7d, 0xb9, 0x95, - 0x05, 0xe1, 0xd8, 0xeb, 0x90, 0xa8, 0x6d, 0xd8, 0x78, 0x35, 0x13, 0x49, 0x49, 0xe8, 0x1f, 0x00, - 0x31, 0x13, 0x6e, 0x87, 0x74, 0x59, 0x42, 0x94, 0x2f, 0xea, 0xfb, 0x4e, 0x4b, 0x07, 0xa0, 0x95, - 0x06, 0xa0, 0x75, 0x92, 0x06, 0x00, 0xd7, 0x62, 0x26, 0x0e, 0x15, 0x18, 0xdd, 0x03, 0xb9, 0x71, - 0xbd, 0xae, 0x20, 0x89, 0x72, 0xd4, 0xbb, 0x25, 0x17, 0x62, 0x26, 0x0e, 0x24, 0xb6, 0xf9, 0xd6, - 0x82, 0xd2, 0x89, 0xc7, 0xfb, 0xe8, 0x04, 0xd6, 0x27, 0x57, 0x72, 0x33, 0xe3, 0xb8, 0xf1, 0xc2, - 0x66, 0x2b, 0x4d, 0xba, 0x3c, 0xc7, 0x3d, 0xb9, 0x80, 0xd7, 0xfc, 0x3c, 0x87, 0xde, 0x05, 0x08, - 0x48, 0x42, 0x5f, 0x7a, 0xca, 0xa1, 0x85, 0xf3, 0x1d, 0xfa, 0xe4, 0x02, 0x9e, 0x42, 0xa2, 0x7f, - 0x41, 0x63, 0xe0, 0x09, 0x92, 0x50, 0x2f, 0xa2, 0x1f, 0x6a, 0x61, 0xed, 0x8f, 0x3f, 0x69, 0xe1, - 0xff, 0xce, 0x32, 0xcd, 0x09, 0xf3, 0x32, 0x87, 0x15, 0x28, 0x09, 0x8f, 0xf7, 0x9b, 0x9f, 0x58, - 0xb0, 0xf2, 0x22, 0x61, 0xc1, 0xc8, 0x27, 0xc9, 0x51, 0xc2, 0x62, 0x41, 0x49, 0x22, 0x13, 0x6f, - 0x68, 0x68, 0xea, 0x92, 0x45, 0x9c, 0xed, 0xd1, 0x36, 0xd4, 0x23, 0x8f, 0x0b, 0xd7, 0x67, 0x83, - 0x01, 0x15, 0xca, 0xf0, 0x0a, 0x06, 0x49, 0x7a, 0xa4, 0x28, 0xe8, 0x1a, 0x2c, 0xf5, 0x68, 0x2c, - 0x48, 0x90, 0x42, 0x8a, 0x0a, 0xb2, 0xa8, 0x89, 0x06, 0x74, 0x09, 0x2a, 0xac, 0xdb, 0xe5, 0x44, - 0xa8, 0x90, 0x14, 0xb1, 0xd9, 0x35, 0xbf, 0x2e, 0x40, 0xe3, 0xdf, 0x6c, 0x94, 0xc4, 0x5e, 0x94, - 0x59, 0xf3, 0x4f, 0xb8, 0xf2, 0x9e, 0x26, 0xb9, 0xb1, 0x37, 0x20, 0xae, 0x48, 0x46, 0xb1, 0xef, - 0x09, 0xe2, 0x06, 0x24, 0x12, 0x9e, 0x32, 0xb0, 0x8c, 0x6d, 0x03, 0x79, 0xe6, 0x0d, 0xc8, 0x89, - 0x01, 0x3c, 0x96, 0x7c, 0xd4, 0x82, 0x8b, 0x33, 0xe2, 0x7c, 0xd4, 0xed, 0xd2, 0x53, 0x65, 0x78, - 0x0d, 0xaf, 0x4e, 0x89, 0xb5, 0x15, 0x03, 0xd9, 0x50, 0xed, 0xd0, 0x38, 0xa0, 0x71, 0xa8, 0x2c, - 0x5f, 0xc2, 0xe9, 0x16, 0xed, 0xc2, 0x8a, 0x2a, 0x21, 0x37, 0x21, 0x5e, 0x60, 0xb4, 0x6b, 0xf3, - 0x97, 0x15, 0x1d, 0x13, 0x2f, 0xd0, 0x3a, 0x6f, 0x00, 0xd2, 0xc8, 0x0e, 0xe9, 0xd1, 0x38, 0xc5, - 0x96, 0x15, 0x56, 0x9f, 0x71, 0xa8, 0x18, 0x1a, 0x7d, 0x0f, 0x6a, 0xa9, 0x7b, 0xb9, 0x5d, 0xd9, - 0x29, 0xaa, 0x60, 0xa6, 0x49, 0x35, 0x1f, 0x1c, 0x3c, 0xc1, 0x36, 0x3d, 0x58, 0xc8, 0xbc, 0x74, - 0x07, 0x16, 0xcc, 0x5d, 0x64, 0x62, 0xca, 0x33, 0xec, 0xec, 0x8c, 0x39, 0x8f, 0xe2, 0x0c, 0x29, - 0xa3, 0xd9, 0x8d, 0x46, 0xbc, 0x47, 0x02, 0x37, 0xe2, 0x32, 0x0d, 0x8b, 0xbb, 0x25, 0x0c, 0x86, - 0x74, 0xcc, 0xe3, 0xe6, 0xb7, 0x05, 0x58, 0x6e, 0x13, 0xce, 0x29, 0x8b, 0x31, 0x79, 0x7f, 0x44, - 0xb8, 0x40, 0x37, 0xa1, 0xc4, 0x86, 0x24, 0x6d, 0x02, 0x1b, 0x99, 0x96, 0x59, 0x58, 0xeb, 0xf9, - 0x90, 0xc4, 0x58, 0x21, 0xd1, 0x03, 0x58, 0x4d, 0x08, 0x1f, 0x0d, 0x88, 0xeb, 0xf7, 0x88, 0xdf, - 0xd7, 0x1d, 0x4d, 0xa7, 0xfc, 0x6a, 0x26, 0x9e, 0x59, 0xb7, 0xa2, 0xb1, 0x8f, 0x32, 0x28, 0x7a, - 0x0e, 0x8d, 0x98, 0x9c, 0x8a, 0x69, 0x69, 0x9d, 0xf3, 0xd7, 0xcf, 0x53, 0xfe, 0x8c, 0x9c, 0x8a, - 0xc9, 0x01, 0x78, 0x39, 0x9e, 0xd9, 0x3b, 0xff, 0x83, 0x92, 0x34, 0x0f, 0x5d, 0xd5, 0x55, 0x60, - 0x6c, 0x59, 0xca, 0x4e, 0x93, 0x75, 0x8f, 0x15, 0x0b, 0x5d, 0x87, 0x0a, 0x97, 0x4d, 0x9c, 0xdb, - 0x45, 0xe5, 0xd5, 0xe5, 0x89, 0x4a, 0x49, 0xc6, 0x86, 0xeb, 0xac, 0xc0, 0xf2, 0xac, 0xd2, 0xe6, - 0xa7, 0x16, 0x34, 0x32, 0xb3, 0xf8, 0x90, 0xc5, 0x5c, 0x76, 0xa3, 0x8a, 0xf4, 0x08, 0x09, 0x8c, - 0xf7, 0xb6, 0xcf, 0x5e, 0x40, 0x23, 0x95, 0xfb, 0x48, 0x80, 0x0d, 0x1c, 0xdd, 0x3f, 0xeb, 0x82, - 0x73, 0x1d, 0x38, 0x7f, 0xdb, 0x05, 0xa8, 0xe8, 0xd3, 0x9a, 0x5f, 0x94, 0x61, 0xb1, 0x1d, 0x51, - 0x9f, 0xa4, 0xb1, 0x6c, 0xcd, 0xc4, 0xd2, 0x99, 0x58, 0x33, 0x05, 0x9a, 0x8e, 0xe4, 0x2d, 0x28, - 0x73, 0xe1, 0x25, 0xa9, 0xf2, 0x2b, 0xf9, 0x02, 0x6d, 0x09, 0xc1, 0x1a, 0x89, 0x1e, 0x00, 0xa8, - 0x85, 0xaa, 0x1a, 0x13, 0xb7, 0xed, 0x77, 0xc9, 0x11, 0x2f, 0xc0, 0x35, 0x9e, 0x2e, 0xd1, 0x7d, - 0xd5, 0x8c, 0xc2, 0x84, 0x70, 0x6e, 0xfa, 0xf7, 0x56, 0xbe, 0xf4, 0x0b, 0x83, 0xc2, 0x19, 0xde, - 0xf9, 0xcc, 0x32, 0x81, 0xde, 0x04, 0xe0, 0xda, 0xc1, 0xae, 0x79, 0x89, 0xab, 0xb8, 0x66, 0x28, - 0x4f, 0x83, 0xdf, 0x30, 0x0f, 0x64, 0x45, 0xa9, 0x95, 0x4b, 0xe3, 0x80, 0x9c, 0x2a, 0x8b, 0x97, - 0x30, 0x28, 0xd2, 0x53, 0x49, 0x71, 0xaa, 0x50, 0x56, 0xf7, 0x74, 0x7e, 0xb2, 0xa0, 0x96, 0xdd, - 0x78, 0xba, 0xed, 0x58, 0xb3, 0x6d, 0xe7, 0xaf, 0x50, 0xe2, 0x43, 0xe2, 0x1b, 0xe3, 0xd6, 0x27, - 0xcf, 0xa6, 0x29, 0x6b, 0xf5, 0xea, 0x2a, 0x08, 0xfa, 0x0b, 0x34, 0xfc, 0x84, 0xc8, 0xde, 0x98, - 0x90, 0x97, 0x94, 0xa7, 0x8f, 0x43, 0x11, 0x2f, 0x6b, 0x32, 0x36, 0x54, 0x74, 0x15, 0x16, 0x07, - 0x2c, 0x98, 0xa0, 0x74, 0x1b, 0xab, 0x0f, 0x58, 0x90, 0x41, 0xe4, 0x90, 0xc2, 0x46, 0x82, 0xa8, - 0xb6, 0x25, 0x87, 0x94, 0x4c, 0x2f, 0x96, 0x64, 0xac, 0xb9, 0xf2, 0x69, 0x9e, 0xca, 0xc9, 0x5f, - 0xec, 0x5e, 0x53, 0x60, 0x07, 0x60, 0x21, 0x8d, 0x59, 0xf3, 0xf3, 0x22, 0x2c, 0x99, 0x68, 0x9a, - 0x52, 0xf9, 0xfb, 0x5c, 0xa9, 0x6c, 0xce, 0x47, 0x3d, 0xbf, 0x50, 0x9e, 0xc0, 0x52, 0x44, 0xb9, - 0xa0, 0x71, 0xe8, 0x7a, 0x41, 0x40, 0x02, 0xe3, 0xb6, 0x6b, 0xe7, 0x48, 0x1f, 0x6b, 0xec, 0x81, - 0x84, 0xe2, 0xc5, 0x68, 0x6a, 0x87, 0x6e, 0x01, 0xa4, 0x89, 0x44, 0xd2, 0xc4, 0xcd, 0xa9, 0xb6, - 0x29, 0xd0, 0xa4, 0xd2, 0x9c, 0x57, 0x16, 0x2c, 0x4e, 0x9f, 0xfd, 0x47, 0x8d, 0x6f, 0xf3, 0xc7, - 0x32, 0xc0, 0x31, 0x0b, 0xd3, 0x86, 0x71, 0x63, 0xa6, 0x61, 0x4c, 0x9e, 0x98, 0x09, 0x64, 0xba, - 0x5d, 0xec, 0x43, 0xc5, 0x1b, 0x0e, 0x49, 0x9c, 0x46, 0xc1, 0xc9, 0xc3, 0x1f, 0x28, 0x04, 0x36, - 0x48, 0xb4, 0x07, 0x65, 0xf5, 0xfe, 0x64, 0x63, 0x4d, 0x8e, 0xc8, 0x91, 0x04, 0x60, 0x8d, 0x73, - 0xbe, 0xfa, 0x95, 0x45, 0x3e, 0xa9, 0xe0, 0xc2, 0x3b, 0x2b, 0xf8, 0x6f, 0xb0, 0xca, 0x65, 0x8e, - 0xb8, 0xd3, 0x75, 0xac, 0x47, 0x81, 0x86, 0x62, 0xb4, 0xb3, 0x62, 0x46, 0xd7, 0xa1, 0x11, 0xb1, - 0xd0, 0x3d, 0x5b, 0xf1, 0x4b, 0x11, 0x0b, 0x27, 0x38, 0xe7, 0xa3, 0x22, 0x54, 0xf4, 0x3d, 0x7f, - 0x3f, 0xe3, 0x8c, 0x9a, 0xf2, 0x28, 0x4b, 0xa8, 0x18, 0x1b, 0xa3, 0xb3, 0xbd, 0x74, 0x65, 0x3a, - 0xe4, 0x78, 0x63, 0xf3, 0xc9, 0x50, 0x4b, 0xf4, 0x7c, 0xe3, 0x8d, 0x67, 0x06, 0xc4, 0xca, 0xdc, - 0x80, 0xb8, 0x06, 0x65, 0x3f, 0x62, 0x7e, 0xdf, 0xae, 0xaa, 0xb9, 0x4f, 0x6f, 0x24, 0xb5, 0x1b, - 0x79, 0x21, 0xb7, 0x17, 0x94, 0x26, 0xbd, 0x91, 0x6a, 0x86, 0x9e, 0xdf, 0x27, 0x81, 0xdb, 0x27, - 0x63, 0xbb, 0xb6, 0x63, 0xed, 0x2e, 0xe2, 0x9a, 0xa6, 0xfc, 0x87, 0x8c, 0x65, 0x16, 0x07, 0xcc, - 0x77, 0xbd, 0xc4, 0xef, 0xd1, 0x97, 0x24, 0xb0, 0x41, 0x01, 0xea, 0x01, 0xf3, 0x0f, 0x0c, 0x49, - 0x4e, 0x5a, 0x9c, 0x8d, 0x12, 0x9f, 0xa8, 0xaf, 0x1b, 0x37, 0x22, 0x71, 0x28, 0x7a, 0x76, 0x5d, - 0x29, 0x59, 0xd1, 0x1c, 0xf9, 0x65, 0x73, 0xac, 0xe8, 0xce, 0x26, 0x94, 0x55, 0xea, 0x28, 0x23, - 0xc7, 0x7e, 0x44, 0x94, 0xbb, 0x4b, 0x58, 0x6f, 0x9a, 0xaf, 0x2c, 0xa8, 0xab, 0x2c, 0x33, 0x2d, - 0xe8, 0xf6, 0x5c, 0x0b, 0xba, 0x32, 0x9b, 0x8b, 0xf9, 0x0d, 0xe8, 0x2e, 0x54, 0xcd, 0xfc, 0x64, - 0x92, 0x7e, 0x23, 0x57, 0xea, 0x48, 0x63, 0x70, 0x0a, 0x9e, 0xea, 0x1d, 0x0f, 0xa1, 0x6a, 0xb8, - 0xf9, 0x76, 0x9e, 0x9d, 0xda, 0xac, 0xd9, 0xa9, 0x6d, 0xff, 0x3b, 0x0b, 0xaa, 0x6d, 0xad, 0x14, - 0x3d, 0x84, 0xaa, 0x99, 0x2d, 0xd0, 0xe5, 0x73, 0xc6, 0x25, 0xc7, 0x3e, 0x6f, 0x0c, 0xd9, 0xb5, - 0x6e, 0x5a, 0xe8, 0x3e, 0x94, 0x55, 0xd3, 0x44, 0xeb, 0xb9, 0x0f, 0xaf, 0x73, 0x29, 0xbf, 0xb7, - 0x2a, 0xd9, 0x3b, 0x50, 0x3c, 0x66, 0x21, 0xba, 0x98, 0x53, 0xc5, 0xce, 0x5a, 0x9e, 0x63, 0xa4, - 0xd4, 0xe1, 0x83, 0xd7, 0x3f, 0x6c, 0x5d, 0x78, 0xfd, 0x66, 0xcb, 0xfa, 0xfe, 0xcd, 0x96, 0xf5, - 0xe5, 0xdb, 0x2d, 0xeb, 0xff, 0x37, 0x42, 0x2a, 0x7a, 0xa3, 0x4e, 0xcb, 0x67, 0x83, 0x3d, 0xc2, - 0xc5, 0xc8, 0x4b, 0xc6, 0xfa, 0x3b, 0x3d, 0xef, 0x67, 0x80, 0x4e, 0x45, 0x91, 0x6e, 0xff, 0x1c, - 0x00, 0x00, 0xff, 0xff, 0x1e, 0x1f, 0x34, 0x69, 0x25, 0x10, 0x00, 0x00, + // 1665 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x58, 0xcd, 0x6f, 0x1c, 0x49, + 0x15, 0x4f, 0x7b, 0xbe, 0x3c, 0xcf, 0xdf, 0x15, 0x3b, 0xdb, 0x74, 0x12, 0xdb, 0x3b, 0x2b, 0x82, + 0x41, 0xd1, 0x78, 0xd7, 0x1b, 0x36, 0x22, 0x12, 0xd1, 0xc6, 0x59, 0xa2, 0x2c, 0x98, 0xdd, 0x50, + 0x13, 0x24, 0x84, 0x90, 0x5a, 0x3d, 0xdd, 0x6f, 0x7a, 0x8a, 0xe9, 0xe9, 0x1a, 0xba, 0x6a, 0x82, + 0x87, 0x33, 0x07, 0x0e, 0x1c, 0xe0, 0x02, 0x57, 0x38, 0x73, 0xe2, 0xc6, 0x85, 0x23, 0xab, 0x3d, + 0xf2, 0x2f, 0xb0, 0x7b, 0xe2, 0x9f, 0x40, 0xa8, 0x3e, 0xba, 0xa7, 0x7b, 0xdc, 0x0e, 0x1c, 0x72, + 0x80, 0x8b, 0x5d, 0xf5, 0xde, 0xef, 0x7d, 0xd4, 0x7b, 0xaf, 0x5e, 0xbd, 0x1e, 0xe8, 0xc5, 0xfc, + 0x74, 0x96, 0x71, 0xc9, 0x43, 0x9e, 0x88, 0x53, 0x31, 0x9e, 0x8f, 0x46, 0x09, 0xe6, 0xff, 0xfb, + 0x9a, 0x43, 0x3a, 0x76, 0xeb, 0x1d, 0x0e, 0x33, 0x3e, 0xc1, 0xac, 0x10, 0x28, 0x16, 0x06, 0xe8, + 0xdd, 0xa9, 0x28, 0x1b, 0x25, 0xfc, 0xe7, 0xfa, 0x8f, 0xe5, 0xee, 0xc7, 0x3c, 0xe6, 0x7a, 0x79, + 0xaa, 0x56, 0x96, 0x7a, 0x14, 0x73, 0x1e, 0x27, 0x68, 0xe4, 0x86, 0xf3, 0xd1, 0xa9, 0x64, 0x53, + 0x14, 0x32, 0x98, 0xce, 0x0c, 0xa0, 0xf7, 0x67, 0x07, 0x5a, 0x83, 0x71, 0x90, 0x45, 0x64, 0x1b, + 0xd6, 0x58, 0xe4, 0x3a, 0xc7, 0xce, 0x49, 0x97, 0xae, 0xb1, 0x88, 0x7c, 0x15, 0x5a, 0x59, 0x90, + 0xc6, 0xe8, 0xae, 0x1d, 0x3b, 0x27, 0x1b, 0x67, 0x3b, 0x7d, 0x6d, 0x8c, 0x2a, 0xd2, 0x60, 0x86, + 0x21, 0x35, 0x5c, 0xe2, 0xc1, 0x3a, 0xa6, 0xd1, 0x8c, 0xb3, 0x54, 0xba, 0x0d, 0x2d, 0x5c, 0xec, + 0xc9, 0x1d, 0xe8, 0x46, 0x2c, 0xc3, 0x50, 0xf2, 0x6c, 0xe1, 0x36, 0x35, 0x73, 0x49, 0x20, 0x0f, + 0xc1, 0xb5, 0x47, 0xf7, 0x23, 0x26, 0x26, 0x7e, 0xc2, 0xa6, 0x4c, 0xfa, 0xc3, 0x85, 0x44, 0xe1, + 0xb6, 0x8e, 0x9d, 0x93, 0x26, 0x3d, 0xb0, 0xfc, 0x8f, 0x98, 0x98, 0x5c, 0x28, 0xee, 0xb9, 0x62, + 0xf6, 0x7e, 0xb5, 0x06, 0xfb, 0x4f, 0x79, 0x92, 0x60, 0x28, 0x19, 0x4f, 0x5f, 0x04, 0x99, 0x64, + 0x6a, 0x21, 0xc8, 0x03, 0x80, 0xb0, 0xa0, 0xeb, 0xa3, 0x6c, 0x9c, 0xed, 0x1b, 0xbf, 0x97, 0x78, + 0xed, 0x7c, 0x09, 0x47, 0x9e, 0x01, 0x99, 0xe5, 0x3a, 0x7c, 0x81, 0x89, 0x76, 0xcf, 0x9e, 0xfa, + 0xad, 0x7e, 0x91, 0x84, 0x8b, 0x60, 0x88, 0xc9, 0xc0, 0xb2, 0xe9, 0x5e, 0x21, 0x92, 0x93, 0xc8, + 0xb7, 0x00, 0x52, 0x2e, 0xfd, 0x21, 0x8e, 0x78, 0x86, 0x3a, 0x16, 0x1b, 0x67, 0x5e, 0xdf, 0x24, + 0xa0, 0x9f, 0x27, 0xa0, 0xff, 0x32, 0x4f, 0x00, 0xed, 0xa6, 0x5c, 0x9e, 0x6b, 0x30, 0x79, 0x08, + 0x6a, 0xe3, 0x07, 0x23, 0x89, 0x99, 0x0e, 0xd4, 0xeb, 0x25, 0xd7, 0x53, 0x2e, 0x9f, 0x28, 0x6c, + 0xef, 0x4b, 0x07, 0x9a, 0x2f, 0x03, 0x31, 0x21, 0x2f, 0xe1, 0x60, 0x79, 0x24, 0xbf, 0x70, 0x4e, + 0xd8, 0x28, 0xdc, 0xed, 0xe7, 0x45, 0x57, 0x17, 0xb8, 0xe7, 0x37, 0xe8, 0x7e, 0x58, 0x17, 0xd0, + 0x0f, 0x00, 0x22, 0xcc, 0xd8, 0xab, 0x40, 0x07, 0x74, 0xed, 0xfa, 0x80, 0x3e, 0xbf, 0x41, 0x4b, + 0x48, 0xf2, 0x1d, 0xd8, 0x99, 0x06, 0x12, 0x33, 0x16, 0x24, 0xec, 0x17, 0x46, 0xd8, 0xc4, 0xe3, + 0x2b, 0x46, 0xf8, 0xfb, 0x55, 0xa6, 0xd5, 0xb0, 0x2a, 0x73, 0xde, 0x86, 0xa6, 0x0c, 0xc4, 0xa4, + 0xf7, 0x6b, 0x07, 0x76, 0x5f, 0x64, 0x3c, 0x9a, 0x87, 0x98, 0x3d, 0xcb, 0x78, 0x2a, 0x19, 0x66, + 0xaa, 0xf0, 0x66, 0x96, 0xa6, 0x0f, 0xd9, 0xa0, 0xc5, 0x9e, 0x1c, 0xc1, 0x46, 0x12, 0x08, 0xe9, + 0x87, 0x7c, 0x3a, 0x65, 0x52, 0x3b, 0xde, 0xa6, 0xa0, 0x48, 0x4f, 0x35, 0x85, 0xbc, 0x03, 0x5b, + 0x63, 0x96, 0x4a, 0x8c, 0x72, 0x48, 0x43, 0x43, 0x36, 0x0d, 0xd1, 0x82, 0x6e, 0x41, 0x9b, 0x8f, + 0x46, 0x02, 0xa5, 0x4e, 0x49, 0x83, 0xda, 0x5d, 0xef, 0x4f, 0x6b, 0xb0, 0xf3, 0x5d, 0x3e, 0xcf, + 0xd2, 0x20, 0x29, 0xbc, 0xf9, 0x36, 0xdc, 0xfe, 0xa9, 0x21, 0xf9, 0x69, 0x30, 0x45, 0x5f, 0x66, + 0xf3, 0x34, 0x0c, 0x24, 0xfa, 0x11, 0x26, 0x32, 0xd0, 0x0e, 0xb6, 0xa8, 0x6b, 0x21, 0x9f, 0x04, + 0x53, 0x7c, 0x69, 0x01, 0x1f, 0x29, 0x3e, 0xe9, 0xc3, 0xcd, 0x8a, 0xb8, 0x98, 0x8f, 0x46, 0xec, + 0x52, 0x3b, 0xde, 0xa5, 0x7b, 0x25, 0xb1, 0x81, 0x66, 0x10, 0x17, 0x3a, 0x43, 0x96, 0x46, 0x2c, + 0x8d, 0xb5, 0xe7, 0x5b, 0x34, 0xdf, 0x92, 0x13, 0xd8, 0xd5, 0x57, 0xc8, 0xcf, 0x30, 0x88, 0xac, + 0x75, 0xe3, 0xfe, 0xb6, 0xa6, 0x53, 0x0c, 0x22, 0x63, 0xf3, 0x3e, 0x10, 0x83, 0x1c, 0xe2, 0x98, + 0xa5, 0x39, 0xb6, 0xa5, 0xb1, 0x46, 0xc7, 0xb9, 0x66, 0x18, 0xf4, 0x43, 0xe8, 0xe6, 0xe1, 0x15, + 0x6e, 0xfb, 0xb8, 0xa1, 0x93, 0x99, 0x17, 0xd5, 0x6a, 0x72, 0xe8, 0x12, 0xdb, 0xfb, 0x5b, 0x03, + 0xd6, 0x8b, 0x30, 0x3d, 0x80, 0x75, 0x7b, 0x18, 0x55, 0x99, 0x4a, 0x89, 0x5b, 0x28, 0x59, 0x09, + 0x29, 0x2d, 0x90, 0x2a, 0x9d, 0xa3, 0x64, 0x2e, 0xc6, 0x18, 0xf9, 0x89, 0x50, 0x75, 0xd8, 0x38, + 0x69, 0x52, 0xb0, 0xa4, 0x0b, 0x91, 0x92, 0x01, 0x1c, 0x24, 0x81, 0x44, 0x21, 0xfd, 0x61, 0x10, + 0x4e, 0x46, 0x2c, 0x49, 0xfc, 0x21, 0xc6, 0x4c, 0x55, 0x9d, 0xb2, 0x71, 0x54, 0xd8, 0xc8, 0x95, + 0xf7, 0xcf, 0x2d, 0xee, 0x5c, 0xc1, 0xe8, 0x4d, 0x23, 0x5d, 0x21, 0x92, 0x9f, 0x80, 0xbb, 0xaa, + 0x34, 0xe4, 0xd3, 0x59, 0x82, 0x12, 0xdd, 0xa6, 0xd6, 0xdb, 0xbb, 0x5e, 0xef, 0x53, 0x8b, 0xa4, + 0xb7, 0xaa, 0xaa, 0x73, 0xba, 0xf7, 0x43, 0xd8, 0xaa, 0x9a, 0x2b, 0x95, 0x80, 0x4e, 0x9d, 0x2d, + 0x01, 0xa7, 0x52, 0x02, 0x2a, 0x7b, 0xb6, 0x04, 0xf6, 0xa1, 0x15, 0x26, 0x3c, 0x9c, 0xd8, 0xea, + 0x36, 0x1b, 0xef, 0x47, 0xb0, 0xbb, 0x6a, 0xea, 0xcd, 0x68, 0xee, 0xfd, 0x65, 0x0d, 0xb6, 0x07, + 0x28, 0x04, 0xe3, 0x29, 0xc5, 0x9f, 0xcd, 0x51, 0x48, 0xf2, 0x2e, 0x34, 0xf9, 0x0c, 0xf3, 0x4e, + 0x7b, 0xa7, 0x88, 0x46, 0x15, 0xd6, 0xff, 0x74, 0x86, 0x29, 0xd5, 0x48, 0xf2, 0x18, 0xf6, 0x32, + 0x14, 0xf3, 0x29, 0xfa, 0xe1, 0x18, 0xc3, 0x89, 0x79, 0x36, 0x4c, 0x5f, 0xd9, 0xbb, 0x12, 0x4c, + 0xba, 0x6b, 0xb0, 0x4f, 0x0b, 0x28, 0xf9, 0x14, 0x76, 0x52, 0xbc, 0x94, 0x65, 0x69, 0xd3, 0x58, + 0xee, 0x5d, 0x67, 0xfc, 0x13, 0xbc, 0x94, 0x4b, 0x05, 0x74, 0x3b, 0xad, 0xec, 0xbd, 0x1f, 0x40, + 0x53, 0xb9, 0x47, 0xde, 0x36, 0xad, 0xc6, 0xfa, 0xb2, 0x55, 0x68, 0x53, 0xcd, 0x95, 0x6a, 0x16, + 0xb9, 0x07, 0x6d, 0xa1, 0x5e, 0x4a, 0x61, 0xab, 0x6a, 0x7b, 0x69, 0x52, 0x91, 0xa9, 0xe5, 0x7a, + 0xbb, 0xb0, 0x5d, 0x35, 0xda, 0xfb, 0x8d, 0x03, 0x3b, 0x85, 0x5b, 0x62, 0xc6, 0x53, 0xa1, 0x5a, + 0x7e, 0x5b, 0x45, 0x04, 0x23, 0x1b, 0xbd, 0xa3, 0xab, 0x07, 0x30, 0x48, 0x1d, 0x3e, 0x8c, 0xa8, + 0x85, 0x93, 0x47, 0x57, 0x43, 0x70, 0x6d, 0x00, 0x57, 0x4f, 0xbb, 0x0e, 0x6d, 0xa3, 0xad, 0xf7, + 0xfb, 0x16, 0x6c, 0x0e, 0x12, 0x16, 0x62, 0x9e, 0xcb, 0x7e, 0x25, 0x97, 0xde, 0xd2, 0x9b, 0x12, + 0xa8, 0x9c, 0xc9, 0xf7, 0xa0, 0x25, 0x64, 0x90, 0xe5, 0xc6, 0x6f, 0xd7, 0x0b, 0x0c, 0x14, 0x84, + 0x1a, 0x24, 0x79, 0x0c, 0xa0, 0x17, 0xba, 0x0a, 0x6d, 0xde, 0x8e, 0x5e, 0x27, 0x87, 0x41, 0x44, + 0xbb, 0x22, 0x5f, 0x92, 0x47, 0xba, 0xe3, 0xc7, 0x19, 0x0a, 0x61, 0x1f, 0xc9, 0xc3, 0x7a, 0xe9, + 0x17, 0x16, 0x45, 0x0b, 0xbc, 0xf7, 0x5b, 0xc7, 0x26, 0xfa, 0x2e, 0x80, 0x30, 0x01, 0xf6, 0xed, + 0xb8, 0xd3, 0xa1, 0x5d, 0x4b, 0xf9, 0x38, 0x7a, 0x83, 0x75, 0xa0, 0xba, 0x96, 0x5e, 0xf9, 0x2c, + 0x8d, 0xf0, 0x52, 0x7b, 0xbc, 0x45, 0x41, 0x93, 0x3e, 0x56, 0x14, 0xaf, 0x03, 0x2d, 0x7d, 0x4e, + 0xef, 0x5f, 0x0e, 0x74, 0x8b, 0x13, 0x97, 0x7b, 0xbb, 0x53, 0xed, 0xed, 0x5f, 0x87, 0xa6, 0x98, + 0x61, 0x68, 0x9d, 0x3b, 0x58, 0xce, 0x26, 0xb6, 0x75, 0xea, 0xd1, 0x46, 0x43, 0xc8, 0xd7, 0x60, + 0x27, 0xcc, 0x50, 0x3d, 0x40, 0x19, 0xbe, 0x62, 0x22, 0x7f, 0x81, 0x1b, 0x74, 0xdb, 0x90, 0xa9, + 0xa5, 0x92, 0xb7, 0x61, 0x73, 0xca, 0xa3, 0x25, 0xca, 0xbc, 0x15, 0x1b, 0x53, 0x1e, 0x15, 0x10, + 0x35, 0x09, 0xf2, 0xb9, 0x44, 0xfd, 0x36, 0xa8, 0x49, 0xb0, 0xb0, 0x4b, 0x15, 0x99, 0x1a, 0xae, + 0x9a, 0x7f, 0x4a, 0x35, 0xf9, 0x1f, 0x9f, 0x88, 0x12, 0xd8, 0x03, 0x58, 0xcf, 0x73, 0xd6, 0xfb, + 0x5d, 0x03, 0xb6, 0x6c, 0x36, 0xed, 0x55, 0xf9, 0xe6, 0xca, 0x55, 0xb9, 0xbb, 0x9a, 0xf5, 0xfa, + 0x8b, 0xf2, 0x1c, 0xb6, 0x12, 0x26, 0x24, 0x4b, 0x63, 0x3f, 0x88, 0x22, 0x8c, 0x6c, 0xd8, 0xde, + 0xb9, 0x46, 0xfa, 0xc2, 0x60, 0x9f, 0x28, 0x28, 0xdd, 0x4c, 0x4a, 0x3b, 0xf2, 0x1e, 0x40, 0x5e, + 0x48, 0x98, 0x17, 0x6e, 0xcd, 0x6d, 0x2b, 0x81, 0x96, 0x37, 0xcd, 0xfb, 0xcc, 0x81, 0xcd, 0xb2, + 0xee, 0xff, 0xd7, 0xfc, 0xf6, 0xfe, 0xd9, 0x02, 0xb8, 0xe0, 0x71, 0xde, 0x30, 0xee, 0x57, 0x1a, + 0xc6, 0xf2, 0x19, 0x5f, 0x42, 0xca, 0xed, 0xe2, 0x0c, 0xda, 0xc1, 0x6c, 0x86, 0x69, 0x9e, 0x05, + 0xaf, 0x0e, 0xff, 0x44, 0x23, 0xa8, 0x45, 0x92, 0x53, 0x68, 0xe9, 0x37, 0xbe, 0x98, 0x1d, 0x6b, + 0x44, 0x9e, 0x29, 0x00, 0x35, 0x38, 0xef, 0x8f, 0xff, 0xe5, 0x25, 0x5f, 0xde, 0xe0, 0xb5, 0xd7, + 0xde, 0xe0, 0x6f, 0xc0, 0x9e, 0x50, 0x35, 0xe2, 0x97, 0xef, 0xb1, 0x99, 0xb7, 0x76, 0x34, 0x63, + 0x50, 0x5c, 0x66, 0x72, 0x0f, 0x76, 0x12, 0x1e, 0xfb, 0x57, 0x6f, 0xfc, 0x56, 0xc2, 0xe3, 0x25, + 0xce, 0xfb, 0x65, 0x03, 0xda, 0xe6, 0x9c, 0xff, 0x3b, 0x33, 0xa3, 0x1e, 0xa5, 0x19, 0xcf, 0x98, + 0x5c, 0x58, 0xa7, 0x8b, 0xbd, 0x0a, 0x65, 0x3e, 0x49, 0x06, 0x0b, 0xfb, 0x5d, 0xd6, 0xcd, 0xcc, + 0x10, 0x19, 0x2c, 0x2a, 0x53, 0x78, 0x7b, 0x65, 0x0a, 0x2f, 0xe6, 0x88, 0x4e, 0x69, 0x8e, 0x50, + 0xd4, 0x51, 0x12, 0xc4, 0xc2, 0x5d, 0xd7, 0x96, 0xcc, 0x46, 0x99, 0x99, 0x05, 0xe1, 0x04, 0x23, + 0x7f, 0x82, 0x0b, 0xb7, 0x7b, 0xec, 0x9c, 0x6c, 0xd2, 0xae, 0xa1, 0x7c, 0x0f, 0x17, 0xaa, 0x8a, + 0x23, 0x1e, 0xfa, 0x41, 0x16, 0x8e, 0xd9, 0x2b, 0x8c, 0x5c, 0xd0, 0x80, 0x8d, 0x88, 0x87, 0x4f, + 0x2c, 0x49, 0x8d, 0xb3, 0x82, 0xcf, 0xb3, 0x10, 0xf5, 0x27, 0xa4, 0x9f, 0x60, 0x1a, 0xcb, 0xb1, + 0xbb, 0xa1, 0x8d, 0xec, 0x1a, 0x8e, 0xfa, 0x7c, 0xbc, 0xd0, 0x74, 0xef, 0x2e, 0xb4, 0x74, 0xe9, + 0x68, 0x27, 0x17, 0x61, 0x82, 0x3a, 0xdc, 0x4d, 0x6a, 0x36, 0xbd, 0xcf, 0x1c, 0xd8, 0xd0, 0x55, + 0x66, 0x5b, 0xd0, 0xfb, 0x2b, 0x2d, 0xe8, 0x76, 0xb5, 0x16, 0xeb, 0x1b, 0xd0, 0x07, 0xd0, 0xb1, + 0x33, 0xaa, 0x2d, 0xfa, 0x3b, 0xb5, 0x52, 0xcf, 0x0c, 0x86, 0xe6, 0xe0, 0x52, 0xef, 0xf8, 0x10, + 0x3a, 0x96, 0x5b, 0xef, 0xe7, 0xd5, 0xc9, 0xd8, 0xa9, 0x4e, 0xc6, 0x67, 0x7f, 0x75, 0xa0, 0x33, + 0x30, 0x46, 0xc9, 0x87, 0xd0, 0xb1, 0xb3, 0x05, 0x79, 0xeb, 0x9a, 0x71, 0xc9, 0x73, 0xaf, 0x1b, + 0x43, 0x4e, 0x9c, 0x77, 0x1d, 0xf2, 0x08, 0x5a, 0xba, 0x69, 0x92, 0x83, 0xda, 0x87, 0xd7, 0xbb, + 0x55, 0xdf, 0x5b, 0xb5, 0xec, 0x03, 0x68, 0x5c, 0xf0, 0x98, 0xdc, 0xac, 0xb9, 0xc5, 0xde, 0x7e, + 0x5d, 0x60, 0x94, 0xd4, 0xf9, 0xe3, 0xcf, 0xff, 0x71, 0x78, 0xe3, 0xf3, 0x2f, 0x0e, 0x9d, 0xbf, + 0x7f, 0x71, 0xe8, 0xfc, 0xe1, 0xcb, 0x43, 0xe7, 0xc7, 0xf7, 0x63, 0x26, 0xc7, 0xf3, 0x61, 0x3f, + 0xe4, 0xd3, 0x53, 0x14, 0x72, 0x1e, 0x64, 0x0b, 0xf3, 0x63, 0x48, 0xdd, 0x6f, 0x2d, 0xc3, 0xb6, + 0x26, 0xbd, 0xff, 0xef, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe8, 0x31, 0xad, 0x01, 0x8a, 0x11, 0x00, + 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -2145,6 +2252,34 @@ func (m *Frontier) MarshalToSizedBuffer(dAtA []byte) (int, error) { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } + if len(m.LatestBackfillComplete) > 0 { + for iNdEx := len(m.LatestBackfillComplete) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.LatestBackfillComplete[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintShuffle(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + } + if len(m.LatestBackfillBegin) > 0 { + for iNdEx := len(m.LatestBackfillBegin) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.LatestBackfillBegin[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintShuffle(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } if len(m.FlushedLsn) > 0 { dAtA10 := make([]byte, len(m.FlushedLsn)*10) var j9 int @@ -2180,6 +2315,86 @@ func (m *Frontier) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *Frontier_BackfillBegin) Marshal() (dAtA []byte, err error) { + size := m.ProtoSize() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Frontier_BackfillBegin) MarshalTo(dAtA []byte) (int, error) { + size := m.ProtoSize() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Frontier_BackfillBegin) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.Clock != 0 { + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.Clock)) + i-- + dAtA[i] = 0x11 + } + if len(m.JournalReadSuffix) > 0 { + i -= len(m.JournalReadSuffix) + copy(dAtA[i:], m.JournalReadSuffix) + i = encodeVarintShuffle(dAtA, i, uint64(len(m.JournalReadSuffix))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *Frontier_BackfillComplete) Marshal() (dAtA []byte, err error) { + size := m.ProtoSize() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Frontier_BackfillComplete) MarshalTo(dAtA []byte) (int, error) { + size := m.ProtoSize() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Frontier_BackfillComplete) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.Clock != 0 { + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.Clock)) + i-- + dAtA[i] = 0x11 + } + if len(m.JournalReadSuffix) > 0 { + i -= len(m.JournalReadSuffix) + copy(dAtA[i:], m.JournalReadSuffix) + i = encodeVarintShuffle(dAtA, i, uint64(len(m.JournalReadSuffix))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func (m *SessionRequest) Marshal() (dAtA []byte, err error) { size := m.ProtoSize() dAtA = make([]byte, size) @@ -3384,6 +3599,56 @@ func (m *Frontier) ProtoSize() (n int) { } n += 1 + sovShuffle(uint64(l)) + l } + if len(m.LatestBackfillBegin) > 0 { + for _, e := range m.LatestBackfillBegin { + l = e.ProtoSize() + n += 1 + l + sovShuffle(uint64(l)) + } + } + if len(m.LatestBackfillComplete) > 0 { + for _, e := range m.LatestBackfillComplete { + l = e.ProtoSize() + n += 1 + l + sovShuffle(uint64(l)) + } + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *Frontier_BackfillBegin) ProtoSize() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.JournalReadSuffix) + if l > 0 { + n += 1 + l + sovShuffle(uint64(l)) + } + if m.Clock != 0 { + n += 9 + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *Frontier_BackfillComplete) ProtoSize() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.JournalReadSuffix) + if l > 0 { + n += 1 + l + sovShuffle(uint64(l)) + } + if m.Clock != 0 { + n += 9 + } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } @@ -4824,6 +5089,260 @@ func (m *Frontier) Unmarshal(dAtA []byte) error { } else { return fmt.Errorf("proto: wrong wireType = %d for field FlushedLsn", wireType) } + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LatestBackfillBegin", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowShuffle + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthShuffle + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthShuffle + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.LatestBackfillBegin = append(m.LatestBackfillBegin, &Frontier_BackfillBegin{}) + if err := m.LatestBackfillBegin[len(m.LatestBackfillBegin)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LatestBackfillComplete", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowShuffle + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthShuffle + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthShuffle + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.LatestBackfillComplete = append(m.LatestBackfillComplete, &Frontier_BackfillComplete{}) + if err := m.LatestBackfillComplete[len(m.LatestBackfillComplete)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipShuffle(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthShuffle + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Frontier_BackfillBegin) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowShuffle + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: BackfillBegin: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: BackfillBegin: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field JournalReadSuffix", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowShuffle + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthShuffle + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthShuffle + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.JournalReadSuffix = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field Clock", wireType) + } + m.Clock = 0 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + m.Clock = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + default: + iNdEx = preIndex + skippy, err := skipShuffle(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthShuffle + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Frontier_BackfillComplete) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowShuffle + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: BackfillComplete: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: BackfillComplete: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field JournalReadSuffix", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowShuffle + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthShuffle + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthShuffle + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.JournalReadSuffix = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field Clock", wireType) + } + m.Clock = 0 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + m.Clock = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 default: iNdEx = preIndex skippy, err := skipShuffle(dAtA[iNdEx:]) diff --git a/go/protocols/shuffle/shuffle.proto b/go/protocols/shuffle/shuffle.proto index 7689ac8af53..03160a8870e 100644 --- a/go/protocols/shuffle/shuffle.proto +++ b/go/protocols/shuffle/shuffle.proto @@ -111,6 +111,32 @@ message Frontier { repeated JournalFrontier journals = 1; // Per-shard flushed LSN, indexed by shard_index. repeated uint64 flushed_lsn = 2; + + // BackfillBegin is a binding's latest backfill-begin clock, keyed by the + // binding's stable journal_read_suffix. + message BackfillBegin { + // Binding's journal read suffix, stable across task versions. + string journal_read_suffix = 1; + // Clock of the binding's most recent backfill-begin. + fixed64 clock = 2; + } + // Latest backfill-begin clock for each binding in the checkpoint delta. + // Populated only on a terminal (empty-journals) frontier of a Progressed + // or NextCheckpoint sequence. Empty otherwise. + repeated BackfillBegin latest_backfill_begin = 3; + + // BackfillComplete reports a binding's most recent backfill-complete event, + // keyed by the binding's stable journal_read_suffix. + message BackfillComplete { + // Binding's journal read suffix, stable across task versions. + string journal_read_suffix = 1; + // Truncation boundary of the completed backfill: the backfill's begin clock. + fixed64 clock = 2; + } + // Latest backfill-complete clock for each binding in the checkpoint delta. + // Populated only on a terminal (empty-journals) frontier of a Progressed + // or NextCheckpoint sequence. Empty otherwise. + repeated BackfillComplete latest_backfill_complete = 4; } // SessionRequest is sent by the Coordinator to manage the shuffle session. diff --git a/go/runtime/materialize.go b/go/runtime/materialize.go index c2976cb6525..06d7574fc03 100644 --- a/go/runtime/materialize.go +++ b/go/runtime/materialize.go @@ -7,6 +7,7 @@ import ( "io" "github.com/estuary/flow/go/bindings" + "github.com/estuary/flow/go/labels" "github.com/estuary/flow/go/protocols/catalog" pf "github.com/estuary/flow/go/protocols/flow" pm "github.com/estuary/flow/go/protocols/materialize" @@ -124,8 +125,12 @@ func (m *materializeApp) ConsumeMessage(shard consumer.Shard, envelope message.E var keyPacked = isr.Arena.Bytes(isr.PackedKey[isr.Index]) var docJson = isr.Arena.Bytes(isr.Docs[isr.Index]) - if message.GetFlags(isr.GetUUID()) == message.Flag_ACK_TXN { - return nil // We just ignore the ACK documents. + var flags = message.GetFlags(isr.GetUUID()) + if flags&0x3 == message.Flag_ACK_TXN { + return nil // Ignore ACK documents. + } + if uint16(flags)&labels.FlagControl != 0 { + return nil // Drop control docs before key extraction. } var request = &pm.Request{ diff --git a/go/shuffle/reader_test.go b/go/shuffle/reader_test.go index 809b79af795..6152a96b7dd 100644 --- a/go/shuffle/reader_test.go +++ b/go/shuffle/reader_test.go @@ -337,7 +337,11 @@ func (a testApp) ConsumeMessage(shard consumer.Shard, store consumer.Store, env var state = *store.(*testStore).State.(*map[string]int) var msg = env.Message.(pr.IndexedShuffleResponse) - if message.GetFlags(env.GetUUID()) == message.Flag_ACK_TXN { + var flags = message.GetFlags(env.GetUUID()) + if flags&0x3 == message.Flag_ACK_TXN { + return nil + } + if uint16(flags)&labels.FlagControl != 0 { return nil } diff --git a/go/shuffle/subscriber.go b/go/shuffle/subscriber.go index 477afd08276..26dfe1ad3db 100644 --- a/go/shuffle/subscriber.go +++ b/go/shuffle/subscriber.go @@ -8,6 +8,7 @@ import ( "sort" "github.com/estuary/flow/go/flow" + "github.com/estuary/flow/go/labels" pf "github.com/estuary/flow/go/protocols/flow" pr "github.com/estuary/flow/go/protocols/runtime" pb "go.gazette.dev/core/broker/protocol" @@ -123,13 +124,17 @@ func (s subscribers) stageResponses(from *pr.ShuffleResponse) { s[i].staged.WriteHead = from.WriteHead } for doc, uuid := range from.UuidParts { - if message.Flags(uuid.Node) == message.Flag_ACK_TXN { + var flags = message.Flags(uuid.Node) + if flags&0x3 == message.Flag_ACK_TXN { // ACK documents are always broadcast to every subscriber. for i := range s { s[i].stageDoc(from, doc) } continue } + if uint16(flags)&labels.FlagControl != 0 { + continue + } var keyHash = flow.PackedKeyHash_HH64(from.Arena.Bytes(from.PackedKey[doc])) var start, stop = s.keySpan(keyHash)