diff --git a/lib/src/adapter/adapter.dart b/lib/src/adapter/adapter.dart index fe24d321..32cbd977 100644 --- a/lib/src/adapter/adapter.dart +++ b/lib/src/adapter/adapter.dart @@ -98,7 +98,12 @@ abstract class Adapter { /// /// For example, for an HTTP server adapter, this might close the underlying /// server socket. - Future close(); + /// + /// If [force] is true, the adapter will immediately close all connections + /// and shut down without waiting for in-flight requests to complete. + /// If [force] is false (the default), the adapter will wait for all + /// in-flight requests to finish before shutting down. + Future close({final bool force = false}); ConnectionsInfo get connectionsInfo; } diff --git a/lib/src/adapter/io/io_adapter.dart b/lib/src/adapter/io/io_adapter.dart index f01505b2..7ceed8bc 100644 --- a/lib/src/adapter/io/io_adapter.dart +++ b/lib/src/adapter/io/io_adapter.dart @@ -99,7 +99,7 @@ class IOAdapter extends Adapter { } @override - Future close() => _server.close(); + Future close({final bool force = false}) => _server.close(force: force); @override ConnectionsInfo get connectionsInfo { diff --git a/lib/src/relic_server.dart b/lib/src/relic_server.dart index f9198373..b73953d4 100644 --- a/lib/src/relic_server.dart +++ b/lib/src/relic_server.dart @@ -18,7 +18,12 @@ sealed class RelicServer { Future mountAndStart(final Handler handler); /// Close the server - Future close(); + /// + /// If [force] is true, the server will immediately close all connections + /// and shut down without waiting for in-flight requests to complete. + /// If [force] is false (the default), the server will wait for all + /// in-flight requests to finish before shutting down. + Future close({final bool force = false}); /// Returns information about the current connections. Future connectionsInfo(); @@ -65,9 +70,9 @@ final class _RelicServer implements RelicServer { } @override - Future close() async { + Future close({final bool force = false}) async { await _stopListening(); - await (await _adapter).close(); + await (await _adapter).close(force: force); _port = null; } @@ -196,8 +201,8 @@ final class _IsolatedRelicServer extends IsolatedObject : super(() => RelicServer(adapterFactory)); @override - Future close() async { - await evaluate((final r) => r.close()); + Future close({final bool force = false}) async { + await evaluate((final r) => r.close(force: force)); await super.close(); _port = null; } @@ -230,10 +235,10 @@ final class _MultiIsolateRelicServer implements RelicServer { ); @override - Future close() async { + Future close({final bool force = false}) async { final children = List.of(_children); _children.clear(); - await children.map((final c) => c.close()).wait; + await children.map((final c) => c.close(force: force)).wait; } @override diff --git a/test/relic_server_graceful_shutdown_test.dart b/test/relic_server_graceful_shutdown_test.dart index 918e118d..f36dabc1 100644 --- a/test/relic_server_graceful_shutdown_test.dart +++ b/test/relic_server_graceful_shutdown_test.dart @@ -231,6 +231,58 @@ void main() { expect(response.statusCode, HttpStatus.ok); } }); + + test( + 'when server.close(force: true) is called with in-flight requests, ' + 'then server shuts down immediately without waiting for requests', + () async { + final (:responseFutures, :canComplete) = await _startInFlightRequests( + server, + ); + + // Close the server forcefully while requests are in-flight + await server.close(force: true); + + // The server should close immediately, even though requests are blocked + // Now allow the requests to try to complete + canComplete.complete(); + + // Wait for all responses - they should fail or be incomplete + final results = await Future.wait( + responseFutures.map( + (final f) => f.then( + (final r) => r, + onError: (final Object e) => null, + ), + ), + ); + + // At least some requests should have failed due to forced closure + // (exact behavior may vary based on timing, but we expect errors) + final failedCount = results.where((final r) => r == null).length; + expect( + failedCount, + greaterThan(0), + reason: 'Some requests should fail when force closing', + ); + }, + ); + + test( + 'when server.close(force: true) is called on idle server, ' + 'then server closes immediately', + () async { + await server.mountAndStart( + (final req) => Response.ok(body: Body.fromString('OK')), + ); + + // Close an idle server with force + await expectLater( + server.close(force: true), + completes, + ); + }, + ); }); group('Given a RelicServer with multi-isolate configuration', () { diff --git a/test/router/relic_app_test.dart b/test/router/relic_app_test.dart index 7ff6c7f5..561cf20d 100644 --- a/test/router/relic_app_test.dart +++ b/test/router/relic_app_test.dart @@ -133,5 +133,5 @@ class _FakeAdapter extends Fake implements Adapter { late int port = Random().nextInt(65536); @override - Future close() async {} + Future close({final bool force = false}) async {} }