Class: BSV::Overlay::HostReputationTracker

Inherits:
Object
  • Object
show all
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

Constructor Details

#initialize(store: nil) ⇒ HostReputationTracker

Returns a new instance of HostReputationTracker.

Parameters:

  • store (#get, #set, nil) (defaults to: nil)

    optional persistence adapter



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:

  1. Filter nil and empty-string entries.

  2. Deduplicate preserving first occurrence order.

  3. Compute a score for each host.

  4. Sort: available hosts first (score asc, then total_successes desc, then original position asc), followed by backed-off hosts (backoff_until asc).

Parameters:

  • hosts (Array<String>)

    candidate host list

  • now (Time) (defaults to: Time.now)

    reference time (default: Time.now)

Returns:

  • (Array<String>)


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.

Parameters:

  • host (String)

    hostname or URL

  • reason (Exception, String, nil) (defaults to: nil)

    error that caused the 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.

Parameters:

  • host (String)

    hostname or URL

  • latency_ms (Numeric)

    observed round-trip time in milliseconds



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

#resetObject

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.

Parameters:

  • host (String)

Returns:

  • (Hash, nil)


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