From ecaa504640149fa0b948e23899d07f89dad173b7 Mon Sep 17 00:00:00 2001 From: Muhannad Al-Khatib Date: Tue, 25 Aug 2026 14:04:37 +0300 Subject: [PATCH] fix: reuse S3 client and bound its timeout/retries ICloudFilesService was registered AddTransient, so CloudFilesService's constructor built a brand-new AmazonS3Client (fresh TCP+TLS handshake, no connection pooling) on every DI resolution, with no explicit Timeout/MaxErrorRetry so it fell back to the AWS SDK defaults (~100s timeout, several backoff retries). Under any latency to the storage endpoint this made calls hang for a long time before failing. AmazonS3Client is documented as thread-safe and meant to be reused, so register it as a singleton. Timeout/MaxErrorRetry are now configurable via S3CloudFilesOptions.TimeoutSeconds/MaxErrorRetry (bound from the CloudFiles config section, or set in the AddS3CloudFiles(options => ...) delegate), defaulting to 15s/2 retries so a genuine outage fails fast instead of retrying for minutes. Co-Authored-By: Claude Sonnet 5 --- SW.CloudFiles.S3.Extensions/IServiceCollectionExtensions.cs | 2 +- SW.CloudFiles.S3/Extensions/CloudFilesOptionsExtensions.cs | 6 ++++-- SW.CloudFiles.S3/S3CloudFilesOptions.cs | 6 ++++++ SW.CloudFiles.S3/Services/CloudFilesService.cs | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/SW.CloudFiles.S3.Extensions/IServiceCollectionExtensions.cs b/SW.CloudFiles.S3.Extensions/IServiceCollectionExtensions.cs index 4e7e089..d4b210c 100644 --- a/SW.CloudFiles.S3.Extensions/IServiceCollectionExtensions.cs +++ b/SW.CloudFiles.S3.Extensions/IServiceCollectionExtensions.cs @@ -130,7 +130,7 @@ public static IServiceCollection AddS3CloudFiles(this IServiceCollection service } serviceCollection.AddSingleton(cloudFilesOptions); - serviceCollection.AddTransient(); + serviceCollection.AddSingleton(); return serviceCollection; } diff --git a/SW.CloudFiles.S3/Extensions/CloudFilesOptionsExtensions.cs b/SW.CloudFiles.S3/Extensions/CloudFilesOptionsExtensions.cs index 2f1f509..74448a0 100644 --- a/SW.CloudFiles.S3/Extensions/CloudFilesOptionsExtensions.cs +++ b/SW.CloudFiles.S3/Extensions/CloudFilesOptionsExtensions.cs @@ -9,14 +9,16 @@ namespace SW.CloudFiles.S3; public static class CloudFilesOptionsExtensions { /// Creates a configured from the given options. - public static AmazonS3Client CreateClient(this CloudFilesOptions cloudFilesOptions) + public static AmazonS3Client CreateClient(this S3CloudFilesOptions cloudFilesOptions) { var clientConfig = new AmazonS3Config { RegionEndpoint = RegionEndpoint.USEast1, ServiceURL = cloudFilesOptions.ServiceUrl, UseHttp = new Uri(cloudFilesOptions.ServiceUrl).Scheme.ToLower() == "http", - ForcePathStyle = true + ForcePathStyle = true, + Timeout = TimeSpan.FromSeconds(cloudFilesOptions.TimeoutSeconds), + MaxErrorRetry = cloudFilesOptions.MaxErrorRetry }; return new AmazonS3Client(cloudFilesOptions.AccessKeyId, cloudFilesOptions.SecretAccessKey, clientConfig); diff --git a/SW.CloudFiles.S3/S3CloudFilesOptions.cs b/SW.CloudFiles.S3/S3CloudFilesOptions.cs index 434210d..57e9cff 100644 --- a/SW.CloudFiles.S3/S3CloudFilesOptions.cs +++ b/SW.CloudFiles.S3/S3CloudFilesOptions.cs @@ -10,4 +10,10 @@ public class S3CloudFilesOptions : CloudFilesOptions /// Lifecycle rules are: temp1/ (1 day), temp7/ (7 days), temp30/ (30 days), temp365/ (365 days). /// public bool DisableAutoLifecycle { get; set; } + + /// Per-request timeout, in seconds, for the underlying . Defaults to 15. + public int TimeoutSeconds { get; set; } = 15; + + /// Max automatic retries for the underlying . Defaults to 2. + public int MaxErrorRetry { get; set; } = 2; } diff --git a/SW.CloudFiles.S3/Services/CloudFilesService.cs b/SW.CloudFiles.S3/Services/CloudFilesService.cs index 0a788e2..ee80076 100644 --- a/SW.CloudFiles.S3/Services/CloudFilesService.cs +++ b/SW.CloudFiles.S3/Services/CloudFilesService.cs @@ -20,7 +20,7 @@ public class CloudFilesService : IDisposable, ICloudFilesService public CloudFilesService(CloudFilesOptions cloudFilesOptions) { this.cloudFilesOptions = cloudFilesOptions; - client = cloudFilesOptions.CreateClient(); + client = ((S3CloudFilesOptions)cloudFilesOptions).CreateClient(); } ///