Skip to main content

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 SET that 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_timeout is 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 back lock_wait_timeout, the wait for a metadata lock. They are different settings, and MySQL ships the one that is read at 31536000 seconds, 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_timeout and SET lock_timeout on the session and SET LOCAL versions of both inside the read transaction, and current_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-level SET outlived 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.