maintenance_tasks 1.0.0 → 1.2.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +180 -22
  3. data/Rakefile +16 -16
  4. data/app/controllers/maintenance_tasks/application_controller.rb +3 -6
  5. data/app/controllers/maintenance_tasks/runs_controller.rb +1 -12
  6. data/app/controllers/maintenance_tasks/tasks_controller.rb +7 -4
  7. data/app/helpers/maintenance_tasks/application_helper.rb +2 -48
  8. data/app/helpers/maintenance_tasks/{task_helper.rb → tasks_helper.rb} +26 -18
  9. data/app/jobs/maintenance_tasks/task_job.rb +49 -6
  10. data/app/models/maintenance_tasks/application_record.rb +2 -2
  11. data/app/models/maintenance_tasks/csv_collection.rb +33 -0
  12. data/app/models/maintenance_tasks/progress.rb +19 -14
  13. data/app/models/maintenance_tasks/run.rb +39 -1
  14. data/app/models/maintenance_tasks/runner.rb +20 -5
  15. data/app/models/maintenance_tasks/runs_page.rb +55 -0
  16. data/app/models/maintenance_tasks/task_data.rb +9 -5
  17. data/app/models/maintenance_tasks/ticker.rb +0 -1
  18. data/app/tasks/maintenance_tasks/task.rb +43 -16
  19. data/app/validators/maintenance_tasks/run_status_validator.rb +15 -13
  20. data/app/views/layouts/maintenance_tasks/_navbar.html.erb +0 -6
  21. data/app/views/maintenance_tasks/runs/_info.html.erb +6 -0
  22. data/app/views/maintenance_tasks/runs/_run.html.erb +1 -9
  23. data/app/views/maintenance_tasks/runs/info/_running.html.erb +0 -2
  24. data/app/views/maintenance_tasks/tasks/show.html.erb +31 -4
  25. data/config/routes.rb +4 -6
  26. data/db/migrate/20210225152418_remove_index_on_task_name.rb +14 -0
  27. data/exe/maintenance_tasks +2 -2
  28. data/lib/generators/maintenance_tasks/install_generator.rb +4 -5
  29. data/lib/generators/maintenance_tasks/task_generator.rb +15 -9
  30. data/lib/generators/maintenance_tasks/templates/csv_task.rb.tt +13 -0
  31. data/lib/generators/maintenance_tasks/templates/task_spec.rb.tt +1 -1
  32. data/lib/generators/maintenance_tasks/templates/task_test.rb.tt +1 -1
  33. data/lib/maintenance_tasks.rb +34 -30
  34. data/lib/maintenance_tasks/cli.rb +18 -4
  35. data/lib/maintenance_tasks/engine.rb +6 -3
  36. metadata +9 -31
  37. data/app/views/maintenance_tasks/runs/index.html.erb +0 -15
  38. data/app/views/maintenance_tasks/tasks/actions/_cancelled.html.erb +0 -1
  39. data/app/views/maintenance_tasks/tasks/actions/_cancelling.html.erb +0 -4
  40. data/app/views/maintenance_tasks/tasks/actions/_enqueued.html.erb +0 -2
  41. data/app/views/maintenance_tasks/tasks/actions/_errored.html.erb +0 -1
  42. data/app/views/maintenance_tasks/tasks/actions/_interrupted.html.erb +0 -2
  43. data/app/views/maintenance_tasks/tasks/actions/_new.html.erb +0 -1
  44. data/app/views/maintenance_tasks/tasks/actions/_paused.html.erb +0 -2
  45. data/app/views/maintenance_tasks/tasks/actions/_pausing.html.erb +0 -2
  46. data/app/views/maintenance_tasks/tasks/actions/_running.html.erb +0 -2
  47. data/app/views/maintenance_tasks/tasks/actions/_succeeded.html.erb +0 -1
  48. data/lib/maintenance_tasks/integrations/bugsnag_handler.rb +0 -4
@@ -41,7 +41,7 @@ module MaintenanceTasks
41
41
  task_names = Task.available_tasks.map(&:name)
42
42
  available_task_runs = Run.where(task_name: task_names)
43
43
  last_runs = Run.where(
44
- id: available_task_runs.select('MAX(id) as id').group(:task_name)
44
+ id: available_task_runs.select("MAX(id) as id").group(:task_name)
45
45
  )
46
46
 
47
47
  task_names.map do |task_name|
@@ -73,7 +73,7 @@ module MaintenanceTasks
73
73
  def code
