Artifact ebdfe2c8930ae9cb09820913766ffee12386acf7eab4d8cb5effbacece273540:
- File
internal/sync/seed_basis_test.go
— part of check-in
[41c092205e]
at
2026-10-28 12:52:58
on branch fix/seed-delta-drop
— seedcheck: keep deltas on a dropped seed readable; make a drop atomic
Review follow-ups to the seed drop.
A delta that arrives on a seed already dropped could never be read: it was stored on a content-less row that is never filled or asked for. It is now not stored as a delta but left a phantom and asked for again in full, on a pull, a clone's phantom pull and a push to an origin. A seed that arrives in full after deltas built on it is stored just long enough to check those deltas, judge them and rewrite them as full text, and then dropped.
seedcheck.Drop runs in a savepoint (or a transaction of its own on a pool), so a failure partway, which the after-dephantomize cascade logs and commits past, leaves the seed and its dependents as they were.
An origin answering a push no longer asks for a refused seed's phantom, and a clone asking for what it left missing no longer counts one. fossil 2.28's rebuild re-adds every size<0 row to the phantom table and deletes the store-local seedrefused and seedpending tables; the CHANGELOG notes this and the one-time drop of refused seeds that older stores kept.
The client now adds to a reply's new-phantom count rather than replacing it, so a phantom made while receiving a file continues the exchange. (user: [email protected], size: 1717) [annotate] [blame] [check-ins using]
package sync import ( "context" "testing" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/hash" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/internal/seedcheck" "github.com/danmestas/go-libfossil/internal/xfer" ) // refusedPhantom leaves r a phantom named for a refused seed, as fossil's // rebuild makes of a dropped seed's row, or a name known before its seed // arrived and was refused. func refusedPhantom(t *testing.T) (*repo.Repo, string) { t.Helper() r := setupSyncTestRepo(t) uuid := hash.SHA1([]byte("a refused seed\n")) if _, err := blob.StorePhantom(r.DB(), uuid); err != nil { t.Fatal(err) } if err := seedcheck.Refuse(r.DB(), uuid); err != nil { t.Fatal(err) } return r, uuid } // A clone that asks for what it left missing never counts a refused seed's // phantom among them: no pull asks for it, so it would stall the clone. func TestPhantomUUIDsSkipsRefused(t *testing.T) { r, uuid := refusedPhantom(t) missing, err := phantomUUIDs(context.Background(), r) if err != nil { t.Fatal(err) } if missing[uuid] { t.Fatal("a refused seed's phantom is counted missing") } } // An origin answering a push never asks the pusher for a refused seed. func TestRequestPhantomsSkipsRefused(t *testing.T) { r, uuid := refusedPhantom(t) grantNobodyPush(t, r) resp, err := HandleSync(context.Background(), r, &xfer.Message{Cards: []xfer.Card{ &xfer.PushCard{ServerCode: "s", ProjectCode: testPC}}}) if err != nil { t.Fatal(err) } for _, c := range resp.Cards { if g, ok := c.(*xfer.GimmeCard); ok && g.UUID == uuid { t.Fatal("the origin asked the pusher for a refused seed") } } }