Skip to content

CSHARP-3549: CSOT: Add timeoutMS to settings #1721

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

Merged
merged 4 commits into from
Jul 30, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
13 changes: 12 additions & 1 deletion src/MongoDB.Driver/AggregateOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Copyright 2015-present MongoDB Inc.
/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -34,6 +34,7 @@ public class AggregateOptions
private BsonDocument _let;
private TimeSpan? _maxAwaitTime;
private TimeSpan? _maxTime;
private TimeSpan? _timeout;
private ExpressionTranslationOptions _translationOptions;
private bool? _useCursor;

Expand Down Expand Up @@ -127,6 +128,16 @@ public TimeSpan? MaxTime
set { _maxTime = Ensure.IsNullOrInfiniteOrGreaterThanOrEqualToZero(value, nameof(value)); }
}

/// <summary>
/// Gets or sets the operation timeout.
/// </summary>
// TODO: CSOT: Make it public when CSOT will be ready for GA
internal TimeSpan? Timeout
{
get => _timeout;
set => _timeout = Ensure.IsNullOrValidTimeout(value, nameof(Timeout));
}

/// <summary>
/// Gets or sets the translation options.
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions src/MongoDB.Driver/BulkWriteOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
* limitations under the License.
*/

using System;
using MongoDB.Bson;
using MongoDB.Driver.Core.Misc;

namespace MongoDB.Driver
{
Expand All @@ -27,6 +29,7 @@ public sealed class BulkWriteOptions
private BsonValue _comment;
private bool _isOrdered;
private BsonDocument _let;
private TimeSpan? _timeout;

// constructors
/// <summary>
Expand Down Expand Up @@ -73,5 +76,15 @@ public BsonDocument Let
get { return _let; }
set { _let = value; }
}

/// <summary>
/// Gets or sets the operation timeout.
/// </summary>
// TODO: CSOT: Make it public when CSOT will be ready for GA
internal TimeSpan? Timeout
{
get => _timeout;
set => _timeout = Ensure.IsNullOrValidTimeout(value, nameof(Timeout));
}
}
}
13 changes: 12 additions & 1 deletion src/MongoDB.Driver/ChangeStreamOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Copyright 2017-present MongoDB Inc.
/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -35,6 +35,7 @@ public class ChangeStreamOptions
private bool? _showExpandedEvents;
private BsonDocument _startAfter;
private BsonTimestamp _startAtOperationTime;
private TimeSpan? _timeout;

// public properties
/// <summary>
Expand Down Expand Up @@ -166,5 +167,15 @@ public BsonTimestamp StartAtOperationTime
get { return _startAtOperationTime; }
set { _startAtOperationTime = value; }
}

/// <summary>
/// Gets or sets the operation timeout.
/// </summary>
// TODO: CSOT: Make it public when CSOT will be ready for GA
internal TimeSpan? Timeout
{
get => _timeout;
set => _timeout = Ensure.IsNullOrValidTimeout(value, nameof(Timeout));
}
}
}
14 changes: 14 additions & 0 deletions src/MongoDB.Driver/ClientBulkWriteOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
* limitations under the License.
*/

using System;
using MongoDB.Bson;
using MongoDB.Driver.Core.Misc;

