rito 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +30 -0
  3. data/LICENSE +21 -0
  4. data/README.md +182 -0
  5. data/lib/rito/client.rb +118 -0
  6. data/lib/rito/connection.rb +39 -0
  7. data/lib/rito/endpoints/account_v1.rb +45 -0
  8. data/lib/rito/endpoints/base.rb +35 -0
  9. data/lib/rito/endpoints/lol/challenges_v1.rb +31 -0
  10. data/lib/rito/endpoints/lol/champion_mastery_v4.rb +33 -0
  11. data/lib/rito/endpoints/lol/champion_v3.rb +17 -0
  12. data/lib/rito/endpoints/lol/clash_v1.rb +27 -0
  13. data/lib/rito/endpoints/lol/league_v4.rb +58 -0
  14. data/lib/rito/endpoints/lol/match_v5.rb +34 -0
  15. data/lib/rito/endpoints/lol/spectator_v5.rb +22 -0
  16. data/lib/rito/endpoints/lol/status_v4.rb +15 -0
  17. data/lib/rito/endpoints/lol/summoner_v4.rb +35 -0
  18. data/lib/rito/endpoints/lol/tournament_v5.rb +69 -0
  19. data/lib/rito/endpoints/lor.rb +95 -0
  20. data/lib/rito/endpoints/tft.rb +123 -0
  21. data/lib/rito/endpoints/valorant.rb +92 -0
  22. data/lib/rito/errors.rb +55 -0
  23. data/lib/rito/instrumentation.rb +17 -0
  24. data/lib/rito/middleware/authentication.rb +21 -0
  25. data/lib/rito/middleware/rate_limit_observe.rb +32 -0
  26. data/lib/rito/middleware/rate_limit_throttle.rb +58 -0
  27. data/lib/rito/middleware/riot_errors.rb +81 -0
  28. data/lib/rito/models/account.rb +38 -0
  29. data/lib/rito/models/lol.rb +80 -0
  30. data/lib/rito/models/match.rb +71 -0
  31. data/lib/rito/models/summoner.rb +23 -0
  32. data/lib/rito/rate_limiting/adaptive_limiter.rb +71 -0
  33. data/lib/rito/rate_limiting/bucket.rb +83 -0
  34. data/lib/rito/rate_limiting/header_parser.rb +53 -0
  35. data/lib/rito/rate_limiting/null_limiter.rb +11 -0
  36. data/lib/rito/rate_limiting/redis_limiter.rb +153 -0
  37. data/lib/rito/routing.rb +79 -0
  38. data/lib/rito/version.rb +5 -0
  39. data/lib/rito.rb +71 -0
  40. metadata +109 -0
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Middleware
5
+ # Raises the mapped Rito::Error for 4xx/5xx responses.
6
+ # Registered below RateLimitObserve so observation happens first.
7
+ class RiotErrors < Faraday::Middleware
8
+ ERROR_CLASSES = {
9
+ 400 => Rito::BadRequest,
10
+ 401 => Rito::Unauthorized,
11
+ 403 => Rito::Forbidden,
12
+ 404 => Rito::NotFound,
13
+ 405 => Rito::MethodNotAllowed,
14
+ 415 => Rito::UnsupportedMediaType
15
+ }.freeze
16
+
17
+ def initialize(app, client:)
18
+ super(app)
19
+ @client = client
20
+ end
21
+
22
+ def call(env)
23
+ @app.call(env).on_complete { |completed| on_complete(completed) }
24
+ rescue Faraday::RetriableResponse => e
25
+ response = e.response
26
+ raise error_class_for(response.status).new(
27
+ "#{response.status} (after transport retries)",
28
+ response: response
29
+ )
30
+ rescue Faraday::ConnectionFailed => e
31
+ raise Rito::ConnectionError.new("#{e.class}: #{e.message}", wrapped: e)
32
+ rescue Faraday::TimeoutError, Timeout::Error => e
33
+ raise Rito::TimeoutError.new("#{e.class}: #{e.message}", wrapped: e)
34
+ rescue Faraday::SSLError => e
35
+ raise Rito::SSLError.new("#{e.class}: #{e.message}", wrapped: e)
36
+ end
37
+
38
+ def on_complete(env)
39
+ return if env.status.between?(200, 299)
40
+ # 5xx are retried by faraday-retry (registered above this middleware);
41
+ # raising here would abort the retry loop on the first attempt.
42
+ return if env.status >= 500
43
+
44
+ error_class = error_class_for(env.status)
45
+ return unless error_class
46
+
47
+ response = Faraday::Response.new(env)
48
+ if error_class == Rito::RateLimited
49
+ headers = env.response_headers
50
+ raise error_class.new(message(env),
51
+ retry_after: RateLimiting::HeaderParser.retry_after(headers),
52
+ limit_type: RateLimiting::HeaderParser.limit_type(headers),
53
+ response: response)
54
+ end
55
+
56
+ raise error_class.new(message(env), response: response)
57
+ end
58
+
59
+ private
60
+
61
+ def error_class_for(status)
62
+ if status == 429
63
+ Rito::RateLimited
64
+ elsif status.between?(500, 599)
65
+ status == 503 ? Rito::ServiceUnavailable : Rito::ServerError
66
+ else
67
+ ERROR_CLASSES[status]
68
+ end
69
+ end
70
+
71
+ def message(env)
72
+ parsed = env.response_body
73
+ parsed = JSON.parse(parsed) if parsed.is_a?(String) && !parsed.empty?
74
+ detail = parsed.is_a?(Hash) ? (parsed['message'] || parsed.dig('status', 'message')) : nil
75
+
76
+ base = "#{env.status} #{env.method.to_s.upcase} #{env.url}"
77
+ detail ? "#{base}: #{detail}" : base
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Models
5
+ Account = Data.define(:puuid, :game_name, :tag_line, :raw) do
6
+ def self.from_api(hash)
7
+ new(
8
+ puuid: hash['puuid'],
9
+ game_name: hash['gameName'],
10
+ tag_line: hash['tagLine'],
11
+ raw: hash.freeze
12
+ )
13
+ end
14
+ end
15
+
16
+ ActiveShard = Data.define(:puuid, :game, :active_shard, :raw) do
17
+ def self.from_api(hash)
18
+ new(
19
+ puuid: hash['puuid'],
20
+ game: hash['game'],
21
+ active_shard: hash['activeShard'],
22
+ raw: hash.freeze
23
+ )
24
+ end
25
+ end
26
+
27
+ AccountRegion = Data.define(:puuid, :game, :region, :raw) do
28
+ def self.from_api(hash)
29
+ new(
30
+ puuid: hash['puuid'],
31
+ game: hash['game'],
32
+ region: hash['region'],
33
+ raw: hash.freeze
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Models
5
+ ChampionMastery = Data.define(
6
+ :puuid, :champion_id, :champion_level, :champion_points,
7
+ :champion_points_until_next_level, :champion_points_since_last_level,
8
+ :last_play_time, :tokens_earned, :raw
9
+ ) do
10
+ def self.from_api(hash)
11
+ new(
12
+ puuid: hash['puuid'],
13
+ champion_id: hash['championId'],
14
+ champion_level: hash['championLevel'],
15
+ champion_points: hash['championPoints'],
16
+ champion_points_until_next_level: hash['championPointsUntilNextLevel'],
17
+ champion_points_since_last_level: hash['championPointsSinceLastLevel'],
18
+ last_play_time: hash['lastPlayTime'],
19
+ tokens_earned: hash['tokensEarned'],
20
+ raw: hash.freeze
21
+ )
22
+ end
23
+ end
24
+
25
+ ChampionInfo = Data.define(:sr, :newplayer, :raw) do
26
+ def self.from_api(hash)
27
+ new(
28
+ sr: hash['sr'],
29
+ newplayer: hash['newplayer'],
30
+ raw: hash.freeze
31
+ )
32
+ end
33
+ end
34
+
35
+ LeagueEntry = Data.define(
36
+ :puuid, :summoner_id, :queue_type, :tier, :rank, :league_id,
37
+ :league_points, :wins, :losses, :veteran, :fresh_blood, :hot_streak,
38
+ :inactive, :raw
39
+ ) do
40
+ def self.from_api(hash)
41
+ new(
42
+ puuid: hash['puuid'],
43
+ summoner_id: hash['summonerId'],
44
+ queue_type: hash['queueType'],
45
+ tier: hash['tier'],
46
+ rank: hash['rank'],
47
+ league_id: hash['leagueId'],
48
+ league_points: hash['leaguePoints'],
49
+ wins: hash['wins'],
50
+ losses: hash['losses'],
51
+ veteran: hash['veteran'],
52
+ fresh_blood: hash['freshBlood'],
53
+ hot_streak: hash['hotStreak'],
54
+ inactive: hash['inactive'],
55
+ raw: hash.freeze
56
+ )
57
+ end
58
+
59
+ def winrate
60
+ total = wins.to_i + losses.to_i
61
+ return nil if total.zero?
62
+
63
+ (wins.to_f / total * 100).round(2)
64
+ end
65
+ end
66
+
67
+ LeagueList = Data.define(:league_id, :tier, :name, :queue, :entries, :raw) do
68
+ def self.from_api(hash)
69
+ new(
70
+ league_id: hash['leagueId'],
71
+ tier: hash['tier'],
72
+ name: hash['name'],
73
+ queue: hash['queue'],
74
+ entries: hash['entries'].to_a.freeze,
75
+ raw: hash.freeze
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Models
5
+ Match = Data.define(:metadata, :info, :raw) do
6
+ def match_id
7
+ metadata&.match_id
8
+ end
9
+
10
+ def self.from_api(hash)
11
+ new(
12
+ metadata: MatchMetadata.from_api(hash['metadata']),
13
+ info: MatchInfo.from_api(hash['info']),
14
+ raw: hash.freeze
15
+ )
16
+ end
17
+ end
18
+
19
+ MatchMetadata = Data.define(:match_id, :participants, :raw) do
20
+ def self.from_api(hash)
21
+ return nil if hash.nil?
22
+
23
+ new(
24
+ match_id: hash['matchId'],
25
+ participants: hash['participants'],
26
+ raw: hash.freeze
27
+ )
28
+ end
29
+ end
30
+
31
+ MatchInfo = Data.define(:game_creation, :game_duration, :game_end_timestamp,
32
+ :game_id, :game_mode, :game_start_timestamp, :game_type,
33
+ :game_version, :map_id, :participants, :queue_id, :raw) do
34
+ def self.from_api(hash)
35
+ return nil if hash.nil?
36
+
37
+ new(
38
+ game_creation: hash['gameCreation'],
39
+ game_duration: hash['gameDuration'],
40
+ game_end_timestamp: hash['gameEndTimestamp'],
41
+ game_id: hash['gameId'],
42
+ game_mode: hash['gameMode'],
43
+ game_start_timestamp: hash['gameStartTimestamp'],
44
+ game_type: hash['gameType'],
45
+ game_version: hash['gameVersion'],
46
+ map_id: hash['mapId'],
47
+ participants: hash['participants']&.map { |p| MatchParticipant.from_api(p) },
48
+ queue_id: hash['queueId'],
49
+ raw: hash.freeze
50
+ )
51
+ end
52
+ end
53
+
54
+ MatchParticipant = Data.define(:champion_name, :kills, :deaths, :assists,
55
+ :puuid, :summoner_name, :team_id, :win, :raw) do
56
+ def self.from_api(hash)
57
+ new(
58
+ champion_name: hash['championName'],
59
+ kills: hash['kills'],
60
+ deaths: hash['deaths'],
61
+ assists: hash['assists'],
62
+ puuid: hash['puuid'],
63
+ summoner_name: hash['summonerName'],
64
+ team_id: hash['teamId'],
65
+ win: hash['win'],
66
+ raw: hash.freeze
67
+ )
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Models
5
+ Summoner = Data.define(
6
+ :id, :account_id, :puuid, :name, :profile_icon_id,
7
+ :revision_date, :summoner_level, :raw
8
+ ) do
9
+ def self.from_api(hash)
10
+ new(
11
+ id: hash['id'],
12
+ account_id: hash['accountId'],
13
+ puuid: hash['puuid'],
14
+ name: hash['name'],
15
+ profile_icon_id: hash['profileIconId'],
16
+ revision_date: hash['revisionDate'],
17
+ summoner_level: hash['summonerLevel'],
18
+ raw: hash.freeze
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module RateLimiting
5
+ # Pre-emptive limiter driven entirely by rate limit headers.
6
+ # Buckets are keyed per (api key, region, endpoint) so multiple keys
7
+ # and regions never share state.
8
+ class AdaptiveLimiter
9
+ def initialize(clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) },
10
+ sleeper: Kernel.method(:sleep))
11
+ @buckets = {}
12
+ @mutex = Mutex.new
13
+ @clock = clock
14
+ @sleeper = sleeper
15
+ end
16
+
17
+ def acquire!(key:, region:, bucket:)
18
+ app_bucket(key, region).wait_until_allowed!
19
+ method_bucket(key, region, bucket).wait_until_allowed!
20
+ end
21
+
22
+ def observe!(response, key:, region:, bucket:)
23
+ headers = response.headers
24
+ status = response.status
25
+
26
+ app = app_bucket(key, region)
27
+ method = method_bucket(key, region, bucket)
28
+
29
+ app.apply!(limits: HeaderParser.app_limits(headers), counts: HeaderParser.app_counts(headers))
30
+ method.apply!(limits: HeaderParser.method_limits(headers), counts: HeaderParser.method_counts(headers))
31
+
32
+ return unless status == 429
33
+
34
+ retry_after = HeaderParser.retry_after(headers)
35
+ case HeaderParser.limit_type(headers)
36
+ when :application
37
+ app.limit_block!(retry_after)
38
+ when :method, :service
39
+ method.limit_block!(retry_after)
40
+ else
41
+ method.limit_block!(retry_after)
42
+ end
43
+ end
44
+
45
+ def bucket_blocked?(key:, region:, bucket: nil, scope: :app)
46
+ bucket_for_scope(key, region, bucket, scope).blocked?
47
+ end
48
+
49
+ private
50
+
51
+ def app_bucket(key, region)
52
+ bucket_for('app', key, region, nil)
53
+ end
54
+
55
+ def method_bucket(key, region, bucket)
56
+ bucket_for('method', key, region, bucket)
57
+ end
58
+
59
+ def bucket_for_scope(key, region, bucket, scope)
60
+ scope == :app ? app_bucket(key, region) : method_bucket(key, region, bucket)
61
+ end
62
+
63
+ def bucket_for(scope, key, region, endpoint)
64
+ @mutex.synchronize do
65
+ @buckets[[scope, key, Routing.normalize(region), endpoint]] ||=
66
+ Bucket.new(clock: @clock, sleeper: @sleeper)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module RateLimiting
5
+ # Tracks windows learned from X-App-Rate-Limit / X-Method-Rate-Limit headers
6
+ # and blocks callers before limits are hit.
7
+ class Bucket
8
+ DEFAULT_BACKOFF = 1
9
+ MAX_BACKOFF = 32
10
+ JITTER = 0.5
11
+
12
+ def initialize(clock:, sleeper: Kernel.method(:sleep))
13
+ @clock = clock
14
+ @sleeper = sleeper
15
+ @mutex = Mutex.new
16
+ @windows = []
17
+ @blocked_until = nil
18
+ @limited_streak = 0
19
+ end
20
+
21
+ def apply!(limits:, counts:)
22
+ @mutex.synchronize do
23
+ now = @clock.call
24
+ if limits.any?
25
+ counts_by_window = counts.to_h { |count, window| [window, count] }
26
+ @windows = limits.map do |limit, window|
27
+ previous = @windows.find { |w| w[:window] == window && w[:reset_at] > now }
28
+ {
29
+ limit: limit,
30
+ window: window,
31
+ reset_at: now + window,
32
+ count: counts_by_window.fetch(window) { previous&.fetch(:count) || 0 }
33
+ }
34
+ end
35
+ end
36
+ @limited_streak = 0
37
+ end
38
+ end
39
+
40
+ # Blocks the bucket. When retry_after is nil (missing Retry-After header),
41
+ # falls back to exponential backoff with jitter based on the streak of
42
+ # consecutive 429s.
43
+ def limit_block!(retry_after)
44
+ @mutex.synchronize do
45
+ now = @clock.call
46
+ @limited_streak += 1
47
+ delay = retry_after || [@limited_streak * DEFAULT_BACKOFF, MAX_BACKOFF].min
48
+ delay += rand * JITTER
49
+ until_time = now + delay
50
+ @blocked_until = until_time if @blocked_until.nil? || @blocked_until < until_time
51
+ end
52
+ end
53
+
54
+ def wait_until_allowed!
55
+ loop do
56
+ delay = nil
57
+ @mutex.synchronize do
58
+ now = @clock.call
59
+ if @blocked_until && @blocked_until > now
60
+ delay = @blocked_until - now
61
+ else
62
+ @windows.reject! { |w| w[:reset_at] <= now }
63
+ violating = @windows.find { |w| w[:count] >= w[:limit] }
64
+ if violating
65
+ delay = violating[:reset_at] - now
66
+ else
67
+ @windows.each { |w| w[:count] += 1 }
68
+ return
69
+ end
70
+ end
71
+ end
72
+ @sleeper.call(delay + 0.001)
73
+ end
74
+ end
75
+
76
+ def blocked?
77
+ @mutex.synchronize do
78
+ @blocked_until && @blocked_until > @clock.call
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module RateLimiting
5
+ module HeaderParser
6
+ module_function
7
+
8
+ def parse_pairs(value)
9
+ return [] if value.nil?
10
+
11
+ value.split(',').filter_map do |part|
12
+ limit, window = part.strip.split(':', 2)
13
+ next if limit.nil? || window.nil?
14
+
15
+ [Integer(limit), Integer(window)]
16
+ rescue ArgumentError
17
+ next
18
+ end
19
+ end
20
+
21
+ def app_limits(headers)
22
+ parse_pairs(headers['X-App-Rate-Limit'])
23
+ end
24
+
25
+ def app_counts(headers)
26
+ parse_pairs(headers['X-App-Rate-Limit-Count'])
27
+ end
28
+
29
+ def method_limits(headers)
30
+ parse_pairs(headers['X-Method-Rate-Limit'])
31
+ end
32
+
33
+ def method_counts(headers)
34
+ parse_pairs(headers['X-Method-Rate-Limit-Count'])
35
+ end
36
+
37
+ def retry_after(headers)
38
+ value = headers['Retry-After']
39
+ Integer(value) if value&.match?(/\A\d+\z/)
40
+ end
41
+
42
+ LIMIT_TYPES = {
43
+ 'application' => :application,
44
+ 'method' => :method,
45
+ 'service' => :service
46
+ }.freeze
47
+
48
+ def limit_type(headers)
49
+ LIMIT_TYPES[headers['X-Rate-Limit-Type']]
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module RateLimiting
5
+ class NullLimiter
6
+ def acquire!(key:, region:, bucket:); end
7
+
8
+ def observe!(response, key:, region:, bucket:); end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module Rito
6
+ module RateLimiting
7
+ # Distributed rate limiter backed by Redis for multi-process / multi-host
8
+ # applications. Soft dependency: requires the `redis` gem at instantiation,
9
+ # not at load time.
10
+ #
11
+ # Fixed windows with atomic check-and-increment (Lua), counters synced from
12
+ # authoritative X-*-Count headers, and per-scope block keys for Retry-After.
13
+ class RedisLimiter
14
+ class DependencyMissing < Error; end
15
+
16
+ LIMITS_TTL = 600
17
+
18
+ # KEYS[1] = app block key, KEYS[2] = method block key, KEYS[3..] = counters
19
+ # ARGV = limit, window pairs matching KEYS[3..]
20
+ ALLOW_SCRIPT = <<~LUA
21
+ for i = 1, 2 do
22
+ if redis.call('EXISTS', KEYS[i]) == 1 then
23
+ return 0
24
+ end
25
+ end
26
+ for i = 3, #KEYS do
27
+ local idx = i - 3
28
+ local limit = tonumber(ARGV[idx * 2 + 1])
29
+ local window = tonumber(ARGV[idx * 2 + 2])
30
+ if limit then
31
+ local count = redis.call('INCR', KEYS[i])
32
+ if count == 1 then
33
+ redis.call('EXPIRE', KEYS[i], window)
34
+ end
35
+ if count > limit then
36
+ redis.call('DECR', KEYS[i])
37
+ return 0
38
+ end
39
+ end
40
+ end
41
+ return 1
42
+ LUA
43
+
44
+ def initialize(redis:, sleeper: Kernel.method(:sleep))
45
+ raise DependencyMissing, 'Add the `redis` gem to your Gemfile to use RedisLimiter' unless defined?(::Redis)
46
+
47
+ @redis = redis
48
+ @sleeper = sleeper
49
+ end
50
+
51
+ def acquire!(key:, region:, bucket:)
52
+ loop do
53
+ return if allowed?(key, region, bucket)
54
+
55
+ @sleeper.call(1)
56
+ end
57
+ end
58
+
59
+ def observe!(response, key:, region:, bucket:)
60
+ headers = response.headers
61
+ status = response.status
62
+ prefix = prefix_for(key, region)
63
+
64
+ if status == 429
65
+ block!(prefix, bucket, headers)
66
+ else
67
+ sync_limits!(prefix, bucket, headers)
68
+ sync_counts!(prefix, bucket, headers)
69
+ end
70
+ end
71
+
72
+ def blocked?(key:, region:, bucket: nil, scope: :app)
73
+ prefix = prefix_for(key, region)
74
+ block_key = scope == :app ? "#{prefix}:block" : "#{prefix}:m:#{bucket}:block"
75
+ @redis.exists?(block_key)
76
+ end
77
+
78
+ private
79
+
80
+ def allowed?(key, region, bucket)
81
+ prefix = prefix_for(key, region)
82
+ method_prefix = "#{prefix}:m:#{bucket}"
83
+
84
+ app_limits = parse_limits(@redis.hget("#{prefix}:limits", 'app'))
85
+ method_limits = parse_limits(@redis.hget("#{prefix}:limits", bucket))
86
+
87
+ keys = ["#{prefix}:block", "#{method_prefix}:block"]
88
+ argv = []
89
+ app_limits.each do |limit, window|
90
+ keys << "#{prefix}:app:#{window}"
91
+ argv << limit.to_s << window.to_s
92
+ end
93
+ method_limits.each do |limit, window|
94
+ keys << "#{method_prefix}:#{window}"
95
+ argv << limit.to_s << window.to_s
96
+ end
97
+
98
+ @redis.eval(ALLOW_SCRIPT, keys: keys, argv: argv) == 1
99
+ end
100
+
101
+ def block!(prefix, bucket, headers)
102
+ retry_after = HeaderParser.retry_after(headers) || 2
103
+ case HeaderParser.limit_type(headers)
104
+ when :application
105
+ @redis.set("#{prefix}:block", '1', ex: retry_after)
106
+ when :method, :service
107
+ @redis.set("#{prefix}:m:#{bucket}:block", '1', ex: retry_after)
108
+ else
109
+ @redis.set("#{prefix}:m:#{bucket}:block", '1', ex: retry_after)
110
+ end
111
+ end
112
+
113
+ def sync_limits!(prefix, bucket, headers)
114
+ app_limits = HeaderParser.app_limits(headers)
115
+ method_limits = HeaderParser.method_limits(headers)
116
+ return if app_limits.empty? && method_limits.empty?
117
+
118
+ mapping = {}
119
+ mapping['app'] = serialize(app_limits) unless app_limits.empty?
120
+ mapping[bucket] = serialize(method_limits) unless method_limits.empty?
121
+ @redis.hset("#{prefix}:limits", mapping)
122
+ @redis.expire("#{prefix}:limits", LIMITS_TTL)
123
+ end
124
+
125
+ def sync_counts!(prefix, bucket, headers)
126
+ sync_scope_counts!(prefix, HeaderParser.app_counts(headers), "#{prefix}:app")
127
+ sync_scope_counts!(prefix, HeaderParser.method_counts(headers), "#{prefix}:m:#{bucket}")
128
+ end
129
+
130
+ def sync_scope_counts!(_prefix, counts, counter_prefix)
131
+ counts.each do |count, window|
132
+ @redis.set("#{counter_prefix}:#{window}", count.to_s, ex: window)
133
+ end
134
+ end
135
+
136
+ def serialize(pairs)
137
+ pairs.map { |limit, window| "#{limit}:#{window}" }.join(',')
138
+ end
139
+
140
+ def parse_limits(value)
141
+ HeaderParser.parse_pairs(value)
142
+ end
143
+
144
+ def prefix_for(key, region)
145
+ "rito:#{digest(key)}:#{Routing.normalize(region)}"
146
+ end
147
+
148
+ def digest(key)
149
+ Digest::SHA256.hexdigest(key.to_s)[0, 16]
150
+ end
151
+ end
152
+ end
153
+ end