skiplock 1.0.16 → 1.0.17

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: 7e9881d695e3f1a6241e3d86ca7844dff77ea3ba2b7431d70546a685dd1829a3
4
- data.tar.gz: e9e2957e874d1a878f9302d4d08244830cf6abb7e9d9c6dc0970a2347344c0b7
3
+ metadata.gz: d9533e3df15a24e784000c160588b96207271986bf927bff095752e556c3376a
4
+ data.tar.gz: dcc40ab796f55fc41363d671d5e4f73600c58c468d7c4543e496fad38c3e9a8a
5
5
  SHA512:
6
- metadata.gz: 7d5045bc4dcee7ebab3838f7970a0ed681cda2756736491df734a60eb04d0b4a2a4525073e52e59df43a6ef2cd1f11ade191002de1be53ad01b0169f8b21687d
7
- data.tar.gz: 75b542459110cbc57ac2ea1dd19a9f3431f1c9d744871c64e0543cdd61a76d87ab3e5c946bef1cab43d2401b2970f17d3d7c35019ed8647a3d96a98f8f5e6a2e
6
+ metadata.gz: 0a0c21835b52f749022d7f3a135f041d122425defffaf6aebb6d8510d4dd0836aed2fb9977daff9e7d3bf2547e139d2dd3ea49f5f55a869ce851c93aaeb56a12
7
+ data.tar.gz: d94c6801c82e1bddb12cff0152b71e8554d03de4d28f51ec0d742807311adecca1b3b142aeadf3cd5e81336416b03206d81848f6da6abf8e0f8c726631a5d898
data/README.md CHANGED
@@ -51,7 +51,7 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
51
51
  # config/skiplock.yml (default settings)
52
52
  ---
53
53
  min_threads: 1
54
- max_threads: 5
54
+ max_threads: 10
55
55
  max_retries: 20
56
56
  logfile: skiplock.log
57
57
  loglevel: info
@@ -70,8 +70,8 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
70
70
  - **logfile** (*string*): filename for skiplock logs; empty logfile will disable logging
71
71
  - **loglevel** (*string*): sets logging level (`debug, info, warn, error, fatal, unknown`)
72
72
  - **notification** (*string*): sets the library to be used for notifying errors and exceptions (`auto, airbrake, bugsnag, exception_notification, custom`); using `auto` will detect library if available. See `Notification system` for more details
73
- - **extensions** (*boolean*): enable or disable the class method extension. See `ClassMethod extension` for more details
74
- - **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)
73
+ - **extensions** (*multi*): enable or disable the class method extension. See `ClassMethod extension` for more details
74
+ - **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); queued jobs can manually override using `purge` option
75
75
  - **queues** (*hash*): defines the set of queues with priorities; lower priority takes precedence
76
76
  - **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**
77
77
 
@@ -89,6 +89,7 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
89
89
  -r, --max-retries NUM Number of maxixum retries
90
90
  -t, --max-threads NUM Number of maximum threads
91
91
  -T, --min-threads NUM Number of minimum threads
92
+ -v, --version Show version information
92
93
  -w, --workers NUM Number of workers
93
94
  -h, --help Show this message
94
95
  ```
@@ -99,19 +100,26 @@ Inside the Rails application:
99
100
  ```ruby
100
101
  MyJob.perform_later
101
102
  ```
102
- - Skiplock supports all ActiveJob features
103
+ - Skiplock supports all ActiveJob features and options
103
104
  ```ruby
104
105
  MyJob.set(queue: 'my_queue', wait: 5.minutes, priority: 10).perform_later(1,2,3)
106
+ MyJob.set(wait_until: Day.tomorrow.noon).perform_later(1,2,3)
107
+ ```
108
+ - Skiplock supports custom options which override the global `Skiplock` configuration options for specified jobs
109
+ - **purge** (*boolean*): whether to remove this job after it has ran successfully
110
+ - **max_retries** (*integer*): set maximum retry attempt for this job
111
+ ```ruby
112
+ MyJob.set(purge: false, max_retries: 5).perform_later(1,2,3)
105
113
  ```
106
114
  Outside the Rails application:
107
115
  - queue the jobs by inserting the job records directly to the database table
108
116
  ```sql
