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.
@@ -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
@@ -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.
@@ -0,0 +1,83 @@
1
+ [![Gem](https://img.shields.io/gem/v/pg_jobs.svg)](https://rubygems.org/gems/pg_jobs)
2
+ [![license MIT](https://img.shields.io/github/license/mbreit/pg_jobs.svg)](https://github.com/mbreit/pg_jobs/blob/codeclimate/MIT-LICENSE)
3
+ [![build](https://img.shields.io/travis/com/mbreit/pg_jobs/master.svg)](https://travis-ci.com/mbreit/pg_jobs)
4
+ [![maintainability](https://img.shields.io/codeclimate/maintainability/mbreit/pg_jobs.svg)](https://codeclimate.com/github/mbreit/pg_jobs)
5
+ [![coverage](https://img.shields.io/codeclimate/coverage/mbreit/pg_jobs.svg)](https://codeclimate.com/github/mbreit/pg_jobs)
6
+ [![docs](https://inch-ci.org/github/mbreit/pg_jobs.svg?branch=master)](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).
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ module PgJobs
2
+ class Engine < ::Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module PgJobs
2
+ VERSION = '0.1.0'.freeze
3
+ 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: []