Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion lib/src/adapter/adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@ abstract class Adapter {
///
/// For example, for an HTTP server adapter, this might close the underlying
/// server socket.
Future<void> 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<void> close({final bool force = false});

ConnectionsInfo get connectionsInfo;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/src/adapter/io/io_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class IOAdapter extends Adapter {
}

@override
Future<void> close() => _server.close();
Future<void> close({final bool force = false}) => _server.close(force: force);

@override
ConnectionsInfo get connectionsInfo {
Expand Down
19 changes: 12 additions & 7 deletions lib/src/relic_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ sealed class RelicServer {
Future<void> mountAndStart(final Handler handler);

/// Close the server
Future<void> 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<void> close({final bool force = false});

/// Returns information about the current connections.
Future<ConnectionsInfo> connectionsInfo();
Expand Down Expand Up @@ -65,9 +70,9 @@ final class _RelicServer implements RelicServer {
}

@override
Future<void> close() async {
Future<void> close({final bool force = false}) async {
await _stopListening();
await (await _adapter).close();
await (await _adapter).close(force: force);
_port = null;
}

Expand Down Expand Up @@ -196,8 +201,8 @@ final class _IsolatedRelicServer extends IsolatedObject<RelicServer>
: super(() => RelicServer(adapterFactory));

@override
Future<void> close() async {
await evaluate((final r) => r.close());
Future<void> close({final bool force = false}) async {
await evaluate((final r) => r.close(force: force));
await super.close();
_port = null;
}
Expand Down Expand Up @@ -230,10 +235,10 @@ final class _MultiIsolateRelicServer implements RelicServer {
);

@override
Future<void> close() async {
Future<void> 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
Expand Down
52 changes: 52 additions & 0 deletions test/relic_server_graceful_shutdown_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<http.Response?>(
(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', () {
Expand Down
2 changes: 1 addition & 1 deletion test/router/relic_app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,5 @@ class _FakeAdapter extends Fake implements Adapter {
late int port = Random().nextInt(65536);

@override
Future<void> close() async {}
Future<void> close({final bool force = false}) async {}
}