Skip to content

Commit a742ee2

Browse files
committed
Merge cold-start/05-service-init-container (synthesis) into cold-start/06-docs-publication
2 parents 3b99758 + ccce26f commit a742ee2

5 files changed

Lines changed: 561 additions & 293 deletions

File tree

src/backend/base/langflow/__main__.py

Lines changed: 124 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
# macOS Objective-C fork-safety guard.
2+
#
3+
# Gunicorn forks workers; on Darwin, Objective-C runtime fork-safety checks
4+
# can SIGSEGV workers unless OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES is set
5+
# in the OS environment *before* Python starts (setting it in Python is too
6+
# late — see langflow_launcher.py for the same pattern).
7+
#
8+
# The `langflow` console script routes through langflow_launcher.py which
9+
# handles this. This guard catches the bypass paths (`python -m langflow`,
10+
# `uv run python -m langflow`, etc.) so they're not silent footguns. Only
11+
# fires for direct CLI invocation; ordinary `import langflow.__main__` is
12+
# unaffected.
13+
if __name__ == "__main__":
14+
import os as _os
15+
import platform as _platform
16+
import sys as _sys
17+
18+
if _platform.system() == "Darwin" and not _os.environ.get("OBJC_DISABLE_INITIALIZE_FORK_SAFETY"):
19+
_os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
20+
_os.execv(_sys.executable, [_sys.executable, "-m", "langflow.__main__", *_sys.argv[1:]]) # noqa: S606
21+
122
import asyncio
223
import inspect
324
import os
@@ -8,6 +29,7 @@
829
import time
930
import warnings
1031
from contextlib import suppress
32+
from functools import partial
1133
from ipaddress import ip_address
1234
from pathlib import Path
1335

@@ -132,6 +154,68 @@ def get_number_of_workers(workers=None):
132154
return workers
133155

134156

157+
# Platforms where `langflow run` bypasses Gunicorn and runs uvicorn directly
158+
# against a pre-built FastAPI app object. On Linux we use Gunicorn (multi-worker
159+
# via fork()); on Windows and macOS forking is unsafe (Windows lacks fork; macOS
160+
# fork-with-threads + libdispatch / asyncio kqueue state crashes workers).
161+
DIRECT_UVICORN_PLATFORMS: tuple[str, ...] = ("Windows", "Darwin")
162+
163+
164+
def use_direct_uvicorn(system: str | None = None) -> bool:
165+
"""Return True iff this platform launches with uvicorn directly (no Gunicorn)."""
166+
return (system or platform.system()) in DIRECT_UVICORN_PLATFORMS
167+
168+
169+
def clamp_uvicorn_workers(requested: int, *, system: str | None = None) -> int:
170+
"""Clamp ``workers`` to 1 when running uvicorn against a pre-built app object.
171+
172+
uvicorn refuses to spawn multiple workers from an app *object* (it needs an
173+
import string), so on the direct-uvicorn platforms we cap workers at 1 and
174+
warn — preferable to uvicorn's own ``sys.exit(1)`` with a generic message.
175+
On Linux this is a no-op since Gunicorn handles multi-worker.
176+
"""
177+
if requested > 1 and use_direct_uvicorn(system):
178+
logger.warning(
179+
"Direct-uvicorn startup on %s does not support workers > 1 "
180+
"(uvicorn requires an import string for multi-worker mode). "
181+
"Falling back to a single worker; requested=%d.",
182+
system or platform.system(),
183+
requested,
184+
)
185+
return 1
186+
return requested
187+
188+
189+
def build_direct_uvicorn_kwargs(
190+
*,
191+
host: str,
192+
port: int,
193+
log_level: str | None,
194+
workers: int,
195+
loop: str,
196+
ssl_cert_file_path: str | None,
197+
ssl_key_file_path: str | None,
198+
system: str | None = None,
199+
) -> dict:
200+
"""Build the kwargs dict for ``uvicorn.run(app, **kwargs)`` on Win/macOS.
201+
202+
Pins the option set (workers clamp, TLS certs, loop type) in one place so
203+
the launch site stays a single call and so tests can assert that things
204+
like TLS cert/key pass through. Mirrors the option set used on the
205+
Gunicorn (Linux) path so platform parity does not drift again.
206+
"""
207+
return {
208+
"host": host,
209+
"port": port,
210+
"log_level": log_level,
211+
"reload": False,
212+
"workers": clamp_uvicorn_workers(workers, system=system),
213+
"loop": loop,
214+
"ssl_certfile": ssl_cert_file_path,
215+
"ssl_keyfile": ssl_key_file_path,
216+
}
217+
218+
135219
def display_results(results) -> None:
136220
"""Display the results of the migration."""
137221
for table_results in results:
@@ -158,8 +242,6 @@ def set_var_for_macos_issue() -> None:
158242
import os
159243

160244
os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
161-
# https://stackoverflow.com/questions/75747888/uwsgi-segmentation-fault-with-flask-python-app-behind-nginx-after-running-for-2 # noqa: E501
162-
os.environ["no_proxy"] = "*" # to avoid error with gunicorn
163245

164246

165247
def wait_for_server_ready(host, port, protocol) -> None:
@@ -338,8 +420,14 @@ def run(
338420
static_files_dir: Path | None = Path(frontend_path) if frontend_path else None
339421

340422
# Step 2: Starting Core Services
423+
app = None
424+
app_factory = None
341425
with progress.step(2):
342-
app = setup_app(static_files_dir=static_files_dir, backend_only=bool(backend_only))
426+
# See DIRECT_UVICORN_PLATFORMS for the rationale (no fork on Win/macOS).
427+
if use_direct_uvicorn():
428+
app = setup_app(static_files_dir=static_files_dir, backend_only=bool(backend_only))
429+
else:
430+
app_factory = partial(setup_app, static_files_dir=static_files_dir, backend_only=bool(backend_only))
343431

344432
# Step 3: Connecting Database (this happens inside setup_app via dependencies)
345433
with progress.step(3):
@@ -371,10 +459,27 @@ def run(
371459
pass # Starter projects are added during app startup
372460

373461
# Step 6: Launching Langflow
374-
if platform.system() == "Windows":
462+
if use_direct_uvicorn():
463+
# LANGFLOW_GUNICORN_PRELOAD is a Gunicorn-only knob: it triggers fork-safe
464+
# master-process preload so workers inherit state via copy-on-write. On
465+
# the direct-uvicorn path there is no master/worker split and no fork,
466+
# so the env var is silently inert. Warn loudly so users diagnosing
467+
# "preload isn't doing anything on my Mac" don't have to read source.
468+
if os.environ.get("LANGFLOW_GUNICORN_PRELOAD", "false").lower() == "true":
469+
logger.warning(
470+
"LANGFLOW_GUNICORN_PRELOAD=true is ignored on %s: this platform "
471+
"uses single-process uvicorn (no fork), so master preload / "
472+
"copy-on-write inheritance does not apply.",
473+
platform.system(),
474+
)
475+
375476
with progress.step(6):
376477
import uvicorn
377478

479+
if app is None:
480+
msg = "Direct-uvicorn startup (Windows/macOS) requires a pre-built FastAPI application."
481+
raise RuntimeError(msg)
482+
378483
# Print summary and banner before starting the server, since uvicorn is a blocking call.
379484
# We _may_ be able to subprocess, but with window's spawn behavior, we'd have to move all
380485
# non-picklable code to the subprocess.
@@ -394,28 +499,35 @@ def run(
394499

395500
uvicorn.run(
396501
app,
397-
host=host,
398-
port=port,
399-
log_level=log_level,
400-
reload=False,
401-
workers=get_number_of_workers(workers),
402-
loop=loop_type,
502+
**build_direct_uvicorn_kwargs(
503+
host=host,
504+
port=port,
505+
log_level=log_level,
506+
workers=get_number_of_workers(workers),
507+
loop=loop_type,
508+
ssl_cert_file_path=ssl_cert_file_path,
509+
ssl_key_file_path=ssl_key_file_path,
510+
),
403511
)
404512
else:
405513
with progress.step(6):
406514
# Use Gunicorn with LangflowUvicornWorker for non-Windows systems
407515
from langflow.server import LangflowApplication
408516

517+
if app_factory is None:
518+
msg = "Gunicorn startup requires an application factory."
519+
raise RuntimeError(msg)
520+
409521
options = {
410522
"bind": f"{host}:{port}",
411523
"workers": get_number_of_workers(workers),
412524
"timeout": worker_timeout,
413525
"certfile": ssl_cert_file_path,
414526
"keyfile": ssl_key_file_path,
415527
"log_level": log_level.lower() if log_level is not None else "info",
416-
"preload_app": os.environ.get("LANGFLOW_GUNICORN_PRELOAD", "true").lower() == "true",
528+
"preload_app": os.environ.get("LANGFLOW_GUNICORN_PRELOAD", "false").lower() == "true",
417529
}
418-
server = LangflowApplication(app, options)
530+
server = LangflowApplication(app_factory, options)
419531

420532
# Start the webapp process
421533
process_manager.webapp_process = Process(target=server.run)

0 commit comments

Comments
 (0)