Skip to content

Commit 0b31339

Browse files
authored
Sending validation request right before committing changes to DB (#7084)
* Sending validation request right before committing changes to DB. With tests. * PR feedback addressed * Sending failure cleanup for symbol package uploads. * comment.
1 parent 70868c6 commit 0b31339

10 files changed

Lines changed: 188 additions & 55 deletions

src/NuGetGallery/Services/AsynchronousPackageValidationInitiator.cs

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,28 +37,19 @@ public AsynchronousPackageValidationInitiator(
3737
_diagnosticsSource = diagnosticsService.SafeGetSource(nameof(AsynchronousPackageValidationInitiator<TPackageEntity>));
3838
}
3939

40+
public PackageStatus GetPackageStatus(TPackageEntity package)
41+
{
42+
// Give an opportunity to the caller to fail early if StartValidationAsync is guaranteed to fail
43+
ValidateAndGetType(package);
44+
45+
return TargetPackageStatus;
46+
}
47+
4048
public async Task<PackageStatus> StartValidationAsync(TPackageEntity package)
4149
{
42-
if (_appConfiguration.ReadOnlyMode)
43-
{
44-
throw new ReadOnlyModeException(Strings.CannotEnqueueDueToReadOnly);
45-
}
50+
var validatingType = ValidateAndGetType(package);
4651

47-
ValidatingType validatingType;
4852
var entityKey = package.Key == default(int) ? (int?)null : package.Key;
49-
if (package is Package)
50-
{
51-
validatingType = ValidatingType.Package;
52-
}
53-
else if (package is SymbolPackage)
54-
{
55-
validatingType = ValidatingType.SymbolPackage;
56-
}
57-
else
58-
{
59-
throw new ArgumentException($"Unknown IPackageEntity type: {nameof(package)}");
60-
}
61-
6253
var data = new PackageValidationMessageData(
6354
package.Id,
6455
package.Version,
@@ -75,12 +66,30 @@ public async Task<PackageStatus> StartValidationAsync(TPackageEntity package)
7566
await _validationEnqueuer.StartValidationAsync(data, postponeProcessingTill);
7667
}
7768

78-
if (_appConfiguration.BlockingAsynchronousPackageValidationEnabled)
69+
return TargetPackageStatus;
70+
}
71+
72+
private PackageStatus TargetPackageStatus => _appConfiguration.BlockingAsynchronousPackageValidationEnabled
73+
? PackageStatus.Validating
74+
: PackageStatus.Available;
75+
76+
private ValidatingType ValidateAndGetType(TPackageEntity package)
77+
{
78+
if (_appConfiguration.ReadOnlyMode)
79+
{
80+
throw new ReadOnlyModeException(Strings.CannotEnqueueDueToReadOnly);
81+
}
82+
83+
if (package is Package)
84+
{
85+
return ValidatingType.Package;
86+
}
87+
else if (package is SymbolPackage)
7988
{
80-
return PackageStatus.Validating;
89+
return ValidatingType.SymbolPackage;
8190
}
8291

83-
return PackageStatus.Available;
92+
throw new ArgumentException($"Unknown IPackageEntity type: {nameof(package)}");
8493
}
8594
}
8695
}

