Skip to content

fix(odsp-driver): preserve epoch in pending container state - #28156

Open
Alex Villarreal (alexvy86) wants to merge 8 commits into
microsoft:mainfrom
alexvy86:alexvy86-add-epoch-pending-state
Open

fix(odsp-driver): preserve epoch in pending container state#28156
Alex Villarreal (alexvy86) wants to merge 8 commits into
microsoft:mainfrom
alexvy86:alexvy86-add-epoch-pending-state

Conversation

@alexvy86

@alexvy86 Alex Villarreal (alexvy86) commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Description

AB#82272

ODSP uses an epoch to detect when a file has been restored server-side. Pending container state did not preserve that epoch, so loading stale pending state could learn the restored file's current epoch and replay stale operations instead of rejecting them.

This change lets document services provide opaque state for pending-state serialization and restores it before connecting to storage or the delta stream. ODSP uses the hook to preserve its epoch. Existing pending state and drivers remain compatible because the hooks and serialized field are optional.

Tests cover pending-state serialization and ODSP epoch restoration.

Reviewer Guidance

The review process is outlined in the pull request guidelines.

The IDocumentService additions affect the @legacy @beta API surface and require API Council review.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added area: tools area: loader Loader related issues area: driver Driver related issues area: repo Repo related work area: website area: tests Tests to add, test infrastructure improvements, etc area: odsp-driver public api change Changes to a public API changeset-present base: main PRs targeted against main branch labels Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Hi! Thank you for opening this PR. Want me to review it?

Based on the diff (382 lines, 17 files), I've queued these reviewers:

  • Correctness — logic errors, race conditions, lifecycle issues
  • Security — vulnerabilities, secret exposure, injection
  • API Compatibility — breaking changes, release tags, type design
  • Performance — algorithmic regressions, memory leaks
  • Testing — coverage gaps, hollow tests

How this works

  • Adjust the reviewer set by ticking/unticking boxes above. Reviewer toggles alone don't trigger anything.

  • Tick Start review below to dispatch the review fleet.

  • After review finishes, tick Start review again to request another run — it auto-resets after each dispatch.

  • This comment updates as new commits land; your reviewer selections are preserved.

  • Start review

Copilot AI left a comment

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.

🟡 Changes recommended

The frozen-service path can lose the persisted epoch, and the moderate API-contract and telemetry issues remain unresolved.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Preserves ODSP epochs in pending container state so stale operations are rejected after server-side restoration.

Changes:

  • Adds optional driver-state serialization and restoration hooks.
  • Restores ODSP epochs before storage or delta-stream connections.
  • Adds tests, API updates, and release metadata.
File summaries
File Review
packages/test/test-service-load/src/faultInjectionDriver.ts Forwards the new hooks in test services.
packages/loader/driver-utils/src/documentServiceProxy.ts Proxies the driver-state hooks.
packages/loader/container-loader/src/test/serializedStateManager.spec.ts Tests driver-state serialization.
packages/loader/container-loader/src/serializedStateManager.ts Serializes opaque driver state.
packages/loader/container-loader/src/frozenServices.ts Critical: Discards restored state without a wrapped service, allowing offline re-serialization to remove the epoch.
packages/loader/container-loader/src/createAndLoadContainerUtils.ts Captures driver state during full-state serialization.
packages/loader/container-loader/src/container.ts Restores state before connecting.
packages/drivers/odsp-driver/src/test/joinSessionCacheTests.spec.ts Nit: Lacks integration coverage for reloading stale state against a mismatched ODSP epoch.
packages/drivers/odsp-driver/src/odspDocumentService.ts Moderate: Restored epochs are incorrectly reported as not originating from cache.
packages/common/driver-definitions/src/storage.ts Moderate: The public contract does not document JSON-serializability, undefined, or restoration timing requirements.
packages/common/driver-definitions/api-report/driver-definitions.legacy.beta.api.md Updates the legacy-beta API surface; API Council review is required.
.changeset/every-feet-post.md Nit: Omits consumer-meaningful container-loader and odsp-driver package entries.
Review details

Suppressed comments (2)

.changeset/every-feet-post.md:2

  • The changeset only assigns the changelog entry to driver-definitions, although the consumer-visible fix is implemented in container-loader and odsp-driver. Repository guidance says to include every package whose change is meaningful to consumers (.changeset/README.md:107-111); otherwise those packages ship this behavior without a changelog entry.
"@fluidframework/driver-definitions": minor

packages/drivers/odsp-driver/src/test/joinSessionCacheTests.spec.ts:66

  • This test only invokes the two methods on one service instance; it never serializes pending state, reloads it through Container, or verifies that a different server epoch rejects the stale state before storage/delta connection. The critical cross-package restoration path can regress while this test still passes. Add an integration test that reloads serialized pending state against a mismatched ODSP epoch and asserts the non-retryable epoch failure.
		const service = await odspDocumentServiceFactory.createDocumentService(odspResolvedUrl);
		service.setDriverState?.("epoch1");
		assert.strictEqual(service.getDriverState?.(), "epoch1");
  • Files reviewed: 12/12 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/loader/container-loader/src/frozenServices.ts Outdated
Comment thread packages/common/driver-definitions/src/storage.ts Outdated
Comment thread packages/drivers/odsp-driver/src/odspDocumentService.ts Outdated
Comment thread packages/drivers/odsp-driver/src/odspDocumentService.ts Outdated
@alexvy86

Copy link
Copy Markdown
Contributor Author

/azp run Build - protocol-definitions,Build - test-tools,server-gitrest,server-gitssh,server-historian,server-routerlicious,Build - client packages,repo-policy-check

@alexvy86

Copy link
Copy Markdown
Contributor Author

/azp run Build - api-markdown-documenter,Build - benchmark-tool,Build - build-common,Build - build-tools,Build - common-utils,Build - eslint-config-fluid

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
6 pipeline(s) were filtered out due to trigger conditions.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 2 pipeline(s).
6 pipeline(s) were filtered out due to trigger conditions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b2fe1f5e-932c-4ea8-9681-14d8fd2d0709
@alexvy86

Copy link
Copy Markdown
Contributor Author

/azp run Build - protocol-definitions,Build - test-tools,server-gitrest,server-gitssh,server-historian,server-routerlicious,Build - client packages,repo-policy-check

@alexvy86

Copy link
Copy Markdown
Contributor Author

/azp run Build - api-markdown-documenter,Build - benchmark-tool,Build - build-common,Build - build-tools,Build - common-utils,Build - eslint-config-fluid

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
6 pipeline(s) were filtered out due to trigger conditions.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 2 pipeline(s).
6 pipeline(s) were filtered out due to trigger conditions.

Copilot AI left a comment

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.

🟡 Changes recommended

Two moderate issues could allow incomplete restoration contracts or stale driver state to be re-serialized.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 14/14 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread packages/common/driver-definitions/src/storage.ts Outdated
Comment thread packages/loader/container-loader/src/frozenServices.ts Outdated
Comment thread packages/drivers/odsp-driver/src/odspDocumentService.ts Outdated

Copilot AI left a comment

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.

🟡 Changes recommended

The persistence API, frozen-state fallback, and timing-dependent rejection test need correction.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

packages/common/driver-definitions/src/storage.ts:381

  • The two independently optional methods allow a service to expose state that the loader serializes but can never restore (or to expose only a setter), and wrappers that always define both methods hide that mismatch further. Model this as one optional persistence capability whose getter and setter are both required so a partial implementation is not representable.
	getDriverState?(): unknown;

packages/loader/container-loader/src/frozenServices.ts:124

  • Using ?? treats a valid JSON-serializable null returned by the inner getter as absent, and it also replaces an inner getter's deliberate undefined (“no state”) with the wrapper's cached value. This can omit valid state or reserialize stale state; fall back only when the inner getter itself is not implemented.
		return this.documentService?.getDriverState?.() ?? this.driverState;
  • Files reviewed: 14/14 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread packages/test/test-end-to-end-tests/src/test/pointInTime/epochMismatch.spec.ts Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b2fe1f5e-932c-4ea8-9681-14d8fd2d0709
@alexvy86

Copy link
Copy Markdown
Contributor Author

Regarding the suggested legacy-pending-state telemetry: I am not adding it in this PR. The ODSP service cannot distinguish an absent legacy value from a load that did not originate from pending state without adding another loader-to-driver signal, and the legacy pending-state population is transient. The default remains backward-compatible and permissive.

@alexvy86

Copy link
Copy Markdown
Contributor Author

/azp run Build - protocol-definitions,Build - test-tools,server-gitrest,server-gitssh,server-historian,server-routerlicious,Build - client packages,repo-policy-check

@alexvy86

Copy link
Copy Markdown
Contributor Author

/azp run Build - api-markdown-documenter,Build - benchmark-tool,Build - build-common,Build - build-tools,Build - common-utils,Build - eslint-config-fluid

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
6 pipeline(s) were filtered out due to trigger conditions.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 2 pipeline(s).
6 pipeline(s) were filtered out due to trigger conditions.

@anthony-murphy

Copy link
Copy Markdown
Contributor

Deep Review: Reasonable call — the legacy-absent case can't be distinguished from a non-pending-state load without adding another loader-to-driver signal, and that population is transient, so declining the telemetry here is fine and the permissive default preserves back-compat. Two narrower observability points are still open and captured inline rather than here: the restored epoch is logged as a cache source in EpochLearnedFirstTime, and the loader-level set call hard-fails silently on malformed or foreign driver state. Neither blocks.

Comment thread packages/drivers/odsp-driver/src/odspDocumentService.ts
Comment thread packages/loader/container-loader/src/container.ts
alexvy86 added 2 commits September 3, 2026 10:36
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b2fe1f5e-932c-4ea8-9681-14d8fd2d0709
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b2fe1f5e-932c-4ea8-9681-14d8fd2d0709
@alexvy86

Copy link
Copy Markdown
Contributor Author

/azp run Build - protocol-definitions,Build - test-tools,server-gitrest,server-gitssh,server-historian,server-routerlicious,Build - client packages,repo-policy-check

@alexvy86

Copy link
Copy Markdown
Contributor Author

/azp run Build - api-markdown-documenter,Build - benchmark-tool,Build - build-common,Build - build-tools,Build - common-utils,Build - eslint-config-fluid

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
6 pipeline(s) were filtered out due to trigger conditions.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 2 pipeline(s).
6 pipeline(s) were filtered out due to trigger conditions.

Comment thread packages/loader/container-loader/src/container.ts
alexvy86 added 2 commits September 3, 2026 13:55
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b2fe1f5e-932c-4ea8-9681-14d8fd2d0709
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b2fe1f5e-932c-4ea8-9681-14d8fd2d0709
Comment thread packages/drivers/odsp-driver/src/odspDocumentService.ts
@alexvy86
Alex Villarreal (alexvy86) marked this pull request as ready for review September 3, 2026 22:43
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b2fe1f5e-932c-4ea8-9681-14d8fd2d0709
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🔗 No broken links found! ✅

Your attention to detail is admirable.

linkcheck output

$ start-server-and-test "npm run serve -- --host 127.0.0.1 --no-open" http://127.0.0.1:3000 check-links
1: starting server using command "npm run serve -- --host 127.0.0.1 --no-open"
and when url "[ 'http://127.0.0.1:3000' ]" is responding with HTTP status code 200
running tests using command "npm run check-links"


> fluid-framework-website@0.0.0 serve
> docusaurus serve --host 127.0.0.1 --no-open

[SUCCESS] Serving "build" directory at: http://127.0.0.1:3000/

> fluid-framework-website@0.0.0 check-links
> linkcheck http://127.0.0.1:3000 --skip-file skipped-urls.txt

Crawling...

Stats:
  340615 links
    2042 destination URLs
    2297 URLs ignored
       0 warnings
       0 errors


@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Bundle size comparison

Base commit: 253e3ad858341d185338d3a8e02a92a6e6246b54
Head commit: 6046dc720d9a8fc36ab081b9e8fd86e18e684a8e

Notable changes

  • 🔴 odspClient.js: parsed 605273 → 606410 (+1137), gzip 162673 → 163107 (+434)
  • 🔴 odspDriver.js: parsed 105689 → 106451 (+762), gzip 32932 → 33222 (+290)
Per-bundle deltas

@fluid-example/bundle-size-tests

  • fluidFrameworkAllAlpha.js: parsed 804412 → 804468 (+56), gzip 220958 → 221015 (+57)
  • azureClient.js: parsed 634027 → 634459 (+432), gzip 169872 → 170081 (+209)
  • 🔴 odspClient.js: parsed 605273 → 606410 (+1137), gzip 162673 → 163107 (+434)
  • aqueduct.js: parsed 537952 → 537963 (+11), gzip 144503 → 144547 (+44)
  • fluidFramework.js: parsed 413678 → 413711 (+33), gzip 117281 → 117287 (+6)
  • sharedTree.js: parsed 403057 → 403083 (+26), gzip 114717 → 114724 (+7)
  • containerRuntime.js: parsed 314719 → 314701 (-18), gzip 86334 → 86332 (-2)
  • sharedString.js: parsed 175239 → 175246 (+7), gzip 49670 → 49677 (+7)
  • experimentalSharedTree.js: parsed 161846 → 161846 (0), gzip 46722 → 46722 (0)
  • matrix.js: parsed 159618 → 159625 (+7), gzip 45914 → 45921 (+7)
  • loader.js: parsed 147327 → 147665 (+338), gzip 40038 → 40139 (+101)
  • 🔴 odspDriver.js: parsed 105689 → 106451 (+762), gzip 32932 → 33222 (+290)
  • directory.js: parsed 65669 → 65676 (+7), gzip 18493 → 18501 (+8)
  • 578.js: parsed 58686 → 58686 (0), gzip 17657 → 17657 (0)
  • odspPrefetchSnapshot.js: parsed 45921 → 45902 (-19), gzip 15346 → 15357 (+11)
  • map.js: parsed 45820 → 45827 (+7), gzip 14119 → 14126 (+7)
  • 252.js: parsed 44384 → 44384 (0), gzip 13741 → 13741 (0)
  • summarizerDelayLoadedModule.js: parsed 31287 → 31287 (0), gzip 7929 → 7929 (0)
  • socketModule.js: parsed 26992 → 26962 (-30), gzip 8017 → 8053 (+36)
  • createNewModule.js: parsed 12464 → 12464 (0), gzip 4792 → 4805 (+13)
  • summaryModule.js: parsed 3888 → 3888 (0), gzip 1874 → 1874 (0)
  • connectionState.js: parsed 909 → 909 (0), gzip 500 → 500 (0)
  • sharedTreeAttributes.js: parsed 845 → 852 (+7), gzip 496 → 505 (+9)
  • debugAssert.js: parsed 429 → 429 (0), gzip 299 → 299 (0)
  • FluidFramework-HashFallback.js: parsed 419 → 419 (0), gzip 313 → 313 (0)

@anthony-murphy

Copy link
Copy Markdown
Contributor

Deep Review

Reviewed commit 6046dc7 on 2026-09-03.

Readiness: 9/10 — ALMOST READY

Close to sign-off; no blocking defects against the changed files. Since the last review the identity-ownership request landed — pending state carrying driverState now delegates same-file validation to the driver, and ODSP validates its hashed document id alongside the epoch. The disposal leak and the cross-driver fail-open are both closed: a service that can't apply driverState now disposes and rejects the load instead of silently reconnecting without the epoch. One low-risk item remains inline: the driver identity contract is enforced by convention (only ODSP validates a foreign documentId) rather than by shared coverage. API Council sign-off on the new @legacy @beta surface is the main gate.

Path to Ready

  • Resolve inline threads

Context for Reviewers

  • Adds a single optional driverStatePersistence (get/set) capability to the @legacy @beta IDocumentService surface — API Council sign-off is required before merge.
  • Closes a real gap: pending-state serialization preserved runtime and snapshot state but not the driver epoch, letting a restored file overwrite the client epoch instead of throwing fileOverwrittenInStorage.
  • Grouping capture+restore under one optional property makes support atomic — TypeScript structurally prevents capture-without-restore; the area owner endorsed this shape.
  • The driverState? field is additive and optional — an older container-loader ignores it and silently reintroduces the bug for that loader/driver combination, so protection is version-gated on the loader.
  • Identity/epoch ownership living in the driver continues the boundary set in Prepare for ODSP cache to be able to store and retrieve more entries #5661 (cache/epoch management behind EpochTracker); ODSP persists { documentId: hashedDocumentId, epoch }, and the hashed id isn't customer-identifying (no-CII caveat documented).
For human reviewer
  • Needs human judgment — API Council sign-off on the @legacy @beta IDocumentService.driverStatePersistence addition (single bundled get/set capability).
  • Needs human judgment — Whether documentation-only enforcement of the driver identity contract is acceptable long-term, or whether the driver should fully own same-file validation with loader-enforced coverage. This is the design context behind the remaining inline note.
  • Needs human judgment — Ratify reusing the existing "cache" fetchType for restored-epoch telemetry (documented tradeoff) vs. a distinct "pendingState" provenance; an epoch owner should confirm.
  • Needs human judgment — Ratify the post-construction set() setter over a constructor/factory parameter (avoids a createDocumentService signature change rippling across every host/proxy/fault-injection factory).
  • Cannot be assessed by the pipeline — Runtime behavior of the end-to-end epochMismatch test against live ODSP (restore → resume → fileOverwrittenInStorage), and cross-client durability of the persisted driverState blob across software upgrades / loader-version skew.
Review history (5 prior reviews)
  • de35c69 2026-09-03 · 9/10 — no blocking defects; schema-version and cross-driver fail-open remained inline
  • 6a0de23 2026-09-03 · 9/10 — disposal leak fixed; schema-version polish remains
  • e706d14 2026-09-03 · 6/10 — disposal leak on restore-failure flagged inline
  • 289948e 2026-09-03 · 8/10 — all five inline threads resolved; two polish items remained
  • 2bd9c00 2026-09-02 · 8/10 — additive fix; two open reviewer threads remaining

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

Labels

area: driver Driver related issues area: loader Loader related issues area: odsp-driver area: repo Repo related work area: tests Tests to add, test infrastructure improvements, etc area: tools area: website base: main PRs targeted against main branch changeset-present deep-review public api change Changes to a public API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants