pg_jobs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []