Skip to content

Commit

Permalink
Use gun instead of Websockex. (#150)
Browse files Browse the repository at this point in the history
* Drop WebSockex, use :gun.

* Use `:gun` instead of WebsockEx.

* Correct typespec.

* Update `mix.lock` to new format.

* Update lockfile.

* Test on latest Elixir & OTP.

* Use `:gun.await_up/2` instead of custom receive block.

* Add upgrading note.

* Make credo happy for now.

* Do not block startup.

* Reconnect on close frame.

* Use new representation for datetimes.

* Increase timeouts.

* Properly display reconnection.

* Handle parrots.

* Format the parrots.

* Handle unhandled messages.

* Read mails at the proper time.

* Clear `:gun_down` on WS close.

* Let gun reconnect.

* Add `fullsweep_after` spawn option.

* Log websocket closes.

* Drop extra `connect/1`.

* Fix someone else's mess.

* Use present tense.

* Add voice state update functions.

* Add missing ).

* Remove dots....

* Only warn on heartbeat timeout.

* Cancel timer on `:gun_down`.

* Send a close frame on `gun_down`.

* Close on heartbeat timeout.

* Drop periods, add `disconnecting`.
  • Loading branch information
jchristgit authored and Kraigie committed Aug 11, 2019
1 parent 4a6642d commit 0e46342
Show file tree
Hide file tree
Showing 11 changed files with 136 additions and 98 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
language: elixir

elixir:
- '1.7.4'
otp_release: '20.3'
- '1.9.0'
otp_release: '22.0'

script:
- mix credo --strict
Expand Down
7 changes: 5 additions & 2 deletions lib/nostrum/shard/event.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ defmodule Nostrum.Shard.Event do
end

def handle(:hello, payload, state) do
state = %{state | heartbeat_interval: payload.d.heartbeat_interval}
state = %{
state
| heartbeat_interval: payload.d.heartbeat_interval
}

WebSockex.cast(state.conn_pid, :heartbeat)
GenServer.cast(state.conn_pid, :heartbeat)

if session_exists?(state) do
Logger.info("RESUMING")
Expand Down
4 changes: 3 additions & 1 deletion lib/nostrum/shard/payload.ex
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ defmodule Nostrum.Shard.Payload do

defp build_payload(data, opcode_name) do
opcode = Constants.opcode_from_name(opcode_name)
%{"op" => opcode, "d" => data} |> :erlang.term_to_binary()

%{"op" => opcode, "d" => data}
|> :erlang.term_to_binary()
end
end
157 changes: 94 additions & 63 deletions lib/nostrum/shard/session.ex
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
defmodule Nostrum.Shard.Session do
@moduledoc false

use WebSockex

alias Nostrum.{Constants, Util}
alias Nostrum.Shard.{Connector, Event, Payload}
alias Nostrum.Struct.WSState

require Logger

use GenServer

@gateway_qs "/?compress=zlib-stream&encoding=etf&v=6"

# Maximum time the initial connection may take, in milliseconds.
@timeout_connect 10_000
# Maximum time the websocket upgrade may take, in milliseconds.
@timeout_ws_upgrade 10_000

def update_status(pid, status, game, stream, type) do
{idle_since, afk} =
case status do
Expand All @@ -22,55 +27,75 @@ defmodule Nostrum.Shard.Session do
end

payload = Payload.status_update_payload(idle_since, game, stream, status, afk, type)
WebSockex.cast(pid, {:status_update, payload})
GenServer.cast(pid, {:status_update, payload})
end

def update_voice_state(pid, guild_id, channel_id, self_mute, self_deaf) do
payload = Payload.update_voice_state_payload(guild_id, channel_id, self_mute, self_deaf)
WebSockex.cast(pid, {:update_voice_state, payload})
GenServer.cast(pid, {:update_voice_state, payload})
end

def request_guild_members(pid, guild_id, limit \\ 0) do
payload = Payload.request_members_payload(guild_id, limit)
WebSockex.cast(pid, {:request_guild_members, payload})
GenServer.cast(pid, {:request_guild_members, payload})
end