namespace MongoDB.Driver
{
Expand All @@ -22,6 +24,8 @@ namespace MongoDB.Driver
/// </summary>
public sealed class ClientBulkWriteOptions
{
private TimeSpan? _timeout;

/// <summary>
/// Initializes a new instance of the <see cref="BulkWriteOptions"/> class.
/// </summary>
Expand Down Expand Up @@ -75,6 +79,16 @@ public ClientBulkWriteOptions(
/// </summary>
public BsonDocument Let { get; set; }

/// <summary>
/// Gets or sets the operation timeout.
/// </summary>
// TODO: CSOT: Make it public when CSOT will be ready for GA
internal TimeSpan? Timeout
{
get => _timeout;
set => _timeout = Ensure.IsNullOrValidTimeout(value, nameof(Timeout));
}

/// <summary>
/// Whether detailed results for each successful operation should be included in the returned results.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,22 @@
* limitations under the License.
*/

using System;
using System.Threading;
namespace MongoDB.Driver;

namespace MongoDB.Driver
internal static class ClientSessionExtensions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this file/class be called IClientSessionExtensions?

{
internal abstract record OperationOptionsBase(TimeSpan Timeout)
public static ReadPreference GetEffectiveReadPreference(this IClientSession session, ReadPreference defaultReadPreference)
{
public OperationContext ToOperationContext(CancellationToken cancellationToken)
=> new (Timeout, cancellationToken);
if (session.IsInTransaction)
{
var transactionReadPreference = session.WrappedCoreSession.CurrentTransaction.TransactionOptions?.ReadPreference;
if (transactionReadPreference != null)
{
return transactionReadPreference;
}
}

return defaultReadPreference ?? ReadPreference.Primary;
}
}

8 changes: 4 additions & 4 deletions src/MongoDB.Driver/Core/Bindings/CoreSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public bool IsInTransaction
EnsureAbortTransactionCanBeCalled(nameof(AbortTransaction));

// TODO: CSOT implement proper way to obtain the operationContext
var operationContext = new OperationContext(Timeout.InfiniteTimeSpan, cancellationToken);
var operationContext = new OperationContext(null, cancellationToken);
try
{
if (_currentTransaction.IsEmpty)
Expand Down Expand Up @@ -197,7 +197,7 @@ public bool IsInTransaction
EnsureAbortTransactionCanBeCalled(nameof(AbortTransaction));

// TODO: CSOT implement proper way to obtain the operationContext
var operationContext = new OperationContext(Timeout.InfiniteTimeSpan, cancellationToken);
var operationContext = new OperationContext(null, cancellationToken);
try
{
if (_currentTransaction.IsEmpty)
Expand Down Expand Up @@ -297,7 +297,7 @@ public long AdvanceTransactionNumber()
EnsureCommitTransactionCanBeCalled(nameof(CommitTransaction));

// TODO: CSOT implement proper way to obtain the operationContext
var operationContext = new OperationContext(Timeout.InfiniteTimeSpan, cancellationToken);
var operationContext = new OperationContext(null, cancellationToken);
try
{
_isCommitTransactionInProgress = true;
Expand Down Expand Up @@ -334,7 +334,7 @@ public long AdvanceTransactionNumber()
EnsureCommitTransactionCanBeCalled(nameof(CommitTransaction));

// TODO: CSOT implement proper way to obtain the operationContext
var operationContext = new OperationContext(Timeout.InfiniteTimeSpan, cancellationToken);
var operationContext = new OperationContext(null, cancellationToken);
try
{
_isCommitTransactionInProgress = true;
Expand Down
16 changes: 15 additions & 1 deletion src/MongoDB.Driver/Core/Configuration/ConnectionString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ public sealed class ConnectionString
private TimeSpan? _socketTimeout;
private int? _srvMaxHosts;
private string _srvServiceName;
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value
private TimeSpan? _timeout;
#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value
private bool? _tls;
private bool? _tlsDisableCertificateRevocationCheck;
private bool? _tlsInsecure;
Expand Down Expand Up @@ -399,7 +402,6 @@ public bool? RetryReads
get { return _retryReads; }
}


/// <summary>
/// Gets a value indicating whether or not to retry writes.
/// </summary>
Expand Down Expand Up @@ -468,6 +470,12 @@ public bool? Ssl
[Obsolete("Use TlsInsecure instead.")]
public bool? SslVerifyCertificate => !_tlsInsecure;

/// <summary>
/// Gets the per-operation timeout.
/// </summary>
// TODO: CSOT: Make it public when CSOT will be ready for GA
internal TimeSpan? Timeout => _timeout;

/// <summary>
/// Gets whether to use TLS.
/// </summary>
Expand Down Expand Up @@ -1089,6 +1097,12 @@ private void ParseOption(string name, string value)
var sslVerifyCertificateValue = ParseBoolean(name, value);
_tlsInsecure = EnsureTlsInsecureIsValid(!sslVerifyCertificateValue);
break;
#if DEBUG // TODO: CSOT: Make it public when CSOT will be ready for GA
case "timeout":
case "timeoutms":
_timeout = value == "0" ? System.Threading.Timeout.InfiniteTimeSpan : ParseTimeSpan(name, value);
break;
#endif
case "tlsdisablecertificaterevocationcheck":
var tlsDisableCertificateRevocationCheckValue = ParseBoolean(name, value);
_tlsDisableCertificateRevocationCheck =
Expand Down
17 changes: 9 additions & 8 deletions src/MongoDB.Driver/Core/Misc/Ensure.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Copyright 2013-present MongoDB Inc.
/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -444,11 +444,12 @@ public static string IsNullOrNotEmpty(string value, string paramName)
/// <returns>The value of the parameter.</returns>
public static TimeSpan? IsNullOrValidTimeout(TimeSpan? value, string paramName)
{
if (value != null)
if (value == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was probably no need to touch this file at all.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind leaving these changes in. I think they improve this code a bit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not...

In general I don't like making random changes to files that have nothing to do with the PR.

If we think these methods could be written in a better way we should make a ticket for it.

The reason that all methods end with return value is that in general an Ensure method might throw more than one exception, so the general pattern is:

  • check for invalid condition 1 and throw exception with suitable message
  • check for invalid condition 2 and throw exception with suitable message
  • ...
  • return value

Copy link
Member Author

@sanych-sun sanych-sun Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same story here as with previous change in the file: some how "quick returns" works better for me to understand the code. But I do not mind to revert the code.
And technically this method is kind of related to the PR: we used to have unused method Ensure.IsValidTimeout. Instead of implementing new method I've decided to adopt it and was making sure the logic is appropriate for the usage. That's the reason why I touched the file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyhow: reverted 🙂

{
IsValidTimeout(value.Value, paramName);
return null;
}
return value;

return IsValidTimeout(value.Value, paramName);
}

/// <summary>
Expand All @@ -459,12 +460,12 @@ public static string IsNullOrNotEmpty(string value, string paramName)
/// <returns>The value of the parameter.</returns>
public static TimeSpan IsValidTimeout(TimeSpan value, string paramName)
{
if (value < TimeSpan.Zero && value != Timeout.InfiniteTimeSpan)
if (value > TimeSpan.Zero || value == Timeout.InfiniteTimeSpan)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you flip the logic in this method?

All the other methods end with return value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somehow this way it's easier to understand for me. But not critical, will revert to follow other methods pattern.

{
var message = string.Format("Invalid timeout: {0}.", value);
throw new ArgumentException(message, paramName);
return value;
}
return value;

throw new ArgumentOutOfRangeException($"Invalid timeout: {value}.", paramName);
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions src/MongoDB.Driver/Core/Misc/Feature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ public void ThrowIfNotSupported(IMongoClient client, CancellationToken cancellat
{
var cluster = client.GetClusterInternal();
// TODO: CSOT implement proper way to obtain the operationContext
var operationContext = new OperationContext(Timeout.InfiniteTimeSpan, cancellationToken);
var operationContext = new OperationContext(null, cancellationToken);
using (var binding = new ReadWriteBindingHandle(new WritableServerBinding(cluster, NoCoreSession.NewHandle())))
using (var channelSource = binding.GetWriteChannelSource(operationContext))
using (var channel = channelSource.GetChannel(operationContext))
Expand All @@ -589,7 +589,7 @@ public async Task ThrowIfNotSupportedAsync(IMongoClient client, CancellationToke
{
var cluster = client.GetClusterInternal();
// TODO: CSOT implement proper way to obtain the operationContext
var operationContext = new OperationContext(Timeout.InfiniteTimeSpan, cancellationToken);
var operationContext = new OperationContext(null, cancellationToken);
using (var binding = new ReadWriteBindingHandle(new WritableServerBinding(cluster, NoCoreSession.NewHandle())))
using (var channelSource = await binding.GetWriteChannelSourceAsync(operationContext).ConfigureAwait(false))
using (var channel = await channelSource.GetChannelAsync(operationContext).ConfigureAwait(false))
Expand Down
12 changes: 6 additions & 6 deletions src/MongoDB.Driver/Core/Operations/AggregateOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ public IAsyncCursor<TResult> Execute(OperationContext operationContext, Retryabl

using (EventContext.BeginOperation())
{
var operation = CreateOperation(context);
var operation = CreateOperation(operationContext, context);
var result = operation.Execute(operationContext, context);

context.ChannelSource.Session.SetSnapshotTimeIfNeeded(result.AtClusterTime);
Expand Down Expand Up @@ -317,7 +317,7 @@ public async Task<IAsyncCursor<TResult>> ExecuteAsync(OperationContext operation

using (EventContext.BeginOperation())
{
var operation = CreateOperation(context);
var operation = CreateOperation(operationContext, context);
var result = await operation.ExecuteAsync(operationContext, context).ConfigureAwait(false);

context.ChannelSource.Session.SetSnapshotTimeIfNeeded(result.AtClusterTime);
Expand All @@ -326,15 +326,15 @@ public async Task<IAsyncCursor<TResult>> ExecuteAsync(OperationContext operation
}
}

internal BsonDocument CreateCommand(ConnectionDescription connectionDescription, ICoreSession session)
internal BsonDocument CreateCommand(OperationContext operationContext, ICoreSession session, ConnectionDescription connectionDescription)
{
var readConcern = ReadConcernHelper.GetReadConcernForCommand(session, connectionDescription, _readConcern);
var command = new BsonDocument
{
{ "aggregate", _collectionNamespace == null ? (BsonValue)1 : _collectionNamespace.CollectionName },
{ "pipeline", new BsonArray(_pipeline) },
{ "allowDiskUse", () => _allowDiskUse.Value, _allowDiskUse.HasValue },
{ "maxTimeMS", () => MaxTimeHelper.ToMaxTimeMS(_maxTime.Value), _maxTime.HasValue },
{ "maxTimeMS", () => MaxTimeHelper.ToMaxTimeMS(_maxTime.Value), _maxTime.HasValue && !operationContext.IsRootContextTimeoutConfigured() },
{ "collation", () => _collation.ToBsonDocument(), _collation != null },
{ "hint", _hint, _hint != null },
{ "let", _let, _let != null },
Expand All @@ -354,10 +354,10 @@ internal BsonDocument CreateCommand(ConnectionDescription connectionDescription,

private IDisposable BeginOperation() => EventContext.BeginOperation(null, "aggregate");

private ReadCommandOperation<AggregateResult> CreateOperation(RetryableReadContext context)
private ReadCommandOperation<AggregateResult> CreateOperation(OperationContext operationContext, RetryableReadContext context)
{
var databaseNamespace = _collectionNamespace == null ? _databaseNamespace : _collectionNamespace.DatabaseNamespace;
var command = CreateCommand(context.Channel.ConnectionDescription, context.Binding.Session);
var command = CreateCommand(operationContext, context.Binding.Session, context.Channel.ConnectionDescription);
var serializer = new AggregateResultDeserializer(_resultSerializer);
return new ReadCommandOperation<AggregateResult>(databaseNamespace, command, serializer, MessageEncoderSettings)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Copyright 2013-present MongoDB Inc.
/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -157,7 +157,7 @@ public BsonDocument Execute(OperationContext operationContext, IWriteBinding bin
using (var channel = channelSource.GetChannel(operationContext))
using (var channelBinding = new ChannelReadWriteBinding(channelSource.Server, channel, binding.Session.Fork()))
{
var operation = CreateOperation(channelBinding.Session, channel.ConnectionDescription, mayUseSecondary.EffectiveReadPreference);
var operation = CreateOperation(operationContext, channelBinding.Session, channel.ConnectionDescription, mayUseSecondary.EffectiveReadPreference);
return operation.Execute(operationContext, channelBinding);
}
}
Expand All @@ -172,12 +172,12 @@ public async Task<BsonDocument> ExecuteAsync(OperationContext operationContext,
using (var channel = await channelSource.GetChannelAsync(operationContext).ConfigureAwait(false))
using (var channelBinding = new ChannelReadWriteBinding(channelSource.Server, channel, binding.Session.Fork()))
{
var operation = CreateOperation(channelBinding.Session, channel.ConnectionDescription, mayUseSecondary.EffectiveReadPreference);
var operation = CreateOperation(operationContext, channelBinding.Session, channel.ConnectionDescription, mayUseSecondary.EffectiveReadPreference);
return await operation.ExecuteAsync(operationContext, channelBinding).ConfigureAwait(false);
}
}

public BsonDocument CreateCommand(ICoreSessionHandle session, ConnectionDescription connectionDescription)
public BsonDocument CreateCommand(OperationContext operationContext, ICoreSessionHandle session, ConnectionDescription connectionDescription)
{
var readConcern = _readConcern != null
? ReadConcernHelper.GetReadConcernForCommand(session, connectionDescription, _readConcern)
Expand All @@ -189,7 +189,7 @@ public BsonDocument CreateCommand(ICoreSessionHandle session, ConnectionDescript
{ "pipeline", new BsonArray(_pipeline) },
{ "allowDiskUse", () => _allowDiskUse.Value, _allowDiskUse.HasValue },
{ "bypassDocumentValidation", () => _bypassDocumentValidation.Value, _bypassDocumentValidation.HasValue },
{ "maxTimeMS", () => MaxTimeHelper.ToMaxTimeMS(_maxTime.Value), _maxTime.HasValue },
{ "maxTimeMS", () => MaxTimeHelper.ToMaxTimeMS(_maxTime.Value), _maxTime.HasValue && !operationContext.IsRootContextTimeoutConfigured() },
{ "collation", () => _collation.ToBsonDocument(), _collation != null },
{ "readConcern", readConcern, readConcern != null },
{ "writeConcern", writeConcern, writeConcern != null },
Expand All @@ -202,9 +202,9 @@ public BsonDocument CreateCommand(ICoreSessionHandle session, ConnectionDescript

private IDisposable BeginOperation() => EventContext.BeginOperation("aggregate");

private WriteCommandOperation<BsonDocument> CreateOperation(ICoreSessionHandle session, ConnectionDescription connectionDescription, ReadPreference effectiveReadPreference)
private WriteCommandOperation<BsonDocument> CreateOperation(OperationContext operationContext, ICoreSessionHandle session, ConnectionDescription connectionDescription, ReadPreference effectiveReadPreference)
{
var command = CreateCommand(session, connectionDescription);
var command = CreateCommand(operationContext, session, connectionDescription);
var operation = new WriteCommandOperation<BsonDocument>(_databaseNamespace, command, BsonDocumentSerializer.Instance, MessageEncoderSettings);
if (effectiveReadPreference != null)
{
Expand Down
Loading