Skip to content

Transaction.Timeout property (#129) #327

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
34 changes: 19 additions & 15 deletions Orm/Xtensive.Orm/Orm/Providers/StorageDriver.Operations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -397,26 +397,26 @@ public async ValueTask ReleaseSavepointAsync(
#region Sync Execute methods

public int ExecuteNonQuery(Session session, DbCommand command) =>
ExecuteCommand(session, command, CommandBehavior.Default, (c, cb) => c.ExecuteNonQuery());
ExecuteCommand(session, command, CommandBehavior.Default, static (c, cb) => c.ExecuteNonQuery());

public object ExecuteScalar(Session session, DbCommand command) =>
ExecuteCommand(session, command, CommandBehavior.Default, (c, cb) => c.ExecuteScalar());
ExecuteCommand(session, command, CommandBehavior.Default, static (c, cb) => c.ExecuteScalar());

public DbDataReader ExecuteReader(Session session, DbCommand command,
CommandBehavior behavior = CommandBehavior.Default) =>
ExecuteCommand(session, command, behavior, (c, cb) => c.ExecuteReader(cb));
ExecuteCommand(session, command, behavior, static (c, cb) => c.ExecuteReader(cb));

#endregion

#region Async Execute methods

public Task<int> ExecuteNonQueryAsync(Session session, DbCommand command, CancellationToken cancellationToken = default) =>
ExecuteCommandAsync(session, command, CommandBehavior.Default, cancellationToken,
(c, cb, ct) => c.ExecuteNonQueryAsync(ct));
static (c, cb, ct) => c.ExecuteNonQueryAsync(ct));

public Task<object> ExecuteScalarAsync(Session session, DbCommand command, CancellationToken cancellationToken = default) =>
ExecuteCommandAsync(session, command, CommandBehavior.Default, cancellationToken,
(c, cb, ct) => c.ExecuteScalarAsync(ct));
static (c, cb, ct) => c.ExecuteScalarAsync(ct));

public Task<DbDataReader> ExecuteReaderAsync(Session session, DbCommand command,
CancellationToken cancellationToken = default) =>
Expand All @@ -425,18 +425,27 @@ public Task<DbDataReader> ExecuteReaderAsync(Session session, DbCommand command,
public Task<DbDataReader> ExecuteReaderAsync(
Session session, DbCommand command, CommandBehavior behavior, CancellationToken cancellationToken = default) =>
ExecuteCommandAsync(session, command, behavior, cancellationToken,
(c, cb, ct) => c.ExecuteReaderAsync(cb, ct));
static (c, cb, ct) => c.ExecuteReaderAsync(cb, ct));

#endregion

private TResult ExecuteCommand<TResult>(
Session session, DbCommand command, CommandBehavior commandBehavior, Func<DbCommand, CommandBehavior, TResult> action)
private void PreDbCommandExecuting(Session session, DbCommand command, CancellationToken ct = default)
{
if (isLoggingEnabled) {
SqlLog.Info(nameof(Strings.LogSessionXQueryY), session.ToStringSafely(), command.ToHumanReadableString());
}

session?.Events.NotifyDbCommandExecuting(command);
ct.ThrowIfCancellationRequested();
if (session is not null) {
session.Transaction?.CheckForTimeout(command);
session.Events.NotifyDbCommandExecuting(command);
}
}

