truffler 0.1.3 → 0.1.5
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 +31 -0
- data/README.md +77 -10
- data/bench/cassettes/1037e0b63ba790c02d14d0ddfc18b68151c11d03ce88d764658044b6bab2e55b.json +1 -0
- data/bench/cassettes/19bba849a846d30a69dc385a337d67704fbe5194506fd3c914d3270d86495fde.json +1 -0
- data/bench/cassettes/3768847dc57df8bf2328771e7763c0b8bd013a167244e7849967dac144420601.json +1 -0
- data/lib/generators/truffler/install/templates/migration.rb.tt +6 -3
- 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 +38 -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/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 +55 -12
- data/lib/truffler/labeling/backfill.rb +100 -37
- data/lib/truffler/labeling/labeler.rb +20 -5
- 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/lenses/lens_label.rb +7 -0
- data/lib/truffler/model.rb +5 -0
- data/lib/truffler/providers/backup.rb +1 -1
- data/lib/truffler/query_encoding/encoder.rb +56 -39
- data/lib/truffler/records/backfill_spend.rb +30 -6
- data/lib/truffler/redaction.rb +1 -1
- data/lib/truffler/search/encoding.rb +50 -12
- data/lib/truffler/search/encoding_cache.rb +4 -3
- data/lib/truffler/search/filler.rb +73 -0
- data/lib/truffler/search/keystroke.rb +29 -15
- data/lib/truffler/search/result.rb +11 -1
- data/lib/truffler/search/sql.rb +93 -15
- data/lib/truffler/smart_search/dispatcher.rb +1 -1
- data/lib/truffler/smart_search/starter.rb +1 -1
- data/lib/truffler/version.rb +1 -1
- data/lib/truffler/vocabulary.rb +13 -3
- metadata +8 -1
|
@@ -2,20 +2,27 @@ 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
|
-
# the
|
|
17
|
+
# current vocabulary version in the truffler_backfill_spends ledger, so
|
|
18
|
+
# reruns and overlapping jobs share it. Tenant-scoped models keep one
|
|
19
|
+
# ledger per tenant (backfill_spend_cap_scope :tenant, the default), so
|
|
20
|
+
# the cap applies to each tenant: a whole-model run skips a tenant at its
|
|
21
|
+
# cap, keeps labeling the others, and ends with `:spend_cap_reached`; a
|
|
22
|
+
# tenant run stops there. Unscoped models, and :app, keep one app-wide
|
|
23
|
+
# ledger. Without that table the cap falls back to this run plus
|
|
24
|
+
# `spent:`. A Jev error releases the claimed rows and ends the run with
|
|
25
|
+
# `:client_error`, so the caller keeps the spend metered so far.
|
|
19
26
|
#
|
|
20
27
|
# A budget denial ends the run with `:budget_denied`, unless the run
|
|
21
28
|
# waits: then it backs off (see .backoff) and retries from the same
|
|
@@ -94,29 +101,35 @@ module Truffler
|
|
|
94
101
|
end
|
|
95
102
|
end
|
|
96
103
|
|
|
97
|
-
def self.status(model)
|
|
98
|
-
new(model).status
|
|
104
|
+
def self.status(model, tenant_key: nil)
|
|
105
|
+
new(model, tenant_key: tenant_key).status
|
|
99
106
|
end
|
|
100
107
|
|
|
101
|
-
# The ledger row for the
|
|
102
|
-
# when nothing was spent yet or the ledger
|
|
103
|
-
|
|
108
|
+
# The ledger row for the current vocabulary version (the tenant's, for
|
|
109
|
+
# a tenant ledger), or nil when nothing was spent yet or the ledger
|
|
110
|
+
# table is missing.
|
|
111
|
+
def self.spend(model, tenant_key: nil)
|
|
104
112
|
return unless Records::BackfillSpend.available?
|
|
105
113
|
|
|
106
|
-
Records::BackfillSpend.
|
|
114
|
+
Records::BackfillSpend.for_ledger(model, tenant_key).find_by(vocabulary_version: ledger_version(model, tenant_key))
|
|
107
115
|
end
|
|
108
116
|
|
|
109
117
|
# Zeroes the current vocabulary version's ledger in place, so a chain
|
|
110
118
|
# still running keeps its row and continues against the fresh total.
|
|
111
|
-
|
|
119
|
+
# `all_tenants: true` zeroes every tenant ledger of the model.
|
|
120
|
+
def self.reset_spend!(model, tenant_key: nil, all_tenants: false)
|
|
112
121
|
return unless Records::BackfillSpend.available?
|
|
113
122
|
|
|
114
|
-
Records::BackfillSpend.
|
|
115
|
-
.
|
|
123
|
+
ledgers = if all_tenants && Records::BackfillSpend.tenant_ledgers?
|
|
124
|
+
Records::BackfillSpend.for_model(model).where.not(tenant_key: nil)
|
|
125
|
+
else
|
|
126
|
+
Records::BackfillSpend.for_ledger(model, tenant_key).where(vocabulary_version: ledger_version(model, tenant_key))
|
|
127
|
+
end
|
|
128
|
+
ledgers.update_all(spent_usd: 0.0, requests: 0, updated_at: Time.current)
|
|
116
129
|
end
|
|
117
130
|
|
|
118
|
-
def self.ledger_version(model)
|
|
119
|
-
model.truffler_definition.vocabulary.version(all_users: true)
|
|
131
|
+
def self.ledger_version(model, tenant_key = nil)
|
|
132
|
+
model.truffler_definition.vocabulary.version(tenant_key: tenant_key, all_users: true)
|
|
120
133
|
end
|
|
121
134
|
|
|
122
135
|
# Seconds to wait after `denials` consecutive budget denials with no
|
|
@@ -128,9 +141,10 @@ module Truffler
|
|
|
128
141
|
|
|
129
142
|
attr_reader :model, :batch_size, :page_size
|
|
130
143
|
|
|
131
|
-
def initialize(model, spend_cap: Truffler.config.backfill_spend_cap, batch_size: Truffler.config.batch_size,
|
|
144
|
+
def initialize(model, tenant_key: nil, spend_cap: Truffler.config.backfill_spend_cap, batch_size: Truffler.config.batch_size,
|
|
132
145
|
page_size: nil, cursor: nil, spent: 0.0, client: Truffler.config.client, budget: Budget.new)
|
|
133
146
|
@model = model
|
|
147
|
+
@tenant_key = tenant_key&.to_s if model.truffler_definition.scoped?
|
|
134
148
|
model.truffler_definition.validate_columns!
|
|
135
149
|
@batch_size = batch_size
|
|
136
150
|
@page_size = page_size || batch_size * 5
|
|
@@ -140,6 +154,9 @@ module Truffler
|
|
|
140
154
|
@client = client
|
|
141
155
|
@budget = budget
|
|
142
156
|
@versions = {}
|
|
157
|
+
@meters = {}
|
|
158
|
+
@enabled = {}
|
|
159
|
+
@capped = Set.new
|
|
143
160
|
end
|
|
144
161
|
|
|
145
162
|
# `progress` is called with the result so far and the delay before each
|
|
@@ -147,17 +164,17 @@ module Truffler
|
|
|
147
164
|
def run(max_pages: nil, wait: false, max_duration: nil, sleeper: self.class.sleeper, clock: self.class.clock,
|
|
148
165
|
progress: nil)
|
|
149
166
|
@labeled = 0
|
|
150
|
-
@started_cost =
|
|
167
|
+
@started_cost = spent_cost
|
|
151
168
|
@pages = 0
|
|
152
169
|
deadline = max_duration && clock.call + max_duration
|
|
153
170
|
denials = 0
|
|
154
171
|
|
|
155
172
|
loop do
|
|
156
|
-
before = [ @labeled,
|
|
173
|
+
before = [ @labeled, requests ]
|
|
157
174
|
status = sweep(max_pages, deadline, clock)
|
|
158
175
|
return result(status) unless wait && status == :budget_denied
|
|
159
176
|
|
|
160
|
-
denials = 0 unless before == [ @labeled,
|
|
177
|
+
denials = 0 unless before == [ @labeled, requests ]
|
|
161
178
|
delay = self.class.backoff(denials, @retry_after)
|
|
162
179
|
return result(:paused) if deadline && clock.call + delay > deadline
|
|
163
180
|
|
|
@@ -167,11 +184,16 @@ module Truffler
|
|
|
167
184
|
end
|
|
168
185
|
end
|
|
169
186
|
|
|
187
|
+
# Counts over what the backfill may touch: index_scope, enabled tenants,
|
|
188
|
+
# and the one tenant when `tenant_key:` is given.
|
|
170
189
|
def status
|
|
171
|
-
|
|
172
|
-
|
|
190
|
+
scope, tenants = status_scope
|
|
191
|
+
tracked = states.where(record_id: scope.select(model.arel_table[model.primary_key]))
|
|
192
|
+
tracked = tracked.where(tenant_key: tenants) if tenants
|
|
193
|
+
counts = tracked.group(:status).count
|
|
194
|
+
labeled = tracked.where(status: "labeled").group(:tenant_key, :vocabulary_version).count
|
|
173
195
|
stale = labeled.sum { |(tenant_key, version), count| version == version_for(tenant_key) ? 0 : count }
|
|
174
|
-
{ total:
|
|
196
|
+
{ total: scope.count, missing: scope.joins(state_join).where("#{STATES}.id IS NULL").count,
|
|
175
197
|
pending: counts["pending"].to_i, labeling: counts["labeling"].to_i, labeled: counts["labeled"].to_i,
|
|
176
198
|
failed: counts["failed"].to_i, stale: stale, current: labeled.values.sum - stale }
|
|
177
199
|
end
|
|
@@ -190,30 +212,64 @@ module Truffler
|
|
|
190
212
|
Records::RecordState.for_model(model)
|
|
191
213
|
end
|
|
192
214
|
|
|
215
|
+
# [relation, tenant keys or nil]: the records status counts, and the
|
|
216
|
+
# tenants it covers when it narrows to some.
|
|
217
|
+
def status_scope
|
|
218
|
+
scope = definition.index_relation(model.all)
|
|
219
|
+
return [ scope, nil ] unless definition.scoped?
|
|
220
|
+
return [ scope.where(definition.tenant_column => @tenant_key), [ @tenant_key ] ] if @tenant_key
|
|
221
|
+
return [ scope, nil ] unless Truffler.config.tenant_enabled
|
|
222
|
+
|
|
223
|
+
tenants = scope.distinct.pluck(definition.tenant_column).map(&:to_s).select { |tenant_key| enabled?(tenant_key) }
|
|
224
|
+
[ scope.where(definition.tenant_column => tenants), tenants ]
|
|
225
|
+
end
|
|
226
|
+
|
|
193
227
|
def queue
|
|
194
228
|
@queue ||= Queue.new(model)
|
|
195
229
|
end
|
|
196
230
|
|
|
197
|
-
#
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
231
|
+
# One meter per ledger: per tenant for tenant ledgers, else one for the
|
|
232
|
+
# run. Spend carried in with `spent:` is ignored when the ledger holds it.
|
|
233
|
+
def meter(tenant_key)
|
|
234
|
+
ledger_tenant = ledger_available? ? definition.ledger_tenant(tenant_key) : nil
|
|
235
|
+
@meters[ledger_tenant] ||= begin
|
|
236
|
+
ledger = Records::BackfillSpend.ledger(model, version_for(ledger_tenant), tenant_key: ledger_tenant) if ledger_available?
|
|
201
237
|
SpendMeter.new(@client, cap: @spend_cap, spent: ledger ? 0.0 : @spent, ledger: ledger)
|
|
202
238
|
end
|
|
203
239
|
end
|
|
204
240
|
|
|
241
|
+
def ledger_available?
|
|
242
|
+
return @ledger_available if defined?(@ledger_available)
|
|
243
|
+
|
|
244
|
+
@ledger_available = Records::BackfillSpend.available?
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def requests
|
|
248
|
+
@meters.each_value.sum(&:requests)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def spent_cost
|
|
252
|
+
@meters.each_value.sum(&:cost)
|
|
253
|
+
end
|
|
254
|
+
|
|
205
255
|
def version_for(tenant_key)
|
|
206
256
|
@versions[tenant_key] ||= definition.vocabulary.version(tenant_key: tenant_key, all_users: true)
|
|
207
257
|
end
|
|
208
258
|
|
|
259
|
+
def enabled?(tenant_key)
|
|
260
|
+
@enabled.fetch(tenant_key) { @enabled[tenant_key] = definition.tenant_enabled?(tenant_key) }
|
|
261
|
+
end
|
|
262
|
+
|
|
209
263
|
def result(status)
|
|
210
|
-
Result.new(status: status, labeled: @labeled, requests:
|
|
264
|
+
Result.new(status: status, labeled: @labeled, requests: requests, cost: spent_cost - @started_cost,
|
|
211
265
|
cursor: @cursor, retry_after: (@retry_after if status == :budget_denied))
|
|
212
266
|
end
|
|
213
267
|
|
|
214
268
|
# Walks pages below @cursor until done or stopped, returning the status.
|
|
215
269
|
def sweep(max_pages, deadline, clock)
|
|
216
270
|
@retry_after = nil
|
|
271
|
+
return complete if @tenant_key && !enabled?(@tenant_key)
|
|
272
|
+
|
|
217
273
|
loop do
|
|
218
274
|
scanned, rows = page(@cursor)
|
|
219
275
|
return complete if scanned.empty?
|
|
@@ -221,6 +277,8 @@ module Truffler
|
|
|
221
277
|
|
|
222
278
|
rows.group_by(&:last).each do |tenant_key, tenant_rows|
|
|
223
279
|
tenant_rows.map(&:first).each_slice(batch_size) do |ids|
|
|
280
|
+
break if @capped.include?(tenant_key)
|
|
281
|
+
|
|
224
282
|
stop = label(ids, tenant_key)
|
|
225
283
|
return stop if stop
|
|
226
284
|
end
|
|
@@ -234,7 +292,7 @@ module Truffler
|
|
|
234
292
|
|
|
235
293
|
def complete
|
|
236
294
|
@cursor = nil
|
|
237
|
-
:complete
|
|
295
|
+
@capped.any? ? :spend_cap_reached : :complete
|
|
238
296
|
end
|
|
239
297
|
|
|
240
298
|
# Returns the scanned ids (for the cursor) and the [id, tenant_key] rows
|
|
@@ -242,7 +300,8 @@ module Truffler
|
|
|
242
300
|
# lenses) compare versions here because each tenant has its own.
|
|
243
301
|
def page(cursor)
|
|
244
302
|
pk = model.primary_key
|
|
245
|
-
scope = model.joins(state_join).where(needs_labeling_sql)
|
|
303
|
+
scope = definition.index_relation(model.joins(state_join).where(needs_labeling_sql))
|
|
304
|
+
scope = scope.where(definition.tenant_column => @tenant_key) if @tenant_key
|
|
246
305
|
scope = scope.where(model.arel_table[pk].lt(cursor)) if cursor
|
|
247
306
|
tenant = definition.scoped? ? model.arel_table[definition.tenant_column] : Arel.sql("NULL")
|
|
248
307
|
plucked = scope.reorder(pk => :desc).limit(page_size)
|
|
@@ -250,7 +309,7 @@ module Truffler
|
|
|
250
309
|
|
|
251
310
|
rows = plucked.filter_map do |id, tenant_key, status, version|
|
|
252
311
|
tenant_key = tenant_key&.to_s
|
|
253
|
-
[ id, tenant_key ] unless status == "labeled" && version == version_for(tenant_key)
|
|
312
|
+
[ id, tenant_key ] unless (status == "labeled" && version == version_for(tenant_key)) || !enabled?(tenant_key)
|
|
254
313
|
end
|
|
255
314
|
[ plucked.map(&:first), rows ]
|
|
256
315
|
end
|
|
@@ -277,16 +336,20 @@ module Truffler
|
|
|
277
336
|
|
|
278
337
|
# Labels one tenant chunk. Returns nil when done, or the status that
|
|
279
338
|
# stops the run; claimed rows that were not labeled go back to pending
|
|
280
|
-
# at backfill priority.
|
|
339
|
+
# at backfill priority. A tenant ledger at its cap only stops that
|
|
340
|
+
# tenant in a whole-model run.
|
|
281
341
|
def label(ids, tenant_key)
|
|
282
342
|
claimed = queue.claim_backfill(ids, tenant_key)
|
|
283
343
|
return if claimed.empty?
|
|
284
344
|
|
|
285
345
|
begin
|
|
286
|
-
Labeler.new(model, client: meter, budget: @budget).label(claimed, priority: :backfill)
|
|
346
|
+
Labeler.new(model, client: meter(tenant_key), budget: @budget).label(claimed, priority: :backfill)
|
|
287
347
|
rescue SpendCapReached
|
|
288
348
|
queue.demote(claimed)
|
|
289
|
-
return :spend_cap_reached
|
|
349
|
+
return :spend_cap_reached if @tenant_key || definition.ledger_tenant(tenant_key).nil? || !ledger_available?
|
|
350
|
+
|
|
351
|
+
@capped << tenant_key
|
|
352
|
+
return
|
|
290
353
|
rescue BudgetExhausted => error
|
|
291
354
|
queue.demote(claimed)
|
|
292
355
|
@retry_after = error.retry_after
|
|
@@ -23,7 +23,7 @@ module Truffler
|
|
|
23
23
|
return Result.new(labeled: 0, requests: 0, cost: 0.0, demoted: false) if states.empty?
|
|
24
24
|
|
|
25
25
|
tenant_key = tenant_key_of(states)
|
|
26
|
-
records = load_records(states)
|
|
26
|
+
records = load_records(states, tenant_key)
|
|
27
27
|
vocabulary = definition.vocabulary
|
|
28
28
|
@labels = vocabulary.labels_for(tenant_key: tenant_key, all_users: true)
|
|
29
29
|
fingerprints = vocabulary.fingerprints(tenant_key: tenant_key, all_users: true)
|
|
@@ -87,17 +87,31 @@ module Truffler
|
|
|
87
87
|
tenants.first
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
# Records deleted or outside index_scope / index_if lose their state
|
|
91
|
+
# rows instead of being labeled. A disabled tenant keeps its state rows:
|
|
92
|
+
# they go back to pending at backfill priority, so re-enabling it
|
|
93
|
+
# relabels only what is stale instead of every record.
|
|
94
|
+
def load_records(states, tenant_key)
|
|
95
|
+
unless definition.tenant_enabled?(tenant_key)
|
|
96
|
+
Records::RecordState.where(id: states.map(&:id))
|
|
97
|
+
.update_all(status: "pending", priority: "backfill", claimed_at: nil, updated_at: Time.current)
|
|
98
|
+
return []
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
records = definition.index_relation(model.where(model.primary_key => states.map(&:record_id))).to_a
|
|
102
|
+
.select { |record| definition.index_if.nil? || definition.index_if.call(record) }
|
|
92
103
|
found = records.map { |record| record.id.to_s }
|
|
93
104
|
gone = states.reject { |state| found.include?(state.record_id.to_s) }
|
|
94
105
|
Records::RecordState.where(id: gone.map(&:id)).delete_all if gone.any?
|
|
95
106
|
records
|
|
96
107
|
end
|
|
97
108
|
|
|
109
|
+
# A label is current when it has stored rows and all carry its current
|
|
110
|
+
# fingerprint; choice labels store only some options (see sparse_choice).
|
|
98
111
|
def stale_keys(stored, fingerprints, tenant_key, askable)
|
|
99
112
|
askable.reject do |label|
|
|
100
|
-
label.storage_keys(tenant_key).
|
|
113
|
+
present = label.storage_keys(tenant_key).select { |key| stored.key?(key) }
|
|
114
|
+
present.any? && present.all? { |key| stored[key] == fingerprints[label.key] }
|
|
101
115
|
end.map(&:key)
|
|
102
116
|
end
|
|
103
117
|
|
|
@@ -154,7 +168,8 @@ module Truffler
|
|
|
154
168
|
case label.type
|
|
155
169
|
when :noul then [ [ label.key, answers.noul(id) ] ]
|
|
156
170
|
when :score then [ [ label.key, answers.score(id) ] ]
|
|
157
|
-
when :choice
|
|
171
|
+
when :choice
|
|
172
|
+
LabelDefinition.sparse_choice(label.options(tenant_key).keys.to_h { |option| [ "#{label.key}:#{option}", answers.probability(id, option) ] }).to_a
|
|
158
173
|
end
|
|
159
174
|
end
|
|
160
175
|
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
|
|
@@ -29,6 +29,13 @@ module Truffler
|
|
|
29
29
|
@question["criteria"].to_h.transform_keys(&:to_s)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
def option_names(tenant_key = nil)
|
|
33
|
+
options(tenant_key).compact
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The lens question, wording included, is already in its fingerprint.
|
|
37
|
+
def encoding_wording(_tenant_key = nil) = nil
|
|
38
|
+
|
|
32
39
|
def storage_keys(tenant_key = nil)
|
|
33
40
|
type == :choice ? options(tenant_key).keys.map { |option| "#{key}:#{option}" } : [ key ]
|
|
34
41
|
end
|
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"
|