diff --git a/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.Operations.cs b/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.Operations.cs index d3eaf5b036..bdeb6ce3e5 100644 --- a/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.Operations.cs +++ b/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.Operations.cs @@ -397,14 +397,14 @@ 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 @@ -412,11 +412,11 @@ public DbDataReader ExecuteReader(Session session, DbCommand command, public Task 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 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 ExecuteReaderAsync(Session session, DbCommand command, CancellationToken cancellationToken = default) => @@ -425,18 +425,27 @@ public Task ExecuteReaderAsync(Session session, DbCommand command, public Task 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( - Session session, DbCommand command, CommandBehavior commandBehavior, Func 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( + Session session, DbCommand command, CommandBehavior commandBehavior, Func action) + { + PreDbCommandExecuting(session, command); TResult result; try { @@ -457,12 +466,7 @@ private async Task ExecuteCommandAsync(Session session, DbCommand command, CommandBehavior commandBehavior, CancellationToken cancellationToken, Func> 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 { diff --git a/Orm/Xtensive.Orm/Orm/Session.Transactions.cs b/Orm/Xtensive.Orm/Orm/Session.Transactions.cs index a68c035d04..5108b51a0f 100644 --- a/Orm/Xtensive.Orm/Orm/Session.Transactions.cs +++ b/Orm/Xtensive.Orm/Orm/Session.Transactions.cs @@ -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 promotedLifetimeTokens; private int nextSavepoint; @@ -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() { diff --git a/Orm/Xtensive.Orm/Orm/Transaction.cs b/Orm/Xtensive.Orm/Orm/Transaction.cs index 60490b54f7..463060e9f3 100644 --- a/Orm/Xtensive.Orm/Orm/Transaction.cs +++ b/Orm/Xtensive.Orm/Orm/Transaction.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; +using System.Data.Common; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -29,12 +30,7 @@ public sealed partial class Transaction : IHasExtensions /// Gets the current object /// using .. /// - public static Transaction Current { - get { - var session = Session.Current; - return session?.Transaction; - } - } + public static Transaction Current => Session.Current?.Transaction; /// /// Gets the current , @@ -45,16 +41,8 @@ public static Transaction Current { /// /// is . /// - 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); /// /// Checks whether a transaction exists or not in the provided session. @@ -69,7 +57,7 @@ public static void Require(Session session) #endregion - private readonly List lifetimeTokens; + private readonly List lifetimeTokens = new(1); private ExtensionCollection extensions; private Transaction inner; @@ -77,50 +65,62 @@ public static void Require(Session session) /// /// Gets a value indicating whether this instance is automatic transaction. /// - public bool IsAutomatic { get; private set; } - + public bool IsAutomatic { get; } + /// /// Gets a value indicating whether this instance is /// transaction running locally. /// - public bool IsDisconnected { get; private set; } - + public bool IsDisconnected { get; } + + private Guid? guid; /// /// Gets the unique identifier of this transaction. /// Nested transactions have the same /// as their outermost. /// - public Guid Guid { get; private set; } + public Guid Guid => Outer?.Guid ?? (guid ??= Guid.NewGuid()); /// /// Gets the session this transaction is bound to. /// - public Session Session { get; private set; } + public Session Session { get; } /// /// Gets the isolation level. /// - public IsolationLevel IsolationLevel { get; private set; } + public IsolationLevel IsolationLevel { get; } /// /// Gets the state of the transaction. /// - public TransactionState State { get; private set; } + public TransactionState State { get; private set; } = TransactionState.NotActivated; /// /// Gets the outer transaction. /// - public Transaction Outer { get; private set; } + public Transaction Outer { get; } /// /// Gets the outermost transaction. /// - public Transaction Outermost { get; private set; } + public Transaction Outermost => Outer?.Outermost ?? this; /// /// Gets the start time of this transaction. /// - public DateTime TimeStamp { get; private set; } + public DateTime TimeStamp { get; } = DateTime.UtcNow; + + private TimeSpan? timeout; + /// + /// Gets or sets Transaction timeout + /// + public TimeSpan? Timeout { + get => timeout; + set => timeout = IsNested + ? throw new InvalidOperationException(Strings.ExNestedTransactionTimeout) + : value; + } /// /// Gets a value indicating whether this transaction is a nested transaction. @@ -130,7 +130,7 @@ public static void Require(Session session) /// /// Gets associated with this transaction. /// - public StateLifetimeToken LifetimeToken { get; private set; } + public StateLifetimeToken LifetimeToken { get; private set; } = new(); #region IHasExtensions Members @@ -139,7 +139,7 @@ public static void Require(Session session) #endregion - internal string SavepointName { get; private set; } + internal string SavepointName { get; } /// /// Indicates whether changes made in this transaction are visible "as is" @@ -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(); - - 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; - } } } -} +} diff --git a/Orm/Xtensive.Orm/Strings.Designer.cs b/Orm/Xtensive.Orm/Strings.Designer.cs index 284d869e30..980d79f662 100644 --- a/Orm/Xtensive.Orm/Strings.Designer.cs +++ b/Orm/Xtensive.Orm/Strings.Designer.cs @@ -3084,6 +3084,15 @@ internal static string ExNestedFieldXIsNotSupported { } } + /// + /// Looks up a localized string similar to Nested transaction cannot have timeout. + /// + internal static string ExNestedTransactionTimeout { + get { + return ResourceManager.GetString("ExNestedTransactionTimeout", resourceCulture); + } + } + /// /// Looks up a localized string similar to Comparer.Current is null.. /// @@ -4353,6 +4362,15 @@ internal static string ExTransactionShouldNotBeActive { } } + /// + /// Looks up a localized string similar to Transaction is longer than {0}. + /// + internal static string ExTransactionTimeout { + get { + return ResourceManager.GetString("ExTransactionTimeout", resourceCulture); + } + } + /// /// Looks up a localized string similar to Translation of DateOnly.ToString(string) with arbitrary arguments is not supported. Use DateOnly.ToString("s").. /// diff --git a/Orm/Xtensive.Orm/Strings.resx b/Orm/Xtensive.Orm/Strings.resx index 730c44a495..e8ff9c95b5 100644 --- a/Orm/Xtensive.Orm/Strings.resx +++ b/Orm/Xtensive.Orm/Strings.resx @@ -2603,4 +2603,10 @@ Error: {1} DomainConfiguration.MaxNumberOfConditions should be between {0} and {1} (included). + + Nested transaction cannot have timeout + + + Transaction is longer than {0} + \ No newline at end of file