Page 1 of 1

SQLite + Multiple Threads

Posted: Mon 03 Jan 2011 22:15
by Stamp
Using latest Unidac on Delphi 2010:

I get "database is locked" when two queries are being done simultaneously on different threads. Each query has its own tUniQuery with a separate instance of tUniConnection in the same application. I've tried it with both disconnected mode and DB pooling turned on and off.

This code was working previously when using the MyDac trial, and now I am adding SQLite support with Unidac.

Thoughts?

Code: Select all

function CreateDBConnection():tUniConnection;
begin
  result := tUniConnection.create(nil);

  result.ProviderName := 'SQLite';

  if result.ProviderName = 'MySQL' then
    begin
      result.Server := 'localhost';
      result.UserName := 'user';
      result.Password := 'pass';
      result.database := 'db';
      result.Port := 3306;
    end
  else if result.ProviderName = 'SQLite' then
    begin
      result.database := GetCurrentDir() + '\db.db';


    end;

  //Tried with on and off
  result.Pooling := true;
  result.PoolingOptions.MaxPoolSize := 100;
  result.PoolingOptions.ConnectionLifetime := 30000;  //30 seconds

  //tried with on and off. Worked fine with off using MyDac.
  result.Options.DisconnectedMode := true;

  //Macros
  SetDBMacros(result);

  result.Connect;
end;

procedure FreeDBConnection(DB : tUniConnection);
begin
  tUniConnection(DB).Disconnect;
  tUniConnection(DB).free;
end;
Edit: This is used with RemObjects SDK. A call to the Remobjects SDK would spawn a new thread.

Edit2: The queries in question are parameterized, hence the multiple connection instances.

Posted: Tue 04 Jan 2011 10:30
by AlexP
Hello,

I can not reproduce the problem.
Please try to execute the following code:

Code: Select all


unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, DB, DBAccess, Uni, UniProvider, SQLiteUniProvider, MemDS, StdCtrls;

type
  TMyThread = class(TThread)
    FMyConnection: TUniConnection;
    FMyQuery: TUniQuery;
  protected
    procedure execute; override;
  published
  public
    property MyConnection: TUniConnection read FMyConnection write FMyConnection;
    property MyQuery: TUniQuery read FMyQuery write FMyQuery;
  end;

  TMyThread1 = class(TThread)
    FMyConnection: TUniConnection;
    FMyQuery: TUniQuery;
  protected
    procedure execute; override;
  published
  public
    property MyConnection: TUniConnection read FMyConnection write FMyConnection;
    property MyQuery: TUniQuery read FMyQuery write FMyQuery;
  end;


  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
    FConnection: TUniConnection;
    function CreateDBConnection:TUniConnection;
    function CreateQuery(Connection:TUniConnection):TUniQuery;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;
  MyThread1: TMyThread;
  MyThread2: TMyThread1;

implementation

{$R *.dfm}

{ TMyThread }

procedure TMyThread.execute;
begin
  FMyQuery.Open;
end;


