kex docs Standard Library

Process

Processes, tasks and external commands.

Kex's concurrency is the BEAM's: lightweight processes that share nothing and communicate by message. There are three things here, and they answer different questions.

Task runs one piece of work somewhere else and gives you the answer back:

let a = Task.start do expensiveThing(1) end
let b = Task.start do expensiveThing(2) end
a.await + b.await

serving plus Process.spawn gives a piece of state its own process, with typed calls into it:

record RateLimiter do
  remaining : Integer
end

serving RateLimiter do
  slot allowed? -> Reply<Bool> do
    let allowed = @remaining > 0
    new.remaining = allowed then @remaining - 1 else @remaining
    return { new, reply: allowed }
  end
end

let api = Process.spawn(RateLimiter { remaining: 2 })
api.allowed?()   # => Ok(true)

Process.run and Process.stream run an external program.

Process.run("git", ["rev-parse", "HEAD"])

Backed by Kex.Intrinsic.Process and the BEAM runtime.

type Pid

An opaque BEAM process identifier.

Obtained from Process.self or Process.whereIs. Send it messages, link to it, monitor it, or ask whether it is still alive.

Methods

send

send(msg: X) -> Void

Sends msg to the process, unchanged, as a raw BEAM term.

Sending never blocks and never fails, even when the process is gone: that is the BEAM's model, not an oversight. Monitor the process when delivery matters.

Parameters
msg
the message to send
Examples
worker.send(:stop)
worker.send(("job", 42))

sendFrom

sendFrom(msg: X) -> Void

Sends the conventional Erlang sender-bearing pair {Process.self, msg}, so the receiver knows where to answer.

Parameters
msg
the message to send
Examples
server.sendFrom(:status)   # the server receives (senderPid, :status)

monitor

monitor : Reference

Starts monitoring this process, and returns the reference that identifies the monitor.

When the monitored process exits, the caller receives a message about it. Unlike link, a monitor is one-directional and does not propagate the exit. Pass the reference to demonitor to stop.

Returns the monitor reference

Examples
let ref = worker.monitor
ref.demonitor

alive?

alive? : Bool

Returns true when the process is still running.

Inherently a snapshot: the process may exit immediately after you ask. Monitor it when the answer has to stay true.

Returns true when the process is alive

Examples
Process.self.alive?   # => true

type Task<A>

A handle on work running in another process, with a result you can await.

Created by Task.start.

Methods

await

await : X

Waits for the task's result, for as long as it takes.

Returns the task's result

Examples
Task.start do 1 + 1 end.await   # => 2
await(timeout: Integer) -> X

Waits for the task's result, giving up after timeout milliseconds.

Answers None if the task has not finished in time. The task itself is not stopped.

Parameters
timeout
how long to wait, in milliseconds

Returns the task's result, or None on timeout

Examples
let t = Task.start do slowThing() end
t.await(1000)

type Reference

A monitor reference, returned by monitor and passed back to demonitor.

Methods

demonitor

demonitor : Void

Stops the monitor this reference identifies.

Examples
let ref = worker.monitor
ref.demonitor

type Process<A>

A typed process handle: like a Pid, but it remembers what kind of message the process accepts.

Methods

A spawned Process<X> is a typed process handle backed by the same runtime pid. It therefore supports the ordinary pid lifecycle operations without erasing its message type.

send

send(msg: X) -> Void

Sends msg to the process. Unlike Pid.send, the message type is checked.

Parameters
msg
the message to send

sendFrom

sendFrom(msg: X) -> Void

Sends the sender-bearing pair {Process.self, msg}, with the message type checked.

Parameters
msg
the message to send

monitor

monitor : Reference

Starts monitoring this process. See Pid.monitor.

Returns the monitor reference

alive?

alive? : Bool

Returns true when the process is still running.

Returns true when the process is alive

type ProcessExitReason

A process termination reason. This remains open because the BEAM permits any term as an exit reason; conventional values include :normal, :shutdown, and structured application errors.

Variants

  • Any

record Reply<A>

What a synchronous slot returns: an answer for the caller, and optionally a new state and a reason to stop.

reply answers the caller immediately. A slot may omit it only when it sends a deferred response with from.reply(...). new installs the next serving state; omitting it preserves the current state. stop terminates the server after applying the transition. Within a serving X block, the checker narrows new from Any? to X?.

