Skip to content

Commit b15d5dd

Browse files
authored
Fix behavior of object visibility tests with handles in a detached state (#24997)
#21213 attempted to remove all calls to bindHandles at the DDS layer, bout could not remove this behavior in directory due to the fluidObjectVisibility tests. It seemed like there was a bug where a handle to a detached DDS stored in an intermediate detached DDS would not be visible from a third detached DDS until the root container attached. However, the assumption that a handle would need to be visible (and therefore able to be resolved) from a root detached container is incorrect - when detached, each handle has direct access to the object it points to. Update the visibility tests accordingly, and remove the call to bindHandles as well. [AB#8168](https://dev.azure.com/fluidframework/235294da-091d-4c29-84fc-cdfc3d90890b/_workitems/edit/8168)
1 parent 035eab8 commit b15d5dd

File tree

3 files changed

+345
-58
lines changed

3 files changed

+345
-58
lines changed

packages/dds/map/src/directory.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import type { IFluidSerializer } from "@fluidframework/shared-object-base/intern
2525
import {
2626
SharedObject,
2727
ValueType,
28-
bindHandles,
2928
parseHandles,
3029
} from "@fluidframework/shared-object-base/internal";
3130
import {
@@ -1272,9 +1271,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
12721271
throw new Error("Undefined and null keys are not supported");
12731272
}
12741273

1275-
// Create a local value and serialize it.
1276-
bindHandles(value, this.serializer, this.directory.handle);
1277-
12781274
// Set the value locally.
12791275
const previousValue = this.setCore(key, value, true);
12801276

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import { strict as assert } from "assert";
7+
8+
import {
9+
createDetachedContainer,
10+
loadExistingContainer,
11+
rehydrateDetachedContainer,
12+
} from "@fluidframework/container-loader/internal";
13+
import type { FluidObject, IFluidHandle } from "@fluidframework/core-interfaces";
14+
import { LocalResolver } from "@fluidframework/local-driver/internal";
15+
import { type IContainerRuntimeBase } from "@fluidframework/runtime-definitions/internal";
16+
import { LocalDeltaConnectionServer } from "@fluidframework/server-local-server";
17+
import {
18+
LoaderContainerTracker,
19+
getContainerEntryPointBackCompat,
20+
waitForContainerConnection,
21+
type ITestFluidObject,
22+
} from "@fluidframework/test-utils/internal";
23+
24+
import { createLoader } from "../utils";
25+
26+
/**
27+
* Creates a non-root data object and validates that it is not visible from the root of the container.
28+
*/
29+
async function createNonRootDataObject(
30+
containerRuntime: IContainerRuntimeBase,
31+
): Promise<ITestFluidObject> {
32+
const dataStore = await containerRuntime.createDataStore("default");
33+
const maybeTestDo: FluidObject<ITestFluidObject> = await dataStore.entryPoint.get();
34+
assert(maybeTestDo.ITestFluidObject !== undefined, "Failed to get ITestFluidObject");
35+
return maybeTestDo.ITestFluidObject;
36+
}
37+
38+
async function getAndValidateDataObject(
39+
fromDataObject: ITestFluidObject,
40+
key: string,
41+
): Promise<ITestFluidObject> {
42+
const dataObjectHandle = fromDataObject.root.get<IFluidHandle<ITestFluidObject>>(key);
43+
assert(dataObjectHandle !== undefined, `Data object handle for key ${key} not found`);
44+
const dataObject = await dataObjectHandle.get();
45+
assert(dataObject !== undefined, `Data object for key ${key} must be visible`);
46+
return dataObject;
47+
}
48+
49+
/**
50+
* Validates that handles in a non-root data store and its dependencies resolve correctly across different container attach states.
51+
* Also, ensure handles are accessible in remote clients and can send ops.
52+
*/
53+
describe("Multi-level handle access", () => {
54+
const documentId = "objectVisibilityTest";
55+
const documentLoadUrl = `https://localhost/${documentId}`;
56+
const urlResolver = new LocalResolver();
57+
58+
it("validates that handles in a non-root data store and its dependencies resolve correctly when a detached container attaches", async () => {
59+
const loaderContainerTracker = new LoaderContainerTracker();
60+
const deltaConnectionServer = LocalDeltaConnectionServer.create();
61+
62+
const loader = createLoader({
63+
deltaConnectionServer,
64+
});
65+
const container1 = await createDetachedContainer(loader);
66+
67+
const dataObject1 = await getContainerEntryPointBackCompat<ITestFluidObject>(container1);
68+
const containerRuntime1 = dataObject1.context.containerRuntime;
69+
70+
const dataObject2 = await createNonRootDataObject(containerRuntime1);
71+
const dataObject3 = await createNonRootDataObject(containerRuntime1);
72+
73+
// Add the handle of dataObject3 to dataObject2's DDS.
74+
dataObject2.root.set("dataObject3", dataObject3.handle);
75+
76+
// Add the handle of dataObject2 to root.
77+
dataObject1.root.set("dataObject2", dataObject2.handle);
78+
// Ensure that handles can be accessed while detached without waiting for the handle to be resolved.
79+
await assert.doesNotReject(
80+
dataObject2.handle.get(),
81+
"Must be able to access data object 2 handle while detached",
82+
);
83+
await assert.doesNotReject(
84+
dataObject3.handle.get(),
85+
"Must be able to access data object 3 handle while detached",
86+
);
87+
88+
// Attach the container.
89+
await container1.attach(urlResolver.createCreateNewRequest(documentId));
90+
await waitForContainerConnection(container1);
91+
loaderContainerTracker.addContainer(container1);
92+
await loaderContainerTracker.ensureSynchronized();
93+
94+
// Validate that the data objects are visible from root after attach.
95+
await assert.doesNotReject(
96+
dataObject2.handle.get(),
97+
"Must be able to access data object 2 handle after attach",
98+
);
99+
await assert.doesNotReject(
100+
dataObject3.handle.get(),
101+
"Must be able to access data object 3 handle after attach",
102+
);
103+
104+
// Load a second container and validate that both the non-root data stores are accessible in it.
105+
const loader2 = createLoader({
106+
deltaConnectionServer,
107+
});
108+
const container2 = await loadExistingContainer({
109+
...loader2.loaderProps,
110+
request: { url: documentLoadUrl },
111+
});
112+
loaderContainerTracker.addContainer(container2);
113+
await loaderContainerTracker.ensureSynchronized();
114+
115+
const dataObject1C2 = await getContainerEntryPointBackCompat<ITestFluidObject>(container2);
116+
const dataObject2C2 = await getAndValidateDataObject(dataObject1C2, "dataObject2");
117+
const dataObject3C2 = await getAndValidateDataObject(dataObject2C2, "dataObject3");
118+
119+
// Validate that the data objects are accessible from the root of the second container.
120+
await assert.doesNotReject(
121+
dataObject2C2.handle.get(),
122+
"Must be able to access data object 2 handle in container 2",
123+
);
124+
await assert.doesNotReject(
125+
dataObject3C2.handle.get(),
126+
"Must be able to access data object 3 handle in container 2",
127+
);
128+
129+
await loaderContainerTracker.ensureSynchronized();
130+
131+
// Send ops for the data stores in both local and remote container and validate that the ops are
132+
// successfully processed.
133+
dataObject2.root.set("key1", "value1");
134+
dataObject2C2.root.set("key2", "value2");
135+
dataObject3.root.set("key1", "value1");
136+
dataObject3C2.root.set("key2", "value2");
137+
await loaderContainerTracker.ensureSynchronized();
138+
assert.strictEqual(dataObject2.root.get("key2"), "value2");
139+
assert.strictEqual(dataObject2C2.root.get("key1"), "value1");
140+
assert.strictEqual(dataObject3.root.get("key2"), "value2");
141+
assert.strictEqual(dataObject3C2.root.get("key1"), "value1");
142+
});
143+
144+
it("validates that handles in a non-root data store and its dependencies resolve correctly when initial container is attached", async () => {
145+
const loaderContainerTracker = new LoaderContainerTracker();
146+
const deltaConnectionServer = LocalDeltaConnectionServer.create();
147+
148+
const loader = createLoader({
149+
deltaConnectionServer,
150+
});
151+
152+
const container1 = await createDetachedContainer(loader);
153+
await container1.attach(urlResolver.createCreateNewRequest(documentId));
154+
155+
await waitForContainerConnection(container1);
156+
loaderContainerTracker.addContainer(container1);
157+
158+
const dataObject1 = await getContainerEntryPointBackCompat<ITestFluidObject>(container1);
159+
const containerRuntime1 = dataObject1.context.containerRuntime;
160+
161+
const dataObject2 = await createNonRootDataObject(containerRuntime1);
162+
const dataObject3 = await createNonRootDataObject(containerRuntime1);
163+
164+
// Add the handle of dataObject3 to dataObject2's DDS.
165+
dataObject2.root.set("dataObject3", dataObject3.handle);
166+
167+
// Add the handle of dataObject2 to root.
168+
dataObject1.root.set("dataObject2", dataObject2.handle);
169+
170+
// Validate that the data objects are accessible from the root of the attached container.
171+
await assert.doesNotReject(
172+
dataObject2.handle.get(),
173+
"Must be able to access data object 2 handle while attached",
174+
);
175+
await assert.doesNotReject(
176+
dataObject3.handle.get(),
177+
"Must be able to access data object 3 handle while attached",
178+
);
179+
180+
// Load a second container and validate that both the non-root data stores are accessible in it.
181+
const loader2 = createLoader({
182+
deltaConnectionServer,
183+
});
184+
const container2 = await loadExistingContainer({
185+
...loader2.loaderProps,
186+
request: { url: documentLoadUrl },
187+
});
188+
loaderContainerTracker.addContainer(container2);
189+
await loaderContainerTracker.ensureSynchronized();
190+
191+
const dataObject1C2 = await getContainerEntryPointBackCompat<ITestFluidObject>(container2);
192+
const dataObject2C2 = await getAndValidateDataObject(dataObject1C2, "dataObject2");
193+
const dataObject3C2 = await getAndValidateDataObject(dataObject2C2, "dataObject3");
194+
195+
// Validate that the data objects are accessible from the root of the second container.
196+
await assert.doesNotReject(
197+
dataObject2C2.handle.get(),
198+
"Must be able to access data object 2 handle in container 2",
199+
);
200+
await assert.doesNotReject(
201+
dataObject3C2.handle.get(),
202+
"Must be able to access data object 3 handle in container 2",
203+
);
204+
await loaderContainerTracker.ensureSynchronized();
205+
206+
// Send ops for the data stores in both local and remote container and validate that the ops are
207+
// successfully processed.
208+
dataObject2.root.set("key1", "value1");
209+
dataObject2C2.root.set("key2", "value2");
210+
dataObject3.root.set("key1", "value1");
211+
dataObject3C2.root.set("key2", "value2");
212+
await loaderContainerTracker.ensureSynchronized();
213+
assert.strictEqual(dataObject2.root.get("key2"), "value2");
214+
assert.strictEqual(dataObject2C2.root.get("key1"), "value1");
215+
assert.strictEqual(dataObject3.root.get("key2"), "value2");
216+
assert.strictEqual(dataObject3C2.root.get("key1"), "value1");
217+
});
218+
219+
it("validates that handles in a non-root data store and its dependencies resolve correctly after container close and rehydrate", async () => {
220+
const loaderContainerTracker = new LoaderContainerTracker();
221+
const deltaConnectionServer = LocalDeltaConnectionServer.create();
222+
223+
const loader = createLoader({
224+
deltaConnectionServer,
225+
});
226+
const container1 = await createDetachedContainer(loader);
227+
loaderContainerTracker.addContainer(container1);
228+
229+
const dataObject1 = await getContainerEntryPointBackCompat<ITestFluidObject>(container1);
230+
const containerRuntime1 = dataObject1.context.containerRuntime;
231+
232+
const dataObject2 = await createNonRootDataObject(containerRuntime1);
233+
const dataObject3 = await createNonRootDataObject(containerRuntime1);
234+
235+
// Add the handle of dataObject3 to dataObject2's DDS.
236+
dataObject2.root.set("dataObject3", dataObject3.handle);
237+
238+
// Add the handle of dataObject2 to root.
239+
dataObject1.root.set("dataObject2", dataObject2.handle);
240+
// Ensure that handles can be accessed while detached without waiting for the handle to be resolved.
241+
await assert.doesNotReject(
242+
dataObject2.handle.get(),
243+
"Must be able to access data object 2 handle while detached",
244+
);
245+
await assert.doesNotReject(
246+
dataObject3.handle.get(),
247+
"Must be able to access data object 3 handle while detached",
248+
);
249+
250+
// Rehydrate a second container from the snapshot of the first container to validate the accessibility of the non-root data stores.
251+
const loader2 = createLoader({
252+
deltaConnectionServer,
253+
});
254+
const snapshot = container1.serialize();
255+
container1.close();
256+
257+
const container2 = await rehydrateDetachedContainer({
258+
...loader2.loaderProps,
259+
serializedState: snapshot,
260+
});
261+
await container2.attach(urlResolver.createCreateNewRequest(documentId));
262+
await waitForContainerConnection(container2);
263+
loaderContainerTracker.addContainer(container2);
264+
265+
const dataObjectC2 = await getContainerEntryPointBackCompat<ITestFluidObject>(container2);
266+
const dataObject2C2 = await getAndValidateDataObject(dataObjectC2, "dataObject2");
267+
const dataObject3C2 = await getAndValidateDataObject(dataObject2C2, "dataObject3");
268+
269+
// Validate that the data objects are accessible from the root of the rehydrated container.
270+
await assert.doesNotReject(
271+
dataObject2C2.handle.get(),
272+
"Must be able to access data object 2 handle in container 2 after rehydrate",
273+
);
274+
await assert.doesNotReject(
275+
dataObject3C2.handle.get(),
276+
"Must be able to access data object 3 handle in container 2 after rehydrate",
277+
);
278+
});
279+
});

0 commit comments

Comments
 (0)