Original release notes
GRAIN v0.7.1
Memory-leak audit Full Changelog: https://github.com/0ceanSlim/grain/compare/v0.7.0...v0.7.1
---
A stability patch. A goroutine- and memory-leak audit of the relay turned up four leaks; all four are fixed here. No new features, no config changes, no data migration - a drop-in upgrade that keeps a long-running relay flat on memory.
The headline fix affects every relay and grows with ordinary connection churn; the other three are narrower (config reloads, flaky upstream relays, and backup-relay forwarding). If your relay's memory crept upward the longer it stayed up, this is why.
---
Authentication state no longer leaks per connection
Affects every relay - this is the main one.
The relay sends a NIP-42 AUTH challenge to every new connection and tracked it - plus the authenticated session, once a client completes AUTH - in two server-global maps keyed by the connection. Those entries were never removed when the connection closed. Because the key *was* the connection, each stale entry pinned the entire connection object (its subscriptions, filters, outbound buffer, and rate limiter), so memory grew in proportion to the total number of connections ever made, not the number currently open. On a public relay - scanners, drive-by readers, anything that connects and never authenticates - that climbs steadily and never comes back down.
Both maps are now cleared when the connection tears down.
Developer detail
The challenges and authSessions maps in server/handlers/auth.go had no removal path; challenges got an entry for *every* connection but was only cleared on a successful AUTH. A new handlers.CleanupClient is called from the clientReader teardown defer (the path that runs for every disconnect). Tracked under #92.
---
Config reloads no longer leak background goroutines
Every config change - including each dashboard save and grain_reloadconfig - restarts the relay's internal services. The previous instance's background loops were never stopped, so each reload leaked a fresh set: the stats logger, connection-rejection aggregator, IP-ban sweeper, event-purge loop, NIP-40 expiration sweeper, the pubkey-cache refreshers, and the entire upstream client pool. Over a session of dashboard tuning they piled up (duplicate stat lines every few minutes, expiration sweepers spinning against a closed database, and so on).
Each server instance is now cleanly torn down on reload before the next one starts.
Developer detail
runServerInstance now owns a per-instance context.Context that's cancelled on teardown and threaded through every background starter; loops exit on ctx.Done() instead of running for the life of the process (the expiration sweeper was even started with context.Background()). CloseCoreClient now actually closes the relay pool, and client.ShutdownClient() runs on teardown. Hot-reload semantics are unchanged - each new instance starts fresh with the reloaded config. Tracked under #93.
---
Upstream relay disconnects no longer leak a goroutine
GRAIN's internal client (it fetches profiles and mute lists from index relays) leaked one goroutine every time an upstream relay dropped the connection from its end - normal flakiness for third-party relays - and the 5-minute health-check reconnect loop made it recur over time.
The reader and writer for a relay connection now always shut down together.
Developer detail
Relay-connection teardown in client/core/relays.go is coordinated through a sync.Once-guarded done channel, so a read-side disconnect (or an explicit close, or a write error) stops the other half exactly once - previously a read-side exit set the status but never closed done, and close() then short-circuited on that status before reaching its close(done), leaving the writer blocked forever. Status writes are now lock-guarded. Tracked under #94.
---
Backup-relay forwarding is bounded and time-limited
*Only affects relays with backup_relay.enabled = true.*
With a backup relay configured, each incoming event spawned a goroutine to forward it - with no dial or write timeout, a fresh connection per event, and a trailing sleep. A slow or unreachable backup relay let those goroutines and half-open sockets pile up without bound under inbound load.
Forwarding is now capped (concurrency-limited, dropped when saturated since it's best-effort) with explicit dial and write deadlines.
Developer detail
A 32-slot semaphore bounds concurrent forwards in HandleEvent; SendToBackupRelay got 5-second dial and write deadlines and lost its 500ms time.Sleep. Tracked under #95.
---
Upgrade
Drop-in. Stop your v0.7.0 instance, swap in the v0.7.1 binary, restart. No config changes, no data migration - nostrdb format is unchanged.
## stop v0.7.0 drop in the v0.7.1 binary restart
---
Full Changelog: https://github.com/0ceanSlim/grain/compare/v0.7.0...v0.7.1
