use std::fmt::Display;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, RwLock};
use std::task::{Context, Poll, Waker};
use log::{error, info};
use serde_derive::{Deserialize, Serialize};
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::Mutex;
use super::docs::{DocGenJob, DocGenJobUpdate};
use crate::model::config::{Configuration, NodeRole};
use crate::utils::apierror::ApiError;
use crate::utils::token::generate_token;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WorkerDescriptor {
pub identifier: String,
pub name: String,
#[serde(rename = "toolchainVersionStable")]
pub toolchain_version_stable: semver::Version,
#[serde(rename = "toolchainVersionNightly")]
pub toolchain_version_nightly: semver::Version,
#[serde(rename = "toolchainHost")]
pub toolchain_host: String,
#[serde(rename = "toolchainInstalledTargets")]
pub toolchain_installed_targets: Vec<String>,
#[serde(rename = "toolchainInstallableTargets")]
pub toolchain_installable_targets: Vec<String>,
pub capabilities: Vec<String>,
}
impl WorkerDescriptor {
#[must_use]
pub fn get_my_descriptor(config: &Configuration) -> WorkerDescriptor {
Self {
identifier: generate_token(32),
name: if let NodeRole::Worker(worker_config) = &config.self_role {
worker_config.name.clone()
} else {
String::new()
},
toolchain_version_stable: config.self_toolchain_version_stable.clone(),
toolchain_version_nightly: config.self_toolchain_version_nightly.clone(),
toolchain_host: config.self_toolchain_host.clone(),
toolchain_installed_targets: config.self_installed_targets.clone(),
toolchain_installable_targets: if config.docs_autoinstall_targets {
config.self_installable_targets.clone()
} else {
Vec::new()
},
capabilities: if let NodeRole::Worker(worker_config) = &config.self_role {
worker_config.capabilities.clone()
} else {
Vec::new()
},
}
}
#[must_use]
pub fn matches(&self, selector: &WorkerSelector) -> bool {
if let Some(host) = &selector.toolchain_host {
if &self.toolchain_host != host {
return false;
}
}
if let Some(target) = &selector.toolchain_installed_target {
if !self.toolchain_installed_targets.contains(target) {
return false;
}
}
if let Some(target) = &selector.toolchain_available_target {
if !self.toolchain_installed_targets.contains(target) && !self.toolchain_installable_targets.contains(target) {
return false;
}
}
selector.capabilities.iter().all(|cap| self.capabilities.contains(cap))
}
}
#[derive(Debug)]
pub struct WorkerRegistrationData {
pub descriptor: WorkerDescriptor,
pub job_sender: Sender<JobSpecification>,
pub update_receiver: Receiver<JobUpdate>,
}
#[derive(Debug)]
enum WorkerState {
Available(Receiver<JobUpdate>),
InUse(JobIdentifier),
}
impl WorkerState {
#[must_use]
pub fn is_available(&self) -> bool {
matches!(self, Self::Available(_))
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum WorkerPublicState {
Available,
InUse(JobIdentifier),
}
impl<'a> From<&'a WorkerState> for WorkerPublicState {
fn from(value: &WorkerState) -> Self {
match value {
WorkerState::Available(_) => Self::Available,
WorkerState::InUse(job_id) => Self::InUse(*job_id),
}
}
}
#[derive(Debug)]
struct WorkerData {
descriptor: WorkerDescriptor,
job_sender: Sender<JobSpecification>,
state: WorkerState,
}
impl WorkerData {
#[must_use]
fn checkout(&mut self, job_id: JobIdentifier) -> Receiver<JobUpdate> {
let mut old_state = WorkerState::InUse(job_id);
std::mem::swap(&mut self.state, &mut old_state);
let WorkerState::Available(receiver) = old_state else {
panic!("expected an available worker")
};
receiver
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WorkerPublicData {
pub descriptor: WorkerDescriptor,
pub state: WorkerPublicState,
}
impl<'a> From<&'a WorkerData> for WorkerPublicData {
fn from(value: &'a WorkerData) -> Self {
Self {
descriptor: value.descriptor.clone(),
state: WorkerPublicState::from(&value.state),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct WorkerSelector {
pub toolchain_host: Option<String>,
pub toolchain_installed_target: Option<String>,
pub toolchain_available_target: Option<String>,
pub capabilities: Vec<String>,
}
impl WorkerSelector {
#[must_use]
pub fn new_native_target(target: String) -> Self {
Self {
toolchain_host: Some(target),
toolchain_installed_target: None,
toolchain_available_target: None,
capabilities: Vec::new(),
}
}
#[must_use]
pub fn new_available_target(target: String) -> Self {
Self {
toolchain_host: None,
toolchain_installed_target: None,
toolchain_available_target: Some(target),
capabilities: Vec::new(),
}
}
}
impl Display for WorkerSelector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut first = true;
write!(f, "(")?;
if let Some(host) = &self.toolchain_host {
write!(f, "host={host}")?;
first = false;
}
if let Some(target) = &self.toolchain_installed_target {
if !first {
write!(f, ", ")?;
}
write!(f, "installed target={target}")?;
first = false;
}
if let Some(target) = &self.toolchain_available_target {
if !first {
write!(f, ", ")?;
}
write!(f, "available target={target}")?;
first = false;
}
if !self.capabilities.is_empty() {
if !first {
write!(f, ", ")?;
}
write!(f, "capabilities=")?;
first = true;
for capability in &self.capabilities {
if !first {
write!(f, ",")?;
}
write!(f, "{capability}")?;
}
}
write!(f, ")")?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct NoMatchingWorkerError {
pub selector: WorkerSelector,
}
impl Display for NoMatchingWorkerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "no connected worker matches the selector: {}", self.selector)
}
}
impl std::error::Error for NoMatchingWorkerError {}
pub struct WorkerWaiter {
manager: WorkersManager,
selector: WorkerSelector,
job_id: JobIdentifier,
worker: Option<WorkerCheckout>,
}
impl Future for WorkerWaiter {
type Output = Result<WorkerCheckout, NoMatchingWorkerError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if let Some(data) = self.worker.take() {
self.manager.send_event(WorkerEvent::WorkerStartedJob {
worker_id: data.descriptor.identifier.clone(),
job_id: self.job_id,
});
return Poll::Ready(Ok(data));
}
let mut inner = self.manager.inner.write().unwrap();
match self
.manager
.clone()
.try_get_worker_for(&mut inner, &self.selector, self.job_id)
{
Ok(Some(worker)) => {
self.manager.send_event(WorkerEvent::WorkerStartedJob {
worker_id: worker.descriptor.identifier.clone(),
job_id: self.job_id,
});
Poll::Ready(Ok(worker))
}
Ok(None) => {
inner.queue.push(QueuedRequest {
selector: self.selector.clone(),
waker: cx.waker().clone(),
});
Poll::Pending
}
Err(error) => Poll::Ready(Err(error)),
}
}
}
#[derive(Debug)]
pub struct WorkerCheckout {
manager: WorkersManager,
descriptor: WorkerDescriptor,
job_sender: Sender<JobSpecification>,
update_receiver: Option<Receiver<JobUpdate>>,
}
impl WorkerCheckout {
pub fn sender(&mut self) -> &mut Sender<JobSpecification> {
&mut self.job_sender
}
pub fn update_receiver(&mut self) -> &mut Receiver<JobUpdate> {
self.update_receiver.as_mut().unwrap()
}
}
impl Drop for WorkerCheckout {
fn drop(&mut self) {
self.manager.clone().put_worker_back(self);
}
}
#[derive(Debug)]
struct QueuedRequest {
selector: WorkerSelector,
waker: Waker,
}
#[derive(Debug, Default)]
struct WorkersManagerInner {
workers: Vec<WorkerData>,
queue: Vec<QueuedRequest>,
}
#[derive(Debug, Default, Clone)]
pub struct WorkersManager {
inner: Arc<RwLock<WorkersManagerInner>>,
listeners: Arc<Mutex<Vec<Sender<WorkerEvent>>>>,
}
impl WorkersManager {
#[must_use]
pub fn has_workers(&self) -> bool {
!self.inner.read().unwrap().workers.is_empty()
}
#[must_use]
pub fn get_workers(&self) -> Vec<WorkerPublicData> {
self.inner
.read()
.unwrap()
.workers
.iter()
.map(WorkerPublicData::from)
.collect::<Vec<_>>()
}
pub fn register_worker(&self, data: WorkerRegistrationData) {
info!("=== registering worker {}", data.descriptor.identifier);
let worker_data = WorkerData {
descriptor: data.descriptor,
job_sender: data.job_sender,
state: WorkerState::Available(data.update_receiver),
};
let event = WorkerEvent::WorkerConnected(Box::new(WorkerPublicData::from(&worker_data)));
self.inner.write().unwrap().workers.push(worker_data);
self.send_event(event);
}
pub fn remove_worker(&self, worker_id: &str) {
info!("=== removing worker {worker_id}");
let found = {
let mut inner = self.inner.write().unwrap();
let size_before = inner.workers.len();
inner.workers.retain(|w| w.descriptor.identifier != worker_id);
let size_after = inner.workers.len();
size_before != size_after
};
if found {
self.send_event(WorkerEvent::WorkerRemoved {
worker_id: worker_id.to_string(),
});
}
}
pub fn get_worker_for(
&self,
selector: WorkerSelector,
job_id: JobIdentifier,
) -> Result<WorkerWaiter, NoMatchingWorkerError> {
let worker = self
.clone()
.try_get_worker_for(&mut self.inner.write().unwrap(), &selector, job_id)?;
Ok(WorkerWaiter {
manager: self.clone(),
selector,
job_id,
worker,
})
}
fn put_worker_back(&self, checkout: &mut WorkerCheckout) {
let mut inner = self.inner.write().unwrap();
let index = if let Some((index, worker)) = inner
.workers
.iter_mut()
.enumerate()
.find(|(_, w)| w.descriptor.identifier == checkout.descriptor.identifier)
{
worker.state = WorkerState::Available(checkout.update_receiver.take().unwrap());
self.send_event(WorkerEvent::WorkerAvailable {
worker_id: worker.descriptor.identifier.clone(),
});
Some(index)
} else {
None
};
if let Some(worker_index) = index {
let index = inner.queue.iter().enumerate().find_map(|(index, item)| {
if inner.workers[worker_index].descriptor.matches(&item.selector) {
Some(index)
} else {
None
}
});
if let Some(index) = index {
let item = inner.queue.remove(index);
item.waker.wake();
}
}
}
fn try_get_worker_for(
self,
inner: &mut WorkersManagerInner,
selector: &WorkerSelector,
job_id: JobIdentifier,
) -> Result<Option<WorkerCheckout>, NoMatchingWorkerError> {
let mut at_least_one = false;
for candidate in inner.workers.iter_mut().filter(|w| w.descriptor.matches(selector)) {
at_least_one = true;
if candidate.state.is_available() {
return Ok(Some(WorkerCheckout {
manager: self,
descriptor: candidate.descriptor.clone(),
job_sender: candidate.job_sender.clone(),
update_receiver: Some(candidate.checkout(job_id)),
}));
}
}
if at_least_one {
Ok(None)
} else {
Err(NoMatchingWorkerError {
selector: selector.clone(),
})
}
}
pub async fn add_listener(&self, listener: tokio::sync::mpsc::Sender<WorkerEvent>) {
self.listeners.lock().await.push(listener);
}
fn send_event(&self, event: WorkerEvent) {
let this = self.clone();
tokio::spawn(async move {
if let Err(e) = this.do_send_event(event).await {
error!("{e}");
if let Some(backtrace) = &e.backtrace {
error!("{backtrace}");
}
}
});
}
async fn do_send_event(&self, event: WorkerEvent) -> Result<(), ApiError> {
let mut listeners = self.listeners.lock().await;
let mut index = if listeners.is_empty() {
None
} else {
Some(listeners.len() - 1)
};
while let Some(i) = index {
if listeners[i].send(event.clone()).await.is_err() {
listeners.swap_remove(i);
}
index = if i == 0 { None } else { Some(i - 1) };
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum JobIdentifier {
DocGen(i64),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum JobSpecification {
DocGen(DocGenJob),
}
impl JobSpecification {
#[must_use]
pub fn get_id(&self) -> JobIdentifier {
match self {
Self::DocGen(doc_gen_job) => JobIdentifier::DocGen(doc_gen_job.id),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum JobUpdate {
DocGen(DocGenJobUpdate),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum WorkerEvent {
WorkerConnected(Box<WorkerPublicData>),
WorkerRemoved { worker_id: String },
WorkerStartedJob { worker_id: String, job_id: JobIdentifier },
WorkerAvailable { worker_id: String },
}