use crate::client::UniversalClient;
use crate::contracts::{UniversalRequest, UniversalResponse};
use crate::identity::EngineInstanceId;
use crate::transport::{BoxedFuture, Transport, TransportError};
pub struct ControlPlaneCommunication<T>
where
T: Transport,
{
client: UniversalClient<T>,
}
impl<T> ControlPlaneCommunication<T>
where
T: Transport,
{
pub fn new(transport: T) -> Self {
Self {
client: UniversalClient::new(transport),
}
}
pub fn send_to_engine(
&self,
target: &EngineInstanceId,
request: UniversalRequest,
) -> BoxedFuture<UniversalResponse, TransportError> {
self.client.send(target, request)
}
pub fn is_connected(&self, target: &EngineInstanceId) -> bool {
self.client.is_connected(target)
}
pub fn connected_targets(&self) -> Vec<EngineInstanceId> {
self.client.connected_targets()
}
pub fn client(&self) -> &UniversalClient<T> {
&self.client
}
pub fn transport(&self) -> &T {
self.client.transport()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contracts::descriptor::{
ContractDescriptor, EncodedPayload, Interaction, PayloadDescriptor, Version,
};
use crate::contracts::envelope::MessageEnvelope;
use crate::contracts::metadata::{ContractMetadata, Participants};
use crate::identity::{
CapabilityId, ContractId, CorrelationId, EngineId, MessageId, OperationId,
};
use crate::operation::{Operation, OperationContext};
use crate::status::Status;
use crate::transport::InMemoryTransport;
fn request_for(
sender: &EngineId,
target: &EngineId,
target_instance: Option<EngineInstanceId>,
message_id: &str,
) -> UniversalRequest {
let descriptor = ContractDescriptor::new(
ContractId::new("communication.test").unwrap(),
CapabilityId::new("communication.test-capability").unwrap(),
Version::new(1, 0, 0),
Interaction::Request,
PayloadDescriptor::new("application/octet-stream", Version::new(1, 0, 0)).unwrap(),
);
let mut participants = Participants::new(sender.clone(), target.clone());
if let Some(instance) = target_instance {
participants = participants.with_target_instance(instance);
}
let metadata = ContractMetadata::new(descriptor.clone(), participants);
let operation_context = OperationContext::new(Operation::new(
OperationId::new("communication-operation").unwrap(),
CorrelationId::new("communication-correlation").unwrap(),
));
let envelope = MessageEnvelope::new(
MessageId::new(message_id).unwrap(),
operation_context,
metadata,
EncodedPayload::new(descriptor.payload, b"communication payload"),
);
UniversalRequest::new(envelope)
}
fn response_for(request: UniversalRequest, status: Status) -> UniversalResponse {
UniversalResponse::new(request.event.envelope, status)
}
#[test]
fn communication_can_send_to_a_concrete_engine_instance() {
let transport = InMemoryTransport::new();
let sender = EngineId::new("control-plane-test").unwrap();
let target_engine = EngineId::new("target-engine").unwrap();
let target_instance = EngineInstanceId::new("target-engine-01").unwrap();
transport.register(target_engine.clone(), target_instance.clone(), |request| {
response_for(request, Status::Success)
});
let communication = ControlPlaneCommunication::new(transport);
let request = request_for(
&sender,
&target_engine,
Some(target_instance.clone()),
"communication-message-01",
);
let response =
futures::executor::block_on(communication.send_to_engine(&target_instance, request))
.unwrap();
assert_eq!(response.status, Status::Success);
assert_eq!(
response.event.envelope.message_id.as_str(),
"communication-message-01"
);
}
#[test]
fn communication_uses_the_concrete_instance_as_the_target() {
let transport = InMemoryTransport::new();
let sender = EngineId::new("control-plane-test").unwrap();
let target_engine = EngineId::new("target-engine").unwrap();
let instance_one = EngineInstanceId::new("target-engine-01").unwrap();
let instance_two = EngineInstanceId::new("target-engine-02").unwrap();
transport.register(target_engine.clone(), instance_one.clone(), |request| {
response_for(request, Status::Success)
});
transport.register(target_engine.clone(), instance_two.clone(), |request| {
response_for(request, Status::TimedOut)
});
let communication = ControlPlaneCommunication::new(transport);
let request = request_for(
&sender,
&target_engine,
Some(instance_two.clone()),
"instance-specific-message",
);
let response =
futures::executor::block_on(communication.send_to_engine(&instance_two, request))
.unwrap();
assert_eq!(response.status, Status::TimedOut);
}
#[test]
fn communication_rejects_a_mismatched_request_target() {
let transport = InMemoryTransport::new();
let sender = EngineId::new("control-plane-test").unwrap();
let target_engine = EngineId::new("target-engine").unwrap();
let transport_instance = EngineInstanceId::new("target-engine-01").unwrap();
let request_instance = EngineInstanceId::new("target-engine-02").unwrap();
transport.register(
target_engine.clone(),
transport_instance.clone(),
|_request| {
panic!("mismatched target must not reach the transport handler");
},
);
let communication = ControlPlaneCommunication::new(transport);
let request = request_for(
&sender,
&target_engine,
Some(request_instance),
"mismatched-target",
);
let result =
futures::executor::block_on(communication.send_to_engine(&transport_instance, request));
assert_eq!(
result,
Err(TransportError::Peer(
"request target instance does not match the client target".into()
))
);
}
#[test]
fn communication_rejects_a_request_without_a_concrete_target() {
let transport = InMemoryTransport::new();
let sender = EngineId::new("control-plane-test").unwrap();
let target_engine = EngineId::new("target-engine").unwrap();
let target_instance = EngineInstanceId::new("target-engine-01").unwrap();
transport.register(target_engine.clone(), target_instance.clone(), |_request| {
panic!("request without a concrete target must not reach the transport handler");
});
let communication = ControlPlaneCommunication::new(transport);
let request = request_for(&sender, &target_engine, None, "missing-target-instance");
let result =
futures::executor::block_on(communication.send_to_engine(&target_instance, request));
assert_eq!(
result,
Err(TransportError::Peer(
"request does not specify a concrete target instance".into()
))
);
}
#[test]
fn communication_reports_transport_connectivity() {
let transport = InMemoryTransport::new();
let engine = EngineId::new("target-engine").unwrap();
let instance = EngineInstanceId::new("target-engine-01").unwrap();
transport.register(engine, instance.clone(), |request| {
response_for(request, Status::Success)
});
let communication = ControlPlaneCommunication::new(transport);
assert!(communication.is_connected(&instance));
assert_eq!(communication.connected_targets(), vec![instance],);
}
#[test]
fn communication_exposes_the_underlying_client_without_replacing_it() {
let transport = InMemoryTransport::new();
let communication = ControlPlaneCommunication::new(transport);
let _: &UniversalClient<InMemoryTransport> = communication.client();
}
}