crono_trigger 0.4.0 → 0.5.0

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: e3d3157a417d9c99b0da61ea75d9c9efc0af12a1f1f15cc3af121b4b3d673757
4
- data.tar.gz: 52c3683c6e3116cfa34b41e83a6b830f63b0f65e4cf49377cc87548b4dc4c5f4
3
+ metadata.gz: 7a1befee8d18a2c52458baa240cc2b12a8d9fea015a53647c0f098cae21d062d
4
+ data.tar.gz: c6fad9a37fdf912c88712c93b64ace75386b9c2f37cdc9bcf609de711a7b8939
5
5
  SHA512:
6
- metadata.gz: 63f1eee5323fd5c1fff137c4d4204b06b9aca648d97d82cc3168fecda11d95d65d771a4b063615dbe4e5ac7b5767e888b02b4f3cbbafd09c912a122601060391
7
- data.tar.gz: b30388357a341e278474d410b5a907e690ac1c97986b4e779fda3a4430d73b76c1aee8ea505cd5852223a63e9983b81b50b2a8a80d97cb9b44825b0071afeb2b
6
+ metadata.gz: 7d383986c2c68d31dbe6fa681599816e564c07dfa3bba92c4edf9cb8b581b934293636446d97a85c2fec87f64af9cd2c81b7ec6edcfc89fe9c905dac6ce1eaf3
7
+ data.tar.gz: 4940e47e60802b4cf2caab3eee22069ba61e1f81b0178b1f696c485c330b5b5340d9d058764f70e40a608839cb2d35354ea877e3bbe32c9377d61fbbaa71233c
@@ -2,8 +2,8 @@ sudo: false
2
2
  language: ruby
3
3
  cache: bundler
4
4
  rvm:
5
- - 2.5.1
6
- - 2.4.3
5
+ - 2.6.1
6
+ - 2.5.2
7
7
  gemfile:
8
8
  - gemfiles/activerecord-51.gemfile
9
9
  - gemfiles/activerecord-52.gemfile
@@ -12,3 +12,5 @@ before_install:
12
12
  env:
13
13
  - DB=sqlite
14
14
  - DB=mysql MYSQL_RESTART_COMMAND="sudo service mysql restart"
15
+ - NO_TIMESTAMP=false
16
+ - NO_TIMESTAMP=true
data/README.md CHANGED
@@ -70,7 +70,7 @@ class CreateMailNotifications < ActiveRecord::Migration
70
70
  t.datetime :next_execute_at
71
71
  t.datetime :last_executed_at
72
72
  t.integer :execute_lock, limit: 8, default: 0, null: false
73
- t.datetime :started_at, null: false
73
+ t.datetime :started_at
74
74
  t.datetime :finished_at
75
75
  t.string :last_error_name
76
76
  t.string :last_error_reason
@@ -118,7 +118,7 @@ class MailNotification < ActiveRecord::Base
118
118
  end
119
119
 
120
120
  # one time schedule
121
- MailNotification.create(next_execute_at: Time.current.since(5.minutes))
121
+ MailNotification.create.activate_schedule!(at: Time.current.since(5.minutes))
122
122
 
123
123
  # cron schedule
124
124
  MailNotification.create(cron: "0 12 * * *").activate_schedule!
data/Rakefile CHANGED
@@ -10,11 +10,13 @@ pwd = File.expand_path('../', __FILE__)
10
10
  gemfiles = Dir.glob(File.join(pwd, "gemfiles", "*.gemfile")).map { |f| File.basename(f, ".*") }
11
11
 
12
12
  namespace :js do
13
+ desc "Cleanup built javascripts"
13
14
  task :clean do
14
15
  rm_r(File.join(pwd, "web", "app", "build")) if File.exist?(File.join(pwd, "web", "app", "build"))
15
16
  rm_r(File.join(pwd, "web", "public"))
16
17
  end
17
18
 
19
+ desc "build javascripts"
18
20
  task build: [:clean] do
19
21
  Dir.chdir(File.join(pwd, "web", "app"))
20
22
  sh({"PUBLIC_URL" => "<%= URI.parse(url('/')).path.chop %>"}, "npm run build") do |ok, res|
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
31
31
  spec.add_dependency "oj"
32
32
  spec.add_dependency "activerecord", ">= 4.2"
33
33
 
34
- spec.add_development_dependency "sqlite3"
34
+ spec.add_development_dependency "sqlite3", "~> 1.3.6"
35
35
  spec.add_development_dependency "mysql2"
36
36
  spec.add_development_dependency "timecop"
37
37
  spec.add_development_dependency "rollbar"
@@ -6,6 +6,7 @@ require "active_record"
6
6
  require "concurrent"
7
7
  require "crono_trigger/models/worker"
8
8
  require "crono_trigger/models/signal"
9
+ require "crono_trigger/models/execution"
9
10
  require "crono_trigger/worker"
10
11
  require "crono_trigger/polling_thread"
11
12
  require "crono_trigger/schedulable"
@@ -0,0 +1,22 @@
1
+ module CronoTrigger
2
+ class ExecutionTracker
3
+ def initialize(schedulable)
4
+ @schedulable = schedulable
5
+ end
6
+
7
+ def track(&pr)
8
+ if @schedulable.track_execution
9
+ begin
10
+ execution = @schedulable.crono_trigger_executions.create_with_timestamp!
11
+ pr.call
12
+ execution.complete!
13
+ rescue => e
14
+ execution.error!(e)
15
+ raise
16
+ end
17
+ else
18
+ pr.call
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ module CronoTrigger
2
+ module Models
3
+ class Execution < ActiveRecord::Base
4
+ self.table_name = "crono_trigger_executions"
5
+
6
+ belongs_to :schedule, polymorphic: true, inverse_of: :crono_trigger_executions
7
+
8
+ scope :recently, ->(schedule_type:) { where(schedule_type: schedule_type).order(executed_at: :desc) }
9
+
10
+ enum status: {
11
+ executing: "executing",
12
+ completed: "completed",
13
+ failed: "failed",
14
+ }
15
+
16
+ def self.create_with_timestamp!
17
+ create!(executed_at: Time.current, status: :executing, worker_id: CronoTrigger.config.worker_id)
18
+ end
19
+
20
+ def complete!
21
+ update!(status: :completed, completed_at: Time.current)
22
+ end
23
+
24
+ def error!(exception)
25
+ update!(status: :failed, completed_at: Time.current, error_name: exception.class.to_s, error_reason: exception.message)
26
+ end
27
+
28
+ def retry!
29
+ return false if schedule.locking?
30
+
31
+ schedule.retry!
32
+ end
33
+ end
34
+ end
35
+ end
@@ -85,7 +85,9 @@ module CronoTrigger
85
85
 
86
86
  private def unlock_overflowed_records(model, overflowed_record_ids)
87
87
  model.connection_pool.with_connection do
88
- model.where(id: overflowed_record_ids).crono_trigger_unlock_all!
88
+ unless overflowed_record_ids.empty?
89
+ model.where(id: overflowed_record_ids).crono_trigger_unlock_all!
90
+ end
89
91
  end
90
92
  rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::LockWaitTimeout, ActiveRecord::StatementTimeout, ActiveRecord::Deadlocked
91
93
  sleep 1
@@ -4,6 +4,7 @@ require "chrono"
4
4
  require "tzinfo"
5
5
 
6
6
  require "crono_trigger/exception_handler"
7
+ require "crono_trigger/execution_tracker"
7
8
 
8
9
  module CronoTrigger
9
10
  module Schedulable
@@ -26,9 +27,12 @@ module CronoTrigger
26
27
 
27
28
  included do
28
29
  CronoTrigger::Schedulable.included_by << self
29
- class_attribute :crono_trigger_options, :executable_conditions
30
+ class_attribute :crono_trigger_options, :executable_conditions, :track_execution
30
31
  self.crono_trigger_options ||= {}
31
32
  self.executable_conditions ||= []
33
+ self.track_execution ||= false
34
+
35
+ has_many :crono_trigger_executions, class_name: "CronoTrigger::Models::Execution", as: :schedule, inverse_of: :schedule
32
36
 
33
37
  define_model_callbacks :execute, :retry
34
38
 
@@ -36,7 +40,7 @@ module CronoTrigger
36
40
  t = arel_table
37
41
 
38
42
  rel = where(t[crono_trigger_column_name(:next_execute_at)].lteq(from))
