Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions packages/runtime/container-runtime/src/dataStoreContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import { channelsTreeName } from "@fluidframework/runtime-definitions/internal";
import {
addBlobToSummary,
isSnapshotFetchRequiredForLoadingGroupId,
dataStoreLoadTelemetryProps,
} from "@fluidframework/runtime-utils/internal";
import {
DataProcessingError,
Expand Down Expand Up @@ -587,12 +588,7 @@ export abstract class FluidDataStoreContext
error,
"realizeFluidDataStoreContext",
);
errorWrapped.addTelemetryProperties(
tagCodeArtifacts({
fullPackageName: this.pkg?.join("/"),
fluidDataStoreId: this.id,
}),
);
errorWrapped.addTelemetryProperties(dataStoreLoadTelemetryProps(this));
Comment thread
markfields marked this conversation as resolved.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Passing this tends to be a code smell, probably prefer either passing the specific members needed or else make it a member method if it truly needs access to this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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;
});
Expand Down
18 changes: 17 additions & 1 deletion packages/runtime/datastore/src/dataStoreRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
exceptionToResponse,
generateHandleContextPath,
processAttachMessageGCData,
dataStoreLoadTelemetryProps,
toFluidHandleInternal,
unpackChildNodesUsedRoutes,
toDeltaManagerErased,
Expand Down Expand Up @@ -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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great idea!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am wondering if DataProcessingError is appropriate here. This runs application entry point logic and has nothing to do with Fluid right? So, maybe this is a UsageError or just a generic error?

Copy link
Copy Markdown
Contributor

@agarwal-navin agarwal-navin May 18, 2026

Choose a reason for hiding this comment

