Artifact bbdc3d0c9bad40be7ad5fc20f414907eb14d97533ad9f45f01e403f75606e06f:
- File
internal/sync/seed_basis.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: 2907) [annotate] [blame] [check-ins using]
package sync import ( "fmt" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/blob" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/manifest" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/internal/seedcheck" "github.com/danmestas/go-libfossil/internal/shun" ) // A delta and a dropped seed it is built on (horizon ยง5.2.3). The seed's // bytes are the only way such a delta can be read, so a store either reads // the delta while it has them (storeSeedBasis) or does not keep the delta as // one at all (onRefusedBasis). // onRefusedBasis reports whether deltaSrc names a seed this store refused, // whose content is gone. A delta on it could never be read, so it is not // stored: uuid becomes a phantom instead and is asked for again, which a // peer answers with its full text. A shunned uuid is left to the caller, // which drops it. func onRefusedBasis(q db.Querier, uuid, deltaSrc string) (bool, error) { if q == nil || uuid == "" { panic("sync.onRefusedBasis: q must not be nil and uuid must not be empty") } if deltaSrc == "" { return false, nil } refused, err := seedcheck.IsRefusedUUID(q, deltaSrc) if err != nil || !refused { return false, err } if shunned, err := shun.IsShunned(q, uuid); err != nil || shunned { return false, err } if _, err := blob.StorePhantom(q, uuid); err != nil { return false, fmt.Errorf("sync: delta %s on a refused seed: %w", uuid, err) } return true, nil } // storeSeedBasis is what storeResolvedContent does with a seed's full text // before refusing it (errSeed). When deltas already stored are built on the // seed's phantom row, the seed is stored just long enough to check those // deltas against their names (the pass), judge any pending on it, and // rewrite them as full text, and is then dropped. Otherwise nothing is // stored. func storeSeedBasis(r *repo.Repo, pass *receivePass, uuid string, full []byte, linker *manifest.ReceiveLinker) error { if r == nil || pass == nil || uuid == "" || full == nil { panic("sync.storeSeedBasis: r, pass, uuid and full must be set") } return withLinkerTx(r, linker, func(tx *db.Tx) error { rid, held := blob.Exists(tx, uuid) if !held { return nil } var size int64 var deps int err := tx.QueryRow("SELECT size, (SELECT count(*) FROM delta WHERE srcid=?1) FROM blob WHERE rid=?1", rid).Scan(&size, &deps) if err != nil { return fmt.Errorf("sync: seed basis %s: %w", uuid, err) } if size >= 0 || deps == 0 { return nil } if _, _, _, err := writeResolved(tx, uuid, full, nil); err != nil { return err } if err := pass.verifyFrom(tx, rid); err != nil { return err } if _, err := seedcheck.JudgeFilled(tx, []libfossil.FslID{rid}); err != nil { return err } return seedcheck.Drop(tx, rid, uuid) }) }