39
- rel = rel.where(t[crono_trigger_column_name(:execute_lock)].lteq(from.to_i - execute_lock_timeout)) unless including_locked
43
+ rel = rel.where(t[crono_trigger_column_name(:execute_lock)].lt(from.to_i - execute_lock_timeout)) unless including_locked
40
44
 
41
45
  rel = rel.where(t[crono_trigger_column_name(:started_at)].lteq(from)) if column_names.include?(crono_trigger_column_name(:started_at))
42
46
  rel = rel.where(t[crono_trigger_column_name(:finished_at)].gt(from).or(t[crono_trigger_column_name(:finished_at)].eq(nil))) if column_names.include?(crono_trigger_column_name(:finished_at))
@@ -61,17 +65,18 @@ module CronoTrigger
61
65
 
62
66
  module ClassMethods
63
67
  def executables_with_lock(limit: CronoTrigger.config.executor_thread * 3 || 100)
64
- records = nil
65
- transaction do
66
- records = executables(limit: limit).lock.to_a
67
- unless records.empty?
68
- where(id: records).update_all(
69
- crono_trigger_column_name(:execute_lock) => Time.current.to_i,
70
- crono_trigger_column_name(:locked_by) => CronoTrigger.config.worker_id
71
- )
68
+ ids = executables(limit: limit).pluck(:id)
69
+ records = []
70
+ ids.each do |id|
71
+ transaction do
72
+ r = all.lock.find(id)
73
+ unless r.locking?
74
+ r.crono_trigger_lock!
75
+ records << r
76
+ end
72
77
  end
73
- records
74
78
  end
79
+ records
75
80
  end
76
81
 
77
82
  def crono_trigger_column_name(name)
@@ -106,12 +111,15 @@ module CronoTrigger
106
111
  end
107
112
 
108
113
  def do_execute
114
+ execution_tracker = ExecutionTracker.new(self)
109
115
  run_callbacks :execute do
110
116
  catch(:ok_without_reset) do
111
117
  catch(:ok) do
112
118
  catch(:retry) do
113
119
  catch(:abort) do
114
- execute
120
+ execution_tracker.track do
121
+ execute
122
+ end
115
123
  throw :ok
116
124
  end
117
125
  raise AbortExecution
@@ -151,16 +159,23 @@ module CronoTrigger
151
159
  if new_record?
152
160
  self.attributes = attributes
153
161
  else
162
+ merge_updated_at_for_crono_trigger!(attributes)
154
163
  update_columns(attributes)
155
164
  end
165
+
166
+ self
156
167
  end
157
168
 
158
- def retry!
169
+ def retry!(immediately: false)
159
170
  run_callbacks :retry do
160
171
  logger.info "Retry #{self.class}-#{id}" if logger
161
172
 
162
173
  now = Time.current
163
- wait = crono_trigger_options[:exponential_backoff] ? retry_interval * [2 * (retry_count - 1), 1].max : retry_interval
174
+ if immediately
175
+ wait = 0
176
+ else
177
+ wait = crono_trigger_options[:exponential_backoff] ? retry_interval * [2 * (retry_count - 1), 1].max : retry_interval
178
+ end
164
179
  attributes = {
165
180
  crono_trigger_column_name(:next_execute_at) => now + wait,
166
181
  crono_trigger_column_name(:execute_lock) => 0,
@@ -171,6 +186,7 @@ module CronoTrigger
171
186
  attributes.merge!(retry_count: retry_count.to_i + 1)
172
187
  end
173
188
 
189
+ merge_updated_at_for_crono_trigger!(attributes, now)
174
190
  update_columns(attributes)
175
191
  end
176
192
  end
@@ -184,22 +200,36 @@ module CronoTrigger
184
200
  crono_trigger_column_name(:locked_by) => nil,
185
201
  }
186
202
 
203
+ now = Time.current
204
+
187
205
  if update_last_executed_at && self.class.column_names.include?(crono_trigger_column_name(:last_executed_at))
188
- attributes.merge!(crono_trigger_column_name(:last_executed_at) => Time.current)
206
+ attributes.merge!(crono_trigger_column_name(:last_executed_at) => now)
189
207
  end
190
208
 
191
209
  if self.class.column_names.include?("retry_count")
192
210
  attributes.merge!(retry_count: 0)
193
211
  end
194
212
 
