Faithlife / FaithlifeData

Helpers for querying ADO.NET-compatible databases.
https://faithlife.github.io/FaithlifeData/
MIT License
6 stars 4 forks source link

Add command timeout customization #18

Closed TyMick closed 3 years ago

TyMick commented 3 years ago

Resolves #6.

Adds the new timeout method and a new property to store the value:

public DbConnectorCommand Timeout(TimeSpan timeSpan);
public int? TimeoutLength;

Should I save the value in that property as a TimeSpan instead of an int? Testing the size of one TimeSpan instance made it look like they use more memory than an int.

I also added an overload method that takes an int instead of a TimeSpan, since it sounded like it'd be a handy alternative in case someone wants to input the value that the CommandTimeout will take. I can take it back out, though.

public DbConnectorCommand Timeout(int timeout);

Come to think of it, should I call the methods SetTimeout and the property Timeout? So the method name contains a verb?


Test-wise, I added some unit tests for the timeout values, but I could not for the life of me figure out a way to make a SQLite command actually time out. I tried writing a really slow recursive statement, but that didn't work—apparently SQLite only times out when waiting to obtain a lock on the table. Then I tried executing a second command while the slow recursive command is running...

[Test]
public async Task TimeoutAsyncTests()
{
    using var connector = CreateConnector();
    connector.Command("create table Sequence(Number integer not null);").Execute();

    string slowSqlStatement =
        @"with recursive count(n) as (select 1 union all select n+1 from count limit 50000000)
        insert into Sequence(Number) select n from count;";
    var slowStatementRun = Task.Run(() => connector.Command(slowSqlStatement).ExecuteAsync());
    Invoking(() => connector.Command("drop table Sequence;").Timeout(TimeSpan.FromSeconds(1)).Execute()).Should().Throw<SQLiteException>();
    await slowStatementRun;
}

...but the second command still waited patiently without throwing an exception. Then I tried creating a read-only database file using System.Security.AccessControl...

private static DbConnector CreateReadOnlyConnector()
{
    string fileName = Path.GetTempFileName();
    var connector = DbConnector.Create(new SQLiteConnection(fileName), new DbConnectorSettings { ProviderMethods = new SqliteProviderMethods(), AutoOpen = true, LazyOpen = true });
    var dbFile = new FileInfo(fileName);
    FileSecurity dbFileSecurity = FileSystemAclExtensions.GetAccessControl(dbFile);
    dbFileSecurity.AddAccessRule(new FileSystemAccessRule(Environment.UserName, FileSystemRights.WriteData, AccessControlType.Deny));
    FileSystemAclExtensions.SetAccessControl(dbFile, dbFileSecurity);
    return connector;
}

[Test]
public void TimeoutTests()
{
    ...
    var readOnlyConnector = CreateReadOnlyConnector();
    Invoking(() => readOnlyConnector.Command("create table Items (ItemId integer primary key, Name text not null)").Timeout(TimeSpan.FromSeconds(1)).Execute()).Should().Throw<SQLiteException>();
}

...but then the test informed me that Access Control List APIs are a Windows-only feature, and I'm on Mac. Then I tried blocking a write command by first opening a read-only FileStream on a database file...

[Test]
public void TimeoutTests()
{
    ...
    string dbFileName = Path.GetTempFileName();
    var readOnlyConnector = CreateConnector($"Data Source={dbFileName}");
    readOnlyConnector.Command("create table Items (ItemId integer primary key, Name text not null);").Execute();
    using (File.OpenRead(dbFileName))
    {
        Invoking(() => readOnlyConnector.Command("insert into Items (Name) values ('one');").Timeout(TimeSpan.FromSeconds(1)).Execute()).Should().Throw<SQLiteException>();
    }
}

...but the command simply waited forever without timing out.

So I'm going to stop banging my head against the wall. Any ideas on how to force a SQLite timeout?

TyMick commented 3 years ago

I'll go fix those new merge conflicts 👌🏼