Skip to content

Commit 19f442e

Browse files
committed
Adding some retry logic to the lifecycle hook
The hook is run before the container image is downloaded (and ollama started) when you run from a completely clean session (like GitHub Actions), and this poses a problem because it would timeout trying to check if the model exists. Added a really simple bit of retry logic in to counter that
1 parent a5028b3 commit 19f442e

File tree

1 file changed

+55
-9
lines changed

1 file changed

+55
-9
lines changed

src/CommunityToolkit.Aspire.Hosting.Ollama/OllamaResourceLifecycleHook.cs

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
1-
using Aspire.Hosting.ApplicationModel;
1+
using Aspire.Hosting;
2+
using Aspire.Hosting.ApplicationModel;
23
using Aspire.Hosting.Lifecycle;
4+
using Microsoft.Extensions.Logging;
35
using OllamaSharp;
46
using OllamaSharp.Models;
7+
using System.Globalization;
58

69
namespace CommunityToolkit.Aspire.Hosting.Ollama;
7-
internal class OllamaResourceLifecycleHook(ResourceNotificationService notificationService) : IDistributedApplicationLifecycleHook, IAsyncDisposable
10+
internal class OllamaResourceLifecycleHook(
11+
ResourceLoggerService loggerService,
12+
ResourceNotificationService notificationService,
13+
DistributedApplicationExecutionContext context) : IDistributedApplicationLifecycleHook, IAsyncDisposable
814
{
915
private readonly ResourceNotificationService _notificationService = notificationService;
1016

1117
private readonly CancellationTokenSource _tokenSource = new();
1218

1319
public Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
1420
{
21+
if (context.IsPublishMode)
22+
{
23+
return Task.CompletedTask;
24+
}
25+
1526
foreach (var resource in appModel.Resources.OfType<OllamaResource>())
1627
{
1728
DownloadModel(resource, _tokenSource.Token);
@@ -27,11 +38,13 @@ private void DownloadModel(OllamaResource resource, CancellationToken cancellati
2738
return;
2839
}
2940

41+
var logger = loggerService.GetLogger(resource);
42+
3043
_ = Task.Run(async () =>
3144
{
3245
try
3346
{
34-
var connectionString = await resource.ConnectionStringExpression.GetValueAsync(cancellationToken);
47+
var connectionString = await resource.ConnectionStringExpression.GetValueAsync(cancellationToken).ConfigureAwait(false);
3548

3649
if (string.IsNullOrWhiteSpace(connectionString))
3750
{
@@ -47,7 +60,18 @@ private void DownloadModel(OllamaResource resource, CancellationToken cancellati
4760

4861
if (!hasModel)
4962
{
50-
await PullModel(resource, ollamaClient, model, cancellationToken);
63+
logger.LogInformation("{TimeStamp}: [{Model}] needs to be downloaded for {ResourceName}",
64+
DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture),
65+
resource.ModelName,
66+
resource.Name);
67+
await PullModel(resource, ollamaClient, model, logger, cancellationToken);
68+
}
69+
else
70+
{
71+
logger.LogInformation("{TimeStamp}: [{Model}] already exists for {ResourceName}",
72+
DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture),
73+
resource.ModelName,
74+
resource.Name);
5175
}
5276

5377
await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot("Running", KnownResourceStateStyles.Success) });
@@ -57,24 +81,42 @@ private void DownloadModel(OllamaResource resource, CancellationToken cancellati
5781
await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot(ex.Message, KnownResourceStateStyles.Error) });
5882
}
5983

60-
}, cancellationToken);
84+
}, cancellationToken).ConfigureAwait(false);
6185
}
6286

6387
private static async Task<bool> HasModelAsync(OllamaApiClient ollamaClient, string model, CancellationToken cancellationToken)
6488
{
65-
var localModels = await ollamaClient.ListLocalModels(cancellationToken);
66-
return localModels.Any(m => m.Name.StartsWith(model));
89+
int retryCount = 0;
90+
while (retryCount < 5)
91+
{
92+
try
93+
{
94+
var localModels = await ollamaClient.ListLocalModels(cancellationToken);
95+
return localModels.Any(m => m.Name.StartsWith(model));
96+
}
97+
catch (TaskCanceledException)
98+
{
99+
// wait 30 seconds before retrying
100+
await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
101+
retryCount++;
102+
}
103+
}
104+
105+
throw new TimeoutException("Failed to list local models after 5 retries. Likely that the container image was not pulled in time, or the container is not running.");
67106
}
68107

69-
private async Task PullModel(OllamaResource resource, OllamaApiClient ollamaClient, string model, CancellationToken cancellationToken)
108+
private async Task PullModel(OllamaResource resource, OllamaApiClient ollamaClient, string model, ILogger logger, CancellationToken cancellationToken)
70109
{
110+
logger.LogInformation("{TimeStamp}: Pulling ollama model {Model}...",
111+
DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture),
112+
model);
71113
await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot("Downloading model", KnownResourceStateStyles.Info) });
72114

73115
long percentage = 0;
74116

75117
await foreach (PullModelResponse? status in ollamaClient.PullModel(model, cancellationToken))
76118
{
77-
if (status == null)
119+
if (status is null)
78120
{
79121
continue;
80122
}
@@ -95,6 +137,10 @@ await _notificationService.PublishUpdateAsync(resource,
95137
}
96138
}
97139
}
140+
141+
logger.LogInformation("{TimeStamp}: Finished pulling ollama model {Model}",
142+
DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture),
143+
model);
98144
}
99145

100146
public ValueTask DisposeAsync()

0 commit comments

Comments
 (0)