Skip to content

STOR-5615: Replace @retryable with retryable() - #7617

Merged
apeacock1991 merged 1 commit into
mainfrom
apeacock/STOR-5615-use-retry-directive
Oct 9, 2026
Merged

apeacock1991 merged 1 commit into
mainfrom
apeacock/STOR-5615-use-retry-directive

Conversation

@apeacock1991

@apeacock1991 apeacock1991 commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

retryable() from cloudflare:durable-objects is now a plain function instead of a method decorator. It marks Durable Object methods as safe to run more than once, usually from a static block next to the method:

import { DurableObject } from "cloudflare:workers";
import { retryable } from "cloudflare:durable-objects";

export class Counter extends DurableObject {
  static {
    retryable(this.prototype.reset);
  }
  async reset() {
    await this.ctx.storage.put("n", 0);
  }
}

The decorator needed a build step. V8 doesn't parse decorators, and Vite's Oxc transform doesn't lower them, so Vite users couldn't use it. An earlier version of this PR used a "use retry" directive instead, with the runtime scanning the start of the method's source. A function call needs no source parsing, marks the exact function, and fails loudly on a typo, so I went with that.

retryable() takes any number of functions, sets the V8 private key the decorator set, and returns undefined. If any argument isn't a function, it throws a TypeError and marks nothing. The decorator's context checks and its warning about later decorators are gone.

The mark is on the function object, so the claim check, the method lookup, and the DURABLE_OBJECT_RETRIES_USERLAND gate don't change. An inherited method stays marked. An override, a bound copy, or a wrapper doesn't. Methods must be on the prototype: RPC still refuses own instance properties, so a class field like reset = retryable(...) isn't supported.

cloudflare:durable-objects still exports only retryable, now with a different signature. The decorator form shipped in v1.20260930.1, but it's undocumented and the autogate keeps its effect off, so we accept the risk of breaking a Worker that uses it.

@apeacock1991
apeacock1991 requested review from a team as code owners October 5, 2026 10:38
@ask-bonk

ask-bonk Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Since last review: 0 resolved, 0 still open, 1 new.

Replaces the retryable decorator with a variadic function API.

Not re-run: api-compat, tests, docs, compat-flags, design-simplicity, jsg-gc, kj-style, memory-safety (no author changes in their files since the last review)


Reviewed commit: 4be0c016 · github run

Comment thread src/cloudflare/durable-objects.ts
Comment thread src/workerd/api/global-scope.c++ Outdated
Comment thread src/workerd/util/function-directive.c++ Outdated
Comment thread src/workerd/util/function-directive.c++ Outdated
@apeacock1991
apeacock1991 force-pushed the apeacock/STOR-5615-use-retry-directive branch from 0eed613 to dde933d Compare October 6, 2026 09:53
@apeacock1991 apeacock1991 changed the title STOR-5615: Replace @retryable with the "use retry" directive STOR-5615: Replace @retryable with retryable() Oct 6, 2026
Comment thread src/workerd/api/jsrpc-call-plan-test.c++
@apeacock1991
apeacock1991 force-pushed the apeacock/STOR-5615-use-retry-directive branch from dde933d to 95dd385 Compare October 6, 2026 10:12
@codecov-commenter

codecov-commenter commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 41.79104% with 39 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.60%. Comparing base (17ff90b) to head (4be0c01).
⚠️ Report is 48 commits behind head on main.

Files with missing lines Patch % Lines
src/workerd/api/tests/retryable-test.js 0.00% 31 Missing ⚠️
src/workerd/api/jsrpc-call-plan-test.c++ 70.58% 0 Missing and 5 partials ⚠️
src/workerd/io/worker-entrypoint-test.c++ 60.00% 0 Missing and 2 partials ⚠️
src/workerd/api/workers-module.c++ 88.88% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7617      +/-   ##
==========================================
+ Coverage   38.51%   38.60%   +0.08%     
==========================================
  Files         868      868              
  Lines      268035   268492     +457     
  Branches    25329    25428      +99     
==========================================
+ Hits       103241   103654     +413     
  Misses     150619   150619              
- Partials    14175    14219      +44     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jamesopstad
jamesopstad self-requested a review October 8, 2026 13:18

@jamesopstad jamesopstad left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think retryPolicy is attached at the wrong layer. Durable Objects are configured as exports; namespace bindings should only reference those exports.

This is observable with ctx.exports: Global::DurableActorNamespace receives the binding’s userDefinedRetryPolicy, while Global::LoopbackDurableActorNamespace explicitly passes kj::none. Consequently, calls through ctx.exports.MyDO always use the system defaults.

Could we store the policy on the durable actor/export configuration and propagate it when constructing both regular and loopback namespaces? That would give the DO consistent retry behavior regardless of whether it is accessed through env or ctx.exports, without duplicating policy across bindings.

@apeacock1991

Copy link
Copy Markdown
Contributor Author

I think retryPolicy is attached at the wrong layer. Durable Objects are configured as exports; namespace bindings should only reference those exports.

This is observable with ctx.exports: Global::DurableActorNamespace receives the binding’s userDefinedRetryPolicy, while Global::LoopbackDurableActorNamespace explicitly passes kj::none. Consequently, calls through ctx.exports.MyDO always use the system defaults.

Could we store the policy on the durable actor/export configuration and propagate it when constructing both regular and loopback namespaces? That would give the DO consistent retry behavior regardless of whether it is accessed through env or ctx.exports, without duplicating policy across bindings.

@jamesopstad do we have any data on the number of namespaces/accounts using exports? As my only concern with this approach is it requires them to be using exports, and I am not sure those have propagated very far yet. My assumption would be the vast majority of people are using the top-level binding, so limiting it to exports is going to hinder its use, even if moving to exports is easy.

Unless I am misunderstanding how exports/top-level bindings interact, but this is my assumption.

@jamesopstad

Copy link
Copy Markdown
Contributor

@jamesopstad do we have any data on the number of namespaces/accounts using exports? As my only concern with this approach is it requires them to be using exports, and I am not sure those have propagated very far yet. My assumption would be the vast majority of people are using the top-level binding, so limiting it to exports is going to hinder its use, even if moving to exports is easy.

Unless I am misunderstanding how exports/top-level bindings interact, but this is my assumption.

In cloudflare.config.ts, there are no migrations and DOs have to be configured via exports. That's where we're starting to push everyone. As this is a new feature, I think it's reasonable to require that.

It's really about the ownership model too. I think DOs, Workflows, WorkerEntrypoints etc. should be configured by the Worker that owns them. Bindings should not contain configuration.

@apeacock1991

Copy link
Copy Markdown
Contributor Author

@jamesopstad do we have any data on the number of namespaces/accounts using exports? As my only concern with this approach is it requires them to be using exports, and I am not sure those have propagated very far yet. My assumption would be the vast majority of people are using the top-level binding, so limiting it to exports is going to hinder its use, even if moving to exports is easy.
Unless I am misunderstanding how exports/top-level bindings interact, but this is my assumption.

In cloudflare.config.ts, there are no migrations and DOs have to be configured via exports. That's where we're starting to push everyone. As this is a new feature, I think it's reasonable to require that.

It's really about the ownership model too. I think DOs, Workflows, WorkerEntrypoints etc. should be configured by the Worker that owns them. Bindings should not contain configuration.

Discussed a little internally, I think it's best to support both for now. exports is great, but it's still relatively new and most, I would wager, are not using it or have migrated yet. Putting "migrate to exports to use this" in the changelog is not really great, even if it's easy enough to do.

I think we can support both, however, with exports being preferred if set, else it uses the binding's config, else the system defaults.

@apeacock1991

Copy link
Copy Markdown
Contributor Author

@jamesopstad can we also untangle these two things also? This one is purely the replacement of the decorator, which has no bearing on the bindings/exports.

I can then open another workerd PR to handle any changes to the retry policy (+ update the Workers SDK PR) and EWC (tweaks needed to map for handling both bindings + exports)

@jamesopstad

jamesopstad commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

@jamesopstad can we also untangle these two things also? This one is purely the replacement of the decorator, which has no bearing on the bindings/exports.

I can then open another workerd PR to handle any changes to the retry policy (+ update the Workers SDK PR) and EWC (tweaks needed to map for handling both bindings + exports)

Yes, I've approved this PR. I originally came back and unapproved it when I understood what was happening in the workers-sdk PR but you're right that this PR is separate to the issues we're discussing can be merged as is.

The @retryable decorator from cloudflare:durable-objects needs a build
step: V8 does not parse decorators, and Vite's Oxc transform does not
lower them. retryable() is now a plain function that marks methods on
the class prototype, usually from a static block:

    class Counter extends DurableObject {
      static {
        retryable(this.prototype.reset);
      }
      async reset() { ... }
    }

It takes any number of functions, sets the V8 private key the
decorator set, and returns undefined. If any argument is not a
function, it throws a TypeError and marks nothing. The decorator's
context checks and its warning about later decorators are gone.

The mark is on the function object, so the claim check, method lookup
and DURABLE_OBJECT_RETRIES_USERLAND gate are unchanged. An inherited
method stays marked; an override, bound copy or wrapper does not.
Methods stay on the prototype, and RPC still refuses own instance
properties.

cloudflare:durable-objects still exports only retryable, now with a
different signature. The decorator form shipped in v1.20260930.1 but
is undocumented and its effect is gated off, so we accept the risk of
breaking a Worker that uses it.
@apeacock1991
apeacock1991 force-pushed the apeacock/STOR-5615-use-retry-directive branch from 95dd385 to 4be0c01 Compare October 9, 2026 09:29
Comment thread src/workerd/api/workers-module.c++
@apeacock1991
apeacock1991 removed the request for review from edmundhung October 9, 2026 11:27
@apeacock1991
apeacock1991 merged commit 13c9b26 into main Oct 9, 2026
45 of 49 checks passed
@apeacock1991
apeacock1991 deleted the apeacock/STOR-5615-use-retry-directive branch October 9, 2026 12:54
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.

4 participants