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 %%% - max_record_bytes record payload
%%% %%%
%%% record %%% record
%%% - 4 unsigned big-endian payload size %%% - 8 magic bytes <<"EFKAOBX1">>
%%% - term_to_binary({Seq, Packet}) payload %%% - 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 %%% - `append/2' record writer segment fsync
@ -32,15 +40,16 @@
%%% - outbox live segment open/1 max_segments %%% - outbox live segment open/1 max_segments
%%% segment live segment %%% segment live segment
%%% packet %%% packet
%%% - segment live segment 5 writer %%% - segment live segment writer
%%% `segment-(N + 1).log' %%% `segment-(N + 1).log'
%%% %%%
%%% %%%
%%% - `next/1' end_seq acked_seq live segment %%% - `next/1' end_seq acked_seq live segment
%%% segment Seq acked_seq record %%% segment Seq acked_seq record
%%% - efka_iot_client ack packet %%% - efka_iot_client ack packet
%%% - `ack/2' Seq %%% - `ack/2' Seq Seq
%%% acked_seq %%% record ack record Seq
%%% segment
%%% %%%
%%% segment %%% segment
%%% - `ack/2' segment record segment %%% - `ack/2' segment record segment
@ -83,6 +92,11 @@
-define(METADATA_FILE, "metadata.term"). -define(METADATA_FILE, "metadata.term").
-define(SEGMENT_PREFIX, "segment-"). -define(SEGMENT_PREFIX, "segment-").
-define(SEGMENT_EXT, ".log"). -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() :: #{ -type open_options() :: #{
dir := file:filename_all(), 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()}. -spec ack(pos_integer(), outbox()) -> {ok, outbox()} | {error, term()}.
ack(Seq, Outbox = #outbox{acked_seq = AckedSeq}) when is_integer(Seq), Seq =< AckedSeq -> ack(Seq, Outbox = #outbox{acked_seq = AckedSeq}) when is_integer(Seq), Seq =< AckedSeq ->
{ok, Outbox}; {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}, AckedOutbox = Outbox#outbox{acked_seq = Seq},
maybe maybe
ok ?= persist_metadata(AckedOutbox), ok ?= persist_metadata(AckedOutbox),
@ -188,7 +202,7 @@ ack(Seq, Outbox = #outbox{acked_seq = AckedSeq}) when is_integer(Seq), Seq =:= A
{error, Reason} {error, Reason}
end; end;
ack(Seq, #outbox{acked_seq = AckedSeq}) when is_integer(Seq) -> 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()}. -spec ensure_dir(file:filename_all()) -> ok | {error, term()}.
ensure_dir(Dir) -> ensure_dir(Dir) ->
@ -491,9 +505,12 @@ scan_segment(Fd, Id, Path, MaxRecordBytes, StartSeq, EndSeq, Records) ->
-spec encode_record(pos_integer(), binary()) -> binary(). -spec encode_record(pos_integer(), binary()) -> binary().
encode_record(Seq, Packet) -> encode_record(Seq, Packet) ->
Payload = term_to_binary({Seq, Packet}), PacketSize = byte_size(Packet),
Size = byte_size(Payload), Magic = ?RECORD_MAGIC,
<<Size:32/unsigned-big, Payload/binary>>. 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()) -> -spec read_next_unacked(file:fd(), non_neg_integer(), pos_integer()) ->
eof | {ok, pos_integer(), binary()} | {error, term()}. 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()}. -spec read_record(file:fd(), pos_integer()) -> eof | {ok, pos_integer(), binary()} | {error, term()}.
read_record(Fd, MaxRecordBytes) -> read_record(Fd, MaxRecordBytes) ->
case file:read(Fd, 4) of case find_next_magic(Fd) of
eof -> eof ->
eof; eof;
{ok, <<Size:32/unsigned-big>>} when Size > 0, Size =< MaxRecordBytes -> {ok, StartPos} ->
read_record_payload(Fd, Size); case read_record_after_magic(Fd, MaxRecordBytes) of
{ok, <<Size:32/unsigned-big>>} -> {ok, Seq, Packet} ->
{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, Seq, Packet}; {ok, Seq, Packet};
Other -> {resync, _Reason} ->
{error, {invalid_record, Other}} case file:position(Fd, {bof, StartPos + 1}) of
catch {ok, _} ->
error:Reason -> read_record(Fd, MaxRecordBytes);
{error, {invalid_record, Reason}} {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. end.