use std::collections::BTreeMap;
use std::sync::{Mutex, PoisonError};
use polyc_proto::kinds;
use polyc_state::feed::FeedRecord;
use crate::feed::PartitionChange;
const CONVERSATION_PARTITION_PREFIX: &str = "conv-";
pub(crate) fn is_conversation_partition(partition: &str) -> bool {
partition
.strip_prefix(CONVERSATION_PARTITION_PREFIX)
.is_some_and(|id| !id.is_empty())
}
pub(crate) const MAX_TRACKED_PARTITIONS: usize = 16_384;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Pending {
IndexThrough(u64),
Rebuild,
Replace,
Remove,
Destroy,
}
impl Pending {
#[allow(
clippy::match_same_arms,
reason = "the first four arms are ordered precedence rules that state the \
arrival-order pair explicitly. Merging the Destroy/Replace arm \
into the catch-all below it changes its answer from Replace to \
Destroy, and merging its mirror erases half a symmetric pair the \
doc above describes as one rule."
)]
fn merge(self, other: Self) -> Self {
match (self, other) {
(Self::Destroy, Self::Replace) => Self::Replace,
(Self::Replace, Self::Destroy) => Self::Destroy,
(Self::Destroy, _) | (_, Self::Destroy) => Self::Destroy,
(Self::Replace, Self::Remove) => Self::Remove,
(Self::Remove, Self::Replace) => Self::Replace,
(Self::Replace, _) | (_, Self::Replace) => Self::Replace,
(Self::Remove, _) | (_, Self::Remove) => Self::Remove,
(Self::Rebuild, _) | (_, Self::Rebuild) => Self::Rebuild,
(Self::IndexThrough(a), Self::IndexThrough(b)) => Self::IndexThrough(a.max(b)),
}
}
}
#[derive(Debug, Default)]
pub(crate) struct DirtySet {
inner: Mutex<DirtyInner>,
}
#[derive(Debug, Default)]
struct DirtyInner {
pending: BTreeMap<String, Pending>,
degraded: bool,
degrades: u64,
}
impl DirtyInner {
const fn degrade(&mut self) {
self.degraded = true;
self.degrades = self.degrades.saturating_add(1);
}
}
impl DirtySet {
pub(crate) fn mark(&self, partition: &str, pending: Pending) {
let mut inner = self.lock();
if let Some(existing) = inner.pending.get_mut(partition) {
*existing = existing.merge(pending);
return;
}
if inner.pending.len() >= MAX_TRACKED_PARTITIONS {
inner.degrade();
return;
}
inner.pending.insert(partition.to_owned(), pending);
}
pub(crate) fn drain(&self) -> BTreeMap<String, Pending> {
std::mem::take(&mut self.lock().pending)
}
pub(crate) fn pending_for(&self, partition: &str) -> Option<Pending> {
self.lock().pending.get(partition).copied()
}
pub(crate) fn degraded(&self) -> bool {
self.lock().degraded
}
pub(crate) fn pending_len(&self) -> usize {
self.lock().pending.len()
}
pub(crate) fn degrade(&self) {
self.lock().degrade();
}
pub(crate) fn degrade_count(&self) -> u64 {
self.lock().degrades
}
pub(crate) fn has_pending_removal(&self) -> bool {
self.lock().pending.values().any(|pending| {
matches!(
pending,
Pending::Destroy | Pending::Remove | Pending::Replace
)
})
}
pub(crate) fn clear_degraded(&self, observed: u64) -> bool {
let mut inner = self.lock();
if inner.degrades != observed {
return false;
}
inner.degraded = false;
true
}
fn lock(&self) -> std::sync::MutexGuard<'_, DirtyInner> {
match self.inner.lock() {
Ok(guard) => guard,
Err(poisoned) => {
let mut guard = PoisonError::into_inner(poisoned);
guard.degrade();
guard
}
}
}
}
pub struct CommitMarks {
dirty: std::sync::Arc<DirtySet>,
}
impl CommitMarks {
pub(crate) const fn new(dirty: std::sync::Arc<DirtySet>) -> Self {
Self { dirty }
}
pub fn note_commit(&self, partition: &str, commits: &[FeedRecord]) {
if !is_conversation_partition(partition) {
return;
}
let mut boundary: Option<u64> = None;
let mut excised = false;
for (position, event) in crate::feed::commit_events(commits) {
let (base, _turn_id) = kinds::parse(&event.kind);
if base == kinds::TURN_COMPLETE {
let candidate = position.saturating_add(1);
boundary = Some(boundary.map_or(candidate, |seen: u64| seen.max(candidate)));
} else if base == kinds::TAINT_EXCISION {
excised = true;
}
}
if excised {
self.dirty.mark(partition, Pending::Rebuild);
return;
}
if let Some(boundary) = boundary {
self.dirty.mark(partition, Pending::IndexThrough(boundary));
}
}
pub fn note_partition_change(&self, partition: &str, change: PartitionChange) {
if !is_conversation_partition(partition) {
return;
}
let pending = match change {
PartitionChange::Destroyed => Pending::Destroy,
PartitionChange::MigratedAway => Pending::Remove,
PartitionChange::Rewritten => Pending::Rebuild,
};
self.dirty.mark(partition, pending);
}
pub fn note_bootstrap(&self, partition: &str) {
if !is_conversation_partition(partition) {
return;
}
self.dirty.mark(partition, Pending::Rebuild);
}
pub fn note_source_replacement(&self, partition: &str) {
if !is_conversation_partition(partition) {
return;
}
self.dirty.mark(partition, Pending::Replace);
}
#[must_use]
pub fn awaits_rebuild(&self, partition: &str) -> bool {
matches!(
self.dirty.pending_for(partition),
Some(Pending::Rebuild | Pending::Replace)
)
}
pub fn note_coverage_doubt(&self) {
self.dirty.degrade();
}
#[must_use]
pub fn coverage_is_doubted(&self) -> bool {
self.dirty.degraded()
}
}
#[cfg(test)]
mod tests;