pub struct AppConfig {Show 40 fields
pub name: String,
pub script: String,
pub args: Vec<String>,
pub cwd: Option<String>,
pub interpreter: Option<String>,
pub env: BTreeMap<String, String>,
pub instances: u32,
pub autorestart: bool,
pub autostart: bool,
pub stop_exit_codes: Vec<i32>,
pub min_uptime: UpDuration,
pub max_restarts: u32,
pub restart_delay: Option<UpDuration>,
pub exp_backoff_restart_delay: Option<UpDuration>,
pub kill_signal: Option<String>,
pub kill_timeout: UpDuration,
pub shutdown_with_message: bool,
pub listen_timeout: UpDuration,
pub graceful_timeout: UpDuration,
pub action_timeout: UpDuration,
pub max_memory: Option<MemSize>,
pub watch: bool,
pub ignore_watch: Vec<String>,
pub watch_delay: Option<UpDuration>,
pub cron_restart: Option<String>,
pub fold: Option<String>,
pub user: Option<String>,
pub group: Option<String>,
pub out_file: Option<String>,
pub err_file: Option<String>,
pub merge_logs: bool,
pub channel: bool,
pub stdin: bool,
pub wait_ready: bool,
pub reuse_port: bool,
pub readiness_probe: Option<ProbeConfig>,
pub liveness_probe: Option<ProbeConfig>,
pub watch_options: Vec<String>,
pub cron_timezone: Option<String>,
pub increment_var: Option<String>,
}Expand description
Per-app configuration — one sheep’s entry in a Flockfile
Field names are the Flockfile contract (sheep-native; pm2 spellings are rejected — the importer translates them). Unknown fields are errors so typos fail loudly at parse time.
§Example
use shep_core::config::AppConfig;
let app: AppConfig = toml::from_str("name = \"web\"\nscript = \"./srv\"").unwrap();
assert!(app.autorestart); // spec defaultFields§
§name: StringUnique sheep name (required)
script: StringExecutable or script path (required)
args: Vec<String>Arguments passed to the script
cwd: Option<String>Working directory (default: daemon’s cwd at spawn registration)
interpreter: Option<String>Interpreter override ("none" = run script directly)
env: BTreeMap<String, String>Environment for the sheep (merged over the daemon’s filtered env)
instances: u32Instance count (“cluster” = N fork instances; spec §4)
autorestart: boolRestart on unexpected exit
autostart: boolStart when the daemon starts / on shep muster
stop_exit_codes: Vec<i32>Exit codes treated as clean stop (no restart)
min_uptime: UpDurationUptime below this marks an exit as unstable
max_restarts: u32Consecutive unstable exits before errored
restart_delay: Option<UpDuration>Fixed delay before every restart (alternative to backoff)
exp_backoff_restart_delay: Option<UpDuration>Initial backoff delay; grows ×1.5 capped at 15s (spec §4)
Defaults to 100ms, not unset. An unstable exit (sooner than
min_uptime) with neither this nor restart_delay configured used
to restart with no delay at all, so an app that could never start
(a missing dependency, a bad config) burned its whole max_restarts
budget inside a second, logging the same failure dozens of times.
min_uptime already existed to name that case as unstable; only the
default that should have throttled it was missing.
All of the above assumes restart_delay is unset. A fixed
restart_delay takes precedence over this field on every exit,
stable or not, so a stable exit restarts immediately only while
restart_delay stays unset, and setting this field to "0"
disables the backoff without producing an immediate restart if a
nonzero restart_delay is also configured.
kill_signal: Option<String>Stop signal, one of SIGTERM/SIGINT/SIGQUIT/SIGUSR2 (the SIG
prefix and the case are both optional). Unset means SIGTERM.
A String rather than a KillSignal so
the Flockfile schema and this struct’s wire form stay plain text;
normalize is what refuses a name outside that set, the same split
cron_restart and the watch globs already use.
kill_timeout: UpDurationGrace period between stop signal and SIGKILL
shutdown_with_message: boolSend {"kind":"shutdown"} on the shepherd channel instead of a signal
listen_timeout: UpDurationReadiness fallback window when no ready signal/probe configured
graceful_timeout: UpDurationDrain window for the old instance during reload
action_timeout: UpDurationHow long a triggered action gets to answer on the shepherd channel
before its row becomes ActionOutcome::TimedOut.
Defaults to 3s — comfortably under the 5s an RPC caller gets when it
sends no deadline of its own (shep-client’s DEFAULT_DEADLINE,
mirrored daemon-side as rpc’s DEFAULT_DEADLINE_MS). The margin
matters more than the number: push this past that budget and a caller
using the plain default gives up with DeadlineExceeded before the
daemon’s own honest TimedOut row ever reaches it. A legitimately
slow action (a cache flush, say) can still ask for longer, but its
caller has to ask for a longer deadline in step —
Client::request_with_deadline, the way shep logs -f already asks
for LOG_PLANE_DEADLINE rather than the client’s default. normalize
refuses a value no caller could ever satisfy, however long a deadline
it asks for; a value merely above the default budget is a caller’s
choice to widen its own deadline, not a config error this crate can
see.
max_memory: Option<MemSize>Memory ceiling — polling enforcer restarts above this
watch: boolWatch files and restart on change
ignore_watch: Vec<String>Watch ignore globs (defaults added daemon-side: dot-entries, node_modules)
watch_delay: Option<UpDuration>Watch debounce window (default 500ms, applied daemon-side)
cron_restart: Option<String>Cron pattern for scheduled restarts (croner dialect)
fold: Option<String>Fold (group) this sheep belongs to
user: Option<String>Run as this user (unix)
group: Option<String>Run as this group (unix)
out_file: Option<String>Stdout log file (default: $SHEP_HOME/logs/<name>-<instance>-out.log; merge_logs collapses to <name>-out.log)
err_file: Option<String>Stderr log file (default: $SHEP_HOME/logs/<name>-<instance>-err.log; merge_logs collapses to <name>-err.log)
merge_logs: boolMerge instance logs into one file pair
channel: boolOpen the shepherd channel on fd 3 for this app on its own, without
needing wait_ready or shutdown_with_message to imply it.
Defaults to false: a socketpair plus two pump tasks per sheep is
real cost weighed against spec §14.11’s single-digit-MB idle-RSS
goal, so a channel is opened only when something asks for one.
stdin: boolOpen a pipe on this sheep’s stdin, so shep whisper can write to it.
Defaults to false, and the default is the decision rather than a
convenience. Without it a sheep gets /dev/null on fd 0, which is what
every sheep has had until now, and three things argue for keeping it
that way unless an app asks otherwise:
- Flipping it for the whole flock is a behaviour change to processes nobody asked to change.
- Programs detect stdin. A closed or null fd 0 is how a great many programs decide they are non-interactive — no prompt, no pager, no readline, no colour. Handing them a pipe silently moves them to the other branch.
- It costs a descriptor and a pump task per sheep for the whole life of
the process, against spec §14.11’s single-digit-MB idle-RSS goal — the
same budget
Self::channel’s own default is protecting.
Unlike channel, nothing implies this: wait_ready and
shutdown_with_message both need fd 3 and so turn channel on for you,
while nothing in shep needs a sheep’s stdin except an operator typing
shep whisper. A sheep without it answers a no_stdin row and names
this field.
The pipe’s write end lives as long as the sheep does, so the app sees EOF on stdin when the process is on its way out, never before.
wait_ready: boolExpect {"kind":"ready"} on the shepherd channel
reuse_port: boolAsserts that the app itself sets SO_REUSEPORT before it binds —
shep binds nothing, so it cannot set the option on the app’s behalf.
The child process owns the mechanism (Node ≥22’s reusePort, Go’s
net.ListenConfig.Control, nginx’s reuseport); shep’s contribution
is permission for the old and new instance to overlap during reload,
not the socket option itself.
This field is inert today. shep never reads it: reload overlap
already happens unconditionally, so setting it changes nothing and
leaving it unset costs nothing. It is kept because shep import
writes it for a cluster-mode pm2 app and shep flock displays it, so
dropping it would silently discard a value out of an imported config.
It becomes load-bearing the day shep gains a reload mode that does NOT
overlap by default, which is when the permission it describes stops
being free — see docs/specs/deferred.md.
readiness_probe: Option<ProbeConfig>Readiness probe — gates reload’s AwaitReady (spec §7)
liveness_probe: Option<ProbeConfig>Liveness probe — failures feed the restart policy (spec §7)
watch_options: Vec<String>Watch include globs (empty = watch cwd)
cron_timezone: Option<String>Timezone for cron_restart (IANA name)
increment_var: Option<String>Env var receiving the instance slot (default SHEP_INSTANCE)
Implementations§
Source§impl AppConfig
impl AppConfig
Sourcepub fn minimal(name: &str, script: &str) -> Self
pub fn minimal(name: &str, script: &str) -> Self
A minimal config with spec defaults — the programmatic entry point
Sourcepub fn drifted_fields(&self, other: &Self) -> Vec<String>
pub fn drifted_fields(&self, other: &Self) -> Vec<String>
The names of the fields whose values differ between self and
other, in field-name order.
Names only, never values. The one caller sends this list across the
wire to be printed at an operator, and AppConfig::env carries
secrets, so a differing env reports "env" and stops there (IR-41).
Compare configs that have both been through
normalize. Two configs differing only
in what normalization would have filled in are not a difference an
operator can act on, and reporting them would make the caller noisy
about nothing.
§Example
use shep_core::config::AppConfig;
let stored = AppConfig::minimal("web", "./srv");
let mut edited = stored.clone();
edited.cwd = Some("/srv".to_string());
assert_eq!(stored.drifted_fields(&edited), vec!["cwd".to_string()]);
assert!(stored.drifted_fields(&stored).is_empty());Trait Implementations§
Source§impl<'de> Deserialize<'de> for AppConfig
impl<'de> Deserialize<'de> for AppConfig
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
impl Eq for AppConfig
Source§impl JsonSchema for AppConfig
impl JsonSchema for AppConfig
Source§fn schema_id() -> Cow<'static, str>
fn schema_id() -> Cow<'static, str>
Source§fn json_schema(generator: &mut SchemaGenerator) -> Schema
fn json_schema(generator: &mut SchemaGenerator) -> Schema
Source§fn inline_schema() -> bool
fn inline_schema() -> bool
$ref keyword. Read moreimpl StructuralPartialEq for AppConfig
Auto Trait Implementations§
impl Freeze for AppConfig
impl RefUnwindSafe for AppConfig
impl Send for AppConfig
impl Sync for AppConfig
impl Unpin for AppConfig
impl UnsafeUnpin for AppConfig
impl UnwindSafe for AppConfig
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.