From db0ae13d5ea08cdd373ebb0781059ed4da6d7939 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:27:56 -0600 Subject: [PATCH] refactor: improve node DB state management and readiness logic (#3932) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../geeksville/mesh/service/MeshService.kt | 94 ++++++++++--------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index c6164fa82..c0e923994 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -393,6 +393,7 @@ class MeshService : Service() { radioConfigRepository.channelSetFlow.onEach { channelSet = it }.launchIn(serviceScope) serviceRepository.serviceAction.onEach(::onServiceAction).launchIn(serviceScope) nodeRepository.myNodeInfo + .onEach { myNodeInfo = it } .flatMapLatest { myNodeEntity -> // When myNodeInfo changes, set up emissions for the "provide-location-nodeNum" pref. if (myNodeEntity == null) { @@ -482,20 +483,16 @@ class MeshService : Service() { // BEGINNING OF MODEL - FIXME, move elsewhere // - private fun loadCachedNodeDB() = serviceScope.handledLaunch { - myNodeInfo = nodeRepository.myNodeInfo.value - nodeDBbyNodeNum.putAll(nodeRepository.getNodeDBbyNum().first()) - // Note: we do not haveNodeDB = true because that means we've got a valid db from a real - // device (rather than - // this possibly stale hint) - } + private fun loadCachedNodeDB() = + serviceScope.handledLaunch { nodeDBbyNodeNum.putAll(nodeRepository.getNodeDBbyNum().first()) } /** discard entire node db & message state - used when downloading a new db from the device */ private fun discardNodeDB() { Timber.d("Discarding NodeDB") myNodeInfo = null nodeDBbyNodeNum.clear() - haveNodeDB = false + isNodeDbReady = false + allowNodeDbWrites = false earlyReceivedPackets.clear() } @@ -509,8 +506,11 @@ class MeshService : Service() { private var moduleConfig: LocalModuleConfig = LocalModuleConfig.getDefaultInstance() private var channelSet: AppOnlyProtos.ChannelSet = AppOnlyProtos.ChannelSet.getDefaultInstance() - // True after we've done our initial node db init - @Volatile private var haveNodeDB = false + // True after we've done our initial node db init, signaling we can process packets immediately + @Volatile private var isNodeDbReady = false + + // True when we are allowed to write node updates to the persistent database + @Volatile private var allowNodeDbWrites = false // The database of active nodes, index is the node number private val nodeDBbyNodeNum = ConcurrentHashMap() @@ -609,7 +609,7 @@ class MeshService : Service() { val info = getOrCreateNodeInfo(nodeNum, channel) updateFn(info) - if (info.user.id.isNotEmpty() && haveNodeDB) { + if (info.user.id.isNotEmpty() && isNodeDbReady) { serviceScope.handledLaunch { nodeRepository.upsert(info) } } @@ -1278,7 +1278,7 @@ class MeshService : Service() { } .build() Timber.d("[packet]: ${packet.toOneLineString()}") - if (haveNodeDB) { + if (isNodeDbReady) { processReceivedMeshPacket(preparedPacket) return } @@ -1745,9 +1745,9 @@ class MeshService : Service() { } /** Convert a protobuf NodeInfo into our model objects and update our node DB */ - private fun installNodeInfo(info: MeshProtos.NodeInfo) { + private fun installNodeInfo(info: MeshProtos.NodeInfo, withBroadcast: Boolean = true) { // Just replace/add any entry - updateNodeInfo(info.num) { + updateNodeInfo(info.num, withBroadcast = withBroadcast) { if (info.hasUser()) { // Check if this is a default/unknown user from firmware (node was evicted and re-created) val shouldPreserve = shouldPreserveExistingUser(it.user, info.user) @@ -1814,6 +1814,43 @@ class MeshService : Service() { serviceRepository.setStatusMessage("Nodes (${newNodes.size})") } + private fun handleNodeInfoComplete() { + Timber.d("NodeInfo complete for nonce $nodeInfoNonce") + val packetToSave = + MeshLog( + uuid = UUID.randomUUID().toString(), + message_type = "NodeInfoComplete", + received_date = System.currentTimeMillis(), + raw_message = nodeInfoNonce.toString(), + fromRadio = fromRadio { this.configCompleteId = nodeInfoNonce }, + ) + insertMeshLog(packetToSave) + if (newNodes.isEmpty()) { + Timber.e("Did not receive a valid node info") + } else { + // Batch update: Update in-memory models first without triggering individual DB writes + val entities = + newNodes.mapNotNull { info -> + installNodeInfo(info, withBroadcast = false) + nodeDBbyNodeNum[info.num] + } + newNodes.clear() + + // Perform a single batch DB transaction for all nodes + myNodeInfo + serviceScope.handledLaunch { myNodeInfo?.let { nodeRepository.installConfig(it, entities) } } + + // Enable DB writes for future individual updates + allowNodeDbWrites = true + isNodeDbReady = true + flushEarlyReceivedPackets("node_info_complete") + sendAnalytics() + onHasSettings() + connectionStateHolder.setState(ConnectionState.Connected) + serviceBroadcasts.broadcastConnection() + updateServiceStatusNotification() + } + } + private var rawMyNodeInfo: MeshProtos.MyNodeInfo? = null /** @@ -2089,35 +2126,6 @@ class MeshService : Service() { } } - private fun handleNodeInfoComplete() { - Timber.d("NodeInfo complete for nonce $nodeInfoNonce") - val packetToSave = - MeshLog( - uuid = UUID.randomUUID().toString(), - message_type = "NodeInfoComplete", - received_date = System.currentTimeMillis(), - raw_message = nodeInfoNonce.toString(), - fromRadio = fromRadio { this.configCompleteId = nodeInfoNonce }, - ) - insertMeshLog(packetToSave) - if (newNodes.isEmpty()) { - Timber.e("Did not receive a valid node info") - } else { - newNodes.forEach(::installNodeInfo) - newNodes.clear() - // Individual nodes are already upserted to DB via updateNodeInfo->nodeRepository.upsert - // Only call installConfig to persist myNodeInfo, not to overwrite all nodes - serviceScope.handledLaunch { myNodeInfo?.let { nodeRepository.installConfig(it, emptyList()) } } - haveNodeDB = true - flushEarlyReceivedPackets("node_info_complete") - sendAnalytics() - onHasSettings() - connectionStateHolder.setState(ConnectionState.Connected) - serviceBroadcasts.broadcastConnection() - updateServiceStatusNotification() - } - } - private fun startConfigOnly() { newMyNodeInfo = null Timber.d("Starting config-only nonce=$configOnlyNonce")