-
Notifications
You must be signed in to change notification settings - Fork 576
feat(datastore): Decorate and log errors thrown during DataStore entrypoint initialization #27327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4c4540d
c99c812
3091f0d
6adb42d
1a05491
4fbb68a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,6 +75,7 @@ import { channelsTreeName } from "@fluidframework/runtime-definitions/internal"; | |
| import { | ||
| addBlobToSummary, | ||
| isSnapshotFetchRequiredForLoadingGroupId, | ||
| dataStoreLoadTelemetryProps, | ||
| } from "@fluidframework/runtime-utils/internal"; | ||
| import { | ||
| DataProcessingError, | ||
|
|
@@ -587,12 +588,7 @@ export abstract class FluidDataStoreContext | |
| error, | ||
| "realizeFluidDataStoreContext", | ||
| ); | ||
| errorWrapped.addTelemetryProperties( | ||
| tagCodeArtifacts({ | ||
| fullPackageName: this.pkg?.join("/"), | ||
| fluidDataStoreId: this.id, | ||
| }), | ||
| ); | ||
| errorWrapped.addTelemetryProperties(dataStoreLoadTelemetryProps(this)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Passing
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't want to muck up the legacy-beta IFluidDataStoreContext interface. I can consider passing in just the props we need off the context.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would be my preference too, passing big/powerful objects for small tasks is also usually a code smell :-D |
||
| this.mc.logger.sendErrorEvent({ eventName: "RealizeError" }, errorWrapped); | ||
| throw errorWrapped; | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,6 +78,7 @@ import { | |
| exceptionToResponse, | ||
| generateHandleContextPath, | ||
| processAttachMessageGCData, | ||
| dataStoreLoadTelemetryProps, | ||
| toFluidHandleInternal, | ||
| unpackChildNodesUsedRoutes, | ||
| toDeltaManagerErased, | ||
|
|
@@ -506,7 +507,22 @@ export class FluidDataStoreRuntime | |
| } | ||
|
|
||
| this.entryPoint = new FluidObjectHandle<FluidObject>( | ||
| new LazyPromise(async () => provideEntryPoint(this)), | ||
| new LazyPromise(async () => | ||
| provideEntryPoint(this).catch((error) => { | ||
| const errorWrapped = DataProcessingError.wrapIfUnrecognized( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great idea!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am wondering if
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thinking more about this - we should separate out the path that loads the data store / DDS data from the path that loads the application logic. And we should be able to tell these apart from telemetry. I would suggest to not use data process error for the latter because that should not cause any problems with the data (it does fail summaries today but that is an unexpected scenario and will be fixed soon) |
||
| error, | ||
| "entryPointInitialization", | ||
| ); | ||
| errorWrapped.addTelemetryProperties( | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: In testing I found that these telemetry props get stripped off on the way up by roundtripping through There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this have to do with privacy concerns? I know there can sometimes be policies to strip all error information away in case it contains user data or identifiable information.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, it has to do with legacy architectural direction (URI-based routing within and across containers)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These properties are correctly tagged as "code artifacts", so hosts can decide if they're safe to log under different circumstances |
||
| dataStoreLoadTelemetryProps(this.dataStoreContext), | ||
| ); | ||
| this.mc.logger.sendErrorEvent( | ||
| { eventName: "EntryPointInitializationFailure" }, | ||
| errorWrapped, | ||
| ); | ||
| throw errorWrapped; | ||
| }), | ||
| ), | ||
| "", | ||
| this.objectsRoutingContext, | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -296,60 +296,70 @@ describeCompat("Summaries", "NoCompat", (getTestObjectProvider, apis) => { | |
| ); | ||
| }); | ||
|
|
||
| it("full initialization of data object should not happen by default", async () => { | ||
| const dataStoreFactory1 = new DataObjectFactory({ | ||
| type: "@fluid-example/test-dataStore1", | ||
| ctor: TestDataObject1, | ||
| }); | ||
| const registryStoreEntries = new Map<string, Promise<IFluidDataStoreFactory>>([ | ||
| [dataStoreFactory1.type, Promise.resolve(dataStoreFactory1)], | ||
| ]); | ||
| const runtimeFactory = new ContainerRuntimeFactoryWithDefaultDataStore({ | ||
| defaultFactory: dataStoreFactory1, | ||
| registryEntries: registryStoreEntries, | ||
| }); | ||
| itExpects( | ||
| "full initialization of data object should not happen by default", | ||
| [ | ||
| { | ||
| eventName: "fluid:telemetry:FluidDataStoreRuntime:EntryPointInitializationFailure", | ||
| error: "Non interactive/summarizer client's data object should not be initialized", | ||
| }, | ||
| ], | ||
| async () => { | ||
| const dataStoreFactory1 = new DataObjectFactory({ | ||
| type: "@fluid-example/test-dataStore1", | ||
| ctor: TestDataObject1, | ||
| }); | ||
| const registryStoreEntries = new Map<string, Promise<IFluidDataStoreFactory>>([ | ||
| [dataStoreFactory1.type, Promise.resolve(dataStoreFactory1)], | ||
| ]); | ||
| const runtimeFactory = new ContainerRuntimeFactoryWithDefaultDataStore({ | ||
| defaultFactory: dataStoreFactory1, | ||
| registryEntries: registryStoreEntries, | ||
| }); | ||
|
|
||
| // Create a container for the first client. | ||
| const container1 = await provider.createContainer(runtimeFactory); | ||
| await assert.doesNotReject( | ||
| container1.getEntryPoint(), | ||
| "Initial creation of container and data store should succeed.", | ||
| ); | ||
| // Create a container for the first client. | ||
| const container1 = await provider.createContainer(runtimeFactory); | ||
| await assert.doesNotReject( | ||
| container1.getEntryPoint(), | ||
| "Initial creation of container and data store should succeed.", | ||
| ); | ||
|
|
||
| // Create a summarizer for the container and do a summary shouldn't throw. | ||
| const createSummarizerResult = await createSummarizerFromFactory( | ||
| provider, | ||
| container1, | ||
| dataStoreFactory1, | ||
| undefined, | ||
| ContainerRuntimeFactoryWithDefaultDataStore, | ||
| registryStoreEntries, | ||
| ); | ||
| await assert.doesNotReject( | ||
| summarizeNow(createSummarizerResult.summarizer, "test"), | ||
| "Summarizing should not throw", | ||
| ); | ||
| // Create a summarizer for the container and do a summary shouldn't throw. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting test. I know you are not changing this but this comment isn't clear. I would suggest updating this to say that the data store is realized / loaded during summarization but is not initialized and hence summarization succeeds. |
||
| const createSummarizerResult = await createSummarizerFromFactory( | ||
| provider, | ||
| container1, | ||
| dataStoreFactory1, | ||
| undefined, | ||
| ContainerRuntimeFactoryWithDefaultDataStore, | ||
| registryStoreEntries, | ||
| ); | ||
| await assert.doesNotReject( | ||
| summarizeNow(createSummarizerResult.summarizer, "test"), | ||
| "Summarizing should not throw", | ||
| ); | ||
|
|
||
| // In summarizer, load the data store should fail. | ||
| await assert.rejects( | ||
| async () => { | ||
| const runtime = (createSummarizerResult.summarizer as any).runtime as ContainerRuntime; | ||
| const dsEntryPoint = await runtime.getAliasedDataStoreEntryPoint("default"); | ||
| await dsEntryPoint?.get(); | ||
| }, | ||
| (e: Error) => | ||
| e.message === | ||
| "Non interactive/summarizer client's data object should not be initialized", | ||
| "Loading data store in summarizer did not throw as it should, or threw an unexpected error.", | ||
| ); | ||
| // In summarizer, load the data store should fail. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update this comment to say that this throws an error and also logs the error in |
||
| await assert.rejects( | ||
| async () => { | ||
| const runtime = (createSummarizerResult.summarizer as any) | ||
| .runtime as ContainerRuntime; | ||
| const dsEntryPoint = await runtime.getAliasedDataStoreEntryPoint("default"); | ||
| await dsEntryPoint?.get(); | ||
| }, | ||
| (e: Error) => | ||
| e.message === | ||
| "Non interactive/summarizer client's data object should not be initialized", | ||
| "Loading data store in summarizer did not throw as it should, or threw an unexpected error.", | ||
| ); | ||
|
|
||
| // Load second container, load the data store will also call initializingFromExisting and succeed. | ||
| const container2 = await provider.loadContainer(runtimeFactory); | ||
| await assert.doesNotReject( | ||
| container2.getEntryPoint(), | ||
| "Initial creation of container and data store should succeed.", | ||
| ); | ||
| }); | ||
| // Load second container, load the data store will also call initializingFromExisting and succeed. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An observation, don't need to change this I guess - This step seems unnecessary. |
||
| const container2 = await provider.loadContainer(runtimeFactory); | ||
| await assert.doesNotReject( | ||
| container2.getEntryPoint(), | ||
| "Initial creation of container and data store should succeed.", | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| /** | ||
| * This test validates that the first summary for a container by the first summarizer client does not violate | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.