Skip to content

v4: implement onCancel callbacks #2022

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 3, 2025
Merged

v4: implement onCancel callbacks #2022

merged 4 commits into from
May 3, 2025

Conversation

ericallam
Copy link
Member

@ericallam ericallam commented May 2, 2025

This PR adds the onCancel lifecycle hook, that allows you to do work when a run is cancelled:

export const cancelExampleTask = task({
  id: "cancel-example",
  // Signal will be aborted when the task is cancelled 👇
  run: async (payload: { message: string }, { signal }) => {
    logger.info("Hello, world from the cancel task", { message: payload.message });

    // This is a global hook that will be called if the task is cancelled
    tasks.onCancel(async () => {
      logger.info("global task onCancel hook but inside of the run function baby!");
    });

    await logger.trace("10s timeout", async (span) => {
      try {
        // We pass the signal to setTimeout to abort the timeout if the task is cancelled
        await setTimeout(10_000, undefined, { signal });
      } catch (error) {
        // If the timeout is aborted, this error will be thrown, we can handle it here
        logger.error("Timeout error", { error });
      }
    });

    logger.info("Hello, world from the cancel task after the timeout", {
      message: payload.message,
    });

    return {
      message: "Hello, world!",
    };
  },
  onCancel: async ({ payload, runPromise }) => {
    logger.info("Hello, world from the onCancel hook", { payload });
    // You can await the runPromise to get the output of the task
    const output = await runPromise;
    
    logger.info("Hello, world from the onCancel hook after the run", { payload, output });

    // You can do work inside the onCancel hook, up to 30 seconds
    await setTimeout(10_000);
    
    logger.info("Hello, world from the onCancel hook after the timeout", { payload });
  },
});

You can use the onCancel hook along with the signal passed into the run function to interrupt a call to an external service, for example using the streamText function from the AI SDK:

export const interruptibleChat = schemaTask({
  id: "interruptible-chat",
  description: "Chat with the AI",
  schema: z.object({
    prompt: z.string().describe("The prompt to chat with the AI"),
  }),
  run: async ({ prompt }, { signal }) => {
    const chunks: TextStreamPart<{}>[] = [];

    // 👇 This is a global onCancel hook, but it's inside of the run function
    tasks.onCancel(async () => {
      // We have access to the chunks here
      logger.info("interruptible-chat: task cancelled with chunks", { chunks });

      await saveChunksToDatabase(chunks);
    });

    try {
      const result = streamText({
        model: getModel(),
        prompt,
        experimental_telemetry: {
          isEnabled: true,
        },
        tools: {},
        abortSignal: signal,
        onChunk: ({ chunk }) => {
          chunks.push(chunk);
        },
      });

      const textParts = [];

      for await (const part of result.textStream) {
        textParts.push(part);
      }

      return textParts.join("");
    } catch (error) {
      if (error instanceof Error && error.name === "AbortError") {
        // streamText will throw an AbortError if the signal is aborted, so we can handle it here
      } else {
        throw error;
      }
    }
  },
});

Summary by CodeRabbit

  • New Features

    • Introduced a new lifecycle hook, onCancel, enabling tasks to handle cancellation events with custom logic.
    • Added support for registering global and task-specific cancellation handlers.
    • Enhanced tasks to receive an abort signal for improved cancellation and timeout handling.
    • Updated sample tasks to demonstrate cancellation handling and logging.
  • Bug Fixes

    • Improved internal cleanup and resource management when tasks are cancelled.
  • Documentation

    • Expanded type exports and updated sample code to illustrate new cancellation features.

Copy link

changeset-bot bot commented May 2, 2025

🦋 Changeset detected

Latest commit: bbfa96c

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Contributor

coderabbitai bot commented May 2, 2025

Walkthrough

This update introduces a comprehensive cancellation mechanism across the task execution lifecycle. It adds a new onCancel lifecycle hook to the SDK and core packages, allowing both global and task-specific cancellation handlers. The worker and managed run worker entry points are modified to support structured cancellation, including IPC messaging and abort signal propagation. The core task executor enforces the presence of the abort signal and supports explicit cancellation hooks, with improved tracing and error handling. The update also modifies message schemas, utility types, and task definitions to support and utilize cancellation. Example tasks and references are updated to demonstrate and leverage the new cancellation features.

Changes

