@@ -11,15 +11,25 @@ import (
1111 "time"
1212)
1313
14+ type FetchStrategy int64
15+
1416const (
17+ Lazy FetchStrategy = iota // Fetch new metadata inline with requests (when not cached)
18+ // Background Fetch new metadata in the background regardless of requests being made. This option was designed
19+ // for eliminating in-line metadata calls and minimizing latency in production use. Warning: this option will
20+ // attempt to seed metadata on initialization and block.
21+ Background
22+
1523 DefaultCacheTtl = 5 * time .Minute
1624)
1725
1826// Options are configurable options for the MetadataProvider.
1927type Options struct {
20- httpClient * http.Client
21- cacheTtl time.Duration
22- clock clock.Clock
28+ httpClient * http.Client
29+ cacheTtl time.Duration
30+ clock clock.Clock
31+ fetchStrategy FetchStrategy
32+ backgroundCtx context.Context
2333}
2434
2535// WithHttpClient allows for a configurable http client.
@@ -42,11 +52,28 @@ func withClock(clock clock.Clock) Option {
4252 }
4353}
4454
55+ // WithFetchStrategy specifies a strategy for fetching new metadata.
56+ func WithFetchStrategy (fetchStrategy FetchStrategy ) Option {
57+ return func (mo * Options ) {
58+ mo .fetchStrategy = fetchStrategy
59+ }
60+ }
61+
62+ // WithBackgroundCtx specified the context to use in order to control the lifecycle of the background fetching
63+ // goroutine.
64+ func WithBackgroundCtx (ctx context.Context ) Option {
65+ return func (mo * Options ) {
66+ mo .backgroundCtx = ctx
67+ }
68+ }
69+
4570func defaultOptions () * Options {
4671 opts := & Options {}
4772 WithHttpClient (http .DefaultClient )(opts )
4873 withClock (clock .New ())(opts )
4974 WithCacheTtl (DefaultCacheTtl )(opts )
75+ WithFetchStrategy (Lazy )(opts )
76+ WithBackgroundCtx (context .Background ())(opts )
5077 return opts
5178}
5279
@@ -72,22 +99,34 @@ type MetadataProvider struct {
7299 metadataMutex sync.Mutex
73100 cacheTtl time.Duration
74101 cachedMetadata * cachedMetadata
102+ fetchStrategy FetchStrategy
75103}
76104
77105// NewMetadataProvider creates a new MetadataProvider for the specified Okta issuer.
78- func NewMetadataProvider (issuer string , options ... Option ) * MetadataProvider {
106+ func NewMetadataProvider (issuer string , options ... Option ) ( * MetadataProvider , error ) {
79107 opts := defaultOptions ()
80108 for _ , option := range options {
81109 option (opts )
82110 }
83111
84112 metadataUrl := fmt .Sprintf ("%s%s" , issuer , "/.well-known/openid-configuration" )
85- return & MetadataProvider {
86- metadataUrl : metadataUrl ,
87- httpClient : opts .httpClient ,
88- clock : opts .clock ,
89- cacheTtl : opts .cacheTtl ,
113+ mp := & MetadataProvider {
114+ metadataUrl : metadataUrl ,
115+ httpClient : opts .httpClient ,
116+ clock : opts .clock ,
117+ cacheTtl : opts .cacheTtl ,
118+ fetchStrategy : opts .fetchStrategy ,
90119 }
120+
121+ if opts .fetchStrategy == Background {
122+ _ , err := mp .backgroundFetchAndCache (opts .backgroundCtx )
123+ if err != nil {
124+ return nil , fmt .Errorf ("failed to seed metadata: %w" , err )
125+ }
126+ go mp .backgroundFetchLoop (opts .backgroundCtx )
127+ }
128+
129+ return mp , nil
91130}
92131
93132// GetMetadata gets metadata for the specified Okta issuer.
@@ -99,12 +138,21 @@ func (mp *MetadataProvider) GetMetadata(ctx context.Context) (metadata.Metadata,
99138 return cachedMetadataCopy .m , nil
100139 }
101140
141+ if mp .fetchStrategy == Lazy {
142+ return mp .lazyFetchAndCache (ctx )
143+ }
144+
145+ return metadata.Metadata {}, fmt .Errorf ("no metadata available" )
146+
147+ }
148+
149+ func (mp * MetadataProvider ) lazyFetchAndCache (ctx context.Context ) (metadata.Metadata , error ) {
102150 // Acquire a lock
103151 mp .metadataMutex .Lock ()
104152 defer mp .metadataMutex .Unlock ()
105153
106154 // Check for a race before continuing
107- cachedMetadataCopy = mp .cachedMetadata
155+ cachedMetadataCopy : = mp .cachedMetadata
108156 if cachedMetadataCopy != nil && mp .clock .Now ().Before (cachedMetadataCopy .expiration ) {
109157 return cachedMetadataCopy .m , nil
110158 }
@@ -120,6 +168,44 @@ func (mp *MetadataProvider) GetMetadata(ctx context.Context) (metadata.Metadata,
120168 return mp .cachedMetadata .m , nil
121169}
122170
171+ func (mp * MetadataProvider ) backgroundFetchAndCache (ctx context.Context ) (metadata.Metadata , error ) {
172+ // Acquire a lock
173+ mp .metadataMutex .Lock ()
174+ defer mp .metadataMutex .Unlock ()
175+
176+ expiration := mp .clock .Now ().Add (mp .cacheTtl )
177+
178+ newMetadata , err := mp .fetchMetadata (ctx )
179+ if err != nil {
180+ return metadata.Metadata {}, fmt .Errorf ("failed to fetch new fresh metadata: %w" , err )
181+ }
182+
183+ mp .cachedMetadata = newCachedMetadata (expiration , newMetadata )
184+ return mp .cachedMetadata .m , nil
185+ }
186+
187+ func (mp * MetadataProvider ) backgroundFetchLoop (ctx context.Context ) {
188+ // Seed cache initially
189+ _ , err := mp .backgroundFetchAndCache (ctx )
190+ if err != nil {
191+ // FIXME: log this
192+ }
193+
194+ ticker := time .NewTicker (mp .cacheTtl / 2 )
195+ defer ticker .Stop ()
196+ for {
197+ select {
198+ case <- ctx .Done ():
199+ return
200+ case <- ticker .C :
201+ _ , err := mp .backgroundFetchAndCache (ctx )
202+ if err != nil {
203+ // FIXME: log this
204+ }
205+ }
206+ }
207+ }
208+
123209func (mp * MetadataProvider ) fetchMetadata (ctx context.Context ) (metadata.Metadata , error ) {
124210 httpRequest , err := http .NewRequestWithContext (ctx , http .MethodGet , mp .metadataUrl , nil )
125211 if err != nil {
0 commit comments