diff --git a/modules/distribution/cmd/distribution-tools/main.go b/modules/distribution/cmd/distribution-tools/main.go index c63bff40..13fcb282 100644 --- a/modules/distribution/cmd/distribution-tools/main.go +++ b/modules/distribution/cmd/distribution-tools/main.go @@ -20,6 +20,7 @@ import ( reg "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/handacts" "git.novox.be/novox/mesh-sdk/go/health" "git.novox.be/novox/mesh-sdk/go/parts" @@ -164,6 +165,12 @@ func learnInto(state casState, module, fp string, attempts int) (bool, error) { return false, fmt.Errorf("%s was written by another writer first %d times: %w", holder.PublishingKey, attempts, stdio.ErrChanged) } +// recordAct records a hand act through the seat mesh-hand-acts alone. +func recordAct(q handacts.Request) error { + _, err := handacts.Recorder{Ask: stdio.Ask}.Record(q) + return err +} + // mirrors is the state mirrors: an ask is created once; one already kept is the same copy, taken on already. type mirrors struct{} @@ -409,12 +416,12 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder. } idle, _ := v.DropIdle() if id := str(args, "drop"); id != "" { - if str(args, "why") == "" { - return nil, errors.New("a drop is a hand act: say why") + t, err := v.DropByHand(id, stdio.CallerOf(args), str(args, "why")) + if err != nil { + return nil, err } - t, err := v.Drop(id) logf("transfer %s dropped by hand by %s: %s", id, stdio.CallerOf(args), str(args, "why")) - return map[string]any{"dropped": t.ID}, err + return map[string]any{"dropped": t.ID}, nil } all, err := v.Transfers.List() used, _ := v.Objects.Used() @@ -498,7 +505,7 @@ func main() { base, _ := url.Parse("http://127.0.0.1:5001") store := &holder.OCI{Base: base, Upstream: &holder.Upstream{}} h := &holder.Holder{Objects: parts.BusObjects(holder.IncomingObjects), Builder: delivery.BuilderVersions{Ask: stdio.Ask}, Store: store, Say: say, Finding: finding, - Transfers: parts.BusTransfers[holder.Opening](holder.TransfersState), Cap: parts.BusCap(holder.IncomingObjects), Idle: parts.BusArm(holder.IdleState)} + Transfers: parts.BusTransfers[holder.Opening](holder.TransfersState), Cap: parts.BusCap(holder.IncomingObjects), Record: recordAct, Idle: parts.BusArm(holder.IdleState)} v := &holder.Verbs{Holder: h, Catalog: store, Holds: kept[holder.Hold]{state: "holds", key: holder.Hold.Key}, Mirrors: mirrors{}} go followMirrors(v) go parts.FollowIdle(holder.IdleState, parts.IDs(h.Transfers), h.Expired, func(id string, dropped bool, err error) { diff --git a/modules/distribution/internal/holder/holder_test.go b/modules/distribution/internal/holder/holder_test.go index 36ae96ae..37dff450 100644 --- a/modules/distribution/internal/holder/holder_test.go +++ b/modules/distribution/internal/holder/holder_test.go @@ -17,6 +17,7 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/handacts" ) type memObjects map[string][]byte @@ -377,6 +378,23 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) { if _, err := b.h.Publish(ArtifactSeat, builder, req("t5")); err == nil || !strings.Contains(err.Error(), "idle bound") { t.Errorf("a publish whose idle key is not written = %v; want it failed loudly", err) } + // Seams note 2: registry_incoming's drop is a hand act recorded through mesh-hand-acts before anything goes; + // an act not recorded leaves the transfer open. + var acts []handacts.Request + b.h.Record = func(q handacts.Request) error { return errors.New("mesh-hand-acts has no holder") } + if _, err := b.h.DropByHand("t5", "person.jochen", "stuck"); err == nil || !strings.Contains(err.Error(), "stays open") { + t.Errorf("a drop whose act is not recorded = %v; want refused, t5 open", err) + } + b.h.Record = func(q handacts.Request) error { acts = append(acts, q); return nil } + if _, err := b.h.DropByHand("t5", "person.jochen", ""); err == nil || len(acts) != 0 { + t.Errorf("a drop with no why = %v, acts %v; want refused, nothing recorded", err, acts) + } + if got, err := b.h.DropByHand("t5", "person.jochen", "stuck"); err != nil || got.ID != "t5" || len(acts) != 1 || acts[0].Why != "stuck" || acts[0].Cause != "registry_incoming" || !strings.Contains(acts[0].What, "t5") || !strings.Contains(acts[0].What, "person.jochen") { + t.Errorf("a drop by hand = %+v, %v, acts %+v; want t5 dropped and its act recorded", got, err, acts) + } + if _, found, _ := b.h.Transfers.Get("t5"); found { + t.Errorf("t5 kept after its drop by hand") + } }) t.Run("incoming's finding at 80 %, cleared below", func(t *testing.T) { b := newBed(true) diff --git a/modules/distribution/internal/holder/transfer.go b/modules/distribution/internal/holder/transfer.go index 599b105f..1ca4a199 100644 --- a/modules/distribution/internal/holder/transfer.go +++ b/modules/distribution/internal/holder/transfer.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-sdk/go/handacts" "git.novox.be/novox/mesh-sdk/go/parts" ) @@ -164,6 +165,9 @@ type Holder struct { Now func() time.Time // Cap reads incoming's cap from its binding (parts.BusCap); IncomingCap when nil or when the binding names none. Cap func() (uint64, error) + // Record records a hand act through the seat mesh-hand-acts (handacts.Recorder); a drop by hand is refused + // without it. + Record func(handacts.Request) error // Idle writes a transfer's idle key with its own time to live, once (parts.BusArm over IdleState); the receiver // arms it on each open that leaves a transfer open. Nil arms nothing. Idle func(id string, ttl time.Duration) error @@ -288,6 +292,29 @@ func (h *Holder) eventOf(seat string, r PublishRequest, ref, reason string) Even // Drop drops a transfer by hand (registry_incoming) or because it was idle for TransferIdle: its chunks and its key. func (h *Holder) Drop(id string) (Transfer, error) { return h.receiver().Drop(id) } +// DropByHand is registry_incoming's drop: by a person, with why, recorded as a hand act through mesh-hand-acts before +// anything goes; an act not recorded leaves the transfer open. +func (h *Holder) DropByHand(id, by, why string) (Transfer, error) { + if strings.TrimSpace(why) == "" { + return Transfer{}, errors.New("a drop is a hand act: say why") + } + if h.Record == nil { + return Transfer{}, errors.New("no hand act can be recorded here, so nothing is dropped") + } + t, found, err := h.Transfers.Get(id) + if err != nil { + return Transfer{}, fmt.Errorf("transfer %s cannot be read, so nothing is dropped: %w", id, err) + } + if !found { + return Transfer{}, fmt.Errorf("transfer %s is not open", id) + } + what := fmt.Sprintf("dropped the open transfer %s of incoming (%s@%s on %s, opened by %s, %d parts lacked), by %s", id, t.Shape.Name, t.Shape.Version, t.Shape.Seat, t.Opener, len(t.Lacks()), by) + if err := h.Record(handacts.Request{What: what, Why: why, Cause: "registry_incoming"}); err != nil { + return Transfer{}, fmt.Errorf("the hand act cannot be recorded, so transfer %s stays open: %w", id, err) + } + return h.Drop(id) +} + // DropIdle drops every transfer with no part for TransferIdle, answering them: registry_incoming's look, by hand. The // drop itself follows each transfer's idle key (Expired); the objects template keeps for good, so nothing else does. func (h *Holder) DropIdle() ([]string, error) { return h.receiver().DropIdle() } diff --git a/modules/distribution/module.json b/modules/distribution/module.json index bf8c4f4b..cfc9b8f2 100644 --- a/modules/distribution/module.json +++ b/modules/distribution/module.json @@ -55,7 +55,8 @@ "distribution.registry_callers", "mesh-delivery-builder.builder_versions", "mesh-delivery-view.node_haves", - "seat:node-engine.manifest-reads" + "seat:node-engine.manifest-reads", + "seat:mesh-hand-acts.hand-act" ], "consumes": [ "mesh-delivery-view.delivered", diff --git a/modules/gitea/cmd/package-registry/incoming.go b/modules/gitea/cmd/package-registry/incoming.go index 945eaf1b..3473dab3 100644 --- a/modules/gitea/cmd/package-registry/incoming.go +++ b/modules/gitea/cmd/package-registry/incoming.go @@ -6,7 +6,6 @@ package main // when its idle key ends (the bus's marker), never on a timer. import ( - "errors" "fmt" "os" "strings" @@ -36,10 +35,7 @@ func incomingTool(typ, seat string, holder func() (*packageseats.Holder, error)) return nil, err } if id := str(args, "drop"); id != "" { - if str(args, "why") == "" { - return nil, errors.New("a drop is a hand act: say why") - } - t, err := h.Drop(seat, id) + t, err := h.DropByHand(seat, id, stdio.CallerOf(args), str(args, "why")) if err != nil { return nil, err } diff --git a/modules/gitea/cmd/package-registry/seats.go b/modules/gitea/cmd/package-registry/seats.go index 36a0ff90..eed19585 100644 --- a/modules/gitea/cmd/package-registry/seats.go +++ b/modules/gitea/cmd/package-registry/seats.go @@ -31,6 +31,7 @@ import ( npmreg "git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/handacts" "git.novox.be/novox/mesh-sdk/go/health" "git.novox.be/novox/mesh-sdk/go/parts" @@ -263,6 +264,12 @@ func finding(kind, subject, words string, raised bool) { } } +// recordAct records a hand act through the seat mesh-hand-acts alone. +func recordAct(q handacts.Request) error { + _, err := handacts.Recorder{Ask: stdio.Ask}.Record(q) + return err +} + // boundCap is incoming's cap from its binding, read once per bundle. var boundCap = parts.BusCap(packageseats.IncomingObjects) @@ -275,7 +282,7 @@ func busHolder(forge func() (*Forge, error)) (*packageseats.Holder, error) { return &packageseats.Holder{Callers: callers, Objects: parts.BusObjects(packageseats.IncomingObjects), Transfers: parts.BusTransfers[packageseats.Opening](packageseats.TransfersState), Builder: delivery.BuilderVersions{Ask: stdio.Ask}, Store: seatStore{forge}, Say: say, Finding: finding, - Cap: boundCap, Idle: parts.BusArm(packageseats.IdleState)}, nil + Cap: boundCap, Record: recordAct, Idle: parts.BusArm(packageseats.IdleState)}, nil } // SeatTools are both seats' verbs as tools .. diff --git a/modules/gitea/internal/packageseats/holder.go b/modules/gitea/internal/packageseats/holder.go index 557fa181..3b586704 100644 --- a/modules/gitea/internal/packageseats/holder.go +++ b/modules/gitea/internal/packageseats/holder.go @@ -13,6 +13,7 @@ import ( "time" stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/handacts" "git.novox.be/novox/mesh-sdk/go/parts" ) @@ -156,6 +157,9 @@ type Holder struct { Now func() time.Time // Cap reads incoming's cap from its binding (parts.BusCap); IncomingCap when nil or when the binding names none. Cap func() (uint64, error) + // Record records a hand act through the seat mesh-hand-acts (handacts.Recorder); a drop by hand is refused + // without it. + Record func(handacts.Request) error // Idle writes a transfer's idle key with its own time to live, once (parts.BusArm over IdleState); the receiver // arms it on each open that leaves a transfer open. Nil arms nothing. Idle func(key string, ttl time.Duration) error @@ -334,6 +338,29 @@ func (h *Holder) Drop(seat, transfer string) (Transfer, error) { return h.receiver().Drop(key(seat, transfer)) } +// DropByHand is _incoming's drop: by a person, with why, recorded as a hand act through mesh-hand-acts before +// anything goes; an act not recorded leaves the transfer open. +func (h *Holder) DropByHand(seat, transfer, by, why string) (Transfer, error) { + if strings.TrimSpace(why) == "" { + return Transfer{}, errors.New("a drop is a hand act: say why") + } + if h.Record == nil { + return Transfer{}, errors.New("no hand act can be recorded here, so nothing is dropped") + } + t, found, err := h.Transfers.Get(key(seat, transfer)) + if err != nil { + return Transfer{}, fmt.Errorf("transfer %s cannot be read, so nothing is dropped: %w", transfer, err) + } + if !found { + return Transfer{}, fmt.Errorf("transfer %s of %s is not open", transfer, seat) + } + what := fmt.Sprintf("dropped the open transfer %s of %s's incoming (%s@%s, opened by %s), by %s", transfer, seat, t.Shape.Name, t.Shape.Version, t.Opener, by) + if err := h.Record(handacts.Request{What: what, Why: why, Cause: Seats[seat] + "_incoming"}); err != nil { + return Transfer{}, fmt.Errorf("the hand act cannot be recorded, so transfer %s stays open: %w", transfer, err) + } + return h.Drop(seat, transfer) +} + // Incoming is what npm_incoming and go_incoming answer of a seat: its open transfers, incoming's fill (both seats // share the store) and the cap its binding carries. type Incoming struct { diff --git a/modules/gitea/internal/packageseats/holder_test.go b/modules/gitea/internal/packageseats/holder_test.go index 700001c0..bf814fea 100644 --- a/modules/gitea/internal/packageseats/holder_test.go +++ b/modules/gitea/internal/packageseats/holder_test.go @@ -9,6 +9,7 @@ import ( "time" stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/handacts" "git.novox.be/novox/mesh-sdk/go/parts" ) @@ -190,6 +191,20 @@ func TestTheForgeHoldsBothPackageSeats(t *testing.T) { if _, err := b.h.Publish(NpmSeat, nodeBuild, pub("d", "0.1.0", file)); err == nil || !strings.Contains(err.Error(), "idle bound") { t.Errorf("a publish whose idle key is not written = %v; want it failed loudly", err) } + // Seams note 2: go_incoming's drop is a hand act recorded through mesh-hand-acts before anything goes; an act + // not recorded leaves the transfer open. + var acts []handacts.Request + b.h.Record = func(handacts.Request) error { return errors.New("mesh-hand-acts has no holder") } + if _, err := b.h.DropByHand(GoSeat, "t-b", "person.jochen", "stuck"); err == nil || !strings.Contains(err.Error(), "stays open") { + t.Errorf("a drop whose act is not recorded = %v; want refused, t-b open", err) + } + b.h.Record = func(q handacts.Request) error { acts = append(acts, q); return nil } + if got, err := b.h.DropByHand(GoSeat, "t-b", "person.jochen", "stuck"); err != nil || got.Shape.Name != "b" || len(acts) != 1 || acts[0].Cause != "go_incoming" || !strings.Contains(acts[0].What, "t-b") || !strings.Contains(acts[0].What, "person.jochen") { + t.Errorf("a drop by hand = %+v, %v, acts %+v; want t-b dropped and its act recorded", got, err, acts) + } + if in, _ := b.h.IncomingOf(GoSeat); len(in.Transfers) != 0 { + t.Errorf("after the drop by hand: %+v; want no Go transfer", in) + } }) t.Run("refusals", func(t *testing.T) { for _, c := range []struct {