Do not send SKIP LOCKED to servers that do not implement it - #71250
Do not send SKIP LOCKED to servers that do not implement 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 really scale other than vertically, *can't easily be sharded, since the scheduler critical section, TI state, XCom and event logs all write to one primary, and we always just get 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 from it, since we already run read replicas. Either route reads explicitly in Airflow's own source, or put query routing rules in a ProxySQL 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/txns. That makes distributing writes interesting, and TiDB uses the MySQL wire protocol. I haven't reached to the point to benchmarked it yet, 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 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? |
Airflow claims work with
SELECT ... FOR UPDATE ... SKIP LOCKEDin eleven places. On TiDB, any ofthose queries that also joins another table fails outright with error 1105, which takes down
SchedulerJob._run_scheduler_loop. Nothing is scheduled until the loop is restarted.The cause is in TiDB's planner.
LogicalLock.PruneColumnsappends every locked table's handle column to
parentUsedColsso those columns survive columnpruning — but only when the lock type is supported.
SKIP LOCKEDis absent fromisSelectForUpdateLockType,so the function returns early and the handle columns get pruned away while
TblID2Handlestillreferences them. Reported as pingcap/tidb#67715
(open,
sig/planner,severity/major,affects-8.5). On a query with no join there is no secondhandle to lose, so the clause is instead accepted and silently ignored — the same absence from the
supported set, a different symptom.
with_row_locks()already degrades gracefully for MySQL-family servers that cannot lock at all.This extends that to a server that accepts
SKIP LOCKEDand does not implement it: read the versionbanner once per engine, and fall back to plain blocking
FOR UPDATE. Claimers serialize instead ofskipping ahead, which is correct and only applies to servers that were never providing the
guarantee. If the version probe fails the previous behaviour is kept. PostgreSQL and MySQL are
untouched.
Testing Done
Two real
airflow schedulerprocesses against one TiDB v8.5.1, asset-triggered Dags, 6 producerruns. Run twice against the same deployment, once per branch:
{queued: 6}{None: 36}{failed: 6}{failed: 36}Without it the loop dies and nothing is ever scheduled — all 36 task instances stay
None. With itthe loop survives and all 36 are scheduled and dispatched. They then fail because this harness runs
no api-server, which Airflow 3 needs to execute tasks; the signal here is loop survival and whether
task instances are scheduled at all.
Raw logs
Reduced to two tables, no Airflow. MySQL 8.4.11 accepts both forms; TiDB rejects the second on
both v8.5.1 and current master (
b76bfbc):Two schedulers, without this change — the loop dies:
Two schedulers, with this change:
Emitted SQL per backend:
New unit tests against the unpatched source — they fail, which is the point:
With the change:
35 passedintests/unit/utils/test_sqlalchemy.py.Regressions:
test_scheduler_job.py -k "critical_section or executable_task_instances or row_lock or pool"— 47 passed.Open question on the shape of this fix
This detects the engine by version banner and degrades silently. That is the weakest of three
options and I would rather land the right one than this one.
The precedent in
with_row_locks()pairs its MariaDB degradation with an explicit statement thatHA scheduling is not supported there. This change does not do that: it makes an engine that cannot
provide the locking semantics Airflow needs look like it works. On the crash path in particular,
one could argue the error is the correct outcome, since it surfaces an unsupported backend
immediately rather than running on with different locking behaviour.
Alternatives, in increasing order of how well they generalise:
SKIP LOCKEDandNOWAIT, and engines that do not are unsupported for HA scheduling. No code,covers every fork.
airflow db check. Run a two-session probe once and failfast with an actionable message instead of crashing in the scheduler loop later. Detects
behaviour rather than matching names, so it covers forks nobody has heard of.
Happy to convert this to (1) or (2) if that is the preferred direction.
Note: the main scheduler claim in
_executable_task_instances_to_queuedruns inside theslot_pool ... FOR UPDATE NOWAITcritical section, and TiDB implements NOWAIT correctly, soconcurrent schedulers already serialize there. This change is about the claim queries that rely on
SKIP LOCKEDalone.Was generative AI tooling used to co-author this PR?
Generated-by: GitHub Copilot CLI (Claude Opus 5) following the guidelines