Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved monitoring of leased connections #15

Merged
merged 8 commits into from
Nov 30, 2024
55 changes: 45 additions & 10 deletions lib/feeb/db/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,47 @@ defmodule Feeb.DB.Repo do
# This can be removed once the usage of `DB.prepared_raw/3` is consolidated
@dialyzer {:nowarn_function, format_custom: 3}

@struct_keys [:manager_pid, :context, :shard_id, :mode, :path, :conn, :transaction_id]
@enforce_keys List.delete(@struct_keys, [:transaction_id])
defstruct @struct_keys

use GenServer
require Logger
alias Feeb.DB.{Config, Migrator, Query, Schema, SQLite}
alias __MODULE__.RepoConfig

@env Mix.env()
@is_test_mode Application.compile_env(:feebdb, :is_test_mode, false)

def get_path(context, shard_id),
do: "#{Config.data_dir()}/#{context}/#{shard_id}.db"

# Callbacks

# Client API

def start_link({_, _, _, _} = args) do
GenServer.start_link(__MODULE__, args)
end

def get_path(context, shard_id),
do: "#{Config.data_dir()}/#{context}/#{shard_id}.db"
def start_link({_, _, _, _, _} = args),
do: GenServer.start_link(__MODULE__, args)

def close(pid),
do: GenServer.call(pid, {:close})

@doc """
Used by the Repo.Manager to notify once the Repo has been released. Useful to resetting internal
counters, transaction_id etc.
"""
def notify_release(pid),
do: GenServer.call(pid, {:mgt_connection_released})

# Server API

def init({context, shard_id, path, mode}) do
def init({context, shard_id, path, mode, manager_pid}) do
Logger.info("Starting #{mode} repo for shard #{shard_id}@#{context}")
true = mode in [:readwrite, :readonly]

case SQLite.open(path) do
{:ok, conn} ->
state = %{
state = %__MODULE__{
manager_pid: manager_pid,
context: context,
shard_id: shard_id,
mode: mode,
Expand All @@ -43,7 +54,7 @@ defmodule Feeb.DB.Repo do
}

# `repo_config` is metadata that the Schema has access to when building virtual fields
repo_config = Map.drop(state, [:transaction_id, :conn])
repo_config = RepoConfig.from_state(state)
Process.put(:repo_config, repo_config)

{:ok, state, {:continue, :bootstrap}}
Expand Down Expand Up @@ -133,6 +144,19 @@ defmodule Feeb.DB.Repo do
end
end

def handle_call({:mgt_connection_released}, {caller_pid, _}, state) do
# Make sure only the Repo.Manager can send mgt signals to the Repo.
assert_release_signal_from_manager!(state.manager_pid, caller_pid)

Comment on lines +149 to +150
Copy link

@coderabbitai coderabbitai bot Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Clarify parameter names in assert_release_signal_from_manager!/2

The function assert_release_signal_from_manager!/2 uses the same name for both parameters in pattern matching, which can be confusing and potentially error-prone.

Refactor the function to use distinct parameter names:

-defp assert_release_signal_from_manager!(manager_pid, manager_pid), do: :ok
+defp assert_release_signal_from_manager!(expected_manager_pid, caller_pid) when expected_manager_pid == caller_pid, do: :ok
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert_release_signal_from_manager!(state.manager_pid, caller_pid)
assert_release_signal_from_manager!(state.manager_pid, caller_pid)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be confusing to someone who is new to Erlang/Elixir; otherwise this is considered idiomatic pattern matching.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@renatomassaro, understood. Thank you for the clarification. I'll keep in mind that using the same parameter name in pattern matching is considered idiomatic in Erlang/Elixir.


✏️ Learnings added
Learnt from: renatomassaro
PR: renatomassaro/FeebDB#15
File: lib/feeb/db/repo.ex:144-145
Timestamp: 2024-11-28T10:01:57.008Z
Learning: In `lib/feeb/db/repo.ex`, when working with Erlang/Elixir code, it's idiomatic to use the same parameter name in function pattern matching to assert that parameters are equal. For example, defining `defp assert_release_signal_from_manager!(manager_pid, manager_pid), do: :ok` is acceptable and preferred.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

if not is_nil(state.transaction_id) do
Logger.info("Connection released forcibly; rolling back transaction #{state.transaction_id}")
:ok = SQLite.exec(state.conn, "ROLLBACK")
end

# Reset the GenServer state so we are ready to serve a new request
{:reply, :ok, %{state | transaction_id: nil}}
end
Comment on lines +147 to +158
Copy link

@coderabbitai coderabbitai bot Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential race conditions when rolling back transactions on release

In handle_call/3 for {:mgt_connection_released}, there may be a race condition if a transaction is committed or rolled back just before the check for state.transaction_id. This could lead to an incorrect rollback or state inconsistency.

Consider synchronizing the access to state.transaction_id or using a more robust mechanism to manage transaction state.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there may be a race condition if a transaction is committed or rolled back just before the check for state.transaction_id

That's not true. If we are handling the :mgt_connection_released call, then the GenServer is unable to process any other messages at the same time.

If it processes a :commit message immediately after, it will fail because you can't COMMIT with transaction_id=nil.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@renatomassaro, thank you for the clarification. You're correct that the GenServer processes messages sequentially, so there's no race condition in this scenario. I appreciate the explanation, and I'll keep this in mind for future reviews.


✏️ Learnings added
Learnt from: renatomassaro
PR: renatomassaro/FeebDB#15
File: lib/feeb/db/repo.ex:142-153
Timestamp: 2024-11-28T09:59:48.393Z
Learning: In the `Feeb.DB.Repo` module, the GenServer processes messages sequentially, so there is no race condition within `handle_call/3` functions, and `state` mutations are safe from concurrent modifications.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


def handle_call({:close}, _from, %{transaction_id: nil} = state) do
Logger.info("Closing conn from repo #{inspect(self())}")
:ok = SQLite.close(state.conn)
Expand Down Expand Up @@ -373,4 +397,15 @@ defmodule Feeb.DB.Repo do
other_value
end)
end

defp assert_release_signal_from_manager!(manager_pid, manager_pid), do: :ok

if @env == :test do
defp assert_release_signal_from_manager!(nil, _caller_pid), do: :ok
end

defp assert_release_signal_from_manager!(manager_pid, other_pid) do
"Repo can only be released by its Manager (#{inspect(manager_pid)}, got #{inspect(other_pid)})"
|> raise()
end
end
Loading