Class: BSV::Overlay::HostReputationTracker
- Inherits:
-
Object
- Object
- BSV::Overlay::HostReputationTracker
- Defined in:
- lib/bsv/overlay/host_reputation_tracker.rb
Overview
Tracks per-host reputation for overlay query dispatch.
Records success and failure events per host, maintains an EWMA latency estimate, applies exponential backoff after repeated failures, and ranks a candidate host list so callers can prefer fast, reliable hosts.
Thread-safe via an internal Mutex.
An optional store adapter may be supplied for persistence. The adapter must respond to #get(key) and #set(key, value). When provided, each updated entry is persisted and unknown hosts are loaded on first access.
Constant Summary collapse
- DEFAULT_LATENCY_MS =
Fallback latency used when no measurements exist yet, or when an invalid (negative / non-finite) value is reported.
1500- LATENCY_SMOOTHING_FACTOR =
EWMA smoothing factor (α). Higher values give more weight to recent observations; lower values produce a smoother, more stable estimate.
0.25- BASE_BACKOFF_MS =
Starting backoff duration in milliseconds.
1000- MAX_BACKOFF_MS =
Maximum backoff duration in milliseconds (60 seconds).
60_000- FAILURE_PENALTY_MS =
Latency penalty added to a host’s score per consecutive failure.
400- SUCCESS_BONUS_MS =
Latency bonus subtracted from a host’s score per recorded success, capped at half the current average latency.
30- FAILURE_BACKOFF_GRACE =
Number of consecutive failures that are forgiven before backoff kicks in. A host may fail this many times without being penalised with a backoff window.
2
Instance Method Summary collapse
-
#initialize(store: nil) ⇒ HostReputationTracker
constructor
A new instance of HostReputationTracker.
-
#rank_hosts(hosts, now = Time.now) ⇒ Array<String>
Ranks
hostsfor query dispatch. -
#record_failure(host, reason = nil) ⇒ Object
Records a failed request to
host. -
#record_success(host, latency_ms) ⇒ Object
Records a successful request to
host. -
#reset ⇒ Object
Clears all in-memory reputation data.
-
#snapshot(host) ⇒ Hash?
Returns a frozen copy of the internal entry hash for
host, ornilif the host is unknown and not in the store.
Constructor Details
#initialize(store: nil) ⇒ HostReputationTracker
Returns a new instance of HostReputationTracker.
44 45 46 47 48 |
# File 'lib/bsv/overlay/host_reputation_tracker.rb', line 44 def initialize(store: nil) @entries = {} @mutex = Mutex.new @store = store end |
Instance Method Details
#rank_hosts(hosts, now = Time.now) ⇒ Array<String>
Ranks hosts for query dispatch.
Steps:
-
Filter nil and empty-string entries.
-
Deduplicate preserving first occurrence order.
-
Compute a score for each host.
-
Sort: available hosts first (score asc, then total_successes desc, then original position asc), followed by backed-off hosts (backoff_until asc).
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/bsv/overlay/host_reputation_tracker.rb', line 134 def rank_hosts(hosts, now = Time.now) @mutex.synchronize do filtered = hosts.select { |h| h.is_a?(String) && !h.empty? } unique = deduplicate(filtered) available = [] backed_off = [] unique.each_with_index do |host, idx| entry = fetch_or_create_locked(host) if in_backoff?(entry, now) backed_off << { host: host, entry: entry, idx: idx } else available << { host: host, entry: entry, idx: idx, score: compute_score(entry, now) } end end sorted_available = available.sort_by { |h| [h[:score], -h[:entry][:total_successes], h[:idx]] } sorted_backed_off = backed_off.sort_by { |h| h[:entry][:backoff_until].to_f } (sorted_available + sorted_backed_off).map { |h| h[:host] } end end |
#record_failure(host, reason = nil) ⇒ Object
Records a failed request to host.
Increments failure counters. Once consecutive failures exceed FAILURE_BACKOFF_GRACE, an exponential backoff window is set.
DNS-level errors (SocketError, Errno::ECONNREFUSED, or messages containing ‘getaddrinfo’ or ‘Failed to fetch’) skip the grace period entirely — backoff is applied from the very first such failure.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/bsv/overlay/host_reputation_tracker.rb', line 92 def record_failure(host, reason = nil) @mutex.synchronize do entry = fetch_or_create(host) now = Time.now entry[:total_failures] += 1 entry[:last_error] = reason.to_s if reason entry[:last_updated_at] = now if dns_error?(reason) # Skip grace period but continue ramping — ensure consecutive_failures # is at least past grace, then keep incrementing on repeated DNS errors. entry[:consecutive_failures] = [entry[:consecutive_failures] + 1, FAILURE_BACKOFF_GRACE + 1].max else entry[:consecutive_failures] += 1 end consecutive = entry[:consecutive_failures] if consecutive > FAILURE_BACKOFF_GRACE exponent = consecutive - FAILURE_BACKOFF_GRACE - 1 backoff_ms = [BASE_BACKOFF_MS * (2**exponent), MAX_BACKOFF_MS].min entry[:backoff_until] = now + (backoff_ms / 1000.0) end persist(host, entry) end end |
#record_success(host, latency_ms) ⇒ Object
Records a successful request to host.
Updates the EWMA latency estimate (first success sets the average directly; subsequent successes apply smoothing). Resets consecutive failure count and clears any active backoff window.
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/bsv/overlay/host_reputation_tracker.rb', line 58 def record_success(host, latency_ms) @mutex.synchronize do entry = fetch_or_create(host) safe_latency = valid_latency?(latency_ms) ? latency_ms.to_f : DEFAULT_LATENCY_MS.to_f entry[:avg_latency_ms] = if entry[:total_successes].zero? safe_latency else (entry[:avg_latency_ms] * (1 - LATENCY_SMOOTHING_FACTOR)) + (safe_latency * LATENCY_SMOOTHING_FACTOR) end entry[:last_latency_ms] = safe_latency entry[:total_successes] += 1 entry[:consecutive_failures] = 0 entry[:backoff_until] = nil entry[:last_updated_at] = Time.now persist(host, entry) end end |
#reset ⇒ Object
Clears all in-memory reputation data.
171 172 173 |
# File 'lib/bsv/overlay/host_reputation_tracker.rb', line 171 def reset @mutex.synchronize { @entries.clear } end |
#snapshot(host) ⇒ Hash?
Returns a frozen copy of the internal entry hash for host, or nil if the host is unknown and not in the store.
163 164 165 166 167 168 |
# File 'lib/bsv/overlay/host_reputation_tracker.rb', line 163 def snapshot(host) @mutex.synchronize do entry = @entries[host] || load_from_store(host) entry&.dup&.freeze end end |