Skip to content

Commit 5d58189

Browse files
committed
Support lazy connections which connect on demand with idle timeout
Add new loadLazy() method to connect only on demand and implement "idle" timeout to close underlying connection when unused. Builds on top of clue/reactphp-redis#87 and friends-of-reactphp/mysql#87 and others.
1 parent 6b77dcc commit 5d58189

File tree

8 files changed

+1050
-38
lines changed

8 files changed

+1050
-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)