Skip to content

Commit 8a82359

Browse files
Janardan S Kaviaerichare
authored andcommitted
refactor(warm-registry): gate on LANGFLOW_DEPLOYMENT_PROFILE=prod (unify with preflight #14393) instead of LANGFLOW_PROD
1 parent efc8bce commit 8a82359

6 files changed

Lines changed: 51 additions & 31 deletions

File tree

src/backend/base/langflow/api/router.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,14 @@ def _include_agentic_router():
151151
# them (one handler per method+path). ``developer_api_guard=False`` because the
152152
# authenticated langflow v2 router has never carried a developer-api gate; the
153153
# default-off setting would otherwise 403 every authenticated request.
154-
# In PROD (``LANGFLOW_PROD``, execution-plane / ``--backend-only``) the warm host
155-
# serves deployed flows from the in-memory registry and skips per-flow RBAC. The
156-
# warm-vs-DB choice is made per ``settings.prod`` but DEFERRED to first use: this
157-
# module is imported before ``load_dotenv(--env-file)`` runs, so reading the env
158-
# (or the settings service) here would miss ``--env-file`` values. The route
159-
# structure is identical for both hosts, so binding the deferred proxy changes
160-
# nothing structurally — only which concrete host each request resolves to.
154+
# Under the production deployment profile (``LANGFLOW_DEPLOYMENT_PROFILE=prod``,
155+
# execution-plane / ``--backend-only``) the warm host serves deployed flows from the
156+
# in-memory registry and skips per-flow RBAC. The warm-vs-DB choice is made per that
157+
# profile but DEFERRED to first use: this module is imported before
158+
# ``load_dotenv(--env-file)`` runs, so reading the env (or the settings service) here
159+
# would miss ``--env-file`` values. The route structure is identical for both hosts,
160+
# so binding the deferred proxy changes nothing structurally — only which concrete
161+
# host each request resolves to.
161162
_workflow_host: WorkflowHost = DeferredWorkflowHost()
162163
assert isinstance(_workflow_host, WorkflowHost) # noqa: S101
163164
router_v2.include_router(

src/backend/base/langflow/api/v2/host_selection.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
"""Deferred selection of the v2 workflow host (warm PROD registry vs DB-backed).
22
3-
Which host serves ``POST /api/v2/workflows`` depends on ``settings.prod``
4-
(``LANGFLOW_PROD``). The problem: ``langflow.__main__`` imports the router module
3+
Which host serves ``POST /api/v2/workflows`` depends on the deployment profile
4+
(``LANGFLOW_DEPLOYMENT_PROFILE=prod`` -> ``settings.deployment_profile``, the same
5+
umbrella flag whose prod value the deployment-profile preflight fail-fast-validates:
6+
enabling the warm/authz-lean host is gated behind that validated profile, not a
7+
standalone switch). The problem: ``langflow.__main__`` imports the router module
58
(and therefore builds this router) *before* it runs ``load_dotenv(--env-file)``, so
69
reading the env — or the settings service — at import time would miss any value
710
supplied via ``--env-file``. That is exactly the ordering trap the extensions router
811
documents for ``LANGFLOW_ENABLE_EXTENSION_RELOAD``.
912
1013
``DeferredWorkflowHost`` defers the choice to first use. By the time any workflow
1114
route (or capability flag) is exercised, ``setup_app``/lifespan has initialized the
12-
settings service from the fully-loaded environment, so ``settings.prod`` is correct.
13-
The route *structure* is identical for both hosts (the shared router only reads
15+
settings service from the fully-loaded environment, so the profile is correct. The
16+
route *structure* is identical for both hosts (the shared router only reads
1417
``supports_*`` at request time, and ``auto_register_job_routes=False`` neutralizes
1518
the one mount-time read), so binding this single proxy at import changes nothing
1619
structurally — only which concrete host each request lands on.
@@ -35,8 +38,21 @@
3538
from lfx.workflow.host import ResolvedFlow, WorkflowAction
3639

3740

41+
def is_prod_deployment(settings: Any) -> bool:
42+
"""True when the production deployment profile is active.
43+
44+
Consumes ``settings.deployment_profile`` (env ``LANGFLOW_DEPLOYMENT_PROFILE``) —
45+
the umbrella flag owned by the deployment-profile preflight feature. Read
46+
defensively via ``getattr`` so this code is safe on branches where that setting
47+
does not exist yet: absent -> ``"dev"`` -> production behavior stays off. The
48+
warm registry + authz-lean host therefore activate only under the same ``prod``
49+
profile the preflight gate validates.
50+
"""
51+
return getattr(settings, "deployment_profile", "dev") == "prod"
52+
53+
3854
class DeferredWorkflowHost(WorkflowHostBase):
39-
"""A ``WorkflowHost`` that picks the concrete host lazily from ``settings.prod``.
55+
"""A ``WorkflowHost`` that picks the concrete host lazily from the deployment profile.
4056
4157
Every member delegates to the resolved host. Resolution is cached only once the
4258
settings service is initialized, so an access during module import (before
@@ -60,7 +76,9 @@ def _resolve(self) -> WorkflowHostBase:
6076
# real choice is still made on the first post-startup call.
6177
if not is_settings_service_initialized():
6278
return LangflowWorkflowHost()
63-
host: WorkflowHostBase = WarmWorkflowHost() if get_settings_service().settings.prod else LangflowWorkflowHost()
79+
host: WorkflowHostBase = (
80+
WarmWorkflowHost() if is_prod_deployment(get_settings_service().settings) else LangflowWorkflowHost()
81+
)
6482
self._host = host
6583
return host
6684

src/backend/base/langflow/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from langflow.api import health_check_router, log_router
3030
from langflow.api.router import router
3131
from langflow.api.v1.mcp_projects import init_mcp_servers
32+
from langflow.api.v2.host_selection import is_prod_deployment
3233
from langflow.initial_setup.setup import (
3334
copy_profile_pictures,
3435
create_or_update_starter_projects,
@@ -491,7 +492,7 @@ async def lifespan(_app: FastAPI):
491492
# build). Warming and the loop are launched INDEPENDENTLY: a failed eager warm
492493
# must not stop the reconcile loop, which self-heals on its next pass. Runs
493494
# once per worker (app "lifespan"); the settings service is safe here.
494-
if get_settings_service().settings.prod:
495+
if is_prod_deployment(get_settings_service().settings):
495496
from langflow.services.warm_registry.reconcile import reconcile_loop, warm_all
496497

497498
try:

src/backend/tests/unit/services/warm_registry/test_warm_registry_reconcile.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -240,23 +240,26 @@ async def _fake_once():
240240

241241

242242
async def test_prod_lifespan_warms_and_cancels_loop(monkeypatch, tmp_path):
243-
"""Boot the app with LANGFLOW_PROD=true to cover the main.py prod wiring.
243+
"""Boot the app under the prod deployment profile to cover the main.py prod wiring.
244244
245245
Startup runs the warm block; shutdown cancels the reconcile loop.
246246
"""
247+
import langflow.main as main_mod
247248
from asgi_lifespan import LifespanManager
248249
from langflow.main import create_app
249250
from langflow.services.deps import get_db_service
250251
from lfx.services.manager import get_service_manager
251252

252253
db_path = tmp_path / "prod.db"
253-
monkeypatch.setenv("LANGFLOW_PROD", "true")
254+
# ``deployment_profile`` is owned by the preflight PR and not present on this branch,
255+
# so drive the prod branch by patching the profile check the lifespan calls.
256+
monkeypatch.setattr(main_mod, "is_prod_deployment", lambda _settings: True)
254257
monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
255258
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "true")
256259
monkeypatch.setenv("DO_NOT_TRACK", "true")
257260

258261
def _init():
259-
# Fresh service stack so settings.prod is re-read from the env above.
262+
# Fresh service stack booted from the env above.
260263
get_service_manager().factories.clear()
261264
get_service_manager().services.clear()
262265
app = create_app()
@@ -279,6 +282,7 @@ def _init():
279282

280283
async def test_prod_lifespan_survives_warm_failure(monkeypatch, tmp_path):
281284
"""A failure in warm_all during prod startup is logged, not fatal (except branch)."""
285+
import langflow.main as main_mod
282286
from asgi_lifespan import LifespanManager
283287
from langflow.main import create_app
284288
from langflow.services.deps import get_db_service
@@ -293,7 +297,8 @@ async def _boom_warm() -> None:
293297
monkeypatch.setattr(reconcile_mod, "warm_all", _boom_warm)
294298

295299
db_path = tmp_path / "prod_fail.db"
296-
monkeypatch.setenv("LANGFLOW_PROD", "true")
300+
# Drive the prod branch via the profile check (see the sibling lifespan test).
301+
monkeypatch.setattr(main_mod, "is_prod_deployment", lambda _settings: True)
297302
monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
298303
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "true")
299304
monkeypatch.setenv("DO_NOT_TRACK", "true")
@@ -517,7 +522,9 @@ def test_deferred_host_resolves_db_when_not_prod(monkeypatch):
517522
from langflow.services import deps
518523