213
+ merge_updated_at_for_crono_trigger!(attributes, now)
214
+ update_columns(attributes)
215
+ end
216
+
217
+ def crono_trigger_lock!
218
+ attributes = {
219
+ crono_trigger_column_name(:execute_lock) => Time.current.to_i,
220
+ crono_trigger_column_name(:locked_by) => CronoTrigger.config.worker_id
221
+ }
222
+ merge_updated_at_for_crono_trigger!(attributes)
195
223
  update_columns(attributes)
196
224
  end
197
225
 
198
226
  def crono_trigger_unlock!
199
- update_columns(
227
+ attributes = {
200
228
  crono_trigger_column_name(:execute_lock) => 0,
201
229
  crono_trigger_column_name(:locked_by) => nil,
202
- )
230
+ }
231
+ merge_updated_at_for_crono_trigger!(attributes)
232
+ update_columns(attributes)
203
233
  end
204
234
 
205
235
  def crono_trigger_status
@@ -223,7 +253,7 @@ module CronoTrigger
223
253
 
224
254
  def locking?(at: Time.now)
225
255
  self[crono_trigger_column_name(:execute_lock)] > 0 &&
226
- self[crono_trigger_column_name(:execute_lock)] > at.to_i - self.class.execute_lock_timeout
256
+ self[crono_trigger_column_name(:execute_lock)] >= at.to_f - self.class.execute_lock_timeout
227
257
  end
228
258
 
229
259
  def assume_executing?
@@ -298,7 +328,14 @@ module CronoTrigger
298
328
  attributes.merge!(last_error_time: now)
299
329
  end
300
330
 
331
+ merge_updated_at_for_crono_trigger!(attributes)
301
332
  update_columns(attributes) unless attributes.empty?
302
333
  end
334
+
335
+ def merge_updated_at_for_crono_trigger!(attributes, time = Time.current)
336
+ if self.class.column_names.include?("updated_at")
337
+ attributes.merge!("updated_at" => time)
338
+ end
339
+ end
303
340
  end
304
341
  end
@@ -1,3 +1,3 @@
1
1
  module CronoTrigger
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -1,6 +1,7 @@
1
1
  require "crono_trigger"
2
2
  require "sinatra/base"
3
3
  require "rack/contrib/post_body_content_type_parser"
4
+ require "oj"
4
5
 
5
6
  module CronoTrigger
6
7
  class Web < Sinatra::Application
@@ -87,6 +88,13 @@ module CronoTrigger
87
88
  end
88
89
  end
89
90
 
91
+
92
+ post "/models/executions/:id/retry" do
93
+ CronoTrigger::Models::Execution.find(params[:id]).retry!
94
+ status 200
95
+ body ""
96
+ end
97
+
90
98
  post "/models/:name/:id/retry" do
91
99
  model_class = CronoTrigger::Schedulable.included_by.find { |c| c.name == params[:name] }
92
100
  if model_class
@@ -110,22 +118,22 @@ module CronoTrigger
110
118
  now = Time.now
111
119
  records = @scheduled_records.map do |r|
