Move closed channels tracking to LightningRepo - #474

Merged
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels
Nov 20, 2025
Merged

Move closed channels tracking to LightningRepo#474
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels

Conversation

@ben-kaufman

Copy link
Copy Markdown
Contributor

Addresses #472 (review)

@claude

claudeBot commented Nov 20, 2025

Copy link
Copy Markdown
Contributor

Code Review - PR #474: Move closed channels tracking to LightningRepo

Overview

This PR refactors closed channel tracking functionality from LightningService to LightningRepo, which improves separation of concerns. The service layer should handle low-level LDK operations, while the repository layer manages business logic and state.


✅ Strengths

  1. Better Separation of Concerns: Moving closed channel tracking to the repository layer is architecturally sound. LightningService should focus on LDK node operations, while LightningRepo handles business logic.

  2. Consistent Coroutine Usage: The refactored code properly uses withContext(bgDispatcher) instead of mixing ServiceQueue.LDK.background and ServiceQueue.CORE.background, which simplifies the concurrency model.

  3. Proper Error Handling: The registerClosedChannel function includes comprehensive error handling with detailed logging for missing channel details and funding transactions.


🔍 Potential Issues & Recommendations

1. Thread Safety Concern - Channel Cache Updates

Severity: Medium

The channelCache operations are split between refreshChannelCache() and event handling:

privatesuspendfunrefreshChannelCache() = withContext(bgDispatcher) {
val channels = lightningService.channels ?:return@withContext
channels.forEach { channel ->
channelCache[channel.channelId] = channel // ← Update
}
}
privatefunhandleLdkEvent(event:Event) {
when (event) {
isEvent.ChannelClosed-> {
scope.launch {
registerClosedChannel(channelId, reason) // ← Read then remove
}
}
}
}

Issue: handleLdkEvent is called synchronously (not a suspend function), and launches coroutines on scope which may use a different dispatcher than bgDispatcher. This could cause race conditions between:

  • refreshChannelCache() updating the cache on bgDispatcher
  • registerClosedChannel() reading/removing from the cache on scope's dispatcher

Recommendation: Make handleLdkEvent a suspend function and ensure all cache operations happen on the same dispatcher:

privatesuspendfunhandleLdkEvent(event:Event) = withContext(bgDispatcher) {
when (event) {
isEvent.ChannelClosed-> {
registerClosedChannel(channelId, reason)
}
// ...
}
}

Then update the call sites to use scope.launch:

lightningService.listenForEvents(onEvent = { event ->
scope.launch {
handleLdkEvent(event)
}
eventHandler?.invoke(event)
})

2. Race Condition - Channel Closed Before Cache Population

Severity: Medium

In the start() method:

lightningService.start(timeout) { event ->
handleLdkEvent(event)
eventHandler?.invoke(event)
ldkNodeEventBus.emit(event)
}
// ...
refreshChannelCache() // ← Called AFTER start completes

Issue: If a ChannelClosed event fires during node startup (before refreshChannelCache() is called), the channel won't be in the cache, and you'll get an error log.

Recommendation: Call refreshChannelCache() before starting event processing, or handle the race condition more gracefully in registerClosedChannel().

3. Memory Leak Risk - Unbounded Cache Growth

Severity: Low-Medium

The channelCache stores all channels indefinitely. While closed channels are removed, there's no cleanup mechanism if:

  • refreshChannelCache() fails partway through
  • Channels become stale but aren't closed properly
  • The cache grows large over time with many channels

Recommendation: Consider adding:

  • A maximum cache size with LRU eviction
  • Periodic cleanup of stale entries
  • Clear documentation about cache lifecycle

4. Error Recovery - Missing Channel Details

Severity: Low

When a channel isn't found in the cache, the code logs an error and returns:

val channel = channelCache[channelId] ?:run {
Logger.error(
"Could not find channel details for closed channel: channelId=$channelId",
context =TAG
)
return@withContext
}

Issue: This silently fails to persist closed channel information, which might be important for user history.

Recommendation: Consider attempting to fetch channel details from lightningService.channels as a fallback before giving up, or store partial information if available.

5. Exception Handling Inconsistency

Severity: Low

