maintenance_tasks 2.1.0 → 2.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2dcb5b54d6f3fd47db28a3f57a487c201e2109b3eb8f78efbe3c972a374999e8
4
- data.tar.gz: eeb95960a0cac8de08aa3af7d12d47f6cfbee4d875ab74f8989f4d17ef545f6c
3
+ metadata.gz: 04c33ddef00ee39487f42a7275fd3b11367bc16f32007a607e2d6fd7a9b3b55d
4
+ data.tar.gz: 841a336d86ce65e52d2ddee9cef746f9a448664328213056ea8c7a9f0d861b76
5
5
  SHA512:
6
- metadata.gz: e99ebde772755c6f3d88d48e65df883805b2ceb4798334f00175bc8e4c83d093bef79931bf0c0c86b303c3d94d2e0b44b3b506b1813ea6e01597ccf47f2c8182
7
- data.tar.gz: 58d11ad3fa36721e9f29a49cecf586d2f362aaf155667ce6b7702b7acd7bb43f1defa2a2a4ed1fa259d563876b9cec5d5f7b66f4d5dcf43747009e64552fa804
6
+ metadata.gz: cefc3a619d3d3aec10c6104cf227c33558e1d30146f0d9338706eb30c4e21f58bda704905c85a8faa87bc4735d5255b714c8690b6b733f9dcde42216231abc3d
7
+ data.tar.gz: 4f23532262622d91da4065926f19e96bb22788877acc10e1e7a0ce327e7136a2c24905e9a29aacbbdbe380c39dc58e593a0254e6bfec2b1c6d929f8e5fe13304
data/README.md CHANGED
@@ -696,6 +696,26 @@ MaintenanceTasks.tasks_module = "TaskModule"
696
696
 
697
697
  If no value is specified, it will default to `Maintenance`.
698
698
 
699
+ #### Organizing tasks using namespaces
700
+
701
+ Tasks may be nested arbitrarily deeply under `app/tasks/maintenance`, for example given a
702
+ task file `app/tasks/maintenance/team_name/service_name/update_posts_task.rb` we
703
+ can define the task as:
704
+
705
+ ```ruby
706
+ module Maintenance
707
+ module TeamName
708
+ module ServiceName
709
+ class UpdatePostsTask < MaintenanceTasks::Task
710
+ def process(rows)
711
+ # ...
712
+ end
713
+ end
714
+ end
715
+ end
716
+ end
717
+ ```
718
+
699
719
  #### Customizing the underlying job class
700
720
 
701
721
  `MaintenanceTasks.job` can be configured to define a Job class for your tasks to
@@ -784,6 +804,34 @@ MaintenanceTasks.backtrace_cleaner = cleaner
784
804
  If none is specified, the default `Rails.backtrace_cleaner` will be used to
785
805
  clean backtraces.
786
806
 
807
+ #### Customizing the parent controller for the web UI
808
+
809
+ `MaintenanceTasks.parent_controller` can be configured to specify a controller class for all of the web UI engine's
810
+ controllers to inherit from.
811
+
812
+ This allows applications with common logic in their `ApplicationController` (or
813
+ any other controller) to optionally configure the web UI to inherit that logic
814
+ with a simple assignment in the initializer.
815
+
816
+ ```ruby
817
+ # config/initializers/maintenance_tasks.rb
818
+
819
+ MaintenanceTasks.parent_controller = "Services::CustomController"
820
+
821
+ # app/controllers/services/custom_controller.rb
822
+
823
+ class Services::CustomController < ActionController::Base
824
+ include CustomSecurityThings
825
+ include CustomLoggingThings
826
+ ...
827
+ end
828
+ ```
829
+
830
+ The parent controller value **must** be a string corresponding to an existing
831
+ controller class which **must inherit** from `ActionController::Base`.
832
+
833
+ If no value is specified, it will default to `"ActionController::Base"`.
834
+
787
835
  ## Upgrading
788
836
 
789
837
  Use bundler to check for and upgrade to newer versions. After installing a new
@@ -4,7 +4,7 @@ module MaintenanceTasks
4
4
  # Base class for all controllers used by this engine.
5
5
  #
6
6
  # Can be extended to add different authentication and authorization code.
7
- class ApplicationController < ActionController::Base
7
+ class ApplicationController < MaintenanceTasks.parent_controller.constantize
8
8
  BULMA_CDN = "https://cdn.jsdelivr.net"
9
9
 
10
10
  content_security_policy do |policy|
@@ -15,16 +15,11 @@ module MaintenanceTasks
15
15
  )
