falqon 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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