crono_trigger 0.0.1

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.
@@ -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: []