File(s) / Path(s) Change Summary
.changeset/real-rats-drop.md Documents the addition of the onCancel lifecycle hook to the SDK.
apps/webapp/app/components/runs/v3/RunIcon.tsx Adds a "task-hook-onCancel" case to the icon switch statement for task hooks.
apps/webapp/app/v3/services/cancelTaskRun.server.ts Removes logic that cancels in-progress events when cancelling a task run; now only cancels the run itself.
packages/cli-v3/src/entryPoints/dev-run-worker.ts Adds cancellation support: introduces cancel controller, IPC CANCEL handler, cancellation hooks, and proper cleanup on cancellation.
packages/cli-v3/src/entryPoints/managed-run-worker.ts Similar to above: adds cancellation support, cancel controller, IPC CANCEL handler, cancellation hooks, and cleanup logic.
packages/cli-v3/src/executions/taskRunProcess.ts Adds explicit cancellation signaling to child processes, introduces a private #cancel method, updates cleanup and execution logic to respect cancellation state.
packages/core/src/utils.ts Adds a Deferred<T> type and promiseWithResolvers<T>() function for deferred promise handling.
packages/core/src/v3/lifecycle-hooks-api.ts Exports new types for cancellation hooks: TaskCancelHookParams, OnCancelHookFunction, AnyOnCancelHookFunction.
packages/core/src/v3/lifecycleHooks/index.ts Extends LifecycleHooksAPI with methods for registering, retrieving, and calling cancel hooks and listeners.
packages/core/src/v3/lifecycleHooks/manager.ts Adds cancel hook support to both standard and no-op lifecycle hook managers, including registration, retrieval, and listener invocation.
packages/core/src/v3/lifecycleHooks/types.ts Makes signal required in all hook params, adds new types/interfaces for cancellation hooks, and extends the lifecycle hooks manager interface.
packages/core/src/v3/schemas/messages.ts Adds a CANCEL message type to the WorkerToExecutorMessageCatalog for IPC cancellation with a timeout.
packages/core/src/v3/timeout/api.ts Updates abortAfterTimeout to accept optional timeout and return AbortController; renames private helper; updates related logic.
packages/core/src/v3/timeout/types.ts Updates TimeoutManager interface: abortAfterTimeout now takes an optional timeout and returns an AbortController.
packages/core/src/v3/timeout/usageTimeoutManager.ts Updates abortAfterTimeout to accept optional timeout, return AbortController, and manage interval ID for usage sampling.
packages/core/src/v3/types/tasks.ts Adds onCancel lifecycle hook to CommonTaskOptions, introduces CancelFnParams, and makes signal required in several parameter types.
packages/core/src/v3/usage-api.ts Exports UsageMeasurement and UsageSample types.
packages/core/src/v3/usage/devUsageManager.ts Adds a check before deleting a measurement in stop method.
packages/core/src/v3/workers/taskExecutor.ts Makes signal required in all relevant methods, adds support for cancellation hooks, uses deferred promise for run tracking, and refines abort/cancellation logic.
packages/core/test/taskExecutor.test.ts Ensures an AbortSignal is always passed to the executor in tests.
packages/trigger-sdk/src/v3/hooks.ts Adds onCancel registration function and exports AnyOnCancelHookFunction.
packages/trigger-sdk/src/v3/shared.ts Registers onCancel hook in task lifecycle registration.
packages/trigger-sdk/src/v3/tasks.ts Adds onCancel to exported tasks object.
references/d3-chat/src/trigger/chat.ts Updates todoChat to use abort signal, adds interruptibleChat task with cancellation support and chunk logging, demonstrates cancellation hooks.
references/hello-world/src/trigger/example.ts Adds onCancel hook to hooksTask, introduces cancelTask with cancellation logic and hooks.
references/hello-world/src/trigger/init.ts Adds a global cancellation handler via tasks.onCancel.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant WebApp
    participant Worker
    participant Executor
    participant Task
    participant LifecycleHooks

    User->>WebApp: Request to cancel a running task
    WebApp->>Worker: Send CANCEL IPC message (with timeout)
    Worker->>Executor: AbortController aborts signal
    Worker->>LifecycleHooks: Call registered onCancel hooks
    LifecycleHooks-->>Task: Invoke onCancel (global/task-specific)
    Executor-->>Task: Task run observes abort signal
    Task-->>Executor: Task run aborts or cleans up
    Executor-->>Worker: Task run completes or aborts
    Worker->>WebApp: Acknowledge cancellation
Loading

Possibly related PRs

  • v4: New lifecycle hooks #1817: Introduces lifecycle hooks such as onWait, onResume, and onComplete but does not include cancellation support; related as lifecycle hooks enhancement.

Suggested reviewers

  • matt-aitken

Poem

In the warren of code, a new path unfurls,
With cancel hooks ready for twitches and twirls.
When tasks must be halted, no need for alarm—
A gentle abort signal brings calm and no harm.
Rabbits can nap, knowing cleanup is neat,
For onCancel now hops in, making endings complete!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

recurseml bot commented May 2, 2025

⚠️ Only 5 files will be analyzed due to processing limits.

1 similar comment
Copy link

recurseml bot commented May 2, 2025

⚠️ Only 5 files will be analyzed due to processing limits.

