skiplock 1.0.8 → 1.0.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ee6a8ff68af1a1029a26db98867457a798c4fb6aacf0a2a2816efd3ef2b977b
4
- data.tar.gz: 2c75fb21c79346f4525b8634b28fb326505acb7ab61795fe7d291c3faa75244f
3
+ metadata.gz: 7a975a3bb22318c6203edd59428a93d6a4abfa5ea1382f35abd774760467dc23
4
+ data.tar.gz: 79b787d23af0db0188aebf1b0361649d906d5f0cca4488d7cfcb3f61927d4781
5
5
  SHA512:
6
- metadata.gz: c34fa2c27f9fc8bcfbe6c54a30de8645418dfe7c13e8db9ea8c30352f75abd9ab9b04d1a294aea7f2a9f33772d9cc5ed96839bd63d1eacbda17749758b4755b2
7
- data.tar.gz: 03cdbef1a70a64fb188258dc44049f9323f53cb9c2e64f8a79a9c8a4a17ca8aea2e7ca718ceb3bc78cc08fd61d6dbd5c3f104f653a9645132ef31f73d22883da
6
+ metadata.gz: 407469700b1fab6c4ea702b83df0571fd46322ad32c8c8fe8ffc0269e5e93cd9f774b8bd4658021320732ca4de6d7609a64430cbc26bddc14c9f14cf184f92e3
7
+ data.tar.gz: 69a5fe7f5c88f21585a4b581d34f44183cb12f25bc0df6e7982497fb63c088316a251cc55c4e712b3cad799ec9aa758c8045d911fbeda393267ed39bc694e9a5
data/README.md CHANGED
@@ -68,48 +68,48 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
68
68
  - **max_retries** (*integer*): sets the maximum attempt a job will be retrying before it is marked expired (see Retry System for more details)
69
69
  - **notification** (*enumeration*): sets the library to be used for notifying errors and exceptions (`auto, airbrake, bugsnag, exception_notification, false`)
70
70
  - **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)
71
- - **queues** (*hash*): defines the set of queues with priorites; lower priority takes precedence
71
+ - **queues** (*hash*): defines the set of queues with priorities; lower priority takes precedence
72
72
  - **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**
73
73
 
74
74
  #### Async mode
75
75
  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 mode web server like Puma, then it should be configured as below:
76
76
  ```ruby
77
77
  # config/puma.rb
78
- before_fork do
79
- # ...
80
- Skiplock::Manager.shutdown
78
+ # ...
79
+ on_worker_fork do |worker_index|
80
+ Skiplock::Manager.shutdown if worker_index == 1
81
81
  end
82
82
 
83
83
  after_worker_fork do |worker_index|
84
84
  # restarts skiplock after all Puma workers have been started
85
- # Skiplock runs in Puma master's process only
86
- Skiplock::Manager.start if (worker_index + 1) == @options[:workers]
85
+ Skiplock::Manager.start(restart: true) if defined?(Skiplock) && worker_index + 1 == @options[:workers]
87
86
  end
88
87
  ```
89
-
90
88
  ## Usage
91
-
92
- - Inside the Rails application, queue your job:
89
+ Inside the Rails application:
90
+ - queue your job
93
91
  ```ruby
94
92
  MyJob.perform_later
95
93
  ```
96
- - Skiplock supports all ActiveJob features:
94
+ - Skiplock supports all ActiveJob features
97
95
  ```ruby
98
96
  MyJob.set(queue: 'my_queue', wait: 5.minutes, priority: 10).perform_later(1,2,3)
99
97
  ```
100
- - Outside of Rails application, queue the jobs by inserting the job records directly to the database table eg:
98
+ Outside the Rails application:
99
+ - queue the jobs by inserting the job records directly to the database table
101
100
  ```sql
102
101
  INSERT INTO skiplock.jobs(job_class) VALUES ('MyJob');
103
102
  ```
104
- - Or with scheduling, priority, queue and arguments:
103
+ - with scheduling, priority, queue and arguments
105
104
  ```sql
106
- INSERT INTO skiplock.jobs(job_class,queue_name,priority,scheduled_at,data) VALUES ('MyJob','my_queue',10,NOW()+INTERVAL '5 min','{"arguments":[1,2,3]}');
105
+ INSERT INTO skiplock.jobs(job_class, queue_name, priority, scheduled_at, data)
106
+ VALUES ('MyJob', 'my_queue', 10, NOW() + INTERVAL '5 min', '{"arguments":[1,2,3]}');
107
107
  ```
