use std::{future::Future, pin::Pin, time::SystemTime};
use tokio::{sync::watch, task::JoinHandle};
use super::{PendingOracleEvent, PendingOracleRuntime, PendingOracleSource, PendingOracleSourceId};
pub type PendingOracleSourceFuture =
Pin<Box<dyn Future<Output = Result<(), PendingOracleSourceError>> + Send + 'static>>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PendingOracleSourceDescriptor {
pub source: PendingOracleSource,
pub id: PendingOracleSourceId,
}
impl PendingOracleSourceDescriptor {
pub fn new(source: PendingOracleSource, id: PendingOracleSourceId) -> Self {
Self { source, id }
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PendingOracleSourceState {
Connecting,
Ready,
Degraded,
Stopped,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PendingOracleSourceHealth {
pub descriptor: PendingOracleSourceDescriptor,
pub state: PendingOracleSourceState,
pub last_transport_message_at: Option<SystemTime>,
pub last_candidate_at: Option<SystemTime>,
pub coverage_gap_count: u64,
pub last_error: Option<String>,
}
impl PendingOracleSourceHealth {
fn connecting(descriptor: PendingOracleSourceDescriptor) -> Self {
Self {
descriptor,
state: PendingOracleSourceState::Connecting,
last_transport_message_at: None,
last_candidate_at: None,
coverage_gap_count: 0,
last_error: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PendingOracleCoverageGap {
pub descriptor: PendingOracleSourceDescriptor,
pub observed_at: SystemTime,
pub reason: String,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum PendingOracleSourceError {
#[error("pending oracle updates are not enabled on this runtime")]
PendingUpdatesDisabled,
#[error("pending source {transport:?} is not enabled")]
SourceDisabled {
transport: PendingOracleSource,
},
#[error("pending source id {id} is already running")]
DuplicateSource {
id: super::PendingOracleSourceId,
},
#[error("a Tokio runtime is required to start a pending oracle source")]
RuntimeUnavailable,
#[error("pending source transport failed: {0}")]
Transport(String),
#[error("pending source task failed: {0}")]
Task(String),
}
pub trait PendingOracleCandidateSource: Send + 'static {
fn descriptor(&self) -> PendingOracleSourceDescriptor;
fn run(
self: Box<Self>,
sink: PendingOracleSourceSink,
shutdown: watch::Receiver<bool>,
) -> PendingOracleSourceFuture;
}
#[derive(Clone, Debug)]
pub struct PendingOracleSourceSink {
runtime: PendingOracleRuntime,
descriptor: PendingOracleSourceDescriptor,
}
impl PendingOracleSourceSink {
pub(crate) fn new(
runtime: PendingOracleRuntime,
descriptor: PendingOracleSourceDescriptor,
) -> Self {
Self {
runtime,
descriptor,
}
}
pub fn ready(&self) {
self.runtime
.update_source_health(&self.descriptor, |health| {
health.state = PendingOracleSourceState::Ready;
health.last_error = None;
});
}
pub fn transport_message(&self) {
self.runtime
.mutate_source_health(&self.descriptor, |health| {
health.last_transport_message_at = Some(SystemTime::now());
});
}
pub fn candidate(&self) {
self.runtime
.mutate_source_health(&self.descriptor, |health| {
health.last_candidate_at = Some(SystemTime::now());
});
}
pub fn coverage_gap(&self, reason: impl Into<String>) {
let reason = reason.into();
self.runtime
.update_source_health(&self.descriptor, |health| {
health.state = PendingOracleSourceState::Degraded;
health.coverage_gap_count = health.coverage_gap_count.saturating_add(1);
health.last_error = Some(reason.clone());
});
self.runtime
.publisher()
.publish(PendingOracleEvent::CoverageGap(PendingOracleCoverageGap {
descriptor: self.descriptor.clone(),
observed_at: SystemTime::now(),
reason,
}));
}
pub fn reconnecting(&self) {
self.runtime
.update_source_health(&self.descriptor, |health| {
health.state = PendingOracleSourceState::Connecting;
});
}
pub fn runtime(&self) -> &PendingOracleRuntime {
&self.runtime
}
pub fn descriptor(&self) -> &PendingOracleSourceDescriptor {
&self.descriptor
}
pub(crate) fn connecting(&self) {
self.runtime
.insert_source_health(PendingOracleSourceHealth::connecting(
self.descriptor.clone(),
));
}
pub(crate) fn stopped(&self) {
self.runtime
.update_source_health(&self.descriptor, |health| {
health.state = PendingOracleSourceState::Stopped;
});
}
pub(crate) fn failed(&self, error: &PendingOracleSourceError) {
let reason = error.to_string();
self.runtime
.update_source_health(&self.descriptor, |health| {
health.state = PendingOracleSourceState::Degraded;
health.coverage_gap_count = health.coverage_gap_count.saturating_add(1);
health.last_error = Some(reason.clone());
});
self.runtime
.publisher()
.publish(PendingOracleEvent::CoverageGap(PendingOracleCoverageGap {
descriptor: self.descriptor.clone(),
observed_at: SystemTime::now(),
reason,
}));
}
}
#[derive(Debug)]
pub struct PendingOracleSourceSession {
descriptor: PendingOracleSourceDescriptor,
shutdown: watch::Sender<bool>,
task: Option<JoinHandle<Result<(), PendingOracleSourceError>>>,
}
impl PendingOracleSourceSession {
pub(crate) fn new(
descriptor: PendingOracleSourceDescriptor,
shutdown: watch::Sender<bool>,
task: JoinHandle<Result<(), PendingOracleSourceError>>,
) -> Self {
Self {
descriptor,
shutdown,
task: Some(task),
}
}
pub fn descriptor(&self) -> &PendingOracleSourceDescriptor {
&self.descriptor
}
pub fn stop(&self) {
let _ = self.shutdown.send(true);
}
pub async fn join(mut self) -> Result<(), PendingOracleSourceError> {
let Some(task) = self.task.take() else {
return Ok(());
};
task.await
.map_err(|error| PendingOracleSourceError::Task(error.to_string()))?
}
}
impl Drop for PendingOracleSourceSession {
fn drop(&mut self) {
let _ = self.shutdown.send(true);
if let Some(task) = self.task.take() {
task.abort();
}
}
}