active_sanction 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ require "sorbet-runtime"
5
5
 
6
6
  require "uri"
7
7
  require "active_sanction/error"
8
+ require "active_sanction/instrumentation"
8
9
  require "active_sanction/sources"
9
10
  require "active_sanction/storage"
10
11
  require "active_sanction/sync/result"
@@ -149,6 +150,17 @@ module ActiveSanction
149
150
  sig { returns(T.untyped) }
150
151
  attr_reader :logger
151
152
 
153
+ # Where the `:sync` and `:store` events go, or nil for nothing listening.
154
+ #
155
+ # The `:fetch` and `:parse` events of the sources this run covers do not
156
+ # come from here: an adapter is constructed by the run and reads the
157
+ # configuration, exactly as it does for its logger and its User-Agent. So
158
+ # a host that instruments through `ActiveSanction.configure` sees all six
159
+ # events, and one that hands a run its own instrumenter sees the two this
160
+ # class emits. See Instrumentation.
161
+ sig { returns(T.untyped) }
162
+ attr_reader :instrumenter
163
+
152
164
  sig { params(options: T.untyped, block: T.untyped).returns(Report) }
153
165
  def self.call(**options, &block) = T.unsafe(self).new(**options).call(&block)
154
166
 
@@ -157,9 +169,10 @@ module ActiveSanction
157
169
  # raises here, before the first list is downloaded, rather than after.
158
170
  sig do
159
171
  params(sources: T.untyped, store: T.untyped, force: T::Boolean, concurrency: T.untyped,
160
- logger: T.untyped).void
172
+ logger: T.untyped, instrumenter: T.untyped).void
161
173
  end
162
- def initialize(sources: nil, store: nil, force: false, concurrency: nil, logger: ActiveSanction.config.logger)
174
+ def initialize(sources: nil, store: nil, force: false, concurrency: nil, logger: ActiveSanction.config.logger,
175
+ instrumenter: ActiveSanction.config.instrumenter)
163
176
  @sources = T.let(resolve(sources), T::Array[T.untyped])
164
177
  @keys = T.let(@sources.map { |source| Sources::Definition.key!(source.key) }, T::Array[Symbol])
165
178
  @store = T.let(store || ActiveSanction.storage, T.untyped)
@@ -168,6 +181,7 @@ module ActiveSanction
168
181
  Configuration.sync_concurrency!(concurrency || ActiveSanction.config.sync_concurrency), Integer
169
182
  )
170
183
  @logger = T.let(logger, T.untyped)
184
+ @instrumenter = T.let(instrumenter, T.untyped)
171
185
  @lock = T.let(Mutex.new, Mutex)
172
186
  # The settings this run was started under, so a worker thread reads them
173
187
  # rather than the default client's. A configuration is fiber-local and a
@@ -195,9 +209,17 @@ module ActiveSanction
195
209
  started_at = Time.now.utc
196
210
  began = monotonic
197
211
  log_start
198
- work = keys.each_with_index.map { |key, at| [at, key, sources.fetch(at)] }
199
- results = run(work, &block).sort_by(&:first).map(&:last)
200
- report = Report.new(results: results, started_at: started_at, duration: elapsed(began))
212
+ # The run's own duration is the Report's, taken from the same clock
213
+ # reading, so an event and the report a caller is holding never disagree
214
+ # about how long a run took.
215
+ report = Instrumentation.instrument(instrumenter, :sync,
216
+ { sources: keys, forced: force, concurrency: concurrency }) do |event|
217
+ work = keys.each_with_index.map { |key, at| [at, key, sources.fetch(at)] }
218
+ results = run(work, &block).sort_by(&:first).map(&:last)
219
+ finished = Report.new(results: results, started_at: started_at, duration: elapsed(began))
220
+ summarize(event, finished)
221
+ finished
222
+ end
201
223
  log_finish(report)
202
224
  report
203
225
  end
@@ -285,13 +307,39 @@ module ActiveSanction
285
307
  snapshot = adapter.sync(force: force || previous.nil?)
286
308
  return complete(key, :unchanged, previous, started) if unchanged?(previous, snapshot)
287
309
 
288
- store.write_snapshot(snapshot)
310
+ write(key, snapshot)
289
311
  complete(key, :updated, Storage::Meta.from_snapshot(snapshot), started)
290
312
  rescue StandardError => e
291
313
  complete(key, :failed, previous, started, stamp(key, e))
292
314
  end
293
315
  end
294
316
 
317
+ # Writes one list, and says what it cost. Separate from the rest of
318
+ # #sync_source so the `:store` event times the write and nothing else --
319
+ # a store that takes eleven seconds to persist 19,321 entities is a
320
+ # different operational problem from a publisher that takes eleven seconds
321
+ # to serve them, and a timing that covered both could not tell a host
322
+ # which one it had.
323
+ sig { params(key: Symbol, snapshot: T.untyped).void }
324
+ def write(key, snapshot)
325
+ fields = { source: key, snapshot_id: snapshot.checksum, entities: snapshot.record_count,
326
+ store: store.class.name }
327
+ Instrumentation.instrument(instrumenter, :store, fields) { store.write_snapshot(snapshot) }
328
+ end
329
+
330
+ # What a run did, as counts rather than as the Report itself: a subscriber
331
+ # forwarding an event to a metrics backend wants numbers, and one that
332
+ # wants the whole report already has it as the return value of the call
333
+ # that emitted this.
334
+ sig { params(event: T.untyped, report: Report).void }
335
+ def summarize(event, report)
336
+ event[:outcomes] = report.results.to_h { |result| [result.source, result.status] }
337
+ event[:updated] = report.updated.size
338
+ event[:unchanged] = report.unchanged.size
339
+ event[:failed] = report.failed.size
340
+ event[:records] = report.record_count
341
+ end
342
+
295
343
  # A failure captured for a source names that source, even when it was
296
344
  # raised somewhere that could not know -- a store that will not open, an
297
345
  # adapter constructor. Only ever fills a blank; see Error#in_source.
@@ -6,7 +6,7 @@ module ActiveSanction
6
6
  # new source adapter, a storage fix or a documentation release -- none of
7
7
  # which change what a name scores. MATCHER_VERSION, below, is the one that
8
8
  # answers that question.
9
- VERSION = "1.0.1"
9
+ VERSION = "1.1.0"
10
10
 
11
11
  # Which matching pipeline scored a decision, stamped onto every MatchResult
12
12
  # and bumped whenever a change to the normalizer, the index, the similarity
@@ -6,6 +6,7 @@ require "sorbet-runtime"
6
6
  require "active_sanction/error"
7
7
  require "active_sanction/version"
8
8
  require "active_sanction/deprecation"
9
+ require "active_sanction/instrumentation"
9
10
  require "active_sanction/configuration"
10
11
  require "active_sanction/name"
11
12
  require "active_sanction/address"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_sanction
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marshall Shen
@@ -102,6 +102,9 @@ files:
102
102
  - lib/active_sanction/index/candidate.rb
103
103
  - lib/active_sanction/index/entry.rb
104
104
  - lib/active_sanction/index/features.rb
105
+ - lib/active_sanction/instrumentation.rb
106
+ - lib/active_sanction/instrumentation/event.rb
107
+ - lib/active_sanction/instrumentation/notifications.rb
105
108
  - lib/active_sanction/match_result.rb
106
109
  - lib/active_sanction/matcher.rb
107
110
  - lib/active_sanction/name.rb