Hold PeerState in an RwLock rather than a Mutex - #2968

Closed
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state
Closed

Hold PeerState in an RwLock rather than a Mutex#2968
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state

Conversation

@tnull

Copy link
Copy Markdown
Contributor

Previously, we would hold the PeerState in a Mutex, which disallows concurrent read-only operations. Here we switch to an RwLock making this possible.

To this end, we first switch all instances of Mutex::lock to RwLock::write, and then selectively adapt some usages permitting for it to RwLock::read.

@tnull
tnull marked this pull request as draft March 26, 2024 08:32
@tnulltnull changed the title Hold PeerState in an RwLock rather than a MutexHold PeerState in an RwLock rather than a MutexMar 26, 2024
@tnull
tnull marked this pull request as ready for review March 26, 2024 14:47
@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 5c6cf90 to 4f4fa58CompareMarch 26, 2024 14:55

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 4f4fa58 to 3658581CompareMarch 27, 2024 10:20
@codecov-commenter

codecov-commenter commented Mar 27, 2024

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 95.34884% with 8 lines in your changes are missing coverage. Please review.

Project coverage is 89.37%. Comparing base (1d9e541) to head (cf2d418).

FilesPatch %Lines
lightning/src/ln/channelmanager.rs95.13%1 Missing and 6 partials ⚠️
lightning/src/ln/functional_test_utils.rs83.33%1 Missing ⚠️

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2968 +/- ##
==========================================
+ Coverage 89.31% 89.37% +0.05% 
==========================================
Files 117 117 Lines 95618 95618 Branches 95618 95618 ==========================================
+ Hits 85404 85456 +52 + Misses 7974 7926 -48 + Partials 2240 2236 -4 

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@tnull

Copy link
Copy Markdown
ContributorAuthor

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

Right, the users who reported this should def. investigate why it was blocking for so long. I now went through all usages and found several places where we could also just us a RwLockReadGuard (forward_intercepted_htlcs, compute_inflight_htlcs, get_relevant_txids), a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

IMO, the other cases make this a worthwhile improvement, independently of the concrete reported issue.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

Writing the manager basically halts the world no matter what due to the consistency write lock.

I'm not entirely convinced by the remaining changes that they're worth it.

@tnull

tnull commented Mar 27, 2024

Copy link
Copy Markdown
ContributorAuthor

Writing the manager basically halts the world no matter what due to the consistency write lock.

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

@tnull

tnull commented Mar 28, 2024

Copy link
Copy Markdown
ContributorAuthor

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

So, IIUC, we can have during access during parts of the serialization, but not for the full write call?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

Ah, okay, the doc's explanation around 'different branches' had me confused whether we're allowed to skip the root or not. Logically it makes sense, the docs aren't fully clear on that though, IMO.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Yea, I guess I was figuring that loop would be pretty quick, but I guess it does write all the channels so its not free, even if pretty quick.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

Yea, its...delicate. IIRC this is the only place we actually take per-peer locks recursively, so the total_consistency_guard saves us, but of course it is a little less fragile to keep relying on the per_peer write lock for that.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 3658581 to cf2d418CompareApril 8, 2024 09:46
@tnull

tnull commented Apr 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Rebased on main to resolve minor conflicts.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not entirely convinced this improves parallelism in any practical cases, honestly.

let mut peer_state_lock = peer_state_rwlock.write().unwrap();
let peer_state = &mut *peer_state_lock;
let peer_state_lock = peer_state_rwlock.read().unwrap();
let peer_state = &*peer_state_lock;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a race condition here where we decide whether or not to include the channel at this step but then the channel gets updated and we write the wrong number of channels? Doesn't seem like it was introduced in this PR but still worth fixing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I noticed that as well and created a TODO for it but so far intended to fix it elsewhere. Looking at it again it seems that this could be fixed by a) only iterating the peer states once and persisting to a temporary buffer while keeping track of serializable_peer_count and number_of_funded_channels or b) taking the per_peer_state.write() lock earlier and holding it for the remainder of the method.

Seems a) is rather inefficient and b) would mean dropping any chance of concurrent access during at least parts of write. I think switching to an RwLock might still be nice in general and going forward, but if you don't agree, possibly we should just do b) in a separate PR and close this one?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

@tnull

Copy link
Copy Markdown
ContributorAuthor

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

Now opened to this effect: #2998

Closing this as the benefits of switching to RwLock are not immediately clear, and we can always do so if we find a compelling reason in the future.

@tnulltnull closed this Apr 17, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tnull@codecov-commenter@TheBlueMatt
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Hold PeerState in an RwLock rather than a Mutex - #2968

Closed
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state
Closed

Hold PeerState in an RwLock rather than a Mutex#2968
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state

Conversation

@tnull

