skiplock 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9731224db727e3ebe4b32b94b4edff6bf7a53faca14be5a6f5d7cc20a02836e9
4
+ data.tar.gz: 9c3cb7ccf0d98d7e8ec4bcd21e33269504d166804f0679fe7d286290527a8457
5
+ SHA512:
6
+ metadata.gz: 560b30ea96a8f255811b1c8a0beb7e161ba1edd2dde238e52cd31625e1a4e4d729ff24f3d4d37e4a29fbb2c3e2543e86b9cb1720553cacab346ac30db2970aea
7
+ data.tar.gz: '093b4ee02119973dbd1c8c905d2ccbcc1d6d9efad440ba23ba8fc05a8a2a3580ec7a7d3c7c47d5f6077b789a440e4c46e455f81a5c10fd032b01ffefae906a0e'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Tin Vo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # Skiplock
2
+
3
+ Skiplock is a background job queuing system that improves the performance and reliability of the job executions while providing the same ACID guarantees as the rest of your data. It is designed for Active Jobs with Ruby on Rails using PostgreSQL database adapter, but it can be modified to work with other frameworks easily.
4
+
5
+ It only uses the `LISTEN/NOTIFY/SKIP LOCKED` features provided natively on PostgreSQL 9.5+ to efficiently and reliably dispatch jobs to worker processes and threads ensuring that each job can be completed successfully **only once**. No other polling or timer is needed.
6
+
7
+ The library is quite small compared to other PostgreSQL job queues (eg. *delay_job*, *queue_classic*, *que*, *good_job*) with less than 400 lines of codes; and it still provides similar set of features and more...
8
+
9
+ #### Compatibility:
10
+
11
+ - MRI Ruby 2.5+
12
+ - PostgreSQL 9.5+
13
+ - Rails 5.2+
14
+
15
+ ## Installation
16
+
17
+ 1. Add `skiplock` to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'skiplock'
21
+ ```
22
+
23
+ 2. Install the gem:
24
+
25
+ ```bash
26
+ $ bundle install
27
+ ```
28
+
29
+ 3. Run the Skiplock install generator. This will generate a configuration file and database migration to store the job records:
30
+
31
+ ```bash
32
+ $ rails g skiplock:install
33
+ ```
34
+
35
+ 4. Run the migration:
36
+
37
+ ```bash
38
+ $ rails db:migrate
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ 1. Configure the ActiveJob adapter:
44
+
45
+ ```ruby
46
+ # config/application.rb
47
+ config.active_job.queue_adapter = :skiplock
48
+ ```
49
+ 2. Skiplock configuration
50
+ ```yaml
51
+ # config/skiplock.yml
52
+ ---
53
+ logging: timestamp
54
+ min_threads: 1
55
+ max_threads: 5
56
+ max_retries: 20
57
+ purge_completion: true
58
+ workers: 0
59
+ ```
60
+ Available configuration options are:
61
+ - **logging** (*enumeration*): sets the logging capability to **true** or **false**; setting to **timestamp** will enable logging with timestamps. The log files are: log/skiplock.log and log/skiplock.error.log
62
+ - **min_threads** (*integer*): sets minimum number of threads staying idle
63
+ - **max_threads** (*integer*): sets the maximum number of threads allowed to run jobs
64
+ - **max_retries** (*integer*): sets the maximum attempt a job will be retrying before it is marked expired (see Retry System for more details)
65
+ - **purge_completion** (*boolean*): when set to **true** will delete jobs after they were completed successfully; if set to **false** then the completed jobs should be purged periodically to maximize performance (eg. clean up old jobs after 3 months)
66
+ - **workers** (*integer*) sets the maximum number of processes when running in standalone mode using the `skiplock` executable; setting this to 0 will enable **async mode**
67
+
68
+ #### Async mode
69
+ When **workers** is set to **0** then the jobs will be performed in the web server process using separate threads. If using multi-worker cluster web server like Puma, then it should be configured as below:
70
+ ```ruby
71
+ # config/puma.rb
72
+ before_fork do
73
+ # ...
74
+ Skiplock::Manager.shutdown
75
+ end
76
+
77
+ on_worker_boot do
78
+ # ...
79
+ Skiplock::Manager.start
80
+ end
81
+
82
+ on_worker_shutdown do
83
+ # ...
84
+ Skiplock::Manager.shutdown
85
+ end
86
+ ```
87
+
88
+ ## Usage
89
+
90
+ - Inside the Rails application, queue your job:
91
+ ```ruby
92
+ MyJob.perform_later
93
+ ```
94
+ - Skiplock supports all ActiveJob features:
95
+ ```ruby
96
+ MyJob.set(wait: 5.minutes, priority: 10).perform_later(1,2,3)
97
+ ```
98
+ - Outside of Rails application, queue the jobs by inserting the job records directly to the database table eg:
99
+ ```sql
100
+ INSERT INTO skiplock.jobs(job_class) VALUES ('MyJob');
101
+ ```
102
+ - Or with scheduling, priority and arguments:
103
+ ```sql
104
+ INSERT INTO skiplock.jobs(job_class,priority,scheduled_at,data) VALUES ('MyJob',10,NOW()+INTERVAL '5 min','{"arguments":[1,2,3]}');
105
+ ```
106
+ ## Cron system
107
+ Skiplock provides the capability to setup cron jobs for running tasks periodically. It fully supports the cron syntax to specify the frequency of the jobs. To setup a cron job, simply assign a valid cron schedule to the constant `CRON` for the Job Class.
108
+ - setup `MyJob` to run as cron job every hour at 30 minutes past
109
+
110
+ ```ruby
111
+ class MyJob < ActiveJob::Base
112
+ CRON = "30 * * * *"
113
+ # ...
114
+ end
115
+ ```
116
+ - setup `CleanupJob` to run at midnight every Wednesdays
117
+ ```ruby
118
+ class CleanupJob < ApplicationJob
119
+ CRON = "0 0 * * 3"
120
+ # ...
121
+ end
122
+ ```
123
+ - to remove the cron schedule from the job, simply comment out the constant definition or delete the line then re-deploy the application. At startup, the cron jobs that were undefined will be removed automatically
124
+
125
+ ## Retry system
126
+ Skiplock fully supports ActiveJob built-in retry system. It also has its own retry system for fallback. To use ActiveJob retry system, define the rescue blocks per ActiveJob's documentation.
127
+ - configures `MyJob` to retry at maximum 20 attempts on StandardError with fixed delay of 5 seconds
128
+ ```ruby
129
+ class MyJob < ActiveJob::Base
130
+ retry_on StandardError, wait: 5, attempts: 20
131
+ # ...
132
+ end
133
+ ```
134
+
135
+ - configures `MyJob` to retry at maximum 10 attempts on StandardError with exponential delay
136
+ ```ruby
137
+ class MyJob < ActiveJob::Base
138
+ retry_on StandardError, wait: :exponentially_longer, attempts: 10
139
+ # ...
140
+ end
141
+ ```
142
+ Once the retry attempt limit configured in ActiveJob has been reached, the control will be passed back to `skiplock` to be marked as an expired job.
143
+
144
+ If the rescue blocks are not defined, then the built-in retry system of `skiplock` will kick in automatically. The retrying schedule is using an exponential formula (5 + 2**attempt). The `skiplock` configuration `max_retries` determines the the limit of attempts before the failing job is marked as expired. The maximum retry limit can be set as high as 20; this allows up to 12 days of retrying before the job is marked as expired.
145
+
146
+ ## Notification system
147
+ ...
148
+
149
+ ## Contributing
150
+
151
+ Bug reports and pull requests are welcome on GitHub at https://github.com/vtt/skiplock.
152
+
153
+ ## License
154
+
155
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/bin/skiplock ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.expand_path("config/environment.rb")
3
+ Skiplock::Manager.start(standalone: true)
@@ -0,0 +1,17 @@
1
+ module ActiveJob
2
+ module QueueAdapters
3
+ class SkiplockAdapter
4
+ def initialize
5
+ Skiplock::Manager.start
6
+ end
7
+
8
+ def enqueue(job)
9
+ enqueue_at(job, nil)
10
+ end
11
+
12
+ def enqueue_at(job, timestamp)
13
+ Skiplock::Job.enqueue_at(job, timestamp)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/active_record'
3
+
4
+ module Skiplock
5
+ class InstallGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+ source_paths << File.join(File.dirname(__FILE__), 'templates')
8
+ desc 'Add configuration & migration for Skiplock'
9
+
10
+ def self.next_migration_number(path)
11
+ ActiveRecord::Generators::Base.next_migration_number(path)
12
+ end
13
+
14
+ def create_config_file
15
+ create_file 'config/skiplock.yml', Skiplock::Settings.to_yaml
16
+ end
17
+
18
+ def create_migration_file
19
+ migration_template 'migration.rb.erb', 'db/migrate/create_skiplock_schema.rb'
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" %>
2
+ def up
3
+ execute 'CREATE SCHEMA skiplock'
4
+ create_table 'skiplock.jobs', id: :uuid do |t|
5
+ t.string :job_class, null: false
6
+ t.string :cron
7
+ t.string :queue_name
8
+ t.string :locale
9
+ t.string :timezone
10
+ t.integer :priority
11
+ t.integer :executions
12
+ t.jsonb :exception_executions
13
+ t.jsonb :data
14
+ t.boolean :running, null: false, default: false
15
+ t.timestamp :expired_at
16
+ t.timestamp :finished_at
17
+ t.timestamp :scheduled_at
18
+ t.timestamps null: false, default: -> { 'now()' }
19
+ end
20
+ execute %(CREATE OR REPLACE FUNCTION skiplock.notify() RETURNS TRIGGER AS $$
21
+ BEGIN
22
+ IF (NEW.finished_at IS NULL AND NEW.expired_at IS NULL) THEN
23
+ PERFORM pg_notify('skiplock', CONCAT(TG_OP,',',NEW.id::TEXT,',',NEW.queue_name,',',NEW.priority,',',CAST(EXTRACT(EPOCH FROM NEW.scheduled_at) AS FLOAT)::text));
24
+ END IF;
25
+ RETURN NULL;
26
+ END;
27
+ $$ LANGUAGE plpgsql
28
+ )
29
+ execute "CREATE TRIGGER notify_job AFTER INSERT OR UPDATE ON skiplock.jobs FOR EACH ROW EXECUTE PROCEDURE skiplock.notify()"
30
+ execute "CREATE INDEX jobs_index ON skiplock.jobs(scheduled_at ASC NULLS FIRST, priority ASC NULLS LAST, created_at ASC) WHERE running = 'f' AND expired_at IS NULL AND finished_at IS NULL"
31
+ execute "CREATE INDEX jobs_retry_index ON skiplock.jobs(scheduled_at) WHERE running = 'f' AND executions IS NOT NULL AND expired_at IS NULL AND finished_at IS NULL"
32
+ execute "CREATE INDEX jobs_cron_index ON skiplock.jobs(scheduled_at ASC NULLS FIRST, priority ASC NULLS LAST, created_at ASC) WHERE cron IS NOT NULL AND finished_at IS NULL"
33
+ execute "CREATE UNIQUE INDEX jobs_unique_cron_index ON skiplock.jobs (job_class) WHERE cron IS NOT NULL"
34
+ end
35
+
36
+ def down
37
+ execute 'DROP SCHEMA skiplock CASCADE'
38
+ end
39
+ end
data/lib/skiplock.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'active_job'
2
+ require 'active_job/queue_adapters/skiplock_adapter'
3
+ require 'active_record'
4
+ require 'skiplock/cron'
5
+ require 'skiplock/dispatcher'
6
+ require 'skiplock/manager'
7
+ require 'skiplock/notification'
8
+ require 'skiplock/job'
9
+ require 'skiplock/version'
10
+
11
+ module Skiplock
12
+ Settings = {
13
+ 'logging' => :timestamp,
14
+ 'min_threads' => 1,
15
+ 'max_threads' => 5,
16
+ 'max_retries' => 20,
17
+ 'purge_completion' => true,
18
+ 'workers' => 0
19
+ }
20
+ end
@@ -0,0 +1,31 @@
1
+ require 'cron_parser'
2
+ module Skiplock
3
+ class Cron
4
+ def self.setup
5
+ cronjobs = []
6
+ ActiveJob::Base.descendants.each do |j|
7
+ next unless j.const_defined?('CRON')
8
+ cron = j.const_get('CRON')
9
+ job = Job.find_by('job_class = ? AND cron IS NOT NULL', j.name) || Job.new(job_class: j.name, cron: cron)
10
+ time = self.next_schedule_at(cron)
11
+ if time
12
+ job.cron = cron
13
+ job.running = false
14
+ job.scheduled_at = Time.at(time)
15
+ job.save!
16
+ cronjobs << j.name
17
+ end
18
+ end
19
+ query = Job.where('cron IS NOT NULL')
20
+ query = query.where('job_class NOT IN (?)', cronjobs) if cronjobs.count > 0
21
+ query.delete_all
22
+ end
23
+
24
+ def self.next_schedule_at(cron)
25
+ time = CronParser.new(cron).next
26
+ time = time + (time <= Time.now ? 60 : Time.now.sec)
27
+ time.to_f
28
+ rescue
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,92 @@
1
+ module Skiplock
2
+ class Dispatcher
3
+ def initialize(master: true)
4
+ @executor = Concurrent::ThreadPoolExecutor.new(min_threads: Settings['min_threads'], max_threads: Settings['max_threads'], max_queue: Settings['max_threads'], idletime: 60, auto_terminate: true, fallback_policy: :discard)
5
+ @master = master
6
+ @next_schedule_at = Time.now.to_f
7
+ @running = true
8
+ end
9
+
10
+ def run
11
+ Thread.new do
12
+ Rails.application.reloader.wrap do
13
+ sleep(0.1) while @running && !Rails.application.initialized?
14
+ ActiveRecord::Base.connection_pool.with_connection do |connection|
15
+ connection.exec_query('LISTEN skiplock')
16
+ if @master
17
+ # reset retries schedules on startup
18
+ Job.where('scheduled_at > NOW() AND executions IS NOT NULL AND expired_at IS NULL AND finished_at IS NULL').update_all(scheduled_at: nil, updated_at: Time.now)
19
+ Cron.setup
20
+ end
21
+ error = false
22
+ while @running
23
+ begin
24
+ if error
25
+ unless connection.active?
26
+ connection.reconnect!
27
+ sleep(0.5)
28
+ connection.exec_query('LISTEN skiplock')
29
+ @next_schedule_at = Time.now
30
+ end
31
+ error = false
32
+ end
33
+ if Time.now.to_f >= @next_schedule_at && @executor.remaining_capacity > 0
34
+ @executor.post { do_work }
35
+ end
36
+ notifications = []
37
+ connection.raw_connection.wait_for_notify(0.1) do |channel, pid, payload|
38
+ notifications << payload if payload
39
+ loop do
40
+ payload = connection.raw_connection.notifies
41
+ break unless @running && payload
42
+ notifications << payload[:extra]
43
+ end
44
+ notifications.each do |n|
45
+ op, id, queue, priority, time = n.split(',')
46
+ if time.to_f <= Time.now.to_f
47
+ @next_schedule_at = Time.now.to_f
48
+ elsif time.to_f < @next_schedule_at
49
+ @next_schedule_at = time.to_f
50
+ end
51
+ end
52
+ end
53
+ rescue Exception => ex
54
+ # TODO: Report exception
55
+ error = true
56
+ timestamp = Time.now
57
+ while @running
58
+ sleep(0.5)
59
+ break if Time.now - timestamp > 10
60
+ end
61
+ end
62
+ sleep(0.1)
63
+ end
64
+ connection.exec_query('UNLISTEN *')
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ def shutdown(wait: true)
71
+ @running = false
72
+ @executor.shutdown
73
+ @executor.wait_for_termination if wait
74
+ end
75
+
76
+ private
77
+
78
+ def do_work
79
+ connection = ActiveRecord::Base.connection_pool.checkout
80
+ while @running
81
+ result = Job.dispatch(connection: connection)
82
+ next if result.is_a?(Hash)
83
+ @next_schedule_at = result if result.is_a?(Float)
84
+ break
85
+ end
86
+ rescue Exception => e
87
+ # TODO: Report exception
88
+ ensure
89
+ ActiveRecord::Base.connection_pool.checkin(connection)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,72 @@
1
+ module Skiplock
2
+ class Job < ActiveRecord::Base
3
+ self.table_name = 'skiplock.jobs'
4
+
5
+ # Accept: An active ActiveRecord database connection (eg. ActiveRecord::Base.connection)
6
+ # The connection should be checked out using ActiveRecord::Base.connection_pool.checkout, and be checked
7
+ # in using ActiveRecord::Base.conection_pool.checkin once all of the job dispatches have been completed.
8
+ # *** IMPORTANT: This connection cannot be shared with the job's execution
9
+ #
10
+ # Return: Attributes hash of the Job if it was executed; otherwise returns the next Job's schedule time in FLOAT
11
+ def self.dispatch(connection: ActiveRecord::Base.connection)
12
+ connection.exec_query('BEGIN')
13
+ job = connection.exec_query("SELECT * FROM #{self.table_name} WHERE running = 'f' AND expired_at IS NULL AND finished_at IS NULL ORDER BY scheduled_at ASC NULLS FIRST, priority ASC NULLS LAST, created_at ASC FOR UPDATE SKIP LOCKED LIMIT 1").first
14
+ if job && job['scheduled_at'].to_f <= Time.now.to_f # found job ready to perform
15
+ # update the job to mark it in progress in case database server goes down during job execution
16
+ connection.exec_query("UPDATE #{self.table_name} SET running = 't' WHERE id = '#{job['id']}'")
17
+ connection.exec_query('END') # close the transaction commit the state of job in progress
18
+ executions = (job['executions'] || 0) + 1
19
+ exceptions = job['exception_executions'] ? JSON.parse(job['exception_executions']) : {}
20
+ data = job['data'] ? JSON.parse(job['data']) : {}
21
+ job_data = job.slice('job_class', 'queue_name', 'locale', 'timezone', 'priority', 'executions').merge('job_id' => job['id'], 'exception_executions' => exceptions, 'enqueued_at' => job['updated_at']).merge(data)
22
+ Thread.current[:skiplock_dispatch_data] = job_data
23
+ begin
24
+ ActiveJob::Base.execute(job_data)
25
+ rescue Exception => ex
26
+ end
27
+ if ex
28
+ # TODO: report exception
29
+ exceptions["[#{ex.class.name}]"] = (exceptions["[#{ex.class.name}]"] || 0) + 1 unless exceptions.key?('activejob_retry')
30
+ if executions >= Settings['max_retries'] || exceptions.key?('activejob_retry')
31
+ connection.exec_query("UPDATE #{self.table_name} SET running = 'f', executions = #{executions}, exception_executions = '#{connection.quote_string(exceptions.to_json.to_s)}', expired_at = NOW(), updated_at = NOW() WHERE id = '#{job['id']}' RETURNING *").first
32
+ else
33
+ timestamp = Time.now + (5 * 2**executions)
34
+ connection.exec_query("UPDATE #{self.table_name} SET running = 'f', executions = #{executions}, exception_executions = '#{connection.quote_string(exceptions.to_json.to_s)}', scheduled_at = TO_TIMESTAMP(#{timestamp.to_f}), updated_at = NOW() WHERE id = '#{job['id']}' RETURNING *").first
35
+ end
36
+ elsif exceptions.key?('activejob_retry')
37
+ connection.exec_query("UPDATE #{self.table_name} SET running = 'f', executions = #{job_data['executions']}, exception_executions = '#{connection.quote_string(job_data['exception_executions'].to_json.to_s)}', scheduled_at = TO_TIMESTAMP(#{job_data['scheduled_at'].to_f}), updated_at = NOW() WHERE id = '#{job['id']}' RETURNING *").first
38
+ elsif job['cron']
39
+ data['last_cron_run'] = Time.now.utc.to_s
40
+ next_cron_at = Cron.next_schedule_at(job['cron'])
41
+ if next_cron_at
42
+ connection.exec_query("UPDATE #{self.table_name} SET running = 'f', scheduled_at = TO_TIMESTAMP(#{next_cron_at}), executions = 1, exception_executions = NULL, data = '#{connection.quote_string(data.to_json.to_s)}', updated_at = NOW() WHERE id = '#{job['id']}' RETURNING *").first
43
+ else
44
+ connection.exec_query("DELETE FROM #{self.table_name} WHERE id = '#{job['id']}' RETURNING *").first
45
+ end
46
+ elsif Settings['purge_completion']
47
+ connection.exec_query("DELETE FROM #{self.table_name} WHERE id = '#{job['id']}' RETURNING *").first
48
+ else
49
+ connection.exec_query("UPDATE #{self.table_name} SET running = 'f', executions = #{executions}, exception_executions = NULL, finished_at = NOW(), updated_at = NOW() WHERE id = '#{job['id']}' RETURNING *").first
50
+ end
51
+ else
52
+ connection.exec_query('END')
53
+ job ? job['scheduled_at'].to_f : Float::INFINITY
54
+ end
55
+ ensure
56
+ Thread.current[:skiplock_dispatch_data] = nil
57
+ end
58
+
59
+ def self.enqueue_at(job, timestamp)
60
+ if Thread.current[:skiplock_dispatch_data]
61
+ job.exception_executions['activejob_retry'] = true
62
+ Thread.current[:skiplock_dispatch_data]['executions'] = job.executions
63
+ Thread.current[:skiplock_dispatch_data]['exception_executions'] = job.exception_executions
64
+ Thread.current[:skiplock_dispatch_data]['scheduled_at'] = Time.at(timestamp)
65
+ self.new(Thread.current[:skiplock_dispatch_data].slice(*self.column_names).merge(id: job.job_id))
66
+ else
67
+ timestamp = Time.at(timestamp) if timestamp
68
+ Job.create!(id: job.job_id, job_class: job.class.name, queue_name: job.queue_name, locale: job.locale, timezone: job.timezone, priority: job.priority, executions: job.executions, data: { 'arguments' => job.arguments }, scheduled_at: timestamp)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,119 @@
1
+ module Skiplock
2
+ class Manager
3
+ def self.start(standalone: false)
4
+ load_settings
5
+ return unless standalone || (caller.any?{|l| l =~ %r{/rack/}} && (Settings['workers'] == 0 || Rails.env.development?))
6
+ if standalone
7
+ self.standalone
8
+ else
9
+ @dispatcher = Dispatcher.new
10
+ @thread = @dispatcher.run
11
+ at_exit { self.shutdown }
12
+ end
13
+ end
14
+
15
+ def self.shutdown(wait: true)
16
+ if @dispatcher && @thread
17
+ @dispatcher.shutdown(wait: wait)
18
+ @thread.join
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def self.load_settings
25
+ return if Settings.frozen?
26
+ config = YAML.load_file('config/skiplock.yml') rescue {}
27
+ Settings.merge!(config)
28
+ Settings['max_retries'] = 20 if Settings['max_retries'] > 20
29
+ Settings['max_retries'] = 0 if Settings['max_retries'] < 0
30
+ Settings['max_threads'] = 1 if Settings['max_threads'] < 1
31
+ Settings['max_threads'] = 20 if Settings['max_threads'] > 20
32
+ Settings['min_threads'] = 0 if Settings['min_threads'] < 0
33
+ Settings['workers'] = 0 if Settings['workers'] < 0
34
+ Settings.freeze
35
+ end
36
+
37
+ def self.standalone
38
+ title = "Skiplock version: #{Skiplock::VERSION} (Ruby #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL})"
39
+ puts "-"*(title.length)
40
+ puts title
41
+ puts "-"*(title.length)
42
+ puts "Additional workers: #{Settings['workers']}"
43
+ puts " Purge completion: #{Settings['purge_completion']}"
44
+ puts " Max retries: #{Settings['max_retries']}"
45
+ puts " Min threads: #{Settings['min_threads']}"
46
+ puts " Max threads: #{Settings['max_threads']}"
47
+ puts " Environment: #{Rails.env}"
48
+ puts " Logging: #{Settings['logging']}"
49
+ puts " PID: #{Process.pid}"
50
+ puts "-"*(title.length)
51
+ if Settings['logging']
52
+ log_timestamp = (Settings['logging'].to_s == 'timestamp')
53
+ logfile = File.open('log/skiplock.log', 'a')
54
+ logfile.sync = true
55
+ $stdout = Demux.new(logfile, STDOUT, timestamp: log_timestamp)
56
+ errfile = File.open('log/skiplock.error.log', 'a')
57
+ errfile.sync = true
58
+ $stderr = Demux.new(errfile, STDERR, timestamp: log_timestamp)
59
+ logger = ActiveSupport::Logger.new($stdout)
60
+ logger.level = Rails.logger.level
61
+ Rails.logger.reopen('/dev/null')
62
+ Rails.logger.extend(ActiveSupport::Logger.broadcast(logger))
63
+ end
64
+ parent_id = Process.pid
65
+ shutdown = false
66
+ Signal.trap("INT") { shutdown = true }
67
+ Signal.trap("TERM") { shutdown = true }
68
+ Settings['workers'].times do |w|
69
+ fork do
70
+ Process.setproctitle("skiplock-worker[#{w+1}]")
71
+ dispatcher = Dispatcher.new(master: false)
72
+ thread = dispatcher.run
73
+ loop do
74
+ sleep 0.5
75
+ break if shutdown || Process.ppid != parent_id
76
+ end
77
+ dispatcher.shutdown(wait: true)
78
+ thread.join
79
+ exit
80
+ end
81
+ end
82
+ sleep 0.1
83
+ Process.setproctitle("skiplock-master")
84
+ dispatcher = Dispatcher.new
85
+ thread = dispatcher.run
86
+ loop do
87
+ sleep 0.5
88
+ break if shutdown
89
+ end
90
+ Process.waitall
91
+ dispatcher.shutdown(wait: true)
92
+ thread.join
93
+ end
94
+
95
+ class Demux
96
+ def initialize(*targets, timestamp: true)
97
+ @targets = targets
98
+ @timestamp = timestamp
99
+ end
100
+
101
+ def close
102
+ @targets.each(&:close)
103
+ end
104
+
105
+ def flush
106
+ @targets.each(&:flush)
107
+ end
108
+
109
+ def tty?
110
+ true
111
+ end
112
+
113
+ def write(*args)
114
+ args.prepend("[#{Time.now.utc}]: ") if @timestamp
115
+ @targets.each {|t| t.write(*args)}
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,5 @@
1
+ module Skiplock
2
+ class Notification
3
+ end
4
+ end
5
+
@@ -0,0 +1,4 @@
1
+ module Skiplock
2
+ VERSION = Version = '1.0.3'
3
+ end
4
+
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: skiplock
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Tin Vo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-03-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activejob
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 5.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 5.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 5.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 5.2.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: concurrent-ruby
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 1.0.2
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 1.0.2
55
+ - !ruby/object:Gem::Dependency
56
+ name: parse-cron
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.1'
69
+ description: High performance ActiveJob Queue Adapter for PostgreSQL that provides
70
+ maximum reliability and ACID compliance
71
+ email:
72
+ - vtt999@gmail.com
73
+ executables:
74
+ - skiplock
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - LICENSE.txt
79
+ - README.md
80
+ - bin/skiplock
81
+ - lib/active_job/queue_adapters/skiplock_adapter.rb
82
+ - lib/generators/skiplock/install_generator.rb
83
+ - lib/generators/skiplock/templates/migration.rb.erb
84
+ - lib/skiplock.rb
85
+ - lib/skiplock/cron.rb
86
+ - lib/skiplock/dispatcher.rb
87
+ - lib/skiplock/job.rb
88
+ - lib/skiplock/manager.rb
89
+ - lib/skiplock/notification.rb
90
+ - lib/skiplock/version.rb
91
+ homepage: https://github.com/vtt/skiplock
92
+ licenses:
93
+ - MIT
94
+ metadata: {}
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 2.5.0
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubygems_version: 3.0.3
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: ActiveJob Queue Adapter for PostgreSQL
114
+ test_files: []