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
|
val w = writer ?: return null
|
||||||
w.println(packetLine)
|
w.println(packetLine)
|
||||||
if (w.checkError()) return Pair(false, "write failed")
|
if (w.checkError()) return Pair(false, "write failed")
|
||||||
}
|
// Read the server response inside the same lock: disconnect() (called
|
||||||
// Try to read the server response (short 3 s timeout; APRS-IS replies with an error line on bad format)
|
// 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 {
|
return runCatching {
|
||||||
val s = socket ?: return@runCatching Pair(true, "OK")
|
val s = socket ?: return@runCatching Pair(true, "OK")
|
||||||
val oldTimeout = s.soTimeout
|
val oldTimeout = s.soTimeout
|
||||||
@@ -96,6 +102,7 @@ class AprsIsClient(
|
|||||||
}
|
}
|
||||||
}.getOrElse { Pair(true, "OK") }
|
}.getOrElse { Pair(true, "OK") }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Read one line (server response; throws on timeout) */
|
/** Read one line (server response; throws on timeout) */
|
||||||
fun readLine(): String? {
|
fun readLine(): String? {
|
||||||
|
|||||||
Reference in new issue
Block a user