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: 8 additions & 0 deletions workflow/internal/checkpoint/checkpoint_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,16 @@ type scopeKeyHasherImpl struct{}

var scopeKeyHasherInstance hashmap.Hasher[workflow.ScopeKey] = scopeKeyHasherImpl{}

// scopeKeyHashSeed is a fixed process-wide seed. maphash.Hash's zero value
// picks a new random seed on first use, so without SetSeed the same key would
// hash differently on every call — breaking Load/Delete and shared-scope key
// collapse on a map restored from JSON. state.go seeds its ScopeKey hasher the
// same way.
var scopeKeyHashSeed = maphash.MakeSeed()

func (scopeKeyHasherImpl) Hash(s workflow.ScopeKey) uint64 {
var h maphash.Hash
h.SetSeed(scopeKeyHashSeed)
s.Hash(&h)
return h.Sum64()
}
Expand Down
26 changes: 26 additions & 0 deletions workflow/internal/checkpoint/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package checkpoint_test

import (
"encoding/json"
"fmt"
"reflect"
"testing"

Expand Down Expand Up @@ -115,3 +116,28 @@ func TestRunnerStateData_JsonRoundtrip(t *testing.T) {
t.Fatalf("ResponsePortOwners = %+v, want %+v", got.ResponsePortOwners, state.ResponsePortOwners)
}
}

// A Checkpoint's StateData map is rebuilt from JSON on Unmarshal. Its scope-key
// hasher must use a fixed seed: a zero-value maphash.Hash picks a new random
// seed on every call, so Load on a restored map would miss the key it just
// stored (and shared-scope keys would not collapse).
func TestCheckpoint_JsonRoundtrip_StateDataRemainsLoadable(t *testing.T) {
key := workflow.ScopeKey{ID: workflow.ScopeID{ExecutorID: "exec1"}, Key: "k"}
keyJSON, err := json.Marshal(key)
if err != nil {
t.Fatalf("marshal key: %v", err)
}
valJSON, err := json.Marshal(workflow.AnyPortableValue("v"))
if err != nil {
t.Fatalf("marshal value: %v", err)
}
data := []byte(fmt.Sprintf(`{"StateData":[{"Key":%s,"Value":%s}]}`, keyJSON, valJSON))

var cp checkpoint.Checkpoint
if err := json.Unmarshal(data, &cp); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if _, ok := cp.StateData.Load(key); !ok {
t.Fatal("restored StateData.Load(key) = false: the scope-key hasher is not deterministic across calls")
}
}