def start_link([gateway, shard_num]) do
GenServer.start_link(__MODULE__, [gateway, shard_num], spawn_opt: [Util.fullsweep_after()])
end

def init([_gateway, _shard_num] = args) do
{:ok, nil, {:continue, args}}
end

def handle_continue([gateway, shard_num], nil) do
Connector.block_until_connect()
Logger.metadata(shard: shard_num)

{:ok, worker} = :gun.open(:binary.bin_to_list(gateway), 443, %{protocols: [:http]})
{:ok, :http} = :gun.await_up(worker, @timeout_connect)
stream = :gun.ws_upgrade(worker, @gateway_qs)
await_ws_upgrade(worker, stream)

zlib_context = :zlib.open()
:zlib.inflateInit(zlib_context)

state = %WSState{
conn_pid: self(),
conn: worker,
shard_num: shard_num,
gateway: gateway <> @gateway_qs,
last_heartbeat_ack: DateTime.utc_now(),
heartbeat_ack: true
heartbeat_ack: true,
zlib_ctx: zlib_context
}

Connector.block_until_connect()
Logger.debug(fn -> "Websocket connection up on worker #{inspect(worker)}" end)

# TODO: Add support for `spawn_opt` start arguments to WebSockex, this does nothing until then.
WebSockex.start_link(
gateway <> @gateway_qs,
__MODULE__,
state,
spawn_opt: [Util.fullsweep_after()]
)
{:noreply, state}
end

def handle_connect(conn, state) do
Logger.metadata(shard: state.shard_num)
defp await_ws_upgrade(worker, stream) do
# TODO: Once gun 2.0 is released, the block below can be simplified to:
# {:upgrade, [<<"websocket">>], _headers} = :gun.await(worker, stream, @timeout_ws_upgrade)

zlib_ctx = :zlib.open()
:zlib.inflateInit(zlib_ctx)
receive do
{:gun_upgrade, ^worker, ^stream, [<<"websocket">>], _headers} ->
:ok

{:ok,
%{
state
| conn: conn,
conn_pid: self(),
zlib_ctx: zlib_ctx,
heartbeat_ack: true
}}
{:gun_error, ^worker, ^stream, reason} ->
exit({:ws_upgrade_failed, reason})
after
@timeout_ws_upgrade ->
Logger.error(fn ->
"Cannot upgrade connection to Websocket after #{@timeout_ws_upgrade / 1000} seconds"
end)

exit(:timeout)
end
end

def handle_frame({:binary, frame}, state) do
def handle_info({:gun_ws, _worker, _stream, {:binary, frame}}, state) do
payload =
state.zlib_ctx
|> :zlib.inflate(frame)
Expand All @@ -86,63 +111,69 @@ defmodule Nostrum.Shard.Session do

case from_handle do
{new_state, reply} ->
{:reply, {:binary, reply}, new_state}
:ok = :gun.ws_send(state.conn, {:binary, reply})
{:noreply, new_state}

new_state ->
{:ok, new_state}
{:noreply, new_state}
end
end

def handle_info({:gun_ws, _conn, _stream, {:close, errno, reason}}, state) do
Logger.info("Shard websocket closed (errno #{errno}, reason #{inspect(reason)})")
{:noreply, state}
end

def handle_info(
{:gun_down, _conn, _proto, _reason, _killed_streams, _unprocessed_streams},
state
) do
# Try to cancel the internal timer, but
# do not explode if it was already cancelled.
:timer.cancel_ref(state.timer_ref)
{:noreply, state}
end

def handle_info({:gun_up, worker, _proto}, state) do
:ok = :zlib.inflateReset(state.zlib_ctx)
stream = :gun.ws_upgrade(worker, @gateway_qs)
await_ws_upgrade(worker, stream)
Logger.warn("Reconnected after connection broke")
{:noreply, state}
end

def handle_cast({:status_update, payload}, state) do
{:reply, {:binary, payload}, state}
:ok = :gun.ws_send(state.conn, {:binary, payload})
{:noreply, state}
end

