allow concurrency in service actor (#2592)

This commit is contained in:
Aiden McClelland
2024-04-08 11:53:35 -06:00
committed by GitHub
parent 75ff541aec
commit e41f8f1d0f
16 changed files with 535 additions and 129 deletions

4
core/Cargo.lock generated
View File

@@ -4968,9 +4968,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.34.0"
version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [
"backtrace",
"bytes",

View File

@@ -160,7 +160,7 @@ ssh-key = { version = "0.6.2", features = ["ed25519"] }
stderrlog = "0.5.4"
tar = "0.4.40"
thiserror = "1.0.49"
tokio = { version = "1", features = ["full"] }
tokio = { version = "1.37", features = ["full"] }
tokio-rustls = "0.25.0"
tokio-socks = "0.5.1"
tokio-stream = { version = "0.1.14", features = ["io-util", "sync", "net"] }

View File

@@ -1,3 +1,4 @@
use std::collections::BTreeSet;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@@ -154,7 +155,7 @@ impl S9pk<Section<MultiCursorFile>> {
i.strip_prefix(&format!("start9/{}/", manifest.id))
.map(|s| s.to_owned())
}) {
new_manifest.images.push(image.parse()?);
new_manifest.images.insert(image.parse()?);
let sqfs_path = images_dir.join(&image).with_extension("squashfs");
let image_name = format!("start9/{}/{}:{}", manifest.id, image, manifest.version);
let id = String::from_utf8(
@@ -334,7 +335,7 @@ impl From<ManifestV1> for Manifest {
marketing_site: value.marketing_site.unwrap_or_else(|| default_url.clone()),
donation_url: value.donation_url,
description: value.description,
images: Vec::new(),
images: BTreeSet::new(),
assets: value
.volumes
.iter()

View File

@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use color_eyre::eyre::eyre;
use helpers::const_true;
@@ -43,9 +43,9 @@ pub struct Manifest {
#[ts(type = "string | null")]
pub donation_url: Option<Url>,
pub description: Description,
pub images: Vec<ImageId>,
pub assets: Vec<VolumeId>, // TODO: AssetsId
pub volumes: Vec<VolumeId>,
pub images: BTreeSet<ImageId>,
pub assets: BTreeSet<VolumeId>, // TODO: AssetsId
pub volumes: BTreeSet<VolumeId>,
#[serde(default)]
pub alerts: Alerts,
#[serde(default)]

View File

@@ -4,19 +4,27 @@ use models::{ActionId, ProcedureName};
use crate::action::ActionResult;
use crate::prelude::*;
use crate::service::config::GetConfig;
use crate::service::dependencies::DependencyConfig;
use crate::service::{Service, ServiceActor};
use crate::util::actor::{BackgroundJobs, Handler};
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::actor::{ConflictBuilder, Handler};
struct Action {
pub(super) struct Action {
id: ActionId,
input: Value,
}
impl Handler<Action> for ServiceActor {
type Response = Result<ActionResult, Error>;
fn conflicts_with(_: &Action) -> ConflictBuilder<Self> {
ConflictBuilder::everything()
.except::<GetConfig>()
.except::<DependencyConfig>()
}
async fn handle(
&mut self,
Action { id, input }: Action,
_: &mut BackgroundJobs,
_: &BackgroundJobQueue,
) -> Self::Response {
let container = &self.0.persistent_container;
container

View File

@@ -3,19 +3,24 @@ use std::time::Duration;
use models::ProcedureName;
use crate::config::action::ConfigRes;
use crate::config::{action::SetResult, ConfigureContext};
use crate::config::ConfigureContext;
use crate::prelude::*;
use crate::service::dependencies::DependencyConfig;
use crate::service::{Service, ServiceActor};
use crate::util::actor::{BackgroundJobs, Handler};
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::actor::{ConflictBuilder, Handler};
use crate::util::serde::NoOutput;
struct Configure(ConfigureContext);
pub(super) struct Configure(ConfigureContext);
impl Handler<Configure> for ServiceActor {
type Response = Result<(), Error>;
fn conflicts_with(_: &Configure) -> ConflictBuilder<Self> {
ConflictBuilder::everything().except::<DependencyConfig>()
}
async fn handle(
&mut self,
Configure(ConfigureContext { timeout, config }): Configure,
_: &mut BackgroundJobs,
_: &BackgroundJobQueue,
) -> Self::Response {
let container = &self.0.persistent_container;
let package_id = &self.0.id;
@@ -41,10 +46,13 @@ impl Handler<Configure> for ServiceActor {
}
}
struct GetConfig;
pub(super) struct GetConfig;
impl Handler<GetConfig> for ServiceActor {
type Response = Result<ConfigRes, Error>;
async fn handle(&mut self, _: GetConfig, _: &mut BackgroundJobs) -> Self::Response {
fn conflicts_with(_: &GetConfig) -> ConflictBuilder<Self> {
ConflictBuilder::nothing().except::<Configure>()
}
async fn handle(&mut self, _: GetConfig, _: &BackgroundJobQueue) -> Self::Response {
let container = &self.0.persistent_container;
container
.execute::<ConfigRes>(

View File

@@ -1,13 +1,21 @@
use crate::prelude::*;
use crate::service::config::GetConfig;
use crate::service::dependencies::DependencyConfig;
use crate::service::start_stop::StartStop;
use crate::service::transition::TransitionKind;
use crate::service::{Service, ServiceActor};
use crate::util::actor::{BackgroundJobs, Handler};
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::actor::{ConflictBuilder, Handler};
struct Start;
pub(super) struct Start;
impl Handler<Start> for ServiceActor {
type Response = ();
async fn handle(&mut self, _: Start, _: &mut BackgroundJobs) -> Self::Response {
fn conflicts_with(_: &Start) -> ConflictBuilder<Self> {
ConflictBuilder::everything()
.except::<GetConfig>()
.except::<DependencyConfig>()
}
async fn handle(&mut self, _: Start, _: &BackgroundJobQueue) -> Self::Response {
self.0.persistent_container.state.send_modify(|x| {
x.desired_state = StartStop::Start;
});
@@ -23,7 +31,12 @@ impl Service {
struct Stop;
impl Handler<Stop> for ServiceActor {
type Response = ();
async fn handle(&mut self, _: Stop, _: &mut BackgroundJobs) -> Self::Response {
fn conflicts_with(_: &Stop) -> ConflictBuilder<Self> {
ConflictBuilder::everything()
.except::<GetConfig>()
.except::<DependencyConfig>()
}
async fn handle(&mut self, _: Stop, _: &BackgroundJobQueue) -> Self::Response {
let mut transition_state = None;
self.0.persistent_container.state.send_modify(|x| {
x.desired_state = StartStop::Stop;

View File

@@ -5,22 +5,26 @@ use models::{PackageId, ProcedureName};
use crate::prelude::*;
use crate::service::{Service, ServiceActor};
use crate::util::actor::{BackgroundJobs, Handler};
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::actor::{ConflictBuilder, Handler};
use crate::Config;
struct DependencyConfig {
pub(super) struct DependencyConfig {
dependency_id: PackageId,
remote_config: Option<Config>,
}
impl Handler<DependencyConfig> for ServiceActor {
type Response = Result<Option<Config>, Error>;
fn conflicts_with(_: &DependencyConfig) -> ConflictBuilder<Self> {
ConflictBuilder::nothing()
}
async fn handle(
&mut self,
DependencyConfig {
dependency_id,
remote_config,
}: DependencyConfig,
_: &mut BackgroundJobs,
_: &BackgroundJobQueue,
) -> Self::Response {
let container = &self.0.persistent_container;
container

View File

@@ -13,7 +13,6 @@ use start_stop::StartStop;
use tokio::sync::Notify;
use ts_rs::TS;
use crate::config::action::ConfigRes;
use crate::context::{CliContext, RpcContext};
use crate::core::rpc_continuations::RequestGuid;
use crate::db::model::package::{
@@ -28,7 +27,9 @@ use crate::service::service_map::InstallProgressHandles;
use crate::service::transition::TransitionKind;
use crate::status::health_check::HealthCheckResult;
use crate::status::MainStatus;
use crate::util::actor::{Actor, BackgroundJobs, SimpleActor};
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::actor::concurrent::ConcurrentActor;
use crate::util::actor::Actor;
use crate::util::serde::Pem;
use crate::volume::data_dir;
@@ -66,7 +67,7 @@ pub enum LoadDisposition {
}
pub struct Service {
actor: SimpleActor<ServiceActor>,
actor: ConcurrentActor<ServiceActor>,
seed: Arc<ServiceActorSeed>,
}
impl Service {
@@ -90,7 +91,7 @@ impl Service {
.init(Arc::downgrade(&seed))
.await?;
Ok(Self {
actor: SimpleActor::new(ServiceActor(seed.clone())),
actor: ConcurrentActor::new(ServiceActor(seed.clone())),
seed,
})
}
@@ -391,10 +392,11 @@ impl ServiceActorSeed {
});
}
}
#[derive(Clone)]
struct ServiceActor(Arc<ServiceActorSeed>);
impl Actor for ServiceActor {
fn init(&mut self, jobs: &mut BackgroundJobs) {
fn init(&mut self, jobs: &BackgroundJobQueue) {
let seed = self.0.clone();
jobs.add_job(async move {
let id = seed.id.clone();

View File

@@ -11,7 +11,7 @@ use clap::Parser;
use emver::VersionRange;
use imbl::OrdMap;
use imbl_value::{json, InternedString};
use models::{ActionId, HealthCheckId, HostId, ImageId, PackageId, VolumeId};
use models::{ActionId, DataUrl, HealthCheckId, HostId, ImageId, PackageId, VolumeId};
use patch_db::json_ptr::JsonPointer;
use rpc_toolkit::{from_fn, from_fn_async, AnyContext, Context, Empty, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
@@ -28,7 +28,9 @@ use crate::disk::mount::filesystem::overlayfs::OverlayGuard;
use crate::net::host::binding::BindOptions;
use crate::net::host::HostKind;
use crate::prelude::*;
use crate::s9pk::merkle_archive::source::http::{HttpReader, HttpSource};
use crate::s9pk::rpc::SKIP_ENV;
use crate::s9pk::S9pk;
use crate::service::cli::ContainerCliContext;
use crate::service::ServiceActorSeed;
use crate::status::health_check::HealthCheckResult;
@@ -1145,11 +1147,36 @@ async fn set_dependencies(
version_spec,
),
};
let icon = todo!();
let title = todo!();
let (icon, title) = match async {
let remote_s9pk = S9pk::deserialize(
&HttpSource::new(
ctx.ctx.client.clone(),
registry_url
.join(&format!("package/v2/{}.s9pk?spec={}", dep_id, version_spec))?,
)
.await?,
)
.await?;
let icon = remote_s9pk.icon_data_url().await?;
Ok::<_, Error>((icon, remote_s9pk.as_manifest().title.clone()))
}
.await
{
Ok(a) => a,
Err(e) => {
tracing::error!("Error fetching remote s9pk: {e}");
tracing::debug!("{e:?}");
(
DataUrl::from_slice("image/png", include_bytes!("../install/package-icon.png")),
dep_id.to_string(),
)
}
};
let config_satisfied = if let Some(dep_service) = &*ctx.ctx.services.get(&dep_id).await {
service
.dependency_config(dep_id, dep_service.get_config().await?.config)
.dependency_config(dep_id.clone(), dep_service.get_config().await?.config)
.await?
.is_none()
} else {
@@ -1158,7 +1185,7 @@ async fn set_dependencies(
deps.insert(
dep_id,
CurrentDependencyInfo {
kind: CurrentDependencyKind::Exists,
kind,
registry_url,
version_spec,
icon,

View File

@@ -5,7 +5,7 @@ use tokio::sync::watch;
use super::persistent_container::ServiceState;
use crate::service::start_stop::StartStop;
use crate::util::actor::BackgroundJobs;
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::future::{CancellationHandle, RemoteCancellable};
pub mod backup;
@@ -41,7 +41,7 @@ impl TransitionState {
fn new(
task: impl Future<Output = ()> + Send + 'static,
kind: TransitionKind,
jobs: &mut BackgroundJobs,
jobs: &BackgroundJobQueue,
) -> Self {
let task = RemoteCancellable::new(task);
let cancel_handle = task.cancellation_handle();

View File

@@ -1,18 +1,24 @@
use std::sync::Arc;
use futures::FutureExt;
use super::TempDesiredState;
use crate::prelude::*;
use crate::service::config::GetConfig;
use crate::service::dependencies::DependencyConfig;
use crate::service::transition::{TransitionKind, TransitionState};
use crate::service::{Service, ServiceActor};
use crate::util::actor::{BackgroundJobs, Handler};
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::actor::{ConflictBuilder, Handler};
use crate::util::future::RemoteCancellable;
struct Restart;
pub(super) struct Restart;
impl Handler<Restart> for ServiceActor {
type Response = ();
async fn handle(&mut self, _: Restart, jobs: &mut BackgroundJobs) -> Self::Response {
fn conflicts_with(_: &Restart) -> ConflictBuilder<Self> {
ConflictBuilder::everything()
.except::<GetConfig>()
.except::<DependencyConfig>()
}
async fn handle(&mut self, _: Restart, jobs: &BackgroundJobQueue) -> Self::Response {
// So Need a handle to just a single field in the state
let temp = TempDesiredState::new(&self.0.persistent_container.state);
let mut current = self.0.persistent_container.state.subscribe();

View File

@@ -0,0 +1,60 @@
use futures::future::BoxFuture;
use futures::{Future, FutureExt};
use tokio::sync::mpsc;
#[derive(Clone)]
pub struct BackgroundJobQueue(mpsc::UnboundedSender<BoxFuture<'static, ()>>);
impl BackgroundJobQueue {
pub fn new() -> (Self, BackgroundJobRunner) {
let (send, recv) = mpsc::unbounded_channel();
(
Self(send),
BackgroundJobRunner {
recv,
jobs: Vec::new(),
},
)
}
pub fn add_job(&self, fut: impl Future<Output = ()> + Send + 'static) {
let _ = self.0.send(fut.boxed());
}
}
pub struct BackgroundJobRunner {
recv: mpsc::UnboundedReceiver<BoxFuture<'static, ()>>,
jobs: Vec<BoxFuture<'static, ()>>,
}
impl BackgroundJobRunner {
pub fn is_empty(&self) -> bool {
self.recv.is_empty() && self.jobs.is_empty()
}
}
impl Future for BackgroundJobRunner {
type Output = ();
fn poll(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
while let std::task::Poll::Ready(Some(job)) = self.recv.poll_recv(cx) {
self.jobs.push(job);
}
let complete = self
.jobs
.iter_mut()
.enumerate()
.filter_map(|(i, f)| match f.poll_unpin(cx) {
std::task::Poll::Pending => None,
std::task::Poll::Ready(_) => Some(i),
})
.collect::<Vec<_>>();
for idx in complete.into_iter().rev() {
#[allow(clippy::let_underscore_future)]
let _ = self.jobs.swap_remove(idx);
}
if self.jobs.is_empty() && self.recv.is_closed() {
std::task::Poll::Ready(())
} else {
std::task::Poll::Pending
}
}
}

View File

@@ -0,0 +1,208 @@
use std::any::Any;
use std::sync::Arc;
use std::time::Duration;
use futures::future::{ready, BoxFuture};
use futures::{Future, FutureExt, TryFutureExt};
use helpers::NonDetachingJoinHandle;
use tokio::sync::{mpsc, oneshot};
use crate::prelude::*;
use crate::util::actor::background::{BackgroundJobQueue, BackgroundJobRunner};
use crate::util::actor::{Actor, ConflictFn, Handler, PendingMessageStrategy, Request};
#[pin_project::pin_project]
struct ConcurrentRunner<A> {
actor: A,
shutdown: Option<oneshot::Receiver<()>>,
waiting: Vec<Request<A>>,
recv: mpsc::UnboundedReceiver<Request<A>>,
handlers: Vec<(
Arc<ConflictFn<A>>,
oneshot::Sender<Box<dyn Any + Send>>,
BoxFuture<'static, Box<dyn Any + Send>>,
)>,
queue: BackgroundJobQueue,
#[pin]
bg_runner: BackgroundJobRunner,
}
impl<A: Actor + Clone> Future for ConcurrentRunner<A> {
type Output = ();
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let mut this = self.project();
*this.shutdown = this.shutdown.take().and_then(|mut s| {
if s.poll_unpin(cx).is_pending() {
Some(s)
} else {
None
}
});
if this.shutdown.is_some() {
while let std::task::Poll::Ready(Some((msg, reply))) = this.recv.poll_recv(cx) {
if this.handlers.iter().any(|(f, _, _)| f(&*msg)) {
this.waiting.push((msg, reply));
} else {
let mut actor = this.actor.clone();
let queue = this.queue.clone();
this.handlers.push((
msg.conflicts_with(),
reply,
async move { msg.handle_with(&mut actor, &queue).await }.boxed(),
))
}
}
}
// handlers
while {
let mut cont = false;
let complete = this
.handlers
.iter_mut()
.enumerate()
.filter_map(|(i, (_, _, f))| match f.poll_unpin(cx) {
std::task::Poll::Pending => None,
std::task::Poll::Ready(res) => Some((i, res)),
})
.collect::<Vec<_>>();
for (idx, res) in complete.into_iter().rev() {
#[allow(clippy::let_underscore_future)]
let (f, reply, _) = this.handlers.swap_remove(idx);
let _ = reply.send(res);
// TODO: replace with Vec::extract_if once stable
if this.shutdown.is_some() {
let mut i = 0;
while i < this.waiting.len() {
if f(&*this.waiting[i].0)
&& !this.handlers.iter().any(|(f, _, _)| f(&*this.waiting[i].0))
{
let (msg, reply) = this.waiting.remove(i);
let mut actor = this.actor.clone();
let queue = this.queue.clone();
this.handlers.push((
msg.conflicts_with(),
reply,
async move { msg.handle_with(&mut actor, &queue).await }.boxed(),
));
cont = true;
} else {
i += 1;
}
}
}
}
cont
} {}
let _ = this.bg_runner.as_mut().poll(cx);
if this.waiting.is_empty() && this.handlers.is_empty() && this.bg_runner.is_empty() {
std::task::Poll::Ready(())
} else {
std::task::Poll::Pending
}
}
}
pub struct ConcurrentActor<A: Actor + Clone> {
shutdown: oneshot::Sender<()>,
runtime: NonDetachingJoinHandle<()>,
messenger: mpsc::UnboundedSender<Request<A>>,
}
impl<A: Actor + Clone> ConcurrentActor<A> {
pub fn new(mut actor: A) -> Self {
let (shutdown_send, shutdown_recv) = oneshot::channel();
let (messenger_send, messenger_recv) = mpsc::unbounded_channel::<Request<A>>();
let runtime = NonDetachingJoinHandle::from(tokio::spawn(async move {
let (queue, runner) = BackgroundJobQueue::new();
actor.init(&queue);
ConcurrentRunner {
actor,
shutdown: Some(shutdown_recv),
waiting: Vec::new(),
recv: messenger_recv,
handlers: Vec::new(),
queue,
bg_runner: runner,
}
.await
}));
Self {
shutdown: shutdown_send,
runtime,
messenger: messenger_send,
}
}
/// Message is guaranteed to be queued immediately
pub fn queue<M: Send + 'static>(
&self,
message: M,
) -> impl Future<Output = Result<A::Response, Error>>
where
A: Handler<M>,
{
if self.runtime.is_finished() {
return futures::future::Either::Left(ready(Err(Error::new(
eyre!("actor runtime has exited"),
ErrorKind::Unknown,
))));
}
let (reply_send, reply_recv) = oneshot::channel();
self.messenger
.send((Box::new(message), reply_send))
.unwrap();
futures::future::Either::Right(
reply_recv
.map_err(|_| Error::new(eyre!("actor runtime has exited"), ErrorKind::Unknown))
.and_then(|a| {
ready(
a.downcast()
.map_err(|_| {
Error::new(
eyre!("received incorrect type in response"),
ErrorKind::Incoherent,
)
})
.map(|a| *a),
)
}),
)
}
pub async fn send<M: Send + 'static>(&self, message: M) -> Result<A::Response, Error>
where
A: Handler<M>,
{
self.queue(message).await
}
pub async fn shutdown(self, strategy: PendingMessageStrategy) {
drop(self.messenger);
let timeout = match strategy {
PendingMessageStrategy::CancelAll => {
self.shutdown.send(()).unwrap();
Some(Duration::from_secs(0))
}
PendingMessageStrategy::FinishCurrentCancelPending { timeout } => {
self.shutdown.send(()).unwrap();
timeout
}
PendingMessageStrategy::FinishAll { timeout } => timeout,
};
let aborter = if let Some(timeout) = timeout {
let hdl = self.runtime.abort_handle();
async move {
tokio::time::sleep(timeout).await;
hdl.abort();
}
.boxed()
} else {
futures::future::pending().boxed()
};
tokio::select! {
_ = aborter => (),
_ = self.runtime => (),
}
}
}

View File

@@ -0,0 +1,148 @@
use std::any::{Any, TypeId};
use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Duration;
use futures::future::BoxFuture;
use futures::{Future, FutureExt};
use tokio::sync::oneshot;
#[allow(unused_imports)]
use crate::prelude::*;
use crate::util::actor::background::BackgroundJobQueue;
pub mod background;
pub mod concurrent;
pub mod simple;
pub trait Actor: Sized + Send + 'static {
#[allow(unused_variables)]
fn init(&mut self, jobs: &BackgroundJobQueue) {}
}
pub trait Handler<M: Any + Send>: Actor {
type Response: Any + Send;
/// DRAGONS: this must be correctly implemented bi-directionally in order to work as expected
fn conflicts_with(#[allow(unused_variables)] msg: &M) -> ConflictBuilder<Self> {
ConflictBuilder::everything()
}
fn handle(
&mut self,
msg: M,
jobs: &BackgroundJobQueue,
) -> impl Future<Output = Self::Response> + Send;
}
type ConflictFn<A> = dyn Fn(&dyn Message<A>) -> bool + Send + Sync;
trait Message<A>: Send + Any {
fn conflicts_with(&self) -> Arc<ConflictFn<A>>;
fn handle_with<'a>(
self: Box<Self>,
actor: &'a mut A,
jobs: &'a BackgroundJobQueue,
) -> BoxFuture<'a, Box<dyn Any + Send>>;
}
impl<M: Send + Any, A: Actor> Message<A> for M
where
A: Handler<M>,
{
fn conflicts_with(&self) -> Arc<ConflictFn<A>> {
A::conflicts_with(self).build()
}
fn handle_with<'a>(
self: Box<Self>,
actor: &'a mut A,
jobs: &'a BackgroundJobQueue,
) -> BoxFuture<'a, Box<dyn Any + Send>> {
async move { Box::new(actor.handle(*self, jobs).await) as Box<dyn Any + Send> }.boxed()
}
}
impl<A: Actor> dyn Message<A> {
#[inline]
pub fn is<M: Message<A>>(&self) -> bool {
let t = TypeId::of::<M>();
let concrete = self.type_id();
t == concrete
}
#[inline]
pub unsafe fn downcast_ref_unchecked<M: Message<A>>(&self) -> &M {
debug_assert!(self.is::<M>());
unsafe { &*(self as *const dyn Message<A> as *const M) }
}
#[inline]
fn downcast_ref<M: Message<A>>(&self) -> Option<&M> {
if self.is::<M>() {
unsafe { Some(self.downcast_ref_unchecked()) }
} else {
None
}
}
}
type Request<A> = (Box<dyn Message<A>>, oneshot::Sender<Box<dyn Any + Send>>);
pub enum PendingMessageStrategy {
CancelAll,
FinishCurrentCancelPending { timeout: Option<Duration> },
FinishAll { timeout: Option<Duration> },
}
pub struct ConflictBuilder<A> {
base: bool,
except: BTreeMap<TypeId, Option<Box<dyn Fn(&dyn Message<A>) -> bool + Send + Sync>>>,
}
impl<A: Actor> ConflictBuilder<A> {
pub const fn everything() -> Self {
Self {
base: true,
except: BTreeMap::new(),
}
}
pub const fn nothing() -> Self {
Self {
base: false,
except: BTreeMap::new(),
}
}
pub fn except<M: Any + Send>(mut self) -> Self
where
A: Handler<M>,
{
self.except.insert(TypeId::of::<M>(), None);
self
}
pub fn except_if<M: Any + Send, F: Fn(&M) -> bool + Send + Sync + 'static>(
mut self,
f: F,
) -> Self
where
A: Handler<M>,
{
self.except.insert(
TypeId::of::<M>(),
Some(Box::new(move |m| {
if let Some(m) = m.downcast_ref() {
f(m)
} else {
false
}
})),
);
self
}
fn build(self) -> Arc<ConflictFn<A>> {
Arc::new(move |m| {
self.base
^ if let Some(entry) = self.except.get(&m.type_id()) {
if let Some(f) = entry {
f(m)
} else {
true
}
} else {
false
}
})
}
}

View File

@@ -1,85 +1,14 @@
use std::any::Any;
use std::future::ready;
use std::time::Duration;
use futures::future::BoxFuture;
use futures::future::ready;
use futures::{Future, FutureExt, TryFutureExt};
use helpers::NonDetachingJoinHandle;
use tokio::sync::oneshot::error::TryRecvError;
use tokio::sync::{mpsc, oneshot};
use crate::prelude::*;
use crate::util::Never;
pub trait Actor: Send + 'static {
#[allow(unused_variables)]
fn init(&mut self, jobs: &mut BackgroundJobs) {}
}
pub trait Handler<M>: Actor {
type Response: Any + Send;
fn handle(
&mut self,
msg: M,
jobs: &mut BackgroundJobs,
) -> impl Future<Output = Self::Response> + Send;
}
#[async_trait::async_trait]
trait Message<A>: Send {
async fn handle_with(
self: Box<Self>,
actor: &mut A,
jobs: &mut BackgroundJobs,
) -> Box<dyn Any + Send>;
}
#[async_trait::async_trait]
impl<M: Send, A: Actor> Message<A> for M
where
A: Handler<M>,
{
async fn handle_with(
self: Box<Self>,
actor: &mut A,
jobs: &mut BackgroundJobs,
) -> Box<dyn Any + Send> {
Box::new(actor.handle(*self, jobs).await)
}
}
type Request<A> = (Box<dyn Message<A>>, oneshot::Sender<Box<dyn Any + Send>>);
#[derive(Default)]
pub struct BackgroundJobs {
jobs: Vec<BoxFuture<'static, ()>>,
}
impl BackgroundJobs {
pub fn add_job(&mut self, fut: impl Future<Output = ()> + Send + 'static) {
self.jobs.push(fut.boxed());
}
}
impl Future for BackgroundJobs {
type Output = Never;
fn poll(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let complete = self
.jobs
.iter_mut()
.enumerate()
.filter_map(|(i, f)| match f.poll_unpin(cx) {
std::task::Poll::Pending => None,
std::task::Poll::Ready(_) => Some(i),
})
.collect::<Vec<_>>();
for idx in complete.into_iter().rev() {
#[allow(clippy::let_underscore_future)]
let _ = self.jobs.swap_remove(idx);
}
std::task::Poll::Pending
}
}
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::actor::{Actor, Handler, PendingMessageStrategy, Request};
pub struct SimpleActor<A: Actor> {
shutdown: oneshot::Sender<()>,
@@ -91,19 +20,17 @@ impl<A: Actor> SimpleActor<A> {
let (shutdown_send, mut shutdown_recv) = oneshot::channel();
let (messenger_send, mut messenger_recv) = mpsc::unbounded_channel::<Request<A>>();
let runtime = NonDetachingJoinHandle::from(tokio::spawn(async move {
let mut bg = BackgroundJobs::default();
actor.init(&mut bg);
let (queue, mut runner) = BackgroundJobQueue::new();
actor.init(&queue);
loop {
tokio::select! {
_ = &mut bg => (),
_ = &mut runner => (),
msg = messenger_recv.recv() => match msg {
Some((msg, reply)) if shutdown_recv.try_recv() == Err(TryRecvError::Empty) => {
let mut new_bg = BackgroundJobs::default();
tokio::select! {
res = msg.handle_with(&mut actor, &mut new_bg) => { let _ = reply.send(res); },
_ = &mut bg => (),
res = msg.handle_with(&mut actor, &queue) => { let _ = reply.send(res); },
_ = &mut runner => (),
}
bg.jobs.append(&mut new_bg.jobs);
}
_ => break,
},
@@ -189,9 +116,3 @@ impl<A: Actor> SimpleActor<A> {
}
}
}
pub enum PendingMessageStrategy {
CancelAll,
FinishCurrentCancelPending { timeout: Option<Duration> },
FinishAll { timeout: Option<Duration> },
}