use rocketmq_error::CanonicalCondition;
use crate::protocol::header_codec::HeaderValueKind;
use crate::protocol::remoting_command_defaults::RemotingCommandDefaultsConflict;
use crate::protocol::remoting_command_facade::InvalidRemotingSerializeType;
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ProtocolContractViolation {
#[error("checkpoint schema version {actual} does not match {expected}")]
CheckpointSchemaVersion {
expected: u16,
actual: u16,
},
#[error("invalid checkpoint field {field}: {reason}")]
InvalidCheckpointField {
field: &'static str,
reason: &'static str,
},
#[error("invalid checkpoint offsets: {reason}")]
InvalidCheckpointOffsets {
reason: &'static str,
},
#[error("checkpoint set has no Store member")]
MissingStoreMembers,
#[error("checkpoint set repeats a Store member")]
DuplicateStoreMember,
#[error("checkpoint member does not match the set barrier")]
CheckpointSetBindingMismatch {
member: &'static str,
},
#[error("checkpoint permits destructive WAL or persistent-volume replacement")]
DestructiveRollback,
#[error("checkpoint restore verification is incomplete")]
RestoreVerificationIncomplete,
#[error("missing required header field {header}.{key}")]
Missing {
header: &'static str,
key: &'static str,
},
#[error("invalid header field {header}.{key}: expected {expected:?}")]
InvalidValue {
header: &'static str,
key: &'static str,
expected: HeaderValueKind,
},
#[error("conflicting canonical and alias values for {header}.{key}")]
Conflict {
header: &'static str,
key: &'static str,
},
#[error("typed header and dynamic field conflict for {header}.{key}")]
DynamicFieldConflict {
header: &'static str,
key: &'static str,
},
#[error("legacy header validation failed for {header}")]
LegacyValidation {
header: &'static str,
},
#[error("legacy header map conversion failed for {header}")]
LegacyMapConversionFailed {
header: &'static str,
},
#[error("header validation failed for {header}: {rule}")]
Validation {
header: &'static str,
rule: &'static str,
},
#[error("header value is outside Java range for {header}.{key}")]
JavaRange {
header: &'static str,
key: &'static str,
},
#[error("header key length exceeds the ROCKETMQ limit for {header}.{key}")]
KeyLengthOverflow {
header: &'static str,
key: &'static str,
},
#[error("header value length exceeds the ROCKETMQ limit for {header}.{key}")]
ValueLengthOverflow {
header: &'static str,
key: &'static str,
},
#[error("ROCKETMQ extension-field payload exceeds the signed 32-bit wire limit")]
ExtensionFieldsLengthOverflow,
#[error("dynamic header key length exceeds the ROCKETMQ wire limit")]
DynamicKeyLengthOverflow,
#[error("dynamic header value length exceeds the ROCKETMQ wire limit")]
DynamicValueLengthOverflow,
#[error("direct binary codec is unavailable for {header}")]
FastCodecUnavailable {
header: &'static str,
},
#[error("invalid remoting serialization type")]
InvalidSerializeType(
#[from]
#[source]
InvalidRemotingSerializeType,
),
#[error("remoting command defaults conflict with the initialized value")]
RemotingDefaultsConflict(
#[from]
#[source]
RemotingCommandDefaultsConflict,
),
#[error("The specified group is blank.")]
BlankSubscriptionGroup,
#[error("The specified group is longer than group max length: {max_length}")]
SubscriptionGroupTooLong {
max_length: usize,
},
#[error("The specified group contains illegal characters, allowing only ^[%|a-zA-Z0-9_-]+$")]
SubscriptionGroupIllegalCharacters,
#[error("The specified group list contains a duplicate group.")]
DuplicateSubscriptionGroup,
}
impl ProtocolContractViolation {
#[must_use]
pub const fn condition(&self) -> CanonicalCondition {
match self {
Self::DestructiveRollback | Self::RestoreVerificationIncomplete | Self::RemotingDefaultsConflict(_) => {
CanonicalCondition::FailedPrecondition
}
Self::CheckpointSchemaVersion { .. }
| Self::InvalidCheckpointField { .. }
| Self::InvalidCheckpointOffsets { .. }
| Self::MissingStoreMembers
| Self::DuplicateStoreMember
| Self::CheckpointSetBindingMismatch { .. }
| Self::Missing { .. }
| Self::InvalidValue { .. }
| Self::Conflict { .. }
| Self::DynamicFieldConflict { .. }
| Self::LegacyValidation { .. }
| Self::LegacyMapConversionFailed { .. }
| Self::Validation { .. }
| Self::JavaRange { .. }
| Self::KeyLengthOverflow { .. }
| Self::ValueLengthOverflow { .. }
| Self::ExtensionFieldsLengthOverflow
| Self::DynamicKeyLengthOverflow
| Self::DynamicValueLengthOverflow
| Self::FastCodecUnavailable { .. }
| Self::InvalidSerializeType(_)
| Self::BlankSubscriptionGroup
| Self::SubscriptionGroupTooLong { .. }
| Self::SubscriptionGroupIllegalCharacters
| Self::DuplicateSubscriptionGroup => CanonicalCondition::InvalidArgument,
}
}
}
#[cfg(test)]
mod tests {
use std::error::Error as _;
use super::*;
use crate::protocol::remoting_command::SERIALIZE_TYPE_PROPERTY;
use crate::protocol::remoting_command_facade::resolve_remoting_serialize_type;
#[test]
fn invalid_configuration_keeps_a_typed_source_behind_fixed_text() {
const SENTINEL: &str = "unsupported-serialize-type-secret";
let error = resolve_remoting_serialize_type(Some(SENTINEL), None).unwrap_err();
assert_eq!(error.condition(), CanonicalCondition::InvalidArgument);
assert_eq!(error.to_string(), "invalid remoting serialization type");
assert!(!format!("{error:?}").contains(SENTINEL));
let source = error
.source()
.and_then(|source| source.downcast_ref::<InvalidRemotingSerializeType>())
.expect("typed remoting serialization source");
assert_eq!(source.key(), SERIALIZE_TYPE_PROPERTY);
assert_eq!(source.value(), SENTINEL);
}
#[test]
fn rollback_contracts_are_failed_preconditions() {
assert_eq!(
ProtocolContractViolation::DestructiveRollback.condition(),
CanonicalCondition::FailedPrecondition
);
assert_eq!(
ProtocolContractViolation::RestoreVerificationIncomplete.condition(),
CanonicalCondition::FailedPrecondition
);
}
}