Skip to content

medium(startup): audit and tiering open redundant SQLite handles to the shared DB and never close them #562

Description

@xe-nvdk

Found while investigating #329 (MQTT opening its own SQLite connection). #329 is not an isolated case — it is one of six long-lived handles to the same physical file.

The shared file

cfg.Auth.DBPath (default ./data/arc.db) is opened independently by:

Component Where Pool settings Closed?
auth.AuthManager internal/auth/auth.go:188 MaxOpenConns(1), MaxIdleConns(1), ConnMaxLifetime(1h), DSN with WAL + busy_timeout + foreign_keys Yes — legitimate owner
mqtt.Repository internal/mqtt/repository.go:40 MaxOpenConns(1), MaxIdleConns(1), DSN with WAL + busy_timeout Yes (own handle) — #329
audit logger cmd/arc/main.go:1858 none — unbounded pool, bare path, no DSN params NO — leaked
tiering manager cmd/arc/main.go:2467 none — unbounded pool, bare path, no DSN params NO — leaked
api.RetentionHandler internal/api/retention.go:114 none Yes
api.ContinuousQueryHandler internal/api/continuous_query.go:157 none Yes

This issue: audit and tiering specifically

These two are the cheapest to fix and the only ones with a leak:

  1. The packages are already correct. audit.LoggerConfig.DB (internal/audit/audit.go:72) and tiering.ManagerConfig.DB (internal/tiering/manager.go:56) both accept a borrowed *sql.DB and neither closes it in Stop(). Only main.go is wrong — it opens a redundant handle instead of passing authManager.GetDB().

  2. Both handles are leaked. There is no auditDB.Close() or tieringDB.Close() anywhere in main.go. audit.Logger.Stop() (audit.go:157) and tiering.Manager.Stop() (manager.go:166) correctly do not close a borrowed DB — but nothing else closes these owned ones either. They stay open for the process lifetime.

  3. No pool limits. Unlike auth and MQTT, these use sql.Open with a bare path — no _busy_timeout, no _journal_mode=WAL, and default MaxOpenConns=0 (unlimited). Multiple connections from these pools can contend for SQLite's single write lock with no busy-timeout backoff, making SQLITE_BUSY more likely than for the handles that do set it.

Fix

Pass authManager.GetDB() at both sites — a main.go-only change.

Guard required: both blocks sit inside licenseClient != nil checks, and neither implies authManager != nil. authManager is only constructed under cfg.Auth.Enabled (main.go:933), and GetDB() on a nil receiver panics. Each site needs its own nil-guard with a documented fallback (open a standalone handle, as today). This is the CLAUDE.md "independently enabled subsystems" shape.

Not in scope here

retention.db_path and continuous_query.db_path are separate config keys (internal/config/config.go:985,989) that merely default to the same file. An operator pointing them elsewhere today gets a genuinely separate database; converting them to borrow the auth DB would silently make those keys meaningless. That needs a deprecation decision, not a refactor — worth its own issue.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions