MDEV-39344: trx_disconnect_prepared() uses wrong mutex#5094
Conversation
trx_t::disconnect_prepared(): Replaces trx_disconnect_prepared(). Protect the data members with trx_t::mutex. trx_sys.trx_list: Use a plain ilist<trx_t> that is protected by lock_sys.latch, which was already protecting some traversal of trx_sys.trx_list. All traversal will now use range for, instead of callback functions. fetch_data_into_cache_low(): Return whether we ran out of memory. fetch_data_into_cache(): Reduce the hold time of lock_sys.latch and do not wrongly reset the cache->is_truncated flag at the end. trx_sys_t::clone_oldest_view(): Protect the list traversal with shared lock_sys.latch. trx_sys_t::register_trx(), trx_sys_t::deregister_trx(): Protect trx_sys.trx_list with an exclusive lock_sys.latch.
|
|
| bool found= false; | ||
| find_interesting_trx f | ||
| {found, withdraw_started, my_hrtime_coarse()}; | ||
| withdraw_started = current_time; | ||
| const my_hrtime_t current_hrtime{my_hrtime_coarse()}; | ||
| bool found{false}; |
There was a problem hiding this comment.
The changes to this file buf0buf.cc are only applicable to the 10.6 branches that do not include #3107. We’re only replacing callback-based traversal with an inline range for loop here.
There was a problem hiding this comment.
Code Review
This pull request refactors the InnoDB transaction system by replacing the thread_safe_trx_ilist_t wrapper with a standard ilist protected by the global lock_sys.latch and converting functor-based iterations into manual loops. Feedback points out a logic error in the InnoDB monitor where view_count might be incorrectly reported as zero, a bug in the Information Schema cache regarding truncation signaling, and potential data races when accessing transaction fields. Additionally, there is a concern that using the central lock_sys.latch for transaction registration could create a scalability bottleneck.
| "--------------\n" | ||
| "ROW OPERATIONS\n" | ||
| "--------------\n" | ||
| "%zu read views open inside InnoDB\n", view_count); |
There was a problem hiding this comment.
The view_count variable may incorrectly report 0 here if the InnoDB lock monitor is disabled. It is initialized to 0 at line 777 and only updated within the conditional block at line 796. To ensure accurate reporting in the monitor output, view_count should be fetched regardless of whether the lock monitor is active, while ensuring that lock_sys.latch is held during the call to trx_sys.view_count().
There was a problem hiding this comment.
This was intentional, but not mentioned in the commit message. If we need to protect trx_sys.trx_list with something else than lock_sys.latch then this problem would go away; we’d unconditionally read the view_count at this spot.
| if (!trx.is_started() || !trx.mysql_thd | ||
| || withdraw_started <= trx.start_time_micro) { |
There was a problem hiding this comment.
Accessing trx.mysql_thd and trx.is_started() without holding trx.mutex is a data race. These fields are modified in trx_t::disconnect_prepared() under the protection of trx.mutex. While the impact in this context (buffer pool resizing diagnostics) is likely limited to incorrect warnings, it is technically undefined behavior. Consider acquiring the transaction mutex before checking these fields.
| lock_sys.wr_lock(SRW_LOCK_CALL); | ||
| trx_list.push_front(*trx); | ||
| lock_sys.wr_unlock(); |
There was a problem hiding this comment.
Protecting the global transaction list with the central lock_sys.latch during register_trx and deregister_trx may introduce a scalability bottleneck. These functions are called at the start and end of every transaction. Moving from a dedicated list mutex to an exclusive lock on the heavily-contended lock_sys.latch could increase contention in high-concurrency environments.
There was a problem hiding this comment.
Yes, exactly this we need to find out by running some performance tests.
| void create() { mysql_mutex_init(trx_sys_mutex_key, &mutex, nullptr); } | ||
| void close() { mysql_mutex_destroy(&mutex); } |
There was a problem hiding this comment.
Sorry, I forgot to remove the trx_sys_mutex_key. If it turns out that this change will reduce performance due to increased contention on lock_sys.latch, I’ll put back the mutex, preferrably as srw_mutex or srw_lock_low to reduce overhead.
svoj
left a comment
There was a problem hiding this comment.
I don't feel I like the idea of moving trx_sys.trx_list under the lock_sys.latch protection. I had hard time trying to follow the updated code. I feel like it increases maintenance cost. And it doesn't directly relate to the reported problem, it was about trx members protection rather than trx_sys.trx_list.
I'm less concerned about extra pressure on the lock_sys.latch, but I agree that it should be tested and we need to suggest correct benchmarks for verification that'd cover worst case scenario. E.g. combination of connect + oltp_read_write?
Why would we want to have this change in 10.6, which is EOL in 1.5 months? Shouldn't such refactoring be targeted for the main?
Also if #5043 is going to be pushed, this list is becoming somewhat redundant. We can usr rw_trx_ids.trxs instead.
| /* todo/fixme: suggest to do it at innodb prepare */ | ||
| trx->will_lock= false; | ||
| trx_sys.rw_trx_hash.put_pins(trx); | ||
| will_lock= false; |
There was a problem hiding this comment.
Previously will_lock was updated out of critical section. IIUC it is always accessed by the owner thread, except for 2 occurrences in lock0lock.cc.
check_trx_state()can be called with eithertrx->mutexortrx->rw_trx_hash_element->mutexlocked, there's one case when both are lockedlock_print_infodoesn't locktrx->mutexwill_lockis updated in lots of places without holdingtrx->mutex
This looks like a very inconsistent locking pattern. I don't see much value in moving this specific will_lock update under trx->mutex. I suggest that we should reevaluate its locking pattern separately.
| ut_ad(mysql_thd); | ||
| ut_ad(!mysql_log_file_name); | ||
| read_view.close(); | ||
| mutex.wr_lock(); |
There was a problem hiding this comment.
Why not mutex_lock()? Because of assert()?
| size_t total_trx= 0, prepared_trx= 0; | ||
|
|
||
| trx_sys.trx_list.for_each([&](const trx_t &trx) { | ||
| for (const trx_t &trx : trx_list) |
There was a problem hiding this comment.
Don't you need a lock here? IIUC it can be executed in multithreaded environment.
trx_t::disconnect_prepared(): Replacestrx_disconnect_prepared(). Protect the data members withtrx_t::mutex.trx_sys.trx_list: Use a plainilist<trx_t>that is protected bylock_sys.latch, which was already protecting some traversal oftrx_sys.trx_list. All traversal will now use range for, instead of callback functions.fetch_data_into_cache_low(): Return whether we ran out of memory.fetch_data_into_cache(): Reduce the hold time oflock_sys.latchand do not wrongly reset thecache->is_truncatedflag at the end.trx_sys_t::clone_oldest_view(): Protect the list traversal with sharedlock_sys.latch.trx_sys_t::register_trx(),trx_sys_t::deregister_trx(): Protecttrx_sys.trx_listwith an exclusivelock_sys.latch.