Skip to content

Commit 66729dd

Browse files
committed
elixir: refactor to avoid use of dirtyio
Potentially-blocking Rust-side calls now use message passing to respond to the caller. Elixir now has `Tailscale.Util.await`, which spawns a task that listens for the relevant response. Signed-off-by: Nathan Perry <nathan@tailscale.com> Change-Id: Iafe01153ca52b9f618c80666de5e6f186a6a6964
1 parent f330bab commit 66729dd

14 files changed

Lines changed: 496 additions & 186 deletions

File tree

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ts_elixir/lib/tailscale.ex

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
defmodule Tailscale do
2+
require Tailscale.Util
3+
24
@moduledoc """
35
Elixir bindings for the Tailscale Rust client.
46
@@ -66,8 +68,8 @@ defmodule Tailscale do
6668
6769
See `t:options/0` for details on available options.
6870
"""
69-
def connect(key_file_path, options) when is_binary(key_file_path) do
70-
case Tailscale.Native.load_key_file(key_file_path) do
71+
def connect(key_file_path, options) when is_binary(key_file_path) and is_list(options) do
72+
case Tailscale.Util.await(Tailscale.Native.load_key_file(key_file_path)) do
7173
{:ok, keys} ->
7274
Keyword.put(options, :keys, keys) |> connect()
7375

@@ -86,8 +88,10 @@ defmodule Tailscale do
8688
"""
8789
def connect(options \\ [])
8890

89-
def connect(options) when is_list(options),
90-
do: :proplists.to_map(options) |> Tailscale.Native.connect()
91+
def connect(options) when is_list(options) do
92+
options = :proplists.to_map(options)
93+
Tailscale.Util.await(Tailscale.Native.connect(options))
94+
end
9195

9296
def connect(key_file_path) when is_binary(key_file_path), do: connect(key_file_path, [])
9397

@@ -97,7 +101,7 @@ defmodule Tailscale do
97101
98102
Blocks until the address is available.
99103
"""
100-
def ipv4_addr(dev), do: Tailscale.Native.ipv4_addr(dev)
104+
def ipv4_addr(dev), do: Tailscale.Util.await(Tailscale.Native.ipv4_addr(dev))
101105

102106
@spec ipv6_addr(t()) :: {:ok, :inet.ip6_address()} | {:error, any()}
103107
@doc """
@@ -108,13 +112,13 @@ defmodule Tailscale do
108112
Note that this address is in `t::inet.ip6_address/0` format (16-bit segments), which may be
109113
difficult to read. See `:inet.ntoa/1` to format to a string.
110114
"""
111-
def ipv6_addr(dev), do: Tailscale.Native.ipv6_addr(dev)
115+
def ipv6_addr(dev), do: Tailscale.Util.await(Tailscale.Native.ipv6_addr(dev))
112116

113117
@spec self_node(t()) :: {:ok, Tailscale.NodeInfo.t()} | {:error, any()}
114118
@doc """
115119
Get this node's `m:Tailscale.NodeInfo`.
116120
"""
117-
defdelegate self_node(dev), to: Tailscale.Native
121+
def self_node(dev), do: Tailscale.Util.await(Tailscale.Native.self_node(dev))
118122

119123
@spec peer_by_name(t(), String.t()) :: {:ok, Tailscale.NodeInfo.t() | nil} | {:error, any()}
120124
@doc """
@@ -123,7 +127,8 @@ defmodule Tailscale do
123127
Returns `{:ok, nil}` if there was no such peer, and `{:error, reason}` if the lookup encountered
124128
an error.
125129
"""
126-
def peer_by_name(dev, name), do: Tailscale.Native.peer_by_name(dev, name)
130+
def peer_by_name(dev, name),
131+
do: Tailscale.Util.await(Tailscale.Native.peer_by_name(dev, name))
127132

