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