Skip to content

Commit b475ee2

Browse files
fix(csharp): Implement host reset (generated)
algolia/api-clients-automation#5886 Co-authored-by: algolia-bot <accounts+algolia-api-client-bot@algolia.com> Co-authored-by: Mario-Alexandru Dan <marioalexandrudan@gmail.com>
1 parent b38634f commit b475ee2

File tree

6 files changed

+184
-17
lines changed

6 files changed

+184
-17
lines changed

algoliasearch/Algolia.Search.csproj

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<RequireLicenseAcceptance>false</RequireLicenseAcceptance>
2222
<Version>7.36.2</Version>
2323
<GenerateDocumentationFile>true</GenerateDocumentationFile>
24-
<TargetFrameworks>netstandard2.1;netstandard2.0</TargetFrameworks>
24+
<TargetFrameworks>netstandard2.1;netstandard2.0;net9.0</TargetFrameworks>
2525
<IncludeSymbols>true</IncludeSymbols>
2626
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
2727
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -42,6 +42,25 @@
4242
<PackageReference Include="System.Text.Json" Version="[8.0.5, 11.0)" />
4343
</ItemGroup>
4444

45+
<!-- Test dependencies only for net9.0 target -->
46+
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
47+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
48+
<PackageReference Include="xunit" Version="2.9.3" />
49+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
50+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
51+
<PrivateAssets>all</PrivateAssets>
52+
</PackageReference>
53+
<PackageReference Include="coverlet.collector" Version="6.0.4">
54+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
55+
<PrivateAssets>all</PrivateAssets>
56+
</PackageReference>
57+
</ItemGroup>
58+
59+
<!-- Exclude Tests directory from netstandard builds -->
60+
<ItemGroup Condition="'$(TargetFramework)' != 'net9.0'">
61+
<Compile Remove="Tests/**/*.cs" />
62+
</ItemGroup>
63+
4564
<ItemGroup>
4665
<Content Include="..\icon.png">
4766
<Pack>true</Pack>

algoliasearch/Http/AlgoliaHttpRequester.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ public async Task<AlgoliaHttpResponse> SendRequestAsync(
7373
}
7474

7575
httpRequestMessage.Headers.Fill(request.Headers);
76-
httpRequestMessage.SetTimeout(requestTimeout + connectTimeout);
76+
77+
httpRequestMessage.SetTimeout(connectTimeout);
7778

