pg_jobs 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/MIT-LICENSE +20 -0
- data/README.md +83 -0
- data/Rakefile +47 -0
- data/app/models/pg_job.rb +66 -0
- data/db/migrate/20170317150614_create_jobs.rb +14 -0
- data/lib/active_job/queue_adapters/pg_jobs_adapter.rb +17 -0
- data/lib/pg_jobs.rb +88 -0
- data/lib/pg_jobs/engine.rb +4 -0
- data/lib/pg_jobs/version.rb +3 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2ce76759a48458e681b0c2b619dae3848f83d776615fff1a3890d0743748070c
|
4
|
+
data.tar.gz: 886badb6799a69f7eb0d257f070d052a978ad715616e26560701d21be19bc2df
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a1b93e2389c7283e54d4c0d28d70441f9fe79c667beaf9c088b776d379ebec58c9d3d70cdfd8863a54cbed1e286508568c8b90fb843c8e310d4fecfa1bb67e74
|
7
|
+
data.tar.gz: 7003bed8eb73bacc539466ddc770acfba1e3d8b52e7eb9567a808f47fedf3f656d6d827083272cf04c9e5d352fd808ed8461631c4d0d7ecf984a4cb939ec6fb1
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2017 Moritz Breit
|
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,83 @@
|
|
1
|
+
[](https://rubygems.org/gems/pg_jobs)
|
2
|
+
[](https://github.com/mbreit/pg_jobs/blob/codeclimate/MIT-LICENSE)
|
3
|
+
[](https://travis-ci.com/mbreit/pg_jobs)
|
4
|
+
[](https://codeclimate.com/github/mbreit/pg_jobs)
|
5
|
+
[](https://codeclimate.com/github/mbreit/pg_jobs)
|
6
|
+
[](https://inch-ci.org/github/mbreit/pg_jobs)
|
7
|
+
|
8
|
+
# PgJobs
|
9
|
+
|
10
|
+
Simple Active Job worker for PostgreSQL using LISTEN/NOTIFY and
|
11
|
+
SKIP LOCKED.
|
12
|
+
|
13
|
+
Supports most Active Job features like multiple queues, priorities
|
14
|
+
and wait times.
|
15
|
+
|
16
|
+
## Dependencies
|
17
|
+
|
18
|
+
* PostgreSQL >= 9.5 to use SKIP LOCKED
|
19
|
+
* Ruby >= 2.3
|
20
|
+
* Rails >= 5.1
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Add this line to your application's Gemfile:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem 'pg_jobs'
|
28
|
+
```
|
29
|
+
|
30
|
+
And then execute:
|
31
|
+
|
32
|
+
```bash
|
33
|
+
bundle
|
34
|
+
```
|
35
|
+
|
36
|
+
Then copy the migrations and migrate your database:
|
37
|
+
|
38
|
+
```bash
|
39
|
+
bin/rails pg_jobs_engine:install:migrations
|
40
|
+
bin/rails db:migrate
|
41
|
+
```
|
42
|
+
|
43
|
+
To configure the Active Job adapter add this to your environment
|
44
|
+
configuration (config/environments/production.rb):
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
config.active_job.queue_adapter = :pg_jobs
|
48
|
+
```
|
49
|
+
|
50
|
+
If you want to run all your jobs in one queue, we recommend to configure
|
51
|
+
ActionMailer to use the `default` queue:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
config.action_mailer.deliver_later_queue_name = 'default'
|
55
|
+
```
|
56
|
+
|
57
|
+
## Usage
|
58
|
+
|
59
|
+
Just schedule your work with Active Job, then run one or multiple
|
60
|
+
workers for the default queue with
|
61
|
+
|
62
|
+
```bash
|
63
|
+
bin/rails runner PgJobs.work
|
64
|
+
```
|
65
|
+
|
66
|
+
or for other queues with
|
67
|
+
|
68
|
+
```bash
|
69
|
+
bin/rails runner "PgJobs.work(:my_queue)"
|
70
|
+
```
|
71
|
+
|
72
|
+
For more documentation about Active Job and how to use different queues,
|
73
|
+
scheduled jobs, priorities and error handling, see the
|
74
|
+
[Active Job Rails Guide](https://guides.rubyonrails.org/active_job_basics.html).
|
75
|
+
|
76
|
+
## Contributing
|
77
|
+
|
78
|
+
Use Github issues and pull requests.
|
79
|
+
|
80
|
+
## License
|
81
|
+
|
82
|
+
The gem is available as open source under the terms of the
|
83
|
+
[MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'PgJobs'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'bundler/gem_tasks'
|
18
|
+
|
19
|
+
namespace :db do
|
20
|
+
task :init do
|
21
|
+
require 'zlib'
|
22
|
+
require 'active_record'
|
23
|
+
|
24
|
+
ActiveRecord::Base.configurations = YAML.load_file('test/database.yml')
|
25
|
+
ActiveRecord::Tasks::DatabaseTasks.migrations_paths = [File.expand_path('db/migrate', __dir__)]
|
26
|
+
end
|
27
|
+
|
28
|
+
desc 'Create test database'
|
29
|
+
task create: :init do
|
30
|
+
ActiveRecord::Tasks::DatabaseTasks.create_current('test')
|
31
|
+
end
|
32
|
+
|
33
|
+
desc 'Migrate test database'
|
34
|
+
task migrate: :init do
|
35
|
+
ActiveRecord::Tasks::DatabaseTasks.migrate
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
require 'rake/testtask'
|
40
|
+
|
41
|
+
Rake::TestTask.new(:test) do |t|
|
42
|
+
t.libs << 'test'
|
43
|
+
t.libs << 'app/models'
|
44
|
+
t.test_files = FileList['test/**/*_test.rb']
|
45
|
+
end
|
46
|
+
|
47
|
+
task default: ['db:create', 'db:migrate', :test]
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# ActiveRecord model for jobs
|
2
|
+
#
|
3
|
+
# Schema:
|
4
|
+
#
|
5
|
+
# | column | type | default | null |
|
6
|
+
# |---------------|-----------|-----------|-------|
|
7
|
+
# | job_data | jsonb | | false |
|
8
|
+
# | priority | integer | 100 | false |
|
9
|
+
# | queue_name | string | 'default' | false |
|
10
|
+
# | created_at | timestamp | | false |
|
11
|
+
# | scheduled_for | timestamp | | true |
|
12
|
+
class PgJob < ActiveRecord::Base
|
13
|
+
scope :due, -> { where('scheduled_for IS NULL OR scheduled_for <= ?', Time.current) }
|
14
|
+
scope :queue, ->(name) { where(queue_name: name).order(:priority, :created_at) }
|
15
|
+
|
16
|
+
validates :queue_name, format: { with: /\A[a-zA-Z0-9_]+\z/ }
|
17
|
+
|
18
|
+
after_create :notify_workers
|
19
|
+
|
20
|
+
# Yields a single job from the given queue that is scheduled for
|
21
|
+
# execution now. Does not block.
|
22
|
+
#
|
23
|
+
# Uses row locking with `SELECT ... FOR UPDATE SKIP LOCKED`
|
24
|
+
# to prevent race conditions.
|
25
|
+
#
|
26
|
+
# Returns false if no job has been found.
|
27
|
+
#
|
28
|
+
# @param queue_name [String] Name of the queue to look for a due job
|
29
|
+
def self.yield_job(queue_name)
|
30
|
+
transaction do
|
31
|
+
job = queue(queue_name).due.lock('FOR UPDATE SKIP LOCKED').first
|
32
|
+
return false unless job
|
33
|
+
|
34
|
+
yield job
|
35
|
+
|
36
|
+
job.destroy!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Yields jobs when they are schedules to be executed.
|
41
|
+
# If the job queue is empty, it uses PostgreSQL LISTEN/NOTIFY support
|
42
|
+
# to block and wait for new jobs.
|
43
|
+
#
|
44
|
+
# @param queue_name [String] The name of the queue to work on
|
45
|
+
# @param timeout [integer] Interval to check for due jobs
|
46
|
+
def self.yield_jobs(queue_name, timeout, &block)
|
47
|
+
connection.execute "LISTEN pg_jobs_#{queue_name}"
|
48
|
+
loop do
|
49
|
+
# Consume all pending NOTIFY events
|
50
|
+
while connection.raw_connection.notifies; end
|
51
|
+
# Work jobs as long as there are pending jobs in the queue
|
52
|
+
while yield_job(queue_name, &block); end
|
53
|
+
# Wait for next NOTIFY event
|
54
|
+
logger.debug "[pg_jobs] [#{queue_name}] No jobs found, calling wait_for_notify(#{timeout})"
|
55
|
+
connection.raw_connection.wait_for_notify(timeout)
|
56
|
+
end
|
57
|
+
ensure
|
58
|
+
connection.execute "UNLISTEN pg_jobs_#{queue_name}"
|
59
|
+
end
|
60
|
+
|
61
|
+
# Notifies job workers that a new job is present using
|
62
|
+
# PostgreSQL NOTIFIY.
|
63
|
+
def notify_workers
|
64
|
+
PgJob.connection.execute "NOTIFY pg_jobs_#{queue_name}"
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateJobs < ActiveRecord::Migration[5.0]
|
2
|
+
def change
|
3
|
+
create_table :pg_jobs do |t|
|
4
|
+
t.jsonb :job_data, null: false
|
5
|
+
t.integer :priority, default: 100, null: false
|
6
|
+
t.string :queue_name, null: false, default: 'default'
|
7
|
+
|
8
|
+
t.timestamp :created_at, null: false
|
9
|
+
t.timestamp :scheduled_for
|
10
|
+
end
|
11
|
+
add_index :pg_jobs, %i[queue_name scheduled_for priority created_at],
|
12
|
+
name: 'index_pg_jobs_worker'
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ActiveJob
|
2
|
+
module QueueAdapters
|
3
|
+
# Adapter for ActiveJob to run jobs with pg_jobs
|
4
|
+
#
|
5
|
+
# This lives in ActiveJob::QueueAdapters module so it can be used with
|
6
|
+
# config.active_job.queue_adapter = :pg_jobs
|
7
|
+
class PgJobsAdapter
|
8
|
+
def enqueue(job)
|
9
|
+
PgJobs.enqueue(job)
|
10
|
+
end
|
11
|
+
|
12
|
+
def enqueue_at(job, timestamp)
|
13
|
+
PgJobs.enqueue(job, timestamp)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/pg_jobs.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'pg_jobs/engine'
|
2
|
+
require 'active_job/queue_adapters/pg_jobs_adapter'
|
3
|
+
|
4
|
+
# Simple ActiveJob worker for PostgreSQL using LISTEN/NOTIFY and
|
5
|
+
# SKIP LOCKED.
|
6
|
+
#
|
7
|
+
# Supports most ActiveJob features like multiple queues, priorities
|
8
|
+
# and wait times.
|
9
|
+
#
|
10
|
+
# To use this as your Rails job queue, add this to your environment
|
11
|
+
# configuration (config/environments/production.rb):
|
12
|
+
#
|
13
|
+
# config.active_job.queue_adapter = :pg_jobs
|
14
|
+
#
|
15
|
+
# Then run one or multiple workers for the default queue with
|
16
|
+
#
|
17
|
+
# bin/rails runner PgJobs.work
|
18
|
+
#
|
19
|
+
# or for other queues with
|
20
|
+
#
|
21
|
+
# bin/rails runner "PgJobs.work(:my_queue)"
|
22
|
+
#
|
23
|
+
# Needs PostgreSQL 9.5 to use SKIP LOCKED.
|
24
|
+
module PgJobs
|
25
|
+
# Run a worker process for a given queue name.
|
26
|
+
# Will run all scheduled jobs in the queue ordered by their
|
27
|
+
# priorities (lowest first) and then wait for PostgreSQL LISTEN
|
28
|
+
# events to run new jobs. For jobs that are scheduled for a later
|
29
|
+
# time, it wakes up in an interval given by the timeout parameter
|
30
|
+
# to check for jobs that became due in the meantime.
|
31
|
+
#
|
32
|
+
# Handles SIGTERM for graceful shutdown. This signal will interrupt
|
33
|
+
# neither the execution of a job nor waiting for a new job,
|
34
|
+
# so a shorter timeout means a faster shutdown on SIGTERM.
|
35
|
+
#
|
36
|
+
# @param queue_name [String] The name of the queue to work on
|
37
|
+
# @param timeout [integer] Interval to check for due jobs
|
38
|
+
# @param exit_signals [Array<String>] Array of signal names for graceful exit
|
39
|
+
def self.work(queue_name = 'default', timeout: 10, exit_signals: %w[INT TERM])
|
40
|
+
exit_signal = false
|
41
|
+
job_running = false
|
42
|
+
|
43
|
+
exit_signals.each do |signal|
|
44
|
+
Signal.trap(signal) do
|
45
|
+
raise SignalException, signal unless job_running
|
46
|
+
|
47
|
+
# Put this message to STDERR because the logger cannot be used in a trap context
|
48
|
+
STDERR.puts "Received signal #{signal}, waiting for current job to finish"
|
49
|
+
exit_signal = true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
Rails.logger.info do
|
54
|
+
"[pg_jobs] [#{queue_name}] " \
|
55
|
+
"Starting pg_jobs worker for queue '#{queue_name}' with wait timeout #{timeout} seconds"
|
56
|
+
end
|
57
|
+
|
58
|
+
PgJob.yield_jobs(queue_name, timeout) do |pg_job|
|
59
|
+
job_running = true
|
60
|
+
execute_job(pg_job)
|
61
|
+
job_running = false
|
62
|
+
|
63
|
+
break if exit_signal
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Enqueue a new job to run at a given time or immediately
|
68
|
+
#
|
69
|
+
# @param job [ActiveJob::Base] The ActiveJob job object to schedule
|
70
|
+
# @param scheduled_for [Integer,Time] Timestamp when the job should be
|
71
|
+
# executed. Use nil if the job should be run immediately.
|
72
|
+
def self.enqueue(job, scheduled_for = nil)
|
73
|
+
PgJob.create!(job_data: job.serialize,
|
74
|
+
scheduled_for: scheduled_for && Time.at(scheduled_for),
|
75
|
+
priority: job.priority || 100,
|
76
|
+
queue_name: job.queue_name || 'default')
|
77
|
+
end
|
78
|
+
|
79
|
+
# Execute a PgJob instance. Calls `ActiveJob::Base.execute`.
|
80
|
+
def self.execute_job(pg_job)
|
81
|
+
ActiveJob::Base.execute(pg_job.job_data)
|
82
|
+
rescue => e
|
83
|
+
Rails.logger.error do
|
84
|
+
"[pg_jobs] [#{pg_job.queue_name}] [#{pg_job.job_data['job_id']}] " \
|
85
|
+
"Error while executing job: #{e}\n" + e.backtrace.join("\n")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pg_jobs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Moritz Breit
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-10-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: pg
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.18'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '2.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.18'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rails
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '5.1'
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '6.0'
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '5.1'
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '6.0'
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: minitest
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 5.9.0
|
60
|
+
type: :development
|
61
|
+
prerelease: false
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 5.9.0
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: rubocop
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - "~>"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: 0.59.1
|
74
|
+
type: :development
|
75
|
+
prerelease: false
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - "~>"
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: 0.59.1
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: simplecov
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 0.16.1
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - "~>"
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: 0.16.1
|
95
|
+
description: Simple ActiveJob queue for PostgreSQL using LISTEN/NOTIFY
|
96
|
+
email:
|
97
|
+
- mail@moritz-breit.de
|
98
|
+
executables: []
|
99
|
+
extensions: []
|
100
|
+
extra_rdoc_files: []
|
101
|
+
files:
|
102
|
+
- MIT-LICENSE
|
103
|
+
- README.md
|
104
|
+
- Rakefile
|
105
|
+
- app/models/pg_job.rb
|
106
|
+
- db/migrate/20170317150614_create_jobs.rb
|
107
|
+
- lib/active_job/queue_adapters/pg_jobs_adapter.rb
|
108
|
+
- lib/pg_jobs.rb
|
109
|
+
- lib/pg_jobs/engine.rb
|
110
|
+
- lib/pg_jobs/version.rb
|
111
|
+
homepage: https://github.com/mbreit/pg_jobs/
|
112
|
+
licenses:
|
113
|
+
- MIT
|
114
|
+
metadata: {}
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '2.3'
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
requirements: []
|
130
|
+
rubyforge_project:
|
131
|
+
rubygems_version: 2.7.6
|
132
|
+
signing_key:
|
133
|
+
specification_version: 4
|
134
|
+
summary: Simple ActiveJob queue for PostgreSQL using LISTEN/NOTIFY
|
135
|
+
test_files: []
|