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 +4 -4
- data/README.md +61 -0
- data/config/initializers/sidekiq.rb +11 -0
- data/lib/thecore_backend_commons/discarded_job_logger.rb +31 -0
- data/lib/thecore_backend_commons/job_failure_policy.rb +54 -0
- data/lib/thecore_backend_commons/version.rb +1 -1
- data/lib/thecore_backend_commons.rb +2 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae67ed15f7e3e684726abb005f023d769f0ee953ebce5d3f019baecb6d2a5d46
|
|
4
|
+
data.tar.gz: 76bfaf830d5bf571797f5fdd42794f087506eca3dd15c43ca0fc7ee6733871eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
@@ -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.
|
|
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
|