22// Licensed under the MIT License.
33
44using System ;
5- using System . Diagnostics . Contracts ;
65using System . Net . Http ;
76using System . Threading ;
87using System . Threading . Tasks ;
@@ -20,17 +19,22 @@ namespace Microsoft.IdentityModel.Protocols
2019 public class ConfigurationManager < T > : BaseConfigurationManager , IConfigurationManager < T > where T : class
2120 {
2221 private DateTimeOffset _syncAfter = DateTimeOffset . MinValue ;
23- private DateTimeOffset _lastRefresh = DateTimeOffset . MinValue ;
22+ private DateTimeOffset _lastRequestRefresh = DateTimeOffset . MinValue ;
2423 private bool _isFirstRefreshRequest = true ;
2524
26- private readonly SemaphoreSlim _refreshLock ;
2725 private readonly IDocumentRetriever _docRetriever ;
2826 private readonly IConfigurationRetriever < T > _configRetriever ;
2927 private readonly IConfigurationValidator < T > _configValidator ;
3028 private T _currentConfiguration ;
31- private Exception _fetchMetadataFailure ;
3229 private TimeSpan _bootstrapRefreshInterval = TimeSpan . FromSeconds ( 1 ) ;
3330
31+ // task states are used to ensure the call to 'update config' (UpdateCurrentConfiguration) is a singleton. Uses Interlocked.CompareExchange.
32+ // metadata is not being obtained
33+ private const int ConfigurationRetrieverIdle = 0 ;
34+ // metadata is being retrieved
35+ private const int ConfigurationRetrieverRunning = 1 ;
36+ private int _configurationRetrieverState = ConfigurationRetrieverIdle ;
37+
3438 /// <summary>
3539 /// Instantiates a new <see cref="ConfigurationManager{T}"/> that manages automatic and controls refreshing on configuration data.
3640 /// </summary>
@@ -92,7 +96,6 @@ public ConfigurationManager(string metadataAddress, IConfigurationRetriever<T> c
9296 MetadataAddress = metadataAddress ;
9397 _docRetriever = docRetriever ;
9498 _configRetriever = configRetriever ;
95- _refreshLock = new SemaphoreSlim ( 1 ) ;
9699 }
97100
98101 /// <summary>
@@ -145,83 +148,149 @@ public async Task<T> GetConfigurationAsync()
145148 public virtual async Task < T > GetConfigurationAsync ( CancellationToken cancel )
146149 {
147150 if ( _currentConfiguration != null && _syncAfter > DateTimeOffset . UtcNow )
148- {
149151 return _currentConfiguration ;
150- }
151152
152- await _refreshLock . WaitAsync ( cancel ) . ConfigureAwait ( false ) ;
153- try
153+ Exception fetchMetadataFailure = null ;
154+
155+ // LOGIC
156+ // if configuration != null => configuration has been retrieved before
157+ // reach out to the metadata endpoint
158+ // else
159+ // if task is running, return the current configuration
160+ // else kick off task to update current configuration
161+ if ( _currentConfiguration == null )
154162 {
155- if ( _syncAfter <= DateTimeOffset . UtcNow )
163+ try
156164 {
157- try
165+ // Don't use the individual CT here, this is a shared operation that shouldn't be affected by an individual's cancellation.
166+ // The transport should have it's own timeouts, etc..
167+ var configuration = await _configRetriever . GetConfigurationAsync ( MetadataAddress , _docRetriever , CancellationToken . None ) . ConfigureAwait ( false ) ;
168+ if ( _configValidator != null )
158169 {
159- // Don't use the individual CT here, this is a shared operation that shouldn't be affected by an individual's cancellation.
160- // The transport should have it's own timeouts, etc..
161- var configuration = await _configRetriever . GetConfigurationAsync ( MetadataAddress , _docRetriever , CancellationToken . None ) . ConfigureAwait ( false ) ;
162- if ( _configValidator != null )
163- {
164- ConfigurationValidationResult result = _configValidator . Validate ( configuration ) ;
165- if ( ! result . Succeeded )
166- throw LogHelper . LogExceptionMessage ( new InvalidConfigurationException ( LogHelper . FormatInvariant ( LogMessages . IDX20810 , result . ErrorMessage ) ) ) ;
167- }
168-
169- _lastRefresh = DateTimeOffset . UtcNow ;
170- // Add a random amount between 0 and 5% of AutomaticRefreshInterval jitter to avoid spike traffic to IdentityProvider.
171- _syncAfter = DateTimeUtil . Add ( DateTime . UtcNow , AutomaticRefreshInterval +
172- TimeSpan . FromSeconds ( new Random ( ) . Next ( ( int ) AutomaticRefreshInterval . TotalSeconds / 20 ) ) ) ;
173- _currentConfiguration = configuration ;
170+ ConfigurationValidationResult result = _configValidator . Validate ( configuration ) ;
171+ // in this case we have never had a valid configuration, so we will throw an exception if the validation fails
172+ if ( ! result . Succeeded )
173+ throw LogHelper . LogExceptionMessage ( new InvalidConfigurationException ( LogHelper . FormatInvariant ( LogMessages . IDX20810 , result . ErrorMessage ) ) ) ;
174174 }
175- catch ( Exception ex )
176- {
177- _fetchMetadataFailure = ex ;
178175
179- if ( _currentConfiguration == null ) // Throw an exception if there's no configuration to return.
176+ // Add a random amount between 0 and 5% of AutomaticRefreshInterval jitter to avoid spike traffic to IdentityProvider.
177+ _syncAfter = DateTimeUtil . Add ( DateTime . UtcNow , AutomaticRefreshInterval +
178+ TimeSpan . FromSeconds ( new Random ( ) . Next ( ( int ) AutomaticRefreshInterval . TotalSeconds / 20 ) ) ) ;
179+
180+ _currentConfiguration = configuration ;
181+ }
182+ catch ( Exception ex )
183+ {
184+ fetchMetadataFailure = ex ;
185+
186+ // In this case configuration was never obtained.
187+ if ( _currentConfiguration == null )
188+ {
189+ if ( _bootstrapRefreshInterval < RefreshInterval )
180190 {
181- if ( _bootstrapRefreshInterval < RefreshInterval )
182- {
183- // Adopt exponential backoff for bootstrap refresh interval with a decorrelated jitter if it is not longer than the refresh interval.
184- TimeSpan _bootstrapRefreshIntervalWithJitter = TimeSpan . FromSeconds ( new Random ( ) . Next ( ( int ) _bootstrapRefreshInterval . TotalSeconds ) ) ;
185- _bootstrapRefreshInterval += _bootstrapRefreshInterval ;
186- _syncAfter = DateTimeUtil . Add ( DateTime . UtcNow , _bootstrapRefreshIntervalWithJitter ) ;
187- }
188- else
189- {
190- _syncAfter = DateTimeUtil . Add ( DateTime . UtcNow , AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval ) ;
191- }
192-
193- throw LogHelper . LogExceptionMessage (
194- new InvalidOperationException (
195- LogHelper . FormatInvariant ( LogMessages . IDX20803 , LogHelper . MarkAsNonPII ( MetadataAddress ?? "null" ) , LogHelper . MarkAsNonPII ( _syncAfter ) , LogHelper . MarkAsNonPII ( ex ) ) , ex ) ) ;
196- }
191+ // Adopt exponential backoff for bootstrap refresh interval with a decorrelated jitter if it is not longer than the refresh interval.
192+ TimeSpan _bootstrapRefreshIntervalWithJitter = TimeSpan . FromSeconds ( new Random ( ) . Next ( ( int ) _bootstrapRefreshInterval . TotalSeconds ) ) ;
193+ _bootstrapRefreshInterval += _bootstrapRefreshInterval ;
194+ _syncAfter = DateTimeUtil . Add ( DateTime . UtcNow , _bootstrapRefreshIntervalWithJitter ) ;
195+ }
197196 else
198197 {
199198 _syncAfter = DateTimeUtil . Add ( DateTime . UtcNow , AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval ) ;
200-
201- LogHelper . LogExceptionMessage (
202- new InvalidOperationException (
203- LogHelper . FormatInvariant ( LogMessages . IDX20806 , LogHelper . MarkAsNonPII ( MetadataAddress ?? "null" ) , LogHelper . MarkAsNonPII ( ex ) ) , ex ) ) ;
204199 }
200+
201+ throw LogHelper . LogExceptionMessage (
202+ new InvalidOperationException (
203+ LogHelper . FormatInvariant (
204+ LogMessages . IDX20803 ,
205+ LogHelper . MarkAsNonPII ( MetadataAddress ?? "null" ) ,
206+ LogHelper . MarkAsNonPII ( _syncAfter ) ,
207+ LogHelper . MarkAsNonPII ( ex ) ) ,
208+ ex ) ) ;
209+ }
210+ else
211+ {
212+ _syncAfter = DateTimeUtil . Add ( DateTime . UtcNow , AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval ) ;
213+
214+ LogHelper . LogExceptionMessage (
215+ new InvalidOperationException (
216+ LogHelper . FormatInvariant (
217+ LogMessages . IDX20806 ,
218+ LogHelper . MarkAsNonPII ( MetadataAddress ?? "null" ) ,
219+ LogHelper . MarkAsNonPII ( ex ) ) ,
220+ ex ) ) ;
205221 }
206222 }
223+ }
224+ else
225+ {
226+ if ( Interlocked . CompareExchange ( ref _configurationRetrieverState , ConfigurationRetrieverIdle , ConfigurationRetrieverRunning ) != ConfigurationRetrieverRunning )
227+ {
228+ _ = Task . Run ( UpdateCurrentConfiguration , CancellationToken . None ) ;
229+ }
230+ }
231+
232+ // If metadata exists return it.
233+ if ( _currentConfiguration != null )
234+ return _currentConfiguration ;
235+
236+ throw LogHelper . LogExceptionMessage (
237+ new InvalidOperationException (
238+ LogHelper . FormatInvariant (
239+ LogMessages . IDX20803 ,
240+ LogHelper . MarkAsNonPII ( MetadataAddress ?? "null" ) ,
241+ LogHelper . MarkAsNonPII ( _syncAfter ) ,
242+ LogHelper . MarkAsNonPII ( fetchMetadataFailure ) ) ,
243+ fetchMetadataFailure ) ) ;
244+ }
245+
246+ /// <summary>
247+ /// This should be called when the configuration needs to be updated either from RequestRefresh or AutomaticRefresh, first checking the state checking state using:
248+ /// if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverIdle, ConfigurationRetrieverRunning) != ConfigurationRetrieverRunning).
249+ /// </summary>
250+ private void UpdateCurrentConfiguration ( )
251+ {
252+ #pragma warning disable CA1031 // Do not catch general exception types
253+ try
254+ {
255+ T configuration = _configRetriever . GetConfigurationAsync (
256+ MetadataAddress ,
257+ _docRetriever ,
258+ CancellationToken . None ) . ConfigureAwait ( false ) . GetAwaiter ( ) . GetResult ( ) ;
207259
208- // Stale metadata is better than no metadata
209- if ( _currentConfiguration != null )
210- return _currentConfiguration ;
260+ if ( _configValidator == null )
261+ {
262+ _currentConfiguration = configuration ;
263+ }
211264 else
212- throw LogHelper . LogExceptionMessage (
213- new InvalidOperationException (
214- LogHelper . FormatInvariant (
215- LogMessages . IDX20803 ,
216- LogHelper . MarkAsNonPII ( MetadataAddress ?? "null" ) ,
217- LogHelper . MarkAsNonPII ( _syncAfter ) ,
218- LogHelper . MarkAsNonPII ( _fetchMetadataFailure ) ) ,
219- _fetchMetadataFailure ) ) ;
265+ {
266+ ConfigurationValidationResult result = _configValidator . Validate ( configuration ) ;
267+
268+ if ( ! result . Succeeded )
269+ LogHelper . LogExceptionMessage (
270+ new InvalidConfigurationException (
271+ LogHelper . FormatInvariant (
272+ LogMessages . IDX20810 ,
273+ result . ErrorMessage ) ) ) ;
274+ else
275+ _currentConfiguration = configuration ;
276+ }
277+ }
278+ catch ( Exception ex )
279+ {
280+ LogHelper . LogExceptionMessage (
281+ new InvalidOperationException (
282+ LogHelper . FormatInvariant (
283+ LogMessages . IDX20806 ,
284+ LogHelper . MarkAsNonPII ( MetadataAddress ?? "null" ) ,
285+ ex ) ,
286+ ex ) ) ;
220287 }
221288 finally
222289 {
223- _refreshLock . Release ( ) ;
290+ _syncAfter = DateTimeUtil . Add ( DateTime . UtcNow , AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval ) ;
291+ Interlocked . Exchange ( ref _configurationRetrieverState , ConfigurationRetrieverIdle ) ;
224292 }
293+ #pragma warning restore CA1031 // Do not catch general exception types
225294 }
226295
227296 /// <summary>
@@ -232,10 +301,8 @@ public virtual async Task<T> GetConfigurationAsync(CancellationToken cancel)
232301 /// <remarks>If the time since the last call is less than <see cref="BaseConfigurationManager.AutomaticRefreshInterval"/> then <see cref="IConfigurationRetriever{T}.GetConfigurationAsync"/> is not called and the current Configuration is returned.</remarks>
233302 public override async Task < BaseConfiguration > GetBaseConfigurationAsync ( CancellationToken cancel )
234303 {
235- var obj = await GetConfigurationAsync ( cancel ) . ConfigureAwait ( false ) ;
236- if ( obj is BaseConfiguration )
237- return obj as BaseConfiguration ;
238- return null ;
304+ T obj = await GetConfigurationAsync ( cancel ) . ConfigureAwait ( false ) ;
305+ return obj as BaseConfiguration ;
239306 }
240307
241308 /// <summary>
@@ -246,14 +313,15 @@ public override async Task<BaseConfiguration> GetBaseConfigurationAsync(Cancella
246313 public override void RequestRefresh ( )
247314 {
248315 DateTimeOffset now = DateTimeOffset . UtcNow ;
249- if ( _isFirstRefreshRequest )
316+
317+ if ( now >= DateTimeUtil . Add ( _lastRequestRefresh . UtcDateTime , RefreshInterval ) || _isFirstRefreshRequest )
250318 {
251- _syncAfter = now ;
252319 _isFirstRefreshRequest = false ;
253- }
254- else if ( now >= DateTimeUtil . Add ( _lastRefresh . UtcDateTime , RefreshInterval ) )
255- {
256- _syncAfter = now ;
320+ _lastRequestRefresh = now ;
321+ if ( Interlocked . CompareExchange ( ref _configurationRetrieverState , ConfigurationRetrieverIdle , ConfigurationRetrieverRunning ) != ConfigurationRetrieverRunning )
322+ {
323+ _ = Task . Run ( UpdateCurrentConfiguration , CancellationToken . None ) ;
324+ }
257325 }
258326 }
259327
0 commit comments