gemerald_beanstalk 0.0.1

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