Copy link
Copy Markdown
Contributor

Previously, we would hold the PeerState in a Mutex, which disallows concurrent read-only operations. Here we switch to an RwLock making this possible.

To this end, we first switch all instances of Mutex::lock to RwLock::write, and then selectively adapt some usages permitting for it to RwLock::read.

@tnull
tnull marked this pull request as draft March 26, 2024 08:32
@tnulltnull changed the title Hold PeerState in an RwLock rather than a MutexHold PeerState in an RwLock rather than a MutexMar 26, 2024
@tnull
tnull marked this pull request as ready for review March 26, 2024 14:47
@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 5c6cf90 to 4f4fa58CompareMarch 26, 2024 14:55

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 4f4fa58 to 3658581CompareMarch 27, 2024 10:20
@codecov-commenter

codecov-commenter commented Mar 27, 2024

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 95.34884% with 8 lines in your changes are missing coverage. Please review.

Project coverage is 89.37%. Comparing base (1d9e541) to head (cf2d418).

FilesPatch %Lines
lightning/src/ln/channelmanager.rs95.13%1 Missing and 6 partials ⚠️
lightning/src/ln/functional_test_utils.rs83.33%1 Missing ⚠️

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2968 +/- ##
==========================================
+ Coverage 89.31% 89.37% +0.05% 
==========================================
Files 117 117 Lines 95618 95618 Branches 95618 95618 ==========================================
+ Hits 85404 85456 +52 + Misses 7974 7926 -48 + Partials 2240 2236 -4 

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@tnull

Copy link
Copy Markdown
ContributorAuthor

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

Right, the users who reported this should def. investigate why it was blocking for so long. I now went through all usages and found several places where we could also just us a RwLockReadGuard (forward_intercepted_htlcs, compute_inflight_htlcs, get_relevant_txids), a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

IMO, the other cases make this a worthwhile improvement, independently of the concrete reported issue.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

Writing the manager basically halts the world no matter what due to the consistency write lock.

I'm not entirely convinced by the remaining changes that they're worth it.

@tnull

tnull commented Mar 27, 2024

Copy link
Copy Markdown
ContributorAuthor

Writing the manager basically halts the world no matter what due to the consistency write lock.

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

@tnull

tnull commented Mar 28, 2024

Copy link
Copy Markdown
ContributorAuthor

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

So, IIUC, we can have during access during parts of the serialization, but not for the full write call?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

Ah, okay, the doc's explanation around 'different branches' had me confused whether we're allowed to skip the root or not. Logically it makes sense, the docs aren't fully clear on that though, IMO.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Yea, I guess I was figuring that loop would be pretty quick, but I guess it does write all the channels so its not free, even if pretty quick.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

Yea, its...delicate. IIRC this is the only place we actually take per-peer locks recursively, so the total_consistency_guard saves us, but of course it is a little less fragile to keep relying on the per_peer write lock for that.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 3658581 to cf2d418CompareApril 8, 2024 09:46
@tnull

tnull commented Apr 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Rebased on main to resolve minor conflicts.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not entirely convinced this improves parallelism in any practical cases, honestly.

let mut peer_state_lock = peer_state_rwlock.write().unwrap();
let peer_state = &mut *peer_state_lock;
let peer_state_lock = peer_state_rwlock.read().unwrap();
let peer_state = &*peer_state_lock;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a race condition here where we decide whether or not to include the channel at this step but then the channel gets updated and we write the wrong number of channels? Doesn't seem like it was introduced in this PR but still worth fixing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I noticed that as well and created a TODO for it but so far intended to fix it elsewhere. Looking at it again it seems that this could be fixed by a) only iterating the peer states once and persisting to a temporary buffer while keeping track of serializable_peer_count and number_of_funded_channels or b) taking the per_peer_state.write() lock earlier and holding it for the remainder of the method.

Seems a) is rather inefficient and b) would mean dropping any chance of concurrent access during at least parts of write. I think switching to an RwLock might still be nice in general and going forward, but if you don't agree, possibly we should just do b) in a separate PR and close this one?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

@tnull

Copy link
Copy Markdown
ContributorAuthor

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

Now opened to this effect: #2998

Closing this as the benefits of switching to RwLock are not immediately clear, and we can always do so if we find a compelling reason in the future.

@tnulltnull closed this Apr 17, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tnull@codecov-commenter@TheBlueMatt
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Hold PeerState in an RwLock rather than a Mutex - #2968

Closed
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state
Closed

Hold PeerState in an RwLock rather than a Mutex#2968
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state

Conversation

@tnull

Copy link
Copy Markdown
Contributor

Previously, we would hold the PeerState in a Mutex, which disallows concurrent read-only operations. Here we switch to an RwLock making this possible.

To this end, we first switch all instances of Mutex::lock to RwLock::write, and then selectively adapt some usages permitting for it to RwLock::read.

@tnull
tnull marked this pull request as draft March 26, 2024 08:32
@tnulltnull changed the title Hold PeerState in an RwLock rather than a MutexHold PeerState in an RwLock rather than a MutexMar 26, 2024
@tnull
tnull marked this pull request as ready for review March 26, 2024 14:47
@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 5c6cf90 to 4f4fa58CompareMarch 26, 2024 14:55

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 4f4fa58 to 3658581CompareMarch 27, 2024 10:20
@codecov-commenter

codecov-commenter commented Mar 27, 2024

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 95.34884% with 8 lines in your changes are missing coverage. Please review.

Project coverage is 89.37%. Comparing base (1d9e541) to head (cf2d418).

FilesPatch %Lines
lightning/src/ln/channelmanager.rs95.13%1 Missing and 6 partials ⚠️
lightning/src/ln/functional_test_utils.rs83.33%1 Missing ⚠️

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2968 +/- ##
==========================================
+ Coverage 89.31% 89.37% +0.05% 
==========================================
Files 117 117 Lines 95618 95618 Branches 95618 95618 ==========================================
+ Hits 85404 85456 +52 + Misses 7974 7926 -48 + Partials 2240 2236 -4 

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@tnull

Copy link
Copy Markdown
ContributorAuthor

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

Right, the users who reported this should def. investigate why it was blocking for so long. I now went through all usages and found several places where we could also just us a RwLockReadGuard (forward_intercepted_htlcs, compute_inflight_htlcs, get_relevant_txids), a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

IMO, the other cases make this a worthwhile improvement, independently of the concrete reported issue.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

Writing the manager basically halts the world no matter what due to the consistency write lock.

I'm not entirely convinced by the remaining changes that they're worth it.

@tnull

tnull commented Mar 27, 2024

Copy link
Copy Markdown
ContributorAuthor

Writing the manager basically halts the world no matter what due to the consistency write lock.

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

@tnull

tnull commented Mar 28, 2024

Copy link
Copy Markdown
ContributorAuthor

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

So, IIUC, we can have during access during parts of the serialization, but not for the full write call?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

Ah, okay, the doc's explanation around 'different branches' had me confused whether we're allowed to skip the root or not. Logically it makes sense, the docs aren't fully clear on that though, IMO.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Yea, I guess I was figuring that loop would be pretty quick, but I guess it does write all the channels so its not free, even if pretty quick.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

Yea, its...delicate. IIRC this is the only place we actually take per-peer locks recursively, so the total_consistency_guard saves us, but of course it is a little less fragile to keep relying on the per_peer write lock for that.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 3658581 to cf2d418CompareApril 8, 2024 09:46
@tnull

tnull commented Apr 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Rebased on main to resolve minor conflicts.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not entirely convinced this improves parallelism in any practical cases, honestly.

let mut peer_state_lock = peer_state_rwlock.write().unwrap();
let peer_state = &mut *peer_state_lock;
let peer_state_lock = peer_state_rwlock.read().unwrap();
let peer_state = &*peer_state_lock;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a race condition here where we decide whether or not to include the channel at this step but then the channel gets updated and we write the wrong number of channels? Doesn't seem like it was introduced in this PR but still worth fixing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I noticed that as well and created a TODO for it but so far intended to fix it elsewhere. Looking at it again it seems that this could be fixed by a) only iterating the peer states once and persisting to a temporary buffer while keeping track of serializable_peer_count and number_of_funded_channels or b) taking the per_peer_state.write() lock earlier and holding it for the remainder of the method.

Seems a) is rather inefficient and b) would mean dropping any chance of concurrent access during at least parts of write. I think switching to an RwLock might still be nice in general and going forward, but if you don't agree, possibly we should just do b) in a separate PR and close this one?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

@tnull

Copy link
Copy Markdown
ContributorAuthor

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

Now opened to this effect: #2998

Closing this as the benefits of switching to RwLock are not immediately clear, and we can always do so if we find a compelling reason in the future.

@tnulltnull closed this Apr 17, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tnull@codecov-commenter@TheBlueMatt
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Hold PeerState in an RwLock rather than a Mutex - #2968

Closed
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state
Closed

Hold PeerState in an RwLock rather than a Mutex#2968
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state

Conversation

@tnull

Copy link
Copy Markdown
Contributor

Previously, we would hold the PeerState in a Mutex, which disallows concurrent read-only operations. Here we switch to an RwLock making this possible.

To this end, we first switch all instances of Mutex::lock to RwLock::write, and then selectively adapt some usages permitting for it to RwLock::read.

