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    /// Runtime selection: `pipe` (phase 4) or `fork` (phase 10).
30    pub runtime: RuntimeKind,
31    /// Maximum time to wait for graceful shutdown after SIGTERM.
32    #[serde(with = "humantime_serde")]
33    pub shutdown_timeout: Duration,
34}
35
36impl Default for ServerConfig {
37    fn default() -> Self {
38        Self {
39            rpc_socket: "/tmp/folk.sock".into(),
40            runtime: RuntimeKind::Pipe,
41            shutdown_timeout: Duration::from_secs(30),
42        }
43    }
44}
45
46#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
47#[serde(rename_all = "snake_case")]
48pub enum RuntimeKind {
49    Pipe,
50    Fork,
51    Embed,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55#[serde(default)]
56pub struct WorkersConfig {
57    /// Path to the PHP worker script (e.g., `vendor/bin/folk-worker`).
58    pub script: String,
59    /// PHP binary path (default: `php`).
60    pub php: String,
61    /// Number of worker processes.
62    pub count: usize,
63    /// Recycle a worker after this many requests.
64    pub max_jobs: u64,
65    /// Recycle a worker that has been alive longer than this.
66    #[serde(with = "humantime_serde")]
67    pub ttl: Duration,
68    /// Recycle a worker exceeding this RSS in MB.
69    pub max_memory_mb: u64,
70    /// Per-request execution timeout.
71    #[serde(with = "humantime_serde")]
72    pub exec_timeout: Duration,
73    /// Per-worker boot timeout (waits for `control.ready`).
74    #[serde(with = "humantime_serde")]
75    pub boot_timeout: Duration,
76}
77
78impl Default for WorkersConfig {
79    fn default() -> Self {
80        Self {
81            script: "vendor/bin/folk-worker".into(),
82            php: "php".into(),
83            count: 4,
84            max_jobs: 1000,
85            ttl: Duration::from_secs(3600),
86            max_memory_mb: 256,
87            exec_timeout: Duration::from_secs(30),
88            boot_timeout: Duration::from_secs(30),
89        }
90    }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(default)]
95pub struct LogConfig {
96    /// Log level filter (e.g., `info`, `debug`, `folk_core=trace`).
97    pub filter: String,
98    /// Output format: `text`, `json`, or `pretty`.
99    pub format: LogFormat,
100}
101
102impl Default for LogConfig {
103    fn default() -> Self {
104        Self {
105            filter: "info".into(),
106            format: LogFormat::Text,
107        }
108    }
109}
110
111#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
112#[serde(rename_all = "snake_case")]
113pub enum LogFormat {
114    Text,
115    Json,
116    Pretty,
117}
118
119impl FolkConfig {
120    /// Load config from `folk.toml` in the current directory plus environment
121    /// variables prefixed with `FOLK_`. Missing file is OK; defaults are used.
122    pub fn load() -> Result<Self> {
123        Self::load_from(Path::new("folk.toml"))
124    }
125
126    /// Load config from a specific path. Missing file is OK; defaults are used.
127    pub fn load_from(path: impl AsRef<Path>) -> Result<Self> {
128        let path = path.as_ref();
129        let mut fig = Figment::from(figment::providers::Serialized::defaults(Self::default()));
130        if path.exists() {
131            fig = fig.merge(Toml::file(path));
132        }
133        fig = fig.merge(Env::prefixed("FOLK_").split("_"));
134        fig.extract().context("failed to parse Folk configuration")
135    }
136}