Skip to content

Commit 1ef8bb6

Browse files
committed
allow multiple pulls at once
1 parent e94e276 commit 1ef8bb6

1 file changed

Lines changed: 18 additions & 27 deletions

File tree

src/Microsoft.ComponentDetection.Common/DockerService.cs

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,10 @@ internal class DockerService : IDockerService
2525
private static readonly DockerClient Client = new DockerClientConfiguration().CreateClient();
2626

2727
/// <summary>
28-
/// Serializes image pull operations so only one pull runs at a time,
29-
/// and tracks which images have already been pulled to avoid redundant work.
28+
/// Tracks in-flight and completed image pulls so each image is pulled at most once.
29+
/// Concurrent callers for the same image await the same task.
3030
/// </summary>
31-
private static readonly SemaphoreSlim PullSemaphore = new(1, 1);
32-
33-
private static readonly ConcurrentDictionary<string, bool> PulledImages = new();
31+
private static readonly ConcurrentDictionary<string, Task<bool>> PullCache = new();
3432

3533
private static int incrementingContainerId;
3634

@@ -105,41 +103,34 @@ private async Task<ImageInspectResponse> InspectImageAndSanitizeVarsAsync(string
105103

106104
public async Task<bool> TryPullImageAsync(string image, CancellationToken cancellationToken = default)
107105
{
108-
// Fast path: already pulled in this process
109-
if (PulledImages.ContainsKey(image))
106+
// Check if already available locally before attempting a pull
107+
if (await this.ImageExistsLocallyAsync(image, cancellationToken))
110108
{
109+
PullCache.TryAdd(image, Task.FromResult(true));
111110
return true;
112111
}
113112

114-
// Check if already available locally before acquiring the semaphore
115-
if (await this.ImageExistsLocallyAsync(image, cancellationToken))
113+
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
114+
var existingTask = PullCache.GetOrAdd(image, tcs.Task);
115+
116+
if (existingTask != tcs.Task)
116117
{
117-
PulledImages.TryAdd(image, true);
118-
return true;
118+
// Another caller is already pulling this image — await their result.
119+
return await existingTask;
119120
}
120121

121-
await PullSemaphore.WaitAsync(cancellationToken);
122+
// We own this cache entry — perform the actual pull.
122123
try
123124
{
124-
// Double-check after acquiring semaphore — another caller may have
125-
// pulled this image (or a different image that satisfied the local check)
126-
// while we were waiting.
127-
if (PulledImages.ContainsKey(image))
128-
{
129-
return true;
130-
}
131-
132125
var result = await this.PullImageCoreAsync(image, cancellationToken);
133-
if (result)
134-
{
135-
PulledImages.TryAdd(image, true);
136-
}
137-
126+
tcs.SetResult(result);
138127
return result;
139128
}
140-
finally
129+
catch (Exception ex)
141130
{
142-
PullSemaphore.Release();
131+
PullCache.TryRemove(image, out _);
132+
tcs.SetException(ex);
133+
throw;
143134
}
144135
}
145136

0 commit comments

Comments
 (0)