ruby_job 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/COPYING.LESSER ADDED
@@ -0,0 +1,165 @@
1
+ GNU LESSER GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+
9
+ This version of the GNU Lesser General Public License incorporates
10
+ the terms and conditions of version 3 of the GNU General Public
11
+ License, supplemented by the additional permissions listed below.
12
+
13
+ 0. Additional Definitions.
14
+
15
+ As used herein, "this License" refers to version 3 of the GNU Lesser
16
+ General Public License, and the "GNU GPL" refers to version 3 of the GNU
17
+ General Public License.
18
+
19
+ "The Library" refers to a covered work governed by this License,
20
+ other than an Application or a Combined Work as defined below.
21
+
22
+ An "Application" is any work that makes use of an interface provided
23
+ by the Library, but which is not otherwise based on the Library.
24
+ Defining a subclass of a class defined by the Library is deemed a mode
25
+ of using an interface provided by the Library.
26
+
27
+ A "Combined Work" is a work produced by combining or linking an
28
+ Application with the Library. The particular version of the Library
29
+ with which the Combined Work was made is also called the "Linked
30
+ Version".
31
+
32
+ The "Minimal Corresponding Source" for a Combined Work means the
33
+ Corresponding Source for the Combined Work, excluding any source code
34
+ for portions of the Combined Work that, considered in isolation, are
35
+ based on the Application, and not on the Linked Version.
36
+
37
+ The "Corresponding Application Code" for a Combined Work means the
38
+ object code and/or source code for the Application, including any data
39
+ and utility programs needed for reproducing the Combined Work from the
40
+ Application, but excluding the System Libraries of the Combined Work.
41
+
42
+ 1. Exception to Section 3 of the GNU GPL.
43
+
44
+ You may convey a covered work under sections 3 and 4 of this License
45
+ without being bound by section 3 of the GNU GPL.
46
+
47
+ 2. Conveying Modified Versions.
48
+
49
+ If you modify a copy of the Library, and, in your modifications, a
50
+ facility refers to a function or data to be supplied by an Application
51
+ that uses the facility (other than as an argument passed when the
52
+ facility is invoked), then you may convey a copy of the modified
53
+ version:
54
+
55
+ a) under this License, provided that you make a good faith effort to
56
+ ensure that, in the event an Application does not supply the
57
+ function or data, the facility still operates, and performs
58
+ whatever part of its purpose remains meaningful, or
59
+
60
+ b) under the GNU GPL, with none of the additional permissions of
61
+ this License applicable to that copy.
62
+
63
+ 3. Object Code Incorporating Material from Library Header Files.
64
+
65
+ The object code form of an Application may incorporate material from
66
+ a header file that is part of the Library. You may convey such object
67
+ code under terms of your choice, provided that, if the incorporated
68
+ material is not limited to numerical parameters, data structure
69
+ layouts and accessors, or small macros, inline functions and templates
70
+ (ten or fewer lines in length), you do both of the following:
71
+
72
+ a) Give prominent notice with each copy of the object code that the
73
+ Library is used in it and that the Library and its use are
74
+ covered by this License.
75
+
76
+ b) Accompany the object code with a copy of the GNU GPL and this license
77
+ document.
78
+
79
+ 4. Combined Works.
80
+
81
+ You may convey a Combined Work under terms of your choice that,
82
+ taken together, effectively do not restrict modification of the
83
+ portions of the Library contained in the Combined Work and reverse
84
+ engineering for debugging such modifications, if you also do each of
85
+ the following:
86
+
87
+ a) Give prominent notice with each copy of the Combined Work that
88
+ the Library is used in it and that the Library and its use are
89
+ covered by this License.
90
+
91
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
92
+ document.
93
+
94
+ c) For a Combined Work that displays copyright notices during
95
+ execution, include the copyright notice for the Library among
96
+ these notices, as well as a reference directing the user to the
97
+ copies of the GNU GPL and this license document.
98
+
99
+ d) Do one of the following:
100
+
101
+ 0) Convey the Minimal Corresponding Source under the terms of this
102
+ License, and the Corresponding Application Code in a form
103
+ suitable for, and under terms that permit, the user to
104
+ recombine or relink the Application with a modified version of
105
+ the Linked Version to produce a modified Combined Work, in the
106
+ manner specified by section 6 of the GNU GPL for conveying
107
+ Corresponding Source.
108
+
109
+ 1) Use a suitable shared library mechanism for linking with the
110
+ Library. A suitable mechanism is one that (a) uses at run time
111
+ a copy of the Library already present on the user's computer
112
+ system, and (b) will operate properly with a modified version
113
+ of the Library that is interface-compatible with the Linked
114
+ Version.
115
+
116
+ e) Provide Installation Information, but only if you would otherwise
117
+ be required to provide such information under section 6 of the
118
+ GNU GPL, and only to the extent that such information is
119
+ necessary to install and execute a modified version of the
120
+ Combined Work produced by recombining or relinking the
121
+ Application with a modified version of the Linked Version. (If
122
+ you use option 4d0, the Installation Information must accompany
123
+ the Minimal Corresponding Source and Corresponding Application
124
+ Code. If you use option 4d1, you must provide the Installation
125
+ Information in the manner specified by section 6 of the GNU GPL
126
+ for conveying Corresponding Source.)
127
+
128
+ 5. Combined Libraries.
129
+
130
+ You may place library facilities that are a work based on the
131
+ Library side by side in a single library together with other library
132
+ facilities that are not Applications and are not covered by this
133
+ License, and convey such a combined library under terms of your
134
+ choice, if you do both of the following:
135
+
136
+ a) Accompany the combined library with a copy of the same work based
137
+ on the Library, uncombined with any other library facilities,
138
+ conveyed under the terms of this License.
139
+
140
+ b) Give prominent notice with the combined library that part of it
141
+ is a work based on the Library, and explaining where to find the
142
+ accompanying uncombined form of the same work.
143
+
144
+ 6. Revised Versions of the GNU Lesser General Public License.
145
+
146
+ The Free Software Foundation may publish revised and/or new versions
147
+ of the GNU Lesser General Public License from time to time. Such new
148
+ versions will be similar in spirit to the present version, but may
149
+ differ in detail to address new problems or concerns.
150
+
151
+ Each version is given a distinguishing version number. If the
152
+ Library as you received it specifies that a certain numbered version
153
+ of the GNU Lesser General Public License "or any later version"
154
+ applies to it, you have the option of following the terms and
155
+ conditions either of that published version or of any later version
156
+ published by the Free Software Foundation. If the Library as you
157
+ received it does not specify a version number of the GNU Lesser
158
+ General Public License, you may choose any version of the GNU Lesser
159
+ General Public License ever published by the Free Software Foundation.
160
+
161
+ If the Library as you received it specifies that a proxy can decide
162
+ whether future versions of the GNU Lesser General Public License shall
163
+ apply, that proxy's public statement of acceptance of any version is
164
+ permanent authorization for you to choose that version for the
165
+ Library.
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in ruby_job.gemspec
8
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A sample Guardfile
4
+ # More info at https://github.com/guard/guard#readme
5
+
6
+ ## Uncomment and set this to only include directories you want to watch
7
+ # directories %w(app lib config test spec features) \
8
+ # .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")}
9
+
10
+ ## Note: if you are using the `directories` clause above and you are not
11
+ ## watching the project directory ('.'), then you will want to move
12
+ ## the Guardfile to a watched dir and symlink it back, e.g.
13
+ #
14
+ # $ mkdir config
15
+ # $ mv Guardfile config/
16
+ # $ ln -s config/Guardfile .
17
+ #
18
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
19
+
20
+ # Note: The cmd option is now required due to the increasing number of ways
21
+ # rspec may be run, below are examples of the most common uses.
22
+ # * bundler: 'bundle exec rspec'
23
+ # * bundler binstubs: 'bin/rspec'
24
+ # * spring: 'bin/rspec' (This will use spring if running and you have
25
+ # installed the spring binstubs per the docs)
26
+ # * zeus: 'zeus rspec' (requires the server to be started separately)
27
+ # * 'just' rspec: 'rspec'
28
+
29
+ guard :rspec, cmd: 'bundle exec rspec' do
30
+ require 'guard/rspec/dsl'
31
+ dsl = Guard::RSpec::Dsl.new(self)
32
+
33
+ # Feel free to open issues for suggestions and improvements
34
+
35
+ # RSpec files
36
+ rspec = dsl.rspec
37
+ watch(rspec.spec_helper) { rspec.spec_dir }
38
+ watch(rspec.spec_support) { rspec.spec_dir }
39
+ watch(rspec.spec_files)
40
+
41
+ # Ruby files
42
+ ruby = dsl.ruby
43
+ dsl.watch_spec_files_for(ruby.lib_files)
44
+
45
+ # Turnip features and steps
46
+ watch(%r{^spec/acceptance/(.+)\.feature$})
47
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
48
+ Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance'
49
+ end
50
+ end
data/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Copyright (c) 2019 Marco Imperatore
2
+
3
+ RubyJob is an Open Source project licensed under the terms of
4
+ the LGPLv3 license. Please see COPYING.LESSER or
5
+ <http://www.gnu.org/licenses/lgpl-3.0.html> for license text.
data/LICENSE.txt ADDED
@@ -0,0 +1,5 @@
1
+ Copyright (c) 2019 Marco Imperatore
2
+
3
+ RubyJob is an Open Source project licensed under the terms of
4
+ the LGPLv3 license. Please see COPYING.LESSER or
5
+ <http://www.gnu.org/licenses/lgpl-3.0.html> for license text.
data/README.md ADDED
@@ -0,0 +1,226 @@
1
+ [![Code Climate](https://codeclimate.com/github/mimperatore/ruby_job.svg)](https://codeclimate.com/github/mimperatore/ruby_job)
2
+ [![Test Coverage](https://codeclimate.com/github/mimperatore/ruby_job/badges/coverage.svg)](https://codeclimate.com/github/mimperator/ruby_job/coverage)
3
+ [![Build Status](https://travis-ci.com/mimperatore/ruby_job.svg?branch=master)](https://codecov.io/gh/mimperatore/ruby_job/branch/master)
4
+
5
+ # RubyJob
6
+
7
+ RubyJob is a framework for running jobs.
8
+
9
+ The current version behaves much like [Sucker Punch](https://github.com/brandonhilkert/sucker_punch), in that it
10
+ only supports an [In-Memory Job Store](https://github.com/mimperatore/ruby_job/blob/master/lib/ruby_job/in_memory_job_store.rb)
11
+ implemented through a fast [Fibonacci Heap](https://github.com/mudge/fibonacci_heap).
12
+
13
+ The initial version, which supports only a single queue, runs **200% faster than Sucker Punch**, capable of processing **1,000,000** simple jobs in **28 seconds**
14
+ vs. Sucker Punch's 59 seconds (measured on on a MacBook Pro 2.3GHz with 16GB of RAM).
15
+
16
+ Additional features are in the works, including:
17
+ - Support for multiple queues & queue priorities
18
+ - Persistent Job Stores for:
19
+ - Redis
20
+ - Cassandra
21
+ - Batches & Job nesting
22
+
23
+ ## Installation
24
+
25
+ Add this line to your application's Gemfile:
26
+
27
+ ```ruby
28
+ gem 'ruby_job'
29
+ ```
30
+
31
+ And then execute:
32
+
33
+ $ bundle
34
+
35
+ Or install it yourself as:
36
+
37
+ $ gem install ruby_job
38
+
39
+ ## Usage
40
+
41
+ ### A simple example
42
+
43
+ #### Define your worker class
44
+ ```ruby
45
+ class MyWorker
46
+ include RubyJob::Worker
47
+
48
+ def perform
49
+ #job code goes here
50
+ end
51
+ end
52
+ ```
53
+
54
+ #### Setup your JobStore
55
+ ```ruby
56
+ MyWorker.jobstore = RubyJob::InMemoryJobStore.new
57
+ MyWorker.perform_async
58
+ ```
59
+
60
+ #### Run your server
61
+ ```ruby
62
+ server = RubyJob::ThreadedServer.new(num_threads: 10, jobstore: MyWorker.jobstore)
63
+ server_thread = server.start
64
+ server_thread.join
65
+ ```
66
+
67
+ ### Setting up the default JobStore
68
+ Jobs are enqueued to the default JobStore of the worker class:
69
+
70
+ ```ruby
71
+ MyWorker.jobstore = RubyJob::InMemoryJobStore.new # attach the JobStore to the MyWorker class
72
+ ```
73
+
74
+ If the worker class doesn't have a JobStore attached to it, jobs will be enqueued to `Worker.jobstore`.
75
+
76
+ ```ruby
77
+ Worker.jobstore = RubyJob::InMemoryJobStore.new # jobs will be queued here, if MyWorker doesn't have `jobstore` set.
78
+ ```
79
+
80
+ ### Enqueuing jobs
81
+ There are 2 ways you can enqueue your jobs:
82
+
83
+ #### Using <i>#perform_*</i> (recommended approach)
84
+ ```ruby
85
+ MyWorker.jobstore = RubyJob::InMemoryJobStore.new
86
+ MyWorker.perform_async # will enqueue on `MyWorker.jobstore`, or `Worker.jobstore` if the former isn't set.
87
+ ```
88
+
89
+ **Note:** you must ensure either `MyWorker.jobstore` or `Worker.jobstore` is set to a valid JobStore.
90
+
91
+ #### Using Job#enqueue
92
+ ```ruby
93
+ MyWorker.jobstore = RubyJob::InMemoryJobStore.new
94
+ job = Job.new(worker_class_name: 'MyWorker', args: [], start_at: Time.now)
95
+ job.enqueue
96
+ ```
97
+
98
+ ### Dequeuing jobs
99
+ In some situations, it's important to remove a previously enqueued job from the queue, so that it does not run in the future.
100
+ To do so:
101
+ ```ruby
102
+ job.dequeue
103
+ ```
104
+
105
+ ### Schedule a Job for execution (asynchronously)
106
+
107
+ **Note:** Jobs are scheduled to nearest **millisecond** of the specified start time.
108
+
109
+ #### Immediately (ASAP)
110
+ ```ruby
111
+ MyWorker.perform_async # schedule to run asynchonously, asap
112
+ ```
113
+
114
+ #### Delayed
115
+ ```ruby
116
+ MyWorker.perform_in(5.5) # schedule to run asynchonously, in 5.5 seconds
117
+ ```
118
+
119
+ #### At a specific time
120
+ ```ruby
121
+ MyWorker.perform_at(a_particular_time) # schedule to run asynchonously, at the specified time
122
+ ```
123
+
124
+ ### Executing a Job immediately (synchronously)
125
+ ```ruby
126
+ MyWorker.perform # run the job synchronously now
127
+ ```
128
+
129
+ ### Threaded Server (the job processor)
130
+ A threaded server is provided to process the queued jobs. It is instantiated by specifying the number of workers (threads) to spawn,
131
+ and the JobStore it will be processing.
132
+ ```ruby
133
+ server = RubyJob::ThreadedServer.new(num_threads: 10, jobstore: MyWorker.jobstore)
134
+ ```
135
+
136
+ #### Server options
137
+ ```ruby
138
+ server.set(wait: true)
139
+ ```
140
+
141
+ - `wait`[boolean]: determines whether the server should exit when there aren't any processable jobs in the queue. Defaults to `true`.
142
+ - `wait_delay`[float]: number of seconds to wait (sleep). Defaults to `0.5`.
143
+
144
+
145
+ #### Starting the server
146
+ Queued jobs will only run when a Server, attached to the JobStore the jobs have been enqueued to, has been started.
147
+
148
+ ```ruby
149
+ server_thread = server.start
150
+ server_thread.join # if needed, depending on your use case
151
+ ```
152
+
153
+ #### Halting the server
154
+ A running server can be halted as follows:
155
+ ```ruby
156
+ server.halt_at(Time.now + 5)
157
+ ```
158
+
159
+ ```ruby
160
+ server.halt # equivalent to halt_at(Time.now)
161
+ ```
162
+
163
+ `Halting` causes the server to stop processing jobs scheduled to start after the specified halt time. Once the halt time has been
164
+ reached, the server waits if the `wait` option is `true`, or exits otherwise.
165
+
166
+ Halting the server can be useful in production, when you want to temporarily pause job processing.
167
+
168
+ #### Resuming the server
169
+ A halted server can be resumed with:
170
+ ```ruby
171
+ server.resume
172
+ ```
173
+
174
+ ```ruby
175
+ server.resume_until(Time.now + 5) # equivalent to: resume && halt_at(Time.now + 5)
176
+ ```
177
+
178
+ With `resume`, the server picks up jobs from where it left off and keeps processing them as if it never stopped. Note that a server
179
+ that's been halted for a significant amount of time will pick up old jobs that may have been intended to start significantly in the past, so
180
+ ensure you take that into account in your job processing code if you care about this situation.
181
+
182
+ ### Retries
183
+ Jobs will be not be retried by default. To have jobs retry, the worker class must define a `retry?` method that
184
+ returns a tuple indicating whether the job should be retried, and how long the retry delay should be: [do_retry, retry_delay]
185
+ ```ruby
186
+ MAX_RETRIES = 5
187
+ INITIAL_RETRY_DELAY = 0.5
188
+
189
+ def retry?(attempt:, error:)
190
+ # determine whether a retry is required, based on the attempt number and error passed in
191
+ do_retry = error.is_a?(RetriableError) && (attempt < MAX_RETRIES)
192
+
193
+ [do_retry, INITIAL_RETRY_DELAY * 2**(attempt-1)] # exponential backoff
194
+ end
195
+ ```
196
+
197
+ `attempt` starts at `1` and `error` is the exception that was raised by the last attempt.
198
+
199
+ **Note:** the current implementation uses `sleep` to implement the retry delay. This isn't ideal, as it prevents the thread
200
+ processing the job from servicing another job that's ready to run. In the future, this will be changed such that the job
201
+ is put back onto the job queue to start at a later time. Feel free to put together a PR if you're interested in seeing this
202
+ change sooner rather than later.
203
+
204
+ **Note:** the retry delay is the time between the end of the last attempt and the start of the new attempt
205
+
206
+ ## Development
207
+
208
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an
209
+ interactive prompt that will allow you to experiment.
210
+
211
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and
212
+ then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
213
+
214
+ ## Contributing
215
+
216
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mimperatore/ruby_job. This project is intended to be a safe, welcoming space
217
+ for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
218
+
219
+ ## License
220
+
221
+ The gem is available as open source under the terms of the [GNU Lesser General Public License Version 3 (LGPLv3)](https://www.gnu.org/licenses/lgpl-3.0.html).
222
+
223
+ ## Code of Conduct
224
+
225
+ Everyone interacting in the RubyJob project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow
226
+ the [code of conduct](https://github.com/mimperatore/ruby_job/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
data/_config.yml ADDED
@@ -0,0 +1 @@
1
+ theme: jekyll-theme-cayman
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'ruby_job'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/codecov.yml ADDED
@@ -0,0 +1,28 @@
1
+ ignore:
2
+ - spec/**
3
+
4
+ codecov:
5
+ require_ci_to_pass: yes
6
+
7
+ coverage:
8
+ precision: 2
9
+ round: down
10
+ range: "99...100"
11
+
12
+ status:
13
+ project: yes
14
+ patch: yes
15
+ changes: no
16
+
17
+ parsers:
18
+ gcov:
19
+ branch_detection:
20
+ conditional: yes
21
+ loop: yes
22
+ method: yes
23
+ macro: no
24
+
25
+ comment:
26
+ layout: "reach,diff,flags,tree"
27
+ behavior: default
28
+ require_changes: no
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'fibonacci_heap'
5
+ require 'digest/sha1'
6
+
7
+ module RubyJob
8
+ class InMemoryJobStore < JobStore
9
+ attr_reader :pause_starting_at
10
+
11
+ def initialize
12
+ super
13
+ @semaphore = Mutex.new
14
+ @next_uuid = 0
15
+ @pause_starting_at = nil
16
+ end
17
+
18
+ def enqueue(job)
19
+ raise 'job does not have an assigned uuid' unless job.uuid
20
+
21
+ @semaphore.synchronize { queue.push(job) }
22
+ end
23
+
24
+ def dequeue(job)
25
+ @semaphore.synchronize { queue.delete(job) }
26
+ end
27
+
28
+ def pause_at(time)
29
+ @pause_starting_at = time
30
+ end
31
+
32
+ def fetch
33
+ @options[:wait] ? fetch_next_or_wait : fetch_next
34
+ end
35
+
36
+ def size
37
+ queue.size
38
+ end
39
+
40
+ def next_uuid
41
+ @semaphore.synchronize { @next_uuid += 1 }
42
+ end
43
+
44
+ private
45
+
46
+ def queue
47
+ @queue ||= JobPriorityQueue.new
48
+ end
49
+
50
+ def paused_before?(time)
51
+ @pause_starting_at && @pause_starting_at <= time
52
+ end
53
+
54
+ def fetch_next
55
+ @semaphore.synchronize do
56
+ queue.pop if (top = queue.top) && top.start_at <= [Time.now, @pause_starting_at].compact.min
57
+ end
58
+ end
59
+
60
+ def fetch_next_or_wait
61
+ job = nil
62
+ loop do
63
+ job = fetch_next
64
+ break if job || (paused_before?(Time.now) && !@options[:wait])
65
+
66
+ sleep(@options[:wait_delay])
67
+ end
68
+ job
69
+ end
70
+
71
+ class JobPriorityQueue
72
+ extend Forwardable
73
+
74
+ def_delegators :@pqueue, :size
75
+
76
+ def initialize
77
+ @pqueue = FibonacciHeap::Heap.new
78
+ end
79
+
80
+ def push(job)
81
+ @pqueue.insert(job, key_for(job))
82
+ end
83
+
84
+ def pop
85
+ @pqueue.pop
86
+ end
87
+
88
+ def top
89
+ @pqueue.min
90
+ end
91
+
92
+ def delete(job)
93
+ @pqueue.delete(job)
94
+ end
95
+
96
+ private
97
+
98
+ def key_for(job)
99
+ job.start_at.to_f.round(3) + job.uuid.to_f / 1_000
100
+ end
101
+ end
102
+ end
103
+ end