22// Licensed under the MIT License.
33
44using System ;
5+ using System . Diagnostics . Contracts ;
56using System . Net . Http ;
67using System . Threading ;
78using System . Threading . Tasks ;
@@ -19,22 +20,17 @@ namespace Microsoft.IdentityModel.Protocols
1920 public class ConfigurationManager < T > : BaseConfigurationManager , IConfigurationManager < T > where T : class
2021 {
2122 private DateTimeOffset _syncAfter = DateTimeOffset . MinValue ;
22- private DateTimeOffset _lastRequestRefresh = DateTimeOffset . MinValue ;
23+ private DateTimeOffset _lastRefresh = DateTimeOffset . MinValue ;
2324 private bool _isFirstRefreshRequest = true ;
2425
26+ private readonly SemaphoreSlim _refreshLock ;
2527 private readonly IDocumentRetriever _docRetriever ;
2628 private readonly IConfigurationRetriever < T > _configRetriever ;
2729 private readonly IConfigurationValidator < T > _configValidator ;
2830 private T _currentConfiguration ;
31+ private Exception _fetchMetadataFailure ;
2932 private TimeSpan _bootstrapRefreshInterval = TimeSpan . FromSeconds ( 1 ) ;
3033
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-
3834 /// <summary>
3935 /// Instantiates a new <see cref="ConfigurationManager{T}"/> that manages automatic and controls refreshing on configuration data.
4036 /// </summary>
@@ -96,6 +92,7 @@ public ConfigurationManager(string metadataAddress, IConfigurationRetriever<T> c
9692 MetadataAddress = metadataAddress ;
9793 _docRetriever = docRetriever ;
9894 _configRetriever = configRetriever ;
95+ _refreshLock = new SemaphoreSlim ( 1 ) ;
9996 }
10097
10198 /// <summary>
@@ -148,149 +145,83 @@ public async Task<T> GetConfigurationAsync()
148145 public virtual async Task < T > GetConfigurationAsync ( CancellationToken cancel )
149146 {
150147 if ( _currentConfiguration != null && _syncAfter > DateTimeOffset . UtcNow )
148+ {
151149 return _currentConfiguration ;
150+ }
152151
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 )
152+ await _refreshLock . WaitAsync ( cancel ) . ConfigureAwait ( false ) ;
153+ try
162154 {
163- try
155+ if ( _syncAfter <= DateTimeOffset . UtcNow )
164156 {
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 )
157+ try
169158 {
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 ) ) ) ;
174- }
175-
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 ;
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+ }
185168
186- // In this case configuration was never obtained.
187- if ( _currentConfiguration == null )
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 ;
174+ }
175+ catch ( Exception ex )
188176 {
189- if ( _bootstrapRefreshInterval < RefreshInterval )
177+ _fetchMetadataFailure = ex ;
178+
179+ if ( _currentConfiguration == null ) // Throw an exception if there's no configuration to return.
190180 {
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- }
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+ }
196197 else
197198 {
198199 _syncAfter = DateTimeUtil . Add ( DateTime . UtcNow , AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval ) ;
199- }
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 ) ;
213200
214- LogHelper . LogExceptionMessage (
215- new InvalidOperationException (
216- LogHelper . FormatInvariant (
217- LogMessages . IDX20806 ,
218- LogHelper . MarkAsNonPII ( MetadataAddress ?? "null" ) ,
219- LogHelper . MarkAsNonPII ( ex ) ) ,
220- ex ) ) ;
201+ LogHelper . LogExceptionMessage (
202+ new InvalidOperationException (
203+ LogHelper . FormatInvariant ( LogMessages . IDX20806 , LogHelper . MarkAsNonPII ( MetadataAddress ?? "null" ) , LogHelper . MarkAsNonPII ( ex ) ) , ex ) ) ;
204+ }
221205 }
222206 }
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 ( ) ;
259207
260- if ( _configValidator == null )
261- {
262- _currentConfiguration = configuration ;
263- }
208+ // Stale metadata is better than no metadata
209+ if ( _currentConfiguration != null )
210+ return _currentConfiguration ;
264211 else
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 ) ) ;
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 ) ) ;
287220 }
288221 finally
289222 {
290- _syncAfter = DateTimeUtil . Add ( DateTime . UtcNow , AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval ) ;
291- Interlocked . Exchange ( ref _configurationRetrieverState , ConfigurationRetrieverIdle ) ;
223+ _refreshLock . Release ( ) ;
292224 }
293- #pragma warning restore CA1031 // Do not catch general exception types
294225 }
295226
296227 /// <summary>
@@ -301,8 +232,10 @@ private void UpdateCurrentConfiguration()
301232 /// <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>
302233 public override async Task < BaseConfiguration > GetBaseConfigurationAsync ( CancellationToken cancel )
303234 {
304- T obj = await GetConfigurationAsync ( cancel ) . ConfigureAwait ( false ) ;
305- return obj as BaseConfiguration ;
235+ var obj = await GetConfigurationAsync ( cancel ) . ConfigureAwait ( false ) ;
236+ if ( obj is BaseConfiguration )
237+ return obj as BaseConfiguration ;
238+ return null ;
306239 }
307240
308241 /// <summary>
@@ -313,15 +246,14 @@ public override async Task<BaseConfiguration> GetBaseConfigurationAsync(Cancella
313246 public override void RequestRefresh ( )
314247 {
315248 DateTimeOffset now = DateTimeOffset . UtcNow ;
316-
317- if ( now >= DateTimeUtil . Add ( _lastRequestRefresh . UtcDateTime , RefreshInterval ) || _isFirstRefreshRequest )
249+ if ( _isFirstRefreshRequest )
318250 {
251+ _syncAfter = now ;
319252 _isFirstRefreshRequest = false ;
320- _lastRequestRefresh = now ;
321- if ( Interlocked . CompareExchange ( ref _configurationRetrieverState , ConfigurationRetrieverIdle , ConfigurationRetrieverRunning ) != ConfigurationRetrieverRunning )
322- {
323- _ = Task . Run ( UpdateCurrentConfiguration , CancellationToken . None ) ;
324- }
253+ }
254+ else if ( now >= DateTimeUtil . Add ( _lastRefresh . UtcDateTime , RefreshInterval ) )
255+ {
256+ _syncAfter = now ;
325257 }
326258 }
327259
0 commit comments