From c66d5240d18ba8458f7443d9fc3bca03b67918b4 Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Wed, 3 Jan 2018 14:33:16 -0800 Subject: [PATCH] Recreate cache of endpoints when data source updated (#503) Addresses #454 --- .../CompositeDispatcherDataSource.cs | 2 +- .../DefaultDispatcherDataSource.cs | 2 +- .../DispatcherDataSource.cs | 4 +- .../Tree/TreeMatcher.cs | 28 ++++-- .../Tree/TreeMatcherTest.cs | 96 +++++++++++++++++++ 5 files changed, 122 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.AspNetCore.Dispatcher/CompositeDispatcherDataSource.cs b/src/Microsoft.AspNetCore.Dispatcher/CompositeDispatcherDataSource.cs index 336b01558c..9925d8a188 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/CompositeDispatcherDataSource.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/CompositeDispatcherDataSource.cs @@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Dispatcher public override IChangeToken ChangeToken { get; } - protected override IReadOnlyList
GetAddesses() + protected override IReadOnlyList
GetAddresses() { var addresses = new List
(); for (var i = 0; i < _dataSources.Length; i++) diff --git a/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherDataSource.cs b/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherDataSource.cs index 5d36971ad3..2f279122d2 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherDataSource.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherDataSource.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Dispatcher public IList Endpoints => _endpoints; - protected override IReadOnlyList
GetAddesses() => _addresses; + protected override IReadOnlyList
GetAddresses() => _addresses; protected override IReadOnlyList GetEndpoints() => _endpoints; } diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherDataSource.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherDataSource.cs index 2767e408dc..b04149bf09 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherDataSource.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherDataSource.cs @@ -10,11 +10,11 @@ namespace Microsoft.AspNetCore.Dispatcher { public abstract IChangeToken ChangeToken { get; } - protected abstract IReadOnlyList
GetAddesses(); + protected abstract IReadOnlyList
GetAddresses(); protected abstract IReadOnlyList GetEndpoints(); - IReadOnlyList
IAddressCollectionProvider.Addresses => GetAddesses(); + IReadOnlyList
IAddressCollectionProvider.Addresses => GetAddresses(); IReadOnlyList IEndpointCollectionProvider.Endpoints => GetEndpoints(); } diff --git a/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs b/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs index 292f99ee22..8a534315ac 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -12,11 +11,13 @@ using Microsoft.AspNetCore.Dispatcher.Patterns; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; +using Primitives = Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Dispatcher { public class TreeMatcher : MatcherBase { + private bool _onChange; private bool _dataInitialized; private object _lock; private Cache _cache; @@ -30,8 +31,6 @@ namespace Microsoft.AspNetCore.Dispatcher _initializer = CreateCache; } - public int Version { get; private set; } - public override async Task MatchAsync(MatcherContext context) { if (context == null) @@ -39,7 +38,7 @@ namespace Microsoft.AspNetCore.Dispatcher throw new ArgumentNullException(nameof(context)); } - EnsureServicesInitialized(context); + EnsureTreeMatcherServicesInitialized(context); var cache = LazyInitializer.EnsureInitialized(ref _cache, ref _dataInitialized, ref _lock, _initializer); @@ -101,6 +100,24 @@ namespace Microsoft.AspNetCore.Dispatcher } } + private void EnsureTreeMatcherServicesInitialized(MatcherContext context) + { + EnsureServicesInitialized(context); + if (Volatile.Read(ref _onChange)) + { + return; + } + + lock (_lock) + { + if (!Volatile.Read(ref _onChange)) + { + _onChange = true; + Primitives.ChangeToken.OnChange(() => ChangeToken, () => Volatile.Write(ref _dataInitialized, false)); + } + } + } + private bool MatchConstraints(HttpContext httpContext, DispatcherValueCollection values, IDictionary constraints) { if (constraints != null) @@ -136,8 +153,7 @@ namespace Microsoft.AspNetCore.Dispatcher { var endpoint = endpoints[i]; - var templateEndpoint = endpoint as IRoutePatternEndpoint; - if (templateEndpoint == null) + if (!(endpoint is IRoutePatternEndpoint templateEndpoint)) { continue; } diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/Tree/TreeMatcherTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/Tree/TreeMatcherTest.cs index a4d2ad88c5..d89663b77d 100644 --- a/test/Microsoft.AspNetCore.Dispatcher.Test/Tree/TreeMatcherTest.cs +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/Tree/TreeMatcherTest.cs @@ -1,12 +1,16 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Moq; using Xunit; @@ -776,6 +780,37 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Null(context.Endpoint); } + [Fact] + public async Task ChangeTokenTriggers_CreateCache() + { + // Arrange 1 + var factory = new TreeMatcherFactory(); + var dataSource = new TestDataSource(new List() { new RoutePatternEndpoint("test/{p1}", new object(), Test_Delegate, "Test"), }); + var matcher = factory.CreateMatcher(dataSource, new List()); + var context = CreateMatcherContext("/test/parameter1"); + + // Act 1 + await matcher.MatchAsync(context); + + // Assert 1 + Assert.Equal("Test", context.Endpoint.DisplayName); + context.Values.TryGetValue("p1", out var value); + Assert.Equal("parameter1", value); + + // Arrange 2 + dataSource.UpdateEndpoints(new List() { new RoutePatternEndpoint("test2/{p2}", new object(), Test_Delegate, "Test2"), }); + context = CreateMatcherContext("/test2/parameter2"); + + // Act 2 + await matcher.MatchAsync(context); + + // Assert 2 + Assert.Equal("Test2", context.Endpoint.DisplayName); + Assert.False(context.Values.TryGetValue("p1", out value)); + context.Values.TryGetValue("p2", out value); + Assert.Equal("parameter2", value); + } + private static MatcherContext CreateMatcherContext(string requestPath) { var request = new Mock(MockBehavior.Strict); @@ -804,5 +839,66 @@ namespace Microsoft.AspNetCore.Dispatcher { return Task.CompletedTask; } + + public class TestDataSource : DispatcherDataSource, IAddressCollectionProvider, IEndpointCollectionProvider + { + private IList
_addresses; + private IList _endpoints; + private TestChangeToken _changeToken; + private readonly object _lock = new object(); + + public TestDataSource(IList endpoints) + { + _addresses = new List
(); + _endpoints = endpoints; + _changeToken = new TestChangeToken(); + } + + public void UpdateEndpoints(IList endpoints) + { + lock (_lock) + { + _endpoints = endpoints; + // Trigger change token here + var previousToken = _changeToken; + _changeToken = new TestChangeToken(); + previousToken.OnChange(); + } + } + + public override IChangeToken ChangeToken + { + get + { + lock (_lock) + { + return _changeToken; + } + } + } + + IReadOnlyList
IAddressCollectionProvider.Addresses => GetAddresses(); + + IReadOnlyList IEndpointCollectionProvider.Endpoints => GetEndpoints(); + + protected override IReadOnlyList
GetAddresses() => _addresses.ToList(); + + protected override IReadOnlyList GetEndpoints() => _endpoints.ToList(); + + public IList Endpoints => _endpoints; + + private class TestChangeToken : IChangeToken + { + private CancellationTokenSource _cts = new CancellationTokenSource(); + + public bool HasChanged => true; + + public bool ActiveChangeCallbacks => true; + + public IDisposable RegisterChangeCallback(Action callback, object state) => _cts.Token.Register(callback, state); + + public void OnChange() => _cts.Cancel(); + } + } } }