Class: EndPointBlank::AccessTokens
- Inherits:
-
Object
- Object
- EndPointBlank::AccessTokens
- Includes:
- Singleton
- Defined in:
- lib/end_point_blank/access_tokens.rb
Overview
Thread-safe singleton holding this process's access tokens, one per application environment.
A token is cached under the canonical base URL intake resolved the request to -- not under the URL the caller supplied. A caller asks for the URL it is about to call; intake answers with the base URL of the environment that URL belongs to, and subsequent calls anywhere under that base URL reuse the entry.
Lookup is a plain exact-or-path-prefix comparison, with the longest match winning. The SDK deliberately does not normalize: intake owns that rule, and a miss costs one extra request rather than a wrong answer.
A lookup has to scan the keys, and the fast path deliberately does not take the mutex, so every write replaces the entries Hash instead of mutating it. A reader then takes one atomic read of @entries and iterates something nobody can change underneath it. Mutating in place would risk "can't add a new key into hash during iteration" as soon as one thread minted a token for a second target while another was doing a lookup.
Constant Summary collapse
- REFRESH_WINDOW =
Replace a token this far ahead of its expiry. An expired token can never be revived, only replaced, so going early is what keeps an in-flight request from carrying one that dies before it lands.
120- PRESENCE_WINDOW =
exists? is used to decide whether a caller can proceed without a round trip, so it answers no while there is barely any life left.
30- DEFAULT_LIFETIME =
How long to hold a token whose expiry the intake sent unreadably.
3600
Class Method Summary collapse
Instance Method Summary collapse
-
#clear ⇒ nil
Discard every held token.
-
#exists?(base_url) ⇒ Boolean
Check whether a token covering base_url is held and is not about to expire.
-
#initialize ⇒ AccessTokens
constructor
A new instance of AccessTokens.
-
#invalidate(stale_token) ⇒ nil
Discard the held token, but only if it is still the one the caller had.
-
#token(base_url) ⇒ String?
Retrieve a token covering base_url, generating one if no usable entry covers it.
Constructor Details
#initialize ⇒ AccessTokens
Returns a new instance of AccessTokens.
41 42 43 44 |
# File 'lib/end_point_blank/access_tokens.rb', line 41 def initialize @mutex = Mutex.new @entries = {} end |
Class Method Details
.token(base_url) ⇒ Object
46 47 48 |
# File 'lib/end_point_blank/access_tokens.rb', line 46 def self.token(base_url) instance.token(base_url) end |
Instance Method Details
#clear ⇒ nil
Discard every held token
111 112 113 |
# File 'lib/end_point_blank/access_tokens.rb', line 111 def clear @mutex.synchronize { @entries = {}.freeze } end |
#exists?(base_url) ⇒ Boolean
Check whether a token covering base_url is held and is not about to expire
140 141 142 143 |
# File 'lib/end_point_blank/access_tokens.rb', line 140 def exists?(base_url) entry = match(base_url) !entry.nil? && entry[:expired_at] > Time.now + PRESENCE_WINDOW end |
#invalidate(stale_token) ⇒ nil
Discard the held token, but only if it is still the one the caller had
Every request in flight when a token is rejected reports the same stale value. Only the first of them should cause an exchange -- the rest are holding a token that has already been replaced, and clearing on their behalf would discard a good token and stampede intake.
The lookup is by token value because a rejected caller has a token, not a URL.
128 129 130 131 132 133 134 |
# File 'lib/end_point_blank/access_tokens.rb', line 128 def invalidate(stale_token) return if stale_token.nil? @mutex.synchronize do @entries = @entries.reject { |_, entry| entry[:token] == stale_token }.freeze end end |
#token(base_url) ⇒ String?
Retrieve a token covering base_url, generating one if no usable entry covers it.
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/end_point_blank/access_tokens.rb', line 58 def token(base_url) entry = match(base_url) return entry[:token] if usable?(entry) @mutex.synchronize do # Another caller may have filled it while this one waited. entry = match(base_url) return entry[:token] if usable?(entry) payload = Commands::GenerateAccessToken.token(base_url) # The key is what intake resolved to, and only that. There is no # fallback to the requested URL: that would key on the resource the # caller happened to ask about, so a service walking /orders/1, # /orders/2, /orders/3 would mint and store a token per resource, and # nothing here evicts. Without a base URL the right application # cannot be found, so no token is handed back either. key = payload && payload[:base_url] if payload && payload[:token] && key # The match that led here may have resolved under a different key # than the one intake just returned -- an environment's base URL # can change to a shorter path in the portal. Drop that stale key # when it differs from the fresh one, or it goes on shadowing it: # being the longer of the two, it keeps winning "longest match # wins", keeps failing usable?, and keeps forcing a mint on every # call until the process restarts. The failure branch below already # does the equivalent for a match that turned out unusable; this is # the same cleanup for a match that turned out to have moved. stale = match_key(base_url, @entries) new_entries = @entries.merge( key => { token: payload[:token], expired_at: parse_expiry(payload[:expired_at]) }.freeze ) new_entries = new_entries.reject { |k, _| k == stale } if stale && stale != key @entries = new_entries.freeze payload[:token] else # A failed refresh must not leave an expiring token behind claiming # to be usable -- callers would keep presenting it right up to the # 401. Only the entry that covers this URL goes: the longest match # is the one that was just found unusable, so a shorter, still-good # entry survives. stale = match_key(base_url, @entries) @entries = @entries.reject { |k, _| k == stale }.freeze if stale EndPointBlank.logger.error "Failed to generate access token for #{base_url}: #{failure_reason(payload)}" nil end end end |