109
117
  INSERT INTO skiplock.jobs(job_class) VALUES ('MyJob');
110
118
  ```
111
- - with scheduling, priority, queue and arguments
119
+ - with scheduling, priority, queue, arguments and custom options
112
120
  ```sql
113
121
  INSERT INTO skiplock.jobs(job_class, queue_name, priority, scheduled_at, data)
114
- VALUES ('MyJob', 'my_queue', 10, NOW() + INTERVAL '5 min', '{"arguments":[1,2,3]}');
122
+ VALUES ('MyJob', 'my_queue', 10, NOW() + INTERVAL '5 min', '{"arguments":[1,2,3],"options":{"purge":false,"max_retries":5}}');
115
123
  ```
116
124
  ## Queue priority vs Job priority
117
125
  *Why do queues use priorities when jobs already have priorities?*
@@ -165,15 +173,21 @@ If the `retry_on` block is not defined, then the built-in retry system of `Skipl
165
173
  # config/initializers/skiplock.rb
166
174
  Skiplock.on_error do |ex, previous|
167
175
  if ex.backtrace != previous.try(:backtrace)
168
- # sends custom email on new exceptions only
176
+ # sends text message using Amazon SNS on new exceptions only
169
177
  # the same repeated exceptions will only be sent once to avoid SPAM
170
178
  # NOTE: exceptions generated from Job executions will not provide 'previous' exceptions
179
+ sms = Aws::SNS::Client.new(region: 'us-west-2', access_key_id: Rails.application.credentials[:aws][:access_key_id], secret_access_key: Rails.application.credentials[:aws][:secret_access_key])
180
+ sms.publish({ phone_number: '+122233334444', message: "Exception: #{ex.message}"[0..130] })
171
181
  end
172
182
  end
173
183
  # supports multiple 'on_error' event callbacks
174
184
  ```
175
185
  ## ClassMethod extension
176
- `Skiplock` can add extension to allow all class methods to be performed as a background job; it is disabled in the default configuration. To enable, edit the `config/skiplock.yml` configuration file and change `extensions` to `true`.
186
+ `Skiplock` can add extension to allow class methods to be performed as a background job; it is disabled in the default configuration. To enable globally for all classes and modules, edit the `config/skiplock.yml` configuration file and change `extensions` to `true`; this can expose remote execution if the `skiplock.jobs` database table is not secured properly. To enable extension for specific classes and modules only then set the configuration to an array of names of the classes and modules eg. `['MyClass', 'MyModule']`
187
+ - An example of remote execution if the extension is enabled globally (ie: configuration is set to `true`) and attacker can insert `skiplock.jobs`
188
+ ```sql
189
+ INSERT INTO skiplock.jobs(job_class, data) VALUES ('Skiplock::Extension::ProxyJob', '{"arguments":["---\n- !ruby/module ''Kernel''\n- :system\n- - rm -rf /tmp/*\n"]}');
190
+ ```
177
191
  - Queue class method `generate_thumbnails` of class `Image` as background job to run as soon as possible
178
192
  ```ruby
179
193
  Image.skiplock.generate_thumbnails(height: 100, ratio: true)
@@ -188,24 +202,25 @@ If the `retry_on` block is not defined, then the built-in retry system of `Skipl
188
202
  ```
189
203
 
190
204
  ## Fault tolerant
191
- `Skiplock` ensures that jobs will be executed sucessfully only once even if database connection is lost during or after the job was dispatched. Successful jobs are marked as completed or removed (with `purge_completion` turned on), and failed or interrupted jobs are marked for retry.
205
+ `Skiplock` ensures that jobs will be executed sucessfully only once even if database connection is lost during or after the job was dispatched. Successful jobs are marked as completed or removed (with `purge_completion` global configuration or `purge` job option); failed or interrupted jobs are marked for retry.
192
206
 
193
207
  However, when the database connection is dropped for any reasons and the commit is lost, `Skiplock` will then save the commit data to local disk (as `tmp/skiplock/<job_id>`) and synchronize with the database when the connection resumes.
194
208
 
195
- This also protects in-progress jobs that were terminated abruptly during a graceful shutdown with timeout; they will be queued for retry.
209
+ This also protects long running in-progress jobs that are terminated abruptly during a graceful shutdown with timeout; these will be queued for retry.
196
210
 