src/NuGetGallery/Services/IPackageValidationInitiator.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ namespace NuGetGallery
1212
public interface IPackageValidationInitiator<TPackageEntity>
1313
where TPackageEntity: IPackageEntity
1414
{
15+
/// <summary>
16+
/// Returns the package status that package should go into when validation is started.
17+
/// Async validations typically return <see cref="PackageStatus.Validating"/>.
18+
/// Sync, non-blocking or no validation typically return <see cref="PackageStatus.Available"/>.
19+
/// Caller still must call <see cref="IPackageValidationInitiator{TPackageEntity}.StartValidationAsync(TPackageEntity)"/>
20+
/// to start the actual validation.
21+
/// </summary>
22+
/// <param name="package">The <see cref="TPackageEntity"/> to get future validation status for.</param>
23+
/// <returns>The new package status assuming the validation will be started later.</returns>
24+
/// <remarks>This method validates the argument the same way <see cref="StartValidationAsync(TPackageEntity)"/> does
25+
/// and will throw on invalid input allowing to fail earlier, before expensive operations are performed.</remarks>
26+
PackageStatus GetPackageStatus(TPackageEntity package);
27+
1528
/// <summary>
1629
/// Starts the validation for the specified IPackageEntity. The validation can be done asynchronously with respect
1730
/// to the gallery and therefore may not be complete when the returned <see cref="Task"/> completes. This

src/NuGetGallery/Services/IValidationService.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,40 @@ namespace NuGetGallery
1313
/// </summary>
1414
public interface IValidationService
1515
{
16+
/// <summary>
17+
/// Updates the package with the expected <see cref="PackageStatus"/> that the package will
18+
/// have after starting the validation.
19+
/// The caller must also call <see cref="StartValidationAsync(Package)"/>
20+
/// at later time.
21+
/// </summary>
22+
/// <param name="package">The package to update</param>
23+
/// <remarks>This method only updates the object passed into it, no database commit is performed.</remarks>
24+
Task UpdatePackageAsync(Package package);
25+
26+
/// <summary>
27+
/// Updates the symbol package with the expected <see cref="PackageStatus"/> that the package will
28+
/// have after starting the validation.
29+
/// The caller must also call <see cref="StartValidationAsync(Package)"/>
30+
/// at later time.
31+
/// </summary>
32+
/// <param name="package">The package to update</param>
33+
/// <remarks>This method only updates the object passed into it, no database commit is performed.</remarks>
34+
Task UpdatePackageAsync(SymbolPackage symbolPackage);
35+
1636
/// <summary>
1737
/// Starts the asynchronous validation for the provided new package and puts the package in the correct
1838
/// <see cref="Package.PackageStatusKey"/>. The commit to the database is the responsibility of the caller.
1939
/// </summary>
2040
/// <param name="package">The package to start validation for.</param>
41+
/// <remarks>This method only updates the object passed into it, no database commit is performed.</remarks>
2142
Task StartValidationAsync(Package package);
2243

2344
/// <summary>
2445
/// Starts the asynchronous validation for the provided new symbol package and puts the symbol package in the correct
2546
/// <see cref="Package.PackageStatusKey"/>. The commit to the database is the responsibility of the caller.
2647
/// </summary>
2748
/// <param name="symbolPackage">The symbol package to start validation for.</param>
49+
/// <remarks>This method only updates the object passed into it, no database commit is performed.</remarks>
2850
Task StartValidationAsync(SymbolPackage symbolPackage);
2951

3052
/// <summary>

src/NuGetGallery/Services/ImmediatePackageValidator.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ namespace NuGetGallery
1313
public class ImmediatePackageValidator<TPackageEntity> : IPackageValidationInitiator<TPackageEntity>
1414
where TPackageEntity: IPackageEntity
1515
{
16+
public PackageStatus GetPackageStatus(TPackageEntity package)
17+
=> PackageStatus.Available;
18+
1619
public Task<PackageStatus> StartValidationAsync(TPackageEntity package)
1720
{
1821
return Task.FromResult(PackageStatus.Available);

src/NuGetGallery/Services/PackageUploadService.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ public async Task<Package> GeneratePackageAsync(
674674

675675
public async Task<PackageCommitResult> CommitPackageAsync(Package package, Stream packageFile)
676676
{
677-
await _validationService.StartValidationAsync(package);
677+
await _validationService.UpdatePackageAsync(package);
678678

679679
if (package.PackageStatusKey != PackageStatus.Available
680680
&& package.PackageStatusKey != PackageStatus.Validating)
@@ -763,12 +763,18 @@ await _coreLicenseFileService.DeleteLicenseFileAsync(
763763

764764
try
765765
{
766+
// Sending the validation request after copying to prevent multiple validation requests
767+
// sent when several pushes for the same package happen concurrently. Copying the file
768+
// resolves the race and only one request will "win" and reach this code.
769+
await _validationService.StartValidationAsync(package);
770+
766771
// commit all changes to database as an atomic transaction
767772
await _entitiesContext.SaveChangesAsync();
768773
}
769774
catch (Exception ex)
770775
{
771-
// If saving to the DB fails for any reason we need to delete the package we just saved.
776+
// If sending the validation request or saving to the DB fails for any reason
777+
// we need to delete the package we just saved.
772778
if (package.PackageStatusKey == PackageStatus.Validating)
773779
{
774780
await _packageFileService.DeleteValidationPackageFileAsync(

src/NuGetGallery/Services/SymbolPackageUploadService.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public async Task<PackageCommitResult> CreateAndUploadSymbolsPackage(Package pac
149149
var previousSymbolsPackage = package.LatestSymbolPackage();
150150
var symbolPackage = _symbolPackageService.CreateSymbolPackage(package, packageStreamMetadata);
151151

152-
await _validationService.StartValidationAsync(symbolPackage);
152+
await _validationService.UpdatePackageAsync(symbolPackage);
153153

154154
if (symbolPackage.StatusKey != PackageStatus.Available
155155
&& symbolPackage.StatusKey != PackageStatus.Validating)
@@ -208,14 +208,19 @@ public async Task<PackageCommitResult> CreateAndUploadSymbolsPackage(Package pac
208208

209209
try
210210
{
211+
// Sending the validation request right before updating the database, so all file operations
212+
// are complete by that time and all possible conflicts are resolved.
213+
await _validationService.StartValidationAsync(symbolPackage);
214+
211215
// commit all changes to database as an atomic transaction
212216
await _entitiesContext.SaveChangesAsync();
213217
}
214218
catch (Exception ex)
215219
{
216220
ex.Log();
217221

218-
// If saving to the DB fails for any reason we need to delete the package we just saved.
222+
// If sending the validation request or saving to the DB fails for any reason
223+
// we need to delete the package we just saved.
219224
if (symbolPackage.StatusKey == PackageStatus.Validating)
220225
{
221226
await _symbolPackageFileService.DeleteValidationPackageFileAsync(

src/NuGetGallery/Services/ValidationService.cs

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,25 @@ public ValidationService(
5050
}
5151
}
5252

53+
public async Task UpdatePackageAsync(Package package)
54+
{
55+
var packageStatus = _packageValidationInitiator.GetPackageStatus(package);
56+
57+
await UpdatePackageInternalAsync(package, packageStatus);
58+
}
59+
60+
public async Task UpdatePackageAsync(SymbolPackage symbolPackage)
61+
{
62+
var symbolPackageStatus = _symbolPackageValidationInitiator.GetPackageStatus(symbolPackage);
63+
64+
await UpdateSymbolPackageInternalAsync(symbolPackage, symbolPackageStatus);
65+
}
66+
5367
public async Task StartValidationAsync(Package package)
5468
{
5569
var packageStatus = await _packageValidationInitiator.StartValidationAsync(package);
5670

57-
await _packageService.UpdatePackageStatusAsync(
58-
package,
59-
packageStatus,
60-
commitChanges: false);
71+
await UpdatePackageInternalAsync(package, packageStatus);
6172
}
6273

6374
public async Task RevalidateAsync(Package package)
@@ -92,6 +103,34 @@ public IReadOnlyList<ValidationIssue> GetLatestPackageValidationIssues(SymbolPac
92103
return GetValidationIssues(symbolPackage.Key, symbolPackage.StatusKey, ValidatingType.SymbolPackage);
93104
}
94105

106+
public async Task StartValidationAsync(SymbolPackage symbolPackage)
107+
{
108+
var symbolPackageStatus = await _symbolPackageValidationInitiator.StartValidationAsync(symbolPackage);
109+
await UpdateSymbolPackageInternalAsync(symbolPackage, symbolPackageStatus);
110+
}
111+
112+
public async Task RevalidateAsync(SymbolPackage symbolPackage)
113+
{
114+
await _symbolPackageValidationInitiator.StartValidationAsync(symbolPackage);
115+
116+
_telemetryService.TrackSymbolPackageRevalidate(symbolPackage.Id, symbolPackage.Version);
117+
}
118+
119+
private async Task UpdatePackageInternalAsync(Package package, PackageStatus packageStatus)
120+
{
121+
await _packageService.UpdatePackageStatusAsync(
122+
package,
123+
packageStatus,
124+
commitChanges: false);
125+
}
126+
127+
private async Task UpdateSymbolPackageInternalAsync(SymbolPackage symbolPackage, PackageStatus symbolPackageStatus)
128+
{
129+
await _symbolPackageService.UpdateStatusAsync(symbolPackage,
130+
symbolPackageStatus,
131+
commitChanges: false);
132+
}
133+
95134
private IReadOnlyList<ValidationIssue> GetValidationIssues(int entityKey, PackageStatus status, ValidatingType validatingType)
96135
{
97136
IReadOnlyList<ValidationIssue> issues = new ValidationIssue[0];
@@ -118,20 +157,5 @@ private IReadOnlyList<ValidationIssue> GetValidationIssues(int entityKey, Packag
118157

119158
return issues;
120159
}
121-
122-
public async Task StartValidationAsync(SymbolPackage symbolPackage)
123-
{
124-
var symbolPackageStatus = await _symbolPackageValidationInitiator.StartValidationAsync(symbolPackage);
125-
await _symbolPackageService.UpdateStatusAsync(symbolPackage,
126-
symbolPackageStatus,
127-
commitChanges: false);
128-
}
129-
130-
public async Task RevalidateAsync(SymbolPackage symbolPackage)
131-
{
132-
await _symbolPackageValidationInitiator.StartValidationAsync(symbolPackage);
133-
134-
_telemetryService.TrackSymbolPackageRevalidate(symbolPackage.Id, symbolPackage.Version);
135-
}
136160
}
137161
}

tests/NuGetGallery.Facts/Services/PackageUploadServiceFacts.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,7 +1489,7 @@ public async Task CommitsAfterSavingSupportedPackageStatuses(PackageStatus packa
14891489
_package.PackageStatusKey = PackageStatus.FailedValidation;
14901490

14911491
_validationService
1492-
.Setup(vs => vs.StartValidationAsync(_package))
1492+
.Setup(vs => vs.UpdatePackageAsync(_package))
14931493
.Returns(Task.CompletedTask)
14941494
.Callback(() => _package.PackageStatusKey = packageStatus);
14951495

@@ -1508,7 +1508,7 @@ public async Task RejectsUnsupportedPackageStatuses(PackageStatus packageStatus)
15081508
_package.PackageStatusKey = PackageStatus.Available;
15091509

15101510
_validationService
1511-
.Setup(vs => vs.StartValidationAsync(_package))
1511+
.Setup(vs => vs.UpdatePackageAsync(_package))
15121512
.Returns(Task.CompletedTask)
15131513
.Callback(() => _package.PackageStatusKey = packageStatus);
15141514

@@ -1538,33 +1538,34 @@ public async Task StartsAsynchronousValidation(PackageStatus packageStatus)
15381538

15391539
[Theory]
15401540
[MemberData(nameof(SupportedPackageStatuses))]
1541-
public async Task StartsValidationBeforeOtherPackageOperations(PackageStatus packageStatus)
1541+
public async Task StartsValidationAfterSavingPackage(PackageStatus packageStatus)
15421542
{
15431543
_package.PackageStatusKey = packageStatus;
15441544

1545-
bool otherOperationsDone = false;
1545+
bool contextSaveDone = false;
1546+
bool packageSaved = false;
15461547
_validationService
1547-
.Setup(vs => vs.StartValidationAsync(It.IsAny<Package>()))
1548+
.Setup(vs => vs.StartValidationAsync(_package))
15481549
.Returns(Task.CompletedTask)
1549-
.Callback(() => Assert.False(otherOperationsDone));
1550+
.Callback(() => Assert.True(packageSaved && !contextSaveDone));
15501551

15511552
_entitiesContext
15521553
.Setup(ec => ec.SaveChangesAsync())
15531554
.Returns(Task.FromResult(1))
1554-
.Callback(() => otherOperationsDone = true);
1555+
.Callback(() => contextSaveDone = true);
15551556
_packageFileService
15561557
.Setup(pfs => pfs.SaveValidationPackageFileAsync(It.IsAny<Package>(), It.IsAny<Stream>()))
15571558
.Returns(Task.CompletedTask)
1558-
.Callback(() => otherOperationsDone = true);
1559+
.Callback(() => packageSaved = true);
15591560
_packageFileService
15601561
.Setup(x => x.SavePackageFileAsync(It.IsAny<Package>(), It.IsAny<Stream>()))
15611562
.Returns(Task.CompletedTask)
1562-
.Callback(() => otherOperationsDone = true);
1563+
.Callback(() => packageSaved = true);
15631564

15641565
var result = await _target.CommitPackageAsync(_package, _packageFile);
15651566

15661567
_validationService
1567-
.Verify(vs => vs.StartValidationAsync(It.IsAny<Package>()),
1568+
.Verify(vs => vs.StartValidationAsync(_package),
15681569
Times.AtLeastOnce);
15691570
_entitiesContext.Verify(
15701571
x => x.SaveChangesAsync(),

tests/NuGetGallery.Facts/Services/SymbolPackageUploadServiceFacts.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ public async Task WillThrowExceptionIfValidationDoesNotSetValidStatus(PackageSta
228228
{
229229
var validationService = new Mock<IValidationService>();
230230
validationService
231-
.Setup(x => x.StartValidationAsync(It.IsAny<SymbolPackage>()))
231+
.Setup(x => x.UpdatePackageAsync(It.IsAny<SymbolPackage>()))
232232
.Returns((SymbolPackage sp) =>
233233
{
234234
sp.StatusKey = invalidStatus;

0 commit comments

Comments
 (0)