fix outbox

This commit is contained in:
anlicheng 2026-05-12 16:48:04 +08:00
parent ee0d9fcc08
commit 50864cf7bd

View File

@ -21,8 +21,16 @@
%%% - max_record_bytes record payload
%%%
%%% record
%%% - 4 unsigned big-endian payload size
%%% - term_to_binary({Seq, Packet}) payload
%%% - 8 magic bytes <<"EFKAOBX1">>
%%% - 1 version 1
%%% - 1 header size 22
%%% - 4 unsigned big-endian packet size
%%% - 8 unsigned big-endian Seq
%%% - Packet
%%% - 4 unsigned big-endian crc32 header + Packet
%%% - magic versionheader sizepacket size
%%% crc32 record magic
%%%
%%%
%%%
%%% - `append/2' record writer segment fsync
@ -32,15 +40,16 @@
%%% - outbox live segment open/1 max_segments
%%% segment live segment
%%% packet
%%% - segment live segment 5 writer
%%% `segment-(N + 1).log'
%%% - segment live segment writer
%%% `segment-(N + 1).log'
%%%
%%%
%%% - `next/1' end_seq acked_seq live segment
%%% segment Seq acked_seq record
%%% - efka_iot_client ack packet
%%% - `ack/2' Seq
%%% acked_seq
%%% - `ack/2' Seq Seq
%%% record ack record Seq
%%% segment
%%%
%%% segment
%%% - `ack/2' segment record segment
@ -83,6 +92,11 @@
-define(METADATA_FILE, "metadata.term").
-define(SEGMENT_PREFIX, "segment-").
-define(SEGMENT_EXT, ".log").
-define(RECORD_MAGIC, <<"EFKAOBX1">>).
-define(RECORD_MAGIC_SIZE, 8).
-define(RECORD_VERSION, 1).
-define(RECORD_HEADER_SIZE, 22).
-define(RECORD_CRC_SIZE, 4).
-type open_options() :: #{
dir := file:filename_all(),
@ -176,7 +190,7 @@ next(#outbox{segments = Segments, acked_seq = AckedSeq, max_record_bytes = MaxRe
-spec ack(pos_integer(), outbox()) -> {ok, outbox()} | {error, term()}.
ack(Seq, Outbox = #outbox{acked_seq = AckedSeq}) when is_integer(Seq), Seq =< AckedSeq ->
{ok, Outbox};
ack(Seq, Outbox = #outbox{acked_seq = AckedSeq}) when is_integer(Seq), Seq =:= AckedSeq + 1 ->
ack(Seq, Outbox = #outbox{acked_seq = AckedSeq}) when is_integer(Seq), Seq > AckedSeq ->
AckedOutbox = Outbox#outbox{acked_seq = Seq},
maybe
ok ?= persist_metadata(AckedOutbox),
@ -188,7 +202,7 @@ ack(Seq, Outbox = #outbox{acked_seq = AckedSeq}) when is_integer(Seq), Seq =:= A
{error, Reason}
end;
ack(Seq, #outbox{acked_seq = AckedSeq}) when is_integer(Seq) ->
{error, {non_contiguous_ack, Seq, AckedSeq}}.
{error, {invalid_ack, Seq, AckedSeq}}.
-spec ensure_dir(file:filename_all()) -> ok | {error, term()}.
ensure_dir(Dir) ->
@ -491,9 +505,12 @@ scan_segment(Fd, Id, Path, MaxRecordBytes, StartSeq, EndSeq, Records) ->
-spec encode_record(pos_integer(), binary()) -> binary().
encode_record(Seq, Packet) ->
Payload = term_to_binary({Seq, Packet}),
Size = byte_size(Payload),
<<Size:32/unsigned-big, Payload/binary>>.
PacketSize = byte_size(Packet),
Magic = ?RECORD_MAGIC,
Header = <<Magic/binary, ?RECORD_VERSION:8/unsigned, ?RECORD_HEADER_SIZE:8/unsigned,
PacketSize:32/unsigned-big, Seq:64/unsigned-big>>,
Crc32 = erlang:crc32(<<Header/binary, Packet/binary>>),
<<Header/binary, Packet/binary, Crc32:32/unsigned-big>>.
-spec read_next_unacked(file:fd(), non_neg_integer(), pos_integer()) ->
eof | {ok, pos_integer(), binary()} | {error, term()}.
@ -511,41 +528,121 @@ read_next_unacked(Fd, AckedSeq, MaxRecordBytes) ->
-spec read_record(file:fd(), pos_integer()) -> eof | {ok, pos_integer(), binary()} | {error, term()}.
read_record(Fd, MaxRecordBytes) ->
case file:read(Fd, 4) of
case find_next_magic(Fd) of
eof ->
eof;
{ok, <<Size:32/unsigned-big>>} when Size > 0, Size =< MaxRecordBytes ->
read_record_payload(Fd, Size);
{ok, <<Size:32/unsigned-big>>} ->
{error, {invalid_record_size, Size}};
{ok, Partial} ->
{error, {truncated_record_header, Partial}};
{error, Reason} ->
{error, Reason}
end.
-spec read_record_payload(file:fd(), pos_integer()) ->
{ok, pos_integer(), binary()} | {error, term()}.
read_record_payload(Fd, Size) ->
case file:read(Fd, Size) of
{ok, Payload} when byte_size(Payload) =:= Size ->
safe_record(Payload);
{ok, Payload} ->
{error, {truncated_record_payload, Size, byte_size(Payload)}};
eof ->
{error, {truncated_record_payload, Size, 0}};
{error, Reason} ->
{error, Reason}
end.
-spec safe_record(binary()) -> {ok, pos_integer(), binary()} | {error, term()}.
safe_record(Payload) ->
try binary_to_term(Payload, [safe]) of
{Seq, Packet} when is_integer(Seq), Seq > 0, is_binary(Packet) ->
{ok, StartPos} ->
case read_record_after_magic(Fd, MaxRecordBytes) of
{ok, Seq, Packet} ->
{ok, Seq, Packet};
Other ->
{error, {invalid_record, Other}}
catch
error:Reason ->
{error, {invalid_record, Reason}}
{resync, _Reason} ->
case file:position(Fd, {bof, StartPos + 1}) of
{ok, _} ->
read_record(Fd, MaxRecordBytes);
{error, Reason} ->
{error, Reason}
end
end;
{error, Reason} ->
{error, Reason}
end.
-spec find_next_magic(file:fd()) -> eof | {ok, non_neg_integer()} | {error, term()}.
find_next_magic(Fd) ->
case file:position(Fd, cur) of
{ok, StartPos} ->
case file:read(Fd, ?RECORD_MAGIC_SIZE) of
eof ->
eof;
{ok, Magic} when byte_size(Magic) < ?RECORD_MAGIC_SIZE ->
eof;
{ok, ?RECORD_MAGIC} ->
{ok, StartPos};
{ok, Window} ->
find_next_magic(Fd, Window);
{error, Reason} ->
{error, Reason}
end;
{error, Reason} ->
{error, Reason}
end.
-spec find_next_magic(file:fd(), binary()) -> eof | {ok, non_neg_integer()} | {error, term()}.
find_next_magic(Fd, Window) ->
case file:read(Fd, 1) of
eof ->
eof;
{ok, Byte} ->
Tail = binary:part(Window, 1, ?RECORD_MAGIC_SIZE - 1),
NWindow = <<Tail/binary, Byte/binary>>,
case NWindow of
?RECORD_MAGIC ->
case file:position(Fd, cur) of
{ok, Pos} ->
{ok, Pos - ?RECORD_MAGIC_SIZE};
{error, Reason} ->
{error, Reason}
end;
_ ->
find_next_magic(Fd, NWindow)
end;
{error, Reason} ->
{error, Reason}
end.
-spec read_record_after_magic(file:fd(), pos_integer()) ->
{ok, pos_integer(), binary()} | {resync, term()}.
read_record_after_magic(Fd, MaxRecordBytes) ->
HeaderRestSize = ?RECORD_HEADER_SIZE - ?RECORD_MAGIC_SIZE,
case file:read(Fd, HeaderRestSize) of
{ok, <<Version:8/unsigned, HeaderSize:8/unsigned, PacketSize:32/unsigned-big,
Seq:64/unsigned-big>>} ->
read_record_payload(Fd, Version, HeaderSize, PacketSize, Seq, MaxRecordBytes);
{ok, Partial} ->
{resync, {truncated_record_header, Partial}};
eof ->
{resync, truncated_record_header};
{error, Reason} ->
{resync, Reason}
end.
-spec read_record_payload(file:fd(), non_neg_integer(), non_neg_integer(),
non_neg_integer(), non_neg_integer(), pos_integer()) ->
{ok, pos_integer(), binary()} | {resync, term()}.
read_record_payload(_Fd, Version, _HeaderSize, _PacketSize, _Seq, _MaxRecordBytes)
when Version =/= ?RECORD_VERSION ->
{resync, {invalid_record_version, Version}};
read_record_payload(_Fd, _Version, HeaderSize, _PacketSize, _Seq, _MaxRecordBytes)
when HeaderSize =/= ?RECORD_HEADER_SIZE ->
{resync, {invalid_record_header_size, HeaderSize}};
read_record_payload(_Fd, _Version, _HeaderSize, PacketSize, _Seq, MaxRecordBytes)
when PacketSize =:= 0; PacketSize > MaxRecordBytes ->
{resync, {invalid_record_packet_size, PacketSize}};
read_record_payload(_Fd, _Version, _HeaderSize, _PacketSize, Seq, _MaxRecordBytes)
when Seq =:= 0 ->
{resync, {invalid_record_seq, Seq}};
read_record_payload(Fd, Version, HeaderSize, PacketSize, Seq, _MaxRecordBytes) ->
case file:read(Fd, PacketSize + ?RECORD_CRC_SIZE) of
{ok, <<Packet:PacketSize/binary, Crc32:32/unsigned-big>>} ->
validate_record_crc(Version, HeaderSize, PacketSize, Seq, Packet, Crc32);
{ok, Partial} ->
{resync, {truncated_record_payload, PacketSize, byte_size(Partial)}};
eof ->
{resync, {truncated_record_payload, PacketSize, 0}};
{error, Reason} ->
{resync, Reason}
end.
-spec validate_record_crc(non_neg_integer(), non_neg_integer(), non_neg_integer(),
pos_integer(), binary(), non_neg_integer()) ->
{ok, pos_integer(), binary()} | {resync, term()}.
validate_record_crc(Version, HeaderSize, PacketSize, Seq, Packet, Crc32) ->
Magic = ?RECORD_MAGIC,
Header = <<Magic/binary, Version:8/unsigned, HeaderSize:8/unsigned,
PacketSize:32/unsigned-big, Seq:64/unsigned-big>>,
case erlang:crc32(<<Header/binary, Packet/binary>>) of
Crc32 ->
{ok, Seq, Packet};
Expected ->
{resync, {crc32_mismatch, Seq, Expected, Crc32}}
end.