Everything in server.yaml that is not one machine's own is now a versioned document in the store (cluster_settings), the same on every control plane. The first start against a store imports the file's cluster keys, with the baseline guard policy when there is none; after that those keys in a file are ignored, with a warning. Each control plane polls the document's version and its own server.yaml's hash. A change is validated and applied: guard live (Gate.Replace), anything else by a graceful restart that systemd brings back, standbys first and the leader 30s later. A version that does not load on a machine is not applied; at start the newest one that loads is used, so a bad edit cannot keep every control plane down. llmgw ctl config show/get/set/unset/edit/history/rollback, the admin API under /api/v1/settings, and the console's Settings screen, which layers the store's document over the machine's file, write it. Commands beside the control plane that read cluster keys (init, node add, server add, backup, upgrade) read the layered configuration; init --bundle writes offline to the store once it holds the settings. A standby that becomes the leader re-reads models, engine choices and facility holds from the store. Retires appendGuardIfMissing: the import does it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
99 lines
3.2 KiB
Go
99 lines
3.2 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// ErrSettingsChanged is a change made against a version of the cluster's
|
|
// settings that is no longer the newest: somebody else changed them first.
|
|
var ErrSettingsChanged = errors.New("the cluster's settings were changed since they were read; read them again")
|
|
|
|
// SettingsVersion is one version of the cluster's settings.
|
|
type SettingsVersion struct {
|
|
Version int64
|
|
Body string
|
|
Note string
|
|
At time.Time
|
|
By string
|
|
}
|
|
|
|
// ClusterSettings is the newest version, Version 0 when there is none yet.
|
|
func (s *DB) ClusterSettings(ctx context.Context) (SettingsVersion, error) {
|
|
return s.settingsWhere(ctx, `version = (SELECT MAX(version) FROM cluster_settings)`)
|
|
}
|
|
|
|
// ClusterSettingsAt is one version, Version 0 when there is no such version.
|
|
func (s *DB) ClusterSettingsAt(ctx context.Context, version int64) (SettingsVersion, error) {
|
|
return s.settingsWhere(ctx, `version = ?`, version)
|
|
}
|
|
|
|
func (s *DB) settingsWhere(ctx context.Context, where string, args ...any) (SettingsVersion, error) {
|
|
var v SettingsVersion
|
|
var at int64
|
|
err := s.db.QueryRowContext(ctx,
|
|
`SELECT version, body, note, created_at, created_by FROM cluster_settings WHERE `+where, args...).
|
|
Scan(&v.Version, &v.Body, &v.Note, &at, &v.By)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return SettingsVersion{}, nil
|
|
}
|
|
v.At = time.UnixMilli(at)
|
|
return v, err
|
|
}
|
|
|
|
// ClusterSettingsVersion is the newest version's number, 0 when there is
|
|
// none: what every control plane asks every few seconds.
|
|
func (s *DB) ClusterSettingsVersion(ctx context.Context) (int64, error) {
|
|
var v sql.NullInt64
|
|
err := s.db.QueryRowContext(ctx, `SELECT MAX(version) FROM cluster_settings`).Scan(&v)
|
|
return v.Int64, err
|
|
}
|
|
|
|
// PutClusterSettings records a new version made from base, the version the
|
|
// change was read from, and returns its number. Refused with
|
|
// ErrSettingsChanged when base is not the newest.
|
|
func (s *DB) PutClusterSettings(ctx context.Context, base int64, body, by, note string) (int64, error) {
|
|
cur, err := s.ClusterSettingsVersion(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if cur != base {
|
|
return 0, ErrSettingsChanged
|
|
}
|
|
// The key settles a race the read above cannot: of two writers from the
|
|
// same base, one inserts base+1 and the other is refused.
|
|
_, err = s.db.ExecContext(ctx,
|
|
`INSERT INTO cluster_settings (version, body, note, created_at, created_by) VALUES (?, ?, ?, ?, ?)`,
|
|
base+1, body, note, time.Now().UnixMilli(), by)
|
|
if isUniqueViolation(err) {
|
|
return 0, ErrSettingsChanged
|
|
}
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return base + 1, nil
|
|
}
|
|
|
|
// ClusterSettingsHistory is the newest versions first, at most limit.
|
|
func (s *DB) ClusterSettingsHistory(ctx context.Context, limit int) ([]SettingsVersion, error) {
|
|
rows, err := s.db.QueryContext(ctx,
|
|
`SELECT version, body, note, created_at, created_by FROM cluster_settings ORDER BY version DESC LIMIT ?`, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []SettingsVersion
|
|
for rows.Next() {
|
|
var v SettingsVersion
|
|
var at int64
|
|
if err := rows.Scan(&v.Version, &v.Body, &v.Note, &at, &v.By); err != nil {
|
|
return nil, err
|
|
}
|
|
v.At = time.UnixMilli(at)
|
|
out = append(out, v)
|
|
}
|
|
return out, rows.Err()
|
|
}
|