Rebus.UnixFD (rebus v0.4.0)
View SourceHandles for Unix file descriptors.
Rebus hands every descriptor it receives to your code as a handle: in the
unix_fds of a Rebus.call/3 reply, and in the unix_fds of a
Rebus.Service.Call. A handle owns one open descriptor and closes it
exactly once: when its owner calls close/1, when its owning process
exits, or when the last reference to it is garbage collected, whichever
comes first. A handle never exposes its descriptor number except inside
with_fd/2, and once closed it invalidates the number, so a stale handle
cannot reach an unrelated descriptor that the operating system has since
given the same number.
Ownership
Ownership follows the controlling-process model of ports and sockets. Each
handle has exactly one owning process. When the owner exits, for any
reason including :kill, the handle closes at once, even if other
processes still hold it, and reads closed for them too. A background
thread then closes the descriptor, so a close that blocks cannot stall a
scheduler; the same happens when a handle is garbage collected.
Any process holding a handle may reach the descriptor with with_fd/2,
pass the handle in a message's fds:, or duplicate it with dup/1. Only
the owner can close it with close/1 or pass ownership on with
controlling_process/2. Each of the first three gives the holder, or the
peer it sends to, a copy of the open file that outlives the handle's owner,
so share a handle only with processes you trust with the file. owner/1
and open?/1 tell who owns a handle and whether it is still open:
{:ok, %Rebus.Message{unix_fds: [handle]}} = Rebus.call(conn, message)
{:ok, pid} = Rebus.UnixFD.owner(handle) # pid == self()
:ok = Rebus.UnixFD.controlling_process(handle, worker)
true = Rebus.UnixFD.open?(handle)Using the descriptor
with_fd/2 lends the raw number to a function and keeps the descriptor
open until the function returns, even if the handle is closed meanwhile.
Hand the number to something that takes its own copy, such as
:socket.open/2, or open /dev/fd/N:
{:ok, {:ok, sock}} =
Rebus.UnixFD.with_fd(handle, fn fd ->
:socket.open(fd, %{domain: :local, type: :stream})
end)
:ok = Rebus.UnixFD.close(handle)Sending
A handle can be sent in the fds: of Rebus.Message.new/2 and the reply
functions of Rebus.Service, by its owner or any other holder. Sending
neither closes nor transfers it. The connection keeps the descriptor open
until the kernel has taken it, even if the handle is closed or its owner
exits meanwhile, and refuses one that is already closed with
{:error, :unix_fd_closed}.
Distribution and platforms
A handle is local to the node that created it. One that has crossed to
another node answers {:error, :not_local} to every operation there; back
on its own node, it works again.
Handles need a small native library, which Rebus builds on Linux and macOS
only when the application enables Unix fd support with
config :rebus, unix_fds: true (see
Enabling descriptor passing).
Without it every function returns {:error, :unsupported}; supported?/0
tells which applies.
See Unix file descriptor passing for the full model.
Summary
Types
A raw descriptor number. Rebus borrows a raw descriptor that it sends: it never duplicates or closes it.
A handle that owns one Unix file descriptor.
Functions
Closes a handle's descriptor.
Makes pid the owner of a handle.
Duplicates a handle's descriptor into a new handle owned by the caller.
Returns whether a handle is open.
Returns the process that owns a handle, or {:error, :closed} once it is
closed.
Returns whether this build of Rebus supports handles, and so can pass descriptors.
Calls fun with the handle's raw descriptor number.
Types
@type error() :: :not_local | :unsupported
@type raw() :: non_neg_integer()
A raw descriptor number. Rebus borrows a raw descriptor that it sends: it never duplicates or closes it.
@opaque t()
A handle that owns one Unix file descriptor.
Functions
Closes a handle's descriptor.
Only the owner can close a handle; any other caller gets
{:error, :not_owner} and the handle stays open. A closed handle answers
{:error, :closed}, whoever asks, so calling this again is harmless.
The descriptor is closed when this returns :ok. Closing it runs on a
dirty I/O scheduler, so if close(2) blocks, for instance on a socket
with lingering data, only the caller waits.
If another process is inside with_fd/2 on the handle, or a connection is
still sending it, the handle is closed at once but the descriptor stays
open until that function returns or the send has passed it on.
:ok = Rebus.UnixFD.close(handle)
{:error, :closed} = Rebus.UnixFD.close(handle)
Makes pid the owner of a handle.
Only the owner can transfer a handle; any other caller gets
{:error, :not_owner}. A closed handle answers {:error, :closed},
whoever asks. If pid is not alive, the handle is closed and the call
returns {:error, :noproc}. A pid on another node gets
{:error, :not_local} and leaves the handle as it was. Making the current
owner the owner again does nothing.
worker = spawn(fn -> receive do {:take, handle} -> use_and_close(handle) end end)
:ok = Rebus.UnixFD.controlling_process(handle, worker)
send(worker, {:take, handle})
Duplicates a handle's descriptor into a new handle owned by the caller.
The two handles refer to the same open file but close independently. The
new handle is owned by the process that calls this, whether or not it owns
the original. Returns {:error, :closed} if the handle is closed, or
{:error, posix} if the operating system cannot duplicate the descriptor.
{:ok, mine} = Rebus.UnixFD.dup(borrowed)
Returns whether a handle is open.
A handle that is not local to this node, or on a platform without handle support, is reported as not open.
Returns the process that owns a handle, or {:error, :closed} once it is
closed.
{:ok, owner} = Rebus.UnixFD.owner(handle)
@spec supported?() :: boolean()
Returns whether this build of Rebus supports handles, and so can pass descriptors.
It is true on Linux and macOS when the application enabled Unix fd support
with config :rebus, unix_fds: true in its config/config.exs. A library
built on Rebus cannot enable it for its users, so one that needs descriptor
passing can check this at startup and tell them to.
unless Rebus.UnixFD.supported?() do
raise "add `config :rebus, unix_fds: true` to config/config.exs"
end
@spec with_fd(t(), (raw() -> result)) :: {:ok, result} | {:error, :closed | error()} when result: term()
Calls fun with the handle's raw descriptor number.
Returns {:ok, result} with what fun returned, or {:error, :closed}
without calling fun if the handle is closed.
The descriptor stays open until fun returns, even if the handle is closed
or its owner exits meanwhile, and the number is valid only until then. Do
not keep it, close it, or hand it to anything that outlives fun without
taking its own copy, as :socket.open/2 does. To keep a descriptor of your
own, use dup/1. If the calling process is killed inside fun, the
descriptor is released when the process's memory is reclaimed.
Exceptions, throws and exits from fun propagate after the descriptor is
released.
{:ok, {:ok, contents}} = Rebus.UnixFD.with_fd(handle, &File.read("/dev/fd/#{&1}"))Returning the number from fun, as in {:ok, fd} = with_fd(handle, & &1),
gives you a number that may already be closed, or reused for something
else, by the time you use it.