1use std::{
33 collections::{HashMap, VecDeque},
34 sync::{Arc, Condvar, Mutex, MutexGuard},
35 thread::JoinHandle,
36 time::{Duration, Instant},
37};
38
39use ts_metrics::{L3ExecutorSnapshot, L3OperationSnapshot};
40
41use crate::{
42 keys,
43 layout::{HostLayout, LayoutError, Range},
44 store::{HostBuffer, IoResult, Store},
45};
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct ExecutorConfig {
50 pub max_operations: usize,
52 pub max_keys_per_operation: usize,
54 pub query_workers: usize,
56 pub read_workers: usize,
58 pub write_workers: usize,
60 pub failure_threshold: u32,
62 pub recovery_interval: Duration,
64}
65
66pub type Clock = Arc<dyn Fn() -> f64 + Send + Sync>;
68
69pub fn monotonic_clock() -> Clock {
71 let start = Instant::now();
72 Arc::new(move || start.elapsed().as_secs_f64())
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct StorageKey {
78 pub group: u32,
80 pub content_hash: String,
82 pub page_offset: i32,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum Operation {
89 Query {
91 op_id: u64,
93 keys: Vec<StorageKey>,
95 },
96 Get {
98 op_id: u64,
100 keys: Vec<StorageKey>,
102 host_blocks: Vec<i32>,
104 },
105 Put {
107 op_id: u64,
109 keys: Vec<StorageKey>,
111 host_blocks: Vec<i32>,
113 },
114 Cancel {
116 op_id: u64,
118 },
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct StorageCompletion {
124 pub op_id: u64,
126 pub success: Vec<bool>,
128 pub retired: bool,
130 pub object_failures: Vec<bool>,
132}
133
134#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
136pub struct StoragePressure {
137 pub queued_operations: i64,
139 pub queued_bytes: i64,
141 pub oldest_operation_us: i64,
143}
144
145#[derive(Debug, Clone, Default, PartialEq, Eq)]
147pub struct StoragePoll {
148 pub now_us: i64,
150 pub completions: Vec<StorageCompletion>,
152 pub pressure: StoragePressure,
154}
155
156#[derive(Debug, thiserror::Error)]
158pub enum ExecutorError {
159 #[error(transparent)]
161 Layout(#[from] LayoutError),
162 #[error("invalid L3 job: {0}")]
164 Invalid(&'static str),
165 #[error("the scheduler exceeded the L3 operation credits")]
167 Credits,
168 #[error("L3 operations must retire before the namespace changes")]
170 Busy,
171 #[error("L3 I/O retirement is unproven; the Host pool is retained; restart required")]
173 Unretired,
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177enum Kind {
178 Query,
179 Get,
180 Put,
181}
182
183const KINDS: [Kind; 3] = [Kind::Query, Kind::Get, Kind::Put];
184
185impl Kind {
186 fn index(self) -> usize {
187 self as usize
188 }
189
190 fn name(self) -> &'static str {
191 match self {
192 Kind::Query => "query",
193 Kind::Get => "get",
194 Kind::Put => "put",
195 }
196 }
197}
198
199#[derive(Debug, Clone)]
202struct Job {
203 op_id: u64,
204 kind: Kind,
205 keys: Vec<String>,
206 ranges: Vec<Option<Range>>,
207}
208
209impl Job {
210 fn local(&self) -> Vec<usize> {
212 match self.kind {
213 Kind::Query => (0..self.keys.len()).collect(),
214 Kind::Get | Kind::Put => (0..self.keys.len())
215 .filter(|&i| self.ranges[i].is_some())
216 .collect(),
217 }
218 }
219
220 fn num_bytes(&self) -> usize {
221 self.ranges.iter().flatten().map(|range| range.size).sum()
222 }
223}
224
225#[derive(Debug)]
226struct JobState {
227 job: Arc<Job>,
228 submitted_at: f64,
229 num_bytes: usize,
230 started_at: Option<f64>,
231 cancelled: bool,
232 completed: bool,
233}
234
235#[derive(Debug, Default, Clone, Copy)]
236struct Counters {
237 accepted: u64,
238 backend_calls: u64,
239 completed: u64,
240 cancelled_queued: u64,
241 cancelled_running: u64,
242 rejected: u64,
243 unretired: u64,
244 success_keys: u64,
245 failed_keys: u64,
246 success_bytes: u64,
247 queue_seconds: f64,
248 backend_seconds: f64,
249}
250
251#[derive(Debug, Default)]
252struct State {
253 jobs: HashMap<u64, JobState>,
254 results: VecDeque<StorageCompletion>,
255 queues: [VecDeque<u64>; 3],
256 counters: [Counters; 3],
257 stopping: bool,
258 unsafe_io: bool,
259 last_submitted: u64,
260 running_workers: usize,
262}
263
264#[derive(Debug, Clone, Copy)]
266struct HealthToken {
267 epoch: u64,
268 recovery: bool,
269}
270
271#[derive(Debug, Default)]
272struct HealthState {
273 epoch: u64,
274 failures: u32,
275 unavailable_until: Option<f64>,
276 probe_inflight: bool,
277}
278
279struct Health {
283 threshold: u32,
284 interval: f64,
285 clock: Clock,
286 state: Mutex<HealthState>,
287}
288
289impl Health {
290 fn lock(&self) -> MutexGuard<'_, HealthState> {
291 self.state.lock().expect("L3 health lock")
292 }
293
294 fn begin(&self) -> Option<HealthToken> {
295 let mut state = self.lock();
296 match state.unavailable_until {
297 None => Some(HealthToken {
298 epoch: state.epoch,
299 recovery: false,
300 }),
301 Some(until) if (self.clock)() < until || state.probe_inflight => None,
302 Some(_) => {
303 state.probe_inflight = true;
304 Some(HealthToken {
305 epoch: state.epoch,
306 recovery: true,
307 })
308 }
309 }
310 }
311
312 fn failure(&self, token: HealthToken) {
313 let mut state = self.lock();
314 if token.epoch != state.epoch {
315 return;
316 }
317 state.failures += 1;
318 if token.recovery || state.failures >= self.threshold {
319 state.epoch += 1;
320 state.unavailable_until = Some((self.clock)() + self.interval);
321 state.probe_inflight = false;
322 }
323 }
324
325 fn success(&self, token: HealthToken) {
326 let mut state = self.lock();
327 if token.epoch != state.epoch {
328 return;
329 }
330 state.failures = 0;
331 if token.recovery {
332 state.epoch += 1;
333 state.unavailable_until = None;
334 state.probe_inflight = false;
335 }
336 }
337
338 fn snapshot(&self) -> (&'static str, u64) {
341 let state = self.lock();
342 let name = match (state.unavailable_until, state.probe_inflight) {
343 (None, _) => "healthy",
344 (Some(_), true) => "half_open",
345 (Some(_), false) => "open",
346 };
347 (name, state.epoch)
348 }
349}
350
351#[derive(Debug, Clone, PartialEq, Eq)]
354pub struct Namespace {
355 pub deployment_id: String,
357 pub has_draft: bool,
360}
361
362impl Namespace {
363 fn prefix(&self, weight_version: &str) -> String {
364 let draft = if self.has_draft { weight_version } else { "" };
365 keys::key_prefix(&self.deployment_id, weight_version, draft)
366 }
367}
368
369struct Shared {
370 store: Box<dyn Store>,
371 host: HostBuffer,
372 layout: HostLayout,
373 namespace: Namespace,
374 config: ExecutorConfig,
375 clock: Clock,
376 health: Health,
377 state: Mutex<State>,
378 work: [Condvar; 3],
380 exited: Condvar,
382 prefix: Mutex<String>,
383}
384
385impl Shared {
386 fn lock(&self) -> MutexGuard<'_, State> {
387 self.state.lock().expect("L3 executor lock")
388 }
389
390 fn finish(&self, state: &mut State, op_id: u64, result: IoResult) {
392 let now = (self.clock)();
393 let Some(job_state) = state.jobs.get_mut(&op_id) else {
394 return;
395 };
396 job_state.completed = true;
397 let job = Arc::clone(&job_state.job);
398 let queued_until_now = job_state
399 .started_at
400 .is_none()
401 .then_some(now - job_state.submitted_at);
402 let counters = &mut state.counters[job.kind.index()];
403 counters.completed += 1;
404 for position in job.local() {
405 if result.success[position] {
406 counters.success_keys += 1;
407 if let Some(range) = job.ranges.get(position).copied().flatten() {
408 counters.success_bytes += range.size as u64;
409 }
410 } else {
411 counters.failed_keys += 1;
412 }
413 }
414 if let Some(queued) = queued_until_now {
415 counters.queue_seconds += queued;
416 }
417 if !result.retired {
418 counters.unretired += 1;
419 state.unsafe_io = true;
420 }
421 state.results.push_back(StorageCompletion {
422 op_id,
423 success: result.success,
424 retired: result.retired,
425 object_failures: result.object_failures,
426 });
427 }
428
429 fn rejected(keys: usize) -> IoResult {
431 IoResult {
432 success: vec![false; keys],
433 retired: true,
434 object_failures: vec![false; keys],
435 }
436 }
437
438 fn submit(&self, job: Job) -> Result<(), ExecutorError> {
439 if job.keys.is_empty() || job.keys.len() > self.config.max_keys_per_operation {
440 return Err(ExecutorError::Invalid("its key count"));
441 }
442 let mut state = self.lock();
443 if job.op_id <= state.last_submitted {
444 return Err(ExecutorError::Invalid("its operation id does not increase"));
445 }
446 state.last_submitted = job.op_id;
447 if state.jobs.len() >= self.config.max_operations {
448 return Err(ExecutorError::Credits);
451 }
452 let (op_id, kind, num_keys) = (job.op_id, job.kind, job.keys.len());
453 let num_bytes = job.num_bytes();
454 state.counters[kind.index()].accepted += 1;
455 state.jobs.insert(
456 op_id,
457 JobState {
458 job: Arc::new(job),
459 submitted_at: (self.clock)(),
460 num_bytes,
461 started_at: None,
462 cancelled: false,
463 completed: false,
464 },
465 );
466 let queue = &state.queues[kind.index()];
467 if state.stopping || state.unsafe_io || queue.len() >= self.config.max_operations {
468 state.counters[kind.index()].rejected += 1;
469 self.finish(&mut state, op_id, Self::rejected(num_keys));
470 return Ok(());
471 }
472 state.queues[kind.index()].push_back(op_id);
473 drop(state);
474 self.work[kind.index()].notify_one();
475 Ok(())
476 }
477
478 fn cancel(&self, op_id: u64) {
479 let mut state = self.lock();
480 let Some(job_state) = state.jobs.get_mut(&op_id) else {
481 return;
482 };
483 if job_state.completed || job_state.cancelled {
484 return;
485 }
486 job_state.cancelled = true;
487 let started = job_state.started_at.is_some();
488 let (kind, keys) = (job_state.job.kind, job_state.job.keys.len());
489 let counters = &mut state.counters[kind.index()];
490 if started {
491 counters.cancelled_running += 1;
492 } else {
493 counters.cancelled_queued += 1;
494 self.finish(&mut state, op_id, Self::rejected(keys));
498 }
499 }
500
501 fn work(&self, kind: Kind) {
503 loop {
504 let mut state = self.lock();
505 let op_id = loop {
506 if let Some(op_id) = state.queues[kind.index()].pop_front() {
507 break Some(op_id);
508 }
509 if state.stopping {
510 break None;
511 }
512 state = self.work[kind.index()]
513 .wait(state)
514 .expect("L3 executor lock");
515 };
516 let Some(op_id) = op_id else {
517 state.running_workers -= 1;
518 drop(state);
519 self.exited.notify_all();
520 return;
521 };
522 let (stopping, unsafe_io) = (state.stopping, state.unsafe_io);
523 let Some(job_state) = state.jobs.get_mut(&op_id) else {
524 continue;
525 };
526 if job_state.completed {
527 continue;
528 }
529 let job = Arc::clone(&job_state.job);
530 if job_state.cancelled || stopping || unsafe_io {
531 state.counters[kind.index()].rejected += 1;
532 self.finish(&mut state, op_id, Self::rejected(job.keys.len()));
533 continue;
534 }
535 let now = (self.clock)();
536 let queued = now - job_state.submitted_at;
537 job_state.started_at = Some(now);
538 state.counters[kind.index()].queue_seconds += queued;
539 drop(state);
540 let result = self.run(&job);
541 let mut state = self.lock();
542 self.finish(&mut state, op_id, result);
543 }
544 }
545
546 fn run(&self, job: &Job) -> IoResult {
548 let local = job.local();
549 let keys = job.keys.len();
550 if local.is_empty() {
551 return IoResult {
554 success: vec![true; keys],
555 retired: true,
556 object_failures: vec![false; keys],
557 };
558 }
559 let local_keys: Vec<String> = local.iter().map(|&i| job.keys[i].clone()).collect();
560 let local_ranges: Vec<Range> = match job.kind {
561 Kind::Query => Vec::new(),
562 Kind::Get | Kind::Put => local
563 .iter()
564 .map(|&i| job.ranges[i].expect("a local position has a range"))
565 .collect(),
566 };
567 let result = match self.health.begin() {
568 None => {
569 self.lock().counters[job.kind.index()].rejected += 1;
570 Self::rejected(local.len())
571 }
572 Some(token) => {
573 self.lock().counters[job.kind.index()].backend_calls += 1;
574 let started = (self.clock)();
575 let called = match job.kind {
576 Kind::Query => self.store.batch_exists(&local_keys).map(|flags| IoResult {
577 object_failures: vec![false; flags.len()],
578 success: flags,
579 retired: true,
580 }),
581 Kind::Get => self
582 .store
583 .batch_get_into(&local_keys, self.host, &local_ranges),
584 Kind::Put => self
585 .store
586 .batch_put_from(&local_keys, self.host, &local_ranges),
587 };
588 let checked = called.map_err(|error| error.0).and_then(|result| {
589 let valid = result.success.len() == local.len()
590 && result.object_failures.len() == local.len()
591 && !result
592 .success
593 .iter()
594 .zip(&result.object_failures)
595 .any(|(&ok, &failed)| ok && failed)
596 && (job.kind == Kind::Get || !result.object_failures.contains(&true));
597 if valid {
598 Ok(result)
599 } else {
600 Err("malformed L3 result".to_owned())
601 }
602 });
603 let result = match checked {
604 Ok(result) => {
605 if job.kind == Kind::Query || result.retired {
608 self.health.success(token);
609 } else {
610 self.health.failure(token);
611 }
612 result
613 }
614 Err(error) => {
615 tracing::error!(
616 kind = job.kind.name(),
617 op_id = job.op_id,
618 %error,
619 "L3 operation failed"
620 );
621 self.health.failure(token);
622 IoResult {
623 success: vec![false; local.len()],
624 retired: job.kind == Kind::Query || self.store.failures_retire(),
625 object_failures: vec![false; local.len()],
626 }
627 }
628 };
629 self.lock().counters[job.kind.index()].backend_seconds += (self.clock)() - started;
630 result
631 }
632 };
633 let mut success = vec![true; keys];
635 let mut object_failures = vec![false; keys];
636 for (k, &position) in local.iter().enumerate() {
637 success[position] = result.success[k];
638 object_failures[position] = result.object_failures[k];
639 }
640 IoResult {
641 success,
642 retired: result.retired,
643 object_failures,
644 }
645 }
646}
647
648pub struct Executor {
651 shared: Arc<Shared>,
652 threads: Vec<JoinHandle<()>>,
653 closed: bool,
654}
655
656impl Executor {
657 pub fn start(
661 store: Box<dyn Store>,
662 host: HostBuffer,
663 layout: HostLayout,
664 namespace: Namespace,
665 weight_version: &str,
666 config: ExecutorConfig,
667 clock: Clock,
668 ) -> Self {
669 assert_eq!(
670 host.len,
671 layout.bytes(),
672 "the Host pool and its layout disagree"
673 );
674 let prefix = namespace.prefix(weight_version);
675 let workers = config.query_workers + config.read_workers + config.write_workers;
676 let shared = Arc::new(Shared {
677 store,
678 host,
679 layout,
680 namespace,
681 config,
682 health: Health {
683 threshold: config.failure_threshold,
684 interval: config.recovery_interval.as_secs_f64(),
685 clock: Arc::clone(&clock),
686 state: Mutex::new(HealthState::default()),
687 },
688 clock,
689 state: Mutex::new(State {
690 running_workers: workers,
691 ..State::default()
692 }),
693 work: [Condvar::new(), Condvar::new(), Condvar::new()],
694 exited: Condvar::new(),
695 prefix: Mutex::new(prefix),
696 });
697 let mut threads = Vec::with_capacity(workers);
698 for (kind, count) in [
699 (Kind::Query, config.query_workers),
700 (Kind::Get, config.read_workers),
701 (Kind::Put, config.write_workers),
702 ] {
703 for index in 0..count {
704 let shared = Arc::clone(&shared);
705 threads.push(
706 std::thread::Builder::new()
707 .name(format!("l3-{}-{index}", kind.name()))
708 .spawn(move || shared.work(kind))
709 .expect("spawning an L3 worker"),
710 );
711 }
712 }
713 Self {
714 shared,
715 threads,
716 closed: false,
717 }
718 }
719
720 pub fn config(&self) -> ExecutorConfig {
722 self.shared.config
723 }
724
725 pub fn key_prefix(&self) -> String {
727 self.shared.prefix.lock().expect("L3 prefix lock").clone()
728 }
729
730 fn object_keys(&self, keys: &[StorageKey]) -> Result<Vec<String>, ExecutorError> {
731 let prefix = self.key_prefix();
732 keys.iter()
733 .map(|key| {
734 Ok(keys::object_key(
735 &prefix,
736 &key.content_hash,
737 key.group,
738 key.page_offset,
739 self.shared.layout.key_rank(key.group)?,
740 ))
741 })
742 .collect()
743 }
744
745 fn ranged_job(
749 &self,
750 op_id: u64,
751 kind: Kind,
752 keys: &[StorageKey],
753 host_blocks: &[i32],
754 ) -> Result<Job, ExecutorError> {
755 if keys.len() != host_blocks.len() {
756 return Err(ExecutorError::Invalid("a Host block per key"));
757 }
758 let ranges = keys
759 .iter()
760 .zip(host_blocks)
761 .map(|(key, &block)| self.shared.layout.owned_range(key.group, block))
762 .collect::<Result<Vec<_>, _>>()?;
763 Ok(Job {
764 op_id,
765 kind,
766 keys: self.object_keys(keys)?,
767 ranges,
768 })
769 }
770
771 pub fn submit(&self, operations: &[Operation]) -> Result<(), ExecutorError> {
773 for operation in operations {
774 let job = match operation {
775 Operation::Cancel { op_id } => {
776 self.shared.cancel(*op_id);
777 continue;
778 }
779 Operation::Query { op_id, keys } => Job {
780 op_id: *op_id,
781 kind: Kind::Query,
782 keys: self.object_keys(keys)?,
783 ranges: Vec::new(),
784 },
785 Operation::Get {
786 op_id,
787 keys,
788 host_blocks,
789 } => self.ranged_job(*op_id, Kind::Get, keys, host_blocks)?,
790 Operation::Put {
791 op_id,
792 keys,
793 host_blocks,
794 } => self.ranged_job(*op_id, Kind::Put, keys, host_blocks)?,
795 };
796 self.shared.submit(job)?;
797 }
798 Ok(())
799 }
800
801 pub fn poll(&self) -> StoragePoll {
805 let mut state = self.shared.lock();
806 let completions: Vec<StorageCompletion> = state.results.drain(..).collect();
807 for completion in &completions {
808 state.jobs.remove(&completion.op_id);
809 }
810 let now = (self.shared.clock)();
811 let mut pressure = StoragePressure::default();
812 let mut oldest = now;
813 for job_state in state.jobs.values().filter(|job| !job.completed) {
814 oldest = oldest.min(job_state.submitted_at);
815 if job_state.started_at.is_none() {
816 pressure.queued_operations += 1;
817 pressure.queued_bytes += job_state.num_bytes as i64;
818 }
819 }
820 pressure.oldest_operation_us = ((now - oldest) * 1_000_000.0).max(0.0) as i64;
821 StoragePoll {
822 now_us: (now * 1_000_000.0) as i64,
823 completions,
824 pressure,
825 }
826 }
827
828 pub fn is_idle(&self) -> bool {
830 let state = self.shared.lock();
831 state.jobs.is_empty() && !state.unsafe_io
832 }
833
834 pub fn snapshot(&self) -> L3ExecutorSnapshot {
837 let state = self.shared.lock();
838 let operations = KINDS
839 .iter()
840 .map(|&kind| {
841 let counters = state.counters[kind.index()];
842 let mut phases = [(0.0, 0.0); 3];
843 for job_state in state.jobs.values().filter(|job| job.job.kind == kind) {
844 let phase = if job_state.completed {
845 2
846 } else if job_state.started_at.is_some() {
847 1
848 } else {
849 0
850 };
851 phases[phase].0 += 1.0;
852 phases[phase].1 += job_state.num_bytes as f64;
853 }
854 let by_phase = |pick: fn(&(f64, f64)) -> f64| -> Vec<(String, f64)> {
855 ["queued", "running", "completed"]
856 .iter()
857 .zip(&phases)
858 .map(|(name, phase)| ((*name).to_owned(), pick(phase)))
859 .collect()
860 };
861 L3OperationSnapshot {
862 kind: kind.name().to_owned(),
863 events: [
864 ("accepted", counters.accepted),
865 ("backend_calls", counters.backend_calls),
866 ("completed", counters.completed),
867 ("cancelled_queued", counters.cancelled_queued),
868 ("cancelled_running", counters.cancelled_running),
869 ("rejected", counters.rejected),
870 ("unretired", counters.unretired),
871 ]
872 .into_iter()
873 .map(|(name, value)| (name.to_owned(), value as f64))
874 .collect(),
875 success_keys: counters.success_keys as f64,
876 failed_keys: counters.failed_keys as f64,
877 success_bytes: counters.success_bytes as f64,
878 queue_seconds: counters.queue_seconds,
879 backend_seconds: counters.backend_seconds,
880 operations: by_phase(|phase| phase.0),
881 bytes: by_phase(|phase| phase.1),
882 }
883 })
884 .collect();
885 drop(state);
886 let (health_state, health_epoch) = self.shared.health.snapshot();
887 L3ExecutorSnapshot {
888 operations,
889 health_state: health_state.to_owned(),
890 health_epoch: health_epoch as f64,
891 unsafe_io: self.shared.lock().unsafe_io,
892 }
893 }
894
895 pub fn set_weight_version(&self, weight_version: &str) -> Result<(), ExecutorError> {
898 if !self.is_idle() {
899 return Err(ExecutorError::Busy);
900 }
901 *self.shared.prefix.lock().expect("L3 prefix lock") =
902 self.shared.namespace.prefix(weight_version);
903 Ok(())
904 }
905
906 pub fn delete_namespace(&self) -> bool {
908 if !self.is_idle() {
909 return false;
910 }
911 let prefix = format!("{}_", self.key_prefix());
912 self.shared.store.remove_by_prefix(&prefix)
913 }
914
915 pub fn close(&mut self, timeout: Duration) -> Result<(), ExecutorError> {
920 if self.closed {
921 return Ok(());
922 }
923 self.closed = true;
924 let deadline = Instant::now() + timeout;
925 let mut state = self.shared.lock();
926 state.stopping = true;
927 for work in &self.shared.work {
928 work.notify_all();
929 }
930 while state.running_workers > 0 {
931 let left = deadline.saturating_duration_since(Instant::now());
932 if left.is_zero() {
933 break;
934 }
935 state = self
936 .shared
937 .exited
938 .wait_timeout(state, left)
939 .expect("L3 executor lock")
940 .0;
941 }
942 let retired = state.running_workers == 0 && !state.unsafe_io;
943 drop(state);
944 if !retired {
945 std::mem::forget(Arc::clone(&self.shared));
948 return Err(ExecutorError::Unretired);
949 }
950 for thread in self.threads.drain(..) {
951 let _ = thread.join();
952 }
953 self.shared.store.close();
954 Ok(())
955 }
956}
957
958impl Drop for Executor {
959 fn drop(&mut self) {
960 if let Err(error) = self.close(Duration::from_secs(5)) {
961 tracing::error!(%error, "closing the L3 executor");
962 }
963 }
964}
965
966#[cfg(test)]
967mod tests {
968 use std::sync::atomic::{AtomicUsize, Ordering};
969
970 use super::*;
971 use crate::{layout::GroupLayout, store::MemoryStore, store::StoreError};
972
973 #[derive(Default)]
975 struct Event {
976 set: Mutex<bool>,
977 changed: Condvar,
978 }
979
980 impl Event {
981 fn set(&self) {
982 *self.set.lock().unwrap() = true;
983 self.changed.notify_all();
984 }
985
986 fn wait(&self, timeout: Duration) -> bool {
987 let guard = self.set.lock().unwrap();
988 *self
989 .changed
990 .wait_timeout_while(guard, timeout, |set| !*set)
991 .unwrap()
992 .0
993 }
994 }
995
996 type ExistsFn = dyn Fn(&[String]) -> Result<Vec<bool>, StoreError> + Send + Sync;
997 type IoFn =
998 dyn Fn(&[String], HostBuffer, &[Range]) -> Result<IoResult, StoreError> + Send + Sync;
999 type Calls = Arc<Mutex<Vec<(&'static str, Vec<String>)>>>;
1001
1002 #[derive(Default)]
1005 struct TestStore {
1006 memory: Arc<MemoryStore>,
1007 exists: Option<Box<ExistsFn>>,
1008 get: Option<Box<IoFn>>,
1009 put: Option<Box<IoFn>>,
1010 remove_fails: bool,
1012 failures_retire: bool,
1013 calls: Calls,
1014 closes: Arc<AtomicUsize>,
1015 }
1016
1017 impl TestStore {
1018 fn new() -> Self {
1019 Self {
1020 failures_retire: true,
1021 ..Self::default()
1022 }
1023 }
1024 }
1025
1026 impl Store for TestStore {
1027 fn failures_retire(&self) -> bool {
1028 self.failures_retire
1029 }
1030
1031 fn batch_exists(&self, keys: &[String]) -> Result<Vec<bool>, StoreError> {
1032 match &self.exists {
1033 Some(exists) => exists(keys),
1034 None => self.memory.batch_exists(keys),
1035 }
1036 }
1037
1038 fn batch_get_into(
1039 &self,
1040 keys: &[String],
1041 host: HostBuffer,
1042 ranges: &[Range],
1043 ) -> Result<IoResult, StoreError> {
1044 self.calls.lock().unwrap().push(("get", keys.to_vec()));
1045 match &self.get {
1046 Some(get) => get(keys, host, ranges),
1047 None => self.memory.batch_get_into(keys, host, ranges),
1048 }
1049 }
1050
1051 fn batch_put_from(
1052 &self,
1053 keys: &[String],
1054 host: HostBuffer,
1055 ranges: &[Range],
1056 ) -> Result<IoResult, StoreError> {
1057 self.calls.lock().unwrap().push(("put", keys.to_vec()));
1058 match &self.put {
1059 Some(put) => put(keys, host, ranges),
1060 None => self.memory.batch_put_from(keys, host, ranges),
1061 }
1062 }
1063
1064 fn remove_by_prefix(&self, prefix: &str) -> bool {
1065 !self.remove_fails && self.memory.remove_by_prefix(prefix)
1066 }
1067
1068 fn close(&self) {
1069 self.closes.fetch_add(1, Ordering::SeqCst);
1070 }
1071 }
1072
1073 const CONFIG: ExecutorConfig = ExecutorConfig {
1074 max_operations: 32,
1075 max_keys_per_operation: 256,
1076 query_workers: 2,
1077 read_workers: 2,
1078 write_workers: 1,
1079 failure_threshold: 1,
1080 recovery_interval: Duration::from_secs(30),
1081 };
1082
1083 fn layout(shard_count: usize, owner_rank: usize, tp_rank: usize) -> HostLayout {
1087 HostLayout {
1088 groups: vec![
1089 GroupLayout {
1090 packing: 1,
1091 block_bytes: 8,
1092 shard_count,
1093 },
1094 GroupLayout {
1095 packing: 1,
1096 block_bytes: 16,
1097 shard_count: 1,
1098 },
1099 ],
1100 lcm_block_bytes: 24,
1101 num_lcm_blocks: 4,
1102 owner_rank,
1103 tp_rank,
1104 }
1105 }
1106
1107 struct Harness {
1109 executor: Option<Executor>,
1110 pool: Vec<u8>,
1111 }
1112
1113 impl Harness {
1114 fn start(
1115 store: TestStore,
1116 config: ExecutorConfig,
1117 layout: HostLayout,
1118 clock: Clock,
1119 ) -> Self {
1120 let mut pool = vec![0_u8; layout.bytes()];
1121 let host = HostBuffer {
1122 base: pool.as_mut_ptr() as usize,
1123 len: pool.len(),
1124 };
1125 let executor = Executor::start(
1126 Box::new(store),
1127 host,
1128 layout,
1129 Namespace {
1130 deployment_id: "deployment".to_owned(),
1131 has_draft: false,
1132 },
1133 "v1",
1134 config,
1135 clock,
1136 );
1137 Self {
1138 executor: Some(executor),
1139 pool,
1140 }
1141 }
1142
1143 fn new(store: TestStore, config: ExecutorConfig) -> Self {
1144 Self::start(store, config, layout(1, 0, 0), monotonic_clock())
1145 }
1146
1147 fn executor(&self) -> &Executor {
1148 self.executor.as_ref().unwrap()
1149 }
1150
1151 fn wait(&self, count: usize) -> Vec<StorageCompletion> {
1153 let deadline = Instant::now() + Duration::from_secs(3);
1154 let mut completions = Vec::new();
1155 while completions.len() < count {
1156 assert!(Instant::now() < deadline, "{completions:?}");
1157 completions.extend(self.executor().poll().completions);
1158 std::thread::sleep(Duration::from_millis(1));
1159 }
1160 assert_eq!(completions.len(), count, "{completions:?}");
1161 completions
1162 }
1163
1164 fn close(&mut self, timeout: Duration) -> Result<(), ExecutorError> {
1165 match self.executor.take() {
1166 Some(mut executor) => executor.close(timeout),
1167 None => Ok(()),
1168 }
1169 }
1170 }
1171
1172 impl Drop for Harness {
1173 fn drop(&mut self) {
1174 let _ = self.close(Duration::from_secs(2));
1177 }
1178 }
1179
1180 fn key(hash: &str) -> StorageKey {
1181 StorageKey {
1182 group: 0,
1183 content_hash: hash.to_owned(),
1184 page_offset: 0,
1185 }
1186 }
1187
1188 fn query(op_id: u64, hashes: &[&str]) -> Operation {
1189 Operation::Query {
1190 op_id,
1191 keys: hashes.iter().map(|hash| key(hash)).collect(),
1192 }
1193 }
1194
1195 fn get(op_id: u64, hash: &str, block: i32) -> Operation {
1196 Operation::Get {
1197 op_id,
1198 keys: vec![key(hash)],
1199 host_blocks: vec![block],
1200 }
1201 }
1202
1203 fn put(op_id: u64, hash: &str, block: i32) -> Operation {
1204 Operation::Put {
1205 op_id,
1206 keys: vec![key(hash)],
1207 host_blocks: vec![block],
1208 }
1209 }
1210
1211 fn row(snapshot: &L3ExecutorSnapshot, kind: &str) -> L3OperationSnapshot {
1212 snapshot
1213 .operations
1214 .iter()
1215 .find(|row| row.kind == kind)
1216 .cloned()
1217 .unwrap()
1218 }
1219
1220 fn value(pairs: &[(String, f64)], name: &str) -> f64 {
1221 pairs.iter().find(|(key, _)| key == name).unwrap().1
1222 }
1223
1224 fn fake_clock(now: &Arc<Mutex<f64>>) -> Clock {
1225 let now = Arc::clone(now);
1226 Arc::new(move || *now.lock().unwrap())
1227 }
1228
1229 #[test]
1230 fn stale_reports_cannot_reopen_a_failed_recovery() {
1231 let now = Arc::new(Mutex::new(1.0));
1232 let health = Health {
1233 threshold: 1,
1234 interval: 10.0,
1235 clock: fake_clock(&now),
1236 state: Mutex::default(),
1237 };
1238 let old = health.begin().unwrap();
1239 let failing = health.begin().unwrap();
1240 health.failure(failing);
1241 health.success(old);
1242 assert!(health.begin().is_none());
1243 *now.lock().unwrap() = 12.0;
1244 let probe = health.begin().unwrap();
1245 assert!(probe.recovery);
1246 assert!(health.begin().is_none());
1247 health.failure(probe);
1248 health.success(probe);
1250 assert!(health.begin().is_none());
1251 assert_eq!(health.snapshot().0, "open");
1252 *now.lock().unwrap() = 23.0;
1253 let recovered = health.begin().unwrap();
1254 assert!(recovered.recovery && health.begin().is_none());
1255 health.success(recovered);
1256 assert!(!health.begin().unwrap().recovery);
1257 }
1258
1259 #[test]
1260 fn a_snapshot_cannot_take_the_recovery_probe() {
1261 let now = Arc::new(Mutex::new(0.0));
1262 let health = Health {
1263 threshold: 1,
1264 interval: 1.0,
1265 clock: fake_clock(&now),
1266 state: Mutex::default(),
1267 };
1268 health.failure(health.begin().unwrap());
1269 *now.lock().unwrap() = 2.0;
1270 for _ in 0..3 {
1271 assert_eq!(health.snapshot().0, "open");
1272 }
1273 let probe = health.begin().unwrap();
1274 assert!(probe.recovery);
1275 assert_eq!(health.snapshot().0, "half_open");
1276 health.success(probe);
1277 assert_eq!(health.snapshot().0, "healthy");
1278 }
1279
1280 #[test]
1281 fn a_slow_query_blocks_neither_another_worker_nor_the_submitter() {
1282 let (started, release) = (Arc::new(Event::default()), Arc::new(Event::default()));
1283 let (s, r) = (Arc::clone(&started), Arc::clone(&release));
1284 let slow = keys::object_key(&keys::key_prefix("deployment", "v1", ""), "slow", 0, 0, 0);
1285 let store = TestStore {
1286 exists: Some(Box::new(move |keys| {
1287 if keys == [slow.clone()] {
1288 s.set();
1289 assert!(r.wait(Duration::from_secs(3)));
1290 }
1291 Ok(vec![true; keys.len()])
1292 })),
1293 ..TestStore::new()
1294 };
1295 let harness = Harness::new(store, CONFIG);
1296 harness.executor().submit(&[query(1, &["slow"])]).unwrap();
1297 assert!(started.wait(Duration::from_secs(1)));
1298 harness.executor().submit(&[query(2, &["fast"])]).unwrap();
1299 let fast = &harness.wait(1)[0];
1300 assert_eq!((fast.op_id, fast.success.as_slice()), (2, &[true][..]));
1301 release.set();
1302 assert_eq!(harness.wait(1)[0].op_id, 1);
1303 }
1304
1305 #[test]
1306 fn a_cancelled_queued_get_never_reaches_the_store() {
1307 let (started, release) = (Arc::new(Event::default()), Arc::new(Event::default()));
1308 let (s, r) = (Arc::clone(&started), Arc::clone(&release));
1309 let store = TestStore {
1310 get: Some(Box::new(move |keys, _, _| {
1311 s.set();
1312 assert!(r.wait(Duration::from_secs(3)));
1313 Ok(IoResult {
1314 success: vec![true; keys.len()],
1315 retired: true,
1316 object_failures: vec![false; keys.len()],
1317 })
1318 })),
1319 ..TestStore::new()
1320 };
1321 let calls = Arc::clone(&store.calls);
1322 let harness = Harness::new(
1323 store,
1324 ExecutorConfig {
1325 read_workers: 1,
1326 ..CONFIG
1327 },
1328 );
1329 harness.executor().submit(&[get(1, "a", 1)]).unwrap();
1330 assert!(started.wait(Duration::from_secs(1)));
1331 harness
1332 .executor()
1333 .submit(&[get(2, "b", 2), Operation::Cancel { op_id: 2 }])
1334 .unwrap();
1335 let cancelled = &harness.wait(1)[0];
1336 assert_eq!(cancelled.op_id, 2);
1337 assert!(cancelled.retired);
1338 assert_eq!(cancelled.success, [false]);
1339 assert_eq!(cancelled.object_failures, [false]);
1340 assert_eq!(calls.lock().unwrap().len(), 1);
1342 release.set();
1343 assert_eq!(harness.wait(1)[0].op_id, 1);
1344 assert_eq!(calls.lock().unwrap().len(), 1);
1345 }
1346
1347 #[test]
1348 fn a_failing_query_degrades_without_killing_its_worker() {
1349 let store = TestStore {
1350 exists: Some(Box::new(|_| Err(StoreError("unavailable".to_owned())))),
1351 ..TestStore::new()
1352 };
1353 let harness = Harness::new(store, CONFIG);
1354 harness.executor().submit(&[query(1, &["a"])]).unwrap();
1355 let failed = &harness.wait(1)[0];
1356 assert!(failed.retired);
1357 assert_eq!(failed.success, [false]);
1358 harness.executor().submit(&[query(2, &["b"])]).unwrap();
1359 assert_eq!(harness.wait(1)[0].op_id, 2);
1360 }
1361
1362 #[test]
1363 fn unproven_retirement_disables_io_and_refuses_a_clean_close() {
1364 for success in [false, true] {
1365 let store = TestStore {
1366 get: Some(Box::new(move |_, _, _| {
1367 Ok(IoResult {
1368 success: vec![success],
1369 retired: false,
1370 object_failures: vec![false],
1371 })
1372 })),
1373 ..TestStore::new()
1374 };
1375 let closes = Arc::clone(&store.closes);
1376 let mut harness = Harness::new(store, CONFIG);
1377 harness.executor().submit(&[get(1, "a", 1)]).unwrap();
1378 assert!(!harness.wait(1)[0].retired);
1379 assert!(!harness.executor().is_idle());
1380 assert_eq!(harness.executor().snapshot().health_state, "open");
1381 harness.executor().submit(&[get(2, "b", 2)]).unwrap();
1382 let refused = &harness.wait(1)[0];
1384 assert!(refused.retired);
1385 assert_eq!(refused.success, [false]);
1386 assert!(matches!(
1387 harness.close(Duration::from_secs(2)),
1388 Err(ExecutorError::Unretired)
1389 ));
1390 assert_eq!(closes.load(Ordering::SeqCst), 0);
1392 std::mem::forget(std::mem::take(&mut harness.pool));
1394 }
1395 }
1396
1397 #[test]
1398 fn independent_executors_share_objects_through_the_store() {
1399 let memory = Arc::new(MemoryStore::new());
1400 let writer = Harness::new(
1401 TestStore {
1402 memory: Arc::clone(&memory),
1403 ..TestStore::new()
1404 },
1405 CONFIG,
1406 );
1407 let reader = Harness::new(
1408 TestStore {
1409 memory: Arc::clone(&memory),
1410 ..TestStore::new()
1411 },
1412 CONFIG,
1413 );
1414 let pool = writer.pool.as_ptr() as *mut u8;
1416 #[allow(unsafe_code)]
1418 unsafe {
1419 std::ptr::copy_nonoverlapping(b"abcdefgh".as_ptr(), pool, 8);
1420 }
1421 writer.executor().submit(&[put(1, "shared", 1)]).unwrap();
1422 assert_eq!(writer.wait(1)[0].success, [true]);
1423 reader.executor().submit(&[get(1, "shared", 3)]).unwrap();
1425 assert_eq!(reader.wait(1)[0].success, [true]);
1426 assert_eq!(&reader.pool[48..56], b"abcdefgh");
1427 assert_eq!(&reader.pool[..8], &[0; 8]);
1428 }
1429
1430 #[test]
1431 fn operation_credits_bound_what_is_in_flight() {
1432 let harness = Harness::new(
1433 TestStore::new(),
1434 ExecutorConfig {
1435 max_operations: 1,
1436 ..CONFIG
1437 },
1438 );
1439 harness.executor().submit(&[query(1, &["a"])]).unwrap();
1440 std::thread::sleep(Duration::from_millis(20));
1442 assert!(matches!(
1443 harness.executor().submit(&[query(2, &["b"])]),
1444 Err(ExecutorError::Credits)
1445 ));
1446 harness.wait(1);
1447 }
1448
1449 #[test]
1450 fn the_key_limit_and_increasing_ids_are_enforced() {
1451 let harness = Harness::new(
1452 TestStore::new(),
1453 ExecutorConfig {
1454 max_keys_per_operation: 2,
1455 ..CONFIG
1456 },
1457 );
1458 assert!(matches!(
1459 harness.executor().submit(&[query(1, &["a", "b", "c"])]),
1460 Err(ExecutorError::Invalid(_))
1461 ));
1462 harness.executor().submit(&[query(1, &["a", "b"])]).unwrap();
1463 assert_eq!(harness.wait(1)[0].success, [false, false]);
1464 assert!(matches!(
1465 harness.executor().submit(&[query(1, &["a"])]),
1466 Err(ExecutorError::Invalid(_))
1467 ));
1468 }
1469
1470 #[test]
1471 fn a_retired_get_miss_is_an_object_failure_not_a_backend_fault() {
1472 let harness = Harness::new(TestStore::new(), CONFIG);
1473 harness.executor().submit(&[get(1, "evicted", 1)]).unwrap();
1474 let miss = &harness.wait(1)[0];
1475 assert!(miss.retired);
1476 assert_eq!(miss.success, [false]);
1477 assert_eq!(miss.object_failures, [true]);
1478 assert_eq!(harness.executor().snapshot().health_state, "healthy");
1479 }
1480
1481 #[test]
1482 fn snapshots_count_cancelled_queued_work_and_late_completions() {
1483 let (started, release) = (Arc::new(Event::default()), Arc::new(Event::default()));
1484 let (s, r) = (Arc::clone(&started), Arc::clone(&release));
1485 let store = TestStore {
1486 get: Some(Box::new(move |keys, _, _| {
1487 s.set();
1488 assert!(r.wait(Duration::from_secs(3)));
1489 Ok(IoResult {
1490 success: vec![true; keys.len()],
1491 retired: true,
1492 object_failures: vec![false; keys.len()],
1493 })
1494 })),
1495 ..TestStore::new()
1496 };
1497 let harness = Harness::new(
1498 store,
1499 ExecutorConfig {
1500 read_workers: 1,
1501 ..CONFIG
1502 },
1503 );
1504 let executor = harness.executor();
1505 executor.submit(&[get(1, "running", 1)]).unwrap();
1506 assert!(started.wait(Duration::from_secs(1)));
1507 executor
1509 .submit(&[Operation::Get {
1510 op_id: 2,
1511 keys: vec![StorageKey {
1512 group: 1,
1513 content_hash: "queued".to_owned(),
1514 page_offset: 0,
1515 }],
1516 host_blocks: vec![1],
1517 }])
1518 .unwrap();
1519 let snapshot = row(&executor.snapshot(), "get");
1520 assert_eq!(
1521 snapshot.operations,
1522 [
1523 ("queued".to_owned(), 1.0),
1524 ("running".to_owned(), 1.0),
1525 ("completed".to_owned(), 0.0)
1526 ]
1527 );
1528 assert_eq!(value(&snapshot.bytes, "queued"), 16.0);
1529 assert_eq!(value(&snapshot.bytes, "running"), 8.0);
1530 executor
1531 .submit(&[
1532 Operation::Cancel { op_id: 2 },
1533 Operation::Cancel { op_id: 2 },
1534 Operation::Cancel { op_id: 1 },
1535 ])
1536 .unwrap();
1537 harness.wait(1);
1538 let cancelled = row(&executor.snapshot(), "get");
1539 assert_eq!(value(&cancelled.events, "cancelled_queued"), 1.0);
1540 assert_eq!(value(&cancelled.events, "cancelled_running"), 1.0);
1541 assert_eq!(value(&cancelled.events, "backend_calls"), 1.0);
1542 assert_eq!(value(&cancelled.operations, "running"), 1.0);
1543 release.set();
1544 let deadline = Instant::now() + Duration::from_secs(3);
1545 while value(&row(&executor.snapshot(), "get").operations, "completed") != 1.0 {
1546 assert!(Instant::now() < deadline);
1547 std::thread::sleep(Duration::from_millis(1));
1548 }
1549 let completed = row(&executor.snapshot(), "get");
1550 assert_eq!(value(&completed.bytes, "completed"), 8.0);
1551 assert_eq!(completed.success_bytes, 8.0);
1553 assert_eq!((completed.success_keys, completed.failed_keys), (1.0, 1.0));
1554 assert!(completed.queue_seconds >= 0.0 && completed.backend_seconds > 0.0);
1555 harness.wait(1);
1556 let drained = row(&executor.snapshot(), "get");
1557 assert!(drained.operations.iter().all(|(_, count)| *count == 0.0));
1558 }
1559
1560 #[test]
1561 fn an_open_backend_rejects_jobs_visibly_without_calling_it() {
1562 let harness = Harness::new(TestStore::new(), CONFIG);
1563 let executor = harness.executor();
1564 let shared = &executor.shared;
1565 shared.health.failure(shared.health.begin().unwrap());
1566 executor.submit(&[put(1, "rejected", 1)]).unwrap();
1567 assert_eq!(harness.wait(1)[0].success, [false]);
1568 let put_row = row(&executor.snapshot(), "put");
1569 assert_eq!(value(&put_row.events, "rejected"), 1.0);
1570 assert_eq!(value(&put_row.events, "backend_calls"), 0.0);
1571 assert_eq!((put_row.success_bytes, put_row.backend_seconds), (0.0, 0.0));
1572 }
1573
1574 #[test]
1575 fn pressure_tracks_the_queue_and_cancelled_native_work_until_it_retires() {
1576 let now = Arc::new(Mutex::new(1.0));
1577 let (started, release) = (Arc::new(Event::default()), Arc::new(Event::default()));
1578 let (s, r) = (Arc::clone(&started), Arc::clone(&release));
1579 let store = TestStore {
1580 get: Some(Box::new(move |keys, _, _| {
1581 s.set();
1582 assert!(r.wait(Duration::from_secs(5)));
1583 Ok(IoResult {
1584 success: vec![true; keys.len()],
1585 retired: true,
1586 object_failures: vec![false; keys.len()],
1587 })
1588 })),
1589 ..TestStore::new()
1590 };
1591 let harness = Harness::start(
1592 store,
1593 ExecutorConfig {
1594 read_workers: 1,
1595 ..CONFIG
1596 },
1597 layout(1, 0, 0),
1598 fake_clock(&now),
1599 );
1600 let executor = harness.executor();
1601 executor.submit(&[get(1, "active", 1)]).unwrap();
1602 assert!(started.wait(Duration::from_secs(1)));
1603 executor.submit(&[get(2, "queued", 2)]).unwrap();
1604 *now.lock().unwrap() = 1.75;
1605 let poll = executor.poll();
1606 assert!(poll.completions.is_empty());
1607 assert_eq!(
1608 poll.pressure,
1609 StoragePressure {
1610 queued_operations: 1,
1611 queued_bytes: 8,
1612 oldest_operation_us: 750_000
1613 }
1614 );
1615 executor.submit(&[Operation::Cancel { op_id: 2 }]).unwrap();
1616 let poll = executor.poll();
1617 assert_eq!(
1618 poll.completions.iter().map(|c| c.op_id).collect::<Vec<_>>(),
1619 [2]
1620 );
1621 assert_eq!(
1622 poll.pressure,
1623 StoragePressure {
1624 queued_operations: 0,
1625 queued_bytes: 0,
1626 oldest_operation_us: 750_000
1627 }
1628 );
1629 executor.submit(&[Operation::Cancel { op_id: 1 }]).unwrap();
1631 assert_eq!(executor.poll().pressure.oldest_operation_us, 750_000);
1632 assert_eq!(executor.snapshot().health_state, "healthy");
1633 release.set();
1634 assert!(harness.wait(1)[0].retired);
1635 assert_eq!(executor.poll().pressure, StoragePressure::default());
1636 assert!(executor.is_idle());
1637 }
1638
1639 #[test]
1640 fn query_pressure_counts_metadata_jobs_without_payload_bytes() {
1641 let now = Arc::new(Mutex::new(1.0));
1642 let (started, release) = (Arc::new(Event::default()), Arc::new(Event::default()));
1643 let (s, r) = (Arc::clone(&started), Arc::clone(&release));
1644 let store = TestStore {
1645 exists: Some(Box::new(move |keys| {
1646 s.set();
1647 assert!(r.wait(Duration::from_secs(5)));
1648 Ok(vec![false; keys.len()])
1649 })),
1650 ..TestStore::new()
1651 };
1652 let harness = Harness::start(
1653 store,
1654 ExecutorConfig {
1655 query_workers: 1,
1656 ..CONFIG
1657 },
1658 layout(1, 0, 0),
1659 fake_clock(&now),
1660 );
1661 let executor = harness.executor();
1662 executor.submit(&[query(1, &["a"])]).unwrap();
1663 assert!(started.wait(Duration::from_secs(1)));
1664 executor.submit(&[query(2, &["b"])]).unwrap();
1665 *now.lock().unwrap() = 2.0;
1666 assert_eq!(
1667 executor.poll().pressure,
1668 StoragePressure {
1669 queued_operations: 1,
1670 queued_bytes: 0,
1671 oldest_operation_us: 1_000_000
1672 }
1673 );
1674 release.set();
1675 assert!(harness.wait(2).iter().all(|c| c.retired));
1676 assert_eq!(executor.poll().pressure.oldest_operation_us, 0);
1677 }
1678
1679 #[test]
1680 fn a_rejected_or_failed_get_reports_no_object_failure() {
1681 for failure in ["health", "queue", "error"] {
1683 let (started, release) = (Arc::new(Event::default()), Arc::new(Event::default()));
1684 let (s, r) = (Arc::clone(&started), Arc::clone(&release));
1685 let blocking = failure == "queue";
1686 let store = TestStore {
1687 get: (failure != "health").then(|| {
1688 Box::new(move |keys: &[String], _: HostBuffer, _: &[Range]| {
1689 if blocking && keys[0].contains("_blocker") {
1690 s.set();
1691 assert!(r.wait(Duration::from_secs(3)));
1692 }
1693 if blocking {
1694 Ok(IoResult {
1695 success: vec![true; keys.len()],
1696 retired: true,
1697 object_failures: vec![false; keys.len()],
1698 })
1699 } else {
1700 Err(StoreError("unavailable".to_owned()))
1701 }
1702 }) as Box<IoFn>
1703 }),
1704 ..TestStore::new()
1705 };
1706 let harness = Harness::new(
1707 store,
1708 ExecutorConfig {
1709 max_operations: 2,
1710 read_workers: 1,
1711 ..CONFIG
1712 },
1713 );
1714 let executor = harness.executor();
1715 let target = match failure {
1716 "health" => {
1717 let shared = &executor.shared;
1718 shared.health.failure(shared.health.begin().unwrap());
1719 1
1720 }
1721 "queue" => {
1722 executor.submit(&[get(1, "blocker", 1)]).unwrap();
1725 assert!(started.wait(Duration::from_secs(1)));
1726 executor
1727 .submit(&[get(2, "stale", 2), Operation::Cancel { op_id: 2 }])
1728 .unwrap();
1729 harness.wait(1);
1730 executor
1731 .submit(&[get(3, "stale", 3), Operation::Cancel { op_id: 3 }])
1732 .unwrap();
1733 harness.wait(1);
1734 4
1735 }
1736 _ => 1,
1737 };
1738 executor.submit(&[get(target, "present", 4)]).unwrap();
1739 let result = &harness.wait(1)[0];
1740 assert_eq!(result.op_id, target);
1741 assert!(result.retired);
1742 assert_eq!(result.success, [false]);
1743 assert_eq!(result.object_failures, [false]);
1744 if failure != "error" {
1745 let get_row = row(&executor.snapshot(), "get");
1746 let calls = if failure == "queue" { 1.0 } else { 0.0 };
1747 assert_eq!(value(&get_row.events, "backend_calls"), calls);
1748 }
1749 release.set();
1750 }
1751 }
1752
1753 #[test]
1754 fn malformed_facts_cannot_publish() {
1755 for (success, object_failures) in [
1756 (vec![true], vec![]),
1757 (vec![true], vec![true]),
1758 (vec![], vec![false]),
1759 ] {
1760 let store = TestStore {
1761 get: Some(Box::new(move |_, _, _| {
1762 Ok(IoResult {
1763 success: success.clone(),
1764 retired: true,
1765 object_failures: object_failures.clone(),
1766 })
1767 })),
1768 ..TestStore::new()
1769 };
1770 let harness = Harness::new(store, CONFIG);
1771 harness.executor().submit(&[get(1, "a", 1)]).unwrap();
1772 let result = &harness.wait(1)[0];
1773 assert_eq!(result.success, [false]);
1774 assert_eq!(result.object_failures, [false]);
1775 assert_eq!(harness.executor().snapshot().health_state, "open");
1776 }
1777 }
1778
1779 #[test]
1780 fn a_put_never_reports_object_failures() {
1781 let store = TestStore {
1782 get: None,
1783 ..TestStore::new()
1784 };
1785 let harness = Harness::new(store, CONFIG);
1786 harness.executor().submit(&[put(1, "a", 1)]).unwrap();
1787 let result = &harness.wait(1)[0];
1788 assert_eq!(
1789 (result.success.as_slice(), result.retired),
1790 (&[true][..], true)
1791 );
1792 assert_eq!(result.object_failures, [false]);
1793 }
1794
1795 #[test]
1796 fn close_waits_for_native_retirement_then_closes_the_store_once() {
1797 let (started, release) = (Arc::new(Event::default()), Arc::new(Event::default()));
1798 let (s, r) = (Arc::clone(&started), Arc::clone(&release));
1799 let memory = Arc::new(MemoryStore::new());
1800 let reader = Arc::clone(&memory);
1801 let store = TestStore {
1802 memory: Arc::clone(&memory),
1803 get: Some(Box::new(move |keys, host, ranges| {
1804 s.set();
1805 assert!(r.wait(Duration::from_secs(3)));
1806 reader.batch_get_into(keys, host, ranges)
1807 })),
1808 ..TestStore::new()
1809 };
1810 let closes = Arc::clone(&store.closes);
1811 let mut harness = Harness::new(store, CONFIG);
1812 let executor = harness.executor();
1813 let object = keys::object_key(&executor.key_prefix(), "a", 0, 0, 0);
1814 let mut source = b"abcdefgh".to_vec();
1815 memory
1816 .batch_put_from(
1817 &[object],
1818 HostBuffer {
1819 base: source.as_mut_ptr() as usize,
1820 len: 8,
1821 },
1822 &[Range { offset: 0, size: 8 }],
1823 )
1824 .unwrap();
1825 executor.submit(&[get(1, "a", 1)]).unwrap();
1826 assert!(started.wait(Duration::from_secs(1)));
1827 assert!(executor.poll().completions.is_empty());
1829 release.set();
1830 assert_eq!(harness.wait(1)[0].success, [true]);
1831 assert_eq!(&harness.pool[..8], b"abcdefgh");
1832 harness.close(Duration::from_secs(2)).unwrap();
1833 harness.close(Duration::from_secs(2)).unwrap();
1834 assert_eq!(closes.load(Ordering::SeqCst), 1);
1835 }
1836
1837 #[test]
1838 fn a_close_timeout_keeps_the_store_and_the_pool() {
1839 let (started, release) = (Arc::new(Event::default()), Arc::new(Event::default()));
1840 let (s, r) = (Arc::clone(&started), Arc::clone(&release));
1841 let store = TestStore {
1842 get: Some(Box::new(move |keys, _, _| {
1843 s.set();
1844 assert!(r.wait(Duration::from_secs(3)));
1845 Ok(IoResult {
1846 success: vec![true; keys.len()],
1847 retired: true,
1848 object_failures: vec![false; keys.len()],
1849 })
1850 })),
1851 ..TestStore::new()
1852 };
1853 let closes = Arc::clone(&store.closes);
1854 let mut harness = Harness::new(store, CONFIG);
1855 harness.executor().submit(&[get(1, "a", 1)]).unwrap();
1856 assert!(started.wait(Duration::from_secs(1)));
1857 assert!(matches!(
1858 harness.close(Duration::ZERO),
1859 Err(ExecutorError::Unretired)
1860 ));
1861 assert_eq!(closes.load(Ordering::SeqCst), 0);
1862 std::mem::forget(std::mem::take(&mut harness.pool));
1864 release.set();
1865 }
1866
1867 #[test]
1868 fn a_namespace_changes_only_while_idle() {
1869 let (started, release) = (Arc::new(Event::default()), Arc::new(Event::default()));
1870 let (s, r) = (Arc::clone(&started), Arc::clone(&release));
1871 let store = TestStore {
1872 exists: Some(Box::new(move |keys| {
1873 s.set();
1874 assert!(r.wait(Duration::from_secs(3)));
1875 Ok(vec![false; keys.len()])
1876 })),
1877 ..TestStore::new()
1878 };
1879 let harness = Harness::new(store, CONFIG);
1880 let executor = harness.executor();
1881 let first = executor.key_prefix();
1882 assert_eq!(first, keys::key_prefix("deployment", "v1", ""));
1883 executor.submit(&[query(1, &["a"])]).unwrap();
1884 assert!(started.wait(Duration::from_secs(1)));
1885 assert!(matches!(
1886 executor.set_weight_version("v2"),
1887 Err(ExecutorError::Busy)
1888 ));
1889 assert!(!executor.delete_namespace());
1890 release.set();
1891 harness.wait(1);
1892 executor.set_weight_version("v2").unwrap();
1893 assert_eq!(
1894 executor.key_prefix(),
1895 keys::key_prefix("deployment", "v2", "")
1896 );
1897 assert!(executor.delete_namespace());
1898 }
1899
1900 #[test]
1901 fn clearing_the_namespace_deletes_its_objects_and_keeps_its_name() {
1902 let harness = Harness::new(TestStore::new(), CONFIG);
1903 let executor = harness.executor();
1904 executor.submit(&[put(1, "h", 1)]).unwrap();
1905 assert_eq!(harness.wait(1)[0].success, [true]);
1906 let prefix = executor.key_prefix();
1907 assert!(executor.delete_namespace());
1908 assert_eq!(executor.key_prefix(), prefix);
1909 executor.submit(&[query(2, &["h"])]).unwrap();
1910 assert_eq!(harness.wait(1)[0].success, [false]);
1911 }
1912
1913 #[test]
1914 fn a_failed_clear_keeps_the_namespace_and_its_objects() {
1915 let store = TestStore {
1916 remove_fails: true,
1917 ..TestStore::new()
1918 };
1919 let harness = Harness::new(store, CONFIG);
1920 let executor = harness.executor();
1921 executor.submit(&[put(1, "h", 1)]).unwrap();
1922 assert_eq!(harness.wait(1)[0].success, [true]);
1923 let prefix = executor.key_prefix();
1924 assert!(!executor.delete_namespace());
1925 assert_eq!(executor.key_prefix(), prefix);
1926 executor.submit(&[query(2, &["h"])]).unwrap();
1927 assert_eq!(harness.wait(1)[0].success, [true]);
1928 }
1929
1930 #[test]
1931 fn a_new_weight_version_publishes_under_a_new_namespace() {
1932 let store = TestStore::new();
1933 let memory = Arc::clone(&store.memory);
1934 let harness = Harness::new(store, CONFIG);
1935 let executor = harness.executor();
1936 executor.submit(&[put(1, "h", 1)]).unwrap();
1937 assert_eq!(harness.wait(1)[0].success, [true]);
1938 let old = keys::object_key(&executor.key_prefix(), "h", 0, 0, 0);
1939 executor.set_weight_version("v2").unwrap();
1940 executor.submit(&[query(2, &["h"])]).unwrap();
1942 assert_eq!(harness.wait(1)[0].success, [false]);
1943 executor.submit(&[put(3, "h", 1)]).unwrap();
1944 assert_eq!(harness.wait(1)[0].success, [true]);
1945 let new = keys::object_key(&executor.key_prefix(), "h", 0, 0, 0);
1946 assert_ne!(new, old);
1947 assert_eq!(memory.batch_exists(&[old, new]).unwrap(), [true, true]);
1948 }
1949
1950 #[test]
1951 fn a_rejected_put_retires_and_keeps_the_backend_in_service() {
1952 let store = TestStore::new();
1953 let memory = Arc::clone(&store.memory);
1954 let pressure = keys::object_key(
1955 &keys::key_prefix("deployment", "v1", ""),
1956 "pressure",
1957 0,
1958 0,
1959 0,
1960 );
1961 let rejected = pressure.clone();
1962 let store = TestStore {
1963 put: Some(Box::new(move |keys, host, ranges| {
1966 let mut success = Vec::new();
1967 for (key, range) in keys.iter().zip(ranges) {
1968 success.push(
1969 *key != rejected
1970 && memory
1971 .batch_put_from(
1972 std::slice::from_ref(key),
1973 host,
1974 std::slice::from_ref(range),
1975 )?
1976 .success[0],
1977 );
1978 }
1979 Ok(IoResult {
1980 object_failures: vec![false; keys.len()],
1981 success,
1982 retired: true,
1983 })
1984 })),
1985 ..store
1986 };
1987 let memory = Arc::clone(&store.memory);
1988 let harness = Harness::new(store, CONFIG);
1989 let executor = harness.executor();
1990 executor
1991 .submit(&[Operation::Put {
1992 op_id: 1,
1993 keys: vec![key("pressure"), key("saved")],
1994 host_blocks: vec![1, 2],
1995 }])
1996 .unwrap();
1997 let done = harness.wait(1).remove(0);
1998 assert_eq!(done.success, [false, true]);
1999 assert!(done.retired);
2000 assert_eq!(memory.batch_exists(&[pressure]).unwrap(), [false]);
2001 let snapshot = executor.snapshot();
2004 assert_eq!(snapshot.health_state, "healthy");
2005 assert!(!snapshot.unsafe_io);
2006 executor.submit(&[put(2, "retry", 3)]).unwrap();
2007 assert_eq!(harness.wait(1)[0].success, [true]);
2008 }
2009
2010 #[test]
2011 fn page_keys_follow_the_shard_and_state_keys_follow_the_rank() {
2012 let store = Arc::new(MemoryStore::new());
2013 let executors: Vec<Harness> = [0, 1, 2]
2014 .into_iter()
2015 .map(|rank| {
2016 Harness::start(
2017 TestStore {
2018 memory: Arc::clone(&store),
2019 ..TestStore::new()
2020 },
2021 CONFIG,
2022 layout(2, rank % 2, rank),
2023 monotonic_clock(),
2024 )
2025 })
2026 .collect();
2027 let keys_of = |harness: &Harness, group: u32| {
2028 harness
2029 .executor()
2030 .object_keys(&[StorageKey {
2031 group,
2032 content_hash: "h".to_owned(),
2033 page_offset: 0,
2034 }])
2035 .unwrap()
2036 };
2037 let (a, b, other) = (&executors[0], &executors[1], &executors[2]);
2038 assert_eq!(keys_of(a, 0), keys_of(b, 0));
2039 assert_ne!(keys_of(a, 0), keys_of(other, 0));
2040 assert_ne!(keys_of(a, 1), keys_of(b, 1));
2041 }
2042
2043 #[test]
2044 fn a_sharded_block_moves_to_its_new_owner_through_the_store() {
2045 let store = Arc::new(MemoryStore::new());
2046 let harnesses = |base: &str| -> Vec<Harness> {
2047 let _ = base;
2048 (0..2)
2049 .map(|rank| {
2050 Harness::start(
2051 TestStore {
2052 memory: Arc::clone(&store),
2053 ..TestStore::new()
2054 },
2055 CONFIG,
2056 layout(2, rank, rank),
2057 monotonic_clock(),
2058 )
2059 })
2060 .collect()
2061 };
2062 let writers = harnesses("writer");
2063 let pool = writers[1].pool.as_ptr() as *mut u8;
2066 #[allow(unsafe_code)]
2068 unsafe {
2069 std::ptr::copy_nonoverlapping(b"KV01KV01".as_ptr(), pool, 8);
2070 }
2071 for writer in &writers {
2072 writer.executor().submit(&[put(1, "h", 2)]).unwrap();
2073 let fact = &writer.wait(1)[0];
2074 assert!(fact.success == [true] && fact.retired);
2075 }
2076 assert_eq!(store.keys().len(), 1);
2077 let readers = harnesses("reader");
2080 for reader in &readers {
2081 reader.executor().submit(&[get(1, "h", 3)]).unwrap();
2082 let fact = &reader.wait(1)[0];
2083 assert!(fact.success == [true] && fact.retired);
2084 assert_eq!(fact.object_failures, [false]);
2085 }
2086 assert_eq!(&readers[0].pool[24..32], b"KV01KV01");
2087 assert!(readers[1].pool.iter().all(|&byte| byte == 0));
2088 }
2089
2090 #[test]
2091 fn a_non_owner_is_neutral_and_moves_no_bytes() {
2092 let store = TestStore::new();
2093 let calls = Arc::clone(&store.calls);
2094 let harness = Harness::start(store, CONFIG, layout(2, 1, 1), monotonic_clock());
2095 harness.executor().submit(&[get(1, "h", 3)]).unwrap();
2097 let fact = &harness.wait(1)[0];
2098 assert!(fact.success == [true] && fact.retired && fact.object_failures == [false]);
2099 assert!(calls.lock().unwrap().is_empty());
2100 let get_row = row(&harness.executor().snapshot(), "get");
2101 assert_eq!((get_row.success_keys, get_row.success_bytes), (0.0, 0.0));
2102 }
2103
2104 #[test]
2105 fn a_job_without_native_work_cannot_recover_an_open_backend() {
2106 let store = TestStore {
2107 exists: Some(Box::new(|_| {
2108 Err(StoreError("metadata unavailable".to_owned()))
2109 })),
2110 ..TestStore::new()
2111 };
2112 let calls = Arc::clone(&store.calls);
2113 let harness = Harness::start(store, CONFIG, layout(2, 1, 1), monotonic_clock());
2114 let executor = harness.executor();
2115 executor.submit(&[query(1, &["h"])]).unwrap();
2116 assert_eq!(harness.wait(1)[0].success, [false]);
2117 let before = executor.snapshot();
2118 assert_ne!(before.health_state, "healthy");
2119 executor.submit(&[get(2, "h", 3)]).unwrap();
2120 assert_eq!(harness.wait(1)[0].success, [true]);
2121 let after = executor.snapshot();
2122 assert_eq!(
2123 (after.health_state, after.health_epoch),
2124 (before.health_state, before.health_epoch)
2125 );
2126 assert!(calls.lock().unwrap().is_empty());
2127 }
2128
2129 #[test]
2130 fn mixed_owner_results_keep_their_logical_order() {
2131 let store = TestStore::new();
2132 let memory = Arc::clone(&store.memory);
2133 let calls = Arc::clone(&store.calls);
2134 let harness = Harness::start(store, CONFIG, layout(2, 0, 0), monotonic_clock());
2135 let executor = harness.executor();
2136 let keys = [
2137 StorageKey {
2138 group: 0,
2139 content_hash: "absent-peer".to_owned(),
2140 page_offset: 0,
2141 },
2142 StorageKey {
2143 group: 0,
2144 content_hash: "absent-local".to_owned(),
2145 page_offset: 0,
2146 },
2147 StorageKey {
2148 group: 1,
2149 content_hash: "state".to_owned(),
2150 page_offset: 0,
2151 },
2152 ];
2153 let names = executor.object_keys(&keys).unwrap();
2154 let mut payload = vec![7_u8; 16];
2155 memory
2156 .batch_put_from(
2157 &names[2..],
2158 HostBuffer {
2159 base: payload.as_mut_ptr() as usize,
2160 len: 16,
2161 },
2162 &[Range {
2163 offset: 0,
2164 size: 16,
2165 }],
2166 )
2167 .unwrap();
2168 executor
2170 .submit(&[Operation::Get {
2171 op_id: 1,
2172 keys: keys.to_vec(),
2173 host_blocks: vec![2, 3, 1],
2174 }])
2175 .unwrap();
2176 let fact = &harness.wait(1)[0];
2177 assert_eq!(fact.success, [true, false, true]);
2178 assert_eq!(fact.object_failures, [false, true, false]);
2179 assert!(fact.retired);
2180 let get_row = row(&executor.snapshot(), "get");
2181 assert_eq!(
2182 (
2183 get_row.success_keys,
2184 get_row.failed_keys,
2185 get_row.success_bytes
2186 ),
2187 (1.0, 1.0, 16.0)
2188 );
2189 assert_eq!(calls.lock().unwrap()[0].1, names[1..].to_vec());
2190 }
2191
2192 #[test]
2193 fn a_failing_store_and_its_cooldown_keep_non_owners_neutral() {
2194 let store = TestStore {
2195 get: Some(Box::new(|_, _, _| {
2196 Err(StoreError("read backend unavailable".to_owned()))
2197 })),
2198 ..TestStore::new()
2199 };
2200 let calls = Arc::clone(&store.calls);
2201 let harness = Harness::start(
2202 store,
2203 ExecutorConfig {
2204 recovery_interval: Duration::from_secs(60),
2205 ..CONFIG
2206 },
2207 layout(2, 0, 0),
2208 monotonic_clock(),
2209 );
2210 let executor = harness.executor();
2211 let keys = vec![key("peer"), key("local")];
2212 for op_id in [1, 2] {
2213 executor
2214 .submit(&[Operation::Get {
2215 op_id,
2216 keys: keys.clone(),
2217 host_blocks: vec![2, 3],
2218 }])
2219 .unwrap();
2220 let fact = &harness.wait(1)[0];
2221 assert_eq!(fact.success, [true, false]);
2222 assert!(fact.retired && fact.object_failures == [false, false]);
2223 }
2224 assert_eq!(calls.lock().unwrap().len(), 1);
2225 let get_row = row(&executor.snapshot(), "get");
2226 assert_eq!(
2227 (
2228 get_row.failed_keys,
2229 get_row.success_keys,
2230 get_row.success_bytes
2231 ),
2232 (2.0, 0.0, 0.0)
2233 );
2234 }
2235}