{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  FConnection:= TUniConnection.Create(nil);
  FConnection.ProviderName := 'SQLite';
  FConnection.database := 'D:\test.db3';
  FConnection.ExecSQL('CREATE TABLE test(name VARCHAR2(50),ID NUMBER(1));', [null]);
  FConnection.Free;
  MyThread1:= TMyThread.Create(true);
  MyThread1.FMyConnection:= CreateDBConnection;
  MyThread1.FMyQuery:= CreateQuery(MyThread1.FMyConnection);
  MyThread2:= TMyThread1.Create(true);
  MyThread2.FMyConnection:= CreateDBConnection;
  MyThread2.FMyQuery:= CreateQuery(MyThread2.FMyConnection);

  MyThread1.Execute;
  MyThread2.Execute;
end;

function TForm1.CreateDBConnection: TUniConnection;
begin
  result := TUniConnection.create(nil);

  result.ProviderName := 'SQLite';
  result.database := 'D:\test.db3';
  result.Pooling := true;
  result.PoolingOptions.MaxPoolSize := 100;
  result.PoolingOptions.ConnectionLifetime := 30000;
  result.Options.DisconnectedMode := true;

  result.Connect;
end;

function TForm1.CreateQuery(Connection:TUniConnection): TUniQuery;
begin
  result := TUniQuery.create(nil);
  result.Connection:=Connection;
  result.SpecificOptions.Values['FetchAll']:= 'true';
  result.CachedUpdates:= true;
  result.SQL.Text:= 'SELECT * FROM TEST';
end;

{ TMyThread1 }

procedure TMyThread1.execute;
begin
  FMyQuery.Open;
end;

end.


if the problem doesn't arise, please modify the code to reproduce the problem, and contact us again.

Posted: Tue 04 Jan 2011 16:11
by Stamp
There are two events: Form.Oncreate, and Button.click.

Code: Select all


unit Unit1;

interface 

uses 
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 
  Dialogs, DB, DBAccess, Uni, UniProvider, SQLiteUniProvider, MemDS, StdCtrls; 

type 
  TMyThread = class(TThread) 
    FMyConnection: TUniConnection; 
    FMyQuery: TUniQuery; 
  protected 
    procedure execute; override; 
  published 
  public 
    property MyConnection: TUniConnection read FMyConnection write FMyConnection; 
    property MyQuery: TUniQuery read FMyQuery write FMyQuery; 
  end; 

  TMyThread1 = class(TThread) 
    FMyConnection: TUniConnection; 
    FMyQuery: TUniQuery; 
  protected 
    procedure execute; override; 
  published 
  public 
    property MyConnection: TUniConnection read FMyConnection write FMyConnection; 
    property MyQuery: TUniQuery read FMyQuery write FMyQuery; 
  end; 


  TForm1 = class(TForm) 
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private 
    { Private declarations } 
    FConnection: TUniConnection; 
    function CreateDBConnection:TUniConnection; 
    function CreateQuery(Connection:TUniConnection):TUniQuery; 
  public 
    { Public declarations } 
  end; 

var 
  Form1: TForm1; 
  MyThread1: TMyThread;
  MyThread2: TMyThread1;

implementation 

{$R *.dfm} 




{ TForm1 } 

procedure TForm1.Button1Click(Sender: TObject); 
begin
  MyThread1:= TMyThread.Create(true);
  MyThread1.FMyConnection:= CreateDBConnection; 
  MyThread1.FMyQuery:= CreateQuery(MyThread1.FMyConnection); 
  MyThread2:= TMyThread1.Create(true); 
  MyThread2.FMyConnection:= CreateDBConnection; 
  MyThread2.FMyQuery:= CreateQuery(MyThread2.FMyConnection); 

  MyThread2.Resume;
  MyThread1.Resume;
end; 

function TForm1.CreateDBConnection: TUniConnection; 
begin 
  result := TUniConnection.create(nil); 

  result.ProviderName := 'SQLite'; 
  result.database := 'C:\test.db3';
  result.Pooling := true; 
  result.PoolingOptions.MaxPoolSize := 100; 
  result.PoolingOptions.ConnectionLifetime := 30000; 
  result.Options.DisconnectedMode := true;

  result.Connect; 
end; 

function TForm1.CreateQuery(Connection:TUniConnection): TUniQuery; 
begin 
  result := TUniQuery.create(nil); 
  result.Connection:=Connection; 
  result.SpecificOptions.Values['FetchAll']:= 'true'; 
  result.CachedUpdates:= true;
end; 

{ TMyThread }

procedure TMyThread.execute;
var
  index : integer;
begin

  for index := 0 to 50000 do
    begin
      FMyQuery.SQL.Text:= 'SELECT * FROM thread';
      FMyQuery.Open;
    end;

  FMyQuery.Connection.free;
  FMyQuery.free;
end;

{ TMyThread1 } 

procedure TMyThread1.execute;
var
  index : integer;
begin
  FMyQuery.sql.Text := 'insert into thread1 (ID, NAME) values (:ID, :NAME)';
  FMyQuery.Prepare;
  for index := 0 to 50000 do
    begin
      FMyQuery.ParamByName('ID').AsInteger := index;
      FMyQuery.ParamByName('NAME').AsString := 'JOHN';
      FMyQuery.ExecSql;
    end;

  FMyQuery.Connection.free;
  FMyQuery.free;

end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FConnection:= TUniConnection.Create(nil); 
  FConnection.ProviderName := 'SQLite';
  FConnection.database := 'C:\test.db3';
  FConnection.ExecSQL('DROP TABLE IF EXISTS thread;', [null]);
  FConnection.ExecSQL('CREATE TABLE thread (name VARCHAR2(50),ID NUMBER(1));', [null]);

  FConnection.ExecSQL('DROP TABLE IF EXISTS thread1;', [null]);
  FConnection.ExecSQL('CREATE TABLE thread1 (name VARCHAR2(50),ID NUMBER(1));', [null]);
  FConnection.Free;
end;

end.




[/code]

Posted: Tue 04 Jan 2011 21:20
by peolsson
As far as I know this is the way SQLite works. It will handle concurrent threads, but if two threads insert at the same time this might occur. The expected way to handle this is to try again. Another workaround is of course to use a mutex in the application.

/Peter

Posted: Tue 04 Jan 2011 22:44
by Stamp
I am alright with having it wait until the DB is unlocked. Perhaps a QueryTimeout specific option in the tUniConnection?

Posted: Thu 06 Jan 2011 14:21
by AlexP
Hello,

We have reproduced the problem.
We will investigate the possibility of adding the timeout feature in one of the next builds of UniDAC.

Posted: Thu 13 Jan 2011 17:17
by Stamp
Do you have a timeline on when the next version may be released? Would this feature be in there? Thank you!!

Posted: Fri 14 Jan 2011 11:21
by AlexP
Hello,

We plan to release the next build next week. But unfortunately we have no time to include this feature to it. We plan to add this feature in the next nearest build. This feature is of high priority for us.

Posted: Tue 25 Jan 2011 15:30
by Stamp
Sounds good! Thank you for the update!

Posted: Fri 18 Feb 2011 08:05
by Stamp
Hi there! Just wondering if uniDac is on schedule to release a version with the mentioned SQLite support above. Just wondering to time my update releases! Thank you!!

Posted: Fri 18 Feb 2011 14:13
by AlexP
Hello,

We plan to release the new version of UniDAC (with support for these features) in two or three weeks.

Posted: Mon 21 Feb 2011 08:02
by Stamp
Awesome, thank you!

Posted: Mon 14 Nov 2011 03:53
by Jink
Hi. What was the outcome of this? If the problem was solved in an update then what is the correct approach now?

Thanks,
Jink

Posted: Wed 16 Nov 2011 14:39
by AlexP
Hello,

We added the EnableSharedCache property (you must set it to True) to solve the problem discussed above. Also we added the following properties for similar situations: BusyTimeout and ReadUncommitted.
Below are short description of these properties (we will add descriptions of these properties to the UniDAC help in the next product version):

ReadUncommitted
Enables or disables Read-Uncommitted isolation mode. A database connection in read-uncommitted mode does not attempt to obtain read-locks before reading from database tables as described above. This can lead to inconsistent query results if another database connection modifies a table while it is being read, but it also means that a read-transaction opened by a connection in read-uncommitted mode can neither block nor be blocked by any other connection.
Default value of this option is False.

EnableSharedCache
Enables or disables the Shared-Cache mode for SQLite database
Default value of this option is False.
This option is available for Delphi 2007 and higher IDE versions.

BusyTimeout
Defines timeout of waiting for locked resource (database or table). If resource is not unlocked during the time specified in BusyTimeout, then SQlite returns the SQLITE_BUSY error.
Default value of this option is 0.