def handle_cast({:update_voice_state, payload}, state) do
{:reply, {:binary, payload}, state}
:ok = :gun.ws_send(state.conn, {:binary, payload})
{:noreply, state}
end

def handle_cast({:request_guild_members, payload}, state) do
{:reply, {:binary, payload}, state}
:ok = :gun.ws_send(state.conn, {:binary, payload})
{:noreply, state}
end

def handle_cast(:heartbeat, %{heartbeat_ack: false} = state) do
Logger.warn("heartbeat_ack not received in time, disconnecting")
{:close, state}
{:ok, :cancel} = :timer.cancel_ref(state.timer_ref)
:gun.ws_send(state.conn, :close)
{:noreply, state}
end

def handle_cast(:heartbeat, state) do
{:ok, ref} =
:timer.apply_after(state.heartbeat_interval, WebSockex, :cast, [state.conn_pid, :heartbeat])

{:reply, {:binary, Payload.heartbeat_payload(state.seq)},
%{state | heartbeat_ref: ref, heartbeat_ack: false, last_heartbeat_send: DateTime.utc_now()}}
end

def handle_disconnect(%{reason: reason}, state) when is_tuple(reason) do
Logger.warn(fn ->
"websocket disconnected with reason #{inspect(reason)}, attempting reconnect"
end)
:timer.apply_after(state.heartbeat_interval, :gen_server, :cast, [
state.conn_pid,
:heartbeat
])

:timer.cancel(state.heartbeat_ref)
:ok = :gun.ws_send(state.conn, {:binary, Payload.heartbeat_payload(state.seq)})

{:reconnect, state}
end

def handle_disconnect(%{reason: reason}, state) do
Logger.warn(fn ->
"websocket errored with reason #{inspect(reason)}, attempting reconnect"
end)

:timer.cancel(state.heartbeat_ref)

{:reconnect, state}
end

def terminate(reason, state) do
:timer.cancel(state.heartbeat_ref)

Logger.warn(fn ->
"websocket closed with reason #{inspect(reason)}"
end)
{:noreply,
%{state | heartbeat_ref: ref, heartbeat_ack: false, last_heartbeat_send: DateTime.utc_now()}}
end
end
3 changes: 2 additions & 1 deletion lib/nostrum/shard/supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ defmodule Nostrum.Shard.Supervisor do

use Supervisor

alias Nostrum.{Shard, Util}
alias Nostrum.Cache.Mapping.GuildShard
alias Nostrum.Error.CacheError
alias Nostrum.Shard
alias Nostrum.Shard.Session
alias Nostrum.Shard.Stage.{Cache, Producer}
alias Nostrum.Util

require Logger

