bitfab 0.36.11 → 0.38.1
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/exe/bitfab-replay +6 -0
- data/lib/bitfab/client.rb +13 -3
- data/lib/bitfab/replay.rb +54 -14
- data/lib/bitfab/replay_cli.rb +58 -0
- data/lib/bitfab/replay_registry.rb +290 -0
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab.rb +2 -0
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2816ce4e3bcd11bb1335ba7dac6b36376277d17075cf853d26c9befa231a758b
|
|
4
|
+
data.tar.gz: 7e923c2efb8dfd38edbdd592972a8f236a3fa81cdfabcbd46b8303ba4160b01e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15a2df82260f61733b90da3c110b195c92373c187c3eddce5faa041f44499bb008ec5a14cd27dab22b63e2973540010cc2e2f10fcc2dcd75481405d018a107f6
|
|
7
|
+
data.tar.gz: 13968f6a9f8d3c88ea2845d0d59e0cc1d31ffadc1054311addeedc1e4ba92d94aea504f0f5d11e9891fd0a233e92d285e635e5d8e85e46ebfdf553cdedc45005
|
data/exe/bitfab-replay
ADDED
data/lib/bitfab/client.rb
CHANGED
|
@@ -91,7 +91,8 @@ module Bitfab
|
|
|
91
91
|
# maximum: 5,000). Ignored when trace_ids or dataset_id is passed because
|
|
92
92
|
# either source already determines how many traces replay. Supplying
|
|
93
93
|
# trace_ids also emits a warning.
|
|
94
|
-
# @param trace_ids [Array<String>, nil] optional list of trace IDs to replay (max 100)
|
|
94
|
+
# @param trace_ids [Array<String>, nil] optional list of trace IDs to replay (max 100),
|
|
95
|
+
# mutually exclusive with dataset_id
|
|
95
96
|
# @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
|
|
96
97
|
# @param code_change_description [String, nil] optional rationale for the
|
|
97
98
|
# code change being tested in this replay (stored on the experiment).
|
|
@@ -105,7 +106,8 @@ module Bitfab
|
|
|
105
106
|
# @param experiment_group_id [String, nil] optional UUID grouping multiple
|
|
106
107
|
# replay runs into a single experiment batch
|
|
107
108
|
# @param dataset_id [String, nil] optional UUID of the dataset this replay
|
|
108
|
-
# runs against,
|
|
109
|
+
# runs against, mutually exclusive with trace_ids and stored on the
|
|
110
|
+
# resulting experiment for durable attribution
|
|
109
111
|
# @param grader_ids [Array<String>, nil] optional UUIDs of graders attached
|
|
110
112
|
# directly to this experiment, graded as the union with the dataset's
|
|
111
113
|
# runnable graders at completion; each must be an active/live grader in the
|
|
@@ -424,7 +426,12 @@ module Bitfab
|
|
|
424
426
|
# Deprecated wire alias, kept so this SDK still reports usage
|
|
425
427
|
# against servers that predate the rename.
|
|
426
428
|
source_trace_id: replay_ctx[:source_bitfab_trace_id],
|
|
427
|
-
accessed: replay_ctx[:db_snapshot_accessed] == true
|
|
429
|
+
accessed: replay_ctx[:db_snapshot_accessed] == true,
|
|
430
|
+
# Echoed verbatim (camelCase inside) rather than re-cased into
|
|
431
|
+
# this record's snake_case: it is the server's own object
|
|
432
|
+
# coming back, and a translation layer here is one more thing
|
|
433
|
+
# to drift.
|
|
434
|
+
timings: replay_ctx[:db_branch_timings]
|
|
428
435
|
}
|
|
429
436
|
end
|
|
430
437
|
|
|
@@ -617,6 +624,9 @@ module Bitfab
|
|
|
617
624
|
usage["source_trace_id"] = db_snapshot_usage[:original_trace_id]
|
|
618
625
|
end
|
|
619
626
|
usage["accessed"] = db_snapshot_usage[:accessed]
|
|
627
|
+
if db_snapshot_usage[:timings]
|
|
628
|
+
usage["timings"] = db_snapshot_usage[:timings]
|
|
629
|
+
end
|
|
620
630
|
raw_trace["db_snapshot_usage"] = usage
|
|
621
631
|
end
|
|
622
632
|
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -74,7 +74,7 @@ module Bitfab
|
|
|
74
74
|
# thread. Child threads and processes intentionally do not inherit it.
|
|
75
75
|
def with_context(test_run_id:, input_source_span_id: nil, input_source_trace_id: nil, trace_id: nil,
|
|
76
76
|
mock_tree: nil, mock_strategy: nil, mock_overrides: nil, fetch_span_output: nil,
|
|
77
|
-
db_branch_lease: nil, source_bitfab_trace_id: nil)
|
|
77
|
+
db_branch_lease: nil, db_branch_timings: nil, source_bitfab_trace_id: nil)
|
|
78
78
|
previous = Thread.current.thread_variable_get(REPLAY_CONTEXT_KEY)
|
|
79
79
|
ctx = {
|
|
80
80
|
test_run_id:,
|
|
@@ -113,6 +113,10 @@ module Bitfab
|
|
|
113
113
|
# accessed for free, and the flag would stop separating "branch was
|
|
114
114
|
# used" from "branch was offered".
|
|
115
115
|
ctx[:db_branch_lease] = db_branch_lease if db_branch_lease
|
|
116
|
+
# Kept off ReplayBranch: customer code reads that mid-replay to reach the
|
|
117
|
+
# branch, and provisioning latency is a property of the run, not of the
|
|
118
|
+
# connection. It rides the context only to be echoed on the completion.
|
|
119
|
+
ctx[:db_branch_timings] = db_branch_timings if db_branch_timings
|
|
116
120
|
ctx[:source_bitfab_trace_id] = source_bitfab_trace_id if source_bitfab_trace_id
|
|
117
121
|
Thread.current.thread_variable_set(REPLAY_CONTEXT_KEY, ctx)
|
|
118
122
|
yield
|
|
@@ -256,11 +260,14 @@ module Bitfab
|
|
|
256
260
|
raise ArgumentError, "trace_ids supports at most 100 trace IDs per replay (got #{trace_ids.length})."
|
|
257
261
|
end
|
|
258
262
|
end
|
|
263
|
+
if trace_ids && dataset_id
|
|
264
|
+
raise ArgumentError,
|
|
265
|
+
"trace_ids and dataset_id select different replay sources and cannot be used together."
|
|
266
|
+
end
|
|
259
267
|
if limit && trace_ids
|
|
260
268
|
warn "Bitfab: limit is ignored when trace_ids is passed: the explicit trace ID list already " \
|
|
261
269
|
"determines how many traces replay."
|
|
262
270
|
end
|
|
263
|
-
|
|
264
271
|
# Reject a trace_function_key that contradicts the method's declared key.
|
|
265
272
|
# replay() fetches historical traces by trace_function_key but records the
|
|
266
273
|
# replayed spans under the method's own bitfab_span key (via send below),
|
|
@@ -765,9 +772,13 @@ module Bitfab
|
|
|
765
772
|
trace_error: result[:trace_error],
|
|
766
773
|
replay_error: result[:replay_error],
|
|
767
774
|
duration_ms: result[:duration_ms],
|
|
775
|
+
original_duration_ms: result[:original_duration_ms],
|
|
776
|
+
original_tokens: result[:original_tokens],
|
|
777
|
+
original_model: result[:original_model],
|
|
768
778
|
tokens: result[:tokens],
|
|
769
779
|
model: result[:model],
|
|
770
|
-
db_snapshot_ref: result[:db_snapshot_ref]
|
|
780
|
+
db_snapshot_ref: result[:db_snapshot_ref],
|
|
781
|
+
db_branch_timings: result[:db_branch_timings]
|
|
771
782
|
}
|
|
772
783
|
})
|
|
773
784
|
rescue => e
|
|
@@ -836,6 +847,9 @@ module Bitfab
|
|
|
836
847
|
# result that looks valid and is not. Fail the item instead, loudly.
|
|
837
848
|
lease_error = include_db_branch_lease ? server_item["dbBranchLeaseError"] : nil
|
|
838
849
|
db_snapshot_ref = server_item["dbSnapshotRef"]
|
|
850
|
+
# Reported whichever way the resolve went, so it is tracked separately
|
|
851
|
+
# from both the lease and the error rather than hanging off either.
|
|
852
|
+
db_branch_timings = include_db_branch_lease ? server_item["dbBranchTimings"] : nil
|
|
839
853
|
if include_db_branch_lease && lease.nil? && lease_error.nil?
|
|
840
854
|
begin
|
|
841
855
|
resolved = http_client.resolve_db_branch_lease(test_run_id, original_trace_id, db_branch_settings)
|
|
@@ -850,6 +864,7 @@ module Bitfab
|
|
|
850
864
|
lease = resolved["lease"]
|
|
851
865
|
lease_error = resolved["leaseError"]
|
|
852
866
|
db_snapshot_ref = resolved["dbSnapshotRef"] || db_snapshot_ref
|
|
867
|
+
db_branch_timings = resolved["timings"] || db_branch_timings
|
|
853
868
|
end
|
|
854
869
|
if lease_error
|
|
855
870
|
raise DbBranchReplayError.new(
|
|
@@ -923,6 +938,7 @@ module Bitfab
|
|
|
923
938
|
adapt_inputs:,
|
|
924
939
|
adapt_ctx:,
|
|
925
940
|
db_branch_lease: lease,
|
|
941
|
+
db_branch_timings:,
|
|
926
942
|
source_bitfab_trace_id: original_trace_id,
|
|
927
943
|
db_snapshot_ref:,
|
|
928
944
|
http_client:
|
|
@@ -936,16 +952,20 @@ module Bitfab
|
|
|
936
952
|
error: replay_item_error_message(e),
|
|
937
953
|
trace_error: nil,
|
|
938
954
|
replay_error: e,
|
|
939
|
-
duration_ms:
|
|
955
|
+
duration_ms: nil,
|
|
956
|
+
original_duration_ms: metrics&.dig(:original_duration_ms),
|
|
957
|
+
original_tokens: metrics&.dig(:original_tokens),
|
|
958
|
+
original_model: metrics&.dig(:original_model),
|
|
940
959
|
tokens: metrics&.dig(:tokens),
|
|
941
|
-
model: metrics&.dig(:
|
|
960
|
+
model: metrics&.dig(:original_model),
|
|
942
961
|
trace_id: nil,
|
|
943
962
|
original_trace_id:,
|
|
944
963
|
original_span_id:,
|
|
945
964
|
# Deprecated aliases for original_trace_id/original_span_id.
|
|
946
965
|
source_trace_id: original_trace_id,
|
|
947
966
|
source_span_id: original_span_id,
|
|
948
|
-
db_snapshot_ref
|
|
967
|
+
db_snapshot_ref:,
|
|
968
|
+
db_branch_timings:
|
|
949
969
|
}
|
|
950
970
|
ensure
|
|
951
971
|
release_db_branch_lease(http_client, lease) if lease
|
|
@@ -1059,11 +1079,15 @@ module Bitfab
|
|
|
1059
1079
|
# the ORIGINAL trace's tokens); the replayed run's tokens are filled in by
|
|
1060
1080
|
# run() from the complete-replay response once spans are aggregated
|
|
1061
1081
|
# server-side, and stay nil here and on older servers.
|
|
1082
|
+
# The original trace's measurements, falling back to the unprefixed keys a
|
|
1083
|
+
# server that predates the rename sends. :tokens stays nil here: it is the
|
|
1084
|
+
# REPLAYED run's, filled in from the complete-replay response.
|
|
1062
1085
|
def extract_server_item_metrics(server_item)
|
|
1063
1086
|
{
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1087
|
+
original_duration_ms: server_item["originalDurationMs"] || server_item["durationMs"],
|
|
1088
|
+
original_tokens: server_item["originalTokens"] || server_item["tokens"],
|
|
1089
|
+
original_model: server_item["originalModel"] || server_item["model"],
|
|
1090
|
+
tokens: nil
|
|
1067
1091
|
}
|
|
1068
1092
|
end
|
|
1069
1093
|
|
|
@@ -1144,13 +1168,15 @@ module Bitfab
|
|
|
1144
1168
|
# Execute a single replay item: deserialize inputs, call method with replay context.
|
|
1145
1169
|
def execute_item(item, receiver, method_name, test_run_id, input_source_span_id = nil, metrics = {},
|
|
1146
1170
|
input_source_trace_id: nil, mock_strategy: "marked", mock_tree: nil, mock_overrides: nil,
|
|
1147
|
-
fetch_span_output: nil, adapt_inputs: nil, adapt_ctx: nil, db_branch_lease: nil,
|
|
1148
|
-
db_snapshot_ref: nil, http_client: nil)
|
|
1171
|
+
fetch_span_output: nil, adapt_inputs: nil, adapt_ctx: nil, db_branch_lease: nil, db_branch_timings: nil,
|
|
1172
|
+
source_bitfab_trace_id: nil, db_snapshot_ref: nil, http_client: nil)
|
|
1149
1173
|
args, kwargs = Serialize.deserialize_inputs(item)
|
|
1150
1174
|
|
|
1151
1175
|
fn_result = nil
|
|
1152
1176
|
fn_error = nil
|
|
1153
1177
|
replay_error = nil
|
|
1178
|
+
replay_duration_ms = nil
|
|
1179
|
+
replay_started = nil
|
|
1154
1180
|
# Client-side correlation id that tags this item's replay spans so the
|
|
1155
1181
|
# server can echo back the row id it minted (resolved in run()'s
|
|
1156
1182
|
# complete-replay loop). Carried on the item under :_sdk_trace_id, never
|
|
@@ -1172,6 +1198,7 @@ module Bitfab
|
|
|
1172
1198
|
mock_overrides:,
|
|
1173
1199
|
fetch_span_output:,
|
|
1174
1200
|
db_branch_lease:,
|
|
1201
|
+
db_branch_timings:,
|
|
1175
1202
|
source_bitfab_trace_id:
|
|
1176
1203
|
) do
|
|
1177
1204
|
begin
|
|
@@ -1190,17 +1217,26 @@ module Bitfab
|
|
|
1190
1217
|
end
|
|
1191
1218
|
if replay_error.nil?
|
|
1192
1219
|
begin
|
|
1220
|
+
replay_started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1193
1221
|
fn_result = if kwargs.empty?
|
|
1194
1222
|
receiver.send(method_name, *args)
|
|
1195
1223
|
else
|
|
1196
1224
|
receiver.send(method_name, *args, **kwargs)
|
|
1197
1225
|
end
|
|
1226
|
+
replay_duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - replay_started) * 1000).round
|
|
1198
1227
|
rescue => e
|
|
1228
|
+
# The method ran and raised, so it still has a duration.
|
|
1229
|
+
if replay_started
|
|
1230
|
+
replay_duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - replay_started) * 1000).round
|
|
1231
|
+
end
|
|
1199
1232
|
fn_error = e
|
|
1200
1233
|
end
|
|
1201
1234
|
end
|
|
1202
1235
|
end
|
|
1203
1236
|
rescue => e
|
|
1237
|
+
if replay_started
|
|
1238
|
+
replay_duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - replay_started) * 1000).round
|
|
1239
|
+
end
|
|
1204
1240
|
replay_error = e
|
|
1205
1241
|
end
|
|
1206
1242
|
|
|
@@ -1212,9 +1248,12 @@ module Bitfab
|
|
|
1212
1248
|
error: item_error&.message,
|
|
1213
1249
|
trace_error: fn_error,
|
|
1214
1250
|
replay_error:,
|
|
1215
|
-
duration_ms:
|
|
1251
|
+
duration_ms: replay_duration_ms,
|
|
1252
|
+
original_duration_ms: metrics[:original_duration_ms],
|
|
1253
|
+
original_tokens: metrics[:original_tokens],
|
|
1254
|
+
original_model: metrics[:original_model],
|
|
1216
1255
|
tokens: metrics[:tokens],
|
|
1217
|
-
model: metrics[:
|
|
1256
|
+
model: metrics[:original_model],
|
|
1218
1257
|
# Written in by run() from the complete-replay response once the server
|
|
1219
1258
|
# has minted this replay trace's row. Nil until then: the client-side
|
|
1220
1259
|
# correlation id (below) is never surfaced as the public :trace_id.
|
|
@@ -1225,7 +1264,8 @@ module Bitfab
|
|
|
1225
1264
|
# Deprecated aliases for original_trace_id/original_span_id.
|
|
1226
1265
|
source_trace_id: source_bitfab_trace_id,
|
|
1227
1266
|
source_span_id: input_source_span_id,
|
|
1228
|
-
db_snapshot_ref
|
|
1267
|
+
db_snapshot_ref:,
|
|
1268
|
+
db_branch_timings:
|
|
1229
1269
|
}
|
|
1230
1270
|
end
|
|
1231
1271
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
# Installed command that loads a project-owned registry module.
|
|
7
|
+
module ReplayCommand
|
|
8
|
+
USAGE = "Usage: bitfab-replay --registry <path> <pipeline> [replay options]"
|
|
9
|
+
HELP = "#{USAGE}\nRun bitfab-replay --registry <path> --help to list replay options."
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def run(argv: ARGV, stdout: $stdout, stderr: $stderr, registry_loader: nil)
|
|
14
|
+
registry_path, replay_argv = parse(argv)
|
|
15
|
+
loader = registry_loader || method(:load_registry)
|
|
16
|
+
registry = loader.call(registry_path)
|
|
17
|
+
ReplayCli.run(registry, argv: replay_argv, stdout:, stderr:)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def main(argv: ARGV, stdout: $stdout, stderr: $stderr)
|
|
21
|
+
if (argv.include?("--help") || argv.include?("-h")) && !argv.include?("--registry")
|
|
22
|
+
stdout.puts HELP
|
|
23
|
+
return 0
|
|
24
|
+
end
|
|
25
|
+
run(argv:, stdout:, stderr:)
|
|
26
|
+
0
|
|
27
|
+
rescue => error
|
|
28
|
+
stderr.puts error.message
|
|
29
|
+
1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def parse(argv)
|
|
33
|
+
values = argv.dup
|
|
34
|
+
registry_index = values.index("--registry")
|
|
35
|
+
registry_path = values[registry_index + 1] if registry_index
|
|
36
|
+
unless registry_path && !registry_path.start_with?("--")
|
|
37
|
+
raise OptionParser::MissingArgument, USAGE
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
values.slice!(registry_index, 2)
|
|
41
|
+
raise OptionParser::MissingArgument, USAGE if values.empty?
|
|
42
|
+
|
|
43
|
+
[File.expand_path(registry_path), values]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def load_registry(path)
|
|
47
|
+
container = Module.new
|
|
48
|
+
container.module_eval(File.read(path), path)
|
|
49
|
+
registry = container.const_get(:REGISTRY, false) if container.const_defined?(:REGISTRY, false)
|
|
50
|
+
unless registry.is_a?(ReplayRegistry)
|
|
51
|
+
raise ArgumentError,
|
|
52
|
+
"Replay registry '#{path}' must define REGISTRY as a Bitfab::ReplayRegistry."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
registry
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
module Bitfab
|
|
7
|
+
# One project function exposed through the SDK-owned replay command.
|
|
8
|
+
ReplayRegistration = Data.define(
|
|
9
|
+
:client,
|
|
10
|
+
:receiver,
|
|
11
|
+
:method_name,
|
|
12
|
+
:trace_function_key,
|
|
13
|
+
:options,
|
|
14
|
+
:options_factory
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
ReplayRegistryContext = Data.define(:params)
|
|
18
|
+
|
|
19
|
+
# Project-owned registry of production replay roots.
|
|
20
|
+
class ReplayRegistry
|
|
21
|
+
OPTION_NAMES = %i[
|
|
22
|
+
limit trace_ids name max_concurrency code_change_description
|
|
23
|
+
code_change_files experiment_group_id dataset_id grader_ids mock
|
|
24
|
+
mock_override adapt_inputs db_branch
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
def initialize
|
|
28
|
+
@entries = {}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def names
|
|
32
|
+
@entries.keys.freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Register the exact traced receiver method production invokes.
|
|
36
|
+
#
|
|
37
|
+
# A +bitfab_span+ method carries its trace function key automatically.
|
|
38
|
+
# Handler-instrumented or otherwise plain methods must pass
|
|
39
|
+
# +trace_function_key:+ explicitly. Pass executable per-function replay
|
|
40
|
+
# behavior such as +mock_override:+ and +adapt_inputs:+ as keyword options;
|
|
41
|
+
# the installed command forwards them to +Bitfab::Client#replay+.
|
|
42
|
+
def register(name, receiver, method_name, client: nil, trace_function_key: nil, options_factory: nil, **options)
|
|
43
|
+
raise ArgumentError, "Replay registry names cannot be empty." if name.to_s.empty?
|
|
44
|
+
raise ArgumentError, "Replay registry already contains '#{name}'." if @entries.key?(name.to_s)
|
|
45
|
+
if options_factory && !options_factory.respond_to?(:call)
|
|
46
|
+
raise ArgumentError, "Replay registry options_factory must be callable."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
key = trace_function_key || Traceable.trace_function_key_for(receiver, method_name)
|
|
50
|
+
unless key
|
|
51
|
+
raise ArgumentError,
|
|
52
|
+
"Replay registry entry uses a plain method. Set trace_function_key: " \
|
|
53
|
+
"to the key its production handler records."
|
|
54
|
+
end
|
|
55
|
+
validate_options(options)
|
|
56
|
+
|
|
57
|
+
@entries[name.to_s] = ReplayRegistration.new(
|
|
58
|
+
client: client || Bitfab.client,
|
|
59
|
+
receiver:,
|
|
60
|
+
method_name: method_name.to_sym,
|
|
61
|
+
trace_function_key: key,
|
|
62
|
+
options: options.freeze,
|
|
63
|
+
options_factory:
|
|
64
|
+
)
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def fetch(name)
|
|
69
|
+
@entries.fetch(name)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate_options(options)
|
|
73
|
+
unknown = options.keys - OPTION_NAMES
|
|
74
|
+
return if unknown.empty?
|
|
75
|
+
|
|
76
|
+
raise ArgumentError,
|
|
77
|
+
"Unknown replay registry option(s): #{unknown.sort.join(", ")}. " \
|
|
78
|
+
"Allowed options: #{OPTION_NAMES.sort.join(", ")}."
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Argument parsing and output for the SDK-owned replay command.
|
|
83
|
+
module ReplayCli
|
|
84
|
+
module_function
|
|
85
|
+
|
|
86
|
+
def run(registry, argv: ARGV, stdout: $stdout, stderr: $stderr)
|
|
87
|
+
args = parse(registry, argv.dup)
|
|
88
|
+
registration = registry.fetch(args.fetch(:pipeline))
|
|
89
|
+
options = registration.options.dup
|
|
90
|
+
params = load_parameters(args[:params], args.fetch(:param, []))
|
|
91
|
+
if registration.options_factory
|
|
92
|
+
dynamic_options = registration.options_factory.call(ReplayRegistryContext.new(params:))
|
|
93
|
+
unless dynamic_options.is_a?(Hash)
|
|
94
|
+
raise ArgumentError, "Replay registry options_factory must return a Hash."
|
|
95
|
+
end
|
|
96
|
+
registry.validate_options(dynamic_options)
|
|
97
|
+
options.merge!(dynamic_options)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if options[:trace_ids] && options[:dataset_id]
|
|
101
|
+
raise ArgumentError,
|
|
102
|
+
"Replay registry options trace_ids and dataset_id select different sources and cannot be used together."
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if args[:trace_ids]
|
|
106
|
+
options.delete(:limit)
|
|
107
|
+
options.delete(:dataset_id)
|
|
108
|
+
options[:trace_ids] = args[:trace_ids]
|
|
109
|
+
elsif args[:dataset_id]
|
|
110
|
+
options.delete(:trace_ids)
|
|
111
|
+
options.delete(:limit)
|
|
112
|
+
elsif args[:limit]
|
|
113
|
+
options.delete(:trace_ids)
|
|
114
|
+
options.delete(:dataset_id)
|
|
115
|
+
options[:limit] = args[:limit]
|
|
116
|
+
elsif options[:trace_ids] || options[:dataset_id]
|
|
117
|
+
options.delete(:limit)
|
|
118
|
+
else
|
|
119
|
+
options[:limit] = options.fetch(:limit, 10)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
%i[name max_concurrency experiment_group_id dataset_id grader_ids mock].each do |key|
|
|
123
|
+
options[key] = args[key] unless args[key].nil?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
unless args[:db_branch].nil?
|
|
127
|
+
if args[:db_branch]
|
|
128
|
+
configured = options[:db_branch]
|
|
129
|
+
options[:db_branch] = (configured.nil? || configured == false) ? true : configured
|
|
130
|
+
else
|
|
131
|
+
options[:db_branch] = false
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if args[:code_change]
|
|
136
|
+
code_change = load_code_change(args[:code_change])
|
|
137
|
+
options[:code_change_description] = code_change.fetch("description")
|
|
138
|
+
options[:code_change_files] = code_change.fetch("files")
|
|
139
|
+
elsif args[:no_code_change]
|
|
140
|
+
options[:code_change_description] = nil
|
|
141
|
+
options[:code_change_files] = nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
reporter = Bitfab.method(:report_replay_progress)
|
|
145
|
+
options[:on_item_start] = reporter
|
|
146
|
+
options[:on_item_finish] = reporter
|
|
147
|
+
|
|
148
|
+
count = options[:trace_ids]&.length || options[:limit] || "dataset"
|
|
149
|
+
stderr.puts "[replay] Replaying #{count} traces from \"#{registration.trace_function_key}\"..."
|
|
150
|
+
|
|
151
|
+
result = begin
|
|
152
|
+
registration.client.replay(
|
|
153
|
+
registration.receiver,
|
|
154
|
+
registration.method_name,
|
|
155
|
+
trace_function_key: registration.trace_function_key,
|
|
156
|
+
**options
|
|
157
|
+
)
|
|
158
|
+
rescue ReplayError => error
|
|
159
|
+
error.items.each do |item|
|
|
160
|
+
item_error = item[:trace_error] || item[:replay_error] || item[:error]
|
|
161
|
+
stderr.puts "#{item[:original_trace_id]}: #{item_error}"
|
|
162
|
+
end
|
|
163
|
+
raise
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
render_summary(args.fetch(:pipeline), result, stderr)
|
|
167
|
+
stdout.puts Bitfab.serialize_replay_result(result)
|
|
168
|
+
result
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def parse(registry, argv)
|
|
172
|
+
if argv.include?("--db-branch") && argv.include?("--no-db-branch")
|
|
173
|
+
raise OptionParser::InvalidArgument, "--db-branch and --no-db-branch cannot be used together"
|
|
174
|
+
end
|
|
175
|
+
args = {}
|
|
176
|
+
parser = OptionParser.new do |options|
|
|
177
|
+
options.banner = "Usage: bitfab-replay <#{registry.names.join("|")}> [options]"
|
|
178
|
+
options.on("--limit N", Integer) { |value| args[:limit] = positive_integer("--limit", value) }
|
|
179
|
+
options.on("--trace-ids IDS") { |value| args[:trace_ids] = comma_separated("--trace-ids", value) }
|
|
180
|
+
options.on("--name NAME") { |value| args[:name] = value }
|
|
181
|
+
options.on("--concurrency N", Integer) do |value|
|
|
182
|
+
args[:max_concurrency] = positive_integer("--concurrency", value)
|
|
183
|
+
end
|
|
184
|
+
options.on("--max-concurrency N", Integer) do |value|
|
|
185
|
+
args[:max_concurrency] = positive_integer("--max-concurrency", value)
|
|
186
|
+
end
|
|
187
|
+
options.on("--code-change PATH") { |value| args[:code_change] = value }
|
|
188
|
+
options.on("--experiment-group-id UUID") { |value| args[:experiment_group_id] = value }
|
|
189
|
+
options.on("--dataset-id UUID") { |value| args[:dataset_id] = value }
|
|
190
|
+
options.on("--grader-ids IDS") { |value| args[:grader_ids] = comma_separated("--grader-ids", value) }
|
|
191
|
+
options.on("--mock STRATEGY", %w[none all marked]) { |value| args[:mock] = value }
|
|
192
|
+
options.on("--db-branch") { args[:db_branch] = true }
|
|
193
|
+
options.on("--no-db-branch") { args[:db_branch] = false }
|
|
194
|
+
options.on("--no-code-change") { args[:no_code_change] = true }
|
|
195
|
+
options.on("--params PATH") { |value| args[:params] = value }
|
|
196
|
+
options.on("--param NAME=VALUE") { |value| (args[:param] ||= []) << value }
|
|
197
|
+
end
|
|
198
|
+
parser.parse!(argv)
|
|
199
|
+
if args[:trace_ids] && args[:dataset_id]
|
|
200
|
+
raise OptionParser::InvalidArgument,
|
|
201
|
+
"--trace-ids and --dataset-id select different replay sources and cannot be used together"
|
|
202
|
+
end
|
|
203
|
+
if args[:code_change] && args[:no_code_change]
|
|
204
|
+
raise OptionParser::InvalidArgument, "--code-change and --no-code-change cannot be used together"
|
|
205
|
+
end
|
|
206
|
+
pipeline = argv.shift
|
|
207
|
+
raise OptionParser::MissingArgument, parser.banner unless pipeline && registry.names.include?(pipeline)
|
|
208
|
+
raise OptionParser::InvalidArgument, "unexpected arguments: #{argv.join(" ")}" unless argv.empty?
|
|
209
|
+
|
|
210
|
+
args.merge(pipeline:)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def positive_integer(flag, value)
|
|
214
|
+
raise OptionParser::InvalidArgument, "#{flag} must be a positive integer" if value < 1
|
|
215
|
+
|
|
216
|
+
value
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def comma_separated(flag, value)
|
|
220
|
+
values = value.split(",").map(&:strip).reject(&:empty?)
|
|
221
|
+
raise OptionParser::InvalidArgument, "#{flag} must contain at least one value" if values.empty?
|
|
222
|
+
|
|
223
|
+
values
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def load_code_change(path)
|
|
227
|
+
value = JSON.parse(File.read(path))
|
|
228
|
+
unless value.is_a?(Hash) && value["description"].is_a?(String) && value["files"].is_a?(Array)
|
|
229
|
+
raise ArgumentError, "Invalid --code-change file '#{path}': expected { description, files }."
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
value
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def parse_parameter(raw)
|
|
236
|
+
key, separator, value = raw.partition("=")
|
|
237
|
+
key = key.strip
|
|
238
|
+
if separator.empty? || key.empty?
|
|
239
|
+
raise OptionParser::InvalidArgument,
|
|
240
|
+
"Invalid --param '#{raw}': expected a non-empty name=value pair"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
parsed = begin
|
|
244
|
+
JSON.parse(value)
|
|
245
|
+
rescue JSON::ParserError
|
|
246
|
+
value
|
|
247
|
+
end
|
|
248
|
+
[key, parsed]
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def load_parameters(path, raw_parameters)
|
|
252
|
+
params = {}
|
|
253
|
+
if path
|
|
254
|
+
value = JSON.parse(File.read(path))
|
|
255
|
+
unless value.is_a?(Hash)
|
|
256
|
+
raise ArgumentError, "Invalid --params file '#{path}': expected a JSON object."
|
|
257
|
+
end
|
|
258
|
+
params.merge!(value)
|
|
259
|
+
end
|
|
260
|
+
raw_parameters.each do |raw|
|
|
261
|
+
key, value = parse_parameter(raw)
|
|
262
|
+
params[key] = value
|
|
263
|
+
end
|
|
264
|
+
params.freeze
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def render_summary(pipeline, result, stderr)
|
|
268
|
+
same = 0
|
|
269
|
+
changed = 0
|
|
270
|
+
errors = 0
|
|
271
|
+
result[:items].each do |item|
|
|
272
|
+
if item[:error]
|
|
273
|
+
errors += 1
|
|
274
|
+
elsif item[:result] == item[:original_output]
|
|
275
|
+
same += 1
|
|
276
|
+
else
|
|
277
|
+
changed += 1
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
stderr.puts "\n─── Summary ───"
|
|
282
|
+
stderr.puts " Pipeline: #{pipeline}"
|
|
283
|
+
stderr.puts " Replayed: #{result[:items].length}"
|
|
284
|
+
stderr.puts " Same: #{same}"
|
|
285
|
+
stderr.puts " Changed: #{changed}"
|
|
286
|
+
stderr.puts " Errors: #{errors}" if errors > 0
|
|
287
|
+
stderr.puts "\n #{result[:test_run_url]}"
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
data/lib/bitfab/version.rb
CHANGED
data/lib/bitfab.rb
CHANGED
|
@@ -15,6 +15,8 @@ require_relative "bitfab/replay"
|
|
|
15
15
|
require_relative "bitfab/replay_branch"
|
|
16
16
|
require_relative "bitfab/client"
|
|
17
17
|
require_relative "bitfab/traceable"
|
|
18
|
+
require_relative "bitfab/replay_registry"
|
|
19
|
+
require_relative "bitfab/replay_cli"
|
|
18
20
|
|
|
19
21
|
module Bitfab
|
|
20
22
|
# No-op span handle returned when outside a span context.
|
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bitfab
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.38.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Harvest Team
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
@@ -131,11 +131,13 @@ description: Client library for sending function execution spans to the Bitfab A
|
|
|
131
131
|
Provides automatic tracing with nested span support.
|
|
132
132
|
email:
|
|
133
133
|
- team@goharvest.ai
|
|
134
|
-
executables:
|
|
134
|
+
executables:
|
|
135
|
+
- bitfab-replay
|
|
135
136
|
extensions: []
|
|
136
137
|
extra_rdoc_files: []
|
|
137
138
|
files:
|
|
138
139
|
- README.md
|
|
140
|
+
- exe/bitfab-replay
|
|
139
141
|
- lib/bitfab.rb
|
|
140
142
|
- lib/bitfab/client.rb
|
|
141
143
|
- lib/bitfab/compress.rb
|
|
@@ -147,6 +149,8 @@ files:
|
|
|
147
149
|
- lib/bitfab/payload_budget.rb
|
|
148
150
|
- lib/bitfab/replay.rb
|
|
149
151
|
- lib/bitfab/replay_branch.rb
|
|
152
|
+
- lib/bitfab/replay_cli.rb
|
|
153
|
+
- lib/bitfab/replay_registry.rb
|
|
150
154
|
- lib/bitfab/serialize.rb
|
|
151
155
|
- lib/bitfab/span_context.rb
|
|
152
156
|
- lib/bitfab/traceable.rb
|