Details
-
Task
-
Status: Open (View Workflow)
-
Major
-
Resolution: Unresolved
-
None
-
None
Description
The setup: a manager-owned "batch table" written by a worker
In the earlier parallel-query design, the manager thread created one temporary "batch" table per worker up front, during optimization, in
JOIN::create_parallel_workers_tmp_tables() (the function the summary references — since removed when the design moved to workers-execute-the-join).
Each table was built with create_tmp_table(do_not_open) + instantiate_tmp_table(cross_thread=true) and parked on the post-join aggregation tab.
The data-flow ownership was deliberately split across two threads:
- the worker thread writes rows into its batch table (the projected/matched result rows it ships up), so the table's backing file grows under the
worker; - the manager thread owns, drains, and frees the table — it created it and tears it down after reading the rows out.
That split is the whole problem, because of how MariaDB accounts for temp-table disk usage.
The accounting mechanism
Aria temp tables register their backing file with the mysys temp-file tracker.
Every time that file grows or shrinks, mysys fires temp_file_size_cb_func() (sql/mysqld.cc:3805). The callback:
1. computes size_change = file_size - previous_file_size (mysqld.cc:3810);
2. charges it to current_thd — i.e. whichever thread is doing the I/O right now:
- thd->status_var.tmp_space_used += size_change (mysqld.cc:3855)
- global_tmp_space_used += size_change (mysqld.cc:3830);
3. enforces the per-connection (max_tmp_space_usage) and global (global_max_tmp_space_usage) ceilings, returning ENOSPC if exceeded;
4. asserts, on entry, that the current thread has already been charged for the prior size:
DBUG_ASSERT(thd->status_var.tmp_space_used >= track->previous_file_size) (mysqld.cc:3826).
That callback is the crux: it implicitly assumes one thread both grows and frees a given temp file, because it always bills current_thd. HEAP
(in-memory) temp tables have no backing file, so they never invoke this callback at all.
How the split tripped the assert
With an on-disk Aria batch table, the charge and the credit landed on different threads:
- The worker grows the Aria file → callback runs with current_thd == worker → the worker's tmp_space_used climbs by the file growth.
- The worker never shrinks the file (it just writes). When the manager frees/truncates the table during teardown, the callback runs with current_thd
== manager → a negative size_change is credited to the manager's tmp_space_used, which never charged it.
So the books never balance per-thread:
- the worker is left with a positive, never-credited tmp_space_used;
- the manager is pushed toward negative (it gets credited for space it never owned).
The visible failure was on the worker side at connection teardown. THD::free_connection() ends with:
DBUG_ASSERT(status_var.tmp_space_used == 0 ||
!debug_assert_on_not_freed_memory); // sql_class.cc:1811
The worker's tmp_space_used was still non-zero (it had been billed for file growth that only the manager ever "freed", on a different THD), so the
assert fired. The companion invariant tmp_space_used >= previous_file_size at mysqld.cc:3826 is the same imbalance seen from the other direction and
is equally at risk on the manager when it frees a file it was never charged for.
HA_LEX_CREATE_GLOBAL_TMP_TABLE would not fix that assert. It targets a different accounting counter than the one that tripped.
Why — two independent flags, two independent counters
There are two separate accounting systems in play, and they're gated by two different open flags, set side-by-side at ma_open.c:1007-1013:
share->malloc_flag =
(open_flags & HA_OPEN_GLOBAL_TMP_TABLE) ? 0 : MY_THREAD_SPECIFIC; // MEMORY
...
share->tracked = MY_TEST(open_flags & HA_OPEN_SIZE_TRACKING); // DISK
- HA_…GLOBAL_TMP_TABLE → controls only share->malloc_flag, i.e. whether memory allocations get MY_THREAD_SPECIFIC (charged to local_memory_used of a specific THD) or counted globally. Inside Aria's create path it does literally one thing — suppress MY_THREAD_SPECIFIC on create-time allocations (ma_create.c:104). Its real purpose (see handler.cc:3920-3927) is replication temp tables that aren't attached to any THD, so their memory
can't be billed to one thread. - HA_OPEN_SIZE_TRACKING → controls share->tracked, which is what drives the disk tmp_space_used accounting. The _ma_update_tmp_file_size() calls that fire on file growth are all guarded by if (info->s->tracked && …) (e.g. ma_dynrec.c:448, ma_bitmap.c:3350), and they funnel into temp_file_size_cb_func() (mysqld.cc:3805).
The counter that actually asserted is the disk one — THD::free_connection() at sql_class.cc:1811 checks status_var.tmp_space_used == 0. That counter is charged exclusively through temp_file_size_cb_func, and that callback unconditionally bills current_thd (mysqld.cc:3855). It never looks at the GLOBAL flag at all.
So the cross-thread imbalance — worker grows the file (charged to worker), manager frees it (credited to manager) — is in the tracked/HA_OPEN_SIZE_TRACKING path. Toggling the GLOBAL flag changes memory attribution and leaves the disk path, and therefore the assert, exactly as it was.
What it would do
If there were also a cross-thread memory imbalance (local_memory_used charged to the worker, freed by the manager), then making those allocations non-thread-specific (the GLOBAL flag) would help that. But that isn't the failure described — the reported assert is the disk tmp_space_used one.
What actually balances the disk counter
1. HEAP table — the fix that was applied. No backing file ⇒ tracking callback never fires.
2. Don't pass HA_OPEN_SIZE_TRACKING (share->tracked = 0) — kills per-thread disk charging for these tables. Downside: you also lose max_tmp_space_usage / global quota enforcement and the Tmp_space_used visibility for them.
3. Single-THD ownership — same thread writes and frees, so charge and credit land together. This is effectively what the workers-execute-the-join redesign achieves (each worker owns its own result table end-to-end).
4. Manual reconciliation at handoff — when the table passes worker→manager, transfer the tmp_space_used delta (subtract from worker, add to manager). Balances both free_connection() asserts but is fiddly.
Bottom line: the GLOBAL flag is a memory-attribution knob; the assert is a disk-temp-space per-thread imbalance. They don't intersect.