-
Notifications
You must be signed in to change notification settings - Fork 4
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
Persistent retries #8
base: main
Are you sure you want to change the base?
Changes from all commits
83d54cb
db64d19
c3d2b35
f2ce63c
3ff4648
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,101 @@ | ||||||
defmodule NervesHubLinkCommon.Journal do | ||||||
@moduledoc """ | ||||||
Simple journaling structure backed by a file on the filesystem | ||||||
Stores data in chunks in the following format: | ||||||
<<length::32, hash::binary-size(32)-unit(8), data::binary-size(length)-unit(8)>> | ||||||
as chunks are streamed with `save_chunk/2` the data is updated both on disk and | ||||||
in the structure. This can be used to rehydrate stateful events after a reboot, such as | ||||||
a firmware update for example. | ||||||
When opening an existing journal (done automatically if the journal exists), | ||||||
the structure will validate all the chunks on disk, stopping on either | ||||||
* the first chunk to fail a hash check | ||||||
* the end of the file | ||||||
In either case, the journal is valid to use at this point | ||||||
""" | ||||||
|
||||||
defstruct [:fd, :content_length, :chunks] | ||||||
|
||||||
@type t :: %__MODULE__{ | ||||||
fd: :file.fd(), | ||||||
content_length: non_neg_integer(), | ||||||
chunks: [binary()] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a little worried about firmware images that exceed available memory. I haven't read the code completely, but keeping a chunk list makes me think they're being cached. How about a setup where the firmware download process starts by pulling chunks from the journal and feeding them to fwup. When it runs out of journal chunks, it makes an http request for the rest? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah i think something along those lines will be required as well. I haven't thought of exactly how yet, but i like your idea. |
||||||
} | ||||||
|
||||||
@doc "Open or create a journal for this meta" | ||||||
@spec open(Path.t()) :: {:ok, t()} | {:error, File.posix()} | ||||||
def open(filename) do | ||||||
with {:ok, fd} <- :file.open(filename, [:write, :read, :binary]), | ||||||
{:ok, 0} <- :file.position(fd, 0), | ||||||
{:ok, journal} <- validate_and_seek(%__MODULE__{fd: fd, content_length: 0, chunks: []}) do | ||||||
{:ok, journal} | ||||||
end | ||||||
end | ||||||
|
||||||
@spec reload(Path.t()) :: {:ok, t()} | {:error, File.posix()} | ||||||
def reload(filename) do | ||||||
if File.exists?(filename) do | ||||||
open(filename) | ||||||
else | ||||||
{:error, :enoent} | ||||||
end | ||||||
end | ||||||
|
||||||
@spec validate_and_seek(t()) :: {:ok, t()} | {:error, File.posix()} | ||||||
def validate_and_seek(%__MODULE__{fd: fd, content_length: content_length} = journal) do | ||||||
with {:ok, <<length::32>>} <- :file.read(fd, 4), | ||||||
{:ok, hash} <- :file.read(fd, 32), | ||||||
{:ok, data} <- :file.read(fd, length), | ||||||
{:hash, ^length, ^hash} <- {:hash, length, :crypto.hash(:sha256, data)} do | ||||||
validate_and_seek(%__MODULE__{ | ||||||
journal | ||||||
| content_length: content_length + length, | ||||||
chunks: journal.chunks ++ [data] | ||||||
}) | ||||||
else | ||||||
# made it thru all chunks in the file | ||||||
:eof -> | ||||||
{:ok, journal} | ||||||
|
||||||
# hash check failed. rewind and break | ||||||
{:hash, length, _} -> | ||||||
rewind(journal, length + 32 + 4) | ||||||
|
||||||
{:error, posix} -> | ||||||
{:error, posix} | ||||||
end | ||||||
end | ||||||
|
||||||
@spec rewind(t(), pos_integer()) :: {:ok, t()} | {:error, File.posix()} | ||||||
def rewind(journal, length) do | ||||||
with {:ok, _} <- :file.position(journal.fd, -length) do | ||||||
{:ok, journal} | ||||||
end | ||||||
end | ||||||
|
||||||
@spec close(t()) :: :ok | ||||||
def close(%__MODULE__{fd: fd} = _journal) do | ||||||
:ok = :file.close(fd) | ||||||
end | ||||||
|
||||||
@spec save_chunk(t(), iodata()) :: {:ok, t()} | {:error, File.posix()} | ||||||
def save_chunk(%__MODULE__{fd: fd} = journal, data) when is_binary(data) do | ||||||
hash = :crypto.hash(:sha256, data) | ||||||
length = byte_size(data) | ||||||
journal_entry = IO.iodata_to_binary([<<length::32>>, hash, data]) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whoops, you're right, good catch |
||||||
|
||||||
with :ok <- :file.write(fd, journal_entry) do | ||||||
{:ok, | ||||||
%__MODULE__{ | ||||||
journal | ||||||
| chunks: journal.chunks ++ [data], | ||||||
content_length: journal.content_length + length | ||||||
}} | ||||||
end | ||||||
end | ||||||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm just making up the journal location so other names work for me too. What I'd like to suggest are:
I haven't gotten to it yet, but I'm guessing that the use of one file might have other ramifications. I do like the simplicity and limiting the number of files that it can create.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah i only defaulted it to /tmp for the test suite since
/data
can't be written to on our host machines. I agree on keeping a single file, but i also wanted this to be a directory because there needs to be some way of identifying a partial download as a particular firmware after a reboot, which i was going to use the Filename for.I also considered putting a header in the partial file, but am still thinking about that.