Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNOW-878067: Retry Strategy #803

Merged
merged 24 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
68e4dc1
SNOW-878067: Add retry strategy for supported endpoints
sfc-gh-ext-simba-lf Oct 26, 2023
b7dc6bd
SNOW-878067: Add tests for retry strategy
sfc-gh-ext-simba-lf Oct 26, 2023
59f1f7e
Merge branch 'master' of https://github.com/snowflakedb/snowflake-con…
sfc-gh-ext-simba-lf Oct 26, 2023
5d7f9e1
SNOW-878067: Fix variable name
sfc-gh-ext-simba-lf Oct 26, 2023
77611e6
SNOW-878067: Increase delta for async timeout test
sfc-gh-ext-simba-lf Oct 26, 2023
9639f43
SNOW-878067: Increase delta for async timeout test
sfc-gh-ext-simba-lf Oct 26, 2023
a441122
SNOW-878067: Increase delta for timeout test
sfc-gh-ext-simba-lf Oct 26, 2023
ddd961e
SNOW-878067: Refactor variable for connection timeout
sfc-gh-ext-simba-lf Oct 27, 2023
a203e19
SNOW-878067: Refactor variable for connection timeout
sfc-gh-ext-simba-lf Oct 27, 2023
949c257
Merge branch 'master' of https://github.com/snowflakedb/snowflake-con…
sfc-gh-ext-simba-lf Oct 27, 2023
da0d1c6
Update CONNECTION_TIMEOUT default value from 120 to 300
sfc-gh-ext-simba-lf Oct 31, 2023
8007e5e
SNOW-878067: Update retry logic
sfc-gh-ext-simba-lf Nov 3, 2023
5b926b7
Update README.md with new parameter "RETRY_TIMEOUT"
sfc-gh-ext-simba-lf Nov 3, 2023
2866dec
SNOW-878067: Modify timeout test cases
sfc-gh-ext-simba-lf Nov 3, 2023
692bb36
Merge branch 'SNOW-878067-Retry-Strategy' of https://github.com/snowf…
sfc-gh-ext-simba-lf Nov 3, 2023
d0a445e
SNOW-878067: Modify timeout test cases
sfc-gh-ext-simba-lf Nov 3, 2023
ce6b2b6
SNOW-878067: Modify timeout test cases
sfc-gh-ext-simba-lf Nov 3, 2023
98f0abe
SNOW-878067: Modify timeout test cases
sfc-gh-ext-simba-lf Nov 3, 2023
b25afec
SNOW-878067: Update comment
sfc-gh-ext-simba-lf Nov 3, 2023
13fa45f
SNOW-878067: Modify timeout test cases
sfc-gh-ext-simba-lf Nov 3, 2023
5e9931e
SNOW-878067: Refactor retry logic
sfc-gh-ext-simba-lf Nov 3, 2023
4cc94f7
SNOW-878067: Set maxHttpRetries to 0 to reach the defined timeout
sfc-gh-ext-simba-lf Nov 3, 2023
ef12cef
SNOW-878067: Remove OS Platform restriction on timeout tests
sfc-gh-ext-simba-lf Nov 3, 2023
4f511f4
SNOW-878067: Refactor setting which timeout to use
sfc-gh-ext-simba-lf Nov 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 43 additions & 35 deletions Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public void TestBasicConnection()
conn.Open();
Assert.AreEqual(ConnectionState.Open, conn.State);

Assert.AreEqual(120, conn.ConnectionTimeout);
Assert.AreEqual(SFSessionHttpClientProperties.s_connectionTimeoutDefault, conn.ConnectionTimeout);
// Data source is empty string for now
Assert.AreEqual("", ((SnowflakeDbConnection)conn).DataSource);

