Skip to content

Commit 7fb3ce8

Browse files
committed
pr feedback
1 parent 60bffba commit 7fb3ce8

3 files changed

Lines changed: 88 additions & 9 deletions

File tree

src/Microsoft.ComponentDetection.Common/DockerService.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,9 @@ public async Task<bool> TryPullImageAsync(string image, CancellationToken cancel
117117

118118
if (existingTask != tcs.Task)
119119
{
120-
// Another caller is already pulling this image — await their result.
120+
// Another caller is already pulling this image — await their result
121121
this.logger.LogDebug("Image {Image} is already being pulled by another caller, waiting", image);
122-
return await existingTask;
122+
return await existingTask.WaitAsync(cancellationToken);
123123
}
124124

125125
// We own this cache entry — perform the actual pull.
@@ -128,6 +128,12 @@ public async Task<bool> TryPullImageAsync(string image, CancellationToken cancel
128128
this.logger.LogDebug("Pulling image {Image}...", image);
129129
var result = await this.PullImageCoreAsync(image, cancellationToken);
130130
this.logger.LogDebug("Pull of image {Image} completed (success={Success})", image, result);
131+
if (!result)
132+
{
133+
// Non-exception failure — remove the entry so later callers can retry.
134+
PullCache.TryRemove(image, out _);
135+
}
136+
131137
tcs.SetResult(result);
132138
return result;
133139
}

src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ internal class LinuxScanner : ILinuxScanner
3333
private static readonly SemaphoreSlim ContainerSemaphore = new SemaphoreSlim(2);
3434

3535
/// <summary>
36-
/// Caches in-flight and completed syft runs keyed by (source, scope).
36+
/// Caches in-flight and completed syft runs.
3737
/// When multiple detectors scan the same image concurrently, the second
3838
/// caller awaits the already-running task instead of launching a new container.
3939
/// </summary>
@@ -261,8 +261,7 @@ private IEnumerable<LayerMappedLinuxComponents> ProcessSyftOutputWithTelemetry(
261261

262262
/// <summary>
263263
/// Runs the Syft scanner container and returns the stdout output.
264-
/// Results are cached by (source, scope, binds) so that concurrent callers
265-
/// with identical parameters share a single container run.
264+
/// Results are cached so that callers with identical parameters share a single container run.
266265
/// </summary>
267266
private async Task<string> RunSyftAsync(
268267
string syftSource,
@@ -279,8 +278,9 @@ private async Task<string> RunSyftAsync(
279278

280279
if (existingTask != tcs.Task)
281280
{
282-
// Another caller is already running syft for this image+scope — await their result.
283-
return await existingTask;
281+
// Another caller is already running syft for this image+scope — await their result,
282+
// but allow this caller's cancellation token to abort the wait.
283+
return await existingTask.WaitAsync(cancellationToken);
284284
}
285285

286286
// We own this cache entry — run syft and propagate the result.

test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,13 +1274,13 @@ public async Task TestLinuxScanner_SyftCacheKey_BindOrderDoesNotMatterAsync()
12741274

12751275
var result1 = await scanner1.GetSyftOutputAsync(
12761276
"oci-dir:/img",
1277-
new List<string> { "/host/a:/container/a:ro", "/host/b:/container/b:ro" },
1277+
["/host/a:/container/a:ro", "/host/b:/container/b:ro"],
12781278
LinuxScannerScope.AllLayers
12791279
);
12801280

12811281
var result2 = await scanner2.GetSyftOutputAsync(
12821282
"oci-dir:/img",
1283-
new List<string> { "/host/b:/container/b:ro", "/host/a:/container/a:ro" },
1283+
["/host/b:/container/b:ro", "/host/a:/container/a:ro"],
12841284
LinuxScannerScope.AllLayers
12851285
);
12861286

@@ -1375,4 +1375,77 @@ await this.linuxScanner.ScanLinuxAsync(
13751375
Times.Exactly(2)
13761376
);
13771377
}
1378+
1379+
[TestMethod]
1380+
public async Task TestLinuxScanner_CancelledCaller_DoesNotBlockOnInFlightSyftRunAsync()
1381+
{
1382+
LinuxScanner.ResetCache();
1383+
1384+
// Use a TCS to control when the syft container "completes",
1385+
// so the first caller's run stays in-flight while we cancel the second.
1386+
var syftCompletionSource = new TaskCompletionSource<(string, string)>();
1387+
1388+
this.mockDockerService.Setup(service =>
1389+
service.CreateAndRunContainerAsync(
1390+
It.IsAny<string>(),
1391+
It.IsAny<List<string>>(),
1392+
It.IsAny<IList<string>>(),
1393+
It.IsAny<CancellationToken>()
1394+
)
1395+
)
1396+
.Returns(syftCompletionSource.Task);
1397+
1398+
var enabledTypes = new HashSet<ComponentType> { ComponentType.Linux };
1399+
1400+
var layers = new[]
1401+
{
1402+
new DockerLayer
1403+
{
1404+
LayerIndex = 0,
1405+
DiffId = "sha256:f95fc50d21d981f1efe1f04109c2c3287c271794f5d9e4fdf9888851a174a971",
1406+
},
1407+
};
1408+
1409+
var scanner1 = new LinuxScanner(
1410+
this.mockDockerService.Object,
1411+
this.mockLogger.Object,
1412+
this.componentFactories,
1413+
this.artifactFilters
1414+
);
1415+
var scanner2 = new LinuxScanner(
1416+
this.mockDockerService.Object,
1417+
this.mockLogger.Object,
1418+
this.componentFactories,
1419+
this.artifactFilters
1420+
);
1421+
1422+
// First caller starts the syft run (it will block on syftCompletionSource).
1423+
var task1 = scanner1.ScanLinuxAsync("cancel_hash", layers, 0, enabledTypes, LinuxScannerScope.AllLayers);
1424+
1425+
// Second caller with a cancellable token joins the same in-flight run.
1426+
using var cts = new CancellationTokenSource();
1427+
var task2 = scanner2.ScanLinuxAsync("cancel_hash", layers, 0, enabledTypes, LinuxScannerScope.AllLayers, cts.Token);
1428+
1429+
// Cancel the second caller while the first is still running.
1430+
await cts.CancelAsync();
1431+
1432+
// The second caller should throw OperationCanceledException promptly.
1433+
try
1434+
{
1435+
await task2;
1436+
Assert.Fail("Expected OperationCanceledException was not thrown");
1437+
}
1438+
catch (OperationCanceledException)
1439+
{
1440+
// Expected — the second caller was cancelled while waiting for the in-flight run.
1441+
}
1442+
1443+
// The first caller should still be running (not cancelled).
1444+
task1.IsCompleted.Should().BeFalse();
1445+
1446+
// Now let the first caller complete normally.
1447+
syftCompletionSource.SetResult((SyftOutputNoAuthorOrLicense, string.Empty));
1448+
var result1 = await task1;
1449+
result1.Should().NotBeEmpty();
1450+
}
13781451
}

0 commit comments

Comments
 (0)