-
Notifications
You must be signed in to change notification settings - Fork 43
Restart closed queues #1345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Restart closed queues #1345
Changes from all commits
abca46b
6ac972b
2b5ec11
5004a0a
d713559
019cf04
57c7710
f309b45
4e064e5
54a954a
80a4151
c97872f
79084ec
03d1cf4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -219,6 +219,121 @@ describe LavinMQ::AMQP::Queue do | |
| end | ||
| end | ||
|
|
||
| describe "Restarting queues" do | ||
| q_name = "restart" | ||
| it "should restart a closed queue" do | ||
| with_amqp_server do |s| | ||
| with_channel(s) do |ch| | ||
| q = ch.queue(q_name, durable: true) | ||
| queue = s.vhosts["/"].queues[q_name].as(LavinMQ::AMQP::DurableQueue) | ||
|
|
||
| # Publish a message | ||
| q.publish_confirm "test message" | ||
| queue.message_count.should eq 1 | ||
|
|
||
| # Close the queue | ||
| queue.close | ||
| queue.closed?.should be_true | ||
|
|
||
| # Restart the queue & verify | ||
| queue.restart!.should be_true | ||
| queue.closed?.should be_false | ||
| queue.message_count.should eq 1 | ||
| msg = q.get(no_ack: true) | ||
| msg.should_not be_nil | ||
| msg.not_nil!.body_io.to_s.should eq "test message" | ||
| end | ||
| end | ||
| end | ||
|
|
||
| it "should restart after corrupt data closes the queue" do | ||
| with_amqp_server do |s| | ||
| vhost = s.vhosts.create("restart_vhost") | ||
| with_channel(s, vhost: vhost.name) do |ch| | ||
| q = ch.queue(q_name, durable: true) | ||
| queue = vhost.queues[q_name].as(LavinMQ::AMQP::DurableQueue) | ||
| q.publish_confirm "test message" | ||
| queue.message_count.should eq 1 | ||
|
|
||
| # Write corrupt data to the segment file | ||
| mfile = queue.@[email protected]_value | ||
| File.open(mfile.path, "w+") do |f| | ||
| f.seek(mfile.size - mfile.size + 4) | ||
| f.write(("x"*10).to_slice) | ||
| end | ||
|
|
||
| # Try to consume, which will trigger the close due to corrupt data | ||
| q.subscribe(tag: "tag", no_ack: false, &.ack) | ||
| should_eventually(be_true) { queue.state.closed? } | ||
|
|
||
| # Delete corrupted segment file | ||
| File.delete(mfile.path) | ||
|
|
||
| # Restart the queue & verify that it is running | ||
| queue.restart!.should be_true | ||
| queue.closed?.should be_false | ||
| queue.state.running?.should be_true | ||
| queue.message_count.should eq 0 | ||
| end | ||
| end | ||
| end | ||
|
|
||
| it "should expire msgs after restarting a queue" do | ||
| with_amqp_server do |s| | ||
| with_channel(s) do |ch| | ||
| q = ch.queue(q_name, durable: true, args: AMQP::Client::Arguments.new( | ||
| {"x-message-ttl" => 500, "x-dead-letter-exchange" => "", "x-dead-letter-routing-key" => "dlq"} | ||
| )) | ||
| queue = s.vhosts["/"].queues[q_name].as(LavinMQ::AMQP::DurableQueue) | ||
|
|
||
| # Publish a message | ||
| q.publish_confirm "test message" | ||
| queue.message_count.should eq 1 | ||
|
|
||
| # Close the queue | ||
| queue.close | ||
| queue.closed?.should be_true | ||
|
|
||
| # Restart the queue & verify | ||
| queue.restart!.should be_true | ||
| queue.closed?.should be_false | ||
| queue.message_count.should eq 1 | ||
| should_eventually(be_true) { queue.message_count == 0 } | ||
| end | ||
| end | ||
| end | ||
|
|
||
| it "should expire queue after restarting a queue" do | ||
| with_amqp_server do |s| | ||
| with_channel(s) do |ch| | ||
| ch.queue(q_name, durable: true, args: AMQP::Client::Arguments.new({"x-expires" => 100})) | ||
| queue = s.vhosts["/"].queues[q_name].as(LavinMQ::AMQP::DurableQueue) | ||
|
|
||
| # Close the queue | ||
| queue.close | ||
| queue.closed?.should be_true | ||
|
|
||
| # Restart the queue & verify | ||
| queue.restart!.should be_true | ||
| queue.closed?.should be_false | ||
| should_eventually(be_true) { queue.closed? } | ||
| end | ||
| end | ||
| end | ||
|
|
||
| it "should not restart if queue is still running" do | ||
| with_amqp_server do |s| | ||
| with_channel(s) do |ch| | ||
| ch.queue(q_name, durable: true) | ||
| queue = s.vhosts["/"].queues[q_name].as(LavinMQ::AMQP::DurableQueue) | ||
|
|
||
| # Try to restart without closing | ||
| queue.restart!.should be_false | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| describe "Purge" do | ||
| x_name = "purge" | ||
| q_name = "purge" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -182,21 +182,48 @@ module LavinMQ::AMQP | |
| @metadata = ::Log::Metadata.new(nil, {queue: @name, vhost: @vhost.name}) | ||
| @log = Logger.new(Log, @metadata) | ||
| File.open(File.join(@data_dir, ".queue"), "w") { |f| f.sync = true; f.print @name } | ||
| if File.exists?(File.join(@data_dir, ".paused")) # Migrate '.paused' files to 'paused' | ||
| File.rename(File.join(@data_dir, ".paused"), File.join(@data_dir, "paused")) | ||
| end | ||
| if File.exists?(File.join(@data_dir, "paused")) | ||
| @state = QueueState::Paused | ||
| @paused.set(true) | ||
| end | ||
| @msg_store = init_msg_store(@data_dir) | ||
| @empty = @msg_store.empty | ||
| start | ||
| end | ||
|
|
||
| private def start : Bool | ||
| if @msg_store.closed | ||
| close | ||
| !close | ||
| else | ||
| if File.exists?(File.join(@data_dir, ".paused")) # Migrate '.paused' files to 'paused' | ||
| File.rename(File.join(@data_dir, ".paused"), File.join(@data_dir, "paused")) | ||
| end | ||
| if File.exists?(File.join(@data_dir, "paused")) | ||
| @state = QueueState::Paused | ||
| @paused.set(true) | ||
| end | ||
| handle_arguments | ||
| spawn queue_expire_loop, name: "Queue#queue_expire_loop #{@vhost.name}/#{@name}" if @expires | ||
| spawn message_expire_loop, name: "Queue#message_expire_loop #{@vhost.name}/#{@name}" | ||
| true | ||
| end | ||
| end | ||
|
|
||
| def restart! : Bool | ||
| return false unless @closed | ||
| reset_queue_state | ||
| @msg_store = init_msg_store(@data_dir) | ||
| @empty = @msg_store.empty | ||
| handle_arguments | ||
| spawn queue_expire_loop, name: "Queue#queue_expire_loop #{@vhost.name}/#{@name}" if @expires | ||
| spawn message_expire_loop, name: "Queue#message_expire_loop #{@vhost.name}/#{@name}" | ||
| start | ||
| end | ||
|
|
||
| private def reset_queue_state | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel that we already have a lot of state to keep track of, while this doesn't add more states it adds more places where we need to keep track of all the states for a queue.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, there is a lot to keep track of. I guess another option would be to delete the queue object in VHost and recreate it, but I think that just moves the complexity to VHost instead. |
||
| @closed = false | ||
| @state = QueueState::Running | ||
| # Recreate channels that were closed | ||
| @queue_expiration_ttl_change = ::Channel(Nil).new | ||
| @message_ttl_change = ::Channel(Nil).new | ||
| @paused = BoolChannel.new(false) | ||
viktorerlingsson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| @consumers_empty = BoolChannel.new(true) | ||
| @single_active_consumer_change = ::Channel(Client::Channel::Consumer).new | ||
| @unacked_count.set(0u32, :relaxed) | ||
| @unacked_bytesize.set(0u64, :relaxed) | ||
| end | ||
|
|
||
| # own method so that it can be overriden in other queue implementations | ||
|
|
@@ -390,6 +417,9 @@ module LavinMQ::AMQP | |
| @msg_store_lock.synchronize do | ||
| @msg_store.close | ||
| end | ||
| @deliveries.clear | ||
| @basic_get_unacked.clear | ||
| @deduper = nil | ||
| # TODO: When closing due to ReadError, queue is deleted if exclusive | ||
| delete if !durable? || @exclusive | ||
| Fiber.yield | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.