SQLite + Multiple Threads

Discussion of open issues, suggestions and bugs regarding UniDAC (Universal Data Access Components) for Delphi, C++Builder, Lazarus (and FPC)
Post Reply
Stamp
Posts: 18
Joined: Mon 09 Aug 2010 21:08

SQLite + Multiple Threads

Post by Stamp » Mon 03 Jan 2011 22:15

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.

AlexP
Devart Team
Posts: 5530
Joined: Tue 10 Aug 2010 11:35

Post by AlexP » Tue 04 Jan 2011 10:30

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.

Stamp
Posts: 18
Joined: Mon 09 Aug 2010 21:08

Post by Stamp » Tue 04 Jan 2011 16:11

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]

peolsson
Posts: 11
Joined: Thu 30 Apr 2009 07:30

Post by peolsson » Tue 04 Jan 2011 21:20

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

Stamp
Posts: 18
Joined: Mon 09 Aug 2010 21:08

Post by Stamp » Tue 04 Jan 2011 22:44

I am alright with having it wait until the DB is unlocked. Perhaps a QueryTimeout specific option in the tUniConnection?

AlexP
Devart Team
Posts: 5530
Joined: Tue 10 Aug 2010 11:35

Post by AlexP » Thu 06 Jan 2011 14:21

Hello,

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

Stamp
Posts: 18
Joined: Mon 09 Aug 2010 21:08

Post by Stamp » Thu 13 Jan 2011 17:17

Do you have a timeline on when the next version may be released? Would this feature be in there? Thank you!!

AlexP
Devart Team
Posts: 5530
Joined: Tue 10 Aug 2010 11:35

Post by AlexP » Fri 14 Jan 2011 11:21

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.

Stamp
Posts: 18
Joined: Mon 09 Aug 2010 21:08

Post by Stamp » Tue 25 Jan 2011 15:30

Sounds good! Thank you for the update!

Stamp
Posts: 18
Joined: Mon 09 Aug 2010 21:08

Post by Stamp » Fri 18 Feb 2011 08:05

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!!

AlexP
Devart Team
Posts: 5530
Joined: Tue 10 Aug 2010 11:35

Post by AlexP » Fri 18 Feb 2011 14:13

Hello,

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

Stamp
Posts: 18
Joined: Mon 09 Aug 2010 21:08

Post by Stamp » Mon 21 Feb 2011 08:02

Awesome, thank you!

Jink
Posts: 1
Joined: Sun 13 Nov 2011 13:49

Post by Jink » Mon 14 Nov 2011 03:53

Hi. What was the outcome of this? If the problem was solved in an update then what is the correct approach now?

Thanks,
Jink

AlexP
Devart Team
Posts: 5530
Joined: Tue 10 Aug 2010 11:35

Post by AlexP » Wed 16 Nov 2011 14:39

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.

Post Reply