Skip to content
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
- NEVER manually append the `Throwable`'s message or any other props to the string passed as the 1st param of `Logger.*` calls, its internals are already enriching the final log message with the details of the `Throwable` passed via the `e` arg
- ALWAYS wrap parameter values in log messages with single quotes, e.g. `Logger.info("Received event '$eventName'", context = TAG)`
- ALWAYS start log messages with a verb, e.g. `Logger.info("Received payment for '$hash'", context = TAG)`
- ALWAYS keep log names, tags, labels, and references mechanically traceable to the caller. Do not rewrite them into long descriptions.
- ALWAYS log errors at the final handling layer where the error is acted upon, not in intermediate layers that just propagate it
- ALWAYS use the Result API instead of try-catch
- NEVER wrap methods returning `Result<T>` in try-catch
Expand Down
20 changes: 2 additions & 18 deletions app/src/main/java/to/bitkit/async/ServiceQueue.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import to.bitkit.ext.callerName
import to.bitkit.utils.AppError
import to.bitkit.utils.measured
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import kotlin.coroutines.CoroutineContext
Expand All @@ -20,30 +18,16 @@ enum class ServiceQueue {

fun <T> blocking(
coroutineContext: CoroutineContext = scope.coroutineContext,
functionName: String = Thread.currentThread().callerName,
block: suspend CoroutineScope.() -> T,
): T = runBlocking(coroutineContext) {
runCatching {
measured(label = functionName, context = TAG) {
block()
}
}.getOrElse { throw AppError(it) }
runCatching { block() }.getOrElse { throw AppError(it) }
}

suspend fun <T> background(
coroutineContext: CoroutineContext = scope.coroutineContext,
functionName: String = Thread.currentThread().callerName,
block: suspend CoroutineScope.() -> T,
): T = withContext(coroutineContext) {
runCatching {
measured(label = functionName, context = TAG) {
block()
}
}.getOrElse { throw AppError(it) }
}

companion object {
private const val TAG = "ServiceQueue"
runCatching { block() }.getOrElse { throw AppError(it) }
}
}

Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class WakeNodeWorker @AssistedInject constructor(
private var notificationPayload: JsonObject? = null

private val timeout = 2.minutes
private val slowWakeNodeThreshold = 10.seconds
private val deliverSignal = CompletableDeferred<Unit>()

override suspend fun doWork(): Result {
Expand All @@ -80,7 +81,7 @@ class WakeNodeWorker @AssistedInject constructor(
}

return runCatching {
measured(label = "doWork", context = TAG) {
measured(label = "doWork", context = TAG, slowThreshold = slowWakeNodeThreshold) {
lightningRepo.start(
walletIndex = 0,
timeout = timeout,
Expand Down
48 changes: 48 additions & 0 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ class LightningRepo @Inject constructor(

// Initial state sync
syncState()
logNodeSupportSummary("node started")
updateGeoBlockState()
refreshChannelCache()

Expand All @@ -376,6 +377,7 @@ class LightningRepo @Inject constructor(
connectToTrustedPeers().onFailure {
Logger.error("Failed to connect to trusted peers", it, context = TAG)
}
logNodeSupportSummary("trusted peers connected")

sync().onFailure { e ->
Logger.warn("Initial sync failed, event-driven sync will retry", e, context = TAG)
Expand Down Expand Up @@ -1294,6 +1296,52 @@ class LightningRepo @Inject constructor(
}
}

private fun logNodeSupportSummary(reason: String) {
val state = _lightningState.value
val connectedPeers = state.peers.count { it.isConnected }
val persistedPeers = state.peers.count { it.isPersisted }
val readyChannels = state.channels.count { it.isChannelReady }
val usableChannels = state.channels.count { it.isUsable }

Logger.info(
"Collected node support summary for '$reason': " +
"nodeId='${state.nodeId}', " +
"lifecycle='${state.nodeLifecycleState}', " +
"peers='${state.peers.size}', " +
"connectedPeers='$connectedPeers', " +
"persistedPeers='$persistedPeers', " +
"channels='${state.channels.size}', " +
"readyChannels='$readyChannels', " +
"usableChannels='$usableChannels'",
context = TAG,
)

state.peers.forEach {
Logger.info(
"Collected peer support summary for '$reason': " +
"nodeId='${it.nodeId}', " +
"address='${it.address}', " +
"connected='${it.isConnected}', " +
"persisted='${it.isPersisted}'",
context = TAG,
)
}

state.channels.forEach {
Logger.info(
"Collected channel support summary for '$reason': " +
"channelId='${it.channelId}', " +
"counterparty='${it.counterpartyNodeId}', " +
"ready='${it.isChannelReady}', " +
"usable='${it.isUsable}', " +
"announced='${it.isAnnounced}', " +
"outboundMsat='${it.outboundCapacityMsat}', " +
"inboundMsat='${it.inboundCapacityMsat}'",
context = TAG,
)
}
}

suspend fun awaitPeerConnected(timeout: Duration = 30.seconds) = withContext(bgDispatcher) {
if (lightningService.peers?.any { it.isConnected } == true) return@withContext
Logger.debug("Waiting for peer to reconnect (timeout='$timeout')...", context = TAG)
Expand Down
Loading
Loading