22// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33
44using System ;
5+ using System . Diagnostics ;
6+ using System . Linq ;
57using System . Threading . Tasks ;
68using Microsoft . Extensions . Logging ;
79using NuGet . Services . Validation . Orchestrator . Telemetry ;
810using NuGetGallery ;
11+ using NuGetGallery . Packaging ;
912
1013namespace NuGet . Services . Validation . Orchestrator
1114{
1215 public class PackageStatusProcessor : IPackageStatusProcessor
1316 {
1417 private readonly ICorePackageService _galleryPackageService ;
1518 private readonly IValidationPackageFileService _packageFileService ;
19+ private readonly IValidatorProvider _validatorProvider ;
1620 private readonly ITelemetryService _telemetryService ;
1721 private readonly ILogger < PackageStatusProcessor > _logger ;
1822
1923 public PackageStatusProcessor (
2024 ICorePackageService galleryPackageService ,
2125 IValidationPackageFileService packageFileService ,
26+ IValidatorProvider validatorProvider ,
2227 ITelemetryService telemetryService ,
2328 ILogger < PackageStatusProcessor > logger )
2429 {
2530 _galleryPackageService = galleryPackageService ?? throw new ArgumentNullException ( nameof ( galleryPackageService ) ) ;
2631 _packageFileService = packageFileService ?? throw new ArgumentNullException ( nameof ( packageFileService ) ) ;
32+ _validatorProvider = validatorProvider ?? throw new ArgumentNullException ( nameof ( validatorProvider ) ) ;
2733 _telemetryService = telemetryService ?? throw new ArgumentNullException ( nameof ( telemetryService ) ) ;
2834 _logger = logger ?? throw new ArgumentNullException ( nameof ( logger ) ) ;
2935 }
@@ -127,8 +133,15 @@ private async Task MoveFileToPublicStorageAndMarkPackageAsAvailable(PackageValid
127133 package . NormalizedVersion ,
128134 validationSet . ValidationTrackingId ) ;
129135
136+ // If the validation set contains any processors, we must use the copy of the package that is specific to
137+ // this validation set. We can't use the original validation package because it does not have any of the
138+ // changes that the processors made. If the validation set package does not exist for some reason and there
139+ // are processors in the validation set, this indicates a bug and an exception will be thrown by the copy
140+ // operation below. This will cause the validation queue message to eventually dead-letter at which point
141+ // the on-call person should investigate.
130142 bool copied ;
131- if ( await _packageFileService . DoesValidationSetPackageExistAsync ( validationSet ) )
143+ if ( validationSet . PackageValidations . Any ( x => _validatorProvider . IsProcessor ( x . Type ) ) ||
144+ await _packageFileService . DoesValidationSetPackageExistAsync ( validationSet ) )
132145 {
133146 copied = await CopyAsync (
134147 validationSet ,
@@ -150,6 +163,39 @@ private async Task MoveFileToPublicStorageAndMarkPackageAsAvailable(PackageValid
150163 x => _packageFileService . CopyValidationPackageToPackageFileAsync ( x . PackageId , x . PackageNormalizedVersion ) ) ;
151164 }
152165
166+ // Use whatever package made it into the packages container. This is what customers will consume so the DB
167+ // record must match.
168+ using ( var packageStream = await _packageFileService . DownloadPackageFileToDiskAsync ( package ) )
169+ {
170+ var stopwatch = Stopwatch . StartNew ( ) ;
171+ var hash = CryptographyService . GenerateHash ( packageStream , CoreConstants . Sha512HashAlgorithmId ) ;
172+ _telemetryService . TrackDurationToHashPackage (
173+ stopwatch . Elapsed ,
174+ package . PackageRegistration . Id ,
175+ package . NormalizedVersion ,
176+ CoreConstants . Sha512HashAlgorithmId ,
177+ packageStream . GetType ( ) . FullName ) ;
178+
179+ var streamMetadata = new PackageStreamMetadata
180+ {
181+ Size = packageStream . Length ,
182+ Hash = hash ,
183+ HashAlgorithm = CoreConstants . Sha512HashAlgorithmId ,
184+ } ;
185+
186+ // We don't immediately commit here. Later, we will commit these changes as well as the new package
187+ // status as part of the same transaction.
188+ if ( streamMetadata . Size != package . PackageFileSize
189+ || streamMetadata . Hash != package . Hash
190+ || streamMetadata . HashAlgorithm != package . HashAlgorithm )
191+ {
192+ await _galleryPackageService . UpdatePackageStreamMetadataAsync (
193+ package ,
194+ streamMetadata ,
195+ commitChanges : false ) ;
196+ }
197+ }
198+
153199 _logger . LogInformation ( "Marking package {PackageId} {PackageVersion}, validation set {ValidationSetId} as {PackageStatus} in DB" ,
154200 package . PackageRegistration . Id ,
155201 package . NormalizedVersion ,
@@ -158,6 +204,7 @@ private async Task MoveFileToPublicStorageAndMarkPackageAsAvailable(PackageValid
158204
159205 try
160206 {
207+ // Make the package available and commit any other pending changes (e.g. updated hash).
161208 await UpdatePackageStatusAsync ( package , PackageStatus . Available ) ;
162209 }
163210 catch ( Exception e )
@@ -171,8 +218,9 @@ private async Task MoveFileToPublicStorageAndMarkPackageAsAvailable(PackageValid
171218 validationSet . ValidationTrackingId ) ;
172219
173220 // If this execution was not the one to copy the package, then don't delete the package on failure.
174- // This prevents the (unlikely) case where two actors attempt the DB update, one suceeds and one fails.
175- // We don't want an available package record with nothing in the packages container!
221+ // This prevents a missing passing in the (unlikely) case where two actors attempt the DB update, one
222+ // succeeds and one fails. We don't want an available package record with nothing in the packages
223+ // container!
176224 if ( copied )
177225 {
178226 await _packageFileService . DeletePackageFileAsync ( package . PackageRegistration . Id , package . Version ) ;
@@ -200,9 +248,10 @@ private async Task<bool> CopyAsync(PackageValidationSet validationSet, Package p
200248 catch ( InvalidOperationException )
201249 {
202250 // The package already exists in the packages container. This can happen if the DB commit below fails
203- // and this flow is retried. We assume that the package content has not changed. Today there is no way
204- // for the content to change. Hard deletes (the one way a package ID and version can get different
205- // content) delete from both the packages and validating container so this can't be a mismatch.
251+ // and this flow is retried or another validation set for the package completed first. Either way, we
252+ // will later attempt to use the hash from the package in the packages container (the destination).
253+ // In other words, we don't care which copy wins, but the DB record must match the package that ends
254+ // up in the packages container.
206255 _logger . LogInformation (
207256 "Package already exists in packages container for {PackageId} {PackageVersion}, validation set {ValidationSetId}" ,
208257 package . PackageRegistration . Id ,
0 commit comments