@tnull
tnull marked this pull request as draft March 26, 2024 08:32
@tnulltnull changed the title Hold PeerState in an RwLock rather than a MutexHold PeerState in an RwLock rather than a MutexMar 26, 2024
@tnull
tnull marked this pull request as ready for review March 26, 2024 14:47
@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 5c6cf90 to 4f4fa58CompareMarch 26, 2024 14:55

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 4f4fa58 to 3658581CompareMarch 27, 2024 10:20
@codecov-commenter

codecov-commenter commented Mar 27, 2024

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 95.34884% with 8 lines in your changes are missing coverage. Please review.

Project coverage is 89.37%. Comparing base (1d9e541) to head (cf2d418).

FilesPatch %Lines
lightning/src/ln/channelmanager.rs95.13%1 Missing and 6 partials ⚠️
lightning/src/ln/functional_test_utils.rs83.33%1 Missing ⚠️

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2968 +/- ##
==========================================
+ Coverage 89.31% 89.37% +0.05% 
==========================================
Files 117 117 Lines 95618 95618 Branches 95618 95618 ==========================================
+ Hits 85404 85456 +52 + Misses 7974 7926 -48 + Partials 2240 2236 -4 

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@tnull

Copy link
Copy Markdown
ContributorAuthor

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

Right, the users who reported this should def. investigate why it was blocking for so long. I now went through all usages and found several places where we could also just us a RwLockReadGuard (forward_intercepted_htlcs, compute_inflight_htlcs, get_relevant_txids), a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

IMO, the other cases make this a worthwhile improvement, independently of the concrete reported issue.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

Writing the manager basically halts the world no matter what due to the consistency write lock.

I'm not entirely convinced by the remaining changes that they're worth it.

@tnull

tnull commented Mar 27, 2024

Copy link
Copy Markdown
ContributorAuthor

Writing the manager basically halts the world no matter what due to the consistency write lock.

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

@tnull

tnull commented Mar 28, 2024

Copy link
Copy Markdown
ContributorAuthor

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

So, IIUC, we can have during access during parts of the serialization, but not for the full write call?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

Ah, okay, the doc's explanation around 'different branches' had me confused whether we're allowed to skip the root or not. Logically it makes sense, the docs aren't fully clear on that though, IMO.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Yea, I guess I was figuring that loop would be pretty quick, but I guess it does write all the channels so its not free, even if pretty quick.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

Yea, its...delicate. IIRC this is the only place we actually take per-peer locks recursively, so the total_consistency_guard saves us, but of course it is a little less fragile to keep relying on the per_peer write lock for that.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 3658581 to cf2d418CompareApril 8, 2024 09:46
@tnull

tnull commented Apr 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Rebased on main to resolve minor conflicts.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not entirely convinced this improves parallelism in any practical cases, honestly.

let mut peer_state_lock = peer_state_rwlock.write().unwrap();
let peer_state = &mut *peer_state_lock;
let peer_state_lock = peer_state_rwlock.read().unwrap();
let peer_state = &*peer_state_lock;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a race condition here where we decide whether or not to include the channel at this step but then the channel gets updated and we write the wrong number of channels? Doesn't seem like it was introduced in this PR but still worth fixing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I noticed that as well and created a TODO for it but so far intended to fix it elsewhere. Looking at it again it seems that this could be fixed by a) only iterating the peer states once and persisting to a temporary buffer while keeping track of serializable_peer_count and number_of_funded_channels or b) taking the per_peer_state.write() lock earlier and holding it for the remainder of the method.

Seems a) is rather inefficient and b) would mean dropping any chance of concurrent access during at least parts of write. I think switching to an RwLock might still be nice in general and going forward, but if you don't agree, possibly we should just do b) in a separate PR and close this one?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

@tnull

Copy link
Copy Markdown
ContributorAuthor

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

Now opened to this effect: #2998

Closing this as the benefits of switching to RwLock are not immediately clear, and we can always do so if we find a compelling reason in the future.

@tnulltnull closed this Apr 17, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tnull@codecov-commenter@TheBlueMatt
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Hold PeerState in an RwLock rather than a Mutex - #2968

Closed
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state
Closed

Hold PeerState in an RwLock rather than a Mutex#2968
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state

Conversation

@tnull

Copy link
Copy Markdown
Contributor

Previously, we would hold the PeerState in a Mutex, which disallows concurrent read-only operations. Here we switch to an RwLock making this possible.

To this end, we first switch all instances of Mutex::lock to RwLock::write, and then selectively adapt some usages permitting for it to RwLock::read.

@tnull
tnull marked this pull request as draft March 26, 2024 08:32
@tnulltnull changed the title Hold PeerState in an RwLock rather than a MutexHold PeerState in an RwLock rather than a MutexMar 26, 2024
@tnull
tnull marked this pull request as ready for review March 26, 2024 14:47
@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 5c6cf90 to 4f4fa58CompareMarch 26, 2024 14:55

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 4f4fa58 to 3658581CompareMarch 27, 2024 10:20
@codecov-commenter