74
74
  return if deleted?
75
75
  task = Task.named(name)
76
- file = task.instance_method(:collection).source_location.first
76
+ file = task.instance_method(:process).source_location.first
77
77
  File.read(file)
78
78
  end
79
79
 
@@ -111,7 +111,7 @@ module MaintenanceTasks
111
111
  #
112
112
  # @return [String] the Task status.
113
113
  def status
114
- last_run&.status || 'new'
114
+ last_run&.status || "new"
115
115
  end
116
116
 
117
117
  # Retrieves the Task's category, which is one of active, new, or completed.
@@ -127,11 +127,15 @@ module MaintenanceTasks
127
127
  end
128
128
  end
129
129
 
130
+ # @return [Boolean] whether the Task inherits from CsvTask.
131
+ def csv_task?
132
+ !deleted? && Task.named(name) < CsvCollection
133
+ end
134
+
130
135
  private
131
136
 
132
137
  def runs
133
- Run.where(task_name: name).order(created_at: :desc)
138
+ Run.where(task_name: name).with_attached_csv.order(created_at: :desc)
134
139
  end
135
140
  end
136
- private_constant :TaskData
137
141
  end
@@ -54,5 +54,4 @@ module MaintenanceTasks
54
54
  Time.now - @last_persisted >= @throttle_duration
55
55
  end
56
56
  end
57
- private_constant :Ticker
58
57
  end
@@ -16,9 +16,12 @@ module MaintenanceTasks
16
16
  #
17
17
  # @raise [NotFoundError] if a Task with the given name does not exist.
18
18
  def named(name)
19
- name.constantize
20
- rescue NameError
21
- raise NotFoundError.new("Task #{name} not found.", name)
19
+ task = name.safe_constantize
20
+ raise NotFoundError.new("Task #{name} not found.", name) unless task
21
+ unless task.is_a?(Class) && task < Task
22
+ raise NotFoundError.new("#{name} is not a Task.", name)
23
+ end
24
+ task
22
25
  end
23
26
 
24
27
  # Returns a list of concrete classes that inherit from the Task
@@ -30,6 +33,41 @@ module MaintenanceTasks
30
33
  descendants
31
34
  end
32
35
 
36
+ # Make this Task a task that handles CSV.
37
+ #
38
+ # An input to upload a CSV will be added in the form to start a Run. The
39
+ # collection and count method are implemented.
40
+ def csv_collection
41
+ include(CsvCollection)
42
+ end
43
+
44
+ # Processes one item.
45
+ #
46
+ # Especially useful for tests.
47
+ #
48
+ # @param item the item to process.
49
+ def process(item)
50
+ new.process(item)
51
+ end
52
+
53
+ # Returns the collection for this Task.
54
+ #
55
+ # Especially useful for tests.
56
+ #
57
+ # @return the collection.
58
+ def collection
59
+ new.collection
60
+ end
61
+
62
+ # Returns the count of items for this Task.
63
+ #
64
+ # Especially useful for tests.
65
+ #
66
+ # @return the count of items.
67
+ def count
68
+ new.count
69
+ end
70
+
33
71
  private
34
72
 
35
73
  def load_constants
@@ -45,8 +83,7 @@ module MaintenanceTasks
45
83
  # @raise [NotImplementedError] with a message advising subclasses to
46
84
  # implement an override for this method.
47
85
  def collection
48
- raise NotImplementedError,
49
- "#{self.class.name} must implement `collection`."
86
+ raise NoMethodError, "#{self.class.name} must implement `collection`."
50
87
  end
51
88
 
52
89
  # Placeholder method to raise in case a subclass fails to implement the
@@ -57,8 +94,7 @@ module MaintenanceTasks
57
94
  # @raise [NotImplementedError] with a message advising subclasses to
58
95
  # implement an override for this method.
59
96
  def process(_item)
60
- raise NotImplementedError,
61
- "#{self.class.name} must implement `process`."
97
+ raise NoMethodError, "#{self.class.name} must implement `process`."
62
98
  end
63
99
 
64
100
  # Total count of iterations to be performed.
@@ -70,14 +106,5 @@ module MaintenanceTasks
70
106
  # @return [Integer, nil]
71
107
  def count
72
108
  end
73
-
74
- # Convenience method to allow tasks define enumerators with cursors for
75
- # compatibility with Job Iteration.
76
- #
77
- # @return [JobIteration::EnumeratorBuilder] instance of an enumerator
78
- # builder available to tasks.
79
- def enumerator_builder
80
- JobIteration.enumerator_builder.new(nil)
81
- end
82
109
  end
