eq 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.gitignore +1 -0
  2. data/README.md +51 -11
  3. data/TODO.md +10 -0
  4. data/benchmarks/all.rb +13 -0
  5. data/benchmarks/parallel.rb +23 -0
  6. data/benchmarks/queue_backend_benchmark.rb +27 -0
  7. data/benchmarks/queueing.rb +13 -23
  8. data/benchmarks/working.rb +14 -25
  9. data/eq.gemspec +12 -2
  10. data/examples/queueing.rb +2 -2
  11. data/examples/scheduling.rb +19 -0
  12. data/examples/simple_usage.rb +20 -8
  13. data/examples/working.rb +2 -2
  14. data/lib/eq-queueing.rb +4 -13
  15. data/lib/eq-queueing/backends.rb +30 -1
  16. data/lib/eq-queueing/backends/leveldb.rb +232 -0
  17. data/lib/eq-queueing/backends/sequel.rb +34 -17
  18. data/lib/eq-queueing/queue.rb +26 -20
  19. data/lib/eq-scheduling.rb +33 -0
  20. data/lib/eq-scheduling/scheduler.rb +19 -0
  21. data/lib/eq-web.rb +5 -0
  22. data/lib/eq-web/server.rb +39 -0
  23. data/lib/eq-web/views/index.erb +45 -0
  24. data/lib/eq-working.rb +15 -7
  25. data/lib/eq-working/worker.rb +30 -3
  26. data/lib/eq.rb +39 -31
  27. data/lib/eq/boot/all.rb +1 -0
  28. data/lib/eq/boot/scheduling.rb +1 -0
  29. data/lib/eq/error.rb +4 -0
  30. data/lib/eq/job.rb +22 -16
  31. data/lib/eq/version.rb +1 -1
  32. data/log/.gitkeep +1 -0
  33. data/spec/lib/eq-queueing/backends/leveldb_spec.rb +32 -0
  34. data/spec/lib/eq-queueing/backends/sequel_spec.rb +5 -4
  35. data/spec/lib/eq-queueing/queue_spec.rb +27 -58
  36. data/spec/lib/eq-queueing_spec.rb +16 -0
  37. data/spec/lib/eq-scheduling_spec.rb +7 -0
  38. data/spec/lib/eq-working/worker_spec.rb +13 -0
  39. data/spec/lib/eq/job_spec.rb +16 -11
  40. data/spec/lib/eq_spec.rb +1 -1
  41. data/spec/mocks/a_job.rb +4 -0
  42. data/spec/mocks/a_unique_job.rb +6 -0
  43. data/spec/spec_helper.rb +12 -0
  44. data/spec/support/shared_examples_for_queue.rb +60 -31
  45. metadata +80 -8
  46. data/lib/eq-working/manager.rb +0 -31
  47. 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 [#to_sequel_block] payload
22
+ # @param [EQ::Job] payload
23
23
  # @return [Fixnum] id of the job
24
- def push payload
25
- jobs.insert payload: payload.to_sequel_blob, created_at: Time.now
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).limit(1).first
44
+ if job = waiting.order(:id).last # asc
38
45
  job[:started_working_at] = Time.now
39
46
  update_job!(job)
40
- [job[:id], job[:payload]]
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
- # 10 seconds ago
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
- # statistics:
94
- # - #job_count
95
- # - #working_count
96
- # - #waiting_count
97
- %w[ job working waiting ].each do |stats_name|
98
- define_method "#{stats_name}_count" do
99
- begin
100
- send(stats_name).send(:count)
101
- rescue ::Sequel::DatabaseError => e
102
- retry if on_error e
103
- end
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
@@ -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 *unserialized_payload
26
- debug "enqueing #{unserialized_payload.inspect} ..."
27
- queue.push EQ::Job.dump(unserialized_payload)
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 serialized_job = queue.reserve
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
- # @return [TrueClass, FalseClass]
42
- def pop job_id
43
- queue.pop job_id
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
- # re-enqueues jobs that timed out
47
- def requeue_timed_out_jobs
48
- queue.requeue_timed_out_jobs
52
+ def clear
53
+ queue.clear
49
54
  end
50
55
 
51
- attr_reader :queue
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
@@ -0,0 +1,5 @@
1
+ module EQ
2
+ module Web
3
+
4
+ end
5
+ end