use portable_atomic::AtomicU64;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use log::{debug, warn};
use wacore_binary::Node;
use wacore_binary::builder::NodeBuilder;
use crate::client::Client;
const BATCH_SIZE: u32 = 200;
const REQUEST_DEBOUNCE: Duration = Duration::from_millis(100);
#[derive(Default)]
pub(crate) struct OfflineBatchCoordinator {
armed: AtomicBool,
generation: AtomicU64,
prev_batch_inflight: AtomicBool,
}
impl OfflineBatchCoordinator {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&self) {
self.armed.store(false, Ordering::Release);
self.generation.store(0, Ordering::Release);
self.prev_batch_inflight.store(false, Ordering::Release);
}
fn arm(&self, generation: u64) {
self.generation.store(generation, Ordering::Release);
self.prev_batch_inflight.store(true, Ordering::Release);
self.armed.store(true, Ordering::Release);
}
fn is_armed_for(&self, generation: u64) -> bool {
self.armed.load(Ordering::Acquire) && self.generation.load(Ordering::Acquire) == generation
}
fn try_claim_first_arrival(&self) -> bool {
self.prev_batch_inflight
.compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
}
fn mark_batch_inflight(&self) {
self.prev_batch_inflight.store(true, Ordering::Release);
}
}
pub(crate) fn build_offline_batch_request(count: u32) -> Node {
NodeBuilder::new("ib")
.children([NodeBuilder::new("offline_batch")
.attr("count", count.to_string())
.build()])
.build()
}
#[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.recv.offline_first_batch", level = "debug", skip_all, fields(total = total)))]
pub(crate) async fn send_first_batch(client: Arc<Client>, total: usize) {
let generation = client.connection_generation.load(Ordering::Acquire);
client.offline_batch.arm(generation);
log::info!(
target: "Client/OfflineResume",
"Resume armed: total={}, requesting first batch of {}",
total,
BATCH_SIZE,
);
let _ = send_batch(&client, BATCH_SIZE).await;
}
pub(crate) fn on_offline_stanza_arrived(client: &Arc<Client>, pending: usize) {
let coord = &client.offline_batch;
let generation = client.connection_generation.load(Ordering::Acquire);
if !coord.is_armed_for(generation) {
return;
}
if pending == 0 {
return;
}
if !coord.try_claim_first_arrival() {
return;
}
schedule_continuation(Arc::clone(client), generation);
}
async fn send_batch(client: &Client, count: u32) -> Result<(), ()> {
let node = build_offline_batch_request(count);
if let Err(e) = client.send_node(node).await {
warn!(
target: "Client/OfflineResume",
"Failed to send <ib><offline_batch count={}>: {:?}",
count, e,
);
return Err(());
}
Ok(())
}
fn schedule_continuation(client: Arc<Client>, generation: u64) {
let runtime = client.runtime.clone();
runtime
.spawn(Box::pin(async move {
client.runtime.sleep(REQUEST_DEBOUNCE).await;
if !client.offline_batch.is_armed_for(generation) {
return;
}
if client.connection_generation.load(Ordering::Acquire) != generation {
return;
}
if client.offline_sync_completed.load(Ordering::Acquire) {
return;
}
debug!(
target: "Client/OfflineResume",
"Continuation batch (count={})",
BATCH_SIZE,
);
let _ = send_batch(&client, BATCH_SIZE).await;
if client.offline_batch.is_armed_for(generation) {
client.offline_batch.mark_batch_inflight();
}
}))
.detach();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn batch_request_stanza_shape() {
let node = build_offline_batch_request(200);
assert_eq!(node.tag.as_ref(), "ib");
let children = node.children().expect("ib must have children");
assert_eq!(children.len(), 1);
let inner = &children[0];
assert_eq!(inner.tag.as_ref(), "offline_batch");
let count = inner
.attrs
.get("count")
.map(|v| v.as_str().into_owned())
.expect("count attr present");
assert_eq!(count, "200");
}
#[test]
fn first_arrival_cas_is_exclusive() {
let c = OfflineBatchCoordinator::new();
c.mark_batch_inflight();
assert!(c.try_claim_first_arrival(), "first caller wins");
assert!(
!c.try_claim_first_arrival(),
"subsequent callers in same cycle fail"
);
c.mark_batch_inflight();
assert!(
c.try_claim_first_arrival(),
"new cycle: another first arrival wins"
);
}
#[test]
fn arm_publishes_first_batch_inflight() {
let c = OfflineBatchCoordinator::new();
assert!(!c.is_armed_for(42));
c.arm(42);
assert!(c.is_armed_for(42));
assert!(!c.is_armed_for(43), "stale generation must not be armed");
assert!(
c.try_claim_first_arrival(),
"first stanza after arm wins CAS"
);
assert!(
!c.try_claim_first_arrival(),
"duplicate arrival in same cycle loses"
);
}
#[test]
fn reset_disarms_and_clears_inflight() {
let c = OfflineBatchCoordinator::new();
c.arm(7);
c.reset();
assert!(!c.is_armed_for(7));
assert!(
!c.try_claim_first_arrival(),
"reset must leave prev_batch_inflight=false"
);
}
}