1use std::error::Error;
14use std::fmt;
15use std::time::{Duration, SystemTime};
16
17use sha2::{Digest, Sha256};
18
19use oxide_batch_core::{
20 BatchStatus, DefinitionRevision, DurableStateKind, ExecutionCounts, ExecutionTimestamps,
21 ExecutionVersion, ExitStatus, FailureSummary, JobExecutionId, JobInstanceId, JobName, NodeId,
22 ParameterName, ParameterValueKind, StateSchemaId, StateSchemaVersion, StepExecutionId,
23 StepName, StepPartitionId,
24};
25
26use crate::{
27 BoxFuture, CanonicalWriter, FlowDecision, OperatorRecord, RecoveryDecision, RepositoryError,
28 RetentionHold, hex_digest,
29};
30
31pub const MAX_PAGE_SIZE: u16 = 500;
33pub const DEFAULT_PAGE_SIZE: u16 = 50;
35pub const MAX_RESPONSE_BYTES: usize = 256 * 1024;
37pub const MAX_CURSOR_BYTES: usize = 256;
39pub const MIN_UNRESOLVED_AGE: Duration = Duration::from_mins(1);
41
42const CURSOR_FORMAT_VERSION: u8 = 1;
43const MAX_CURSOR_NAME_BYTES: usize = 128;
44const KEY_TAG_IDENTITY: u8 = 1;
45const KEY_TAG_ORDERED: u8 = 2;
46const KEY_TAG_NAME: u8 = 3;
47const BINDING_BYTES: usize = 8;
48
49#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
51pub struct PageSize(u16);
52
53impl PageSize {
54 pub const fn new(value: u16) -> Result<Self, ExplorerError> {
60 if value == 0 || value > MAX_PAGE_SIZE {
61 return Err(ExplorerError::PageSizeOutOfRange { requested: value });
62 }
63 Ok(Self(value))
64 }
65
66 #[must_use]
68 pub const fn get(self) -> u16 {
69 self.0
70 }
71}
72
73impl Default for PageSize {
74 fn default() -> Self {
75 Self(DEFAULT_PAGE_SIZE)
76 }
77}
78
79#[derive(Clone, Debug, Default, Eq, PartialEq)]
81pub struct PageRequest {
82 size: PageSize,
83 cursor: Option<Cursor>,
84}
85
86impl PageRequest {
87 #[must_use]
89 pub const fn first(size: PageSize) -> Self {
90 Self { size, cursor: None }
91 }
92
93 #[must_use]
95 pub const fn resume(size: PageSize, cursor: Cursor) -> Self {
96 Self {
97 size,
98 cursor: Some(cursor),
99 }
100 }
101
102 #[must_use]
104 pub const fn size(&self) -> PageSize {
105 self.size
106 }
107
108 #[must_use]
110 pub const fn cursor(&self) -> Option<&Cursor> {
111 self.cursor.as_ref()
112 }
113}
114
115#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
121pub struct Cursor(Vec<u8>);
122
123impl Cursor {
124 pub fn from_bytes(value: impl Into<Vec<u8>>) -> Result<Self, CursorError> {
131 let value = value.into();
132 if value.is_empty() || value.len() > MAX_CURSOR_BYTES {
133 return Err(CursorError::CursorInvalid);
134 }
135 Ok(Self(value))
136 }
137
138 pub fn from_hex(value: &str) -> Result<Self, CursorError> {
145 if !value.len().is_multiple_of(2) {
146 return Err(CursorError::CursorInvalid);
147 }
148 let mut bytes = Vec::with_capacity(value.len() / 2);
149 let raw = value.as_bytes();
150 for pair in raw.chunks_exact(2) {
151 let high = hex_value(pair[0]).ok_or(CursorError::CursorInvalid)?;
152 let low = hex_value(pair[1]).ok_or(CursorError::CursorInvalid)?;
153 bytes.push((high << 4) | low);
154 }
155 Self::from_bytes(bytes)
156 }
157
158 #[must_use]
160 pub fn as_bytes(&self) -> &[u8] {
161 &self.0
162 }
163}
164
165fn hex_value(value: u8) -> Option<u8> {
166 match value {
167 b'0'..=b'9' => Some(value - b'0'),
168 b'a'..=b'f' => Some(value - b'a' + 10),
169 _ => None,
170 }
171}
172
173impl fmt::Debug for Cursor {
174 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
175 formatter
176 .debug_struct("Cursor")
177 .field("bytes", &self.0.len())
178 .finish()
179 }
180}
181
182impl fmt::Display for Cursor {
183 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
184 formatter.write_str(&hex_digest(&self.0))
185 }
186}
187
188#[derive(Clone, Debug, Eq, PartialEq)]
190pub struct Page<T> {
191 rows: Vec<T>,
192 next: Option<Cursor>,
193}
194
195impl<T> Page<T> {
196 pub(crate) const fn new(rows: Vec<T>, next: Option<Cursor>) -> Self {
197 Self { rows, next }
198 }
199
200 #[must_use]
202 pub fn rows(&self) -> &[T] {
203 &self.rows
204 }
205
206 #[must_use]
208 pub fn into_rows(self) -> Vec<T> {
209 self.rows
210 }
211
212 #[must_use]
214 pub const fn next_cursor(&self) -> Option<&Cursor> {
215 self.next.as_ref()
216 }
217}
218
219#[derive(Clone, Debug, Eq, PartialEq)]
224#[non_exhaustive]
225pub enum ExplorerQuery {
226 JobNames,
228 Instances {
230 job_name: JobName,
232 },
233 Executions {
235 job_instance_id: JobInstanceId,
237 },
238 StepExecutions {
240 job_execution_id: JobExecutionId,
242 },
243 UnresolvedExecutions {
245 minimum_age: Duration,
247 },
248 RecoveryDecisions {
250 job_execution_id: JobExecutionId,
252 },
253 FlowDecisions {
255 job_execution_id: JobExecutionId,
257 },
258 StepPartitions {
260 step_execution_id: StepExecutionId,
262 },
263 OperatorRequests {
265 job_execution_id: JobExecutionId,
267 },
268}
269
270impl ExplorerQuery {
271 const fn discriminant(&self) -> u8 {
272 match self {
273 Self::JobNames => 1,
274 Self::Instances { .. } => 2,
275 Self::Executions { .. } => 3,
276 Self::StepExecutions { .. } => 4,
277 Self::UnresolvedExecutions { .. } => 5,
278 Self::RecoveryDecisions { .. } => 6,
279 Self::FlowDecisions { .. } => 7,
280 Self::StepPartitions { .. } => 8,
281 Self::OperatorRequests { .. } => 9,
282 }
283 }
284
285 #[must_use]
287 pub const fn name(&self) -> &'static str {
288 match self {
289 Self::JobNames => "list_job_names",
290 Self::Instances { .. } => "list_instances",
291 Self::Executions { .. } => "list_executions",
292 Self::StepExecutions { .. } => "list_step_executions",
293 Self::UnresolvedExecutions { .. } => "list_unresolved_executions",
294 Self::RecoveryDecisions { .. } => "list_recovery_decisions",
295 Self::FlowDecisions { .. } => "list_flow_decisions",
296 Self::StepPartitions { .. } => "list_step_partitions",
297 Self::OperatorRequests { .. } => "list_operator_requests",
298 }
299 }
300
301 fn identity(&self, size: PageSize) -> [u8; 32] {
302 let mut writer = CanonicalWriter::new("oxide-batch.explorer-query.v1");
303 writer.push_str(self.name());
304 writer.push_u64(u64::from(size.get()));
305 match self {
306 Self::JobNames => writer.push_str(""),
307 Self::Instances { job_name } => writer.push_str(job_name.as_str()),
308 Self::Executions { job_instance_id } => writer.push_u64(job_instance_id.get()),
309 Self::StepExecutions { job_execution_id }
310 | Self::RecoveryDecisions { job_execution_id }
311 | Self::FlowDecisions { job_execution_id }
312 | Self::OperatorRequests { job_execution_id } => {
313 writer.push_u64(job_execution_id.get());
314 }
315 Self::UnresolvedExecutions { minimum_age } => {
316 writer.push_u64(minimum_age.as_secs());
317 }
318 Self::StepPartitions { step_execution_id } => writer.push_u64(step_execution_id.get()),
319 }
320 writer.digest()
321 }
322}
323
324#[derive(Clone, Debug, Eq, PartialEq)]
326#[non_exhaustive]
327pub enum CursorKey {
328 Identity(u64),
330 Ordered {
332 primary: u64,
334 identity: u64,
336 },
337 Name(String),
339}
340
341impl CursorKey {
342 fn encode(&self, target: &mut Vec<u8>) -> Result<(), CursorError> {
343 match self {
344 Self::Identity(value) => {
345 target.push(KEY_TAG_IDENTITY);
346 target.extend_from_slice(&value.to_be_bytes());
347 }
348 Self::Ordered { primary, identity } => {
349 target.push(KEY_TAG_ORDERED);
350 target.extend_from_slice(&primary.to_be_bytes());
351 target.extend_from_slice(&identity.to_be_bytes());
352 }
353 Self::Name(value) => {
354 if value.len() > MAX_CURSOR_NAME_BYTES {
355 return Err(CursorError::CursorInvalid);
356 }
357 target.push(KEY_TAG_NAME);
358 let length = u8::try_from(value.len()).map_err(|_| CursorError::CursorInvalid)?;
359 target.push(length);
360 target.extend_from_slice(value.as_bytes());
361 }
362 }
363 Ok(())
364 }
365
366 fn decode(bytes: &[u8]) -> Result<(Self, &[u8]), CursorError> {
367 let (tag, rest) = bytes.split_first().ok_or(CursorError::CursorInvalid)?;
368 match *tag {
369 KEY_TAG_IDENTITY => {
370 let (value, rest) = read_u64(rest)?;
371 Ok((Self::Identity(value), rest))
372 }
373 KEY_TAG_ORDERED => {
374 let (primary, rest) = read_u64(rest)?;
375 let (identity, rest) = read_u64(rest)?;
376 Ok((Self::Ordered { primary, identity }, rest))
377 }
378 KEY_TAG_NAME => {
379 let (length, rest) = rest.split_first().ok_or(CursorError::CursorInvalid)?;
380 let length = usize::from(*length);
381 if rest.len() < length {
382 return Err(CursorError::CursorInvalid);
383 }
384 let (value, rest) = rest.split_at(length);
385 let value = core::str::from_utf8(value).map_err(|_| CursorError::CursorInvalid)?;
386 Ok((Self::Name(value.to_owned()), rest))
387 }
388 _ => Err(CursorError::CursorInvalid),
389 }
390 }
391}
392
393fn read_u64(bytes: &[u8]) -> Result<(u64, &[u8]), CursorError> {
394 if bytes.len() < 8 {
395 return Err(CursorError::CursorInvalid);
396 }
397 let (head, rest) = bytes.split_at(8);
398 let mut value = [0_u8; 8];
399 value.copy_from_slice(head);
400 Ok((u64::from_be_bytes(value), rest))
401}
402
403#[derive(Clone, Debug, Eq, PartialEq)]
405pub struct QueryWindow {
406 after: Option<CursorKey>,
407 ceiling: u64,
408 limit: u16,
409}
410
411impl QueryWindow {
412 pub(crate) const fn new(after: Option<CursorKey>, ceiling: u64, limit: u16) -> Self {
413 Self {
414 after,
415 ceiling,
416 limit,
417 }
418 }
419
420 #[must_use]
422 pub const fn after(&self) -> Option<&CursorKey> {
423 self.after.as_ref()
424 }
425
426 #[must_use]
431 pub const fn ceiling(&self) -> u64 {
432 self.ceiling
433 }
434
435 #[must_use]
437 pub const fn limit(&self) -> u16 {
438 self.limit
439 }
440}
441
442#[derive(Clone, Debug, Eq, PartialEq)]
447pub struct ParameterDescriptor {
448 name: ParameterName,
449 kind: ParameterValueKind,
450 identifying: bool,
451}
452
453impl ParameterDescriptor {
454 #[doc(hidden)]
456 #[must_use]
457 pub const fn new(name: ParameterName, kind: ParameterValueKind, identifying: bool) -> Self {
458 Self {
459 name,
460 kind,
461 identifying,
462 }
463 }
464
465 #[must_use]
467 pub const fn name(&self) -> &ParameterName {
468 &self.name
469 }
470
471 #[must_use]
473 pub const fn kind(&self) -> ParameterValueKind {
474 self.kind
475 }
476
477 #[must_use]
479 pub const fn is_identifying(&self) -> bool {
480 self.identifying
481 }
482}
483
484#[derive(Clone, Debug, Eq, PartialEq)]
489pub struct StateEnvelopeDescriptor {
490 kind: DurableStateKind,
491 format_version: u16,
492 schema_id: StateSchemaId,
493 schema_version: StateSchemaVersion,
494 encoded_len: usize,
495}
496
497impl StateEnvelopeDescriptor {
498 #[doc(hidden)]
503 #[must_use]
504 pub const fn new(
505 kind: DurableStateKind,
506 format_version: u16,
507 schema_id: StateSchemaId,
508 schema_version: StateSchemaVersion,
509 encoded_len: usize,
510 ) -> Self {
511 Self {
512 kind,
513 format_version,
514 schema_id,
515 schema_version,
516 encoded_len,
517 }
518 }
519
520 #[must_use]
522 pub const fn kind(&self) -> DurableStateKind {
523 self.kind
524 }
525
526 #[must_use]
528 pub const fn format_version(&self) -> u16 {
529 self.format_version
530 }
531
532 #[must_use]
534 pub const fn schema_id(&self) -> &StateSchemaId {
535 &self.schema_id
536 }
537
538 #[must_use]
540 pub const fn schema_version(&self) -> StateSchemaVersion {
541 self.schema_version
542 }
543
544 #[must_use]
546 pub const fn encoded_len(&self) -> usize {
547 self.encoded_len
548 }
549}
550
551#[derive(Clone, Debug, Eq, PartialEq)]
553pub struct DefinitionDescriptor {
554 revision: DefinitionRevision,
555 manifest_format: u16,
556 manifest_digest: [u8; 32],
557}
558
559impl DefinitionDescriptor {
560 #[doc(hidden)]
562 #[must_use]
563 pub const fn new(
564 revision: DefinitionRevision,
565 manifest_format: u16,
566 manifest_digest: [u8; 32],
567 ) -> Self {
568 Self {
569 revision,
570 manifest_format,
571 manifest_digest,
572 }
573 }
574
575 #[must_use]
577 pub const fn revision(&self) -> &DefinitionRevision {
578 &self.revision
579 }
580
581 #[must_use]
583 pub const fn manifest_format(&self) -> u16 {
584 self.manifest_format
585 }
586
587 #[must_use]
589 pub const fn manifest_digest(&self) -> &[u8; 32] {
590 &self.manifest_digest
591 }
592
593 #[must_use]
595 pub fn manifest_digest_hex(&self) -> String {
596 hex_digest(&self.manifest_digest)
597 }
598}
599
600#[derive(Clone, Debug, Eq, PartialEq)]
602pub struct JobInstanceProjection {
603 id: JobInstanceId,
604 job_name: JobName,
605 instance_key_digest: [u8; 32],
606 parameters: Vec<ParameterDescriptor>,
607 created_at: Option<SystemTime>,
608 hold: Option<RetentionHold>,
609}
610
611impl JobInstanceProjection {
612 #[doc(hidden)]
614 #[must_use]
615 pub const fn new(
616 id: JobInstanceId,
617 job_name: JobName,
618 instance_key_digest: [u8; 32],
619 parameters: Vec<ParameterDescriptor>,
620 created_at: Option<SystemTime>,
621 hold: Option<RetentionHold>,
622 ) -> Self {
623 Self {
624 id,
625 job_name,
626 instance_key_digest,
627 parameters,
628 created_at,
629 hold,
630 }
631 }
632
633 #[must_use]
635 pub const fn id(&self) -> JobInstanceId {
636 self.id
637 }
638
639 #[must_use]
641 pub const fn job_name(&self) -> &JobName {
642 &self.job_name
643 }
644
645 #[must_use]
647 pub const fn instance_key_digest(&self) -> &[u8; 32] {
648 &self.instance_key_digest
649 }
650
651 #[must_use]
653 pub fn instance_key_digest_hex(&self) -> String {
654 hex_digest(&self.instance_key_digest)
655 }
656
657 #[must_use]
659 pub fn parameters(&self) -> &[ParameterDescriptor] {
660 &self.parameters
661 }
662
663 #[must_use]
665 pub const fn created_at(&self) -> Option<SystemTime> {
666 self.created_at
667 }
668
669 #[must_use]
671 pub const fn hold(&self) -> Option<&RetentionHold> {
672 self.hold.as_ref()
673 }
674}
675
676#[derive(Clone, Debug, Eq, PartialEq)]
678pub struct JobExecutionProjection {
679 id: JobExecutionId,
680 job_instance_id: JobInstanceId,
681 job_name: JobName,
682 attempt: u32,
683 status: BatchStatus,
684 exit_status: ExitStatus,
685 counts: ExecutionCounts,
686 version: ExecutionVersion,
687 timestamps: ExecutionTimestamps,
688 updated_at: SystemTime,
689 failure: Option<FailureSummary>,
690 definition: Option<DefinitionDescriptor>,
691 context: Option<StateEnvelopeDescriptor>,
692 stop_requested_at: Option<SystemTime>,
693 owner_recorded: bool,
694}
695
696impl JobExecutionProjection {
697 #[allow(clippy::too_many_arguments)]
699 #[doc(hidden)]
700 #[must_use]
701 pub const fn new(
702 id: JobExecutionId,
703 job_instance_id: JobInstanceId,
704 job_name: JobName,
705 attempt: u32,
706 status: BatchStatus,
707 exit_status: ExitStatus,
708 counts: ExecutionCounts,
709 version: ExecutionVersion,
710 timestamps: ExecutionTimestamps,
711 updated_at: SystemTime,
712 failure: Option<FailureSummary>,
713 definition: Option<DefinitionDescriptor>,
714 context: Option<StateEnvelopeDescriptor>,
715 stop_requested_at: Option<SystemTime>,
716 owner_recorded: bool,
717 ) -> Self {
718 Self {
719 id,
720 job_instance_id,
721 job_name,
722 attempt,
723 status,
724 exit_status,
725 counts,
726 version,
727 timestamps,
728 updated_at,
729 failure,
730 definition,
731 context,
732 stop_requested_at,
733 owner_recorded,
734 }
735 }
736
737 #[must_use]
739 pub const fn id(&self) -> JobExecutionId {
740 self.id
741 }
742
743 #[must_use]
745 pub const fn job_instance_id(&self) -> JobInstanceId {
746 self.job_instance_id
747 }
748
749 #[must_use]
751 pub const fn job_name(&self) -> &JobName {
752 &self.job_name
753 }
754
755 #[must_use]
757 pub const fn attempt(&self) -> u32 {
758 self.attempt
759 }
760
761 #[must_use]
763 pub const fn status(&self) -> BatchStatus {
764 self.status
765 }
766
767 #[must_use]
769 pub const fn exit_status(&self) -> &ExitStatus {
770 &self.exit_status
771 }
772
773 #[must_use]
775 pub const fn counts(&self) -> ExecutionCounts {
776 self.counts
777 }
778
779 #[must_use]
781 pub const fn version(&self) -> ExecutionVersion {
782 self.version
783 }
784
785 #[must_use]
787 pub const fn timestamps(&self) -> ExecutionTimestamps {
788 self.timestamps
789 }
790
791 #[must_use]
793 pub const fn updated_at(&self) -> SystemTime {
794 self.updated_at
795 }
796
797 #[must_use]
799 pub const fn failure(&self) -> Option<FailureSummary> {
800 self.failure
801 }
802
803 #[must_use]
805 pub const fn definition(&self) -> Option<&DefinitionDescriptor> {
806 self.definition.as_ref()
807 }
808
809 #[must_use]
811 pub const fn context(&self) -> Option<&StateEnvelopeDescriptor> {
812 self.context.as_ref()
813 }
814
815 #[must_use]
817 pub const fn stop_requested_at(&self) -> Option<SystemTime> {
818 self.stop_requested_at
819 }
820
821 #[must_use]
826 pub const fn owner_recorded(&self) -> bool {
827 self.owner_recorded
828 }
829}
830
831#[derive(Clone, Debug, Eq, PartialEq)]
833pub struct StepExecutionProjection {
834 id: StepExecutionId,
835 job_execution_id: JobExecutionId,
836 step_name: StepName,
837 node_id: Option<NodeId>,
838 status: BatchStatus,
839 exit_status: ExitStatus,
840 counts: ExecutionCounts,
841 version: ExecutionVersion,
842 timestamps: ExecutionTimestamps,
843 failure: Option<FailureSummary>,
844 checkpoint: Option<StateEnvelopeDescriptor>,
845 context: Option<StateEnvelopeDescriptor>,
846}
847
848impl StepExecutionProjection {
849 #[allow(clippy::too_many_arguments)]
851 #[doc(hidden)]
852 #[must_use]
853 pub const fn new(
854 id: StepExecutionId,
855 job_execution_id: JobExecutionId,
856 step_name: StepName,
857 node_id: Option<NodeId>,
858 status: BatchStatus,
859 exit_status: ExitStatus,
860 counts: ExecutionCounts,
861 version: ExecutionVersion,
862 timestamps: ExecutionTimestamps,
863 failure: Option<FailureSummary>,
864 checkpoint: Option<StateEnvelopeDescriptor>,
865 context: Option<StateEnvelopeDescriptor>,
866 ) -> Self {
867 Self {
868 id,
869 job_execution_id,
870 step_name,
871 node_id,
872 status,
873 exit_status,
874 counts,
875 version,
876 timestamps,
877 failure,
878 checkpoint,
879 context,
880 }
881 }
882
883 #[must_use]
885 pub const fn id(&self) -> StepExecutionId {
886 self.id
887 }
888
889 #[must_use]
891 pub const fn job_execution_id(&self) -> JobExecutionId {
892 self.job_execution_id
893 }
894
895 #[must_use]
897 pub const fn step_name(&self) -> &StepName {
898 &self.step_name
899 }
900
901 #[must_use]
903 pub const fn node_id(&self) -> Option<&NodeId> {
904 self.node_id.as_ref()
905 }
906
907 #[must_use]
909 pub const fn status(&self) -> BatchStatus {
910 self.status
911 }
912
913 #[must_use]
915 pub const fn exit_status(&self) -> &ExitStatus {
916 &self.exit_status
917 }
918
919 #[must_use]
921 pub const fn counts(&self) -> ExecutionCounts {
922 self.counts
923 }
924
925 #[must_use]
927 pub const fn version(&self) -> ExecutionVersion {
928 self.version
929 }
930
931 #[must_use]
933 pub const fn timestamps(&self) -> ExecutionTimestamps {
934 self.timestamps
935 }
936
937 #[must_use]
939 pub const fn failure(&self) -> Option<FailureSummary> {
940 self.failure
941 }
942
943 #[must_use]
945 pub const fn checkpoint(&self) -> Option<&StateEnvelopeDescriptor> {
946 self.checkpoint.as_ref()
947 }
948
949 #[must_use]
951 pub const fn context(&self) -> Option<&StateEnvelopeDescriptor> {
952 self.context.as_ref()
953 }
954}
955
956#[derive(Clone, Debug, Eq, PartialEq)]
961pub struct StepPartitionProjection {
962 id: StepPartitionId,
963 step_execution_id: StepExecutionId,
964 partition_key: String,
965 ordinal: u32,
966 status: BatchStatus,
967 exit_status: ExitStatus,
968 counts: ExecutionCounts,
969 version: ExecutionVersion,
970 worker_step_execution_id: Option<StepExecutionId>,
971 context: Option<StateEnvelopeDescriptor>,
972}
973
974impl StepPartitionProjection {
975 #[allow(clippy::too_many_arguments)]
977 #[doc(hidden)]
978 #[must_use]
979 pub const fn new(
980 id: StepPartitionId,
981 step_execution_id: StepExecutionId,
982 partition_key: String,
983 ordinal: u32,
984 status: BatchStatus,
985 exit_status: ExitStatus,
986 counts: ExecutionCounts,
987 version: ExecutionVersion,
988 worker_step_execution_id: Option<StepExecutionId>,
989 context: Option<StateEnvelopeDescriptor>,
990 ) -> Self {
991 Self {
992 id,
993 step_execution_id,
994 partition_key,
995 ordinal,
996 status,
997 exit_status,
998 counts,
999 version,
1000 worker_step_execution_id,
1001 context,
1002 }
1003 }
1004
1005 #[must_use]
1007 pub const fn id(&self) -> StepPartitionId {
1008 self.id
1009 }
1010
1011 #[must_use]
1013 pub const fn step_execution_id(&self) -> StepExecutionId {
1014 self.step_execution_id
1015 }
1016
1017 #[must_use]
1019 pub fn partition_key(&self) -> &str {
1020 &self.partition_key
1021 }
1022
1023 #[must_use]
1025 pub const fn ordinal(&self) -> u32 {
1026 self.ordinal
1027 }
1028
1029 #[must_use]
1031 pub const fn status(&self) -> BatchStatus {
1032 self.status
1033 }
1034
1035 #[must_use]
1037 pub const fn exit_status(&self) -> &ExitStatus {
1038 &self.exit_status
1039 }
1040
1041 #[must_use]
1043 pub const fn counts(&self) -> ExecutionCounts {
1044 self.counts
1045 }
1046
1047 #[must_use]
1049 pub const fn version(&self) -> ExecutionVersion {
1050 self.version
1051 }
1052
1053 #[must_use]
1055 pub const fn worker_step_execution_id(&self) -> Option<StepExecutionId> {
1056 self.worker_step_execution_id
1057 }
1058
1059 #[must_use]
1061 pub const fn context(&self) -> Option<&StateEnvelopeDescriptor> {
1062 self.context.as_ref()
1063 }
1064}
1065
1066pub trait ExplorerRepository: Send + Sync {
1072 fn identity_ceiling<'a>(
1074 &'a self,
1075 query: &'a ExplorerQuery,
1076 ) -> BoxFuture<'a, Result<u64, ExplorerError>>;
1077
1078 fn job_names<'a>(
1080 &'a self,
1081 window: &'a QueryWindow,
1082 ) -> BoxFuture<'a, Result<Vec<JobName>, ExplorerError>>;
1083
1084 fn instances<'a>(
1086 &'a self,
1087 job_name: &'a JobName,
1088 window: &'a QueryWindow,
1089 ) -> BoxFuture<'a, Result<Vec<JobInstanceProjection>, ExplorerError>>;
1090
1091 fn executions<'a>(
1093 &'a self,
1094 job_instance_id: JobInstanceId,
1095 window: &'a QueryWindow,
1096 ) -> BoxFuture<'a, Result<Vec<JobExecutionProjection>, ExplorerError>>;
1097
1098 fn execution(
1100 &self,
1101 job_execution_id: JobExecutionId,
1102 ) -> BoxFuture<'_, Result<Option<JobExecutionProjection>, ExplorerError>>;
1103
1104 fn step_executions<'a>(
1106 &'a self,
1107 job_execution_id: JobExecutionId,
1108 window: &'a QueryWindow,
1109 ) -> BoxFuture<'a, Result<Vec<StepExecutionProjection>, ExplorerError>>;
1110
1111 fn unresolved_executions<'a>(
1113 &'a self,
1114 minimum_age: Duration,
1115 window: &'a QueryWindow,
1116 ) -> BoxFuture<'a, Result<Vec<JobExecutionProjection>, ExplorerError>>;
1117
1118 fn recovery_decisions<'a>(
1120 &'a self,
1121 job_execution_id: JobExecutionId,
1122 window: &'a QueryWindow,
1123 ) -> BoxFuture<'a, Result<Vec<RecoveryDecision>, ExplorerError>>;
1124
1125 fn flow_decisions<'a>(
1127 &'a self,
1128 job_execution_id: JobExecutionId,
1129 window: &'a QueryWindow,
1130 ) -> BoxFuture<'a, Result<Vec<FlowDecision>, ExplorerError>>;
1131
1132 fn step_partitions<'a>(
1134 &'a self,
1135 step_execution_id: StepExecutionId,
1136 window: &'a QueryWindow,
1137 ) -> BoxFuture<'a, Result<Vec<StepPartitionProjection>, ExplorerError>>;
1138
1139 fn operator_requests<'a>(
1141 &'a self,
1142 job_execution_id: JobExecutionId,
1143 window: &'a QueryWindow,
1144 ) -> BoxFuture<'a, Result<Vec<OperatorRecord>, ExplorerError>>;
1145}
1146
1147#[derive(Clone, Debug, Eq, PartialEq)]
1149#[non_exhaustive]
1150pub enum ExplorerError {
1151 PageSizeOutOfRange {
1153 requested: u16,
1155 },
1156 AgeBoundTooSmall {
1158 minimum: Duration,
1160 },
1161 Cursor(CursorError),
1163 ResponseTooLarge {
1165 limit: usize,
1167 },
1168 Timeout,
1170 UnsupportedCapability,
1172 Repository(RepositoryError),
1174}
1175
1176impl fmt::Display for ExplorerError {
1177 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1178 match self {
1179 Self::PageSizeOutOfRange { requested } => write!(
1180 formatter,
1181 "page size {requested} is outside 1..={MAX_PAGE_SIZE}"
1182 ),
1183 Self::AgeBoundTooSmall { minimum } => write!(
1184 formatter,
1185 "the age bound must be at least {} seconds",
1186 minimum.as_secs()
1187 ),
1188 Self::Cursor(error) => error.fmt(formatter),
1189 Self::ResponseTooLarge { limit } => {
1190 write!(formatter, "the encoded response exceeds {limit} bytes")
1191 }
1192 Self::Timeout => {
1193 formatter.write_str("the bounded query exceeded its statement timeout")
1194 }
1195 Self::UnsupportedCapability => {
1196 formatter.write_str("the adapter does not support keyset pagination")
1197 }
1198 Self::Repository(error) => error.fmt(formatter),
1199 }
1200 }
1201}
1202
1203impl Error for ExplorerError {
1204 fn source(&self) -> Option<&(dyn Error + 'static)> {
1205 match self {
1206 Self::Cursor(error) => Some(error),
1207 Self::Repository(error) => Some(error),
1208 _ => None,
1209 }
1210 }
1211}
1212
1213impl From<CursorError> for ExplorerError {
1214 fn from(value: CursorError) -> Self {
1215 Self::Cursor(value)
1216 }
1217}
1218
1219impl From<RepositoryError> for ExplorerError {
1220 fn from(value: RepositoryError) -> Self {
1221 Self::Repository(value)
1222 }
1223}
1224
1225#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1227#[non_exhaustive]
1228pub enum CursorError {
1229 CursorInvalid,
1231 CursorQueryMismatch,
1233}
1234
1235impl fmt::Display for CursorError {
1236 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1237 match self {
1238 Self::CursorInvalid => formatter.write_str("the continuation token is not valid"),
1239 Self::CursorQueryMismatch => {
1240 formatter.write_str("the continuation token belongs to a different query")
1241 }
1242 }
1243 }
1244}
1245
1246impl Error for CursorError {}
1247
1248fn encode_cursor(
1249 query: &ExplorerQuery,
1250 size: PageSize,
1251 key: &CursorKey,
1252 ceiling: u64,
1253) -> Result<Cursor, CursorError> {
1254 let mut bytes = Vec::with_capacity(80);
1255 bytes.push(CURSOR_FORMAT_VERSION);
1256 bytes.push(query.discriminant());
1257 key.encode(&mut bytes)?;
1258 bytes.extend_from_slice(&ceiling.to_be_bytes());
1259 bytes.extend_from_slice(&query_binding(query, size));
1260 let checksum = cursor_checksum(&bytes);
1261 bytes.extend_from_slice(&checksum);
1262 Cursor::from_bytes(bytes)
1263}
1264
1265fn decode_cursor(
1266 cursor: &Cursor,
1267 query: &ExplorerQuery,
1268 size: PageSize,
1269) -> Result<(CursorKey, u64), ExplorerError> {
1270 let bytes = cursor.as_bytes();
1271 if bytes.len() <= 32 {
1272 return Err(CursorError::CursorInvalid.into());
1273 }
1274 let (body, checksum) = bytes.split_at(bytes.len() - 32);
1275 if cursor_checksum(body) != checksum {
1276 return Err(CursorError::CursorInvalid.into());
1277 }
1278 let (version, rest) = body.split_first().ok_or(CursorError::CursorInvalid)?;
1279 if *version != CURSOR_FORMAT_VERSION {
1280 return Err(CursorError::CursorInvalid.into());
1281 }
1282 let (discriminant, rest) = rest.split_first().ok_or(CursorError::CursorInvalid)?;
1283 let (key, rest) = CursorKey::decode(rest)?;
1284 let (ceiling, rest) = read_u64(rest)?;
1285 if rest.len() != BINDING_BYTES {
1286 return Err(CursorError::CursorInvalid.into());
1287 }
1288 if *discriminant != query.discriminant() || rest != query_binding(query, size) {
1292 return Err(CursorError::CursorQueryMismatch.into());
1293 }
1294 Ok((key, ceiling))
1295}
1296
1297fn query_binding(query: &ExplorerQuery, size: PageSize) -> [u8; BINDING_BYTES] {
1298 let identity = query.identity(size);
1299 let mut binding = [0_u8; BINDING_BYTES];
1300 binding.copy_from_slice(&identity[..BINDING_BYTES]);
1301 binding
1302}
1303
1304fn cursor_checksum(body: &[u8]) -> [u8; 32] {
1305 let mut hasher = Sha256::new();
1306 hasher.update(body);
1307 hasher.finalize().into()
1308}
1309
1310#[doc(hidden)]
1312pub trait ExplorerRow {
1313 fn cursor_key(&self) -> CursorKey;
1315
1316 fn encoded_len(&self) -> usize;
1318}
1319
1320#[doc(hidden)]
1322#[must_use]
1323pub const fn start_window(request: &PageRequest, ceiling: u64) -> QueryWindow {
1324 QueryWindow::new(None, ceiling, request.size().get())
1325}
1326
1327#[doc(hidden)]
1334pub fn resume_window(
1335 cursor: &Cursor,
1336 query: &ExplorerQuery,
1337 request: &PageRequest,
1338) -> Result<QueryWindow, ExplorerError> {
1339 let (after, ceiling) = decode_cursor(cursor, query, request.size())?;
1340 Ok(QueryWindow::new(Some(after), ceiling, request.size().get()))
1341}
1342
1343#[doc(hidden)]
1351pub fn page<T: ExplorerRow>(
1352 query: &ExplorerQuery,
1353 request: &PageRequest,
1354 ceiling: u64,
1355 rows: Vec<T>,
1356) -> Result<Page<T>, ExplorerError> {
1357 let limit = usize::from(request.size().get());
1358 let full = rows.len() >= limit;
1359 let mut kept = Vec::with_capacity(rows.len().min(limit));
1360 let mut encoded = 0_usize;
1361 let mut truncated = false;
1362 for row in rows.into_iter().take(limit) {
1363 let next = encoded.saturating_add(row.encoded_len());
1364 if next > MAX_RESPONSE_BYTES {
1365 if kept.is_empty() {
1366 return Err(ExplorerError::ResponseTooLarge {
1367 limit: MAX_RESPONSE_BYTES,
1368 });
1369 }
1370 truncated = true;
1371 break;
1372 }
1373 encoded = next;
1374 kept.push(row);
1375 }
1376 let next = if (full || truncated) && !kept.is_empty() {
1377 let key = kept
1378 .last()
1379 .map(ExplorerRow::cursor_key)
1380 .ok_or(ExplorerError::Cursor(CursorError::CursorInvalid))?;
1381 Some(encode_cursor(query, request.size(), &key, ceiling)?)
1382 } else {
1383 None
1384 };
1385 Ok(Page::new(kept, next))
1386}
1387
1388impl ExplorerRow for JobName {
1389 fn cursor_key(&self) -> CursorKey {
1390 CursorKey::Name(self.as_str().to_owned())
1391 }
1392
1393 fn encoded_len(&self) -> usize {
1394 self.as_str().len().saturating_add(8)
1395 }
1396}
1397
1398impl ExplorerRow for JobInstanceProjection {
1399 fn cursor_key(&self) -> CursorKey {
1400 CursorKey::Identity(self.id().get())
1401 }
1402
1403 fn encoded_len(&self) -> usize {
1404 let parameters = self
1405 .parameters()
1406 .iter()
1407 .map(|parameter| parameter.name().as_str().len().saturating_add(24))
1408 .fold(0_usize, usize::saturating_add);
1409 self.job_name()
1410 .as_str()
1411 .len()
1412 .saturating_add(160)
1413 .saturating_add(parameters)
1414 }
1415}
1416
1417impl ExplorerRow for JobExecutionProjection {
1418 fn cursor_key(&self) -> CursorKey {
1419 CursorKey::Ordered {
1420 primary: u64::from(self.attempt()),
1421 identity: self.id().get(),
1422 }
1423 }
1424
1425 fn encoded_len(&self) -> usize {
1426 self.job_name()
1427 .as_str()
1428 .len()
1429 .saturating_add(self.exit_status().code().as_str().len())
1430 .saturating_add(256)
1431 }
1432}
1433
1434impl ExplorerRow for StepExecutionProjection {
1435 fn cursor_key(&self) -> CursorKey {
1436 CursorKey::Identity(self.id().get())
1437 }
1438
1439 fn encoded_len(&self) -> usize {
1440 self.step_name()
1441 .as_str()
1442 .len()
1443 .saturating_add(self.exit_status().code().as_str().len())
1444 .saturating_add(256)
1445 }
1446}
1447
1448impl ExplorerRow for StepPartitionProjection {
1449 fn cursor_key(&self) -> CursorKey {
1450 CursorKey::Identity(self.id().get())
1451 }
1452
1453 fn encoded_len(&self) -> usize {
1454 self.partition_key().len().saturating_add(192)
1455 }
1456}
1457
1458impl ExplorerRow for RecoveryDecision {
1459 fn cursor_key(&self) -> CursorKey {
1460 CursorKey::Identity(self.id().get())
1461 }
1462
1463 fn encoded_len(&self) -> usize {
1464 self.reason_code()
1465 .len()
1466 .saturating_add(self.operator_reference().len())
1467 .saturating_add(160)
1468 }
1469}
1470
1471impl ExplorerRow for FlowDecision {
1472 fn cursor_key(&self) -> CursorKey {
1473 CursorKey::Ordered {
1474 primary: self.sequence().get(),
1475 identity: self.id().get(),
1476 }
1477 }
1478
1479 fn encoded_len(&self) -> usize {
1480 self.source_node_id()
1481 .as_str()
1482 .len()
1483 .saturating_add(self.observed_outcome().as_str().len())
1484 .saturating_add(224)
1485 }
1486}
1487
1488impl ExplorerRow for OperatorRecord {
1489 fn cursor_key(&self) -> CursorKey {
1490 CursorKey::Identity(self.id().get())
1491 }
1492
1493 fn encoded_len(&self) -> usize {
1494 self.operation_id()
1495 .as_str()
1496 .len()
1497 .saturating_add(self.actor().as_str().len())
1498 .saturating_add(self.reason().map_or(0, |reason| reason.as_str().len()))
1499 .saturating_add(192)
1500 }
1501}