Stress test demo
This commit is contained in:
parent
53e506893f
commit
7103786ec9
@ -1,10 +1,12 @@
|
|||||||
package space.kscience.controls.spec
|
package space.kscience.controls.spec
|
||||||
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -14,19 +16,23 @@ import kotlin.time.Duration
|
|||||||
*
|
*
|
||||||
* The flow is canceled when the device scope is canceled
|
* The flow is canceled when the device scope is canceled
|
||||||
*/
|
*/
|
||||||
public fun <D : DeviceBase<D>, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
|
public fun <D : Device, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
kotlinx.coroutines.delay(interval)
|
delay(interval)
|
||||||
|
launch {
|
||||||
emit(reader())
|
emit(reader())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do a recurring (with a fixed delay) task on a device.
|
* Do a recurring (with a fixed delay) task on a device.
|
||||||
*/
|
*/
|
||||||
public fun <D : DeviceBase<D>> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
|
public fun <D : Device> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
kotlinx.coroutines.delay(interval)
|
delay(interval)
|
||||||
|
launch {
|
||||||
task()
|
task()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
@ -14,9 +14,9 @@ kscience {
|
|||||||
json()
|
json()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.magix.magixApi)
|
api(projects.magix.magixApi)
|
||||||
implementation(projects.controlsCore)
|
api(projects.controlsCore)
|
||||||
implementation("com.benasher44:uuid:0.7.0")
|
api("com.benasher44:uuid:0.7.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ public class DeviceClient(
|
|||||||
private val send: suspend (DeviceMessage) -> Unit,
|
private val send: suspend (DeviceMessage) -> Unit,
|
||||||
) : Device {
|
) : Device {
|
||||||
|
|
||||||
|
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||||
override val coroutineContext: CoroutineContext = newCoroutineContext(context.coroutineContext)
|
override val coroutineContext: CoroutineContext = newCoroutineContext(context.coroutineContext)
|
||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
@ -32,7 +32,7 @@ public fun DeviceManager.connectToMagix(
|
|||||||
endpoint: MagixEndpoint,
|
endpoint: MagixEndpoint,
|
||||||
endpointID: String = controlsMagixFormat.defaultFormat,
|
endpointID: String = controlsMagixFormat.defaultFormat,
|
||||||
): Job = context.launch {
|
): Job = context.launch {
|
||||||
endpoint.subscribe(controlsMagixFormat).onEach { (request, payload) ->
|
endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID)).onEach { (request, payload) ->
|
||||||
val responsePayload = respondHubMessage(payload)
|
val responsePayload = respondHubMessage(payload)
|
||||||
if (responsePayload != null) {
|
if (responsePayload != null) {
|
||||||
endpoint.broadcast(
|
endpoint.broadcast(
|
||||||
@ -44,7 +44,7 @@ public fun DeviceManager.connectToMagix(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.catch { error ->
|
}.catch { error ->
|
||||||
logger.error(error) { "Error while responding to message" }
|
logger.error(error) { "Error while responding to message: ${error.message}" }
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
|
|
||||||
hubMessageFlow(this).onEach { payload ->
|
hubMessageFlow(this).onEach { payload ->
|
||||||
@ -55,7 +55,7 @@ public fun DeviceManager.connectToMagix(
|
|||||||
id = "df[${payload.hashCode()}]"
|
id = "df[${payload.hashCode()}]"
|
||||||
)
|
)
|
||||||
}.catch { error ->
|
}.catch { error ->
|
||||||
logger.error(error) { "Error while sending a message" }
|
logger.error(error) { "Error while sending a message: ${error.message}" }
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,9 @@ internal val tangoMagixFormat = MagixFormat(
|
|||||||
setOf("tango")
|
setOf("tango")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls-kt device binding for Tango-flavored magix loop
|
||||||
|
*/
|
||||||
public fun DeviceManager.launchTangoMagix(
|
public fun DeviceManager.launchTangoMagix(
|
||||||
endpoint: MagixEndpoint,
|
endpoint: MagixEndpoint,
|
||||||
endpointID: String = TANGO_MAGIX_FORMAT,
|
endpointID: String = TANGO_MAGIX_FORMAT,
|
||||||
|
4
demo/many-devices/README.md
Normal file
4
demo/many-devices/README.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Module all-things
|
||||||
|
|
||||||
|
|
||||||
|
|
39
demo/many-devices/build.gradle.kts
Normal file
39
demo/many-devices/build.gradle.kts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("jvm")
|
||||||
|
application
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://repo.kotlin.link")
|
||||||
|
}
|
||||||
|
|
||||||
|
val ktorVersion: String by rootProject.extra
|
||||||
|
val rsocketVersion: String by rootProject.extra
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.magix.magixServer)
|
||||||
|
implementation(projects.controlsMagixClient)
|
||||||
|
implementation(projects.magix.magixRsocket)
|
||||||
|
|
||||||
|
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
||||||
|
implementation("space.kscience:plotlykt-server:0.5.3")
|
||||||
|
implementation(spclibs.logback.classic)
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin{
|
||||||
|
jvmToolchain(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||||
|
kotlinOptions {
|
||||||
|
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("space.kscience.controls.demo.DemoControllerViewKt")
|
||||||
|
}
|
@ -0,0 +1,118 @@
|
|||||||
|
package space.kscience.controls.demo
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import space.kscience.controls.client.connectToMagix
|
||||||
|
import space.kscience.controls.client.controlsMagixFormat
|
||||||
|
import space.kscience.controls.manager.DeviceManager
|
||||||
|
import space.kscience.controls.manager.install
|
||||||
|
import space.kscience.controls.spec.*
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.context.Factory
|
||||||
|
import space.kscience.dataforge.context.request
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.get
|
||||||
|
import space.kscience.dataforge.meta.int
|
||||||
|
import space.kscience.magix.api.MagixEndpoint
|
||||||
|
import space.kscience.magix.api.subscribe
|
||||||
|
import space.kscience.magix.rsocket.rSocketWithTcp
|
||||||
|
import space.kscience.magix.rsocket.rSocketWithWebSockets
|
||||||
|
import space.kscience.magix.server.RSocketMagixFlowPlugin
|
||||||
|
import space.kscience.magix.server.startMagixServer
|
||||||
|
import space.kscience.plotly.Plotly
|
||||||
|
import space.kscience.plotly.layout
|
||||||
|
import space.kscience.plotly.plot
|
||||||
|
import space.kscience.plotly.scatter
|
||||||
|
import space.kscience.plotly.server.PlotlyUpdateMode
|
||||||
|
import space.kscience.plotly.server.serve
|
||||||
|
import space.kscience.plotly.server.show
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
class MassDevice(context: Context, meta: Meta) : DeviceBySpec<MassDevice>(MassDevice, context, meta) {
|
||||||
|
private val rng = Random(meta["seed"].int ?: 0)
|
||||||
|
|
||||||
|
private val randomValue get() = rng.nextDouble()
|
||||||
|
|
||||||
|
companion object : DeviceSpec<MassDevice>(), Factory<MassDevice> {
|
||||||
|
|
||||||
|
override fun build(context: Context, meta: Meta): MassDevice = MassDevice(context, meta)
|
||||||
|
|
||||||
|
val value by doubleProperty { randomValue }
|
||||||
|
|
||||||
|
override suspend fun MassDevice.onOpen() {
|
||||||
|
doRecurring(200.milliseconds) {
|
||||||
|
read(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
val context = Context("Mass")
|
||||||
|
|
||||||
|
context.startMagixServer(
|
||||||
|
RSocketMagixFlowPlugin()
|
||||||
|
)
|
||||||
|
|
||||||
|
val numDevices = 1000
|
||||||
|
|
||||||
|
repeat(numDevices) {
|
||||||
|
val deviceContext = Context("Device${it}") {
|
||||||
|
plugin(DeviceManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
val deviceManager = deviceContext.request(DeviceManager)
|
||||||
|
|
||||||
|
deviceManager.install("device$it", MassDevice)
|
||||||
|
|
||||||
|
deviceContext.launch(Dispatchers.Default) {
|
||||||
|
val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
|
||||||
|
deviceManager.connectToMagix(deviceEndpoint, "device$it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val application = Plotly.serve(port = 9091, scope = context) {
|
||||||
|
updateMode = PlotlyUpdateMode.PUSH
|
||||||
|
updateInterval = 1000
|
||||||
|
page { container ->
|
||||||
|
plot(renderer = container) {
|
||||||
|
layout {
|
||||||
|
title = "Latest event"
|
||||||
|
}
|
||||||
|
scatter {
|
||||||
|
launch(Dispatchers.Default) {
|
||||||
|
val monitorEndpoint = MagixEndpoint.rSocketWithTcp("localhost")
|
||||||
|
|
||||||
|
val latest = ConcurrentHashMap<String, Instant>()
|
||||||
|
|
||||||
|
monitorEndpoint.subscribe(controlsMagixFormat).onEach { (magixMessage, payload) ->
|
||||||
|
latest[magixMessage.origin] = payload.time ?: Clock.System.now()
|
||||||
|
}.launchIn(this)
|
||||||
|
|
||||||
|
while (isActive) {
|
||||||
|
delay(1000)
|
||||||
|
val now = Clock.System.now()
|
||||||
|
x.strings = latest.keys
|
||||||
|
y.numbers = latest.values.map { now.minus(it).inWholeMilliseconds / 1000.0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
application.show()
|
||||||
|
|
||||||
|
while (readlnOrNull().isNullOrBlank()) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -19,9 +19,7 @@ public interface MagixEndpoint {
|
|||||||
/**
|
/**
|
||||||
* Send an event
|
* Send an event
|
||||||
*/
|
*/
|
||||||
public suspend fun broadcast(
|
public suspend fun broadcast(message: MagixMessage)
|
||||||
message: MagixMessage,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the endpoint and the associated connection if it exists
|
* Close the endpoint and the associated connection if it exists
|
||||||
|
@ -10,6 +10,12 @@ public data class MagixMessageFilter(
|
|||||||
val origin: Collection<String?>? = null,
|
val origin: Collection<String?>? = null,
|
||||||
val target: Collection<String?>? = null,
|
val target: Collection<String?>? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
public fun accepts(message: MagixMessage): Boolean =
|
||||||
|
format?.contains(message.format) ?: true
|
||||||
|
&& origin?.contains(message.origin) ?: true
|
||||||
|
&& target?.contains(message.target) ?: true
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
public val ALL: MagixMessageFilter = MagixMessageFilter()
|
public val ALL: MagixMessageFilter = MagixMessageFilter()
|
||||||
}
|
}
|
||||||
@ -22,9 +28,5 @@ public fun Flow<MagixMessage>.filter(filter: MagixMessageFilter): Flow<MagixMess
|
|||||||
if (filter == MagixMessageFilter.ALL) {
|
if (filter == MagixMessageFilter.ALL) {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
return filter { message ->
|
return filter(filter::accepts)
|
||||||
filter.format?.contains(message.format) ?: true
|
|
||||||
&& filter.origin?.contains(message.origin) ?: true
|
|
||||||
&& filter.target?.contains(message.target) ?: true
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -18,22 +18,20 @@ import space.kscience.magix.api.MagixEndpoint
|
|||||||
import space.kscience.magix.api.MagixMessage
|
import space.kscience.magix.api.MagixMessage
|
||||||
import space.kscience.magix.api.MagixMessageFilter
|
import space.kscience.magix.api.MagixMessageFilter
|
||||||
import space.kscience.magix.api.filter
|
import space.kscience.magix.api.filter
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
public class RSocketMagixEndpoint(
|
public class RSocketMagixEndpoint(private val rSocket: RSocket) : MagixEndpoint, Closeable {
|
||||||
private val rSocket: RSocket,
|
|
||||||
private val coroutineContext: CoroutineContext,
|
|
||||||
) : MagixEndpoint, Closeable {
|
|
||||||
|
|
||||||
override fun subscribe(
|
override fun subscribe(
|
||||||
filter: MagixMessageFilter,
|
filter: MagixMessageFilter,
|
||||||
): Flow<MagixMessage> {
|
): Flow<MagixMessage> {
|
||||||
val payload = buildPayload { data(MagixEndpoint.magixJson.encodeToString(MagixMessageFilter.serializer(), filter)) }
|
val payload = buildPayload {
|
||||||
|
data(MagixEndpoint.magixJson.encodeToString(MagixMessageFilter.serializer(), filter))
|
||||||
|
}
|
||||||
val flow = rSocket.requestStream(payload)
|
val flow = rSocket.requestStream(payload)
|
||||||
return flow.map {
|
return flow.map {
|
||||||
MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), it.data.readText())
|
MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), it.data.readText())
|
||||||
}.filter(filter).flowOn(coroutineContext[CoroutineDispatcher] ?: Dispatchers.Unconfined)
|
}.filter(filter).flowOn(rSocket.coroutineContext[CoroutineDispatcher] ?: Dispatchers.Unconfined)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun broadcast(message: MagixMessage): Unit = withContext(coroutineContext) {
|
override suspend fun broadcast(message: MagixMessage): Unit = withContext(coroutineContext) {
|
||||||
@ -80,5 +78,5 @@ public suspend fun MagixEndpoint.Companion.rSocketWithWebSockets(
|
|||||||
client.close()
|
client.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return RSocketMagixEndpoint(rSocket, coroutineContext)
|
return RSocketMagixEndpoint(rSocket)
|
||||||
}
|
}
|
@ -26,7 +26,7 @@ import kotlin.coroutines.CoroutineContext
|
|||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RSocket endpoint based on established channel. This way it works a bit faster than [RSocketMagixEndpoint]
|
* RSocket endpoint based on an established channel. This way it works a bit faster than [RSocketMagixEndpoint]
|
||||||
* for sending and receiving, but less flexible in terms of filters. One general [streamFilter] could be set
|
* for sending and receiving, but less flexible in terms of filters. One general [streamFilter] could be set
|
||||||
* in constructor and applied on the loop side. Filters in [subscribe] are applied on the endpoint side on top
|
* in constructor and applied on the loop side. Filters in [subscribe] are applied on the endpoint side on top
|
||||||
* of received data.
|
* of received data.
|
||||||
|
@ -23,7 +23,7 @@ public suspend fun MagixEndpoint.Companion.rSocketWithTcp(
|
|||||||
)
|
)
|
||||||
val rSocket = buildConnector(rSocketConfig).connect(transport)
|
val rSocket = buildConnector(rSocketConfig).connect(transport)
|
||||||
|
|
||||||
return RSocketMagixEndpoint(rSocket, coroutineContext)
|
return RSocketMagixEndpoint(rSocket)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ public class RSocketMagixFlowPlugin(public val port: Int = DEFAULT_MAGIX_RAW_POR
|
|||||||
val message = MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), request.data.readText())
|
val message = MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), request.data.readText())
|
||||||
magixFlow.emit(message)
|
magixFlow.emit(message)
|
||||||
}
|
}
|
||||||
// bi-directional connection
|
// bidirectional connection, not covered by a standard
|
||||||
requestChannel { request: Payload, input: Flow<Payload> ->
|
requestChannel { request: Payload, input: Flow<Payload> ->
|
||||||
input.onEach {
|
input.onEach {
|
||||||
magixFlow.emit(MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), it.data.readText()))
|
magixFlow.emit(MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), it.data.readText()))
|
||||||
|
@ -62,6 +62,7 @@ include(
|
|||||||
":magix:magix-storage:magix-storage-xodus",
|
":magix:magix-storage:magix-storage-xodus",
|
||||||
":controls-magix-client",
|
":controls-magix-client",
|
||||||
":demo:all-things",
|
":demo:all-things",
|
||||||
|
":demo:many-devices",
|
||||||
":demo:magix-demo",
|
":demo:magix-demo",
|
||||||
":demo:car",
|
":demo:car",
|
||||||
":demo:motors",
|
":demo:motors",
|
||||||
|
Loading…
Reference in New Issue
Block a user