In the original code (LightningService:758):

} catch (e:Exception) {

In the new code (LightningRepo:393):

} catch (e:Throwable) {

Note: Catching Throwable is more comprehensive but also catches Error subclasses (like OutOfMemoryError). This is generally acceptable in top-level handlers, but ensure this is intentional.


🔒 Security Considerations

No significant security concerns identified. The code:

  • ✅ Properly validates required fields (fundingTxo)
  • ✅ Uses safe string operations (take, orEmpty)
  • ✅ Logs errors without exposing sensitive data

⚡ Performance Considerations

  1. Positive: Removing ServiceQueue.LDK.background and ServiceQueue.CORE.background reduces thread context switching and queue overhead.

  2. Minor Concern: refreshChannelCache() iterates through all channels on every sync. For users with many channels, consider:

    • Only updating changed channels
    • Batching updates
    • Using a more efficient data structure if channel counts grow large

🧪 Test Coverage

Concern: No new tests were added for the refactored functionality.

Recommendation: Add unit tests for:

  • handleLdkEvent() with different event types
  • registerClosedChannel() success and failure cases
  • refreshChannelCache() behavior
  • Race condition scenarios

The existing LightningRepoTest.kt would be the appropriate place for these tests.


📋 Additional Notes

  1. Code Quality: The refactoring maintains consistent Kotlin idioms and follows the existing codebase patterns.

  2. Logging: Comprehensive logging is present, which will help with debugging.

  3. Documentation: Consider adding KDoc comments to handleLdkEvent() and registerClosedChannel() explaining their purpose and threading guarantees.


Summary

This is a solid architectural improvement. The main concerns are around thread safety and race conditions with the channel cache. I recommend addressing the concurrency issues before merging to avoid potential bugs in production.

Recommendation: Request changes to address the thread safety concerns in points #1 and #2.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tested:

  • Open and close a channel
  • Wipe and restore app -> check channels

@jvsena42
jvsena42 merged commit 14a29cc into masterNov 20, 2025
14 checks passed
@jvsena42
jvsena42 deleted the chore/move-closed-channels branch November 20, 2025 13:02
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.

2 participants

@ben-kaufman@jvsena42
, '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

Move closed channels tracking to LightningRepo - #474

Merged
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels
Nov 20, 2025
Merged

Move closed channels tracking to LightningRepo#474
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels

Conversation

@ben-kaufman

Copy link
Copy Markdown
Contributor

Addresses #472 (review)

@claude

claudeBot commented Nov 20, 2025

Copy link
Copy Markdown
Contributor

Code Review - PR #474: Move closed channels tracking to LightningRepo

Overview

This PR refactors closed channel tracking functionality from LightningService to LightningRepo, which improves separation of concerns. The service layer should handle low-level LDK operations, while the repository layer manages business logic and state.


✅ Strengths

  1. Better Separation of Concerns: Moving closed channel tracking to the repository layer is architecturally sound. LightningService should focus on LDK node operations, while LightningRepo handles business logic.

  2. Consistent Coroutine Usage: The refactored code properly uses withContext(bgDispatcher) instead of mixing ServiceQueue.LDK.background and ServiceQueue.CORE.background, which simplifies the concurrency model.

  3. Proper Error Handling: The registerClosedChannel function includes comprehensive error handling with detailed logging for missing channel details and funding transactions.


🔍 Potential Issues & Recommendations

1. Thread Safety Concern - Channel Cache Updates

Severity: Medium

The channelCache operations are split between refreshChannelCache() and event handling:

privatesuspendfunrefreshChannelCache() = withContext(bgDispatcher) {
val channels = lightningService.channels ?:return@withContext
channels.forEach { channel ->
channelCache[channel.channelId] = channel // ← Update
}
}
privatefunhandleLdkEvent(event:Event) {
when (event) {
isEvent.ChannelClosed-> {
scope.launch {
registerClosedChannel(channelId, reason) // ← Read then remove
}
}
}
}

Issue: handleLdkEvent is called synchronously (not a suspend function), and launches coroutines on scope which may use a different dispatcher than bgDispatcher. This could cause race conditions between:

  • refreshChannelCache() updating the cache on bgDispatcher
  • registerClosedChannel() reading/removing from the cache on scope's dispatcher

Recommendation: Make handleLdkEvent a suspend function and ensure all cache operations happen on the same dispatcher:

privatesuspendfunhandleLdkEvent(event:Event) = withContext(bgDispatcher) {
when (event) {
isEvent.ChannelClosed-> {
registerClosedChannel(channelId, reason)
}
// ...
}
}

Then update the call sites to use scope.launch:

lightningService.listenForEvents(onEvent = { event ->
scope.launch {
handleLdkEvent(event)
}
eventHandler?.invoke(event)
})

2. Race Condition - Channel Closed Before Cache Population

Severity: Medium

In the start() method:

lightningService.start(timeout) { event ->
handleLdkEvent(event)
eventHandler?.invoke(event)
ldkNodeEventBus.emit(event)
}
// ...
refreshChannelCache() // ← Called AFTER start completes

Issue: If a ChannelClosed event fires during node startup (before refreshChannelCache() is called), the channel won't be in the cache, and you'll get an error log.

Recommendation: Call refreshChannelCache() before starting event processing, or handle the race condition more gracefully in registerClosedChannel().

3. Memory Leak Risk - Unbounded Cache Growth

Severity: Low-Medium

The channelCache stores all channels indefinitely. While closed channels are removed, there's no cleanup mechanism if:

  • refreshChannelCache() fails partway through
  • Channels become stale but aren't closed properly
  • The cache grows large over time with many channels

Recommendation: Consider adding:

  • A maximum cache size with LRU eviction
  • Periodic cleanup of stale entries
  • Clear documentation about cache lifecycle

4. Error Recovery - Missing Channel Details

Severity: Low

When a channel isn't found in the cache, the code logs an error and returns:

val channel = channelCache[channelId] ?:run {
Logger.error(
"Could not find channel details for closed channel: channelId=$channelId",
context =TAG
)
return@withContext
}

Issue: This silently fails to persist closed channel information, which might be important for user history.

Recommendation: Consider attempting to fetch channel details from lightningService.channels as a fallback before giving up, or store partial information if available.

5. Exception Handling Inconsistency

Severity: Low

In the original code (LightningService:758):

} catch (e:Exception) {

In the new code (LightningRepo:393):

} catch (e:Throwable) {

Note: Catching Throwable is more comprehensive but also catches Error subclasses (like OutOfMemoryError). This is generally acceptable in top-level handlers, but ensure this is intentional.


🔒 Security Considerations

No significant security concerns identified. The code:

  • ✅ Properly validates required fields (fundingTxo)
  • ✅ Uses safe string operations (take, orEmpty)
  • ✅ Logs errors without exposing sensitive data

⚡ Performance Considerations

  1. Positive: Removing ServiceQueue.LDK.background and ServiceQueue.CORE.background reduces thread context switching and queue overhead.

  2. Minor Concern: refreshChannelCache() iterates through all channels on every sync. For users with many channels, consider:

    • Only updating changed channels
    • Batching updates
    • Using a more efficient data structure if channel counts grow large

🧪 Test Coverage

Concern: No new tests were added for the refactored functionality.

Recommendation: Add unit tests for:

  • handleLdkEvent() with different event types
  • registerClosedChannel() success and failure cases
  • refreshChannelCache() behavior
  • Race condition scenarios

The existing LightningRepoTest.kt would be the appropriate place for these tests.


📋 Additional Notes

  1. Code Quality: The refactoring maintains consistent Kotlin idioms and follows the existing codebase patterns.

  2. Logging: Comprehensive logging is present, which will help with debugging.

  3. Documentation: Consider adding KDoc comments to handleLdkEvent() and registerClosedChannel() explaining their purpose and threading guarantees.


Summary

This is a solid architectural improvement. The main concerns are around thread safety and race conditions with the channel cache. I recommend addressing the concurrency issues before merging to avoid potential bugs in production.

Recommendation: Request changes to address the thread safety concerns in points #1 and #2.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tested:

  • Open and close a channel
  • Wipe and restore app -> check channels

@jvsena42
jvsena42 merged commit 14a29cc into masterNov 20, 2025
14 checks passed
@jvsena42
jvsena42 deleted the chore/move-closed-channels branch November 20, 2025 13:02
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.

2 participants

@ben-kaufman@jvsena42
, '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

Move closed channels tracking to LightningRepo - #474

Merged
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels
Nov 20, 2025
Merged

Move closed channels tracking to LightningRepo#474
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels

Conversation

@ben-kaufman

Copy link
Copy Markdown
Contributor

Addresses #472 (review)

@claude

claudeBot commented Nov 20, 2025

Copy link
Copy Markdown
Contributor

Code Review - PR #474: Move closed channels tracking to LightningRepo

Overview

This PR refactors closed channel tracking functionality from LightningService to LightningRepo, which improves separation of concerns. The service layer should handle low-level LDK operations, while the repository layer manages business logic and state.


✅ Strengths

  1. Better Separation of Concerns: Moving closed channel tracking to the repository layer is architecturally sound. LightningService should focus on LDK node operations, while LightningRepo handles business logic.

  2. Consistent Coroutine Usage: The refactored code properly uses withContext(bgDispatcher) instead of mixing ServiceQueue.LDK.background and ServiceQueue.CORE.background, which simplifies the concurrency model.

  3. Proper Error Handling: The registerClosedChannel function includes comprehensive error handling with detailed logging for missing channel details and funding transactions.


🔍 Potential Issues & Recommendations

1. Thread Safety Concern - Channel Cache Updates

Severity: Medium

The channelCache operations are split between refreshChannelCache() and event handling:

privatesuspendfunrefreshChannelCache() = withContext(bgDispatcher) {
val channels = lightningService.channels ?:return@withContext
channels.forEach { channel ->
channelCache[channel.channelId] = channel // ← Update
}
}
privatefunhandleLdkEvent(event:Event) {
when (event) {
isEvent.ChannelClosed-> {
scope.launch {
registerClosedChannel(channelId, reason) // ← Read then remove
}
}
}
}

Issue: handleLdkEvent is called synchronously (not a suspend function), and launches coroutines on scope which may use a different dispatcher than bgDispatcher. This could cause race conditions between:

  • refreshChannelCache() updating the cache on bgDispatcher
  • registerClosedChannel() reading/removing from the cache on scope's dispatcher

Recommendation: Make handleLdkEvent a suspend function and ensure all cache operations happen on the same dispatcher:

privatesuspendfunhandleLdkEvent(event:Event) = withContext(bgDispatcher) {
when (event) {
isEvent.ChannelClosed-> {
registerClosedChannel(channelId, reason)
}
// ...
}
}

Then update the call sites to use scope.launch:

lightningService.listenForEvents(onEvent = { event ->
scope.launch {
handleLdkEvent(event)
}
eventHandler?.invoke(event)
})

2. Race Condition - Channel Closed Before Cache Population

Severity: Medium

In the start() method:

lightningService.start(timeout) { event ->
handleLdkEvent(event)
eventHandler?.invoke(event)
ldkNodeEventBus.emit(event)
}
// ...
refreshChannelCache() // ← Called AFTER start completes

Issue: If a ChannelClosed event fires during node startup (before refreshChannelCache() is called), the channel won't be in the cache, and you'll get an error log.

Recommendation: Call refreshChannelCache() before starting event processing, or handle the race condition more gracefully in registerClosedChannel().

3. Memory Leak Risk - Unbounded Cache Growth

Severity: Low-Medium

The channelCache stores all channels indefinitely. While closed channels are removed, there's no cleanup mechanism if:

  • refreshChannelCache() fails partway through
  • Channels become stale but aren't closed properly
  • The cache grows large over time with many channels

Recommendation: Consider adding:

  • A maximum cache size with LRU eviction
  • Periodic cleanup of stale entries
  • Clear documentation about cache lifecycle

4. Error Recovery - Missing Channel Details

Severity: Low

When a channel isn't found in the cache, the code logs an error and returns:

val channel = channelCache[channelId] ?:run {
Logger.error(
"Could not find channel details for closed channel: channelId=$channelId",
context =TAG
)
return@withContext
}

Issue: This silently fails to persist closed channel information, which might be important for user history.

Recommendation: Consider attempting to fetch channel details from lightningService.channels as a fallback before giving up, or store partial information if available.

5. Exception Handling Inconsistency

Severity: Low

In the original code (LightningService:758):

} catch (e:Exception) {

In the new code (LightningRepo:393):

} catch (e:Throwable) {

Note: Catching Throwable is more comprehensive but also catches Error subclasses (like OutOfMemoryError). This is generally acceptable in top-level handlers, but ensure this is intentional.


🔒 Security Considerations

No significant security concerns identified. The code:

  • ✅ Properly validates required fields (fundingTxo)
  • ✅ Uses safe string operations (take, orEmpty)
  • ✅ Logs errors without exposing sensitive data

⚡ Performance Considerations

  1. Positive: Removing ServiceQueue.LDK.background and ServiceQueue.CORE.background reduces thread context switching and queue overhead.

  2. Minor Concern: refreshChannelCache() iterates through all channels on every sync. For users with many channels, consider:

    • Only updating changed channels
    • Batching updates
    • Using a more efficient data structure if channel counts grow large

🧪 Test Coverage

Concern: No new tests were added for the refactored functionality.

Recommendation: Add unit tests for:

  • handleLdkEvent() with different event types
  • registerClosedChannel() success and failure cases
  • refreshChannelCache() behavior
  • Race condition scenarios

The existing LightningRepoTest.kt would be the appropriate place for these tests.


📋 Additional Notes

  1. Code Quality: The refactoring maintains consistent Kotlin idioms and follows the existing codebase patterns.

  2. Logging: Comprehensive logging is present, which will help with debugging.

  3. Documentation: Consider adding KDoc comments to handleLdkEvent() and registerClosedChannel() explaining their purpose and threading guarantees.


Summary

This is a solid architectural improvement. The main concerns are around thread safety and race conditions with the channel cache. I recommend addressing the concurrency issues before merging to avoid potential bugs in production.

Recommendation: Request changes to address the thread safety concerns in points #1 and #2.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tested:

  • Open and close a channel
  • Wipe and restore app -> check channels

@jvsena42
jvsena42 merged commit 14a29cc into masterNov 20, 2025
14 checks passed
@jvsena42
jvsena42 deleted the chore/move-closed-channels branch November 20, 2025 13:02
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.

2 participants

@ben-kaufman@jvsena42
, '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

Move closed channels tracking to LightningRepo - #474

Merged
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels
Nov 20, 2025
Merged

Move closed channels tracking to LightningRepo#474
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels

Conversation

@ben-kaufman

Copy link
Copy Markdown
Contributor

Addresses #472 (review)

@claude

claudeBot commented Nov 20, 2025

Copy link
Copy Markdown
Contributor

Code Review - PR #474: Move closed channels tracking to LightningRepo

Overview

This PR refactors closed channel tracking functionality from LightningService to LightningRepo, which improves separation of concerns. The service layer should handle low-level LDK operations, while the repository layer manages business logic and state.


✅ Strengths

  1. Better Separation of Concerns: Moving closed channel tracking to the repository layer is architecturally sound. LightningService should focus on LDK node operations, while LightningRepo handles business logic.

  2. Consistent Coroutine Usage: The refactored code properly uses withContext(bgDispatcher) instead of mixing ServiceQueue.LDK.background and ServiceQueue.CORE.background, which simplifies the concurrency model.

  3. Proper Error Handling: The registerClosedChannel function includes comprehensive error handling with detailed logging for missing channel details and funding transactions.


🔍 Potential Issues & Recommendations

1. Thread Safety Concern - Channel Cache Updates

Severity: Medium

The channelCache operations are split between refreshChannelCache() and event handling:

privatesuspendfunrefreshChannelCache() = withContext(bgDispatcher) {
val channels = lightningService.channels ?:return@withContext
channels.forEach { channel ->
channelCache[channel.channelId] = channel // ← Update
}
}
privatefunhandleLdkEvent(event:Event) {
when (event) {
isEvent.ChannelClosed-> {
scope.launch {
registerClosedChannel(channelId, reason) // ← Read then remove
}
}
}
}

Issue: handleLdkEvent is called synchronously (not a suspend function), and launches coroutines on scope which may use a different dispatcher than bgDispatcher. This could cause race conditions between:

  • refreshChannelCache() updating the cache on bgDispatcher
  • registerClosedChannel() reading/removing from the cache on scope's dispatcher

Recommendation: Make handleLdkEvent a suspend function and ensure all cache operations happen on the same dispatcher:

privatesuspendfunhandleLdkEvent(event:Event) = withContext(bgDispatcher) {
when (event) {
isEvent.ChannelClosed-> {
registerClosedChannel(channelId, reason)
}
// ...
}
}

Then update the call sites to use scope.launch:

lightningService.listenForEvents(onEvent = { event ->
scope.launch {
handleLdkEvent(event)
}
eventHandler?.invoke(event)
})

2. Race Condition - Channel Closed Before Cache Population

Severity: Medium

In the start() method:

lightningService.start(timeout) { event ->
handleLdkEvent(event)
eventHandler?.invoke(event)
ldkNodeEventBus.emit(event)
}
// ...
refreshChannelCache() // ← Called AFTER start completes

Issue: If a ChannelClosed event fires during node startup (before refreshChannelCache() is called), the channel won't be in the cache, and you'll get an error log.

Recommendation: Call refreshChannelCache() before starting event processing, or handle the race condition more gracefully in registerClosedChannel().

3. Memory Leak Risk - Unbounded Cache Growth

Severity: Low-Medium

The channelCache stores all channels indefinitely. While closed channels are removed, there's no cleanup mechanism if:

  • refreshChannelCache() fails partway through
  • Channels become stale but aren't closed properly
  • The cache grows large over time with many channels

Recommendation: Consider adding:

  • A maximum cache size with LRU eviction
  • Periodic cleanup of stale entries
  • Clear documentation about cache lifecycle

4. Error Recovery - Missing Channel Details

Severity: Low

When a channel isn't found in the cache, the code logs an error and returns:

val channel = channelCache[channelId] ?:run {
Logger.error(
"Could not find channel details for closed channel: channelId=$channelId",
context =TAG
)
return@withContext
}

Issue: This silently fails to persist closed channel information, which might be important for user history.

Recommendation: Consider attempting to fetch channel details from lightningService.channels as a fallback before giving up, or store partial information if available.

5. Exception Handling Inconsistency

Severity: Low

In the original code (LightningService:758):

} catch (e:Exception) {

In the new code (LightningRepo:393):

} catch (e:Throwable) {

Note: Catching Throwable is more comprehensive but also catches Error subclasses (like OutOfMemoryError). This is generally acceptable in top-level handlers, but ensure this is intentional.


🔒 Security Considerations

No significant security concerns identified. The code:

  • ✅ Properly validates required fields (fundingTxo)
  • ✅ Uses safe string operations (take, orEmpty)
  • ✅ Logs errors without exposing sensitive data

⚡ Performance Considerations

  1. Positive: Removing ServiceQueue.LDK.background and ServiceQueue.CORE.background reduces thread context switching and queue overhead.

  2. Minor Concern: refreshChannelCache() iterates through all channels on every sync. For users with many channels, consider:

    • Only updating changed channels
    • Batching updates
    • Using a more efficient data structure if channel counts grow large

🧪 Test Coverage

Concern: No new tests were added for the refactored functionality.

Recommendation: Add unit tests for:

  • handleLdkEvent() with different event types
  • registerClosedChannel() success and failure cases
  • refreshChannelCache() behavior
  • Race condition scenarios

The existing LightningRepoTest.kt would be the appropriate place for these tests.


📋 Additional Notes

  1. Code Quality: The refactoring maintains consistent Kotlin idioms and follows the existing codebase patterns.

  2. Logging: Comprehensive logging is present, which will help with debugging.

  3. Documentation: Consider adding KDoc comments to handleLdkEvent() and registerClosedChannel() explaining their purpose and threading guarantees.


Summary

This is a solid architectural improvement. The main concerns are around thread safety and race conditions with the channel cache. I recommend addressing the concurrency issues before merging to avoid potential bugs in production.

Recommendation: Request changes to address the thread safety concerns in points #1 and #2.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tested:

  • Open and close a channel
  • Wipe and restore app -> check channels

@jvsena42
jvsena42 merged commit 14a29cc into masterNov 20, 2025
14 checks passed
@jvsena42
jvsena42 deleted the chore/move-closed-channels branch November 20, 2025 13:02
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.

2 participants

@ben-kaufman@jvsena42
, '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

Move closed channels tracking to LightningRepo - #474

Merged
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels
Nov 20, 2025
Merged

Move closed channels tracking to LightningRepo#474
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels

Conversation

@ben-kaufman

Copy link
Copy Markdown
Contributor

Addresses #472 (review)

@claude

claudeBot commented Nov 20, 2025

Copy link
Copy Markdown
Contributor

Code Review - PR #474: Move closed channels tracking to LightningRepo

Overview

This PR refactors closed channel tracking functionality from LightningService to LightningRepo, which improves separation of concerns. The service layer should handle low-level LDK operations, while the repository layer manages business logic and state.


✅ Strengths

  1. Better Separation of Concerns: Moving closed channel tracking to the repository layer is architecturally sound. LightningService should focus on LDK node operations, while LightningRepo handles business logic.

  2. Consistent Coroutine Usage: The refactored code properly uses withContext(bgDispatcher) instead of mixing ServiceQueue.LDK.background and ServiceQueue.CORE.background, which simplifies the concurrency model.

  3. Proper Error Handling: The registerClosedChannel function includes comprehensive error handling with detailed logging for missing channel details and funding transactions.


🔍 Potential Issues & Recommendations

1. Thread Safety Concern - Channel Cache Updates

Severity: Medium

The channelCache operations are split between refreshChannelCache() and event handling:

privatesuspendfunrefreshChannelCache() = withContext(bgDispatcher) {
val channels = lightningService.channels ?:return@withContext
channels.forEach { channel ->
channelCache[channel.channelId] = channel // ← Update
}
}
privatefunhandleLdkEvent(event:Event) {
when (event) {
isEvent.ChannelClosed-> {
scope.launch {
registerClosedChannel(channelId, reason) // ← Read then remove
}
}
}
}

Issue: handleLdkEvent is called synchronously (not a suspend function), and launches coroutines on scope which may use a different dispatcher than bgDispatcher. This could cause race conditions between:

  • refreshChannelCache() updating the cache on bgDispatcher
  • registerClosedChannel() reading/removing from the cache on scope's dispatcher

Recommendation: Make handleLdkEvent a suspend function and ensure all cache operations happen on the same dispatcher:

privatesuspendfunhandleLdkEvent(event:Event) = withContext(bgDispatcher) {
when (event) {
isEvent.ChannelClosed-> {
registerClosedChannel(channelId, reason)
}
// ...
}
}

Then update the call sites to use scope.launch:

lightningService.listenForEvents(onEvent = { event ->
scope.launch {
handleLdkEvent(event)
}
eventHandler?.invoke(event)
})

2. Race Condition - Channel Closed Before Cache Population

Severity: Medium

In the start() method:

lightningService.start(timeout) { event ->
handleLdkEvent(event)
eventHandler?.invoke(event)
ldkNodeEventBus.emit(event)
}
// ...
refreshChannelCache() // ← Called AFTER start completes

Issue: If a ChannelClosed event fires during node startup (before refreshChannelCache() is called), the channel won't be in the cache, and you'll get an error log.

Recommendation: Call refreshChannelCache() before starting event processing, or handle the race condition more gracefully in registerClosedChannel().

3. Memory Leak Risk - Unbounded Cache Growth

Severity: Low-Medium

The channelCache stores all channels indefinitely. While closed channels are removed, there's no cleanup mechanism if:

  • refreshChannelCache() fails partway through
  • Channels become stale but aren't closed properly
  • The cache grows large over time with many channels

Recommendation: Consider adding:

  • A maximum cache size with LRU eviction
  • Periodic cleanup of stale entries
  • Clear documentation about cache lifecycle

4. Error Recovery - Missing Channel Details

Severity: Low

When a channel isn't found in the cache, the code logs an error and returns:

val channel = channelCache[channelId] ?:run {
Logger.error(
"Could not find channel details for closed channel: channelId=$channelId",
context =TAG
)
return@withContext
}

Issue: This silently fails to persist closed channel information, which might be important for user history.

Recommendation: Consider attempting to fetch channel details from lightningService.channels as a fallback before giving up, or store partial information if available.

5. Exception Handling Inconsistency

Severity: Low

In the original code (LightningService:758):

} catch (e:Exception) {

In the new code (LightningRepo:393):

} catch (e:Throwable) {

Note: Catching Throwable is more comprehensive but also catches Error subclasses (like OutOfMemoryError). This is generally acceptable in top-level handlers, but ensure this is intentional.


🔒 Security Considerations

No significant security concerns identified. The code:

  • ✅ Properly validates required fields (fundingTxo)
  • ✅ Uses safe string operations (take, orEmpty)
  • ✅ Logs errors without exposing sensitive data

⚡ Performance Considerations

  1. Positive: Removing ServiceQueue.LDK.background and ServiceQueue.CORE.background reduces thread context switching and queue overhead.

  2. Minor Concern: refreshChannelCache() iterates through all channels on every sync. For users with many channels, consider:

    • Only updating changed channels
    • Batching updates
    • Using a more efficient data structure if channel counts grow large

🧪 Test Coverage

Concern: No new tests were added for the refactored functionality.

Recommendation: Add unit tests for:

  • handleLdkEvent() with different event types
  • registerClosedChannel() success and failure cases
  • refreshChannelCache() behavior
  • Race condition scenarios

The existing LightningRepoTest.kt would be the appropriate place for these tests.


📋 Additional Notes

  1. Code Quality: The refactoring maintains consistent Kotlin idioms and follows the existing codebase patterns.

  2. Logging: Comprehensive logging is present, which will help with debugging.

  3. Documentation: Consider adding KDoc comments to handleLdkEvent() and registerClosedChannel() explaining their purpose and threading guarantees.


Summary

This is a solid architectural improvement. The main concerns are around thread safety and race conditions with the channel cache. I recommend addressing the concurrency issues before merging to avoid potential bugs in production.

Recommendation: Request changes to address the thread safety concerns in points #1 and #2.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tested:

  • Open and close a channel
  • Wipe and restore app -> check channels

@jvsena42
jvsena42 merged commit 14a29cc into masterNov 20, 2025
14 checks passed
@jvsena42
jvsena42 deleted the chore/move-closed-channels branch November 20, 2025 13:02
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.

2 participants

@ben-kaufman@jvsena42
, '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

Move closed channels tracking to LightningRepo - #474

Merged
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels
Nov 20, 2025
Merged

Move closed channels tracking to LightningRepo#474
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels

Conversation

@ben-kaufman

Copy link
Copy Markdown
Contributor

Addresses #472 (review)

@claude

claudeBot commented Nov 20, 2025

Copy link
Copy Markdown
Contributor

Code Review - PR #474: Move closed channels tracking to LightningRepo

Overview

This PR refactors closed channel tracking functionality from LightningService to LightningRepo, which improves separation of concerns. The service layer should handle low-level LDK operations, while the repository layer manages business logic and state.


✅ Strengths

  1. Better Separation of Concerns: Moving closed channel tracking to the repository layer is architecturally sound. LightningService should focus on LDK node operations, while LightningRepo handles business logic.

  2. Consistent Coroutine Usage: The refactored code properly uses withContext(bgDispatcher) instead of mixing ServiceQueue.LDK.background and ServiceQueue.CORE.background, which simplifies the concurrency model.

  3. Proper Error Handling: The registerClosedChannel function includes comprehensive error handling with detailed logging for missing channel details and funding transactions.


🔍 Potential Issues & Recommendations

1. Thread Safety Concern - Channel Cache Updates

Severity: Medium

The channelCache operations are split between refreshChannelCache() and event handling:

privatesuspendfunrefreshChannelCache() = withContext(bgDispatcher) {
val channels = lightningService.channels ?:return@withContext
channels.forEach { channel ->
channelCache[channel.channelId] = channel // ← Update
}
}
privatefunhandleLdkEvent(event:Event) {
when (event) {
isEvent.ChannelClosed-> {
scope.launch {
registerClosedChannel(channelId, reason) // ← Read then remove
}
}
}
}

Issue: handleLdkEvent is called synchronously (not a suspend function), and launches coroutines on scope which may use a different dispatcher than bgDispatcher. This could cause race conditions between:

  • refreshChannelCache() updating the cache on bgDispatcher
  • registerClosedChannel() reading/removing from the cache on scope's dispatcher

Recommendation: Make handleLdkEvent a suspend function and ensure all cache operations happen on the same dispatcher:

privatesuspendfunhandleLdkEvent(event:Event) = withContext(bgDispatcher) {
when (event) {
isEvent.ChannelClosed-> {
registerClosedChannel(channelId, reason)
}
// ...
}
}

Then update the call sites to use scope.launch:

lightningService.listenForEvents(onEvent = { event ->
scope.launch {
handleLdkEvent(event)
}
eventHandler?.invoke(event)
})

2. Race Condition - Channel Closed Before Cache Population

Severity: Medium

In the start() method:

lightningService.start(timeout) { event ->
handleLdkEvent(event)
eventHandler?.invoke(event)
ldkNodeEventBus.emit(event)
}
// ...
refreshChannelCache() // ← Called AFTER start completes

Issue: If a ChannelClosed event fires during node startup (before refreshChannelCache() is called), the channel won't be in the cache, and you'll get an error log.

Recommendation: Call refreshChannelCache() before starting event processing, or handle the race condition more gracefully in registerClosedChannel().

3. Memory Leak Risk - Unbounded Cache Growth

Severity: Low-Medium

The channelCache stores all channels indefinitely. While closed channels are removed, there's no cleanup mechanism if:

  • refreshChannelCache() fails partway through
  • Channels become stale but aren't closed properly
  • The cache grows large over time with many channels

Recommendation: Consider adding:

  • A maximum cache size with LRU eviction
  • Periodic cleanup of stale entries
  • Clear documentation about cache lifecycle

4. Error Recovery - Missing Channel Details

Severity: Low

When a channel isn't found in the cache, the code logs an error and returns:

val channel = channelCache[channelId] ?:run {
Logger.error(
"Could not find channel details for closed channel: channelId=$channelId",
context =TAG
)
return@withContext
}

Issue: This silently fails to persist closed channel information, which might be important for user history.

Recommendation: Consider attempting to fetch channel details from lightningService.channels as a fallback before giving up, or store partial information if available.

5. Exception Handling Inconsistency

Severity: Low

In the original code (LightningService:758):

} catch (e:Exception) {

In the new code (LightningRepo:393):

} catch (e:Throwable) {

Note: Catching Throwable is more comprehensive but also catches Error subclasses (like OutOfMemoryError). This is generally acceptable in top-level handlers, but ensure this is intentional.


🔒 Security Considerations

No significant security concerns identified. The code:

  • ✅ Properly validates required fields (fundingTxo)
  • ✅ Uses safe string operations (take, orEmpty)
  • ✅ Logs errors without exposing sensitive data

⚡ Performance Considerations

  1. Positive: Removing ServiceQueue.LDK.background and ServiceQueue.CORE.background reduces thread context switching and queue overhead.

  2. Minor Concern: refreshChannelCache() iterates through all channels on every sync. For users with many channels, consider:

    • Only updating changed channels
    • Batching updates
    • Using a more efficient data structure if channel counts grow large

🧪 Test Coverage

Concern: No new tests were added for the refactored functionality.

Recommendation: Add unit tests for:

  • handleLdkEvent() with different event types
  • registerClosedChannel() success and failure cases
  • refreshChannelCache() behavior
  • Race condition scenarios

The existing LightningRepoTest.kt would be the appropriate place for these tests.


📋 Additional Notes

  1. Code Quality: The refactoring maintains consistent Kotlin idioms and follows the existing codebase patterns.

  2. Logging: Comprehensive logging is present, which will help with debugging.

  3. Documentation: Consider adding KDoc comments to handleLdkEvent() and registerClosedChannel() explaining their purpose and threading guarantees.


Summary

This is a solid architectural improvement. The main concerns are around thread safety and race conditions with the channel cache. I recommend addressing the concurrency issues before merging to avoid potential bugs in production.

Recommendation: Request changes to address the thread safety concerns in points #1 and #2.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tested:

  • Open and close a channel
  • Wipe and restore app -> check channels

@jvsena42
jvsena42 merged commit 14a29cc into masterNov 20, 2025
14 checks passed
@jvsena42
jvsena42 deleted the chore/move-closed-channels branch November 20, 2025 13:02
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.

2 participants

@ben-kaufman@jvsena42
, '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

Move closed channels tracking to LightningRepo - #474

Merged
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels
Nov 20, 2025
Merged

Move closed channels tracking to LightningRepo#474
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels

Conversation

@ben-kaufman

Copy link
Copy Markdown
Contributor

Addresses #472 (review)

@claude

claudeBot commented Nov 20, 2025

Copy link
Copy Markdown
Contributor

Code Review - PR #474: Move closed channels tracking to LightningRepo

Overview

This PR refactors closed channel tracking functionality from LightningService to LightningRepo, which improves separation of concerns. The service layer should handle low-level LDK operations, while the repository layer manages business logic and state.


✅ Strengths

  1. Better Separation of Concerns: Moving closed channel tracking to the repository layer is architecturally sound. LightningService should focus on LDK node operations, while LightningRepo handles business logic.

  2. Consistent Coroutine Usage: The refactored code properly uses withContext(bgDispatcher) instead of mixing ServiceQueue.LDK.background and ServiceQueue.CORE.background, which simplifies the concurrency model.

  3. Proper Error Handling: The registerClosedChannel function includes comprehensive error handling with detailed logging for missing channel details and funding transactions.


🔍 Potential Issues & Recommendations

1. Thread Safety Concern - Channel Cache Updates

Severity: Medium

The channelCache operations are split between refreshChannelCache() and event handling:

privatesuspendfunrefreshChannelCache() = withContext(bgDispatcher) {
val channels = lightningService.channels ?:return@withContext
channels.forEach { channel ->
channelCache[channel.channelId] = channel // ← Update
}
}
privatefunhandleLdkEvent(event:Event) {
when (event) {
isEvent.ChannelClosed-> {
scope.launch {
registerClosedChannel(channelId, reason) // ← Read then remove
}
}
}
}

Issue: handleLdkEvent is called synchronously (not a suspend function), and launches coroutines on scope which may use a different dispatcher than bgDispatcher. This could cause race conditions between:

  • refreshChannelCache() updating the cache on bgDispatcher
  • registerClosedChannel() reading/removing from the cache on scope's dispatcher

Recommendation: Make handleLdkEvent a suspend function and ensure all cache operations happen on the same dispatcher:

privatesuspendfunhandleLdkEvent(event:Event) = withContext(bgDispatcher) {
when (event) {
isEvent.ChannelClosed-> {
registerClosedChannel(channelId, reason)
}
// ...
}
}

Then update the call sites to use scope.launch:

lightningService.listenForEvents(onEvent = { event ->
scope.launch {
handleLdkEvent(event)
}
eventHandler?.invoke(event)
})

2. Race Condition - Channel Closed Before Cache Population

Severity: Medium

In the start() method:

lightningService.start(timeout) { event ->
handleLdkEvent(event)
eventHandler?.invoke(event)
ldkNodeEventBus.emit(event)
}
// ...
refreshChannelCache() // ← Called AFTER start completes

Issue: If a ChannelClosed event fires during node startup (before refreshChannelCache() is called), the channel won't be in the cache, and you'll get an error log.

Recommendation: Call refreshChannelCache() before starting event processing, or handle the race condition more gracefully in registerClosedChannel().

3. Memory Leak Risk - Unbounded Cache Growth

Severity: Low-Medium

The channelCache stores all channels indefinitely. While closed channels are removed, there's no cleanup mechanism if:

  • refreshChannelCache() fails partway through
  • Channels become stale but aren't closed properly
  • The cache grows large over time with many channels

Recommendation: Consider adding:

  • A maximum cache size with LRU eviction
  • Periodic cleanup of stale entries
  • Clear documentation about cache lifecycle

4. Error Recovery - Missing Channel Details

Severity: Low

When a channel isn't found in the cache, the code logs an error and returns:

val channel = channelCache[channelId] ?:run {
Logger.error(
"Could not find channel details for closed channel: channelId=$channelId",
context =TAG
)
return@withContext
}

Issue: This silently fails to persist closed channel information, which might be important for user history.

Recommendation: Consider attempting to fetch channel details from lightningService.channels as a fallback before giving up, or store partial information if available.

5. Exception Handling Inconsistency

Severity: Low

In the original code (LightningService:758):

} catch (e:Exception) {

In the new code (LightningRepo:393):

} catch (e:Throwable) {

Note: Catching Throwable is more comprehensive but also catches Error subclasses (like OutOfMemoryError). This is generally acceptable in top-level handlers, but ensure this is intentional.


🔒 Security Considerations

No significant security concerns identified. The code:

  • ✅ Properly validates required fields (fundingTxo)
  • ✅ Uses safe string operations (take, orEmpty)
  • ✅ Logs errors without exposing sensitive data

⚡ Performance Considerations

  1. Positive: Removing ServiceQueue.LDK.background and ServiceQueue.CORE.background reduces thread context switching and queue overhead.

  2. Minor Concern: refreshChannelCache() iterates through all channels on every sync. For users with many channels, consider:

    • Only updating changed channels
    • Batching updates
    • Using a more efficient data structure if channel counts grow large

🧪 Test Coverage

Concern: No new tests were added for the refactored functionality.

Recommendation: Add unit tests for:

  • handleLdkEvent() with different event types
  • registerClosedChannel() success and failure cases
  • refreshChannelCache() behavior
  • Race condition scenarios

The existing LightningRepoTest.kt would be the appropriate place for these tests.


📋 Additional Notes

  1. Code Quality: The refactoring maintains consistent Kotlin idioms and follows the existing codebase patterns.

  2. Logging: Comprehensive logging is present, which will help with debugging.

  3. Documentation: Consider adding KDoc comments to handleLdkEvent() and registerClosedChannel() explaining their purpose and threading guarantees.


Summary

This is a solid architectural improvement. The main concerns are around thread safety and race conditions with the channel cache. I recommend addressing the concurrency issues before merging to avoid potential bugs in production.

Recommendation: Request changes to address the thread safety concerns in points #1 and #2.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tested:

  • Open and close a channel
  • Wipe and restore app -> check channels

@jvsena42
jvsena42 merged commit 14a29cc into masterNov 20, 2025
14 checks passed
@jvsena42
jvsena42 deleted the chore/move-closed-channels branch November 20, 2025 13:02
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.

2 participants

@ben-kaufman@jvsena42
, '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

Move closed channels tracking to LightningRepo - #474

Merged
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels
Nov 20, 2025
Merged

Move closed channels tracking to LightningRepo#474
jvsena42 merged 1 commit into
masterfrom
chore/move-closed-channels

Conversation

@ben-kaufman

Copy link
Copy Markdown
Contributor

Addresses #472 (review)

@claude

claudeBot commented Nov 20, 2025

Copy link
Copy Markdown
Contributor

Code Review - PR #474: Move closed channels tracking to LightningRepo

Overview

This PR refactors closed channel tracking functionality from LightningService to LightningRepo, which improves separation of concerns. The service layer should handle low-level LDK operations, while the repository layer manages business logic and state.


✅ Strengths

  1. Better Separation of Concerns: Moving closed channel tracking to the repository layer is architecturally sound. LightningService should focus on LDK node operations, while LightningRepo handles business logic.

  2. Consistent Coroutine Usage: The refactored code properly uses withContext(bgDispatcher) instead of mixing ServiceQueue.LDK.background and ServiceQueue.CORE.background, which simplifies the concurrency model.

  3. Proper Error Handling: The registerClosedChannel function includes comprehensive error handling with detailed logging for missing channel details and funding transactions.


🔍 Potential Issues & Recommendations

1. Thread Safety Concern - Channel Cache Updates

Severity: Medium

The channelCache operations are split between refreshChannelCache() and event handling:

privatesuspendfunrefreshChannelCache() = withContext(bgDispatcher) {
val channels = lightningService.channels ?:return@withContext
channels.forEach { channel ->
channelCache[channel.channelId] = channel // ← Update
}
}
privatefunhandleLdkEvent(event:Event) {
when (event) {
isEvent.ChannelClosed-> {
scope.launch {
registerClosedChannel(channelId, reason) // ← Read then remove
}
}
}
}

Issue: handleLdkEvent is called synchronously (not a suspend function), and launches coroutines on scope which may use a different dispatcher than bgDispatcher. This could cause race conditions between:

  • refreshChannelCache() updating the cache on bgDispatcher
  • registerClosedChannel() reading/removing from the cache on scope's dispatcher

Recommendation: Make handleLdkEvent a suspend function and ensure all cache operations happen on the same dispatcher:

privatesuspendfunhandleLdkEvent(event:Event) = withContext(bgDispatcher) {
when (event) {
isEvent.ChannelClosed-> {
registerClosedChannel(channelId, reason)
}
// ...
}
}

Then update the call sites to use scope.launch:

lightningService.listenForEvents(onEvent = { event ->
scope.launch {
handleLdkEvent(event)
}
eventHandler?.invoke(event)
})

2. Race Condition - Channel Closed Before Cache Population

Severity: Medium

In the start() method:

lightningService.start(timeout) { event ->
handleLdkEvent(event)
eventHandler?.invoke(event)
ldkNodeEventBus.emit(event)
}
// ...
refreshChannelCache() // ← Called AFTER start completes

Issue: If a ChannelClosed event fires during node startup (before refreshChannelCache() is called), the channel won't be in the cache, and you'll get an error log.

Recommendation: Call refreshChannelCache() before starting event processing, or handle the race condition more gracefully in registerClosedChannel().

3. Memory Leak Risk - Unbounded Cache Growth

Severity: Low-Medium

The channelCache stores all channels indefinitely. While closed channels are removed, there's no cleanup mechanism if:

  • refreshChannelCache() fails partway through
  • Channels become stale but aren't closed properly
  • The cache grows large over time with many channels

Recommendation: Consider adding:

  • A maximum cache size with LRU eviction
  • Periodic cleanup of stale entries
  • Clear documentation about cache lifecycle

4. Error Recovery - Missing Channel Details

Severity: Low

When a channel isn't found in the cache, the code logs an error and returns:

val channel = channelCache[channelId] ?:run {
Logger.error(
"Could not find channel details for closed channel: channelId=$channelId",
context =TAG
)
return@withContext
}

Issue: This silently fails to persist closed channel information, which might be important for user history.

Recommendation: Consider attempting to fetch channel details from lightningService.channels as a fallback before giving up, or store partial information if available.

5. Exception Handling Inconsistency

Severity: Low

In the original code (LightningService:758):

} catch (e:Exception) {

In the new code (LightningRepo:393):

} catch (e:Throwable) {

Note: Catching Throwable is more comprehensive but also catches Error subclasses (like OutOfMemoryError). This is generally acceptable in top-level handlers, but ensure this is intentional.


🔒 Security Considerations

No significant security concerns identified. The code:

  • ✅ Properly validates required fields (fundingTxo)
  • ✅ Uses safe string operations (take, orEmpty)
  • ✅ Logs errors without exposing sensitive data

⚡ Performance Considerations

  1. Positive: Removing ServiceQueue.LDK.background and ServiceQueue.CORE.background reduces thread context switching and queue overhead.

  2. Minor Concern: refreshChannelCache() iterates through all channels on every sync. For users with many channels, consider:

    • Only updating changed channels
    • Batching updates
    • Using a more efficient data structure if channel counts grow large

🧪 Test Coverage

Concern: No new tests were added for the refactored functionality.

Recommendation: Add unit tests for:

  • handleLdkEvent() with different event types
  • registerClosedChannel() success and failure cases
  • refreshChannelCache() behavior
  • Race condition scenarios

The existing LightningRepoTest.kt would be the appropriate place for these tests.


📋 Additional Notes

  1. Code Quality: The refactoring maintains consistent Kotlin idioms and follows the existing codebase patterns.

  2. Logging: Comprehensive logging is present, which will help with debugging.

  3. Documentation: Consider adding KDoc comments to handleLdkEvent() and registerClosedChannel() explaining their purpose and threading guarantees.


Summary

This is a solid architectural improvement. The main concerns are around thread safety and race conditions with the channel cache. I recommend addressing the concurrency issues before merging to avoid potential bugs in production.

Recommendation: Request changes to address the thread safety concerns in points #1 and #2.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tested:

  • Open and close a channel
  • Wipe and restore app -> check channels

@jvsena42
jvsena42 merged commit 14a29cc into masterNov 20, 2025
14 checks passed
@jvsena42
jvsena42 deleted the chore/move-closed-channels branch November 20, 2025 13:02
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.

2 participants

@ben-kaufman@jvsena42