Class: ActiveHarness::RateLimit::RiskHoldback

Inherits:
Object
  • Object
show all
Defined in:
lib/active_harness/rate_limit/risk_holdback.rb

Overview

Progressive hold-back for users who repeatedly submit risky requests.

Every RISKY_THRESHOLD blocked requests trigger a hold. The hold duration escalates with each offense:

Offense 1 (3rd  risky request) →  5 minutes
Offense 2 (6th  risky request) → 30 minutes
Offense 3+ (9th+ risky request) →  2 hours

Storage is in-memory (per-process). Thread-safe via Mutex.

Usage:

holdback = RiskHoldback.new
holdback.check!(user_id)           # before each request; raises if held
holdback.record_risky!(user_id)    # after guard blocks a request

Constant Summary collapse

RISKY_THRESHOLD =
3
HOLD_SCHEDULE =

5 min / 30 min / 2 h

[5 * 60, 30 * 60, 2 * 60 * 60].freeze

Instance Method Summary collapse

Constructor Details

#initialize(risky_threshold: RISKY_THRESHOLD) ⇒ RiskHoldback

Returns a new instance of RiskHoldback.

Parameters:

  • risky_threshold (Integer) (defaults to: RISKY_THRESHOLD)

    number of risky requests before a hold is applied



24
25
26
27
28
# File 'lib/active_harness/rate_limit/risk_holdback.rb', line 24

def initialize(risky_threshold: RISKY_THRESHOLD)
  @risky_threshold = risky_threshold
  @state           = {}
  @mutex           = Mutex.new
end

Instance Method Details

#check!(user_id) ⇒ Object

Raises if the user is currently held back due to risky behaviour.

Parameters:

  • user_id (String, Integer, nil)

    nil disables the check

Raises:



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/active_harness/rate_limit/risk_holdback.rb', line 52

def check!(user_id)
  return if user_id.nil?

  key = user_id.to_s
  @mutex.synchronize do
    s = @state[key]
    return unless s&.dig(:held_until)
    return if Time.now.to_f >= s[:held_until]

    remaining = (s[:held_until] - Time.now.to_f).ceil
    raise Errors::UserHoldbackError,
      "Requests paused due to repeated safety violations. " \
      "Retry in #{remaining}s (user: #{key})"
  end
end

#record_risky!(user_id) ⇒ Object

Records a risky (guard-blocked) request for the user. Applies a hold when the count reaches the next threshold multiple.

Parameters:

  • user_id (String, Integer, nil)

    nil is a no-op



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/active_harness/rate_limit/risk_holdback.rb', line 33

def record_risky!(user_id)
  return if user_id.nil?

  key = user_id.to_s
  @mutex.synchronize do
    s = @state[key] ||= { risky_count: 0, offense_count: 0, held_until: nil }
    s[:risky_count] += 1

    if (s[:risky_count] % @risky_threshold).zero?
      offense_idx      = [s[:offense_count], HOLD_SCHEDULE.size - 1].min
      s[:held_until]   = Time.now.to_f + HOLD_SCHEDULE[offense_idx]
      s[:offense_count] += 1
    end
  end
end