@twin.org/dataspace-data-plane-service 0.0.3-next.25 → 0.0.3-next.27

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.
@@ -4,7 +4,7 @@ import { ArrayHelper, BaseError, ComponentFactory, ConflictError, Converter, Gen
4
4
  import { Blake2b } from "@twin.org/crypto";
5
5
  import { DataTypeHelper, JsonSchemaHelper } from "@twin.org/data-core";
6
6
  import { JsonLdDataTypes, JsonLdHelper, JsonLdProcessor } from "@twin.org/data-json-ld";
7
- 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";
8
8
  import { EngineCoreFactory } from "@twin.org/engine-models";
9
9
  import { ComparisonOperator, LogicalOperator } from "@twin.org/entity";
10
10
  import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
@@ -70,6 +70,11 @@ export class DataspaceDataPlaneService {
70
70
  * @internal
71
71
  */
72
72
  _activityLogStatusCallbacks;
73
+ /**
74
+ * Track task handler registrations to avoid resetting worker pools on every task.
75
+ * @internal
76
+ */
77
+ _registeredTaskTypes;
73
78
  /**
74
79
  * Task retention. -1 retain forever.
75
80
  * @internal
@@ -80,6 +85,11 @@ export class DataspaceDataPlaneService {
80
85
  * @internal
81
86
  */
82
87
  _retainActivityLogsFor;
88
+ /**
89
+ * Retry count for failed tasks.
90
+ * @internal
91
+ */
92
+ _retryCount;
83
93
  /**
84
94
  * Clean up interval for activity logs.
85
95
  * @internal
@@ -121,6 +131,11 @@ export class DataspaceDataPlaneService {
121
131
  * @internal
122
132
  */
123
133
  _transferProcessStorage;
134
+ /**
135
+ * Entity storage for tenant-supplied Dataspace App Dataset entities.
136
+ * @internal
137
+ */
138
+ _dataspaceAppDatasetStorage;
124
139
  /**
125
140
  * Create a new instance of DataspaceDataPlane.
126
141
  * @param options The options for the data plane.
@@ -138,17 +153,20 @@ export class DataspaceDataPlaneService {
138
153
  // Entity storage for Transfer Process state lookup
139
154
  // Used to read transfer state from shared storage (written by Control Plane)
140
155
  this._transferProcessStorage = EntityStorageConnectorFactory.get(options?.transferProcessEntityStorageType ?? "transfer-process");
156
+ this._dataspaceAppDatasetStorage = EntityStorageConnectorFactory.get(options?.dataspaceAppDatasetEntityStorageType ?? "dataspace-app-dataset");
141
157
  JsonLdDataTypes.registerTypes();
142
158
  DataspaceDataTypes.registerTypes();
143
159
  SchemaOrgDataTypes.registerRedirects();
144
160
  DataspaceProtocolDataTypes.registerRedirects();
145
161
  DataspaceProtocolDataTypes.registerTypes();
146
162
  this._activityLogStatusCallbacks = {};
163
+ this._registeredTaskTypes = [];
147
164
  this._partitionContextIds = options?.partitionContextIds;
148
165
  this._retainTasksFor =
149
166
  DataspaceDataPlaneService._DEFAULT_RETAIN_INTERVAL * DataspaceDataPlaneService._MS_PER_MINUTE;
150
167
  this._retainActivityLogsFor =
151
168
  DataspaceDataPlaneService._DEFAULT_RETAIN_INTERVAL * DataspaceDataPlaneService._MS_PER_MINUTE;
169
+ this._retryCount = options?.config?.retryCount;
152
170
  this._activityLogCleanUpInterval = DataspaceDataPlaneService._DEFAULT_CLEANUP_INTERVAL;
153
171
  this._cleanUpProcessOngoing = false;
154
172
  const validationErrors = [];
@@ -225,7 +243,7 @@ export class DataspaceDataPlaneService {
225
243
  /**
226
244
  * Notify an Activity.
227
245
  * @param activity The Activity notified.
228
- * @returns The Activity's Log Entry identifier.
246
+ * @returns The activity's id or entry.
229
247
  */
230
248
  async notifyActivity(activity) {
231
249
  Guards.object(DataspaceDataPlaneService.CLASS_NAME, "activity", activity);
@@ -255,10 +273,11 @@ export class DataspaceDataPlaneService {
255
273
  const activityLogId = Converter.bytesToHex(Blake2b.sum256(canonicalBytes));
256
274
  const activityLogEntryId = `urn:x-activity-log:${activityLogId}`;
257
275
  // Check if entry already exists
258
- const existingLogEntry = await this._entityStorageActivityLogs.get(activityLogEntryId);
276
+ let logEntry = await this._entityStorageActivityLogs.get(activityLogEntryId);
259
277
  let existingSuccessfulApps = [];
260
278
  let isRetry = false;
261
- if (!Is.undefined(existingLogEntry)) {
279
+ const now = Date.now();
280
+ if (!Is.undefined(logEntry)) {
262
281
  // Check if there are failed tasks that can be retried
263
282
  const existingEntry = await this.getActivityLogEntry(activityLogEntryId);
264
283
  // If all tasks completed successfully, this is a duplicate
@@ -276,69 +295,46 @@ export class DataspaceDataPlaneService {
276
295
  isRetry = true;
277
296
  }
278
297
  else {
279
- const logEntry = {
298
+ logEntry = {
280
299
  id: activityLogEntryId,
281
- activityId: Is.string(activity.id) ? activity.id : undefined,
300
+ activityId: activity.id,
282
301
  generator: this.calculateActivityGeneratorIdentity(activity),
283
- dateCreated: new Date().toISOString(),
284
- dateModified: new Date().toISOString()
302
+ dateCreated: new Date(now).toISOString(),
303
+ dateModified: new Date(now).toISOString()
285
304
  };
286
305
  await this._entityStorageActivityLogs.set(logEntry);
287
306
  }
288
307
  const activityQuerySet = await this.calculateActivityQuerySet(activity);
289
- const tasksScheduled = [];
290
- const dataspaceAppIds = [];
308
+ const taskEntries = [];
309
+ const handlerApps = {};
291
310
  for (const query of activityQuerySet) {
292
- const appIds = this.getAppForActivityQuery(query);
293
- for (const appId of appIds) {
294
- if (!dataspaceAppIds.includes(appId)) {
295
- dataspaceAppIds.push(appId);
311
+ const apps = this.getAppForActivityQuery(query);
312
+ for (const appId in apps) {
313
+ // Only process apps that haven't already completed successfully
314
+ if (!handlerApps[appId] && !existingSuccessfulApps.includes(appId)) {
315
+ handlerApps[appId] = apps[appId];
296
316
  }
297
317
  }
298
318
  }
299
- for (const dataspaceAppId of dataspaceAppIds) {
300
- // Only process apps that haven't already completed successfully
301
- if (!existingSuccessfulApps.includes(dataspaceAppId)) {
302
- const payload = {
303
- activityLogEntryId,
304
- activity: activity,
305
- executorApp: dataspaceAppId
306
- };
307
- const taskType = Converter.bytesToHex(RandomHelper.generate(16));
308
- const taskId = await this._backgroundTaskComponent.create(taskType, payload, {
309
- retainFor: this._retainTasksFor
310
- });
311
- await this._backgroundTaskComponent.registerHandler(taskType, "@twin.org/dataspace-app-runner", "appRunner", async (task) => {
312
- await this.finaliseTask(task);
313
- }, {
314
- initialiseMethod: "appRunnerStart",
315
- shutdownMethod: "appRunnerEnd"
316
- });
317
- tasksScheduled.push({
318
- taskId,
319
- dataspaceAppId
320
- });
321
- await this._logging?.log({
322
- level: "info",
323
- source: DataspaceDataPlaneService.CLASS_NAME,
324
- message: "scheduledTask",
325
- data: {
326
- taskId,
327
- dataspaceAppId,
328
- isRetry
329
- }
330
- });
319
+ let inlineCount = 0;
320
+ for (const handlerAppId in handlerApps) {
321
+ if (await this.processTask(activityLogEntryId, activity, handlerApps, handlerAppId, taskEntries, isRetry)) {
322
+ inlineCount++;
331
323
  }
332
324
  }
333
325
  const existingActivityTasks = isRetry
334
326
  ? await this._entityStorageActivityTasks.get(activityLogEntryId)
335
327
  : undefined;
336
328
  const existingTasksToKeep = existingActivityTasks?.associatedTasks.filter(t => existingSuccessfulApps.includes(t.dataspaceAppId)) ?? [];
337
- await this._entityStorageActivityTasks.set({
329
+ const activityTask = {
338
330
  activityLogEntryId,
339
- associatedTasks: [...existingTasksToKeep, ...tasksScheduled]
340
- });
341
- return activityLogEntryId;
331
+ associatedTasks: [...existingTasksToKeep, ...taskEntries]
332
+ };
333
+ await this._entityStorageActivityTasks.set(activityTask);
334
+ if (inlineCount === taskEntries.length) {
335
+ return this.finaliseActivityLogEntry(activityLogEntryId);
336
+ }
337
+ return activityTask.activityLogEntryId;
342
338
  }
343
339
  /**
344
340
  * Subscribes to the activity log.
@@ -370,68 +366,12 @@ export class DataspaceDataPlaneService {
370
366
  */
371
367
  async getActivityLogEntry(logEntryId) {
372
368
  Guards.stringValue(DataspaceDataPlaneService.CLASS_NAME, "logEntryId", logEntryId);
373
- const result = await this._entityStorageActivityLogs.get(logEntryId);
374
- if (Is.undefined(result)) {
369
+ const activityLog = await this._entityStorageActivityLogs.get(logEntryId);
370
+ if (Is.undefined(activityLog)) {
375
371
  throw new NotFoundError(DataspaceDataPlaneService.CLASS_NAME, "activityLogEntryNotFound", logEntryId);
376
372
  }
377
- let pendingTasks;
378
- let runningTasks;
379
- let finalizedTasks;
380
- let inErrorTasks;
381
- // For calculating the processing status. `Registering` if we cannot determine the activity tasks yet
382
- let status = ActivityProcessingStatus.Registering;
383
- // Now query the associated tasks
384
373
  const activityTasks = await this._entityStorageActivityTasks.get(logEntryId);
385
- // If activity tasks is undefined it is because the corresponding store has not been persisted yet
386
- if (!Is.undefined(activityTasks)) {
387
- pendingTasks = [];
388
- runningTasks = [];
389
- finalizedTasks = [];
390
- inErrorTasks = [];
391
- for (const entity of activityTasks.associatedTasks) {
392
- const taskDetails = await this._backgroundTaskComponent.get(entity.taskId);
393
- if (Is.object(taskDetails)) {
394
- switch (taskDetails.status) {
395
- case TaskStatus.Success:
396
- finalizedTasks.push({
397
- ...entity,
398
- result: JSON.stringify(taskDetails.result),
399
- startDate: taskDetails?.dateCreated,
400
- endDate: taskDetails?.dateCompleted
401
- });
402
- break;
403
- case TaskStatus.Pending:
404
- pendingTasks.push(entity);
405
- break;
406
- case TaskStatus.Processing:
407
- runningTasks.push({ ...entity, startDate: taskDetails.dateCreated });
408
- break;
409
- case TaskStatus.Failed:
410
- inErrorTasks.push({
411
- ...entity,
412
- error: taskDetails.error
413
- });
414
- break;
415
- case TaskStatus.Cancelled:
416
- // Nothing to do for cancelled tasks
417
- break;
418
- }
419
- }
420
- }
421
- if (Is.arrayValue(inErrorTasks)) {
422
- status = ActivityProcessingStatus.Error;
423
- }
424
- else if (Is.arrayValue(runningTasks)) {
425
- status = ActivityProcessingStatus.Running;
426
- }
427
- else if (Is.arrayValue(pendingTasks)) {
428
- status = ActivityProcessingStatus.Pending;
429
- }
430
- else {
431
- status = ActivityProcessingStatus.Completed;
432
- }
433
- }
434
- return { ...result, status, pendingTasks, runningTasks, finalizedTasks, inErrorTasks };
374
+ return this.constructLogEntry(activityLog, activityTasks);
435
375
  }
436
376
  /**
437
377
  * Get Data Asset entities. Allows to retrieve entities by their type or id.
@@ -471,9 +411,7 @@ export class DataspaceDataPlaneService {
471
411
  }
472
412
  const datasetId = serviceDataset["@id"];
473
413
  Guards.stringValue(DataspaceDataPlaneService.CLASS_NAME, "datasetId", datasetId);
474
- const appId = await this.getAppForDataAssetQuery({ datasetId });
475
- // getAppForDataAssetQuery already validates app exists
476
- const app = DataspaceAppFactory.get(appId);
414
+ const app = await this.getAppForDataAssetQuery({ datasetId });
477
415
  const handleDataRequest = app.handleDataRequest?.bind(app);
478
416
  Guards.function(DataspaceDataPlaneService.CLASS_NAME, "handleDataRequest", handleDataRequest);
479
417
  const dataRequest = {
@@ -537,8 +475,7 @@ export class DataspaceDataPlaneService {
537
475
  const serviceDataset = await this.getDatasetFromApps(resolvedDatasetId);
538
476
  const datasetId = serviceDataset["@id"];
539
477
  Guards.stringValue(DataspaceDataPlaneService.CLASS_NAME, "datasetId", datasetId);
540
- const appId = await this.getAppForDataAssetQuery({ datasetId });
541
- const app = DataspaceAppFactory.get(appId);
478
+ const app = await this.getAppForDataAssetQuery({ datasetId });
542
479
  if (!app.supportedQueryTypes().includes(query.type)) {
543
480
  throw new UnprocessableError(DataspaceDataPlaneService.CLASS_NAME, "queryTypeNotSupported", {
544
481
  queryType: query.type
@@ -643,55 +580,65 @@ export class DataspaceDataPlaneService {
643
580
  }
644
581
  /**
645
582
  * Process activity task finalization.
646
- * @param proofEntity The proof entity to process.
583
+ * @param taskId The Id of the Activity Log Entry.
584
+ * @param status The final status of the task.
585
+ * @param payload The execution payload of the task, required to correlate to the Activity Log Entry and update the processing status.
647
586
  * @internal
648
587
  */
649
- async finaliseTask(task) {
650
- const payload = task.payload;
588
+ async finaliseBackgroundTask(taskId, status, payload) {
651
589
  if (Is.empty(payload)) {
652
590
  return;
653
591
  }
654
- const activityLogEntry = await this._entityStorageActivityLogs.get(payload.activityLogEntryId);
655
- if (Is.undefined(activityLogEntry)) {
656
- await this._logging?.log({
657
- level: "error",
658
- source: DataspaceDataPlaneService.CLASS_NAME,
659
- message: "unknownActivityLogEntryId",
660
- data: {
661
- activityLogEntryId: payload.activityLogEntryId
592
+ if (status === TaskStatus.Success || status === TaskStatus.Failed) {
593
+ await this.notifyTaskStatusChanged(payload.activityLogEntryId, payload.activity.id, payload.dataspaceAppId, taskId, status);
594
+ await this.finaliseActivityLogEntry(payload.activityLogEntryId);
595
+ }
596
+ }
597
+ /**
598
+ * Notify registered callbacks about a task status change.
599
+ * @param activityLogEntryId The Id of the Activity Log Entry.
600
+ * @param activityId The Id of the Activity.
601
+ * @param dataspaceAppId The Id of the Dataspace App associated with the task.
602
+ * @param taskId The Id of the task.
603
+ * @param taskStatus The new status of the task.
604
+ * @internal
605
+ */
606
+ async notifyTaskStatusChanged(activityLogEntryId, activityId, dataspaceAppId, taskId, taskStatus) {
607
+ for (const callback of Object.values(this._activityLogStatusCallbacks)) {
608
+ await callback({
609
+ activityLogEntryId,
610
+ activityId,
611
+ taskProcessingStatus: {
612
+ dataspaceAppId,
613
+ taskId,
614
+ taskStatus
662
615
  }
663
616
  });
664
617
  }
665
- if (task.status === TaskStatus.Success || task.status === TaskStatus.Failed) {
666
- for (const callback of Object.values(this._activityLogStatusCallbacks)) {
667
- await callback({
668
- activityLogEntryId: payload.activityLogEntryId,
669
- activityId: Is.string(payload.activity.id) ? payload.activity.id : undefined,
670
- taskProcessingStatus: {
671
- dataspaceAppId: payload.executorApp,
672
- taskId: task.id,
673
- taskStatus: task.status
674
- }
675
- });
676
- }
677
- // Now let's see if the full activity processing has completed, if so the entry must be marked for retention
678
- if (this._retainActivityLogsFor !== -1) {
679
- const entry = await this.getActivityLogEntry(payload.activityLogEntryId);
680
- if (entry.status === ActivityProcessingStatus.Completed ||
681
- entry.status === ActivityProcessingStatus.Error) {
682
- const retainUntil = Date.now() + this._retainActivityLogsFor;
683
- await this._entityStorageActivityLogs.set({
684
- id: entry.id,
685
- activityId: entry.activityId,
686
- generator: entry.generator,
687
- dateCreated: entry.dateCreated,
688
- dateModified: entry.dateModified,
689
- retainUntil,
690
- retryCount: entry.retryCount
691
- });
692
- }
693
- }
618
+ }
619
+ /**
620
+ * 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.
621
+ * @param activityLogEntryId The Id of the Activity Log Entry to finalize.
622
+ * @returns The Activity Log Entry with updated retention details if applicable.
623
+ * @internal
624
+ */
625
+ async finaliseActivityLogEntry(activityLogEntryId) {
626
+ const entry = await this.getActivityLogEntry(activityLogEntryId);
627
+ if (this._retainActivityLogsFor !== -1 &&
628
+ (entry.status === ActivityProcessingStatus.Completed ||
629
+ entry.status === ActivityProcessingStatus.Error)) {
630
+ const retainUntil = Date.now() + this._retainActivityLogsFor;
631
+ const updatedEntry = {
632
+ id: entry.id,
633
+ activityId: entry.activityId,
634
+ generator: entry.generator,
635
+ dateCreated: entry.dateCreated,
636
+ dateModified: entry.dateModified,
637
+ retainUntil
638
+ };
639
+ await this._entityStorageActivityLogs.set(updatedEntry);
694
640
  }
641
+ return entry;
695
642
  }
696
643
  /**
697
644
  * Cleans up the activity log by deleting those entries that no longer shall be retained.
@@ -850,7 +797,7 @@ export class DataspaceDataPlaneService {
850
797
  * @internal
851
798
  */
852
799
  getAppForActivityQuery(activityQuery) {
853
- const matchingElements = [];
800
+ const matchingElements = {};
854
801
  const appNames = DataspaceAppFactory.names();
855
802
  for (const appId of appNames) {
856
803
  const app = DataspaceAppFactory.get(appId);
@@ -860,17 +807,18 @@ export class DataspaceDataPlaneService {
860
807
  (Is.undefined(appQuery.activityType) ||
861
808
  appQuery.activityType === activityQuery.activityType) &&
862
809
  (Is.undefined(appQuery.targetType) || appQuery.targetType === activityQuery.targetType)) {
863
- // Avoid duplicates. Only one DS App can be executed per activity
864
- if (!matchingElements.includes(appId)) {
865
- matchingElements.push(appId);
866
- }
810
+ matchingElements[appId] = {
811
+ app,
812
+ processingGroupId: appQuery.processingGroupId
813
+ };
867
814
  }
868
815
  }
869
816
  }
870
817
  return matchingElements;
871
818
  }
872
819
  /**
873
- * Get a dataset from registered apps by its ID.
820
+ * Get a dataset by its ID. Resolves via the tenant-supplied dataspace app
821
+ * datasets stored by the Control Plane.
874
822
  * @param datasetId The dataset identifier (@id)
875
823
  * @returns The dataset
876
824
  * @throws NotFoundError if no app handles this dataset
@@ -878,14 +826,12 @@ export class DataspaceDataPlaneService {
878
826
  */
879
827
  async getDatasetFromApps(datasetId) {
880
828
  Guards.stringValue(DataspaceDataPlaneService.CLASS_NAME, "datasetId", datasetId);
881
- const appNames = DataspaceAppFactory.names();
882
- for (const appId of appNames) {
883
- const app = DataspaceAppFactory.get(appId);
884
- const datasets = await app.datasetsHandled();
885
- const dataset = datasets.find(d => d["@id"] === datasetId);
886
- if (dataset) {
887
- return dataset;
888
- }
829
+ const fromAppDataset = await this._dataspaceAppDatasetStorage.get(datasetId);
830
+ if (!Is.empty(fromAppDataset)) {
831
+ return {
832
+ ...fromAppDataset.dataset,
833
+ "@id": datasetId
834
+ };
889
835
  }
890
836
  throw new NotFoundError(DataspaceDataPlaneService.CLASS_NAME, "noAppRegistered", datasetId, {
891
837
  datasetId
@@ -899,18 +845,16 @@ export class DataspaceDataPlaneService {
899
845
  */
900
846
  async getAppForDataAssetQuery(dataAssetQuery) {
901
847
  const matchingElements = [];
902
- const appNames = DataspaceAppFactory.names();
903
- for (const appId of appNames) {
904
- const app = DataspaceAppFactory.get(appId);
905
- const datasets = await app.datasetsHandled();
906
- for (const dataset of datasets) {
907
- if (dataset["@id"] === dataAssetQuery.datasetId) {
908
- matchingElements.push(appId);
909
- }
910
- }
848
+ const matchingIds = [];
849
+ // Storage primary key is the dataset's @id, so a single get() resolves it.
850
+ const fromAppDataset = await this._dataspaceAppDatasetStorage.get(dataAssetQuery.datasetId);
851
+ if (!Is.empty(fromAppDataset)) {
852
+ const app = DataspaceAppFactory.get(fromAppDataset.appId);
853
+ matchingElements.push(app);
854
+ matchingIds.push(fromAppDataset.appId);
911
855
  }
912
856
  if (matchingElements.length > 1) {
913
- const error = new ConflictError(DataspaceDataPlaneService.CLASS_NAME, "tooManyAppsRegistered", dataAssetQuery.datasetId, matchingElements, {
857
+ const error = new ConflictError(DataspaceDataPlaneService.CLASS_NAME, "tooManyAppsRegistered", dataAssetQuery.datasetId, matchingIds, {
914
858
  datasetId: dataAssetQuery.datasetId
915
859
  });
916
860
  await this._logging?.log({
@@ -949,11 +893,15 @@ export class DataspaceDataPlaneService {
949
893
  * @internal
950
894
  */
951
895
  async prepareForRetry(activityLogEntryId, existingEntry) {
952
- const appsToRetry = existingEntry.inErrorTasks?.map(t => t.dataspaceAppId) ?? [];
896
+ const appsToRetry = existingEntry.tasks
897
+ ?.filter(t => t.status === ActivityTaskStatus.Failed)
898
+ .map(t => t.dataspaceAppId) ?? [];
953
899
  if (!Is.arrayValue(appsToRetry)) {
954
900
  throw new NotFoundError(DataspaceDataPlaneService.CLASS_NAME, "noFailedTasksToRetry", activityLogEntryId);
955
901
  }
956
- const successfulApps = existingEntry.finalizedTasks?.map(t => t.dataspaceAppId) ?? [];
902
+ const successfulApps = existingEntry.tasks
903
+ ?.filter(t => t.status === ActivityTaskStatus.Success)
904
+ .map(t => t.dataspaceAppId) ?? [];
957
905
  await this._logging?.log({
958
906
  level: "debug",
959
907
  source: DataspaceDataPlaneService.CLASS_NAME,
@@ -971,8 +919,6 @@ export class DataspaceDataPlaneService {
971
919
  if (this._retainActivityLogsFor !== -1) {
972
920
  logEntry.retainUntil = Date.now() + this._retainActivityLogsFor;
973
921
  }
974
- // Monitoring purposes
975
- logEntry.retryCount = (logEntry.retryCount ?? 0) + 1;
976
922
  await this._entityStorageActivityLogs.set(logEntry);
977
923
  }
978
924
  return successfulApps;
@@ -1068,5 +1014,183 @@ export class DataspaceDataPlaneService {
1068
1014
  });
1069
1015
  }
1070
1016
  }
1017
+ /**
1018
+ * Processes a task for an activity, by creating a background task and registering the handler.
1019
+ * @param activityLogEntryId The ID of the activity log entry.
1020
+ * @param activity The activity to be processed.
1021
+ * @param handlerApps The handler applications for the activity.
1022
+ * @param dataspaceAppId The ID of the handler application.
1023
+ * @param taskEntries The list of activity log entries.
1024
+ * @param isRetry Indicates if this is a retry of a previous task.
1025
+ * @returns True if the task was processed inline.
1026
+ * @internal
1027
+ */
1028
+ async processTask(activityLogEntryId, activity, handlerApps, dataspaceAppId, taskEntries, isRetry) {
1029
+ const handlerApp = handlerApps[dataspaceAppId].app;
1030
+ const processingGroupId = handlerApps[dataspaceAppId].processingGroupId;
1031
+ const payload = {
1032
+ activityLogEntryId,
1033
+ activity: activity,
1034
+ dataspaceAppId
1035
+ };
1036
+ // If there is no processing group we execute the task inline without creating a background task
1037
+ const isInlineTask = !Is.stringValue(processingGroupId);
1038
+ if (isInlineTask) {
1039
+ const handleActivity = handlerApp?.handleActivity?.bind(handlerApp);
1040
+ if (!Is.function(handleActivity)) {
1041
+ throw new GeneralError(DataspaceDataPlaneService.CLASS_NAME, "missingHandleActivity", {
1042
+ dataspaceAppId
1043
+ });
1044
+ }
1045
+ let taskError;
1046
+ let taskResult;
1047
+ try {
1048
+ taskResult = await handleActivity(activity);
1049
+ }
1050
+ catch (error) {
1051
+ taskError = BaseError.fromError(error);
1052
+ }
1053
+ const now = Date.now();
1054
+ const taskEntry = {
1055
+ taskId: RandomHelper.generateUuidV7("compact"),
1056
+ dataspaceAppId,
1057
+ processingGroupId,
1058
+ result: taskResult,
1059
+ startDate: new Date(now).toISOString(),
1060
+ endDate: new Date(now).toISOString(),
1061
+ status: Is.empty(taskError) ? ActivityTaskStatus.Success : ActivityTaskStatus.Failed,
1062
+ error: taskError
1063
+ };
1064
+ taskEntries.push(taskEntry);
1065
+ await this.notifyTaskStatusChanged(payload.activityLogEntryId, payload.activity.id, payload.dataspaceAppId, taskEntry.taskId, taskEntry.status);
1066
+ }
1067
+ else {
1068
+ const processingGroups = handlerApp.processingGroups?.() ?? {};
1069
+ if (Is.empty(processingGroups[processingGroupId])) {
1070
+ throw new GeneralError(DataspaceDataPlaneService.CLASS_NAME, "invalidProcessingGroupId", {
1071
+ processingGroupId
1072
+ });
1073
+ }
1074
+ const processingGroupOptions = processingGroups[processingGroupId];
1075
+ const taskType = `${dataspaceAppId}${processingGroupId ? `-${processingGroupId}` : ""}`;
1076
+ const taskId = await this._backgroundTaskComponent.create(taskType, payload, {
1077
+ retainFor: this._retainTasksFor,
1078
+ retryCount: processingGroupOptions?.retryCount ?? this._retryCount
1079
+ });
1080
+ if (!this._registeredTaskTypes.includes(taskType)) {
1081
+ this._registeredTaskTypes.push(taskType);
1082
+ await this._backgroundTaskComponent.registerHandler(taskType, "@twin.org/dataspace-app-runner", "appRunner", async (task) => {
1083
+ await this.finaliseBackgroundTask(task.id, task.status, task.payload);
1084
+ }, {
1085
+ maxWorkerCount: processingGroupOptions?.concurrentTasks,
1086
+ idleShutdownTimeout: processingGroupOptions?.idleShutdownTimeout,
1087
+ initialiseMethod: "appRunnerStart",
1088
+ shutdownMethod: "appRunnerEnd"
1089
+ });
1090
+ }
1091
+ taskEntries.push({
1092
+ taskId,
1093
+ dataspaceAppId,
1094
+ processingGroupId,
1095
+ status: ActivityTaskStatus.Pending
1096
+ });
1097
+ await this._logging?.log({
1098
+ level: "info",
1099
+ source: DataspaceDataPlaneService.CLASS_NAME,
1100
+ message: "scheduledTask",
1101
+ data: {
1102
+ taskId,
1103
+ dataspaceAppId,
1104
+ isRetry
1105
+ }
1106
+ });
1107
+ }
1108
+ return isInlineTask;
1109
+ }
1110
+ /**
1111
+ * Constructs the activity log entry with processing status and associated tasks.
1112
+ * @param activityLog The activity log details retrieved from storage.
1113
+ * @param activityTasks The activity tasks associated with the log entry, if any.
1114
+ * @returns The complete activity log entry with status and tasks.
1115
+ * @internal
1116
+ */
1117
+ async constructLogEntry(activityLog, activityTasks) {
1118
+ let tasks;
1119
+ // For calculating the processing status. `Registering` if we cannot determine the activity tasks yet
1120
+ let status = ActivityProcessingStatus.Registering;
1121
+ // Now query the associated tasks
1122
+ // If activity tasks is undefined it is because the corresponding store has not been persisted yet
1123
+ if (!Is.undefined(activityTasks)) {
1124
+ tasks = [];
1125
+ const typeCount = {
1126
+ [TaskStatus.Pending]: 0,
1127
+ [TaskStatus.Processing]: 0,
1128
+ [TaskStatus.Success]: 0,
1129
+ [TaskStatus.Failed]: 0,
1130
+ [TaskStatus.Cancelled]: 0
1131
+ };
1132
+ for (const entity of activityTasks.associatedTasks) {
1133
+ let entry;
1134
+ if (!Is.stringValue(entity.processingGroupId)) {
1135
+ // If there is no process group, the task was processed inline so the task status is already available in the entity
1136
+ typeCount[entity.status]++;
1137
+ entry = entity;
1138
+ }
1139
+ else {
1140
+ const taskDetails = await this._backgroundTaskComponent.get(entity.taskId);
1141
+ if (!Is.empty(taskDetails)) {
1142
+ typeCount[taskDetails.status]++;
1143
+ switch (taskDetails.status) {
1144
+ case TaskStatus.Success:
1145
+ entry = {
1146
+ ...entity,
1147
+ status: ActivityTaskStatus.Success,
1148
+ result: taskDetails.result,
1149
+ startDate: taskDetails?.dateCreated,
1150
+ endDate: taskDetails?.dateCompleted
1151
+ };
1152
+ break;
1153
+ case TaskStatus.Pending:
1154
+ entry = { ...entity, status: ActivityTaskStatus.Pending };
1155
+ break;
1156
+ case TaskStatus.Processing:
1157
+ entry = {
1158
+ ...entity,
1159
+ status: ActivityTaskStatus.Processing,
1160
+ startDate: taskDetails.dateCreated
1161
+ };
1162
+ break;
1163
+ case TaskStatus.Failed:
1164
+ entry = {
1165
+ ...entity,
1166
+ status: ActivityTaskStatus.Failed,
1167
+ error: taskDetails.error
1168
+ };
1169
+ break;
1170
+ case TaskStatus.Cancelled:
1171
+ // Nothing to do for cancelled tasks
1172
+ break;
1173
+ }
1174
+ }
1175
+ }
1176
+ if (!Is.empty(entry)) {
1177
+ tasks.push(entry);
1178
+ }
1179
+ }
1180
+ if (typeCount[TaskStatus.Failed] > 0) {
1181
+ status = ActivityProcessingStatus.Error;
1182
+ }
1183
+ else if (typeCount[TaskStatus.Processing] > 0) {
1184
+ status = ActivityProcessingStatus.Running;
1185
+ }
1186
+ else if (typeCount[TaskStatus.Pending] > 0) {
1187
+ status = ActivityProcessingStatus.Pending;
1188
+ }
1189
+ else {
1190
+ status = ActivityProcessingStatus.Completed;
1191
+ }
1192
+ }
1193
+ return { ...activityLog, status, tasks };
1194
+ }
1071
1195
  }
1072
1196
  //# sourceMappingURL=dataspaceDataPlaneService.js.map