mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
allow concurrency in service actor (#2592)
This commit is contained in:
4
core/Cargo.lock
generated
4
core/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
60
core/startos/src/util/actor/background.rs
Normal file
60
core/startos/src/util/actor/background.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
208
core/startos/src/util/actor/concurrent.rs
Normal file
208
core/startos/src/util/actor/concurrent.rs
Normal 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 => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
148
core/startos/src/util/actor/mod.rs
Normal file
148
core/startos/src/util/actor/mod.rs
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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> },
|
||||
}
|
||||
Reference in New Issue
Block a user