128133
@spec peer_by_tailnet_ip(t(), Tailscale.ip_addr()) ::
129134
{:ok, Tailscale.NodeInfo.t() | nil} | {:error, any()}
@@ -132,12 +137,14 @@ defmodule Tailscale do
132137
133138
Returns `{:ok, nil}` if there was no such peer. `:error` if the lookup encountered an error.
134139
"""
135-
defdelegate peer_by_tailnet_ip(dev, ip), to: Tailscale.Native
140+
def peer_by_tailnet_ip(dev, ip),
141+
do: Tailscale.Util.await(Tailscale.Native.peer_by_tailnet_ip(dev, ip))
136142

137143
@spec peers_with_route(t(), Tailscale.ip_addr()) ::
138144
{:ok, [Tailscale.NodeInfo.t()]} | {:error, any()}
139145
@doc """
140146
Retrieve the most narrow set of peers that accept packets for the specified IP.
141147
"""
142-
defdelegate peers_with_route(dev, ip), to: Tailscale.Native
148+
def peers_with_route(dev, ip),
149+
do: Tailscale.Util.await(Tailscale.Native.peers_with_route(dev, ip))
143150
end

ts_elixir/lib/tailscale/native.ex

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,39 @@ defmodule Tailscale.Native do
3232
"""
3333
@opaque tcp_stream :: reference()
3434

35+
@typedoc """
36+
NIFs provided here may have asynchronous effects that would typically block and require the use of
37+
the DirtyIO scheduler. This is undesirable as we may have a large number of concurrent calls into
38+
the NIFs, which could exhaust the DirtyIO thread pool. Instead, we use message passing on the Rust
39+
side to send replies back into the BEAM. Functions that use this model return `async_reply`
40+
without blocking. The `:async` case means the reply will be sent asynchronously using a message of
41+
the format `{:tailscale, REF, PAYLOAD}`, where `REF` is the reference associated with the `:async`
42+
response, guaranteed unique per call.
43+
44+
The `:error` response means that an error was encountered before dispatching the asynchronous
45+
call.
46+
47+
The `:nif_panic` response means that the NIF panicked during execution; the second parameter is
48+
the reason for the panic (if given).
49+
50+
`{:raise, TERM}` means `TERM` should be raised as an exception.
51+
52+
`m:Tailscale.Util` has helpers for decoding messages of this form.
53+
"""
54+
@type async_reply() ::
55+
{:async, reference()}
56+
| {:error, any()}
57+
| {:nif_panic, String.t() | {}}
58+
| {:raise, any()}
59+
3560
defp err, do: :erlang.nif_error(:nif_not_loaded)
3661

3762
@doc """
3863
Open a new tailnet connection.
3964
4065
See `t:Tailscale.options/0` for details on what options are supported.
4166
"""
42-
@spec connect(%{}) :: {:ok, device()} | {:error, any()}
67+
@spec connect(%{}) :: async_reply()
4368
def connect(_opts), do: err()
4469

4570
@doc """
@@ -51,7 +76,7 @@ defmodule Tailscale.Native do
5176
- `port`: the port to which the socket should bind.
5277
"""
5378
@spec udp_bind(device(), Tailscale.ip_addr() | :ip4 | :ip6, :inet.port_number()) ::
54-
{:ok, udp_socket()} | {:error, any()}
79+
async_reply()
5580
def udp_bind(_dev, _addr, _port), do: err()
5681

5782
@doc """
@@ -65,14 +90,14 @@ defmodule Tailscale.Native do
6590
- `msg`: the packet to send.
6691
"""
6792
@spec udp_send(udp_socket(), Tailscale.ip_addr(), :inet.port_number(), binary()) ::
68-
:ok | {:error, any()}
93+
async_reply()
6994
def udp_send(_sock, _ip, _port, _msg), do: err()
7095

7196
@doc """
7297
Receive an incoming UDP packet on the given socket.
7398
"""
7499
@spec udp_recv(udp_socket()) ::
75-
{:ok, :inet.ip_address(), :inet.port_number(), binary()} | {:error, any()}
100+
async_reply()
76101
def udp_recv(_sock), do: err()
77102

78103
@doc """
@@ -92,7 +117,7 @@ defmodule Tailscale.Native do
92117
Start a TCP listener on the given device, address, and port.
93118
"""
94119
@spec tcp_listen(device(), Tailscale.ip_addr() | :ip4 | :ip6, :inet.port_number()) ::
95-
{:ok, tcp_listener()} | {:error, any()}
120+
async_reply()
96121
def tcp_listen(_dev, _addr, _port), do: err()
97122

98123
@doc """
@@ -105,13 +130,13 @@ defmodule Tailscale.Native do
105130
Connect to the given TCP endpoint using the given device.
106131
"""
107132
@spec tcp_connect(device(), Tailscale.ip_addr(), :inet.port_number()) ::
108-
{:ok, tcp_stream()} | {:error, any()}
133+
async_reply()
109134
def tcp_connect(_dev, _addr, _port), do: err()
110135