112
120
  {
113
- "crono_trigger_status" => r.crono_trigger_status,
114
- "id" => r.id,
115
- "cron" => r[r.crono_trigger_column_name(:cron)],
116
- "next_execute_at" => r[r.crono_trigger_column_name(:next_execute_at)],
117
- "last_executed_at" => r[r.crono_trigger_column_name(:last_executed_at)],
118
- "timezone" => r[r.crono_trigger_column_name(:timezone)],
119
- "execute_lock" => r[r.crono_trigger_column_name(:execute_lock)],
120
- "locked_by" => r[r.crono_trigger_column_name(:locked_by)],
121
- "started_at" => r[r.crono_trigger_column_name(:started_at)],
122
- "finished_at" => r[r.crono_trigger_column_name(:finished_at)],
123
- "last_error_name" => r[r.crono_trigger_column_name(:last_error_name)],
124
- "last_error_reason" => r[r.crono_trigger_column_name(:last_error_reason)],
125
- "last_error_time" => r[r.crono_trigger_column_name(:last_error_time)],
126
- "retry_count" => r[r.crono_trigger_column_name(:retry_count)],
127
- "time_to_unlock" => [(r.class.execute_lock_timeout + r[r.crono_trigger_column_name(:execute_lock)]) - now.to_i, 0].max,
128
- "delay_sec" => r.locking?(at: now) ? 0 : (now - r[r.crono_trigger_column_name(:next_execute_at)]).to_i,
121
+ -"crono_trigger_status" => r.crono_trigger_status,
122
+ -"id" => r.id,
123
+ -"cron" => r[r.crono_trigger_column_name(:cron)],
124
+ -"next_execute_at" => r[r.crono_trigger_column_name(:next_execute_at)],
125
+ -"last_executed_at" => r[r.crono_trigger_column_name(:last_executed_at)],
126
+ -"timezone" => r[r.crono_trigger_column_name(:timezone)],
127
+ -"execute_lock" => r[r.crono_trigger_column_name(:execute_lock)],
128
+ -"locked_by" => r[r.crono_trigger_column_name(:locked_by)],
129
+ -"started_at" => r[r.crono_trigger_column_name(:started_at)],
130
+ -"finished_at" => r[r.crono_trigger_column_name(:finished_at)],
131
+ -"last_error_name" => r[r.crono_trigger_column_name(:last_error_name)],
132
+ -"last_error_reason" => r[r.crono_trigger_column_name(:last_error_reason)],
133
+ -"last_error_time" => r[r.crono_trigger_column_name(:last_error_time)],
134
+ -"retry_count" => r[r.crono_trigger_column_name(:retry_count)],
135
+ -"time_to_unlock" => [(r.class.execute_lock_timeout + r[r.crono_trigger_column_name(:execute_lock)]) - now.to_i, 0].max,
136
+ -"delay_sec" => r.locking?(at: now) ? 0 : (now - r[r.crono_trigger_column_name(:next_execute_at)]).to_i,
129
137
  }
130
138
  end
131
139
  Oj.dump({
@@ -159,5 +167,38 @@ module CronoTrigger
159
167
  get "/models" do
160
168
  erb :index
161
169
  end
170
+
171
+ get "/models/:name/executions.:format" do
172
+ if params[:format] == "json"
173
+ model_class = CronoTrigger::Schedulable.included_by.find { |c| c.name == params[:name] }
174
+ if model_class
175
+ rel = CronoTrigger::Models::Execution.recently(schedule_type: model_class)
176
+ rel.where!("executed_at >= ?", Time.parse(params[:from])) if params[:from]
177
+ rel.where!("executed_at <= ?", Time.parse(params[:to])) if params[:to]
178
+ rel = rel.limit(params[:limit] || 100)
179
+ records = rel.map do |r|
180
+ {
181
+ -"id" => r.id,
182
+ -"schedule_id" => r.schedule_id,
183
+ -"schedule_type" => r.schedule_type,
184
+ -"worker_id" => r.worker_id,
185
+ -"executed_at" => r.executed_at,
186
+ -"completed_at" => r.completed_at,
187
+ -"status" => r.status,
188
+ -"error_name" => r.error_name,
189
+ -"error_reason" => r.error_reason,
190
+ }
191
+ end
192
+ Oj.dump({
193
+ records: records,
194
+ }, mode: :compat)
195
+ else
196
+ status 404
197
+ "Model Class is not found"
198
+ end
199
+ else
200
+ raise "unknown format"
201
+ end
202
+ end
162
203
  end
163
204
  end
@@ -19,5 +19,20 @@ class CreateCronoTriggerSystemTables < ActiveRecord::Migration<%= Rails::VERSION
19
19
  end
20
20
 
21
21
  add_index :crono_trigger_signals, [:sent_at, :worker_id]
22
+
23
+ create_table :crono_trigger_executions do |t|
24
+ t.integer :schedule_id, null: false
25
+ t.string :schedule_type, null: false
26
+ t.string :worker_id, null: false
27
+ t.datetime :executed_at, null: false
28
+ t.datetime :completed_at
29
+ t.string :status, null: false, default: "executing"
30
+ t.string :error_name
31
+ t.string :error_reason
32
+ end
33
+
34
+ add_index :crono_trigger_executions, [:schedule_type, :schedule_id, :executed_at], name: "index_crono_trigger_executions_on_schtype_schid_executed_at"
35
+ add_index :crono_trigger_executions, [:schedule_type, :executed_at]
36
+ add_index :crono_trigger_executions, [:executed_at]
22
37
  end
23
38
  end