crono_trigger 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f8410e0b2dd3dc398df839baba03aaec050d3e31
4
+ data.tar.gz: 945ece057b38511d03324505c4fe960420b9f581
5
+ SHA512:
6
+ metadata.gz: 76e9b4270e9d9ea439106e88e5e031a2489212f4caaf8dedd0e39c0ba3d64189b0a3c7ecc6e67d9dfbdaffcfcfb27cbe8839a4860268c00fcede1f8c5a4f5fd5
7
+ data.tar.gz: 5adc3071703de67b4d9a894b32e4ede1677dcc119fdb8fdc021fdae4f232e56d297984363eaf5d4d1eec586226841666bef9ffa6d2713e166ff524fc4c583a74
@@ -0,0 +1,20 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /gemfiles/.bundle
11
+ *.gemfile.lock
12
+
13
+ # rspec failure tracking
14
+ .rspec_status
15
+
16
+ log/*.log
17
+ spec/dummy/db/*.sqlite3
18
+ spec/dummy/db/*.sqlite3-journal
19
+ spec/dummy/log/*.log
20
+ spec/dummy/tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,10 @@
1
+ sudo: false
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.4.1
6
+ - 2.3.3
7
+ gemfile:
8
+ - gemfiles/activerecord-42.gemfile
9
+ - gemfiles/activerecord-50.gemfile
10
+ - gemfiles/activerecord-51.gemfile
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in crono_trigger.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 joker1007
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,146 @@
1
+ # CronoTrigger
2
+
3
+ Asynchronous Job Scheduler for Rails.
4
+
5
+ The purpose of this gem is to integrate job schedule into Service Domain.
6
+
7
+ Because of it, this gem uses ActiveRecord model as definition of job schedule.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'crono_trigger'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install crono_trigger
24
+
25
+ ## Usage
26
+
27
+ #### Execute `crono_trigger:model` generator.
28
+
29
+ ```
30
+ $ rails g crono_trigger:model mail_notification
31
+ create db/migrate/20170619064928_create_mail_notifications.rb
32
+ create app/models/mail_notification.rb
33
+ # ...
34
+ ```
35
+
36
+ #### Migration sample
37
+
38
+ ```ruby
39
+ class CreateMailNotifications < ActiveRecord::Migration
40
+ def change
41
+ create_table :mail_notifications do |t|
42
+
43
+ # columns for CronoTrigger::Schedulable
44
+ t.string :cron
45
+ t.datetime :next_execute_at
46
+ t.datetime :last_executed_at
47
+ t.integer :execute_lock, limit: 8, default: 0, null: false
48
+ t.datetime :started_at, null: false
49
+ t.datetime :finished_at
50
+ t.string :last_error_name
51
+ t.string :last_error_reason
52
+ t.datetime :last_error_time
53
+ t.integer :retry_count, default: 0, null: false
54
+
55
+
56
+ t.timestamps
57
+ end
58
+ add_index :mail_notifications, [:next_execute_at, :execute_lock, :started_at, :finished_at], name: "crono_trigger_index_on_mail_notifications"
59
+ end
60
+ end
61
+ ```
62
+
63
+ #### Implement `#execute` method
64
+
65
+ ```ruby
66
+ class MailNotification < ActiveRecord::Base
67
+ include CronoTrigger::Schedulable
68
+
69
+ self.crono_trigger_options = {
70
+ retry_limit: 5,
71
+ retry_interval: 10,
72
+ exponential_backoff: true,
73
+ execute_lock_timeout: 300,
74
+ }
75
+
76
+ # `execute` callback is defined
77
+ # can use `before_execute`, `after_execute`, `around_execute`
78
+
79
+ # If execute method raise Exception, worker retry task until reach `retry_limit`
80
+ # If `retry_count` reaches `retry_limit`, task schedule is reset.
81
+ #
82
+ # If record has cron value, reset process set next execution time by cron definition
83
+ # If record has no cron value, reset process clear next execution time
84
+ def execute
85
+ send_mail
86
+
87
+ throw :retry # break execution and retry task
88
+ throw :abort # break execution and raise AbortExecution. AbortExecution is not retried
89
+ throw :ok # break execution and handle task as success
90
+ end
91
+ end
92
+ ```
93
+
94
+ #### Run Worker
95
+
96
+ ```
97
+ $ crono_trigger MailNotification
98
+ ```
99
+
100
+ ```
101
+ $ crono_trigger --help
102
+ Usage: crono_trigger [options] MODEL [MODEL..]
103
+ -f, --config-file=CONFIG Config file (ex. ./crono_trigger.rb)
104
+ -e, --envornment=ENV Set environment name (ex. development, production)
105
+ -p, --polling-thread=SIZE Polling thread size (Default: 1)
106
+ -i, --polling-interval=SECOND Polling interval seconds (Default: 5)
107
+ -c, --concurrency=SIZE Execute thread size (Default: 25)
108
+ -l, --log=LOGFILE Set log output destination (Default: STDOUT or ./crono_trigger.log if daemonize is true)
109
+ --log-level=LEVEL Set log level (Default: info)
110
+ -d, --daemonize Daemon mode
111
+ --pid=PIDFILE Set pid file
112
+ -h, --help Prints this help
113
+ ```
114
+
115
+ ## Specification
116
+
117
+ ### Columns
118
+
119
+ |name |type |required|description |
120
+ |-----------------|--------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
121
+ |cron |string |no |Recurring schedule formatted by cron style |
122
+ |next_execute_at |datetime|yes |Timestamp of next execution. Worker executes task if this column <= now |
123
+ |last_executed_at |datetime|no |Timestamp of last execution |
124
+ |execute_lock |integer |yes |Timestamp of fetching record in order to hide record from other transaction during execute lock timeout. <br> when execution complete this column is reset to 0|
125
+ |started_at |datetime|no |Timestamp of schedule activated |
126
+ |finished_at |datetime|no |Timestamp of schedule deactivated |
127
+ |last_error_name |string |no |Class name of last error |
128
+ |last_error_reason|string |no |Error message of last error |
129
+ |last_error_time |datetime|no |Timestamp of last error occured |
130
+ |retry_count |integer |no |Retry count. <br> If execution succeed retry_count is reset to 0 |
131
+
132
+ ## Development
133
+
134
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
135
+
136
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
137
+
138
+ ## Contributing
139
+
140
+ Bug reports and pull requests are welcome on GitHub at https://github.com/joker1007/crono_trigger.
141
+
142
+
143
+ ## License
144
+
145
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
146
+
@@ -0,0 +1,47 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
8
+ pwd = File.expand_path('../', __FILE__)
9
+
10
+ gemfiles = Dir.glob(File.join(pwd, "gemfiles", "*.gemfile")).map { |f| File.basename(f, ".*") }
11
+
12
+ namespace :spec do
13
+ gemfiles.each do |gemfile|
14
+ desc "Run Tests by #{gemfile}.gemfile"
15
+ task gemfile do
16
+ Bundler.with_clean_env do
17
+ sh "BUNDLE_GEMFILE='#{pwd}/gemfiles/#{gemfile}.gemfile' bundle install --path #{pwd}/.bundle"
18
+ sh "BUNDLE_GEMFILE='#{pwd}/gemfiles/#{gemfile}.gemfile' bundle exec rake -t spec"
19
+ end
20
+ end
21
+ end
22
+
23
+ desc "Run All Tests"
24
+ task :all do
25
+ gemfiles.each do |gemfile|
26
+ Rake::Task["spec:#{gemfile}"].invoke
27
+ end
28
+ end
29
+ end
30
+
31
+ namespace :bundle_update do
32
+ gemfiles.each do |gemfile|
33
+ desc "Run Tests by #{gemfile}.gemfile"
34
+ task gemfile do
35
+ Bundler.with_clean_env do
36
+ sh "BUNDLE_GEMFILE='#{pwd}/gemfiles/#{gemfile}.gemfile' bundle update"
37
+ end
38
+ end
39
+ end
40
+
41
+ desc "Run All Tests"
42
+ task :all do
43
+ gemfiles.each do |gemfile|
44
+ Rake::Task["bundle_update:#{gemfile}"].invoke
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "crono_trigger"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'crono_trigger/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "crono_trigger"
8
+ spec.version = CronoTrigger::VERSION
9
+ spec.authors = ["joker1007"]
10
+ spec.email = ["kakyoin.hierophant@gmail.com"]
11
+
12
+ spec.summary = %q{In Service Asynchronous Job Scheduler for Rails}
13
+ spec.description = %q{In Service Asynchronous Job Scheduler for Rails. This gem handles ActiveRecord model as schedule definition.}
14
+ spec.homepage = "https://github.com/joker1007/crono_trigger"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+
25
+ spec.add_dependency "chrono"
26
+ spec.add_dependency "serverengine"
27
+ spec.add_dependency "concurrent-ruby"
28
+ spec.add_dependency "activerecord", ">= 4.2"
29
+
30
+ spec.add_development_dependency "sqlite3"
31
+ spec.add_development_dependency "database_rewinder"
32
+ spec.add_development_dependency "timecop"
33
+ spec.add_development_dependency "bundler", "~> 1.14"
34
+ spec.add_development_dependency "rake", "~> 10.0"
35
+ spec.add_development_dependency "rspec", "~> 3.0"
36
+ end
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "crono_trigger/cli"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 4.2.0"
4
+
5
+ gemspec :path => "../"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 5.0.0"
4
+
5
+ gemspec :path => "../"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 5.1.0"
4
+
5
+ gemspec :path => "../"
@@ -0,0 +1,36 @@
1
+ require "crono_trigger/version"
2
+
3
+ require "ostruct"
4
+ require "active_record"
5
+ require "concurrent"
6
+ require "crono_trigger/worker"
7
+ require "crono_trigger/polling_thread"
8
+ require "crono_trigger/schedulable"
9
+
10
+ module CronoTrigger
11
+ @config = OpenStruct.new(
12
+ polling_thread: 1,
13
+ polling_interval: 5,
14
+ executor_thread: 25,
15
+ model_names: [],
16
+ )
17
+
18
+ def self.config
19
+ @config
20
+ end
21
+
22
+ def self.configure
23
+ yield config
24
+ end
25
+
26
+ def self.load_config(yml, environment = nil)
27
+ config = YAML.load_file(yml)[environment || "default"]
28
+ config.each do |k, v|
29
+ @config[k] = v
30
+ end
31
+ end
32
+ end
33
+
34
+ if defined?(Rails)
35
+ require "crono_trigger/railtie"
36
+ end
@@ -0,0 +1,80 @@
1
+ require "optparse"
2
+ require "crono_trigger"
3
+ require "serverengine"
4
+
5
+ options = {
6
+ daemonize: false,
7
+ pid_path: "./crono_trigger.pid",
8
+ }
9
+
10
+ opt_parser = OptionParser.new do |opts|
11
+ opts.banner = "Usage: crono_trigger [options] MODEL [MODEL..]"
12
+
13
+ opts.on("-f", "--config-file=CONFIG", "Config file (ex. ./crono_trigger.rb)") do |cfg|
14
+ options[:config] = cfg
15
+ end
16
+
17
+ opts.on("-e", "--envornment=ENV", "Set environment name (ex. development, production)") do |env|
18
+ options[:env] = env
19
+ end
20
+
21
+ opts.on("-p", "--polling-thread=SIZE", Integer, "Polling thread size (Default: 1)") do |i|
22
+ options[:polling_thread] = i
23
+ end
24
+
25
+ opts.on("-i", "--polling-interval=SECOND", Integer, "Polling interval seconds (Default: 5)") do |i|
26
+ options[:polling_interval] = i
27
+ end
28
+
29
+ opts.on("-c", "--concurrency=SIZE", Integer, "Execute thread size (Default: 25)") do |i|
30
+ options[:execute_thread] = i
31
+ end
32
+
33
+ opts.on("-l", "--log=LOGFILE", "Set log output destination (Default: STDOUT or ./crono_trigger.log if daemonize is true)") do |log|
34
+ options[:log] = log
35
+ end
36
+
37
+ opts.on("--log-level=LEVEL", "Set log level (Default: info)") do |log_level|
38
+ options[:log_level] = log_level
39
+ end
40
+
41
+ opts.on("-d", "--daemonize", "Daemon mode") do
42
+ options[:daemonize] = true
43
+ end
44
+
45
+ opts.on("--pid=PIDFILE", "Set pid file") do |pid|
46
+ options[:pid_path] = pid
47
+ end
48
+
49
+ opts.on("-h", "--help", "Prints this help") do
50
+ puts opts
51
+ exit
52
+ end
53
+ end
54
+
55
+ opt_parser.parse!
56
+
57
+ begin
58
+ require "rails"
59
+ require File.expand_path("./config/environment", Rails.root)
60
+ rescue LoadError
61
+ end
62
+
63
+ CronoTrigger.load_config(options[:config], options[:env]) if options[:config]
64
+
65
+ %i(polling_thread polling_interval execute_thread).each do |name|
66
+ CronoTrigger.config[name] = options[name] if options[name]
67
+ end
68
+
69
+ CronoTrigger.config.model_names.concat(ARGV)
70
+
71
+ se = ServerEngine.create(nil, CronoTrigger::Worker, {
72
+ daemonize: options[:daemonize],
73
+ log: options[:log] || (options[:daemonize] ? "./crono_trigger.log" : "-"),
74
+ log_level: options[:log_level] || "info",
75
+ pid_path: options[:pid_path] || (options[:daemonize] ? "./crono_trigger.pid" : nil),
76
+ supervisor: true,
77
+ server_process_name: "crono_trigger[worker]",
78
+ restart_server_process: true,
79
+ })
80
+ se.run
@@ -0,0 +1,60 @@
1
+ module CronoTrigger
2
+ class PollingThread
3
+ def initialize(model_queue, stop_flag, logger, executor)
4
+ @model_queue = model_queue
5
+ @stop_flag = stop_flag
6
+ @logger = logger
7
+ @executor = executor
8
+ end
9
+
10
+ def run
11
+ @thread = Thread.start do
12
+ @logger.info "(polling-thread-#{Thread.current.object_id}) Start polling thread"
13
+ until @stop_flag.wait_for_set(CronoTrigger.config.polling_interval)
14
+ begin
15
+ model = @model_queue.pop(true)
16
+ poll(model)
17
+ rescue ThreadError => e
18
+ logger.error(e) unless e.message == "queue empty"
19
+ rescue => e
20
+ logger.error(e)
21
+ ensure
22
+ @model_queue << model if model
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def join
29
+ @thread.join
30
+ end
31
+
32
+ def poll(model)
33
+ @logger.debug "(polling-thread-#{Thread.current.object_id}) Poll #{model}"
34
+ records = []
35
+ primary_key_offset = nil
36
+ begin
37
+ begin
38
+ conn = model.connection_pool.checkout
39
+ records = model.executables_with_lock(primary_key_offset: primary_key_offset)
40
+ primary_key_offset = records.last && records.last.id
41
+ ensure
42
+ model.connection_pool.checkin(conn)
43
+ end
44
+
45
+ records.each do |record|
46
+ @executor.post do
47
+ model.connection_pool.with_connection do
48
+ @logger.info "(executor-thread-#{Thread.current.object_id}) Execute #{record.class}-#{record.id}"
49
+ begin
50
+ record.do_execute
51
+ rescue Exception => e
52
+ @logger.error(e)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end while records.any?
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,4 @@
1
+ module CronoTrigger
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ end
@@ -0,0 +1,158 @@
1
+ require "active_support/concern"
2
+ require "chrono"
3
+
4
+ module CronoTrigger
5
+ module Schedulable
6
+ DEFAULT_RETRY_LIMIT = 10
7
+ DEFAULT_RETRY_INTERVAL = 4
8
+ DEFAULT_EXECUTE_LOCK_TIMEOUT = 600
9
+
10
+ class AbortExecution < StandardError; end
11
+
12
+ extend ActiveSupport::Concern
13
+ include ActiveSupport::Callbacks
14
+
15
+ included do
16
+ class_attribute :crono_trigger_options
17
+ self.crono_trigger_options = {}
18
+
19
+ define_model_callbacks :execute
20
+
21
+ scope :executables, ->(from: Time.current, primary_key_offset: nil, limit: 1000) do
22
+ t = arel_table
23
+
24
+ rel = where(t[:next_execute_at].lteq(from))
25
+ .where(t[:execute_lock].lteq(from.to_i - (crono_trigger_options[:execute_lock_timeout] || DEFAULT_EXECUTE_LOCK_TIMEOUT)))
26
+
27
+ rel = rel.where(t[:started_at].lteq(from)) if column_names.include?("started_at")
28
+ rel = rel.where(t[:finished_at].gt(from).or(t[:finished_at].eq(nil))) if column_names.include?("finished_at")
29
+ rel = rel.where(t[primary_key].gt(primary_key_offset)) if primary_key_offset
30
+
31
+ rel = rel.order("#{quoted_table_name}.#{quoted_primary_key} ASC").limit(limit)
32
+
33
+ rel
34
+ end
35
+
36
+ before_create :ensure_next_execute_at
37
+ end
38
+
39
+ module ClassMethods
40
+ def executables_with_lock(primary_key_offset: nil, limit: 1000)
41
+ records = nil
42
+ transaction do
43
+ records = executables(primary_key_offset: primary_key_offset, limit: limit).lock.to_a
44
+ unless records.empty?
45
+ where(id: records).update_all(execute_lock: Time.current.to_i)
46
+ end
47
+ records
48
+ end
49
+ end
50
+ end
51
+
52
+ def do_execute
53
+ run_callbacks :execute do
54
+ catch(:ok) do
55
+ catch(:retry) do
56
+ catch(:abort) do
57
+ execute
58
+ throw :ok
59
+ end
60
+ raise AbortExecution
61
+ end
62
+ retry!
63
+ return
64
+ end
65
+ reset!(true)
66
+ end
67
+ rescue AbortExecution => ex
68
+ save_last_error_info(ex)
69
+ reset!
70
+
71
+ raise
72
+ rescue Exception => ex
73
+ save_last_error_info(ex)
74
+ retry_or_reset!
75
+
76
+ raise
77
+ end
78
+
79
+ def retry!
80
+ logger.info "Retry #{self.class}-#{id}" if logger
81
+
82
+ now = Time.current
83
+ wait = crono_trigger_options[:exponential_backoff] ? retry_interval * [2 * (retry_count - 1), 1].max : retry_interval
84
+ attributes = {next_execute_at: now + wait, execute_lock: 0}
85
+
86
+ if self.class.column_names.include?("retry_count")
87
+ attributes.merge!(retry_count: retry_count.to_i + 1)
88
+ end
89
+
90
+ update_columns(attributes)
91
+ end
92
+
93
+ def reset!(update_last_executed_at = false)
94
+ logger.info "Reset execution schedule #{self.class}-#{id}" if logger
95
+
96
+ attributes = {next_execute_at: calculate_next_execute_at, execute_lock: 0}
97
+
98
+ if update_last_executed_at && self.class.column_names.include?("last_executed_at")
99
+ attributes.merge!(last_executed_at: Time.current)
100
+ end
101
+
102
+ if self.class.column_names.include?("retry_count")
103
+ attributes.merge!(retry_count: 0)
104
+ end
105
+
106
+ update_columns(attributes)
107
+ end
108
+
109
+ private
110
+
111
+ def retry_or_reset!
112
+ if respond_to?(:retry_count) && retry_count.to_i <= retry_limit
113
+ retry!
114
+ else
115
+ reset!
116
+ end
117
+ end
118
+
119
+ def calculate_next_execute_at
120
+ if respond_to?(:cron) && cron
121
+ it = Chrono::Iterator.new(cron)
122
+ it.next
123
+ end
124
+ end
125
+
126
+ def ensure_next_execute_at
127
+ self.next_execute_at ||= calculate_next_execute_at || Time.current
128
+ end
129
+
130
+ def retry_limit
131
+ crono_trigger_options[:retry_limit] || DEFAULT_RETRY_LIMIT
132
+ end
133
+
134
+ def retry_interval
135
+ crono_trigger_options[:retry_interval] || DEFAULT_RETRY_INTERVAL
136
+ end
137
+
138
+ def save_last_error_info(ex)
139
+ columns = self.class.column_names
140
+ attributes = {}
141
+ now = Time.current
142
+
143
+ if columns.include?("last_error_name")
144
+ attributes.merge!(last_error_name: ex.class.to_s)
145
+ end
146
+
147
+ if columns.include?("last_error_reason")
148
+ attributes.merge!(last_error_reason: ex.message)
149
+ end
150
+
151
+ if columns.include?("last_error_time")
152
+ attributes.merge!(last_error_time: now)
153
+ end
154
+
155
+ update_columns(attributes)
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,3 @@
1
+ module CronoTrigger
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,32 @@
1
+ require "active_support/core_ext/string"
2
+
3
+ module CronoTrigger
4
+ module Worker
5
+ def initialize
6
+ @stop_flag = ServerEngine::BlockingFlag.new
7
+ @model_queue = Queue.new
8
+ CronoTrigger.config.model_names.each do |model_name|
9
+ model = model_name.classify.constantize
10
+ @model_queue << model
11
+ end
12
+ @executor = Concurrent::ThreadPoolExecutor.new(
13
+ min_threads: 1,
14
+ max_threads: CronoTrigger.config.executor_thread,
15
+ )
16
+ ActiveRecord::Base.logger = logger
17
+ end
18
+
19
+ def run
20
+ polling_threads = CronoTrigger.config.polling_thread.times.map { PollingThread.new(@model_queue, @stop_flag, logger, @executor) }
21
+ polling_threads.each(&:run)
22
+ polling_threads.each(&:join)
23
+
24
+ @executor.shutdown
25
+ @executor.wait_for_termination
26
+ end
27
+
28
+ def stop
29
+ @stop_flag.set!
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ require "rails/generators/active_record/migration/migration_generator"
2
+
3
+ module CronoTrigger
4
+ module Generators
5
+ class MigrationGenerator < ActiveRecord::Generators::MigrationGenerator
6
+ source_root File.expand_path("../templates", __FILE__)
7
+
8
+ desc "Create migration for Scheduled Job"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,38 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= Rails::VERSION::MAJOR >= 5 ? "[#{ActiveRecord::Migration.current_version}]" : "" %>
2
+ def change
3
+ create_table :<%= table_name %><%= respond_to?(:primary_key_type) ? primary_key_type : "" %> do |t|
4
+ <% attributes.each do |attribute| -%>
5
+ <% if attribute.password_digest? -%>
6
+ t.string :password_digest<%= attribute.inject_options %>
7
+ <% elsif attribute.respond_to?(:token?) && attribute.token? -%>
8
+ t.string :<%= attribute.name %><%= attribute.inject_options %>
9
+ <% else -%>
10
+ t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
11
+ <% end -%>
12
+ <% end -%>
13
+
14
+ # columns for CronoTrigger::Schedulable
15
+ t.string :cron
16
+ t.datetime :next_execute_at
17
+ t.datetime :last_executed_at
18
+ t.integer :execute_lock, limit: 8, default: 0, null: false
19
+ t.datetime :started_at, null: false
20
+ t.datetime :finished_at
21
+ t.string :last_error_name
22
+ t.string :last_error_reason
23
+ t.datetime :last_error_time
24
+ t.integer :retry_count, default: 0, null: false
25
+
26
+ <% if options[:timestamps] %>
27
+ t.timestamps<%= Rails::VERSION::MAJOR >=5 ? " null: false" : "" %>
28
+ <% end -%>
29
+ end
30
+ <% attributes.select { |attribute| attribute.respond_to?(:token?) && attribute.token? }.each do |attribute| -%>
31
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
32
+ <% end -%>
33
+ <% attributes_with_index.each do |attribute| -%>
34
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
35
+ <% end -%>
36
+ add_index :<%= table_name %>, [:next_execute_at, :execute_lock, :started_at, :finished_at], name: "crono_trigger_index_on_<%= table_name %>"
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= Rails::VERSION::MAJOR >= 5 ? "[#{ActiveRecord::Migration.current_version}]" : "" %>
2
+ def change
3
+ # columns for CronoTrigger::Schedulable
4
+ add_column :<%= table_name %>, :cron, :string
5
+ add_column :<%= table_name %>, :next_execute_at, :datetime
6
+ add_column :<%= table_name %>, :last_executed_at, :datetime
7
+ add_column :<%= table_name %>, :execute_lock, :integer, limit: 8, default: 0, null: false
8
+ add_column :<%= table_name %>, :started_at, :datetime, null: false
9
+ add_column :<%= table_name %>, :finished_at, :datetime
10
+ add_column :<%= table_name %>, :last_error_name, :string
11
+ add_column :<%= table_name %>, :last_error_reason, :string
12
+ add_column :<%= table_name %>, :last_error_time, :string
13
+ add_column :<%= table_name %>, :retry_count, :integer, default: 0, null: false
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ require "rails/generators/active_record/model/model_generator"
2
+
3
+ module CronoTrigger
4
+ module Generators
5
+ class ModelGenerator < ActiveRecord::Generators::ModelGenerator
6
+ source_root File.expand_path("../templates", __FILE__)
7
+
8
+ desc "Create model for Scheduled Job"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %> < <%= parent_class_name.classify %>
3
+ <% attributes.select(&:reference?).each do |attribute| -%>
4
+ belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %>
5
+ <% end -%>
6
+ <% attributes.select { |attribute| attribute.respond_to?(:token?) && attribute.token? }.each do |attribute| -%>
7
+ has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %>
8
+ <% end -%>
9
+ <% if attributes.any?(&:password_digest?) -%>
10
+ has_secure_password
11
+ <% end -%>
12
+
13
+ def execute
14
+ # Implement this
15
+ end
16
+ end
17
+ <% end -%>
metadata ADDED
@@ -0,0 +1,212 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crono_trigger
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - joker1007
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-06-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: chrono
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: serverengine
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: concurrent-ruby
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activerecord
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '4.2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '4.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: database_rewinder
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: timecop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: bundler
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.14'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.14'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '10.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '10.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rspec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '3.0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '3.0'
153
+ description: In Service Asynchronous Job Scheduler for Rails. This gem handles ActiveRecord
154
+ model as schedule definition.
155
+ email:
156
+ - kakyoin.hierophant@gmail.com
157
+ executables:
158
+ - crono_trigger
159
+ extensions: []
160
+ extra_rdoc_files: []
161
+ files:
162
+ - ".gitignore"
163
+ - ".rspec"
164
+ - ".travis.yml"
165
+ - Gemfile
166
+ - LICENSE.txt
167
+ - README.md
168
+ - Rakefile
169
+ - bin/console
170
+ - bin/setup
171
+ - crono_trigger.gemspec
172
+ - exe/crono_trigger
173
+ - gemfiles/activerecord-42.gemfile
174
+ - gemfiles/activerecord-50.gemfile
175
+ - gemfiles/activerecord-51.gemfile
176
+ - lib/crono_trigger.rb
177
+ - lib/crono_trigger/cli.rb
178
+ - lib/crono_trigger/polling_thread.rb
179
+ - lib/crono_trigger/railtie.rb
180
+ - lib/crono_trigger/schedulable.rb
181
+ - lib/crono_trigger/version.rb
182
+ - lib/crono_trigger/worker.rb
183
+ - lib/generators/crono_trigger/migration/migration_generator.rb
184
+ - lib/generators/crono_trigger/migration/templates/create_table_migration.rb
185
+ - lib/generators/crono_trigger/migration/templates/migration.rb
186
+ - lib/generators/crono_trigger/model/model_generator.rb
187
+ - lib/generators/crono_trigger/model/templates/model.rb
188
+ homepage: https://github.com/joker1007/crono_trigger
189
+ licenses:
190
+ - MIT
191
+ metadata: {}
192
+ post_install_message:
193
+ rdoc_options: []
194
+ require_paths:
195
+ - lib
196
+ required_ruby_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ required_rubygems_version: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ requirements: []
207
+ rubyforge_project:
208
+ rubygems_version: 2.6.11
209
+ signing_key:
210
+ specification_version: 4
211
+ summary: In Service Asynchronous Job Scheduler for Rails
212
+ test_files: []