use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use chromiumoxide::Page;
use tokio::sync::{Mutex, Semaphore};
use tracing::{debug, info, warn};
use super::manager::{BrowserManager, BrowserOptions};
use crate::error::{AuditError, Result};
#[derive(Debug, Clone)]
pub struct PoolConfig {
pub max_pages: usize,
pub browser_options: BrowserOptions,
pub acquire_timeout_secs: u64,
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
max_pages: 4,
browser_options: BrowserOptions::default(),
acquire_timeout_secs: 60,
}
}
}
pub struct PooledPage {
page: Option<Page>,
pool: Arc<BrowserPoolInner>,
}
impl PooledPage {
pub fn page(&self) -> Result<&Page> {
self.page.as_ref().ok_or_else(|| {
crate::error::AuditError::ConfigError("Page already returned to pool".to_string())
})
}
}
impl Drop for PooledPage {
fn drop(&mut self) {
if let Some(page) = self.page.take() {
let pool = Arc::clone(&self.pool);
tokio::spawn(async move {
pool.return_page(page).await;
});
}
}
}
struct BrowserPoolInner {
browser: BrowserManager,
pages: Mutex<Vec<Page>>,
semaphore: Semaphore,
pages_created: AtomicUsize,
max_pages: usize,
acquire_timeout: Duration,
}
impl BrowserPoolInner {
async fn return_page(&self, page: Page) {
if let Err(e) = page.goto("about:blank").await {
warn!("Failed to reset page: {}", e);
self.semaphore.add_permits(1);
return;
}
let mut pages = self.pages.lock().await;
pages.push(page);
self.semaphore.add_permits(1);
debug!("Page returned to pool ({} available)", pages.len());
}
}
pub struct BrowserPool {
inner: Arc<BrowserPoolInner>,
}
impl BrowserPool {
pub async fn new(config: PoolConfig) -> Result<Self> {
info!("Creating browser pool with max {} pages", config.max_pages);
let browser = BrowserManager::with_options(config.browser_options).await?;
let inner = Arc::new(BrowserPoolInner {
browser,
pages: Mutex::new(Vec::with_capacity(config.max_pages)),
semaphore: Semaphore::new(config.max_pages),
pages_created: AtomicUsize::new(0),
max_pages: config.max_pages,
acquire_timeout: Duration::from_secs(config.acquire_timeout_secs),
});
Ok(Self { inner })
}
pub async fn with_concurrency(concurrency: usize) -> Result<Self> {
Self::new(PoolConfig {
max_pages: concurrency,
..Default::default()
})
.await
}
pub async fn acquire(&self) -> Result<PooledPage> {
debug!("Acquiring page from pool...");
let permit =
tokio::time::timeout(self.inner.acquire_timeout, self.inner.semaphore.acquire())
.await
.map_err(|_| AuditError::PoolTimeout {
timeout_secs: self.inner.acquire_timeout.as_secs(),
})?
.map_err(|_| AuditError::PoolClosed)?;
permit.forget();
let mut pages = self.inner.pages.lock().await;
if let Some(page) = pages.pop() {
debug!("Reusing existing page from pool");
return Ok(PooledPage {
page: Some(page),
pool: Arc::clone(&self.inner),
});
}
drop(pages);
let current = self.inner.pages_created.fetch_add(1, Ordering::SeqCst);
if current >= self.inner.max_pages {
self.inner.pages_created.fetch_sub(1, Ordering::SeqCst);
return Err(AuditError::PoolExhausted);
}
debug!(
"Creating new page ({}/{})",
current + 1,
self.inner.max_pages
);
let page = self.inner.browser.new_page().await?;
Ok(PooledPage {
page: Some(page),
pool: Arc::clone(&self.inner),
})
}
pub fn browser(&self) -> &BrowserManager {
&self.inner.browser
}
pub async fn available_pages(&self) -> usize {
self.inner.pages.lock().await.len()
}
pub fn stats(&self) -> PoolStats {
PoolStats {
max_pages: self.inner.max_pages,
pages_created: self.inner.pages_created.load(Ordering::SeqCst),
permits_available: self.inner.semaphore.available_permits(),
}
}
pub async fn close(self) -> Result<()> {
info!("Closing browser pool...");
let mut pages = self.inner.pages.lock().await;
for page in pages.drain(..) {
if let Err(e) = page.close().await {
warn!("Failed to close pooled page: {}", e);
}
}
info!("Browser pool closed");
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct PoolStats {
pub max_pages: usize,
pub pages_created: usize,
pub permits_available: usize,
}
impl std::fmt::Display for PoolStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Pool: {}/{} pages, {} available",
self.pages_created, self.max_pages, self.permits_available
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pool_config_default() {
let config = PoolConfig::default();
assert_eq!(config.max_pages, 4);
assert_eq!(config.acquire_timeout_secs, 60);
}
#[test]
fn test_pool_stats_display() {
let stats = PoolStats {
max_pages: 4,
pages_created: 2,
permits_available: 2,
};
let display = format!("{}", stats);
assert!(display.contains("2/4"));
assert!(display.contains("2 available"));
}
}