Copy link

recurseml bot commented May 2, 2025

😱 Found 3 issues. Time to roll up your sleeves! 😱

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (18)
references/hello-world/src/trigger/init.ts (1)

9-11: Good example implementation of global onCancel handler

This demonstrates the proper usage of the new onCancel hook as a global handler. The async function signature and logging pattern matches other lifecycle hooks in the file.

Consider adding an example that shows how to perform cleanup operations during cancellation, such as releasing resources or stopping long-running operations, which would showcase the practical utility of this feature.

 tasks.onCancel(async ({ ctx, payload }) => {
   logger.info("Hello, world from the global cancel", { ctx, payload });
+  // Example: Clean up resources when tasks are cancelled
+  await releaseAnyHeldResources();
 });
references/hello-world/src/trigger/example.ts (2)

240-246: onCancel hook may hang indefinitely by await-ing runPromise and adding a 10 s sleep

If the run was aborted part-way through, runPromise might never resolve (e.g. if the run is stuck in un-cancellable I/O), causing the hook itself to block forever.
In addition, the extra await setTimeout(10_000) delays shutdown and defeats fast-fail semantics.

Consider:

-  const output = await runPromise;
-  ...
-  await setTimeout(10_000);
+  let output: TOutput | undefined;
+  try {
+    output = await Promise.race([runPromise, Promise.resolve(undefined)]);
+  } catch (_) {
+    /* swallow – already cancelling */
+  }
+  // Remove the additional 10 s delay to let the worker exit promptly

210-213: Leverage full hook parameters for richer diagnostics

onCancel receives { ctx, signal, init, runPromise, … }.
Logging only the payload misses useful context (run id, attempt, etc.). Consider logging ctx and checking signal.aborted to aid debugging.

packages/core/src/v3/types/tasks.ts (1)

92-114: Making signal mandatory is a breaking API change

All existing user-defined run, init, middleware, start etc. implementations must now accept a non-optional signal.
Ensure:

  1. The change is documented in release notes.
  2. Code-mods or compile-time errors guide users to update their signatures.
packages/core/src/v3/timeout/api.ts (2)

6-9: Clarify the no-op semantics of abortAfterTimeout

abortAfterTimeout() in NoopTimeoutManager ignores the timeoutInSeconds argument and simply returns a fresh AbortController.
That’s perfectly fine for a noop implementation, but it can be confusing to future readers and may trigger “unused-parameter” lint warnings.

