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 . Collections . Generic ;
56using System . Data . Entity ;
67using System . Data . Entity . Infrastructure ;
78using System . Linq ;
@@ -37,19 +38,17 @@ public RevalidationQueue(
3738 _logger = logger ?? throw new ArgumentNullException ( nameof ( logger ) ) ;
3839 }
3940
40- public async Task < PackageRevalidation > NextOrNullAsync ( )
41+ public async Task < IReadOnlyList < PackageRevalidation > > NextAsync ( )
4142 {
42- for ( var i = 0 ; i < _config . MaximumAttempts ; i ++ )
43+ // Find the next package to revalidate. We will skip packages if:
44+ // 1. The package has more than "MaximumPackageVersions" versions
45+ // 2. The package has already been enqueued for revalidation
46+ // 3. The package's revalidation was completed by an external factory (like manual admin revalidation)
47+ List < PackageRevalidation > next ;
48+ using ( _telemetry . TrackFindNextRevalidations ( ) )
4349 {
44- _logger . LogInformation (
45- "Attempting to find the next revalidation. Try {Attempt} of {MaxAttempts}" ,
46- i + 1 ,
47- _config . MaximumAttempts ) ;
48-
49- // Find the next package to revalidate. We will skip packages if:
50- // 1. The package has more than "MaximumPackageVersions" versions
51- // 2. The package has already been enqueued for revalidation
52- // 3. The package's revalidation was completed by an external factory (like manual admin revalidation)
50+ _logger . LogInformation ( "Finding the next packages to revalidate..." ) ;
51+
5352 IQueryable < PackageRevalidation > query = _validationContext . PackageRevalidations ;
5453
5554 if ( _config . MaximumPackageVersions . HasValue )
@@ -61,82 +60,130 @@ public async Task<PackageRevalidation> NextOrNullAsync()
6160 . Any ( g => g . Key == r . PackageId ) ) ;
6261 }
6362
64- var next = await query
63+ next = await query
6564 . Where ( r => r . Enqueued == null )
6665 . Where ( r => r . Completed == false )
6766 . OrderBy ( r => r . Key )
68- . FirstOrDefaultAsync ( ) ;
67+ . Take ( _config . MaxBatchSize )
68+ . ToListAsync ( ) ;
69+ }
6970
70- if ( next == null )
71- {
72- _logger . LogWarning ( "Could not find any incomplete revalidations" ) ;
73- return null ;
74- }
71+ _logger . LogInformation ( "Found {Revalidations} packages to revalidate" , next . Count ) ;
7572
76- // Don't revalidate packages that already have a repository signature or that no longer exist.
77- if ( await HasRepositorySignature ( next ) || await IsDeleted ( next ) )
78- {
79- await MarkAsCompleted ( next ) ;
80- await Task . Delay ( _config . SleepBetweenAttempts ) ;
73+ // Return all the revalidations that aren't already completed.
74+ return await FilterCompletedRevalidationsAsync ( next ) ;
75+ }
8176
82- continue ;
83- }
77+ private async Task < IReadOnlyList < PackageRevalidation > > FilterCompletedRevalidationsAsync ( IReadOnlyList < PackageRevalidation > revalidations )
78+ {
79+ if ( ! revalidations . Any ( ) )
80+ {
81+ return revalidations ;
82+ }
8483
85- _logger . LogInformation (
86- "Found revalidation for {PackageId} {PackageNormalizedVersion} after {Attempt} attempts" ,
87- next . PackageId ,
88- next . PackageNormalizedVersion ,
89- i + 1 ) ;
84+ var completed = new List < PackageRevalidation > ( ) ;
85+ var uncompleted = revalidations . ToDictionary (
86+ r => $ "{ r . PackageId } /{ r . PackageNormalizedVersion } ",
87+ r => r ) ;
9088
91- return next ;
92- }
89+ // Packages that already have a repository signature do not need to be revalidated.
90+ _logger . LogInformation ( "Finding revalidations that can be skipped because their packages are already repository signed..." ) ;
91+
92+ var hasRepositorySignatures = await _validationContext . PackageSigningStates
93+ . Select ( s => new {
94+ IdAndVersion = s . PackageId + "/" + s . PackageNormalizedVersion ,
95+ s . PackageSignatures
96+ } )
97+ . Where ( s => uncompleted . Keys . Contains ( s . IdAndVersion ) )
98+ . Where ( s => s . PackageSignatures . Any ( sig => sig . Type == PackageSignatureType . Repository ) )
99+ . Select ( s => s . IdAndVersion )
100+ . ToListAsync ( ) ;
93101
94102 _logger . LogInformation (
95- "Did not find any revalidations after {MaxAttempts}. Retry later... " ,
96- _config . MaximumAttempts ) ;
103+ "Found {RevalidationCount} revalidations that can be skipped because their packages are already repository signed " ,
104+ hasRepositorySignatures . Count ) ;
97105
98- return null ;
99- }
106+ foreach ( var idAndVersion in hasRepositorySignatures )
107+ {
108+ completed . Add ( uncompleted [ idAndVersion ] ) ;
109+ uncompleted . Remove ( idAndVersion ) ;
110+ }
100111
101- private Task < bool > HasRepositorySignature ( PackageRevalidation revalidation )
102- {
103- return _validationContext . PackageSigningStates
104- . Where ( s => s . PackageId == revalidation . PackageId )
105- . Where ( s => s . PackageNormalizedVersion == revalidation . PackageNormalizedVersion )
106- . Where ( s => s . PackageSignatures . Any ( sig => sig . Type == PackageSignatureType . Repository ) )
107- . AnyAsync ( ) ;
108- }
112+ // Packages that are no longer available should not be revalidated.
113+ _logger . LogInformation ( "Finding revalidations' package statuses..." ) ;
109114
110- private async Task < bool > IsDeleted ( PackageRevalidation revalidation )
111- {
112- var packageStatus = await _galleryContext . Set < Package > ( )
113- . Where ( p => p . PackageRegistration . Id == revalidation . PackageId )
114- . Where ( p => p . NormalizedVersion == revalidation . PackageNormalizedVersion )
115- . Select ( p => ( PackageStatus ? ) p . PackageStatusKey )
116- . FirstOrDefaultAsync ( ) ;
115+ var packageStatuses = await _galleryContext . Set < Package > ( )
116+ . Select ( p => new
117+ {
118+ Identity = p . PackageRegistration . Id + "/" + p . NormalizedVersion ,
119+ p . PackageStatusKey
120+ } )
121+ . Where ( p => uncompleted . Keys . Contains ( p . Identity ) )
122+ . ToDictionaryAsync (
123+ p => p . Identity ,
124+ p => p . PackageStatusKey ) ;
117125
118- return ( packageStatus == null || packageStatus == PackageStatus . Deleted ) ;
119- }
126+ _logger . LogInformation ( "Found {PackageStatusCount} revalidations' package statuses" , packageStatuses . Count ) ;
127+
128+ foreach ( var key in uncompleted . Keys . ToList ( ) )
129+ {
130+ // Packages that are hard deleted won't have a status.
131+ if ( ! packageStatuses . TryGetValue ( key , out var status ) || status == PackageStatus . Deleted )
132+ {
133+ completed . Add ( uncompleted [ key ] ) ;
134+ uncompleted . Remove ( key ) ;
135+ continue ;
136+ }
137+ }
120138
121- private async Task MarkAsCompleted ( PackageRevalidation revalidation )
122- {
123139 _logger . LogInformation (
124- "Marking package revalidation as completed as it has a repository signature or is deleted for {PackageId} {PackageNormalizedVersion}" ,
125- revalidation . PackageId ,
126- revalidation . PackageNormalizedVersion ) ;
140+ "Found {CompletedRevalidations} revalidations that can be skipped. There are {UncompletedRevalidations} " +
141+ "revalidations remaining in this batch" ,
142+ completed . Count ,
143+ uncompleted . Count ) ;
127144
145+ // Update revalidations that were determined to be completed and return the remaining revalidations.
146+ if ( completed . Any ( ) )
147+ {
148+ await MarkRevalidationsAsCompletedAsync ( completed ) ;
149+ }
150+
151+ return uncompleted . Values . ToList ( ) ;
152+ }
153+
154+ private async Task MarkRevalidationsAsCompletedAsync ( IReadOnlyList < PackageRevalidation > revalidations )
155+ {
128156 try
129157 {
130- revalidation . Completed = true ;
158+ foreach ( var revalidation in revalidations )
159+ {
160+ _logger . LogInformation (
161+ "Marking package {PackageId} {PackageNormalizedVersion} revalidation as completed as the package is unavailable or the package is already repository signed..." ,
162+ revalidation . PackageId ,
163+ revalidation . PackageNormalizedVersion ) ;
164+
165+ revalidation . Completed = true ;
166+ }
131167
132168 await _validationContext . SaveChangesAsync ( ) ;
133169
134- _telemetry . TrackPackageRevalidationMarkedAsCompleted ( revalidation . PackageId , revalidation . PackageNormalizedVersion ) ;
170+ foreach ( var revalidation in revalidations )
171+ {
172+ _logger . LogInformation (
173+ "Marked package {PackageId} {PackageNormalizedVersion} revalidation as completed" ,
174+ revalidation . PackageId ,
175+ revalidation . PackageNormalizedVersion ) ;
176+
177+ _telemetry . TrackPackageRevalidationMarkedAsCompleted ( revalidation . PackageId , revalidation . PackageNormalizedVersion ) ;
178+ }
135179 }
136- catch ( DbUpdateConcurrencyException )
180+ catch ( DbUpdateConcurrencyException e )
137181 {
138- // Swallow concurrency exceptions. The package will be marked as completed
139- // on the next iteration of "NextOrNullAsync".
182+ _logger . LogError (
183+ 0 ,
184+ e ,
185+ "Failed to mark package revalidations as completed. " +
186+ $ "These revalidations will be marked as completed on the next iteration of { nameof ( NextAsync ) } ...") ;
140187 }
141188 }
142189 }
0 commit comments