Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lib/firmboot/water_leak/durable.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ defmodule Firmboot.WaterLeak.Durable do
when point in [:before_write, :partial_write, :after_write, :after_sync, :sync_error],
do: GenServer.call(pid, {:fault, point})

@doc """
Opens the journal at `path`, replays it and builds the initial GenServer state.

Stops with the underlying reason on an invalid `repair_tail` option, a missing or
malformed journal, or a replay failure; never starts from partial state.
"""
@impl true
def init({path, repair_tail}) do
with true <- is_boolean(repair_tail),
Expand Down Expand Up @@ -90,6 +96,10 @@ defmodule Firmboot.WaterLeak.Durable do
end
end

@doc """
Serves `:snapshot`, `:status` and fault-arming calls directly, and `:submit` by
journaling and applying the command before replying.
"""
@impl true
def handle_call(:snapshot, _from, s), do: {:reply, s.domain, s}
def handle_call({:fault, fault}, _from, s), do: {:reply, :ok, %{s | fault: fault}}
Expand Down Expand Up @@ -137,6 +147,8 @@ defmodule Firmboot.WaterLeak.Durable do
end
end

# Applies a new command, journals it, and only then replies; a failed write/sync
# stops the process rather than reply from state that may not match the file.
defp commit(s, id, command) do
if s.journal.count >= s.journal.config.max_records do
{:reply, {:error, :storage_capacity}, s}
Expand Down Expand Up @@ -176,6 +188,7 @@ defmodule Firmboot.WaterLeak.Durable do
end
end

@doc "Closes the journal file as the process terminates."
@impl true
def terminate(_reason, s), do: Journal.close(s.journal)
end
3 changes: 3 additions & 0 deletions lib/firmboot/water_leak/durable/codec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ defmodule Firmboot.WaterLeak.Durable.Codec do
end
end

# Mirrors encode/1: one clause per tag byte, binary pattern matches enforcing shape.
defp parse(
<<0, cap::32, threshold::64, silence::32, fingerprint::binary-size(32), n,
source::binary-size(n)>>
Expand Down Expand Up @@ -128,6 +129,8 @@ defmodule Firmboot.WaterLeak.Durable.Codec do

defp parse(<<4, bytes::16>>), do: {:tail_repair, bytes}
defp parse(_), do: :invalid

# Small enum <-> wire-byte mappings used by encode/1 and parse/1 above.
defp version(:v1), do: 1
defp version(:v2), do: 2
defp fault(f), do: Enum.find_index([:none, :position, :reported, :history], &(&1 == f))
Expand Down
5 changes: 5 additions & 0 deletions lib/firmboot/water_leak/durable/journal.ex
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ defmodule Firmboot.WaterLeak.Durable.Journal do
end
end

# Kills the process when the armed fault point matches the current point; a no-op otherwise.
defp crash(point, point) when not is_nil(point), do: Process.exit(self(), :kill)
defp crash(_, _), do: :ok

Expand Down Expand Up @@ -163,6 +164,8 @@ defmodule Firmboot.WaterLeak.Durable.Journal do
end
end

# Reads chained slots until eof, an incomplete tail, or the record limit; verifies
# each slot's hash against the running chain as it goes.
defp read_records(fd, hash, remaining, records) do
case :file.read(fd, @slot) do
:eof ->
Expand Down Expand Up @@ -191,6 +194,7 @@ defmodule Firmboot.WaterLeak.Durable.Journal do
end
end

# Encodes `record` and wraps it with the magic prefix, chained hash and zero padding.
defp frame(record, previous) do
payload = Codec.encode(record)
size = byte_size(payload)
Expand All @@ -201,6 +205,7 @@ defmodule Firmboot.WaterLeak.Durable.Journal do
0::size((@payload_size - size) * 8)>>, hash}
end

# Inverse of frame/2: verifies the magic, chained hash and zero padding, then decodes.
defp unframe(
<<@magic, size::16, hash::binary-size(32), rest::binary-size(@payload_size)>>,
previous
Expand Down
2 changes: 2 additions & 0 deletions lib/firmboot/water_leak/durable/transition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ defmodule Firmboot.WaterLeak.Durable.Transition do
Enum.reverse(ranges)
end

# One clause per command kind; returns the possibly-updated state and its reply.
defp execute(s, _id, {:sample, reading}) do
existing =
Map.get(s.waiting, reading.seq) ||
Expand Down Expand Up @@ -113,6 +114,7 @@ defmodule Firmboot.WaterLeak.Durable.Transition do
end
end

# Applies any buffered readings that are now contiguous with the processed sequence.
defp drain(s) do
case Map.pop(s.waiting, s.water.model.seq + 1) do
{nil, _} ->
Expand Down
3 changes: 3 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule FirmbootReference.MixProject do
@moduledoc "Build configuration for the Firmboot reference project."
use Mix.Project

@doc "The Mix project configuration: app name, version, Elixir constraint and deps."
def project do
[
app: :firmboot_reference,
Expand All @@ -11,5 +13,6 @@ defmodule FirmbootReference.MixProject do
]
end

@doc "Declares `:crypto` as a runtime application, needed by the journal's SHA-256 chain."
def application, do: [extra_applications: [:crypto]]
end
5 changes: 5 additions & 0 deletions test/firmboot/durable_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -363,12 +363,15 @@ defmodule Firmboot.DurableTest do
assert WaterLeak.outstanding(Durable.snapshot(recovered).water) == [@key]
end

# Starts a writer under test and registers it to be stopped on exit.
defp start(path, options \\ []) do
assert {:ok, pid} = Durable.start(path, options)
on_exit(fn -> if Process.alive?(pid), do: Durable.stop(pid) end)
pid
end

# Submits the fixture's first six readings in order, including the mid-stream
# :update at seq 5, so a test can start from a known, journaled baseline.
defp seed(pid) do
for reading <- Enum.take(Experiment.readings(), 6) do
if reading.seq == 5 do
Expand All @@ -380,11 +383,13 @@ defmodule Firmboot.DurableTest do
end
end

# All orderings of a list, used to exercise every arrival order of a fixture.
defp permutations([]), do: [[]]

defp permutations(xs),
do: for(x <- xs, tail <- permutations(List.delete(xs, x)), do: [x | tail])

# Flips one bit at `offset`, used to simulate single-byte journal corruption.
defp flip(binary, offset) do
<<before::binary-size(offset), byte, after_bytes::binary>> = binary
before <> <<Bitwise.bxor(byte, 1)>> <> after_bytes
Expand Down
Loading