Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ba2d230
Implement unimplemented concurrency methods for all server classes
alerickson Jun 30, 2026
fbb940f
build fixes
alerickson Jun 30, 2026
a4730af
Fix thread-safety in ContainerRegistryServerAPICalls.InstallPackageAsync
Copilot Jul 1, 2026
f800970
Fix copilot commit for container registry
alerickson Jul 1, 2026
6ab7367
Avoid cmdlet stream writes in NuGet FindVersionAsync path
Copilot Jul 1, 2026
dbd1689
Fix V3 async helper logging to use debug queues
Copilot Jul 1, 2026
efb000d
Potential fix for pull request finding
alerickson Jul 1, 2026
0e9c684
Fix specific-version async dependency error handling path
Copilot Jul 1, 2026
e3c1af5
Scope async dependency queue handling to current operation
Copilot Jul 1, 2026
9b92e77
Fix version-range async path: reset errRecord and flush concurrent qu…
Copilot Jul 1, 2026
900db22
build fixes
alerickson Jul 1, 2026
4cdbea8
Incorporate code review changes - complete TODOs and remove unneeded …
alerickson Jul 21, 2026
40138f1
Add concurrency for parent packages
alerickson Jul 24, 2026
8c5e184
Merge branch 'master' of https://github.com/powershell/PSResourceGet …
alerickson Jul 31, 2026
06e8a86
Use ConcurrentQueue for commented out debug msg
alerickson Jul 31, 2026
90d11f7
Bug fix for filtering on version
alerickson Jul 31, 2026
4055330
Update formula for pagination count so it does not over-fetch
alerickson Jul 31, 2026
205b4cb
Use InstallPackageAsync for parent install parallelization
alerickson Jul 31, 2026
169070f
add concurrentqueues to appropriate places
alerickson Aug 3, 2026
2ab3a0e
InstallPackageAsync now uses queue-based InstallVersionAsync/HttpRequ…
alerickson Aug 3, 2026
a993a9f
uses concurrentQueue.IsEmpty instead of .Count()
alerickson Aug 3, 2026
a689796
implement findNameAsync in NuGetServerApi
alerickson Aug 3, 2026
6b26aae
Create async methods in ContainerRegistryServer class
alerickson Aug 3, 2026
b4dbcc4
Implement Async methods in NuGetServer class
alerickson Aug 3, 2026
a5d9222
Merge branch 'parentPkgConcurrency' of https://github.com/powershell/…
alerickson Aug 3, 2026
6eff027
pass in debug concurrency queue to container registery methods
alerickson Aug 5, 2026
4da4140
Add comments and test
alerickson Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 136 additions & 80 deletions src/code/ContainerRegistryServerAPICalls.cs

Large diffs are not rendered by default.

300 changes: 273 additions & 27 deletions src/code/FindHelper.cs

Large diffs are not rendered by default.

355 changes: 183 additions & 172 deletions src/code/InstallHelper.cs

Large diffs are not rendered by default.

177 changes: 161 additions & 16 deletions src/code/NuGetServerAPICalls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,9 @@ public override Task<FindResults> FindVersionAsync(string packageName, string ve
});
var filterBuilder = queryBuilder.FilterBuilder;

// We need to explicitly add 'Id eq <packageName>' whenever $filter is used, otherwise arbitrary results are returned.
filterBuilder.AddCriterion($"Id eq '{packageName}'");
filterBuilder.AddCriterion($"NormalizedVersion eq '{packageName}'");

