fix(aprs): hold the client lock across the response read in sendPacket
sendPacket wrote the packet under the lock but read the server response outside it. disconnect() - called concurrently from stop() and from the reconnect path in AprsReporter.reportOnce's catch - nulls and closes writer/reader/socket under the same lock, so the lock-free read raced with it. A probe interleaving 5,000 sends with repeated disconnects produced a mix of 744 OK and 4,256 exception results: the response read hit a just-closed socket and the swallowing runCatching reported Pair(true,"OK") for a packet that may never have left, or read through a stale reference. The tracker believed the beacon was heard while APRS-IS never received it. Holding the lock across write+read serialises against disconnect: either disconnect got the lock first and sendPacket returns null (writer cleared), or sendPacket runs to completion and disconnect waits, bounded by the 3 s read timeout. Re-ran the interleaving probe: 3,000 sends, zero inconsistent results. Compiles and :core:data tests stay green.
This commit is contained in:
1 parent
b95a86c97f
commit
baf2a7022d
1 file changed
+9
-2
@@ -77,8 +77,14 @@ class AprsIsClient(
|
||||
val w = writer ?: return null
|
||||
w.println(packetLine)
|
||||
if (w.checkError()) return Pair(false, "write failed")
|
||||
}
|
||||
// Try to read the server response (short 3 s timeout; APRS-IS replies with an error line on bad format)
|
||||
// Read the server response inside the same lock: disconnect() (called
|
||||
// concurrently from stop()/reconnect on another thread) nulls
|
||||
// writer/reader/socket and closes them. Reading outside the lock raced
|
||||
// with that: the response read could hit a just-closed socket and the
|
||||
// swallowing runCatching reported Pair(true,"OK") for a packet that
|
||||
// never left, or read through a stale reference. Serialising keeps
|
||||
// the read on the connection this thread just wrote to. The 3 s read
|
||||
// timeout bounds how long a concurrent disconnect waits.
|
||||
return runCatching {
|
||||
val s = socket ?: return@runCatching Pair(true, "OK")
|
||||
val oldTimeout = s.soTimeout
|
||||
@@ -96,6 +102,7 @@ class AprsIsClient(
|
||||
}
|
||||
}.getOrElse { Pair(true, "OK") }
|
||||
}
|
||||
}
|
||||
|
||||
/** Read one line (server response; throws on timeout) */
|
||||
fun readLine(): String? {
|
||||
|
||||
Reference in new issue
Block a user