Give the gate a settings file for the bounds ADR 0505 sets on saying a refusal and a needed new head
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The tests of novox/hq ADR 0505 "How it is checked" for the gate: a refusal the gate is already answering is no
|
||||
// stall yet (§4), and a delivery whose cure is its author's new head is its author's for two hours (§5).
|
||||
|
||||
// ADR 0505 §4, §5: the two bounds are settings with their decided defaults.
|
||||
func TestTheBoundsAreSettingsWithTheirDefaults(t *testing.T) {
|
||||
read := func(body string) (Settings, error) {
|
||||
p := filepath.Join(t.TempDir(), "settings.json")
|
||||
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return readSettings(p)
|
||||
}
|
||||
h := &Holder{}
|
||||
if h.refusalFloor() != 10*time.Minute || h.authorBound() != 2*time.Hour {
|
||||
t.Fatalf("no settings: floor %s, author %s", h.refusalFloor(), h.authorBound())
|
||||
}
|
||||
s, err := read(`{"refusal-stall-floor-minutes": 5, "author-new-head-minutes": 60}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h.Settings = func() Settings { return s }
|
||||
if h.refusalFloor() != 5*time.Minute || h.authorBound() != time.Hour {
|
||||
t.Fatalf("set: floor %s, author %s", h.refusalFloor(), h.authorBound())
|
||||
}
|
||||
s, _ = read(`{}`)
|
||||
if h.refusalFloor() != 10*time.Minute || h.authorBound() != 2*time.Hour {
|
||||
t.Fatalf("empty file: floor %s, author %s", h.refusalFloor(), h.authorBound())
|
||||
}
|
||||
if _, err := read(`{"author-new-head-minutes": 0}`); err == nil {
|
||||
t.Fatalf("a bound of zero minutes was taken")
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
|
||||
// Holder is the seat's holder.
|
||||
type Holder struct {
|
||||
// Settings reads the operator's values (settings.go); nil keeps every default.
|
||||
Settings func() Settings
|
||||
Store Store
|
||||
Controller Controller
|
||||
Forge Forge
|
||||
|
||||
@@ -41,7 +41,24 @@ const forgePattern = "mesh-delivery-forge-bridge.forge.**"
|
||||
|
||||
func main() {
|
||||
occupancy := &busOccupancy{}
|
||||
settingsFile := os.Getenv("MESH_DELIVERY_GATE_SETTINGS")
|
||||
var saidMu sync.Mutex
|
||||
lastSaid := ""
|
||||
settings := func() Settings {
|
||||
s, err := readSettings(settingsFile)
|
||||
saidMu.Lock()
|
||||
defer saidMu.Unlock()
|
||||
if err != nil && err.Error() != lastSaid {
|
||||
logf("[mesh-delivery-gate] settings cannot be read, the defaults hold: %v", err)
|
||||
}
|
||||
lastSaid = ""
|
||||
if err != nil {
|
||||
lastSaid = err.Error()
|
||||
}
|
||||
return s
|
||||
}
|
||||
h := &Holder{
|
||||
Settings: settings,
|
||||
Store: kvStore{},
|
||||
Controller: seatController{ask: stdio.Ask},
|
||||
Forge: toolForge{ask: stdio.Ask},
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
// The gate's settings (novox/hq ADR 0505 §4, §5), merged by the mesh into one JSON file the env names. Each has a
|
||||
// default here, so a gate started without the file, or with a value it cannot read, keeps the decided bound and says
|
||||
// why once.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultRefusalFloor is the least a refusal the gate is already answering (the trunk moved and its check was
|
||||
// asked again) waits before it is said as stalled, when the setting refusal-stall-floor-minutes gives none.
|
||||
DefaultRefusalFloor = 10 * time.Minute
|
||||
// DefaultAuthorBound is how long a delivery whose cure is its author's new head waits for one before the operator
|
||||
// hears of it, when the setting author-new-head-minutes gives none (ADR 0505 §5).
|
||||
DefaultAuthorBound = 2 * time.Hour
|
||||
)
|
||||
|
||||
// Settings are the operator's values for this module.
|
||||
type Settings struct {
|
||||
RefusalFloorMinutes *int `json:"refusal-stall-floor-minutes"`
|
||||
AuthorBoundMinutes *int `json:"author-new-head-minutes"`
|
||||
}
|
||||
|
||||
// readSettings reads the settings file; a value that is not a whole number of minutes, one or more, is refused.
|
||||
func readSettings(path string) (Settings, error) {
|
||||
var s Settings
|
||||
if path == "" {
|
||||
return s, errors.New("started without a settings file")
|
||||
}
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
var loose map[string]any
|
||||
if err := json.Unmarshal(raw, &loose); err != nil {
|
||||
return s, fmt.Errorf("the settings file is not JSON: %v", err)
|
||||
}
|
||||
for name, into := range map[string]**int{"refusal-stall-floor-minutes": &s.RefusalFloorMinutes,
|
||||
"author-new-head-minutes": &s.AuthorBoundMinutes} {
|
||||
switch v := loose[name].(type) {
|
||||
case nil:
|
||||
case float64:
|
||||
n := int(v)
|
||||
if v < 1 || v != float64(n) {
|
||||
return Settings{}, fmt.Errorf("%s is not a whole number of minutes, one or more", name)
|
||||
}
|
||||
*into = &n
|
||||
default:
|
||||
return Settings{}, fmt.Errorf("%s is not a number", name)
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// minutesOr is a setting's minutes, or the default when it gives none.
|
||||
func minutesOr(n *int, def time.Duration) time.Duration {
|
||||
if n == nil {
|
||||
return def
|
||||
}
|
||||
return time.Duration(*n) * time.Minute
|
||||
}
|
||||
|
||||
// refusalFloor is the least an answered refusal waits before it is said as stalled (ADR 0505 §4).
|
||||
func (h *Holder) refusalFloor() time.Duration {
|
||||
if h.Settings == nil {
|
||||
return DefaultRefusalFloor
|
||||
}
|
||||
return minutesOr(h.Settings().RefusalFloorMinutes, DefaultRefusalFloor)
|
||||
}
|
||||
|
||||
// authorBound is how long a delivery that needs its author's new head waits before the operator hears of it (§5).
|
||||
func (h *Holder) authorBound() time.Duration {
|
||||
if h.Settings == nil {
|
||||
return DefaultAuthorBound
|
||||
}
|
||||
return minutesOr(h.Settings().AuthorBoundMinutes, DefaultAuthorBound)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"refusal-stall-floor-minutes": 10,
|
||||
"author-new-head-minutes": 120
|
||||
}
|
||||
@@ -84,6 +84,24 @@
|
||||
"tools": [
|
||||
"delivery_status"
|
||||
],
|
||||
"config": {
|
||||
"refusal-stall-floor-minutes": {
|
||||
"type": "number",
|
||||
"description": "minutes, at least, before a merge refused because its trunk moved, whose check the gate asked again, is said as stalled; twice the repository's last check time when that is longer (ADR 0505 §4).",
|
||||
"minimum": 1,
|
||||
"maximum": 1440,
|
||||
"trusted": false,
|
||||
"default": 10
|
||||
},
|
||||
"author-new-head-minutes": {
|
||||
"type": "number",
|
||||
"description": "minutes a delivery that needs its author's new head (a conflict, a failed check, a rejected group) waits for one before the operator hears of it (ADR 0505 §5).",
|
||||
"minimum": 1,
|
||||
"maximum": 10080,
|
||||
"trusted": false,
|
||||
"default": 120
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "state",
|
||||
@@ -91,6 +109,18 @@
|
||||
"mode": "0700",
|
||||
"place": ".",
|
||||
"unknown": "program"
|
||||
},
|
||||
{
|
||||
"id": "settings",
|
||||
"type": "file",
|
||||
"path": "${dir:state}/settings.json",
|
||||
"mode": "0600",
|
||||
"takes-config": [
|
||||
"refusal-stall-floor-minutes",
|
||||
"author-new-head-minutes"
|
||||
],
|
||||
"merge": "json",
|
||||
"template": "files/settings.json"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
@@ -104,7 +134,15 @@
|
||||
"binary": "mesh-delivery-gate",
|
||||
"loads": [
|
||||
"mesh-delivery-gate"
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"MESH_DELIVERY_GATE_SETTINGS": "${dir:state}/settings.json"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "files",
|
||||
"kind": "archive",
|
||||
"from": "files"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user