83
110
  end
@@ -11,8 +11,9 @@ module MaintenanceTasks
11
11
  # enqueued -> pausing occurs when the task is paused before starting.
12
12
  # enqueued -> cancelling occurs when the task is cancelled
13
13
  # before starting.
14
- # enqueued -> errored occurs when the task job fails to be enqueued.
15
- 'enqueued' => ['running', 'pausing', 'cancelling', 'errored'],
14
+ # enqueued -> errored occurs when the task job fails to be enqueued, or
15
+ # if the Task is deleted before is starts running.
16
+ "enqueued" => ["running", "pausing", "cancelling", "errored"],
16
17
  # pausing -> paused occurs when the task actually halts performing and
17
18
  # occupies a status of paused.
18
19
  # pausing -> cancelling occurs when the user cancels a task immediately
@@ -23,14 +24,14 @@ module MaintenanceTasks
23
24
  # nothing in its collection to process.
24
25
  # pausing -> errored occurs when the job raises an exception after the
25
26
  # user has paused it.
26
- 'pausing' => ['paused', 'cancelling', 'succeeded', 'errored'],
27
+ "pausing" => ["paused", "cancelling", "succeeded", "errored"],
27
28
  # cancelling -> cancelled occurs when the task actually halts performing
28
29
  # and occupies a status of cancelled.
29
30
  # cancelling -> succeeded occurs when the task completes immediately after
30
31
  # being cancelled. See description for pausing -> succeeded.
31
32
  # cancelling -> errored occurs when the job raises an exception after the
32
33
  # user has cancelled it.
33
- 'cancelling' => ['cancelled', 'succeeded', 'errored'],
34
+ "cancelling" => ["cancelled", "succeeded", "errored"],
34
35
  # running -> succeeded occurs when the task completes successfully.
35
36
  # running -> pausing occurs when a user pauses the task as
36
37
  # it's performing.
@@ -39,24 +40,26 @@ module MaintenanceTasks
39
40
  # running -> interrupted occurs when the job infra shuts down the task as
40
41
  # it's performing.
41
42
  # running -> errored occurs when the job raises an exception when running.
42
- 'running' => [
43
- 'succeeded',
44
- 'pausing',
45
- 'cancelling',
46
- 'interrupted',
47
- 'errored',
43
+ "running" => [
44
+ "succeeded",
45
+ "pausing",
46
+ "cancelling",
47
+ "interrupted",
48
+ "errored",
48
49
  ],
49
50
  # paused -> enqueued occurs when the task is resumed after being paused.
50
51
  # paused -> cancelling when the user cancels the task after it is paused.
51
52
  # paused -> cancelled when the user cancels the task after it is paused.
52
- 'paused' => ['enqueued', 'cancelling', 'cancelled'],
53
+ "paused" => ["enqueued", "cancelling", "cancelled"],
53
54
  # interrupted -> running occurs when the task is resumed after being
54
55
  # interrupted by the job infrastructure.
55
56
  # interrupted -> pausing occurs when the task is paused by the user while
56
57
  # it is interrupted.
57
58
  # interrupted -> cancelling occurs when the task is cancelled by the user
58
59
  # while it is interrupted.
59
- 'interrupted' => ['running', 'pausing', 'cancelling'],
60
+ # interrupted -> errored occurs when the task is deleted while it is
61
+ # interrupted.
62
+ "interrupted" => ["running", "pausing", "cancelling", "errored"],
60
63
  }
61
64
 
62
65
  # Validate whether a transition from one Run status
@@ -82,5 +85,4 @@ module MaintenanceTasks
82
85
  )
83
86
  end
84
87
  end
85
- private_constant :RunStatusValidator
86
88
  end
@@ -2,10 +2,4 @@
2
2
  <div class="navbar-brand">
3
3
  <%= link_to 'Maintenance Tasks', root_path, class: 'navbar-item is-size-4 has-text-weight-semibold has-text-danger' %>
4
4
  </div>
5
-
6
- <div id="navbarMenu" class="navbar-menu is-active">
7
- <div class="navbar-start">
8
- <%= link_to "Runs", runs_path, class: 'navbar-item' %>
9
- </div>
10
- </div>
11
5
  </nav>
@@ -8,3 +8,9 @@
8
8
  <div class="content">
9
9
  <%= render "maintenance_tasks/runs/info/#{run.status}", run: run %>
10
10
  </div>
