Skip to content

Commit ad2d541

Browse files
Add graceful shutdown configuration and handler enhancements
- Introduced `ShutdownConfig` class to encapsulate graceful shutdown settings, including timeout and enabled status. - Updated `ApplicationConfig` to include `shutdown_config` for managing shutdown behavior. - Enhanced `GracefulShutdownHandler` to accept timeout parameters and manage shutdown timing. - Modified `PySpringApplication` to utilize the new shutdown configuration during initialization and shutdown processes.
1 parent 0f49572 commit ad2d541

File tree

4 files changed

+105
-5
lines changed

4 files changed

+105
-5
lines changed

py_spring_core/commons/config_file_template_generator/templates.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"properties_file_path": "./application-properties.json",
77
"loguru_config": {"log_file_path": "./logs/app.log", "log_level": "DEBUG"},
88
"type_checking_mode": "strict",
9+
"shutdown_config": {"timeout_seconds": 30.0, "enabled": True},
910
}
1011

1112
app_properties_template: dict[str, Any] = {}

py_spring_core/core/application/application_config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ class ServerConfig(BaseModel):
2121
enabled: bool = Field(default=True)
2222

2323

24+
class ShutdownConfig(BaseModel):
25+
"""
26+
Represents the configuration for graceful shutdown.
27+
28+
Attributes:
29+
timeout_seconds: The maximum time in seconds to wait for graceful shutdown before forcing termination.
30+
enabled: A boolean flag indicating whether graceful shutdown timeout is enabled.
31+
"""
32+
33+
timeout_seconds: float = Field(default=30.0, description="Timeout in seconds for graceful shutdown")
34+
enabled: bool = Field(default=True, description="Whether graceful shutdown timeout is enabled")
35+
36+
2437
class ApplicationConfig(BaseModel):
2538
"""
2639
Represents the configuration for the application.
@@ -31,6 +44,7 @@ class ApplicationConfig(BaseModel):
3144
sqlalchemy_database_uri: The URI for the SQLAlchemy database connection.
3245
properties_file_path: The file path for the application properties.
3346
model_file_postfix_patterns: A list of file name patterns for model (for table creation) files.
47+
shutdown_config: The configuration for graceful shutdown.
3448
"""
3549

3650
model_config = ConfigDict(protected_namespaces=())
@@ -40,6 +54,7 @@ class ApplicationConfig(BaseModel):
4054
server_config: ServerConfig
4155
properties_file_path: str
4256
loguru_config: LoguruConfig
57+
shutdown_config: ShutdownConfig = Field(default_factory=ShutdownConfig)
4358

4459

4560
class ApplicationConfigRepository(JsonConfigRepository[ApplicationConfig]):

py_spring_core/core/application/py_spring_application.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
import os
3-
from typing import Any, Callable, Iterable, Type
3+
from typing import Any, Callable, Iterable, Optional, Type
44

55
import uvicorn
66
from fastapi import APIRouter, FastAPI
@@ -106,6 +106,7 @@ def __init__(
106106
self.type_checking_service = TypeCheckingService(
107107
self.app_config.app_src_target_dir
108108
)
109+
self.shutdown_handler: Optional[GracefulShutdownHandler] = None
109110

110111
def __configure_logging(self):
111112
"""Applies the logging configuration using Loguru."""
@@ -292,10 +293,19 @@ def __init_middlewares(self) -> None:
292293
def __init_graceful_shutdown(self) -> None:
293294
handler_type = GracefulShutdownHandler.__name__
294295
logger.debug(f"[{handler_type} INIT] Initialize graceful shutdown...")
295-
handler_cls = self._init_external_handler(GracefulShutdownHandler)
296+
handler_cls: Optional[Type[GracefulShutdownHandler]] = self._init_external_handler(GracefulShutdownHandler)
296297
if handler_cls is None:
297298
return
298-
handler_cls()
299+
300+
# Get shutdown configuration
301+
shutdown_config = self.app_config.shutdown_config
302+
logger.debug(f"[{handler_type} INIT] Shutdown timeout: {shutdown_config.timeout_seconds}s, enabled: {shutdown_config.enabled}")
303+
304+
# Initialize handler with timeout configuration
305+
self.shutdown_handler = handler_cls(
306+
timeout_seconds=shutdown_config.timeout_seconds,
307+
timeout_enabled=shutdown_config.enabled
308+
)
299309
logger.debug(f"[{handler_type} INIT] Graceful shutdown initialized")
300310

301311
def __configure_uvicorn_logging(self):
@@ -344,4 +354,10 @@ def run(self) -> None:
344354
if self.app_config.server_config.enabled:
345355
self.__run_server()
346356
finally:
357+
# Handle component lifecycle destruction
347358
self._handle_singleton_components_life_cycle(ComponentLifeCycle.Destruction)
359+
# Handle graceful shutdown completion
360+
if self.shutdown_handler:
361+
self.shutdown_handler.complete_shutdown()
362+
363+

py_spring_core/core/interfaces/graceful_shutdown_handler.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
from abc import ABC, abstractmethod
44
from enum import Enum, auto
5+
import os
56
import signal
67
import threading
8+
import time
79
from types import FrameType
810
from typing import Optional
911

12+
from loguru import logger
13+
1014
from py_spring_core.core.interfaces.single_inheritance_required import SingleInheritanceRequired
1115

1216
class ShutdownType(Enum):
@@ -22,9 +26,13 @@ class GracefulShutdownHandler(SingleInheritanceRequired, ABC):
2226
A mixin class that provides a method to handle graceful shutdown.
2327
"""
2428

25-
def __init__(self) -> None:
29+
def __init__(self, timeout_seconds: float, timeout_enabled: bool) -> None:
2630
self._shutdown_event = threading.Event()
2731
self._shutdown_type: Optional[ShutdownType] = None
32+
self._timeout_seconds = timeout_seconds
33+
self._timeout_enabled = timeout_enabled
34+
self._timeout_timer: Optional[threading.Timer] = None
35+
self._shutdown_start_time: Optional[float] = None
2836

2937
signal.signal(signal.SIGINT, self._handle_sigint)
3038
signal.signal(signal.SIGTERM, self._handle_sigterm)
@@ -34,6 +42,7 @@ def _handle_sigint(self, signum: int, frame: Optional[FrameType]) -> None:
3442
print("[Signal] SIGINT received")
3543
self._shutdown_type = ShutdownType.MANUAL
3644
self._shutdown_event.set()
45+
self._start_shutdown_timer()
3746
self.on_shutdown(ShutdownType.MANUAL)
3847
except Exception as error:
3948
self.on_error(error)
@@ -43,17 +52,76 @@ def _handle_sigterm(self, signum: int, frame: Optional[FrameType]) -> None:
4352
print("[Signal] SIGTERM received")
4453
self._shutdown_type = ShutdownType.SIGTERM
4554
self._shutdown_event.set()
55+
self._start_shutdown_timer()
4656
self.on_shutdown(ShutdownType.SIGTERM)
4757
except Exception as error:
4858
self.on_error(error)
49-
59+
60+
def _start_shutdown_timer(self) -> None:
61+
"""Start the shutdown timeout timer if enabled."""
62+
if not self._timeout_enabled:
63+
return
64+
65+
self._shutdown_start_time = time.time()
66+
logger.info(f"[Shutdown Timer] Starting shutdown timer for {self._timeout_seconds} seconds")
67+
68+
self._timeout_timer = threading.Timer(self._timeout_seconds, self._handle_timeout)
69+
self._timeout_timer.daemon = True
70+
self._timeout_timer.start()
71+
72+
def _handle_timeout(self) -> None:
73+
"""Handle shutdown timeout."""
74+
try:
75+
if not self._shutdown_event.is_set():
76+
return # Shutdown was not initiated, ignore timeout
77+
78+
logger.info(f"[Shutdown Timer] Shutdown timeout reached after {self._timeout_seconds} seconds")
79+
self._shutdown_type = ShutdownType.TIMEOUT
80+
self.on_timeout()
81+
except Exception as error:
82+
self.on_error(error)
83+
finally:
84+
logger.critical("[Shutdown Timer] Timer exited, exiting application")
85+
os._exit(0)
86+
87+
def complete_shutdown(self) -> None:
88+
"""Mark shutdown as complete and cancel timeout timer."""
89+
if self._timeout_timer and self._timeout_timer.is_alive():
90+
self._timeout_timer.cancel()
91+
92+
if self._shutdown_start_time:
93+
elapsed = time.time() - self._shutdown_start_time
94+
logger.success(f"[Shutdown Timer] Shutdown completed successfully in {elapsed:.2f} seconds")
5095

5196
def is_shutdown(self) -> bool:
5297
return self._shutdown_event.is_set()
5398

5499
def get_type(self) -> Optional[ShutdownType]:
55100
return self._shutdown_type
56101

102+
def get_timeout_seconds(self) -> float:
103+
return self._timeout_seconds
104+
105+
def is_timeout_enabled(self) -> bool:
106+
return self._timeout_enabled
107+
108+
def get_shutdown_elapsed_time(self) -> Optional[float]:
109+
"""Get the elapsed time since shutdown started."""
110+
if self._shutdown_start_time is None:
111+
return None
112+
return time.time() - self._shutdown_start_time
113+
114+
def trigger_manual_shutdown(self, shutdown_type: ShutdownType = ShutdownType.MANUAL) -> None:
115+
"""Manually trigger shutdown without signal."""
116+
try:
117+
logger.info(f"[Manual Shutdown] Triggering manual shutdown: {shutdown_type.name}")
118+
self._shutdown_type = shutdown_type
119+
self._shutdown_event.set()
120+
self._start_shutdown_timer()
121+
self.on_shutdown(shutdown_type)
122+
except Exception as error:
123+
self.on_error(error)
124+
57125
@abstractmethod
58126
def on_shutdown(self, shutdown_type: ShutdownType) -> None:
59127
"""

0 commit comments

Comments
 (0)