Skip to content

Commit 4696ab8

Browse files
rstamDmitryLukyanov
authored andcommitted
CSHARP-2616: Allow applications to set maxTimeMS for commitTransaction.
1 parent 536f100 commit 4696ab8

23 files changed

+1167
-65
lines changed

src/MongoDB.Driver.Core/Core/Bindings/CoreSession.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,9 @@ private IReadOperation<BsonDocument> CreateAbortTransactionOperation()
376376

377377
private IReadOperation<BsonDocument> CreateCommitTransactionOperation(bool isCommitRetry)
378378
{
379-
return new CommitTransactionOperation(_currentTransaction.RecoveryToken, GetCommitTransactionWriteConcern(isCommitRetry));
379+
var writeConcern = GetCommitTransactionWriteConcern(isCommitRetry);
380+
var maxCommitTime = _currentTransaction.TransactionOptions.MaxCommitTime;
381+
return new CommitTransactionOperation(_currentTransaction.RecoveryToken, writeConcern) { MaxCommitTime = maxCommitTime };
380382
}
381383

382384
private void EnsureAbortTransactionCanBeCalled(string methodName)
@@ -490,7 +492,8 @@ private TransactionOptions GetEffectiveTransactionOptions(TransactionOptions tra
490492
var readConcern = transactionOptions?.ReadConcern ?? _options.DefaultTransactionOptions?.ReadConcern ?? ReadConcern.Default;
491493
var readPreference = transactionOptions?.ReadPreference ?? _options.DefaultTransactionOptions?.ReadPreference ?? ReadPreference.Primary;
492494
var writeConcern = transactionOptions?.WriteConcern ?? _options.DefaultTransactionOptions?.WriteConcern ?? new WriteConcern();
493-
return new TransactionOptions(readConcern, readPreference, writeConcern);
495+
var maxCommitTime = transactionOptions?.MaxCommitTime ?? _options.DefaultTransactionOptions?.MaxCommitTime;
496+
return new TransactionOptions(readConcern, readPreference, writeConcern, maxCommitTime);
494497
}
495498

496499
private WriteConcern GetTransactionWriteConcern()

src/MongoDB.Driver.Core/Core/Misc/ExceptionMapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static Exception Map(ConnectionId connectionId, BsonDocument response)
4444
case 13475:
4545
case 16986:
4646
case 16712:
47-
return new MongoExecutionTimeoutException(connectionId, message: "Operation exceeded time limit.");
47+
return new MongoExecutionTimeoutException(connectionId, message: "Operation exceeded time limit.", response);
4848
}
4949
}
5050

