Repository navigation
feat(appkit): configurable time limit for generate-types --wait #735
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
base: main
Are you sure you want to change the base?
Changes from all commits
776876b
7eee2af
46d19eb
f913ff3
8a1c630
4fb6ac7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,20 +31,20 @@ import { | |
| sqlTypeToMarker, | ||
| } from "./types"; | ||
| import { | ||
| describeStatusTimeout, | ||
| describeWaitTimeout, | ||
| getWarehouseState, | ||
| remainingWaitMs, | ||
| resolveWarehouseWaitTimeoutMs, | ||
| startWarehouse, | ||
| statusRequestTimeoutMs, | ||
| WarehouseStatusTimeoutError, | ||
| WarehouseWaitTimeoutError, | ||
| waitUntilRunning, | ||
| } from "./warehouse-status"; | ||
|
|
||
| const logger = createLogger("type-generator:query-registry"); | ||
|
|
||
| /** | ||
| * Upper bound on how long a `blocking`-mode preflight will wait for a starting | ||
| * warehouse to reach RUNNING before giving up (~5 min). Generous enough to ride | ||
| * out a cold start without hanging an interactive CLI invocation indefinitely. | ||
| */ | ||
| const PREFLIGHT_WAIT_MAX_MS = 300_000; | ||
|
|
||
| /** | ||
| * Numeric-context patterns for positional type inference. | ||
| * Hoisted to module scope — safe because matchAll() clones the regex internally. | ||
|
|
@@ -498,6 +498,11 @@ export function inferParameterTypes( | |
| * immediately), "blocking" waits for a starting warehouse and starts (then | ||
| * waits for) a stopped one, treating only a deleted/deleting warehouse as | ||
| * fatal. Defaults to "non-blocking". | ||
| * @param options.waitTimeoutMs - "blocking" only: how long to wait for the | ||
| * warehouse to reach RUNNING. Defaults to {@link resolveWarehouseWaitTimeoutMs} | ||
| * (`APPKIT_TYPEGEN_WAIT_TIMEOUT` seconds, else 5 min). When it runs out the | ||
| * run degrades like any environmental failure and reports the timeout in | ||
| * `environmentalDetail`. | ||
| * @returns an array of query schemas | ||
| */ | ||
| export async function generateQueriesFromDescribe( | ||
|
|
@@ -507,13 +512,17 @@ export async function generateQueriesFromDescribe( | |
| noCache?: boolean; | ||
| concurrency?: number; | ||
| mode?: PreflightMode; | ||
| waitTimeoutMs?: number; | ||
| /** Internal: epoch-ms deadline shared with the metric-view wait. */ | ||
| waitDeadline?: number; | ||
| } = {}, | ||
| ): Promise<QueryGenerationResult> { | ||
| const { | ||
| noCache = false, | ||
| concurrency: rawConcurrency = 10, | ||
| mode = "non-blocking", | ||
| } = options; | ||
| const waitTimeoutMs = resolveWarehouseWaitTimeoutMs(options.waitTimeoutMs); | ||
| const concurrency = | ||
| typeof rawConcurrency === "number" && Number.isFinite(rawConcurrency) | ||
| ? Math.max(1, Math.floor(rawConcurrency)) | ||
|
|
@@ -651,6 +660,9 @@ export async function generateQueriesFromDescribe( | |
| // gate's warning can say why generation fell back to committed types. | ||
| let hadEnvironmentalFailure = false; | ||
| let environmentalCause: "auth" | "unreachable" | "unavailable" | undefined; | ||
| // Human-readable reason for the environmental failure, when there is a more | ||
| // specific one than the coarse cause (e.g. the wait limit ran out). | ||
| let environmentalDetail: string | undefined; | ||
|
|
||
| if (uncachedQueries.length > 0) { | ||
| // One-time warehouse preflight (before issuing any DESCRIBE). A single | ||
|
|
@@ -672,7 +684,11 @@ export async function generateQueriesFromDescribe( | |
| decision = "degradeAll"; | ||
| } else { | ||
| try { | ||
| const state = await getWarehouseState(client, warehouseId); | ||
| const state = await getWarehouseState(client, warehouseId, { | ||
| timeoutMs: statusRequestTimeoutMs( | ||
| remainingWaitMs(waitTimeoutMs, options.waitDeadline), | ||
| ), | ||
| }); | ||
| decision = decidePreflight(state, mode); | ||
| if (decision === "fatal") { | ||
| // DELETED/DELETING is state-based and environmental. | ||
|
|
@@ -685,10 +701,15 @@ export async function generateQueriesFromDescribe( | |
| // poll to RUNNING. treatStoppedAsTransient rides out the stale | ||
| // pre-start STOPPED/STOPPING reading the start hasn't propagated past | ||
| // yet — only DELETED/DELETING (or the deadline) ends the wait early. | ||
| await startWarehouse(client, warehouseId); | ||
| await startWarehouse(client, warehouseId, { | ||
| timeoutMs: statusRequestTimeoutMs( | ||
| remainingWaitMs(waitTimeoutMs, options.waitDeadline), | ||
| ), | ||
| }); | ||
| const final = await waitUntilRunning(client, warehouseId, { | ||
| maxMs: PREFLIGHT_WAIT_MAX_MS, | ||
| maxMs: remainingWaitMs(waitTimeoutMs, options.waitDeadline), | ||
| treatStoppedAsTransient: true, | ||
| lastKnownState: state, | ||
| }); | ||
| if (final === "RUNNING") { | ||
| decision = "proceed"; | ||
|
|
@@ -701,7 +722,8 @@ export async function generateQueriesFromDescribe( | |
| } | ||
| if (decision === "waitThenProceed") { | ||
| const final = await waitUntilRunning(client, warehouseId, { | ||
| maxMs: PREFLIGHT_WAIT_MAX_MS, | ||
| maxMs: remainingWaitMs(waitTimeoutMs, options.waitDeadline), | ||
| lastKnownState: state, | ||
| }); | ||
| if (final === "RUNNING") { | ||
| decision = "proceed"; | ||
|
|
@@ -713,7 +735,21 @@ export async function generateQueriesFromDescribe( | |
| } | ||
| } | ||
| } catch (err) { | ||
| if (isConnectivityError(err)) { | ||
| if (err instanceof WarehouseWaitTimeoutError) { | ||
| // The wait limit ran out with the warehouse still not RUNNING. This is | ||
| // environmental: the has-types gate falls back to committed types or | ||
| // fails with this detail. | ||
| decision = "degradeAll"; | ||
| isEnvironmental = true; | ||
| environmentalCause = "unavailable"; | ||
| environmentalDetail = describeWaitTimeout(err, waitTimeoutMs); | ||
| } else if (err instanceof WarehouseStatusTimeoutError) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No test reaches this branch or its twin at |
||
| // The warehouse API itself didn't answer in time: same handling. | ||
| decision = "degradeAll"; | ||
| isEnvironmental = true; | ||
| environmentalCause = "unavailable"; | ||
| environmentalDetail = describeStatusTimeout(err); | ||
| } else if (isConnectivityError(err)) { | ||
| // Warehouse unreachable (transient outage): degrade rather than fail — | ||
| // never fail a build on a blip. Still environmental, so the caller's | ||
| // has-types gate decides warn-and-fall-back (committed types present) | ||
|
|
@@ -1080,6 +1116,9 @@ export async function generateQueriesFromDescribe( | |
| environmentalCause: hadEnvironmentalFailure | ||
| ? environmentalCause | ||
| : undefined, | ||
| environmentalDetail: hadEnvironmentalFailure | ||
| ? environmentalDetail | ||
| : undefined, | ||
| }; | ||
| } | ||
|
|
||
|
|
||
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.
0still starts a stopped warehouse. Suggest: "0checks the warehouse once (starting it if it's stopped) and doesn't wait."