-
Notifications
You must be signed in to change notification settings - Fork 106
Expand file tree
/
Copy pathchamp_external_data_concern.rb
More file actions
133 lines (108 loc) · 4.18 KB
/
Copy pathchamp_external_data_concern.rb
File metadata and controls
133 lines (108 loc) · 4.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# frozen_string_literal: true
module ChampExternalDataConcern
extend ActiveSupport::Concern
include Dry::Monads[:result]
# A champ is updated, a reset and fetch later event is triggered
# from the controller
# idle -> waiting_for_job
# A ChampFetchExternalDataJob is processed, the fetch event is triggered
# waiting_for_job -> fetching
# if an retryable error occurs, the retry event is triggered and the job is re-enqueued
# fetching -> waiting_for_job
# if a non-retryable error occurs, the external_data_error event is triggered
# fetching -> external_error
# if the data is fetched successfully, the external_data_fetched event is triggered
# fetching -> fetched
included do
include AASM
attribute :fetch_external_data_exceptions, :external_data_exception, array: true
# useful to serialize idle as nil
# otherwise, all the champ are mark as dirty and saved on first dossier.save
enum :external_state, {
idle: nil, # initial state
waiting_for_job: 'waiting_for_job',
fetching: 'fetching',
fetched: 'fetched',
external_error: 'external_error',
}
aasm column: :external_state, enum: true do
state :idle, initial: true
state :waiting_for_job
state :fetching
state :fetched
state :external_error
event :fetch_later, after_commit: :fetch_external_data_later do
transitions from: :idle, to: :waiting_for_job, guard: :ready_for_external_call?
end
event :fetch, after_commit: :fetch_and_handle_result do
transitions from: [:waiting_for_job], to: :fetching
end
event :external_data_fetched do
# waiting_for_job: a champ can converge to fetched from the retry_on
# exhaustion block (see #handle_exhausted_external_data_retries!)
transitions from: [:fetching, :waiting_for_job], to: :fetched
end
event :external_data_error do
transitions from: [:waiting_for_job, :fetching], to: :external_error
end
event :retry do
transitions from: [:fetching], to: :waiting_for_job
end
event :reset_external_data, after: :after_reset_external_data do
transitions from: [:idle, :waiting_for_job, :fetching, :fetched, :external_error], to: :idle
end
end
end
def pending? = waiting_for_job? || fetching?
def done? = fetched? || external_error?
def external_data_not_found? = external_error? && fetch_external_data_exceptions.last.not_found?
def has_async_external_data? = false
def external_data_needed_for_validation? = has_async_external_data?
# Called by ChampFetchExternalDataJob when all retry attempts are exhausted.
def handle_exhausted_external_data_retries!
external_data_error!
end
private
def ready_for_external_call? = external_id.present?
def fetch_external_data_later(wait: nil)
ChampFetchExternalDataJob.set(wait:).perform_later(self, external_id)
end
# it should only be called after fetch! event callback
def fetch_and_handle_result
fetch_external_data.then { handle_result(it) }
end
def fetch_external_data
raise NotImplemented.new(:fetch_external_data)
end
def handle_result(result)
if result.is_a?(Dry::Monads::Result)
case result
in Success(hash)
update_external_data!(hash)
external_data_fetched!
in Failure(retryable: true, error:, code:)
save_external_error(error, code)
retry!
raise RetryableFetchError.new(error)
in Failure(retryable: false, error:, code:)
save_external_error(error, code)
Sentry.capture_exception(error) if code != 404
external_data_error!
end
elsif result.present?
update_external_data!(data: result)
external_data_fetched!
end
end
def update_external_data!(hash)
update!(hash.merge(fetch_external_data_exceptions: []))
end
def save_external_error(error, code)
exceptions = fetch_external_data_exceptions || []
exceptions << ExternalDataException.new(error: error.inspect, code:)
update_columns(fetch_external_data_exceptions: exceptions)
end
def after_reset_external_data(opts = {})
update(opts.merge(data: nil, value_json: nil, fetch_external_data_exceptions: []))
end
end