private TResult ExecuteCommand<TResult>(
Session session, DbCommand command, CommandBehavior commandBehavior, Func<DbCommand, CommandBehavior, TResult> action)
{
PreDbCommandExecuting(session, command);

TResult result;
try {
Expand All @@ -457,12 +466,7 @@ private async Task<TResult> ExecuteCommandAsync<TResult>(Session session,
DbCommand command, CommandBehavior commandBehavior,
CancellationToken cancellationToken, Func<DbCommand, CommandBehavior, CancellationToken, Task<TResult>> action)
{
if (isLoggingEnabled) {
SqlLog.Info(nameof(Strings.LogSessionXQueryY), session.ToStringSafely(), command.ToHumanReadableString());
}

cancellationToken.ThrowIfCancellationRequested();
session?.Events.NotifyDbCommandExecuting(command);
PreDbCommandExecuting(session, command, cancellationToken);

TResult result;
try {
Expand Down
7 changes: 2 additions & 5 deletions Orm/Xtensive.Orm/Orm/Session.Transactions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public partial class Session
{
private const string SavepointNameFormat = "s{0}";

private readonly StateLifetimeToken sessionLifetimeToken;
private readonly StateLifetimeToken sessionLifetimeToken = new StateLifetimeToken();
private readonly List<StateLifetimeToken> promotedLifetimeTokens;
private int nextSavepoint;

Expand Down Expand Up @@ -409,10 +409,7 @@ private void EnsureIsolationLevelCompatibility(IsolationLevel current, Isolation
throw new InvalidOperationException(Strings.ExCanNotReuseOpenedTransactionRequestedIsolationLevelIsDifferent);
}

private string GetNextSavepointName()
{
return string.Format(SavepointNameFormat, nextSavepoint++);
}
private string GetNextSavepointName() => $"s{nextSavepoint++}";

private void ClearChangeRegistry()
{
Expand Down
94 changes: 44 additions & 50 deletions Orm/Xtensive.Orm/Orm/Transaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -29,12 +30,7 @@ public sealed partial class Transaction : IHasExtensions
/// Gets the current <see cref="Transaction"/> object
/// using <see cref="Session"/>.<see cref="Orm.Session.Current"/>.
/// </summary>
public static Transaction Current {
get {
var session = Session.Current;
return session?.Transaction;
}
}
public static Transaction Current => Session.Current?.Transaction;

/// <summary>
/// Gets the current <see cref="Transaction"/>,
Expand All @@ -45,16 +41,8 @@ public static Transaction Current {
/// <exception cref="InvalidOperationException">
/// <see cref="Transaction.Current"/> <see cref="Transaction"/> is <see langword="null" />.
/// </exception>
public static Transaction Demand()
{
var current = Current;
if (current == null) {
throw new InvalidOperationException(
Strings.ExActiveTransactionIsRequiredForThisOperationUseSessionOpenTransactionToOpenIt);
}

return current;
}
public static Transaction Demand() =>
Current ?? throw new InvalidOperationException(Strings.ExActiveTransactionIsRequiredForThisOperationUseSessionOpenTransactionToOpenIt);

/// <summary>
/// Checks whether a transaction exists or not in the provided session.
Expand All @@ -69,58 +57,70 @@ public static void Require(Session session)

#endregion

private readonly List<StateLifetimeToken> lifetimeTokens;
private readonly List<StateLifetimeToken> lifetimeTokens = new(1);

private ExtensionCollection extensions;
private Transaction inner;

/// <summary>
/// Gets a value indicating whether this instance is automatic transaction.
/// </summary>
public bool IsAutomatic { get; private set; }
public bool IsAutomatic { get; }

/// <summary>
/// Gets a value indicating whether this instance is
/// transaction running locally.
/// </summary>
public bool IsDisconnected { get; private set; }

public bool IsDisconnected { get; }

private Guid? guid;
/// <summary>
/// Gets the unique identifier of this transaction.
/// Nested transactions have the same <see cref="Guid"/>
/// as their outermost.
/// </summary>
public Guid Guid { get; private set; }
public Guid Guid => Outer?.Guid ?? (guid ??= Guid.NewGuid());

/// <summary>
/// Gets the session this transaction is bound to.
/// </summary>
public Session Session { get; private set; }
public Session Session { get; }

/// <summary>
/// Gets the isolation level.
/// </summary>
public IsolationLevel IsolationLevel { get; private set; }
public IsolationLevel IsolationLevel { get; }

/// <summary>
/// Gets the state of the transaction.
/// </summary>
public TransactionState State { get; private set; }
public TransactionState State { get; private set; } = TransactionState.NotActivated;

/// <summary>
/// Gets the outer transaction.
/// </summary>
public Transaction Outer { get; private set; }
public Transaction Outer { get; }

/// <summary>
/// Gets the outermost transaction.
/// </summary>
public Transaction Outermost { get; private set; }
public Transaction Outermost => Outer?.Outermost ?? this;

/// <summary>
/// Gets the start time of this transaction.
/// </summary>
public DateTime TimeStamp { get; private set; }
public DateTime TimeStamp { get; } = DateTime.UtcNow;

private TimeSpan? timeout;
/// <summary>
/// Gets or sets Transaction timeout
/// </summary>
public TimeSpan? Timeout {
get => timeout;
set => timeout = IsNested
? throw new InvalidOperationException(Strings.ExNestedTransactionTimeout)
: value;
}

/// <summary>
/// Gets a value indicating whether this transaction is a nested transaction.
Expand All @@ -130,7 +130,7 @@ public static void Require(Session session)
/// <summary>
/// Gets <see cref="StateLifetimeToken"/> associated with this transaction.
/// </summary>
public StateLifetimeToken LifetimeToken { get; private set; }
public StateLifetimeToken LifetimeToken { get; private set; } = new();

#region IHasExtensions Members

Expand All @@ -139,7 +139,7 @@ public static void Require(Session session)

#endregion

internal string SavepointName { get; private set; }
internal string SavepointName { get; }

/// <summary>
/// Indicates whether changes made in this transaction are visible "as is"
Expand Down Expand Up @@ -287,40 +287,34 @@ private void ClearLifetimeTokens()
LifetimeToken = null;
}

internal void CheckForTimeout(DbCommand command)
{
if (Timeout is not null) {
var remain = TimeStamp + Timeout.Value - DateTime.UtcNow;
command.CommandTimeout = remain.Ticks > 0
? Math.Max(1, (int) remain.TotalSeconds)
: throw new TimeoutException(String.Format(Strings.ExTransactionTimeout, Timeout));
}
}

#endregion


// Constructors

internal Transaction(Session session, IsolationLevel isolationLevel, bool isAutomatic)
: this(session, isolationLevel, isAutomatic, null, null)
{
}
// Constructors

internal Transaction(Session session, IsolationLevel isolationLevel, bool isAutomatic, Transaction outer,
string savepointName)
internal Transaction(Session session, IsolationLevel isolationLevel, bool isAutomatic, Transaction outer = null,
string savepointName = null)
{
lifetimeTokens = new List<StateLifetimeToken>();

Guid = Guid.NewGuid();
State = TransactionState.NotActivated;
Session = session;
IsolationLevel = isolationLevel;
IsAutomatic = isAutomatic;
IsDisconnected = session.IsDisconnected;
TimeStamp = DateTime.UtcNow;
LifetimeToken = new StateLifetimeToken();
lifetimeTokens.Add(LifetimeToken);

if (outer != null) {
Outer = outer;
Guid = outer.Guid;
Outermost = outer.Outermost;
SavepointName = savepointName;
}
else {
Outermost = this;
}
}
}
}
}
18 changes: 18 additions & 0 deletions Orm/Xtensive.Orm/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Orm/Xtensive.Orm/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2603,4 +2603,10 @@ Error: {1}</value>
<data name="ExMaxNumberOfConditionsShouldBeBetweenXAndY" xml:space="preserve">
<value>DomainConfiguration.MaxNumberOfConditions should be between {0} and {1} (included).</value>
</data>
<data name="ExNestedTransactionTimeout" xml:space="preserve">
<value>Nested transaction cannot have timeout</value>
</data>
<data name="ExTransactionTimeout" xml:space="preserve">
<value>Transaction is longer than {0}</value>
</data>
</root>