Fix BLE identity state races

This commit is contained in:
jack
2026-07-10 20:20:45 +02:00
parent 733098bb63
commit 8da6dba905
5 changed files with 193 additions and 41 deletions
@@ -5,7 +5,7 @@ import Testing
struct BLEAnnounceThrottleTests {
@Test
func firstAnnounceIsAllowed() {
var throttle = BLEAnnounceThrottle(normalMinimumInterval: 10, forcedMinimumInterval: 2)
let throttle = BLEAnnounceThrottle(normalMinimumInterval: 10, forcedMinimumInterval: 2)
let shouldSend = throttle.shouldSend(force: false, now: Date(timeIntervalSince1970: 100))
@@ -15,7 +15,7 @@ struct BLEAnnounceThrottleTests {
@Test
func regularAnnounceUsesNormalMinimumInterval() {
let now = Date(timeIntervalSince1970: 100)
var throttle = BLEAnnounceThrottle(normalMinimumInterval: 10, forcedMinimumInterval: 2)
let throttle = BLEAnnounceThrottle(normalMinimumInterval: 10, forcedMinimumInterval: 2)
let first = throttle.shouldSend(force: false, now: now)
let suppressed = throttle.shouldSend(force: false, now: now.addingTimeInterval(9.9))
@@ -29,7 +29,7 @@ struct BLEAnnounceThrottleTests {
@Test
func forcedAnnounceUsesShorterMinimumInterval() {
let now = Date(timeIntervalSince1970: 100)
var throttle = BLEAnnounceThrottle(normalMinimumInterval: 10, forcedMinimumInterval: 2)
let throttle = BLEAnnounceThrottle(normalMinimumInterval: 10, forcedMinimumInterval: 2)
let first = throttle.shouldSend(force: false, now: now)
let suppressed = throttle.shouldSend(force: true, now: now.addingTimeInterval(1.9))
@@ -43,10 +43,40 @@ struct BLEAnnounceThrottleTests {
@Test
func elapsedReportsTimeSinceAcceptedSend() {
let now = Date(timeIntervalSince1970: 100)
var throttle = BLEAnnounceThrottle(normalMinimumInterval: 10, forcedMinimumInterval: 2)
let throttle = BLEAnnounceThrottle(normalMinimumInterval: 10, forcedMinimumInterval: 2)
_ = throttle.shouldSend(force: false, now: now)
#expect(throttle.elapsed(since: now.addingTimeInterval(3)) == 3)
}
@Test
func concurrentRequestsAdmitOnlyOneAnnounce() {
let now = Date(timeIntervalSince1970: 100)
let throttle = BLEAnnounceThrottle(
normalMinimumInterval: 10,
forcedMinimumInterval: 2
)
let accepted = LockedCounter()
DispatchQueue.concurrentPerform(iterations: 1_000) { _ in
if throttle.shouldSend(force: false, now: now) {
accepted.increment()
}
}
#expect(accepted.value == 1)
#expect(throttle.elapsed(since: now.addingTimeInterval(3)) == 3)
}
}
private final class LockedCounter: @unchecked Sendable {
private let lock = NSLock()
private var count = 0
var value: Int { lock.withLock { count } }
func increment() {
lock.withLock { count += 1 }
}
}
@@ -0,0 +1,57 @@
import BitFoundation
import Foundation
import Testing
@testable import bitchat
struct BLELocalIdentityStateStoreTests {
@Test
func identityReplacementUpdatesWireBytesAtomically() throws {
let initial = PeerID(str: "0011223344556677")
let replacement = PeerID(str: "8899aabbccddeeff")
let store = BLELocalIdentityStateStore(peerID: initial, nickname: "alice")
store.replacePeerIdentity(with: replacement)
let snapshot = store.snapshot()
#expect(snapshot.peerID == replacement)
#expect(snapshot.peerIDData == Data(hexString: replacement.id))
#expect(snapshot.nickname == "alice")
}
@Test
func concurrentReadsNeverObserveSplitIdentityState() {
let peerIDs = [
PeerID(str: "0011223344556677"),
PeerID(str: "8899aabbccddeeff")
]
let store = BLELocalIdentityStateStore(peerID: peerIDs[0], nickname: "alice")
let failures = LockedFailureRecorder()
DispatchQueue.concurrentPerform(iterations: 2_000) { index in
if index.isMultiple(of: 2) {
store.replacePeerIdentity(with: peerIDs[index % peerIDs.count])
} else {
store.setNickname(index.isMultiple(of: 3) ? "alice" : "bob")
}
let snapshot = store.snapshot()
let expectedWireID = Data(hexString: snapshot.peerID.id) ?? Data()
if snapshot.peerIDData != expectedWireID {
failures.record()
}
}
#expect(!failures.hasFailure)
}
}
private final class LockedFailureRecorder: @unchecked Sendable {
private let lock = NSLock()
private var failed = false
var hasFailure: Bool { lock.withLock { failed } }
func record() {
lock.withLock { failed = true }
}
}