Show placement candidates and each node's answer in indexer_placement, add indexer_placement_reread, and drop a group's candidates when its head changes no placement file, so a missed message or a revert is recoverable by tool (issue 1126, ADR 0559 §3, §7, §8)

This commit is contained in:
2026-10-27 02:18:21 +01:00
parent ad60ba3a46
commit 04bb2d1398
17 changed files with 493 additions and 22 deletions
@@ -43,6 +43,8 @@ type Reader struct {
HavesState StateRead // the node-engine's haves
ReachState StateRead // the view's own reach (ADR 0556)
PlacementState StateRead // the indexer's placement
// PlacementCandidatesState is the indexer's placement candidates (issue 1126); unset reads none.
PlacementCandidatesState StateRead
}
// ReachReadTool reads the view's reach: per repository, how far each module reached and the reached head (ADR 0556).
@@ -652,11 +654,19 @@ func (r Reader) Haves(node, module string) (KeptAnswer, error) {
// Placement is the indexer's placement as stored (ADR 0549 §2): one node's key with node, every node placing a module
// with module (each node's key whole, so its pins, ports and bindings are beside the module), and every node's key with
// neither. A key that does not read as a placement is listed as unreadable, never dropped.
//
// Beside it, candidates (issue 1126, ADR 0549 §4): each open catalogue pull request's placement of a node that differs from
// the trunk's (the indexer's placement-candidates `<group>.<node>`), narrowed the same way, each with the node's answer from
// its `<node>.node` have and whether that answer is for the candidate as it stands (its head and key revision).
func (r Reader) Placement(node, module string) (map[string]any, error) {
node, module = strings.TrimSpace(node), strings.TrimSpace(module)
if r.PlacementState.Keys == nil || r.PlacementState.Get == nil {
return nil, fmt.Errorf("the placement state cannot be read here")
}
candidates, err := r.PlacementCandidates(node, module)
if err != nil {
return nil, err
}
if node != "" {
key, err := delivery.PlacementKey(node)
if err != nil {
@@ -666,10 +676,14 @@ func (r Reader) Placement(node, module string) (map[string]any, error) {
if err != nil {
return nil, err
}
if len(raw) == 0 {
if len(raw) == 0 && len(candidates) == 0 {
return nil, fmt.Errorf("no placement of %s: the catalogue holds no placement file for it, or the indexer has read none yet", node)
}
return map[string]any{"placement": map[string]json.RawMessage{node: raw}}, nil
out := map[string]json.RawMessage{}
if len(raw) > 0 {
out[node] = raw
}
return map[string]any{"placement": out, "candidates": candidates}, nil
}
keys, err := r.PlacementState.Keys()
if err != nil {
@@ -698,5 +712,102 @@ func (r Reader) Placement(node, module string) (map[string]any, error) {
}
out[n] = values[i]
}
return map[string]any{"placement": out, "unreadable": unreadable}, nil
return map[string]any{"placement": out, "unreadable": unreadable, "candidates": candidates}, nil
}
// The states of a node's answer to a placement candidate, as the view and the assigner's check read it.
const (
// AnswerNone: the node's have holds no answer for the candidate's group (a node asleep, or not composing yet).
AnswerNone = "none"
// AnswerStale: the node answered an earlier head or revision of the candidate.
AnswerStale = "stale"
)
// CandidateRow is one placement candidate and the node's answer to it.
type CandidateRow struct {
Group string `json:"group"`
Node string `json:"node"`
Pull delivery.Head `json:"pull"`
Revision uint64 `json:"revision"`
Placement json.RawMessage `json:"placement,omitempty"` // the candidate value as stored
Removes bool `json:"removes,omitempty"` // the head removes the node's file
Answered string `json:"answered"` // same, differs, refused, stale or none
Answer *delivery.CandidateAnswer `json:"answer,omitempty"`
}
// PlacementCandidates is every placement candidate, narrowed by node and by a module its placement names, with each
// node's answer. A key that does not read is a row naming only its key's group and node, answered none, never dropped.
func (r Reader) PlacementCandidates(node, module string) ([]CandidateRow, error) {
st := r.PlacementCandidatesState
if st.Keys == nil || st.GetEntry == nil {
return []CandidateRow{}, nil
}
keys, err := st.Keys()
if err != nil {
return nil, fmt.Errorf("reading the placement candidates: %w", err)
}
sort.Strings(keys)
answers := map[string]map[string]delivery.CandidateAnswer{} // node, then group
rows := []CandidateRow{}
for _, key := range keys {
group, n, err := delivery.ParsePlacementCandidateKey(key)
if err != nil || (node != "" && n != node) {
continue
}
raw, rev, err := st.GetEntry(key)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", key, err)
}
if len(raw) == 0 {
continue
}
row := CandidateRow{Group: group, Node: n, Revision: rev, Placement: raw, Answered: AnswerNone}
c, err := delivery.DecodePlacementCandidate(raw)
if err == nil {
row.Pull, row.Removes = c.Pull, c.Placement == nil
if module != "" && (c.Placement == nil || !slices.Contains(c.Placement.Assigned(), module)) {
continue
}
} else if module != "" {
continue
}
if _, read := answers[n]; !read {
answers[n] = r.candidateAnswers(n)
}
if a, ok := answers[n][group]; ok && err == nil {
row.Answer = &a
row.Answered = AnswerOf(c, rev, a)
}
rows = append(rows, row)
}
return rows, nil
}
// AnswerOf is the state of a node's answer a to the candidate c at key revision rev: its verdict when it answers that
// head and revision, AnswerStale otherwise.
func AnswerOf(c delivery.PlacementCandidate, rev uint64, a delivery.CandidateAnswer) string {
if a.Head != c.Pull.Head || a.Revision != rev {
return AnswerStale
}
return a.Verdict
}
// candidateAnswers is a node's answers to its candidates, by group, from its `<node>.node` have; none when it has none.
func (r Reader) candidateAnswers(node string) map[string]delivery.CandidateAnswer {
if r.HavesState.Get == nil {
return nil
}
key, err := delivery.HaveNodeKey(node)
if err != nil {
return nil
}
raw, err := r.HavesState.Get(key)
if err != nil || len(raw) == 0 {
return nil
}
h, err := delivery.DecodeHaveNode(raw)
if err != nil {
return nil
}
return h.Candidates
}