Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions docs/content/docs/java/guides/extensibility/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,18 @@ the worker (`Worker.Builder.trackWorkflows()`).
| Event | Fires when | Payload |
|---|---|---|
| `PREDICATE_REJECTED` (`predicate.rejected`) | A registered predicate rejects an enqueue. | `PredicateEvent` |
| `PREDICATE_DEFERRED` (`predicate.deferred`) | Reserved — see note below. | `PredicateEvent` |
| `PREDICATE_SKIPPED` (`predicate.skipped`) | Reserved — see note below. | `PredicateEvent` |
| `PREDICATE_CANCELLED` (`predicate.cancelled`) | Reserved — see note below. | `PredicateEvent` |

<Callout type="info">
The cross-SDK event contract also defines `predicate.deferred` and
`predicate.cancelled`, for runtimes whose predicates can defer or cancel a
submission. This SDK's predicates are pass/reject only, so neither has an
`EventName` constant here.
This SDK's predicates are pass/reject only, so `PREDICATE_REJECTED` is the
only one it emits. The other three are constants for the rest of the
cross-SDK contract — work held back for a delay (at enqueue or at dispatch,
where the payload also carries the job's id), an enqueue dropped without
raising, and a dispatch-time cancellation of an already-enqueued job. They exist here
so a webhook subscription written against another SDK still resolves through
`EventName.fromWire` and matches nothing rather than silently dropping.
</Callout>

<Callout type="info">
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/java/guides/extensibility/webhooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ webhooks.delete(hook.id);

| Builder method | Default | Description |
|---|---|---|
| `on(EventName...)` | — | Events to deliver — any of the 26 dotted wire names (job/worker/queue/workflow/predicate); see [Events](/java/guides/extensibility/events). |
| `on(EventName...)` | — | Events to deliver — any of the 29 dotted wire names (job/worker/queue/workflow/predicate); see [Events](/java/guides/extensibility/events). |
| `secret(String)` | none | Signs each delivery as `X-Taskito-Signature: sha256=<hex>`. |
| `taskFilter(String)` | all tasks | Only deliver for this exact task name — screens task-bearing events only (see below). |
| `header(String, String)` | — | Extra headers added to every delivery. |
Expand Down
23 changes: 17 additions & 6 deletions docs/content/docs/node/guides/extensibility/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ queue.off("job.retrying", onRetry); // same reference required to unsubscribe
`queue.on` and `queue.off` are generically typed over `EventMap` — the event
name narrows the handler's payload type automatically, so `e` above is
`OutcomeEvent` for `job.retrying` and `WorkerEvent` for `worker.offline` with
no casts needed. The package barrel also exports `EVENT_NAMES` (all 26 wire
no casts needed. The package barrel also exports `EVENT_NAMES` (all 29 wire
names, as a const tuple) and the `EventMap` / `EventPayload` types for typing
handlers declared elsewhere:

Expand Down Expand Up @@ -112,15 +112,22 @@ that run's final `state` and `error`.

## Predicate events

Gates run at enqueue time; each of the three non-`allow` decisions has its own
event. See [Predicates](/node/guides/core/predicates) for the decision API.

| Event | Fires when | Payload |
|---|---|---|
| `predicate.rejected` | A registered predicate rejects an enqueue, just before `PredicateRejectedError` throws. | `PredicateEvent` |
| `predicate.rejected` | A gate returns `Decision.reject()` (or bare `false`), just before `PredicateRejectedError` throws. | `PredicateEvent` |
| `predicate.skipped` | A gate returns `Decision.skip()` — the enqueue is dropped without throwing, and `tryEnqueue` returns `null`. | `PredicateEvent` |
| `predicate.deferred` | A gate returns `Decision.defer(delayMs)` — the job is enqueued, delayed by `delayMs`. | `PredicateEvent` |
| `predicate.cancelled` | Reserved — see note below. | `PredicateEvent` |

<Callout type="info">
The cross-SDK event contract also defines `predicate.deferred` and
`predicate.cancelled`, for runtimes whose predicates can defer or cancel a
submission. This SDK's predicates are pass/reject only, so neither is
emitted here — see [Predicates](/node/guides/core/predicates).
`predicate.cancelled` means a *dispatch-time* predicate cancelled a job that
was already enqueued — an outcome only the Python SDK produces. This SDK's
gates run at enqueue only, where a terminal skip is `predicate.skipped` (no
job exists yet), so nothing emits it here. It stays in `EVENT_NAMES` so a
webhook subscription written against another SDK still loads and validates.
</Callout>

## Payload types
Expand Down Expand Up @@ -168,6 +175,10 @@ interface NodeCompensationEvent {

interface PredicateEvent {
taskName: string;
/** The gate's reason, when it gave one (`predicate.rejected` / `.skipped`). */
reason?: string;
/** How long the enqueue was held back (`predicate.deferred` only). */
delayMs?: number;
}
```

Expand Down
8 changes: 4 additions & 4 deletions docs/content/docs/node/guides/extensibility/webhooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ restarts.
```ts
const hook = queue.webhooks.create({
url: "https://hooks.example.com/jobs",
events: ["job.dead", "job.completed", "worker.offline"], // any of the 26 wire names — omit for all
events: ["job.dead", "job.completed", "worker.offline"], // any of the 29 wire names — omit for all
secret: process.env.WEBHOOK_SECRET, // signs X-Taskito-Signature: sha256=...
taskFilter: ["send_email"], // optional — only screens task-bearing events
});
Expand All @@ -32,7 +32,7 @@ queue.webhooks.delete(hook.id);
| Field | Description |
|---|---|
| `url` | Endpoint to POST events to. |
| `events` | Event names to deliver — any of the 26 dotted wire names (see [Events](/node/guides/extensibility/events) for the full list). Omit for all. |
| `events` | Event names to deliver — any of the 29 dotted wire names (see [Events](/node/guides/extensibility/events) for the full list). Omit for all. |
| `secret` | Signs each delivery as `X-Taskito-Signature: sha256=...`. |
| `taskFilter` | Only deliver for these task names. |
| `maxRetries` | Extra attempts after the first (default `3`). |
Expand All @@ -43,7 +43,7 @@ Deliveries for non-job events — worker, queue, workflow, and predicate — car
no job identity; their `payload` matches the shapes documented on the
[Events](/node/guides/extensibility/events) page instead of `OutcomeEvent`. A
subscription's `taskFilter` only screens events that carry a task name
(`job.*` and `predicate.rejected`) — worker, queue, and workflow events always
(`job.*` and `predicate.*`) — worker, queue, and workflow events always
deliver regardless of `taskFilter`.

Deliveries fire from the **worker process** (where events originate). Verify the
Expand Down Expand Up @@ -73,7 +73,7 @@ for (const delivery of queue.webhooks.deliveries(hook.id)) {
| `ok` | `true` when `status` is `"delivered"`. |
| `attempts` | Number of HTTP attempts made. |
| `payload` | The JSON body that was POSTed. |
| `taskName` / `jobId` | The originating task and job. Both absent for worker, queue, and workflow events; `predicate.rejected` carries `taskName` but no `jobId` (it fires before any job exists). |
| `taskName` / `jobId` | The originating task and job. Both absent for worker, queue, and workflow events; `predicate.*` carries `taskName` but no `jobId`, since this SDK's gates run at enqueue, before any job exists. |
| `responseCode` | Last HTTP status, or `null` on a network error. |
| `responseBody` | Last response body, truncated to 2 KiB, or `null`. |
| `latencyMs` | Time from first attempt to the final outcome. |
Expand Down
9 changes: 9 additions & 0 deletions docs/content/docs/python/guides/extensibility/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ delivery logs, and the REST API — e.g.
| `PREDICATE_DEFERRED` | A predicate defers a task (enqueue- or dispatch-time) | `task_name`, `queue`, `defer_seconds`, `phase` (+ `job_id` at dispatch) |
| `PREDICATE_CANCELLED` | A dispatch-time predicate cancels a job | `task_name`, `job_id`, `queue`, `phase` (+ `reason`) |
| `PREDICATE_REJECTED` | An enqueue-time predicate rejects a task | `task_name`, `queue`, `phase` (+ `reason`) |
| `PREDICATE_SKIPPED` | Reserved — see note below | `task_name` (+ `reason`) |

<Callout type="info">
`PREDICATE_SKIPPED` is an enqueue dropped without raising, which the Node
SDK produces via `Decision.skip()`. Here a `Cancel` at enqueue raises
`PredicateRejectedError` and emits `PREDICATE_REJECTED` instead, so nothing
emits it. It stays in `EventType` so a webhook subscription written against
another SDK still validates and round-trips.
</Callout>

`duration_ms` is how long the job ran, so a listener needn't time it. On the
outcome events (`JOB_RETRYING`, `JOB_DEAD`, `JOB_CANCELLED`) it is `None` when
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,24 @@ public enum EventName {
/** One node's compensation job failed ({@code workflow.node_compensation_failed}). */
WORKFLOW_NODE_COMPENSATION_FAILED("workflow.node_compensation_failed"),
/** An enqueue was rejected by a predicate or gate ({@code predicate.rejected}). */
PREDICATE_REJECTED("predicate.rejected");
PREDICATE_REJECTED("predicate.rejected"),
/**
* A predicate held work back for a delay ({@code predicate.deferred}) — at
* enqueue, or at dispatch, where the payload also carries the job's id.
* Reserved: this SDK's predicates are pass/reject only.
*/
PREDICATE_DEFERRED("predicate.deferred"),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/**
* An enqueue was dropped by a predicate without raising
* ({@code predicate.skipped}). Reserved: this SDK's predicates are
* pass/reject only.
*/
PREDICATE_SKIPPED("predicate.skipped"),
/**
* A dispatch-time predicate cancelled an already-enqueued job
* ({@code predicate.cancelled}). Reserved: this SDK gates only at enqueue.
*/
PREDICATE_CANCELLED("predicate.cancelled");

/**
* Wire names (+ the pre-taxonomy outcome aliases) to constants. The legacy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ void wireNamesAreUniqueAcrossAllValues() {
for (EventName name : EventName.values()) {
assertTrue(seen.add(name.wireName()), "duplicate wire name: " + name.wireName());
}
assertEquals(26, seen.size());
assertEquals(29, seen.size());
}

@Test
void everyPredicateWireNameInTheContractHasAConstant() {
assertEquals("predicate.rejected", EventName.PREDICATE_REJECTED.wireName());
assertEquals("predicate.deferred", EventName.PREDICATE_DEFERRED.wireName());
assertEquals("predicate.skipped", EventName.PREDICATE_SKIPPED.wireName());
assertEquals("predicate.cancelled", EventName.PREDICATE_CANCELLED.wireName());
}

@Test
Expand Down
11 changes: 10 additions & 1 deletion sdks/node/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { createLogger } from "./utils";

const log = createLogger("events");

/** Every event name the queue can emit — the single source of truth. */
/**
* The cross-SDK event taxonomy — the single source of truth for what this
* queue emits and what a webhook can subscribe to. Nearly every name is
* emitted here; the few reserved contract entries are noted inline.
*/
export const EVENT_NAMES = [
"job.enqueued",
"job.completed",
Expand Down Expand Up @@ -32,6 +36,10 @@ export const EVENT_NAMES = [
"predicate.rejected",
"predicate.skipped",
"predicate.deferred",
// Reserved: a dispatch-time predicate cancelling an already-enqueued job.
// This SDK gates only at enqueue, where a terminal skip is
// `predicate.skipped` (no job exists yet), so nothing emits it here.
"predicate.cancelled",
] as const;

/** A queue event name. */
Expand Down Expand Up @@ -137,6 +145,7 @@ export interface EventMap {
"predicate.rejected": PredicateEvent;
"predicate.skipped": PredicateEvent;
"predicate.deferred": PredicateEvent;
"predicate.cancelled": PredicateEvent;
}

/**
Expand Down
8 changes: 5 additions & 3 deletions sdks/node/src/predicates/metrics.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { DecisionKind } from "./decisions";

/**
* What the gates decided, as returned by `Queue.predicateStats`. Node's keys
* follow its decision kinds; the Python SDK's equivalents are `denied` for
* `rejected` and `cancelled` for `skipped`.
* What the gates decided, as returned by `Queue.predicateStats`. These keys
* follow Node's decision kinds. Python's `predicate_stats` counts by outcome
* sentinel instead, so the two do not line up one-to-one: its `denied` (a bare
* `false`) and `cancelled` (a `Cancel`) both block the enqueue by raising, the
* way `rejected` does here, and it has no enqueue-time skip.
*/
export interface PredicateStats {
allowed: number;
Expand Down
6 changes: 4 additions & 2 deletions sdks/node/test/core/webhooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,18 @@ it("still delivers task-less events to a task-filtered webhook", async () => {
expect(events).not.toContain("job.enqueued");
});

it("exposes all 28 subscribable event names", () => {
it("exposes all 29 subscribable event names", () => {
const queue = newQueue();
const names = queue.webhooks.eventTypes();
expect(names).toHaveLength(28);
expect(names).toHaveLength(29);
expect(names).toContain("job.enqueued");
expect(names).toContain("worker.online");
expect(names).toContain("workflow.submitted");
expect(names).toContain("predicate.rejected");
expect(names).toContain("predicate.skipped");
expect(names).toContain("predicate.deferred");
// Reserved here, but subscribable so a cross-SDK webhook config still loads.
expect(names).toContain("predicate.cancelled");
});

it("creates, lists, and deletes webhooks", () => {
Expand Down
12 changes: 11 additions & 1 deletion sdks/python/taskito/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@


class EventType(enum.Enum):
"""Types of job and worker lifecycle events."""
"""The cross-SDK event taxonomy, as job and worker lifecycle events.

Nearly every member is emitted by this runtime; the few reserved
contract entries other SDKs emit are noted per member, and exist so a
webhook subscription stays portable across SDKs.
"""

JOB_ENQUEUED = "job.enqueued"
JOB_COMPLETED = "job.completed"
Expand Down Expand Up @@ -44,8 +49,13 @@ class EventType(enum.Enum):
NODE_COMPENSATED = "workflow.node_compensated"
NODE_COMPENSATION_FAILED = "workflow.node_compensation_failed"
PREDICATE_DEFERRED = "predicate.deferred"
# A dispatch-time predicate cancelling an already-enqueued job.
PREDICATE_CANCELLED = "predicate.cancelled"
PREDICATE_REJECTED = "predicate.rejected"
# Reserved: an enqueue dropped without raising. Here a ``Cancel`` at
# enqueue raises ``PredicateRejectedError`` and emits PREDICATE_REJECTED
# instead, so nothing emits this.
PREDICATE_SKIPPED = "predicate.skipped"


class EventBus:
Expand Down
1 change: 1 addition & 0 deletions sdks/python/tests/observability/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,6 @@ def test_all_event_types_exist() -> None:
"predicate.deferred",
"predicate.cancelled",
"predicate.rejected",
"predicate.skipped",
}
assert {e.value for e in EventType} == expected
Loading