Expand Down
2 changes: 1 addition & 1 deletion lib/nostrum/snowflake.ex
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ defmodule Nostrum.Snowflake do
```Elixir
iex> Nostrum.Snowflake.creation_time(177888205536886784)
#DateTime<2016-05-05 21:04:13.203Z>
~U[2016-05-05 21:04:13.203Z]
```
"""
@spec creation_time(t) :: DateTime.t()
Expand Down
4 changes: 2 additions & 2 deletions lib/nostrum/struct/ws_state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ defmodule Nostrum.Struct.WSState do
@typedoc "PID of the shard containing this state"
@type shard_pid :: pid

@typedoc "Websockex connection state map"
@type conn :: map
@typedoc "PID of the `:gun` worker connected to the websocket"
@type conn :: pid

@typedoc "PID of the connection process"
@type conn_pid :: pid
Expand Down
2 changes: 1 addition & 1 deletion lib/nostrum/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ defmodule Nostrum.Util do
{:ok, body} ->
body = Poison.decode!(body)

url = body["url"]
"wss://" <> url = body["url"]
shards = if body["shards"], do: body["shards"], else: 1

:ets.insert(:gateway_url, {"url", url, shards})
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ defmodule Nostrum.Mixfile do
[
{:httpoison, "~> 1.5"},
{:poison, "~> 3.0"},
{:gun, "~> 1.3"},
{:ex_doc, "~> 0.14", only: :dev},
{:credo, "~> 0.4", only: [:dev, :test]},
{:dialyxir, "~> 0.5", only: [:dev], runtime: false},
{:websockex, "~> 0.4"},
{:gen_stage, "~> 0.11"},
{:recon, "~> 2.3", only: :dev}
]
Expand Down
45 changes: 23 additions & 22 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
%{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []},
"certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, optional: false]}]},
"credo": {:hex, :credo, "0.10.2", "03ad3a1eff79a16664ed42fc2975b5e5d0ce243d69318060c626c34720a49512", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, optional: false]}, {:jason, "~> 1.0", [hex: :jason, optional: false]}]},
"dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], []},
"earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], []},
"ex_doc": {:hex, :ex_doc, "0.19.2", "6f4081ccd9ed081b6dc0bd5af97a41e87f5554de469e7d76025fba535180565f", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, optional: false]}]},
"gen_stage": {:hex, :gen_stage, "0.14.1", "9d46723fda072d4f4bb31a102560013f7960f5d80ea44dcb96fd6304ed61e7a4", [:mix], []},
"hackney": {:hex, :hackney, "1.15.0", "287a5d2304d516f63e56c469511c42b016423bcb167e61b611f6bad47e3ca60e", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, optional: false]}, {:idna, "6.0.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, optional: false]}]},
"httpoison": {:hex, :httpoison, "1.5.0", "71ae9f304bdf7f00e9cd1823f275c955bdfc68282bc5eb5c85c3a9ade865d68e", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, optional: false]}]},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, optional: false]}]},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, optional: true]}]},
"makeup": {:hex, :makeup, "0.6.0", "e0fd985525e8d42352782bd76253105fbab0a783ac298708ca9020636c9568af", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, optional: false]}]},
"makeup_elixir": {:hex, :makeup_elixir, "0.11.0", "aa3446f67356afa5801618867587a8863f176f9c632fb62b20f49bd1ea335e8a", [:mix], [{:makeup, "~> 0.6", [hex: :makeup, optional: false]}]},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], []},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], []},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []},
"recon": {:hex, :recon, "2.4.0", "901ff78b39c754fb4d6fd72dcf0dbd398967bbd2e4d59c08d4d7aa44a73de91d", [:rebar3], []},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], []},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], []},
"websockex": {:hex, :websockex, "0.4.2", "9a3b7dc25655517ecd3f8ff7109a77fce94956096b942836cdcfbc7c86603ecc", [:mix], []},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "2.6.0", "8aa629f81a0fc189f261dc98a42243fa842625feea3c7ec56c48f4ccdb55490f", [:rebar3], [], "hexpm"},
"credo": {:hex, :credo, "0.10.2", "03ad3a1eff79a16664ed42fc2975b5e5d0ce243d69318060c626c34720a49512", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"gen_stage": {:hex, :gen_stage, "0.14.2", "6a2a578a510c5bfca8a45e6b27552f613b41cf584b58210f017088d3d17d0b14", [:mix], [], "hexpm"},
"gun": {:hex, :gun, "1.3.0", "18e5d269649c987af95aec309f68a27ffc3930531dd227a6eaa0884d6684286e", [:rebar3], [{:cowlib, "~> 2.6.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm"},
"hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"httpoison": {:hex, :httpoison, "1.5.1", "0f55b5b673b03c5c327dac7015a67cb571b99b631acc0bc1b0b98dcd6b9f2104", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
"recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
}
4 changes: 2 additions & 2 deletions test/nostrum/snowflake_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ defmodule Nostrum.SnowflakeTest do
min_datetime = Snowflake.creation_time(0)
max_datetime = Snowflake.creation_time(0xFFFFFFFFFFFFFFFF)

assert(inspect(min_datetime) === "#DateTime<2015-01-01 00:00:00.000Z>")
assert(inspect(max_datetime) === "#DateTime<2154-05-15 07:35:11.103Z>")
assert(inspect(min_datetime) === "~U[2015-01-01 00:00:00.000Z]")
assert(inspect(max_datetime) === "~U[2154-05-15 07:35:11.103Z]")
end
end
end

0 comments on commit 0e46342

Please # to comment.