use std::sync::{Arc, atomic::AtomicBool};
use connectrpc::{ConnectError, RequestContext, Response, Router, ServiceRequest, ServiceResult};
use polyc_proto::proto::polychrome::state::v1::{
AdvanceClockReply, AdvanceClockRequest, CreateSnapshotReply, CreateSnapshotRequest,
DescribeStreamReply, DescribeStreamRequest, GetReceiptReply, GetReceiptRequest, ObserveReply,
ObserveRequest, ReadChunkReply, ReadChunkRequest, ReadPageReply, ReadPageRequest,
StateConformanceService, StateConformanceServiceExt, SubmitReply, SubmitRequest,
};
use polyc_state::{
conformance::{SyntheticCommand, family},
context::CallContext,
error::StateError,
id::{Audience, CommandId, OperationFamily},
page::PageRequest,
stream::StreamRequest,
};
use crate::{
admission::{
check_audience, check_call_context_version, check_not_draining, check_transport_deadline,
},
conformance::backend::ConformanceBackend,
error::to_connect_error,
wire::{DeclaredCall, Kernel, declared_call, duration_from_nanos},
};
pub struct ConformanceSvc {
backend: Arc<dyn ConformanceBackend>,
draining: Arc<AtomicBool>,
}
impl ConformanceSvc {
#[must_use]
pub const fn new(backend: Arc<dyn ConformanceBackend>, draining: Arc<AtomicBool>) -> Self {
Self { backend, draining }
}
#[must_use]
pub fn register_on(self, router: Router) -> Router {
Arc::new(self).register(router)
}
fn family() -> OperationFamily {
OperationFamily::new(family::FAMILY)
}
fn served_audience() -> Audience {
Audience::new(family::AUDIENCE)
}
fn admit(
&self,
ctx: &RequestContext,
context: impl Into<Option<polyc_proto::proto::polychrome::state::v1::CallContext>>,
) -> Result<CallContext, ConnectError> {
check_not_draining(self.draining.load(std::sync::atomic::Ordering::Relaxed))?;
let declared: DeclaredCall = declared_call(context).map_err(|e| to_connect_error(&e))?;
let family = Self::family();
check_call_context_version(declared.version).map_err(|e| to_connect_error(&e))?;
check_audience(&declared.audience, &Self::served_audience(), &family)
.map_err(|e| to_connect_error(&e))?;
check_transport_deadline(ctx.time_remaining(), &family)
.map_err(|e| to_connect_error(&e))?;
Ok(declared
.origin_relative_context()
.in_frame(self.backend.now()))
}
}
#[allow(refining_impl_trait)]
impl StateConformanceService for ConformanceSvc {
async fn submit(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, SubmitRequest>,
) -> ServiceResult<SubmitReply> {
let message = request.to_owned_message();
let context = self.admit(&ctx, message.context)?;
let command = Kernel::<SyntheticCommand>::try_from(
message.command.into_option().ok_or_else(|| {
to_connect_error(&StateError::Malformed {
field: "command".to_owned(),
reason: "a submission carries the command it submits".to_owned(),
})
})?,
)
.map_err(|e| to_connect_error(&e))?
.into_inner();
let receipt = self
.backend
.submit(command, &context)
.map_err(|e| to_connect_error(&e))?;
Response::ok(SubmitReply {
receipt: buffa::MessageField::some(Kernel(&receipt).into()),
__buffa_unknown_fields: buffa::UnknownFields::default(),
})
}
async fn get_receipt(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, GetReceiptRequest>,
) -> ServiceResult<GetReceiptReply> {
let message = request.to_owned_message();
let _context = self.admit(&ctx, message.context)?;
let recorded = self
.backend
.receipt(&CommandId::new(message.command_id))
.map_err(|e| to_connect_error(&e))?;
Response::ok(GetReceiptReply {
receipt: recorded
.as_ref()
.map_or_else(buffa::MessageField::default, |receipt| {
buffa::MessageField::some(Kernel(receipt).into())
}),
__buffa_unknown_fields: buffa::UnknownFields::default(),
})
}
async fn create_snapshot(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, CreateSnapshotRequest>,
) -> ServiceResult<CreateSnapshotReply> {
let message = request.to_owned_message();
let context = self.admit(&ctx, message.context)?;
let snapshot = self
.backend
.create_snapshot(&context)
.map_err(|e| to_connect_error(&e))?;
Response::ok(CreateSnapshotReply {
snapshot: snapshot.as_str().to_owned(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
})
}
async fn read_page(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, ReadPageRequest>,
) -> ServiceResult<ReadPageReply> {
let message = request.to_owned_message();
let context = self.admit(&ctx, message.context)?;
let page_request =
Kernel::<PageRequest>::try_from(message.page.into_option().ok_or_else(|| {
to_connect_error(&StateError::Malformed {
field: "page".to_owned(),
reason: "a bounded read carries the page it asks for".to_owned(),
})
})?)
.map_err(|e| to_connect_error(&e))?
.into_inner();
let page = self
.backend
.read_page(page_request, &context)
.map_err(|e| to_connect_error(&e))?;
Response::ok(ReadPageReply {
page: buffa::MessageField::some(Kernel(&page).into()),
__buffa_unknown_fields: buffa::UnknownFields::default(),
})
}
async fn read_chunk(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, ReadChunkRequest>,
) -> ServiceResult<ReadChunkReply> {
let message = request.to_owned_message();
let context = self.admit(&ctx, message.context)?;
let chunk_request =
Kernel::<StreamRequest>::try_from(message.chunk.into_option().ok_or_else(|| {
to_connect_error(&StateError::Malformed {
field: "chunk".to_owned(),
reason: "a bounded stream read carries the chunk it asks for".to_owned(),
})
})?)
.map_err(|e| to_connect_error(&e))?
.into_inner();
let chunk = self
.backend
.read_chunk(chunk_request, &context)
.map_err(|e| to_connect_error(&e))?;
Response::ok(ReadChunkReply {
chunk: buffa::MessageField::some(Kernel(&chunk).into()),
__buffa_unknown_fields: buffa::UnknownFields::default(),
})
}
async fn describe_stream(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, DescribeStreamRequest>,
) -> ServiceResult<DescribeStreamReply> {
let message = request.to_owned_message();
let _context = self.admit(&ctx, message.context)?;
Response::ok(DescribeStreamReply {
contract: buffa::MessageField::some(Kernel(self.backend.stream_contract()).into()),
__buffa_unknown_fields: buffa::UnknownFields::default(),
})
}
async fn observe(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, ObserveRequest>,
) -> ServiceResult<ObserveReply> {
let message = request.to_owned_message();
let _context = self.admit(&ctx, message.context)?;
Response::ok(ObserveReply {
applied_effects: self.backend.applied_effects(),
now_nanos: self.backend.now().as_nanos(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
})
}
async fn advance_clock(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, AdvanceClockRequest>,
) -> ServiceResult<AdvanceClockReply> {
let message = request.to_owned_message();
let _context = self.admit(&ctx, message.context)?;
self.backend
.advance_clock(duration_from_nanos(message.budget_nanos));
Response::ok(AdvanceClockReply {
now_nanos: self.backend.now().as_nanos(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
})
}
}