use std::fmt::Display; use meilisearch_types::error::{Code, ErrorCode}; use meilisearch_types::tasks::{Kind, Status}; use meilisearch_types::{heed, milli}; use thiserror::Error; use crate::TaskId; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum DateField { BeforeEnqueuedAt, AfterEnqueuedAt, BeforeStartedAt, AfterStartedAt, BeforeFinishedAt, AfterFinishedAt, } impl Display for DateField { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { DateField::BeforeEnqueuedAt => write!(f, "beforeEnqueuedAt"), DateField::AfterEnqueuedAt => write!(f, "afterEnqueuedAt"), DateField::BeforeStartedAt => write!(f, "beforeStartedAt"), DateField::AfterStartedAt => write!(f, "afterStartedAt"), DateField::BeforeFinishedAt => write!(f, "beforeFinishedAt"), DateField::AfterFinishedAt => write!(f, "afterFinishedAt"), } } } impl From for Code { fn from(date: DateField) -> Self { match date { DateField::BeforeEnqueuedAt => Code::InvalidTaskBeforeEnqueuedAt, DateField::AfterEnqueuedAt => Code::InvalidTaskAfterEnqueuedAt, DateField::BeforeStartedAt => Code::InvalidTaskBeforeStartedAt, DateField::AfterStartedAt => Code::InvalidTaskAfterStartedAt, DateField::BeforeFinishedAt => Code::InvalidTaskBeforeFinishedAt, DateField::AfterFinishedAt => Code::InvalidTaskAfterFinishedAt, } } } #[allow(clippy::large_enum_variant)] #[derive(Error, Debug)] pub enum Error { #[error("{1}")] WithCustomErrorCode(Code, Box), #[error("Index `{0}` not found.")] IndexNotFound(String), #[error("Index `{0}` already exists.")] IndexAlreadyExists(String), #[error( "Indexes must be declared only once during a swap. `{0}` was specified several times." )] SwapDuplicateIndexFound(String), #[error( "Indexes must be declared only once during a swap. {} were specified several times.", .0.iter().map(|s| format!("`{}`", s)).collect::>().join(", ") )] SwapDuplicateIndexesFound(Vec), #[error("Index `{0}` not found.")] SwapIndexNotFound(String), #[error("Meilisearch cannot receive write operations because the limit of the task database has been reached. Please delete tasks to continue performing write operations.")] NoSpaceLeftInTaskQueue, #[error( "Indexes {} not found.", .0.iter().map(|s| format!("`{}`", s)).collect::>().join(", ") )] SwapIndexesNotFound(Vec), #[error("Corrupted dump.")] CorruptedDump, #[error( "Task `{field}` `{date}` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format." )] InvalidTaskDate { field: DateField, date: String }, #[error("Task uid `{task_uid}` is invalid. It should only contain numeric characters.")] InvalidTaskUids { task_uid: String }, #[error( "Task status `{status}` is invalid. Available task statuses are {}.", enum_iterator::all::() .map(|s| format!("`{s}`")) .collect::>() .join(", ") )] InvalidTaskStatuses { status: String }, #[error( "Task type `{type_}` is invalid. Available task types are {}", enum_iterator::all::() .map(|s| format!("`{s}`")) .collect::>() .join(", ") )] InvalidTaskTypes { type_: String }, #[error( "Task canceledBy `{canceled_by}` is invalid. It should only contains numeric characters separated by `,` character." )] InvalidTaskCanceledBy { canceled_by: String }, #[error( "{index_uid} is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_)." )] InvalidIndexUid { index_uid: String }, #[error("Task `{0}` not found.")] TaskNotFound(TaskId), #[error("Query parameters to filter the tasks to delete are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `canceledBy`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.")] TaskDeletionWithEmptyQuery, #[error("Query parameters to filter the tasks to cancel are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `canceledBy`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.")] TaskCancelationWithEmptyQuery, #[error(transparent)] Dump(#[from] dump::Error), #[error(transparent)] Heed(#[from] heed::Error), #[error(transparent)] Milli(#[from] milli::Error), #[error("An unexpected crash occurred when processing the task.")] ProcessBatchPanicked, #[error(transparent)] FileStore(#[from] file_store::Error), #[error(transparent)] IoError(#[from] std::io::Error), #[error(transparent)] Persist(#[from] tempfile::PersistError), #[error(transparent)] FeatureNotEnabled(#[from] FeatureNotEnabledError), #[error(transparent)] Anyhow(#[from] anyhow::Error), // Irrecoverable errors: #[error(transparent)] CreateBatch(Box), #[error("Corrupted task queue.")] CorruptedTaskQueue, #[error(transparent)] TaskDatabaseUpdate(Box), #[error(transparent)] HeedTransaction(heed::Error), #[cfg(test)] #[error("Planned failure for tests.")] PlannedFailure, } #[derive(Debug, thiserror::Error)] #[error( "{disabled_action} requires enabling the `{feature}` experimental feature. See {issue_link}" )] pub struct FeatureNotEnabledError { pub disabled_action: &'static str, pub feature: &'static str, pub issue_link: &'static str, } impl Error { pub fn is_recoverable(&self) -> bool { match self { Error::IndexNotFound(_) | Error::WithCustomErrorCode(_, _) | Error::IndexAlreadyExists(_) | Error::SwapDuplicateIndexFound(_) | Error::SwapDuplicateIndexesFound(_) | Error::SwapIndexNotFound(_) | Error::NoSpaceLeftInTaskQueue | Error::SwapIndexesNotFound(_) | Error::CorruptedDump | Error::InvalidTaskDate { .. } | Error::InvalidTaskUids { .. } | Error::InvalidTaskStatuses { .. } | Error::InvalidTaskTypes { .. } | Error::InvalidTaskCanceledBy { .. } | Error::InvalidIndexUid { .. } | Error::TaskNotFound(_) | Error::TaskDeletionWithEmptyQuery | Error::TaskCancelationWithEmptyQuery | Error::Dump(_) | Error::Heed(_) | Error::Milli(_) | Error::ProcessBatchPanicked | Error::FileStore(_) | Error::IoError(_) | Error::Persist(_) | Error::FeatureNotEnabled(_) | Error::Anyhow(_) => true, Error::CreateBatch(_) | Error::CorruptedTaskQueue | Error::TaskDatabaseUpdate(_) | Error::HeedTransaction(_) => false, #[cfg(test)] Error::PlannedFailure => false, } } pub fn with_custom_error_code(self, code: Code) -> Self { Self::WithCustomErrorCode(code, Box::new(self)) } } impl ErrorCode for Error { fn error_code(&self) -> Code { match self { Error::WithCustomErrorCode(code, _) => *code, Error::IndexNotFound(_) => Code::IndexNotFound, Error::IndexAlreadyExists(_) => Code::IndexAlreadyExists, Error::SwapDuplicateIndexesFound(_) => Code::InvalidSwapDuplicateIndexFound, Error::SwapDuplicateIndexFound(_) => Code::InvalidSwapDuplicateIndexFound, Error::SwapIndexNotFound(_) => Code::IndexNotFound, Error::SwapIndexesNotFound(_) => Code::IndexNotFound, Error::InvalidTaskDate { field, .. } => (*field).into(), Error::InvalidTaskUids { .. } => Code::InvalidTaskUids, Error::InvalidTaskStatuses { .. } => Code::InvalidTaskStatuses, Error::InvalidTaskTypes { .. } => Code::InvalidTaskTypes, Error::InvalidTaskCanceledBy { .. } => Code::InvalidTaskCanceledBy, Error::InvalidIndexUid { .. } => Code::InvalidIndexUid, Error::TaskNotFound(_) => Code::TaskNotFound, Error::TaskDeletionWithEmptyQuery => Code::MissingTaskFilters, Error::TaskCancelationWithEmptyQuery => Code::MissingTaskFilters, // TODO: not sure of the Code to use Error::NoSpaceLeftInTaskQueue => Code::NoSpaceLeftOnDevice, Error::Dump(e) => e.error_code(), Error::Milli(e) => e.error_code(), Error::ProcessBatchPanicked => Code::Internal, Error::Heed(e) => e.error_code(), Error::HeedTransaction(e) => e.error_code(), Error::FileStore(e) => e.error_code(), Error::IoError(e) => e.error_code(), Error::Persist(e) => e.error_code(), Error::FeatureNotEnabled(_) => Code::FeatureNotEnabled, // Irrecoverable errors Error::Anyhow(_) => Code::Internal, Error::CorruptedTaskQueue => Code::Internal, Error::CorruptedDump => Code::Internal, Error::TaskDatabaseUpdate(_) => Code::Internal, Error::CreateBatch(_) => Code::Internal, #[cfg(test)] Error::PlannedFailure => Code::Internal, } } }