use otel_arrow_dfe_config::SubscriptionGroupName;
use otel_arrow_dfe_config::topic::{TopicBroadcastAckMode, TopicBroadcastOnLagPolicy};
use otel_arrow_dfe_telemetry::otel_warn;
use parking_lot::Mutex;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::sync::{Notify, OwnedSemaphorePermit};
use tokio::time::{Instant, sleep_until};
#[derive(Debug)]
pub struct Envelope<T> {
pub id: u64,
pub tracked: bool,
pub payload: Arc<T>,
}
impl<T> Clone for Envelope<T> {
fn clone(&self) -> Self {
Self {
id: self.id,
tracked: self.tracked,
payload: Arc::clone(&self.payload),
}
}
}
#[derive(Debug)]
pub enum RecvItem<T> {
Message(Envelope<T>),
Lagged {
missed: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PublishOutcome {
Published,
DroppedOnFull,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TrackedPublishOutcome {
Ack,
Nack {
reason: Arc<str>,
},
TimedOut,
TopicClosed,
}
#[derive(Debug)]
pub enum TrackedTryPublishOutcome {
Published(TrackedPublishReceipt),
DroppedOnFull,
MaxInFlightReached,
}
pub struct TrackedPublishPermit(Option<OwnedSemaphorePermit>);
impl std::fmt::Debug for TrackedPublishPermit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("TrackedPublishPermit(..)")
}
}
impl TrackedPublishPermit {
pub(crate) fn from_tokio_owned(permit: OwnedSemaphorePermit) -> Self {
Self(Some(permit))
}
}
impl Drop for TrackedPublishPermit {
fn drop(&mut self) {
_ = self.0.take();
}
}
#[derive(Clone)]
pub struct TrackedPublishTracker {
inner: Arc<TrackedPublishTrackerInner>,
}
struct TrackedPublishTrackerInner {
entries: Mutex<HashMap<u64, Arc<TrackedPublishEntry>>>,
closed: AtomicBool,
wakeups: Notify,
timeout_worker_started: AtomicBool,
}
impl std::fmt::Debug for TrackedPublishTracker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let pending = self.inner.entries.lock().len();
f.debug_struct("TrackedPublishTracker")
.field("pending", &pending)
.finish()
}
}
impl Default for TrackedPublishTracker {
fn default() -> Self {
Self::new()
}
}
impl TrackedPublishTracker {
#[must_use]
pub fn new() -> Self {
Self {
inner: Arc::new(TrackedPublishTrackerInner {
entries: Mutex::new(HashMap::new()),
closed: AtomicBool::new(false),
wakeups: Notify::new(),
timeout_worker_started: AtomicBool::new(false),
}),
}
}
pub fn register(
&self,
message_id: u64,
timeout: Duration,
permit: TrackedPublishPermit,
) -> TrackedPublishReceipt {
if self.inner.closed.load(Ordering::Acquire) {
let entry = Arc::new(TrackedPublishEntry::new(Instant::now(), permit));
let _resolved = entry.resolve(TrackedPublishOutcome::TopicClosed);
return TrackedPublishReceipt::new(message_id, entry);
}
self.ensure_timeout_worker();
let entry = Arc::new(TrackedPublishEntry::new(Instant::now() + timeout, permit));
let replaced = self
.inner
.entries
.lock()
.insert(message_id, Arc::clone(&entry));
if replaced.is_some() {
otel_warn!(
"topic.tracked_publish.duplicate_message_id",
message_id = message_id,
message = "Tracked publish tracker registered a duplicate message id and overwrote the previous entry"
);
}
self.inner.wakeups.notify_one();
TrackedPublishReceipt::new(message_id, entry)
}
pub fn register_consensus(
&self,
message_id: u64,
timeout: Duration,
permit: TrackedPublishPermit,
members: impl Into<Arc<HashSet<BroadcastSubscriberId>>>,
seq: u64,
) -> TrackedPublishReceipt {
let members = members.into();
if self.inner.closed.load(Ordering::Acquire) {
let entry = Arc::new(TrackedPublishEntry::new_consensus(
Instant::now(),
permit,
members,
seq,
));
let _resolved = entry.resolve(TrackedPublishOutcome::TopicClosed);
return TrackedPublishReceipt::new(message_id, entry);
}
if members.is_empty() {
let entry = Arc::new(TrackedPublishEntry::new_consensus(
Instant::now(),
permit,
members,
seq,
));
let _resolved = entry.resolve(TrackedPublishOutcome::Nack {
reason: Arc::from("broadcast publish had no eligible subscribers"),
});
return TrackedPublishReceipt::new(message_id, entry);
}
self.ensure_timeout_worker();
let entry = Arc::new(TrackedPublishEntry::new_consensus(
Instant::now() + timeout,
permit,
members,
seq,
));
let replaced = self
.inner
.entries
.lock()
.insert(message_id, Arc::clone(&entry));
if replaced.is_some() {
otel_warn!(
"topic.tracked_publish.duplicate_message_id",
message_id = message_id,
message = "Tracked publish tracker registered a duplicate message id and overwrote the previous entry"
);
}
self.inner.wakeups.notify_one();
TrackedPublishReceipt::new(message_id, entry)
}
#[must_use]
pub fn resolve_ack_from(
&self,
message_id: u64,
subscriber_id: BroadcastSubscriberId,
) -> AckFromResult {
let entry = {
let entries = self.inner.entries.lock();
let Some(entry) = entries.get(&message_id).cloned() else {
return AckFromResult::NotTracked;
};
entry
};
let result = entry.resolve_ack_from(subscriber_id);
if matches!(result, AckFromResult::Resolved) {
let mut entries = self.inner.entries.lock();
if entries
.get(&message_id)
.is_some_and(|current| Arc::ptr_eq(current, &entry))
{
let _ = entries.remove(&message_id);
drop(entries);
self.inner.wakeups.notify_one();
}
}
result
}
#[must_use]
pub fn resolve_nack_from(
&self,
message_id: u64,
subscriber_id: BroadcastSubscriberId,
reason: Arc<str>,
) -> NackFromResult {
let mut entries = self.inner.entries.lock();
let Some(entry) = entries.get(&message_id).cloned() else {
return NackFromResult::NotTracked;
};
match entry.nack_if_requires(subscriber_id, &reason) {
NackFromResult::Resolved => {
let _ = entries.remove(&message_id);
drop(entries);
self.inner.wakeups.notify_one();
NackFromResult::Resolved
}
NackFromResult::NotRequired => NackFromResult::NotRequired,
NackFromResult::NotTracked => NackFromResult::NotTracked,
}
}
pub fn nack_pending_for_subscriber(
&self,
subscriber_id: BroadcastSubscriberId,
reason: Arc<str>,
) {
let mut resolved_any = false;
self.inner.entries.lock().retain(|_, entry| {
let resolved = matches!(
entry.nack_if_requires(subscriber_id, &reason),
NackFromResult::Resolved
);
if resolved {
resolved_any = true;
}
!resolved
});
if resolved_any {
self.inner.wakeups.notify_one();
}
}
pub fn nack_owed_before(
&self,
subscriber_id: BroadcastSubscriberId,
seq_threshold: u64,
reason: Arc<str>,
) {
let mut resolved_any = false;
self.inner.entries.lock().retain(|_, entry| {
let resolved = entry.nack_if_requires_before(subscriber_id, seq_threshold, &reason);
if resolved {
resolved_any = true;
}
!resolved
});
if resolved_any {
self.inner.wakeups.notify_one();
}
}
#[must_use]
pub fn resolve(&self, message_id: u64, outcome: TrackedPublishOutcome) -> bool {
let Some(entry) = self.inner.entries.lock().remove(&message_id) else {
return false;
};
let resolved = entry.resolve(outcome);
self.inner.wakeups.notify_one();
resolved
}
#[must_use]
pub fn discard(&self, message_id: u64) -> bool {
let removed = self.inner.entries.lock().remove(&message_id).is_some();
if removed {
self.inner.wakeups.notify_one();
}
removed
}
pub fn close_all(&self) {
self.inner.closed.store(true, Ordering::Release);
let drained = {
self.inner
.entries
.lock()
.drain()
.map(|(_, entry)| entry)
.collect::<Vec<_>>()
};
for entry in drained {
let _resolved = entry.resolve(TrackedPublishOutcome::TopicClosed);
}
self.inner.wakeups.notify_waiters();
}
fn ensure_timeout_worker(&self) {
if self
.inner
.timeout_worker_started
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
let tracker = self.clone();
_ = tokio::spawn(async move {
tracker.timeout_worker().await;
});
}
}
async fn timeout_worker(self) {
loop {
if self.inner.closed.load(Ordering::Acquire) && self.inner.entries.lock().is_empty() {
break;
}
let next_deadline = self.next_deadline();
match next_deadline {
Some(deadline) => {
tokio::select! {
_ = sleep_until(deadline) => self.resolve_expired(deadline),
_ = self.inner.wakeups.notified() => {}
}
}
None => {
self.inner.wakeups.notified().await;
}
}
}
}
fn next_deadline(&self) -> Option<Instant> {
self.inner
.entries
.lock()
.values()
.filter(|entry| entry.is_pending())
.map(|entry| entry.deadline())
.min()
}
fn resolve_expired(&self, now: Instant) {
let expired = {
let mut entries = self.inner.entries.lock();
let expired_ids: Vec<u64> = entries
.iter()
.filter(|(_, entry)| entry.is_pending() && entry.deadline() <= now)
.map(|(id, _)| *id)
.collect();
expired_ids
.into_iter()
.filter_map(|id| entries.remove(&id))
.collect::<Vec<_>>()
};
for entry in expired {
let _resolved = entry.resolve(TrackedPublishOutcome::TimedOut);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TopicPublishOutcomeConfig {
pub max_in_flight: usize,
pub timeout: Duration,
}
impl Default for TopicPublishOutcomeConfig {
fn default() -> Self {
Self {
max_in_flight: 1024,
timeout: Duration::from_secs(30),
}
}
}
pub struct TrackedPublishReceipt {
message_id: u64,
entry: Arc<TrackedPublishEntry>,
}
impl std::fmt::Debug for TrackedPublishReceipt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TrackedPublishReceipt")
.field("message_id", &self.message_id)
.finish()
}
}
impl TrackedPublishReceipt {
pub(crate) fn new(message_id: u64, entry: Arc<TrackedPublishEntry>) -> Self {
Self { message_id, entry }
}
#[must_use]
pub const fn message_id(&self) -> u64 {
self.message_id
}
pub async fn wait_for_outcome(self) -> TrackedPublishOutcome {
self.entry.wait_for_outcome().await
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BroadcastSubscriberId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AckFromResult {
Resolved,
StillPending,
NotTracked,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NackFromResult {
Resolved,
NotRequired,
NotTracked,
}
#[derive(Debug, Clone)]
enum PendingKind {
First,
All {
members: Arc<HashSet<BroadcastSubscriberId>>,
acked: HashSet<BroadcastSubscriberId>,
seq: u64,
},
}
#[derive(Debug, Clone)]
enum TrackedPublishState {
Pending(PendingKind),
Resolved(TrackedPublishOutcome),
}
pub(crate) struct TrackedPublishEntry {
state: Mutex<TrackedPublishState>,
deadline: Instant,
permit: Mutex<Option<TrackedPublishPermit>>,
notify: Notify,
}
impl TrackedPublishEntry {
pub(crate) fn new(deadline: Instant, permit: TrackedPublishPermit) -> Self {
Self {
state: Mutex::new(TrackedPublishState::Pending(PendingKind::First)),
deadline,
permit: Mutex::new(Some(permit)),
notify: Notify::new(),
}
}
pub(crate) fn new_consensus(
deadline: Instant,
permit: TrackedPublishPermit,
members: Arc<HashSet<BroadcastSubscriberId>>,
seq: u64,
) -> Self {
Self {
state: Mutex::new(TrackedPublishState::Pending(PendingKind::All {
members,
acked: HashSet::new(),
seq,
})),
deadline,
permit: Mutex::new(Some(permit)),
notify: Notify::new(),
}
}
#[must_use]
pub(crate) fn deadline(&self) -> Instant {
self.deadline
}
#[must_use]
pub(crate) fn is_pending(&self) -> bool {
matches!(*self.state.lock(), TrackedPublishState::Pending(_))
}
pub(crate) fn resolve(&self, outcome: TrackedPublishOutcome) -> bool {
let mut state = self.state.lock();
if matches!(*state, TrackedPublishState::Resolved(_)) {
return false;
}
*state = TrackedPublishState::Resolved(outcome);
drop(state);
_ = self.permit.lock().take();
self.notify.notify_waiters();
true
}
pub(crate) fn resolve_ack_from(&self, subscriber_id: BroadcastSubscriberId) -> AckFromResult {
let mut state = self.state.lock();
match &mut *state {
TrackedPublishState::Resolved(_) => AckFromResult::NotTracked,
TrackedPublishState::Pending(PendingKind::First) => AckFromResult::NotTracked,
TrackedPublishState::Pending(PendingKind::All { members, acked, .. }) => {
if !members.contains(&subscriber_id) {
return AckFromResult::StillPending;
}
let _ = acked.insert(subscriber_id);
if acked.len() == members.len() {
*state = TrackedPublishState::Resolved(TrackedPublishOutcome::Ack);
drop(state);
_ = self.permit.lock().take();
self.notify.notify_waiters();
AckFromResult::Resolved
} else {
AckFromResult::StillPending
}
}
}
}
fn nack_if_requires(
&self,
subscriber_id: BroadcastSubscriberId,
reason: &Arc<str>,
) -> NackFromResult {
let mut state = self.state.lock();
match &*state {
TrackedPublishState::Resolved(_) | TrackedPublishState::Pending(PendingKind::First) => {
return NackFromResult::NotTracked;
}
TrackedPublishState::Pending(PendingKind::All { members, acked, .. })
if !members.contains(&subscriber_id) || acked.contains(&subscriber_id) =>
{
return NackFromResult::NotRequired;
}
TrackedPublishState::Pending(PendingKind::All { .. }) => {}
}
*state = TrackedPublishState::Resolved(TrackedPublishOutcome::Nack {
reason: Arc::clone(reason),
});
drop(state);
_ = self.permit.lock().take();
self.notify.notify_waiters();
NackFromResult::Resolved
}
pub(crate) fn nack_if_requires_before(
&self,
subscriber_id: BroadcastSubscriberId,
seq_threshold: u64,
reason: &Arc<str>,
) -> bool {
let mut state = self.state.lock();
let requires = matches!(
&*state,
TrackedPublishState::Pending(PendingKind::All { members, acked, seq })
if *seq < seq_threshold
&& members.contains(&subscriber_id)
&& !acked.contains(&subscriber_id)
);
if !requires {
return false;
}
*state = TrackedPublishState::Resolved(TrackedPublishOutcome::Nack {
reason: Arc::clone(reason),
});
drop(state);
_ = self.permit.lock().take();
self.notify.notify_waiters();
true
}
async fn wait_for_outcome(&self) -> TrackedPublishOutcome {
loop {
let notified = self.notify.notified();
if let Some(outcome) = self.current_outcome() {
return outcome;
}
notified.await;
}
}
fn current_outcome(&self) -> Option<TrackedPublishOutcome> {
match &*self.state.lock() {
TrackedPublishState::Pending(_) => None,
TrackedPublishState::Resolved(outcome) => Some(outcome.clone()),
}
}
}
#[derive(Debug, Clone)]
pub enum SubscriptionMode {
Balanced {
group: SubscriptionGroupName,
},
Broadcast,
}
#[derive(Debug, Clone, Default)]
pub struct SubscriberOptions {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TopicOptions {
BalancedOnly {
capacity: usize,
},
BroadcastOnly {
capacity: usize,
on_lag: TopicBroadcastOnLagPolicy,
ack_mode: TopicBroadcastAckMode,
},
Mixed {
balanced_capacity: usize,
broadcast_capacity: usize,
on_lag: TopicBroadcastOnLagPolicy,
ack_mode: TopicBroadcastAckMode,
},
}
impl TopicOptions {
pub const DEFAULT_BALANCED_CAPACITY: usize = 1024;
pub const DEFAULT_BROADCAST_CAPACITY: usize = 1024;
}
impl Default for TopicOptions {
fn default() -> Self {
Self::Mixed {
balanced_capacity: TopicOptions::DEFAULT_BALANCED_CAPACITY,
broadcast_capacity: TopicOptions::DEFAULT_BROADCAST_CAPACITY,
on_lag: TopicBroadcastOnLagPolicy::DropOldest,
ack_mode: TopicBroadcastAckMode::First,
}
}
}