197
211
  ## Scalability
198
212
  `Skiplock` can scale both vertically and horizontally. To scale vertically, simply increase the number of `Skiplock` workers per host. To scale horizontally, simply deploy `Skiplock` to multiple hosts sharing the same PostgreSQL database.
199
213
 
200
- ## Statistics and counters
214
+ ## Statistics, analytics and counters
201
215
  The `skiplock.workers` database table contains all the `Skiplock` workers running on all the hosts. Active worker will update its timestamp column (`updated_at`) every minute; and dispatched jobs would be associated with the running workers. At any given time, a list of active workers running a list of jobs can be determined using the database table.
202
216
 
203
- The `skiplock.counters` database table contains all historical job dispatches, completions, expiries, failures and retries. The counters are recorded by dates; so it's possible to get statistical data for any given day or range of dates.
217
+ The `skiplock.jobs` database table contains all the `Skiplob` jobs. Each job's successful execution stores the result to its `data['result']` field column. If job completions are not purged then their execution results can be used for analytic purposes.
204
218
 
219
+ The `skiplock.counters` database table contains all the counters for job dispatches, completions, expiries, failures and retries. The counters are recorded by dates; so it's possible to get statistical data for any given day or range of dates.
205
220
  - **completions**: numbers of jobs completed successfully
206
221
  - **dispatches**: number of jobs dispatched for the first time (**retries** are not counted here)
207
- - **expiries**: number of jobs exceeded `max_retry` and still failed to complete
208
- - **failures**: number of jobs interrupted by graceful shutdown or errors (exceptions)
222
+ - **expiries**: number of jobs exceeded `max_retries` and still failed to complete
223
+ - **failures**: number of jobs interrupted by graceful shutdown or unable to complete due to errors (exceptions)
209
224
  - **retries**: number of jobs dispatched for retrying
210
225
 
211
226
  Code examples of gathering counters information:
@@ -56,7 +56,7 @@ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSIO
56
56
  INSERT INTO skiplock.counters (day,completions) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET completions = skiplock.counters.completions + 1;
57
57
  ELSIF (record.expired_at IS NOT NULL) THEN
58
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 > 0) THEN
59
+ ELSIF (TG_OP = 'UPDATE' AND OLD.running = TRUE AND NEW.running = FALSE) THEN
60
60
  INSERT INTO skiplock.counters (day,failures) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET failures = skiplock.counters.failures + 1;
61
61
  END IF;
62
62
  PERFORM pg_notify('skiplock::jobs', CONCAT(TG_OP,',',record.id::TEXT,',',record.worker_id::TEXT,',',record.job_class,',',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));
data/lib/skiplock/cron.rb CHANGED
@@ -6,7 +6,7 @@ module Skiplock
6
6
  ActiveJob::Base.descendants.each do |j|
7
7
  next unless j.const_defined?('CRON')
8
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, locale: I18n.locale, timezone: Time.zone.name)
9
+ job = Job.find_by('job_class = ? AND cron IS NOT NULL', j.name) || Job.new(job_class: j.name, queue_name: j.queue_as, cron: cron, locale: I18n.locale, timezone: Time.zone.name)
10
10
  time = self.next_schedule_at(cron)
11
11
  if time
12
12
  job.cron = cron
data/lib/skiplock/job.rb CHANGED
@@ -4,21 +4,6 @@ module Skiplock
4
4
  attr_accessor :activejob_retry
5
5
  belongs_to :worker, inverse_of: :jobs, required: false
6
6
 
7
- # resynchronize jobs that could not commit to database and retry any abandoned jobs
8
- def self.cleanup(purge_completion: true, max_retries: 20)
9
- Dir.mkdir('tmp/skiplock') unless Dir.exist?('tmp/skiplock')
10
- Dir.glob('tmp/skiplock/*').each do |f|
11
- job_from_db = self.find_by(id: File.basename(f), running: true)
12
- disposed = true
13
- if job_from_db
14
- job, ex = YAML.load_file(f) rescue nil
15
- disposed = job.dispose(ex, purge_completion: purge_completion, max_retries: max_retries) if job
16
- end
17
- (File.delete(f) rescue nil) if disposed
18
- end
19
- self.where(running: true).where.not(worker_id: Worker.ids).update_all(running: false, worker_id: nil)
20
- end
21
-
22
7
  def self.dispatch(purge_completion: true, max_retries: 20)
