Class: ThrottleMachines::Storage::Memory

Inherits:
Base
  • Object
show all
Defined in:
lib/throttle_machines/storage/memory.rb

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#with_timeout

Constructor Details

#initialize(options = {}) ⇒ Memory

Returns a new instance of Memory.



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/throttle_machines/storage/memory.rb', line 8

def initialize(options = {})
  super
  @counters = Concurrent::Hash.new
  @gcra_states = Concurrent::Hash.new
  @token_buckets = Concurrent::Hash.new

  # Use a striped lock pattern - pool of locks for fine-grained concurrency
  @lock_pool_size = options[:lock_pool_size] || 32
  @locks = Array.new(@lock_pool_size) { Concurrent::ReadWriteLock.new }

  # Background cleanup thread
  @cleanup_interval = options[:cleanup_interval] || 60
  @shutdown = false
  @cleanup_thread = start_cleanup_thread if options[:auto_cleanup] != false

  # Ensure cleanup on garbage collection
  ObjectSpace.define_finalizer(self, self.class.finalizer(@cleanup_thread))
end

Class Method Details

.finalizer(cleanup_thread) ⇒ Object



27
28
29
# File 'lib/throttle_machines/storage/memory.rb', line 27

def self.finalizer(cleanup_thread)
  proc { cleanup_thread&.kill }
end

Instance Method Details

#check_gcra_limit(key, emission_interval, delay_tolerance, ttl) ⇒ Object

GCRA operations (atomic simulation)



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/throttle_machines/storage/memory.rb', line 83

def check_gcra_limit(key, emission_interval, delay_tolerance, ttl)
  with_write_lock(key) do
    now = current_time
    state = @gcra_states[key] || { tat: 0.0 }

    tat = [state[:tat], now].max
    allow = tat - now <= delay_tolerance

    if allow
      new_tat = tat + emission_interval
      @gcra_states[key] = { tat: new_tat, expires_at: now + ttl }
    end

    {
      allowed: allow,
      retry_after: allow ? 0 : (tat - now - delay_tolerance),
      tat: tat
    }
  end
end

#check_token_bucket(key, capacity, refill_rate, ttl) ⇒ Object

Token bucket operations (atomic simulation)



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/throttle_machines/storage/memory.rb', line 121

def check_token_bucket(key, capacity, refill_rate, ttl)
  with_write_lock(key) do
    now = current_time
    bucket = @token_buckets[key] || { tokens: capacity, last_refill: now }

    # Refill tokens
    elapsed = now - bucket[:last_refill]
    tokens_to_add = elapsed * refill_rate
    bucket[:tokens] = [bucket[:tokens] + tokens_to_add, capacity].min
    bucket[:last_refill] = now

    # Check if we can consume a token
    if bucket[:tokens] >= 1
      bucket[:tokens] -= 1
      @token_buckets[key] = bucket.merge(expires_at: now + ttl)

      {
        allowed: true,
        retry_after: 0,
        tokens_remaining: bucket[:tokens].floor
      }
    else
      retry_after = (1 - bucket[:tokens]) / refill_rate

      {
        allowed: false,
        retry_after: retry_after,
        tokens_remaining: 0
      }
    end
  end
end

#clear(pattern = nil) ⇒ Object

Utility operations



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/throttle_machines/storage/memory.rb', line 185

def clear(pattern = nil)
  if pattern
    regex = Regexp.new(pattern.gsub('*', '.*'))

    # Clear matching keys from all stores
    [@counters, @gcra_states, @token_buckets].each do |store|
      store.each_key do |k|
        store.delete(k) if k&.match?(regex)
      end
    end
  else
    @counters.clear
    @gcra_states.clear
    @token_buckets.clear
  end
end

#get_counter(key, window) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
# File 'lib/throttle_machines/storage/memory.rb', line 53

def get_counter(key, window)
  window_key = "#{key}:#{window}"

  with_read_lock(window_key) do
    counter = @counters[window_key]
    return 0 unless counter
    return 0 if counter[:expires_at] <= current_time

    counter[:count]
  end
end

#get_counter_ttl(key, window) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
# File 'lib/throttle_machines/storage/memory.rb', line 65

def get_counter_ttl(key, window)
  window_key = "#{key}:#{window}"

  with_read_lock(window_key) do
    counter = @counters[window_key]
    return 0 unless counter

    ttl = counter[:expires_at] - current_time
    [ttl, 0].max
  end
end

#healthy?Boolean

Returns:

  • (Boolean)


202
203
204
# File 'lib/throttle_machines/storage/memory.rb', line 202

def healthy?
  true
end

#increment_counter(key, window, amount = 1) ⇒ Object

Rate limiting operations



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/throttle_machines/storage/memory.rb', line 32

def increment_counter(key, window, amount = 1)
  window_key = "#{key}:#{window}"

  with_write_lock(window_key) do
    now = current_time
    # Fetch fresh value inside the lock to ensure consistency
    counter = @counters[window_key]

    if counter.nil? || counter[:expires_at] <= now
      # Create or reset counter atomically
      new_count = amount
      @counters[window_key] = { count: new_count, expires_at: now + window }
    else
      # Increment existing counter atomically
      new_count = counter[:count] + amount
      @counters[window_key] = { count: new_count, expires_at: counter[:expires_at] }
    end
    new_count
  end
end

#peek_gcra_limit(key, _emission_interval, delay_tolerance) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/throttle_machines/storage/memory.rb', line 104

def peek_gcra_limit(key, _emission_interval, delay_tolerance)
  with_read_lock(key) do
    now = current_time
    state = @gcra_states[key] || { tat: 0.0 }

    tat = [state[:tat], now].max
    allow = tat - now <= delay_tolerance

    {
      allowed: allow,
      retry_after: allow ? 0 : (tat - now - delay_tolerance),
      tat: tat
    }
  end
end

#peek_token_bucket(key, capacity, refill_rate) ⇒ Object



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/throttle_machines/storage/memory.rb', line 154

def peek_token_bucket(key, capacity, refill_rate)
  with_read_lock(key) do
    now = current_time
    bucket = @token_buckets[key] || { tokens: capacity, last_refill: now }

    # Calculate tokens without modifying state
    elapsed = now - bucket[:last_refill]
    tokens_to_add = elapsed * refill_rate
    current_tokens = [bucket[:tokens] + tokens_to_add, capacity].min

    if current_tokens >= 1
      {
        allowed: true,
        retry_after: 0,
        tokens_remaining: (current_tokens - 1).floor
      }
    else
      retry_after = (1 - current_tokens) / refill_rate

      {
        allowed: false,
        retry_after: retry_after,
        tokens_remaining: 0
      }
    end
  end
end

#reset_counter(key, window) ⇒ Object



77
78
79
80
# File 'lib/throttle_machines/storage/memory.rb', line 77

def reset_counter(key, window)
  window_key = "#{key}:#{window}"
  with_write_lock(window_key) { @counters.delete(window_key) }
end

#shutdownObject



206
207
208
209
210
211
# File 'lib/throttle_machines/storage/memory.rb', line 206

def shutdown
  @shutdown = true
  @cleanup_thread&.join(1) # Wait up to 1 second for graceful shutdown
  @cleanup_thread&.kill if @cleanup_thread&.alive?
  @cleanup_thread = nil
end