good_job 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.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +87 -0
- data/bin/good_job +3 -0
- data/lib/good_job/adapter.rb +32 -0
- data/lib/good_job/cli.rb +19 -0
- data/lib/good_job/inline_scheduler.rb +10 -0
- data/lib/good_job/job.rb +6 -0
- data/lib/good_job/job_wrapper.rb +16 -0
- data/lib/good_job/lockable.rb +97 -0
- data/lib/good_job/railtie.rb +4 -0
- data/lib/good_job/scheduler.rb +89 -0
- data/lib/good_job/version.rb +3 -0
- data/lib/good_job.rb +12 -0
- metadata +155 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9604c74cf4073b178401ec0cb606e55732f087464058bebe968b666c31c242d4
|
4
|
+
data.tar.gz: f4af6118d55f231d15c3ffa2a3aac9f625911889a4f1adce5c14dcb2626af4d5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 46e92a0efba3937d274942ea55371f65cc3aa0451bc625bff6856be09525446960d192473548820bf1c289713151ee82e5cf1bdef21252da6a0927baafe5cc9c
|
7
|
+
data.tar.gz: 9e1d108190398d5d698713e12ba981d63ee121f095185990f1f0963bee36e4f4760a22c9f64f8148624ba6be931a89943e48a601c4395ad825254d5d23c86c58
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2020 Ben Sheldon
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# GoodJob
|
2
|
+
|
3
|
+
GoodJob is a multithreaded, Postgres-based ActiveJob backend for Ruby on Rails.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
1. Create a database migration:
|
8
|
+
```bash
|
9
|
+
bin/rails g migration CreateGoodJobs
|
10
|
+
```
|
11
|
+
|
12
|
+
And then add to the newly created file:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
class CreateGoodJobs < ActiveRecord::Migration[6.0]
|
16
|
+
def change
|
17
|
+
enable_extension 'pgcrypto'
|
18
|
+
|
19
|
+
create_table :good_jobs, id: :uuid do |t|
|
20
|
+
t.timestamps
|
21
|
+
|
22
|
+
t.text :queue_name
|
23
|
+
t.integer :priority
|
24
|
+
t.jsonb :serialized_params
|
25
|
+
t.timestamp :scheduled_at
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
```
|
30
|
+
1. Configure the ActiveJob adapter:
|
31
|
+
```ruby
|
32
|
+
# config/environments/production.rb
|
33
|
+
config.active_job.queue_adapter = GoodJob::Adapter.new
|
34
|
+
|
35
|
+
# config/environments/development.rb
|
36
|
+
config.active_job.queue_adapter = GoodJob::Adapter.new(inline: true)
|
37
|
+
```
|
38
|
+
|
39
|
+
1. In production, the scheduler is designed to run in its own process:
|
40
|
+
```bash
|
41
|
+
$ bundle exec good_job
|
42
|
+
```
|
43
|
+
|
44
|
+
## Installation
|
45
|
+
Add this line to your application's Gemfile:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
gem 'good_job', github: 'bensheldon/good_job'
|
49
|
+
```
|
50
|
+
|
51
|
+
And then execute:
|
52
|
+
```bash
|
53
|
+
$ bundle
|
54
|
+
```
|
55
|
+
|
56
|
+
## Development
|
57
|
+
|
58
|
+
To run tests:
|
59
|
+
|
60
|
+
```bash
|
61
|
+
# Clone the repository locally
|
62
|
+
$ git clone git@github.com:bensheldon/good_job.git
|
63
|
+
|
64
|
+
# Set up the local environment
|
65
|
+
$ bin/setup_test
|
66
|
+
|
67
|
+
# Run the tests
|
68
|
+
$ bin/rspec
|
69
|
+
```
|
70
|
+
|
71
|
+
For developing locally within another Ruby on Rails project:
|
72
|
+
|
73
|
+
```bash
|
74
|
+
# Within Ruby on Rails directory...
|
75
|
+
$ bundle config local.good_job /path/to/local/git/repository
|
76
|
+
|
77
|
+
# Confirm that the local copy is used
|
78
|
+
$ bundle install
|
79
|
+
|
80
|
+
# => Using good_job 0.1.0 from https://github.com/bensheldon/good_job.git (at /Users/You/Projects/good_job@dc57fb0)
|
81
|
+
```
|
82
|
+
|
83
|
+
## Contributing
|
84
|
+
Contribution directions go here.
|
85
|
+
|
86
|
+
## License
|
87
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/bin/good_job
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module GoodJob
|
2
|
+
class Adapter
|
3
|
+
def initialize(options = {})
|
4
|
+
@options = options
|
5
|
+
@scheduler = InlineScheduler.new if inline?
|
6
|
+
end
|
7
|
+
|
8
|
+
def inline?
|
9
|
+
@options.fetch(:inline, false)
|
10
|
+
end
|
11
|
+
|
12
|
+
def enqueue(job)
|
13
|
+
enqueue_at(job, nil)
|
14
|
+
end
|
15
|
+
|
16
|
+
def enqueue_at(job, timestamp)
|
17
|
+
params = {
|
18
|
+
queue_name: job.queue_name,
|
19
|
+
priority: job.priority,
|
20
|
+
serialized_params: job.serialize,
|
21
|
+
}
|
22
|
+
params[:scheduled_at] = Time.at(timestamp) if timestamp
|
23
|
+
|
24
|
+
good_job = GoodJob::Job.create(params)
|
25
|
+
@scheduler.enqueue(good_job) if inline?
|
26
|
+
end
|
27
|
+
|
28
|
+
def shutdown(wait: true)
|
29
|
+
@scheduler&.shutdown(wait: wait)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/good_job/cli.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module GoodJob
|
4
|
+
class CLI < Thor
|
5
|
+
RAILS_ENVIRONMENT_RB = File.expand_path("config/environment.rb")
|
6
|
+
|
7
|
+
desc :start, "Start jobs"
|
8
|
+
def start
|
9
|
+
require RAILS_ENVIRONMENT_RB
|
10
|
+
|
11
|
+
scheduler = GoodJob::Scheduler.new
|
12
|
+
Kernel.loop do
|
13
|
+
sleep 1
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
default_task :start
|
18
|
+
end
|
19
|
+
end
|
data/lib/good_job/job.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module GoodJob
|
2
|
+
class JobWrapper
|
3
|
+
def initialize(good_job)
|
4
|
+
@good_job = good_job
|
5
|
+
end
|
6
|
+
|
7
|
+
def perform
|
8
|
+
serialized_params = @good_job.serialized_params.merge(
|
9
|
+
"provider_job_id" => @good_job.id
|
10
|
+
)
|
11
|
+
ActiveJob::Base.execute(serialized_params)
|
12
|
+
|
13
|
+
@good_job.destroy!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module GoodJob
|
2
|
+
module Lockable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
RecordAlreadyAdvisoryLockedError = Class.new(StandardError)
|
6
|
+
|
7
|
+
included do
|
8
|
+
scope :joins_advisory_locks, (lambda do
|
9
|
+
joins(<<~SQL)
|
10
|
+
LEFT JOIN pg_locks ON pg_locks.locktype = 'advisory'
|
11
|
+
AND pg_locks.objsubid = 1
|
12
|
+
AND pg_locks.classid = ('x'||substr(md5(good_jobs.id::text), 1, 16))::bit(32)::int
|
13
|
+
AND pg_locks.objid = (('x'||substr(md5(good_jobs.id::text), 1, 16))::bit(64) << 32)::bit(32)::int
|
14
|
+
SQL
|
15
|
+
end)
|
16
|
+
|
17
|
+
scope :advisory_unlocked, -> { joins_advisory_locks.where(pg_locks: { locktype: nil }) }
|
18
|
+
scope :with_advisory_lock, (lambda do
|
19
|
+
where(<<~SQL)
|
20
|
+
pg_try_advisory_lock(('x'||substr(md5(id::text), 1, 16))::bit(64)::bigint)
|
21
|
+
SQL
|
22
|
+
end)
|
23
|
+
|
24
|
+
def self.first_advisory_locked_row(query)
|
25
|
+
find_by_sql(<<~SQL)
|
26
|
+
WITH rows AS (#{query.to_sql})
|
27
|
+
SELECT rows.id
|
28
|
+
FROM rows
|
29
|
+
WHERE pg_try_advisory_lock(('x'||substr(md5(id::text), 1, 16))::bit(64)::bigint)
|
30
|
+
SQL
|
31
|
+
end
|
32
|
+
# private_class_method :first_advisory_locked_row
|
33
|
+
|
34
|
+
# https://www.postgresql.org/docs/9.6/view-pg-locks.html
|
35
|
+
# Advisory locks can be acquired on keys consisting of either a single bigint value or two integer values.
|
36
|
+
# A bigint key is displayed with its high-order half in the classid column, its low-order half in the objid column, and objsubid equal to 1.
|
37
|
+
# The original bigint value can be reassembled with the expression (classid::bigint << 32) | objid::bigint.
|
38
|
+
# Integer keys are displayed with the first key in the classid column, the second key in the objid column, and objsubid equal to 2.
|
39
|
+
# The actual meaning of the keys is up to the user. Advisory locks are local to each database, so the database column is meaningful for an advisory lock.
|
40
|
+
def self.advisory_lock_details
|
41
|
+
connection.select("SELECT * FROM pg_locks WHERE locktype = 'advisory' AND objsubid = 1")
|
42
|
+
end
|
43
|
+
|
44
|
+
def advisory_lock
|
45
|
+
self.class.connection.execute(self.class.sanitize_sql_for_conditions(["SELECT 1 as one WHERE pg_try_advisory_lock(('x'||substr(md5(?), 1, 16))::bit(64)::bigint)", id])).ntuples > 0
|
46
|
+
end
|
47
|
+
|
48
|
+
def advisory_lock!
|
49
|
+
result = advisory_lock
|
50
|
+
result || raise(RecordAlreadyAdvisoryLockedError)
|
51
|
+
end
|
52
|
+
|
53
|
+
def with_advisory_lock
|
54
|
+
begin
|
55
|
+
advisory_lock!
|
56
|
+
yield
|
57
|
+
rescue StandardError => e
|
58
|
+
advisory_unlock unless e.is_a? RecordAlreadyAdvisoryLockedError
|
59
|
+
raise
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def advisory_locked?
|
64
|
+
self.class.connection.execute(<<~SQL).ntuples > 0
|
65
|
+
SELECT 1 as one
|
66
|
+
FROM pg_locks
|
67
|
+
WHERE
|
68
|
+
locktype = 'advisory'
|
69
|
+
AND objsubid = 1
|
70
|
+
AND classid = ('x'||substr(md5('#{id}'), 1, 16))::bit(32)::int
|
71
|
+
AND objid = (('x'||substr(md5('#{id}'), 1, 16))::bit(64) << 32)::bit(32)::int
|
72
|
+
SQL
|
73
|
+
end
|
74
|
+
|
75
|
+
def owns_advisory_lock?
|
76
|
+
self.class.connection.execute(<<~SQL).ntuples > 0
|
77
|
+
SELECT 1 as one
|
78
|
+
FROM pg_locks
|
79
|
+
WHERE
|
80
|
+
locktype = 'advisory'
|
81
|
+
AND objsubid = 1
|
82
|
+
AND classid = ('x'||substr(md5('#{id}'), 1, 16))::bit(32)::int
|
83
|
+
AND objid = (('x'||substr(md5('#{id}'), 1, 16))::bit(64) << 32)::bit(32)::int
|
84
|
+
AND pid = pg_backend_pid()
|
85
|
+
SQL
|
86
|
+
end
|
87
|
+
|
88
|
+
def advisory_unlock
|
89
|
+
self.class.connection.execute("SELECT pg_advisory_unlock(('x'||substr(md5('#{id}'), 1, 16))::bit(64)::bigint)").first["pg_advisory_unlock"]
|
90
|
+
end
|
91
|
+
|
92
|
+
def advisory_unlock!
|
93
|
+
advisory_unlock while advisory_locked?
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "concurrent/scheduled_task"
|
2
|
+
require "concurrent/executor/thread_pool_executor"
|
3
|
+
require "concurrent/utility/processor_counter"
|
4
|
+
|
5
|
+
module GoodJob
|
6
|
+
class Scheduler
|
7
|
+
MINIMUM_EXECUTION_INTERVAL = 0.1
|
8
|
+
|
9
|
+
DEFAULT_TIMER_OPTIONS = {
|
10
|
+
execution_interval: 1,
|
11
|
+
timeout_interval: 1,
|
12
|
+
run_now: true
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
MAX_THREADS = Concurrent.processor_count
|
16
|
+
|
17
|
+
DEFAULT_POOL_OPTIONS = {
|
18
|
+
name: 'good_job',
|
19
|
+
min_threads: 0,
|
20
|
+
max_threads: MAX_THREADS,
|
21
|
+
auto_terminate: true,
|
22
|
+
idletime: 0,
|
23
|
+
max_queue: 0,
|
24
|
+
fallback_policy: :abort # shouldn't matter -- 0 max queue
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
def initialize(query = GoodJob::Job.all, **options)
|
28
|
+
@query = query
|
29
|
+
|
30
|
+
@pool = Concurrent::ThreadPoolExecutor.new(DEFAULT_POOL_OPTIONS)
|
31
|
+
@timer = Concurrent::TimerTask.new(DEFAULT_TIMER_OPTIONS) do
|
32
|
+
idle_threads = @pool.max_length - @pool.length
|
33
|
+
puts "There are idle_threads: #{idle_threads}"
|
34
|
+
create_thread if idle_threads.positive?
|
35
|
+
true
|
36
|
+
end
|
37
|
+
@timer.execute
|
38
|
+
end
|
39
|
+
|
40
|
+
def execute
|
41
|
+
end
|
42
|
+
|
43
|
+
def shutdown(wait: true)
|
44
|
+
if @timer.running?
|
45
|
+
@timer.shutdown
|
46
|
+
@timer.wait_for_termination if wait
|
47
|
+
end
|
48
|
+
|
49
|
+
if @pool.running?
|
50
|
+
@pool.shutdown
|
51
|
+
@pool.wait_for_termination if wait
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_thread
|
56
|
+
future = Concurrent::Future.new(args: [@query], executor: @pool) do |query|
|
57
|
+
Rails.application.executor.wrap do
|
58
|
+
thread_name = Thread.current.name || Thread.current.object_id
|
59
|
+
while job = query.with_advisory_lock.first
|
60
|
+
puts "Executing job #{job.id} in thread #{thread_name}"
|
61
|
+
|
62
|
+
JobWrapper.new(job).perform
|
63
|
+
|
64
|
+
job.advisory_unlock
|
65
|
+
end
|
66
|
+
true
|
67
|
+
end
|
68
|
+
end
|
69
|
+
future.add_observer(TaskObserver.new(self))
|
70
|
+
future.execute
|
71
|
+
end
|
72
|
+
|
73
|
+
class TaskObserver
|
74
|
+
def initialize(scheduler)
|
75
|
+
@scheduler = scheduler
|
76
|
+
end
|
77
|
+
|
78
|
+
def update(time, result, ex)
|
79
|
+
if result
|
80
|
+
puts "(#{time}) Execution successfully returned #{result}\n"
|
81
|
+
elsif ex.is_a?(Concurrent::TimeoutError)
|
82
|
+
puts "(#{time}) Execution timed out\n"
|
83
|
+
else
|
84
|
+
puts "(#{time}) Execution failed with error #{result} #{ex}\n"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/good_job.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "rails"
|
2
|
+
|
3
|
+
require 'good_job/lockable'
|
4
|
+
require 'good_job/job'
|
5
|
+
require 'good_job/inline_scheduler'
|
6
|
+
require "good_job/scheduler"
|
7
|
+
require "good_job/job_wrapper"
|
8
|
+
require 'good_job/adapter'
|
9
|
+
|
10
|
+
module GoodJob
|
11
|
+
# Your code goes here...
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: good_job
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ben Sheldon
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-03-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: concurrent-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: thor
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: database_cleaner
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: gem-release
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pg
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec-rails
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: GoodJob is a minimal postgres based job queue system for Rails
|
112
|
+
email:
|
113
|
+
- bensheldon@gmail.com
|
114
|
+
executables:
|
115
|
+
- good_job
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- LICENSE.txt
|
120
|
+
- README.md
|
121
|
+
- bin/good_job
|
122
|
+
- lib/good_job.rb
|
123
|
+
- lib/good_job/adapter.rb
|
124
|
+
- lib/good_job/cli.rb
|
125
|
+
- lib/good_job/inline_scheduler.rb
|
126
|
+
- lib/good_job/job.rb
|
127
|
+
- lib/good_job/job_wrapper.rb
|
128
|
+
- lib/good_job/lockable.rb
|
129
|
+
- lib/good_job/railtie.rb
|
130
|
+
- lib/good_job/scheduler.rb
|
131
|
+
- lib/good_job/version.rb
|
132
|
+
homepage: https://github.com/benheldon/good_job
|
133
|
+
licenses:
|
134
|
+
- MIT
|
135
|
+
metadata: {}
|
136
|
+
post_install_message:
|
137
|
+
rdoc_options: []
|
138
|
+
require_paths:
|
139
|
+
- lib
|
140
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
requirements: []
|
151
|
+
rubygems_version: 3.1.2
|
152
|
+
signing_key:
|
153
|
+
specification_version: 4
|
154
|
+
summary: GoodJob is a minimal postgres based job queue system for Rails
|
155
|
+
test_files: []
|