108
- ## Queues priority vs Job priority
109
- *Why do queues have priorities when jobs already have priorities?*
108
+ ## Queue priority vs Job priority
109
+ *Why do queues use priorities when jobs already have priorities?*
110
110
  - Jobs are only prioritized with other jobs from the same queue
111
111
  - Queues, on the other hand, are prioritized with other queues
112
- - Rails has built-in queues that dispatches jobs without priorities (eg. Mail Delivery will queue as **mailers** with no priority)
112
+ - Rails has built-in queues that dispatch jobs without priorities (eg. Mail Delivery will queue as **mailers** with no priority)
113
113
 
114
114
  ## Cron system
115
115
  `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.
@@ -131,7 +131,7 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
131
131
  - 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
132
132
 
133
133
  ## Retry system
134
- `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.
134
+ `Skiplock` fully supports ActiveJob built-in retry system. It also has its own retry system for fallback. To use ActiveJob retry system, define the `retry_on` block per ActiveJob's documentation.
135
135
  - configures `MyJob` to retry at maximum 20 attempts on StandardError with fixed delay of 5 seconds
136
136
  ```ruby
137
137
  class MyJob < ActiveJob::Base
@@ -147,20 +147,21 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
147
147
  # ...
148
148
  end
149
149
  ```
150
- 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.
150
+ If the retry attempt limit configured in ActiveJob has been reached, then the control will be passed back to `skiplock` to be marked as an expired job.
151
151
 
152
- 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.
152
+ If the `retry_on` block is 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.
153
153
 
154
154
  ## Notification system
155
- `Skiplock` can use existing exception notification library to notify errors and exceptions. It supports `airbrake`, `bugsnag`, and `exception_notification`. A customized function can also be called whenever an exception occurs; this can be configured in an initializer as below:
155
+ `Skiplock` can use existing exception notification library to notify errors and exceptions. It supports `airbrake`, `bugsnag`, and `exception_notification` as shown in the **Configuration** section above. Custom function can also be called whenever an exception occurs; it can be configured in an initializer like below:
156
156
  ```ruby
157
157
  # config/initializers/skiplock.rb
158
- Skiplock.on_error = -> (ex, previous = nil) do
158
+ Skiplock.on_error do |ex, previous|
159
159
  if ex.backtrace != previous.try(:backtrace)
160
160
  # sends custom email on new exceptions only
161
161
  # the same repeated exceptions will only be sent once to avoid SPAM
162
162
  end
163
163
  end
164
+ # supports multiple on_error callbacks
164
165
  ```
165
166
 
166
167
  ## Contributing
data/bin/skiplock CHANGED
@@ -1,3 +1,26 @@
1
1
  #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ options = {}
4
+ begin
5
+ op = OptionParser.new do |opts|
6
+ opts.banner = "Usage: #{File.basename($0)} [options]"
7
+ opts.on('-e', '--environment STRING', String, 'Rails environment')
8
+ opts.on('-l', '--logging STRING', String, 'Possible values: true, false, timestamp')
9
+ opts.on('-r', '--max_retries NUM', Integer, 'Number of maxixum retries')
10
+ opts.on('-t', '--max_threads NUM', Integer, 'Number of maximum threads')
11
+ opts.on('-T', '--min_threads NUM', Integer, 'Number of minimum threads')
12
+ opts.on('-w', '--workers NUM', Integer, 'Number of workers')
13
+ opts.on_tail('-h', '--help', 'Show this message') do
14
+ exit
15
+ end
16
+ end
17
+ op.parse!(into: options)
18
+ rescue Exception => e
19
+ puts "\n#{e.message}\n\n" unless e.is_a?(SystemExit)
20
+ puts op
21
+ exit
22
+ end
23
+ env = options.delete(:environment)
24
+ ENV['RAILS_ENV'] = env if env
2
25
  require File.expand_path("config/environment.rb")
3
- Skiplock::Manager.start(standalone: true)
26
+ Skiplock::Manager.start(standalone: true, **options)
@@ -6,7 +6,7 @@ module ActiveJob
6
6
  end
7
7
 
8
8
  def enqueue(job)
9
- enqueue_at(job, nil)
9
+ Skiplock::Job.enqueue(job)
10
10
  end
11
11
 
12
12
  def enqueue_at(job, timestamp)
@@ -1,6 +1,14 @@
1
1
  class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" %>
2
2
  def up
3
3
  execute 'CREATE SCHEMA skiplock'
4
+ create_table 'skiplock.counters', id: :uuid do |t|
5
+ t.integer :completions, null: false, default: 0
6
+ t.integer :dispatches, null: false, default: 0
7
+ t.integer :expiries, null: false, default: 0
8
+ t.integer :failures, null: false, default: 0
9
+ t.integer :retries, null: false, default: 0
10
+ t.date :day, null: false, index: { unique: true }
11
+ end
4
12
  create_table 'skiplock.jobs', id: :uuid do |t|
5
13
  t.uuid :worker_id, index: true
6
14
  t.string :job_class, null: false
@@ -31,10 +39,25 @@ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSIO
31
39
  DECLARE
32
40
  record RECORD;
33
41
  BEGIN
42
+ record = NEW;
34
43
  IF (TG_OP = 'DELETE') THEN
35
44
  record = OLD;
36
- ELSE
37
- record = NEW;
45
+ IF (record.finished_at IS NOT NULL OR record.expired_at IS NOT NULL) THEN
46
+ RETURN NULL;
47
+ END IF;
48
+ INSERT INTO skiplock.counters (day,completions) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET completions = skiplock.counters.completions + 1;
49
+ ELSIF (record.running = TRUE) THEN
50
+ IF (record.executions IS NULL) THEN
51
+ INSERT INTO skiplock.counters (day,dispatches) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET dispatches = skiplock.counters.dispatches + 1;
52
+ ELSE
53
+ INSERT INTO skiplock.counters (day,retries) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET retries = skiplock.counters.retries + 1;
54
+ END IF;
55
+ ELSIF (record.finished_at IS NOT NULL) THEN
56
+ INSERT INTO skiplock.counters (day,completions) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET completions = skiplock.counters.completions + 1;
57
+ ELSIF (record.expired_at IS NOT NULL) THEN
58
+ INSERT INTO skiplock.counters (day,expiries) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET expiries = skiplock.counters.expiries + 1;
59
+ ELSIF (record.executions IS NOT NULL AND record.scheduled_at IS NOT NULL) THEN
60
+ INSERT INTO skiplock.counters (day,failures) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET failures = skiplock.counters.failures + 1;
38
61
  END IF;
39
62
  PERFORM pg_notify('skiplock::jobs', CONCAT(TG_OP,',',record.id::TEXT,',',record.worker_id::TEXT,',',record.queue_name,',',record.running::TEXT,',',CAST(EXTRACT(EPOCH FROM record.expired_at) AS FLOAT)::TEXT,',',CAST(EXTRACT(EPOCH FROM record.finished_at) AS FLOAT)::TEXT,',',CAST(EXTRACT(EPOCH FROM CASE WHEN record.scheduled_at IS NULL THEN record.updated_at ELSE record.scheduled_at END) AS FLOAT)::TEXT));
40
63
  RETURN NULL;
data/lib/skiplock.rb CHANGED
@@ -1,10 +1,11 @@
1
1
  require 'active_job'
2
2
  require 'active_job/queue_adapters/skiplock_adapter'
3
3
  require 'active_record'
4
+ require 'skiplock/counter'
4
5
  require 'skiplock/cron'
5
6
  require 'skiplock/dispatcher'
6
- require 'skiplock/manager'
7
7
  require 'skiplock/job'
8
+ require 'skiplock/manager'
8
9
  require 'skiplock/worker'
9
10
  require 'skiplock/version'
10
11
 
@@ -22,5 +23,13 @@ module Skiplock
22
23
  },
23
24
  'workers' => 0
24
25
  }
25
- mattr_accessor :on_error
26
+ mattr_reader :on_errors, default: []
27
+
28
+ def self.on_error(&block)
29
+ @@on_errors << block
30
+ end
31
+
32
+ def self.table_name_prefix
33
+ 'skiplock.'
34
+ end
26
35
  end
@@ -0,0 +1,4 @@
1
+ module Skiplock
2
+ class Counter < ActiveRecord::Base
3
+ end
4
+ end
@@ -16,121 +16,120 @@ module Skiplock
16
16
 
17
17
  def run
18
18
  Thread.new do
19
- sleep(1) while @running && !Rails.application.initialized?
20
- Process.setproctitle("skiplock-#{@master ? 'master[0]' : 'worker[' + @worker_num.to_s + ']'}") if Settings['workers'] > 0
21
- ActiveRecord::Base.connection_pool.with_connection do |connection|
22
- connection.exec_query('LISTEN "skiplock::jobs"')
23
- hostname = `hostname -f`.strip
24
- @worker = Worker.create!(pid: Process.pid, ppid: (@master ? nil : Process.ppid), capacity: Settings['max_threads'], hostname: hostname)
25
- if @master
26
- connection.exec_query('LISTEN "skiplock::workers"')
27
- if File.exists?('tmp/cache/skiplock')
28
- # get performed jobs that could not sync with database
29
- job_ids = File.read('tmp/cache/skiplock').split("\n")
30
- if Settings['purge_completion']
31
- Job.where(id: job_ids).delete_all
32
- else
33
- Job.where(id: job_ids).update_all(running: false, finished_at: File.mtime('tmp/cache/skiplock'), updated_at: Time.now)
19
+ Rails.application.reloader.wrap do
20
+ sleep(1) while @running && !Rails.application.initialized?
21
+ Rails.application.eager_load!
22
+ Process.setproctitle("skiplock-#{@master ? 'master[0]' : 'worker[' + @worker_num.to_s + ']'}") if Settings['workers'] > 0 && !Rails.env.development?
23
+ ActiveRecord::Base.connection_pool.with_connection do |connection|
24
+ connection.exec_query('LISTEN "skiplock::jobs"')
25
+ hostname = `hostname -f`.strip
26
+ @worker = Worker.create!(pid: Process.pid, ppid: (@master ? nil : Process.ppid), capacity: Settings['max_threads'], hostname: hostname)
27
+ if @master
28
+ connection.exec_query('LISTEN "skiplock::workers"')
29
+ Dir.mkdir('tmp/skiplock') unless Dir.exist?('tmp/skiplock')
30
+ check_sync_errors
31
+ # get dead worker ids
32
+ dead_worker_ids = Worker.where(hostname: hostname).where.not(pid: @worker_pids).ids
33
+ if dead_worker_ids.count > 0
34
+ # reset orphaned jobs of the dead worker ids for retry
35
+ Job.where(running: true).where(worker_id: dead_worker_ids).update_all(running: false, worker_id: nil)
36
+ # remove dead workers
37
+ Worker.where(id: dead_worker_ids).delete_all
34
38
  end
35
- File.delete('tmp/cache/skiplock')
39
+ # reset retries schedules on startup
40
+ 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)
41
+ Cron.setup
36
42
  end
37
- # get dead worker ids
38
- dead_worker_ids = Worker.where(hostname: hostname).where.not(pid: @worker_pids).ids
39
- if dead_worker_ids.count > 0
40
- # reset orphaned jobs of the dead worker ids for retry
41
- Job.where(running: true).where(worker_id: dead_worker_ids).update_all(running: false, worker_id: nil)
42
- # remove dead workers
43
- Worker.where(id: dead_worker_ids).delete_all
44
- end
45
- # reset retries schedules on startup
46
- 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)
47
- Cron.setup
48
- end
49
- error = false
50
- while @running
51
- begin
52
- if error
53
- unless connection.active?
54
- connection.reconnect!
55
- sleep(0.5)
56
- connection.exec_query('LISTEN "skiplock::jobs"')
57
- connection.exec_query('LISTEN "skiplock::workers"') if @master
58
- @next_schedule_at = Time.now.to_f
59
- end
60
- error = false
61
- end
62
- if Job::Errors.keys.count > 0
63
- completed_ids = Job::Errors.keys.map { |k| k if Job::Errors[k] }.compact
64
- if Settings['purge_completion'] && completed_ids.count > 0
65
- Job.where(id: completed_ids, running: true).delete_all
66
- elsif completed_ids.count > 0
67
- Job.where(id: completed_ids, running: true).update_all(running: false, finished_at: Time.now, updated_at: Time.now)
68
- end
69
- orphaned_ids = Job::Errors.keys.map { |k| k unless Job::Errors[k] }.compact
70
- Job.where(id: orphaned_ids, running: true).update_all(running: false, worker_id: nil, scheduled_at: (Time.now + 10), updated_at: Time.now) if orphaned_ids.count > 0
71
- Job::Errors.clear
72
- end
73
- notifications = { 'skiplock::jobs' => [], 'skiplock::workers' => [] }
74
- connection.raw_connection.wait_for_notify(0.1) do |channel, pid, payload|
75
- notifications[channel] << payload if payload
76
- loop do
77
- payload = connection.raw_connection.notifies
78
- break unless @running && payload
79
- notifications[payload[:relname]] << payload[:extra]
80
- end
81
- notifications['skiplock::jobs'].each do |n|
82
- op, id, worker_id, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
83
- if @master
84
- # TODO: report job status to action cable
85
- end
86
- next if op == 'DELETE' || running == 'true' || expired_at.to_f > 0 || finished_at.to_f > 0 || scheduled_at.to_f < @last_dispatch_at
87
- if scheduled_at.to_f < Time.now.to_f
43
+ error = false
44
+ while @running
45
+ begin
46
+ if error
47
+ unless connection.active?
48
+ connection.reconnect!
49
+ sleep(0.5)
50
+ connection.exec_query('LISTEN "skiplock::jobs"')
51
+ connection.exec_query('LISTEN "skiplock::workers"') if @master
88
52
  @next_schedule_at = Time.now.to_f
89
- elsif scheduled_at.to_f < @next_schedule_at
90
- @next_schedule_at = scheduled_at.to_f
91
53
  end
54
+ check_sync_errors
55
+ error = false
92
56
  end
93
- if @master
94
- # TODO: report worker status to action cable
95
- notifications['skiplock::workers'].each do |n|
57
+ notifications = { 'skiplock::jobs' => [], 'skiplock::workers' => [] }
58
+ connection.raw_connection.wait_for_notify(0.1) do |channel, pid, payload|
59
+ notifications[channel] << payload if payload
60
+ loop do
61
+ payload = connection.raw_connection.notifies
62
+ break unless @running && payload
63
+ notifications[payload[:relname]] << payload[:extra]
64
+ end
65
+ notifications['skiplock::jobs'].each do |n|
66
+ op, id, worker_id, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
67
+ if @master
68
+ # TODO: report job status to action cable
69
+ end
70
+ next if op == 'DELETE' || running == 'true' || expired_at.to_f > 0 || finished_at.to_f > 0 || scheduled_at.to_f < @last_dispatch_at
71
+ if scheduled_at.to_f < Time.now.to_f
72
+ @next_schedule_at = Time.now.to_f
73
+ elsif scheduled_at.to_f < @next_schedule_at
74
+ @next_schedule_at = scheduled_at.to_f
75
+ end
76
+ end
77
+ if @master
78
+ # TODO: report worker status to action cable
79
+ notifications['skiplock::workers'].each do |n|
80
+ end
96
81
  end
97
82
  end
83
+ if Time.now.to_f >= @next_schedule_at && @executor.remaining_capacity > 0
84
+ @executor.post { do_work }
85
+ end
86
+ rescue Exception => ex
87
+ # most likely error with database connection
88
+ STDERR.puts ex.message
89
+ STDERR.puts ex.backtrace
90
+ Skiplock.on_errors.each { |p| p.call(ex, @last_exception) }
91
+ error = true
92
+ t = Time.now
93
+ while @running
94
+ sleep(0.5)
95
+ break if Time.now - t > 5
96
+ end
97
+ @last_exception = ex
98
98
  end
99
- if Time.now.to_f >= @next_schedule_at && @executor.remaining_capacity > 0
100
- @executor.post { do_work }
101
- end
102
- rescue Exception => ex
103
- # most likely error with database connection
104
- STDERR.puts ex.message
105
- STDERR.puts ex.backtrace
106
- Skiplock.on_error.call(ex, @last_exception) if Skiplock.on_error.is_a?(Proc)
107
- error = true
108
- t = Time.now
109
- while @running
110
- sleep(0.5)
111
- break if Time.now - t > 5
112
- end
113
- @last_exception = ex
99
+ sleep(0.2)
114
100
  end
115
- sleep(0.2)
101
+ connection.exec_query('UNLISTEN *')
102
+ @executor.shutdown
103
+ @executor.wait_for_termination if @wait
104
+ @worker.delete if @worker
116
105
  end
117
- connection.exec_query('UNLISTEN *')
118
106
  end
119
107
  end
120
108
  end
121
109
 
122
110
  def shutdown(wait: true)
123
111
  @running = false
124
- @executor.shutdown
125
- @executor.wait_for_termination if wait
126
- @worker.delete if @worker
112
+ @wait = wait
127
113
  end
128
114
 
129
115
  private
130
116
 
117
+ def check_sync_errors
118
+ # get performed jobs that could not sync with database
119
+ Dir.glob('tmp/skiplock/*').each do |f|
120
+ job_from_db = Job.find_by(id: File.basename(f), running: true)
121
+ disposed = true
122
+ if job_from_db
123
+ job, ex = YAML.load_file(f) rescue nil
124
+ disposed = job.dispose(ex)
125
+ end
126
+ File.delete(f) if disposed
127
+ end
128
+ end
129
+
131
130
  def do_work
132
131
  while @running
133
- @last_dispatch_at = Time.now.to_f
132
+ @last_dispatch_at = Time.now.to_f - 1 # 1 second allowance for timedrift
134
133
  result = Job.dispatch(queues_order_query: @queues_order_query, worker_id: @worker.id)
135
134
  next if result.is_a?(Job)
136
135
  @next_schedule_at = result if result.is_a?(Float)
@@ -139,7 +138,7 @@ module Skiplock
139
138
  rescue Exception => ex
140
139
  STDERR.puts ex.message
141
140
  STDERR.puts ex.backtrace
142
- Skiplock.on_error.call(ex, @last_exception) if Skiplock.on_error.is_a?(Proc)
141
+ Skiplock.on_errors.each { |p| p.call(ex, @last_exception) }
143
142
  @last_exception = ex
144
143
  end
145
144
  end
data/lib/skiplock/job.rb CHANGED
@@ -1,18 +1,13 @@
1
1
  module Skiplock
2
2
  class Job < ActiveRecord::Base
3
- self.table_name = 'skiplock.jobs'
4
- Errors = Concurrent::Map.new
5
-
6
- # Return: Skiplock::Job if it was executed; otherwise returns the next Job's schedule time in FLOAT
7
3
  def self.dispatch(queues_order_query: nil, worker_id: nil)
8
- performed = false
9
4
  self.connection.exec_query('BEGIN')
10
5
  job = self.find_by_sql("SELECT id, scheduled_at FROM #{self.table_name} WHERE running = FALSE AND expired_at IS NULL AND finished_at IS NULL ORDER BY scheduled_at ASC NULLS FIRST,#{queues_order_query ? ' CASE ' + queues_order_query + ' ELSE NULL END ASC NULLS LAST,' : ''} priority ASC NULLS LAST, created_at ASC FOR UPDATE SKIP LOCKED LIMIT 1").first
11
6
  if job.nil? || job.scheduled_at.to_f > Time.now.to_f
12
7
  self.connection.exec_query('END')
13
8
  return (job ? job.scheduled_at.to_f : Float::INFINITY)
14
9
  end
15
- job = Skiplock::Job.find_by_sql("UPDATE #{self.table_name} SET running = TRUE, worker_id = #{self.connection.quote(worker_id)} WHERE id = '#{job.id}' RETURNING *").first
10
+ job = Skiplock::Job.find_by_sql("UPDATE #{self.table_name} SET running = TRUE, worker_id = #{self.connection.quote(worker_id)}, updated_at = NOW() WHERE id = '#{job.id}' RETURNING *").first
16
11
  self.connection.exec_query('END')
17
12
  job.data ||= {}
18
13
  job.exception_executions ||= {}
@@ -23,51 +18,15 @@ module Skiplock
23
18
  ActiveJob::Base.execute(job_data)
24
19
  rescue Exception => ex
25
20
  end
26
- performed = true
27
- job.running = false
28
- if ex
29
- job.exception_executions["[#{ex.class.name}]"] = (job.exception_executions["[#{ex.class.name}]"] || 0) + 1 unless job.exception_executions.key?('activejob_retry')
30
- if job.executions >= Settings['max_retries'] || job.exception_executions.key?('activejob_retry')
31
- job.expired_at = Time.now
32
- job.save!
33
- else
34
- job.scheduled_at = Time.now + (5 * 2**job.executions)
35
- job.save!
36
- end
37
- Skiplock.on_error.call(ex) if Skiplock.on_error.is_a?(Proc) && (job.executions % 3 == 1)
38
- elsif job.exception_executions.key?('activejob_retry')
39
- job.save!
40
- elsif job.cron
41
- job.data['last_cron_run'] = Time.now.utc.to_s
42
- next_cron_at = Cron.next_schedule_at(job.cron)
43
- if next_cron_at
44
- job.executions = 1
45
- job.exception_executions = nil
46
- job.scheduled_at = Time.at(next_cron_at)
47
- job.save!
48
- else
49
- job.delete
50
- end
51
- elsif Settings['purge_completion']
52
- job.delete
53
- else
54
- job.finished_at = Time.now
55
- job.exception_executions = nil
56
- job.save!
57
- end
58
- job
59
- rescue Exception => ex
60
- if performed
61
- Errors[job.id] = true
62
- File.write('tmp/cache/skiplock', job.id + "\n", mode: 'a')
63
- else
64
- Errors[job.id] = false
65
- end
66
- raise ex
21
+ job.dispose(ex)
67
22
  ensure
