Coverage Summary for Class: Beacons (it.unibo.tuprolog.argumentation.parallel.federation)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| Beacons |
100%
(8/8)
|
81.5%
(44/54)
|
100%
(41/41)
|
98.4%
(504/512)
|
| Beacons$Companion |
|
| Beacons$heard$lambda$1$$inlined$sortedBy$1 |
0%
(0/1)
|
|
| Total |
88.9%
(8/9)
|
81.5%
(44/54)
|
100%
(41/41)
|
98.4%
(504/512)
|
package it.unibo.tuprolog.argumentation.parallel.federation
/**
* The discovery of the sites of a local network, whatever the way their beacons travel (UDP multicast on the JVM and
* on Node.js): each discovering site announces itself, every [interval] milliseconds, with its name, the port of its
* WebSocket server ([port]) and whether it requires a code (see [Site.requiresCode]); the sites it hears, of its
* version, are its nearby ones (see [Site.nearby]), until they are heard no more.
*
* A site [connect]s to the sites it hears that are not its peers yet, unless one of the two requires a code: those it
* is paired with ([Site.pairedSites]) alone then, proving it knows the token of their pairing, the others waiting to
* be paired by hand. Of two sites hearing each other, the one with the lower identifier connects; the other connects
* as well if no link came after a few beacons (if its own beacons do not reach the first, say). A site that left and
* comes back is linked to again.
*
* Anyone may send a beacon: a site whose identifier is claimed by the beacons of two hosts at once (this one counting
* as one, by [local], as its own beacons reach it on every interface) is neither nearby nor linked to, until only
* one of them is heard; a site claiming the identifier of a site paired with this one is never linked to (it cannot
* prove it knows the token of their pairing, see [Pairing]).
*
* The constructor and the beacons are those of the transports of the module; the [port][PORT] and the [group][GROUP]
* of the beacons are public.
*/
class Beacons internal constructor(
private val site: Site,
private val port: Int,
private val interval: Long,
private val local: (host: String) -> Boolean,
private val connect: (url: String, code: String?, done: () -> Unit) -> Unit,
) {
private val lock = Any()
private val heard = mutableMapOf<String, Long>()
private val connecting = mutableSetOf<String>()
// The sites heard, with when they were last
private val nearby = mutableMapOf<String, Pair<NearbySite, Long>>()
// The hosts the beacons of each site came from lately (this one as LOCAL), with when they last did
private val hosts = mutableMapOf<String, MutableMap<String, Long>>()
/** The beacon of the site, to send every [interval] milliseconds */
internal val beacon: String
get() =
listOf(PREFIX, Site.VERSION, site.id, port.toString(), if (site.requiresCode) "1" else "0", site.name)
.joinToString(SEPARATOR)
/** A beacon [text] from [host], heard at [now] (milliseconds): a site nearby, and a link opened to it if it needs one */
internal fun heard(
text: String,
host: String,
now: Long,
) {
val fields = text.split(SEPARATOR, limit = FIELDS)
val valid = fields.size == FIELDS && fields[0] == PREFIX && fields[1] == Site.VERSION && fields[3].toIntOrNull() != null
val id = fields.getOrNull(2)?.takeIf { valid && it != site.id }
val url = "ws://${if (':' in host) "[$host]" else host}:${fields.getOrNull(3)}"
val gated = fields.getOrNull(4) == "1"
val forgotten = now - PATIENCE * interval
val (sites, claimed) =
locked(lock) {
id?.let { hosts.getOrPut(it) { mutableMapOf() }[if (local(host)) LOCAL else host] = now }
hosts.values.forEach { it.values.removeAll { last -> last < forgotten } }
hosts.values.removeAll { it.isEmpty() }
if (id != null) nearby[id] = NearbySite(id, fields[5], url, gated) to now
// the sites not heard for a few beacons gone (this one hears its own beacons, if no other)
nearby.entries.removeAll { it.value.second < forgotten }
val contested = hosts.filterValues { it.size > 1 }.keys
val listed = nearby.filterKeys { it !in contested }.values.map { it.first }
listed.sortedBy { it.id } to id?.takeIf { it !in contested }
}
site.heard(sites)
if (claimed == null) return
if (claimed in site.peerSites) {
// linked: when it was first heard counts again from its next beacons, once the link closes
locked(lock) { heard -= claimed }
return
}
val token = site.tokenOf(claimed)
if (token == null && (gated || site.requiresCode)) return
val due =
locked(lock) {
val first = heard.getOrPut(claimed) { now }
val turn = site.id < claimed || now - first > PATIENCE * interval
(turn && connecting.add(claimed)).also { if (it) heard -= claimed }
}
if (due) connect(url, token) { locked(lock) { connecting -= claimed } }
}
companion object {
/** The UDP port the beacons are sent to */
const val PORT: Int = 9430
/** The multicast group the beacons are sent to, within the local network */
const val GROUP: String = "239.255.42.99"
private const val PREFIX = "arg2p"
private const val SEPARATOR = "|"
// The host of this site, among those the beacons come from
private const val LOCAL = ""
// The fields of a beacon, the name last (it may hold the separator)
private const val FIELDS = 6
// The beacons the site with the higher identifier waits for a link before opening one itself, and those a site
// nearby may miss before it is gone
private const val PATIENCE = 3
}
}