3131module 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
82133end
0 commit comments