maintenance_tasks 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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