Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions docs/guides/ready-checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ ready_output = "Serving HTTP on"

## HTTP Check

Wait until an HTTP endpoint returns a 2xx status code.
Wait until an HTTP endpoint returns a 2xx status code, or a configured exact
status code.

**CLI:**
```bash
Expand All @@ -67,12 +68,18 @@ ready_http = "http://localhost:8000/health"
[daemons.webserver]
run = "node server.js"
ready_http = "http://localhost:3000/ready"

[daemons.private_api]
run = "node server.js"
ready_http = { url = "http://localhost:3000/health", status = [200, 401] }
```

**Best for:** Web services with health check endpoints.

::: tip
The HTTP check polls every 500ms with a 5 second timeout per request.
The HTTP check polls every 500ms with a 5 second timeout per request. The string
form accepts any 2xx response; the object form accepts the exact `status` codes
you list.
:::

## Port Check
Expand Down Expand Up @@ -135,7 +142,7 @@ The command check polls every 500ms. Use this when you need more complex readine
|------------|-----------|
| Delay | Daemon runs for N seconds without crashing |
| Output | Pattern matches stdout/stderr |
| HTTP | Endpoint returns 2xx status |
| HTTP | Endpoint returns 2xx status, or a configured exact status |
| Port | TCP connection to port succeeds |
| Command | Shell command returns exit code 0 |

Expand Down
42 changes: 38 additions & 4 deletions docs/public/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,14 @@
"minimum": 0
},
"ready_http": {
"description": "HTTP URL to poll for readiness (expects 2xx response)",
"type": [
"string",
"null"
"description": "HTTP URL to poll for readiness. Accepts any 2xx response by default, or configured statuses.",
"anyOf": [
{
"$ref": "#/$defs/ReadyHttp"
},
{
"type": "null"
}
]
},
"ready_output": {
Expand Down Expand Up @@ -442,6 +446,36 @@
}
]
},
"ReadyHttp": {
"description": "HTTP readiness check: a URL string accepting any 2xx response, or { url, status } object with exact accepted status codes",
"oneOf": [
{
"description": "HTTP URL to poll for readiness; any 2xx response is ready",
"type": "string"
},
{
"type": "object",
"properties": {
"status": {
"description": "Exact HTTP status codes that indicate readiness. Omit to accept any 2xx response.",
"type": "array",
"items": {
"type": "integer",
"maximum": 599,
"minimum": 100
}
},
"url": {
"description": "HTTP URL to poll for readiness",
"type": "string"
}
},
"required": [
"url"
]
}
]
},
"Retry": {
"description": "Retry: true = indefinite, false/0 = none, number = count",
"oneOf": [
Expand Down
8 changes: 7 additions & 1 deletion docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,18 @@ ready_output = "ready to accept connections"

### `ready_http`

HTTP endpoint URL to poll for readiness (2xx = ready).
HTTP endpoint URL to poll for readiness. By default, any 2xx response is ready.
Use the object form when specific non-2xx statuses also mean the service is up
(for example an authenticated endpoint returning 401).

```toml
[daemons.api]
run = "npm run server"
ready_http = "http://localhost:3000/health"

[daemons.private_api]
run = "npm run server"
ready_http = { url = "http://localhost:3000/health", status = [200, 401] }
```

### `ready_port`
Expand Down
4 changes: 2 additions & 2 deletions src/cli/config/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::daemon_id::DaemonId;
use crate::env;
use crate::pitchfork_toml::{
CronRetrigger, PitchforkToml, PitchforkTomlAuto, PitchforkTomlCron, PitchforkTomlDaemon,
PitchforkTomlHooks, PortBump, PortConfig, Retry, namespace_from_path,
PitchforkTomlHooks, PortBump, PortConfig, ReadyHttp, Retry, namespace_from_path,
};
use crate::settings::settings;
use indexmap::IndexMap;
Expand Down Expand Up @@ -246,7 +246,7 @@ impl Add {
retry,
ready_delay: self.ready_delay,
ready_output: self.ready_output.clone(),
ready_http: self.ready_http.clone(),
ready_http: self.ready_http.clone().map(ReadyHttp::new),
ready_port: self.ready_port,
ready_cmd: self.ready_cmd.clone(),
port: {
Expand Down
136 changes: 136 additions & 0 deletions src/config_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,142 @@ impl JsonSchema for CpuLimit {
}
}

// ---------------------------------------------------------------------------
// ReadyHttp
// ---------------------------------------------------------------------------

/// HTTP readiness check configuration.
///
/// Accepts two TOML forms:
/// ```toml
/// ready_http = "http://localhost:3000/health" # shorthand, any 2xx response
/// ready_http = { url = "http://localhost:3000/health", status = [200, 401] }
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ReadyHttp {
pub url: String,
/// Exact status codes that indicate readiness. Empty means any 2xx response.
pub status: Vec<u16>,
}

impl ReadyHttp {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
status: vec![],
}
}

pub fn accepts_status(&self, status: u16) -> bool {
if self.status.is_empty() {
(200..=299).contains(&status)
} else {
self.status.contains(&status)
}
}
}

impl std::fmt::Display for ReadyHttp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.status.is_empty() {
f.write_str(&self.url)
} else {
let status = self
.status
.iter()
.map(u16::to_string)
.collect::<Vec<_>>()
.join(", ");
write!(f, "{} (status: {status})", self.url)
}
}
}

