캡스톤 5 안전한 종료와 부하 조절 문서의 문제 1 풀이 코드다.

//! 캡스톤 5단계: 4단계 서비스에 안전한 종료와 부하 조절을 더한다.
//! CancellationToken + TaskTracker로 서버와 백그라운드 통계 태스크를 함께 멈추고,
//! 요청 시간 제한(503), 전역 동시 처리 상한, 본문 크기 상한(413)을 tower·tower-http 레이어로 건다.
//! 의존성: thiserror = "2", tokio = { version = "1", features = ["full"] }, tokio-util = { version = "0.7", features = ["rt"] },
//!         axum = "0.8", serde = { version = "1", features = ["derive"] }, serde_json = "1", futures = "0.3",
//!         tower = { version = "0.5", features = ["util", "limit"] },
//!         tower-http = { version = "0.7", features = ["trace", "timeout", "limit"] },
//!         sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "sqlite"] },
//!         tracing = "0.1", tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/4.연습문제/46_capstone-5_p1.rs"
//! 진짜 서버로: SERVE=1 BIND_ADDR=127.0.0.1:3000 REQUEST_TIMEOUT_MS=5000 MAX_IN_FLIGHT=256 cargo run --bin <이름>
//!   Ctrl+C나 SIGTERM(kill, docker stop)을 받으면 새 연결을 끊고, 처리 중인 요청을 최대 10초 기다린 뒤 끝난다.

use std::collections::HashMap;
use std::fmt;
use std::hash::{BuildHasher, RandomState};
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock, PoisonError};
use std::time::{Duration, Instant};

use axum::extract::rejection::JsonRejection;
use axum::extract::{DefaultBodyLimit, Path, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Redirect, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use serde::{Deserialize, Serialize};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::time::MissedTickBehavior;
use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker;
use tower::ServiceBuilder;
use tower::limit::GlobalConcurrencyLimitLayer;
use tower_http::limit::RequestBodyLimitLayer;
use tower_http::timeout::TimeoutLayer;
use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer};
use tracing::{Level, instrument};
use tracing_subscriber::EnvFilter;

// ===== 1. 도메인 상수 =====

/// 짧은 코드의 글자 수. 62^7 = 3,521,614,606,208가지.
pub const CODE_LEN: usize = 7;
/// 받아 주는 URL의 최대 길이(바이트).
pub const MAX_URL_LEN: usize = 2048;
const ALPHABET: &[u8; 62] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

// ===== 2. 도메인 에러 =====

/// 입력이 도메인 규칙을 어겼을 때의 에러. 메시지는 API 응답에 그대로 실어도 안전하다.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DomainError {
    #[error("url is empty")]
    EmptyUrl,
    #[error("url is {len} bytes, longer than the {max}-byte limit")]
    UrlTooLong { len: usize, max: usize },
    #[error("url contains a character outside visible ASCII at byte {index}")]
    InvalidCharacter { index: usize },
    #[error("url must start with http:// or https://")]
    UnsupportedScheme,
    #[error("url has no host")]
    MissingHost,
    #[error("url must not contain user info (user@host)")]
    UserInfo,
    #[error("code must be exactly {} characters from [0-9A-Za-z]", CODE_LEN)]
    InvalidCode,
}

// ===== 3. Url 뉴타입: 만들어졌으면 검증을 통과한 것 =====

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Url(String);

fn strip_prefix_ignore_case<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
    let head = s.get(..prefix.len())?;
    head.eq_ignore_ascii_case(prefix)
        .then(|| &s[prefix.len()..])
}