11
+
12
+ <% if run.csv_file.present? %>
13
+ <div class="block">
14
+ <%= link_to('Download CSV', csv_file_download_path(run)) %>
15
+ </div>
16
+ <% end %>
@@ -1,11 +1,3 @@
1
1
  <div class="box">
2
- <% if with_task_name %>
3
- <h3 class="title is-3">
4
- <%= link_to run.task_name, task_path(run.task_name) %>
5
- <%= status_tag run.status %>
6
- </h3>
7
- <%= render 'maintenance_tasks/runs/info', run: run, with_status: false %>
8
- <% else %>
9
- <%= render 'maintenance_tasks/runs/info', run: run, with_status: true %>
10
- <% end %>
2
+ <%= render 'maintenance_tasks/runs/info', run: run, with_status: true %>
11
3
  </div>
@@ -1,7 +1,5 @@
1
1
  <p>
2
2
  <% if run.estimated_completion_time %>
3
3
  <%= estimated_time_to_completion(run).capitalize %> remaining.
4
- <% else %>
5
- Processed <%= pluralize run.tick_count, 'item' %> so far.
6
4
  <% end %>
7
5
  </p>
@@ -7,19 +7,46 @@
7
7
  <%= render 'maintenance_tasks/runs/info', run: @task.last_run, with_status: false if @task.last_run %>
8
8
 
9
9
  <div class="buttons">
10
- <%= render "maintenance_tasks/tasks/actions/#{@task.status}", task: @task %>
10
+ <% last_run = @task.last_run %>
11
+ <% if last_run.nil? || last_run.completed? %>
12
+ <%= form_with url: run_task_path(@task), method: :put do |form| %>
13
+ <% if @task.csv_task? %>
14
+ <div class="block">
15
+ <%= form.label :csv_file %>
16
+ <%= form.file_field :csv_file %>
17
+ </div>
18
+ <% end %>
19
+ <div class="block">
20
+ <%= form.submit 'Run', class: "button is-success", disabled: @task.deleted? %>
21
+ </div>
22
+ <% end %>
23
+ <% elsif last_run.cancelling? %>
24
+ <%= button_to 'Run', run_task_path(@task), method: :put, class: 'button is-success', disabled: true %>
25
+ <% if last_run.stuck? %>
26
+ <%= button_to 'Cancel', cancel_task_run_path(@task, last_run), method: :put, class: 'button is-danger', disabled: @task.deleted? %>
27
+ <% end %>
28
+ <% elsif last_run.pausing? %>
29
+ <%= button_to 'Pausing', pause_task_run_path(@task, last_run), method: :put, class: 'button is-warning', disabled: true %>
30
+ <%= button_to 'Cancel', cancel_task_run_path(@task, last_run), method: :put, class: 'button is-danger' %>
31
+ <% elsif last_run.paused? %>
32
+ <%= button_to 'Resume', run_task_path(@task), method: :put, class: 'button is-primary', disabled: @task.deleted? %>
33
+ <%= button_to 'Cancel', cancel_task_run_path(@task, last_run), method: :put, class: 'button is-danger' %>
34
+ <% else %>
35
+ <%= button_to 'Pause', pause_task_run_path(@task, last_run), method: :put, class: 'button is-warning', disabled: @task.deleted? %>
36
+ <%= button_to 'Cancel', cancel_task_run_path(@task, last_run), method: :put, class: 'button is-danger' %>
37
+ <% end%>
11
38
  </div>
12
39
 
13
40
  <% if (code = @task.code) %>
14
41
  <pre><code><%= highlight_code(code) %></code></pre>
15
42
  <% end %>
16
43
 
17
- <% if @previous_runs.present? %>
44
+ <% if @runs_page.records.present? %>
18
45
  <hr/>
19
46
 
20
47
  <h4 class="title is-4">Previous Runs</h4>
21
48
 
22
- <%= render @previous_runs, with_task_name: false %>
49
+ <%= render @runs_page.records %>
23
50
 
24
- <%= pagination(@pagy) %>
51
+ <%= link_to "Next page", task_path(@task, cursor: @runs_page.next_cursor) unless @runs_page.last? %>
25
52
  <% end %>
data/config/routes.rb CHANGED
@@ -1,19 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
  MaintenanceTasks::Engine.routes.draw do
3
- resources :runs, only: [:index], format: false
4
-
5
3
  resources :tasks, only: [:index, :show], format: false do
6
4
  member do
7
- put 'run'
5
+ put "run"
8
6
  end
9
7
 