// We need to explicitly add 'Id eq <packageName>' whenever $filter is used, otherwise arbitrary results are returned.
filterBuilder.AddCriterion($"Id eq '{packageName}'");
filterBuilder.AddCriterion($"NormalizedVersion eq '{version}'");
var requestUrl = $"{Repository.Uri}/FindPackagesById()?{queryBuilder.BuildQueryString()}";
string response = HttpRequestCallAsync(requestUrl, debugMsgs, out ErrorRecord errRecord);
FindResults findResponse = new FindResults(stringResponse: new string[] { response }, hashtableResponse: emptyHashResponses, responseType: FindResponseType);
Expand All @@ -87,13 +86,46 @@ public override Task<FindResults> FindVersionAsync(string packageName, string ve
public override Task<FindResults> FindVersionGlobbingAsync(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, ConcurrentQueue<ErrorRecord> errorMsgs, ConcurrentQueue<string> warningMsgs, ConcurrentQueue<string> debugMsgs, ConcurrentQueue<string> verboseMsgs)
{
debugMsgs.Enqueue("In NuGetServerAPICalls::FindVersionGlobbingAsync()");
FindResults findResponse = FindVersionGlobbing(packageName, versionRange, includePrerelease, type, getOnlyLatest, out ErrorRecord errRecord);
List<string> responses = new List<string>();
int skip = 0;

var initialResponse = FindVersionGlobbingFromEndpointAsync(packageName, versionRange, includePrerelease, skip, getOnlyLatest, debugMsgs, out ErrorRecord errRecord);
if (errRecord != null)
{
errorMsgs.Enqueue(errRecord);
return Task.FromResult(new FindResults(stringResponse: responses.ToArray(), hashtableResponse: emptyHashResponses, responseType: FindResponseType));
}

return Task.FromResult(findResponse);
responses.Add(initialResponse);

if (!getOnlyLatest)
{
int initialCount = GetCountFromResponse(initialResponse, out errRecord);
if (errRecord != null)
{
errorMsgs.Enqueue(errRecord);
return Task.FromResult(new FindResults(stringResponse: responses.ToArray(), hashtableResponse: emptyHashResponses, responseType: FindResponseType));
}

int count = (int)Math.Ceiling((double)initialCount / 100) - 1;

while (count > 0)
{
// skip 100
skip += 100;
var tmpResponse = FindVersionGlobbingFromEndpointAsync(packageName, versionRange, includePrerelease, skip, getOnlyLatest, debugMsgs, out errRecord);
if (errRecord != null)
{
errorMsgs.Enqueue(errRecord);
return Task.FromResult(new FindResults(stringResponse: responses.ToArray(), hashtableResponse: emptyHashResponses, responseType: FindResponseType));
}

responses.Add(tmpResponse);
count--;
}
}

return Task.FromResult(new FindResults(stringResponse: responses.ToArray(), hashtableResponse: emptyHashResponses, responseType: FindResponseType));
}
/// <summary>
/// Find method which allows for searching for all packages from a repository and returns latest version for each.
Expand Down Expand Up @@ -122,7 +154,7 @@ public override FindResults FindAll(bool includePrerelease, ResourceType type, o
return new FindResults(stringResponse: responses.ToArray(), hashtableResponse: emptyHashResponses, responseType: FindResponseType);
}

int count = initialCount / 6000;
int count = (int)Math.Ceiling((double)initialCount / 6000) - 1;
// if more than 100 count, loop and add response to list
while (count > 0)
{
Expand Down Expand Up @@ -166,7 +198,7 @@ public override FindResults FindTags(string[] tags, bool includePrerelease, Reso
return new FindResults(stringResponse: responses.ToArray(), hashtableResponse: emptyHashResponses, responseType: FindResponseType);
}

int count = initialCount / 100;
int count = (int)Math.Ceiling((double)initialCount / 100) - 1;
// if more than 100 count, loop and add response to list
while (count > 0)
{
Expand Down Expand Up @@ -238,7 +270,19 @@ public override FindResults FindName(string packageName, bool includePrerelease,
public override Task<FindResults> FindNameAsync(string packageName, bool includePrerelease, ResourceType type, ConcurrentQueue<ErrorRecord> errorMsgs, ConcurrentQueue<string> warningMsgs, ConcurrentQueue<string> debugMsgs, ConcurrentQueue<string> verboseMsgs)
{
debugMsgs.Enqueue("In NuGetServerAPICalls::FindNameAsync()");
FindResults findResponse = FindName(packageName, includePrerelease, type, out ErrorRecord errRecord);
var queryBuilder = new NuGetV2QueryBuilder(new Dictionary<string, string>{
{ "id", $"'{packageName}'" },
});
var filterBuilder = queryBuilder.FilterBuilder;

filterBuilder.AddCriterion(includePrerelease ? "IsAbsoluteLatestVersion" : "IsLatestVersion");

// We need to explicitly add 'Id eq <packageName>' whenever $filter is used, otherwise arbitrary results are returned.
filterBuilder.AddCriterion($"Id eq '{packageName}'");

var requestUrl = $"{Repository.Uri}/FindPackagesById()?{queryBuilder.BuildQueryString()}";
string response = HttpRequestCallAsync(requestUrl, debugMsgs, out ErrorRecord errRecord);
FindResults findResponse = new FindResults(stringResponse: new string[] { response }, hashtableResponse: emptyHashResponses, responseType: FindResponseType);
if (errRecord != null)
{
errorMsgs.Enqueue(errRecord);
Expand Down Expand Up @@ -310,7 +354,7 @@ public override FindResults FindNameGlobbing(string packageName, bool includePre
return new FindResults(stringResponse: responses.ToArray(), hashtableResponse: emptyHashResponses, responseType: FindResponseType);
}

int count = initialCount / 100;
int count = (int)Math.Ceiling((double)initialCount / 100) - 1;
// if more than 100 count, loop and add response to list
while (count > 0)
{
Expand Down Expand Up @@ -355,7 +399,7 @@ public override FindResults FindNameGlobbingWithTag(string packageName, string[]
return new FindResults(stringResponse: responses.ToArray(), hashtableResponse: emptyHashResponses, responseType: FindResponseType);
}

int count = initialCount / 100;
int count = (int)Math.Ceiling((double)initialCount / 100) - 1;
// if more than 100 count, loop and add response to list
while (count > 0)
{
Expand Down Expand Up @@ -404,7 +448,7 @@ public override FindResults FindVersionGlobbing(string packageName, VersionRange
return new FindResults(stringResponse: responses.ToArray(), hashtableResponse: emptyHashResponses, responseType: FindResponseType);
}

int count = initialCount / 100;
int count = (int)Math.Ceiling((double)initialCount / 100) - 1;

while (count > 0)
{
Expand Down Expand Up @@ -521,7 +565,19 @@ public override Stream InstallPackage(string packageName, string packageVersion,
public override Task<Stream> InstallPackageAsync(string packageName, string packageVersion, bool includePrerelease, ConcurrentQueue<ErrorRecord> errorMsgs, ConcurrentQueue<string> warningMsgs, ConcurrentQueue<string> debugMsgs, ConcurrentQueue<string> verboseMsgs)
{
debugMsgs.Enqueue("In NuGetServerAPICalls::InstallPackageAsync()");
Stream results = InstallPackage(packageName, packageVersion, includePrerelease, out ErrorRecord errRecord);
Stream results = new MemoryStream();
if (string.IsNullOrEmpty(packageVersion))
{
errorMsgs.Enqueue(new ErrorRecord(
exception: new ArgumentNullException($"Package version could not be found for {packageName}"),
"PackageVersionNullOrEmptyError",
ErrorCategory.InvalidArgument,
_cmdletPassedIn));

return Task.FromResult(results);
}

results = InstallVersionAsync(packageName, packageVersion, debugMsgs, out ErrorRecord errRecord);
if (errRecord != null)
{
errorMsgs.Enqueue(errRecord);
Expand Down Expand Up @@ -628,6 +684,55 @@ private HttpContent HttpRequestCallForContent(string requestUrl, out ErrorRecord
return content;
}

/// <summary>
/// Helper method that makes the HTTP request for install APIs on worker threads; enqueues diagnostics instead of writing to cmdlet streams.
/// </summary>
private HttpContent HttpRequestCallForContentAsync(string requestUrl, ConcurrentQueue<string> debugMsgs, out ErrorRecord errRecord)
{
debugMsgs.Enqueue("In NuGetServerAPICalls::HttpRequestCallForContentAsync()");
errRecord = null;
HttpContent content = null;

try
{
debugMsgs.Enqueue($"Request url is: '{requestUrl}'");
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);

content = SendRequestForContentAsync(request, _sessionClient).GetAwaiter().GetResult();
}
catch (HttpRequestException e)
{
errRecord = new ErrorRecord(
exception: e,
"HttpRequestFailure",
ErrorCategory.ConnectionError ,
this);
}
catch (ArgumentNullException e)
{
errRecord = new ErrorRecord(
exception: e,
"HttpRequestFailure",
ErrorCategory.InvalidData,
this);
}
catch (InvalidOperationException e)
{
errRecord = new ErrorRecord(
exception: e,
"HttpRequestFailure",
ErrorCategory.InvalidOperation,
this);
}

if (string.IsNullOrEmpty(content?.ToString()))
{
debugMsgs.Enqueue("Response is empty");
}

return content;
}

/// <summary>
/// Helper method that makes the HTTP request for the NuGet server protocol url passed in for async find APIs.
/// This helper writes diagnostics to the provided debug queue and avoids cmdlet stream writes.
Expand Down Expand Up @@ -912,6 +1017,25 @@ private string FindNameGlobbingWithTag(string packageName, string[] tags, bool i
private string FindVersionGlobbing(string packageName, VersionRange versionRange, bool includePrerelease, int skip, bool getOnlyLatest, out ErrorRecord errRecord)
{
_cmdletPassedIn.WriteDebug("In NuGetServerAPICalls::FindVersionGlobbing()");
var requestUrl = GetVersionGlobbingRequestUrl(packageName, versionRange, includePrerelease, skip, getOnlyLatest);
return HttpRequestCall(requestUrl, out errRecord);
}

/// <summary>
/// Worker-thread counterpart of FindVersionGlobbing(); enqueues diagnostics instead of writing to cmdlet streams.
/// </summary>
private string FindVersionGlobbingFromEndpointAsync(string packageName, VersionRange versionRange, bool includePrerelease, int skip, bool getOnlyLatest, ConcurrentQueue<string> debugMsgs, out ErrorRecord errRecord)
{
debugMsgs.Enqueue("In NuGetServerAPICalls::FindVersionGlobbingFromEndpointAsync()");
var requestUrl = GetVersionGlobbingRequestUrl(packageName, versionRange, includePrerelease, skip, getOnlyLatest);
return HttpRequestCallAsync(requestUrl, debugMsgs, out errRecord);
}

/// <summary>
/// Builds the FindPackagesById() request url for version-globbing searches.
/// </summary>
private string GetVersionGlobbingRequestUrl(string packageName, VersionRange versionRange, bool includePrerelease, int skip, bool getOnlyLatest)
{
//https://www.powershellgallery.com/api/v2//FindPackagesById()?id='blah'&includePrerelease=false&$filter= NormalizedVersion gt '1.0.0' and NormalizedVersion lt '2.2.5' and substringof('PSModule', Tags) eq true
//https://www.powershellgallery.com/api/v2//FindPackagesById()?id='PowerShellGet'&includePrerelease=false&$filter= NormalizedVersion gt '1.1.1' and NormalizedVersion lt '2.2.5'
// NormalizedVersion doesn't include trailing zeroes
Expand Down Expand Up @@ -980,9 +1104,7 @@ private string FindVersionGlobbing(string packageName, VersionRange versionRange
// We need to explicitly add 'Id eq <packageName>' whenever $filter is used, otherwise arbitrary results are returned.
filterBuilder.AddCriterion($"Id eq '{packageName}'");

var requestUrl = $"{Repository.Uri}/FindPackagesById()?{queryBuilder.BuildQueryString()}";

return HttpRequestCall(requestUrl, out errRecord);
return $"{Repository.Uri}/FindPackagesById()?{queryBuilder.BuildQueryString()}";
}

/// <summary>
Expand Down Expand Up @@ -1040,6 +1162,29 @@ private Stream InstallVersion(string packageName, string version, out ErrorRecor
return response.ReadAsStreamAsync().Result;
}

/// <summary>
/// Worker-thread counterpart of InstallVersion(); enqueues diagnostics instead of writing to cmdlet streams.
/// </summary>
private Stream InstallVersionAsync(string packageName, string version, ConcurrentQueue<string> debugMsgs, out ErrorRecord errRecord)
{
debugMsgs.Enqueue("In NuGetServerAPICalls::InstallVersionAsync()");
var requestUrl = $"{Repository.Uri}/Packages(Id='{packageName}',Version='{version}')/Download";
var response = HttpRequestCallForContentAsync(requestUrl, debugMsgs, out errRecord);

if (response is null)
{
errRecord = new ErrorRecord(
new Exception($"No content was returned by repository '{Repository.Name}'"),
"InstallFailureContentNullNuGetServer",
ErrorCategory.InvalidResult,
this);

return null;
}

return response.ReadAsStreamAsync().Result;
}

/// <summary>
/// Helper method that makes gets 'count' property from http response string.
/// The count property is used to determine the number of total results found (for pagination).
Expand Down
14 changes: 7 additions & 7 deletions src/code/V2ServerAPICalls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ public override FindResults FindTags(string[] tags, bool includePrerelease, Reso
if (initialScriptCount != 0)
{
responses.Add(initialScriptResponse);
int count = initialScriptCount / 100;
int count = (int)Math.Ceiling((double)initialScriptCount / 100) - 1;
// if more than 100 count, loop and add response to list
while (count > 0)
{
Expand Down Expand Up @@ -242,7 +242,7 @@ public override FindResults FindTags(string[] tags, bool includePrerelease, Reso
if (initialModuleCount != 0)
{
responses.Add(initialModuleResponse);
int count = initialModuleCount / 100;
int count = (int)Math.Ceiling((double)initialModuleCount / 100) - 1;
// if more than 100 count, loop and add response to list
while (count > 0)
{
Expand Down Expand Up @@ -296,7 +296,7 @@ public override FindResults FindCommandOrDscResource(string[] tags, bool include
if (initialCount != 0)
{
responses.Add(initialResponse);
int count = (int)Math.Ceiling((double)(initialCount / 100));
int count = (int)Math.Ceiling((double)initialCount / 100) - 1;

while (count > 0)
{
Expand Down Expand Up @@ -596,7 +596,7 @@ public override FindResults FindNameGlobbing(string packageName, bool includePre
return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v2FindResponseType);
}

int count = (int)Math.Ceiling((double)(initialCount / 100));
int count = (int)Math.Ceiling((double)initialCount / 100) - 1;
// if more than 100 count, loop and add response to list
while (count > 0)
{
Expand Down Expand Up @@ -648,7 +648,7 @@ public override FindResults FindNameGlobbingWithTag(string packageName, string[]
return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v2FindResponseType);
}

int count = (int)Math.Ceiling((double)(initialCount / 100));
int count = (int)Math.Ceiling((double)initialCount / 100) - 1;
// if more than 100 count, loop and add response to list
while (count > 0)
{
Expand Down Expand Up @@ -704,7 +704,7 @@ public override FindResults FindVersionGlobbing(string packageName, VersionRange

if (!getOnlyLatest)
{
int count = (int)Math.Ceiling((double)(initialCount / 100));
int count = (int)Math.Ceiling((double)initialCount / 100) - 1;

while (count > 0)
{
Expand Down Expand Up @@ -1735,7 +1735,7 @@ public override async Task<FindResults> FindVersionGlobbingAsync(string packageN

if (!getOnlyLatest)
{
int count = (int)Math.Ceiling((double)(initialCount / 100));
int count = (int)Math.Ceiling((double)initialCount / 100) - 1;

while (count > 0)
{
Expand Down
6 changes: 3 additions & 3 deletions src/code/V3ServerAPICalls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ private FindResults FindVersionHelper(string packageName, string version, string

return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType);
}
//_cmdletPassedIn.WriteDebug($"'{packageName}' version parsed as '{requiredVersion}'");
debugMsgs.Enqueue($"'{packageName}' version parsed as '{requiredVersion}'");

string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, catalogEntryProperty, isSearch: true, out errRecord, errorMsgs, debugMsgs, verboseMsgs);
if (errRecord != null)
Expand Down Expand Up @@ -1097,8 +1097,8 @@ private List<JsonElement> GetVersionedPackageEntriesFromSearchQueryResource(stri
// Get responses for all packages that contain the required tags
pkgEntries.AddRange(GetJsonElementArr(query, dataName, out int initialCount, out errRecord, errorMsgs, debugMsgs, verboseMsgs).ToList());

// check count (ie "totalHits") 425 ==> count/100 ~~> 4 calls ~~> + 1 = 5 calls
int count = initialCount / 100 + 1;
// check count (ie "totalHits") 425 ==> ceil(425/100) - 1 ~~> 4 more calls (initial page already fetched)
int count = (int)Math.Ceiling((double)initialCount / 100) - 1;
// if more than 100 count, loop and add response to list
while (count > 0)
{
Expand Down
Loading
Loading