ExBashkit.Pool (ExBashkit v0.1.4)

Copy Markdown View Source

An optional bounded-concurrency gate for running sandboxed scripts under load.

Every ExBashkit.exec/1 and ExBashkit.Session.exec/2 call blocks a dirty scheduler thread for the whole duration of the script (the native interpreter runs synchronously inside a dirty NIF). The dirty pool is bounded — roughly one thread per scheduler — so if you let untrusted, possibly-slow scripts run with unbounded concurrency, enough of them in flight at once can occupy every dirty thread and starve all native work on the node until they time out.

This pool caps how many run concurrently. It hands out a fixed number of permits; callers over that limit wait in a bounded queue, and callers past the queue are rejected with {:error, :overloaded} so you can shed load instead of piling work onto exhausted schedulers. It is opt-in: add it to your supervision tree and route untrusted execs through it.

children = [
  {ExBashkit.Pool, size: 8, max_queue: 100}
]

# Anywhere you'd run an untrusted script:
ExBashkit.Pool.run(fn -> ExBashkit.Session.exec(session, script) end)
#=> {:ok, %ExBashkit.Result{}} | {:error, :overloaded}

size and max_queue may also be set in config and are overridden by the options you pass to the child spec:

config :ex_bashkit, pool_size: 8, pool_max_queue: 100

size defaults to System.schedulers_online/0 and max_queue to 50. Pick a size no larger than your dirty-scheduler budget — keeping it below the scheduler count leaves headroom for other native work.

Notes

  • The script runs in the calling process, not in the pool — the pool only gates entry, so throughput still scales across size schedulers.
  • A permit is released when the work returns, raises, or the caller dies: each holder is monitored, so a crashed or killed caller can never leak a slot.
  • run/1,2 blocks (with no call timeout) while queued; it returns only once a permit is granted or the queue is full. A script's own :timeout_ms still bounds how long any one permit is held.
  • A process must not call run/1,2 reentrantly on a pool it already holds a permit on — that would deadlock waiting for a slot only it could free, so it raises instead. Nested work belongs on a different pool.
  • The pool is a singleton registered under :name (default ExBashkit.Pool). To run several isolated pools, give each a distinct :name.

Summary

Functions

Returns a specification to start this module under a supervisor.

Convenience wrapper: run(pool, fn -> ExBashkit.exec(script) end).

Run fun (a 0-arity function) under a pool permit, returning its value.

Convenience wrapper: run(pool, fn -> ExBashkit.Session.exec(session, script) end).

Start the pool. Options

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

exec(pool \\ __MODULE__, script)

Convenience wrapper: run(pool, fn -> ExBashkit.exec(script) end).

run(fun)

Run fun (a 0-arity function) under a pool permit, returning its value.

Blocks while waiting for a permit if the pool is at capacity; returns {:error, :overloaded} immediately if the wait queue is also full. The permit is released when fun returns or raises (and automatically if the caller dies).

run(pool, fun)

session_exec(pool \\ __MODULE__, session, script)

Convenience wrapper: run(pool, fn -> ExBashkit.Session.exec(session, script) end).

start_link(opts \\ [])

Start the pool. Options:

  • :name - the registered name (default ExBashkit.Pool).
  • :size - max concurrent permits (default: config :ex_bashkit, :pool_size, else System.schedulers_online/0).
  • :max_queue - max callers that may wait for a permit before new callers are rejected with {:error, :overloaded} (default: config :ex_bashkit, :pool_max_queue, else 50).