Skip to content
This repository was archived by the owner on Jul 30, 2024. It is now read-only.

Commit b5caf0b

Browse files
authored
Prevent duplicate validations from starting during some grace period (#315)
* Added check for existence of the previously created validation set for a given package id&version. Validation request is discarded if such validation set exists. * Typo fix * TryGetOrCreateValidationSetAsync * OtherRecentValidationSetForPackageExists * NewValidationRequestDeduplicationWindow * Log on deduping.
1 parent 49e608b commit b5caf0b

10 files changed

Lines changed: 139 additions & 12 deletions

src/NuGet.Services.Validation.Orchestrator/IValidationSetProvider.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ public interface IValidationSetProvider
1717
/// </summary>
1818
/// <param name="validationTrackingId">Validation tracking id</param>
1919
/// <param name="package">Package details from Gallery DB</param>
20-
/// <returns><see cref="PackageValidationSet"/> object with information about requested <paramref name="validationTrackingId"/></returns>
21-
Task<PackageValidationSet> GetOrCreateValidationSetAsync(Guid validationTrackingId, Package package);
20+
/// <returns><see cref="PackageValidationSet"/> object with information about
21+
/// requested <paramref name="validationTrackingId"/>. Null if no further processing
22+
/// should be made (e.g. duplicate validation request was detected).
23+
/// </returns>
24+
Task<PackageValidationSet> TryGetOrCreateValidationSetAsync(Guid validationTrackingId, Package package);
2225
}
2326
}

src/NuGet.Services.Validation.Orchestrator/IValidationStorageService.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,21 @@ public interface IValidationStorageService
5050
/// <param name="validationResult">The result of the validation.</param>
5151
/// <returns>Task object tracking the async operation status.</returns>
5252
Task UpdateValidationStatusAsync(PackageValidation packageValidation, IValidationResult validationResult);
53+
54+
/// <summary>
55+
/// Checks whether a validation set was created within the time range specified
56+
/// by <paramref name="recentDuration"/> argument with tracking id that is different
57+
/// from the supplied as a <paramref name="currentValidationSetTrackingId"/> argument.
58+
/// </summary>
59+
/// <param name="packageId">Package ID for which recent validation sets are to be looked up.</param>
60+
/// <param name="normalizedVersion">Normalized version of the package.</param>
61+
/// <param name="recentDuration">Max amount of time to look back.</param>
62+
/// <param name="currentValidationSetTrackingId">Validation set tracking for the currently processed request.</param>
63+
/// <returns>True if validation set exists, false otherwise.</returns>
64+
Task<bool> OtherRecentValidationSetForPackageExists(
65+
string packageId,
66+
string normalizedVersion,
67+
TimeSpan recentDuration,
68+
Guid currentValidationSetTrackingId);
5369
}
5470
}

src/NuGet.Services.Validation.Orchestrator/ValidationConfiguration.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,13 @@ public class ValidationConfiguration
2525
/// Time to wait between checking the state of a certain validation.
2626
/// </summary>
2727
public TimeSpan ValidationMessageRecheckPeriod { get; set; }
28+
29+
/// <summary>
30+
/// The duplication detection window used for new validation requests.
31+
/// Validation requests will be ignored if there exists another validation
32+
/// request for the same package id and version that was created within
33+
/// this window.
34+
/// </summary>
35+
public TimeSpan NewValidationRequestDeduplicationWindow { get; set; }
2836
}
2937
}