src/MongoDB.Driver.Core/Core/Operations/EndTransactionOperation.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public virtual async Task<BsonDocument> ExecuteAsync(IReadBinding binding, Cance
119119
/// Creates the command for the operation.
120120
/// </summary>
121121
/// <returns>The command.</returns>
122-
private BsonDocument CreateCommand()
122+
protected virtual BsonDocument CreateCommand()
123123
{
124124
return new BsonDocument
125125
{
@@ -175,6 +175,9 @@ public AbortTransactionOperation(WriteConcern writeConcern)
175175
/// </summary>
176176
public sealed class CommitTransactionOperation : EndTransactionOperation
177177
{
178+
// private fields
179+
private TimeSpan? _maxCommitTime;
180+
178181
// public constructors
179182
/// <summary>
180183
/// Initializes a new instance of the <see cref="AbortTransactionOperation"/> class.
@@ -195,6 +198,15 @@ public CommitTransactionOperation(BsonDocument recoveryToken, WriteConcern write
195198
{
196199
}
197200

201+
// public properties
202+
/// <summary>Gets the maximum commit time.</summary>
203+
/// <value>The maximum commit time.</value>
204+
public TimeSpan? MaxCommitTime
205+
{
206+
get => _maxCommitTime;
207+
set => _maxCommitTime = Ensure.IsNullOrGreaterThanZero(value, nameof(value));
208+
}
209+
198210
// protected properties
199211
/// <inheritdoc />
200212
protected override string CommandName => "commitTransaction";
@@ -228,6 +240,18 @@ public override async Task<BsonDocument> ExecuteAsync(IReadBinding binding, Canc
228240
}
229241
}
230242

243+
// protected methods
244+
/// <inheritdoc />
245+
protected override BsonDocument CreateCommand()
246+
{
247+
var command = base.CreateCommand();
248+
if (_maxCommitTime.HasValue)
249+
{
250+
command.Add("maxTimeMS", (long)_maxCommitTime.Value.TotalMilliseconds);
251+
}
252+
return command;
253+
}
254+
231255
// private methods
232256
private void ReplaceTransientTransactionErrorWithUnknownTransactionCommitResult(MongoException exception)
233257
{
@@ -242,7 +266,9 @@ private bool ShouldReplaceTransientTransactionErrorWithUnknownTransactionCommitR
242266
return true;
243267
}
244268

245-
if (exception is MongoNotPrimaryException || exception is MongoNodeIsRecoveringException)
269+
if (exception is MongoNotPrimaryException ||
270+
exception is MongoNodeIsRecoveringException ||
271+
exception is MongoExecutionTimeoutException) // MaxTimeMSExpired
246272
{
247273
return true;
248274
}

src/MongoDB.Driver.Core/MongoExecutionTimeoutException.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515

1616
using System;
17+
using MongoDB.Bson;
1718
#if NET452
1819
using System.Runtime.Serialization;
1920
#endif
@@ -29,6 +30,8 @@ namespace MongoDB.Driver
2930
#endif
3031
public class MongoExecutionTimeoutException : MongoServerException
3132
{
33+
private readonly BsonDocument _result;
34+
3235
// constructors
3336
/// <summary>
3437
/// Initializes a new instance of the <see cref="MongoExecutionTimeoutException"/> class.
@@ -51,6 +54,30 @@ public MongoExecutionTimeoutException(ConnectionId connectionId, string message,
5154
{
5255
}
5356

57+
/// <summary>
58+
/// Initializes a new instance of the <see cref="MongoExecutionTimeoutException"/> class.
59+
/// </summary>
60+
/// <param name="connectionId">The connection identifier.</param>
61+
/// <param name="message">The error message.</param>
62+
/// <param name="result">The command result.</param>
63+
public MongoExecutionTimeoutException(ConnectionId connectionId, string message, BsonDocument result)
64+
: this(connectionId, message, null, result)
65+
{
66+
}
67+
68+
/// <summary>
69+
/// Initializes a new instance of the <see cref="MongoExecutionTimeoutException"/> class.
70+
/// </summary>
71+
/// <param name="connectionId">The connection identifier.</param>
72+
/// <param name="message">The error message.</param>
73+
/// <param name="innerException">The inner exception.</param>
74+
/// <param name="result">The command result.</param>
75+
public MongoExecutionTimeoutException(ConnectionId connectionId, string message, Exception innerException, BsonDocument result)
76+
: base(connectionId, message, innerException)
77+
{
78+
_result = result;
79+
}
80+
5481
#if NET452
5582
/// <summary>
5683
/// Initializes a new instance of the <see cref="MongoExecutionTimeoutException"/> class.
@@ -62,5 +89,25 @@ public MongoExecutionTimeoutException(SerializationInfo info, StreamingContext c
6289
{
6390
}
6491
#endif
92+
93+
// properties
94+
/// <summary>
95+
/// Gets the error code.
96+
/// </summary>
97+
/// <value>
98+
/// The error code.
99+
/// </value>
100+
public int Code =>
101+
_result != null && _result.TryGetValue("code", out var code)
102+
? code.ToInt32()
103+
: -1;
104+
105+
/// <summary>
106+
/// Gets the name of the error code.
107+
/// </summary>
108+
/// <value>
109+
/// The name of the error code.
110+
/// </value>
111+
public string CodeName => _result?.GetValue("codeName", null)?.AsString;
65112
}
66113
}

src/MongoDB.Driver.Core/TransactionOptions.cs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@
1414
*/
1515

1616
using System;
17-
using System.Collections.Generic;
18-
using System.Linq;
19-
using System.Text;
20-
using System.Threading.Tasks;
21-
using MongoDB.Bson;
2217

2318
namespace MongoDB.Driver
2419
{
@@ -28,6 +23,7 @@ namespace MongoDB.Driver
2823
public class TransactionOptions
2924
{
3025
// private fields
26+
private readonly TimeSpan? _maxCommitTime;
3127
private readonly ReadConcern _readConcern;
3228
private readonly ReadPreference _readPreference;
3329
private readonly WriteConcern _writeConcern;
@@ -39,17 +35,28 @@ public class TransactionOptions
3935
/// <param name="readConcern">The read concern.</param>
4036
/// <param name="readPreference">The read preference.</param>
4137
/// <param name="writeConcern">The write concern.</param>
38+
/// <param name="maxCommitTime">The max commit time.</param>
4239
public TransactionOptions(
4340
Optional<ReadConcern> readConcern = default(Optional<ReadConcern>),
4441
Optional<ReadPreference> readPreference = default(Optional<ReadPreference>),
45-
Optional<WriteConcern> writeConcern = default(Optional<WriteConcern>))
42+
Optional<WriteConcern> writeConcern = default(Optional<WriteConcern>),
43+
Optional<TimeSpan?> maxCommitTime = default(Optional<TimeSpan?>))
4644
{
4745
_readConcern = readConcern.WithDefault(null);
4846
_readPreference = readPreference.WithDefault(null);
4947
_writeConcern = writeConcern.WithDefault(null);
48+
_maxCommitTime = maxCommitTime.WithDefault(null);
5049
}
5150

5251
// public properties
52+
/// <summary>
53+
/// Gets the max commit time.
54+
/// </summary>
55+
/// <value>
56+
/// The max commit time.
57+
/// </value>
58+
public TimeSpan? MaxCommitTime => _maxCommitTime;
59+
5360
/// <summary>
5461
/// Gets the read concern.
5562
/// </summary>
@@ -81,18 +88,21 @@ public TransactionOptions(
8188
/// <param name="readConcern">The new read concern.</param>
8289
/// <param name="readPreference">The read preference.</param>
8390
/// <param name="writeConcern">The new write concern.</param>
91+
/// <param name="maxCommitTime">The max commit time.</param>
8492
/// <returns>
8593
/// The new TransactionOptions.
8694
/// </returns>
8795
public TransactionOptions With(
8896
Optional<ReadConcern> readConcern = default(Optional<ReadConcern>),
8997
Optional<ReadPreference> readPreference = default(Optional<ReadPreference>),
90-
Optional<WriteConcern> writeConcern = default(Optional<WriteConcern>))
98+
Optional<WriteConcern> writeConcern = default(Optional<WriteConcern>),
99+
Optional<TimeSpan?> maxCommitTime = default(Optional<TimeSpan?>))
91100
{
92101
return new TransactionOptions(
93102
readConcern: readConcern.WithDefault(_readConcern),
94103
readPreference: readPreference.WithDefault(_readPreference),
95-
writeConcern: writeConcern.WithDefault(_writeConcern));
104+
writeConcern: writeConcern.WithDefault(_writeConcern),
105+
maxCommitTime: maxCommitTime.WithDefault(_maxCommitTime));
96106
}
97107
}
98108
}

src/MongoDB.Driver/ClientSessionHandle.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,9 @@ private TransactionOptions GetEffectiveTransactionOptions(TransactionOptions tra
166166
var readConcern = transactionOptions?.ReadConcern ?? defaultTransactionOptions?.ReadConcern ?? _client.Settings?.ReadConcern ?? ReadConcern.Default;
167167
var readPreference = transactionOptions?.ReadPreference ?? defaultTransactionOptions?.ReadPreference ?? _client.Settings?.ReadPreference ?? ReadPreference.Primary;
168168
var writeConcern = transactionOptions?.WriteConcern ?? defaultTransactionOptions?.WriteConcern ?? _client.Settings?.WriteConcern ?? new WriteConcern();
169+
var maxCommitTime = transactionOptions?.MaxCommitTime ?? defaultTransactionOptions?.MaxCommitTime;
169170

170-
return new TransactionOptions(readConcern, readPreference, writeConcern);
171+
return new TransactionOptions(readConcern, readPreference, writeConcern, maxCommitTime);
171172
}
172173
}
173174
}

src/MongoDB.Driver/TransactionExecutor.cs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ internal static class TransactionExecutor
2626
// constants
2727
private const string TransientTransactionErrorLabel = "TransientTransactionError";
2828
private const string UnknownTransactionCommitResultLabel = "UnknownTransactionCommitResult";
29+
private const int ExceededTimeLimitErrorCode = 50;
2930
private static readonly TimeSpan __transactionTimeout = TimeSpan.FromSeconds(120);
3031

3132
public static TResult ExecuteWithRetries<TResult>(
@@ -152,7 +153,7 @@ private static bool CommitWithRetries(IClientSessionHandle clientSession, DateTi
152153
catch (Exception ex)
153154
{
154155
var now = clock.UtcNow; // call UtcNow once since we need to facilitate predictable mocking
155-
if (HasErrorLabel(ex, UnknownTransactionCommitResultLabel) && !HasTimedOut(startTime, now))
156+
if (ShouldRetryCommit(ex, startTime, now))
156157
{
157158
continue;
158159
}
@@ -179,7 +180,7 @@ private static async Task<bool> CommitWithRetriesAsync(IClientSessionHandle clie
179180
catch (Exception ex)
180181
{
181182
var now = clock.UtcNow; // call UtcNow once since we need to facilitate predictable mocking
182-
if (HasErrorLabel(ex, UnknownTransactionCommitResultLabel) && !HasTimedOut(startTime, now))
183+
if (ShouldRetryCommit(ex, startTime, now))
183184
{
184185
continue;
185186
}
@@ -206,6 +207,30 @@ private static bool HasErrorLabel(Exception ex, string errorLabel)
206207
}
207208
}
208209

210+
private static bool IsExceededTimeLimitException(Exception ex)
211+
{
212+
if (ex is MongoExecutionTimeoutException timeoutException &&
213+
timeoutException.Code == ExceededTimeLimitErrorCode)
214+
{
215+
return true;
216+
}
217+
218+
if (ex is MongoWriteConcernException writeConcernException)
219+
{
220+
var writeConcernError = writeConcernException.WriteConcernResult.Response?.GetValue("writeConcernError", null)?.AsBsonDocument;
221+
if (writeConcernError != null)
222+
{
223+
var code = writeConcernError.GetValue("code", -1).ToInt32();
224+
if (code == ExceededTimeLimitErrorCode)
225+
{
226+
return true;
227+
}
228+
}
229+
}
230+
231+
return false;
232+
}
233+
209234
private static bool IsTransactionInStartingOrInProgressState(IClientSessionHandle clientSession)
210235
{
211236
var currentTransaction = clientSession.WrappedCoreSession.CurrentTransaction;
@@ -220,6 +245,14 @@ private static bool IsTransactionInStartingOrInProgressState(IClientSessionHandl
220245
}
221246
}
222247

248+
private static bool ShouldRetryCommit(Exception ex, DateTime startTime, DateTime now)
249+
{
250+
return
251+
HasErrorLabel(ex, UnknownTransactionCommitResultLabel) &&
252+
!HasTimedOut(startTime, now) &&
253+
!IsExceededTimeLimitException(ex);
254+
}
255+
223256
// nested types
224257
internal abstract class CallbackOutcome<TResult>
225258
{

tests/MongoDB.Driver.Core.TestHelpers/JsonDrivenTests/CommandStartedEventAsserter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ private void AssertCommandAspect(BsonDocument actualCommand, string name, BsonVa
100100
case "startTransaction":
101101
case "txnNumber":
102102
case "writeConcern":
103+
case "maxTimeMS":
103104
if (actualCommand.Contains(name))
104105
{
105106
throw new AssertionFailedException($"Did not expect field '{name}' in command: {actualCommand.ToJson()}.");
@@ -130,7 +131,7 @@ private void AssertCommandAspect(BsonDocument actualCommand, string name, BsonVa
130131
AdaptExpectedUpdateModels(actualValue.AsBsonArray.Cast<BsonDocument>().ToList(), expectedValue.AsBsonArray.Cast<BsonDocument>().ToList());
131132
}
132133

133-
var namesToUseOrderInsensitiveComparisonWith = new[] { "writeConcern" };
134+
var namesToUseOrderInsensitiveComparisonWith = new[] { "writeConcern", "maxTimeMS" };
134135
var useOrderInsensitiveComparison = namesToUseOrderInsensitiveComparisonWith.Contains(name);
135136

136137
if (!(useOrderInsensitiveComparison ? BsonValueEquivalencyComparer.Compare(actualValue, expectedValue) : actualValue.Equals(expectedValue)))

tests/MongoDB.Driver.Tests/JsonDrivenTests/JsonDrivenAggregateTest.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* limitations under the License.
1414
*/
1515

16+
using System;
1617
using System.Collections.Generic;
1718
using System.Linq;
1819
using System.Threading;
@@ -93,6 +94,10 @@ protected override void SetArgument(string name, BsonValue value)
9394
case "session":
9495
_session = (IClientSessionHandle)_objectMap[value.AsString];
9596
return;
97+
98+
case "maxTimeMS":
99+
_options.MaxTime = TimeSpan.FromMilliseconds(value.ToInt32());
100+
return;
96101
}
97102

98103
base.SetArgument(name, value);

tests/MongoDB.Driver.Tests/JsonDrivenTests/JsonDrivenCommandTest.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ protected override void AssertException()
5151
}
5252
}
5353

54+
if (actualErrorCodeName == null && _actualException is MongoExecutionTimeoutException mongoExecutionTimeout)
55+
{
56+
actualErrorCodeName = mongoExecutionTimeout.CodeName;
57+
}
58+
5459
if (actualErrorCodeName == null)
5560
{
5661
throw new Exception("Exception was missing \"errorCodeName\".");

0 commit comments

Comments
 (0)