gemerald_beanstalk 0.0.1

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,300 @@
1
+ module GemeraldBeanstalk::BeanstalkHelper
2
+
3
+ BAD_FORMAT = "BAD_FORMAT\r\n"
4
+ BURIED = "BURIED\r\n"
5
+ CRLF = "\r\n"
6
+ DEADLINE_SOON = "DEADLINE_SOON\r\n"
7
+ DELETED = "DELETED\r\n"
8
+ EXPECTED_CRLF = "EXPECTED_CRLF\r\n"
9
+ JOB_TOO_BIG = "JOB_TOO_BIG\r\n"
10
+ KICKED = "KICKED\r\n"
11
+ NOT_FOUND = "NOT_FOUND\r\n"
12
+ NOT_IGNORED = "NOT_IGNORED\r\n"
13
+ PAUSED = "PAUSED\r\n"
14
+ RELEASED = "RELEASED\r\n"
15
+ TIMED_OUT = "TIMED_OUT\r\n"
16
+ TOUCHED = "TOUCHED\r\n"
17
+ UNKNOWN_COMMAND = "UNKNOWN_COMMAND\r\n"
18
+
19
+ JOB_INACTIVE_STATES = GemeraldBeanstalk::Job::INACTIVE_STATES
20
+ JOB_RESERVED_STATES = GemeraldBeanstalk::Job::RESERVED_STATES
21
+
22
+ # ease handling of odd case where put can return BAD_FORMAT but increment stats
23
+ def adjust_stats_cmd_put
24
+ adjust_stats_key(:'cmd-put')
25
+ end
26
+
27
+
28
+ def connect(connection = nil)
29
+ beanstalk_connection = GemeraldBeanstalk::Connection.new(self, connection)
30
+ @connections << beanstalk_connection
31
+ adjust_stats_key(:'total-connections')
32
+ return beanstalk_connection
33
+ end
34
+
35
+
36
+ def disconnect(connection)
37
+ connection.close_connection
38
+ tube(connection.tube_used).stop_use
39
+ connection.tubes_watched.each do |watched_tube|
40
+ tube(watched_tube).ignore
41
+ connection.ignore(watched_tube, :force)
42
+ end
43
+ @reserved[connection].each do |job|
44
+ job.release(connection, job.priority, 0, false)
45
+ end
46
+ @reserved.delete(connection)
47
+ @connections.delete(connection)
48
+ end
49
+
50
+
51
+ def execute(command)
52
+ return send(command.method_name, *command.arguments)
53
+ end
54
+
55
+
56
+ def register_job_timeout(connection, job)
57
+ @reserved[connection].delete(job)
58
+ adjust_stats_key(:'job-timeouts')
59
+ honor_reservations(job)
60
+ end
61
+
62
+ private
63
+
64
+ def active_tubes
65
+ tubes = {}
66
+ @tubes.each_pair { |tube_name, tube| tubes[tube_name] = tube if tube.active? }
67
+ return tubes
68
+ end
69
+
70
+
71
+ def adjust_stats_key(key, adjustment = 1)
72
+ @stats[key] += adjustment
73
+ end
74
+
75
+
76
+ def cancel_reservations(connection)
77
+ connection.tubes_watched.each do |tube_name|
78
+ tube(tube_name).cancel_reservation(connection)
79
+ end
80
+ return connection
81
+ end
82
+
83
+
84
+ def deadline_pending?(connection)
85
+ return @reserved[connection].any?(&:deadline_pending?)
86
+ end
87
+
88
+
89
+ def find_job(job_id, options = {})
90
+ return unless (job_id = job_id.to_i) > 0
91
+ only = Array(options[:only])
92
+ except = Array(options[:except]).unshift(:deleted)
93
+
94
+ job = @jobs[job_id - 1]
95
+
96
+ return nil if job.nil? || except.include?(job.state)
97
+ return (only.empty? || only.include?(job.state)) ? job : nil
98
+ end
99
+
100
+
101
+ def honor_reservations(job_or_tube)
102
+ if job_or_tube.is_a?(GemeraldBeanstalk::Job)
103
+ job = job_or_tube
104
+ tube = tube(job.tube_name)
105
+ elsif job_or_tube.is_a?(GemeraldBeanstalk::Tube)
106
+ tube = job_or_tube
107
+ job = tube.next_job
108
+ end
109
+
110
+ while job && (next_reservation = tube.next_reservation)
111
+ next unless try_dispatch(next_reservation, job)
112
+ job = tube.next_job
113
+ end
114
+ end
115
+
116
+
117
+ def next_job(connection, state = :ready)
118
+ best_candidate = nil
119
+ connection.tubes_watched.each do |tube_name|
120
+ candidate = tube(tube_name).next_job(state)
121
+ next if candidate.nil?
122
+
123
+ best_candidate = candidate if best_candidate.nil? || candidate < best_candidate
124
+ end
125
+
126
+ return best_candidate
127
+ end
128
+
129
+
130
+ def peek_by_state(connection, state)
131
+ adjust_stats_key(:"cmd-peek-#{state}")
132
+ return peek_message(tube(connection.tube_used).next_job(state, :peek))
133
+ end
134
+
135
+
136
+ def peek_message(job)
137
+ job.nil? ? NOT_FOUND : "FOUND #{job.id} #{job.bytes}\r\n#{job.body}\r\n"
138
+ end
139
+
140
+
141
+ def reserve_job(connection, timeout = 0)
142
+ connection.worker = true
143
+
144
+ if deadline_pending?(connection)
145
+ connection.transmit(DEADLINE_SOON)
146
+ return true
147
+ end
148
+
149
+ connection.tubes_watched.each do |tube_name|
150
+ tube(tube_name).reserve(connection)
151
+ end
152
+ connection.wait(timeout <= 0 ? nil : Time.now.to_f + timeout)
153
+
154
+ dispatched = false
155
+ while !dispatched
156
+ break if (job = next_job(connection)).nil?
157
+ dispatched = try_dispatch(connection, job)
158
+ end
159
+
160
+ return dispatched
161
+ end
162
+
163
+
164
+ def stats_commands
165
+ return {
166
+ 'cmd-put' => @stats[:'cmd-put'],
167
+ 'cmd-peek' => @stats[:'cmd-peek'],
168
+ 'cmd-peek-ready' => @stats[:'cmd-peek-ready'],
169
+ 'cmd-peek-delayed' => @stats[:'cmd-peek-delayed'],
170
+ 'cmd-peek-buried' => @stats[:'cmd-peek-buried'],
171
+ 'cmd-reserve' => @stats[:'cmd-reserve'],
172
+ 'cmd-reserve-with-timeout' => @stats[:'cmd-reserve-with-timeout'],
173
+ 'cmd-delete' => @stats[:'cmd-delete'],
174
+ 'cmd-release' => @stats[:'cmd-release'],
175
+ 'cmd-use' => @stats[:'cmd-use'],
176
+ 'cmd-watch' => @stats[:'cmd-watch'],
177
+ 'cmd-ignore' => @stats[:'cmd-ignore'],
178
+ 'cmd-bury' => @stats[:'cmd-bury'],
179
+ 'cmd-kick' => @stats[:'cmd-kick'],
180
+ 'cmd-touch' => @stats[:'cmd-touch'],
181
+ 'cmd-stats' => @stats[:'cmd-stats'],
182
+ 'cmd-stats-job' => @stats[:'cmd-stats-job'],
183
+ 'cmd-stats-tube' => @stats[:'cmd-stats-tube'],
184
+ 'cmd-list-tubes' => @stats[:'cmd-list-tubes'],
185
+ 'cmd-list-tube-used' => @stats[:'cmd-list-tube-used'],
186
+ 'cmd-list-tubes-watched' => @stats[:'cmd-list-tubes-watched'],
187
+ 'cmd-pause-tube' => @stats[:'cmd-pause-tube'],
188
+ }
189
+ end
190
+
191
+
192
+ def stats_connections
193
+ conn_stats = {
194
+ 'current-connections' => @connections.length,
195
+ 'current-producers' => 0,
196
+ 'current-workers' => 0,
197
+ 'current-waiting' => 0,
198
+ 'total-connections' => @stats[:'total-connections']
199
+ }
200
+ @connections.each do |connection|
201
+ conn_stats['current-producers'] += 1 if connection.producer?
202
+ conn_stats['current-waiting'] += 1 if connection.waiting?
203
+ conn_stats['current-workers'] += 1 if connection.worker?
204
+ end
205
+ return conn_stats
206
+ end
207
+
208
+
209
+ def try_dispatch(connection, job)
210
+ connection.mutex.synchronize do
211
+ # Make sure connection still waiting and job not claimed
212
+ return false unless connection.waiting? && job.reserve(connection)
213
+ connection.transmit("RESERVED #{job.id} #{job.bytes}\r\n#{job.body}\r\n")
214
+ cancel_reservations(connection)
215
+ end
216
+ @reserved[connection] << job
217
+ return true
218
+ end
219
+
220
+
221
+ def tube(tube_name, create_if_missing = false)
222
+ tube = @tubes[tube_name]
223
+
224
+ return tube unless tube.nil? || tube.deactivated?
225
+
226
+ return @tubes[tube_name] = GemeraldBeanstalk::Tube.new(tube_name) if create_if_missing
227
+
228
+ @tubes.delete(tube_name) unless tube.nil?
229
+ return nil
230
+ end
231
+
232
+
233
+ def tube_list(tube_list)
234
+ return yaml_response(tube_list.map { |key| "- #{key}" })
235
+ end
236
+
237
+
238
+ def update_state
239
+ update_waiting
240
+ update_timeouts
241
+ end
242
+
243
+
244
+ def update_timeouts
245
+ @reserved.values.flatten.each(&:state)
246
+ @delayed.keep_if do |job|
247
+ case job.state
248
+ when :delayed
249
+ true
250
+ when :ready
251
+ honor_reservations(job)
252
+ false
253
+ else
254
+ false
255
+ end
256
+ end
257
+ @paused.keep_if do |tube|
258
+ if tube.paused?
259
+ true
260
+ else
261
+ honor_reservations(tube)
262
+ false
263
+ end
264
+ end
265
+ end
266
+
267
+
268
+ def update_waiting
269
+ waiting_connections.each do |connection|
270
+ if connection.waiting? && deadline_pending?(connection)
271
+ message_for_connection = DEADLINE_SOON
272
+ elsif connection.timed_out?
273
+ message_for_connection = TIMED_OUT
274
+ else
275
+ next
276
+ end
277
+
278
+ cancel_reservations(connection)
279
+ connection.transmit(message_for_connection)
280
+ end
281
+ end
282
+
283
+
284
+ def uptime
285
+ (Time.now.to_f - @up_at).to_i
286
+ end
287
+
288
+
289
+ def waiting_connections
290
+ return @connections.select {|connection| connection.waiting? || connection.timed_out? }
291
+ end
292
+
293
+
294
+ def yaml_response(data)
295
+ response = %w[---].concat(data).join("\n")
296
+ return "OK #{response.bytesize}\r\n#{response}\r\n"
297
+ end
298
+
299
+
300
+ end
@@ -0,0 +1,365 @@
1
+ class GemeraldBeanstalk::Command
2
+
3
+ COMMAND_PARSER_REGEX = /(?:(?<command>.*?)\r\n)(?<body>.*\r\n)?\z/m
4
+ TRAILING_SPACE_REGEX = /\s+\z/
5
+ WHITE_SPACE_REGEX = / /
6
+ ZERO_STRING_REGEX = /^0+[^1-9]*/
7
+
8
+ VALID_TUBE_NAME_REGEX = /\A[a-zA-Z0-9_+\/;.$()]{1}[a-zA-Z0-9_\-+\/;.$()]*\z/
9
+
10
+ BAD_FORMAT = GemeraldBeanstalk::Beanstalk::BAD_FORMAT
11
+ UNKNOWN_COMMAND = GemeraldBeanstalk::Beanstalk::UNKNOWN_COMMAND
12
+
13
+ COMMANDS = [
14
+ :bury, :delete, :ignore, :kick, :'kick-job', :'list-tubes', :'list-tube-used', :'list-tubes-watched',
15
+ :'pause-tube', :peek, :'peek-buried', :'peek-delayed', :'peek-ready', :put, :quit, :release, :reserve,
16
+ :'reserve-with-timeout', :stats, :'stats-job', :'stats-tube', :touch, :use, :watch,
17
+ ]
18
+
19
+ COMMAND_METHOD_NAMES = Hash[COMMANDS.zip(COMMANDS)].merge!({
20
+ :'kick-job' => 'kick_job', :'list-tubes' => 'list_tubes', :'list-tube-used' => 'list_tube_used',
21
+ :'list-tubes-watched' => :'list_tubes_watched', :'pause-tube' => 'pause_tube', :'peek-buried' => 'peek_buried',
22
+ :'peek-delayed' => 'peek_delayed', :'peek-ready' => 'peek_ready', :'reserve-with-timeout' => 'reserve_with_timeout',
23
+ :'stats-job' => 'stats_job', :'stats-tube' => 'stats_tube',
24
+ })
25
+
26
+ COMMANDS_RECOGNIZED_WITHOUT_SPACE_AFTER_COMMAND = [
27
+ :'list-tubes', :'list-tube-used', :'list-tubes-watched', :'peek-buried', :'peek-delayed', :'peek-ready',
28
+ :'pause-tube', :quit, :reserve, :'reserve-with-timeout', :stats, :'stats-job', :'stats-tube',
29
+ ]
30
+
31
+ COMMANDS_REQUIRING_SPACE_AFTER_COMMAND = COMMANDS - COMMANDS_RECOGNIZED_WITHOUT_SPACE_AFTER_COMMAND
32
+
33
+ attr_reader :command, :connection, :error
34
+ attr_accessor :body
35
+
36
+
37
+ def arguments
38
+ if command == :put
39
+ return @args[0, @argument_cardnality].unshift(connection).push(body)
40
+ else
41
+ return @args[0, @argument_cardnality].unshift(connection)
42
+ end
43
+ end
44
+
45
+
46
+ def initialize(raw_command, connection)
47
+ @connection = connection
48
+ parse(raw_command)
49
+ end
50
+
51
+
52
+ def method_name
53
+ return COMMAND_METHOD_NAMES[command]
54
+ end
55
+
56
+
57
+ def multi_part_request?
58
+ return command == :put && body.nil?
59
+ end
60
+
61
+
62
+ def to_s
63
+ return "#{command} #{@args.join(' ')}"
64
+ end
65
+
66
+
67
+ def valid?
68
+ return @valid
69
+ end
70
+
71
+ private
72
+
73
+ def bad_format!
74
+ invalidate(BAD_FORMAT)
75
+ end
76
+
77
+
78
+ def bury
79
+ @argument_cardnality = 2
80
+ return requires_no_space_after_line &&
81
+ requires_exact_argument_count &&
82
+ requires_valid_integer(@args[0]) &&
83
+ requires_valid_integer(@args[1], :positive => true)
84
+ end
85
+
86
+
87
+ def delete
88
+ @argument_cardnality = 1
89
+ return true
90
+ end
91
+
92
+
93
+ def ignore
94
+ @argument_cardnality = 1
95
+ return requires_no_space_after_line &&
96
+ requires_exact_argument_count &&
97
+ requires_valid_tube_name(@args[0])
98
+ end
99
+
100
+
101
+ def invalidate(error)
102
+ @error = error
103
+ @value = false
104
+ end
105
+
106
+
107
+ def kick
108
+ @argument_cardnality = 1
109
+ return requires(@args[0]) &&
110
+ requires_valid_integer(@args[0], :allow_trailing => true)
111
+ end
112
+
113
+
114
+ def kick_job
115
+ @argument_cardnality = 1
116
+ return true
117
+ end
118
+
119
+
120
+ def list_tubes
121
+ @argument_cardnality = 0
122
+ return requires_only_command
123
+ end
124
+
125
+
126
+ def list_tube_used
127
+ @argument_cardnality = 0
128
+ return requires_only_command
129
+ end
130
+
131
+
132
+ def list_tubes_watched
133
+ @argument_cardnality = 0
134
+ return requires_only_command
135
+ end
136
+
137
+
138
+ def parse(raw_command)
139
+ @command_lines = raw_command.match(COMMAND_PARSER_REGEX)
140
+ if @command_lines.nil?
141
+ return unknown_command!
142
+ end
143
+
144
+ @args = @command_lines[:command].split(WHITE_SPACE_REGEX)
145
+ @command = @args.shift.to_sym rescue nil
146
+
147
+ @space_after_command = @command && !!(raw_command[@command.length] =~ WHITE_SPACE_REGEX)
148
+ @body = @command_lines[:body]
149
+
150
+ return unknown_command! unless valid_command?
151
+
152
+ return bad_format! unless send(method_name)
153
+ @valid = true
154
+ end
155
+
156
+
157
+ def pause_tube
158
+ @argument_cardnality = 2
159
+ return requires_no_space_after_line &&
160
+ requires(@args[0]) &&
161
+ requires_valid_integer(@args[1]) &&
162
+ requires_valid_tube_name(@args[0])
163
+ end
164
+
165
+
166
+ def peek
167
+ @argument_cardnality = 1
168
+ return true
169
+ end
170
+
171
+
172
+ def peek_buried
173
+ @argument_cardnality = 0
174
+ return requires_only_command
175
+ end
176
+
177
+
178
+ def peek_delayed
179
+ @argument_cardnality = 0
180
+ return requires_only_command
181
+ end
182
+
183
+
184
+ def peek_ready
185
+ @argument_cardnality = 0
186
+ return requires_only_command
187
+ end
188
+
189
+
190
+ def put
191
+ @argument_cardnality = 5
192
+ return false unless @args.all? do |arg|
193
+ requires_valid_integer(arg, :positive => true)
194
+ end
195
+
196
+ # Handle weird case where BAD_FORMAT, but increments stats
197
+ is_valid = requires_no_space_after_line
198
+ unless is_valid
199
+ connection.beanstalk.adjust_stats_cmd_put
200
+ end
201
+ return is_valid
202
+ end
203
+
204
+
205
+ def quit
206
+ @argument_cardnality = 0
207
+ return requires_only_command
208
+ end
209
+
210
+
211
+ def release
212
+ @argument_cardnality = 3
213
+ return requires_no_space_after_line &&
214
+ requires_exact_argument_count &&
215
+ requires_valid_integer(@args[0]) &&
216
+ requires_valid_integer(@args[1], :positive => true) &&
217
+ requires_valid_integer(@args[2], :positive => true)
218
+ end
219
+
220
+
221
+ def requires(arg)
222
+ return true unless arg.nil?
223
+
224
+ bad_format!
225
+ return false
226
+ end
227
+
228
+
229
+ def requires_exact_argument_count
230
+ return true if @args.length == @argument_cardnality
231
+
232
+ bad_format!
233
+ return false
234
+ end
235
+
236
+
237
+ def requires_no_space_after_command
238
+ return true unless @space_after_command
239
+
240
+ bad_format!
241
+ return false
242
+ end
243
+
244
+
245
+ def requires_no_space_after_line
246
+ return true unless @command_lines[:command] =~ TRAILING_SPACE_REGEX
247
+
248
+ bad_format!
249
+ return false
250
+ end
251
+
252
+
253
+ def requires_only_command
254
+ return requires_no_space_after_command && requires_exact_argument_count
255
+ end
256
+
257
+
258
+ def requires_space_after_command
259
+ return true if @space_after_command
260
+
261
+ bad_format!
262
+ return false
263
+ end
264
+
265
+
266
+ def requires_valid_integer(arg, opts = {})
267
+ arg_to_i = arg.to_i
268
+ if opts[:allow_trailing]
269
+ if arg_to_i == 0 && arg !~ ZERO_STRING_REGEX
270
+ bad_format!
271
+ return false
272
+ end
273
+ else
274
+ if arg_to_i.to_s != arg
275
+ bad_format!
276
+ return false
277
+ end
278
+ end
279
+
280
+ return true if opts[:positive] ? arg_to_i >= 0 : true
281
+
282
+ bad_format!
283
+ return false
284
+ end
285
+
286
+
287
+ def requires_valid_tube_name(tube_name)
288
+ return true if valid_tube_name?(tube_name)
289
+
290
+ bad_format!
291
+ return false
292
+ end
293
+
294
+
295
+ def reserve
296
+ @argument_cardnality = 0
297
+ return requires_only_command
298
+ end
299
+
300
+
301
+ def reserve_with_timeout
302
+ @argument_cardnality = 1
303
+ return requires_space_after_command
304
+ end
305
+
306
+
307
+ def stats
308
+ @argument_cardnality = 0
309
+ return requires_only_command
310
+ end
311
+
312
+
313
+ def stats_job
314
+ @argument_cardnality = 1
315
+ return requires_space_after_command
316
+ end
317
+
318
+
319
+ def stats_tube
320
+ @argument_cardnality = 1
321
+ return requires_space_after_command &&
322
+ requires_no_space_after_line &&
323
+ requires_valid_tube_name(@args[0])
324
+ end
325
+
326
+
327
+ def touch
328
+ @argument_cardnality = 1
329
+ return true
330
+ end
331
+
332
+
333
+ def use
334
+ @argument_cardnality = 1
335
+ return requires_no_space_after_line &&
336
+ requires_exact_argument_count &&
337
+ requires_valid_tube_name(@args[0])
338
+ end
339
+
340
+
341
+ def unknown_command!
342
+ invalidate(UNKNOWN_COMMAND)
343
+ end
344
+
345
+
346
+ def valid_command?
347
+ return COMMANDS_RECOGNIZED_WITHOUT_SPACE_AFTER_COMMAND.include?(command) || (
348
+ COMMANDS_REQUIRING_SPACE_AFTER_COMMAND.include?(command) && @space_after_command
349
+ )
350
+ end
351
+
352
+
353
+ def valid_tube_name?(tube_name)
354
+ return !tube_name.nil? && tube_name.bytesize <= 200 && VALID_TUBE_NAME_REGEX =~ tube_name
355
+ end
356
+
357
+
358
+ def watch
359
+ @argument_cardnality = 1
360
+ return requires_no_space_after_line &&
361
+ requires_exact_argument_count &&
362
+ requires_valid_tube_name(@args[0])
363
+ end
364
+
365
+ end