From 7f653b79d26ce182a5d3274acfec8a4c7017b654 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 15 Oct 2026 12:40:33 +0200 Subject: [PATCH] fix(infra): settle stuck observability destination updates without the API The first beta.83 prd deploy left workers-observability-{logs,traces} in state status "updating" after its 403. Alchemy resumes an interrupted update whatever the diff returns, so #1479's diff patch never applied and every prd deploy still reconciled, listed destinations, got a 403, and skipped the api Worker. reconcile now returns the stored attributes, with destinationConf wrapped as Redacted, when the last-applied url, headers and enabled flag match the desired ones. Those rows commit as "updated" without any API call and later deploys diff to noop. A real change still observes and PATCHes as before. --- patches/alchemy@2.0.0-beta.83.patch | 82 ++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/patches/alchemy@2.0.0-beta.83.patch b/patches/alchemy@2.0.0-beta.83.patch index c9c9175a1..f896c273e 100644 --- a/patches/alchemy@2.0.0-beta.83.patch +++ b/patches/alchemy@2.0.0-beta.83.patch @@ -1,5 +1,5 @@ diff --git a/lib/Cloudflare/Workers/ObservabilityDestination.js b/lib/Cloudflare/Workers/ObservabilityDestination.js -index 1cbcfbac8a32c68db0ed62a78aba2f1564141748..2f241274b02fe993b349053fbef2d2c9b77d5d9f 100644 +index 1cbcfba..10f5f89 100644 --- a/lib/Cloudflare/Workers/ObservabilityDestination.js +++ b/lib/Cloudflare/Workers/ObservabilityDestination.js @@ -96,12 +96,6 @@ export const ObservabilityDestinationProvider = () => Provider.succeed(Observabi @@ -15,8 +15,41 @@ index 1cbcfbac8a32c68db0ed62a78aba2f1564141748..2f241274b02fe993b349053fbef2d2c9 return undefined; }), read: Effect.fn(function* ({ id, output, olds }) { +@@ -121,7 +115,7 @@ export const ObservabilityDestinationProvider = () => Provider.succeed(Observabi + const observed = yield* findByName(acct, name); + return observed ? Unowned(toAttributes(observed, acct)) : undefined; + }), +- reconcile: Effect.fn(function* ({ id, news, output }) { ++ reconcile: Effect.fn(function* ({ id, news, olds, output }) { + const { accountId } = yield* yield* CloudflareEnvironment; + // Prefer the deployed name: regenerating would target a different + // resource if the generator's output for this id ever drifts. +@@ -130,6 +124,12 @@ export const ObservabilityDestinationProvider = () => Provider.succeed(Observabi + const url = news.url; + const headers = unwrapRedacted(news.headers ?? {}); + const enabled = news.enabled ?? true; ++ // maple patch: settle an unchanged destination from its stored state. ++ // Observing lists the account's destinations, which the deploy token ++ // may not do, so a resumed no-change update would otherwise 403. ++ if (output?.slug && olds !== undefined && sameDesired(olds, url, headers, enabled)) { ++ return { ...output, destinationConf: Redacted.isRedacted(output.destinationConf) ? output.destinationConf : Redacted.make(output.destinationConf) }; ++ } + // 1. Observe — the slug cached on `output` is a hint, not a + // guarantee; fall back to the name (Cloudflare enforces name + // uniqueness, and ownership of a name match has already been +@@ -248,6 +248,10 @@ const freshObserved = (accountId, slug) => findBySlug(accountId, slug).pipe(Effe + * it unless the user explicitly pinned one, so an empty desired set is + * not perpetually "dirty". + */ ++/** maple patch: whether the last-applied props already match the desired url, headers and enabled flag. */ ++const sameDesired = (olds, url, headers, enabled) => olds.url === url && ++ (olds.enabled ?? true) === enabled && ++ sameHeaders(unwrapRedacted(olds.headers ?? {}), headers); + const sameHeaders = (observed, desired) => { + const normalized = Object.fromEntries(Object.entries(observed) + .filter(([key]) => key !== "content-type" || "content-type" in desired) diff --git a/src/Cloudflare/Workers/ObservabilityDestination.ts b/src/Cloudflare/Workers/ObservabilityDestination.ts -index 5dad75a9b76c2b94f4b71cd1dfb1aa29706e2973..51f1974730e34d2a884379cdd33f2fe47bbad99b 100644 +index 5dad75a..39b6edb 100644 --- a/src/Cloudflare/Workers/ObservabilityDestination.ts +++ b/src/Cloudflare/Workers/ObservabilityDestination.ts @@ -205,12 +205,6 @@ export const ObservabilityDestinationProvider = () => @@ -32,3 +65,48 @@ index 5dad75a9b76c2b94f4b71cd1dfb1aa29706e2973..51f1974730e34d2a884379cdd33f2fe4 return undefined; }), +@@ -233,7 +227,7 @@ export const ObservabilityDestinationProvider = () => + return observed ? Unowned(toAttributes(observed, acct)) : undefined; + }), + +- reconcile: Effect.fn(function* ({ id, news, output }) { ++ reconcile: Effect.fn(function* ({ id, news, olds, output }) { + const { accountId } = yield* yield* CloudflareEnvironment; + // Prefer the deployed name: regenerating would target a different + // resource if the generator's output for this id ever drifts. +@@ -242,6 +236,17 @@ export const ObservabilityDestinationProvider = () => + const url = news.url as string; + const headers = unwrapRedacted(news.headers ?? {}); + const enabled = news.enabled ?? true; ++ // maple patch: settle an unchanged destination from its stored state. ++ // Observing lists the account's destinations, which the deploy token ++ // may not do, so a resumed no-change update would otherwise 403. ++ if (output?.slug && olds !== undefined && sameDesired(olds, url, headers, enabled)) { ++ return { ++ ...output, ++ destinationConf: Redacted.isRedacted(output.destinationConf) ++ ? output.destinationConf ++ : Redacted.make(output.destinationConf as unknown as string), ++ }; ++ } + + // 1. Observe — the slug cached on `output` is a hint, not a + // guarantee; fall back to the name (Cloudflare enforces name +@@ -405,6 +410,17 @@ const freshObserved = (accountId: string, slug: string) => + * it unless the user explicitly pinned one, so an empty desired set is + * not perpetually "dirty". + */ ++/** maple patch: whether the last-applied props already match the desired url, headers and enabled flag. */ ++const sameDesired = ( ++ olds: ObservabilityDestinationProps, ++ url: string, ++ headers: Record, ++ enabled: boolean, ++) => ++ olds.url === url && ++ (olds.enabled ?? true) === enabled && ++ sameHeaders(unwrapRedacted(olds.headers ?? {}), headers); ++ + const sameHeaders = (observed: Record, desired: Record) => { + const normalized = Object.fromEntries( + Object.entries(observed)