Do not send SKIP LOCKED to servers that silently ignore it - #71250
Do not send SKIP LOCKED to servers that silently ignore it#712501fanwang wants to merge 1 commit into
Conversation
|
Oh, fascinating Stefan, I am not familiar with TiDB. |
bda3af6 to
42455fd
Compare
Hey Vikram, I was actually just reading more about #46175 and #65453. To be forthcoming - I'm not proposing official support for Airflow on a TiDB backend, and this PR isn't meant to be a step toward asking for it, at least for now. Let me share
For scale context, our largest single cluster is reaching 25k+ Dags and still growing. Most of the scaling problems we've hit have answers that stay close to upstream: add schedulers, tune the executor, etc.. The metadata DB is the one that doesn't. It's the piece *can't easily be sharded, since the scheduler critical section, TI state, XCom and event logs all land on one primary, and when that saturates the options are a bigger box (we've done that many times) or changes that drift away from OSS Airflow. I'd rather not drift. We've also been trying read/write splitting to take some pressure off it, since we already run read replicas. Either route reads explicitly in Airflow's own source, or put query routing rules in a [ProxySQL](https://proxysql.com/) layer so Airflow core can stay generic. No numbers to share yet, but the challenge is clear - it only moves read load, and the scheduler's hot path is writes. The queries that actually hurt are the ones you can't route away. That's what makes distributing writes interesting, and TiDB uses the MySQL wire protocol. I haven't benchmarked it though, so "scales writes" is a motivation and not a result I can show at this point. So far this is just a small local cluster with dev Airflow pointed at it, checking the SQL queries (the ones scheduler depends on). Most of it holds up: pessimistic FOR UPDATE blocks, NOWAIT errors, GET_LOCK is exclusive, READ COMMITTED behaves, savepoints roll back, FK cascades are enforced. The 3 prs I opened addresses some of the minor issues I found during testing. For this PR tho, my original thought is it isn't really a TiDB-specific problem. Any server that accepts SKIP LOCKED and quietly drops it hands two schedulers the same rows, and Airflow never finds out (it should fail loudly instead). I do plan on trying this on our internal Airflow and TiDB clusters. Happy to share what we find running Airflow on TiDB at scale if folks are interested. The three that came out of the exercise, for reference: |
The scheduler claims task instances with SELECT ... FOR UPDATE SKIP LOCKED and relies on the clause to keep concurrent schedulers off the same rows. A server that accepts the clause and discards it hands the same rows to every scheduler at once, with no error and no warning, so the safety property is lost silently rather than loudly. Signed-off-by: 1fanwang <1fannnw@gmail.com>
42455fd to
d12c1fe
Compare
|
I don’t quite understand. So TiDB silently ignores SKIP LOCKED, but this PR simply makes Airflow not send that. So the end result is unchanged? Why is this PR needed? |
The scheduler claims task instances with
SELECT ... FOR UPDATE SKIP LOCKEDandrelies on the clause to keep concurrent schedulers off the same rows. A server
that accepts the clause and then discards it hands the same rows to every
scheduler at once — no error, no warning, no degraded-mode log line.
with_row_locks()already guards the case where a MySQL-family server cannotlock at all. This extends that to a server that claims to skip but does not: read
the version banner once per engine, and fall back to plain blocking
FOR UPDATEwhen the server is known to ignore
SKIP LOCKED. Schedulers then serializeinstead of skipping ahead, which is correct — and only on servers that were
never providing the guarantee. PostgreSQL and MySQL are untouched.
TiDB is the case I hit: it has parsed and ignored
SKIP LOCKEDsince 2020(pingcap/tidb#18207, still open,
listed under unsupported features).
Unlike
FOR SHARE, which it rejects loudly with error 1235, there is nothing foran operator to notice.
If the version probe fails the previous behaviour is kept, so a transient
connection problem cannot silently disable
SKIP LOCKEDon a server thatsupports it.
Testing Done
Against TiDB v8.5.1, with MySQL 8.4 and PostgreSQL 16 as controls:
FOR UPDATE OF task_instanceFOR UPDATE OF task_instance SKIP LOCKEDFOR NO KEY UPDATE OF task_instance SKIP LOCKEDTwo schedulers against one TiDB, 15 dag runs: all successful, zero retries, zero
duplicate attempts.
Raw logs
The hazard. Two workers running the scheduler's claim shape, each holding its
read before either commits:
Both claimed every row, both committed, the later write won.
SHOW WARNINGSwasempty,
tidb_enable_noop_functionswasOFF. On MySQL 8 the claim sets are disjoint.Same race with the fallback (plain
FOR UPDATE, READ COMMITTED):Emitted SQL per backend, and the warning operators get:
New tests against the unpatched source — they fail, which is the point:
With the change:
35 passedintests/unit/utils/test_sqlalchemy.py.Two schedulers, one TiDB, 15 dag runs:
Regressions:
test_scheduler_job.py -k "critical_section or executable_task_instances or row_lock or pool"— 47 passed.Was generative AI tooling used to co-author this PR?
Generated-by: GitHub Copilot CLI (Claude Opus 5) following the guidelines