Packages
ex_esdb
0.2.3
0.11.0
0.10.0
0.9.0
0.8.0
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.1
0.5.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
0.0.20
0.0.19
0.0.18
0.0.17
0.0.16
0.0.15
0.0.14-alpha
0.0.13-alpha
0.0.12-alpha
0.0.11-alpha
0.0.10-alpha
0.0.9-alpha
0.0.8-alpha
0.0.6-alpha
0.0.5-alpha
0.0.4-alpha
0.0.3-alpha
0.0.2-alfa
0.0.1-alfa
ExESDB is a reincarnation of rabbitmq/khepri, specialized for use as a BEAM-native event store.
Current section
Files
Jump to
Current section
Files
lib/ex_esdb/consistency_checker.ex
defmodule ExESDB.ConsistencyChecker do
@moduledoc """
Provides tools for verifying consistency across ExESDB cluster stores.
This module leverages Khepri and Ra APIs to verify that stores are consistent
across cluster nodes, detect split-brain scenarios, and ensure data integrity.
"""
require Logger
@type store_id :: atom()
@type node_name :: atom()
@type consistency_result :: :consistent | :inconsistent | :error
@type check_result :: {:ok, map()} | {:error, term()}
@doc """
Performs a comprehensive consistency check across all cluster nodes for a given store.
## Returns
- `{:ok, report}` - Detailed consistency report
- `{:error, reason}` - Error occurred during check
## Example
iex> ExESDB.ConsistencyChecker.verify_cluster_consistency(:my_store)
{:ok, %{
status: :consistent,
nodes_checked: 3,
leader: :"node1@host",
members: [:"node1@host", :"node2@host", :"node3@host"],
raft_status: :healthy,
potential_issues: []
}}
"""
@spec verify_cluster_consistency(store_id()) :: check_result()
def verify_cluster_consistency(store_id) do
Logger.info("Starting consistency check for store: #{inspect(store_id)}")
try do
with {:ok, local_members} <- get_cluster_members(store_id),
{:ok, leader_info} <- get_leader_info(store_id),
{:ok, cross_node_check} <- verify_cross_node_consistency(store_id, local_members),
{:ok, raft_status} <- check_raft_status(store_id) do
report = compile_consistency_report(store_id, local_members, leader_info, cross_node_check, raft_status)
Logger.info("Consistency check completed for store: #{inspect(store_id)}")
{:ok, report}
else
{:error, reason} = error ->
Logger.error("Consistency check failed for store #{inspect(store_id)}: #{inspect(reason)}")
error
end
rescue
error ->
Logger.error("Exception during consistency check: #{inspect(error)}")
{:error, {:exception, error}}
end
end
@doc """
Quick health check to verify if a store is accessible and responsive across nodes.
"""
@spec quick_health_check(store_id()) :: check_result()
def quick_health_check(store_id) do
try do
case :khepri_cluster.members(store_id) do
{:ok, members} when members != [] ->
{:ok, %{
status: :healthy,
store_id: store_id,
member_count: length(members),
members: members
}}
{:ok, []} ->
{:error, :no_members}
{:error, reason} ->
{:error, reason}
end
rescue
error -> {:error, {:exception, error}}
end
end
@doc """
Verifies that all nodes agree on cluster membership.
"""
@spec verify_membership_consensus(store_id()) :: check_result()
def verify_membership_consensus(store_id) do
with {:ok, local_members} <- get_cluster_members(store_id) do
nodes = extract_nodes_from_members(local_members)
membership_views = get_membership_views_from_nodes(store_id, nodes)
case analyze_membership_consensus(membership_views) do
:consensus ->
{:ok, %{
status: :consensus,
nodes_checked: length(nodes),
consistent_view: local_members
}}
{:split_brain, conflicts} ->
{:ok, %{
status: :split_brain_detected,
conflicts: conflicts,
nodes_checked: length(nodes)
}}
{:partial_failure, failed_nodes} ->
{:ok, %{
status: :partial_check,
failed_nodes: failed_nodes,
nodes_checked: length(nodes)
}}
end
end
end
@doc """
Checks Raft log consistency across cluster members.
This is a more intensive check that examines log indices and terms.
"""
@spec check_raft_log_consistency(store_id()) :: check_result()
def check_raft_log_consistency(store_id) do
with {:ok, members} <- get_cluster_members(store_id),
{:ok, log_info} <- gather_raft_log_info(store_id, members) do
consistency_analysis = analyze_raft_log_consistency(log_info)
{:ok, %{
store_id: store_id,
raft_logs: log_info,
consistency: consistency_analysis
}}
end
end
@doc """
Monitors consistency over time and reports any deviations.
"""
@spec start_consistency_monitoring(store_id(), pos_integer()) :: {:ok, pid()}
def start_consistency_monitoring(store_id, interval_ms \\ 30_000) do
Task.start(fn ->
monitor_consistency_loop(store_id, interval_ms)
end)
end
## Private Functions
defp get_cluster_members(store_id) do
case :khepri_cluster.members(store_id) do
{:ok, members} -> {:ok, members}
{:error, reason} -> {:error, {:members_failed, reason}}
end
end
defp get_leader_info(store_id) do
case :ra_leaderboard.lookup_leader(store_id) do
{store_id, leader_node} when is_atom(leader_node) ->
{:ok, %{leader: leader_node, store: store_id}}
other ->
{:error, {:unexpected_leader_response, other}}
end
end
defp verify_cross_node_consistency(store_id, local_members) do
nodes = extract_nodes_from_members(local_members)
cross_checks = Enum.map(nodes, fn node ->
case :rpc.call(node, :khepri_cluster, :members, [store_id], 5000) do
{:ok, remote_members} ->
{node, :ok, remote_members}
{:error, reason} ->
{node, :error, reason}
{:badrpc, reason} ->
{node, :rpc_error, reason}
end
end)
{:ok, cross_checks}
end
defp check_raft_status(store_id) do
# Check if we can query Ra status
case :ra.members({store_id, node()}) do
{_status, members, leader} when is_list(members) ->
{:ok, %{
status: :healthy,
members: members,
leader: leader,
quorum_size: calculate_quorum_size(length(members))
}}
{_status, reason} ->
Logger.warning("Raft status check failed: #{inspect(reason)}")
{:ok, %{status: :error, reason: reason}}
end
rescue
error ->
Logger.warning("Exception checking Raft status: #{inspect(error)}")
{:ok, %{status: :exception, error: error}}
end
defp extract_nodes_from_members(members) do
Enum.map(members, fn {_store, node} -> node end)
end
defp get_membership_views_from_nodes(store_id, nodes) do
Enum.map(nodes, fn node ->
case :rpc.call(node, :khepri_cluster, :members, [store_id], 5000) do
{:ok, members} -> {node, :ok, members}
error -> {node, :error, error}
end
end)
end
defp analyze_membership_consensus(membership_views) do
{successful, failed} = Enum.split_with(membership_views, fn {_node, status, _data} ->
status == :ok
end)
if length(failed) > 0 do
failed_nodes = Enum.map(failed, fn {node, _status, _data} -> node end)
{:partial_failure, failed_nodes}
else
# Check if all successful views are identical
member_lists = Enum.map(successful, fn {_node, :ok, members} ->
Enum.sort(members)
end)
case Enum.uniq(member_lists) do
[_single_view] -> :consensus
multiple_views -> {:split_brain, %{views: multiple_views, nodes: successful}}
end
end
end
defp gather_raft_log_info(_store_id, members) do
log_info = Enum.map(members, fn {store, node} ->
case :rpc.call(node, :ra, :log_overview, [{store, node}], 5000) do
{:ok, overview} -> {node, :ok, overview}
error -> {node, :error, error}
end
end)
{:ok, log_info}
end
defp analyze_raft_log_consistency(log_info) do
{successful, failed} = Enum.split_with(log_info, fn {_node, status, _data} ->
status == :ok
end)
if length(failed) > 0 do
%{
status: :partial_failure,
failed_nodes: Enum.map(failed, fn {node, _status, _data} -> node end),
successful_count: length(successful)
}
else
# Analyze log terms and indices for consistency
log_details = Enum.map(successful, fn {node, :ok, overview} ->
{node, extract_log_details(overview)}
end)
%{
status: :analyzed,
log_details: log_details,
consistency: check_log_term_consistency(log_details)
}
end
end
defp extract_log_details(overview) do
# Extract relevant details from Ra log overview
# This is a simplified version - actual implementation would need
# to handle the specific structure returned by :ra.log_overview
%{
last_index: Map.get(overview, :last_index, :unknown),
last_term: Map.get(overview, :last_term, :unknown),
commit_index: Map.get(overview, :commit_index, :unknown)
}
end
defp check_log_term_consistency(log_details) do
# Check if all nodes have consistent log terms and indices
terms = Enum.map(log_details, fn {_node, details} -> details.last_term end)
indices = Enum.map(log_details, fn {_node, details} -> details.last_index end)
%{
term_consistency: length(Enum.uniq(terms)) <= 1,
index_range: {Enum.min(indices), Enum.max(indices)},
details: log_details
}
end
defp calculate_quorum_size(member_count) when member_count > 0 do
div(member_count, 2) + 1
end
defp compile_consistency_report(store_id, members, leader_info, cross_node_check, raft_status) do
issues = detect_potential_issues(members, leader_info, cross_node_check, raft_status)
overall_status = if Enum.empty?(issues), do: :consistent, else: :inconsistent
%{
store_id: store_id,
timestamp: DateTime.utc_now(),
status: overall_status,
nodes_checked: length(members),
leader: Map.get(leader_info, :leader),
members: members,
raft_status: raft_status,
cross_node_verification: cross_node_check,
potential_issues: issues,
recommendations: generate_recommendations(issues)
}
end
defp detect_potential_issues(members, leader_info, cross_node_check, _raft_status) do
issues = []
# Check for missing leader
issues = if Map.get(leader_info, :leader) == nil do
[:no_leader | issues]
else
issues
end
# Check for node communication failures
failed_nodes = Enum.filter(cross_node_check, fn {_node, status, _data} ->
status != :ok
end)
issues = if length(failed_nodes) > 0 do
[{:communication_failures, failed_nodes} | issues]
else
issues
end
# Check for membership inconsistencies
member_views = Enum.map(cross_node_check, fn
{node, :ok, remote_members} -> {node, remote_members}
{node, _error, _} -> {node, []}
end)
local_members = Enum.sort(members)
inconsistent_views = Enum.filter(member_views, fn {_node, remote_members} ->
Enum.sort(remote_members) != local_members
end)
issues = if length(inconsistent_views) > 0 do
[{:membership_inconsistency, inconsistent_views} | issues]
else
issues
end
issues
end
defp generate_recommendations(issues) do
Enum.flat_map(issues, fn
:no_leader ->
["Check cluster connectivity and ensure quorum is available"]
{:communication_failures, failed_nodes} ->
nodes = Enum.map(failed_nodes, fn {node, _status, _data} -> node end)
["Investigate connectivity issues with nodes: #{inspect(nodes)}"]
{:membership_inconsistency, _inconsistent_views} ->
["Resolve membership inconsistencies - potential split-brain scenario detected"]
_ ->
["Review cluster configuration and connectivity"]
end)
end
defp monitor_consistency_loop(store_id, interval_ms) do
case verify_cluster_consistency(store_id) do
{:ok, %{status: :consistent}} ->
Logger.debug("Consistency monitor: Store #{inspect(store_id)} is consistent")
{:ok, %{status: :inconsistent, potential_issues: issues}} ->
Logger.warning("Consistency monitor: Issues detected in store #{inspect(store_id)}: #{inspect(issues)}")
{:error, reason} ->
Logger.error("Consistency monitor: Failed to check store #{inspect(store_id)}: #{inspect(reason)}")
end
Process.sleep(interval_ms)
monitor_consistency_loop(store_id, interval_ms)
end
end