mirror of
https://github.com/corda/corda.git
synced 2025-06-06 09:21:47 +00:00
Thread safety and messaging bug fixes.
* Use the new AffinityExecutor code to fix some thread affinity issues where callbacks were running on the wrong threads. Add affinity assertions. * Remove sleeps from UpdateBusinessDayProtocol. * Remove a one-shot message handler before the callback is executed. * Store un-routed messages in memory in ArtemisMessagingService to fix handler registration/message races. This is a temporary kludge until we use Artemis/MQ better.
This commit is contained in:
parent
63b8579669
commit
746aca8290
@ -69,8 +69,8 @@ interface MessagingService {
|
|||||||
*/
|
*/
|
||||||
fun MessagingService.runOnNextMessage(topic: String = "", executor: Executor? = null, callback: (Message) -> Unit) {
|
fun MessagingService.runOnNextMessage(topic: String = "", executor: Executor? = null, callback: (Message) -> Unit) {
|
||||||
addMessageHandler(topic, executor) { msg, reg ->
|
addMessageHandler(topic, executor) { msg, reg ->
|
||||||
callback(msg)
|
|
||||||
removeMessageHandler(reg)
|
removeMessageHandler(reg)
|
||||||
|
callback(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import core.serialization.THREAD_LOCAL_KRYO
|
|||||||
import core.serialization.createKryo
|
import core.serialization.createKryo
|
||||||
import core.serialization.deserialize
|
import core.serialization.deserialize
|
||||||
import core.serialization.serialize
|
import core.serialization.serialize
|
||||||
|
import core.utilities.AffinityExecutor
|
||||||
import core.utilities.ProgressTracker
|
import core.utilities.ProgressTracker
|
||||||
import core.utilities.trace
|
import core.utilities.trace
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
@ -24,7 +25,6 @@ import org.slf4j.LoggerFactory
|
|||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.Executor
|
|
||||||
import javax.annotation.concurrent.ThreadSafe
|
import javax.annotation.concurrent.ThreadSafe
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,6 +38,9 @@ import javax.annotation.concurrent.ThreadSafe
|
|||||||
* A "state machine" is a class with a single call method. The call method and any others it invokes are rewritten by
|
* A "state machine" is a class with a single call method. The call method and any others it invokes are rewritten by
|
||||||
* a bytecode rewriting engine called Quasar, to ensure the code can be suspended and resumed at any point.
|
* a bytecode rewriting engine called Quasar, to ensure the code can be suspended and resumed at any point.
|
||||||
*
|
*
|
||||||
|
* The SMM will always invoke the protocol fibers on the given [AffinityExecutor], regardless of which thread actually
|
||||||
|
* starts them via [add].
|
||||||
|
*
|
||||||
* TODO: Session IDs should be set up and propagated automatically, on demand.
|
* TODO: Session IDs should be set up and propagated automatically, on demand.
|
||||||
* TODO: Consider the issue of continuation identity more deeply: is it a safe assumption that a serialised
|
* TODO: Consider the issue of continuation identity more deeply: is it a safe assumption that a serialised
|
||||||
* continuation is always unique?
|
* continuation is always unique?
|
||||||
@ -50,7 +53,7 @@ import javax.annotation.concurrent.ThreadSafe
|
|||||||
* TODO: Implement stub/skel classes that provide a basic RPC framework on top of this.
|
* TODO: Implement stub/skel classes that provide a basic RPC framework on top of this.
|
||||||
*/
|
*/
|
||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) {
|
class StateMachineManager(val serviceHub: ServiceHub, val executor: AffinityExecutor) {
|
||||||
// This map is backed by a database and will be used to store serialised state machines to disk, so we can resurrect
|
// This map is backed by a database and will be used to store serialised state machines to disk, so we can resurrect
|
||||||
// them across node restarts.
|
// them across node restarts.
|
||||||
private val checkpointsMap = serviceHub.storageService.stateMachines
|
private val checkpointsMap = serviceHub.storageService.stateMachines
|
||||||
@ -114,7 +117,7 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor)
|
|||||||
val topic = checkpoint.awaitingTopic
|
val topic = checkpoint.awaitingTopic
|
||||||
|
|
||||||
// And now re-wire the deserialised continuation back up to the network service.
|
// And now re-wire the deserialised continuation back up to the network service.
|
||||||
serviceHub.networkService.runOnNextMessage(topic, runInThread) { netMsg ->
|
serviceHub.networkService.runOnNextMessage(topic, executor) { netMsg ->
|
||||||
// TODO: See security note below.
|
// TODO: See security note below.
|
||||||
val obj: Any = THREAD_LOCAL_KRYO.get().readClassAndObject(Input(netMsg.data))
|
val obj: Any = THREAD_LOCAL_KRYO.get().readClassAndObject(Input(netMsg.data))
|
||||||
if (!awaitingObjectOfType.isInstance(obj))
|
if (!awaitingObjectOfType.isInstance(obj))
|
||||||
@ -154,15 +157,22 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor)
|
|||||||
* restarted with checkpointed state machines in the storage service.
|
* restarted with checkpointed state machines in the storage service.
|
||||||
*/
|
*/
|
||||||
fun <T> add(loggerName: String, logic: ProtocolLogic<T>): ListenableFuture<T> {
|
fun <T> add(loggerName: String, logic: ProtocolLogic<T>): ListenableFuture<T> {
|
||||||
val logger = LoggerFactory.getLogger(loggerName)
|
try {
|
||||||
val fiber = ProtocolStateMachine(logic)
|
val logger = LoggerFactory.getLogger(loggerName)
|
||||||
// Need to add before iterating in case of immediate completion
|
val fiber = ProtocolStateMachine(logic)
|
||||||
_stateMachines.add(logic)
|
// Need to add before iterating in case of immediate completion
|
||||||
iterateStateMachine(fiber, serviceHub.networkService, logger, null, null) {
|
_stateMachines.add(logic)
|
||||||
it.start()
|
executor.executeASAP {
|
||||||
|
iterateStateMachine(fiber, serviceHub.networkService, logger, null, null) {
|
||||||
|
it.start()
|
||||||
|
}
|
||||||
|
totalStartedProtocols.inc()
|
||||||
|
}
|
||||||
|
return fiber.resultFuture
|
||||||
|
} catch(e: Throwable) {
|
||||||
|
e.printStackTrace()
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
totalStartedProtocols.inc()
|
|
||||||
return fiber.resultFuture
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun persistCheckpoint(prevCheckpointKey: SecureHash?, new: ByteArray): SecureHash {
|
private fun persistCheckpoint(prevCheckpointKey: SecureHash?, new: ByteArray): SecureHash {
|
||||||
@ -178,12 +188,12 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor)
|
|||||||
|
|
||||||
private fun iterateStateMachine(psm: ProtocolStateMachine<*>, net: MessagingService, logger: Logger,
|
private fun iterateStateMachine(psm: ProtocolStateMachine<*>, net: MessagingService, logger: Logger,
|
||||||
obj: Any?, prevCheckpointKey: SecureHash?, resumeFunc: (ProtocolStateMachine<*>) -> Unit) {
|
obj: Any?, prevCheckpointKey: SecureHash?, resumeFunc: (ProtocolStateMachine<*>) -> Unit) {
|
||||||
|
executor.checkOnThread()
|
||||||
val onSuspend = fun(request: FiberRequest, serFiber: ByteArray) {
|
val onSuspend = fun(request: FiberRequest, serFiber: ByteArray) {
|
||||||
// We have a request to do something: send, receive, or send-and-receive.
|
// We have a request to do something: send, receive, or send-and-receive.
|
||||||
if (request is FiberRequest.ExpectingResponse<*>) {
|
if (request is FiberRequest.ExpectingResponse<*>) {
|
||||||
// Prepare a listener on the network that runs in the background thread when we received a message.
|
// Prepare a listener on the network that runs in the background thread when we received a message.
|
||||||
checkpointAndSetupMessageHandler(logger, net, psm, request.responseType,
|
checkpointAndSetupMessageHandler(logger, net, psm, request, prevCheckpointKey, serFiber)
|
||||||
"${request.topic}.${request.sessionIDForReceive}", prevCheckpointKey, serFiber)
|
|
||||||
}
|
}
|
||||||
// If an object to send was provided (not null), send it now.
|
// If an object to send was provided (not null), send it now.
|
||||||
request.obj?.let {
|
request.obj?.let {
|
||||||
@ -217,13 +227,22 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor)
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkpointAndSetupMessageHandler(logger: Logger, net: MessagingService, psm: ProtocolStateMachine<*>,
|
private fun checkpointAndSetupMessageHandler(logger: Logger, net: MessagingService, psm: ProtocolStateMachine<*>,
|
||||||
responseType: Class<*>, topic: String, prevCheckpointKey: SecureHash?,
|
request: FiberRequest.ExpectingResponse<*>, prevCheckpointKey: SecureHash?,
|
||||||
serialisedFiber: ByteArray) {
|
serialisedFiber: ByteArray) {
|
||||||
val checkpoint = Checkpoint(serialisedFiber, logger.name, topic, responseType.name)
|
executor.checkOnThread()
|
||||||
|
val topic = "${request.topic}.${request.sessionIDForReceive}"
|
||||||
|
val checkpoint = Checkpoint(serialisedFiber, logger.name, topic, request.responseType.name)
|
||||||
val curPersistedBytes = checkpoint.serialize().bits
|
val curPersistedBytes = checkpoint.serialize().bits
|
||||||
persistCheckpoint(prevCheckpointKey, curPersistedBytes)
|
persistCheckpoint(prevCheckpointKey, curPersistedBytes)
|
||||||
val newCheckpointKey = curPersistedBytes.sha256()
|
val newCheckpointKey = curPersistedBytes.sha256()
|
||||||
net.runOnNextMessage(topic, runInThread) { netMsg ->
|
logger.trace { "Waiting for message of type ${request.responseType.name} on $topic" }
|
||||||
|
var consumed = false
|
||||||
|
net.runOnNextMessage(topic, executor) { netMsg ->
|
||||||
|
// Some assertions to ensure we don't execute on the wrong thread or get executed more than once.
|
||||||
|
executor.checkOnThread()
|
||||||
|
check(netMsg.topic == topic) { "Topic mismatch: ${netMsg.topic} vs $topic" }
|
||||||
|
check(!consumed)
|
||||||
|
consumed = true
|
||||||
// TODO: This is insecure: we should not deserialise whatever we find and *then* check.
|
// TODO: This is insecure: we should not deserialise whatever we find and *then* check.
|
||||||
//
|
//
|
||||||
// We should instead verify as we read the data that it's what we are expecting and throw as early as
|
// We should instead verify as we read the data that it's what we are expecting and throw as early as
|
||||||
@ -232,9 +251,8 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor)
|
|||||||
// at the last moment when we do the downcast. However this would make protocol code harder to read and
|
// at the last moment when we do the downcast. However this would make protocol code harder to read and
|
||||||
// make it more difficult to migrate to a more explicit serialisation scheme later.
|
// make it more difficult to migrate to a more explicit serialisation scheme later.
|
||||||
val obj: Any = THREAD_LOCAL_KRYO.get().readClassAndObject(Input(netMsg.data))
|
val obj: Any = THREAD_LOCAL_KRYO.get().readClassAndObject(Input(netMsg.data))
|
||||||
if (!responseType.isInstance(obj))
|
if (!request.responseType.isInstance(obj))
|
||||||
throw ClassCastException("Expected message of type ${responseType.name} but got ${obj.javaClass.name}")
|
throw IllegalStateException("Expected message of type ${request.responseType.name} but got ${obj.javaClass.name}", request.stackTraceInCaseOfProblems)
|
||||||
logger.trace { "<- $topic : message of type ${obj.javaClass.name}" }
|
|
||||||
iterateStateMachine(psm, net, logger, obj, newCheckpointKey) {
|
iterateStateMachine(psm, net, logger, obj, newCheckpointKey) {
|
||||||
try {
|
try {
|
||||||
Fiber.unpark(it, QUASAR_UNBLOCKER)
|
Fiber.unpark(it, QUASAR_UNBLOCKER)
|
||||||
@ -245,11 +263,16 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Override more of this to avoid the case where Strand.sleep triggers a call to a scheduler that then runs on the wrong thread.
|
||||||
object SameThreadFiberScheduler : FiberExecutorScheduler("Same thread scheduler", MoreExecutors.directExecutor())
|
object SameThreadFiberScheduler : FiberExecutorScheduler("Same thread scheduler", MoreExecutors.directExecutor())
|
||||||
|
|
||||||
// TODO: Clean this up
|
// TODO: Clean this up
|
||||||
open class FiberRequest(val topic: String, val destination: MessageRecipients?,
|
open class FiberRequest(val topic: String, val destination: MessageRecipients?,
|
||||||
val sessionIDForSend: Long, val sessionIDForReceive: Long, val obj: Any?) {
|
val sessionIDForSend: Long, val sessionIDForReceive: Long, val obj: Any?) {
|
||||||
|
// This is used to identify where we suspended, in case of message mismatch errors and other things where we
|
||||||
|
// don't have the original stack trace because it's in a suspended fiber.
|
||||||
|
val stackTraceInCaseOfProblems = StackSnapshot()
|
||||||
|
|
||||||
class ExpectingResponse<R : Any>(
|
class ExpectingResponse<R : Any>(
|
||||||
topic: String,
|
topic: String,
|
||||||
destination: MessageRecipients?,
|
destination: MessageRecipients?,
|
||||||
@ -266,4 +289,7 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor)
|
|||||||
obj: Any?
|
obj: Any?
|
||||||
) : FiberRequest(topic, destination, sessionIDForSend, -1, obj)
|
) : FiberRequest(topic, destination, sessionIDForSend, -1, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StackSnapshot : Throwable("This is a stack trace to help identify the source of the underlying problem")
|
@ -3,10 +3,7 @@ package core.node
|
|||||||
import api.APIServer
|
import api.APIServer
|
||||||
import api.APIServerImpl
|
import api.APIServerImpl
|
||||||
import com.codahale.metrics.MetricRegistry
|
import com.codahale.metrics.MetricRegistry
|
||||||
import contracts.*
|
|
||||||
import core.Contract
|
|
||||||
import core.Party
|
import core.Party
|
||||||
import core.crypto.SecureHash
|
|
||||||
import core.crypto.generateKeyPair
|
import core.crypto.generateKeyPair
|
||||||
import core.messaging.MessagingService
|
import core.messaging.MessagingService
|
||||||
import core.messaging.StateMachineManager
|
import core.messaging.StateMachineManager
|
||||||
@ -14,6 +11,7 @@ import core.node.services.*
|
|||||||
import core.serialization.deserialize
|
import core.serialization.deserialize
|
||||||
import core.serialization.serialize
|
import core.serialization.serialize
|
||||||
import core.testing.MockNetworkMapCache
|
import core.testing.MockNetworkMapCache
|
||||||
|
import core.utilities.AffinityExecutor
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import java.nio.file.FileAlreadyExistsException
|
import java.nio.file.FileAlreadyExistsException
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
@ -22,7 +20,6 @@ import java.security.KeyPair
|
|||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A base node implementation that can be customised either for production (with real implementations that do real
|
* A base node implementation that can be customised either for production (with real implementations that do real
|
||||||
@ -36,9 +33,9 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
|||||||
|
|
||||||
protected abstract val log: Logger
|
protected abstract val log: Logger
|
||||||
|
|
||||||
// We will run as much stuff in this thread as possible to keep the risk of thread safety bugs low during the
|
// We will run as much stuff in this single thread as possible to keep the risk of thread safety bugs low during the
|
||||||
// low-performance prototyping period.
|
// low-performance prototyping period.
|
||||||
protected open val serverThread = Executors.newSingleThreadExecutor()
|
protected open val serverThread: AffinityExecutor = AffinityExecutor.ServiceAffinityExecutor("Node thread", 1)
|
||||||
|
|
||||||
// Objects in this list will be scanned by the DataUploadServlet and can be handed new data via HTTP.
|
// Objects in this list will be scanned by the DataUploadServlet and can be handed new data via HTTP.
|
||||||
// Don't mutate this after startup.
|
// Don't mutate this after startup.
|
||||||
|
@ -58,9 +58,11 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
|
|||||||
// when our process shuts down, but we try in stop() anyway just to be nice.
|
// when our process shuts down, but we try in stop() anyway just to be nice.
|
||||||
private var nodeFileLock: FileLock? = null
|
private var nodeFileLock: FileLock? = null
|
||||||
|
|
||||||
override fun makeMessagingService(): MessagingService = ArtemisMessagingService(dir, p2pAddr)
|
override fun makeMessagingService(): MessagingService = ArtemisMessagingService(dir, p2pAddr, serverThread)
|
||||||
|
|
||||||
private fun initWebServer(): Server {
|
private fun initWebServer(): Server {
|
||||||
|
// Note that the web server handlers will all run concurrently, and not on the node thread.
|
||||||
|
|
||||||
val port = p2pAddr.port + 1 // TODO: Move this into the node config file.
|
val port = p2pAddr.port + 1 // TODO: Move this into the node config file.
|
||||||
val server = Server(port)
|
val server = Server(port)
|
||||||
|
|
||||||
|
@ -44,9 +44,14 @@ import javax.annotation.concurrent.ThreadSafe
|
|||||||
* The current implementation is skeletal and lacks features like security or firewall tunnelling (that is, you must
|
* The current implementation is skeletal and lacks features like security or firewall tunnelling (that is, you must
|
||||||
* be able to receive TCP connections in order to receive messages). It is good enough for local communication within
|
* be able to receive TCP connections in order to receive messages). It is good enough for local communication within
|
||||||
* a fully connected network, trusted network or on localhost.
|
* a fully connected network, trusted network or on localhost.
|
||||||
|
*
|
||||||
|
* @param directory A place where Artemis can stash its message journal and other files.
|
||||||
|
* @param myHostPort What host and port to bind to for receiving inbound connections.
|
||||||
|
* @param defaultExecutor This will be used as the default executor to run message handlers on, if no other is specified.
|
||||||
*/
|
*/
|
||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort) : MessagingService {
|
class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort,
|
||||||
|
val defaultExecutor: Executor = RunOnCallerThread) : MessagingService {
|
||||||
// In future: can contain onion routing info, etc.
|
// In future: can contain onion routing info, etc.
|
||||||
private data class Address(val hostAndPort: HostAndPort) : SingleMessageRecipient
|
private data class Address(val hostAndPort: HostAndPort) : SingleMessageRecipient
|
||||||
|
|
||||||
@ -83,6 +88,9 @@ class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort)
|
|||||||
|
|
||||||
private val handlers = CopyOnWriteArrayList<Handler>()
|
private val handlers = CopyOnWriteArrayList<Handler>()
|
||||||
|
|
||||||
|
// TODO: This is not robust and needs to be replaced by more intelligently using the message queue server.
|
||||||
|
private val undeliveredMessages = CopyOnWriteArrayList<Message>()
|
||||||
|
|
||||||
private fun getSendClient(addr: Address): ClientProducer {
|
private fun getSendClient(addr: Address): ClientProducer {
|
||||||
return mutex.locked {
|
return mutex.locked {
|
||||||
sendClients.getOrPut(addr) {
|
sendClients.getOrPut(addr) {
|
||||||
@ -131,20 +139,10 @@ class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort)
|
|||||||
// This code runs for every inbound message.
|
// This code runs for every inbound message.
|
||||||
try {
|
try {
|
||||||
if (!message.containsProperty(TOPIC_PROPERTY)) {
|
if (!message.containsProperty(TOPIC_PROPERTY)) {
|
||||||
log.warn("Received message without a ${TOPIC_PROPERTY} property, ignoring")
|
log.warn("Received message without a $TOPIC_PROPERTY property, ignoring")
|
||||||
return@setMessageHandler
|
return@setMessageHandler
|
||||||
}
|
}
|
||||||
val topic = message.getStringProperty(TOPIC_PROPERTY)
|
val topic = message.getStringProperty(TOPIC_PROPERTY)
|
||||||
// Because handlers is a COW list, the loop inside filter will operate on a snapshot. Handlers being added
|
|
||||||
// or removed whilst the filter is executing will not affect anything.
|
|
||||||
val deliverTo = handlers.filter { if (it.topic.isBlank()) true else it.topic == topic }
|
|
||||||
|
|
||||||
if (deliverTo.isEmpty()) {
|
|
||||||
// This should probably be downgraded to a trace in future, so the protocol can evolve with new topics
|
|
||||||
// without causing log spam.
|
|
||||||
log.warn("Received message for $topic that doesn't have any registered handlers.")
|
|
||||||
return@setMessageHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
val bits = ByteArray(message.bodySize)
|
val bits = ByteArray(message.bodySize)
|
||||||
message.bodyBuffer.readBytes(bits)
|
message.bodyBuffer.readBytes(bits)
|
||||||
@ -156,15 +154,8 @@ class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort)
|
|||||||
override val debugMessageID: String = message.messageID.toString()
|
override val debugMessageID: String = message.messageID.toString()
|
||||||
override fun serialise(): ByteArray = bits
|
override fun serialise(): ByteArray = bits
|
||||||
}
|
}
|
||||||
for (handler in deliverTo) {
|
|
||||||
(handler.executor ?: RunOnCallerThread).execute {
|
deliverMessage(msg)
|
||||||
try {
|
|
||||||
handler.callback(msg, handler)
|
|
||||||
} catch(e: Exception) {
|
|
||||||
log.error("Caught exception whilst executing message handler for $topic", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
message.acknowledge()
|
message.acknowledge()
|
||||||
}
|
}
|
||||||
@ -174,6 +165,36 @@ class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort)
|
|||||||
mutex.locked { running = true }
|
mutex.locked { running = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun deliverMessage(msg: Message): Boolean {
|
||||||
|
// Because handlers is a COW list, the loop inside filter will operate on a snapshot. Handlers being added
|
||||||
|
// or removed whilst the filter is executing will not affect anything.
|
||||||
|
val deliverTo = handlers.filter { if (it.topic.isBlank()) true else it.topic == msg.topic }
|
||||||
|
|
||||||
|
if (deliverTo.isEmpty()) {
|
||||||
|
// This should probably be downgraded to a trace in future, so the protocol can evolve with new topics
|
||||||
|
// without causing log spam.
|
||||||
|
log.warn("Received message for ${msg.topic} that doesn't have any registered handlers yet")
|
||||||
|
|
||||||
|
// This is a hack; transient messages held in memory isn't crash resistant.
|
||||||
|
// TODO: Use Artemis API more effectively so we don't pop messages off a queue that we aren't ready to use.
|
||||||
|
undeliveredMessages += msg
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (handler in deliverTo) {
|
||||||
|
(handler.executor ?: defaultExecutor).execute {
|
||||||
|
try {
|
||||||
|
handler.callback(msg, handler)
|
||||||
|
} catch(e: Exception) {
|
||||||
|
log.error("Caught exception whilst executing message handler for ${msg.topic}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
mutex.locked {
|
mutex.locked {
|
||||||
for (producer in sendClients.values)
|
for (producer in sendClients.values)
|
||||||
@ -200,6 +221,7 @@ class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort)
|
|||||||
callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration {
|
callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration {
|
||||||
val handler = Handler(executor, topic, callback)
|
val handler = Handler(executor, topic, callback)
|
||||||
handlers.add(handler)
|
handlers.add(handler)
|
||||||
|
undeliveredMessages.removeIf { deliverMessage(it) }
|
||||||
return handler
|
return handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package core.testing
|
package core.testing
|
||||||
|
|
||||||
import com.google.common.jimfs.Jimfs
|
import com.google.common.jimfs.Jimfs
|
||||||
import com.google.common.util.concurrent.MoreExecutors
|
|
||||||
import core.Party
|
import core.Party
|
||||||
import core.messaging.MessagingService
|
import core.messaging.MessagingService
|
||||||
import core.messaging.SingleMessageRecipient
|
import core.messaging.SingleMessageRecipient
|
||||||
@ -12,14 +11,13 @@ import core.node.PhysicalLocation
|
|||||||
import core.testing.MockIdentityService
|
import core.testing.MockIdentityService
|
||||||
import core.node.services.ServiceType
|
import core.node.services.ServiceType
|
||||||
import core.node.services.TimestamperService
|
import core.node.services.TimestamperService
|
||||||
|
import core.utilities.AffinityExecutor
|
||||||
import core.utilities.loggerFor
|
import core.utilities.loggerFor
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ExecutorService
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing.
|
* A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing.
|
||||||
@ -61,11 +59,11 @@ class MockNetwork(private val threadPerNode: Boolean = false,
|
|||||||
open class MockNode(dir: Path, config: NodeConfiguration, val mockNet: MockNetwork,
|
open class MockNode(dir: Path, config: NodeConfiguration, val mockNet: MockNetwork,
|
||||||
withTimestamper: NodeInfo?, val id: Int) : AbstractNode(dir, config, withTimestamper, Clock.systemUTC()) {
|
withTimestamper: NodeInfo?, val id: Int) : AbstractNode(dir, config, withTimestamper, Clock.systemUTC()) {
|
||||||
override val log: Logger = loggerFor<MockNode>()
|
override val log: Logger = loggerFor<MockNode>()
|
||||||
override val serverThread: ExecutorService =
|
override val serverThread: AffinityExecutor =
|
||||||
if (mockNet.threadPerNode)
|
if (mockNet.threadPerNode)
|
||||||
Executors.newSingleThreadExecutor()
|
AffinityExecutor.ServiceAffinityExecutor("Mock node thread", 1)
|
||||||
else
|
else
|
||||||
MoreExecutors.newDirectExecutorService()
|
AffinityExecutor.SAME_THREAD
|
||||||
|
|
||||||
// We only need to override the messaging service here, as currently everything that hits disk does so
|
// We only need to override the messaging service here, as currently everything that hits disk does so
|
||||||
// through the java.nio API which we are already mocking via Jimfs.
|
// through the java.nio API which we are already mocking via Jimfs.
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package core.utilities
|
package core.utilities
|
||||||
|
|
||||||
import com.google.common.base.Preconditions.checkState
|
|
||||||
import com.google.common.util.concurrent.Uninterruptibles
|
import com.google.common.util.concurrent.Uninterruptibles
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -16,10 +15,18 @@ interface AffinityExecutor : Executor {
|
|||||||
val isOnThread: Boolean
|
val isOnThread: Boolean
|
||||||
|
|
||||||
/** Throws an IllegalStateException if the current thread is equal to the thread this executor is backed by. */
|
/** Throws an IllegalStateException if the current thread is equal to the thread this executor is backed by. */
|
||||||
fun checkOnThread()
|
fun checkOnThread() {
|
||||||
|
if (!isOnThread)
|
||||||
|
throw IllegalStateException("On wrong thread: " + Thread.currentThread())
|
||||||
|
}
|
||||||
|
|
||||||
/** If isOnThread() then runnable is invoked immediately, otherwise the closure is queued onto the backing thread. */
|
/** If isOnThread() then runnable is invoked immediately, otherwise the closure is queued onto the backing thread. */
|
||||||
fun executeASAP(runnable: () -> Unit)
|
fun executeASAP(runnable: () -> Unit) {
|
||||||
|
if (isOnThread)
|
||||||
|
runnable()
|
||||||
|
else
|
||||||
|
execute(runnable)
|
||||||
|
}
|
||||||
|
|
||||||
/** Terminates any backing thread (pool) without waiting for tasks to finish. */
|
/** Terminates any backing thread (pool) without waiting for tasks to finish. */
|
||||||
fun shutdownNow()
|
fun shutdownNow()
|
||||||
@ -35,43 +42,11 @@ interface AffinityExecutor : Executor {
|
|||||||
return CompletableFuture.supplyAsync(Supplier { fetcher() }, this).get()
|
return CompletableFuture.supplyAsync(Supplier { fetcher() }, this).get()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class BaseAffinityExecutor protected constructor() : AffinityExecutor {
|
|
||||||
protected val exceptionHandler: Thread.UncaughtExceptionHandler
|
|
||||||
|
|
||||||
init {
|
|
||||||
exceptionHandler = Thread.currentThread().uncaughtExceptionHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract override val isOnThread: Boolean
|
|
||||||
|
|
||||||
override fun checkOnThread() {
|
|
||||||
checkState(isOnThread, "On wrong thread: %s", Thread.currentThread())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun executeASAP(runnable: () -> Unit) {
|
|
||||||
val command = {
|
|
||||||
try {
|
|
||||||
runnable()
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
exceptionHandler.uncaughtException(Thread.currentThread(), throwable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isOnThread)
|
|
||||||
command()
|
|
||||||
else {
|
|
||||||
execute(command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must comply with the Executor definition w.r.t. exceptions here.
|
|
||||||
abstract override fun execute(command: Runnable)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An executor backed by thread pool (which may often have a single thread) which makes it easy to schedule
|
* An executor backed by thread pool (which may often have a single thread) which makes it easy to schedule
|
||||||
* tasks in the future and verify code is running on the executor.
|
* tasks in the future and verify code is running on the executor.
|
||||||
*/
|
*/
|
||||||
class ServiceAffinityExecutor(threadName: String, numThreads: Int) : BaseAffinityExecutor() {
|
class ServiceAffinityExecutor(threadName: String, numThreads: Int) : AffinityExecutor {
|
||||||
protected val threads = Collections.synchronizedSet(HashSet<Thread>())
|
protected val threads = Collections.synchronizedSet(HashSet<Thread>())
|
||||||
|
|
||||||
private val handler = Thread.currentThread().uncaughtExceptionHandler
|
private val handler = Thread.currentThread().uncaughtExceptionHandler
|
||||||
@ -81,8 +56,15 @@ interface AffinityExecutor : Executor {
|
|||||||
val threadFactory = fun(runnable: Runnable): Thread {
|
val threadFactory = fun(runnable: Runnable): Thread {
|
||||||
val thread = object : Thread() {
|
val thread = object : Thread() {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
runnable.run()
|
try {
|
||||||
threads -= this
|
runnable.run()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
e.printStackTrace()
|
||||||
|
handler.uncaughtException(this, e)
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
threads -= this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
thread.isDaemon = true
|
thread.isDaemon = true
|
||||||
@ -100,29 +82,12 @@ interface AffinityExecutor : Executor {
|
|||||||
|
|
||||||
override fun execute(command: Runnable) {
|
override fun execute(command: Runnable) {
|
||||||
service.execute {
|
service.execute {
|
||||||
try {
|
command.run()
|
||||||
command.run()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
if (handler != null)
|
|
||||||
handler.uncaughtException(Thread.currentThread(), e)
|
|
||||||
else
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> executeIn(time: Duration, command: () -> T): ScheduledFuture<T> {
|
fun <T> executeIn(time: Duration, command: () -> T): ScheduledFuture<T> {
|
||||||
return service.schedule(Callable {
|
return service.schedule(Callable { command() }, time.toMillis(), TimeUnit.MILLISECONDS)
|
||||||
try {
|
|
||||||
command()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
if (handler != null)
|
|
||||||
handler.uncaughtException(Thread.currentThread(), e)
|
|
||||||
else
|
|
||||||
e.printStackTrace()
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}, time.toMillis(), TimeUnit.MILLISECONDS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shutdownNow() {
|
override fun shutdownNow() {
|
||||||
@ -140,7 +105,7 @@ interface AffinityExecutor : Executor {
|
|||||||
*
|
*
|
||||||
* @param alwaysQueue If true, executeASAP will never short-circuit and will always queue up.
|
* @param alwaysQueue If true, executeASAP will never short-circuit and will always queue up.
|
||||||
*/
|
*/
|
||||||
class Gate(private val alwaysQueue: Boolean = false) : BaseAffinityExecutor() {
|
class Gate(private val alwaysQueue: Boolean = false) : AffinityExecutor {
|
||||||
private val thisThread = Thread.currentThread()
|
private val thisThread = Thread.currentThread()
|
||||||
private val commandQ = LinkedBlockingQueue<Runnable>()
|
private val commandQ = LinkedBlockingQueue<Runnable>()
|
||||||
|
|
||||||
@ -163,7 +128,7 @@ interface AffinityExecutor : Executor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val SAME_THREAD: AffinityExecutor = object : BaseAffinityExecutor() {
|
val SAME_THREAD: AffinityExecutor = object : AffinityExecutor {
|
||||||
override val isOnThread: Boolean get() = true
|
override val isOnThread: Boolean get() = true
|
||||||
override fun execute(command: Runnable) = command.run()
|
override fun execute(command: Runnable) = command.run()
|
||||||
override fun shutdownNow() {
|
override fun shutdownNow() {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package demos.protocols
|
package demos.protocols
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import co.paralleluniverse.strands.Strand
|
|
||||||
import contracts.DealState
|
import contracts.DealState
|
||||||
import contracts.InterestRateSwap
|
import contracts.InterestRateSwap
|
||||||
import core.StateAndRef
|
import core.StateAndRef
|
||||||
@ -100,7 +99,6 @@ object UpdateBusinessDayProtocol {
|
|||||||
progressTracker.currentStep = FIXING
|
progressTracker.currentStep = FIXING
|
||||||
|
|
||||||
val participant = TwoPartyDealProtocol.Floater(party.address, sessionID, serviceHub.networkMapCache.timestampingNodes[0], dealStateAndRef, serviceHub.keyManagementService.freshKey(), sessionID, progressTracker.childrenFor[FIXING]!!)
|
val participant = TwoPartyDealProtocol.Floater(party.address, sessionID, serviceHub.networkMapCache.timestampingNodes[0], dealStateAndRef, serviceHub.keyManagementService.freshKey(), sessionID, progressTracker.childrenFor[FIXING]!!)
|
||||||
Strand.sleep(100)
|
|
||||||
val result = subProtocol(participant)
|
val result = subProtocol(participant)
|
||||||
return result.tx.outRef(0)
|
return result.tx.outRef(0)
|
||||||
}
|
}
|
||||||
@ -119,7 +117,6 @@ object UpdateBusinessDayProtocol {
|
|||||||
data class UpdateBusinessDayMessage(val date: LocalDate, val sessionID: Long)
|
data class UpdateBusinessDayMessage(val date: LocalDate, val sessionID: Long)
|
||||||
|
|
||||||
object Handler {
|
object Handler {
|
||||||
|
|
||||||
fun register(node: Node) {
|
fun register(node: Node) {
|
||||||
node.net.addMessageHandler("${TOPIC}.0") { msg, registration ->
|
node.net.addMessageHandler("${TOPIC}.0") { msg, registration ->
|
||||||
// Just to validate we got the message
|
// Just to validate we got the message
|
||||||
|
@ -79,7 +79,7 @@ object TwoPartyDealProtocol {
|
|||||||
val sessionID = random63BitValue()
|
val sessionID = random63BitValue()
|
||||||
|
|
||||||
// Make the first message we'll send to kick off the protocol.
|
// Make the first message we'll send to kick off the protocol.
|
||||||
val hello = Handshake<U>(payload, myKeyPair.public, sessionID)
|
val hello = Handshake(payload, myKeyPair.public, sessionID)
|
||||||
|
|
||||||
val maybeSTX = sendAndReceive<SignedTransaction>(DEAL_TOPIC, otherSide, otherSessionID, sessionID, hello)
|
val maybeSTX = sendAndReceive<SignedTransaction>(DEAL_TOPIC, otherSide, otherSessionID, sessionID, hello)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user