A service may omit its state, so unassigning an uplink module never stops the machine's network manager (hq ADR 0117)

This commit is contained in:
jochen
2026-09-27 00:11:58 +02:00
parent fdc768c476
commit 06aaac0820
7 changed files with 374 additions and 24 deletions
+30 -7
View File
@@ -645,10 +645,20 @@ func (d *Process) validate(where string, _ bool) []string {
// (it will come back at boot), or disabled and running (started by hand, gone after a reboot).
// Folding them into one field would make the second expressible only by accident.
type Service struct {
ID string `json:"id"`
Type Type `json:"type"`
Unit string `json:"unit"`
State string `json:"state"`
ID string `json:"id"`
Type Type `json:"type"`
Unit string `json:"unit"`
// State is "running" or "stopped" — or absent, and then **the unit's lifecycle is the
// machine's; the mesh only reflects its triggers** (novox/hq ADR 0117). The uplink modules
// declare the machine's own network manager this way: the mesh writes into its configuration
// and needs it to read that again, and nothing more. Stated, the host would start the manager
// on a machine that uses another one — two managers fighting over the same links — and, when
// the module was unassigned, stop it: the machine's network, the channel the mesh itself
// arrives on, gone at the moment of a routine change. So a service without a state is never
// started, stopped, enabled or disabled, is reloaded or restarted only when a trigger changed
// and it is already running, and undeclared is simply forgotten. It says nothing unless it
// names a trigger, and it may not say boot or takes-over, which are both lifecycle.
State string `json:"state,omitempty"`
// Boot is "enabled" or "disabled" — whether the unit starts at boot. Optional: absent means
// the host asserts nothing about it and leaves whatever is there.
//
@@ -692,6 +702,10 @@ type TakeOver struct {
Config string `json:"config"`
}
// Stateless reports whether the unit's lifecycle is the machine's, and the mesh only reflects the
// service's triggers (novox/hq ADR 0117).
func (s *Service) Stateless() bool { return s.State == "" }
func (s *Service) Identity() string { return s.ID }
func (s *Service) Kind() Type { return TypeService }
func (s *Service) Target() string { return s.Unit }
@@ -701,9 +715,18 @@ func (s *Service) validate(where string, _ bool) []string {
if s.Unit == "" {
problems = append(problems, where+": a service needs a unit")
}
if s.State != "running" && s.State != "stopped" {
switch {
case s.State == "running" || s.State == "stopped":
case s.State != "":
problems = append(problems, fmt.Sprintf(
"%s: state %q; a service is \"running\" or \"stopped\"", where, s.State))
"%s: state %q; a service is \"running\" or \"stopped\", or omits state to leave the "+
"unit's lifecycle to the machine", where, s.State))
case s.Boot != "" || s.TakesOver != nil:
problems = append(problems, where+": a service that omits state leaves the unit's lifecycle "+
"to the machine, and boot and takes-over are both its lifecycle")
case len(s.RestartOn) == 0 && len(s.ReloadOn) == 0:
problems = append(problems, where+": a service that omits state leaves the unit's lifecycle "+
"to the machine, and names no restart-on or reload-on — it declares nothing")
}
if s.Boot != "" && s.Boot != "enabled" && s.Boot != "disabled" {
problems = append(problems, fmt.Sprintf(
@@ -718,7 +741,7 @@ func (s *Service) validate(where string, _ bool) []string {
case t.Unit == s.Unit:
problems = append(problems, fmt.Sprintf("%s: takes-over names %s, which is this service's own unit",
where, t.Unit))
case s.State != "running":
case s.State != "running" && s.State != "":
problems = append(problems, where+": a service that takes over a tunnel is running — stopping "+
"the found one for a service that will not run would leave the peers with nothing")
}