Skip to content
Draft
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
22 changes: 22 additions & 0 deletions .changeset/race-id-dds-create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@fluidframework/datastore": minor
"@fluidframework/datastore-definitions": minor
"@fluidframework/runtime-definitions": minor
"__section": feature
---
Race-id DDS create: deterministic FWW resolution for concurrent channel creates

Check warning on line 7 in .changeset/race-id-dds-create.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Acronyms] 'FWW' has no definition. Raw Output: {"message": "[Microsoft.Acronyms] 'FWW' has no definition.", "location": {"path": ".changeset/race-id-dds-create.md", "range": {"start": {"line": 7, "column": 35}}}, "severity": "INFO"}

Check warning on line 7 in .changeset/race-id-dds-create.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Acronyms] 'DDS' has no definition. Raw Output: {"message": "[Microsoft.Acronyms] 'DDS' has no definition.", "location": {"path": ".changeset/race-id-dds-create.md", "range": {"start": {"line": 7, "column": 9}}}, "severity": "INFO"}

Adds an opt-in alpha API for resolving racing DDS creates with deterministic first-writer-wins (FWW) semantics. When multiple clients independently create a channel that they consider semantically the same (for example, a singleton DDS attached to a shared key), all clients converge to the same winner without breaking optimistic local application.

Check warning on line 9 in .changeset/race-id-dds-create.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Acronyms] 'DDS' has no definition. Raw Output: {"message": "[Microsoft.Acronyms] 'DDS' has no definition.", "location": {"path": ".changeset/race-id-dds-create.md", "range": {"start": {"line": 9, "column": 233}}}, "severity": "INFO"}

Check warning on line 9 in .changeset/race-id-dds-create.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.SentenceLength] Try to keep sentences short (< 30 words). Raw Output: {"message": "[Microsoft.SentenceLength] Try to keep sentences short (\u003c 30 words).", "location": {"path": ".changeset/race-id-dds-create.md", "range": {"start": {"line": 9, "column": 113}}}, "severity": "INFO"}

Check warning on line 9 in .changeset/race-id-dds-create.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Acronyms] 'FWW' has no definition. Raw Output: {"message": "[Microsoft.Acronyms] 'FWW' has no definition.", "location": {"path": ".changeset/race-id-dds-create.md", "range": {"start": {"line": 9, "column": 97}}}, "severity": "INFO"}

Check warning on line 9 in .changeset/race-id-dds-create.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Acronyms] 'DDS' has no definition. Raw Output: {"message": "[Microsoft.Acronyms] 'DDS' has no definition.", "location": {"path": ".changeset/race-id-dds-create.md", "range": {"start": {"line": 9, "column": 47}}}, "severity": "INFO"}

API surface (alpha):
- `IFluidDataStoreRuntime.createChannel(raceId, type, { onLost })` overload — pass a shared `raceId` agreed across racing clients; the runtime mints a unique internal channel id (`${raceId}#${guid}`).

Check failure on line 12 in .changeset/race-id-dds-create.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Dashes] Remove the spaces around ' — '. Raw Output: {"message": "[Microsoft.Dashes] Remove the spaces around ' — '.", "location": {"path": ".changeset/race-id-dds-create.md", "range": {"start": {"line": 12, "column": 76}}}, "severity": "ERROR"}
- `IAttachMessage.raceId` — optional field that propagates the race id with the attach op.

Check failure on line 13 in .changeset/race-id-dds-create.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Dashes] Remove the spaces around ' — '. Raw Output: {"message": "[Microsoft.Dashes] Remove the spaces around ' — '.", "location": {"path": ".changeset/race-id-dds-create.md", "range": {"start": {"line": 13, "column": 26}}}, "severity": "ERROR"}
- `raceResolved` event on `IFluidDataStoreRuntime` — fires with `{ raceId, winnerChannelId, loserChannelIds }`.

Check failure on line 14 in .changeset/race-id-dds-create.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Dashes] Remove the spaces around ' — '. Raw Output: {"message": "[Microsoft.Dashes] Remove the spaces around ' — '.", "location": {"path": ".changeset/race-id-dds-create.md", "range": {"start": {"line": 14, "column": 51}}}, "severity": "ERROR"}
- `OnRaceLost` callback — invoked on losing clients so the app can merge local edits from the loser channel into the winner.

Check failure on line 15 in .changeset/race-id-dds-create.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Dashes] Remove the spaces around ' — '. Raw Output: {"message": "[Microsoft.Dashes] Remove the spaces around ' — '.", "location": {"path": ".changeset/race-id-dds-create.md", "range": {"start": {"line": 15, "column": 24}}}, "severity": "ERROR"}

Resolution semantics: the first attach op for a given `raceId` (per the sequenced order) wins. Subsequent attaches with the same `raceId`, and any channel ops addressed to loser channel ids, are dropped on every client deterministically. Loser->winner redirects are persisted in a `.races` summary blob.

v1 limitations (tracked as follow-ups):
- Race-id handles, optimistic handle storage, data-store-level races, public `IChannel.dispose()`, and async `onLost` are out of scope.

Check failure on line 20 in .changeset/race-id-dds-create.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'async'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'async'?", "location": {"path": ".changeset/race-id-dds-create.md", "range": {"start": {"line": 20, "column": 104}}}, "severity": "ERROR"}
- The race overload is rejected while the data store is detached or in staging mode.
- The summary redirect table is rehydrated asynchronously on load; ops to historical losers may transiently be applied during the load window.

