Meilisearch index synchronization can run concurrently on multiple API replicas #15461
Replies: 2 comments 2 replies
|
The guard in That handler can rewrite Meilisearch settings, flip Claim the job with something atomic, then run the handler:
|
|
The NX-lock direction is right, but I'd reach for Mongo before Redis here, since Mongo is the one store both replicas are guaranteed to share (LibreChat needs it to run at all; Redis is only in the picture if you've configured it as the cache/pubsub backend). You can get the atomic claim out of the same collection const claimed = await FlowState.findOneAndUpdate(
{
flowId,
$or: [
{ status: { $exists: false } },
{ status: 'FAILED' },
{ status: 'PENDING', lastHeartbeat: { $lt: new Date(Date.now() - STALE_MS) } },
],
},
{ $set: { status: 'PENDING', owner: replicaId, lastHeartbeat: new Date() } },
{ upsert: true, returnDocument: 'after' }
);
if (claimed.owner !== replicaId) return; // someone else already owns this flowThat collapses the get, sleep, get, set into a single conditional write, so there's no window where two replicas both observe "no flow." Whoever loses the The part neither the original report nor a plain TTL lock handles well is that this specific job can run long, 95k messages in the log you posted. A lock TTL picked once at claim time is wrong either direction: long enough to survive a real full sync, and a crashed replica's lock lingers for the whole window before anyone can retry; short enough to fail over fast, and a slow-but-healthy sync gets preempted mid-run by a second replica that thinks the first one died. The fix is to stop treating staleness as "time since claim" and instead track "time since last heartbeat." The owning replica updates |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
What happened?
When two or more API replicas start against the same MongoDB and Meilisearch instances, more than one replica can execute
api/db/indexSync.jsconcurrently.indexSync()coordinates throughFlowStateManager.createFlowWithHandler(). That method performs aget, waits 250 ms, performs anotherget, and then writes the initialPENDINGstate. The final claim is not atomic, so replicas starting together can all observe no flow and execute the handler.The handler can update shared Meilisearch settings, reset MongoDB
_meiliIndexflags, clean indexes, and start full message/conversation indexing. Concurrent handlers therefore duplicate expensive work and can reset progress while another replica is indexing.Expected: only one API replica performs the cluster-wide Meilisearch reconciliation.
Actual: synchronized replicas can all enter the reconciliation handler.
Version Information
Confirmed in current
devcommit8f6228095fcf118ebe4ee28fd960638f0820704b. The relevant non-atomic claim is present inpackages/api/src/flow/manager.ts, andapi/db/indexSync.jsuses it as the concurrency guard.Steps to Reproduce
SEARCH=true, sharing MongoDB, Redis, and Meilisearch._meiliIndex != true, or index settings require an update).createFlowWithHandler()before either initial state write becomes visible.[indexSync] Starting index synchronization check.../ sync-start log sequences and concurrent writes to the shared index state.A deterministic unit reproduction can use two
FlowStateManagerinstances over the same delayed store and pause both calls after their secondget; both proceed tosetand execute their handlers.What browsers are you seeing the problem on?
No response — backend concurrency issue.
Relevant log output
Code of Conduct
I agree to follow this project's Code of Conduct.
Proposed fix
The fix is implemented in #15465.
All reactions