Skip to content

Commit cdaa12e

Browse files
authored
Merge pull request #226 from prefab-cloud/cache-config-fetches
Cache config fetches
2 parents 4224c4e + ef42157 commit cdaa12e

File tree

7 files changed

+465
-14
lines changed

7 files changed

+465
-14
lines changed

lib/prefab-cloud-ruby.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,5 @@ module Prefab
5555
require 'prefab/murmer3'
5656
require 'prefab/javascript_stub'
5757
require 'prefab/semver'
58+
require 'prefab/fixed_size_hash'
59+
require 'prefab/caching_http_connection'
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# frozen_string_literal: true
2+
3+
module Prefab
4+
class CachingHttpConnection
5+
CACHE_SIZE = 2.freeze
6+
CacheEntry = Struct.new(:data, :etag, :expires_at)
7+
8+
class << self
9+
def cache
10+
@cache ||= FixedSizeHash.new(CACHE_SIZE)
11+
end
12+
13+
def reset_cache!
14+
@cache = FixedSizeHash.new(CACHE_SIZE)
15+
end
16+
end
17+
18+
def initialize(uri, api_key)
19+
@connection = HttpConnection.new(uri, api_key)
20+
end
21+
22+
def get(path)
23+
now = Time.now.to_i
24+
cache_key = "#{@connection.uri}#{path}"
25+
cached = self.class.cache[cache_key]
26+
27+
# Check if we have a valid cached response
28+
if cached&.data && cached.expires_at && now < cached.expires_at
29+
return Faraday::Response.new(
30+
status: 200,
31+
body: cached.data,
32+
response_headers: {
33+
'ETag' => cached.etag,
34+
'X-Cache' => 'HIT',
35+
'X-Cache-Expires-At' => cached.expires_at.to_s
36+
}
37+
)
38+
end
39+
40+
# Make request with conditional GET if we have an ETag
41+
response = if cached&.etag
42+
@connection.get(path, { 'If-None-Match' => cached.etag })
43+
else
44+
@connection.get(path)
45+
end
46+
47+
# Handle 304 Not Modified
48+
if response.status == 304 && cached&.data
49+
return Faraday::Response.new(
50+
status: 200,
51+
body: cached.data,
52+
response_headers: {
53+
'ETag' => cached.etag,
54+
'X-Cache' => 'HIT',
55+
'X-Cache-Expires-At' => cached.expires_at.to_s
56+
}
57+
)
58+
end
59+
60+
# Parse caching headers
61+
cache_control = response.headers['Cache-Control'].to_s
62+
etag = response.headers['ETag']
63+
64+
# Always add X-Cache header
65+
response.headers['X-Cache'] = 'MISS'
66+
67+
# Don't cache if no-store is present
68+
return response if cache_control.include?('no-store')
69+
70+
# Calculate expiration
71+
max_age = cache_control.match(/max-age=(\d+)/)&.captures&.first&.to_i
72+
expires_at = max_age ? now + max_age : nil
73+
74+
# Cache the response if we have caching headers
75+
if etag || expires_at
76+
self.class.cache[cache_key] = CacheEntry.new(
77+
response.body,
78+
etag,
79+
expires_at
80+
)
81+
end
82+
83+
response
84+
end
85+
86+
# Delegate other methods to the underlying connection
87+
def post(path, body)
88+
@connection.post(path, body)
89+
end
90+
91+
def uri
92+
@connection.uri
93+
end
94+
end
95+
end

lib/prefab/config_client.rb

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def _get(key, properties)
118118
end
119119

120120
def load_checkpoint
121-
success = load_source_checkpoint
121+
success = load_source_checkpoint(:start_at_id => @config_loader.highwater_mark)
122122
return if success
123123

124124
success = load_cache
@@ -127,11 +127,10 @@ def load_checkpoint
127127
LOG.warn 'No success loading checkpoints'
128128
end
129129

130-
def load_source_checkpoint
130+
def load_source_checkpoint(start_at_id: 0)
131131
@options.config_sources.each do |source|
132-
conn = Prefab::HttpConnection.new("#{source}/api/v1/configs/0", @base_client.api_key)
132+
conn = Prefab::CachingHttpConnection.new("#{source}/api/v1/configs/#{start_at_id}", @base_client.api_key)
133133
result = load_url(conn, :remote_api)
134-
135134
return true if result
136135
end
137136

@@ -146,18 +145,18 @@ def load_url(conn, source)
146145
cache_configs(configs)
147146
true
148147
else
149-
LOG.info "Checkpoint #{source} failed to load. Response #{resp.status}"
148+
LOG.info "Checkpoint #{source} [#{conn.uri}] failed to load. Response #{resp.status}"
150149
false
151150
end
152151
rescue Faraday::ConnectionFailed => e
153152
if !initialized?
154-
LOG.warn "Connection Fail loading #{source} checkpoint."
153+
LOG.warn "Connection Fail loading #{source} [#{conn.uri}] checkpoint."
155154
else
156-
LOG.debug "Connection Fail loading #{source} checkpoint."
155+
LOG.debug "Connection Fail loading #{source} [#{conn.uri}] checkpoint."
157156
end
158157
false
159158
rescue StandardError => e
160-
LOG.warn "Unexpected #{source} problem loading checkpoint #{e} #{conn}"
159+
LOG.warn "Unexpected #{source} [#{conn.uri}] problem loading checkpoint #{e} #{conn}"
161160
LOG.debug e.backtrace
162161
false
163162
end

lib/prefab/fixed_size_hash.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
module Prefab
3+
class FixedSizeHash < Hash
4+
def initialize(max_size)
5+
@max_size = max_size
6+
super()
7+
end
8+
9+
def []=(key, value)
10+
shift if size >= @max_size && !key?(key) # Only evict if adding a new key
11+
super
12+
end
13+
end
14+
end

lib/prefab/http_connection.rb

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ class HttpConnection
99
'X-PrefabCloud-Client-Version' => "prefab-cloud-ruby-#{Prefab::VERSION}"
1010
}.freeze
1111

12-
def initialize(api_root, api_key)
13-
@api_root = api_root
12+
def initialize(uri, api_key)
13+
@uri = uri
1414
@api_key = api_key
1515
end
1616

17-
def get(path)
18-
connection(PROTO_HEADERS).get(path)
17+
def uri
18+
@uri
19+
end
20+
21+
def get(path, headers = {})
22+
connection(PROTO_HEADERS.merge(headers)).get(path)
1923
end
2024

2125
def post(path, body)
@@ -24,13 +28,13 @@ def post(path, body)
2428

2529
def connection(headers = {})
2630
if Faraday::VERSION[0].to_i >= 2
27-
Faraday.new(@api_root) do |conn|
31+
Faraday.new(@uri) do |conn|
2832
conn.request :authorization, :basic, AUTH_USER, @api_key
2933

3034
conn.headers.merge!(headers)
3135
end
3236
else
33-
Faraday.new(@api_root) do |conn|
37+
Faraday.new(@uri) do |conn|
3438
conn.request :basic_auth, AUTH_USER, @api_key
3539

3640
conn.headers.merge!(headers)

0 commit comments

Comments
 (0)