16
16
  policy.script_src(
17
17
  # page refresh script
18
- "'sha256-2RPaBS4XCMLp0JJ/sW407W9l4qjC+WQAHmTOFJTGfqo='",
18
+ "'sha256-NiHKryHWudRC2IteTqmY9v1VkaDUA/5jhgXkMTkgo2w='",
19
19
  )
20
20
  policy.frame_ancestors(:self)
21
21
  end
22
22
 
23
- before_action do
24
- request.content_security_policy_nonce_generator ||= ->(_request) { SecureRandom.base64(16) }
25
- request.content_security_policy_nonce_directives = ["style-src"]
26
- end
27
-
28
23
  protect_from_forgery with: :exception
29
24
  end
30
25
  end
@@ -20,7 +20,7 @@ module MaintenanceTasks
20
20
  rescue ActiveRecord::RecordInvalid => error
21
21
  redirect_to(task_path(error.record.task_name), alert: error.message)
22
22
  rescue ActiveRecord::ValueTooLong => error
23
- task_name = params.fetch(:id)
23
+ task_name = params.fetch(:task_id)
24
24
  redirect_to(task_path(task_name), alert: error.message)
25
25
  rescue Runner::EnqueuingError => error
26
26
  redirect_to(task_path(error.run.task_name), alert: error.message)
@@ -109,7 +109,7 @@ module MaintenanceTasks
109
109
  when ActiveModel::Type::Decimal, ActiveModel::Type::Float
110
110
  form_builder.number_field(parameter_name, { step: "any" })
111
111
  when ActiveModel::Type::DateTime
112
- form_builder.datetime_field(parameter_name)
112
+ form_builder.datetime_field(parameter_name) + datetime_field_help_text
113
113
  when ActiveModel::Type::Date
114
114
  form_builder.date_field(parameter_name)
115
115
  when ActiveModel::Type::Time
@@ -120,5 +120,19 @@ module MaintenanceTasks
120
120
  form_builder.text_area(parameter_name, class: "textarea")
121
121
  end
122
122
  end
123
+
124
+ # Return helper text for the datetime-local form field.
125
+ def datetime_field_help_text
126
+ text =
127
+ if Time.zone_default.nil? || Time.zone_default.name == "UTC"
128
+ "Timezone: UTC."
129
+ else
130
+ "Timezone: #{Time.now.zone}."
131
+ end
132
+ tag.div(
133
+ tag.p(text),
134
+ class: "content is-small",
135
+ )
136
+ end
123
137
  end
124
138
  end
@@ -56,14 +56,14 @@ module MaintenanceTasks
56
56
  batch_size: collection.batch_size,
57
57
  )
58
58
  when Array
59
- enumerator_builder.build_array_enumerator(collection, cursor: cursor)
59
+ enumerator_builder.build_array_enumerator(collection, cursor: cursor&.to_i)
60
60
  when BatchCsvCollectionBuilder::BatchCsv
61
61
  JobIteration::CsvEnumerator.new(collection.csv).batches(
62
62
  batch_size: collection.batch_size,
63
- cursor: cursor,
63
+ cursor: cursor&.to_i,
64
64
  )
65
65
  when CSV
66
- JobIteration::CsvEnumerator.new(collection).rows(cursor: cursor)
66
+ JobIteration::CsvEnumerator.new(collection).rows(cursor: cursor&.to_i)
67
67
  else
68
68
  raise ArgumentError, <<~MSG.squish
69
69
  #{@task.class.name}#collection must be either an
@@ -32,7 +32,6 @@ module MaintenanceTasks
32
32
  :cancelled,
33
33
  ]
34
34
  COMPLETED_STATUSES = [:succeeded, :errored, :cancelled]
35
- COMPLETED_RUNS_LIMIT = 10
36
35
  STUCK_TASK_TIMEOUT = 5.minutes
37
36
 
38
37
  enum status: STATUSES.to_h { |status| [status, status.to_s] }
@@ -175,7 +175,13 @@ module MaintenanceTasks
175
175
  namespace = MaintenanceTasks.tasks_module.safe_constantize
176
176
  return unless namespace
177
177
 
178
- namespace.constants.map { |constant| namespace.const_get(constant) }
178
+ load_const = lambda do |root|
179
+ root.constants.each do |name|
180
+ object = root.const_get(name)
181
+ load_const.call(object) if object.instance_of?(Module)
182
+ end
183
+ end
184
+ load_const.call(namespace)
179
185
  end
180
186
  end
181
187
 
@@ -33,17 +33,19 @@
33
33
 
34
34
  <script>
