@@ -22,7 +22,8 @@ class ServerImplementation implements DriftServer {
2222 /// Whether this server should close the executor after shutting down.
2323 final bool closeExecutorWhenShutdown;
2424
25- final Map <int , QueryExecutor > _managedExecutors = {};
25+ final Map <int , _ManagedExecutor > _managedExecutors = {};
26+
2627 int _currentExecutorId = 0 ;
2728 int _knownSchemaVersion = 0 ;
2829
@@ -78,7 +79,10 @@ class ServerImplementation implements DriftServer {
7879 comm.notify (ServerInfo (connection.dialect));
7980
8081 _activeChannels.add (comm);
81- return comm.closed.then ((_) => _activeChannels.remove (comm));
82+ return comm.closed.whenComplete (() {
83+ _activeChannels.remove (comm);
84+ return _abandonExecutorsOf (comm);
85+ });
8286 }
8387
8488 @override
@@ -119,11 +123,10 @@ class ServerImplementation implements DriftServer {
119123 return _handleEnsureOpen (comms, payload);
120124 } else if (payload is ExecuteQuery ) {
121125 final token = runCancellable (
122- () => _runQuery (
123- payload.method,
124- payload.sql,
125- payload.args,
126+ () => _useExecutor (
126127 payload.executorId,
128+ (executor) =>
129+ _runQuery (payload.method, payload.sql, payload.args, executor),
127130 ),
128131 );
129132 _cancellableOperations[request.id] = token;
@@ -136,7 +139,11 @@ class ServerImplementation implements DriftServer {
136139 _tableUpdateNotifications.add (payload);
137140 dispatchTableUpdateNotification (payload, comms);
138141 } else if (payload is RunNestedExecutorControl ) {
139- return _transactionControl (comms, payload.control, payload.executorId);
142+ return _useExecutor (
143+ payload.executorId,
144+ (ex) =>
145+ _transactionControl (comms, payload.control, payload.executorId, ex),
146+ );
140147 } else if (payload is RequestCancellation ) {
141148 _cancellableOperations[payload.originalRequestId]? .cancel ();
142149 return null ;
@@ -148,23 +155,24 @@ class ServerImplementation implements DriftServer {
148155 Future <ResponsePayload > _handleEnsureOpen (
149156 DriftCommunication comms,
150157 EnsureOpen open,
151- ) async {
152- final executor = await _loadExecutor (open.executorId);
153- _knownSchemaVersion = open.schemaVersion;
158+ ) {
159+ return _useExecutor (open.executorId, (executor) async {
160+ _knownSchemaVersion = open.schemaVersion;
154161
155- return PrimitiveResponsePayload .bool (
156- await executor.ensureOpen (_ServerDbUser (this , comms, open.schemaVersion)),
157- );
162+ return PrimitiveResponsePayload .bool (
163+ await executor.ensureOpen (
164+ _ServerDbUser (this , comms, open.schemaVersion),
165+ ),
166+ );
167+ });
158168 }
159169
160170 Future <ResponsePayload ?> _runQuery (
161171 StatementMethod method,
162172 String sql,
163173 List <Object ?> args,
164- int ? transactionId ,
174+ QueryExecutor executor ,
165175 ) async {
166- final executor = await _loadExecutor (transactionId);
167-
168176 // Give cancellations more time to come in
169177 await Future <void >.delayed (Duration .zero);
170178 checkIfCancelled ();
@@ -190,35 +198,58 @@ class ServerImplementation implements DriftServer {
190198 BatchedStatements stmts,
191199 int ? transactionId,
192200 ) async {
193- final executor = await _loadExecutor (transactionId);
194- await executor.runBatched (stmts);
201+ await _useExecutor (transactionId, (tx) => tx.runBatched (stmts));
195202 return null ;
196203 }
197204
198- Future <QueryExecutor > _loadExecutor (int ? transactionId) async {
199- await _waitForTurn (transactionId);
200- return transactionId != null
201- ? _managedExecutors[transactionId]!
202- : connection;
205+ Future <T > _useExecutor <T >(
206+ int ? executorId,
207+ Future <T > Function (QueryExecutor executor) block,
208+ ) {
209+ if (executorId != null ) {
210+ final managed = _managedExecutors[executorId]! ;
211+ if (managed.owner.isClosed) {
212+ throw StateError ('Owner closed' );
213+ }
214+
215+ final closeGuard = Completer <void >();
216+ managed.closeGuards.add (closeGuard.future);
217+
218+ return _waitForTurn (
219+ executorId,
220+ ).then ((_) => block (managed.executor)).whenComplete (() {
221+ managed.closeGuards.remove (closeGuard.future);
222+ closeGuard.complete ();
223+ });
224+ } else {
225+ return _waitForTurn (null ).then ((_) => block (connection));
226+ }
203227 }
204228
205- Future <int > _spawnTransaction (DriftCommunication comm, int ? executor) async {
206- final transaction = (await _loadExecutor (executor)).beginTransaction ();
207- await transaction.ensureOpen (
208- _ServerDbUser (this , comm, _knownSchemaVersion),
209- );
210- return _putExecutor (transaction, beforeCurrent: true );
229+ Future <int > _spawnTransaction (
230+ DriftCommunication comm,
231+ QueryExecutor ex,
232+ ) async {
233+ final tx = ex.beginTransaction ();
234+ await tx.ensureOpen (_ServerDbUser (this , comm, _knownSchemaVersion));
235+
236+ return _putOwnedExecutor (tx, comm);
211237 }
212238
213- Future <int > _spawnExclusive (DriftCommunication comm, int ? executor ) async {
214- final exclusive = ( await _loadExecutor (executor)) .beginExclusive ();
239+ Future <int > _spawnExclusive (DriftCommunication comm, QueryExecutor ex ) async {
240+ final exclusive = ex .beginExclusive ();
215241 await exclusive.ensureOpen (_ServerDbUser (this , comm, _knownSchemaVersion));
216- return _putExecutor (exclusive, beforeCurrent: true );
242+
243+ return _putOwnedExecutor (exclusive, comm);
217244 }
218245
219- int _putExecutor (QueryExecutor executor, {bool beforeCurrent = false }) {
246+ int _putExecutor (
247+ QueryExecutor executor,
248+ DriftCommunication owner, {
249+ bool beforeCurrent = false ,
250+ }) {
220251 final id = _currentExecutorId++ ;
221- _managedExecutors[id] = executor;
252+ _managedExecutors[id] = _ManagedExecutor ( executor, owner) ;
222253
223254 if (beforeCurrent && _executorBacklog.isNotEmpty) {
224255 _executorBacklog.insert (0 , id);
@@ -229,22 +260,32 @@ class ServerImplementation implements DriftServer {
229260 return id;
230261 }
231262
263+ int _putOwnedExecutor (QueryExecutor executor, DriftCommunication owner) {
264+ final id = _putExecutor (executor, owner, beforeCurrent: true );
265+ if (owner.isClosed) {
266+ // The client went away while this executor was waiting for its turn,
267+ // so nothing will ever commit it or roll it back.
268+ unawaited (_abandonExecutor (id));
269+ }
270+ return id;
271+ }
272+
232273 Future <ResponsePayload ?> _transactionControl (
233274 DriftCommunication comm,
234275 NestedExecutorControl action,
235276 int ? executorId,
277+ QueryExecutor executor,
236278 ) async {
237279 if (action == NestedExecutorControl .beginTransaction) {
238280 return PrimitiveResponsePayload .int (
239- await _spawnTransaction (comm, executorId ),
281+ await _spawnTransaction (comm, executor ),
240282 );
241283 } else if (action == NestedExecutorControl .startExclusive) {
242284 return PrimitiveResponsePayload .int (
243- await _spawnExclusive (comm, executorId ),
285+ await _spawnExclusive (comm, executor ),
244286 );
245287 }
246288
247- final executor = await _loadExecutor (executorId);
248289 if (action == NestedExecutorControl .endExclusive) {
249290 await executor.close ();
250291 _releaseExecutor (executorId! );
@@ -284,6 +325,39 @@ class ServerImplementation implements DriftServer {
284325 return null ;
285326 }
286327
328+ /// Rolls back the transactions and closes the exclusive executors [comm]
329+ /// opened and can no longer finish because it closed.
330+ ///
331+ /// A client that disappears mid-transaction, like a browser tab that is
332+ /// closed, otherwise leaves its executor at the head of the backlog.
333+ Future <void > _abandonExecutorsOf (DriftCommunication comm) async {
334+ await Future .wait ([
335+ for (final MapEntry (: key, : value) in _managedExecutors.entries)
336+ if (value.owner == comm) _abandonExecutor (key),
337+ ]);
338+ }
339+
340+ Future <void > _abandonExecutor (int id) async {
341+ final executor = _managedExecutors[id];
342+ if (executor == null ) return ;
343+
344+ await _waitForTurn (id);
345+ // Statements that already hold this executor finish first: rolling back
346+ // underneath one would let it run outside its transaction.
347+ while (executor.closeGuards.isNotEmpty) {
348+ await executor.closeGuards.first;
349+ }
350+
351+ try {
352+ await switch (executor.executor) {
353+ final TransactionExecutor tx => tx.rollback (),
354+ final other => other.close (),
355+ };
356+ } finally {
357+ _releaseExecutor (id);
358+ }
359+ }
360+
287361 void _releaseExecutor (int id) {
288362 _managedExecutors.remove (id);
289363 _executorBacklog.remove (id);
@@ -325,6 +399,22 @@ class ServerImplementation implements DriftServer {
325399 }
326400}
327401
402+ /// A managed executor and the channel that opened it.
403+ ///
404+ /// A client can disappear without closing its transactions - a closed browser
405+ /// tab sends no rollback and no close message - so the server has to know who
406+ /// opened an executor to end it when that client is gone. [owner] is null for
407+ /// the executors the server itself puts up, such as the one a `beforeOpen`
408+ /// callback runs on.
409+ final class _ManagedExecutor {
410+ final QueryExecutor executor;
411+ final DriftCommunication owner;
412+
413+ final Set <Future <void >> closeGuards = {};
414+
415+ _ManagedExecutor (this .executor, this .owner);
416+ }
417+
328418class _ServerDbUser implements QueryExecutorUser {
329419 final ServerImplementation _server;
330420 final DriftCommunication connection;
@@ -338,7 +428,7 @@ class _ServerDbUser implements QueryExecutorUser {
338428 QueryExecutor executor,
339429 OpeningDetails details,
340430 ) async {
341- final id = _server._putExecutor (executor, beforeCurrent: true );
431+ final id = _server._putExecutor (executor, connection, beforeCurrent: true );
342432 try {
343433 await connection.request <void >(RunBeforeOpen (details, id));
344434 } finally {
0 commit comments