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