;;; Graph model compose example
;;;
;;; The dependency graph is declared as first-class values before any
;;; execution begins. `run` topologically sorts and executes the graph;
;;; pass :parallel to run independent nodes concurrently within each tier.
;;;
;;; Dependency graph:
;;;
;;; db ──→ db-url ──→ migrate ──→ app
;;; db ──→ db-url ─────────────→ app (DATABASE_URL env)
;;; cache ──→ cache-url ─────────→ app
;;;
;;; Usage:
;;; sudo -E pelagos compose up -f compose.reml -p imperative-demo
;; ── Service declarations ──────────────────────────────────────────────────
(define-service svc-db "db"
:image "postgres:16"
:network "app-net"
:env ("POSTGRES_PASSWORD" . "secret")
("POSTGRES_DB" . "appdb")
("POSTGRES_USER" . "app"))
(define-service svc-cache "cache"
:image "redis:7-alpine"
:network "app-net")
(define-service svc-migrate "migrate"
:image "alpine:latest"
:network "app-net"
:command "/bin/sh" "-c"
"echo \"migrating ${DATABASE_URL}\"; exit 0")
(define-service svc-app "app"
:image "alpine:latest"
:network "app-net"
:command "/bin/sh" "-c"
"echo \"db=${DATABASE_URL} cache=${CACHE_URL}\"; sleep 5; echo done")
;; ── Declare the graph — nothing executes yet ─────────────────────────────
;; Independent services: no dependencies, eligible to start in parallel
(define-nodes
(db svc-db)
(cache svc-cache))
;; Compute connection strings once each container is up
(define-then db-url db (h)
(format "postgres://app:secret@~a/appdb" (container-ip h)))
(define-then cache-url cache (h)
(format "redis://~a:6379" (container-ip h)))
;; Migration: needs db-url injected into its environment
(define migrate (start svc-migrate
:needs (list db-url)
:env (lambda (url) `(("DATABASE_URL" . ,url)))))
;; Application: waits for migration to complete, then starts with both URLs
(define app (start svc-app
:needs (list migrate db-url cache-url)
:env (lambda (_ db-url cache-url)
`(("DATABASE_URL" . ,db-url)
("CACHE_URL" . ,cache-url)))))
;; ── Execute and bind ──────────────────────────────────────────────────────
;; Only bind what you need. Containers (app) resolve to handles; transforms
;; (db-url, cache-url) resolve to their computed values — both work the same way.
;; db and cache execute as transitive deps and stop automatically via the cascade;
;; add e.g. (db-handle db) here to inspect or manually stop them.
(define-run :parallel
(app-handle app)
(db-url db-url)
(cache-url cache-url))
(logf "db-url: ~a" db-url)
(logf "cache-url: ~a" cache-url)
;; cascade stops migrate, cache, db automatically in reverse startup order
(with-cleanup (lambda (result)
(if (ok? result)
(logf "app exited cleanly (code ~a)" (ok-value result))
(logf "app failed: ~a" (err-reason result))))
(container-wait app-handle))
(log "Done")