Break down topic into component parts

Break down what is referred to as "topic" of a message into its component parts. This splits the
general topic from the session ID, so it's clear where a session ID is provided, and whether any
given topic string includes a session ID or not.
This commit is contained in:
Ross Nicoll
2016-07-29 10:36:21 +01:00
parent 7d39a101d4
commit cf4bb0c9af
20 changed files with 198 additions and 87 deletions

View File

@ -1,6 +1,7 @@
package com.r3corda.core.messaging
import com.google.common.util.concurrent.ListenableFuture
import com.r3corda.core.node.services.DEFAULT_SESSION_ID
import com.r3corda.core.serialization.DeserializeAsKotlinObjectDef
import com.r3corda.core.serialization.serialize
import java.time.Instant
@ -22,7 +23,7 @@ import javax.annotation.concurrent.ThreadSafe
interface MessagingService {
/**
* The provided function will be invoked for each received message whose topic matches the given string, on the given
* executor. The topic can be the empty string to match all messages.
* executor.
*
* If no executor is received then the callback will run on threads provided by the messaging service, and the
* callback is expected to be thread safe as a result.
@ -30,8 +31,28 @@ interface MessagingService {
* The returned object is an opaque handle that may be used to un-register handlers later with [removeMessageHandler].
* The handle is passed to the callback as well, to avoid race conditions whereby the callback wants to unregister
* itself and yet addMessageHandler hasn't returned the handle yet.
*
* @param topic identifier for the general subject of the message, for example "platform.network_map.fetch".
* The topic can be the empty string to match all messages (session ID must be [DEFAULT_SESSION_ID]).
* @param sessionID identifier for the session the message is part of. For services listening before
* a session is established, use [DEFAULT_SESSION_ID].
*/
fun addMessageHandler(topic: String = "", executor: Executor? = null, callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration
fun addMessageHandler(topic: String = "", sessionID: Long = DEFAULT_SESSION_ID, executor: Executor? = null, callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration
/**
* The provided function will be invoked for each received message whose topic and session matches, on the
* given executor.
*
* If no executor is received then the callback will run on threads provided by the messaging service, and the
* callback is expected to be thread safe as a result.
*
* The returned object is an opaque handle that may be used to un-register handlers later with [removeMessageHandler].
* The handle is passed to the callback as well, to avoid race conditions whereby the callback wants to unregister
* itself and yet addMessageHandler hasn't returned the handle yet.
*
* @param topicSession identifier for the topic and session to listen for messages arriving on.
*/
fun addMessageHandler(topicSession: TopicSession, executor: Executor? = null, callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration
/**
* Removes a handler given the object returned from [addMessageHandler]. The callback will no longer be invoked once
@ -55,34 +76,81 @@ interface MessagingService {
/**
* Returns an initialised [Message] with the current time, etc, already filled in.
*
* @param topic identifier for the general subject of the message, for example "platform.network_map.fetch".
* Must not be blank.
* @param sessionID identifier for the session the message is part of. For messages sent to services before the
* construction of a session, use [DEFAULT_SESSION_ID].
*/
fun createMessage(topic: String, data: ByteArray): Message
fun createMessage(topic: String, sessionID: Long = DEFAULT_SESSION_ID, data: ByteArray): Message
/**
* Returns an initialised [Message] with the current time, etc, already filled in.
*
* @param topicSession identifier for the topic and session the message is sent to.
*/
fun createMessage(topicSession: TopicSession, data: ByteArray): Message
/** Returns an address that refers to this node. */
val myAddress: SingleMessageRecipient
}
/**
* Registers a handler for the given topic that runs the given callback with the message and then removes itself. This
* is useful for one-shot handlers that aren't supposed to stick around permanently. Note that this callback doesn't
* take the registration object, unlike the callback to [MessagingService.addMessageHandler].
* Registers a handler for the given topic and session ID that runs the given callback with the message and then removes
* itself. This is useful for one-shot handlers that aren't supposed to stick around permanently. Note that this callback
* doesn't take the registration object, unlike the callback to [MessagingService.addMessageHandler], as the handler is
* automatically deregistered before the callback runs.
*
* @param topic identifier for the general subject of the message, for example "platform.network_map.fetch".
* The topic can be the empty string to match all messages (session ID must be [DEFAULT_SESSION_ID]).
* @param sessionID identifier for the session the message is part of. For services listening before
* a session is established, use [DEFAULT_SESSION_ID].
*/
fun MessagingService.runOnNextMessage(topic: String = "", executor: Executor? = null, callback: (Message) -> Unit) {
fun MessagingService.runOnNextMessage(topic: String, sessionID: Long, executor: Executor? = null, callback: (Message) -> Unit)
= runOnNextMessage(TopicSession(topic, sessionID), executor, callback)
/**
* Registers a handler for the given topic and session that runs the given callback with the message and then removes
* itself. This is useful for one-shot handlers that aren't supposed to stick around permanently. Note that this callback
* doesn't take the registration object, unlike the callback to [MessagingService.addMessageHandler].
*
* @param topicSession identifier for the topic and session to listen for messages arriving on.
*/
fun MessagingService.runOnNextMessage(topicSession: TopicSession, executor: Executor? = null, callback: (Message) -> Unit) {
val consumed = AtomicBoolean()
addMessageHandler(topic, executor) { msg, reg ->
addMessageHandler(topicSession, executor) { msg, reg ->
removeMessageHandler(reg)
check(!consumed.getAndSet(true)) { "Called more than once" }
check(msg.topic == topic) { "Topic mismatch: ${msg.topic} vs $topic" }
check(msg.topicSession == topicSession) { "Topic/session mismatch: ${msg.topicSession} vs $topicSession" }
callback(msg)
}
}
fun MessagingService.send(topic: String, payload: Any, to: MessageRecipients) {
send(createMessage(topic, payload.serialize().bits), to)
}
fun MessagingService.send(topic: String, sessionID: Long, payload: Any, to: MessageRecipients)
= send(TopicSession(topic, sessionID), payload, to)
fun MessagingService.send(topicSession: TopicSession, payload: Any, to: MessageRecipients)
= send(createMessage(topicSession, payload.serialize().bits), to)
interface MessageHandlerRegistration
/**
* An identifier for the endpoint [MessagingService] message handlers listen at.
*
* @param topic identifier for the general subject of the message, for example "platform.network_map.fetch".
* The topic can be the empty string to match all messages (session ID must be [DEFAULT_SESSION_ID]).
* @param sessionID identifier for the session the message is part of. For services listening before
* a session is established, use [DEFAULT_SESSION_ID].
*/
data class TopicSession(val topic: String, val sessionID: Long = DEFAULT_SESSION_ID) {
companion object {
val Blank = TopicSession("", DEFAULT_SESSION_ID)
}
fun isBlank() = topic.isBlank() && sessionID == DEFAULT_SESSION_ID
override fun toString(): String = "${topic}.${sessionID}"
}
/**
* A message is defined, at this level, to be a (topic, timestamp, byte arrays) triple, where the topic is a string in
* Java-style reverse dns form, with "platform." being a prefix reserved by the platform for its own use. Vendor
@ -94,7 +162,7 @@ interface MessageHandlerRegistration
* the timestamp field they probably will be, even if an implementation just uses a hash prefix as the message id.
*/
interface Message {
val topic: String
val topicSession: TopicSession
val data: ByteArray
val debugTimestamp: Instant
val debugMessageID: String

View File

@ -10,9 +10,10 @@ import java.security.PrivateKey
import java.security.PublicKey
/**
* Postfix for base topics when sending a request to a service.
* Session ID to use for services listening for the first message in a session (before a
* specific session ID has been established).
*/
val TOPIC_DEFAULT_POSTFIX = ".0"
val DEFAULT_SESSION_ID = 0L
/**
* This file defines various 'services' which are not currently fleshed out. A service is a module that provides

View File

@ -7,6 +7,7 @@ import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.signWithECDSA
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.services.DEFAULT_SESSION_ID
import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.random63BitValue
import com.r3corda.core.seconds
@ -157,7 +158,7 @@ object TwoPartyDealProtocol {
// Copy the transaction to every regulator in the network. This is obviously completely bogus, it's
// just for demo purposes.
for (regulator in regulators) {
send(regulator.identity, 0, fullySigned)
send(regulator.identity, DEFAULT_SESSION_ID, fullySigned)
}
}
@ -461,7 +462,7 @@ object TwoPartyDealProtocol {
val initation = FixingSessionInitiation(sessionID, sortedParties[0], serviceHub.storageService.myLegalIdentity, timeout)
// Send initiation to other side to launch one side of the fixing protocol (the Fixer).
send(sortedParties[1], 0, initation)
send(sortedParties[1], DEFAULT_SESSION_ID, initation)
// Then start the other side of the fixing protocol.
val protocol = Floater(ref, sessionID)