Signal-Driven Sync
The storefront reads products from the Matrix read model, so an edit in PIM, a new price or a stock change is
invisible until the read model is rebuilt. Signal-driven sync does the rebuild for the affected SKUs: a signal handler
puts sku:channel_idx into a Redis sorted set, a debounce lock collects rapid edits, and one Celery task dispatches
the rebuilds. The PIM and price handlers are off until enabled; the QMS handler has no switch.
Signal Catalog
Section titled “Signal Catalog”PIM, in django_pim/signals/handlers.py:
| Model | Signal | What is enqueued |
|---|---|---|
Product | post_save, post_delete | real_product.sku and shop.idx of the product |
ProductAttribute | post_save, post_delete | the product of the row |
RealProduct | post_save | every Product of that real product, one per channel |
Attribute | post_save | every product that uses the attribute |
Feature | post_save | every product whose feature set contains the feature |
ProductPicture | post_save, post_delete | the product of the row |
ProductVideo | post_save, post_delete | the product of the row |
ProductInCategory | post_save, post_delete | the product of the row |
The price manager, in django_pricemanager/signals/handlers.py:
| Model | Signal | What is enqueued |
|---|---|---|
CurrentPrice | post_save, post_delete | product.sku and channel.idx of the price |
QMS, in django_qms/signals/handlers.py:
| Signal | Sent when | What is enqueued |
|---|---|---|
warehouse_stock_changed (a QMS signal, not a model signal) | a stock batch created or changed at least one warehouse stock record | every SKU of the batch, for every channel of the warehouse |
All three write to the same sorted set and the same lock, so one flush task serves them.
The category tree is not part of the read model. The Matrix category endpoint reads PIM’s ProductCategory table
directly, so a renamed category needs no signal. Assigning a product to a category does: that is
ProductInCategory above.
Kill Switches
Section titled “Kill Switches”Each source has its own switches.
A PIM handler does nothing when any of these holds:
-
The toggle is off.
PimSettings.matrix_signals_enabledis a boolean on the singletonPimSettingsmodel,Falseby default. Its value is cached for 60 seconds; saving it in the Django admin clears the cache. -
The code suppressed it. A thread-local context manager, for code that calls
save()in a loop:from django_pim.signals.killswitch import suppress_matrix_signalswith suppress_matrix_signals():for product in products:product.save()Suppression in one thread does not affect another.
-
The channel is on the denylist.
PIM_MATRIX_SIGNALS_CHANNEL_DENYLISTin the Django settings holds channelidxvalues. -
The save comes from a fixture. Handlers skip
rawsaves, soloaddataenqueues nothing.
Price Manager
Section titled “Price Manager”The same three layers under its own names: PriceManagerSettings.matrix_signals_enabled (False by default, cached
for 60 seconds, cleared when the row is saved), suppress_price_matrix_signals() from
django_pricemanager.signals.killswitch, and PRICEMANAGER_MATRIX_SIGNALS_CHANNEL_DENYLIST.
No toggle, no suppression and no denylist. The handler enqueues for every active warehouse when django_pim is
installed and enabled, and skips inactive warehouses.
Debounce and Batching
Section titled “Debounce and Batching”Every enqueue adds sku:channel_idx to the sorted set pim:matrix:pending with the current time as its score. The
same pair written twice is one entry.
The handler then tries to set pim:matrix:flush_scheduled if it is absent, with a lifetime of the debounce window
plus one second. The handler that gets the lock sends django_matrix.tasks.flush_pending_matrix_sync to the
matrix_pull queue with a countdown equal to the debounce window. Every other handler in that window only adds to
the set.
The flush task reads and deletes the set in one Redis pipeline, drops the lock and groups the entries by channel.
A channel with more pending SKUs than PIM_MATRIX_SIGNALS_BATCH_THRESHOLD gets one
fill_read_model_for_channel_task; any other channel gets one fill_read_model_for_product_task per SKU.
The Attribute and Feature handlers count the affected products first, across all channels. Above the same
threshold they enqueue the
marker __CHANNEL_REBUILD__ for each affected channel instead of every SKU, and the flush task turns the marker into
a channel rebuild.
Settings
Section titled “Settings”| Setting | Default | Meaning |
|---|---|---|
PIM_MATRIX_SIGNALS_DEBOUNCE_SECONDS | 5 | debounce window of the PIM and QMS handlers |
PIM_MATRIX_SIGNALS_BATCH_THRESHOLD | 500 | pending SKUs per channel above which the channel is rebuilt as a whole |
PIM_MATRIX_SIGNALS_CHANNEL_DENYLIST | [] | channel idx values the PIM handlers skip |
PRICEMANAGER_MATRIX_SIGNALS_DEBOUNCE_SECONDS | 5 | debounce window of the price handlers |
PRICEMANAGER_MATRIX_SIGNALS_CHANNEL_DENYLIST | () | channel idx values the price handlers skip |
Enabling
Section titled “Enabling”The PIM and price toggles are off by default, so on a fresh installation only stock changes from QMS are enqueued.
- Run a Celery worker that consumes the
matrix_pullqueue. In the entirius-zeno harness the worker takes its queues from the recipe, and the default recipe listsmatrix_pull. - In the Django admin, open the PIM settings (
/admin/django_pim/pimsettings/), set Matrix signals enabled and save. Do the same in the price manager settings for price changes. - Check with
python manage.py matrix-sync-status: the first line readsmatrix_signals_enabled: True.
Bulk Imports
Section titled “Bulk Imports”bulk_create() and bulk_update() send no post_save, so an import built on them enqueues nothing. An import that
calls save() per row runs inside suppress_matrix_signals(), shown under Kill Switches. Either
way the read model is rebuilt once at the end:
from django_matrix.tasks import fill_read_model_with_flags
fill_read_model_with_flags(channel_idx=channel_idx)Observability
Section titled “Observability”The enqueue code logs through the standard logging module. PIM’s, which QMS calls too, uses the logger
django_pim.signals and writes:
Flush scheduled in 5s, enqueued SKU=<sku> channel=<channel_idx>Flush already scheduled, enqueued SKU=<sku> channel=<channel_idx>Cascade threshold exceeded, enqueued channel rebuild for channel=<channel_idx>Redis unavailable, skipping signal sync for SKU=<sku> channel=<channel_idx>Redis error during enqueue for SKU=<sku> channel=<channel_idx>The second line is logged at debug level, the last two as warnings. The price manager logs under
django_pricemanager.signals.dispatch with its own wording, such as
Price sync flush scheduled in 5s, enqueued <sku>:<channel_idx>.
The flush task and the channel rebuild task log through ProcessLogger("matrix.signal_sync") with two codes:
| Code | Meaning | Example message |
|---|---|---|
| 5320 | flush and channel rebuild | Flush complete: 47 pending entries, 47 tasks dispatched |
| 5329 | error | Redis unavailable during flush: <error>, Flush failed: <error> |
The per-product task logs through ProcessLogger("matrix.fill_read_model") with code 5200, as a manual rebuild of
one product does.
Status Command
Section titled “Status Command”python manage.py matrix-sync-statusIt prints the PIM toggle, the three PIM settings, the number of pending entries with the first twenty, whether a flush is scheduled, how long ago the last flush ran, and the number of active and reserved Matrix tasks that the Celery workers report.
Redis Keys
Section titled “Redis Keys”| Key | Type | Lifetime | Holds |
|---|---|---|---|
pim:matrix:pending | sorted set | 300 s | pending sku:channel_idx entries, score is the time |
pim:matrix:flush_scheduled | string | debounce window + 1 s | the lock that keeps one flush per window |
pim:matrix:last_flush | string | 86400 s | time of the last flush |
The two toggles sit in the Django cache under pim:matrix_signals_enabled and pricemanager:matrix_signals_enabled
for 60 seconds.
Known Limitations
Section titled “Known Limitations”Matrix copies picture and thumbnail references from PIM during a rebuild and generates no thumbnails. A picture
that has no thumbnails yet enters the read model with its original image only. Generate them with
pim-thumbs-generate and rebuild the product.
Troubleshooting
Section titled “Troubleshooting”Changes Do Not Reach the Storefront
Section titled “Changes Do Not Reach the Storefront”python manage.py matrix-sync-status: ismatrix_signals_enabledtrue, are entries pending, is a flush scheduled.- A worker consumes the
matrix_pullqueue. - The channel is not on a denylist of PIM or the price manager.
- The worker log has no line with code 5329 and no failed
fill_read_model_for_product_task.
The Pending Set Keeps Growing
Section titled “The Pending Set Keeps Growing”- A bulk operation calls
save()per row withoutsuppress_matrix_signals(). - No worker consumes
matrix_pull, so the flush task never runs. The set expires 300 seconds after the last enqueue and those changes are lost to the signal path: rebuild withfill-read-model.
Redis Is Unavailable
Section titled “Redis Is Unavailable”The enqueue code catches every exception, logs a warning and returns, so the save in PIM or the price manager
succeeds and nothing is synced. The flush task retries up to three times, 30 seconds apart. fill-read-model does
not use the pending set and works without it.
Not Built
Section titled “Not Built”Thumbnails on upload: the PIM picture upload endpoint does not generate thumbnails today, which is the limitation above.
One product followed from the catalog through the read model to an order in the CMS: Commerce Journey.