-
Notifications
You must be signed in to change notification settings - Fork 3.5k
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
test (with sqlite db) randomly fails #19065
Comments
hi Please add your code to the test project and reshare it. Thanks. |
@maliming the test project contain these files - in the entity framework core test project |
hi I think this is an EF Core-related issue. It's fixed on 9.0.0-preview1 |
thanks for pointing that out - hopefully this will be included in an (LTS) dotnet 8 update |
hi @puschie286 A temporary solution private static SqliteConnection CreateDatabaseAndGetConnection()
{
var connection = new AbpSqliteConnection("Data Source=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<MyProjectNameDbContext>()
.UseSqlite(connection)
.Options;
using (var context = new MyProjectNameDbContext(options))
{
context.GetService<IRelationalDatabaseCreator>().CreateTables();
}
return connection;
} using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Reflection;
using Microsoft.Data.Sqlite;
namespace MyCompanyName.MyProjectName.EntityFrameworkCore;
public class AbpSqliteConnection : SqliteConnection
{
public AbpSqliteConnection(string connectionString)
: base(connectionString)
{
}
public override void Close()
{
if (State != ConnectionState.Open)
{
return;
}
Transaction?.Dispose();
var baseCommands = typeof(SqliteConnection).GetField("_commands", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(this).As<List<WeakReference<SqliteCommand>>>();
var commands = baseCommands;
for (var i = commands.Count - 1; i >= 0; i--)
{
var reference = commands[i];
if (reference.TryGetTarget(out var command))
{
// NB: Calls RemoveCommand()
command.Dispose();
}
else
{
baseCommands.Remove(reference);
}
}
Debug.Assert(baseCommands.Count == 0);
baseCommands.Clear();
var baseInnerConnection = typeof(SqliteConnection).GetField("_innerConnection", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(this);
var closeMethod = baseInnerConnection!.GetType().GetMethod("Close", BindingFlags.Public | BindingFlags.Instance);
closeMethod.Invoke(baseInnerConnection, null);
baseInnerConnection = null;
var baseState = typeof(SqliteConnection).GetField("_state", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(this).To<ConnectionState>();
baseState = ConnectionState.Closed;
var baseFromOpenToClosedEventArgs = typeof(SqliteConnection).GetField("_fromOpenToClosedEventArgs", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static).GetValue(this).As<StateChangeEventArgs>();
OnStateChange(baseFromOpenToClosedEventArgs);
}
} |
@maliming thanks for your effort Unfortunately, it does not solve the problem - i think it may be related to some other problem that cause the connection to be accessed from multiple threads at the same time in the callstack you see the call of It shows that the execution of the RemoveCommand method (probably from another thread) overlaps with the Close method execution (these are the only places remove entries from the _command list) We created a version that lock before iterating over commands in Close/RemoveCommand and this seems to prevent the problem. FixedSqlCommand using System.ComponentModel;
using System.Reflection;
namespace Microsoft.Data.Sqlite;
public class FixedSqlCommand : SqliteCommand
{
private static MethodInfo? _disposePreparedStatementsMethod;
private static FieldInfo? _connectionField;
private static FieldInfo? _eventsField;
private static FieldInfo? _eventDisposedField;
private void DisposePreparedStatements( bool disposing )
{
if( _disposePreparedStatementsMethod == null )
{
_disposePreparedStatementsMethod = typeof( SqliteCommand ).GetMethod( "DisposePreparedStatements", BindingFlags.NonPublic | BindingFlags.Instance )!;
}
_disposePreparedStatementsMethod.Invoke( this, [disposing] );
}
private FixedSqlConnection? GetConnection()
{
if( _connectionField == null )
{
_connectionField = typeof( SqliteCommand ).GetField( "_connection", BindingFlags.NonPublic | BindingFlags.Instance )!;
}
return _connectionField.GetValue( this )?.As<FixedSqlConnection>();
}
private EventHandlerList? GetEvents()
{
if( _eventsField == null )
{
_eventsField = typeof( Component ).GetField( "_events", BindingFlags.NonPublic | BindingFlags.Instance )!;
}
return _eventsField.GetValue( this )?.As<EventHandlerList>();
}
private object GetEventDisposed()
{
if( _eventDisposedField == null )
{
_eventDisposedField = typeof( Component ).GetField( "s_eventDisposed", BindingFlags.NonPublic | BindingFlags.Static )!;
}
return _eventDisposedField.GetValue( this )!;
}
private void ComponentsDispose( bool disposing )
{
if( disposing )
{
lock( this )
{
Container?.Remove( this );
EventHandlerList? events = GetEvents();
if( events != null )
{
object eventDisposed = GetEventDisposed();
( (EventHandler?)events[eventDisposed] )?.Invoke( this, EventArgs.Empty );
}
}
}
}
protected override void Dispose( bool disposing )
{
DisposePreparedStatements( disposing );
if( disposing )
{
GetConnection()?.RemoveCommand( this );
}
ComponentsDispose( disposing );
}
} FixedSqlConnection using System.Data;
using System.Diagnostics;
using System.Reflection;
namespace Microsoft.Data.Sqlite;
public class FixedSqlConnection( string connectionString ) : SqliteConnection( connectionString )
{
private static FieldInfo? _commandField;
private static FieldInfo? _innerConnectionField;
private static FieldInfo? _stateField;
private static FieldInfo? _fromOpenToClosedEventArgsField;
private static MethodInfo? _innerConnectionCloseMethod;
private List<WeakReference<SqliteCommand>> GetCommand()
{
if( _commandField == null )
{
_commandField = typeof( SqliteConnection ).GetField( "_commands", BindingFlags.NonPublic | BindingFlags.Instance )!;
}
return _commandField.GetValue( this )?.As<List<WeakReference<SqliteCommand>>>()!;
}
private void CloseInnerConnection()
{
if( _innerConnectionField == null )
{
_innerConnectionField = typeof( SqliteConnection ).GetField( "_innerConnection", BindingFlags.NonPublic | BindingFlags.Instance )!;
}
object instance = _innerConnectionField.GetValue( this )!;
if( _innerConnectionCloseMethod == null )
{
_innerConnectionCloseMethod = instance.GetType().GetMethod( "Close", BindingFlags.Public | BindingFlags.Instance )!;
}
_innerConnectionCloseMethod.Invoke( instance, null );
_innerConnectionField.SetValue( this, null );
}
private void SetCloseState( ConnectionState state )
{
if( _stateField == null )
{
_stateField = typeof( SqliteConnection ).GetField( "_state", BindingFlags.NonPublic | BindingFlags.Instance )!;
}
_stateField.SetValue( this, state );
}
private StateChangeEventArgs GetChangeEventArgs()
{
if( _fromOpenToClosedEventArgsField == null )
{
_fromOpenToClosedEventArgsField = typeof( SqliteConnection ).GetField( "_fromOpenToClosedEventArgs", BindingFlags.NonPublic | BindingFlags.Static )!;
}
return _fromOpenToClosedEventArgsField.GetValue( this )?.As<StateChangeEventArgs>()!;
}
public override void Close()
{
if( State != ConnectionState.Open )
{
return;
}
Transaction?.Dispose();
List<WeakReference<SqliteCommand>> _commands = GetCommand();
lock( this )
{
for( int i = _commands.Count - 1; i >= 0; i-- )
{
if( _commands[i].TryGetTarget( out SqliteCommand? command ) )
{
command.Dispose();
}
else
{
_commands.RemoveAt( i );
}
}
Debug.Assert( _commands.Count == 0 );
_commands.Clear();
}
CloseInnerConnection();
SetCloseState( ConnectionState.Closed );
OnStateChange( GetChangeEventArgs() );
}
public override SqliteCommand CreateCommand() => new FixedSqlCommand
{
Connection = this,
CommandTimeout = DefaultTimeout,
Transaction = Transaction
};
public void RemoveCommand( FixedSqlCommand command )
{
List<WeakReference<SqliteCommand>> _commands = GetCommand();
lock( this )
{
for( int i = _commands.Count - 1; i >= 0; i-- )
{
if( _commands[i].TryGetTarget( out SqliteCommand? item ) && item == command )
{
_commands.RemoveAt( i );
}
}
}
}
} |
I copied their latest code. This means that EF Core didn't fix the problem at all. |
yes - the question is if this is related to the test setup in abp or dotnet implementation of some other function |
Our unit tests are concurrent, but sqlite may have a problem with that. We can have the unit tests run sequentially in our project rather than concurrently, but it's best if sqlite is compatible with this case. |
I will make some compatibility code. |
hi @puschie286 Can you test the code below? It works on using System.Threading;
using Microsoft.Data.Sqlite;
using Volo.Abp.Threading;
namespace RandomTestFail.EntityFrameworkCore;
public class AbpSqliteConnection : SqliteConnection
{
public AbpSqliteConnection(string connectionString)
: base(connectionString)
{
}
public override SqliteCommand CreateCommand()
{
var command = new AbpSqliteCommand();
command.Connection = this;
command.CommandTimeout = DefaultTimeout;
command.Transaction = Transaction;
return command;
}
}
public class AbpSqliteCommand : SqliteCommand
{
private static readonly SemaphoreSlim SyncSemaphore = new SemaphoreSlim(1, 1);
public override SqliteConnection? Connection
{
get => base.Connection;
set
{
using (SyncSemaphore.Lock())
{
base.Connection = value;
}
}
}
protected override void Dispose(bool disposing)
{
using (SyncSemaphore.Lock())
{
base.Dispose(disposing);
}
}
} |
@maliming hi, yes, seems to work |
ok, I will add it to the framework. |
Is there an existing issue for this?
Description
We notice that some test randomly fail with "Index was out of range" exception - same like here. Sometimes it happen right away, after a few (5) iterations or only after more than 1k iterations.
The exception state that the sqlite connection is shared between multiple test in some rare cases or that some operations are still running after the test is done and the connection is reused in the next test.
We use a permission test for investigation and notice some "strange" behavior:
Exception:
Permission Test:
Custom Seeder: (Identical to PermissionDataSeeder except using GetListAsync without names parameter)
Reproduction Steps
Expected behavior
Endless clean runs
Actual behavior
Test throw exception after a few run
Regression?
No response
Known Workarounds
With seeding enabled and disposing of sqlite connection after application shutdown has called (OnPostApplicationShutdown) seems to prevent this problem (1k runs without fail)
Version
8.0.3
User Interface
MVC
Database Provider
EF Core (Default)
Tiered or separate authentication server
None (Default)
Operation System
Windows (Default)
Other information
Test project:
RandomTestFail.zip
The text was updated successfully, but these errors were encountered: