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.
- 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
|