Skip to content

Comments

Move isFluidHandle to shared-object-base#17328

Closed
CraigMacomber wants to merge 21 commits intomicrosoft:mainfrom
CraigMacomber:isFluidHandle
Closed

Move isFluidHandle to shared-object-base#17328
CraigMacomber wants to merge 21 commits intomicrosoft:mainfrom
CraigMacomber:isFluidHandle

Conversation

@CraigMacomber
Copy link
Contributor

Description

FluidSeralizer now uses a more robust check and more documented types for its IFluidHandle detection.

Once an assert (to detect data where the behavior changed, which we expect currently not to be present) is removed, this will allow FluidSeralizer to safely handle arbitrary object keys (for example user provided strings). Before this change, if such data happend to be an object key "IFluidHandle", it would treat it as an IFluidHandle.

Further adopting the new FluidSerializableReadOnly in the API will be a breaking change, and thus is not part of this PR.

Breaking Changes

FluidSeralizer will now treat objects with a "IFluidHandle" field that is not a IFluidHandle (or is one but doesn't refer to itself in its .IFluidHandle field) as not a fluid handle.

This is not expected to break any usages, but any data impacted by this change will now assert.

Reviewer Guidance

The review process is outlined on this wiki page.

@CraigMacomber CraigMacomber marked this pull request as ready for review September 15, 2023 20:18
@CraigMacomber CraigMacomber requested review from a team as code owners September 15, 2023 20:18
@github-actions github-actions bot added area: dds Issues related to distributed data structures area: dds: tree public api change Changes to a public API base: main PRs targeted against main branch labels Sep 15, 2023
@msfluid-bot
Copy link
Collaborator

msfluid-bot commented Sep 15, 2023

@fluid-example/bundle-size-tests: +3.49 KB
Metric NameBaseline SizeCompare SizeSize Diff
aqueduct.js 450.63 KB 451.35 KB +737 Bytes
connectionState.js 680 Bytes 680 Bytes No change
containerRuntime.js 240.94 KB 240.94 KB No change
loader.js 167.41 KB 167.41 KB +2 Bytes
map.js 48.05 KB 48.77 KB +737 Bytes
matrix.js 141.84 KB 142.56 KB +737 Bytes
odspDriver.js 90.35 KB 90.36 KB +2 Bytes
odspPrefetchSnapshot.js 41.82 KB 41.82 KB +2 Bytes
sharedString.js 162.77 KB 163.49 KB +735 Bytes
sharedTree2.js 287.81 KB 288.41 KB +621 Bytes
Total Size 1.75 MB 1.75 MB +3.49 KB

Baseline commit: 09c8d09

Generated by 🚫 dangerJS against 710b1e7

// To help detect if the limitation of this old handle detection was impacting behavior, keep it around for now:
const isHandleLegacy = (value as Partial<IFluidHandle>)?.IFluidHandle !== undefined;
const isHandle = isFluidHandle(value);
assert(isHandleLegacy === isHandle, "new isFluidHandle should not change existing policy.");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check is breaking some "ci:tst:realsvc:local" tests related to GC.

I don't know if thats finding a real issue (we have code which uses handles which do not have cyclic refs), or test issue (the test is doing something test specific that breaks this)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I talked to @DLehenbauer About this and got some of the history for this pattern. Turns out that I think the assumption that these refs are cyclic might not be required to be true (but we could possibly add this requirement).

Alternatively we could make handle use symbols to identify themselves.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated this PR to permit noncyclic handles, changing how the detection works and rewriting most of the documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still fails tests. All the docs in this code claim its trying to detect IFluidHandle, but the actual implementation seems to detect IProvideFluidHandle. My changes make it actually detect IFluidHandle, and this breaks a 14 of GC related end-to-end tests.

It is unclear to me if the tests are wrong, and happened to work due to incorrect detecting of IFluidHandles in this code, or if this code is documented incorrectly and should be made to detect and special case IProvideFluidHandle instances instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the CI run, its the same test failing 14 times because it runs in different compat mode. I believe the test is incorrect. Let me take a stab at fixing it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the fix - #18551

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since its possible others might have made the same or similar mistakes in their code, I have added a changeset to this PR detailing that this could be a problem and how to fix it.

markfields
markfields previously approved these changes Oct 2, 2023
Copy link
Member

@markfields markfields left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some small suggestions, but looks good

@markfields markfields self-assigned this Dec 3, 2023
return extractContentMap(content);
} else if (content !== null && typeof content === "object") {
const flexNode = tryGetEditNode(content);
// Only run isFluidHandle if content can't be an unhydrated node,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the function description and code and can't quite tie this comment to what's happening here. Must be some implicit connection between if it's a Map and being "unhydrated"? Is "flex" a well-defined term? How does it relate to hydration etc?

attachGraph: () => assert.fail(),
bind: () => assert.fail(),
absolutePath: "",
} satisfies IFluidHandle),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't familiar with satisfies, looked it up, very cool! Always glad to learn to TS features from you!

* Data Store serializer implementation.
*
* @privateRemarks
* Since this type is package exported (not just the Interface above),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's @internal now! I had wanted to stop it from being public anyway, and it just happened in bulk. Maybe it's kind to wait until after internal.8.0 "just in case", but we should be good to change stuff like this with more liberty now AFAIK.