The 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(
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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 exceptionToResponse and responseToException. A bit curious to look into that flow and if we could cut out those tranforms.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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,
);
Expand Down
81 changes: 81 additions & 0 deletions packages/runtime/datastore/src/test/dataStoreRuntime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import type {
ISequencedMessageEnvelope,
MinimumVersionForCollab,
} from "@fluidframework/runtime-definitions/internal";
import {
isFluidError,
MockLogger,
TelemetryDataTag,
} from "@fluidframework/telemetry-utils/internal";
import {
MockFluidDataStoreContext,
validateAssertionError,
Expand Down Expand Up @@ -68,6 +73,7 @@ describe("FluidDataStoreRuntime Tests", () => {
// back-compat 0.38 - DataStoreRuntime looks in container runtime for certain properties that are unavailable
// in the data store context.
dataStoreContext.containerRuntime = {} as unknown as IContainerRuntimeBase;
dataStoreContext.packagePath = [];
sharedObjectRegistry = {
get(type: string) {
return {
Expand Down Expand Up @@ -226,6 +232,81 @@ describe("FluidDataStoreRuntime Tests", () => {
"entryPoint was not initialized",
);
});

describe("entryPoint initialization failure", () => {
it("entryPoint provider is not invoked until entryPoint is consumed", () => {
let invoked = false;
createRuntime(dataStoreContext, sharedObjectRegistry, async () => {
invoked = true;
return {};
});
assert.strictEqual(
invoked,
false,
"entryPoint provider should not run during construction",
);
});

it("rejected entryPoint provider is wrapped and logged", async () => {
const mockLogger = new MockLogger();
const contextWithMockLogger = new MockFluidDataStoreContext(
"testDataStoreId",
false,
mockLogger.toTelemetryLogger(),
);
contextWithMockLogger.containerRuntime = {} as unknown as IContainerRuntimeBase;
contextWithMockLogger.packagePath = ["pkgA", "pkgB"];
const dataStoreRuntime = createRuntime(
contextWithMockLogger,
sharedObjectRegistry,
async () => {
throw new Error("entryPoint failed");
},
);
await assert.rejects(
async () => dataStoreRuntime.entryPoint.get(),
(error: IErrorBase) => {
assert.strictEqual(
error.errorType,
ContainerErrorTypes.dataProcessingError,
"thrown error should be a DataProcessingError",
);
assert(isFluidError(error), "thrown error should be a Fluid error");
const props = error.getTelemetryProperties();
assert.deepStrictEqual(
props.fluidDataStoreId,
{ value: "testDataStoreId", tag: TelemetryDataTag.CodeArtifact },
"error should carry tagged fluidDataStoreId",
);
assert.deepStrictEqual(
props.fullPackageName,
{ value: "pkgA/pkgB", tag: TelemetryDataTag.CodeArtifact },
"error should carry tagged fullPackageName",
);
return true;
},
);
const failureEvent = mockLogger.events.find(
(event) =>
typeof event.eventName === "string" &&
event.eventName.endsWith("EntryPointInitializationFailure"),
);
assert(
failureEvent !== undefined,
"EntryPointInitializationFailure event should have been logged",
);
assert.deepStrictEqual(
failureEvent.fluidDataStoreId,
{ value: "testDataStoreId", tag: TelemetryDataTag.CodeArtifact },
"event should include tagged fluidDataStoreId",
);
assert.deepStrictEqual(
failureEvent.fullPackageName,
{ value: "pkgA/pkgB", tag: TelemetryDataTag.CodeArtifact },
"event should include tagged fullPackageName",
);
});
});
});

describe("FluidDataStoreRuntime.isDirty tracking", () => {
Expand Down
31 changes: 30 additions & 1 deletion packages/runtime/runtime-utils/src/dataStoreHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ import type {
import type {
ContainerRuntimeBaseAlpha,
IContainerRuntimeBase,
IFluidDataStoreContext,
} from "@fluidframework/runtime-definitions/internal";
import { generateErrorWithStack } from "@fluidframework/telemetry-utils/internal";
import {
generateErrorWithStack,
tagCodeArtifacts,
type ITelemetryPropertiesExt,
} from "@fluidframework/telemetry-utils/internal";

interface IResponseException extends Error {
errorFromRequestFluidObject: true;
Expand Down Expand Up @@ -140,6 +145,30 @@ export function createResponseError(
};
}

/**
* Returns the canonical set of code-artifact-tagged telemetry properties identifying a data store.
* Use this anywhere a data store identity needs to appear in telemetry, so all such logs use
* consistent property names.
* @internal
*/
export function dataStoreLoadTelemetryProps(
Comment thread
markfields marked this conversation as resolved.
context: IFluidDataStoreContext,
): ITelemetryPropertiesExt {
// `packagePath` is typed as always defined, but its implementation asserts on the package being
// set — and during early load-failure paths (e.g. realize() rejecting before pkg is read) it
// can throw. Swallow that so error decoration never replaces the underlying error.
let fullPackageName: string | undefined;
try {
fullPackageName = context.packagePath.join("/");
} catch {
// `packagePath` is unset during early failures; leave fullPackageName undefined.
Comment thread
markfields marked this conversation as resolved.
}
return tagCodeArtifacts({
fullPackageName,
fluidDataStoreId: context.id,
});
}

/**
* Converts types to their alpha counterparts to expose alpha functionality.
* @legacy @alpha
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/runtime-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export {
exceptionToResponse,
responseToException,
asLegacyAlpha,
dataStoreLoadTelemetryProps,
} from "./dataStoreHelpers.js";
export {
compareFluidHandles,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 itExpects

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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Expand Down
3 changes: 1 addition & 2 deletions packages/utils/telemetry-utils/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,7 @@ export class DataProcessingError extends LoggingError implements IErrorBase, IFl
messageLike?: MessageLike,
): IFluidErrorBase {
return wrapDataProcessingErrorIfUnrecognized(
(errorMessage: string, props?: ITelemetryBaseProperties) =>
new DataProcessingError(errorMessage, props),
(errorMessage: string) => new DataProcessingError(errorMessage),
originalError,
dataProcessingCodepath,
messageLike,
Expand Down
Loading