CAT-D5: the publishing builds are written with compare-and-set on the key's revision, so two first publishes at once both count
A blind write after a read could lose the build of the publish that wrote first until a bundle restart learned it again. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -154,23 +154,62 @@ func learnPublishing(windows kept[holder.Window], caller string) {
|
||||
logf("node_haves names no build of %s: the direct-write window counts it at its next publish", caller)
|
||||
return
|
||||
}
|
||||
pub := kept[holder.Publishing]{state: "windows", key: func(p holder.Publishing) string { return holder.PublishingKey }}
|
||||
cur, _, err := pub.Get(holder.PublishingKey)
|
||||
learnt, err := learnInto(stdio.State("windows"), module, a.Keys[caller].FP, learnAttempts)
|
||||
if err != nil {
|
||||
logf("the publishing builds cannot be read: %v", err)
|
||||
logf("the publishing build of %s is not kept: %v", caller, err)
|
||||
return
|
||||
}
|
||||
cur.Name = holder.PublishingKey
|
||||
if cur.Learn(module, a.Keys[caller].FP) {
|
||||
if err := pub.Put(cur); err != nil {
|
||||
logf("the publishing build of %s is not kept: %v", caller, err)
|
||||
return
|
||||
}
|
||||
if learnt {
|
||||
logf("%s's build %s publishes through the seats", module, a.Keys[caller].FP)
|
||||
}
|
||||
learned.Store(caller, true)
|
||||
}
|
||||
|
||||
// learnAttempts bounds learnInto's tries against other writers of the key.
|
||||
const learnAttempts = 8
|
||||
|
||||
// casState is the part of a kept state learnInto writes through: a read with its revision, a create, an update at a
|
||||
// revision (stdio.KeptState).
|
||||
type casState interface {
|
||||
Get(key string) (*stdio.StateEntry, error)
|
||||
Create(key string, value any) (uint64, error)
|
||||
Update(key string, value any, revision uint64) (uint64, error)
|
||||
}
|
||||
|
||||
// learnInto adds fp for module to the publishing builds kept at holder.PublishingKey with compare-and-set on the key's
|
||||
// revision: two first publishes at once each read, add and write only if nobody wrote meanwhile, else read again, so
|
||||
// neither is lost. It answers whether fp was new.
|
||||
func learnInto(state casState, module, fp string, attempts int) (bool, error) {
|
||||
for i := 0; i < attempts; i++ {
|
||||
e, err := state.Get(holder.PublishingKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var cur holder.Publishing
|
||||
if e != nil {
|
||||
if err := json.Unmarshal(e.Value, &cur); err != nil {
|
||||
return false, fmt.Errorf("%s holds no publishing builds: %w", holder.PublishingKey, err)
|
||||
}
|
||||
}
|
||||
cur.Name = holder.PublishingKey
|
||||
if !cur.Learn(module, fp) {
|
||||
return false, nil
|
||||
}
|
||||
if e == nil {
|
||||
_, err = state.Create(holder.PublishingKey, cur)
|
||||
} else {
|
||||
_, err = state.Update(holder.PublishingKey, cur, e.Revision)
|
||||
}
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if !errors.Is(err, stdio.ErrChanged) {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return false, fmt.Errorf("%s was written by another writer first %d times: %w", holder.PublishingKey, attempts, stdio.ErrChanged)
|
||||
}
|
||||
|
||||
func arg[T any](args map[string]any) (T, error) {
|
||||
var v T
|
||||
clean := map[string]any{}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
|
||||
"distribution/internal/holder"
|
||||
)
|
||||
|
||||
// casMem is a kept state with revisions; before writes it lets another writer write first, once.
|
||||
type casMem struct {
|
||||
value []byte
|
||||
rev uint64
|
||||
before func()
|
||||
}
|
||||
|
||||
func (m *casMem) Get(string) (*stdio.StateEntry, error) {
|
||||
if m.value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &stdio.StateEntry{Value: m.value, Revision: m.rev}, nil
|
||||
}
|
||||
func (m *casMem) write(v any, at uint64) (uint64, error) {
|
||||
if f := m.before; f != nil {
|
||||
m.before = nil
|
||||
f()
|
||||
}
|
||||
if m.rev != at {
|
||||
return 0, stdio.ErrChanged
|
||||
}
|
||||
m.value, _ = json.Marshal(v)
|
||||
m.rev++
|
||||
return m.rev, nil
|
||||
}
|
||||
func (m *casMem) Create(_ string, v any) (uint64, error) { return m.write(v, 0) }
|
||||
func (m *casMem) Update(_ string, v any, rev uint64) (uint64, error) { return m.write(v, rev) }
|
||||
|
||||
// TestTwoFirstPublishesAtOnceAreBothKept: the publishing builds are written with compare-and-set, so a writer that lost
|
||||
// the race reads again and neither build is lost (A3f finding 2).
|
||||
func TestTwoFirstPublishesAtOnceAreBothKept(t *testing.T) {
|
||||
m := &casMem{}
|
||||
m.before = func() {
|
||||
// Another tools process learns mesh-build-go's build first.
|
||||
m.value, _ = json.Marshal(holder.Publishing{Name: holder.PublishingKey, Modules: map[string][]string{"mesh-build-go": {"fp-go"}}})
|
||||
m.rev = 1
|
||||
}
|
||||
if learnt, err := learnInto(m, "mesh-build-node", "fp-node", 3); err != nil || !learnt {
|
||||
t.Fatalf("learnInto = %v, %v", learnt, err)
|
||||
}
|
||||
var got holder.Publishing
|
||||
_ = json.Unmarshal(m.value, &got)
|
||||
want := map[string][]string{"mesh-build-go": {"fp-go"}, "mesh-build-node": {"fp-node"}}
|
||||
if !reflect.DeepEqual(got.Modules, want) {
|
||||
t.Errorf("kept %v, want both builds %v", got.Modules, want)
|
||||
}
|
||||
if learnt, err := learnInto(m, "mesh-build-node", "fp-node", 3); err != nil || learnt {
|
||||
t.Errorf("learning a kept build again = %v, %v, want not new and no write", learnt, err)
|
||||
}
|
||||
m.before = func() { m.rev++ }
|
||||
if _, err := learnInto(m, "mesh-build-oci", "fp-oci", 1); err == nil {
|
||||
t.Error("a write lost on every attempt answered no error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user