@fluidframework/container-runtime 2.0.0-dev-rc.1.0.0.228517 → 2.0.0-dev-rc.1.0.0.232845

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.
Files changed (149) hide show
  1. package/README.md +1 -1
  2. package/api-report/container-runtime.api.md +13 -3
  3. package/dist/container-runtime-alpha.d.ts +11 -3
  4. package/dist/container-runtime-beta.d.ts +7 -0
  5. package/dist/container-runtime-public.d.ts +7 -0
  6. package/dist/container-runtime-untrimmed.d.ts +28 -3
  7. package/dist/containerRuntime.d.ts +5 -2
  8. package/dist/containerRuntime.d.ts.map +1 -1
  9. package/dist/containerRuntime.js +55 -67
  10. package/dist/containerRuntime.js.map +1 -1
  11. package/dist/dataStoreContext.d.ts +4 -1
  12. package/dist/dataStoreContext.d.ts.map +1 -1
  13. package/dist/dataStoreContext.js +2 -3
  14. package/dist/dataStoreContext.js.map +1 -1
  15. package/dist/dataStores.d.ts +4 -2
  16. package/dist/dataStores.d.ts.map +1 -1
  17. package/dist/dataStores.js +8 -2
  18. package/dist/dataStores.js.map +1 -1
  19. package/dist/gc/garbageCollection.d.ts +17 -1
  20. package/dist/gc/garbageCollection.d.ts.map +1 -1
  21. package/dist/gc/garbageCollection.js +72 -29
  22. package/dist/gc/garbageCollection.js.map +1 -1
  23. package/dist/gc/gcDefinitions.d.ts +18 -2
  24. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  25. package/dist/gc/gcDefinitions.js +5 -1
  26. package/dist/gc/gcDefinitions.js.map +1 -1
  27. package/dist/gc/gcTelemetry.d.ts +1 -0
  28. package/dist/gc/gcTelemetry.d.ts.map +1 -1
  29. package/dist/gc/gcTelemetry.js +0 -2
  30. package/dist/gc/gcTelemetry.js.map +1 -1
  31. package/dist/gc/gcUnreferencedStateTracker.d.ts +5 -0
  32. package/dist/gc/gcUnreferencedStateTracker.d.ts.map +1 -1
  33. package/dist/gc/gcUnreferencedStateTracker.js +12 -1
  34. package/dist/gc/gcUnreferencedStateTracker.js.map +1 -1
  35. package/dist/gc/index.d.ts +1 -1
  36. package/dist/gc/index.d.ts.map +1 -1
  37. package/dist/gc/index.js +2 -1
  38. package/dist/gc/index.js.map +1 -1
  39. package/dist/index.d.ts +2 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +3 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/messageTypes.d.ts +1 -1
  44. package/dist/messageTypes.js.map +1 -1
  45. package/dist/packageVersion.d.ts +1 -1
  46. package/dist/packageVersion.js +1 -1
  47. package/dist/packageVersion.js.map +1 -1
  48. package/dist/summary/runningSummarizer.d.ts +6 -6
  49. package/dist/summary/runningSummarizer.d.ts.map +1 -1
  50. package/dist/summary/runningSummarizer.js +90 -72
  51. package/dist/summary/runningSummarizer.js.map +1 -1
  52. package/dist/summary/summarizerNode/summarizerNode.d.ts +2 -3
  53. package/dist/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  54. package/dist/summary/summarizerNode/summarizerNode.js +6 -48
  55. package/dist/summary/summarizerNode/summarizerNode.js.map +1 -1
  56. package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts +1 -18
  57. package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  58. package/dist/summary/summarizerNode/summarizerNodeUtils.js +1 -21
  59. package/dist/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  60. package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts +3 -3
  61. package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
  62. package/dist/summary/summarizerNode/summarizerNodeWithGc.js +5 -5
  63. package/dist/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
  64. package/dist/summary/summarizerTypes.d.ts +1 -1
  65. package/dist/summary/summarizerTypes.d.ts.map +1 -1
  66. package/dist/summary/summarizerTypes.js.map +1 -1
  67. package/dist/tsdoc-metadata.json +1 -1
  68. package/lib/container-runtime-alpha.d.mts +11 -3
  69. package/lib/container-runtime-beta.d.mts +7 -0
  70. package/lib/container-runtime-public.d.mts +7 -0
  71. package/lib/container-runtime-untrimmed.d.mts +28 -3
  72. package/lib/containerRuntime.d.mts +5 -2
  73. package/lib/containerRuntime.d.mts.map +1 -1
  74. package/lib/containerRuntime.mjs +56 -68
  75. package/lib/containerRuntime.mjs.map +1 -1
  76. package/lib/dataStoreContext.d.mts +4 -1
  77. package/lib/dataStoreContext.d.mts.map +1 -1
  78. package/lib/dataStoreContext.mjs +2 -3
  79. package/lib/dataStoreContext.mjs.map +1 -1
  80. package/lib/dataStores.d.mts +4 -2
  81. package/lib/dataStores.d.mts.map +1 -1
  82. package/lib/dataStores.mjs +8 -2
  83. package/lib/dataStores.mjs.map +1 -1
  84. package/lib/gc/garbageCollection.d.mts +17 -1
  85. package/lib/gc/garbageCollection.d.mts.map +1 -1
  86. package/lib/gc/garbageCollection.mjs +74 -31
  87. package/lib/gc/garbageCollection.mjs.map +1 -1
  88. package/lib/gc/gcDefinitions.d.mts +18 -2
  89. package/lib/gc/gcDefinitions.d.mts.map +1 -1
  90. package/lib/gc/gcDefinitions.mjs +4 -0
  91. package/lib/gc/gcDefinitions.mjs.map +1 -1
  92. package/lib/gc/gcTelemetry.d.mts +1 -0
  93. package/lib/gc/gcTelemetry.d.mts.map +1 -1
  94. package/lib/gc/gcTelemetry.mjs +0 -2
  95. package/lib/gc/gcTelemetry.mjs.map +1 -1
  96. package/lib/gc/gcUnreferencedStateTracker.d.mts +5 -0
  97. package/lib/gc/gcUnreferencedStateTracker.d.mts.map +1 -1
  98. package/lib/gc/gcUnreferencedStateTracker.mjs +10 -0
  99. package/lib/gc/gcUnreferencedStateTracker.mjs.map +1 -1
  100. package/lib/gc/index.d.mts +1 -1
  101. package/lib/gc/index.d.mts.map +1 -1
  102. package/lib/gc/index.mjs +1 -1
  103. package/lib/gc/index.mjs.map +1 -1
  104. package/lib/index.d.mts +2 -1
  105. package/lib/index.d.mts.map +1 -1
  106. package/lib/index.mjs +1 -0
  107. package/lib/index.mjs.map +1 -1
  108. package/lib/messageTypes.d.mts +1 -1
  109. package/lib/messageTypes.mjs.map +1 -1
  110. package/lib/packageVersion.d.mts +1 -1
  111. package/lib/packageVersion.mjs +1 -1
  112. package/lib/packageVersion.mjs.map +1 -1
  113. package/lib/summary/runningSummarizer.d.mts +6 -6
  114. package/lib/summary/runningSummarizer.d.mts.map +1 -1
  115. package/lib/summary/runningSummarizer.mjs +90 -72
  116. package/lib/summary/runningSummarizer.mjs.map +1 -1
  117. package/lib/summary/summarizerNode/summarizerNode.d.mts +2 -3
  118. package/lib/summary/summarizerNode/summarizerNode.d.mts.map +1 -1
  119. package/lib/summary/summarizerNode/summarizerNode.mjs +8 -50
  120. package/lib/summary/summarizerNode/summarizerNode.mjs.map +1 -1
  121. package/lib/summary/summarizerNode/summarizerNodeUtils.d.mts +1 -18
  122. package/lib/summary/summarizerNode/summarizerNodeUtils.d.mts.map +1 -1
  123. package/lib/summary/summarizerNode/summarizerNodeUtils.mjs +0 -19
  124. package/lib/summary/summarizerNode/summarizerNodeUtils.mjs.map +1 -1
  125. package/lib/summary/summarizerNode/summarizerNodeWithGc.d.mts +3 -3
  126. package/lib/summary/summarizerNode/summarizerNodeWithGc.d.mts.map +1 -1
  127. package/lib/summary/summarizerNode/summarizerNodeWithGc.mjs +5 -5
  128. package/lib/summary/summarizerNode/summarizerNodeWithGc.mjs.map +1 -1
  129. package/lib/summary/summarizerTypes.d.mts +1 -1
  130. package/lib/summary/summarizerTypes.d.mts.map +1 -1
  131. package/lib/summary/summarizerTypes.mjs.map +1 -1
  132. package/package.json +27 -21
  133. package/src/containerRuntime.ts +96 -85
  134. package/src/dataStoreContext.ts +6 -4
  135. package/src/dataStores.ts +8 -1
  136. package/src/gc/garbageCollection.ts +86 -30
  137. package/src/gc/gcDefinitions.ts +19 -3
  138. package/src/gc/gcTelemetry.ts +1 -2
  139. package/src/gc/gcUnreferencedStateTracker.ts +11 -0
  140. package/src/gc/index.ts +1 -0
  141. package/src/index.ts +2 -0
  142. package/src/messageTypes.ts +1 -1
  143. package/src/packageVersion.ts +1 -1
  144. package/src/summary/runningSummarizer.ts +116 -88
  145. package/src/summary/summarizerNode/summarizerNode.ts +4 -64
  146. package/src/summary/summarizerNode/summarizerNodeUtils.ts +2 -33
  147. package/src/summary/summarizerNode/summarizerNodeWithGc.ts +0 -6
  148. package/src/summary/summarizerTypes.ts +1 -1
  149. /package/{.eslintrc.js → .eslintrc.cjs} +0 -0
@@ -11,6 +11,7 @@ import {
11
11
  IRequest,
12
12
  IResponse,
13
13
  IProvideFluidHandleContext,
14
+ ISignalEnvelope,
14
15
  } from "@fluidframework/core-interfaces";
15
16
  import {
16
17
  IAudience,
@@ -46,12 +47,13 @@ import {
46
47
  ITelemetryLoggerExt,
47
48
  UsageError,
48
49
  LoggingError,
50
+ createSampledLogger,
51
+ IEventSampler,
49
52
  } from "@fluidframework/telemetry-utils";
50
53
  import {
51
54
  DriverHeader,
52
55
  FetchSource,
53
56
  IDocumentStorageService,
54
- ISummaryContext,
55
57
  } from "@fluidframework/driver-definitions";
56
58
  import { readAndParse } from "@fluidframework/driver-utils";
57
59
  import {
@@ -77,7 +79,6 @@ import {
77
79
  IGarbageCollectionData,
78
80
  IEnvelope,
79
81
  IInboundSignalMessage,
80
- ISignalEnvelope,
81
82
  NamedFluidDataStoreRegistryEntries,
82
83
  ISummaryTreeWithStats,
83
84
  ISummarizeInternalResult,
@@ -862,14 +863,34 @@ export class ContainerRuntime
862
863
  "@fluidframework/id-compressor"
863
864
  );
864
865
 
865
- const pendingLocalState = context.pendingLocalState as IPendingRuntimeState;
866
+ /**
867
+ * Because the IdCompressor emits so much telemetry, this function is used to sample
868
+ * approximately 5% of all clients. Only the given percentage of sessions will emit telemetry.
869
+ */
870
+ const idCompressorEventSampler: IEventSampler = (() => {
871
+ const isIdCompressorTelemetryEnabled = Math.random() < 0.05;
872
+ return {
873
+ sample: () => {
874
+ return isIdCompressorTelemetryEnabled;
875
+ },
876
+ };
877
+ })();
866
878
 
879
+ const compressorLogger = createSampledLogger(logger, idCompressorEventSampler);
880
+ const pendingLocalState = context.pendingLocalState as IPendingRuntimeState;
867
881
  if (pendingLocalState?.pendingIdCompressorState !== undefined) {
868
- idCompressor = deserializeIdCompressor(pendingLocalState.pendingIdCompressorState);
882
+ idCompressor = deserializeIdCompressor(
883
+ pendingLocalState.pendingIdCompressorState,
884
+ compressorLogger,
885
+ );
869
886
  } else if (serializedIdCompressor !== undefined) {
870
- idCompressor = deserializeIdCompressor(serializedIdCompressor, createSessionId());
887
+ idCompressor = deserializeIdCompressor(
888
+ serializedIdCompressor,
889
+ createSessionId(),
890
+ compressorLogger,
891
+ );
871
892
  } else {
872
- idCompressor = createIdCompressor(logger);
893
+ idCompressor = createIdCompressor(compressorLogger);
873
894
  }
874
895
  }
875
896
 
@@ -1371,15 +1392,17 @@ export class ContainerRuntime
1371
1392
 
1372
1393
  const pendingRuntimeState = pendingLocalState as IPendingRuntimeState | undefined;
1373
1394
 
1374
- const maxSnapshotCacheDurationMs = this._storage?.policies?.maximumCacheDurationMs;
1375
- if (
1376
- maxSnapshotCacheDurationMs !== undefined &&
1377
- maxSnapshotCacheDurationMs > 5 * 24 * 60 * 60 * 1000
1378
- ) {
1379
- // This is a runtime enforcement of what's already explicit in the policy's type itself,
1380
- // which dictates the value is either undefined or exactly 5 days in ms.
1381
- // As long as the actual value is less than 5 days, the assumptions GC makes here are valid.
1382
- throw new UsageError("Driver's maximumCacheDurationMs policy cannot exceed 5 days");
1395
+ if (context.attachState === AttachState.Attached) {
1396
+ const maxSnapshotCacheDurationMs = this._storage?.policies?.maximumCacheDurationMs;
1397
+ if (
1398
+ maxSnapshotCacheDurationMs !== undefined &&
1399
+ maxSnapshotCacheDurationMs > 5 * 24 * 60 * 60 * 1000
1400
+ ) {
1401
+ // This is a runtime enforcement of what's already explicit in the policy's type itself,
1402
+ // which dictates the value is either undefined or exactly 5 days in ms.
1403
+ // As long as the actual value is less than 5 days, the assumptions GC makes here are valid.
1404
+ throw new UsageError("Driver's maximumCacheDurationMs policy cannot exceed 5 days");
1405
+ }
1383
1406
  }
1384
1407
 
1385
1408
  this.garbageCollector = GarbageCollector.create({
@@ -1411,9 +1434,6 @@ export class ContainerRuntime
1411
1434
  // Must set to false to prevent sending summary handle which would be pointing to
1412
1435
  // a summary with an older protocol state.
1413
1436
  canReuseHandle: false,
1414
- // Must set to true to throw on any data stores failure that was too severe to be handled.
1415
- // We also are not decoding the base summaries at the root.
1416
- throwOnFailure: true,
1417
1437
  // If GC should not run, let the summarizer node know so that it does not track GC state.
1418
1438
  gcDisabled: !this.garbageCollector.shouldRunGC,
1419
1439
  },
@@ -2054,6 +2074,8 @@ export class ContainerRuntime
2054
2074
  this.closeFn(error);
2055
2075
  throw error;
2056
2076
  }
2077
+ // Note: Even if its compat behavior allows it, we don't know how to apply this stashed op.
2078
+ // All we can do is ignore it (similar to on process).
2057
2079
  }
2058
2080
  }
2059
2081
  }
@@ -2408,6 +2430,9 @@ export class ContainerRuntime
2408
2430
  assert(this.outbox.isEmpty, 0x3cf /* reentrancy */);
2409
2431
  }
2410
2432
 
2433
+ /**
2434
+ * {@inheritDoc @fluidframework/runtime-definitions#IContainerRuntimeBase.orderSequentially}
2435
+ */
2411
2436
  public orderSequentially<T>(callback: () => T): T {
2412
2437
  let checkpoint: IBatchCheckpoint | undefined;
2413
2438
  let result: T;
@@ -2439,9 +2464,21 @@ export class ContainerRuntime
2439
2464
  throw error2;
2440
2465
  }
2441
2466
  } else {
2442
- // pre-0.58 error message: orderSequentiallyCallbackException
2443
- this.closeFn(new GenericError("orderSequentially callback exception", error));
2467
+ this.closeFn(
2468
+ wrapError(
2469
+ error,
2470
+ (errorMessage) =>
2471
+ new GenericError(
2472
+ `orderSequentially callback exception: ${errorMessage}`,
2473
+ error,
2474
+ {
2475
+ orderSequentiallyCalls: this._orderSequentiallyCalls,
2476
+ },
2477
+ ),
2478
+ ),
2479
+ );
2444
2480
  }
