diff --git a/build.py b/build.py index 9b82104b..989525e4 100644 --- a/build.py +++ b/build.py @@ -133,7 +133,7 @@ class Module: name="v2-market-stream", language="Ruby", dir=ROOT / "v2" / "services", - build_cmd=["ruby", "-c", "market_stream.rb"], + build_cmd=["ruby", "market_stream_test_runner.rb"], clean_cmd=["echo", "Ruby has no build artifacts to clean"], build_dir=None, ), diff --git a/diagnostic/build-00000000.json b/diagnostic/build-00000000.json deleted file mode 100644 index 33e2ca62..00000000 --- a/diagnostic/build-00000000.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "generated_at": "2026-06-16T15:23:47.496569+00:00", - "commit": "00000000", - "diagnostic_logd": "diagnostic/build-00000000.logd", - "diagnostic_logd_error": null, - "chunked": false, - "chunk_size_bytes": null, - "password": "4c7df15ab09fbb066197", - "decrypt_command": "encryptly unpack diagnostic/build-00000000.logd --password 4c7df15ab09fbb066197", - "total_modules": 1, - "passed": 0, - "failed": 1, - "modules": [ - { - "name": "frailbox", - "status": "FAIL", - "elapsed_seconds": 0, - "artifact": null, - "output": "Command not found: [Errno 2] No such file or directory: 'make'" - } - ], - "pr_note": "Include this JSON diagnostic report and diagnostic/build-00000000.logd in your PR. Maintainers may ask you to remove these diagnostic artifacts before merging." -} diff --git a/diagnostic/build-00000000.logd b/diagnostic/build-00000000.logd deleted file mode 100644 index b5a046a2..00000000 --- a/diagnostic/build-00000000.logd +++ /dev/null @@ -1 +0,0 @@ -stub diagnostic logd placeholder diff --git a/diagnostic/build-6c985129-part001.logd b/diagnostic/build-6c985129-part001.logd new file mode 100644 index 00000000..91fcaefe Binary files /dev/null and b/diagnostic/build-6c985129-part001.logd differ diff --git a/diagnostic/build-6c985129-part002.logd b/diagnostic/build-6c985129-part002.logd new file mode 100644 index 00000000..76d0d3e9 Binary files /dev/null and b/diagnostic/build-6c985129-part002.logd differ diff --git a/diagnostic/build-6c985129.json b/diagnostic/build-6c985129.json new file mode 100644 index 00000000..9f552f43 --- /dev/null +++ b/diagnostic/build-6c985129.json @@ -0,0 +1,26 @@ +{ + "generated_at": "2026-06-20T15:53:17.144005+00:00", + "commit": "6c985129", + "diagnostic_logd": [ + "diagnostic\\build-6c985129-part001.logd", + "diagnostic\\build-6c985129-part002.logd" + ], + "diagnostic_logd_error": null, + "chunked": true, + "chunk_size_bytes": 41943040, + "password": "551a715c6ecfe495fc90", + "decrypt_command": "encryptly unpack diagnostic\\build-6c985129.logd --password 551a715c6ecfe495fc90", + "total_modules": 1, + "passed": 0, + "failed": 1, + "modules": [ + { + "name": "v2-market-stream", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] \u7cfb\u7edf\u627e\u4e0d\u5230\u6307\u5b9a\u7684\u6587\u4ef6\u3002" + } + ], + "pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic\\build-6c985129-part001.logd, diagnostic\\build-6c985129-part002.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging." +} diff --git a/v2/services/market_stream.rb b/v2/services/market_stream.rb index 6eeb63a4..1210a046 100644 --- a/v2/services/market_stream.rb +++ b/v2/services/market_stream.rb @@ -55,6 +55,8 @@ require 'sinatra/base' require 'logger' +require_relative 'market_stream_backoff' + # ===─ Fucking Constants =================================================================================─ V2_VERSION = '2.0.0' @@ -216,10 +218,11 @@ def schedule_reconnect # v2 reconnection: exponential backoff with max. We learned. We grew. return if Constants::WS_MAX_RECONNECTS && @reconnect_attempt >= Constants::WS_MAX_RECONNECTS - delay = [ - Constants::WS_RECONNECT_BASE * (2 ** @reconnect_attempt), - Constants::WS_RECONNECT_MAX - ].min + delay = MarketStreamBackoff.delay_for( + @reconnect_attempt, + base: Constants::WS_RECONNECT_BASE, + max: Constants::WS_RECONNECT_MAX + ) @reconnect_attempt += 1 diff --git a/v2/services/market_stream_backoff.rb b/v2/services/market_stream_backoff.rb new file mode 100644 index 00000000..57b7995a --- /dev/null +++ b/v2/services/market_stream_backoff.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module MarketStreamBackoff + module_function + + def delay_for(attempt, base: 1, max: 120, jitter: 0.0, rng: Random::DEFAULT) + raise ArgumentError, 'attempt must be non-negative' if attempt.negative? + raise ArgumentError, 'base must be positive' unless base.positive? + raise ArgumentError, 'max must be positive' unless max.positive? + raise ArgumentError, 'jitter must be between 0.0 and 1.0' unless jitter.between?(0.0, 1.0) + + capped_delay = [base.to_f * (2**attempt), max.to_f].min + return normalized(capped_delay) if jitter.zero? + + spread = capped_delay * jitter + jittered_delay = capped_delay + (((rng.rand * 2.0) - 1.0) * spread) + normalized([[jittered_delay, 0.0].max, max.to_f].min) + end + + def normalized(value) + value == value.to_i ? value.to_i : value + end +end diff --git a/v2/services/market_stream_backoff_test.rb b/v2/services/market_stream_backoff_test.rb new file mode 100644 index 00000000..d4fe699f --- /dev/null +++ b/v2/services/market_stream_backoff_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require_relative 'market_stream_backoff' + +class MarketStreamBackoffTest < Minitest::Test + class FixedRandom + def initialize(*values) + @values = values + end + + def rand + @values.shift || 0.5 + end + end + + def test_initial_delay_uses_base_delay + assert_equal 1, MarketStreamBackoff.delay_for(0, base: 1, max: 120) + end + + def test_delay_grows_exponentially + assert_equal 2, MarketStreamBackoff.delay_for(1, base: 1, max: 120) + assert_equal 64, MarketStreamBackoff.delay_for(6, base: 1, max: 120) + end + + def test_delay_is_capped_at_maximum + assert_equal 120, MarketStreamBackoff.delay_for(7, base: 1, max: 120) + assert_equal 120, MarketStreamBackoff.delay_for(20, base: 1, max: 120) + end + + def test_jitter_respects_lower_and_upper_bounds_before_cap + assert_in_delta 8.0, MarketStreamBackoff.delay_for(0, base: 10, max: 100, jitter: 0.2, rng: FixedRandom.new(0.0)), 0.0001 + assert_in_delta 12.0, MarketStreamBackoff.delay_for(0, base: 10, max: 100, jitter: 0.2, rng: FixedRandom.new(1.0)), 0.0001 + end + + def test_jitter_never_exceeds_maximum_cap + assert_equal 50, MarketStreamBackoff.delay_for(4, base: 10, max: 50, jitter: 0.5, rng: FixedRandom.new(1.0)) + end + + def test_invalid_parameters_are_rejected + assert_raises(ArgumentError) { MarketStreamBackoff.delay_for(-1) } + assert_raises(ArgumentError) { MarketStreamBackoff.delay_for(0, base: 0) } + assert_raises(ArgumentError) { MarketStreamBackoff.delay_for(0, max: 0) } + assert_raises(ArgumentError) { MarketStreamBackoff.delay_for(0, jitter: 1.1) } + end +end diff --git a/v2/services/market_stream_test_runner.rb b/v2/services/market_stream_test_runner.rb new file mode 100644 index 00000000..cfd2b80b --- /dev/null +++ b/v2/services/market_stream_test_runner.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'rbconfig' + +service_path = File.expand_path('market_stream.rb', __dir__) +unless system(RbConfig.ruby, '-c', service_path) + warn 'market_stream.rb syntax check failed' + exit 1 +end + +require_relative 'market_stream_backoff_test'