Artifact 17f5d0bd3378af0e3f8320afbb397c4afa97b83a481df8726c7770c58ac9b0eb:
- File
internal/sync/receive_budget.go
— part of check-in
[dd7401ad01]
at
2026-10-29 03:06:02
on branch main
— feat(sync): return a receive-budget refusal to the host as *RefusedError
With HandleOpts.MaxStoredBytes set, a refused push rolls its request transaction back and replies with one error card. A host whose storage treats any rollback as an abort of the whole call (a Durable Object running the request in one transactionSync) discards that reply, so the client saw only "transaction rolled back".
HandleSyncWithOpts now returns the encoded reply together with a *RefusedError whose Message is the error card's text, matchable with errors.As. Such a host can report or render the refusal itself without parsing xfer; a host serving HTTP sends the reply as before. Only the budget refusal reports this way, so no released behaviour changes: the budget is unreleased and opt-in. (user: [email protected], size: 4277) [annotate] [blame] [check-ins using] [more...]
package sync import ( "database/sql" "errors" "fmt" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/blob" ) // RefusedError is returned, beside the reply, by a HandleSyncWithOpts request // that HandleOpts.MaxStoredBytes refused. Message is the reply's one error // card, so a host whose storage aborts on the request's rollback can report // the refusal without parsing the reply. type RefusedError struct { Message string } // Error returns Message. func (e *RefusedError) Error() string { return e.Message } // overBudget reports the refusal text for an artifact whose stored form // would exceed the handler's MaxStoredBytes, "" when it fits, there is no // budget, or the store already holds it (nothing would be written). payload // is a file card's content, a delta when the card names a basis; storedBlob // is a cfile card's stored form, nil for a file card. func (h *handler) overBudget(uuid string, payload, storedBlob []byte) (string, error) { if h.maxStored < 0 { panic("handler.overBudget: maxStored must not be negative") } if h.maxStored == 0 { return "", nil } if held, err := holdsContent(h.repo.DB(), uuid); err != nil || held { return "", err } n, err := budgetSize(payload, storedBlob, h.maxStored) if err != nil { return "", err } if n <= h.maxStored { return "", nil } msg := fmt.Sprintf("artifact %s is %d bytes stored; this repository stores at most %d bytes per artifact", uuid, n, h.maxStored) // The name and check-in are read from this repository, so only a pusher // who may read it learns them (phantomName also skips private check-ins). // Today push implies read (auth.CanRead), so this holds by construction. if !h.canRead() { return msg, nil } name, checkin, err := phantomName(h.repo.DB(), uuid) if err != nil { return "", err } if name != "" { msg += fmt.Sprintf(", named %s by check-in %s", name, checkin) } return msg, nil } // budgetSize is the length payload occupies in blob.content: storedBlob's // when the card carried one, else payload's blob.Compress form. A payload // that cannot reach limit compressed is not compressed just to measure it, // and is reported as its upper bound, which is within limit. func budgetSize(payload, storedBlob []byte, limit int) (int, error) { if storedBlob != nil { return len(storedBlob), nil } if bound := compressBound(len(payload)); bound <= limit { return bound, nil } c, err := blob.Compress(payload) if err != nil { return 0, err } return len(c), nil } // compressBound bounds blob.Compress's output for n input bytes: the 4-byte // size prefix, zlib's 6 bytes of framing, and deflate's 5-byte header per // stored block, which is what incompressible input becomes. Blocks are // counted at a quarter of deflate's 65535-byte maximum, plus 64 bytes, so // the bound may overstate but never understates: len(payload)+4 alone does, // for random bytes. func compressBound(n int) int { if n < 0 { panic("compressBound: n must not be negative") } return n + 4 + 6 + 5*(n/16384+2) + 64 } // holdsContent reports whether uuid is in the store with its content, not // as a phantom: a card for it then stores nothing. func holdsContent(q db.Querier, uuid string) (bool, error) { var size int64 err := q.QueryRow("SELECT size FROM blob WHERE uuid=?", uuid).Scan(&size) if errors.Is(err, sql.ErrNoRows) { return false, nil } if err != nil { return false, err } return size >= 0, nil } // phantomName returns a file name and the check-in naming uuid, when a // public check-in this store already crosslinked lists it, so a refusal can // say which file is too big; "" when none does. A private check-in is never // named: the refusal goes to whoever pushed, private capability or not. func phantomName(q db.Querier, uuid string) (name, checkin string, err error) { err = q.QueryRow(`SELECT filename.name, ci.uuid FROM blob f JOIN mlink ON mlink.fid = f.rid JOIN filename ON filename.fnid = mlink.fnid JOIN blob ci ON ci.rid = mlink.mid WHERE f.uuid = ? AND NOT EXISTS(SELECT 1 FROM private WHERE private.rid = mlink.mid) ORDER BY mlink.mid LIMIT 1`, uuid).Scan(&name, &checkin) if errors.Is(err, sql.ErrNoRows) { return "", "", nil } return name, checkin, err }