-
Notifications
You must be signed in to change notification settings - Fork 149
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
232 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
defmodule Electric.Timeline do | ||
@moduledoc """ | ||
Module containing helper functions for handling Postgres timelines. | ||
""" | ||
require Logger | ||
alias Electric.Shapes | ||
alias Electric.TimelineCache | ||
|
||
@type timeline :: integer() | nil | ||
|
||
@doc """ | ||
Checks the provided `pg_timeline` against Electric's timeline. | ||
Normally, Postgres and Electric are on the same timeline and nothing must be done. | ||
If the timelines differ, that indicates that a Point In Time Recovery (PITR) has occurred and all shapes must be cleaned. | ||
If we fail to fetch timeline information, we also clean all shapes for safety as we can't be sure that Postgres and Electric are on the same timeline. | ||
""" | ||
@spec check(timeline(), keyword()) :: :ok | ||
def check(pg_timeline, opts) do | ||
cache = Keyword.fetch!(opts, :timeline_cache) | ||
electric_timeline = TimelineCache.get_timeline(cache) | ||
handle(pg_timeline, electric_timeline, opts) | ||
end | ||
|
||
# Handles the timelines from Postgres and Electric. | ||
# Normally, Postgres and Electric are on the same timeline and nothing must be done. | ||
# If the timelines differ, that indicates that a Point In Time Recovery (PITR) has occurred and all shapes must be cleaned. | ||
# If we fail to fetch timeline information, we also clean all shapes for safety as we can't be sure that Postgres and Electric are on the same timeline. | ||
@spec handle(timeline(), timeline(), keyword()) :: :ok | ||
defp handle(nil, _, opts) do | ||
Logger.warning("Unknown Postgres timeline; rotating shapes.") | ||
Shapes.clean_all_shapes(opts) | ||
cache = Keyword.fetch!(opts, :timeline_cache) | ||
TimelineCache.store_timeline(cache, nil) | ||
end | ||
|
||
defp handle(pg_timeline_id, electric_timeline_id, _opts) | ||
when pg_timeline_id == electric_timeline_id do | ||
Logger.info("Connected to Postgres timeline #{pg_timeline_id}") | ||
:ok | ||
end | ||
|
||
defp handle(pg_timeline_id, nil, opts) do | ||
Logger.info("No previous timeline detected.") | ||
Logger.info("Connected to Postgres timeline #{pg_timeline_id}") | ||
# Store new timeline | ||
cache = Keyword.fetch!(opts, :timeline_cache) | ||
TimelineCache.store_timeline(cache, pg_timeline_id) | ||
end | ||
|
||
defp handle(pg_timeline_id, _electric_timeline_id, opts) do | ||
Logger.info("Detected PITR to timeline #{pg_timeline_id}; rotating shapes.") | ||
Electric.Shapes.clean_all_shapes(opts) | ||
# Store new timeline only after all shapes have been cleaned | ||
cache = Keyword.fetch!(opts, :timeline_cache) | ||
TimelineCache.store_timeline(cache, pg_timeline_id) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
defmodule Electric.TimelineCache do | ||
@moduledoc """ | ||
In-memory cache for storing the Postgres timeline on which Electric is running. | ||
""" | ||
use GenServer | ||
|
||
def start_link(timeline_id \\ nil) when is_nil(timeline_id) or is_integer(timeline_id) do | ||
GenServer.start_link(__MODULE__, timeline_id) | ||
end | ||
|
||
@doc """ | ||
Store the timeline ID on which Electric is running. | ||
""" | ||
@spec store_timeline(GenServer.name(), integer()) :: :ok | ||
def store_timeline(server \\ __MODULE__, timeline_id) do | ||
GenServer.call(server, {:store, timeline_id}) | ||
end | ||
|
||
@doc """ | ||
Get the timeline ID on which Electric is running. | ||
Returns nil if the timeline ID is not set. | ||
""" | ||
@spec get_timeline(GenServer.name()) :: integer() | nil | ||
def get_timeline(server \\ __MODULE__) do | ||
GenServer.call(server, :get) | ||
end | ||
|
||
@impl true | ||
def init(timeline_id) do | ||
{:ok, %{id: timeline_id}} | ||
end | ||
|
||
@impl true | ||
def handle_call({:store, timeline_id}, _from, state) do | ||
{:reply, :ok, %{state | id: timeline_id}} | ||
end | ||
|
||
def handle_call(:get, _from, state) do | ||
{:reply, Map.get(state, :id, nil), state} | ||
end | ||
end |
20 changes: 20 additions & 0 deletions
20
packages/sync-service/test/electric/timeline_cache_test.exs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
defmodule Electric.TimelineCacheTest do | ||
use ExUnit.Case, async: false | ||
alias Electric.TimelineCache | ||
|
||
describe "get_timeline/1" do | ||
test "returns the timeline ID" do | ||
timeline = 5 | ||
{:ok, pid} = TimelineCache.start_link(timeline) | ||
assert TimelineCache.get_timeline(pid) == timeline | ||
end | ||
end | ||
|
||
describe "store_timeline/2" do | ||
test "stores the timeline ID" do | ||
{:ok, pid} = TimelineCache.start_link(3) | ||
assert TimelineCache.store_timeline(pid, 4) == :ok | ||
assert TimelineCache.get_timeline(pid) == 4 | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
defmodule Electric.TimelineTest do | ||
use ExUnit.Case, async: true | ||
alias Electric.Timeline | ||
alias Electric.TimelineCache | ||
alias Electric.ShapeCacheMock | ||
|
||
import Mox | ||
|
||
describe "check/2" do | ||
setup context do | ||
timeline = context[:electric_timeline] | ||
|
||
pid = | ||
case timeline do | ||
nil -> | ||
{:ok, pid} = TimelineCache.start_link() | ||
pid | ||
|
||
_ -> | ||
{:ok, pid} = TimelineCache.start_link(timeline) | ||
pid | ||
end | ||
|
||
opts = [timeline_cache: pid, shape_cache: {ShapeCacheMock, []}] | ||
{:ok, [timeline: timeline, opts: opts]} | ||
end | ||
|
||
@tag electric_timeline: nil | ||
test "stores the Postgres timeline if Electric has no timeline yet", %{opts: opts} do | ||
timeline = 5 | ||
assert :ok = Timeline.check(timeline, opts) | ||
assert ^timeline = TimelineCache.get_timeline(opts[:timeline_cache]) | ||
end | ||
|
||
@tag electric_timeline: 3 | ||
test "proceeds without changes if Postgres' timeline matches Electric's timeline", %{ | ||
timeline: timeline, | ||
opts: opts | ||
} do | ||
assert :ok = Timeline.check(timeline, opts) | ||
assert ^timeline = TimelineCache.get_timeline(opts[:timeline_cache]) | ||
end | ||
|
||
@tag electric_timeline: 3 | ||
test "cleans all shapes if Postgres' timeline does not match Electric's timeline", %{ | ||
opts: opts | ||
} do | ||
ShapeCacheMock | ||
|> expect(:clean_all_shapes, fn _ -> :ok end) | ||
|
||
pg_timeline = 4 | ||
assert :ok = Timeline.check(pg_timeline, opts) | ||
assert ^pg_timeline = TimelineCache.get_timeline(opts[:timeline_cache]) | ||
end | ||
|
||
@tag electric_timeline: 3 | ||
test "cleans all shapes if Postgres' timeline is unknown", %{opts: opts} do | ||
ShapeCacheMock | ||
|> expect(:clean_all_shapes, fn _ -> :ok end) | ||
|
||
assert :ok = Timeline.check(nil, opts) | ||
assert TimelineCache.get_timeline(opts[:timeline_cache]) == nil | ||
end | ||
end | ||
end |