Skip to content

Commit 09555a4

Browse files
authored
fix(node): reconcile stale identity replays after renumbering (#6259)
1 parent d12fc8e commit 09555a4

14 files changed

Lines changed: 2661 additions & 115 deletions

File tree

core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/NodeInfoReadDataSource.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,11 @@ interface NodeInfoReadDataSource {
3737
suspend fun getNodesOlderThan(lastHeard: Int): List<NodeEntity>
3838

3939
suspend fun getUnknownNodes(): List<NodeEntity>
40+
41+
/**
42+
* One-shot snapshot of every node row in the currently selected database. Bypasses the process-wide
43+
* [nodeDBbyNumFlow] cache so callers see the live DB state at invocation time rather than a `stateIn` value that
44+
* may belong to a previous transport.
45+
*/
46+
suspend fun getNodeDbSnapshot(): Map<Int, NodeWithRelations>
4047
}

core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,7 @@ class SwitchingNodeInfoReadDataSource(private val dbManager: DatabaseProvider) :
5555

5656
override suspend fun getUnknownNodes(): List<NodeEntity> =
5757
dbManager.withReadDb { it.nodeInfoDao().getUnknownNodes() }
58+
59+
override suspend fun getNodeDbSnapshot(): Map<Int, NodeWithRelations> =
60+
dbManager.withReadDb { it.nodeInfoDao().nodeDBbyNumSnapshot() }
5861
}

core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ class MeshConfigFlowManagerImpl(
296296
if (!isActiveSession(session)) return
297297
if (removedNums.isNotEmpty()) {
298298
Logger.i { "Config install migrated ${removedNums.size} stale node identit(y/ies)" }
299-
removedNums.forEach(nodeManager::removeByNodenum)
299+
nodeManager.applyTrustedIdentityMigrations(removedNums)
300300
}
301301

302302
val published =

core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt

Lines changed: 711 additions & 113 deletions
Large diffs are not rendered by default.

core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ class NodeRepositoryImpl(
227227
override suspend fun getUnknownNodes(): List<Node> =
228228
withContext(dispatchers.io) { nodeInfoReadDataSource.getUnknownNodes().map { it.toModel() } }
229229

230+
override suspend fun getNodeDbSnapshot(): Map<Int, Node> =
231+
withContext(dispatchers.io) { nodeInfoReadDataSource.getNodeDbSnapshot().mapValues { (_, it) -> it.toModel() } }
232+
230233
/** Persists hardware metadata for a node. */
231234
override suspend fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) =
232235
withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(MetadataEntity(nodeNum, metadata)) }

core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImplTest.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,37 @@ class MeshConfigFlowManagerImplTest {
626626
verifySuspend { connectionManager.onNodeDbReady() }
627627
}
628628

629+
@Test
630+
fun `Stage 2 applies trusted migrations before readiness and replay`() = testScope.runTest {
631+
val retiredNum = 456
632+
val callOrder = mutableListOf<String>()
633+
everySuspend { nodeRepository.installConfig(any(), any()) } calls
634+
{
635+
callOrder.add("installConfig")
636+
listOf(retiredNum)
637+
}
638+
every { nodeManager.applyTrustedIdentityMigrations(any()) } calls { callOrder.add("applyMigrations") }
639+
every { nodeManager.setNodeDbReady(true) } calls { callOrder.add("nodeDbReady") }
640+
every { nodeManager.setAllowNodeDbWrites(true) } calls { callOrder.add("writesReady") }
641+
everySuspend { connectionManager.onNodeDbReady() } calls { callOrder.add("replayReady") }
642+
643+
handleMyInfo(protoMyNodeInfo)
644+
advanceUntilIdle()
645+
manager.handleLocalMetadata(metadata)
646+
advanceUntilIdle()
647+
manager.handleConfigComplete(HandshakeConstants.CONFIG_NONCE)
648+
advanceTimeBy(STAGE_TRANSITION_ADVANCE_MS)
649+
runCurrent()
650+
manager.handleConfigComplete(HandshakeConstants.NODE_INFO_NONCE)
651+
advanceUntilIdle()
652+
653+
assertEquals(
654+
listOf("installConfig", "applyMigrations", "nodeDbReady", "writesReady", "replayReady"),
655+
callOrder,
656+
)
657+
verify { nodeManager.applyTrustedIdentityMigrations(listOf(retiredNum)) }
658+
}
659+
629660
@Test
630661
fun `Stage 2 complete id ignored when not in ReceivingNodeInfo state`() = testScope.runTest {
631662
manager.handleConfigComplete(HandshakeConstants.NODE_INFO_NONCE)

core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/NodeManagerConnectionIdentityTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package org.meshtastic.core.data.manager
1919
import dev.mokkery.MockMode
2020
import dev.mokkery.answering.returns
2121
import dev.mokkery.every
22+
import dev.mokkery.everySuspend
2223
import dev.mokkery.mock
2324
import kotlinx.coroutines.flow.MutableStateFlow
2425
import kotlinx.coroutines.launch
@@ -55,6 +56,7 @@ class NodeManagerConnectionIdentityTest {
5556

5657
@BeforeTest
5758
fun setUp() {
59+
everySuspend { nodeRepository.getNodeDbSnapshot() } returns emptyMap()
5860
nodeManager = NodeManagerImpl(nodeRepository, notificationManager, radioInterfaceService, testScope)
5961
}
6062

0 commit comments

Comments
 (0)