@oneuptime/common 11.5.13 → 11.6.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/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +41 -24
- package/Models/AnalyticsModels/MetricBaselineHourly.ts +2 -0
- package/Models/AnalyticsModels/MetricItemAggMV1m.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByContainer.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +5 -2
- package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
- package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
- package/Models/DatabaseModels/GlobalConfig.ts +76 -0
- package/Server/API/EnterpriseLicenseAPI.ts +57 -0
- package/Server/API/GlobalConfigAPI.ts +62 -1
- package/Server/EnvironmentConfig.ts +22 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/AnalyticsDatabaseService.ts +61 -46
- package/Server/Services/DatabaseService.ts +66 -1
- package/Server/Services/WorkflowService.ts +100 -39
- package/Server/Types/Workflow/Components/Schedule.ts +57 -32
- package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +41 -4
- package/Server/Utils/Express.ts +13 -0
- package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
- package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
- package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
- package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
- package/Tests/Models/AnalyticsModels/AnalyticsBaseModel.test.ts +122 -0
- package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
- package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
- package/Tests/Server/Services/WorkflowService.test.ts +308 -0
- package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +164 -3
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +99 -0
- package/Tests/Server/Utils/Attribution.test.ts +203 -0
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
- package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
- package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
- package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
- package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
- package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
- package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
- package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
- package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
- package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
- package/Tests/Types/HashCode.test.ts +41 -0
- package/Tests/Types/Log/LogQueryParser.test.ts +186 -0
- package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
- package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
- package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
- package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
- package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
- package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
- package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
- package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
- package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
- package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
- package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
- package/Tests/Utils/Array.test.ts +156 -0
- package/Tests/Utils/CronTab.test.ts +199 -0
- package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +197 -0
- package/Tests/Utils/ModelImportExport.test.ts +101 -0
- package/Tests/Utils/Number.test.ts +179 -0
- package/Tests/Utils/ValueFormatter.test.ts +305 -0
- package/Tests/Utils/VersionUtil.test.ts +348 -0
- package/Tests/jest.setup.ts +1 -0
- package/Types/AnalyticsDatabase/TableColumn.ts +23 -0
- package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
- package/Types/Monitor/MonitorStep.ts +24 -4
- package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
- package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
- package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
- package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
- package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
- package/UI/Utils/NotificationMethodUtil.ts +310 -0
- package/Utils/CronTab.ts +823 -0
- package/Utils/ModelImportExport.ts +21 -6
- package/Utils/VersionUtil.ts +260 -0
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +25 -20
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js +2 -0
- package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js.map +1 -1
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +17 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +53 -17
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +53 -1
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/WorkflowService.js +80 -30
- package/build/dist/Server/Services/WorkflowService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
- package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -4
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Express.js +12 -0
- package/build/dist/Server/Utils/Express.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
- package/build/dist/Types/AnalyticsDatabase/TableColumn.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +20 -4
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
- package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
- package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
- package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
- package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
- package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
- package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
- package/build/dist/Utils/CronTab.js +609 -0
- package/build/dist/Utils/CronTab.js.map +1 -0
- package/build/dist/Utils/ModelImportExport.js +20 -6
- package/build/dist/Utils/ModelImportExport.js.map +1 -1
- package/build/dist/Utils/VersionUtil.js +193 -0
- package/build/dist/Utils/VersionUtil.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import logger from "../Logger";
|
|
2
|
+
import Sleep from "../../../Types/Sleep";
|
|
3
|
+
import UUID from "../../../Utils/UUID";
|
|
4
|
+
import GracefulShutdown, { ShutdownPriority } from "../GracefulShutdown";
|
|
5
|
+
import { nextInsertDedupToken } from "../AnalyticsDatabase/InsertDedupContext";
|
|
6
|
+
import { ClickHouseError } from "@clickhouse/client";
|
|
7
|
+
/*
|
|
8
|
+
* Marker for transient failures raised by a pluggable insert transport
|
|
9
|
+
* (e.g. the remote telemetry-writer client): overload shedding (HTTP 429),
|
|
10
|
+
* writer-tier unavailability (503), or an ambiguous network failure. The
|
|
11
|
+
* writer retries these with the SAME dedup token, so an ambiguous failure
|
|
12
|
+
* whose rows actually landed never double-writes.
|
|
13
|
+
*/
|
|
14
|
+
export class TransientInsertError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "TransientInsertError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class FanInInsertError extends Error {
|
|
21
|
+
constructor(data) {
|
|
22
|
+
const causeMessage = data.cause instanceof Error ? data.cause.message : String(data.cause);
|
|
23
|
+
super(`Fan-in insert into ${data.tableName} failed after ${data.attempts} attempt(s): ${causeMessage}`);
|
|
24
|
+
this.name = "FanInInsertError";
|
|
25
|
+
this.causeError = data.cause;
|
|
26
|
+
if (data.cause instanceof Error && data.cause.stack) {
|
|
27
|
+
this.stack = `${this.stack}\nCaused by: ${data.cause.stack}`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/*
|
|
32
|
+
* ClickHouse server error codes that indicate transient overload or an
|
|
33
|
+
* ambiguous outcome. All are safe to retry here because every insert
|
|
34
|
+
* carries a dedup token that is stable across the writer's retries: a
|
|
35
|
+
* retry of a block that actually landed is dropped server-side.
|
|
36
|
+
* 202 TOO_MANY_SIMULTANEOUS_QUERIES, 209 SOCKET_TIMEOUT,
|
|
37
|
+
* 210 NETWORK_ERROR, 241 MEMORY_LIMIT_EXCEEDED, 252 TOO_MANY_PARTS,
|
|
38
|
+
* 319 UNKNOWN_STATUS_OF_INSERT
|
|
39
|
+
* NOTE: the client exposes `code` as a STRING (e.g. "202").
|
|
40
|
+
*/
|
|
41
|
+
const RETRYABLE_CLICKHOUSE_CODES = new Set([
|
|
42
|
+
"202",
|
|
43
|
+
"209",
|
|
44
|
+
"210",
|
|
45
|
+
"241",
|
|
46
|
+
"252",
|
|
47
|
+
"319",
|
|
48
|
+
]);
|
|
49
|
+
const NUMERIC_ERROR_CODE_REGEX = /^\d+$/;
|
|
50
|
+
const RETRYABLE_SOCKET_CODES = new Set([
|
|
51
|
+
"ECONNRESET",
|
|
52
|
+
"ECONNREFUSED",
|
|
53
|
+
"ECONNABORTED",
|
|
54
|
+
"ETIMEDOUT",
|
|
55
|
+
"EPIPE",
|
|
56
|
+
"EAI_AGAIN",
|
|
57
|
+
]);
|
|
58
|
+
export function isRetryableInsertError(err) {
|
|
59
|
+
if (err instanceof ClickHouseError) {
|
|
60
|
+
return RETRYABLE_CLICKHOUSE_CODES.has(err.code);
|
|
61
|
+
}
|
|
62
|
+
if (err instanceof TransientInsertError) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (err instanceof Error) {
|
|
66
|
+
// Duck-typed TransientInsertError (duplicated module instances).
|
|
67
|
+
if (err.name === "TransientInsertError") {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
const code = err["code"];
|
|
71
|
+
if (typeof code === "string") {
|
|
72
|
+
if (RETRYABLE_SOCKET_CODES.has(code)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
// Duck-typed ClickHouseError (in case of duplicated package instances).
|
|
76
|
+
if (NUMERIC_ERROR_CODE_REGEX.test(code)) {
|
|
77
|
+
return RETRYABLE_CLICKHOUSE_CODES.has(code);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/*
|
|
81
|
+
* @clickhouse/client surfaces its socket-idle request_timeout as a plain
|
|
82
|
+
* Error("Timeout error."), with no code. The outcome is ambiguous (the
|
|
83
|
+
* body may have been received), which is exactly what the per-batch dedup
|
|
84
|
+
* token exists for — retry.
|
|
85
|
+
*/
|
|
86
|
+
if (err.message.includes("Timeout error") ||
|
|
87
|
+
err.message.includes("socket hang up")) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const readPositiveInt = (envKey, defaultValue) => {
|
|
94
|
+
const raw = process.env[envKey];
|
|
95
|
+
if (!raw) {
|
|
96
|
+
return defaultValue;
|
|
97
|
+
}
|
|
98
|
+
const parsed = parseInt(raw, 10);
|
|
99
|
+
return !isNaN(parsed) && parsed > 0 ? parsed : defaultValue;
|
|
100
|
+
};
|
|
101
|
+
export function readFanInWriterOptionsFromEnv() {
|
|
102
|
+
return {
|
|
103
|
+
maxBatchRows: readPositiveInt("TELEMETRY_FANIN_MAX_BATCH_ROWS", 100000),
|
|
104
|
+
maxWaitMs: readPositiveInt("TELEMETRY_FANIN_MAX_WAIT_MS", 5000),
|
|
105
|
+
maxConcurrentInserts: readPositiveInt("TELEMETRY_FANIN_MAX_CONCURRENT_INSERTS", 4),
|
|
106
|
+
/*
|
|
107
|
+
* Must stay comfortably above maxBatchRows: if the two are equal, the
|
|
108
|
+
* instant a full batch is cut every submitter blocks until that whole
|
|
109
|
+
* batch finishes inserting — no pipelining. 2x allows one batch in
|
|
110
|
+
* flight while the next fills.
|
|
111
|
+
*/
|
|
112
|
+
maxPendingRows: readPositiveInt("TELEMETRY_FANIN_MAX_PENDING_ROWS", 200000),
|
|
113
|
+
retryMaxAttempts: readPositiveInt("TELEMETRY_FANIN_RETRY_MAX_ATTEMPTS", 6),
|
|
114
|
+
retryBaseDelayMs: readPositiveInt("TELEMETRY_FANIN_RETRY_BASE_DELAY_MS", 250),
|
|
115
|
+
retryMaxDelayMs: readPositiveInt("TELEMETRY_FANIN_RETRY_MAX_DELAY_MS", 10000),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export class TelemetryFanInWriter {
|
|
119
|
+
constructor(options) {
|
|
120
|
+
this.buffers = new Map();
|
|
121
|
+
this.pendingRows = 0;
|
|
122
|
+
this.capacityWaiters = [];
|
|
123
|
+
this.activeInserts = 0;
|
|
124
|
+
this.insertSlotWaiters = [];
|
|
125
|
+
this.inFlightDispatches = new Set();
|
|
126
|
+
/*
|
|
127
|
+
* Submits that have been called but have not yet buffered their rows
|
|
128
|
+
* (parked at the waitForCapacity await, or merely yielding its one
|
|
129
|
+
* microtask). flushAll() must not conclude the drain while any exist —
|
|
130
|
+
* their rows would land just after the final cut and strand.
|
|
131
|
+
*/
|
|
132
|
+
this.acceptingSubmits = 0;
|
|
133
|
+
this.shutdownHandlerRegistered = false;
|
|
134
|
+
this.insertTransport = null;
|
|
135
|
+
this.options = options;
|
|
136
|
+
}
|
|
137
|
+
/*
|
|
138
|
+
* Install (or clear, with null) a custom delivery transport for token
|
|
139
|
+
* groups. Set once at boot when TELEMETRY_WRITER_URL is configured;
|
|
140
|
+
* batching, dedup-token capture, the per-pod semaphore, retry/backoff,
|
|
141
|
+
* and backpressure all stay in front of it unchanged.
|
|
142
|
+
*/
|
|
143
|
+
setInsertTransport(transport) {
|
|
144
|
+
this.insertTransport = transport;
|
|
145
|
+
}
|
|
146
|
+
hasInsertTransport() {
|
|
147
|
+
return this.insertTransport !== null;
|
|
148
|
+
}
|
|
149
|
+
/** Test hook: override options on the shared instance (e.g. shrink maxWaitMs). */
|
|
150
|
+
configure(partial) {
|
|
151
|
+
this.options = Object.assign(Object.assign({}, this.options), partial);
|
|
152
|
+
}
|
|
153
|
+
getStats() {
|
|
154
|
+
let bufferedRows = 0;
|
|
155
|
+
for (const buffer of this.buffers.values()) {
|
|
156
|
+
bufferedRows += buffer.rowCount;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
bufferedRows,
|
|
160
|
+
pendingRows: this.pendingRows,
|
|
161
|
+
activeInserts: this.activeInserts,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/*
|
|
165
|
+
* Hand a batch of rows to the writer. Takes ownership of the `rows` array —
|
|
166
|
+
* callers must not mutate it afterwards.
|
|
167
|
+
*
|
|
168
|
+
* Awaiting submit() itself is the backpressure point: it resolves once the
|
|
169
|
+
* rows are accepted into the buffer (immediately under normal load, later
|
|
170
|
+
* when the pod is at maxPendingRows). The returned `flushed` promise is the
|
|
171
|
+
* acceptance ack — resolve it into the job's pending-ack list and await
|
|
172
|
+
* before completing the job.
|
|
173
|
+
*
|
|
174
|
+
* All submitters of one table must pass identical clickhouseSettings: a
|
|
175
|
+
* batch mixes submissions and applies the first submission's settings.
|
|
176
|
+
*/
|
|
177
|
+
async submit(target, rows, options) {
|
|
178
|
+
var _a, _b, _c;
|
|
179
|
+
if (!rows || rows.length === 0) {
|
|
180
|
+
return { flushed: Promise.resolve() };
|
|
181
|
+
}
|
|
182
|
+
this.registerShutdownHandlerOnce();
|
|
183
|
+
const tableName = target.model.tableName;
|
|
184
|
+
/*
|
|
185
|
+
* Capture the deterministic per-job dedup token NOW, synchronously in
|
|
186
|
+
* the caller's flow: submit() runs inside the job's runWithInsertDedup
|
|
187
|
+
* scope, while the dispatch that eventually inserts these rows runs on
|
|
188
|
+
* a timer with no ambient context. Capturing before any await also
|
|
189
|
+
* keeps token order deterministic across retries of the same payload.
|
|
190
|
+
*/
|
|
191
|
+
const dedupToken = (_a = options === null || options === void 0 ? void 0 : options.dedupToken) !== null && _a !== void 0 ? _a : nextInsertDedupToken(tableName);
|
|
192
|
+
this.acceptingSubmits++;
|
|
193
|
+
try {
|
|
194
|
+
await this.waitForCapacity();
|
|
195
|
+
let buffer = this.buffers.get(tableName);
|
|
196
|
+
if (!buffer) {
|
|
197
|
+
buffer = {
|
|
198
|
+
target,
|
|
199
|
+
submissions: [],
|
|
200
|
+
rowCount: 0,
|
|
201
|
+
timer: null,
|
|
202
|
+
};
|
|
203
|
+
this.buffers.set(tableName, buffer);
|
|
204
|
+
}
|
|
205
|
+
let resolveAck = () => { };
|
|
206
|
+
let rejectAck = () => { };
|
|
207
|
+
const flushed = new Promise((resolve, reject) => {
|
|
208
|
+
resolveAck = resolve;
|
|
209
|
+
rejectAck = reject;
|
|
210
|
+
});
|
|
211
|
+
buffer.submissions.push({
|
|
212
|
+
rows,
|
|
213
|
+
clickhouseSettings: options === null || options === void 0 ? void 0 : options.clickhouseSettings,
|
|
214
|
+
dedupToken,
|
|
215
|
+
resolveAck,
|
|
216
|
+
rejectAck,
|
|
217
|
+
});
|
|
218
|
+
buffer.rowCount += rows.length;
|
|
219
|
+
this.pendingRows += rows.length;
|
|
220
|
+
if (buffer.rowCount >= this.options.maxBatchRows) {
|
|
221
|
+
this.cutAndDispatch(tableName, false);
|
|
222
|
+
}
|
|
223
|
+
else if (!buffer.timer) {
|
|
224
|
+
buffer.timer = setTimeout(() => {
|
|
225
|
+
const timedBuffer = this.buffers.get(tableName);
|
|
226
|
+
if (timedBuffer) {
|
|
227
|
+
timedBuffer.timer = null;
|
|
228
|
+
}
|
|
229
|
+
this.cutAndDispatch(tableName, true);
|
|
230
|
+
}, this.options.maxWaitMs);
|
|
231
|
+
/*
|
|
232
|
+
* Never keep the process alive just for a pending flush timer —
|
|
233
|
+
* graceful shutdown drains buffers explicitly.
|
|
234
|
+
*/
|
|
235
|
+
(_c = (_b = buffer.timer).unref) === null || _c === void 0 ? void 0 : _c.call(_b);
|
|
236
|
+
}
|
|
237
|
+
return { flushed };
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
this.acceptingSubmits--;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** Force-flush everything buffered and wait for all in-flight inserts to settle. */
|
|
244
|
+
async flushAll() {
|
|
245
|
+
/*
|
|
246
|
+
* Dispatch promises never reject (failures are routed to submission
|
|
247
|
+
* acks), so a plain all() is safe. Re-cut on EVERY pass: a submit
|
|
248
|
+
* parked in waitForCapacity() resumes when a completing dispatch
|
|
249
|
+
* releases capacity — scheduled BEFORE the dispatch promise's own
|
|
250
|
+
* reactions — and buffers its rows WITHOUT creating a dispatch when
|
|
251
|
+
* they are below maxBatchRows (its flush timer is unref'd and will
|
|
252
|
+
* never fire before process exit). Waiting on inFlightDispatches alone
|
|
253
|
+
* would therefore return with those rows stranded. Exit only when a
|
|
254
|
+
* fresh cut pass leaves nothing in flight and no submit is still
|
|
255
|
+
* mid-acceptance; the microtask yield lets a not-yet-buffered submit
|
|
256
|
+
* (parked only on waitForCapacity's fast-path await) land its rows so
|
|
257
|
+
* the next pass picks them up.
|
|
258
|
+
*/
|
|
259
|
+
for (;;) {
|
|
260
|
+
for (const tableName of Array.from(this.buffers.keys())) {
|
|
261
|
+
this.cutAndDispatch(tableName, true);
|
|
262
|
+
}
|
|
263
|
+
if (this.inFlightDispatches.size > 0) {
|
|
264
|
+
await Promise.all(Array.from(this.inFlightDispatches));
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (this.acceptingSubmits === 0) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
await Promise.resolve();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
registerShutdownHandlerOnce() {
|
|
274
|
+
if (this.shutdownHandlerRegistered) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.shutdownHandlerRegistered = true;
|
|
278
|
+
GracefulShutdown.registerHandler("TelemetryFanInWriter", ShutdownPriority.Buffers, async () => {
|
|
279
|
+
await this.flushAll();
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
async waitForCapacity() {
|
|
283
|
+
while (this.pendingRows >= this.options.maxPendingRows) {
|
|
284
|
+
await new Promise((resolve) => {
|
|
285
|
+
this.capacityWaiters.push(resolve);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
releaseCapacity() {
|
|
290
|
+
/*
|
|
291
|
+
* Wake all waiters; each re-checks the high-water mark in its
|
|
292
|
+
* waitForCapacity loop, so overshoot stays bounded to one submission
|
|
293
|
+
* per waiter.
|
|
294
|
+
*/
|
|
295
|
+
const waiters = this.capacityWaiters;
|
|
296
|
+
this.capacityWaiters = [];
|
|
297
|
+
for (const wake of waiters) {
|
|
298
|
+
wake();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async acquireInsertSlot() {
|
|
302
|
+
while (this.activeInserts >= this.options.maxConcurrentInserts) {
|
|
303
|
+
await new Promise((resolve) => {
|
|
304
|
+
this.insertSlotWaiters.push(resolve);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
this.activeInserts++;
|
|
308
|
+
}
|
|
309
|
+
releaseInsertSlot() {
|
|
310
|
+
this.activeInserts--;
|
|
311
|
+
const next = this.insertSlotWaiters.shift();
|
|
312
|
+
if (next) {
|
|
313
|
+
next();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/*
|
|
317
|
+
* Cut batches from a table buffer and dispatch them. When `drain` is false,
|
|
318
|
+
* only full batches (>= maxBatchRows) are cut — the remainder keeps waiting
|
|
319
|
+
* for more rows or the timer. When true, everything goes.
|
|
320
|
+
*
|
|
321
|
+
* A batch is a FIFO run of WHOLE submissions packed up to maxBatchRows
|
|
322
|
+
* (a single oversized submission forms its own batch), so an insert failure
|
|
323
|
+
* maps 1:1 onto the submissions it contained.
|
|
324
|
+
*/
|
|
325
|
+
cutAndDispatch(tableName, drain) {
|
|
326
|
+
const buffer = this.buffers.get(tableName);
|
|
327
|
+
if (!buffer) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
while (buffer.rowCount >= this.options.maxBatchRows ||
|
|
331
|
+
(drain && buffer.rowCount > 0)) {
|
|
332
|
+
const batch = [];
|
|
333
|
+
let batchRows = 0;
|
|
334
|
+
while (buffer.submissions.length > 0) {
|
|
335
|
+
const next = buffer.submissions[0];
|
|
336
|
+
if (batch.length > 0 &&
|
|
337
|
+
batchRows + next.rows.length > this.options.maxBatchRows) {
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
buffer.submissions.shift();
|
|
341
|
+
batch.push(next);
|
|
342
|
+
batchRows += next.rows.length;
|
|
343
|
+
if (batchRows >= this.options.maxBatchRows) {
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (batch.length === 0) {
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
buffer.rowCount -= batchRows;
|
|
351
|
+
const dispatch = this.dispatchInsert(buffer.target, tableName, batch, batchRows);
|
|
352
|
+
this.inFlightDispatches.add(dispatch);
|
|
353
|
+
dispatch.finally(() => {
|
|
354
|
+
this.inFlightDispatches.delete(dispatch);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (buffer.rowCount === 0 && buffer.timer) {
|
|
358
|
+
clearTimeout(buffer.timer);
|
|
359
|
+
buffer.timer = null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/*
|
|
363
|
+
* Insert one batch while holding a single insert slot. Submissions that
|
|
364
|
+
* carry a deterministic per-job dedup token are inserted INDIVIDUALLY
|
|
365
|
+
* under their own token — this is what keeps BullMQ job retries
|
|
366
|
+
* duplicate-free: the retry re-derives byte-identical tokens and
|
|
367
|
+
* ClickHouse drops blocks a prior attempt already landed. Merging them
|
|
368
|
+
* under one shared cross-job token would break that (a token dedups by
|
|
369
|
+
* token, not content, so a differently-composed block under a reused
|
|
370
|
+
* token would silently drop other jobs' rows — the same hazard that kept
|
|
371
|
+
* the probe paths off dedup tokens historically). Untokened submissions
|
|
372
|
+
* are merged into one insert under a minted per-batch token.
|
|
373
|
+
*
|
|
374
|
+
* The tokened inserts run sequentially within this one slot, so the
|
|
375
|
+
* per-pod concurrency cap — the property that fixes the
|
|
376
|
+
* max_concurrent_queries incident — is unaffected; only the statement
|
|
377
|
+
* count returns to master's per-chunk granularity, and ClickHouse's
|
|
378
|
+
* async-insert coalescing (async_insert=1) still assembles fat parts
|
|
379
|
+
* server-side.
|
|
380
|
+
*
|
|
381
|
+
* Never rejects — outcomes are routed to the submissions' acks.
|
|
382
|
+
*/
|
|
383
|
+
async dispatchInsert(target, tableName, batch, batchRows) {
|
|
384
|
+
await this.acquireInsertSlot();
|
|
385
|
+
try {
|
|
386
|
+
const untokened = [];
|
|
387
|
+
for (const submission of batch) {
|
|
388
|
+
if (submission.dedupToken) {
|
|
389
|
+
await this.insertGroupWithRetry(target, tableName, [submission], submission.dedupToken);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
untokened.push(submission);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (untokened.length > 0) {
|
|
396
|
+
await this.insertGroupWithRetry(target, tableName, untokened, `fanin:${tableName}:${UUID.generateTimeOrdered()}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
this.pendingRows -= batchRows;
|
|
401
|
+
this.releaseCapacity();
|
|
402
|
+
this.releaseInsertSlot();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/*
|
|
406
|
+
* Insert one token group, retrying transient failures with full-jitter
|
|
407
|
+
* exponential backoff and the SAME dedup token — an ambiguous failure
|
|
408
|
+
* (timeout, connection reset) whose rows actually landed is dropped
|
|
409
|
+
* server-side on the retry. The insert slot is held across backoff sleeps
|
|
410
|
+
* on purpose: when ClickHouse is saturated, offering it MORE concurrent
|
|
411
|
+
* load from this pod is the failure mode this component exists to
|
|
412
|
+
* prevent. Never rejects — outcomes are routed to the group's acks, so
|
|
413
|
+
* one group's definitive failure does not fail the batch's other groups.
|
|
414
|
+
*/
|
|
415
|
+
async insertGroupWithRetry(target, tableName, group, dedupToken) {
|
|
416
|
+
var _a, _b;
|
|
417
|
+
const rows = [];
|
|
418
|
+
for (const submission of group) {
|
|
419
|
+
for (const row of submission.rows) {
|
|
420
|
+
rows.push(row);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const clickhouseSettings = (_a = group[0]) === null || _a === void 0 ? void 0 : _a.clickhouseSettings;
|
|
424
|
+
const sleep = (_b = this.options.sleep) !== null && _b !== void 0 ? _b : ((ms) => {
|
|
425
|
+
return Sleep.sleep(ms);
|
|
426
|
+
});
|
|
427
|
+
let lastError = null;
|
|
428
|
+
let attemptsMade = 0;
|
|
429
|
+
for (let attempt = 1; attempt <= this.options.retryMaxAttempts; attempt++) {
|
|
430
|
+
attemptsMade = attempt;
|
|
431
|
+
try {
|
|
432
|
+
if (this.insertTransport) {
|
|
433
|
+
await this.insertTransport(target, rows, {
|
|
434
|
+
dedupToken,
|
|
435
|
+
clickhouseSettings,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
await target.insertJsonRows(rows, {
|
|
440
|
+
dedupToken,
|
|
441
|
+
clickhouseSettings,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
for (const submission of group) {
|
|
445
|
+
submission.resolveAck();
|
|
446
|
+
}
|
|
447
|
+
if (attempt > 1) {
|
|
448
|
+
logger.info(`TelemetryFanInWriter: insert into ${tableName} (${rows.length} rows) succeeded on attempt ${attempt}.`);
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
lastError = err;
|
|
454
|
+
if (!isRetryableInsertError(err) ||
|
|
455
|
+
attempt === this.options.retryMaxAttempts) {
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
const expDelay = Math.min(this.options.retryMaxDelayMs, this.options.retryBaseDelayMs * Math.pow(2, attempt - 1));
|
|
459
|
+
const jitteredDelay = Math.ceil(Math.random() * expDelay);
|
|
460
|
+
logger.warn(`TelemetryFanInWriter: transient insert failure on ${tableName} (${rows.length} rows, attempt ${attempt}/${this.options.retryMaxAttempts}); retrying with the same dedup token in ${jitteredDelay}ms: ${err instanceof Error ? err.message : String(err)}`);
|
|
461
|
+
await sleep(jitteredDelay);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const finalError = new FanInInsertError({
|
|
465
|
+
tableName,
|
|
466
|
+
attempts: attemptsMade,
|
|
467
|
+
cause: lastError,
|
|
468
|
+
});
|
|
469
|
+
logger.error(`TelemetryFanInWriter: giving up on insert into ${tableName} (${rows.length} rows); failing ${group.length} submission(s).`);
|
|
470
|
+
logger.error(finalError);
|
|
471
|
+
for (const submission of group) {
|
|
472
|
+
submission.rejectAck(finalError);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/*
|
|
477
|
+
* Register a durability ack on a job's pending-ack list, wrapped in the
|
|
478
|
+
* service's storage error class and pre-observed: a mid-job batch failure
|
|
479
|
+
* would otherwise reject the stored promise long before the job's final
|
|
480
|
+
* `await Promise.all(pendingAcks)` reaches it, firing the process-level
|
|
481
|
+
* unhandledRejection logger once per ack. The no-op catch marks the promise
|
|
482
|
+
* handled without swallowing anything — the rejection is still delivered at
|
|
483
|
+
* the job's await point, failing the job so BullMQ retries the payload.
|
|
484
|
+
*/
|
|
485
|
+
export function pushObservedAck(pendingAcks, flushed, wrapError) {
|
|
486
|
+
const ack = flushed.catch((error) => {
|
|
487
|
+
throw wrapError(error);
|
|
488
|
+
});
|
|
489
|
+
ack.catch(() => {
|
|
490
|
+
// Pre-observed; delivered for real at the job's await point.
|
|
491
|
+
});
|
|
492
|
+
pendingAcks.push(ack);
|
|
493
|
+
}
|
|
494
|
+
const defaultWriter = new TelemetryFanInWriter(readFanInWriterOptionsFromEnv());
|
|
495
|
+
export default defaultWriter;
|
|
496
|
+
//# sourceMappingURL=TelemetryFanInWriter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TelemetryFanInWriter.js","sourceRoot":"","sources":["../../../../../Server/Utils/Telemetry/TelemetryFanInWriter.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,OAAO,KAAK,MAAM,sBAAsB,CAAC;AACzC,OAAO,IAAI,MAAM,qBAAqB,CAAC;AAEvC,OAAO,gBAAgB,EAAE,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;AAC/E,OAAO,EAAE,eAAe,EAA2B,MAAM,oBAAoB,CAAC;AAwH9E;;;;;;GAMG;AACH,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,YAAmB,OAAe;QAChC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AAED,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAQzC,YAAmB,IAIlB;QACC,MAAM,YAAY,GAChB,IAAI,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxE,KAAK,CACH,sBAAsB,IAAI,CAAC,SAAS,iBAAiB,IAAI,CAAC,QAAQ,gBAAgB,YAAY,EAAE,CACjG,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;QAC/B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC;QAC7B,IAAI,IAAI,CAAC,KAAK,YAAY,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YACpD,IAAI,CAAC,KAAK,GAAG,GAAG,IAAI,CAAC,KAAK,gBAAgB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QAC/D,CAAC;IACH,CAAC;CACF;AAyBD;;;;;;;;;GASG;AACH,MAAM,0BAA0B,GAAgB,IAAI,GAAG,CAAC;IACtD,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;CACN,CAAC,CAAC;AAEH,MAAM,wBAAwB,GAAW,OAAO,CAAC;AAEjD,MAAM,sBAAsB,GAAgB,IAAI,GAAG,CAAC;IAClD,YAAY;IACZ,cAAc;IACd,cAAc;IACd,WAAW;IACX,OAAO;IACP,WAAW;CACZ,CAAC,CAAC;AAEH,MAAM,UAAU,sBAAsB,CAAC,GAAY;IACjD,IAAI,GAAG,YAAY,eAAe,EAAE,CAAC;QACnC,OAAO,0BAA0B,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC;IAED,IAAI,GAAG,YAAY,oBAAoB,EAAE,CAAC;QACxC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;QACzB,iEAAiE;QACjE,IAAI,GAAG,CAAC,IAAI,KAAK,sBAAsB,EAAE,CAAC;YACxC,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,IAAI,GAAa,GAA6B,CAAC,MAAM,CAAC,CAAC;QAC7D,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,IAAI,sBAAsB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACrC,OAAO,IAAI,CAAC;YACd,CAAC;YACD,wEAAwE;YACxE,IAAI,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxC,OAAO,0BAA0B,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9C,CAAC;QACH,CAAC;QACD;;;;;WAKG;QACH,IACE,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC;YACrC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EACtC,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAID,MAAM,eAAe,GAAiB,CACpC,MAAc,EACd,YAAoB,EACZ,EAAE;IACV,MAAM,GAAG,GAAuB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACpD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,MAAM,MAAM,GAAW,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACzC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC;AAC9D,CAAC,CAAC;AAEF,MAAM,UAAU,6BAA6B;IAC3C,OAAO;QACL,YAAY,EAAE,eAAe,CAAC,gCAAgC,EAAE,MAAO,CAAC;QACxE,SAAS,EAAE,eAAe,CAAC,6BAA6B,EAAE,IAAI,CAAC;QAC/D,oBAAoB,EAAE,eAAe,CACnC,wCAAwC,EACxC,CAAC,CACF;QACD;;;;;WAKG;QACH,cAAc,EAAE,eAAe,CAC7B,kCAAkC,EAClC,MAAO,CACR;QACD,gBAAgB,EAAE,eAAe,CAAC,oCAAoC,EAAE,CAAC,CAAC;QAC1E,gBAAgB,EAAE,eAAe,CAC/B,qCAAqC,EACrC,GAAG,CACJ;QACD,eAAe,EAAE,eAAe,CAC9B,oCAAoC,EACpC,KAAM,CACP;KACF,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,oBAAoB;IAkB/B,YAAmB,OAA2B;QAhBtC,YAAO,GAA6B,IAAI,GAAG,EAAE,CAAC;QAC9C,gBAAW,GAAW,CAAC,CAAC;QACxB,oBAAe,GAAsB,EAAE,CAAC;QACxC,kBAAa,GAAW,CAAC,CAAC;QAC1B,sBAAiB,GAAsB,EAAE,CAAC;QAC1C,uBAAkB,GAAuB,IAAI,GAAG,EAAE,CAAC;QAC3D;;;;;WAKG;QACK,qBAAgB,GAAW,CAAC,CAAC;QAC7B,8BAAyB,GAAY,KAAK,CAAC;QAC3C,oBAAe,GAAgC,IAAI,CAAC;QAG1D,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED;;;;;OAKG;IACI,kBAAkB,CAAC,SAAsC;QAC9D,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;IACnC,CAAC;IAEM,kBAAkB;QACvB,OAAO,IAAI,CAAC,eAAe,KAAK,IAAI,CAAC;IACvC,CAAC;IAED,kFAAkF;IAC3E,SAAS,CAAC,OAAoC;QACnD,IAAI,CAAC,OAAO,mCAAQ,IAAI,CAAC,OAAO,GAAK,OAAO,CAAE,CAAC;IACjD,CAAC;IAEM,QAAQ;QAKb,IAAI,YAAY,GAAW,CAAC,CAAC;QAC7B,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YAC3C,YAAY,IAAI,MAAM,CAAC,QAAQ,CAAC;QAClC,CAAC;QACD,OAAO;YACL,YAAY;YACZ,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,aAAa,EAAE,IAAI,CAAC,aAAa;SAClC,CAAC;IACJ,CAAC;IAED;;;;;;;;;;;;OAYG;IACI,KAAK,CAAC,MAAM,CACjB,MAAyB,EACzB,IAAuB,EACvB,OASC;;QAED,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QACxC,CAAC;QAED,IAAI,CAAC,2BAA2B,EAAE,CAAC;QAEnC,MAAM,SAAS,GAAW,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC;QAEjD;;;;;;WAMG;QACH,MAAM,UAAU,GACd,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,UAAU,mCAAI,oBAAoB,CAAC,SAAS,CAAC,CAAC;QAEzD,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;YAE7B,IAAI,MAAM,GAA4B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAClE,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,GAAG;oBACP,MAAM;oBACN,WAAW,EAAE,EAAE;oBACf,QAAQ,EAAE,CAAC;oBACX,KAAK,EAAE,IAAI;iBACZ,CAAC;gBACF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;YACtC,CAAC;YAED,IAAI,UAAU,GAAe,GAAG,EAAE,GAAE,CAAC,CAAC;YACtC,IAAI,SAAS,GAAyB,GAAG,EAAE,GAAE,CAAC,CAAC;YAC/C,MAAM,OAAO,GAAkB,IAAI,OAAO,CACxC,CAAC,OAAmB,EAAE,MAA4B,EAAE,EAAE;gBACpD,UAAU,GAAG,OAAO,CAAC;gBACrB,SAAS,GAAG,MAAM,CAAC;YACrB,CAAC,CACF,CAAC;YAEF,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC;gBACtB,IAAI;gBACJ,kBAAkB,EAAE,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,kBAAkB;gBAC/C,UAAU;gBACV,UAAU;gBACV,SAAS;aACV,CAAC,CAAC;YACH,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC;YAC/B,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,MAAM,CAAC;YAEhC,IAAI,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;gBACjD,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;YACxC,CAAC;iBAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;gBACzB,MAAM,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;oBAC7B,MAAM,WAAW,GACf,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;oBAC9B,IAAI,WAAW,EAAE,CAAC;wBAChB,WAAW,CAAC,KAAK,GAAG,IAAI,CAAC;oBAC3B,CAAC;oBACD,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;gBACvC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;gBAC3B;;;mBAGG;gBACH,MAAA,MAAA,MAAM,CAAC,KAAK,EAAC,KAAK,kDAAI,CAAC;YACzB,CAAC;YAED,OAAO,EAAE,OAAO,EAAE,CAAC;QACrB,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,oFAAoF;IAC7E,KAAK,CAAC,QAAQ;QACnB;;;;;;;;;;;;;WAaG;QACH,SAAS,CAAC;YACR,KAAK,MAAM,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;gBACxD,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YACvC,CAAC;YAED,IAAI,IAAI,CAAC,kBAAkB,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;gBACrC,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC;gBACvD,SAAS;YACX,CAAC;YAED,IAAI,IAAI,CAAC,gBAAgB,KAAK,CAAC,EAAE,CAAC;gBAChC,OAAO;YACT,CAAC;YAED,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QAC1B,CAAC;IACH,CAAC;IAEO,2BAA2B;QACjC,IAAI,IAAI,CAAC,yBAAyB,EAAE,CAAC;YACnC,OAAO;QACT,CAAC;QACD,IAAI,CAAC,yBAAyB,GAAG,IAAI,CAAC;QACtC,gBAAgB,CAAC,eAAe,CAC9B,sBAAsB,EACtB,gBAAgB,CAAC,OAAO,EACxB,KAAK,IAAI,EAAE;YACT,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACxB,CAAC,CACF,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,eAAe;QAC3B,OAAO,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;YACvD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAmB,EAAE,EAAE;gBAC9C,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACrC,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAEO,eAAe;QACrB;;;;WAIG;QACH,MAAM,OAAO,GAAsB,IAAI,CAAC,eAAe,CAAC;QACxD,IAAI,CAAC,eAAe,GAAG,EAAE,CAAC;QAC1B,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,IAAI,EAAE,CAAC;QACT,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,iBAAiB;QAC7B,OAAO,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,CAAC;YAC/D,MAAM,IAAI,OAAO,CAAO,CAAC,OAAmB,EAAE,EAAE;gBAC9C,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACvC,CAAC,CAAC,CAAC;QACL,CAAC;QACD,IAAI,CAAC,aAAa,EAAE,CAAC;IACvB,CAAC;IAEO,iBAAiB;QACvB,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,MAAM,IAAI,GAA6B,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,CAAC;QACtE,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,EAAE,CAAC;QACT,CAAC;IACH,CAAC;IAED;;;;;;;;OAQG;IACK,cAAc,CAAC,SAAiB,EAAE,KAAc;QACtD,MAAM,MAAM,GAA4B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACpE,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO;QACT,CAAC;QAED,OACE,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,YAAY;YAC5C,CAAC,KAAK,IAAI,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC,EAC9B,CAAC;YACD,MAAM,KAAK,GAA6B,EAAE,CAAC;YAC3C,IAAI,SAAS,GAAW,CAAC,CAAC;YAE1B,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACrC,MAAM,IAAI,GAAsB,MAAM,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC;gBACvD,IACE,KAAK,CAAC,MAAM,GAAG,CAAC;oBAChB,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,EACxD,CAAC;oBACD,MAAM;gBACR,CAAC;gBACD,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;gBAC3B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACjB,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;gBAC9B,IAAI,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;oBAC3C,MAAM;gBACR,CAAC;YACH,CAAC;YAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACvB,MAAM;YACR,CAAC;YAED,MAAM,CAAC,QAAQ,IAAI,SAAS,CAAC;YAE7B,MAAM,QAAQ,GAAkB,IAAI,CAAC,cAAc,CACjD,MAAM,CAAC,MAAM,EACb,SAAS,EACT,KAAK,EACL,SAAS,CACV,CAAC;YACF,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACtC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE;gBACpB,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC3C,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YAC1C,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC3B,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC;QACtB,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACK,KAAK,CAAC,cAAc,CAC1B,MAAyB,EACzB,SAAiB,EACjB,KAA+B,EAC/B,SAAiB;QAEjB,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAE/B,IAAI,CAAC;YACH,MAAM,SAAS,GAA6B,EAAE,CAAC;YAE/C,KAAK,MAAM,UAAU,IAAI,KAAK,EAAE,CAAC;gBAC/B,IAAI,UAAU,CAAC,UAAU,EAAE,CAAC;oBAC1B,MAAM,IAAI,CAAC,oBAAoB,CAC7B,MAAM,EACN,SAAS,EACT,CAAC,UAAU,CAAC,EACZ,UAAU,CAAC,UAAU,CACtB,CAAC;gBACJ,CAAC;qBAAM,CAAC;oBACN,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;gBAC7B,CAAC;YACH,CAAC;YAED,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzB,MAAM,IAAI,CAAC,oBAAoB,CAC7B,MAAM,EACN,SAAS,EACT,SAAS,EACT,SAAS,SAAS,IAAI,IAAI,CAAC,mBAAmB,EAAE,EAAE,CACnD,CAAC;YACJ,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,WAAW,IAAI,SAAS,CAAC;YAC9B,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC3B,CAAC;IACH,CAAC;IAED;;;;;;;;;OASG;IACK,KAAK,CAAC,oBAAoB,CAChC,MAAyB,EACzB,SAAiB,EACjB,KAA+B,EAC/B,UAAkB;;QAElB,MAAM,IAAI,GAAsB,EAAE,CAAC;QACnC,KAAK,MAAM,UAAU,IAAI,KAAK,EAAE,CAAC;YAC/B,KAAK,MAAM,GAAG,IAAI,UAAU,CAAC,IAAI,EAAE,CAAC;gBAClC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACjB,CAAC;QACH,CAAC;QAED,MAAM,kBAAkB,GACtB,MAAA,KAAK,CAAC,CAAC,CAAC,0CAAE,kBAAkB,CAAC;QAC/B,MAAM,KAAK,GACT,MAAA,IAAI,CAAC,OAAO,CAAC,KAAK,mCAClB,CAAC,CAAC,EAAU,EAAiB,EAAE;YAC7B,OAAO,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC;QAEL,IAAI,SAAS,GAAY,IAAI,CAAC;QAC9B,IAAI,YAAY,GAAW,CAAC,CAAC;QAE7B,KACE,IAAI,OAAO,GAAW,CAAC,EACvB,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,gBAAgB,EACxC,OAAO,EAAE,EACT,CAAC;YACD,YAAY,GAAG,OAAO,CAAC;YACvB,IAAI,CAAC;gBACH,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;oBACzB,MAAM,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,IAAI,EAAE;wBACvC,UAAU;wBACV,kBAAkB;qBACnB,CAAC,CAAC;gBACL,CAAC;qBAAM,CAAC;oBACN,MAAM,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE;wBAChC,UAAU;wBACV,kBAAkB;qBACnB,CAAC,CAAC;gBACL,CAAC;gBAED,KAAK,MAAM,UAAU,IAAI,KAAK,EAAE,CAAC;oBAC/B,UAAU,CAAC,UAAU,EAAE,CAAC;gBAC1B,CAAC;gBACD,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;oBAChB,MAAM,CAAC,IAAI,CACT,qCAAqC,SAAS,KAAK,IAAI,CAAC,MAAM,+BAA+B,OAAO,GAAG,CACxG,CAAC;gBACJ,CAAC;gBACD,OAAO;YACT,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,SAAS,GAAG,GAAG,CAAC;gBAEhB,IACE,CAAC,sBAAsB,CAAC,GAAG,CAAC;oBAC5B,OAAO,KAAK,IAAI,CAAC,OAAO,CAAC,gBAAgB,EACzC,CAAC;oBACD,MAAM;gBACR,CAAC;gBAED,MAAM,QAAQ,GAAW,IAAI,CAAC,GAAG,CAC/B,IAAI,CAAC,OAAO,CAAC,eAAe,EAC5B,IAAI,CAAC,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CACzD,CAAC;gBACF,MAAM,aAAa,GAAW,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC;gBAClE,MAAM,CAAC,IAAI,CACT,qDAAqD,SAAS,KAAK,IAAI,CAAC,MAAM,kBAAkB,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,gBAAgB,4CAA4C,aAAa,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC3P,CAAC;gBACF,MAAM,KAAK,CAAC,aAAa,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QAED,MAAM,UAAU,GAAqB,IAAI,gBAAgB,CAAC;YACxD,SAAS;YACT,QAAQ,EAAE,YAAY;YACtB,KAAK,EAAE,SAAS;SACjB,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CACV,kDAAkD,SAAS,KAAK,IAAI,CAAC,MAAM,mBAAmB,KAAK,CAAC,MAAM,iBAAiB,CAC5H,CAAC;QACF,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACzB,KAAK,MAAM,UAAU,IAAI,KAAK,EAAE,CAAC;YAC/B,UAAU,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAC7B,WAAiC,EACjC,OAAsB,EACtB,SAAgC;IAEhC,MAAM,GAAG,GAAkB,OAAO,CAAC,KAAK,CAAC,CAAC,KAAY,EAAE,EAAE;QACxD,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;IACH,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE;QACb,6DAA6D;IAC/D,CAAC,CAAC,CAAC;IACH,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,aAAa,GAAyB,IAAI,oBAAoB,CAClE,6BAA6B,EAAE,CAChC,CAAC;AAEF,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import ClusterKeyAuthorization from "../../Middleware/ClusterKeyAuthorization";
|
|
2
|
+
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
|
3
|
+
import URL from "../../../Types/API/URL";
|
|
4
|
+
import API from "../../../Utils/API";
|
|
5
|
+
import { TransientInsertError, } from "./TelemetryFanInWriter";
|
|
6
|
+
/*
|
|
7
|
+
* Worker-side client for the telemetry-writer tier.
|
|
8
|
+
*
|
|
9
|
+
* When TELEMETRY_WRITER_URL is set on a pod, its fan-in writer delivers
|
|
10
|
+
* every token group as one HTTP POST to the writer tier instead of
|
|
11
|
+
* inserting into ClickHouse itself. This decouples worker replica count
|
|
12
|
+
* from ClickHouse INSERT concurrency: telemetry insert load on ClickHouse
|
|
13
|
+
* is (writerReplicas × TELEMETRY_FANIN_MAX_CONCURRENT_INSERTS) regardless
|
|
14
|
+
* of how far the worker fleet scales out. (Reads and low-volume direct
|
|
15
|
+
* inserts that never go through the fan-in writer still use this pod's own
|
|
16
|
+
* ClickHouse pools.)
|
|
17
|
+
*
|
|
18
|
+
* Failure classification maps onto the fan-in writer's retry semantics:
|
|
19
|
+
* every payload carries a dedup token that is byte-identical across
|
|
20
|
+
* retries, so any AMBIGUOUS outcome (network error, timeout, writer pod
|
|
21
|
+
* dying mid-request) is safe to classify transient — a retry of a payload
|
|
22
|
+
* that actually landed is dropped server-side by ClickHouse. Only a
|
|
23
|
+
* definitive verdict from the writer (2xx success, 4xx/500 permanent
|
|
24
|
+
* failure) is treated as final.
|
|
25
|
+
*/
|
|
26
|
+
export const TELEMETRY_WRITER_INSERT_ROUTE = "/telemetry-writer/insert";
|
|
27
|
+
export function getTelemetryWriterUrl() {
|
|
28
|
+
const raw = process.env["TELEMETRY_WRITER_URL"];
|
|
29
|
+
const trimmed = (raw || "").trim();
|
|
30
|
+
if (!trimmed) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
// Normalize away a trailing slash so route concatenation is predictable.
|
|
34
|
+
return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
35
|
+
}
|
|
36
|
+
export function isTelemetryWriterForwardingEnabled() {
|
|
37
|
+
return getTelemetryWriterUrl() !== null;
|
|
38
|
+
}
|
|
39
|
+
export function getTelemetryWriterRequestTimeoutMs() {
|
|
40
|
+
const raw = process.env["TELEMETRY_WRITER_REQUEST_TIMEOUT_MS"];
|
|
41
|
+
const parsed = parseInt(raw || "", 10);
|
|
42
|
+
/*
|
|
43
|
+
* Generous (the writer holds the request until ClickHouse accepts the
|
|
44
|
+
* rows, which under load includes its batching wait and retry loop) but
|
|
45
|
+
* deliberately under the BullMQ job lock: the fan-in writer makes up to
|
|
46
|
+
* 6 attempts, and 6 x 90s + ~8s backoff ≈ 9.2 min must stay inside
|
|
47
|
+
* TELEMETRY_LOCK_DURATION_MS (10 min) — otherwise a saturated writer
|
|
48
|
+
* tier stalls jobs into redelivery churn instead of failing them
|
|
49
|
+
* cleanly for retry.
|
|
50
|
+
*/
|
|
51
|
+
return !isNaN(parsed) && parsed > 0 ? parsed : 90000;
|
|
52
|
+
}
|
|
53
|
+
export function getTelemetryWriterMaxBodyBytes() {
|
|
54
|
+
const raw = process.env["TELEMETRY_WRITER_MAX_BODY_BYTES"];
|
|
55
|
+
const parsed = parseInt(raw || "", 10);
|
|
56
|
+
/*
|
|
57
|
+
* Stay well under the writer route's 50 MB express.json limit (plus
|
|
58
|
+
* headroom for headers/encoding). Bodies above this are split before
|
|
59
|
+
* sending — a 413 would be classified permanent and drop rows.
|
|
60
|
+
*/
|
|
61
|
+
return !isNaN(parsed) && parsed > 0 ? parsed : 30000000;
|
|
62
|
+
}
|
|
63
|
+
const defaultPost = async (args) => {
|
|
64
|
+
let response;
|
|
65
|
+
try {
|
|
66
|
+
response = await API.post({
|
|
67
|
+
url: URL.fromString(args.url),
|
|
68
|
+
data: args.body,
|
|
69
|
+
headers: Object.assign({}, ClusterKeyAuthorization.getClusterKeyHeaders()),
|
|
70
|
+
options: {
|
|
71
|
+
timeout: args.timeoutMs,
|
|
72
|
+
skipAuthRefresh: true,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
/*
|
|
78
|
+
* API.post throws (APIException) when no HTTP response exists at all —
|
|
79
|
+
* connection refused, reset, DNS, timeout. Outcome unknown; statusCode 0
|
|
80
|
+
* classifies it transient.
|
|
81
|
+
*/
|
|
82
|
+
return {
|
|
83
|
+
statusCode: 0,
|
|
84
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (response instanceof HTTPErrorResponse) {
|
|
88
|
+
return {
|
|
89
|
+
statusCode: response.statusCode,
|
|
90
|
+
errorMessage: response.message,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { statusCode: response.statusCode };
|
|
94
|
+
};
|
|
95
|
+
/*
|
|
96
|
+
* Writer-tier verdicts that mean "retry with the same token": overload
|
|
97
|
+
* shedding (429) and gateway/unavailability statuses where the request may
|
|
98
|
+
* or may not have been processed.
|
|
99
|
+
*/
|
|
100
|
+
const TRANSIENT_HTTP_STATUSES = new Set([429, 502, 503, 504]);
|
|
101
|
+
function isDefinitiveStatus(statusCode) {
|
|
102
|
+
/*
|
|
103
|
+
* Anything outside real HTTP semantics (0, -1, undefined coerced) is a
|
|
104
|
+
* network-level failure with an unknown outcome — transient by the dedup
|
|
105
|
+
* token argument above.
|
|
106
|
+
*/
|
|
107
|
+
return statusCode >= 100 && statusCode < 600;
|
|
108
|
+
}
|
|
109
|
+
export function createTelemetryWriterTransport(postFn) {
|
|
110
|
+
const post = postFn !== null && postFn !== void 0 ? postFn : defaultPost;
|
|
111
|
+
const postGroup = async (tableName, rows, dedupToken, clickhouseSettings, baseUrl, maxBodyBytes) => {
|
|
112
|
+
const body = Object.assign({ tableName: tableName, rows: rows, dedupToken: dedupToken }, (clickhouseSettings
|
|
113
|
+
? { clickhouseSettings: clickhouseSettings }
|
|
114
|
+
: {}));
|
|
115
|
+
/*
|
|
116
|
+
* Byte-aware splitting: the writer route sits behind a 50 MB JSON
|
|
117
|
+
* parser, and a 413 is (correctly) classified permanent — so an
|
|
118
|
+
* oversized group would be dropped after retries, a failure mode the
|
|
119
|
+
* direct ClickHouse path never had. Halve deterministically instead:
|
|
120
|
+
* a BullMQ retry re-produces byte-identical rows, hence the identical
|
|
121
|
+
* split and identical derived tokens ("<token>#0"/"<token>#1"), so
|
|
122
|
+
* idempotence is preserved (async-insert dedup is a content hash per
|
|
123
|
+
* insert body, and the distinct suffixes keep tokens collision-free
|
|
124
|
+
* for any future sync-insert mode). Halves post sequentially — this
|
|
125
|
+
* all happens under one fan-in insert slot, and splitting must not
|
|
126
|
+
* multiply the pod's concurrent load.
|
|
127
|
+
*/
|
|
128
|
+
if (rows.length > 1) {
|
|
129
|
+
const bodyBytes = Buffer.byteLength(JSON.stringify(body), "utf8");
|
|
130
|
+
if (bodyBytes > maxBodyBytes) {
|
|
131
|
+
const mid = Math.floor(rows.length / 2);
|
|
132
|
+
await postGroup(tableName, rows.slice(0, mid), `${dedupToken}#0`, clickhouseSettings, baseUrl, maxBodyBytes);
|
|
133
|
+
await postGroup(tableName, rows.slice(mid), `${dedupToken}#1`, clickhouseSettings, baseUrl, maxBodyBytes);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const result = await post({
|
|
138
|
+
url: `${baseUrl}${TELEMETRY_WRITER_INSERT_ROUTE}`,
|
|
139
|
+
body: body,
|
|
140
|
+
timeoutMs: getTelemetryWriterRequestTimeoutMs(),
|
|
141
|
+
});
|
|
142
|
+
if (result.statusCode >= 200 && result.statusCode < 300) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const detail = `telemetry-writer insert into ${tableName} (${rows.length} rows) returned status ${result.statusCode}${result.errorMessage ? `: ${result.errorMessage}` : ""}`;
|
|
146
|
+
if (TRANSIENT_HTTP_STATUSES.has(result.statusCode) ||
|
|
147
|
+
!isDefinitiveStatus(result.statusCode)) {
|
|
148
|
+
throw new TransientInsertError(detail);
|
|
149
|
+
}
|
|
150
|
+
throw new Error(detail);
|
|
151
|
+
};
|
|
152
|
+
return async (target, rows, options) => {
|
|
153
|
+
const baseUrl = getTelemetryWriterUrl();
|
|
154
|
+
if (!baseUrl) {
|
|
155
|
+
/*
|
|
156
|
+
* The transport is only installed when the URL is configured; losing
|
|
157
|
+
* it mid-flight (env mutation in tests) is a permanent config error,
|
|
158
|
+
* not something a same-token retry can fix.
|
|
159
|
+
*/
|
|
160
|
+
throw new Error("TelemetryWriterClient: TELEMETRY_WRITER_URL is not configured.");
|
|
161
|
+
}
|
|
162
|
+
await postGroup(target.model.tableName, rows, options.dedupToken, options.clickhouseSettings, baseUrl, getTelemetryWriterMaxBodyBytes());
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=TelemetryWriterClient.js.map
|