feat(go-services): Phase 2 foundation — isolated shadow DB + schema
Stands up the shadow-ingest substrate without touching production: - schema.go: faithful replica of db_async.init_db_async (idempotent DDL), run only when an instance OWNS its DB (READ_ONLY=false). Fixes for a fresh DB: spawn_events has no sole-id PK (so it can be a hypertable), telemetry_events compression is enabled before its policy, and the portal unique index uses ROUND(..,1) to match main.py's ON CONFLICT. 35/35 statements OK. - store.go: read-only transaction enforcement is now conditional (on for production read parity, off for ingest). - main.go: READ_ONLY + SHADOW_INGEST_WS config; schema init on boot when owning the DB. - compose override: a SEPARATE TimescaleDB `dereth-go-db` (isolated volume, 127.0.0.1:5434) and a `dereth-tracker-go-shadow` instance (image reused via dereth-tracker-go:local) that owns it. Production DB never written. Verified: dereth_go has all 13 tables; telemetry_events + spawn_events are hypertables; the read-side instance still serves production read-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b6d2871cf0
commit
6a839e69bc
4 changed files with 277 additions and 12 deletions
|
|
@ -77,7 +77,7 @@ func main() {
|
|||
logger.Warn("DATABASE_URL unset — running without DB; DB-backed endpoints will be empty")
|
||||
} else {
|
||||
connectCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
pool, err := newPool(connectCtx, cfg.DatabaseURL)
|
||||
pool, err := newPool(connectCtx, cfg.DatabaseURL, cfg.ReadOnly)
|
||||
cancel()
|
||||
if err != nil {
|
||||
logger.Error("db pool init failed", "err", err)
|
||||
|
|
@ -85,9 +85,18 @@ func main() {
|
|||
}
|
||||
defer pool.Close()
|
||||
srv.pool = pool
|
||||
|
||||
// Ingest/shadow mode owns its own DB: create the schema on first run.
|
||||
if !cfg.ReadOnly {
|
||||
schemaCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
initSchema(schemaCtx, pool, logger)
|
||||
cancel()
|
||||
}
|
||||
|
||||
go srv.runCacheLoop(ctx)
|
||||
go srv.runTotalsLoop(ctx)
|
||||
logger.Info("db connected; cache loops started", "live_interval", cacheInterval.String(), "totals_interval", totalsInterval.String())
|
||||
logger.Info("db connected; cache loops started",
|
||||
"read_only", cfg.ReadOnly, "live_interval", cacheInterval.String(), "totals_interval", totalsInterval.String())
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
|
@ -121,19 +130,23 @@ func main() {
|
|||
// matching the Python service's env var names where they overlap.
|
||||
type config struct {
|
||||
Addr string // listen address, e.g. ":8770"
|
||||
DatabaseURL string // dereth TimescaleDB DSN (read-only use)
|
||||
DatabaseURL string // dereth TimescaleDB DSN
|
||||
ReadOnly bool // true = read-side parity (force read-only txns); false = ingest/shadow (owns its DB)
|
||||
InventoryURL string // inventory-service base URL
|
||||
StaticDir string // directory for static assets / openissues.json
|
||||
SecretKey string // session-cookie signing key (must match the Python service)
|
||||
IngestWS string // optional: a /ws/live URL to shadow-ingest from (Python tracker)
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
return config{
|
||||
Addr: ":" + envOr("PORT", "8770"),
|
||||
DatabaseURL: os.Getenv("DATABASE_URL"),
|
||||
ReadOnly: envOr("READ_ONLY", "true") != "false",
|
||||
InventoryURL: envOr("INVENTORY_SERVICE_URL", "http://inventory-service:8000"),
|
||||
StaticDir: envOr("STATIC_DIR", "static"),
|
||||
SecretKey: os.Getenv("SECRET_KEY"),
|
||||
IngestWS: os.Getenv("SHADOW_INGEST_WS"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue