ADR-0046 — Runtime Architecture & Service Boundaries (P15)
Status: Accepted
Date: 2026-06-12
Supersedes: Parts of ADR-0045 (inline plugin pool)
Context
P14 (ADR-0045) decomposed the monolith into 8 internal/ packages with compiler-enforced boundaries. However, the plugin pool remained inline in main.go (~150 lines of concurrent infrastructure), and unit tests were absent for most internal packages.
Two problems drove this ADR:
- Plugin pool as highest-risk unextracted code: The goroutine pool, panic recovery, failure counting, and auto-disable logic is complex concurrent infrastructure that belongs behind a stable API, not scattered in main.go.
- No unit tests for internal packages: Without tests, regressions from future refactors are invisible until production.
Decision
1. Extract plugin subsystem to internal/plugins
internal/plugins exposes:
Registry— thread-safe hook registration (Register,Handlers)Manager— worker pool with context cancellation, WaitGroup drain, panic recovery, auto-disable afterfailThreshfailures (New,Start,Fire,Shutdown)HookFunc— the hook function typeDefaultPoolSize = 4,DefaultQueueDepth = 32
main.go reduced to two package-level vars and two thin wrapper functions:
var (
pluginRegistry = plugins.NewRegistry()
pluginManager = plugins.New(pluginRegistry)
)
func RegisterHook(event string, fn plugins.HookFunc) { pluginRegistry.Register(event, fn) }
func FireHook(event string, payload map[string]interface{}) {
if os.Getenv("VAYU_PLUGINS_ENABLED") != "true" { return }
pluginManager.Fire(event, payload)
}
2. Package-level unit tests for all internal packages
Tests added (with -race passing):
internal/metrics— histogram recording, percentiles, Prometheus format, cache hit ratiointernal/auth— CSRF round-trip/invalid, auth lockout/reset, Argon2id, RequireAPIKey middlewareinternal/logging— secret redaction regex, LogInfo/LogError no-panic, ReplaceAll redactioninternal/config— EnvOr, GetEnvAsInt, LoadDefaults, ConfigVersioninternal/plugins— register+fire, unknown event, panic isolation, hook disable after failures, shutdown drains, registry snapshotinternal/health— liveness, DB health, ready with/without workers, ethics, Meilisearch up/down, storage, queueinternal/queue— queue status, replay empty, processOneJob maintenance/empty/insert, backoff cap
3. SQLite migration compatibility fix
Removed IF NOT EXISTS from ALTER TABLE ADD COLUMN in migrations 003 and 004. This syntax is not supported in the SQLite version present in the test environment. The existing runMigrations() error handler already catches "duplicate column" errors for idempotency.
Consequences
internal/pluginsis independently testable and replaceable- All 7 internal packages with logic have unit tests
main.goplugin section: ~150 lines → ~15 linesgo test -race ./internal/...passes cleanly- Dependency graph unchanged:
pluginssits abovemetrics/logging, imported only bymain