Skip to content

Commit fecc2f0

Browse files
Round sub-milliseconds up to the next milliseconds on transaction timeout (#720)
* Round sub-milliseconds up to the next milliseconds on transaction timeout. * Minor tidying * Fixup * Fix failing tests * Added non-integer milliseconds to tests * blacklist test that is failing in .NET * xml docs * Fix mutating default Tx Config --------- Co-authored-by: grant lodge <[email protected]>
1 parent 1a5e46d commit fecc2f0

File tree

7 files changed

+124
-55
lines changed

7 files changed

+124
-55
lines changed

Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/TestBlackList.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ private static readonly (string Name, string Reason)[] BlackListNames =
8888
"driver does not report errors on RUN before consuming results"),
8989
("tests.stub.tx_run.test_tx_run.TestTxRun.test_should_prevent_pull_after_tx_termination_on_run",
9090
"driver does not report errors on RUN before consuming results"),
91+
("stub.tx_run.test_tx_run.TestTxRun.test_should_prevent_commit_after_tx_termination",
92+
"driver does not report errors on RUN before consuming results"),
9193

9294
//TODO:
9395
("stub.tx_run.test_tx_run.TestTxRun.test_should_prevent_pull_after_tx_termination_on_pull",

Neo4j.Driver/Neo4j.Driver.Tests/Internal/Util/ConfigBuildersTests.cs

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,30 +23,6 @@ namespace Neo4j.Driver.Internal.Util
2323
{
2424
public class ConfigBuildersTests
2525
{
26-
public class BuildTransactionOptions
27-
{
28-
[Fact]
29-
public void ShouldReturnEmptyTxOptionsWhenBuilderIsNull()
30-
{
31-
var options = ConfigBuilders.BuildTransactionConfig(null);
32-
options.Should().Be(TransactionConfig.Default);
33-
}
34-
35-
[Fact]
36-
public void ShouldReturnNewTxOptions()
37-
{
38-
var options1 = ConfigBuilders.BuildTransactionConfig(o => o.WithTimeout(TimeSpan.FromSeconds(5)));
39-
var options2 = ConfigBuilders.BuildTransactionConfig(o => o.WithTimeout(TimeSpan.FromSeconds(30)));
40-
options1.Timeout.Should().Be(TimeSpan.FromSeconds(5));
41-
options2.Timeout.Should().Be(TimeSpan.FromSeconds(30));
42-
43-
// When I reset to another value
44-
options1.Timeout = TimeSpan.FromMinutes(1);
45-
options1.Timeout.Should().Be(TimeSpan.FromMinutes(1));
46-
options2.Timeout.Should().Be(TimeSpan.FromSeconds(30));
47-
}
48-
}
49-
5026
public class BuildSessionOptions
5127
{
5228
[Fact]
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) "Neo4j"
2+
// Neo4j Sweden AB [http://neo4j.com]
3+
//
4+
// This file is part of Neo4j.
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License").
7+
// You may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
18+
using System;
19+
using FluentAssertions;
20+
using Moq;
21+
using Moq.AutoMock;
22+
using Xunit;
23+
24+
namespace Neo4j.Driver.Internal.Util
25+
{
26+
public class TransactionTimeoutTests
27+
{
28+
[Theory]
29+
[InlineData(0, 0, 0)]
30+
[InlineData(0, 1, 1)]
31+
[InlineData(0.1, 0, 1)]
32+
[InlineData(1, 0, 1)]
33+
[InlineData(1, 1, 2)]
34+
[InlineData(1.23, 0, 2)]
35+
[InlineData(598, 1, 599)]
36+
[InlineData(598.2, 0, 599)]
37+
[InlineData(2000, 96, 2001)]
38+
[InlineData(2000.8, 0, 2001)]
39+
public void ShouldRoundSubmillisecondTimeoutsToMilliseconds(
40+
double milliseconds,
41+
int ticks,
42+
int expectedMilliseconds)
43+
{
44+
var inputMilliseconds = TimeSpan.FromMilliseconds(milliseconds);
45+
var inputTicks = TimeSpan.FromTicks(ticks);
46+
var totalInput = inputMilliseconds + inputTicks;
47+
var expectedTimeout = TimeSpan.FromMilliseconds(expectedMilliseconds);
48+
49+
var autoMocker = new AutoMocker(MockBehavior.Strict);
50+
if (expectedTimeout != inputMilliseconds)
51+
{
52+
// only logs if it changes the timeout
53+
autoMocker.GetMock<ILogger>()
54+
.Setup(
55+
x => x.Info(
56+
It.Is<string>(s => s.Contains("rounded up")),
57+
It.IsAny<object[]>()))
58+
.Verifiable();
59+
}
60+
61+
var sut =
62+
new TransactionConfigBuilder(autoMocker.GetMock<ILogger>().Object, TransactionConfig.Default)
63+
.WithTimeout(totalInput);
64+
65+
var result = sut.Build().Timeout;
66+
67+
result.Should().Be(expectedTimeout);
68+
(result?.Ticks % TimeSpan.TicksPerMillisecond).Should().Be(0);
69+
autoMocker.VerifyAll();
70+
}
71+
}
72+
}

Neo4j.Driver/Neo4j.Driver.Tests/TransactionConfigTests.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public class TimeoutField
3636
{
3737
new object[] { null },
3838
new object[] { (TimeSpan?)TimeSpan.Zero },
39-
new object[] { (TimeSpan?)TimeSpan.FromMilliseconds(0.1) },
39+
new object[] { (TimeSpan?)TimeSpan.FromMilliseconds(1) },
4040
new object[] { (TimeSpan?)TimeSpan.FromMinutes(30) },
4141
new object[] { (TimeSpan?)TimeSpan.MaxValue }
4242
};
@@ -53,7 +53,7 @@ public void ShouldReturnDefaultValueAsNull()
5353
[MemberData(nameof(ValidTimeSpanValues))]
5454
public void ShouldAllowToSetToNewValue(TimeSpan? input)
5555
{
56-
var builder = TransactionConfig.Builder;
56+
var builder = new TransactionConfigBuilder(null, TransactionConfig.Default);
5757
builder.WithTimeout(input);
5858

5959
var config = builder.Build();
@@ -65,7 +65,7 @@ public void ShouldAllowToSetToNewValue(TimeSpan? input)
6565
[MemberData(nameof(InvalidTimeSpanValues))]
6666
public void ShouldThrowExceptionIfAssigningValueLessThanZero(TimeSpan input)
6767
{
68-
var error = Record.Exception(() => TransactionConfig.Builder.WithTimeout(input));
68+
var error = Record.Exception(() => new TransactionConfigBuilder(null, TransactionConfig.Default).WithTimeout(input));
6969

7070
error.Should().BeOfType<ArgumentOutOfRangeException>();
7171
error.Message.Should().Contain("not be negative");
@@ -85,8 +85,8 @@ public void ShouldReturnDefaultValueEmptyDictionary()
8585
[Fact]
8686
public void ShouldAllowToSetToNewValue()
8787
{
88-
var builder = TransactionConfig.Builder;
89-
builder.WithMetadata(new Dictionary<string, object> { { "key", "value" } });
88+
var builder = new TransactionConfigBuilder(null, TransactionConfig.Default)
89+
.WithMetadata(new Dictionary<string, object> { { "key", "value" } });
9090

9191
var config = builder.Build();
9292

@@ -98,7 +98,8 @@ public void ShouldAllowToSetToNewValue()
9898
[Fact]
9999
public void ShouldThrowExceptionIfAssigningNull()
100100
{
101-
var error = Record.Exception(() => TransactionConfig.Builder.WithMetadata(null));
101+
var error = Record.Exception(
102+
() => new TransactionConfigBuilder(null, TransactionConfig.Default).WithMetadata(null));
102103

103104
error.Should().BeOfType<ArgumentNullException>();
104105
error.Message.Should().Contain("should not be null");

Neo4j.Driver/Neo4j.Driver/Internal/AsyncSession.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,17 @@ public Task<IResultCursor> RunAsync(
193193
return result;
194194
}
195195

196+
private TransactionConfig BuildTransactionConfig(Action<TransactionConfigBuilder> action)
197+
{
198+
if (action == null)
199+
{
200+
return TransactionConfig.Default;
201+
}
202+
var builder = new TransactionConfigBuilder(_logger, new TransactionConfig());
203+
action.Invoke(builder);
204+
return builder.Build();
205+
}
206+
196207
public Task<T> ReadTransactionAsync<T>(
197208
Func<IAsyncTransaction, Task<T>> work,
198209
Action<TransactionConfigBuilder> action = null)

Neo4j.Driver/Neo4j.Driver/Internal/Util/ConfigBuilders.cs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,6 @@ namespace Neo4j.Driver.Internal.Util;
2121

2222
internal static class ConfigBuilders
2323
{
24-
public static TransactionConfig BuildTransactionConfig(Action<TransactionConfigBuilder> action)
25-
{
26-
TransactionConfig config;
27-
if (action == null)
28-
{
29-
config = TransactionConfig.Default;
30-
}
31-
else
32-
{
33-
var builder = TransactionConfig.Builder;
34-
action.Invoke(builder);
35-
config = builder.Build();
36-
}
37-
38-
return config;
39-
}
40-
4124
public static SessionConfig BuildSessionConfig(Action<SessionConfigBuilder> action)
4225
{
4326
SessionConfig config;

Neo4j.Driver/Neo4j.Driver/TransactionConfig.cs

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ internal TransactionConfig(IDictionary<string, object> metadata, TimeSpan? timeo
4949
_timeout = timeout;
5050
}
5151

52-
internal static TransactionConfigBuilder Builder => new(new TransactionConfig());
53-
5452
/// <summary>
5553
/// Transaction timeout. Transactions that execute longer than the configured timeout will be terminated by the
5654
/// database. This functionality allows to limit query/transaction execution time. Specified timeout overrides the default
@@ -103,10 +101,14 @@ public override string ToString()
103101
/// <summary>The builder to create a <see cref="TransactionConfig"/></summary>
104102
public sealed class TransactionConfigBuilder
105103
{
104+
private readonly ILogger _logger;
106105
private readonly TransactionConfig _config;
107106

108-
internal TransactionConfigBuilder(TransactionConfig config)
107+
internal TransactionConfigBuilder(
108+
ILogger logger,
109+
TransactionConfig config)
109110
{
111+
_logger = logger;
110112
_config = config;
111113
}
112114

@@ -115,19 +117,41 @@ internal TransactionConfigBuilder(TransactionConfig config)
115117
/// by the database. This functionality allows to limit query/transaction execution time. Specified timeout overrides the
116118
/// default timeout configured in the database using <code>dbms.transaction.timeout</code> setting. Leave this field
117119
/// unmodified to use default timeout configured on database. Setting a zero timeout will result in no timeout.
120+
/// <para/>
121+
/// If the timeout is not an exact number of milliseconds, it will be rounded up to the next millisecond.
118122
/// </summary>
119123
/// <exception cref="ArgumentOutOfRangeException">
120-
/// If the value given to transaction timeout in milliseconds is less than
121-
/// zero
124+
/// If the value given to transaction timeout in milliseconds is less than zero.
122125
/// </exception>
123-
/// <param name="timeout">the new timeout</param>
124-
/// <returns>this <see cref="TransactionConfigBuilder"/> instance</returns>
126+
/// <param name="timeout">The new timeout.</param>
127+
/// <returns>this <see cref="TransactionConfigBuilder"/> instance.</returns>
125128
public TransactionConfigBuilder WithTimeout(TimeSpan? timeout)
126129
{
127-
_config.Timeout = timeout;
130+
_config.Timeout = FixSubmilliseconds(timeout);
128131
return this;
129132
}
130133

134+
private TimeSpan? FixSubmilliseconds(TimeSpan? timeout)
135+
{
136+
if(timeout == null || timeout == TimeSpan.MaxValue)
137+
{
138+
return timeout;
139+
}
140+
141+
if (timeout.Value.Ticks % TimeSpan.TicksPerMillisecond == 0)
142+
{
143+
return timeout;
144+
}
145+
146+
var result = TimeSpan.FromMilliseconds(Math.Ceiling(timeout.Value.TotalMilliseconds));
147+
_logger?.Info(
148+
"Transaction timeout {timeout} contains sub-millisecond precision and will be rounded up to {result}.",
149+
timeout,
150+
result);
151+
152+
return result;
153+
}
154+
131155
/// <summary>
132156
/// The transaction metadata. Specified metadata will be attached to the executing transaction and visible in the
133157
/// output of <code>dbms.listQueries</code> and <code>dbms.listTransactions</code> procedures. It will also get logged to

0 commit comments

Comments
 (0)