-
Notifications
You must be signed in to change notification settings - Fork 848
Avoid Stripe Mutex lock contention for RWW #12794
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR optimizes cache read performance by introducing a dedicated reader-writer lock (ts::bravo::shared_mutex) in the OpenDir class, reducing mutex contention for reader-while-writer (RWW) scenarios. The change allows concurrent read operations on OpenDir without requiring the StripeSM mutex, resulting in a reported 9.9% improvement in maximum requests per second under specific test conditions.
Changes:
- Added
ts::bravo::shared_mutexto OpenDir for fine-grained locking instead of relying on StripeSM mutex - Refactored open_read to work without holding the stripe mutex, enabling lock-free RWW path
- Introduced
new_CacheVC_for_readhelper function to reduce code duplication in Cache.cc - Added
CACHE_EVENT_OPEN_DIR_RETRYevent type for handling retries in the new locking scheme - Made OpenDir members private and reorganized API documentation in StripeSM.h
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/iocore/cache/StripeSM.h | Added API grouping comments to clarify OpenDir and PreservationTable methods |
| src/iocore/cache/P_CacheDir.h | Changed OpenDir to class with private members, added _shared_mutex for concurrency control |
| src/iocore/cache/CacheDir.cc | Implemented shared/exclusive locking in open_write, close_write, and open_read; updated signal_readers for new locking model |
| src/iocore/cache/Cache.cc | Refactored open_read to check OpenDir without stripe mutex first, added helper function for CacheVC creation |
| include/iocore/cache/CacheDefs.h | Added CACHE_EVENT_OPEN_DIR_RETRY event type for retry handling |
Comments suppressed due to low confidence (1)
src/iocore/cache/CacheDir.cc:193
- Potential use-after-free race condition: The open_read() method returns a raw pointer to an OpenDirEntry while holding a shared lock, but the lock is released when the function returns (line 183 creates a scoped lock). The caller then uses this pointer without any lock protection. Meanwhile, close_write() can acquire the writer lock and free the OpenDirEntry (line 174: THREAD_FREE). This creates a window where the returned OpenDirEntry pointer could be freed while the caller is still using it, leading to a use-after-free. The old code avoided this because both operations held the stripe mutex. Consider using reference counting for OpenDirEntry or ensuring the caller holds some protection until done with the entry.
OpenDirEntry *
OpenDir::open_read(const CryptoHash *key) const
{
ts::bravo::shared_lock lock(_shared_mutex);
unsigned int h = key->slice32(0);
int b = h % OPEN_DIR_BUCKETS;
for (OpenDirEntry *d = _bucket[b].head; d; d = d->link.next) {
if (d->writers.head->first_key == *key) {
return d;
}
}
return nullptr;
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| */ | ||
| int | ||
| OpenDir::signal_readers(int /* event ATS_UNUSED */, Event * /* e ATS_UNUSED */) | ||
| OpenDir::signal_readers(int event, Event * /* ATS UNUSED*/) |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment formatting uses "ATS UNUSED" with a space, but elsewhere in the codebase it's typically "ATS_UNUSED" with an underscore. Consider maintaining consistency with the existing pattern.
| OpenDir::signal_readers(int event, Event * /* ATS UNUSED*/) | |
| OpenDir::signal_readers(int event, Event * /* ATS_UNUSED */) |
| // Read-While-Writer | ||
| // This OpenDirEntry lookup doesn't need stripe mutex lock because OpenDir has own reader-writer lock | ||
| od = stripe->open_read(key); |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential lock ordering issue: This code calls stripe->open_read() (which now acquires _shared_mutex as a reader) while holding stripe->mutex. In other places like CacheRead.cc line 212, the same pattern exists. However, in Cache.cc line 570, open_read() is called without holding stripe->mutex. While this change is intentional to reduce contention, ensure that this mixed lock ordering (sometimes stripe->mutex then _shared_mutex, sometimes just _shared_mutex) doesn't introduce potential deadlock scenarios. The Bravo reader-writer lock should handle this safely since multiple readers can coexist, but this should be verified through testing under high concurrency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this is fine, because prior to the change, when we jump to the CacheVC::openReadFromWriter from the Lwriter label, it's out of scope of StripeSM mutex lock. CacheVC::openReadFromWriter has another check of OpenDir.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
trafficserver/src/iocore/cache/CacheRead.cc
Line 212 in a5363d2
| od = stripe->open_read(&first_key); // recheck in case the lock failed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, the reader lock of OpenDir might be need to be hold while this OpenDirEntry is used in the CacheVC::openReadFromWriter. Let me consider in case.
| class OpenDir : public Continuation | ||
| { | ||
| public: | ||
| OpenDir(); | ||
|
|
||
| int open_write(CacheVC *c, int allow_if_writers, int max_writers); | ||
| int close_write(CacheVC *c); | ||
| OpenDirEntry *open_read(const CryptoHash *key) const; | ||
| int signal_readers(int event, Event *e); | ||
|
|
||
| OpenDir(); | ||
| // event handler | ||
| int signal_readers(int event, Event *e); | ||
|
|
||
| private: | ||
| mutable ts::bravo::shared_mutex _shared_mutex; | ||
|
|
||
| Queue<CacheVC, Link_CacheVC_opendir_link> _delayed_readers; | ||
| DLL<OpenDirEntry> _bucket[OPEN_DIR_BUCKETS]; | ||
| }; |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing documentation: The class documentation should be updated to explain the new locking mechanism. Specifically, document that OpenDir now uses its own reader-writer lock (_shared_mutex) instead of relying on the StripeSM mutex, which allows concurrent reads while writes are in progress. This is a significant architectural change that should be clearly documented for maintainability.
| // OpenDir API | ||
| int open_write(CacheVC *cont, int allow_if_writers, int max_writers); | ||
| int open_write_lock(CacheVC *cont, int allow_if_writers, int max_writers); | ||
| int close_write(CacheVC *cont); | ||
| OpenDirEntry *open_read(const CryptoHash *key) const; | ||
| int close_read(CacheVC *cont) const; | ||
|
|
||
| // PreservationTable API | ||
| int begin_read(CacheVC *cont) const; | ||
| int close_read(CacheVC *cont) const; |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent formatting: The comment "// OpenDir API" and "// PreservationTable API" use double slashes, but there are also methods without such grouping comments. Consider whether this level of API grouping is helpful, and if so, ensure it's applied consistently throughout the class. For example, other method groups could benefit from similar API comments.
| #ifdef DEBUG | ||
|
|
||
| DbgCtl dbg_ctl_cache_stats{"cache_stats"}; | ||
| DbgCtl dbg_ctl_cache_open_dir{"cache_open_dir"}; |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused debug control: The debug control variable dbg_ctl_cache_open_dir is defined but never used in the code. If this was added for future debugging purposes, consider adding a comment explaining its intended use. Otherwise, it should be removed to avoid clutter.
| DbgCtl dbg_ctl_cache_open_dir{"cache_open_dir"}; |
I found that if
OpenDirhas own reader-writer lock, it doesn't needStripeSM mutex. This means we can avoid the StripeSM mutex lock contention issue for reader-while-writer cases.Benchmarking RWW is a bit tricky but one of benchmark says max rps is improved 9.9% in below conditions.
Conditions:
Cache-Control: public, max-age=0///< some requests goes RWW pathResult:
part of #12788