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