Service notifications#
Thumbnail Service both consumes and publishes events through the Notification Service. It consumes storage lifecycle events from a durable queue (see Storage event consumption), and it publishes three event types of its own: one that carries generation work to the worker, and two that tell any interested consumer that a thumbnail association was created or removed. This page describes what each published event is for, when it is emitted, the event schema on the wire, how consumers receive them, and how to enable and credential the publisher in the Helm chart.
Every outgoing event goes to the Notification Service’s Event Aggregation API at NOTIFICATIONS_EVENT_AGGREGATION_ENDPOINT (Helm notifications.eventAggregation.endpoint), authenticated with the shared serviceIdentity credentials; see Authenticating to the Notification Service below.
Events the service publishes#
Event type |
Purpose |
Consumed by |
|---|---|---|
|
Durable hand-off of one thumbnail’s worth of work from the service to the worker: source, fully resolved specification, and the exact destination address. |
|
|
A thumbnail has been associated with a target resource — either because the worker finished a generation, or because a caller registered a pre-generated thumbnail through |
Clients that cache or render thumbnails, indexers, audit pipelines. |
|
An association was removed — by the targeted |
The same consumers, to invalidate what they hold. |
Thumbnail Service is the only publisher of association events. The worker never publishes them: after uploading a thumbnail it calls AssociateThumbnail on the service, and the service emits omni.thumbnail.associated after its own metadata write commits. See Thumbnail generation worker.
When events are published#
Generate events#
One omni.thumbnail.generate event is published per thumbnail to be produced — never a batch:
Trigger |
Events published |
|---|---|
Eligible |
One per configured |
|
One per caller specification, after the source |
|
One per configured |
The generate publish is durable dispatch, not best-effort: on the event-driven path the publish result decides whether the storage delivery is acknowledged, and on the API path a publish that fails is reported to the caller. A publish that times out is not proof that nothing was enqueued — the broker may have accepted it before the deadline.
Association events#
Operation |
Event |
Count |
|---|---|---|
|
|
One |
Worker-driven association after a successful generation (same RPC) |
|
One per generated thumbnail |
|
|
One; none when the thumbnail was not associated (idempotent no-op) |
|
|
One per fully removed entry |
Both association events are emitted after the metadata write is persisted, so a consumer that reads the association back on receipt sees the new state. Failed or rejected writes publish nothing.
Note
Association events are best-effort. The publish is dispatched from a detached task after the write commits, so the API caller receives its response regardless of whether the event was delivered. If the Notification Service is unreachable or rejects the publish, the failure is logged and the association is kept. When the publisher receives Unauthenticated, the service refreshes its service-identity token and retries the publish once before giving up.
When NOTIFICATIONS_EVENT_AGGREGATION_ENDPOINT is not configured (Helm notifications.eventAggregation.enabled: false), association publishes are skipped with a warning. Generate publishes are not skippable: with no publisher configured, generation dispatch fails as a retryable error, so a storage delivery is never acknowledged for work that was not published.
Event schema#
Every event is the Event message of the Notification Service’s publisher API, sent with the PublishEvent RPC. All three share the same envelope:
Field |
Type |
Value |
|---|---|---|
|
string |
|
|
|
Time at which the service published the event. |
|
|
JSON-shaped object whose fields depend on |
|
|
|
|
|
|---|---|
|
The source |
|
The association target’s |
|
The thumbnail’s |
Numbers in message are JSON numbers; every enum is an UPPER_SNAKE_CASE string. Resource addresses are the canonical (percent-encoded) form the service stores.
omni.thumbnail.generate#
One event is exactly one thumbnail: one source, one fully resolved specification, one destination.
Field |
Type |
Description |
|---|---|---|
|
integer |
Currently |
|
string |
Address of the file to thumbnail. Required. |
|
string |
Storage identity of that file, as resolved by the service. Required for |
|
string |
The exact, absolute address the worker writes to, rendered by the service from |
|
string |
|
|
object |
Fully resolved image-to-image specification: |
|
string (optional) |
W3C trace context of the publishing span, so the worker’s conversion joins the same trace. Omitted when no span is active. |
{
"event_type": "omni.thumbnail.generate",
"occurred_at": "2026-09-11T12:34:56Z",
"message": {
"schema_version": 1,
"source": {
"resource_address": "https://acct.blob.core.windows.net/assets/photos/image.jpg",
"resource_identity": "https://acct.blob.core.windows.net/assets/photos/image.jpg?version-id=v3"
},
"destination_resource_address": "https://acct.blob.core.windows.net/thumbnails/475673a39cfd8a2fb955ee5c352c1082/256x256/image.jpg.auto.png",
"target_type": "FILE",
"specification": {
"width": 256,
"height": 256,
"encoding_format": "PNG",
"transform_type": "FIT",
"crop_strategy": "SMART",
"quality": 85,
"preserve_aspect_ratio": true
},
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
},
"resource": {
"resource_id": "https://acct.blob.core.windows.net/assets/photos/image.jpg"
}
}
The worker accepts the legacy alias crop_direction for crop_strategy and ignores unknown fields, so additive changes do not require a new schema_version.
omni.thumbnail.associated#
The message is the same value the service stores as association metadata (see Association metadata and queries), minus create_time:
Field |
Description |
|---|---|
|
The thumbnail resource. |
|
The resource the thumbnail belongs to. |
|
Present for |
|
|
|
The flat, |
{
"event_type": "omni.thumbnail.associated",
"occurred_at": "2026-09-11T12:35:02Z",
"message": {
"thumbnail": {
"resource_address": "https://acct.blob.core.windows.net/thumbnails/475673a39cfd8a2fb955ee5c352c1082/256x256/image.jpg.auto.png",
"resource_identity": "https://acct.blob.core.windows.net/thumbnails/475673a39cfd8a2fb955ee5c352c1082/256x256/image.jpg.auto.png?version-id=v1"
},
"association_target": {
"resource_address": "https://acct.blob.core.windows.net/assets/photos/image.jpg",
"resource_identity": "https://acct.blob.core.windows.net/assets/photos/image.jpg?version-id=v3"
},
"target_type": "FILE",
"specification": {
"type": "IMAGE_TO_IMAGE",
"width": 256,
"height": 256,
"encoding_format": "PNG",
"transform_type": "FIT",
"crop_strategy": "SMART",
"quality": 85,
"preserve_aspect_ratio": true
}
},
"resource": {
"resource_id": "https://acct.blob.core.windows.net/assets/photos/image.jpg"
}
}
omni.thumbnail.disassociated#
Same shape as the associated event without the specification field:
{
"event_type": "omni.thumbnail.disassociated",
"occurred_at": "2026-09-11T13:00:00Z",
"message": {
"thumbnail": {
"resource_address": "https://acct.blob.core.windows.net/thumbnails/475673a39cfd8a2fb955ee5c352c1082/256x256/image.jpg.auto.png",
"resource_identity": "https://acct.blob.core.windows.net/thumbnails/475673a39cfd8a2fb955ee5c352c1082/256x256/image.jpg.auto.png?version-id=v1"
},
"association_target": {
"resource_address": "https://acct.blob.core.windows.net/assets/photos/image.jpg",
"resource_identity": "https://acct.blob.core.windows.net/assets/photos/image.jpg?version-id=v3"
},
"target_type": "FILE"
},
"resource": {
"resource_id": "https://acct.blob.core.windows.net/thumbnails/475673a39cfd8a2fb955ee5c352c1082/256x256/image.jpg.auto.png?version-id=v1"
}
}
A disassociated event says the metadata link was removed. Whether the thumbnail blob still exists depends on which path removed it — the targeted API leaves the blob in place, while the storage-deleted cleanup deletes blobs the service generated. See Blob deletion policy.
Consuming the events#
Consumers register with the Notification Service’s Consumer API, which owns the subscription model, durable queues, and delivery semantics. Two filters are useful in practice:
Filter by
event_type— subscribe toomni.thumbnail.associatedandomni.thumbnail.disassociatedto track association changes and ignore other traffic on the broker.Filter by
resource.resource_id— a consumer that only displays a subtree can add a prefix filter on the association target’s address, for example:{ "filter_type": "FILTER_TYPE_STARTS_WITH_GREEDY", "resource_id": "https://acct.blob.core.windows.net/assets/photos/" }
For
omni.thumbnail.associatedthe filter matches the source address. Foromni.thumbnail.disassociatedit matches the thumbnail identity, so a consumer that wants both for one subtree should filter onevent_typeand match the association target insidemessageitself.
omni.thumbnail.generate is an internal hand-off. Consuming it from anywhere other than the worker’s WorkerEventQueue is unsupported: the worker’s queue filters on the same source-address prefixes the service watches (NOTIFICATIONS_QUEUE_RESOURCE_PREFIXES), and a second consumer would compete for deliveries.
Note
An omni.thumbnail.associated event proves the association metadata exists. It does not prove the blob at thumbnail.resource_address is readable by the consumer’s principal — read access to thumbnails is governed by the storage service, exactly as for any other object.
Enabling notifications in the Helm chart#
The chart exposes publishing and consuming under the root notifications key:
notifications:
eventAggregation:
enabled: true
endpoint: "event-aggregation-service:50051"
eventConsumer:
endpoint: "event-consumer-service:50051"
resourcePrefixes:
- "https://acct.blob.core.windows.net/assets/"
storageEventQueue:
enabled: true
Value |
Purpose |
|---|---|
|
When |
|
gRPC endpoint of the Notification Service’s Event Aggregation API. Default |
|
gRPC endpoint of the Consumer API, shared by StorageEventQueue (service) and WorkerEventQueue (worker). Default |
|
Storage-container URL prefixes. One greedy-prefix filter per entry on both durable queues, and the input to both derived queue ids. Required non-empty whenever a consumer is enabled. |
|
Enables the service’s storage-event consumer. Default |
At startup the service opens one gRPC channel to the aggregation endpoint and reuses it for every publish. Provisioning and consumption of the durable queues are described in Durable queue self-provisioning.
Authenticating to the Notification Service#
Publishing and consuming are authorized actions on the Notification Service side, so every call carries the service-identity bearer token produced by the shared serviceIdentity configuration — client-credentials flow or access-token file. The notifications section has no credentials of its own. See Outbound service identity for the two modes.
The principal the service presents needs these grants on the deployment that guards the Notification Service:
Grant |
Needed for |
|---|---|
|
Publishing generate work (and, in practice, the association events). |
|
Self-provisioning StorageEventQueue and WorkerEventQueue on startup. |
|
Consuming both queues. |
When those grants are enforced by a Permission Service deployment, the publish permit looks like:
permit(
principal == Principal::"thumbnail-service",
action == Action::"event-aggregation-service:publish-event",
resource == EventType::"omni.thumbnail.generate"
);
Troubleshooting#
No
omni.thumbnail.associatedevents after associations succeed. Check thatnotifications.eventAggregation.enabledistrueand the endpoint is reachable. A skipped publish is logged with a warning naming the missing endpoint; a rejected publish is logged at error level with the failure reason. The association itself is still committed — read it back withGET /v1alpha/thumbnails.CreateThumbnailsreturns502/INTERNALwith “channel unconfigured” or every specification infailed. The aggregation endpoint is unset or unreachable. Generate publishes are not best-effort; fix the endpoint and retry — on the event-driven path the storage delivery is retried automatically.Publishes fail with
Unauthenticatedrepeatedly. One refresh-and-retry is built in; persistent failures mean the service identity is rejected by the Notification Service. Check the token source described in Authenticating with an OpenID Connect Identity Provider.Publishes fail with
PermissionDenied. The principal lacksevent-aggregation-service:publish-eventforomni.thumbnail.generate. Grant it on the Notification Service’s authorization deployment.Generate events publish fine but no thumbnails appear. The worker’s WorkerEventQueue may not exist, or its prefix filters may not match the source addresses. Compare the
-storage-and-worker-queue ids in a queue listing: matching digests mean both components agree onresourcePrefixes. See Queue id digest check.Consumers receive
omni.thumbnail.disassociatedfor entries they never saw associated. The storage-deleted cleanup emits one event per removed entry, including entries created before the consumer subscribed. Treat a disassociated event for an unknown thumbnail as a no-op.
See also#
Storage event consumption — the queue this service consumes, and how the generate publish decides the storage delivery’s acknowledgement.
Thumbnail generation worker — the consumer of
omni.thumbnail.generateand theAssociateThumbnailcall that leads toomni.thumbnail.associated.Association metadata and queries — the stored value the association events mirror.
Authenticating with an OpenID Connect Identity Provider — the
serviceIdentitycredentials used for every publish.Observability — spans and log lines emitted around each publish.