codecov-commenter commented Mar 27, 2024

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 95.34884% with 8 lines in your changes are missing coverage. Please review.

Project coverage is 89.37%. Comparing base (1d9e541) to head (cf2d418).

FilesPatch %Lines
lightning/src/ln/channelmanager.rs95.13%1 Missing and 6 partials ⚠️
lightning/src/ln/functional_test_utils.rs83.33%1 Missing ⚠️

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2968 +/- ##
==========================================
+ Coverage 89.31% 89.37% +0.05% 
==========================================
Files 117 117 Lines 95618 95618 Branches 95618 95618 ==========================================
+ Hits 85404 85456 +52 + Misses 7974 7926 -48 + Partials 2240 2236 -4 

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@tnull

Copy link
Copy Markdown
ContributorAuthor

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

Right, the users who reported this should def. investigate why it was blocking for so long. I now went through all usages and found several places where we could also just us a RwLockReadGuard (forward_intercepted_htlcs, compute_inflight_htlcs, get_relevant_txids), a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

IMO, the other cases make this a worthwhile improvement, independently of the concrete reported issue.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

Writing the manager basically halts the world no matter what due to the consistency write lock.

I'm not entirely convinced by the remaining changes that they're worth it.

@tnull

tnull commented Mar 27, 2024

Copy link
Copy Markdown
ContributorAuthor

Writing the manager basically halts the world no matter what due to the consistency write lock.

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

@tnull

tnull commented Mar 28, 2024

Copy link
Copy Markdown
ContributorAuthor

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

So, IIUC, we can have during access during parts of the serialization, but not for the full write call?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

Ah, okay, the doc's explanation around 'different branches' had me confused whether we're allowed to skip the root or not. Logically it makes sense, the docs aren't fully clear on that though, IMO.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Yea, I guess I was figuring that loop would be pretty quick, but I guess it does write all the channels so its not free, even if pretty quick.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

Yea, its...delicate. IIRC this is the only place we actually take per-peer locks recursively, so the total_consistency_guard saves us, but of course it is a little less fragile to keep relying on the per_peer write lock for that.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 3658581 to cf2d418CompareApril 8, 2024 09:46
@tnull

tnull commented Apr 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Rebased on main to resolve minor conflicts.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not entirely convinced this improves parallelism in any practical cases, honestly.

let mut peer_state_lock = peer_state_rwlock.write().unwrap();
let peer_state = &mut *peer_state_lock;
let peer_state_lock = peer_state_rwlock.read().unwrap();
let peer_state = &*peer_state_lock;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a race condition here where we decide whether or not to include the channel at this step but then the channel gets updated and we write the wrong number of channels? Doesn't seem like it was introduced in this PR but still worth fixing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I noticed that as well and created a TODO for it but so far intended to fix it elsewhere. Looking at it again it seems that this could be fixed by a) only iterating the peer states once and persisting to a temporary buffer while keeping track of serializable_peer_count and number_of_funded_channels or b) taking the per_peer_state.write() lock earlier and holding it for the remainder of the method.

Seems a) is rather inefficient and b) would mean dropping any chance of concurrent access during at least parts of write. I think switching to an RwLock might still be nice in general and going forward, but if you don't agree, possibly we should just do b) in a separate PR and close this one?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

@tnull

Copy link
Copy Markdown
ContributorAuthor

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

Now opened to this effect: #2998

Closing this as the benefits of switching to RwLock are not immediately clear, and we can always do so if we find a compelling reason in the future.

@tnulltnull closed this Apr 17, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tnull@codecov-commenter@TheBlueMatt
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Hold PeerState in an RwLock rather than a Mutex - #2968

Closed
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state
Closed

Hold PeerState in an RwLock rather than a Mutex#2968
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state

Conversation

@tnull

Copy link
Copy Markdown
Contributor

Previously, we would hold the PeerState in a Mutex, which disallows concurrent read-only operations. Here we switch to an RwLock making this possible.

To this end, we first switch all instances of Mutex::lock to RwLock::write, and then selectively adapt some usages permitting for it to RwLock::read.

@tnull
tnull marked this pull request as draft March 26, 2024 08:32
@tnulltnull changed the title Hold PeerState in an RwLock rather than a MutexHold PeerState in an RwLock rather than a MutexMar 26, 2024
@tnull
tnull marked this pull request as ready for review March 26, 2024 14:47
@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 5c6cf90 to 4f4fa58CompareMarch 26, 2024 14:55

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 4f4fa58 to 3658581CompareMarch 27, 2024 10:20
@codecov-commenter

