Class: Greenroom::Recorder

Inherits:
Object
  • Object
show all
Defined in:
lib/greenroom/recorder.rb

Overview

The tallying half of a census run. A census job installs one of these on the thread it is running on; while it is installed, enabled? is true and comparisons flow into it instead of going nowhere. Uninstalling it turns comparisons back off, so the presence of a recorder is the one mechanism behind both "is the experiment running" and "where do the results go".

Defined Under Namespace

Classes: Tally

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(run_id:, experiment: nil, sample_limit: 100) ⇒ Recorder

Returns a new instance of Recorder.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/greenroom/recorder.rb', line 83

def initialize(run_id:, experiment: nil, sample_limit: 100)
  @run_id = run_id
  @sample_limit = sample_limit
  @rows_scanned = 0
  @row_errors = 0
  @tallies = {}
  @samples = []
  @subject = nil

  # A run that never triggers a single comparison (a module-only
  # include, or a target whose `call` never reaches `Scientist.run`)
  # would otherwise leave `tallies` empty and `flush_total` silent. A
  # silent run and a run that never started look identical from the
  # outside, so the zero must be sent, not inferred from an absence.
  tally_for(experiment) if experiment
end

Instance Attribute Details

#rows_scannedObject (readonly)

Returns the value of attribute rows_scanned.



80
81
82
# File 'lib/greenroom/recorder.rb', line 80

def rows_scanned
  @rows_scanned
end

#run_idObject (readonly)

Returns the value of attribute run_id.



80
81
82
# File 'lib/greenroom/recorder.rb', line 80

def run_id
  @run_id
end

#sample_limitObject (readonly)

Returns the value of attribute sample_limit.



80
81
82
# File 'lib/greenroom/recorder.rb', line 80

def sample_limit
  @sample_limit
end

#samplesObject (readonly)

Returns the value of attribute samples.



80
81
82
# File 'lib/greenroom/recorder.rb', line 80

def samples
  @samples
end

#subjectObject

Returns the value of attribute subject.



81
82
83
# File 'lib/greenroom/recorder.rb', line 81

def subject
  @subject
end

#talliesObject (readonly)

Returns the value of attribute tallies.



80
81
82
# File 'lib/greenroom/recorder.rb', line 80

def tallies
  @tallies
end

Class Method Details

.currentObject



51
52
53
# File 'lib/greenroom/recorder.rb', line 51

def current
  Thread.current[THREAD_KEY]
end

.install(recorder) ⇒ Object



55
56
57
# File 'lib/greenroom/recorder.rb', line 55

def install(recorder)
  Thread.current[THREAD_KEY] = recorder
end

.uninstallObject

Returns the recorder that was installed, so a caller that wants to flush it (on_stop) does not need to have held onto a reference itself.



62
63
64
65
66
# File 'lib/greenroom/recorder.rb', line 62

def uninstall
  recorder = Thread.current[THREAD_KEY]
  Thread.current[THREAD_KEY] = nil
  recorder
end

.with(recorder) ⇒ Object

For tests and any plain call site that wants a recorder installed for the duration of a block. ensure uninstalls even if the block raises, so a failing test cannot leave a recorder behind for the next one.



72
73
74
75
76
77
# File 'lib/greenroom/recorder.rb', line 72

def with(recorder)
  install(recorder)
  yield
ensure
  uninstall
end

Instance Method Details

#count_rowObject



100
101
102
# File 'lib/greenroom/recorder.rb', line 100

def count_row
  @rows_scanned += 1
end

#flush_segment(reporter) ⇒ Object

Sends one progress event (this run is alive, with the counts so far) and one mismatch event per sample gathered since the last flush. The samples that were just sent are discarded afterward: a sample is evidence, not part of the denominator, so re-sending it on resume (see Census::Cursor) is harmless, and keeping it around forever would only grow memory for no benefit.



158
159
160
161
162
# File 'lib/greenroom/recorder.rb', line 158

def flush_segment(reporter)
  emit(reporter, "GreenroomCensusProgress")
  @samples.each { |sample| reporter.record("GreenroomMismatch", sample) }
  @samples.clear
end

#flush_total(reporter) ⇒ Object

Sends the run's aggregate event: the one number a judge reads as the denominator. Splitting counts across flush_segment calls and asking a judge to sum them would make that sum itself something to verify for double-counting or gaps; a single event at the end avoids that.



168
169
170
# File 'lib/greenroom/recorder.rb', line 168

def flush_total(reporter)
  emit(reporter, "GreenroomCensus")
end

#record(result) ⇒ Object

Receives a Scientist::Result from publish. Counts are split into "mismatched" and "ignored" so that a host's ignore block, which exists to suppress noisy known-differences, does not also hide how much it is suppressing.



112
113
114
115
116
117
118
119
120
121
# File 'lib/greenroom/recorder.rb', line 112

def record(result)
  tally = tally_for(result.experiment_name)
  tally.comparisons += 1
  tally.mismatches += 1 if result.mismatched?
  tally.ignored += 1 if result.ignored?
  tally.candidate_errors += result.candidates.count(&:raised?)
  tally.control_errors += 1 if result.control&.raised?

  sample(result) if result.mismatched?
end

#record_row_error(_error) ⇒ Object



104
105
106
# File 'lib/greenroom/recorder.rb', line 104

def record_row_error(_error)
  @row_errors += 1
end

#restore(snapshot) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/greenroom/recorder.rb', line 131

def restore(snapshot)
  @rows_scanned = snapshot.fetch("rows_scanned", 0).to_i
  @row_errors = snapshot.fetch("row_errors", 0).to_i
  # Merges into whatever `@tallies` already holds instead of replacing
  # it outright. A resumed segment's `Recorder.new(experiment:)` seeds a
  # zero tally under that name (see the comment in `initialize`) before
  # `restore` ever runs; a snapshot taken before the first comparison
  # for that name does not carry it, and replacing `@tallies` wholesale
  # would erase the seeded zero and lose the name -- the run's later
  # `flush_total` would then report `experiment: nil` instead of the
  # name it was actually comparing.
  snapshot.fetch("tallies", {}).each do |name, counts|
    tally = tally_for(name)
    tally.comparisons = counts.fetch("comparisons", 0).to_i
    tally.mismatches = counts.fetch("mismatches", 0).to_i
    tally.ignored = counts.fetch("ignored", 0).to_i
    tally.candidate_errors = counts.fetch("candidate_errors", 0).to_i
    tally.control_errors = counts.fetch("control_errors", 0).to_i
  end
end

#snapshotObject



123
124
125
126
127
128
129
# File 'lib/greenroom/recorder.rb', line 123

def snapshot
  {
    "rows_scanned" => @rows_scanned,
    "row_errors" => @row_errors,
    "tallies" => @tallies.transform_values(&:to_h)
  }
end