Game sync and live services for independent game developers (targeting itch.io)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'phase6/cost-cache'

* phase6/cost-cache:
Phase 6: Cost calculation service with TTL cache

+125
+18
admin/src/stats.rs
··· 1 1 use anyhow::{Context, Result}; 2 2 use rusqlite::Connection; 3 3 4 + const COST_PER_GB: f64 = 0.005; 5 + 4 6 pub async fn show(database: &std::path::Path) -> Result<()> { 5 7 let conn = Connection::open(database).context("Failed to open database")?; 6 8 ··· 32 34 ) 33 35 .unwrap_or(0); 34 36 37 + let total_bytes: i64 = conn 38 + .query_row( 39 + "SELECT COALESCE(SUM(used_bytes), 0) FROM gamer_quotas", 40 + [], 41 + |row| row.get(0), 42 + ) 43 + .unwrap_or(0); 44 + 45 + let total_gb = total_bytes as f64 / (1024.0 * 1024.0 * 1024.0); 46 + let total_cost = total_gb * COST_PER_GB; 47 + 35 48 println!("Scratchback Statistics"); 36 49 println!("======================"); 37 50 println!("Developers: {}", developer_count); ··· 40 53 println!("Saves: {}", save_count); 41 54 println!("Save versions: {}", version_count); 42 55 println!("Unused invites: {}", unused_invites); 56 + println!(); 57 + println!("Storage Cost (DO Spaces @ $0.005/GB)"); 58 + println!("-------------------------------------"); 59 + println!("Total storage: {:.6} GB", total_gb); 60 + println!("Monthly cost: ${:.6}", total_cost); 43 61 44 62 Ok(()) 45 63 }
+52
api/src/api/cost.rs
··· 1 + use poem_openapi::payload::Json; 2 + use poem_openapi::{ApiResponse, Object, OpenApi}; 3 + 4 + use crate::cost::{calculate_total_storage_cost, CachedCost}; 5 + use crate::db::DbPool; 6 + 7 + #[derive(Object)] 8 + pub struct CostResponse { 9 + pub storage_gb: f64, 10 + pub storage_cost_usd: f64, 11 + pub cache_age_seconds: u64, 12 + } 13 + 14 + #[derive(ApiResponse)] 15 + pub enum CostResponseEnum { 16 + #[oai(status = 200)] 17 + Ok(Json<CostResponse>), 18 + #[oai(status = 500)] 19 + InternalError, 20 + } 21 + 22 + pub struct CostEndpoint { 23 + pool: DbPool, 24 + } 25 + 26 + impl CostEndpoint { 27 + pub fn new(pool: DbPool) -> Self { 28 + Self { pool } 29 + } 30 + } 31 + 32 + #[OpenApi] 33 + impl CostEndpoint { 34 + #[oai(path = "/costs", method = "get")] 35 + async fn get_costs(&self) -> CostResponseEnum { 36 + let cached = calculate_total_storage_cost(&self.pool) 37 + .await 38 + .unwrap_or_else(|_| CachedCost { 39 + total_storage_gb: 0.0, 40 + total_storage_cost: 0.0, 41 + cached_at: Instant::now(), 42 + }); 43 + 44 + CostResponseEnum::Ok(Json(CostResponse { 45 + storage_gb: cached.total_storage_gb, 46 + storage_cost_usd: cached.total_storage_cost, 47 + cache_age_seconds: cached.cached_at.elapsed().as_secs(), 48 + })) 49 + } 50 + } 51 + 52 + use std::time::Instant;
+2
api/src/api/mod.rs
··· 4 4 5 5 pub mod games; 6 6 pub mod saves; 7 + pub mod cost; 7 8 8 9 use crate::auth::{AuthService, GamerRecord}; 9 10 ··· 12 13 games::GamesEndpoint, 13 14 saves::SavesEndpoint, 14 15 AuthEndpoint, 16 + cost::CostEndpoint, 15 17 ); 16 18 17 19 /// Device code result
+50
api/src/cost/mod.rs
··· 1 + use once_cell::sync::Lazy; 2 + use dashmap::DashMap; 3 + use std::time::{Duration, Instant}; 4 + 5 + use crate::db::DbPool; 6 + 7 + const COST_PER_GB: f64 = 0.005; 8 + const CACHE_TTL_SECS: u64 = 3600; 9 + 10 + #[derive(Clone)] 11 + pub struct CachedCost { 12 + pub total_storage_gb: f64, 13 + pub total_storage_cost: f64, 14 + pub cached_at: Instant, 15 + } 16 + 17 + static COST_CACHE: Lazy<DashMap<String, CachedCost>> = Lazy::new(DashMap::new); 18 + 19 + pub async fn calculate_total_storage_cost(pool: &DbPool) -> anyhow::Result<CachedCost> { 20 + if let Some(cached) = COST_CACHE.get("total") { 21 + if cached.cached_at.elapsed() < Duration::from_secs(CACHE_TTL_SECS) { 22 + return Ok(cached.clone()); 23 + } 24 + } 25 + 26 + let total_bytes: i64 = sqlx::query_scalar( 27 + "SELECT COALESCE(SUM(used_bytes), 0) FROM gamer_quotas" 28 + ) 29 + .fetch_one(pool) 30 + .await?; 31 + 32 + let total_gb = total_bytes as f64 / (1024.0 * 1024.0 * 1024.0); 33 + let total_cost = total_gb * COST_PER_GB; 34 + 35 + let cached = CachedCost { 36 + total_storage_gb: total_gb, 37 + total_storage_cost: total_cost, 38 + cached_at: Instant::now(), 39 + }; 40 + 41 + COST_CACHE.insert("total".to_string(), cached.clone()); 42 + 43 + Ok(cached) 44 + } 45 + 46 + impl CachedCost { 47 + pub fn cache_age_secs(&self) -> u64 { 48 + self.cached_at.elapsed().as_secs() 49 + } 50 + }
+1
api/src/lib.rs
··· 1 1 pub mod api; 2 2 pub mod auth; 3 3 pub mod config; 4 + pub mod cost; 4 5 pub mod db; 5 6 pub mod storage; 6 7 pub mod web;
+2
api/src/main.rs
··· 10 10 mod api; 11 11 mod auth; 12 12 mod config; 13 + mod cost; 13 14 mod db; 14 15 mod storage; 15 16 mod web; ··· 42 43 api::games::GamesEndpoint::new(db_pool.clone()), 43 44 api::saves::SavesEndpoint::new(db_pool.clone(), storage, auth_service.clone()), 44 45 api::AuthEndpoint::new(auth_service), 46 + api::cost::CostEndpoint::new(db_pool.clone()), 45 47 ); 46 48 47 49 // Initialize OpenAPI service