@adcp/sdk 15.9.0 → 15.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/reporting/bigquery/index.d.mts +41 -20
- package/dist/lib/reporting/bigquery/index.d.ts +41 -20
- package/dist/lib/reporting/bigquery/index.js +166 -61
- package/dist/lib/reporting/bigquery/index.mjs +164 -60
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/docs/TYPE-SUMMARY.md +2 -2
- package/docs/guides/REPORTING-LEDGER.md +48 -22
- package/docs/guides/REPORTING-OPERATIONS.md +14 -3
- package/docs/llms.txt +2 -2
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/package.json +1 -1
|
@@ -11,11 +11,16 @@ import type { PostgresReportingLedgerStore, ReportingLedgerRevisionMetadataV1, R
|
|
|
11
11
|
* are the adopter's.
|
|
12
12
|
*
|
|
13
13
|
* Exactly-once loading: a batch (change-feed range, revision list, job IDs) is
|
|
14
|
-
* planned durably in PostgreSQL before any load.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* planned durably in PostgreSQL before any load. Before every submission the
|
|
15
|
+
* sink looks the deterministic job ID up in the destination project and
|
|
16
|
+
* location: a job that finished cleanly counts as loaded, a job still running
|
|
17
|
+
* is waited for (never uploaded again), and only a missing job is uploaded.
|
|
18
|
+
* A crash or timeout between the load and the cursor commit therefore replays
|
|
19
|
+
* the same batch under the same job IDs without a second upload. A failed load
|
|
20
|
+
* job is retried under a new attempt suffix only after BigQuery reports that
|
|
21
|
+
* job finished with an error, and each table keeps its own attempt count, so
|
|
22
|
+
* no batch lands twice. A load that outlasts the deadline never changes the job
|
|
23
|
+
* ID: the sink grows that table's deadline instead (persisted with the plan).
|
|
19
24
|
* Concurrent runs claim one durable plan with compare-and-swap. Replays of
|
|
20
25
|
* that plan share job IDs; stale runs cannot change its attempts or cursor.
|
|
21
26
|
*/
|
|
@@ -36,7 +41,7 @@ export interface ReportingBigQueryClientV1 {
|
|
|
36
41
|
location?: string;
|
|
37
42
|
}): {
|
|
38
43
|
getMetadata(): Promise<unknown>;
|
|
39
|
-
/**
|
|
44
|
+
/** @deprecated Unused: BigQuery does not stop a running load, so the sink never cancels. Kept so wrappers that declare it still type-check. */
|
|
40
45
|
cancel?(): Promise<unknown>;
|
|
41
46
|
};
|
|
42
47
|
query(options: {
|
|
@@ -74,7 +79,11 @@ export interface CreateBigQueryReportingWarehouseSinkOptionsV1 {
|
|
|
74
79
|
datasetId: string;
|
|
75
80
|
rowsTableId: string;
|
|
76
81
|
revisionsTableId: string;
|
|
77
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Dataset location, so jobs run in the dataset's region. Set it for any dataset outside the US and EU
|
|
84
|
+
* multi-regions: BigQuery answers a job lookup without the right location with 404, and the sink then
|
|
85
|
+
* cannot find the jobs it created.
|
|
86
|
+
*/
|
|
78
87
|
location?: string;
|
|
79
88
|
/** Restrict the sink to one account; omit for the deployment-wide feed. */
|
|
80
89
|
account_id?: string;
|
|
@@ -98,18 +107,24 @@ export interface CreateBigQueryReportingWarehouseSinkOptionsV1 {
|
|
|
98
107
|
*/
|
|
99
108
|
labels?: Readonly<Record<string, string>>;
|
|
100
109
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
110
|
+
* Base deadline for one run's work on one load. Default 10 minutes. It bounds
|
|
111
|
+
* how long a run uploads a file or waits for a running job; it does not bound
|
|
112
|
+
* the job. BigQuery does not stop a load when the client gives up (a cancel
|
|
113
|
+
* is accepted and the job finishes anyway), so the sink never cancels.
|
|
114
|
+
* Before every submission it looks the deterministic job ID up in `projectId`
|
|
115
|
+
* and `location`: a job that finished cleanly is accepted, a job that is
|
|
116
|
+
* pending or running is waited for (polled within the deadline) instead of
|
|
117
|
+
* being uploaded again, and only a missing job is uploaded. When the
|
|
118
|
+
* deadline passes, the run rejects with the plan retained and nothing
|
|
119
|
+
* advanced, and the next run checks the job again. Each such timeout doubles
|
|
120
|
+
* that table's deadline for later runs (up to 6 hours, persisted with the
|
|
121
|
+
* plan, and never changing the job ID), so an upload slower than the
|
|
122
|
+
* deadline eventually gets enough time. Set it above your slowest expected
|
|
123
|
+
* load to avoid the extra runs. Only a job that finished with an error
|
|
124
|
+
* advances the attempt. A status check that times out is bounded by this base
|
|
125
|
+
* deadline and does not grow it. This is a client-side deadline: BigQuery's
|
|
126
|
+
* server-side `jobTimeoutMs` cannot be set on a local-file load through the
|
|
127
|
+
* official client.
|
|
113
128
|
*/
|
|
114
129
|
loadTimeoutMilliseconds?: number;
|
|
115
130
|
}
|
|
@@ -153,7 +168,8 @@ export interface BigQueryReportingWarehouseSinkV1 {
|
|
|
153
168
|
* the destination tables first, or use a new sink name against new tables.
|
|
154
169
|
* Each reset starts a new job ID generation, so reloaded batches submit
|
|
155
170
|
* fresh BigQuery jobs instead of matching the finished jobs of the earlier
|
|
156
|
-
* history.
|
|
171
|
+
* history. The sink never cancels a load, so stop every worker and let its
|
|
172
|
+
* in-flight uploads and jobs finish before emptying the destination tables. Nothing holds change retention between `reset()` and the first
|
|
157
173
|
* committed batch. If `reset()` rejects after clearing the sink's state
|
|
158
174
|
* (the consumer could not be unregistered), call it again before restarting
|
|
159
175
|
* workers; it is idempotent. Needs a store with `removeFeedConsumer`.
|
|
@@ -170,6 +186,11 @@ export interface BigQueryReportingWarehouseSinkV1 {
|
|
|
170
186
|
readonly currentRowsViewSql: string;
|
|
171
187
|
}
|
|
172
188
|
export declare function createBigQueryReportingWarehouseSinkV1(options: CreateBigQueryReportingWarehouseSinkOptionsV1): BigQueryReportingWarehouseSinkV1;
|
|
189
|
+
/**
|
|
190
|
+
* How long one run may upload a file or wait for a running job: the configured
|
|
191
|
+
* base doubled once per earlier timeout of that table's load, capped at 6 hours.
|
|
192
|
+
*/
|
|
193
|
+
export declare function reportingWarehouseLoadDeadlineV1(baseMilliseconds: number, timeouts: number): number;
|
|
173
194
|
/** Deterministic load job ID for one planned change-feed range. */
|
|
174
195
|
export declare function reportingWarehouseJobIdV1(name: string, from: string | null, to: string, generation?: number): string;
|
|
175
196
|
interface WarehouseTablesV1 {
|
|
@@ -11,11 +11,16 @@ import type { PostgresReportingLedgerStore, ReportingLedgerRevisionMetadataV1, R
|
|
|
11
11
|
* are the adopter's.
|
|
12
12
|
*
|
|
13
13
|
* Exactly-once loading: a batch (change-feed range, revision list, job IDs) is
|
|
14
|
-
* planned durably in PostgreSQL before any load.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* planned durably in PostgreSQL before any load. Before every submission the
|
|
15
|
+
* sink looks the deterministic job ID up in the destination project and
|
|
16
|
+
* location: a job that finished cleanly counts as loaded, a job still running
|
|
17
|
+
* is waited for (never uploaded again), and only a missing job is uploaded.
|
|
18
|
+
* A crash or timeout between the load and the cursor commit therefore replays
|
|
19
|
+
* the same batch under the same job IDs without a second upload. A failed load
|
|
20
|
+
* job is retried under a new attempt suffix only after BigQuery reports that
|
|
21
|
+
* job finished with an error, and each table keeps its own attempt count, so
|
|
22
|
+
* no batch lands twice. A load that outlasts the deadline never changes the job
|
|
23
|
+
* ID: the sink grows that table's deadline instead (persisted with the plan).
|
|
19
24
|
* Concurrent runs claim one durable plan with compare-and-swap. Replays of
|
|
20
25
|
* that plan share job IDs; stale runs cannot change its attempts or cursor.
|
|
21
26
|
*/
|
|
@@ -36,7 +41,7 @@ export interface ReportingBigQueryClientV1 {
|
|
|
36
41
|
location?: string;
|
|
37
42
|
}): {
|
|
38
43
|
getMetadata(): Promise<unknown>;
|
|
39
|
-
/**
|
|
44
|
+
/** @deprecated Unused: BigQuery does not stop a running load, so the sink never cancels. Kept so wrappers that declare it still type-check. */
|
|
40
45
|
cancel?(): Promise<unknown>;
|
|
41
46
|
};
|
|
42
47
|
query(options: {
|
|
@@ -74,7 +79,11 @@ export interface CreateBigQueryReportingWarehouseSinkOptionsV1 {
|
|
|
74
79
|
datasetId: string;
|
|
75
80
|
rowsTableId: string;
|
|
76
81
|
revisionsTableId: string;
|
|
77
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Dataset location, so jobs run in the dataset's region. Set it for any dataset outside the US and EU
|
|
84
|
+
* multi-regions: BigQuery answers a job lookup without the right location with 404, and the sink then
|
|
85
|
+
* cannot find the jobs it created.
|
|
86
|
+
*/
|
|
78
87
|
location?: string;
|
|
79
88
|
/** Restrict the sink to one account; omit for the deployment-wide feed. */
|
|
80
89
|
account_id?: string;
|
|
@@ -98,18 +107,24 @@ export interface CreateBigQueryReportingWarehouseSinkOptionsV1 {
|
|
|
98
107
|
*/
|
|
99
108
|
labels?: Readonly<Record<string, string>>;
|
|
100
109
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
110
|
+
* Base deadline for one run's work on one load. Default 10 minutes. It bounds
|
|
111
|
+
* how long a run uploads a file or waits for a running job; it does not bound
|
|
112
|
+
* the job. BigQuery does not stop a load when the client gives up (a cancel
|
|
113
|
+
* is accepted and the job finishes anyway), so the sink never cancels.
|
|
114
|
+
* Before every submission it looks the deterministic job ID up in `projectId`
|
|
115
|
+
* and `location`: a job that finished cleanly is accepted, a job that is
|
|
116
|
+
* pending or running is waited for (polled within the deadline) instead of
|
|
117
|
+
* being uploaded again, and only a missing job is uploaded. When the
|
|
118
|
+
* deadline passes, the run rejects with the plan retained and nothing
|
|
119
|
+
* advanced, and the next run checks the job again. Each such timeout doubles
|
|
120
|
+
* that table's deadline for later runs (up to 6 hours, persisted with the
|
|
121
|
+
* plan, and never changing the job ID), so an upload slower than the
|
|
122
|
+
* deadline eventually gets enough time. Set it above your slowest expected
|
|
123
|
+
* load to avoid the extra runs. Only a job that finished with an error
|
|
124
|
+
* advances the attempt. A status check that times out is bounded by this base
|
|
125
|
+
* deadline and does not grow it. This is a client-side deadline: BigQuery's
|
|
126
|
+
* server-side `jobTimeoutMs` cannot be set on a local-file load through the
|
|
127
|
+
* official client.
|
|
113
128
|
*/
|
|
114
129
|
loadTimeoutMilliseconds?: number;
|
|
115
130
|
}
|
|
@@ -153,7 +168,8 @@ export interface BigQueryReportingWarehouseSinkV1 {
|
|
|
153
168
|
* the destination tables first, or use a new sink name against new tables.
|
|
154
169
|
* Each reset starts a new job ID generation, so reloaded batches submit
|
|
155
170
|
* fresh BigQuery jobs instead of matching the finished jobs of the earlier
|
|
156
|
-
* history.
|
|
171
|
+
* history. The sink never cancels a load, so stop every worker and let its
|
|
172
|
+
* in-flight uploads and jobs finish before emptying the destination tables. Nothing holds change retention between `reset()` and the first
|
|
157
173
|
* committed batch. If `reset()` rejects after clearing the sink's state
|
|
158
174
|
* (the consumer could not be unregistered), call it again before restarting
|
|
159
175
|
* workers; it is idempotent. Needs a store with `removeFeedConsumer`.
|
|
@@ -170,6 +186,11 @@ export interface BigQueryReportingWarehouseSinkV1 {
|
|
|
170
186
|
readonly currentRowsViewSql: string;
|
|
171
187
|
}
|
|
172
188
|
export declare function createBigQueryReportingWarehouseSinkV1(options: CreateBigQueryReportingWarehouseSinkOptionsV1): BigQueryReportingWarehouseSinkV1;
|
|
189
|
+
/**
|
|
190
|
+
* How long one run may upload a file or wait for a running job: the configured
|
|
191
|
+
* base doubled once per earlier timeout of that table's load, capped at 6 hours.
|
|
192
|
+
*/
|
|
193
|
+
export declare function reportingWarehouseLoadDeadlineV1(baseMilliseconds: number, timeouts: number): number;
|
|
173
194
|
/** Deterministic load job ID for one planned change-feed range. */
|
|
174
195
|
export declare function reportingWarehouseJobIdV1(name: string, from: string | null, to: string, generation?: number): string;
|
|
175
196
|
interface WarehouseTablesV1 {
|
|
@@ -32,7 +32,8 @@ __export(bigquery_exports, {
|
|
|
32
32
|
createBigQueryReportingWarehouseSinkV1: () => createBigQueryReportingWarehouseSinkV1,
|
|
33
33
|
reportingWarehouseCurrentRowsViewSqlV1: () => reportingWarehouseCurrentRowsViewSqlV1,
|
|
34
34
|
reportingWarehouseDefaultTablesSqlV1: () => reportingWarehouseDefaultTablesSqlV1,
|
|
35
|
-
reportingWarehouseJobIdV1: () => reportingWarehouseJobIdV1
|
|
35
|
+
reportingWarehouseJobIdV1: () => reportingWarehouseJobIdV1,
|
|
36
|
+
reportingWarehouseLoadDeadlineV1: () => reportingWarehouseLoadDeadlineV1
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(bigquery_exports);
|
|
38
39
|
var import_crypto = require("crypto");
|
|
@@ -49,14 +50,21 @@ CREATE TABLE IF NOT EXISTS adcp_reporting_warehouse_sinks (
|
|
|
49
50
|
planned_revisions JSONB,
|
|
50
51
|
planned_rows_attempt INTEGER NOT NULL DEFAULT 0 CHECK (planned_rows_attempt >= 0),
|
|
51
52
|
planned_revisions_attempt INTEGER NOT NULL DEFAULT 0 CHECK (planned_revisions_attempt >= 0),
|
|
53
|
+
planned_rows_timeouts INTEGER NOT NULL DEFAULT 0 CHECK (planned_rows_timeouts >= 0),
|
|
54
|
+
planned_revisions_timeouts INTEGER NOT NULL DEFAULT 0 CHECK (planned_revisions_timeouts >= 0),
|
|
52
55
|
reset_generation INTEGER NOT NULL DEFAULT 0 CHECK (reset_generation >= 0),
|
|
53
56
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
54
57
|
CHECK ((planned_to_cursor IS NULL) = (planned_revisions IS NULL))
|
|
55
58
|
);
|
|
56
59
|
ALTER TABLE adcp_reporting_warehouse_sinks
|
|
57
60
|
ADD COLUMN IF NOT EXISTS reset_generation INTEGER NOT NULL DEFAULT 0 CHECK (reset_generation >= 0);
|
|
61
|
+
ALTER TABLE adcp_reporting_warehouse_sinks
|
|
62
|
+
ADD COLUMN IF NOT EXISTS planned_rows_timeouts INTEGER NOT NULL DEFAULT 0 CHECK (planned_rows_timeouts >= 0);
|
|
63
|
+
ALTER TABLE adcp_reporting_warehouse_sinks
|
|
64
|
+
ADD COLUMN IF NOT EXISTS planned_revisions_timeouts INTEGER NOT NULL DEFAULT 0 CHECK (planned_revisions_timeouts >= 0);
|
|
58
65
|
`.trim();
|
|
59
66
|
const MAX_LOAD_TIMEOUT = 6 * 36e5;
|
|
67
|
+
const MAX_TIMEOUT_DOUBLINGS = 16;
|
|
60
68
|
let warnedLabelsIgnored = false;
|
|
61
69
|
const PROJECT = /^[a-z][a-z0-9-]{4,61}[a-z0-9]$/;
|
|
62
70
|
const DATASET_OR_TABLE = /^[A-Za-z_][A-Za-z0-9_]{0,1023}$/;
|
|
@@ -109,51 +117,104 @@ function createBigQueryReportingWarehouseSinkV1(options) {
|
|
|
109
117
|
...options.projectId ? { projectId: options.projectId } : {},
|
|
110
118
|
...options.location ? { location: options.location } : {}
|
|
111
119
|
} : void 0;
|
|
112
|
-
|
|
120
|
+
const uploads = /* @__PURE__ */ new Map();
|
|
121
|
+
function startUpload(tableId, jobId, lines) {
|
|
122
|
+
const entry = {
|
|
123
|
+
startedAt: Date.now(),
|
|
124
|
+
done: Promise.resolve()
|
|
125
|
+
};
|
|
126
|
+
const cleanup = async () => {
|
|
127
|
+
if (uploads.get(jobId) === entry) uploads.delete(jobId);
|
|
128
|
+
if (entry.directory) await import_fs.promises.rm(entry.directory, { recursive: true, force: true }).catch(() => {
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
entry.done = (async () => {
|
|
132
|
+
const directory = await import_fs.promises.mkdtemp(path.join((0, import_os.tmpdir)(), "adcp-warehouse-"));
|
|
133
|
+
entry.directory = directory;
|
|
134
|
+
const file = path.join(directory, `${jobId}.ndjson`);
|
|
135
|
+
await import_fs.promises.writeFile(file, lines.length ? lines.join("\n") + "\n" : "", { mode: 384 });
|
|
136
|
+
return dataset.table(tableId).load(file, {
|
|
137
|
+
jobId,
|
|
138
|
+
sourceFormat: "NEWLINE_DELIMITED_JSON",
|
|
139
|
+
writeDisposition: "WRITE_APPEND",
|
|
140
|
+
...options.location ? { location: options.location } : {}
|
|
141
|
+
});
|
|
142
|
+
})();
|
|
143
|
+
const replaced = uploads.get(jobId);
|
|
144
|
+
uploads.set(jobId, entry);
|
|
145
|
+
void entry.done.then(cleanup, cleanup);
|
|
146
|
+
if (replaced?.directory) void import_fs.promises.rm(replaced.directory, { recursive: true, force: true }).catch(() => {
|
|
147
|
+
});
|
|
148
|
+
return entry.done;
|
|
149
|
+
}
|
|
150
|
+
async function jobStatus(jobId, signal, boundMilliseconds = loadTimeout, context) {
|
|
151
|
+
let metadata;
|
|
113
152
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const code = error?.code;
|
|
120
|
-
if (code === 404 || signal?.aborted) return;
|
|
121
|
-
process.emitWarning(
|
|
122
|
-
`Cancelling BigQuery load job ${jobId} failed (${typeof code === "number" || typeof code === "string" ? code : "no status code"}); the job may still be running`,
|
|
123
|
-
{ code: "ADCP_BIGQUERY_SINK_CANCEL_FAILED" }
|
|
153
|
+
metadata = await withinDeadline(
|
|
154
|
+
() => options.bigquery.job(jobId, jobOptions).getMetadata(),
|
|
155
|
+
boundMilliseconds,
|
|
156
|
+
signal,
|
|
157
|
+
context
|
|
124
158
|
);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error?.code === 404) return "missing";
|
|
161
|
+
throw error;
|
|
125
162
|
}
|
|
163
|
+
const record = Array.isArray(metadata) ? metadata[0] : metadata;
|
|
164
|
+
if (record?.status?.state !== "DONE") return "running";
|
|
165
|
+
if (record.status.errorResult) {
|
|
166
|
+
const failure = new Error("A BigQuery load job for this batch failed; it will be retried as a new attempt");
|
|
167
|
+
Object.defineProperty(failure, "adcpJobFailed", { value: true });
|
|
168
|
+
throw failure;
|
|
169
|
+
}
|
|
170
|
+
return "done";
|
|
126
171
|
}
|
|
127
|
-
async function loadOnce(tableId, jobId, lines,
|
|
128
|
-
const loadDeadline = Math.min(loadTimeout * 2 ** Math.min(attempt, 16), MAX_LOAD_TIMEOUT);
|
|
172
|
+
async function loadOnce(role, tableId, jobId, lines, timeouts, signal) {
|
|
129
173
|
signal?.throwIfAborted();
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
174
|
+
const table = `the ${role} table "${tableId}" (timeout count ${timeouts})`;
|
|
175
|
+
let status = await jobStatus(jobId, signal, loadTimeout, table);
|
|
176
|
+
if (status === "done") return;
|
|
177
|
+
const loadDeadline = reportingWarehouseLoadDeadlineV1(loadTimeout, timeouts);
|
|
178
|
+
const deadlineAt = Date.now() + loadDeadline;
|
|
179
|
+
const timedOut = (what) => {
|
|
180
|
+
const error = new Error(
|
|
181
|
+
`${what} exceeded this run's ${loadDeadline} ms deadline for ${table}; the next run checks the job again`
|
|
182
|
+
);
|
|
183
|
+
Object.defineProperty(error, "adcpDeadlineExceeded", { value: true });
|
|
184
|
+
Object.defineProperty(error, "adcpLoadTimedOut", { value: true });
|
|
185
|
+
return error;
|
|
186
|
+
};
|
|
187
|
+
if (status === "missing") {
|
|
134
188
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
writeDisposition: "WRITE_APPEND",
|
|
140
|
-
...options.location ? { location: options.location } : {}
|
|
141
|
-
}),
|
|
142
|
-
loadDeadline,
|
|
143
|
-
signal
|
|
144
|
-
);
|
|
189
|
+
const running = uploads.get(jobId);
|
|
190
|
+
const upload = running && Date.now() - running.startedAt < MAX_LOAD_TIMEOUT ? running.done : startUpload(tableId, jobId, lines);
|
|
191
|
+
await withinDeadline(() => upload, Math.max(deadlineAt - Date.now(), 1), signal);
|
|
192
|
+
return;
|
|
145
193
|
} catch (error) {
|
|
146
|
-
if (isDeadlineExceeded(error))
|
|
194
|
+
if (isDeadlineExceeded(error)) throw timedOut("Uploading the load");
|
|
147
195
|
if (!isAlreadyExists(error)) throw error;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
196
|
+
status = await jobStatus(jobId, signal, loadTimeout, table);
|
|
197
|
+
if (status === "missing") {
|
|
198
|
+
throw new Error(
|
|
199
|
+
"BigQuery refused the load job ID as already existing, but the job cannot be found. Check that projectId and location match the dataset (location is required for datasets outside the US and EU multi-regions)",
|
|
200
|
+
{ cause: error }
|
|
201
|
+
);
|
|
202
|
+
}
|
|
154
203
|
}
|
|
155
|
-
}
|
|
156
|
-
|
|
204
|
+
}
|
|
205
|
+
const pollMilliseconds = Math.min(5e3, Math.max(250, Math.floor(loadDeadline / 10)));
|
|
206
|
+
while (status !== "done") {
|
|
207
|
+
const remaining = deadlineAt - Date.now();
|
|
208
|
+
if (remaining <= 0) throw timedOut("Waiting for the running load job");
|
|
209
|
+
await sleep(Math.min(pollMilliseconds, remaining), signal);
|
|
210
|
+
const bound = Math.max(Math.min(loadTimeout, deadlineAt - Date.now()), 1);
|
|
211
|
+
try {
|
|
212
|
+
status = await jobStatus(jobId, signal, bound, table);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (isDeadlineExceeded(error) && bound < loadTimeout) throw timedOut("Waiting for the running load job");
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
if (status === "missing") throw new Error("A BigQuery load job for this batch disappeared while it was awaited");
|
|
157
218
|
}
|
|
158
219
|
}
|
|
159
220
|
return {
|
|
@@ -168,6 +229,7 @@ function createBigQueryReportingWarehouseSinkV1(options) {
|
|
|
168
229
|
`UPDATE adcp_reporting_warehouse_sinks
|
|
169
230
|
SET committed_cursor = NULL, planned_from_cursor = NULL, planned_to_cursor = NULL,
|
|
170
231
|
planned_revisions = NULL, planned_rows_attempt = 0, planned_revisions_attempt = 0,
|
|
232
|
+
planned_rows_timeouts = 0, planned_revisions_timeouts = 0,
|
|
171
233
|
reset_generation = reset_generation + 1, updated_at = clock_timestamp()
|
|
172
234
|
WHERE sink_name = $1 AND ($2::boolean OR planned_to_cursor IS NULL)
|
|
173
235
|
RETURNING sink_name`,
|
|
@@ -201,7 +263,8 @@ function createBigQueryReportingWarehouseSinkV1(options) {
|
|
|
201
263
|
);
|
|
202
264
|
const state = (await options.db.query(
|
|
203
265
|
`SELECT committed_cursor, planned_from_cursor, planned_to_cursor, planned_revisions,
|
|
204
|
-
planned_rows_attempt, planned_revisions_attempt,
|
|
266
|
+
planned_rows_attempt, planned_revisions_attempt,
|
|
267
|
+
planned_rows_timeouts, planned_revisions_timeouts, reset_generation
|
|
205
268
|
FROM adcp_reporting_warehouse_sinks WHERE sink_name = $1`,
|
|
206
269
|
[options.name]
|
|
207
270
|
).catch(missingMigration)).rows[0];
|
|
@@ -220,7 +283,9 @@ function createBigQueryReportingWarehouseSinkV1(options) {
|
|
|
220
283
|
to: state.planned_to_cursor,
|
|
221
284
|
revisions: state.planned_revisions,
|
|
222
285
|
rowsAttempt: state.planned_rows_attempt,
|
|
223
|
-
revisionsAttempt: state.planned_revisions_attempt
|
|
286
|
+
revisionsAttempt: state.planned_revisions_attempt,
|
|
287
|
+
rowsTimeouts: state.planned_rows_timeouts,
|
|
288
|
+
revisionsTimeouts: state.planned_revisions_timeouts
|
|
224
289
|
} : void 0;
|
|
225
290
|
if (!plan) {
|
|
226
291
|
const position = {
|
|
@@ -269,12 +334,15 @@ function createBigQueryReportingWarehouseSinkV1(options) {
|
|
|
269
334
|
to: page.cursor,
|
|
270
335
|
revisions: page.records.filter((record) => record.kind === "revision").map((record) => ({ id: record.record_id, account_id: record.account_id })),
|
|
271
336
|
rowsAttempt: 0,
|
|
272
|
-
revisionsAttempt: 0
|
|
337
|
+
revisionsAttempt: 0,
|
|
338
|
+
rowsTimeouts: 0,
|
|
339
|
+
revisionsTimeouts: 0
|
|
273
340
|
};
|
|
274
341
|
const claimed = await options.db.query(
|
|
275
342
|
`UPDATE adcp_reporting_warehouse_sinks
|
|
276
343
|
SET planned_from_cursor = $2, planned_to_cursor = $3, planned_revisions = $4::jsonb,
|
|
277
|
-
planned_rows_attempt = 0, planned_revisions_attempt = 0,
|
|
344
|
+
planned_rows_attempt = 0, planned_revisions_attempt = 0,
|
|
345
|
+
planned_rows_timeouts = 0, planned_revisions_timeouts = 0, updated_at = clock_timestamp()
|
|
278
346
|
WHERE sink_name = $1 AND committed_cursor IS NOT DISTINCT FROM $2
|
|
279
347
|
AND planned_to_cursor IS NULL AND planned_revisions IS NULL AND reset_generation = $5
|
|
280
348
|
RETURNING sink_name`,
|
|
@@ -347,27 +415,52 @@ function createBigQueryReportingWarehouseSinkV1(options) {
|
|
|
347
415
|
const baseJobId = reportingWarehouseJobIdV1(options.name, plan.from, plan.to, state.reset_generation);
|
|
348
416
|
const suffix = (attempt) => attempt ? `_a${attempt}` : "";
|
|
349
417
|
const steps = [
|
|
350
|
-
{
|
|
351
|
-
|
|
418
|
+
{
|
|
419
|
+
table: options.rowsTableId,
|
|
420
|
+
role: "rows",
|
|
421
|
+
kind: "rows",
|
|
422
|
+
lines: rowLines,
|
|
423
|
+
attempt: plan.rowsAttempt,
|
|
424
|
+
timeouts: plan.rowsTimeouts,
|
|
425
|
+
timeoutColumn: "planned_rows_timeouts",
|
|
426
|
+
attemptColumn: "planned_rows_attempt"
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
table: options.revisionsTableId,
|
|
430
|
+
role: "revisions",
|
|
431
|
+
kind: "revs",
|
|
432
|
+
lines: revisionLines,
|
|
433
|
+
attempt: plan.revisionsAttempt,
|
|
434
|
+
timeouts: plan.revisionsTimeouts,
|
|
435
|
+
timeoutColumn: "planned_revisions_timeouts",
|
|
436
|
+
attemptColumn: "planned_revisions_attempt"
|
|
437
|
+
}
|
|
352
438
|
];
|
|
353
439
|
for (const step of steps) {
|
|
354
440
|
if (!step.lines.length) continue;
|
|
355
441
|
try {
|
|
356
442
|
await loadOnce(
|
|
443
|
+
step.role,
|
|
357
444
|
step.table,
|
|
358
445
|
`${baseJobId}_${step.kind}${suffix(step.attempt)}`,
|
|
359
446
|
[...step.lines],
|
|
360
|
-
step.
|
|
447
|
+
step.timeouts,
|
|
361
448
|
signal
|
|
362
449
|
);
|
|
363
450
|
} catch (error) {
|
|
364
451
|
if (isFinishedWithError(error)) {
|
|
365
|
-
const column = step.kind === "rows" ? "planned_rows_attempt" : "planned_revisions_attempt";
|
|
366
452
|
await options.db.query(
|
|
367
|
-
`UPDATE adcp_reporting_warehouse_sinks SET ${
|
|
453
|
+
`UPDATE adcp_reporting_warehouse_sinks SET ${step.attemptColumn} = ${step.attemptColumn} + 1,
|
|
368
454
|
updated_at = clock_timestamp() ${matchesPlan}`,
|
|
369
455
|
planValues
|
|
370
456
|
);
|
|
457
|
+
} else if (isLoadTimedOut(error)) {
|
|
458
|
+
await options.db.query(
|
|
459
|
+
`UPDATE adcp_reporting_warehouse_sinks
|
|
460
|
+
SET ${step.timeoutColumn} = $8::integer + 1, updated_at = clock_timestamp()
|
|
461
|
+
${matchesPlan} AND ${step.timeoutColumn} = $8::integer`,
|
|
462
|
+
[...planValues, step.timeouts]
|
|
463
|
+
);
|
|
371
464
|
}
|
|
372
465
|
throw error;
|
|
373
466
|
}
|
|
@@ -376,7 +469,7 @@ function createBigQueryReportingWarehouseSinkV1(options) {
|
|
|
376
469
|
`UPDATE adcp_reporting_warehouse_sinks
|
|
377
470
|
SET committed_cursor = $3, planned_from_cursor = NULL, planned_to_cursor = NULL,
|
|
378
471
|
planned_revisions = NULL, planned_rows_attempt = 0, planned_revisions_attempt = 0,
|
|
379
|
-
updated_at = clock_timestamp()
|
|
472
|
+
planned_rows_timeouts = 0, planned_revisions_timeouts = 0, updated_at = clock_timestamp()
|
|
380
473
|
${matchesPlan} RETURNING sink_name`,
|
|
381
474
|
planValues
|
|
382
475
|
);
|
|
@@ -395,6 +488,9 @@ function missingMigration(error) {
|
|
|
395
488
|
}
|
|
396
489
|
throw error;
|
|
397
490
|
}
|
|
491
|
+
function reportingWarehouseLoadDeadlineV1(baseMilliseconds, timeouts) {
|
|
492
|
+
return Math.min(baseMilliseconds * 2 ** Math.min(Math.max(timeouts, 0), MAX_TIMEOUT_DOUBLINGS), MAX_LOAD_TIMEOUT);
|
|
493
|
+
}
|
|
398
494
|
function reportingWarehouseJobIdV1(name, from, to, generation = 0) {
|
|
399
495
|
return `adcp_rr_${(0, import_crypto.createHash)("sha256").update(JSON.stringify(generation > 0 ? [name, from, to, generation] : [name, from, to])).digest("hex").slice(0, 40)}`;
|
|
400
496
|
}
|
|
@@ -467,7 +563,7 @@ function sourceLocalDate(start, timeZone) {
|
|
|
467
563
|
return instant.toISOString().slice(0, 10);
|
|
468
564
|
}
|
|
469
565
|
}
|
|
470
|
-
async function withinDeadline(operation, milliseconds, signal) {
|
|
566
|
+
async function withinDeadline(operation, milliseconds, signal, context) {
|
|
471
567
|
signal?.throwIfAborted();
|
|
472
568
|
let timer;
|
|
473
569
|
let onAbort;
|
|
@@ -476,7 +572,9 @@ async function withinDeadline(operation, milliseconds, signal) {
|
|
|
476
572
|
operation(),
|
|
477
573
|
new Promise((_, reject) => {
|
|
478
574
|
timer = setTimeout(() => {
|
|
479
|
-
const error = new Error(
|
|
575
|
+
const error = new Error(
|
|
576
|
+
`BigQuery call exceeded its deadline${context ? ` of ${milliseconds} ms for ${context}` : ""}; the batch will be retried`
|
|
577
|
+
);
|
|
480
578
|
Object.defineProperty(error, "adcpDeadlineExceeded", { value: true });
|
|
481
579
|
reject(error);
|
|
482
580
|
}, milliseconds);
|
|
@@ -498,25 +596,32 @@ function isAlreadyExists(error) {
|
|
|
498
596
|
const record = error;
|
|
499
597
|
return record?.code === 409 || !!record?.errors?.some((entry) => entry.reason === "duplicate");
|
|
500
598
|
}
|
|
599
|
+
function isLoadTimedOut(error) {
|
|
600
|
+
return error?.adcpLoadTimedOut === true;
|
|
601
|
+
}
|
|
602
|
+
function sleep(milliseconds, signal) {
|
|
603
|
+
signal?.throwIfAborted();
|
|
604
|
+
return new Promise((resolve, reject) => {
|
|
605
|
+
const onAbort = () => {
|
|
606
|
+
clearTimeout(timer);
|
|
607
|
+
reject(signal?.reason ?? new Error("aborted"));
|
|
608
|
+
};
|
|
609
|
+
const timer = setTimeout(() => {
|
|
610
|
+
signal?.removeEventListener("abort", onAbort);
|
|
611
|
+
resolve();
|
|
612
|
+
}, milliseconds);
|
|
613
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
614
|
+
});
|
|
615
|
+
}
|
|
501
616
|
function isFinishedWithError(error) {
|
|
502
617
|
return error?.adcpJobFailed === true;
|
|
503
618
|
}
|
|
504
|
-
function assertJobSucceeded(metadata) {
|
|
505
|
-
const record = Array.isArray(metadata) ? metadata[0] : metadata;
|
|
506
|
-
if (record?.status?.state !== "DONE") {
|
|
507
|
-
throw new Error("A BigQuery load job for this batch is still running; retry later");
|
|
508
|
-
}
|
|
509
|
-
if (record.status.errorResult) {
|
|
510
|
-
const failure = new Error("A BigQuery load job for this batch failed; it will be retried as a new attempt");
|
|
511
|
-
Object.defineProperty(failure, "adcpJobFailed", { value: true });
|
|
512
|
-
throw failure;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
619
|
// Annotate the CommonJS export names for ESM import in node:
|
|
516
620
|
0 && (module.exports = {
|
|
517
621
|
REPORTING_WAREHOUSE_SINK_MIGRATION,
|
|
518
622
|
createBigQueryReportingWarehouseSinkV1,
|
|
519
623
|
reportingWarehouseCurrentRowsViewSqlV1,
|
|
520
624
|
reportingWarehouseDefaultTablesSqlV1,
|
|
521
|
-
reportingWarehouseJobIdV1
|
|
625
|
+
reportingWarehouseJobIdV1,
|
|
626
|
+
reportingWarehouseLoadDeadlineV1
|
|
522
627
|
});
|
|
@@ -12,14 +12,21 @@ CREATE TABLE IF NOT EXISTS adcp_reporting_warehouse_sinks (
|
|
|
12
12
|
planned_revisions JSONB,
|
|
13
13
|
planned_rows_attempt INTEGER NOT NULL DEFAULT 0 CHECK (planned_rows_attempt >= 0),
|
|
14
14
|
planned_revisions_attempt INTEGER NOT NULL DEFAULT 0 CHECK (planned_revisions_attempt >= 0),
|
|
15
|
+
planned_rows_timeouts INTEGER NOT NULL DEFAULT 0 CHECK (planned_rows_timeouts >= 0),
|
|
16
|
+
planned_revisions_timeouts INTEGER NOT NULL DEFAULT 0 CHECK (planned_revisions_timeouts >= 0),
|
|
15
17
|
reset_generation INTEGER NOT NULL DEFAULT 0 CHECK (reset_generation >= 0),
|
|
16
18
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
17
19
|
CHECK ((planned_to_cursor IS NULL) = (planned_revisions IS NULL))
|
|
18
20
|
);
|
|
19
21
|
ALTER TABLE adcp_reporting_warehouse_sinks
|
|
20
22
|
ADD COLUMN IF NOT EXISTS reset_generation INTEGER NOT NULL DEFAULT 0 CHECK (reset_generation >= 0);
|
|
23
|
+
ALTER TABLE adcp_reporting_warehouse_sinks
|
|
24
|
+
ADD COLUMN IF NOT EXISTS planned_rows_timeouts INTEGER NOT NULL DEFAULT 0 CHECK (planned_rows_timeouts >= 0);
|
|
25
|
+
ALTER TABLE adcp_reporting_warehouse_sinks
|
|
26
|
+
ADD COLUMN IF NOT EXISTS planned_revisions_timeouts INTEGER NOT NULL DEFAULT 0 CHECK (planned_revisions_timeouts >= 0);
|
|
21
27
|
`.trim();
|
|
22
28
|
const MAX_LOAD_TIMEOUT = 6 * 36e5;
|
|
29
|
+
const MAX_TIMEOUT_DOUBLINGS = 16;
|
|
23
30
|
let warnedLabelsIgnored = false;
|
|
24
31
|
const PROJECT = /^[a-z][a-z0-9-]{4,61}[a-z0-9]$/;
|
|
25
32
|
const DATASET_OR_TABLE = /^[A-Za-z_][A-Za-z0-9_]{0,1023}$/;
|
|
@@ -72,51 +79,104 @@ function createBigQueryReportingWarehouseSinkV1(options) {
|
|
|
72
79
|
...options.projectId ? { projectId: options.projectId } : {},
|
|
73
80
|
...options.location ? { location: options.location } : {}
|
|
74
81
|
} : void 0;
|
|
75
|
-
|
|
82
|
+
const uploads = /* @__PURE__ */ new Map();
|
|
83
|
+
function startUpload(tableId, jobId, lines) {
|
|
84
|
+
const entry = {
|
|
85
|
+
startedAt: Date.now(),
|
|
86
|
+
done: Promise.resolve()
|
|
87
|
+
};
|
|
88
|
+
const cleanup = async () => {
|
|
89
|
+
if (uploads.get(jobId) === entry) uploads.delete(jobId);
|
|
90
|
+
if (entry.directory) await fs.rm(entry.directory, { recursive: true, force: true }).catch(() => {
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
entry.done = (async () => {
|
|
94
|
+
const directory = await fs.mkdtemp(path.join(tmpdir(), "adcp-warehouse-"));
|
|
95
|
+
entry.directory = directory;
|
|
96
|
+
const file = path.join(directory, `${jobId}.ndjson`);
|
|
97
|
+
await fs.writeFile(file, lines.length ? lines.join("\n") + "\n" : "", { mode: 384 });
|
|
98
|
+
return dataset.table(tableId).load(file, {
|
|
99
|
+
jobId,
|
|
100
|
+
sourceFormat: "NEWLINE_DELIMITED_JSON",
|
|
101
|
+
writeDisposition: "WRITE_APPEND",
|
|
102
|
+
...options.location ? { location: options.location } : {}
|
|
103
|
+
});
|
|
104
|
+
})();
|
|
105
|
+
const replaced = uploads.get(jobId);
|
|
106
|
+
uploads.set(jobId, entry);
|
|
107
|
+
void entry.done.then(cleanup, cleanup);
|
|
108
|
+
if (replaced?.directory) void fs.rm(replaced.directory, { recursive: true, force: true }).catch(() => {
|
|
109
|
+
});
|
|
110
|
+
return entry.done;
|
|
111
|
+
}
|
|
112
|
+
async function jobStatus(jobId, signal, boundMilliseconds = loadTimeout, context) {
|
|
113
|
+
let metadata;
|
|
76
114
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const code = error?.code;
|
|
83
|
-
if (code === 404 || signal?.aborted) return;
|
|
84
|
-
process.emitWarning(
|
|
85
|
-
`Cancelling BigQuery load job ${jobId} failed (${typeof code === "number" || typeof code === "string" ? code : "no status code"}); the job may still be running`,
|
|
86
|
-
{ code: "ADCP_BIGQUERY_SINK_CANCEL_FAILED" }
|
|
115
|
+
metadata = await withinDeadline(
|
|
116
|
+
() => options.bigquery.job(jobId, jobOptions).getMetadata(),
|
|
117
|
+
boundMilliseconds,
|
|
118
|
+
signal,
|
|
119
|
+
context
|
|
87
120
|
);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error?.code === 404) return "missing";
|
|
123
|
+
throw error;
|
|
88
124
|
}
|
|
125
|
+
const record = Array.isArray(metadata) ? metadata[0] : metadata;
|
|
126
|
+
if (record?.status?.state !== "DONE") return "running";
|
|
127
|
+
if (record.status.errorResult) {
|
|
128
|
+
const failure = new Error("A BigQuery load job for this batch failed; it will be retried as a new attempt");
|
|
129
|
+
Object.defineProperty(failure, "adcpJobFailed", { value: true });
|
|
130
|
+
throw failure;
|
|
131
|
+
}
|
|
132
|
+
return "done";
|
|
89
133
|
}
|
|
90
|
-
async function loadOnce(tableId, jobId, lines,
|
|
91
|
-
const loadDeadline = Math.min(loadTimeout * 2 ** Math.min(attempt, 16), MAX_LOAD_TIMEOUT);
|
|
134
|
+
async function loadOnce(role, tableId, jobId, lines, timeouts, signal) {
|
|
92
135
|
signal?.throwIfAborted();
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
136
|
+
const table = `the ${role} table "${tableId}" (timeout count ${timeouts})`;
|
|
137
|
+
let status = await jobStatus(jobId, signal, loadTimeout, table);
|
|
138
|
+
if (status === "done") return;
|
|
139
|
+
const loadDeadline = reportingWarehouseLoadDeadlineV1(loadTimeout, timeouts);
|
|
140
|
+
const deadlineAt = Date.now() + loadDeadline;
|
|
141
|
+
const timedOut = (what) => {
|
|
142
|
+
const error = new Error(
|
|
143
|
+
`${what} exceeded this run's ${loadDeadline} ms deadline for ${table}; the next run checks the job again`
|
|
144
|
+
);
|
|
145
|
+
Object.defineProperty(error, "adcpDeadlineExceeded", { value: true });
|
|
146
|
+
Object.defineProperty(error, "adcpLoadTimedOut", { value: true });
|
|
147
|
+
return error;
|
|
148
|
+
};
|
|
149
|
+
if (status === "missing") {
|
|
97
150
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
writeDisposition: "WRITE_APPEND",
|
|
103
|
-
...options.location ? { location: options.location } : {}
|
|
104
|
-
}),
|
|
105
|
-
loadDeadline,
|
|
106
|
-
signal
|
|
107
|
-
);
|
|
151
|
+
const running = uploads.get(jobId);
|
|
152
|
+
const upload = running && Date.now() - running.startedAt < MAX_LOAD_TIMEOUT ? running.done : startUpload(tableId, jobId, lines);
|
|
153
|
+
await withinDeadline(() => upload, Math.max(deadlineAt - Date.now(), 1), signal);
|
|
154
|
+
return;
|
|
108
155
|
} catch (error) {
|
|
109
|
-
if (isDeadlineExceeded(error))
|
|
156
|
+
if (isDeadlineExceeded(error)) throw timedOut("Uploading the load");
|
|
110
157
|
if (!isAlreadyExists(error)) throw error;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
158
|
+
status = await jobStatus(jobId, signal, loadTimeout, table);
|
|
159
|
+
if (status === "missing") {
|
|
160
|
+
throw new Error(
|
|
161
|
+
"BigQuery refused the load job ID as already existing, but the job cannot be found. Check that projectId and location match the dataset (location is required for datasets outside the US and EU multi-regions)",
|
|
162
|
+
{ cause: error }
|
|
163
|
+
);
|
|
164
|
+
}
|
|
117
165
|
}
|
|
118
|
-
}
|
|
119
|
-
|
|
166
|
+
}
|
|
167
|
+
const pollMilliseconds = Math.min(5e3, Math.max(250, Math.floor(loadDeadline / 10)));
|
|
168
|
+
while (status !== "done") {
|
|
169
|
+
const remaining = deadlineAt - Date.now();
|
|
170
|
+
if (remaining <= 0) throw timedOut("Waiting for the running load job");
|
|
171
|
+
await sleep(Math.min(pollMilliseconds, remaining), signal);
|
|
172
|
+
const bound = Math.max(Math.min(loadTimeout, deadlineAt - Date.now()), 1);
|
|
173
|
+
try {
|
|
174
|
+
status = await jobStatus(jobId, signal, bound, table);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (isDeadlineExceeded(error) && bound < loadTimeout) throw timedOut("Waiting for the running load job");
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
if (status === "missing") throw new Error("A BigQuery load job for this batch disappeared while it was awaited");
|
|
120
180
|
}
|
|
121
181
|
}
|
|
122
182
|
return {
|
|
@@ -131,6 +191,7 @@ function createBigQueryReportingWarehouseSinkV1(options) {
|
|
|
131
191
|
`UPDATE adcp_reporting_warehouse_sinks
|
|
132
192
|
SET committed_cursor = NULL, planned_from_cursor = NULL, planned_to_cursor = NULL,
|
|
133
193
|
planned_revisions = NULL, planned_rows_attempt = 0, planned_revisions_attempt = 0,
|
|
194
|
+
planned_rows_timeouts = 0, planned_revisions_timeouts = 0,
|
|
134
195
|
reset_generation = reset_generation + 1, updated_at = clock_timestamp()
|
|
135
196
|
WHERE sink_name = $1 AND ($2::boolean OR planned_to_cursor IS NULL)
|
|
136
197
|
RETURNING sink_name`,
|
|
@@ -164,7 +225,8 @@ function createBigQueryReportingWarehouseSinkV1(options) {
|
|
|
164
225
|
);
|
|
165
226
|
const state = (await options.db.query(
|
|
166
227
|
`SELECT committed_cursor, planned_from_cursor, planned_to_cursor, planned_revisions,
|
|
167
|
-
planned_rows_attempt, planned_revisions_attempt,
|
|
228
|
+
planned_rows_attempt, planned_revisions_attempt,
|
|
229
|
+
planned_rows_timeouts, planned_revisions_timeouts, reset_generation
|
|
168
230
|
FROM adcp_reporting_warehouse_sinks WHERE sink_name = $1`,
|
|
169
231
|
[options.name]
|
|
170
232
|
).catch(missingMigration)).rows[0];
|
|
@@ -183,7 +245,9 @@ function createBigQueryReportingWarehouseSinkV1(options) {
|
|
|
183
245
|
to: state.planned_to_cursor,
|
|
184
246
|
revisions: state.planned_revisions,
|
|
185
247
|
rowsAttempt: state.planned_rows_attempt,
|
|
186
|
-
revisionsAttempt: state.planned_revisions_attempt
|
|
248
|
+
revisionsAttempt: state.planned_revisions_attempt,
|
|
249
|
+
rowsTimeouts: state.planned_rows_timeouts,
|
|
250
|
+
revisionsTimeouts: state.planned_revisions_timeouts
|
|
187
251
|
} : void 0;
|
|
188
252
|
if (!plan) {
|
|
189
253
|
const position = {
|
|
@@ -232,12 +296,15 @@ function createBigQueryReportingWarehouseSinkV1(options) {
|
|
|
232
296
|
to: page.cursor,
|
|
233
297
|
revisions: page.records.filter((record) => record.kind === "revision").map((record) => ({ id: record.record_id, account_id: record.account_id })),
|
|
234
298
|
rowsAttempt: 0,
|
|
235
|
-
revisionsAttempt: 0
|
|
299
|
+
revisionsAttempt: 0,
|
|
300
|
+
rowsTimeouts: 0,
|
|
301
|
+
revisionsTimeouts: 0
|
|
236
302
|
};
|
|
237
303
|
const claimed = await options.db.query(
|
|
238
304
|
`UPDATE adcp_reporting_warehouse_sinks
|
|
239
305
|
SET planned_from_cursor = $2, planned_to_cursor = $3, planned_revisions = $4::jsonb,
|
|
240
|
-
planned_rows_attempt = 0, planned_revisions_attempt = 0,
|
|
306
|
+
planned_rows_attempt = 0, planned_revisions_attempt = 0,
|
|
307
|
+
planned_rows_timeouts = 0, planned_revisions_timeouts = 0, updated_at = clock_timestamp()
|
|
241
308
|
WHERE sink_name = $1 AND committed_cursor IS NOT DISTINCT FROM $2
|
|
242
309
|
AND planned_to_cursor IS NULL AND planned_revisions IS NULL AND reset_generation = $5
|
|
243
310
|
RETURNING sink_name`,
|
|
@@ -310,27 +377,52 @@ function createBigQueryReportingWarehouseSinkV1(options) {
|
|
|
310
377
|
const baseJobId = reportingWarehouseJobIdV1(options.name, plan.from, plan.to, state.reset_generation);
|
|
311
378
|
const suffix = (attempt) => attempt ? `_a${attempt}` : "";
|
|
312
379
|
const steps = [
|
|
313
|
-
{
|
|
314
|
-
|
|
380
|
+
{
|
|
381
|
+
table: options.rowsTableId,
|
|
382
|
+
role: "rows",
|
|
383
|
+
kind: "rows",
|
|
384
|
+
lines: rowLines,
|
|
385
|
+
attempt: plan.rowsAttempt,
|
|
386
|
+
timeouts: plan.rowsTimeouts,
|
|
387
|
+
timeoutColumn: "planned_rows_timeouts",
|
|
388
|
+
attemptColumn: "planned_rows_attempt"
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
table: options.revisionsTableId,
|
|
392
|
+
role: "revisions",
|
|
393
|
+
kind: "revs",
|
|
394
|
+
lines: revisionLines,
|
|
395
|
+
attempt: plan.revisionsAttempt,
|
|
396
|
+
timeouts: plan.revisionsTimeouts,
|
|
397
|
+
timeoutColumn: "planned_revisions_timeouts",
|
|
398
|
+
attemptColumn: "planned_revisions_attempt"
|
|
399
|
+
}
|
|
315
400
|
];
|
|
316
401
|
for (const step of steps) {
|
|
317
402
|
if (!step.lines.length) continue;
|
|
318
403
|
try {
|
|
319
404
|
await loadOnce(
|
|
405
|
+
step.role,
|
|
320
406
|
step.table,
|
|
321
407
|
`${baseJobId}_${step.kind}${suffix(step.attempt)}`,
|
|
322
408
|
[...step.lines],
|
|
323
|
-
step.
|
|
409
|
+
step.timeouts,
|
|
324
410
|
signal
|
|
325
411
|
);
|
|
326
412
|
} catch (error) {
|
|
327
413
|
if (isFinishedWithError(error)) {
|
|
328
|
-
const column = step.kind === "rows" ? "planned_rows_attempt" : "planned_revisions_attempt";
|
|
329
414
|
await options.db.query(
|
|
330
|
-
`UPDATE adcp_reporting_warehouse_sinks SET ${
|
|
415
|
+
`UPDATE adcp_reporting_warehouse_sinks SET ${step.attemptColumn} = ${step.attemptColumn} + 1,
|
|
331
416
|
updated_at = clock_timestamp() ${matchesPlan}`,
|
|
332
417
|
planValues
|
|
333
418
|
);
|
|
419
|
+
} else if (isLoadTimedOut(error)) {
|
|
420
|
+
await options.db.query(
|
|
421
|
+
`UPDATE adcp_reporting_warehouse_sinks
|
|
422
|
+
SET ${step.timeoutColumn} = $8::integer + 1, updated_at = clock_timestamp()
|
|
423
|
+
${matchesPlan} AND ${step.timeoutColumn} = $8::integer`,
|
|
424
|
+
[...planValues, step.timeouts]
|
|
425
|
+
);
|
|
334
426
|
}
|
|
335
427
|
throw error;
|
|
336
428
|
}
|
|
@@ -339,7 +431,7 @@ function createBigQueryReportingWarehouseSinkV1(options) {
|
|
|
339
431
|
`UPDATE adcp_reporting_warehouse_sinks
|
|
340
432
|
SET committed_cursor = $3, planned_from_cursor = NULL, planned_to_cursor = NULL,
|
|
341
433
|
planned_revisions = NULL, planned_rows_attempt = 0, planned_revisions_attempt = 0,
|
|
342
|
-
updated_at = clock_timestamp()
|
|
434
|
+
planned_rows_timeouts = 0, planned_revisions_timeouts = 0, updated_at = clock_timestamp()
|
|
343
435
|
${matchesPlan} RETURNING sink_name`,
|
|
344
436
|
planValues
|
|
345
437
|
);
|
|
@@ -358,6 +450,9 @@ function missingMigration(error) {
|
|
|
358
450
|
}
|
|
359
451
|
throw error;
|
|
360
452
|
}
|
|
453
|
+
function reportingWarehouseLoadDeadlineV1(baseMilliseconds, timeouts) {
|
|
454
|
+
return Math.min(baseMilliseconds * 2 ** Math.min(Math.max(timeouts, 0), MAX_TIMEOUT_DOUBLINGS), MAX_LOAD_TIMEOUT);
|
|
455
|
+
}
|
|
361
456
|
function reportingWarehouseJobIdV1(name, from, to, generation = 0) {
|
|
362
457
|
return `adcp_rr_${createHash("sha256").update(JSON.stringify(generation > 0 ? [name, from, to, generation] : [name, from, to])).digest("hex").slice(0, 40)}`;
|
|
363
458
|
}
|
|
@@ -430,7 +525,7 @@ function sourceLocalDate(start, timeZone) {
|
|
|
430
525
|
return instant.toISOString().slice(0, 10);
|
|
431
526
|
}
|
|
432
527
|
}
|
|
433
|
-
async function withinDeadline(operation, milliseconds, signal) {
|
|
528
|
+
async function withinDeadline(operation, milliseconds, signal, context) {
|
|
434
529
|
signal?.throwIfAborted();
|
|
435
530
|
let timer;
|
|
436
531
|
let onAbort;
|
|
@@ -439,7 +534,9 @@ async function withinDeadline(operation, milliseconds, signal) {
|
|
|
439
534
|
operation(),
|
|
440
535
|
new Promise((_, reject) => {
|
|
441
536
|
timer = setTimeout(() => {
|
|
442
|
-
const error = new Error(
|
|
537
|
+
const error = new Error(
|
|
538
|
+
`BigQuery call exceeded its deadline${context ? ` of ${milliseconds} ms for ${context}` : ""}; the batch will be retried`
|
|
539
|
+
);
|
|
443
540
|
Object.defineProperty(error, "adcpDeadlineExceeded", { value: true });
|
|
444
541
|
reject(error);
|
|
445
542
|
}, milliseconds);
|
|
@@ -461,24 +558,31 @@ function isAlreadyExists(error) {
|
|
|
461
558
|
const record = error;
|
|
462
559
|
return record?.code === 409 || !!record?.errors?.some((entry) => entry.reason === "duplicate");
|
|
463
560
|
}
|
|
561
|
+
function isLoadTimedOut(error) {
|
|
562
|
+
return error?.adcpLoadTimedOut === true;
|
|
563
|
+
}
|
|
564
|
+
function sleep(milliseconds, signal) {
|
|
565
|
+
signal?.throwIfAborted();
|
|
566
|
+
return new Promise((resolve, reject) => {
|
|
567
|
+
const onAbort = () => {
|
|
568
|
+
clearTimeout(timer);
|
|
569
|
+
reject(signal?.reason ?? new Error("aborted"));
|
|
570
|
+
};
|
|
571
|
+
const timer = setTimeout(() => {
|
|
572
|
+
signal?.removeEventListener("abort", onAbort);
|
|
573
|
+
resolve();
|
|
574
|
+
}, milliseconds);
|
|
575
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
576
|
+
});
|
|
577
|
+
}
|
|
464
578
|
function isFinishedWithError(error) {
|
|
465
579
|
return error?.adcpJobFailed === true;
|
|
466
580
|
}
|
|
467
|
-
function assertJobSucceeded(metadata) {
|
|
468
|
-
const record = Array.isArray(metadata) ? metadata[0] : metadata;
|
|
469
|
-
if (record?.status?.state !== "DONE") {
|
|
470
|
-
throw new Error("A BigQuery load job for this batch is still running; retry later");
|
|
471
|
-
}
|
|
472
|
-
if (record.status.errorResult) {
|
|
473
|
-
const failure = new Error("A BigQuery load job for this batch failed; it will be retried as a new attempt");
|
|
474
|
-
Object.defineProperty(failure, "adcpJobFailed", { value: true });
|
|
475
|
-
throw failure;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
581
|
export {
|
|
479
582
|
REPORTING_WAREHOUSE_SINK_MIGRATION,
|
|
480
583
|
createBigQueryReportingWarehouseSinkV1,
|
|
481
584
|
reportingWarehouseCurrentRowsViewSqlV1,
|
|
482
585
|
reportingWarehouseDefaultTablesSqlV1,
|
|
483
|
-
reportingWarehouseJobIdV1
|
|
586
|
+
reportingWarehouseJobIdV1,
|
|
587
|
+
reportingWarehouseLoadDeadlineV1
|
|
484
588
|
};
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
"source_sha": "4e553ad955f83b49c7d221ab5c3ff78237ad02e3",
|
|
5
5
|
"source_tarball_sha256": "580656d6466ef9f0d1119985e6726c2efea718dc671e2ad30957fcb2fd54af0f",
|
|
6
6
|
"upstream_adcp_version": "2.5.3",
|
|
7
|
-
"synced_at": "2026-10-
|
|
7
|
+
"synced_at": "2026-10-14T11:39:27.882Z"
|
|
8
8
|
}
|
|
@@ -197,7 +197,7 @@ export interface UpstreamTrafficQueryResult {
|
|
|
197
197
|
*/
|
|
198
198
|
export declare function runValidations(validations: StoryboardValidation[], context: ValidationContext): ValidationResult[];
|
|
199
199
|
/** Capability semver emitted in run summaries and used for advisory expiry. */
|
|
200
|
-
export declare const RUNNER_CAPABILITY_VERSION = "15.9.
|
|
200
|
+
export declare const RUNNER_CAPABILITY_VERSION = "15.9.1";
|
|
201
201
|
/** True when a failed validation contributes to the owning step's grade. */
|
|
202
202
|
export declare function validationFailsStep(result: ValidationResult): boolean;
|
|
203
203
|
/**
|
|
@@ -197,7 +197,7 @@ export interface UpstreamTrafficQueryResult {
|
|
|
197
197
|
*/
|
|
198
198
|
export declare function runValidations(validations: StoryboardValidation[], context: ValidationContext): ValidationResult[];
|
|
199
199
|
/** Capability semver emitted in run summaries and used for advisory expiry. */
|
|
200
|
-
export declare const RUNNER_CAPABILITY_VERSION = "15.9.
|
|
200
|
+
export declare const RUNNER_CAPABILITY_VERSION = "15.9.1";
|
|
201
201
|
/** True when a failed validation contributes to the owning step's grade. */
|
|
202
202
|
export declare function validationFailsStep(result: ValidationResult): boolean;
|
|
203
203
|
/**
|
package/dist/lib/version.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AdCP SDK library version
|
|
3
3
|
*/
|
|
4
|
-
export declare const LIBRARY_VERSION = "15.9.
|
|
4
|
+
export declare const LIBRARY_VERSION = "15.9.1";
|
|
5
5
|
/**
|
|
6
6
|
* AdCP specification version this library is built for
|
|
7
7
|
*/
|
|
@@ -33,10 +33,10 @@ export type AdcpVersion = (typeof COMPATIBLE_ADCP_VERSIONS)[number];
|
|
|
33
33
|
* Full version information
|
|
34
34
|
*/
|
|
35
35
|
export declare const VERSION_INFO: {
|
|
36
|
-
readonly library: "15.9.
|
|
36
|
+
readonly library: "15.9.1";
|
|
37
37
|
readonly adcp: "3.2.4";
|
|
38
38
|
readonly compatibleVersions: readonly ["v2.5", "v2.6", "v3", "3.0.0", "3.0", "3.0.1", "3.0.2", "3.0.3", "3.0.4", "3.0.5", "3.0.6", "3.0.7", "3.0.8", "3.0.9", "3.0.10", "3.0.11", "3.0.12", "3.0.13", "3.0.14", "3.0.15", "3.0.16", "3.0.17", "3.0.18", "3.0.19", "3.0.20", "3.0.21", "3.0.22", "3.0.23", "3.0.24", "3.0.25", "3.1.0", "3.1", "3.1.1", "3.1.2", "3.1.3", "3.1.4", "3.1.5", "3.1.6", "3.1.7", "3.1.8", "3.1.9", "3.1.10", "3.1.11", "3.1.12", "3.1.13", "3.1.14", "3.1.15", "3.1.16", "3.1.17", "3.1.18", "3.1.19", "3.1.20", "3.1.21", "3.1.22", "3.1.23", "3.1.24", "3.2.1", "3.2", "3.2.2", "3.2.3", "3.2.4"];
|
|
39
|
-
readonly generatedAt: "2026-10-
|
|
39
|
+
readonly generatedAt: "2026-10-14T11:09:46.080Z";
|
|
40
40
|
};
|
|
41
41
|
/**
|
|
42
42
|
* Get the AdCP specification version this library is built for
|
package/dist/lib/version.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AdCP SDK library version
|
|
3
3
|
*/
|
|
4
|
-
export declare const LIBRARY_VERSION = "15.9.
|
|
4
|
+
export declare const LIBRARY_VERSION = "15.9.1";
|
|
5
5
|
/**
|
|
6
6
|
* AdCP specification version this library is built for
|
|
7
7
|
*/
|
|
@@ -33,10 +33,10 @@ export type AdcpVersion = (typeof COMPATIBLE_ADCP_VERSIONS)[number];
|
|
|
33
33
|
* Full version information
|
|
34
34
|
*/
|
|
35
35
|
export declare const VERSION_INFO: {
|
|
36
|
-
readonly library: "15.9.
|
|
36
|
+
readonly library: "15.9.1";
|
|
37
37
|
readonly adcp: "3.2.4";
|
|
38
38
|
readonly compatibleVersions: readonly ["v2.5", "v2.6", "v3", "3.0.0", "3.0", "3.0.1", "3.0.2", "3.0.3", "3.0.4", "3.0.5", "3.0.6", "3.0.7", "3.0.8", "3.0.9", "3.0.10", "3.0.11", "3.0.12", "3.0.13", "3.0.14", "3.0.15", "3.0.16", "3.0.17", "3.0.18", "3.0.19", "3.0.20", "3.0.21", "3.0.22", "3.0.23", "3.0.24", "3.0.25", "3.1.0", "3.1", "3.1.1", "3.1.2", "3.1.3", "3.1.4", "3.1.5", "3.1.6", "3.1.7", "3.1.8", "3.1.9", "3.1.10", "3.1.11", "3.1.12", "3.1.13", "3.1.14", "3.1.15", "3.1.16", "3.1.17", "3.1.18", "3.1.19", "3.1.20", "3.1.21", "3.1.22", "3.1.23", "3.1.24", "3.2.1", "3.2", "3.2.2", "3.2.3", "3.2.4"];
|
|
39
|
-
readonly generatedAt: "2026-10-
|
|
39
|
+
readonly generatedAt: "2026-10-14T11:09:46.080Z";
|
|
40
40
|
};
|
|
41
41
|
/**
|
|
42
42
|
* Get the AdCP specification version this library is built for
|
package/dist/lib/version.js
CHANGED
|
@@ -31,7 +31,7 @@ __export(version_exports, {
|
|
|
31
31
|
toReleasePrecisionVersion: () => toReleasePrecisionVersion
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(version_exports);
|
|
34
|
-
const LIBRARY_VERSION = "15.9.
|
|
34
|
+
const LIBRARY_VERSION = "15.9.1";
|
|
35
35
|
const ADCP_VERSION = "3.2.4";
|
|
36
36
|
const ADCP_MAJOR_VERSION = 3;
|
|
37
37
|
const COMPATIBLE_ADCP_VERSIONS = [
|
|
@@ -98,10 +98,10 @@ const COMPATIBLE_ADCP_VERSIONS = [
|
|
|
98
98
|
"3.2.4"
|
|
99
99
|
];
|
|
100
100
|
const VERSION_INFO = {
|
|
101
|
-
library: "15.9.
|
|
101
|
+
library: "15.9.1",
|
|
102
102
|
adcp: "3.2.4",
|
|
103
103
|
compatibleVersions: COMPATIBLE_ADCP_VERSIONS,
|
|
104
|
-
generatedAt: "2026-10-
|
|
104
|
+
generatedAt: "2026-10-14T11:09:46.080Z"
|
|
105
105
|
};
|
|
106
106
|
function getAdcpVersion() {
|
|
107
107
|
return ADCP_VERSION;
|
package/dist/lib/version.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const LIBRARY_VERSION = "15.9.
|
|
1
|
+
const LIBRARY_VERSION = "15.9.1";
|
|
2
2
|
const ADCP_VERSION = "3.2.4";
|
|
3
3
|
const ADCP_MAJOR_VERSION = 3;
|
|
4
4
|
const COMPATIBLE_ADCP_VERSIONS = [
|
|
@@ -65,10 +65,10 @@ const COMPATIBLE_ADCP_VERSIONS = [
|
|
|
65
65
|
"3.2.4"
|
|
66
66
|
];
|
|
67
67
|
const VERSION_INFO = {
|
|
68
|
-
library: "15.9.
|
|
68
|
+
library: "15.9.1",
|
|
69
69
|
adcp: "3.2.4",
|
|
70
70
|
compatibleVersions: COMPATIBLE_ADCP_VERSIONS,
|
|
71
|
-
generatedAt: "2026-10-
|
|
71
|
+
generatedAt: "2026-10-14T11:09:46.080Z"
|
|
72
72
|
};
|
|
73
73
|
function getAdcpVersion() {
|
|
74
74
|
return ADCP_VERSION;
|
package/docs/TYPE-SUMMARY.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# AdCP Type Summary
|
|
2
2
|
|
|
3
|
-
> Generated at: 2026-10-
|
|
4
|
-
> @adcp/sdk v15.9.
|
|
3
|
+
> Generated at: 2026-10-14
|
|
4
|
+
> @adcp/sdk v15.9.1
|
|
5
5
|
|
|
6
6
|
Curated reference of the types that matter for using the AdCP client. For full generated types see `src/lib/types/tools.generated.ts` and `src/lib/types/core.generated.ts`.
|
|
7
7
|
|
|
@@ -911,29 +911,53 @@ setInterval(() => {
|
|
|
911
911
|
`revision_number`, `finality`, `period_date` (the period's date in its source
|
|
912
912
|
timezone) and `ordinal` to your mapped columns.
|
|
913
913
|
- Batches are planned durably before loading, and job IDs derive from the
|
|
914
|
-
change-feed range.
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
only.
|
|
914
|
+
change-feed range. Before every submission the sink looks that job ID up in
|
|
915
|
+
the destination project and location: a job that finished cleanly counts as
|
|
916
|
+
loaded, a job that is pending or running is waited for (never uploaded
|
|
917
|
+
again), and only a missing job (404) is uploaded. A crash or timeout between a
|
|
918
|
+
load and the cursor commit therefore replays the same batch under the same
|
|
919
|
+
job ID without a second upload. An upload an earlier run abandoned at its
|
|
920
|
+
deadline is still running in the same process (the official client cannot
|
|
921
|
+
abort it), so the next run waits for it, within its own deadline, rather than
|
|
922
|
+
starting another, however long ago it started (that age includes the gap
|
|
923
|
+
between runs). If that wait outlasts the run's deadline it counts as a timeout
|
|
924
|
+
and the deadline grows. Only an upload still pending after 6 hours, the
|
|
925
|
+
largest deadline, is abandoned and replaced; BigQuery refuses a duplicate job
|
|
926
|
+
ID, so a replacement can never load the batch twice. A job that finished with
|
|
927
|
+
an error is retried under a new attempt for that table only. Deadline and
|
|
928
|
+
timeout errors name the table (`rows` or `revisions`, and its ID), the
|
|
929
|
+
deadline in force and that table's timeout count, never a file path or
|
|
930
|
+
credential.
|
|
918
931
|
- `projectId` scopes both load submission and replay status checks. When it is
|
|
919
932
|
omitted, both use the BigQuery client's default project. Each replica must
|
|
920
|
-
be able to read the BigQuery jobs submitted by the other replicas.
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
the
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
`
|
|
933
|
+
be able to read the BigQuery jobs submitted by the other replicas. Set
|
|
934
|
+
`location` for any dataset outside the US and EU multi-regions: BigQuery
|
|
935
|
+
answers a job lookup without the right location with 404, and the sink then
|
|
936
|
+
cannot find the jobs it created.
|
|
937
|
+
- `loadTimeoutMilliseconds` (default 10 minutes) is the base deadline for one
|
|
938
|
+
run's work on one load: uploading the file, or waiting for a job that is still
|
|
939
|
+
running. It bounds a run, not the job. BigQuery does not stop a load when the
|
|
940
|
+
client gives up (a cancel is accepted and the job finishes anyway), and it
|
|
941
|
+
refuses a resubmitted job ID only after the whole file is uploaded again, so
|
|
942
|
+
the sink never cancels and never relies on that refusal. When the deadline
|
|
943
|
+
passes, the run rejects with the plan retained and nothing advanced: the job
|
|
944
|
+
ID, the attempt and the cursor stay as they were, and the next run looks the
|
|
945
|
+
job up by its ID again. A job whose upload outlasted a deadline is created in
|
|
946
|
+
the background and is picked up by the next run with no second upload. Each
|
|
947
|
+
such timeout doubles that table's deadline for later runs (up to 6 hours).
|
|
948
|
+
The growth is kept with the plan in `planned_rows_timeouts` and
|
|
949
|
+
`planned_revisions_timeouts` (so it survives restarts and applies to every
|
|
950
|
+
replica, and clears when the batch commits), so an upload slower than the
|
|
951
|
+
deadline eventually gets enough time. Set the deadline above your slowest
|
|
952
|
+
expected load to avoid the extra runs. Only a job BigQuery reports as
|
|
953
|
+
finished with an error advances the attempt. A status check that times out
|
|
954
|
+
is bounded by the base deadline and does not grow it. Re-run
|
|
955
|
+
`REPORTING_WAREHOUSE_SINK_MIGRATION` after upgrading: it adds the two timeout
|
|
956
|
+
columns (a sink table without them rejects with a message saying so).
|
|
957
|
+
BigQuery's server-side `jobTimeoutMs` is not used: for a local-file load the
|
|
958
|
+
official client builds `configuration.load` from the load options and cannot
|
|
959
|
+
set job-level fields, and BigQuery silently drops `labels` and `jobTimeoutMs`
|
|
960
|
+
found inside `configuration.load`. (Staging the file in GCS and submitting the load with
|
|
937
961
|
`bigquery.createJob` can set both; the sink does not stage files.) For the
|
|
938
962
|
same reason the `labels` option has no effect (it warns once per process and
|
|
939
963
|
is deprecated). To attribute load cost, query
|
|
@@ -974,7 +998,9 @@ setInterval(() => {
|
|
|
974
998
|
jobs may have succeeded; reconcile the plan against BigQuery, then call
|
|
975
999
|
`reset({ discardRetainedPlan: true })`. It does not touch the warehouse: a sink
|
|
976
1000
|
that already loaded history reloads the retained revisions, so empty the
|
|
977
|
-
destination tables first
|
|
1001
|
+
destination tables first (after stopping every worker and letting in-flight
|
|
1002
|
+
uploads and jobs finish, since the sink never cancels a load) or use a new
|
|
1003
|
+
sink name against new tables. Every
|
|
978
1004
|
reset starts a new job ID generation, so reloaded batches submit fresh BigQuery
|
|
979
1005
|
jobs instead of matching the finished jobs of the earlier history. Nothing holds
|
|
980
1006
|
change retention between `reset()` and the first committed batch. If `reset()`
|
|
@@ -334,9 +334,20 @@ What it checks:
|
|
|
334
334
|
duplicate rows, and that `current_rows` returns one revision per obligation.
|
|
335
335
|
It also asserts (`PASS`/`FAIL`) that the sink sends no `labels` or
|
|
336
336
|
`jobTimeoutMs` inside `configuration.load`, where BigQuery silently drops
|
|
337
|
-
them
|
|
338
|
-
|
|
339
|
-
|
|
337
|
+
them. A deadline scenario runs a sink with the minimum 1-second load deadline
|
|
338
|
+
and holds the first load's result past it: the run must reject with the plan
|
|
339
|
+
and job ID unchanged and that table's persisted deadline grown, the job must
|
|
340
|
+
end `DONE` without an error (the sink never cancels it), and later runs under
|
|
341
|
+
the same sink name must converge with no duplicate rows, the same job ID and
|
|
342
|
+
no second upload. A #3234 scenario runs a sink whose upload of a 50,000-row
|
|
343
|
+
batch outlasts its 1-second deadline and asserts that it converges within 8
|
|
344
|
+
runs, loads every row exactly once, never changes the job ID, and never
|
|
345
|
+
starts an upload after the job exists (a link fast enough to beat the
|
|
346
|
+
deadline passes and says so).
|
|
347
|
+
- **Cancel probe of the service** (`INFO`, never a failure). It cancels a
|
|
348
|
+
5000-row load the moment it is created and reports whether BigQuery stopped
|
|
349
|
+
it; in practice it accepts the cancel and lets the load finish, which is why
|
|
350
|
+
the sink never cancels a load and finds it by job ID instead.
|
|
340
351
|
- **Overlapping runs** (`INFO`, never a failure). Two concurrent `runOnce()`
|
|
341
352
|
calls over one pending batch, and a deterministic reproduction of #3196: one
|
|
342
353
|
run reads its sink state, another completes the batch and a new revision
|
package/docs/llms.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Ad Context Protocol (AdCP)
|
|
2
2
|
|
|
3
|
-
> Generated at: 2026-10-
|
|
4
|
-
> Library: @adcp/sdk v15.9.
|
|
3
|
+
> Generated at: 2026-10-14
|
|
4
|
+
> Library: @adcp/sdk v15.9.1
|
|
5
5
|
> AdCP major version: 3
|
|
6
6
|
> Canonical URL: https://adcontextprotocol.github.io/adcp-client/llms.txt
|
|
7
7
|
> Note: the `Library` stamp reflects the package.json version at doc-generation time. The narrative below describes the surface that lands on the next-published minor — including any 6.7 helpers documented here ahead of the release tag.
|
|
@@ -11,8 +11,8 @@ below as the publication/deployment gate.
|
|
|
11
11
|
|
|
12
12
|
| Fact | Value |
|
|
13
13
|
| --- | --- |
|
|
14
|
-
| Exact npm package | `@adcp/sdk@15.9.
|
|
15
|
-
| npm integrity | registry-derived after publication; run `npm view @adcp/sdk@15.9.
|
|
14
|
+
| Exact npm package | `@adcp/sdk@15.9.1` |
|
|
15
|
+
| npm integrity | registry-derived after publication; run `npm view @adcp/sdk@15.9.1 dist.integrity` |
|
|
16
16
|
| Node.js runtime | `^20.19.0 || >=22.12.0` |
|
|
17
17
|
| Default AdCP wire release | `3.2.4` |
|
|
18
18
|
| Maintained wire releases | `v2.5`, `v2.6`, `v3`, `3.0.0`, `3.0`, `3.0.1`, `3.0.2`, `3.0.3`, `3.0.4`, `3.0.5`, `3.0.6`, `3.0.7`, `3.0.8`, `3.0.9`, `3.0.10`, `3.0.11`, `3.0.12`, `3.0.13`, `3.0.14`, `3.0.15`, `3.0.16`, `3.0.17`, `3.0.18`, `3.0.19`, `3.0.20`, `3.0.21`, `3.0.22`, `3.0.23`, `3.0.24`, `3.0.25`, `3.1.0`, `3.1`, `3.1.1`, `3.1.2`, `3.1.3`, `3.1.4`, `3.1.5`, `3.1.6`, `3.1.7`, `3.1.8`, `3.1.9`, `3.1.10`, `3.1.11`, `3.1.12`, `3.1.13`, `3.1.14`, `3.1.15`, `3.1.16`, `3.1.17`, `3.1.18`, `3.1.19`, `3.1.20`, `3.1.21`, `3.1.22`, `3.1.23`, `3.1.24`, `3.2.1`, `3.2`, `3.2.2`, `3.2.3`, `3.2.4` |
|
|
@@ -21,8 +21,8 @@ below as the publication/deployment gate.
|
|
|
21
21
|
Install exact production inputs rather than a moving range or dist-tag:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npm install --save-exact '@adcp/sdk@15.9.
|
|
25
|
-
npm view '@adcp/sdk@15.9.
|
|
24
|
+
npm install --save-exact '@adcp/sdk@15.9.1'
|
|
25
|
+
npm view '@adcp/sdk@15.9.1' dist.integrity
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
### Required and optional peers
|