@adcp/sdk 15.9.0 → 15.9.2
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/core/TaskExecutor.js +5 -1
- package/dist/lib/core/TaskExecutor.mjs +5 -1
- 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/client.js +2 -1
- package/dist/lib/testing/client.mjs +2 -1
- package/dist/lib/testing/scenarios/capabilities.js +1 -1
- package/dist/lib/testing/scenarios/capabilities.mjs +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/validation/schema-loader.js +25 -0
- package/dist/lib/validation/schema-loader.mjs +24 -0
- 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
|
@@ -47,6 +47,7 @@ var import_signedRequestRejection = require('../protocols/signedRequestRejection
|
|
|
47
47
|
var import_errors = require('../errors/index.js');
|
|
48
48
|
var import_client_hooks = require('../validation/client-hooks.js');
|
|
49
49
|
var import_schema_validator = require('../validation/schema-validator.js');
|
|
50
|
+
var import_schema_loader = require('../validation/schema-loader.js');
|
|
50
51
|
var import_version = require('../version.js');
|
|
51
52
|
var import_response_unwrapper = require('../utils/response-unwrapper.js');
|
|
52
53
|
var import_error_extraction = require('../utils/error-extraction.js');
|
|
@@ -4397,7 +4398,10 @@ class TaskExecutor {
|
|
|
4397
4398
|
const mode = this.responseValidationMode;
|
|
4398
4399
|
const logViolations = this.config.logSchemaViolations !== false;
|
|
4399
4400
|
try {
|
|
4400
|
-
|
|
4401
|
+
let validationVersion = (serverVersion ?? this.lastKnownServerVersion) === "v2" ? "v2.5" : this.responseAdcpVersionForValidation() ?? this.config.adcpVersion ?? import_version.ADCP_VERSION;
|
|
4402
|
+
if (taskName === "get_adcp_capabilities" && validationVersion !== "v2.5") {
|
|
4403
|
+
validationVersion = (0, import_schema_loader.resolveServedValidationVersion)(response, validationVersion);
|
|
4404
|
+
}
|
|
4401
4405
|
const normalizedResponse = this.normalizeResponseForValidation(response, taskName, validationVersion === "v2.5");
|
|
4402
4406
|
const outcome = (0, import_client_hooks.validateIncomingResponse)(taskName, normalizedResponse, mode, void 0, validationVersion);
|
|
4403
4407
|
if (outcome.valid) return { valid: true, errors: [] };
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
resolveValidationModes
|
|
16
16
|
} from "../validation/client-hooks.mjs";
|
|
17
17
|
import { formatIssues } from "../validation/schema-validator.mjs";
|
|
18
|
+
import { resolveServedValidationVersion } from "../validation/schema-loader.mjs";
|
|
18
19
|
import { ADCP_VERSION } from "../version.mjs";
|
|
19
20
|
import { unwrapProtocolResponse, isAdcpOperationSuccess } from "../utils/response-unwrapper.mjs";
|
|
20
21
|
import { extractAdcpErrorInfo, extractCorrelationId } from "../utils/error-extraction.mjs";
|
|
@@ -4370,7 +4371,10 @@ class TaskExecutor {
|
|
|
4370
4371
|
const mode = this.responseValidationMode;
|
|
4371
4372
|
const logViolations = this.config.logSchemaViolations !== false;
|
|
4372
4373
|
try {
|
|
4373
|
-
|
|
4374
|
+
let validationVersion = (serverVersion ?? this.lastKnownServerVersion) === "v2" ? "v2.5" : this.responseAdcpVersionForValidation() ?? this.config.adcpVersion ?? ADCP_VERSION;
|
|
4375
|
+
if (taskName === "get_adcp_capabilities" && validationVersion !== "v2.5") {
|
|
4376
|
+
validationVersion = resolveServedValidationVersion(response, validationVersion);
|
|
4377
|
+
}
|
|
4374
4378
|
const normalizedResponse = this.normalizeResponseForValidation(response, taskName, validationVersion === "v2.5");
|
|
4375
4379
|
const outcome = validateIncomingResponse(taskName, normalizedResponse, mode, void 0, validationVersion);
|
|
4376
4380
|
if (outcome.valid) return { valid: true, errors: [] };
|
|
@@ -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
|
});
|