Current section

65 Versions

Jump to

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"},