35
35
  function refresh() {
36
- if (!("refresh" in document.body.dataset)) return
36
+ const target = document.querySelector("[data-refresh]")
37
+ if (!target || !target.dataset.refresh) return
37
38
  window.setTimeout(() => {
38
39
  document.body.style.cursor = "wait"
39
40
  fetch(document.location, { headers: { "X-Requested-With": "XMLHttpRequest" } }).then(
40
41
  async response => {
41
42
  const text = await response.text()
42
43
  const newDocument = new DOMParser().parseFromString(text, "text/html")
43
- document.body.replaceWith(newDocument.body)
44
- <%# force a redraw for Safari %>
45
- window.scrollTo({ top: document.documentElement.scrollTop + 1 })
46
- window.scrollTo({ top: document.documentElement.scrollTop - 1 })
44
+ const newTarget = newDocument.querySelector("[data-refresh]")
45
+ if (newTarget) {
46
+ target.replaceWith(newTarget)
47
+ }
48
+ document.body.style.cursor = ""
47
49
  refresh()
48
50
  },
49
51
  error => location.reload()
@@ -54,7 +56,7 @@
54
56
  </script>
55
57
  </head>
56
58
 
57
- <%= tag.body(data: { refresh: defined?(@refresh) && @refresh }) do %>
59
+ <body>
58
60
  <%= render 'layouts/maintenance_tasks/navbar' %>
59
61
 
60
62
  <section class="section">
@@ -68,5 +70,5 @@
68
70
  <%= yield %>
69
71
  </div>
70
72
  </div>
71
- <% end %>
73
+ </body>
72
74
  </html>
@@ -9,9 +9,9 @@
9
9
  <td>
10
10
  <% next if value.empty? %>
11
11
  <% if value.include?("\n") %>
12
- <pre><%= value %><pre>
12
+ <pre><%= value %></pre>
13
13
  <% else %>
14
- <code><%= value %><code>
14
+ <code><%= value %></code>
15
15
  <% end %>
16
16
  </td>
17
17
  </tr>
@@ -1,6 +1,6 @@
1
1
  <div class="box">
2
2
  <h5 class="title is-5">
3
- <%= time_tag run.created_at, title: run.created_at %>
3
+ <%= time_tag run.created_at, title: run.created_at.utc.iso8601 %>
4
4
  <%= status_tag run.status %>
5
5
  </h5>
6
6
 
@@ -1,22 +1,24 @@
1
- <% if @available_tasks.empty? %>
2
- <div class="content is-large">
3
- <h3 class="title is-3"> The MaintenanceTasks gem has been successfully installed! </h3>
4
- <p>
5
- Any new Tasks will show up here. To start writing your first Task,
6
- run <code>bin/rails generate maintenance_tasks:task my_task</code>.
7
- </p>
8
- </div>
9
- <% else %>
10
- <% if active_tasks = @available_tasks[:active] %>
11
- <h3 class="title is-3">Active Tasks</h3>
12
- <%= render partial: 'task', collection: active_tasks %>
13
- <% end %>
14
- <% if new_tasks = @available_tasks[:new] %>
15
- <h3 class="title is-3">New Tasks</h3>
16
- <%= render partial: 'task', collection: new_tasks %>
17
- <% end %>
18
- <% if completed_tasks = @available_tasks[:completed] %>
19
- <h3 class="title is-3">Completed Tasks</h3>
20
- <%= render partial: 'task', collection: completed_tasks %>
1
+ <%= tag.div(data: { refresh: (defined?(@refresh) && @refresh) || "" }) do %>
2
+ <% if @available_tasks.empty? %>
3
+ <div class="content is-large">
4
+ <h3 class="title is-3"> The MaintenanceTasks gem has been successfully installed! </h3>
5
+ <p>
6
+ Any new Tasks will show up here. To start writing your first Task,
7
+ run <code>bin/rails generate maintenance_tasks:task my_task</code>.
8
+ </p>
9
+ </div>
10
+ <% else %>
11
+ <% if active_tasks = @available_tasks[:active] %>
12
+ <h3 class="title is-3">Active Tasks</h3>
13
+ <%= render partial: 'task', collection: active_tasks %>
14
+ <% end %>
15
+ <% if new_tasks = @available_tasks[:new] %>
16
+ <h3 class="title is-3">New Tasks</h3>
17
+ <%= render partial: 'task', collection: new_tasks %>
18
+ <% end %>
19
+ <% if completed_tasks = @available_tasks[:completed] %>
20
+ <h3 class="title is-3">Completed Tasks</h3>
21
+ <%= render partial: 'task', collection: completed_tasks %>
22
+ <% end %>
21
23
  <% end %>
22
24
  <% end %>
@@ -38,20 +38,22 @@
38
38
  <pre><code><%= highlight_code(code) %></code></pre>
39
39
  <% end %>
40
40
 
41
- <% if @task.active_runs.any? %>
42
- <hr/>
41
+ <%= tag.div(data: { refresh: (defined?(@refresh) && @refresh) || "" }) do %>
42
+ <% if @task.active_runs.any? %>
43
+ <hr/>
43
44
 
44
- <h4 class="title is-4">Active Runs</h4>
45
+ <h4 class="title is-4">Active Runs</h4>
45
46
 
46
- <%= render @task.active_runs %>
47
- <% end %>
47
+ <%= render @task.active_runs %>
48
+ <% end %>
48
49
 
49
- <% if @runs_page.records.present? %>
50
- <hr/>
50
+ <% if @runs_page.records.present? %>
51
+ <hr/>
51
52
 
52
- <h4 class="title is-4">Previous Runs</h4>
53
+ <h4 class="title is-4">Previous Runs</h4>
53
54
 
54
- <%= render @runs_page.records %>
55
+ <%= render @runs_page.records %>
55
56
 
56
- <%= link_to "Next page", task_path(@task, cursor: @runs_page.next_cursor) unless @runs_page.last? %>
57
+ <%= link_to "Next page", task_path(@task, cursor: @runs_page.next_cursor) unless @runs_page.last? %>
58
+ <% end %>
57
59
  <% end %>
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ChangeCursorToString < ActiveRecord::Migration[6.0]
4
+ # This migration will clear all existing data in the cursor column with MySQL.
5
+ # Ensure no Tasks are paused when this migration is deployed, or they will be resumed from the start.
6
+ # Running tasks are able to gracefully handle this change, even if interrupted.
7
+ def up
8
+ change_table(:maintenance_tasks_runs) do |t|
9
+ t.change(:cursor, :string)
10
+ end
11
+ end
12
+
13
+ def down
14
+ change_table(:maintenance_tasks_runs) do |t|
15
+ t.change(:cursor, :bigint)
16
+ end
17
+ end
18
+ end
@@ -72,4 +72,14 @@ module MaintenanceTasks
72
72
  # @return [Proc] the callback to perform when an error occurs in the Task.
73
73
  mattr_accessor :error_handler, default:
74
74
  ->(_error, _task_context, _errored_element) {}
75
+
76
+ # @!attribute parent_controller
77
+ # @scope class
78
+ #
79
+ # The parent controller all web UI controllers will inherit from.
80
+ # Must be a class that inherits from `ActionController::Base`.
81
+ # Defaults to `"ActionController::Base"`
82
+ #
83
+ # @return [String] the name of the parent controller for web UI.
84
+ mattr_accessor :parent_controller, default: "ActionController::Base"
75
85
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maintenance_tasks
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify Engineering
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-02-27 00:00:00.000000000 Z
11
+ date: 2023-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -131,6 +131,7 @@ files:
131
131
  - app/views/maintenance_tasks/tasks/show.html.erb
132
132
  - config/routes.rb
133
133
  - db/migrate/20201211151756_create_maintenance_tasks_runs.rb
134
+ - db/migrate/20210219212931_change_cursor_to_string.rb
134
135
  - db/migrate/20210225152418_remove_index_on_task_name.rb
135
136
  - db/migrate/20210517131953_add_arguments_to_maintenance_tasks_runs.rb
136
137
  - db/migrate/20211210152329_add_lock_version_to_maintenance_tasks_runs.rb
@@ -154,7 +155,7 @@ homepage: https://github.com/Shopify/maintenance_tasks
154
155
  licenses:
155
156
  - MIT
156
157
  metadata:
157
- source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v2.1.0
158
+ source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v2.2.0
158
159
  allowed_push_host: https://rubygems.org
159
160
  post_install_message:
160
161
  rdoc_options: []
@@ -164,14 +165,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
164
165
  requirements:
165
166
  - - ">="
166
167
  - !ruby/object:Gem::Version
167
- version: '0'
168
+ version: '3.0'
168
169
  required_rubygems_version: !ruby/object:Gem::Requirement
169
170
  requirements:
170
171
  - - ">="
171
172
  - !ruby/object:Gem::Version
172
173
  version: '0'
173
174
  requirements: []
174
- rubygems_version: 3.3.3
175
+ rubygems_version: 3.4.17
175
176
  signing_key:
176
177
  specification_version: 4
177
178
  summary: A Rails engine for queuing and managing maintenance tasks