From 8796f431796cfd7f62f250e4c57fe29160b73d76 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 15 Sep 2026 14:36:30 -0400 Subject: [PATCH 1/3] chore: support only the last three Elixir and OTP releases Carrying releases that upstream no longer patches costs CI time and blocks adopting anything newer than Elixir 1.11, while leaving us on a Postgrex with two open SQL injection advisories. The `fsm` dependency is vendored because it has had no release since 2017 and its generated code cannot pass the current type checker from inside `deps/`. Signed-off-by: Yordis Prieto --- .github/workflows/test.yml | 17 +- .tool-versions | 2 - config/bench.exs | 2 +- config/jsonb.exs | 2 +- config/migration.exs | 2 +- config/test.exs | 2 +- config/text_ids.exs | 2 +- lib/event_store/fsm.ex | 175 ++++++++++++++++++ .../sql/statements/insert_events.sql.eex | 30 +-- lib/event_store/storage/snapshot.ex | 9 +- .../subscriptions/subscription_fsm.ex | 8 +- mise.toml | 3 + mix.exs | 18 +- mix.lock | 29 ++- test/event_store_test.exs | 8 +- .../notifications_supervisor_test.exs | 7 +- .../subscribe_to_stream_test.exs | 7 +- 17 files changed, 256 insertions(+), 67 deletions(-) delete mode 100644 .tool-versions create mode 100644 lib/event_store/fsm.ex create mode 100644 mise.toml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f9df5250..746e18fd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,18 +14,12 @@ jobs: strategy: matrix: include: - - elixir: 1.17.x + - elixir: 1.20.x + otp: 29 + - elixir: 1.19.x + otp: 28 + - elixir: 1.18.x otp: 27 - - elixir: 1.16.x - otp: 26 - - elixir: 1.15.x - otp: 26 - - elixir: 1.14.x - otp: 26 - check_formatted: "ignore" - - elixir: 1.13.x - otp: 25 - check_formatted: "ignore" services: postgres: @@ -61,7 +55,6 @@ jobs: run: mix deps.get - name: Check formatting - if: ${{ matrix.check_formatted != 'ignore' }} run: mix format --check-formatted - name: Setup EventStore test databases diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index 1a5e6c89..00000000 --- a/.tool-versions +++ /dev/null @@ -1,2 +0,0 @@ -elixir 1.16.0-otp-26 -erlang 26.2.1 \ No newline at end of file diff --git a/config/bench.exs b/config/bench.exs index f5ce46da..b787dbfd 100644 --- a/config/bench.exs +++ b/config/bench.exs @@ -1,7 +1,7 @@ import Config # no logging for benchmarking -config :logger, backends: [] +config :logger, :default_handler, false config :ex_unit, assert_receive_timeout: 2_000, diff --git a/config/jsonb.exs b/config/jsonb.exs index 6b7bf9b3..f9f558ef 100644 --- a/config/jsonb.exs +++ b/config/jsonb.exs @@ -1,6 +1,6 @@ import Config -config :logger, backends: [] +config :logger, :default_handler, false config :ex_unit, capture_log: true, diff --git a/config/migration.exs b/config/migration.exs index c433d139..a9ac443a 100644 --- a/config/migration.exs +++ b/config/migration.exs @@ -1,6 +1,6 @@ import Config -config :logger, backends: [] +config :logger, :default_handler, false config :ex_unit, capture_log: true, diff --git a/config/test.exs b/config/test.exs index 5fe9f4b9..4cd36720 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,6 +1,6 @@ import Config -config :logger, backends: [] +config :logger, :default_handler, false config :ex_unit, capture_log: [level: :warning], diff --git a/config/text_ids.exs b/config/text_ids.exs index 21aaef93..56e14bb9 100644 --- a/config/text_ids.exs +++ b/config/text_ids.exs @@ -1,6 +1,6 @@ import Config -config :logger, backends: [] +config :logger, :default_handler, false config :ex_unit, capture_log: true, diff --git a/lib/event_store/fsm.ex b/lib/event_store/fsm.ex new file mode 100644 index 00000000..26f82d39 --- /dev/null +++ b/lib/event_store/fsm.ex @@ -0,0 +1,175 @@ +defmodule EventStore.Fsm do + @moduledoc false + + # Vendored from the `fsm` package (v0.3.1, MIT, Copyright (c) 2013 Saša Jurić) + # because upstream is unmaintained and its generated code does not compile + # cleanly under the Elixir type checker. + + defmacro __using__(opts) do + quote do + import EventStore.Fsm + + defstruct state: unquote(opts[:initial_state]), + data: unquote(opts[:initial_data]) + + @declaring_state nil + @declared_events MapSet.new() + + def new(params \\ []), do: struct!(__MODULE__, params) + + def state(%__MODULE__{state: state}), do: state + def data(%__MODULE__{data: data}), do: data + + defp change_state(%__MODULE__{} = fsm, {:action_responses, responses}), + do: parse_action_responses(fsm, responses) + + defp parse_action_responses(%__MODULE__{} = fsm, responses) do + Enum.reduce(responses, fsm, fn response, fsm -> + handle_action_response(fsm, response) + end) + end + + defp handle_action_response(%__MODULE__{} = fsm, {:next_state, next_state}) do + %__MODULE__{fsm | state: next_state} + end + + defp handle_action_response(%__MODULE__{} = fsm, {:new_data, new_data}) do + %__MODULE__{fsm | data: new_data} + end + + defp handle_action_response(%__MODULE__{} = fsm, {:respond, response}) do + {response, fsm} + end + end + end + + def next_state(state), do: {:action_responses, [next_state: state]} + def next_state(state, data), do: {:action_responses, [next_state: state, new_data: data]} + + def respond(response), do: {:action_responses, [respond: response]} + def respond(response, state), do: {:action_responses, [next_state: state, respond: response]} + + def respond(response, state, data), + do: {:action_responses, [next_state: state, new_data: data, respond: response]} + + defmacro defstate(state, state_def) do + quote do + state_name = + case unquote(Macro.escape(state, unquote: true)) do + name when is_atom(name) -> name + {name, _, _} -> name + end + + @declaring_state state_name + unquote(state_def) + @declaring_state nil + end + end + + defmacro defevent(event, opts) do + do_defevent(event, opts, opts[:do]) + end + + defmacro defevent(event, opts, do: event_def) do + do_defevent(event, opts, event_def) + end + + defmacro defeventp(event, opts) do + do_defevent(event, [{:private, true} | opts], opts[:do]) + end + + defmacro defeventp(event, opts, do: event_def) do + do_defevent(event, [{:private, true} | opts], event_def) + end + + defp do_defevent(event_decl, opts, event_def) do + quote do + unquote(extract_args(event_decl, opts, event_def)) + unquote(define_interface()) + unquote(implement_transition()) + end + end + + defp extract_args(event_decl, opts, event_def) do + quote do + {event_name, args} = + case unquote(Macro.escape(event_decl, unquote: true)) do + :_ -> {:_, []} + name when is_atom(name) -> {name, []} + {name, _, args} -> {name, args || []} + end + + private = unquote(opts[:private]) + state_arg = unquote(Macro.escape(opts[:state] || quote(do: _), unquote: true)) + data_arg = unquote(Macro.escape(opts[:data] || quote(do: _), unquote: true)) + event_arg = unquote(Macro.escape(opts[:event] || quote(do: _), unquote: true)) + args_arg = unquote(Macro.escape(opts[:args] || quote(do: _), unquote: true)) + event_def = unquote(Macro.escape(event_def, unquote: true)) + guard = unquote(Macro.escape(opts[:when])) + end + end + + defp define_interface do + quote bind_quoted: [] do + unless event_name == :_ or MapSet.member?(@declared_events, {event_name, length(args)}) do + interface_args = + Enum.reduce(args, {0, []}, fn _, {index, args} -> + { + index + 1, + [{:"arg#{index}", [], nil} | args] + } + end) + |> elem(1) + |> Enum.reverse() + + body = + quote do + transition(fsm, unquote(event_name), [unquote_splicing(interface_args)]) + end + + interface_args = [quote(do: fsm) | interface_args] + + if private do + defp unquote(event_name)(unquote_splicing(interface_args)), do: unquote(body) + else + def unquote(event_name)(unquote_splicing(interface_args)), do: unquote(body) + end + + @declared_events MapSet.put(@declared_events, {event_name, length(args)}) + end + end + end + + defp implement_transition do + quote bind_quoted: [] do + transition_args = [ + if @declaring_state do + quote do + %__MODULE__{ + state: unquote(@declaring_state) = unquote(state_arg), + data: unquote(data_arg) + } = fsm + end + else + quote do + %__MODULE__{state: unquote(state_arg), data: unquote(data_arg)} = fsm + end + end, + quote do + unquote(if event_name == :_, do: quote(do: _), else: event_name) = unquote(event_arg) + end, + quote do + unquote(if event_name == :_, do: quote(do: _), else: args) = unquote(args_arg) + end + ] + + body = quote(do: change_state(fsm, unquote(event_def))) + + if guard do + def transition(unquote_splicing(transition_args)) when unquote(guard), do: unquote(body) + else + def transition(unquote_splicing(transition_args)), do: unquote(body) + end + end + end +end diff --git a/lib/event_store/sql/statements/insert_events.sql.eex b/lib/event_store/sql/statements/insert_events.sql.eex index fa830f62..2348b5c9 100644 --- a/lib/event_store/sql/statements/insert_events.sql.eex +++ b/lib/event_store/sql/statements/insert_events.sql.eex @@ -1,4 +1,4 @@ -<% +<%!-- # Elixir template variables: # schema - string # stream_id - integer @@ -18,15 +18,15 @@ # 9 - created_at - timestamp # 10 - index - integer # 11 - stream_version - integer -%> +--%> WITH - <% + <%!-- # create a table variable with: # event_id - uuid - the id for the new event # index - integer - the increase in the stream version for any stream it is linked to # stream_version - integer - the final stream version after all of the events have been inserted - %> + --%> new_events_indexes (event_id, index, stream_version) AS ( VALUES <%= for i <- 0..(number_of_events - 1) do %> @@ -35,11 +35,11 @@ WITH <% end %> ), events AS ( - <% + <%!-- # insert the new events into the events table # using the 7 bind variables from 3 to 9 inclusive # n.b.: the bind for the event_id is re-generated here - %> + --%> INSERT INTO "<%= schema %>".events ( event_id, @@ -57,7 +57,7 @@ WITH <% end %> ), stream AS ( - <% # Increase the version to the stream version given %> + <%!-- Increase the version to the stream version given --%> <%= cond do %> <% stream_id -> %> UPDATE "<%= schema %>".streams @@ -65,10 +65,10 @@ WITH WHERE stream_id = $1::bigint returning stream_id <% created_at -> %> - <% + <%!-- # the event created_at date has been provided as the last bind variable # use that instead of generating one - %> + --%> INSERT INTO "<%= schema %>".streams (stream_uuid, stream_version, created_at) VALUES ($1, $2::bigint, $<%= number_of_events*9 + 3 %>) returning stream_id @@ -79,14 +79,14 @@ WITH <% end %> ), source_stream_events AS ( - <% + <%!-- # link the new events into it's source stream # we're using the passed in event_ids rather than reading/joining from tables # each insert uses the stream_version calculated for the corresponding event # we're joining here, so we'll get the product of: # the stream (1) # the rows in the table variable (number_of_events) - %> + --%> INSERT INTO "<%= schema %>".stream_events ( event_id, @@ -104,21 +104,21 @@ WITH FROM new_events_indexes, stream ), linked_stream AS ( - <% + <%!-- # Update the all streams version by the number of events # This is the value of the expected version at append time + the number of events # Returns the version before the update - %> + --%> UPDATE "<%= schema %>".streams SET stream_version = stream_version + $2::bigint WHERE stream_id = 0 RETURNING stream_version - $2::bigint as initial_stream_version ), linked_stream_events AS ( - <% + <%!-- # Link the new events into the $all stream # 1 row for each event - %> + --%> INSERT INTO "<%= schema %>".stream_events ( event_id, diff --git a/lib/event_store/storage/snapshot.ex b/lib/event_store/storage/snapshot.ex index 75cae185..d2b21adc 100644 --- a/lib/event_store/storage/snapshot.ex +++ b/lib/event_store/storage/snapshot.ex @@ -73,7 +73,14 @@ defmodule EventStore.Storage.Snapshot do end end - defp to_snapshot_from_row([source_uuid, source_version, source_type, data, metadata, created_at]) do + defp to_snapshot_from_row([ + source_uuid, + source_version, + source_type, + data, + metadata, + created_at + ]) do %SnapshotData{ source_uuid: source_uuid, source_version: source_version, diff --git a/lib/event_store/subscriptions/subscription_fsm.ex b/lib/event_store/subscriptions/subscription_fsm.ex index 492d5289..42bbe443 100644 --- a/lib/event_store/subscriptions/subscription_fsm.ex +++ b/lib/event_store/subscriptions/subscription_fsm.ex @@ -5,7 +5,7 @@ defmodule EventStore.Subscriptions.SubscriptionFsm do alias EventStore.Streams.Stream alias EventStore.Subscriptions.{SubscriptionState, Subscriber} - use Fsm, initial_state: :initial, initial_data: %SubscriptionState{} + use EventStore.Fsm, initial_state: :initial, initial_data: %SubscriptionState{} require Logger @@ -536,7 +536,11 @@ defmodule EventStore.Subscriptions.SubscriptionFsm do end end - defp notify_partition_subscriber(data, partition_key, events_to_send \\ []) do + defp notify_partition_subscriber( + %SubscriptionState{} = data, + partition_key, + events_to_send \\ [] + ) do %SubscriptionState{ partitions: partitions, subscribers: subscribers, diff --git a/mise.toml b/mise.toml new file mode 100644 index 00000000..b668a7bf --- /dev/null +++ b/mise.toml @@ -0,0 +1,3 @@ +[tools] +erlang = "29.0.6" +elixir = "1.20.4-otp-29" diff --git a/mix.exs b/mix.exs index 82493696..ab14c91a 100644 --- a/mix.exs +++ b/mix.exs @@ -8,7 +8,7 @@ defmodule EventStore.Mixfile do [ app: :eventstore, version: @version, - elixir: "~> 1.11", + elixir: "~> 1.18", elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), description: description(), @@ -18,7 +18,6 @@ defmodule EventStore.Mixfile do start_permanent: Mix.env() == :prod, consolidate_protocols: Mix.env() == :prod, aliases: aliases(), - preferred_cli_env: preferred_cli_env(), dialyzer: dialyzer(), name: "EventStore", source_url: @source_url @@ -39,9 +38,8 @@ defmodule EventStore.Mixfile do defp deps do [ - {:fsm, "~> 0.3"}, {:gen_stage, "~> 1.2"}, - {:postgrex, "~> 0.17"}, + {:postgrex, "~> 0.22"}, {:telemetry, "~> 1.0"}, # Optional dependencies @@ -147,12 +145,14 @@ defmodule EventStore.Mixfile do ] end - defp preferred_cli_env do + def cli do [ - "test.all": :test, - "test.jsonb": :test, - "test.text_ids": :test, - "test.migration": :test + preferred_envs: [ + "test.all": :test, + "test.jsonb": :test, + "test.text_ids": :test, + "test.migration": :test + ] ] end diff --git a/mix.lock b/mix.lock index 210c2141..85f2ddcb 100644 --- a/mix.lock +++ b/mix.lock @@ -1,20 +1,19 @@ %{ "benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm", "23f27cbc482cbac03fc8926441eb60a5e111759c17642bac005c3225f5eb809d"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, - "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, - "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"}, - "fsm": {:hex, :fsm, "0.3.1", "087aa9b02779a84320dc7a2d8464452b5308e29877921b2bde81cdba32a12390", [:mix], [], "hexpm", "fbf0d53f89e9082b326b0b5828b94b4c549ff9d1452bbfd00b4d1ac082208e96"}, - "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, - "makeup_elixir": {:hex, :makeup_elixir, "1.0.0", "74bb8348c9b3a51d5c589bf5aebb0466a84b33274150e3b6ece1da45584afc82", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49159b7d7d999e836bedaf09dcf35ca18b312230cf901b725a64f3f42e407983"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "db_connection": {:hex, :db_connection, "2.10.2", "ae391e803a5adff104da913c2fc1c0c14a37f8b10001dcef568796e1fb7bf95c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "510b14482330f1af6490a2fa0efd8d4f1435d1529b165647df22ac0f2df0fa93"}, + "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"}, + "dialyxir": {:hex, :dialyxir, "1.4.8", "7ef671a8aff9948b091d8c30f09467fbb16e77305cda451bce48109a0f5e021c", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cbd5a851571e5dfeb32aaf2e840bfa98b7864cb3071bf2ef5d95d1276b12e072"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.46", "67607a0532e810c6f630a515c548d0b24949643f168cc556303bee4cf96105c7", [:mix], [], "hexpm", "9c44636e8a1c68c62f526b2dcd85d941dbbcee7ab82cf64ba06ce28bef8e89f5"}, + "erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"}, + "ex_doc": {:hex, :ex_doc, "0.40.4", "66f2e42bf588594d5a8aab31cad87f2ddad09d0da1b1a2f379340ec2c2e497cb", [:mix], [{:earmark_parser, "~> 1.4.46", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "6222b9e423d76584ee34df2c82a5ed72c2d53dc153f7f483ad28b378694186cc"}, + "gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"}, + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, + "makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, - "postgrex": {:hex, :postgrex, "0.17.4", "5777781f80f53b7c431a001c8dad83ee167bcebcf3a793e3906efff680ab62b3", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "6458f7d5b70652bc81c3ea759f91736c16a31be000f306d3c64bcdfe9a18b3cc"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "postgrex": {:hex, :postgrex, "0.22.4", "d271f595dfd25230b6398354e19d17bb5e2d20130fd2d9bdca7e15f125d43552", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "4aae45a2d60e35b04eea2602440be152fae332901f1fc7a60fc7cb7f0f9a9c5a"}, + "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, } diff --git a/test/event_store_test.exs b/test/event_store_test.exs index 630c0775..c2a6e138 100644 --- a/test/event_store_test.exs +++ b/test/event_store_test.exs @@ -167,7 +167,7 @@ defmodule EventStore.EventStoreTest do {:ok, recorded_events} = EventStore.read_stream_backward(stream_uuid) - assert_recorded_events(stream_uuid, 10..1, Enum.reverse(events), recorded_events) + assert_recorded_events(stream_uuid, 10..1//-1, Enum.reverse(events), recorded_events) end test "stream backward", %{stream_uuid: stream_uuid, events: events} do @@ -176,7 +176,7 @@ defmodule EventStore.EventStoreTest do recorded_events = EventStore.stream_backward(stream_uuid, -1, batch_size: 5) |> Enum.to_list() - assert_recorded_events(stream_uuid, 10..1, Enum.reverse(events), recorded_events) + assert_recorded_events(stream_uuid, 10..1//-1, Enum.reverse(events), recorded_events) end test "stream all backward", %{stream_uuid: stream_uuid, events: events} do @@ -184,7 +184,7 @@ defmodule EventStore.EventStoreTest do recorded_events = EventStore.stream_all_backward(-1, batch_size: 5) |> Enum.to_list() - assert_recorded_events(stream_uuid, 10..1, Enum.reverse(events), recorded_events) + assert_recorded_events(stream_uuid, 10..1//-1, Enum.reverse(events), recorded_events) end end @@ -485,7 +485,7 @@ defmodule EventStore.EventStoreTest do end test "record snapshot" do - assert record_snapshot() != nil + assert %SnapshotData{} = record_snapshot() end test "read a snapshot" do diff --git a/test/notifications/notifications_supervisor_test.exs b/test/notifications/notifications_supervisor_test.exs index 8877d3eb..0f54f8c3 100644 --- a/test/notifications/notifications_supervisor_test.exs +++ b/test/notifications/notifications_supervisor_test.exs @@ -47,8 +47,13 @@ defmodule EventStore.Notifications.NotificationsSupervisorTest do end end + # OTP 28 reports the `gen_server` hibernation loop where earlier releases + # reported `:erlang.hibernate/3`. + @hibernated_functions [{:erlang, :hibernate, 3}, {:gen_server, :loop_hibernate, 4}] + defp assert_hibernated(pid) do - assert Process.info(pid, :current_function) == {:current_function, {:erlang, :hibernate, 3}} + assert {:current_function, current_function} = Process.info(pid, :current_function) + assert current_function in @hibernated_functions end defp append_events(stream_uuid, count, expected_version \\ 0) do diff --git a/test/subscriptions/subscribe_to_stream_test.exs b/test/subscriptions/subscribe_to_stream_test.exs index 01d8c7b8..3938c05d 100644 --- a/test/subscriptions/subscribe_to_stream_test.exs +++ b/test/subscriptions/subscribe_to_stream_test.exs @@ -804,8 +804,13 @@ defmodule EventStore.Subscriptions.SubscribeToStreamTest do end end + # OTP 28 reports the `gen_server` hibernation loop where earlier releases + # reported `:erlang.hibernate/3`. + @hibernated_functions [{:erlang, :hibernate, 3}, {:gen_server, :loop_hibernate, 4}] + defp assert_hibernated(pid) do - assert Process.info(pid, :current_function) == {:current_function, {:erlang, :hibernate, 3}} + assert {:current_function, current_function} = Process.info(pid, :current_function) + assert current_function in @hibernated_functions end # Append events to another stream so that for single stream subscription tests From 3b7b9eefe24bfdabb77a0ca2121a695cb51c3108 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 15 Sep 2026 15:08:49 -0400 Subject: [PATCH 2/3] ci: let every matrix row report its own result A single flaky row cancelled the other two, so a red build said nothing about whether the other Elixir and OTP pairs actually work. Signed-off-by: Yordis Prieto --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 746e18fd..afef0b4c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,7 @@ jobs: name: Build and test runs-on: ubuntu-latest strategy: + fail-fast: false matrix: include: - elixir: 1.20.x From f60cadccb4e96634d3bea04e1c8ee3a36b2fb2ca Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 15 Sep 2026 15:21:29 -0400 Subject: [PATCH 3/3] ci: seed the migration database before the suite runs `mix test.all` exercises the migration environment, but only StorageCase created that database, so any seed that scheduled EventStore.ConfigTest first failed on a database that did not exist yet. The flake predates the matrix change and hit a different row on each run. Signed-off-by: Yordis Prieto --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index afef0b4c..e1792080 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,6 +63,7 @@ jobs: MIX_ENV=test mix event_store.setup MIX_ENV=jsonb mix event_store.setup MIX_ENV=text_ids mix event_store.setup + MIX_ENV=migration mix event_store.setup - name: Compile run: mix compile --warnings-as-errors