gemerald_beanstalk 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.travis.yml +27 -0
- data/Gemfile +8 -0
- data/LICENSE +22 -0
- data/README.md +67 -0
- data/Rakefile +19 -0
- data/gemerald_beanstalk.gemspec +25 -0
- data/lib/gemerald_beanstalk.rb +10 -0
- data/lib/gemerald_beanstalk/beanstalk.rb +289 -0
- data/lib/gemerald_beanstalk/beanstalk_helper.rb +300 -0
- data/lib/gemerald_beanstalk/command.rb +365 -0
- data/lib/gemerald_beanstalk/connection.rb +170 -0
- data/lib/gemerald_beanstalk/job.rb +229 -0
- data/lib/gemerald_beanstalk/jobs.rb +39 -0
- data/lib/gemerald_beanstalk/server.rb +54 -0
- data/lib/gemerald_beanstalk/tube.rb +164 -0
- data/lib/gemerald_beanstalk/version.rb +3 -0
- data/test/beanstalk_integration_tests_test.rb +2 -0
- data/test/test_helper.rb +8 -0
- metadata +133 -0
@@ -0,0 +1,170 @@
|
|
1
|
+
class GemeraldBeanstalk::Connection
|
2
|
+
|
3
|
+
BEGIN_REQUEST_STATES = [:ready, :multi_part_request_in_progress]
|
4
|
+
|
5
|
+
attr_reader :beanstalk, :mutex, :tube_used, :tubes_watched
|
6
|
+
attr_writer :producer, :waiting, :worker
|
7
|
+
|
8
|
+
|
9
|
+
def alive?
|
10
|
+
return @inbound_state != :closed && @oubound_state != :closed
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
def begin_multi_part_request(multi_part_request)
|
15
|
+
return false unless outbound_ready?
|
16
|
+
@multi_part_request = multi_part_request
|
17
|
+
@outbound_state = :multi_part_request_in_progress
|
18
|
+
return true
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def begin_request
|
23
|
+
return false unless BEGIN_REQUEST_STATES.include?(@outbound_state)
|
24
|
+
@outbound_state = :request_in_progress
|
25
|
+
return true
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def close_connection
|
30
|
+
@inbound_state = @outbound_state = :closed
|
31
|
+
@connection.close_connection unless @connection.nil?
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def complete_request
|
36
|
+
return false unless request_in_progress?
|
37
|
+
@outbound_state = :ready
|
38
|
+
return true
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def execute(raw_command)
|
43
|
+
command = response = nil
|
44
|
+
@mutex.synchronize do
|
45
|
+
return if waiting? || request_in_progress?
|
46
|
+
puts "#{Time.now.to_f}: #{raw_command}" if ENV['VERBOSE']
|
47
|
+
if multi_part_request_in_progress?
|
48
|
+
(command = @multi_part_request).body = raw_command
|
49
|
+
else
|
50
|
+
command = GemeraldBeanstalk::Command.new(raw_command, self)
|
51
|
+
if !command.valid?
|
52
|
+
response = command.error
|
53
|
+
elsif command.multi_part_request?
|
54
|
+
return begin_multi_part_request(command)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
begin_request
|
58
|
+
end
|
59
|
+
puts "#{Time.now.to_f}: #{command.inspect}" if ENV['VERBOSE']
|
60
|
+
# Execute command unless parsing already yielded a response
|
61
|
+
response ||= beanstalk.execute(command)
|
62
|
+
transmit(response) unless response.nil?
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
def ignore(tube, force = false)
|
67
|
+
return nil unless @tubes_watched.length > 1 || force
|
68
|
+
@tubes_watched.delete(tube)
|
69
|
+
return @tubes_watched.length
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def inbound_ready?
|
74
|
+
return @inbound_state == :ready
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
def initialize(beanstalk, connection = nil)
|
79
|
+
@beanstalk = beanstalk
|
80
|
+
@connection = connection
|
81
|
+
@inbound_state = :ready
|
82
|
+
@mutex = Mutex.new
|
83
|
+
@outbound_state = :ready
|
84
|
+
@tube_used = 'default'
|
85
|
+
@tubes_watched = Set.new(%w[default])
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def multi_part_request_in_progress?
|
90
|
+
return @outbound_state == :multi_part_request_in_progress
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
def outbound_ready?
|
95
|
+
return @outbound_state == :ready
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
def producer?
|
100
|
+
return !!@producer
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
def request_in_progress?
|
105
|
+
return @outbound_state == :request_in_progress
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
def response_received
|
110
|
+
return false unless waiting? || timed_out?
|
111
|
+
@inbound_state = :ready
|
112
|
+
return true
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
def timed_out?
|
117
|
+
return @inbound_state == :timed_out
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
def transmit(message)
|
122
|
+
return if !alive? || @connection.nil?
|
123
|
+
puts "#{Time.now.to_f}: #{message}" if ENV['VERBOSE']
|
124
|
+
@connection.send_data(message)
|
125
|
+
complete_request
|
126
|
+
response_received
|
127
|
+
return self
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
def use(tube_name)
|
132
|
+
@tube_used = tube_name
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
def wait(timeout = nil)
|
137
|
+
return false unless inbound_ready?
|
138
|
+
@wait_timeout = timeout
|
139
|
+
@inbound_state = :waiting
|
140
|
+
return true
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
def wait_timed_out
|
145
|
+
return false unless @inbound_state == :waiting
|
146
|
+
@wait_timeout = nil
|
147
|
+
@inbound_state = :timed_out
|
148
|
+
return true
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
def waiting?
|
153
|
+
return false unless @inbound_state == :waiting
|
154
|
+
return true if @wait_timeout.nil? || @wait_timeout > Time.now.to_f
|
155
|
+
wait_timed_out
|
156
|
+
return false
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
def watch(tube)
|
161
|
+
@tubes_watched << tube
|
162
|
+
return @tubes_watched.length
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
def worker?
|
167
|
+
return !!@worker
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
class GemeraldBeanstalk::Job
|
2
|
+
|
3
|
+
MAX_JOB_PRIORITY = 2**32
|
4
|
+
|
5
|
+
INACTIVE_STATES = [:buried, :delayed]
|
6
|
+
RESERVED_STATES = [:deadline_pending, :reserved]
|
7
|
+
UPDATE_STATES = [:deadline_pending, :delayed, :reserved]
|
8
|
+
|
9
|
+
attr_reader :beanstalk, :reserved_at, :reserved_by, :timeout_at
|
10
|
+
attr_accessor :priority, :tube_name, :delay, :ready_at, :body,
|
11
|
+
:bytes, :created_at, :ttr, :id, :buried_at
|
12
|
+
|
13
|
+
|
14
|
+
def <(other_job)
|
15
|
+
return (self <=> other_job) == -1
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def <=>(other_job)
|
20
|
+
raise 'Cannot compare job with nil' if other_job.nil?
|
21
|
+
current_state = state
|
22
|
+
raise 'Cannot compare jobs with different states' if current_state != other_job.state
|
23
|
+
|
24
|
+
case current_state
|
25
|
+
when :ready
|
26
|
+
return -1 if self.priority < other_job.priority ||
|
27
|
+
self.priority == other_job.priority && self.created_at < other_job.created_at
|
28
|
+
when :delayed
|
29
|
+
return -1 if self.ready_at < other_job.ready_at
|
30
|
+
when :buried
|
31
|
+
return -1 if self.buried_at < other_job.buried_at
|
32
|
+
else
|
33
|
+
raise "Cannot compare job with state of #{current_state}"
|
34
|
+
end
|
35
|
+
return 1
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def buried?
|
40
|
+
return state == :buried
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def bury(connection, priority, *args)
|
45
|
+
return false unless reserved_by_connection?(connection)
|
46
|
+
|
47
|
+
reset_reserve_state
|
48
|
+
@state = :buried
|
49
|
+
@stats_hash[:'buries'] += 1
|
50
|
+
self.priority = priority.to_i
|
51
|
+
self.buried_at = Time.now.to_f
|
52
|
+
self.ready_at = nil
|
53
|
+
return true
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
# Must look at @state to avoid infinite recursion
|
58
|
+
def deadline_approaching(*args)
|
59
|
+
return false unless @state == :reserved
|
60
|
+
@state = :deadline_pending
|
61
|
+
return true
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def deadline_pending?
|
66
|
+
return state == :deadline_pending
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def delayed?
|
71
|
+
return state == :delayed
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def delete(connection, *args)
|
76
|
+
return false if RESERVED_STATES.include?(state) && !reserved_by_connection?(connection)
|
77
|
+
@state = :deleted
|
78
|
+
return true
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def initialize(beanstalk, id, tube_name, priority, delay, ttr, bytes, body)
|
83
|
+
priority, delay, ttr = priority.to_i, delay.to_i, ttr.to_i
|
84
|
+
@beanstalk = beanstalk
|
85
|
+
@stats_hash = Hash.new(0)
|
86
|
+
self.id = id
|
87
|
+
self.tube_name = tube_name
|
88
|
+
self.priority = priority % MAX_JOB_PRIORITY
|
89
|
+
self.delay = delay
|
90
|
+
self.ttr = ttr == 0 ? 1 : ttr
|
91
|
+
self.bytes = bytes
|
92
|
+
self.body = body
|
93
|
+
self.created_at = Time.now.to_f
|
94
|
+
self.ready_at = self.created_at + delay
|
95
|
+
|
96
|
+
@state = delay > 0 ? :delayed : :ready
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
def kick(*args)
|
101
|
+
return false unless INACTIVE_STATES.include?(state)
|
102
|
+
|
103
|
+
@state = :ready
|
104
|
+
@stats_hash[:'kicks'] += 1
|
105
|
+
self.ready_at = Time.now.to_f
|
106
|
+
self.buried_at = nil
|
107
|
+
return true
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
def ready?
|
112
|
+
return state == :ready
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
def release(connection, priority, delay, increment_stats = true, *args)
|
117
|
+
return false unless reserved_by_connection?(connection)
|
118
|
+
|
119
|
+
delay = delay.to_i
|
120
|
+
reset_reserve_state
|
121
|
+
@state = delay > 0 ? :delayed : :ready
|
122
|
+
@stats_hash[:'releases'] += 1 if increment_stats
|
123
|
+
self.priority = priority.to_i
|
124
|
+
self.delay = delay
|
125
|
+
self.ready_at = Time.now.to_f + delay
|
126
|
+
return true
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
def reserve(connection, *args)
|
131
|
+
return false unless ready?
|
132
|
+
|
133
|
+
@state = :reserved
|
134
|
+
@stats_hash[:'reserves'] += 1
|
135
|
+
@reserved_by = connection
|
136
|
+
@reserved_at = Time.now.to_f
|
137
|
+
@timeout_at = @reserved_at + self.ttr
|
138
|
+
return true
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
def reserved_by_connection?(connection)
|
143
|
+
return RESERVED_STATES.include?(state) && self.reserved_by == connection ? true : false
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
def reset_reserve_state
|
148
|
+
@timeout_at = nil
|
149
|
+
@reserved_at = nil
|
150
|
+
@reserved_by = nil
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
def state
|
155
|
+
return @state unless UPDATE_STATES.include?(@state)
|
156
|
+
|
157
|
+
now = Time.now.to_f
|
158
|
+
if @state == :delayed && self.ready_at <= now
|
159
|
+
@state = :ready
|
160
|
+
elsif RESERVED_STATES.include?(@state)
|
161
|
+
# Rescue from timeout being reset by other thread
|
162
|
+
if (now > self.timeout_at rescue false)
|
163
|
+
timed_out
|
164
|
+
elsif (@state == :reserved && now + 1 > self.timeout_at rescue false)
|
165
|
+
deadline_approaching
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
return @state
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
def stats
|
174
|
+
now = Time.now.to_f
|
175
|
+
current_state = state
|
176
|
+
return {
|
177
|
+
'id' => self.id,
|
178
|
+
'tube' => self.tube_name,
|
179
|
+
'state' => current_state == :deadline_pending ? 'reserved' : current_state.to_s,
|
180
|
+
'pri' => self.priority,
|
181
|
+
'age' => (now - self.created_at).to_i,
|
182
|
+
'delay' => self.delay.to_i,
|
183
|
+
'ttr' => self.ttr,
|
184
|
+
'time-left' => time_left(now),
|
185
|
+
'file' => 0,
|
186
|
+
'reserves' => @stats_hash[:'reserves'],
|
187
|
+
'timeouts' => @stats_hash[:'timeouts'],
|
188
|
+
'releases' => @stats_hash[:'releases'],
|
189
|
+
'buries' => @stats_hash[:'buries'],
|
190
|
+
'kicks' => @stats_hash[:'kicks'],
|
191
|
+
}
|
192
|
+
end
|
193
|
+
|
194
|
+
|
195
|
+
def time_left(current_time = Time.now.to_f)
|
196
|
+
if self.timeout_at
|
197
|
+
time_left = self.timeout_at - current_time
|
198
|
+
elsif self.ready_at
|
199
|
+
time_left = self.ready_at - current_time
|
200
|
+
end
|
201
|
+
return time_left.to_i
|
202
|
+
end
|
203
|
+
|
204
|
+
|
205
|
+
# Must reference @state to avoid infinite recursion
|
206
|
+
def timed_out(*args)
|
207
|
+
return false unless RESERVED_STATES.include?(@state)
|
208
|
+
@state = :ready
|
209
|
+
@stats_hash[:'timeouts'] += 1
|
210
|
+
connection = self.reserved_by
|
211
|
+
reset_reserve_state
|
212
|
+
self.beanstalk.register_job_timeout(connection, self)
|
213
|
+
return true
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
def timed_out?
|
218
|
+
return state == :timed_out
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
def touch(connection)
|
223
|
+
return false unless reserved_by_connection?(connection)
|
224
|
+
@state = :reserved
|
225
|
+
@timeout_at = Time.now.to_f + self.ttr
|
226
|
+
return true
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class GemeraldBeanstalk::Jobs < ThreadSafe::Array
|
2
|
+
attr_reader :total_jobs
|
3
|
+
|
4
|
+
def counts_by_state
|
5
|
+
job_stats = {
|
6
|
+
'current-jobs-urgent' => 0,
|
7
|
+
'current-jobs-ready' => 0,
|
8
|
+
'current-jobs-reserved' => 0,
|
9
|
+
'current-jobs-delayed' => 0,
|
10
|
+
'current-jobs-buried' => 0,
|
11
|
+
}
|
12
|
+
self.compact.each do |job|
|
13
|
+
state = job.state
|
14
|
+
|
15
|
+
job_stats["current-jobs-#{state}"] += 1
|
16
|
+
job_stats['current-jobs-urgent'] += 1 if state == :ready && job.priority < 1024
|
17
|
+
end
|
18
|
+
return job_stats
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def enqueue(job)
|
23
|
+
@total_jobs += 1
|
24
|
+
push(job)
|
25
|
+
return self
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def initialize(*)
|
30
|
+
@total_jobs = 0
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def next_id
|
36
|
+
return @total_jobs + 1
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module GemeraldBeanstalk::Server
|
4
|
+
|
5
|
+
def self.start(bind_address = nil, port = nil)
|
6
|
+
bind_address ||= '0.0.0.0'
|
7
|
+
port ||= 11300
|
8
|
+
full_address = "#{bind_address}:#{port}"
|
9
|
+
beanstalk = GemeraldBeanstalk::Beanstalk.new(full_address)
|
10
|
+
thread = Thread.new do
|
11
|
+
EventMachine.run do
|
12
|
+
EventMachine.start_server(bind_address, port, GemeraldBeanstalk::Server, beanstalk)
|
13
|
+
EventMachine.add_periodic_timer(0.01, beanstalk.method(:update_state))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
$PROGRAM_NAME = "gemerald_beanstalk:#{full_address}"
|
17
|
+
return [thread, beanstalk]
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def beanstalk
|
22
|
+
return @beanstalk
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def initialize(beanstalk)
|
27
|
+
@beanstalk = beanstalk
|
28
|
+
@partial_message = ''
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def post_init
|
34
|
+
@connection = beanstalk.connect(self)
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def receive_data(data)
|
39
|
+
if data[-2, 2] == "\r\n"
|
40
|
+
message = @partial_message + data
|
41
|
+
@partial_message = ''
|
42
|
+
EventMachine.defer(proc { @connection.execute(message) })
|
43
|
+
else
|
44
|
+
@partial_message += data
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def unbind
|
50
|
+
beanstalk.disconnect(@connection)
|
51
|
+
@connection.close_connection
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|