6fd050855a
Replace the Phase 2 in-flight chat gate (buffered channel) with a real SQLite-backed job queue and single worker loop. Every /api/chat request now creates a job row, blocks until the worker completes it, and returns the result transparently. Key changes: - internal/store: NextJob (drain-by-model ordering), IncrementAttempt, ResetInterruptedJobs, DeleteTerminalJobsBefore; busy_timeout pragma - internal/worker: single-threaded worker loop with Notifier for sync handler completion signaling; retry on ConnectionError, terminal fail on HTTPError; crash recovery resets interrupted jobs on startup - internal/webhook: dispatcher infrastructure for async webhook delivery - internal/server: chat handler rewritten to enqueue+wait; old chatGate removed; embeddings remain direct concurrent proxies (ADR-0013) - internal/config: FOREMAN_MAX_ATTEMPTS, FOREMAN_JOB_TTL Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
3.3 KiB
Go
105 lines
3.3 KiB
Go
// Package config loads foreman's runtime configuration from environment variables.
|
|
//
|
|
// Why: centralizes all env-based configuration into a single, validated struct so
|
|
// callers never read raw env vars directly.
|
|
// What: reads FOREMAN_* environment variables, applies defaults, and validates
|
|
// required values.
|
|
// Test: set required env vars, call Load(), assert fields match; omit required vars,
|
|
// assert Load() returns an error.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// Config holds all runtime configuration for the foreman daemon.
|
|
type Config struct {
|
|
// Addr is the listen address for the HTTP server (default ":8080").
|
|
Addr string
|
|
|
|
// OllamaURL is the base URL of the Ollama target (required).
|
|
OllamaURL string
|
|
|
|
// OllamaToken is an optional bearer token sent to the Ollama target.
|
|
OllamaToken string
|
|
|
|
// Token is an optional bearer token that callers must present to foreman.
|
|
Token string
|
|
|
|
// EmbedModel is the always-resident embedder model name (e.g. "nomic-embed-text").
|
|
EmbedModel string
|
|
|
|
// DBPath is the path to the SQLite database file (default "foreman.db").
|
|
DBPath string
|
|
|
|
// PollInterval controls how often the model poller hits the target (default 30s).
|
|
PollInterval time.Duration
|
|
|
|
// WebhookSecret is an optional HMAC key for signing webhook payloads.
|
|
WebhookSecret string
|
|
|
|
// MaxAttempts is the maximum number of retry attempts for a job before it is
|
|
// marked as failed (default 3).
|
|
MaxAttempts int
|
|
|
|
// JobTTL is how long terminal jobs are retained before the pruner deletes them
|
|
// (default 24h).
|
|
JobTTL time.Duration
|
|
}
|
|
|
|
// Load reads configuration from environment variables and returns a validated Config.
|
|
//
|
|
// Why: provides a single entry point for configuration with sensible defaults.
|
|
// What: reads FOREMAN_* env vars, applies defaults, validates required fields.
|
|
// Test: call with FOREMAN_OLLAMA_URL set, assert success; call without it, assert error.
|
|
func Load() (Config, error) {
|
|
cfg := Config{
|
|
Addr: envOr("FOREMAN_ADDR", ":8080"),
|
|
OllamaURL: os.Getenv("FOREMAN_OLLAMA_URL"),
|
|
OllamaToken: os.Getenv("FOREMAN_OLLAMA_TOKEN"),
|
|
Token: os.Getenv("FOREMAN_TOKEN"),
|
|
EmbedModel: os.Getenv("FOREMAN_EMBED_MODEL"),
|
|
DBPath: envOr("FOREMAN_DB_PATH", "foreman.db"),
|
|
WebhookSecret: os.Getenv("FOREMAN_WEBHOOK_SECRET"),
|
|
}
|
|
|
|
pollStr := envOr("FOREMAN_POLL_INTERVAL", "30s")
|
|
dur, err := time.ParseDuration(pollStr)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("invalid FOREMAN_POLL_INTERVAL %q: %w", pollStr, err)
|
|
}
|
|
cfg.PollInterval = dur
|
|
|
|
maxAttemptsStr := envOr("FOREMAN_MAX_ATTEMPTS", "3")
|
|
maxAttempts, err := strconv.Atoi(maxAttemptsStr)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("invalid FOREMAN_MAX_ATTEMPTS %q: %w", maxAttemptsStr, err)
|
|
}
|
|
cfg.MaxAttempts = maxAttempts
|
|
|
|
jobTTLStr := envOr("FOREMAN_JOB_TTL", "24h")
|
|
jobTTL, err := time.ParseDuration(jobTTLStr)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("invalid FOREMAN_JOB_TTL %q: %w", jobTTLStr, err)
|
|
}
|
|
cfg.JobTTL = jobTTL
|
|
|
|
if cfg.OllamaURL == "" {
|
|
return Config{}, fmt.Errorf("FOREMAN_OLLAMA_URL is required")
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// envOr returns the value of the environment variable named by key, or fallback
|
|
// if the variable is empty or unset.
|
|
func envOr(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|