Skip to content

Commit d048f16

Browse files
authored
Merge pull request #10742 from NuGet/dev
[ReleasePrep][2026.03.16]RI of dev into main
2 parents ee811c3 + 3fa5e0f commit d048f16

23 files changed

+389
-48
lines changed

python/StatsLogParser/loginterpretation/knownclients.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,8 @@
223223

224224
# NuGet - Keep this one at the bottom of this file as a catch-all resolver
225225
- regex: '(NuGet)/?(\d+)\.(\d+)\.?(\d+)?'
226-
family_replacement: 'NuGet'
226+
family_replacement: 'NuGet'
227+
228+
# GetNuTool
229+
- regex: '(GetNuTool)/(\d+)\.(\d+)\.?(\d+)?'
230+
family_replacement: 'GetNuTool'

python/StatsLogParser/tests/test_useragentparser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
("Bazel/release 6.0.0", "Bazel", "6", "0", "0"),
6666
("Visual Studio/6.4.0", "Visual Studio", "6", "4", "0"),
6767
("NuGetMirror/6.0.0", "NuGetMirror", "6", "0", "0"),
68-
("BaGet/1.0.0", "BaGet", "1", "0", "0")
68+
("BaGet/1.0.0", "BaGet", "1", "0", "0"),
69+
("GetNuTool/1.10.0.5939+e5f5c45", "GetNuTool", "1", "10", "0")
6970
])
7071
def test_recognizes_custom_clients(user_agent, expected_client, expected_major, expected_minor, expected_patch):
7172
parsed = UserAgentParser.parse(user_agent)

src/Catalog/Dnx/DnxCatalogCollector.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,15 @@ protected override Task<IEnumerable<CatalogCommitItemBatch>> CreateBatchesAsync(
7979
return Task.FromResult(batches);
8080
}
8181

82-
protected override Task<bool> FetchAsync(
82+
protected override async Task<bool> FetchAsync(
8383
CollectorHttpClient client,
8484
ReadWriteCursor front,
8585
ReadCursor back,
8686
CancellationToken cancellationToken)
8787
{
88-
return CatalogCommitUtilities.ProcessCatalogCommitsAsync(
88+
await DnxPackageVersionIndexCacheControl.LoadPackageIdsToIncludeAsync(_storageFactory.Create(), _logger, cancellationToken);
89+
90+
return await CatalogCommitUtilities.ProcessCatalogCommitsAsync(
8991
client,
9092
front,
9193
back,
@@ -156,7 +158,7 @@ await catalogEntries.ForEachAsync(_maxConcurrentCommitItemsWithinBatch, async ca
156158
cancellationToken);
157159
var areRequiredPropertiesPresent = await AreRequiredPropertiesPresentAsync(destinationStorage, destinationUri);
158160

159-
if (isNupkgSynchronized && isPackageInIndex && areRequiredPropertiesPresent)
161+
if (isNupkgSynchronized && areRequiredPropertiesPresent && isPackageInIndex && !DnxPackageVersionIndexCacheControl.PackageIdsToInclude.Contains(packageId))
160162
{
161163
_logger.LogInformation("No changes detected: {Id}/{Version}", packageId, normalizedPackageVersion);
162164

src/Catalog/Dnx/DnxMaker.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ public async Task UpdatePackageVersionIndexAsync(string id, Action<HashSet<NuGet
219219
// Store versions (sorted)
220220
result.Sort();
221221

222-
await storage.SaveAsync(resourceUri, CreateContent(result.Select(version => version.ToNormalizedString())), cancellationToken);
222+
await storage.SaveAsync(resourceUri, CreateContentForPackageVersionIndex(id, result.Select(version => version.ToNormalizedString())), cancellationToken);
223223
}
224224
else
225225
{
@@ -260,10 +260,10 @@ private static HashSet<NuGetVersion> GetVersions(string json)
260260
return result;
261261
}
262262

263-
private StorageContent CreateContent(IEnumerable<string> versions)
263+
private StorageContent CreateContentForPackageVersionIndex(string id, IEnumerable<string> versions)
264264
{
265265
JObject obj = new JObject { { "versions", new JArray(versions) } };
266-
return new StringStorageContent(obj.ToString(), "application/json", Constants.NoStoreCacheControl);
266+
return new StringStorageContent(obj.ToString(), "application/json", DnxPackageVersionIndexCacheControl.GetCacheControl(id, _logger));
267267
}
268268

269269
private async Task<Uri> SaveNupkgAsync(Stream nupkgStream, CatalogStorage storage, string id, string version, CancellationToken cancellationToken)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.Extensions.Logging;
8+
using Newtonsoft.Json.Linq;
9+
using NuGet.Services.Metadata.Catalog.Persistence;
10+
11+
namespace NuGet.Services.Metadata.Catalog.Dnx
12+
{
13+
public static class DnxPackageVersionIndexCacheControl
14+
{
15+
private const string DefaultCacheControlForPackageVersionIndex = "max-age=10";
16+
private const string BlobNameOfPackageIdsToInclude = "PackageIdsToIncludeForCachingPackageVersionIndex.json";
17+
18+
public static HashSet<string> PackageIdsToInclude = new HashSet<string>();
19+
20+
public static string GetCacheControl(string id, ILogger logger)
21+
{
22+
if (PackageIdsToInclude.Contains(id))
23+
{
24+
logger.LogInformation("Add caching to the package version index of Package Id: {id}.", id);
25+
26+
return DefaultCacheControlForPackageVersionIndex;
27+
}
28+
else
29+
{
30+
return Constants.NoStoreCacheControl;
31+
}
32+
}
33+
34+
public static async Task LoadPackageIdsToIncludeAsync(IStorage storage, ILogger logger, CancellationToken cancellationToken)
35+
{
36+
if (!storage.Exists(BlobNameOfPackageIdsToInclude))
37+
{
38+
logger.LogInformation("{BlobName} does not exist, at {Address}.", BlobNameOfPackageIdsToInclude, storage.BaseAddress);
39+
40+
return;
41+
}
42+
43+
logger.LogInformation("Loading the list of Package Ids from {BlobName}, at {Address}.", BlobNameOfPackageIdsToInclude, storage.BaseAddress);
44+
45+
PackageIdsToInclude = new HashSet<string>();
46+
string jsonFile = await storage.LoadStringAsync(storage.ResolveUri(BlobNameOfPackageIdsToInclude), cancellationToken);
47+
if (jsonFile != null)
48+
{
49+
JObject obj = JObject.Parse(jsonFile);
50+
JArray ids = obj["ids"] as JArray;
51+
if (ids != null)
52+
{
53+
foreach (JToken id in ids)
54+
{
55+
PackageIdsToInclude.Add(id.ToString().ToLowerInvariant());
56+
}
57+
}
58+
}
59+
60+
logger.LogInformation("Loaded the list of Package Ids (Count: {Count}) from {BlobName}, at {Address}.", PackageIdsToInclude.Count, BlobNameOfPackageIdsToInclude, storage.BaseAddress);
61+
}
62+
}
63+
}

src/Catalog/DurableCursorWithUpdates.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ public class DurableCursorWithUpdates : DurableCursor
1919

2020
private readonly int _maxNumberOfUpdatesToKeep;
2121
private readonly TimeSpan _minIntervalBetweenTwoUpdates;
22+
private readonly TimeSpan _minIntervalBeforeToReadUpdate;
2223

2324
public DurableCursorWithUpdates(Uri address, Persistence.Storage storage, DateTime defaultValue, ILogger logger,
24-
int maxNumberOfUpdatesToKeep, TimeSpan minIntervalBetweenTwoUpdates)
25+
int maxNumberOfUpdatesToKeep, TimeSpan minIntervalBetweenTwoUpdates, TimeSpan minIntervalBeforeToReadUpdate)
2526
: base(address, storage, defaultValue)
2627
{
2728
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -36,8 +37,14 @@ public DurableCursorWithUpdates(Uri address, Persistence.Storage storage, DateTi
3637
throw new ArgumentOutOfRangeException(nameof(minIntervalBetweenTwoUpdates), $"{nameof(minIntervalBetweenTwoUpdates)} must be equal or larger than 0.");
3738
}
3839

40+
if (minIntervalBeforeToReadUpdate < TimeSpan.Zero)
41+
{
42+
throw new ArgumentOutOfRangeException(nameof(minIntervalBeforeToReadUpdate), $"{nameof(minIntervalBeforeToReadUpdate)} must be equal or larger than 0.");
43+
}
44+
3945
_maxNumberOfUpdatesToKeep = maxNumberOfUpdatesToKeep;
4046
_minIntervalBetweenTwoUpdates = minIntervalBetweenTwoUpdates;
47+
_minIntervalBeforeToReadUpdate = minIntervalBeforeToReadUpdate;
4148
}
4249

4350
public override async Task SaveAsync(CancellationToken cancellationToken)
@@ -54,6 +61,7 @@ public override async Task SaveAsync(CancellationToken cancellationToken)
5461
}
5562

5663
cursorValueWithUpdates.Value = Value.ToString("O");
64+
cursorValueWithUpdates.MinIntervalBeforeToReadUpdate = _minIntervalBeforeToReadUpdate;
5765
if (storageContent != null)
5866
{
5967
cursorValueWithUpdates.Updates = GetUpdates(cursorValueWithUpdates, storageContent.StorageDateTimeInUtc);

src/Catalog/Helpers/CursorValueWithUpdates.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public class CursorValueWithUpdates
1616

1717
[JsonProperty("value")]
1818
public string Value { get; set; }
19+
20+
// This is for the cursor reader to determine which update (in the list of updates) of the cursor value to read.
21+
// The timestamp of the update to read should be at least before the current timestamp minus this interval.
22+
[JsonProperty("minIntervalBeforeToReadUpdate")]
23+
public TimeSpan MinIntervalBeforeToReadUpdate { get; set; }
24+
1925
[JsonProperty("updates")]
2026
public IList<CursorValueUpdate> Updates { get; set; } = new List<CursorValueUpdate>();
2127
}
@@ -30,6 +36,7 @@ public CursorValueUpdate(DateTime timeStamp, string value)
3036

3137
[JsonProperty("timeStamp")]
3238
public DateTime TimeStamp { get; set; }
39+
3340
[JsonProperty("value")]
3441
public string Value { get; set; }
3542
}

src/Catalog/HttpReadCursor.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -14,9 +14,9 @@ namespace NuGet.Services.Metadata.Catalog
1414
{
1515
public class HttpReadCursor : ReadCursor
1616
{
17-
private readonly Uri _address;
18-
private readonly DateTime? _defaultValue;
19-
private readonly Func<HttpMessageHandler> _handlerFunc;
17+
protected readonly Uri _address;
18+
protected readonly DateTime? _defaultValue;
19+
protected readonly Func<HttpMessageHandler> _handlerFunc;
2020

2121
public HttpReadCursor(Uri address, DateTime defaultValue, Func<HttpMessageHandler> handlerFunc = null)
2222
{
@@ -52,9 +52,9 @@ await Retry.IncrementalAsync(
5252
{
5353
response.EnsureSuccessStatusCode();
5454

55-
string json = await response.Content.ReadAsStringAsync();
55+
string valueInJson = await GetValueInJsonAsync(response);
5656

57-
JObject obj = JObject.Parse(json);
57+
JObject obj = JObject.Parse(valueInJson);
5858
Value = obj["value"].ToObject<DateTime>();
5959
}
6060
}
@@ -66,5 +66,10 @@ await Retry.IncrementalAsync(
6666
initialWaitInterval: TimeSpan.Zero,
6767
waitIncrement: TimeSpan.FromSeconds(10));
6868
}
69+
70+
public virtual async Task<string> GetValueInJsonAsync(HttpResponseMessage response)
71+
{
72+
return await response.Content.ReadAsStringAsync();
73+
}
6974
}
70-
}
75+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Linq;
6+
using System.Net.Http;
7+
using System.Threading.Tasks;
8+
using Microsoft.Extensions.Logging;
9+
using Newtonsoft.Json;
10+
11+
namespace NuGet.Services.Metadata.Catalog
12+
{
13+
public class HttpReadCursorWithUpdates : HttpReadCursor
14+
{
15+
private readonly ILogger _logger;
16+
17+
public HttpReadCursorWithUpdates(Uri address, ILogger logger, Func<HttpMessageHandler> handlerFunc = null)
18+
: base(address, handlerFunc)
19+
{
20+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
21+
}
22+
23+
public override async Task<string> GetValueInJsonAsync(HttpResponseMessage response)
24+
{
25+
var storageDateTimeOffset = response.Headers.Date;
26+
DateTime? storageDateTimeInUtc = null;
27+
if (storageDateTimeOffset.HasValue)
28+
{
29+
storageDateTimeInUtc = storageDateTimeOffset.Value.UtcDateTime;
30+
}
31+
32+
var update = await GetUpdate(response, storageDateTimeInUtc);
33+
34+
return JsonConvert.SerializeObject(new { value = update.Value });
35+
}
36+
37+
private async Task<CursorValueUpdate> GetUpdate(HttpResponseMessage response, DateTime? storageDateTimeInUtc)
38+
{
39+
if (!storageDateTimeInUtc.HasValue)
40+
{
41+
throw new ArgumentNullException(nameof(storageDateTimeInUtc));
42+
}
43+
44+
var content = await response.Content.ReadAsStringAsync();
45+
46+
var cursorValueWithUpdates = JsonConvert.DeserializeObject<CursorValueWithUpdates>(content, CursorValueWithUpdates.SerializerSettings);
47+
var minIntervalBeforeToReadUpdate = cursorValueWithUpdates.MinIntervalBeforeToReadUpdate;
48+
var updates = cursorValueWithUpdates.Updates.OrderByDescending(u => u.TimeStamp).ToList();
49+
50+
foreach (var update in updates)
51+
{
52+
if (update.TimeStamp <= storageDateTimeInUtc.Value - minIntervalBeforeToReadUpdate)
53+
{
54+
_logger.LogInformation("Read the cursor update with timeStamp: {TimeStamp} and value: {UpdateValue}, at {Address}. " +
55+
"(Storage DateTime: {StorageDateTime}, MinIntervalBeforeToReadUpdate: {MinIntervalBeforeToReadUpdate})",
56+
update.TimeStamp.ToString(CursorValueWithUpdates.SerializerSettings.DateFormatString),
57+
update.Value,
58+
_address.AbsoluteUri,
59+
storageDateTimeInUtc.Value.ToString(CursorValueWithUpdates.SerializerSettings.DateFormatString),
60+
minIntervalBeforeToReadUpdate);
61+
62+
return update;
63+
}
64+
}
65+
66+
if (updates.Count > 0)
67+
{
68+
_logger.LogWarning("Unable to find the cursor update and the oldest cursor update has timeStamp: {TimeStamp}, at {Address}. " +
69+
"(Storage DateTime: {StorageDateTime}, MinIntervalBeforeToReadUpdate: {MinIntervalBeforeToReadUpdate})",
70+
updates.Last().TimeStamp.ToString(CursorValueWithUpdates.SerializerSettings.DateFormatString),
71+
_address.AbsoluteUri,
72+
storageDateTimeInUtc.Value.ToString(CursorValueWithUpdates.SerializerSettings.DateFormatString),
73+
minIntervalBeforeToReadUpdate);
74+
}
75+
else
76+
{
77+
_logger.LogWarning("Unable to find the cursor update and the count of updates is {CursorUpdatesCount}, at {Address}.",
78+
updates.Count,
79+
_address.AbsoluteUri);
80+
}
81+
82+
throw new InvalidOperationException("Unable to find the cursor update.");
83+
}
84+
}
85+
}

src/Catalog/Persistence/IStorage.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -27,6 +27,7 @@ Task<OptimisticConcurrencyControlToken> GetOptimisticConcurrencyControlTokenAsyn
2727
Task<string> LoadStringAsync(Uri resourceUri, CancellationToken cancellationToken);
2828
Uri ResolveUri(string relativeUri);
2929
Task SaveAsync(Uri resourceUri, StorageContent content, CancellationToken cancellationToken);
30+
bool Exists(string fileName);
3031

3132
/// <summary>
3233
/// Updates the cache control header on the provided resource URI (blob). This method throws an exception if
@@ -38,4 +39,4 @@ Task<OptimisticConcurrencyControlToken> GetOptimisticConcurrencyControlTokenAsyn
3839
/// <returns>True if the Cache-Control changes, false if the Cache-Control already matched the provided value.</returns>
3940
Task<bool> UpdateCacheControlAsync(Uri resourceUri, string cacheControl, CancellationToken cancellationToken);
4041
}
41-
}
42+
}

0 commit comments

Comments
 (0)