Class: EndPointBlank::AccessTokens

Inherits:
Object
  • Object
show all
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

Constructor Details

#initializeAccessTokens

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

#clearnil

Discard every held token

Returns:

  • (nil)


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

Parameters:

  • base_url (String)

    the URL to check coverage for

Returns:

  • (Boolean)


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.

Parameters:

  • stale_token (String, nil)

    the token the caller was rejected for; ignored when it is no longer the one held for its base URL.

Returns:

  • (nil)


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.

Parameters:

  • base_url (String)

    the URL you are about to call, with any query string and fragment removed. It is sent verbatim; intake normalizes it and matches it against registered base URLs by longest path prefix.

Returns:

  • (String, nil)

    The access token string, or nil if generation failed -- which includes a response that carried a token but no base_url.



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