#[derive(serde::Deserialize, serde::Serialize)]
#[doc(hidden)]
pub struct ReadyHttpRaw {
url: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
status: Vec<u16>,
}

impl StringOrStruct for ReadyHttp {
type Short = String;
type Raw = ReadyHttpRaw;

fn from_short(url: String) -> Self {
Self::new(url)
}

fn from_raw(raw: ReadyHttpRaw) -> std::result::Result<Self, String> {
for status in &raw.status {
if !(100..=599).contains(status) {
return Err(format!(
"ready_http status must be between 100 and 599: {status}"
));
}
}
Ok(Self {
url: raw.url,
status: raw.status,
})
}

fn is_shorthand(&self) -> bool {
self.status.is_empty()
}

fn to_short(&self) -> String {
self.url.clone()
}

fn to_raw(&self) -> ReadyHttpRaw {
ReadyHttpRaw {
url: self.url.clone(),
status: self.status.clone(),
}
}
}

impl Serialize for ReadyHttp {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.string_or_struct_serialize(s)
}
}

impl<'de> Deserialize<'de> for ReadyHttp {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
Self::string_or_struct_deserialize(d)
}
}

impl JsonSchema for ReadyHttp {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("ReadyHttp")
}

fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"description": "HTTP readiness check: a URL string accepting any 2xx response, or { url, status } object with exact accepted status codes",
"oneOf": [
{ "type": "string", "description": "HTTP URL to poll for readiness; any 2xx response is ready" },
{
"type": "object",
"properties": {
"url": { "type": "string", "description": "HTTP URL to poll for readiness" },
"status": {
"type": "array",
"description": "Exact HTTP status codes that indicate readiness. Omit to accept any 2xx response.",
"items": { "type": "integer", "minimum": 100, "maximum": 599 }
}
},
"required": ["url"]
}
]
})
}
}

