-
-
Notifications
You must be signed in to change notification settings - Fork 74
feat(server): reload configuration without a restart via --reload #644
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a6c7bfa
84c1add
0062843
0e35bbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -111,6 +111,7 @@ Full reference: `.env.template` and `config/config.yaml` | |
| - `GOMODEL_MASTER_KEY` (empty = unsafe mode). Managed API keys (dashboard API Keys page / `POST /admin/auth-keys`) carry a per-key `dashboard_access` flag (default false, changeable via `PUT /admin/auth-keys/{id}/dashboard-access`): only the master key and flagged keys can call the admin REST API endpoints under `/admin/*` (others get 403 `dashboard_access_denied`); the dashboard UI shell and static assets (`/admin/dashboard`, `/admin/static/*`) skip auth entirely — only the admin data they load is gated; model endpoints and `GET /v1/usage` stay open to every key, and the no-master-key lockout-recovery path (auth skipped on `/admin/*`) is unaffected. | ||
| - `BODY_SIZE_LIMIT` ("10M") | ||
| - `USER_PATH_HEADER` (`X-GoModel-User-Path`: Header used to read/write request `user_path` values) | ||
| - `PID_FILE` / `server.pid_file` (`data/gomodel.pid` next to a `./data` directory, otherwise the OS per-user data dir — same resolution as `SQLITE_PATH`): where the running gateway records its process id. `gomodel --reload` reads it and signals that process (SIGHUP; `kill -HUP` works too) to reload configuration without a restart, like `nginx -s reload`. The reload re-reads `.env` (exported variables still win over the file; variables removed from the file are unset) and the whole config, then rebuilds the application — so every setting reloads, not a curated subset. The replacement is built before the running one is stopped, so a broken config keeps the current one serving; the listening socket is held across generations, so no connection is refused mid-reload. `PORT` and `PID_FILE` changes still need a restart (warned about), and in-memory state — rate limit counters, session affinity pins, live log buffers — resets as it would on restart. `server.pid_file: ""` in `config.yaml` disables the pid file and `--reload` (an empty `PID_FILE` env var reads as unset and keeps the default). Not available on Windows (POSIX signals). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Qualify the socket-handoff guarantee.
As per coding guidelines, “Document new configuration or API behavior and mention relevant provider-specific behavior.” 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| - `ENABLE_PASSTHROUGH_ROUTES` (true: Enable provider-native passthrough routes under /p/{provider}/...) | ||
| - `ALLOW_PASSTHROUGH_V1_ALIAS` (true: Allow /p/{provider}/v1/... aliases while keeping /p/{provider}/... canonical) | ||
| - `ENABLED_PASSTHROUGH_PROVIDERS` (openai,anthropic,openrouter,zai,vllm: Comma-separated list of enabled passthrough providers) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,8 @@ import ( | |
| "regexp" | ||
| "strconv" | ||
| "strings" | ||
|
|
||
| "github.com/enterpilot/gomodel/internal/platformdir" | ||
| ) | ||
|
|
||
| // Body size limit constants | ||
|
|
@@ -43,6 +45,26 @@ type ServerConfig struct { | |
| // at /v1/realtime and the /p/{provider}/v1/realtime passthrough upgrade. | ||
| // Default: true. Only providers implementing realtime accept sessions. | ||
| RealtimeEnabled bool `yaml:"realtime_enabled" env:"REALTIME_ENABLED"` | ||
| // PIDFile records the process id of the running gateway so `gomodel --reload` | ||
| // can find it. Default: DefaultPIDFilePath(). Set it per instance when | ||
| // several gateways share a host, or to "" in config.yaml to write no pid | ||
| // file at all, which also disables `--reload` (an empty PID_FILE reads as | ||
| // unset, like every other env var here, and keeps the default). Changing it | ||
| // needs a restart — it names the process that is already running — so a | ||
| // reload only warns about it. | ||
| PIDFile string `yaml:"pid_file" env:"PID_FILE"` | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // LegacyPIDFilePath is the pid file location used next to a project-local | ||
| // ./data directory, matching where the SQLite database lands in the same setup. | ||
| const LegacyPIDFilePath = "data/gomodel.pid" | ||
|
|
||
| // DefaultPIDFilePath returns the pid file path used when none is configured: | ||
| // LegacyPIDFilePath when a ./data directory already exists (Docker images and | ||
| // existing deployments), otherwise the OS-conventional per-user data directory | ||
| // — the same resolution the database uses, so both land together. | ||
| func DefaultPIDFilePath() string { | ||
| return platformdir.DataFile("gomodel.pid") | ||
| } | ||
|
Comment on lines
+62
to
68
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Compare DefaultPIDFilePath with storage.DefaultSQLitePath for duplicated logic.
fd -t f storage.go | xargs -I{} rg -n -A 12 'func DefaultSQLitePath' {}Repository: ENTERPILOT/GoModel Length of output: 509 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== candidate files =="
fd -t f '(^storage\.go$|server\.go$)'
echo
echo "== config/server.go relevant section =="
cat -n config/server.go | sed -n '1,100p'
echo
echo "== storage storage.go relevant section =="
cat -n storage.go | sed -n '1,70p'
echo
echo "== platformdir DataDir and relevant symbols =="
fd -t f 'platformdir' -d .
for f in $(fd -t f 'platformdir' -d .); do
echo "--- $f"
cat -n "$f" | sed -n '1,120p'
doneRepository: ENTERPILOT/GoModel Length of output: 5536 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== config/storage.go =="
cat -n config/storage.go | sed -n '1,90p'
echo
echo "== internal/storage/storage.go relevant section =="
cat -n internal/storage/storage.go | sed -n '1,80p'
echo
echo "== platformdir files =="
fd -t f 'platformdir' . -x sh -c 'echo "--- $1"; cat -n "$1" | sed -n "1,140p"' sh {}Repository: ENTERPILOT/GoModel Length of output: 10263 Extract the shared data-directory fallback.
🤖 Prompt for AI Agents |
||
|
|
||
| var headerNameRegex = regexp.MustCompile(`^[!#$%&'*+\-.^_` + "`" + `|~0-9A-Za-z]+$`) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| package config | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/enterpilot/gomodel/internal/platformdir" | ||
| ) | ||
|
|
||
| // The pid file follows the database instead of scattering GoModel's state | ||
| // across the filesystem: a Docker image with /app/data keeps both | ||
| // project-local, and a binary install started from an arbitrary working | ||
| // directory keeps both in the per-user data directory. | ||
| func TestDefaultPIDFilePath(t *testing.T) { | ||
| platformDataDir, err := platformdir.DataDir() | ||
| if err != nil { | ||
| t.Fatalf("platformdir.DataDir() error: %v", err) | ||
| } | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| setup func(t *testing.T, dir string) | ||
| want string | ||
| }{ | ||
| { | ||
| name: "data directory exists keeps the project-local path", | ||
| setup: func(t *testing.T, dir string) { | ||
| if err := os.Mkdir(filepath.Join(dir, "data"), 0o755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| }, | ||
| want: LegacyPIDFilePath, | ||
| }, | ||
| { | ||
| name: "no data directory uses the platform path", | ||
| setup: func(t *testing.T, dir string) {}, | ||
| want: filepath.Join(platformDataDir, "gomodel.pid"), | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| dir := t.TempDir() | ||
| tt.setup(t, dir) | ||
| t.Chdir(dir) | ||
|
|
||
| if got := DefaultPIDFilePath(); got != tt.want { | ||
| t.Errorf("DefaultPIDFilePath() = %q, want %q", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestPIDFilePathResolution(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| env string | ||
| configYAML string | ||
| want string | ||
| }{ | ||
| { | ||
| name: "env var wins", | ||
| env: "/var/run/gomodel/custom.pid", | ||
| want: "/var/run/gomodel/custom.pid", | ||
| }, | ||
| { | ||
| // Empty env vars are "unset" everywhere in this config, so PID_FILE= | ||
| // keeps the default rather than disabling the pid file. Asserted so | ||
| // the documented way to disable it stays the config file. | ||
| name: "empty env var keeps the default", | ||
| env: "", | ||
| want: DefaultPIDFilePath(), | ||
| }, | ||
| { | ||
| name: "empty config value writes no pid file", | ||
| configYAML: "server:\n pid_file: \"\"\n", | ||
| want: "", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| dir := t.TempDir() | ||
| t.Chdir(dir) | ||
| t.Setenv("PID_FILE", tt.env) | ||
| if tt.configYAML != "" { | ||
| if err := os.WriteFile(filepath.Join(dir, "config.yaml"), []byte(tt.configYAML), 0o600); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| } | ||
|
|
||
| result, err := Load() | ||
| if err != nil { | ||
| t.Fatalf("Load() error = %v", err) | ||
| } | ||
| if got := result.Config.Server.PIDFile; got != tt.want { | ||
| t.Errorf("Server.PIDFile = %q, want %q", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.