truffler 0.1.4 → 0.1.6
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +79 -12
- data/lib/generators/truffler/install/templates/migration.rb.tt +14 -11
- data/lib/generators/truffler/upgrade/templates/backfill_spends_migration.rb.tt +1 -1
- data/lib/generators/truffler/upgrade/templates/backfill_spends_tenant_key_migration.rb.tt +25 -0
- data/lib/generators/truffler/upgrade/templates/labels_search_covering_migration.rb.tt +32 -0
- data/lib/generators/truffler/upgrade/upgrade_generator.rb +49 -3
- data/lib/tasks/truffler.rake +28 -13
- data/lib/truffler/benchmark/runner.rb +1 -1
- data/lib/truffler/clients/evaluator.rb +44 -0
- data/lib/truffler/configuration.rb +12 -0
- data/lib/truffler/current.rb +30 -0
- data/lib/truffler/definition.rb +52 -0
- data/lib/truffler/embeddings/backfill.rb +40 -11
- data/lib/truffler/embeddings/label_vector.rb +1 -1
- data/lib/truffler/embeddings/neighbor_store.rb +33 -7
- data/lib/truffler/embeddings/vector_store.rb +9 -3
- data/lib/truffler/jobs/backfill_job.rb +14 -5
- data/lib/truffler/jobs/embed_job.rb +2 -0
- data/lib/truffler/jobs/label_flush_job.rb +4 -2
- data/lib/truffler/jobs/resume_job.rb +22 -9
- data/lib/truffler/label_definition.rb +17 -3
- data/lib/truffler/labeling/backfill.rb +115 -37
- data/lib/truffler/labeling/labeler.rb +58 -41
- data/lib/truffler/labeling/queue.rb +14 -9
- data/lib/truffler/labeling/supplied.rb +16 -5
- data/lib/truffler/lenses/backfill.rb +28 -10
- data/lib/truffler/model.rb +5 -0
- data/lib/truffler/providers/backup.rb +1 -1
- data/lib/truffler/query_encoding/encoder.rb +75 -49
- data/lib/truffler/query_encoding/present_options.rb +54 -0
- data/lib/truffler/records/backfill_spend.rb +60 -6
- data/lib/truffler/redaction.rb +1 -1
- data/lib/truffler/search/encoding.rb +41 -10
- data/lib/truffler/search/encoding_cache.rb +12 -1
- data/lib/truffler/search/filler.rb +31 -6
- data/lib/truffler/search/keystroke.rb +60 -29
- data/lib/truffler/search/relaxation.rb +95 -0
- data/lib/truffler/search/result.rb +21 -5
- data/lib/truffler/search/sql.rb +81 -10
- data/lib/truffler/smart_search/dispatcher.rb +36 -24
- data/lib/truffler/smart_search/reranker.rb +7 -5
- data/lib/truffler/smart_search/run.rb +10 -3
- data/lib/truffler/smart_search/starter.rb +13 -11
- data/lib/truffler/version.rb +1 -1
- data/lib/truffler/vocabulary.rb +10 -0
- metadata +7 -1
|
@@ -2,20 +2,28 @@ module Truffler
|
|
|
2
2
|
module Labeling
|
|
3
3
|
# Relabels one model's records whose labels are missing, stale (labeled
|
|
4
4
|
# under another vocabulary version), failed, or demoted to backfill
|
|
5
|
-
# priority. It walks newest-first below an id cursor
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# and
|
|
5
|
+
# priority. It walks newest-first below an id cursor over the
|
|
6
|
+
# definition's index_scope (one tenant's records with `tenant_key:`),
|
|
7
|
+
# splits each page by tenant, skips disabled tenants, packs batch_size
|
|
8
|
+
# records per request at backfill priority, and asks only the stale
|
|
9
|
+
# questions. Live pending rows belong to the flush job and are never
|
|
10
|
+
# touched.
|
|
9
11
|
#
|
|
10
12
|
# Resumable: the cursor moves past a page only once the whole page is
|
|
11
13
|
# done, and records already current are skipped, so a rerun never asks
|
|
12
14
|
# Jev about them again. A spend cap stops the run before a request would
|
|
13
15
|
# exceed it; host-supplied labels cost nothing, so they are still written
|
|
14
16
|
# once the cap is reached. The cap counts everything spent under the
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
17
|
+
# current ledger version (the asked labels' fingerprints, so supplied
|
|
18
|
+
# labels never reset it) in the truffler_backfill_spends ledger, so
|
|
19
|
+
# reruns and overlapping jobs share it. Tenant-scoped models keep one
|
|
20
|
+
# ledger per tenant (backfill_spend_cap_scope :tenant, the default), so
|
|
21
|
+
# the cap applies to each tenant: a whole-model run skips a tenant at its
|
|
22
|
+
# cap, keeps labeling the others, and ends with `:spend_cap_reached`; a
|
|
23
|
+
# tenant run stops there. Unscoped models, and :app, keep one app-wide
|
|
24
|
+
# ledger. Without that table the cap falls back to this run plus
|
|
25
|
+
# `spent:`. A Jev error releases the claimed rows and ends the run with
|
|
26
|
+
# `:client_error`, so the caller keeps the spend metered so far.
|
|
19
27
|
#
|
|
20
28
|
# A budget denial ends the run with `:budget_denied`, unless the run
|
|
21
29
|
# waits: then it backs off (see .backoff) and retries from the same
|
|
@@ -94,29 +102,46 @@ module Truffler
|
|
|
94
102
|
end
|
|
95
103
|
end
|
|
96
104
|
|
|
97
|
-
def self.status(model)
|
|
98
|
-
new(model).status
|
|
105
|
+
def self.status(model, tenant_key: nil)
|
|
106
|
+
new(model, tenant_key: tenant_key).status
|
|
99
107
|
end
|
|
100
108
|
|
|
101
|
-
# The ledger row for the
|
|
102
|
-
# when nothing was spent yet or the ledger table
|
|
103
|
-
|
|
109
|
+
# The ledger row for the current ledger version (the tenant's, for a
|
|
110
|
+
# tenant ledger), or nil when nothing was spent yet or the ledger table
|
|
111
|
+
# is missing. A row still keyed by the pre-0.1.6 vocabulary version
|
|
112
|
+
# counts until a backfill takes it over.
|
|
113
|
+
def self.spend(model, tenant_key: nil)
|
|
104
114
|
return unless Records::BackfillSpend.available?
|
|
105
115
|
|
|
106
|
-
Records::BackfillSpend.
|
|
116
|
+
ledgers = Records::BackfillSpend.for_ledger(model, tenant_key)
|
|
117
|
+
ledgers.find_by(vocabulary_version: ledger_version(model, tenant_key)) ||
|
|
118
|
+
ledgers.find_by(vocabulary_version: legacy_ledger_version(model, tenant_key))
|
|
107
119
|
end
|
|
108
120
|
|
|
109
121
|
# Zeroes the current vocabulary version's ledger in place, so a chain
|
|
110
122
|
# still running keeps its row and continues against the fresh total.
|
|
111
|
-
|
|
123
|
+
# `all_tenants: true` zeroes every tenant ledger of the model.
|
|
124
|
+
def self.reset_spend!(model, tenant_key: nil, all_tenants: false)
|
|
112
125
|
return unless Records::BackfillSpend.available?
|
|
113
126
|
|
|
114
|
-
Records::BackfillSpend.
|
|
115
|
-
.
|
|
127
|
+
ledgers = if all_tenants && Records::BackfillSpend.tenant_ledgers?
|
|
128
|
+
Records::BackfillSpend.for_model(model).where.not(tenant_key: nil)
|
|
129
|
+
else
|
|
130
|
+
Records::BackfillSpend.for_ledger(model, tenant_key)
|
|
131
|
+
.where(vocabulary_version: [ ledger_version(model, tenant_key), legacy_ledger_version(model, tenant_key) ])
|
|
132
|
+
end
|
|
133
|
+
ledgers.update_all(spent_usd: 0.0, requests: 0, updated_at: Time.current)
|
|
116
134
|
end
|
|
117
135
|
|
|
118
|
-
|
|
119
|
-
|
|
136
|
+
# Spend ledgers key on the asked labels only (Vocabulary#ledger_version),
|
|
137
|
+
# so a supplied-label change never resets the cap.
|
|
138
|
+
def self.ledger_version(model, tenant_key = nil)
|
|
139
|
+
model.truffler_definition.vocabulary.ledger_version(tenant_key: tenant_key, all_users: true)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# What ledgers were keyed by before 0.1.6: the whole vocabulary version.
|
|
143
|
+
def self.legacy_ledger_version(model, tenant_key = nil)
|
|
144
|
+
model.truffler_definition.vocabulary.version(tenant_key: tenant_key, all_users: true)
|
|
120
145
|
end
|
|
121
146
|
|
|
122
147
|
# Seconds to wait after `denials` consecutive budget denials with no
|
|
@@ -128,9 +153,10 @@ module Truffler
|
|
|
128
153
|
|
|
129
154
|
attr_reader :model, :batch_size, :page_size
|
|
130
155
|
|
|
131
|
-
def initialize(model, spend_cap: Truffler.config.backfill_spend_cap, batch_size: Truffler.config.batch_size,
|
|
156
|
+
def initialize(model, tenant_key: nil, spend_cap: Truffler.config.backfill_spend_cap, batch_size: Truffler.config.batch_size,
|
|
132
157
|
page_size: nil, cursor: nil, spent: 0.0, client: Truffler.config.client, budget: Budget.new)
|
|
133
158
|
@model = model
|
|
159
|
+
@tenant_key = tenant_key&.to_s if model.truffler_definition.scoped?
|
|
134
160
|
model.truffler_definition.validate_columns!
|
|
135
161
|
@batch_size = batch_size
|
|
136
162
|
@page_size = page_size || batch_size * 5
|
|
@@ -140,6 +166,9 @@ module Truffler
|
|
|
140
166
|
@client = client
|
|
141
167
|
@budget = budget
|
|
142
168
|
@versions = {}
|
|
169
|
+
@meters = {}
|
|
170
|
+
@enabled = {}
|
|
171
|
+
@capped = Set.new
|
|
143
172
|
end
|
|
144
173
|
|
|
145
174
|
# `progress` is called with the result so far and the delay before each
|
|
@@ -147,17 +176,17 @@ module Truffler
|
|
|
147
176
|
def run(max_pages: nil, wait: false, max_duration: nil, sleeper: self.class.sleeper, clock: self.class.clock,
|
|
148
177
|
progress: nil)
|
|
149
178
|
@labeled = 0
|
|
150
|
-
@started_cost =
|
|
179
|
+
@started_cost = spent_cost
|
|
151
180
|
@pages = 0
|
|
152
181
|
deadline = max_duration && clock.call + max_duration
|
|
153
182
|
denials = 0
|
|
154
183
|
|
|
155
184
|
loop do
|
|
156
|
-
before = [ @labeled,
|
|
185
|
+
before = [ @labeled, requests ]
|
|
157
186
|
status = sweep(max_pages, deadline, clock)
|
|
158
187
|
return result(status) unless wait && status == :budget_denied
|
|
159
188
|
|
|
160
|
-
denials = 0 unless before == [ @labeled,
|
|
189
|
+
denials = 0 unless before == [ @labeled, requests ]
|
|
161
190
|
delay = self.class.backoff(denials, @retry_after)
|
|
162
191
|
return result(:paused) if deadline && clock.call + delay > deadline
|
|
163
192
|
|
|
@@ -167,11 +196,16 @@ module Truffler
|
|
|
167
196
|
end
|
|
168
197
|
end
|
|
169
198
|
|
|
199
|
+
# Counts over what the backfill may touch: index_scope, enabled tenants,
|
|
200
|
+
# and the one tenant when `tenant_key:` is given.
|
|
170
201
|
def status
|
|
171
|
-
|
|
172
|
-
|
|
202
|
+
scope, tenants = status_scope
|
|
203
|
+
tracked = states.where(record_id: scope.select(model.arel_table[model.primary_key]))
|
|
204
|
+
tracked = tracked.where(tenant_key: tenants) if tenants
|
|
205
|
+
counts = tracked.group(:status).count
|
|
206
|
+
labeled = tracked.where(status: "labeled").group(:tenant_key, :vocabulary_version).count
|
|
173
207
|
stale = labeled.sum { |(tenant_key, version), count| version == version_for(tenant_key) ? 0 : count }
|
|
174
|
-
{ total:
|
|
208
|
+
{ total: scope.count, missing: scope.joins(state_join).where("#{STATES}.id IS NULL").count,
|
|
175
209
|
pending: counts["pending"].to_i, labeling: counts["labeling"].to_i, labeled: counts["labeled"].to_i,
|
|
176
210
|
failed: counts["failed"].to_i, stale: stale, current: labeled.values.sum - stale }
|
|
177
211
|
end
|
|
@@ -190,30 +224,67 @@ module Truffler
|
|
|
190
224
|
Records::RecordState.for_model(model)
|
|
191
225
|
end
|
|
192
226
|
|
|
227
|
+
# [relation, tenant keys or nil]: the records status counts, and the
|
|
228
|
+
# tenants it covers when it narrows to some.
|
|
229
|
+
def status_scope
|
|
230
|
+
scope = definition.index_relation(model.all)
|
|
231
|
+
return [ scope, nil ] unless definition.scoped?
|
|
232
|
+
return [ scope.where(definition.tenant_column => @tenant_key), [ @tenant_key ] ] if @tenant_key
|
|
233
|
+
return [ scope, nil ] unless Truffler.config.tenant_enabled
|
|
234
|
+
|
|
235
|
+
tenants = scope.reorder(nil).distinct.pluck(definition.tenant_column).map(&:to_s).select { |tenant_key| enabled?(tenant_key) }
|
|
236
|
+
[ scope.where(definition.tenant_column => tenants), tenants ]
|
|
237
|
+
end
|
|
238
|
+
|
|
193
239
|
def queue
|
|
194
240
|
@queue ||= Queue.new(model)
|
|
195
241
|
end
|
|
196
242
|
|
|
197
|
-
#
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
243
|
+
# One meter per ledger: per tenant for tenant ledgers, else one for the
|
|
244
|
+
# run. Spend carried in with `spent:` is ignored when the ledger holds it.
|
|
245
|
+
def meter(tenant_key)
|
|
246
|
+
ledger_tenant = ledger_available? ? definition.ledger_tenant(tenant_key) : nil
|
|
247
|
+
@meters[ledger_tenant] ||= begin
|
|
248
|
+
if ledger_available?
|
|
249
|
+
ledger = Records::BackfillSpend.ledger(model, self.class.ledger_version(model, ledger_tenant), tenant_key: ledger_tenant,
|
|
250
|
+
legacy_version: version_for(ledger_tenant))
|
|
251
|
+
end
|
|
201
252
|
SpendMeter.new(@client, cap: @spend_cap, spent: ledger ? 0.0 : @spent, ledger: ledger)
|
|
202
253
|
end
|
|
203
254
|
end
|
|
204
255
|
|
|
256
|
+
def ledger_available?
|
|
257
|
+
return @ledger_available if defined?(@ledger_available)
|
|
258
|
+
|
|
259
|
+
@ledger_available = Records::BackfillSpend.available?
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def requests
|
|
263
|
+
@meters.each_value.sum(&:requests)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def spent_cost
|
|
267
|
+
@meters.each_value.sum(&:cost)
|
|
268
|
+
end
|
|
269
|
+
|
|
205
270
|
def version_for(tenant_key)
|
|
206
271
|
@versions[tenant_key] ||= definition.vocabulary.version(tenant_key: tenant_key, all_users: true)
|
|
207
272
|
end
|
|
208
273
|
|
|
274
|
+
def enabled?(tenant_key)
|
|
275
|
+
@enabled.fetch(tenant_key) { @enabled[tenant_key] = definition.tenant_enabled?(tenant_key) }
|
|
276
|
+
end
|
|
277
|
+
|
|
209
278
|
def result(status)
|
|
210
|
-
Result.new(status: status, labeled: @labeled, requests:
|
|
279
|
+
Result.new(status: status, labeled: @labeled, requests: requests, cost: spent_cost - @started_cost,
|
|
211
280
|
cursor: @cursor, retry_after: (@retry_after if status == :budget_denied))
|
|
212
281
|
end
|
|
213
282
|
|
|
214
283
|
# Walks pages below @cursor until done or stopped, returning the status.
|
|
215
284
|
def sweep(max_pages, deadline, clock)
|
|
216
285
|
@retry_after = nil
|
|
286
|
+
return complete if @tenant_key && !enabled?(@tenant_key)
|
|
287
|
+
|
|
217
288
|
loop do
|
|
218
289
|
scanned, rows = page(@cursor)
|
|
219
290
|
return complete if scanned.empty?
|
|
@@ -221,6 +292,8 @@ module Truffler
|
|
|
221
292
|
|
|
222
293
|
rows.group_by(&:last).each do |tenant_key, tenant_rows|
|
|
223
294
|
tenant_rows.map(&:first).each_slice(batch_size) do |ids|
|
|
295
|
+
break if @capped.include?(tenant_key)
|
|
296
|
+
|
|
224
297
|
stop = label(ids, tenant_key)
|
|
225
298
|
return stop if stop
|
|
226
299
|
end
|
|
@@ -234,7 +307,7 @@ module Truffler
|
|
|
234
307
|
|
|
235
308
|
def complete
|
|
236
309
|
@cursor = nil
|
|
237
|
-
:complete
|
|
310
|
+
@capped.any? ? :spend_cap_reached : :complete
|
|
238
311
|
end
|
|
239
312
|
|
|
240
313
|
# Returns the scanned ids (for the cursor) and the [id, tenant_key] rows
|
|
@@ -242,7 +315,8 @@ module Truffler
|
|
|
242
315
|
# lenses) compare versions here because each tenant has its own.
|
|
243
316
|
def page(cursor)
|
|
244
317
|
pk = model.primary_key
|
|
245
|
-
scope = model.joins(state_join).where(needs_labeling_sql)
|
|
318
|
+
scope = definition.index_relation(model.joins(state_join).where(needs_labeling_sql))
|
|
319
|
+
scope = scope.where(definition.tenant_column => @tenant_key) if @tenant_key
|
|
246
320
|
scope = scope.where(model.arel_table[pk].lt(cursor)) if cursor
|
|
247
321
|
tenant = definition.scoped? ? model.arel_table[definition.tenant_column] : Arel.sql("NULL")
|
|
248
322
|
plucked = scope.reorder(pk => :desc).limit(page_size)
|
|
@@ -250,7 +324,7 @@ module Truffler
|
|
|
250
324
|
|
|
251
325
|
rows = plucked.filter_map do |id, tenant_key, status, version|
|
|
252
326
|
tenant_key = tenant_key&.to_s
|
|
253
|
-
[ id, tenant_key ] unless status == "labeled" && version == version_for(tenant_key)
|
|
327
|
+
[ id, tenant_key ] unless (status == "labeled" && version == version_for(tenant_key)) || !enabled?(tenant_key)
|
|
254
328
|
end
|
|
255
329
|
[ plucked.map(&:first), rows ]
|
|
256
330
|
end
|
|
@@ -277,16 +351,20 @@ module Truffler
|
|
|
277
351
|
|
|
278
352
|
# Labels one tenant chunk. Returns nil when done, or the status that
|
|
279
353
|
# stops the run; claimed rows that were not labeled go back to pending
|
|
280
|
-
# at backfill priority.
|
|
354
|
+
# at backfill priority. A tenant ledger at its cap only stops that
|
|
355
|
+
# tenant in a whole-model run.
|
|
281
356
|
def label(ids, tenant_key)
|
|
282
357
|
claimed = queue.claim_backfill(ids, tenant_key)
|
|
283
358
|
return if claimed.empty?
|
|
284
359
|
|
|
285
360
|
begin
|
|
286
|
-
Labeler.new(model, client: meter, budget: @budget).label(claimed, priority: :backfill)
|
|
361
|
+
Labeler.new(model, client: meter(tenant_key), budget: @budget).label(claimed, priority: :backfill)
|
|
287
362
|
rescue SpendCapReached
|
|
288
363
|
queue.demote(claimed)
|
|
289
|
-
return :spend_cap_reached
|
|
364
|
+
return :spend_cap_reached if @tenant_key || definition.ledger_tenant(tenant_key).nil? || !ledger_available?
|
|
365
|
+
|
|
366
|
+
@capped << tenant_key
|
|
367
|
+
return
|
|
290
368
|
rescue BudgetExhausted => error
|
|
291
369
|
queue.demote(claimed)
|
|
292
370
|
@retry_after = error.retry_after
|
|
@@ -20,46 +20,48 @@ module Truffler
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def label(states, priority:)
|
|
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
|
-
|
|
23
|
+
Current.scope do
|
|
24
|
+
return Result.new(labeled: 0, requests: 0, cost: 0.0, demoted: false) if states.empty?
|
|
25
|
+
|
|
26
|
+
tenant_key = tenant_key_of(states)
|
|
27
|
+
records = load_records(states, tenant_key)
|
|
28
|
+
vocabulary = definition.vocabulary
|
|
29
|
+
@labels = vocabulary.labels_for(tenant_key: tenant_key, all_users: true)
|
|
30
|
+
fingerprints = vocabulary.fingerprints(tenant_key: tenant_key, all_users: true)
|
|
31
|
+
version = vocabulary.version(tenant_key: tenant_key, all_users: true)
|
|
32
|
+
states_by_id = states.index_by { |state| state.record_id.to_s }
|
|
33
|
+
|
|
34
|
+
stored = stored_fingerprints(records)
|
|
35
|
+
askable = askable_labels
|
|
36
|
+
stale = records.map { |record| [ record, stale_keys(stored[record.id.to_s].to_h, fingerprints, tenant_key, askable) ] }
|
|
37
|
+
supplied = askable.select(&:supplied?).map(&:key)
|
|
38
|
+
supplier = Supplied.new(model)
|
|
39
|
+
written = supplier.write(stale.map { |record, keys| [ record, keys & supplied ] }, tenant_key: tenant_key)
|
|
40
|
+
current, pending = stale.map { |record, keys| [ record, keys - supplied ] }.partition { |_, keys| keys.empty? }
|
|
41
|
+
settled = current.reject { |record, _| supplier.failed_ids.include?(record.id) }
|
|
42
|
+
Records::RecordState.mark_labeled(settled.map { |record, _| states_by_id[record.id.to_s].id }, version: version)
|
|
43
|
+
Embeddings::LabelVector.new(model).write(current.map { |record, _| record.id } - written, tenant_key: tenant_key)
|
|
44
|
+
|
|
45
|
+
requests = RequestBuilder.new(definition, tenant_key: tenant_key, labels: @labels).build(pending)
|
|
46
|
+
cost = 0.0
|
|
47
|
+
requests.each_with_index do |request, index|
|
|
48
|
+
decision = budget.acquire(priority: priority, tenant_key: (tenant_key if index.zero?),
|
|
49
|
+
records: index.zero? ? pending.size : 1)
|
|
50
|
+
if decision.demoted?
|
|
51
|
+
retry_failed_supplied(supplier.failed_ids, states_by_id)
|
|
52
|
+
return Result.new(labeled: current.size, requests: index, cost: cost, demoted: true)
|
|
53
|
+
end
|
|
54
|
+
raise BudgetExhausted.new("no Jev budget for #{priority} labeling", retry_after: decision.retry_after) if decision.denied?
|
|
55
|
+
|
|
56
|
+
answers = client.ask(state: request.state, questions: request.questions, priority: decision.priority)
|
|
57
|
+
cost += answers.usage&.cost.to_f
|
|
58
|
+
charge_lenses(request, answers.usage&.cost.to_f)
|
|
59
|
+
store(request, answers, fingerprints, tenant_key, version, states_by_id)
|
|
52
60
|
end
|
|
53
|
-
raise BudgetExhausted.new("no Jev budget for #{priority} labeling", retry_after: decision.retry_after) if decision.denied?
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
charge_lenses(request, answers.usage&.cost.to_f)
|
|
58
|
-
store(request, answers, fingerprints, tenant_key, version, states_by_id)
|
|
62
|
+
retry_failed_supplied(supplier.failed_ids, states_by_id)
|
|
63
|
+
Result.new(labeled: records.size, requests: requests.size, cost: cost, demoted: false)
|
|
59
64
|
end
|
|
60
|
-
|
|
61
|
-
retry_failed_supplied(supplier.failed_ids, states_by_id)
|
|
62
|
-
Result.new(labeled: records.size, requests: requests.size, cost: cost, demoted: false)
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
private
|
|
@@ -87,17 +89,31 @@ module Truffler
|
|
|
87
89
|
tenants.first
|
|
88
90
|
end
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
# Records deleted or outside index_scope / index_if lose their state
|
|
93
|
+
# rows instead of being labeled. A disabled tenant keeps its state rows:
|
|
94
|
+
# they go back to pending at backfill priority, so re-enabling it
|
|
95
|
+
# relabels only what is stale instead of every record.
|
|
96
|
+
def load_records(states, tenant_key)
|
|
97
|
+
unless definition.tenant_enabled?(tenant_key)
|
|
98
|
+
Records::RecordState.where(id: states.map(&:id))
|
|
99
|
+
.update_all(status: "pending", priority: "backfill", claimed_at: nil, updated_at: Time.current)
|
|
100
|
+
return []
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
records = definition.index_relation(model.where(model.primary_key => states.map(&:record_id))).to_a
|
|
104
|
+
.select { |record| definition.index_if.nil? || definition.index_if.call(record) }
|
|
92
105
|
found = records.map { |record| record.id.to_s }
|
|
93
106
|
gone = states.reject { |state| found.include?(state.record_id.to_s) }
|
|
94
107
|
Records::RecordState.where(id: gone.map(&:id)).delete_all if gone.any?
|
|
95
108
|
records
|
|
96
109
|
end
|
|
97
110
|
|
|
111
|
+
# A label is current when it has stored rows and all carry its current
|
|
112
|
+
# fingerprint; choice labels store only some options (see sparse_choice).
|
|
98
113
|
def stale_keys(stored, fingerprints, tenant_key, askable)
|
|
99
114
|
askable.reject do |label|
|
|
100
|
-
label.storage_keys(tenant_key).
|
|
115
|
+
present = label.storage_keys(tenant_key).select { |key| stored.key?(key) }
|
|
116
|
+
present.any? && present.all? { |key| stored[key] == fingerprints[label.key] }
|
|
101
117
|
end.map(&:key)
|
|
102
118
|
end
|
|
103
119
|
|
|
@@ -154,7 +170,8 @@ module Truffler
|
|
|
154
170
|
case label.type
|
|
155
171
|
when :noul then [ [ label.key, answers.noul(id) ] ]
|
|
156
172
|
when :score then [ [ label.key, answers.score(id) ] ]
|
|
157
|
-
when :choice
|
|
173
|
+
when :choice
|
|
174
|
+
LabelDefinition.sparse_choice(label.options(tenant_key).keys.to_h { |option| [ "#{label.key}:#{option}", answers.probability(id, option) ] }).to_a
|
|
158
175
|
end
|
|
159
176
|
end
|
|
160
177
|
end
|
|
@@ -3,6 +3,8 @@ module Truffler
|
|
|
3
3
|
# State transitions on truffler_record_states for one model, plus the
|
|
4
4
|
# deduplicated scheduling of LabelFlushJob per tenant and of BackfillJob
|
|
5
5
|
# for demoted rows. Jobs carry only the record type and tenant key.
|
|
6
|
+
# Records the definition does not index (a disabled tenant, `index_if`)
|
|
7
|
+
# are never enqueued.
|
|
6
8
|
class Queue
|
|
7
9
|
BACKFILL_WAIT = 1.minute
|
|
8
10
|
|
|
@@ -15,13 +17,15 @@ module Truffler
|
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def enqueue(record, priority: :live)
|
|
20
|
+
return unless definition.indexable?(record)
|
|
21
|
+
|
|
18
22
|
tenant_key = definition.tenant_key_for(record)
|
|
19
23
|
now = Time.current
|
|
20
24
|
Records::RecordState.upsert(
|
|
21
25
|
{ record_type: record_type, record_id: record.id, tenant_key: tenant_key, status: "pending",
|
|
22
26
|
priority: priority.to_s, attempts: 0, created_at: now, updated_at: now },
|
|
23
27
|
unique_by: %i[record_type record_id],
|
|
24
|
-
update_only: %i[tenant_key status priority attempts
|
|
28
|
+
update_only: %i[tenant_key status priority attempts]
|
|
25
29
|
)
|
|
26
30
|
schedule(tenant_key)
|
|
27
31
|
end
|
|
@@ -41,13 +45,14 @@ module Truffler
|
|
|
41
45
|
job.perform_later(record_type, tenant_key)
|
|
42
46
|
end
|
|
43
47
|
|
|
44
|
-
# Starts a backfill shortly after live rows are
|
|
45
|
-
# cap. One per
|
|
46
|
-
# dropped job leaves behind.
|
|
47
|
-
def schedule_backfill
|
|
48
|
-
return unless
|
|
48
|
+
# Starts a backfill of the tenant shortly after its live rows are
|
|
49
|
+
# demoted over the tenant cap. One per tenant per marker lifetime;
|
|
50
|
+
# ResumeJob still catches rows a dropped job leaves behind.
|
|
51
|
+
def schedule_backfill(tenant_key = nil)
|
|
52
|
+
return unless definition.tenant_enabled?(tenant_key)
|
|
53
|
+
return unless config.cache_store.write(backfill_marker(tenant_key), true, unless_exist: true, expires_in: BACKFILL_WAIT + 300)
|
|
49
54
|
|
|
50
|
-
Jobs::BackfillJob.set(wait: BACKFILL_WAIT).perform_later(record_type)
|
|
55
|
+
Jobs::BackfillJob.set(wait: BACKFILL_WAIT).perform_later(record_type, **Jobs::BackfillJob.tenant_argument(model, tenant_key))
|
|
51
56
|
end
|
|
52
57
|
|
|
53
58
|
def clear_marker(tenant_key)
|
|
@@ -118,8 +123,8 @@ module Truffler
|
|
|
118
123
|
"truffler/flush/#{record_type}/#{tenant_key}"
|
|
119
124
|
end
|
|
120
125
|
|
|
121
|
-
def backfill_marker
|
|
122
|
-
"truffler/backfill/#{record_type}"
|
|
126
|
+
def backfill_marker(tenant_key)
|
|
127
|
+
"truffler/backfill/#{record_type}/#{tenant_key}"
|
|
123
128
|
end
|
|
124
129
|
end
|
|
125
130
|
end
|
|
@@ -4,9 +4,13 @@ module Truffler
|
|
|
4
4
|
# of one tenant into truffler_labels, under the same storage keys as asked
|
|
5
5
|
# labels, and rewrites those records' label vectors. No Jev request, no
|
|
6
6
|
# budget slot, no spend. A nil answer stores nothing, so the label reads
|
|
7
|
-
# as missing rather than 0.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# as missing rather than 0. Both failures are instrumented as
|
|
8
|
+
# `supplied_label_failed`. An answer out of shape (InvalidSuppliedAnswer)
|
|
9
|
+
# is permanent (`permanent: true`): it stores nothing for that label,
|
|
10
|
+
# like a nil answer, and the record settles, since retrying the same
|
|
11
|
+
# answer cannot succeed. A `from` that raises anything else is transient:
|
|
12
|
+
# the label is skipped and listed in `failed_ids` so the caller retries
|
|
13
|
+
# it (failed after max_attempts); its stored rows serve meanwhile.
|
|
10
14
|
class Supplied
|
|
11
15
|
attr_reader :model, :failed_ids
|
|
12
16
|
|
|
@@ -61,11 +65,18 @@ module Truffler
|
|
|
61
65
|
|
|
62
66
|
def answer(label, record, tenant_key)
|
|
63
67
|
label.supplied_values(record, tenant_key)
|
|
68
|
+
rescue InvalidSuppliedAnswer => error
|
|
69
|
+
instrument_failure(label, record, tenant_key, error, permanent: true)
|
|
70
|
+
nil
|
|
64
71
|
rescue StandardError => error
|
|
65
|
-
|
|
66
|
-
label_key: label.key, error_class: error.class.name)
|
|
72
|
+
instrument_failure(label, record, tenant_key, error, permanent: false)
|
|
67
73
|
:failed
|
|
68
74
|
end
|
|
75
|
+
|
|
76
|
+
def instrument_failure(label, record, tenant_key, error, permanent:)
|
|
77
|
+
Instrumentation.instrument(:supplied_label_failed, record_type: record_type, tenant_key: tenant_key, record_id: record.id,
|
|
78
|
+
label_key: label.key, error_class: error.class.name, permanent: permanent)
|
|
79
|
+
end
|
|
69
80
|
end
|
|
70
81
|
end
|
|
71
82
|
end
|
|
@@ -37,6 +37,8 @@ module Truffler
|
|
|
37
37
|
|
|
38
38
|
attempted.concat(records.map(&:id))
|
|
39
39
|
records.group_by { |record| definition.tenant_key_for(record) }.each do |tenant_key, slice|
|
|
40
|
+
next unless definition.tenant_enabled?(tenant_key)
|
|
41
|
+
|
|
40
42
|
stop = label(slice, tenant_key)
|
|
41
43
|
return result(stop) if stop
|
|
42
44
|
end
|
|
@@ -70,22 +72,38 @@ module Truffler
|
|
|
70
72
|
# lens row.
|
|
71
73
|
def page(attempted)
|
|
72
74
|
pk = model.primary_key
|
|
73
|
-
scope = model.all
|
|
74
|
-
|
|
75
|
+
scope = definition.index_relation(model.all)
|
|
76
|
+
if definition.scoped? && lens.tenant_key
|
|
77
|
+
return [] unless definition.tenant_enabled?(lens.tenant_key)
|
|
78
|
+
|
|
79
|
+
scope = scope.where(definition.tenant_column => lens.tenant_key)
|
|
80
|
+
elsif definition.scoped? && Truffler.config.tenant_enabled
|
|
81
|
+
scope = scope.where(definition.tenant_column => enabled_tenants)
|
|
82
|
+
end
|
|
75
83
|
scope = scope.where.not(pk => attempted) if attempted.any?
|
|
76
84
|
scope.where(Arel.sql(stale_sql)).reorder(definition.arrival_order).limit(batch_size).to_a
|
|
77
85
|
end
|
|
78
86
|
|
|
87
|
+
# Tenants in index_scope that config.tenant_enabled allows, looked up
|
|
88
|
+
# once per run, so disabled tenants are never paged (and never refetched
|
|
89
|
+
# by the next LensBackfillJob).
|
|
90
|
+
def enabled_tenants
|
|
91
|
+
@enabled_tenants ||= definition.index_relation(model.all).reorder(nil).distinct.pluck(definition.tenant_column)
|
|
92
|
+
.select { |tenant_key| definition.tenant_enabled?(tenant_key.to_s) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Stale unless every lens label has a row under its current fingerprint;
|
|
96
|
+
# a choice label stores only some of its options.
|
|
79
97
|
def stale_sql
|
|
80
|
-
expected = lens.labels.values.flat_map do |label|
|
|
81
|
-
print = Lenses.fingerprint(label.question)
|
|
82
|
-
label.storage_keys.map { |key| ActiveRecord::Base.sanitize_sql_array([ "(label_key = ? AND fingerprint = ?)", key, print ]) }
|
|
83
|
-
end
|
|
84
98
|
pk = "#{model.quoted_table_name}.#{model.connection.quote_column_name(model.primary_key)}"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
99
|
+
current = lens.labels.values.map do |label|
|
|
100
|
+
ActiveRecord::Base.sanitize_sql_array([
|
|
101
|
+
"EXISTS (SELECT 1 FROM #{LABELS} WHERE #{LABELS}.record_type = ? AND #{LABELS}.record_id = #{pk} " \
|
|
102
|
+
"AND #{LABELS}.label_key IN (?) AND #{LABELS}.fingerprint = ?)",
|
|
103
|
+
model.polymorphic_name, label.storage_keys, Lenses.fingerprint(label.question)
|
|
104
|
+
])
|
|
105
|
+
end
|
|
106
|
+
"NOT (#{current.join(' AND ')})"
|
|
89
107
|
end
|
|
90
108
|
|
|
91
109
|
# Labels one tenant's records through the labeler, which asks only
|
data/lib/truffler/model.rb
CHANGED
|
@@ -62,6 +62,8 @@ module Truffler
|
|
|
62
62
|
# outside the watched columns, e.g. from the job that classified it.
|
|
63
63
|
def truffler_refresh_labels!
|
|
64
64
|
definition = self.class.truffler_definition
|
|
65
|
+
return self unless definition.indexable?(self)
|
|
66
|
+
|
|
65
67
|
tenant_key = definition.tenant_key_for(self)
|
|
66
68
|
keys = definition.supplied_labels.select { |label| label.available?(tenant_key) }.map(&:key)
|
|
67
69
|
Labeling::Supplied.new(self.class).write([ [ self, keys ] ], tenant_key: tenant_key) if keys.any?
|
|
@@ -72,6 +74,8 @@ module Truffler
|
|
|
72
74
|
|
|
73
75
|
def truffler_enqueue_labeling
|
|
74
76
|
definition = self.class.truffler_definition
|
|
77
|
+
return unless definition.indexable?(self)
|
|
78
|
+
|
|
75
79
|
if previously_new_record?
|
|
76
80
|
Labeling::Queue.new(self.class).enqueue(self)
|
|
77
81
|
return
|
|
@@ -105,6 +109,7 @@ module Truffler
|
|
|
105
109
|
definition = self.class.truffler_definition
|
|
106
110
|
return unless Embeddings.managed?(definition)
|
|
107
111
|
return unless previously_new_record? || saved_changes.keys.intersect?(definition.relabel_columns)
|
|
112
|
+
return unless definition.indexable?(self)
|
|
108
113
|
|
|
109
114
|
Jobs::EmbedJob.perform_later(self.class.polymorphic_name, id)
|
|
110
115
|
end
|
|
@@ -39,7 +39,7 @@ module Truffler
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def weak_local?
|
|
42
|
-
return @local_result.
|
|
42
|
+
return @local_result.local_weak? if @local_result
|
|
43
43
|
return @run.local_weak? if @run.respond_to?(:local_weak?)
|
|
44
44
|
unless @run.respond_to?(:candidate_ids)
|
|
45
45
|
raise ArgumentError, "Providers.start needs local_result: or a run that responds to local_weak? or candidate_ids"
|