Expand Down Expand Up @@ -379,7 +379,8 @@ public void TestLoginTimeout()
{
using (IDbConnection conn = new MockSnowflakeDbConnection())
{
int timeoutSec = 5;
// Connection timeout can only be increased
int timeoutSec = 5 + SFSessionHttpClientProperties.s_connectionTimeoutDefault;
string loginTimeOut5sec = String.Format(ConnectionString + "connection_timeout={0}",
timeoutSec);

Expand All @@ -395,17 +396,19 @@ public void TestLoginTimeout()
}
catch (AggregateException e)
{
Assert.AreEqual(SFError.REQUEST_TIMEOUT.GetAttribute<SFErrorAttr>().errorCode,
// Jitter can cause the request to reach max number of retries before reaching the timeout
Assert.IsTrue(e.InnerException is TaskCanceledException ||
SFError.REQUEST_TIMEOUT.GetAttribute<SFErrorAttr>().errorCode ==
((SnowflakeDbException)e.InnerException).ErrorCode);
}
stopwatch.Stop();
int detla = 10; //in case server time slower.
int delta = 100; // in case server time slower.

//Should timeout before the default timeout (120 sec) * 1000
Assert.Less(stopwatch.ElapsedMilliseconds, 120 * 1000);
// Should timeout after the defined connection timeout
Assert.GreaterOrEqual(stopwatch.ElapsedMilliseconds, timeoutSec * 1000 - detla);
Assert.AreEqual(5, conn.ConnectionTimeout);
// Should timeout after the minimum possible timeout with jitter: 14 seconds (50% of 4 * 2) after 7 retries
Assert.GreaterOrEqual(stopwatch.ElapsedMilliseconds, 14 * 1000);
// Should timeout before the defined connection timeout
Assert.LessOrEqual(stopwatch.ElapsedMilliseconds, timeoutSec * 1000 + delta);
Assert.AreEqual(timeoutSec, conn.ConnectionTimeout);
}
}
}
Expand All @@ -426,15 +429,20 @@ public void TestLoginWithMaxRetryReached()
conn.Open();
Assert.Fail();
}
catch
catch (Exception e)
{
// Jitter can cause the request to reach max number of retries before reaching the timeout
Assert.IsTrue(e.InnerException is TaskCanceledException ||
SFError.REQUEST_TIMEOUT.GetAttribute<SFErrorAttr>().errorCode ==
((SnowflakeDbException)e.InnerException).ErrorCode);
}
stopwatch.Stop();

// retry 5 times with backoff 1, 2, 4, 8, 16 seconds
// but should not delay more than another 16 seconds
Assert.Less(stopwatch.ElapsedMilliseconds, 51 * 1000);
Assert.GreaterOrEqual(stopwatch.ElapsedMilliseconds, 30 * 1000);
// retry 5 times with starting backoff of 4 seconds with jitter
// but should not delay more than the default connection timeout of 300 seconds
// and should not take less time than 10 (with max negative jitter for 5 retries) seconds
Assert.Less(stopwatch.ElapsedMilliseconds, SFSessionHttpClientProperties.s_connectionTimeoutDefault * 1000);
Assert.GreaterOrEqual(stopwatch.ElapsedMilliseconds, 10 * 1000);
}
}

Expand Down Expand Up @@ -1665,8 +1673,8 @@ public void TestCancelLoginBeforeTimeout()
{
using (var conn = new MockSnowflakeDbConnection())
{
// No timeout
int timeoutSec = 0;
// Add 10 seconds to default timeout
int timeoutSec = 10 + SFSessionHttpClientProperties.s_connectionTimeoutDefault;
string infiniteLoginTimeOut = String.Format(ConnectionString + ";connection_timeout={0};maxHttpRetries=0",
timeoutSec);

Expand All @@ -1678,12 +1686,11 @@ public void TestCancelLoginBeforeTimeout()
//Assert.AreEqual(120, conn.ConnectionTimeout);

CancellationTokenSource connectionCancelToken = new CancellationTokenSource();
Task connectTask = conn.OpenAsync(connectionCancelToken.Token);
Task connectTask = conn.OpenAsync(connectionCancelToken.Token);

// Sleep for the specified timeout minus 10 seconds (just before the actual timeout is reached)
Thread.Sleep((timeoutSec - 10) * 1000);

// Sleep for 130 sec (more than the default connection timeout and the httpclient
// timeout to make sure there are no false positive )
Thread.Sleep(130*1000);

Assert.AreEqual(ConnectionState.Connecting, conn.State);

// Cancel the connection because it will never succeed since there is no
Expand All @@ -1704,7 +1711,7 @@ public void TestCancelLoginBeforeTimeout()
}

Assert.AreEqual(ConnectionState.Closed, conn.State);
Assert.AreEqual(0, conn.ConnectionTimeout);
Assert.AreEqual(timeoutSec, conn.ConnectionTimeout);
}
}

