DEPLOY.CONTEXT.SESSION_DEFENSE_NOT_APPLIED — The timeouts this run set on its own session are not in force
- Category: safety
- Severity: high
- Level: 0
- Downtime class:
online— the finding is about session state, not about a DDL operation - Stability: stable
- Suites: deploy
- Applies to: PostgreSQL and MySQL
What it reports
The session SQLens reads through issues its own timeouts before it reads anything. This check asks that same session, on the same connection, which timeouts are actually in force, and reports when the answer is "none".
One statement per engine, per look. PostgreSQL:
select current_setting('statement_timeout') as statement_timeout,
current_setting('lock_timeout') as lock_timeout
MySQL:
select @@session.max_execution_time as max_execution_time,
@@session.lock_wait_timeout as lock_wait_timeout
Each answer is reduced to milliseconds. PostgreSQL answers with a unit suffix (5s, 5000ms) and
MySQL with a bare number; ms, s and min are the suffixes read. MySQL's lock_wait_timeout
answers in seconds and is converted here rather than compared raw — a lock wait of 50 is fifty
seconds, and read as milliseconds it would look like the tightest bound in the run.
A value counts as unbounded when it comes back as zero or when it does not parse as a number. Zero is deliberately not read as a very tight bound: zero means no timeout on both engines, which is exactly the state this check exists to catch.
If every value came back bounded, the check passes. Otherwise it fails with a single finding naming the ones that did not:
This run set its own timeouts and the session reports statement_timeout and lock_timeout as unbounded when read back. A
SETthat returns no error can still stop applying — under transaction pooling, session state does not survive the end of a transaction — so the run is not bounded even though it asked to be. Point the preflight at a session-pooled connection, or at the database directly.
The finding is located on the connection this run addressed, under session timeouts, as a setting.
No table is involved, and the finding reports under sqlens.deploy — the subject rather than the
command, because both deploy commands run the same checks.
The run header shows the numbers this check judges on, taken through the same reader — one implementation behind one seam, not a second, independent way of asking. Two ways of reading one fact are two chances to disagree, and the disagreement would surface as a header claiming a bound beside a verdict saying there is none. The header does ask again, in a read of its own, and when that read fails it stays silent rather than printing what the run requested.
Why it matters
A set value is not an assurance; a read-back one is.
Under PgBouncer in transaction pooling mode, session state does not reliably survive the end of a
transaction. The SET is accepted, returns no error, and quietly stops applying. Nothing about that
is visible from the setting side: the command would print the values it asked for, a reader would
believe the run was bounded, and a statement that hung would hang for as long as the server allows.
That is silent green at the most sensitive point in the whole gate — the place where this tool cannot hurt your database is supposed to become true. Every promise the deploy gate makes about not harming a production database rests on those timeouts holding.
So the values are read back and the check fails rather than warns. A bounded run and an unbounded one are different products, and continuing under an assurance that does not hold is the thing this package refuses everywhere else.
What to do about it
The decision is which connection the preflight uses, and there are three answers: through the
transaction pooler, through a session-pooled connection, or straight at the database. Only the last
two keep a SET alive beyond the transaction that issued it, which is the shape the session defense
is written for.
Having decided, name it:
// config/sqlens.php
'preflight' => [
// Session pooling, or past the pooler to the database. Null by default.
'connection' => 'sqlens_preflight',
],
Left null, the reading falls back to sqlens.connection and then stops. It never falls through to
your application's default connection — that is the one running your migrations, and a preflight
reading through it would hold ALTER and DROP it never needs. When the connection it does resolve
happens to be the migration connection, the run reads and says so rather than refusing. See
the preflight connection and its privileges
for what that connection needs to be able to read.
When it reports undetermined instead
When the session will not answer at all, the check reports undetermined with the reason attached,
never a pass:
the session would not say which timeouts are in force (…), so nothing is known about whether this run is bounded. An unread bound is not a bound: the statements that follow could run for as long as the server allows.
A read that succeeds and returns no row is reported as its own reason — the session answered nothing about its timeouts — because "the read failed" and "the read succeeded and said nothing" arrive at the same place and mean different things.
Undetermined blocks a deploy the same way a finding does. --allow-undetermined is the deliberate way
out, and it opens exactly one door: a run whose only blockers could not answer proceeds.
What it does not claim
- It reports that SQLens' own session bounds are not in effect for THIS run. It says nothing about the timeouts the migration itself sets.
- A session setting can be overridden after this check ran, so the finding describes the state at the moment it looked.
- It reads the two values named above, per engine, and no others. PostgreSQL's
idle_in_transaction_session_timeoutis set by the session defense and is not read back here, so a pass is not a statement that every bound this run asked for took. - On MySQL, the lock value it reads is not the one this run set. The session defense bounds
innodb_lock_wait_timeout— the wait for a row lock — while this check reads backlock_wait_timeout, the wait for a metadata lock. They are different settings, and MySQL ships the one that is read at31536000seconds, so what a pass reports there is the server's default rather than a bound this run established. The setting the run did bound is not read back. - On PostgreSQL it reads what the READING transaction has in force. The session defense issues
SET statement_timeoutandSET lock_timeouton the session andSET LOCALversions of both inside the read transaction, andcurrent_setting()inside that transaction answers with the transaction-local value. A pass therefore says the bounds hold for the statement that asked — not that the session-levelSEToutlived a transaction pooler. - An unparseable answer and a genuine zero produce the same finding. A value the check cannot read as a number is treated as unbounded rather than assumed fine — the safe direction, and the reason the message says unbounded rather than guessing which of the two happened.
- It does not name why the bound was lost. Transaction pooling is the case the message calls out because it is the common one; the check reads a value, not a topology.