use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BufferedTransaction {
pub request_id: String,
pub user_id: String,
pub tx_type: String,
pub amount: f64,
pub balance_after: f64,
pub worker_id: String,
pub duration_ms: f64,
pub source_node: Option<String>,
pub worker_name: Option<String>,
pub worker_uri: Option<String>,
pub price_per_hour: f64,
}
pub struct TransactionBuffer {
pending: DashMap<String, BufferedTransaction>,
balance_snapshots: DashMap<String, f64>,
total_flushed: AtomicU64,
}
impl TransactionBuffer {
pub fn new() -> Self {
Self {
pending: DashMap::new(),
balance_snapshots: DashMap::new(),
total_flushed: AtomicU64::new(0),
}
}
pub fn push_transaction(&self, tx: BufferedTransaction) {
self.pending.insert(tx.request_id.clone(), tx);
}
pub fn snapshot_balance(&self, user_id: &str, balance: f64) {
self.balance_snapshots.insert(user_id.to_string(), balance);
}
pub fn pending_count(&self) -> usize {
self.pending.len()
}
pub fn total_flushed(&self) -> u64 {
self.total_flushed.load(Ordering::Relaxed)
}
fn drain(&self) -> (Vec<BufferedTransaction>, Vec<(String, f64)>) {
let mut txs = Vec::with_capacity(self.pending.len());
let keys: Vec<String> = self.pending.iter().map(|e| e.key().clone()).collect();
for key in keys {
if let Some((_, tx)) = self.pending.remove(&key) {
txs.push(tx);
}
}
let mut balances = Vec::with_capacity(self.balance_snapshots.len());
let bkeys: Vec<String> = self.balance_snapshots.iter().map(|e| e.key().clone()).collect();
for key in bkeys {
if let Some((uid, bal)) = self.balance_snapshots.remove(&key) {
balances.push((uid, bal));
}
}
(txs, balances)
}
pub fn flush_to_api(&self, api_url: &str, api_key: &str) -> usize {
let (txs, _balances) = self.drain();
if txs.is_empty() {
return 0;
}
let tx_count = txs.len();
let payload: Vec<serde_json::Value> = txs
.iter()
.map(|tx| {
let dashboard_type = match tx.tx_type.as_str() {
"commit" => "job_execution",
"cancel" => "job_execution",
"credit" => "credit_purchase",
other => other,
};
let (job_name, credits_amount, status) = match tx.tx_type.as_str() {
"cancel" => (
format!("Cancelled Job ({})", tx.worker_name.as_deref().unwrap_or(&tx.worker_id)),
0.0f64,
"failed",
),
_ => (
format!("Compute Job ({})", tx.worker_name.as_deref().unwrap_or(&tx.worker_id)),
tx.amount,
"completed",
),
};
serde_json::json!({
"zakuro_user_id": tx.user_id,
"job_name": job_name,
"transaction_type": dashboard_type,
"credits_amount": credits_amount,
"status": status,
"duration_ms": if tx.duration_ms > 0.0 { serde_json::Value::Number(serde_json::Number::from_f64(tx.duration_ms).unwrap()) } else { serde_json::Value::Null },
"worker_id": tx.worker_name,
"source_node": tx.source_node,
"compute_hours": if tx.duration_ms > 0.0 { Some(tx.duration_ms / 3_600_000.0) } else { None::<f64> },
"executor": tx.worker_name,
"destination": tx.worker_uri,
"price_per_hour": if tx.price_per_hour > 0.0 { Some(tx.price_per_hour) } else { None::<f64> },
"metadata": null
})
})
.collect();
let endpoint = format!("{}/api/broker/batch-sync", api_url.trim_end_matches('/'));
let payload_str = match serde_json::to_string(&payload) {
Ok(s) => s,
Err(e) => {
eprintln!(" [FLUSH] Failed to serialize payload: {}", e);
for tx in txs {
self.pending.insert(tx.request_id.clone(), tx);
}
return 0;
}
};
match ureq::post(&endpoint)
.set("X-Broker-Api-Key", api_key)
.set("Content-Type", "application/json")
.send_string(&payload_str)
{
Ok(resp) if resp.status() == 200 => {
let body = resp.into_string().unwrap_or_default();
let parsed: serde_json::Value = serde_json::from_str(&body).unwrap_or_default();
let inserted = parsed["inserted"].as_u64().unwrap_or(tx_count as u64);
let failed = parsed["failed"].as_u64().unwrap_or(0);
let success = parsed["success"].as_bool().unwrap_or(true);
self.total_flushed.fetch_add(inserted, Ordering::Relaxed);
if !success || failed > 0 {
eprintln!(" [FLUSH] Partial sync to {}: {} inserted, {} failed",
api_url, inserted, failed);
if let Some(errors) = parsed["errors"].as_array() {
for e in errors.iter().take(3) {
eprintln!(" [FLUSH] → {}: {}",
e["job_name"].as_str().unwrap_or("?"),
e["error"].as_str().unwrap_or("unknown"));
}
if errors.len() > 3 {
eprintln!(" [FLUSH] … and {} more errors", errors.len() - 3);
}
}
} else {
println!(" [FLUSH] Synced {} transactions to {} via API", inserted, api_url);
}
inserted as usize
}
Ok(resp) => {
eprintln!(" [FLUSH] API sync failed: status {}", resp.status());
for tx in txs {
self.pending.insert(tx.request_id.clone(), tx);
}
0
}
Err(e) => {
eprintln!(" [FLUSH] API sync failed: {}", e);
for tx in txs {
self.pending.insert(tx.request_id.clone(), tx);
}
0
}
}
}
}
impl std::fmt::Debug for TransactionBuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TransactionBuffer")
.field("pending", &self.pending.len())
.field("balance_snapshots", &self.balance_snapshots.len())
.field("total_flushed", &self.total_flushed())
.finish()
}
}