Skip to content

Commit 6fbb033

Browse files
authored
Merge pull request #23 from clue-labs/lazy
Add new `loadLazy()` method to connect only on demand and implement "idle" timeout to close underlying connection when unused.
2 parents 6b77dcc + 0c54e19 commit 6fbb033

File tree

8 files changed

+1168
-38
lines changed

8 files changed

+1168
-38
lines changed

README.md

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ built on top of [ReactPHP](https://reactphp.org/).
99
* [Usage](#usage)
1010
* [Factory](#factory)
1111
* [open()](#open)
12+
* [openLazy()](#openlazy)
1213
* [DatabaseInterface](#databaseinterface)
1314
* [exec()](#exec)
1415
* [query()](#query)
@@ -31,24 +32,18 @@ existing SQLite database file (or automatically create it on first run) and then
3132
$loop = React\EventLoop\Factory::create();
3233
$factory = new Clue\React\SQLite\Factory($loop);
3334

35+
$db = $factory->openLazy('users.db');
36+
$db->exec('CREATE TABLE IF NOT EXISTS foo (id INTEGER PRIMARY KEY AUTOINCREMENT, bar STRING)');
37+
3438
$name = 'Alice';
35-
$factory->open('users.db')->then(
36-
function (Clue\React\SQLite\DatabaseInterface $db) use ($name) {
37-
$db->exec('CREATE TABLE IF NOT EXISTS foo (id INTEGER PRIMARY KEY AUTOINCREMENT, bar STRING)');
38-
39-
$db->query('INSERT INTO foo (bar) VALUES (?)', array($name))->then(
40-
function (Clue\React\SQLite\Result $result) use ($name) {
41-
echo 'New ID for ' . $name . ': ' . $result->insertId . PHP_EOL;
42-
}
43-
);
44-
45-
$db->quit();
46-
},
47-
function (Exception $e) {
48-
echo 'Error: ' . $e->getMessage() . PHP_EOL;
39+
$db->query('INSERT INTO foo (bar) VALUES (?)', [$name])->then(
40+
function (Clue\React\SQLite\Result $result) use ($name) {
41+
echo 'New ID for ' . $name . ': ' . $result->insertId . PHP_EOL;
4942
}
5043
);
5144

45+
$db->quit();
46+
5247
$loop->run();
5348
```
5449

@@ -101,6 +96,75 @@ $factory->open('users.db', SQLITE3_OPEN_READONLY)->then(function (DatabaseInterf
10196
});
10297
```
10398

99+
#### openLazy()
100+
101+
The `openLazy(string $filename, int $flags = null, array $options = []): DatabaseInterface` method can be used to
102+
open a new database connection for the given SQLite database file.
103+
104+
```php
105+
$db = $factory->openLazy('users.db');
106+
107+
$db->query('INSERT INTO users (name) VALUES ("test")');
108+
$db->quit();
109+
```
110+
111+
This method immediately returns a "virtual" connection implementing the
112+
[`DatabaseInterface`](#databaseinterface) that can be used to
113+
interface with your SQLite database. Internally, it lazily creates the
114+
underlying database process only on demand once the first request is
115+
invoked on this instance and will queue all outstanding requests until
116+
the underlying database is ready. Additionally, it will only keep this
117+
underlying database in an "idle" state for 60s by default and will
118+
automatically end the underlying database when it is no longer needed.
119+
120+
From a consumer side this means that you can start sending queries to the
121+
database right away while the underlying database process may still be
122+
outstanding. Because creating this underlying process may take some
123+
time, it will enqueue all oustanding commands and will ensure that all
124+
commands will be executed in correct order once the database is ready.
125+
In other words, this "virtual" database behaves just like a "real"
126+
database as described in the `DatabaseInterface` and frees you from
127+
having to deal with its async resolution.
128+
129+
If the underlying database process fails, it will reject all
130+
outstanding commands and will return to the initial "idle" state. This
131+
means that you can keep sending additional commands at a later time which
132+
will again try to open a new underlying database. Note that this may
133+
require special care if you're using transactions that are kept open for
134+
longer than the idle period.
135+
136+
Note that creating the underlying database will be deferred until the
137+
first request is invoked. Accordingly, any eventual connection issues
138+
will be detected once this instance is first used. You can use the
139+
`quit()` method to ensure that the "virtual" connection will be soft-closed
140+
and no further commands can be enqueued. Similarly, calling `quit()` on
141+
this instance when not currently connected will succeed immediately and
142+
will not have to wait for an actual underlying connection.
143+
144+
Depending on your particular use case, you may prefer this method or the
145+
underlying `open()` method which resolves with a promise. For many
146+
simple use cases it may be easier to create a lazy connection.
147+
148+
The optional `$flags` parameter is used to determine how to open the
149+
SQLite database. By default, open uses `SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE`.
150+
151+
```php
152+
$db = $factory->openLazy('users.db', SQLITE3_OPEN_READONLY);
153+
```
154+
155+
By default, this method will keep "idle" connection open for 60s and will
156+
then end the underlying connection. The next request after an "idle"
157+
connection ended will automatically create a new underlying connection.
158+
This ensure you always get a "fresh" connection and as such should not be
159+
confused with a "keepalive" or "heartbeat" mechanism, as this will not
160+
actively try to probe the connection. You can explicitly pass a custom
161+
idle timeout value in seconds (or use a negative number to not apply a
162+
timeout) like this:
163+
164+
```php
165+
$db = $factory->openLazy('users.db', null, ['idle' => 0.1]);
166+
```
167+
104168
### DatabaseInterface
105169

106170
The `DatabaseInterface` represents a connection that is responsible for
@@ -149,7 +213,7 @@ method instead.
149213

150214
#### query()
151215

152-
The `query(string $query, array $params = array()): PromiseInterface<Result>` method can be used to
216+
The `query(string $query, array $params = []): PromiseInterface<Result>` method can be used to
153217
perform an async query.
154218

155219

examples/insert.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<?php
22

3-
use Clue\React\SQLite\DatabaseInterface;
43
use Clue\React\SQLite\Factory;
54
use Clue\React\SQLite\Result;
65

@@ -10,16 +9,17 @@
109
$factory = new Factory($loop);
1110

1211
$n = isset($argv[1]) ? $argv[1] : 1;
13-
$factory->open('test.db')->then(function (DatabaseInterface $db) use ($n) {
14-
$db->exec('CREATE TABLE IF NOT EXISTS foo (id INTEGER PRIMARY KEY AUTOINCREMENT, bar STRING)');
12+
$db = $factory->openLazy('test.db');
1513

16-
for ($i = 0; $i < $n; ++$i) {
17-
$db->exec("INSERT INTO foo (bar) VALUES ('This is a test')")->then(function (Result $result) {
18-
echo 'New row ' . $result->insertId . PHP_EOL;
19-
});
20-
}
14+
$promise = $db->exec('CREATE TABLE IF NOT EXISTS foo (id INTEGER PRIMARY KEY AUTOINCREMENT, bar STRING)');
15+
$promise->then(null, 'printf');
2116

22-
$db->quit();
23-
}, 'printf');
17+
for ($i = 0; $i < $n; ++$i) {
18+
$db->exec("INSERT INTO foo (bar) VALUES ('This is a test')")->then(function (Result $result) {
19+
echo 'New row ' . $result->insertId . PHP_EOL;
20+
});
21+
}
22+
23+
$db->quit();
2424

2525
$loop->run();

examples/search.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
$factory = new Factory($loop);
1111

1212
$search = isset($argv[1]) ? $argv[1] : 'foo';
13-
$factory->open('test.db')->then(function (DatabaseInterface $db) use ($search){
14-
$db->query('SELECT * FROM foo WHERE bar LIKE ?', ['%' . $search . '%'])->then(function (Result $result) {
15-
echo 'Found ' . count($result->rows) . ' rows: ' . PHP_EOL;
16-
echo implode("\t", $result->columns) . PHP_EOL;
17-
foreach ($result->rows as $row) {
18-
echo implode("\t", $row) . PHP_EOL;
19-
}
20-
}, 'printf');
21-
$db->quit();
13+
$db = $factory->openLazy('test.db');
14+
15+
$db->query('SELECT * FROM foo WHERE bar LIKE ?', ['%' . $search . '%'])->then(function (Result $result) {
16+
echo 'Found ' . count($result->rows) . ' rows: ' . PHP_EOL;
17+
echo implode("\t", $result->columns) . PHP_EOL;
18+
foreach ($result->rows as $row) {
19+
echo implode("\t", $row) . PHP_EOL;
20+
}
2221
}, 'printf');
22+
$db->quit();
2323

2424
$loop->run();

src/Factory.php

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
namespace Clue\React\SQLite;
44

5+
use Clue\React\SQLite\Io\LazyDatabase;
6+
use Clue\React\SQLite\Io\ProcessIoDatabase;
57
use React\ChildProcess\Process;
68
use React\EventLoop\LoopInterface;
7-
use Clue\React\SQLite\Io\ProcessIoDatabase;
8-
use React\Stream\DuplexResourceStream;
99
use React\Promise\Deferred;
10-
use React\Stream\ThroughStream;
10+
use React\Stream\DuplexResourceStream;
1111

1212
class Factory
1313
{
@@ -88,6 +88,83 @@ public function open($filename, $flags = null)
8888
return $this->useSocket ? $this->openSocketIo($filename, $flags) : $this->openProcessIo($filename, $flags);
8989
}
9090

91+
/**
92+
* Opens a new database connection for the given SQLite database file.
93+
*
94+
* ```php
95+
* $db = $factory->openLazy('users.db');
96+
*
97+
* $db->query('INSERT INTO users (name) VALUES ("test")');
98+
* $db->quit();
99+
* ```
100+
*
101+
* This method immediately returns a "virtual" connection implementing the
102+
* [`DatabaseInterface`](#databaseinterface) that can be used to
103+
* interface with your SQLite database. Internally, it lazily creates the
104+
* underlying database process only on demand once the first request is
105+
* invoked on this instance and will queue all outstanding requests until
106+
* the underlying database is ready. Additionally, it will only keep this
107+
* underlying database in an "idle" state for 60s by default and will
108+
* automatically end the underlying database when it is no longer needed.
109+
*
110+
* From a consumer side this means that you can start sending queries to the
111+
* database right away while the underlying database process may still be
112+
* outstanding. Because creating this underlying process may take some
113+
* time, it will enqueue all oustanding commands and will ensure that all
114+
* commands will be executed in correct order once the database is ready.
115+
* In other words, this "virtual" database behaves just like a "real"
116+
* database as described in the `DatabaseInterface` and frees you from
117+
* having to deal with its async resolution.
118+
*
119+
* If the underlying database process fails, it will reject all
120+
* outstanding commands and will return to the initial "idle" state. This
121+
* means that you can keep sending additional commands at a later time which
122+
* will again try to open a new underlying database. Note that this may
123+
* require special care if you're using transactions that are kept open for
124+
* longer than the idle period.
125+
*
126+
* Note that creating the underlying database will be deferred until the
127+
* first request is invoked. Accordingly, any eventual connection issues
128+
* will be detected once this instance is first used. You can use the
129+
* `quit()` method to ensure that the "virtual" connection will be soft-closed
130+
* and no further commands can be enqueued. Similarly, calling `quit()` on
131+
* this instance when not currently connected will succeed immediately and
132+
* will not have to wait for an actual underlying connection.
133+
*
134+
* Depending on your particular use case, you may prefer this method or the
135+
* underlying `open()` method which resolves with a promise. For many
136+
* simple use cases it may be easier to create a lazy connection.
137+
*
138+
* The optional `$flags` parameter is used to determine how to open the
139+
* SQLite database. By default, open uses `SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE`.
140+
*
141+
* ```php
142+
* $db = $factory->openLazy('users.db', SQLITE3_OPEN_READONLY);
143+
* ```
144+
*
145+
* By default, this method will keep "idle" connection open for 60s and will
146+
* then end the underlying connection. The next request after an "idle"
147+
* connection ended will automatically create a new underlying connection.
148+
* This ensure you always get a "fresh" connection and as such should not be
149+
* confused with a "keepalive" or "heartbeat" mechanism, as this will not
150+
* actively try to probe the connection. You can explicitly pass a custom
151+
* idle timeout value in seconds (or use a negative number to not apply a
152+
* timeout) like this:
153+
*
154+
* ```php
155+
* $db = $factory->openLazy('users.db', null, ['idle' => 0.1]);
156+
* ```
157+
*
158+
* @param string $filename
159+
* @param ?int $flags
160+
* @param array $options
161+
* @return DatabaseInterface
162+
*/
163+
public function openLazy($filename, $flags = null, array $options = [])
164+
{
165+
return new LazyDatabase($filename, $flags, $options, $this, $this->loop);
166+
}
167+
91168
private function openProcessIo($filename, $flags = null)
92169
{
93170
$command = 'exec ' . \escapeshellarg($this->bin) . ' sqlite-worker.php';

0 commit comments

Comments
 (0)