.agents/skills/hybrid-cloud-outboxes/references/signal-receivers.md
Manual signal receivers are used for OutboxCategory values that are not tied to a ReplicatedCellModel or ReplicatedControlModel. The model mixins auto-connect receivers via connect_cell_model_updates() / connect_control_model_updates() — you only write manual receivers for categories with custom dispatch logic.
Source files:
src/sentry/receivers/outbox/cell.py — cell outbox receiverssrc/sentry/receivers/outbox/control.py — control outbox receiverssrc/sentry/receivers/outbox/__init__.py — maybe_process_tombstone helpersrc/sentry/receivers/outbox/cell.py (or a new file under src/sentry/receivers/outbox/)src/sentry/receivers/outbox/control.py (or a new file under src/sentry/receivers/outbox/)src/sentry/receivers/__init__.py or a file that is.Cell outbox signals fire with these keyword arguments:
sender: OutboxCategory enum valuepayload: dict | None — the JSON payload from the outboxobject_identifier: int — the ID of the source objectshard_identifier: int — the shard key (e.g., organization_id)shard_scope: int — the OutboxScope valueFor categories that carry all data in the payload (no DB lookup needed):
from django.dispatch import receiver
from sentry.hybridcloud.outbox.signals import process_cell_outbox
from sentry.hybridcloud.outbox.category import OutboxCategory
@receiver(process_cell_outbox, sender=OutboxCategory.MY_CATEGORY)
def process_my_category(payload: Any, **kwds: Any) -> None:
if payload is not None:
my_rpc_service.do_something(data=MyRpcData(**payload))
For categories tied to a model where you need to detect create/update vs delete:
from django.dispatch import receiver
from sentry.hybridcloud.outbox.signals import process_cell_outbox
from sentry.hybridcloud.outbox.category import OutboxCategory
from sentry.receivers.outbox import maybe_process_tombstone
@receiver(process_cell_outbox, sender=OutboxCategory.MY_CATEGORY)
def process_my_category(object_identifier: int, **kwds: Any) -> None:
if (instance := maybe_process_tombstone(MyModel, object_identifier)) is None:
return # Object was deleted — tombstone recorded
# Object exists — replicate
my_rpc_service.sync(model_id=instance.id, data=serialize(instance))
When you need both the payload and a tombstone check:
@receiver(process_cell_outbox, sender=OutboxCategory.MY_CATEGORY)
def process_my_category(object_identifier: int, payload: Any, **kwds: Any) -> None:
if (instance := maybe_process_tombstone(MyModel, object_identifier)) is None:
return
if payload and "extra_field" in payload:
my_rpc_service.sync_with_extra(
model_id=instance.id,
extra_field=payload["extra_field"],
)
Control outbox signals include an additional cell_name argument:
sender: OutboxCategory enum valuepayload: dict | Noneobject_identifier: intshard_identifier: intcell_name: str — the target cellshard_scope: intdate_added: datetimescheduled_for: datetimefrom django.dispatch import receiver
from sentry.hybridcloud.outbox.signals import process_control_outbox
from sentry.hybridcloud.outbox.category import OutboxCategory
from sentry.receivers.outbox import maybe_process_tombstone
@receiver(process_control_outbox, sender=OutboxCategory.MY_CATEGORY)
def process_my_category(object_identifier: int, cell_name: str, **kwds: Any) -> None:
if (instance := maybe_process_tombstone(
MyModel, object_identifier, cell_name=cell_name
)) is None:
return
# Replicate to the specific cell
my_cell_service.sync(cell_name=cell_name, data=serialize(instance))
For categories where the receiver makes an RPC call without looking up a model:
@receiver(process_control_outbox, sender=OutboxCategory.MY_CATEGORY)
def process_my_category(
payload: Mapping[str, Any], shard_identifier: int, **kwds: Any
) -> None:
my_cell_service.do_something(
organization_id=shard_identifier,
data=payload["data"],
)
maybe_process_tombstone Patterndef maybe_process_tombstone(
model: type[T],
object_identifier: int,
cell_name: str | None = None,
) -> T | None:
This function:
model.objects.filter(id=object_identifier).last()cell_tombstone_service or control_tombstone_service and returns NoneThe tombstone system drives HybridCloudForeignKey cascade deletes across silos. When an object is deleted from one silo, the tombstone propagated to the other silo triggers cleanup of dependent records.
When to use: Any receiver that needs to distinguish between "object was created/updated" and "object was deleted". Not needed for payload-only categories (audit logs, IP events) where the payload carries all necessary data.
cell_name parameter: Pass cell_name for control outbox receivers (tombstone goes to the cell). Omit for cell outbox receivers (tombstone goes to control).