Skip to main content

Crate holdon

Crate holdon 

Source
Expand description

holdon logo

holdon

Wait for anything. Know why if it doesn't.

A next-gen "wait for service ready" CLI in Rust. One static binary, parallel by default, protocol-aware, with diagnostic failures that actually tell you what broke.

crates.io docs.rs CI MSRV 1.85 license

$ holdon postgres://db:5432 redis://cache:6379 https://api/health
✓ ready postgres://db:5432 · 27ms
✓ ready redis://cache:6379 · 14ms
✗ failed https://api/health · 5.0s · ▁▂▄▆█ · 510ms
├ dns ✓ 2ms
├ tcp ✓ 3ms
└ http ✗ status 503
hint: service may still be initializing
→ 2/3 ready · 5.1s

§Why holdon

Diagnostic stages, not “timed out”. Every probe is multi-stage (DNS, TCP, TLS, protocol). When a target fails you get the stage that broke and an operator-facing hint, not a stack trace.

Parallel by default. Pass a dozen targets in one command. They run concurrently. Sequential mode is opt-in via --sequential.

Protocol-aware probes for 15 schemes. TCP, HTTP, DNS, file, exec, log, Postgres, MySQL/MariaDB, Redis, MongoDB, RabbitMQ (AMQP), Kafka, Temporal, InfluxDB (v1/v2/v3), and gRPC Health/Check. Each probe speaks the real protocol instead of just opening a socket.

Type-safe URL DSL. mongodb://, kafka://, temporal://, etc. Query parameters validated at parse time. URL passwords and ?token= values redacted in every error path, in Display, in Debug, and in CLI parse errors.

One static binary. musl build is under 4 MB with default features, under 1.5 MB with no defaults. No runtime, no shell-out, no OpenSSL anywhere in the dependency tree.

Rustls everywhere. Postgres, MySQL, Redis, MongoDB, RabbitMQ, Kafka, Temporal, HTTP, and gRPC all share one TLS stack with bundled webpki roots. No native-tls.

Machine output. --output json emits a stable line-delimited schema (v: 1) ready for jq. POSIX-aligned exit codes (0, 2, 124, 126, 127, 130, 143).

§Install

The recommended path is cargo:

cargo install holdon

Pick a feature set based on which probes you need:

cargo install holdon --no-default-features --features http,postgres
cargo install holdon --features all-databases
cargo install holdon --features full

Skip the compile step with cargo binstall:

cargo binstall holdon

Homebrew (macOS, Linux):

brew install imjustprism/holdon/holdon

Scoop (Windows):

scoop bucket add holdon https://github.com/imjustprism/scoop-holdon
scoop install holdon

Prebuilt binaries for Linux (gnu/musl, x86_64 + aarch64), macOS (x86_64 + arm64), and Windows ship with every release:

curl -fsSL https://raw.githubusercontent.com/imjustprism/holdon/main/install.sh | sh

Or grab a tarball from GitHub Releases.

A multi-arch Docker image is published to the GitHub Container Registry:

docker pull ghcr.io/imjustprism/holdon
docker run --rm ghcr.io/imjustprism/holdon tcp://db:5432

Verify the install:

holdon --version

Minimum supported Rust version: 1.85.

§Quickstart

holdon :5432                              # wait for localhost:5432
holdon :5432 :6379 :3000                  # several ports in parallel
holdon :5432 -- npm run migrate           # exec a command once ready
holdon https://api.local/health -t 60s    # http with custom timeout
holdon postgres://user:pw@db/app          # postgres handshake
holdon exec:///usr/local/bin/check.sh     # custom readiness command

The argument after -- is the command to run once every target is ready. holdon execs it directly (no shell), so quoting and signals work the same as timeout(1) or kubectl exec.

§Protocols

SchemeWhat it checks
tcp://, :port, host:portDNS resolve, TCP connect
http://, https://TCP, TLS, HTTP request (-H, --method, --expect-body, --expect-body-regex, --expect-json, --no-follow-redirects, --ca-cert, --tls-min)
dns://Hostname resolves
file:///pathPath exists (?mode=absent inverse)
postgres://, postgresql://Connect + SELECT 1 (TLS by default)
mysql://, mariadb://Connect + SELECT 1 (TLS by default)
redis://, rediss://Connect + PING (rediss:// for TLS)
grpc://, grpcs://grpc.health.v1.Health/Check unary (optional /Service path)
influxdb://, influxdbs:///ping for v1, v2, v3. Optional ?expect-version=1|2|3 and ?token=... (Bearer/Token auth for v3 OSS)
mongodb://, mongodb+srv://Connect + admin ping command (SRV-aware)
amqp://, amqps://RabbitMQ AMQP connect, optional ?queue= / ?exchange= passive declare
kafka://, kafkas://Kafka broker Metadata fetch, optional ?topic= and ?expect-partitions=
temporal://, temporals://Temporal server gRPC Health/Check on WorkflowService
log:///path?match=...Wait for a substring or regex to appear in a local log file (last 1 MiB)
exec://program?arg=...External command, ready iff exit 0

§Feature flags

Defaults (http + json-output) cover most CI use cases. Database and message-broker probes are opt-in to keep the default binary small.

FeatureAdds
httpHTTP / HTTPS probes (rustls)
postgresPostgres probe via tokio-postgres + rustls
mysqlMySQL / MariaDB probe via mysql_async + rustls
redisRedis probe via redis crate + rustls
mongodbMongoDB probe via mongodb driver + rustls (SRV-aware)
rabbitmqRabbitMQ AMQP probe via lapin + rustls (optional queue/exchange check)
kafkaKafka Metadata probe via pure-Rust rskafka + rustls (optional topic/partition check)
temporalTemporal server gRPC Health/Check probe (depends on grpc)
influxdbInfluxDB /ping probe (depends on http)
grpcgRPC Health/Check probe via tonic + rustls
json-output--output json line-delimited events
all-databasespostgres + mysql + redis + mongodb
fullEverything above

§Config file

Pass --config holdon.toml, or drop holdon.toml / .holdon.toml next to where you run holdon and it’s auto-detected.

interval = "200ms"
timeout = "60s"
success_threshold = 2

targets = [
  "tcp://db:5432",
  "https://api.local/health",
]

Explicit CLI flags always win over the config file. See examples/holdon.toml.

§Output modes

  • Plain (default). Live spinner, colored status, sparklines on stderr. Auto-disabled in non-TTY environments and when NO_COLOR is set.
  • JSON (--output json). Line-delimited events on stdout, stable schema documented in docs/json-schema.md. Versioned (v: 1). Adding fields is non-breaking, removing or renaming is.
  • Quiet (-q). Only the exit code.

§Exit codes

CodeMeaning
0All targets ready
2CLI misuse or parse error
124Overall timeout elapsed (GNU timeout convention)
126Exec’d child not executable
127Exec’d child binary not found
130Interrupted by SIGINT (Ctrl-C)
143Interrupted by SIGTERM

Override the timeout exit code with --timeout-exit-code <N> when wrapping in Docker/Kubernetes lifecycle hooks that expect a specific code.

§Shell completions and man page

holdon --generate-completion bash          > /etc/bash_completion.d/holdon
holdon --generate-completion zsh           > ~/.zsh/completions/_holdon
holdon --generate-completion fish          > ~/.config/fish/completions/holdon.fish
holdon --generate-completion power-shell   | iex
holdon --generate-manpage                  > /usr/local/share/man/man1/holdon.1

Prebuilt completions for every shell plus the man page are attached to each release as holdon-completions-and-manpage.tar.gz.

§Library

holdon is also a Rust crate. The same probe engine is exposed through Runner and Target:

use std::time::Duration;
use holdon::{Runner, Target};
use holdon::runner::RunnerConfig;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let targets = vec![
        "postgres-host:5432".parse::<Target>()?,
        "redis-host:6379".parse::<Target>()?,
    ];
    let cfg = RunnerConfig::default().timeout(Duration::from_secs(30));
    let report = Runner::new(cfg).run(targets, None).await;
    report.assert_all_ready()?;
    Ok(())
}

See the examples directory and the API docs.

§Security

  • TLS is rustls only. No OpenSSL anywhere in the tree. cargo-deny blocks it.
  • Rustls everywhere. Every TLS-capable probe (HTTP, Postgres, MySQL, Redis, MongoDB, RabbitMQ, Kafka, Temporal, gRPC) uses the same ring-backed rustls stack with bundled webpki roots.
  • Password redaction. URL passwords are stripped in Display, Debug, and every error path. Same for ?token= query values on schemes that accept them.
  • Parse errors scrub secrets. CLI errors like “invalid target …” percent-decode query keys before matching, so ?to%6Bken=... cannot bypass the redaction.
  • HTTP redirect policy. Followed up to 5 hops. https → http downgrades refused.
  • --insecure is HTTP-only. Prints a stderr warning on every run. Do not use in production.
  • exec:// runs whatever you point it at. Treat target strings as code at the invocation site.
  • file:// and log:// use symlink_metadata. Symlinks are not followed into attacker-controlled paths.
  • No telemetry. No phone-home, no analytics, ever.

See SECURITY.md for the full threat model and disclosure instructions.

§Contributing

Bug reports, feature requests, and PRs are welcome.

  • Branch naming: feat/<short-name>, fix/<short-name>, docs/<short-name>, chore/<short-name>.
  • Run cargo fmt, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features before opening a PR.
  • New probes follow the src/checker/<name>.rs shape: a pub(super) async fn probe(...) returning Vec<Stage> plus a feature gate in Cargo.toml.

§Star History

Star History Chart

§Contributors

Contributors

§License

Dual MIT or Apache-2.0, at your option. Public Rust API for the holdon wait-for-readiness tool.

The CLI binary is the primary surface. This library exposes the same probe engine for programmatic use. Most callers want the Runner and Target pair: build a config, parse targets, drive the runner, inspect the resulting Report.

Re-exports§

pub use error::Error;
pub use error::Result;
pub use runner::Direction;
pub use runner::Event;
pub use runner::Report;
pub use runner::Runner;
pub use runner::RunnerConfig;
pub use runner::Schedule;
pub use runner::TargetReport;
pub use target::Hostname;
pub use target::LogMatcher;
pub use target::Target;
pub use util::parse_duration;

Modules§

diagnostic
Per-stage diagnostic types produced by every probe.
error
Error types returned by parsing and probing.
runner
Runner orchestration: scheduling, retries, reporting.
target
Target enum and URL parsing.
util
Utility helpers re-exported for downstream consumers.