codecov-commenter commented Mar 27, 2024

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 95.34884% with 8 lines in your changes are missing coverage. Please review.

Project coverage is 89.37%. Comparing base (1d9e541) to head (cf2d418).

FilesPatch %Lines
lightning/src/ln/channelmanager.rs95.13%1 Missing and 6 partials ⚠️
lightning/src/ln/functional_test_utils.rs83.33%1 Missing ⚠️

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2968 +/- ##
==========================================
+ Coverage 89.31% 89.37% +0.05% 
==========================================
Files 117 117 Lines 95618 95618 Branches 95618 95618 ==========================================
+ Hits 85404 85456 +52 + Misses 7974 7926 -48 + Partials 2240 2236 -4 

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@tnull

Copy link
Copy Markdown
ContributorAuthor

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

Right, the users who reported this should def. investigate why it was blocking for so long. I now went through all usages and found several places where we could also just us a RwLockReadGuard (forward_intercepted_htlcs, compute_inflight_htlcs, get_relevant_txids), a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

IMO, the other cases make this a worthwhile improvement, independently of the concrete reported issue.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

Writing the manager basically halts the world no matter what due to the consistency write lock.

I'm not entirely convinced by the remaining changes that they're worth it.

@tnull

tnull commented Mar 27, 2024

Copy link
Copy Markdown
ContributorAuthor

Writing the manager basically halts the world no matter what due to the consistency write lock.

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

@tnull

tnull commented Mar 28, 2024

Copy link
Copy Markdown
ContributorAuthor

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

So, IIUC, we can have during access during parts of the serialization, but not for the full write call?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

Ah, okay, the doc's explanation around 'different branches' had me confused whether we're allowed to skip the root or not. Logically it makes sense, the docs aren't fully clear on that though, IMO.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Yea, I guess I was figuring that loop would be pretty quick, but I guess it does write all the channels so its not free, even if pretty quick.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

Yea, its...delicate. IIRC this is the only place we actually take per-peer locks recursively, so the total_consistency_guard saves us, but of course it is a little less fragile to keep relying on the per_peer write lock for that.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 3658581 to cf2d418CompareApril 8, 2024 09:46
@tnull

tnull commented Apr 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Rebased on main to resolve minor conflicts.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not entirely convinced this improves parallelism in any practical cases, honestly.

let mut peer_state_lock = peer_state_rwlock.write().unwrap();
let peer_state = &mut *peer_state_lock;
let peer_state_lock = peer_state_rwlock.read().unwrap();
let peer_state = &*peer_state_lock;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a race condition here where we decide whether or not to include the channel at this step but then the channel gets updated and we write the wrong number of channels? Doesn't seem like it was introduced in this PR but still worth fixing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I noticed that as well and created a TODO for it but so far intended to fix it elsewhere. Looking at it again it seems that this could be fixed by a) only iterating the peer states once and persisting to a temporary buffer while keeping track of serializable_peer_count and number_of_funded_channels or b) taking the per_peer_state.write() lock earlier and holding it for the remainder of the method.

Seems a) is rather inefficient and b) would mean dropping any chance of concurrent access during at least parts of write. I think switching to an RwLock might still be nice in general and going forward, but if you don't agree, possibly we should just do b) in a separate PR and close this one?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

@tnull

Copy link
Copy Markdown
ContributorAuthor

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

Now opened to this effect: #2998

Closing this as the benefits of switching to RwLock are not immediately clear, and we can always do so if we find a compelling reason in the future.

@tnulltnull closed this Apr 17, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tnull@codecov-commenter@TheBlueMatt
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Hold PeerState in an RwLock rather than a Mutex - #2968

Closed
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state
Closed

Hold PeerState in an RwLock rather than a Mutex#2968
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state

Conversation

@tnull

Copy link
Copy Markdown
Contributor

Previously, we would hold the PeerState in a Mutex, which disallows concurrent read-only operations. Here we switch to an RwLock making this possible.

To this end, we first switch all instances of Mutex::lock to RwLock::write, and then selectively adapt some usages permitting for it to RwLock::read.

@tnull
tnull marked this pull request as draft March 26, 2024 08:32
@tnulltnull changed the title Hold PeerState in an RwLock rather than a MutexHold PeerState in an RwLock rather than a MutexMar 26, 2024
@tnull
tnull marked this pull request as ready for review March 26, 2024 14:47
@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 5c6cf90 to 4f4fa58CompareMarch 26, 2024 14:55

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 4f4fa58 to 3658581CompareMarch 27, 2024 10:20
@codecov-commenter

codecov-commenter commented Mar 27, 2024

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 95.34884% with 8 lines in your changes are missing coverage. Please review.

Project coverage is 89.37%. Comparing base (1d9e541) to head (cf2d418).