slot allowed? -> Reply<Bool> do
  let allowed = @remaining > 0
  new.remaining = allowed then @remaining - 1 else @remaining
  return { new, reply: allowed }
end

slot stop -> Reply<Integer> = { stop: :normal, reply: @remaining }

Fields

reply
A
new
Any? optional
stop
ProcessExitReason? optional

type From<A>

The identity of a pending caller, for a slot that answers later rather than immediately. Hand it reply when the answer is ready.

Methods

reply

reply(value: X) -> Void

Answers the pending call this value identifies.

A slot that cannot answer immediately, because it is waiting on something else: omits reply from its transition and calls this later instead.

Parameters
value
the answer to send
Examples

Answering after the work is done

Task.start do
  from.reply(expensiveThing())
end

pid

pid : Pid

Returns the waiting caller's Pid.

Returns the caller

type CallError

Why a call into a server failed: it took too long, the process is gone, or it crashed handling the call.

Variants

  • Timeout
  • NoProcess
  • CallFailed

record Server<X>

A running server and the default timeout for calls into it.

Returned by Process.spawn. Every slot declared in the type's serving block becomes a method on it, answering a Result.

Fields

process
Process<X>
timeout
Integer optional

Methods

within

within(timeout: Integer) -> Server<X>

Returns the same server with a different default call timeout, in milliseconds.

The server is untouched: this is a new view of it, so one slow call can be given more room without changing anything for other callers.

Parameters
timeout
the call timeout in milliseconds

Returns a view of the server with that timeout

Examples

Giving one call longer to answer

api.within(30000).rebuildIndex()

monitor

monitor : Reference

Starts monitoring the server. See Pid.monitor.

Returns the monitor reference

alive?

alive? : Bool

Returns true when the server is still running.

Returns true when the server is alive

Examples
api.alive?   # => true

module Process

Spawning servers, running external commands, and the ambient process operations.

spawn

spawn(state: X) -> Server<X>

Starts a process running the serving implementation attached to state's type, and returns a handle on it.

The state you pass is the server's initial state. Every slot in the serving block becomes a method on the returned Server, and each answers a Result: a call can time out or find the process gone.

Parameters
state
the server's initial state

Returns a handle on the running server

Examples
let api = Process.spawn(RateLimiter { remaining: 2 })
api.allowed?()   # => Ok(true)
api.allowed?()   # => Ok(true)
api.allowed?()   # => Ok(false)
api.stop()

run

run(command: String, args: [String]) -> Result<ProcessResult, String>
run(command: String, args: [String]) -> Integer -> Result<ProcessResult, String>

Runs an executable with an argument vector and captures its output.

No shell is involved, so nothing is glob-expanded or word-split and arguments containing spaces need no quoting. Output is captured as UTF-8 strings.

A non-zero exit status is still Ok: the program ran and said something, which is information, not a failure to run it. Error means the child could not be started at all.

Parameters
command
the executable to run
args
its arguments, one per element

Returns the captured result, or why it could not start

Examples
match Process.run("echo", ["hello"]) do
  Ok(r)    => IO.printLine(r.stdout.trim)   # prints: hello
  Error(e) => IO.printError(e)
end

A non-zero status is still Ok

Process.run("false", [])   # => Ok(ProcessResult { exitCode: 1, ... })

A command that does not exist is an Error

Process.run("no-such-command", [])   # => Error("executable not found")

Reading a command's output as data

Process.run("git", ["rev-parse", "HEAD"])
  .map { |r| r.stdout.trim }
  .or("unknown")

stream

stream(command: String, args: [String]) -> Result<Integer, String>

Runs an executable with the CALLER's stdout and stderr, so its output appears as it is produced rather than in one block when it exits.

