@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.
- package/README.md +1 -1
- package/api-report/container-runtime.api.md +13 -3
- package/dist/container-runtime-alpha.d.ts +11 -3
- package/dist/container-runtime-beta.d.ts +7 -0
- package/dist/container-runtime-public.d.ts +7 -0
- package/dist/container-runtime-untrimmed.d.ts +28 -3
- package/dist/containerRuntime.d.ts +5 -2
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +55 -67
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStoreContext.d.ts +4 -1
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js +2 -3
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/dataStores.d.ts +4 -2
- package/dist/dataStores.d.ts.map +1 -1
- package/dist/dataStores.js +8 -2
- package/dist/dataStores.js.map +1 -1
- package/dist/gc/garbageCollection.d.ts +17 -1
- package/dist/gc/garbageCollection.d.ts.map +1 -1
- package/dist/gc/garbageCollection.js +72 -29
- package/dist/gc/garbageCollection.js.map +1 -1
- package/dist/gc/gcDefinitions.d.ts +18 -2
- package/dist/gc/gcDefinitions.d.ts.map +1 -1
- package/dist/gc/gcDefinitions.js +5 -1
- package/dist/gc/gcDefinitions.js.map +1 -1
- package/dist/gc/gcTelemetry.d.ts +1 -0
- package/dist/gc/gcTelemetry.d.ts.map +1 -1
- package/dist/gc/gcTelemetry.js +0 -2
- package/dist/gc/gcTelemetry.js.map +1 -1
- package/dist/gc/gcUnreferencedStateTracker.d.ts +5 -0
- package/dist/gc/gcUnreferencedStateTracker.d.ts.map +1 -1
- package/dist/gc/gcUnreferencedStateTracker.js +12 -1
- package/dist/gc/gcUnreferencedStateTracker.js.map +1 -1
- package/dist/gc/index.d.ts +1 -1
- package/dist/gc/index.d.ts.map +1 -1
- package/dist/gc/index.js +2 -1
- package/dist/gc/index.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/messageTypes.d.ts +1 -1
- package/dist/messageTypes.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/summary/runningSummarizer.d.ts +6 -6
- package/dist/summary/runningSummarizer.d.ts.map +1 -1
- package/dist/summary/runningSummarizer.js +90 -72
- package/dist/summary/runningSummarizer.js.map +1 -1
- package/dist/summary/summarizerNode/summarizerNode.d.ts +2 -3
- package/dist/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
- package/dist/summary/summarizerNode/summarizerNode.js +6 -48
- package/dist/summary/summarizerNode/summarizerNode.js.map +1 -1
- package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts +1 -18
- package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
- package/dist/summary/summarizerNode/summarizerNodeUtils.js +1 -21
- package/dist/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
- package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts +3 -3
- package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
- package/dist/summary/summarizerNode/summarizerNodeWithGc.js +5 -5
- package/dist/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
- package/dist/summary/summarizerTypes.d.ts +1 -1
- package/dist/summary/summarizerTypes.d.ts.map +1 -1
- package/dist/summary/summarizerTypes.js.map +1 -1
- package/dist/tsdoc-metadata.json +1 -1
- package/lib/container-runtime-alpha.d.mts +11 -3
- package/lib/container-runtime-beta.d.mts +7 -0
- package/lib/container-runtime-public.d.mts +7 -0
- package/lib/container-runtime-untrimmed.d.mts +28 -3
- package/lib/containerRuntime.d.mts +5 -2
- package/lib/containerRuntime.d.mts.map +1 -1
- package/lib/containerRuntime.mjs +56 -68
- package/lib/containerRuntime.mjs.map +1 -1
- package/lib/dataStoreContext.d.mts +4 -1
- package/lib/dataStoreContext.d.mts.map +1 -1
- package/lib/dataStoreContext.mjs +2 -3
- package/lib/dataStoreContext.mjs.map +1 -1
- package/lib/dataStores.d.mts +4 -2
- package/lib/dataStores.d.mts.map +1 -1
- package/lib/dataStores.mjs +8 -2
- package/lib/dataStores.mjs.map +1 -1
- package/lib/gc/garbageCollection.d.mts +17 -1
- package/lib/gc/garbageCollection.d.mts.map +1 -1
- package/lib/gc/garbageCollection.mjs +74 -31
- package/lib/gc/garbageCollection.mjs.map +1 -1
- package/lib/gc/gcDefinitions.d.mts +18 -2
- package/lib/gc/gcDefinitions.d.mts.map +1 -1
- package/lib/gc/gcDefinitions.mjs +4 -0
- package/lib/gc/gcDefinitions.mjs.map +1 -1
- package/lib/gc/gcTelemetry.d.mts +1 -0
- package/lib/gc/gcTelemetry.d.mts.map +1 -1
- package/lib/gc/gcTelemetry.mjs +0 -2
- package/lib/gc/gcTelemetry.mjs.map +1 -1
- package/lib/gc/gcUnreferencedStateTracker.d.mts +5 -0
- package/lib/gc/gcUnreferencedStateTracker.d.mts.map +1 -1
- package/lib/gc/gcUnreferencedStateTracker.mjs +10 -0
- package/lib/gc/gcUnreferencedStateTracker.mjs.map +1 -1
- package/lib/gc/index.d.mts +1 -1
- package/lib/gc/index.d.mts.map +1 -1
- package/lib/gc/index.mjs +1 -1
- package/lib/gc/index.mjs.map +1 -1
- package/lib/index.d.mts +2 -1
- package/lib/index.d.mts.map +1 -1
- package/lib/index.mjs +1 -0
- package/lib/index.mjs.map +1 -1
- package/lib/messageTypes.d.mts +1 -1
- package/lib/messageTypes.mjs.map +1 -1
- package/lib/packageVersion.d.mts +1 -1
- package/lib/packageVersion.mjs +1 -1
- package/lib/packageVersion.mjs.map +1 -1
- package/lib/summary/runningSummarizer.d.mts +6 -6
- package/lib/summary/runningSummarizer.d.mts.map +1 -1
- package/lib/summary/runningSummarizer.mjs +90 -72
- package/lib/summary/runningSummarizer.mjs.map +1 -1
- package/lib/summary/summarizerNode/summarizerNode.d.mts +2 -3
- package/lib/summary/summarizerNode/summarizerNode.d.mts.map +1 -1
- package/lib/summary/summarizerNode/summarizerNode.mjs +8 -50
- package/lib/summary/summarizerNode/summarizerNode.mjs.map +1 -1
- package/lib/summary/summarizerNode/summarizerNodeUtils.d.mts +1 -18
- package/lib/summary/summarizerNode/summarizerNodeUtils.d.mts.map +1 -1
- package/lib/summary/summarizerNode/summarizerNodeUtils.mjs +0 -19
- package/lib/summary/summarizerNode/summarizerNodeUtils.mjs.map +1 -1
- package/lib/summary/summarizerNode/summarizerNodeWithGc.d.mts +3 -3
- package/lib/summary/summarizerNode/summarizerNodeWithGc.d.mts.map +1 -1
- package/lib/summary/summarizerNode/summarizerNodeWithGc.mjs +5 -5
- package/lib/summary/summarizerNode/summarizerNodeWithGc.mjs.map +1 -1
- package/lib/summary/summarizerTypes.d.mts +1 -1
- package/lib/summary/summarizerTypes.d.mts.map +1 -1
- package/lib/summary/summarizerTypes.mjs.map +1 -1
- package/package.json +27 -21
- package/src/containerRuntime.ts +96 -85
- package/src/dataStoreContext.ts +6 -4
- package/src/dataStores.ts +8 -1
- package/src/gc/garbageCollection.ts +86 -30
- package/src/gc/gcDefinitions.ts +19 -3
- package/src/gc/gcTelemetry.ts +1 -2
- package/src/gc/gcUnreferencedStateTracker.ts +11 -0
- package/src/gc/index.ts +1 -0
- package/src/index.ts +2 -0
- package/src/messageTypes.ts +1 -1
- package/src/packageVersion.ts +1 -1
- package/src/summary/runningSummarizer.ts +116 -88
- package/src/summary/summarizerNode/summarizerNode.ts +4 -64
- package/src/summary/summarizerNode/summarizerNodeUtils.ts +2 -33
- package/src/summary/summarizerNode/summarizerNodeWithGc.ts +0 -6
- package/src/summary/summarizerTypes.ts +1 -1
- /package/{.eslintrc.js → .eslintrc.cjs} +0 -0
package/src/containerRuntime.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
882
|
+
idCompressor = deserializeIdCompressor(
|
|
883
|
+
pendingLocalState.pendingIdCompressorState,
|
|
884
|
+
compressorLogger,
|
|
885
|
+
);
|
|
869
886
|
} else if (serializedIdCompressor !== undefined) {
|
|
870
|
-
idCompressor = deserializeIdCompressor(
|
|
887
|
+
idCompressor = deserializeIdCompressor(
|
|
888
|
+
serializedIdCompressor,
|
|
889
|
+
createSessionId(),
|
|
890
|
+
compressorLogger,
|
|
891
|
+
);
|
|
871
892
|
} else {
|
|
872
|
-
idCompressor = createIdCompressor(
|
|
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
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
-
|
|
2443
|
-
|
|
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(
|
|
2500
|
-
|
|
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(
|
|
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
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
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
|
-
|
|
3712
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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");
|
package/src/dataStoreContext.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
860
|
-
|
|
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
|
-
|
|
885
|
+
gcMessageType,
|
|
869
886
|
message.compatDetails?.behavior,
|
|
870
887
|
)
|
|
871
888
|
) {
|
|
872
889
|
const error = DataProcessingError.create(
|
|
873
|
-
`Garbage collection message of unknown 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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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,
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
1153
|
+
if (!isReferenced) {
|
|
1098
1154
|
markPhaseStats.unrefAttachmentBlobCount++;
|
|
1099
1155
|
}
|
|
1100
1156
|
}
|