feat(broker): migrations-based install, roles, RLS, and job dependency groups

Replace the ad-hoc tables/procedures install layout with versioned,
ordered SQL migrations tracked in broker_schema_migrations. Add
optional least-privilege role provisioning (--with-roles), multi-tenant
row-level security, lease-based job claiming with stale-lease recovery,
and job dependencies -- both by job id and by fan-in job group. Add
Docker/Compose support for running the broker and its test suite.
This commit is contained in:
2026-09-17 22:09:41 +02:00
parent 602997bcdb
commit 4c8e1066d4
53 changed files with 2911 additions and 1016 deletions
+24 -5
View File
@@ -192,8 +192,11 @@ func (p *PostgresAdapter) Listen(ctx context.Context, channel string, handler No
p.logger.Info("listening on channel", "channel", channel)
// Start notification handler in goroutine
go func() {
// Start notification handler in a supervised goroutine: it must keep
// running for the life of the process, so a panic (e.g. from a
// misbehaving handler) is logged and the loop restarted rather than
// silently dying.
SupervisedGo(p.logger, "listener-"+channel, func() {
for {
select {
case n := <-listener.Notify:
@@ -208,10 +211,10 @@ func (p *PostgresAdapter) Listen(ctx context.Context, channel string, handler No
p.logger.Info("stopping listener", "channel", channel)
return
case <-time.After(90 * time.Second):
go listener.Ping()
SafeGo(p.logger, "listener-ping-"+channel, func() { listener.Ping() })
}
}
}()
})
return nil
}
@@ -237,7 +240,7 @@ func (p *PostgresAdapter) buildConnectionString() string {
}
return fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s options='-c search_path=broker,public'",
p.config.Host,
p.config.Port,
p.config.User,
@@ -247,6 +250,22 @@ func (p *PostgresAdapter) buildConnectionString() string {
)
}
// Conn returns a single physical connection pinned out of the pool, for
// session-scoped operations (e.g. pg_try_advisory_lock) that must survive
// across calls and must not be silently reaped or handed to another caller
// by the pool. The caller owns its lifecycle and must Close() it.
func (p *PostgresAdapter) Conn(ctx context.Context) (*sql.Conn, error) {
p.mu.RLock()
db := p.db
p.mu.RUnlock()
if db == nil {
return nil, fmt.Errorf("database connection not established")
}
return db.Conn(ctx)
}
// postgresTransaction implements DBTransaction
type postgresTransaction struct {
tx *sql.Tx