519524
monkeypatch.setattr(deps, "is_settings_service_initialized", lambda: True)
520-
monkeypatch.setattr(deps, "get_settings_service", lambda: SimpleNamespace(settings=SimpleNamespace(prod=False)))
525+
monkeypatch.setattr(
526+
deps, "get_settings_service", lambda: SimpleNamespace(settings=SimpleNamespace(deployment_profile="dev"))
527+
)
521528
host = DeferredWorkflowHost()
522529
assert isinstance(host._resolve(), LangflowWorkflowHost)
523530

@@ -531,7 +538,9 @@ def test_deferred_host_resolves_warm_when_prod(monkeypatch):
531538
from langflow.services import deps
532539

533540
monkeypatch.setattr(deps, "is_settings_service_initialized", lambda: True)
534-
monkeypatch.setattr(deps, "get_settings_service", lambda: SimpleNamespace(settings=SimpleNamespace(prod=True)))
541+
monkeypatch.setattr(
542+
deps, "get_settings_service", lambda: SimpleNamespace(settings=SimpleNamespace(deployment_profile="prod"))
543+
)
535544
host = DeferredWorkflowHost()
536545
resolved = host._resolve()
537546
assert isinstance(resolved, WarmWorkflowHost)

src/lfx/src/lfx/services/settings/groups/runtime.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,14 @@ class RuntimeSettings(BaseModel):
1616
dev: bool = False
1717
"""If True, Langflow will run in development mode."""
1818