111136
@doc """
112137
Accept an incoming TCP connection. Blocks until one is available.
113138
"""
114-
@spec tcp_accept(tcp_listener()) :: {:ok, tcp_stream()} | {:error, any()}
139+
@spec tcp_accept(tcp_listener()) :: async_reply()
115140
def tcp_accept(_listener), do: err()
116141

117142
@doc """
@@ -120,13 +145,13 @@ defmodule Tailscale.Native do
120145
121146
Returns the number of bytes actually written to the remote.
122147
"""
123-
@spec tcp_send(tcp_stream(), binary()) :: {:ok, integer()} | {:error, any()}
148+
@spec tcp_send(tcp_stream(), binary()) :: async_reply()
124149
def tcp_send(_stream, _msg), do: err()
125150

126151
@doc """
127152
Receive incoming data from the tcp socket, blocking until at least one byte can be received.
128153
"""
129-
@spec tcp_recv(tcp_stream()) :: {:ok, binary()} | {:error, any()}
154+
@spec tcp_recv(tcp_stream()) :: async_reply()
130155
def tcp_recv(_stream), do: err()
131156

132157
@doc """
@@ -146,44 +171,76 @@ defmodule Tailscale.Native do
146171
147172
Blocks until the device is connected and gets its address from control.
148173
"""
149-
@spec ipv4_addr(device()) :: {:ok, :inet.ip4_address()} | {:error, any()}
174+
@spec ipv4_addr(device()) :: async_reply()
150175
def ipv4_addr(_dev), do: err()
151176

152177
@doc """
153178
Retrieve the IPv6 address for the given tailscale device.
154179
155180
Blocks until the device is connected and gets its address from control.
156181
"""
157-
@spec ipv6_addr(device()) :: {:ok, :inet.ip6_address()} | {:error, any()}
182+
@spec ipv6_addr(device()) :: async_reply()
158183
def ipv6_addr(_dev), do: err()
159184

160185
@doc """
161186
Retrieve a peer by name.
162187
"""
163-
@spec peer_by_name(device(), String.t()) :: {:ok, %{} | nil} | {:error, any()}
188+
@spec peer_by_name(device(), String.t()) :: async_reply()
164189
def peer_by_name(_dev, _name), do: err()
165190

166191
@doc """
167192
Retrieve this node's info
168193
"""
169-
@spec self_node(device()) :: {:ok, %{}} | {:error, any()}
194+
@spec self_node(device()) :: async_reply()
170195
def self_node(_dev), do: err()
171196

172197
@doc """
173198
Retrieve a peer by its tailnet IP.
174199
"""
175-
@spec peer_by_tailnet_ip(device(), Tailscale.ip_addr()) :: {:ok, %{} | nil} | {:error, any()}
200+
@spec peer_by_tailnet_ip(device(), Tailscale.ip_addr()) :: async_reply()
176201
def peer_by_tailnet_ip(_dev, _ip), do: err()
177202

178203
@doc """
179204
Retrieve the most narrow set of peers that accept packets for the specified IP.
180205
"""
181-
@spec peers_with_route(device(), Tailscale.ip_addr()) :: {:ok, [%{}]} | {:error, any()}
206+
@spec peers_with_route(device(), Tailscale.ip_addr()) :: async_reply()
182207
def peers_with_route(_dev, _ip), do: err()
183208

184209
@doc """
185210
Load key state from the specified path, generating a new state if the file doesn't exist.
186211
"""
187-
@spec load_key_file(String.t()) :: {:ok, Tailscale.Keystate.t()} | {:error, any()}
212+
@spec load_key_file(String.t()) :: async_reply()
188213
def load_key_file(_path), do: err()
214+
215+
@doc """
216+
Raise a `:badarg` exception.
217+
"""
218+
@spec raise_badarg() :: nil
219+
def raise_badarg(), do: err()
220+
221+
if @testing_nifs do
222+
@doc """
223+
DEV ONLY: trigger an async panic in the Rust code with the given message (if provided).
224+
"""
225+
@spec async_panic(String.t() | nil) :: async_reply()
226+
def async_panic(_msg \\ nil), do: err()
227+
228+
@doc """
229+
DEV ONLY: trigger a raised exception in the Rust code with the given message.
230+
"""
231+
@spec async_raise(String.t(), boolean()) :: async_reply()
232+
def async_raise(_msg, _atom \\ false), do: err()
233+
234+
@doc """
235+
DEV ONLY: trigger an asynchronous error in the Rust code with the given message.
236+
"""
237+
@spec async_error(String.t(), boolean()) :: async_reply()
238+
def async_error(_msg, _atom \\ false), do: err()
239+
240+
@doc """
241+
DEV ONLY: trigger an asynchronous `:badarg` in the Rust code with the given message.
242+
"""
243+
@spec async_badarg() :: async_reply()
244+
def async_badarg(), do: err()
245+
end
189246
end

