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