23
8
  job = nil
24
9
  self.connection.transaction do
@@ -35,44 +20,61 @@ module Skiplock
35
20
 
36
21
  def self.enqueue_at(activejob, timestamp)
37
22
  timestamp = Time.at(timestamp) if timestamp
38
- if Thread.current[:skiplock_dispatch_job].try(:id) == activejob.job_id
39
- Thread.current[:skiplock_dispatch_job].activejob_retry = true
40
- Thread.current[:skiplock_dispatch_job].executions = activejob.executions
41
- Thread.current[:skiplock_dispatch_job].exception_executions = activejob.exception_executions
42
- Thread.current[:skiplock_dispatch_job].scheduled_at = timestamp
43
- Thread.current[:skiplock_dispatch_job]
23
+ if Thread.current[:skiplock_job].try(:id) == activejob.job_id
24
+ Thread.current[:skiplock_job].activejob_retry = true
25
+ Thread.current[:skiplock_job].executions = activejob.executions
26
+ Thread.current[:skiplock_job].exception_executions = activejob.exception_executions
27
+ Thread.current[:skiplock_job].scheduled_at = timestamp
28
+ Thread.current[:skiplock_job]
44
29
  else
30
+ options = activejob.instance_variable_get('@skiplock_options') || {}
45
31
  serialize = activejob.serialize
46
- self.create!(serialize.slice(*self.column_names).merge('id' => serialize['job_id'], 'data' => { 'arguments' => serialize['arguments'] }, 'scheduled_at' => timestamp))
32
+ self.create!(serialize.slice(*self.column_names).merge('id' => serialize['job_id'], 'data' => { 'arguments' => serialize['arguments'], 'options' => options }, 'scheduled_at' => timestamp))
47
33
  end
48
34
  end
49
35
 
36
+ # resynchronize jobs that could not commit to database and retry any abandoned jobs
37
+ def self.flush
38
+ Dir.mkdir('tmp/skiplock') unless Dir.exist?('tmp/skiplock')
39
+ Dir.glob('tmp/skiplock/*').each do |f|
40
+ disposed = true
41
+ if self.exists?(id: File.basename(f), running: true)
42
+ job = Marshal.load(File.binread(f)) rescue nil
43
+ disposed = job.dispose if job.is_a?(Skiplock::Job)
44
+ end
45
+ (File.delete(f) rescue nil) if disposed
46
+ end
47
+ self.where(running: true).where.not(worker_id: Worker.ids).update_all(running: false, worker_id: nil)
48
+ true
49
+ end
50
+
50
51
  def self.reset_retry_schedules
51
- self.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)
52
+ self.where('scheduled_at > NOW() AND executions > 0 AND expired_at IS NULL AND finished_at IS NULL').update_all(scheduled_at: nil, updated_at: Time.now)
52
53
  end
53
54
 
54
- def dispose(ex, purge_completion: true, max_retries: 20)
55
- yaml = [self, ex].to_yaml
55
+ def dispose
56
+ return unless @max_retries
57
+ dump = Marshal.dump(self)
56
58
  purging = false
57
59
  self.running = false
58
60
  self.worker_id = nil
59
61
  self.updated_at = Time.now > self.updated_at ? Time.now : self.updated_at + 1 # in case of clock drifting
60
- if ex
61
- self.exception_executions ||= {}
62
- self.exception_executions["[#{ex.class.name}]"] = self.exception_executions["[#{ex.class.name}]"].to_i + 1 unless self.activejob_retry
63
- if self.executions.to_i >= max_retries || self.activejob_retry
62
+ if @exception
63
+ self.exception_executions["[#{@exception.class.name}]"] = self.exception_executions["[#{@exception.class.name}]"].to_i + 1 unless self.activejob_retry
64
+ if (self.executions.to_i >= @max_retries + 1) || self.activejob_retry
64
65
  self.expired_at = Time.now
65
66
  else
66
67
  self.scheduled_at = Time.now + (5 * 2**self.executions.to_i)
67
68
  end
68
- Skiplock.on_errors.each { |p| p.call(ex) }
69
69
  elsif self.finished_at
70
70
  if self.cron
71
- self.data ||= {}
72
- self.data['crons'] = (self.data['crons'] || 0) + 1
73
- self.data['last_cron_at'] = Time.now.utc.to_s
71
+ self.data['cron'] ||= {}
72
+ self.data['cron']['executions'] = self.data['cron']['executions'].to_i + 1
73
+ self.data['cron']['last_finished_at'] = self.finished_at.utc.to_s
74
74
  next_cron_at = Cron.next_schedule_at(self.cron)
75
75
  if next_cron_at
76
+ # update job to record completions counter before resetting finished_at to nil
77
+ self.update_columns(self.attributes.slice(*self.changes.keys))
76
78
  self.finished_at = nil
77
79
  self.executions = nil
78
80
  self.exception_executions = nil
@@ -81,13 +83,13 @@ module Skiplock
81
83
  Skiplock.logger.error("[Skiplock] ERROR: Invalid CRON '#{self.cron}' for Job #{self.job_class}") if Skiplock.logger
82
84
  purging = true
83
85
  end
84
- elsif purge_completion
86
+ elsif @purge == true
85
87
  purging = true
86
88
  end
87
89
  end
88
90
  purging ? self.delete : self.update_columns(self.attributes.slice(*self.changes.keys))
89
91
  rescue Exception => e
90
- File.write("tmp/skiplock/#{self.id}", yaml) rescue nil
92
+ File.binwrite("tmp/skiplock/#{self.id}", dump) rescue nil
91
93
  if Skiplock.logger
92
94
  Skiplock.logger.error(e.to_s)
93
95
  Skiplock.logger.error(e.backtrace.join("\n"))
@@ -97,25 +99,31 @@ module Skiplock
97
99
  end
98
100
 
99
101
  def execute(purge_completion: true, max_retries: 20)
102
+ raise 'Job has already been completed' if self.finished_at
100
103
  Skiplock.logger.info("[Skiplock] Performing #{self.job_class} (#{self.id}) from queue '#{self.queue_name || 'default'}'...") if Skiplock.logger
101
104
  self.data ||= {}
105
+ self.data.delete('result')
102
106
  self.exception_executions ||= {}
103
107
  self.activejob_retry = false
108
+ @max_retries = (self.data['options'].key?('max_retries') ? self.data['options']['max_retries'].to_i : max_retries) rescue max_retries
109
+ @max_retries = 20 if @max_retries < 0 || @max_retries > 20
110
+ @purge = (self.data['options'].key?('purge') ? self.data['options']['purge'] : purge_completion) rescue purge_completion
104
111
  job_data = self.attributes.slice('job_class', 'queue_name', 'locale', 'timezone', 'priority', 'executions', 'exception_executions').merge('job_id' => self.id, 'enqueued_at' => self.updated_at, 'arguments' => (self.data['arguments'] || []))
105
112
  self.executions = self.executions.to_i + 1
106
- Thread.current[:skiplock_dispatch_job] = self
113
+ Thread.current[:skiplock_job] = self
107
114
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
108
115
  begin
109
- ActiveJob::Base.execute(job_data)
110
- self.finished_at = Time.now unless self.activejob_retry
116
+ self.data['result'] = ActiveJob::Base.execute(job_data)
111
117
  rescue Exception => ex
118
+ @exception = ex
119
+ Skiplock.on_errors.each { |p| p.call(@exception) }
112
120
  end
113
121
  if Skiplock.logger
114
- if ex || self.activejob_retry
122
+ if @exception || self.activejob_retry
115
123
  Skiplock.logger.error("[Skiplock] Job #{self.job_class} (#{self.id}) was interrupted by an exception#{ ' (rescued and retried by ActiveJob)' if self.activejob_retry }")
116
- if ex
117
- Skiplock.logger.error(ex.to_s)
118
- Skiplock.logger.error(ex.backtrace.join("\n"))
124
+ if @exception
125
+ Skiplock.logger.error(@exception.to_s)
126
+ Skiplock.logger.error(@exception.backtrace.join("\n"))
119
127
  end