src/NuGet.Services.Validation.Orchestrator/ValidationFailureBehavior.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
namespace NuGet.Services.Validation.Orchestrator
55
{
66
/// <summary>
7-
/// Options for orchestrator for how to handle valiation failure
7+
/// Options for orchestrator for how to handle validation failure
88
/// </summary>
99
public enum ValidationFailureBehavior
1010
{

src/NuGet.Services.Validation.Orchestrator/ValidationMessageHandler.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,16 @@ public async Task<bool> HandleAsync(PackageValidationMessageData message)
4646

4747
using (_logger.BeginScope("Handling message for {PackageId} {PackageVersion} validation set {ValidationSetId}", message.PackageId, message.PackageVersion, message.ValidationTrackingId))
4848
{
49-
var validationSet = await _validationSetProvider.GetOrCreateValidationSetAsync(message.ValidationTrackingId, package);
49+
var validationSet = await _validationSetProvider.TryGetOrCreateValidationSetAsync(message.ValidationTrackingId, package);
50+
51+
if (validationSet == null)
52+
{
53+
_logger.LogInformation("The validation request for {PackageId} {PackageVersion} validation set {ValidationSetId} is a duplicate. Discarding.",
54+
message.PackageId,
55+
message.PackageVersion,
56+
message.ValidationTrackingId);
57+
return true;
58+
}
5059

5160
await _validationSetProcessor.ProcessValidationsAsync(validationSet, package);
5261
await _validationOutcomeProcessor.ProcessValidationOutcomeAsync(validationSet, package);

src/NuGet.Services.Validation.Orchestrator/ValidationSetProvider.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,21 @@ public ValidationSetProvider(
3030
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
3131
}
3232

33-
public async Task<PackageValidationSet> GetOrCreateValidationSetAsync(Guid validationTrackingId, Package package)
33+
public async Task<PackageValidationSet> TryGetOrCreateValidationSetAsync(Guid validationTrackingId, Package package)
3434
{
3535
var validationSet = await _validationStorageService.GetValidationSetAsync(validationTrackingId);
3636

3737
if (validationSet == null)
3838
{
39+
var shouldSkip = await _validationStorageService.OtherRecentValidationSetForPackageExists(
40+
package.PackageRegistration.Id,
41+
package.NormalizedVersion,
42+
_validationConfiguration.NewValidationRequestDeduplicationWindow,
43+
validationTrackingId);
44+
if (shouldSkip)
45+
{
46+
return null;
47+
}
3948
validationSet = await CreateValidationSet(validationTrackingId, package);
4049
}
4150
else

src/NuGet.Services.Validation.Orchestrator/ValidationStorageService.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Data.Entity;
7+
using System.Linq;
78
using System.Threading.Tasks;
89
using Microsoft.Extensions.Logging;
910

@@ -101,6 +102,21 @@ public async Task UpdateValidationStatusAsync(PackageValidation packageValidatio
101102
await _validationContext.SaveChangesAsync();
102103
}
103104

105+
public async Task<bool> OtherRecentValidationSetForPackageExists(
106+
string packageId,
107+
string normalizedVersion,
108+
TimeSpan recentDuration,
109+
Guid currentValidationSetTrackingId)
110+
{
111+
var cutoffTimestamp = DateTime.UtcNow - recentDuration;
112+
return await _validationContext
113+
.PackageValidationSets
114+
.AnyAsync(pvs => pvs.PackageId == packageId
115+
&& pvs.PackageNormalizedVersion == normalizedVersion
116+
&& pvs.Created > cutoffTimestamp
117+
&& pvs.ValidationTrackingId != currentValidationSetTrackingId);
118+
}
119+
104120
private void AddValidationIssues(PackageValidation packageValidation, IReadOnlyList<IValidationIssue> validationIssues)
105121
{
106122
foreach (var validationIssue in validationIssues)

src/NuGet.Services.Validation.Orchestrator/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
}
1111
],
1212
"ValidationStorageConnectionString": "",
13-
"ValidationMessageRecheckPeriod": "00:01:00"
13+
"ValidationMessageRecheckPeriod": "00:01:00",
14+
"NewValidationRequestDeduplicationWindow": "00:10:00"
1415
},
1516
"Vcs": {
1617
"ContainerName": "validation",

tests/NuGet.Services.Validation.Orchestrator.Tests/ValidationMessageHandlerFacts.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,34 @@ public async Task WaitsForPackageAvailabilityInGalleryDB()
3535

3636
CorePackageServiceMock.Verify(ps => ps.FindPackageByIdAndVersionStrict(messageData.PackageId, messageData.PackageVersion), Times.Once());
3737
}
38+
39+
[Fact]
40+
public async Task DropsMessageOnDuplicateValidationRequest()
41+
{
42+
var messageData = new PackageValidationMessageData("packageId", "1.2.3", Guid.NewGuid());
43+
var validationConfiguration = new ValidationConfiguration();
44+
var package = new Package();
45+
46+
CorePackageServiceMock
47+
.Setup(ps => ps.FindPackageByIdAndVersionStrict(messageData.PackageId, messageData.PackageVersion))
48+
.Returns(package)
49+
.Verifiable();
50+
51+
ValidationSetProviderMock
52+
.Setup(vsp => vsp.TryGetOrCreateValidationSetAsync(messageData.ValidationTrackingId, package))
53+
.ReturnsAsync(null)
54+
.Verifiable();
55+
56+
var handler = CreateHandler();
57+
58+
var result = await handler.HandleAsync(messageData);
59+
60+
Assert.True(result);
61+
CorePackageServiceMock
62+
.Verify(ps => ps.FindPackageByIdAndVersionStrict(messageData.PackageId, messageData.PackageVersion), Times.Once());
63+
ValidationSetProviderMock
64+
.Verify(vsp => vsp.TryGetOrCreateValidationSetAsync(messageData.ValidationTrackingId, package), Times.Once());
65+
}
3866
}
3967

4068
public class ValidationMessageHandlerLooseFacts : ValidationMessageHandlerFactsBase
@@ -55,7 +83,7 @@ public ValidationMessageHandlerLooseFacts()
5583
.Returns(Package);
5684

5785
ValidationSetProviderMock
58-
.Setup(vsp => vsp.GetOrCreateValidationSetAsync(MessageData.ValidationTrackingId, Package))
86+
.Setup(vsp => vsp.TryGetOrCreateValidationSetAsync(MessageData.ValidationTrackingId, Package))
5987
.ReturnsAsync(ValidationSet);
6088
}
6189

@@ -66,7 +94,7 @@ public async Task MakesSureValidationSetExists()
6694
await handler.HandleAsync(MessageData);
6795

