diff --git a/.changeset/quiet-otters-retry.md b/.changeset/quiet-otters-retry.md new file mode 100644 index 00000000000..7673c470252 --- /dev/null +++ b/.changeset/quiet-otters-retry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor HTTP-date `Retry-After` values when retrying OTLP exports. diff --git a/packages/effect/src/unstable/observability/OtlpExporter.ts b/packages/effect/src/unstable/observability/OtlpExporter.ts index c02f13177ee..84cc1184425 100644 --- a/packages/effect/src/unstable/observability/OtlpExporter.ts +++ b/packages/effect/src/unstable/observability/OtlpExporter.ts @@ -26,6 +26,24 @@ import * as HttpClientError from "../../unstable/http/HttpClientError.ts" import * as HttpClientRequest from "../../unstable/http/HttpClientRequest.ts" import type { HttpBody } from "../http/HttpBody.ts" +const retryAfterDelay = (value: string | undefined): Effect.Effect => { + const seconds = Option.fromUndefinedOr(value).pipe(Option.flatMap(Num.parse)) + if (Option.isSome(seconds)) { + return Effect.succeed(Duration.seconds(seconds.value)) + } + if (value === undefined) { + return Effect.succeed(Duration.seconds(5)) + } + const timestamp = Date.parse(value) + if (Number.isNaN(timestamp)) { + return Effect.succeed(Duration.seconds(5)) + } + return Effect.map( + Clock, + (clock) => Duration.millis(Math.max(timestamp - clock.currentTimeMillisUnsafe(), 1)) + ) +} + const policy = Schedule.forever.pipe( Schedule.passthrough, Schedule.addDelay(({ output: error }) => { @@ -34,11 +52,7 @@ const policy = Schedule.forever.pipe( && error.reason._tag === "StatusCodeError" && error.reason.response.status === 429 ) { - const retryAfter = Option.fromUndefinedOr(error.reason.response.headers["retry-after"]).pipe( - Option.flatMap(Num.parse), - Option.getOrElse(() => 5) - ) - return Effect.succeed(Duration.seconds(retryAfter)) + return retryAfterDelay(error.reason.response.headers["retry-after"]) } return Effect.succeed(Duration.seconds(1)) }) diff --git a/packages/effect/test/unstable/observability/OtlpExporter.test.ts b/packages/effect/test/unstable/observability/OtlpExporter.test.ts index 8635d926e86..d3d933a3dbb 100644 --- a/packages/effect/test/unstable/observability/OtlpExporter.test.ts +++ b/packages/effect/test/unstable/observability/OtlpExporter.test.ts @@ -270,6 +270,27 @@ describe("OtlpExporter", () => { }) )) + it.effect("retries status 429 with HTTP-date retry-after delay", () => + Effect.scoped( + Effect.gen(function*() { + const { attempts, httpClient } = yield* makeHttpClient("Thu, 01 Jan 1970 00:01:00 GMT") + const exporter = yield* makeExporter(httpClient) + + exporter.push({ value: 1 }) + yield* yieldNowN(3) + + assert.strictEqual(yield* Ref.get(attempts), 1) + + yield* TestClock.adjust("5 seconds") + yield* yieldNowN(2) + assert.strictEqual(yield* Ref.get(attempts), 1) + + yield* TestClock.adjust("55 seconds") + yield* yieldNowN(2) + assert.strictEqual(yield* Ref.get(attempts), 2) + }) + )) + it.effect("uses fallback retry-after delay when header is non-numeric", () => Effect.scoped( Effect.gen(function*() {