use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SlotPhase {
Workspace,
Merge,
Retained,
}
struct OwnedSlot {
phase: SlotPhase,
retention_confirmed: bool,
_permit: OwnedSemaphorePermit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SlotReservation {
AlreadyOwned,
Reserved,
Unavailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SlotTransition {
Transfer(SlotPhase),
Release,
Keep,
}
pub(super) fn workspace_completion_transition(
errored: bool,
rejected: bool,
has_final_revision: bool,
) -> SlotTransition {
if !errored && !rejected && has_final_revision {
SlotTransition::Transfer(SlotPhase::Merge)
} else {
SlotTransition::Release
}
}
pub(super) fn merge_result_transition(merged: bool, run_fatal: bool) -> SlotTransition {
match (merged, run_fatal) {
(true, _) => SlotTransition::Release,
(false, true) => SlotTransition::Keep,
(false, false) => SlotTransition::Transfer(SlotPhase::Retained),
}
}
#[derive(Debug, Default, PartialEq, Eq)]
pub(super) struct RetainedReconciliation {
pub(super) reserved: Vec<String>,
pub(super) unavailable: Vec<String>,
pub(super) released: Vec<String>,
}
pub(super) struct LifecycleSlots {
capacity: usize,
semaphore: Arc<Semaphore>,
slots: HashMap<String, OwnedSlot>,
members: HashSet<String>,
unreserved: HashSet<String>,
}
impl LifecycleSlots {
pub(super) fn new(capacity: usize) -> Self {
Self {
capacity,
semaphore: Arc::new(Semaphore::new(capacity)),
slots: HashMap::new(),
members: HashSet::new(),
unreserved: HashSet::new(),
}
}
pub(super) fn ensure_capacity(&mut self, max_parallelism: usize) {
if max_parallelism > self.capacity {
self.semaphore.add_permits(max_parallelism - self.capacity);
self.capacity = max_parallelism;
}
}
pub(super) fn semaphore(&self) -> Arc<Semaphore> {
self.semaphore.clone()
}
pub(super) fn members(&self) -> &HashSet<String> {
&self.members
}
pub(super) fn len(&self) -> usize {
self.slots.len()
}
pub(super) fn contains(&self, change_id: &str) -> bool {
self.slots.contains_key(change_id)
}
pub(super) fn occupancy_with(&self, also_occupied: &HashSet<String>) -> usize {
self.slots.len()
+ also_occupied
.iter()
.filter(|change_id| !self.slots.contains_key(*change_id))
.count()
}
pub(super) fn has_unreserved(&self) -> bool {
!self.unreserved.is_empty()
}
pub(super) fn available(
&self,
max_parallelism: usize,
also_occupied: &HashSet<String>,
) -> usize {
if self.has_unreserved() {
return 0;
}
max_parallelism.saturating_sub(self.occupancy_with(also_occupied))
}
pub(super) fn apply_transition(&mut self, change_id: &str, transition: SlotTransition) -> bool {
match transition {
SlotTransition::Transfer(phase) => {
self.transfer(change_id, phase);
false
}
SlotTransition::Release => self.release(change_id),
SlotTransition::Keep => false,
}
}
pub(super) fn occupy(
&mut self,
change_id: &str,
permit: OwnedSemaphorePermit,
phase: SlotPhase,
) -> bool {
if self.slots.contains_key(change_id) {
return false;
}
self.slots.insert(
change_id.to_string(),
OwnedSlot {
phase,
retention_confirmed: false,
_permit: permit,
},
);
self.members.insert(change_id.to_string());
self.unreserved.remove(change_id);
true
}
pub(super) fn transfer(&mut self, change_id: &str, phase: SlotPhase) -> bool {
match self.slots.get_mut(change_id) {
Some(slot) => {
slot.phase = phase;
true
}
None => false,
}
}
pub(super) fn reserve_retained(&mut self, change_id: &str) -> SlotReservation {
if let Some(slot) = self.slots.get_mut(change_id) {
slot.retention_confirmed = true;
return SlotReservation::AlreadyOwned;
}
match self.semaphore.clone().try_acquire_owned() {
Ok(permit) => {
self.occupy(change_id, permit, SlotPhase::Retained);
if let Some(slot) = self.slots.get_mut(change_id) {
slot.retention_confirmed = true;
}
SlotReservation::Reserved
}
Err(_) => {
self.unreserved.insert(change_id.to_string());
SlotReservation::Unavailable
}
}
}
pub(super) fn release(&mut self, change_id: &str) -> bool {
self.members.remove(change_id);
self.unreserved.remove(change_id);
self.slots.remove(change_id).is_some()
}
pub(super) fn reconcile_retained(
&mut self,
retained: &HashSet<String>,
active: &HashSet<String>,
settled: &HashSet<String>,
) -> RetainedReconciliation {
let mut report = RetainedReconciliation::default();
for change_id in retained {
match self.reserve_retained(change_id) {
SlotReservation::Reserved => report.reserved.push(change_id.clone()),
SlotReservation::Unavailable => report.unavailable.push(change_id.clone()),
SlotReservation::AlreadyOwned => {}
}
}
for change_id in self.releasable_retained(retained, active, settled) {
if self.release(&change_id) {
report.released.push(change_id);
}
}
report.reserved.sort();
report.unavailable.sort();
report.released.sort();
report
}
pub(super) fn releasable_retained(
&self,
retained: &HashSet<String>,
active: &HashSet<String>,
settled: &HashSet<String>,
) -> Vec<String> {
self.slots
.iter()
.filter(|(change_id, slot)| {
if !matches!(slot.phase, SlotPhase::Retained) {
return false;
}
if settled.contains(*change_id) {
return true;
}
slot.retention_confirmed
&& !retained.contains(*change_id)
&& !active.contains(*change_id)
})
.map(|(change_id, _)| change_id.clone())
.collect()
}
pub(super) fn clear(&mut self) {
self.slots.clear();
self.members.clear();
self.unreserved.clear();
}
#[cfg(test)]
pub(super) fn phase(&self, change_id: &str) -> Option<SlotPhase> {
self.slots.get(change_id).map(|slot| slot.phase)
}
#[cfg(test)]
pub(super) async fn occupy_now(&mut self, change_id: &str, phase: SlotPhase) -> bool {
let permit = self
.semaphore
.clone()
.acquire_owned()
.await
.expect("lifecycle slot semaphore is never closed");
self.occupy(change_id, permit, phase)
}
}