10
8
  resources :runs, only: [], format: false do
11
9
  member do
12
- put 'pause'
13
- put 'cancel'
10
+ put "pause"
11
+ put "cancel"
14
12
  end
15
13
  end
16
14
  end
17
15
 
18
- root to: 'tasks#index'
16
+ root to: "tasks#index"
19
17
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ class RemoveIndexOnTaskName < ActiveRecord::Migration[6.0]
3
+ def up
4
+ change_table(:maintenance_tasks_runs) do |t|
5
+ t.remove_index(:task_name)
6
+ end
7
+ end
8
+
9
+ def down
10
+ change_table(:maintenance_tasks_runs) do |t|
11
+ t.index(:task_name)
12
+ end
13
+ end
14
+ end
@@ -2,11 +2,11 @@
2
2
 
3
3
  # frozen_string_literal: true
4
4
 
5
- require File.expand_path('config/application', Dir.pwd)
5
+ require File.expand_path("config/application", Dir.pwd)
6
6
 
7
7
  Rails.application.require_environment!
8
8
 
9
- require 'maintenance_tasks/cli'
9
+ require "maintenance_tasks/cli"
10
10
 
11
11
  module MaintenanceTasks
12
12
  CLI.start(ARGV)
@@ -5,18 +5,17 @@ module MaintenanceTasks
5
5
  #
6
6
  # @api private
7
7
  class InstallGenerator < Rails::Generators::Base
8
- source_root File.expand_path('templates', __dir__)
8
+ source_root File.expand_path("templates", __dir__)
9
9
 
10
10
  # Mounts the engine in the host application's config/routes.rb
11
11
  def mount_engine
12
- route("mount MaintenanceTasks::Engine => '/maintenance_tasks'")
12
+ route("mount MaintenanceTasks::Engine => \"/maintenance_tasks\"")
13
13
  end
14
14
 
15
15
  # Copies engine migrations to host application and migrates the database
16
16
  def install_migrations
17
- rake('maintenance_tasks:install:migrations')
18
- rake('db:migrate')
17
+ rake("maintenance_tasks:install:migrations")
18
+ rake("db:migrate")
19
19
  end
20
20
  end
21
- private_constant :InstallGenerator
22
21
  end
@@ -5,11 +5,14 @@ module MaintenanceTasks
5
5
  #
6
6
  # @api private
7
7
  class TaskGenerator < Rails::Generators::NamedBase
8
- source_root File.expand_path('templates', __dir__)
9
- desc 'This generator creates a task file at app/tasks and a corresponding '\
10
- 'test.'
8
+ source_root File.expand_path("templates", __dir__)
9
+ desc "This generator creates a task file at app/tasks and a corresponding "\
10
+ "test."
11
11
 
12
- check_class_collision suffix: 'Task'
12
+ class_option :csv, type: :boolean, default: false,
13
+ desc: "Generate a CSV Task."
14
+
15
+ check_class_collision suffix: "Task"
13
16
 
14
17
  # Creates the Task file.
15
18
  def create_task_file
@@ -18,7 +21,11 @@ module MaintenanceTasks
18
21
  class_path,
19
22
  "#{file_name}_task.rb"
20
23
  )
21
- template('task.rb', template_file)
24
+ if options[:csv]
25
+ template("csv_task.rb", template_file)
26
+ else
27
+ template("task.rb", template_file)
28
+ end
22
29
  end
23
30
 
24
31
  # Creates the Task test file, according to the app's test framework.
@@ -42,7 +49,7 @@ module MaintenanceTasks
42
49
  class_path,
43
50
  "#{file_name}_task_test.rb"
44
51
  )
45
- template('task_test.rb', template_file)
52
+ template("task_test.rb", template_file)
46
53
  end
47
54
 
48
55
  def create_task_spec_file
@@ -51,11 +58,11 @@ module MaintenanceTasks
51
58
  class_path,
52
59
  "#{file_name}_task_spec.rb"
53
60
  )
54
- template('task_spec.rb', template_file)
61
+ template("task_spec.rb", template_file)
55
62
  end
56
63
 
57
64
  def file_name
58
- super.sub(/_task\z/i, '')
65
+ super.sub(/_task\z/i, "")
59
66
  end
60
67
 
61
68
  def tasks_module
@@ -70,5 +77,4 @@ module MaintenanceTasks
70
77
  Rails.application.config.generators.options[:rails][:test_framework]
71
78
  end
72
79
  end
73
- private_constant :TaskGenerator
74
80
  end