Expand All @@ -1715,7 +1722,8 @@ public void TestAsyncLoginTimeout()
{
using (var conn = new MockSnowflakeDbConnection())
{
int timeoutSec = 5;
// Connection timeout can only be increased
int timeoutSec = 5 + SFSessionHttpClientProperties.s_connectionTimeoutDefault;
string loginTimeOut5sec = String.Format(ConnectionString + "connection_timeout={0}",
timeoutSec);
conn.ConnectionString = loginTimeOut5sec;
Expand All @@ -1735,15 +1743,15 @@ public void TestAsyncLoginTimeout()

}
stopwatch.Stop();
int detla = 10; //in case server time slower.
int delta = 100; // in case server time slower.

// Should timeout after 5sec
Assert.GreaterOrEqual(stopwatch.ElapsedMilliseconds, 5 * 1000 - detla);
// But never more than 1 sec (max backoff) after the default timeout
Assert.LessOrEqual(stopwatch.ElapsedMilliseconds, (6) * 1000);
// Should timeout after the minimum possible timeout with jitter: 14 seconds (50% of 4 * 2) after 7 retries
Assert.GreaterOrEqual(stopwatch.ElapsedMilliseconds, 14 * 1000);
// Should timeout before the defined connection timeout
Assert.LessOrEqual(stopwatch.ElapsedMilliseconds, timeoutSec * 1000 + delta);

Assert.AreEqual(ConnectionState.Closed, conn.State);
Assert.AreEqual(5, conn.ConnectionTimeout);
Assert.AreEqual(timeoutSec, conn.ConnectionTimeout);
}
}
}
Expand All @@ -1770,15 +1778,15 @@ public void TestAsyncDefaultLoginTimeout()
((SnowflakeDbException)e.InnerException).ErrorCode);
}
stopwatch.Stop();
int detla = 10; //in case server time slower.
int delta = 100; // in case server time slower.

// Should timeout after the default timeout (120 sec)
Assert.GreaterOrEqual(stopwatch.ElapsedMilliseconds, 120 * 1000 - detla);
// But never more than 16 sec (max backoff) after the default timeout
Assert.LessOrEqual(stopwatch.ElapsedMilliseconds, (120 + 16) * 1000);
// Should timeout after the default timeout (300 sec)
Assert.GreaterOrEqual(stopwatch.ElapsedMilliseconds, SFSessionHttpClientProperties.s_connectionTimeoutDefault * 1000 - delta);
// But never more because there's no connection timeout remaining
Assert.LessOrEqual(stopwatch.ElapsedMilliseconds, SFSessionHttpClientProperties.s_connectionTimeoutDefault * 1000 + delta);

Assert.AreEqual(ConnectionState.Closed, conn.State);
Assert.AreEqual(120, conn.ConnectionTimeout);
Assert.AreEqual(SFSessionHttpClientProperties.s_connectionTimeoutDefault, conn.ConnectionTimeout);
}
}

Expand Down
78 changes: 78 additions & 0 deletions Snowflake.Data.Tests/IntegrationTests/SFDbCommandIT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ namespace Snowflake.Data.Tests.IntegrationTests
using System.Diagnostics;
using System.Collections.Generic;
using System.Globalization;
using Snowflake.Data.Tests.Mock;
using Snowflake.Data.Core;

[TestFixture]
class SFDbCommandITAsync : SFBaseTestAsync
Expand Down Expand Up @@ -90,6 +92,44 @@ public void TestCancelExecuteAsync()
}
}

[Test]
public void TestExecuteAsyncWithMaxRetryReached()
{
var mockRestRequester = new MockRetryUntilRestTimeoutRestRequester()
{
_forceTimeoutForNonLoginRequestsOnly = true
};

using (DbConnection conn = new MockSnowflakeDbConnection(mockRestRequester))
{
string maxRetryConnStr = ConnectionString + "maxHttpRetries=5";

conn.ConnectionString = maxRetryConnStr;
conn.Open();

Stopwatch stopwatch = Stopwatch.StartNew();
try
{
using (DbCommand command = conn.CreateCommand())
{
command.CommandText = "select 1;";
Task<object> t = command.ExecuteScalarAsync();
t.Wait();
}
Assert.Fail();
}
catch (Exception e)
{
Assert.IsInstanceOf<TaskCanceledException>(e.InnerException);
}
stopwatch.Stop();

// retry 5 times with backoff 1, 2, 4, 8, 16 seconds
// but should not delay more than another 16 seconds
Assert.Less(stopwatch.ElapsedMilliseconds, 51 * 1000);
Assert.GreaterOrEqual(stopwatch.ElapsedMilliseconds, 30 * 1000);
sfc-gh-dstempniak marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

[TestFixture]
Expand Down Expand Up @@ -544,6 +584,44 @@ public void TestExecuteScalarNull()
}
}

