bitfab 0.30.0 → 0.31.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.
- checksums.yaml +4 -4
- data/lib/bitfab/client.rb +2 -2
- data/lib/bitfab/http_client.rb +2 -1
- data/lib/bitfab/replay.rb +226 -4
- data/lib/bitfab/replay_environment.rb +8 -0
- data/lib/bitfab/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d85c35718e6138d5095d4d20278fdac5f3c6f31858684622cb3a68bad6b5416
|
|
4
|
+
data.tar.gz: 1de8519b600a8ea4a29593a82f938669d8aa9e9ec616c95102c0abe6d06045c6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c6d45ca5bbaedfd2a162e2d05c4bef6da21b31c482db91d5539fb46b79be936aa4600e2ea5d0875ecf0e608907ee92a028660edbcd08dc367ce942003658538
|
|
7
|
+
data.tar.gz: db3e762b1ce6b7cb60f4996f4642d6e2a3d5e6a7e6b886a14c319b98ae198f0f210e7af857fda35c53d4e2a49339283ae929f6338d1bae3f64f9be6b97023acc
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -114,12 +114,12 @@ module Bitfab
|
|
|
114
114
|
# @return [Hash] with :items, :test_run_id, :test_run_url
|
|
115
115
|
def replay(receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, max_concurrency: 10,
|
|
116
116
|
name: nil, code_change_description: nil, code_change_files: nil, experiment_group_id: nil, dataset_id: nil, grader_ids: nil, mock: "marked",
|
|
117
|
-
adapt_inputs: nil, mock_override: nil, environment: nil, on_progress: nil)
|
|
117
|
+
adapt_inputs: nil, mock_override: nil, environment: nil, db_branch: nil, on_progress: nil)
|
|
118
118
|
Replay.run(
|
|
119
119
|
self, receiver, method_name,
|
|
120
120
|
trace_function_key:, limit:, trace_ids:, name:, max_concurrency:,
|
|
121
121
|
code_change_description:, code_change_files:, experiment_group_id:, dataset_id:, grader_ids:, mock:, adapt_inputs:,
|
|
122
|
-
mock_override:, environment:,
|
|
122
|
+
mock_override:, environment:, db_branch:,
|
|
123
123
|
on_progress:
|
|
124
124
|
)
|
|
125
125
|
end
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -118,7 +118,7 @@ module Bitfab
|
|
|
118
118
|
# same org and trace function or the server rejects the replay
|
|
119
119
|
def start_replay(trace_function_key, limit, trace_ids: nil, code_change_description: nil,
|
|
120
120
|
code_change_files: nil, experiment_group_id: nil, name: nil, include_db_branch_lease: false, dataset_id: nil,
|
|
121
|
-
grader_ids: nil)
|
|
121
|
+
grader_ids: nil, db_branch_settings: nil)
|
|
122
122
|
payload = {
|
|
123
123
|
"traceFunctionKey" => trace_function_key
|
|
124
124
|
}
|
|
@@ -133,6 +133,7 @@ module Bitfab
|
|
|
133
133
|
payload["includeDbBranchLease"] = true if include_db_branch_lease
|
|
134
134
|
payload["datasetId"] = dataset_id unless dataset_id.nil?
|
|
135
135
|
payload["graderIds"] = grader_ids unless grader_ids.nil?
|
|
136
|
+
payload["dbBranchSettings"] = db_branch_settings unless db_branch_settings.nil?
|
|
136
137
|
|
|
137
138
|
# When DB branching is on, the server resolves a Neon preview branch per
|
|
138
139
|
# item (snapshot + restore + poll), which can run several seconds each.
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "json"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "timeout"
|
|
5
7
|
|
|
6
8
|
require_relative "constants"
|
|
7
9
|
require_relative "mock_override"
|
|
@@ -134,7 +136,7 @@ module Bitfab
|
|
|
134
136
|
def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, name: nil,
|
|
135
137
|
max_concurrency: 10, code_change_description: nil, code_change_files: nil, experiment_group_id: nil,
|
|
136
138
|
dataset_id: nil, grader_ids: nil, mock: "marked",
|
|
137
|
-
adapt_inputs: nil, mock_override: nil, environment: nil, on_progress: nil)
|
|
139
|
+
adapt_inputs: nil, mock_override: nil, environment: nil, db_branch: nil, on_progress: nil)
|
|
138
140
|
unless MOCK_STRATEGIES.include?(mock.to_s)
|
|
139
141
|
raise ArgumentError, "Invalid mock strategy '#{mock}'. Must be one of: #{MOCK_STRATEGIES.join(", ")}"
|
|
140
142
|
end
|
|
@@ -179,6 +181,19 @@ module Bitfab
|
|
|
179
181
|
|
|
180
182
|
include_db_branch_lease = !environment.nil?
|
|
181
183
|
|
|
184
|
+
# An explicit code change always wins; only when the caller passed neither
|
|
185
|
+
# field do we fall back to the payload the replay wrapper captured from git
|
|
186
|
+
# and handed over via BITFAB_CODE_CHANGE_PATH. This is what makes an
|
|
187
|
+
# experiment show its diff even when the run happened outside the
|
|
188
|
+
# assistant's make-change rails.
|
|
189
|
+
if code_change_description.nil? && code_change_files.nil?
|
|
190
|
+
captured = resolve_auto_code_change(name)
|
|
191
|
+
unless captured.nil?
|
|
192
|
+
code_change_description = captured[0]
|
|
193
|
+
code_change_files = captured[1]
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
182
197
|
replay_data = http_client.start_replay(
|
|
183
198
|
trace_function_key,
|
|
184
199
|
effective_limit,
|
|
@@ -189,7 +204,8 @@ module Bitfab
|
|
|
189
204
|
experiment_group_id:,
|
|
190
205
|
include_db_branch_lease:,
|
|
191
206
|
dataset_id:,
|
|
192
|
-
grader_ids
|
|
207
|
+
grader_ids:,
|
|
208
|
+
db_branch_settings: db_branch_settings(db_branch)
|
|
193
209
|
)
|
|
194
210
|
test_run_id = replay_data["testRunId"]
|
|
195
211
|
test_run_url = replay_data["testRunUrl"]
|
|
@@ -295,6 +311,202 @@ module Bitfab
|
|
|
295
311
|
result
|
|
296
312
|
end
|
|
297
313
|
|
|
314
|
+
# Read the code-change payload the replay wrapper captured and pointed us at
|
|
315
|
+
# via BITFAB_CODE_CHANGE_PATH. Returns [description, files] or nil.
|
|
316
|
+
# Best-effort: any error (no env var, missing file, bad JSON) yields nil so
|
|
317
|
+
# the replay proceeds with no code change rather than raising.
|
|
318
|
+
TRUNK_CANDIDATES = ["origin/HEAD", "origin/main", "origin/master", "main", "master"].freeze
|
|
319
|
+
CC_MAX_FILES = 60
|
|
320
|
+
CC_MAX_FILE_BYTES = 500_000
|
|
321
|
+
CC_MAX_TOTAL_BYTES = 2_000_000
|
|
322
|
+
|
|
323
|
+
# Convert the caller's +db_branch+ options to the wire shape, or nil when
|
|
324
|
+
# nothing was set. Dropping the key entirely keeps the request identical to
|
|
325
|
+
# what older SDKs send.
|
|
326
|
+
#
|
|
327
|
+
# Keys: :min_cu and :max_cu are the branch compute's autoscaling floor and
|
|
328
|
+
# ceiling in Neon Compute Units (equal values pin the size, keeping items
|
|
329
|
+
# comparable); :warmup_sql is appended to the branch's readiness check so it
|
|
330
|
+
# warms the cache before the replayed function sees the lease.
|
|
331
|
+
def db_branch_settings(db_branch)
|
|
332
|
+
return nil if db_branch.nil? || db_branch.empty?
|
|
333
|
+
|
|
334
|
+
# Symbol or string keys, matching normalize_code_change_files: a hash
|
|
335
|
+
# loaded from JSON or YAML would otherwise be read as empty, leaving the
|
|
336
|
+
# branch on the mirror's defaults with no error to notice.
|
|
337
|
+
min_cu = db_branch[:min_cu] || db_branch["min_cu"]
|
|
338
|
+
max_cu = db_branch[:max_cu] || db_branch["max_cu"]
|
|
339
|
+
warmup_sql = db_branch[:warmup_sql] || db_branch["warmup_sql"]
|
|
340
|
+
|
|
341
|
+
settings = {}
|
|
342
|
+
settings["minCu"] = min_cu unless min_cu.nil?
|
|
343
|
+
settings["maxCu"] = max_cu unless max_cu.nil?
|
|
344
|
+
settings["warmupSql"] = warmup_sql unless warmup_sql.nil?
|
|
345
|
+
settings.empty? ? nil : settings
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Auto-capture the code change to attach when the caller passed none. Lives
|
|
349
|
+
# in replay() (the one call guaranteed to run on every replay) so it works no
|
|
350
|
+
# matter how the replay was launched. Precedence: a BITFAB_CODE_CHANGE_PATH
|
|
351
|
+
# override, else the git diff vs trunk. Best-effort: any failure yields nil.
|
|
352
|
+
# An explicit code_change_files always wins over this.
|
|
353
|
+
def resolve_auto_code_change(label = nil)
|
|
354
|
+
return nil if ENV["BITFAB_DISABLE_CODE_CHANGE_CAPTURE"]
|
|
355
|
+
|
|
356
|
+
from_env = read_code_change_file
|
|
357
|
+
return from_env unless from_env.nil?
|
|
358
|
+
|
|
359
|
+
capture_code_change_from_git(Dir.pwd, label)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def read_code_change_file
|
|
363
|
+
path = ENV["BITFAB_CODE_CHANGE_PATH"]
|
|
364
|
+
return nil if path.nil? || path.empty?
|
|
365
|
+
|
|
366
|
+
begin
|
|
367
|
+
parsed = JSON.parse(File.read(path))
|
|
368
|
+
files = parsed["files"]
|
|
369
|
+
# A malformed payload (non-array, or entries that aren't hashes) must
|
|
370
|
+
# yield no code change, never crash normalize_code_change_files.
|
|
371
|
+
files = nil unless files.is_a?(Array) && files.all?(Hash)
|
|
372
|
+
description = parsed["description"]
|
|
373
|
+
description = nil unless description.is_a?(String)
|
|
374
|
+
return nil if files.nil? && description.nil?
|
|
375
|
+
|
|
376
|
+
[description, files]
|
|
377
|
+
rescue
|
|
378
|
+
nil
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def git(cwd, args)
|
|
383
|
+
# capture3 so git's stderr (e.g. "not a git repository") is swallowed, not
|
|
384
|
+
# leaked to the host process's console.
|
|
385
|
+
out, _err, status = Timeout.timeout(30) do
|
|
386
|
+
Open3.capture3("git", *args, chdir: cwd)
|
|
387
|
+
end
|
|
388
|
+
return nil unless status.success?
|
|
389
|
+
|
|
390
|
+
# Tag as UTF-8 and scrub invalid bytes so blob contents compare cleanly
|
|
391
|
+
# against the UTF-8 working-file reads and never break JSON.generate later
|
|
392
|
+
# (a non-UTF-8 file that slips past the null-byte check would otherwise
|
|
393
|
+
# abort the whole start-replay request).
|
|
394
|
+
out.force_encoding("UTF-8").scrub
|
|
395
|
+
rescue
|
|
396
|
+
nil
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def cc_ref_exists?(root, ref)
|
|
400
|
+
!git(root, ["rev-parse", "--verify", "#{ref}^{object}"]).nil?
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Returns [base, from_trunk]; from_trunk is false on the HEAD fallback.
|
|
404
|
+
def cc_resolve_base(root)
|
|
405
|
+
forced = ENV["BITFAB_CODE_CHANGE_BASE"]
|
|
406
|
+
if forced && cc_ref_exists?(root, forced)
|
|
407
|
+
mb = git(root, ["merge-base", "HEAD", forced])
|
|
408
|
+
return [mb.strip, true] if mb && !mb.strip.empty?
|
|
409
|
+
|
|
410
|
+
rp = git(root, ["rev-parse", "--verify", forced])
|
|
411
|
+
return (rp && !rp.strip.empty?) ? [rp.strip, true] : nil
|
|
412
|
+
end
|
|
413
|
+
TRUNK_CANDIDATES.each do |candidate|
|
|
414
|
+
next unless cc_ref_exists?(root, candidate)
|
|
415
|
+
|
|
416
|
+
mb = git(root, ["merge-base", "HEAD", candidate])
|
|
417
|
+
return [mb.strip, true] if mb && !mb.strip.empty?
|
|
418
|
+
end
|
|
419
|
+
cc_ref_exists?(root, "HEAD") ? ["HEAD", false] : nil
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def cc_blob_bytes(root, base, path)
|
|
423
|
+
out = git(root, ["cat-file", "-s", "#{base}:#{path}"])
|
|
424
|
+
(out && !out.strip.empty?) ? out.strip.to_i : 0
|
|
425
|
+
rescue
|
|
426
|
+
0
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def cc_working_bytes(root, path)
|
|
430
|
+
File.size(File.join(root, path))
|
|
431
|
+
rescue
|
|
432
|
+
0
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def capture_code_change_from_git(cwd, label = nil)
|
|
436
|
+
root = git(cwd, ["rev-parse", "--show-toplevel"])
|
|
437
|
+
return nil if root.nil? || root.strip.empty?
|
|
438
|
+
|
|
439
|
+
root = root.strip
|
|
440
|
+
resolved = cc_resolve_base(root)
|
|
441
|
+
return nil if resolved.nil?
|
|
442
|
+
|
|
443
|
+
base, from_trunk = resolved
|
|
444
|
+
|
|
445
|
+
tracked = git(root, ["diff", "--name-status", "--no-renames", "-z", base, "--", ":!.bitfab"]) || ""
|
|
446
|
+
untracked = git(root, ["ls-files", "--others", "--exclude-standard", "-z", "--", ":!.bitfab"]) || ""
|
|
447
|
+
|
|
448
|
+
entries = cc_parse_name_status_z(tracked)
|
|
449
|
+
untracked.split("\0").reject(&:empty?).each { |p| entries << ["A", p] }
|
|
450
|
+
return nil if entries.empty?
|
|
451
|
+
|
|
452
|
+
files = []
|
|
453
|
+
total = 0
|
|
454
|
+
entries.each do |status, path|
|
|
455
|
+
break if files.length >= CC_MAX_FILES
|
|
456
|
+
|
|
457
|
+
# Skip oversized files by size BEFORE reading their full contents, so a
|
|
458
|
+
# huge changed file can't OOM/stall replay just to be discarded.
|
|
459
|
+
before_bytes = (status == "A") ? 0 : cc_blob_bytes(root, base, path)
|
|
460
|
+
after_bytes = (status == "D") ? 0 : cc_working_bytes(root, path)
|
|
461
|
+
next if before_bytes > CC_MAX_FILE_BYTES || after_bytes > CC_MAX_FILE_BYTES
|
|
462
|
+
|
|
463
|
+
# Normalize CRLF -> LF on both sides: git's blob is already LF-normalized
|
|
464
|
+
# (autocrlf clean), so a raw CRLF working file would otherwise skew every
|
|
465
|
+
# line as changed. Comparing/storing LF keeps the diff meaningful.
|
|
466
|
+
before = (status == "A") ? "" : (git(root, ["show", "#{base}:#{path}"]) || "").gsub("\r\n", "\n")
|
|
467
|
+
after = (status == "D") ? "" : cc_read_working_file(root, path).gsub("\r\n", "\n")
|
|
468
|
+
next if before == after
|
|
469
|
+
|
|
470
|
+
size = before.bytesize + after.bytesize
|
|
471
|
+
next if total + size > CC_MAX_TOTAL_BYTES ||
|
|
472
|
+
cc_looks_binary?(before) || cc_looks_binary?(after)
|
|
473
|
+
|
|
474
|
+
total += size
|
|
475
|
+
files << {"path" => path, "before" => before, "after" => after}
|
|
476
|
+
end
|
|
477
|
+
return nil if files.empty?
|
|
478
|
+
|
|
479
|
+
subject = git(root, ["log", "-1", "--format=%s", "HEAD"])
|
|
480
|
+
subject = subject ? subject.strip : ""
|
|
481
|
+
head = ((label && !label.strip.empty?) ? label.strip : nil) || (subject.empty? ? nil : subject) || "Working-tree change"
|
|
482
|
+
word = (files.length == 1) ? "file" : "files"
|
|
483
|
+
against = from_trunk ? "vs trunk" : "uncommitted (vs HEAD)"
|
|
484
|
+
["#{head} (#{files.length} #{word} changed #{against})", files]
|
|
485
|
+
rescue
|
|
486
|
+
nil
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def cc_parse_name_status_z(raw)
|
|
490
|
+
parts = raw.split("\0").reject(&:empty?)
|
|
491
|
+
out = []
|
|
492
|
+
i = 0
|
|
493
|
+
while i + 1 < parts.length
|
|
494
|
+
out << [parts[i][0], parts[i + 1]]
|
|
495
|
+
i += 2
|
|
496
|
+
end
|
|
497
|
+
out
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def cc_read_working_file(root, path)
|
|
501
|
+
File.read(File.join(root, path), encoding: "UTF-8")
|
|
502
|
+
rescue
|
|
503
|
+
""
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def cc_looks_binary?(str)
|
|
507
|
+
str[0, 8000].include?("\0")
|
|
508
|
+
end
|
|
509
|
+
|
|
298
510
|
def write_replay_result_file(result)
|
|
299
511
|
result_path = ENV["BITFAB_REPLAY_RESULT_PATH"]
|
|
300
512
|
return if result_path.nil? || result_path.empty?
|
|
@@ -413,9 +625,19 @@ module Bitfab
|
|
|
413
625
|
# (only when include_db_branch_lease was sent). Release it in the +ensure+
|
|
414
626
|
# below so any raise (span fetch, mock-tree build, or the replayed
|
|
415
627
|
# method) frees the Neon resource. Items whose source trace had no
|
|
416
|
-
# snapshot ref
|
|
417
|
-
#
|
|
628
|
+
# snapshot ref arrive without a lease (env.active? is false), and
|
|
629
|
+
# replaying those against the live database is correct.
|
|
418
630
|
lease = include_db_branch_lease ? server_item["dbBranchLease"] : nil
|
|
631
|
+
# A resolve that was ATTEMPTED and failed is different: the caller asked
|
|
632
|
+
# for a branch, so running their method against live data would produce a
|
|
633
|
+
# result that looks valid and is not. Fail the item instead, loudly.
|
|
634
|
+
lease_error = include_db_branch_lease ? server_item["dbBranchLeaseError"] : nil
|
|
635
|
+
if lease_error
|
|
636
|
+
raise "Replay requested a database branch for trace #{original_trace_id} but it " \
|
|
637
|
+
"could not be resolved (#{lease_error["code"]}): #{lease_error["message"]}. " \
|
|
638
|
+
"The method was not run, because replaying it against the live database would " \
|
|
639
|
+
"produce a result that looks valid but did not use the historical data you asked for."
|
|
640
|
+
end
|
|
419
641
|
|
|
420
642
|
span = http_client.get_external_span(original_span_id)
|
|
421
643
|
item_data = extract_span_data(span)
|
|
@@ -44,6 +44,13 @@ module Bitfab
|
|
|
44
44
|
require_snapshot[:read_only]
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# The branch's region, e.g. "aws-us-east-1". A compute runs in its
|
|
48
|
+
# project's region, so a replay runner elsewhere pays that round trip on
|
|
49
|
+
# every query.
|
|
50
|
+
def region
|
|
51
|
+
require_snapshot[:region]
|
|
52
|
+
end
|
|
53
|
+
|
|
47
54
|
# The historical trace ID that produced the input for this replay item.
|
|
48
55
|
def trace_id
|
|
49
56
|
require_snapshot.fetch(:trace_id)
|
|
@@ -95,6 +102,7 @@ module Bitfab
|
|
|
95
102
|
expires_at: lease["expiresAt"],
|
|
96
103
|
provider_console_url: lease["providerConsoleUrl"],
|
|
97
104
|
read_only: lease["readOnly"],
|
|
105
|
+
region: lease["region"],
|
|
98
106
|
trace_id:
|
|
99
107
|
}
|
|
100
108
|
end
|
data/lib/bitfab/version.rb
CHANGED