FilesPatch %Lines
lightning/src/ln/channelmanager.rs95.13%1 Missing and 6 partials ⚠️
lightning/src/ln/functional_test_utils.rs83.33%1 Missing ⚠️

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2968 +/- ##
==========================================
+ Coverage 89.31% 89.37% +0.05% 
==========================================
Files 117 117 Lines 95618 95618 Branches 95618 95618 ==========================================
+ Hits 85404 85456 +52 + Misses 7974 7926 -48 + Partials 2240 2236 -4 

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@tnull

Copy link
Copy Markdown
ContributorAuthor

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

Right, the users who reported this should def. investigate why it was blocking for so long. I now went through all usages and found several places where we could also just us a RwLockReadGuard (forward_intercepted_htlcs, compute_inflight_htlcs, get_relevant_txids), a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

IMO, the other cases make this a worthwhile improvement, independently of the concrete reported issue.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

Writing the manager basically halts the world no matter what due to the consistency write lock.

I'm not entirely convinced by the remaining changes that they're worth it.

@tnull

tnull commented Mar 27, 2024

Copy link
Copy Markdown
ContributorAuthor

Writing the manager basically halts the world no matter what due to the consistency write lock.

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

@tnull

tnull commented Mar 28, 2024

Copy link
Copy Markdown
ContributorAuthor

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

So, IIUC, we can have during access during parts of the serialization, but not for the full write call?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

Ah, okay, the doc's explanation around 'different branches' had me confused whether we're allowed to skip the root or not. Logically it makes sense, the docs aren't fully clear on that though, IMO.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Yea, I guess I was figuring that loop would be pretty quick, but I guess it does write all the channels so its not free, even if pretty quick.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

Yea, its...delicate. IIRC this is the only place we actually take per-peer locks recursively, so the total_consistency_guard saves us, but of course it is a little less fragile to keep relying on the per_peer write lock for that.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 3658581 to cf2d418CompareApril 8, 2024 09:46
@tnull

tnull commented Apr 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Rebased on main to resolve minor conflicts.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not entirely convinced this improves parallelism in any practical cases, honestly.

let mut peer_state_lock = peer_state_rwlock.write().unwrap();
let peer_state = &mut *peer_state_lock;
let peer_state_lock = peer_state_rwlock.read().unwrap();
let peer_state = &*peer_state_lock;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a race condition here where we decide whether or not to include the channel at this step but then the channel gets updated and we write the wrong number of channels? Doesn't seem like it was introduced in this PR but still worth fixing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I noticed that as well and created a TODO for it but so far intended to fix it elsewhere. Looking at it again it seems that this could be fixed by a) only iterating the peer states once and persisting to a temporary buffer while keeping track of serializable_peer_count and number_of_funded_channels or b) taking the per_peer_state.write() lock earlier and holding it for the remainder of the method.

Seems a) is rather inefficient and b) would mean dropping any chance of concurrent access during at least parts of write. I think switching to an RwLock might still be nice in general and going forward, but if you don't agree, possibly we should just do b) in a separate PR and close this one?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

@tnull

Copy link
Copy Markdown
ContributorAuthor

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

Now opened to this effect: #2998

Closing this as the benefits of switching to RwLock are not immediately clear, and we can always do so if we find a compelling reason in the future.

@tnulltnull closed this Apr 17, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tnull@codecov-commenter@TheBlueMatt
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Hold PeerState in an RwLock rather than a Mutex - #2968

Closed
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state
Closed

Hold PeerState in an RwLock rather than a Mutex#2968
tnull wants to merge 6 commits into
lightningdevkit:mainfrom
tnull:2024-03-rwlock-peer-state

Conversation

@tnull

Copy link
Copy Markdown
Contributor

Previously, we would hold the PeerState in a Mutex, which disallows concurrent read-only operations. Here we switch to an RwLock making this possible.

To this end, we first switch all instances of Mutex::lock to RwLock::write, and then selectively adapt some usages permitting for it to RwLock::read.

@tnull
tnull marked this pull request as draft March 26, 2024 08:32
@tnulltnull changed the title Hold PeerState in an RwLock rather than a MutexHold PeerState in an RwLock rather than a MutexMar 26, 2024
@tnull
tnull marked this pull request as ready for review March 26, 2024 14:47
@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 5c6cf90 to 4f4fa58CompareMarch 26, 2024 14:55

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 4f4fa58 to 3658581CompareMarch 27, 2024 10:20
@codecov-commenter

codecov-commenter commented Mar 27, 2024

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 95.34884% with 8 lines in your changes are missing coverage. Please review.

Project coverage is 89.37%. Comparing base (1d9e541) to head (cf2d418).