[Test]
public void TestExecuteWithMaxRetryReached()
{
var mockRestRequester = new MockRetryUntilRestTimeoutRestRequester()
{
_forceTimeoutForNonLoginRequestsOnly = true
};

using (IDbConnection conn = new MockSnowflakeDbConnection(mockRestRequester))
{
string maxRetryConnStr = ConnectionString + "maxHttpRetries=5";

conn.ConnectionString = maxRetryConnStr;
conn.Open();

Stopwatch stopwatch = Stopwatch.StartNew();
try
{
using (IDbCommand command = conn.CreateCommand())
{
command.CommandText = "select 1;";
command.ExecuteScalar();
}
Assert.Fail();
}
catch (Exception e)
{
Assert.IsInstanceOf<TaskCanceledException>(e.InnerException);
}
stopwatch.Stop();

// retry 5 times with backoff 1, 2, 4, 8, 16 seconds
// but should not delay more than another 16 seconds
Assert.Less(stopwatch.ElapsedMilliseconds, 51 * 1000);
Assert.GreaterOrEqual(stopwatch.ElapsedMilliseconds, 30 * 1000);
}
}

[Test]
public void TestCreateCommandBeforeOpeningConnection()
{
Expand Down
11 changes: 9 additions & 2 deletions Snowflake.Data.Tests/Mock/MockRetryUntilRestTimeout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ namespace Snowflake.Data.Tests.Mock

class MockRetryUntilRestTimeoutRestRequester : RestRequester, IMockRestRequester
{
internal bool _forceTimeoutForNonLoginRequestsOnly = false;

public MockRetryUntilRestTimeoutRestRequester() : base(null)
{
// Does nothing
Expand All @@ -30,8 +32,13 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
CancellationToken externalCancellationToken,
string sid = "")
{
// Override the http timeout and set to 1ms to force all http request to timeout and retry
message.Properties[BaseRestRequest.HTTP_REQUEST_TIMEOUT_KEY] = TimeSpan.FromTicks(0);
if (!_forceTimeoutForNonLoginRequestsOnly ||
_forceTimeoutForNonLoginRequestsOnly && !message.RequestUri.AbsolutePath.Equals(RestPath.SF_LOGIN_PATH))
{
// Override the http timeout and set to 1ms to force all http request to timeout and retry
message.Properties[BaseRestRequest.HTTP_REQUEST_TIMEOUT_KEY] = TimeSpan.FromTicks(0);
}

return await (base.SendAsync(message, restTimeout, externalCancellationToken).ConfigureAwait(false));
}
}
Expand Down
44 changes: 44 additions & 0 deletions Snowflake.Data.Tests/UnitTests/HttpUtilTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Snowflake.Data.Tests.UnitTests
using RichardSzalay.MockHttp;
using System.Threading.Tasks;
using System.Net;
using System;

[TestFixture]
class HttpUtilTest
Expand Down Expand Up @@ -39,6 +40,49 @@ public async Task TestIsRetryableHTTPCode(HttpStatusCode statusCode, bool forceR
Assert.AreEqual(expectedIsRetryable, actualIsRetryable);
}

// Parameters: request url, expected value
[TestCase("https://test.snowflakecomputing.com/session/v1/login-request", true)]
[TestCase("https://test.snowflakecomputing.com/session/authenticator-request", true)]
[TestCase("https://test.snowflakecomputing.com/session/token-request", true)]
[TestCase("https://test.snowflakecomputing.com/queries/v1/query-request", false)]
[Test]
public void TestIsLoginUrl(string requestUrl, bool expectedIsLoginEndpoint)
{
// given
var uri = new Uri(requestUrl);

// when
bool isLoginEndpoint = HttpUtil.IsLoginEndpoint(uri.AbsolutePath);

// then
Assert.AreEqual(expectedIsLoginEndpoint, isLoginEndpoint);
}

// Parameters: time in seconds
[TestCase(4)]
[TestCase(8)]
[TestCase(16)]
[TestCase(32)]
[TestCase(64)]
[TestCase(128)]
[Test]
public void TestGetJitter(int seconds)
{
// given
var lowerBound = -(seconds / 2);
var upperBound = seconds / 2;

double jitter;
for (var i = 0; i < 10; i++)
{
// when
jitter = HttpUtil.GetJitter(seconds);
sfc-gh-dstempniak marked this conversation as resolved.
Show resolved Hide resolved

// then
Assert.IsTrue(jitter >= lowerBound && jitter <= upperBound);
}
}

[Test]
public void ShouldCreateHttpClientHandlerWithProxy()
{
Expand Down
2 changes: 1 addition & 1 deletion Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
string defHost = "testaccount.snowflakecomputing.com";
string defAuthenticator = "snowflake";
string defScheme = "https";
string defConnectionTimeout = "120";
string defConnectionTimeout = "300";
string defBrowserResponseTime = "120";
string defPassword = "123";
string defPort = "443";
Expand Down
Loading
Loading