Skip to content

[Ulmo] Library v2 commit endpoint timeout with large Meilisearch indexes #38993

Description

@dwong2708

Problem

Image

The library commit endpoint /api/libraries/v2/<lib_key>/commit/ returns a 500 error when publishing 3+ blocks on instances with large Meilisearch indexes (500K+ documents). The publish itself succeeds (data is committed in Learning Core), but the post-publish search index update exceeds the hardcoded 15-second timeout.

This leaves the library MFE showing blocks in an intermediate state — marked as unpublished with no publish option visible — because the frontend relies on Meilisearch (/multi-search) for the component listing and the search index was never updated from the frontend's perspective.

Root Cause

wait_for_post_publish_events in tasks.py dispatches a Celery task and blocks with:

result.get(timeout=15, disable_sync_subtasks=False)

The Celery task fires LIBRARY_BLOCK_PUBLISHED for each block, which triggers library_block_published_handler -> upsert_library_block_index_doc.apply(). The .apply() call runs synchronously and includes _wait_for_meili_task() — a polling loop that blocks until Meilisearch confirms indexing.

On a 500K-document index, each document takes 5-7 seconds to index (inherent to Meilisearch's inverted index rebuild for large indexes). This is not resource-bound — we tested with 30Gi RAM / 6 CPU and observed the same 7s per document. Three blocks = 21 seconds, exceeding the 15-second timeout.

Proposed Short-Term Fix

Make the timeout configurable: Replace the hardcoded timeout=15 in dispatch_and_wait with getattr(settings, 'CONTENT_LIBRARIES_PUBLISH_TIMEOUT', 15) so operators can tune it for their index size without patching.

Batch document submissions: In send_events_after_publish (or send_change_events_for_modified_entities on master), collect all documents from the publish operation and submit them to Meilisearch in a single update_documents call instead of one document at a time. Meilisearch processes a batch of N documents as a single indexing operation — the rebuild time is roughly the same whether it's 1 or 10 documents. This would reduce a 3-block commit from 3x7s = 21s down to ~7s total.

Proposed Long-Term Fix

There are two architectural options to eliminate this problem at the root:

Option A: Decouple the library component listing from Meilisearch

Currently, the library authoring MFE retrieves the list of components directly from Meilisearch (/multi-search). This creates a hard dependency: after any write operation, the API must wait for Meilisearch to be consistent before responding, otherwise the frontend shows stale data.

The fix: use Meilisearch only for search functionality (full-text search, filtering, facets). For the library component listing (the default view showing all components), fetch directly from the Learning Core relational database via a dedicated REST endpoint.

With this approach:

  • The commit endpoint writes to Learning Core (the source of truth) and returns immediately.
  • The indexing events are triggered asynchronously in the background.
  • The frontend fetches the updated component list directly from the database — which already has the committed state — and renders it immediately.
  • Meilisearch catches up in the background; search results become consistent within seconds without blocking the user.
  • This eliminates the coupling between publish latency and search indexing entirely, while preserving Meilisearch's role for full-text search where eventual consistency (a few seconds delay) is acceptable.

Option B: Fully async indexing with per-component loading state in the frontend

Instead of blocking the API response until all indexing is complete, make publish a fire-and-forget operation:

  • The commit endpoint triggers the indexing events asynchronously (no result.get() wait) and returns immediately with a 200.
  • The frontend (library authoring MFE) shows a per-component loading/publishing state — each block transitions individually from "publishing" to "published" as its indexing completes.
  • The frontend polls or subscribes (via WebSocket/SSE) to the indexing status to know when each component is done.

This moves the responsibility of handling eventual consistency to the frontend, which is better positioned to give users real-time feedback on the progress of bulk operations. The backend no longer needs to block a synchronous HTTP request waiting for an external service (Meilisearch) to finish.

Environment

edx-platform release/ulmo.3 (also affects master)
Meilisearch v1.8 with ~500K documents in tutor_studio_content index
Tutor 21.x k8s deployment

Steps to Reproduce

  1. Have a Meilisearch instance with >200K documents indexed
  2. Create a content library with 3+ components
  3. Edit all components (create pending draft changes)
  4. Click "Publish All" (calls /api/libraries/v2/<lib_key>/commit/)
  5. Observe 500 error (on ulmo.3) or stale MFE state (on master where exception is caught)
  6. The content IS published (visible in the DB) but the search index is not updated within the timeout window, leaving the MFE showing stale state.

Evidence:

Image Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    ulmoReleased in Ulmo

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions