skiplock 1.0.8 → 1.0.9

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