feat: Move Native Embedder and Reranker Into Isolated Workers w/ Job Queues | Add Document Embedding Status Events#5192
feat: Move Native Embedder and Reranker Into Isolated Workers w/ Job Queues | Add Document Embedding Status Events#5192angelplusultra wants to merge 45 commits intomasterfrom
Conversation
…r embedding progress
The SSE connection opens before the embedding API call fires, so the server sees no buffered history and immediately sends all_complete. Firefox dispatches this eagerly enough that it closes the EventSource before real progress events arrive, causing the progress UI to clear and fall back to the loading spinner. Chrome's EventSource timing masks the race. Track slugs where startEmbedding was called but no real progress event has arrived yet via awaitingProgressRef. Ignore the first all_complete for those slugs and keep the connection open for the real events.
Removed unnecessary tracking of slugs for premature all_complete events in the EmbeddingProgressProvider. Updated the server-side logic to avoid sending all_complete when no embedding is in progress, allowing the connection to remain open for real events. Adjusted the embedding initiation flow to ensure the server processes the job before the SSE connection opens, improving the reliability of progress updates.
…component Extracted the Reranking Worker Idle Timeout input from GeneralEmbeddingPreference and integrated it into the LanceDBOptions component. This change enhances modularity and maintains a cleaner structure for the settings interface.
timothycarambat
left a comment
There was a problem hiding this comment.
-
Timeout ≠ TTL. This looks like we are talking about how long we should keep workers alive after doing some work just to keep the worker hot. This is a TTL, so lets rename that.
-
Lets remove the UI components to specify TTL for now. Most people will not ever want to touch these nor want to even change it. Node Worker time to start is small enough to shoulder here.
-
Lets also then remove the associated
systemSettingskey entires since we wont be sending them to the UI. We should keep theprotectedKeysin dumpENV just in case people DO want to set them manually.
Clarifying questions:
- How are embedding jobs user segmented? If user A is embedding 10 docs and user B is embedding 1 - when A is queued and B's job finishes and gets
all_completedoesnt A get that event back at the same time or is this simply based on the job ref in their renderer processes
frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx
Outdated
Show resolved
Hide resolved
frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx
Outdated
Show resolved
Hide resolved
frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx
Outdated
Show resolved
Hide resolved
frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx
Show resolved
Hide resolved
| module.exports = { | ||
| queueEmbedding, | ||
| queueReranking, | ||
| embeddingProgressBus, | ||
| }; |
There was a problem hiding this comment.
For this whole file, lets find a better way to break this up - this file is pretty messy and lots of different things going on in the same file.
Each user's "Save" triggers its own You can see this in action in the /**
* Register an SSE listener filtered by workspace and user.
* Replays any buffered events for the workspace before subscribing to live events.
* @param {{ workspaceSlug: string, userId?: number }} filter
* @param {function} callback - receives the progress event payload
* @returns {{ unsubscribe: function }}
*/
subscribe(filter, callback) {
// Replay buffered events so reconnecting clients catch up.
if (filter.workspaceSlug && this.#history.has(filter.workspaceSlug)) {
for (const event of this.#history.get(filter.workspaceSlug)) {
if (filter.userId && event.userId && event.userId !== filter.userId)
continue;
callback(event);
}
}
const handler = (event) => {
if (filter.workspaceSlug && event.workspaceSlug !== filter.workspaceSlug)
return;
if (filter.userId && event.userId && event.userId !== filter.userId)
return;
callback(event);
};
this.on("progress", handler);
return {
unsubscribe: () => this.off("progress", handler),
};
} |
…d of native EventSource API.
…embedding progress SSE
…ed by workers instead of process.send checks
Pull Request Type
Relevant Issues
resolves #
Description
Moves the native embedder and reranker from the main process into isolated child processes using
child_process.fork()with a serial job queue. This prevents OOM crashes from large document batches from taking down the entire server, while adding real-time document-level progress reporting to the UI via SSE.Architecture:
WorkerQueueclass manages a forked child process with serial FIFO job processing, auto-fork on first job, and configurable idle timeout that kills the worker when inactiveEmbeddingProgressBus(singleton EventEmitter) acts as the central hub for progress events betweenDocument.addDocumentsand SSE endpoint listeners, with event buffering for late-joining clients after page reloadsProgress Reporting:
/workspace/:slug/embed-progressstreams document-level events (batch_starting,doc_starting,doc_complete,doc_failed,all_complete)EmbeddingProgressContext(React Context) manages progress state globally and connects SSE on component mount — server-side event replay catches up on any in-progress jobs without needing client-side persistenceWorker Timeouts:
NATIVE_EMBEDDING_WORKER_TIMEOUT,NATIVE_RERANKING_WORKER_TIMEOUT) with defaults of 300s and 900s respectivelyOther Changes:
NativeEmbedder.embedChunks()detects whether it's running in the main process or worker viaprocess.sendand routes through the worker queue automatically — no changes needed in vector DB providers or non-native embedding enginesNativeEmbeddingReranker.rerankViaWorker()added as static method to route through the queue from LanceDB, using the sameprocess.senddetection patternbatch_startingevent emitted at the start of a batch with the full file list, so SSE history replay can seed all files as "pending" for late-joining clientsdoc_failedevent emitted whenfileData()returns null or when the database write fails (previously files were silently skipped, stuck as "pending" in UI)Visuals (if applicable)
Embedding Status Events w/ Persistence Across Renders and Reloads
output.mp4
Worker Timeouts Are Configurable

The document management modal now shows real-time embedding progress when documents are being embedded into a workspace, with per-file status indicators (Queued, Embedding, Complete, Failed).
Additional Information
Developer Validations
yarn lintfrom the root of the repo & committed changes