use crate::Context;
use crate::effect::BoxFuture;
use std::any::{Any, TypeId};
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::error::Error;
use std::future::Future;
use std::sync::Arc;
#[derive(Clone)]
pub(crate) struct ConfiguredLayer {
service_type: TypeId,
value: Arc<dyn Any + Send + Sync>,
}
impl ConfiguredLayer {
pub(crate) fn new<S: crate::Service + 'static, L: Send + Sync + 'static>(value: L) -> Self {
Self {
service_type: TypeId::of::<S>(),
value: Arc::new(value),
}
}
pub(crate) fn matches<S: crate::Service + 'static>(&self) -> bool {
self.service_type == TypeId::of::<S>()
}
pub(crate) fn downcast_ref<L: 'static>(&self) -> Option<&L> {
self.value.downcast_ref()
}
}
#[derive(Clone)]
pub(crate) struct InjectEntry {
pub(crate) name: String,
pub(crate) configured: Option<ConfiguredLayer>,
}
#[derive(Clone, Default)]
#[must_use = "an InjectSpec has no effect until it is sealed into a PreparedPlugin"]
pub struct InjectSpec {
entries: BTreeMap<String, InjectEntry>,
}
impl std::fmt::Debug for InjectSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_map()
.entries(
self.entries
.values()
.map(|entry| (&entry.name, entry.configured.is_some())),
)
.finish()
}
}
impl InjectSpec {
pub fn none() -> Self {
Self::default()
}
pub fn require(mut self, name: impl Into<String>) -> Self {
let name = name.into();
self.entries.insert(
name.clone(),
InjectEntry {
name,
configured: None,
},
);
self
}
pub fn require_configured<S: crate::ConfigurableService>(mut self, layer: S::Layer) -> Self {
let name = S::NAME.to_owned();
self.entries.insert(
name.clone(),
InjectEntry {
name,
configured: Some(ConfiguredLayer::new::<S, _>(layer)),
},
);
self
}
pub(crate) fn overlay(mut self, overlay: Self) -> Self {
self.entries.extend(overlay.entries);
self
}
pub(crate) fn entries(&self) -> impl Iterator<Item = &InjectEntry> {
self.entries.values()
}
}
pub trait Plugin: Send + 'static {
type Config;
type Input: Send + 'static;
type PrepareError: Error;
type ApplyError: Error;
fn name(&self) -> Cow<'_, str> {
Cow::Borrowed(std::any::type_name::<Self>())
}
fn inject(&self) -> InjectSpec {
InjectSpec::none()
}
fn prepare(&self, config: Self::Config) -> Result<Self::Input, Self::PrepareError>;
fn apply(
&self,
ctx: Context,
input: &Self::Input,
) -> impl Future<Output = Result<(), Self::ApplyError>> + Send;
}
impl<P: Plugin + Sync> Plugin for Arc<P> {
type Config = P::Config;
type Input = P::Input;
type PrepareError = P::PrepareError;
type ApplyError = P::ApplyError;
fn name(&self) -> Cow<'_, str> {
(**self).name()
}
fn inject(&self) -> InjectSpec {
(**self).inject()
}
fn prepare(&self, config: Self::Config) -> Result<Self::Input, Self::PrepareError> {
(**self).prepare(config)
}
fn apply(
&self,
ctx: Context,
input: &Self::Input,
) -> impl Future<Output = Result<(), Self::ApplyError>> + Send {
(**self).apply(ctx, input)
}
}
pub(crate) trait SealedPlugin: Send + Sync {
fn apply_boxed(
self: Arc<Self>,
ctx: Context,
) -> BoxFuture<Result<(), crate::fiber::PluginFailure>>;
fn swap_input(&self, change: PreparedChange) -> Result<(), PreparedChange>;
fn into_successor(
self: Arc<Self>,
change: PreparedChange,
) -> Result<Arc<dyn SealedPlugin>, PreparedChange>;
}
struct TypedPlugin<P: Plugin> {
state: parking_lot::Mutex<Option<(P, P::Input)>>,
}
struct StateLease<P: Plugin> {
owner: Arc<TypedPlugin<P>>,
state: Option<(P, P::Input)>,
}
impl<P: Plugin> Drop for StateLease<P> {
fn drop(&mut self) {
let state = self
.state
.take()
.expect("a Plugin state lease owns its value");
let replaced = self.owner.state.lock().replace(state);
debug_assert!(replaced.is_none(), "a Plugin cannot apply concurrently");
drop(replaced);
}
}
impl<P: Plugin> SealedPlugin for TypedPlugin<P> {
fn apply_boxed(
self: Arc<Self>,
ctx: Context,
) -> BoxFuture<Result<(), crate::fiber::PluginFailure>> {
Box::pin(async move {
let state = self
.state
.lock()
.take()
.expect("the lifecycle arbiter serializes Plugin apply");
let lease = StateLease {
owner: self,
state: Some(state),
};
let (plugin, input) = lease
.state
.as_ref()
.expect("the state lease remains populated during apply");
let result = plugin.apply(ctx, input).await;
result.map_err(|error| crate::fiber::PluginFailure::returned(error.to_string()))
})
}
fn swap_input(&self, change: PreparedChange) -> Result<(), PreparedChange> {
let (contract, input) = change.into_parts();
if contract != TypeId::of::<P>() {
return Err(PreparedChange::from_parts(contract, input));
}
let input = match input.downcast::<P::Input>() {
Ok(input) => *input,
Err(input) => return Err(PreparedChange::from_parts(contract, input)),
};
let superseded = {
let mut state = self.state.lock();
match state.as_mut() {
Some((_, current)) => std::mem::replace(current, input),
None => {
return Err(PreparedChange::from_parts(
contract,
Box::new(input) as Box<dyn Any + Send>,
));
}
}
};
drop(superseded);
Ok(())
}
fn into_successor(
self: Arc<Self>,
change: PreparedChange,
) -> Result<Arc<dyn SealedPlugin>, PreparedChange> {
let (contract, input) = change.into_parts();
if contract != TypeId::of::<P>() {
return Err(PreparedChange::from_parts(contract, input));
}
let input = match input.downcast::<P::Input>() {
Ok(input) => *input,
Err(input) => return Err(PreparedChange::from_parts(contract, input)),
};
let taken = self.state.lock().take();
match taken {
Some((plugin, _superseded)) => Ok(Arc::new(TypedPlugin {
state: parking_lot::Mutex::new(Some((plugin, input))),
})),
None => Err(PreparedChange::from_parts(
contract,
Box::new(input) as Box<dyn Any + Send>,
)),
}
}
}
#[must_use = "a PreparedPlugin must be passed to Context::spawn to begin lifecycle work"]
pub struct PreparedPlugin {
pub(crate) plugin: Arc<dyn SealedPlugin>,
pub(crate) name: String,
pub(crate) inject: InjectSpec,
pub(crate) contract: TypeId,
}
impl PreparedPlugin {
pub fn from_input<P: Plugin>(plugin: P, input: P::Input) -> Self {
let name = plugin.name().into_owned();
let inject = plugin.inject();
Self {
plugin: Arc::new(TypedPlugin {
state: parking_lot::Mutex::new(Some((plugin, input))),
}),
name,
inject,
contract: TypeId::of::<P>(),
}
}
pub fn with_inject_overlay(mut self, overlay: InjectSpec) -> Self {
self.inject = self.inject.overlay(overlay);
self
}
}
#[must_use = "a PreparedChange is consumed by one update or era-replacement attempt"]
pub struct PreparedChange {
pub(crate) contract: TypeId,
pub(crate) input: Box<dyn Any + Send>,
}
impl PreparedChange {
pub fn from_input<P: Plugin>(input: P::Input) -> Self {
Self {
contract: TypeId::of::<P>(),
input: Box::new(input),
}
}
pub(crate) fn contract(&self) -> TypeId {
self.contract
}
pub(crate) fn into_parts(self) -> (TypeId, Box<dyn Any + Send>) {
(self.contract, self.input)
}
pub(crate) fn from_parts(contract: TypeId, input: Box<dyn Any + Send>) -> Self {
Self { contract, input }
}
}
#[cfg(test)]
mod critical_section_tests {
use super::{PreparedChange, SealedPlugin, TypedPlugin};
use crate::{Context, Plugin};
use std::convert::Infallible;
use std::future::ready;
use std::sync::{Arc, mpsc};
use std::time::Duration;
struct PreparedDropProbe {
entered: Option<mpsc::Sender<()>>,
release: Option<mpsc::Receiver<()>>,
}
impl PreparedDropProbe {
fn inert() -> Self {
Self {
entered: None,
release: None,
}
}
}
impl Drop for PreparedDropProbe {
fn drop(&mut self) {
if let Some(entered) = self.entered.take() {
entered.send(()).expect("drop observer is alive");
self.release
.take()
.expect("blocking probe has a release receiver")
.recv()
.expect("drop release is sent");
}
}
}
struct DropPreparedPlugin;
impl Plugin for DropPreparedPlugin {
type Config = ();
type Input = PreparedDropProbe;
type PrepareError = Infallible;
type ApplyError = Infallible;
fn prepare(&self, (): ()) -> Result<Self::Input, Self::PrepareError> {
Ok(PreparedDropProbe::inert())
}
fn apply(
&self,
_ctx: Context,
_input: &Self::Input,
) -> impl Future<Output = Result<(), Self::ApplyError>> + Send {
ready(Ok(()))
}
}
#[test]
fn prepared_replacement_releases_bookkeeping_before_superseded_drop() {
let (entered_tx, entered_rx) = mpsc::channel();
let (release_tx, release_rx) = mpsc::channel();
let plugin = Arc::new(TypedPlugin {
state: parking_lot::Mutex::new(Some((
DropPreparedPlugin,
PreparedDropProbe {
entered: Some(entered_tx),
release: Some(release_rx),
},
))),
});
let swapping = Arc::clone(&plugin);
let worker = std::thread::spawn(move || {
let result = swapping.swap_input(PreparedChange::from_input::<DropPreparedPlugin>(
PreparedDropProbe::inert(),
));
assert!(result.is_ok(), "matching prepared change is accepted");
});
entered_rx
.recv_timeout(Duration::from_secs(2))
.expect("superseded input value reaches Drop");
let bookkeeping_is_open = plugin.state.try_lock().is_some();
release_tx.send(()).expect("release blocked Drop");
worker.join().expect("replacement worker completes");
assert!(
bookkeeping_is_open,
"a blocked user Drop must not retain the Plugin state critical section"
);
}
}