Check failure on line 22 in .changeset/race-id-dds-create.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'rehydrated'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'rehydrated'?", "location": {"path": ".changeset/race-id-dds-create.md", "range": {"start": {"line": 22, "column": 33}}}, "severity": "ERROR"}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ export interface IFluidDataStoreRuntime extends IEventProvider<IFluidDataStoreRu
// (undocumented)
readonly connected: boolean;
createChannel(id: string | undefined, type: string): IChannel;
// @alpha
createChannel(raceId: string, type: string, raceOptions: {
onLost?: OnRaceLost;
}): IChannel;
// (undocumented)
readonly deltaManager: IDeltaManagerErased;
readonly entryPoint: IFluidHandle<FluidObject>;
Expand Down Expand Up @@ -130,6 +134,12 @@ export interface IFluidDataStoreRuntimeEvents extends IEvent {
(event: "connected", listener: (clientId: string) => void): any;
// (undocumented)
(event: "readonly", listener: (isReadOnly: boolean) => void): any;
// @alpha
(event: "raceResolved", listener: (info: {
raceId: string;
winnerChannelId: string;
loserChannelIds: readonly string[];
}) => void): any;
}

// @beta @legacy (undocumented)
Expand All @@ -146,6 +156,9 @@ export type Jsonable<T, TReplaced = never> = boolean extends (T extends never ?
// @beta @legacy
export type JsonableTypeWith<T> = undefined | null | boolean | number | string | T | Internal_InterfaceOfJsonableTypesWith<T> | ArrayLike<JsonableTypeWith<T>>;

// @alpha
export type OnRaceLost = (loser: IChannel, winnerChannelId: string) => void;

// @beta @legacy
export type Serializable<T> = Jsonable<T, IFluidHandle>;

Expand Down
70 changes: 70 additions & 0 deletions packages/runtime/datastore-definitions/src/dataStoreRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ import type {

import type { IChannel } from "./channel.js";

/**
* Callback invoked on the losing client after a channel-creation "race"
* resolves. The runtime schedules this asynchronously after the current op
* processing step; it does not block op processing.
*
* @param loser - The local channel that lost the race. This channel's context
* has been removed from the runtime; the consumer should stop using it after
* this callback returns. The callback should read any state from `loser` and
* apply it to the winner via `runtime.getChannel(winnerChannelId)`.
* @param winnerChannelId - The id of the winning channel. Use
* `IFluidDataStoreRuntime.getChannel` to obtain a handle to it.
*
* @alpha
*/
export type OnRaceLost = (loser: IChannel, winnerChannelId: string) => void;

/**
* Events emitted by {@link IFluidDataStoreRuntime}.
* @legacy @beta
Expand All @@ -41,6 +57,21 @@ export interface IFluidDataStoreRuntimeEvents extends IEvent {
* The isReadOnly param will express the new readonly state.
*/
(event: "readonly", listener: (isReadOnly: boolean) => void);

/**
* Fired after a "race" between concurrent channel creations resolves
* deterministically across all clients. See `createChannel`'s race overload.
*
* @alpha
*/
(
event: "raceResolved",
listener: (info: {
raceId: string;
winnerChannelId: string;
loserChannelIds: readonly string[];
}) => void,
);
}

/**
Expand Down Expand Up @@ -109,6 +140,45 @@ export interface IFluidDataStoreRuntime
*/
createChannel(id: string | undefined, type: string): IChannel;

/**
* Creates a new channel that participates in a first-attach-wins "race"
* with concurrent creations on other clients.
*
* @remarks
* All clients calling this overload with the same `raceId` converge on a
* single attached channel: the first attach op sequenced for a given
* `raceId` wins, and every other client's locally-created channel becomes
* a "loser" whose subsequent ops are dropped deterministically by every
* client. The losing client may register an `onLost` callback to merge
* its local state into the winner.
*
* Each racing client receives its own locally-unique channel id; only the
* `raceId` is shared across clients. Use `IChannel.id` on the returned
* channel for local routing.
*
* Throws a `UsageError` if:
* - The document schema has not enabled the race-id channel-create feature.
* - The data store is detached or in staging mode.
* - This client has already created a racing channel with the same `raceId`.
*
* `onLost` is invoked asynchronously (after the current op processing
* step) on the losing client; it does not block op processing. If `onLost`
* is not provided and this client loses, the loser context is silently
* removed and a telemetry event is fired.
*
* @param raceId - Identifier shared across racing clients.
* @param type - Type of the channel.
* @param raceOptions - Race semantics opt-in. Presence of this argument
* marks the call as a race participant.
*
* @alpha
*/
createChannel(
raceId: string,
type: string,
raceOptions: { onLost?: OnRaceLost },
): IChannel;

/**
* Adds an existing channel to the data store.
*
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/datastore-definitions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type {
IFluidDataStoreRuntimeAlpha,
IFluidDataStoreRuntimeEvents,
IFluidDataStoreRuntimeInternalConfig,
OnRaceLost,
IDeltaManagerErased,
} from "./dataStoreRuntime.js";
export type {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ export class FluidDataStoreRuntime extends TypedEventEmitter<IFluidDataStoreRunt
get clientId(): string | undefined;
// (undocumented)
get connected(): boolean;
// (undocumented)
createChannel(idArg: string | undefined, type: string): IChannel;
createChannel(id: string | undefined, type: string): IChannel;
// (undocumented)
get deltaManager(): IDeltaManagerErased;
// (undocumented)
Expand Down
Loading
Loading