crono_trigger 0.4.0 → 0.5.0

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: 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