impl Url {
    /// 앞뒤 공백을 걷어 낸 뒤 검사한다. 검사 순서: 비었나 → 길이 → 글자 → 스킴 → 호스트.
    pub fn parse(input: &str) -> Result<Self, DomainError> {
        let s = input.trim();
        if s.is_empty() {
            return Err(DomainError::EmptyUrl);
        }
        if s.len() > MAX_URL_LEN {
            return Err(DomainError::UrlTooLong {
                len: s.len(),
                max: MAX_URL_LEN,
            });
        }
        // Location 헤더에 넣을 수 있는 글자(보이는 ASCII 0x21..=0x7E)만 받는다.
        if let Some(index) = s.bytes().position(|b| !(0x21..=0x7e).contains(&b)) {
            return Err(DomainError::InvalidCharacter { index });
        }
        let rest = strip_prefix_ignore_case(s, "https://")
            .or_else(|| strip_prefix_ignore_case(s, "http://"))
            .ok_or(DomainError::UnsupportedScheme)?;
        let authority = rest.split(['/', '?', '#']).next().unwrap_or("");
        if authority.contains('@') {
            return Err(DomainError::UserInfo);
        }
        if authority.is_empty() || authority.starts_with(':') {
            return Err(DomainError::MissingHost);
        }
        Ok(Self(s.to_owned()))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl FromStr for Url {
    type Err = DomainError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::parse(s)
    }
}

impl fmt::Display for Url {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

// ===== 4. Code 뉴타입: 정확히 7글자 base62 =====

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Code(String);

impl Code {
    pub fn parse(s: &str) -> Result<Self, DomainError> {
        if s.len() == CODE_LEN && s.bytes().all(|b| b.is_ascii_alphanumeric()) {
            Ok(Self(s.to_owned()))
        } else {
            Err(DomainError::InvalidCode)
        }
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl FromStr for Code {
    type Err = DomainError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::parse(s)
    }
}

impl fmt::Display for Code {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

// ===== 5. 코드 생성기: SplitMix64 + 거절 샘플링 =====

/// 씨앗이 같으면 같은 코드 순서를 낸다(테스트 재현용). 암호학적으로 안전한 난수는 아니다.
pub struct CodeGen {
    state: u64,
}

impl CodeGen {
    pub fn from_seed(seed: u64) -> Self {
        Self { state: seed }
    }

    /// 프로세스마다 다른 씨앗. std의 RandomState 키와 현재 시각을 섞는다.
    pub fn from_entropy() -> Self {
        Self::from_seed(RandomState::new().hash_one(std::time::SystemTime::now()))
    }

    fn next_u64(&mut self) -> u64 {
        self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
        let mut z = self.state;
        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
        z ^ (z >> 31)
    }

    pub fn next_code(&mut self) -> Code {
        let mut out = String::with_capacity(CODE_LEN);
        while out.len() < CODE_LEN {
            for b in self.next_u64().to_le_bytes() {
                // 248 = 62 × 4. 248 이상을 버려야 62글자가 모두 같은 확률로 나온다.
                if b < 248 && out.len() < CODE_LEN {
                    out.push(char::from(ALPHABET[usize::from(b % 62)]));
                }
            }
        }
        Code(out)
    }
}

// ===== 6. 저장소 에러와 LinkStore 트레이트 (2단계) =====

#[derive(Debug, thiserror::Error)]
pub enum StoreError {
    #[error("code already exists")]
    Duplicate,
    #[error("stored row is invalid")]
    Corrupt(#[from] DomainError),
    #[error("database error")]
    Backend(#[source] sqlx::Error),
}

/// 저장소가 해 줄 일. 반환 퓨처에 `Send`를 요구해야 axum 핸들러(3단계)에서 쓸 수 있다.
pub trait LinkStore: Send + Sync + 'static {
    fn insert(&self, code: &Code, url: &Url)
    -> impl Future<Output = Result<(), StoreError>> + Send;
    fn resolve(&self, code: &Code) -> impl Future<Output = Result<Option<Url>, StoreError>> + Send;
    fn count(&self) -> impl Future<Output = Result<u64, StoreError>> + Send;
}

// ===== 7. SqliteStore =====

const SCHEMA: &str = "CREATE TABLE IF NOT EXISTS links (
    code       TEXT PRIMARY KEY NOT NULL CHECK (length(code) = 7),
    url        TEXT NOT NULL CHECK (length(url) <= 2048),
    hits       INTEGER NOT NULL DEFAULT 0,
    created_at INTEGER NOT NULL DEFAULT (unixepoch())
) STRICT";

pub struct SqliteStore {
    pool: SqlitePool,
}

impl SqliteStore {
    /// 연결하고 스키마를 만든다. 메모리 DB는 연결이 모두 닫히면 사라지므로 연결 하나를 계속 쥔다.
    pub async fn connect(db_url: &str) -> Result<Self, sqlx::Error> {
        let opts = SqliteConnectOptions::from_str(db_url)?.create_if_missing(true);
        let pool = if db_url.contains(":memory:") {
            // 5단계: 시간 제한이 acquire 도중의 퓨처를 버리면 sqlx는 그 연결을 풀에 돌려주지 않고 버린다.
            // 메모리 DB는 연결 하나가 곧 DB라서 표가 통째로 사라진다. 연결 전 점검(.await 하나)을 끈다.
            SqlitePoolOptions::new()
                .max_connections(1)
                .idle_timeout(None)
                .max_lifetime(None)
                .test_before_acquire(false)
        } else {
            SqlitePoolOptions::new().max_connections(8)
        }
        .connect_with(opts)
        .await?;
        sqlx::query(SCHEMA).execute(&pool).await?;
        Ok(Self { pool })
    }

