use std::error::Error;
use std::fmt;
use std::time::{Duration, SystemTime};
use sha2::{Digest, Sha256};
use oxide_batch_core::{
BatchStatus, DefinitionRevision, DurableStateKind, ExecutionCounts, ExecutionTimestamps,
ExecutionVersion, ExitStatus, FailureSummary, JobExecutionId, JobInstanceId, JobName, NodeId,
ParameterName, ParameterValueKind, StateSchemaId, StateSchemaVersion, StepExecutionId,
StepName, StepPartitionId,
};
use crate::{
BoxFuture, CanonicalWriter, FlowDecision, OperatorRecord, RecoveryDecision, RepositoryError,
RetentionHold, hex_digest,
};
pub const MAX_PAGE_SIZE: u16 = 500;
pub const DEFAULT_PAGE_SIZE: u16 = 50;
pub const MAX_RESPONSE_BYTES: usize = 256 * 1024;
pub const MAX_CURSOR_BYTES: usize = 256;
pub const MIN_UNRESOLVED_AGE: Duration = Duration::from_mins(1);
const CURSOR_FORMAT_VERSION: u8 = 1;
const MAX_CURSOR_NAME_BYTES: usize = 128;
const KEY_TAG_IDENTITY: u8 = 1;
const KEY_TAG_ORDERED: u8 = 2;
const KEY_TAG_NAME: u8 = 3;
const BINDING_BYTES: usize = 8;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PageSize(u16);
impl PageSize {
pub const fn new(value: u16) -> Result<Self, ExplorerError> {
if value == 0 || value > MAX_PAGE_SIZE {
return Err(ExplorerError::PageSizeOutOfRange { requested: value });
}
Ok(Self(value))
}
#[must_use]
pub const fn get(self) -> u16 {
self.0
}
}
impl Default for PageSize {
fn default() -> Self {
Self(DEFAULT_PAGE_SIZE)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PageRequest {
size: PageSize,
cursor: Option<Cursor>,
}
impl PageRequest {
#[must_use]
pub const fn first(size: PageSize) -> Self {
Self { size, cursor: None }
}
#[must_use]
pub const fn resume(size: PageSize, cursor: Cursor) -> Self {
Self {
size,
cursor: Some(cursor),
}
}
#[must_use]
pub const fn size(&self) -> PageSize {
self.size
}
#[must_use]
pub const fn cursor(&self) -> Option<&Cursor> {
self.cursor.as_ref()
}
}
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Cursor(Vec<u8>);
impl Cursor {
pub fn from_bytes(value: impl Into<Vec<u8>>) -> Result<Self, CursorError> {
let value = value.into();
if value.is_empty() || value.len() > MAX_CURSOR_BYTES {
return Err(CursorError::CursorInvalid);
}
Ok(Self(value))
}
pub fn from_hex(value: &str) -> Result<Self, CursorError> {
if !value.len().is_multiple_of(2) {
return Err(CursorError::CursorInvalid);
}
let mut bytes = Vec::with_capacity(value.len() / 2);
let raw = value.as_bytes();
for pair in raw.chunks_exact(2) {
let high = hex_value(pair[0]).ok_or(CursorError::CursorInvalid)?;
let low = hex_value(pair[1]).ok_or(CursorError::CursorInvalid)?;
bytes.push((high << 4) | low);
}
Self::from_bytes(bytes)
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
fn hex_value(value: u8) -> Option<u8> {
match value {
b'0'..=b'9' => Some(value - b'0'),
b'a'..=b'f' => Some(value - b'a' + 10),
_ => None,
}
}
impl fmt::Debug for Cursor {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Cursor")
.field("bytes", &self.0.len())
.finish()
}
}
impl fmt::Display for Cursor {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&hex_digest(&self.0))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Page<T> {
rows: Vec<T>,
next: Option<Cursor>,
}
impl<T> Page<T> {
pub(crate) const fn new(rows: Vec<T>, next: Option<Cursor>) -> Self {
Self { rows, next }
}
#[must_use]
pub fn rows(&self) -> &[T] {
&self.rows
}
#[must_use]
pub fn into_rows(self) -> Vec<T> {
self.rows
}
#[must_use]
pub const fn next_cursor(&self) -> Option<&Cursor> {
self.next.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ExplorerQuery {
JobNames,
Instances {
job_name: JobName,
},
Executions {
job_instance_id: JobInstanceId,
},
StepExecutions {
job_execution_id: JobExecutionId,
},
UnresolvedExecutions {
minimum_age: Duration,
},
RecoveryDecisions {
job_execution_id: JobExecutionId,
},
FlowDecisions {
job_execution_id: JobExecutionId,
},
StepPartitions {
step_execution_id: StepExecutionId,
},
OperatorRequests {
job_execution_id: JobExecutionId,
},
}
impl ExplorerQuery {
const fn discriminant(&self) -> u8 {
match self {
Self::JobNames => 1,
Self::Instances { .. } => 2,
Self::Executions { .. } => 3,
Self::StepExecutions { .. } => 4,
Self::UnresolvedExecutions { .. } => 5,
Self::RecoveryDecisions { .. } => 6,
Self::FlowDecisions { .. } => 7,
Self::StepPartitions { .. } => 8,
Self::OperatorRequests { .. } => 9,
}
}
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::JobNames => "list_job_names",
Self::Instances { .. } => "list_instances",
Self::Executions { .. } => "list_executions",
Self::StepExecutions { .. } => "list_step_executions",
Self::UnresolvedExecutions { .. } => "list_unresolved_executions",
Self::RecoveryDecisions { .. } => "list_recovery_decisions",
Self::FlowDecisions { .. } => "list_flow_decisions",
Self::StepPartitions { .. } => "list_step_partitions",
Self::OperatorRequests { .. } => "list_operator_requests",
}
}
fn identity(&self, size: PageSize) -> [u8; 32] {
let mut writer = CanonicalWriter::new("oxide-batch.explorer-query.v1");
writer.push_str(self.name());
writer.push_u64(u64::from(size.get()));
match self {
Self::JobNames => writer.push_str(""),
Self::Instances { job_name } => writer.push_str(job_name.as_str()),
Self::Executions { job_instance_id } => writer.push_u64(job_instance_id.get()),
Self::StepExecutions { job_execution_id }
| Self::RecoveryDecisions { job_execution_id }
| Self::FlowDecisions { job_execution_id }
| Self::OperatorRequests { job_execution_id } => {
writer.push_u64(job_execution_id.get());
}
Self::UnresolvedExecutions { minimum_age } => {
writer.push_u64(minimum_age.as_secs());
}
Self::StepPartitions { step_execution_id } => writer.push_u64(step_execution_id.get()),
}
writer.digest()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CursorKey {
Identity(u64),
Ordered {
primary: u64,
identity: u64,
},
Name(String),
}
impl CursorKey {
fn encode(&self, target: &mut Vec<u8>) -> Result<(), CursorError> {
match self {
Self::Identity(value) => {
target.push(KEY_TAG_IDENTITY);
target.extend_from_slice(&value.to_be_bytes());
}
Self::Ordered { primary, identity } => {
target.push(KEY_TAG_ORDERED);
target.extend_from_slice(&primary.to_be_bytes());
target.extend_from_slice(&identity.to_be_bytes());
}
Self::Name(value) => {
if value.len() > MAX_CURSOR_NAME_BYTES {
return Err(CursorError::CursorInvalid);
}
target.push(KEY_TAG_NAME);
let length = u8::try_from(value.len()).map_err(|_| CursorError::CursorInvalid)?;
target.push(length);
target.extend_from_slice(value.as_bytes());
}
}
Ok(())
}
fn decode(bytes: &[u8]) -> Result<(Self, &[u8]), CursorError> {
let (tag, rest) = bytes.split_first().ok_or(CursorError::CursorInvalid)?;
match *tag {
KEY_TAG_IDENTITY => {
let (value, rest) = read_u64(rest)?;
Ok((Self::Identity(value), rest))
}
KEY_TAG_ORDERED => {
let (primary, rest) = read_u64(rest)?;
let (identity, rest) = read_u64(rest)?;
Ok((Self::Ordered { primary, identity }, rest))
}
KEY_TAG_NAME => {
let (length, rest) = rest.split_first().ok_or(CursorError::CursorInvalid)?;
let length = usize::from(*length);
if rest.len() < length {
return Err(CursorError::CursorInvalid);
}
let (value, rest) = rest.split_at(length);
let value = core::str::from_utf8(value).map_err(|_| CursorError::CursorInvalid)?;
Ok((Self::Name(value.to_owned()), rest))
}
_ => Err(CursorError::CursorInvalid),
}
}
}
fn read_u64(bytes: &[u8]) -> Result<(u64, &[u8]), CursorError> {
if bytes.len() < 8 {
return Err(CursorError::CursorInvalid);
}
let (head, rest) = bytes.split_at(8);
let mut value = [0_u8; 8];
value.copy_from_slice(head);
Ok((u64::from_be_bytes(value), rest))
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QueryWindow {
after: Option<CursorKey>,
ceiling: u64,
limit: u16,
}
impl QueryWindow {
pub(crate) const fn new(after: Option<CursorKey>, ceiling: u64, limit: u16) -> Self {
Self {
after,
ceiling,
limit,
}
}
#[must_use]
pub const fn after(&self) -> Option<&CursorKey> {
self.after.as_ref()
}
#[must_use]
pub const fn ceiling(&self) -> u64 {
self.ceiling
}
#[must_use]
pub const fn limit(&self) -> u16 {
self.limit
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParameterDescriptor {
name: ParameterName,
kind: ParameterValueKind,
identifying: bool,
}
impl ParameterDescriptor {
#[doc(hidden)]
#[must_use]
pub const fn new(name: ParameterName, kind: ParameterValueKind, identifying: bool) -> Self {
Self {
name,
kind,
identifying,
}
}
#[must_use]
pub const fn name(&self) -> &ParameterName {
&self.name
}
#[must_use]
pub const fn kind(&self) -> ParameterValueKind {
self.kind
}
#[must_use]
pub const fn is_identifying(&self) -> bool {
self.identifying
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StateEnvelopeDescriptor {
kind: DurableStateKind,
format_version: u16,
schema_id: StateSchemaId,
schema_version: StateSchemaVersion,
encoded_len: usize,
}
impl StateEnvelopeDescriptor {
#[doc(hidden)]
#[must_use]
pub const fn new(
kind: DurableStateKind,
format_version: u16,
schema_id: StateSchemaId,
schema_version: StateSchemaVersion,
encoded_len: usize,
) -> Self {
Self {
kind,
format_version,
schema_id,
schema_version,
encoded_len,
}
}
#[must_use]
pub const fn kind(&self) -> DurableStateKind {
self.kind
}
#[must_use]
pub const fn format_version(&self) -> u16 {
self.format_version
}
#[must_use]
pub const fn schema_id(&self) -> &StateSchemaId {
&self.schema_id
}
#[must_use]
pub const fn schema_version(&self) -> StateSchemaVersion {
self.schema_version
}
#[must_use]
pub const fn encoded_len(&self) -> usize {
self.encoded_len
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DefinitionDescriptor {
revision: DefinitionRevision,
manifest_format: u16,
manifest_digest: [u8; 32],
}
impl DefinitionDescriptor {
#[doc(hidden)]
#[must_use]
pub const fn new(
revision: DefinitionRevision,
manifest_format: u16,
manifest_digest: [u8; 32],
) -> Self {
Self {
revision,
manifest_format,
manifest_digest,
}
}
#[must_use]
pub const fn revision(&self) -> &DefinitionRevision {
&self.revision
}
#[must_use]
pub const fn manifest_format(&self) -> u16 {
self.manifest_format
}
#[must_use]
pub const fn manifest_digest(&self) -> &[u8; 32] {
&self.manifest_digest
}
#[must_use]
pub fn manifest_digest_hex(&self) -> String {
hex_digest(&self.manifest_digest)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct JobInstanceProjection {
id: JobInstanceId,
job_name: JobName,
instance_key_digest: [u8; 32],
parameters: Vec<ParameterDescriptor>,
created_at: Option<SystemTime>,
hold: Option<RetentionHold>,
}
impl JobInstanceProjection {
#[doc(hidden)]
#[must_use]
pub const fn new(
id: JobInstanceId,
job_name: JobName,
instance_key_digest: [u8; 32],
parameters: Vec<ParameterDescriptor>,
created_at: Option<SystemTime>,
hold: Option<RetentionHold>,
) -> Self {
Self {
id,
job_name,
instance_key_digest,
parameters,
created_at,
hold,
}
}
#[must_use]
pub const fn id(&self) -> JobInstanceId {
self.id
}
#[must_use]
pub const fn job_name(&self) -> &JobName {
&self.job_name
}
#[must_use]
pub const fn instance_key_digest(&self) -> &[u8; 32] {
&self.instance_key_digest
}
#[must_use]
pub fn instance_key_digest_hex(&self) -> String {
hex_digest(&self.instance_key_digest)
}
#[must_use]
pub fn parameters(&self) -> &[ParameterDescriptor] {
&self.parameters
}
#[must_use]
pub const fn created_at(&self) -> Option<SystemTime> {
self.created_at
}
#[must_use]
pub const fn hold(&self) -> Option<&RetentionHold> {
self.hold.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct JobExecutionProjection {
id: JobExecutionId,
job_instance_id: JobInstanceId,
job_name: JobName,
attempt: u32,
status: BatchStatus,
exit_status: ExitStatus,
counts: ExecutionCounts,
version: ExecutionVersion,
timestamps: ExecutionTimestamps,
updated_at: SystemTime,
failure: Option<FailureSummary>,
definition: Option<DefinitionDescriptor>,
context: Option<StateEnvelopeDescriptor>,
stop_requested_at: Option<SystemTime>,
owner_recorded: bool,
}
impl JobExecutionProjection {
#[allow(clippy::too_many_arguments)]
#[doc(hidden)]
#[must_use]
pub const fn new(
id: JobExecutionId,
job_instance_id: JobInstanceId,
job_name: JobName,
attempt: u32,
status: BatchStatus,
exit_status: ExitStatus,
counts: ExecutionCounts,
version: ExecutionVersion,
timestamps: ExecutionTimestamps,
updated_at: SystemTime,
failure: Option<FailureSummary>,
definition: Option<DefinitionDescriptor>,
context: Option<StateEnvelopeDescriptor>,
stop_requested_at: Option<SystemTime>,
owner_recorded: bool,
) -> Self {
Self {
id,
job_instance_id,
job_name,
attempt,
status,
exit_status,
counts,
version,
timestamps,
updated_at,
failure,
definition,
context,
stop_requested_at,
owner_recorded,
}
}
#[must_use]
pub const fn id(&self) -> JobExecutionId {
self.id
}
#[must_use]
pub const fn job_instance_id(&self) -> JobInstanceId {
self.job_instance_id
}
#[must_use]
pub const fn job_name(&self) -> &JobName {
&self.job_name
}
#[must_use]
pub const fn attempt(&self) -> u32 {
self.attempt
}
#[must_use]
pub const fn status(&self) -> BatchStatus {
self.status
}
#[must_use]
pub const fn exit_status(&self) -> &ExitStatus {
&self.exit_status
}
#[must_use]
pub const fn counts(&self) -> ExecutionCounts {
self.counts
}
#[must_use]
pub const fn version(&self) -> ExecutionVersion {
self.version
}
#[must_use]
pub const fn timestamps(&self) -> ExecutionTimestamps {
self.timestamps
}
#[must_use]
pub const fn updated_at(&self) -> SystemTime {
self.updated_at
}
#[must_use]
pub const fn failure(&self) -> Option<FailureSummary> {
self.failure
}
#[must_use]
pub const fn definition(&self) -> Option<&DefinitionDescriptor> {
self.definition.as_ref()
}
#[must_use]
pub const fn context(&self) -> Option<&StateEnvelopeDescriptor> {
self.context.as_ref()
}
#[must_use]
pub const fn stop_requested_at(&self) -> Option<SystemTime> {
self.stop_requested_at
}
#[must_use]
pub const fn owner_recorded(&self) -> bool {
self.owner_recorded
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StepExecutionProjection {
id: StepExecutionId,
job_execution_id: JobExecutionId,
step_name: StepName,
node_id: Option<NodeId>,
status: BatchStatus,
exit_status: ExitStatus,
counts: ExecutionCounts,
version: ExecutionVersion,
timestamps: ExecutionTimestamps,
failure: Option<FailureSummary>,
checkpoint: Option<StateEnvelopeDescriptor>,
context: Option<StateEnvelopeDescriptor>,
}
impl StepExecutionProjection {
#[allow(clippy::too_many_arguments)]
#[doc(hidden)]
#[must_use]
pub const fn new(
id: StepExecutionId,
job_execution_id: JobExecutionId,
step_name: StepName,
node_id: Option<NodeId>,
status: BatchStatus,
exit_status: ExitStatus,
counts: ExecutionCounts,
version: ExecutionVersion,
timestamps: ExecutionTimestamps,
failure: Option<FailureSummary>,
checkpoint: Option<StateEnvelopeDescriptor>,
context: Option<StateEnvelopeDescriptor>,
) -> Self {
Self {
id,
job_execution_id,
step_name,
node_id,
status,
exit_status,
counts,
version,
timestamps,
failure,
checkpoint,
context,
}
}
#[must_use]
pub const fn id(&self) -> StepExecutionId {
self.id
}
#[must_use]
pub const fn job_execution_id(&self) -> JobExecutionId {
self.job_execution_id
}
#[must_use]
pub const fn step_name(&self) -> &StepName {
&self.step_name
}
#[must_use]
pub const fn node_id(&self) -> Option<&NodeId> {
self.node_id.as_ref()
}
#[must_use]
pub const fn status(&self) -> BatchStatus {
self.status
}
#[must_use]
pub const fn exit_status(&self) -> &ExitStatus {
&self.exit_status
}
#[must_use]
pub const fn counts(&self) -> ExecutionCounts {
self.counts
}
#[must_use]
pub const fn version(&self) -> ExecutionVersion {
self.version
}
#[must_use]
pub const fn timestamps(&self) -> ExecutionTimestamps {
self.timestamps
}
#[must_use]
pub const fn failure(&self) -> Option<FailureSummary> {
self.failure
}
#[must_use]
pub const fn checkpoint(&self) -> Option<&StateEnvelopeDescriptor> {
self.checkpoint.as_ref()
}
#[must_use]
pub const fn context(&self) -> Option<&StateEnvelopeDescriptor> {
self.context.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StepPartitionProjection {
id: StepPartitionId,
step_execution_id: StepExecutionId,
partition_key: String,
ordinal: u32,
status: BatchStatus,
exit_status: ExitStatus,
counts: ExecutionCounts,
version: ExecutionVersion,
worker_step_execution_id: Option<StepExecutionId>,
context: Option<StateEnvelopeDescriptor>,
}
impl StepPartitionProjection {
#[allow(clippy::too_many_arguments)]
#[doc(hidden)]
#[must_use]
pub const fn new(
id: StepPartitionId,
step_execution_id: StepExecutionId,
partition_key: String,
ordinal: u32,
status: BatchStatus,
exit_status: ExitStatus,
counts: ExecutionCounts,
version: ExecutionVersion,
worker_step_execution_id: Option<StepExecutionId>,
context: Option<StateEnvelopeDescriptor>,
) -> Self {
Self {
id,
step_execution_id,
partition_key,
ordinal,
status,
exit_status,
counts,
version,
worker_step_execution_id,
context,
}
}
#[must_use]
pub const fn id(&self) -> StepPartitionId {
self.id
}
#[must_use]
pub const fn step_execution_id(&self) -> StepExecutionId {
self.step_execution_id
}
#[must_use]
pub fn partition_key(&self) -> &str {
&self.partition_key
}
#[must_use]
pub const fn ordinal(&self) -> u32 {
self.ordinal
}
#[must_use]
pub const fn status(&self) -> BatchStatus {
self.status
}
#[must_use]
pub const fn exit_status(&self) -> &ExitStatus {
&self.exit_status
}
#[must_use]
pub const fn counts(&self) -> ExecutionCounts {
self.counts
}
#[must_use]
pub const fn version(&self) -> ExecutionVersion {
self.version
}
#[must_use]
pub const fn worker_step_execution_id(&self) -> Option<StepExecutionId> {
self.worker_step_execution_id
}
#[must_use]
pub const fn context(&self) -> Option<&StateEnvelopeDescriptor> {
self.context.as_ref()
}
}
pub trait ExplorerRepository: Send + Sync {
fn identity_ceiling<'a>(
&'a self,
query: &'a ExplorerQuery,
) -> BoxFuture<'a, Result<u64, ExplorerError>>;
fn job_names<'a>(
&'a self,
window: &'a QueryWindow,
) -> BoxFuture<'a, Result<Vec<JobName>, ExplorerError>>;
fn instances<'a>(
&'a self,
job_name: &'a JobName,
window: &'a QueryWindow,
) -> BoxFuture<'a, Result<Vec<JobInstanceProjection>, ExplorerError>>;
fn executions<'a>(
&'a self,
job_instance_id: JobInstanceId,
window: &'a QueryWindow,
) -> BoxFuture<'a, Result<Vec<JobExecutionProjection>, ExplorerError>>;
fn execution(
&self,
job_execution_id: JobExecutionId,
) -> BoxFuture<'_, Result<Option<JobExecutionProjection>, ExplorerError>>;
fn step_executions<'a>(
&'a self,
job_execution_id: JobExecutionId,
window: &'a QueryWindow,
) -> BoxFuture<'a, Result<Vec<StepExecutionProjection>, ExplorerError>>;
fn unresolved_executions<'a>(
&'a self,
minimum_age: Duration,
window: &'a QueryWindow,
) -> BoxFuture<'a, Result<Vec<JobExecutionProjection>, ExplorerError>>;
fn recovery_decisions<'a>(
&'a self,
job_execution_id: JobExecutionId,
window: &'a QueryWindow,
) -> BoxFuture<'a, Result<Vec<RecoveryDecision>, ExplorerError>>;
fn flow_decisions<'a>(
&'a self,
job_execution_id: JobExecutionId,
window: &'a QueryWindow,
) -> BoxFuture<'a, Result<Vec<FlowDecision>, ExplorerError>>;
fn step_partitions<'a>(
&'a self,
step_execution_id: StepExecutionId,
window: &'a QueryWindow,
) -> BoxFuture<'a, Result<Vec<StepPartitionProjection>, ExplorerError>>;
fn operator_requests<'a>(
&'a self,
job_execution_id: JobExecutionId,
window: &'a QueryWindow,
) -> BoxFuture<'a, Result<Vec<OperatorRecord>, ExplorerError>>;
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ExplorerError {
PageSizeOutOfRange {
requested: u16,
},
AgeBoundTooSmall {
minimum: Duration,
},
Cursor(CursorError),
ResponseTooLarge {
limit: usize,
},
Timeout,
UnsupportedCapability,
Repository(RepositoryError),
}
impl fmt::Display for ExplorerError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PageSizeOutOfRange { requested } => write!(
formatter,
"page size {requested} is outside 1..={MAX_PAGE_SIZE}"
),
Self::AgeBoundTooSmall { minimum } => write!(
formatter,
"the age bound must be at least {} seconds",
minimum.as_secs()
),
Self::Cursor(error) => error.fmt(formatter),
Self::ResponseTooLarge { limit } => {
write!(formatter, "the encoded response exceeds {limit} bytes")
}
Self::Timeout => {
formatter.write_str("the bounded query exceeded its statement timeout")
}
Self::UnsupportedCapability => {
formatter.write_str("the adapter does not support keyset pagination")
}
Self::Repository(error) => error.fmt(formatter),
}
}
}
impl Error for ExplorerError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Cursor(error) => Some(error),
Self::Repository(error) => Some(error),
_ => None,
}
}
}
impl From<CursorError> for ExplorerError {
fn from(value: CursorError) -> Self {
Self::Cursor(value)
}
}
impl From<RepositoryError> for ExplorerError {
fn from(value: RepositoryError) -> Self {
Self::Repository(value)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CursorError {
CursorInvalid,
CursorQueryMismatch,
}
impl fmt::Display for CursorError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CursorInvalid => formatter.write_str("the continuation token is not valid"),
Self::CursorQueryMismatch => {
formatter.write_str("the continuation token belongs to a different query")
}
}
}
}
impl Error for CursorError {}
fn encode_cursor(
query: &ExplorerQuery,
size: PageSize,
key: &CursorKey,
ceiling: u64,
) -> Result<Cursor, CursorError> {
let mut bytes = Vec::with_capacity(80);
bytes.push(CURSOR_FORMAT_VERSION);
bytes.push(query.discriminant());
key.encode(&mut bytes)?;
bytes.extend_from_slice(&ceiling.to_be_bytes());
bytes.extend_from_slice(&query_binding(query, size));
let checksum = cursor_checksum(&bytes);
bytes.extend_from_slice(&checksum);
Cursor::from_bytes(bytes)
}
fn decode_cursor(
cursor: &Cursor,
query: &ExplorerQuery,
size: PageSize,
) -> Result<(CursorKey, u64), ExplorerError> {
let bytes = cursor.as_bytes();
if bytes.len() <= 32 {
return Err(CursorError::CursorInvalid.into());
}
let (body, checksum) = bytes.split_at(bytes.len() - 32);
if cursor_checksum(body) != checksum {
return Err(CursorError::CursorInvalid.into());
}
let (version, rest) = body.split_first().ok_or(CursorError::CursorInvalid)?;
if *version != CURSOR_FORMAT_VERSION {
return Err(CursorError::CursorInvalid.into());
}
let (discriminant, rest) = rest.split_first().ok_or(CursorError::CursorInvalid)?;
let (key, rest) = CursorKey::decode(rest)?;
let (ceiling, rest) = read_u64(rest)?;
if rest.len() != BINDING_BYTES {
return Err(CursorError::CursorInvalid.into());
}
if *discriminant != query.discriminant() || rest != query_binding(query, size) {
return Err(CursorError::CursorQueryMismatch.into());
}
Ok((key, ceiling))
}
fn query_binding(query: &ExplorerQuery, size: PageSize) -> [u8; BINDING_BYTES] {
let identity = query.identity(size);
let mut binding = [0_u8; BINDING_BYTES];
binding.copy_from_slice(&identity[..BINDING_BYTES]);
binding
}
fn cursor_checksum(body: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(body);
hasher.finalize().into()
}
#[doc(hidden)]
pub trait ExplorerRow {
fn cursor_key(&self) -> CursorKey;
fn encoded_len(&self) -> usize;
}
#[doc(hidden)]
#[must_use]
pub const fn start_window(request: &PageRequest, ceiling: u64) -> QueryWindow {
QueryWindow::new(None, ceiling, request.size().get())
}
#[doc(hidden)]
pub fn resume_window(
cursor: &Cursor,
query: &ExplorerQuery,
request: &PageRequest,
) -> Result<QueryWindow, ExplorerError> {
let (after, ceiling) = decode_cursor(cursor, query, request.size())?;
Ok(QueryWindow::new(Some(after), ceiling, request.size().get()))
}
#[doc(hidden)]
pub fn page<T: ExplorerRow>(
query: &ExplorerQuery,
request: &PageRequest,
ceiling: u64,
rows: Vec<T>,
) -> Result<Page<T>, ExplorerError> {
let limit = usize::from(request.size().get());
let full = rows.len() >= limit;
let mut kept = Vec::with_capacity(rows.len().min(limit));
let mut encoded = 0_usize;
let mut truncated = false;
for row in rows.into_iter().take(limit) {
let next = encoded.saturating_add(row.encoded_len());
if next > MAX_RESPONSE_BYTES {
if kept.is_empty() {
return Err(ExplorerError::ResponseTooLarge {
limit: MAX_RESPONSE_BYTES,
});
}
truncated = true;
break;
}
encoded = next;
kept.push(row);
}
let next = if (full || truncated) && !kept.is_empty() {
let key = kept
.last()
.map(ExplorerRow::cursor_key)
.ok_or(ExplorerError::Cursor(CursorError::CursorInvalid))?;
Some(encode_cursor(query, request.size(), &key, ceiling)?)
} else {
None
};
Ok(Page::new(kept, next))
}
impl ExplorerRow for JobName {
fn cursor_key(&self) -> CursorKey {
CursorKey::Name(self.as_str().to_owned())
}
fn encoded_len(&self) -> usize {
self.as_str().len().saturating_add(8)
}
}
impl ExplorerRow for JobInstanceProjection {
fn cursor_key(&self) -> CursorKey {
CursorKey::Identity(self.id().get())
}
fn encoded_len(&self) -> usize {
let parameters = self
.parameters()
.iter()
.map(|parameter| parameter.name().as_str().len().saturating_add(24))
.fold(0_usize, usize::saturating_add);
self.job_name()
.as_str()
.len()
.saturating_add(160)
.saturating_add(parameters)
}
}
impl ExplorerRow for JobExecutionProjection {
fn cursor_key(&self) -> CursorKey {
CursorKey::Ordered {
primary: u64::from(self.attempt()),
identity: self.id().get(),
}
}
fn encoded_len(&self) -> usize {
self.job_name()
.as_str()
.len()
.saturating_add(self.exit_status().code().as_str().len())
.saturating_add(256)
}
}
impl ExplorerRow for StepExecutionProjection {
fn cursor_key(&self) -> CursorKey {
CursorKey::Identity(self.id().get())
}
fn encoded_len(&self) -> usize {
self.step_name()
.as_str()
.len()
.saturating_add(self.exit_status().code().as_str().len())
.saturating_add(256)
}
}
impl ExplorerRow for StepPartitionProjection {
fn cursor_key(&self) -> CursorKey {
CursorKey::Identity(self.id().get())
}
fn encoded_len(&self) -> usize {
self.partition_key().len().saturating_add(192)
}
}
impl ExplorerRow for RecoveryDecision {
fn cursor_key(&self) -> CursorKey {
CursorKey::Identity(self.id().get())
}
fn encoded_len(&self) -> usize {
self.reason_code()
.len()
.saturating_add(self.operator_reference().len())
.saturating_add(160)
}
}
impl ExplorerRow for FlowDecision {
fn cursor_key(&self) -> CursorKey {
CursorKey::Ordered {
primary: self.sequence().get(),
identity: self.id().get(),
}
}
fn encoded_len(&self) -> usize {
self.source_node_id()
.as_str()
.len()
.saturating_add(self.observed_outcome().as_str().len())
.saturating_add(224)
}
}
impl ExplorerRow for OperatorRecord {
fn cursor_key(&self) -> CursorKey {
CursorKey::Identity(self.id().get())
}
fn encoded_len(&self) -> usize {
self.operation_id()
.as_str()
.len()
.saturating_add(self.actor().as_str().len())
.saturating_add(self.reason().map_or(0, |reason| reason.as_str().len()))
.saturating_add(192)
}
}