use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use tokio::sync::{Notify, Semaphore};
use super::{Lanes, app::App};
use harmos_runtime::builtin;
use harmos_runtime::operations::instance::{Outcome, Phase};
use harmos_runtime::operations::{Context, Host, Instance, Operation, OperationDeclaration, OperationPolicy, RestartPolicy, RetryPolicy};
use harmos_runtime::{Error, Refusal, Runtime};
const POLICY: OperationPolicy = OperationPolicy {
timeout: Duration::from_secs(5),
attempt_timeout: Duration::from_secs(5),
retries: RetryPolicy::Never,
restart: RestartPolicy::Never,
};
#[harmos_runtime::message]
#[harmos_runtime::operation(id = "fixture.hold", version = 1, response = u64, policy = POLICY)]
struct Hold {
value: u64,
}
struct Gate {
started: Notify,
release: Semaphore,
completed: AtomicUsize,
cleaned: AtomicUsize,
cleanup: Notify,
}
struct Holding(Arc<Gate>);
#[harmos_runtime::message]
#[harmos_runtime::operation(
id = "fixture.ticker",
version = 1,
response = (),
policy = harmos_runtime::services::POLICY
)]
struct Ticker {}
impl Operation for Holding {
type Declaration = Hold;
type Resources = Gate;
type Error = Error;
type Context<'a> = Context<'a>;
type Endpoints = ();
async fn initialize<'ctx>(gate: Arc<Gate>, _: Self::Context<'ctx>) -> Result<Self, Error> {
Ok(Self(gate))
}
async fn run<'ctx>(
&mut self, _: Self::Context<'ctx>, request: &mut Hold, _: &mut Self::Endpoints,
) -> Result<harmos_runtime::operations::Response<u64>, Error> {
self.0.started.notify_one();
self.0.release.acquire().await.unwrap().forget();
self.0.completed.fetch_add(1, Ordering::SeqCst);
Ok((request.value * 2).into())
}
async fn cleanup(&mut self) -> Result<(), Error> {
self.0.cleaned.fetch_add(1, Ordering::SeqCst);
self.0.cleanup.notify_one();
Ok(())
}
}
async fn runtime() -> (Runtime<App>, Arc<Gate>) {
let gate = Arc::new(Gate {
started: Notify::new(),
release: Semaphore::new(0),
completed: AtomicUsize::new(0),
cleaned: AtomicUsize::new(0),
cleanup: Notify::new(),
});
let runtime = Runtime::<App>::builder((), ()).register(Lanes::catalog()).runtime().await.unwrap();
runtime.operations.register_operation::<Holding>("app", Lanes::Open, gate.clone()).unwrap();
builtin::Catalog::register(&runtime.operations, "host", Lanes::Open).unwrap();
(runtime, gate)
}
async fn submitted<H: Host>(host: &H, request: Hold) -> Result<impl Instance<Hold, Error = H::Error>, H::Error> {
host.submit(request).await
}
fn hidden(error: Option<Error>) -> bool {
let Some(Error::OperationFailed { source, .. }) = error else {
return false;
};
matches!(source.downcast_ref::<Error>(), Some(Error::NotRegistered { .. }))
}
#[tokio::test]
async fn host_instances_settle() {
{
let (runtime, gate) = runtime().await;
let instance = submitted(&runtime.operations, Hold { value: 21 }).await.unwrap();
gate.started.notified().await;
let status = instance.status().await.unwrap();
assert_eq!(status.phase, Phase::Running);
assert_eq!(status.outcome, None);
gate.release.add_permits(1);
let response = instance.join().await.unwrap();
assert_eq!(*response, 42);
let called = Host::call(&runtime.operations, Hold { value: 4 });
gate.release.add_permits(1);
assert_eq!(*called.await.unwrap(), 8);
assert_eq!(gate.completed.load(Ordering::SeqCst), 2);
runtime.stop().await.unwrap();
}
{
let (runtime, gate) = runtime().await;
let instance = submitted(&runtime.operations, Hold { value: 1 }).await.unwrap();
gate.started.notified().await;
assert!(instance.cancel_and_join().await.is_err());
assert_eq!(gate.completed.load(Ordering::SeqCst), 0);
assert_eq!(gate.cleaned.load(Ordering::SeqCst), 1);
runtime.stop().await.unwrap();
}
{
let (runtime, gate) = runtime().await;
let instance = runtime.operations.submit(Hold { value: 1 }).unwrap();
gate.started.notified().await;
let cleaned = gate.cleanup.notified();
drop(instance);
cleaned.await;
assert_eq!(gate.completed.load(Ordering::SeqCst), 0);
runtime.stop().await.unwrap();
}
{
let (runtime, gate) = runtime().await;
let instance = runtime.operations.submit(Hold { value: 1 }).unwrap();
gate.started.notified().await;
let id = Instance::detach(instance);
let status = runtime.operations.call(builtin::Status::of(id)).await;
assert_eq!(status.unwrap().phase, Phase::Running.as_str());
let cleaned = gate.cleanup.notified();
gate.release.add_permits(1);
cleaned.await;
assert_eq!(gate.completed.load(Ordering::SeqCst), 1);
runtime.stop().await.unwrap();
}
}
#[tokio::test]
async fn builtin_controls_instance() {
let (runtime, gate) = runtime().await;
let operations = &runtime.operations;
let mut instance = operations.submit(Hold { value: 1 }).unwrap();
gate.started.notified().await;
let id = instance.id();
let status = operations.call(builtin::Status::of(id)).await.unwrap();
assert_eq!(status.phase, Phase::Running.as_str());
assert_eq!(status.outcome, "");
assert!(!status.cancellation_requested);
let cancelled = operations.call(builtin::Cancel::of(id)).await.unwrap();
assert!(cancelled.cancellation_requested);
assert!(matches!((&mut instance).await, Err(Error::OperationRefused(Refusal::Cancelled))));
instance.settle().await.unwrap();
let settled = operations.call(builtin::Status::of(id)).await;
assert!(hidden(settled.err()));
let unknown = harmos_runtime::operations::OperationInstanceId::new(u64::MAX);
let refused = operations.call(builtin::Cancel::of(unknown)).await;
assert!(hidden(refused.err()));
let status = harmos_runtime::operations::instance::Status::try_from(&*cancelled);
assert_ne!(status.unwrap().outcome, Some(Outcome::Succeeded));
runtime.stop().await.unwrap();
}
#[tokio::test]
async fn resident_leases_watched() {
let (runtime, gate) = runtime().await;
let operations = &runtime.operations;
operations
.register_service::<App, Gate, _, _>(harmos_runtime::services::ServiceDefinition::new(Ticker::ID), |gate: Arc<Gate>| {
move |mut context: harmos_runtime::services::Service<App>| {
let gate = gate.clone();
async move {
context.ready();
context.stopping().await;
gate.cleaned.fetch_add(1, Ordering::SeqCst);
Ok::<(), harmos_runtime::services::Fault<&str>>(())
}
}
})
.unwrap();
let first = operations.submit_with_resources::<Ticker, Gate>(Ticker {}, gate.clone()).await.unwrap();
let second = operations.submit_with_resources::<Ticker, Gate>(Ticker {}, gate.clone()).await.unwrap();
assert_eq!(first.id(), second.id());
first.ready().await.unwrap();
let id = first.id();
let key = operations.resident_key(id).unwrap();
assert_eq!(operations.resident(&key).unwrap().leases, 2);
let status = operations.call(builtin::Status::of(id)).await.unwrap();
assert_eq!(status.phase, Phase::Running.as_str());
let settings = harmos_runtime::operations::StreamSettings {
items: std::num::NonZeroUsize::new(16).unwrap(),
bytes: std::num::NonZeroUsize::new(4096).unwrap(),
};
let mut watch = operations.download::<builtin::Watch>(builtin::Watch::of(id), settings).unwrap();
let current = watch.next().await.unwrap().unwrap();
assert_eq!(current.phase, Phase::Running.as_str());
drop(first);
assert_eq!(operations.resident(&key).unwrap().leases, 1);
assert_eq!(gate.cleaned.load(Ordering::SeqCst), 0);
second.cancel();
let mut phases = Vec::new();
while let Some(state) = watch.next().await.unwrap() {
phases.push(state.phase.clone());
}
let last = watch.join().await.unwrap();
assert_eq!(last.phase, Phase::Stopped.as_str());
assert_eq!(phases.last().map(String::as_str), Some(Phase::Stopped.as_str()));
assert!(phases.iter().any(|phase| phase == Phase::Stopping.as_str()));
second.settle().await.unwrap();
assert_eq!(gate.cleaned.load(Ordering::SeqCst), 1);
let stopped = operations.call(builtin::Status::of(id)).await;
assert!(hidden(stopped.err()));
runtime.stop().await.unwrap();
}