7879
try
7980
{
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Linq;
5+
using System.Net.Http;
6+
using System.Threading.Tasks;
7+
using Algolia.Search.Clients;
8+
using Algolia.Search.Exceptions;
9+
using Algolia.Search.Http;
10+
using Algolia.Search.Transport;
11+
using Microsoft.Extensions.Logging;
12+
using Microsoft.Extensions.Logging.Abstractions;
13+
using Xunit;
14+
15+
namespace Algolia.Search.Tests;
16+
17+
public class TimeoutIntegrationTests
18+
{
19+
private static (AlgoliaConfig, StatefulHost) CreateConfigWithHost(string hostUrl)
20+
{
21+
var config = new SearchConfig("test-app", "test-key");
22+
var host = new StatefulHost { Url = hostUrl, Accept = CallType.Read | CallType.Write };
23+
config.CustomHosts = new List<StatefulHost> { host };
24+
return (config, host);
25+
}
26+
27+
private static StatefulHost CreateServerHost()
28+
{
29+
var serverHost =
30+
Environment.GetEnvironmentVariable("CI") == "true" ? "localhost" : "host.docker.internal";
31+
32+
return new StatefulHost
33+
{
34+
Url = serverHost,
35+
Port = 6676,
36+
Scheme = HttpScheme.Http,
37+
Accept = CallType.Read | CallType.Write,
38+
};
39+
}
40+
41+
[Fact]
42+
public async Task RetryCountStateful()
43+
{
44+
// connect timeout increases across failed requests: 2s -> 4s -> 6s
45+
var (config, _) = CreateConfigWithHost("10.255.255.1");
46+
var transport = new HttpTransport(
47+
config,
48+
new AlgoliaHttpRequester(NullLoggerFactory.Instance),
49+
NullLoggerFactory.Instance
50+
);
51+
52+
var times = new List<double>();
53+
for (int i = 0; i < 3; i++)
54+
{
55+
var sw = Stopwatch.StartNew();
56+
try
57+
{
58+
await transport.ExecuteRequestAsync(
59+
HttpMethod.Get,
60+
"/test",
61+
new InternalRequestOptions { UseReadTransporter = true }
62+
);
63+
}
64+
catch (Exception)
65+
{
66+
sw.Stop();
67+
times.Add(sw.Elapsed.TotalSeconds);
68+
}
69+
}
70+
71+
// Connect timeout scales: ConnectTimeout (2s default) * (RetryCount+1)
72+
// Request 1: 2s * 1 = 2s
73+
// Request 2: 2s * 2 = 4s
74+
// Request 3: 2s * 3 = 6s
75+
Assert.True(times[0] > 1.5 && times[0] < 2.5, $"Request 1 should be ~2s, got {times[0]:F2}s");
76+
Assert.True(times[1] > 3.5 && times[1] < 4.5, $"Request 2 should be ~4s, got {times[1]:F2}s");
77+
Assert.True(times[2] > 5.5 && times[2] < 7.0, $"Request 3 should be ~6s, got {times[2]:F2}s");
78+
}
79+
80+
[Fact]
81+
public async Task RetryCountResets()
82+
{
83+
// retry_count resets to 0 after successful request
84+
var (config, badHost) = CreateConfigWithHost("10.255.255.1");
85+
var goodHost = CreateServerHost();
86+
var transport = new HttpTransport(
87+
config,
88+
new AlgoliaHttpRequester(NullLoggerFactory.Instance),
89+
NullLoggerFactory.Instance
90+
);
91+
92+
// fail twice to increment retry_count
93+
for (int i = 0; i < 2; i++)
94+
{
95+
try
96+
{
97+
await transport.ExecuteRequestAsync(
98+
HttpMethod.Get,
99+
"/test",
100+
new InternalRequestOptions { UseReadTransporter = true }
101+
);
102+
}
103+
catch (Exception)
104+
{
105+
// expected to fail
106+
}
107+
}
108+
109+
// switch to good host and succeed
110+
config.CustomHosts = new List<StatefulHost> { goodHost };
111+
goodHost.RetryCount = badHost.RetryCount;
112+
transport = new HttpTransport(
113+
config,
114+
new AlgoliaHttpRequester(NullLoggerFactory.Instance),
115+
NullLoggerFactory.Instance
116+
);
117+
118+
var response = await transport.ExecuteRequestAsync<AlgoliaHttpResponse>(
119+
HttpMethod.Get,
120+
"/1/test/instant",
121+
new InternalRequestOptions { UseReadTransporter = true }
122+
);
123+
124+
Assert.Equal(200, response.HttpStatusCode);
125+
Assert.True(
126+
goodHost.RetryCount == 0,
127+
$"retry_count should reset to 0, got {goodHost.RetryCount}"
128+
);
129+
130+
// point to bad host again, should timeout at 2s (not 6s)
131+
goodHost.Url = "10.255.255.1";
132+
goodHost.Port = null;
133+
goodHost.Scheme = HttpScheme.Https;
134+
135+
var sw = Stopwatch.StartNew();
136+
try
137+
{
138+
await transport.ExecuteRequestAsync(
139+
HttpMethod.Get,
140+
"/test",
141+
new InternalRequestOptions { UseReadTransporter = true }
142+
);
143+
Assert.Fail("Request should have timed out");
144+
}
145+
catch (Exception)
146+
{
147+
sw.Stop();
148+
var elapsed = sw.Elapsed.TotalSeconds;
149+
Assert.True(elapsed > 1.5 && elapsed < 2.5, $"After reset should be ~2s, got {elapsed:F2}s");
150+
}
151+
}
152+
}

algoliasearch/Transport/HttpTransport.cs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,10 @@ private async Task<TResult> ExecuteRequestAsync<TResult, TData>(
132132
requestOptions?.PathParameters,
133133
requestOptions?.QueryParameters
134134
);
135-
var requestTimeout = TimeSpan.FromTicks(
136-
(GetTimeOut(callType, requestOptions)).Ticks * (host.RetryCount + 1)
137-
);
135+
var requestTimeout = GetTimeOut(callType, requestOptions);
136+
var baseConnectTimeout =
137+
requestOptions?.ConnectTimeout ?? _algoliaConfig.ConnectTimeout ?? Defaults.ConnectTimeout;
138+
var connectTimeout = TimeSpan.FromTicks(baseConnectTimeout.Ticks * (host.RetryCount + 1));
138139

139140
if (request.Body == null && (method == HttpMethod.Post || method == HttpMethod.Put))
140141
{
@@ -145,21 +146,15 @@ private async Task<TResult> ExecuteRequestAsync<TResult, TData>(
145146
{
146147
_logger.LogTrace("Sending request to {Method} {Uri}", request.Method, request.Uri);
147148
_logger.LogTrace("Request timeout: {RequestTimeout} (s)", requestTimeout.TotalSeconds);
149+
_logger.LogTrace("Connect timeout: {ConnectTimeout} (s)", connectTimeout.TotalSeconds);
148150
foreach (var header in request.Headers)
149151
{
150152
_logger.LogTrace("Header: {HeaderName} : {HeaderValue}", header.Key, header.Value);
151153
}
152154
}
153155

154156
var response = await _httpClient
155-
.SendRequestAsync(
156-
request,
157-
requestTimeout,
158-
requestOptions?.ConnectTimeout
159-
?? _algoliaConfig.ConnectTimeout
160-
?? Defaults.ConnectTimeout,
161-
ct
162-
)
157+
.SendRequestAsync(request, requestTimeout, connectTimeout, ct)
163158
.ConfigureAwait(false);
164159

165160
_errorMessage = response.Error;

algoliasearch/Transport/RetryStrategy.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Algolia.Search.Http;
77

88
[assembly: InternalsVisibleTo("Algolia.Search.Tests")]
9+
[assembly: InternalsVisibleTo("Algolia.Search.IntegrationTests")]
910

1011
namespace Algolia.Search.Transport;
1112

@@ -71,8 +72,7 @@ public RetryOutcomeType Decide(StatefulHost tryableHost, AlgoliaHttpResponse res
7172
{
7273
if (!response.IsTimedOut && IsSuccess(response))
7374
{
74-
tryableHost.Up = true;
75-
tryableHost.LastUse = DateTime.UtcNow;
75+
Reset(tryableHost);
7676
return RetryOutcomeType.Success;
7777
}
7878

algoliasearch/Utils/SearchClientExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -972,7 +972,7 @@ await WaitForTaskAsync(
972972
await DeleteIndexAsync(tmpIndexName, cancellationToken: cancellationToken)
973973
.ConfigureAwait(false);
974974

975-
throw ex;
975+
throw;
976976
}
977977
}
978978

@@ -1234,7 +1234,7 @@ public async Task<bool> IndexExistsAsync(
12341234
}
12351235
catch (Exception ex)
12361236
{
1237-
throw ex;
1237+
throw;
12381238
}
12391239

12401240
return await Task.FromResult(true);

0 commit comments

Comments
 (0)