Skip to content

About

Run many times, compile once.

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Latest commit

 

History

120 Commits

Folders and files

Repository files navigation

DaemonicCabal.jl

Run a script many times, compile it once.

Quickstart

julia --project=temp -e 'using Pkg; Pkg.add(url="https://github.com/tecosaur/DaemonicCabal.jl.git"); using DaemonicCabal; DaemonicCabal.install()'

Once installed, DaemonicCabal.jl provides juliaclient, which serves as a drop-in replacement for a subset of julia, with a few extra tricks.

~$ juliaclient --help

    juliaclient [switches] -- [programfile] [args...]

Switches (a '*' marks the default value, if applicable):

 -v, --version              Display version information
 -h, --help                 Print this message
 -P, --project[=<dir>|@.]    Set <dir> as the home project/environment
 -e, --eval <expr>          Evaluate <expr>
 -E, --print <expr>         Evaluate <expr> and display the result
 -L, --load <file>          Load <file> immediately on all processors
 -i                         Interactive mode; REPL runs and `isinteractive()` is true
 -t, --threads <N|auto>[,<M|auto>]  Launch N threads (and M interactive threads)
 -q, --quiet                Quiet startup: no banner, suppress REPL warnings
 --banner={yes|no|auto*}    Enable or disable startup banner
 --color={yes|no|auto*}     Enable or disable color text
 --history-file={yes*|no}   Load or save history

Client-specific switches:

 -a, --address <addr>       Connect to conductor at <addr> instead of default
 --session[=<label>]        Reuse worker state in Main module. With a label,
                            multiple clients can share the same session.
 --sync                     Attach to shared REPL (requires --session=<label>)
 --revise[=yes|no*]         Enable or disable Revise.jl integration
 --restart                  Kill workers for the project and exit
 --sandbox                  Run in an isolated sandbox (Linux only)
 --status[=live|json]       Show the state of the workers, optionally in json

Daemon management (systemd):

 systemctl --user {start | stop | restart | status} julia-daemon

Supported platforms: Linux, macOS, FreeBSD, and OpenBSD.

Sessions

By default, each juliaclient invocation runs in a fresh module. Use --session to persist state in Main across invocations:

~$ juliaclient --session -e 'x = 42'
~$ juliaclient --session -e 'println(x)'  # prints 42

Without a label, session affinity is based on the parent process (shell). With a label, multiple clients share the same session:

# Terminal A
~$ juliaclient --session=shared -e 'data = [1,2,3]'

# Terminal B (different shell, same label)
~$ juliaclient --session=shared -e 'push!(data, 4); data'  # [1, 2, 3, 4]

Labeled sessions bypass the per-worker client limit, and workers holding idle labelled sessions persist for longer, their minimum lifetime is the geometric mean of MIN_TTL and MAX_TTL.

Collaborative sessions (--sync)

Adding --sync upgrades a labeled session to a shared terminal — multiple terminals attach to the same live REPL.

# Terminal A: start a shared REPL
~$ juliaclient --sync --session=work

# Terminal B: attach to the same REPL
~$ juliaclient --sync --session=work

# Terminal C: inject code (output appears in both REPLs)
~$ juliaclient --sync --session=work -e 'x = 42'

Interactive clients share a merged stdin and broadcast stdout/stderr. Non-interactive clients (-e, -E, programfile) run in the shared Main with output broadcast to all attached terminals. Ctrl-D detaches without killing the session; the REPL exits when the last interactive client disconnects.

Sandboxing

Sandboxed workers run inside Linux user namespaces (no root required) with a minimal root filesystem, providing isolation without containers.

Local sandboxing (--sandbox)

Pass --sandbox to run in an isolated environment with write access only to the working directory:

~$ cd ~/myproject
~$ juliaclient --sandbox -e 'println(pwd()); touch("ok")'  # works
~$ juliaclient --sandbox -e 'rm("/etc/passwd")'             # permission denied

The sandbox gives read-write access to:

  • The project directory when the working directory is inside it (the project mount subsumes the working directory)
  • Just the working directory otherwise (the project is mounted read-only)

Named environments (@v1.11 etc.) are not mounted read-write.

Remote sandboxing

