tasks_scheduler 0.0.2

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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +3 -0
  4. data/Rakefile +33 -0
  5. data/app/assets/javascripts/tasks_scheduler.js +59 -0
  6. data/app/assets/stylesheets/tasks_scheduler.scss +34 -0
  7. data/app/controllers/scheduled_tasks_controller.rb +45 -0
  8. data/app/helpers/scheduled_tasks_helper.rb +17 -0
  9. data/app/models/scheduled_task.rb +61 -0
  10. data/app/models/scheduled_task/checker.rb +54 -0
  11. data/app/models/scheduled_task/log.rb +28 -0
  12. data/app/models/scheduled_task/runner.rb +50 -0
  13. data/app/models/scheduled_task/status.rb +53 -0
  14. data/app/views/scheduled_tasks/log.html.erb +9 -0
  15. data/app/views/scheduled_tasks/status.html.erb +7 -0
  16. data/app/views/scheduled_tasks/status_content.html.erb +47 -0
  17. data/config/initializers/assets.rb +1 -0
  18. data/config/locales/en.yml +22 -0
  19. data/config/locales/pt-BR.yml +22 -0
  20. data/config/routes.rb +13 -0
  21. data/db/migrate/20161122123828_create_scheduled_tasks.rb +11 -0
  22. data/db/migrate/20161123130153_add_status_attributes_to_scheduled_tasks.rb +9 -0
  23. data/db/migrate/20161124200712_add_pid_to_scheduled_tasks.rb +5 -0
  24. data/db/migrate/20161128163702_add_args_to_scheduled_tasks.rb +5 -0
  25. data/exe/tasks_scheduler +19 -0
  26. data/exe/tasks_scheduler_run_task +14 -0
  27. data/lib/tasks_scheduler.rb +9 -0
  28. data/lib/tasks_scheduler/checker.rb +20 -0
  29. data/lib/tasks_scheduler/cron_parser_patch.rb +27 -0
  30. data/lib/tasks_scheduler/cron_scheduling_validator.rb +18 -0
  31. data/lib/tasks_scheduler/engine.rb +9 -0
  32. data/lib/tasks_scheduler/version.rb +3 -0
  33. data/test/dummy/README.rdoc +28 -0
  34. data/test/dummy/Rakefile +6 -0
  35. data/test/dummy/app/assets/javascripts/application.js +13 -0
  36. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  37. data/test/dummy/app/controllers/application_controller.rb +5 -0
  38. data/test/dummy/app/helpers/application_helper.rb +2 -0
  39. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  40. data/test/dummy/bin/bundle +3 -0
  41. data/test/dummy/bin/rails +4 -0
  42. data/test/dummy/bin/rake +4 -0
  43. data/test/dummy/bin/setup +29 -0
  44. data/test/dummy/config.ru +4 -0
  45. data/test/dummy/config/application.rb +25 -0
  46. data/test/dummy/config/boot.rb +5 -0
  47. data/test/dummy/config/database.yml +25 -0
  48. data/test/dummy/config/environment.rb +5 -0
  49. data/test/dummy/config/environments/development.rb +41 -0
  50. data/test/dummy/config/environments/production.rb +81 -0
  51. data/test/dummy/config/environments/test.rb +42 -0
  52. data/test/dummy/config/initializers/assets.rb +11 -0
  53. data/test/dummy/config/initializers/backtrace_silencers.rb +9 -0
  54. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  55. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  56. data/test/dummy/config/initializers/inflections.rb +16 -0
  57. data/test/dummy/config/initializers/mime_types.rb +4 -0
  58. data/test/dummy/config/initializers/session_store.rb +3 -0
  59. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  60. data/test/dummy/config/locales/en.yml +23 -0
  61. data/test/dummy/config/routes.rb +56 -0
  62. data/test/dummy/config/secrets.yml +22 -0
  63. data/test/dummy/db/schema.rb +31 -0
  64. data/test/dummy/public/404.html +67 -0
  65. data/test/dummy/public/422.html +67 -0
  66. data/test/dummy/public/500.html +66 -0
  67. data/test/dummy/public/favicon.ico +0 -0
  68. data/test/fixtures/scheduled_tasks.yml +4 -0
  69. data/test/models/scheduled_task_test.rb +72 -0
  70. data/test/test_helper.rb +36 -0
  71. metadata +223 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f9254c9d8a1935a5a50a998c497502693a00a062
