picodata-plugin 26.1.2

Toolkit to build plugins for picodata.io DBMS
Documentation
use tarantool::error::TarantoolErrorCode;
use tarantool::static_assert;

tarantool::define_enum_with_introspection! {
    /// Error codes used with [`BoxError`] which are only generated by picodata.
    ///
    /// [`BoxError`]: tarantool::error::BoxError
    pub enum ErrorCode {
        // NOTE: At the moment of writing this the maximum tarantool error code
        // on master branch is 286, but we still leave a huge window between
        // that and our first error code just to be absolutely safe.
        //
        // We also don't want to go too big because of the msgpack encoding size
        // considerations (65535 is the largest number which serializes into 3 bytes).

        /// This is the first picodata error code. Use this for errors which
        /// are hard to categorize and for which there's no specific way of handling.
        ///
        /// Also please use a **unique error message** for these kinds of errors,
        /// so it's easy to find where it was generated in code.
        ///
        /// If your error instead should be handled in a specific way please
        /// add a new one if the existing ones don't satisfy your needs.
        Other = 10000,

        /// Requested instance is not a leader.
        NotALeader = 10001,

        /// Contents of a builtin table has invalid format.
        StorageCorrupted = 10002,

        /// Operation request from different term.
        TermMismatch = 10003,

        /// Raft log is temporarily unavailable.
        RaftLogUnavailable = 10004,

        /// Can't check the predicate because raft log is compacted.
        RaftLogCompacted = 10005,

        /// Nearly impossible error indicating invalid request.
        CasNoSuchRaftIndex = 10006,

        /// Checking the predicate revealed a collision.
        CasConflictFound = 10007,

        /// Request expected raft entry to have a different term.
        CasEntryTermMismatch = 10008,

        /// SpaceNotAllowed: space {space} is prohibited for use in a predicate
        CasTableNotAllowed = 10009,

        /// Unexpected traft operation kind.
        CasInvalidOpKind = 10010,

        NoSuchService = 10011,
        ServiceNotStarted = 10012,
        ServicePoisoned = 10013,
        ServiceNotAvailable = 10014,
        WrongPluginVersion = 10015,

        NoSuchInstance = 10016,
        NoSuchReplicaset = 10017,

        LeaderUnknown = 10018,

        // Error in plugin system.
        PluginError = 10019,

        // Instance in question was expelled from the cluster.
        InstanceExpelled = 10020,

        // Replicaset in question was expelled from the cluster.
        ReplicasetExpelled = 10021,

        // Instance unavailiable due to it's target state is Offline
        InstanceUnavaliable = 10022,

        /// TableNotOperable: table {table} is prohibited for use in a predicate
        CasTableNotOperable = 10023,

        /// Picodata machinery is not yet initialized on the instance.
        Uninitialized = 10024,

        /// Instance is not allowed to be expelled in the given situation for
        /// some reason. The most often solution is to use `picodata expel --force`.
        ExpelNotAllowed = 10025,

        /// TableNotOperable: table {table} is prohibited for use in a predicate
        CasConfigNotAllowed = 10026,

        /// Raft proposal was dropped by the leader.
        RaftProposalDropped = 10027,

        /// Generic sbroad error
        SbroadError = 10028,

        /// A raft snapshot read view is not available.
        RaftSnapshotReadViewNotAvailable = 10029,

        /// Can't apply the snapshot because the local schema is not up to date yet.
        /// The schema should be propagated via tarantool replication.
        LocalSchemaNotUpToDate = 10030,

        /// Tarantool replication is broken for some reason, likely due to
        /// replication conflict.
        ReplicationBroken = 10031,

        /// Not an actual error code, just designates the start of the range.
        UserDefinedErrorCodesStart = 20000,
        // Plugin writers should use error codes in this range
    }
}

impl ErrorCode {
    /// These types of errors signify different kinds of conflicts which can
    /// occur during a [`compare_and_swap`] RPC request. If such an error
    /// happens it's safe to retry the request under the following conditions:
    /// - The raft read index operation is performed before each retry
    /// - The preconditions of the request are checked before each retry
    /// - The request is generated before each retry with an up to date raft index
    ///
    /// [`compare_and_swap`]: crate::internal::cas::compare_and_swap
    #[inline]
    pub fn is_retriable_for_cas(&self) -> bool {
        match *self {
            // Raft leader is in the middle of being changed.
            // The client should synchronize and retry the request.
            ErrorCode::LeaderUnknown
            // Raft leader has changed since the CaS request was generated.
            // The client should synchronize and retry the request.
            | ErrorCode::NotALeader
            // Raft term has changed since the CaS request was generated.
            // The client should synchronize and retry the request.
            | ErrorCode::TermMismatch
            // Raft log was compacted on the leader, so the predicate cannot be checked.
            // The client should synchronize and retry the request.
            | ErrorCode::RaftLogCompacted
            // Some raft log entries have disappeared on the leader, so the predicate cannot be checked.
            // XXX Not sure how this would happen.
            // The client should synchronize and retry the request.
            | ErrorCode::RaftLogUnavailable
            // Entry at requested index has a mismatched term in the leader's log.
            // The client should synchronize ( raft log will likely be truncated) and retry the request.
            | ErrorCode::CasEntryTermMismatch
            // Leader checked the predicate and found a conflict.
            // The client should synchronize and check the preconditions.
            | ErrorCode::CasConflictFound
            // Raft proposal was dropped by the leader.
            // The client should synchronize and retry the request.
            | ErrorCode::RaftProposalDropped
            => true,
            _ => false,
        }
    }
}

#[inline]
pub fn error_code_is_retriable_for_cas(code: u32) -> bool {
    let Ok(error_code) = ErrorCode::try_from(code) else {
        return false;
    };

    error_code.is_retriable_for_cas()
}

impl From<ErrorCode> for u32 {
    #[inline(always)]
    fn from(code: ErrorCode) -> u32 {
        code as _
    }
}

static_assert!(
    ErrorCode::MIN as u32 > TarantoolErrorCode::MAX as u32,
    "picodata error code range must not intersect tarantool's ones"
);
static_assert!(
    ErrorCode::MAX as u32 <= ErrorCode::UserDefinedErrorCodesStart as u32,
    "picodata builtin error code range must not intersect with user defined ones"
);