@oneuptime/common 11.5.13 → 11.6.0
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/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 +48 -45
- 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/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/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/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/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/ModelImportExport.test.ts +101 -0
- package/Tests/Utils/Number.test.ts +179 -0
- package/Tests/Utils/VersionUtil.test.ts +348 -0
- package/Tests/jest.setup.ts +1 -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/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 +44 -16
- 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/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/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
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
ResponseJSON,
|
|
48
48
|
ResultSet,
|
|
49
49
|
} from "@clickhouse/client";
|
|
50
|
-
import {
|
|
50
|
+
import { nextInsertDedupToken } from "../Utils/AnalyticsDatabase/InsertDedupContext";
|
|
51
51
|
import AnalyticsBaseModel from "../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
|
52
52
|
import { WorkflowRoute } from "../../ServiceRoute";
|
|
53
53
|
import Protocol from "../../Types/API/Protocol";
|
|
@@ -140,40 +140,42 @@ export const MigrationExecuteOptions: ClickhouseExecuteOptions = {
|
|
|
140
140
|
},
|
|
141
141
|
};
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* "<tokenBase>:<table>:<chunkIndex>"` plus async_insert_deduplicate=1 /
|
|
149
|
-
* wait_for_async_insert=1, so a stalled-job retry that re-processes the
|
|
150
|
-
* same payload re-issues byte-identical tokens and ClickHouse drops the
|
|
151
|
-
* duplicate blocks (on replicated tables; on plain MergeTree the token is
|
|
152
|
-
* ignored unless non_replicated_deduplication_window is set — no harm
|
|
153
|
-
* either way). The chunk counter is per table because one job inserts
|
|
154
|
-
* into several tables (e.g. Span + ExceptionInstance) in a deterministic
|
|
155
|
-
* order.
|
|
143
|
+
/*
|
|
144
|
+
* The insert-dedup ambient context lives in
|
|
145
|
+
* Utils/AnalyticsDatabase/InsertDedupContext so that both this service and
|
|
146
|
+
* TelemetryFanInWriter can consume deterministic tokens without an import
|
|
147
|
+
* cycle. Re-exported here for existing callers.
|
|
156
148
|
*
|
|
157
|
-
* HTTP-path inserts run outside the context and
|
|
158
|
-
*
|
|
159
|
-
* affordable off the request
|
|
149
|
+
* HTTP-path inserts run outside the context and are always fire-and-forget
|
|
150
|
+
* (wait_for_async_insert=0) — flush waiting, when opted into via
|
|
151
|
+
* TELEMETRY_WAIT_FOR_ASYNC_INSERT, is only affordable off the request
|
|
152
|
+
* thread and therefore only applies to tokened (queue-job) inserts.
|
|
160
153
|
*/
|
|
161
|
-
export
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
154
|
+
export {
|
|
155
|
+
runWithInsertDedup,
|
|
156
|
+
nextInsertDedupToken,
|
|
157
|
+
type InsertDedupContextStore,
|
|
158
|
+
} from "../Utils/AnalyticsDatabase/InsertDedupContext";
|
|
165
159
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
160
|
+
/*
|
|
161
|
+
* Ack mode for telemetry ClickHouse inserts.
|
|
162
|
+
*
|
|
163
|
+
* Default (false): wait_for_async_insert=0 — fire-and-forget. ClickHouse
|
|
164
|
+
* acks as soon as the batch is accepted into its async-insert buffer and
|
|
165
|
+
* owns flushing from there; inserts release their query slot almost
|
|
166
|
+
* immediately. The trade: flush-time errors surface only in server logs
|
|
167
|
+
* (system.asynchronous_inserts / part log), and a ClickHouse crash between
|
|
168
|
+
* buffer-accept and flush loses that buffer even though callers were acked.
|
|
169
|
+
*
|
|
170
|
+
* Set TELEMETRY_WAIT_FOR_ASYNC_INSERT=true to make every tokened insert
|
|
171
|
+
* wait for the durable flush before acking (ack-after-flush end to end
|
|
172
|
+
* through the fan-in writer and writer tier) — at the cost of each waiting
|
|
173
|
+
* insert holding a ClickHouse query slot until its buffer flushes.
|
|
174
|
+
*/
|
|
175
|
+
export function shouldWaitForAsyncInsert(): boolean {
|
|
176
|
+
const raw: string | undefined =
|
|
177
|
+
process.env["TELEMETRY_WAIT_FOR_ASYNC_INSERT"];
|
|
178
|
+
return raw === "true" || raw === "1";
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
export default class AnalyticsDatabaseService<
|
|
@@ -259,30 +261,31 @@ export default class AnalyticsDatabaseService<
|
|
|
259
261
|
let dedupToken: string | undefined = options?.dedupToken;
|
|
260
262
|
|
|
261
263
|
if (!dedupToken) {
|
|
262
|
-
|
|
263
|
-
insertDedupContext.getStore();
|
|
264
|
-
if (dedupStore) {
|
|
265
|
-
const chunkIndex: number =
|
|
266
|
-
dedupStore.chunkIndexByTable.get(tableName) ?? 0;
|
|
267
|
-
dedupStore.chunkIndexByTable.set(tableName, chunkIndex + 1);
|
|
268
|
-
dedupToken = `${dedupStore.tokenBase}:${tableName}:${chunkIndex}`;
|
|
269
|
-
}
|
|
264
|
+
dedupToken = nextInsertDedupToken(tableName);
|
|
270
265
|
}
|
|
271
266
|
|
|
267
|
+
const waitForAsyncInsert: 0 | 1 = shouldWaitForAsyncInsert() ? 1 : 0;
|
|
268
|
+
|
|
272
269
|
let clickhouseSettings: ClickHouseSettings = {
|
|
273
270
|
async_insert: 1,
|
|
274
|
-
wait_for_async_insert:
|
|
271
|
+
wait_for_async_insert: waitForAsyncInsert,
|
|
275
272
|
};
|
|
276
273
|
|
|
277
274
|
if (dedupToken) {
|
|
278
275
|
/*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
276
|
+
* Dedup settings ride along in both ack modes. For async inserts
|
|
277
|
+
* ClickHouse dedups by content hash of the insert body when
|
|
278
|
+
* async_insert_deduplicate=1 (the explicit token is not yet honored
|
|
279
|
+
* for async inserts — ClickHouse #52018), so byte-identical queue
|
|
280
|
+
* retries are still dropped. The ack mode is a separate, deliberate
|
|
281
|
+
* trade (see shouldWaitForAsyncInsert): by default ClickHouse owns
|
|
282
|
+
* flushing and an ack means "accepted into the async-insert buffer";
|
|
283
|
+
* with TELEMETRY_WAIT_FOR_ASYNC_INSERT=true the ack waits for the
|
|
284
|
+
* durable flush instead.
|
|
282
285
|
*/
|
|
283
286
|
clickhouseSettings = {
|
|
284
287
|
async_insert: 1,
|
|
285
|
-
wait_for_async_insert:
|
|
288
|
+
wait_for_async_insert: waitForAsyncInsert,
|
|
286
289
|
async_insert_deduplicate: 1,
|
|
287
290
|
insert_deduplication_token: dedupToken,
|
|
288
291
|
};
|
|
@@ -1743,10 +1743,67 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
1743
1743
|
});
|
|
1744
1744
|
}
|
|
1745
1745
|
|
|
1746
|
+
/*
|
|
1747
|
+
* Update `data` may arrive as a full model instance rather than a plain
|
|
1748
|
+
* partial — the QueryDeepPartialEntity type structurally admits both, and
|
|
1749
|
+
* callers do construct `new Model()` payloads. A model instance carries
|
|
1750
|
+
* non-column own properties from DatabaseBaseModel (every column
|
|
1751
|
+
* initializer plus `isPermissionIf = {}`), and _updateBy turns every data
|
|
1752
|
+
* key into a select column for its internal find, so the extra keys made
|
|
1753
|
+
* that find throw `TableColumnMetadata not found for isPermissionIf
|
|
1754
|
+
* column` and fail the whole update. Strip a model instance down to its
|
|
1755
|
+
* set table columns; plain objects pass through untouched so a typo'd
|
|
1756
|
+
* column name in a literal still fails loudly instead of being silently
|
|
1757
|
+
* dropped.
|
|
1758
|
+
*
|
|
1759
|
+
* `_id`, `createdAt`, `updatedAt` and `version` are dropped for model
|
|
1760
|
+
* instances the same way BaseAPI drops them from client payloads: the row
|
|
1761
|
+
* is located by the update query (spreading a foreign `_id` into the save
|
|
1762
|
+
* payload would redirect the write), timestamps are database-managed, and
|
|
1763
|
+
* `version` is TypeORM's optimistic-concurrency counter.
|
|
1764
|
+
*/
|
|
1765
|
+
public sanitizeUpdateData(
|
|
1766
|
+
data: UpdateBy<TBaseModel>["data"],
|
|
1767
|
+
): UpdateBy<TBaseModel>["data"] {
|
|
1768
|
+
if (!(data instanceof BaseModel)) {
|
|
1769
|
+
return data;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
const plainData: JSONObject = {};
|
|
1773
|
+
|
|
1774
|
+
for (const key of Object.keys(data)) {
|
|
1775
|
+
if (!this.model.isTableColumn(key)) {
|
|
1776
|
+
continue;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
if (
|
|
1780
|
+
key === "_id" ||
|
|
1781
|
+
key === "createdAt" ||
|
|
1782
|
+
key === "updatedAt" ||
|
|
1783
|
+
key === "version"
|
|
1784
|
+
) {
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
const value: JSONValue = (data as any)[key];
|
|
1789
|
+
|
|
1790
|
+
// Unset columns must not become writes (or select columns).
|
|
1791
|
+
if (value === undefined) {
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
plainData[key] = value;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
return plainData as UpdateBy<TBaseModel>["data"];
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1746
1801
|
private async _updateBy(updateBy: UpdateBy<TBaseModel>): Promise<number> {
|
|
1747
1802
|
try {
|
|
1748
1803
|
this.setTelemetryContextFromProps(updateBy.props);
|
|
1749
1804
|
|
|
1805
|
+
updateBy.data = this.sanitizeUpdateData(updateBy.data);
|
|
1806
|
+
|
|
1750
1807
|
const onUpdate: OnUpdate<TBaseModel> = updateBy.props.ignoreHooks
|
|
1751
1808
|
? { updateBy, carryForward: [] }
|
|
1752
1809
|
: await this.onBeforeUpdate(updateBy);
|
|
@@ -1830,9 +1887,17 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
1830
1887
|
});
|
|
1831
1888
|
|
|
1832
1889
|
for (const item of items) {
|
|
1890
|
+
/*
|
|
1891
|
+
* _id must be set AFTER the spread: update data can carry an
|
|
1892
|
+
* explicit `_id: undefined` (sanitizeUpdateData strips it from model
|
|
1893
|
+
* instances, but a plain object can still hold it), and spreading it
|
|
1894
|
+
* after _id would clobber the located row's id — save() then sees no
|
|
1895
|
+
* primary key, INSERTs instead of updating, and dies on the first
|
|
1896
|
+
* NOT NULL column.
|
|
1897
|
+
*/
|
|
1833
1898
|
const updatedItem: any = {
|
|
1834
|
-
_id: item._id!,
|
|
1835
1899
|
...data,
|
|
1900
|
+
_id: item._id!,
|
|
1836
1901
|
} as any;
|
|
1837
1902
|
|
|
1838
1903
|
logger.debug("Updated Item", {
|
|
@@ -49,6 +49,40 @@ export class Service extends DatabaseService<Model> {
|
|
|
49
49
|
createdItem.webhookSecretKey = secretKey;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/*
|
|
53
|
+
* A workflow that arrives with its graph already in place - imported from
|
|
54
|
+
* a JSON export, or duplicated from another workflow - never passes
|
|
55
|
+
* through onUpdateSuccess, so nothing has denormalized its trigger onto
|
|
56
|
+
* the row. The runner looks workflows up by triggerId, so without this the
|
|
57
|
+
* workflow is saved and looks correct in the builder but silently never
|
|
58
|
+
* fires.
|
|
59
|
+
*/
|
|
60
|
+
if (createdItem.id && createdItem.graph) {
|
|
61
|
+
await this.saveTriggerFromGraph({
|
|
62
|
+
workflowId: createdItem.id,
|
|
63
|
+
graph: createdItem.graph,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/*
|
|
67
|
+
* Registers schedule triggers with the runner. Best effort: the row and
|
|
68
|
+
* its trigger are already persisted, so a workflow service outage must
|
|
69
|
+
* not fail the create. The trigger is already on the row, so the next
|
|
70
|
+
* save - or the runner's own startup scan in Schedule.init, which
|
|
71
|
+
* queries by triggerId - picks the workflow up.
|
|
72
|
+
*/
|
|
73
|
+
try {
|
|
74
|
+
await this.notifyWorkflowService(createdItem.id);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
logger.error(
|
|
77
|
+
`Error notifying workflow service of created workflow: ${error}`,
|
|
78
|
+
{
|
|
79
|
+
projectId: createdItem.projectId?.toString(),
|
|
80
|
+
workflowId: createdItem.id?.toString(),
|
|
81
|
+
} as LogAttributes,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
52
86
|
if (createdItem.projectId && createdItem.id) {
|
|
53
87
|
/*
|
|
54
88
|
* Run label rule first so rule-added labels are persisted before
|
|
@@ -87,39 +121,13 @@ export class Service extends DatabaseService<Model> {
|
|
|
87
121
|
): Promise<OnUpdate<Model>> {
|
|
88
122
|
/// save trigger and trigger args.
|
|
89
123
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
(onUpdate.updateBy.data as any).graph &&
|
|
93
|
-
(((onUpdate.updateBy.data as any).graph as any)[
|
|
94
|
-
"nodes"
|
|
95
|
-
] as Array<JSONObject>)
|
|
96
|
-
) {
|
|
97
|
-
let trigger: NodeDataProp | null = null;
|
|
98
|
-
|
|
99
|
-
// check if it has a trigger node.
|
|
100
|
-
for (const node of ((onUpdate.updateBy.data as any).graph as any)[
|
|
101
|
-
"nodes"
|
|
102
|
-
] as Array<JSONObject>) {
|
|
103
|
-
const nodeData: NodeDataProp = node["data"] as any;
|
|
104
|
-
if (
|
|
105
|
-
nodeData.componentType === ComponentType.Trigger &&
|
|
106
|
-
nodeData.nodeType === NodeType.Node
|
|
107
|
-
) {
|
|
108
|
-
// found the trigger;
|
|
109
|
-
trigger = nodeData;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
124
|
+
const updatedGraph: JSONObject | undefined = (onUpdate.updateBy.data as any)
|
|
125
|
+
?.graph as JSONObject | undefined;
|
|
112
126
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
triggerArguments: trigger?.arguments || {},
|
|
118
|
-
} as any,
|
|
119
|
-
props: {
|
|
120
|
-
isRoot: true,
|
|
121
|
-
ignoreHooks: true,
|
|
122
|
-
},
|
|
127
|
+
if (updatedGraph) {
|
|
128
|
+
await this.saveTriggerFromGraph({
|
|
129
|
+
workflowId: new ObjectID(onUpdate.updateBy.query._id! as any),
|
|
130
|
+
graph: updatedGraph,
|
|
123
131
|
});
|
|
124
132
|
}
|
|
125
133
|
|
|
@@ -127,23 +135,76 @@ export class Service extends DatabaseService<Model> {
|
|
|
127
135
|
workflowId: onUpdate.updateBy.query._id?.toString(),
|
|
128
136
|
} as LogAttributes);
|
|
129
137
|
|
|
138
|
+
await this.notifyWorkflowService(
|
|
139
|
+
new ObjectID(onUpdate.updateBy.query._id! as any),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
logger.debug("Updated workflow on the workflow service", {
|
|
143
|
+
workflowId: onUpdate.updateBy.query._id?.toString(),
|
|
144
|
+
} as LogAttributes);
|
|
145
|
+
|
|
146
|
+
return onUpdate;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/*
|
|
150
|
+
* The trigger node is denormalized out of the graph onto triggerId /
|
|
151
|
+
* triggerArguments because the runner queries workflows by trigger and
|
|
152
|
+
* cannot parse every graph to do it. A graph with no trigger node clears
|
|
153
|
+
* both columns, which is how a workflow stops firing when its trigger is
|
|
154
|
+
* removed in the builder.
|
|
155
|
+
*/
|
|
156
|
+
private async saveTriggerFromGraph(data: {
|
|
157
|
+
workflowId: ObjectID;
|
|
158
|
+
graph: JSONObject;
|
|
159
|
+
}): Promise<void> {
|
|
160
|
+
const nodes: Array<JSONObject> | undefined = data.graph["nodes"] as
|
|
161
|
+
| Array<JSONObject>
|
|
162
|
+
| undefined;
|
|
163
|
+
|
|
164
|
+
if (!nodes || !Array.isArray(nodes)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let trigger: NodeDataProp | null = null;
|
|
169
|
+
|
|
170
|
+
// check if it has a trigger node.
|
|
171
|
+
for (const node of nodes) {
|
|
172
|
+
const nodeData: NodeDataProp = node["data"] as any;
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
nodeData?.componentType === ComponentType.Trigger &&
|
|
176
|
+
nodeData?.nodeType === NodeType.Node
|
|
177
|
+
) {
|
|
178
|
+
// found the trigger;
|
|
179
|
+
trigger = nodeData;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await this.updateOneById({
|
|
184
|
+
id: data.workflowId,
|
|
185
|
+
data: {
|
|
186
|
+
triggerId: trigger?.metadataId || null,
|
|
187
|
+
triggerArguments: trigger?.arguments || {},
|
|
188
|
+
} as any,
|
|
189
|
+
props: {
|
|
190
|
+
isRoot: true,
|
|
191
|
+
ignoreHooks: true,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private async notifyWorkflowService(workflowId: ObjectID): Promise<void> {
|
|
130
197
|
await API.post<EmptyResponseData>({
|
|
131
198
|
url: new URL(
|
|
132
199
|
Protocol.HTTP,
|
|
133
200
|
WorkflowHostname,
|
|
134
|
-
new Route("/workflow/update/" +
|
|
201
|
+
new Route("/workflow/update/" + workflowId.toString()),
|
|
135
202
|
),
|
|
136
203
|
data: {},
|
|
137
204
|
headers: {
|
|
138
205
|
...ClusterKeyAuthorization.getClusterKeyHeaders(),
|
|
139
206
|
},
|
|
140
207
|
});
|
|
141
|
-
|
|
142
|
-
logger.debug("Updated workflow on the workflow service", {
|
|
143
|
-
workflowId: onUpdate.updateBy.query._id?.toString(),
|
|
144
|
-
} as LogAttributes);
|
|
145
|
-
|
|
146
|
-
return onUpdate;
|
|
147
208
|
}
|
|
148
209
|
}
|
|
149
210
|
export default new Service();
|
|
@@ -15,6 +15,7 @@ import ComponentID from "../../../../Types/Workflow/ComponentID";
|
|
|
15
15
|
import ScheduleComponents from "../../../../Types/Workflow/Components/Schedule";
|
|
16
16
|
import Workflow from "../../../../Models/DatabaseModels/Workflow";
|
|
17
17
|
import CaptureSpan from "../../../Utils/Telemetry/CaptureSpan";
|
|
18
|
+
import logger from "../../../Utils/Logger";
|
|
18
19
|
|
|
19
20
|
export default class WebhookTrigger extends TriggerCode {
|
|
20
21
|
public constructor() {
|
|
@@ -52,24 +53,37 @@ export default class WebhookTrigger extends TriggerCode {
|
|
|
52
53
|
|
|
53
54
|
// query all workflows.
|
|
54
55
|
for (const workflow of workflows) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
/*
|
|
57
|
+
* Isolate each workflow: a single workflow whose schedule can't be
|
|
58
|
+
* registered (e.g. an invalid cron, or a transient queue error) must not
|
|
59
|
+
* abort the startup scan and leave every other scheduled workflow
|
|
60
|
+
* unregistered.
|
|
61
|
+
*/
|
|
62
|
+
try {
|
|
63
|
+
const executeWorkflow: ExecuteWorkflowType = {
|
|
64
|
+
workflowId: new ObjectID(workflow._id!),
|
|
65
|
+
returnValues: {},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
workflow.triggerArguments &&
|
|
70
|
+
workflow.triggerArguments["schedule"] &&
|
|
71
|
+
workflow.isEnabled
|
|
72
|
+
) {
|
|
73
|
+
await props.scheduleWorkflow(
|
|
74
|
+
executeWorkflow,
|
|
75
|
+
workflow.triggerArguments["schedule"] as string,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!workflow.isEnabled) {
|
|
80
|
+
await props.removeWorkflow(workflow.id!);
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logger.error(
|
|
84
|
+
`Failed to register schedule for workflow ${workflow._id}`,
|
|
68
85
|
);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (!workflow.isEnabled) {
|
|
72
|
-
await props.removeWorkflow(workflow.id!);
|
|
86
|
+
logger.error(err);
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
89
|
}
|
|
@@ -128,23 +142,34 @@ export default class WebhookTrigger extends TriggerCode {
|
|
|
128
142
|
returnValues: {},
|
|
129
143
|
};
|
|
130
144
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
/*
|
|
146
|
+
* Guard the (re)registration so a bad schedule can't fail the workflow
|
|
147
|
+
* save that triggered this update. Invalid crons are already surfaced as a
|
|
148
|
+
* workflow log by the queue layer; this catch is a backstop for unexpected
|
|
149
|
+
* errors (e.g. a transient queue error).
|
|
150
|
+
*/
|
|
151
|
+
try {
|
|
152
|
+
if (
|
|
153
|
+
workflow.triggerArguments &&
|
|
154
|
+
workflow.triggerArguments["schedule"] &&
|
|
155
|
+
workflow.isEnabled
|
|
156
|
+
) {
|
|
157
|
+
await this.scheduleWorkflow(
|
|
158
|
+
executeWorkflow,
|
|
159
|
+
workflow.triggerArguments["schedule"] as string,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
141
162
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
163
|
+
if (!this.removeWorkflow) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
145
166
|
|
|
146
|
-
|
|
147
|
-
|
|
167
|
+
if (!workflow.isEnabled) {
|
|
168
|
+
await this.removeWorkflow(workflow.id!);
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
logger.error(`Failed to update schedule for workflow ${workflow._id}`);
|
|
172
|
+
logger.error(err);
|
|
148
173
|
}
|
|
149
174
|
}
|
|
150
175
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ambient context that makes ClickHouse inserts idempotent across queue
|
|
5
|
+
* retries. The telemetry queue worker wraps each job in
|
|
6
|
+
* `runWithInsertDedup(jobId, ...)`; token consumers then derive
|
|
7
|
+
* `insert_deduplication_token = "<tokenBase>:<table>:<chunkIndex>"`, so a
|
|
8
|
+
* stalled-job recovery or attempts-based retry that re-processes the same
|
|
9
|
+
* payload re-issues byte-identical tokens and ClickHouse drops the duplicate
|
|
10
|
+
* blocks (on replicated tables; on plain MergeTree the token is ignored
|
|
11
|
+
* unless non_replicated_deduplication_window is set — no harm either way).
|
|
12
|
+
* The chunk counter is per table because one job inserts into several tables
|
|
13
|
+
* (e.g. Span + ExceptionInstance) in a deterministic order.
|
|
14
|
+
*
|
|
15
|
+
* Two consumers exist:
|
|
16
|
+
* - AnalyticsDatabaseService.insertJsonRows stamps direct inserts that run
|
|
17
|
+
* inside the context without an explicit token.
|
|
18
|
+
* - TelemetryFanInWriter captures a token per submission AT SUBMIT TIME
|
|
19
|
+
* (still inside the job's context) so cross-job batching downstream can
|
|
20
|
+
* preserve per-job retry idempotence.
|
|
21
|
+
*/
|
|
22
|
+
export interface InsertDedupContextStore {
|
|
23
|
+
tokenBase: string;
|
|
24
|
+
chunkIndexByTable: Map<string, number>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const insertDedupContext: AsyncLocalStorage<InsertDedupContextStore> =
|
|
28
|
+
new AsyncLocalStorage<InsertDedupContextStore>();
|
|
29
|
+
|
|
30
|
+
export function runWithInsertDedup<T>(
|
|
31
|
+
tokenBase: string,
|
|
32
|
+
fn: () => Promise<T>,
|
|
33
|
+
): Promise<T> {
|
|
34
|
+
return insertDedupContext.run(
|
|
35
|
+
{ tokenBase, chunkIndexByTable: new Map<string, number>() },
|
|
36
|
+
fn,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Consume the next deterministic dedup token for `tableName` from the
|
|
42
|
+
* ambient context, or undefined when called outside a runWithInsertDedup
|
|
43
|
+
* scope. Consuming advances the per-table chunk counter, so calls must
|
|
44
|
+
* happen in a deterministic order relative to the payload — true for the
|
|
45
|
+
* ingest services, whose chunking is a pure function of the (byte-identical
|
|
46
|
+
* across retries) job body.
|
|
47
|
+
*/
|
|
48
|
+
export function nextInsertDedupToken(tableName: string): string | undefined {
|
|
49
|
+
const store: InsertDedupContextStore | undefined =
|
|
50
|
+
insertDedupContext.getStore();
|
|
51
|
+
|
|
52
|
+
if (!store) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const chunkIndex: number = store.chunkIndexByTable.get(tableName) ?? 0;
|
|
57
|
+
store.chunkIndexByTable.set(tableName, chunkIndex + 1);
|
|
58
|
+
return `${store.tokenBase}:${tableName}:${chunkIndex}`;
|
|
59
|
+
}
|
package/Server/Utils/Express.ts
CHANGED
|
@@ -103,6 +103,19 @@ class Express {
|
|
|
103
103
|
|
|
104
104
|
if (!this.httpServer) {
|
|
105
105
|
this.httpServer = createServer(this.app);
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
* Keep upstream (nginx) connection reuse safe. When the ingress pools
|
|
109
|
+
* idle upstream connections (proxy keepalive), the backend must NOT be
|
|
110
|
+
* the side that closes an idle socket first — otherwise nginx can pick a
|
|
111
|
+
* socket the backend already closed and 502 a request it cannot safely
|
|
112
|
+
* retry (e.g. a POST on the telemetry ingest path). Node's default
|
|
113
|
+
* keepAliveTimeout (~5s) is well below nginx's upstream keepalive_timeout
|
|
114
|
+
* (60s), so raise it above that window. headersTimeout must stay greater
|
|
115
|
+
* than keepAliveTimeout or Node can cut an idle socket mid-request.
|
|
116
|
+
*/
|
|
117
|
+
this.httpServer.keepAliveTimeout = 75 * 1000;
|
|
118
|
+
this.httpServer.headersTimeout = 76 * 1000;
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
/*
|