19-
prod: bool = False
20-
"""If True, run in production execution-plane mode (LANGFLOW_PROD).
21-
22-
Serves deployed flows from a warm in-memory registry instead of rebuilding the
23-
graph from the DB on every request, and skips per-flow RBAC on the workflow run
24-
path (single-tenant trust model: any authenticated caller may run any deployed
25-
flow). Intended for ``--backend-only`` execution machines. The prod preflight
26-
fail-fast gate (Postgres/S3/SECRET_KEY checks) is a separate feature."""
27-
2819
warm_reconcile_interval: float = Field(default=20.0, gt=0)
2920
"""Seconds between warm-registry reconcile passes (LANGFLOW_WARM_RECONCILE_INTERVAL).
3021
3122
Each execution machine independently diffs its in-memory registry against the
3223
shared ``flow`` table every ``interval`` seconds, so a deploy or delete takes up
3324
to this long to propagate across the fleet. Lower for faster convergence at the
34-
cost of more manifest queries. Only used when ``prod`` is True. Must be > 0."""
25+
cost of more manifest queries. Only used under the production deployment profile
26+
(``LANGFLOW_DEPLOYMENT_PROFILE=prod``). Must be > 0."""
3527

3628
# Job Queue
3729
job_queue_type: Literal["asyncio", "redis"] = "asyncio"

src/lfx/tests/unit/services/settings/test_settings_composition.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ def import_without_openai(name, *args, **kwargs):
157157
"like_webhook_url",
158158
# RuntimeSettings
159159
"dev",
160-
"prod",
161160
"warm_reconcile_interval",
162161
"event_delivery",
163162
"worker_timeout",

0 commit comments

Comments
 (0)