Skip to content

Commit 957208a

Browse files
committed
fix: persist relay call count, exit node bytes tracking, UI data display cleanup
1 parent 92b20d0 commit 957208a

3 files changed

Lines changed: 51 additions & 39 deletions

File tree

src/bin/ui.rs

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,7 +1278,7 @@ impl eframe::App for App {
12781278
.num_columns(4)
12791279
.spacing([16.0, 4.0])
12801280
.show(ui, |ui| {
1281-
// Row 1: fetches today | bytes relayed
1281+
// Row 1: fetches today | resets in
12821282
ui.add_sized(
12831283
[110.0, 18.0],
12841284
egui::Label::new(
@@ -1290,22 +1290,6 @@ impl eframe::App for App {
12901290
[140.0, 18.0],
12911291
egui::Label::new(calls_text),
12921292
);
1293-
ui.add_sized(
1294-
[110.0, 18.0],
1295-
egui::Label::new(
1296-
egui::RichText::new("bytes relayed")
1297-
.color(egui::Color32::from_gray(150)),
1298-
),
1299-
);
1300-
ui.add_sized(
1301-
[140.0, 18.0],
1302-
egui::Label::new(
1303-
egui::RichText::new(fmt_bytes(s.bytes_relayed)).monospace(),
1304-
),
1305-
);
1306-
ui.end_row();
1307-
1308-
// Row 2: next reset (remaining removed — already shown in fetches today X/Y)
13091293
ui.add_sized(
13101294
[110.0, 18.0],
13111295
egui::Label::new(
@@ -1319,8 +1303,6 @@ impl eframe::App for App {
13191303
egui::RichText::new(&reset_str).monospace(),
13201304
),
13211305
);
1322-
ui.label("");
1323-
ui.label("");
13241306
ui.end_row();
13251307

13261308
// Row 3: relay calls+failures | cache
@@ -1408,19 +1390,23 @@ impl eframe::App for App {
14081390
}
14091391
ui.end_row();
14101392

1411-
// Row 5: data transferred with estimated daily total
1393+
// Row 5: data transferred with stable estimated daily total.
1394+
// Uses bytes_relayed (all relay paths: exit node + Apps Script).
1395+
// Estimate clamps avg bytes/call to 50 KB–500 KB so early sparse
1396+
// samples don't make the projection swing wildly.
14121397
if let Some(q) = &quota_state {
1413-
if q.bytes_total > 0 {
1414-
let data_str = if q.requests_used_total > 0 {
1415-
let avg = q.bytes_total as f64 / q.requests_used_total as f64;
1398+
if s.bytes_relayed > 0 {
1399+
let data_str = if s.total_relay_calls >= 5 {
1400+
let raw_avg = s.bytes_relayed as f64 / s.total_relay_calls as f64;
1401+
let avg = raw_avg.clamp(50_000.0, 500_000.0);
14161402
let est = (avg * q.daily_capacity_total as f64) as u64;
14171403
format!(
14181404
"{} / {} (est.)",
1419-
fmt_bytes_approx(q.bytes_total),
1405+
fmt_bytes_approx(s.bytes_relayed),
14201406
fmt_bytes_approx(est),
14211407
)
14221408
} else {
1423-
fmt_bytes_approx(q.bytes_total)
1409+
fmt_bytes_approx(s.bytes_relayed)
14241410
};
14251411
ui.add_sized(
14261412
[110.0, 18.0],

src/domain_fronter.rs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -365,11 +365,6 @@ pub struct DomainFronter {
365365
/// strike state is per-deployment health bookkeeping, not the
366366
/// permanent ban list.
367367
script_timeouts: Arc<std::sync::Mutex<HashMap<String, (Instant, u32)>>>,
368-
/// Every call to `relay()` increments this — exit node AND Apps Script.
369-
/// Use this for UI "fetches today" display. Distinct from `relay_calls`
370-
/// (Apps-Script-direct only) and from the quota tracker's `requests_used`
371-
/// (also Apps-Script-only).
372-
total_relay_calls: AtomicU64,
373368
relay_calls: AtomicU64,
374369
relay_failures: AtomicU64,
375370
bytes_relayed: AtomicU64,
@@ -638,7 +633,6 @@ impl DomainFronter {
638633
coalesced: AtomicU64::new(0),
639634
blacklist: Arc::new(std::sync::Mutex::new(HashMap::new())),
640635
script_timeouts: Arc::new(std::sync::Mutex::new(HashMap::new())),
641-
total_relay_calls: AtomicU64::new(0),
642636
relay_calls: AtomicU64::new(0),
643637
relay_failures: AtomicU64::new(0),
644638
bytes_relayed: AtomicU64::new(0),
@@ -780,8 +774,9 @@ impl DomainFronter {
780774
}
781775
guard.clone()
782776
};
777+
let quota = self.quota_tracker.summary();
783778
StatsSnapshot {
784-
total_relay_calls: self.total_relay_calls.load(Ordering::Relaxed),
779+
total_relay_calls: quota.total_relay_calls,
785780
relay_calls: self.relay_calls.load(Ordering::Relaxed),
786781
relay_failures: self.relay_failures.load(Ordering::Relaxed),
787782
coalesced: self.coalesced.load(Ordering::Relaxed),
@@ -798,7 +793,7 @@ impl DomainFronter {
798793
h2_calls: self.h2_calls.load(Ordering::Relaxed),
799794
h2_fallbacks: self.h2_fallbacks.load(Ordering::Relaxed),
800795
h2_disabled: self.h2_disabled.load(Ordering::Relaxed),
801-
quota: self.quota_tracker.summary(),
796+
quota,
802797
}
803798
}
804799

@@ -1791,7 +1786,7 @@ impl DomainFronter {
17911786
headers: &[(String, String)],
17921787
body: &[u8],
17931788
) -> Vec<u8> {
1794-
self.total_relay_calls.fetch_add(1, Ordering::Relaxed);
1789+
self.quota_tracker.record_relay();
17951790

17961791
// Block ALL relay paths (exit node + Apps Script) when every account
17971792
// bucket is quota-exhausted. Checked here so the exit node short-circuit
@@ -1842,6 +1837,10 @@ impl DomainFronter {
18421837
bytes.len() as u64,
18431838
t0.elapsed().as_nanos() as u64,
18441839
);
1840+
self.bytes_relayed.fetch_add(
1841+
(body.len() + bytes.len()) as u64,
1842+
Ordering::Relaxed,
1843+
);
18451844
return bytes;
18461845
}
18471846
Err(e) if !e.is_retryable() => {
@@ -4931,8 +4930,8 @@ fn decode_js_string_escapes(s: &str) -> Option<String> {
49314930

49324931
#[derive(Debug, Clone)]
49334932
pub struct StatsSnapshot {
4934-
/// Total calls to `relay()` — all traffic through this fronter including
4935-
/// exit node and Apps Script. Use for "fetches today" display.
4933+
/// Total relay() calls today (exit node + Apps Script). Sourced from the
4934+
/// persisted quota tracker so this survives proxy restarts.
49364935
pub total_relay_calls: u64,
49374936
pub relay_calls: u64,
49384937
pub relay_failures: u64,

src/quota_tracker.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,24 @@ pub struct QuotaSummary {
9292
/// hard-stopped ones. Used by the UI to show a meaningful reset time even
9393
/// when all accounts are exhausted.
9494
pub next_reset_at_any: Option<u64>,
95+
/// Total relay() calls today (all paths). Persisted across restarts.
96+
/// Resets at UTC midnight.
97+
pub total_relay_calls: u64,
9598
}
9699

97100
// ── Disk state wrapper ────────────────────────────────────────────────────────
98101

99102
#[derive(Serialize, Deserialize, Default)]
100103
struct QuotaState {
101104
buckets: HashMap<String, AccountBucket>,
105+
/// Total relay() calls today across all paths (exit node + Apps Script).
106+
/// Persisted so restarts don't reset the "fetches today" counter.
107+
#[serde(default)]
108+
total_relay_calls: u64,
109+
/// UTC day number (unix_secs / 86400) when total_relay_calls was last reset.
110+
/// When the day changes, total_relay_calls is zeroed on the next record_relay().
111+
#[serde(default)]
112+
relay_today_day: u64,
102113
}
103114

104115
// ── Tracker ───────────────────────────────────────────────────────────────────
@@ -377,6 +388,7 @@ impl QuotaTracker {
377388
let mut hard_stopped = 0usize;
378389
let mut next_reset: Option<u64> = None;
379390
let mut next_reset_any: Option<u64> = None;
391+
let total_relay_calls = st.total_relay_calls;
380392

381393
for sid in &self.script_ids {
382394
let Some(b) = st.buckets.get(sid) else { continue };
@@ -422,6 +434,7 @@ impl QuotaTracker {
422434
global_hard_stop: global_stop,
423435
next_reset_at: next_reset,
424436
next_reset_at_any: next_reset_any,
437+
total_relay_calls,
425438
}
426439
}
427440

@@ -443,7 +456,7 @@ impl QuotaTracker {
443456
}
444457

445458
/// Save if any mutations have occurred since the last flush.
446-
/// Called from the periodic 60-second stats task.
459+
/// Called from the 1-second save task and periodic stats task.
447460
pub fn save_if_needed(&self) {
448461
if self.dirty_count.load(Ordering::Relaxed) > 0 {
449462
self.save();
@@ -453,7 +466,7 @@ impl QuotaTracker {
453466
}
454467

455468
/// Roll any expired 24-hour windows for all tracked buckets.
456-
/// Called from the 60-second stats task so windows reset even when the
469+
/// Called from the periodic stats task so windows reset even when the
457470
/// proxy is idle and no new requests arrive to trigger record_attempt.
458471
pub fn roll_expired_windows(&self) {
459472
let now = now_unix();
@@ -486,7 +499,20 @@ impl QuotaTracker {
486499
}
487500
}
488501

489-
/// Build a human-readable startup summary line.
502+
/// Record one call to relay() — all paths (exit node + Apps Script).
503+
/// Persisted so "fetches today" survives proxy restarts. Resets at UTC midnight.
504+
pub fn record_relay(&self) {
505+
let today = now_unix() / 86_400;
506+
let mut st = self.state.lock().unwrap();
507+
if st.relay_today_day != today {
508+
st.relay_today_day = today;
509+
st.total_relay_calls = 0;
510+
}
511+
st.total_relay_calls += 1;
512+
drop(st);
513+
self.dirty_count.fetch_add(1, Ordering::Relaxed);
514+
}
515+
490516
/// Log the masked ID and exhaustion reason for every hard-stopped bucket.
491517
/// Called once when global hard stop transitions from false to true.
492518
pub fn log_exhaustion_details(&self) {
@@ -500,6 +526,7 @@ impl QuotaTracker {
500526
}
501527
}
502528

529+
/// Build a human-readable startup summary line.
503530
pub fn startup_summary(&self) -> String {
504531
let s = self.summary();
505532
let now = now_unix();

0 commit comments

Comments
 (0)