indexer: a catalog revision per watched branch, branch.<branch>.<n> beside trunk.<n>, and revision answering per branch
The first watched branch's revisions stay trunk.<n>; catalog-revision names its branch, escaped. Nothing yet takes a non-first branch's source revision in (KeepBranchEntries has no caller). Builds: design 47 G §291, §614, A-85 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
)
|
||||
|
||||
// Design 47 G A-85, §291: the indexer keeps a catalog revision per watched branch, trunk.<n> for the first and
|
||||
// branch.<branch>.<n> (the branch escaped) for another, each numbered by its own branch, and the verb revision answers the
|
||||
// branch it is asked for; the event names the branch, escaped.
|
||||
func TestACatalogRevisionIsKeptPerWatchedBranch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ix := &Indexer{Revisions: newMemObjects(1 << 20), Now: func() time.Time { return time.Date(2026, 11, 1, 3, 0, 0, 0, time.UTC) }}
|
||||
var said []meshcatalog.CatalogRevisionEvent
|
||||
ix.SayCatalogRevision = func(_ context.Context, e meshcatalog.CatalogRevisionEvent) error { said = append(said, e); return nil }
|
||||
reader := meshcatalog.CatalogEntry{Module: "reader", Repository: "novox/r", Commit: "c1", Invokes: []string{RevisionInvoke}, Manifest: map[string]any{}}
|
||||
tip := func(commit string) map[string]meshcatalog.CatalogEntry {
|
||||
e := reader
|
||||
e.Commit = commit
|
||||
return map[string]meshcatalog.CatalogEntry{"reader": e}
|
||||
}
|
||||
if err := ix.KeepBranchEntries(ctx, "main", 5, tip("c1")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ix.KeepBranchEntries(ctx, "feature/x.y", 7, tip("c2")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ix.KeepBranchEntries(ctx, "feature/x.y", 8, tip("c3")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
names, _ := ix.Revisions.Names(ctx)
|
||||
have := map[string]bool{}
|
||||
for _, n := range names {
|
||||
have[n] = true
|
||||
}
|
||||
for _, want := range []string{"trunk.1", "branch.feature/x=2ey.1", "branch.feature/x=2ey.2"} {
|
||||
if !have[want] {
|
||||
t.Errorf("the objects hold %v, want %s", names, want)
|
||||
}
|
||||
}
|
||||
if len(said) != 3 || said[0].Branch != "main" || said[1].Branch != "feature/x=2ey" || said[2].Revision != 2 {
|
||||
t.Errorf("said %+v, want main 1, then the escaped branch 1 and 2", said)
|
||||
}
|
||||
answer := func(branch string, revision int64) meshcatalog.RevisionAnswer {
|
||||
a, err := Revision(ctx, ix.Revisions, "indexer", "reader", meshcatalog.RevisionInput{Branch: branch, Revision: revision})
|
||||
if err != nil {
|
||||
t.Fatalf("revision of %q: %v", branch, err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
if a := answer("main", 0); a.Revision.Revision != 1 || a.Entries[0].Commit != "c1" {
|
||||
t.Errorf("main answered %+v", a)
|
||||
}
|
||||
if a := answer("feature/x=2ey", 0); a.Revision.Revision != 2 || a.Revision.Branch != "feature/x.y" || a.Entries[0].Commit != "c3" {
|
||||
t.Errorf("the branch's newest answered %+v, want its revision 2 at c3", a.Revision)
|
||||
}
|
||||
if a := answer("feature/x=2ey", 1); a.Revision.Revision != 1 || a.Entries[0].Commit != "c2" {
|
||||
t.Errorf("the branch's revision 1 answered %+v", a.Revision)
|
||||
}
|
||||
if a := answer("beta", 0); !a.NoneYet {
|
||||
t.Errorf("a branch with no revision answered %+v, want none-yet", a)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
@@ -46,15 +47,41 @@ type Objects interface {
|
||||
// EntryObject is the object name of the entry of that digest.
|
||||
func EntryObject(digest string) string { return "entry." + digest }
|
||||
|
||||
// RevisionObject is the object name of a revision: trunk.<revision>, or candidate.<group>.<heads> for a candidate,
|
||||
// TrunkBranch is the first watched branch, whose revisions are kept as trunk.<revision> (design 47 G §291, A-85).
|
||||
const TrunkBranch = "main"
|
||||
|
||||
// isTrunkBranch says whether a branch's revisions are the trunk's: the first watched branch, or none named.
|
||||
func isTrunkBranch(branch string) bool { return branch == "" || branch == TrunkBranch }
|
||||
|
||||
// BranchObject is the object name of a revision of a watched branch other than the first: branch.<branch>.<revision>,
|
||||
// the branch escaped as a name (A-85), so a dot in it never reads as a part.
|
||||
func BranchObject(branch string, revision int64) string {
|
||||
return "branch." + keys.EscapeName(branch) + "." + strconv.FormatInt(revision, 10)
|
||||
}
|
||||
|
||||
// RevisionObject is the object name of a revision: trunk.<revision> for the first watched branch,
|
||||
// branch.<branch>.<revision> for another watched branch (A-85), or candidate.<group>.<heads> for a candidate,
|
||||
// which keeps its newest revision only.
|
||||
func RevisionObject(rev meshcatalog.CatalogRevision) string {
|
||||
if rev.Kind == "candidate" {
|
||||
return "candidate." + rev.Group + "." + rev.Heads
|
||||
}
|
||||
if !isTrunkBranch(rev.Branch) {
|
||||
return BranchObject(rev.Branch, rev.Revision)
|
||||
}
|
||||
return "trunk." + strconv.FormatInt(rev.Revision, 10)
|
||||
}
|
||||
|
||||
// BranchRevisionOf is the revision number a branch object's name holds for that branch, and whether it is one.
|
||||
func BranchRevisionOf(branch, name string) (int64, bool) {
|
||||
n, ok := strings.CutPrefix(name, "branch."+keys.EscapeName(branch)+".")
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
r, err := strconv.ParseInt(n, 10, 64)
|
||||
return r, err == nil
|
||||
}
|
||||
|
||||
// TrunkRevisionOf is the revision number a trunk object's name holds, and whether it is one.
|
||||
func TrunkRevisionOf(name string) (int64, bool) {
|
||||
n, ok := strings.CutPrefix(name, "trunk.")
|
||||
@@ -128,26 +155,37 @@ func ReadRevision(ctx context.Context, o Objects, name string) (meshcatalog.Cata
|
||||
|
||||
// newestTrunk is the newest trunk revision the bucket keeps, its list only; none is a zero revision and false.
|
||||
func newestTrunk(ctx context.Context, o Objects) (meshcatalog.CatalogRevision, bool, error) {
|
||||
return newestOf(ctx, o, "")
|
||||
}
|
||||
|
||||
// newestOf is the newest revision of a watched branch the bucket keeps (the trunk's for the first watched branch or none
|
||||
// named), its list only; none is a zero revision and false.
|
||||
func newestOf(ctx context.Context, o Objects, branch string) (meshcatalog.CatalogRevision, bool, error) {
|
||||
names, err := o.Names(ctx)
|
||||
if err != nil {
|
||||
return meshcatalog.CatalogRevision{}, false, err
|
||||
}
|
||||
number := TrunkRevisionOf
|
||||
if !isTrunkBranch(branch) {
|
||||
number = func(n string) (int64, bool) { return BranchRevisionOf(branch, n) }
|
||||
}
|
||||
var newest int64
|
||||
for _, n := range names {
|
||||
if r, ok := TrunkRevisionOf(n); ok && r > newest {
|
||||
if r, ok := number(n); ok && r > newest {
|
||||
newest = r
|
||||
}
|
||||
}
|
||||
if newest == 0 {
|
||||
return meshcatalog.CatalogRevision{}, false, nil
|
||||
}
|
||||
data, err := o.Get(ctx, "trunk."+strconv.FormatInt(newest, 10))
|
||||
name := RevisionObject(meshcatalog.CatalogRevision{Branch: branch, Revision: newest})
|
||||
data, err := o.Get(ctx, name)
|
||||
if err != nil {
|
||||
return meshcatalog.CatalogRevision{}, false, err
|
||||
}
|
||||
var rev meshcatalog.CatalogRevision
|
||||
if err := json.Unmarshal(data, &rev); err != nil {
|
||||
return rev, false, fmt.Errorf("trunk.%d: %w", newest, err)
|
||||
return rev, false, fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
return rev, true, nil
|
||||
}
|
||||
@@ -246,12 +284,26 @@ func (ix *Indexer) keepEntries(ctx context.Context, entries map[string]meshcatal
|
||||
if ix.catchingUp.Load() {
|
||||
return nil // the catch-up of a first start keeps its one revision at its end (CatchUp)
|
||||
}
|
||||
ix.keepMu.Lock()
|
||||
defer ix.keepMu.Unlock()
|
||||
ix.mu.Lock()
|
||||
src := int64(ix.source.rev)
|
||||
ix.mu.Unlock()
|
||||
newest, any, err := newestTrunk(ctx, ix.Revisions)
|
||||
return ix.keepBranchEntries(ctx, TrunkBranch, src, entries)
|
||||
}
|
||||
|
||||
// KeepBranchEntries keeps entries as the revision of a watched branch after that branch's newest (design 47 G §291,
|
||||
// A-85): trunk.<revision> for the first watched branch, branch.<branch>.<revision> for another, numbered per branch, and
|
||||
// says catalog-revision naming the branch. src is the branch's source revision the entries were taken in from.
|
||||
func (ix *Indexer) KeepBranchEntries(ctx context.Context, branch string, src int64, entries map[string]meshcatalog.CatalogEntry) error {
|
||||
return ix.keepBranchEntries(ctx, branch, src, entries)
|
||||
}
|
||||
|
||||
func (ix *Indexer) keepBranchEntries(ctx context.Context, branch string, src int64, entries map[string]meshcatalog.CatalogEntry) error {
|
||||
if branch == "" {
|
||||
branch = TrunkBranch
|
||||
}
|
||||
ix.keepMu.Lock()
|
||||
defer ix.keepMu.Unlock()
|
||||
newest, any, err := newestOf(ctx, ix.Revisions, branch)
|
||||
if errors.Is(err, process.ErrNotYet) {
|
||||
ix.logf("catalog graph: no revision is kept: its objects %s are not on the bus yet", CatalogRevisionsObjects)
|
||||
return nil
|
||||
@@ -261,7 +313,7 @@ func (ix *Indexer) keepEntries(ctx context.Context, entries map[string]meshcatal
|
||||
return fmt.Errorf("the newest catalog graph revision: %w", err)
|
||||
}
|
||||
at := ix.now().UTC()
|
||||
rev, objects, err := CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: newest.Revision + 1, Kind: "trunk", Source: src, At: &at}, entries)
|
||||
rev, objects, err := CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: newest.Revision + 1, Kind: "trunk", Branch: branch, Source: src, At: &at}, entries)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -292,7 +344,7 @@ func (ix *Indexer) keepEntries(ctx context.Context, entries map[string]meshcatal
|
||||
ix.logf("catalog graph: revision %d of source revision %d kept, %d entries, %d changed", rev.Revision, src, len(rev.Entries), len(changed))
|
||||
if ix.SayCatalogRevision != nil {
|
||||
if err := ix.SayCatalogRevision(ctx, meshcatalog.CatalogRevisionEvent{V: CatalogEntryLayout, Revision: rev.Revision, Kind: rev.Kind,
|
||||
Source: src, At: &at, Changed: changed, Entries: int64(len(rev.Entries))}); err != nil {
|
||||
Branch: keys.EscapeName(branch), Source: src, At: &at, Changed: changed, Entries: int64(len(rev.Entries))}); err != nil {
|
||||
ix.logf("catalog graph: revision %d is not said: %v", rev.Revision, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
@@ -53,6 +53,22 @@ func Revision(ctx context.Context, o Objects, holder, caller string, in meshcata
|
||||
if err := views.Refusal(meshcatalog.Seat+"."+meshcatalog.VerbRevision, RevisionCallers(trunkEntries, holder), caller); err != nil {
|
||||
return out, fmt.Errorf("%w (novox/hq ADR 0663 §2: a part reads it by declaring %s in invokes)", err, RevisionInvoke)
|
||||
}
|
||||
// A watched branch other than the first keeps its own revisions (A-85); the first's are the trunk's. The branch is
|
||||
// named as a key name is escaped, so it is read back to the branch's own name.
|
||||
branch, err := keys.UnescapeName(in.Branch)
|
||||
if err != nil && in.Branch != "" {
|
||||
return out, fmt.Errorf("the branch %q is no escaped name: %w", in.Branch, err)
|
||||
}
|
||||
if !isTrunkBranch(branch) {
|
||||
other, found, err := newestOf(ctx, o, branch)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("the catalog graph revisions of %s cannot be read: %w", branch, err)
|
||||
}
|
||||
if !found {
|
||||
return meshcatalog.RevisionAnswer{NoneYet: true}, nil
|
||||
}
|
||||
newest = other
|
||||
}
|
||||
name := RevisionObject(newest)
|
||||
switch {
|
||||
case in.Group != "" || in.Heads != "":
|
||||
@@ -61,10 +77,14 @@ func Revision(ctx context.Context, o Objects, holder, caller string, in meshcata
|
||||
}
|
||||
name = RevisionObject(meshcatalog.CatalogRevision{Kind: "candidate", Group: in.Group, Heads: in.Heads})
|
||||
case in.Revision != 0 && in.Revision != newest.Revision:
|
||||
name = "trunk." + strconv.FormatInt(in.Revision, 10)
|
||||
name = RevisionObject(meshcatalog.CatalogRevision{Branch: branch, Revision: in.Revision})
|
||||
}
|
||||
rev, entries := newest, trunkEntries
|
||||
if name != RevisionObject(newest) {
|
||||
if !isTrunkBranch(branch) && in.Group == "" && in.Heads == "" {
|
||||
if rev, entries, err = ReadRevision(ctx, o, name); err != nil {
|
||||
return out, err
|
||||
}
|
||||
} else if name != RevisionObject(newest) {
|
||||
if rev, entries, err = ReadRevision(ctx, o, name); err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user