Skip to content

Commit 85a6320

Browse files
author
Jay OConnor
committed
adding a mutex for certificate fetches
and DRYing up the class chooser based on the configuration
1 parent fbfdd24 commit 85a6320

File tree

4 files changed

+62
-15
lines changed

4 files changed

+62
-15
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ FirebaseIdToken::Certificates.find('ec8f292sd30224afac5c55540df66d1f999d')
112112

113113
#### Downloading in Rails
114114
If you pass in the `cache_store` configuration option (see [configuration](#configuration)), the certificates will be
115-
requested at runtime when needed using `ActiveSupport::Cache.fetch()` and you can ignore this section.
115+
requested at runtime when needed and you can ignore this section.
116116

117117
If you are using Rails, it's clever to download certificates in a cron task, you can use [whenever](https://github.com/javan/whenever).
118118

lib/firebase_id_token/certificates.rb

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class Certificates
4747
# @return [nil, Hash]
4848
# @see Certificates.request!
4949
def self.request
50-
new.request
50+
new_child.request
5151
end
5252

5353
# Triggers a HTTPS request to Google's x509 certificates API. If it
@@ -61,7 +61,7 @@ def self.request
6161
# {Exceptions::CertificatesTtlError}. You are mostly like to never face it.
6262
# @return [Hash]
6363
def self.request!
64-
new.request!
64+
new_child.request!
6565
end
6666

6767
# @deprecated Use only `request!` in favor of Ruby conventions.
@@ -71,7 +71,7 @@ def self.request_anyway
7171
warn 'WARNING: FirebaseIdToken::Certificates.request_anyway is '\
7272
'deprecated. Use FirebaseIdToken::Certificates.request! instead.'
7373

74-
new.request!
74+
new_child.request!
7575
end
7676

7777
# Returns `true` if there's certificates data in the cache, `false` otherwise.
@@ -80,7 +80,7 @@ def self.request_anyway
8080
# FirebaseIdToken::Certificates.request
8181
# FirebaseIdToken::Certificates.present? #=> true
8282
def self.present?
83-
! new.local_certs.empty?
83+
! new_child.local_certs.empty?
8484
end
8585

8686
# Returns an array of hashes, each hash is a single `{key => value}` pair
@@ -94,7 +94,7 @@ def self.present?
9494
# certs = FirebaseIdToken::Certificates.all
9595
# certs.first #=> {"1d6d01c7[...]" => #<OpenSSL::X509::Certificate[...]}
9696
def self.all
97-
new.local_certs.map { |kid, cert|
97+
new_child.local_certs.map { |kid, cert|
9898
{ kid => OpenSSL::X509::Certificate.new(cert) } }
9999
end
100100

@@ -110,7 +110,7 @@ def self.all
110110
# cert = FirebaseIdToken::Certificates.find "1d6d01f4w7d54c7[...]"
111111
# #=> <OpenSSL::X509::Certificate: subject=#<OpenSSL [...]
112112
def self.find(kid, raise_error: false)
113-
certs = new.local_certs
113+
certs = new_child.local_certs
114114
raise Exceptions::NoCertificatesError if certs.empty?
115115

116116
return OpenSSL::X509::Certificate.new certs[kid] if certs[kid]
@@ -145,8 +145,13 @@ def self.find!(kid)
145145
# @return [Fixnum]
146146
def self.ttl
147147
# call a child class based on the configuration
148-
return FirebaseIdToken::Certificates::Redis.ttl if FirebaseIdToken.configuration.redis
149-
FirebaseIdToken::Certificates::ActiveSupport.ttl
148+
klass = FirebaseIdToken.configuration.klass
149+
klass.ttl
150+
end
151+
152+
def self.new_child
153+
klass = FirebaseIdToken.configuration.klass
154+
klass.new
150155
end
151156

152157
# Sets two instance attributes: `:cach_store` and `:local_certs`. Those are

lib/firebase_id_token/certificates/active_support.rb

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,54 @@ def self.ttl
2222
private
2323

2424
def read_certificates
25-
entry = cache_store.fetch('certificates', race_condition_ttl: RACE_CONDITION_TIME) do
26-
request
25+
entry = cache_store.read('certificates')
26+
if entry.nil?
27+
lock do
28+
request!
29+
end
30+
entry = cache_store.read('certificates')
2731
end
2832
certs = {}
2933
certs = JSON.parse(JSON.parse(entry)["data"]) if entry
3034
certs
31-
rescue StandardError
35+
rescue StandardError => e
3236
return {}
3337
end
3438

39+
# we can't use ActiveSupport's fetch, because we need to set the expiration time of
40+
# the key based on a value we read from the request. This is a rudimentary mutex instead
41+
def lock
42+
acquire_lock
43+
yield
44+
ensure
45+
release_lock
46+
end
47+
48+
def acquire_lock
49+
maybe_sleep
50+
cache_store.write('certificate_lock', expires_in: 5.seconds)
51+
end
52+
53+
def maybe_sleep
54+
iteration = 0
55+
while cache_store.exist?('certificate_lock')
56+
iteration += 1
57+
sleep 1
58+
break if iteration > 5
59+
end
60+
end
61+
62+
def release_lock
63+
cache_store.delete('certificate_lock')
64+
end
65+
3566
def save_certificates
3667
expires_at = Time.now.to_i + ttl
3768
# set the expiration of the key to the certification expiration - RACE_CONDITION_TIME, so that the entry
3869
# will be expired before the certificate is
3970
cache_store.write 'certificates', { data: @request.body, expires_at: expires_at }.to_json,
4071
expires_in: (ttl - RACE_CONDITION_TIME)
41-
@local_certs = read_certificates
72+
@local_certs = @request.body
4273
end
4374

4475
def ttl

lib/firebase_id_token/configuration.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,22 @@ module FirebaseIdToken
44
LIB_PATH = File.expand_path('../../', __FILE__)
55

66
class Configuration
7-
attr_accessor :redis, :project_ids, :certificates, :cache_store
7+
attr_accessor :redis, :project_ids, :cache_store
88

99
def initialize
1010
@project_ids = []
11-
@certificates = FirebaseIdToken::Certificates
11+
end
12+
13+
def certificates
14+
klass
15+
end
16+
17+
def certificates=(value)
18+
@certificates = klass
19+
end
20+
21+
def klass
22+
redis ? FirebaseIdToken::Certificates::Redis : FirebaseIdToken::Certificates::ActiveSupport
1223
end
1324
end
1425
end

0 commit comments

Comments
 (0)