Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright (c) 2022-2024. AxonIQ B.V.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.axoniq.console.framework.client
import io.axoniq.console.framework.api.ClientSettingsV2
import io.axoniq.console.framework.api.Routes
import io.axoniq.console.framework.api.notifications.Notification
import io.axoniq.console.framework.api.notifications.NotificationLevel
import io.axoniq.console.framework.api.notifications.NotificationList
import io.axoniq.console.framework.client.strategy.RSocketPayloadEncodingStrategy
import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.CompositeByteBuf
import io.rsocket.Payload
import io.rsocket.RSocket
import io.rsocket.core.RSocketConnector
import io.rsocket.metadata.WellKnownMimeType
import io.rsocket.transport.netty.client.TcpClientTransport
import org.axonframework.lifecycle.Lifecycle
import org.axonframework.lifecycle.Phase
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono
import reactor.netty.tcp.TcpClient
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import kotlin.math.pow
/**
* The beating heart of the Console client. This class is responsible for connecting to the Console, and keeping
* the connection alive. It will also ensure that the connection is re-established in case of a disconnect.
*
* Establishing a connection works as follows:
* - The client will send a setup payload to the server, containing the authentication information
* - The client will retrieve the settings from the server, and update the [ClientSettingsService] with the new settings
* - The client will start sending heartbeats to the server, and will check if it receives heartbeats from the server
*
* The server is in control of these settings. Of course, the user can manipulate these as well themselves.
* The server should be resilient against this manipulation in the form of rate limiting.
*/
@Suppress("MemberVisibilityCanBePrivate")
class AxoniqConsoleRSocketClient(
private val environmentId: String,
private val accessToken: String,
private val applicationName: String,
private val host: String,
private val port: Int,
private val secure: Boolean,
private val initialDelay: Long,
private val setupPayloadCreator: SetupPayloadCreator,
private val registrar: RSocketHandlerRegistrar,
private val encodingStrategy: RSocketPayloadEncodingStrategy,
private val clientSettingsService: ClientSettingsService,
private val executor: ScheduledExecutorService,
private val nodeName: String,
) : Lifecycle {
private val heartbeatOrchestrator = HeartbeatOrchestrator()
private var maintenanceTask: ScheduledFuture<*>? = null
private val logger = LoggerFactory.getLogger(this::class.java)
private var rsocket: RSocket? = null
private var lastConnectionTry = Instant.EPOCH
private var connectionRetryCount = 0
private var pausedReports = false
init {
clientSettingsService.subscribeToSettings(heartbeatOrchestrator)
// Server can send updated settings if necessary
registrar.registerHandlerWithPayload(Routes.Management.SETTINGS, ClientSettingsV2::class.java) {
clientSettingsService.updateSettings(it)
}
// Server can block and unblock reports
registrar.registerHandlerWithoutPayload(Routes.Management.STOP_REPORTS) {
pausedReports = true
true
}
registrar.registerHandlerWithoutPayload(Routes.Management.START_REPORTS) {
pausedReports = false
true
}
// Server can send log requests
registrar.registerHandlerWithPayload(Routes.Management.LOG, Notification::class.java) {
logger.logNotification(it)
}
}
override fun registerLifecycleHandlers(registry: Lifecycle.LifecycleRegistry) {
registry.onStart(Phase.EXTERNAL_CONNECTIONS, this::start)
registry.onShutdown(Phase.EXTERNAL_CONNECTIONS, this::disposeClient)
}
/**
* Sends a report to AxonIQ Console
*/
fun sendReport(route: String, payload: Any): Mono {
if(pausedReports) {
return Mono.empty()
}
return sendMessage(payload, route)
}
/**
* Sends a message to the AxonIQ Console. If there is no connection active, does nothing silently.
* Do not use this method for reports, as it does not check if reports are paused. Use [sendReport] instead.
*/
fun sendMessage(payload: Any, route: String) = (rsocket
?.requestResponse(encodingStrategy.encode(payload, createRoutingMetadata(route)))
?.map {
val notifications = encodingStrategy.decode(it, NotificationList::class.java)
logger.log(notifications)
}
?: Mono.empty())
/**
* Starts the connection, and starts the maintenance task.
* The task will ensure that if heartbeats are missed the connection is killed, as well as re-setup in case
* the connection was lost. The task will do so with an exponential backoff with factor 2, up to a maximum of
* 60 seconds.
*/
fun start() {
if (this.maintenanceTask != null) {
return
}
this.maintenanceTask = executor.scheduleWithFixedDelay(
this::ensureConnected,
initialDelay,
1000, TimeUnit.MILLISECONDS
)
}
private fun ensureConnected() {
if (!isConnected()) {
val secondsToWaitForReconnect = BACKOFF_FACTOR.pow(connectionRetryCount.toDouble()).coerceAtMost(60.0)
if (ChronoUnit.SECONDS.between(lastConnectionTry, Instant.now()) < secondsToWaitForReconnect) {
return
}
connectionRetryCount += 1
lastConnectionTry = Instant.now()
logger.debug("Connecting to AxonIQ Console...")
connectSafely()
}
}
private fun connectSafely() {
try {
rsocket = createRSocket()
// Fetch the client settings from the server
val settings = retrieveSettings().block()
?: throw IllegalStateException("Could not receive the settings from AxonIQ console!")
clientSettingsService.updateSettings(settings)
logger.info("Connection to AxonIQ Console set up successfully! Settings: $settings")
connectionRetryCount = 0
} catch (e: Exception) {
if (connectionRetryCount == 5) {
logger.error("Failed to connect to AxonIQ Console. Error: ${e.message}. Will keep trying to connect...")
}
disposeCurrentConnection()
logger.debug("Failed to connect to AxonIQ Console", e)
}
}
private fun createRSocket(): RSocket {
val authentication = io.axoniq.console.framework.api.ConsoleClientAuthentication(
identification = io.axoniq.console.framework.api.ConsoleClientIdentifier(
environmentId = environmentId,
applicationName = applicationName,
nodeName = nodeName
),
accessToken = accessToken
)
val setupPayload = encodingStrategy.encode(
setupPayloadCreator.createReport(),
createSetupMetadata(authentication)
)
val rsocket = RSocketConnector.create()
.metadataMimeType(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.string)
.dataMimeType(encodingStrategy.getMimeType().string)
.setupPayload(setupPayload)
.acceptor { _, rsocket ->
Mono.just(registrar.createRespondingRSocketFor(rsocket))
}
.connect(tcpClientTransport())
.block()!!
return rsocket
}
private fun createRoutingMetadata(route: String): CompositeByteBuf {
val metadata: CompositeByteBuf = ByteBufAllocator.DEFAULT.compositeBuffer()
metadata.addRouteMetadata(route)
return metadata
}
private fun createSetupMetadata(auth: io.axoniq.console.framework.api.ConsoleClientAuthentication): CompositeByteBuf {
val metadata: CompositeByteBuf = ByteBufAllocator.DEFAULT.compositeBuffer()
metadata.addRouteMetadata("client")
metadata.addAuthMetadata(auth)
return metadata
}
private fun tcpClientTransport() =
TcpClientTransport.create(tcpClient())
private fun tcpClient(): TcpClient {
val client = TcpClient.create()
.host(host)
.port(port)
.doOnDisconnected {
disposeCurrentConnection()
}
return if (secure) {
return client.secure()
} else client
}
fun isConnected() = rsocket != null
fun disposeCurrentConnection() {
rsocket?.dispose()
rsocket = null
clientSettingsService.clearSettings()
}
fun disposeClient() {
disposeCurrentConnection()
maintenanceTask?.cancel(true)
maintenanceTask = null
}
companion object {
private const val BACKOFF_FACTOR = 2.0
}
private inner class HeartbeatOrchestrator : ClientSettingsObserver {
private var heartbeatSendTask: ScheduledFuture<*>? = null
private var heartbeatCheckTask: ScheduledFuture<*>? = null
private var lastReceivedHeartbeat = Instant.now()
init {
registrar.registerHandlerWithoutPayload(Routes.Management.HEARTBEAT) {
logger.debug("Received heartbeat from AxonIQ Console. Last one was: {}", lastReceivedHeartbeat)
lastReceivedHeartbeat = Instant.now()
lastReceivedHeartbeat
}
}
override fun onConnectedWithSettings(settings: ClientSettingsV2) {
lastReceivedHeartbeat = Instant.now()
this.heartbeatSendTask = executor.scheduleWithFixedDelay(
{ sendHeartbeat().subscribe() },
0,
settings.heartbeatInterval,
TimeUnit.MILLISECONDS
)
this.heartbeatCheckTask = executor.scheduleWithFixedDelay(
{ checkHeartbeats(settings.heartbeatTimeout) },
0,
1000,
TimeUnit.MILLISECONDS
)
}
override fun onDisconnected() {
logger.info("This application has lost it's connection to AxonIQ Console. Reconnection will be automatically attempted.")
this.heartbeatSendTask?.cancel(true)
this.heartbeatCheckTask?.cancel(true)
}
private fun checkHeartbeats(heartbeatTimeout: Long) {
if (lastReceivedHeartbeat < Instant.now().minusMillis(heartbeatTimeout)) {
logger.debug("Haven't received a heartbeat for {} seconds from AxonIQ Console. Reconnecting...", ChronoUnit.SECONDS.between(lastReceivedHeartbeat, Instant.now()))
disposeCurrentConnection()
}
}
private fun sendHeartbeat(): Mono {
return rsocket
?.requestResponse(encodingStrategy.encode("", createRoutingMetadata(Routes.Management.HEARTBEAT)))
?.doOnSuccess {
logger.debug("Heartbeat successfully sent to AxonIQ Console")
}
?: Mono.empty()
}
}
private fun retrieveSettings(): Mono {
return rsocket!!
.requestResponse(encodingStrategy.encode("", createRoutingMetadata(Routes.Management.SETTINGS_V2)))
.map {
encodingStrategy.decode(it, ClientSettingsV2::class.java)
}
.doOnError {
if (it.message?.contains("Access Denied") == true) {
logger.error("Was unable to send call to AxonIQ Console since authentication was incorrect!")
}
}
}
private fun Logger.log(notificationList: NotificationList) {
notificationList.messages.forEach {
logNotification(it)
}
}
private fun Logger.logNotification(it: Notification) {
val text = it.message
when (it.level) {
NotificationLevel.Debug -> this.debug(text)
NotificationLevel.Info -> this.info(text)
NotificationLevel.Warn -> this.warn(text)
}
}
}