use ahash::HashMap;
use jsonrpsee::{
ConnectionId, MethodResponse, MethodSink,
server::{
IntoSubscriptionCloseResponse, MethodCallback, Methods, RegisterMethodError,
ResponsePayload,
},
types::{ErrorObjectOwned, Id, Params, error::ErrorCode},
};
use parking_lot::Mutex;
use serde_json::value::{RawValue, to_raw_value};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::sync::broadcast::error::RecvError;
use tokio::sync::{mpsc, oneshot};
use super::error::ServerError;
pub const NOTIF_METHOD_NAME: &str = "xrpc.ch.val";
pub const CANCEL_METHOD_NAME: &str = "xrpc.cancel";
pub type ChannelId = u64;
pub type Subscribers =
Arc<Mutex<HashMap<(ConnectionId, Id<'static>), (MethodSink, mpsc::Receiver<()>, ChannelId)>>>;
#[derive(Debug)]
#[must_use = "PendingSubscriptionSink does nothing unless `accept` or `reject` is called"]
pub struct PendingSubscriptionSink {
pub(crate) inner: MethodSink,
pub(crate) method: &'static str,
pub(crate) subscribers: Subscribers,
pub(crate) id: Id<'static>,
pub(crate) subscribe: oneshot::Sender<MethodResponse>,
pub(crate) channel_id: ChannelId,
pub(crate) connection_id: ConnectionId,
}
impl PendingSubscriptionSink {
pub async fn accept(self) -> Result<SubscriptionSink, String> {
let channel_id = self.channel_id();
let id = self.id.clone();
let response = MethodResponse::subscription_response(
self.id,
ResponsePayload::success_borrowed(&channel_id),
self.inner.max_response_size() as usize,
);
let success = response.is_success();
self.inner
.send(response.to_json())
.await
.map_err(|e| e.to_string())?;
self.subscribe
.send(response)
.map_err(|e| format!("accept error: {}", e.as_json()))?;
if success {
let (tx, rx) = mpsc::channel(1);
self.subscribers.lock().insert(
(self.connection_id, id),
(self.inner.clone(), rx, self.channel_id),
);
tracing::debug!(
"Accepting subscription (conn_id={}, chann_id={})",
self.connection_id.0,
self.channel_id
);
Ok(SubscriptionSink {
inner: self.inner,
method: self.method,
unsubscribe: IsUnsubscribed(tx),
channel_id: self.channel_id,
})
} else {
panic!(
"The subscription response was too big; adjust the `max_response_size` or change Subscription ID generation"
);
}
}
pub fn channel_id(&self) -> ChannelId {
self.channel_id
}
}
#[derive(Debug, Clone)]
pub struct IsUnsubscribed(mpsc::Sender<()>);
impl IsUnsubscribed {
pub async fn unsubscribed(&self) {
self.0.closed().await;
}
}
#[derive(Debug, Clone)]
pub struct SubscriptionSink {
inner: MethodSink,
method: &'static str,
unsubscribe: IsUnsubscribed,
channel_id: ChannelId,
}
impl SubscriptionSink {
pub fn method_name(&self) -> &str {
self.method
}
pub fn channel_id(&self) -> ChannelId {
self.channel_id
}
pub async fn send(&self, msg: Box<serde_json::value::RawValue>) -> Result<(), String> {
if self.is_closed() {
return Err(format!("disconnect error: {msg}"));
}
self.inner.send(msg).await.map_err(|e| e.to_string())
}
pub fn is_closed(&self) -> bool {
self.inner.is_closed()
}
pub async fn closed(&self) {
tokio::select! {
_ = self.inner.closed() => (),
_ = self.unsubscribe.unsubscribed() => (),
}
}
}
fn create_notif_message(
sink: &SubscriptionSink,
result: &impl serde::Serialize,
) -> anyhow::Result<Box<RawValue>> {
let method = sink.method_name();
let channel_id = sink.channel_id();
let result = serde_json::to_value(result)?;
let msg = serde_json::json!({
"jsonrpc": "2.0",
"method": method,
"params": [channel_id, result]
});
tracing::debug!("Sending notification: {}", msg);
Ok(to_raw_value(&msg)?)
}
fn close_payload(channel_id: ChannelId) -> serde_json::Value {
serde_json::json!({
"jsonrpc":"2.0",
"method":"xrpc.ch.close",
"params":[channel_id]
})
}
fn close_channel_response(channel_id: ChannelId) -> MethodResponse {
MethodResponse::response(
Id::Null,
ResponsePayload::success(close_payload(channel_id)),
1024,
)
}
async fn send_close(sink: &SubscriptionSink) {
if let Ok(payload) = to_raw_value(&close_payload(sink.channel_id())) {
let _ = sink.send(payload).await;
}
}
#[derive(Debug, Clone)]
pub struct RpcModule {
id_provider: Arc<AtomicU64>,
channels: Subscribers,
methods: Methods,
}
impl From<RpcModule> for Methods {
fn from(module: RpcModule) -> Methods {
module.methods
}
}
impl Default for RpcModule {
fn default() -> Self {
let mut methods = Methods::default();
let channels = Subscribers::default();
methods
.verify_and_insert(
CANCEL_METHOD_NAME,
MethodCallback::Unsubscription(Arc::new({
let channels = channels.clone();
move |id,
params: Params,
connection_id: ConnectionId,
_max_response,
_extensions| {
let cb = || {
let [id]: [Id<'_>; 1] = params.parse()?;
let sub_id = id.into_owned();
tracing::debug!("Got cancel request (id={sub_id})");
let opt = channels.lock().remove(&(connection_id, sub_id));
match opt {
Some((_, _, channel_id)) => {
Ok::<ChannelId, ServerError>(channel_id)
}
None => Err::<ChannelId, ServerError>(ServerError::from(
anyhow::anyhow!("channel not found"),
)),
}
};
let result = cb();
match result {
Ok(channel_id) => {
let resp = close_channel_response(channel_id);
tracing::debug!("Sending close message: {}", resp.as_json());
resp
}
Err(e) => {
let error: ErrorObjectOwned = e.into();
MethodResponse::error(id, error)
}
}
}
})),
)
.expect("Inserting a method into an empty methods map is infallible.");
Self {
id_provider: Arc::new(AtomicU64::new(0)),
channels,
methods,
}
}
}
impl RpcModule {
pub fn register_channel<R, F>(
&mut self,
subscribe_method_name: &'static str,
callback: F,
) -> Result<&mut MethodCallback, RegisterMethodError>
where
F: (Fn(Params) -> tokio::sync::broadcast::Receiver<R>) + Send + Sync + 'static,
R: serde::Serialize + Clone + Send + 'static,
{
self.register_channel_raw(subscribe_method_name, {
move |params, pending| {
let mut receiver = callback(params);
tokio::spawn(async move {
let sink = if let Ok(sink) = pending.accept().await {
sink
} else {
tracing::error!("Failed to accept subscription");
return;
};
tracing::debug!("Channel created: chann_id={}", sink.channel_id);
loop {
tokio::select! {
action = receiver.recv() => {
match action {
Ok(msg) => {
match create_notif_message(&sink, &msg) {
Ok(msg) => {
if let Err(e) = sink.send(msg).await {
tracing::error!("Failed to send message: {:?}", e);
break;
}
}
Err(e) => {
tracing::error!("Failed to serialize channel message: {:?}", e);
break;
}
}
}
Err(RecvError::Closed) => {
send_close(&sink).await;
break;
}
Err(RecvError::Lagged(n)) => {
tracing::warn!(
"closing channel {}: subscriber lagged by {n} messages",
sink.channel_id()
);
send_close(&sink).await;
break;
}
}
},
_ = sink.closed() => {
break;
}
}
}
tracing::debug!("Send notification task ended (chann_id={})", sink.channel_id);
});
}
})
}
fn register_channel_raw<R, F>(
&mut self,
subscribe_method_name: &'static str,
callback: F,
) -> Result<&mut MethodCallback, RegisterMethodError>
where
F: (Fn(Params, PendingSubscriptionSink) -> R) + Send + Sync + 'static,
R: IntoSubscriptionCloseResponse,
{
self.methods.verify_method_name(subscribe_method_name)?;
let subscribers = self.channels.clone();
self.methods.verify_and_insert(
subscribe_method_name,
MethodCallback::Subscription(Arc::new({
let id_provider = self.id_provider.clone();
move |id, params, method_sink, conn, _extensions| {
let channel_id = id_provider.fetch_add(1, Ordering::Relaxed);
let (tx, rx) = oneshot::channel();
let sink = PendingSubscriptionSink {
inner: method_sink,
method: NOTIF_METHOD_NAME,
subscribers: subscribers.clone(),
id: id.clone().into_owned(),
subscribe: tx,
channel_id,
connection_id: conn.conn_id,
};
callback(params, sink);
let id = id.into_owned();
Box::pin(async move {
match rx.await {
Ok(rp) => rp,
Err(_) => MethodResponse::error(id, ErrorCode::InternalError),
}
})
}
})),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{Value, json};
use std::time::Duration;
use tokio::sync::broadcast;
const TEST_METHOD: &str = "test.channel";
const RECV_TIMEOUT: Duration = Duration::from_secs(1);
const SOURCE_CAPACITY: usize = 4;
const STREAM_BUF_SIZE: usize = 256;
fn test_methods(events: &broadcast::Sender<String>) -> Methods {
let mut module = RpcModule::default();
let prototype = events.subscribe();
module
.register_channel(TEST_METHOD, move |_params| prototype.resubscribe())
.unwrap();
module.into()
}
async fn subscribe(
methods: &Methods,
request_id: u64,
) -> (ChannelId, mpsc::Receiver<Box<RawValue>>) {
let request = format!(
r#"{{"jsonrpc":"2.0","id":{request_id},"method":"{TEST_METHOD}","params":[]}}"#
);
let (response, frames) = methods
.raw_json_request(&request, STREAM_BUF_SIZE)
.await
.unwrap();
let response: Value = serde_json::from_str(response.get()).unwrap();
assert_eq!(response.get("id"), Some(&json!(request_id)));
let channel_id = response
.get("result")
.and_then(Value::as_u64)
.unwrap_or_else(|| panic!("channel id must be a bare u64: {response}"));
(channel_id, frames)
}
const CANCEL_REQUEST_ID: u64 = 999;
async fn cancel(methods: &Methods, target_request_id: u64) -> Value {
let request = format!(
r#"{{"jsonrpc":"2.0","id":{CANCEL_REQUEST_ID},"method":"{CANCEL_METHOD_NAME}","params":[{target_request_id}]}}"#
);
let (response, _) = methods
.raw_json_request(&request, STREAM_BUF_SIZE)
.await
.unwrap();
serde_json::from_str(response.get()).unwrap()
}
async fn next_frame(frames: &mut mpsc::Receiver<Box<RawValue>>) -> Value {
let frame = tokio::time::timeout(RECV_TIMEOUT, frames.recv())
.await
.expect("timed out waiting for a frame")
.expect("stream closed while waiting for a frame");
serde_json::from_str(frame.get()).unwrap()
}
async fn assert_stream_closed(frames: &mut mpsc::Receiver<Box<RawValue>>) {
let frame = tokio::time::timeout(RECV_TIMEOUT, frames.recv())
.await
.expect("timed out waiting for the stream to close");
assert!(
frame.is_none(),
"expected the stream to close, got frame: {}",
frame.unwrap().get()
);
}
fn val_frame(channel_id: ChannelId, payload: &str) -> Value {
json!({"jsonrpc": "2.0", "method": NOTIF_METHOD_NAME, "params": [channel_id, payload]})
}
fn close_frame(channel_id: ChannelId) -> Value {
json!({"jsonrpc": "2.0", "method": "xrpc.ch.close", "params": [channel_id]})
}
fn close_response(channel_id: ChannelId) -> Value {
json!({"jsonrpc": "2.0", "id": null, "result": close_frame(channel_id)})
}
#[tokio::test]
async fn subscribe_returns_u64_channel_id() {
let (events, _) = broadcast::channel::<String>(SOURCE_CAPACITY);
let methods = test_methods(&events);
let (first_channel, _first_frames) = subscribe(&methods, 1).await;
let (second_channel, _second_frames) = subscribe(&methods, 2).await;
assert_eq!(second_channel, first_channel + 1);
}
#[tokio::test]
async fn value_framing_positional() {
let (events, _) = broadcast::channel(SOURCE_CAPACITY);
let methods = test_methods(&events);
let (channel_id, mut frames) = subscribe(&methods, 1).await;
events.send("head-change".into()).unwrap();
drop(events);
assert_eq!(
next_frame(&mut frames).await,
val_frame(channel_id, "head-change")
);
assert_eq!(next_frame(&mut frames).await, close_frame(channel_id));
}
#[tokio::test]
async fn two_channels_one_conn_independent() {
let (events, _) = broadcast::channel(SOURCE_CAPACITY);
let methods = test_methods(&events);
let (first_channel, mut first_frames) = subscribe(&methods, 1).await;
let (second_channel, mut second_frames) = subscribe(&methods, 2).await;
assert_ne!(first_channel, second_channel);
events.send("both".into()).unwrap();
assert_eq!(
next_frame(&mut first_frames).await,
val_frame(first_channel, "both")
);
assert_eq!(
next_frame(&mut second_frames).await,
val_frame(second_channel, "both")
);
assert_eq!(cancel(&methods, 1).await, close_response(first_channel));
assert_stream_closed(&mut first_frames).await;
events.send("second-only".into()).unwrap();
assert_eq!(
next_frame(&mut second_frames).await,
val_frame(second_channel, "second-only")
);
}
#[tokio::test]
async fn hundred_channel_fanout() {
let (events, _) = broadcast::channel(SOURCE_CAPACITY);
let methods = test_methods(&events);
let mut channels = Vec::new();
for request_id in 1..=100 {
channels.push(subscribe(&methods, request_id).await);
}
events.send("fan-out".into()).unwrap();
let mut seen = ahash::HashSet::default();
for (channel_id, frames) in &mut channels {
assert_eq!(next_frame(frames).await, val_frame(*channel_id, "fan-out"));
assert!(seen.insert(*channel_id), "channel ids must be unique");
}
}
#[tokio::test]
async fn cancel_unknown_id_errors() {
let (events, _) = broadcast::channel(SOURCE_CAPACITY);
let methods = test_methods(&events);
let (channel_id, mut frames) = subscribe(&methods, 1).await;
let response = cancel(&methods, 99).await;
assert!(
response.get("error").is_some(),
"cancelling an unknown id must return an error response: {response}"
);
assert!(response.get("result").is_none());
assert_eq!(response.get("id"), Some(&json!(CANCEL_REQUEST_ID)));
events.send("still-open".into()).unwrap();
assert_eq!(
next_frame(&mut frames).await,
val_frame(channel_id, "still-open")
);
}
#[tokio::test]
async fn source_closed_sends_bare_close() {
let (events, _) = broadcast::channel::<String>(SOURCE_CAPACITY);
let methods = test_methods(&events);
let (channel_id, mut frames) = subscribe(&methods, 1).await;
drop(events);
assert_eq!(next_frame(&mut frames).await, close_frame(channel_id));
}
#[tokio::test]
async fn lagged_consumer_channel_closes() {
let (events, lagged_rx) = broadcast::channel(SOURCE_CAPACITY);
for n in 0..SOURCE_CAPACITY + 2 {
events.send(format!("event-{n}")).unwrap();
}
let lagged_rx = Mutex::new(Some(lagged_rx));
let mut module = RpcModule::default();
module
.register_channel(TEST_METHOD, move |_params| {
lagged_rx.lock().take().expect("single subscriber")
})
.unwrap();
let methods: Methods = module.into();
let (channel_id, mut frames) = subscribe(&methods, 1).await;
assert_eq!(next_frame(&mut frames).await, close_frame(channel_id));
assert!(events.send("after-close".into()).is_err());
tokio::task::yield_now().await;
assert!(matches!(
frames.try_recv(),
Err(mpsc::error::TryRecvError::Empty)
));
}
}