This commit is contained in:
Louis Dureuil 2025-04-02 15:29:47 +02:00
parent 19f4c1ac98
commit 31bda976f2
No known key found for this signature in database
5 changed files with 288 additions and 68 deletions

View File

@ -5,9 +5,10 @@ tasks affecting a single index into a [batch](crate::batch::Batch).
The main function of the autobatcher is [`next_autobatch`].
*/
use meilisearch_types::tasks::TaskId;
use std::ops::ControlFlow::{self, Break, Continue};
use meilisearch_types::tasks::{BatchStopReason, TaskId};
use crate::KindWithContent;
/// Succinctly describes a task's [`Kind`](meilisearch_types::tasks::Kind)
@ -147,14 +148,38 @@ impl BatchKind {
task_id: TaskId,
kind: KindWithContent,
primary_key: Option<&str>,
) -> (ControlFlow<BatchKind, BatchKind>, bool) {
) -> (ControlFlow<(BatchKind, BatchStopReason), BatchKind>, bool) {
use AutobatchKind as K;
match AutobatchKind::from(kind) {
K::IndexCreation => (Break(BatchKind::IndexCreation { id: task_id }), true),
K::IndexDeletion => (Break(BatchKind::IndexDeletion { ids: vec![task_id] }), false),
K::IndexUpdate => (Break(BatchKind::IndexUpdate { id: task_id }), false),
K::IndexSwap => (Break(BatchKind::IndexSwap { id: task_id }), false),
K::IndexCreation => (
Break((
BatchKind::IndexCreation { id: task_id },
BatchStopReason::TaskCannotBeBatched { kind, id: task_id },
)),
true,
),
K::IndexDeletion => (
Break((
BatchKind::IndexDeletion { ids: vec![task_id] },
BatchStopReason::TaskCannotBeBatched { kind, id: task_id },
)),
false,
),
K::IndexUpdate => (
Break((
BatchKind::IndexUpdate { id: task_id },
BatchStopReason::TaskCannotBeBatched { kind, id: task_id },
)),
false,
),
K::IndexSwap => (
Break((
BatchKind::IndexSwap { id: task_id },
BatchStopReason::TaskCannotBeBatched { kind, id: task_id },
)),
false,
),
K::DocumentClear => (Continue(BatchKind::DocumentClear { ids: vec![task_id] }), false),
K::DocumentImport { allow_index_creation, primary_key: pk }
if primary_key.is_none() || pk.is_none() || primary_key == pk.as_deref() =>
@ -169,15 +194,28 @@ impl BatchKind {
)
}
// if the primary key set in the task was different than ours we should stop and make this batch fail asap.
K::DocumentImport { allow_index_creation, primary_key } => (
Break(BatchKind::DocumentOperation {
K::DocumentImport { allow_index_creation, primary_key: pk } => (
Break((
BatchKind::DocumentOperation {
allow_index_creation,
primary_key,
primary_key: pk,
operation_ids: vec![task_id],
}),
},
BatchStopReason::PrimaryKeyIndexMismatch {
id: task_id,
in_index: primary_key.unwrap().to_owned(),
in_task: pk.unwrap(),
},
)),
allow_index_creation,
),
K::DocumentEdition => (Break(BatchKind::DocumentEdition { id: task_id }), false),
K::DocumentEdition => (
Break((
BatchKind::DocumentEdition { id: task_id },
BatchStopReason::TaskCannotBeBatched { kind, id: task_id },
)),
false,
),
K::DocumentDeletion { by_filter: includes_by_filter } => (
Continue(BatchKind::DocumentDeletion {
deletion_ids: vec![task_id],
@ -197,43 +235,40 @@ impl BatchKind {
/// To ease the writing of the code. `true` can be returned when you don't need to create an index
/// but false can't be returned if you needs to create an index.
#[rustfmt::skip]
fn accumulate(self, id: TaskId, kind: AutobatchKind, index_already_exists: bool, primary_key: Option<&str>) -> ControlFlow<BatchKind, BatchKind> {
fn accumulate(self, id: TaskId, kind: AutobatchKind, index_already_exists: bool, primary_key: Option<&str>) -> ControlFlow<(BatchKind, BatchStopReason), BatchKind> {
use AutobatchKind as K;
let pk: Option<String> = match (self.primary_key(), kind.primary_key(), primary_key) {
// 1. If both task don't interact with primary key -> we can continue
(batch_pk, None | Some(None), _) => {
batch_pk.flatten().to_owned()
},
// 2.1 If we already have a primary-key ->
// 2.1.1 If the task we're trying to accumulate have a pk it must be equal to our primary key
(batch_pk, Some(Some(task_pk)), Some(index_pk)) => if task_pk == index_pk {
Some(task_pk.to_owned())
} else {
return Break((this, BatchStopReason::PrimaryKeyMismatch { id, batch_pk: todo!(), task_pk: todo!() }))
},
// 2.2 If we don't have a primary-key ->
// 2.2.2 If the batch is set to Some(None), the task should be too
(Some(None), Some(None), None) => None,
(Some(None), Some(Some(_)), None) => return Break((this, BatchStopReason::PrimaryKeyMismatch { id, batch_pk: todo!(), task_pk: todo!() })),
(Some(Some(batch_pk)), Some(None), None) => Some(batch_pk.to_owned()),
(Some(Some(batch_pk)), Some(Some(task_pk)), None) => if task_pk == batch_pk {
Some(task_pk.to_owned())
} else {
return Break((this, BatchStopReason::PrimaryKeyMismatch { id, batch_pk: todo!(), task_pk: todo!() }))
},
(None, Some(Some(task_pk)), None) => Some(task_pk.to_owned())
};
match (self, kind) {
// We don't batch any of these operations
(this, K::IndexCreation | K::IndexUpdate | K::IndexSwap | K::DocumentEdition) => Break(this),
(this, K::IndexCreation | K::IndexUpdate | K::IndexSwap | K::DocumentEdition) => Break((this, BatchStopReason::TaskCannotBeBatched { kind, id })),
// We must not batch tasks that don't have the same index creation rights if the index doesn't already exists.
(this, kind) if !index_already_exists && this.allow_index_creation() == Some(false) && kind.allow_index_creation() == Some(true) => {
Break(this)
},
// NOTE: We need to negate the whole condition since we're checking if we need to break instead of continue.
// I wrote it this way because it's easier to understand than the other way around.
(this, kind) if !(
// 1. If both task don't interact with primary key -> we can continue
(this.primary_key().is_none() && kind.primary_key().is_none()) ||
// 2. Else ->
(
// 2.1 If we already have a primary-key ->
(
primary_key.is_some() &&
// 2.1.1 If the task we're trying to accumulate have a pk it must be equal to our primary key
// 2.1.2 If the task don't have a primary-key -> we can continue
kind.primary_key().is_none_or(|pk| pk == primary_key)
) ||
// 2.2 If we don't have a primary-key ->
(
// 2.2.1 If both the batch and the task have a primary key they should be equal
// 2.2.2 If the batch is set to Some(None), the task should be too
// 2.2.3 If the batch is set to None -> we can continue
this.primary_key().zip(kind.primary_key()).map_or(true, |(this, kind)| this == kind)
)
)
) // closing the negation
=> {
Break(this)
Break((this, BatchStopReason::IndexCreationMismatch { id }))
},
// The index deletion can batch with everything but must stop after
(
@ -244,7 +279,7 @@ impl BatchKind {
K::IndexDeletion,
) => {
ids.push(id);
Break(BatchKind::IndexDeletion { ids })
Break((BatchKind::IndexDeletion { ids }, BatchStopReason::IndexDeletion { id }))
}
(
BatchKind::ClearAndSettings { settings_ids: mut ids, allow_index_creation: _, mut other },
@ -252,7 +287,7 @@ impl BatchKind {
) => {
ids.push(id);
ids.append(&mut other);
Break(BatchKind::IndexDeletion { ids })
Break((BatchKind::IndexDeletion { ids }, BatchStopReason::IndexDeletion { id }))
}
(
@ -265,7 +300,7 @@ impl BatchKind {
(
this @ BatchKind::DocumentClear { .. },
K::DocumentImport { .. } | K::Settings { .. },
) => Break(this),
) => Break((this, BatchStopReason::DocumentOperationWithSettings { id })),
(
BatchKind::DocumentOperation { allow_index_creation: _, primary_key: _, mut operation_ids },
K::DocumentClear,
@ -277,7 +312,7 @@ impl BatchKind {
// we can autobatch different kind of document operations and mix replacements with updates
(
BatchKind::DocumentOperation { allow_index_creation, primary_key: _, mut operation_ids },
K::DocumentImport { primary_key: pk, .. },
K::DocumentImport { primary_key, .. },
) => {
operation_ids.push(id);
Continue(BatchKind::DocumentOperation {
@ -287,15 +322,15 @@ impl BatchKind {
})
}
(
BatchKind::DocumentOperation { allow_index_creation, primary_key, mut operation_ids },
BatchKind::DocumentOperation { allow_index_creation, primary_key: _, mut operation_ids },
K::DocumentDeletion { by_filter: false },
) => {
operation_ids.push(id);
Continue(BatchKind::DocumentOperation {
allow_index_creation,
primary_key,
operation_ids,
primary_key: pk,
})
}
// We can't batch a document operation with a delete by filter
@ -303,12 +338,12 @@ impl BatchKind {
this @ BatchKind::DocumentOperation { .. },
K::DocumentDeletion { by_filter: true },
) => {
Break(this)
Break((this, BatchStopReason::DocumentOperationWithDeletionByFilter { id }))
}
(
this @ BatchKind::DocumentOperation { .. },
K::Settings { .. },
) => Break(this),
) => Break((this, BatchStopReason::DocumentOperationWithSettings { id })),
(BatchKind::DocumentDeletion { mut deletion_ids, includes_by_filter: _ }, K::DocumentClear) => {
deletion_ids.push(id);
@ -318,7 +353,7 @@ impl BatchKind {
(
this @ BatchKind::DocumentDeletion { deletion_ids: _, includes_by_filter: true },
K::DocumentImport { .. }
) => Break(this),
) => Break((this, BatchStopReason::DeletionByFilterWithDocumentOperation { id })),
// we can autobatch the deletion and import if the index already exists
(
BatchKind::DocumentDeletion { mut deletion_ids, includes_by_filter: false },
@ -345,18 +380,18 @@ impl BatchKind {
operation_ids: deletion_ids,
})
}
// we can't autobatch a deletion and an import if the index does not exists but would be created by an addition
// we can't autobatch a deletion and an import if the index does not exist but would be created by an addition
(
this @ BatchKind::DocumentDeletion { .. },
K::DocumentImport { .. }
) => {
Break(this)
Break((this, BatchStopReason::IndexCreationMismatch { id }))
}
(BatchKind::DocumentDeletion { mut deletion_ids, includes_by_filter }, K::DocumentDeletion { by_filter }) => {
deletion_ids.push(id);
Continue(BatchKind::DocumentDeletion { deletion_ids, includes_by_filter: includes_by_filter | by_filter })
}
(this @ BatchKind::DocumentDeletion { .. }, K::Settings { .. }) => Break(this),
(this @ BatchKind::DocumentDeletion { .. }, K::Settings { .. }) => Break((this, BatchStopReason::DocumentOperationWithSettings { id })),
(
BatchKind::Settings { settings_ids, allow_index_creation },
@ -369,7 +404,7 @@ impl BatchKind {
(
this @ BatchKind::Settings { .. },
K::DocumentImport { .. } | K::DocumentDeletion { .. },
) => Break(this),
) => Break((this, BatchStopReason::SettingsWithDocumentOperation { id })),
(
BatchKind::Settings { mut settings_ids, allow_index_creation },
K::Settings { .. },
@ -448,7 +483,7 @@ pub fn autobatch(
enqueued: Vec<(TaskId, KindWithContent)>,
index_already_exists: bool,
primary_key: Option<&str>,
) -> Option<(BatchKind, bool)> {
) -> Option<(BatchKind, bool, Option<BatchStopReason>)> {
let mut enqueued = enqueued.into_iter();
let (id, kind) = enqueued.next()?;
@ -457,7 +492,9 @@ pub fn autobatch(
let (mut acc, must_create_index) = match BatchKind::new(id, kind, primary_key) {
(Continue(acc), create) => (acc, create),
(Break(acc), create) => return Some((acc, create)),
(Break((acc, batch_stop_reason)), create) => {
return Some((acc, create, Some(batch_stop_reason)))
}
};
// if an index has been created in the previous step we can consider it as existing.
@ -466,9 +503,11 @@ pub fn autobatch(
for (id, kind) in enqueued {
acc = match acc.accumulate(id, kind.into(), index_exist, primary_key) {
Continue(acc) => acc,
Break(acc) => return Some((acc, must_create_index)),
Break((acc, batch_stop_reason)) => {
return Some((acc, must_create_index, Some(batch_stop_reason)))
}
};
}
Some((acc, must_create_index))
Some((acc, must_create_index, None))
}

View File

@ -3,7 +3,7 @@ use std::fmt;
use meilisearch_types::heed::RoTxn;
use meilisearch_types::milli::update::IndexDocumentsMethod;
use meilisearch_types::settings::{Settings, Unchecked};
use meilisearch_types::tasks::{Kind, KindWithContent, Status, Task};
use meilisearch_types::tasks::{BatchStopReason, Kind, KindWithContent, Status, Task};
use roaring::RoaringBitmap;
use uuid::Uuid;
@ -440,6 +440,7 @@ impl IndexScheduler {
let mut current_batch = ProcessingBatch::new(batch_id);
let enqueued = &self.queue.tasks.get_status(rtxn, Status::Enqueued)?;
let count_total_enqueued = enqueued.len();
let failed = &self.queue.tasks.get_status(rtxn, Status::Failed)?;
// 0. The priority over everything is to upgrade the instance
@ -453,6 +454,10 @@ impl IndexScheduler {
current_batch.uid = batch_uid;
}
current_batch.processing(&mut tasks);
current_batch.reason(BatchStopReason::TaskCannotBeBatched {
kind: Kind::UpgradeDatabase,
id: tasks.last().unwrap(),
});
return Ok(Some((Batch::UpgradeDatabase { tasks }, current_batch)));
}
@ -462,6 +467,10 @@ impl IndexScheduler {
let mut task =
self.queue.tasks.get_task(rtxn, task_id)?.ok_or(Error::CorruptedTaskQueue)?;
current_batch.processing(Some(&mut task));
current_batch.reason(BatchStopReason::TaskCannotBeBatched {
kind: Kind::TaskCancelation,
id: tasks.last().unwrap(),
});
return Ok(Some((Batch::TaskCancelation { task }, current_batch)));
}
@ -470,6 +479,10 @@ impl IndexScheduler {
if !to_delete.is_empty() {
let mut tasks = self.queue.tasks.get_existing_tasks(rtxn, to_delete)?;
current_batch.processing(&mut tasks);
current_batch.reason(BatchStopReason::TaskCannotBeBatched {
kind: Kind::TaskDeletion,
id: tasks.last().unwrap(),
});
return Ok(Some((Batch::TaskDeletions(tasks), current_batch)));
}
@ -478,6 +491,10 @@ impl IndexScheduler {
if !to_snapshot.is_empty() {
let mut tasks = self.queue.tasks.get_existing_tasks(rtxn, to_snapshot)?;
current_batch.processing(&mut tasks);
current_batch.reason(BatchStopReason::TaskCannotBeBatched {
kind: Kind::SnapshotCreation,
id: tasks.last().unwrap(),
});
return Ok(Some((Batch::SnapshotCreation(tasks), current_batch)));
}
@ -487,6 +504,10 @@ impl IndexScheduler {
let mut task =
self.queue.tasks.get_task(rtxn, to_dump)?.ok_or(Error::CorruptedTaskQueue)?;
current_batch.processing(Some(&mut task));
current_batch.reason(BatchStopReason::TaskCannotBeBatched {
kind: Kind::DumpCreation,
id: tasks.last().unwrap(),
});
return Ok(Some((Batch::Dump(task), current_batch)));
}
@ -504,6 +525,10 @@ impl IndexScheduler {
} else {
assert!(matches!(&task.kind, KindWithContent::IndexSwap { swaps } if swaps.is_empty()));
current_batch.processing(Some(&mut task));
current_batch.reason(BatchStopReason::TaskCannotBeBatched {
kind: Kind::IndexSwap,
id: tasks.last().unwrap(),
});
return Ok(Some((Batch::IndexSwap { task }, current_batch)));
};
@ -525,9 +550,14 @@ impl IndexScheduler {
1
};
let mut stop_reason = BatchStopReason::default();
let mut enqueued = Vec::new();
let mut total_size: u64 = 0;
for task_id in index_tasks.into_iter().take(tasks_limit) {
for task_id in index_tasks.into_iter() {
if enqueued.len() >= task_limit {
stop_reason = BatchStopReason::ReachedTaskLimit { task_limit };
break;
}
let task = self
.queue
.tasks
@ -539,16 +569,27 @@ impl IndexScheduler {
total_size = total_size.saturating_add(content_size);
}
if total_size > self.scheduler.batched_tasks_size_limit && !enqueued.is_empty() {
let size_limit = self.scheduler.batched_tasks_size_limit;
if total_size > size_limit && !enqueued.is_empty() {
stop_reason = BatchStopReason::ReachedSizeLimit { size_limit, size: total_size };
break;
}
enqueued.push((task.uid, task.kind));
}
if let Some((batchkind, create_index)) =
stop_reason.replace_unspecified({
if enqueued.len() == count_total_enqueued as usize {
BatchStopReason::ExhaustedEnqueuedTasks
} else {
BatchStopReason::ExhaustedEnqueuedTasksForIndex { index: index_name.to_owned() }
}
});
if let Some((batchkind, create_index, autobatch_stop_reason)) =
autobatcher::autobatch(enqueued, index_already_exists, primary_key.as_deref())
{
current_batch.reason(autobatch_stop_reason.unwrap_or(stop_reason));
return Ok(self
.create_next_batch_index(
rtxn,

View File

@ -7,7 +7,9 @@ use meilisearch_types::batches::{Batch, BatchEnqueuedAt, BatchId, BatchStats};
use meilisearch_types::heed::{Database, RoTxn, RwTxn};
use meilisearch_types::milli::CboRoaringBitmapCodec;
use meilisearch_types::task_view::DetailsView;
use meilisearch_types::tasks::{Details, IndexSwap, Kind, KindWithContent, Status};
use meilisearch_types::tasks::{
BatchStopReason, Details, IndexSwap, Kind, KindWithContent, Status,
};
use roaring::RoaringBitmap;
use time::OffsetDateTime;
@ -33,6 +35,7 @@ pub struct ProcessingBatch {
pub enqueued_at: Option<BatchEnqueuedAt>,
pub started_at: OffsetDateTime,
pub finished_at: Option<OffsetDateTime>,
pub reason: BatchStopReason,
}
impl ProcessingBatch {
@ -53,6 +56,7 @@ impl ProcessingBatch {
enqueued_at: None,
started_at: OffsetDateTime::now_utc(),
finished_at: None,
reason: Default::default(),
}
}
@ -93,6 +97,10 @@ impl ProcessingBatch {
}
}
pub fn reason(&mut self, reason: BatchStopReason) {
self.reason = reason;
}
/// Must be called once the batch has finished processing.
pub fn finished(&mut self) {
self.details = DetailsView::default();
@ -141,6 +149,7 @@ impl ProcessingBatch {
started_at: self.started_at,
finished_at: self.finished_at,
enqueued_at: self.enqueued_at,
stop_reason: self.reason.to_string(),
}
}
}

View File

@ -6,7 +6,7 @@ use time::OffsetDateTime;
use utoipa::ToSchema;
use crate::task_view::DetailsView;
use crate::tasks::{Kind, Status};
use crate::tasks::{BatchStopReason, Kind, Status};
pub type BatchId = u32;
@ -28,11 +28,26 @@ pub struct Batch {
// Enqueued at is never displayed and is only required when removing a batch.
// It's always some except when upgrading from a database pre v1.12
pub enqueued_at: Option<BatchEnqueuedAt>,
#[serde(default = "default_stop_reason")]
pub stop_reason: String,
}
fn default_stop_reason() -> String {
BatchStopReason::default().to_string()
}
impl PartialEq for Batch {
fn eq(&self, other: &Self) -> bool {
let Self { uid, progress, details, stats, started_at, finished_at, enqueued_at } = self;
let Self {
uid,
progress,
details,
stats,
started_at,
finished_at,
enqueued_at,
stop_reason,
} = self;
*uid == other.uid
&& progress.is_none() == other.progress.is_none()
@ -41,6 +56,7 @@ impl PartialEq for Batch {
&& started_at == &other.started_at
&& finished_at == &other.finished_at
&& enqueued_at == &other.enqueued_at
&& stop_reason == &other.stop_reason
}
}

View File

@ -675,6 +675,121 @@ impl Details {
}
}
#[derive(Default, Debug, Clone)]
pub enum BatchStopReason {
#[default]
Unspecified,
TaskCannotBeBatched {
kind: Kind,
id: TaskId,
},
ExhaustedEnqueuedTasks,
ExhaustedEnqueuedTasksForIndex {
index: String,
},
ReachedTaskLimit {
task_limit: usize,
},
ReachedSizeLimit {
size_limit: usize,
size: usize,
},
PrimaryKeyIndexMismatch {
id: TaskId,
in_index: String,
in_task: String,
},
IndexCreationMismatch {
id: TaskId,
},
PrimaryKeyMismatch {
id: TaskId,
batch_pk: Option<String>,
task_pk: Option<String>,
},
IndexDeletion {
id: TaskId,
},
DocumentOperationWithSettings {
id: TaskId,
},
DocumentOperationWithDeletionByFilter {
id: TaskId,
},
DeletionByFilterWithDocumentOperation {
id: TaskId,
},
SettingsWithDocumentOperation {
id: TaskId,
},
}
impl BatchStopReason {
pub fn replace_unspecified(&mut self, new: BatchStopReason) {
if let BatchStopReason::Unspecified = self {
*self = new;
}
}
}
pub enum PrimaryKeyMismatchReason {}
impl Display for BatchStopReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BatchStopReason::Unspecified => f.write_str("unspecified"),
BatchStopReason::TaskCannotBeBatched { kind, id } => {
write!(f, "task with id {id} of type `{kind}` cannot be batched")
}
BatchStopReason::ExhaustedEnqueuedTasks => f.write_str("batched all enqueued tasks"),
BatchStopReason::ExhaustedEnqueuedTasksForIndex { index } => {
write!(f, "batched all enqueued tasks for index `{index}`")
}
BatchStopReason::ReachedTaskLimit { task_limit } => {
write!(f, "reached configured batch limit of {task_limit} tasks")
}
BatchStopReason::ReachedSizeLimit { size_limit, size } => write!(
f,
"reached configured batch size limit of {size_limit}B with a total of {size}B"
),
BatchStopReason::PrimaryKeyIndexMismatch { id, in_index, in_task } => {
write!(f, "primary key `{in_task}` in task with id {id} is different from the primary key of the index `{in_index}`")
}
BatchStopReason::IndexCreationMismatch { id } => {
write!(f, "task with id {id} has different index creation rules as in the batch")
}
BatchStopReason::PrimaryKeyMismatch { id, batch_pk, task_pk } => {}
BatchStopReason::IndexDeletion { id } => {
write!(f, "task with id {id} deletes the index")
}
BatchStopReason::DocumentOperationWithSettings { id } => {
write!(
f,
"task with id {id} is a settings change in a batch of document operations"
)
}
BatchStopReason::DocumentOperationWithDeletionByFilter { id } => {
write!(
f,
"task with id {id} is a deletion by filter in a batch of document operations"
)
}
BatchStopReason::DeletionByFilterWithDocumentOperation { id } => {
write!(
f,
"task with id {id} is a document operation in a batch of deletions by filter"
)
}
BatchStopReason::SettingsWithDocumentOperation { id } => {
write!(
f,
"task with id {id} is a document operation in a batch of settings changes"
)
}
}
}
}
/// Serialize a `time::Duration` as a best effort ISO 8601 while waiting for
/// https://github.com/time-rs/time/issues/378.
/// This code is a port of the old code of time that was removed in 0.2.