@forzalabs/remora 1.1.11 → 1.1.13
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/documentation/default_resources/consumer.json +16 -52
- package/documentation/default_resources/mock_data.csv +6 -0
- package/documentation/default_resources/producer.json +15 -32
- package/documentation/default_resources/project.json +2 -2
- package/documentation/default_resources/source.json +7 -14
- package/index.js +138 -55
- package/package.json +1 -1
- package/workers/ExecutorWorker.js +127 -45
|
@@ -1,53 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
],
|
|
20
|
-
"fields": [
|
|
21
|
-
{ "key": "<producer field name>", "from": "<producer name>" },
|
|
22
|
-
{ "key": "<original field name>", "from": "<producer name>", "alias": "<new field name>" },
|
|
23
|
-
{ "key": "<secondary producer field name>", "from": "<secondary producer name>" },
|
|
24
|
-
{ "key": "<another field name>", "from": "<producer name>" }
|
|
25
|
-
],
|
|
26
|
-
"filters": [
|
|
27
|
-
{
|
|
28
|
-
"sql": "<filter condition>"
|
|
29
|
-
}
|
|
30
|
-
],
|
|
31
|
-
"outputs": [
|
|
32
|
-
{ "format": "API" },
|
|
33
|
-
{
|
|
34
|
-
"format": "JSON",
|
|
35
|
-
"exportDestination": "<export destination>"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"format": "CSV",
|
|
39
|
-
"exportDestination": "<export destination>",
|
|
40
|
-
"trigger": {
|
|
41
|
-
"type": "CRON",
|
|
42
|
-
"value": "0 0 * * *"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
],
|
|
46
|
-
"metadata": {
|
|
47
|
-
"<metadata tag key>": "<metadata tag value>",
|
|
48
|
-
"owner_email": "<owner email>"
|
|
49
|
-
},
|
|
50
|
-
"project": "<project name>",
|
|
51
|
-
"schema": "<schema name>",
|
|
52
|
-
"_version": 1
|
|
53
|
-
}
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/consumer-schema.json",
|
|
3
|
+
"name": "c_default",
|
|
4
|
+
"producers": [
|
|
5
|
+
{ "name": "p_default" }
|
|
6
|
+
],
|
|
7
|
+
"fields": [
|
|
8
|
+
{ "key": "id" },
|
|
9
|
+
{ "key": "name" },
|
|
10
|
+
{ "key": "age" },
|
|
11
|
+
{ "key": "country" },
|
|
12
|
+
{ "key": "spend" }
|
|
13
|
+
],
|
|
14
|
+
"outputs": [
|
|
15
|
+
{ "format": "JSON", "exportDestination": "s_default" }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -1,33 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"mask": "<hash | mask | crypt>",
|
|
18
|
-
"description": "<field description>"
|
|
19
|
-
}
|
|
20
|
-
],
|
|
21
|
-
"measures": [
|
|
22
|
-
{
|
|
23
|
-
"name": "<calculated measure name>",
|
|
24
|
-
"description": "<measure description>",
|
|
25
|
-
"sql": "<sql expression>"
|
|
26
|
-
}
|
|
27
|
-
],
|
|
28
|
-
"settings": {
|
|
29
|
-
"sqlTable": "<source table name>",
|
|
30
|
-
"direct": true
|
|
31
|
-
},
|
|
32
|
-
"_version": 1
|
|
33
|
-
}
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/producer-schema.json",
|
|
3
|
+
"name": "p_default",
|
|
4
|
+
"source": "s_default",
|
|
5
|
+
"dimensions": [
|
|
6
|
+
{ "name": "id", "type": "string" },
|
|
7
|
+
{ "name": "name", "type": "string" },
|
|
8
|
+
{ "name": "age", "type": "number" },
|
|
9
|
+
{ "name": "country", "type": "string" },
|
|
10
|
+
{ "name": "spend", "type": "number" }
|
|
11
|
+
],
|
|
12
|
+
"settings": {
|
|
13
|
+
"fileType": "CSV",
|
|
14
|
+
"fileKey": "mock_data.csv"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/project-schema.json",
|
|
3
|
-
"name": "
|
|
3
|
+
"name": "Default Remora Project",
|
|
4
4
|
"version": "1.0.0",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "This is the standard Remora project created when you run init.",
|
|
6
6
|
"consumers": ["/consumers"],
|
|
7
7
|
"producers": ["/producers"],
|
|
8
8
|
"sources": ["/sources"],
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"accessKey": "<access key var from env>",
|
|
10
|
-
"secretKey": "<secret key var from env>",
|
|
11
|
-
"region": "<aws region>",
|
|
12
|
-
"database": "<db name>",
|
|
13
|
-
"workgroup": "<db workgroup>"
|
|
14
|
-
},
|
|
15
|
-
"engine": "<aws-redshift | aws-dynamodb | aws-s3 | postgres | local>"
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/source-schema.json",
|
|
3
|
+
"name": "s_default",
|
|
4
|
+
"authentication": {
|
|
5
|
+
"method": "implicit",
|
|
6
|
+
"path": "./default_remora_data"
|
|
7
|
+
},
|
|
8
|
+
"engine": "local"
|
|
16
9
|
}
|
package/index.js
CHANGED
|
@@ -13228,6 +13228,8 @@ var FileLogServiceClass = class {
|
|
|
13228
13228
|
constructor() {
|
|
13229
13229
|
this._enabled = false;
|
|
13230
13230
|
this.enable = (folder = "logs", file = "remora.log") => {
|
|
13231
|
+
this._folder = folder;
|
|
13232
|
+
this._file = file;
|
|
13231
13233
|
this._logger = import_winston.default.createLogger({
|
|
13232
13234
|
level: "debug",
|
|
13233
13235
|
format: import_winston.default.format.combine(
|
|
@@ -13260,6 +13262,23 @@ ${stack}` : base;
|
|
|
13260
13262
|
});
|
|
13261
13263
|
};
|
|
13262
13264
|
this.flush = () => {
|
|
13265
|
+
if (!this._enabled || !this._logger) return Promise.resolve();
|
|
13266
|
+
return new Promise((resolve) => {
|
|
13267
|
+
let pending = this._logger.transports.length;
|
|
13268
|
+
if (pending === 0) return resolve();
|
|
13269
|
+
for (const transport of this._logger.transports) {
|
|
13270
|
+
if (typeof transport.on === "function") {
|
|
13271
|
+
transport.once("logged", () => {
|
|
13272
|
+
if (--pending === 0) resolve();
|
|
13273
|
+
});
|
|
13274
|
+
this._logger.info("");
|
|
13275
|
+
} else {
|
|
13276
|
+
if (--pending === 0) resolve();
|
|
13277
|
+
}
|
|
13278
|
+
}
|
|
13279
|
+
});
|
|
13280
|
+
};
|
|
13281
|
+
this.close = () => {
|
|
13263
13282
|
if (!this._enabled || !this._logger) return Promise.resolve();
|
|
13264
13283
|
return new Promise((resolve) => {
|
|
13265
13284
|
this._logger.on("finish", resolve);
|
|
@@ -13339,6 +13358,7 @@ var Logger = class {
|
|
|
13339
13358
|
FileLogService_default.write("INFO", String(message));
|
|
13340
13359
|
};
|
|
13341
13360
|
this.flush = () => FileLogService_default.flush();
|
|
13361
|
+
this.close = () => FileLogService_default.close();
|
|
13342
13362
|
this.error = (error) => {
|
|
13343
13363
|
let message;
|
|
13344
13364
|
let stack;
|
|
@@ -13480,7 +13500,7 @@ var import_promises = __toESM(require("fs/promises"), 1);
|
|
|
13480
13500
|
|
|
13481
13501
|
// ../../packages/constants/src/Constants.ts
|
|
13482
13502
|
var CONSTANTS = {
|
|
13483
|
-
cliVersion: "1.1.
|
|
13503
|
+
cliVersion: "1.1.13",
|
|
13484
13504
|
backendVersion: 1,
|
|
13485
13505
|
backendPort: 5088,
|
|
13486
13506
|
workerVersion: 2,
|
|
@@ -13652,6 +13672,42 @@ var SchemaValidatorClass = class {
|
|
|
13652
13672
|
var SchemaValidator = new SchemaValidatorClass();
|
|
13653
13673
|
var SchemaValidator_default = SchemaValidator;
|
|
13654
13674
|
|
|
13675
|
+
// ../../packages/common/src/ResourcesUtils.ts
|
|
13676
|
+
var ResourcesUtilsClass = class {
|
|
13677
|
+
constructor() {
|
|
13678
|
+
this.getAvailableColumns = (consumer) => {
|
|
13679
|
+
return consumer.producers.flatMap((cProd) => {
|
|
13680
|
+
const producer = Environment_default.getProducer(cProd.name);
|
|
13681
|
+
if (!producer) {
|
|
13682
|
+
const subConsumer = Environment_default.getConsumer(cProd.name);
|
|
13683
|
+
Affirm_default(subConsumer, `No producer found with name "${cProd.name}"`);
|
|
13684
|
+
return this.getAvailableColumns(subConsumer);
|
|
13685
|
+
} else {
|
|
13686
|
+
const dims = producer.dimensions.map((x) => ({
|
|
13687
|
+
consumerAlias: null,
|
|
13688
|
+
consumerKey: null,
|
|
13689
|
+
nameInProducer: x.name,
|
|
13690
|
+
aliasInProducer: x.alias,
|
|
13691
|
+
dimension: x,
|
|
13692
|
+
owner: cProd.name
|
|
13693
|
+
}));
|
|
13694
|
+
const meas = producer.measures?.map((x) => ({
|
|
13695
|
+
consumerAlias: null,
|
|
13696
|
+
consumerKey: null,
|
|
13697
|
+
nameInProducer: x.name,
|
|
13698
|
+
aliasInProducer: x.name,
|
|
13699
|
+
measure: x,
|
|
13700
|
+
owner: cProd.name
|
|
13701
|
+
})) ?? [];
|
|
13702
|
+
return [...dims, ...meas];
|
|
13703
|
+
}
|
|
13704
|
+
});
|
|
13705
|
+
};
|
|
13706
|
+
}
|
|
13707
|
+
};
|
|
13708
|
+
var ResourcesUtils = new ResourcesUtilsClass();
|
|
13709
|
+
var ResourcesUtils_default = ResourcesUtils;
|
|
13710
|
+
|
|
13655
13711
|
// ../../packages/common/src/validation/Validator.ts
|
|
13656
13712
|
var ValidatorClass = class {
|
|
13657
13713
|
constructor() {
|
|
@@ -13762,6 +13818,38 @@ var ValidatorClass = class {
|
|
|
13762
13818
|
const uniqNames = Algo_default.uniqBy(sources, "name");
|
|
13763
13819
|
if (uniqNames.length !== 1)
|
|
13764
13820
|
errors.push(`Producers with different sources are used in the consumer "${consumer.name}" (${uniqNames.join(", ")})`);
|
|
13821
|
+
const availableColumns = ResourcesUtils_default.getAvailableColumns(consumer);
|
|
13822
|
+
const availableFieldsByProducer = /* @__PURE__ */ new Map();
|
|
13823
|
+
for (const col of availableColumns) {
|
|
13824
|
+
const existing = availableFieldsByProducer.get(col.owner) ?? [];
|
|
13825
|
+
if (!existing.includes(col.nameInProducer))
|
|
13826
|
+
existing.push(col.nameInProducer);
|
|
13827
|
+
availableFieldsByProducer.set(col.owner, existing);
|
|
13828
|
+
}
|
|
13829
|
+
const allAvailableFields = [...availableFieldsByProducer.values()].flat();
|
|
13830
|
+
for (const field of consumer.fields) {
|
|
13831
|
+
if (field.key === "*" || field.fixed || field.copyFrom) continue;
|
|
13832
|
+
if (field.from) {
|
|
13833
|
+
const producerFields = availableFieldsByProducer.get(field.from);
|
|
13834
|
+
if (producerFields && !producerFields.includes(field.key))
|
|
13835
|
+
errors.push(`Field "${field.key}" (from: "${field.from}") is not found in producer "${field.from}" in consumer "${consumer.name}". Available fields: ${producerFields.join(", ")}`);
|
|
13836
|
+
} else if (consumer.producers.length === 1) {
|
|
13837
|
+
const producerFields = availableFieldsByProducer.get(consumer.producers[0].name);
|
|
13838
|
+
if (producerFields && !producerFields.includes(field.key))
|
|
13839
|
+
errors.push(`Field "${field.key}" is not found in producer "${consumer.producers[0].name}" in consumer "${consumer.name}". Available fields: ${producerFields.join(", ")}`);
|
|
13840
|
+
} else {
|
|
13841
|
+
if (allAvailableFields.length > 0 && !allAvailableFields.includes(field.key))
|
|
13842
|
+
errors.push(`Field "${field.key}" is not found in any of the producers of consumer "${consumer.name}".`);
|
|
13843
|
+
}
|
|
13844
|
+
}
|
|
13845
|
+
for (let i = 0; i < consumer.fields.length; i++) {
|
|
13846
|
+
const field = consumer.fields[i];
|
|
13847
|
+
if (!field.copyFrom) continue;
|
|
13848
|
+
const precedingFields = consumer.fields.slice(0, i);
|
|
13849
|
+
const found = precedingFields.find((f) => (f.alias ?? f.key) === field.copyFrom);
|
|
13850
|
+
if (!found)
|
|
13851
|
+
errors.push(`Field "${field.alias ?? field.key}" uses copyFrom "${field.copyFrom}" but no field with that name/alias exists before it in consumer "${consumer.name}".`);
|
|
13852
|
+
}
|
|
13765
13853
|
if (consumer.filters && consumer.filters.length > 0) {
|
|
13766
13854
|
if (consumer.filters.some((x) => x.sql && x.rule))
|
|
13767
13855
|
errors.push(`A single consumer can't have both filters based on SQL and filters based on rules.`);
|
|
@@ -13834,6 +13922,20 @@ var ValidatorClass = class {
|
|
|
13834
13922
|
if (consumer.options) {
|
|
13835
13923
|
if (Algo_default.hasVal(consumer.options.distinct) && Algo_default.hasVal(consumer.options.distinctOn))
|
|
13836
13924
|
errors.push(`Can't specify a "distinct" and a "distinctOn" clause on the same consumer (${consumer.name}); use one or the other.`);
|
|
13925
|
+
if (Algo_default.hasVal(consumer.options.distinctOn)) {
|
|
13926
|
+
const { distinctOn } = consumer.options;
|
|
13927
|
+
const hasWildcard = consumer.fields.some((x) => x.key === "*");
|
|
13928
|
+
const consumerFieldKeys = consumer.fields.map((x) => x.alias ?? x.key);
|
|
13929
|
+
if (!hasWildcard) {
|
|
13930
|
+
const missingKeys = distinctOn.keys.filter((k) => !consumerFieldKeys.includes(k));
|
|
13931
|
+
if (missingKeys.length > 0)
|
|
13932
|
+
errors.push(`distinctOn references key(s) "${missingKeys.join(", ")}" that are not present in the consumer "${consumer.name}".`);
|
|
13933
|
+
}
|
|
13934
|
+
if (distinctOn.resolution?.orderBy && !hasWildcard) {
|
|
13935
|
+
if (!consumerFieldKeys.includes(distinctOn.resolution.orderBy))
|
|
13936
|
+
errors.push(`distinctOn resolution orderBy field "${distinctOn.resolution.orderBy}" is not present in the consumer "${consumer.name}".`);
|
|
13937
|
+
}
|
|
13938
|
+
}
|
|
13837
13939
|
if (Algo_default.hasVal(consumer.options.pivot)) {
|
|
13838
13940
|
if (Algo_default.hasVal(consumer.options.distinct) || Algo_default.hasVal(consumer.options.distinctOn))
|
|
13839
13941
|
errors.push(`Can't specify "pivot" together with "distinct" or "distinctOn" on the same consumer (${consumer.name}).`);
|
|
@@ -16071,7 +16173,7 @@ var DOCUMENTATION_DIR = resolveAssetDir("documentation");
|
|
|
16071
16173
|
|
|
16072
16174
|
// src/actions/init.ts
|
|
16073
16175
|
var init = async () => {
|
|
16074
|
-
console.log(import_chalk5.default.blue.bold("\u{1F4E6} Initializing your
|
|
16176
|
+
console.log(import_chalk5.default.blue.bold("\u{1F4E6} Initializing your Remora app (make sure you have set your REMORA_LICENCE_KEY in the environment variables)..."));
|
|
16075
16177
|
try {
|
|
16076
16178
|
const spinner = (0, import_ora3.default)("Creating configuration files...").start();
|
|
16077
16179
|
const directories = [
|
|
@@ -16079,23 +16181,22 @@ var init = async () => {
|
|
|
16079
16181
|
"remora/consumers",
|
|
16080
16182
|
"remora/producers",
|
|
16081
16183
|
"remora/schemas",
|
|
16082
|
-
"remora/sources"
|
|
16184
|
+
"remora/sources",
|
|
16185
|
+
"default_remora_data"
|
|
16083
16186
|
];
|
|
16084
16187
|
const defaultSource = import_fs_extra2.default.readFileSync(import_path13.default.join(DOCUMENTATION_DIR, "default_resources/source.json"), "utf-8");
|
|
16085
16188
|
const defaultConsumer = import_fs_extra2.default.readFileSync(import_path13.default.join(DOCUMENTATION_DIR, "default_resources/consumer.json"), "utf-8");
|
|
16086
16189
|
const defaultProducer = import_fs_extra2.default.readFileSync(import_path13.default.join(DOCUMENTATION_DIR, "default_resources/producer.json"), "utf-8");
|
|
16087
16190
|
const defaultRemoraProject = import_fs_extra2.default.readFileSync(import_path13.default.join(DOCUMENTATION_DIR, "default_resources/project.json"), "utf-8");
|
|
16191
|
+
const defaultMockData = import_fs_extra2.default.readFileSync(import_path13.default.join(DOCUMENTATION_DIR, "default_resources/mock_data.csv"), "utf-8");
|
|
16088
16192
|
const readme = import_fs_extra2.default.readFileSync(import_path13.default.join(DOCUMENTATION_DIR, "README.md"), "utf-8");
|
|
16089
16193
|
const files = [
|
|
16090
|
-
{ path: "remora/sources
|
|
16091
|
-
{ path: "remora/
|
|
16092
|
-
{ path: "remora/
|
|
16093
|
-
{ path: "remora/consumers/default-consumer.json", content: defaultConsumer },
|
|
16094
|
-
{ path: "remora/producers/.gitkeep", content: "" },
|
|
16095
|
-
{ path: "remora/producers/default-producer.json", content: defaultProducer },
|
|
16096
|
-
{ path: "remora/schemas/.gitkeep", content: "" },
|
|
16194
|
+
{ path: "remora/sources/s_default.json", content: defaultSource },
|
|
16195
|
+
{ path: "remora/consumers/c_default.json", content: defaultConsumer },
|
|
16196
|
+
{ path: "remora/producers/p_default.json", content: defaultProducer },
|
|
16097
16197
|
{ path: "remora/project.json", content: defaultRemoraProject },
|
|
16098
|
-
{ path: "remora/README.md", content: readme }
|
|
16198
|
+
{ path: "remora/README.md", content: readme },
|
|
16199
|
+
{ path: "default_remora_data/mock_data.csv", content: defaultMockData }
|
|
16099
16200
|
];
|
|
16100
16201
|
for (let i = 0; i < directories.length; i++) {
|
|
16101
16202
|
const dir = directories[i];
|
|
@@ -16136,8 +16237,15 @@ var DatabaseEngineClass = class {
|
|
|
16136
16237
|
this.MAX_TRY_CONNECTION = 3;
|
|
16137
16238
|
this.db = () => this._db;
|
|
16138
16239
|
this.connect = async () => {
|
|
16139
|
-
|
|
16140
|
-
|
|
16240
|
+
if (!ProcessENVManager_default.getEnvVariable("MONGO_URI")) {
|
|
16241
|
+
if (Helper_default.isDev()) {
|
|
16242
|
+
this._uri = "mongodb://mongo:27017/remora";
|
|
16243
|
+
} else {
|
|
16244
|
+
this._uri = "mongodb://localhost:27017/remora";
|
|
16245
|
+
}
|
|
16246
|
+
} else {
|
|
16247
|
+
this._uri = ProcessENVManager_default.getEnvVariable("MONGO_URI");
|
|
16248
|
+
}
|
|
16141
16249
|
const errors = [];
|
|
16142
16250
|
this._client = new import_mongodb.MongoClient(this._uri);
|
|
16143
16251
|
for (let i = 0; i < this.MAX_TRY_CONNECTION; i++) {
|
|
@@ -17715,33 +17823,7 @@ var ConsumerManagerClass = class {
|
|
|
17715
17823
|
return expandedFields;
|
|
17716
17824
|
};
|
|
17717
17825
|
this.getAvailableColumns = (consumer) => {
|
|
17718
|
-
|
|
17719
|
-
const producer = Environment_default.getProducer(cProd.name);
|
|
17720
|
-
if (!producer) {
|
|
17721
|
-
const subConsumer = Environment_default.getConsumer(cProd.name);
|
|
17722
|
-
Affirm_default(subConsumer, `No producer found with name "${cProd.name}"`);
|
|
17723
|
-
return this.getAvailableColumns(subConsumer);
|
|
17724
|
-
} else {
|
|
17725
|
-
const dims = producer.dimensions.map((x) => ({
|
|
17726
|
-
consumerAlias: null,
|
|
17727
|
-
consumerKey: null,
|
|
17728
|
-
nameInProducer: x.name,
|
|
17729
|
-
aliasInProducer: x.alias,
|
|
17730
|
-
dimension: x,
|
|
17731
|
-
owner: cProd.name
|
|
17732
|
-
}));
|
|
17733
|
-
const meas = producer.measures?.map((x) => ({
|
|
17734
|
-
consumerAlias: null,
|
|
17735
|
-
consumerKey: null,
|
|
17736
|
-
nameInProducer: x.name,
|
|
17737
|
-
aliasInProducer: x.name,
|
|
17738
|
-
measure: x,
|
|
17739
|
-
owner: cProd.name
|
|
17740
|
-
})) ?? [];
|
|
17741
|
-
return [...dims, ...meas];
|
|
17742
|
-
}
|
|
17743
|
-
});
|
|
17744
|
-
return availableColumns;
|
|
17826
|
+
return ResourcesUtils_default.getAvailableColumns(consumer);
|
|
17745
17827
|
};
|
|
17746
17828
|
this.expandField = (consumer, field, availableColumns) => {
|
|
17747
17829
|
Affirm_default(consumer, "Invalid consumer");
|
|
@@ -19326,19 +19408,17 @@ var ExecutorWriter_default = ExecutorWriter;
|
|
|
19326
19408
|
var import_promises10 = require("stream/promises");
|
|
19327
19409
|
var ExecutorOrchestratorClass = class {
|
|
19328
19410
|
constructor() {
|
|
19329
|
-
this.
|
|
19330
|
-
|
|
19331
|
-
|
|
19332
|
-
|
|
19333
|
-
|
|
19334
|
-
maxOldGenerationSizeMb: Constants_default.defaults.MIN_RUNTIME_HEAP_MB
|
|
19335
|
-
}
|
|
19411
|
+
this.createPool = () => {
|
|
19412
|
+
const options = {
|
|
19413
|
+
workerThreadOpts: {
|
|
19414
|
+
resourceLimits: {
|
|
19415
|
+
maxOldGenerationSizeMb: Constants_default.defaults.MIN_RUNTIME_HEAP_MB
|
|
19336
19416
|
}
|
|
19337
|
-
}
|
|
19338
|
-
|
|
19339
|
-
|
|
19340
|
-
|
|
19341
|
-
|
|
19417
|
+
}
|
|
19418
|
+
};
|
|
19419
|
+
const workerPath = this._getWorkerPath();
|
|
19420
|
+
Logger_default.log(`Initializing worker pool from ${workerPath} (heap limit: ${Constants_default.defaults.MIN_RUNTIME_HEAP_MB}MB)`);
|
|
19421
|
+
return import_workerpool.default.pool(import_path19.default.join(workerPath, "ExecutorWorker.js"), options);
|
|
19342
19422
|
};
|
|
19343
19423
|
this.launch = async (request) => {
|
|
19344
19424
|
Affirm_default(request, "Invalid options");
|
|
@@ -19349,9 +19429,9 @@ var ExecutorOrchestratorClass = class {
|
|
|
19349
19429
|
const _progress = new ExecutorProgress_default(logProgress);
|
|
19350
19430
|
const { usageId } = UsageManager_default.startUsage(consumer, details);
|
|
19351
19431
|
const scope = { id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], limitFileSize: consumer.MaximumFileSize };
|
|
19432
|
+
const pool = this.createPool();
|
|
19352
19433
|
try {
|
|
19353
19434
|
const start = performance.now();
|
|
19354
|
-
this.init();
|
|
19355
19435
|
const executorResults = [];
|
|
19356
19436
|
Logger_default.log(`[${usageId}] Launching consumer "${consumer.name}" (invoked by: ${details.invokedBy}, user: ${details.user?.name ?? "unknown"}, producer(s): ${consumer.producers.length})`);
|
|
19357
19437
|
let counter = performance.now();
|
|
@@ -19397,16 +19477,16 @@ var ExecutorOrchestratorClass = class {
|
|
|
19397
19477
|
_progress.register((currentWorkerIndex + 1).toString(), prod.name, fileIndex, totalFiles);
|
|
19398
19478
|
scope.workersId.push(workerId);
|
|
19399
19479
|
Logger_default.log(`[${usageId}] Spawning worker ${workerId} for producer "${prod.name}" \u2014 chunk ${chunk.start}-${chunk.end} (${Math.round((chunk.end - chunk.start) / 1024)}KB)`);
|
|
19400
|
-
workerThreads.push(
|
|
19480
|
+
workerThreads.push(pool.exec("executor", [workerData], {
|
|
19401
19481
|
on: (payload) => this.onWorkAdvanced(payload, currentWorkerIndex, _progress)
|
|
19402
19482
|
}));
|
|
19403
19483
|
}
|
|
19404
19484
|
Logger_default.log(`[${usageId}] Waiting for ${workerThreads.length} worker(s) to complete`);
|
|
19405
19485
|
executorResults.push(...await Promise.all(workerThreads));
|
|
19406
19486
|
Logger_default.log(`[${usageId}] All ${workerThreads.length} worker(s) finished for producer "${prod.name}" file ${fileIndex + 1}/${totalFiles}`);
|
|
19407
|
-
await this._executorPool.terminate();
|
|
19408
19487
|
}
|
|
19409
19488
|
}
|
|
19489
|
+
await pool.terminate();
|
|
19410
19490
|
_progress.complete();
|
|
19411
19491
|
if (executorResults.some((x) => !Algo_default.hasVal(x)))
|
|
19412
19492
|
throw new Error(`${executorResults.filter((x) => !Algo_default.hasVal(x)).length} worker(s) failed to produce valid results`);
|
|
@@ -19465,6 +19545,7 @@ var ExecutorOrchestratorClass = class {
|
|
|
19465
19545
|
} catch (error) {
|
|
19466
19546
|
Logger_default.log(`[${usageId}] Consumer "${consumer.name}" failed: ${Helper_default.asError(error).message}`);
|
|
19467
19547
|
Logger_default.error(Helper_default.asError(error));
|
|
19548
|
+
await pool.terminate();
|
|
19468
19549
|
await ConsumerOnFinishManager_default.onConsumerError(consumer, usageId);
|
|
19469
19550
|
Logger_default.log(`[${usageId}] Running cleanup after failure`);
|
|
19470
19551
|
await this.performCleanupOperations(scope, tracker);
|
|
@@ -20070,6 +20151,8 @@ var mock = async (producerName, records) => {
|
|
|
20070
20151
|
|
|
20071
20152
|
// src/index.ts
|
|
20072
20153
|
import_dotenv.default.configDotenv();
|
|
20154
|
+
if (!process.env.NODE_ENV)
|
|
20155
|
+
process.env.NODE_ENV = "production";
|
|
20073
20156
|
if (process.env.NODE_ENV !== "development" && process.env.REMORA_DEBUG_MODE === "true") {
|
|
20074
20157
|
Logger_default.enableFileLogging("./remora/logs");
|
|
20075
20158
|
console.log(`Enabled file logger.`);
|
package/package.json
CHANGED
|
@@ -13222,6 +13222,8 @@ var FileLogServiceClass = class {
|
|
|
13222
13222
|
constructor() {
|
|
13223
13223
|
this._enabled = false;
|
|
13224
13224
|
this.enable = (folder = "logs", file = "remora.log") => {
|
|
13225
|
+
this._folder = folder;
|
|
13226
|
+
this._file = file;
|
|
13225
13227
|
this._logger = import_winston.default.createLogger({
|
|
13226
13228
|
level: "debug",
|
|
13227
13229
|
format: import_winston.default.format.combine(
|
|
@@ -13254,6 +13256,23 @@ ${stack}` : base;
|
|
|
13254
13256
|
});
|
|
13255
13257
|
};
|
|
13256
13258
|
this.flush = () => {
|
|
13259
|
+
if (!this._enabled || !this._logger) return Promise.resolve();
|
|
13260
|
+
return new Promise((resolve) => {
|
|
13261
|
+
let pending = this._logger.transports.length;
|
|
13262
|
+
if (pending === 0) return resolve();
|
|
13263
|
+
for (const transport of this._logger.transports) {
|
|
13264
|
+
if (typeof transport.on === "function") {
|
|
13265
|
+
transport.once("logged", () => {
|
|
13266
|
+
if (--pending === 0) resolve();
|
|
13267
|
+
});
|
|
13268
|
+
this._logger.info("");
|
|
13269
|
+
} else {
|
|
13270
|
+
if (--pending === 0) resolve();
|
|
13271
|
+
}
|
|
13272
|
+
}
|
|
13273
|
+
});
|
|
13274
|
+
};
|
|
13275
|
+
this.close = () => {
|
|
13257
13276
|
if (!this._enabled || !this._logger) return Promise.resolve();
|
|
13258
13277
|
return new Promise((resolve) => {
|
|
13259
13278
|
this._logger.on("finish", resolve);
|
|
@@ -13333,6 +13352,7 @@ var Logger = class {
|
|
|
13333
13352
|
FileLogService_default.write("INFO", String(message));
|
|
13334
13353
|
};
|
|
13335
13354
|
this.flush = () => FileLogService_default.flush();
|
|
13355
|
+
this.close = () => FileLogService_default.close();
|
|
13336
13356
|
this.error = (error) => {
|
|
13337
13357
|
let message;
|
|
13338
13358
|
let stack;
|
|
@@ -13474,7 +13494,7 @@ var import_promises = __toESM(require("fs/promises"), 1);
|
|
|
13474
13494
|
|
|
13475
13495
|
// ../../packages/constants/src/Constants.ts
|
|
13476
13496
|
var CONSTANTS = {
|
|
13477
|
-
cliVersion: "1.1.
|
|
13497
|
+
cliVersion: "1.1.13",
|
|
13478
13498
|
backendVersion: 1,
|
|
13479
13499
|
backendPort: 5088,
|
|
13480
13500
|
workerVersion: 2,
|
|
@@ -13646,6 +13666,42 @@ var SchemaValidatorClass = class {
|
|
|
13646
13666
|
var SchemaValidator = new SchemaValidatorClass();
|
|
13647
13667
|
var SchemaValidator_default = SchemaValidator;
|
|
13648
13668
|
|
|
13669
|
+
// ../../packages/common/src/ResourcesUtils.ts
|
|
13670
|
+
var ResourcesUtilsClass = class {
|
|
13671
|
+
constructor() {
|
|
13672
|
+
this.getAvailableColumns = (consumer) => {
|
|
13673
|
+
return consumer.producers.flatMap((cProd) => {
|
|
13674
|
+
const producer = Environment_default.getProducer(cProd.name);
|
|
13675
|
+
if (!producer) {
|
|
13676
|
+
const subConsumer = Environment_default.getConsumer(cProd.name);
|
|
13677
|
+
Affirm_default(subConsumer, `No producer found with name "${cProd.name}"`);
|
|
13678
|
+
return this.getAvailableColumns(subConsumer);
|
|
13679
|
+
} else {
|
|
13680
|
+
const dims = producer.dimensions.map((x) => ({
|
|
13681
|
+
consumerAlias: null,
|
|
13682
|
+
consumerKey: null,
|
|
13683
|
+
nameInProducer: x.name,
|
|
13684
|
+
aliasInProducer: x.alias,
|
|
13685
|
+
dimension: x,
|
|
13686
|
+
owner: cProd.name
|
|
13687
|
+
}));
|
|
13688
|
+
const meas = producer.measures?.map((x) => ({
|
|
13689
|
+
consumerAlias: null,
|
|
13690
|
+
consumerKey: null,
|
|
13691
|
+
nameInProducer: x.name,
|
|
13692
|
+
aliasInProducer: x.name,
|
|
13693
|
+
measure: x,
|
|
13694
|
+
owner: cProd.name
|
|
13695
|
+
})) ?? [];
|
|
13696
|
+
return [...dims, ...meas];
|
|
13697
|
+
}
|
|
13698
|
+
});
|
|
13699
|
+
};
|
|
13700
|
+
}
|
|
13701
|
+
};
|
|
13702
|
+
var ResourcesUtils = new ResourcesUtilsClass();
|
|
13703
|
+
var ResourcesUtils_default = ResourcesUtils;
|
|
13704
|
+
|
|
13649
13705
|
// ../../packages/common/src/validation/Validator.ts
|
|
13650
13706
|
var ValidatorClass = class {
|
|
13651
13707
|
constructor() {
|
|
@@ -13756,6 +13812,38 @@ var ValidatorClass = class {
|
|
|
13756
13812
|
const uniqNames = Algo_default.uniqBy(sources, "name");
|
|
13757
13813
|
if (uniqNames.length !== 1)
|
|
13758
13814
|
errors.push(`Producers with different sources are used in the consumer "${consumer.name}" (${uniqNames.join(", ")})`);
|
|
13815
|
+
const availableColumns = ResourcesUtils_default.getAvailableColumns(consumer);
|
|
13816
|
+
const availableFieldsByProducer = /* @__PURE__ */ new Map();
|
|
13817
|
+
for (const col of availableColumns) {
|
|
13818
|
+
const existing = availableFieldsByProducer.get(col.owner) ?? [];
|
|
13819
|
+
if (!existing.includes(col.nameInProducer))
|
|
13820
|
+
existing.push(col.nameInProducer);
|
|
13821
|
+
availableFieldsByProducer.set(col.owner, existing);
|
|
13822
|
+
}
|
|
13823
|
+
const allAvailableFields = [...availableFieldsByProducer.values()].flat();
|
|
13824
|
+
for (const field of consumer.fields) {
|
|
13825
|
+
if (field.key === "*" || field.fixed || field.copyFrom) continue;
|
|
13826
|
+
if (field.from) {
|
|
13827
|
+
const producerFields = availableFieldsByProducer.get(field.from);
|
|
13828
|
+
if (producerFields && !producerFields.includes(field.key))
|
|
13829
|
+
errors.push(`Field "${field.key}" (from: "${field.from}") is not found in producer "${field.from}" in consumer "${consumer.name}". Available fields: ${producerFields.join(", ")}`);
|
|
13830
|
+
} else if (consumer.producers.length === 1) {
|
|
13831
|
+
const producerFields = availableFieldsByProducer.get(consumer.producers[0].name);
|
|
13832
|
+
if (producerFields && !producerFields.includes(field.key))
|
|
13833
|
+
errors.push(`Field "${field.key}" is not found in producer "${consumer.producers[0].name}" in consumer "${consumer.name}". Available fields: ${producerFields.join(", ")}`);
|
|
13834
|
+
} else {
|
|
13835
|
+
if (allAvailableFields.length > 0 && !allAvailableFields.includes(field.key))
|
|
13836
|
+
errors.push(`Field "${field.key}" is not found in any of the producers of consumer "${consumer.name}".`);
|
|
13837
|
+
}
|
|
13838
|
+
}
|
|
13839
|
+
for (let i = 0; i < consumer.fields.length; i++) {
|
|
13840
|
+
const field = consumer.fields[i];
|
|
13841
|
+
if (!field.copyFrom) continue;
|
|
13842
|
+
const precedingFields = consumer.fields.slice(0, i);
|
|
13843
|
+
const found = precedingFields.find((f) => (f.alias ?? f.key) === field.copyFrom);
|
|
13844
|
+
if (!found)
|
|
13845
|
+
errors.push(`Field "${field.alias ?? field.key}" uses copyFrom "${field.copyFrom}" but no field with that name/alias exists before it in consumer "${consumer.name}".`);
|
|
13846
|
+
}
|
|
13759
13847
|
if (consumer.filters && consumer.filters.length > 0) {
|
|
13760
13848
|
if (consumer.filters.some((x) => x.sql && x.rule))
|
|
13761
13849
|
errors.push(`A single consumer can't have both filters based on SQL and filters based on rules.`);
|
|
@@ -13828,6 +13916,20 @@ var ValidatorClass = class {
|
|
|
13828
13916
|
if (consumer.options) {
|
|
13829
13917
|
if (Algo_default.hasVal(consumer.options.distinct) && Algo_default.hasVal(consumer.options.distinctOn))
|
|
13830
13918
|
errors.push(`Can't specify a "distinct" and a "distinctOn" clause on the same consumer (${consumer.name}); use one or the other.`);
|
|
13919
|
+
if (Algo_default.hasVal(consumer.options.distinctOn)) {
|
|
13920
|
+
const { distinctOn } = consumer.options;
|
|
13921
|
+
const hasWildcard = consumer.fields.some((x) => x.key === "*");
|
|
13922
|
+
const consumerFieldKeys = consumer.fields.map((x) => x.alias ?? x.key);
|
|
13923
|
+
if (!hasWildcard) {
|
|
13924
|
+
const missingKeys = distinctOn.keys.filter((k) => !consumerFieldKeys.includes(k));
|
|
13925
|
+
if (missingKeys.length > 0)
|
|
13926
|
+
errors.push(`distinctOn references key(s) "${missingKeys.join(", ")}" that are not present in the consumer "${consumer.name}".`);
|
|
13927
|
+
}
|
|
13928
|
+
if (distinctOn.resolution?.orderBy && !hasWildcard) {
|
|
13929
|
+
if (!consumerFieldKeys.includes(distinctOn.resolution.orderBy))
|
|
13930
|
+
errors.push(`distinctOn resolution orderBy field "${distinctOn.resolution.orderBy}" is not present in the consumer "${consumer.name}".`);
|
|
13931
|
+
}
|
|
13932
|
+
}
|
|
13831
13933
|
if (Algo_default.hasVal(consumer.options.pivot)) {
|
|
13832
13934
|
if (Algo_default.hasVal(consumer.options.distinct) || Algo_default.hasVal(consumer.options.distinctOn))
|
|
13833
13935
|
errors.push(`Can't specify "pivot" together with "distinct" or "distinctOn" on the same consumer (${consumer.name}).`);
|
|
@@ -17051,33 +17153,7 @@ var ConsumerManagerClass = class {
|
|
|
17051
17153
|
return expandedFields;
|
|
17052
17154
|
};
|
|
17053
17155
|
this.getAvailableColumns = (consumer) => {
|
|
17054
|
-
|
|
17055
|
-
const producer = Environment_default.getProducer(cProd.name);
|
|
17056
|
-
if (!producer) {
|
|
17057
|
-
const subConsumer = Environment_default.getConsumer(cProd.name);
|
|
17058
|
-
Affirm_default(subConsumer, `No producer found with name "${cProd.name}"`);
|
|
17059
|
-
return this.getAvailableColumns(subConsumer);
|
|
17060
|
-
} else {
|
|
17061
|
-
const dims = producer.dimensions.map((x) => ({
|
|
17062
|
-
consumerAlias: null,
|
|
17063
|
-
consumerKey: null,
|
|
17064
|
-
nameInProducer: x.name,
|
|
17065
|
-
aliasInProducer: x.alias,
|
|
17066
|
-
dimension: x,
|
|
17067
|
-
owner: cProd.name
|
|
17068
|
-
}));
|
|
17069
|
-
const meas = producer.measures?.map((x) => ({
|
|
17070
|
-
consumerAlias: null,
|
|
17071
|
-
consumerKey: null,
|
|
17072
|
-
nameInProducer: x.name,
|
|
17073
|
-
aliasInProducer: x.name,
|
|
17074
|
-
measure: x,
|
|
17075
|
-
owner: cProd.name
|
|
17076
|
-
})) ?? [];
|
|
17077
|
-
return [...dims, ...meas];
|
|
17078
|
-
}
|
|
17079
|
-
});
|
|
17080
|
-
return availableColumns;
|
|
17156
|
+
return ResourcesUtils_default.getAvailableColumns(consumer);
|
|
17081
17157
|
};
|
|
17082
17158
|
this.expandField = (consumer, field, availableColumns) => {
|
|
17083
17159
|
Affirm_default(consumer, "Invalid consumer");
|
|
@@ -17789,8 +17865,15 @@ var DatabaseEngineClass = class {
|
|
|
17789
17865
|
this.MAX_TRY_CONNECTION = 3;
|
|
17790
17866
|
this.db = () => this._db;
|
|
17791
17867
|
this.connect = async () => {
|
|
17792
|
-
|
|
17793
|
-
|
|
17868
|
+
if (!ProcessENVManager_default.getEnvVariable("MONGO_URI")) {
|
|
17869
|
+
if (Helper_default.isDev()) {
|
|
17870
|
+
this._uri = "mongodb://mongo:27017/remora";
|
|
17871
|
+
} else {
|
|
17872
|
+
this._uri = "mongodb://localhost:27017/remora";
|
|
17873
|
+
}
|
|
17874
|
+
} else {
|
|
17875
|
+
this._uri = ProcessENVManager_default.getEnvVariable("MONGO_URI");
|
|
17876
|
+
}
|
|
17794
17877
|
const errors = [];
|
|
17795
17878
|
this._client = new import_mongodb.MongoClient(this._uri);
|
|
17796
17879
|
for (let i = 0; i < this.MAX_TRY_CONNECTION; i++) {
|
|
@@ -19084,19 +19167,17 @@ var ExecutorWriter_default = ExecutorWriter;
|
|
|
19084
19167
|
var import_promises10 = require("stream/promises");
|
|
19085
19168
|
var ExecutorOrchestratorClass = class {
|
|
19086
19169
|
constructor() {
|
|
19087
|
-
this.
|
|
19088
|
-
|
|
19089
|
-
|
|
19090
|
-
|
|
19091
|
-
|
|
19092
|
-
maxOldGenerationSizeMb: Constants_default.defaults.MIN_RUNTIME_HEAP_MB
|
|
19093
|
-
}
|
|
19170
|
+
this.createPool = () => {
|
|
19171
|
+
const options = {
|
|
19172
|
+
workerThreadOpts: {
|
|
19173
|
+
resourceLimits: {
|
|
19174
|
+
maxOldGenerationSizeMb: Constants_default.defaults.MIN_RUNTIME_HEAP_MB
|
|
19094
19175
|
}
|
|
19095
|
-
}
|
|
19096
|
-
|
|
19097
|
-
|
|
19098
|
-
|
|
19099
|
-
|
|
19176
|
+
}
|
|
19177
|
+
};
|
|
19178
|
+
const workerPath = this._getWorkerPath();
|
|
19179
|
+
Logger_default.log(`Initializing worker pool from ${workerPath} (heap limit: ${Constants_default.defaults.MIN_RUNTIME_HEAP_MB}MB)`);
|
|
19180
|
+
return import_workerpool.default.pool(import_path16.default.join(workerPath, "ExecutorWorker.js"), options);
|
|
19100
19181
|
};
|
|
19101
19182
|
this.launch = async (request) => {
|
|
19102
19183
|
Affirm_default(request, "Invalid options");
|
|
@@ -19107,9 +19188,9 @@ var ExecutorOrchestratorClass = class {
|
|
|
19107
19188
|
const _progress = new ExecutorProgress_default(logProgress);
|
|
19108
19189
|
const { usageId } = UsageManager_default.startUsage(consumer, details);
|
|
19109
19190
|
const scope = { id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], limitFileSize: consumer.MaximumFileSize };
|
|
19191
|
+
const pool = this.createPool();
|
|
19110
19192
|
try {
|
|
19111
19193
|
const start = performance.now();
|
|
19112
|
-
this.init();
|
|
19113
19194
|
const executorResults = [];
|
|
19114
19195
|
Logger_default.log(`[${usageId}] Launching consumer "${consumer.name}" (invoked by: ${details.invokedBy}, user: ${details.user?.name ?? "unknown"}, producer(s): ${consumer.producers.length})`);
|
|
19115
19196
|
let counter = performance.now();
|
|
@@ -19155,16 +19236,16 @@ var ExecutorOrchestratorClass = class {
|
|
|
19155
19236
|
_progress.register((currentWorkerIndex + 1).toString(), prod.name, fileIndex, totalFiles);
|
|
19156
19237
|
scope.workersId.push(workerId);
|
|
19157
19238
|
Logger_default.log(`[${usageId}] Spawning worker ${workerId} for producer "${prod.name}" \u2014 chunk ${chunk.start}-${chunk.end} (${Math.round((chunk.end - chunk.start) / 1024)}KB)`);
|
|
19158
|
-
workerThreads.push(
|
|
19239
|
+
workerThreads.push(pool.exec("executor", [workerData], {
|
|
19159
19240
|
on: (payload) => this.onWorkAdvanced(payload, currentWorkerIndex, _progress)
|
|
19160
19241
|
}));
|
|
19161
19242
|
}
|
|
19162
19243
|
Logger_default.log(`[${usageId}] Waiting for ${workerThreads.length} worker(s) to complete`);
|
|
19163
19244
|
executorResults.push(...await Promise.all(workerThreads));
|
|
19164
19245
|
Logger_default.log(`[${usageId}] All ${workerThreads.length} worker(s) finished for producer "${prod.name}" file ${fileIndex + 1}/${totalFiles}`);
|
|
19165
|
-
await this._executorPool.terminate();
|
|
19166
19246
|
}
|
|
19167
19247
|
}
|
|
19248
|
+
await pool.terminate();
|
|
19168
19249
|
_progress.complete();
|
|
19169
19250
|
if (executorResults.some((x) => !Algo_default.hasVal(x)))
|
|
19170
19251
|
throw new Error(`${executorResults.filter((x) => !Algo_default.hasVal(x)).length} worker(s) failed to produce valid results`);
|
|
@@ -19223,6 +19304,7 @@ var ExecutorOrchestratorClass = class {
|
|
|
19223
19304
|
} catch (error) {
|
|
19224
19305
|
Logger_default.log(`[${usageId}] Consumer "${consumer.name}" failed: ${Helper_default.asError(error).message}`);
|
|
19225
19306
|
Logger_default.error(Helper_default.asError(error));
|
|
19307
|
+
await pool.terminate();
|
|
19226
19308
|
await ConsumerOnFinishManager_default.onConsumerError(consumer, usageId);
|
|
19227
19309
|
Logger_default.log(`[${usageId}] Running cleanup after failure`);
|
|
19228
19310
|
await this.performCleanupOperations(scope, tracker);
|