6896
ValidationSetProviderMock
69-
.Verify(vsp => vsp.GetOrCreateValidationSetAsync(MessageData.ValidationTrackingId, Package));
97+
.Verify(vsp => vsp.TryGetOrCreateValidationSetAsync(MessageData.ValidationTrackingId, Package));
7098
}
7199

72100
[Fact]

tests/NuGet.Services.Validation.Orchestrator.Tests/ValidationSetProviderFacts.cs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public async Task TriesToGetValidationSetFirst()
3232

3333
var provider = CreateProvider();
3434

35-
var set = await provider.GetOrCreateValidationSetAsync(ValidationSet.ValidationTrackingId, Package);
35+
var set = await provider.TryGetOrCreateValidationSetAsync(ValidationSet.ValidationTrackingId, Package);
3636

3737
ValidationStorageMock
3838
.Verify(vs => vs.GetValidationSetAsync(ValidationSet.ValidationTrackingId), Times.Once());
@@ -51,7 +51,7 @@ public async Task ThrowsIfPackageIdDoesNotMatchValidationSet()
5151

5252
var provider = CreateProvider();
5353

54-
var ex = await Assert.ThrowsAsync<Exception>(() => provider.GetOrCreateValidationSetAsync(ValidationSet.ValidationTrackingId, Package));
54+
var ex = await Assert.ThrowsAsync<Exception>(() => provider.TryGetOrCreateValidationSetAsync(ValidationSet.ValidationTrackingId, Package));
5555
Assert.Contains(ValidationSet.PackageId, ex.Message);
5656
Assert.Contains(Package.PackageRegistration.Id, ex.Message);
5757
}
@@ -67,7 +67,7 @@ public async Task ThrowsIfPackageVersionDoesNotMatchValidationSet()
6767

6868
var provider = CreateProvider();
6969

70-
var ex = await Assert.ThrowsAsync<Exception>(() => provider.GetOrCreateValidationSetAsync(ValidationSet.ValidationTrackingId, Package));
70+
var ex = await Assert.ThrowsAsync<Exception>(() => provider.TryGetOrCreateValidationSetAsync(ValidationSet.ValidationTrackingId, Package));
7171
Assert.Contains(ValidationSet.PackageNormalizedVersion, ex.Message);
7272
Assert.Contains(Package.NormalizedVersion, ex.Message);
7373
}
@@ -89,6 +89,10 @@ public async Task ProperlyConstructsValidationSet()
8989
.ReturnsAsync(null)
9090
.Verifiable();
9191

92+
ValidationStorageMock
93+
.Setup(vs => vs.OtherRecentValidationSetForPackageExists(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<TimeSpan>(), validationTrackingId))
94+
.ReturnsAsync(false);
95+
9296
PackageValidationSet createdSet = null;
9397
ValidationStorageMock
9498
.Setup(vs => vs.CreateValidationSetAsync(It.IsAny<PackageValidationSet>()))
@@ -101,7 +105,7 @@ public async Task ProperlyConstructsValidationSet()
101105
ConfigurationAccessorMock.Object,
102106
LoggerMock.Object);
103107

104-
var returnedSet = await provider.GetOrCreateValidationSetAsync(validationTrackingId, Package);
108+
var returnedSet = await provider.TryGetOrCreateValidationSetAsync(validationTrackingId, Package);
105109
var endOfCallTimestamp = DateTime.UtcNow;
106110

107111
ValidationStorageMock
@@ -127,6 +131,39 @@ public async Task ProperlyConstructsValidationSet()
127131
Assert.Contains(createdSet.PackageValidations, v => v.Type == validation2);
128132
}
129133

134+
[Fact]
135+
public async Task GetOrCreateValidationSetAsyncDoesNotCreateDuplicateValidationSet()
136+
{
137+
Guid validationTrackingId = Guid.NewGuid();
138+
139+
ValidationStorageMock
140+
.Setup(vs => vs.GetValidationSetAsync(validationTrackingId))
141+
.ReturnsAsync(null);
142+
143+
ValidationStorageMock
144+
.Setup(vs => vs.OtherRecentValidationSetForPackageExists(
145+
Package.PackageRegistration.Id,
146+
Package.NormalizedVersion,
147+
It.IsAny<TimeSpan>(),
148+
validationTrackingId))
149+
.ReturnsAsync(true);
150+
151+
var provider = CreateProvider();
152+
var result = await provider.TryGetOrCreateValidationSetAsync(validationTrackingId, Package);
153+
154+
Assert.Null(result);
155+
ValidationStorageMock
156+
.Verify(
157+
vs => vs.OtherRecentValidationSetForPackageExists(
158+
Package.PackageRegistration.Id,
159+
Package.NormalizedVersion,
160+
It.IsAny<TimeSpan>(),
161+
validationTrackingId),
162+
Times.Once);
163+
ValidationStorageMock
164+
.Verify(vs => vs.CreateValidationSetAsync(It.IsAny<PackageValidationSet>()), Times.Never);
165+
}
166+
130167
public ValidationSetProviderFacts()
131168
{
132169
ValidationStorageMock = new Mock<IValidationStorageService>(MockBehavior.Strict);

0 commit comments

Comments
 (0)