1use std::collections::{BTreeMap, BTreeSet};
13use std::mem;
14
15use bytesize::ByteSize;
16use differential_dataflow::lattice::Lattice;
17use mz_expr::row::RowCollection;
18use mz_ore::cast::CastInto;
19use mz_ore::soft_panic_or_log;
20use mz_ore::tracing::OpenTelemetryContext;
21use mz_repr::{GlobalId, Timestamp, UpdateCollection};
22use mz_service::client::{Partitionable, PartitionedState};
23use timely::PartialOrder;
24use timely::progress::frontier::{Antichain, MutableAntichain};
25use uuid::Uuid;
26
27use crate::protocol::command::ComputeCommand;
28use crate::protocol::response::{
29 ComputeResponse, CopyToResponse, FrontiersResponse, PeekError, PeekResponse,
30 StashedPeekResponse, SubscribeBatch, SubscribeResponse,
31};
32
33#[derive(Debug)]
56pub struct PartitionedComputeState {
57 parts: usize,
59 max_result_size: u64,
63 frontiers: BTreeMap<GlobalId, TrackedFrontiers>,
76 peek_responses: BTreeMap<Uuid, PendingPeek>,
87 copy_to_responses: BTreeMap<GlobalId, (CopyToResponse, BTreeSet<usize>)>,
98 pending_subscribes: BTreeMap<GlobalId, PendingSubscribe>,
122}
123
124impl Partitionable<ComputeCommand, ComputeResponse> for (ComputeCommand, ComputeResponse) {
125 type PartitionedState = PartitionedComputeState;
126
127 fn new(parts: usize) -> PartitionedComputeState {
128 PartitionedComputeState {
129 parts,
130 max_result_size: u64::MAX,
131 frontiers: BTreeMap::new(),
132 peek_responses: BTreeMap::new(),
133 pending_subscribes: BTreeMap::new(),
134 copy_to_responses: BTreeMap::new(),
135 }
136 }
137}
138
139impl PartitionedComputeState {
140 pub fn observe_command(&mut self, command: &ComputeCommand) {
142 match command {
143 ComputeCommand::UpdateConfiguration(config) => {
144 if let Some(max_result_size) = config.max_result_size {
145 self.max_result_size = max_result_size;
146 }
147 }
148 _ => {
149 }
152 }
153 }
154
155 fn absorb_frontiers(
157 &mut self,
158 shard_id: usize,
159 collection_id: GlobalId,
160 frontiers: FrontiersResponse,
161 ) -> Option<ComputeResponse> {
162 let tracked = self
163 .frontiers
164 .entry(collection_id)
165 .or_insert_with(|| TrackedFrontiers::new(self.parts));
166
167 let write_frontier = frontiers
168 .write_frontier
169 .and_then(|f| tracked.update_write_frontier(shard_id, &f));
170 let input_frontier = frontiers
171 .input_frontier
172 .and_then(|f| tracked.update_input_frontier(shard_id, &f));
173 let output_frontier = frontiers
174 .output_frontier
175 .and_then(|f| tracked.update_output_frontier(shard_id, &f));
176
177 let frontiers = FrontiersResponse {
178 write_frontier,
179 input_frontier,
180 output_frontier,
181 };
182 let result = frontiers
183 .has_updates()
184 .then_some(ComputeResponse::Frontiers(collection_id, frontiers));
185
186 if tracked.all_empty() {
187 self.frontiers.remove(&collection_id);
190 }
191
192 result
193 }
194
195 fn absorb_peek_response(
197 &mut self,
198 shard_id: usize,
199 uuid: Uuid,
200 response: PeekResponse,
201 otel_ctx: OpenTelemetryContext,
202 ) -> Option<ComputeResponse> {
203 let pending = self
204 .peek_responses
205 .entry(uuid)
206 .or_insert_with(PendingPeek::new);
207 pending.absorb(shard_id, response, self.max_result_size);
208
209 if pending.ready_shards.len() == self.parts {
210 let response = self.peek_responses.remove(&uuid).unwrap().response;
211 Some(ComputeResponse::PeekResponse(uuid, response, otel_ctx))
212 } else {
213 None
214 }
215 }
216
217 fn absorb_copy_to_response(
219 &mut self,
220 shard_id: usize,
221 copyto_id: GlobalId,
222 response: CopyToResponse,
223 ) -> Option<ComputeResponse> {
224 use CopyToResponse::*;
225
226 let (merged, ready_shards) = self
227 .copy_to_responses
228 .entry(copyto_id)
229 .or_insert((CopyToResponse::RowCount(0), BTreeSet::new()));
230
231 let first = ready_shards.insert(shard_id);
232 assert!(first, "duplicate copy-to response");
233
234 let resp1 = mem::replace(merged, Dropped);
235 *merged = match (resp1, response) {
236 (Dropped, _) | (_, Dropped) => Dropped,
237 (Error(e), _) | (_, Error(e)) => Error(e),
238 (RowCount(r1), RowCount(r2)) => RowCount(r1 + r2),
239 };
240
241 if ready_shards.len() == self.parts {
242 let (response, _) = self.copy_to_responses.remove(©to_id).unwrap();
243 Some(ComputeResponse::CopyToResponse(copyto_id, response))
244 } else {
245 None
246 }
247 }
248
249 fn absorb_subscribe_response(
251 &mut self,
252 subscribe_id: GlobalId,
253 response: SubscribeResponse,
254 ) -> Option<ComputeResponse> {
255 let tracked = self
256 .pending_subscribes
257 .entry(subscribe_id)
258 .or_insert_with(|| PendingSubscribe::new(self.parts));
259
260 let emit_response = match response {
261 SubscribeResponse::Batch(batch) => {
262 let frontiers = &mut tracked.frontiers;
263 let old_frontier = frontiers.frontier().to_owned();
264 frontiers.update_iter(batch.lower.into_iter().map(|t| (t, -1)));
265 frontiers.update_iter(batch.upper.into_iter().map(|t| (t, 1)));
266 let new_frontier = frontiers.frontier().to_owned();
267
268 tracked.stash(batch.updates, self.max_result_size);
269
270 if old_frontier != new_frontier && !tracked.dropped {
274 let updates = match &mut tracked.stashed_updates {
275 Ok(stashed_updates) => {
276 let mut ship = vec![];
278 let mut keep = vec![];
279 for collection in stashed_updates.drain(..) {
280 let partition_point = collection
281 .times()
282 .partition_point(|t| !new_frontier.less_equal(t));
283 let (ship_coll, keep_coll) = collection.split_at(partition_point);
284 if ship_coll.len() > 0 {
285 ship.push(ship_coll);
286 }
287 if keep_coll.len() > 0 {
288 keep.push(keep_coll);
289 }
290 }
291 tracked.stashed_result_size = keep.iter().map(|c| c.byte_len()).sum();
292 tracked.stashed_updates = Ok(keep);
293 Ok(ship)
294 }
295 Err(text) => Err(text.clone()),
296 };
297 Some(ComputeResponse::SubscribeResponse(
298 subscribe_id,
299 SubscribeResponse::Batch(SubscribeBatch {
300 lower: old_frontier,
301 upper: new_frontier,
302 updates,
303 }),
304 ))
305 } else {
306 None
307 }
308 }
309 SubscribeResponse::DroppedAt(frontier) => {
310 tracked
311 .frontiers
312 .update_iter(frontier.iter().map(|t| (t.clone(), -1)));
313
314 if tracked.dropped {
315 None
316 } else {
317 tracked.dropped = true;
318 Some(ComputeResponse::SubscribeResponse(
319 subscribe_id,
320 SubscribeResponse::DroppedAt(frontier),
321 ))
322 }
323 }
324 };
325
326 if tracked.frontiers.frontier().is_empty() {
327 self.pending_subscribes.remove(&subscribe_id);
330 }
331
332 emit_response
333 }
334}
335
336impl PartitionedState<ComputeCommand, ComputeResponse> for PartitionedComputeState {
337 fn split_command(&mut self, command: ComputeCommand) -> Vec<Option<ComputeCommand>> {
338 self.observe_command(&command);
339
340 match command {
344 command @ ComputeCommand::Hello { .. }
345 | command @ ComputeCommand::UpdateConfiguration(_) => {
346 vec![Some(command); self.parts]
347 }
348 command => {
349 let mut r = vec![None; self.parts];
350 r[0] = Some(command);
351 r
352 }
353 }
354 }
355
356 fn absorb_response(
357 &mut self,
358 shard_id: usize,
359 message: ComputeResponse,
360 ) -> Option<Result<ComputeResponse, anyhow::Error>> {
361 let response = match message {
362 ComputeResponse::Frontiers(id, frontiers) => {
363 self.absorb_frontiers(shard_id, id, frontiers)
364 }
365 ComputeResponse::PeekResponse(uuid, response, otel_ctx) => {
366 self.absorb_peek_response(shard_id, uuid, response, otel_ctx)
367 }
368 ComputeResponse::SubscribeResponse(id, response) => {
369 self.absorb_subscribe_response(id, response)
370 }
371 ComputeResponse::CopyToResponse(id, response) => {
372 self.absorb_copy_to_response(shard_id, id, response)
373 }
374 response @ ComputeResponse::Status(_) => {
375 Some(response)
377 }
378 };
379
380 response.map(Ok)
381 }
382}
383
384#[derive(Debug)]
389struct TrackedFrontiers {
390 write_frontier: (MutableAntichain<Timestamp>, Vec<Antichain<Timestamp>>),
392 input_frontier: (MutableAntichain<Timestamp>, Vec<Antichain<Timestamp>>),
394 output_frontier: (MutableAntichain<Timestamp>, Vec<Antichain<Timestamp>>),
396}
397
398impl TrackedFrontiers {
399 fn new(parts: usize) -> Self {
401 #[allow(clippy::as_conversions)]
403 let parts_diff = parts as i64;
404
405 let mut frontier = MutableAntichain::new();
406 frontier.update_iter([(Timestamp::MIN, parts_diff)]);
407 let part_frontiers = vec![Antichain::from_elem(Timestamp::MIN); parts];
408 let frontier_entry = (frontier, part_frontiers);
409
410 Self {
411 write_frontier: frontier_entry.clone(),
412 input_frontier: frontier_entry.clone(),
413 output_frontier: frontier_entry,
414 }
415 }
416
417 fn all_empty(&self) -> bool {
419 self.write_frontier.0.frontier().is_empty()
420 && self.input_frontier.0.frontier().is_empty()
421 && self.output_frontier.0.frontier().is_empty()
422 }
423
424 fn update_write_frontier(
428 &mut self,
429 shard_id: usize,
430 new_shard_frontier: &Antichain<Timestamp>,
431 ) -> Option<Antichain<Timestamp>> {
432 Self::update_frontier(&mut self.write_frontier, shard_id, new_shard_frontier)
433 }
434
435 fn update_input_frontier(
439 &mut self,
440 shard_id: usize,
441 new_shard_frontier: &Antichain<Timestamp>,
442 ) -> Option<Antichain<Timestamp>> {
443 Self::update_frontier(&mut self.input_frontier, shard_id, new_shard_frontier)
444 }
445
446 fn update_output_frontier(
450 &mut self,
451 shard_id: usize,
452 new_shard_frontier: &Antichain<Timestamp>,
453 ) -> Option<Antichain<Timestamp>> {
454 Self::update_frontier(&mut self.output_frontier, shard_id, new_shard_frontier)
455 }
456
457 fn update_frontier(
459 entry: &mut (MutableAntichain<Timestamp>, Vec<Antichain<Timestamp>>),
460 shard_id: usize,
461 new_shard_frontier: &Antichain<Timestamp>,
462 ) -> Option<Antichain<Timestamp>> {
463 let (frontier, shard_frontiers) = entry;
464
465 let old_frontier = frontier.frontier().to_owned();
466 let shard_frontier = &mut shard_frontiers[shard_id];
467 frontier.update_iter(shard_frontier.iter().map(|t| (t.clone(), -1)));
468 shard_frontier.join_assign(new_shard_frontier);
469 frontier.update_iter(shard_frontier.iter().map(|t| (t.clone(), 1)));
470
471 let new_frontier = frontier.frontier();
472
473 if PartialOrder::less_than(&old_frontier.borrow(), &new_frontier) {
474 Some(new_frontier.to_owned())
475 } else {
476 None
477 }
478 }
479}
480
481#[derive(Debug)]
482struct PendingSubscribe {
483 frontiers: MutableAntichain<Timestamp>,
485 stashed_updates: Result<Vec<UpdateCollection>, String>,
487 stashed_result_size: usize,
489 dropped: bool,
493}
494
495impl PendingSubscribe {
496 fn new(parts: usize) -> Self {
497 let mut frontiers = MutableAntichain::new();
498 #[allow(clippy::as_conversions)]
500 frontiers.update_iter([(Timestamp::MIN, parts as i64)]);
501
502 Self {
503 frontiers,
504 stashed_updates: Ok(Vec::new()),
505 stashed_result_size: 0,
506 dropped: false,
507 }
508 }
509
510 fn stash(&mut self, new_updates: Result<Vec<UpdateCollection>, String>, max_result_size: u64) {
515 match (&mut self.stashed_updates, new_updates) {
516 (Err(_), _) => {
517 }
520 (_, Err(text)) => {
521 self.stashed_updates = Err(text);
522 }
523 (Ok(stashed), Ok(new)) => {
524 let new_size: usize = new.iter().map(|coll| coll.byte_len()).sum();
525 self.stashed_result_size += new_size;
526
527 if self.stashed_result_size > max_result_size.cast_into() {
528 self.stashed_updates = Err(format!(
529 "total result exceeds max size of {}",
530 ByteSize::b(max_result_size)
531 ));
532 } else {
533 stashed.extend(new);
534 }
535 }
536 }
537 }
538}
539
540#[derive(Debug)]
543struct PendingPeek {
544 response: PeekResponse,
546 inline_byte_len: usize,
552 ready_shards: BTreeSet<usize>,
554}
555
556impl PendingPeek {
557 fn new() -> Self {
558 Self {
559 response: PeekResponse::Rows(vec![RowCollection::default()]),
560 inline_byte_len: 0,
561 ready_shards: BTreeSet::new(),
562 }
563 }
564
565 fn absorb(&mut self, shard_id: usize, response: PeekResponse, max_result_size: u64) {
566 let first = self.ready_shards.insert(shard_id);
567 assert!(first, "duplicate peek response");
568
569 self.inline_byte_len = self
570 .inline_byte_len
571 .saturating_add(response.inline_byte_len());
572 let current = mem::replace(&mut self.response, PeekResponse::Canceled);
573 self.response = merge_peek_responses(current, response);
574
575 if self.inline_byte_len > max_result_size.cast_into() {
578 let error = PeekError::ResultExceedsMaxSize {
579 max_result_size: max_result_size.cast_into(),
580 };
581 let current = mem::replace(&mut self.response, PeekResponse::Canceled);
582 self.response = merge_peek_responses(current, PeekResponse::Error(error));
583 }
584 }
585}
586
587fn merge_peek_responses(resp1: PeekResponse, resp2: PeekResponse) -> PeekResponse {
589 use PeekResponse::*;
590
591 let (resp1, resp2) = match (resp1, resp2) {
593 (Canceled, _) | (_, Canceled) => return Canceled,
594 (Error(e1), Error(e2)) => return Error(merge_peek_errors(e1, e2)),
595 (Error(e), _) | (_, Error(e)) => return Error(e),
596 resps => resps,
597 };
598
599 match (resp1, resp2) {
600 (Rows(mut rows1), Rows(rows2)) => {
601 rows1.extend(rows2);
602 Rows(rows1)
603 }
604 (Rows(rows), Stashed(mut stashed)) | (Stashed(mut stashed), Rows(rows)) => {
605 stashed.inline_rows.extend(rows);
606 Stashed(stashed)
607 }
608 (Stashed(stashed1), Stashed(stashed2)) => {
609 let StashedPeekResponse {
612 num_rows_batches: num_rows_batches1,
613 encoded_size_bytes: encoded_size_bytes1,
614 relation_desc: relation_desc1,
615 shard_id: shard_id1,
616 batches: mut batches1,
617 inline_rows: mut inline_rows1,
618 } = *stashed1;
619 let StashedPeekResponse {
620 num_rows_batches: num_rows_batches2,
621 encoded_size_bytes: encoded_size_bytes2,
622 relation_desc: relation_desc2,
623 shard_id: shard_id2,
624 batches: mut batches2,
625 inline_rows: inline_rows2,
626 } = *stashed2;
627
628 if shard_id1 != shard_id2 {
629 soft_panic_or_log!(
630 "shard IDs of stashed responses do not match: \
631 {shard_id1} != {shard_id2}"
632 );
633 return Error(PeekError::unstructured("internal error"));
634 }
635 if relation_desc1 != relation_desc2 {
636 soft_panic_or_log!(
637 "relation descs of stashed responses do not match: \
638 {relation_desc1:?} != {relation_desc2:?}"
639 );
640 return Error(PeekError::unstructured("internal error"));
641 }
642
643 batches1.append(&mut batches2);
644 inline_rows1.extend(inline_rows2);
645
646 Stashed(Box::new(StashedPeekResponse {
647 num_rows_batches: num_rows_batches1 + num_rows_batches2,
648 encoded_size_bytes: encoded_size_bytes1 + encoded_size_bytes2,
649 relation_desc: relation_desc1,
650 shard_id: shard_id1,
651 batches: batches1,
652 inline_rows: inline_rows1,
653 }))
654 }
655 _ => unreachable!("handled above"),
656 }
657}
658
659fn merge_peek_errors(error1: PeekError, error2: PeekError) -> PeekError {
665 match (error1, error2) {
666 (
667 PeekError::RowIterationLimitExceeded { limit: limit1 },
668 PeekError::RowIterationLimitExceeded { limit: limit2 },
669 ) => PeekError::RowIterationLimitExceeded {
670 limit: limit1.min(limit2),
671 },
672 (PeekError::RowIterationLimitExceeded { .. }, error)
673 | (error, PeekError::RowIterationLimitExceeded { .. }) => error,
674 (error, _) => error,
675 }
676}
677
678#[cfg(test)]
679mod tests;