Packages
gnat
1.9.1
1.16.0
1.15.2
1.15.1
1.15.0
1.14.1
1.14.0
1.14.0-rc1
1.14.0-rc0
1.13.1
1.13.0
1.12.1
1.12.0
1.11.1
1.11.0
1.10.2
1.10.2-rc1
1.10.2-rc0
1.10.1
1.10.0
1.9.1
1.9.0
1.8.5
1.8.4
1.8.3
1.8.2
1.8.1
1.7.1
1.7.0
1.6.0
1.5.2
1.5.1
1.5.0
1.4.0
1.3.0
1.3.0-rc0
1.2.1
1.2.0
1.1.3
1.1.2
1.1.1
1.1.0
1.0.1
1.0.0
0.7.0
0.6.1
0.6.0
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
A nats client in pure elixir. Resilience, Performance, Ease-of-Use.
Current section
65 Versions
Jump to
Current section
65 Versions
Compare versions
5
files changed
+231
additions
-140
deletions
| @@ -15,9 +15,9 @@ The goals of the project are resiliency, performance, and ease of use. | |
| 15 15 | ## Usage |
| 16 16 | |
| 17 17 | ``` elixir |
| 18 | - {:ok, gnat} = Gnat.start_link(%{host: '127.0.0.1', port: 4222}) |
| 18 | + {:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222}) |
| 19 19 | # Or if the server requires TLS you can start a connection with: |
| 20 | - # {:ok, gnat} = Gnat.start_link(%{host: '127.0.0.1', port: 4222, tls: true}) |
| 20 | + # {:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222, tls: true}) |
| 21 21 | |
| 22 22 | {:ok, subscription} = Gnat.sub(gnat, self(), "pawnee.*") |
| 23 23 | :ok = Gnat.pub(gnat, "pawnee.news", "Leslie Knope recalled from city council (Jammed)") |
| @@ -31,16 +31,16 @@ end | |
| 31 31 | |
| 32 32 | ``` elixir |
| 33 33 | # with user and password |
| 34 | - {:ok, gnat} = Gnat.start_link(%{host: '127.0.0.1', port: 4222, username: "joe", password: "123", auth_required: true}) |
| 34 | + {:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222, username: "joe", password: "123", auth_required: true}) |
| 35 35 | |
| 36 36 | # with token |
| 37 | - {:ok, gnat} = Gnat.start_link(%{host: '127.0.0.1', port: 4222, token: "secret", auth_required: true}) |
| 37 | + {:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222, token: "secret", auth_required: true}) |
| 38 38 | |
| 39 39 | # with an nkey seed |
| 40 | - {:ok, gnat} = Gnat.start_link(%{host: '127.0.0.1', port: 4222, nkey_seed: "SUAM...", auth_required: true}) |
| 40 | + {:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222, nkey_seed: "SUAM...", auth_required: true}) |
| 41 41 | |
| 42 42 | # with decentralized user credentials (JWT) |
| 43 | - {:ok, gnat} = Gnat.start_link(%{host: '127.0.0.1', port: 4222, nkey_seed: "SUAM...", jwt: "eyJ0eX...", auth_required: true}) |
| 43 | + {:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222, nkey_seed: "SUAM...", jwt: "eyJ0eX...", auth_required: true}) |
| 44 44 | |
| 45 45 | # connect to NGS with JWT |
| 46 46 | {:ok, gnat} = Gnat.start_link(%{host: "connect.ngs.global", tls: true, jwt: "ey...", nkey_seed: "SUAM..."}) |
| @@ -53,7 +53,7 @@ In order to connect to these clusters you'll want to pass some extra TLS setting | |
| 53 53 | |
| 54 54 | ``` elixir |
| 55 55 | # using a basic TLS connection |
| 56 | - {:ok, gnat} = Gnat.start_link(%{host: '127.0.0.1', port: 4222, tls: true}) |
| 56 | + {:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222, tls: true}) |
| 57 57 | |
| 58 58 | # Passing a Client Certificate for verification |
| 59 59 | {:ok, gnat} = Gnat.start_link(%{tls: true, ssl_opts: [certfile: "client-cert.pem", keyfile: "client-key.pem"]}) |
| @@ -3,7 +3,7 @@ | |
| 3 3 | <<"https://github.com/nats-io/nats.ex/blob/master/CHANGELOG.md">>}, |
| 4 4 | {<<"Github">>,<<"https://github.com/nats-io/nats.ex">>}]}. |
| 5 5 | {<<"name">>,<<"gnat">>}. |
| 6 | - {<<"version">>,<<"1.9.0">>}. |
| 6 | + {<<"version">>,<<"1.9.1">>}. |
| 7 7 | {<<"description">>, |
| 8 8 | <<"A nats client in pure elixir. Resilience, Performance, Ease-of-Use.">>}. |
| 9 9 | {<<"elixir">>,<<"~> 1.14">>}. |
| @@ -29,15 +29,15 @@ defmodule Gnat do | |
| 29 29 | * `description` - A string description of the `status` |
| 30 30 | """ |
| 31 31 | @type message :: %{ |
| 32 | - required(:gnat) => t(), |
| 33 | - required(:topic) => binary(), |
| 34 | - required(:body) => iodata(), |
| 35 | - required(:sid) => non_neg_integer(), |
| 36 | - optional(:reply_to) => binary(), |
| 37 | - optional(:headers) => headers(), |
| 38 | - optional(:status) => String.t(), |
| 39 | - optional(:description) => String.t() |
| 40 | - } |
| 32 | + required(:gnat) => t(), |
| 33 | + required(:topic) => binary(), |
| 34 | + required(:body) => iodata(), |
| 35 | + required(:sid) => non_neg_integer(), |
| 36 | + optional(:reply_to) => binary(), |
| 37 | + optional(:headers) => headers(), |
| 38 | + optional(:status) => String.t(), |
| 39 | + optional(:description) => String.t() |
| 40 | + } |
| 41 41 | @type sent_message :: {:msg, message()} |
| 42 42 | |
| 43 43 | @typedoc """ |
| @@ -51,15 +51,15 @@ defmodule Gnat do | |
| 51 51 | * `no_responders` - Enable the no responders behavior (see `Gnat.request/4`) |
| 52 52 | """ |
| 53 53 | @type connection_settings :: %{ |
| 54 | - optional(:connection_timeout) => non_neg_integer(), |
| 55 | - optional(:host) => binary(), |
| 56 | - optional(:inbox_prefix) => binary(), |
| 57 | - optional(:port) => non_neg_integer(), |
| 58 | - optional(:ssl_opts) => list(), |
| 59 | - optional(:tcp_opts) => list(), |
| 60 | - optional(:tls) => boolean(), |
| 61 | - optional(:no_responders) => boolean(), |
| 62 | - } |
| 54 | + optional(:connection_timeout) => non_neg_integer(), |
| 55 | + optional(:host) => binary(), |
| 56 | + optional(:inbox_prefix) => binary(), |
| 57 | + optional(:port) => non_neg_integer(), |
| 58 | + optional(:ssl_opts) => list(), |
| 59 | + optional(:tcp_opts) => list(), |
| 60 | + optional(:tls) => boolean(), |
| 61 | + optional(:no_responders) => boolean() |
| 62 | + } |
| 63 63 | |
| 64 64 | @typedoc """ |
| 65 65 | [Info Protocol](https://docs.nats.io/reference/reference-protocols/nats-protocol#info) |
| @@ -88,30 +88,30 @@ defmodule Gnat do | |
| 88 88 | * `tls_available` - If the server can use TLS |
| 89 89 | """ |
| 90 90 | @type server_info :: %{ |
| 91 | - :client_id => non_neg_integer(), |
| 92 | - :client_ip => binary(), |
| 93 | - optional(:ip) => binary(), |
| 94 | - optional(:cluster) => binary(), |
| 95 | - optional(:cluster_dynamic) => boolean(), |
| 96 | - optional(:connect_urls) => list(binary()), |
| 97 | - optional(:ws_connect_urls) => list(binary()), |
| 98 | - optional(:git_commit) => binary(), |
| 99 | - :go => binary(), |
| 100 | - :headers => boolean(), |
| 101 | - :host => binary(), |
| 102 | - optional(:jetstream) => binary(), |
| 103 | - :max_payload => integer(), |
| 104 | - :port => non_neg_integer(), |
| 105 | - :proto => integer(), |
| 106 | - :server_id => binary(), |
| 107 | - :server_name => binary(), |
| 108 | - :version => binary(), |
| 109 | - optional(:ldm) => boolean(), |
| 110 | - optional(:tls_verify) => boolean(), |
| 111 | - optional(:tls_available) => boolean(), |
| 112 | - optional(:tls_required) => boolean(), |
| 113 | - optional(:auth_required) => boolean(), |
| 114 | - } |
| 91 | + :client_id => non_neg_integer(), |
| 92 | + :client_ip => binary(), |
| 93 | + optional(:ip) => binary(), |
| 94 | + optional(:cluster) => binary(), |
| 95 | + optional(:cluster_dynamic) => boolean(), |
| 96 | + optional(:connect_urls) => list(binary()), |
| 97 | + optional(:ws_connect_urls) => list(binary()), |
| 98 | + optional(:git_commit) => binary(), |
| 99 | + :go => binary(), |
| 100 | + :headers => boolean(), |
| 101 | + :host => binary(), |
| 102 | + optional(:jetstream) => binary(), |
| 103 | + :max_payload => integer(), |
| 104 | + :port => non_neg_integer(), |
| 105 | + :proto => integer(), |
| 106 | + :server_id => binary(), |
| 107 | + :server_name => binary(), |
| 108 | + :version => binary(), |
| 109 | + optional(:ldm) => boolean(), |
| 110 | + optional(:tls_verify) => boolean(), |
| 111 | + optional(:tls_available) => boolean(), |
| 112 | + optional(:tls_required) => boolean(), |
| 113 | + optional(:auth_required) => boolean() |
| 114 | + } |
| 115 115 | |
| 116 116 | @default_connection_settings %{ |
| 117 117 | host: ~c"localhost", |
| @@ -130,15 +130,15 @@ defmodule Gnat do | |
| 130 130 | Starts a connection to a nats broker |
| 131 131 | |
| 132 132 | ``` |
| 133 | - {:ok, gnat} = Gnat.start_link(%{host: '127.0.0.1', port: 4222}) |
| 133 | + {:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222}) |
| 134 134 | # if the server requires TLS you can start a connection with: |
| 135 | - {:ok, gnat} = Gnat.start_link(%{host: '127.0.0.1', port: 4222, tls: true}) |
| 135 | + {:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222, tls: true}) |
| 136 136 | # if the server requires TLS and a client certificate you can start a connection with: |
| 137 137 | {:ok, gnat} = Gnat.start_link(%{tls: true, ssl_opts: [certfile: "client-cert.pem", keyfile: "client-key.pem"]}) |
| 138 138 | # you can customize default "_INBOX." inbox prefix with: |
| 139 | - {:ok, gnat} = Gnat.start_link(%{host: '127.0.0.1', port: 4222, inbox_prefix: "my_prefix._INBOX."}) |
| 139 | + {:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222, inbox_prefix: "my_prefix._INBOX."}) |
| 140 140 | # you can use IPv6 addresses too |
| 141 | - {:ok, gnat} = Gnat.start_link(%{host: '::1', port: 4222, tcp_opts: [:inet6, :binary]}) |
| 141 | + {:ok, gnat} = Gnat.start_link(%{host: "::1", port: 4222, tcp_opts: [:inet6, :binary]}) |
| 142 142 | ``` |
| 143 143 | |
| 144 144 | You can also pass arbitrary SSL or TCP options in the `tcp_opts` and `ssl_opts` keys. |
| @@ -146,7 +146,7 @@ defmodule Gnat do | |
| 146 146 | |
| 147 147 | The final `opts` argument will be passed to the `GenServer.start_link` call so you can pass things like `[name: :gnat_connection]`. |
| 148 148 | """ |
| 149 | - @spec start_link(connection_settings(), keyword()) :: GenServer.on_start |
| 149 | + @spec start_link(connection_settings(), keyword()) :: GenServer.on_start() |
| 150 150 | def start_link(connection_settings \\ %{}, opts \\ []) do |
| 151 151 | GenServer.start_link(__MODULE__, connection_settings, opts) |
| 152 152 | end |
| @@ -183,7 +183,8 @@ defmodule Gnat do | |
| 183 183 | end |
| 184 184 | ``` |
| 185 185 | """ |
| 186 | - @spec sub(t(), pid(), String.t, keyword()) :: {:ok, non_neg_integer()} | {:ok, String.t} | {:error, String.t} |
| 186 | + @spec sub(t(), pid(), String.t(), keyword()) :: |
| 187 | + {:ok, non_neg_integer()} | {:ok, String.t()} | {:error, String.t()} |
| 187 188 | def sub(pid, subscriber, topic, opts \\ []) do |
| 188 189 | start = :erlang.monotonic_time() |
| 189 190 | result = GenServer.call(pid, {:sub, subscriber, topic, opts}) |
| @@ -220,13 +221,13 @@ defmodule Gnat do | |
| 220 221 | (see [the Nats 2.2 release notes for details](https://docs.nats.io/whats_new_22#message-headers)), |
| 221 222 | so only use them when they are really valuable. |
| 222 223 | """ |
| 223 | - @spec pub(t(), String.t, binary(), keyword()) :: :ok |
| 224 | + @spec pub(t(), String.t(), binary(), keyword()) :: :ok |
| 224 225 | def pub(pid, topic, message, opts \\ []) do |
| 225 226 | start = :erlang.monotonic_time() |
| 226 227 | opts = prepare_headers(opts) |
| 227 228 | result = GenServer.call(pid, {:pub, topic, message, opts}) |
| 228 229 | latency = :erlang.monotonic_time() - start |
| 229 | - :telemetry.execute([:gnat, :pub], %{latency: latency} , %{topic: topic}) |
| 230 | + :telemetry.execute([:gnat, :pub], %{latency: latency}, %{topic: topic}) |
| 230 231 | result |
| 231 232 | end |
| 232 233 | |
| @@ -255,12 +256,14 @@ defmodule Gnat do | |
| 255 256 | your Gnat connection with the `no_responders: true` option and this function will return very quickly with |
| 256 257 | an `{:error, :no_responders}` value. This behavior also works with `request_multi/4` |
| 257 258 | """ |
| 258 | - @spec request(t(), String.t, binary(), keyword()) :: {:ok, message} | {:error, :timeout} | {:error, :no_responders} |
| 259 | + @spec request(t(), String.t(), binary(), keyword()) :: |
| 260 | + {:ok, message} | {:error, :timeout} | {:error, :no_responders} |
| 259 261 | def request(pid, topic, body, opts \\ []) do |
| 260 262 | start = :erlang.monotonic_time() |
| 261 263 | receive_timeout = Keyword.get(opts, :receive_timeout, 60_000) |
| 262 264 | req = %{recipient: self(), body: body, topic: topic} |
| 263 265 | opts = prepare_headers(opts) |
| 266 | + |
| 264 267 | req = |
| 265 268 | case Keyword.get(opts, :headers) do |
| 266 269 | nil -> req |
| @@ -271,7 +274,7 @@ defmodule Gnat do | |
| 271 274 | response = receive_request_response(subscription, receive_timeout) |
| 272 275 | :ok = unsub(pid, subscription) |
| 273 276 | latency = :erlang.monotonic_time() - start |
| 274 | - :telemetry.execute([:gnat, :request], %{latency: latency}, %{topic: topic}) |
| 277 | + :telemetry.execute([:gnat, :request], %{latency: latency}, %{topic: topic}) |
| 275 278 | response |
| 276 279 | end |
| 277 280 | |
| @@ -292,7 +295,8 @@ defmodule Gnat do | |
| 292 295 | Enum.count(responses) #=> 5 |
| 293 296 | ``` |
| 294 297 | """ |
| 295 | - @spec request_multi(t(), String.t(), binary(), keyword()) :: {:ok, list(message())} | {:error, :no_responders} |
| 298 | + @spec request_multi(t(), String.t(), binary(), keyword()) :: |
| 299 | + {:ok, list(message())} | {:error, :no_responders} |
| 296 300 | def request_multi(pid, topic, body, opts \\ []) do |
| 297 301 | start = :erlang.monotonic_time() |
| 298 302 | receive_timeout_ms = Keyword.get(opts, :receive_timeout, 60_000) |
| @@ -301,6 +305,7 @@ defmodule Gnat do | |
| 301 305 | |
| 302 306 | req = %{recipient: self(), body: body, topic: topic} |
| 303 307 | opts = prepare_headers(opts) |
| 308 | + |
| 304 309 | req = |
| 305 310 | case Keyword.get(opts, :headers) do |
| 306 311 | nil -> req |
| @@ -308,14 +313,16 @@ defmodule Gnat do | |
| 308 313 | end |
| 309 314 | |
| 310 315 | {:ok, subscription} = GenServer.call(pid, {:request, req}) |
| 316 | + |
| 311 317 | result = |
| 312 318 | case receive_multi_request_responses(subscription, expiration, max_messages) do |
| 313 319 | {:error, :no_responders} -> {:error, :no_responders} |
| 314 320 | responses when is_list(responses) -> {:ok, responses} |
| 315 321 | end |
| 322 | + |
| 316 323 | :ok = unsub(pid, subscription) |
| 317 324 | latency = :erlang.monotonic_time() - start |
| 318 | - :telemetry.execute([:gnat, :request_multi], %{latency: latency}, %{topic: topic}) |
| 325 | + :telemetry.execute([:gnat, :request_multi], %{latency: latency}, %{topic: topic}) |
| 319 326 | result |
| 320 327 | end |
| 321 328 | |
| @@ -338,7 +345,7 @@ defmodule Gnat do | |
| 338 345 | :ok = Gnat.unsub(gnat, subscription, max_messages: 2) |
| 339 346 | ``` |
| 340 347 | """ |
| 341 | - @spec unsub(t(), non_neg_integer() | String.t, keyword()) :: :ok |
| 348 | + @spec unsub(t(), non_neg_integer() | String.t(), keyword()) :: :ok |
| 342 349 | def unsub(pid, sid, opts \\ []) do |
| 343 350 | start = :erlang.monotonic_time() |
| 344 351 | result = GenServer.call(pid, {:unsub, sid, opts}) |
| @@ -359,6 +366,7 @@ defmodule Gnat do | |
| 359 366 | @deprecated "Pinging is handled internally by the connection, this functionality will be removed" |
| 360 367 | def ping(pid) do |
| 361 368 | GenServer.call(pid, {:ping, self()}) |
| 369 | + |
| 362 370 | receive do |
| 363 371 | :pong -> :ok |
| 364 372 | after |
| @@ -386,56 +394,70 @@ defmodule Gnat do | |
| 386 394 | |
| 387 395 | case Gnat.Handshake.connect(connection_settings) do |
| 388 396 | {:ok, socket, server_info} -> |
| 389 | - parser = Parsec.new |
| 397 | + parser = Parsec.new() |
| 390 398 | |
| 391 399 | request_inbox_prefix = Map.fetch!(connection_settings, :inbox_prefix) <> "#{nuid()}." |
| 392 400 | |
| 393 | - state = %{socket: socket, |
| 394 | - connection_settings: connection_settings, |
| 395 | - server_info: server_info, |
| 396 | - next_sid: 1, |
| 397 | - receivers: %{}, |
| 398 | - parser: parser, |
| 399 | - request_receivers: %{}, |
| 400 | - request_inbox_prefix: request_inbox_prefix} |
| 401 | + state = %{ |
| 402 | + socket: socket, |
| 403 | + connection_settings: connection_settings, |
| 404 | + server_info: server_info, |
| 405 | + next_sid: 1, |
| 406 | + receivers: %{}, |
| 407 | + parser: parser, |
| 408 | + request_receivers: %{}, |
| 409 | + request_inbox_prefix: request_inbox_prefix |
| 410 | + } |
| 401 411 | |
| 402 412 | state = create_request_subscription(state) |
| 403 413 | {:ok, state} |
| 414 | + |
| 404 415 | {:error, reason} -> |
| 405 416 | {:stop, reason} |
| 406 417 | end |
| 407 418 | end |
| 408 419 | |
| 409 420 | @impl GenServer |
| 410 | - def handle_info({:tcp, socket, data}, %{socket: socket}=state) do |
| 421 | + def handle_info({:tcp, socket, data}, %{socket: socket} = state) do |
| 411 422 | data_packets = receive_additional_tcp_data(socket, [data], 10) |
| 412 | - new_state = Enum.reduce(data_packets, state, fn(data, %{parser: parser}=state) -> |
| 413 | - {new_parser, messages} = Parsec.parse(parser, data) |
| 414 | - new_state = %{state | parser: new_parser} |
| 415 | - Enum.reduce(messages, new_state, &process_message/2) |
| 416 | - end) |
| 423 | + |
| 424 | + new_state = |
| 425 | + Enum.reduce(data_packets, state, fn data, %{parser: parser} = state -> |
| 426 | + {new_parser, messages} = Parsec.parse(parser, data) |
| 427 | + new_state = %{state | parser: new_parser} |
| 428 | + Enum.reduce(messages, new_state, &process_message/2) |
| 429 | + end) |
| 430 | + |
| 417 431 | {:noreply, new_state} |
| 418 432 | end |
| 433 | + |
| 419 434 | def handle_info({:ssl, socket, data}, state) do |
| 420 435 | handle_info({:tcp, socket, data}, state) |
| 421 436 | end |
| 437 | + |
| 422 438 | def handle_info({:tcp_closed, _}, state) do |
| 423 439 | {:stop, "connection closed", state} |
| 424 440 | end |
| 441 | + |
| 425 442 | def handle_info({:ssl_closed, _}, state) do |
| 426 443 | {:stop, "connection closed", state} |
| 427 444 | end |
| 445 | + |
| 428 446 | def handle_info({:tcp_error, _, reason}, state) do |
| 429 447 | {:stop, "tcp transport error #{inspect(reason)}", state} |
| 430 448 | end |
| 449 | + |
| 431 450 | def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do |
| 432 | - {sid, _receiver} = Enum.find(state.receivers, fn {_sid, receiver} -> receiver.recipient == pid end) |
| 451 | + {sid, _receiver} = |
| 452 | + Enum.find(state.receivers, fn {_sid, receiver} -> receiver.recipient == pid end) |
| 453 | + |
| 433 454 | state = unsub_sid(sid, [], state) |
| 434 455 | |
| 435 456 | {:noreply, state} |
| 436 457 | end |
| 458 | + |
| 437 459 | def handle_info(other, state) do |
| 438 | - Logger.error "#{__MODULE__} received unexpected message: #{inspect other}" |
| 460 | + Logger.error("#{__MODULE__} received unexpected message: #{inspect(other)}") |
| 439 461 | {:noreply, state} |
| 440 462 | end |
| 441 463 | |
| @@ -444,34 +466,48 @@ defmodule Gnat do | |
| 444 466 | socket_close(state) |
| 445 467 | {:stop, :normal, :ok, state} |
| 446 468 | end |
| 447 | - def handle_call({:sub, receiver, topic, opts}, _from, %{next_sid: sid}=state) do |
| 469 | + |
| 470 | + def handle_call({:sub, receiver, topic, opts}, _from, %{next_sid: sid} = state) do |
| 448 471 | sub = Command.build(:sub, topic, sid, opts) |
| 449 472 | :ok = socket_write(state, sub) |
| 450 473 | ref = Process.monitor(receiver) |
| 451 | - next_state = add_subscription_to_state(state, sid, receiver, ref) |> Map.put(:next_sid, sid + 1) |
| 474 | + |
| 475 | + next_state = |
| 476 | + add_subscription_to_state(state, sid, receiver, ref) |> Map.put(:next_sid, sid + 1) |
| 477 | + |
| 452 478 | {:reply, {:ok, sid}, next_state} |
| 453 479 | end |
| 480 | + |
| 454 481 | def handle_call({:pub, topic, message, opts}, from, state) do |
| 455 482 | commands = [Command.build(:pub, topic, message, opts)] |
| 456 483 | froms = [from] |
| 457 484 | {commands, froms} = receive_additional_pubs(commands, froms, 10) |
| 458 485 | :ok = socket_write(state, commands) |
| 459 | - Enum.each(froms, fn(from) -> GenServer.reply(from, :ok) end) |
| 486 | + Enum.each(froms, fn from -> GenServer.reply(from, :ok) end) |
| 460 487 | {:noreply, state} |
| 461 488 | end |
| 489 | + |
| 462 490 | def handle_call({:request, request}, _from, state) do |
| 463 491 | inbox = make_new_inbox(state) |
| 464 | - new_state = %{state | request_receivers: Map.put(state.request_receivers, inbox, request.recipient)} |
| 492 | + |
| 493 | + new_state = %{ |
| 494 | + state |
| 495 | + | request_receivers: Map.put(state.request_receivers, inbox, request.recipient) |
| 496 | + } |
| 497 | + |
| 465 498 | pub = |
| 466 499 | case request do |
| 467 500 | %{headers: headers} -> |
| 468 501 | Command.build(:pub, request.topic, request.body, headers: headers, reply_to: inbox) |
| 502 | + |
| 469 503 | _ -> |
| 470 504 | Command.build(:pub, request.topic, request.body, reply_to: inbox) |
| 471 505 | end |
| 506 | + |
| 472 507 | :ok = socket_write(new_state, [pub]) |
| 473 508 | {:reply, {:ok, inbox}, new_state} |
| 474 509 | end |
| 510 | + |
| 475 511 | # When the SID is a string, it's a topic, which is used as a key in the request receiver map. |
| 476 512 | def handle_call({:unsub, topic, _opts}, _from, state) when is_binary(topic) do |
| 477 513 | if Map.has_key?(state.request_receivers, topic) do |
| @@ -482,18 +518,22 @@ defmodule Gnat do | |
| 482 518 | {:reply, :ok, state} |
| 483 519 | end |
| 484 520 | end |
| 521 | + |
| 485 522 | def handle_call({:unsub, sid, opts}, _from, state) do |
| 486 523 | state = unsub_sid(sid, opts, state) |
| 487 524 | {:reply, :ok, state} |
| 488 525 | end |
| 526 | + |
| 489 527 | def handle_call({:ping, pinger}, _from, state) do |
| 490 528 | :ok = socket_write(state, "PING\r\n") |
| 491 529 | {:reply, :ok, Map.put(state, :pinger, pinger)} |
| 492 530 | end |
| 531 | + |
| 493 532 | def handle_call(:active_subscriptions, _from, state) do |
| 494 533 | active_subscriptions = Enum.count(state.receivers) |
| 495 534 | {:reply, {:ok, active_subscriptions}, state} |
| 496 535 | end |
| 536 | + |
| 497 537 | def handle_call(:server_info, _from, state) do |
| 498 538 | {:reply, state.server_info, state} |
| 499 539 | end |
| @@ -502,16 +542,17 @@ defmodule Gnat do | |
| 502 542 | case Map.get(state.receivers, sid) do |
| 503 543 | nil -> |
| 504 544 | state |
| 505 | - %{monitor_ref: ref} -> |
| 506 | - command = Command.build(:unsub, sid, opts) |
| 507 | - :ok = socket_write(state, command) |
| 508 | - Process.demonitor(ref) |
| 509 | - state = cleanup_subscription_from_state(state, sid, opts) |
| 510 | - state |
| 545 | + |
| 546 | + %{monitor_ref: ref} -> |
| 547 | + command = Command.build(:unsub, sid, opts) |
| 548 | + :ok = socket_write(state, command) |
| 549 | + Process.demonitor(ref) |
| 550 | + state = cleanup_subscription_from_state(state, sid, opts) |
| 551 | + state |
| 511 552 | end |
| 512 553 | end |
| 513 554 | |
| 514 | - defp create_request_subscription(%{request_inbox_prefix: request_inbox_prefix}=state) do |
| 555 | + defp create_request_subscription(%{request_inbox_prefix: request_inbox_prefix} = state) do |
| 515 556 | # Example: "_INBOX.Jhf7AcTGP3x4dAV9.*" |
| 516 557 | wildcard_inbox_topic = request_inbox_prefix <> "*" |
| 517 558 | sub = Command.build(:sub, wildcard_inbox_topic, @request_sid, []) |
| @@ -521,7 +562,7 @@ defmodule Gnat do | |
| 521 562 | |
| 522 563 | defp make_new_inbox(%{request_inbox_prefix: prefix}), do: prefix <> nuid() |
| 523 564 | |
| 524 | - defp nuid(), do: :crypto.strong_rand_bytes(12) |> Base.encode64 |
| 565 | + defp nuid(), do: :crypto.strong_rand_bytes(12) |> Base.encode64() |
| 525 566 | |
| 526 567 | defp prepare_headers(opts) do |
| 527 568 | if Keyword.has_key?(opts, :headers) do |
| @@ -538,42 +579,60 @@ defmodule Gnat do | |
| 538 579 | defp socket_write(%{socket: socket, connection_settings: %{tls: true}}, iodata) do |
| 539 580 | :ssl.send(socket, iodata) |
| 540 581 | end |
| 582 | + |
| 541 583 | defp socket_write(%{socket: socket}, iodata), do: :gen_tcp.send(socket, iodata) |
| 542 584 | |
| 543 | - defp add_subscription_to_state(%{receivers: receivers}=state, sid, pid, ref) do |
| 544 | - receivers = Map.put(receivers, sid, %{recipient: pid, monitor_ref: ref, unsub_after: :infinity}) |
| 585 | + defp add_subscription_to_state(%{receivers: receivers} = state, sid, pid, ref) do |
| 586 | + receivers = |
| 587 | + Map.put(receivers, sid, %{recipient: pid, monitor_ref: ref, unsub_after: :infinity}) |
| 588 | + |
| 545 589 | %{state | receivers: receivers} |
| 546 590 | end |
| 547 591 | |
| 548 | - defp cleanup_subscription_from_state(%{receivers: receivers}=state, sid, []) do |
| 592 | + defp cleanup_subscription_from_state(%{receivers: receivers} = state, sid, []) do |
| 549 593 | receivers = Map.delete(receivers, sid) |
| 550 594 | %{state | receivers: receivers} |
| 551 595 | end |
| 552 | - defp cleanup_subscription_from_state(%{receivers: receivers}=state, sid, [max_messages: n]) do |
| 596 | + |
| 597 | + defp cleanup_subscription_from_state(%{receivers: receivers} = state, sid, max_messages: n) do |
| 553 598 | receivers = put_in(receivers, [sid, :unsub_after], n) |
| 554 599 | %{state | receivers: receivers} |
| 555 600 | end |
| 556 601 | |
| 557 602 | defp process_message({:msg, topic, @request_sid, reply_to, body}, state) do |
| 558 603 | if Map.has_key?(state.request_receivers, topic) do |
| 559 | - send state.request_receivers[topic], {:msg, %{topic: topic, body: body, reply_to: reply_to, gnat: self()}} |
| 604 | + send( |
| 605 | + state.request_receivers[topic], |
| 606 | + {:msg, %{topic: topic, body: body, reply_to: reply_to, gnat: self()}} |
| 607 | + ) |
| 608 | + |
| 560 609 | state |
| 561 610 | else |
| 562 | - Logger.error "#{__MODULE__} got a response for a request, but that is no longer registered" |
| 611 | + Logger.error("#{__MODULE__} got a response for a request, but that is no longer registered") |
| 563 612 | state |
| 564 613 | end |
| 565 614 | end |
| 615 | + |
| 566 616 | defp process_message({:msg, topic, sid, reply_to, body}, state) do |
| 567 617 | unless is_nil(state.receivers[sid]) do |
| 568 618 | :telemetry.execute([:gnat, :message_received], %{count: 1}, %{topic: topic}) |
| 569 | - send state.receivers[sid].recipient, {:msg, %{topic: topic, body: body, reply_to: reply_to, sid: sid, gnat: self()}} |
| 619 | + |
| 620 | + send( |
| 621 | + state.receivers[sid].recipient, |
| 622 | + {:msg, %{topic: topic, body: body, reply_to: reply_to, sid: sid, gnat: self()}} |
| 623 | + ) |
| 624 | + |
| 570 625 | update_subscriptions_after_delivering_message(state, sid) |
| 571 626 | else |
| 572 | - Logger.error "#{__MODULE__} got message for sid #{sid}, but that is no longer registered" |
| 627 | + Logger.error("#{__MODULE__} got message for sid #{sid}, but that is no longer registered") |
| 573 628 | state |
| 574 629 | end |
| 575 630 | end |
| 576 | - defp process_message({:hmsg, topic, @request_sid, reply_to, status, description, headers, body}, state) do |
| 631 | + |
| 632 | + defp process_message( |
| 633 | + {:hmsg, topic, @request_sid, reply_to, status, description, headers, body}, |
| 634 | + state |
| 635 | + ) do |
| 577 636 | if Map.has_key?(state.request_receivers, topic) do |
| 578 637 | map = %{ |
| 579 638 | topic: topic, |
| @@ -584,16 +643,19 @@ defmodule Gnat do | |
| 584 643 | status: status, |
| 585 644 | description: description |
| 586 645 | } |
| 587 | - send state.request_receivers[topic], {:msg, map} |
| 646 | + |
| 647 | + send(state.request_receivers[topic], {:msg, map}) |
| 588 648 | state |
| 589 649 | else |
| 590 | - Logger.error "#{__MODULE__} got a response for a request, but that is no longer registered" |
| 650 | + Logger.error("#{__MODULE__} got a response for a request, but that is no longer registered") |
| 591 651 | state |
| 592 652 | end |
| 593 653 | end |
| 654 | + |
| 594 655 | defp process_message({:hmsg, topic, sid, reply_to, status, description, headers, body}, state) do |
| 595 656 | unless is_nil(state.receivers[sid]) do |
| 596 657 | :telemetry.execute([:gnat, :message_received], %{count: 1}, %{topic: topic}) |
| 658 | + |
| 597 659 | map = %{ |
| 598 660 | topic: topic, |
| 599 661 | body: body, |
| @@ -604,30 +666,36 @@ defmodule Gnat do | |
| 604 666 | status: status, |
| 605 667 | description: description |
| 606 668 | } |
| 607 | - send state.receivers[sid].recipient, {:msg, map} |
| 669 | + |
| 670 | + send(state.receivers[sid].recipient, {:msg, map}) |
| 608 671 | update_subscriptions_after_delivering_message(state, sid) |
| 609 672 | else |
| 610 | - Logger.error "#{__MODULE__} got message for sid #{sid}, but that is no longer registered" |
| 673 | + Logger.error("#{__MODULE__} got message for sid #{sid}, but that is no longer registered") |
| 611 674 | state |
| 612 675 | end |
| 613 676 | end |
| 677 | + |
| 614 678 | defp process_message(:ping, state) do |
| 615 679 | socket_write(state, "PONG\r\n") |
| 616 680 | state |
| 617 681 | end |
| 682 | + |
| 618 683 | defp process_message(:pong, state) do |
| 619 | - send state.pinger, :pong |
| 684 | + send(state.pinger, :pong) |
| 620 685 | state |
| 621 686 | end |
| 687 | + |
| 622 688 | defp process_message({:error, message}, state) do |
| 623 | - :error_logger.error_report([ |
| 689 | + :error_logger.error_report( |
| 624 690 | type: :gnat_error_from_broker, |
| 625 | - message: message, |
| 626 | - ]) |
| 691 | + message: message |
| 692 | + ) |
| 693 | + |
| 627 694 | state |
| 628 695 | end |
| 629 696 | |
| 630 697 | defp receive_additional_pubs(commands, froms, 0), do: {commands, froms} |
| 698 | + |
| 631 699 | defp receive_additional_pubs(commands, froms, how_many_more) do |
| 632 700 | receive do |
| 633 701 | {:"$gen_call", from, {:pub, topic, message, opts}} -> |
| @@ -640,21 +708,24 @@ defmodule Gnat do | |
| 640 708 | end |
| 641 709 | |
| 642 710 | defp receive_additional_tcp_data(_socket, packets, 0), do: Enum.reverse(packets) |
| 711 | + |
| 643 712 | defp receive_additional_tcp_data(socket, packets, n) do |
| 644 713 | receive do |
| 645 714 | {:tcp, ^socket, data} -> |
| 646 715 | receive_additional_tcp_data(socket, [data | packets], n - 1) |
| 647 | - after |
| 648 | - 0 -> Enum.reverse(packets) |
| 716 | + after |
| 717 | + 0 -> Enum.reverse(packets) |
| 649 718 | end |
| 650 719 | end |
| 651 720 | |
| 652 | - defp update_subscriptions_after_delivering_message(%{receivers: receivers}=state, sid) do |
| 653 | - receivers = case get_in(receivers, [sid, :unsub_after]) do |
| 654 | - :infinity -> receivers |
| 655 | - 1 -> Map.delete(receivers, sid) |
| 656 | - n -> put_in(receivers, [sid, :unsub_after], n - 1) |
| 657 | - end |
| 721 | + defp update_subscriptions_after_delivering_message(%{receivers: receivers} = state, sid) do |
| 722 | + receivers = |
| 723 | + case get_in(receivers, [sid, :unsub_after]) do |
| 724 | + :infinity -> receivers |
| 725 | + 1 -> Map.delete(receivers, sid) |
| 726 | + n -> put_in(receivers, [sid, :unsub_after], n - 1) |
| 727 | + end |
| 728 | + |
| 658 729 | %{state | receivers: receivers} |
| 659 730 | end |
| 660 731 | |
| @@ -662,15 +733,19 @@ defmodule Gnat do | |
| 662 733 | |
| 663 734 | defp receive_multi_request_responses(subscription, expiration, max_messages) do |
| 664 735 | timeout = expiration - :erlang.monotonic_time(:millisecond) |
| 736 | + |
| 665 737 | cond do |
| 666 738 | timeout < 1 -> |
| 667 739 | [] |
| 740 | + |
| 668 741 | true -> |
| 669 742 | case receive_request_response(subscription, timeout) do |
| 670 743 | {:error, :no_responders} -> |
| 671 744 | {:error, :no_responders} |
| 745 | + |
| 672 746 | {:error, :timeout} -> |
| 673 747 | [] |
| 748 | + |
| 674 749 | {:ok, msg} -> |
| 675 750 | [msg | receive_multi_request_responses(subscription, expiration, max_messages - 1)] |
| 676 751 | end |
| @@ -681,9 +756,11 @@ defmodule Gnat do | |
| 681 756 | receive do |
| 682 757 | {:msg, %{topic: ^subscription, status: "503"}} -> |
| 683 758 | {:error, :no_responders} |
| 684 | - {:msg, %{topic: ^subscription}=msg} -> |
| 759 | + |
| 760 | + {:msg, %{topic: ^subscription} = msg} -> |
| 685 761 | {:ok, msg} |
| 686 | - after timeout -> |
| 762 | + after |
| 763 | + timeout -> |
| 687 764 | {:error, :timeout} |
| 688 765 | end |
| 689 766 | end |
| @@ -34,7 +34,7 @@ defmodule Gnat.Jetstream.API.KV do | |
| 34 34 | |
| 35 35 | ## Examples |
| 36 36 | |
| 37 | - iex>{:ok, info} = Jetstream.API.KV.create_bucket(:gnat, "my_bucket") |
| 37 | + iex> {:ok, info} = Jetstream.API.KV.create_bucket(:gnat, "my_bucket") |
| 38 38 | """ |
| 39 39 | @spec create_bucket(conn :: Gnat.t(), bucket_name :: binary(), params :: [bucket_options()]) :: |
| 40 40 | {:ok, Stream.info()} | {:error, any()} |
| @@ -75,7 +75,7 @@ defmodule Gnat.Jetstream.API.KV do | |
| 75 75 | |
| 76 76 | ## Examples |
| 77 77 | |
| 78 | - iex>:ok = Jetstream.API.KV.delete_bucket(:gnat, "my_bucket") |
| 78 | + iex> :ok = Jetstream.API.KV.delete_bucket(:gnat, "my_bucket") |
| 79 79 | """ |
| 80 80 | @spec delete_bucket(conn :: Gnat.t(), bucket_name :: binary()) :: :ok | {:error, any()} |
| 81 81 | def delete_bucket(conn, bucket_name) do |
| @@ -85,9 +85,13 @@ defmodule Gnat.Jetstream.API.KV do | |
| 85 85 | @doc """ |
| 86 86 | Create a Key in a Key/Value Bucket |
| 87 87 | |
| 88 | + ## Options |
| 89 | + |
| 90 | + * `:timeout` - receive timeout for the request |
| 91 | + |
| 88 92 | ## Examples |
| 89 93 | |
| 90 | - iex>:ok = Jetstream.API.KV.create_key(:gnat, "my_bucket", "my_key", "my_value") |
| 94 | + iex> :ok = Jetstream.API.KV.create_key(:gnat, "my_bucket", "my_key", "my_value") |
| 91 95 | """ |
| 92 96 | @spec create_key(conn :: Gnat.t(), bucket_name :: binary(), key :: binary(), value :: binary()) :: |
| 93 97 | :ok | {:error, any()} |
| @@ -107,7 +111,7 @@ defmodule Gnat.Jetstream.API.KV do | |
| 107 111 | |
| 108 112 | ## Examples |
| 109 113 | |
| 110 | - iex>:ok = Jetstream.API.KV.delete_key(:gnat, "my_bucket", "my_key") |
| 114 | + iex> :ok = Jetstream.API.KV.delete_key(:gnat, "my_bucket", "my_key") |
| 111 115 | """ |
| 112 116 | @spec delete_key(conn :: Gnat.t(), bucket_name :: binary(), key :: binary()) :: |
| 113 117 | :ok | {:error, any()} |
| @@ -131,7 +135,7 @@ defmodule Gnat.Jetstream.API.KV do | |
| 131 135 | |
| 132 136 | ## Examples |
| 133 137 | |
| 134 | - iex>:ok = Jetstream.API.KV.purge_key(:gnat, "my_bucket", "my_key") |
| 138 | + iex> :ok = Jetstream.API.KV.purge_key(:gnat, "my_bucket", "my_key") |
| 135 139 | """ |
| 136 140 | @spec purge_key(conn :: Gnat.t(), bucket_name :: binary(), key :: binary()) :: |
| 137 141 | :ok | {:error, any()} |
| @@ -155,7 +159,7 @@ defmodule Gnat.Jetstream.API.KV do | |
| 155 159 | |
| 156 160 | ## Examples |
| 157 161 | |
| 158 | - iex>:ok = Jetstream.API.KV.put_value(:gnat, "my_bucket", "my_key", "my_value") |
| 162 | + iex> :ok = Jetstream.API.KV.put_value(:gnat, "my_bucket", "my_key", "my_value") |
| 159 163 | """ |
| 160 164 | @spec put_value(conn :: Gnat.t(), bucket_name :: binary(), key :: binary(), value :: binary()) :: |
| 161 165 | :ok | {:error, any()} |
| @@ -175,7 +179,7 @@ defmodule Gnat.Jetstream.API.KV do | |
| 175 179 | |
| 176 180 | ## Examples |
| 177 181 | |
| 178 | - iex>"my_value" = Jetstream.API.KV.get_value(:gnat, "my_bucket", "my_key") |
| 182 | + iex> "my_value" = Jetstream.API.KV.get_value(:gnat, "my_bucket", "my_key") |
| 179 183 | """ |
| 180 184 | @spec get_value(conn :: Gnat.t(), bucket_name :: binary(), key :: binary()) :: |
| 181 185 | binary() | {:error, any()} | nil |
| @@ -193,7 +197,7 @@ defmodule Gnat.Jetstream.API.KV do | |
| 193 197 | |
| 194 198 | ## Examples |
| 195 199 | |
| 196 | - iex>{:ok, %{"key1" => "value1"}} = Jetstream.API.KV.contents(:gnat, "my_bucket") |
| 200 | + iex> {:ok, %{"key1" => "value1"}} = Jetstream.API.KV.contents(:gnat, "my_bucket") |
| 197 201 | """ |
| 198 202 | @spec contents(conn :: Gnat.t(), bucket_name :: binary(), domain :: nil | binary()) :: |
| 199 203 | {:ok, map()} | {:error, binary()} |
| @@ -228,12 +232,16 @@ defmodule Gnat.Jetstream.API.KV do | |
| 228 232 | |
| 229 233 | ## Examples |
| 230 234 | |
| 231 | - iex>{:ok, _pid} = Jetstream.API.KV.watch(:gnat, "my_bucket", fn action, key, value -> |
| 232 | - IO.puts("#{action} taken on #{key}") |
| 233 | - end) |
| 235 | + iex> {:ok, _pid} = Jetstream.API.KV.watch(:gnat, "my_bucket", fn action, key, value -> |
| 236 | + ...> IO.puts("#{action} taken on #{key}") |
| 237 | + ...> end) |
| 234 238 | """ |
| 235 239 | def watch(conn, bucket_name, handler) do |
| 236 | - Gnat.Jetstream.API.KV.Watcher.start_link(conn: conn, bucket_name: bucket_name, handler: handler) |
| 240 | + Gnat.Jetstream.API.KV.Watcher.start_link( |
| 241 | + conn: conn, |
| 242 | + bucket_name: bucket_name, |
| 243 | + handler: handler |
| 244 | + ) |
| 237 245 | end |
| 238 246 | |
| 239 247 | @doc ~S""" |
| @@ -242,7 +250,7 @@ defmodule Gnat.Jetstream.API.KV do | |
| 242 250 | |
| 243 251 | ## Examples |
| 244 252 | |
| 245 | - iex>:ok = Jetstream.API.KV.unwatch(pid) |
| 253 | + iex> :ok = Jetstream.API.KV.unwatch(pid) |
| 246 254 | """ |
| 247 255 | def unwatch(pid) do |
| 248 256 | Gnat.Jetstream.API.KV.Watcher.stop(pid) |
| @@ -265,14 +273,20 @@ defmodule Gnat.Jetstream.API.KV do | |
| 265 273 | end |
| 266 274 | end |
| 267 275 | |
| 276 | + @spec is_kv_bucket_stream?(stream_name :: binary()) :: boolean() |
| 277 | + @deprecated "Use Gnat.Jetstream.API.KV.kv_bucket_stream?/1 instead" |
| 278 | + def is_kv_bucket_stream?(stream_name) do |
| 279 | + kv_bucket_stream?(stream_name) |
| 280 | + end |
| 281 | + |
| 268 282 | @doc """ |
| 269 283 | Returns true if the provided stream is a KV bucket, false otherwise |
| 270 284 | |
| 271 285 | ## Parameters |
| 272 286 | * `stream_name` - the stream name to test |
| 273 287 | """ |
| 274 | - @spec is_kv_bucket_stream?(stream_name :: binary()) :: boolean() |
| 275 | - def is_kv_bucket_stream?(stream_name) do |
| 288 | + @spec kv_bucket_stream?(stream_name :: binary()) :: boolean() |
| 289 | + def kv_bucket_stream?(stream_name) do |
| 276 290 | String.starts_with?(stream_name, "KV_") |
| 277 291 | end |
| 278 292 | |
| @@ -285,12 +299,13 @@ defmodule Gnat.Jetstream.API.KV do | |
| 285 299 | stream_names = |
| 286 300 | streams |
| 287 301 | |> Enum.flat_map(fn bucket -> |
| 288 | - if is_kv_bucket_stream?(bucket) do |
| 289 | - [bucket |> String.trim_leading(@stream_prefix)] |
| 302 | + if kv_bucket_stream?(bucket) do |
| 303 | + [bucket |> String.trim_leading(@stream_prefix)] |
| 290 304 | else |
| 291 | - [] |
| 305 | + [] |
| 292 306 | end |
| 293 307 | end) |
| 308 | + |
| 294 309 | {:ok, stream_names} |
| 295 310 | else |
| 296 311 | {:error, reason} -> |
| @@ -298,7 +313,6 @@ defmodule Gnat.Jetstream.API.KV do | |
| 298 313 | end |
| 299 314 | end |
| 300 315 | |
| 301 | - |
| 302 316 | @doc false |
| 303 317 | def stream_name(bucket_name) do |
| 304 318 | "#{@stream_prefix}#{bucket_name}" |
| @@ -2,7 +2,7 @@ defmodule Gnat.Mixfile do | |
| 2 2 | use Mix.Project |
| 3 3 | |
| 4 4 | @source_url "https://github.com/nats-io/nats.ex" |
| 5 | - @version "1.9.0" |
| 5 | + @version "1.9.1" |
| 6 6 | |
| 7 7 | def project do |
| 8 8 | [ |
| @@ -29,7 +29,7 @@ defmodule Gnat.Mixfile do | |
| 29 29 | {:benchee, "~> 1.0", only: :dev}, |
| 30 30 | {:cowlib, "~> 2.0"}, |
| 31 31 | {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}, |
| 32 | - {:ex_doc, "~> 0.27.3", only: :dev}, |
| 32 | + {:ex_doc, "~> 0.36", only: :dev}, |
| 33 33 | {:jason, "~> 1.1"}, |
| 34 34 | {:connection, "~> 1.1"}, |
| 35 35 | {:nimble_parsec, "~> 0.5 or ~> 1.0"}, |