68
23
  Thread.current[:skiplock_dispatch_job] = nil
69
24
  end
70
25
 
26
+ def self.enqueue(activejob)
27
+ self.enqueue_at(activejob, nil)
28
+ end
29
+
71
30
  def self.enqueue_at(activejob, timestamp)
72
31
  timestamp = Time.at(timestamp) if timestamp
73
32
  if Thread.current[:skiplock_dispatch_job].try(:id) == activejob.job_id
@@ -76,8 +35,49 @@ module Skiplock
76
35
  Thread.current[:skiplock_dispatch_job].scheduled_at = timestamp
77
36
  Thread.current[:skiplock_dispatch_job]
78
37
  else
79
- Job.create!(id: activejob.job_id, job_class: activejob.class.name, queue_name: activejob.queue_name, locale: activejob.locale, timezone: activejob.timezone, priority: activejob.priority, executions: activejob.executions, data: { 'arguments' => activejob.serialize['arguments'] }, scheduled_at: timestamp)
38
+ Job.create!(id: activejob.job_id, job_class: activejob.class.name, queue_name: activejob.queue_name, locale: activejob.locale, timezone: activejob.timezone, priority: activejob.priority, data: { 'arguments' => activejob.serialize['arguments'] }, scheduled_at: timestamp)
39
+ end
40
+ end
41
+
42
+ def dispose(ex)
43
+ self.running = false
44
+ self.updated_at = (Time.now > self.updated_at ? Time.now : self.updated_at + 1)
45
+ if ex
46
+ self.exception_executions["[#{ex.class.name}]"] = (self.exception_executions["[#{ex.class.name}]"] || 0) + 1 unless self.exception_executions.key?('activejob_retry')
47
+ if self.executions >= Settings['max_retries'] || self.exception_executions.key?('activejob_retry')
48
+ self.expired_at = Time.now
49
+ self.save!
50
+ else
51
+ self.scheduled_at = Time.now + (5 * 2**self.executions)
52
+ self.save!
53
+ end
54
+ Skiplock.on_errors.each { |p| p.call(ex) }
55
+ elsif self.exception_executions.try(:key?, 'activejob_retry')
56
+ self.save!
57
+ elsif self.cron
58
+ self.data ||= {}
59
+ self.data['crons'] = (self.data['crons'] || 0) + 1
60
+ self.data['last_cron_at'] = Time.now.utc.to_s
61
+ next_cron_at = Cron.next_schedule_at(self.cron)
62
+ if next_cron_at
63
+ self.executions = nil
64
+ self.exception_executions = nil
65
+ self.scheduled_at = Time.at(next_cron_at)
66
+ self.save!
67
+ else
68
+ self.delete
69
+ end
70
+ elsif Settings['purge_completion']
71
+ self.delete
72
+ else
73
+ self.finished_at = Time.now
74
+ self.exception_executions = nil
75
+ self.save!
80
76
  end
77
+ self
78
+ rescue
79
+ File.write("tmp/skiplock/#{self.id}", [self, ex].to_yaml)
80
+ nil
81
81
  end
82
82
  end
83
83
  end
@@ -14,9 +14,10 @@ module Skiplock
14
14
  Settings['max_threads'] = 20 if Settings['max_threads'] > 20
15
15
  Settings['min_threads'] = 0 if Settings['min_threads'] < 0
16
16
  Settings['workers'] = 0 if Settings['workers'] < 0
17
+ Settings['workers'] = 1 if standalone && Settings['workers'] <= 0
17
18
  Settings.freeze
18
19
  end
19
- return unless standalone || restart || (caller.any?{|l| l =~ %r{/rack/}} && (Settings['workers'] == 0 || Rails.env.development?))
20
+ return unless standalone || restart || (caller.any?{|l| l =~ %r{/rack/}} && Settings['workers'] == 0)
20
21
  if standalone
21
22
  self.standalone
22
23
  else
@@ -42,27 +43,35 @@ module Skiplock
42
43
  config = YAML.load_file('config/skiplock.yml') rescue {}
43
44
  Settings.merge!(config)
44
45
  Settings['queues'].values.each { |v| raise 'Queue value must be an integer' unless v.is_a?(Integer) } if Settings['queues'].is_a?(Hash)
45
- case Settings['notification'].to_s.downcase
46
- when 'auto'
46
+ @notification = Settings['notification'] = Settings['notification'].to_s.downcase
47
+ if @notification == 'auto'
47
48
  if defined?(Airbrake)