When listening on TCP (JULIA_DAEMON_SERVER=tcp://...), non-loopback connections are automatically sandboxed. Each remote client gets a fresh, isolated worker with a read-only view of the host depot and no access to the host home directory. Optional cgroup limits can restrict memory and CPU per sandbox.

Local (loopback) TCP clients are not sandboxed unless --sandbox is passed. Remote clients on non-Linux platforms are rejected.

Configuration

Environment variables read at daemon startup:

VariableDefaultDescription
JULIA_DAEMON_RUNTIME(platform-specific)Runtime directory for sockets and PID file
JULIA_DAEMON_SERVER<runtime_dir>/conductor.sockSocket path or tcp://host:port for TCP mode
JULIA_DAEMON_BIND(from server addr)TCP bind address override (e.g. 0.0.0.0)
JULIA_DAEMON_PORTSunsetWorker port range in TCP mode (e.g. 10000-10100)
JULIA_DAEMON_WORKER_EXECUTABLEjuliaJulia binary for workers
JULIA_DAEMON_WORKER_ARGS--startup-file=noArguments passed to worker processes (split on whitespace)
JULIA_DAEMON_WORKER_PROJECT(required)Project for the worker itself (not client projects)
JULIA_DAEMON_WORKER_MAXCLIENTS1Max clients per worker (0 = unlimited)
JULIA_DAEMON_MAX_TTL7200Idle worker deadline in seconds; always culled past this (2h). Supersedes JULIA_DAEMON_WORKER_TTL, falling back to it when unset
JULIA_DAEMON_MIN_TTL120Protected floor: idle workers younger than this are never evicted under memory pressure
JULIA_DAEMON_LABEL_TTL90Session label retention after last client disconnects (seconds)
JULIA_DAEMON_PING_INTERVAL30Health check interval (seconds)
JULIA_DAEMON_PING_TIMEOUT5Ping timeout before killing unresponsive worker (seconds)
JULIA_DAEMON_REVISEnoEnable Revise.jl by default (also settable with --revise)

Memory-pressure eviction:

VariableDefaultDescription
JULIA_DAEMON_MEMORY_PRESSURE1Shed idle workers under host memory pressure (0 to disable)
JULIA_DAEMON_PSI_THRESHOLD10.0PSI some avg10 % that counts as pressure (when PSI is the active source)
JULIA_DAEMON_MEMFREE_LOW10%Enter pressure below this free memory; % of total or absolute (2G)
JULIA_DAEMON_MEMFREE_HIGH15%Leave pressure above this free memory (hysteresis; must exceed MEMFREE_LOW)

Sandbox configuration (Linux only, applies to both --sandbox and remote clients):

VariableDefaultDescription
JULIA_DAEMON_SANDBOX_REMOTE_CLIENTS1Sandbox remote (non-loopback) TCP clients (0 to disable)
JULIA_DAEMON_SANDBOX_EMPTY_ENVIRONMENT1Mask ~/.julia/environments in sandbox (0 to expose host envs)
JULIA_DAEMON_SANDBOX_MAX_MEMORYunsetPer-sandbox cgroup memory limit (e.g. 4G, 512M)
JULIA_DAEMON_SANDBOX_MAX_CPUunsetPer-sandbox cgroup CPU limit as percentage (e.g. 200 = 2 cores)
JULIA_DAEMON_SANDBOX_SESSION_BYPASS0Allow remote --session=<label> to join local unsandboxed workers

Runtime directory defaults: Linux /run/user/$UID/julia-daemon, macOS ~/Library/Application Support/julia-daemon, BSD /tmp/julia-daemon-$UID.

The client reads JULIA_DAEMON_SERVER and JULIA_DAEMON_RUNTIME to locate the conductor socket.

Memory pressure

Idle workers are large (a warmed Julia process is hundreds of MB), so on a busy or shared host the conductor treats them as a memory-pressure-reactive cache: when the host runs low on memory it sheds the least valuable idle workers, leaving them alone while RAM is plentiful. This is on by default; set JULIA_DAEMON_MEMORY_PRESSURE=0 for TTL-only behaviour.

A worker is eligible for eviction only once it has been idle past MIN_TTL (a recently-used worker is never culled), and the conductor evicts the lowest-value-per-byte first — a worker kept warm by frequent or long-running use ranks above a cold one, and a large worker yields before a small one of equal warmth. Eviction never touches a worker with a connected client. An evicted worker simply cold-starts on its next invocation, exactly as it would after its idle MAX_TTL deadline.

Pressure is read from the kernel’s PSI stall information where available, else from the free-memory level (MemAvailable). On FreeBSD only the pressure trigger is implemented (workers are ranked without a size term); OpenBSD is TTL-only. juliaclient --status shows each idle worker’s eviction warmth while pressure handling is active.

Architecture

┌─────────────────┐      ┌──────────────────┐      ┌──────────────────────┐
│  Client (Zig)   │─────▶│  Conductor (Zig) │─────▶│  Worker Pool (Julia) │
└────────┬────────┘      └──────────────────┘      └────────┬─────────────┘
         │                                                  │
         └────────────────── stdio ─────────────────────────┘
  • Client: Zig binary users invoke. Connects to the conductor, then proxies stdio/signals directly with the assigned worker.
  • Conductor: Zig daemon managing the worker pool. Spawns workers per-project and routes client requests.
  • Worker: Julia process serving client sessions for a single project.

Platform-specific I/O: io_uring on Linux, kqueue on macOS/BSD.

Caveats

  • Workers are per-project, so different projects incur separate startup costs.

About

Run many times, compile once.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages