-
Notifications
You must be signed in to change notification settings - Fork 101
fix: quarantine broken-warehouse orgs, skip revoked Cloudflare integrations, gate non-prod alerting crons #239
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import { assert, describe, it } from "@effect/vitest" | ||
| import { WarehouseAuthError, WarehouseConfigError, WarehouseUpstreamError } from "@maple/domain/http" | ||
| import { makeEdgeCacheService, makeMemoryBackend } from "@maple/query-engine/caching" | ||
| import { Cause, Effect } from "effect" | ||
| import { | ||
| causeHasWarehouseConfigClassError, | ||
| isOrgWarehouseQuarantined, | ||
| quarantineOnConfigClassCause, | ||
| quarantineOrgWarehouse, | ||
| } from "./warehouse-org-quarantine" | ||
|
|
||
| const authError = new WarehouseAuthError({ message: "invalid authentication token", pipeName: "p" }) | ||
|
|
||
| describe("causeHasWarehouseConfigClassError", () => { | ||
| it("matches a direct auth failure", () => { | ||
| assert.isTrue(causeHasWarehouseConfigClassError(Cause.fail(authError))) | ||
| }) | ||
|
|
||
| it("matches a config failure", () => { | ||
| assert.isTrue( | ||
| causeHasWarehouseConfigClassError( | ||
| Cause.fail(new WarehouseConfigError({ message: "unknown database", pipeName: "p" })), | ||
| ), | ||
| ) | ||
| }) | ||
|
|
||
| it("matches an auth error wrapped as the cause of a service error", () => { | ||
| const wrapped = { _tag: "@maple/http/errors/AnomalyPersistenceError", cause: authError } | ||
| assert.isTrue(causeHasWarehouseConfigClassError(Cause.fail(wrapped))) | ||
| }) | ||
|
|
||
| it("matches an auth error raised as a defect", () => { | ||
| assert.isTrue(causeHasWarehouseConfigClassError(Cause.die(authError))) | ||
| }) | ||
|
|
||
| it("does not match transient upstream failures", () => { | ||
| assert.isFalse( | ||
| causeHasWarehouseConfigClassError( | ||
| Cause.fail(new WarehouseUpstreamError({ message: "503", pipeName: "p" })), | ||
| ), | ||
| ) | ||
| }) | ||
|
|
||
| it("does not match untagged errors", () => { | ||
| assert.isFalse(causeHasWarehouseConfigClassError(Cause.fail(new Error("boom")))) | ||
| }) | ||
| }) | ||
|
|
||
| describe("org quarantine", () => { | ||
| it.effect("round-trips through the edge cache", () => | ||
| Effect.gen(function* () { | ||
| const edgeCache = makeEdgeCacheService(makeMemoryBackend()) | ||
| assert.isFalse(yield* isOrgWarehouseQuarantined(edgeCache, "org_a")) | ||
| yield* quarantineOrgWarehouse(edgeCache, "org_a", 1_000) | ||
| assert.isTrue(yield* isOrgWarehouseQuarantined(edgeCache, "org_a")) | ||
| assert.isFalse(yield* isOrgWarehouseQuarantined(edgeCache, "org_b")) | ||
| }), | ||
| ) | ||
|
|
||
| it.effect("quarantines only on config-class causes", () => | ||
| Effect.gen(function* () { | ||
| const edgeCache = makeEdgeCacheService(makeMemoryBackend()) | ||
| const transient = yield* quarantineOnConfigClassCause( | ||
| edgeCache, | ||
| "org_a", | ||
| Cause.fail(new WarehouseUpstreamError({ message: "503", pipeName: "p" })), | ||
| 1_000, | ||
| ) | ||
| assert.isFalse(transient) | ||
| assert.isFalse(yield* isOrgWarehouseQuarantined(edgeCache, "org_a")) | ||
|
|
||
| const config = yield* quarantineOnConfigClassCause(edgeCache, "org_a", Cause.fail(authError), 1_000) | ||
| assert.isTrue(config) | ||
| assert.isTrue(yield* isOrgWarehouseQuarantined(edgeCache, "org_a")) | ||
| }), | ||
| ) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import type { EdgeCacheServiceShape } from "@maple/query-engine/caching" | ||
| import { Cause, Effect, Option } from "effect" | ||
|
|
||
| /** | ||
| * Per-org quarantine for the cross-org alerting ticks (errors / anomalies / | ||
| * digests). An org whose warehouse rejects queries with an auth/config-class | ||
| * error (e.g. a Tinybird token whose workspace no longer exists) fails | ||
| * identically on every tick — retrying each 1–5 min only burns quota and floods | ||
| * the error dashboards. When a tick's per-org failure carries such an error, | ||
| * the org is parked in the edge cache and skipped until the TTL expires, which | ||
| * doubles as the automatic retry once the org's config is repaired. | ||
| * | ||
| * Scoped to warehouse-connectivity errors only: transient upstream failures | ||
| * (`WarehouseUpstreamError`) and genuine query bugs keep retrying every tick. | ||
| */ | ||
|
|
||
| const QUARANTINE_BUCKET = "warehouse-org-quarantine" | ||
| const QUARANTINE_TTL_S = 6 * 60 * 60 | ||
|
|
||
| type EdgeCache = EdgeCacheServiceShape | ||
|
|
||
| /** | ||
| * Errors that indicate the org's warehouse is unusable until an operator fixes | ||
| * its configuration — not until the next retry. | ||
| */ | ||
| const QUARANTINE_ERROR_TAGS: ReadonlySet<string> = new Set([ | ||
| "@maple/http/errors/WarehouseAuthError", | ||
| "@maple/http/errors/WarehouseConfigError", | ||
| ]) | ||
|
|
||
| const hasQuarantineTag = (value: unknown, depth = 0): boolean => { | ||
| if (depth > 8 || typeof value !== "object" || value === null) return false | ||
| const candidate = value as { readonly _tag?: unknown; readonly cause?: unknown } | ||
| if (typeof candidate._tag === "string" && QUARANTINE_ERROR_TAGS.has(candidate._tag)) return true | ||
| // Services wrap warehouse failures in their own tagged errors (e.g. | ||
| // AnomalyPersistenceError) with the original error as `cause` — walk it. | ||
| return hasQuarantineTag(candidate.cause, depth + 1) | ||
| } | ||
|
|
||
| /** Does this cause (or anything wrapped inside it) carry an auth/config-class warehouse error? */ | ||
| export const causeHasWarehouseConfigClassError = <E>(cause: Cause.Cause<E>): boolean => | ||
| cause.reasons.some((reason) => | ||
| Cause.isFailReason(reason) | ||
| ? hasQuarantineTag(reason.error) | ||
| : Cause.isDieReason(reason) && hasQuarantineTag(reason.defect), | ||
| ) | ||
|
|
||
| /** Best-effort check — a cache failure never blocks the tick, it just disables the skip. */ | ||
| export const isOrgWarehouseQuarantined = (edgeCache: EdgeCache, orgId: string) => | ||
| edgeCache | ||
| .rawGet<number>(QUARANTINE_BUCKET, orgId) | ||
| .pipe( | ||
| Effect.map(Option.isSome), | ||
| Effect.orElseSucceed(() => false), | ||
| ) | ||
|
|
||
| /** Park the org for `QUARANTINE_TTL_S`. Best-effort; failures are ignored. */ | ||
| export const quarantineOrgWarehouse = (edgeCache: EdgeCache, orgId: string, nowMs: number) => | ||
| edgeCache.rawPut(QUARANTINE_BUCKET, orgId, nowMs, QUARANTINE_TTL_S).pipe(Effect.ignore) | ||
|
|
||
| /** | ||
| * Shared per-org failure seam for the alerting ticks: when the failure is | ||
| * config-class, quarantine the org and log at Info (expected, parked); anything | ||
| * else is logged at Error as before. Returns whether the org was quarantined. | ||
| */ | ||
| export const quarantineOnConfigClassCause = <E>( | ||
| edgeCache: EdgeCache, | ||
| orgId: string, | ||
| cause: Cause.Cause<E>, | ||
| nowMs: number, | ||
| ) => | ||
| causeHasWarehouseConfigClassError(cause) | ||
| ? quarantineOrgWarehouse(edgeCache, orgId, nowMs).pipe(Effect.as(true)) | ||
| : Effect.succeed(false) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Broken-warehouse organizations are never actually parked, so the error floods this change targets keep happening
The check that decides whether to park a broken organization (
causeHasWarehouseConfigClassErroratapps/api/src/lib/warehouse-org-quarantine.ts:41-46) looks for the warehouse auth/config marker by walking an error'scause, but the ticks first repackage the warehouse failure into a persistence error whose cause is flattened to plain text, so the marker is gone and the organization keeps being retried every tick.Impact: The three dead-token organizations this PR aims to silence keep failing on every errors/anomaly tick and keep flooding the error dashboards, exactly the behavior the change was meant to stop.
How the tag is destroyed before the quarantine matcher sees it
In
AnomalyDetectionService, every warehouse call is wrapped:warehouse.compiledQuery(...).pipe(Effect.mapError(makePersistenceError))(e.g.apps/api/src/services/AnomalyDetectionService.ts:858,884).makePersistenceError(apps/api/src/services/AnomalyDetectionService.ts:116-125) builds anAnomalyPersistenceErrorwhosecauseisdescribeCause(error.cause), anddescribeCause(apps/api/src/services/AnomalyDetectionService.ts:105-114) returns a string (stack/JSON.stringify/String). So the failure reachingrunTick'scatchCauseis anAnomalyPersistenceError(_tag=@maple/http/anomalies/AnomalyPersistenceError, not inQUARANTINE_ERROR_TAGS) whose.causeis a plain string.hasQuarantineTag(apps/api/src/lib/warehouse-org-quarantine.ts:31-38) returns false for the wrapper tag, then recurses into.cause; because that value is a string (typeof value !== "object") it immediately returns false. The originalWarehouseAuthError/WarehouseConfigErrorobject — the only thing carrying@maple/http/errors/WarehouseAuthError— was discarded during wrapping, soquarantineOnConfigClassCausereturns false and the org is never parked.The same wrapping happens in
ErrorsService(apps/api/src/services/ErrorsService.ts:2357-2359maps the per-org warehouse scan throughmakePersistenceError, which also stringifies the cause at:178-190).The unit test
warehouse-org-quarantine.test.ts:27-30masks this by hand-constructing{ _tag: ..., cause: authError }with the real error object ascause, which never occurs at runtime.Prompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.