@twin.org/dataspace-data-plane-service 0.0.3-next.24 → 0.0.3-next.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/dataspaceDataPlaneRoutes.js +17 -10
- package/dist/es/dataspaceDataPlaneRoutes.js.map +1 -1
- package/dist/es/dataspaceDataPlaneService.js +322 -196
- package/dist/es/dataspaceDataPlaneService.js.map +1 -1
- package/dist/es/entities/activityLogDetails.js +0 -8
- package/dist/es/entities/activityLogDetails.js.map +1 -1
- package/dist/es/entities/activityTask.js.map +1 -1
- package/dist/es/models/IDataspaceDataPlaneServiceConfig.js.map +1 -1
- package/dist/es/models/IDataspaceDataPlaneServiceConstructorOptions.js.map +1 -1
- package/dist/types/dataspaceDataPlaneRoutes.d.ts +3 -3
- package/dist/types/dataspaceDataPlaneService.d.ts +2 -2
- package/dist/types/entities/activityLogDetails.d.ts +0 -4
- package/dist/types/entities/activityTask.d.ts +2 -2
- package/dist/types/models/IDataspaceDataPlaneServiceConfig.d.ts +5 -0
- package/dist/types/models/IDataspaceDataPlaneServiceConstructorOptions.d.ts +5 -0
- package/docs/changelog.md +34 -0
- package/docs/open-api/spec.json +77 -109
- package/docs/reference/classes/ActivityLogDetails.md +0 -8
- package/docs/reference/classes/ActivityTask.md +1 -1
- package/docs/reference/classes/DataspaceDataPlaneService.md +3 -3
- package/docs/reference/functions/activityStreamNotify.md +2 -2
- package/docs/reference/interfaces/IDataspaceDataPlaneServiceConfig.md +14 -0
- package/docs/reference/interfaces/IDataspaceDataPlaneServiceConstructorOptions.md +14 -0
- package/locales/en.json +4 -2
- package/package.json +3 -3
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
// Copyright 2025 IOTA Stiftung.
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0.
|
|
3
1
|
import { TaskStatus } from "@twin.org/background-task-models";
|
|
4
2
|
import { ContextIdKeys, ContextIdStore } from "@twin.org/context";
|
|
5
|
-
import { ArrayHelper, ComponentFactory, ConflictError, Converter, GeneralError, GuardError, Guards, Is, JsonHelper, NotFoundError, RandomHelper, UnprocessableError, Validation } from "@twin.org/core";
|
|
3
|
+
import { ArrayHelper, BaseError, ComponentFactory, ConflictError, Converter, GeneralError, GuardError, Guards, Is, JsonHelper, NotFoundError, RandomHelper, UnprocessableError, Validation } from "@twin.org/core";
|
|
6
4
|
import { Blake2b } from "@twin.org/crypto";
|
|
7
5
|
import { DataTypeHelper, JsonSchemaHelper } from "@twin.org/data-core";
|
|
8
6
|
import { JsonLdDataTypes, JsonLdHelper, JsonLdProcessor } from "@twin.org/data-json-ld";
|
|
9
|
-
import { ActivityProcessingStatus, DataRequestType, DataspaceAppFactory, DataspaceContexts, DataspaceDataTypes, DataspaceTypes } from "@twin.org/dataspace-models";
|
|
7
|
+
import { ActivityProcessingStatus, ActivityTaskStatus, DataRequestType, DataspaceAppFactory, DataspaceContexts, DataspaceDataTypes, DataspaceTypes } from "@twin.org/dataspace-models";
|
|
10
8
|
import { EngineCoreFactory } from "@twin.org/engine-models";
|
|
11
9
|
import { ComparisonOperator, LogicalOperator } from "@twin.org/entity";
|
|
12
10
|
import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
|
|
@@ -32,11 +30,6 @@ export class DataspaceDataPlaneService {
|
|
|
32
30
|
* @internal
|
|
33
31
|
*/
|
|
34
32
|
static _MINUTES_PER_DAY = 24 * 60;
|
|
35
|
-
/**
|
|
36
|
-
* Milliseconds per day (24 hours).
|
|
37
|
-
* @internal
|
|
38
|
-
*/
|
|
39
|
-
static _MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
40
33
|
/**
|
|
41
34
|
* The default cleanup interval in minutes. (1 hour)
|
|
42
35
|
* @internal
|
|
@@ -77,6 +70,11 @@ export class DataspaceDataPlaneService {
|
|
|
77
70
|
* @internal
|
|
78
71
|
*/
|
|
79
72
|
_activityLogStatusCallbacks;
|
|
73
|
+
/**
|
|
74
|
+
* Track task handler registrations to avoid resetting worker pools on every task.
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
_registeredTaskTypes;
|
|
80
78
|
/**
|
|
81
79
|
* Task retention. -1 retain forever.
|
|
82
80
|
* @internal
|
|
@@ -87,6 +85,11 @@ export class DataspaceDataPlaneService {
|
|
|
87
85
|
* @internal
|
|
88
86
|
*/
|
|
89
87
|
_retainActivityLogsFor;
|
|
88
|
+
/**
|
|
89
|
+
* Retry count for failed tasks.
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
_retryCount;
|
|
90
93
|
/**
|
|
91
94
|
* Clean up interval for activity logs.
|
|
92
95
|
* @internal
|
|
@@ -118,16 +121,16 @@ export class DataspaceDataPlaneService {
|
|
|
118
121
|
*/
|
|
119
122
|
_policyEnforcementPoint;
|
|
120
123
|
/**
|
|
121
|
-
*
|
|
122
|
-
* Used to read transfer state from shared storage (written by Control Plane).
|
|
124
|
+
* The tenant admin component.
|
|
123
125
|
* @internal
|
|
124
126
|
*/
|
|
125
|
-
|
|
127
|
+
_tenantAdmin;
|
|
126
128
|
/**
|
|
127
|
-
*
|
|
129
|
+
* Entity storage for Transfer Process entities.
|
|
130
|
+
* Used to read transfer state from shared storage (written by Control Plane).
|
|
128
131
|
* @internal
|
|
129
132
|
*/
|
|
130
|
-
|
|
133
|
+
_transferProcessStorage;
|
|
131
134
|
/**
|
|
132
135
|
* Create a new instance of DataspaceDataPlane.
|
|
133
136
|
* @param options The options for the data plane.
|
|
@@ -141,6 +144,7 @@ export class DataspaceDataPlaneService {
|
|
|
141
144
|
this._taskScheduler = ComponentFactory.get(options?.taskSchedulerComponentType ?? "task-scheduler");
|
|
142
145
|
this._trustComponent = ComponentFactory.get(options?.trustComponentType ?? "trust");
|
|
143
146
|
this._policyEnforcementPoint = ComponentFactory.getIfExists(options?.pepComponentType ?? "policy-enforcement-point-service");
|
|
147
|
+
this._tenantAdmin = ComponentFactory.getIfExists(options?.tenantAdminType ?? "tenant-admin");
|
|
144
148
|
// Entity storage for Transfer Process state lookup
|
|
145
149
|
// Used to read transfer state from shared storage (written by Control Plane)
|
|
146
150
|
this._transferProcessStorage = EntityStorageConnectorFactory.get(options?.transferProcessEntityStorageType ?? "transfer-process");
|
|
@@ -150,12 +154,13 @@ export class DataspaceDataPlaneService {
|
|
|
150
154
|
DataspaceProtocolDataTypes.registerRedirects();
|
|
151
155
|
DataspaceProtocolDataTypes.registerTypes();
|
|
152
156
|
this._activityLogStatusCallbacks = {};
|
|
153
|
-
this.
|
|
157
|
+
this._registeredTaskTypes = [];
|
|
154
158
|
this._partitionContextIds = options?.partitionContextIds;
|
|
155
159
|
this._retainTasksFor =
|
|
156
160
|
DataspaceDataPlaneService._DEFAULT_RETAIN_INTERVAL * DataspaceDataPlaneService._MS_PER_MINUTE;
|
|
157
161
|
this._retainActivityLogsFor =
|
|
158
162
|
DataspaceDataPlaneService._DEFAULT_RETAIN_INTERVAL * DataspaceDataPlaneService._MS_PER_MINUTE;
|
|
163
|
+
this._retryCount = options?.config?.retryCount;
|
|
159
164
|
this._activityLogCleanUpInterval = DataspaceDataPlaneService._DEFAULT_CLEANUP_INTERVAL;
|
|
160
165
|
this._cleanUpProcessOngoing = false;
|
|
161
166
|
const validationErrors = [];
|
|
@@ -232,11 +237,10 @@ export class DataspaceDataPlaneService {
|
|
|
232
237
|
/**
|
|
233
238
|
* Notify an Activity.
|
|
234
239
|
* @param activity The Activity notified.
|
|
235
|
-
* @returns The
|
|
240
|
+
* @returns The activity's id or entry.
|
|
236
241
|
*/
|
|
237
242
|
async notifyActivity(activity) {
|
|
238
243
|
Guards.object(DataspaceDataPlaneService.CLASS_NAME, "activity", activity);
|
|
239
|
-
await this.updateActiveTenants();
|
|
240
244
|
await this._logging?.log({
|
|
241
245
|
level: "debug",
|
|
242
246
|
source: DataspaceDataPlaneService.CLASS_NAME,
|
|
@@ -263,10 +267,11 @@ export class DataspaceDataPlaneService {
|
|
|
263
267
|
const activityLogId = Converter.bytesToHex(Blake2b.sum256(canonicalBytes));
|
|
264
268
|
const activityLogEntryId = `urn:x-activity-log:${activityLogId}`;
|
|
265
269
|
// Check if entry already exists
|
|
266
|
-
|
|
270
|
+
let logEntry = await this._entityStorageActivityLogs.get(activityLogEntryId);
|
|
267
271
|
let existingSuccessfulApps = [];
|
|
268
272
|
let isRetry = false;
|
|
269
|
-
|
|
273
|
+
const now = Date.now();
|
|
274
|
+
if (!Is.undefined(logEntry)) {
|
|
270
275
|
// Check if there are failed tasks that can be retried
|
|
271
276
|
const existingEntry = await this.getActivityLogEntry(activityLogEntryId);
|
|
272
277
|
// If all tasks completed successfully, this is a duplicate
|
|
@@ -284,69 +289,46 @@ export class DataspaceDataPlaneService {
|
|
|
284
289
|
isRetry = true;
|
|
285
290
|
}
|
|
286
291
|
else {
|
|
287
|
-
|
|
292
|
+
logEntry = {
|
|
288
293
|
id: activityLogEntryId,
|
|
289
|
-
activityId:
|
|
294
|
+
activityId: activity.id,
|
|
290
295
|
generator: this.calculateActivityGeneratorIdentity(activity),
|
|
291
|
-
dateCreated: new Date().toISOString(),
|
|
292
|
-
dateModified: new Date().toISOString()
|
|
296
|
+
dateCreated: new Date(now).toISOString(),
|
|
297
|
+
dateModified: new Date(now).toISOString()
|
|
293
298
|
};
|
|
294
299
|
await this._entityStorageActivityLogs.set(logEntry);
|
|
295
300
|
}
|
|
296
301
|
const activityQuerySet = await this.calculateActivityQuerySet(activity);
|
|
297
|
-
const
|
|
298
|
-
const
|
|
302
|
+
const taskEntries = [];
|
|
303
|
+
const handlerApps = {};
|
|
299
304
|
for (const query of activityQuerySet) {
|
|
300
|
-
const
|
|
301
|
-
for (const appId
|
|
302
|
-
|
|
303
|
-
|
|
305
|
+
const apps = this.getAppForActivityQuery(query);
|
|
306
|
+
for (const appId in apps) {
|
|
307
|
+
// Only process apps that haven't already completed successfully
|
|
308
|
+
if (!handlerApps[appId] && !existingSuccessfulApps.includes(appId)) {
|
|
309
|
+
handlerApps[appId] = apps[appId];
|
|
304
310
|
}
|
|
305
311
|
}
|
|
306
312
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
activityLogEntryId,
|
|
312
|
-
activity: activity,
|
|
313
|
-
executorApp: dataspaceAppId
|
|
314
|
-
};
|
|
315
|
-
const taskType = Converter.bytesToHex(RandomHelper.generate(16));
|
|
316
|
-
const taskId = await this._backgroundTaskComponent.create(taskType, payload, {
|
|
317
|
-
retainFor: this._retainTasksFor
|
|
318
|
-
});
|
|
319
|
-
await this._backgroundTaskComponent.registerHandler(taskType, "@twin.org/dataspace-app-runner", "appRunner", async (task) => {
|
|
320
|
-
await this.finaliseTask(task);
|
|
321
|
-
}, {
|
|
322
|
-
initialiseMethod: "appRunnerStart",
|
|
323
|
-
shutdownMethod: "appRunnerEnd"
|
|
324
|
-
});
|
|
325
|
-
tasksScheduled.push({
|
|
326
|
-
taskId,
|
|
327
|
-
dataspaceAppId
|
|
328
|
-
});
|
|
329
|
-
await this._logging?.log({
|
|
330
|
-
level: "info",
|
|
331
|
-
source: DataspaceDataPlaneService.CLASS_NAME,
|
|
332
|
-
message: "scheduledTask",
|
|
333
|
-
data: {
|
|
334
|
-
taskId,
|
|
335
|
-
dataspaceAppId,
|
|
336
|
-
isRetry
|
|
337
|
-
}
|
|
338
|
-
});
|
|
313
|
+
let inlineCount = 0;
|
|
314
|
+
for (const handlerAppId in handlerApps) {
|
|
315
|
+
if (await this.processTask(activityLogEntryId, activity, handlerApps, handlerAppId, taskEntries, isRetry)) {
|
|
316
|
+
inlineCount++;
|
|
339
317
|
}
|
|
340
318
|
}
|
|
341
319
|
const existingActivityTasks = isRetry
|
|
342
320
|
? await this._entityStorageActivityTasks.get(activityLogEntryId)
|
|
343
321
|
: undefined;
|
|
344
322
|
const existingTasksToKeep = existingActivityTasks?.associatedTasks.filter(t => existingSuccessfulApps.includes(t.dataspaceAppId)) ?? [];
|
|
345
|
-
|
|
323
|
+
const activityTask = {
|
|
346
324
|
activityLogEntryId,
|
|
347
|
-
associatedTasks: [...existingTasksToKeep, ...
|
|
348
|
-
}
|
|
349
|
-
|
|
325
|
+
associatedTasks: [...existingTasksToKeep, ...taskEntries]
|
|
326
|
+
};
|
|
327
|
+
await this._entityStorageActivityTasks.set(activityTask);
|
|
328
|
+
if (inlineCount === taskEntries.length) {
|
|
329
|
+
return this.finaliseActivityLogEntry(activityLogEntryId);
|
|
330
|
+
}
|
|
331
|
+
return activityTask.activityLogEntryId;
|
|
350
332
|
}
|
|
351
333
|
/**
|
|
352
334
|
* Subscribes to the activity log.
|
|
@@ -378,68 +360,12 @@ export class DataspaceDataPlaneService {
|
|
|
378
360
|
*/
|
|
379
361
|
async getActivityLogEntry(logEntryId) {
|
|
380
362
|
Guards.stringValue(DataspaceDataPlaneService.CLASS_NAME, "logEntryId", logEntryId);
|
|
381
|
-
const
|
|
382
|
-
if (Is.undefined(
|
|
363
|
+
const activityLog = await this._entityStorageActivityLogs.get(logEntryId);
|
|
364
|
+
if (Is.undefined(activityLog)) {
|
|
383
365
|
throw new NotFoundError(DataspaceDataPlaneService.CLASS_NAME, "activityLogEntryNotFound", logEntryId);
|
|
384
366
|
}
|
|
385
|
-
let pendingTasks;
|
|
386
|
-
let runningTasks;
|
|
387
|
-
let finalizedTasks;
|
|
388
|
-
let inErrorTasks;
|
|
389
|
-
// For calculating the processing status. `Registering` if we cannot determine the activity tasks yet
|
|
390
|
-
let status = ActivityProcessingStatus.Registering;
|
|
391
|
-
// Now query the associated tasks
|
|
392
367
|
const activityTasks = await this._entityStorageActivityTasks.get(logEntryId);
|
|
393
|
-
|
|
394
|
-
if (!Is.undefined(activityTasks)) {
|
|
395
|
-
pendingTasks = [];
|
|
396
|
-
runningTasks = [];
|
|
397
|
-
finalizedTasks = [];
|
|
398
|
-
inErrorTasks = [];
|
|
399
|
-
for (const entity of activityTasks.associatedTasks) {
|
|
400
|
-
const taskDetails = await this._backgroundTaskComponent.get(entity.taskId);
|
|
401
|
-
if (Is.object(taskDetails)) {
|
|
402
|
-
switch (taskDetails.status) {
|
|
403
|
-
case TaskStatus.Success:
|
|
404
|
-
finalizedTasks.push({
|
|
405
|
-
...entity,
|
|
406
|
-
result: JSON.stringify(taskDetails.result),
|
|
407
|
-
startDate: taskDetails?.dateCreated,
|
|
408
|
-
endDate: taskDetails?.dateCompleted
|
|
409
|
-
});
|
|
410
|
-
break;
|
|
411
|
-
case TaskStatus.Pending:
|
|
412
|
-
pendingTasks.push(entity);
|
|
413
|
-
break;
|
|
414
|
-
case TaskStatus.Processing:
|
|
415
|
-
runningTasks.push({ ...entity, startDate: taskDetails.dateCreated });
|
|
416
|
-
break;
|
|
417
|
-
case TaskStatus.Failed:
|
|
418
|
-
inErrorTasks.push({
|
|
419
|
-
...entity,
|
|
420
|
-
error: taskDetails.error
|
|
421
|
-
});
|
|
422
|
-
break;
|
|
423
|
-
case TaskStatus.Cancelled:
|
|
424
|
-
// Nothing to do for cancelled tasks
|
|
425
|
-
break;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
if (Is.arrayValue(inErrorTasks)) {
|
|
430
|
-
status = ActivityProcessingStatus.Error;
|
|
431
|
-
}
|
|
432
|
-
else if (Is.arrayValue(runningTasks)) {
|
|
433
|
-
status = ActivityProcessingStatus.Running;
|
|
434
|
-
}
|
|
435
|
-
else if (Is.arrayValue(pendingTasks)) {
|
|
436
|
-
status = ActivityProcessingStatus.Pending;
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
status = ActivityProcessingStatus.Completed;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return { ...result, status, pendingTasks, runningTasks, finalizedTasks, inErrorTasks };
|
|
368
|
+
return this.constructLogEntry(activityLog, activityTasks);
|
|
443
369
|
}
|
|
444
370
|
/**
|
|
445
371
|
* Get Data Asset entities. Allows to retrieve entities by their type or id.
|
|
@@ -479,9 +405,7 @@ export class DataspaceDataPlaneService {
|
|
|
479
405
|
}
|
|
480
406
|
const datasetId = serviceDataset["@id"];
|
|
481
407
|
Guards.stringValue(DataspaceDataPlaneService.CLASS_NAME, "datasetId", datasetId);
|
|
482
|
-
const
|
|
483
|
-
// getAppForDataAssetQuery already validates app exists
|
|
484
|
-
const app = DataspaceAppFactory.get(appId);
|
|
408
|
+
const app = await this.getAppForDataAssetQuery({ datasetId });
|
|
485
409
|
const handleDataRequest = app.handleDataRequest?.bind(app);
|
|
486
410
|
Guards.function(DataspaceDataPlaneService.CLASS_NAME, "handleDataRequest", handleDataRequest);
|
|
487
411
|
const dataRequest = {
|
|
@@ -545,8 +469,7 @@ export class DataspaceDataPlaneService {
|
|
|
545
469
|
const serviceDataset = await this.getDatasetFromApps(resolvedDatasetId);
|
|
546
470
|
const datasetId = serviceDataset["@id"];
|
|
547
471
|
Guards.stringValue(DataspaceDataPlaneService.CLASS_NAME, "datasetId", datasetId);
|
|
548
|
-
const
|
|
549
|
-
const app = DataspaceAppFactory.get(appId);
|
|
472
|
+
const app = await this.getAppForDataAssetQuery({ datasetId });
|
|
550
473
|
if (!app.supportedQueryTypes().includes(query.type)) {
|
|
551
474
|
throw new UnprocessableError(DataspaceDataPlaneService.CLASS_NAME, "queryTypeNotSupported", {
|
|
552
475
|
queryType: query.type
|
|
@@ -651,66 +574,65 @@ export class DataspaceDataPlaneService {
|
|
|
651
574
|
}
|
|
652
575
|
/**
|
|
653
576
|
* Process activity task finalization.
|
|
654
|
-
* @param
|
|
577
|
+
* @param taskId The Id of the Activity Log Entry.
|
|
578
|
+
* @param status The final status of the task.
|
|
579
|
+
* @param payload The execution payload of the task, required to correlate to the Activity Log Entry and update the processing status.
|
|
655
580
|
* @internal
|
|
656
581
|
*/
|
|
657
|
-
async
|
|
658
|
-
const payload = task.payload;
|
|
582
|
+
async finaliseBackgroundTask(taskId, status, payload) {
|
|
659
583
|
if (Is.empty(payload)) {
|
|
660
584
|
return;
|
|
661
585
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
await this.
|
|
665
|
-
level: "error",
|
|
666
|
-
source: DataspaceDataPlaneService.CLASS_NAME,
|
|
667
|
-
message: "unknownActivityLogEntryId",
|
|
668
|
-
data: {
|
|
669
|
-
activityLogEntryId: payload.activityLogEntryId
|
|
670
|
-
}
|
|
671
|
-
});
|
|
586
|
+
if (status === TaskStatus.Success || status === TaskStatus.Failed) {
|
|
587
|
+
await this.notifyTaskStatusChanged(payload.activityLogEntryId, payload.activity.id, payload.dataspaceAppId, taskId, status);
|
|
588
|
+
await this.finaliseActivityLogEntry(payload.activityLogEntryId);
|
|
672
589
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
id: entry.id,
|
|
693
|
-
activityId: entry.activityId,
|
|
694
|
-
generator: entry.generator,
|
|
695
|
-
dateCreated: entry.dateCreated,
|
|
696
|
-
dateModified: entry.dateModified,
|
|
697
|
-
retainUntil,
|
|
698
|
-
retryCount: entry.retryCount
|
|
699
|
-
});
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Notify registered callbacks about a task status change.
|
|
593
|
+
* @param activityLogEntryId The Id of the Activity Log Entry.
|
|
594
|
+
* @param activityId The Id of the Activity.
|
|
595
|
+
* @param dataspaceAppId The Id of the Dataspace App associated with the task.
|
|
596
|
+
* @param taskId The Id of the task.
|
|
597
|
+
* @param taskStatus The new status of the task.
|
|
598
|
+
* @internal
|
|
599
|
+
*/
|
|
600
|
+
async notifyTaskStatusChanged(activityLogEntryId, activityId, dataspaceAppId, taskId, taskStatus) {
|
|
601
|
+
for (const callback of Object.values(this._activityLogStatusCallbacks)) {
|
|
602
|
+
await callback({
|
|
603
|
+
activityLogEntryId,
|
|
604
|
+
activityId,
|
|
605
|
+
taskProcessingStatus: {
|
|
606
|
+
dataspaceAppId,
|
|
607
|
+
taskId,
|
|
608
|
+
taskStatus
|
|
700
609
|
}
|
|
701
|
-
}
|
|
610
|
+
});
|
|
702
611
|
}
|
|
703
612
|
}
|
|
704
613
|
/**
|
|
705
|
-
*
|
|
614
|
+
* Finalizes the Activity Log Entry by checking if all associated tasks have completed and, if so, updating the entry to be retained for the configured retention period.
|
|
615
|
+
* @param activityLogEntryId The Id of the Activity Log Entry to finalize.
|
|
616
|
+
* @returns The Activity Log Entry with updated retention details if applicable.
|
|
706
617
|
* @internal
|
|
707
618
|
*/
|
|
708
|
-
async
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
619
|
+
async finaliseActivityLogEntry(activityLogEntryId) {
|
|
620
|
+
const entry = await this.getActivityLogEntry(activityLogEntryId);
|
|
621
|
+
if (this._retainActivityLogsFor !== -1 &&
|
|
622
|
+
(entry.status === ActivityProcessingStatus.Completed ||
|
|
623
|
+
entry.status === ActivityProcessingStatus.Error)) {
|
|
624
|
+
const retainUntil = Date.now() + this._retainActivityLogsFor;
|
|
625
|
+
const updatedEntry = {
|
|
626
|
+
id: entry.id,
|
|
627
|
+
activityId: entry.activityId,
|
|
628
|
+
generator: entry.generator,
|
|
629
|
+
dateCreated: entry.dateCreated,
|
|
630
|
+
dateModified: entry.dateModified,
|
|
631
|
+
retainUntil
|
|
632
|
+
};
|
|
633
|
+
await this._entityStorageActivityLogs.set(updatedEntry);
|
|
713
634
|
}
|
|
635
|
+
return entry;
|
|
714
636
|
}
|
|
715
637
|
/**
|
|
716
638
|
* Cleans up the activity log by deleting those entries that no longer shall be retained.
|
|
@@ -728,13 +650,30 @@ export class DataspaceDataPlaneService {
|
|
|
728
650
|
this._cleanUpProcessOngoing = true;
|
|
729
651
|
let numRecordsDeleted = 0;
|
|
730
652
|
if (this._partitionContextIds?.includes(ContextIdKeys.Tenant)) {
|
|
731
|
-
// The cleanup must be done tenant
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
653
|
+
// The cleanup must be done by tenant as the data is partitioned
|
|
654
|
+
try {
|
|
655
|
+
let cursor;
|
|
656
|
+
do {
|
|
657
|
+
const result = await this._tenantAdmin?.query(undefined, cursor);
|
|
658
|
+
cursor = result?.cursor;
|
|
659
|
+
if (!Is.empty(result)) {
|
|
660
|
+
for (const tenantId of result.tenants.map(t => t.id)) {
|
|
661
|
+
const localContextIds = (await ContextIdStore.getContextIds()) ?? {};
|
|
662
|
+
localContextIds[ContextIdKeys.Tenant] = tenantId;
|
|
663
|
+
await ContextIdStore.run(localContextIds, async () => {
|
|
664
|
+
numRecordsDeleted += await this.cleanupActivityLogPartition();
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
} while (Is.stringValue(cursor));
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
await this._logging?.log({
|
|
672
|
+
level: "error",
|
|
673
|
+
message: "cleanupFailed",
|
|
674
|
+
ts: Date.now(),
|
|
675
|
+
source: DataspaceDataPlaneService.CLASS_NAME,
|
|
676
|
+
error: BaseError.fromError(error)
|
|
738
677
|
});
|
|
739
678
|
}
|
|
740
679
|
}
|
|
@@ -789,8 +728,13 @@ export class DataspaceDataPlaneService {
|
|
|
789
728
|
}
|
|
790
729
|
} while (Is.stringValue(cursor));
|
|
791
730
|
}
|
|
792
|
-
catch {
|
|
793
|
-
|
|
731
|
+
catch (error) {
|
|
732
|
+
await this._logging?.log({
|
|
733
|
+
level: "error",
|
|
734
|
+
message: "cleanupFailed",
|
|
735
|
+
source: DataspaceDataPlaneService.CLASS_NAME,
|
|
736
|
+
error: BaseError.fromError(error)
|
|
737
|
+
});
|
|
794
738
|
}
|
|
795
739
|
return numRecordsDeleted;
|
|
796
740
|
}
|
|
@@ -847,7 +791,7 @@ export class DataspaceDataPlaneService {
|
|
|
847
791
|
* @internal
|
|
848
792
|
*/
|
|
849
793
|
getAppForActivityQuery(activityQuery) {
|
|
850
|
-
const matchingElements =
|
|
794
|
+
const matchingElements = {};
|
|
851
795
|
const appNames = DataspaceAppFactory.names();
|
|
852
796
|
for (const appId of appNames) {
|
|
853
797
|
const app = DataspaceAppFactory.get(appId);
|
|
@@ -857,10 +801,10 @@ export class DataspaceDataPlaneService {
|
|
|
857
801
|
(Is.undefined(appQuery.activityType) ||
|
|
858
802
|
appQuery.activityType === activityQuery.activityType) &&
|
|
859
803
|
(Is.undefined(appQuery.targetType) || appQuery.targetType === activityQuery.targetType)) {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
}
|
|
804
|
+
matchingElements[appId] = {
|
|
805
|
+
app,
|
|
806
|
+
processingGroupId: appQuery.processingGroupId
|
|
807
|
+
};
|
|
864
808
|
}
|
|
865
809
|
}
|
|
866
810
|
}
|
|
@@ -896,18 +840,20 @@ export class DataspaceDataPlaneService {
|
|
|
896
840
|
*/
|
|
897
841
|
async getAppForDataAssetQuery(dataAssetQuery) {
|
|
898
842
|
const matchingElements = [];
|
|
843
|
+
const matchingIds = [];
|
|
899
844
|
const appNames = DataspaceAppFactory.names();
|
|
900
845
|
for (const appId of appNames) {
|
|
901
846
|
const app = DataspaceAppFactory.get(appId);
|
|
902
847
|
const datasets = await app.datasetsHandled();
|
|
903
848
|
for (const dataset of datasets) {
|
|
904
849
|
if (dataset["@id"] === dataAssetQuery.datasetId) {
|
|
905
|
-
matchingElements.push(
|
|
850
|
+
matchingElements.push(app);
|
|
851
|
+
matchingIds.push(appId);
|
|
906
852
|
}
|
|
907
853
|
}
|
|
908
854
|
}
|
|
909
855
|
if (matchingElements.length > 1) {
|
|
910
|
-
const error = new ConflictError(DataspaceDataPlaneService.CLASS_NAME, "tooManyAppsRegistered", dataAssetQuery.datasetId,
|
|
856
|
+
const error = new ConflictError(DataspaceDataPlaneService.CLASS_NAME, "tooManyAppsRegistered", dataAssetQuery.datasetId, matchingIds, {
|
|
911
857
|
datasetId: dataAssetQuery.datasetId
|
|
912
858
|
});
|
|
913
859
|
await this._logging?.log({
|
|
@@ -946,11 +892,15 @@ export class DataspaceDataPlaneService {
|
|
|
946
892
|
* @internal
|
|
947
893
|
*/
|
|
948
894
|
async prepareForRetry(activityLogEntryId, existingEntry) {
|
|
949
|
-
const appsToRetry = existingEntry.
|
|
895
|
+
const appsToRetry = existingEntry.tasks
|
|
896
|
+
?.filter(t => t.status === ActivityTaskStatus.Failed)
|
|
897
|
+
.map(t => t.dataspaceAppId) ?? [];
|
|
950
898
|
if (!Is.arrayValue(appsToRetry)) {
|
|
951
899
|
throw new NotFoundError(DataspaceDataPlaneService.CLASS_NAME, "noFailedTasksToRetry", activityLogEntryId);
|
|
952
900
|
}
|
|
953
|
-
const successfulApps = existingEntry.
|
|
901
|
+
const successfulApps = existingEntry.tasks
|
|
902
|
+
?.filter(t => t.status === ActivityTaskStatus.Success)
|
|
903
|
+
.map(t => t.dataspaceAppId) ?? [];
|
|
954
904
|
await this._logging?.log({
|
|
955
905
|
level: "debug",
|
|
956
906
|
source: DataspaceDataPlaneService.CLASS_NAME,
|
|
@@ -968,8 +918,6 @@ export class DataspaceDataPlaneService {
|
|
|
968
918
|
if (this._retainActivityLogsFor !== -1) {
|
|
969
919
|
logEntry.retainUntil = Date.now() + this._retainActivityLogsFor;
|
|
970
920
|
}
|
|
971
|
-
// Monitoring purposes
|
|
972
|
-
logEntry.retryCount = (logEntry.retryCount ?? 0) + 1;
|
|
973
921
|
await this._entityStorageActivityLogs.set(logEntry);
|
|
974
922
|
}
|
|
975
923
|
return successfulApps;
|
|
@@ -1065,5 +1013,183 @@ export class DataspaceDataPlaneService {
|
|
|
1065
1013
|
});
|
|
1066
1014
|
}
|
|
1067
1015
|
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Processes a task for an activity, by creating a background task and registering the handler.
|
|
1018
|
+
* @param activityLogEntryId The ID of the activity log entry.
|
|
1019
|
+
* @param activity The activity to be processed.
|
|
1020
|
+
* @param handlerApps The handler applications for the activity.
|
|
1021
|
+
* @param dataspaceAppId The ID of the handler application.
|
|
1022
|
+
* @param taskEntries The list of activity log entries.
|
|
1023
|
+
* @param isRetry Indicates if this is a retry of a previous task.
|
|
1024
|
+
* @returns True if the task was processed inline.
|
|
1025
|
+
* @internal
|
|
1026
|
+
*/
|
|
1027
|
+
async processTask(activityLogEntryId, activity, handlerApps, dataspaceAppId, taskEntries, isRetry) {
|
|
1028
|
+
const handlerApp = handlerApps[dataspaceAppId].app;
|
|
1029
|
+
const processingGroupId = handlerApps[dataspaceAppId].processingGroupId;
|
|
1030
|
+
const payload = {
|
|
1031
|
+
activityLogEntryId,
|
|
1032
|
+
activity: activity,
|
|
1033
|
+
dataspaceAppId
|
|
1034
|
+
};
|
|
1035
|
+
// If there is no processing group we execute the task inline without creating a background task
|
|
1036
|
+
const isInlineTask = !Is.stringValue(processingGroupId);
|
|
1037
|
+
if (isInlineTask) {
|
|
1038
|
+
const handleActivity = handlerApp?.handleActivity?.bind(handlerApp);
|
|
1039
|
+
if (!Is.function(handleActivity)) {
|
|
1040
|
+
throw new GeneralError(DataspaceDataPlaneService.CLASS_NAME, "missingHandleActivity", {
|
|
1041
|
+
dataspaceAppId
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
let taskError;
|
|
1045
|
+
let taskResult;
|
|
1046
|
+
try {
|
|
1047
|
+
taskResult = await handleActivity(activity);
|
|
1048
|
+
}
|
|
1049
|
+
catch (error) {
|
|
1050
|
+
taskError = BaseError.fromError(error);
|
|
1051
|
+
}
|
|
1052
|
+
const now = Date.now();
|
|
1053
|
+
const taskEntry = {
|
|
1054
|
+
taskId: RandomHelper.generateUuidV7("compact"),
|
|
1055
|
+
dataspaceAppId,
|
|
1056
|
+
processingGroupId,
|
|
1057
|
+
result: taskResult,
|
|
1058
|
+
startDate: new Date(now).toISOString(),
|
|
1059
|
+
endDate: new Date(now).toISOString(),
|
|
1060
|
+
status: Is.empty(taskError) ? ActivityTaskStatus.Success : ActivityTaskStatus.Failed,
|
|
1061
|
+
error: taskError
|
|
1062
|
+
};
|
|
1063
|
+
taskEntries.push(taskEntry);
|
|
1064
|
+
await this.notifyTaskStatusChanged(payload.activityLogEntryId, payload.activity.id, payload.dataspaceAppId, taskEntry.taskId, taskEntry.status);
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
const processingGroups = handlerApp.processingGroups?.() ?? {};
|
|
1068
|
+
if (Is.empty(processingGroups[processingGroupId])) {
|
|
1069
|
+
throw new GeneralError(DataspaceDataPlaneService.CLASS_NAME, "invalidProcessingGroupId", {
|
|
1070
|
+
processingGroupId
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
const processingGroupOptions = processingGroups[processingGroupId];
|
|
1074
|
+
const taskType = `${dataspaceAppId}${processingGroupId ? `-${processingGroupId}` : ""}`;
|
|
1075
|
+
const taskId = await this._backgroundTaskComponent.create(taskType, payload, {
|
|
1076
|
+
retainFor: this._retainTasksFor,
|
|
1077
|
+
retryCount: processingGroupOptions?.retryCount ?? this._retryCount
|
|
1078
|
+
});
|
|
1079
|
+
if (!this._registeredTaskTypes.includes(taskType)) {
|
|
1080
|
+
this._registeredTaskTypes.push(taskType);
|
|
1081
|
+
await this._backgroundTaskComponent.registerHandler(taskType, "@twin.org/dataspace-app-runner", "appRunner", async (task) => {
|
|
1082
|
+
await this.finaliseBackgroundTask(task.id, task.status, task.payload);
|
|
1083
|
+
}, {
|
|
1084
|
+
maxWorkerCount: processingGroupOptions?.concurrentTasks,
|
|
1085
|
+
idleShutdownTimeout: processingGroupOptions?.idleShutdownTimeout,
|
|
1086
|
+
initialiseMethod: "appRunnerStart",
|
|
1087
|
+
shutdownMethod: "appRunnerEnd"
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
taskEntries.push({
|
|
1091
|
+
taskId,
|
|
1092
|
+
dataspaceAppId,
|
|
1093
|
+
processingGroupId,
|
|
1094
|
+
status: ActivityTaskStatus.Pending
|
|
1095
|
+
});
|
|
1096
|
+
await this._logging?.log({
|
|
1097
|
+
level: "info",
|
|
1098
|
+
source: DataspaceDataPlaneService.CLASS_NAME,
|
|
1099
|
+
message: "scheduledTask",
|
|
1100
|
+
data: {
|
|
1101
|
+
taskId,
|
|
1102
|
+
dataspaceAppId,
|
|
1103
|
+
isRetry
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
return isInlineTask;
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Constructs the activity log entry with processing status and associated tasks.
|
|
1111
|
+
* @param activityLog The activity log details retrieved from storage.
|
|
1112
|
+
* @param activityTasks The activity tasks associated with the log entry, if any.
|
|
1113
|
+
* @returns The complete activity log entry with status and tasks.
|
|
1114
|
+
* @internal
|
|
1115
|
+
*/
|
|
1116
|
+
async constructLogEntry(activityLog, activityTasks) {
|
|
1117
|
+
let tasks;
|
|
1118
|
+
// For calculating the processing status. `Registering` if we cannot determine the activity tasks yet
|
|
1119
|
+
let status = ActivityProcessingStatus.Registering;
|
|
1120
|
+
// Now query the associated tasks
|
|
1121
|
+
// If activity tasks is undefined it is because the corresponding store has not been persisted yet
|
|
1122
|
+
if (!Is.undefined(activityTasks)) {
|
|
1123
|
+
tasks = [];
|
|
1124
|
+
const typeCount = {
|
|
1125
|
+
[TaskStatus.Pending]: 0,
|
|
1126
|
+
[TaskStatus.Processing]: 0,
|
|
1127
|
+
[TaskStatus.Success]: 0,
|
|
1128
|
+
[TaskStatus.Failed]: 0,
|
|
1129
|
+
[TaskStatus.Cancelled]: 0
|
|
1130
|
+
};
|
|
1131
|
+
for (const entity of activityTasks.associatedTasks) {
|
|
1132
|
+
let entry;
|
|
1133
|
+
if (!Is.stringValue(entity.processingGroupId)) {
|
|
1134
|
+
// If there is no process group, the task was processed inline so the task status is already available in the entity
|
|
1135
|
+
typeCount[entity.status]++;
|
|
1136
|
+
entry = entity;
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
const taskDetails = await this._backgroundTaskComponent.get(entity.taskId);
|
|
1140
|
+
if (!Is.empty(taskDetails)) {
|
|
1141
|
+
typeCount[taskDetails.status]++;
|
|
1142
|
+
switch (taskDetails.status) {
|
|
1143
|
+
case TaskStatus.Success:
|
|
1144
|
+
entry = {
|
|
1145
|
+
...entity,
|
|
1146
|
+
status: ActivityTaskStatus.Success,
|
|
1147
|
+
result: taskDetails.result,
|
|
1148
|
+
startDate: taskDetails?.dateCreated,
|
|
1149
|
+
endDate: taskDetails?.dateCompleted
|
|
1150
|
+
};
|
|
1151
|
+
break;
|
|
1152
|
+
case TaskStatus.Pending:
|
|
1153
|
+
entry = { ...entity, status: ActivityTaskStatus.Pending };
|
|
1154
|
+
break;
|
|
1155
|
+
case TaskStatus.Processing:
|
|
1156
|
+
entry = {
|
|
1157
|
+
...entity,
|
|
1158
|
+
status: ActivityTaskStatus.Processing,
|
|
1159
|
+
startDate: taskDetails.dateCreated
|
|
1160
|
+
};
|
|
1161
|
+
break;
|
|
1162
|
+
case TaskStatus.Failed:
|
|
1163
|
+
entry = {
|
|
1164
|
+
...entity,
|
|
1165
|
+
status: ActivityTaskStatus.Failed,
|
|
1166
|
+
error: taskDetails.error
|
|
1167
|
+
};
|
|
1168
|
+
break;
|
|
1169
|
+
case TaskStatus.Cancelled:
|
|
1170
|
+
// Nothing to do for cancelled tasks
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (!Is.empty(entry)) {
|
|
1176
|
+
tasks.push(entry);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (typeCount[TaskStatus.Failed] > 0) {
|
|
1180
|
+
status = ActivityProcessingStatus.Error;
|
|
1181
|
+
}
|
|
1182
|
+
else if (typeCount[TaskStatus.Processing] > 0) {
|
|
1183
|
+
status = ActivityProcessingStatus.Running;
|
|
1184
|
+
}
|
|
1185
|
+
else if (typeCount[TaskStatus.Pending] > 0) {
|
|
1186
|
+
status = ActivityProcessingStatus.Pending;
|
|
1187
|
+
}
|
|
1188
|
+
else {
|
|
1189
|
+
status = ActivityProcessingStatus.Completed;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return { ...activityLog, status, tasks };
|
|
1193
|
+
}
|
|
1068
1194
|
}
|
|
1069
1195
|
//# sourceMappingURL=dataspaceDataPlaneService.js.map
|