1
1
import 'dart:async' ;
2
+ import 'dart:collection' ;
2
3
3
4
import 'mutex.dart' ;
4
5
import 'port_channel.dart' ;
@@ -12,7 +13,9 @@ import 'update_notification.dart';
12
13
class SqliteConnectionPool with SqliteQueries implements SqliteConnection {
13
14
SqliteConnection ? _writeConnection;
14
15
15
- final List <SqliteConnectionImpl > _readConnections = [];
16
+ final Set <SqliteConnectionImpl > _allReadConnections = {};
17
+ final Queue <SqliteConnectionImpl > _availableReadConnections = Queue ();
18
+ final Queue <_PendingItem > _queue = Queue ();
16
19
17
20
final SqliteOpenFactory _factory;
18
21
final SerializedPortClient _upstreamPort;
@@ -53,72 +56,84 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection {
53
56
@override
54
57
Future <bool > getAutoCommit () async {
55
58
if (_writeConnection == null ) {
56
- throw AssertionError ( 'Closed' );
59
+ throw ClosedException ( );
57
60
}
58
61
return await _writeConnection! .getAutoCommit ();
59
62
}
60
63
61
- @override
62
- Future <T > readLock <T >(Future <T > Function (SqliteReadContext tx) callback,
63
- {Duration ? lockTimeout, String ? debugContext}) async {
64
- await _expandPool ();
65
-
66
- return _runZoned (() async {
67
- bool haveLock = false ;
68
- var completer = Completer <T >();
64
+ void _nextRead () {
65
+ if (_queue.isEmpty) {
66
+ // Wait for queue item
67
+ return ;
68
+ } else if (closed) {
69
+ while (_queue.isNotEmpty) {
70
+ final nextItem = _queue.removeFirst ();
71
+ nextItem.completer.completeError (const ClosedException ());
72
+ }
73
+ return ;
74
+ }
69
75
70
- var futures = _readConnections.sublist (0 ).map ((connection) async {
71
- if (connection.closed) {
72
- _readConnections.remove (connection);
73
- }
74
- try {
75
- return await connection.readLock ((ctx) async {
76
- if (haveLock) {
77
- // Already have a different lock - release this one.
78
- return false ;
79
- }
80
- haveLock = true ;
81
-
82
- var future = callback (ctx);
83
- completer.complete (future);
84
-
85
- // We have to wait for the future to complete before we can release the
86
- // lock.
87
- try {
88
- await future;
89
- } catch (_) {
90
- // Ignore
91
- }
92
-
93
- return true ;
94
- }, lockTimeout: lockTimeout, debugContext: debugContext);
95
- } on TimeoutException {
96
- return false ;
97
- }
98
- });
76
+ while (_availableReadConnections.isNotEmpty &&
77
+ _availableReadConnections.last.closed) {
78
+ // Remove connections that may have errored
79
+ final connection = _availableReadConnections.removeLast ();
80
+ _allReadConnections.remove (connection);
81
+ }
99
82
100
- final stream = Stream <bool >.fromFutures (futures);
101
- var gotAny = await stream.any ((element) => element);
83
+ if (_availableReadConnections.isEmpty &&
84
+ _allReadConnections.length == maxReaders) {
85
+ // Wait for available connection
86
+ return ;
87
+ }
102
88
103
- if (! gotAny) {
104
- // All TimeoutExceptions
105
- throw TimeoutException ('Failed to get a read connection' , lockTimeout);
89
+ var nextItem = _queue.removeFirst ();
90
+ while (nextItem.completer.isCompleted) {
91
+ // This item already timed out - try the next one if available
92
+ if (_queue.isEmpty) {
93
+ return ;
106
94
}
95
+ nextItem = _queue.removeFirst ();
96
+ }
97
+
98
+ nextItem.lockTimer? .cancel ();
107
99
100
+ nextItem.completer.complete (Future .sync (() async {
101
+ final nextConnection = _availableReadConnections.isEmpty
102
+ ? await _expandPool ()
103
+ : _availableReadConnections.removeLast ();
108
104
try {
109
- return await completer.future;
110
- } catch (e) {
111
- // throw e;
112
- rethrow ;
105
+ // At this point the connection is expected to be available immediately.
106
+ // No need to calculate a new lockTimeout here.
107
+ final result = await nextConnection.readLock (nextItem.callback);
108
+ return result;
109
+ } finally {
110
+ _availableReadConnections.add (nextConnection);
111
+ Timer .run (_nextRead);
113
112
}
114
- }, debugContext: debugContext ?? 'get*()' );
113
+ }));
114
+ }
115
+
116
+ @override
117
+ Future <T > readLock <T >(ReadCallback <T > callback,
118
+ {Duration ? lockTimeout, String ? debugContext}) async {
119
+ if (closed) {
120
+ throw ClosedException ();
121
+ }
122
+ final zone = _getZone (debugContext: debugContext ?? 'get*()' );
123
+ final item = _PendingItem ((ctx) {
124
+ return zone.runUnary (callback, ctx);
125
+ }, lockTimeout: lockTimeout);
126
+ _queue.add (item);
127
+ _nextRead ();
128
+
129
+ return (await item.future) as T ;
115
130
}
116
131
117
132
@override
118
133
Future <T > writeLock <T >(Future <T > Function (SqliteWriteContext tx) callback,
119
134
{Duration ? lockTimeout, String ? debugContext}) {
120
135
if (closed) {
121
- throw AssertionError ( 'Closed' );
136
+ throw ClosedException ( );
122
137
}
123
138
if (_writeConnection? .closed == true ) {
124
139
_writeConnection = null ;
@@ -144,46 +159,52 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection {
144
159
/// connection (with a different lock).
145
160
/// 2. Give a more specific error message when it happens.
146
161
T _runZoned <T >(T Function () callback, {required String debugContext}) {
162
+ return _getZone (debugContext: debugContext).run (callback);
163
+ }
164
+
165
+ Zone _getZone ({required String debugContext}) {
147
166
if (Zone .current[this ] != null ) {
148
167
throw LockError (
149
168
'Recursive lock is not allowed. Use `tx.$debugContext ` instead of `db.$debugContext `.' );
150
169
}
151
- var zone = Zone .current.fork (zoneValues: {this : true });
152
- return zone.run (callback);
170
+ return Zone .current.fork (zoneValues: {this : true });
153
171
}
154
172
155
- Future <void > _expandPool () async {
156
- if (closed || _readConnections.length >= maxReaders) {
157
- return ;
158
- }
159
- bool hasCapacity = _readConnections.any ((connection) => ! connection.locked);
160
- if (! hasCapacity) {
161
- var name = debugName == null
162
- ? null
163
- : '$debugName -${_readConnections .length + 1 }' ;
164
- var connection = SqliteConnectionImpl (
165
- upstreamPort: _upstreamPort,
166
- primary: false ,
167
- updates: updates,
168
- debugName: name,
169
- mutex: mutex,
170
- readOnly: true ,
171
- openFactory: _factory);
172
- _readConnections.add (connection);
173
-
174
- // Edge case:
175
- // If we don't await here, there is a chance that a different connection
176
- // is used for the transaction, and that it finishes and deletes the database
177
- // while this one is still opening. This is specifically triggered in tests.
178
- // To avoid that, we wait for the connection to be ready.
179
- await connection.ready;
180
- }
173
+ Future <SqliteConnectionImpl > _expandPool () async {
174
+ var name = debugName == null
175
+ ? null
176
+ : '$debugName -${_allReadConnections .length + 1 }' ;
177
+ var connection = SqliteConnectionImpl (
178
+ upstreamPort: _upstreamPort,
179
+ primary: false ,
180
+ updates: updates,
181
+ debugName: name,
182
+ mutex: mutex,
183
+ readOnly: true ,
184
+ openFactory: _factory);
185
+ _allReadConnections.add (connection);
186
+
187
+ // Edge case:
188
+ // If we don't await here, there is a chance that a different connection
189
+ // is used for the transaction, and that it finishes and deletes the database
190
+ // while this one is still opening. This is specifically triggered in tests.
191
+ // To avoid that, we wait for the connection to be ready.
192
+ await connection.ready;
193
+ return connection;
181
194
}
182
195
183
196
@override
184
197
Future <void > close () async {
185
198
closed = true ;
186
- for (var connection in _readConnections) {
199
+
200
+ // It is possible that `readLock()` removes connections from the pool while we're
201
+ // closing connections, but not possible for new connections to be added.
202
+ // Create a copy of the list, to avoid this triggering "Concurrent modification during iteration"
203
+ final toClose = _allReadConnections.toList ();
204
+ for (var connection in toClose) {
205
+ // Wait for connection initialization, so that any existing readLock()
206
+ // requests go through before closing.
207
+ await connection.ready;
187
208
await connection.close ();
188
209
}
189
210
// Closing the write connection cleans up the journal files (-shm and -wal files).
@@ -192,3 +213,34 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection {
192
213
await _writeConnection? .close ();
193
214
}
194
215
}
216
+
217
+ typedef ReadCallback <T > = Future <T > Function (SqliteReadContext tx);
218
+
219
+ class _PendingItem {
220
+ ReadCallback <dynamic > callback;
221
+ Completer <dynamic > completer = Completer .sync ();
222
+ late Future <dynamic > future = completer.future;
223
+ DateTime ? deadline;
224
+ final Duration ? lockTimeout;
225
+ late final Timer ? lockTimer;
226
+
227
+ _PendingItem (this .callback, {this .lockTimeout}) {
228
+ if (lockTimeout != null ) {
229
+ deadline = DateTime .now ().add (lockTimeout! );
230
+ lockTimer = Timer (lockTimeout! , () {
231
+ // Note: isCompleted is true when `nextItem.completer.complete` is called, not when the result is available.
232
+ // This matches the behavior we need for a timeout on the lock, but not the entire operation.
233
+ if (! completer.isCompleted) {
234
+ // completer.completeError(
235
+ // TimeoutException('Failed to get a read connection', lockTimeout));
236
+ completer.complete (Future .sync (() async {
237
+ throw TimeoutException (
238
+ 'Failed to get a read connection' , lockTimeout);
239
+ }));
240
+ }
241
+ });
242
+ } else {
243
+ lockTimer = null ;
244
+ }
245
+ }
246
+ }
0 commit comments