falqon 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ba4e39b3f74379b319b0946fd639ecb58fe7306f2f062edbeca42ad7cb3224f
4
- data.tar.gz: 91aa2ebc4e44d890ad6343b85960d968aa33b10ef158a393f2119e8561db75d4
3
+ metadata.gz: 315b92a750d6358553f6b0a46623c016f871e556eb9c787fe154454cd426ef56
4
+ data.tar.gz: c0fda1f5f23662c9c0ff2316bdf976fec620fb3f4b8a3be29c79fd65b5da5ac9
5
5
  SHA512:
6
- metadata.gz: 6ebe08c3284edf73c79a37a0fb909dec8ce12cb52790426b0aeaad175000255204b09c1ead7834eac35872893825d43128b18567d18b7e9c3a919aef00e48c40
7
- data.tar.gz: a738b12bdcf0776ee1008cf833e87477e08d6a551c6881cbe92945ea9499e0830fd81908beaa6ca184809ca32a14350fc960fda728a47d365b01d43434c52e1a
6
+ metadata.gz: 2205cb5ebf976ee5a61b33dd8157619d27ae6d2d0f2f8eca5d93fbf52b2e6719d38d697218525bfab105ec7db6fd496f70a681587a2eb4da6677e10b20a170a4
7
+ data.tar.gz: d460ebf86b84a2814f41cdb94f134f28eafcebaab3d7ec9bf90fe11d1b8329f739ae7a17c4b40c07c69834c393fad1db79997602ae45b8a1637aa4b2838d451a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
- ## Falqon v0.0.1 (2022-06-22)
3
+ ## Falqon v0.1.0 (2024-11-16)
4
4
 
5
5
  Initial release
6
+
7
+ ## Falqon v0.0.1 (2023-06-22)
8
+
9
+ # Changelog
10
+
11
+ All notable changes to this project will be documented in this file.
12
+
13
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
14
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
15
+
16
+ ## [Unreleased]
17
+
18
+ ### Added
19
+
20
+ ### Changed
21
+
22
+ ### Removed
23
+
24
+ [unreleased]: https://github.com/floriandejonckheere/falqon/compare/v0.1.0...HEAD
25
+ [0.1.0]: https://github.com/floriandejonckheere/falqon/compare/v0.0.1...v0.1.0
data/Gemfile CHANGED
@@ -5,13 +5,45 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in falqon.gemspec
6
6
  gemspec
7
7
 
8
+ # Task runner
9
+ gem "rake", "13.2.1", require: false
10
+
11
+ group :development do
12
+ # Documentation
13
+ gem "yard", "0.9.37", require: false
14
+ gem "yard-sorbet", "0.9.0", require: false
15
+
16
+ # Simple server
17
+ gem "webrick", "1.8.2", require: false
18
+ end
19
+
8
20
  group :development, :test do
9
- gem "debug", require: false
10
- gem "ffaker", require: false
11
- gem "rake", require: false
12
- gem "rspec", require: false
13
- gem "rubocop", require: false
14
- gem "rubocop-performance", require: false
15
- gem "rubocop-rspec", require: false
16
- gem "timecop", require: false
21
+ # Debugger
22
+ gem "debug", "1.9.2", require: false
23
+
24
+ # Build objects for tests
25
+ gem "factory_bot", "6.5.0", require: false
26
+
27
+ # Generate fake data
28
+ gem "ffaker", "2.23.0", require: false
29
+
30
+ # Mock Redis server
31
+ gem "mock_redis", "0.45.0", require: false
32
+
33
+ # Behavior-driven test framework
34
+ gem "rspec", "3.13.0", require: false
35
+
36
+ # Linter
37
+ gem "rubocop", "1.68.0", require: false
38
+ gem "rubocop-factory_bot", "2.26.1", require: false
39
+ gem "rubocop-performance", "1.22.1", require: false
40
+ gem "rubocop-rake", "0.6.0", require: false
41
+ gem "rubocop-rspec", "3.2.0", require: false
42
+
43
+ # Type checker
44
+ gem "sorbet", "0.5.11645", require: false
45
+ gem "tapioca", "0.16.4", require: false
46
+
47
+ # Time control
48
+ gem "timecop", "0.9.10", require: false
17
49
  end
data/README.md CHANGED
@@ -1,13 +1,39 @@
1
1
  # Falqon