// ---------------------------------------------------------------------------
// StopSignal
// ---------------------------------------------------------------------------
Expand Down
6 changes: 3 additions & 3 deletions src/daemon.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::daemon_id::DaemonId;
use crate::daemon_status::DaemonStatus;
use crate::pitchfork_toml::{
CpuLimit, CronRetrigger, Dir, MemoryLimit, PortConfig, Retry, StopConfig, WatchMode,
CpuLimit, CronRetrigger, Dir, MemoryLimit, PortConfig, ReadyHttp, Retry, StopConfig, WatchMode,
};
use indexmap::IndexMap;
use std::fmt::Display;
Expand Down Expand Up @@ -86,7 +86,7 @@ pub struct Daemon {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub ready_output: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub ready_http: Option<String>,
pub ready_http: Option<ReadyHttp>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub ready_port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none", default)]
Expand Down Expand Up @@ -159,7 +159,7 @@ pub struct RunOptions {
pub retry_count: u32,
pub ready_delay: Option<u64>,
pub ready_output: Option<String>,
pub ready_http: Option<String>,
pub ready_http: Option<ReadyHttp>,
pub ready_port: Option<u16>,
pub ready_cmd: Option<String>,
pub port: Option<PortConfig>,
Expand Down
12 changes: 8 additions & 4 deletions src/ipc/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::daemon_id::DaemonId;
use crate::deps::{compute_reverse_stop_order, resolve_dependencies};
use crate::ipc::client::IpcClient;
use crate::pitchfork_toml::{
PitchforkToml, PitchforkTomlDaemon, is_dot_config_pitchfork, is_global_config,
PitchforkToml, PitchforkTomlDaemon, ReadyHttp, is_dot_config_pitchfork, is_global_config,
};

use chrono::{DateTime, Local};
Expand Down Expand Up @@ -101,7 +101,11 @@ pub fn build_run_options(
run_opts.wait_ready = true;
run_opts.ready_delay = opts.delay.or(run_opts.ready_delay).or(Some(3));
run_opts.ready_output = opts.output.clone().or(run_opts.ready_output);
run_opts.ready_http = opts.http.clone().or(run_opts.ready_http);
run_opts.ready_http = opts
.http
.clone()
.map(ReadyHttp::new)
.or(run_opts.ready_http);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
run_opts.ready_port = opts.port.or(run_opts.ready_port);
run_opts.ready_cmd = opts.cmd.clone().or(run_opts.ready_cmd);
if let Some(ref expected) = opts.expected_port {
Expand Down Expand Up @@ -534,7 +538,7 @@ impl IpcClient {
let force = opts.force && is_explicitly_requested;
let delay = opts.delay;
let output = opts.output.clone();
let http = opts.http.clone();
let http = opts.http.clone().map(ReadyHttp::new);
let port = opts.port;
let ready_cmd = opts.cmd.clone();
let expected_port = opts.expected_port.clone();
Expand Down Expand Up @@ -713,7 +717,7 @@ impl IpcClient {
retry: opts.retry.unwrap_or_default(),
ready_delay: opts.delay.or(Some(3)),
ready_output: opts.output,
ready_http: opts.http,
ready_http: opts.http.map(ReadyHttp::new),
ready_port: opts.port,
ready_cmd: opts.cmd.clone(),
port: crate::config_types::PortConfig::from_parts(
Expand Down
8 changes: 4 additions & 4 deletions src/pitchfork_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::path::{Path, PathBuf};
// Re-export config value types so existing `use crate::pitchfork_toml::X` paths keep working.
pub use crate::config_types::{
CpuLimit, CronRetrigger, Dir, MemoryLimit, OnOutputHook, PitchforkTomlAuto, PitchforkTomlCron,
PitchforkTomlHooks, PortBump, PortConfig, Retry, StopConfig, StopSignal, WatchMode,
PitchforkTomlHooks, PortBump, PortConfig, ReadyHttp, Retry, StopConfig, StopSignal, WatchMode,
};

/// Raw slug entry as read from TOML (uses String for dir path).
Expand Down Expand Up @@ -75,7 +75,7 @@ struct PitchforkTomlDaemonRaw {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub ready_output: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub ready_http: Option<String>,
pub ready_http: Option<ReadyHttp>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub ready_port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none", default)]
Expand Down Expand Up @@ -1153,8 +1153,8 @@ pub struct PitchforkTomlDaemon {
pub ready_delay: Option<u64>,
/// Regex pattern to match in ANSI-stripped stdout/stderr to determine readiness
pub ready_output: Option<String>,
/// HTTP URL to poll for readiness (expects 2xx response)
pub ready_http: Option<String>,
/// HTTP URL to poll for readiness. Accepts any 2xx response by default, or configured statuses.
pub ready_http: Option<ReadyHttp>,
/// TCP port to check for readiness (connection success = ready)
#[schemars(range(min = 1, max = 65535))]
pub ready_port: Option<u16>,
Expand Down
6 changes: 3 additions & 3 deletions src/supervisor/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,9 +797,9 @@ impl Supervisor {
std::future::pending::<()>().await;
}
}, if !ready_notified && ready_http.is_some() => {
if let (Some(url), Some(client)) = (&ready_http, &http_client) {
match client.get(url).send().await {
Ok(response) if response.status().is_success() => {
if let (Some(http), Some(client)) = (&ready_http, &http_client) {
match client.get(&http.url).send().await {
Ok(response) if http.accepts_status(response.status().as_u16()) => {
info!("daemon {id} ready: HTTP check passed (status {})", response.status());
ready_notified = true;
let _ = log_appender.flush().await;
Expand Down
Loading
Loading