Skip to content

yerd-services

yerd-services owns Yerd's local database and cache services: installing prebuilt service binaries, supervising one instance per engine, reporting their live state, and performing SQL database administration (create / drop / list / backup / restore). It is the services counterpart to yerd-php and is structured the same way - every decision is pure and unit-testable; every byte of I/O sits behind the yerd-supervise trait seams.

The four engines it models:

ServiceidDisplay nameKindDefault portServer binary
RedisredisRedis (Valkey)Cache6379valkey-server
MySQLmysqlMySQLDatabase3306mysqld
MariaDBmariadbMariaDBDatabase3306mariadbd
PostgreSQLpostgresPostgreSQLDatabase5432postgres

Redis is served by Valkey

The "Redis" slot is filled by Valkey - the BSD-licensed fork - because Redis 7.4+ is SSPL/RSALv2 and not cleanly redistributable. It stays wire-compatible, so clients are unaffected. The server binary is valkey-server; the user-facing display name is Redis (Valkey).

Crate metadata

description: Local database / cache service supervision and version management for Yerd. #![forbid(unsafe_code)]. Internal deps: yerd-platform (PlatformDirs, PortBinder), yerd-supervise (the trait seams + state machine). External: thiserror, async-trait, serde, serde_json, tokio.

See also the Crates overview, yerd-supervise, and the user-facing Services guide.

Module map

text
src/
├── lib.rs            # re-exports + a compile-time Send+'static guard
├── service.rs        # Service / ServiceKind - pure per-engine metadata
├── database.rs       # pure SQL-admin logic (name validation, quoting, argv)
├── config_render.rs  # pure config rendering (redis.conf / my.cnf / postgresql.conf)
├── release.rs        # pure artifact resolution from the hosted listing
├── version.rs        # version labels + on-disk path layout; discover_installed
├── health.rs         # readiness probes (Redis PING, MySQL/Postgres handshake)
├── manager.rs        # ServiceManager - the I/O driver that runs the state machine
└── error.rs          # ServiceError

Pure modules: service, database, config_render, release, plus the path builders in version and error. I/O lives in manager (spawning, fs, ports), health (sockets), and version::discover_installed.

Browse the source on GitHub.

service.rs - pure engine metadata

Service (Redis, MySql, MariaDb, Postgres) and ServiceKind (Cache, Database) are compile-time facts about each engine - no I/O. Service::ALL is the canonical 4-element iteration order. Beyond the table at the top of this page, each Service exposes:

MethodRedisMySqlMariaDbPostgres
client_binary()Nonemysqlmariadbpsql
dump_binary()Nonemysqldumpmariadb-dumppg_dump
needs_init()falsetruetruetrue
init_binary()Nonemysqld --initialize-insecuremariadb-install-dbinitdb
kind()CacheDatabaseDatabaseDatabase
datadir_pinned_to_major()falsefalsefalsetrue

MySQL and MariaDB share port 3306, so only one can run on it at a time (the config layer allows a per-instance override). PostgreSQL pins its data directory to its major version (data-<major>) and refuses to start against a datadir written by a different major.

database.rs - the SQL-admin security boundary (pure)

All database administration logic is pure and constructs SQL that cannot be injected. The daemon's db_admin edge calls these functions and passes the result to the engine's client as a single argv element - never through a shell.

  • validate_db_name(name) - strict allowlist: non-empty, ≤ 63 chars (the lowest engine cap), first char an ASCII letter or _, the rest [A-Za-z0-9_]. Returns DbNameError (Empty, TooLong, BadStart, BadChar(char)). This makes injection impossible by construction.
  • is_system_database(service, name) - case-insensitive guard. MySQL/MariaDB: information_schema, performance_schema, mysql, sys; Postgres: postgres, template0, template1. System databases cannot be dropped or restored over.
  • quote_ident(service, name) - backticks for MySQL/MariaDB, double-quotes for Postgres (each doubling the quote char).
  • create_sql / drop_sql / list_sql - per-engine statements. Postgres drop uses DROP DATABASE <ident> WITH (FORCE); (PG13+); Postgres list queries pg_database WHERE datistemplate = false; MySQL/MariaDB use SHOW DATABASES;.
  • client_args / dump_args / restore_args - build the argv for the interactive client / dump tool. MySQL & MariaDB connect over the local Unix socket as a passwordless root; Postgres connects over TCP loopback as postgres. Restore reuses the client binary (replaying SQL on stdin), not the dump binary.
  • parse_db_list(service, stdout) - trims, drops empties, filters system DBs, sorts, and dedups the client's output.

config_render.rs - pure config rendering

One renderer per engine, all returning text the caller writes to disk (no I/O here):

  • render_redis_conf - loopback bind, protected-mode, daemonize no, no password (local-dev posture).
  • render_my_cnf - one [mysqld] renderer for both MySQL and MariaDB: bind 127.0.0.1, skip-name-resolve, a Unix socket, and a pid-file.
  • render_postgresql_conf - listen_addresses = '127.0.0.1' and unix_socket_directories = '' (Postgres uses TCP loopback only; the macOS sun_path limit rules out a socket here), with hba/ident pinned to the datadir.

