-
Notifications
You must be signed in to change notification settings - Fork 7
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
1 parent
fb417d9
commit 5942ea2
Showing
11 changed files
with
233 additions
and
17 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
defmodule Console.Schema.Leader do | ||
use Piazza.Ecto.Schema | ||
|
||
schema "leaders" do | ||
field :name, :string | ||
field :ref, Piazza.Ecto.Types.Erlang | ||
field :heartbeat, :utc_datetime_usec | ||
|
||
timestamps() | ||
end | ||
|
||
def with_lock(query \\ __MODULE__) do | ||
from(l in query, lock: "FOR UPDATE") | ||
end | ||
|
||
@valid ~w(name ref heartbeat)a | ||
|
||
def changeset(model, attrs \\ %{}) do | ||
model | ||
|> cast(attrs, @valid) | ||
|> unique_constraint(:name) | ||
|> validate_required(@valid) | ||
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,61 @@ | ||
defmodule Console.Services.LeaderElection do | ||
use Console.Services.Base | ||
alias Console.Schema.Leader | ||
|
||
@type error :: {:error, term} | ||
@type leader_resp :: {:ok, Leader.t} | error | ||
|
||
@spec get(binary) :: Leader.t | nil | ||
def get(name), do: Console.Repo.get_by(Leader, name: name) | ||
|
||
@spec atomic_get(binary) :: Leader.t | nil | ||
def atomic_get(name), do: Console.Repo.get_by(Leader.with_lock(), name: name) | ||
|
||
@doc """ | ||
Wipes the leader record if `ref` owns `name`, otherwise fails | ||
""" | ||
@spec clear(term, binary) :: leader_resp | ||
def clear(ref, name) do | ||
start_transaction() | ||
|> add_operation(:fetch, fn _ -> | ||
case atomic_get(name) do | ||
%Leader{ref: ^ref} = l -> {:ok, l} | ||
_ -> {:error, :following} | ||
end | ||
end) | ||
|> add_operation(:update, fn %{fetch: l} -> Console.Repo.delete(l) end) | ||
|> execute(extract: :update) | ||
end | ||
|
||
@doc """ | ||
Locks the record for `name` then if either `ref` currently owns it or it does not exist, upserts | ||
the record with a current heartbeat. | ||
If `ref` does not own the record, it fails | ||
""" | ||
@spec elect(term, binary) :: leader_resp | ||
def elect(ref, name) do | ||
start_transaction() | ||
|> add_operation(:fetch, fn _ -> | ||
case atomic_get(name) do | ||
%Leader{ref: ^ref} = leader -> {:ok, leader} | ||
%Leader{} = leader -> check_hearbeat(leader) | ||
nil -> {:ok, %Leader{name: name}} | ||
end | ||
end) | ||
|> add_operation(:update, fn %{fetch: fetch} -> | ||
fetch | ||
|> Leader.changeset(%{ref: ref, heartbeat: Timex.now()}) | ||
|> Console.Repo.insert_or_update() | ||
end) | ||
|> execute(extract: :update) | ||
end | ||
|
||
defp check_hearbeat(%Leader{heartbeat: beat} = leader) do | ||
expired = Timex.now() |> Timex.shift(seconds: -30) | ||
case Timex.before?(beat, expired) do | ||
true -> {:ok, leader} | ||
false -> {:error, :following} | ||
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
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,16 @@ | ||
defmodule Console.Repo.Migrations.AddLeaders do | ||
use Ecto.Migration | ||
|
||
def change do | ||
create table(:leaders, primary_key: false) do | ||
add :id, :uuid, primary_key: true | ||
add :heartbeat, :utc_datetime_usec | ||
add :ref, :binary | ||
add :name, :string | ||
|
||
timestamps() | ||
end | ||
|
||
create unique_index(:leaders, [:name]) | ||
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
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,59 @@ | ||
defmodule Console.Services.LeaderElectionTest do | ||
use Console.DataCase, async: true | ||
alias Console.Services.LeaderElection | ||
|
||
describe "elect/2" do | ||
test "if no leader exists it will elect" do | ||
{:ok, leader} = LeaderElection.elect(self(), "usa") | ||
|
||
assert leader.name == "usa" | ||
assert leader.ref == self() | ||
assert leader.heartbeat | ||
end | ||
|
||
test "if another leader exists, it will fail" do | ||
insert(:leader, ref: :else, name: "usa") | ||
|
||
{:error, _} = LeaderElection.elect(self(), "usa") | ||
end | ||
|
||
test "if a stale leader exists, it will take ownership" do | ||
insert(:leader, ref: :else, name: "usa", heartbeat: Timex.now() |> Timex.shift(minutes: -1)) | ||
|
||
{:ok, leader} = LeaderElection.elect(self(), "usa") | ||
|
||
assert leader.name == "usa" | ||
assert leader.ref == self() | ||
assert leader.heartbeat | ||
end | ||
|
||
test "if you are leader, the hearbeat will be updated" do | ||
old = insert(:leader, name: "usa", heartbeat: Timex.now() |> Timex.shift(seconds: -10)) | ||
|
||
{:ok, leader} = LeaderElection.elect(self(), "usa") | ||
|
||
assert leader.name == "usa" | ||
assert leader.ref == self() | ||
assert Timex.after?(leader.heartbeat, old.heartbeat) | ||
end | ||
end | ||
|
||
describe "#clear/2" do | ||
test "if you're leader, it will clear" do | ||
leader = insert(:leader, name: "usa") | ||
|
||
{:ok, del} = LeaderElection.clear(self(), "usa") | ||
|
||
assert del.id == leader.id | ||
refute refetch(del) | ||
end | ||
|
||
test "if you aren't leader, it will ignore" do | ||
leader = insert(:leader, ref: :other, name: "usa") | ||
|
||
{:error, _} = LeaderElection.clear(self(), "usa") | ||
|
||
assert refetch(leader) | ||
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