Skip to content

Commit 4027d48

Browse files
authored
CSHARP-5708: Avoid disposal of OperationContext shared in transaction (#1760)
1 parent 05c177a commit 4027d48

File tree

12 files changed

+152
-153
lines changed

12 files changed

+152
-153
lines changed

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

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,11 @@ void ICoreSessionInternal.AbortTransaction(AbortTransactionOptions options, Canc
159159

160160
try
161161
{
162-
var firstAttempt = CreateAbortTransactionOperation();
162+
var firstAttempt = CreateAbortTransactionOperation(operationContext);
163163
ExecuteEndTransactionOnPrimary(operationContext, firstAttempt);
164164
return;
165165
}
166-
catch (Exception exception) when (ShouldRetryEndTransactionException(exception))
166+
catch (Exception exception) when (ShouldRetryEndTransactionException(operationContext, exception))
167167
{
168168
// unpin if retryable error
169169
_currentTransaction.UnpinAll();
@@ -177,7 +177,7 @@ void ICoreSessionInternal.AbortTransaction(AbortTransactionOptions options, Canc
177177

178178
try
179179
{
180-
var secondAttempt = CreateAbortTransactionOperation();
180+
var secondAttempt = CreateAbortTransactionOperation(operationContext);
181181
ExecuteEndTransactionOnPrimary(operationContext, secondAttempt);
182182
}
183183
catch
@@ -213,11 +213,11 @@ async Task ICoreSessionInternal.AbortTransactionAsync(AbortTransactionOptions op
213213

214214
try
215215
{
216-
var firstAttempt = CreateAbortTransactionOperation();
216+
var firstAttempt = CreateAbortTransactionOperation(operationContext);
217217
await ExecuteEndTransactionOnPrimaryAsync(operationContext, firstAttempt).ConfigureAwait(false);
218218
return;
219219
}
220-
catch (Exception exception) when (ShouldRetryEndTransactionException(exception))
220+
catch (Exception exception) when (ShouldRetryEndTransactionException(operationContext, exception))
221221
{
222222
// unpin if retryable error
223223
_currentTransaction.UnpinAll();
@@ -231,7 +231,7 @@ async Task ICoreSessionInternal.AbortTransactionAsync(AbortTransactionOptions op
231231

232232
try
233233
{
234-
var secondAttempt = CreateAbortTransactionOperation();
234+
var secondAttempt = CreateAbortTransactionOperation(operationContext);
235235
await ExecuteEndTransactionOnPrimaryAsync(operationContext, secondAttempt).ConfigureAwait(false);
236236
}
237237
catch
@@ -317,17 +317,17 @@ void ICoreSessionInternal.CommitTransaction(CommitTransactionOptions options, Ca
317317

318318
try
319319
{
320-
var firstAttempt = CreateCommitTransactionOperation(IsFirstCommitAttemptRetry());
320+
var firstAttempt = CreateCommitTransactionOperation(operationContext,IsFirstCommitAttemptRetry());
321321
ExecuteEndTransactionOnPrimary(operationContext, firstAttempt);
322322
return;
323323
}
324-
catch (Exception exception) when (ShouldRetryEndTransactionException(exception))
324+
catch (Exception exception) when (ShouldRetryEndTransactionException(operationContext, exception))
325325
{
326326
// unpin server if needed, then ignore exception and retry
327327
TransactionHelper.UnpinServerIfNeededOnRetryableCommitException(_currentTransaction, exception);
328328
}
329329

330-
var secondAttempt = CreateCommitTransactionOperation(isCommitRetry: true);
330+
var secondAttempt = CreateCommitTransactionOperation(operationContext,isCommitRetry: true);
331331
ExecuteEndTransactionOnPrimary(operationContext, secondAttempt);
332332
}
333333
finally
@@ -357,17 +357,17 @@ async Task ICoreSessionInternal.CommitTransactionAsync(CommitTransactionOptions
357357

358358
try
359359
{
360-
var firstAttempt = CreateCommitTransactionOperation(IsFirstCommitAttemptRetry());
360+
var firstAttempt = CreateCommitTransactionOperation(operationContext, IsFirstCommitAttemptRetry());
361361
await ExecuteEndTransactionOnPrimaryAsync(operationContext, firstAttempt).ConfigureAwait(false);
362362
return;
363363
}
364-
catch (Exception exception) when (ShouldRetryEndTransactionException(exception))
364+
catch (Exception exception) when (ShouldRetryEndTransactionException(operationContext, exception))
365365
{
366366
// unpin server if needed, then ignore exception and retry
367367
TransactionHelper.UnpinServerIfNeededOnRetryableCommitException(_currentTransaction, exception);
368368
}
369369

370-
var secondAttempt = CreateCommitTransactionOperation(isCommitRetry: true);
370+
var secondAttempt = CreateCommitTransactionOperation(operationContext, isCommitRetry: true);
371371
await ExecuteEndTransactionOnPrimaryAsync(operationContext, secondAttempt).ConfigureAwait(false);
372372
}
373373
finally
@@ -444,14 +444,14 @@ public void WasUsed()
444444
}
445445

446446
// private methods
447-
private IReadOperation<BsonDocument> CreateAbortTransactionOperation()
447+
private IReadOperation<BsonDocument> CreateAbortTransactionOperation(OperationContext operationContext)
448448
{
449-
return new AbortTransactionOperation(_currentTransaction.RecoveryToken, GetTransactionWriteConcern());
449+
return new AbortTransactionOperation(_currentTransaction.RecoveryToken, GetTransactionWriteConcern(operationContext));
450450
}
451451

452-
private IReadOperation<BsonDocument> CreateCommitTransactionOperation(bool isCommitRetry)
452+
private IReadOperation<BsonDocument> CreateCommitTransactionOperation(OperationContext operationContext, bool isCommitRetry)
453453
{
454-
var writeConcern = GetCommitTransactionWriteConcern(isCommitRetry);
454+
var writeConcern = GetCommitTransactionWriteConcern(operationContext, isCommitRetry);
455455
var maxCommitTime = _currentTransaction.TransactionOptions.MaxCommitTime;
456456
return new CommitTransactionOperation(_currentTransaction.RecoveryToken, writeConcern) { MaxCommitTime = maxCommitTime };
457457
}
@@ -587,21 +587,27 @@ private TransactionOptions GetEffectiveTransactionOptions(TransactionOptions tra
587587
return new TransactionOptions(readConcern, readPreference, writeConcern, maxCommitTime);
588588
}
589589

590-
private WriteConcern GetTransactionWriteConcern()
590+
private WriteConcern GetTransactionWriteConcern(OperationContext operationContext)
591591
{
592-
return
593-
_currentTransaction.TransactionOptions?.WriteConcern ??
594-
_options.DefaultTransactionOptions?.WriteConcern ??
595-
WriteConcern.WMajority;
592+
var writeConcern = _currentTransaction.TransactionOptions?.WriteConcern ??
593+
_options.DefaultTransactionOptions?.WriteConcern ??
594+
WriteConcern.WMajority;
595+
596+
if (operationContext.IsRootContextTimeoutConfigured())
597+
{
598+
writeConcern = writeConcern.With(wTimeout: null);
599+
}
600+
601+
return writeConcern;
596602
}
597603

598-
private WriteConcern GetCommitTransactionWriteConcern(bool isCommitRetry)
604+
private WriteConcern GetCommitTransactionWriteConcern(OperationContext operationContext, bool isCommitRetry)
599605
{
600-
var writeConcern = GetTransactionWriteConcern();
606+
var writeConcern = GetTransactionWriteConcern(operationContext);
601607
if (isCommitRetry)
602608
{
603609
writeConcern = writeConcern.With(mode: "majority");
604-
if (writeConcern.WTimeout == null)
610+
if (writeConcern.WTimeout == null && !operationContext.IsRootContextTimeoutConfigured())
605611
{
606612
writeConcern = writeConcern.With(wTimeout: TimeSpan.FromMilliseconds(10000));
607613
}
@@ -616,9 +622,14 @@ private bool IsFirstCommitAttemptRetry()
616622
return _currentTransaction.State == CoreTransactionState.Committed;
617623
}
618624

619-
private bool ShouldRetryEndTransactionException(Exception exception)
625+
private bool ShouldRetryEndTransactionException(OperationContext operationContext, Exception exception)
620626
{
621-
return RetryabilityHelper.IsRetryableWriteException(exception);
627+
if (!RetryabilityHelper.IsRetryableWriteException(exception))
628+
{
629+
return false;
630+
}
631+
632+
return operationContext.IsRootContextTimeoutConfigured() ? !operationContext.IsTimedOut() : true;
622633
}
623634
}
624635
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ namespace MongoDB.Driver.Core.Misc
1919
{
2020
internal interface IClock
2121
{
22+
long Frequency { get; }
23+
2224
DateTime UtcNow { get; }
2325

24-
IStopwatch StartStopwatch();
26+
long GetTimestamp();
2527
}
2628
}

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

Lines changed: 0 additions & 24 deletions
This file was deleted.

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515

1616
using System;
17+
using System.Diagnostics;
1718

1819
namespace MongoDB.Driver.Core.Misc
1920
{
@@ -28,11 +29,13 @@ private SystemClock()
2829
}
2930

3031
// public properties
32+
public long Frequency => Stopwatch.Frequency;
33+
3134
public DateTime UtcNow
3235
{
3336
get { return DateTime.UtcNow; }
3437
}
3538

36-
public IStopwatch StartStopwatch() => new SystemStopwatch();
39+
public long GetTimestamp() => Stopwatch.GetTimestamp();
3740
}
3841
}

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

Lines changed: 0 additions & 32 deletions
This file was deleted.

src/MongoDB.Driver/MongoClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,7 @@ private OperationContext CreateOperationContext(IClientSessionHandle session, Ti
571571
throw new InvalidOperationException("Cannot specify per operation timeout inside transaction.");
572572
}
573573

574-
return operationContext ?? new OperationContext(timeout ?? _settings.Timeout, cancellationToken);
574+
return operationContext?.Fork() ?? new OperationContext(timeout ?? _settings.Timeout, cancellationToken);
575575
}
576576

577577
private TResult ExecuteReadOperation<TResult>(IClientSessionHandle session, IReadOperation<TResult> operation, TimeSpan? timeout, CancellationToken cancellationToken)

src/MongoDB.Driver/MongoCollectionImpl.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1207,7 +1207,7 @@ private OperationContext CreateOperationContext(IClientSessionHandle session, Ti
12071207
throw new InvalidOperationException("Cannot specify per operation timeout inside transaction.");
12081208
}
12091209

1210-
return operationContext ?? new OperationContext(timeout ?? _settings.Timeout, cancellationToken);
1210+
return operationContext?.Fork() ?? new OperationContext(timeout ?? _settings.Timeout, cancellationToken);
12111211
}
12121212

12131213
private TResult ExecuteReadOperation<TResult>(IClientSessionHandle session, IReadOperation<TResult> operation, TimeSpan? timeout, CancellationToken cancellationToken)

src/MongoDB.Driver/MongoDatabase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,7 @@ private OperationContext CreateOperationContext(IClientSessionHandle session, Ti
763763
throw new InvalidOperationException("Cannot specify per operation timeout inside transaction.");
764764
}
765765

766-
return operationContext ?? new OperationContext(timeout ?? _settings.Timeout, cancellationToken);
766+
return operationContext?.Fork() ?? new OperationContext(timeout ?? _settings.Timeout, cancellationToken);
767767
}
768768

769769
private TResult ExecuteReadOperation<TResult>(IClientSessionHandle session, IReadOperation<TResult> operation, TimeSpan? timeout, CancellationToken cancellationToken)

src/MongoDB.Driver/OperationContext.cs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,19 @@ internal sealed class OperationContext : IDisposable
2929
private CancellationTokenSource _combinedCancellationTokenSource;
3030

3131
public OperationContext(TimeSpan? timeout, CancellationToken cancellationToken)
32-
: this(SystemClock.Instance, timeout, cancellationToken)
32+
: this(SystemClock.Instance, SystemClock.Instance.GetTimestamp(), timeout, cancellationToken)
3333
{
3434
}
3535

3636
internal OperationContext(IClock clock, TimeSpan? timeout, CancellationToken cancellationToken)
37+
: this(clock, clock.GetTimestamp(), timeout, cancellationToken)
38+
{
39+
}
40+
41+
internal OperationContext(IClock clock, long initialTimestamp, TimeSpan? timeout, CancellationToken cancellationToken)
3742
{
3843
Clock = Ensure.IsNotNull(clock, nameof(clock));
39-
Stopwatch = clock.StartStopwatch();
44+
InitialTimestamp = initialTimestamp;
4045
Timeout = Ensure.IsNullOrInfiniteOrGreaterThanOrEqualToZero(timeout, nameof(timeout));
4146
CancellationToken = cancellationToken;
4247
RootContext = this;
@@ -55,7 +60,7 @@ public TimeSpan RemainingTimeout
5560
return System.Threading.Timeout.InfiniteTimeSpan;
5661
}
5762

58-
var result = Timeout.Value - Stopwatch.Elapsed;
63+
var result = Timeout.Value - Elapsed;
5964
if (result < TimeSpan.Zero)
6065
{
6166
result = TimeSpan.Zero;
@@ -85,11 +90,18 @@ public CancellationToken CombinedCancellationToken
8590
return _combinedCancellationTokenSource.Token;
8691
}
8792
}
88-
private IStopwatch Stopwatch { get; }
93+
private long InitialTimestamp { get; }
8994