-  abortAfterTimeout(timeoutInSeconds?: number): AbortController {
-    return new AbortController();
+  // Intentionally ignores `timeoutInSeconds` – this manager never aborts.
+  abortAfterTimeout(_timeoutInSeconds?: number): AbortController {
+    return new AbortController();
   }

27-33: Consider providing a non-undefined fallback for signal

signal currently forwards whatever the active TimeoutManager exposes.
If a custom manager forgets to define .signal, callers will suddenly receive undefined, which forces extra defensive checks throughout the codebase.

A tiny fallback avoids that foot-gun:

-  public get signal(): AbortSignal | undefined {
-    return this.#getManager().signal;
+  public get signal(): AbortSignal | undefined {
+    return this.#getManager().signal ?? undefined; // <- guarantees the property exists
   }

(Or return a dummy AbortSignal such as new AbortController().signal.)
Not critical, but it makes the API more predictable.

packages/cli-v3/src/entryPoints/managed-run-worker.ts (2)

410-420: Detach the timeout listener to avoid leaking closures

The listener that forwards timeoutController.signal.abort to cancelController.abort() is never removed.
For a long-lived worker processing many runs, this will accumulate one redundant listener per execution.

A quick fix inside the same block:

 const timeoutController = timeout.abortAfterTimeout(execution.run.maxDuration);

 const onTimeoutAbort = () => {
   if (_isCancelled || cancelController.signal.aborted) {
     return;
   }
   cancelController.abort(timeoutController.signal.reason);
 };
 timeoutController.signal.addEventListener("abort", onTimeoutAbort);
 // 🧹 remove on finish
 try {
   const { result } = await executor.execute(
     execution,
     metadata,
     traceContext,
     cancelController.signal
   );
 ...
 } finally {
+  timeoutController.signal.removeEventListener("abort", onTimeoutAbort);
 }

476-484: Send an acknowledgement back to the executor after handling CANCEL

The executor currently fires a CANCEL IPC message but never receives confirmation that the worker finished flushing / calling hooks.
If the controller side awaits such a message, it may hang indefinitely.

Consider replying with a lightweight CANCEL_HANDLED (or similar) message:

await flushAll(timeoutInMs);
await sender.send("CANCEL_HANDLED", { id: _execution?.run.id });

Even if the executor doesn’t wait today, the explicit ack future-proofs the protocol.

references/d3-chat/src/trigger/chat.ts (2)

160-183: chunks is collected but never used

chunks is populated via onChunk, yet it’s not referenced afterwards.
If it’s only for debugging, wrap it in a conditional log; otherwise consider removing it to save memory in long streams.


230-233: Avoid the banned {} type

Biome flags {} as “any non-nullable value”.
Use unknown or an explicit record instead:

-const chunks: TextStreamPart<{}>[] = [];
+const chunks: TextStreamPart<Array<Record<string, unknown>>>[] = [];

Or, if you don’t care about the tool payload, simply: TextStreamPart<unknown>[].

🧰 Tools
🪛 Biome (1.9.4)

[error] 230-230: Don't use '{}' as a type.

Prefer explicitly define the object shape. '{}' means "any non-nullable value".

(lint/complexity/noBannedTypes)

packages/cli-v3/src/entryPoints/dev-run-worker.ts (3)

413-426: Potential listener leak on timeoutController.signal

A new listener is attached for every run, but never removed. After many runs the process accumulates event listeners and Node.js will warn (“Possible EventEmitter memory leak”). Remove the listener in the finally block that clears _isRunning:

 finally {
+  timeoutController.signal.removeEventListener("abort", onTimeoutAbort);
   _execution = undefined;
   _isRunning = false;
 }

(where onTimeoutAbort is the named callback you pass to addEventListener).


480-488: Double invocation of cancel hooks

callCancelHooks() is invoked here, but the same hooks are also executed inside TaskExecutor when it detects abortSignal.aborted. Unless the manager de-duplicates, listeners will fire twice.
Consider calling the hooks only after the executor finishes, or guard inside callOnCancelHookListeners to ensure idempotency.


501-511: Timeout does not actually cancel long-running hooks

Promise.race with setTimeout only hides late resolution; the hook Promise still runs to completion in the background. If a hook never resolves, the process keeps running. Pass an AbortSignal to the hooks or reject/cleanup after the timeout:

const controller = new AbortController();
const timeout = setTimeout(timeoutInMs).then(() => controller.abort());

await lifecycleHooks.callOnCancelHookListeners(controller.signal);

This lets hook implementers respect the cancellation deadline.

packages/cli-v3/src/executions/taskRunProcess.ts (2)

107-116: Error handling in cancel() hides IPC failures

The blanket catch just logs to console.error, whereas the caller may need to know that cancellation failed (e.g. child already exited). Re-throw after logging so the supervisor can decide what to do:

-  } catch (err) {
-    console.error("Error cancelling task run process", { err });
+  } catch (err) {
+    console.error("Error cancelling task run process", { err });
+    throw err;
   }

231-235: Grace-period before SIGKILL would be safer

kill() is invoked immediately after sending the CANCEL IPC message. If the child honours the cancel quickly this is fine, but in the common case it will still be shutting down and may be hard-killed. Consider:

  1. Send CANCEL.
  2. Wait e.g. timeoutInMs + 2 s for the child to exit voluntarily.
  3. Only then escalate with SIGKILL.

This mirrors the graceful-shutdown pattern used elsewhere in the codebase.

packages/core/src/v3/lifecycleHooks/index.ts (1)

264-290: Public API docs & typings should be updated for new cancel hooks

Great to see full parity for onCancel hooks. Please remember to:

  1. Add JSDoc comments so SDK consumers discover the new methods.
  2. Export the new AnyOnCancelHookFunction and related types from the package root to avoid forcing deep imports.

No code change required here, just documentation/exposure.

packages/core/src/v3/lifecycleHooks/types.ts (1)

246-252: Consider replacing void with undefined in union type

The return type union includes void, which can be confusing in TypeScript union types. Consider using only undefined for consistency and clarity.

- ) => undefined | void | Promise<undefined | void>;
+ ) => undefined | Promise<undefined | void>;
🧰 Tools
🪛 Biome (1.9.4)

[error] 252-252: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

packages/core/src/v3/workers/taskExecutor.ts (1)

422-437: Remove debug console.log in production code

