Skip to content

Commit 3f149cb

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

2 files changed

Lines changed: 66 additions & 13 deletions

File tree

app/workers/custom_styles/seed_remote_asset_job.rb

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,33 +31,70 @@
3131
module CustomStyles
3232
# Downloads a design asset seeded through OPENPROJECT_SEED_DESIGN_* as a remote URL.
3333
class SeedRemoteAssetJob < ApplicationJob
34-
retry_on StandardError, wait: :polynomially_longer, attempts: 5
34+
include GoodJob::ActiveJobExtensions::Concurrency
35+
36+
# Only one asset for a given CustomStyle may be stored at a time. During seed,
37+
# GoodJob runs inline so jobs are mostly serial already, but retries and
38+
# non-inline enqueues could create a race condition for the same record/fog uploads.
39+
good_job_control_concurrency_with(
40+
perform_limit: 1,
41+
key: -> { "#{self.class.name}-#{arguments.first.id}" }
42+
)
43+
44+
retry_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError,
45+
wait: 5.seconds,
46+
attempts: :unlimited
47+
48+
# Retry transient HTTP failures a few times, after which we discard with a log entry.
49+
retry_on StandardError, wait: :polynomially_longer, attempts: 5, report: true do |job, error|
50+
job.log_discard(error)
51+
end
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
3961

4062
queue_with_priority :low
4163

4264
def perform(custom_style, key, url)
4365
download(custom_style, key, url)
4466

4567
Rails.logger.info "Seeded design asset '#{key}' from #{url}."
68+
rescue OpenProject::ServerSideRequestForgeryError, ActiveJob::DeserializationError
69+
raise
4670
rescue StandardError => e
47-
Rails.logger.error "Failed to seed design asset '#{key}' from #{url} " \
48-
"on attempt #{executions}: #{e.message}"
71+
log_attempt_failure(key, url, e)
4972
raise
5073
end
5174

75+
def log_discard(error)
76+
_custom_style, key, url = arguments
77+
Rails.logger.error "Discarding design asset seed for '#{key}' from #{url} " \
78+
"after #{executions} attempt(s): #{error.message}"
79+
end
80+
5281
private
5382

5483
def download(custom_style, key, url)
5584
response = OpenProject.httpx.get(url)
5685
response.raise_for_status
5786

58-
build_attachable_file(key.to_s, response.body.to_s) do |file|
59-
custom_style.public_send("#{key}=", file)
60-
custom_style.save!
87+
CustomStyle.transaction do
88+
style = CustomStyle.lock.find(custom_style.id)
89+
90+
build_attachable_file(key.to_s, response.body.to_s) do |file|
91+
style.public_send("#{key}=", file)
92+
style.save!
93+
end
94+
95+
unless style.public_send(key).readable?
96+
raise "Stored design asset '#{key}' is not readable in file storage"
97+
end
6198
end
6299
end
63100

@@ -78,5 +115,10 @@ def build_attachable_file(file_name, data)
78115
yield(file)
79116
end
80117
end
118+
119+
def log_attempt_failure(key, url, error)
120+
Rails.logger.error "Failed to seed design asset '#{key}' from #{url} " \
121+
"on attempt #{executions}: #{error.message}"
122+
end
81123
end
82124
end

spec/workers/custom_styles/seed_remote_asset_job_spec.rb

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

79-
it "swallows the error and reschedules itself instead" do
79+
it "logs the failed attempt and reschedules itself" do
80+
allow(Rails.logger).to receive(:error)
81+
8082
expect { perform }.not_to raise_error
8183

8284
expect(described_class).to have_been_enqueued.with(custom_style, :logo, url)
8385
expect(custom_style.reload.logo.file).to be_nil
86+
expect(Rails.logger)
87+
.to have_received(:error)
88+
.with(a_string_starting_with("Failed to seed design asset 'logo' from #{url} on attempt 1: HTTP Error: 404"))
8489
end
8590

86-
it "logs the failed attempt" do
91+
it "discards and logs after retries are exhausted" do
8792
allow(Rails.logger).to receive(:error)
8893

89-
perform
94+
job = described_class.new(custom_style, :logo, url)
95+
allow(job).to receive_messages(executions: 5, executions_for: 5)
9096

97+
expect { job.perform_now }.not_to raise_error
98+
expect(described_class).not_to have_been_enqueued
9199
expect(Rails.logger)
92100
.to have_received(:error)
93-
.with(a_string_starting_with("Failed to seed design asset 'logo' from #{url} on attempt 1: HTTP Error: 404"))
101+
.with(a_string_starting_with("Discarding design asset seed for 'logo' from #{url} after 5 attempt(s): HTTP Error: 404"))
94102
end
95103
end
96104

@@ -121,7 +129,7 @@
121129
expect(custom_style.reload.logo.file).to be_nil
122130
end
123131

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

127135
perform
@@ -130,6 +138,9 @@
130138
.to have_received(:error)
131139
.with(a_string_including("resolves only to private IP addresses",
132140
"OPENPROJECT_SSRF_PROTECTION_IP_ALLOWLIST"))
141+
expect(Rails.logger)
142+
.to have_received(:error)
143+
.with(a_string_starting_with("Discarding design asset seed for 'logo' from #{url} after"))
133144
end
134145

135146
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)