DEPLOY.PREFLIGHT.METADATA_LOCK_BLOCKER — a session is sitting on a table this migration is about to change
- Category: safety
- Severity: high
- Level: 0
- Downtime class:
blocking— the finding this check emits carries it. A metadata lock a DDL will queue behind is precisely a fact about concurrent traffic, which is what this axis measures - Stability: stable
- Suites: deploy
- Applies to: MySQL — the check answers
appliesTo()for themysqldriver and for nothing else
What it reports
A session that has been running longer than this run's threshold on one of the tables the pending migration is about to change.
The check builds that table list itself, from the pending statements the deploy already
canonicalized. A statement is considered only if its kind is one that takes an exclusive metadata
lock at its boundaries — AlterTable, DropTable, TruncateTable, AddColumn, AlterColumn,
DropColumn, AddConstraint, AddPrimaryKey, AddForeignKey, DropConstraint, CreateIndex,
DropIndex, Rename. Of that statement's targets it keeps the ones that are tables and that the
statement acts on, not the ones it merely points at.
Those table names then go into the activity reading as its focus, together with the run's
longRunningMs threshold — 5,000 ms unless the project configures another. Handing the targets in is
what makes the match possible at all: a long-running session carries a relation only for the tables
the caller named, so a reading asked without a focus comes back with every relation null and nothing
can ever match, however blocked the instance is.
On MySQL that reading comes from the server's own state views. The long transactions come from
information_schema.INNODB_TRX; which of the focus tables each session currently holds a granted
metadata lock on comes from performance_schema.metadata_locks, joined to performance_schema.threads
so that the lock's OWNER_THREAD_ID lines up with the connection id INNODB_TRX speaks.
One finding is emitted per matching session, and it says which thread, in which state, on which table, and for how long:
Thread <session> has been <state> on `<table>` for <seconds> s, and this deploy is about to change
that table. […] Nothing here says that thread is wrong to be running; it says the migration is about
to collide with it.
Why it matters
InnoDB's online DDL is online in the middle and not at the ends. It needs a brief EXCLUSIVE metadata lock to start and another one to finish, and a transaction open on the target table blocks exactly those two moments — whatever the algorithm, because the two ends are exclusive in all of them.
While the ALTER waits for its metadata lock, every new statement touching that table queues behind
it. The table stops answering, and the ALTER itself is doing nothing wrong. MySQL grants metadata
locks in order, so one long transaction blocks the migration and every reader queued behind it:
the finding is about the queue, not only about the one session it names.
That is why the finding carries the blocking downtime class — what it describes is a lock that
blocks concurrent access for its duration. The check as a catalog entry carries none, for the reason
in the metadata above: a deploy check reports on the state of a target rather than on a DDL operation
of its own.
What to do about it
The finding does not tell you to kill anything. It hands you the three facts the decision needs, and the decision is will this session still be there when the migration starts:
- Which session. The label is the connection id, because that is the number
SHOW PROCESSLISTandKILLaccept —performance_schemaidentifies a session byTHREAD_ID, which is a different numbering space and correlates with nothing else in the same snapshot. This finding always names a connection: the long runners come fromINNODB_TRX, which speaks connection ids, and the held-lock reading drops every row whosePROCESSLIST_IDis null. The reader's thread spelling — held by something you cannotKILL, which it uses rather than fabricating an id — therefore belongs to its lock-wait rows, not to this check. - What it is doing. The state comes through in the server's own vocabulary, and it is what separates the two very different cases: a long active query is somebody's slow report, while a long idle transaction is a connection somebody forgot to close.
- How long it has been running. Against the threshold this run was configured with.
Then act on that: let the session finish, end it, or move the deploy — and re-run the preflight, because the reading is a snapshot and the answer is only about the moment it looked.
If the result is undetermined rather than fail, the reason names what to fix and the result
blocks exactly as a failure does — anything that is not a pass blocks. sqlens:predeploy is
fail-closed, and --allow-undetermined is the one door out of that: it proceeds when every
blocker is a check that could not answer, and never when one of them found something. Three shapes
reach that state:
- no activity reader at all for this run, so what is holding a metadata lock is unknown — that is not the same as nothing holding one
- the activity views could not be read, with the server's own refusal carried through verbatim rather than paraphrased, so it points at the right grant
- the reading came back empty AND incomplete, with every gap named — the instrument, and why it was not readable
Enabling performance_schema needs a server restart, not a grant. Which privileges each state view
wants, and what each one does without them, is on
the deploy readers page and in
what sqlens:predeploy needs.
Why an empty reading is not automatically a pass
This is the whole check, and the reason it exists separately from its PostgreSQL counterpart.
performance_schema can be off, and its metadata_locks instrument is disabled by default in
several distributions. Neither produces an error: the views are present, they answer, and they answer
with an empty set. So no blocker and the instrument that would have told me is switched off
arrive as the very same answer.
The check therefore asks whether the reading is safe to read silence from before it reports anything
clean. An empty reading with a named gap is undetermined; an empty reading with no gaps is a pass.
Reporting a pass off the empty list alone would be the single failure this package exists to prevent.
What produces that named gap is worth knowing exactly, because it is narrower than the paragraph
above. The activity reading asks the server one question — select @@performance_schema — and names
the gap when the answer is off; that is the shape this check turns into undetermined. It does not
read setup_instruments, so the other case, performance_schema on with the metadata_locks
instrument alone disabled, leaves no gap behind and this check passes on it. See the limit below.
What it does not claim
- It is a snapshot, like its PostgreSQL counterpart: the blocking session may be gone by the time the migration runs.
- It sees the tables the statements ACT ON. The referenced side of a foreign key is deliberately excluded: MySQL takes a shared metadata lock there, which is not what this finding claims, and the finding says this deploy is about to CHANGE the table it names. The cost is named rather than hidden — a long transaction on the referenced table can still delay the statement, and this check does not look for that.
- A constraint is not a relation the activity views can report, so a constraint name never enters the reading.
- It does not prove that the
metadata_locksinstrument is on. The gap it can name isperformance_schemabeing off as a whole, asked as a server variable. A server running withperformance_schemaon and only the metadata-lock instrument disabled answers with an empty set and no gap, so the check passes — the one silence this reading cannot tell from calm. Before reading a pass here as proof that nothing is sitting on the table, ask the server yourself:select ENABLED from performance_schema.setup_instruments where NAME = 'wait/lock/metadata/sql/mdl'. - A statement that takes no exclusive metadata lock is not considered. Plain DML is not in the
list of kinds above, so a backfill never makes a table look like a blocked
ALTER. - Nothing is reported when nothing is pending. With no pending migration the check passes without reading anything, because there is no table for a session to collide with.
- One session holding two of the deploy's tables produces one finding naming one of them. The deploy is blocked either way.
- It says nothing about a session on a table this migration does not touch. A long-running session is reported only when its relation is one of the tables the pending statements will change.
Related
- Understanding
undetermined— why a check that could not run is reported rather than swallowed - The deploy readers — what the activity reader needs to see, and what a partial answer means
- What
sqlens:predeployneeds — the read-only role, and which checks go undetermined without which privilege