use std::future::Future;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::mpsc;
use super::on_cleanup;
use super::signal::{Signal, create_stored};
pub struct ServiceContext {
running: Arc<AtomicBool>,
}
impl ServiceContext {
pub fn is_running(&self) -> bool {
self.running.load(Ordering::SeqCst)
}
}
pub struct Service<Cmd> {
sender: Signal<mpsc::UnboundedSender<Cmd>>,
}
impl<Cmd> Clone for Service<Cmd> {
fn clone(&self) -> Self {
*self
}
}
impl<Cmd> Copy for Service<Cmd> {}
impl<Cmd: 'static> Service<Cmd> {
pub fn send(&self, cmd: Cmd) {
if !self.sender.is_live() {
log::warn!("ignoring command to disposed service");
return;
}
self.sender.with_untracked(|tx| {
let _ = tx.send(cmd);
});
}
pub fn sender(&self) -> mpsc::UnboundedSender<Cmd> {
self.sender.with_untracked(|tx| tx.clone())
}
}
pub fn create_service<Cmd, F, Fut>(f: F) -> Service<Cmd>
where
Cmd: Send + 'static,
F: FnOnce(mpsc::UnboundedReceiver<Cmd>, ServiceContext) -> Fut,
Fut: Future<Output = ()> + Send + 'static,
{
let (tx, rx) = mpsc::unbounded_channel();
let sender = create_stored(tx);
spawn_owned(|ctx| f(rx, ctx));
Service { sender }
}
fn spawn_owned<F, Fut>(f: F)
where
F: FnOnce(ServiceContext) -> Fut,
Fut: Future<Output = ()> + Send + 'static,
{
let running = Arc::new(AtomicBool::new(true));
let running_for_cleanup = running.clone();
let handle = service_runtime().spawn(f(ServiceContext { running }));
on_cleanup(move || {
running_for_cleanup.store(false, Ordering::SeqCst);
handle.abort();
});
}
pub fn create_task<F, Fut>(f: F)
where
F: FnOnce(ServiceContext) -> Fut,
Fut: Future<Output = ()> + Send + 'static,
{
spawn_owned(f);
}
fn service_runtime() -> tokio::runtime::Handle {
if let Ok(handle) = tokio::runtime::Handle::try_current() {
return handle;
}
static HANDLE: std::sync::OnceLock<tokio::runtime::Handle> = std::sync::OnceLock::new();
HANDLE
.get_or_init(|| {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.expect("failed to build guido service runtime");
let handle = runtime.handle().clone();
std::thread::Builder::new()
.name("guido-services".into())
.spawn(move || {
runtime.block_on(std::future::pending::<()>());
})
.expect("failed to spawn guido service thread");
handle
})
.clone()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reactive::owner::{dispose_owner_now, with_owner};
use std::sync::atomic::AtomicI32;
use std::time::Duration;
#[test]
fn sending_after_service_disposal_is_a_noop() {
let (service, owner) = with_owner(|| create_service(|_rx, _ctx| async {}));
dispose_owner_now(owner);
service.send(());
service.send(());
}
#[test]
fn a_disposed_service_cannot_send_to_a_recycled_channel() {
let ((stale, mut old_rx), owner) = with_owner(|| {
let (tx, rx) = mpsc::unbounded_channel::<i32>();
(
Service {
sender: create_stored(tx),
},
rx,
)
});
dispose_owner_now(owner);
let (tx, mut rx) = mpsc::unbounded_channel::<i32>();
let replacement = Service {
sender: create_stored(tx),
};
assert!(!stale.sender.is_live());
assert_eq!(crate::reactive::storage::slot_count(), 1);
stale.send(11);
replacement.send(23);
assert_eq!(rx.try_recv(), Ok(23));
assert_eq!(rx.try_recv(), Err(mpsc::error::TryRecvError::Empty));
assert_eq!(
old_rx.try_recv(),
Err(mpsc::error::TryRecvError::Disconnected)
);
}
#[test]
fn a_live_service_still_sends_and_ignores_a_closed_receiver() {
let (tx, mut rx) = mpsc::unbounded_channel::<i32>();
let (service, owner) = with_owner(|| Service {
sender: create_stored(tx),
});
service.send(17);
assert_eq!(rx.try_recv(), Ok(17));
drop(rx);
service.send(29);
dispose_owner_now(owner);
}
#[test]
fn test_service_without_ambient_runtime() {
let received = Arc::new(AtomicI32::new(0));
let received_clone = received.clone();
let (service, owner_id) = with_owner(|| {
create_service::<i32, _, _>(move |mut rx, _ctx| async move {
while let Some(v) = rx.recv().await {
received_clone.fetch_add(v, Ordering::SeqCst);
}
})
});
service.send(5);
let mut waited = 0;
while received.load(Ordering::SeqCst) == 0 && waited < 2000 {
std::thread::sleep(Duration::from_millis(10));
waited += 10;
}
assert_eq!(received.load(Ordering::SeqCst), 5);
dispose_owner_now(owner_id);
}
#[test]
fn a_task_allocates_no_channel() {
let before = crate::reactive::storage::slot_count();
let (_, owner_id) = with_owner(|| {
create_task(|ctx| async move {
let _ = ctx.is_running();
});
});
assert_eq!(
crate::reactive::storage::slot_count(),
before,
"a task must claim no arena slot"
);
dispose_owner_now(owner_id);
let before = crate::reactive::storage::slot_count();
let (_, owner_id) = with_owner(|| {
create_service::<i32, _, _>(|mut rx, _ctx| async move {
let _ = rx.recv().await;
})
});
assert!(crate::reactive::storage::slot_count() > before);
dispose_owner_now(owner_id);
}
#[tokio::test]
async fn test_service_stops_on_cleanup() {
let counter = Arc::new(AtomicI32::new(0));
let counter_clone = counter.clone();
let (_, owner_id) = with_owner(|| {
create_task(move |ctx| async move {
while ctx.is_running() {
counter_clone.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(10)).await;
}
});
});
tokio::time::sleep(Duration::from_millis(50)).await;
let count_before = counter.load(Ordering::SeqCst);
assert!(count_before > 0, "Service should have run at least once");
dispose_owner_now(owner_id);
tokio::time::sleep(Duration::from_millis(50)).await;
let count_after = counter.load(Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(50)).await;
let count_final = counter.load(Ordering::SeqCst);
assert_eq!(
count_after, count_final,
"Service should have stopped after owner disposal"
);
}
#[tokio::test]
async fn test_service_receives_commands() {
let received = Arc::new(AtomicI32::new(0));
let received_clone = received.clone();
let (service, owner_id) = with_owner(|| {
create_service::<i32, _, _>(move |mut rx, ctx| async move {
while ctx.is_running() {
match rx.try_recv() {
Ok(cmd) => {
received_clone.fetch_add(cmd, Ordering::SeqCst);
}
Err(_) => {
tokio::time::sleep(Duration::from_millis(5)).await;
}
}
}
})
});
service.send(10);
service.send(20);
service.send(30);
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(received.load(Ordering::SeqCst), 60);
dispose_owner_now(owner_id);
}
#[tokio::test]
async fn test_service_handle_is_clone() {
let received = Arc::new(AtomicI32::new(0));
let received_clone = received.clone();
let (service, owner_id) = with_owner(|| {
create_service::<i32, _, _>(move |mut rx, ctx| async move {
while ctx.is_running() {
match rx.try_recv() {
Ok(cmd) => {
received_clone.fetch_add(cmd, Ordering::SeqCst);
}
Err(_) => {
tokio::time::sleep(Duration::from_millis(5)).await;
}
}
}
})
});
let service2 = service;
service.send(5);
service2.send(7);
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(received.load(Ordering::SeqCst), 12);
dispose_owner_now(owner_id);
}
}
#[cfg(test)]
mod factory_bounds {
use super::*;
#[allow(dead_code)]
fn a_factory_may_read_a_non_send_value() {
let local = std::rc::Rc::new(7u8);
create_task(move |_ctx| {
let owned = *local;
async move {
let _ = owned;
}
});
let local = std::rc::Rc::new(9u8);
let _service = create_service::<(), _, _>(move |_rx, _ctx| {
let owned = *local;
async move {
let _ = owned;
}
});
}
}