thecore_backend_commons 3.9.1 → 3.10.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: 3aa559045828d8dc8ca4981fad05e6ff9ef2f80a7191f2e16db4f45ddcaf1c24
4
- data.tar.gz: cd8fe33d14e2373a1b01ccd7290ceba1cb75048df57b6591a35c9cb135e077d5
3
+ metadata.gz: ae67ed15f7e3e684726abb005f023d769f0ee953ebce5d3f019baecb6d2a5d46
4
+ data.tar.gz: 76bfaf830d5bf571797f5fdd42794f087506eca3dd15c43ca0fc7ee6733871eb
5
5
  SHA512:
6
- metadata.gz: 2a94d02e27478addb9660b69f693d4c8760e92dd11c5698130a7dc38601e1a745d3ca041ee6103204e4aeb070d138ef64877537debba042153ad5d2125650414
7
- data.tar.gz: 15fc51c68e34e67042f8a02699639d5fb953e8710fce8e5923c225fa414e6c7c52c6ca5e22d3d618b817b88df09597dff65d31b35935bcbbc884b3678029ae2c
6
+ metadata.gz: 1b1554f540e0b404c3544ffee0cbc2ca531c861fc7d910a00892ca7867fbcc866f93e29860626afc4d41c4da31cc783cc73d5396235d8d3eb0c9fad78d9914a7
7
+ data.tar.gz: a25a66bf6cefa142a62fc176576efebb259424323563726a3256f132fea54c398b777b7782f501b59ea20de37ffca37201682aafb2eaa46a4c61db31d3d39b64
data/README.md CHANGED
@@ -159,6 +159,67 @@ overrides it. Full mechanics (idempotency, abstract/STI exclusion, `to_prepare`
159
159
 
160
160
  ---
161
161
 
162
+ ## Background jobs: no retries by default
163
+
164
+ Every app using this gem runs its background jobs on Sidekiq in production, and by default:
165
+
166
+ - **a failed job is never retried** (`retry: false`): it runs once, and Sidekiq keeps it in
167
+ neither the retry set nor the Dead Set;
168
+ - **only the latest failed job is kept in Redis**: `sidekiq-failures` (from
169
+ `thecore_background_jobs`) stores each failed job's full payload, arguments included, in the
170
+ Redis `failed` set; it is capped at 1, still visible in the Sidekiq web UI's Failures tab;
171
+ - **every failure is written to the Rails log** by `ThecoreBackendCommons::DiscardedJobLogger`:
172
+ job class (or `Mailer#action`), queue, jid and error, never the arguments.
173
+
174
+ Why zero: in a Thecore app in production, mail jobs carrying a whole PDF in their arguments
175
+ failed with a permanent SMTP error and were retried for weeks, then kept in the Dead Set. Redis
176
+ grew to 23 GB until the host's OOM killer put it in a restart loop and no job ran for three days
177
+ (`docs/adr/0002-no-job-retries-failures-logged.md`).
178
+
179
+ ### Changing the defaults on an installation
180
+
181
+ Set these in the environment of both the `backend` and the `worker` containers. On an
182
+ installation deployed by `thecore_devcontainer` that is the target's
183
+ `vendor/deploytargets/<provider>/<customer>.env`, which reaches both containers through their
184
+ `env_file` (`thecore_devcontainer` ADR 0002); on a hand-written compose, its `environment:`.
185
+
186
+ | Variable | Unset or blank | Accepted values | Effect |
187
+ | --- | --- | --- | --- |
188
+ | `THECORE_JOB_RETRIES` | `0` (no retries) | a whole number >= 0 | how many times Sidekiq retries a failed job, app-wide |
189
+ | `THECORE_FAILED_JOBS_KEPT` | `1` | a whole number >= 1 | how many failed jobs stay in the Redis `failed` set |
190
+
191
+ Any other value (`yes`, `-1`, `1.5`, ...) stops the app at boot with an error naming the
192
+ variable: a typo never silently turns retries back on. With retries above 0, a job that uses
193
+ them all ends in Sidekiq's Dead Set, which is capped by count and age but not by size.
194
+
195
+ ### Changing it for one job
196
+
197
+ A job class sets its own retry count with Sidekiq's option, which wins over the app-wide value
198
+ in both directions (it works on ActiveJob classes and mailers' jobs alike):
199
+
200
+ ```ruby
201
+ class SyncInventoryJob < ApplicationJob
202
+ sidekiq_options retry: 3 # or retry: false when THECORE_JOB_RETRIES is above 0
203
+ end
204
+ ```
205
+
206
+ **Don't use ActiveJob's `retry_on` for this.** It retries independently of Sidekiq: on failure
207
+ ActiveJob enqueues a new copy of the job, arguments included, whatever `THECORE_JOB_RETRIES`
208
+ says, and the copies never show up as Sidekiq retries.
209
+
210
+ ### Logging failures somewhere else too
211
+
212
+ Add your own Sidekiq death handler next to the default one; both run:
213
+
214
+ ```ruby
215
+ # config/initializers/sidekiq.rb in the app
216
+ Sidekiq.configure_server do |config|
217
+ config.death_handlers << ->(job, exception) { MyErrorLog.record(job["jid"], exception) }
218
+ end
219
+ ```
220
+
221
+ ---
222
+
162
223
  ## Invio email e configurazione SMTP
163
224
 
164
225
  ### Configurazione
@@ -0,0 +1,11 @@
1
+ # What a failed background job leaves behind, app-wide, from the container's environment:
2
+ # THECORE_JOB_RETRIES (default 0, no retries) and THECORE_FAILED_JOBS_KEPT (default 1, the latest
3
+ # failure in the sidekiq-failures set); an invalid value refuses to boot (JobFailurePolicy).
4
+ # Why: docs/adr/0002-no-job-retries-failures-logged.md.
5
+ ThecoreBackendCommons::JobFailurePolicy.apply!
6
+
7
+ # Every failed job leaves one line in the Rails log (DiscardedJobLogger); apps append their own
8
+ # handlers next to it. Required, not autoloaded: see docs/adr/0002-no-job-retries-failures-logged.md.
9
+ Sidekiq.configure_server do |config|
10
+ config.death_handlers << ThecoreBackendCommons::DiscardedJobLogger unless config.death_handlers.include?(ThecoreBackendCommons::DiscardedJobLogger)
11
+ end
@@ -0,0 +1,31 @@
1
+ module ThecoreBackendCommons
2
+ # Generic Sidekiq death handler, registered by config/initializers/sidekiq.rb: with retries off
3
+ # Sidekiq drops a failed job without keeping it anywhere, so this writes one line to the Rails log
4
+ # per failure. Only the job's identity goes in: class (or Mailer#action for a mail job), queue, jid
5
+ # and the error, never the arguments, which can be huge or sensitive (docs/adr/0002). Apps add their own handlers next to this one (`config.death_handlers << MyHandler`
6
+ # inside `Sidekiq.configure_server`), e.g. to record failures in their own integration log.
7
+ module DiscardedJobLogger
8
+ MAX_MESSAGE_LENGTH = 500
9
+
10
+ def self.call(job, exception)
11
+ error = "#{exception.class}: #{exception.message}".truncate(MAX_MESSAGE_LENGTH)
12
+ Rails.logger.error(
13
+ "Background job #{name_of(job)} failed and was discarded (no retry left): " \
14
+ "queue=#{job["queue"]} jid=#{job["jid"]} error=#{error}"
15
+ )
16
+ rescue StandardError => e
17
+ Rails.logger.error("DiscardedJobLogger could not record a failed job: #{e.class}: #{e.message}")
18
+ end
19
+
20
+ def self.name_of(job)
21
+ wrapped = (job["wrapped"] || job["class"]).to_s
22
+ first = Array(job["args"]).first
23
+ arguments = first.is_a?(Hash) ? Array(first["arguments"]) : []
24
+ return wrapped unless wrapped == "ActionMailer::MailDeliveryJob" && arguments.size >= 2
25
+
26
+ "#{arguments[0]}##{arguments[1]}"
27
+ end
28
+
29
+ private_class_method :name_of
30
+ end
31
+ end
@@ -0,0 +1,54 @@
1
+ module ThecoreBackendCommons
2
+ # What happens to a failed background job, app-wide, read from the container's environment by
3
+ # config/initializers/sidekiq.rb (docs/adr/0002-no-job-retries-failures-logged.md):
4
+ #
5
+ # - THECORE_JOB_RETRIES: how many times Sidekiq retries a failed job. Unset or blank = 0, i.e.
6
+ # `retry: false` (not `retry: 0`, which would still send the job to the Dead Set).
7
+ # - THECORE_FAILED_JOBS_KEPT: how many failed jobs sidekiq-failures keeps in the Redis `failed`
8
+ # set, payload and arguments included (Sidekiq.failures_max_count). Unset or blank = 1, the
9
+ # latest, still shown in the Failures tab.
10
+ #
11
+ # Any other value refuses to boot: a typo must never silently re-enable retries or keep every
12
+ # failed payload. A job class still overrides the retry count with `sidekiq_options retry: N`.
13
+ # Required, not autoloaded: see the ADR.
14
+ module JobFailurePolicy
15
+ RETRIES_VARIABLE = "THECORE_JOB_RETRIES".freeze
16
+ KEPT_VARIABLE = "THECORE_FAILED_JOBS_KEPT".freeze
17
+ DEFAULT_RETRIES = 0
18
+ DEFAULT_KEPT = 1
19
+
20
+ class InvalidValue < StandardError; end
21
+
22
+ # Sets Sidekiq's app-wide default retry option and the sidekiq-failures cap.
23
+ def self.apply!
24
+ Sidekiq.default_job_options = Sidekiq.default_job_options.merge("retry" => sidekiq_retry_option)
25
+ Sidekiq.failures_max_count = failed_jobs_kept
26
+ end
27
+
28
+ # The value for Sidekiq's `retry` option: false for no retries, else the count.
29
+ def self.sidekiq_retry_option
30
+ retries.zero? ? false : retries
31
+ end
32
+
33
+ def self.retries
34
+ whole_number_from_env(RETRIES_VARIABLE, default: DEFAULT_RETRIES, minimum: 0, expected: "a whole number >= 0")
35
+ end
36
+
37
+ def self.failed_jobs_kept
38
+ whole_number_from_env(KEPT_VARIABLE, default: DEFAULT_KEPT, minimum: 1, expected: "a whole number >= 1")
39
+ end
40
+
41
+ # The whole number in the environment variable, default when unset or blank; raises InvalidValue
42
+ # on anything else or below minimum.
43
+ def self.whole_number_from_env(variable, default:, minimum:, expected:)
44
+ raw = ENV.fetch(variable, nil)
45
+ return default if raw.nil? || raw.strip.empty?
46
+
47
+ value = raw.strip
48
+ return value.to_i if value.match?(/\A\d+\z/) && value.to_i >= minimum
49
+
50
+ raise InvalidValue, "#{variable}=#{raw.inspect} is not valid: use #{expected}, or leave it unset (default #{default})"
51
+ end
52
+ private_class_method :whole_number_from_env
53
+ end
54
+ end
@@ -1,3 +1,3 @@
1
1
  module ThecoreBackendCommons
2
- VERSION = "3.9.1".freeze
2
+ VERSION = "3.10.0".freeze
3
3
  end
@@ -19,6 +19,8 @@ require "thecore_backend_commons/smtp_config"
19
19
  require "thecore_backend_commons/smtp_tester"
20
20
  require "thecore_backend_commons/push_notification_service"
21
21
  require "thecore_backend_commons/default_module_registry"
22
+ require "thecore_backend_commons/discarded_job_logger"
23
+ require "thecore_backend_commons/job_failure_policy"
22
24
 
23
25
  module ThecoreBackendCommons
24
26
  # Deep-merges +dest+ into +src+ (mutating and returning +src+), extending existing arrays
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thecore_backend_commons
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.9.1
4
+ version: 3.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriele Tassoni
@@ -234,6 +234,7 @@ files:
234
234
  - config/initializers/default_module_registry.rb
235
235
  - config/initializers/extension_nil.rb
236
236
  - config/initializers/extension_string.rb
237
+ - config/initializers/sidekiq.rb
237
238
  - config/locales/en.devise.custom.yml
238
239
  - config/locales/en.yml
239
240
  - config/locales/it.devise.custom.yml
@@ -247,7 +248,9 @@ files:
247
248
  - lib/tasks/thecore_backend_commons_tasks.rake
248
249
  - lib/thecore_backend_commons.rb
249
250
  - lib/thecore_backend_commons/default_module_registry.rb
251
+ - lib/thecore_backend_commons/discarded_job_logger.rb
250
252
  - lib/thecore_backend_commons/engine.rb
253
+ - lib/thecore_backend_commons/job_failure_policy.rb
251
254
  - lib/thecore_backend_commons/push_notification_service.rb
252
255
  - lib/thecore_backend_commons/smtp_config.rb
253
256
  - lib/thecore_backend_commons/smtp_tester.rb