9095
private IClock Clock { get; }
9196

92-
public TimeSpan Elapsed => Stopwatch.Elapsed;
97+
public TimeSpan Elapsed
98+
{
99+
get
100+
{
101+
var totalSeconds = (Clock.GetTimestamp() - InitialTimestamp) / (double)Clock.Frequency;
102+
return TimeSpan.FromSeconds(totalSeconds);
103+
}
104+
}
93105

94106
public TimeSpan? Timeout { get; }
95107

@@ -99,11 +111,15 @@ public void Dispose()
99111
_combinedCancellationTokenSource?.Dispose();
100112
}
101113

102-
public bool IsTimedOut()
103-
=> RemainingTimeout == TimeSpan.Zero;
114+
public OperationContext Fork() =>
115+
new (Clock, InitialTimestamp, Timeout, CancellationToken)
116+
{
117+
RootContext = RootContext
118+
};
119+
120+
public bool IsTimedOut() => RemainingTimeout == TimeSpan.Zero;
104121

105-
public bool IsCancelledOrTimedOut()
106-
=> IsTimedOut() || CancellationToken.IsCancellationRequested;
122+
public bool IsCancelledOrTimedOut() => IsTimedOut() || CancellationToken.IsCancellationRequested;
107123

108124
public void ThrowIfTimedOutOrCanceled()
109125
{

0 commit comments

Comments
 (0)