mnesia: Release table fixes when coordinator dies - #11517
Open
stnby wants to merge 1 commit into
Open
Conversation
Contributor
CT Test Results 2 files 59 suites 18m 35s ⏱️ Results for commit 67fb374. ♻️ This comment has been updated with latest results. To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass. See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally. Artifacts
// Erlang/OTP Github Action Bot |
Iterating a table in a transaction (mnesia:first/1 and friends on
anything but an ordered_set) makes mnesia_tm fix the table. The fix is
held by mnesia_tm and not by the coordinator, so it has to be released
explicitly when the coordinator dies. The normal termination path does
that, recover_coordinator/2 did not.
A fixed table never reclaims the space of deleted objects, so a dets
table backing disc_only_copies grows without bound at a constant row
count.
To verify:
mnesia:create_table(foo, [{type, set}, {ram_copies, [node()]}]),
P = spawn(fun() -> mnesia:transaction(
fun() -> mnesia:first(foo), timer:sleep(infinity) end) end),
timer:sleep(100),
exit(P, kill),
timer:sleep(100),
%% the table is still fixed, by the mnesia_tm process
{_, [{_, 1}]} = ets:info(foo, safe_fixed_monotonic_time).
Added a test case to the atomicity suite.
stnby
force-pushed
the
mnesia/fix-fixtable-leak-on-coordinator-death
branch
from
August 20, 2026 11:08
914ea3a to
67fb374
Compare
Contributor
|
Please sign the CLA |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A transaction that iterates a table leaks a
safe_fixtablehold whenever itscoordinator dies abnormally. The table stays fixed for the lifetime of the node.
mnesia:first/1,last/1,next/2,prev/2,selectandselect_reverseon anything but an
ordered_setroute throughdo_fixtable/2, which asksmnesia_tmto fix the table. The fix is therefore taken by themnesia_tmprocess, not by the coordinator, so the automatic release that
ets:safe_fixtable/2anddets:safe_fixtable/2perform when the fixing processdies never fires:
mnesia_tmoutlives the transaction.mnesia_tmhas torelease the fix explicitly.
The normal termination path does that when it handles
{delete_transaction, Tid}(
mnesia_tm.erl:411), butrecover_coordinator/2never did.An application cannot work around this, because the client process never owned
the fix.
Which failures leak
Everything originating inside the transaction fun is handled correctly; only
signals arriving from outside the coordinator leak:
mnesia:abort/1throwin the funexit(Pid, kill)exit(Pid, shutdown)Impact
A fixed table never reclaims the space of deleted objects, so this is not merely
a leaked handle.
With one leaked fix, a
disc_only_copiestable under steady-state churn(20000 rows deleted and 20000 reinserted per cycle, row count constant) grew
from 19 197 280 to 177 473 872 bytes over four cycles, roughly 39.5 MB per
cycle without bound. The same table with no leaked fix stayed flat at
19 402 600 bytes.
An
ets-backed table retains every deleted tuple under a sliding-windowworkload, 5.2x the clean control after four cycles at identical row count.
Two properties worth noting:
produce byte-identical results, since fixation is a refcount and the retained
garbage depends on churn while fixed. A single kill is enough to disable space
reclamation on a table permanently.
ordered_setavoids thefix entirely, but mnesia rejects
ordered_setwithdisc_only_copies(
{not_supported, ordered_set, disc_only_copies}).It is silent: no crash, no error return, and the only log is a
verbose/2"Coordinator died" that does not mention fixtables.
Reproduction
Age
The mechanism was introduced whole in mnesia 4.2 (OTP R10B, 2004-10-05) and was
already asymmetric there:
clear_fixtablewas wired into thedelete_transactionpath but not intorecover_coordinator/2. mnesia 4.1.12(R9C-2) has neither
do_fixtablenorclear_fixtable.git log -S'clear_fixtable' -- lib/mnesia/src/mnesia_tm.erlreturns a singlecommit,
84adefa"The R13B03 release", the initial import, so the defectpredates the git history.
The change
One line in
recover_coordinator/2, placed beforeerase_ets_tabs/1becauseclear_fixtable/1reads the store thaterase_ets_tabs/1destroys. This is thesame order the normal path uses.
Nested transactions are covered, because
copy_ets/2copies the parent storeinto each nested store, so
hd(Etabs)carries all accumulatedfixtableentries.
Test
fixtable_released_when_coordinator_dies/1inmnesia_atomicity_test, coveringboth
ram_copiesanddisc_only_copies. It asserts that a transaction ending byitself releases its fix, that a fix is actually taken before the kill, and that
it is released after the coordinator is killed.
Verified to fail before the change (2 assertion failures, one per storage type)
and pass after it.
Verification
mnesia_atomicity_test55/55 with the change, 54/55 without it (the new casebeing the failure)
mnesia_trans_access_test40/40,mnesia_isolation_test48/48,mnesia_evil_coverage_test55/55, identical with and without the changePerformance:
recover_coordinator/2has a single caller,handle_exit/3,reached only on abnormal coordinator death, so the normal transaction path is
untouched. Measured over 4 VM runs of 7 reps each, hot-path transaction
throughput is unchanged within run-to-run variance. The cold path costs
+1.9 us per abnormally terminated coordinator holding no fix (one
ets:lookup/2returning[]) and +5.9 us when a fix is held, which is therelease work that was previously skipped.
mnesia_tmstill recovers roughly31 000 killed coordinators per second.
Not addressed here
mnesia.erltakes the fix and records it in the transaction store in twoseparate statements. A coordinator killed between them leaves a hold that
clear_fixtable/1cannot see, because it was never recorded. I confirmed thewindow is reachable by widening it artificially, but it needs a different and
more invasive fix, so it is deliberately left out of this change.
Caveats
Tested against an installed OTP 29.0.1 / mnesia 4.26 rather than a from-source
OTP tree, driving the suites through
mnesia_test_lib:eval_test_case/3../otp_build checkhas therefore not been run.