%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2017. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
-module(raw_file_io_delayed).
-behavior(gen_statem).
-export([close/1, sync/1, datasync/1, truncate/1, advise/4, allocate/3,
position/2, write/2, pwrite/2, pwrite/3,
read_line/1, read/2, pread/2, pread/3]).
%% OTP internal.
-export([ipread_s32bu_p32bu/3, sendfile/8]).
-export([open_layer/3]).
-export([init/1, callback_mode/0, terminate/3]).
-export([opening/3, opened/3]).
-include("file_int.hrl").
open_layer(Filename, Modes, Options) ->
Secret = make_ref(),
case gen_statem:start(?MODULE, {self(), Secret, Options}, []) of
{ok, Pid} ->
gen_statem:call(Pid, {'$open', Secret, Filename, Modes}, infinity);
Other ->
Other
end.
callback_mode() -> state_functions.
init({Owner, Secret, Options}) ->
Monitor = monitor(process, Owner),
Defaults =
#{ owner => Owner,
monitor => Monitor,
secret => Secret,
timer => none,
pid => self(),
buffer => prim_buffer:new(),
delay_size => 64 bsl 10,
delay_time => 2000 },
Data = fill_delay_values(Defaults, Options),
{ok, opening, Data}.
fill_delay_values(Data, []) ->
Data;
fill_delay_values(Data, [{delayed_write, Size, Time} | Options]) ->
fill_delay_values(Data#{ delay_size => Size, delay_time => Time }, Options);
fill_delay_values(Data, [_ | Options]) ->
fill_delay_values(Data, Options).
opening({call, From}, {'$open', Secret, Filename, Modes}, #{ secret := Secret } = Data) ->
case raw_file_io:open(Filename, Modes) of
{ok, PrivateFd} ->
PublicData = maps:with([owner, buffer, delay_size, pid], Data),
PublicFd = #file_descriptor{ module = ?MODULE, data = PublicData },
NewData = Data#{ handle => PrivateFd },
Response = {ok, PublicFd},
{next_state, opened, NewData, [{reply, From, Response}]};
Other ->
{stop_and_reply, normal, [{reply, From, Other}]}
end;
opening(_Event, _Contents, _Data) ->
{keep_state_and_data, [postpone]}.
%%
opened(info, {'$timed_out', Secret}, #{ secret := Secret } = Data) ->
%% If the user writes something at this exact moment, the flush will fail
%% and the timer won't reset on the next write since the buffer won't be
%% empty (Unless we collided on a flush). We therefore reset the timeout to
%% ensure that data won't sit idle for extended periods of time.
case try_flush_write_buffer(Data) of
busy -> gen_statem:cast(self(), '$reset_timeout');
ok -> ok
end,
{keep_state, Data#{ timer => none }, []};
opened(info, {'DOWN', Monitor, process, _Owner, Reason}, #{ monitor := Monitor } = Data) ->
if
Reason =/= kill -> try_flush_write_buffer(Data);
Reason =:= kill -> ignored
end,
{stop, shutdown};
opened(info, _Message, _Data) ->
keep_state_and_data;
opened({call, {Owner, _Tag} = From}, [close], #{ owner := Owner } = Data) ->
case flush_write_buffer(Data) of
ok ->
#{ handle := PrivateFd } = Data,
Response = ?CALL_FD(PrivateFd, close, []),
{stop_and_reply, normal, [{reply, From, Response}]};
Other ->
{stop_and_reply, normal, [{reply, From, Other}]}
end;
opened({call, {Owner, _Tag} = From}, '$wait', #{ owner := Owner }) ->
%% Used in write/2 to synchronize writes on lock conflicts.
{keep_state_and_data, [{reply, From, ok}]};
opened({call, {Owner, _Tag} = From}, '$synchronous_flush', #{ owner := Owner } = Data) ->
cancel_flush_timeout(Data),
Response = flush_write_buffer(Data),
{keep_state_and_data, [{reply, From, Response}]};
opened({call, {Owner, _Tag} = From}, Command, #{ owner := Owner } = Data) ->
Response =
case flush_write_buffer(Data) of
ok -> dispatch_command(Data, Command);
Other -> Other
end,
{keep_state_and_data, [{reply, From, Response}]};
opened({call, _From}, _Command, _Data) ->
%% The client functions filter this out, so we'll crash if the user does
%% anything stupid on purpose.
{shutdown, protocol_violation};
opened(cast, '$reset_timeout', #{ delay_time := Timeout, secret := Secret } = Data) ->
cancel_flush_timeout(Data),
Timer = erlang:send_after(Timeout, self(), {'$timed_out', Secret}),
{keep_state, Data#{ timer => Timer }, []};
opened(cast, _Message, _Data) ->
{keep_state_and_data, []}.
dispatch_command(Data, [Function | Args]) ->
#{ handle := Handle } = Data,
Module = Handle#file_descriptor.module,
apply(Module, Function, [Handle | Args]).
cancel_flush_timeout(#{ timer := none }) ->
ok;
cancel_flush_timeout(#{ timer := Timer }) ->
_ = erlang:cancel_timer(Timer, [{async, true}]),
ok.
try_flush_write_buffer(#{ buffer := Buffer, handle := PrivateFd }) ->
case prim_buffer:try_lock(Buffer) of
acquired ->
flush_write_buffer_1(Buffer, PrivateFd),
prim_buffer:unlock(Buffer),
ok;
busy ->
busy
end.
%% This is only safe to use when there is no chance of conflict with the owner
%% process, or in other words, "during synchronous calls outside of the locked
%% section of write/2"
flush_write_buffer(#{ buffer := Buffer, handle := PrivateFd }) ->
acquired = prim_buffer:try_lock(Buffer),
Result = flush_write_buffer_1(Buffer, PrivateFd),
prim_buffer:unlock(Buffer),
Result.
flush_write_buffer_1(Buffer, PrivateFd) ->
case prim_buffer:size(Buffer) of
Size when Size > 0 ->
?CALL_FD(PrivateFd, write, [prim_buffer:read_iovec(Buffer, Size)]);
0 ->
ok
end.
terminate(_Reason, _State, _Data) ->
ok.
%% Client functions
write(Fd, IOData) ->
try
enqueue_write(Fd, erlang:iolist_to_iovec(IOData))
catch
error:badarg -> {error, badarg}
end.
enqueue_write(_Fd, []) ->
ok;
enqueue_write(Fd, IOVec) ->
%% get_fd_data will reject everyone except the process that opened the Fd,
%% so we can't race with anyone except the wrapper process.
#{ delay_size := DelaySize,
buffer := Buffer,
pid := Pid } = get_fd_data(Fd),
case prim_buffer:try_lock(Buffer) of
acquired ->
%% (The wrapper process will exit without flushing if we're killed
%% while holding the lock).
enqueue_write_locked(Pid, Buffer, DelaySize, IOVec);
busy ->
%% This can only happen while we're processing a timeout in the
%% wrapper process, so we perform a bogus call to get a completion
%% notification before trying again.
gen_statem:call(Pid, '$wait'),
enqueue_write(Fd, IOVec)
end.
enqueue_write_locked(Pid, Buffer, DelaySize, IOVec) ->
%% The synchronous operations (write, forced flush) are safe since we're
%% running on the only process that can fill the buffer; a timeout being
%% processed just before $synchronous_flush will cause the flush to nop,
%% and a timeout sneaking in just before a synchronous write won't do
%% anything since the buffer is guaranteed to be empty at that point.
BufSize = prim_buffer:size(Buffer),
case is_iovec_smaller_than(IOVec, DelaySize - BufSize) of
true when BufSize > 0 ->
prim_buffer:write(Buffer, IOVec),
prim_buffer:unlock(Buffer);
true ->
prim_buffer:write(Buffer, IOVec),
prim_buffer:unlock(Buffer),
gen_statem:cast(Pid, '$reset_timeout');
false when BufSize > 0 ->
prim_buffer:write(Buffer, IOVec),
prim_buffer:unlock(Buffer),
gen_statem:call(Pid, '$synchronous_flush');
false ->
prim_buffer:unlock(Buffer),
gen_statem:call(Pid, [write, IOVec])
end.
%% iolist_size/1 will always look through the entire list to get a precise
%% amount, which is pretty inefficient since we only need to know whether we've
%% hit the buffer threshold or not.
%%
%% We only handle the binary case since write/2 forcibly translates input to
%% erlang:iovec().
is_iovec_smaller_than(IOVec, Max) ->
is_iovec_smaller_than_1(IOVec, Max, 0).
is_iovec_smaller_than_1(_IOVec, Max, Acc) when Acc >= Max ->
false;
is_iovec_smaller_than_1([], _Max, _Acc) ->
true;
is_iovec_smaller_than_1([Binary | Rest], Max, Acc) when is_binary(Binary) ->
is_iovec_smaller_than_1(Rest, Max, Acc + byte_size(Binary)).
close(Fd) ->
wrap_call(Fd, [close]).
sync(Fd) ->
wrap_call(Fd, [sync]).
datasync(Fd) ->
wrap_call(Fd, [datasync]).
truncate(Fd) ->
wrap_call(Fd, [truncate]).
advise(Fd, Offset, Length, Advise) ->
wrap_call(Fd, [advise, Offset, Length, Advise]).
allocate(Fd, Offset, Length) ->
wrap_call(Fd, [allocate, Offset, Length]).
position(Fd, Mark) ->
wrap_call(Fd, [position, Mark]).
pwrite(Fd, Offset, IOData) ->
try
CompactedData = erlang:iolist_to_iovec(IOData),
wrap_call(Fd, [pwrite, Offset, CompactedData])
catch
error:badarg -> {error, badarg}
end.
pwrite(Fd, LocBytes) ->
try
CompactedLocBytes =
[ {Offset, erlang:iolist_to_iovec(IOData)} ||
{Offset, IOData} <- LocBytes ],
wrap_call(Fd, [pwrite, CompactedLocBytes])
catch
error:badarg -> {error, badarg}
end.
read_line(Fd) ->
wrap_call(Fd, [read_line]).
read(Fd, Size) ->
wrap_call(Fd, [read, Size]).
pread(Fd, Offset, Size) ->
wrap_call(Fd, [pread, Offset, Size]).
pread(Fd, LocNums) ->
wrap_call(Fd, [pread, LocNums]).
ipread_s32bu_p32bu(Fd, Offset, MaxSize) ->
wrap_call(Fd, [ipread_s32bu_p32bu, Offset, MaxSize]).
sendfile(_,_,_,_,_,_,_,_) ->
{error, enotsup}.
wrap_call(Fd, Command) ->
#{ pid := Pid } = get_fd_data(Fd),
try gen_statem:call(Pid, Command, infinity) of
Result -> Result
catch
exit:{noproc, _StackTrace} -> {error, einval}
end.
get_fd_data(#file_descriptor{ data = Data }) ->
#{ owner := Owner } = Data,
case self() of
Owner -> Data;
_ -> error(not_on_controlling_process)
end.