use std::{
cell::Cell, cell::RefCell, error::Error, fmt, marker::PhantomData, rc::Rc, time::Instant,
};
use backoff::{Clock, ExponentialBackoff, backoff::Backoff};
use ntex_error::fmt_err_string;
use ntex_service::{BindCtx, Ctx, ReadyBinding, ReadyNotifier, Service, ServiceFactory};
use crate::time;
#[derive(Debug)]
pub struct Reconnect<P, Sf, St> {
name: &'static str,
policy: P,
factory: Rc<Sf>,
retry: usize,
max_retry: usize,
ph: PhantomData<St>,
}
pub trait ReconnectPolicy<E>: Clone + Sized + 'static {
fn reconnect(&self, err: &E) -> bool;
}
#[derive(Copy, Clone, Debug)]
pub struct DefaultPolicy;
impl<E> ReconnectPolicy<E> for DefaultPolicy {
fn reconnect(&self, _: &E) -> bool {
true
}
}
impl<T, E> ReconnectPolicy<E> for T
where
T: Fn(&E) -> bool + Clone + 'static,
{
fn reconnect(&self, err: &E) -> bool {
(*self)(err)
}
}
impl<Sf, St> Reconnect<DefaultPolicy, Sf, St>
where
St: 'static,
Sf: 'static,
{
pub fn new<Req>(name: &'static str, factory: Sf) -> Self
where
Sf: ServiceFactory<St, Req> + 'static,
Sf::InitError: Into<Sf::Error>,
{
Self {
name,
factory: Rc::new(factory),
policy: DefaultPolicy,
retry: 3,
max_retry: 3,
ph: PhantomData,
}
}
}
impl<P, Sf, St> Reconnect<P, Sf, St> {
#[must_use]
pub fn retry(mut self, retry: usize) -> Self {
self.retry = retry;
self.max_retry = retry;
self
}
pub fn policy<T, Req>(self, policy: T) -> Reconnect<T, Sf, St>
where
T: ReconnectPolicy<Sf::Error>,
Sf: ServiceFactory<St, Req>,
{
Reconnect {
policy,
name: self.name,
factory: self.factory,
retry: self.retry,
max_retry: self.max_retry,
ph: PhantomData,
}
}
}
impl<P, Sf, St> Clone for Reconnect<P, Sf, St>
where
P: Clone,
{
fn clone(&self) -> Self {
Self {
name: self.name,
policy: self.policy.clone(),
factory: self.factory.clone(),
retry: self.retry,
max_retry: self.max_retry,
ph: PhantomData,
}
}
}
impl<P, Sf, St, Req> ServiceFactory<St, Req> for Reconnect<P, Sf, St>
where
P: ReconnectPolicy<Sf::Error>,
Sf: ServiceFactory<St, Req>,
Sf::Error: Error,
Sf::InitError: Into<Sf::Error> + Error,
St: 'static,
{
type Res = Sf::Res;
type Error = Sf::Error;
type Service = ReconnectService<P, Sf, Sf::Service, St>;
type InitError = Sf::Error;
async fn create(&self, _: &St) -> Result<Self::Service, Self::InitError> {
Ok(ReconnectService {
inner: ReadyBinding::new(ReconnectServiceInner {
name: self.name,
policy: self.policy.clone(),
factory: self.factory.clone(),
service: Cell::new(None),
err: Cell::new(false),
changed: ReadyNotifier::new(),
retry: Cell::new(self.retry),
max_retry: self.max_retry,
backoff: RefCell::new(ExponentialBackoff::default()),
backoff_active: Cell::new(false),
retry_at: Cell::new(None),
ph: PhantomData,
}),
})
}
}
pub struct ReconnectService<P, Sf, S, St> {
inner: ReadyBinding<ReconnectServiceInner<P, Sf, S, St>>,
}
struct ReconnectServiceInner<P, Sf, S, St> {
name: &'static str,
policy: P,
factory: Rc<Sf>,
service: Cell<Option<Rc<ReadyBinding<S>>>>,
err: Cell<bool>,
changed: ReadyNotifier,
retry: Cell<usize>,
max_retry: usize,
backoff: RefCell<ExponentialBackoff>,
backoff_active: Cell<bool>,
retry_at: Cell<Option<Instant>>,
ph: PhantomData<St>,
}
impl<P, Sf, S, St> fmt::Debug for ReconnectService<P, Sf, S, St> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner.get_ref().fmt(f)
}
}
impl<P: Clone, Sf, S, St> Clone for ReconnectService<P, Sf, S, St> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<P, Sf, S, St> fmt::Debug for ReconnectServiceInner<P, Sf, S, St> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ReconnectService")
.field("name", &self.name)
.field("policy", &std::any::type_name::<P>())
.field("factory", &std::any::type_name::<Sf>())
.field("service", &std::any::type_name::<S>())
.field("err", &self.err)
.field("retry", &self.retry)
.field("max_retry", &self.max_retry)
.field("backoff_active", &self.backoff_active)
.field("retry_at", &self.retry_at)
.finish()
}
}
impl<P: Clone, Sf, S, St> Clone for ReconnectServiceInner<P, Sf, S, St> {
fn clone(&self) -> Self {
Self {
name: self.name,
policy: self.policy.clone(),
factory: self.factory.clone(),
service: Cell::new(None),
err: Cell::new(false),
changed: ReadyNotifier::new(),
retry: Cell::new(self.retry.get()),
max_retry: self.max_retry,
backoff: RefCell::new(self.backoff.borrow().clone()),
backoff_active: Cell::new(false),
retry_at: Cell::new(None),
ph: PhantomData,
}
}
}
impl<Sf, S, St> ReconnectService<DefaultPolicy, Sf, S, St>
where
St: 'static,
Sf: 'static,
{
pub fn new<Req>(name: &'static str, factory: Sf) -> Self
where
Sf: ServiceFactory<St, Req, Service = S, Error = S::Error> + 'static,
Sf::Error: Error,
Sf::InitError: Into<Sf::Error> + Error,
S: Service<St, Req>,
{
Self {
inner: ReadyBinding::new(ReconnectServiceInner {
name,
policy: DefaultPolicy,
factory: Rc::new(factory),
service: Cell::new(None),
err: Cell::new(false),
changed: ReadyNotifier::new(),
retry: Cell::new(3),
max_retry: 3,
backoff: RefCell::new(ExponentialBackoff::default()),
backoff_active: Cell::new(false),
retry_at: Cell::new(None),
ph: PhantomData,
}),
}
}
}
impl<P, Sf, S, St> ReconnectService<P, Sf, S, St> {
#[must_use]
pub fn retry(self, retry: usize) -> Self {
Self {
inner: self.inner.map(|mut inner| {
inner.retry.set(retry);
inner.max_retry = retry;
inner
}),
}
}
pub fn policy<T, Req>(self, policy: T) -> ReconnectService<T, Sf, S, St>
where
T: ReconnectPolicy<Sf::Error>,
Sf: ServiceFactory<St, Req>,
{
ReconnectService {
inner: self.inner.map(|inner| ReconnectServiceInner {
policy,
name: inner.name,
factory: inner.factory,
service: inner.service,
err: inner.err,
changed: inner.changed,
retry: inner.retry,
max_retry: inner.max_retry,
backoff: inner.backoff,
backoff_active: inner.backoff_active,
retry_at: inner.retry_at,
ph: PhantomData,
}),
}
}
}
impl<P, Sf, S, St> ReconnectServiceInner<P, Sf, S, St> {
fn current(&self) -> Option<Rc<ReadyBinding<S>>> {
let svc = self.service.take();
self.service.set(svc.clone());
svc
}
fn remove(&self, svc: &Rc<ReadyBinding<S>>) {
if let Some(current) = self.service.take()
&& !Rc::ptr_eq(¤t, svc)
{
self.service.set(Some(current));
}
}
fn start_backoff(&self) {
if !self.backoff_active.replace(true) {
let mut backoff = self.backoff.borrow_mut();
backoff.start_time = backoff.clock.now();
}
}
async fn ensure<Req>(&self, ctx: Ctx<'_, Self, St>) -> Result<Rc<ReadyBinding<S>>, S::Error>
where
P: ReconnectPolicy<Sf::Error>,
Sf: ServiceFactory<St, Req, Service = S, Error = S::Error>,
Sf::Error: Error,
Sf::InitError: Into<Sf::Error> + Error,
S: Service<St, Req>,
St: 'static,
{
if self.err.get() {
log::trace!("State marked as errored, reconnecting: {:?}", self.name);
self.err.set(false);
let _ = self.service.take();
}
loop {
let svc = if let Some(svc) = self.current() {
svc
} else {
if let Some(retry_at) = self.retry_at.get() {
let now = time::now();
if now < retry_at {
time::sleep(retry_at - now).await;
continue;
}
self.retry_at.set(None);
}
self.start_backoff();
let factory = ntex_service::fn_factory(async |st: &St| {
self.factory.create(st).await.map(ReadyBinding::new)
});
match ctx.create(&factory).await {
Ok(service) => {
log::trace!("Re-connect is done, service '{}' is creates", self.name);
self.backoff.borrow_mut().reset();
self.retry_at.set(None);
let svc = Rc::new(service);
self.service.set(Some(svc.clone()));
svc
}
Err(err) => {
let s = fmt_err_string(&err);
log::trace!("Failed to create service: {s}");
let err = err.into();
if self.policy.reconnect(&err) {
let retry = self.retry.get();
if retry > 0 {
let timeout = self.backoff.borrow_mut().next_backoff();
if let Some(timeout) = timeout {
self.retry.set(retry - 1);
log::trace!(
"Error during re-connecting '{}': {s}, sleep: {timeout:?}",
self.name
);
self.retry_at.set(Some(Instant::now() + timeout));
log::trace!(
"Re-connecting, checking service '{}' readiness",
self.name
);
continue;
}
}
}
self.err.set(true);
return Err(err);
}
}
};
break if let Err(err) = ctx.ready(svc.as_ref()).await {
log::trace!(
"Error during service '{}' readiness check: {}",
self.name,
fmt_err_string(&err)
);
self.remove(&svc);
self.start_backoff();
if self.policy.reconnect(&err) {
let retry = self.retry.get();
if retry > 0 {
let timeout = self.backoff.borrow_mut().next_backoff();
if let Some(timeout) = timeout {
self.retry.set(retry - 1);
log::trace!(
"Error during checking '{}' readiness: {}, sleep: {timeout:?}",
self.name,
fmt_err_string(&err)
);
self.retry_at.set(Some(Instant::now() + timeout));
continue;
}
}
}
self.err.set(true);
Err(err)
} else {
self.retry.set(self.max_retry);
self.backoff_active.set(false);
Ok(svc)
};
}
}
}
impl<P, Sf, S, St, Req> Service<St, Req> for ReconnectService<P, Sf, S, St>
where
P: ReconnectPolicy<Sf::Error>,
Sf: ServiceFactory<St, Req, Service = S, Error = S::Error>,
Sf::Error: Error,
Sf::InitError: Into<Sf::Error> + Error,
S: Service<St, Req>,
St: 'static,
{
type Res = S::Res;
type Error = S::Error;
fn bind(&self, ctx: BindCtx<'_>) {
ctx.bind(&self.inner);
}
async fn ready(&self, ctx: Ctx<'_, Self, St>) -> Result<(), Self::Error> {
ctx.ready(&self.inner).await
}
async fn call(&self, req: Req, ctx: Ctx<'_, Self, St>) -> Result<Self::Res, Self::Error> {
ctx.call(&self.inner, req).await
}
async fn shutdown(&self, ctx: Ctx<'_, Self, St>) {
ctx.shutdown(&self.inner).await;
}
}
impl<P, Sf, S, St, Req> Service<St, Req> for ReconnectServiceInner<P, Sf, S, St>
where
P: ReconnectPolicy<Sf::Error>,
Sf: ServiceFactory<St, Req, Service = S, Error = S::Error>,
Sf::Error: Error,
Sf::InitError: Into<Sf::Error> + Error,
S: Service<St, Req>,
St: 'static,
{
type Res = S::Res;
type Error = S::Error;
fn bind(&self, ctx: BindCtx<'_>) {
ctx.subscribe(&self.changed);
if let Some(svc) = self.current() {
ctx.bind(svc.as_ref());
}
}
async fn ready(&self, ctx: Ctx<'_, Self, St>) -> Result<(), Self::Error> {
self.ensure(ctx).await.map(|_| ())
}
async fn shutdown(&self, ctx: Ctx<'_, Self, St>) {
if let Some(svc) = self.current() {
ctx.shutdown(svc.as_ref()).await;
}
}
async fn call(&self, req: Req, ctx: Ctx<'_, Self, St>) -> Result<Self::Res, Self::Error> {
let svc = match self.current() {
Some(svc) if !self.err.get() => svc,
_ => self.ensure(ctx).await?,
};
let result = ctx.call(svc.as_ref(), req).await;
if let Err(ref err) = result {
log::trace!("Service failed: {}", fmt_err_string(err));
if self
.current()
.is_some_and(|current| Rc::ptr_eq(¤t, &svc))
&& self.policy.reconnect(err)
{
self.err.set(true);
self.changed.notify();
}
}
result
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use std::{future::Future, future::poll_fn, io, pin::pin, task::Context, task::Poll};
use ntex_service::{Pipeline, ReadyBinding, ReadyCallback, fn_factory, fn_service};
use super::*;
use crate::channel::oneshot;
struct Probe {
id: usize,
ready: Cell<bool>,
fail: Cell<bool>,
changed: ReadyNotifier,
}
impl Service<(), ()> for Probe {
type Res = usize;
type Error = io::Error;
fn bind(&self, ctx: BindCtx<'_>) {
ctx.subscribe(&self.changed);
}
async fn ready(&self, _: Ctx<'_, Self>) -> Result<(), io::Error> {
poll_fn(|_| {
if self.ready.get() {
Poll::Ready(Ok(()))
} else {
Poll::Pending
}
})
.await
}
async fn call(&self, (): (), _: Ctx<'_, Self>) -> Result<usize, io::Error> {
if self.fail.get() {
Err(io::Error::other("call failed"))
} else {
Ok(self.id)
}
}
}
struct Branches<S> {
first: ReadyBinding<S>,
second: ReadyBinding<S>,
}
impl<S> Service<(), bool> for Branches<S>
where
S: Service<(), (), Res = usize, Error = io::Error>,
{
type Res = usize;
type Error = io::Error;
fn bind(&self, ctx: BindCtx<'_>) {
ctx.bind(&self.first);
ctx.bind(&self.second);
}
async fn call(&self, second: bool, ctx: Ctx<'_, Self>) -> Result<usize, io::Error> {
if second {
ctx.call(&self.second, ()).await
} else {
ctx.call(&self.first, ()).await
}
}
}
#[ntex::test]
async fn factory_clones_preserve_configuration_and_defer_state() {
#[derive(Debug)]
struct InitError(io::ErrorKind);
impl fmt::Display for InitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "initialization failed: {:?}", self.0)
}
}
impl Error for InitError {}
impl From<InitError> for io::Error {
fn from(err: InitError) -> Self {
err.0.into()
}
}
let attempts = Rc::new(RefCell::new(Vec::new()));
let failure = Rc::new(Cell::new(Some(io::ErrorKind::Interrupted)));
let decisions = Rc::new(RefCell::new(Vec::new()));
let factory = fn_factory({
let attempts = attempts.clone();
let failure = failure.clone();
move |state: &usize| {
let state = *state;
attempts.borrow_mut().push(state);
let failure = failure.get();
async move {
if let Some(kind) = failure {
Err(InitError(kind))
} else {
Ok(fn_service(move |(): ()| async move {
Ok::<_, io::Error>(state)
}))
}
}
}
});
let factory = Reconnect::new("factory-clone", factory).retry(1).policy({
let decisions = decisions.clone();
move |err: &io::Error| {
decisions.borrow_mut().push(err.kind());
err.kind() == io::ErrorKind::Interrupted
}
});
let cloned = factory.clone();
let first = factory.create(&100).await.unwrap();
let second = cloned.create(&200).await.unwrap();
for service in [&first, &second] {
let mut backoff = service.inner.get_ref().backoff.borrow_mut();
backoff.initial_interval = Duration::ZERO;
backoff.reset();
}
let first = Pipeline::new(1, first);
let second = Pipeline::new(2, second);
assert!(attempts.borrow().is_empty());
assert_eq!(
first.ready().await.unwrap_err().kind(),
io::ErrorKind::Interrupted
);
assert_eq!(*attempts.borrow(), [1, 1]);
failure.set(Some(io::ErrorKind::PermissionDenied));
assert_eq!(
second.ready().await.unwrap_err().kind(),
io::ErrorKind::PermissionDenied
);
assert_eq!(*attempts.borrow(), [1, 1, 2]);
failure.set(Some(io::ErrorKind::Interrupted));
assert_eq!(
second.ready().await.unwrap_err().kind(),
io::ErrorKind::Interrupted
);
assert_eq!(*attempts.borrow(), [1, 1, 2, 2, 2]);
assert_eq!(
*decisions.borrow(),
[
io::ErrorKind::Interrupted,
io::ErrorKind::Interrupted,
io::ErrorKind::PermissionDenied,
io::ErrorKind::Interrupted,
io::ErrorKind::Interrupted,
]
);
failure.set(None);
assert_eq!(first.call(()).await.unwrap(), 1);
assert_eq!(second.call(()).await.unwrap(), 2);
assert_eq!(first.call(()).await.unwrap(), 1);
assert_eq!(*attempts.borrow(), [1, 1, 2, 2, 2, 1, 2]);
}
#[ntex::test]
async fn rejected_readiness_retry_discards_child() {
struct RejectReady(usize);
impl Service<(), ()> for RejectReady {
type Res = usize;
type Error = io::Error;
async fn ready(&self, _: Ctx<'_, Self>) -> Result<(), io::Error> {
if self.0 == 0 {
Err(io::ErrorKind::PermissionDenied.into())
} else {
Ok(())
}
}
async fn call(&self, (): (), _: Ctx<'_, Self>) -> Result<usize, io::Error> {
Ok(self.0)
}
}
let children = Rc::new(RefCell::new(Vec::new()));
let decisions = Rc::new(Cell::new(0));
let factory = fn_factory({
let children = children.clone();
move |(): &()| {
let id = children.borrow().len();
let child = Rc::new(RejectReady(id));
children.borrow_mut().push(Rc::downgrade(&child));
async move { Ok::<_, io::Error>(child) }
}
});
let service = ReconnectService::new("reject-readiness", factory).policy({
let decisions = decisions.clone();
move |err: &io::Error| {
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
decisions.set(decisions.get() + 1);
false
}
});
let pipeline = Pipeline::new((), service);
assert_eq!(
pipeline.call(()).await.unwrap_err().kind(),
io::ErrorKind::PermissionDenied
);
assert_eq!(children.borrow().len(), 1);
assert!(children.borrow()[0].upgrade().is_none());
assert_eq!(decisions.get(), 1);
assert_eq!(pipeline.call(()).await.unwrap(), 1);
assert_eq!(children.borrow().len(), 2);
assert!(children.borrow()[1].upgrade().is_some());
assert_eq!(decisions.get(), 1);
}
#[ntex::test]
async fn call_policy_controls_replacement_without_replaying() {
let created = Rc::new(Cell::new(0));
let calls = Rc::new(RefCell::new(Vec::new()));
let allow = Rc::new(Cell::new(false));
let decisions = Rc::new(Cell::new(0));
let factory = fn_factory({
let created = created.clone();
let calls = calls.clone();
move |(): &()| {
let id = created.get();
created.set(id + 1);
let calls = calls.clone();
async move {
Ok::<_, io::Error>(fn_service(move |fail: bool| {
calls.borrow_mut().push((id, fail));
async move {
if fail {
Err(io::ErrorKind::BrokenPipe.into())
} else {
Ok(id)
}
}
}))
}
}
});
let service = ReconnectService::new("call-policy", factory).policy({
let allow = allow.clone();
let decisions = decisions.clone();
move |err: &io::Error| {
assert_eq!(err.kind(), io::ErrorKind::BrokenPipe);
decisions.set(decisions.get() + 1);
allow.get()
}
});
let pipeline = Pipeline::new((), service);
assert_eq!(pipeline.call(false).await.unwrap(), 0);
assert_eq!(
pipeline.call(true).await.unwrap_err().kind(),
io::ErrorKind::BrokenPipe
);
assert!(pipeline.is_ready());
assert_eq!(pipeline.call(false).await.unwrap(), 0);
assert_eq!(created.get(), 1);
assert_eq!(decisions.get(), 1);
allow.set(true);
assert!(pipeline.call(true).await.is_err());
assert!(!pipeline.is_ready());
assert_eq!(created.get(), 1);
assert_eq!(
*calls.borrow(),
[(0, false), (0, true), (0, false), (0, true)]
);
assert_eq!(pipeline.call(false).await.unwrap(), 1);
assert_eq!(
*calls.borrow(),
[(0, false), (0, true), (0, false), (0, true), (1, false)]
);
assert_eq!(created.get(), 2);
assert_eq!(decisions.get(), 2);
}
#[ntex::test]
async fn shared_service_rejects_another_pipeline() {
use std::panic::{AssertUnwindSafe, catch_unwind};
for connected in [false, true] {
let created = Rc::new(Cell::new(0));
let factory = fn_factory({
let created = created.clone();
move |(): &()| {
created.set(created.get() + 1);
async {
Ok::<_, io::Error>(fn_service(|(): ()| async { Ok::<_, io::Error>(()) }))
}
}
});
let service = Rc::new(ReconnectService::new("single-owner", factory));
let original = Pipeline::new((), service.clone());
if connected {
original.ready().await.unwrap();
}
assert!(catch_unwind(AssertUnwindSafe(|| Pipeline::new((), service.clone()))).is_err());
assert_eq!(created.get(), usize::from(connected));
original.call(()).await.unwrap();
assert_eq!(created.get(), 1);
drop(original);
let service = Rc::try_unwrap(service)
.unwrap()
.retry(5)
.policy(|_: &io::Error| true);
assert!(catch_unwind(AssertUnwindSafe(|| Pipeline::new((), service))).is_err());
assert_eq!(created.get(), 1);
}
}
#[ntex::test]
async fn same_pipeline_parents_share_reconnect_scope() {
for bound_child in [false, true] {
let created = Rc::new(Cell::new(0));
let children = Rc::new(RefCell::new(Vec::new()));
let (release, gate) = oneshot::channel();
let gate = Rc::new(gate);
let factory = fn_factory({
let created = created.clone();
let children = children.clone();
move |(): &()| {
let id = created.get();
created.set(id + 1);
let children = children.clone();
let gate = gate.clone();
async move {
if id == 0 {
gate.recv().await.unwrap();
}
let child = Rc::new(Probe {
id,
ready: Cell::new(true),
fail: Cell::new(false),
changed: ReadyNotifier::new(),
});
children.borrow_mut().push(child.clone());
Ok::<_, io::Error>(if bound_child {
ntex_service::boxed::service(ReadyBinding::new(child))
} else {
ntex_service::boxed::service(child)
})
}
}
});
let service = Rc::new(ReconnectService::new("shared-scope", factory));
let pipeline = Pipeline::new(
(),
Branches {
first: ReadyBinding::new(service.clone()),
second: ReadyBinding::new(service),
},
);
assert_eq!(created.get(), 0);
let mut cx = Context::from_waker(std::task::Waker::noop());
{
let mut first = pin!(pipeline.call(false));
let mut second = pin!(pipeline.call(true));
assert!(first.as_mut().poll(&mut cx).is_pending());
assert!(second.as_mut().poll(&mut cx).is_pending());
assert_eq!(created.get(), 1);
release.send(()).unwrap();
assert_eq!(first.await.unwrap(), 0);
assert_eq!(second.await.unwrap(), 0);
}
for id in 0..2 {
assert_eq!(pipeline.call(false).await.unwrap(), id);
assert_eq!(pipeline.call(true).await.unwrap(), id);
let child = children.borrow()[id].clone();
child.ready.set(false);
child.changed.notify();
assert!(!pipeline.is_ready());
let mut call = pin!(pipeline.call(true));
assert!(call.as_mut().poll(&mut cx).is_pending());
child.ready.set(true);
child.changed.notify();
assert_eq!(call.await.unwrap(), id);
if id == 0 {
child.fail.set(true);
assert!(pipeline.call(false).await.is_err());
}
}
assert_eq!(created.get(), 2);
}
}
#[ntex::test]
async fn late_parent_binding_reuses_connected_child() {
struct Rebind<S>(Rc<S>);
impl<S> Service<(), bool> for Rebind<S>
where
S: Service<(), (), Res = usize, Error = io::Error>,
{
type Res = usize;
type Error = io::Error;
fn bind(&self, ctx: BindCtx<'_>) {
ctx.bind(self.0.as_ref());
}
async fn call(&self, rebind: bool, ctx: Ctx<'_, Self>) -> Result<usize, io::Error> {
if rebind {
let factory = fn_factory(async |(): &()| {
Ok::<_, io::Error>(ReadyBinding::new(self.0.clone()))
});
let parent = ctx.create(&factory).await?;
ctx.call(&parent, ()).await
} else {
ctx.call(self.0.as_ref(), ()).await
}
}
}
let created = Rc::new(Cell::new(0));
let child = Rc::new(Probe {
id: 0,
ready: Cell::new(true),
fail: Cell::new(false),
changed: ReadyNotifier::new(),
});
let factory = fn_factory({
let created = created.clone();
let child = child.clone();
move |(): &()| {
created.set(created.get() + 1);
let child = child.clone();
async move { Ok::<_, io::Error>(child) }
}
});
let service = Rc::new(ReconnectService::new("late-parent", factory));
let pipeline = Pipeline::new((), Rebind(service));
assert_eq!(pipeline.call(false).await.unwrap(), 0);
assert_eq!(pipeline.call(true).await.unwrap(), 0);
child.ready.set(false);
child.changed.notify();
let mut call = pin!(pipeline.call(true));
let mut cx = Context::from_waker(std::task::Waker::noop());
assert!(call.as_mut().poll(&mut cx).is_pending());
assert_eq!(created.get(), 1);
child.ready.set(true);
child.changed.notify();
assert_eq!(call.await.unwrap(), 0);
assert_eq!(pipeline.call(false).await.unwrap(), 0);
assert_eq!(created.get(), 1);
}
#[ntex::test]
async fn retired_children_release_registrations() {
struct ScopedProbe {
id: usize,
fail_ready: Rc<Cell<bool>>,
changed: ReadyNotifier,
callback: ReadyCallback,
}
impl Service<(), (bool, Option<oneshot::Receiver<()>>)> for ScopedProbe {
type Res = usize;
type Error = io::Error;
fn bind(&self, ctx: BindCtx<'_>) {
ctx.subscribe(&self.changed);
ctx.on_notify(&self.callback);
}
async fn ready(&self, _: Ctx<'_, Self>) -> Result<(), io::Error> {
if self.fail_ready.get() {
Err(io::Error::other("readiness failed"))
} else {
Ok(())
}
}
async fn call(
&self,
(fail, gate): (bool, Option<oneshot::Receiver<()>>),
_: Ctx<'_, Self>,
) -> Result<usize, io::Error> {
if let Some(gate) = gate {
gate.await.unwrap();
}
if fail {
Err(io::Error::other("call failed"))
} else {
Ok(self.id)
}
}
}
let registrations = Rc::new(RefCell::new(Vec::new()));
let fail_ready = Rc::new(Cell::new(false));
let factory = fn_factory({
let registrations = registrations.clone();
let fail_ready = fail_ready.clone();
move |(): &()| {
let id = registrations.borrow().len();
let changed = ReadyNotifier::new();
let notifications = Rc::new(Cell::new(0));
registrations
.borrow_mut()
.push((changed.clone(), Rc::downgrade(¬ifications)));
let fail_ready = fail_ready.clone();
async move {
Ok::<_, io::Error>(ScopedProbe {
id,
fail_ready,
changed,
callback: ReadyCallback::new(move |_| {
notifications.set(notifications.get() + 1);
}),
})
}
}
});
let pipeline = Pipeline::new(
(),
ReconnectService::new("child-registrations", factory).retry(0),
);
let (release, gate) = oneshot::channel();
let mut held = pin!(pipeline.call((false, Some(gate))));
let mut cx = Context::from_waker(std::task::Waker::noop());
assert!(held.as_mut().poll(&mut cx).is_pending());
assert!(pipeline.call((true, None)).await.is_err());
assert_eq!(pipeline.call((false, None)).await.unwrap(), 1);
assert!(registrations.borrow()[0].1.upgrade().is_some());
release.send(()).unwrap();
assert_eq!(held.await.unwrap(), 0);
assert!(registrations.borrow()[0].1.upgrade().is_none());
registrations.borrow()[0].0.notify();
assert!(pipeline.is_ready());
for id in 1..4 {
assert!(pipeline.call((true, None)).await.is_err());
assert_eq!(pipeline.call((false, None)).await.unwrap(), id + 1);
assert!(registrations.borrow()[id].1.upgrade().is_none());
registrations.borrow()[id].0.notify();
assert!(pipeline.is_ready());
}
fail_ready.set(true);
registrations.borrow()[4].0.notify();
assert_eq!(registrations.borrow()[4].1.upgrade().unwrap().get(), 1);
assert!(!pipeline.is_ready());
assert_eq!(
pipeline.ready().await.unwrap_err().to_string(),
"readiness failed"
);
assert!(registrations.borrow()[4].1.upgrade().is_none());
}
#[ntex::test]
async fn retired_notifications_preserve_replacement_creation() {
let attempts = Rc::new(Cell::new(0));
let retired_source = ReadyNotifier::new();
let (release, gate) = oneshot::channel();
let gate = Rc::new(gate);
let factory = fn_factory({
let attempts = attempts.clone();
let retired_source = retired_source.clone();
move |(): &()| {
let id = attempts.get();
attempts.set(id + 1);
let gate = gate.clone();
let changed = if id == 0 {
retired_source.clone()
} else {
ReadyNotifier::new()
};
async move {
if id > 0 {
gate.recv().await.unwrap();
}
Ok::<_, io::Error>(Probe {
id,
ready: Cell::new(true),
fail: Cell::new(id == 0),
changed,
})
}
}
});
let pipeline = Pipeline::new((), ReconnectService::new("retired-source", factory));
assert!(pipeline.call(()).await.is_err());
assert_eq!(attempts.get(), 1);
let mut ready = pin!(pipeline.ready());
let mut cx = Context::from_waker(std::task::Waker::noop());
assert!(ready.as_mut().poll(&mut cx).is_pending());
assert_eq!(attempts.get(), 2);
retired_source.notify();
assert!(ready.as_mut().poll(&mut cx).is_pending());
assert_eq!(attempts.get(), 2);
release.send(()).unwrap();
ready.await.unwrap();
assert_eq!(pipeline.call(()).await.unwrap(), 1);
retired_source.notify();
assert!(pipeline.is_ready());
assert_eq!(attempts.get(), 2);
}
#[ntex::test]
async fn clones_create_independent_children() {
for connected in [false, true] {
let children = Rc::new(RefCell::new(Vec::new()));
let factory = fn_factory({
let children = children.clone();
move |(): &()| {
let mut children = children.borrow_mut();
let child = Rc::new(Probe {
id: children.len(),
ready: Cell::new(true),
fail: Cell::new(false),
changed: ReadyNotifier::new(),
});
children.push(child.clone());
async move { Ok::<_, io::Error>(ReadyBinding::new(child)) }
}
});
let service = Rc::new(ReconnectService::new("clone", factory));
let original = Pipeline::new((), service.clone());
if connected {
original.ready().await.unwrap();
}
let cloned = Pipeline::new((), service.as_ref().clone());
assert_eq!(children.borrow().len(), usize::from(connected));
original.ready().await.unwrap();
cloned.ready().await.unwrap();
assert_eq!(children.borrow().len(), 2);
assert_eq!(original.call(()).await.unwrap(), 0);
assert_eq!(cloned.call(()).await.unwrap(), 1);
let mut cx = Context::from_waker(std::task::Waker::noop());
for (id, pipeline, other) in [(0, &original, &cloned), (1, &cloned, &original)] {
let child = children.borrow()[id].clone();
child.ready.set(false);
child.changed.notify();
assert!(!pipeline.is_ready());
assert!(other.is_ready());
let mut call = pin!(pipeline.call(()));
assert!(call.as_mut().poll(&mut cx).is_pending());
assert_eq!(other.call(()).await.unwrap(), 1 - id);
child.ready.set(true);
child.changed.notify();
assert_eq!(call.await.unwrap(), id);
}
assert_eq!(children.borrow().len(), 2);
}
}
#[ntex::test]
async fn late_call_error_preserves_replacement() {
let created = Rc::new(Cell::new(0));
let decisions = Rc::new(Cell::new(0));
let factory = fn_factory({
let created = created.clone();
move |(): &()| {
let id = created.get();
created.set(id + 1);
async move {
Ok::<_, io::Error>(fn_service(
move |fail: Option<oneshot::Receiver<()>>| async move {
if let Some(fail) = fail {
fail.await.unwrap();
Err(io::Error::other("call failed"))
} else {
Ok(id)
}
},
))
}
}
});
let service = ReconnectService::new("late-error", factory).policy({
let decisions = decisions.clone();
move |_: &io::Error| {
decisions.set(decisions.get() + 1);
true
}
});
let pipeline = Pipeline::new((), service);
let (late_tx, late_rx) = oneshot::channel();
let mut late_call = pin!(pipeline.call(Some(late_rx)));
let mut cx = Context::from_waker(std::task::Waker::noop());
assert!(late_call.as_mut().poll(&mut cx).is_pending());
assert_eq!(created.get(), 1);
let (fail_tx, fail_rx) = oneshot::channel();
fail_tx.send(()).unwrap();
assert!(pipeline.call(Some(fail_rx)).await.is_err());
assert!(!pipeline.is_ready());
assert_eq!(pipeline.call(None).await.unwrap(), 1);
assert!(pipeline.is_ready());
assert_eq!(decisions.get(), 1);
late_tx.send(()).unwrap();
assert!(late_call.await.is_err());
assert_eq!(decisions.get(), 1);
assert!(pipeline.is_ready());
assert_eq!(pipeline.call(None).await.unwrap(), 1);
assert_eq!(created.get(), 2);
}
#[ntex::test]
async fn retry_delay_survives_readiness_restarts() {
struct FailReady(Rc<Cell<bool>>);
impl Service<(), ()> for FailReady {
type Res = ();
type Error = io::Error;
async fn ready(&self, _: Ctx<'_, Self>) -> Result<(), io::Error> {
if self.0.get() {
Err(io::Error::other("readiness failed"))
} else {
Ok(())
}
}
async fn call(&self, (): (), _: Ctx<'_, Self>) -> Result<(), io::Error> {
Ok(())
}
}
for fail_creation in [false, true] {
let created = Rc::new(Cell::new(0));
let failing = Rc::new(Cell::new(true));
let factory = fn_factory({
let created = created.clone();
let failing = failing.clone();
move |(): &()| {
created.set(created.get() + 1);
let failing = failing.clone();
async move {
if fail_creation && failing.get() {
Err(io::Error::other("creation failed"))
} else {
Ok(FailReady(failing))
}
}
}
});
let service = Rc::new(ReconnectService::new("retry-delay", factory));
let inner = service.inner.get_ref();
{
let mut backoff = inner.backoff.borrow_mut();
backoff.initial_interval = Duration::from_secs(30);
backoff.randomization_factor = 0.0;
backoff.reset();
}
let pipeline = Pipeline::new((), service.clone());
let mut cx = Context::from_waker(std::task::Waker::noop());
{
let mut ready = pin!(pipeline.ready());
for _ in 0..3 {
assert!(ready.as_mut().poll(&mut cx).is_pending());
assert_eq!(created.get(), 1);
}
inner.changed.notify();
assert!(ready.as_mut().poll(&mut cx).is_pending());
assert_eq!(created.get(), 1);
}
let retry_at = inner.retry_at.get();
failing.set(false);
{
let mut ready = pin!(pipeline.ready());
assert!(ready.as_mut().poll(&mut cx).is_pending());
assert_eq!(created.get(), 1);
}
assert_eq!(inner.retry_at.get(), retry_at);
let cloned = Pipeline::new((), service.as_ref().clone());
{
let mut ready = pin!(cloned.ready());
assert!(matches!(ready.as_mut().poll(&mut cx), Poll::Ready(Ok(()))));
}
assert_eq!(created.get(), 2);
inner
.retry_at
.set(Some(Instant::now() + Duration::from_millis(1)));
pipeline.ready().await.unwrap();
pipeline.call(()).await.unwrap();
assert_eq!(created.get(), 3);
assert_eq!(
inner.backoff.borrow().current_interval,
Duration::from_secs(30)
);
}
}
#[ntex::test]
async fn creation_resets_backoff_before_readiness() {
struct GatedReady {
id: usize,
gate: Rc<oneshot::Receiver<()>>,
}
impl Service<(), ()> for GatedReady {
type Res = usize;
type Error = io::Error;
async fn ready(&self, _: Ctx<'_, Self>) -> Result<(), io::Error> {
if self.id == 1 {
self.gate.recv().await.unwrap();
Err(io::Error::other("readiness failed"))
} else {
Ok(())
}
}
async fn call(&self, (): (), _: Ctx<'_, Self>) -> Result<usize, io::Error> {
Ok(self.id)
}
}
let attempts = Rc::new(Cell::new(0));
let (release, gate) = oneshot::channel();
let gate = Rc::new(gate);
let factory = fn_factory({
let attempts = attempts.clone();
move |(): &()| {
let id = attempts.get();
attempts.set(id + 1);
let gate = gate.clone();
async move {
if id == 0 {
Err(io::Error::other("creation failed"))
} else {
Ok(GatedReady { id, gate })
}
}
}
});
let service = Rc::new(ReconnectService::new("creation-reset", factory).retry(2));
let inner = service.inner.get_ref();
{
let mut backoff = inner.backoff.borrow_mut();
backoff.initial_interval = Duration::from_secs(30);
backoff.randomization_factor = 0.0;
backoff.reset();
}
let pipeline = Pipeline::new((), service.clone());
let mut cx = Context::from_waker(std::task::Waker::noop());
{
let mut ready = pin!(pipeline.ready());
assert!(ready.as_mut().poll(&mut cx).is_pending());
}
assert_eq!(attempts.get(), 1);
assert_eq!(inner.retry.get(), 1);
assert!(inner.backoff.borrow().current_interval > Duration::from_secs(30));
let expired_start = {
let mut backoff = inner.backoff.borrow_mut();
backoff.start_time -= Duration::from_mins(16);
backoff.start_time
};
inner.retry_at.set(Some(time::now()));
{
let mut ready = pin!(pipeline.ready());
assert!(ready.as_mut().poll(&mut cx).is_pending());
assert_eq!(attempts.get(), 2);
assert_eq!(
inner.backoff.borrow().current_interval,
Duration::from_secs(30)
);
assert!(inner.backoff.borrow().start_time > expired_start);
assert_eq!(inner.retry.get(), 1);
release.send(()).unwrap();
assert!(ready.as_mut().poll(&mut cx).is_pending());
assert_eq!(attempts.get(), 2);
assert_eq!(inner.retry.get(), 0);
}
inner.retry_at.set(Some(time::now()));
assert_eq!(pipeline.call(()).await.unwrap(), 2);
assert_eq!(attempts.get(), 3);
assert_eq!(inner.retry.get(), 2);
}
#[ntex::test]
async fn exhausted_backoff_stops_retries() {
struct FailReady;
impl Service<(), ()> for FailReady {
type Res = ();
type Error = io::Error;
async fn ready(&self, _: Ctx<'_, Self>) -> Result<(), io::Error> {
Err(io::Error::other("readiness failed"))
}
async fn call(&self, (): (), _: Ctx<'_, Self>) -> Result<(), io::Error> {
Ok(())
}
}
for fail_creation in [true, false] {
let attempts = Rc::new(Cell::new(0));
let factory = fn_factory({
let attempts = attempts.clone();
move |(): &()| {
attempts.set(attempts.get() + 1);
async move {
if fail_creation {
Err(io::Error::other("creation failed"))
} else {
Ok(FailReady)
}
}
}
});
let service = ReconnectService::new("exhausted-backoff", factory);
service
.inner
.get_ref()
.backoff
.borrow_mut()
.max_elapsed_time = Some(Duration::ZERO);
let pipeline = Pipeline::new((), service);
let err = pipeline.ready().await.unwrap_err();
assert_eq!(
err.to_string(),
if fail_creation {
"creation failed"
} else {
"readiness failed"
}
);
assert_eq!(attempts.get(), 1, "creation error: {fail_creation}");
}
}
#[ntex::test]
async fn idle_time_preserves_retry_window() {
struct RecoveryProbe {
fail_ready: Rc<Cell<bool>>,
changed: ReadyNotifier,
}
impl Service<(), ()> for RecoveryProbe {
type Res = ();
type Error = io::Error;
fn bind(&self, ctx: BindCtx<'_>) {
ctx.subscribe(&self.changed);
}
async fn ready(&self, _: Ctx<'_, Self>) -> Result<(), io::Error> {
if self.fail_ready.get() {
Err(io::Error::other("readiness failed"))
} else {
Ok(())
}
}
async fn call(&self, (): (), _: Ctx<'_, Self>) -> Result<(), io::Error> {
Err(io::Error::other("call failed"))
}
}
for (connected, fail_readiness) in [(false, false), (true, false), (true, true)] {
let attempts = Rc::new(Cell::new(0));
let fail_creation = Rc::new(Cell::new(false));
let fail_ready = Rc::new(Cell::new(false));
let changed = ReadyNotifier::new();
let factory = fn_factory({
let attempts = attempts.clone();
let fail_creation = fail_creation.clone();
let fail_ready = fail_ready.clone();
let changed = changed.clone();
move |(): &()| {
attempts.set(attempts.get() + 1);
let fail = fail_creation.get();
let fail_ready = fail_ready.clone();
let changed = changed.clone();
async move {
if fail {
Err(io::Error::other("creation failed"))
} else {
Ok(RecoveryProbe {
fail_ready,
changed,
})
}
}
}
});
let service = Rc::new(if connected {
ReconnectService::new("idle-window", factory).retry(1)
} else {
Reconnect::new("idle-window", factory)
.retry(1)
.create(&())
.await
.unwrap()
});
let pipeline = Pipeline::new((), service.clone());
if connected {
pipeline.ready().await.unwrap();
}
{
let mut backoff = service.inner.get_ref().backoff.borrow_mut();
backoff.initial_interval = Duration::ZERO;
backoff.current_interval = Duration::ZERO;
backoff.start_time -= Duration::from_mins(16);
}
if fail_readiness {
fail_ready.set(true);
changed.notify();
} else if connected {
assert!(pipeline.call(()).await.is_err());
}
fail_creation.set(true);
let before = attempts.get();
assert_eq!(
pipeline.ready().await.unwrap_err().to_string(),
"creation failed"
);
assert_eq!(attempts.get() - before, if fail_readiness { 1 } else { 2 });
}
}
#[ntex::test]
async fn retry_window_survives_readiness_restarts() {
for pending_creation in [false, true] {
let attempts = Rc::new(Cell::new(0));
let factory = fn_factory({
let attempts = attempts.clone();
move |(): &()| {
let attempt = attempts.get();
attempts.set(attempt + 1);
async move {
if pending_creation && attempt == 0 {
std::future::pending::<()>().await;
}
Err::<ReadyBinding<Probe>, _>(io::Error::other("creation failed"))
}
}
});
let service = Rc::new(ReconnectService::new("active-window", factory));
let inner = service.inner.get_ref();
{
let mut backoff = inner.backoff.borrow_mut();
backoff.initial_interval = Duration::from_secs(30);
backoff.randomization_factor = 0.0;
backoff.reset();
}
let pipeline = Pipeline::new((), service.clone());
let mut cx = Context::from_waker(std::task::Waker::noop());
{
let mut ready = pin!(pipeline.ready());
assert!(ready.as_mut().poll(&mut cx).is_pending());
assert_eq!(attempts.get(), 1);
inner.backoff.borrow_mut().start_time -= Duration::from_mins(16);
inner.changed.notify();
}
if !pending_creation {
let retry_at = inner.retry_at.get();
{
let mut ready = pin!(pipeline.ready());
assert!(ready.as_mut().poll(&mut cx).is_pending());
}
assert_eq!(attempts.get(), 1);
assert_eq!(inner.retry_at.get(), retry_at);
inner.retry_at.set(Some(time::now()));
}
assert_eq!(
pipeline.ready().await.unwrap_err().to_string(),
"creation failed"
);
assert_eq!(attempts.get(), 2);
assert!(pipeline.ready().await.is_err());
assert_eq!(attempts.get(), 3);
}
}
#[ntex::test]
async fn configured_retry_limit_survives_success() {
for use_factory in [false, true] {
for retry in [0, 5] {
let attempts = Rc::new(Cell::new(0));
let fail_creation = Rc::new(Cell::new(false));
let factory = fn_factory({
let attempts = attempts.clone();
let fail_creation = fail_creation.clone();
move |(): &()| {
attempts.set(attempts.get() + 1);
let fail = fail_creation.get();
async move {
if fail {
Err(io::Error::other("creation failed"))
} else {
Ok(fn_service(|(): ()| async {
Err::<(), _>(io::Error::other("call failed"))
}))
}
}
}
});
let service = if use_factory {
Reconnect::new("retry-limit", factory)
.retry(retry)
.create(&())
.await
.unwrap()
} else {
ReconnectService::new("retry-limit", factory).retry(retry)
};
{
let mut backoff = service.inner.get_ref().backoff.borrow_mut();
backoff.initial_interval = Duration::ZERO;
backoff.max_elapsed_time = None;
}
let pipeline = Pipeline::new((), service);
for _ in 0..2 {
fail_creation.set(false);
pipeline.ready().await.unwrap();
assert!(pipeline.call(()).await.is_err());
fail_creation.set(true);
let before = attempts.get();
assert!(pipeline.ready().await.is_err());
assert_eq!(
attempts.get() - before,
retry + 1,
"factory setter: {use_factory}, retry limit: {retry}"
);
}
}
}
}
#[ntex::test]
async fn shutdown_awaits_current_child_without_reconnecting() {
struct ShutdownProbe {
started: Cell<usize>,
finished: Cell<bool>,
complete: oneshot::Receiver<()>,
}
impl Service<(), ()> for ShutdownProbe {
type Res = ();
type Error = io::Error;
async fn call(&self, (): (), _: Ctx<'_, Self>) -> Result<(), io::Error> {
Err(io::Error::other("call failed"))
}
async fn shutdown(&self, _: Ctx<'_, Self>) {
self.started.set(self.started.get() + 1);
self.complete.recv().await.unwrap();
self.finished.set(true);
}
}
for (connected, failed) in [(false, false), (true, false), (true, true)] {
let created = Rc::new(Cell::new(0));
let (complete, receiver) = oneshot::channel();
let child = Rc::new(ShutdownProbe {
started: Cell::new(0),
finished: Cell::new(false),
complete: receiver,
});
let factory = fn_factory({
let created = created.clone();
let child = child.clone();
move |(): &()| {
created.set(created.get() + 1);
let child = child.clone();
async move { Ok::<_, io::Error>(ReadyBinding::new(child)) }
}
});
let pipeline = Pipeline::new((), ReconnectService::new("shutdown", factory));
if connected {
pipeline.ready().await.unwrap();
}
if failed {
assert!(pipeline.call(()).await.is_err());
}
let mut shutdown = pin!(pipeline.shutdown());
let mut cx = Context::from_waker(std::task::Waker::noop());
if connected {
assert!(shutdown.as_mut().poll(&mut cx).is_pending());
assert_eq!(child.started.get(), 1);
assert!(!child.finished.get());
complete.send(()).unwrap();
shutdown.await;
assert!(child.finished.get());
} else {
assert!(shutdown.as_mut().poll(&mut cx).is_ready());
assert_eq!(child.started.get(), 0);
assert!(!child.finished.get());
}
assert_eq!(created.get(), usize::from(connected));
}
}
}