4
+ data.tar.gz: f1161054c8feebbad1210375262e0ad6e925f56b
5
+ SHA512:
6
+ metadata.gz: dd68161f972bc3632c370b1cd6ebdb18c4e825719156022324f3327fb743f4d9ce7e5de6bf3ffe991aa1143ffa46eb1c9dd696fd9f2561ccf991af7935138c8c
7
+ data.tar.gz: b05bd8fbcaab8d142be5798d0d47b5701368237d492eb5b4f108c6b3ffd9e0b8a726c9d40930b8210596890a53c46fd07f61ad5b014caee8ddd8e321ab155461
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Eduardo H. Bogoni
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,3 @@
1
+ = TasksScheduler
2
+
3
+ This project rocks and uses MIT-LICENSE.
@@ -0,0 +1,33 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'TasksScheduler'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path('../test/dummy/Rakefile', __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'lib'
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+ task default: :test
@@ -0,0 +1,59 @@
1
+ //= require active_scaffold
2
+
3
+ function TasksScheduler() {
4
+ }
5
+
6
+ TasksScheduler.Status = function () {
7
+ }
8
+
9
+ // Shortcut
10
+ var _S = TasksScheduler.Status;
11
+
12
+ _S.initialized = false;
13
+
14
+ _S.init = function (url, interval_max) {
15
+ if (!_S.initialized) {
16
+ _S.initialized = true;
17
+ _S.url = url;
18
+ _S.interval_max = interval_max;
19
+ _S.update();
20
+ }
21
+ };
22
+
23
+ _S.content = function () {
24
+ return $('#TaskScheduler_Status_Content');
25
+ };
26
+
27
+ _S.status = function () {
28
+ return $('#TaskScheduler_Status_Status');
29
+ };
30
+
31
+ _S.update_status = function () {
32
+ _S.status().html(
33
+ "Updating in " + _S.interval + " seconds..."
34
+ );
35
+ };
36
+
37
+ _S.check = function () {
38
+ if (_S.interval <= 0) {
39
+ _S.update();
40
+ } else {
41
+ _S.interval--;
42
+ _S.update_status();
43
+ setTimeout(_S.check, 1000);
44
+ }
45
+ };
46
+
47
+ _S.update = function () {
48
+ $.ajax({
49
+ url: _S.url,
50
+ success: function (result) {
51
+ _S.content().html(result);
52
+ },
53
+ complete: function (result) {
54
+ _S.interval = _S.interval_max + 1;
55
+ _S.last_update = new Date();
56
+ _S.check();
57
+ }
58
+ });
59
+ };
@@ -0,0 +1,34 @@
1
+ @import 'active_scaffold';
2
+
3
+ #TaskScheduler_Status_Status {
4
+ font-size: small;
5
+ margin-bottom: 1.0em;
6
+ }
7
+
8
+ #TaskScheduler_Status_Content {
9
+ table {
10
+ border-collapse: collapse;
11
+
12
+ td, th {
13
+ border: thin solid black;
14
+ padding: 0.3em;
15
+ text-align: center;
16
+ }
17
+
18
+ td.status_running {
19
+ color: blue;
20
+ }
21
+
22
+ td.status_waiting {
23
+ color: darkgrey;
24
+ }
25
+
26
+ td.status_failed {
27
+ color: red;
28
+ }
29
+ }
30
+
31
+ .last_run {
32
+ font-weight: bold;
33
+ }
34
+ }
@@ -0,0 +1,45 @@
1
+ class ScheduledTasksController < ApplicationController
2
+ active_scaffold :scheduled_task do |conf|
3
+ [:create, :update, :list].each do |action|
4
+ conf.send(action).columns.exclude(:next_run, :last_run_start,
5
+ :last_run_successful_start, :last_run_unsuccessful_start,
6
+ :last_run_successful_end, :last_run_unsuccessful_end,
7
+ :pid)
8
+ end
9
+ conf.columns[:task].form_ui = :select
10
+ conf.columns[:task].options ||= {}
11
+ conf.columns[:task].options[:options] = task_column_options
12
+ conf.action_links.add :status, label: I18n.t(:tasks_scheduler_status), position: true
13
+ conf.action_links.add :run_now, label: I18n.t(:run_now), type: :member,
14
+ crud_type: :update, method: :put, position: false
15
+ end
16
+
17
+ def log
18
+ record = find_if_allowed(params[:id], :read)
19
+ @log_file = record.log_file(params[:identifier])
20
+ end
21
+
22
+ def status
23
+ end
24
+
25
+ def status_content
26
+ @scheduled_tasks = ::ScheduledTask.order(task: :asc, scheduling: :asc)
27
+ render layout: false
28
+ end
29
+
30
+ def run_now
31
+ process_action_link_action do |record|
32
+ record.update_attributes!(next_run: Time.zone.now)
33
+ record.reload
34
+ flash[:info] = "Next run adjusted to #{record.next_run}"
35
+ end
36
+ end
37
+
38
+ class << self
39
+ private
40
+
41
+ def task_column_options
42
+ ::ScheduledTask.rake_tasks.map { |st| [st, st] }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ module ScheduledTasksHelper
2
+ def scheduled_tasks_status_time(time)
3
+ if time.present?
4
+ I18n.l(time, format: :short)
5
+ else
6
+ '-'
7
+ end
8
+ end
9
+
10
+ def scheduled_tasks_log(scheduled_task, log_identifier)
11
+ if File.exist?(scheduled_task.log_file(log_identifier))
12
+ link_to I18n.t(:log), log_scheduled_task_path(scheduled_task, identifier: log_identifier)
13
+ else
14
+ '-'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,61 @@
1
+ require 'rake'
2
+
3
+ class ScheduledTask < ActiveRecord::Base
4
+ include ::ScheduledTask::Checker
5
+ include ::ScheduledTask::Log
6
+ include ::ScheduledTask::Runner
7
+ include ::ScheduledTask::Status
8
+
9
+ class << self
10
+ def rake_tasks
11
+ @rake_tasks ||= begin
12
+ Rails.application.load_tasks
13
+ Rake.application.tasks.map(&:name)
14
+ end
15
+ end
16
+ end
17
+
18
+ validates :scheduling, presence: true, 'tasks_scheduler/cron_scheduling': true
19
+ validates :task, presence: true, inclusion: { in: rake_tasks }
20
+
21
+ STATUS_RUNNING = 'running'
22
+ STATUS_FAILED = 'failed'
23
+ STATUS_WAITING = 'waiting'
24
+
25
+ LOG_RUNNING = 'running'
26
+ LOG_SUCCESSFUL = 'successful'
27
+ LOG_UNSUCCESSFUL = 'unsuccessful'
28
+
29
+ def cron_parser
30
+ @cron_parser ||= ::CronParser.new(scheduling)
31
+ end
32
+
33
+ def to_s
34
+ "S: #{scheduling}, T: #{task}, NR: #{next_run.present? ? next_run.in_time_zone : '-'}"
35
+ end
36
+
37
+ def calculate_next_run(time = nil)
38
+ if time.present?
39
+ cron_parser.next(time.utc)
40
+ else
41
+ cron_parser.next
42
+ end
43
+ end
44
+
45
+ def write_attribute(name, value)
46
+ @cron_parser = nil if name == 'scheduling'
47
+ super
48
+ end
49
+
50
+ def process_running?
51
+ return false if pid.nil?
52
+ Process.kill(0, pid)
53
+ return true
54
+ rescue Errno::EPERM
55
+ raise "No permission to query #{pid}!"
56
+ rescue Errno::ESRCH
57
+ return false
58
+ rescue
59
+ raise "Unable to determine status for #{pid}"
60
+ end
61
+ end
@@ -0,0 +1,54 @@
1
+ require 'rake'
2
+
3
+ class ScheduledTask < ActiveRecord::Base
4
+ module Checker
5
+ def check
6
+ check_banner
7
+ if process_running?
8
+ check_log("Already running (PID: #{pid})")
9
+ return
10
+ end
11
+ if next_run.present?
12
+ check_task_with_next_run
13
+ else
14
+ check_task_without_next_run
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def check_log(message, method = :info)
21
+ Rails.logger.send(method, "TASK_CHECK(#{id}): #{message}")
22
+ end
23
+
24
+ def check_banner
25
+ check_log("Task: #{self}")
26
+ end
27
+
28
+ def check_task_without_next_run
29
+ check_log('Next run blank')
30
+ update_attributes!(next_run: calculate_next_run)
31
+ check_log("Next run @scheduled_taskored: #{next_run.in_time_zone}")
32
+ end
33
+
34
+ def check_task_with_next_run
35
+ if next_run < Time.zone.now
36
+ check_log('Next run reached. Running...')
37
+ spawn_task
38
+ else
39
+ check_log('Next run not reached')
40
+ end
41
+ end
42
+
43
+ def spawn_task
44
+ params = ['bundle', 'exec', 'tasks_scheduler_run_task', id.to_s]
45
+ check_log("Spawn command: #{params}")
46
+ spawn_pid = nil
47
+ Dir.chdir(Rails.root) do
48
+ spawn_pid = Process.spawn(*params)
49
+ end
50
+ Process.detach(spawn_pid)
51
+ update_attributes!(pid: spawn_pid)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,28 @@
1
+ class ScheduledTask < ActiveRecord::Base
2
+ module Log
3
+ def log_file(identifier)
4
+ unless log_identifiers.include?(identifier)
5
+ fail "Log identifier unknown: \"#{identifier}\" (Valid: #{log_identifiers})"
6
+ end
7
+ Rails.root.join('log', 'tasks_scheduler', "#{id}_#{identifier}.log")
8
+ end
9
+
10
+ private
11
+
12
+ def log_identifiers
13
+ [LOG_RUNNING, LOG_UNSUCCESSFUL, LOG_SUCCESSFUL]
14
+ end
15
+
16
+ def log_on_start
17
+ FileUtils.mkdir_p(File.dirname(log_file(LOG_RUNNING)))
18
+ File.unlink(log_file(LOG_RUNNING)) if File.exist?(log_file(LOG_RUNNING))
19
+ Rails.logger = ActiveSupport::Logger.new(log_file(LOG_RUNNING))
20
+ end
21
+
22
+ def log_on_end(exception)
23
+ target_log = exception ? log_file(LOG_UNSUCCESSFUL) : log_file(LOG_SUCCESSFUL)
24
+ File.unlink(target_log) if File.exist?(target_log)
25
+ File.rename(log_file(LOG_RUNNING), target_log)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,50 @@
1
+ class ScheduledTask < ActiveRecord::Base
2
+ module Runner
3
+ def run
4
+ log_on_start
5
+ run_banner
6
+ return if process_running? && pid != Process.pid
7
+ status_on_start
8
+ exception = invoke_task
9
+ run_log(exception, :fatal) if exception
10
+ status_on_end(exception)
11
+ log_on_end(exception)
12
+ run_log("Next run: #{next_run.in_time_zone}")
13
+ end
14
+
15
+ private
16
+
17
+ def run_log(message, method = :info)
18
+ if message.is_a?(Exception)
19
+ run_log("#{message.class}: #{message.message}")
20
+ run_log(message.backtrace.join("\n"))
21
+ else
22
+ Rails.logger.send(method, "TASK_RUN(#{id}): #{message}")
23
+ end
24
+ end
25
+
26
+ def run_banner
27
+ run_log("Task: #{self}")
28
+ run_log("PID: #{pid ? pid : '-'} (Current: #{Process.pid})")
29
+ run_log("Process running? #{process_running? ? 'Yes' : 'No'}")
30
+ end
31
+
32
+ def invoke_task
33
+ exception = nil
34
+ begin
35
+ Rake::Task.clear
36
+ Rails.application.load_tasks
37
+ Rake::Task[task].invoke(invoke_args)
38
+ rescue StandardError => ex
39
+ run_log(ex, :fatal)
40
+ exception = ex
41
+ end
42
+ exception
43
+ end
44
+
45
+ def invoke_args
46
+ return [] unless args.present?
47
+ args.split('|')
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,53 @@
1
+ class ScheduledTask < ActiveRecord::Base
2
+ module Status
3
+ def status
4
+ return STATUS_RUNNING if running?
5
+ return STATUS_WAITING if waiting?
6
+ return STATUS_FAILED if failed?
7
+ fail "Unknown status (#{status_attributes_values})"
8
+ end
9
+
10
+ def running?
11
+ last_run_start.present?
12
+ end
13
+
14
+ def waiting?
15
+ return true if ended?(last_run_successful_end, last_run_unsuccessful_end)
16
+ status_attributes.all? { |a| send(a).blank? }
17
+ end
18
+
19
+ def failed?
20
+ ended?(last_run_unsuccessful_end, last_run_successful_end)
21
+ end
22
+
23
+ def ended?(time, oposite_time)
24
+ !running? && time.present? && (oposite_time.blank? || oposite_time < time)
25
+ end
26
+
27
+ private
28
+
29
+ def status_on_start
30
+ update_attributes!(last_run_start: Time.zone.now)
31
+ end
32
+
33
+ def status_on_end(exception)
34
+ update_attributes!(
35
+ next_run: calculate_next_run,
36
+ (exception ? :last_run_unsuccessful_start : :last_run_successful_start) => last_run_start,
37
+ (exception ? :last_run_unsuccessful_end : :last_run_successful_end) => Time.zone.now,
38
+ last_run_start: nil,
39
+ pid: nil
40
+ )
41
+ end
42
+
43
+ def status_attributes
44
+ %w(start successful_start successful_end unsuccessful_start unsuccessful_end).map do |a|
45
+ "last_run_#{a}"
46
+ end
47
+ end
48
+
49
+ def status_attributes_values
50
+ status_attributes.map { |a| "#{a}: #{send(a)}" }.join(', ')
51
+ end
52
+ end
53
+ end