120
128
  else
121
129
  end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -128,7 +136,8 @@ module Skiplock
128
136
  end
129
137
  end
130
138
  ensure
131
- self.dispose(ex, purge_completion: purge_completion, max_retries: max_retries)
139
+ self.finished_at ||= Time.now if self.data.key?('result') && !self.activejob_retry
140
+ self.dispose
132
141
  end
133
142
  end
134
143
  end
@@ -6,13 +6,12 @@ module Skiplock
6
6
  @config.symbolize_keys!
7
7
  @config.transform_values! {|v| v.is_a?(String) ? v.downcase : v}
8
8
  @config.merge!(config)
9
- @config[:hostname] = `hostname -f`.strip
9
+ @hostname = Socket.gethostname
10
10
  configure
11
11
  setup_logger
12
- Module.__send__(:include, Skiplock::Extension) if @config[:extensions] == true
13
12
  if (caller.any?{ |l| l =~ %r{/rack/} } && @config[:workers] == 0)
14
- cleanup_workers
15
- @worker = create_worker
13
+ Worker.cleanup(@hostname)
14
+ @worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname)
16
15
  @worker.start(**@config)
17
16
  at_exit { @worker.shutdown }
18
17
  end
@@ -28,8 +27,8 @@ module Skiplock
28
27
  @config[:workers] = 1 if @config[:workers] <= 0
29
28
  @config[:standalone] = true
30
29
  banner
31
- cleanup_workers
32
- @worker = create_worker
30
+ Worker.cleanup(@hostname)
31
+ @worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname)
33
32
  @parent_id = Process.pid
34
33
  @shutdown = false
35
34
  Signal.trap('INT') { @shutdown = true }
@@ -38,7 +37,7 @@ module Skiplock
38
37
  (@config[:workers] - 1).times do |n|
39
38
  fork do
40
39
  sleep 1
41
- worker = create_worker(master: false)
40
+ worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname, master: false)
42
41
  worker.start(worker_num: n + 1, **@config)
43
42
  loop do
44
43
  sleep 0.5
@@ -81,22 +80,6 @@ module Skiplock
81
80
  @logger.warn "[Skiplock] Custom notification has no registered 'on_error' callback" if Skiplock.on_errors.count == 0
82
81
  end
83
82
 
84
- def cleanup_workers
85
- Rails.application.eager_load! if Rails.env.development?
86
- delete_ids = []
87
- Worker.where(hostname: @config[:hostname]).each do |worker|
88
- sid = Process.getsid(worker.pid) rescue nil
89
- delete_ids << worker.id if worker.sid != sid || worker.updated_at < 10.minutes.ago
90
- end
91
- Worker.where(id: delete_ids).delete_all if delete_ids.count > 0
92
- end
93
-
94
- def create_worker(master: true)
95
- Worker.create!(pid: Process.pid, sid: Process.getsid(), master: master, hostname: @config[:hostname], capacity: @config[:max_threads])
96
- rescue
97
- Worker.create!(pid: Process.pid, sid: Process.getsid(), master: false, hostname: @config[:hostname], capacity: @config[:max_threads])
98
- end
99
-
100
83
  def configure
101
84
  @config[:graceful_shutdown] = 300 if @config[:graceful_shutdown] > 300
102
85
  @config[:graceful_shutdown] = nil if @config[:graceful_shutdown] < 0
@@ -137,21 +120,27 @@ module Skiplock
137
120
  else
138
121
  @config[:notification] = 'custom'
139
122
  end
123
+ Rails.application.eager_load! if Rails.env.development?
124
+ if @config[:extensions] == true
125
+ Module.__send__(:include, Skiplock::Extension)
126
+ elsif @config[:extensions].is_a?(Array)
127
+ @config[:extensions].each { |n| n.constantize.__send__(:extend, Skiplock::Extension) if n.safe_constantize }
128
+ end
140
129
  Skiplock.on_errors.freeze unless Skiplock.on_errors.frozen?
141
130
  end
142
131
 
143
132
  def setup_logger