    pub async fn close(&self) {
        self.pool.close().await;
    }
}

fn map_insert_error(e: sqlx::Error) -> StoreError {
    match e {
        sqlx::Error::Database(db) if db.is_unique_violation() => StoreError::Duplicate,
        other => StoreError::Backend(other),
    }
}

impl LinkStore for SqliteStore {
    // url은 기록하지 않는다. 사용자 URL의 쿼리에 토큰이 들어 있을 수 있다.
    #[instrument(skip(self, url), fields(code = %code), err(level = Level::WARN))]
    async fn insert(&self, code: &Code, url: &Url) -> Result<(), StoreError> {
        sqlx::query("INSERT INTO links (code, url) VALUES ($1, $2)")
            .bind(code.as_str())
            .bind(url.as_str())
            .execute(&self.pool)
            .await
            .map_err(map_insert_error)?;
        Ok(())
    }

    #[instrument(skip(self), fields(code = %code))]
    async fn resolve(&self, code: &Code) -> Result<Option<Url>, StoreError> {
        // 찾기와 조회수 올리기를 한 문장으로 한다(왕복 1번, 경쟁 없음).
        let raw: Option<String> =
            sqlx::query_scalar("UPDATE links SET hits = hits + 1 WHERE code = $1 RETURNING url")
                .bind(code.as_str())
                .fetch_optional(&self.pool)
                .await
                .map_err(StoreError::Backend)?;
        // DB에서 읽은 값도 다시 검증한다. 누가 SQL로 직접 넣은 값일 수 있다.
        Ok(raw.map(|s| Url::parse(&s)).transpose()?)
    }

    #[instrument(skip(self))]
    async fn count(&self) -> Result<u64, StoreError> {
        let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM links")
            .fetch_one(&self.pool)
            .await
            .map_err(StoreError::Backend)?;
        Ok(u64::try_from(n).unwrap_or(0))
    }
}

// ===== 8. 서비스 함수: 재시도와 에러 바꾸기 =====

pub const MAX_ATTEMPTS: u32 = 5;

#[derive(Debug, thiserror::Error)]
pub enum LinkError {
    #[error(transparent)]
    Invalid(#[from] DomainError),
    #[error("link not found")]
    NotFound,
    #[error("no free code after {0} attempts")]
    CodeSpaceExhausted(u32),
    #[error("storage failure")]
    Storage(#[source] StoreError),
}

#[instrument(skip_all)]
pub async fn shorten<S: LinkStore>(
    store: &S,
    codes: &Mutex<CodeGen>,
    raw_url: &str,
) -> Result<(Code, Url), LinkError> {
    let url = Url::parse(raw_url)?;
    for attempt in 1..=MAX_ATTEMPTS {
        // 잠금은 이 문장 끝에서 풀린다. std MutexGuard를 .await 너머로 들고 가면 퓨처가 Send가 아니다.
        let code = codes
            .lock()
            .unwrap_or_else(PoisonError::into_inner)
            .next_code();
        match store.insert(&code, &url).await {
            Ok(()) => {
                tracing::info!(code = %code, attempt, "link created");
                return Ok((code, url));
            }
            Err(StoreError::Duplicate) => {
                tracing::warn!(attempt, "code collision, retrying");
            }
            Err(e) => return Err(LinkError::Storage(e)),
        }
    }
    Err(LinkError::CodeSpaceExhausted(MAX_ATTEMPTS))
}

/// 형식이 틀린 코드는 DB에 묻지 않고 "없음"으로 답한다.
pub async fn resolve<S: LinkStore>(store: &S, raw_code: &str) -> Result<Url, LinkError> {
    let Ok(code) = Code::parse(raw_code) else {
        return Err(LinkError::NotFound);
    };
    store
        .resolve(&code)
        .await
        .map_err(LinkError::Storage)?
        .ok_or(LinkError::NotFound)
}

// ===== 9. HTTP 계층: 상태, 요청·응답 모양 (3단계) =====

/// 핸들러가 공유하는 것. 복제는 Arc 두 개를 복제할 뿐이라 싸다.
pub struct AppState<S> {
    store: Arc<S>,
    codes: Arc<Mutex<CodeGen>>,
    /// 통계 태스크가 마지막으로 센 링크 수. /health가 DB에 묻지 않고 읽는다.
    stats: Arc<AtomicU64>,
}

// #[derive(Clone)]은 S: Clone을 요구하므로 손으로 쓴다.
impl<S> Clone for AppState<S> {
    fn clone(&self) -> Self {
        Self {
            store: Arc::clone(&self.store),
            codes: Arc::clone(&self.codes),
            stats: Arc::clone(&self.stats),
        }
    }
}

impl<S> AppState<S> {
    pub fn new(store: S, codes: CodeGen) -> Self {
        Self {
            store: Arc::new(store),
            codes: Arc::new(Mutex::new(codes)),
            stats: Arc::default(),
        }
    }
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct CreateLink {
    url: String,
}

#[derive(Serialize)]
struct LinkBody {
    code: String,
    url: String,
}

// ===== 10. 에러 → HTTP 응답 =====

pub enum ApiError {
    BadJson(JsonRejection),
    Link(LinkError),
}

impl From<JsonRejection> for ApiError {
    fn from(r: JsonRejection) -> Self {
        Self::BadJson(r)
    }
}

impl From<LinkError> for ApiError {
    fn from(e: LinkError) -> Self {
        Self::Link(e)
    }
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, kind, message) = match &self {
            Self::BadJson(r) => (r.status(), "bad_request", r.body_text()),
            Self::Link(LinkError::Invalid(e)) => (
                StatusCode::UNPROCESSABLE_ENTITY,
                "invalid_url",
                e.to_string(),
            ),
            Self::Link(LinkError::NotFound) => {
                (StatusCode::NOT_FOUND, "not_found", "no such link".into())
            }
            Self::Link(LinkError::CodeSpaceExhausted(_)) => (
                StatusCode::SERVICE_UNAVAILABLE,
                "unavailable",
                "try again later".into(),
            ),
            // 내부 원인은 응답에 싣지 않고 로그에 원인 체인까지 남긴다.
            Self::Link(LinkError::Storage(e)) => {
                tracing::error!(error = %e, cause = ?std::error::Error::source(e), "storage failure");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "internal",
                    "internal error".into(),
                )
            }
        };
        let body = serde_json::json!({ "error": { "kind": kind, "message": message } });
        (status, Json(body)).into_response()
    }
}

// ===== 11. 핸들러와 라우터 =====

async fn create_link<S: LinkStore>(
    State(st): State<AppState<S>>,
    body: Result<Json<CreateLink>, JsonRejection>,
) -> Result<(StatusCode, Json<LinkBody>), ApiError> {
    let Json(req) = body?;
    let (code, url) = shorten(&*st.store, &st.codes, &req.url).await?;
    let body = LinkBody {
        code: code.to_string(),
        url: url.to_string(),
    };
    Ok((StatusCode::CREATED, Json(body)))
}

async fn follow<S: LinkStore>(
    State(st): State<AppState<S>>,
    Path(code): Path<String>,
) -> Result<Redirect, ApiError> {
    let url = resolve(&*st.store, &code).await?;
    // 307: 임시 이동. 브라우저가 영구 저장하지 않으므로 링크를 지우거나 세면 바로 반영된다.
    Ok(Redirect::temporary(url.as_str()))
}

async fn health<S>(State(st): State<AppState<S>>) -> Json<serde_json::Value> {
    let links = st.stats.load(Ordering::Relaxed);
    Json(serde_json::json!({ "status": "ok", "links": links }))
}

async fn not_found() -> ApiError {
    ApiError::Link(LinkError::NotFound)
}

pub fn router<S: LinkStore>(state: AppState<S>, cfg: &Config) -> Router {
    Router::new()
        .route("/health", get(health::<S>))
        .route("/links", post(create_link::<S>))
        .route("/{code}", get(follow::<S>))
        .fallback(not_found)
        .with_state(state)
        // 추출기별 기본 상한은 끄고, 모든 요청에 걸리는 tower-http 상한 하나만 둔다.
        .layer(DefaultBodyLimit::disable())
        // ServiceBuilder는 위에 쓴 레이어가 바깥이다: 추적 → 시간 제한 → 동시 처리 상한 → 본문 상한.
        .layer(
            ServiceBuilder::new()
                .layer(
                    TraceLayer::new_for_http()
                        .make_span_with(DefaultMakeSpan::new().level(Level::INFO))
                        .on_response(DefaultOnResponse::new().level(Level::INFO)),
                )
                // 핸들러가 이 시간 안에 못 끝내면 503. 단, 아래 상한에서 줄 선 시간은 재지 않는다.
                .layer(TimeoutLayer::with_status_code(
                    StatusCode::SERVICE_UNAVAILABLE,
                    cfg.request_timeout,
                ))
                // 세마포어 하나를 서버 전체가 나눠 쓴다(ConcurrencyLimitLayer는 감싼 서비스마다 따로 센다). 넘치면 줄 세운다.
                .layer(GlobalConcurrencyLimitLayer::new(cfg.max_in_flight))
                .layer(RequestBodyLimitLayer::new(cfg.max_body_bytes)),
        )
}

// ===== 12. 설정과 비밀값 (4단계) =====

/// 로그·Debug 출력에 값이 새지 않게 감싼다. 값을 꺼내려면 expose()를 불러야 한다(검색하기 쉽다).
#[derive(Clone)]
pub struct Secret<T>(T);

impl<T> Secret<T> {
    pub fn new(value: T) -> Self {
        Self(value)
    }
    pub fn expose(&self) -> &T {
        &self.0
    }
}

impl<T> fmt::Debug for Secret<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("[REDACTED]")
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogFormat {
    Pretty,
    Json,
}

#[derive(Debug, Clone)]
pub struct Config {
    pub bind: SocketAddr,
    pub database_url: Secret<String>,
    pub max_body_bytes: usize,
    pub log_format: LogFormat,
    pub request_timeout: Duration,
    pub max_in_flight: usize,
    pub stats_interval: Duration,
}

/// 정수 설정 하나를 읽는다. 없으면 기본값, 범위를 벗어나면 에러.
fn read_num(
    get: &impl Fn(&str) -> Option<String>,
    key: &'static str,
    default: u64,
    range: std::ops::RangeInclusive<u64>,
    reason: &'static str,
) -> Result<u64, ConfigError> {
    match get(key) {
        None => Ok(default),
        Some(v) => v
            .parse()
            .ok()
            .filter(|n| range.contains(n))
            .ok_or(ConfigError { key, reason }),
    }
}

/// 메시지에 입력값을 싣지 않는다. 비밀값이 섞인 변수일 수 있기 때문이다.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[error("invalid {key}: {reason}")]
pub struct ConfigError {
    key: &'static str,
    reason: &'static str,
}

impl Config {
    /// 환경 변수 대신 함수를 받는다. 테스트가 전역 환경을 건드리지 않아도 된다.
    pub fn from_lookup(get: impl Fn(&str) -> Option<String>) -> Result<Self, ConfigError> {
        let bad = |key, reason| ConfigError { key, reason };
        let bind = match get("BIND_ADDR") {
            None => SocketAddr::from(([127, 0, 0, 1], 3000)),
            Some(v) => v
                .parse()
                .map_err(|_| bad("BIND_ADDR", "expected ip:port"))?,
        };
        let database_url = get("DATABASE_URL").unwrap_or_else(|| "sqlite://links.db".into());
        if !database_url.starts_with("sqlite:") {
            return Err(bad("DATABASE_URL", "only sqlite: urls are supported"));
        }
        let max_body_bytes = match get("MAX_BODY_BYTES") {
            None => 16 * 1024,
            Some(v) => v
                .parse()
                .ok()
                .filter(|n| (64..=1024 * 1024).contains(n))
                .ok_or(bad("MAX_BODY_BYTES", "expected an integer in 64..=1048576"))?,
        };
        let log_format = match get("LOG_FORMAT").as_deref() {
            None | Some("pretty") => LogFormat::Pretty,
            Some("json") => LogFormat::Json,
            Some(_) => return Err(bad("LOG_FORMAT", "expected pretty or json")),
        };
        let ms = |key, default, reason| {
            read_num(&get, key, default, 1..=600_000, reason).map(Duration::from_millis)
        };
        let request_timeout = ms(
            "REQUEST_TIMEOUT_MS",
            5_000,
            "expected milliseconds in 1..=600000",
        )?;
        let stats_interval = ms(
            "STATS_INTERVAL_MS",
            30_000,
            "expected milliseconds in 1..=600000",
        )?;
        let max_in_flight = read_num(&get, "MAX_IN_FLIGHT", 256, 1..=10_000, "expected 1..=10000")?;
        Ok(Self {
            bind,
            database_url: Secret::new(database_url),
            max_body_bytes,
            log_format,
            request_timeout,
            max_in_flight: usize::try_from(max_in_flight).unwrap_or(256),
            stats_interval,
        })
    }

