summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/rabbit.hrl22
-rw-r--r--include/rabbit_queue.hrl58
-rw-r--r--src/file_handle_cache.erl455
-rw-r--r--src/rabbit.erl23
-rw-r--r--src/rabbit_amqqueue.erl143
-rw-r--r--src/rabbit_amqqueue_process.erl699
-rw-r--r--src/rabbit_amqqueue_sup.erl4
-rw-r--r--src/rabbit_basic.erl17
-rw-r--r--src/rabbit_binary_generator.erl19
-rw-r--r--src/rabbit_channel.erl10
-rw-r--r--src/rabbit_control.erl5
-rw-r--r--src/rabbit_guid.erl7
-rw-r--r--src/rabbit_memory_manager.erl404
-rw-r--r--src/rabbit_mnesia.erl11
-rw-r--r--src/rabbit_msg_file.erl141
-rw-r--r--src/rabbit_msg_store.erl1080
-rw-r--r--src/rabbit_persister.erl523
-rw-r--r--src/rabbit_queue_index.erl862
-rw-r--r--src/rabbit_queue_prefetcher.erl295
-rw-r--r--src/rabbit_tests.erl698
-rw-r--r--src/rabbit_variable_queue.erl1019
21 files changed, 5555 insertions, 940 deletions
diff --git a/include/rabbit.hrl b/include/rabbit.hrl
index 5703d0d619..330eef80d7 100644
--- a/include/rabbit.hrl
+++ b/include/rabbit.hrl
@@ -62,7 +62,10 @@
-record(listener, {node, protocol, host, port}).
--record(basic_message, {exchange_name, routing_key, content, persistent_key}).
+-record(basic_message, {exchange_name, routing_key, content,
+ guid, is_persistent}).
+
+-record(dq_msg_loc, {queue_and_seq_id, is_delivered, is_persistent, msg_id}).
-record(ssl_socket, {tcp, ssl}).
-record(delivery, {mandatory, immediate, txn, sender, message}).
@@ -83,9 +86,12 @@
-type(info_key() :: atom()).
-type(info() :: {info_key(), any()}).
-type(regexp() :: binary()).
+-type(file_path() :: any()).
+-type(io_device() :: any()).
+-type(file_open_mode() :: any()).
%% this is really an abstract type, but dialyzer does not support them
--type(guid() :: any()).
+-type(guid() :: binary()).
-type(txn() :: guid()).
-type(pkey() :: guid()).
-type(r(Kind) ::
@@ -128,17 +134,24 @@
properties :: amqp_properties(),
properties_bin :: 'none',
payload_fragments_rev :: [binary()]}).
+-type(unencoded_content() :: undecoded_content()).
-type(decoded_content() ::
#content{class_id :: amqp_class_id(),
properties :: amqp_properties(),
properties_bin :: maybe(binary()),
payload_fragments_rev :: [binary()]}).
+-type(encoded_content() ::
+ #content{class_id :: amqp_class_id(),
+ properties :: maybe(amqp_properties()),
+ properties_bin :: binary(),
+ payload_fragments_rev :: [binary()]}).
-type(content() :: undecoded_content() | decoded_content()).
-type(basic_message() ::
#basic_message{exchange_name :: exchange_name(),
routing_key :: routing_key(),
content :: content(),
- persistent_key :: maybe(pkey())}).
+ guid :: guid(),
+ is_persistent :: boolean()}).
-type(message() :: basic_message()).
-type(delivery() ::
#delivery{mandatory :: boolean(),
@@ -146,9 +159,6 @@
txn :: maybe(txn()),
sender :: pid(),
message :: message()}).
-%% this really should be an abstract type
--type(msg_id() :: non_neg_integer()).
--type(msg() :: {queue_name(), pid(), msg_id(), boolean(), message()}).
-type(listener() ::
#listener{node :: erlang_node(),
protocol :: atom(),
diff --git a/include/rabbit_queue.hrl b/include/rabbit_queue.hrl
new file mode 100644
index 0000000000..165a7e7b99
--- /dev/null
+++ b/include/rabbit_queue.hrl
@@ -0,0 +1,58 @@
+%% The contents of this file are subject to the Mozilla Public License
+%% Version 1.1 (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.mozilla.org/MPL/
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+%% License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% The Original Code is RabbitMQ.
+%%
+%% The Initial Developers of the Original Code are LShift Ltd,
+%% Cohesive Financial Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created before 22-Nov-2008 00:00:00 GMT by LShift Ltd,
+%% Cohesive Financial Technologies LLC, or Rabbit Technologies Ltd
+%% are Copyright (C) 2007-2008 LShift Ltd, Cohesive Financial
+%% Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created by LShift Ltd are Copyright (C) 2007-2009 LShift
+%% Ltd. Portions created by Cohesive Financial Technologies LLC are
+%% Copyright (C) 2007-2009 Cohesive Financial Technologies
+%% LLC. Portions created by Rabbit Technologies Ltd are Copyright
+%% (C) 2007-2009 Rabbit Technologies Ltd.
+%%
+%% All Rights Reserved.
+%%
+%% Contributor(s): ______________________________________.
+%%
+
+-record(alpha,
+ { msg,
+ seq_id,
+ is_delivered,
+ msg_on_disk,
+ index_on_disk
+ }).
+
+-record(beta,
+ { msg_id,
+ seq_id,
+ is_persistent,
+ is_delivered,
+ index_on_disk
+ }).
+
+-record(gamma,
+ { seq_id,
+ count
+ }).
+
+-ifdef(use_specs).
+
+-type(gamma() :: #gamma { seq_id :: non_neg_integer(),
+ count :: non_neg_integer () }).
+
+-endif.
diff --git a/src/file_handle_cache.erl b/src/file_handle_cache.erl
new file mode 100644
index 0000000000..5c1c5a83d4
--- /dev/null
+++ b/src/file_handle_cache.erl
@@ -0,0 +1,455 @@
+%% The contents of this file are subject to the Mozilla Public License
+%% Version 1.1 (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.mozilla.org/MPL/
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+%% License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% The Original Code is RabbitMQ.
+%%
+%% The Initial Developers of the Original Code are LShift Ltd,
+%% Cohesive Financial Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created before 22-Nov-2008 00:00:00 GMT by LShift Ltd,
+%% Cohesive Financial Technologies LLC, or Rabbit Technologies Ltd
+%% are Copyright (C) 2007-2008 LShift Ltd, Cohesive Financial
+%% Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created by LShift Ltd are Copyright (C) 2007-2009 LShift
+%% Ltd. Portions created by Cohesive Financial Technologies LLC are
+%% Copyright (C) 2007-2009 Cohesive Financial Technologies
+%% LLC. Portions created by Rabbit Technologies Ltd are Copyright
+%% (C) 2007-2009 Rabbit Technologies Ltd.
+%%
+%% All Rights Reserved.
+%%
+%% Contributor(s): ______________________________________.
+%%
+
+-module(file_handle_cache).
+
+-export([open/3, close/1, read/2, append/2, sync/1, position/2, truncate/1,
+ last_sync_offset/1, current_virtual_offset/1, current_raw_offset/1,
+ append_write_buffer/1, copy/3]).
+
+%%----------------------------------------------------------------------------
+
+-record(file,
+ { reader_count,
+ has_writer,
+ path
+ }).
+
+-record(handle,
+ { hdl,
+ offset,
+ trusted_offset,
+ is_dirty,
+ write_buffer_size,
+ write_buffer_size_limit,
+ write_buffer,
+ at_eof,
+ is_write,
+ is_read,
+ mode,
+ options,
+ global_key,
+ last_used_at
+ }).
+
+%%----------------------------------------------------------------------------
+%% Specs
+
+-ifdef(use_specs).
+
+-type(ref() :: any()).
+-type(error() :: {'error', any()}).
+-type(ok_or_error() :: ('ok' | error())).
+-type(position() :: ('bof' | 'eof' | {'bof',integer()} | {'eof',integer()}
+ | {'cur',integer()} | integer())).
+
+-spec(open/3 :: (string(), [any()], [any()]) -> ({'ok', ref()} | error())).
+-spec(close/1 :: (ref()) -> ('ok' | error())).
+-spec(read/2 :: (ref(), integer()) ->
+ ({'ok', ([char()]|binary())} | eof | error())).
+-spec(append/2 :: (ref(), iodata()) -> ok_or_error()).
+-spec(sync/1 :: (ref()) -> ok_or_error()).
+-spec(position/2 :: (ref(), position()) ->
+ ({'ok', non_neg_integer()} | error())).
+-spec(truncate/1 :: (ref()) -> ok_or_error()).
+-spec(last_sync_offset/1 :: (ref()) -> ({'ok', integer()} | error())).
+-spec(current_virtual_offset/1 :: (ref()) -> ({'ok', integer()} | error())).
+-spec(current_raw_offset/1 :: (ref()) -> ({'ok', integer()} | error())).
+-spec(append_write_buffer/1 :: (ref()) -> ok_or_error()).
+-spec(copy/3 :: (ref(), ref(), non_neg_integer()) ->
+ ({'ok', integer()} | error())).
+
+-endif.
+
+%%----------------------------------------------------------------------------
+%% Public API
+
+open(Path, Mode, Options) ->
+ case is_appender(Mode) of
+ true -> {error, append_not_supported};
+ false ->
+ Path1 = filename:absname(Path),
+ case get({Path1, fhc_path}) of
+ {gref, GRef} ->
+ #file { reader_count = RCount, has_writer = HasWriter }
+ = File = get({GRef, fhc_file}),
+ Mode1 = lists:usort(Mode),
+ IsWriter = is_writer(Mode1),
+ case IsWriter andalso HasWriter of
+ true ->
+ {error, writer_exists};
+ false ->
+ RCount1 = case is_reader(Mode1) of
+ true -> RCount + 1;
+ false -> RCount
+ end,
+ put({GRef, fhc_file},
+ File #file {
+ reader_count = RCount1,
+ has_writer = HasWriter orelse IsWriter }),
+ Ref = make_ref(),
+ case open1(Path1, Mode1, Options, Ref, GRef) of
+ {ok, _Handle} -> {ok, Ref};
+ Error -> Error
+ end
+ end;
+ undefined ->
+ GRef = make_ref(),
+ put({Path1, fhc_path}, {gref, GRef}),
+ put({GRef, fhc_file},
+ #file { reader_count = 0, has_writer = false,
+ path = Path1 }),
+ open(Path, Mode, Options)
+ end
+ end.
+
+close(Ref) ->
+ case erase({Ref, fhc_handle}) of
+ undefined -> ok;
+ Handle ->
+ case write_buffer(Handle) of
+ {ok, #handle { hdl = Hdl, global_key = GRef, is_dirty = IsDirty,
+ is_read = IsReader, is_write = IsWriter }} ->
+ case Hdl of
+ closed -> ok;
+ _ -> ok = case IsDirty of
+ true -> file:sync(Hdl);
+ false -> ok
+ end,
+ ok = file:close(Hdl)
+ end,
+ #file { reader_count = RCount, has_writer = HasWriter,
+ path = Path } = File = get({GRef, fhc_file}),
+ RCount1 = case IsReader of
+ true -> RCount - 1;
+ false -> RCount
+ end,
+ HasWriter1 = HasWriter andalso not IsWriter,
+ case RCount1 =:= 0 andalso not HasWriter1 of
+ true -> erase({GRef, fhc_file}),
+ erase({Path, fhc_path});
+ false -> put({GRef, fhc_file},
+ File #file { reader_count = RCount1,
+ has_writer = HasWriter1 })
+ end,
+ ok;
+ {Error, Handle1} ->
+ put({Ref, fhc_handle}, Handle1),
+ Error
+ end
+ end.
+
+read(Ref, Count) ->
+ case get_or_reopen(Ref) of
+ {ok, #handle { is_read = false }} ->
+ {error, not_open_for_reading};
+ {ok, Handle} ->
+ {Result, Handle1} =
+ case write_buffer(Handle) of
+ {ok, Handle2 = #handle { hdl = Hdl, offset = Offset }} ->
+ case file:read(Hdl, Count) of
+ {ok, Data} = Obj ->
+ Size = iolist_size(Data),
+ {Obj,
+ Handle2 #handle { offset = Offset + Size }};
+ eof -> {eof, Handle2 #handle { at_eof = true }};
+ Error -> {Error, Handle2}
+ end;
+ {Error, Handle2} -> {Error, Handle2}
+ end,
+ put({Ref, fhc_handle}, Handle1),
+ Result;
+ Error -> Error
+ end.
+
+append(Ref, Data) ->
+ case get_or_reopen(Ref) of
+ {ok, #handle { is_write = false }} ->
+ {error, not_open_for_writing};
+ {ok, Handle} ->
+ {Result, Handle1} =
+ case maybe_seek(eof, Handle) of
+ {{ok, _Offset}, Handle2 = #handle { at_eof = true }} ->
+ write_to_buffer(Data, Handle2);
+ {{error, _} = Error, Handle2} ->
+ {Error, Handle2}
+ end,
+ put({Ref, fhc_handle}, Handle1),
+ Result;
+ Error -> Error
+ end.
+
+sync(Ref) ->
+ case get_or_reopen(Ref) of
+ {ok, #handle { is_dirty = false, write_buffer = [] }} ->
+ ok;
+ {ok, Handle} ->
+ %% write_buffer will set is_dirty, or leave it set if buffer empty
+ {Result, Handle1} =
+ case write_buffer(Handle) of
+ {ok, Handle2 = #handle {
+ hdl = Hdl, offset = Offset, is_dirty = true }} ->
+ case file:sync(Hdl) of
+ ok -> {ok,
+ Handle2 #handle { trusted_offset = Offset,
+ is_dirty = false }};
+ Error -> {Error, Handle2}
+ end;
+ Error -> {Error, Handle}
+ end,
+ put({Ref, fhc_handle}, Handle1),
+ Result;
+ Error -> Error
+ end.
+
+position(Ref, NewOffset) ->
+ case get_or_reopen(Ref) of
+ {ok, Handle} ->
+ {Result, Handle1} =
+ case write_buffer(Handle) of
+ {ok, Handle2} -> maybe_seek(NewOffset, Handle2);
+ {Error, Handle2} -> {Error, Handle2}
+ end,
+ put({Ref, fhc_handle}, Handle1),
+ Result;
+ Error -> Error
+ end.
+
+truncate(Ref) ->
+ case get_or_reopen(Ref) of
+ {ok, #handle { is_write = false }} ->
+ {error, not_open_for_writing};
+ {ok, Handle} ->
+ {Result, Handle1} =
+ case write_buffer(Handle) of
+ {ok,
+ Handle2 = #handle { hdl = Hdl, offset = Offset,
+ trusted_offset = TrustedOffset }} ->
+ case file:truncate(Hdl) of
+ ok ->
+ {ok,
+ Handle2 #handle {
+ at_eof = true,
+ trusted_offset = lists:min([Offset,
+ TrustedOffset])
+ }};
+ Error -> {Error, Handle2}
+ end;
+ {Error, Handle2} -> {Error, Handle2}
+ end,
+ put({Ref, fhc_handle}, Handle1),
+ Result;
+ Error -> Error
+ end.
+
+last_sync_offset(Ref) ->
+ case get_or_reopen(Ref) of
+ {ok, #handle { trusted_offset = TrustedOffset }} -> {ok, TrustedOffset};
+ Error -> Error
+ end.
+
+current_virtual_offset(Ref) ->
+ case get_or_reopen(Ref) of
+ {ok, #handle { at_eof = true, is_write = true, offset = Offset,
+ write_buffer_size = Size }} ->
+ {ok, Offset + Size};
+ {ok, #handle { offset = Offset }} -> {ok, Offset};
+ Error -> Error
+ end.
+
+current_raw_offset(Ref) ->
+ case get_or_reopen(Ref) of
+ {ok, #handle { offset = Offset }} -> {ok, Offset};
+ Error -> Error
+ end.
+
+append_write_buffer(Ref) ->
+ case get_or_reopen(Ref) of
+ {ok, Handle} ->
+ {Result, Handle1} = write_buffer(Handle),
+ put({Ref, fhc_handle}, Handle1),
+ Result;
+ Error -> Error
+ end.
+
+copy(Src, Dest, Count) ->
+ case get_or_reopen(Src) of
+ {ok, SHandle = #handle { is_read = true }} ->
+ case get_or_reopen(Dest) of
+ {ok, DHandle = #handle { is_write = true }} ->
+ {Result, SHandle1, DHandle1} =
+ case write_buffer(SHandle) of
+ {ok, SHandle2 = #handle { hdl = SHdl,
+ offset = SOffset }} ->
+ case write_buffer(DHandle) of
+ {ok,
+ DHandle2 = #handle { hdl = DHdl,
+ offset = DOffset }} ->
+ Result1 = file:copy(SHdl, DHdl, Count),
+ case Result1 of
+ {ok, Count1} ->
+ {Result1,
+ SHandle2 #handle {
+ offset = SOffset + Count1 },
+ DHandle2 #handle {
+ offset = DOffset + Count1 }};
+ Error ->
+ {Error, SHandle2, DHandle2}
+ end;
+ Error -> {Error, SHandle2, DHandle}
+ end;
+ Error -> {Error, SHandle, DHandle}
+ end,
+ put({Src, fhc_handle}, SHandle1),
+ put({Dest, fhc_handle}, DHandle1),
+ Result;
+ {ok, _} -> {error, destination_not_open_for_writing};
+ Error -> Error
+ end;
+ {ok, _} -> {error, source_not_open_for_reading};
+ Error -> Error
+ end.
+
+
+%%----------------------------------------------------------------------------
+%% Internal functions
+%%----------------------------------------------------------------------------
+
+get_or_reopen(Ref) ->
+ case get({Ref, fhc_handle}) of
+ undefined -> {error, not_open, Ref};
+ #handle { hdl = closed, mode = Mode, global_key = GRef,
+ options = Options } ->
+ #file { path = Path } = get({GRef, fhc_file}),
+ open1(Path, Mode, Options, Ref, GRef);
+ Handle -> {ok, Handle #handle { last_used_at = now() }}
+ end.
+
+open1(Path, Mode, Options, Ref, GRef) ->
+ case file:open(Path, Mode) of
+ {ok, Hdl} ->
+ WriteBufferSize =
+ case proplists:get_value(write_buffer, Options, unbuffered) of
+ unbuffered -> 0;
+ infinity -> infinity;
+ N when is_integer(N) -> N
+ end,
+ Handle =
+ #handle { hdl = Hdl, offset = 0, trusted_offset = 0,
+ write_buffer_size = 0, options = Options,
+ write_buffer_size_limit = WriteBufferSize,
+ write_buffer = [], at_eof = false, mode = Mode,
+ is_write = is_writer(Mode), is_read = is_reader(Mode),
+ global_key = GRef, last_used_at = now(),
+ is_dirty = false },
+ put({Ref, fhc_handle}, Handle),
+ {ok, Handle};
+ {error, Reason} ->
+ {error, Reason}
+ end.
+
+maybe_seek(NewOffset, Handle = #handle { hdl = Hdl, at_eof = AtEoF,
+ offset = Offset }) ->
+ {AtEoF1, NeedsSeek} = needs_seek(AtEoF, Offset, NewOffset),
+ Result = case NeedsSeek of
+ true -> file:position(Hdl, NewOffset);
+ false -> {ok, Offset}
+ end,
+ case Result of
+ {ok, Offset1} ->
+ {Result, Handle #handle { at_eof = AtEoF1, offset = Offset1 }};
+ {error, _} = Error -> {Error, Handle}
+ end.
+
+write_to_buffer(Data, Handle = #handle { hdl = Hdl, offset = Offset,
+ write_buffer_size_limit = 0 }) ->
+ Offset1 = Offset + iolist_size(Data),
+ {file:write(Hdl, Data),
+ Handle #handle { is_dirty = true, offset = Offset1 }};
+write_to_buffer(Data, Handle =
+ #handle { write_buffer = WriteBuffer,
+ write_buffer_size = Size,
+ write_buffer_size_limit = Limit }) ->
+ Size1 = Size + iolist_size(Data),
+ Handle1 = Handle #handle { write_buffer = [ Data | WriteBuffer ],
+ write_buffer_size = Size1 },
+ case Limit /= infinity andalso Size1 > Limit of
+ true -> write_buffer(Handle1);
+ false -> {ok, Handle1}
+ end.
+
+write_buffer(Handle = #handle { write_buffer = [] }) ->
+ {ok, Handle};
+write_buffer(Handle = #handle { hdl = Hdl, offset = Offset,
+ write_buffer = WriteBuffer,
+ write_buffer_size = DataSize,
+ at_eof = true }) ->
+ case file:write(Hdl, lists:reverse(WriteBuffer)) of
+ ok ->
+ Offset1 = Offset + DataSize,
+ {ok, Handle #handle { offset = Offset1, write_buffer = [],
+ write_buffer_size = 0, is_dirty = true }};
+ {error, _} = Error ->
+ {Error, Handle}
+ end.
+
+is_reader(Mode) -> lists:member(read, Mode).
+
+is_writer(Mode) -> lists:member(write, Mode).
+
+is_appender(Mode) -> lists:member(append, Mode).
+
+needs_seek(AtEof, _CurOffset, DesiredOffset)
+ when DesiredOffset == cur orelse DesiredOffset == {cur, 0} ->
+ {AtEof, false};
+needs_seek(true, _CurOffset, DesiredOffset)
+ when DesiredOffset == eof orelse DesiredOffset == {eof, 0} ->
+ {true, false};
+needs_seek(false, _CurOffset, DesiredOffset)
+ when DesiredOffset == eof orelse DesiredOffset == {eof, 0} ->
+ {true, true};
+needs_seek(AtEof, 0, DesiredOffset)
+ when DesiredOffset == bof orelse DesiredOffset == {bof, 0} ->
+ {AtEof, false};
+needs_seek(AtEof, CurOffset, CurOffset) ->
+ {AtEof, false};
+needs_seek(true, CurOffset, {bof, DesiredOffset})
+ when DesiredOffset >= CurOffset ->
+ {true, true};
+needs_seek(true, _CurOffset, {cur, DesiredOffset})
+ when DesiredOffset > 0 ->
+ {true, true};
+needs_seek(true, CurOffset, DesiredOffset) %% same as {bof, DO}
+ when is_integer(DesiredOffset) andalso DesiredOffset >= CurOffset ->
+ {true, true};
+%% because we can't really track size, we could well end up at EoF and not know
+needs_seek(_AtEoF, _CurOffset, _DesiredOffset) ->
+ {false, true}.
diff --git a/src/rabbit.erl b/src/rabbit.erl
index b17711b473..1c5a07c7b9 100644
--- a/src/rabbit.erl
+++ b/src/rabbit.erl
@@ -37,7 +37,7 @@
-export([start/2, stop/1]).
--export([log_location/1]).
+-export([log_location/1, start_child/2]).
-import(application).
-import(mnesia).
@@ -67,6 +67,7 @@
{nodes, [erlang_node()]} |
{running_nodes, [erlang_node()]}]).
-spec(log_location/1 :: ('sasl' | 'kernel') -> log_location()).
+-spec(start_child/2 :: (atom(), [any()]) -> 'ok').
-endif.
@@ -154,6 +155,7 @@ start(normal, []) ->
ok = rabbit_amqqueue:start(),
ok = start_child(rabbit_router),
+ ok = start_child(rabbit_guid),
ok = start_child(rabbit_node_monitor),
ok = start_child(rabbit_memory_monitor)
end},
@@ -161,15 +163,14 @@ start(normal, []) ->
fun () ->
ok = maybe_insert_default_data(),
ok = rabbit_exchange:recover(),
- ok = rabbit_amqqueue:recover()
- end},
- {"persister",
- fun () ->
- ok = start_child(rabbit_persister)
- end},
- {"guid generator",
- fun () ->
- ok = start_child(rabbit_guid)
+ DurableQueues = rabbit_amqqueue:find_durable_queues(),
+ ok = rabbit_queue_index:start_msg_store(DurableQueues),
+ {ok, _RealDurableQueues} = rabbit_amqqueue:recover(DurableQueues)
+ %% TODO - RealDurableQueues is a subset of
+ %% DurableQueues. It may have queues removed which
+ %% have since been recreated on another node in our
+ %% cluster. We need to remove DurableQueues --
+ %% RealDurableQueues somehow. See also bug 20916
end},
{"builtin applications",
fun () ->
@@ -282,7 +283,7 @@ start_child(Mod) ->
start_child(Mod, Args) ->
{ok,_} = supervisor:start_child(rabbit_sup,
{Mod, {Mod, start_link, Args},
- transient, 100, worker, [Mod]}),
+ transient, 5000, worker, [Mod]}),
ok.
ensure_working_log_handlers() ->
diff --git a/src/rabbit_amqqueue.erl b/src/rabbit_amqqueue.erl
index 4abfcd0ba7..82a0f5b4fa 100644
--- a/src/rabbit_amqqueue.erl
+++ b/src/rabbit_amqqueue.erl
@@ -31,16 +31,17 @@
-module(rabbit_amqqueue).
--export([start/0, recover/0, declare/4, delete/3, purge/1]).
--export([internal_declare/2, internal_delete/1]).
+-export([start/0, recover/1, find_durable_queues/0, declare/4, delete/3,
+ purge/1]).
+-export([internal_declare/2, internal_delete/1, remeasure_egress_rate/1]).
-export([pseudo_queue/2]).
-export([lookup/1, with/2, with_or_die/2,
stat/1, stat_all/0, deliver/2, redeliver/2, requeue/3, ack/4]).
-export([list/1, info/1, info/2, info_all/1, info_all/2]).
-export([claim_queue/2]).
-export([basic_get/3, basic_consume/8, basic_cancel/4]).
--export([notify_sent/2, unblock/2, set_queue_duration/2,
- send_memory_monitor_update/1]).
+-export([notify_sent/2, unblock/2, tx_commit_msg_store_callback/4,
+ tx_commit_vq_callback/1]).
-export([commit_all/2, rollback_all/2, notify_down_all/2, limit_all/3]).
-export([on_node_down/1]).
@@ -56,14 +57,19 @@
-ifdef(use_specs).
+-type(msg_id() :: non_neg_integer()).
+-type(msg() :: {queue_name(), pid(), msg_id(), boolean(), message()}).
-type(qstats() :: {'ok', queue_name(), non_neg_integer(), non_neg_integer()}).
-type(qlen() :: {'ok', non_neg_integer()}).
-type(qfun(A) :: fun ((amqqueue()) -> A)).
-type(ok_or_errors() ::
'ok' | {'error', [{'error' | 'exit' | 'throw', any()}]}).
+-type(seq_id() :: non_neg_integer()).
+-type(acktag() :: ('ack_not_on_disk' | {'ack_index_and_store', msg_id(), seq_id()})).
-spec(start/0 :: () -> 'ok').
--spec(recover/0 :: () -> 'ok').
+-spec(recover/1 :: ([amqqueue()]) -> {'ok', [amqqueue()]}).
+-spec(find_durable_queues/0 :: () -> [amqqueue()]).
-spec(declare/4 :: (queue_name(), boolean(), boolean(), amqp_table()) ->
amqqueue()).
-spec(lookup/1 :: (queue_name()) -> {'ok', amqqueue()} | not_found()).
@@ -102,10 +108,12 @@
-spec(basic_cancel/4 :: (amqqueue(), pid(), ctag(), any()) -> 'ok').
-spec(notify_sent/2 :: (pid(), pid()) -> 'ok').
-spec(unblock/2 :: (pid(), pid()) -> 'ok').
--spec(set_queue_duration/2 :: (pid(), number()) -> 'ok').
--spec(send_memory_monitor_update/1 :: (pid()) -> 'ok').
+-spec(tx_commit_msg_store_callback/4 ::
+ (pid(), [message()], [acktag()], {pid(), any()}) -> 'ok').
+-spec(tx_commit_vq_callback/1 :: (pid()) -> 'ok').
-spec(internal_declare/2 :: (amqqueue(), boolean()) -> amqqueue()).
-spec(internal_delete/1 :: (queue_name()) -> 'ok' | not_found()).
+-spec(remeasure_egress_rate/1 :: (pid()) -> 'ok').
-spec(on_node_down/1 :: (erlang_node()) -> 'ok').
-spec(pseudo_queue/2 :: (binary(), pid()) -> amqqueue()).
@@ -121,40 +129,47 @@ start() ->
transient, infinity, supervisor, [rabbit_amqqueue_sup]}),
ok.
-recover() ->
- ok = recover_durable_queues(),
- ok.
-
-recover_durable_queues() ->
+recover(DurableQueues) ->
+ {ok, _RealDurableQueues} = recover_durable_queues(DurableQueues).
+
+recover_durable_queues(DurableQueues) ->
+ RealDurableQueues =
+ lists:foldl(
+ fun (RecoveredQ, Acc) ->
+ Q = start_queue_process(RecoveredQ),
+ %% We need to catch the case where a client connected to
+ %% another node has deleted the queue (and possibly
+ %% re-created it).
+ case rabbit_misc:execute_mnesia_transaction(
+ fun () ->
+ Match =
+ mnesia:match_object(
+ rabbit_durable_queue, RecoveredQ, read),
+ case Match of
+ [_] -> ok = store_queue(Q),
+ true;
+ [] -> false
+ end
+ end) of
+ true -> [Q|Acc];
+ false -> exit(Q#amqqueue.pid, shutdown),
+ Acc
+ end
+ end, [], DurableQueues),
+ {ok, RealDurableQueues}.
+
+find_durable_queues() ->
Node = node(),
- lists:foreach(
- fun (RecoveredQ) ->
- Q = start_queue_process(RecoveredQ),
- %% We need to catch the case where a client connected to
- %% another node has deleted the queue (and possibly
- %% re-created it).
- case rabbit_misc:execute_mnesia_transaction(
- fun () -> case mnesia:match_object(
- rabbit_durable_queue, RecoveredQ, read) of
- [_] -> ok = store_queue(Q),
- true;
- [] -> false
- end
- end) of
- true -> ok;
- false -> exit(Q#amqqueue.pid, shutdown)
- end
- end,
- %% TODO: use dirty ops instead
- rabbit_misc:execute_mnesia_transaction(
- fun () ->
- qlc:e(qlc:q([Q || Q = #amqqueue{pid = Pid}
- <- mnesia:table(rabbit_durable_queue),
- node(Pid) == Node]))
- end)),
- ok.
+ %% TODO: use dirty ops instead
+ rabbit_misc:execute_mnesia_transaction(
+ fun () ->
+ qlc:e(qlc:q([Q || Q = #amqqueue{pid = Pid}
+ <- mnesia:table(rabbit_durable_queue),
+ node(Pid) == Node]))
+ end).
declare(QueueName, Durable, AutoDelete, Args) ->
+ prune_queue_childspecs(),
Q = start_queue_process(#amqqueue{name = QueueName,
durable = Durable,
auto_delete = AutoDelete,
@@ -165,6 +180,10 @@ declare(QueueName, Durable, AutoDelete, Args) ->
internal_declare(Q = #amqqueue{name = QueueName}, WantDefaultBinding) ->
case rabbit_misc:execute_mnesia_transaction(
fun () ->
+ %% we could still find that mnesia has another
+ %% entry here because the queue may exist on
+ %% another node, beyond the knowledge of our own
+ %% local queue_sup.
case mnesia:wread({rabbit_queue, QueueName}) of
[] -> ok = store_queue(Q),
case WantDefaultBinding of
@@ -188,9 +207,30 @@ store_queue(Q = #amqqueue{durable = false}) ->
ok = mnesia:write(rabbit_queue, Q, write),
ok.
-start_queue_process(Q) ->
- {ok, Pid} = supervisor:start_child(rabbit_amqqueue_sup, [Q]),
- Q#amqqueue{pid = Pid}.
+start_queue_process(Q = #amqqueue{name = QueueName}) ->
+ case supervisor:start_child(
+ rabbit_amqqueue_sup,
+ {QueueName, {rabbit_amqqueue_process, start_link, [Q]},
+ %% 4294967295 is 2^32 - 1, which is the highest value allowed
+ temporary, 4294967295, worker, [rabbit_amqqueue_process]}) of
+ {ok, Pid} ->
+ Q#amqqueue{pid = Pid};
+ {error, already_present} ->
+ supervisor:delete_child(rabbit_amqqueue_sup, QueueName),
+ start_queue_process(Q);
+ {error, {already_started, _QPid}} ->
+ case rabbit_misc:execute_mnesia_transaction(
+ fun () ->
+ case mnesia:wread({rabbit_queue, QueueName}) of
+ %% it's vanished in the mean time, try again
+ [] -> try_again;
+ [ExistingQ] -> ExistingQ
+ end
+ end) of
+ try_again -> start_queue_process(Q);
+ ExistingQ -> ExistingQ
+ end
+ end.
add_default_binding(#amqqueue{name = QueueName}) ->
Exchange = rabbit_misc:r(QueueName, exchange, <<>>),
@@ -238,6 +278,7 @@ stat_all() ->
lists:map(fun stat/1, rabbit_misc:dirty_read_all(rabbit_queue)).
delete(#amqqueue{ pid = QPid }, IfUnused, IfEmpty) ->
+ prune_queue_childspecs(),
gen_server2:call(QPid, {delete, IfUnused, IfEmpty}, infinity).
purge(#amqqueue{ pid = QPid }) -> gen_server2:call(QPid, purge, infinity).
@@ -311,11 +352,12 @@ notify_sent(QPid, ChPid) ->
unblock(QPid, ChPid) ->
gen_server2:pcast(QPid, 8, {unblock, ChPid}).
-set_queue_duration(QPid, Duration) ->
- gen_server2:pcast(QPid, 7, {set_queue_duration, Duration}).
+tx_commit_msg_store_callback(QPid, Pubs, AckTags, From) ->
+ gen_server2:pcast(QPid, 8,
+ {tx_commit_msg_store_callback, Pubs, AckTags, From}).
-send_memory_monitor_update(QPid) ->
- gen_server2:pcast(QPid, 7, send_memory_monitor_update).
+tx_commit_vq_callback(QPid) ->
+ gen_server2:pcast(QPid, 8, tx_commit_vq_callback).
internal_delete(QueueName) ->
rabbit_misc:execute_mnesia_transaction(
@@ -330,6 +372,17 @@ internal_delete(QueueName) ->
end
end).
+remeasure_egress_rate(QPid) ->
+ gen_server2:pcast(QPid, 8, remeasure_egress_rate).
+
+prune_queue_childspecs() ->
+ lists:foreach(
+ fun ({Name, undefined, _Type, _Mods}) ->
+ supervisor:delete_child(rabbit_amqqueue_sup, Name);
+ (_) -> ok
+ end, supervisor:which_children(rabbit_amqqueue_sup)),
+ ok.
+
on_node_down(Node) ->
rabbit_misc:execute_mnesia_transaction(
fun () ->
diff --git a/src/rabbit_amqqueue_process.erl b/src/rabbit_amqqueue_process.erl
index 2d264fc274..cd70979a1f 100644
--- a/src/rabbit_amqqueue_process.erl
+++ b/src/rabbit_amqqueue_process.erl
@@ -35,13 +35,16 @@
-behaviour(gen_server2).
--define(UNSENT_MESSAGE_LIMIT, 100).
--define(HIBERNATE_AFTER_MIN, 1000).
--define(DESIRED_HIBERNATE, 10000).
+-define(UNSENT_MESSAGE_LIMIT, 100).
+-define(HIBERNATE_AFTER_MIN, 1000).
+-define(DESIRED_HIBERNATE, 10000).
+-define(SYNC_INTERVAL, 5). %% milliseconds
+-define(EGRESS_REMEASURE_INTERVAL, 5000).
-export([start_link/1]).
--export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2, handle_info/2]).
+-export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2,
+ handle_info/2, handle_pre_hibernate/1]).
-import(queue).
-import(erlang).
@@ -52,20 +55,17 @@
owner,
exclusive_consumer,
has_had_consumers,
+ variable_queue_state,
next_msg_id,
- message_buffer,
active_consumers,
blocked_consumers,
- drain_ratio}).
+ sync_timer_ref,
+ egress_rate_timer_ref
+ }).
-record(consumer, {tag, ack_required}).
--record(tx, {ch_pid, is_persistent, pending_messages, pending_acks}).
-
--record(ratio, {ratio, %% float. messages/microsecond_us
- t0, %% previous timestamp (us)
- next_msg_id %% previous next_msg_id
- }).
+-record(tx, {ch_pid, pending_messages, pending_acks}).
%% These are held in our process dictionary
-record(cr, {consumer_count,
@@ -90,7 +90,8 @@
acks_uncommitted,
consumers,
transactions,
- memory]).
+ memory
+ ]).
%%----------------------------------------------------------------------------
@@ -98,43 +99,105 @@ start_link(Q) ->
gen_server2:start_link(?MODULE, Q, []).
%%----------------------------------------------------------------------------
-init(Q) ->
+
+init(Q = #amqqueue { name = QName }) ->
?LOGDEBUG("Queue starting - ~p~n", [Q]),
- rabbit_memory_monitor:register(self(), {rabbit_amqqueue, set_queue_duration,
- [self()]}),
- %% Beware. This breaks hibernation!
- timer:apply_interval(2500, rabbit_amqqueue, send_memory_monitor_update,
- [self()]),
- {ok, #q{q = Q,
- owner = none,
- exclusive_consumer = none,
- has_had_consumers = false,
- next_msg_id = 1,
- message_buffer = queue:new(),
- active_consumers = queue:new(),
- blocked_consumers = queue:new(),
- drain_ratio = #ratio{ratio = 0.0,
- t0 = now(),
- next_msg_id = 1}
- }, hibernate,
+ process_flag(trap_exit, true),
+ ok = rabbit_memory_manager:register
+ (self(), false, rabbit_amqqueue, set_storage_mode, [self()]),
+ VQS = rabbit_variable_queue:init(QName),
+ State = #q{q = Q,
+ owner = none,
+ exclusive_consumer = none,
+ has_had_consumers = false,
+ variable_queue_state = VQS,
+ next_msg_id = 1,
+ active_consumers = queue:new(),
+ blocked_consumers = queue:new(),
+ sync_timer_ref = undefined,
+ egress_rate_timer_ref = undefined
+ },
+ {ok, State, hibernate,
{backoff, ?HIBERNATE_AFTER_MIN, ?HIBERNATE_AFTER_MIN, ?DESIRED_HIBERNATE}}.
-terminate(_Reason, State) ->
+terminate(shutdown, #q{variable_queue_state = VQS}) ->
+ _VQS = rabbit_variable_queue:terminate(VQS);
+terminate(_Reason, State = #q{variable_queue_state = VQS}) ->
%% FIXME: How do we cancel active subscriptions?
- QName = qname(State),
- lists:foreach(fun (Txn) -> ok = rollback_work(Txn, QName) end,
- all_tx()),
- ok = purge_message_buffer(QName, State#q.message_buffer),
- ok = rabbit_amqqueue:internal_delete(QName).
+ %% Ensure that any persisted tx messages are removed.
+ %% TODO: wait for all in flight tx_commits to complete
+ VQS1 = rabbit_variable_queue:tx_rollback(
+ lists:concat([PM || #tx { pending_messages = PM } <-
+ all_tx_record()]), VQS),
+ %% Delete from disk first. If we crash at this point, when a
+ %% durable queue, we will be recreated at startup, possibly with
+ %% partial content. The alternative is much worse however - if we
+ %% called internal_delete first, we would then have a race between
+ %% the disk delete and a new queue with the same name being
+ %% created and published to.
+ _VQS = rabbit_variable_queue:delete(VQS1),
+ ok = rabbit_amqqueue:internal_delete(qname(State)).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%----------------------------------------------------------------------------
-reply(Reply, NewState) -> {reply, Reply, NewState, hibernate}.
-
-noreply(NewState) -> {noreply, NewState, hibernate}.
+reply(Reply, NewState) ->
+ assert_invariant(NewState),
+ {NewState1, Timeout} = next_state(NewState),
+ {reply, Reply, NewState1, Timeout}.
+
+noreply(NewState) ->
+ assert_invariant(NewState),
+ {NewState1, Timeout} = next_state(NewState),
+ {noreply, NewState1, Timeout}.
+
+next_state(State = #q{variable_queue_state = VQS}) ->
+ next_state1(ensure_egress_rate_timer(State),
+ rabbit_variable_queue:needs_sync(VQS)).
+
+next_state1(State = #q{sync_timer_ref = undefined}, true) ->
+ {start_sync_timer(State), 0};
+next_state1(State, true) ->
+ {State, 0};
+next_state1(State = #q{sync_timer_ref = undefined,
+ variable_queue_state = VQS}, false) ->
+ {State, case rabbit_variable_queue:can_flush_journal(VQS) of
+ true -> 0;
+ false -> hibernate
+ end};
+next_state1(State, false) ->
+ {stop_sync_timer(State), 0}.
+
+ensure_egress_rate_timer(State = #q{egress_rate_timer_ref = undefined}) ->
+ {ok, TRef} = timer:apply_after(?EGRESS_REMEASURE_INTERVAL, rabbit_amqqueue,
+ remeasure_egress_rate, [self()]),
+ State#q{egress_rate_timer_ref = TRef};
+ensure_egress_rate_timer(State = #q{egress_rate_timer_ref = just_measured}) ->
+ State#q{egress_rate_timer_ref = undefined};
+ensure_egress_rate_timer(State) ->
+ State.
+
+stop_egress_rate_timer(State = #q{egress_rate_timer_ref = undefined}) ->
+ State;
+stop_egress_rate_timer(State = #q{egress_rate_timer_ref = just_measured}) ->
+ State#q{egress_rate_timer_ref = undefined};
+stop_egress_rate_timer(State = #q{egress_rate_timer_ref = TRef}) ->
+ {ok, cancel} = timer:cancel(TRef),
+ State#q{egress_rate_timer_ref = undefined}.
+
+start_sync_timer(State = #q{sync_timer_ref = undefined}) ->
+ {ok, TRef} = timer:apply_after(?SYNC_INTERVAL, rabbit_amqqueue,
+ tx_commit_vq_callback, [self()]),
+ State#q{sync_timer_ref = TRef}.
+
+stop_sync_timer(State = #q{sync_timer_ref = TRef}) ->
+ {ok, cancel} = timer:cancel(TRef),
+ State#q{sync_timer_ref = undefined}.
+
+assert_invariant(#q{active_consumers = AC, variable_queue_state = VQS}) ->
+ true = (queue:is_empty(AC) orelse rabbit_variable_queue:is_empty(VQS)).
lookup_ch(ChPid) ->
case get({ch, ChPid}) of
@@ -181,12 +244,12 @@ record_current_channel_tx(ChPid, Txn) ->
%% that wasn't happening already)
store_ch_record((ch_record(ChPid))#cr{txn = Txn}).
-deliver_immediately(Message, Delivered,
- State = #q{q = #amqqueue{name = QName},
- active_consumers = ActiveConsumers,
- blocked_consumers = BlockedConsumers,
- next_msg_id = NextId}) ->
- ?LOGDEBUG("AMQQUEUE ~p DELIVERY:~n~p~n", [QName, Message]),
+deliver_msgs_to_consumers(
+ Funs = {PredFun, DeliverFun}, FunAcc,
+ State = #q{q = #amqqueue{name = QName},
+ active_consumers = ActiveConsumers,
+ blocked_consumers = BlockedConsumers,
+ next_msg_id = NextId}) ->
case queue:out(ActiveConsumers) of
{{value, QEntry = {ChPid, #consumer{tag = ConsumerTag,
ack_required = AckRequired}}},
@@ -194,15 +257,21 @@ deliver_immediately(Message, Delivered,
C = #cr{limiter_pid = LimiterPid,
unsent_message_count = Count,
unacked_messages = UAM} = ch_record(ChPid),
- case rabbit_limiter:can_send(LimiterPid, self(), AckRequired) of
+ IsMsgReady = PredFun(FunAcc, State),
+ case (IsMsgReady andalso
+ rabbit_limiter:can_send( LimiterPid, self(), AckRequired )) of
true ->
+ {{Msg, IsDelivered, AckTag}, FunAcc1, State1} =
+ DeliverFun(AckRequired, FunAcc, State),
+ ?LOGDEBUG("AMQQUEUE ~p DELIVERY:~n~p~n", [QName, Msg]),
rabbit_channel:deliver(
ChPid, ConsumerTag, AckRequired,
- {QName, self(), NextId, Delivered, Message}),
- NewUAM = case AckRequired of
- true -> dict:store(NextId, Message, UAM);
- false -> UAM
- end,
+ {QName, self(), NextId, IsDelivered, Msg}),
+ NewUAM =
+ case AckRequired of
+ true -> dict:store(NextId, {Msg, AckTag}, UAM);
+ false -> UAM
+ end,
NewC = C#cr{unsent_message_count = Count + 1,
unacked_messages = NewUAM},
store_ch_record(NewC),
@@ -218,54 +287,111 @@ deliver_immediately(Message, Delivered,
{ActiveConsumers1,
queue:in(QEntry, BlockedConsumers1)}
end,
- {offered, AckRequired,
- State#q{active_consumers = NewActiveConsumers,
- blocked_consumers = NewBlockedConsumers,
- next_msg_id = NextId + 1}};
- false ->
+ State2 = State1 #q {
+ active_consumers = NewActiveConsumers,
+ blocked_consumers = NewBlockedConsumers,
+ next_msg_id = NextId + 1
+ },
+ deliver_msgs_to_consumers(Funs, FunAcc1, State2);
+ %% if IsMsgReady then we've hit the limiter
+ false when IsMsgReady ->
store_ch_record(C#cr{is_limit_active = true}),
{NewActiveConsumers, NewBlockedConsumers} =
move_consumers(ChPid,
ActiveConsumers,
BlockedConsumers),
- deliver_immediately(
- Message, Delivered,
+ deliver_msgs_to_consumers(
+ Funs, FunAcc,
State#q{active_consumers = NewActiveConsumers,
- blocked_consumers = NewBlockedConsumers})
+ blocked_consumers = NewBlockedConsumers});
+ false ->
+ %% no message was ready, so we don't need to block anyone
+ {FunAcc, State}
end;
{empty, _} ->
- {not_offered, State}
+ {FunAcc, State}
end.
-attempt_delivery(none, _ChPid, Message, State) ->
- case deliver_immediately(Message, false, State) of
- {offered, false, State1} ->
- {true, State1};
- {offered, true, State1} ->
- persist_message(none, qname(State), Message),
- persist_delivery(qname(State), Message, false),
- {true, State1};
- {not_offered, State1} ->
- {false, State1}
- end;
-attempt_delivery(Txn, ChPid, Message, State) ->
- persist_message(Txn, qname(State), Message),
- record_pending_message(Txn, ChPid, Message),
- {true, State}.
-
-deliver_or_enqueue(Txn, ChPid, Message, State) ->
- case attempt_delivery(Txn, ChPid, Message, State) of
+deliver_from_queue_pred({IsEmpty, _AutoAcks}, _State) ->
+ not IsEmpty.
+deliver_from_queue_deliver(AckRequired, {false, AutoAcks},
+ State = #q { variable_queue_state = VQS }) ->
+ {{Msg, IsDelivered, AckTag, Remaining}, VQS1} =
+ rabbit_variable_queue:fetch(VQS),
+ AutoAcks1 = case AckRequired of
+ true -> AutoAcks;
+ false -> [AckTag | AutoAcks]
+ end,
+ {{Msg, IsDelivered, AckTag}, {0 == Remaining, AutoAcks1},
+ State #q { variable_queue_state = VQS1 }}.
+
+run_message_queue(State = #q { variable_queue_state = VQS }) ->
+ Funs = { fun deliver_from_queue_pred/2,
+ fun deliver_from_queue_deliver/3 },
+ IsEmpty = rabbit_variable_queue:is_empty(VQS),
+ {{_IsEmpty1, AutoAcks}, State1} =
+ deliver_msgs_to_consumers(Funs, {IsEmpty, []}, State),
+ VQS1 = rabbit_variable_queue:ack(AutoAcks, State1 #q.variable_queue_state),
+ State1 #q { variable_queue_state = VQS1 }.
+
+attempt_immediate_delivery(none, _ChPid, Msg, State) ->
+ PredFun = fun (IsEmpty, _State) -> not IsEmpty end,
+ DeliverFun =
+ fun (AckRequired, false, State1) ->
+ {AckTag, State2} =
+ case AckRequired of
+ true ->
+ {AckTag1, VQS} =
+ rabbit_variable_queue:publish_delivered(
+ Msg, State1 #q.variable_queue_state),
+ {AckTag1, State1 #q { variable_queue_state = VQS }};
+ false ->
+ {noack, State1}
+ end,
+ {{Msg, false, AckTag}, true, State2}
+ end,
+ deliver_msgs_to_consumers({ PredFun, DeliverFun }, false, State);
+attempt_immediate_delivery(Txn, ChPid, Msg, State) ->
+ VQS = rabbit_variable_queue:tx_publish(Msg, State #q.variable_queue_state),
+ record_pending_message(Txn, ChPid, Msg),
+ {true, State #q { variable_queue_state = VQS }}.
+
+deliver_or_enqueue(Txn, ChPid, Msg, State) ->
+ case attempt_immediate_delivery(Txn, ChPid, Msg, State) of
{true, NewState} ->
{true, NewState};
{false, NewState} ->
- persist_message(Txn, qname(State), Message),
- NewMB = queue:in({Message, false}, NewState#q.message_buffer),
- {false, NewState#q{message_buffer = NewMB}}
+ %% Txn is none and no unblocked channels with consumers
+ {_SeqId, VQS} = rabbit_variable_queue:publish(
+ Msg, State #q.variable_queue_state),
+ {false, NewState #q { variable_queue_state = VQS }}
end.
-deliver_or_enqueue_n(Messages, State = #q{message_buffer = MessageBuffer}) ->
- run_poke_burst(queue:join(MessageBuffer, queue:from_list(Messages)),
- State).
+%% all these messages have already been delivered at least once and
+%% not ack'd, but need to be either redelivered or requeued
+deliver_or_requeue_n([], State) ->
+ State;
+deliver_or_requeue_n(MsgsWithAcks, State) ->
+ Funs = { fun deliver_or_requeue_msgs_pred/2,
+ fun deliver_or_requeue_msgs_deliver/3 },
+ {{_RemainingLengthMinusOne, AutoAcks, OutstandingMsgs}, NewState} =
+ deliver_msgs_to_consumers(
+ Funs, {length(MsgsWithAcks), [], MsgsWithAcks}, State),
+ VQS = rabbit_variable_queue:ack(AutoAcks, NewState #q.variable_queue_state),
+ case OutstandingMsgs of
+ [] -> NewState #q { variable_queue_state = VQS };
+ _ -> VQS1 = rabbit_variable_queue:requeue(OutstandingMsgs, VQS),
+ NewState #q { variable_queue_state = VQS1 }
+ end.
+
+deliver_or_requeue_msgs_pred({Len, _AcksAcc, _MsgsWithAcks}, _State) ->
+ 0 < Len.
+deliver_or_requeue_msgs_deliver(
+ false, {Len, AcksAcc, [{Msg, AckTag} | MsgsWithAcks]}, State) ->
+ {{Msg, true, noack}, {Len - 1, [AckTag | AcksAcc], MsgsWithAcks}, State};
+deliver_or_requeue_msgs_deliver(
+ true, {Len, AcksAcc, [{Msg, AckTag} | MsgsWithAcks]}, State) ->
+ {{Msg, true, AckTag}, {Len - 1, AcksAcc, MsgsWithAcks}, State}.
add_consumer(ChPid, Consumer, Queue) -> queue:in({ChPid, Consumer}, Queue).
@@ -299,7 +425,7 @@ possibly_unblock(State, ChPid, Update) ->
move_consumers(ChPid,
State#q.blocked_consumers,
State#q.active_consumers),
- run_poke_burst(
+ run_message_queue(
State#q{active_consumers = NewActiveConsumers,
blocked_consumers = NewBlockedeConsumers})
end
@@ -316,27 +442,27 @@ handle_ch_down(DownPid, State = #q{exclusive_consumer = Holder}) ->
unacked_messages = UAM} ->
erlang:demonitor(MonitorRef),
erase({ch, ChPid}),
- case Txn of
- none -> ok;
- _ -> ok = rollback_work(Txn, qname(State)),
- erase_tx(Txn)
- end,
- NewState =
- deliver_or_enqueue_n(
- [{Message, true} ||
- {_Messsage_id, Message} <- dict:to_list(UAM)],
- State#q{
- exclusive_consumer = case Holder of
- {ChPid, _} -> none;
- Other -> Other
- end,
- active_consumers = remove_consumers(
- ChPid, State#q.active_consumers),
- blocked_consumers = remove_consumers(
- ChPid, State#q.blocked_consumers)}),
- case should_auto_delete(NewState) of
- false -> {ok, NewState};
- true -> {stop, NewState}
+ State1 = State#q{
+ exclusive_consumer = case Holder of
+ {ChPid, _} -> none;
+ Other -> Other
+ end,
+ active_consumers = remove_consumers(
+ ChPid, State#q.active_consumers),
+ blocked_consumers = remove_consumers(
+ ChPid, State#q.blocked_consumers)},
+ case should_auto_delete(State1) of
+ true ->
+ {stop, State1};
+ false ->
+ State2 = case Txn of
+ none -> State1;
+ _ -> rollback_transaction(Txn, State1)
+ end,
+ {ok,
+ deliver_or_requeue_n(
+ [MsgWithAck ||
+ {_MsgId, MsgWithAck} <- dict:to_list(UAM)], State2)}
end
end.
@@ -359,26 +485,6 @@ check_exclusive_access(none, true, State) ->
false -> in_use
end.
-run_poke_burst(State = #q{message_buffer = MessageBuffer}) ->
- run_poke_burst(MessageBuffer, State).
-
-run_poke_burst(MessageBuffer, State) ->
- case queue:out(MessageBuffer) of
- {{value, {Message, Delivered}}, BufferTail} ->
- case deliver_immediately(Message, Delivered, State) of
- {offered, true, NewState} ->
- persist_delivery(qname(State), Message, Delivered),
- run_poke_burst(BufferTail, NewState);
- {offered, false, NewState} ->
- persist_auto_ack(qname(State), Message),
- run_poke_burst(BufferTail, NewState);
- {not_offered, NewState} ->
- NewState#q{message_buffer = MessageBuffer}
- end;
- {empty, _} ->
- State#q{message_buffer = MessageBuffer}
- end.
-
is_unused(State) -> queue:is_empty(State#q.active_consumers) andalso
queue:is_empty(State#q.blocked_consumers).
@@ -387,66 +493,9 @@ maybe_send_reply(ChPid, Msg) -> ok = rabbit_channel:send_command(ChPid, Msg).
qname(#q{q = #amqqueue{name = QName}}) -> QName.
-persist_message(_Txn, _QName, #basic_message{persistent_key = none}) ->
- ok;
-persist_message(Txn, QName, Message) ->
- M = Message#basic_message{
- %% don't persist any recoverable decoded properties, rebuild from properties_bin on restore
- content = rabbit_binary_parser:clear_decoded_content(
- Message#basic_message.content)},
- persist_work(Txn, QName,
- [{publish, M, {QName, M#basic_message.persistent_key}}]).
-
-persist_delivery(_QName, _Message,
- true) ->
- ok;
-persist_delivery(_QName, #basic_message{persistent_key = none},
- _Delivered) ->
- ok;
-persist_delivery(QName, #basic_message{persistent_key = PKey},
- _Delivered) ->
- persist_work(none, QName, [{deliver, {QName, PKey}}]).
-
-persist_acks(Txn, QName, Messages) ->
- persist_work(Txn, QName,
- [{ack, {QName, PKey}} ||
- #basic_message{persistent_key = PKey} <- Messages,
- PKey =/= none]).
-
-persist_auto_ack(_QName, #basic_message{persistent_key = none}) ->
- ok;
-persist_auto_ack(QName, #basic_message{persistent_key = PKey}) ->
- %% auto-acks are always non-transactional
- rabbit_persister:dirty_work([{ack, {QName, PKey}}]).
-
-persist_work(_Txn,_QName, []) ->
- ok;
-persist_work(none, _QName, WorkList) ->
- rabbit_persister:dirty_work(WorkList);
-persist_work(Txn, QName, WorkList) ->
- mark_tx_persistent(Txn),
- rabbit_persister:extend_transaction({Txn, QName}, WorkList).
-
-commit_work(Txn, QName) ->
- do_if_persistent(fun rabbit_persister:commit_transaction/1,
- Txn, QName).
-
-rollback_work(Txn, QName) ->
- do_if_persistent(fun rabbit_persister:rollback_transaction/1,
- Txn, QName).
-
-%% optimisation: don't do unnecessary work
-%% it would be nice if this was handled by the persister
-do_if_persistent(F, Txn, QName) ->
- case is_tx_persistent(Txn) of
- false -> ok;
- true -> ok = F({Txn, QName})
- end.
-
lookup_tx(Txn) ->
case get({txn, Txn}) of
undefined -> #tx{ch_pid = none,
- is_persistent = false,
pending_messages = [],
pending_acks = []};
V -> V
@@ -461,22 +510,10 @@ erase_tx(Txn) ->
all_tx_record() ->
[T || {{txn, _}, T} <- get()].
-all_tx() ->
- [Txn || {{txn, Txn}, _} <- get()].
-
-mark_tx_persistent(Txn) ->
- Tx = lookup_tx(Txn),
- store_tx(Txn, Tx#tx{is_persistent = true}).
-
-is_tx_persistent(Txn) ->
- #tx{is_persistent = Res} = lookup_tx(Txn),
- Res.
-
record_pending_message(Txn, ChPid, Message) ->
Tx = #tx{pending_messages = Pending} = lookup_tx(Txn),
record_current_channel_tx(ChPid, Txn),
- store_tx(Txn, Tx#tx{pending_messages = [{Message, false} | Pending],
- ch_pid = ChPid}).
+ store_tx(Txn, Tx #tx { pending_messages = [Message | Pending] }).
record_pending_acks(Txn, ChPid, MsgIds) ->
Tx = #tx{pending_acks = Pending} = lookup_tx(Txn),
@@ -484,38 +521,42 @@ record_pending_acks(Txn, ChPid, MsgIds) ->
store_tx(Txn, Tx#tx{pending_acks = [MsgIds | Pending],
ch_pid = ChPid}).
-process_pending(Txn, State) ->
- #tx{ch_pid = ChPid,
- pending_messages = PendingMessages,
- pending_acks = PendingAcks} = lookup_tx(Txn),
- case lookup_ch(ChPid) of
- not_found -> ok;
- C = #cr{unacked_messages = UAM} ->
- {_Acked, Remaining} =
- collect_messages(lists:append(PendingAcks), UAM),
- store_ch_record(C#cr{unacked_messages = Remaining})
- end,
- deliver_or_enqueue_n(lists:reverse(PendingMessages), State).
+commit_transaction(Txn, From, State) ->
+ #tx { ch_pid = ChPid,
+ pending_messages = PendingMessages,
+ pending_acks = PendingAcks
+ } = lookup_tx(Txn),
+ PendingMessagesOrdered = lists:reverse(PendingMessages),
+ PendingAcksOrdered = lists:append(PendingAcks),
+ Acks =
+ case lookup_ch(ChPid) of
+ not_found -> [];
+ C = #cr { unacked_messages = UAM } ->
+ {MsgsWithAcks, Remaining} =
+ collect_messages(PendingAcksOrdered, UAM),
+ store_ch_record(C#cr{unacked_messages = Remaining}),
+ [AckTag || {_Msg, AckTag} <- MsgsWithAcks]
+ end,
+ {RunQueue, VQS} =
+ rabbit_variable_queue:tx_commit(
+ PendingMessagesOrdered, Acks, From, State #q.variable_queue_state),
+ {RunQueue, State #q { variable_queue_state = VQS }}.
+
+rollback_transaction(Txn, State) ->
+ #tx { pending_messages = PendingMessages
+ } = lookup_tx(Txn),
+ VQS = rabbit_variable_queue:tx_rollback(PendingMessages,
+ State #q.variable_queue_state),
+ erase_tx(Txn),
+ State #q { variable_queue_state = VQS }.
+%% {A, B} = collect_messages(C, D) %% A = C `intersect` D; B = D \\ C
+%% err, A = C `intersect` D , via projection through the dict that is C
collect_messages(MsgIds, UAM) ->
lists:mapfoldl(
fun (MsgId, D) -> {dict:fetch(MsgId, D), dict:erase(MsgId, D)} end,
UAM, MsgIds).
-purge_message_buffer(QName, MessageBuffer) ->
- Messages =
- [[Message || {Message, _Delivered} <-
- queue:to_list(MessageBuffer)] |
- lists:map(
- fun (#cr{unacked_messages = UAM}) ->
- [Message || {_MessageId, Message} <- dict:to_list(UAM)]
- end,
- all_ch_record())],
- %% the simplest, though certainly not the most obvious or
- %% efficient, way to purge messages from the persister is to
- %% artifically ack them.
- persist_acks(none, QName, lists:append(Messages)).
-
infos(Items, State) -> [{Item, i(Item, State)} || Item <- Items].
i(name, #q{q = #amqqueue{name = Name}}) -> Name;
@@ -524,8 +565,8 @@ i(auto_delete, #q{q = #amqqueue{auto_delete = AutoDelete}}) -> AutoDelete;
i(arguments, #q{q = #amqqueue{arguments = Arguments}}) -> Arguments;
i(pid, _) ->
self();
-i(messages_ready, #q{message_buffer = MessageBuffer}) ->
- queue:len(MessageBuffer);
+i(messages_ready, #q { variable_queue_state = VQS }) ->
+ rabbit_variable_queue:len(VQS);
i(messages_unacknowledged, _) ->
lists:sum([dict:size(UAM) ||
#cr{unacked_messages = UAM} <- all_ch_record()]);
@@ -574,7 +615,8 @@ handle_call({deliver_immediately, Txn, Message, ChPid}, _From, State) ->
%% just all ready-to-consume queues get the message, with unready
%% queues discarding the message?
%%
- {Delivered, NewState} = attempt_delivery(Txn, ChPid, Message, State),
+ {Delivered, NewState} =
+ attempt_immediate_delivery(Txn, ChPid, Message, State),
reply(Delivered, NewState);
handle_call({deliver, Txn, Message, ChPid}, _From, State) ->
@@ -583,12 +625,12 @@ handle_call({deliver, Txn, Message, ChPid}, _From, State) ->
reply(Delivered, NewState);
handle_call({commit, Txn}, From, State) ->
- ok = commit_work(Txn, qname(State)),
- %% optimisation: we reply straight away so the sender can continue
- gen_server2:reply(From, ok),
- NewState = process_pending(Txn, State),
+ {RunQueue, NewState} = commit_transaction(Txn, From, State),
erase_tx(Txn),
- noreply(NewState);
+ noreply(case RunQueue of
+ true -> run_message_queue(NewState);
+ false -> NewState
+ end);
handle_call({notify_down, ChPid}, _From, State) ->
%% we want to do this synchronously, so that auto_deleted queues
@@ -604,25 +646,25 @@ handle_call({notify_down, ChPid}, _From, State) ->
handle_call({basic_get, ChPid, NoAck}, _From,
State = #q{q = #amqqueue{name = QName},
next_msg_id = NextId,
- message_buffer = MessageBuffer}) ->
- case queue:out(MessageBuffer) of
- {{value, {Message, Delivered}}, BufferTail} ->
+ variable_queue_state = VQS
+ }) ->
+ case rabbit_variable_queue:fetch(VQS) of
+ {empty, VQS1} -> reply(empty, State #q { variable_queue_state = VQS1 });
+ {{Msg, IsDelivered, AckTag, Remaining}, VQS1} ->
AckRequired = not(NoAck),
- case AckRequired of
- true ->
- persist_delivery(QName, Message, Delivered),
- C = #cr{unacked_messages = UAM} = ch_record(ChPid),
- NewUAM = dict:store(NextId, Message, UAM),
- store_ch_record(C#cr{unacked_messages = NewUAM});
- false ->
- persist_auto_ack(QName, Message)
- end,
- Msg = {QName, self(), NextId, Delivered, Message},
- reply({ok, queue:len(BufferTail), Msg},
- State#q{message_buffer = BufferTail,
- next_msg_id = NextId + 1});
- {empty, _} ->
- reply(empty, State)
+ VQS2 =
+ case AckRequired of
+ true ->
+ C = #cr{unacked_messages = UAM} = ch_record(ChPid),
+ NewUAM = dict:store(NextId, {Msg, AckTag}, UAM),
+ store_ch_record(C#cr{unacked_messages = NewUAM}),
+ VQS1;
+ false ->
+ rabbit_variable_queue:ack([AckTag], VQS1)
+ end,
+ Message = {QName, self(), NextId, IsDelivered, Msg},
+ reply({ok, Remaining, Message},
+ State #q { next_msg_id = NextId + 1, variable_queue_state = VQS2 })
end;
handle_call({basic_consume, NoAck, ReaderPid, ChPid, LimiterPid,
@@ -643,15 +685,14 @@ handle_call({basic_consume, NoAck, ReaderPid, ChPid, LimiterPid,
ack_required = not(NoAck)},
store_ch_record(C#cr{consumer_count = ConsumerCount +1,
limiter_pid = LimiterPid}),
- if ConsumerCount == 0 ->
- ok = rabbit_limiter:register(LimiterPid, self());
- true ->
- ok
+ case ConsumerCount of
+ 0 -> ok = rabbit_limiter:register(LimiterPid, self());
+ _ -> ok
end,
- ExclusiveConsumer =
- if ExclusiveConsume -> {ChPid, ConsumerTag};
- true -> ExistingHolder
- end,
+ ExclusiveConsumer = case ExclusiveConsume of
+ true -> {ChPid, ConsumerTag};
+ false -> ExistingHolder
+ end,
State1 = State#q{has_had_consumers = true,
exclusive_consumer = ExclusiveConsumer},
ok = maybe_send_reply(ChPid, OkMsg),
@@ -662,7 +703,7 @@ handle_call({basic_consume, NoAck, ReaderPid, ChPid, LimiterPid,
add_consumer(
ChPid, Consumer,
State1#q.blocked_consumers)};
- false -> run_poke_burst(
+ false -> run_message_queue(
State1#q{
active_consumers =
add_consumer(
@@ -681,11 +722,10 @@ handle_call({basic_cancel, ChPid, ConsumerTag, OkMsg}, _From,
reply(ok, State);
C = #cr{consumer_count = ConsumerCount, limiter_pid = LimiterPid} ->
store_ch_record(C#cr{consumer_count = ConsumerCount - 1}),
- if ConsumerCount == 1 ->
- ok = rabbit_limiter:unregister(LimiterPid, self());
- true ->
- ok
- end,
+ ok = case ConsumerCount of
+ 1 -> rabbit_limiter:unregister(LimiterPid, self());
+ _ -> ok
+ end,
ok = maybe_send_reply(ChPid, OkMsg),
NewState =
State#q{exclusive_consumer = cancel_holder(ChPid,
@@ -704,14 +744,15 @@ handle_call({basic_cancel, ChPid, ConsumerTag, OkMsg}, _From,
end;
handle_call(stat, _From, State = #q{q = #amqqueue{name = Name},
- message_buffer = MessageBuffer,
+ variable_queue_state = VQS,
active_consumers = ActiveConsumers}) ->
- reply({ok, Name, queue:len(MessageBuffer), queue:len(ActiveConsumers)},
- State);
+ Length = rabbit_variable_queue:len(VQS),
+ reply({ok, Name, Length, queue:len(ActiveConsumers)}, State);
handle_call({delete, IfUnused, IfEmpty}, _From,
- State = #q{message_buffer = MessageBuffer}) ->
- IsEmpty = queue:is_empty(MessageBuffer),
+ State = #q { variable_queue_state = VQS }) ->
+ Length = rabbit_variable_queue:len(VQS),
+ IsEmpty = Length == 0,
IsUnused = is_unused(State),
if
IfEmpty and not(IsEmpty) ->
@@ -719,16 +760,15 @@ handle_call({delete, IfUnused, IfEmpty}, _From,
IfUnused and not(IsUnused) ->
reply({error, in_use}, State);
true ->
- {stop, normal, {ok, queue:len(MessageBuffer)}, State}
+ {stop, normal, {ok, Length}, State}
end;
-handle_call(purge, _From, State = #q{message_buffer = MessageBuffer}) ->
- ok = purge_message_buffer(qname(State), MessageBuffer),
- reply({ok, queue:len(MessageBuffer)},
- State#q{message_buffer = queue:new()});
+handle_call(purge, _From, State) ->
+ {Count, VQS} = rabbit_variable_queue:purge(State #q.variable_queue_state),
+ reply({ok, Count}, State #q { variable_queue_state = VQS });
-handle_call({claim_queue, ReaderPid}, _From, State = #q{owner = Owner,
- exclusive_consumer = Holder}) ->
+handle_call({claim_queue, ReaderPid}, _From,
+ State = #q{owner = Owner, exclusive_consumer = Holder}) ->
case Owner of
none ->
case check_exclusive_access(Holder, true, State) of
@@ -741,7 +781,9 @@ handle_call({claim_queue, ReaderPid}, _From, State = #q{owner = Owner,
%% pid...
reply(locked, State);
ok ->
- reply(ok, State#q{owner = {ReaderPid, erlang:monitor(process, ReaderPid)}})
+ reply(ok,
+ State#q{ owner = {ReaderPid, erlang:monitor(
+ process, ReaderPid)} })
end;
{ReaderPid, _MonitorRef} ->
reply(ok, State);
@@ -759,24 +801,22 @@ handle_cast({ack, Txn, MsgIds, ChPid}, State) ->
not_found ->
noreply(State);
C = #cr{unacked_messages = UAM} ->
- {Acked, Remaining} = collect_messages(MsgIds, UAM),
- persist_acks(Txn, qname(State), Acked),
case Txn of
none ->
- store_ch_record(C#cr{unacked_messages = Remaining});
+ {MsgWithAcks, Remaining} = collect_messages(MsgIds, UAM),
+ VQS = rabbit_variable_queue:ack(
+ [AckTag || {_Msg, AckTag} <- MsgWithAcks],
+ State #q.variable_queue_state),
+ store_ch_record(C#cr{unacked_messages = Remaining}),
+ noreply(State #q { variable_queue_state = VQS });
_ ->
- record_pending_acks(Txn, ChPid, MsgIds)
- end,
- noreply(State)
+ record_pending_acks(Txn, ChPid, MsgIds),
+ noreply(State)
+ end
end;
handle_cast({rollback, Txn}, State) ->
- ok = rollback_work(Txn, qname(State)),
- erase_tx(Txn),
- noreply(State);
-
-handle_cast({redeliver, Messages}, State) ->
- noreply(deliver_or_enqueue_n(Messages, State));
+ noreply(rollback_transaction(Txn, State));
handle_cast({requeue, MsgIds, ChPid}, State) ->
case lookup_ch(ChPid) of
@@ -785,10 +825,9 @@ handle_cast({requeue, MsgIds, ChPid}, State) ->
[ChPid]),
noreply(State);
C = #cr{unacked_messages = UAM} ->
- {Messages, NewUAM} = collect_messages(MsgIds, UAM),
+ {MsgWithAcks, NewUAM} = collect_messages(MsgIds, UAM),
store_ch_record(C#cr{unacked_messages = NewUAM}),
- noreply(deliver_or_enqueue_n(
- [{Message, true} || Message <- Messages], State))
+ noreply(deliver_or_requeue_n(MsgWithAcks, State))
end;
handle_cast({unblock, ChPid}, State) ->
@@ -803,6 +842,19 @@ handle_cast({notify_sent, ChPid}, State) ->
C#cr{unsent_message_count = Count - 1}
end));
+handle_cast({tx_commit_msg_store_callback, Pubs, AckTags, From},
+ State = #q{variable_queue_state = VQS}) ->
+ noreply(
+ State#q{variable_queue_state =
+ rabbit_variable_queue:tx_commit_from_msg_store(
+ Pubs, AckTags, From, VQS)});
+
+handle_cast(tx_commit_vq_callback, State = #q{variable_queue_state = VQS}) ->
+ noreply(
+ run_message_queue(
+ State#q{variable_queue_state =
+ rabbit_variable_queue:tx_commit_from_vq(VQS)}));
+
handle_cast({limit, ChPid, LimiterPid}, State) ->
noreply(
possibly_unblock(
@@ -819,49 +871,10 @@ handle_cast({limit, ChPid, LimiterPid}, State) ->
C#cr{limiter_pid = LimiterPid, is_limit_active = NewLimited}
end));
-handle_cast(send_memory_monitor_update, State) ->
- DrainRatio1 = update_ratio(State#q.drain_ratio, State#q.next_msg_id),
- MsgSec = DrainRatio1#ratio.ratio * 1000000, % msg/sec
- QueueDuration =
- case MsgSec == 0 of
- true -> infinity;
- false -> queue:len(State#q.message_buffer) / MsgSec % seconds
- end,
- DesiredQueueDuration = rabbit_memory_monitor:report_queue_duration(
- self(), QueueDuration),
- ?LOGDEBUG("TIMER ~p Queue length is ~8p, should be ~p~n",
- [(State#q.q)#amqqueue.name, queue:len(State#q.message_buffer),
- case DesiredQueueDuration of
- infinity -> infinity;
- _ -> MsgSec * DesiredQueueDuration
- end]),
- noreply(State#q{drain_ratio = DrainRatio1});
-
-handle_cast({set_queue_duration, DesiredQueueDuration}, State) ->
- DrainRatio = State#q.drain_ratio,
- DesiredBufLength =
- case DesiredQueueDuration of
- infinity -> infinity;
- _ -> DesiredQueueDuration * DrainRatio#ratio.ratio * 1000000
- end,
- ?LOGDEBUG("MAGIC ~p Queue length is ~8p, should be ~p~n",
- [(State#q.q)#amqqueue.name, queue:len(State#q.message_buffer),
- DesiredBufLength]),
- noreply(State).
-
-%% Based on kernel load average, as descibed:
-%% http://www.teamquest.com/resources/gunther/display/5/
-calc_load(Load, Exp, N) ->
- Load*Exp + N*(1.0-Exp).
-
-update_ratio(_RatioRec = #ratio{ratio=Ratio, t0 = T0, next_msg_id = MsgCount0}, MsgCount1) ->
- T1 = now(),
- Td = timer:now_diff(T1, T0),
- MsgCount = MsgCount1 - MsgCount0,
- MsgUSec = MsgCount / Td, % msg/usec
- %% Td is in usec. We're interested in "load average" from last 30 seconds.
- Ratio1 = calc_load(Ratio, 1.0/ (math:exp(Td/(30*1000000))), MsgUSec),
- #ratio{ratio = Ratio1, t0=T1, next_msg_id = MsgCount1}.
+handle_cast(remeasure_egress_rate, State = #q{variable_queue_state = VQS}) ->
+ noreply(State#q{egress_rate_timer_ref = just_measured,
+ variable_queue_state =
+ rabbit_variable_queue:remeasure_egress_rate(VQS)}).
handle_info({'DOWN', MonitorRef, process, DownPid, _Reason},
State = #q{owner = {DownPid, MonitorRef}}) ->
@@ -882,6 +895,24 @@ handle_info({'DOWN', _MonitorRef, process, DownPid, _Reason}, State) ->
{stop, NewState} -> {stop, normal, NewState}
end;
+handle_info(timeout, State = #q{variable_queue_state = VQS,
+ sync_timer_ref = undefined}) ->
+ %% if sync_timer_ref is undefined then we must have set the
+ %% timeout to zero because we thought we could flush the journal
+ noreply(State#q{variable_queue_state =
+ rabbit_variable_queue:flush_journal(VQS)});
+
+handle_info(timeout, State = #q{variable_queue_state = VQS}) ->
+ noreply(
+ run_message_queue(
+ State#q{variable_queue_state =
+ rabbit_variable_queue:tx_commit_from_vq(VQS)}));
+
handle_info(Info, State) ->
?LOGDEBUG("Info in queue: ~p~n", [Info]),
{stop, {unhandled_info, Info}, State}.
+
+handle_pre_hibernate(State = #q{ variable_queue_state = VQS }) ->
+ VQS1 = rabbit_variable_queue:maybe_start_prefetcher(VQS),
+ {hibernate, stop_egress_rate_timer(
+ State#q{ variable_queue_state = VQS1 })}.
diff --git a/src/rabbit_amqqueue_sup.erl b/src/rabbit_amqqueue_sup.erl
index 46d23a4075..f06e4c5380 100644
--- a/src/rabbit_amqqueue_sup.erl
+++ b/src/rabbit_amqqueue_sup.erl
@@ -43,6 +43,4 @@ start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
init([]) ->
- {ok, {{simple_one_for_one, 10, 10},
- [{rabbit_amqqueue, {rabbit_amqqueue_process, start_link, []},
- temporary, brutal_kill, worker, [rabbit_amqqueue_process]}]}}.
+ {ok, {{one_for_one, 10, 10}, []}}.
diff --git a/src/rabbit_basic.erl b/src/rabbit_basic.erl
index bec2cd0845..14c655a6d5 100644
--- a/src/rabbit_basic.erl
+++ b/src/rabbit_basic.erl
@@ -33,8 +33,8 @@
-include("rabbit.hrl").
-include("rabbit_framing.hrl").
--export([publish/1, message/4, properties/1, delivery/4]).
--export([publish/4, publish/7]).
+-export([publish/1, message/4, message/5, message/6, delivery/4]).
+-export([properties/1, publish/4, publish/7]).
-export([build_content/2, from_content/1]).
%%----------------------------------------------------------------------------
@@ -49,6 +49,10 @@
delivery()).
-spec(message/4 :: (exchange_name(), routing_key(), properties_input(),
binary()) -> message()).
+-spec(message/5 :: (exchange_name(), routing_key(), properties_input(),
+ binary(), guid()) -> message()).
+-spec(message/6 :: (exchange_name(), routing_key(), properties_input(),
+ binary(), guid(), boolean()) -> message()).
-spec(properties/1 :: (properties_input()) -> amqp_properties()).
-spec(publish/4 :: (exchange_name(), routing_key(), properties_input(),
binary()) -> publish_result()).
@@ -92,11 +96,18 @@ from_content(Content) ->
{Props, list_to_binary(lists:reverse(FragmentsRev))}.
message(ExchangeName, RoutingKeyBin, RawProperties, BodyBin) ->
+ message(ExchangeName, RoutingKeyBin, RawProperties, BodyBin, rabbit_guid:guid()).
+
+message(ExchangeName, RoutingKeyBin, RawProperties, BodyBin, MsgId) ->
+ message(ExchangeName, RoutingKeyBin, RawProperties, BodyBin, MsgId, false).
+
+message(ExchangeName, RoutingKeyBin, RawProperties, BodyBin, MsgId, IsPersistent) ->
Properties = properties(RawProperties),
#basic_message{exchange_name = ExchangeName,
routing_key = RoutingKeyBin,
content = build_content(Properties, BodyBin),
- persistent_key = none}.
+ guid = MsgId,
+ is_persistent = IsPersistent}.
properties(P = #'P_basic'{}) ->
P;
diff --git a/src/rabbit_binary_generator.erl b/src/rabbit_binary_generator.erl
index 6cfa9e6d17..0b68f33f41 100644
--- a/src/rabbit_binary_generator.erl
+++ b/src/rabbit_binary_generator.erl
@@ -46,6 +46,7 @@
build_heartbeat_frame/0]).
-export([generate_table/1, encode_properties/2]).
-export([check_empty_content_body_frame_size/0]).
+-export([ensure_content_encoded/1, clear_encoded_content/1]).
-import(lists).
@@ -63,6 +64,8 @@
-spec(generate_table/1 :: (amqp_table()) -> binary()).
-spec(encode_properties/2 :: ([amqp_property_type()], [any()]) -> binary()).
-spec(check_empty_content_body_frame_size/0 :: () -> 'ok').
+-spec(ensure_content_encoded/1 :: (content()) -> encoded_content()).
+-spec(clear_encoded_content/1 :: (content()) -> unencoded_content()).
-endif.
@@ -275,3 +278,19 @@ check_empty_content_body_frame_size() ->
exit({incorrect_empty_content_body_frame_size,
ComputedSize, ?EMPTY_CONTENT_BODY_FRAME_SIZE})
end.
+
+ensure_content_encoded(Content = #content{properties_bin = PropsBin})
+ when PropsBin =/= 'none' ->
+ Content;
+ensure_content_encoded(Content = #content{properties = Props}) ->
+ Content #content{properties_bin = rabbit_framing:encode_properties(Props)}.
+
+clear_encoded_content(Content = #content{properties_bin = none}) ->
+ Content;
+clear_encoded_content(Content = #content{properties = none}) ->
+ %% Only clear when we can rebuild the properties later in
+ %% accordance to the content record definition comment - maximum
+ %% one of properties and properties_bin can be 'none'
+ Content;
+clear_encoded_content(Content = #content{}) ->
+ Content#content{properties_bin = none}.
diff --git a/src/rabbit_channel.erl b/src/rabbit_channel.erl
index c20cb16ca1..6afd0bc9a7 100644
--- a/src/rabbit_channel.erl
+++ b/src/rabbit_channel.erl
@@ -54,6 +54,9 @@
-ifdef(use_specs).
+-type(msg_id() :: non_neg_integer()).
+-type(msg() :: {queue_name(), pid(), msg_id(), boolean(), message()}).
+
-spec(start_link/5 ::
(channel_number(), pid(), pid(), username(), vhost()) -> pid()).
-spec(do/2 :: (pid(), amqp_method()) -> 'ok').
@@ -311,14 +314,11 @@ handle_method(#'basic.publish'{exchange = ExchangeNameBin,
%% We decode the content's properties here because we're almost
%% certain to want to look at delivery-mode and priority.
DecodedContent = rabbit_binary_parser:ensure_content_decoded(Content),
- PersistentKey = case is_message_persistent(DecodedContent) of
- true -> rabbit_guid:guid();
- false -> none
- end,
Message = #basic_message{exchange_name = ExchangeName,
routing_key = RoutingKey,
content = DecodedContent,
- persistent_key = PersistentKey},
+ guid = rabbit_guid:guid(),
+ is_persistent = is_message_persistent(DecodedContent)},
{RoutingRes, DeliveredQPids} =
rabbit_exchange:publish(
Exchange,
diff --git a/src/rabbit_control.erl b/src/rabbit_control.erl
index 1957972990..5e6229b138 100644
--- a/src/rabbit_control.erl
+++ b/src/rabbit_control.erl
@@ -174,8 +174,8 @@ virtual host parameter for which to display results. The default value is \"/\".
<QueueInfoItem> must be a member of the list [name, durable, auto_delete,
arguments, node, messages_ready, messages_unacknowledged, messages_uncommitted,
-messages, acks_uncommitted, consumers, transactions, memory]. The default is
- to display name and (number of) messages.
+messages, acks_uncommitted, consumers, transactions, memory, storage_mode]. The
+default is to display name and (number of) messages.
<ExchangeInfoItem> must be a member of the list [name, type, durable,
auto_delete, arguments]. The default is to display name and type.
@@ -187,7 +187,6 @@ exchange name, queue name, routing key and arguments, in that order.
peer_address, peer_port, state, channels, user, vhost, timeout, frame_max,
recv_oct, recv_cnt, send_oct, send_cnt, send_pend]. The default is to display
user, peer_address, peer_port and state.
-
"),
halt(1).
diff --git a/src/rabbit_guid.erl b/src/rabbit_guid.erl
index ea61a679e8..dee21c3421 100644
--- a/src/rabbit_guid.erl
+++ b/src/rabbit_guid.erl
@@ -67,7 +67,7 @@ update_disk_serial() ->
Filename = filename:join(rabbit_mnesia:dir(), ?SERIAL_FILENAME),
Serial = case rabbit_misc:read_term_file(Filename) of
{ok, [Num]} -> Num;
- {error, enoent} -> rabbit_persister:serial();
+ {error, enoent} -> 0;
{error, Reason} ->
throw({error, {cannot_read_serial_file, Filename, Reason}})
end,
@@ -99,13 +99,12 @@ guid() ->
{S, I} -> {S, I+1}
end,
put(guid, G),
- G.
+ erlang:md5(term_to_binary(G)).
%% generate a readable string representation of a guid. Note that any
%% monotonicity of the guid is not preserved in the encoding.
string_guid(Prefix) ->
- Prefix ++ "-" ++ base64:encode_to_string(
- erlang:md5(term_to_binary(guid()))).
+ Prefix ++ "-" ++ base64:encode_to_string(guid()).
binstring_guid(Prefix) ->
list_to_binary(string_guid(Prefix)).
diff --git a/src/rabbit_memory_manager.erl b/src/rabbit_memory_manager.erl
new file mode 100644
index 0000000000..a73f03e27d
--- /dev/null
+++ b/src/rabbit_memory_manager.erl
@@ -0,0 +1,404 @@
+%% The contents of this file are subject to the Mozilla Public License
+%% Version 1.1 (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.mozilla.org/MPL/
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+%% License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% The Original Code is RabbitMQ.
+%%
+%% The Initial Developers of the Original Code are LShift Ltd,
+%% Cohesive Financial Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created before 22-Nov-2008 00:00:00 GMT by LShift Ltd,
+%% Cohesive Financial Technologies LLC, or Rabbit Technologies Ltd
+%% are Copyright (C) 2007-2008 LShift Ltd, Cohesive Financial
+%% Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created by LShift Ltd are Copyright (C) 2007-2009 LShift
+%% Ltd. Portions created by Cohesive Financial Technologies LLC are
+%% Copyright (C) 2007-2009 Cohesive Financial Technologies
+%% LLC. Portions created by Rabbit Technologies Ltd are Copyright
+%% (C) 2007-2009 Rabbit Technologies Ltd.
+%%
+%% All Rights Reserved.
+%%
+%% Contributor(s): ______________________________________.
+%%
+
+-module(rabbit_memory_manager).
+
+-behaviour(gen_server2).
+
+-export([start_link/0]).
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-export([register/5, report_memory/3, info/0, conserve_memory/2]).
+
+-define(TOTAL_TOKENS, 10000000).
+-define(THRESHOLD_MULTIPLIER, 0.05).
+-define(THRESHOLD_OFFSET, ?TOTAL_TOKENS * ?THRESHOLD_MULTIPLIER).
+
+-define(SERVER, ?MODULE).
+
+%%----------------------------------------------------------------------------
+
+-ifdef(use_specs).
+
+-spec(start_link/0 :: () ->
+ ({'ok', pid()} | 'ignore' | {'error', any()})).
+-spec(register/5 :: (pid(), boolean(), atom(), atom(), list()) -> 'ok').
+-spec(report_memory/3 :: (pid(), non_neg_integer(), boolean()) -> 'ok').
+-spec(info/0 :: () -> [{atom(), any()}]).
+-spec(conserve_memory/2 :: (pid(), boolean()) -> 'ok').
+
+-endif.
+
+%%----------------------------------------------------------------------------
+
+-record(state, { available_tokens,
+ processes,
+ callbacks,
+ tokens_per_byte,
+ hibernate,
+ unoppressable,
+ alarmed
+ }).
+
+%% Token-credit based memory management
+
+%% Start off by working out the amount of memory available in the
+%% system (RAM). Then, work out how many tokens each byte corresponds
+%% to. This is the tokens_per_byte field. When a process registers, it
+%% must provide an M-F-A triple to a function that needs one further
+%% argument, which is the new mode. This will either be 'liberated' or
+%% 'oppressed'.
+%%
+%% Processes then report their own memory usage, in bytes, and the
+%% manager takes care of the rest.
+%%
+%% There are a finite number of tokens in the system. These are
+%% allocated to processes as the processes report their memory
+%% usage. We keep track of processes which have hibernated. When a
+%% process reports memory use which can't be satisfied by the
+%% available tokens, we try and oppress processes first from the
+%% hibernated group. The hibernated group is a simple queue, and so is
+%% implicitly sorted by the order in which processes were added to the
+%% queue. This means that when removing from the queue, we evict the
+%% sleepiest (and most passive) pid first.
+%%
+%% If the reported memory use still can't be satisfied after
+%% oppressing everyone from those two groups (and note that we check
+%% first whether or not oppressing them would make available enough
+%% tokens to satisfy the reported use rather than just oppressing all
+%% those processes and then going "whoops, didn't help after all"),
+%% then we oppress the reporting process. When a process registers, it
+%% can declare itself "unoppressable". If a process is unoppressable
+%% then it will not be oppressed as a result of other processes
+%% needing more tokens. However, if it itself needs additional tokens
+%% which aren't available then it is still oppressed as before. This
+%% feature is only used by the disk_queue, because if the disk queue
+%% is not being used, and hibernates, and then memory pressure gets
+%% tight, the disk_queue would typically be one of the first processes
+%% to be oppressed (sent to disk_only mode), which cripples
+%% performance. Thus by setting it unoppressable, it is only possible
+%% for the disk_queue to be oppressed when it is active and attempting
+%% to increase its memory allocation.
+%%
+%% If a process has been oppressed, it continues making memory
+%% reports, as if it was liberated. As soon as a reported amount of
+%% memory can be satisfied (and this can include oppressing other
+%% processes in the way described above), *and* the number of
+%% available tokens has changed by ?THRESHOLD_MULTIPLIER since the
+%% processes was oppressed, it will be liberated. This later condition
+%% prevents processes from continually oppressing each other if they
+%% themselves can be liberated by oppressing other processes.
+%%
+%% Note that the hibernate group can get very out of date. This is
+%% fine, and somewhat unavoidable given the absence of useful APIs for
+%% queues. Thus we allow them to get out of date (processes will be
+%% left in there when they change groups, duplicates can appear, dead
+%% processes are not pruned etc etc etc), and when we go through the
+%% groups, summing up their allocated tokens, we tidy up at that
+%% point.
+%%
+%% A liberated process, which is reporting a smaller amount of RAM
+%% than its last report will remain liberated. A liberated process
+%% that is busy but consuming an unchanging amount of RAM will never
+%% be oppressed.
+
+%% Specific notes as applied to queues and the disk_queue:
+%%
+%% The disk_queue is managed in the same way as queues. This means
+%% that a queue that has gone back to mixed mode after being in disk
+%% mode now has its messages counted twice as they are counted both in
+%% the report made by the queue (even though they may not yet be in
+%% RAM (though see the prefetcher)) and also by the disk_queue. Thus
+%% the amount of available RAM must be higher when going disk -> mixed
+%% than when going mixed -> disk. This is fairly sensible as it
+%% reduces the risk of any oscillations occurring.
+%%
+%% The queue process deliberately reports 4 times its estimated RAM
+%% usage, and the disk_queue 2.5 times. In practise, this seems to
+%% work well. Note that we are deliberately running out of tokes a
+%% little early because of the fact that the mixed -> disk transition
+%% can transiently eat a lot of memory and take some time (flushing a
+%% few million messages to disk is never going to be instantaneous).
+
+start_link() ->
+ gen_server2:start_link({local, ?SERVER}, ?MODULE, [], []).
+
+register(Pid, Unoppressable, Module, Function, Args) ->
+ gen_server2:cast(?SERVER, {register, Pid, Unoppressable,
+ Module, Function, Args}).
+
+report_memory(Pid, Memory, Hibernating) ->
+ gen_server2:cast(?SERVER, {report_memory, Pid, Memory, Hibernating}).
+
+info() ->
+ gen_server2:call(?SERVER, info).
+
+conserve_memory(_Pid, Conserve) ->
+ gen_server2:pcast(?SERVER, 9, {conserve_memory, Conserve}).
+
+%%----------------------------------------------------------------------------
+
+init([]) ->
+ process_flag(trap_exit, true),
+ rabbit_alarm:register(self(), {?MODULE, conserve_memory, []}),
+ {MemTotal, MemUsed, _BigProc} = memsup:get_memory_data(),
+ MemAvail = MemTotal - MemUsed,
+ TPB = if MemAvail == 0 -> 0;
+ true -> ?TOTAL_TOKENS / MemAvail
+ end,
+ {ok, #state { available_tokens = ?TOTAL_TOKENS,
+ processes = dict:new(),
+ callbacks = dict:new(),
+ tokens_per_byte = TPB,
+ hibernate = queue:new(),
+ unoppressable = sets:new(),
+ alarmed = false
+ }}.
+
+handle_call(info, _From, State) ->
+ State1 = #state { available_tokens = Avail,
+ processes = Procs,
+ hibernate = Sleepy,
+ unoppressable = Unoppressable } =
+ free_upto(undefined, 1 + ?TOTAL_TOKENS, State), %% just tidy
+ {reply, [{ available_tokens, Avail },
+ { processes, dict:to_list(Procs) },
+ { hibernated_processes, queue:to_list(Sleepy) },
+ { unoppressable_processes, sets:to_list(Unoppressable) }], State1}.
+
+handle_cast({report_memory, Pid, Memory, Hibernating},
+ State = #state { processes = Procs,
+ available_tokens = Avail,
+ callbacks = Callbacks,
+ tokens_per_byte = TPB,
+ alarmed = Alarmed }) ->
+ Req = rabbit_misc:ceil(TPB * Memory),
+ LibreActivity = if Hibernating -> hibernate;
+ true -> active
+ end,
+ {StateN = #state { hibernate = Sleepy }, ActivityNew} =
+ case find_process(Pid, Procs) of
+ {libre, OAlloc, _OActivity} ->
+ Avail1 = Avail + OAlloc,
+ State1 = #state { available_tokens = Avail2,
+ processes = Procs1 }
+ = free_upto(Pid, Req,
+ State #state { available_tokens = Avail1 }),
+ case Req > Avail2 of
+ true -> %% nowt we can do, oppress the process
+ Procs2 =
+ set_process_mode(Procs1, Callbacks, Pid, oppressed,
+ {oppressed, Avail2}),
+ {State1 #state { processes = Procs2 }, oppressed};
+ false -> %% keep liberated
+ {State1 #state
+ { processes =
+ dict:store(Pid, {libre, Req, LibreActivity}, Procs1),
+ available_tokens = Avail2 - Req },
+ LibreActivity}
+ end;
+ {oppressed, OrigAvail} ->
+ case Req > 0 andalso
+ ( Alarmed orelse Hibernating orelse
+ (Avail > (OrigAvail - ?THRESHOLD_OFFSET) andalso
+ Avail < (OrigAvail + ?THRESHOLD_OFFSET)) ) of
+ true ->
+ {State, oppressed};
+ false ->
+ State1 = #state { available_tokens = Avail1,
+ processes = Procs1 } =
+ free_upto(Pid, Req, State),
+ case Req > Avail1 of
+ true ->
+ %% not enough space, so stay oppressed
+ {State1, oppressed};
+ false -> %% can liberate the process
+ Procs2 = set_process_mode(
+ Procs1, Callbacks, Pid, liberated,
+ {libre, Req, LibreActivity}),
+ {State1 #state {
+ processes = Procs2,
+ available_tokens = Avail1 - Req },
+ LibreActivity}
+ end
+ end
+ end,
+ StateN1 =
+ case ActivityNew of
+ active -> StateN;
+ oppressed -> StateN;
+ hibernate ->
+ StateN #state { hibernate = queue:in(Pid, Sleepy) }
+ end,
+ {noreply, StateN1};
+
+handle_cast({register, Pid, IsUnoppressable, Module, Function, Args},
+ State = #state { callbacks = Callbacks,
+ unoppressable = Unoppressable }) ->
+ _MRef = erlang:monitor(process, Pid),
+ Unoppressable1 = case IsUnoppressable of
+ true -> sets:add_element(Pid, Unoppressable);
+ false -> Unoppressable
+ end,
+ {noreply, State #state { callbacks = dict:store
+ (Pid, {Module, Function, Args}, Callbacks),
+ unoppressable = Unoppressable1
+ }};
+
+handle_cast({conserve_memory, Conserve}, State) ->
+ {noreply, State #state { alarmed = Conserve }}.
+
+handle_info({'DOWN', _MRef, process, Pid, _Reason},
+ State = #state { available_tokens = Avail,
+ processes = Procs,
+ callbacks = Callbacks }) ->
+ State1 = State #state { processes = dict:erase(Pid, Procs),
+ callbacks = dict:erase(Pid, Callbacks) },
+ {noreply, case find_process(Pid, Procs) of
+ {oppressed, _OrigReq} ->
+ State1;
+ {libre, Alloc, _Activity} ->
+ State1 #state { available_tokens = Avail + Alloc }
+ end};
+handle_info({'EXIT', _Pid, Reason}, State) ->
+ {stop, Reason, State};
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, State) ->
+ State.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%----------------------------------------------------------------------------
+
+find_process(Pid, Procs) ->
+ case dict:find(Pid, Procs) of
+ {ok, Value} -> Value;
+ error -> {oppressed, 0}
+ end.
+
+set_process_mode(Procs, Callbacks, Pid, Mode, Record) ->
+ {Module, Function, Args} = dict:fetch(Pid, Callbacks),
+ ok = erlang:apply(Module, Function, Args ++ [Mode]),
+ dict:store(Pid, Record, Procs).
+
+tidy_and_sum_sleepy(IgnorePids, Sleepy, Procs) ->
+ tidy_and_sum(hibernate, Procs, fun queue:out/1,
+ fun (Pid, _Alloc, Queue) -> queue:in(Pid, Queue) end,
+ IgnorePids, Sleepy, queue:new(), 0).
+
+tidy_and_sum(AtomExpected, Procs, Generator, Consumer, DupCheckSet,
+ GenInit, ConInit, AllocAcc) ->
+ case Generator(GenInit) of
+ {empty, _GetInit} -> {ConInit, AllocAcc};
+ {{value, Pid}, GenInit1} ->
+ {DupCheckSet1, ConInit1, AllocAcc1} =
+ case sets:is_element(Pid, DupCheckSet) of
+ true ->
+ {DupCheckSet, ConInit, AllocAcc};
+ false ->
+ case find_process(Pid, Procs) of
+ {libre, Alloc, AtomExpected} ->
+ {sets:add_element(Pid, DupCheckSet),
+ Consumer(Pid, Alloc, ConInit),
+ Alloc + AllocAcc};
+ _ ->
+ {DupCheckSet, ConInit, AllocAcc}
+ end
+ end,
+ tidy_and_sum(AtomExpected, Procs, Generator, Consumer,
+ DupCheckSet1, GenInit1, ConInit1, AllocAcc1)
+ end.
+
+free_upto_sleepy(IgnorePids, Callbacks, Sleepy, Procs, Req, Avail) ->
+ free_from(Callbacks,
+ fun(Procs1, Sleepy1, SleepyAcc) ->
+ case queue:out(Sleepy1) of
+ {empty, _Sleepy2} ->
+ empty;
+ {{value, Pid}, Sleepy2} ->
+ case sets:is_element(Pid, IgnorePids) of
+ true -> {skip, Sleepy2,
+ queue:in(Pid, SleepyAcc)};
+ false -> {libre, Alloc, hibernate} =
+ dict:fetch(Pid, Procs1),
+ {value, Sleepy2, Pid, Alloc}
+ end
+ end
+ end, fun queue:join/2, Procs, Sleepy, queue:new(), Req, Avail).
+
+free_from(
+ Callbacks, Transformer, BaseCase, Procs, DestroyMe, CreateMe, Req, Avail) ->
+ case Transformer(Procs, DestroyMe, CreateMe) of
+ empty ->
+ {CreateMe, Procs, Req};
+ {skip, DestroyMe1, CreateMe1} ->
+ free_from(Callbacks, Transformer, BaseCase, Procs, DestroyMe1,
+ CreateMe1, Req, Avail);
+ {value, DestroyMe1, Pid, Alloc} ->
+ Procs1 = set_process_mode(
+ Procs, Callbacks, Pid, oppressed, {oppressed, Avail}),
+ Req1 = Req - Alloc,
+ case Req1 > 0 of
+ true -> free_from(Callbacks, Transformer, BaseCase, Procs1,
+ DestroyMe1, CreateMe, Req1, Avail);
+ false -> {BaseCase(DestroyMe1, CreateMe), Procs1, Req1}
+ end
+ end.
+
+free_upto(Pid, Req, State = #state { available_tokens = Avail,
+ processes = Procs,
+ callbacks = Callbacks,
+ hibernate = Sleepy,
+ unoppressable = Unoppressable })
+ when Req > Avail ->
+ Unoppressable1 = sets:add_element(Pid, Unoppressable),
+ {Sleepy1, SleepySum} = tidy_and_sum_sleepy(Unoppressable1, Sleepy, Procs),
+ case Req > Avail + SleepySum of
+ true -> %% not enough in sleepy, just return tidied state
+ State #state { hibernate = Sleepy1 };
+ false ->
+ %% ReqRem will be <= 0 because it's likely we'll have
+ %% freed more than we need, thus Req - ReqRem is total
+ %% freed
+ {Sleepy2, Procs1, ReqRem} =
+ free_upto_sleepy(Unoppressable1, Callbacks,
+ Sleepy1, Procs, Req, Avail),
+ State #state { available_tokens = Avail + (Req - ReqRem),
+ processes = Procs1,
+ hibernate = Sleepy2 }
+ end;
+free_upto(_Pid, _Req, State) ->
+ State.
diff --git a/src/rabbit_mnesia.erl b/src/rabbit_mnesia.erl
index 749038dbb1..a1d886bb57 100644
--- a/src/rabbit_mnesia.erl
+++ b/src/rabbit_mnesia.erl
@@ -162,7 +162,13 @@ table_definitions() ->
{disc_copies, [node()]}]},
{rabbit_queue,
[{record_name, amqqueue},
- {attributes, record_info(fields, amqqueue)}]}].
+ {attributes, record_info(fields, amqqueue)}]},
+ {rabbit_disk_queue,
+ [{record_name, dq_msg_loc},
+ {attributes, record_info(fields, dq_msg_loc)},
+ {disc_copies, [node()]},
+ {local_content, true}]}
+ ].
table_names() ->
[Tab || {Tab, _} <- table_definitions()].
@@ -196,7 +202,8 @@ ensure_mnesia_not_running() ->
check_schema_integrity() ->
%%TODO: more thorough checks
- case catch [mnesia:table_info(Tab, version) || Tab <- table_names()] of
+ case catch [mnesia:table_info(Tab, version)
+ || Tab <- table_names()] of
{'EXIT', Reason} -> {error, Reason};
_ -> ok
end.
diff --git a/src/rabbit_msg_file.erl b/src/rabbit_msg_file.erl
new file mode 100644
index 0000000000..c08261591d
--- /dev/null
+++ b/src/rabbit_msg_file.erl
@@ -0,0 +1,141 @@
+%% The contents of this file are subject to the Mozilla Public License
+%% Version 1.1 (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.mozilla.org/MPL/
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+%% License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% The Original Code is RabbitMQ.
+%%
+%% The Initial Developers of the Original Code are LShift Ltd,
+%% Cohesive Financial Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created before 22-Nov-2008 00:00:00 GMT by LShift Ltd,
+%% Cohesive Financial Technologies LLC, or Rabbit Technologies Ltd
+%% are Copyright (C) 2007-2008 LShift Ltd, Cohesive Financial
+%% Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created by LShift Ltd are Copyright (C) 2007-2009 LShift
+%% Ltd. Portions created by Cohesive Financial Technologies LLC are
+%% Copyright (C) 2007-2009 Cohesive Financial Technologies
+%% LLC. Portions created by Rabbit Technologies Ltd are Copyright
+%% (C) 2007-2009 Rabbit Technologies Ltd.
+%%
+%% All Rights Reserved.
+%%
+%% Contributor(s): ______________________________________.
+%%
+
+-module(rabbit_msg_file).
+
+-export([append/3, read/2, scan/1]).
+
+%%----------------------------------------------------------------------------
+
+-define(INTEGER_SIZE_BYTES, 8).
+-define(INTEGER_SIZE_BITS, (8 * ?INTEGER_SIZE_BYTES)).
+-define(WRITE_OK_SIZE_BITS, 8).
+-define(WRITE_OK_MARKER, 255).
+-define(FILE_PACKING_ADJUSTMENT, (1 + ?INTEGER_SIZE_BYTES)).
+-define(MSG_ID_SIZE_BYTES, 16).
+-define(MSG_ID_SIZE_BITS, (8 * ?MSG_ID_SIZE_BYTES)).
+-define(SIZE_AND_MSG_ID_BYTES, (?MSG_ID_SIZE_BYTES + ?INTEGER_SIZE_BYTES)).
+
+%%----------------------------------------------------------------------------
+
+-ifdef(use_specs).
+
+-type(io_device() :: any()).
+-type(msg_id() :: binary()).
+-type(msg() :: any()).
+-type(position() :: non_neg_integer()).
+-type(msg_size() :: non_neg_integer()).
+
+-spec(append/3 :: (io_device(), msg_id(), msg()) ->
+ ({'ok', msg_size()} | {'error', any()})).
+-spec(read/2 :: (io_device(), msg_size()) ->
+ ({'ok', {msg_id(), msg()}} | {'error', any()})).
+-spec(scan/1 :: (io_device()) ->
+ {'ok', [{msg_id(), msg_size(), position()}]}).
+
+-endif.
+
+%%----------------------------------------------------------------------------
+
+append(FileHdl, MsgId, MsgBody)
+ when is_binary(MsgId) andalso size(MsgId) =< ?MSG_ID_SIZE_BYTES ->
+ MsgBodyBin = term_to_binary(MsgBody),
+ MsgBodyBinSize = size(MsgBodyBin),
+ Size = MsgBodyBinSize + ?MSG_ID_SIZE_BYTES,
+ case file_handle_cache:append(FileHdl,
+ <<Size:?INTEGER_SIZE_BITS,
+ MsgId:?MSG_ID_SIZE_BYTES/binary,
+ MsgBodyBin:MsgBodyBinSize/binary,
+ ?WRITE_OK_MARKER:?WRITE_OK_SIZE_BITS>>) of
+ ok -> {ok, Size + ?FILE_PACKING_ADJUSTMENT};
+ KO -> KO
+ end.
+
+read(FileHdl, TotalSize) ->
+ Size = TotalSize - ?FILE_PACKING_ADJUSTMENT,
+ BodyBinSize = Size - ?MSG_ID_SIZE_BYTES,
+ case file_handle_cache:read(FileHdl, TotalSize) of
+ {ok, <<Size:?INTEGER_SIZE_BITS,
+ MsgId:?MSG_ID_SIZE_BYTES/binary,
+ MsgBodyBin:BodyBinSize/binary,
+ ?WRITE_OK_MARKER:?WRITE_OK_SIZE_BITS>>} ->
+ {ok, {MsgId, binary_to_term(MsgBodyBin)}};
+ KO -> KO
+ end.
+
+scan(FileHdl) -> scan(FileHdl, 0, []).
+
+scan(FileHdl, Offset, Acc) ->
+ case read_next(FileHdl, Offset) of
+ eof -> {ok, Acc};
+ {corrupted, NextOffset} ->
+ scan(FileHdl, NextOffset, Acc);
+ {ok, {MsgId, TotalSize, NextOffset}} ->
+ scan(FileHdl, NextOffset, [{MsgId, TotalSize, Offset} | Acc]);
+ _KO ->
+ %% bad message, but we may still have recovered some valid messages
+ {ok, Acc}
+ end.
+
+read_next(FileHdl, Offset) ->
+ case file_handle_cache:read(FileHdl, ?SIZE_AND_MSG_ID_BYTES) of
+ %% Here we take option 5 from
+ %% http://www.erlang.org/cgi-bin/ezmlm-cgi?2:mss:1569 in which
+ %% we read the MsgId as a number, and then convert it back to
+ %% a binary in order to work around bugs in Erlang's GC.
+ {ok, <<Size:?INTEGER_SIZE_BITS, MsgIdNum:?MSG_ID_SIZE_BITS>>} ->
+ case Size of
+ 0 -> eof; %% Nothing we can do other than stop
+ _ ->
+ TotalSize = Size + ?FILE_PACKING_ADJUSTMENT,
+ ExpectedAbsPos = Offset + TotalSize - 1,
+ case file_handle_cache:position(
+ FileHdl, {cur, Size - ?MSG_ID_SIZE_BYTES}) of
+ {ok, ExpectedAbsPos} ->
+ NextOffset = ExpectedAbsPos + 1,
+ case file_handle_cache:read(FileHdl, 1) of
+ {ok,
+ <<?WRITE_OK_MARKER: ?WRITE_OK_SIZE_BITS>>} ->
+ <<MsgId:?MSG_ID_SIZE_BYTES/binary>> =
+ <<MsgIdNum:?MSG_ID_SIZE_BITS>>,
+ {ok, {MsgId, TotalSize, NextOffset}};
+ {ok, _SomeOtherData} ->
+ {corrupted, NextOffset};
+ KO -> KO
+ end;
+ {ok, _SomeOtherPos} ->
+ %% seek failed, so give up
+ eof;
+ KO -> KO
+ end
+ end;
+ Other -> Other
+ end.
diff --git a/src/rabbit_msg_store.erl b/src/rabbit_msg_store.erl
new file mode 100644
index 0000000000..591435ba01
--- /dev/null
+++ b/src/rabbit_msg_store.erl
@@ -0,0 +1,1080 @@
+%% The contents of this file are subject to the Mozilla Public License
+%% Version 1.1 (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.mozilla.org/MPL/
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+%% License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% The Original Code is RabbitMQ.
+%%
+%% The Initial Developers of the Original Code are LShift Ltd,
+%% Cohesive Financial Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created before 22-Nov-2008 00:00:00 GMT by LShift Ltd,
+%% Cohesive Financial Technologies LLC, or Rabbit Technologies Ltd
+%% are Copyright (C) 2007-2008 LShift Ltd, Cohesive Financial
+%% Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created by LShift Ltd are Copyright (C) 2007-2009 LShift
+%% Ltd. Portions created by Cohesive Financial Technologies LLC are
+%% Copyright (C) 2007-2009 Cohesive Financial Technologies
+%% LLC. Portions created by Rabbit Technologies Ltd are Copyright
+%% (C) 2007-2009 Rabbit Technologies Ltd.
+%%
+%% All Rights Reserved.
+%%
+%% Contributor(s): ______________________________________.
+%%
+
+-module(rabbit_msg_store).
+
+-behaviour(gen_server2).
+
+-export([start_link/3, write/2, read/1, peruse/2, contains/1, remove/1,
+ release/1, sync/2]).
+
+-export([sync/0]). %% internal
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-define(SERVER, ?MODULE).
+
+-define(MAX_READ_FILE_HANDLES, 256).
+-define(FILE_SIZE_LIMIT, (256*1024*1024)).
+-define(SYNC_INTERVAL, 5). %% milliseconds
+-define(HANDLE_CACHE_BUFFER_SIZE, 1048576). %% 1MB
+
+%%----------------------------------------------------------------------------
+
+-ifdef(use_specs).
+
+-type(msg_id() :: binary()).
+-type(msg() :: any()).
+-type(file_path() :: any()).
+
+-spec(start_link/3 ::
+ (file_path(),
+ (fun ((A) -> 'finished' | {msg_id(), non_neg_integer(), A})), A) ->
+ {'ok', pid()} | 'ignore' | {'error', any()}).
+-spec(write/2 :: (msg_id(), msg()) -> 'ok').
+-spec(read/1 :: (msg_id()) -> {'ok', msg()} | 'not_found').
+-spec(peruse/2 :: (msg_id(), fun (({'ok', msg()} | 'not_found') -> 'ok')) ->
+ 'ok').
+-spec(contains/1 :: (msg_id()) -> boolean()).
+-spec(remove/1 :: ([msg_id()]) -> 'ok').
+-spec(release/1 :: ([msg_id()]) -> 'ok').
+-spec(sync/2 :: ([msg_id()], fun (() -> any())) -> 'ok').
+
+-endif.
+
+%%----------------------------------------------------------------------------
+
+-record(msstate,
+ {dir, %% store directory
+ msg_locations, %% where are messages?
+ file_summary, %% what's in the files?
+ current_file, %% current file name as number
+ current_file_handle, %% current file handle
+ %% since the last fsync?
+ file_size_limit, %% how big can our files get?
+ file_handle_cache, %% file handle cache
+ on_sync, %% pending sync requests
+ sync_timer_ref, %% TRef for our interval timer
+ message_cache %% ets message cache
+ }).
+
+-record(msg_location,
+ {msg_id, ref_count, file, offset, total_size}).
+
+-record(file_summary,
+ {file, valid_total_size, contiguous_top, left, right}).
+
+-define(MSG_LOC_NAME, rabbit_disk_queue_msg_location).
+-define(FILE_SUMMARY_ETS_NAME, rabbit_disk_queue_file_summary).
+-define(FILE_EXTENSION, ".rdq").
+-define(FILE_EXTENSION_TMP, ".rdt").
+-define(CACHE_ETS_NAME, rabbit_disk_queue_cache).
+
+-define(BINARY_MODE, [raw, binary]).
+-define(READ_MODE, [read]).
+-define(READ_AHEAD_MODE, [read_ahead | ?READ_MODE]).
+-define(WRITE_MODE, [write]).
+
+%% The components:
+%%
+%% MsgLocation: this is an ets table which contains:
+%% {MsgId, RefCount, File, Offset, TotalSize}
+%% FileSummary: this is an ets table which contains:
+%% {File, ValidTotalSize, ContiguousTop, Left, Right}
+%%
+%% The basic idea is that messages are appended to the current file up
+%% until that file becomes too big (> file_size_limit). At that point,
+%% the file is closed and a new file is created on the _right_ of the
+%% old file which is used for new messages. Files are named
+%% numerically ascending, thus the file with the lowest name is the
+%% eldest file.
+%%
+%% We need to keep track of which messages are in which files (this is
+%% the MsgLocation table); how much useful data is in each file and
+%% which files are on the left and right of each other. This is the
+%% purpose of the FileSummary table.
+%%
+%% As messages are removed from files, holes appear in these
+%% files. The field ValidTotalSize contains the total amount of useful
+%% data left in the file, whilst ContiguousTop contains the amount of
+%% valid data right at the start of each file. These are needed for
+%% garbage collection.
+%%
+%% When we discover that either a file is now empty or that it can be
+%% combined with the useful data in either its left or right file, we
+%% compact the two files together. This keeps disk utilisation high
+%% and aids performance.
+%%
+%% Given the compaction between two files, the left file is considered
+%% the ultimate destination for the good data in the right file. If
+%% necessary, the good data in the left file which is fragmented
+%% throughout the file is written out to a temporary file, then read
+%% back in to form a contiguous chunk of good data at the start of the
+%% left file. Thus the left file is garbage collected and
+%% compacted. Then the good data from the right file is copied onto
+%% the end of the left file. MsgLocation and FileSummary tables are
+%% updated.
+%%
+%% On startup, we scan the files we discover, dealing with the
+%% possibilites of a crash have occured during a compaction (this
+%% consists of tidyup - the compaction is deliberately designed such
+%% that data is duplicated on disk rather than risking it being lost),
+%% and rebuild the ets tables (MsgLocation, FileSummary).
+%%
+%% So, with this design, messages move to the left. Eventually, they
+%% should end up in a contiguous block on the left and are then never
+%% rewritten. But this isn't quite the case. If in a file there is one
+%% message that is being ignored, for some reason, and messages in the
+%% file to the right and in the current block are being read all the
+%% time then it will repeatedly be the case that the good data from
+%% both files can be combined and will be written out to a new
+%% file. Whenever this happens, our shunned message will be rewritten.
+%%
+%% So, provided that we combine messages in the right order,
+%% (i.e. left file, bottom to top, right file, bottom to top),
+%% eventually our shunned message will end up at the bottom of the
+%% left file. The compaction/combining algorithm is smart enough to
+%% read in good data from the left file that is scattered throughout
+%% (i.e. C and D in the below diagram), then truncate the file to just
+%% above B (i.e. truncate to the limit of the good contiguous region
+%% at the start of the file), then write C and D on top and then write
+%% E, F and G from the right file on top. Thus contiguous blocks of
+%% good data at the bottom of files are not rewritten (yes, this is
+%% the data the size of which is tracked by the ContiguousTop
+%% variable. Judicious use of a mirror is required).
+%%
+%% +-------+ +-------+ +-------+
+%% | X | | G | | G |
+%% +-------+ +-------+ +-------+
+%% | D | | X | | F |
+%% +-------+ +-------+ +-------+
+%% | X | | X | | E |
+%% +-------+ +-------+ +-------+
+%% | C | | F | ===> | D |
+%% +-------+ +-------+ +-------+
+%% | X | | X | | C |
+%% +-------+ +-------+ +-------+
+%% | B | | X | | B |
+%% +-------+ +-------+ +-------+
+%% | A | | E | | A |
+%% +-------+ +-------+ +-------+
+%% left right left
+%%
+%% From this reasoning, we do have a bound on the number of times the
+%% message is rewritten. From when it is inserted, there can be no
+%% files inserted between it and the head of the queue, and the worst
+%% case is that everytime it is rewritten, it moves one position lower
+%% in the file (for it to stay at the same position requires that
+%% there are no holes beneath it, which means truncate would be used
+%% and so it would not be rewritten at all). Thus this seems to
+%% suggest the limit is the number of messages ahead of it in the
+%% queue, though it's likely that that's pessimistic, given the
+%% requirements for compaction/combination of files.
+%%
+%% The other property is that we have is the bound on the lowest
+%% utilisation, which should be 50% - worst case is that all files are
+%% fractionally over half full and can't be combined (equivalent is
+%% alternating full files and files with only one tiny message in
+%% them).
+%%
+%% Messages are reference-counted. When a message with the same id is
+%% written several times we only store it once, and only remove it
+%% from the store when it has been removed the same number of times.
+%%
+%% The reference counts do not persist. Therefore the initialisation
+%% function must be provided with a generator that produces ref count
+%% deltas for all recovered messages.
+%%
+%% Read messages with a reference count greater than one are entered
+%% into a message cache. The purpose of the cache is not especially
+%% performance, though it can help there too, but prevention of memory
+%% explosion. It ensures that as messages with a high reference count
+%% are read from several processes they are read back as the same
+%% binary object rather than multiples of identical binary
+%% objects.
+
+%%----------------------------------------------------------------------------
+%% public API
+%%----------------------------------------------------------------------------
+
+start_link(Dir, MsgRefDeltaGen, MsgRefDeltaGenInit) ->
+ gen_server2:start_link({local, ?SERVER}, ?MODULE,
+ [Dir, MsgRefDeltaGen, MsgRefDeltaGenInit],
+ [{timeout, infinity}]).
+
+write(MsgId, Msg) -> gen_server2:cast(?SERVER, {write, MsgId, Msg}).
+read(MsgId) -> gen_server2:call(?SERVER, {read, MsgId}, infinity).
+peruse(MsgId, Fun) -> gen_server2:pcast(?SERVER, -1, {peruse, MsgId, Fun}).
+contains(MsgId) -> gen_server2:call(?SERVER, {contains, MsgId}, infinity).
+remove(MsgIds) -> gen_server2:cast(?SERVER, {remove, MsgIds}).
+release(MsgIds) -> gen_server2:cast(?SERVER, {release, MsgIds}).
+sync(MsgIds, K) -> gen_server2:cast(?SERVER, {sync, MsgIds, K}).
+sync() -> gen_server2:pcast(?SERVER, 9, sync). %% internal
+
+%%----------------------------------------------------------------------------
+%% gen_server callbacks
+%%----------------------------------------------------------------------------
+
+init([Dir, MsgRefDeltaGen, MsgRefDeltaGenInit]) ->
+ process_flag(trap_exit, true),
+
+ ok = filelib:ensure_dir(filename:join(Dir, "nothing")),
+
+ MsgLocations = ets:new(?MSG_LOC_NAME,
+ [set, private, {keypos, #msg_location.msg_id}]),
+
+ InitFile = 0,
+ FileSummary = ets:new(?FILE_SUMMARY_ETS_NAME,
+ [set, private, {keypos, #file_summary.file}]),
+ MessageCache = ets:new(?CACHE_ETS_NAME, [set, private]),
+ State =
+ #msstate { dir = Dir,
+ msg_locations = MsgLocations,
+ file_summary = FileSummary,
+ current_file = InitFile,
+ current_file_handle = undefined,
+ file_size_limit = ?FILE_SIZE_LIMIT,
+ file_handle_cache = dict:new(),
+ on_sync = [],
+ sync_timer_ref = undefined,
+ message_cache = MessageCache
+ },
+
+ ok = count_msg_refs(MsgRefDeltaGen, MsgRefDeltaGenInit, State),
+ FileNames =
+ sort_file_names(filelib:wildcard("*" ++ ?FILE_EXTENSION, Dir)),
+ TmpFileNames =
+ sort_file_names(filelib:wildcard("*" ++ ?FILE_EXTENSION_TMP, Dir)),
+ ok = recover_crashed_compactions(Dir, FileNames, TmpFileNames),
+ %% There should be no more tmp files now, so go ahead and load the
+ %% whole lot
+ Files = [filename_to_num(FileName) || FileName <- FileNames],
+ {Offset, State1 = #msstate { current_file = CurFile }} =
+ build_index(Files, State),
+
+ %% read is only needed so that we can seek
+ {ok, FileHdl} = open_file(Dir, filenum_to_name(CurFile),
+ [read | ?WRITE_MODE]),
+ {ok, Offset} = file_handle_cache:position(FileHdl, Offset),
+ ok = file_handle_cache:truncate(FileHdl),
+
+ {ok, State1 #msstate { current_file_handle = FileHdl }}.
+
+handle_call({read, MsgId}, _From, State) ->
+ {Result, State1} = internal_read_message(MsgId, State),
+ reply(Result, State1);
+
+handle_call({contains, MsgId}, _From, State) ->
+ reply(case index_lookup(MsgId, State) of
+ not_found -> false;
+ #msg_location {} -> true
+ end, State).
+
+handle_cast({write, MsgId, Msg},
+ State = #msstate { current_file_handle = CurHdl,
+ current_file = CurFile,
+ file_summary = FileSummary }) ->
+ case index_lookup(MsgId, State) of
+ not_found ->
+ %% New message, lots to do
+ {ok, CurOffset} = file_handle_cache:current_virtual_offset(CurHdl),
+ {ok, TotalSize} = rabbit_msg_file:append(CurHdl, MsgId, Msg),
+ ok = index_insert(#msg_location {
+ msg_id = MsgId, ref_count = 1, file = CurFile,
+ offset = CurOffset, total_size = TotalSize },
+ State),
+ [FSEntry = #file_summary { valid_total_size = ValidTotalSize,
+ contiguous_top = ContiguousTop,
+ right = undefined }] =
+ ets:lookup(FileSummary, CurFile),
+ ValidTotalSize1 = ValidTotalSize + TotalSize,
+ ContiguousTop1 = if CurOffset =:= ContiguousTop ->
+ %% can't be any holes in this file
+ ValidTotalSize1;
+ true -> ContiguousTop
+ end,
+ true = ets:insert(FileSummary, FSEntry #file_summary {
+ valid_total_size = ValidTotalSize1,
+ contiguous_top = ContiguousTop1 }),
+ NextOffset = CurOffset + TotalSize,
+ noreply(maybe_roll_to_new_file(NextOffset, State));
+ StoreEntry = #msg_location { ref_count = RefCount } ->
+ %% We already know about it, just update counter
+ ok = index_update(StoreEntry #msg_location {
+ ref_count = RefCount + 1 }, State),
+ noreply(State)
+ end;
+
+handle_cast({peruse, MsgId, Fun}, State) ->
+ {Result, State1} = internal_read_message(MsgId, State),
+ Fun(Result),
+ noreply(State1);
+
+handle_cast({remove, MsgIds}, State = #msstate { current_file = CurFile }) ->
+ noreply(
+ compact(sets:to_list(
+ lists:foldl(
+ fun (MsgId, Files1) ->
+ case remove_message(MsgId, State) of
+ {compact, File} ->
+ if CurFile =:= File -> Files1;
+ true -> sets:add_element(File, Files1)
+ end;
+ no_compact -> Files1
+ end
+ end, sets:new(), MsgIds)),
+ State));
+
+handle_cast({release, MsgIds}, State) ->
+ lists:foreach(fun (MsgId) -> decrement_cache(MsgId, State) end, MsgIds),
+ noreply(State);
+
+handle_cast({sync, MsgIds, K},
+ State = #msstate { current_file = CurFile,
+ current_file_handle = CurHdl,
+ on_sync = Syncs }) ->
+ {ok, SyncOffset} = file_handle_cache:last_sync_offset(CurHdl),
+ case lists:any(fun (MsgId) ->
+ #msg_location { file = File, offset = Offset } =
+ index_lookup(MsgId, State),
+ File =:= CurFile andalso Offset >= SyncOffset
+ end, MsgIds) of
+ false -> K(),
+ noreply(State);
+ true -> noreply(State #msstate { on_sync = [K | Syncs] })
+ end;
+
+handle_cast(sync, State) ->
+ noreply(sync(State)).
+
+handle_info(timeout, State) ->
+ noreply(sync(State)).
+
+terminate(_Reason, State = #msstate { msg_locations = MsgLocations,
+ file_summary = FileSummary,
+ current_file_handle = FileHdl }) ->
+ State1 = case FileHdl of
+ undefined -> State;
+ _ -> State2 = sync(State),
+ file_handle_cache:close(FileHdl),
+ State2
+ end,
+ State3 = close_all_handles(State1),
+ ets:delete(MsgLocations),
+ ets:delete(FileSummary),
+ State3 #msstate { msg_locations = undefined,
+ file_summary = undefined,
+ current_file_handle = undefined }.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%----------------------------------------------------------------------------
+%% general helper functions
+%%----------------------------------------------------------------------------
+
+noreply(State) ->
+ {State1, Timeout} = next_state(State),
+ {noreply, State1, Timeout}.
+
+reply(Reply, State) ->
+ {State1, Timeout} = next_state(State),
+ {reply, Reply, State1, Timeout}.
+
+next_state(State = #msstate { on_sync = [], sync_timer_ref = undefined }) ->
+ {State, infinity};
+next_state(State = #msstate { sync_timer_ref = undefined }) ->
+ {start_sync_timer(State), 0};
+next_state(State = #msstate { on_sync = [] }) ->
+ {stop_sync_timer(State), infinity};
+next_state(State) ->
+ {State, 0}.
+
+start_sync_timer(State = #msstate { sync_timer_ref = undefined }) ->
+ {ok, TRef} = timer:apply_after(?SYNC_INTERVAL, ?MODULE, sync, []),
+ State #msstate { sync_timer_ref = TRef }.
+
+stop_sync_timer(State = #msstate { sync_timer_ref = undefined }) ->
+ State;
+stop_sync_timer(State = #msstate { sync_timer_ref = TRef }) ->
+ {ok, cancel} = timer:cancel(TRef),
+ State #msstate { sync_timer_ref = undefined }.
+
+form_filename(Dir, Name) -> filename:join(Dir, Name).
+
+filenum_to_name(File) -> integer_to_list(File) ++ ?FILE_EXTENSION.
+
+filename_to_num(FileName) -> list_to_integer(filename:rootname(FileName)).
+
+sort_file_names(FileNames) ->
+ lists:sort(fun (A, B) -> filename_to_num(A) < filename_to_num(B) end,
+ FileNames).
+
+preallocate(Hdl, FileSizeLimit, FinalPos) ->
+ {ok, FileSizeLimit} = file_handle_cache:position(Hdl, FileSizeLimit),
+ ok = file_handle_cache:truncate(Hdl),
+ {ok, FinalPos} = file_handle_cache:position(Hdl, FinalPos),
+ ok.
+
+truncate_and_extend_file(FileHdl, Lowpoint, Highpoint) ->
+ {ok, Lowpoint} = file_handle_cache:position(FileHdl, Lowpoint),
+ ok = file_handle_cache:truncate(FileHdl),
+ ok = preallocate(FileHdl, Highpoint, Lowpoint).
+
+sync(State = #msstate { current_file_handle = CurHdl,
+ on_sync = Syncs }) ->
+ State1 = stop_sync_timer(State),
+ case Syncs of
+ [] -> State1;
+ _ ->
+ ok = file_handle_cache:sync(CurHdl),
+ lists:foreach(fun (K) -> K() end, lists:reverse(Syncs)),
+ State1 #msstate { on_sync = [] }
+ end.
+
+remove_message(MsgId, State = #msstate { file_summary = FileSummary }) ->
+ StoreEntry = #msg_location { ref_count = RefCount, file = File,
+ offset = Offset, total_size = TotalSize } =
+ index_lookup(MsgId, State),
+ case RefCount of
+ 1 ->
+ ok = index_delete(MsgId, State),
+ ok = remove_cache_entry(MsgId, State),
+ [FSEntry = #file_summary { valid_total_size = ValidTotalSize,
+ contiguous_top = ContiguousTop }] =
+ ets:lookup(FileSummary, File),
+ ContiguousTop1 = lists:min([ContiguousTop, Offset]),
+ ValidTotalSize1 = ValidTotalSize - TotalSize,
+ true = ets:insert(FileSummary, FSEntry #file_summary {
+ valid_total_size = ValidTotalSize1,
+ contiguous_top = ContiguousTop1 }),
+ {compact, File};
+ _ when 1 < RefCount ->
+ ok = decrement_cache(MsgId, State),
+ ok = index_update(StoreEntry #msg_location {
+ ref_count = RefCount - 1 }, State),
+ no_compact
+ end.
+
+internal_read_message(MsgId,
+ State = #msstate { current_file = CurFile,
+ current_file_handle = CurHdl }) ->
+ case index_lookup(MsgId, State) of
+ not_found -> {not_found, State};
+ #msg_location { ref_count = RefCount,
+ file = File,
+ offset = Offset,
+ total_size = TotalSize } ->
+ case fetch_and_increment_cache(MsgId, State) of
+ not_found ->
+ {ok, CurOffset} =
+ file_handle_cache:current_raw_offset(CurHdl),
+ ok = case CurFile =:= File andalso Offset >= CurOffset of
+ true ->
+ file_handle_cache:append_write_buffer(CurHdl);
+ false ->
+ ok
+ end,
+ {Hdl, State1} = get_read_handle(File, State),
+ {ok, Offset} = file_handle_cache:position(Hdl, Offset),
+ {ok, {MsgId, Msg}} =
+ case rabbit_msg_file:read(Hdl, TotalSize) of
+ {ok, {MsgId, _}} = Obj -> Obj;
+ Rest ->
+ throw({error, {misread, [{old_state, State},
+ {file_num, File},
+ {offset, Offset},
+ {read, Rest},
+ {proc_dict, get()}]}})
+ end,
+ ok = if RefCount > 1 ->
+ insert_into_cache(MsgId, Msg, State1);
+ true -> ok
+ %% it's not in the cache and we
+ %% only have one reference to the
+ %% message. So don't bother
+ %% putting it in the cache.
+ end,
+ {{ok, Msg}, State1};
+ {Msg, _RefCount} ->
+ {{ok, Msg}, State}
+ end
+ end.
+
+close_handle(Key, State = #msstate { file_handle_cache = FHC }) ->
+ case dict:find(Key, FHC) of
+ {ok, Hdl} ->
+ ok = close_file(Hdl),
+ State #msstate { file_handle_cache = dict:erase(Key, FHC) };
+ error -> State
+ end.
+
+close_all_handles(State = #msstate { file_handle_cache = FHC }) ->
+ ok = dict:fold(fun (_Key, Hdl, ok) ->
+ file_handle_cache:close(Hdl)
+ end, ok, FHC),
+ State #msstate { file_handle_cache = dict:new() }.
+
+get_read_handle(FileNum, State = #msstate { file_handle_cache = FHC }) ->
+ case dict:find(FileNum, FHC) of
+ {ok, Hdl} -> {Hdl, State};
+ error -> new_handle(FileNum, filenum_to_name(FileNum),
+ [read | ?BINARY_MODE], State)
+ end.
+
+new_handle(Key, FileName, Mode, State = #msstate { file_handle_cache = FHC,
+ dir = Dir }) ->
+ {ok, Hdl} = open_file(Dir, FileName, Mode),
+ {Hdl, State #msstate { file_handle_cache = dict:store(Key, Hdl, FHC) }}.
+
+open_file(Dir, FileName, Mode) ->
+ file_handle_cache:open(form_filename(Dir, FileName), ?BINARY_MODE ++ Mode,
+ [{write_buffer, ?HANDLE_CACHE_BUFFER_SIZE}]).
+
+close_file(Hdl) ->
+ file_handle_cache:close(Hdl).
+
+%%----------------------------------------------------------------------------
+%% message cache helper functions
+%%----------------------------------------------------------------------------
+
+remove_cache_entry(MsgId, #msstate { message_cache = Cache }) ->
+ true = ets:delete(Cache, MsgId),
+ ok.
+
+fetch_and_increment_cache(MsgId, #msstate { message_cache = Cache }) ->
+ case ets:lookup(Cache, MsgId) of
+ [] ->
+ not_found;
+ [{MsgId, Msg, _RefCount}] ->
+ NewRefCount = ets:update_counter(Cache, MsgId, {3, 1}),
+ {Msg, NewRefCount}
+ end.
+
+decrement_cache(MsgId, #msstate { message_cache = Cache }) ->
+ true = try case ets:update_counter(Cache, MsgId, {3, -1}) of
+ N when N =< 0 -> true = ets:delete(Cache, MsgId);
+ _N -> true
+ end
+ catch error:badarg ->
+ %% MsgId is not in there because although it's been
+ %% delivered, it's never actually been read (think:
+ %% persistent message in mixed queue)
+ true
+ end,
+ ok.
+
+insert_into_cache(MsgId, Msg, #msstate { message_cache = Cache }) ->
+ true = ets:insert_new(Cache, {MsgId, Msg, 1}),
+ ok.
+
+%%----------------------------------------------------------------------------
+%% index
+%%----------------------------------------------------------------------------
+
+index_lookup(Key, #msstate { msg_locations = MsgLocations }) ->
+ case ets:lookup(MsgLocations, Key) of
+ [] -> not_found;
+ [Entry] -> Entry
+ end.
+
+index_insert(Obj, #msstate { msg_locations = MsgLocations }) ->
+ true = ets:insert_new(MsgLocations, Obj),
+ ok.
+
+index_update(Obj, #msstate { msg_locations = MsgLocations }) ->
+ true = ets:insert(MsgLocations, Obj),
+ ok.
+
+index_delete(Key, #msstate { msg_locations = MsgLocations }) ->
+ true = ets:delete(MsgLocations, Key),
+ ok.
+
+index_search_by_file(File, #msstate { msg_locations = MsgLocations }) ->
+ lists:sort(fun (#msg_location { offset = OffA },
+ #msg_location { offset = OffB }) ->
+ OffA < OffB
+ end, ets:match_object(MsgLocations,
+ #msg_location { file = File, _ = '_' })).
+
+
+index_delete_by_file(File, #msstate { msg_locations = MsgLocations }) ->
+ MatchHead = #msg_location { file = File, _ = '_' },
+ ets:select_delete(MsgLocations, [{MatchHead, [], [true]}]),
+ ok.
+
+%%----------------------------------------------------------------------------
+%% recovery
+%%----------------------------------------------------------------------------
+
+count_msg_refs(Gen, Seed, State) ->
+ case Gen(Seed) of
+ finished -> ok;
+ {_MsgId, 0, Next} -> count_msg_refs(Gen, Next, State);
+ {MsgId, Delta, Next} ->
+ ok = case index_lookup(MsgId, State) of
+ not_found ->
+ index_insert(#msg_location { msg_id = MsgId,
+ ref_count = Delta },
+ State);
+ StoreEntry = #msg_location { ref_count = RefCount } ->
+ NewRefCount = RefCount + Delta,
+ case NewRefCount of
+ 0 -> index_delete(MsgId, State);
+ _ -> index_update(StoreEntry #msg_location {
+ ref_count = NewRefCount },
+ State)
+ end
+ end,
+ count_msg_refs(Gen, Next, State)
+ end.
+
+recover_crashed_compactions(Dir, FileNames, TmpFileNames) ->
+ lists:foreach(fun (TmpFileName) ->
+ ok = recover_crashed_compactions1(
+ Dir, FileNames, TmpFileName)
+ end, TmpFileNames),
+ ok.
+
+recover_crashed_compactions1(Dir, FileNames, TmpFileName) ->
+ NonTmpRelatedFileName = filename:rootname(TmpFileName) ++ ?FILE_EXTENSION,
+ true = lists:member(NonTmpRelatedFileName, FileNames),
+ {ok, UncorruptedMessagesTmp, MsgIdsTmp} =
+ scan_file_for_valid_messages_msg_ids(Dir, TmpFileName),
+ {ok, UncorruptedMessages, MsgIds} =
+ scan_file_for_valid_messages_msg_ids(Dir, NonTmpRelatedFileName),
+ %% 1) It's possible that everything in the tmp file is also in the
+ %% main file such that the main file is (prefix ++
+ %% tmpfile). This means that compaction failed immediately
+ %% prior to the final step of deleting the tmp file. Plan: just
+ %% delete the tmp file
+ %% 2) It's possible that everything in the tmp file is also in the
+ %% main file but with holes throughout (or just somthing like
+ %% main = (prefix ++ hole ++ tmpfile)). This means that
+ %% compaction wrote out the tmp file successfully and then
+ %% failed. Plan: just delete the tmp file and allow the
+ %% compaction to eventually be triggered later
+ %% 3) It's possible that everything in the tmp file is also in the
+ %% main file but such that the main file does not end with tmp
+ %% file (and there are valid messages in the suffix; main =
+ %% (prefix ++ tmpfile[with extra holes?] ++ suffix)). This
+ %% means that compaction failed as we were writing out the tmp
+ %% file. Plan: just delete the tmp file and allow the
+ %% compaction to eventually be triggered later
+ %% 4) It's possible that there are messages in the tmp file which
+ %% are not in the main file. This means that writing out the
+ %% tmp file succeeded, but then we failed as we were copying
+ %% them back over to the main file, after truncating the main
+ %% file. As the main file has already been truncated, it should
+ %% consist only of valid messages. Plan: Truncate the main file
+ %% back to before any of the files in the tmp file and copy
+ %% them over again
+ TmpPath = form_filename(Dir, TmpFileName),
+ case is_sublist(MsgIdsTmp, MsgIds) of
+ true -> %% we're in case 1, 2 or 3 above. Just delete the tmp file
+ %% note this also catches the case when the tmp file
+ %% is empty
+ ok = file:delete(TmpPath);
+ false ->
+ %% We're in case 4 above. We only care about the inital
+ %% msgs in main file that are not in the tmp file. If
+ %% there are no msgs in the tmp file then we would be in
+ %% the 'true' branch of this case, so we know the
+ %% lists:last call is safe.
+ EldestTmpMsgId = lists:last(MsgIdsTmp),
+ {MsgIds1, UncorruptedMessages1}
+ = case lists:splitwith(
+ fun (MsgId) -> MsgId /= EldestTmpMsgId end, MsgIds) of
+ {_MsgIds, []} -> %% no msgs from tmp in main
+ {MsgIds, UncorruptedMessages};
+ {Dropped, [EldestTmpMsgId | Rest]} ->
+ %% Msgs in Dropped are in tmp, so forget them.
+ %% *cry*. Lists indexed from 1.
+ {Rest, lists:sublist(UncorruptedMessages,
+ 2 + length(Dropped),
+ length(Rest))}
+ end,
+ %% The main file prefix should be contiguous
+ {Top, MsgIds1} = find_contiguous_block_prefix(
+ lists:reverse(UncorruptedMessages1)),
+ %% we should have that none of the messages in the prefix
+ %% are in the tmp file
+ true = is_disjoint(MsgIds1, MsgIdsTmp),
+ %% must open with read flag, otherwise will stomp over contents
+ {ok, MainHdl} = open_file(Dir, NonTmpRelatedFileName,
+ [read | ?WRITE_MODE]),
+ %% Wipe out any rubbish at the end of the file. Remember
+ %% the head of the list will be the highest entry in the
+ %% file.
+ [{_, TmpTopTotalSize, TmpTopOffset}|_] = UncorruptedMessagesTmp,
+ TmpSize = TmpTopOffset + TmpTopTotalSize,
+ %% Extend the main file as big as necessary in a single
+ %% move. If we run out of disk space, this truncate could
+ %% fail, but we still aren't risking losing data
+ ok = truncate_and_extend_file(MainHdl, Top, Top + TmpSize),
+ {ok, TmpHdl} = open_file(Dir, TmpFileName, ?READ_AHEAD_MODE),
+ {ok, TmpSize} = file_handle_cache:copy(TmpHdl, MainHdl, TmpSize),
+ ok = file_handle_cache:close(MainHdl),
+ ok = file_handle_cache:close(TmpHdl),
+ ok = file:delete(TmpPath),
+
+ {ok, _MainMessages, MsgIdsMain} =
+ scan_file_for_valid_messages_msg_ids(
+ Dir, NonTmpRelatedFileName),
+ %% check that everything in MsgIds1 is in MsgIdsMain
+ true = is_sublist(MsgIds1, MsgIdsMain),
+ %% check that everything in MsgIdsTmp is in MsgIdsMain
+ true = is_sublist(MsgIdsTmp, MsgIdsMain)
+ end,
+ ok.
+
+is_sublist(SmallerL, BiggerL) ->
+ lists:all(fun (Item) -> lists:member(Item, BiggerL) end, SmallerL).
+
+is_disjoint(SmallerL, BiggerL) ->
+ lists:all(fun (Item) -> not lists:member(Item, BiggerL) end, SmallerL).
+
+scan_file_for_valid_messages_msg_ids(Dir, FileName) ->
+ {ok, Messages} = scan_file_for_valid_messages(Dir, FileName),
+ {ok, Messages, [MsgId || {MsgId, _TotalSize, _FileOffset} <- Messages]}.
+
+scan_file_for_valid_messages(Dir, FileName) ->
+ case open_file(Dir, FileName, ?READ_MODE) of
+ {ok, Hdl} ->
+ Valid = rabbit_msg_file:scan(Hdl),
+ %% if something really bad's happened, the close could fail,
+ %% but ignore
+ file_handle_cache:close(Hdl),
+ Valid;
+ {error, enoent} -> {ok, []};
+ {error, Reason} -> throw({error,
+ {unable_to_scan_file, FileName, Reason}})
+ end.
+
+%% Takes the list in *ascending* order (i.e. eldest message
+%% first). This is the opposite of what scan_file_for_valid_messages
+%% produces. The list of msgs that is produced is youngest first.
+find_contiguous_block_prefix([]) -> {0, []};
+find_contiguous_block_prefix(List) ->
+ find_contiguous_block_prefix(List, 0, []).
+
+find_contiguous_block_prefix([], ExpectedOffset, MsgIds) ->
+ {ExpectedOffset, MsgIds};
+find_contiguous_block_prefix([{MsgId, TotalSize, ExpectedOffset} | Tail],
+ ExpectedOffset, MsgIds) ->
+ ExpectedOffset1 = ExpectedOffset + TotalSize,
+ find_contiguous_block_prefix(Tail, ExpectedOffset1, [MsgId | MsgIds]);
+find_contiguous_block_prefix([_MsgAfterGap | _Tail], ExpectedOffset, MsgIds) ->
+ {ExpectedOffset, MsgIds}.
+
+build_index([], State) ->
+ CurFile = State #msstate.current_file,
+ build_index(undefined, [CurFile], [], State);
+build_index(Files, State) ->
+ build_index(undefined, Files, [], State).
+
+build_index(Left, [], FilesToCompact, State) ->
+ ok = index_delete_by_file(undefined, State),
+ Offset = case lists:reverse(index_search_by_file(Left, State)) of
+ [] -> 0;
+ [#msg_location { offset = MaxOffset,
+ total_size = TotalSize } | _] ->
+ MaxOffset + TotalSize
+ end,
+ {Offset, compact(FilesToCompact, %% this never includes the current file
+ State #msstate { current_file = Left })};
+build_index(Left, [File|Files], FilesToCompact,
+ State = #msstate { dir = Dir, file_summary = FileSummary }) ->
+ {ok, Messages} = scan_file_for_valid_messages(Dir, filenum_to_name(File)),
+ {ValidMessages, ValidTotalSize, AllValid} =
+ lists:foldl(
+ fun (Obj = {MsgId, TotalSize, Offset},
+ {VMAcc, VTSAcc, AVAcc}) ->
+ case index_lookup(MsgId, State) of
+ not_found -> {VMAcc, VTSAcc, false};
+ StoreEntry ->
+ ok = index_update(StoreEntry #msg_location {
+ file = File, offset = Offset,
+ total_size = TotalSize },
+ State),
+ {[Obj | VMAcc], VTSAcc + TotalSize, AVAcc}
+ end
+ end, {[], 0, Messages =/= []}, Messages),
+ %% foldl reverses lists, find_contiguous_block_prefix needs
+ %% msgs eldest first, so, ValidMessages is the right way round
+ {ContiguousTop, _} = find_contiguous_block_prefix(ValidMessages),
+ Right = case Files of
+ [] -> undefined;
+ [F|_] -> F
+ end,
+ true = ets:insert_new(FileSummary, #file_summary {
+ file = File, valid_total_size = ValidTotalSize,
+ contiguous_top = ContiguousTop,
+ left = Left, right = Right }),
+ FilesToCompact1 = case AllValid orelse Right =:= undefined of
+ true -> FilesToCompact;
+ false -> [File | FilesToCompact]
+ end,
+ build_index(File, Files, FilesToCompact1, State).
+
+%%----------------------------------------------------------------------------
+%% garbage collection / compaction / aggregation
+%%----------------------------------------------------------------------------
+
+maybe_roll_to_new_file(Offset,
+ State = #msstate { dir = Dir,
+ file_size_limit = FileSizeLimit,
+ current_file_handle = CurHdl,
+ current_file = CurFile,
+ file_summary = FileSummary })
+ when Offset >= FileSizeLimit ->
+ State1 = sync(State),
+ ok = close_file(CurHdl),
+ NextFile = CurFile + 1,
+ {ok, NextHdl} = open_file(Dir, filenum_to_name(NextFile), ?WRITE_MODE),
+ true = ets:update_element(FileSummary, CurFile,
+ {#file_summary.right, NextFile}),
+ true = ets:insert_new(
+ FileSummary, #file_summary {
+ file = NextFile, valid_total_size = 0, contiguous_top = 0,
+ left = CurFile, right = undefined }),
+ State2 = State1 #msstate { current_file_handle = NextHdl,
+ current_file = NextFile },
+ compact([CurFile], State2);
+maybe_roll_to_new_file(_, State) ->
+ State.
+
+compact(Files, State) ->
+ %% smallest number, hence eldest, hence left-most, first
+ SortedFiles = lists:sort(Files),
+ %% foldl reverses, so now youngest/right-most first
+ RemainingFiles =
+ lists:foldl(fun (File, Acc) ->
+ case delete_file_if_empty(File, State) of
+ true -> Acc;
+ false -> [File | Acc]
+ end
+ end, [], SortedFiles),
+ lists:foldl(fun combine_file/2, State, lists:reverse(RemainingFiles)).
+
+%% At this stage, we simply know that the file has had msgs removed
+%% from it. However, we don't know if we need to merge it left (which
+%% is what we would prefer), or merge it right. If we merge left, then
+%% this file is the source, and the left file is the destination. If
+%% we merge right then this file is the destination and the right file
+%% is the source.
+combine_file(File, State = #msstate { file_summary = FileSummary,
+ current_file = CurFile }) ->
+ %% the file we're looking at may no longer exist as it may have
+ %% been deleted within the current GC run
+ case ets:lookup(FileSummary, File) of
+ [] -> State;
+ [FSEntry = #file_summary { left = Left, right = Right }] ->
+ GoRight =
+ fun() ->
+ case Right of
+ undefined -> State;
+ _ when not (CurFile == Right) ->
+ [FSRight] = ets:lookup(FileSummary, Right),
+ {_, State1} = adjust_meta_and_combine(
+ FSEntry, FSRight, State),
+ State1;
+ _ -> State
+ end
+ end,
+ case Left of
+ undefined ->
+ GoRight();
+ _ -> [FSLeft] = ets:lookup(FileSummary, Left),
+ case adjust_meta_and_combine(FSLeft, FSEntry, State) of
+ {true, State1} -> State1;
+ {false, State} -> GoRight()
+ end
+ end
+ end.
+
+adjust_meta_and_combine(
+ LeftObj = #file_summary {
+ file = LeftFile, valid_total_size = LeftValidData, right = RightFile },
+ RightObj = #file_summary {
+ file = RightFile, valid_total_size = RightValidData, left = LeftFile,
+ right = RightRight },
+ State = #msstate { file_size_limit = FileSizeLimit,
+ file_summary = FileSummary }) ->
+ TotalValidData = LeftValidData + RightValidData,
+ if FileSizeLimit >= TotalValidData ->
+ State1 = combine_files(RightObj, LeftObj, State),
+ %% this could fail if RightRight is undefined
+ ets:update_element(FileSummary, RightRight,
+ {#file_summary.left, LeftFile}),
+ true = ets:insert(FileSummary, LeftObj #file_summary {
+ valid_total_size = TotalValidData,
+ contiguous_top = TotalValidData,
+ right = RightRight }),
+ true = ets:delete(FileSummary, RightFile),
+ {true, State1};
+ true -> {false, State}
+ end.
+
+combine_files(#file_summary { file = Source,
+ valid_total_size = SourceValid,
+ left = Destination },
+ #file_summary { file = Destination,
+ valid_total_size = DestinationValid,
+ contiguous_top = DestinationContiguousTop,
+ right = Source },
+ State = #msstate { dir = Dir }) ->
+ State1 = close_handle(Source, close_handle(Destination, State)),
+ SourceName = filenum_to_name(Source),
+ DestinationName = filenum_to_name(Destination),
+ {ok, SourceHdl} = open_file(Dir, SourceName, ?READ_AHEAD_MODE),
+ {ok, DestinationHdl} = open_file(Dir, DestinationName,
+ ?READ_AHEAD_MODE ++ ?WRITE_MODE),
+ ExpectedSize = SourceValid + DestinationValid,
+ %% if DestinationValid =:= DestinationContiguousTop then we don't
+ %% need a tmp file
+ %% if they're not equal, then we need to write out everything past
+ %% the DestinationContiguousTop to a tmp file then truncate,
+ %% copy back in, and then copy over from Source
+ %% otherwise we just truncate straight away and copy over from Source
+ if DestinationContiguousTop =:= DestinationValid ->
+ ok = truncate_and_extend_file(DestinationHdl,
+ DestinationValid, ExpectedSize);
+ true ->
+ Tmp = filename:rootname(DestinationName) ++ ?FILE_EXTENSION_TMP,
+ {ok, TmpHdl} = open_file(Dir, Tmp, ?READ_AHEAD_MODE ++ ?WRITE_MODE),
+ Worklist =
+ lists:dropwhile(
+ fun (#msg_location { offset = Offset })
+ when Offset /= DestinationContiguousTop ->
+ %% it cannot be that Offset ==
+ %% DestinationContiguousTop because if it
+ %% was then DestinationContiguousTop would
+ %% have been extended by TotalSize
+ Offset < DestinationContiguousTop
+ %% Given expected access patterns, I suspect
+ %% that the list should be naturally sorted
+ %% as we require, however, we need to
+ %% enforce it anyway
+ end, index_search_by_file(Destination, State1)),
+ ok = copy_messages(
+ Worklist, DestinationContiguousTop, DestinationValid,
+ DestinationHdl, TmpHdl, Destination, State1),
+ TmpSize = DestinationValid - DestinationContiguousTop,
+ %% so now Tmp contains everything we need to salvage from
+ %% Destination, and MsgLocationDets has been updated to
+ %% reflect compaction of Destination so truncate
+ %% Destination and copy from Tmp back to the end
+ {ok, 0} = file_handle_cache:position(TmpHdl, 0),
+ ok = truncate_and_extend_file(
+ DestinationHdl, DestinationContiguousTop, ExpectedSize),
+ {ok, TmpSize} =
+ file_handle_cache:copy(TmpHdl, DestinationHdl, TmpSize),
+ %% position in DestinationHdl should now be DestinationValid
+ ok = file_handle_cache:sync(DestinationHdl),
+ ok = close_file(TmpHdl),
+ ok = file:delete(form_filename(Dir, Tmp))
+ end,
+ SourceWorkList = index_search_by_file(Source, State1),
+ ok = copy_messages(SourceWorkList, DestinationValid, ExpectedSize,
+ SourceHdl, DestinationHdl, Destination, State1),
+ %% tidy up
+ ok = close_file(SourceHdl),
+ ok = close_file(DestinationHdl),
+ ok = file:delete(form_filename(Dir, SourceName)),
+ State1.
+
+copy_messages(WorkList, InitOffset, FinalOffset, SourceHdl, DestinationHdl,
+ Destination, State) ->
+ {FinalOffset, BlockStart1, BlockEnd1} =
+ lists:foldl(
+ fun (StoreEntry = #msg_location { offset = Offset,
+ total_size = TotalSize },
+ {CurOffset, BlockStart, BlockEnd}) ->
+ %% CurOffset is in the DestinationFile.
+ %% Offset, BlockStart and BlockEnd are in the SourceFile
+ %% update MsgLocationDets to reflect change of file and offset
+ ok = index_update(StoreEntry #msg_location {
+ file = Destination,
+ offset = CurOffset }, State),
+ NextOffset = CurOffset + TotalSize,
+ if BlockStart =:= undefined ->
+ %% base case, called only for the first list elem
+ {NextOffset, Offset, Offset + TotalSize};
+ Offset =:= BlockEnd ->
+ %% extend the current block because the next
+ %% msg follows straight on
+ {NextOffset, BlockStart, BlockEnd + TotalSize};
+ true ->
+ %% found a gap, so actually do the work for
+ %% the previous block
+ BSize = BlockEnd - BlockStart,
+ {ok, BlockStart} =
+ file_handle_cache:position(SourceHdl, BlockStart),
+ {ok, BSize} = file_handle_cache:copy(
+ SourceHdl, DestinationHdl, BSize),
+ {NextOffset, Offset, Offset + TotalSize}
+ end
+ end, {InitOffset, undefined, undefined}, WorkList),
+ %% do the last remaining block
+ BSize1 = BlockEnd1 - BlockStart1,
+ {ok, BlockStart1} = file_handle_cache:position(SourceHdl, BlockStart1),
+ {ok, BSize1} = file_handle_cache:copy(SourceHdl, DestinationHdl, BSize1),
+ ok = file_handle_cache:sync(DestinationHdl),
+ ok.
+
+delete_file_if_empty(File,
+ #msstate { dir = Dir, file_summary = FileSummary }) ->
+ [#file_summary { valid_total_size = ValidData,
+ left = Left, right = Right }] =
+ ets:lookup(FileSummary, File),
+ case ValidData of
+ %% we should NEVER find the current file in here hence right
+ %% should always be a file, not undefined
+ 0 -> case {Left, Right} of
+ {undefined, _} when not is_atom(Right) ->
+ %% the eldest file is empty.
+ true = ets:update_element(
+ FileSummary, Right,
+ {#file_summary.left, undefined});
+ {_, _} when not (is_atom(Right)) ->
+ true = ets:update_element(FileSummary, Right,
+ {#file_summary.left, Left}),
+ true =
+ ets:update_element(FileSummary, Left,
+ {#file_summary.right, Right})
+ end,
+ true = ets:delete(FileSummary, File),
+ ok = file:delete(form_filename(Dir, filenum_to_name(File))),
+ true;
+ _ -> false
+ end.
diff --git a/src/rabbit_persister.erl b/src/rabbit_persister.erl
deleted file mode 100644
index d0d60ddf3d..0000000000
--- a/src/rabbit_persister.erl
+++ /dev/null
@@ -1,523 +0,0 @@
-%% The contents of this file are subject to the Mozilla Public License
-%% Version 1.1 (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.mozilla.org/MPL/
-%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
-%% License for the specific language governing rights and limitations
-%% under the License.
-%%
-%% The Original Code is RabbitMQ.
-%%
-%% The Initial Developers of the Original Code are LShift Ltd,
-%% Cohesive Financial Technologies LLC, and Rabbit Technologies Ltd.
-%%
-%% Portions created before 22-Nov-2008 00:00:00 GMT by LShift Ltd,
-%% Cohesive Financial Technologies LLC, or Rabbit Technologies Ltd
-%% are Copyright (C) 2007-2008 LShift Ltd, Cohesive Financial
-%% Technologies LLC, and Rabbit Technologies Ltd.
-%%
-%% Portions created by LShift Ltd are Copyright (C) 2007-2009 LShift
-%% Ltd. Portions created by Cohesive Financial Technologies LLC are
-%% Copyright (C) 2007-2009 Cohesive Financial Technologies
-%% LLC. Portions created by Rabbit Technologies Ltd are Copyright
-%% (C) 2007-2009 Rabbit Technologies Ltd.
-%%
-%% All Rights Reserved.
-%%
-%% Contributor(s): ______________________________________.
-%%
-
--module(rabbit_persister).
-
--behaviour(gen_server).
-
--export([start_link/0]).
-
--export([init/1, handle_call/3, handle_cast/2, handle_info/2,
- terminate/2, code_change/3]).
-
--export([transaction/1, extend_transaction/2, dirty_work/1,
- commit_transaction/1, rollback_transaction/1,
- force_snapshot/0, serial/0]).
-
--include("rabbit.hrl").
-
--define(SERVER, ?MODULE).
-
--define(LOG_BUNDLE_DELAY, 5).
--define(COMPLETE_BUNDLE_DELAY, 2).
-
--define(HIBERNATE_AFTER, 10000).
-
--define(MAX_WRAP_ENTRIES, 500).
-
--define(PERSISTER_LOG_FORMAT_VERSION, {2, 4}).
-
--record(pstate, {log_handle, entry_count, deadline,
- pending_logs, pending_replies,
- snapshot}).
-
-%% two tables for efficient persistency
-%% one maps a key to a message
-%% the other maps a key to one or more queues.
-%% The aim is to reduce the overload of storing a message multiple times
-%% when it appears in several queues.
--record(psnapshot, {serial, transactions, messages, queues}).
-
-%%----------------------------------------------------------------------------
-
--ifdef(use_specs).
-
--type(qmsg() :: {amqqueue(), pkey()}).
--type(work_item() ::
- {publish, message(), qmsg()} |
- {deliver, qmsg()} |
- {ack, qmsg()}).
-
--spec(start_link/0 :: () -> {'ok', pid()} | 'ignore' | {'error', any()}).
--spec(transaction/1 :: ([work_item()]) -> 'ok').
--spec(extend_transaction/2 :: (txn(), [work_item()]) -> 'ok').
--spec(dirty_work/1 :: ([work_item()]) -> 'ok').
--spec(commit_transaction/1 :: (txn()) -> 'ok').
--spec(rollback_transaction/1 :: (txn()) -> 'ok').
--spec(force_snapshot/0 :: () -> 'ok').
--spec(serial/0 :: () -> non_neg_integer()).
-
--endif.
-
-%%----------------------------------------------------------------------------
-
-start_link() ->
- gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
-
-transaction(MessageList) ->
- ?LOGDEBUG("transaction ~p~n", [MessageList]),
- TxnKey = rabbit_guid:guid(),
- gen_server:call(?SERVER, {transaction, TxnKey, MessageList}, infinity).
-
-extend_transaction(TxnKey, MessageList) ->
- ?LOGDEBUG("extend_transaction ~p ~p~n", [TxnKey, MessageList]),
- gen_server:cast(?SERVER, {extend_transaction, TxnKey, MessageList}).
-
-dirty_work(MessageList) ->
- ?LOGDEBUG("dirty_work ~p~n", [MessageList]),
- gen_server:cast(?SERVER, {dirty_work, MessageList}).
-
-commit_transaction(TxnKey) ->
- ?LOGDEBUG("commit_transaction ~p~n", [TxnKey]),
- gen_server:call(?SERVER, {commit_transaction, TxnKey}, infinity).
-
-rollback_transaction(TxnKey) ->
- ?LOGDEBUG("rollback_transaction ~p~n", [TxnKey]),
- gen_server:cast(?SERVER, {rollback_transaction, TxnKey}).
-
-force_snapshot() ->
- gen_server:call(?SERVER, force_snapshot, infinity).
-
-serial() ->
- gen_server:call(?SERVER, serial, infinity).
-
-%%--------------------------------------------------------------------
-
-init(_Args) ->
- process_flag(trap_exit, true),
- FileName = base_filename(),
- ok = filelib:ensure_dir(FileName),
- Snapshot = #psnapshot{serial = 0,
- transactions = dict:new(),
- messages = ets:new(messages, []),
- queues = ets:new(queues, [])},
- LogHandle =
- case disk_log:open([{name, rabbit_persister},
- {head, current_snapshot(Snapshot)},
- {file, FileName}]) of
- {ok, LH} -> LH;
- {repaired, LH, {recovered, Recovered}, {badbytes, Bad}} ->
- WarningFun = if
- Bad > 0 -> fun rabbit_log:warning/2;
- true -> fun rabbit_log:info/2
- end,
- WarningFun("Repaired persister log - ~p recovered, ~p bad~n",
- [Recovered, Bad]),
- LH
- end,
- {Res, LoadedSnapshot} = internal_load_snapshot(LogHandle, Snapshot),
- NewSnapshot = LoadedSnapshot#psnapshot{
- serial = LoadedSnapshot#psnapshot.serial + 1},
- case Res of
- ok ->
- ok = take_snapshot(LogHandle, NewSnapshot);
- {error, Reason} ->
- rabbit_log:error("Failed to load persister log: ~p~n", [Reason]),
- ok = take_snapshot_and_save_old(LogHandle, NewSnapshot)
- end,
- State = #pstate{log_handle = LogHandle,
- entry_count = 0,
- deadline = infinity,
- pending_logs = [],
- pending_replies = [],
- snapshot = NewSnapshot},
- {ok, State}.
-
-handle_call({transaction, Key, MessageList}, From, State) ->
- NewState = internal_extend(Key, MessageList, State),
- do_noreply(internal_commit(From, Key, NewState));
-handle_call({commit_transaction, TxnKey}, From, State) ->
- do_noreply(internal_commit(From, TxnKey, State));
-handle_call(force_snapshot, _From, State) ->
- do_reply(ok, flush(true, State));
-handle_call(serial, _From,
- State = #pstate{snapshot = #psnapshot{serial = Serial}}) ->
- do_reply(Serial, State);
-handle_call(_Request, _From, State) ->
- {noreply, State}.
-
-handle_cast({rollback_transaction, TxnKey}, State) ->
- do_noreply(internal_rollback(TxnKey, State));
-handle_cast({dirty_work, MessageList}, State) ->
- do_noreply(internal_dirty_work(MessageList, State));
-handle_cast({extend_transaction, TxnKey, MessageList}, State) ->
- do_noreply(internal_extend(TxnKey, MessageList, State));
-handle_cast(_Msg, State) ->
- {noreply, State}.
-
-handle_info(timeout, State = #pstate{deadline = infinity}) ->
- State1 = flush(true, State),
- %% TODO: Once we drop support for R11B-5, we can change this to
- %% {noreply, State1, hibernate};
- proc_lib:hibernate(gen_server2, enter_loop, [?MODULE, [], State1]);
-handle_info(timeout, State) ->
- do_noreply(flush(State));
-handle_info(_Info, State) ->
- {noreply, State}.
-
-terminate(_Reason, State = #pstate{log_handle = LogHandle}) ->
- flush(State),
- disk_log:close(LogHandle),
- ok.
-
-code_change(_OldVsn, State, _Extra) ->
- {ok, flush(State)}.
-
-%%--------------------------------------------------------------------
-
-internal_extend(Key, MessageList, State) ->
- log_work(fun (ML) -> {extend_transaction, Key, ML} end,
- MessageList, State).
-
-internal_dirty_work(MessageList, State) ->
- log_work(fun (ML) -> {dirty_work, ML} end,
- MessageList, State).
-
-internal_commit(From, Key, State = #pstate{snapshot = Snapshot}) ->
- Unit = {commit_transaction, Key},
- NewSnapshot = internal_integrate1(Unit, Snapshot),
- complete(From, Unit, State#pstate{snapshot = NewSnapshot}).
-
-internal_rollback(Key, State = #pstate{snapshot = Snapshot}) ->
- Unit = {rollback_transaction, Key},
- NewSnapshot = internal_integrate1(Unit, Snapshot),
- log(State#pstate{snapshot = NewSnapshot}, Unit).
-
-complete(From, Item, State = #pstate{deadline = ExistingDeadline,
- pending_logs = Logs,
- pending_replies = Waiting}) ->
- State#pstate{deadline = compute_deadline(
- ?COMPLETE_BUNDLE_DELAY, ExistingDeadline),
- pending_logs = [Item | Logs],
- pending_replies = [From | Waiting]}.
-
-%% This is made to limit disk usage by writing messages only once onto
-%% disk. We keep a table associating pkeys to messages, and provided
-%% the list of messages to output is left to right, we can guarantee
-%% that pkeys will be a backreference to a message in memory when a
-%% "tied" is met.
-log_work(CreateWorkUnit, MessageList,
- State = #pstate{
- snapshot = Snapshot = #psnapshot{
- messages = Messages}}) ->
- Unit = CreateWorkUnit(
- rabbit_misc:map_in_order(
- fun(M = {publish, Message, QK = {_QName, PKey}}) ->
- case ets:lookup(Messages, PKey) of
- [_] -> {tied, QK};
- [] -> ets:insert(Messages, {PKey, Message}),
- M
- end;
- (M) -> M
- end,
- MessageList)),
- NewSnapshot = internal_integrate1(Unit, Snapshot),
- log(State#pstate{snapshot = NewSnapshot}, Unit).
-
-log(State = #pstate{deadline = ExistingDeadline, pending_logs = Logs},
- Message) ->
- State#pstate{deadline = compute_deadline(?LOG_BUNDLE_DELAY,
- ExistingDeadline),
- pending_logs = [Message | Logs]}.
-
-base_filename() ->
- rabbit_mnesia:dir() ++ "/rabbit_persister.LOG".
-
-take_snapshot(LogHandle, OldFileName, Snapshot) ->
- ok = disk_log:sync(LogHandle),
- %% current_snapshot is the Head (ie. first thing logged)
- ok = disk_log:reopen(LogHandle, OldFileName, current_snapshot(Snapshot)).
-
-take_snapshot(LogHandle, Snapshot) ->
- OldFileName = lists:flatten(base_filename() ++ ".previous"),
- file:delete(OldFileName),
- rabbit_log:info("Rolling persister log to ~p~n", [OldFileName]),
- ok = take_snapshot(LogHandle, OldFileName, Snapshot).
-
-take_snapshot_and_save_old(LogHandle, Snapshot) ->
- {MegaSecs, Secs, MicroSecs} = erlang:now(),
- Timestamp = MegaSecs * 1000000 + Secs * 1000 + MicroSecs,
- OldFileName = lists:flatten(io_lib:format("~s.saved.~p",
- [base_filename(), Timestamp])),
- rabbit_log:info("Saving persister log in ~p~n", [OldFileName]),
- ok = take_snapshot(LogHandle, OldFileName, Snapshot).
-
-maybe_take_snapshot(Force, State = #pstate{entry_count = EntryCount,
- log_handle = LH,
- snapshot = Snapshot})
- when Force orelse EntryCount >= ?MAX_WRAP_ENTRIES ->
- ok = take_snapshot(LH, Snapshot),
- State#pstate{entry_count = 0};
-maybe_take_snapshot(_Force, State) ->
- State.
-
-later_ms(DeltaMilliSec) ->
- {MegaSec, Sec, MicroSec} = now(),
- %% Note: not normalised. Unimportant for this application.
- {MegaSec, Sec, MicroSec + (DeltaMilliSec * 1000)}.
-
-%% Result = B - A, more or less
-time_diff({B1, B2, B3}, {A1, A2, A3}) ->
- (B1 - A1) * 1000000 + (B2 - A2) + (B3 - A3) / 1000000.0 .
-
-compute_deadline(TimerDelay, infinity) ->
- later_ms(TimerDelay);
-compute_deadline(_TimerDelay, ExistingDeadline) ->
- ExistingDeadline.
-
-compute_timeout(infinity) ->
- ?HIBERNATE_AFTER;
-compute_timeout(Deadline) ->
- DeltaMilliSec = time_diff(Deadline, now()) * 1000.0,
- if
- DeltaMilliSec =< 1 ->
- 0;
- true ->
- round(DeltaMilliSec)
- end.
-
-do_noreply(State = #pstate{deadline = Deadline}) ->
- {noreply, State, compute_timeout(Deadline)}.
-
-do_reply(Reply, State = #pstate{deadline = Deadline}) ->
- {reply, Reply, State, compute_timeout(Deadline)}.
-
-flush(State) -> flush(false, State).
-
-flush(ForceSnapshot, State = #pstate{pending_logs = PendingLogs,
- pending_replies = Waiting,
- log_handle = LogHandle}) ->
- State1 = if PendingLogs /= [] ->
- disk_log:alog(LogHandle, lists:reverse(PendingLogs)),
- State#pstate{entry_count = State#pstate.entry_count + 1};
- true ->
- State
- end,
- State2 = maybe_take_snapshot(ForceSnapshot, State1),
- if Waiting /= [] ->
- ok = disk_log:sync(LogHandle),
- lists:foreach(fun (From) -> gen_server:reply(From, ok) end,
- Waiting);
- true ->
- ok
- end,
- State2#pstate{deadline = infinity,
- pending_logs = [],
- pending_replies = []}.
-
-current_snapshot(_Snapshot = #psnapshot{serial = Serial,
- transactions= Ts,
- messages = Messages,
- queues = Queues}) ->
- %% Avoid infinite growth of the table by removing messages not
- %% bound to a queue anymore
- prune_table(Messages, ets:foldl(
- fun ({{_QName, PKey}, _Delivered}, S) ->
- sets:add_element(PKey, S)
- end, sets:new(), Queues)),
- InnerSnapshot = {{serial, Serial},
- {txns, Ts},
- {messages, ets:tab2list(Messages)},
- {queues, ets:tab2list(Queues)}},
- ?LOGDEBUG("Inner snapshot: ~p~n", [InnerSnapshot]),
- {persist_snapshot, {vsn, ?PERSISTER_LOG_FORMAT_VERSION},
- term_to_binary(InnerSnapshot)}.
-
-prune_table(Tab, Keys) ->
- true = ets:safe_fixtable(Tab, true),
- ok = prune_table(Tab, Keys, ets:first(Tab)),
- true = ets:safe_fixtable(Tab, false).
-
-prune_table(_Tab, _Keys, '$end_of_table') -> ok;
-prune_table(Tab, Keys, Key) ->
- case sets:is_element(Key, Keys) of
- true -> ok;
- false -> ets:delete(Tab, Key)
- end,
- prune_table(Tab, Keys, ets:next(Tab, Key)).
-
-internal_load_snapshot(LogHandle,
- Snapshot = #psnapshot{messages = Messages,
- queues = Queues}) ->
- {K, [Loaded_Snapshot | Items]} = disk_log:chunk(LogHandle, start),
- case check_version(Loaded_Snapshot) of
- {ok, StateBin} ->
- {{serial, Serial}, {txns, Ts}, {messages, Ms}, {queues, Qs}} =
- binary_to_term(StateBin),
- true = ets:insert(Messages, Ms),
- true = ets:insert(Queues, Qs),
- Snapshot1 = replay(Items, LogHandle, K,
- Snapshot#psnapshot{
- serial = Serial,
- transactions = Ts}),
- Snapshot2 = requeue_messages(Snapshot1),
- %% uncompleted transactions are discarded - this is TRTTD
- %% since we only get into this code on node restart, so
- %% any uncompleted transactions will have been aborted.
- {ok, Snapshot2#psnapshot{transactions = dict:new()}};
- {error, Reason} -> {{error, Reason}, Snapshot}
- end.
-
-check_version({persist_snapshot, {vsn, ?PERSISTER_LOG_FORMAT_VERSION},
- StateBin}) ->
- {ok, StateBin};
-check_version({persist_snapshot, {vsn, Vsn}, _StateBin}) ->
- {error, {unsupported_persister_log_format, Vsn}};
-check_version(_Other) ->
- {error, unrecognised_persister_log_format}.
-
-requeue_messages(Snapshot = #psnapshot{messages = Messages,
- queues = Queues}) ->
- Work = ets:foldl(fun accumulate_requeues/2, dict:new(), Queues),
- %% unstable parallel map, because order doesn't matter
- L = lists:append(
- rabbit_misc:upmap(
- %% we do as much work as possible in spawned worker
- %% processes, but we need to make sure the ets:inserts are
- %% performed in self()
- fun ({QName, Requeues}) ->
- requeue(QName, Requeues, Messages)
- end, dict:to_list(Work))),
- NewMessages = [{K, M} || {{_Q, K}, M, _D} <- L],
- NewQueues = [{QK, D} || {QK, _M, D} <- L],
- ets:delete_all_objects(Messages),
- ets:delete_all_objects(Queues),
- true = ets:insert(Messages, NewMessages),
- true = ets:insert(Queues, NewQueues),
- %% contains the mutated messages and queues tables
- Snapshot.
-
-accumulate_requeues({{QName, PKey}, Delivered}, Acc) ->
- Requeue = {PKey, Delivered},
- dict:update(QName,
- fun (Requeues) -> [Requeue | Requeues] end,
- [Requeue],
- Acc).
-
-requeue(QName, Requeues, Messages) ->
- case rabbit_amqqueue:lookup(QName) of
- {ok, #amqqueue{pid = QPid}} ->
- RequeueMessages =
- [{{QName, PKey}, Message, Delivered} ||
- {PKey, Delivered} <- Requeues,
- {_, Message} <- ets:lookup(Messages, PKey)],
- rabbit_amqqueue:redeliver(
- QPid,
- %% Messages published by the same process receive
- %% persistence keys that are monotonically
- %% increasing. Since message ordering is defined on a
- %% per-channel basis, and channels are bound to specific
- %% processes, sorting the list does provide the correct
- %% ordering properties.
- [{Message, Delivered} || {_, Message, Delivered} <-
- lists:sort(RequeueMessages)]),
- RequeueMessages;
- {error, not_found} ->
- []
- end.
-
-replay([], LogHandle, K, Snapshot) ->
- case disk_log:chunk(LogHandle, K) of
- {K1, Items} ->
- replay(Items, LogHandle, K1, Snapshot);
- {K1, Items, Badbytes} ->
- rabbit_log:warning("~p bad bytes recovering persister log~n",
- [Badbytes]),
- replay(Items, LogHandle, K1, Snapshot);
- eof -> Snapshot
- end;
-replay([Item | Items], LogHandle, K, Snapshot) ->
- NewSnapshot = internal_integrate_messages(Item, Snapshot),
- replay(Items, LogHandle, K, NewSnapshot).
-
-internal_integrate_messages(Items, Snapshot) ->
- lists:foldl(fun (Item, Snap) -> internal_integrate1(Item, Snap) end,
- Snapshot, Items).
-
-internal_integrate1({extend_transaction, Key, MessageList},
- Snapshot = #psnapshot {transactions = Transactions}) ->
- NewTransactions =
- dict:update(Key,
- fun (MessageLists) -> [MessageList | MessageLists] end,
- [MessageList],
- Transactions),
- Snapshot#psnapshot{transactions = NewTransactions};
-internal_integrate1({rollback_transaction, Key},
- Snapshot = #psnapshot{transactions = Transactions}) ->
- Snapshot#psnapshot{transactions = dict:erase(Key, Transactions)};
-internal_integrate1({commit_transaction, Key},
- Snapshot = #psnapshot{transactions = Transactions,
- messages = Messages,
- queues = Queues}) ->
- case dict:find(Key, Transactions) of
- {ok, MessageLists} ->
- ?LOGDEBUG("persist committing txn ~p~n", [Key]),
- lists:foreach(fun (ML) -> perform_work(ML, Messages, Queues) end,
- lists:reverse(MessageLists)),
- Snapshot#psnapshot{transactions = dict:erase(Key, Transactions)};
- error ->
- Snapshot
- end;
-internal_integrate1({dirty_work, MessageList},
- Snapshot = #psnapshot {messages = Messages,
- queues = Queues}) ->
- perform_work(MessageList, Messages, Queues),
- Snapshot.
-
-perform_work(MessageList, Messages, Queues) ->
- lists:foreach(
- fun (Item) -> perform_work_item(Item, Messages, Queues) end,
- MessageList).
-
-perform_work_item({publish, Message, QK = {_QName, PKey}}, Messages, Queues) ->
- ets:insert(Messages, {PKey, Message}),
- ets:insert(Queues, {QK, false});
-
-perform_work_item({tied, QK}, _Messages, Queues) ->
- ets:insert(Queues, {QK, false});
-
-perform_work_item({deliver, QK}, _Messages, Queues) ->
- %% from R12B-2 onward we could use ets:update_element/3 here
- ets:delete(Queues, QK),
- ets:insert(Queues, {QK, true});
-
-perform_work_item({ack, QK}, _Messages, Queues) ->
- ets:delete(Queues, QK).
diff --git a/src/rabbit_queue_index.erl b/src/rabbit_queue_index.erl
new file mode 100644
index 0000000000..a198ba51ee
--- /dev/null
+++ b/src/rabbit_queue_index.erl
@@ -0,0 +1,862 @@
+%% The contents of this file are subject to the Mozilla Public License
+%% Version 1.1 (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.mozilla.org/MPL/
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+%% License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% The Original Code is RabbitMQ.
+%%
+%% The Initial Developers of the Original Code are LShift Ltd,
+%% Cohesive Financial Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created before 22-Nov-2008 00:00:00 GMT by LShift Ltd,
+%% Cohesive Financial Technologies LLC, or Rabbit Technologies Ltd
+%% are Copyright (C) 2007-2008 LShift Ltd, Cohesive Financial
+%% Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created by LShift Ltd are Copyright (C) 2007-2009 LShift
+%% Ltd. Portions created by Cohesive Financial Technologies LLC are
+%% Copyright (C) 2007-2009 Cohesive Financial Technologies
+%% LLC. Portions created by Rabbit Technologies Ltd are Copyright
+%% (C) 2007-2009 Rabbit Technologies Ltd.
+%%
+%% All Rights Reserved.
+%%
+%% Contributor(s): ______________________________________.
+%%
+
+-module(rabbit_queue_index).
+
+-export([init/1, terminate/1, terminate_and_erase/1, write_published/4,
+ write_delivered/2, write_acks/2, sync_seq_ids/3, can_flush_journal/1,
+ flush_journal/1, read_segment_entries/2, next_segment_boundary/1,
+ segment_size/0, find_lowest_seq_id_seg_and_next_seq_id/1,
+ start_msg_store/1]).
+
+%%----------------------------------------------------------------------------
+%% The queue disk index
+%%
+%% The queue disk index operates over a journal, and a number of
+%% segment files. Each segment is the same size, both in max number of
+%% entries, and max file size, owing to fixed sized records.
+%%
+%% Publishes are written directly to the segment files. The segment is
+%% found by dividing the sequence id by the the max number of entries
+%% per segment. Only the relative sequence within the segment is
+%% recorded as the sequence id within a segment file (i.e. sequence id
+%% modulo max number of entries per segment). This is keeps entries
+%% as small as possible. Publishes are only ever going to be received
+%% in contiguous ascending order.
+%%
+%% Acks and deliveries are written to a bounded journal and are also
+%% held in memory, each in a dict with the segment as the key. Again,
+%% the records are fixed size: the entire sequence id is written and
+%% is limited to a 63-bit unsigned integer. The remaining bit
+%% indicates whether the journal entry is for a delivery or an
+%% ack. When the journal gets too big, or flush_journal is called, the
+%% journal is (possibly incrementally) flushed out to the segment
+%% files. As acks and delivery notes can be received in any order
+%% (this is not obvious for deliveries, but consider what happens when
+%% eg msgs are *re*queued - you'll publish and then mark the msgs
+%% delivered immediately, which may be out of order), this journal
+%% reduces seeking, and batches writes to the segment files, keeping
+%% performance high.
+%%
+%% On startup, the journal is read along with all the segment files,
+%% and the journal is fully flushed out to the segment files. Care is
+%% taken to ensure that no message can be delivered or ack'd twice.
+%%
+%%----------------------------------------------------------------------------
+
+-define(CLEAN_FILENAME, "clean.dot").
+
+-define(MAX_ACK_JOURNAL_ENTRY_COUNT, 32768).
+-define(ACK_JOURNAL_FILENAME, "journal.jif").
+
+-define(DEL_BIT, 0).
+-define(ACK_BIT, 1).
+-define(SEQ_BYTES, 8).
+-define(SEQ_BITS, ((?SEQ_BYTES * 8) - 1)).
+-define(SEGMENT_EXTENSION, ".idx").
+
+-define(REL_SEQ_BITS, 14).
+-define(REL_SEQ_BITS_BYTE_ALIGNED, (?REL_SEQ_BITS + 8 - (?REL_SEQ_BITS rem 8))).
+-define(SEGMENT_ENTRIES_COUNT, 16384). %% trunc(math:pow(2,?REL_SEQ_BITS))).
+
+%% seq only is binary 00 followed by 14 bits of rel seq id
+%% (range: 0 - 16383)
+-define(REL_SEQ_ONLY_PREFIX, 00).
+-define(REL_SEQ_ONLY_PREFIX_BITS, 2).
+-define(REL_SEQ_ONLY_ENTRY_LENGTH_BYTES, 2).
+
+%% publish record is binary 1 followed by a bit for is_persistent,
+%% then 14 bits of rel seq id, and 128 bits of md5sum msg id
+-define(PUBLISH_PREFIX, 1).
+-define(PUBLISH_PREFIX_BITS, 1).
+
+-define(MSG_ID_BYTES, 16). %% md5sum is 128 bit or 16 bytes
+-define(MSG_ID_BITS, (?MSG_ID_BYTES * 8)).
+%% 16 bytes for md5sum + 2 for seq, bits and prefix
+-define(PUBLISH_RECORD_LENGTH_BYTES, ?MSG_ID_BYTES + 2).
+
+%% 1 publish, 1 deliver, 1 ack per msg
+-define(SEGMENT_TOTAL_SIZE, ?SEGMENT_ENTRIES_COUNT *
+ (?PUBLISH_RECORD_LENGTH_BYTES +
+ (2 * ?REL_SEQ_ONLY_ENTRY_LENGTH_BYTES))).
+
+%%----------------------------------------------------------------------------
+
+-record(qistate,
+ { dir,
+ seg_num_handles,
+ journal_count,
+ journal_ack_dict,
+ journal_del_dict,
+ seg_ack_counts,
+ publish_handle,
+ partial_segments
+ }).
+
+-include("rabbit.hrl").
+
+%%----------------------------------------------------------------------------
+
+-ifdef(use_specs).
+
+-type(hdl() :: ('undefined' | any())).
+-type(msg_id() :: binary()).
+-type(seq_id() :: integer()).
+-type(hdl_and_count() :: ('undefined' |
+ {non_neg_integer(), hdl(), non_neg_integer()})).
+-type(qistate() :: #qistate { dir :: file_path(),
+ seg_num_handles :: dict(),
+ journal_count :: integer(),
+ journal_ack_dict :: dict(),
+ journal_del_dict :: dict(),
+ seg_ack_counts :: dict(),
+ publish_handle :: hdl_and_count(),
+ partial_segments :: dict()
+ }).
+
+-spec(init/1 :: (queue_name()) -> {non_neg_integer(), qistate()}).
+-spec(terminate/1 :: (qistate()) -> qistate()).
+-spec(terminate_and_erase/1 :: (qistate()) -> qistate()).
+-spec(write_published/4 :: (msg_id(), seq_id(), boolean(), qistate())
+ -> qistate()).
+-spec(write_delivered/2 :: (seq_id(), qistate()) -> qistate()).
+-spec(write_acks/2 :: ([seq_id()], qistate()) -> qistate()).
+-spec(sync_seq_ids/3 :: ([seq_id()], boolean(), qistate()) -> qistate()).
+-spec(can_flush_journal/1 :: (qistate()) -> boolean()).
+-spec(flush_journal/1 :: (qistate()) -> qistate()).
+-spec(read_segment_entries/2 :: (seq_id(), qistate()) ->
+ {[{msg_id(), seq_id(), boolean(), boolean()}], qistate()}).
+-spec(next_segment_boundary/1 :: (seq_id()) -> seq_id()).
+-spec(segment_size/0 :: () -> non_neg_integer()).
+-spec(find_lowest_seq_id_seg_and_next_seq_id/1 :: (qistate()) ->
+ {non_neg_integer(), non_neg_integer(), qistate()}).
+-spec(start_msg_store/1 :: ([amqqueue()]) -> 'ok').
+
+-endif.
+
+
+%%----------------------------------------------------------------------------
+%% Public API
+%%----------------------------------------------------------------------------
+
+init(Name) ->
+ State = blank_state(Name),
+ {TotalMsgCount, State1} = read_and_prune_segments(State),
+ scatter_journal(TotalMsgCount, State1).
+
+terminate(State = #qistate { seg_num_handles = SegHdls }) ->
+ case 0 == dict:size(SegHdls) of
+ true -> State;
+ false -> State1 = #qistate { dir = Dir } = close_all_handles(State),
+ store_clean_shutdown(Dir),
+ State1 #qistate { publish_handle = undefined }
+ end.
+
+terminate_and_erase(State) ->
+ State1 = terminate(State),
+ ok = delete_queue_directory(State1 #qistate.dir),
+ State1.
+
+write_published(MsgId, SeqId, IsPersistent, State)
+ when is_binary(MsgId) ->
+ ?MSG_ID_BYTES = size(MsgId),
+ {SegNum, RelSeq} = seq_id_to_seg_and_rel_seq_id(SeqId),
+ {Hdl, State1} = get_pub_handle(SegNum, State),
+ ok = file_handle_cache:append(Hdl,
+ <<?PUBLISH_PREFIX:?PUBLISH_PREFIX_BITS,
+ (bool_to_int(IsPersistent)):1,
+ RelSeq:?REL_SEQ_BITS, MsgId/binary>>),
+ State1.
+
+write_delivered(SeqId, State = #qistate { journal_del_dict = JDelDict }) ->
+ {JDelDict1, State1} =
+ write_to_journal([<<?DEL_BIT:1, SeqId:?SEQ_BITS>>],
+ [SeqId], JDelDict, State),
+ maybe_full_flush(State1 #qistate { journal_del_dict = JDelDict1 }).
+
+write_acks(SeqIds, State = #qistate { journal_ack_dict = JAckDict }) ->
+ {JAckDict1, State1} =
+ write_to_journal([<<?ACK_BIT:1, SeqId:?SEQ_BITS>> || SeqId <- SeqIds],
+ SeqIds, JAckDict, State),
+ maybe_full_flush(State1 #qistate { journal_ack_dict = JAckDict1 }).
+
+sync_seq_ids(SeqIds, SyncAckJournal, State) ->
+ State1 = case SyncAckJournal of
+ true -> {Hdl, State2} = get_journal_handle(State),
+ ok = file_handle_cache:sync(Hdl),
+ State2;
+ false -> State
+ end,
+ SegNumsSet =
+ lists:foldl(
+ fun (SeqId, Set) ->
+ {SegNum, _RelSeq} = seq_id_to_seg_and_rel_seq_id(SeqId),
+ sets:add_element(SegNum, Set)
+ end, sets:new(), SeqIds),
+ sets:fold(
+ fun (SegNum, StateN) ->
+ {Hdl1, StateM} = get_seg_handle(SegNum, StateN),
+ ok = file_handle_cache:sync(Hdl1),
+ StateM
+ end, State1, SegNumsSet).
+
+can_flush_journal(#qistate { journal_count = 0 }) ->
+ false;
+can_flush_journal(_) ->
+ true.
+
+flush_journal(State = #qistate { journal_count = 0 }) ->
+ State;
+flush_journal(State = #qistate { journal_ack_dict = JAckDict,
+ journal_del_dict = JDelDict,
+ journal_count = JCount }) ->
+ SegNum = case dict:fetch_keys(JAckDict) of
+ [] -> hd(dict:fetch_keys(JDelDict));
+ [N|_] -> N
+ end,
+ Dels = seg_entries_from_dict(SegNum, JDelDict),
+ Acks = seg_entries_from_dict(SegNum, JAckDict),
+ State1 = append_dels_to_segment(SegNum, Dels, State),
+ State2 = append_acks_to_segment(SegNum, Acks, State1),
+ JCount1 = JCount - length(Dels) - length(Acks),
+ State3 = State2 #qistate { journal_del_dict = dict:erase(SegNum, JDelDict),
+ journal_ack_dict = dict:erase(SegNum, JAckDict),
+ journal_count = JCount1 },
+ if
+ JCount1 == 0 ->
+ {Hdl, State4} = get_journal_handle(State3),
+ {ok, 0} = file_handle_cache:position(Hdl, bof),
+ ok = file_handle_cache:truncate(Hdl),
+ ok = file_handle_cache:sync(Hdl),
+ State4;
+ JCount1 > ?MAX_ACK_JOURNAL_ENTRY_COUNT ->
+ flush_journal(State3);
+ true ->
+ State3
+ end.
+
+read_segment_entries(InitSeqId, State) ->
+ {SegNum, 0} = seq_id_to_seg_and_rel_seq_id(InitSeqId),
+ {SDict, _PubCount, _AckCount, _HighRelSeq, State1} =
+ load_segment(SegNum, State),
+ %% deliberately sort the list desc, because foldl will reverse it
+ RelSeqs = rev_sort(dict:fetch_keys(SDict)),
+ {lists:foldl(fun (RelSeq, Acc) ->
+ {MsgId, IsDelivered, IsPersistent} =
+ dict:fetch(RelSeq, SDict),
+ [ {MsgId, reconstruct_seq_id(SegNum, RelSeq),
+ IsPersistent, IsDelivered} | Acc]
+ end, [], RelSeqs),
+ State1}.
+
+next_segment_boundary(SeqId) ->
+ {SegNum, _RelSeq} = seq_id_to_seg_and_rel_seq_id(SeqId),
+ reconstruct_seq_id(SegNum + 1, 0).
+
+segment_size() ->
+ ?SEGMENT_ENTRIES_COUNT.
+
+find_lowest_seq_id_seg_and_next_seq_id(State = #qistate { dir = Dir }) ->
+ SegNums = all_segment_nums(Dir),
+ %% We don't want the lowest seq_id, merely the seq_id of the start
+ %% of the lowest segment. That seq_id may not actually exist, but
+ %% that's fine. The important thing is that the segment exists and
+ %% the seq_id reported is on a segment boundary.
+
+ %% SegNums is sorted, ascending.
+ LowSeqIdSeg =
+ case SegNums of
+ [] -> 0;
+ [MinSegNum|_] -> reconstruct_seq_id(MinSegNum, 0)
+ end,
+ {NextSeqId, State1} =
+ case SegNums of
+ [] -> {0, State};
+ _ -> MaxSegNum = lists:last(SegNums),
+ {_SDict, PubCount, _AckCount, HighRelSeq, State2} =
+ load_segment(MaxSegNum, State),
+ NextSeqId1 = reconstruct_seq_id(MaxSegNum, HighRelSeq),
+ NextSeqId2 = case PubCount of
+ 0 -> NextSeqId1;
+ _ -> NextSeqId1 + 1
+ end,
+ {NextSeqId2, State2}
+ end,
+ {LowSeqIdSeg, NextSeqId, State1}.
+
+start_msg_store(DurableQueues) ->
+ DurableDict =
+ dict:from_list([ {queue_name_to_dir_name(Queue #amqqueue.name),
+ Queue #amqqueue.name} || Queue <- DurableQueues ]),
+ QueuesDir = queues_dir(),
+ Directories = case file:list_dir(QueuesDir) of
+ {ok, Entries} ->
+ [ Entry || Entry <- Entries,
+ filelib:is_dir(
+ filename:join(QueuesDir, Entry)) ];
+ {error, enoent} ->
+ []
+ end,
+ DurableDirectories = sets:from_list(dict:fetch_keys(DurableDict)),
+ {DurableQueueNames, TransientDirs} =
+ lists:foldl(
+ fun (QueueDir, {DurableAcc, TransientAcc}) ->
+ case sets:is_element(QueueDir, DurableDirectories) of
+ true ->
+ {[dict:fetch(QueueDir, DurableDict) | DurableAcc],
+ TransientAcc};
+ false ->
+ {DurableAcc, [QueueDir | TransientAcc]}
+ end
+ end, {[], []}, Directories),
+ MsgStoreDir = filename:join(rabbit_mnesia:dir(), "msg_store"),
+ ok = rabbit:start_child(rabbit_msg_store, [MsgStoreDir,
+ fun queue_index_walker/1,
+ DurableQueueNames]),
+ lists:foreach(fun (DirName) ->
+ Dir = filename:join(queues_dir(), DirName),
+ ok = delete_queue_directory(Dir)
+ end, TransientDirs),
+ ok.
+
+
+%%----------------------------------------------------------------------------
+%% Minor Helpers
+%%----------------------------------------------------------------------------
+
+write_to_journal(BinList, SeqIds, Dict,
+ State = #qistate { journal_count = JCount }) ->
+ {Hdl, State1} = get_journal_handle(State),
+ ok = file_handle_cache:append(Hdl, BinList),
+ {Dict1, JCount1} =
+ lists:foldl(
+ fun (SeqId, {Dict2, JCount2}) ->
+ {add_seqid_to_dict(SeqId, Dict2), JCount2 + 1}
+ end, {Dict, JCount}, SeqIds),
+ {Dict1, State1 #qistate { journal_count = JCount1 }}.
+
+maybe_full_flush(State = #qistate { journal_count = JCount }) ->
+ case JCount > ?MAX_ACK_JOURNAL_ENTRY_COUNT of
+ true -> full_flush_journal(State);
+ false -> State
+ end.
+
+full_flush_journal(State) ->
+ case can_flush_journal(State) of
+ true -> State1 = flush_journal(State),
+ full_flush_journal(State1);
+ false -> State
+ end.
+
+queue_name_to_dir_name(Name = #resource { kind = queue }) ->
+ Bin = term_to_binary(Name),
+ Size = 8*size(Bin),
+ <<Num:Size>> = Bin,
+ lists:flatten(io_lib:format("~.36B", [Num])).
+
+queues_dir() ->
+ filename:join(rabbit_mnesia:dir(), "queues").
+
+rev_sort(List) ->
+ lists:sort(fun (A, B) -> B < A end, List).
+
+get_journal_handle(State = #qistate { dir = Dir, seg_num_handles = SegHdls }) ->
+ case dict:find(journal, SegHdls) of
+ {ok, Hdl} -> {Hdl, State};
+ error ->
+ Path = filename:join(Dir, ?ACK_JOURNAL_FILENAME),
+ Mode = [raw, binary, delayed_write, write, read, read_ahead],
+ new_handle(journal, Path, Mode, State)
+ end.
+
+get_pub_handle(SegNum, State = #qistate { publish_handle = PubHandle }) ->
+ {State1, PubHandle1 = {_SegNum, Hdl, _Count}} =
+ get_counted_handle(SegNum, State, PubHandle),
+ {Hdl, State1 #qistate { publish_handle = PubHandle1 }}.
+
+get_counted_handle(SegNum, State, undefined) ->
+ get_counted_handle(SegNum, State, {SegNum, undefined, 0});
+get_counted_handle(SegNum, State = #qistate { partial_segments = Partials },
+ {SegNum, undefined, Count}) ->
+ {Hdl, State1} = get_seg_handle(SegNum, State),
+ {CountExtra, Partials1} =
+ case dict:find(SegNum, Partials) of
+ {ok, CountExtra1} -> {CountExtra1, dict:erase(SegNum, Partials)};
+ error -> {0, Partials}
+ end,
+ Count1 = Count + 1 + CountExtra,
+ {State1 #qistate { partial_segments = Partials1 }, {SegNum, Hdl, Count1}};
+get_counted_handle(SegNum, State, {SegNum, Hdl, Count})
+ when Count < ?SEGMENT_ENTRIES_COUNT ->
+ {State, {SegNum, Hdl, Count + 1}};
+get_counted_handle(SegNumA, State, {SegNumB, Hdl, ?SEGMENT_ENTRIES_COUNT})
+ when SegNumA == SegNumB + 1 ->
+ ok = file_handle_cache:append_write_buffer(Hdl),
+ get_counted_handle(SegNumA, State, undefined);
+get_counted_handle(SegNumA, State = #qistate { partial_segments = Partials,
+ seg_ack_counts = AckCounts,
+ dir = Dir },
+ {SegNumB, Hdl, Count}) ->
+ %% don't flush here because it's possible SegNumB has been deleted
+ State1 =
+ case dict:find(SegNumB, AckCounts) of
+ {ok, Count} ->
+ %% #acks == #pubs, and we're moving to different
+ %% segment, so delete.
+ delete_segment(SegNumB, State);
+ _ ->
+ State #qistate {
+ partial_segments = dict:store(SegNumB, Count, Partials) }
+ end,
+ get_counted_handle(SegNumA, State1, undefined).
+
+get_seg_handle(SegNum, State = #qistate { dir = Dir, seg_num_handles = SegHdls }) ->
+ case dict:find(SegNum, SegHdls) of
+ {ok, Hdl} -> {Hdl, State};
+ error ->
+ new_handle(SegNum, seg_num_to_path(Dir, SegNum),
+ [binary, raw, read, write,
+ {delayed_write, ?SEGMENT_TOTAL_SIZE, 1000},
+ {read_ahead, ?SEGMENT_TOTAL_SIZE}],
+ State)
+ end.
+
+delete_segment(SegNum, State = #qistate { dir = Dir,
+ seg_ack_counts = AckCounts,
+ partial_segments = Partials }) ->
+ State1 = close_handle(SegNum, State),
+ ok = case file:delete(seg_num_to_path(Dir, SegNum)) of
+ ok -> ok;
+ {error, enoent} -> ok
+ end,
+ State1 #qistate {seg_ack_counts = dict:erase(SegNum, AckCounts),
+ partial_segments = dict:erase(SegNum, Partials) }.
+
+new_handle(Key, Path, Mode, State = #qistate { seg_num_handles = SegHdls }) ->
+ {ok, Hdl} = file_handle_cache:open(Path, Mode, [{write_buffer, infinity}]),
+ {Hdl, State #qistate { seg_num_handles = dict:store(Key, Hdl, SegHdls) }}.
+
+close_handle(Key, State = #qistate { seg_num_handles = SegHdls }) ->
+ case dict:find(Key, SegHdls) of
+ {ok, Hdl} ->
+ ok = file_handle_cache:close(Hdl),
+ State #qistate { seg_num_handles = dict:erase(Key, SegHdls) };
+ error -> State
+ end.
+
+close_all_handles(State = #qistate { seg_num_handles = SegHdls }) ->
+ ok = dict:fold(fun (_Key, Hdl, ok) ->
+ file_handle_cache:close(Hdl)
+ end, ok, SegHdls),
+ State #qistate { seg_num_handles = dict:new() }.
+
+bool_to_int(true ) -> 1;
+bool_to_int(false) -> 0.
+
+seq_id_to_seg_and_rel_seq_id(SeqId) ->
+ { SeqId div ?SEGMENT_ENTRIES_COUNT, SeqId rem ?SEGMENT_ENTRIES_COUNT }.
+
+reconstruct_seq_id(SegNum, RelSeq) ->
+ (SegNum * ?SEGMENT_ENTRIES_COUNT) + RelSeq.
+
+seg_num_to_path(Dir, SegNum) ->
+ SegName = integer_to_list(SegNum),
+ filename:join(Dir, SegName ++ ?SEGMENT_EXTENSION).
+
+delete_queue_directory(Dir) ->
+ {ok, Entries} = file:list_dir(Dir),
+ ok = lists:foldl(fun (Entry, ok) ->
+ file:delete(filename:join(Dir, Entry))
+ end, ok, Entries),
+ ok = file:del_dir(Dir).
+
+add_seqid_to_dict(SeqId, Dict) ->
+ {SegNum, RelSeq} = seq_id_to_seg_and_rel_seq_id(SeqId),
+ add_seqid_to_dict(SegNum, RelSeq, Dict).
+
+add_seqid_to_dict(SegNum, RelSeq, Dict) ->
+ dict:update(SegNum, fun(Lst) -> [RelSeq|Lst] end, [RelSeq], Dict).
+
+all_segment_nums(Dir) ->
+ lists:sort(
+ [list_to_integer(
+ lists:takewhile(fun(C) -> $0 =< C andalso C =< $9 end, SegName))
+ || SegName <- filelib:wildcard("*" ++ ?SEGMENT_EXTENSION, Dir)]).
+
+blank_state(QueueName) ->
+ StrName = queue_name_to_dir_name(QueueName),
+ Dir = filename:join(queues_dir(), StrName),
+ ok = filelib:ensure_dir(filename:join(Dir, "nothing")),
+ #qistate { dir = Dir,
+ seg_num_handles = dict:new(),
+ journal_count = 0,
+ journal_ack_dict = dict:new(),
+ journal_del_dict = dict:new(),
+ seg_ack_counts = dict:new(),
+ publish_handle = undefined,
+ partial_segments = dict:new()
+ }.
+
+detect_clean_shutdown(Dir) ->
+ case file:delete(filename:join(Dir, ?CLEAN_FILENAME)) of
+ ok -> true;
+ {error, enoent} -> false
+ end.
+
+store_clean_shutdown(Dir) ->
+ {ok, Hdl} = file_handle_cache:open(filename:join(Dir, ?CLEAN_FILENAME),
+ [write, raw, binary],
+ [{write_buffer, unbuffered}]),
+ ok = file_handle_cache:close(Hdl).
+
+seg_entries_from_dict(SegNum, Dict) ->
+ case dict:find(SegNum, Dict) of
+ {ok, Entries} -> Entries;
+ error -> []
+ end.
+
+
+%%----------------------------------------------------------------------------
+%% Msg Store Startup Delta Function
+%%----------------------------------------------------------------------------
+
+queue_index_walker([]) ->
+ finished;
+queue_index_walker([QueueName|QueueNames]) ->
+ State = blank_state(QueueName),
+ {Hdl, State1} = get_journal_handle(State),
+ {_JDelDict, JAckDict} = load_journal(Hdl, dict:new(), dict:new()),
+ State2 = #qistate { dir = Dir } =
+ close_handle(journal, State1 #qistate { journal_ack_dict = JAckDict }),
+ SegNums = all_segment_nums(Dir),
+ queue_index_walker({SegNums, State2, QueueNames});
+
+queue_index_walker({[], State, QueueNames}) ->
+ _State = terminate(State),
+ queue_index_walker(QueueNames);
+queue_index_walker({[SegNum | SegNums], State, QueueNames}) ->
+ {SDict, _PubCount, _AckCount, _HighRelSeq, State1} =
+ load_segment(SegNum, State),
+ queue_index_walker({dict:to_list(SDict), State1, SegNums, QueueNames});
+
+queue_index_walker({[], State, SegNums, QueueNames}) ->
+ queue_index_walker({SegNums, State, QueueNames});
+queue_index_walker({[{_RelSeq, {MsgId, _IsDelivered, IsPersistent}} | Msgs],
+ State, SegNums, QueueNames}) ->
+ case IsPersistent of
+ true -> {MsgId, 1, {Msgs, State, SegNums, QueueNames}};
+ false -> queue_index_walker({Msgs, State, SegNums, QueueNames})
+ end.
+
+
+%%----------------------------------------------------------------------------
+%% Startup Functions
+%%----------------------------------------------------------------------------
+
+read_and_prune_segments(State = #qistate { dir = Dir }) ->
+ SegNums = all_segment_nums(Dir),
+ CleanShutdown = detect_clean_shutdown(Dir),
+ {TotalMsgCount, State1} =
+ lists:foldl(
+ fun (SegNum, {TotalMsgCount1, StateN =
+ #qistate { publish_handle = PublishHandle,
+ partial_segments = Partials }}) ->
+ {SDict, PubCount, AckCount, _HighRelSeq, StateM} =
+ load_segment(SegNum, StateN),
+ StateL = #qistate { seg_ack_counts = AckCounts } =
+ drop_and_deliver(SegNum, SDict, CleanShutdown, StateM),
+ %% ignore the effect of drop_and_deliver on
+ %% TotalMsgCount and AckCounts, as drop_and_deliver
+ %% will add to the journal dicts, which will then
+ %% effect TotalMsgCount when we scatter the journal
+ TotalMsgCount2 = TotalMsgCount1 + dict:size(SDict),
+ AckCounts1 = case AckCount of
+ 0 -> AckCounts;
+ N -> dict:store(SegNum, N, AckCounts)
+ end,
+ %% In the following, whilst there may be several
+ %% partial segments, we only remember the last
+ %% one. All other partial segments get added into
+ %% the partial_segments dict
+ {PublishHandle1, Partials1} =
+ case PubCount of
+ ?SEGMENT_ENTRIES_COUNT ->
+ {PublishHandle, Partials};
+ 0 ->
+ {PublishHandle, Partials};
+ _ ->
+ {{SegNum, undefined, PubCount},
+ case PublishHandle of
+ undefined ->
+ Partials;
+ {SegNumOld, undefined, PubCountOld} ->
+ dict:store(SegNumOld, PubCountOld,
+ Partials)
+ end}
+ end,
+ {TotalMsgCount2,
+ StateL #qistate { seg_ack_counts = AckCounts1,
+ publish_handle = PublishHandle1,
+ partial_segments = Partials1 }}
+ end, {0, State}, SegNums),
+ {TotalMsgCount, State1}.
+
+scatter_journal(TotalMsgCount, State = #qistate { dir = Dir }) ->
+ {Hdl, State1 = #qistate { journal_del_dict = JDelDict,
+ journal_ack_dict = JAckDict }} =
+ get_journal_handle(State),
+ %% ADict and DDict may well contain duplicates. However, this is
+ %% ok, because we use sets to eliminate dups before writing to
+ %% segments
+ {ADict, DDict} = load_journal(Hdl, JAckDict, JDelDict),
+ State2 = close_handle(journal, State1),
+ {TotalMsgCount1, ADict1, State3} =
+ dict:fold(fun replay_journal_to_segment/3,
+ {TotalMsgCount, ADict,
+ %% supply empty dicts so that when
+ %% replay_journal_to_segment loads segments, it
+ %% gets all msgs, and ignores anything we've found
+ %% in the journal.
+ State2 #qistate { journal_del_dict = dict:new(),
+ journal_ack_dict = dict:new() }}, DDict),
+ %% replay for segments which only had acks, and no deliveries
+ {TotalMsgCount2, State4} =
+ dict:fold(fun replay_journal_acks_to_segment/3,
+ {TotalMsgCount1, State3}, ADict1),
+ JournalPath = filename:join(Dir, ?ACK_JOURNAL_FILENAME),
+ ok = file:delete(JournalPath),
+ {TotalMsgCount2, State4}.
+
+load_journal(Hdl, ADict, DDict) ->
+ case file_handle_cache:read(Hdl, ?SEQ_BYTES) of
+ {ok, <<?DEL_BIT:1, SeqId:?SEQ_BITS>>} ->
+ load_journal(Hdl, ADict, add_seqid_to_dict(SeqId, DDict));
+ {ok, <<?ACK_BIT:1, SeqId:?SEQ_BITS>>} ->
+ load_journal(Hdl, add_seqid_to_dict(SeqId, ADict), DDict);
+ _ErrOrEoF -> {ADict, DDict}
+ end.
+
+replay_journal_to_segment(_SegNum, [], {TotalMsgCount, ADict, State}) ->
+ {TotalMsgCount, ADict, State};
+replay_journal_to_segment(SegNum, Dels, {TotalMsgCount, ADict, State}) ->
+ {SDict, _PubCount, _AckCount, _HighRelSeq, State1} =
+ load_segment(SegNum, State),
+ ValidDels = sets:to_list(
+ sets:filter(
+ fun (RelSeq) ->
+ case dict:find(RelSeq, SDict) of
+ {ok, {_MsgId, false, _IsPersistent}} -> true;
+ _ -> false
+ end
+ end, sets:from_list(Dels))),
+ State2 = append_dels_to_segment(SegNum, ValidDels, State1),
+ Acks = seg_entries_from_dict(SegNum, ADict),
+ case Acks of
+ [] -> {TotalMsgCount, ADict, State2};
+ _ ->
+ ADict1 = dict:erase(SegNum, ADict),
+ {Count, State3} = filter_acks_and_append_to_segment(SegNum, SDict,
+ Acks, State2),
+ {TotalMsgCount - Count, ADict1, State3}
+ end.
+
+replay_journal_acks_to_segment(_SegNum, [], {TotalMsgCount, State}) ->
+ {TotalMsgCount, State};
+replay_journal_acks_to_segment(SegNum, Acks, {TotalMsgCount, State}) ->
+ {SDict, _PubCount, _AckCount, _HighRelSeq, State1} =
+ load_segment(SegNum, State),
+ {Count, State2} =
+ filter_acks_and_append_to_segment(SegNum, SDict, Acks, State1),
+ {TotalMsgCount - Count, State2}.
+
+filter_acks_and_append_to_segment(SegNum, SDict, Acks, State) ->
+ ValidRelSeqIds = dict:fetch_keys(SDict),
+ ValidAcks = sets:to_list(sets:intersection(sets:from_list(ValidRelSeqIds),
+ sets:from_list(Acks))),
+ {length(ValidAcks), append_acks_to_segment(SegNum, ValidAcks, State)}.
+
+drop_and_deliver(SegNum, SDict, CleanShutdown,
+ State = #qistate { journal_del_dict = JDelDict,
+ journal_ack_dict = JAckDict }) ->
+ {JDelDict1, JAckDict1} =
+ dict:fold(
+ fun (RelSeq, {MsgId, IsDelivered, true}, {JDelDict2, JAckDict2}) ->
+ %% msg is persistent, keep only if the msg_store has it
+ case {IsDelivered, rabbit_msg_store:contains(MsgId)} of
+ {false, true} when not CleanShutdown ->
+ %% not delivered, but dirty shutdown => mark delivered
+ {add_seqid_to_dict(SegNum, RelSeq, JDelDict2),
+ JAckDict2};
+ {_, true} ->
+ {JDelDict2, JAckDict2};
+ {true, false} ->
+ {JDelDict2,
+ add_seqid_to_dict(SegNum, RelSeq, JAckDict2)};
+ {false, false} ->
+ {add_seqid_to_dict(SegNum, RelSeq, JDelDict2),
+ add_seqid_to_dict(SegNum, RelSeq, JAckDict2)}
+ end;
+ (RelSeq, {_MsgId, false, false}, {JDelDict2, JAckDict2}) ->
+ %% not persistent and not delivered => deliver and ack it
+ {add_seqid_to_dict(SegNum, RelSeq, JDelDict2),
+ add_seqid_to_dict(SegNum, RelSeq, JAckDict2)};
+ (RelSeq, {_MsgId, true, false}, {JDelDict2, JAckDict2}) ->
+ %% not persistent but delivered => ack it
+ {JDelDict2,
+ add_seqid_to_dict(SegNum, RelSeq, JAckDict2)}
+ end, {JDelDict, JAckDict}, SDict),
+ State #qistate { journal_del_dict = JDelDict1,
+ journal_ack_dict = JAckDict1 }.
+
+
+%%----------------------------------------------------------------------------
+%% Loading Segments
+%%----------------------------------------------------------------------------
+
+load_segment(SegNum, State = #qistate { seg_num_handles = SegHdls,
+ dir = Dir }) ->
+ SegmentExists = case dict:find(SegNum, SegHdls) of
+ {ok, _} -> true;
+ error -> filelib:is_file(seg_num_to_path(Dir, SegNum))
+ end,
+ case SegmentExists of
+ false -> {dict:new(), 0, 0, 0, State};
+ true ->
+ {Hdl, State1 = #qistate { journal_del_dict = JDelDict,
+ journal_ack_dict = JAckDict }} =
+ get_seg_handle(SegNum, State),
+ {ok, 0} = file_handle_cache:position(Hdl, bof),
+ {SDict, PubCount, AckCount, HighRelSeq} =
+ load_segment_entries(Hdl, dict:new(), 0, 0, 0),
+ %% delete ack'd msgs first
+ {SDict1, AckCount1} =
+ lists:foldl(fun (RelSeq, {SDict2, AckCount2}) ->
+ {dict:erase(RelSeq, SDict2), AckCount2 + 1}
+ end, {SDict, AckCount},
+ seg_entries_from_dict(SegNum, JAckDict)),
+ %% ensure remaining msgs are delivered as necessary
+ SDict3 =
+ lists:foldl(
+ fun (RelSeq, SDict4) ->
+ case dict:find(RelSeq, SDict4) of
+ {ok, {MsgId, false, IsPersistent}} ->
+ dict:store(RelSeq, {MsgId, true, IsPersistent},
+ SDict4);
+ _ -> SDict4
+ end
+ end, SDict1, seg_entries_from_dict(SegNum, JDelDict)),
+
+ {SDict3, PubCount, AckCount1, HighRelSeq, State1}
+ end.
+
+load_segment_entries(Hdl, SDict, PubCount, AckCount, HighRelSeq) ->
+ case file_handle_cache:read(Hdl, 1) of
+ {ok, <<?REL_SEQ_ONLY_PREFIX:?REL_SEQ_ONLY_PREFIX_BITS,
+ MSB:(8-?REL_SEQ_ONLY_PREFIX_BITS)>>} ->
+ {ok, LSB} = file_handle_cache:read(
+ Hdl, ?REL_SEQ_ONLY_ENTRY_LENGTH_BYTES - 1),
+ <<RelSeq:?REL_SEQ_BITS_BYTE_ALIGNED>> = <<MSB, LSB/binary>>,
+ {SDict1, AckCount1} = deliver_or_ack_msg(SDict, AckCount, RelSeq),
+ load_segment_entries(Hdl, SDict1, PubCount, AckCount1, HighRelSeq);
+ {ok, <<?PUBLISH_PREFIX:?PUBLISH_PREFIX_BITS,
+ IsPersistentNum:1, MSB:(7-?PUBLISH_PREFIX_BITS)>>} ->
+ %% because we specify /binary, and binaries are complete
+ %% bytes, the size spec is in bytes, not bits.
+ {ok, <<LSB:1/binary, MsgId:?MSG_ID_BYTES/binary>>} =
+ file_handle_cache:read(
+ Hdl, ?PUBLISH_RECORD_LENGTH_BYTES - 1),
+ <<RelSeq:?REL_SEQ_BITS_BYTE_ALIGNED>> = <<MSB, LSB/binary>>,
+ HighRelSeq1 = lists:max([RelSeq, HighRelSeq]),
+ load_segment_entries(
+ Hdl, dict:store(RelSeq, {MsgId, false, 1 == IsPersistentNum},
+ SDict), PubCount + 1, AckCount, HighRelSeq1);
+ _ErrOrEoF -> {SDict, PubCount, AckCount, HighRelSeq}
+ end.
+
+deliver_or_ack_msg(SDict, AckCount, RelSeq) ->
+ case dict:find(RelSeq, SDict) of
+ {ok, {MsgId, false, IsPersistent}} ->
+ {dict:store(RelSeq, {MsgId, true, IsPersistent}, SDict), AckCount};
+ {ok, {_MsgId, true, _IsPersistent}} ->
+ {dict:erase(RelSeq, SDict), AckCount + 1}
+ end.
+
+
+%%----------------------------------------------------------------------------
+%% Appending Acks or Dels to Segments
+%%----------------------------------------------------------------------------
+
+append_acks_to_segment(SegNum, Acks,
+ State = #qistate { seg_ack_counts = AckCounts,
+ partial_segments = Partials }) ->
+ AckCount = case dict:find(SegNum, AckCounts) of
+ {ok, AckCount1} -> AckCount1;
+ error -> 0
+ end,
+ AckTarget = case dict:find(SegNum, Partials) of
+ {ok, PubCount} -> PubCount;
+ error -> ?SEGMENT_ENTRIES_COUNT
+ end,
+ AckCount2 = AckCount + length(Acks),
+ append_acks_to_segment(SegNum, AckCount2, Acks, AckTarget, State).
+
+append_acks_to_segment(SegNum, AckCount, _Acks, AckCount, State =
+ #qistate { publish_handle = PubHdl }) ->
+ PubHdl1 = case PubHdl of
+ %% If we're adjusting the pubhdl here then there
+ %% will be no entry in partials, thus the target ack
+ %% count must be SEGMENT_ENTRIES_COUNT
+ {SegNum, Hdl, AckCount = ?SEGMENT_ENTRIES_COUNT}
+ when Hdl /= undefined ->
+ {SegNum + 1, undefined, 0};
+ _ -> PubHdl
+ end,
+ delete_segment(SegNum, State #qistate { publish_handle = PubHdl1 });
+append_acks_to_segment(_SegNum, _AckCount, [], _AckTarget, State) ->
+ State;
+append_acks_to_segment(SegNum, AckCount, Acks, AckTarget, State =
+ #qistate { seg_ack_counts = AckCounts })
+ when AckCount < AckTarget ->
+ {Hdl, State1} = append_to_segment(SegNum, Acks, State),
+ ok = file_handle_cache:sync(Hdl),
+ State1 #qistate { seg_ack_counts =
+ dict:store(SegNum, AckCount, AckCounts) }.
+
+append_dels_to_segment(SegNum, Dels, State) ->
+ {_Hdl, State1} = append_to_segment(SegNum, Dels, State),
+ State1.
+
+append_to_segment(SegNum, AcksOrDels, State) ->
+ {Hdl, State1} = get_seg_handle(SegNum, State),
+ ok = file_handle_cache:append(
+ Hdl, [<<?REL_SEQ_ONLY_PREFIX:?REL_SEQ_ONLY_PREFIX_BITS,
+ RelSeq:?REL_SEQ_BITS>> || RelSeq <- AcksOrDels ]),
+ {Hdl, State1}.
diff --git a/src/rabbit_queue_prefetcher.erl b/src/rabbit_queue_prefetcher.erl
new file mode 100644
index 0000000000..f5e717f55b
--- /dev/null
+++ b/src/rabbit_queue_prefetcher.erl
@@ -0,0 +1,295 @@
+%% The contents of this file are subject to the Mozilla Public License
+%% Version 1.1 (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.mozilla.org/MPL/
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+%% License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% The Original Code is RabbitMQ.
+%%
+%% The Initial Developers of the Original Code are LShift Ltd,
+%% Cohesive Financial Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created before 22-Nov-2008 00:00:00 GMT by LShift Ltd,
+%% Cohesive Financial Technologies LLC, or Rabbit Technologies Ltd
+%% are Copyright (C) 2007-2008 LShift Ltd, Cohesive Financial
+%% Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created by LShift Ltd are Copyright (C) 2007-2009 LShift
+%% Ltd. Portions created by Cohesive Financial Technologies LLC are
+%% Copyright (C) 2007-2009 Cohesive Financial Technologies
+%% LLC. Portions created by Rabbit Technologies Ltd are Copyright
+%% (C) 2007-2009 Rabbit Technologies Ltd.
+%%
+%% All Rights Reserved.
+%%
+%% Contributor(s): ______________________________________.
+%%
+
+-module(rabbit_queue_prefetcher).
+
+-behaviour(gen_server2).
+
+-export([start_link/1]).
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-export([publish/2, drain/1, drain_and_stop/1, stop/1]).
+
+-include("rabbit.hrl").
+-include("rabbit_queue.hrl").
+
+-define(HIBERNATE_AFTER_MIN, 1000).
+-define(DESIRED_HIBERNATE, 10000).
+
+-record(pstate,
+ { alphas,
+ betas,
+ queue_mref,
+ peruse_cb
+ }).
+
+%%----------------------------------------------------------------------------
+%% Novel
+%%----------------------------------------------------------------------------
+
+%% The design of the prefetcher is based on the following:
+%%
+%% a) It must issue low-priority (-ve) requests to the disk queue for
+%% the next message.
+%% b) If the prefetcher is empty and the amqqueue_process
+%% (mixed_queue) asks it for a message, it must exit immediately,
+%% telling the mixed_queue that it is empty so that the mixed_queue
+%% can then take the more efficient path and communicate with the
+%% disk_queue directly
+%% c) No message can accidentally be delivered twice, or lost
+%% d) The prefetcher must only cause load when the disk_queue is
+%% otherwise idle, and must not worsen performance in a loaded
+%% situation.
+%%
+%% As such, it's a little tricky. It must never issue a call to the
+%% disk_queue - if it did, then that could potentially block, thus
+%% causing pain to the mixed_queue that needs fast answers as to
+%% whether the prefetcher has prefetched content or not. It behaves as
+%% follows:
+%%
+%% 1) disk_queue:prefetch(Q)
+%% This is a low priority cast
+%%
+%% 2) The disk_queue may pick up the cast, at which point it'll read
+%% the next message and invoke prefetcher:publish(Msg) - normal
+%% priority cast. Note that in the mean time, the mixed_queue could
+%% have come along, found the prefetcher empty, asked it to
+%% exit. This means the effective "reply" from the disk_queue will
+%% go no where. As a result, the disk_queue should not advance the
+%% queue. However, it does mark the messages as delivered. The
+%% reasoning is that if it didn't, there would be the possibility
+%% that the message was delivered without it being marked as such
+%% on disk. We must maintain the property that a message which is
+%% marked as non-redelivered really hasn't been delivered anywhere
+%% before. The downside is that should the prefetcher not receive
+%% this message, the queue will then fetch the message from the
+%% disk_queue directly, and this message will have its delivered
+%% bit set. The queue will not be advanced though - if it did
+%% advance the queue and the msg was then lost, then the queue
+%% would have lost a msg that the mixed_queue would not pick up.
+%%
+%% 3) The prefetcher hopefully receives the call from
+%% prefetcher:publish(Msg). It replies immediately, and then adds
+%% to its internal queue. A cast is not sufficient as a pseudo
+%% "reply" here because the mixed_queue could come along, drain the
+%% prefetcher, thus catching the msg just sent by the disk_queue
+%% and then call disk_queue:fetch(Q) which is normal priority call,
+%% which could overtake a reply cast from the prefetcher to the
+%% disk queue, resulting in the same message being delivered
+%% twice. Thus when the disk_queue calls prefetcher:publish(Msg),
+%% it is briefly blocked. However, a) the prefetcher replies
+%% immediately, and b) the prefetcher should never have more than
+%% two items in its mailbox anyway (one from the queue process /
+%% mixed_queue and one from the disk_queue), so this should not
+%% cause a problem to the disk_queue.
+%%
+%% 4) The disk_queue receives the reply, and advances the Q to the
+%% next msg.
+%%
+%% 5) If the prefetcher has not met its target then it goes back to
+%% 1). Otherwise it just sits and waits for the mixed_queue to
+%% drain it.
+%%
+%% Now at some point, the mixed_queue will come along and will call
+%% prefetcher:drain() - normal priority call. The prefetcher then
+%% replies with its internal queue and a flag saying if the prefetcher
+%% has finished or is continuing; if the prefetch target was reached,
+%% the prefetcher stops normally at this point. If it hasn't been
+%% reached, then the prefetcher continues to hang around (it almost
+%% certainly has issued a disk_queue:prefetch(Q) cast and is waiting
+%% for a reply from the disk_queue).
+%%
+%% If the mixed_queue calls prefetcher:drain() and the prefetcher's
+%% internal queue is empty then the prefetcher replies with 'empty',
+%% and it exits. This informs the mixed_queue that it should from now
+%% on talk directly with the disk_queue and not via the
+%% prefetcher. This is more efficient and the mixed_queue will use
+%% normal priority blocking calls to the disk_queue and thus get
+%% better service.
+%%
+%% The prefetcher may at this point have issued a
+%% disk_queue:prefetch(Q) cast which has not yet been picked up by the
+%% disk_queue. This msg won't go away and the disk_queue will
+%% eventually find it. However, when it does, it'll simply read the
+%% next message from the queue (which could now be empty), possibly
+%% populate the cache (no harm done), mark the message as delivered
+%% (oh well, not a spec violation, and better than the alternative)
+%% and try and call prefetcher:publish(Msg) which will result in an
+%% error, which the disk_queue catches, as the publish call is to a
+%% non-existant process. However, the state of the queue has not been
+%% altered so the mixed_queue will be able to fetch this message as if
+%% it had never been prefetched.
+%%
+%% The only point at which the queue is advanced is when the
+%% prefetcher replies to the publish call. At this point the message
+%% has been received by the prefetcher and so we guarantee it will be
+%% passed to the mixed_queue when the mixed_queue tries to drain the
+%% prefetcher. We must therefore ensure that this msg can't also be
+%% delivered to the mixed_queue directly by the disk_queue through the
+%% mixed_queue calling disk_queue:fetch(Q) which is why the
+%% prefetcher:publish function is a call and not a cast, thus blocking
+%% the disk_queue.
+%%
+%% Finally, the prefetcher is only created when the mixed_queue is
+%% operating in mixed mode and it sees that the next N messages are
+%% all on disk, and the queue process is about to hibernate. During
+%% this phase, the mixed_queue can be asked to go back to disk_only
+%% mode. When this happens, it calls prefetcher:drain_and_stop() which
+%% behaves like two consecutive calls to drain() - i.e. replies with
+%% all prefetched messages and causes the prefetcher to exit.
+%%
+%% Note there is a flaw here in that we end up marking messages which
+%% have come through the prefetcher as delivered even if they don't
+%% get delivered (e.g. prefetcher fetches them, then broker
+%% dies). However, the alternative is that the mixed_queue must do a
+%% call to the disk_queue when it effectively passes them out to the
+%% rabbit_writer. This would hurt performance, and even at that stage,
+%% we have no guarantee that the message will really go out of the
+%% socket. What we do still have is that messages which have the
+%% redelivered bit set false really are guaranteed to have not been
+%% delivered already.
+
+%%----------------------------------------------------------------------------
+
+-ifdef(use_specs).
+
+-spec(start_link/1 :: (queue()) ->
+ ({'ok', pid()} | 'ignore' | {'error', any()})).
+-spec(publish/2 :: (pid(), (message()| 'not_found')) -> 'ok').
+-spec(drain/1 :: (pid()) -> ({('finished' | 'continuing' | 'empty'), queue()})).
+-spec(drain_and_stop/1 :: (pid()) -> ({('empty' | queue()), queue()})).
+-spec(stop/1 :: (pid()) -> 'ok').
+
+-endif.
+
+%%----------------------------------------------------------------------------
+
+start_link(Betas) ->
+ false = queue:is_empty(Betas), %% ASSERTION
+ gen_server2:start_link(?MODULE, [Betas, self()], []).
+
+publish(Prefetcher, Obj = #basic_message {}) ->
+ gen_server2:call(Prefetcher, {publish, Obj}, infinity);
+publish(Prefetcher, not_found) ->
+ gen_server2:call(Prefetcher, publish_empty, infinity).
+
+drain(Prefetcher) ->
+ gen_server2:call(Prefetcher, drain, infinity).
+
+drain_and_stop(Prefetcher) ->
+ gen_server2:call(Prefetcher, drain_and_stop, infinity).
+
+stop(Prefetcher) ->
+ gen_server2:call(Prefetcher, stop, infinity).
+
+%%----------------------------------------------------------------------------
+
+init([Betas, QPid]) when is_pid(QPid) ->
+ %% link isn't enough because the signal will not appear if the
+ %% queue exits normally. Thus have to use monitor.
+ MRef = erlang:monitor(process, QPid),
+ Self = self(),
+ CB = fun (Result) ->
+ rabbit_misc:with_exit_handler(
+ fun () -> ok end,
+ fun () -> case Result of
+ {ok, Msg} -> publish(Self, Msg);
+ not_found -> publish(Self, not_found)
+ end
+ end)
+ end,
+ State = #pstate { alphas = queue:new(),
+ betas = Betas,
+ queue_mref = MRef,
+ peruse_cb = CB
+ },
+ {ok, prefetch(State), infinity, {backoff, ?HIBERNATE_AFTER_MIN,
+ ?HIBERNATE_AFTER_MIN, ?DESIRED_HIBERNATE}}.
+
+handle_call({publish, Msg = #basic_message { guid = MsgId,
+ is_persistent = IsPersistent }},
+ DiskQueue, State = #pstate { alphas = Alphas, betas = Betas }) ->
+ gen_server2:reply(DiskQueue, ok),
+ {{value, #beta { msg_id = MsgId, seq_id = SeqId,
+ is_persistent = IsPersistent,
+ is_delivered = IsDelivered,
+ index_on_disk = IndexOnDisk}}, Betas1} = queue:out(Betas),
+ Alphas1 = queue:in(#alpha { msg = Msg, seq_id = SeqId,
+ is_delivered = IsDelivered, msg_on_disk = true,
+ index_on_disk = IndexOnDisk }, Alphas),
+ State1 = State #pstate { alphas = Alphas1, betas = Betas1 },
+ {Timeout, State2} = case queue:is_empty(Betas1) of
+ true -> {hibernate, State1};
+ false -> {infinity, prefetch(State1)}
+ end,
+ {noreply, State2, Timeout};
+handle_call(publish_empty, _From, State) ->
+ %% Very odd. This could happen if the queue is deleted or purged
+ %% and the mixed queue fails to shut us down.
+ {reply, ok, State, hibernate};
+handle_call(drain, _From, State = #pstate { alphas = Alphas, betas = Betas }) ->
+ case {queue:is_empty(Betas), queue:is_empty(Alphas)} of
+ {true , _ } -> {stop, normal, {finished, Alphas}, State};
+ {false, true } -> {stop, normal, {empty, Betas}, State};
+ {false, false} -> {reply, {continuing, Alphas},
+ State #pstate { alphas = queue:new() }}
+ end;
+handle_call(drain_and_stop, _From, State = #pstate { alphas = Alphas,
+ betas = Betas }) ->
+ Res = case queue:is_empty(Alphas) of
+ true -> {empty, Betas};
+ false -> {Alphas, Betas}
+ end,
+ {stop, normal, Res, State};
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State}.
+
+handle_cast(Msg, State) ->
+ exit({unexpected_message_cast_to_prefetcher, Msg, State}).
+
+handle_info({'DOWN', MRef, process, _Pid, _Reason},
+ State = #pstate { queue_mref = MRef }) ->
+ %% this is the amqqueue_process going down, so we should go down
+ %% too
+ {stop, normal, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+prefetch(State = #pstate { betas = Betas, peruse_cb = CB }) ->
+ {{value, #beta { msg_id = MsgId }}, _Betas1} = queue:out(Betas),
+ ok = rabbit_msg_store:peruse(MsgId, CB),
+ State.
diff --git a/src/rabbit_tests.erl b/src/rabbit_tests.erl
index c5a7d05eb4..d74f998e33 100644
--- a/src/rabbit_tests.erl
+++ b/src/rabbit_tests.erl
@@ -31,6 +31,8 @@
-module(rabbit_tests).
+-compile(export_all).
+
-export([all_tests/0, test_parsing/0]).
%% Exported so the hook mechanism can call back
@@ -48,6 +50,9 @@ test_content_prop_roundtrip(Datum, Binary) ->
Binary = rabbit_binary_generator:encode_properties(Types, Values). %% assertion
all_tests() ->
+ passed = test_msg_store(),
+ passed = test_queue_index(),
+ passed = test_variable_queue(),
passed = test_priority_queue(),
passed = test_unfold(),
passed = test_parsing(),
@@ -168,7 +173,6 @@ priority_queue_out_all(Q) ->
{empty, _} -> [];
{{value, V}, Q1} -> [V | priority_queue_out_all(Q1)]
end.
-
test_priority_queue(Q) ->
{priority_queue:is_queue(Q),
priority_queue:is_empty(Q),
@@ -819,3 +823,695 @@ bad_handle_hook(_, _, _) ->
bad:bad().
extra_arg_hook(Hookname, Handler, Args, Extra1, Extra2) ->
handle_hook(Hookname, Handler, {Args, Extra1, Extra2}).
+
+msg_store_dir() ->
+ filename:join(rabbit_mnesia:dir(), "msg_store").
+
+start_msg_store_empty() ->
+ start_msg_store(fun (ok) -> finished end, ok).
+
+start_msg_store(MsgRefDeltaGen, MsgRefDeltaGenInit) ->
+ rabbit:start_child(rabbit_msg_store, [msg_store_dir(), MsgRefDeltaGen,
+ MsgRefDeltaGenInit]).
+
+stop_msg_store() ->
+ case supervisor:terminate_child(rabbit_sup, rabbit_msg_store) of
+ ok -> supervisor:delete_child(rabbit_sup, rabbit_msg_store);
+ E -> E
+ end.
+
+msg_id_bin(X) ->
+ erlang:md5(term_to_binary(X)).
+
+msg_store_contains(Atom, MsgIds) ->
+ Atom = lists:foldl(
+ fun (MsgId, Atom1) when Atom1 =:= Atom ->
+ rabbit_msg_store:contains(MsgId) end, Atom, MsgIds).
+
+msg_store_sync(MsgIds) ->
+ Ref = make_ref(),
+ Self = self(),
+ ok = rabbit_msg_store:sync(MsgIds,
+ fun () -> Self ! {sync, Ref} end),
+ receive
+ {sync, Ref} -> ok
+ after
+ 10000 ->
+ io:format("Sync from msg_store missing for msg_ids ~p~n", [MsgIds]),
+ throw(timeout)
+ end.
+
+msg_store_read(MsgIds) ->
+ ok =
+ lists:foldl(
+ fun (MsgId, ok) -> {ok, MsgId} = rabbit_msg_store:read(MsgId), ok end,
+ ok, MsgIds).
+
+msg_store_write(MsgIds) ->
+ ok = lists:foldl(
+ fun (MsgId, ok) -> rabbit_msg_store:write(MsgId, MsgId) end,
+ ok, MsgIds).
+
+test_msg_store() ->
+ stop_msg_store(),
+ ok = start_msg_store_empty(),
+ Self = self(),
+ MsgIds = [msg_id_bin(M) || M <- lists:seq(1,100)],
+ {MsgIds1stHalf, MsgIds2ndHalf} = lists:split(50, MsgIds),
+ %% check we don't contain any of the msgs we're about to publish
+ false = msg_store_contains(false, MsgIds),
+ %% publish the first half
+ ok = msg_store_write(MsgIds1stHalf),
+ %% sync on the first half
+ ok = msg_store_sync(MsgIds1stHalf),
+ %% publish the second half
+ ok = msg_store_write(MsgIds2ndHalf),
+ %% sync on the first half again - the msg_store will be dirty, but
+ %% we won't need the fsync
+ ok = msg_store_sync(MsgIds1stHalf),
+ %% check they're all in there
+ true = msg_store_contains(true, MsgIds),
+ %% publish the latter half twice so we hit the caching and ref count code
+ ok = msg_store_write(MsgIds2ndHalf),
+ %% check they're still all in there
+ true = msg_store_contains(true, MsgIds),
+ %% sync on the 2nd half, but do lots of individual syncs to try
+ %% and cause coalescing to happen
+ ok = lists:foldl(
+ fun (MsgId, ok) -> rabbit_msg_store:sync(
+ [MsgId], fun () -> Self ! {sync, MsgId} end)
+ end, ok, MsgIds2ndHalf),
+ lists:foldl(
+ fun(MsgId, ok) ->
+ receive
+ {sync, MsgId} -> ok
+ after
+ 10000 ->
+ io:format("Sync from msg_store missing (msg_id: ~p)~n",
+ [MsgId]),
+ throw(timeout)
+ end
+ end, ok, MsgIds2ndHalf),
+ %% it's very likely we're not dirty here, so the 1st half sync
+ %% should hit a different code path
+ ok = msg_store_sync(MsgIds1stHalf),
+ %% read them all
+ ok = msg_store_read(MsgIds),
+ %% read them all again - this will hit the cache, not disk
+ ok = msg_store_read(MsgIds),
+ %% remove them all
+ ok = rabbit_msg_store:remove(MsgIds),
+ %% check first half doesn't exist
+ false = msg_store_contains(false, MsgIds1stHalf),
+ %% check second half does exist
+ true = msg_store_contains(true, MsgIds2ndHalf),
+ %% read the second half again
+ ok = msg_store_read(MsgIds2ndHalf),
+ %% release the second half, just for fun (aka code coverage)
+ ok = rabbit_msg_store:release(MsgIds2ndHalf),
+ %% read the second half again, just for fun (aka code coverage)
+ ok = msg_store_read(MsgIds2ndHalf),
+ %% read the second half via peruse
+ lists:foldl(
+ fun (MsgId, ok) ->
+ rabbit_msg_store:peruse(MsgId,
+ fun ({ok, MsgId1}) when MsgId1 == MsgId ->
+ Self ! {peruse, MsgId1}
+ end),
+ receive
+ {peruse, MsgId} ->
+ ok
+ after
+ 10000 ->
+ io:format("Failed to receive response via peruse~n"),
+ throw(timeout)
+ end
+ end, ok, MsgIds2ndHalf),
+ %% stop and restart, preserving every other msg in 2nd half
+ ok = stop_msg_store(),
+ ok = start_msg_store(fun ([]) -> finished;
+ ([MsgId|MsgIdsTail])
+ when length(MsgIdsTail) rem 2 == 0 ->
+ {MsgId, 1, MsgIdsTail};
+ ([MsgId|MsgIdsTail]) ->
+ {MsgId, 0, MsgIdsTail}
+ end, MsgIds2ndHalf),
+ %% check we have the right msgs left
+ lists:foldl(
+ fun (MsgId, Bool) ->
+ not(Bool = rabbit_msg_store:contains(MsgId))
+ end, false, MsgIds2ndHalf),
+ %% restart empty
+ ok = stop_msg_store(),
+ ok = start_msg_store_empty(),
+ %% check we don't contain any of the msgs
+ false = msg_store_contains(false, MsgIds),
+ %% publish the first half again
+ ok = msg_store_write(MsgIds1stHalf),
+ %% this should force some sort of sync internally otherwise misread
+ ok = msg_store_read(MsgIds1stHalf),
+ ok = rabbit_msg_store:remove(MsgIds1stHalf),
+ %% push a lot of msgs in...
+ BigCount = 100000,
+ MsgIdsBig = lists:seq(1, BigCount),
+ Payload = << 0:65536 >>,
+ ok = lists:foldl(
+ fun (MsgId, ok) ->
+ rabbit_msg_store:write(msg_id_bin(MsgId), Payload)
+ end, ok, MsgIdsBig),
+ %% .., then remove even numbers ascending, and odd numbers
+ %% descending. This hits the GC.
+ ok = lists:foldl(
+ fun (MsgId, ok) ->
+ rabbit_msg_store:remove([msg_id_bin(
+ case MsgId rem 2 of
+ 0 -> MsgId;
+ 1 -> BigCount - MsgId
+ end)])
+ end, ok, MsgIdsBig),
+ %% ensure empty
+ false = msg_store_contains(false, [msg_id_bin(M) || M <- MsgIdsBig]),
+ %% restart empty
+ ok = stop_msg_store(),
+ ok = start_msg_store_empty(),
+ passed.
+
+queue_name(Name) ->
+ rabbit_misc:r(<<"/">>, queue, term_to_binary(Name)).
+
+test_queue() ->
+ queue_name(test).
+
+test_amqqueue(Durable) ->
+ #amqqueue{name = test_queue(),
+ durable = Durable,
+ auto_delete = true,
+ arguments = [],
+ pid = none}.
+
+empty_test_queue() ->
+ ok = rabbit_queue_index:start_msg_store([]),
+ {0, Qi1} = rabbit_queue_index:init(test_queue()),
+ _Qi2 = rabbit_queue_index:terminate_and_erase(Qi1),
+ ok.
+
+queue_index_publish(SeqIds, Persistent, Qi) ->
+ lists:foldl(
+ fun (SeqId, {QiN, SeqIdsMsgIdsAcc}) ->
+ MsgId = rabbit_guid:guid(),
+ QiM = rabbit_queue_index:write_published(MsgId, SeqId, Persistent,
+ QiN),
+ ok = rabbit_msg_store:write(MsgId, MsgId),
+ {QiM, [{SeqId, MsgId} | SeqIdsMsgIdsAcc]}
+ end, {Qi, []}, SeqIds).
+
+queue_index_deliver(SeqIds, Qi) ->
+ lists:foldl(
+ fun (SeqId, QiN) ->
+ rabbit_queue_index:write_delivered(SeqId, QiN)
+ end, Qi, SeqIds).
+
+queue_index_flush_journal(Qi) ->
+ {_Oks, {false, Qi1}} =
+ rabbit_misc:unfold(
+ fun ({true, QiN}) ->
+ QiM = rabbit_queue_index:flush_journal(QiN),
+ {true, ok, {rabbit_queue_index:can_flush_journal(QiM), QiM}};
+ ({false, _QiN}) ->
+ false
+ end, {true, Qi}),
+ Qi1.
+
+verify_read_with_published(_Delivered, _Persistent, [], _) ->
+ ok;
+verify_read_with_published(Delivered, Persistent,
+ [{MsgId, SeqId, Persistent, Delivered}|Read],
+ [{SeqId, MsgId}|Published]) ->
+ verify_read_with_published(Delivered, Persistent, Read, Published);
+verify_read_with_published(_Delivered, _Persistent, _Read, _Published) ->
+ ko.
+
+test_queue_index() ->
+ stop_msg_store(),
+ ok = empty_test_queue(),
+ SeqIdsA = lists:seq(0,9999),
+ SeqIdsB = lists:seq(10000,19999),
+ {0, Qi0} = rabbit_queue_index:init(test_queue()),
+ {0, 0, Qi1} =
+ rabbit_queue_index:find_lowest_seq_id_seg_and_next_seq_id(Qi0),
+ {Qi2, SeqIdsMsgIdsA} = queue_index_publish(SeqIdsA, false, Qi1),
+ {0, 10000, Qi3} =
+ rabbit_queue_index:find_lowest_seq_id_seg_and_next_seq_id(Qi2),
+ {ReadA, Qi4} = rabbit_queue_index:read_segment_entries(0, Qi3),
+ ok = verify_read_with_published(false, false, ReadA,
+ lists:reverse(SeqIdsMsgIdsA)),
+ %% call terminate twice to prove it's idempotent
+ _Qi5 = rabbit_queue_index:terminate(rabbit_queue_index:terminate(Qi4)),
+ ok = stop_msg_store(),
+ ok = rabbit_queue_index:start_msg_store([test_amqqueue(true)]),
+ %% should get length back as 0, as all the msgs were transient
+ {0, Qi6} = rabbit_queue_index:init(test_queue()),
+ false = rabbit_queue_index:can_flush_journal(Qi6),
+ {0, 10000, Qi7} =
+ rabbit_queue_index:find_lowest_seq_id_seg_and_next_seq_id(Qi6),
+ {Qi8, SeqIdsMsgIdsB} = queue_index_publish(SeqIdsB, true, Qi7),
+ {0, 20000, Qi9} =
+ rabbit_queue_index:find_lowest_seq_id_seg_and_next_seq_id(Qi8),
+ {ReadB, Qi10} = rabbit_queue_index:read_segment_entries(0, Qi9),
+ ok = verify_read_with_published(false, true, ReadB,
+ lists:reverse(SeqIdsMsgIdsB)),
+ _Qi11 = rabbit_queue_index:terminate(Qi10),
+ ok = stop_msg_store(),
+ ok = rabbit_queue_index:start_msg_store([test_amqqueue(true)]),
+ %% should get length back as 10000
+ LenB = length(SeqIdsB),
+ {LenB, Qi12} = rabbit_queue_index:init(test_queue()),
+ {0, 20000, Qi13} =
+ rabbit_queue_index:find_lowest_seq_id_seg_and_next_seq_id(Qi12),
+ Qi14 = queue_index_deliver(SeqIdsB, Qi13),
+ {ReadC, Qi15} = rabbit_queue_index:read_segment_entries(0, Qi14),
+ ok = verify_read_with_published(true, true, ReadC,
+ lists:reverse(SeqIdsMsgIdsB)),
+ Qi16 = rabbit_queue_index:write_acks(SeqIdsB, Qi15),
+ true = rabbit_queue_index:can_flush_journal(Qi16),
+ Qi17 = rabbit_queue_index:flush_journal(Qi16),
+ %% the entire first segment will have gone as they were firstly
+ %% transient, and secondly ack'd
+ SegmentSize = rabbit_queue_index:segment_size(),
+ {SegmentSize, 20000, Qi18} =
+ rabbit_queue_index:find_lowest_seq_id_seg_and_next_seq_id(Qi17),
+ _Qi19 = rabbit_queue_index:terminate(Qi18),
+ ok = stop_msg_store(),
+ ok = rabbit_queue_index:start_msg_store([test_amqqueue(true)]),
+ %% should get length back as 0 because all persistent msgs have been acked
+ {0, Qi20} = rabbit_queue_index:init(test_queue()),
+ _Qi21 = rabbit_queue_index:terminate_and_erase(Qi20),
+ ok = stop_msg_store(),
+ ok = empty_test_queue(),
+
+ %% These next bits are just to hit the auto deletion of segment files.
+ %% First, partials:
+ %% a) partial pub+del+ack, then move to new segment
+ SeqIdsC = lists:seq(0,trunc(SegmentSize/2)),
+ {0, Qi22} = rabbit_queue_index:init(test_queue()),
+ {Qi23, _SeqIdsMsgIdsC} = queue_index_publish(SeqIdsC, false, Qi22),
+ Qi24 = queue_index_deliver(SeqIdsC, Qi23),
+ Qi25 = rabbit_queue_index:write_acks(SeqIdsC, Qi24),
+ Qi26 = queue_index_flush_journal(Qi25),
+ {Qi27, _SeqIdsMsgIdsC1} = queue_index_publish([SegmentSize], false, Qi26),
+ _Qi28 = rabbit_queue_index:terminate_and_erase(Qi27),
+ ok = stop_msg_store(),
+ ok = empty_test_queue(),
+
+ %% b) partial pub+del, then move to new segment, then ack all in old segment
+ {0, Qi29} = rabbit_queue_index:init(test_queue()),
+ {Qi30, _SeqIdsMsgIdsC2} = queue_index_publish(SeqIdsC, false, Qi29),
+ Qi31 = queue_index_deliver(SeqIdsC, Qi30),
+ {Qi32, _SeqIdsMsgIdsC3} = queue_index_publish([SegmentSize], false, Qi31),
+ Qi33 = rabbit_queue_index:write_acks(SeqIdsC, Qi32),
+ Qi34 = queue_index_flush_journal(Qi33),
+ _Qi35 = rabbit_queue_index:terminate_and_erase(Qi34),
+ ok = stop_msg_store(),
+ ok = empty_test_queue(),
+
+ %% c) just fill up several segments of all pubs, then +dels, then +acks
+ SeqIdsD = lists:seq(0,SegmentSize*4),
+ {0, Qi36} = rabbit_queue_index:init(test_queue()),
+ {Qi37, _SeqIdsMsgIdsD} = queue_index_publish(SeqIdsD, false, Qi36),
+ Qi38 = queue_index_deliver(SeqIdsD, Qi37),
+ Qi39 = rabbit_queue_index:write_acks(SeqIdsD, Qi38),
+ Qi40 = queue_index_flush_journal(Qi39),
+ _Qi41 = rabbit_queue_index:terminate_and_erase(Qi40),
+ ok = stop_msg_store(),
+ ok = rabbit_queue_index:start_msg_store([]),
+ ok = stop_msg_store(),
+ passed.
+
+variable_queue_publish(IsPersistent, Count, VQ) ->
+ lists:foldl(
+ fun (_N, {Acc, VQ1}) ->
+ {SeqId, VQ2} = rabbit_variable_queue:publish(
+ rabbit_basic:message(
+ rabbit_misc:r(<<>>, exchange, <<>>),
+ <<>>, [], <<>>, rabbit_guid:guid(),
+ IsPersistent), VQ1),
+ {[SeqId | Acc], VQ2}
+ end, {[], VQ}, lists:seq(1, Count)).
+
+variable_queue_fetch(Count, IsPersistent, IsDelivered, Len, VQ) ->
+ lists:foldl(fun (N, {VQN, AckTagsAcc}) ->
+ Rem = Len - N,
+ {{#basic_message { is_persistent = IsPersistent },
+ IsDelivered, AckTagN, Rem}, VQM} =
+ rabbit_variable_queue:fetch(VQN),
+ {VQM, [AckTagN | AckTagsAcc]}
+ end, {VQ, []}, lists:seq(1, Count)).
+
+assert_prop(List, Prop, Value) ->
+ Value = proplists:get_value(Prop, List).
+
+fresh_variable_queue() ->
+ stop_msg_store(),
+ ok = empty_test_queue(),
+ VQ = rabbit_variable_queue:init(test_queue()),
+ S0 = rabbit_variable_queue:status(VQ),
+ assert_prop(S0, len, 0),
+ assert_prop(S0, prefetching, false),
+ assert_prop(S0, q1, 0),
+ assert_prop(S0, q2, 0),
+ assert_prop(S0, gamma, {gamma, undefined, 0}),
+ assert_prop(S0, q3, 0),
+ assert_prop(S0, q4, 0),
+ VQ.
+
+test_variable_queue() ->
+ passed = test_variable_queue_prefetching_and_gammas_to_betas(),
+ passed = test_variable_queue_prefetching_during_publish(0),
+ passed = test_variable_queue_prefetching_during_publish(5000),
+ passed = test_variable_queue_prefetch_evicts_q1(),
+ passed = test_variable_queue_dynamic_duration_change(),
+ passed.
+
+test_variable_queue_dynamic_duration_change() ->
+ SegmentSize = rabbit_queue_index:segment_size(),
+ VQ0 = fresh_variable_queue(),
+ %% start by sending in a couple of segments worth
+ Len1 = 2*SegmentSize,
+ {_SeqIds, VQ1} = variable_queue_publish(false, Len1, VQ0),
+ VQ2 = rabbit_variable_queue:remeasure_egress_rate(VQ1),
+ {ok, _TRef} = timer:send_after(1000, {duration, 60,
+ fun (V) -> (V*0.75)-1 end}),
+ VQ3 = test_variable_queue_dynamic_duration_change_f(Len1, VQ2),
+ {VQ4, AckTags} = variable_queue_fetch(Len1, false, false, Len1, VQ3),
+ VQ5 = rabbit_variable_queue:ack(AckTags, VQ4),
+ {empty, VQ6} = rabbit_variable_queue:fetch(VQ5),
+
+ %% just publish and fetch some persistent msgs, this hits the the
+ %% partial segment path in queue_index due to the period when
+ %% duration was 0 and the entire queue was gamma.
+ {_SeqIds1, VQ7} = variable_queue_publish(true, 20, VQ6),
+ {VQ8, AckTags1} = variable_queue_fetch(20, true, false, 20, VQ7),
+ VQ9 = rabbit_variable_queue:ack(AckTags1, VQ8),
+ VQ10 = rabbit_variable_queue:flush_journal(VQ9),
+ VQ11 = rabbit_variable_queue:flush_journal(VQ10),
+ {empty, VQ12} = rabbit_variable_queue:fetch(VQ11),
+
+ rabbit_variable_queue:terminate(VQ12),
+
+ passed.
+
+test_variable_queue_dynamic_duration_change_f(Len, VQ0) ->
+ {_SeqIds, VQ1} = variable_queue_publish(false, 1, VQ0),
+ {{_Msg, false, AckTag, Len}, VQ2} = rabbit_variable_queue:fetch(VQ1),
+ VQ3 = rabbit_variable_queue:ack([AckTag], VQ2),
+ receive
+ {duration, _, stop} ->
+ VQ3;
+ {duration, N, Fun} ->
+ N1 = lists:max([Fun(N), 0]),
+ Fun1 = case N1 of
+ 0 -> fun (V) -> (V+1)/0.75 end;
+ _ when N1 > 400 -> stop;
+ _ -> Fun
+ end,
+ {ok, _TRef} = timer:send_after(1000, {duration, N1, Fun1}),
+ VQ4 = rabbit_variable_queue:remeasure_egress_rate(VQ3),
+ VQ5 = %% /37 otherwise the duration is just to high to stress things
+ rabbit_variable_queue:set_queue_ram_duration_target(N/37, VQ4),
+ io:format("~p:~n~p~n~n", [N, rabbit_variable_queue:status(VQ5)]),
+ test_variable_queue_dynamic_duration_change_f(Len, VQ5)
+ after 0 ->
+ test_variable_queue_dynamic_duration_change_f(Len, VQ3)
+ end.
+
+test_variable_queue_prefetch_evicts_q1() ->
+ SegmentSize = rabbit_queue_index:segment_size(),
+ VQ0 = fresh_variable_queue(),
+ VQ1 = rabbit_variable_queue:set_queue_ram_duration_target(0, VQ0),
+ assert_prop(rabbit_variable_queue:status(VQ1), target_ram_msg_count, 0),
+ Len1 = 2*SegmentSize,
+ {_SeqIds, VQ2} = variable_queue_publish(true, Len1, VQ1),
+ %% one segment will be in q3, the other in gamma. We want to fetch
+ %% all of q3 so that gamma is then moved into q3, emptying gamma
+
+ VQ3 = rabbit_variable_queue:remeasure_egress_rate(VQ2),
+ Start = now(),
+ {VQ4, AckTags} = variable_queue_fetch(SegmentSize, true, false, Len1, VQ3),
+ End = now(),
+ VQ5 = rabbit_variable_queue:ack(AckTags, VQ4),
+ S5 = rabbit_variable_queue:status(VQ5),
+ assert_prop(S5, q4, 0),
+ assert_prop(S5, q3, SegmentSize),
+ assert_prop(S5, gamma, {gamma, undefined, 0}),
+ assert_prop(S5, len, SegmentSize),
+ assert_prop(S5, prefetching, false),
+
+ VQ6 = rabbit_variable_queue:remeasure_egress_rate(VQ5),
+ %% half the seconds taken to fetch one segment
+ Duration = timer:now_diff(End, Start) / 2000000,
+ VQ7 = rabbit_variable_queue:set_queue_ram_duration_target(Duration, VQ6),
+ S7 = rabbit_variable_queue:status(VQ7),
+ assert_prop(S7, q4, 0),
+ Q3 = proplists:get_value(q3, S7),
+ true = Q3 > 0, %% not prefetching everything
+ assert_prop(S7, gamma, {gamma, undefined, 0}),
+ assert_prop(S7, len, SegmentSize),
+ assert_prop(S7, prefetching, true),
+
+ %% now publish a segment, this'll go half in q1, half in q3, in
+ %% theory.
+ {_SeqIds1, VQ8} = variable_queue_publish(true, SegmentSize, VQ7),
+ S8 = rabbit_variable_queue:status(VQ8),
+ assert_prop(S8, q4, 0),
+ assert_prop(S8, q2, 0),
+ assert_prop(S8, len, Len1),
+ assert_prop(S8, prefetching, true),
+ Q3a = proplists:get_value(q3, S8),
+ Q3a_new = Q3a - Q3,
+ Q1a = proplists:get_value(q1, S8),
+ true = (Q3a_new + Q1a == SegmentSize) andalso Q1a < SegmentSize,
+
+ %% wait a bit, to let the prefetcher do its thing
+ timer:sleep(2000),
+ %% fetch a msg. The prefetcher *should* have finished, but can't
+ %% guarantee it.
+ Len2 = Len1-1,
+ {{_Msg, false, AckTag, Len2}, VQ9} = rabbit_variable_queue:fetch(VQ8),
+ S9 = rabbit_variable_queue:status(VQ9),
+ case proplists:get_value(prefetching, S9) of
+ true ->
+ %% bits of q1 could have moved into q3, and the prefetcher
+ %% won't have returned any betas for q3. So q3 can not
+ %% have shrunk.
+ Q3b = proplists:get_value(q3, S9),
+ Q1b = proplists:get_value(q1, S9),
+ true = (Q1a + Q3a) == (Q1b + Q3b) andalso Q3b >= Q3a;
+ false ->
+ %% there should be content in q4 and q3 (we only did 1
+ %% fetch. This is not sufficient to kill the prefetcher
+ %% through draining it when it's empty, thus if it's not
+ %% running, it must have finished, not been killed, thus
+ %% q4 will not be empty), and q1 should have gone into q3.
+ Q1b = proplists:get_value(q1, S9),
+ Q3b = proplists:get_value(q3, S9),
+ Q4b = proplists:get_value(q4, S9),
+ NotPrefetched = Q3b - (SegmentSize - Q1b),
+ SegmentSize = NotPrefetched + Q4b + 1 %% we fetched one
+ end,
+
+ %% just for the fun of it, set duration to 0. This should push
+ %% everything back into gamma, except the eldest (partial) segment
+ %% in q3
+ VQ10 = rabbit_variable_queue:set_queue_ram_duration_target(0, VQ9),
+ S10 = rabbit_variable_queue:status(VQ10),
+ assert_prop(S10, len, Len2),
+ assert_prop(S10, prefetching, false),
+ assert_prop(S10, q1, 0),
+ assert_prop(S10, q2, 0),
+ assert_prop(S10, gamma, {gamma, Len1, SegmentSize}),
+ assert_prop(S10, q3, (Len2 - SegmentSize)),
+ assert_prop(S10, q4, 0),
+
+ {VQ11, AckTags1} = variable_queue_fetch(Len2, true, false, Len2, VQ10),
+ VQ12 = rabbit_variable_queue:ack([AckTag|AckTags1], VQ11),
+ {empty, VQ13} = rabbit_variable_queue:fetch(VQ12),
+ rabbit_variable_queue:terminate(VQ13),
+
+ passed.
+
+test_variable_queue_prefetching_during_publish(PrefetchDelay) ->
+ SegmentSize = rabbit_queue_index:segment_size(),
+ VQ0 = fresh_variable_queue(),
+ VQ1 = rabbit_variable_queue:set_queue_ram_duration_target(0, VQ0),
+ assert_prop(rabbit_variable_queue:status(VQ1), target_ram_msg_count, 0),
+
+ Len1 = 2*SegmentSize,
+ {_SeqIds, VQ2} = variable_queue_publish(true, Len1, VQ1),
+ %% one segment will be in q3, the other in gamma. We want to fetch
+ %% all of q3 so that gamma is then moved into q3, emptying gamma
+
+ VQ3 = rabbit_variable_queue:remeasure_egress_rate(VQ2),
+ {VQ4, AckTags} = variable_queue_fetch(SegmentSize, true, false, Len1, VQ3),
+ VQ5 = rabbit_variable_queue:ack(AckTags, VQ4),
+ S5 = rabbit_variable_queue:status(VQ5),
+ assert_prop(S5, q4, 0),
+ assert_prop(S5, q3, SegmentSize),
+ assert_prop(S5, gamma, {gamma, undefined, 0}),
+ assert_prop(S5, len, SegmentSize),
+ assert_prop(S5, prefetching, false),
+
+ %% we assume that we can fetch at > 1 msg a second
+ VQ6 = rabbit_variable_queue:remeasure_egress_rate(VQ5),
+ VQ7 = rabbit_variable_queue:set_queue_ram_duration_target(Len1, VQ6),
+ S7 = rabbit_variable_queue:status(VQ7),
+ assert_prop(S7, q4, 0),
+ assert_prop(S7, q3, 0),
+ assert_prop(S7, gamma, {gamma, undefined, 0}),
+ assert_prop(S7, len, SegmentSize),
+ assert_prop(S7, prefetching, true),
+
+ timer:sleep(PrefetchDelay),
+
+ {_SeqIds1, VQ8} = variable_queue_publish(true, SegmentSize, VQ7),
+ S8 = rabbit_variable_queue:status(VQ8),
+ assert_prop(S8, q4, 0),
+ assert_prop(S8, q2, 0),
+ assert_prop(S8, q1, SegmentSize),
+ assert_prop(S8, len, Len1),
+ assert_prop(S8, prefetching, true),
+
+ {VQ9, AckTags1} =
+ variable_queue_fetch(SegmentSize-1, true, false, Len1, VQ8),
+ VQ10 = rabbit_variable_queue:ack(AckTags1, VQ9),
+ %% can't guarantee the prefetcher has stopped here. If it is still
+ %% running, then we must have SegmentSize is q1. If it's not
+ %% running, and it completed, then we'll find SegmentSize + 1 in
+ %% q4 (q1 will have been joined to q4), otherwise, we'll find
+ %% SegmentSize in q1 and 1 in q3 and q4 empty.
+ S10 = rabbit_variable_queue:status(VQ10),
+ assert_prop(S10, q2, 0),
+ assert_prop(S10, len, (SegmentSize+1)),
+ case proplists:get_value(prefetching, S10) of
+ true -> assert_prop(S10, q1, SegmentSize),
+ assert_prop(S10, q3, 0),
+ assert_prop(S10, q4, 0);
+ false -> case proplists:get_value(q3, S10) of
+ 0 -> assert_prop(S10, q4, SegmentSize+1),
+ assert_prop(S10, q1, 0);
+ 1 -> assert_prop(S10, q4, 0),
+ assert_prop(S10, q1, SegmentSize)
+ end
+ end,
+
+ {VQ11, AckTags2} =
+ variable_queue_fetch(SegmentSize+1, true, false, SegmentSize+1, VQ10),
+ VQ12 = rabbit_variable_queue:ack(AckTags2, VQ11),
+
+ {empty, VQ13} = rabbit_variable_queue:fetch(VQ12),
+ rabbit_variable_queue:terminate(VQ13),
+
+ passed.
+
+test_variable_queue_prefetching_and_gammas_to_betas() ->
+ SegmentSize = rabbit_queue_index:segment_size(),
+ VQ0 = fresh_variable_queue(),
+
+ VQ1 = rabbit_variable_queue:set_queue_ram_duration_target(10, VQ0),
+ assert_prop(rabbit_variable_queue:status(VQ1), target_ram_msg_count, 0),
+
+ {_SeqIds, VQ2} = variable_queue_publish(false, 3 * SegmentSize, VQ1),
+ S2 = rabbit_variable_queue:status(VQ2),
+ assert_prop(S2, gamma, {gamma, SegmentSize, 2*SegmentSize}),
+ assert_prop(S2, q3, SegmentSize),
+ assert_prop(S2, len, 3*SegmentSize),
+
+ VQ3 = rabbit_variable_queue:remeasure_egress_rate(VQ2),
+ Len1 = 3*SegmentSize - 1,
+ {{_Msg, false, AckTag, Len1}, VQ4} = rabbit_variable_queue:fetch(VQ3),
+ timer:sleep(1000),
+ VQ5 = rabbit_variable_queue:remeasure_egress_rate(VQ4),
+ VQ6 = rabbit_variable_queue:set_queue_ram_duration_target(10, VQ5),
+ timer:sleep(1000), %% let the prefetcher run and grab enough - about 4 msgs
+ S6 = rabbit_variable_queue:status(VQ6),
+ RamCount = proplists:get_value(target_ram_msg_count, S6),
+ assert_prop(S6, prefetching, true),
+ assert_prop(S6, q4, 0),
+ assert_prop(S6, q3, (Len1 - RamCount)),
+ assert_prop(S6, gamma, {gamma, undefined, 0}),
+
+ Len2 = Len1 - 1,
+ %% this should be enough to stop + drain the prefetcher
+ {{_Msg1, false, AckTag1, Len2}, VQ7} = rabbit_variable_queue:fetch(VQ6),
+ S7 = rabbit_variable_queue:status(VQ7),
+ assert_prop(S7, prefetching, false),
+ assert_prop(S7, q4, (RamCount - 1)),
+ assert_prop(S7, q3, (Len1 - RamCount)),
+
+ %% now fetch SegmentSize - 1 which will exhaust q4 and work through a bit of q3
+ %% bringing in a segment from gamma:
+ {VQ8, AckTags} = variable_queue_fetch(SegmentSize-1, false, false, Len2, VQ7),
+ Len3 = Len2 - (SegmentSize - 1),
+ S8 = rabbit_variable_queue:status(VQ8),
+ assert_prop(S8, prefetching, false),
+ assert_prop(S8, q4, 0),
+ assert_prop(S8, q3, Len3),
+ assert_prop(S8, len, Len3),
+
+ VQ9 = rabbit_variable_queue:remeasure_egress_rate(VQ8),
+ VQ10 = rabbit_variable_queue:ack(AckTags, VQ9),
+
+ S10 = rabbit_variable_queue:status(VQ10),
+ assert_prop(S10, prefetching, true),
+ %% egress rate should be really high, so it's likely if we wait a
+ %% little bit, lots of msgs will be brought in
+ timer:sleep(2000),
+ PrefetchCount = lists:min([proplists:get_value(target_ram_msg_count, S10) -
+ proplists:get_value(ram_msg_count, S10),
+ Len3]),
+ Len4 = Len3 - 1,
+ {{_Msg2, false, AckTag2, Len4}, VQ11} = rabbit_variable_queue:fetch(VQ10),
+ S11 = rabbit_variable_queue:status(VQ11),
+ %% prefetcher will stop if it's fast enough and has completed by
+ %% now, or may still be running if PrefetchCount > 1
+ Prefetched = proplists:get_value(q4, S11),
+ true = PrefetchCount > Prefetched, %% already fetched 1, thus >, not >=
+ %% q3 will contain whatever the prefetcher was not allowed to
+ %% prefetch, due to memory constraints. If the prefetcher is still
+ %% running, this will be less than (Len4 - Prefetched) because
+ %% Prefetched will not reflect the true number of msgs that it's
+ %% trying to prefetch.
+ case proplists:get_value(prefetching, S11) of
+ true -> true = (Len4 - Prefetched) > proplists:get_value(q3, S11);
+ false -> assert_prop(S11, q3, Len4 - Prefetched)
+ end,
+ assert_prop(S11, gamma, {gamma, undefined, 0}),
+ assert_prop(S11, q2, 0),
+ assert_prop(S11, q1, 0),
+
+ VQ12 = rabbit_variable_queue:maybe_start_prefetcher(VQ11),
+ S12 = rabbit_variable_queue:status(VQ12),
+ assert_prop(S12, prefetching, (Len4 - Prefetched) > 0),
+ timer:sleep(2000),
+ %% we have to fetch all of q4 before the prefetcher will be drained
+ {VQ13, AckTags1} =
+ variable_queue_fetch(Prefetched, false, false, Len4, VQ12),
+ {VQ16, Acks} =
+ case Len4 == Prefetched of
+ true ->
+ {VQ13, [AckTag2, AckTag1, AckTag, AckTags1]};
+ false ->
+ Len5 = Len4 - Prefetched - 1,
+ {{_Msg3, false, AckTag3, Len5}, VQ14} =
+ rabbit_variable_queue:fetch(VQ13),
+ assert_prop(rabbit_variable_queue:status(VQ14),
+ prefetching, false),
+ {VQ15, AckTags2} =
+ variable_queue_fetch(Len5, false, false, Len5, VQ14),
+ {VQ15, [AckTag3, AckTag2, AckTag1, AckTag, AckTags1, AckTags2]}
+ end,
+ VQ17 = rabbit_variable_queue:ack(lists:flatten(Acks), VQ16),
+
+ {empty, VQ18} = rabbit_variable_queue:fetch(VQ17),
+
+ rabbit_variable_queue:terminate(VQ18),
+ passed.
diff --git a/src/rabbit_variable_queue.erl b/src/rabbit_variable_queue.erl
new file mode 100644
index 0000000000..15caf81bb3
--- /dev/null
+++ b/src/rabbit_variable_queue.erl
@@ -0,0 +1,1019 @@
+%% The contents of this file are subject to the Mozilla Public License
+%% Version 1.1 (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.mozilla.org/MPL/
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+%% License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% The Original Code is RabbitMQ.
+%%
+%% The Initial Developers of the Original Code are LShift Ltd,
+%% Cohesive Financial Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created before 22-Nov-2008 00:00:00 GMT by LShift Ltd,
+%% Cohesive Financial Technologies LLC, or Rabbit Technologies Ltd
+%% are Copyright (C) 2007-2008 LShift Ltd, Cohesive Financial
+%% Technologies LLC, and Rabbit Technologies Ltd.
+%%
+%% Portions created by LShift Ltd are Copyright (C) 2007-2009 LShift
+%% Ltd. Portions created by Cohesive Financial Technologies LLC are
+%% Copyright (C) 2007-2009 Cohesive Financial Technologies
+%% LLC. Portions created by Rabbit Technologies Ltd are Copyright
+%% (C) 2007-2009 Rabbit Technologies Ltd.
+%%
+%% All Rights Reserved.
+%%
+%% Contributor(s): ______________________________________.
+%%
+
+-module(rabbit_variable_queue).
+
+-export([init/1, terminate/1, publish/2, publish_delivered/2,
+ set_queue_ram_duration_target/2, remeasure_egress_rate/1, fetch/1,
+ ack/2, len/1, is_empty/1, maybe_start_prefetcher/1, purge/1, delete/1,
+ requeue/2, tx_publish/2, tx_rollback/2, tx_commit/4,
+ tx_commit_from_msg_store/4, tx_commit_from_vq/1, needs_sync/1,
+ can_flush_journal/1, flush_journal/1, status/1]).
+
+%%----------------------------------------------------------------------------
+
+-record(vqstate,
+ { q1,
+ q2,
+ gamma,
+ q3,
+ q4,
+ duration_target,
+ target_ram_msg_count,
+ ram_msg_count,
+ queue,
+ index_state,
+ next_seq_id,
+ out_counter,
+ egress_rate,
+ avg_egress_rate,
+ egress_rate_timestamp,
+ prefetcher,
+ len,
+ on_sync
+ }).
+
+-include("rabbit.hrl").
+-include("rabbit_queue.hrl").
+
+%%----------------------------------------------------------------------------
+
+%% Basic premise is that msgs move from q1 -> q2 -> gamma -> q3 -> q4
+%% but they can only do so in the right form. q1 and q4 only hold
+%% alphas (msgs in ram), q2 and q3 only hold betas (msg on disk, index
+%% in ram), and gamma is just a count of the number of index entries
+%% on disk at that stage (msg on disk, index on disk).
+%%
+%% When a msg arrives, we decide in which form it should be. It is
+%% then added to the right-most appropriate queue, maintaining
+%% order. Thus if the msg is to be an alpha, it will be added to q1,
+%% unless all of q2, gamma and q3 are empty, in which case it will go
+%% to q4. If it is to be a beta, it will be added to q2 unless gamma
+%% is empty, in which case it will go to q3.
+%%
+%% The major invariant is that if the msg is to be a beta, q1 will be
+%% empty, and if it is to be a gamma then both q1 and q2 will be empty.
+%%
+%% When taking msgs out of the queue, if q4 is empty then we drain the
+%% prefetcher. If that doesn't help then we read directly from q3, or
+%% gamma, if q3 is empty. If q3 and gamma are empty then we have an
+%% invariant that q2 must be empty because q2 can only grow if gamma
+%% is non empty.
+%%
+%% A further invariant is that if the queue is non empty, either q4 or
+%% q3 contains at least one entry. I.e. we never allow gamma to
+%% contain all msgs in the queue. Also, if q4 is non empty and gamma
+%% is non empty then q3 must be non empty.
+
+%%----------------------------------------------------------------------------
+
+-ifdef(use_specs).
+
+-type(msg_id() :: binary()).
+-type(seq_id() :: non_neg_integer()).
+-type(ack() :: {'ack_index_and_store', msg_id(), seq_id()}
+ | 'ack_not_on_disk').
+-type(vqstate() :: #vqstate {
+ q1 :: queue(),
+ q2 :: queue(),
+ gamma :: gamma(),
+ q3 :: queue(),
+ q4 :: queue(),
+ duration_target :: non_neg_integer(),
+ target_ram_msg_count :: non_neg_integer(),
+ queue :: queue_name(),
+ index_state :: any(),
+ next_seq_id :: seq_id(),
+ out_counter :: non_neg_integer(),
+ egress_rate :: float(),
+ avg_egress_rate :: float(),
+ egress_rate_timestamp :: {integer(), integer(), integer()},
+ prefetcher :: ('undefined' | pid()),
+ len :: non_neg_integer(),
+ on_sync :: {[ack()], [msg_id()], [{pid(), any()}]}
+ }).
+
+-spec(init/1 :: (queue_name()) -> vqstate()).
+-spec(terminate/1 :: (vqstate()) -> vqstate()).
+-spec(publish/2 :: (basic_message(), vqstate()) ->
+ {seq_id(), vqstate()}).
+-spec(publish_delivered/2 :: (basic_message(), vqstate()) ->
+ {ack(), vqstate()}).
+-spec(set_queue_ram_duration_target/2 ::
+ (('undefined' | number()), vqstate()) -> vqstate()).
+-spec(remeasure_egress_rate/1 :: (vqstate()) -> vqstate()).
+-spec(fetch/1 :: (vqstate()) ->
+ {('empty'|{basic_message(), boolean(), ack(), non_neg_integer()}),
+ vqstate()}).
+-spec(ack/2 :: ([ack()], vqstate()) -> vqstate()).
+-spec(len/1 :: (vqstate()) -> non_neg_integer()).
+-spec(is_empty/1 :: (vqstate()) -> boolean()).
+-spec(maybe_start_prefetcher/1 :: (vqstate()) -> vqstate()).
+-spec(purge/1 :: (vqstate()) -> {non_neg_integer(), vqstate()}).
+-spec(delete/1 :: (vqstate()) -> vqstate()).
+-spec(requeue/2 :: ([{basic_message(), ack()}], vqstate()) -> vqstate()).
+-spec(tx_publish/2 :: (basic_message(), vqstate()) -> vqstate()).
+-spec(tx_rollback/2 :: ([msg_id()], vqstate()) -> vqstate()).
+-spec(tx_commit/4 :: ([msg_id()], [ack()], {pid(), any()}, vqstate()) ->
+ {boolean(), vqstate()}).
+-spec(tx_commit_from_msg_store/4 ::
+ ([msg_id()], [ack()], {pid(), any()}, vqstate()) -> vqstate()).
+-spec(tx_commit_from_vq/1 :: (vqstate()) -> vqstate()).
+-spec(needs_sync/1 :: (vqstate()) -> boolean()).
+-spec(can_flush_journal/1 :: (vqstate()) -> boolean()).
+-spec(flush_journal/1 :: (vqstate()) -> vqstate()).
+-spec(status/1 :: (vqstate()) -> [{atom(), any()}]).
+
+-endif.
+
+%%----------------------------------------------------------------------------
+%% Public API
+%%----------------------------------------------------------------------------
+
+init(QueueName) ->
+ {GammaCount, IndexState} =
+ rabbit_queue_index:init(QueueName),
+ {GammaSeqId, NextSeqId, IndexState1} =
+ rabbit_queue_index:find_lowest_seq_id_seg_and_next_seq_id(IndexState),
+ Gamma = case GammaCount of
+ 0 -> #gamma { seq_id = undefined, count = 0 };
+ _ -> #gamma { seq_id = GammaSeqId, count = GammaCount }
+ end,
+ State =
+ #vqstate { q1 = queue:new(), q2 = queue:new(),
+ gamma = Gamma,
+ q3 = queue:new(), q4 = queue:new(),
+ target_ram_msg_count = undefined,
+ duration_target = undefined,
+ ram_msg_count = 0,
+ queue = QueueName,
+ index_state = IndexState1,
+ next_seq_id = NextSeqId,
+ out_counter = 0,
+ egress_rate = 0,
+ avg_egress_rate = 0,
+ egress_rate_timestamp = now(),
+ prefetcher = undefined,
+ len = GammaCount,
+ on_sync = {[], [], []}
+ },
+ maybe_gammas_to_betas(State).
+
+terminate(State = #vqstate { index_state = IndexState }) ->
+ State #vqstate { index_state = rabbit_queue_index:terminate(IndexState) }.
+
+publish(Msg, State) ->
+ publish(Msg, false, false, State).
+
+publish_delivered(Msg = #basic_message { guid = MsgId,
+ is_persistent = IsPersistent },
+ State = #vqstate { len = 0, index_state = IndexState,
+ next_seq_id = SeqId }) ->
+ case maybe_write_msg_to_disk(false, false, Msg) of
+ true ->
+ {true, IndexState1} =
+ maybe_write_index_to_disk(false, IsPersistent, MsgId, SeqId,
+ true, IndexState),
+ {{ack_index_and_store, MsgId, SeqId},
+ State #vqstate { index_state = IndexState1,
+ next_seq_id = SeqId + 1 }};
+ false ->
+ {ack_not_on_disk, State}
+ end.
+
+set_queue_ram_duration_target(undefined, State) ->
+ State;
+set_queue_ram_duration_target(
+ DurationTarget, State = #vqstate { avg_egress_rate = EgressRate,
+ target_ram_msg_count = TargetRamMsgCount
+ }) ->
+ TargetRamMsgCount1 = trunc(DurationTarget * EgressRate), %% msgs = sec * msgs/sec
+ State1 = State #vqstate { target_ram_msg_count = TargetRamMsgCount1,
+ duration_target = DurationTarget },
+ if TargetRamMsgCount == TargetRamMsgCount1 ->
+ State1;
+ TargetRamMsgCount == undefined orelse
+ TargetRamMsgCount < TargetRamMsgCount1 ->
+ maybe_start_prefetcher(State1);
+ true ->
+ reduce_memory_use(State1)
+ end.
+
+remeasure_egress_rate(State = #vqstate { egress_rate = OldEgressRate,
+ egress_rate_timestamp = Timestamp,
+ out_counter = OutCount,
+ duration_target = DurationTarget }) ->
+ %% We do an average over the last two values, but also hold the
+ %% current value separately so that the average always only
+ %% incorporates the last two values, and not the current value and
+ %% the last average. Averaging helps smooth out spikes.
+ Now = now(),
+ %% EgressRate is in seconds, and now_diff is in microseconds
+ EgressRate = 1000000 * OutCount / timer:now_diff(Now, Timestamp),
+ AvgEgressRate = (EgressRate + OldEgressRate) / 2,
+ set_queue_ram_duration_target(
+ DurationTarget,
+ State #vqstate { egress_rate = EgressRate,
+ avg_egress_rate = AvgEgressRate,
+ egress_rate_timestamp = Now,
+ out_counter = 0 }).
+
+fetch(State =
+ #vqstate { q4 = Q4, ram_msg_count = RamMsgCount,
+ out_counter = OutCount, prefetcher = Prefetcher,
+ index_state = IndexState, len = Len }) ->
+ case queue:out(Q4) of
+ {empty, _Q4} when Prefetcher == undefined ->
+ fetch_from_q3_or_gamma(State);
+ {empty, _Q4} ->
+ fetch(drain_prefetcher(drain, State));
+ {{value,
+ #alpha { msg = Msg = #basic_message { guid = MsgId,
+ is_persistent = IsPersistent },
+ seq_id = SeqId, is_delivered = IsDelivered,
+ msg_on_disk = MsgOnDisk, index_on_disk = IndexOnDisk }},
+ Q4a} ->
+ {IndexState1, IndexOnDisk1} =
+ case IndexOnDisk of
+ true ->
+ IndexState2 =
+ case IsDelivered of
+ false -> rabbit_queue_index:write_delivered(
+ SeqId, IndexState);
+ true -> IndexState
+ end,
+ case IsPersistent of
+ true -> {IndexState2, true};
+ false -> {rabbit_queue_index:write_acks(
+ [SeqId], IndexState2), false}
+ end;
+ false ->
+ {IndexState, false}
+ end,
+ _MsgOnDisk1 = IndexOnDisk1 =
+ case IndexOnDisk1 of
+ true -> true = IsPersistent, %% ASSERTION
+ true = MsgOnDisk; %% ASSERTION
+ false -> ok = case MsgOnDisk andalso not IsPersistent of
+ true -> rabbit_msg_store:remove([MsgId]);
+ false -> ok
+ end,
+ false
+ end,
+ AckTag = case IndexOnDisk1 of
+ true -> {ack_index_and_store, MsgId, SeqId};
+ false -> ack_not_on_disk
+ end,
+ Len1 = Len - 1,
+ {{Msg, IsDelivered, AckTag, Len1},
+ State #vqstate { q4 = Q4a, out_counter = OutCount + 1,
+ ram_msg_count = RamMsgCount - 1,
+ index_state = IndexState1, len = Len1 }}
+ end.
+
+ack(AckTags, State = #vqstate { index_state = IndexState }) ->
+ {MsgIds, SeqIds} =
+ lists:foldl(
+ fun (ack_not_on_disk, Acc) -> Acc;
+ ({ack_index_and_store, MsgId, SeqId}, {MsgIds, SeqIds}) ->
+ {[MsgId | MsgIds], [SeqId | SeqIds]}
+ end, {[], []}, AckTags),
+ IndexState1 = case SeqIds of
+ [] -> IndexState;
+ _ -> rabbit_queue_index:write_acks(SeqIds, IndexState)
+ end,
+ ok = case MsgIds of
+ [] -> ok;
+ _ -> rabbit_msg_store:remove(MsgIds)
+ end,
+ State #vqstate { index_state = IndexState1 }.
+
+len(#vqstate { len = Len }) ->
+ Len.
+
+is_empty(State) ->
+ 0 == len(State).
+
+maybe_start_prefetcher(State = #vqstate { target_ram_msg_count = 0 }) ->
+ State;
+maybe_start_prefetcher(State = #vqstate { prefetcher = undefined }) ->
+ %% ensure we have as much index in RAM as we can
+ State1 = #vqstate { ram_msg_count = RamMsgCount,
+ target_ram_msg_count = TargetRamMsgCount,
+ q1 = Q1, q3 = Q3 } = maybe_gammas_to_betas(State),
+ case queue:is_empty(Q3) of
+ true -> %% nothing to do
+ State1;
+ false ->
+ %% prefetched content takes priority over q1
+ AvailableSpace =
+ case TargetRamMsgCount of
+ undefined -> queue:len(Q3);
+ _ -> (TargetRamMsgCount - RamMsgCount) + queue:len(Q1)
+ end,
+ PrefetchCount = lists:min([queue:len(Q3), AvailableSpace]),
+ case PrefetchCount =< 0 of
+ true -> State1;
+ false ->
+ {PrefetchQueue, Q3a} = queue:split(PrefetchCount, Q3),
+ {ok, Prefetcher} =
+ rabbit_queue_prefetcher:start_link(PrefetchQueue),
+ State1 #vqstate { q3 = Q3a, prefetcher = Prefetcher }
+ end
+ end;
+maybe_start_prefetcher(State) ->
+ State.
+
+purge(State = #vqstate { prefetcher = undefined, q4 = Q4,
+ index_state = IndexState, len = Len }) ->
+ {Q4Count, IndexState1} = remove_queue_entries(Q4, IndexState),
+ {Len, State1} =
+ purge1(Q4Count, State #vqstate { index_state = IndexState1,
+ q4 = queue:new() }),
+ {Len, State1 #vqstate { len = 0 }};
+purge(State) ->
+ purge(drain_prefetcher(stop, State)).
+
+%% the only difference between purge and delete is that delete also
+%% needs to delete everything that's been delivered and not ack'd.
+delete(State) ->
+ {_PurgeCount, State1 = #vqstate { index_state = IndexState }} = purge(State),
+ IndexState1 =
+ case rabbit_queue_index:find_lowest_seq_id_seg_and_next_seq_id(
+ IndexState) of
+ {N, N, IndexState2} ->
+ IndexState2;
+ {GammaSeqId, NextSeqId, IndexState2} ->
+ {_DeleteCount, IndexState3} =
+ delete1(NextSeqId, 0, GammaSeqId, IndexState2),
+ IndexState3
+ end,
+ IndexState4 = rabbit_queue_index:terminate_and_erase(IndexState1),
+ State1 #vqstate { index_state = IndexState4 }.
+
+%% [{Msg, AckTag}]
+%% We guarantee that after fetch, only persistent msgs are left on
+%% disk. This means that in a requeue, we set
+%% PersistentMsgsAlreadyOnDisk to true, thus avoiding calls to
+%% msg_store:write for persistent msgs. It also means that we don't
+%% need to worry about calling msg_store:remove (as ack would do)
+%% because transient msgs won't be on disk anyway, thus they won't
+%% need to be removed. However, we do call msg_store:release so that
+%% the cache isn't held full of msgs which are now at the tail of the
+%% queue.
+requeue(MsgsWithAckTags, State) ->
+ {SeqIds, MsgIds, State1 = #vqstate { index_state = IndexState }} =
+ lists:foldl(
+ fun ({Msg = #basic_message { guid = MsgId }, AckTag},
+ {SeqIdsAcc, MsgIdsAcc, StateN}) ->
+ {_SeqId, StateN1} = publish(Msg, true, true, StateN),
+ {SeqIdsAcc1, MsgIdsAcc1} =
+ case AckTag of
+ ack_not_on_disk ->
+ {SeqIdsAcc, MsgIdsAcc};
+ {ack_index_and_store, MsgId, SeqId} ->
+ {[SeqId | SeqIdsAcc], [MsgId | MsgIdsAcc]}
+ end,
+ {SeqIdsAcc1, MsgIdsAcc1, StateN1}
+ end, {[], [], State}, MsgsWithAckTags),
+ IndexState1 = case SeqIds of
+ [] -> IndexState;
+ _ -> rabbit_queue_index:write_acks(SeqIds, IndexState)
+ end,
+ ok = case MsgIds of
+ [] -> ok;
+ _ -> rabbit_msg_store:release(MsgIds)
+ end,
+ State1 #vqstate { index_state = IndexState1 }.
+
+tx_publish(Msg = #basic_message { is_persistent = true }, State) ->
+ true = maybe_write_msg_to_disk(true, false, Msg),
+ State;
+tx_publish(_Msg, State) ->
+ State.
+
+tx_rollback(Pubs, State) ->
+ ok = case persistent_msg_ids(Pubs) of
+ [] -> ok;
+ PP -> rabbit_msg_store:remove(PP)
+ end,
+ State.
+
+tx_commit(Pubs, AckTags, From, State) ->
+ case persistent_msg_ids(Pubs) of
+ [] ->
+ {true, tx_commit_from_msg_store(Pubs, AckTags, From, State)};
+ PersistentMsgIds ->
+ Self = self(),
+ ok = rabbit_msg_store:sync(
+ PersistentMsgIds,
+ fun () -> ok = rabbit_amqqueue:tx_commit_msg_store_callback(
+ Self, Pubs, AckTags, From)
+ end),
+ {false, State}
+ end.
+
+tx_commit_from_msg_store(Pubs, AckTags, From,
+ State = #vqstate { on_sync = {SAcks, SPubs, SFroms} }) ->
+ DiskAcks =
+ lists:filter(fun (AckTag) -> AckTag /= ack_not_on_disk end, AckTags),
+ State #vqstate { on_sync = { [DiskAcks | SAcks],
+ [Pubs | SPubs],
+ [From | SFroms] }}.
+
+tx_commit_from_vq(State = #vqstate { on_sync = {SAcks, SPubs, SFroms} }) ->
+ State1 = ack(lists:flatten(SAcks), State),
+ {PubSeqIds, State2 = #vqstate { index_state = IndexState }} =
+ lists:foldl(
+ fun (Msg = #basic_message { is_persistent = IsPersistent },
+ {SeqIdsAcc, StateN}) ->
+ {SeqId, StateN1} = publish(Msg, false, true, StateN),
+ SeqIdsAcc1 = case IsPersistent of
+ true -> [SeqId | SeqIdsAcc];
+ false -> SeqIdsAcc
+ end,
+ {SeqIdsAcc1, StateN1}
+ end, {[], State1}, lists:flatten(lists:reverse(SPubs))),
+ IndexState1 =
+ rabbit_queue_index:sync_seq_ids(PubSeqIds, [] /= SAcks, IndexState),
+ [ gen_server2:reply(From, ok) || From <- lists:reverse(SFroms) ],
+ State2 #vqstate { index_state = IndexState1, on_sync = {[], [], []} }.
+
+needs_sync(#vqstate { on_sync = {_, _, []} }) ->
+ false;
+needs_sync(_) ->
+ true.
+
+can_flush_journal(#vqstate { index_state = IndexState }) ->
+ rabbit_queue_index:can_flush_journal(IndexState).
+
+flush_journal(State = #vqstate { index_state = IndexState }) ->
+ State #vqstate { index_state =
+ rabbit_queue_index:flush_journal(IndexState) }.
+
+status(#vqstate { q1 = Q1, q2 = Q2, gamma = Gamma, q3 = Q3, q4 = Q4,
+ len = Len, on_sync = {_, _, From},
+ target_ram_msg_count = TargetRamMsgCount,
+ ram_msg_count = RamMsgCount, prefetcher = Prefetcher,
+ avg_egress_rate = AvgEgressRate }) ->
+ [ {q1, queue:len(Q1)},
+ {q2, queue:len(Q2)},
+ {gamma, Gamma},
+ {q3, queue:len(Q3)},
+ {q4, queue:len(Q4)},
+ {len, Len},
+ {outstanding_txns, length(From)},
+ {target_ram_msg_count, TargetRamMsgCount},
+ {ram_msg_count, RamMsgCount},
+ {avg_egress_rate, AvgEgressRate},
+ {prefetching, Prefetcher /= undefined} ].
+
+%%----------------------------------------------------------------------------
+%% Minor helpers
+%%----------------------------------------------------------------------------
+
+persistent_msg_ids(Pubs) ->
+ [MsgId || Obj = #basic_message { guid = MsgId } <- Pubs,
+ Obj #basic_message.is_persistent].
+
+entry_salient_details(#alpha { msg = #basic_message { guid = MsgId },
+ seq_id = SeqId, is_delivered = IsDelivered,
+ msg_on_disk = MsgOnDisk,
+ index_on_disk = IndexOnDisk }) ->
+ {MsgId, SeqId, IsDelivered, MsgOnDisk, IndexOnDisk};
+entry_salient_details(#beta { msg_id = MsgId, seq_id = SeqId,
+ is_delivered = IsDelivered,
+ index_on_disk = IndexOnDisk }) ->
+ {MsgId, SeqId, IsDelivered, true, IndexOnDisk}.
+
+betas_from_segment_entries(List) ->
+ queue:from_list([#beta { msg_id = MsgId, seq_id = SeqId,
+ is_persistent = IsPersistent,
+ is_delivered = IsDelivered,
+ index_on_disk = true }
+ || {MsgId, SeqId, IsPersistent, IsDelivered} <- List]).
+
+read_index_segment(SeqId, IndexState) ->
+ SeqId1 = SeqId + rabbit_queue_index:segment_size(),
+ case rabbit_queue_index:read_segment_entries(SeqId, IndexState) of
+ {[], IndexState1} -> read_index_segment(SeqId1, IndexState1);
+ {List, IndexState1} -> {List, IndexState1, SeqId1}
+ end.
+
+ensure_binary_properties(Msg = #basic_message { content = Content }) ->
+ Msg #basic_message {
+ content = rabbit_binary_parser:clear_decoded_content(
+ rabbit_binary_generator:ensure_content_encoded(Content)) }.
+
+%% the first arg is the older gamma
+combine_gammas(#gamma { count = 0 }, #gamma { count = 0 }) ->
+ #gamma { seq_id = undefined, count = 0 };
+combine_gammas(#gamma { count = 0 }, #gamma { } = B) -> B;
+combine_gammas(#gamma { } = A, #gamma { count = 0 }) -> A;
+combine_gammas(#gamma { seq_id = SeqIdLow, count = CountLow },
+ #gamma { seq_id = SeqIdHigh, count = CountHigh}) ->
+ true = SeqIdLow =< SeqIdHigh, %% ASSERTION
+ #gamma { seq_id = SeqIdLow, count = CountLow + CountHigh}.
+
+%%----------------------------------------------------------------------------
+%% Internal major helpers for Public API
+%%----------------------------------------------------------------------------
+
+delete1(NextSeqId, Count, GammaSeqId, IndexState)
+ when GammaSeqId >= NextSeqId ->
+ {Count, IndexState};
+delete1(NextSeqId, Count, GammaSeqId, IndexState) ->
+ Gamma1SeqId = GammaSeqId + rabbit_queue_index:segment_size(),
+ case rabbit_queue_index:read_segment_entries(GammaSeqId, IndexState) of
+ {[], IndexState1} ->
+ delete1(NextSeqId, Count, Gamma1SeqId, IndexState1);
+ {List, IndexState1} ->
+ Q = betas_from_segment_entries(List),
+ {QCount, IndexState2} = remove_queue_entries(Q, IndexState1),
+ delete1(NextSeqId, Count + QCount, Gamma1SeqId, IndexState2)
+ end.
+
+purge1(Count, State = #vqstate { q3 = Q3, index_state = IndexState }) ->
+ case queue:is_empty(Q3) of
+ true ->
+ {Q1Count, IndexState1} =
+ remove_queue_entries(State #vqstate.q1, IndexState),
+ {Count + Q1Count, State #vqstate { q1 = queue:new(),
+ index_state = IndexState1 }};
+ false ->
+ {Q3Count, IndexState1} = remove_queue_entries(Q3, IndexState),
+ purge1(Count + Q3Count,
+ maybe_gammas_to_betas(
+ State #vqstate { index_state = IndexState1,
+ q3 = queue:new() }))
+ end.
+
+remove_queue_entries(Q, IndexState) ->
+ {Count, MsgIds, SeqIds, IndexState1} =
+ lists:foldl(
+ fun (Entry, {CountN, MsgIdsAcc, SeqIdsAcc, IndexStateN}) ->
+ {MsgId, SeqId, IsDelivered, MsgOnDisk, IndexOnDisk} =
+ entry_salient_details(Entry),
+ IndexStateN1 = case IndexOnDisk andalso not IsDelivered of
+ true -> rabbit_queue_index:write_delivered(
+ SeqId, IndexStateN);
+ false -> IndexStateN
+ end,
+ SeqIdsAcc1 = case IndexOnDisk of
+ true -> [SeqId | SeqIdsAcc];
+ false -> SeqIdsAcc
+ end,
+ MsgIdsAcc1 = case MsgOnDisk of
+ true -> [MsgId | MsgIdsAcc];
+ false -> MsgIdsAcc
+ end,
+ {CountN + 1, MsgIdsAcc1, SeqIdsAcc1, IndexStateN1}
+ %% we need to write the delivered records in order otherwise
+ %% we upset the qi. So don't reverse.
+ end, {0, [], [], IndexState}, queue:to_list(Q)),
+ ok = case MsgIds of
+ [] -> ok;
+ _ -> rabbit_msg_store:remove(MsgIds)
+ end,
+ IndexState2 =
+ case SeqIds of
+ [] -> IndexState1;
+ _ -> rabbit_queue_index:write_acks(SeqIds, IndexState1)
+ end,
+ {Count, IndexState2}.
+
+fetch_from_q3_or_gamma(State = #vqstate {
+ q1 = Q1, q2 = Q2, gamma = #gamma { count = GammaCount },
+ q3 = Q3, q4 = Q4, ram_msg_count = RamMsgCount }) ->
+ case queue:out(Q3) of
+ {empty, _Q3} ->
+ 0 = GammaCount, %% ASSERTION
+ true = queue:is_empty(Q2), %% ASSERTION
+ true = queue:is_empty(Q1), %% ASSERTION
+ {empty, State};
+ {{value,
+ #beta { msg_id = MsgId, seq_id = SeqId, is_delivered = IsDelivered,
+ is_persistent = IsPersistent, index_on_disk = IndexOnDisk }},
+ Q3a} ->
+ {ok, Msg = #basic_message { is_persistent = IsPersistent,
+ guid = MsgId }} =
+ rabbit_msg_store:read(MsgId),
+ Q4a = queue:in(
+ #alpha { msg = Msg, seq_id = SeqId,
+ is_delivered = IsDelivered, msg_on_disk = true,
+ index_on_disk = IndexOnDisk }, Q4),
+ State1 = State #vqstate { q3 = Q3a, q4 = Q4a,
+ ram_msg_count = RamMsgCount + 1 },
+ State2 =
+ case {queue:is_empty(Q3a), 0 == GammaCount} of
+ {true, true} ->
+ %% q3 is now empty, it wasn't before; gamma is
+ %% still empty. So q2 must be empty, and q1
+ %% can now be joined onto q4
+ true = queue:is_empty(Q2), %% ASSERTION
+ State1 #vqstate { q1 = queue:new(),
+ q4 = queue:join(Q4a, Q1) };
+ {true, false} ->
+ maybe_gammas_to_betas(State1);
+ {false, _} ->
+ %% q3 still isn't empty, we've not touched
+ %% gamma, so the invariants between q1, q2,
+ %% gamma and q3 are maintained
+ State1
+ end,
+ fetch(State2)
+ end.
+
+drain_prefetcher(_DrainOrStop, State = #vqstate { prefetcher = undefined }) ->
+ State;
+drain_prefetcher(DrainOrStop,
+ State = #vqstate { prefetcher = Prefetcher, q1 = Q1, q2 = Q2,
+ gamma = #gamma { count = GammaCount },
+ q3 = Q3, q4 = Q4,
+ ram_msg_count = RamMsgCount }) ->
+ Fun = case DrainOrStop of
+ drain -> fun rabbit_queue_prefetcher:drain/1;
+ stop -> fun rabbit_queue_prefetcher:drain_and_stop/1
+ end,
+ {Q3a, Q4a, Prefetcher1, RamMsgCountAdj} =
+ case Fun(Prefetcher) of
+ {empty, Betas} -> %% drain or drain_and_stop
+ {queue:join(Betas, Q3), Q4, undefined, 0};
+ {finished, Alphas} -> %% just drain
+ {Q3, queue:join(Q4, Alphas), undefined, queue:len(Alphas)};
+ {continuing, Alphas} -> %% just drain
+ {Q3, queue:join(Q4, Alphas), Prefetcher, queue:len(Alphas)};
+ {Alphas, Betas} -> %% just drain_and_stop
+ {queue:join(Betas, Q3), queue:join(Q4, Alphas), undefined,
+ queue:len(Alphas)}
+ end,
+ State1 = State #vqstate { prefetcher = Prefetcher1, q3 = Q3a, q4 = Q4a,
+ ram_msg_count = RamMsgCount + RamMsgCountAdj },
+ %% don't join up with q1/q2 unless the prefetcher has stopped
+ State2 = case GammaCount == 0 andalso Prefetcher1 == undefined of
+ true -> case queue:is_empty(Q3a) andalso queue:is_empty(Q2) of
+ true ->
+ State1 #vqstate { q1 = queue:new(),
+ q4 = queue:join(Q4a, Q1) };
+ false ->
+ State1 #vqstate { q3 = queue:join(Q3a, Q2) }
+ end;
+ false -> State1
+ end,
+ maybe_push_q1_to_betas(State2).
+
+reduce_memory_use(State = #vqstate { ram_msg_count = RamMsgCount,
+ target_ram_msg_count = TargetRamMsgCount })
+ when TargetRamMsgCount == undefined orelse TargetRamMsgCount >= RamMsgCount ->
+ State;
+reduce_memory_use(State =
+ #vqstate { target_ram_msg_count = TargetRamMsgCount }) ->
+ %% strictly, it's not necessary to stop the prefetcher this early,
+ %% but because of its potential effect on q1 and the
+ %% ram_msg_count, it's just much simpler to stop it sooner and
+ %% relaunch when we next hibernate.
+ State1 = maybe_push_q4_to_betas(maybe_push_q1_to_betas(
+ drain_prefetcher(stop, State))),
+ case TargetRamMsgCount of
+ 0 -> push_betas_to_gammas(State1);
+ _ -> State1
+ end.
+
+%%----------------------------------------------------------------------------
+%% Internal gubbins for publishing
+%%----------------------------------------------------------------------------
+
+test_keep_msg_in_ram(SeqId, #vqstate { target_ram_msg_count = TargetRamMsgCount,
+ ram_msg_count = RamMsgCount,
+ q1 = Q1, q3 = Q3 }) ->
+ case TargetRamMsgCount of
+ undefined ->
+ msg;
+ 0 ->
+ case queue:out(Q3) of
+ {empty, _Q3} ->
+ %% if TargetRamMsgCount == 0, we know we have no
+ %% alphas. If q3 is empty then gamma must be empty
+ %% too, so create a beta, which should end up in
+ %% q3
+ index;
+ {{value, #beta { seq_id = OldSeqId }}, _Q3a} ->
+ %% Don't look at the current gamma as it may be
+ %% empty. If the SeqId is still within the current
+ %% segment, it'll be a beta, else it'll go into
+ %% gamma
+ case SeqId >= rabbit_queue_index:next_segment_boundary(OldSeqId) of
+ true -> neither;
+ false -> index
+ end
+ end;
+ _ when TargetRamMsgCount > RamMsgCount ->
+ msg;
+ _ ->
+ case queue:is_empty(Q1) of
+ true -> index;
+ false -> msg %% can push out elders to disk
+ end
+ end.
+
+publish(Msg, IsDelivered, PersistentMsgsAlreadyOnDisk,
+ State = #vqstate { next_seq_id = SeqId, len = Len }) ->
+ {SeqId, publish(test_keep_msg_in_ram(SeqId, State), Msg, SeqId, IsDelivered,
+ PersistentMsgsAlreadyOnDisk,
+ State #vqstate { next_seq_id = SeqId + 1, len = Len + 1 })}.
+
+publish(msg, Msg = #basic_message { guid = MsgId,
+ is_persistent = IsPersistent },
+ SeqId, IsDelivered, PersistentMsgsAlreadyOnDisk,
+ State = #vqstate { index_state = IndexState,
+ ram_msg_count = RamMsgCount }) ->
+ MsgOnDisk =
+ maybe_write_msg_to_disk(false, PersistentMsgsAlreadyOnDisk, Msg),
+ {IndexOnDisk, IndexState1} =
+ maybe_write_index_to_disk(false, IsPersistent, MsgId, SeqId,
+ IsDelivered, IndexState),
+ Entry = #alpha { msg = Msg, seq_id = SeqId, is_delivered = IsDelivered,
+ msg_on_disk = MsgOnDisk, index_on_disk = IndexOnDisk },
+ State1 = State #vqstate { ram_msg_count = RamMsgCount + 1,
+ index_state = IndexState1 },
+ store_alpha_entry(Entry, State1);
+
+publish(index, Msg = #basic_message { guid = MsgId,
+ is_persistent = IsPersistent },
+ SeqId, IsDelivered, PersistentMsgsAlreadyOnDisk,
+ State = #vqstate { index_state = IndexState, q1 = Q1 }) ->
+ true = maybe_write_msg_to_disk(true, PersistentMsgsAlreadyOnDisk, Msg),
+ {IndexOnDisk, IndexState1} =
+ maybe_write_index_to_disk(false, IsPersistent, MsgId, SeqId,
+ IsDelivered, IndexState),
+ Entry = #beta { msg_id = MsgId, seq_id = SeqId, is_delivered = IsDelivered,
+ is_persistent = IsPersistent, index_on_disk = IndexOnDisk },
+ State1 = State #vqstate { index_state = IndexState1 },
+ true = queue:is_empty(Q1), %% ASSERTION
+ store_beta_entry(Entry, State1);
+
+publish(neither, Msg = #basic_message { guid = MsgId,
+ is_persistent = IsPersistent },
+ SeqId, IsDelivered, PersistentMsgsAlreadyOnDisk,
+ State = #vqstate { index_state = IndexState, q1 = Q1, q2 = Q2,
+ gamma = Gamma }) ->
+ true = maybe_write_msg_to_disk(true, PersistentMsgsAlreadyOnDisk, Msg),
+ {true, IndexState1} =
+ maybe_write_index_to_disk(true, IsPersistent, MsgId, SeqId,
+ IsDelivered, IndexState),
+ true = queue:is_empty(Q1) andalso queue:is_empty(Q2), %% ASSERTION
+ %% gamma may be empty, seq_id > next_segment_boundary from q3
+ %% head, so we need to find where the segment boundary is before
+ %% or equal to seq_id
+ GammaSeqId = rabbit_queue_index:next_segment_boundary(SeqId) -
+ rabbit_queue_index:segment_size(),
+ Gamma1 = #gamma { seq_id = GammaSeqId, count = 1 },
+ State #vqstate { index_state = IndexState1,
+ gamma = combine_gammas(Gamma, Gamma1) }.
+
+store_alpha_entry(Entry = #alpha {}, State =
+ #vqstate { q1 = Q1, q2 = Q2,
+ gamma = #gamma { count = GammaCount },
+ q3 = Q3, q4 = Q4, prefetcher = Prefetcher }) ->
+ case queue:is_empty(Q2) andalso GammaCount == 0 andalso
+ queue:is_empty(Q3) andalso Prefetcher == undefined of
+ true ->
+ State #vqstate { q4 = queue:in(Entry, Q4) };
+ false ->
+ maybe_push_q1_to_betas(State #vqstate { q1 = queue:in(Entry, Q1) })
+ end.
+
+store_beta_entry(Entry = #beta {}, State =
+ #vqstate { q2 = Q2, gamma = #gamma { count = GammaCount },
+ q3 = Q3 }) ->
+ case GammaCount == 0 of
+ true -> State #vqstate { q3 = queue:in(Entry, Q3) };
+ false -> State #vqstate { q2 = queue:in(Entry, Q2) }
+ end.
+
+%% Bool IsPersistent PersistentMsgsAlreadyOnDisk | WriteToDisk?
+%% -----------------------------------------------+-------------
+%% false false false | false 1
+%% false true false | true 2
+%% false false true | false 3
+%% false true true | false 4
+%% true false false | true 5
+%% true true false | true 6
+%% true false true | true 7
+%% true true true | false 8
+
+%% (Bool and not (IsPersistent and PersistentMsgsAlreadyOnDisk)) or | 5 6 7
+%% (IsPersistent and (not PersistentMsgsAlreadyOnDisk)) | 2 6
+maybe_write_msg_to_disk(Bool, PersistentMsgsAlreadyOnDisk,
+ Msg = #basic_message { guid = MsgId,
+ is_persistent = IsPersistent })
+ when (Bool andalso not (IsPersistent andalso PersistentMsgsAlreadyOnDisk))
+ orelse (IsPersistent andalso not PersistentMsgsAlreadyOnDisk) ->
+ ok = rabbit_msg_store:write(MsgId, ensure_binary_properties(Msg)),
+ true;
+maybe_write_msg_to_disk(_Bool, true, #basic_message { is_persistent = true }) ->
+ true;
+maybe_write_msg_to_disk(_Bool, _PersistentMsgsAlreadyOnDisk, _Msg) ->
+ false.
+
+maybe_write_index_to_disk(Bool, IsPersistent, MsgId, SeqId, IsDelivered,
+ IndexState) when Bool orelse IsPersistent ->
+ IndexState1 = rabbit_queue_index:write_published(
+ MsgId, SeqId, IsPersistent, IndexState),
+ {true, case IsDelivered of
+ true -> rabbit_queue_index:write_delivered(SeqId, IndexState1);
+ false -> IndexState1
+ end};
+maybe_write_index_to_disk(_Bool, _IsPersistent, _MsgId, _SeqId, _IsDelivered,
+ IndexState) ->
+ {false, IndexState}.
+
+%%----------------------------------------------------------------------------
+%% Phase changes
+%%----------------------------------------------------------------------------
+
+maybe_gammas_to_betas(State = #vqstate { gamma = #gamma { count = 0 } }) ->
+ State;
+maybe_gammas_to_betas(State =
+ #vqstate { index_state = IndexState, q2 = Q2, q3 = Q3,
+ target_ram_msg_count = TargetRamMsgCount,
+ gamma = #gamma { seq_id = GammaSeqId,
+ count = GammaCount }}) ->
+ case (not queue:is_empty(Q3)) andalso 0 == TargetRamMsgCount of
+ true ->
+ State;
+ false ->
+ %% either q3 is empty, in which case we load at least one
+ %% segment, or TargetRamMsgCount > 0, meaning we should
+ %% really be holding all the betas in memory.
+ {List, IndexState1, Gamma1SeqId} =
+ read_index_segment(GammaSeqId, IndexState),
+ State1 = State #vqstate { index_state = IndexState1 },
+ %% length(List) may be < segment_size because of acks. But
+ %% it can't be []
+ Q3a = queue:join(Q3, betas_from_segment_entries(List)),
+ case GammaCount - length(List) of
+ 0 ->
+ %% gamma is now empty, but it wasn't before, so
+ %% can now join q2 onto q3
+ State1 #vqstate { gamma = #gamma { seq_id = undefined,
+ count = 0 },
+ q2 = queue:new(),
+ q3 = queue:join(Q3a, Q2) };
+ N when N > 0 ->
+ maybe_gammas_to_betas(
+ State1 #vqstate { q3 = Q3a,
+ gamma = #gamma { seq_id = Gamma1SeqId,
+ count = N } })
+ end
+ end.
+
+maybe_push_q1_to_betas(State = #vqstate { q1 = Q1 }) ->
+ maybe_push_alphas_to_betas(
+ fun queue:out/1,
+ fun (Beta, Q1a, State1) ->
+ %% these could legally go to q3 if gamma and q2 are empty
+ store_beta_entry(Beta, State1 #vqstate { q1 = Q1a })
+ end, Q1, State).
+
+maybe_push_q4_to_betas(State = #vqstate { q4 = Q4 }) ->
+ maybe_push_alphas_to_betas(
+ fun queue:out_r/1,
+ fun (Beta, Q4a, State1 = #vqstate { q3 = Q3 }) ->
+ %% these must go to q3
+ State1 #vqstate { q3 = queue:in_r(Beta, Q3), q4 = Q4a }
+ end, Q4, State).
+
+maybe_push_alphas_to_betas(_Generator, _Consumer, _Q, State =
+ #vqstate { ram_msg_count = RamMsgCount,
+ target_ram_msg_count = TargetRamMsgCount })
+ when TargetRamMsgCount == undefined orelse TargetRamMsgCount >= RamMsgCount ->
+ State;
+maybe_push_alphas_to_betas(Generator, Consumer, Q, State =
+ #vqstate { ram_msg_count = RamMsgCount }) ->
+ case Generator(Q) of
+ {empty, _Q} -> State;
+ {{value,
+ #alpha { msg = Msg = #basic_message { guid = MsgId,
+ is_persistent = IsPersistent },
+ seq_id = SeqId, is_delivered = IsDelivered,
+ index_on_disk = IndexOnDisk }},
+ Qa} ->
+ true = maybe_write_msg_to_disk(true, true, Msg),
+ Beta = #beta { msg_id = MsgId, seq_id = SeqId,
+ is_persistent = IsPersistent,
+ is_delivered = IsDelivered,
+ index_on_disk = IndexOnDisk },
+ State1 = State #vqstate { ram_msg_count = RamMsgCount - 1 },
+ maybe_push_alphas_to_betas(Generator, Consumer, Qa,
+ Consumer(Beta, Qa, State1))
+ end.
+
+push_betas_to_gammas(State = #vqstate { q2 = Q2, gamma = Gamma, q3 = Q3,
+ index_state = IndexState }) ->
+ %% HighSeqId is high in the sense that it must be higher than the
+ %% seq_id in Gamma, but it's also the lowest of the betas that we
+ %% transfer from q2 to gamma.
+ {HighSeqId, Len1, Q2a, IndexState1} =
+ push_betas_to_gammas(fun queue:out/1, undefined, Q2, IndexState),
+ Gamma1 = #gamma { seq_id = Gamma1SeqId } =
+ combine_gammas(Gamma, #gamma { seq_id = HighSeqId, count = Len1 }),
+ State1 = State #vqstate { q2 = Q2a, gamma = Gamma1,
+ index_state = IndexState1 },
+ case queue:out(Q3) of
+ {empty, _Q3} -> State1;
+ {{value, #beta { seq_id = SeqId }}, _Q3a} ->
+ Limit = rabbit_queue_index:next_segment_boundary(SeqId),
+ case Gamma1SeqId of
+ Limit -> %% already only holding the minimum, nothing to do
+ State1;
+ _ when Gamma1SeqId == undefined orelse
+ (is_integer(Gamma1SeqId) andalso Gamma1SeqId > Limit) ->
+ %% ASSERTION (sadly large!)
+ %% This says that if Gamma1SeqId /= undefined then
+ %% the gap from Limit to Gamma1SeqId is an integer
+ %% multiple of segment_size
+ 0 = case Gamma1SeqId of
+ undefined -> 0;
+ _ -> (Gamma1SeqId - Limit) rem
+ rabbit_queue_index:segment_size()
+ end,
+ %% LowSeqId is low in the sense that it must be
+ %% lower than the seq_id in gamma1, in fact either
+ %% gamma1 has undefined as its seq_id or there
+ %% does not exist a seq_id X s.t. X > LowSeqId and
+ %% X < gamma1's seq_id (would be +1 if it wasn't
+ %% for the possibility of gaps in the seq_ids).
+ %% But because we use queue:out_r, LowSeqId is
+ %% actually also the highest seq_id of the betas we
+ %% transfer from q3 to gammas.
+ {LowSeqId, Len2, Q3b, IndexState2} =
+ push_betas_to_gammas(fun queue:out_r/1, Limit, Q3,
+ IndexState1),
+ true = Gamma1SeqId > LowSeqId, %% ASSERTION
+ Gamma2 = combine_gammas(
+ #gamma { seq_id = Limit, count = Len2}, Gamma1),
+ State1 #vqstate { q3 = Q3b, gamma = Gamma2,
+ index_state = IndexState2 }
+ end
+ end.
+
+push_betas_to_gammas(Generator, Limit, Q, IndexState) ->
+ case Generator(Q) of
+ {empty, Qa} -> {undefined, 0, Qa, IndexState};
+ {{value, #beta { seq_id = SeqId }}, _Qa} ->
+ {Count, Qb, IndexState1} =
+ push_betas_to_gammas(Generator, Limit, Q, 0, IndexState),
+ {SeqId, Count, Qb, IndexState1}
+ end.
+
+push_betas_to_gammas(Generator, Limit, Q, Count, IndexState) ->
+ case Generator(Q) of
+ {empty, Qa} -> {Count, Qa, IndexState};
+ {{value, #beta { seq_id = SeqId }}, _Qa}
+ when Limit /= undefined andalso SeqId < Limit ->
+ {Count, Q, IndexState};
+ {{value, #beta { msg_id = MsgId, seq_id = SeqId,
+ is_persistent = IsPersistent,
+ is_delivered = IsDelivered,
+ index_on_disk = IndexOnDisk}}, Qa} ->
+ IndexState1 =
+ case IndexOnDisk of
+ true -> IndexState;
+ false ->
+ {true, IndexState2} =
+ maybe_write_index_to_disk(
+ true, IsPersistent, MsgId,
+ SeqId, IsDelivered, IndexState),
+ IndexState2
+ end,
+ push_betas_to_gammas(Generator, Limit, Qa, Count + 1, IndexState1)
+ end.