144
- @config[:loglevel] = 'info' unless ['debug','info','warn','error','fatal','unknown'].include?(@config[:loglevel].to_s)
133
+ @config[:loglevel] = 'info' unless ['debug','info','warn','error','fatal','unknown'].include?(@config[:loglevel].to_s.downcase)
145
134
  @logger = ActiveSupport::Logger.new(STDOUT)
146
- @logger.level = @config[:loglevel].to_sym
135
+ @logger.level = @config[:loglevel].downcase.to_sym
147
136
  Skiplock.logger = @logger
148
137
  if @config[:logfile].to_s.length > 0
149
138
  @logger.extend(ActiveSupport::Logger.broadcast(::Logger.new(File.join(Rails.root, 'log', @config[:logfile].to_s), 'daily')))
150
139
  ActiveJob::Base.logger = nil
151
140
  end
152
141
  rescue Exception => ex
153
- puts "Exception with logger: #{ex.to_s}"
154
- puts ex.backtrace.join("\n")
142
+ @logger.error "Exception with logger: #{ex.to_s}"
143
+ @logger.error ex.backtrace.join("\n")
155
144
  Skiplock.on_errors.each { |p| p.call(ex) }
156
145
  end
157
146
  end
@@ -0,0 +1,8 @@
1
+ module Skiplock
2
+ module Patch
3
+ def enqueue(options = {})
4
+ self.instance_variable_set('@skiplock_options', options)
5
+ super
6
+ end
7
+ end
8
+ end
@@ -1,4 +1,4 @@
1
1
  module Skiplock
2
- VERSION = Version = '1.0.16'
2
+ VERSION = Version = '1.0.17'
3
3
  end
4
4
 
@@ -3,18 +3,32 @@ module Skiplock
3
3
  self.implicit_order_column = 'created_at'
4
4
  has_many :jobs, inverse_of: :worker
5
5
 
6
+ def self.cleanup(hostname = nil)
7
+ delete_ids = []
8
+ self.where(hostname: hostname || Socket.gethostname).each do |worker|
9
+ sid = Process.getsid(worker.pid) rescue nil
10
+ delete_ids << worker.id if worker.sid != sid || worker.updated_at < 10.minutes.ago
11
+ end
12
+ self.where(id: delete_ids).delete_all if delete_ids.count > 0
13
+ end
14
+
15
+ def self.generate(capacity:, hostname:, master: true)
16
+ self.create!(pid: Process.pid, sid: Process.getsid(), master: master, hostname: hostname, capacity: capacity)
17
+ rescue
18
+ self.create!(pid: Process.pid, sid: Process.getsid(), master: false, hostname: hostname, capacity: capacity)
19
+ end
20
+
6
21
  def start(worker_num: 0, **config)
7
- @config = config
8
- @queues_order_query = @config[:queues].map { |q,v| "WHEN queue_name = '#{q}' THEN #{v}" }.join(' ') if @config[:queues].is_a?(Hash) && @config[:queues].count > 0
9
- @next_schedule_at = Time.now.to_f
10
- @executor = Concurrent::ThreadPoolExecutor.new(min_threads: @config[:min_threads] + 1, max_threads: @config[:max_threads] + 1, max_queue: @config[:max_threads], idletime: 60, auto_terminate: true, fallback_policy: :discard)
11
22
  if self.master
12
- Job.cleanup(purge_completion: @config[:purge_completion], max_retries: @config[:max_retries])
23
+ Job.flush
13
24
  Cron.setup
14
25
  end
26
+ @config = config
27
+ @queues_order_query = @config[:queues].map { |q,v| "WHEN queue_name = '#{q}' THEN #{v}" }.join(' ') if @config[:queues].is_a?(Hash) && @config[:queues].count > 0
15
28
  @running = true
16
- Process.setproctitle("skiplock-#{self.master ? 'master[0]' : 'worker[' + worker_num.to_s + ']'}") if @config[:standalone]
29
+ @executor = Concurrent::ThreadPoolExecutor.new(min_threads: @config[:min_threads] + 1, max_threads: @config[:max_threads] + 1, max_queue: @config[:max_threads], idletime: 60, auto_terminate: true, fallback_policy: :discard)
17
30
  @executor.post { run }
31
+ Process.setproctitle("skiplock-#{self.master ? 'master[0]' : 'worker[' + worker_num.to_s + ']'}") if @config[:standalone]
18
32
  end