FilesPatch %Lines
lightning/src/ln/channelmanager.rs95.13%1 Missing and 6 partials ⚠️
lightning/src/ln/functional_test_utils.rs83.33%1 Missing ⚠️

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2968 +/- ##
==========================================
+ Coverage 89.31% 89.37% +0.05% 
==========================================
Files 117 117 Lines 95618 95618 Branches 95618 95618 ==========================================
+ Hits 85404 85456 +52 + Misses 7974 7926 -48 + Partials 2240 2236 -4 

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@tnull

Copy link
Copy Markdown
ContributorAuthor

I'd like to better understand the reported use for this - list_channels, specifically, should be plenty fast enough that the time under a single lock is ~instant and this doesn't change much of anything. If there's some other places where we can be read-only for longer (eg I/O) then I'd say lets do it.

Right, the users who reported this should def. investigate why it was blocking for so long. I now went through all usages and found several places where we could also just us a RwLockReadGuard (forward_intercepted_htlcs, compute_inflight_htlcs, get_relevant_txids), a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

IMO, the other cases make this a worthwhile improvement, independently of the concrete reported issue.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

a particularly promising case being taking a RwLockReadGuard in ChannelManager::write, during channel persistence.

Writing the manager basically halts the world no matter what due to the consistency write lock.

I'm not entirely convinced by the remaining changes that they're worth it.

@tnull

tnull commented Mar 27, 2024

Copy link
Copy Markdown
ContributorAuthor

Writing the manager basically halts the world no matter what due to the consistency write lock.

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Yes, for many operations, but we don't acquire total_consistency_lock in a lot of places, e.g. list_channels and get_relevant_txids. So they could still be executed concurrently during a ChannelManager::write operation (at least during persisting channels, at some point we then acquire the write lock on per_peer_state).

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

What am I missing? Is not acquiring it a lock order violation we need to fix? The docs seem to indicate so, but I'm not sure taking total_consistency_lock for each list_channels makes a lot of sense?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

@tnull

tnull commented Mar 28, 2024

Copy link
Copy Markdown
ContributorAuthor

Ah, fair point, duh. Note that write currently takes the top-level per_peer_statewrite lock, but that's an easy swap in this PR.

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

So, IIUC, we can have during access during parts of the serialization, but not for the full write call?

No, there's no lockorder violation if we skip a lock, only if they're in the wrong order.

Ah, okay, the doc's explanation around 'different branches' had me confused whether we're allowed to skip the root or not. Logically it makes sense, the docs aren't fully clear on that though, IMO.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

I don't think so? During channel data serialization we take the read lock here, which would allow concurrent access with the changes in this PR.

Yea, I guess I was figuring that loop would be pretty quick, but I guess it does write all the channels so its not free, even if pretty quick.

Then later we take the write lock here, but this comment has me believe we can't change that to assert we'd never violate lock order?

Yea, its...delicate. IIRC this is the only place we actually take per-peer locks recursively, so the total_consistency_guard saves us, but of course it is a little less fragile to keep relying on the per_peer write lock for that.

@tnull
tnullforce-pushed the 2024-03-rwlock-peer-state branch from 3658581 to cf2d418CompareApril 8, 2024 09:46
@tnull

tnull commented Apr 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Rebased on main to resolve minor conflicts.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not entirely convinced this improves parallelism in any practical cases, honestly.

let mut peer_state_lock = peer_state_rwlock.write().unwrap();
let peer_state = &mut *peer_state_lock;
let peer_state_lock = peer_state_rwlock.read().unwrap();
let peer_state = &*peer_state_lock;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a race condition here where we decide whether or not to include the channel at this step but then the channel gets updated and we write the wrong number of channels? Doesn't seem like it was introduced in this PR but still worth fixing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I noticed that as well and created a TODO for it but so far intended to fix it elsewhere. Looking at it again it seems that this could be fixed by a) only iterating the peer states once and persisting to a temporary buffer while keeping track of serializable_peer_count and number_of_funded_channels or b) taking the per_peer_state.write() lock earlier and holding it for the remainder of the method.

Seems a) is rather inefficient and b) would mean dropping any chance of concurrent access during at least parts of write. I think switching to an RwLock might still be nice in general and going forward, but if you don't agree, possibly we should just do b) in a separate PR and close this one?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

@tnull

Copy link
Copy Markdown
ContributorAuthor

IIRC we discussed offline and concluded to abandon this in favor of fixing the issue at #2968 (comment) by holding the top-level write lock for longer. Leaving this open until we move that to an issue or open the followup pr.

Now opened to this effect: #2998

Closing this as the benefits of switching to RwLock are not immediately clear, and we can always do so if we find a compelling reason in the future.

@tnulltnull closed this Apr 17, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tnull@codecov-commenter@TheBlueMatt