// Detect if 'value' is an IFluidHandle.
const handle = value?.IFluidHandle;
// To help detect if the limitation of this old handle detection was impacting behavior, keep it around for now:
const isHandleLegacy = (value as Partial<IFluidHandle>)?.IFluidHandle !== undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm always torn on asserts like the one below. If there's a case out there that violates it (like you found), this code will obviously throw. But is the calling code prepared to properly handle that throw? It's hard to anticipate exactly how that error will manifest. But I guess what else can you do. It's a great validation that our test (now) don't hit it.

No suggestion here, it's fine as-is, just thinking out loud.

* (due to decodeValue's use of isSerializedHandle causing them to be wrongly parsed as handles) and should be avoided.
* @public
*/
export type FluidSerializableReadOnly =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jason-ha has a PR out about some similar recursive types, is this new type harmonious with whatever he's doing? I haven't looked as his PR yet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will not work with the current type test support.
TypeOnly utility will not be able to handle anything this such a type in it. (Although currently if this only appears two levels of properties down the error may not be observed.)
See #18523 that injects an interface where there is recursion to workaround the TypeOnly issue.

| null
| readonly FluidSerializableReadOnly[]
| { readonly [P in string]?: FluidSerializableReadOnly };

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you contrast [P in string] v. something like [P in keyof object]? I guess Symbol keys would be excluded, not sure how it would play out. How did you land on P in string?

/**
* Check if a value in {@link FluidSerializableReadOnly} data is an {@link @fluidframework/core-interfaces#IFluidHandle}.
*
* Warning: Non-IFluidHandle objects with a key "IFluidHandle" will cause an assert.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why assert instead of merely returning false? Seems unnecessarily disruptive if a violation were to happen.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I read the comment in the implementation. Similar to the assert in encodeValue - it's about failing fast during this transition to stricter runtime checks.

Do you expect to come back later and remove these asserts?

return false;
}
const partialHandle = value as Partial<IFluidHandle>;
const innerHandle = partialHandle.IFluidHandle;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the pattern where innerHandle === value, calling it innerHandle could be misleading to an outside. Maybe add a comment saying something like nnerHandle is expected to be a cyclical reference to value itself, not a secondary "inner" handle.

);

assert(
innerHandle.IFluidHandle?.IFluidHandle?.IFluidHandle?.IFluidHandle !== undefined,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol

* @privateRemarks
* Any of the following changes to IFluidHandle would solve the ambiguity problem:
*
* - The "IFluidHandle" property could be required to be a cyclic reference back to the parent.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's pretty tempting to just enforce this. Maybe after the next release we enforce that further restriction.

// If the object fails this check, it will assert to detect problematic data that would have incorrectly been treated as a handle in previous versions of the serializer.

// Since json compatible data shouldn't have methods, and IFluidHandle requires one, use that as a redundant check:
const getMember = (value as Partial<IFluidHandle>).get;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since isFluidHandle is called repeatedly/recursively on a critical path (op processing), I'm wondering if we need to deliberately optimize it. Putting this comment here because maybe we put this under the if (value !== innerHandle) check so it (likely) never actually has to run.

But the comment appllies in general, but the happy path of the function looks pretty lean. Do we have any perf benchmarks that cover this code that we could observe before/after? I think we do but not sure where or how to intepret them.

Copy link
Member

@markfields markfields left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left plenty of comments to consider, but nothing blocking. Nice incremental change to make!

Copy link
Contributor

@jason-ha jason-ha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FluidSerializableReadOnly needs altered to allow TypeOnly to wrap it.
Currently:

declare function use_current_TypeAlias_FluidSerializableReadOnly(
    use: TypeOnly<current.FluidSerializableReadOnly>): void;

will result in

@fluidframework/shared-object-base: [cjs]: src/test/types/validateSharedObjectBasePrevious.generated.ts:25:10 - error TS2589: Type instantiation is excessively deep and possibly infinite.
@fluidframework/shared-object-base: 
@fluidframework/shared-object-base: 25     use: TypeOnly<current.FluidSerializableReadOnly>): void;

(The type test generator should really create a declaration for all types in current, even if not present in the old to expose such an issue.)

const getMember = (value as Partial<IFluidHandle>).get;
assert(
typeof getMember === "function",
"Fluid handle detection found IFluidHandle field, but not have a get method",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...but +does+ not have or similar wording fix needed

@CraigMacomber
Copy link
Contributor Author

I think the best thing to do here is fix the fact that its impossible to robustly detect handles, which can be done by using a class or symbol for the handles. I believe https://dev.azure.com/fluidframework/internal/_workitems/edit/1507 is tracking this internally. If we are unable to make handles more robust, then maybe landing something like this PR makes sense.

@microsoft-github-policy-service
Copy link
Contributor

This PR has been automatically marked as stale because it has had no activity for 60 days. It will be closed if no further activity occurs within 8 days of this comment. Thank you for your contributions to Fluid Framework!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: dds: tree area: dds Issues related to distributed data structures base: main PRs targeted against main branch community-contribution public api change Changes to a public API status: stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants