Skip to content

Commit 070d556

Browse files
authored
Implement Graceful Shutdown Handler with Configurable Timeout (#19)
1 parent b987d2a commit 070d556

19 files changed

+965
-37
lines changed

py_spring_core/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from py_spring_core.core.application.py_spring_application import PySpringApplication
2-
from py_spring_core.core.entities.bean_collection import BeanCollection
3-
from py_spring_core.core.entities.component import Component, ComponentScope
2+
from py_spring_core.core.entities.bean_collection.bean_collection import BeanCollection
3+
from py_spring_core.core.entities.component.component import Component, ComponentScope
44
from py_spring_core.core.entities.controllers.rest_controller import RestController
55
from py_spring_core.core.entities.controllers.route_mapping import (
66
DeleteMapping,
@@ -9,7 +9,7 @@
99
PostMapping,
1010
PutMapping,
1111
)
12-
from py_spring_core.core.entities.entity_provider import EntityProvider
12+
from py_spring_core.core.entities.entity_provider.entity_provider import EntityProvider
1313
from py_spring_core.core.entities.middlewares.middleware import Middleware
1414
from py_spring_core.core.entities.middlewares.middleware_registry import (
1515
MiddlewareRegistry,
@@ -22,7 +22,7 @@
2222
from py_spring_core.event.application_event_publisher import ApplicationEventPublisher
2323
from py_spring_core.event.commons import ApplicationEvent
2424

25-
__version__ = "0.0.23"
25+
__version__ = "0.0.24"
2626

2727
__all__ = [
2828
"PySpringApplication",

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/commons.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from py_spring_core.core.entities.bean_collection import BeanCollection
2-
from py_spring_core.core.entities.component import Component
1+
from py_spring_core.core.entities.bean_collection.bean_collection import BeanCollection
2+
from py_spring_core.core.entities.component.component import Component
33
from py_spring_core.core.entities.controllers.rest_controller import RestController
44
from py_spring_core.core.entities.properties.properties import Properties
55

py_spring_core/core/application/context/application_context.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@
2222
from py_spring_core.core.application.context.application_context_config import (
2323
ApplicationContextConfig,
2424
)
25-
from py_spring_core.core.entities.bean_collection import (
25+
from py_spring_core.core.entities.bean_collection.bean_collection import (
2626
BeanCollection,
2727
BeanConflictError,
2828
BeanView,
2929
InvalidBeanError,
3030
)
31-
from py_spring_core.core.entities.component import Component, ComponentScope
31+
from py_spring_core.core.entities.component.component import Component, ComponentScope
3232
from py_spring_core.core.entities.controllers.rest_controller import RestController
33-
from py_spring_core.core.entities.entity_provider import EntityProvider
33+
from py_spring_core.core.entities.entity_provider.entity_provider import EntityProvider
3434
from py_spring_core.core.entities.properties.properties import Properties
3535
from py_spring_core.core.entities.properties.properties_loader import _PropertiesLoader
3636

py_spring_core/core/application/py_spring_application.py

Lines changed: 74 additions & 17 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
@@ -23,23 +23,27 @@
2323
ApplicationContextConfig,
2424
)
2525
from py_spring_core.core.application.loguru_config import LogFormat
26-
from py_spring_core.core.entities.bean_collection import BeanCollection
27-
from py_spring_core.core.entities.component import Component, ComponentLifeCycle
26+
from py_spring_core.core.entities.bean_collection.bean_collection import BeanCollection
27+
from py_spring_core.core.entities.component.component import Component, ComponentLifeCycle
2828
from py_spring_core.core.entities.controllers.rest_controller import RestController
2929
from py_spring_core.core.entities.controllers.route_mapping import RouteMapping
30-
from py_spring_core.core.entities.entity_provider import EntityProvider
30+
from py_spring_core.core.entities.entity_provider.entity_provider import EntityProvider
31+
from py_spring_core.core.entities.middlewares.middleware import Middleware
3132
from py_spring_core.core.entities.middlewares.middleware_registry import (
3233
MiddlewareRegistry,
3334
)
3435
from py_spring_core.core.entities.properties.properties import Properties
3536
from py_spring_core.core.interfaces.application_context_required import (
3637
ApplicationContextRequired,
3738
)
39+
from py_spring_core.core.interfaces.graceful_shutdown_handler import GracefulShutdownHandler
40+
from py_spring_core.core.interfaces.single_inheritance_required import SingleInheritanceRequired
3841
from py_spring_core.event.application_event_handler_registry import (
3942
ApplicationEventHandlerRegistry,
4043
)
4144
from py_spring_core.event.application_event_publisher import ApplicationEventPublisher
4245

46+
import py_spring_core.core.utils as framework_utils
4347

4448
class PySpringApplication:
4549
"""
@@ -102,6 +106,7 @@ def __init__(
102106
self.type_checking_service = TypeCheckingService(
103107
self.app_config.app_src_target_dir
104108
)
109+
self.shutdown_handler: Optional[GracefulShutdownHandler] = None
105110

106111
def __configure_logging(self):
107112
"""Applies the logging configuration using Loguru."""
@@ -232,31 +237,76 @@ def __init_controllers(self) -> None:
232237
self.fastapi.include_router(router)
233238
logger.debug(f"[CONTROLLER INIT] Controller {name} initialized")
234239

235-
def __init_middlewares(self) -> None:
236-
logger.debug("[MIDDLEWARE INIT] Initialize middlewares...")
237-
self_defined_registry_cls = MiddlewareRegistry.get_subclass()
238-
if self_defined_registry_cls is None:
239-
logger.debug("[MIDDLEWARE INIT] No self defined registry class found")
240-
return
240+
def _init_external_handler(self, base_class: Type[SingleInheritanceRequired]) -> Type[Any] | None:
241+
"""Initialize an external handler (middleware registry or graceful shutdown handler).
242+
243+
Args:
244+
base_class: The base class to get subclass from
245+
handler_type: The type of handler for logging purposes
246+
247+
Returns:
248+
The initialized handler class or None if no handler is found
249+
250+
Raises:
251+
RuntimeError: If the handler has unimplemented abstract methods
252+
"""
253+
handler_type = base_class.__name__
254+
self_defined_handler_cls = base_class.get_subclass()
255+
if self_defined_handler_cls is None:
256+
logger.debug(f"[{handler_type} INIT] No self defined {handler_type.lower()} class found")
257+
return None
258+
259+
unimplemented_abstract_methods = framework_utils.get_unimplemented_abstract_methods(self_defined_handler_cls)
260+
if len(unimplemented_abstract_methods) > 0:
261+
error_message = f"[{handler_type} INIT] Self defined {handler_type.lower()} class: {self_defined_handler_cls.__name__} has unimplemented abstract methods: {unimplemented_abstract_methods}"
262+
logger.error(error_message)
263+
raise RuntimeError(error_message)
264+
241265
logger.debug(
242-
f"[MIDDLEWARE INIT] Self defined registry class: {self_defined_registry_cls.__name__}"
266+
f"[{handler_type} INIT] Self defined {handler_type.lower()} class: {self_defined_handler_cls.__name__}"
243267
)
244268
logger.debug(
245-
f"[MIDDLEWARE INIT] Inject dependencies for external object: {self_defined_registry_cls.__name__}"
269+
f"[{handler_type} INIT] Inject dependencies for external object: {self_defined_handler_cls.__name__}"
246270
)
247271
self.app_context.inject_dependencies_for_external_object(
248-
self_defined_registry_cls
272+
self_defined_handler_cls
249273
)
250-
registry = self_defined_registry_cls()
274+
return self_defined_handler_cls
251275

252-
middleware_classes = registry.get_middleware_classes()
276+
def __init_middlewares(self) -> None:
277+
handler_type = MiddlewareRegistry.__name__
278+
logger.debug(f"[{handler_type} INIT] Initialize middlewares...")
279+
registry_cls = self._init_external_handler(MiddlewareRegistry)
280+
if registry_cls is None:
281+
return
282+
283+
registry: MiddlewareRegistry = registry_cls()
284+
middleware_classes: list[Type[Middleware]] = registry.get_middleware_classes()
253285
for middleware_class in middleware_classes:
254286
logger.debug(
255-
f"[MIDDLEWARE INIT] Inject dependencies for middleware: {middleware_class.__name__}"
287+
f"[{handler_type} INIT] Inject dependencies for middleware: {middleware_class.__name__}"
256288
)
257289
self.app_context.inject_dependencies_for_external_object(middleware_class)
258290
registry.apply_middlewares(self.fastapi)
259-
logger.debug("[MIDDLEWARE INIT] Middlewares initialized")
291+
logger.debug(f"[{handler_type} INIT] Middlewares initialized")
292+
293+
def __init_graceful_shutdown(self) -> None:
294+
handler_type = GracefulShutdownHandler.__name__
295+
logger.debug(f"[{handler_type} INIT] Initialize graceful shutdown...")
296+
handler_cls: Optional[Type[GracefulShutdownHandler]] = self._init_external_handler(GracefulShutdownHandler)
297+
if handler_cls is None:
298+
return
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+
)
309+
logger.debug(f"[{handler_type} INIT] Graceful shutdown initialized")
260310

261311
def __configure_uvicorn_logging(self):
262312
"""Configure Uvicorn to use Loguru instead of default logging."""
@@ -300,7 +350,14 @@ def run(self) -> None:
300350
self.__init_app()
301351
self.__init_controllers()
302352
self.__init_middlewares()
353+
self.__init_graceful_shutdown()
303354
if self.app_config.server_config.enabled:
304355
self.__run_server()
305356
finally:
357+
# Handle component lifecycle destruction
306358
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+
File renamed without changes.
File renamed without changes.

py_spring_core/core/entities/entity_provider.py renamed to py_spring_core/core/entities/entity_provider/entity_provider.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
from typing import Any, Optional, Type
33

44
from py_spring_core.core.application.commons import AppEntities
5-
from py_spring_core.core.entities.bean_collection import BeanCollection
6-
from py_spring_core.core.entities.component import Component
5+
from py_spring_core.core.entities.bean_collection.bean_collection import BeanCollection
6+
from py_spring_core.core.entities.component.component import Component
77
from py_spring_core.core.entities.controllers.rest_controller import RestController
88
from py_spring_core.core.entities.properties.properties import Properties
99

0 commit comments

Comments
 (0)