Skip to content
Closed
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
60 changes: 58 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const sendEmailReliablyWithRetries = mutation({
});

export const emailSent = internalMutation({
args: vOnCompleteValidator(
args: vOnCompleteArgs(
v.object({ emailType: v.string(), userId: v.id("users") }),
),
handler: async (ctx, { workId, context, result }) => {
Expand Down Expand Up @@ -147,6 +147,52 @@ export const emailSent = pool.defineOnComplete<DataModel>({
});
```

If you need to define a separate mutation to run in the same transaction as the
job, you can use the `onSuccess` option instead of `onComplete`. You can then
define mutations for `onFailure` and `onCancel`. Note that unlike `onSuccess`,
the `onFailure` and `onCancel` handlers will run in a separate transaction.
Comment on lines +150 to +153

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clarify that onSuccess is best-effort, not a transactional workflow step.

These sections currently read as if onSuccess participates in the job's success/failure semantics. In this implementation, a thrown onSuccess handler does not flip the work to failed, so users should not rely on it for state that must be coupled to the work outcome or retry behavior.

Based on learnings: if onSuccess throws, work is still finalized as successful; the callback is decoupled from the work's own success/failure and retry semantics.

Also applies to: 358-362

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 149 - 152, Clarify in the README that onSuccess is a
best-effort, post-completion callback and is not part of the job's transactional
success/failure semantics: update the text around the
onSuccess/onComplete/onFailure/onCancel descriptions (mentioning onSuccess,
onComplete, onFailure, onCancel explicitly) to state that thrown errors in an
onSuccess handler do not mark the job as failed or affect retry behavior, and
that onSuccess is decoupled from the job transaction (unlike atomic
transactional steps) so users must not rely on it for state that must be tightly
coupled to the work outcome; mirror this clarification in the second location
referenced (lines ~358-362).


You can pass the same `onComplete` mutation into the `onSuccess`, `onFailure`,
and `onCancel` mutations.

```ts
await pool.enqueueAction(
ctx,
internal.email.checkStatus,
{ userId },
{
onSuccess: internal.email.handleEmailStatus,
onFailure: internal.email.handleEmailStatus,
onCancel: internal.email.handleEmailStatus,
context: { emailLogId },
},
);
```

You can also define separate mutations for each case:

```ts
export const emailSentSuccess = internalMutation({
args: vOnSuccessArgs(
v.object({ emailType: v.string(), userId: v.id("users") }),
),
handler: async (ctx, { workId, context, result }) => {
// result.kind === "success"
console.log(result.returnValue);
},
});

await pool.enqueueAction(
ctx,
internal.email.checkStatus,
{ userId },
{
onSuccess: internal.email.emailSentSuccess,
// etc.
},
);
```

### Idempotency

Idempotent actions are actions that can be run multiple times safely. This
Expand Down Expand Up @@ -310,7 +356,17 @@ options include:
set to `true`, it will use the `defaultRetryBehavior`. If it's set to a custom
config, it will use that (and do retries).
- `onComplete`: A mutation to run after the function finishes.
- `context`: Any data you want to pass to the `onComplete` mutation.
- `onSuccess`: A mutation to run after the function finishes successfully. Runs
in the same transaction as the function, so ensure that onSuccess does not
cause the overall transaction to exceed transaction limits. Cannot be used
with `onComplete`, but you write one `onComplete` handler and use it for all
of `onSuccess`, `onFailure`, and `onCancel`.
- `onFailure`: A mutation to run after the function finishes with a failure.
Cannot be used with `onComplete`.
- `onCancel`: A mutation to run after the function is canceled. Cannot be used
with `onComplete`.
- `context`: Any data you want to pass to the `onComplete`, `onSuccess`,
`onFailure`, and `onCancel` mutations.
- `runAt` and `runAfter`: Similar to `ctx.scheduler.run*`, allows you to
schedule the work to run later. By default it's immediate.

Expand Down
28 changes: 28 additions & 0 deletions example/convex/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
vWorkId,
Workpool,
vOnCompleteArgs,
vOnSuccessArgs,
} from "@convex-dev/workpool";
import { v } from "convex/values";
import { createLogger } from "../../src/component/logging";
Expand Down Expand Up @@ -323,6 +324,15 @@ export const enqueueFailingOnComplete = internalMutation({
},
});

// Define an onSuccess mutation.
export const success = internalMutation({
args: vOnSuccessArgs(v.number()),
handler: async (ctx, args) => {
console.warn("onSuccess delay", Date.now() - args.result.returnValue);
console.warn("total", (Date.now() - args.context) / 1000);
},
});

export const singleLatency = internalMutation({
args: {},
handler: async (ctx, _args) => {
Expand All @@ -339,6 +349,24 @@ export const singleLatency = internalMutation({
},
});

export const singleLatencyOnSuccess = internalMutation({
args: {},
handler: async (ctx, _args) => {
const started = Date.now();
await bigPool.enqueueMutation(
ctx,
internal.example.noop,
{ started },
{
context: started,
onSuccess: internal.example.success,
onFailure: internal.example.complete,
onCancel: internal.example.complete,
},
);
},
});

const N = 100;
const BASE_MS = 100;
const MAX_MS = 1000;
Expand Down
Loading
Loading