eq 0.0.1 → 0.1.0
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 +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
|