Repository navigation
Open
Description
Version
Xtensive.Orm.SqlServer 7.2.1 and 7.2.2, SQL Server (LocalDB 16), .NET 8.
Session options: LegacyProfile | NonTransactionalReads | AutoSaveChanges.
What happens
- In a transaction, many entities are pending, and one of them has a value too long for its column.
- Still in that transaction, the constructor of another entity runs a query. The query triggers an automatic persist, which fails (
String or binary data would be truncated), so the constructor throws. - The transaction is rolled back.
- In a new transaction of the same session, any query throws:
System.NullReferenceException: Object reference not set to an instance of an object.
at Xtensive.Orm.Internals.SortingPersistActionGenerator.SortAndRemoveLoopEdges(IEnumerable`1 entityStates, Boolean rollbackDifferenceBeforeSort)
at Xtensive.Orm.Internals.SortingPersistActionGenerator.GetDeleteSequence(IEnumerable`1 entityStates)+MoveNext()
at Xtensive.Orm.Internals.SortingPersistActionGenerator.GetPersistSequence(EntityChangeRegistry registry)+MoveNext()
at Xtensive.Orm.Providers.Persister.Persist(EntityChangeRegistry registry, CommandProcessor processor)
at Xtensive.Orm.Providers.SqlSessionHandler.Persist(EntityChangeRegistry registry, Boolean allowPartialExecution)
at Xtensive.Orm.Session.Persist(PersistReason reason, Boolean isAsync, CancellationToken token)
at Xtensive.Orm.Session.Persist(PersistReason reason)
Expected: the session is usable again after the rollback.
The session stays broken: every later persist throws the same exception.
Reproduction
A console app referencing only Xtensive.Orm.SqlServer 7.2.1. Run it with an empty database.
using System;
using System.Linq;
using Xtensive.Orm;
using Xtensive.Orm.Configuration;
[HierarchyRoot]
public class Holder : Entity
{
[Field, Key] public int Id { get; private set; }
[Field(Length = 10)] public string Short { get; set; }
[Field] public Built Built { get; set; }
}
[HierarchyRoot]
public class Built : Entity
{
[Field, Key] public int Id { get; private set; }
[Field] public Holder Holder { get; set; }
public Built()
{
// Any query inside the constructor triggers the automatic persist.
_ = Session.Query.All<Holder>().Count();
}
}
public static class Program
{
public static void Main()
{
var configuration = new DomainConfiguration("sqlserver", @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=XtRepro;Integrated Security=True")
{
UpgradeMode = DomainUpgradeMode.Recreate,
};
configuration.Types.Register(typeof(Holder).Assembly);
configuration.Sessions.Add(new SessionConfiguration(WellKnown.Sessions.Default)
{
Options = SessionOptions.LegacyProfile | SessionOptions.NonTransactionalReads | SessionOptions.AutoSaveChanges,
});
using var domain = Domain.Build(configuration);
using var session = domain.OpenSession();
using (var transaction = session.OpenTransaction())
{
for (var i = 0; i < 30; i++)
_ = new Holder { Short = "ok" };
_ = new Holder { Short = new string('x', 11) };
for (var i = 0; i < 30; i++)
_ = new Holder { Short = "ok" };
try
{
_ = new Built();
}
catch (StorageException)
{
// Expected: the persist failed inside Built's constructor. The transaction is rolled back.
}
}
using (var transaction = session.OpenTransaction())
{
_ = session.Query.All<Holder>().Count(); // NullReferenceException
}
}
}What we observed while narrowing it down:
| Variant | Result |
|---|---|
| As above, 7.2.1 or 7.2.2 | NullReferenceException |
Without NonTransactionalReads |
No error |
| Without the two reference fields | No error |
| Without the 30 entities before the bad one | No error: the failing insert is deferred and runs with the query, so persistingIsFailed stays false |
After the rollback, EntityChangeRegistry still holds Built in its Removed set, with no tuple. That entity was never inserted.
Why, as far as we can read the code
Session.Persist(Session.Persist.cs, thefinallyaround line 182): withoutNonTransactionalEntityStates, a failed persist still marks theNewstates asSynchronizedand clearsEntityChangeRegistry.Entity.SystemInitializationError(Entity.cs, around line 612):Built's constructor failed, so its state is set toRemovedand invalidated.Builtis no longer in theNewset, soEntityChangeRegistry.Registeradds it to theRemovedset.Session.RollbackTransaction(Session.Transactions.cs, around line 315): the registry is cleared onlyif (!persistingIsFailed || !Configuration.Supports(SessionOptions.NonTransactionalReads)). The persist failed and the session hasNonTransactionalReads, so theRemovedstate survives the rollback.- The next persist builds the delete sequence.
SortAndRemoveLoopEdgesreads the references of that invalidated state and throws.
The two conditions disagree: the persist decides on NonTransactionalEntityStates, the rollback on NonTransactionalReads. A session with NonTransactionalReads only falls between them.
Possible fixes
- In
RollbackTransaction, testNonTransactionalEntityStateslikePersistdoes. - Or, in
SystemInitializationError, drop from the registry a state that was never persisted, instead of registering it asRemoved.
Metadata
Metadata
Assignees
Labels
No labels
Activity