Skip to content

Commit a46581e

Browse files
committed
feat(server): implement graceful shutdown and atomic logging
Implements two key production-readiness features based on code review: 1. **Graceful Shutdown:** The `main.dart` entrypoint now listens for `SIGINT` and `SIGTERM` signals. Upon receiving a signal, it closes the HTTP server to stop accepting new connections, disposes of all application dependencies (like the database connection), and then exits cleanly. This prevents resource leaks. 2. **Atomic Logging:** The logger configuration in `main.dart` has been refactored to use a `StringBuffer` and a single `stdout.write()` call. This ensures that multi-line log entries are written to the console atomically, preventing interleaved output and improving log readability. A defensive check has also been added to `AppDependencies.dispose` to ensure it can be called safely even if initialization failed.
1 parent 758765d commit a46581e

File tree

2 files changed

+37
-10
lines changed

2 files changed

+37
-10
lines changed

bin/main.dart

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
// ignore_for_file: avoid_print
22

3+
import 'dart:async';
34
import 'dart:io';
45

56
import 'package:dart_frog/dart_frog.dart';
67
import 'package:flutter_news_app_api_server_full_source_code/src/config/app_dependencies.dart';
78
import 'package:logging/logging.dart';
89

910
// Import the generated server entrypoint to access `buildRootHandler`.
10-
import '../.dart_frog/server.dart' as server;
11+
import '../.dart_frog/server.dart' as dart_frog;
1112

1213
/// The main entrypoint for the application.
1314
///
@@ -25,20 +26,37 @@ Future<void> main(List<String> args) async {
2526
// application, as it's guaranteed to run only once at startup.
2627
Logger.root.level = Level.ALL;
2728
Logger.root.onRecord.listen((record) {
28-
// A more detailed logger that includes the error and stack trace.
29-
print(
30-
'${record.level.name}: ${record.time}: ${record.loggerName}: '
31-
'${record.message}',
32-
);
29+
final message = StringBuffer()
30+
..write('${record.level.name}: ${record.time}: ${record.loggerName}: ')
31+
..writeln(record.message);
32+
3333
if (record.error != null) {
34-
print(' ERROR: ${record.error}');
34+
message.writeln(' ERROR: ${record.error}');
3535
}
3636
if (record.stackTrace != null) {
37-
print(' STACK TRACE: ${record.stackTrace}');
37+
message.writeln(' STACK TRACE: ${record.stackTrace}');
3838
}
39+
40+
// Write the log message atomically to stdout.
41+
stdout.write(message.toString());
3942
});
4043

4144
final log = Logger('EagerEntrypoint');
45+
HttpServer? server;
46+
47+
Future<void> shutdown([String? signal]) async {
48+
log.info('Received ${signal ?? 'signal'}. Shutting down gracefully...');
49+
// Stop accepting new connections.
50+
await server?.close();
51+
// Dispose all application dependencies.
52+
await AppDependencies.instance.dispose();
53+
log.info('Shutdown complete.');
54+
exit(0);
55+
}
56+
57+
// Listen for termination signals.
58+
ProcessSignal.sigint.watch().listen((_) => shutdown('SIGINT'));
59+
ProcessSignal.sigterm.watch().listen((_) => shutdown('SIGTERM'));
4260

4361
try {
4462
log.info('EAGER_INIT: Initializing application dependencies...');
@@ -52,7 +70,10 @@ Future<void> main(List<String> args) async {
5270
// Start the server directly without the hot reload wrapper.
5371
final address = InternetAddress.anyIPv6;
5472
final port = int.tryParse(Platform.environment['PORT'] ?? '8080') ?? 8080;
55-
await serve(server.buildRootHandler(), address, port);
73+
server = await serve(dart_frog.buildRootHandler(), address, port);
74+
log.info(
75+
'Server listening on http://${server.address.host}:${server.port}',
76+
);
5677
} catch (e, s) {
5778
log.severe('EAGER_INIT: FATAL: Failed to start server.', e, s);
5879
// Exit the process if initialization fails.

lib/src/config/app_dependencies.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ class AppDependencies {
4242

4343
final _log = Logger('AppDependencies');
4444

45+
// A flag to track if initialization has started, for safe disposal.
46+
bool _initStarted = false;
47+
4548
// --- Late-initialized fields for all dependencies ---
4649

4750
// Database
@@ -80,6 +83,7 @@ class AppDependencies {
8083
/// exception if any part of the initialization fails, which will be caught
8184
/// by the entrypoint to terminate the server process.
8285
Future<void> init() async {
86+
_initStarted = true;
8387
_log.info('Initializing application dependencies...');
8488

8589
// 1. Initialize Database Connection
@@ -268,7 +272,9 @@ class AppDependencies {
268272

269273
/// Disposes of resources, such as closing the database connection.
270274
Future<void> dispose() async {
271-
await _mongoDbConnectionManager.close();
275+
if (_initStarted) {
276+
await _mongoDbConnectionManager.close();
277+
}
272278
tokenBlacklistService.dispose();
273279
rateLimitService.dispose();
274280
countryQueryService.dispose(); // Dispose the new service

0 commit comments

Comments
 (0)