ts_elixir/lib/tailscale/tcp.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
defmodule Tailscale.Tcp do
2+
require Tailscale.Util
3+
24
@moduledoc """
35
Functionality to create tailscale TCP sockets.
46
@@ -19,7 +21,7 @@ defmodule Tailscale.Tcp do
1921
@spec listen(Tailscale.t(), Tailscale.ip_addr() | :ip4 | :ip6, :inet.port_number()) ::
2022
{:ok, Tailscale.Tcp.Listener.t()} | {:error, any()}
2123
def listen(dev, addr, port) do
22-
Tailscale.Native.tcp_listen(dev, addr, port)
24+
Tailscale.Util.await(Tailscale.Native.tcp_listen(dev, addr, port))
2325
end
2426

2527
@doc """
@@ -28,6 +30,6 @@ defmodule Tailscale.Tcp do
2830
@spec connect(Tailscale.t(), Tailscale.ip_addr(), :inet.port_number()) ::
2931
{:ok, Tailscale.Tcp.Stream.t()} | {:error, any()}
3032
def connect(dev, addr, port) do
31-
Tailscale.Native.tcp_connect(dev, addr, port)
33+
Tailscale.Util.await(Tailscale.Native.tcp_connect(dev, addr, port))
3234
end
3335
end

ts_elixir/lib/tailscale/tcp/listener.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
defmodule Tailscale.Tcp.Listener do
2+
require Tailscale.Util
3+
24
@moduledoc """
35
Tailscale TCP listening socket functionality.
46
"""
@@ -15,7 +17,7 @@ defmodule Tailscale.Tcp.Listener do
1517
Blocks until a connection is ready.
1618
"""
1719
def accept(res) do
18-
Tailscale.Native.tcp_accept(res)
20+
Tailscale.Util.await(Tailscale.Native.tcp_accept(res))
1921
end
2022

2123
@doc """

ts_elixir/lib/tailscale/tcp/stream.ex

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ defmodule Tailscale.Tcp.Stream do
33
Tailscale TCP sockets (connected).
44
"""
55

6+
require Tailscale.Util
7+
68
@typedoc """
79
A handle to a TCP stream (connected socket).
810
"""
@@ -15,7 +17,7 @@ defmodule Tailscale.Tcp.Stream do
1517
Returns the number of bytes actually sent.
1618
"""
1719
def send(res, msg) do
18-
Tailscale.Native.tcp_send(res, msg)
20+
Tailscale.Util.await(Tailscale.Native.tcp_send(res, msg))
1921
end
2022

2123
@spec send_all(t(), binary()) :: :ok | {:error, any()}
@@ -27,7 +29,7 @@ defmodule Tailscale.Tcp.Stream do
2729

2830
case Tailscale.Tcp.Stream.send(res, msg) do
2931
{:ok, ^len} -> :ok
30-
{:ok, n} -> Tailscale.Tcp.Stream.send_all(res, binary_slice(msg, n..len))
32+
{:ok, n} -> send_all(res, binary_slice(msg, n..len))
3133
err -> err
3234
end
3335
end
@@ -37,7 +39,7 @@ defmodule Tailscale.Tcp.Stream do
3739
Receive data from the TCP socket, blocking until at least one byte can be received.
3840
"""
3941
def recv(res) do
40-
Tailscale.Native.tcp_recv(res)
42+
Tailscale.Util.await(Tailscale.Native.tcp_recv(res))
4143
end
4244

4345
@spec local_addr(t()) :: {:inet.ip_address(), :inet.port_number()}

0 commit comments

Comments
 (0)