Class: Hyperion::TLS::HandshakeRateLimiter

Inherits:
Object
  • Object
show all
Defined in:
lib/hyperion/tls.rb

Overview

2.3-B: TLS handshake CPU throttle. Per-worker token bucket sized at the operator’s ‘tls.handshake_rate_limit` (handshakes/sec). Capacity == rate so a steady-state handshake stream of `rate` handshakes/sec passes cleanly while a burst above the rate is rate-limited; tokens refill at `rate` per second uniformly.

**When this fires.** A flood of new TLS handshakes (e.g., during a deployment when nginx restarts and reconnects everything) can starve regular requests of CPU — RSA/ECDHE handshakes are the most expensive op the server does. The bucket caps that starvation by closing the TCP connection at the listener edge before SSL_accept runs; clients see a clean TCP RST/FIN and retry. Default ‘:unlimited` keeps 2.2.0 behaviour.

**For nginx-fronted topologies** this is mostly defensive: nginx keeps long-lived upstream connections, so handshake rate is normally near-zero. Real value is for direct-exposure operators or staging environments where misconfiguration causes a handshake storm.

Concurrency. A Mutex-guarded refill+take. Hold time is one ‘Process.clock_gettime` + a couple of arithmetic ops — tens of nanoseconds. Contention is bounded by handshake rate (orders of magnitude lower than request rate), so the mutex is never on the hot per-request path.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(rate) ⇒ HandshakeRateLimiter

Build a limiter for ‘rate` handshakes/sec/worker, or `:unlimited` to short-circuit every `acquire_token!` to true (no throttle). Anything else raises ArgumentError so config typos surface at boot.



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/hyperion/tls.rb', line 322

def initialize(rate)
  if rate == :unlimited || rate.nil?
    @rate     = :unlimited
    @capacity = nil
    @tokens   = nil
    @last_refill_at = nil
    @mutex    = nil
  elsif rate.is_a?(Integer) && rate.positive?
    @rate     = rate
    @capacity = rate.to_f
    @tokens   = @capacity
    @last_refill_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    @mutex = Mutex.new
  else
    raise ArgumentError,
          "tls.handshake_rate_limit must be a positive integer or :unlimited (got #{rate.inspect})"
  end
  @rejected = 0
end

Instance Attribute Details

#capacityObject (readonly)

Returns the value of attribute capacity.



316
317
318
# File 'lib/hyperion/tls.rb', line 316

def capacity
  @capacity
end

#rateObject (readonly)

Returns the value of attribute rate.



316
317
318
# File 'lib/hyperion/tls.rb', line 316

def rate
  @rate
end

Instance Method Details

#acquire_token!Object

True when the bucket had a token to spend (handshake proceeds). False when the bucket is empty (caller should close the TCP connection without running SSL_accept — saves the CPU cost of the asymmetric crypto under handshake-storm conditions).



346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/hyperion/tls.rb', line 346

def acquire_token!
  return true if @rate == :unlimited

  @mutex.synchronize do
    refill_locked!
    if @tokens >= 1.0
      @tokens -= 1.0
      true
    else
      @rejected += 1
      false
    end
  end
end

#statsObject

Snapshot for stats / logging. ‘tokens` is the current bucket level (float), `rejected` is the cumulative count of denied handshake attempts since limiter construction.



364
365
366
367
368
369
370
371
# File 'lib/hyperion/tls.rb', line 364

def stats
  return { rate: :unlimited, rejected: 0 } if @rate == :unlimited

  @mutex.synchronize do
    refill_locked!
    { rate: @rate, capacity: @capacity, tokens: @tokens, rejected: @rejected }
  end
end