48
- Skiplock.on_error = -> (ex, previous = nil) { Airbrake.notify_sync(ex) unless ex.backtrace == previous.try(:backtrace) }
49
+ @notification = 'airbrake'
49
50
  elsif defined?(Bugsnag)
50
- Skiplock.on_error = -> (ex, previous = nil) { Bugsnag.notify(ex) unless ex.backtrace == previous.try(:backtrace) }
51
+ @notification = 'bugsnag'
51
52
  elsif defined?(ExceptionNotifier)
52
- Skiplock.on_error = -> (ex, previous = nil) { ExceptionNotifier.notify_exception(ex) unless ex.backtrace == previous.try(:backtrace) }
53
+ @notification = 'exception_notification'
53
54
  else
54
- puts "Unable to detect any known exception notification gem. Please define custom 'on_error' function and disable notification in 'config/skiplock.yml'"
55
+ puts "Unable to detect any known exception notification gem. Please define custom 'on_error' callback function and disable 'auto' notification in 'config/skiplock.yml'"
55
56
  exit
56
57
  end
58
+ end
59
+ case @notification
57
60
  when 'airbrake'
58
61
  raise 'airbrake gem not found' unless defined?(Airbrake)
59
- Skiplock.on_error = -> (ex, previous = nil) { Airbrake.notify_sync(ex) unless ex.backtrace == previous.try(:backtrace) }
62
+ Skiplock.on_error do |ex, previous|
63
+ Airbrake.notify_sync(ex) unless ex.backtrace == previous.try(:backtrace)
64
+ end
60
65
  when 'bugsnag'
61
66
  raise 'bugsnag gem not found' unless defined?(Bugsnag)
62
- Skiplock.on_error = -> (ex, previous = nil) { Bugsnag.notify(ex) unless ex.backtrace == previous.try(:backtrace) }
67
+ Skiplock.on_error do |ex, previous|
68
+ Bugsnag.notify(ex) unless ex.backtrace == previous.try(:backtrace)
69
+ end
63
70
  when 'exception_notification'
64
71
  raise 'exception_notification gem not found' unless defined?(ExceptionNotifier)
65
- Skiplock.on_error = -> (ex, previous = nil) { ExceptionNotifier.notify_exception(ex) unless ex.backtrace == previous.try(:backtrace) }
72
+ Skiplock.on_error do |ex, previous|
73
+ ExceptionNotifier.notify_exception(ex) unless ex.backtrace == previous.try(:backtrace)
74
+ end
66
75
  end
67
76
  rescue Exception => e
68
77
  STDERR.puts "Invalid configuration 'config/skiplock.yml': #{e.message}"
@@ -88,7 +97,7 @@ module Skiplock
88
97
  puts title
89
98
  puts "-"*(title.length)
90
99
  puts "Purge completion: #{Settings['purge_completion']}"
91
- puts " Notification: #{Settings['notification']}"
100
+ puts " Notification: #{Settings['notification']}#{(' (' + @notification + ')') if Settings['notification'] == 'auto'}"
92
101
  puts " Max retries: #{Settings['max_retries']}"
93
102
  puts " Min threads: #{Settings['min_threads']}"
94
103
  puts " Max threads: #{Settings['max_threads']}"
@@ -1,4 +1,4 @@
1
1
  module Skiplock
2
- VERSION = Version = '1.0.8'
2
+ VERSION = Version = '1.0.9'
3
3
  end
4
4
 
@@ -1,5 +1,4 @@
1
1
  module Skiplock
2
2
  class Worker < ActiveRecord::Base
3
- self.table_name = 'skiplock.workers'
4
3
  end
5
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skiplock
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.8
4
+ version: 1.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tin Vo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-01 00:00:00.000000000 Z
11
+ date: 2021-05-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -82,6 +82,7 @@ files:
82
82
  - lib/generators/skiplock/install_generator.rb
83
83
  - lib/generators/skiplock/templates/migration.rb.erb
84
84
  - lib/skiplock.rb
85
+ - lib/skiplock/counter.rb
85
86
  - lib/skiplock/cron.rb
86
87
  - lib/skiplock/dispatcher.rb
87
88
  - lib/skiplock/job.rb
@@ -107,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
108
  - !ruby/object:Gem::Version
108
109
  version: '0'
109
110
  requirements: []
110
- rubygems_version: 3.1.4
111
+ rubygems_version: 3.0.3
111
112
  signing_key:
112
113
  specification_version: 4
113
114
  summary: ActiveJob Queue Adapter for PostgreSQL