Class: Philiprehberger::HttpClient::Cache

Inherits:
Object
  • Object
show all
Defined in:
lib/philiprehberger/http_client/cache.rb

Overview

Simple in-memory cache for GET responses. Respects Cache-Control headers (max-age, no-cache, no-store) and supports conditional requests via ETag and Last-Modified headers.

The store is bounded: once it holds max_entries entries, the least recently used entry is evicted on the next insert. Both #lookup hits and #refresh mark an entry as recently used.

Examples:

cache = Cache.new(max_entries: 500)
cache.store(uri, response)
cached = cache.lookup(uri)

Defined Under Namespace

Classes: CacheEntry

Constant Summary collapse

DEFAULT_MAX_ENTRIES =

Default upper bound on the number of cached entries.

1000

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(max_entries: DEFAULT_MAX_ENTRIES) ⇒ Cache

Returns a new instance of Cache.

Parameters:

  • max_entries (Integer) (defaults to: DEFAULT_MAX_ENTRIES)

    maximum number of entries retained before least-recently-used eviction kicks in (default: 1000)



28
29
30
31
32
33
34
35
36
# File 'lib/philiprehberger/http_client/cache.rb', line 28

def initialize(max_entries: DEFAULT_MAX_ENTRIES)
  unless max_entries.is_a?(Integer) && max_entries.positive?
    raise ArgumentError, "max_entries must be a positive Integer, got #{max_entries.inspect}"
  end

  @monitor = Monitor.new
  @store = {}
  @max_entries = max_entries
end

Instance Attribute Details

#max_entriesInteger (readonly)

Maximum number of entries retained before LRU eviction.

Returns:

  • (Integer)


41
42
43
# File 'lib/philiprehberger/http_client/cache.rb', line 41

def max_entries
  @max_entries
end

Instance Method Details

#clear!void

This method returns an undefined value.

Remove all entries from the cache.



135
136
137
# File 'lib/philiprehberger/http_client/cache.rb', line 135

def clear!
  @monitor.synchronize { @store.clear }
end

#entry_for(uri) ⇒ CacheEntry?

Return the cache entry for conditional request headers. Returns nil if no entry exists (even if expired).

Parameters:

  • uri (URI)

    the request URI

Returns:



71
72
73
74
# File 'lib/philiprehberger/http_client/cache.rb', line 71

def entry_for(uri)
  key = cache_key(uri)
  @monitor.synchronize { @store[key] }
end

#lookup(uri) ⇒ Response?

Look up a cached response for the given URI. Returns nil if not cached or expired. A hit marks the entry as the most recently used.

Parameters:

  • uri (URI)

    the request URI

Returns:

  • (Response, nil)

    the cached response or nil



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/philiprehberger/http_client/cache.rb', line 49

def lookup(uri)
  key = cache_key(uri)
  @monitor.synchronize do
    entry = @store[key]
    return nil unless entry

    if expired?(entry)
      # Keep entries with etag/last_modified for conditional requests
      @store.delete(key) unless entry.etag || entry.last_modified
      nil
    else
      touch(key, entry)
      entry.response
    end
  end
end

#refresh(uri, response) ⇒ Response?

Refresh a stored entry after a 304 Not Modified revalidation.

Resets stored_at and applies any fresh max-age, ETag, or Last-Modified carried by the 304 response, then returns the stored response (with its body) so the caller can serve cached content. Marks the entry as most recently used. Returns nil when no entry exists for the URI.

Parameters:

  • uri (URI)

    the request URI

  • response (Response)

    the 304 Not Modified response

Returns:

  • (Response, nil)

    the stored response, or nil if not cached



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/philiprehberger/http_client/cache.rb', line 115

def refresh(uri, response)
  key = cache_key(uri)
  cc = parse_cache_control(response)

  @monitor.synchronize do
    entry = @store[key]
    return nil unless entry

    entry.stored_at = now
    entry.max_age = cc[:max_age] if cc[:max_age]
    entry.etag = response.headers['etag'] if response.headers['etag']
    entry.last_modified = response.headers['last-modified'] if response.headers['last-modified']
    touch(key, entry)
    entry.response
  end
end

#sizeInteger

Return the number of cached entries.

Returns:

  • (Integer)


142
143
144
# File 'lib/philiprehberger/http_client/cache.rb', line 142

def size
  @monitor.synchronize { @store.size }
end

#store(uri, response) ⇒ void

This method returns an undefined value.

Store a response in the cache. Respects Cache-Control: no-store (does not cache). Evicts the least recently used entry when the store exceeds max_entries.

Parameters:

  • uri (URI)

    the request URI

  • response (Response)

    the response to cache



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

def store(uri, response)
  return if no_store?(response)

  key = cache_key(uri)
  cc = parse_cache_control(response)

  entry = CacheEntry.new(
    response: response,
    stored_at: now,
    max_age: cc[:max_age],
    etag: response.headers['etag'],
    last_modified: response.headers['last-modified']
  )

  @monitor.synchronize do
    @store.delete(key)
    @store[key] = entry
    evict_lru
  end
end