There's a console.log statement that appears to be debugging code. This should be removed before the PR is merged.

  signal.addEventListener("abort", () => {
    if (typeof signal.reason === "string" && signal.reason.includes("cancel")) {
-     console.log("abortPromise: cancel");
      return;
    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 15816e9 and cce0fb2.

📒 Files selected for processing (26)
  • .changeset/real-rats-drop.md (1 hunks)
  • apps/webapp/app/components/runs/v3/RunIcon.tsx (1 hunks)
  • apps/webapp/app/v3/services/cancelTaskRun.server.ts (0 hunks)
  • packages/cli-v3/src/entryPoints/dev-run-worker.ts (5 hunks)
  • packages/cli-v3/src/entryPoints/managed-run-worker.ts (5 hunks)
  • packages/cli-v3/src/executions/taskRunProcess.ts (3 hunks)
  • packages/core/src/utils.ts (1 hunks)
  • packages/core/src/v3/lifecycle-hooks-api.ts (1 hunks)
  • packages/core/src/v3/lifecycleHooks/index.ts (2 hunks)
  • packages/core/src/v3/lifecycleHooks/manager.ts (3 hunks)
  • packages/core/src/v3/lifecycleHooks/types.ts (12 hunks)
  • packages/core/src/v3/schemas/messages.ts (1 hunks)
  • packages/core/src/v3/timeout/api.ts (3 hunks)
  • packages/core/src/v3/timeout/types.ts (1 hunks)
  • packages/core/src/v3/timeout/usageTimeoutManager.ts (3 hunks)
  • packages/core/src/v3/types/tasks.ts (3 hunks)
  • packages/core/src/v3/usage-api.ts (1 hunks)
  • packages/core/src/v3/usage/devUsageManager.ts (1 hunks)
  • packages/core/src/v3/workers/taskExecutor.ts (19 hunks)
  • packages/core/test/taskExecutor.test.ts (1 hunks)
  • packages/trigger-sdk/src/v3/hooks.ts (3 hunks)
  • packages/trigger-sdk/src/v3/shared.ts (2 hunks)
  • packages/trigger-sdk/src/v3/tasks.ts (2 hunks)
  • references/d3-chat/src/trigger/chat.ts (5 hunks)
  • references/hello-world/src/trigger/example.ts (2 hunks)
  • references/hello-world/src/trigger/init.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/webapp/app/v3/services/cancelTaskRun.server.ts
🧰 Additional context used
🧬 Code Graph Analysis (5)
packages/core/src/v3/timeout/usageTimeoutManager.ts (1)
packages/core/src/v3/usage/devUsageManager.ts (2)
  • sample (18-37)
  • sample (53-55)
packages/trigger-sdk/src/v3/shared.ts (3)
packages/core/src/v3/lifecycle-hooks-api.ts (2)
  • lifecycleHooks (5-5)
  • AnyOnCancelHookFunction (37-37)
packages/core/src/v3/lifecycleHooks/types.ts (1)
  • AnyOnCancelHookFunction (254-254)
packages/trigger-sdk/src/v3/hooks.ts (1)
  • AnyOnCancelHookFunction (29-29)
packages/core/src/v3/timeout/api.ts (1)
packages/core/src/v3/timeout/types.ts (1)
  • TimeoutManager (1-4)
packages/cli-v3/src/executions/taskRunProcess.ts (1)
packages/cli-v3/src/utilities/logger.ts (1)
  • logger (113-113)
packages/core/src/v3/lifecycleHooks/manager.ts (3)
packages/core/src/v3/lifecycle-hooks-api.ts (4)
  • RegisteredHookFunction (10-10)
  • AnyOnWaitHookFunction (22-22)
  • TaskWait (34-34)
  • AnyOnCancelHookFunction (37-37)
packages/core/src/v3/lifecycleHooks/types.ts (6)
  • RegisteredHookFunction (174-178)
  • AnyOnWaitHookFunction (71-71)
  • TaskWait (36-53)
  • AnyOnCancelHookFunction (254-254)
  • RegisterHookFunctionParams (169-172)
  • LifecycleHooksManager (256-344)
packages/core/src/v3/workers/taskExecutor.ts (2)
  • wait (453-523)
  • wait (607-677)
🪛 Biome (1.9.4)
references/d3-chat/src/trigger/chat.ts

[error] 230-230: Don't use '{}' as a type.

Prefer explicitly define the object shape. '{}' means "any non-nullable value".

(lint/complexity/noBannedTypes)

packages/core/src/v3/lifecycleHooks/types.ts

[error] 252-252: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: units / 🧪 Unit Tests
  • GitHub Check: typecheck / typecheck
🔇 Additional comments (36)
apps/webapp/app/components/runs/v3/RunIcon.tsx (1)

100-100: Consistent addition of onCancel hook rendering

The addition of "task-hook-onCancel" to the switch statement is implemented correctly, maintaining consistency with the other non-error lifecycle hooks that render using the FunctionIcon with text-text-dimmed styling.

.changeset/real-rats-drop.md (1)

1-5: LGTM - Clear and concise changeset entry

The changeset correctly specifies a patch update to the "@trigger.dev/sdk" package with a clear description of the new functionality being added.

packages/trigger-sdk/src/v3/tasks.ts (2)

11-11: Appropriate addition of onCancel import

The onCancel hook is correctly imported from the hooks module.


99-99: Correct export of onCancel in tasks object

The onCancel hook is properly exposed in the tasks export object, making it available for users of the SDK.

packages/core/src/v3/usage-api.ts (1)

7-7: Type exports for usage monitoring

The addition of UsageMeasurement and UsageSample type exports is appropriate for tracking resource usage during task execution, which would be relevant for cancellation scenarios where resources need to be properly accounted for.

packages/core/src/v3/lifecycle-hooks-api.ts (1)

35-37: Properly extends the lifecycle hooks API with new cancel hook types.

These new exported types (TaskCancelHookParams, OnCancelHookFunction, and AnyOnCancelHookFunction) are essential for implementing the new cancellation hook support throughout the codebase. The addition follows the existing pattern established for other lifecycle hooks.

packages/core/src/v3/usage/devUsageManager.ts (1)

77-79: Added defensive check before deleting measurement.

This change prevents potential errors by ensuring the measurement exists in the map before attempting to delete it. This is particularly important in cancellation scenarios when measurements might already be cleaned up by other processes.

packages/core/src/v3/schemas/messages.ts (1)

246-251: Good addition of CANCEL message type to support task cancellation.

The new message type with the timeoutInMs parameter provides a clear protocol for signaling cancellation between workers and executors. This is a key component for implementing structured cancellation support as described in the PR.

packages/core/test/taskExecutor.test.ts (1)

1908-1910: Ensures consistent abort signal availability in tests.

This change guarantees that the task executor always receives a valid abort signal, even if none is provided. This is crucial for proper cancellation handling and aligns with the broader implementation of cancellation hooks.

packages/core/src/v3/timeout/types.ts (1)

2-2: Improved API for AbortController management

The change to return AbortController instead of just AbortSignal and making the timeout parameter optional provides better flexibility for timeout management. This enhances the cancellation mechanism by giving callers more control over aborting operations.

packages/trigger-sdk/src/v3/shared.ts (2)

45-45: Added import for cancel hook type

The AnyOnCancelHookFunction import aligns with the implementation of the new onCancel lifecycle hook.


1642-1646: Added support for onCancel lifecycle hook

The implementation follows the same pattern as other lifecycle hooks, properly registering task-specific cancel hooks with the lifecycle hooks manager.

packages/core/src/utils.ts (1)

20-40: Added helpful promise utility for cancellation support

The Deferred<T> type and promiseWithResolvers<T>() function are well-implemented utilities that will help with external promise resolution control, which is particularly useful for cancellation scenarios.

packages/trigger-sdk/src/v3/hooks.ts (3)

14-14: Added import for new cancel hook type

Adding the AnyOnCancelHookFunction type import supports the new onCancel hook implementation.


29-29: Exported cancel hook type for SDK consumers

Exporting the AnyOnCancelHookFunction type allows SDK users to properly type their cancel hook implementations.


137-147: Added onCancel global hook registration

The new onCancel hook follows the same pattern as other lifecycle hooks, with proper overloads to support both named and unnamed hooks. This implementation enables users to register cancellation handlers that will be invoked when a task is cancelled.

packages/cli-v3/src/entryPoints/dev-run-worker.ts (1)

410-435: _isCancelled must be cleared before measuring success path

The success branch is gated by

if (_isRunning && !_isCancelled) {  }

If _isCancelled remained true from a previous run (see issue above), the worker would never send TASK_RUN_COMPLETED, leaking the process and leaving the parent hanging. Ensure you reset _isCancelled = false before every run or, better, derive this flag purely from the current controller’s aborted state.

packages/cli-v3/src/executions/taskRunProcess.ts (1)

123-126: Early return from cleanup() may leave child process alive

If cleanup() is called while _isBeingCancelled is true, the function returns without killing the child (because kill = true is ignored). If the previous cancel() failed for any reason the worker could remain running indefinitely.
Consider waiting for onExit with a timeout here, or fall back to kill("SIGKILL") after a grace period.

packages/core/src/v3/lifecycleHooks/types.ts (4)

10-10: Signal parameter now required across all hook interfaces

Making signal a required parameter in all lifecycle hooks ensures that cancellation state is always available, improving the robustness of task execution.


233-244: Well-structured addition of cancellation hook parameters

The TaskCancelHookParams interface follows the established pattern for other lifecycle hooks while adding the important runPromise parameter, which allows hooks to observe the current task run state.


334-340: Well-designed cancel hook registration methods

The implementation of cancel hook registration and retrieval methods follows the established pattern for other lifecycle hooks, ensuring consistency in the API.


342-343: Cancel listener methods match established patterns

These listener methods maintain consistency with other hook listeners, making the API intuitive for developers who are already familiar with the other lifecycle hooks.

packages/core/src/v3/lifecycleHooks/manager.ts (6)

16-16: Consistent import of new cancel hook type

The import of AnyOnCancelHookFunction aligns with the pattern used for other lifecycle hook types.


60-62: Minor reordering of wait hooks fields

This is just a reordering of the wait hooks declarations with no functional change, keeping related fields together for better organization.


66-69: Good implementation of cancel hook storage

The private fields for global and task-specific cancel hooks follow the established pattern for other lifecycle hooks, maintaining consistency in the implementation.


71-77: Efficient implementation of cancel hook listeners

The listener registration and calling methods correctly use Promise.allSettled to ensure all listeners are called even if some fail, which is crucial for cleanup operations.


412-441: Well-implemented cancel hook registration methods

The implementation of the cancel hook registration and retrieval methods follows the exact same pattern as other lifecycle hooks, ensuring consistency and predictability in the API.


445-468: Comprehensive NoopLifecycleHooksManager implementation

The cancel hook methods in the NoopLifecycleHooksManager correctly implement the interface with no-op behavior, ensuring that the interface can be used in contexts where hooks are not needed.

packages/core/src/v3/workers/taskExecutor.ts (8)

1-1: Import Context for proper span context propagation

The additional import of Context from OpenTelemetry supports proper context propagation to child spans, which is important for maintaining trace continuity during cancellation.


54-54: Added promiseWithResolvers utility

This utility simplifies creating a promise with exposed resolve/reject functions, which is used to track task execution status and provide it to cancellation hooks.


94-94: Signal parameter now required

Making the AbortSignal parameter required ensures that cancellation is always supported throughout the task execution lifecycle.


156-164: Well-designed runPromise implementation

The creation of a promise with resolvers to track task execution status is a clean approach. The catch handler prevents unhandled promise rejections, which is a good defensive programming practice.


165-174: Proper cancel hook listener registration

The implementation registers a cancel hook listener that calls the cancel functions with the runPromise, allowing hooks to observe the current execution state.


198-199: Task error properly rejects runPromise

When a task fails, the runPromise is rejected, ensuring that cancellation hooks see the correct execution state.


248-249: Task success properly resolves runPromise

When a task succeeds, the runPromise is resolved, ensuring that cancellation hooks see the correct execution state.


525-605: Well-implemented callOnCancelFunctions method

The method to call cancellation hooks follows the same pattern as other lifecycle hook methods, with proper tracing, error handling, and hook invocation. The method is comprehensive and consistent with the rest of the codebase.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
references/hello-world/src/trigger/example.ts (1)

224-227: ⚠️ Potential issue

Avoid registering a global tasks.onCancel handler inside every task run.

tasks.onCancel() attaches a global listener that lives for the lifetime of the process. Calling it inside cancelTask.run() means every invocation of the task adds yet another listener, leading to unbounded growth and memory leaks.

-    tasks.onCancel(async () => {
-      logger.info("global task onCancel hook but inside of the run function baby!");
-    });
+// Move this to module scope (executed once at start-up)
+tasks.onCancel(async () => {
+  logger.info("Global tasks.onCancel hook");
+});
packages/cli-v3/src/entryPoints/managed-run-worker.ts (1)

236-236: ⚠️ Potential issue

AbortController must be re-created per run – current design causes false early cancellations.

cancelController is declared const at module scope and is aborted when a CANCEL message is received. Every subsequent task run will reuse the same already-aborted signal, so executor.execute() will immediately observe signal.aborted === true and bail out.

-const cancelController = new AbortController();
+// Must be mutable so we can reset it before each EXECUTE_TASK_RUN
+let cancelController: AbortController = new AbortController();

Then, at the very start of the EXECUTE_TASK_RUN handler (right after we ensure we're not already running):

+// Fresh controller for this execution
+cancelController = new AbortController();
+_isCancelled = false;
references/d3-chat/src/trigger/chat.ts (1)

233-236: ⚠️ Potential issue

tasks.onCancel must be registered outside the run function.

Registering tasks.onCancel inside the run function leaks handlers across executions. Every invocation of the task adds a new global listener that's never removed.

Either move it to module scope or capture and clean up the disposer:

-    tasks.onCancel(async () => {
-      // We have access to the chunks here
-      logger.info("interruptible-chat: task cancelled with chunks", { chunks });
-    });
+    const disposer = tasks.onCancel(async () => {
+      // We have access to the chunks here
+      logger.info("interruptible-chat: task cancelled with chunks", { chunks });
+    });
+    
+    try {
+      // ...existing code...
+    } finally {
+      // Remove the listener when the task is done
+      disposer();
+    }
🧹 Nitpick comments (1)
references/d3-chat/src/trigger/chat.ts (1)

230-230: Fix the empty object type notation.

Don't use {} as a type. Prefer explicitly defining the object shape or use a more appropriate type.

-    const chunks: TextStreamPart<{}>[] = [];
+    const chunks: TextStreamPart<Record<string, never>>[] = [];
🧰 Tools
🪛 Biome (1.9.4)

[error] 230-230: Don't use '{}' as a type.

Prefer explicitly define the object shape. '{}' means "any non-nullable value".

(lint/complexity/noBannedTypes)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cce0fb2 and bbfa96c.

📒 Files selected for processing (6)
  • packages/cli-v3/src/entryPoints/dev-run-worker.ts (5 hunks)
  • packages/cli-v3/src/entryPoints/managed-run-worker.ts (5 hunks)
  • packages/core/src/v3/timeout/usageTimeoutManager.ts (3 hunks)
  • packages/core/src/v3/types/tasks.ts (3 hunks)
  • references/d3-chat/src/trigger/chat.ts (5 hunks)
  • references/hello-world/src/trigger/example.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/core/src/v3/timeout/usageTimeoutManager.ts
  • packages/core/src/v3/types/tasks.ts
  • packages/cli-v3/src/entryPoints/dev-run-worker.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
references/d3-chat/src/trigger/chat.ts (3)
packages/core/src/v3/timeout/usageTimeoutManager.ts (1)
  • signal (13-15)
packages/core/src/v3/timeout/api.ts (1)
  • signal (27-29)
packages/trigger-sdk/src/v3/tasks.ts (2)
  • schemaTask (83-83)
  • tasks (87-104)
🪛 Biome (1.9.4)
references/d3-chat/src/trigger/chat.ts

[error] 230-230: Don't use '{}' as a type.

Prefer explicitly define the object shape. '{}' means "any non-nullable value".

(lint/complexity/noBannedTypes)

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: units / 🧪 Unit Tests
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (9)
references/hello-world/src/trigger/example.ts (3)

210-212: Well-implemented onCancel hook for hooksTask.

The onCancel hook is implemented properly alongside other lifecycle hooks, providing consistent error handling for task cancellation.


228-236: Good use of abort signal with error handling.

The code properly passes the signal to setTimeout and catches any abort errors that might be thrown when the task is cancelled, demonstrating best practices for handling cancellation during async operations.


246-257: Well-implemented task-specific onCancel hook.

The onCancel hook demonstrates several important capabilities:

  1. Accessing the task payload
  2. Awaiting the original run's promise to get output
  3. Performing asynchronous work (with timeout)
  4. Proper logging throughout the cancellation process

This provides a good example of how to implement cancellation handling.

packages/cli-v3/src/entryPoints/managed-run-worker.ts (4)

409-409: Good implementation of combined abort signal.

Using AbortSignal.any() to race between cancellation and timeout signals is an excellent approach. This allows cancellation to preempt execution, regardless of whether it comes from an explicit cancel request or a timeout.


413-414: Proper check for cancellation state before sending result.

The code correctly verifies both _isRunning and !_isCancelled before sending the task completion message, which prevents sending completion messages for cancelled tasks.


460-468: Well-structured CANCEL message handler.

The handler for the CANCEL message follows a good sequence:

  1. Sets the cancellation flag
  2. Aborts the controller with a reason
  3. Calls cancellation hooks with a timeout
  4. Stops usage measurement if active
  5. Flushes all telemetry data

This ensures proper cleanup and reporting when a task is cancelled.


478-488: Good implementation of callCancelHooks with timeout.

The callCancelHooks function properly uses Promise.race with a timeout to ensure cancellation hooks don't hang indefinitely. This is important for system stability when user-defined hooks might be problematic.

references/d3-chat/src/trigger/chat.ts (2)

113-113: Well-updated task signature with signal parameter.

The run method has been properly updated to accept the signal parameter, enabling cancellation support.


160-183: Good implementation of chunk collection and abort signal passing.

The code collects streamed text chunks while properly passing the abort signal to streamText, enabling cancellation of the streaming operation.

Comment on lines +259 to +265
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
// streamText will throw an AbortError if the signal is aborted, so we can handle it here
} else {
throw error;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Function may return undefined, violating the declared return type.

When a cancellation error is caught, the function falls through without a return value, potentially returning undefined instead of the declared string.

      if (error instanceof Error && error.name === "AbortError") {
        // streamText will throw an AbortError if the signal is aborted, so we can handle it here
+       return ""; // Return empty string to satisfy the return type
      } else {
        throw error;
      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
// streamText will throw an AbortError if the signal is aborted, so we can handle it here
} else {
throw error;
}
}
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
// streamText will throw an AbortError if the signal is aborted, so we can handle it here
return ""; // Return empty string to satisfy the return type
} else {
throw error;
}
}

@ericallam ericallam merged commit 13f750a into main May 3, 2025
12 checks passed
@ericallam ericallam deleted the v4/on-cancel-callbacks branch May 3, 2025 19:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants