diff --git a/NuGet.Server.Common.sln b/NuGet.Server.Common.sln index 3c2422a6..9cb1a571 100644 --- a/NuGet.Server.Common.sln +++ b/NuGet.Server.Common.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27005.2 +VisualStudioVersion = 15.0.27004.2006 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8415FED7-1BED-4227-8B4F-BB7C24E041CD}" EndProject @@ -50,6 +50,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Services.Contracts.Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Services.ServiceBus.Tests", "tests\NuGet.Services.ServiceBus.Tests\NuGet.Services.ServiceBus.Tests.csproj", "{FF5CA51A-CD6A-463F-AE9A-5737FF0FCFA7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Services.KeyVault.Tests", "tests\NuGet.Services.KeyVault.Tests\NuGet.Services.KeyVault.Tests.csproj", "{BA1FB5F1-8F6B-4558-862B-F47C3995B06A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -132,6 +134,10 @@ Global {FF5CA51A-CD6A-463F-AE9A-5737FF0FCFA7}.Debug|Any CPU.Build.0 = Debug|Any CPU {FF5CA51A-CD6A-463F-AE9A-5737FF0FCFA7}.Release|Any CPU.ActiveCfg = Release|Any CPU {FF5CA51A-CD6A-463F-AE9A-5737FF0FCFA7}.Release|Any CPU.Build.0 = Release|Any CPU + {BA1FB5F1-8F6B-4558-862B-F47C3995B06A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA1FB5F1-8F6B-4558-862B-F47C3995B06A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA1FB5F1-8F6B-4558-862B-F47C3995B06A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA1FB5F1-8F6B-4558-862B-F47C3995B06A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -156,6 +162,7 @@ Global {E29F54DF-DFB8-4E27-940D-21ECCB9B6FC1} = {7783A106-0F4C-4055-9AB4-413FB2C7B8F0} {79F72C83-E94D-4D04-B904-5A4DA161168E} = {7783A106-0F4C-4055-9AB4-413FB2C7B8F0} {FF5CA51A-CD6A-463F-AE9A-5737FF0FCFA7} = {7783A106-0F4C-4055-9AB4-413FB2C7B8F0} + {BA1FB5F1-8F6B-4558-862B-F47C3995B06A} = {7783A106-0F4C-4055-9AB4-413FB2C7B8F0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AA413DB0-5475-4B5D-A3AF-6323DA8D538B} diff --git a/src/NuGet.Services.KeyVault/CachingSecretReader.cs b/src/NuGet.Services.KeyVault/CachingSecretReader.cs index 293ff567..403703bb 100644 --- a/src/NuGet.Services.KeyVault/CachingSecretReader.cs +++ b/src/NuGet.Services.KeyVault/CachingSecretReader.cs @@ -2,7 +2,7 @@ // 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.Collections.Concurrent; using System.Threading.Tasks; namespace NuGet.Services.KeyVault @@ -10,39 +10,65 @@ namespace NuGet.Services.KeyVault public class CachingSecretReader : ISecretReader { public const int DefaultRefreshIntervalSec = 60 * 60 * 24; // 1 day - private readonly int _refreshIntervalSec; private readonly ISecretReader _internalReader; - private readonly Dictionary> _cache; + private readonly ConcurrentDictionary _cache; + private readonly TimeSpan _refreshInterval; public CachingSecretReader(ISecretReader secretReader, int refreshIntervalSec = DefaultRefreshIntervalSec) { - if (secretReader == null) + _internalReader = secretReader ?? throw new ArgumentNullException(nameof(secretReader)); + _cache = new ConcurrentDictionary(); + + _refreshInterval = TimeSpan.FromSeconds(refreshIntervalSec); + } + + public async Task GetSecretAsync(string secretName) + { + if (string.IsNullOrEmpty(secretName)) { - throw new ArgumentNullException(nameof(secretReader)); + throw new ArgumentException("Null or empty secret name", nameof(secretName)); } - _internalReader = secretReader; - _cache = new Dictionary>(); + // If the cache contains the secret and it is not expired, return the cached value. + if (_cache.TryGetValue(secretName, out CachedSecret result) + && !IsSecretOutdated(result)) + { + return result.Value; + } + + // The cache does not contain a fresh copy of the secret. Fetch and cache the secret. + var updatedValue = new CachedSecret(await _internalReader.GetSecretAsync(secretName)); - _refreshIntervalSec = refreshIntervalSec; + return _cache.AddOrUpdate(secretName, updatedValue, (key, old) => updatedValue) + .Value; } - public virtual bool IsSecretOutdated(Tuple cachedSecret) + private bool IsSecretOutdated(CachedSecret secret) { - return DateTime.UtcNow.Subtract(cachedSecret.Item2).TotalSeconds >= _refreshIntervalSec; + return (DateTime.UtcNow - secret.CacheTime) >= _refreshInterval; } - public async Task GetSecretAsync(string secretName) + /// + /// A cached secret. + /// + private class CachedSecret { - if (!_cache.ContainsKey(secretName) || IsSecretOutdated(_cache[secretName])) + public CachedSecret(string value) { - // Get the secret if it is not yet in the cache or it is outdated. - var secretValue = await _internalReader.GetSecretAsync(secretName); - _cache[secretName] = Tuple.Create(secretValue, DateTime.UtcNow); + Value = value; + CacheTime = DateTimeOffset.UtcNow; } - return _cache[secretName].Item1; + /// + /// The value of the cached secret. + /// + public string Value { get; } + + /// + /// The time at which the secret was cached. + /// + public DateTimeOffset CacheTime { get; } } } } \ No newline at end of file diff --git a/src/NuGet.Services.ServiceBus/NuGet.Services.ServiceBus.csproj b/src/NuGet.Services.ServiceBus/NuGet.Services.ServiceBus.csproj index f49cab35..94268b72 100644 --- a/src/NuGet.Services.ServiceBus/NuGet.Services.ServiceBus.csproj +++ b/src/NuGet.Services.ServiceBus/NuGet.Services.ServiceBus.csproj @@ -46,6 +46,7 @@ + diff --git a/src/NuGet.Services.ServiceBus/ScopedMessageHandler.cs b/src/NuGet.Services.ServiceBus/ScopedMessageHandler.cs new file mode 100644 index 00000000..24ab8f64 --- /dev/null +++ b/src/NuGet.Services.ServiceBus/ScopedMessageHandler.cs @@ -0,0 +1,52 @@ +// 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.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace NuGet.Services.ServiceBus +{ + /// + /// Handles messages received by a . + /// Each message will be handled within its own dependency injection scope. + /// + /// The type of messages this handler handles. + public class ScopedMessageHandler + { + /// + /// The factory used to create independent dependency injection scopes for each message. + /// + private readonly IServiceScopeFactory _scopeFactory; + + public ScopedMessageHandler(IServiceScopeFactory scopeFactory) + { + _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); + } + + /// + /// Handle the message in its own dependency injection scope. + /// + /// The received message. + /// Whether the message has been handled. If false, the message will be requeued to be handled again later. + public async Task HandleAsync(TMessage message) + { + // Create a new scope for this message. + using (var scope = _scopeFactory.CreateScope()) + { + // Resolve a new message handler for the newly created scope and let it handle the message. + return await ResolveMessageHandler(scope).HandleAsync(message); + } + } + + /// + /// Resolve the message handler given a specific dependency injection scope. + /// + /// The dependency injection scope that should be used to resolve services. + /// The resolved message handler service from the given scope. + private IMessageHandler ResolveMessageHandler(IServiceScope scope) + { + return scope.ServiceProvider.GetRequiredService>(); + } + } +} diff --git a/src/NuGet.Services.ServiceBus/project.json b/src/NuGet.Services.ServiceBus/project.json index 8ec43fa3..1ea393cd 100644 --- a/src/NuGet.Services.ServiceBus/project.json +++ b/src/NuGet.Services.ServiceBus/project.json @@ -1,5 +1,6 @@ { "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "1.1.1", "Microsoft.Extensions.Logging": "1.1.2", "Newtonsoft.Json": "9.0.1", "WindowsAzure.ServiceBus": "4.1.3" diff --git a/src/NuGet.Services.Validation/Entities/PackageSignature.cs b/src/NuGet.Services.Validation/Entities/PackageSignature.cs index 39273723..055298e6 100644 --- a/src/NuGet.Services.Validation/Entities/PackageSignature.cs +++ b/src/NuGet.Services.Validation/Entities/PackageSignature.cs @@ -37,6 +37,11 @@ public class PackageSignature /// public PackageSignatureStatus Status { get; set; } + /// + /// Used for optimistic concurrency when updating package signatures. + /// + public byte[] RowVersion { get; set; } + /// /// The that owns this . If this signature /// has a Status of "Invalid", the overall will also be "Invalid". Note that diff --git a/src/NuGet.Services.Validation/Entities/ValidationContext.cs b/src/NuGet.Services.Validation/Entities/ValidationContext.cs index cbc99a7e..ca8d4f7b 100644 --- a/src/NuGet.Services.Validation/Entities/ValidationContext.cs +++ b/src/NuGet.Services.Validation/Entities/ValidationContext.cs @@ -262,15 +262,21 @@ private void RegisterPackageSigningEntities(DbModelBuilder modelBuilder) .Property(s => s.CreatedAt) .HasColumnType("datetime2"); + modelBuilder.Entity() + .Property(c => c.RowVersion) + .IsRowVersion(); + modelBuilder.Entity() .HasMany(s => s.TrustedTimestamps) .WithRequired(t => t.PackageSignature) - .HasForeignKey(t => t.PackageSignatureKey); + .HasForeignKey(t => t.PackageSignatureKey) + .WillCascadeOnDelete(); modelBuilder.Entity() .HasRequired(s => s.Certificate) .WithMany(c => c.PackageSignatures) - .HasForeignKey(s => s.CertificateKey); + .HasForeignKey(s => s.CertificateKey) + .WillCascadeOnDelete(false); modelBuilder.Entity() .ToTable(TrustedTimestampsTable, SignatureSchema) @@ -283,7 +289,8 @@ private void RegisterPackageSigningEntities(DbModelBuilder modelBuilder) modelBuilder.Entity() .HasRequired(s => s.Certificate) .WithMany(c => c.TrustedTimestamps) - .HasForeignKey(s => s.CertificateKey); + .HasForeignKey(s => s.CertificateKey) + .WillCascadeOnDelete(false); modelBuilder.Entity() .ToTable(CertificatesTable, SignatureSchema) diff --git a/src/NuGet.Services.Validation/Migrations/201710191647050_AddPackageSigningSchema.cs b/src/NuGet.Services.Validation/Migrations/201710191647050_AddPackageSigningSchema.cs index bc55256a..f1d9c1cb 100644 --- a/src/NuGet.Services.Validation/Migrations/201710191647050_AddPackageSigningSchema.cs +++ b/src/NuGet.Services.Validation/Migrations/201710191647050_AddPackageSigningSchema.cs @@ -33,9 +33,10 @@ public override void Up() CertificateKey = c.Long(nullable: false), CreatedAt = c.DateTime(nullable: false, precision: 7, storeType: "datetime2"), Status = c.Int(nullable: false), + RowVersion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"), }) .PrimaryKey(t => t.Key) - .ForeignKey("signature.Certificates", t => t.CertificateKey, cascadeDelete: true) + .ForeignKey("signature.Certificates", t => t.CertificateKey) .ForeignKey("signature.PackageSigningStates", t => t.PackageKey, cascadeDelete: true) .Index(t => t.PackageKey, name: "IX_PackageSignatures_PackageKey") .Index(t => t.CertificateKey, name: "IX_PackageSignatures_CertificateKey") @@ -63,7 +64,7 @@ public override void Up() Value = c.DateTime(nullable: false, precision: 7, storeType: "datetime2"), }) .PrimaryKey(t => t.Key) - .ForeignKey("signature.Certificates", t => t.CertificateKey, cascadeDelete: true) + .ForeignKey("signature.Certificates", t => t.CertificateKey) .ForeignKey("signature.PackageSignatures", t => t.PackageSignatureKey, cascadeDelete: true) .Index(t => t.PackageSignatureKey) .Index(t => t.CertificateKey); diff --git a/src/NuGet.Services.Validation/Migrations/201710191647050_AddPackageSigningSchema.resx b/src/NuGet.Services.Validation/Migrations/201710191647050_AddPackageSigningSchema.resx index 3addc573..b9424eb2 100644 --- a/src/NuGet.Services.Validation/Migrations/201710191647050_AddPackageSigningSchema.resx +++ b/src/NuGet.Services.Validation/Migrations/201710191647050_AddPackageSigningSchema.resx @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - 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 dbo diff --git a/tests/NuGet.Services.KeyVault.Tests/CachingSecretReaderFacts.cs b/tests/NuGet.Services.KeyVault.Tests/CachingSecretReaderFacts.cs index 2aa8bc01..2be8bce1 100644 --- a/tests/NuGet.Services.KeyVault.Tests/CachingSecretReaderFacts.cs +++ b/tests/NuGet.Services.KeyVault.Tests/CachingSecretReaderFacts.cs @@ -6,6 +6,7 @@ using Moq; using Xunit; using System.Threading; +using System.Diagnostics; namespace NuGet.Services.KeyVault.Tests { @@ -31,27 +32,6 @@ public async Task WhenGetSecretIsCalledCacheIsUsed() Assert.Equal(value1, value2); } - [Theory] - // Secret was refreshed more recently than the refresh interval and is not outdated. - [InlineData(2, 1, false)] - // Secret was refreshed after the refresh interval is outdated - [InlineData(2, 3, true)] - // Secret was refreshed exactly on the refresh interval and is outdated. - [InlineData(2, 2, true)] - public void CorrectlyIdentifiesOutdatedSecrets(int refreshIntervalSec, int secretLastRefreshedSec, bool isOutdated) - { - // Arrange - var cachingSecretReaderMock = new Mock(new Mock().Object, refreshIntervalSec) {CallBase = true}; - var secretToCheck = Tuple.Create("secretName", - DateTime.UtcNow.Add(new TimeSpan(0, 0, -secretLastRefreshedSec))); - - // Act - var result = cachingSecretReaderMock.Object.IsSecretOutdated(secretToCheck); - - // Assert - Assert.Equal(isOutdated, result); - } - [Fact] public async Task WhenGetSecretIsCalledCacheIsRefreshedIfPastInterval() { @@ -62,50 +42,35 @@ public async Task WhenGetSecretIsCalledCacheIsRefreshedIfPastInterval() const int refreshIntervalSec = 1; var mockSecretReader = new Mock(); - mockSecretReader.Setup(x => x.GetSecretAsync(It.IsAny())).Returns(Task.FromResult(firstSecret)); - - var cachingSecretReaderMock = new Mock(mockSecretReader.Object, refreshIntervalSec) - { - CallBase = true - }; - var hasIntervalPassed = false; - cachingSecretReaderMock.Setup(x => x.IsSecretOutdated(It.IsAny>())).Returns(() => - { - // If the interval hasn't passed, the secret we have stored is not outdated. - if (!hasIntervalPassed) - { - return false; - } + mockSecretReader + .SetupSequence(x => x.GetSecretAsync(It.IsAny())) + .Returns(Task.FromResult(firstSecret)) + .Returns(Task.FromResult(secondSecret)); - // If the interval has passed, the secret is outdated. - // It will be refreshed and then the interval will not have passed again. - hasIntervalPassed = false; - return true; - }); + var cachingSecretReader = new CachingSecretReader(mockSecretReader.Object, refreshIntervalSec); // Act - var firstValue1 = await cachingSecretReaderMock.Object.GetSecretAsync(secretName); - var firstValue2 = await cachingSecretReaderMock.Object.GetSecretAsync(secretName); + var firstValue1 = await cachingSecretReader.GetSecretAsync(secretName); + var firstValue2 = await cachingSecretReader.GetSecretAsync(secretName); // Assert mockSecretReader.Verify(x => x.GetSecretAsync(It.IsAny()), Times.Once); Assert.Equal(firstSecret, firstValue1); - Assert.Equal(firstValue1, firstValue2); + Assert.Equal(firstSecret, firstValue2); // Arrange 2 // We are now x seconds later after refreshIntervalSec has passed. - hasIntervalPassed = true; - mockSecretReader.Setup(x => x.GetSecretAsync(It.IsAny())).Returns(Task.FromResult(secondSecret)); + await Task.Delay(TimeSpan.FromSeconds(refreshIntervalSec * 2)); // Act 2 - var secondValue1 = await cachingSecretReaderMock.Object.GetSecretAsync(secretName); - var secondValue2 = await cachingSecretReaderMock.Object.GetSecretAsync(secretName); + var secondValue1 = await cachingSecretReader.GetSecretAsync(secretName); + var secondValue2 = await cachingSecretReader.GetSecretAsync(secretName); // Assert 2 mockSecretReader.Verify(x => x.GetSecretAsync(It.IsAny()), Times.Exactly(2)); Assert.Equal(secondSecret, secondValue1); - Assert.Equal(secondValue1, secondValue2); + Assert.Equal(secondSecret, secondValue2); } } } diff --git a/tests/NuGet.Services.ServiceBus.Tests/NuGet.Services.ServiceBus.Tests.csproj b/tests/NuGet.Services.ServiceBus.Tests/NuGet.Services.ServiceBus.Tests.csproj index 48a7d773..6ffdc036 100644 --- a/tests/NuGet.Services.ServiceBus.Tests/NuGet.Services.ServiceBus.Tests.csproj +++ b/tests/NuGet.Services.ServiceBus.Tests/NuGet.Services.ServiceBus.Tests.csproj @@ -43,6 +43,7 @@ + diff --git a/tests/NuGet.Services.ServiceBus.Tests/ScopedMessageHandlerFacts.cs b/tests/NuGet.Services.ServiceBus.Tests/ScopedMessageHandlerFacts.cs new file mode 100644 index 00000000..9f19fae2 --- /dev/null +++ b/tests/NuGet.Services.ServiceBus.Tests/ScopedMessageHandlerFacts.cs @@ -0,0 +1,47 @@ +// 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.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace NuGet.Services.ServiceBus.Tests +{ + public class ScopedMessageHandlerFacts + { + [Fact] + public async Task CreatesMessageHandlerUsingScope() + { + // Arrange + var scopeFactoryMock = new Mock(); + var scopeMock = new Mock(); + var serviceProviderMock = new Mock(); + var handlerMock = new Mock>(); + + scopeFactoryMock + .Setup(f => f.CreateScope()) + .Returns(scopeMock.Object); + + scopeMock + .SetupGet(s => s.ServiceProvider) + .Returns(serviceProviderMock.Object); + + serviceProviderMock + .Setup(p => p.GetService(typeof(IMessageHandler))) + .Returns(handlerMock.Object); + + var target = new ScopedMessageHandler(scopeFactoryMock.Object); + + // Act - send two empty messages. + await target.HandleAsync(new object()); + await target.HandleAsync(new object()); + + // Assert + scopeFactoryMock.Verify(f => f.CreateScope(), Times.Exactly(2)); + serviceProviderMock.Verify(p => p.GetService(typeof(IMessageHandler)), Times.Exactly(2)); + handlerMock.Verify(h => h.HandleAsync(It.IsAny()), Times.Exactly(2)); + } + } +}