falqon 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Falqon
4
+ class CLI
5
+ # Display all active (registered) queues
6
+ #
7
+ # Usage:
8
+ # falqon list
9
+ #
10
+ # @example
11
+ # $ falqon list
12
+ # jobs
13
+ # emails
14
+ class List < Base
15
+ # @!visibility private
16
+ def validate; end
17
+
18
+ # @!visibility private
19
+ def execute
20
+ Falqon::Queue.all.each do |queue|
21
+ puts queue.name
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Falqon
4
+ class CLI
5
+ # Refill queue (move processing messages to pending)
6
+ #
7
+ # This command moves all messages from the processing queue back to the pending queue (in order).
8
+ # It is useful when a worker crashes or is stopped, and messages are left in the processing queue.
9
+ #
10
+ # Usage:
11
+ # falqon refill -q, --queue=QUEUE
12
+ #
13
+ # Options:
14
+ # -q, --queue=QUEUE # Queue name
15
+ #
16
+ # @example Refill the queue
17
+ # $ falqon refill --queue jobs
18
+ # Refilled 3 messages in queue jobs
19
+ #
20
+ class Refill < Base
21
+ # @!visibility private
22
+ def validate
23
+ raise "No queue registered with this name: #{options[:queue]}" if options[:queue] && !Falqon::Queue.all.map(&:name).include?(options[:queue])
24
+ end
25
+
26
+ # @!visibility private
27
+ def execute
28
+ queue = Falqon::Queue.new(options[:queue])
29
+
30
+ ids = queue.refill
31
+
32
+ puts "Refilled #{pluralize(ids.count, 'message', 'messages')} in queue #{queue.name}"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Falqon
4
+ class CLI
5
+ # Revive queue (move dead messages to pending)
6
+ #
7
+ # This command moves all messages from the dead queue back to the pending queue (in order).
8
+ # It is useful when messages are moved to the dead queue due to repeated failures, and need to be retried.
9
+ #
10
+ # Usage:
11
+ # falqon revive -q, --queue=QUEUE
12
+ #
13
+ # Options:
14
+ # -q, --queue=QUEUE # Queue name
15
+ #
16
+ # @example Revive the queue
17
+ # $ falqon revive --queue jobs
18
+ # Revived 3 messages in queue jobs
19
+ #
20
+ class Revive < Base
21
+ # @!visibility private
22
+ def validate
23
+ raise "No queue registered with this name: #{options[:queue]}" if options[:queue] && !Falqon::Queue.all.map(&:name).include?(options[:queue])
24
+ end
25
+
26
+ # @!visibility private
27
+ def execute
28
+ queue = Falqon::Queue.new(options[:queue])
29
+
30
+ ids = queue.revive
31
+
32
+ puts "Revived #{pluralize(ids.count, 'message', 'messages')} in queue #{queue.name}"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Falqon
4
+ class CLI
5
+ # Schedule failed messages for a retry
6
+ #
7
+ # This command moves all eligible messages from the scheduled queue back to the head of the pending queue (in order).
8
+ # Messages are eligible for a retry according to the configured retry strategy.
9
+ #
10
+ # Usage:
11
+ # falqon schedule -q, --queue=QUEUE
12
+ #
13
+ # Options:
14
+ # -q, --queue=QUEUE # Queue name
15
+ #
16
+ # @example Schedule eligible failed messages for retry
17
+ # $ falqon schedule --queue jobs
18
+ # Scheduled 3 messages for a retry in queue jobs
19
+ class Schedule < Falqon::CLI::Base
20
+ # @!visibility private
21
+ def validate
22
+ raise "No queue registered with this name: #{options[:queue]}" if options[:queue] && !Falqon::Queue.all.map(&:name).include?(options[:queue])
23
+ end
24
+
25
+ # @!visibility private
26
+ def execute
27
+ # Schedule failed messages
28
+ message_ids = queue.schedule
29
+
30
+ puts "Scheduled #{pluralize(message_ids.count, 'failed message', 'failed messages')} for a retry in queue #{queue.name}"
31
+ end
32
+
33
+ private
34
+
35
+ def queue
36
+ @queue ||= Falqon::Queue.new(options[:queue])
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Falqon
4
+ class CLI
5
+ # Display messages in a queue
6
+ #
7
+ # Usage:
8
+ # falqon show -q, --queue=QUEUE
9
+ #
10
+ # Options:
11
+ # -q, --queue=QUEUE # Queue name
12
+ # [--pending], [--no-pending], [--skip-pending] # Display pending messages (default)
13
+ # [--processing], [--no-processing], [--skip-processing] # Display processing messages
14
+ # [--dead], [--no-dead], [--skip-dead] # Display dead messages
15
+ # -d, [--data], [--no-data], [--skip-data] # Display raw data
16
+ # -m, [--meta], [--no-meta], [--skip-meta] # Display additional metadata
17
+ # [--head=N] # Display N messages from head of queue
18
+ # [--tail=N] # Display N messages from tail of queue
19
+ # [--index=N] # Display message at index N
20
+ # [--range=N M] # Display messages at index N to M
21
+ # [--id=N] # Display message with ID N
22
+ #
23
+ # @example Print all messages in the queue (by default only pending messages are displayed)
24
+ # $ falqon show --queue jobs
25
+ # id = 1 data = 8742 bytes
26
+ #
27
+ # @example Display only pending messages
28
+ # $ falqon show --queue jobs --pending
29
+ # ...
30
+ #
31
+ # @example Display only processing messages
32
+ # $ falqon show --queue jobs --processing
33
+ # ...
34
+ #
35
+ # @example Display only scheduled messages
36
+ # $ falqon show --queue jobs --scheduled
37
+ # ...
38
+ #
39
+ # @example Display only dead messages
40
+ # $ falqon show --queue jobs --dead
41
+ # ...
42
+ #
43
+ # @example Display raw data
44
+ # $ falqon show --queue jobs --data
45
+ # {"id":1,"message":"Hello, world!"}
46
+ #
47
+ # @example Display additional metadata
48
+ # $ falqon show --queue jobs --meta
49
+ # id = 1 retries = 0 created_at = 1970-01-01 00:00:00 +0000 updated_at = 1970-01-01 00:00:00 +0000 data = 8742 bytes
50
+ #
51
+ # @example Display first 5 messages
52
+ # $ falqon show --queue jobs --head 5
53
+ # id = 1 data = 8742 bytes
54
+ # id = 2 data = 8742 bytes
55
+ # id = 3 data = 8742 bytes
56
+ # id = 4 data = 8742 bytes
57
+ # id = 5 data = 8742 bytes
58
+ #
59
+ # @example Display last 5 messages
60
+ # $ falqon show --queue jobs --tail 5
61
+ # ...
62
+ #
63
+ # @example Display message at index 5
64
+ # $ falqon show --queue jobs --index 3 --index 5
65
+ # id = 3 data = 8742 bytes
66
+ # id = 5 data = 8742 bytes
67
+ #
68
+ # @example Display messages from index 5 to 10
69
+ # $ falqon show --queue jobs --range 5 10
70
+ # ...
71
+ #
72
+ # @example Display message with ID 5
73
+ # $ falqon show --queue jobs --id 5 --id 1
74
+ # id = 5 data = 8742 bytes
75
+ # id = 1 data = 8742 bytes
76
+ #
77
+ class Show < Base
78
+ # @!visibility private
79
+ def validate
80
+ raise "No queue registered with this name: #{options[:queue]}" if options[:queue] && !Falqon::Queue.all.map(&:name).include?(options[:queue])
81
+
82
+ raise "--pending, --processing, --scheduled, and --dead are mutually exclusive" if [options[:pending], options[:processing], options[:scheduled], options[:dead]].count(true) > 1
83
+ raise "--meta and --data are mutually exclusive" if [options[:meta], options[:data]].count(true) > 1
84
+
85
+ raise "--head, --tail, --index, and --range are mutually exclusive" if [options[:head], options[:tail], options[:index], options[:range]].count { |o| o } > 1
86
+ raise "--range must be specified as two integers" if options[:range] && options[:range].count != 2
87
+
88
+ 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?
89
+ end
90
+
91
+ # @!visibility private
92
+ def execute
93
+ # Collect identifiers
94
+ ids = if options[:id]
95
+ Array(options[:id])
96
+ else
97
+ queue.redis.with do |r|
98
+ if options[:index]
99
+ Array(options[:index]).map do |i|
100
+ r.lindex(subqueue.id, i) || raise("No message at index #{i}")
101
+ end
102
+ else
103
+ r.lrange(subqueue.id, *range_options)
104
+ end
105
+ end
106
+ end
107
+
108
+ # Transform identifiers to messages
109
+ messages = ids.map do |id|
110
+ message = Falqon::Message.new(queue, id: id.to_i)
111
+
112
+ raise "No message with ID #{id}" unless message.exists?
113
+
114
+ message
115
+ end
116
+
117
+ # Serialize messages
118
+ messages.each do |message|
119
+ puts Serializer
120
+ .new(message, meta: options[:meta], data: options[:data])
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ def queue
127
+ @queue ||= Falqon::Queue.new(options[:queue])
128
+ end
129
+
130
+ def subqueue
131
+ @subqueue ||= if options[:processing]
132
+ queue.processing
133
+ elsif options[:scheduled]
134
+ queue.scheduled
135
+ elsif options[:dead]
136
+ queue.dead
137
+ else
138
+ queue.pending
139
+ end
140
+ end
141
+
142
+ def range_options
143
+ if options[:tail]
144
+ [
145
+ -options[:tail],
146
+ -1,
147
+ ]
148
+ elsif options[:range]
149
+ [
150
+ options[:range].first,
151
+ options[:range].last,
152
+ ]
153
+ else # options[:head]
154
+ [
155
+ 0,
156
+ options.fetch(:head, 0) - 1,
157
+ ]
158
+ end
159
+ end
160
+
161
+ # @!visibility private
162
+ class Serializer
163
+ attr_reader :message, :meta, :data
164
+
165
+ def initialize(message, meta: false, data: false)
166
+ @message = message
167
+ @meta = meta
168
+ @data = data
169
+ end
170
+
171
+ def to_s
172
+ return message.data if data
173
+
174
+ if meta
175
+ "id = #{message.id} " \
176
+ "retries = #{message.metadata.retries} " \
177
+ "retried_at = #{message.metadata.retried_at ? Time.at(message.metadata.retried_at) : 'N/A'} " \
178
+ "retry_error = #{message.metadata.retry_error || 'N/A'} " \
179
+ "created_at = #{Time.at(message.metadata.created_at)} " \
180
+ "updated_at = #{Time.at(message.metadata.updated_at)} " \
181
+ "data = #{message.data.length} bytes"
182
+ else
183
+ "id = #{message.id} data = #{message.data.length} bytes"
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Falqon
6
+ class CLI
7
+ # Display queue statistics
8
+ #
9
+ # Usage:
10
+ # falqon stats
11
+ #
12
+ # Options:
13
+ # -q, [--queue=QUEUE] # Queue name
14
+ #
15
+ # @example Print statistics of all queues
16
+ # $ falqon stats
17
+ # jobs: 492 processed, 43 failed, 15 retried (created: 1970-01-01 00:00:00 +0000, updated: 1970-01-01 00:00:00 +0000)
18
+ #
19
+ # @example Print statistics of a specific queue
20
+ # $ falqon status --queue jobs
21
+ # jobs: 492 processed, 43 failed, 15 retried (created: 1970-01-01 00:00:00 +0000, updated: 1970-01-01 00:00:00 +0000)
22
+ #
23
+ class Stats < Base
24
+ # @!visibility private
25
+ def validate
26
+ raise "No queue registered with this name: #{options[:queue]}" if options[:queue] && !Falqon::Queue.all.map(&:name).include?(options[:queue])
27
+
28
+ raise "No queues registered" if Falqon::Queue.all.empty?
29
+ end
30
+
31
+ # @!visibility private
32
+ def execute
33
+ queues = options[:queue] ? [Falqon::Queue.new(options[:queue])] : Falqon::Queue.all
34
+
35
+ # Left pad queue names to the same length
36
+ length = queues.map { |q| q.name.length }.max
37
+
38
+ queues.each do |queue|
39
+ puts "#{queue.name.ljust length}: #{queue.metadata.processed} processed, #{queue.metadata.failed} failed, #{queue.metadata.retried} retried (created: #{Time.at(queue.metadata.created_at).to_datetime.iso8601}, updated: #{Time.at(queue.metadata.updated_at).to_datetime.iso8601})"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Falqon
4
+ class CLI
5
+ # Display queue status
6
+ #
7
+ # Usage:
8
+ # falqon status
9
+ #
10
+ # Options:
11
+ # -q, [--queue=QUEUE] # Queue name
12
+ #
13
+ # @example Print status of all queues
14
+ # $ falqon status
15
+ # jobs: 41 messages (34 pending, 2 processing, 0 scheduled, 5 dead)
16
+ # emails: empty
17
+ #
18
+ # @example Print status of a specific queue
19
+ # $ falqon status --queue jobs
20
+ # jobs: 41 messages (34 pending, 2 processing, 0 scheduled, 5 dead)
21
+ #
22
+ class Status < Base
23
+ # @!visibility private
24
+ def validate
25
+ raise "No queue registered with this name: #{options[:queue]}" if options[:queue] && !Falqon::Queue.all.map(&:name).include?(options[:queue])
26
+
27
+ raise "No queues registered" if Falqon::Queue.all.empty?
28
+ end
29
+
30
+ # @!visibility private
31
+ def execute
32
+ queues = options[:queue] ? [Falqon::Queue.new(options[:queue])] : Falqon::Queue.all
33
+
34
+ # Left pad queue names to the same length
35
+ length = queues.map { |q| q.name.length }.max
36
+
37
+ queues.each do |queue|
38
+ if queue.pending.empty? && queue.processing.empty? && queue.dead.empty?
39
+ puts "#{queue.name.ljust length}: empty"
40
+ else
41
+ puts "#{queue.name.ljust length}: #{queue.pending.size} pending, #{queue.processing.size} processing, #{queue.scheduled.size} scheduled, #{queue.dead.size} dead"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Falqon
4
+ class CLI
5
+ # @!visibility private
6
+ class Version < Base
7
+ def validate; end
8
+
9
+ def execute
10
+ puts "Falqon #{Falqon::VERSION}"
11
+ end
12
+ end
13
+ end
14
+ end
data/lib/falqon/cli.rb ADDED
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Falqon
6
+ # Falqon includes a command-line interface (CLI) to manage queues and messages
7
+ #
8
+ # After installing Falqon, run +falqon+ to see the available commands.
9
+ #
10
+ # $ falqon
11
+ # Commands:
12
+ # falqon help [COMMAND] # Describe available commands or one specific command
13
+ # falqon status # Print queue status
14
+ # falqon version # Print version
15
+ #
16
+ # To see the available options for a command, run +falqon help COMMAND+.
17
+ # The command-line interface assumes the default Falqon configuration.
18
+ # To use a custom configuration, set the corresponding environment variables:
19
+ #
20
+ # # Configure global queue name prefix
21
+ # FALQON_PREFIX=falqon
22
+ #
23
+ # # Configure Redis connection pool
24
+ # REDIS_URL=redis://localhost:6379/0
25
+ #
26
+ class CLI < Thor
27
+ # @!visibility private
28
+ def self.exit_on_failure?
29
+ true
30
+ end
31
+
32
+ desc "version", "Display version"
33
+ # @!visibility private
34
+ def version
35
+ Version
36
+ .new(options)
37
+ .call
38
+ end
39
+
40
+ desc "list", "Display all active (registered) queues"
41
+ # @!visibility private
42
+ def list
43
+ List
44
+ .new(options)
45
+ .call
46
+ end
47
+
48
+ desc "status", "Display queue status"
49
+ option :queue, aliases: "-q", type: :string, desc: "Queue name"
50
+ # @!visibility private
51
+ def status
52
+ Status
53
+ .new(options)
54
+ .call
55
+ end
56
+
57
+ desc "stats", "Display queue statistics"
58
+ option :queue, aliases: "-q", type: :string, desc: "Queue name"
59
+ # @!visibility private
60
+ def stats
61
+ Stats
62
+ .new(options)
63
+ .call
64
+ end
65
+
66
+ desc "show", "Display messages in a queue"
67
+ option :queue, aliases: "-q", type: :string, desc: "Queue name", required: true
68
+
69
+ option :pending, type: :boolean, desc: "Display pending messages (default)"
70
+ option :processing, type: :boolean, desc: "Display processing messages"
71
+ option :dead, type: :boolean, desc: "Display dead messages"
72
+
73
+ option :data, aliases: "-d", type: :boolean, desc: "Display raw data"
74
+ option :meta, aliases: "-m", type: :boolean, desc: "Display additional metadata"
75
+
76
+ option :head, type: :numeric, desc: "Display N messages from head of queue"
77
+ option :tail, type: :numeric, desc: "Display N messages from tail of queue"
78
+ option :index, type: :numeric, desc: "Display message at index N", repeatable: true
79
+ option :range, type: :array, desc: "Display messages at index N to M", banner: "N M"
80
+
81
+ option :id, type: :numeric, desc: "Display message with ID N", repeatable: true
82
+ # @!visibility private
83
+ def show
84
+ Show
85
+ .new(options)
86
+ .call
87
+ end
88
+
89
+ desc "delete", "Delete messages from a queue"
90
+ option :queue, aliases: "-q", type: :string, desc: "Queue name", required: true
91
+
92
+ option :pending, type: :boolean, desc: "Delete only pending messages (default)"
93
+ option :processing, type: :boolean, desc: "Delete only processing messages"
94
+ option :dead, type: :boolean, desc: "Delete only dead messages"
95
+
96
+ option :head, type: :numeric, desc: "Delete N messages from head of queue"
97
+ option :tail, type: :numeric, desc: "Delete N messages from tail of queue"
98
+ option :index, type: :numeric, desc: "Delete message at index N", repeatable: true
99
+ option :range, type: :array, desc: "Delete messages at index N to M", banner: "N M"
100
+
101
+ option :id, type: :numeric, desc: "Delete message with ID N", repeatable: true
102
+ # @!visibility private
103
+ def delete
104
+ Delete
105
+ .new(options)
106
+ .call
107
+ end
108
+
109
+ desc "kill", "Kill messages in a queue"
110
+ option :queue, aliases: "-q", type: :string, desc: "Queue name", required: true
111
+
112
+ option :pending, type: :boolean, desc: "Kill only pending messages (default)"
113
+ option :processing, type: :boolean, desc: "Kill only processing messages"
114
+
115
+ option :head, type: :numeric, desc: "Kill N messages from head of queue"
116
+ option :tail, type: :numeric, desc: "Kill N messages from tail of queue"
117
+ option :index, type: :numeric, desc: "Kill message at index N", repeatable: true
118
+ option :range, type: :array, desc: "Kill messages at index N to M", banner: "N M"
119
+
120
+ option :id, type: :numeric, desc: "Kill message with ID N", repeatable: true
121
+ # @!visibility private
122
+ def kill
123
+ Kill
124
+ .new(options)
125
+ .call
126
+ end
127
+
128
+ desc "clear", "Clear messages from a queue"
129
+ option :queue, aliases: "-q", type: :string, desc: "Queue name", required: true
130
+
131
+ option :pending, type: :boolean, desc: "Clear only pending messages"
132
+ option :processing, type: :boolean, desc: "Clear only processing messages"
133
+ option :dead, type: :boolean, desc: "Clear only dead messages"
134
+ # @!visibility private
135
+ def clear
136
+ Clear
137
+ .new(options)
138
+ .call
139
+ end
140
+
141
+ desc "refill", "Refill queue (move processing messages to pending)"
142
+ option :queue, aliases: "-q", type: :string, desc: "Queue name", required: true
143
+ # @!visibility private
144
+ def refill
145
+ Refill
146
+ .new(options)
147
+ .call
148
+ end
149
+
150
+ desc "revive", "Revive queue (move dead messages to pending)"
151
+ option :queue, aliases: "-q", type: :string, desc: "Queue name", required: true
152
+ # @!visibility private
153
+ def revive
154
+ Revive
155
+ .new(options)
156
+ .call
157
+ end
158
+
159
+ desc "schedule", "Schedule failed messages for a retry"
160
+ option :queue, aliases: "-q", type: :string, desc: "Queue name", required: true
161
+ # @!visibility private
162
+ def schedule
163
+ Schedule
164
+ .new(options)
165
+ .call
166
+ end
167
+ end
168
+ end