package buildstep import ( "context" "encoding/json" "errors" "fmt" "os" "strconv" "strings" "time" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/delivery" ) // **A step left running** (novox/hq issue 1890, note 5). A build module that stops while a step runs leaves its key in // state running for good: nothing else writes that key's end, and the stream's redelivery only rewrites it when the same // ask comes back. At its start a module ends the steps its node left running by another process (EndStale: every step // names its owner, delivery.Step.Owner; note 21), and its step-end tool ends one by hand (EndOne). The conformance kit's // left-running case holds every build module to the start sweep (conformance.LeftRunning). // RestartReason is why a step a previous process left running is ended. const RestartReason = "the build module restarted while it ran" // Boot is when this process began. A step with no owner (written by an older build) started at or after it is taken // for this process's, which may still run it, so EndOne refuses it. var Boot = time.Now() // Owner is this process as a step's owner (delivery.Step.Owner): `..`, the node's boot id // (/proc/sys/kernel/random/boot_id), the pid and Boot in milliseconds, base 36. The start keeps two processes apart // that a reboot or a pid namespace gave the same pid. A running step whose owner is another is a previous process's. var Owner = ownerOf(bootID, os.Getpid(), Boot) // bootID is the node's boot id as Owner holds it; noBootID when it could not be read, which EndStaleAtStart says once. var bootID = readBootID() const noBootID = "no-boot-id" func ownerOf(bootID string, pid int, start time.Time) string { return bootID + "." + strconv.Itoa(pid) + "." + strconv.FormatInt(start.UnixMilli(), 36) } // readBootID is the node's boot id, or noBootID where it cannot be read (the pid and the start then tell the processes // apart). func readBootID() string { b, err := os.ReadFile("/proc/sys/kernel/random/boot_id") if id := strings.TrimSpace(string(b)); err == nil && id != "" { return id } return noBootID } // Scan reads and writes a steps state with its revisions: its keys, a key's stored value and revision (nil and 0 for // none), a write that lands only while the key is still at the revision read (KeptState.Update), and a delete of a key // still at the revision read; both fail with stdio.ErrChanged when another writer was first. type Scan struct { Keys func() ([]string, error) Get func(key string) (json.RawMessage, uint64, error) Update func(key string, value any, revision uint64) (uint64, error) Delete func(key string, revision uint64) error } // ScanOf is the Scan of a state the process keeps. // // Its Delete reads the key again and deletes it only while it is still at the revision read: the runtime's // mesh/state.delete takes no revision yet, so a write landing between that read and the delete (a few milliseconds) is // deleted with it. For a step key that is a redelivered ask begun on this node in that moment: its running key leaves // the steps state until its run writes its end, which its asker still reads (a watch takes puts only). func ScanOf(s stdio.KeptState) Scan { get := func(key string) (json.RawMessage, uint64, error) { e, err := s.Get(key) if err != nil || e == nil { return nil, 0, err } return e.Value, e.Revision, nil } return Scan{Keys: s.Keys, Update: s.Update, Get: get, Delete: func(key string, revision uint64) error { _, rev, err := get(key) if err != nil { return err } if rev != revision { return stdio.ErrChanged } return s.Delete(key) }} } // isAsk says a step is a pulled ask's: its key is . and its tool is " ask" (a hand-started step // has the tool's name). An ask's key is the stream's to settle, never a restart's: the puller leaves an ask it was // running unacknowledged so the stream redelivers it, and the asker's wait takes the first end it reads on the key. // A key a gone process owns is deleted, never ended (EndStale); one with no owner is ended only once the asker's bound // has passed (delivery.StepBound), when it is nobody's. func isAsk(key string, s delivery.Step) bool { _, _, err := delivery.ParseAskStepKey(key) return err == nil && strings.HasSuffix(s.Tool, " ask") } // AskWhat says what an ask is, in one line: its kind, and its repository, head and levels, or its module and // fingerprint. func AskWhat(a delivery.StepAsk) string { short := func(s string) string { if len(s) > 12 { return s[:12] } return s } str := func(m map[string]any, k string) string { v, _ := m[k].(string); return v } parts := []string{a.Kind + " ask"} switch { case a.Check != nil: parts = append(parts, str(a.Check.Fetch, "repository")+"@"+short(str(a.Check.Fetch, "commit")), "levels "+strings.Join(a.Check.Levels, ",")) if a.Check.Run != "" { parts = append(parts, "run "+a.Check.Run) } case a.Build != nil: parts = append(parts, "module "+a.Build.Module) if r := str(a.Build.Fetch, "repository"); r != "" { parts = append(parts, r+"@"+short(str(a.Build.Fetch, "commit"))) } if a.Build.FP != "" { parts = append(parts, "fp "+short(a.Build.FP)) } case a.Compose != nil: parts = append(parts, "compose", str(a.Compose.Fetch, "repository")+"@"+short(str(a.Compose.Fetch, "commit"))) } return strings.Join(parts, ", ") } // endRunning writes step ended failed with why, only while its key is still at revision. func endRunning(scan Scan, key string, step delivery.Step, revision uint64, why string) error { step.State, step.Reason, step.Ended, step.Result = delivery.StepFailed, why, now().UTC(), nil raw, err := delivery.EncodeStep(step) if err != nil { return err } _, err = scan.Update(key, json.RawMessage(raw), revision) return err } // EndStale ends this node's running steps that no process of this module runs any more, and answers their ids. Only keys // of node are read; a value that is no step is left alone. // - A hand-started step with an owner (delivery.Step.Owner) that is not this process (Owner) is ended failed with // RestartReason at once, whatever its age: its process is gone and nothing redelivers it, so nothing else would // write its end (issue 1890, note 21). // - A pulled ask's step with an owner that is not this process is deleted, not ended (novox/hq ADR 0525 ยง6): the // stream redelivers an ask whose node stopped (the puller neither ends nor acknowledges one at its stop), and its // asker takes the first end it reads, so an end written here would fail a check the redelivery would still run. Its // asker's watch takes puts only and keeps waiting for the redelivery's key, its bound the backstop; the stale running // key leaves the steps state, and with it the delivery view's and the checker's count of what runs (note 21: it // held one of shanks' two slots). // - A step owned by this process is left: it may run now. A redelivered ask begun on this node is owned by it. // - A step with no owner (written by an older build) keeps the earlier rule: a hand-started step started before // `before` (the start of this process) is ended; a pulled ask is left to the stream's redelivery (isAsk) until its // start is older than delivery.StepBound, and then ended "past the step bound: its asker stopped waiting at