ADR 0627 §2: a transfer read back from the bus takes its first chunk

A transfer is kept in a key-value state as JSON, and its empty Chunks map is omitted, so the one
read back for the first chunk had none and Take panicked: every publish in parts through
BusTransfers. The unit tests' transfers now round-trip JSON as the bus does. Found by the forge
module's whole test (CAT-G3).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 14:19:21 +01:00
parent 54e1ecce45
commit 0424e87399
3 changed files with 31 additions and 5 deletions
+4
View File
@@ -1,10 +1,13 @@
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc= github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I= github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24= github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
@@ -19,3 +22,4 @@ golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
+4
View File
@@ -240,6 +240,10 @@ func (r *Receiver[M]) Take(id, opener, digest string, sequence int, last bool, d
if err := r.Objects.Put(r.chunkName(id, digest, sequence), data); err != nil { if err := r.Objects.Put(r.chunkName(id, digest, sequence), data); err != nil {
return Received{}, fmt.Errorf("the objects did not take the chunk: %w", err) return Received{}, fmt.Errorf("the objects did not take the chunk: %w", err)
} }
// A transfer read back from a key-value state has no Chunks until one was taken: the empty map is kept omitted.
if t.Chunks == nil {
t.Chunks = map[string]int{}
}
t.Chunks[digest] = sequence + 1 t.Chunks[digest] = sequence + 1
t.Last = r.now() t.Last = r.now()
if last { if last {
+23 -5
View File
@@ -3,6 +3,7 @@ package parts
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json"
"errors" "errors"
"reflect" "reflect"
"strings" "strings"
@@ -28,14 +29,31 @@ func (m memObjects) Used() (uint64, error) {
return u, nil return u, nil
} }
type memTransfers[M any] map[string]Transfer[M] // memTransfers keeps each transfer as the bus keeps it, its JSON, so every case reads back what a key-value state
// gives (BusTransfers): an empty map kept with omitempty comes back nil.
type memTransfers[M any] map[string][]byte
func (m memTransfers[M]) Get(id string) (Transfer[M], bool, error) { t, ok := m[id]; return t, ok, nil } func (m memTransfers[M]) Get(id string) (Transfer[M], bool, error) {
func (m memTransfers[M]) Put(t Transfer[M]) error { m[t.ID] = t; return nil } var t Transfer[M]
func (m memTransfers[M]) Delete(id string) error { delete(m, id); return nil } raw, ok := m[id]
if !ok {
return t, false, nil
}
return t, true, json.Unmarshal(raw, &t)
}
func (m memTransfers[M]) Put(t Transfer[M]) error {
raw, err := json.Marshal(t)
m[t.ID] = raw
return err
}
func (m memTransfers[M]) Delete(id string) error { delete(m, id); return nil }
func (m memTransfers[M]) List() ([]Transfer[M], error) { func (m memTransfers[M]) List() ([]Transfer[M], error) {
var out []Transfer[M] var out []Transfer[M]
for _, t := range m { for id := range m {
t, _, err := m.Get(id)
if err != nil {
return nil, err
}
out = append(out, t) out = append(out, t)
} }
return out, nil return out, nil