use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use headless_chrome::Browser;
use crate::error::{BrowserPoolError, Result};
use crate::traits::Healthcheck;
pub(crate) struct TrackedBrowser {
id: u64,
browser: Arc<Browser>,
last_ping: Arc<Mutex<Instant>>,
created_at: Instant,
is_healthy: AtomicBool,
}
impl TrackedBrowser {
pub(crate) fn new(browser: Browser) -> Result<Self> {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
let browser = Arc::new(browser);
let created_at = Instant::now();
log::debug!("🔍 Validating new browser instance...");
let tab = browser.new_tab().map_err(|e| {
log::error!("❌ Browser validation failed at new_tab(): {}", e);
BrowserPoolError::BrowserCreation(e.to_string())
})?;
tab.navigate_to("data:text/html,<html></html>")
.map_err(|e| {
log::error!("❌ Browser validation failed at navigate_to(): {}", e);
let _ = tab.close(true); BrowserPoolError::BrowserCreation(e.to_string())
})?;
let _ = tab.close(true);
log::debug!("✅ Browser validation passed");
Ok(TrackedBrowser {
id: NEXT_ID.fetch_add(1, Ordering::SeqCst),
browser,
last_ping: Arc::new(Mutex::new(Instant::now())),
created_at,
is_healthy: AtomicBool::new(true),
})
}
#[inline]
pub(crate) fn mark_unhealthy(&self) {
self.is_healthy.store(false, Ordering::Release);
}
#[inline]
pub(crate) fn is_healthy(&self) -> bool {
self.is_healthy.load(Ordering::Acquire)
}
#[inline]
pub(crate) fn id(&self) -> u64 {
self.id
}
#[inline]
pub(crate) fn browser(&self) -> &Arc<Browser> {
&self.browser
}
#[inline]
pub(crate) fn is_expired(&self, ttl: Duration) -> bool {
self.created_at.elapsed() > ttl
}
#[inline]
pub(crate) fn age(&self) -> Duration {
self.created_at.elapsed()
}
#[inline]
pub(crate) fn age_minutes(&self) -> u64 {
self.created_at.elapsed().as_secs() / 60
}
#[inline]
pub(crate) fn created_at(&self) -> Instant {
self.created_at
}
#[allow(dead_code)]
pub(crate) fn last_ping_time(&self) -> Option<Instant> {
self.last_ping.lock().ok().map(|guard| *guard)
}
}
impl Healthcheck for TrackedBrowser {
fn ping(&self) -> Result<()> {
log::trace!("⚡ Pinging browser {}...", self.id);
let tab = self.browser.new_tab().map_err(|e| {
log::error!("❌ Browser {} ping failed (new_tab): {}", self.id, e);
BrowserPoolError::HealthCheckFailed(e.to_string())
})?;
let _ = tab.close(true);
match self.last_ping.lock() {
Ok(mut ping) => {
*ping = Instant::now();
log::trace!("✅ Browser {} ping successful", self.id);
}
Err(e) => {
log::warn!(
"⚠️ Browser {} ping succeeded but failed to update timestamp: {}",
self.id,
e
);
}
}
Ok(())
}
}
impl std::fmt::Debug for TrackedBrowser {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TrackedBrowser")
.field("id", &self.id)
.field("created_at", &self.created_at)
.field("age_minutes", &self.age_minutes())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(not(windows))]
fn test_tracked_browser_expiry_logic() {
let created_at = Instant::now() - Duration::from_secs(3700);
let ttl = Duration::from_secs(3600);
let age = created_at.elapsed();
assert!(
age > ttl,
"Browser age ({:?}) should exceed TTL ({:?})",
age,
ttl
);
let age_minutes = age.as_secs() / 60;
assert!(
age_minutes > 60,
"Browser age should exceed 60 minutes, got {} minutes",
age_minutes
);
}
#[test]
#[cfg(windows)]
fn test_tracked_browser_expiry_logic() {
let ttl = Duration::from_secs(3600);
let age_expired = Duration::from_secs(3700);
assert!(
age_expired > ttl,
"Age ({:?}) should exceed TTL ({:?})",
age_expired,
ttl
);
let age_not_expired = Duration::from_secs(3500);
assert!(
age_not_expired <= ttl,
"Age ({:?}) should NOT exceed TTL ({:?})",
age_not_expired,
ttl
);
let age_minutes = age_expired.as_secs() / 60;
assert!(
age_minutes > 60,
"Browser age should exceed 60 minutes, got {} minutes",
age_minutes
);
}
#[test]
#[cfg(not(windows))]
fn test_is_expired_boundary() {
let ttl = Duration::from_secs(100);
let just_created = Instant::now();
assert!(just_created.elapsed() < ttl);
let old = Instant::now() - Duration::from_secs(101);
assert!(old.elapsed() > ttl);
}
#[test]
#[cfg(windows)]
fn test_is_expired_boundary() {
let ttl = Duration::from_secs(100);
let age_new = Duration::from_secs(0);
assert!(age_new <= ttl, "New browser should not be expired");
let age_at_ttl = Duration::from_secs(100);
assert!(
!(age_at_ttl > ttl),
"Browser at exactly TTL should not be expired"
);
let age_expired = Duration::from_secs(101);
assert!(age_expired > ttl, "Browser over TTL should be expired");
}
#[test]
fn test_age_minutes_calculation() {
let seconds: u64 = 3700;
let minutes = seconds / 60;
assert_eq!(minutes, 61);
assert_eq!(59u64 / 60, 0); assert_eq!(60u64 / 60, 1); assert_eq!(119u64 / 60, 1); assert_eq!(120u64 / 60, 2); }
}