2481
+
2445
2482
  throw error; // throw the original error for the consumer of the runtime
2446
2483
  } finally {
2447
2484
  this._orderSequentiallyCalls--;
@@ -2496,15 +2533,23 @@ export class ContainerRuntime
2496
2533
  return this.dataStores.createDetachedDataStoreCore(pkg, true, rootDataStoreId);
2497
2534
  }
2498
2535
 
2499
- public createDetachedDataStore(pkg: Readonly<string[]>): IFluidDataStoreContextDetached {
2500
- return this.dataStores.createDetachedDataStoreCore(pkg, false);
2536
+ public createDetachedDataStore(
2537
+ pkg: Readonly<string[]>,
2538
+ groupId?: string,
2539
+ ): IFluidDataStoreContextDetached {
2540
+ return this.dataStores.createDetachedDataStoreCore(pkg, false, undefined, groupId);
2501
2541
  }
2502
2542
 
2503
- public async createDataStore(pkg: string | string[]): Promise<IDataStore> {
2543
+ public async createDataStore(pkg: string | string[], groupId?: string): Promise<IDataStore> {
2504
2544
  const id = uuid();
2505
2545
  return channelToDataStore(
2506
2546
  await this.dataStores
2507
- ._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id)
2547
+ ._createFluidDataStoreContext(
2548
+ Array.isArray(pkg) ? pkg : [pkg],
2549
+ id,
2550
+ undefined,
2551
+ groupId,
2552
+ )
2508
2553
  .realize(),
2509
2554
  id,
2510
2555
  this,
@@ -2905,6 +2950,12 @@ export class ContainerRuntime
2905
2950
  * data store or an attachment blob.
2906
2951
  */
2907
2952
  public async getGCNodePackagePath(nodePath: string): Promise<readonly string[] | undefined> {
2953
+ // GC uses "/" when adding "root" references, e.g. for Aliasing or as part of Tombstone Auto-Recovery.
2954
+ // These have no package path so return a special value.
2955
+ if (nodePath === "/") {
2956
+ return ["<GCROOT>"];
2957
+ }
2958
+
2908
2959
  switch (this.getNodeType(nodePath)) {
2909
2960
  case GCNodeType.Blob:
2910
2961
  return [BlobManager.basePath];
@@ -3000,7 +3051,6 @@ export class ContainerRuntime
3000
3051
  assert(this.outbox.isEmpty, 0x3d1 /* Can't trigger summary in the middle of a batch */);
3001
3052
 
3002
3053
  // We close the summarizer and download a new snapshot and reload the container
3003
- let latestSnapshotVersionId: string | undefined;
3004
3054
  if (refreshLatestAck === true) {
3005
3055
  return this.prefetchLatestSummaryThenClose(
3006
3056
  createChildLogger({
@@ -3216,34 +3266,18 @@ export class ContainerRuntime
3216
3266
  return { stage: "generate", ...generateSummaryData, error: continueResult.error };
3217
3267
  }
3218
3268
 
3219
- // It may happen that the lastAck it not correct due to missing summaryAck in case of single commit
3220
- // summary. So if the previous summarizer closes just after submitting the summary and before
3221
- // submitting the summaryOp then we can't rely on summaryAck. So in case we have
3222
- // latestSnapshotVersionId from storage and it does not match with the lastAck ackHandle, then use
3223
- // the one fetched from storage as parent as that is the latest.
3224
- let summaryContext: ISummaryContext;
3225
- if (
3226
- lastAck?.summaryAck.contents.handle !== latestSnapshotVersionId &&
3227
- latestSnapshotVersionId !== undefined
3228
- ) {
3229
- summaryContext = {
3230
- proposalHandle: undefined,
3231
- ackHandle: latestSnapshotVersionId,
3232
- referenceSequenceNumber: summaryRefSeqNum,
3233
- };
3234
- } else if (lastAck === undefined) {
3235
- summaryContext = {
3236
- proposalHandle: undefined,
3237
- ackHandle: this.loadedFromVersionId,
3238
- referenceSequenceNumber: summaryRefSeqNum,
3239
- };
3240
- } else {
3241
- summaryContext = {
3242
- proposalHandle: lastAck.summaryOp.contents.handle,
3243
- ackHandle: lastAck.summaryAck.contents.handle,
3244
- referenceSequenceNumber: summaryRefSeqNum,
3245
- };
3246
- }
3269
+ const summaryContext =
3270
+ lastAck === undefined
3271
+ ? {
3272
+ proposalHandle: undefined,
3273
+ ackHandle: this.loadedFromVersionId,
3274
+ referenceSequenceNumber: summaryRefSeqNum,
3275
+ }
3276
+ : {
3277
+ proposalHandle: lastAck.summaryOp.contents.handle,
3278
+ ackHandle: lastAck.summaryAck.contents.handle,
3279
+ referenceSequenceNumber: summaryRefSeqNum,
3280
+ };
3247
3281
 
3248
3282
  let handle: string;
3249
3283
  try {
@@ -3687,6 +3721,7 @@ export class ContainerRuntime
3687
3721
  localOpMetadata: unknown,
3688
3722
  opMetadata: Record<string, unknown> | undefined,
3689
3723
  ) {
3724
+ assert(!this.isSummarizerClient, "Summarizer never reconnects so should never resubmit");
3690
3725
  switch (message.type) {
3691
3726
  case ContainerMessageType.FluidDataStoreOp:
3692
3727
  // For Operations, call resubmitDataStoreOp which will find the right store
@@ -3708,13 +3743,14 @@ export class ContainerRuntime
3708
3743
  this.submit(message);
3709
3744
  break;
3710
3745
  case ContainerMessageType.GC:
3711
- // GC op is only sent in summarizer which should never reconnect.
3712
- throw new LoggingError("GC op not expected to be resubmitted in summarizer");
3746
+ this.submit(message);
3747
+ break;
3713
3748
  default: {
3714
3749
  // This case should be very rare - it would imply an op was stashed from a
3715
- // future version of runtime code and now is being applied on an older version
3750
+ // future version of runtime code and now is being applied on an older version.
3716
3751
  const compatBehavior = message.compatDetails?.behavior;
3717
3752
  if (compatBehaviorAllowsMessageType(message.type, compatBehavior)) {
3753
+ // We do not ultimately resubmit it, to be consistent with this version of the code.
3718
3754
  this.logger.sendTelemetryEvent({
3719
3755
  eventName: "resubmitUnrecognizedMessageTypeAllowed",
3720
3756
  messageDetails: { type: message.type, compatBehavior },
@@ -3771,7 +3807,7 @@ export class ContainerRuntime
3771
3807
  * and then close as the current main client is likely to be re-elected as the parent summarizer again.
3772
3808
  */
3773
3809
  if (!result.isSummaryTracked && result.isSummaryNewer) {
3774
- const fetchResult = await this.fetchLatestSnapshotFromStorage(
3810
+ await this.fetchLatestSnapshotFromStorage(
3775
3811
  summaryLogger,
3776
3812
  {
3777
3813
  eventName: "RefreshLatestSummaryAckFetch",
@@ -3781,32 +3817,7 @@ export class ContainerRuntime
3781
3817
  readAndParseBlob,
3782
3818
  );
3783
3819
 
3784
- /**
3785
- * If the fetched snapshot is older than the one for which the ack was received, close the container.
3786
- * This should never happen because an ack should be sent after the latest summary is updated in the server.
3787
- * However, there are couple of scenarios where it's possible:
3788
- * 1. A file was modified externally resulting in modifying the snapshot's sequence number. This can lead to
3789
- * the document being unusable and we should not proceed.
3790
- * 2. The server DB failed after the ack was sent which may delete the corresponding snapshot. Ideally, in
3791
- * such cases, the file will be rolled back along with the ack and we will eventually reach a consistent
3792
- * state.
3793
- */
3794
- if (fetchResult.latestSnapshotRefSeq < summaryRefSeq) {
3795
- const error = DataProcessingError.create(
3796
- "Fetched snapshot is older than the received ack",
3797
- "RefreshLatestSummaryAck",
3798
- undefined /* sequencedMessage */,
3799
- {
3800
- ackHandle,
3801
- summaryRefSeq,
3802
- fetchedSnapshotRefSeq: fetchResult.latestSnapshotRefSeq,
3803
- },
3804
- );
3805
- this.disposeFn(error);
3806
- throw error;
3807
- }
3808
-
3809
- await this.closeStaleSummarizer("RefreshLatestSummaryAckFetch");
3820
+ await this.closeStaleSummarizer();
3810
3821
  return;
3811
3822
  }
3812
3823
 
@@ -3835,7 +3846,7 @@ export class ContainerRuntime
3835
3846
  readAndParseBlob,
3836
3847
  );
3837
3848
 
3838
- await this.closeStaleSummarizer("RefreshLatestSummaryFromServerFetch");
3849
+ await this.closeStaleSummarizer();
3839
3850
 
3840
3851
  return {
3841
3852
  stage: "base",
@@ -3845,7 +3856,7 @@ export class ContainerRuntime
3845
3856
  };
3846
3857
  }
3847
3858
 
3848
- private async closeStaleSummarizer(codePath: string): Promise<void> {
3859
+ private async closeStaleSummarizer(): Promise<void> {
3849
3860
  // Delay before restarting summarizer to prevent the summarizer from restarting too frequently.
3850
3861
  await delay(this.closeSummarizerDelayMs);
3851
3862
  this._summarizer?.stop("latestSummaryStateStale");
@@ -50,8 +50,6 @@ import {
50
50
  ISummarizerNodeWithGC,
51
51
  SummarizeInternalFn,
52
52
  ITelemetryContext,
53
- IIdCompressor,
54
- IIdCompressorCore,
55
53
  VisibilityState,
56
54
  } from "@fluidframework/runtime-definitions";
57
55
  import { addBlobToSummary, convertSummaryTreeToITree } from "@fluidframework/runtime-utils";
@@ -67,6 +65,7 @@ import {
67
65
  tagCodeArtifacts,
68
66
  ThresholdCounter,
69
67
  } from "@fluidframework/telemetry-utils";
68
+ import { IIdCompressor, IIdCompressorCore } from "@fluidframework/id-compressor";
70
69
  import {
71
70
  dataStoreAttributesBlobName,
72
71
  hasIsolatedChannels,
@@ -115,6 +114,7 @@ export interface IFluidDataStoreContextProps {
115
114
  readonly scope: FluidObject;
116
115
  readonly createSummarizerNodeFn: CreateChildSummarizerNodeFn;
117
116
  readonly pkg?: Readonly<string[]>;
117
+ readonly groupId?: string;
118
118
  }
119
119
 
120
120
  /** Properties necessary for creating a local FluidDataStoreContext */
@@ -272,6 +272,8 @@ export abstract class FluidDataStoreContext
272
272
  private readonly _containerRuntime: ContainerRuntime;
273
273
  public readonly storage: IDocumentStorageService;
274
274
  public readonly scope: FluidObject;
275
+ // Represents the group to which the data store belongs too.
276
+ public readonly groupId: string | undefined;
275
277
  protected pkg?: readonly string[];
276
278
 
277
279
  constructor(
@@ -287,6 +289,7 @@ export abstract class FluidDataStoreContext
287
289
  this.storage = props.storage;
288
290
  this.scope = props.scope;
289
291
  this.pkg = props.pkg;
292
+ this.groupId = props.groupId;
290
293
 
291
294
  // URIs use slashes as delimiters. Handles use URIs.
292
295
  // Thus having slashes in types almost guarantees trouble down the road!
@@ -948,8 +951,7 @@ export abstract class FluidDataStoreContext
948
951
  summarizeInternal,
949
952
  id,
950
953
  createParam,
951
- // DDS will not create failure summaries
952
- { throwOnFailure: true },
954
+ undefined /* config */,
953
955
  getGCDataFn,
954
956
  );
955
957
  }
package/src/dataStores.ts CHANGED
@@ -153,6 +153,7 @@ export class DataStores implements IDisposable {
153
153
  createSummarizerNodeFn: this.getCreateChildSummarizerNodeFn(key, {
154
154
  type: CreateSummarizerNodeSource.FromSummary,
155
155
  }),
156
+ groupId: value.groupId,
156
157
  });
157
158
  } else {
158
159
  if (typeof value !== "object") {
@@ -239,6 +240,7 @@ export class DataStores implements IDisposable {
239
240
  runtime: this.runtime,
240
241
  storage: new StorageServiceWithAttachBlobs(this.runtime.storage, flatAttachBlobs),
241
242
  scope: this.runtime.scope,
243
+ groupId: snapshotTree?.groupId,
242
244
  createSummarizerNodeFn: this.getCreateChildSummarizerNodeFn(attachMessage.id, {
243
245
  type: CreateSummarizerNodeSource.FromAttach,
244
246
  sequenceNumber: message.sequenceNumber,
@@ -344,6 +346,7 @@ export class DataStores implements IDisposable {
344
346
  pkg: Readonly<string[]>,
345
347
  isRoot: boolean,
346
348
  id = uuid(),
349
+ groupId?: string,
347
350
  ): IFluidDataStoreContextDetached {
348
351
  assert(!id.includes("/"), 0x30c /* Id cannot contain slashes */);
349
352
 
@@ -359,12 +362,13 @@ export class DataStores implements IDisposable {
359
362
  makeLocallyVisibleFn: () => this.makeDataStoreLocallyVisible(id),
360
363
  snapshotTree: undefined,
361
364
  isRootDataStore: isRoot,
365
+ groupId,
362
366
  });
363
367
  this.contexts.addUnbound(context);
364
368
  return context;
365
369
  }
366
370
 
367
- public _createFluidDataStoreContext(pkg: string[], id: string, props?: any) {
371
+ public _createFluidDataStoreContext(pkg: string[], id: string, props?: any, groupId?: string) {
368
372
  assert(!id.includes("/"), 0x30d /* Id cannot contain slashes */);
369
373
  const context = new LocalFluidDataStoreContext({
370
374
  id,
@@ -379,6 +383,7 @@ export class DataStores implements IDisposable {
379
383
  snapshotTree: undefined,
380
384
  isRootDataStore: false,
381
385
  createProps: props,
386
+ groupId,
382
387
  });
383
388
  this.contexts.addUnbound(context);
384
389
  return context;
@@ -962,6 +967,8 @@ export function getSummaryForDatastores(
962
967
 
963
968
  /**
964
969
  * Traverse this op's contents and detect any outbound routes that were added by this op.
970
+ *
971
+ * @internal
965
972
  */
966
973
  export function detectOutboundReferences(
967
974
  envelope: IEnvelope,
@@ -46,6 +46,7 @@ import {
46
46
  ISweepPhaseStats,
47
47
  GarbageCollectionMessage,
48
48
  GarbageCollectionMessageType,
49
+ disableAutoRecoveryKey,
49
50
  } from "./gcDefinitions";
50
51
  import {
51
52
  cloneGCData,
@@ -56,7 +57,10 @@ import {
56
57
  import { runGarbageCollection } from "./gcReferenceGraphAlgorithm";
57
58
  import { IGarbageCollectionSnapshotData, IGarbageCollectionState } from "./gcSummaryDefinitions";
58
59
  import { GCSummaryStateTracker } from "./gcSummaryStateTracker";
59
- import { UnreferencedStateTracker } from "./gcUnreferencedStateTracker";
60
+ import {
61
+ UnreferencedStateTracker,
62
+ UnreferencedStateTrackerMap,
63
+ } from "./gcUnreferencedStateTracker";
60
64
  import { GCTelemetryTracker } from "./gcTelemetry";
61
65
 
62
66
  /**
@@ -110,8 +114,15 @@ export class GarbageCollector implements IGarbageCollector {
110
114
  private readonly initializeGCStateFromBaseSnapshotP: Promise<void>;
111
115
  // The GC details generated from the base snapshot.
112
116
  private readonly baseGCDetailsP: Promise<IGarbageCollectionDetailsBase>;
113
- // Map of node ids to their unreferenced state tracker.
114
- private readonly unreferencedNodesState: Map<string, UnreferencedStateTracker> = new Map();
117
+
118
+ /**
119
+ * Map of node ids to their unreferenced state tracker
120
+ * NOTE: The set of keys in this map is considered as the set of unreferenced nodes
121
+ * as of the last GC run. So in between runs, nothing should be added or removed.
122
+ */
123
+ private readonly unreferencedNodesState: UnreferencedStateTrackerMap =
124
+ new UnreferencedStateTrackerMap();
125
+
115
126
  // The Timer responsible for closing the container when the session has expired
116
127
  private sessionExpiryTimer: Timer | undefined;
117
128
 
@@ -592,16 +603,10 @@ export class GarbageCollector implements IGarbageCollector {
592
603
  ): { tombstoneReadyNodeIds: Set<string>; sweepReadyNodeIds: Set<string> } {
593
604
  // 1. Marks all referenced nodes by clearing their unreferenced tracker, if any.
594
605
  for (const nodeId of allReferencedNodeIds) {
595
- const nodeStateTracker = this.unreferencedNodesState.get(nodeId);
596
- if (nodeStateTracker !== undefined) {
597
- // Stop tracking so as to clear out any running timers.
598
- nodeStateTracker.stopTracking();
599
- // Delete the node as we don't need to track it any more.
600
- this.unreferencedNodesState.delete(nodeId);
601
- }
606
+ this.unreferencedNodesState.delete(nodeId);
602
607
  }
603
608
 
604
- // 2. Mark unreferenced nodes in this run by starting unreferenced tracking for them.
609
+ // 2. Mark unreferenced nodes in this run by starting or updating unreferenced tracking for them.
605
610
  const tombstoneReadyNodeIds: Set<string> = new Set();
606
611
  const sweepReadyNodeIds: Set<string> = new Set();
607
612
  for (const nodeId of gcResult.deletedNodeIds) {
@@ -856,21 +861,33 @@ export class GarbageCollector implements IGarbageCollector {
856
861
  * @param local - Whether it was send by this client.
857
862
  */
858
863
  public processMessage(message: ContainerRuntimeGCMessage, local: boolean) {
859
- switch (message.contents.type) {
860
- case "Sweep": {
864
+ const gcMessageType = message.contents.type;
865
+ switch (gcMessageType) {
866
+ case GarbageCollectionMessageType.Sweep: {
861
867
  // Delete the nodes whose ids are present in the contents.
862
868
  this.deleteSweepReadyNodes(message.contents.deletedNodeIds);
863
869
  break;
864
870
  }
871
+ case GarbageCollectionMessageType.TombstoneLoaded: {
872
+ if (this.mc.config.getBoolean(disableAutoRecoveryKey) === true) {
873
+ break;
874
+ }
875
+
876
+ // Mark the node as referenced to ensure it isn't Swept
877
+ const tombstonedNodePath = message.contents.nodePath;
878
+ this.addedOutboundReference("/", tombstonedNodePath);
879
+
880
+ break;
881
+ }
865
882
  default: {
866
883
  if (
867
884
  !compatBehaviorAllowsGCMessageType(
868
- message.contents.type,
885
+ gcMessageType,
869
886
  message.compatDetails?.behavior,
870
887
  )
871
888
  ) {
872
889
  const error = DataProcessingError.create(
873
- `Garbage collection message of unknown type ${message.contents.type}`,
890
+ `Garbage collection message of unknown type ${gcMessageType}`,
874
891
  "processMessage",
875
892
  );
876
893
  throw error;
@@ -911,13 +928,9 @@ export class GarbageCollector implements IGarbageCollector {
911
928
 
912
929
  // Clear unreferenced state tracking for deleted nodes.
913
930
  for (const nodeId of deletedNodeIds) {
914
- const nodeStateTracker = this.unreferencedNodesState.get(nodeId);
915
- if (nodeStateTracker !== undefined) {
916
- // Stop tracking so as to clear out any running timers.
917
- nodeStateTracker.stopTracking();
918
- // Delete the node as we don't need to track it any more.
919
- this.unreferencedNodesState.delete(nodeId);
920
- }
931
+ // Usually we avoid modifying the set of unreferencedNodesState keys in between GC runs,
932
+ // but this is ok since this node won't exist at all in the next GC run.
933
+ this.unreferencedNodesState.delete(nodeId);
921
934
  this.deletedNodes.add(nodeId);
922
935
  }
923
936
  }
@@ -959,6 +972,15 @@ export class GarbageCollector implements IGarbageCollector {
959
972
  headers: headerData,
960
973
  });
961
974
 
975
+ // Any time we log a Tombstone Loaded error (via Telemetry Tracker),
976
+ // we want to also trigger autorecovery to avoid the object being deleted
977
+ // Note: We don't need to trigger on "Changed" because any change will cause the object
978
+ // to be loaded by the Summarizer, and auto-recovery will be triggered then.
979
+ if (isTombstoned && reason === "Loaded") {
980
+ // Note that when a DataStore and its DDS are all loaded, each will trigger AutoRecovery for itself.
981
+ this.triggerAutoRecovery(nodePath);
982
+ }
983
+
962
984
  const nodeType = this.runtime.getNodeType(nodePath);
963
985
 
964
986
  // Unless this is a Loaded event for a Blob or DataStore, we're done after telemetry tracking
@@ -967,7 +989,6 @@ export class GarbageCollector implements IGarbageCollector {
967
989
  }
968
990
 
969
991
  const errorRequest: IRequest = request ?? { url: nodePath };
970
- // If the object is tombstoned and tombstone enforcement is configured, throw an error.
971
992
  if (isTombstoned && this.throwOnTombstoneLoad && headerData?.allowTombstone !== true) {
972
993
  // The requested data store is removed by gc. Create a 404 gc response exception.
973
994
  throw responseToException(
@@ -995,14 +1016,42 @@ export class GarbageCollector implements IGarbageCollector {
995
1016
  }
996
1017
  }
997
1018
 
1019
+ /**
1020
+ * The given node should have its unreferenced state reset in the next GC,
1021
+ * even if the true GC graph shows it is unreferenced. This will
1022
+ * prevent it from being deleted by Sweep (after the Grace Period).
1023
+ *
1024
+ * Submit a GC op indicating that the Tombstone with the given path has been loaded.
1025
+ * Broadcasting this information in the op stream allows the Summarizer to reset unreferenced state
1026
+ * before runnint GC next.
1027
+ */
1028
+ private triggerAutoRecovery(nodePath: string) {
1029
+ if (this.mc.config.getBoolean(disableAutoRecoveryKey) === true) {
1030
+ return;
1031
+ }
1032
+
1033
+ // Use compat behavior "Ignore" since this is an optimization to opportunistically protect
1034
+ // objects from deletion, so it's fine for older clients to ignore this op.
1035
+ const containerGCMessage: ContainerRuntimeGCMessage = {
1036
+ type: ContainerMessageType.GC,
1037
+ contents: {
1038
+ type: "TombstoneLoaded",
1039
+ nodePath,
1040
+ },
1041
+ compatDetails: { behavior: "Ignore" },
1042
+ };
1043
+ this.submitMessage(containerGCMessage);
1044
+ }
1045
+
998
1046
  /**
999
1047
  * Called when an outbound reference is added to a node. This is used to identify all nodes that have been
1000
1048
  * referenced between summaries so that their unreferenced timestamp can be reset.
1001
1049
  *
1002
1050
  * @param fromNodePath - The node from which the reference is added.
1003
1051
  * @param toNodePath - The node to which the reference is added.
1052
+ * @param autorecovery - This reference is added artificially, for autorecovery. Used for logging.
1004
1053
  */
1005
- public addedOutboundReference(fromNodePath: string, toNodePath: string) {
1054
+ public addedOutboundReference(fromNodePath: string, toNodePath: string, autorecovery?: true) {
1006
1055
  if (!this.configs.shouldRunGC) {
1007
1056
  return;
1008
1057
  }
@@ -1032,7 +1081,14 @@ export class GarbageCollector implements IGarbageCollector {
1032
1081
  isTombstoned: this.tombstones.includes(toNodePath),
1033
1082
  lastSummaryTime: this.getLastSummaryTimestampMs(),
1034
1083
  fromId: fromNodePath,
1084
+ autorecovery,
1035
1085
  });
1086
+
1087
+ // This node is referenced - Clear its unreferenced state
1088
+ // But don't delete the node id from the map yet.
1089
+ // When generating GC stats, the set of nodes in here is used as the baseline for
1090
+ // what was unreferenced in the last GC run.
1091
+ this.unreferencedNodesState.get(toNodePath)?.stopTracking();
1036
1092
  }
1037
1093
 
1038
1094
  /**
@@ -1066,17 +1122,17 @@ export class GarbageCollector implements IGarbageCollector {
1066
1122
  updatedAttachmentBlobCount: 0,
1067
1123
  };
1068
1124
 
1069
- const updateNodeStats = (nodeId: string, referenced: boolean) => {
1125
+ const updateNodeStats = (nodeId: string, isReferenced: boolean) => {
1070
1126
  markPhaseStats.nodeCount++;
1071
1127
  // If there is no previous GC data, every node's state is generated and is considered as updated.
1072
1128
  // Otherwise, find out if any node went from referenced to unreferenced or vice-versa.
1129
+ const wasNotReferenced = this.unreferencedNodesState.has(nodeId);
1073
1130
  const stateUpdated =
1074
- this.gcDataFromLastRun === undefined ||
1075
- this.unreferencedNodesState.has(nodeId) === referenced;
1131
+ this.gcDataFromLastRun === undefined || wasNotReferenced === isReferenced;
1076
1132
  if (stateUpdated) {
1077
1133
  markPhaseStats.updatedNodeCount++;
1078
1134
  }
1079
- if (!referenced) {
1135
+ if (!isReferenced) {
1080
1136
  markPhaseStats.unrefNodeCount++;
1081
1137
  }
1082
1138
 
@@ -1085,7 +1141,7 @@ export class GarbageCollector implements IGarbageCollector {
1085
1141
  if (stateUpdated) {
1086
1142
  markPhaseStats.updatedDataStoreCount++;
1087
1143
  }
1088
- if (!referenced) {
1144
+ if (!isReferenced) {
1089
1145
  markPhaseStats.unrefDataStoreCount++;
1090
1146
  }
1091
1147
  }
@@ -1094,7 +1150,7 @@ export class GarbageCollector implements IGarbageCollector {
1094
1150
  if (stateUpdated) {
1095
1151
  markPhaseStats.updatedAttachmentBlobCount++;
1096
1152
  }
1097
- if (!referenced) {
1153
+ if (!isReferenced) {
1098
1154
  markPhaseStats.unrefAttachmentBlobCount++;
1099
1155
  }
1100
1156
  }