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
|
|
@ -11,23 +11,27 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// newPool creates a pgx pool against the dereth TimescaleDB.
|
||||
// newPool creates a pgx pool against a dereth TimescaleDB.
|
||||
//
|
||||
// Phase 1 is strictly read-only. As defense-in-depth we force every pooled
|
||||
// connection into read-only transaction mode, so even a buggy or future write
|
||||
// statement cannot mutate the live production data the Python service owns.
|
||||
func newPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
|
||||
// When readOnly is true (the default — read-side parity against the live
|
||||
// production dereth DB), every pooled connection is forced into read-only
|
||||
// transaction mode as defense-in-depth, so even a buggy write cannot mutate the
|
||||
// data the Python service owns. When false (ingest/shadow mode against this
|
||||
// instance's OWN database), writes are permitted.
|
||||
func newPool(ctx context.Context, dsn string, readOnly bool) (*pgxpool.Pool, error) {
|
||||
cfg, err := pgxpool.ParseConfig(dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse DATABASE_URL: %w", err)
|
||||
}
|
||||
cfg.MaxConns = 10
|
||||
cfg.MaxConnIdleTime = 5 * time.Minute
|
||||
cfg.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
|
||||
if _, err := conn.Exec(ctx, "SET default_transaction_read_only = on"); err != nil {
|
||||
return fmt.Errorf("set read-only: %w", err)
|
||||
if readOnly {
|
||||
cfg.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
|
||||
if _, err := conn.Exec(ctx, "SET default_transaction_read_only = on"); err != nil {
|
||||
return fmt.Errorf("set read-only: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
pool, err := pgxpool.NewWithConfig(ctx, cfg)
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue