Skip to content

Commit bd926ca

Browse files
refactor(settings): split monolithic Settings into per-domain group mixins (#13141)
* refactor(settings): split monolithic Settings into per-domain group mixins Move the ~70-field Settings class from one 755-line base.py into 13 cohesive BaseModel mixins under lfx/services/settings/groups/ (paths, server, database, cache, storage, mcp, telemetry, observability, security, components, ui, runtime, variables). Settings now composes them via multiple inheritance. Inheritance order is chosen so cross-group validators see their dependencies in info.data: PathSettings rightmost (config_dir before database_url), ServerSettings just left of it (workers before event_delivery). No env var or call-site changes. BASE_COMPONENTS_PATH re-export preserved for tests that import it transitively. * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * docs(settings): fix wrong mcp_server_timeout docstring and idle-timeout comment mcp_server_timeout's docstring was copy-pasted from a database setting and mentioned 'lock to released' / 'database connection'. Replace with text that describes the actual field. mcp_session_idle_timeout's comment said 'Defaults to 5 minutes' but 400s is ~6.7 minutes. Drop the misleading 'minutes' claim and keep the value. * test(settings): add structural safety tests for the group composition Adds 26 tests to guard the refactor: - All 105 fields that lived on the monolithic Settings still exist on the composed class. A missing group in the inheritance list trips this loudly. - A sampling of critical scalar and dict defaults (host, port, workers, cache_type, sqlite_pragmas, db_connection_settings, etc.) are byte-for-byte unchanged. - Cross-group validator dependencies still resolve via info.data: workers > 1 forces event_delivery=direct (ServerSettings -> RuntimeSettings) and database_url falls back to a sqlite path under config_dir without raising 'config_dir not set' (PathSettings -> DatabaseSettings). - A parametrized sweep verifies a representative set of LANGFLOW_* env vars still populate their fields. - Back-compat exports (CustomSource, is_list_of_any, yaml helpers, BASE_COMPONENTS_PATH) are still importable from settings.base. - update_settings handles scalars and list-with-no-duplicates correctly. - save_settings_to_yaml round-trips without error. * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent b0b5849 commit bd926ca

16 files changed

Lines changed: 1141 additions & 625 deletions

File tree

src/lfx/src/lfx/services/settings/base.py

Lines changed: 61 additions & 625 deletions
Large diffs are not rendered by default.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Logical groupings of Langflow settings.
2+
3+
Each module defines a ``BaseModel`` mixin that owns a cohesive subset of fields
4+
plus their intra-group validators. They are composed into the final
5+
``Settings`` class in :mod:`lfx.services.settings.base`.
6+
7+
Mixins inherit from ``BaseModel`` (not ``BaseSettings``) and are not intended
8+
to be instantiated directly.
9+
"""
10+
11+
from lfx.services.settings.groups.cache import CacheSettings
12+
from lfx.services.settings.groups.components import ComponentsSettings
13+
from lfx.services.settings.groups.database import DatabaseSettings
14+
from lfx.services.settings.groups.mcp import McpSettings
15+
from lfx.services.settings.groups.observability import ObservabilitySettings
16+
from lfx.services.settings.groups.paths import PathSettings
17+
from lfx.services.settings.groups.runtime import RuntimeSettings
18+
from lfx.services.settings.groups.security import SecuritySettings
19+
from lfx.services.settings.groups.server import ServerSettings
20+
from lfx.services.settings.groups.storage import StorageSettings
21+
from lfx.services.settings.groups.telemetry import TelemetrySettings
22+
from lfx.services.settings.groups.ui import UiSettings
23+
from lfx.services.settings.groups.variables import VariablesSettings
24+
25+
__all__ = [
26+
"CacheSettings",
27+
"ComponentsSettings",
28+
"DatabaseSettings",
29+
"McpSettings",
30+
"ObservabilitySettings",
31+
"PathSettings",
32+
"RuntimeSettings",
33+
"SecuritySettings",
34+
"ServerSettings",
35+
"StorageSettings",
36+
"TelemetrySettings",
37+
"UiSettings",
38+
"VariablesSettings",
39+
]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from typing import Literal
2+
3+
from pydantic import BaseModel
4+
5+
6+
class CacheSettings(BaseModel):
7+
"""In-memory, disk, and Redis cache settings."""
8+
9+
cache_type: Literal["async", "redis", "memory", "disk"] = "async"
10+
"""The cache type can be 'async' or 'redis'."""
11+
cache_expire: int = 3600
12+
"""The cache expire in seconds."""
13+
langchain_cache: str = "InMemoryCache"
14+
15+
# Redis
16+
redis_host: str = "localhost"
17+
redis_port: int = 6379
18+
redis_db: int = 0
19+
redis_url: str | None = None
20+
redis_cache_expire: int = 3600
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import os
2+
from pathlib import Path
3+
4+
from pydantic import BaseModel, field_validator
5+
6+
from lfx.constants import BASE_COMPONENTS_PATH
7+
from lfx.log.logger import logger
8+
9+
10+
class ComponentsSettings(BaseModel):
11+
"""Component discovery, indexing, and startup-load behavior."""
12+
13+
components_path: list[str] = []
14+
"""List of paths to custom components.
15+
16+
Security: This setting defines an allow-list of custom components
17+
permitted to execute, even when LANGFLOW_ALLOW_CUSTOM_COMPONENTS is False.
18+
"""
19+
components_index_path: str | None = None
20+
"""Path or URL to a prebuilt component index JSON file.
21+
22+
If None, uses the built-in index at lfx/_assets/component_index.json.
23+
Set to a file path (e.g., '/path/to/index.json') or URL (e.g., 'https://example.com/index.json')
24+
to use a custom index.
25+
"""
26+
27+
load_flows_path: str | None = None
28+
bundle_urls: list[str] = []
29+
30+
lazy_load_components: bool = False
31+
"""If set to True, Langflow will only partially load components at startup and fully load them on demand.
32+
This significantly reduces startup time but may cause a slight delay when a component is first used."""
33+
34+
# Starter Projects
35+
create_starter_projects: bool = True
36+
"""If set to True, Langflow will create starter projects. If False, skips all starter project setup.
37+
Note that this doesn't check if the starter projects are already loaded in the db;
38+
this is intended to be used to skip all startup project logic."""
39+
update_starter_projects: bool = True
40+
"""If set to True, Langflow will update starter projects."""
41+
42+
@field_validator("components_path", mode="before")
43+
@classmethod
44+
def set_components_path(cls, value):
45+
"""Processes and updates the components path list, incorporating environment variable overrides.
46+
47+
If the `LANGFLOW_COMPONENTS_PATH` environment variable is set and points to an existing path, it is
48+
appended to the provided list if not already present. If the input list is empty or missing, it is
49+
set to an empty list.
50+
"""
51+
if os.getenv("LANGFLOW_COMPONENTS_PATH"):
52+
logger.debug("Adding LANGFLOW_COMPONENTS_PATH to components_path")
53+
langflow_component_path = os.getenv("LANGFLOW_COMPONENTS_PATH")
54+
if Path(langflow_component_path).exists() and langflow_component_path not in value:
55+
if isinstance(langflow_component_path, list):
56+
for path in langflow_component_path:
57+
if path not in value:
58+
value.append(path)
59+
logger.debug(f"Extending {langflow_component_path} to components_path")
60+
elif langflow_component_path not in value:
61+
value.append(langflow_component_path)
62+
logger.debug(f"Appending {langflow_component_path} to components_path")
63+
64+
if not value:
65+
value = [BASE_COMPONENTS_PATH]
66+
elif isinstance(value, Path):
67+
value = [str(value)]
68+
elif isinstance(value, list):
69+
value = [str(p) if isinstance(p, Path) else p for p in value]
70+
return value
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import os
2+
from pathlib import Path
3+
from shutil import copy2
4+
5+
from pydantic import BaseModel, field_validator
6+
7+
from lfx.log.logger import logger
8+
from lfx.utils.util_strings import is_valid_database_url, sanitize_database_url
9+
10+
11+
class DatabaseSettings(BaseModel):
12+
"""Database connection, pooling, and migration settings.
13+
14+
Note: ``database_url`` is validated at the :class:`Settings` level because
15+
it reads ``config_dir`` from :class:`PathSettings`.
16+
"""
17+
18+
save_db_in_config_dir: bool = False
19+
"""Define if langflow database should be saved in LANGFLOW_CONFIG_DIR or in the langflow directory
20+
(i.e. in the package directory)."""
21+
22+
database_url: str | None = None
23+
"""Database URL for Langflow. If not provided, Langflow will use a SQLite database.
24+
The driver shall be an async one like `sqlite+aiosqlite` (`sqlite` and `postgresql`
25+
will be automatically converted to the async drivers `sqlite+aiosqlite` and
26+
`postgresql+psycopg` respectively)."""
27+
28+
database_connection_retry: bool = False
29+
"""If True, Langflow will retry to connect to the database if it fails."""
30+
31+
pool_size: int = 20
32+
"""The number of connections to keep open in the connection pool.
33+
For high load scenarios, this should be increased based on expected concurrent users."""
34+
35+
max_overflow: int = 30
36+
"""The number of connections to allow that can be opened beyond the pool size.
37+
Should be 2x the pool_size for optimal performance under load."""
38+
39+
db_connect_timeout: int = 30
40+
"""The number of seconds to wait before giving up on a lock to released or establishing a connection to the
41+
database."""
42+
43+
migration_lock_namespace: str | None = None
44+
"""Optional namespace identifier for PostgreSQL advisory lock during migrations.
45+
If not provided, a hash of the database URL will be used. Useful when multiple Langflow
46+
instances share the same database and need coordinated migration locking."""
47+
48+
sqlite_pragmas: dict | None = {"synchronous": "NORMAL", "journal_mode": "WAL", "busy_timeout": 30000}
49+
"""SQLite pragmas to use when connecting to the database."""
50+
51+
db_driver_connection_settings: dict | None = None
52+
"""Database driver connection settings."""
53+
54+
db_connection_settings: dict | None = {
55+
"pool_size": 20,
56+
"max_overflow": 30,
57+
"pool_timeout": 30,
58+
"pool_pre_ping": True,
59+
"pool_recycle": 1800,
60+
"echo": False,
61+
}
62+
"""Database connection settings optimized for high load scenarios.
63+
Note: These settings are most effective with PostgreSQL. For SQLite:
64+
- Reduce pool_size and max_overflow if experiencing lock contention
65+
- SQLite has limited concurrent write capability even with WAL mode
66+
- Best for read-heavy or moderate write workloads
67+
68+
Settings:
69+
- pool_size: Number of connections to maintain (increase for higher concurrency)
70+
- max_overflow: Additional connections allowed beyond pool_size
71+
- pool_timeout: Seconds to wait for an available connection
72+
- pool_pre_ping: Validates connections before use to prevent stale connections
73+
- pool_recycle: Seconds before connections are recycled (prevents timeouts)
74+
- echo: Enable SQL query logging (development only)
75+
"""
76+
77+
use_noop_database: bool = False
78+
"""If True, disables all database operations and uses a no-op session.
79+
Controlled by LANGFLOW_USE_NOOP_DATABASE env variable."""
80+
81+
@field_validator("use_noop_database", mode="before")
82+
@classmethod
83+
def set_use_noop_database(cls, value):
84+
if value:
85+
logger.info("Running with NOOP database session. All DB operations are disabled.")
86+
return value
87+
88+
@field_validator("database_url", mode="before")
89+
@classmethod
90+
def set_database_url(cls, value, info):
91+
if value and not is_valid_database_url(value):
92+
sanitized = sanitize_database_url(value)
93+
msg = f"Invalid database_url provided: '{sanitized}'"
94+
raise ValueError(msg)
95+
96+
if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"):
97+
value = langflow_database_url
98+
logger.debug("Using LANGFLOW_DATABASE_URL env variable")
99+
else:
100+
if not info.data.get("config_dir"):
101+
msg = "config_dir not set, please set it or provide a database_url"
102+
raise ValueError(msg)
103+
104+
from lfx.utils.version import get_version_info
105+
from lfx.utils.version import is_pre_release as langflow_is_pre_release
106+
107+
version = get_version_info()["version"]
108+
is_pre_release = langflow_is_pre_release(version)
109+
110+
if info.data["save_db_in_config_dir"]:
111+
database_dir = info.data["config_dir"]
112+
else:
113+
try:
114+
import langflow
115+
116+
database_dir = Path(langflow.__file__).parent.resolve()
117+
except ImportError:
118+
database_dir = Path(__file__).parent.parent.parent.parent.resolve()
119+
120+
pre_db_file_name = "langflow-pre.db"
121+
db_file_name = "langflow.db"
122+
new_pre_path = f"{database_dir}/{pre_db_file_name}"
123+
new_path = f"{database_dir}/{db_file_name}"
124+
final_path = None
125+
if is_pre_release:
126+
if Path(new_pre_path).exists():
127+
final_path = new_pre_path
128+
elif Path(new_path).exists() and info.data["save_db_in_config_dir"]:
129+
logger.debug("Copying existing database to new location")
130+
copy2(new_path, new_pre_path)
131+
logger.debug(f"Copied existing database to {new_pre_path}")
132+
elif Path(f"./{db_file_name}").exists() and info.data["save_db_in_config_dir"]:
133+
logger.debug("Copying existing database to new location")
134+
copy2(f"./{db_file_name}", new_pre_path)
135+
logger.debug(f"Copied existing database to {new_pre_path}")
136+
else:
137+
logger.debug(f"Creating new database at {new_pre_path}")
138+
final_path = new_pre_path
139+
elif Path(new_path).exists():
140+
final_path = new_path
141+
elif Path(f"./{db_file_name}").exists():
142+
try:
143+
logger.debug("Copying existing database to new location")
144+
copy2(f"./{db_file_name}", new_path)
145+
logger.debug(f"Copied existing database to {new_path}")
146+
except OSError:
147+
logger.exception("Failed to copy database, using default path")
148+
new_path = f"./{db_file_name}"
149+
else:
150+
final_path = new_path
151+
152+
if final_path is None:
153+
final_path = new_pre_path if is_pre_release else new_path
154+
155+
value = f"sqlite:///{final_path}"
156+
157+
return value
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import re
2+
3+
from pydantic import BaseModel, field_validator
4+
5+
from lfx.log.logger import logger
6+
7+
8+
class McpSettings(BaseModel):
9+
"""MCP server, session manager, and composer settings."""
10+
11+
mcp_base_url: str = ""
12+
"""External base URL used to build MCP server URLs in the UI configuration JSON
13+
(e.g. 'https://langflow.example.com'). When empty, the frontend falls back to
14+
the browser's window.location.origin."""
15+
16+
mcp_server_timeout: int = 20
17+
"""Timeout in seconds for MCP server operations (tool calls, server requests)."""
18+
19+
# ---------------------------------------------------------------------
20+
# MCP Session-manager tuning
21+
# ---------------------------------------------------------------------
22+
mcp_max_sessions_per_server: int = 10
23+
"""Maximum number of MCP sessions to keep per unique server (command/url).
24+
Mirrors the default constant MAX_SESSIONS_PER_SERVER in util.py. Adjust to
25+
control resource usage or concurrency per server."""
26+
27+
mcp_session_idle_timeout: int = 400 # seconds (~6.7 minutes)
28+
"""How long (in seconds) an MCP session can stay idle before the background
29+
cleanup task disposes of it."""
30+
31+
mcp_session_cleanup_interval: int = 120 # seconds
32+
"""Frequency (in seconds) at which the background cleanup task wakes up to
33+
reap idle sessions."""
34+
35+
# MCP Server
36+
mcp_server_enabled: bool = True
37+
"""If set to False, Langflow will not enable the MCP server."""
38+
mcp_server_enable_progress_notifications: bool = False
39+
"""If set to False, Langflow will not send progress notifications in the MCP server."""
40+
41+
# Add projects to MCP servers automatically on creation
42+
add_projects_to_mcp_servers: bool = True
43+
"""If set to True, newly created projects will be added to the user's MCP servers config automatically."""
44+
45+
# MCP Composer
46+
mcp_composer_enabled: bool = True
47+
"""If set to False, Langflow will not start the MCP Composer service."""
48+
mcp_composer_version: str = "==0.1.0.8.10"
49+
"""Version constraint for mcp-composer when using uvx. Uses PEP 440 syntax."""
50+
51+
@field_validator("mcp_composer_version", mode="before")
52+
@classmethod
53+
def validate_mcp_composer_version(cls, value):
54+
"""Ensure the version string has a version specifier prefix.
55+
56+
If a bare version like '0.1.0.7' is provided, prepend '~=' to allow patch updates.
57+
Supports PEP 440 specifiers: ==, !=, <=, >=, <, >, ~=, ===
58+
"""
59+
if not value:
60+
return "==0.1.0.8.10" # Default
61+
62+
specifiers = ["===", "==", "!=", "<=", ">=", "~=", "<", ">"]
63+
if any(value.startswith(spec) for spec in specifiers):
64+
return value
65+
66+
if re.match(r"^\d+(\.\d+)*", value):
67+
logger.debug(f"Adding ~= prefix to bare version '{value}' -> '~={value}'")
68+
return f"~={value}"
69+
70+
return value
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from pydantic import BaseModel
2+
3+
4+
class ObservabilitySettings(BaseModel):
5+
"""Metrics exposure and historical record retention."""
6+
7+
prometheus_enabled: bool = False
8+
"""If set to True, Langflow will expose Prometheus metrics."""
9+
prometheus_port: int = 9090
10+
"""The port on which Langflow will expose Prometheus metrics. 9090 is the default port."""
11+
12+
max_transactions_to_keep: int = 3000
13+
"""The maximum number of transactions to keep in the database."""
14+
max_vertex_builds_to_keep: int = 3000
15+
"""The maximum number of vertex builds to keep in the database."""
16+
max_vertex_builds_per_vertex: int = 50
17+
"""The maximum number of builds to keep per vertex. Older builds will be deleted."""
18+
max_flow_version_entries_per_flow: int = 50
19+
"""Max version history entries per flow. Oldest entries pruned on next snapshot.
20+
21+
If retroactively lowered below the current count for a flow,
22+
the oldest entries are deleted only when the next entry is created.
23+
"""

0 commit comments

Comments
 (0)