Skip to main content

folk_core/
config.rs

1//! Configuration loading.
2//!
3//! Folk reads `folk.toml` from the working directory by default, with
4//! environment-variable overrides via the `FOLK_` prefix (e.g.,
5//! `FOLK_WORKERS_COUNT=8`).
6
7use std::path::Path;
8use std::time::Duration;
9
10use anyhow::{Context, Result};
11use figment::Figment;
12use figment::providers::{Env, Format, Toml};
13use serde::{Deserialize, Serialize};
14
15/// Top-level Folk configuration.
16#[derive(Debug, Clone, Default, Serialize, Deserialize)]
17#[serde(default)]
18pub struct FolkConfig {
19    pub server: ServerConfig,
20    pub workers: WorkersConfig,
21    pub log: LogConfig,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(default)]
26pub struct ServerConfig {
27    /// Path to the admin RPC Unix socket. Default: `/tmp/folk.sock`.
28    pub rpc_socket: String,
29    /// Maximum time to wait for graceful shutdown after SIGTERM.
30    #[serde(with = "humantime_serde")]
31    pub shutdown_timeout: Duration,
32}
33
34impl Default for ServerConfig {
35    fn default() -> Self {
36        Self {
37            rpc_socket: "/tmp/folk.sock".into(),
38            shutdown_timeout: Duration::from_secs(30),
39        }
40    }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(default)]
45pub struct WorkersConfig {
46    /// Path to the PHP worker script (e.g., `vendor/bin/folk-worker`).
47    pub script: String,
48    /// PHP binary path (default: `php`).
49    pub php: String,
50    /// Number of worker processes.
51    pub count: usize,
52    /// Recycle a worker after this many requests.
53    pub max_jobs: u64,
54    /// Recycle a worker that has been alive longer than this.
55    #[serde(with = "humantime_serde")]
56    pub ttl: Duration,
57    /// Recycle a worker exceeding this RSS in MB.
58    pub max_memory_mb: u64,
59    /// Per-request execution timeout.
60    #[serde(with = "humantime_serde")]
61    pub exec_timeout: Duration,
62    /// Per-worker boot timeout (waits for `control.ready`).
63    #[serde(with = "humantime_serde")]
64    pub boot_timeout: Duration,
65}
66
67impl Default for WorkersConfig {
68    fn default() -> Self {
69        Self {
70            script: "vendor/bin/folk-worker".into(),
71            php: "php".into(),
72            count: 4,
73            max_jobs: 1000,
74            ttl: Duration::from_secs(3600),
75            max_memory_mb: 256,
76            exec_timeout: Duration::from_secs(30),
77            boot_timeout: Duration::from_secs(30),
78        }
79    }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(default)]
84pub struct LogConfig {
85    /// Log level filter (e.g., `info`, `debug`, `folk_core=trace`).
86    pub filter: String,
87    /// Output format: `text`, `json`, or `pretty`.
88    pub format: LogFormat,
89}
90
91impl Default for LogConfig {
92    fn default() -> Self {
93        Self {
94            filter: "info".into(),
95            format: LogFormat::Text,
96        }
97    }
98}
99
100#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
101#[serde(rename_all = "snake_case")]
102pub enum LogFormat {
103    Text,
104    Json,
105    Pretty,
106}
107
108impl FolkConfig {
109    /// Load config from `folk.toml` in the current directory plus environment
110    /// variables prefixed with `FOLK_`. Missing file is OK; defaults are used.
111    pub fn load() -> Result<Self> {
112        Self::load_from(Path::new("folk.toml"))
113    }
114
115    /// Load config from a specific path. Missing file is OK; defaults are used.
116    pub fn load_from(path: impl AsRef<Path>) -> Result<Self> {
117        let path = path.as_ref();
118        let mut fig = Figment::from(figment::providers::Serialized::defaults(Self::default()));
119        if path.exists() {
120            fig = fig.merge(Toml::file(path));
121        }
122        fig = fig.merge(Env::prefixed("FOLK_").split("_"));
123        fig.extract().context("failed to parse Folk configuration")
124    }
125}