v0.83.0
Highlights
The maintenance observer no longer holds a session on a product database — #464
A product that maintains itself typically follows this sequence: set its maintenance flag, wait until all connections to its database are closed, then take the database exclusively (SINGLE_USER, restore). RSGO's SQL observer is a participant in that handshake — it detects the flag and stops the product's containers, whose connections then go away.
But the observer's own connection was pooled: Dispose only returns it to the ADO.NET pool, and because the next poll refreshed it, the pool's idle cleanup never reclaimed it. The product's wait then never completed — the component that reacts to the flag prevented the effect of its own reaction. Where a product waits without a timeout, its update simply never finishes.
Connections RSGO opens itself are now non-pooled. Both SQL observers, the SQL setter and the SQL connection test endpoint: the session exists for the duration of a single read and is gone afterwards. Sessions are tagged with an application name (ReadyStackGo-Maintenance, ReadyStackGo-ConnectionTest) so they can be identified in sys.dm_exec_sessions or sp_who2. Product containers are unaffected — RSGO does not build their connection strings, and they keep their pooling.
RSGO no longer reads a database it must not touch. The maintenance flag usually lives inside the very database that becomes exclusive, so during that window it cannot be read — and a connect attempt can occupy the single available single-user slot and fail the product's own update with error 924. Before every read, a SQL observer now checks the database's availability in sys.databases over a master connection derived from the same credentials:
masterstays reachable while another database isSINGLE_USER,RESTORINGorOFFLINE, and the query takes no lock on the target database.- While the database is unavailable, the observer reports maintenance with an observed value of
database-exclusive (<state>/<user access>)and leaves the database alone. - Once it is
ONLINEandMULTI_USERagain, the next poll reads the flag normally — the automatic return to normal operation keeps working, however long the product takes. - If availability cannot be determined (the observer login has no access to
master), RSGO logs one warning and falls back to the previous behaviour, rather than parking a deployment in maintenance over a permission problem.
Manual maintenance suspends observer polling. Trigger ownership means an observer may not end manually activated maintenance, so the read could only ever leave a session behind. Entering maintenance manually is now also a reliable way to keep RSGO off a product entirely.
Fixed along the way
pollingIntervalwas ignored. Observer state lived on a scoped service while the background service creates a fresh scope every cycle, so every cycle looked like a first check — the effective interval was always 30s. State moved into a singleton store.- The observer status endpoint was always empty.
GET /api/health/deployments/{id}/observerread the last result from a different scope and therefore always reportedHasObserver=false. - A cached observer survived a config change. A manifest edit picked up by a redeploy now takes effect without restarting RSGO — while keeping the check timestamp, so a config change cannot be used to read a product database more often than its interval allows.
- The connection test endpoint (
POST /api/connections/test/sqlserver) left an idle session on the product database for the pool's lifetime.
No migration and no manifest change required: the availability check derives its master connection from the existing observer connection string, so the fix applies to already-deployed manifests as they are. Deployments without a SQL observer are unaffected.
Documentation: Maintenance mode → Database access during maintenance. The docs also described an observer enabled field and a PUT /api/deployments/{id}/maintenance-observer endpoint — neither exists, both removed.
Full changelog: v0.82.0...v0.83.0