Recovery of photos and link2pay on novox, 2026-10-29: their composition and grant were lost and no tool wrote a done key again. Quick fix: rewrite the done key unchanged as a new revision. Thorough fix: ADR 0547, the node agent composes its own and the assigner stops waiting on controller compositions (design 47).
54 lines
2.4 KiB
Go
54 lines
2.4 KiB
Go
package build
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// **A done build is announced again by hand** (recovery of photos and link2pay on novox, 2026-10-29). The controller takes a
|
|
// build in when a write of the build key reaches its take-in consumer, and composes then; a done key is never written again
|
|
// by the builder, so a composition or a grant lost afterwards could not be made again by any tool. AnnounceAgain writes the
|
|
// done key again with its own bytes, as a new revision of the same key: its content, state, artifacts and manifest stay
|
|
// identical, and only the write is new. Who asked and why is in the builder's log and in the reasks answer.
|
|
|
|
// AnnounceAgain rewrites the done key of module at fp unchanged, as a new revision, and answers the revision. A key that is
|
|
// not done is refused with why: failed (builder_build_again builds it) or waiting (builder_ask_again).
|
|
func (b *Builder) AnnounceAgain(ctx context.Context, module, fp, why, by string) BuildAgainResult {
|
|
b.init()
|
|
key, err := delivery.BuilderBuildKey(module, fp)
|
|
if err != nil {
|
|
return BuildAgainResult{Refused: err.Error()}
|
|
}
|
|
value, rev, found, err := b.Claimer.Store.Get(ctx, key)
|
|
if err != nil {
|
|
return BuildAgainResult{Refused: fmt.Sprintf("%s cannot be read: %v", key, err)}
|
|
}
|
|
if !found {
|
|
return BuildAgainResult{Refused: fmt.Sprintf("%s has no build key: nothing to announce", key)}
|
|
}
|
|
cur, err := delivery.DecodeBuilderBuild(value)
|
|
if err != nil {
|
|
return BuildAgainResult{Refused: fmt.Sprintf("%s holds what is not a build: %v", key, err)}
|
|
}
|
|
switch cur.State {
|
|
case delivery.BuildDone:
|
|
case delivery.BuildFailed:
|
|
return BuildAgainResult{Refused: fmt.Sprintf("%s is failed, not done: %s builds it again", key, BuildAgainTool)}
|
|
default:
|
|
return BuildAgainResult{Refused: fmt.Sprintf("%s is %q, not done: %s asks its waiting asks again", key, cur.State, AskAgainTool)}
|
|
}
|
|
// Compare-and-set at the revision read: a build written meanwhile is not overwritten.
|
|
nrev, err := b.Claimer.Store.Update(ctx, key, value, rev)
|
|
if err != nil {
|
|
if errors.Is(err, ErrConflict) {
|
|
return BuildAgainResult{Refused: fmt.Sprintf("%s changed while it was announced: read it again", key)}
|
|
}
|
|
return BuildAgainResult{Refused: fmt.Sprintf("%s could not be written: %v", key, err)}
|
|
}
|
|
b.logf("%s: done, announced again by %s (revision %d): %s", key, by, nrev, why)
|
|
return BuildAgainResult{Revision: nrev}
|
|
}
|