Skip to content

Seed the checkpoint scope-key hasher for deterministic restore#526

Open
PratikDhanave wants to merge 1 commit into
microsoft:mainfrom
PratikDhanave:fix-checkpoint-hasher-seed
Open

Seed the checkpoint scope-key hasher for deterministic restore#526
PratikDhanave wants to merge 1 commit into
microsoft:mainfrom
PratikDhanave:fix-checkpoint-hasher-seed

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

Summary

Checkpoint.UnmarshalJSON (workflow/internal/checkpoint/checkpoint_json.go) rebuilds StateData with scopeKeyHasherImpl, whose Hash used a zero-value maphash.Hash:

func (scopeKeyHasherImpl) Hash(s workflow.ScopeKey) uint64 {
	var h maphash.Hash   // zero value → new RANDOM seed on first use
	s.Hash(&h)
	return h.Sum64()
}

A zero-value maphash.Hash selects a fresh random seed on first use, so the same ScopeKey hashes to a different value on every call. On the hashmap.Map produced by unmarshal this means:

  • Load/Delete/overwrite break — the value stored during unmarshal is hashed with one seed, and a later Load re-hashes with a different seed and misses.
  • Shared-scope keys no longer collapse — two keys that are Equal (shared scope ignores the executor ID) get different hashes and are kept as separate entries, so StateManager.ImportState re-imports both and which value survives a restore is non-deterministic.

The sibling hasher in workflow/internal/execution/state.go already seeds with a fixed process-wide theSeed; the checkpoint copy just forgot to.

Fix

Add a fixed process-wide scopeKeyHashSeed = maphash.MakeSeed() and h.SetSeed(scopeKeyHashSeed) in Hash, mirroring state.go.

Public API

No exported symbols change.

Tests

Adds TestCheckpoint_JsonRoundtrip_StateDataRemainsLoadable (black-box, in json_test.go): round-trips a Checkpoint through JSON and asserts its StateData is still Load-able. It fails before this change (Load returns false) and passes after. (Existing tests missed it because they only consume restored StateData via .All() iteration, which is seed-independent.)

Checkpoint.UnmarshalJSON rebuilds StateData with scopeKeyHasherImpl, whose
Hash used a zero-value maphash.Hash. A zero-value maphash.Hash selects a new
random seed on first use, so the same ScopeKey hashes differently on every
call. On a map restored from JSON this breaks Load/Delete (the value stored
during unmarshal can no longer be found) and stops shared-scope keys from
collapsing. state.go's sibling ScopeKey hasher already seeds with a fixed
process-wide seed; do the same here.

Adds a black-box test that round-trips a Checkpoint through JSON and asserts
its StateData is still Loadable — false before this change, true after.
Copilot AI review requested due to automatic review settings July 17, 2026 18:41
@PratikDhanave
PratikDhanave requested a review from a team as a code owner July 17, 2026 18:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a determinism bug in checkpoint JSON restore by ensuring the ScopeKey hasher used to rebuild Checkpoint.StateData is seeded consistently, so restored state remains usable via Load/Delete and key-collapsing behavior works as intended.

Changes:

  • Seed the checkpoint-local ScopeKey hasher with a process-wide maphash.Seed during hashing.
  • Add a regression test that round-trips a Checkpoint JSON payload and asserts restored StateData.Load(key) succeeds.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
workflow/internal/checkpoint/checkpoint_json.go Seed the ScopeKey hasher used during JSON unmarshal so hashing is consistent across calls within the process.
workflow/internal/checkpoint/json_test.go Add a test that validates restored StateData remains Load-able after JSON unmarshal.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants