eq 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.md +51 -11
- data/TODO.md +10 -0
- data/benchmarks/all.rb +13 -0
- data/benchmarks/parallel.rb +23 -0
- data/benchmarks/queue_backend_benchmark.rb +27 -0
- data/benchmarks/queueing.rb +13 -23
- data/benchmarks/working.rb +14 -25
- data/eq.gemspec +12 -2
- data/examples/queueing.rb +2 -2
- data/examples/scheduling.rb +19 -0
- data/examples/simple_usage.rb +20 -8
- data/examples/working.rb +2 -2
- data/lib/eq-queueing.rb +4 -13
- data/lib/eq-queueing/backends.rb +30 -1
- data/lib/eq-queueing/backends/leveldb.rb +232 -0
- data/lib/eq-queueing/backends/sequel.rb +34 -17
- data/lib/eq-queueing/queue.rb +26 -20
- data/lib/eq-scheduling.rb +33 -0
- data/lib/eq-scheduling/scheduler.rb +19 -0
- data/lib/eq-web.rb +5 -0
- data/lib/eq-web/server.rb +39 -0
- data/lib/eq-web/views/index.erb +45 -0
- data/lib/eq-working.rb +15 -7
- data/lib/eq-working/worker.rb +30 -3
- data/lib/eq.rb +39 -31
- data/lib/eq/boot/all.rb +1 -0
- data/lib/eq/boot/scheduling.rb +1 -0
- data/lib/eq/error.rb +4 -0
- data/lib/eq/job.rb +22 -16
- data/lib/eq/version.rb +1 -1
- data/log/.gitkeep +1 -0
- data/spec/lib/eq-queueing/backends/leveldb_spec.rb +32 -0
- data/spec/lib/eq-queueing/backends/sequel_spec.rb +5 -4
- data/spec/lib/eq-queueing/queue_spec.rb +27 -58
- data/spec/lib/eq-queueing_spec.rb +16 -0
- data/spec/lib/eq-scheduling_spec.rb +7 -0
- data/spec/lib/eq-working/worker_spec.rb +13 -0
- data/spec/lib/eq/job_spec.rb +16 -11
- data/spec/lib/eq_spec.rb +1 -1
- data/spec/mocks/a_job.rb +4 -0
- data/spec/mocks/a_unique_job.rb +6 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/shared_examples_for_queue.rb +60 -31
- metadata +80 -8
- data/lib/eq-working/manager.rb +0 -31
- data/lib/eq-working/system.rb +0 -10
@@ -0,0 +1,232 @@
|
|
1
|
+
require 'leveldb'
|
2
|
+
|
3
|
+
module EQ::Queueing::Backends
|
4
|
+
|
5
|
+
# @note This is a unoreded storage, so there is no guaranteed work order
|
6
|
+
# @note assume there is nothing else than jobs
|
7
|
+
class LevelDB
|
8
|
+
class JobsCollection < Struct.new(:db, :name)
|
9
|
+
include EQ::Logging
|
10
|
+
|
11
|
+
QUEUE = 'queue'.freeze
|
12
|
+
PAYLOAD = 'payload'.freeze
|
13
|
+
CREATED_AT = 'created_at'.freeze
|
14
|
+
STARTED_WORKING_AT = 'started_working_at'.freeze
|
15
|
+
NOT_WORKING = ''.freeze
|
16
|
+
|
17
|
+
# @param [EQ::Job] job
|
18
|
+
def push job
|
19
|
+
job_id = find_free_job_id
|
20
|
+
db["#{QUEUE}:#{job_id}"] = job.queue
|
21
|
+
db["#{PAYLOAD}:#{job_id}"] = serialize(job.payload) unless job.payload.nil?
|
22
|
+
db["#{CREATED_AT}:#{job_id}"] = serialize(Time.now)
|
23
|
+
db["#{STARTED_WORKING_AT}:#{job_id}"] = NOT_WORKING
|
24
|
+
job_id
|
25
|
+
end
|
26
|
+
|
27
|
+
def first_waiting
|
28
|
+
db.each do |k,v|
|
29
|
+
if k.include?(STARTED_WORKING_AT) && v == NOT_WORKING
|
30
|
+
return job_id_from_key(k)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def working_iterator
|
37
|
+
db.each do |k,v|
|
38
|
+
if k.include?(STARTED_WORKING_AT) && v != NOT_WORKING
|
39
|
+
yield job_id_from_key(k), deserialize(v)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param [EQ::Job] job without id
|
45
|
+
def exists? job
|
46
|
+
db.each do |k,v|
|
47
|
+
if k.include?(QUEUE) && v == job.queue
|
48
|
+
if find_payload(job_id_from_key(k)) == job.payload
|
49
|
+
return true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
def delete job_id
|
57
|
+
did_exist = !db["#{QUEUE}:#{job_id}"].nil?
|
58
|
+
db.batch do |batch|
|
59
|
+
batch.delete "#{QUEUE}:#{job_id}"
|
60
|
+
batch.delete "#{PAYLOAD}:#{job_id}"
|
61
|
+
batch.delete "#{CREATED_AT}:#{job_id}"
|
62
|
+
batch.delete "#{STARTED_WORKING_AT}:#{job_id}"
|
63
|
+
end
|
64
|
+
does_not_exist = db["#{QUEUE}:#{job_id}"].nil?
|
65
|
+
did_exist && does_not_exist
|
66
|
+
end
|
67
|
+
|
68
|
+
def start_working job_id
|
69
|
+
db["#{STARTED_WORKING_AT}:#{job_id}"] = serialize(Time.now)
|
70
|
+
end
|
71
|
+
|
72
|
+
def stop_working job_id
|
73
|
+
db["#{STARTED_WORKING_AT}:#{job_id}"] = NOT_WORKING
|
74
|
+
end
|
75
|
+
|
76
|
+
def find_queue job_id
|
77
|
+
db["#{QUEUE}:#{job_id}"]
|
78
|
+
end
|
79
|
+
|
80
|
+
def find_payload job_id
|
81
|
+
if raw = db["#{PAYLOAD}:#{job_id}"]
|
82
|
+
deserialize db["#{PAYLOAD}:#{job_id}"]
|
83
|
+
else
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def find_created_at job_id
|
89
|
+
if serialized_time = db["#{CREATED_AT}:#{job_id}"]
|
90
|
+
deserialize(serialized_time)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def find_started_working_at job_id
|
95
|
+
if serialized_time = db["#{STARTED_WORKING_AT}:#{job_id}"]
|
96
|
+
deserialize(serialized_time)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def job_id_from_key key
|
101
|
+
prefix, job_id = *key.split(':')
|
102
|
+
job_id
|
103
|
+
end
|
104
|
+
|
105
|
+
# try as hard as you can to find a free slot
|
106
|
+
def find_free_job_id
|
107
|
+
loop do
|
108
|
+
job_id = generate_id
|
109
|
+
return job_id unless db.contains? "#{QUEUE}:#{job_id}"
|
110
|
+
debug "#{job_id} is not free"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Time in milliseconds and 4 digit random
|
115
|
+
# @note Maybe this is a stupid idea, but for now it kinda works :)
|
116
|
+
def generate_id
|
117
|
+
'%d%04d' % [(Time.now.to_f * 1000.0).to_i, Kernel.rand(1000)]
|
118
|
+
end
|
119
|
+
|
120
|
+
def serialize data
|
121
|
+
Marshal.dump(data)
|
122
|
+
end
|
123
|
+
|
124
|
+
def deserialize data
|
125
|
+
Marshal.load(data)
|
126
|
+
end
|
127
|
+
|
128
|
+
def count
|
129
|
+
result = 0
|
130
|
+
db.each do |k,v|
|
131
|
+
result += 1 if k.include?(QUEUE)
|
132
|
+
end
|
133
|
+
result
|
134
|
+
end
|
135
|
+
|
136
|
+
def count_waiting
|
137
|
+
result = 0
|
138
|
+
db.each do |k,v|
|
139
|
+
if k.include?(STARTED_WORKING_AT) && v == NOT_WORKING
|
140
|
+
result += 1
|
141
|
+
end
|
142
|
+
end
|
143
|
+
result
|
144
|
+
end
|
145
|
+
|
146
|
+
def count_working
|
147
|
+
result = 0
|
148
|
+
db.each do |k,v|
|
149
|
+
if k.include?(STARTED_WORKING_AT) && v != NOT_WORKING
|
150
|
+
result += 1
|
151
|
+
end
|
152
|
+
end
|
153
|
+
result
|
154
|
+
end
|
155
|
+
|
156
|
+
def each
|
157
|
+
# TODO optimize this (and others) using range queries
|
158
|
+
db.each do |k,v|
|
159
|
+
if k.include?(QUEUE)
|
160
|
+
job_id = job_id_from_key(k)
|
161
|
+
yield(id: job_id,
|
162
|
+
queue: v,
|
163
|
+
payload: find_payload(job_id),
|
164
|
+
created_at: find_created_at(job_id),
|
165
|
+
started_working_at: find_started_working_at(job_id))
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
attr_reader :db
|
172
|
+
attr_reader :jobs
|
173
|
+
|
174
|
+
def initialize config
|
175
|
+
@db = ::LevelDB::DB.new config
|
176
|
+
@jobs = JobsCollection.new(db)
|
177
|
+
end
|
178
|
+
|
179
|
+
# @param [EQ::Job] job
|
180
|
+
def push job
|
181
|
+
if job.unique? && jobs.exists?(job)
|
182
|
+
false
|
183
|
+
else
|
184
|
+
jobs.push job
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def reserve
|
189
|
+
if job_id = jobs.first_waiting
|
190
|
+
jobs.start_working job_id
|
191
|
+
EQ::Job.new job_id, jobs.find_queue(job_id), jobs.find_payload(job_id)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def pop job_id
|
196
|
+
jobs.delete job_id
|
197
|
+
end
|
198
|
+
|
199
|
+
def requeue_timed_out_jobs
|
200
|
+
requeued = 0
|
201
|
+
jobs.working_iterator do |job_id, started_working_at|
|
202
|
+
# older than x
|
203
|
+
if started_working_at <= (Time.now - EQ.config.job_timeout)
|
204
|
+
jobs.stop_working job_id
|
205
|
+
requeued += 1
|
206
|
+
end
|
207
|
+
end
|
208
|
+
requeued
|
209
|
+
end
|
210
|
+
|
211
|
+
def clear
|
212
|
+
db.each{|k,v| db.delete k}
|
213
|
+
end
|
214
|
+
|
215
|
+
def count name=nil
|
216
|
+
case name
|
217
|
+
when :waiting
|
218
|
+
jobs.count_waiting
|
219
|
+
when :working
|
220
|
+
jobs.count_working
|
221
|
+
else
|
222
|
+
jobs.count
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def iterator
|
227
|
+
jobs.each do |job|
|
228
|
+
yield job
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -19,10 +19,17 @@ module EQ::Queueing::Backends
|
|
19
19
|
create_table_if_not_exists!
|
20
20
|
end
|
21
21
|
|
22
|
-
# @param [
|
22
|
+
# @param [EQ::Job] payload
|
23
23
|
# @return [Fixnum] id of the job
|
24
|
-
def push
|
25
|
-
|
24
|
+
def push eq_job
|
25
|
+
job = {queue: eq_job.queue}
|
26
|
+
job[:payload] = Marshal.dump(eq_job.payload).to_sequel_blob unless eq_job.payload.nil?
|
27
|
+
if eq_job.unique? && jobs.where(job).count > 0
|
28
|
+
false
|
29
|
+
else
|
30
|
+
job[:created_at] = Time.now
|
31
|
+
jobs.insert job
|
32
|
+
end
|
26
33
|
rescue ::Sequel::DatabaseError => e
|
27
34
|
retry if on_error e
|
28
35
|
end
|
@@ -34,10 +41,11 @@ module EQ::Queueing::Backends
|
|
34
41
|
# @return [Array<Fixnum, String>] job data consisting of id and payload
|
35
42
|
def reserve
|
36
43
|
db.transaction do
|
37
|
-
if job = waiting.order(:id.asc
|
44
|
+
if job = waiting.order(:id).last # asc
|
38
45
|
job[:started_working_at] = Time.now
|
39
46
|
update_job!(job)
|
40
|
-
|
47
|
+
payload = job[:payload].nil? ? nil : Marshal.load(job[:payload])
|
48
|
+
EQ::Job.new(job[:id], job[:queue], payload)
|
41
49
|
end
|
42
50
|
end
|
43
51
|
rescue ::Sequel::DatabaseError => e
|
@@ -85,22 +93,30 @@ module EQ::Queueing::Backends
|
|
85
93
|
# this re-enqueues jobs that timed out
|
86
94
|
# @return [Fixnum] number of jobs that were re-enqueued
|
87
95
|
def requeue_timed_out_jobs
|
88
|
-
#
|
96
|
+
# older than x
|
89
97
|
jobs.where{started_working_at <= (Time.now - EQ.config.job_timeout)}\
|
90
98
|
.update(started_working_at: nil)
|
91
99
|
end
|
92
100
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
101
|
+
def count name=nil
|
102
|
+
case name
|
103
|
+
when :waiting
|
104
|
+
waiting.count
|
105
|
+
when :working
|
106
|
+
working.count
|
107
|
+
else
|
108
|
+
jobs.count
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def clear
|
113
|
+
jobs.delete
|
114
|
+
end
|
115
|
+
|
116
|
+
def iterator
|
117
|
+
jobs.each do |job|
|
118
|
+
job[:payload] = Marshal.load(job[:payload]) if job[:payload]
|
119
|
+
yield job
|
104
120
|
end
|
105
121
|
end
|
106
122
|
|
@@ -116,6 +132,7 @@ module EQ::Queueing::Backends
|
|
116
132
|
def create_table_if_not_exists!
|
117
133
|
db.create_table? TABLE_NAME do
|
118
134
|
primary_key :id
|
135
|
+
String :queue
|
119
136
|
Timestamp :created_at
|
120
137
|
Timestamp :started_working_at
|
121
138
|
Blob :payload
|
data/lib/eq-queueing/queue.rb
CHANGED
@@ -5,15 +5,10 @@ module EQ::Queueing
|
|
5
5
|
# furthermore this class adds some functionality to serialize / deserialze
|
6
6
|
# using the Job class
|
7
7
|
class Queue
|
8
|
+
extend SingleForwardable
|
9
|
+
|
8
10
|
include Celluloid
|
9
11
|
include EQ::Logging
|
10
|
-
|
11
|
-
%w[ job_count waiting_count working_count waiting working ].each do |method_name|
|
12
|
-
define_method method_name do
|
13
|
-
queue.send(method_name)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
alias :size :job_count
|
17
12
|
|
18
13
|
# @param [Object] queue_backend
|
19
14
|
def initialize queue_backend
|
@@ -22,33 +17,44 @@ module EQ::Queueing
|
|
22
17
|
|
23
18
|
# @param [Array<Class, *payload>] unserialized_payload
|
24
19
|
# @return [Fixnum] job_id
|
25
|
-
def push *
|
26
|
-
debug "enqueing #{
|
27
|
-
queue.push EQ::Job.
|
20
|
+
def push job_class, *job_payload
|
21
|
+
debug "enqueing #{job_payload.inspect} ..."
|
22
|
+
queue.push EQ::Job.new(nil, job_class, job_payload)
|
28
23
|
end
|
29
24
|
|
30
25
|
# @return [EQ::Job, nilClass] job instance
|
31
26
|
def reserve
|
32
27
|
requeue_timed_out_jobs
|
33
|
-
if
|
34
|
-
job_id, serialized_payload = *serialized_job
|
35
|
-
job = EQ::Job.load job_id, serialized_payload
|
28
|
+
if job = queue.reserve
|
36
29
|
debug "dequeud #{job.inspect}"
|
37
30
|
job
|
38
31
|
end
|
39
32
|
end
|
40
33
|
|
41
|
-
#
|
42
|
-
|
43
|
-
|
34
|
+
#
|
35
|
+
# TODO #pop method: shall we add a check, when the job is worked on, if we are the worker?
|
36
|
+
#
|
37
|
+
def pop *args
|
38
|
+
queue.pop *args
|
39
|
+
end
|
40
|
+
|
41
|
+
def requeue_timed_out_jobs; queue.requeue_timed_out_jobs; end
|
42
|
+
|
43
|
+
def jobs; queue.jobs; end
|
44
|
+
def working; queue.working; end
|
45
|
+
def waiting; queue.waiting; end
|
46
|
+
def count name=nil; queue.count name; end
|
47
|
+
|
48
|
+
def iterator &block
|
49
|
+
queue.iterator &block
|
44
50
|
end
|
45
51
|
|
46
|
-
|
47
|
-
|
48
|
-
queue.requeue_timed_out_jobs
|
52
|
+
def clear
|
53
|
+
queue.clear
|
49
54
|
end
|
50
55
|
|
51
|
-
|
56
|
+
private
|
52
57
|
|
58
|
+
def queue; @queue; end
|
53
59
|
end
|
54
60
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'eq')
|
2
|
+
|
3
|
+
require 'clockwork'
|
4
|
+
Clockwork.handler do |job|
|
5
|
+
EQ.push job if EQ.queue
|
6
|
+
end
|
7
|
+
|
8
|
+
require File.join(File.dirname(__FILE__), 'eq-scheduling', 'scheduler')
|
9
|
+
|
10
|
+
module EQ::Scheduling
|
11
|
+
module_function
|
12
|
+
|
13
|
+
EQ_SCHEDULER = :_eq_scheduler
|
14
|
+
|
15
|
+
def boot
|
16
|
+
EQ::Scheduling::Scheduler.supervise_as EQ_SCHEDULER, EQ.config
|
17
|
+
end
|
18
|
+
|
19
|
+
def shutdown
|
20
|
+
scheduler.terminate! if scheduler
|
21
|
+
end
|
22
|
+
|
23
|
+
def scheduler
|
24
|
+
Celluloid::Actor[EQ_SCHEDULER]
|
25
|
+
end
|
26
|
+
|
27
|
+
def events
|
28
|
+
Clockwork.class_variable_get('@@events').map do |event|
|
29
|
+
[ event.job,
|
30
|
+
event.instance_variable_get('@period') ]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module EQ::Scheduling
|
2
|
+
class Scheduler
|
3
|
+
include EQ::Logging
|
4
|
+
include Celluloid
|
5
|
+
|
6
|
+
def initialize config
|
7
|
+
clockwork!
|
8
|
+
end
|
9
|
+
|
10
|
+
def clockwork
|
11
|
+
debug 'scheduler running'
|
12
|
+
loop do
|
13
|
+
Clockwork.tick
|
14
|
+
sleep(Clockwork.config[:sleep_timeout])
|
15
|
+
return unless Actor.current.alive?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|