Skip to content

Commit 1a806fe

Browse files
Increase verbosity of remote asset job
1 parent 971961f commit 1a806fe

2 files changed

Lines changed: 68 additions & 11 deletions

File tree

app/workers/custom_styles/seed_remote_asset_job.rb

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,33 +31,79 @@
3131
module CustomStyles
3232
# Downloads a design asset seeded through OPENPROJECT_SEED_DESIGN_* as a remote URL.
3333
class SeedRemoteAssetJob < ApplicationJob
34+
# Separate error class for non-retriable failures (e.g. HTTP 4xx) that we should log, then discard.
35+
class PermanentFailure < StandardError; end
36+
37+
include GoodJob::ActiveJobExtensions::Concurrency
38+
39+
# Only one asset for a given CustomStyle may be stored at a time. During seed,
40+
# GoodJob runs inline so jobs are mostly serial already, but retries and
41+
# non-inline enqueues could create a race condition for the same record/fog uploads.
42+
good_job_control_concurrency_with(
43+
perform_limit: 1,
44+
key: -> { "#{self.class.name}-#{arguments.first.id}" }
45+
)
46+
47+
retry_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError,
48+
wait: 5.seconds,
49+
attempts: :unlimited
50+
3451
retry_on StandardError, wait: :polynomially_longer, attempts: 5
3552

3653
# Declared after retry_on StandardError so they take precedence
37-
discard_on ActiveJob::DeserializationError
38-
discard_on OpenProject::ServerSideRequestForgeryError
54+
discard_on ActiveJob::DeserializationError do |job, error|
55+
job.log_discard(error)
56+
end
57+
58+
discard_on OpenProject::ServerSideRequestForgeryError do |job, error|
59+
job.log_discard(error)
60+
end
61+
62+
discard_on PermanentFailure do |job, error|
63+
job.log_discard(error)
64+
end
3965

4066
queue_with_priority :low
4167

4268
def perform(custom_style, key, url)
4369
download(custom_style, key, url)
4470

4571
Rails.logger.info "Seeded design asset '#{key}' from #{url}."
72+
rescue PermanentFailure, OpenProject::ServerSideRequestForgeryError, ActiveJob::DeserializationError
73+
raise
74+
rescue HTTPX::HTTPError => e
75+
log_attempt_failure(key, url, e)
76+
raise PermanentFailure, e.message if e.status.to_i.between?(400, 499)
77+
78+
raise
4679
rescue StandardError => e
47-
Rails.logger.error "Failed to seed design asset '#{key}' from #{url} " \
48-
"on attempt #{executions}: #{e.message}"
80+
log_attempt_failure(key, url, e)
4981
raise
5082
end
5183

84+
def log_discard(error)
85+
_custom_style, key, url = arguments
86+
Rails.logger.error "Discarding design asset seed for '#{key}' from #{url}: #{error.message}"
87+
end
88+
5289
private
5390

5491
def download(custom_style, key, url)
5592
response = OpenProject.httpx.get(url)
5693
response.raise_for_status
5794

58-
build_attachable_file(key.to_s, response.body.to_s) do |file|
59-
custom_style.public_send("#{key}=", file)
60-
custom_style.save!
95+
CustomStyle.transaction do
96+
style = CustomStyle.lock.find(custom_style.id)
97+
98+
build_attachable_file(key.to_s, response.body.to_s) do |file|
99+
style.public_send("#{key}=", file)
100+
style.save!
101+
end
102+
103+
unless style.public_send(key).readable?
104+
raise PermanentFailure,
105+
"Stored design asset '#{key}' is not readable in file storage"
106+
end
61107
end
62108
end
63109

@@ -78,5 +124,10 @@ def build_attachable_file(file_name, data)
78124
yield(file)
79125
end
80126
end
127+
128+
def log_attempt_failure(key, url, error)
129+
Rails.logger.error "Failed to seed design asset '#{key}' from #{url} " \
130+
"on attempt #{executions}: #{error.message}"
131+
end
81132
end
82133
end

spec/workers/custom_styles/seed_remote_asset_job_spec.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,21 +76,24 @@
7676
stub_request(:get, url).to_return(status: 404)
7777
end
7878

79-
it "swallows the error and reschedules itself instead" do
79+
it "discards the job without retrying" do
8080
expect { perform }.not_to raise_error
8181

82-
expect(described_class).to have_been_enqueued.with(custom_style, :logo, url)
82+
expect(described_class).not_to have_been_enqueued
8383
expect(custom_style.reload.logo.file).to be_nil
8484
end
8585

86-
it "logs the failed attempt" do
86+
it "logs the failed attempt and the discard" do
8787
allow(Rails.logger).to receive(:error)
8888

8989
perform
9090

9191
expect(Rails.logger)
9292
.to have_received(:error)
9393
.with(a_string_starting_with("Failed to seed design asset 'logo' from #{url} on attempt 1: HTTP Error: 404"))
94+
expect(Rails.logger)
95+
.to have_received(:error)
96+
.with(a_string_starting_with("Discarding design asset seed for 'logo' from #{url}: HTTP Error: 404"))
9497
end
9598
end
9699

@@ -121,7 +124,7 @@
121124
expect(custom_style.reload.logo.file).to be_nil
122125
end
123126

124-
it "logs why it was blocked" do
127+
it "logs why it was blocked and that the job is discarded" do
125128
allow(Rails.logger).to receive(:error)
126129

127130
perform
@@ -130,6 +133,9 @@
130133
.to have_received(:error)
131134
.with(a_string_including("resolves only to private IP addresses",
132135
"OPENPROJECT_SSRF_PROTECTION_IP_ALLOWLIST"))
136+
expect(Rails.logger)
137+
.to have_received(:error)
138+
.with(a_string_starting_with("Discarding design asset seed for 'logo' from #{url}:"))
133139
end
134140

135141
context "when the IP address is on the SSRF allowlist", with_ssrf_ip_allowlist: %w[127.0.0.1] do

0 commit comments

Comments
 (0)