@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.
@@ -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
- * Entity storage for Transfer Process entities.
122
- * Used to read transfer state from shared storage (written by Control Plane).
124
+ * The tenant admin component.
123
125
  * @internal
124
126
  */
125
- _transferProcessStorage;
127
+ _tenantAdmin;
126
128
  /**
127
- * The list of active tenants required for task cleanup.
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
- _activeTenants;
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._activeTenants = [];
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 Activity's Log Entry identifier.
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
- const existingLogEntry = await this._entityStorageActivityLogs.get(activityLogEntryId);
270
+ let logEntry = await this._entityStorageActivityLogs.get(activityLogEntryId);
267
271
  let existingSuccessfulApps = [];
268
272
  let isRetry = false;
269
- if (!Is.undefined(existingLogEntry)) {
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
- const logEntry = {
292
+ logEntry = {
288
293
  id: activityLogEntryId,
289
- activityId: Is.string(activity.id) ? activity.id : undefined,
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 tasksScheduled = [];
298
- const dataspaceAppIds = [];
302
+ const taskEntries = [];
303
+ const handlerApps = {};
299
304
  for (const query of activityQuerySet) {
300
- const appIds = this.getAppForActivityQuery(query);
301
- for (const appId of appIds) {
302
- if (!dataspaceAppIds.includes(appId)) {
303
- dataspaceAppIds.push(appId);
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
- for (const dataspaceAppId of dataspaceAppIds) {
308
- // Only process apps that haven't already completed successfully
309
- if (!existingSuccessfulApps.includes(dataspaceAppId)) {
310
- const payload = {
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
- await this._entityStorageActivityTasks.set({
323
+ const activityTask = {
346
324
  activityLogEntryId,
347
- associatedTasks: [...existingTasksToKeep, ...tasksScheduled]
348
- });
349
- return activityLogEntryId;
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 result = await this._entityStorageActivityLogs.get(logEntryId);
382
- if (Is.undefined(result)) {
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
- // If activity tasks is undefined it is because the corresponding store has not been persisted yet
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 appId = await this.getAppForDataAssetQuery({ datasetId });
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 appId = await this.getAppForDataAssetQuery({ datasetId });
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 proofEntity The proof entity to process.
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 finaliseTask(task) {
658
- const payload = task.payload;
582
+ async finaliseBackgroundTask(taskId, status, payload) {
659
583
  if (Is.empty(payload)) {
660
584
  return;
661
585
  }
662
- const activityLogEntry = await this._entityStorageActivityLogs.get(payload.activityLogEntryId);
663
- if (Is.undefined(activityLogEntry)) {
664
- await this._logging?.log({
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
- if (task.status === TaskStatus.Success || task.status === TaskStatus.Failed) {
674
- for (const callback of Object.values(this._activityLogStatusCallbacks)) {
675
- await callback({
676
- activityLogEntryId: payload.activityLogEntryId,
677
- activityId: Is.string(payload.activity.id) ? payload.activity.id : undefined,
678
- taskProcessingStatus: {
679
- dataspaceAppId: payload.executorApp,
680
- taskId: task.id,
681
- taskStatus: task.status
682
- }
683
- });
684
- }
685
- // Now let's see if the full activity processing has completed, if so the entry must be marked for retention
686
- if (this._retainActivityLogsFor !== -1) {
687
- const entry = await this.getActivityLogEntry(payload.activityLogEntryId);
688
- if (entry.status === ActivityProcessingStatus.Completed ||
689
- entry.status === ActivityProcessingStatus.Error) {
690
- const retainUntil = Date.now() + this._retainActivityLogsFor;
691
- await this._entityStorageActivityLogs.set({
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
- * Updates the list of active tenants for cleanup tasks.
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 updateActiveTenants() {
709
- const contextIds = await ContextIdStore.getContextIds();
710
- const tenantId = contextIds?.[ContextIdKeys.Tenant];
711
- if (Is.stringValue(tenantId) && !this._activeTenants.includes(tenantId)) {
712
- this._activeTenants.push(tenantId);
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 by tenant
732
- // as the data behind the scenes might be partitioned
733
- for (const tenantId of this._activeTenants) {
734
- const localContextIds = (await ContextIdStore.getContextIds()) ?? {};
735
- localContextIds[ContextIdKeys.Tenant] = tenantId;
736
- await ContextIdStore.run(localContextIds, async () => {
737
- numRecordsDeleted += await this.cleanupActivityLogPartition();
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
- // If cleaning up the retained items fail we don't really care, they will get cleaned up on the next sweep.
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
- // Avoid duplicates. Only one DS App can be executed per activity
861
- if (!matchingElements.includes(appId)) {
862
- matchingElements.push(appId);
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(appId);
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, matchingElements, {
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.inErrorTasks?.map(t => t.dataspaceAppId) ?? [];
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.finalizedTasks?.map(t => t.dataspaceAppId) ?? [];
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