19
33
 
20
34
  def shutdown
@@ -39,6 +53,7 @@ module Skiplock
39
53
  def run
40
54
  error = false
41
55
  listen = false
56
+ next_schedule_at = Time.now.to_f
42
57
  timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
43
58
  while @running
44
59
  Rails.application.reloader.wrap do
@@ -53,17 +68,17 @@ module Skiplock
53
68
  @connection.reconnect!
54
69
  sleep(0.5)
55
70
  @connection.exec_query('LISTEN "skiplock::jobs"')
56
- @next_schedule_at = Time.now.to_f
71
+ next_schedule_at = Time.now.to_f
57
72
  end
58
- Job.cleanup if self.master
73
+ Job.flush if self.master
59
74
  error = false
60
75
  end
61
- if Time.now.to_f >= @next_schedule_at && @executor.remaining_capacity > 0
76
+ if Time.now.to_f >= next_schedule_at && @executor.remaining_capacity > 0
62
77
  job = get_next_available_job
63
78
  if job.try(:running)
64
79
  @executor.post { Rails.application.reloader.wrap { job.execute(purge_completion: @config[:purge_completion], max_retries: @config[:max_retries]) } }
65
80
  else
66
- @next_schedule_at = (job ? job.scheduled_at.to_f : Float::INFINITY)
81
+ next_schedule_at = (job ? job.scheduled_at.to_f : Float::INFINITY)
67
82
  end
68
83
  end
69
84
  job_notifications = []
@@ -77,7 +92,7 @@ module Skiplock
77
92
  job_notifications.each do |n|
78
93
  op, id, worker_id, job_class, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
79
94
  next if op == 'DELETE' || running == 'true' || expired_at.to_f > 0 || finished_at.to_f > 0
80
- @next_schedule_at = scheduled_at.to_f if scheduled_at.to_f < @next_schedule_at
95
+ next_schedule_at = scheduled_at.to_f if scheduled_at.to_f < next_schedule_at
81
96
  end
82
97
  end
83
98
  if Process.clock_gettime(Process::CLOCK_MONOTONIC) - timestamp > 60
data/lib/skiplock.rb CHANGED
@@ -6,11 +6,12 @@ require 'skiplock/cron'
6
6
  require 'skiplock/extension'
7
7
  require 'skiplock/job'
8
8
  require 'skiplock/manager'
9
+ require 'skiplock/patch'
9
10
  require 'skiplock/worker'
10
11
  require 'skiplock/version'
11
12
 
12
13
  module Skiplock
13
- DEFAULT_CONFIG = { 'extensions' => false, 'logfile' => 'skiplock.log', 'loglevel' => 'info', 'graceful_shutdown' => 15, 'min_threads' => 1, 'max_threads' => 5, 'max_retries' => 20, 'notification' => 'custom', 'purge_completion' => true, 'queues' => { 'default' => 100, 'mailers' => 999 }, 'workers' => 0 }.freeze
14
+ DEFAULT_CONFIG = { 'extensions' => false, 'logfile' => 'skiplock.log', 'loglevel' => 'info', 'graceful_shutdown' => 15, 'min_threads' => 1, 'max_threads' => 10, 'max_retries' => 20, 'notification' => 'custom', 'purge_completion' => true, 'queues' => { 'default' => 100, 'mailers' => 999 }, 'workers' => 0 }.freeze
14
15
 
15
16
  def self.logger=(l)
16
17
  @logger = l
@@ -33,4 +34,5 @@ module Skiplock
33
34
  def self.table_name_prefix
34
35
  'skiplock.'
35
36
  end
36
- end
37
+ end
38
+ ActiveJob::Base.__send__(:include, Skiplock::Patch)
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.16
4
+ version: 1.0.17
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-09-10 00:00:00.000000000 Z
11
+ date: 2021-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -87,6 +87,7 @@ files:
87
87
  - lib/skiplock/extension.rb
88
88
  - lib/skiplock/job.rb
89
89
  - lib/skiplock/manager.rb
90
+ - lib/skiplock/patch.rb
90
91
  - lib/skiplock/version.rb
91
92
  - lib/skiplock/worker.rb
92
93
  homepage: https://github.com/vtt/skiplock