@@ -77,7 +77,8 @@ class RetryMetadataOutgoingFactory final: public Fetcher::OutgoingFactory {
7777 }
7878
7979 kj::Own<WorkerInterface> newSingleUseClientWithActorRetryMetadata (kj::Maybe<kj::String>,
80- kj::Maybe<IoChannelFactory::ActorRetryRequestMetadata> actorRetryRequestMetadata) override {
80+ kj::Maybe<IoChannelFactory::ActorRetryRequestMetadata> actorRetryRequestMetadata,
81+ IsActorFetchRetryAttempt isRetryAttempt) override {
8182 capturedMetadata = kj::mv (actorRetryRequestMetadata);
8283 return kj::heap<MockFetchTarget>();
8384 }
@@ -100,6 +101,7 @@ struct ReplayState {
100101 bool acceptWebSocket = false ;
101102 kj::Maybe<kj::Own<kj::WebSocket>> acceptedWebSocket;
102103 kj::Vector<IoChannelFactory::ActorRetryRequestMetadata> metadata;
104+ kj::Vector<IsActorFetchRetryAttempt> retryAttempts;
103105 kj::Vector<kj::Array<kj::byte>> requestBodies;
104106 uint requestCount = 0 ;
105107 uint webSocketRequestCount = 0 ;
@@ -192,8 +194,10 @@ class ReplayOutgoingFactory final: public Fetcher::OutgoingFactory {
192194 }
193195
194196 kj::Own<WorkerInterface> newSingleUseClientWithActorRetryMetadata (kj::Maybe<kj::String>,
195- kj::Maybe<IoChannelFactory::ActorRetryRequestMetadata> actorRetryRequestMetadata) override {
197+ kj::Maybe<IoChannelFactory::ActorRetryRequestMetadata> actorRetryRequestMetadata,
198+ IsActorFetchRetryAttempt isRetryAttempt) override {
196199 state.metadata .add (KJ_REQUIRE_NONNULL (actorRetryRequestMetadata));
200+ state.retryAttempts .add (isRetryAttempt);
197201 return kj::heap<ReplayFetchTarget>(state);
198202 }
199203
@@ -314,6 +318,28 @@ class RecordingActorChannel final: public IoChannelFactory::ActorChannel {
314318 kj::Maybe<IoChannelFactory::ActorRetryRequestMetadata>& capturedMetadata;
315319};
316320
321+ class ReplayActorChannel final : public IoChannelFactory::ActorChannel {
322+ public:
323+ ReplayActorChannel (ReplayState& state): state(state) {}
324+
325+ kj::Own<WorkerInterface> startRequest (IoChannelFactory::SubrequestMetadata metadata) override {
326+ state.metadata .add (KJ_REQUIRE_NONNULL (metadata.actorRetryRequestMetadata ));
327+ return kj::heap<ReplayFetchTarget>(state);
328+ }
329+
330+ void requireAllowsTransfer () override {
331+ KJ_UNIMPLEMENTED (" not used in this test" );
332+ }
333+
334+ kj::OneOf<kj::Array<byte>, kj::Promise<kj::Array<byte>>> getTokenMaybeSync (
335+ IoChannelFactory::ChannelTokenUsage) override {
336+ KJ_UNIMPLEMENTED (" not used in this test" );
337+ }
338+
339+ private:
340+ ReplayState& state;
341+ };
342+
317343struct ActorIoChannelFactory final : public TestFixture::DummyIoChannelFactory {
318344 ActorIoChannelFactory (TimerChannel& timer,
319345 kj::Maybe<IoChannelFactory::ActorRetryRequestMetadata>& capturedMetadata,
@@ -438,6 +464,11 @@ KJ_TEST("actor fetch updates retry metadata and rewinds the body") {
438464 KJ_EXPECT (state.metadata [1 ].isRetry == IsActorRetry::NO );
439465 KJ_EXPECT (state.metadata [2 ].isRetry == IsActorRetry::YES );
440466 KJ_EXPECT (state.metadata [3 ].isRetry == IsActorRetry::YES );
467+ KJ_ASSERT (state.retryAttempts .size () == 4 );
468+ KJ_EXPECT (state.retryAttempts [0 ] == IsActorFetchRetryAttempt::NO );
469+ KJ_EXPECT (state.retryAttempts [1 ] == IsActorFetchRetryAttempt::YES );
470+ KJ_EXPECT (state.retryAttempts [2 ] == IsActorFetchRetryAttempt::YES );
471+ KJ_EXPECT (state.retryAttempts [3 ] == IsActorFetchRetryAttempt::YES );
441472 KJ_ASSERT (state.requestBodies .size () == 4 );
442473 for (auto & body: state.requestBodies ) {
443474 KJ_EXPECT (body == " request body" _kj.asBytes ());
@@ -553,10 +584,39 @@ KJ_TEST("actor fetch stops when the retry budget expires") {
553584 KJ_EXPECT (state.retryCount == 1 );
554585}
555586
556- // Global actor factories must place the caller's metadata on the actor subrequest.
557- KJ_TEST (" GlobalActorOutgoingFactory places actor retry metadata on the actor subrequest" ) {
587+ KJ_TEST (" actor fetch replay consumes one subrequest limit unit" ) {
588+ ReplayState state{.failures = kj::arr (ReplayFailure::NOT_DELIVERED )};
589+ uint checkedSubrequestCount = 0 ;
590+ TestFixture fixture (TestFixture::SetupParams{
591+ .useRealTimers = true ,
592+ .checkedSubrequestCount = checkedSubrequestCount,
593+ });
594+ util::Autogate::initAutogateNamesForTest (
595+ {" durable-object-retries-fetch" _kj,
596+ " durable-object-retries-fetch-retry-requests" _kj},
597+ util::IgnoreAllAutogatesEnv::YES );
598+
599+ fixture.runInIoContext ([&](const TestFixture::Environment& env) {
600+ auto fetcher = env.js .alloc <Fetcher>(
601+ env.context .addObject <Fetcher::OutgoingFactory>(kj::heap<ReplicaActorOutgoingFactory>(
602+ kj::refcounted<ReplayActorChannel>(state), kj::str (" actor-id" ))),
603+ Fetcher::RequiresHostAndProtocol::YES );
604+ auto promise = fetcher->fetch (env.js , kj::str (" http://example.com" ), kj::none);
605+ return env.context .awaitJs (env.js , kj::mv (promise)).ignoreResult ().attach (kj::mv (fetcher));
606+ });
607+
608+ KJ_EXPECT (state.requestCount == 2 );
609+ KJ_ASSERT (state.metadata .size () == 2 );
610+ KJ_EXPECT (state.metadata [0 ].nonce != state.metadata [1 ].nonce );
611+ KJ_EXPECT (state.metadata [0 ].isRetry == IsActorRetry::NO );
612+ KJ_EXPECT (state.metadata [1 ].isRetry == IsActorRetry::NO );
613+ KJ_EXPECT (checkedSubrequestCount == 1 );
614+ }
615+
616+ KJ_TEST (" GlobalActorOutgoingFactory forwards metadata without counting retries against limits" ) {
558617 kj::Maybe<IoChannelFactory::ActorRetryRequestMetadata> capturedMetadata;
559618 uint channelCount = 0 ;
619+ uint checkedSubrequestCount = 0 ;
560620 kj::Vector<kj::String> locationHints;
561621 kj::Vector<kj::String> cohorts;
562622 TestFixture fixture (TestFixture::SetupParams{
@@ -566,6 +626,7 @@ KJ_TEST("GlobalActorOutgoingFactory places actor retry metadata on the actor sub
566626 return kj::rc<ActorIoChannelFactory>(
567627 timer, capturedMetadata, channelCount, locationHints, cohorts);
568628 }),
629+ .checkedSubrequestCount = checkedSubrequestCount,
569630 });
570631
571632 fixture.runInIoContext ([&](const TestFixture::Environment& env) {
@@ -581,7 +642,8 @@ KJ_TEST("GlobalActorOutgoingFactory places actor retry metadata on the actor sub
581642 .nonce = 0x123456789abcdef0 ,
582643 .createdAt = kj::UNIX_EPOCH + 123 * kj::MILLISECONDS ,
583644 .isRetry = IsActorRetry::YES ,
584- });
645+ },
646+ IsActorFetchRetryAttempt::NO );
585647
586648 KJ_IF_SOME (metadata, capturedMetadata) {
587649 KJ_EXPECT (metadata.nonce == 0x123456789abcdef0 );
@@ -597,7 +659,9 @@ KJ_TEST("GlobalActorOutgoingFactory places actor retry metadata on the actor sub
597659 .nonce = 0xfedcba9876543210 ,
598660 .createdAt = kj::UNIX_EPOCH + 456 * kj::MILLISECONDS ,
599661 .isRetry = IsActorRetry::YES ,
600- });
662+ },
663+ IsActorFetchRetryAttempt::YES );
664+ KJ_EXPECT (checkedSubrequestCount == 1 );
601665 KJ_EXPECT (channelCount == 2 );
602666 KJ_ASSERT (locationHints.size () == 2 );
603667 KJ_EXPECT (locationHints[0 ] == " location" );
@@ -608,9 +672,13 @@ KJ_TEST("GlobalActorOutgoingFactory places actor retry metadata on the actor sub
608672 });
609673}
610674
611- KJ_TEST (" ReplicaActorOutgoingFactory places actor retry metadata on the actor subrequest " ) {
675+ KJ_TEST (" ReplicaActorOutgoingFactory forwards metadata without counting retries against limits " ) {
612676 kj::Maybe<IoChannelFactory::ActorRetryRequestMetadata> capturedMetadata;
613- TestFixture fixture;
677+ uint checkedSubrequestCount = 0 ;
678+ TestFixture fixture (TestFixture::SetupParams{
679+ .useRealTimers = false ,
680+ .checkedSubrequestCount = checkedSubrequestCount,
681+ });
614682
615683 fixture.runInIoContext ([&](const TestFixture::Environment& env) {
616684 ReplicaActorOutgoingFactory factory (
@@ -622,7 +690,8 @@ KJ_TEST("ReplicaActorOutgoingFactory places actor retry metadata on the actor su
622690 .nonce = 0x123456789abcdef0 ,
623691 .createdAt = kj::UNIX_EPOCH + 123 * kj::MILLISECONDS ,
624692 .isRetry = IsActorRetry::YES ,
625- });
693+ },
694+ IsActorFetchRetryAttempt::NO );
626695
627696 KJ_IF_SOME (metadata, capturedMetadata) {
628697 KJ_EXPECT (metadata.nonce == 0x123456789abcdef0 );
@@ -638,7 +707,9 @@ KJ_TEST("ReplicaActorOutgoingFactory places actor retry metadata on the actor su
638707 .nonce = 0xfedcba9876543210 ,
639708 .createdAt = kj::UNIX_EPOCH + 456 * kj::MILLISECONDS ,
640709 .isRetry = IsActorRetry::YES ,
641- });
710+ },
711+ IsActorFetchRetryAttempt::YES );
712+ KJ_EXPECT (checkedSubrequestCount == 1 );
642713 KJ_IF_SOME (metadata, capturedMetadata) {
643714 KJ_EXPECT (metadata.nonce == 0xfedcba9876543210 );
644715 } else {
0 commit comments