2
-
3
- ![Continuous Integration](https://github.com/floriandejonckheere/falqon/workflows/Continuous%20Integration/badge.svg)
2
+ [![Continuous Integration](https://github.com/floriandejonckheere/falqon/actions/workflows/ci.yml/badge.svg)](https://github.com/floriandejonckheere/falqon/actions/workflows/ci.yml)
4
3
  ![Release](https://img.shields.io/github/v/release/floriandejonckheere/falqon?label=Latest%20release)
5
4
 
6
- Simple, efficient messaging queue for Ruby.
5
+ Simple, efficient, and reliable messaging queue for Ruby.
6
+
7
+ Falqon is a simple messaging queue implementation, backed by the in-memory Redis key-value store.
8
+ It exposes a simple Ruby API to send and receive messages between different processes, between threads in the same process, or even fibers in the same thread.
9
+ It is perfect when you require a lightweight solution for processing messages, but don't want to deal with the complexity of a full-blown message queue like RabbitMQ or Kafka.
10
+
11
+ See the [documentation](https://docs.falqon.dev) for more information on how to use Falqon in your application.
12
+
13
+ ## Features
14
+
15
+ Falqon offers an elegant solution for messaging queues in Ruby.
16
+
17
+ - Elegant: only two methods to send and receive messages
18
+ - Reliable: no data is lost when a client crashes unexpectedly
19
+ - Fast: Falqon is built on top of Redis, a fast in-memory data store
20
+ - Flexible: tune the behaviour of the queue to your needs
21
+
22
+ ## Get started
23
+
24
+ - Install Falqon and get working with messaging queues in a heartbeat using the [quickstart guide](#quickstart)
25
+ - Check out the [API documentation](https://docs.falqon.dev/) for more information on how to use Falqon in your application
26
+ - Check out the CLI documentation](https://docs.falqon.dev/) for more information on how to manage queues and messages from the command line
27
+ - Read the [architecture documentation](#architecture) to learn more about how Falqon works under the hood
28
+
29
+ ## Quickstart
30
+
31
+ ### Requirements
7
32
 
8
- Falqon offers a simple messaging queue implementation backed by Redis.
33
+ Falqon requires a Redis 6+ server to be available.
34
+ Use the [docker-compose.yml](https://github.com/floriandejonckheere/falqon/blob/master/docker-compose.yml) file to quickly spin up a Redis server.
9
35
 
10
- ## Installation
36
+ ### Installation
11
37
 
12
38
  Add this line to your application's Gemfile:
13
39
 
@@ -23,14 +49,72 @@ Or install it yourself as:
23
49
 
24
50
  $ gem install falqon
25
51
 
26
- ## Usage
52
+ ### Configuration
53
+
54
+ The default configuration works out of the box with the provided `docker-compose.yml` file.
55
+ See [configuration](#configuration) if you want to adjust the configuration.
56
+
57
+ ### Usage
27
58
 
28
59
  ```ruby
29
60
  require "falqon"
30
61
 
31
- # TODO
62
+ queue = Falqon::Queue.new("my_queue")
63
+
64
+ # Push a message to the queue
65
+ queue.push("Hello, world!", "Hello, world again!")
66
+
67
+ # Pop a message from the queue (return style)
68
+ puts queue.pop # => "Hello, world!"
69
+
70
+ queue.empty? # => false
71
+
72
+ queue.peek # => "Hello, world again!"
73
+
74
+ # Pop a message from the queue (block style)
75
+ queue.pop do |message|
76
+ puts message # => "Hello, world again!"
77
+
78
+ # Raising a Falqon::Error exception will cause the message to be requeued
79
+ raise Falqon::Error, "Something went wrong"
80
+ end
81
+
82
+ queue.empty? # => false
83
+
84
+ puts queue.pop # => "Hello, world again!"
85
+
86
+ queue.empty? # => true
87
+
88
+ queue.peek # => nil
32
89
  ```
33
90
 
91
+ For more comprehensive examples, see the [examples directory](examples/) in the repository.
92
+
93
+ ## Architecture
94
+
95
+ A queue is identified with a name, which is used as a key prefix.
96
+ Queues are stored in Redis as a list of incrementing integers representing unique message identifiers.
97
+ The messages itself are stored in Redis as strings.
98
+
99
+ The following Redis keys are used to store data.
100
+
101
+ - `[{prefix}:]queues`: set of queue names
102
+
103
+ - `[{prefix}/]{name}`: list of message identifiers on the (pending) queue
104
+
105
+ - `[{prefix}/]{name}:id`: message identifier sequence
106
+
107
+ - `[{prefix}/]{name}:processing`: list of message identifiers being processed
108
+
109
+ - `[{prefix}/]{name}:scheduled`: list of message identifiers scheduled to retry
110
+
111
+ - `[{prefix}/]{name}:dead`: list of message identifiers that have been discarded
112
+
113
+ - `[{prefix}/]{name}:data:{id}`: message data for identifier `{id}`
114
+
115
+ - `[{prefix}/]{name}:metadata`: metadata for the queue
116
+
117
+ - `[{prefix}/]{name}:metadata:{id}`: metadata for identifier `{id}`
34
118
 
35
119
  ## Testing
36
120
 
@@ -44,10 +128,25 @@ bundle exec rspec
44
128
  To release a new version, update the version number in `lib/falqon/version.rb`, update the changelog, commit the files and create a git tag starting with `v`, and push it to the repository.
45
129
  Github Actions will automatically run the test suite, build the `.gem` file and push it to [rubygems.org](https://rubygems.org).
46
130
 
131
+ ## Documentation
132
+
133
+ The documentation in `docs/` is automatically built by [YARD](https://yardoc.org) and pushed to [docs.falqon.dev](https://docs.falqon.dev) on every push to the `main` branch.
134
+ Locally, you can build the documentation using the following commands:
135
+
136
+ ```sh
137
+ rake yrad
138
+ ```
139
+
140
+ In development, you can start a local server to preview the documentation:
141
+
142
+ ```sh
143
+ yard server --reload
144
+ ```
145
+
47
146
  ## Contributing
48
147
 
49
148
  Bug reports and pull requests are welcome on GitHub at [https://github.com/floriandejonckheere/falqon](https://github.com/floriandejonckheere/falqon).
50
149
 
51
150
  ## License
52
151
 
53
- The software is available as open source under the terms of the [LGPLv3 License](https://www.gnu.org/licenses/lgpl-3.0.html).
152
+ The software is available as open source under the terms of the [LGPL-3.0 License](https://www.gnu.org/licenses/lgpl-3.0.html).
data/bin/falqon ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+
6
+ require "falqon"
7
+
8
+ Falqon::CLI.start(ARGV)
@@ -1,3 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Falqon.loader.inflector.inflect({})
3
+ Falqon.loader.inflector.inflect({
4
+ "cli" => "CLI",
5
+ })
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Falqon
4
+ class CLI
5
+ # @!visibility private
6
+ class Base
7
+ attr_reader :options
8
+
9
+ def initialize(options = {})
10
+ @options = options
11
+ end
12
+
13
+ def call
14
+ validate
15
+ execute
16
+ rescue StandardError => e
17
+ puts e.message
18
+ end
19
+
20
+ protected
21
+
22
+ def validate
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def execute
27
+ raise NotImplementedError
28
+ end
29
+
30
+ def pluralize(count, singular, plural)
31
+ "#{count} #{count == 1 ? singular : plural}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Falqon
4
+ class CLI
5
+ # Clear messages from a queue
6
+ #
7
+ # Clearing a subqueue removes all messages and their data from the subqueue.
8
+ #
9
+ # Usage:
10
+ # falqon clear -q, --queue=QUEUE
11
+ #
12
+ # Options:
13
+ # -q, --queue=QUEUE # Queue name
14
+ # [--pending], [--no-pending], [--skip-pending] # Clear only pending messages
15
+ # [--processing], [--no-processing], [--skip-processing] # Clear only processing messages
16
+ # [--dead], [--no-dead], [--skip-dead] # Clear only dead messages
17
+ #
18
+ # If none of the +--pending+, +--processing+, +--scheduled+, or +--dead+ options are specified, all messages are cleared.
19
+ #
20
+ # @example Clear all messages in a queue
21
+ # $ falqon clear --queue jobs
22
+ # Cleared 3 messages from queue jobs
23
+ #
24
+ # @example Clear only pending messages
25
+ # $ falqon clear --queue jobs --pending
26
+ # Cleared 3 messages from queue jobs
27
+ #
28
+ # @example Clear only processing messages
29
+ # $ falqon clear --queue jobs --processing
30
+ # Cleared 3 messages from queue jobs
31
+ #
32
+ # @example Clear only scheduled messages
33
+ # $ falqon clear --queue jobs --scheduled
34
+ # Cleared 3 messages from queue jobs
35
+ #
36
+ # @example Clear only dead messages
37
+ # $ falqon clear --queue jobs --dead
38
+ # Cleared 3 messages from queue jobs
39
+ #
40
+ class Clear < Base
41
+ # @!visibility private
42
+ def validate
43
+ raise "No queue registered with this name: #{options[:queue]}" if options[:queue] && !Falqon::Queue.all.map(&:name).include?(options[:queue])
44
+ raise "--pending, --processing, --scheduled, and --dead are mutually exclusive" if [options[:pending], options[:processing], options[:scheduled], options[:dead]].count(true) > 1
45
+ end
46
+
47
+ # @!visibility private
48
+ def execute
49
+ # Clear messages
50
+ ids = subqueue.clear
51
+
52
+ if options[:pending]
53
+ puts "Cleared #{pluralize(ids.count, 'pending message', 'pending messages')} from queue #{queue.name}"
54
+ elsif options[:processing]
55
+ puts "Cleared #{pluralize(ids.count, 'processing message', 'processing messages')} from queue #{queue.name}"
56
+ elsif options[:scheduled]
57
+ puts "Cleared #{pluralize(ids.count, 'scheduled message', 'scheduled messages')} from queue #{queue.name}"
58
+ elsif options[:dead]
59
+ puts "Cleared #{pluralize(ids.count, 'dead message', 'dead messages')} from queue #{queue.name}"
60
+ else
61
+ puts "Cleared #{pluralize(ids.count, 'message', 'messages')} from queue #{queue.name}"
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def queue
68
+ @queue ||= Falqon::Queue.new(options[:queue])
69
+ end
70
+
71
+ def subqueue
72
+ @subqueue ||= if options[:pending]
73
+ queue.pending
74
+ elsif options[:processing]
75
+ queue.processing
76
+ elsif options[:scheduled]
77
+ queue.scheduled
78
+ elsif options[:dead]
79
+ queue.dead
80
+ else
81
+ queue
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Falqon
4
+ class CLI
5
+ # Delete messages from a queue
6
+ #
7
+ # Deleting a message removes it including its data from the queue.
8
+ #
9
+ # Usage:
10
+ # falqon delete -q, --queue=QUEUE
11
+ #
12
+ # Options:
13
+ # -q, --queue=QUEUE # Queue name
14
+ # [--pending], [--no-pending], [--skip-pending] # Delete only pending messages (default)
15
+ # [--processing], [--no-processing], [--skip-processing] # Delete only processing messages
16
+ # [--dead], [--no-dead], [--skip-dead] # Delete only dead messages
17
+ # [--head=N] # Delete N messages from head of queue
18
+ # [--tail=N] # Delete N messages from tail of queue
19
+ # [--index=N] # Delete message at index N
20
+ # [--range=N M] # Delete messages at index N to M
21
+ # [--id=N] # Delete message with ID N
22
+ #
23
+ # @example Delete all messages in the queue (by default only pending messages are deleted)
24
+ # $ falqon delete --queue jobs
25
+ # Deleted 10 messages from queue jobs
26
+ #
27
+ # @example Delete only pending messages
28
+ # $ falqon delete --queue jobs --pending
29
+ # Deleted 10 pending messages from queue jobs
30
+ #
31
+ # @example Delete only processing messages
32
+ # $ falqon delete --queue jobs --processing
33
+ # Deleted 1 processing message from queue jobs
34
+ #
35
+ # @example Delete only scheduled messages
36
+ # $ falqon delete --queue jobs --scheduled
37
+ # Deleted 1 scheduled message from queue jobs
38
+ #
39
+ # @example Delete only dead messages
40
+ # $ falqon delete --queue jobs --dead
41
+ # Deleted 5 dead messages from queue jobs
42
+ #
43
+ # @example Delete first 5 messages
44
+ # $ falqon delete --queue jobs --head 5
45
+ # Deleted 5 messages from queue jobs
46
+ #
47
+ # @example Delete last 5 messages
48
+ # $ falqon delete --queue jobs --tail 5
49
+ # Deleted 5 messages from queue jobs
50
+ #
51
+ # @example Delete message at index 5
52
+ # $ falqon delete --queue jobs --index 3 --index 5
53
+ # Deleted 1 message from queue jobs
54
+ #
55
+ # @example Delete messages from index 5 to 10
56
+ # $ falqon delete --queue jobs --range 5 10
57
+ # Deleted 6 messages from queue jobs
58
+ #
59
+ # @example Delete message with ID 5
60
+ # $ falqon delete --queue jobs --id 5 --id 1
61
+ # Deleted 2 messages from queue jobs
62
+ #
63
+ class Delete < Base
64
+ # @!visibility private
65
+ def validate
66
+ raise "No queue registered with this name: #{options[:queue]}" if options[:queue] && !Falqon::Queue.all.map(&:name).include?(options[:queue])
67
+
68
+ raise "--pending, --processing, --scheduled, and --dead are mutually exclusive" if [options[:pending], options[:processing], options[:scheduled], options[:dead]].count(true) > 1
69
+
70
+ raise "--head, --tail, --index, and --range are mutually exclusive" if [options[:head], options[:tail], options[:index], options[:range]].count { |o| o } > 1
71
+ raise "--range must be specified as two integers" if options[:range] && options[:range].count != 2
72
+
73
+ raise "--id is mutually exclusive with --head, --tail, --index, and --range" if options[:id] && [options[:head], options[:tail], options[:index], options[:range]].count { |o| o }.positive?
74
+ end
75
+
76
+ # @!visibility private
77
+ def execute
78
+ # Collect identifiers
79
+ ids = if options[:id]
80
+ Array(options[:id])
81
+ elsif options[:index]
82
+ Array(options[:index]).map do |i|
83
+ subqueue.peek(index: i) || raise("No message at index #{i}")
84
+ end
85
+ else
86
+ start, stop = range_options
87
+
88
+ subqueue.range(start:, stop:).map(&:to_i)
89
+ end
90
+
91
+ # Transform identifiers to messages
92
+ messages = ids.map do |id|
93
+ message = Falqon::Message.new(queue, id: id.to_i)
94
+
95
+ raise "No message with ID #{id}" unless message.exists?
96
+
97
+ message
98
+ end
99
+
100
+ # Delete messages
101
+ messages.each(&:delete)
102
+
103
+ if options[:processing]
104
+ puts "Deleted #{pluralize(messages.count, 'processing message', 'processing messages')} from queue #{queue.name}"
105
+ elsif options[:scheduled]
106
+ puts "Deleted #{pluralize(messages.count, 'scheduled message', 'scheduled messages')} from queue #{queue.name}"
107
+ elsif options[:dead]
108
+ puts "Deleted #{pluralize(messages.count, 'dead message', 'dead messages')} from queue #{queue.name}"
109
+ else # options[:pending]
110
+ puts "Deleted #{pluralize(messages.count, 'pending message', 'pending messages')} from queue #{queue.name}"
111
+ end
112
+ end
113
+
114
+ private
115
+
116
+ def queue
117
+ @queue ||= Falqon::Queue.new(options[:queue])
118
+ end
119
+
120
+ def subqueue
121
+ @subqueue ||= if options[:processing]
122
+ queue.processing
123
+ elsif options[:scheduled]
124
+ queue.scheduled
125
+ elsif options[:dead]
126
+ queue.dead
127
+ else # options[:pending]
128
+ queue.pending
129
+ end
130
+ end
131
+
132
+ def range_options
133
+ if options[:tail]
134
+ [
135
+ -options[:tail],
136
+ -1,
137
+ ]
138
+ elsif options[:range]
139
+ [
140
+ options[:range].first,
141
+ options[:range].last,
142
+ ]
143
+ else # options[:head]
144
+ [
145
+ 0,
146
+ options.fetch(:head, 0) - 1,
147
+ ]
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Falqon
4
+ class CLI
5
+ # Kill messages in a queue
6
+ #
7
+ # Killing a message removes it from the pending or scheduled queue, and moves it to the dead queue.
8
+ #
9
+ # Usage:
10
+ # falqon kill -q, --queue=QUEUE
11
+ #
12
+ # Options:
13
+ # -q, --queue=QUEUE # Queue name
14
+ # [--pending], [--no-pending], [--skip-pending] # Kill only pending messages (default)
15
+ # [--processing], [--no-processing], [--skip-processing] # Kill only processing messages
16
+ # [--head=N] # Kill N messages from head of queue
17
+ # [--tail=N] # Kill N messages from tail of queue
18
+ # [--index=N] # Kill message at index N
19
+ # [--range=N M] # Kill messages at index N to M
20
+ # [--id=N] # Kill message with ID N
21
+ #
22
+ # @example Kill all messages in the queue (by default only pending messages are killed)
23
+ # $ falqon kill --queue jobs
24
+ # Killed 10 messages from queue jobs
25
+ #
26
+ # @example Kill only pending messages
27
+ # $ falqon kill --queue jobs --pending
28
+ # Killed 10 pending messages from queue jobs
29
+ #
30
+ # @example Kill only processing messages
31
+ # $ falqon kill --queue jobs --processing
32
+ # Killed 1 processing message from queue jobs
33
+ #
34
+ # @example Kill only scheduled messages
35
+ # $ falqon kill --queue jobs --scheduled
36
+ # Killed 1 scheduled message from queue jobs
37
+ #
38
+ # @example Kill first 5 messages
39
+ # $ falqon kill --queue jobs --head 5
40
+ # Killed 5 messages from queue jobs
41
+ #
42
+ # @example Kill last 5 messages
43
+ # $ falqon kill --queue jobs --tail 5
44
+ # Killed 5 messages from queue jobs
45
+ #
46
+ # @example Kill message at index 5
47
+ # $ falqon kill --queue jobs --index 3 --index 5
48
+ # Killed 1 message from queue jobs
49
+ #
50
+ # @example Kill messages from index 5 to 10
51
+ # $ falqon kill --queue jobs --range 5 10
52
+ # Killed 6 messages from queue jobs
53
+ #
54
+ # @example Kill message with ID 5
55
+ # $ falqon kill --queue jobs --id 5 --id 1
56
+ # Killed 2 messages from queue jobs
57
+ #
58
+ class Kill < Base
59
+ # @!visibility private
60
+ def validate
61
+ raise "No queue registered with this name: #{options[:queue]}" if options[:queue] && !Falqon::Queue.all.map(&:name).include?(options[:queue])
62
+
63
+ raise "--pending, --scheduled, and --processing are mutually exclusive" if [options[:pending], options[:scheduled], options[:processing]].count(true) > 1
64
+
65
+ raise "--head, --tail, --index, and --range are mutually exclusive" if [options[:head], options[:tail], options[:index], options[:range]].count { |o| o } > 1
66
+ raise "--range must be specified as two integers" if options[:range] && options[:range].count != 2
67
+
68
+ raise "--id is mutually exclusive with --head, --tail, --index, and --range" if options[:id] && [options[:head], options[:tail], options[:index], options[:range]].count { |o| o }.positive?
69
+ end
70
+
71
+ # @!visibility private
72
+ def execute
73
+ # Collect identifiers
74
+ ids = if options[:id]
75
+ Array(options[:id])
76
+ elsif options[:index]
77
+ Array(options[:index]).map do |i|
78
+ subqueue.peek(index: i) || raise("No message at index #{i}")
79
+ end
80
+ else
81
+ start, stop = range_options
82
+
83
+ subqueue.range(start:, stop:).map(&:to_i)
84
+ end
85
+
86
+ # Transform identifiers to messages
87
+ messages = ids.map do |id|
88
+ message = Falqon::Message.new(queue, id: id.to_i)
89
+
90
+ raise "No message with ID #{id}" unless message.exists?
91
+
92
+ message
93
+ end
94
+
95
+ # Kill messages
96
+ messages.each(&:kill)
97
+
98
+ if options[:processing]
99
+ puts "Killed #{pluralize(messages.count, 'processing message', 'processing messages')} in queue #{queue.name}"
100
+ elsif options[:scheduled]
101
+ puts "Killed #{pluralize(messages.count, 'scheduled message', 'scheduled messages')} in queue #{queue.name}"
102
+ else # options[:pending]
103
+ puts "Killed #{pluralize(messages.count, 'pending message', 'pending messages')} in queue #{queue.name}"
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def queue
110
+ @queue ||= Falqon::Queue.new(options[:queue])
111
+ end
112
+
113
+ def subqueue
114
+ @subqueue ||= if options[:processing]
115
+ queue.processing
116
+ elsif options[:scheduled]
117
+ queue.scheduled
118
+ else # options[:pending]
119
+ queue.pending
120
+ end
121
+ end
122
+
123
+ def range_options
124
+ if options[:tail]
125
+ [
126
+ -options[:tail],
127
+ -1,
128
+ ]
129
+ elsif options[:range]
130
+ [
131
+ options[:range].first,
132
+ options[:range].last,
133
+ ]
134
+ else # options[:head]
135
+ [
136
+ 0,
137
+ options.fetch(:head, 0) - 1,
138
+ ]
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end