    pub fn from_env() -> Result<Self, ConfigError> {
        Self::from_lookup(|k| std::env::var(k).ok())
    }
}

// ===== 13. 로그 초기화 =====

/// RUST_LOG가 있으면 그것을, 없으면 info를 쓴다. 이미 설치돼 있으면 조용히 넘어간다.
pub fn init_tracing(format: LogFormat) {
    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
    let builder = tracing_subscriber::fmt().with_env_filter(filter);
    let _ = match format {
        LogFormat::Json => builder.json().try_init(),
        LogFormat::Pretty => builder.try_init(),
    };
}

/// 자체 테스트용: JSON 로그를 메모리에 모은다. 전역 구독자는 프로세스에 하나라서 OnceLock으로 한 번만 설치한다.
#[derive(Clone)]
struct Capture(Arc<Mutex<Vec<u8>>>);

impl std::io::Write for Capture {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        self.0
            .lock()
            .unwrap_or_else(PoisonError::into_inner)
            .extend_from_slice(buf);
        Ok(buf.len())
    }
    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

fn captured_logs() -> String {
    static CAPTURE: OnceLock<Capture> = OnceLock::new();
    let cap = CAPTURE.get_or_init(|| {
        let cap = Capture(Arc::default());
        let writer = cap.clone();
        let _ = tracing_subscriber::fmt()
            .json()
            .with_env_filter(EnvFilter::new("info"))
            .with_writer(move || writer.clone())
            .try_init();
        cap
    });
    let bytes = cap.0.lock().unwrap_or_else(PoisonError::into_inner);
    String::from_utf8_lossy(&bytes).into_owned()
}

// ===== 14. 백그라운드 태스크: 주기적으로 링크 수 세기 (5단계) =====

/// 토큰이 취소되면 다음 틱을 기다리지 않고 바로 끝난다.
#[instrument(skip_all)]
async fn stats_task<S: LinkStore>(
    store: Arc<S>,
    stats: Arc<AtomicU64>,
    every: Duration,
    token: CancellationToken,
) {
    let mut ticker = tokio::time::interval(every);
    ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
    loop {
        tokio::select! {
            () = token.cancelled() => break,
            _ = ticker.tick() => match store.count().await {
                Ok(n) => stats.store(n, Ordering::Relaxed),
                Err(e) => tracing::warn!(error = %e, "stats count failed"),
            },
        }
    }
    tracing::info!("stats task stopped");
}

// ===== 15. 서버 시작과 안전한 종료 =====

pub const SHUTDOWN_GRACE: Duration = Duration::from_secs(10);

pub struct Running {
    pub addr: SocketAddr,
    token: CancellationToken,
    tracker: TaskTracker,
}

/// 서버와 통계 태스크를 같은 토큰·트래커 아래에 띄운다.
pub async fn start<S: LinkStore>(
    cfg: &Config,
    store: S,
    codes: CodeGen,
) -> std::io::Result<Running> {
    let state = AppState::new(store, codes);
    let (token, tracker) = (CancellationToken::new(), TaskTracker::new());
    tracker.spawn(stats_task(
        Arc::clone(&state.store),
        Arc::clone(&state.stats),
        cfg.stats_interval,
        token.child_token(),
    ));
    let listener = TcpListener::bind(cfg.bind).await?;
    let addr = listener.local_addr()?;
    let app = router(state, cfg);
    let signal = token.clone().cancelled_owned();
    tracker.spawn(async move {
        if let Err(e) = axum::serve(listener, app)
            .with_graceful_shutdown(signal)
            .await
        {
            tracing::error!(error = %e, "server error");
        }
        tracing::info!("server stopped");
    });
    tracing::info!(%addr, "listening");
    Ok(Running {
        addr,
        token,
        tracker,
    })
}

impl Running {
    /// 새 연결을 그만 받고, 처리 중인 요청과 태스크가 끝나기를 grace만큼 기다린다.
    pub async fn shutdown(self, grace: Duration) -> bool {
        self.token.cancel();
        self.tracker.close();
        let done = tokio::time::timeout(grace, self.tracker.wait())
            .await
            .is_ok();
        if !done {
            tracing::warn!(
                remaining = self.tracker.len(),
                "grace period over, exiting anyway"
            );
        }
        done
    }
}

async fn shutdown_signal() {
    let ctrl_c = async {
        tokio::signal::ctrl_c().await.ok();
    };
    #[cfg(unix)]
    let term = async {
        use tokio::signal::unix::{SignalKind, signal};
        if let Ok(mut s) = signal(SignalKind::terminate()) {
            s.recv().await;
        }
    };
    #[cfg(not(unix))]
    let term = std::future::pending::<()>();
    tokio::select! {
        () = ctrl_c => {},
        () = term => {},
    }
}

// ===== 16. 자체 테스트용: resolve를 일부러 늦추는 저장소 =====

pub struct Slow<S> {
    inner: S,
    delay: Duration,
}

impl<S: LinkStore> LinkStore for Slow<S> {
    async fn insert(&self, code: &Code, url: &Url) -> Result<(), StoreError> {
        self.inner.insert(code, url).await
    }
    async fn resolve(&self, code: &Code) -> Result<Option<Url>, StoreError> {
        tokio::time::sleep(self.delay).await;
        self.inner.resolve(code).await
    }
    async fn count(&self) -> Result<u64, StoreError> {
        self.inner.count().await
    }
}

// ===== 17. 진짜 소켓으로 보내는 작은 HTTP 클라이언트 (자체 테스트용) =====

pub struct Reply {
    status: u16,
    head: String,
    body: String,
}

pub async fn http(addr: SocketAddr, method: &str, path: &str, body: &str) -> Reply {
    let mut s = tokio::net::TcpStream::connect(addr).await.expect("connect");
    let req = format!(
        "{method} {path} HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n\
         Content-Type: application/json\r\nContent-Length: {}\r\n\r\n{body}",
        body.len()
    );
    s.write_all(req.as_bytes()).await.expect("write");
    let mut raw = String::new();
    s.read_to_string(&mut raw).await.expect("read");
    let (head, body) = raw.split_once("\r\n\r\n").expect("head/body");
    let status = head.split(' ').nth(1).and_then(|c| c.parse().ok());
    Reply {
        status: status.expect("status"),
        head: head.to_ascii_lowercase(),
        body: body.to_owned(),
    }
}

// ===== 18. main: 자체 테스트(기본) 또는 서버(SERVE=1) =====

fn test_config(extra: &[(&str, &str)]) -> Config {
    let mut vars: HashMap<String, String> = [
        ("BIND_ADDR", "127.0.0.1:0"),
        ("DATABASE_URL", "sqlite::memory:"),
        ("MAX_BODY_BYTES", "256"),
        ("STATS_INTERVAL_MS", "20"),
    ]
    .iter()
    .map(|(k, v)| (k.to_string(), v.to_string()))
    .collect();
    vars.extend(extra.iter().map(|(k, v)| (k.to_string(), v.to_string())));
    Config::from_lookup(|k| vars.get(k).cloned()).expect("test config")
}

async fn self_check() {
    captured_logs();
    let cfg = test_config(&[]);
    let store = SqliteStore::connect(cfg.database_url.expose())
        .await
        .expect("db");
    let slow = Slow {
        inner: store,
        delay: Duration::from_millis(300),
    };
    let running = start(&cfg, slow, CodeGen::from_seed(3))
        .await
        .expect("start");
    let addr = running.addr;

    let r = http(addr, "POST", "/links", r#"{"url":"https://example.com/x"}"#).await;
    assert_eq!(r.status, 201, "{}", r.body);
    let v: serde_json::Value = serde_json::from_str(&r.body).expect("json");
    let code = v["code"].as_str().expect("code").to_owned();
    tokio::time::sleep(Duration::from_millis(60)).await;
    let r = http(addr, "GET", "/health", "").await;
    assert_eq!(r.body, r#"{"links":1,"status":"ok"}"#);
    let big = format!(r#"{{"url":"https://a.b/{}"}}"#, "x".repeat(300));
    let r = http(addr, "POST", "/links", &big).await;
    assert_eq!(r.status, 413);

    // 처리 중(300ms 걸리는) 요청을 걸어 둔 채 종료를 시작한다.
    let in_flight = tokio::spawn(async move { http(addr, "GET", &format!("/{code}"), "").await });
    tokio::time::sleep(Duration::from_millis(50)).await;
    let t0 = Instant::now();
    assert!(
        running.shutdown(SHUTDOWN_GRACE).await,
        "graceful shutdown timed out"
    );
    let r = in_flight.await.expect("join");
    println!(
        "종료 중에도 끝난 요청: {} ({:?} 걸려 종료)",
        r.status,
        t0.elapsed()
    );
    assert_eq!(r.status, 307);
    assert!(r.head.contains("location: https://example.com/x"));
    assert!(
        tokio::net::TcpStream::connect(addr).await.is_err(),
        "종료 뒤 새 연결은 거절돼야 한다"
    );

    let logs = captured_logs();
    assert!(logs.contains("stats task stopped") && logs.contains("server stopped"));
}

async fn serve_forever() {
    let cfg = Config::from_env().unwrap_or_else(|e| {
        eprintln!("config error: {e}");
        std::process::exit(2);
    });
    init_tracing(cfg.log_format);
    tracing::info!(config = ?cfg, "starting");
    let store = SqliteStore::connect(cfg.database_url.expose())
        .await
        .expect("db");
    let running = start(&cfg, store, CodeGen::from_entropy())
        .await
        .expect("start");
    shutdown_signal().await;
    tracing::info!("shutdown signal received");
    if !running.shutdown(SHUTDOWN_GRACE).await {
        std::process::exit(1);
    }
}

#[tokio::main]
async fn main() {
    if std::env::var_os("SERVE").is_some() {
        serve_forever().await;
        return;
    }
    self_check().await;
    println!("ALL CHECKS PASSED");
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::body::Body;
    use axum::http::Request;
    use tower::ServiceExt;

    async fn slow_app(delay_ms: u64, extra: &[(&str, &str)]) -> Router {
        let store = SqliteStore::connect("sqlite::memory:").await.unwrap();
        let code = Code::parse("abcdefg").unwrap();
        store
            .insert(&code, &Url::parse("https://a.b").unwrap())
            .await
            .unwrap();
        let slow = Slow {
            inner: store,
            delay: Duration::from_millis(delay_ms),
        };
        router(
            AppState::new(slow, CodeGen::from_seed(5)),
            &test_config(extra),
        )
    }

    fn get(uri: &str) -> Request<Body> {
        Request::builder().uri(uri).body(Body::empty()).unwrap()
    }

    #[tokio::test]
    async fn self_check_passes() {
        self_check().await;
    }

    #[tokio::test]
    async fn slow_handler_times_out_with_503() {
        let app = slow_app(300, &[("REQUEST_TIMEOUT_MS", "50")]).await;
        let resp = app.oneshot(get("/abcdefg")).await.unwrap();
        assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
    }

    #[tokio::test]
    async fn concurrency_limit_queues_and_queue_time_is_not_timed() {
        let cfg = [("REQUEST_TIMEOUT_MS", "300"), ("MAX_IN_FLIGHT", "1")];
        let app = slow_app(200, &cfg).await;
        let t0 = Instant::now();
        let (a, b) = tokio::join!(
            app.clone().oneshot(get("/abcdefg")),
            app.oneshot(get("/abcdefg"))
        );
        // 둘째 요청은 허가를 200ms 기다린 뒤 200ms 일한다. 합 400ms > 300ms인데도 503이 아니다:
        // 기다림은 poll_ready에서 일어나고, TimeoutLayer의 시계는 call부터 돈다.
        assert_eq!(a.unwrap().status(), StatusCode::TEMPORARY_REDIRECT);
        assert_eq!(b.unwrap().status(), StatusCode::TEMPORARY_REDIRECT);
        assert!(
            t0.elapsed() >= Duration::from_millis(390),
            "{:?}",
            t0.elapsed()
        );
    }

    #[tokio::test]
    async fn body_limit_applies_without_content_length() {
        let app = slow_app(0, &[]).await;
        let chunks = futures::stream::iter(
            (0..10).map(|_| Ok::<_, std::io::Error>(axum::body::Bytes::from(vec![b' '; 100]))),
        );
        let req = Request::builder()
            .method("POST")
            .uri("/links")
            .header("content-type", "application/json")
            .body(Body::from_stream(chunks))
            .unwrap();
        let resp = app.oneshot(req).await.unwrap();
        assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
    }

    /// 시간 제한은 진행 중인 저장소 퓨처를 버린다. 몇 번 poll한 뒤 버리기를 되풀이해도 메모리 DB가 남아야 한다.
    #[tokio::test]
    async fn dropped_queries_keep_memory_db() {
        let store = SqliteStore::connect("sqlite::memory:").await.unwrap();
        let code = Code::parse("abcdefg").unwrap();
        store
            .insert(&code, &Url::parse("https://a.b").unwrap())
            .await
            .unwrap();
        for polls in 1..30 {
            for _ in 0..10 {
                let mut f = std::pin::pin!(store.resolve(&code));
                for _ in 0..polls {
                    let ready = std::future::poll_fn(|cx| {
                        std::task::Poll::Ready(f.as_mut().poll(cx).is_ready())
                    })
                    .await;
                    if ready {
                        break;
                    }
                    tokio::task::yield_now().await;
                }
            }
        }
        assert_eq!(store.count().await.unwrap(), 1, "메모리 DB가 사라졌다");
    }

    #[test]
    fn config_rejects_bad_limits() {
        let get = |k: &'static str, v: &'static str| {
            Config::from_lookup(move |x| (x == k).then(|| v.into()))
        };
        assert!(get("REQUEST_TIMEOUT_MS", "0").is_err());
        assert!(get("MAX_IN_FLIGHT", "0").is_err());
        assert!(get("STATS_INTERVAL_MS", "abc").is_err());
        let cfg = Config::from_lookup(|_| None).unwrap();
        assert_eq!(cfg.request_timeout, Duration::from_secs(5));
        assert_eq!(cfg.max_in_flight, 256);
    }
}