Answers the exit code; nothing is captured: that is the trade, and it is what a long-running child a person is watching needs (kexhq/kex#187).

run remains the one to use when the output is data to be READ.

Parameters
command
the executable to run
args
its arguments, one per element

Returns the exit code, or why it could not start

Examples

Watching a build as it runs

match Process.stream("make", ["-j8"]) do
  Ok(0)    => IO.printLine("build succeeded")
  Ok(code) => IO.printError("build failed with ${code}")
  Error(e) => IO.printError(e)
end

exec

exec(command: String, args: [String]) -> Integer

Runs an executable and returns just its exit code, discarding its output.

A command that could not be started answers 127, the shell's convention for "command not found". Use run when you need the output or want to tell a failed start from a failed run.

Parameters
command
the executable to run
args
its arguments, one per element

Returns the exit code, or 127 if it could not start

Examples
Process.exec("true", [])              # => 0
Process.exec("false", [])             # => 1
Process.exec("no-such-command", [])   # => 127

Testing for a tool's presence

let haveGit = Process.exec("git", ["--version"]) == 0

self

self : Pid

Returns the calling process's own Pid.

Returns this process's identifier

Examples

Telling another process where to answer

worker.send((Process.self, :ping))

exit

exit(pid: Pid, reason: X) -> Void

Sends an exit signal carrying reason to pid.

:normal is the ordinary shutdown reason; :kill cannot be trapped.

Parameters
pid
the process to signal
reason
the exit reason
Examples
Process.exit(worker, :shutdown)

register

register(pid: Pid, name: Atom) -> Void
register(pid: Process<X>, name: Atom) -> Void

Registers pid under the atom name, so it can be found by name rather than by passing the Pid around.

Parameters
pid
the process to register
name
the name to register it under
Examples
Process.register(Process.self, :main)

whereIs

whereIs(name: Atom) -> Pid?

Returns the Pid registered under name, or None when nothing is.

Parameters
name
the registered name

Returns the process, or None

Examples
Process.whereIs(:main)        # => Just(pid)
Process.whereIs(:not_there)   # => None

Sending to a named process if it is there

Process.whereIs(:logger).map { |pid| pid.send(message) }

type Shared<X>

A named, typed slot of state that every process can read without asking another process for it.

State kept in a serving process is copied into the caller on every read, which is costly for a large value read by every request. A Shared value is stored once for the whole node (on the BEAM, in persistent_term): reading it copies nothing, however large it is.

The price is on the other side: replacing a value makes the runtime scan every process for references to the old one. So it suits state that is read constantly and written rarely, such as configuration or a loaded site, and not a counter.

Get a handle with Process.Shared.named. The slot is identified by its name alone, so two handles with the same name are the same slot; the type parameter is what a handle promises to store and read back, so give every handle on one name the same one.

Examples

Loading once, reading from every request handler

let site : Process.Shared<Site> = Process.Shared.named("site")
site.put(loadSite(root))
...
let current = site.get.or(emptySite)

Methods

put

put(value: X) -> Void

Stores value, replacing whatever the slot held.

Expensive: see Process.Shared.

Parameters
value
the value to store

get

get : X?

The stored value, or None when nothing has been stored yet.

Cheap, whatever the value's size.

Returns the stored value

delete

delete : Bool

Empties the slot.

Returns true when it held a value

module Process.Shared

named

named(name: String) -> Process.Shared<X>

The handle on the slot called name. Nothing is stored or read until put or get.

Parameters
name
the slot's name

Returns a handle on the slot

record ProcessResult

What an external command left behind: its exit status and its output.

Returned inside Ok by Process.run, whatever the exit status.

Fields

exitCode
Integer
stdout
String
stderr
String

module Task

Running work in another process and collecting the answer.

sleep

sleep(duration: Duration) -> Void

Suspends for an elapsed duration. Negative durations are treated as zero.

start

start(f: Block<X>) -> Task

Runs f in a new process and returns a handle on its result.

The block starts immediately, so starting several tasks and awaiting them afterwards is what makes them run at the same time.

Parameters
f
the work to run

Returns a handle on the result

Examples
let t = Task.start do 1 + 1 end
t.await   # => 2

Two pieces of work at once

let a = Task.start do slowThing("a") end
let b = Task.start do slowThing("b") end
(a.await, b.await)

awaitAll

awaitAll(tasks: [Task]) -> [X]

Waits for every task in tasks and returns their results, in the order the tasks were given.

Each result comes back wrapped in a Result, so one task failing does not cost you the others' answers. That differs from Task.await on a single task, which hands back the value itself.

Parameters
tasks
the tasks to wait for

Returns their results, in order

Examples
let tasks = [Task.start do 1 end, Task.start do 2 end]
Task.awaitAll(tasks)   # => [Ok(1), Ok(2)]

Fanning work out over a list

let results = Task.awaitAll(paths.map { |p| Task.start do process(p) end })

function worker

worker(block: Block<Pid>) -> (Atom, Block<Pid>)

Wraps a spawn block into a worker spec for Supervisor.start.

Parameters
block
the block that spawns the worker

Returns the worker spec