release.rs - artifact resolution (pure)

Yerd hosts its own multi-platform service distribution (the forjedio/yerd-services build matrix) with a services.json listing (LISTING_SCHEMA = 1). SERVICES_BASE_URL points at that GitHub release. The pure functions here resolve a (service, version, os, arch) request against a fetched listing body:

FunctionPurpose
resolve_from_listing(...)Finds the build for an exact version, returning an Artifact { service, version, url }. Errors VersionUnavailable / ListingParse / UnsupportedListingSchema.
available_versions(...)Every version published for the platform (infallible; empty on parse error). Feeds the GUI dropdown and yerd service available.
artifact_url / listing_url / platform_tokenURL construction.
current_os_arch()Resolves the host (Os, Arch); errors on Windows / 32-bit.

The daemon performs the actual HTTPS download (via the Downloader seam) and hands the bytes back; integrity rests on HTTPS to the host (there is no separate checksum sidecar for service builds).

version.rs - layout & discovery

ServiceVersion is an opaque, validated version label (services don't share PHP's major.minor shape). The on-disk layout under PlatformDirs:

text
{data}/services/<id>/<version>/bin/<server_binary>   # the install
{data}/services/<id>/<version>/data        (or data-<major> for Postgres)
{state}/services/<id>/<id>.conf            # rendered config
{state}/services/<id>/<id>.log             # captured stdout/stderr
{runtime}/services/<id>/<id>.sock          # Unix socket (MySQL/MariaDB)

discover_installed(&PlatformDirs) scans for version directories that contain a real server binary; the daemon calls it at startup.

health.rs - readiness probes

ReadinessProbe (service-aware) and ServiceProbes (the production dispatcher) implement yerd-supervise's address-only HealthProbe. A bare TCP accept is not enough during datadir init, so each probe requires a real protocol response:

  • RedisProbe - sends PING, expects +PONG.
  • MySqlProbe - reads the initial handshake packet (first byte 0x0a greeting or 0xff ERR). MariaDB reuses this probe (same wire protocol).
  • PostgresProbe - sends a startup message, accepts an R (auth) or E (error) reply tag.

manager.rs - the ServiceManager driver

ServiceManager<S, C, P> is generic over the ProcessSpawner, Clock, and ReadinessProbe seams, and always drives the state machine under SupervisorPolicy::database(). It holds one Instance per Service in a BTreeMap (deterministic shutdown order).

MethodBehaviour
ensure(service, version, port)Idempotent. Fast path returns the cached Listen if already running; otherwise runs one-time datadir init (staging dir + atomic rename), pre-flights the port via PortBinder (→ PortInUse), renders + writes the config, and drives the supervisor to Running.
restart / stopDrive the supervisor through a stop (then re-ensure for restart).
shutdown()Stops every instance in BTreeMap order.
snapshots()Read-only ServiceSnapshot { service, version, state, pid, listen } per instance.

ServiceRunState is Running or Failed (the daemon fills in Stopped for an engine with no live instance). Per-engine specifics live in build_cmd (Redis valkey-server <config>; MySQL/MariaDB --defaults-file=<config>; Postgres -D <datadir> -c config_file=<config>) and stop_protocol (Postgres → MasterInterrupt, all others → GroupTerm).

How the daemon wires it in

yerdd instantiates ServiceManager<TokioProcessSpawner, SystemClock, ServiceProbes> and exposes the crate over IPC:

  • bin/yerdd/src/services.rs handles ListServices, AvailableServices (downloads the listing on demand), InstallService (download + unpack, then start), ChangeServiceVersion (install → restart → remove old, keeping the datadir), UninstallService (optional --purge deletes data), StartService / StopService (toggle the status-only enabled flag; boot auto-start keys on installed versions, not this flag), RestartService, SetServicePort, and ServiceLogs. The per-service status reports supports_databases from ServiceKind.
  • bin/yerdd/src/db_admin.rs is the I/O edge for database administration. It requires a running SQL engine with a client binary present, passes the pure SQL as a single argv element (no shell), streams BackupDatabase to a temp sibling and atomically renames it (never truncating the target), and streams RestoreDatabase into the client's stdin. Engine errors are classified into typed ErrorCodes (AlreadyExists / NotFound / Internal).

See IPC Protocol for the full service/database message set.

Error model

ServiceError (#[non_exhaustive], not Clone + Eq - it wraps io::Error and PlatformError) pins the failure surface: Unsupported, VersionNotInstalled, DiscoveryIo, Init, Spawn, ConfigWrite, HealthCheckTimedOut, PermanentFailure, PortInUse, Bind, Kill, VersionUnavailable, UnsupportedPlatform, ListingParse, UnsupportedListingSchema, Download(DownloadError), and Extract.

Engine availability is a distribution question, not a code one

All four engines are implemented end-to-end - supervision, init, config rendering, health probing, and (for the SQL engines) database administration. Whether a given engine/version installs depends only on whether a prebuilt build is published in the hosted services.json listing for your platform. An unpublished build surfaces as VersionUnavailable rather than a missing feature.

A Forjed project. Released under the MIT License.