feat: add SIGTERM handling and update typer dependency (#4548)

* Update `typer` dependency to version 0.13.0 in `pyproject.toml`

* refactor: Simplify exception handling in the CLI

* Enhance lifespan function with clean shutdown and logging improvements

* Add graceful shutdown handling for SIGTERM and SIGINT signals

- Introduce signal handlers to manage SIGTERM and SIGINT for graceful server shutdown.
- Update exception handling to ensure processes terminate properly and log shutdown events.
- Modify server run logic to support signal-based shutdowns, improving reliability.

---------

Co-authored-by: Nadir J <31660040+NadirJ@users.noreply.github.com>
This commit is contained in:
Gabriel Luiz Freitas Almeida 2024-11-12 21:42:18 -03:00 • committed by GitHub
commit f8f9b7cace
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 66 additions and 25 deletions

View file

@ -1,6 +1,7 @@
import asyncio
import inspect
import platform
import signal
import socket
import sys
import time
@ -24,9 +25,7 @@ from sqlmodel import select
from langflow.logging.logger import configure, logger
from langflow.main import setup_app
from langflow.services.database.models.folder.utils import (
create_default_folder_if_it_doesnt_exist,
)
from langflow.services.database.models.folder.utils import create_default_folder_if_it_doesnt_exist
from langflow.services.database.utils import session_getter
from langflow.services.deps import async_session_scope, get_db_service, get_settings_service
from langflow.services.settings.constants import DEFAULT_SUPERUSER
@ -77,6 +76,13 @@ def set_var_for_macos_issue() -> None:
logger.debug("Set OBJC_DISABLE_INITIALIZE_FORK_SAFETY to YES to avoid error")
def handle_sigterm(signum, frame): # noqa: ARG001
"""Handle SIGTERM signal gracefully."""
logger.info("Received SIGTERM signal. Performing graceful shutdown...")
# Raise SystemExit to trigger graceful shutdown
sys.exit(0)
@app.command()
def run(
*,
@ -150,6 +156,9 @@ def run(
),
) -> None:
"""Run Langflow."""
# Register SIGTERM handler
signal.signal(signal.SIGTERM, handle_sigterm)
if env_file:
load_dotenv(env_file, override=True)
@ -211,13 +220,20 @@ def run(
click.launch(f"http://{host}:{port}")
if process:
process.join()
except KeyboardInterrupt:
except (KeyboardInterrupt, SystemExit) as e:
logger.info("Shutting down server...")
if process is not None:
process.terminate()
sys.exit(0)
except Exception as e: # noqa: BLE001
process.join(timeout=15) # Wait up to 15 seconds for process to terminate
if process.is_alive():
logger.warning("Process did not terminate gracefully, forcing...")
process.kill()
raise typer.Exit(0) from e
except Exception as e:
logger.exception(e)
sys.exit(1)
if process is not None:
process.terminate()
raise typer.Exit(1) from e
def wait_for_server_ready(host, port) -> None:
@ -359,9 +375,6 @@ def print_banner(host: str, port: int) -> None:
def run_langflow(host, port, log_level, options, app) -> None:
"""Run Langflow server on localhost."""
if platform.system() == "Windows":
# Run using uvicorn on MacOS and Windows
# Windows doesn't support gunicorn
# MacOS requires an env variable to be set to use gunicorn
import uvicorn
uvicorn.run(
@ -374,7 +387,28 @@ def run_langflow(host, port, log_level, options, app) -> None:
else:
from langflow.server import LangflowApplication
LangflowApplication(app, options).run()
server = LangflowApplication(app, options)
def graceful_shutdown(signum, frame): # noqa: ARG001
"""Gracefully shutdown the server when receiving SIGTERM."""
# Suppress click exceptions during shutdown
import click
click.echo = lambda *args, **kwargs: None # noqa: ARG005
logger.info("Gracefully shutting down server...")
# For Gunicorn workers, we raise SystemExit to trigger graceful shutdown
raise SystemExit(0)
# Register signal handlers
signal.signal(signal.SIGTERM, graceful_shutdown)
signal.signal(signal.SIGINT, graceful_shutdown)
try:
server.run()
except (KeyboardInterrupt, SystemExit):
# Suppress the exception output
sys.exit(0)
@app.command()
@ -499,10 +533,7 @@ def api_key(
)
return None
from langflow.services.database.models.api_key import ApiKey, ApiKeyCreate
from langflow.services.database.models.api_key.crud import (
create_api_key,
delete_api_key,
)
from langflow.services.database.models.api_key.crud import create_api_key, delete_api_key
api_key = (await session.exec(select(ApiKey).where(ApiKey.user_id == superuser.id))).first()
if api_key:
@ -568,4 +599,8 @@ def main() -> None:
if __name__ == "__main__":
main()
try:
main()
except Exception as e:
logger.exception(e)
raise typer.Exit(1) from e

View file

@ -97,6 +97,7 @@ def get_lifespan(*, fix_migration=False, version=None):
@asynccontextmanager
async def lifespan(_app: FastAPI):
configure(async_file=True)
# Startup message
if version:
rprint(f"[bold green]Starting Langflow v{version}...[/bold green]")
@ -108,15 +109,20 @@ def get_lifespan(*, fix_migration=False, version=None):
await asyncio.to_thread(create_or_update_starter_projects, all_types_dict)
telemetry_service.start()
await asyncio.to_thread(load_flows_from_directory)
yield
except Exception as exc:
if "langflow migration --fix" not in str(exc):
logger.exception(exc)
raise
# Shutdown message
rprint("[bold red]Shutting down Langflow...[/bold red]")
await teardown_services()
await logger.complete()
finally:
# Clean shutdown
logger.info("Cleaning up resources...")
await teardown_services()
await logger.complete()
# Final message
rprint("[bold red]Langflow shutdown complete[/bold red]")
return lifespan

View file

@ -119,7 +119,7 @@ dependencies = [
"langchain-experimental>=0.0.61",
"pydantic>=2.7.0",
"pydantic-settings>=2.2.0",
"typer>=0.12.0",
"typer>=0.13.0",
"cachetools>=5.3.1",
"platformdirs>=4.2.0",
"python-multipart>=0.0.12",