Class: Anthropic::Credentials::TokenCache

Inherits:
Object
  • Object
show all
Defined in:
lib/anthropic/credentials/token_cache.rb,
sig/anthropic/credentials.rbs

Overview

Thread-safe cache wrapping an access token provider with two-tier proactive refresh and single-flight semantics.

Refresh policy on each #get_token call:

  • No cached token → call provider (blocking), cache, return.
  • Cached with expires_at=nil → return cached forever (never refresh).
  • More than advisory_refresh_seconds remaining → return cached.
  • Between mandatory_refresh_seconds and advisory_refresh_seconds remaining (advisory window) → try provider; on success swap cache; on failure log a warning and return the stale cached token. If another caller is already refreshing, the advisory caller just returns the cached token — no second refresh, no waiting.
  • Less than mandatory_refresh_seconds remaining or already expired (mandatory window) → call provider; on failure RAISE. Concurrent mandatory callers wait on a shared condition so exactly one provider call is in flight.

The lock is released before the provider call so a 30-second HTTP POST doesn't serialize unrelated callers through a single thread.

Constant Summary collapse

ADVISORY_REFRESH_BACKOFF_SECONDS =

Seconds to skip advisory refreshes after a failure to avoid hammering a failing token endpoint.

Returns:

5

Instance Method Summary collapse

Constructor Details

#initialize(provider, advisory_refresh_seconds: ADVISORY_REFRESH_SECONDS, mandatory_refresh_seconds: MANDATORY_REFRESH_SECONDS, time_source: -> { Time.now.to_f }) ⇒ TokenCache

Returns a new instance of TokenCache.

Parameters:

  • an access token provider that responds to call and returns an AccessToken

  • (defaults to: ADVISORY_REFRESH_SECONDS)

    seconds before expiry to attempt a non-blocking advisory refresh (default: 120)

  • (defaults to: MANDATORY_REFRESH_SECONDS)

    seconds before expiry when refresh becomes mandatory and blocking (default: 30)

  • (defaults to: -> { Time.now.to_f })

    callable returning current time as Float (for testing)



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/anthropic/credentials/token_cache.rb', line 37

def initialize(
  provider,
  advisory_refresh_seconds: ADVISORY_REFRESH_SECONDS,
  mandatory_refresh_seconds: MANDATORY_REFRESH_SECONDS,
  time_source: -> { Time.now.to_f }
)
  @provider = provider
  @advisory = advisory_refresh_seconds
  @mandatory = mandatory_refresh_seconds
  @time_source = time_source
  @lock = Monitor.new
  @cached = nil
  @refresh_in_progress = false
  @refresh_condition = @lock.new_cond
  @next_force = false
  @last_advisory_failure_time = 0.0
  @provider_accepts_force_refresh = detect_force_refresh_support
end

Instance Method Details

#get_tokenString

Returns a valid bearer token, refreshing if necessary.

This method is thread-safe. Multiple threads calling get_token simultaneously will coordinate to avoid redundant provider calls.

Returns:

  • the bearer token string

Raises:

  • if the provider fails during mandatory refresh



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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/anthropic/credentials/token_cache.rb', line 63

def get_token
  loop do # rubocop:disable Metrics/BlockLength
    advisory_fallback = nil
    should_refresh = false

    @lock.synchronize do
      if @cached
        if @cached.expires_at.nil?
          return @cached.token
        end

        remaining = @cached.expires_at - @time_source.call
        if remaining > @advisory
          return @cached.token
        end

        if remaining > @mandatory
          if @refresh_in_progress
            return @cached.token
          end
          if @time_source.call - @last_advisory_failure_time < ADVISORY_REFRESH_BACKOFF_SECONDS
            return @cached.token
          end
          advisory_fallback = @cached
        end
      end

      if @refresh_in_progress
        @refresh_condition.wait
        next
      end

      @refresh_in_progress = true
      should_refresh = true
    end

    next unless should_refresh

    begin
      force = @lock.synchronize { @next_force }
      fresh = invoke_provider(force: force)
      @lock.synchronize do
        @next_force = false
        @cached = fresh
      end
      return fresh.token
    rescue Anthropic::Errors::Error, IOError
      raise if advisory_fallback.nil?

      @lock.synchronize { @last_advisory_failure_time = @time_source.call }
      return advisory_fallback.token
    ensure
      @lock.synchronize do
        @refresh_in_progress = false
        @refresh_condition.broadcast
      end
    end
  end
end

#invalidatevoid

This method returns an undefined value.

Clears the cached token so the next #get_token re-invokes the provider.

Also sets a one-shot force_refresh flag so on-disk providers skip their freshness short-circuit instead of re-serving the revoked token.



129
130
131
132
133
134
# File 'lib/anthropic/credentials/token_cache.rb', line 129

def invalidate
  @lock.synchronize do
    @cached = nil
    @next_force = true
  end
end

#invoke_provider(force:) ⇒ AccessToken

Parameters:

Returns:



150
151
152
153
154
155
156
# File 'lib/anthropic/credentials/token_cache.rb', line 150

def invoke_provider(force:)
  if @provider_accepts_force_refresh
    @provider.call(force_refresh: force)
  else
    @provider.call
  end
end