Hand a replaced resource over to the process that replaces it (hq issue 213)

The controller moves from a container to a process on the one machine
that runs it (novox/hq issue 213). Every orphan is removed before anything
is applied, so the container would go first and nothing would answer the
mesh's verbs while the process was fetched, unpacked and started — and
never again, if it did not start.

- a process may say what it `replaces`: resources the declaration no
  longer declares. Such an orphan is kept through the up-front sweep and
  removed right after the process applied and is up: active and running
  at two looks ten seconds apart, the same main process, no restart in
  between (stricter than ADR 0184's second look, which reads a unit
  waiting to restart as running). If the process failed, was skipped
  behind its module's step, or is not running, the orphan stays running
  and recorded, reported kept, and the next apply hands it over.
  Refused: naming something still declared, itself, an empty id, one
  thing named by two processes, and `replaces` on a step or a schedule.
- a run-once process is run by a oneshot unit, so a step has its
  process's user, working directory and environment and `./name` is its
  own bundle's binary; it was run directly by the host, as root, with no
  environment. It is not enabled.
- a run-once process that fails gates its module, as a run-once
  container already did, so a version whose preparation failed is not
  started.
- an unchanged run-once process is not run again, and an unchanged
  scheduled one is kept up by its timer: both were "a daemon that had
  stopped" and were started on every apply.
This commit is contained in:
jochen
2026-10-04 00:48:20 +02:00
parent a7bf0f6e39
commit 161ac1ff01
5 changed files with 636 additions and 8 deletions
+60
View File
@@ -569,6 +569,18 @@ type Process struct {
// that runs once does not run on a schedule, and something that is not running cannot be
// restarted when a file changes.
Schedule string `json:"schedule,omitempty"`
// Replaces names resources this declaration no longer declares that this process takes the
// place of (novox/hq issue 213). Such a resource is not removed with the other orphans, before
// anything is applied: it is removed only once this process is applied and still running a
// moment later, and kept when it is not. So the thing being replaced answers until the thing
// replacing it does — the controller moving from its container to a process is the case: the
// container removed first left nothing answering the mesh's verbs for as long as fetching,
// unpacking and starting the process took, and for ever if the process did not start.
//
// For a process that stays up; a step or a scheduled run is not running a moment later by
// design, so there is nothing to hand over to.
Replaces []string `json:"replaces,omitempty"`
}
func (d *Process) Identity() string { return d.ID }
@@ -654,6 +666,20 @@ func (d *Process) validate(where string, _ bool) []string {
problems = append(problems, where+": "+err.Error())
}
}
if len(d.Replaces) > 0 && (d.RunOnce || d.Schedule != "") {
problems = append(problems, where+
": only a process that stays up replaces something — a step or a scheduled run is not "+
"running a moment later, so what it replaced would be removed with nothing in its place, "+
"or never")
}
for _, id := range d.Replaces {
switch {
case strings.TrimSpace(id) == "":
problems = append(problems, where+": replaces names an empty id")
case id == d.ID:
problems = append(problems, where+": a process cannot replace itself")
}
}
return problems
}
@@ -1457,6 +1483,7 @@ func parse(raw []byte, allowActions bool) (*Declaration, error) {
d.Resources = append(d.Resources, resource)
}
problems = append(problems, checkAdoption(env.Adoption, d.Resources, allowActions)...)
problems = append(problems, checkReplaces(d.Resources)...)
if env.Adoption == nil {
for _, r := range d.Resources {
if r.Kind() == TypeOpening {
@@ -1640,3 +1667,36 @@ func stripComments(raw []byte) []byte {
}
return []byte(strings.Join(kept, "\n"))
}
// checkReplaces refuses a `replaces` naming something this same declaration still declares, or
// named by two processes (novox/hq issue 213). What is replaced is what the declaration no longer
// says — a resource still declared is applied, not handed over, and one handed to two replacements
// would go when the first of them came up, whatever became of the second.
func checkReplaces(resources []Resource) []string {
declared := map[string]bool{}
for _, r := range resources {
declared[r.Identity()] = true
}
var problems []string
by := map[string]string{}
for _, r := range resources {
p, ok := r.(*Process)
if !ok {
continue
}
for _, id := range p.Replaces {
if declared[id] {
problems = append(problems, fmt.Sprintf(
"resource %q: replaces %q, which this declaration still declares — a process "+
"replaces what the declaration no longer says", p.ID, id))
}
if other, taken := by[id]; taken && other != p.ID {
problems = append(problems, fmt.Sprintf(
"resource %q: replaces %q, which %q replaces too — one thing has one replacement",
p.ID, id, other))
}
by[id] = p.ID
}
}
return problems
}