Class: ActiveSanction::Sync::Result
- Inherits:
-
Object
- Object
- ActiveSanction::Sync::Result
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/sync/result.rb
Overview
What one source did in one sync run, and what is stored for it now.
result.source # => :ofac_sdn
result.status # => :updated, :unchanged or :failed
result.record_count # => 19015
result.duration # => 12.41
result.age # => 0
Three statuses, and the distinction between the last two is the whole point of isolating sources from each other:
:updated a new list version was parsed and stored
:unchanged the publisher confirmed the copy we hold; nothing was written
:failed something raised; **the previous snapshot was kept**
A failed source still says what is being screened against
checksum, record_count, fetched_at and age describe the snapshot
that is in storage now, which for a failure is the one that was there
before the run. That is deliberate and it is the reason this object
carries an age at all: a source that has failed to refresh for nine days
is still answering screening calls, and the only thing standing between
that and an undetected compliance gap is that the age is visible. An
alert on result.failed? fires once; an alert on result.age is what
notices a source that has been quietly failing since Tuesday.
stored? is the loud case underneath it: a source that failed with
nothing stored behind it is not stale, it is absent, and screening will
not cover that list at all.
Serializable, on purpose
A host application alerts on a degrading source, and it should not have
to parse a log line to do it. #to_h is JSON-ready and .from_h rebuilds
it; the one thing that does not survive the round-trip is the exception
object, since a backtrace is not something to put in a metrics pipeline.
error_class and error_message do survive, because those are what an
alert is written against.
Instances are frozen on construction and compare by value.
Constant Summary collapse
- STATUSES =
How a sync of one source can come out.
unchangedis the publisher answering 304, which is the common case on a list that changes daily at most, and it is a success rather than a no-op. T.let(%i[updated unchanged failed].freeze, T::Array[Symbol])
Instance Attribute Summary collapse
-
#age ⇒ Integer?
readonly
Seconds between
fetched_atand the end of this source's sync, fixed here rather than computed on demand so that a serialized report says how stale the data was when the run saw it and not how long the report has since sat in a queue. -
#checksum ⇒ String?
readonly
The checksum of the snapshot in storage now, which is what a match result cites.
-
#duration ⇒ Float
readonly
Wall-clock seconds this source took, fetch through store.
-
#exception ⇒ Exception?
readonly
The exception that was captured, for a caller that wants the backtrace.
-
#fetched_at ⇒ Time?
readonly
When the snapshot in storage now was fetched -- for a failure, before this run.
-
#record_count ⇒ Integer?
readonly
Records stored for this source now -- not records fetched.
- #source ⇒ Symbol readonly
-
#status ⇒ Symbol
readonly
One of STATUSES.
Class Method Summary collapse
-
.from_h(hash) ⇒ T.attached_class
Rebuilds from #to_h output, accepting string keys so a report survives the trip through JSON.
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
-
#age_in_words ⇒ String
"3h old" rather than "10800 seconds": this is read by a human in a summary table, and the question being asked of it is only ever "is that a long time?".
-
#error ⇒ String?
The failure on one line, for a log or a table.
-
#error_class ⇒ String?
The exception's class name, and its message.
- #error_message ⇒ String?
- #failed? ⇒ Boolean
- #hash ⇒ Integer
-
#initialize(source:, status:, duration: 0.0, record_count: nil, checksum: nil, fetched_at: nil, age: nil, error: nil) ⇒ void
constructor
errortakes the exception itself -- which is what the orchestrator captured -- or the{class:, message:}pair #to_h wrote. - #inspect ⇒ String
-
#retained? ⇒ Boolean
A failure that kept its previous good snapshot -- the behaviour this whole run is arranged around.
-
#stored? ⇒ Boolean
Whether there is a snapshot to screen this source against.
- #to_h ⇒ Hash{Symbol => T.untyped}
- #to_s ⇒ String
- #unchanged? ⇒ Boolean
- #updated? ⇒ Boolean
Constructor Details
#initialize(source:, status:, duration: 0.0, record_count: nil, checksum: nil, fetched_at: nil, age: nil, error: nil) ⇒ void
error takes the exception itself -- which is what the orchestrator
captured -- or the {class:, message:} pair #to_h wrote.
124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/active_sanction/sync/result.rb', line 124 def initialize(source:, status:, duration: 0.0, record_count: nil, checksum: nil, fetched_at: nil, age: nil, error: nil) @source = T.let(symbol!(:source, source), Symbol) @status = T.let(status!(status), Symbol) @duration = T.let(duration.to_f, Float) @record_count = T.let(count!(record_count), T.nilable(Integer)) @checksum = T.let(string_or_nil(checksum), T.nilable(String)) @fetched_at = T.let(time_or_nil(fetched_at), T.nilable(Time)) @age = T.let(integer_or_nil(age), T.nilable(Integer)) @exception = T.let(error.is_a?(Exception) ? error : nil, T.nilable(Exception)) @failure = T.let(failure!(error), T.nilable(T::Hash[Symbol, String])) freeze end |
Instance Attribute Details
#age ⇒ Integer? (readonly)
Seconds between fetched_at and the end of this source's sync, fixed
here rather than computed on demand so that a serialized report says
how stale the data was when the run saw it and not how long the report
has since sat in a queue.
93 94 95 |
# File 'lib/active_sanction/sync/result.rb', line 93 def age @age end |
#checksum ⇒ String? (readonly)
The checksum of the snapshot in storage now, which is what a match result cites. Unchanged between two runs means the same list answered both.
81 82 83 |
# File 'lib/active_sanction/sync/result.rb', line 81 def checksum @checksum end |
#duration ⇒ Float (readonly)
Wall-clock seconds this source took, fetch through store.
97 98 99 |
# File 'lib/active_sanction/sync/result.rb', line 97 def duration @duration end |
#exception ⇒ Exception? (readonly)
The exception that was captured, for a caller that wants the backtrace. nil after a round-trip through #to_h -- see the class comment.
102 103 104 |
# File 'lib/active_sanction/sync/result.rb', line 102 def exception @exception end |
#fetched_at ⇒ Time? (readonly)
When the snapshot in storage now was fetched -- for a failure, before this run.
86 87 88 |
# File 'lib/active_sanction/sync/result.rb', line 86 def fetched_at @fetched_at end |
#record_count ⇒ Integer? (readonly)
Records stored for this source now -- not records fetched. A failed source reports what its retained snapshot holds, and nil only when there is no snapshot at all.
75 76 77 |
# File 'lib/active_sanction/sync/result.rb', line 75 def record_count @record_count end |
#source ⇒ Symbol (readonly)
65 66 67 |
# File 'lib/active_sanction/sync/result.rb', line 65 def source @source end |
#status ⇒ Symbol (readonly)
One of STATUSES.
69 70 71 |
# File 'lib/active_sanction/sync/result.rb', line 69 def status @status end |
Class Method Details
.from_h(hash) ⇒ T.attached_class
Rebuilds from #to_h output, accepting string keys so a report survives the trip through JSON.
107 108 109 110 111 112 113 114 115 116 |
# File 'lib/active_sanction/sync/result.rb', line 107 def self.from_h(hash) attributes = hash.to_h.transform_keys(&:to_sym) unknown = attributes.keys - MEMBERS raise InvalidArgument, "unknown Sync::Result attribute(s): #{unknown.join(", ")}" if unknown.any? # `new(**hash)` past required keyword parameters is one of the few # things Sorbet cannot check statically; #initialize validates what # arrives. T.unsafe(self).new(**attributes) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
219 220 221 222 223 |
# File 'lib/active_sanction/sync/result.rb', line 219 def ==(other) return false unless other.instance_of?(self.class) to_h == other.to_h end |
#age_in_words ⇒ String
"3h old" rather than "10800 seconds": this is read by a human in a summary table, and the question being asked of it is only ever "is that a long time?". A source with nothing stored says so instead, because the age of a list that is not there is not the problem with it.
200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/active_sanction/sync/result.rb', line 200 def age_in_words return "nothing stored" unless stored? seconds = age return "unknown age" if seconds.nil? [[86_400, "d"], [3_600, "h"], [60, "m"]].each do |(unit, suffix)| return "#{seconds / unit}#{suffix} old" if seconds >= unit end "just fetched" end |
#error ⇒ String?
The failure on one line, for a log or a table. nil when nothing failed.
170 171 172 173 174 175 176 177 178 179 |
# File 'lib/active_sanction/sync/result.rb', line 170 def error return nil unless error_class = # `raise SomeError` with no message gives a message that is the class # name, and "SomeError: SomeError" is not a line worth logging. return error_class if .nil? || .empty? || == error_class "#{error_class}: #{}" end |
#error_class ⇒ String?
The exception's class name, and its message. Strings rather than the class itself: this is what survives into a metrics pipeline, and a constant that no longer exists in the process reading a year-old report is not something to make it resolve.
143 |
# File 'lib/active_sanction/sync/result.rb', line 143 def error_class = @failure&.fetch(:class) |
#error_message ⇒ String?
146 |
# File 'lib/active_sanction/sync/result.rb', line 146 def = @failure&.fetch(:message) |
#failed? ⇒ Boolean
155 |
# File 'lib/active_sanction/sync/result.rb', line 155 def failed? = status == :failed |
#hash ⇒ Integer
227 |
# File 'lib/active_sanction/sync/result.rb', line 227 def hash = [self.class, to_h].hash |
#inspect ⇒ String
230 |
# File 'lib/active_sanction/sync/result.rb', line 230 def inspect = "#<#{self.class} #{self}>" |
#retained? ⇒ Boolean
A failure that kept its previous good snapshot -- the behaviour this whole run is arranged around.
166 |
# File 'lib/active_sanction/sync/result.rb', line 166 def retained? = failed? && stored? |
#stored? ⇒ Boolean
Whether there is a snapshot to screen this source against. False is the state that matters more than a failure: a list nothing is stored for is not covered by a screening run at all.
161 |
# File 'lib/active_sanction/sync/result.rb', line 161 def stored? = !checksum.nil? |
#to_h ⇒ Hash{Symbol => T.untyped}
182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/active_sanction/sync/result.rb', line 182 def to_h { source: source, status: status, record_count: record_count, checksum: checksum, fetched_at: fetched_at&.iso8601, age: age, duration: duration, error: @failure } end |
#to_s ⇒ String
213 214 215 216 |
# File 'lib/active_sanction/sync/result.rb', line 213 def to_s detail = failed? ? error.to_s : "#{record_count || 0} records, #{age_in_words}" "#{source} #{status} in #{format("%.2f", duration)}s: #{detail}" end |
#unchanged? ⇒ Boolean
152 |
# File 'lib/active_sanction/sync/result.rb', line 152 def unchanged? = status == :unchanged |
#updated? ⇒ Boolean
149 |
# File 'lib/active_sanction/sync/result.rb', line 149 def updated? = status == :updated |