Coverage Summary for Class: ThreadedCluster (it.unibo.tuprolog.argumentation.parallel.cluster)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| ThreadedCluster |
95.2%
(20/21)
|
66.7%
(16/24)
|
86.7%
(39/45)
|
89%
(331/372)
|
| ThreadedCluster$Companion |
66.7%
(2/3)
|
|
66.7%
(2/3)
|
88.2%
(15/17)
|
| ThreadedCluster$Waited |
100%
(3/3)
|
75%
(3/4)
|
100%
(11/11)
|
96.5%
(82/85)
|
| Total |
92.6%
(25/27)
|
67.9%
(19/28)
|
88.1%
(52/59)
|
90.3%
(428/474)
|
package it.unibo.tuprolog.argumentation.parallel.cluster
import it.unibo.tuprolog.argumentation.Arg2pException
import it.unibo.tuprolog.argumentation.Flags
import it.unibo.tuprolog.argumentation.parallel.federation.Beacons
import it.unibo.tuprolog.argumentation.parallel.federation.LanDiscovery
import it.unibo.tuprolog.argumentation.parallel.federation.Link
import it.unibo.tuprolog.argumentation.parallel.federation.NearbySite
import it.unibo.tuprolog.argumentation.parallel.federation.PoolScheduler
import it.unibo.tuprolog.argumentation.parallel.federation.Site
import it.unibo.tuprolog.argumentation.parallel.federation.StaticContent
import it.unibo.tuprolog.argumentation.parallel.federation.WebSocketLinks
import it.unibo.tuprolog.argumentation.parallel.message.EvaluationResponse
import java.net.InetAddress
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
/**
* A distributed solver on the threads of this JVM: each node (the catalog, each worker) handles its messages one
* at a time, the nodes in parallel on [threads] threads. The theory is split into at most [parts] partitions if
* given, as many as the workers of the cluster and of the sites it is federated with otherwise (see [site]); the
* graph is labelled as the standard solver does with [flags]. The nodes share the terms of their messages; with
* [textual], every message is written as text by its sender and read by its receiver instead, as between processes
* (see [it.unibo.tuprolog.argumentation.parallel.message.MessageCodec]).
*/
class ThreadedCluster private constructor(
private val scheduler: PoolScheduler,
site: Site,
) : DiscoveringCluster(site),
AutoCloseable {
@JvmOverloads
constructor(
threads: Int = processors,
parts: Int? = null,
flags: Flags = Flags.defaults(),
textual: Boolean = false,
lends: Boolean = true,
) : this(PoolScheduler(threads), threads, parts, flags, textual, lends)
// the site running a worker on each thread
private constructor(
scheduler: PoolScheduler,
threads: Int,
parts: Int?,
flags: Flags,
textual: Boolean,
lends: Boolean,
) : this(scheduler, Site(scheduler, threads, parts, flags, lends, textual).apply { name = HOST_NAME })
// made when the cluster first listens or connects
private var links: WebSocketLinks? = null
private var port: Int? = null
private var discovery: LanDiscovery? = null
private fun links() = links ?: WebSocketLinks(site).also { links = it }
// The replies of the site, waited for: the evaluations of several callers run in parallel, those waiting when the
// cluster closes failing then
private val replies = Waited()
/** The identifier of the site of the cluster, as the others know it */
val id: String get() = site.id
/** The sites the cluster is federated with */
val peers: Set<String> get() = site.peerSites
/**
* Accepts the links of other sites (on any platform) over WebSockets, on [port] of [host] (an ephemeral port for
* 0): the port taken
*/
@JvmOverloads
fun listen(
port: Int = 0,
host: String = "0.0.0.0",
): Int = links().listen(port, host).also { this.port = it }
/**
* Federates the cluster with the clusters of the local network that discover the others as well, as they come
* (see [Beacons]): it listens (on an ephemeral port, if it did not already), and its beacons go to [beaconPort].
* With [requireCode] (see [requiresCode]), the clusters heard are [nearby] alone, linked to once paired by hand
* ([pair]), and linked again with no code from then on.
*/
@JvmOverloads
fun discover(
beaconPort: Int = Beacons.PORT,
requireCode: Boolean = false,
) {
check(discovery == null) { "Already discovering" }
if (requireCode) requiresCode = true
discovery = LanDiscovery(site, links(), port ?: listen(), beaconPort)
}
/**
* Federates the cluster with the site listening at [uri] (ws://host:port), proving it knows [code] if it requires
* one: an IllegalStateException if no site answers in a few seconds
*/
@JvmOverloads
fun connect(
uri: String,
code: String? = null,
) = links().connect(uri, code = code)
/** The sites heard on the local network, not linked to this cluster (see [discover]) */
val nearby: List<NearbySite> get() = site.nearby
/**
* Links this cluster to [site], nearby, proving it knows its [code]: the two paired, linked again with no code
* while they run; an Arg2pException if it refuses the code, does not prove it knows it as well (a site claiming
* the identifier of another), does not answer in a few seconds, or if no site nearby is [site]
*/
fun pair(
site: String,
code: String,
) {
val nearby = nearbySite(site)
val greeted = CompletableFuture<String?>()
var opened: Link? = null
try {
links().connect(nearby.address, PAIRING, code) { link ->
opened = link
this.site.greeting(link) { refusal -> greeted.complete(refusal) }
}
greeted.get(PAIRING, TimeUnit.MILLISECONDS)?.let { throw Arg2pException("$site refused the link: $it") }
} catch (_: TimeoutException) {
opened?.close()
throw Arg2pException("$site did not answer in time")
} catch (failure: IllegalStateException) {
throw Arg2pException("$site cannot be reached: ${failure.message}")
} finally {
opened?.let { this.site.forgetGreeting(it) }
}
}
/**
* Serves [pages] over plain HTTP on the port the cluster listens on (see [WebSocketLinks.pages]): the page a GET
* of a path is answered with, none (null) for a 404
*/
fun serve(pages: (path: String) -> StaticContent?) {
links().pages = pages
}
/**
* The arguments for [statement], as Prolog text, by label: the evaluations of several callers run in parallel (an
* Arg2pException if the cluster closes meanwhile)
*/
fun solve(statement: String): EvaluationResponse = answered(replies.replied { site.solve(statement, it) })
/**
* The cluster as a solver of arg2p: any goal, its solutions and their graphs, the answers of queries. The goals
* of several callers are solved in turn by the catalog, each solved again from its start when another one was
* solved since its last solution.
*/
val solver: ClusterSolver = ClusterSolver(site, replies)
/** Closes the links to the other sites, and stops the threads: the evaluations waiting fail */
override fun close() {
replies.close()
discovery?.close()
links?.close()
scheduler.close()
}
// The replies of the site, each waited for by the thread asking: those waiting when the cluster closes, and those
// asked for since, an Arg2pException
private class Waited : Replies {
private val waiting = ConcurrentHashMap.newKeySet<CompletableFuture<*>>()
@Volatile private var closed = false
override fun <T> replied(ask: ((T) -> Unit) -> Unit): T {
val response = CompletableFuture<T>()
waiting += response
try {
if (closed) throw CLOSED
ask { response.complete(it) }
return response.join()
} catch (failure: CompletionException) {
throw failure.cause ?: failure
} finally {
waiting -= response
}
}
fun close() {
closed = true
waiting.forEach { it.completeExceptionally(CLOSED) }
}
}
companion object {
/** The threads of a cluster by default: as many as the processors */
@JvmStatic
val processors: Int get() = Runtime.getRuntime().availableProcessors()
// How long a pairing waits for the other site, in milliseconds
private const val PAIRING = 10_000L
private val CLOSED get() = Arg2pException("The cluster is closed")
/** The name of this host, if it has one (empty otherwise): the name of the sites of the clusters by default */
@JvmStatic
val HOST_NAME: String by lazy { runCatching { InetAddress.getLocalHost().hostName }.getOrDefault("") }
}
}