Uploaded image for project: 'MariaDB Server'
  1. MariaDB Server
  2. MDEV-40121

Aria memory accounting imbalance

    XMLWordPrintable

Details

    • Task
    • Status: Open (View Workflow)
    • Major
    • Resolution: Unresolved
    • None
    • Server
    • 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.

      Attachments

        Activity

          People

            Unassigned Unassigned
            Johnston Rex Johnston
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

            Dates

              Created:
              Updated:

              Git Integration

                Error rendering 'com.xiplink.jira.git.jira_git_plugin:git-issue-webpanel'. Please contact your Jira administrators.