849 lines
30 KiB
Python
849 lines
30 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""
|
|
LMCache Configuration Base Module
|
|
|
|
This module provides common configuration utilities and base classes
|
|
for all LMCache configuration systems to avoid code duplication.
|
|
"""
|
|
|
|
# Standard
|
|
from dataclasses import make_dataclass
|
|
from typing import Any, Callable, Dict, Optional, Protocol, Union
|
|
import ast
|
|
import json
|
|
import os
|
|
import threading
|
|
import uuid
|
|
|
|
# Third Party
|
|
import requests
|
|
import yaml
|
|
|
|
# First Party
|
|
from lmcache.logging import init_logger
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
|
|
def _apply_env_converter_safely(config_definitions, name, value):
|
|
"""Apply env_converter to a value safely."""
|
|
if name not in config_definitions:
|
|
return value
|
|
|
|
config = config_definitions[name]
|
|
env_converter = config.get("env_converter")
|
|
if env_converter:
|
|
try:
|
|
# Don't apply converter if value is None
|
|
if value is None:
|
|
return None
|
|
return env_converter(value)
|
|
except (ValueError, json.JSONDecodeError) as e:
|
|
log_message = f"Failed to convert value for {name}={value!r}: {e}"
|
|
logger.warning(log_message)
|
|
# Return None if conversion fails
|
|
return None
|
|
return value
|
|
|
|
|
|
# Common configuration parsing utilities
|
|
def _parse_local_disk(local_disk) -> Optional[str]:
|
|
"""Parse local disk path configuration.
|
|
|
|
Accepts a single path or comma-separated paths, each optionally
|
|
prefixed with ``file://``. Returns a comma-joined string of bare
|
|
directory paths suitable for ``LocalDiskBackend``.
|
|
|
|
Examples::
|
|
|
|
"file:///mnt/nvme0/" -> "/mnt/nvme0/"
|
|
"/mnt/nvme0/,/mnt/nvme1/" -> "/mnt/nvme0/,/mnt/nvme1/"
|
|
"file:///mnt/nvme0/,file:///mnt/nvme1/"
|
|
-> "/mnt/nvme0/,/mnt/nvme1/"
|
|
|
|
Args:
|
|
local_disk: Raw config value — ``None``, a single path, or
|
|
comma-separated paths.
|
|
|
|
Returns:
|
|
Comma-joined bare directory paths, or ``None`` if disabled.
|
|
"""
|
|
if local_disk is None:
|
|
return None
|
|
|
|
raw_parts = [p.strip() for p in str(local_disk).split(",") if p.strip()]
|
|
if not raw_parts:
|
|
return None
|
|
|
|
parsed: list[str] = []
|
|
for part in raw_parts:
|
|
if part.startswith("file://"):
|
|
parsed.append(part[7:])
|
|
else:
|
|
parsed.append(part)
|
|
|
|
return ",".join(parsed)
|
|
|
|
|
|
def _to_int_list(
|
|
value: Optional[Union[str, int, list[Any]]],
|
|
) -> Optional[list[int]]:
|
|
"""Convert value to list of integers"""
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, list):
|
|
return [int(x) for x in value]
|
|
if isinstance(value, int):
|
|
return [value]
|
|
parts = [p.strip() for p in str(value).split(",") if p.strip()]
|
|
return [int(p) for p in parts]
|
|
|
|
|
|
def _to_float_list(
|
|
value: Optional[Union[str, float, list[Any]]],
|
|
) -> Optional[list[float]]:
|
|
"""Convert value to list of floats"""
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, list):
|
|
return [float(x) for x in value]
|
|
if isinstance(value, float):
|
|
return [value]
|
|
parts = [p.strip() for p in str(value).split(",") if p.strip()]
|
|
return [float(p) for p in parts]
|
|
|
|
|
|
def _to_str_list(
|
|
value: Optional[Union[str, list[str]]],
|
|
) -> Optional[list[str]]:
|
|
"""Convert value to list of strings"""
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, list):
|
|
return value
|
|
parts = [p.strip() for p in value.split(",") if p.strip()]
|
|
return [p for p in parts]
|
|
|
|
|
|
def _to_bool(
|
|
value: Optional[Union[bool, int, str]],
|
|
) -> bool:
|
|
"""Convert value to boolean"""
|
|
if isinstance(value, bool):
|
|
return value
|
|
return str(value).strip().lower() in ["true", "1"]
|
|
|
|
|
|
def _parse_quoted_string(value: str) -> str:
|
|
"""Parse a string that may be surrounded by quotes and handle escape characters.
|
|
|
|
Args:
|
|
value: The input string that may be quoted
|
|
|
|
Returns:
|
|
The unquoted string with escape characters properly handled
|
|
"""
|
|
if not value:
|
|
return value
|
|
|
|
value = value.strip()
|
|
|
|
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
|
|
try:
|
|
evaluated = ast.literal_eval(value)
|
|
if isinstance(evaluated, str):
|
|
return evaluated
|
|
except (ValueError, SyntaxError):
|
|
# If ast.literal_eval fails, it's not a valid Python literal.
|
|
# Fall back to simply stripping the outer quotes.
|
|
return value[1:-1]
|
|
|
|
return value
|
|
|
|
|
|
def _to_json(obj: Any) -> str:
|
|
"""Convert object to JSON string"""
|
|
# If object has to_dict method, use it to convert to dict first
|
|
if hasattr(obj, "to_dict"):
|
|
return json.dumps(obj.to_dict(), indent=2)
|
|
# Otherwise try to serialize directly
|
|
return json.dumps(obj, indent=2)
|
|
|
|
|
|
def _from_json(cls, json_str: str):
|
|
"""Deserialize a JSON string into a configuration object."""
|
|
try:
|
|
config_dict = json.loads(json_str)
|
|
return cls.from_dict(config_dict)
|
|
except json.JSONDecodeError as e:
|
|
logger.error("Invalid JSON input: %s", e)
|
|
raise
|
|
|
|
|
|
# Configuration aliases and deprecated mappings utility
|
|
def _resolve_config_aliases(
|
|
config_dict: dict,
|
|
source: str,
|
|
config_definitions: dict,
|
|
config_aliases: dict,
|
|
deprecated_configs: dict,
|
|
) -> dict:
|
|
"""Resolve configuration aliases and handle deprecated configurations."""
|
|
resolved = {}
|
|
|
|
# Process each key in the input
|
|
for key, value in config_dict.items():
|
|
if key in deprecated_configs:
|
|
# Log deprecation warning
|
|
logger.warning("%s (source: %s)", deprecated_configs[key], source)
|
|
|
|
# Map to new key if alias exists
|
|
if key in config_aliases:
|
|
new_key = config_aliases[key]
|
|
resolved[new_key] = value
|
|
else:
|
|
# Keep deprecated key for backward compatibility
|
|
resolved[key] = value
|
|
elif key in config_definitions:
|
|
# Valid configuration key
|
|
resolved[key] = value
|
|
else:
|
|
# Unknown configuration key
|
|
logger.warning("Unknown configuration key: %s (source: %s)", key, source)
|
|
|
|
return resolved
|
|
|
|
|
|
# Base configuration class creator
|
|
def create_config_class(
|
|
config_name: str,
|
|
config_definitions: dict[str, dict[str, Any]],
|
|
config_aliases: Optional[dict[str, str]] = None,
|
|
deprecated_configs: Optional[dict[str, str]] = None,
|
|
namespace_extras: Optional[dict[str, Any]] = None,
|
|
env_prefix: str = "LMCACHE_",
|
|
):
|
|
"""Create a configuration class dynamically with common functionality.
|
|
|
|
Args:
|
|
config_name: Name of the configuration class
|
|
config_definitions: Dictionary of configuration definitions
|
|
config_aliases: Optional mapping of deprecated names to current names
|
|
deprecated_configs: Optional mapping of deprecated names to warning messages
|
|
namespace_extras: Optional additional namespace items for the class
|
|
env_prefix: Environment variable prefix (default: "LMCACHE_")
|
|
|
|
Returns:
|
|
A dynamically created dataclass with configuration functionality
|
|
"""
|
|
# Default values
|
|
config_aliases = config_aliases or {}
|
|
deprecated_configs = deprecated_configs or {}
|
|
namespace_extras = namespace_extras or {}
|
|
|
|
# Extract fields from configuration definitions
|
|
fields_dict = {}
|
|
for name, config in config_definitions.items():
|
|
fields_dict[name] = (config["type"], config["default"])
|
|
|
|
def _post_init(self):
|
|
"""Post-initialization setup"""
|
|
# Initialize user-set keys tracking set
|
|
# This tracks which config keys were explicitly set by user
|
|
# (via file, env vars, or overrides) vs. using default values
|
|
if not hasattr(self, "_user_set_keys"):
|
|
object.__setattr__(self, "_user_set_keys", set())
|
|
# Generate instance ID if not set
|
|
if not getattr(self, "lmcache_instance_id", None):
|
|
self.lmcache_instance_id = f"{config_name.lower()}_{uuid.uuid4().hex}"
|
|
|
|
def _from_env(cls):
|
|
"""Load configuration from environment variables"""
|
|
|
|
def get_env_name(attr_name: str) -> str:
|
|
return f"{env_prefix}{attr_name.upper()}"
|
|
|
|
# Collect all defined and deprecated env vars
|
|
all_keys = list(config_definitions.keys()) + list(config_aliases.keys())
|
|
env_config = {}
|
|
for name in all_keys:
|
|
env_name = get_env_name(name)
|
|
env_value = os.getenv(env_name)
|
|
if env_value is not None:
|
|
env_config[name] = env_value
|
|
|
|
# Resolve aliases and handle deprecated configurations
|
|
resolved_config = _resolve_config_aliases(
|
|
env_config,
|
|
"environment variables",
|
|
config_definitions,
|
|
config_aliases,
|
|
deprecated_configs,
|
|
)
|
|
|
|
config_values = {}
|
|
user_set_keys = set() # Track keys explicitly set by user
|
|
for name, config in config_definitions.items():
|
|
if name in resolved_config:
|
|
try:
|
|
raw_value = resolved_config[name]
|
|
value = _parse_quoted_string(raw_value)
|
|
# Apply env_converter safely
|
|
config_values[name] = _apply_env_converter_safely(
|
|
config_definitions, name, value
|
|
)
|
|
user_set_keys.add(name) # Mark as user-set
|
|
except (ValueError, json.JSONDecodeError) as e:
|
|
raw_value_for_log = resolved_config.get(name, "unknown value")
|
|
log_message = (
|
|
f"Failed to parse {get_env_name(name)}"
|
|
f"={raw_value_for_log!r}: {e}"
|
|
)
|
|
logger.warning(log_message)
|
|
# Use default value with conversion
|
|
config_values[name] = _apply_env_converter_safely(
|
|
config_definitions, name, config["default"]
|
|
)
|
|
else:
|
|
# Use default value with conversion
|
|
config_values[name] = _apply_env_converter_safely(
|
|
config_definitions, name, config["default"]
|
|
)
|
|
|
|
instance = cls(**config_values)
|
|
# Store user-set keys in the instance
|
|
object.__setattr__(instance, "_user_set_keys", user_set_keys)
|
|
return instance
|
|
|
|
def _from_file(cls, file_path: str):
|
|
"""Load configuration from file"""
|
|
with open(file_path, "r") as fin:
|
|
file_config = yaml.safe_load(fin) or {}
|
|
|
|
# Resolve aliases and handle deprecated configurations
|
|
resolved_config = _resolve_config_aliases(
|
|
file_config,
|
|
f"file: {file_path}",
|
|
config_definitions,
|
|
config_aliases,
|
|
deprecated_configs,
|
|
)
|
|
|
|
config_values = {}
|
|
user_set_keys = set() # Track keys explicitly set by user
|
|
for name, config in config_definitions.items():
|
|
if name in resolved_config:
|
|
value = resolved_config[name]
|
|
user_set_keys.add(name) # Mark as user-set
|
|
else:
|
|
value = config["default"]
|
|
# Apply env_converter safely regardless of whether value is None or not
|
|
config_values[name] = _apply_env_converter_safely(
|
|
config_definitions, name, value
|
|
)
|
|
|
|
instance = cls(**config_values)
|
|
# Store user-set keys in the instance
|
|
object.__setattr__(instance, "_user_set_keys", user_set_keys)
|
|
return instance
|
|
|
|
def _from_defaults(cls, **kwargs):
|
|
"""Create configuration from defaults"""
|
|
config_values = {}
|
|
user_set_keys = set() # Track keys explicitly set by user
|
|
for name, config in config_definitions.items():
|
|
if name in kwargs:
|
|
value = kwargs[name]
|
|
user_set_keys.add(name) # Mark as user-set
|
|
else:
|
|
value = config["default"]
|
|
# Apply env_converter safely regardless of whether value is None or not
|
|
config_values[name] = _apply_env_converter_safely(
|
|
config_definitions, name, value
|
|
)
|
|
|
|
instance = cls(**config_values)
|
|
# Store user-set keys in the instance
|
|
object.__setattr__(instance, "_user_set_keys", user_set_keys)
|
|
return instance
|
|
|
|
def _update_config_from_env(self):
|
|
"""Update an existing config object with environment variable configurations."""
|
|
|
|
def get_env_name(attr_name: str) -> str:
|
|
return f"{env_prefix}{attr_name.upper()}"
|
|
|
|
env_config = {}
|
|
# Collect all defined and deprecated env vars
|
|
all_keys = list(config_definitions.keys()) + list(config_aliases.keys())
|
|
for name in all_keys:
|
|
env_name = get_env_name(name)
|
|
env_value = os.getenv(env_name)
|
|
if env_value is not None:
|
|
env_config[name] = env_value
|
|
|
|
# Resolve aliases
|
|
resolved_config = _resolve_config_aliases(
|
|
env_config,
|
|
"environment variables",
|
|
config_definitions,
|
|
config_aliases,
|
|
deprecated_configs,
|
|
)
|
|
|
|
# Ensure _user_set_keys exists
|
|
if not hasattr(self, "_user_set_keys"):
|
|
object.__setattr__(self, "_user_set_keys", set())
|
|
|
|
# Update config object
|
|
for name, config in config_definitions.items():
|
|
if name in resolved_config:
|
|
try:
|
|
raw_value = resolved_config[name]
|
|
value = _parse_quoted_string(raw_value)
|
|
converted_value = config["env_converter"](value)
|
|
setattr(self, name, converted_value)
|
|
# Mark as user-set
|
|
self._user_set_keys.add(name)
|
|
except (ValueError, json.JSONDecodeError) as e:
|
|
raw_value_for_log = resolved_config.get(name, "unknown value")
|
|
log_message = (
|
|
f"Failed to parse {get_env_name(name)}"
|
|
f"={raw_value_for_log!r}: {e}"
|
|
)
|
|
logger.warning(log_message)
|
|
|
|
return self
|
|
|
|
def _from_dict(cls, config_dict: dict):
|
|
"""Create configuration from a dictionary."""
|
|
resolved_config = _resolve_config_aliases(
|
|
config_dict,
|
|
"dictionary input",
|
|
config_definitions,
|
|
config_aliases,
|
|
deprecated_configs,
|
|
)
|
|
config_values = {}
|
|
user_set_keys = set() # Track keys explicitly set by user
|
|
for name, config in config_definitions.items():
|
|
if name in resolved_config:
|
|
value = resolved_config[name]
|
|
user_set_keys.add(name) # Mark as user-set
|
|
else:
|
|
value = config["default"]
|
|
if value is not None:
|
|
value = config["env_converter"](value)
|
|
config_values[name] = value
|
|
instance = cls(**config_values)
|
|
# Store user-set keys in the instance
|
|
object.__setattr__(instance, "_user_set_keys", user_set_keys)
|
|
return instance
|
|
|
|
def _to_dict(self):
|
|
"""Convert the configuration object into a dictionary."""
|
|
return {name: getattr(self, name) for name in config_definitions}
|
|
|
|
# Build namespace
|
|
namespace = {
|
|
"__post_init__": _post_init,
|
|
"from_defaults": classmethod(_from_defaults),
|
|
"from_file": classmethod(_from_file),
|
|
"from_env": classmethod(_from_env),
|
|
"update_config_from_env": _update_config_from_env,
|
|
"from_dict": classmethod(_from_dict),
|
|
"to_dict": _to_dict,
|
|
"to_json": _to_json,
|
|
"from_json": classmethod(_from_json),
|
|
"__str__": lambda self: str(
|
|
{name: getattr(self, name) for name in config_definitions}
|
|
),
|
|
}
|
|
|
|
# Add extra namespace items
|
|
namespace.update(namespace_extras)
|
|
|
|
# Create class
|
|
cls = make_dataclass(
|
|
config_name,
|
|
[(name, type_, default) for name, (type_, default) in fields_dict.items()],
|
|
namespace=namespace,
|
|
)
|
|
|
|
# Add config_definitions as a class attribute for accessing converters
|
|
cls._config_definitions = config_definitions # type: ignore[attr-defined]
|
|
|
|
return cls
|
|
|
|
|
|
# Thread-safe singleton utility
|
|
class SingletonGetter(Protocol):
|
|
"""Protocol for singleton getter functions"""
|
|
|
|
def __call__(self) -> Any: ...
|
|
|
|
reset: Callable[[], None]
|
|
|
|
|
|
def create_singleton_config(
|
|
getter_func_name: str,
|
|
config_class,
|
|
config_env_var: str = "LMCACHE_CONFIG_FILE",
|
|
) -> SingletonGetter:
|
|
"""Create thread-safe singleton configuration access pattern.
|
|
|
|
Args:
|
|
getter_func_name: Name for the singleton getter function
|
|
config_class: The configuration class to create singleton for
|
|
config_env_var: Environment variable name for configuration file path
|
|
"""
|
|
|
|
_config_instance = None
|
|
_config_lock = threading.Lock()
|
|
|
|
def get_or_create_config() -> config_class:
|
|
"""Get the configuration singleton"""
|
|
nonlocal _config_instance
|
|
|
|
# Double-checked locking for thread-safe singleton
|
|
if _config_instance is None:
|
|
with _config_lock:
|
|
if _config_instance is None: # Check again within lock
|
|
if config_env_var not in os.environ:
|
|
logger.warning(
|
|
"No configuration file is set. Trying to read "
|
|
"configurations from the environment variables."
|
|
)
|
|
logger.warning(
|
|
f"You can set the configuration file through "
|
|
f"the environment variable: {config_env_var}"
|
|
)
|
|
_config_instance = config_class.from_env()
|
|
else:
|
|
config_file = os.environ[config_env_var]
|
|
logger.info("Loading config file %s", config_file)
|
|
_config_instance = config_class.from_file(config_file)
|
|
# Update config from environment variables
|
|
_config_instance.update_config_from_env()
|
|
|
|
return _config_instance
|
|
|
|
def reset_config_instance() -> None:
|
|
"""Reset the configuration singleton for testing"""
|
|
nonlocal _config_instance
|
|
with _config_lock:
|
|
_config_instance = None
|
|
|
|
# Set the function name for better debugging
|
|
get_or_create_config.__name__ = getter_func_name
|
|
get_or_create_config.reset = reset_config_instance # type: ignore[attr-defined]
|
|
|
|
return get_or_create_config # type: ignore[return-value]
|
|
|
|
|
|
def load_config_with_overrides(
|
|
config_class,
|
|
config_file_env_var: str = "LMCACHE_CONFIG_FILE",
|
|
config_file_path: Optional[str] = None,
|
|
overrides: Optional[Dict[str, Any]] = None,
|
|
):
|
|
"""
|
|
Load configuration with support for file, environment variables, and overrides.
|
|
|
|
This is a generic utility function that can be reused across different
|
|
configuration classes (LMCacheEngineConfig, ControllerConfig, etc.)
|
|
|
|
Args:
|
|
config_class: The configuration class to instantiate
|
|
config_file_env_var: Environment variable name for config file path
|
|
config_file_path: Optional direct config file path (overrides env var)
|
|
overrides: Optional dictionary of configuration overrides
|
|
|
|
Returns:
|
|
Loaded and validated configuration instance
|
|
"""
|
|
# Load configuration from file or environment
|
|
actual_config_path = config_file_path or os.getenv(config_file_env_var)
|
|
|
|
if actual_config_path:
|
|
logger.info("Loading config file: %s", actual_config_path)
|
|
config = config_class.from_file(actual_config_path)
|
|
# Allow environment variables to override file settings
|
|
config.update_config_from_env()
|
|
else:
|
|
logger.info("No config file specified, loading from environment variables.")
|
|
config = config_class.from_env()
|
|
|
|
# Apply any overrides
|
|
if overrides:
|
|
for key, value in overrides.items():
|
|
if hasattr(config, key):
|
|
old_value = getattr(config, key)
|
|
|
|
# Check if this configuration class has definitions with converters
|
|
if (
|
|
hasattr(config, "_config_definitions")
|
|
and key in config._config_definitions
|
|
):
|
|
# Use the global helper function to safely apply env_converter
|
|
new_value = _apply_env_converter_safely(
|
|
config._config_definitions, key, value
|
|
)
|
|
setattr(config, key, new_value)
|
|
else:
|
|
setattr(config, key, value)
|
|
|
|
new_value = getattr(config, key)
|
|
if old_value != new_value:
|
|
logger.info(
|
|
"Override config: %s = %s (was %s)", key, new_value, old_value
|
|
)
|
|
else:
|
|
logger.warning("Unknown config key: %s, ignoring", key)
|
|
|
|
# Validate configuration
|
|
if hasattr(config, "validate"):
|
|
config.validate()
|
|
|
|
# Log configuration
|
|
if hasattr(config, "log_config"):
|
|
config.log_config()
|
|
|
|
return config
|
|
|
|
|
|
def parse_command_line_extra_params(extra_args: list[str]) -> dict[str, Any]:
|
|
"""
|
|
Parse extra command-line parameters in key=value format.
|
|
|
|
Args:
|
|
extra_args: List of strings in format "key=value"
|
|
|
|
Returns:
|
|
Dictionary of parsed parameters
|
|
"""
|
|
params = {}
|
|
for arg in extra_args:
|
|
if "=" in arg:
|
|
key, value = arg.split("=", 1)
|
|
key = key.lstrip("-")
|
|
try:
|
|
if value.lower() in ("true", "false"):
|
|
params[key] = value.lower() == "true"
|
|
elif value.isdigit():
|
|
params[key] = int(value) # type: ignore[assignment]
|
|
elif value.replace(".", "", 1).isdigit():
|
|
params[key] = float(value) # type: ignore[assignment]
|
|
else:
|
|
params[key] = value # type: ignore[assignment]
|
|
except ValueError:
|
|
params[key] = value # type: ignore[assignment]
|
|
logger.info("Extra parameter: %s = %s", key, params[key])
|
|
return params
|
|
|
|
|
|
def validate_and_set_config_value(config, config_key, value, override: bool = True):
|
|
"""Validate and set configuration value.
|
|
|
|
Args:
|
|
config: Configuration object to update.
|
|
config_key: The configuration key to set.
|
|
value: The value to set.
|
|
override: If True, completely replace the value. If False:
|
|
- For 'extra_config': merge with existing dict (new values take
|
|
precedence for conflicting keys).
|
|
- For other keys: skip if key was user-set.
|
|
Default is True.
|
|
|
|
Returns:
|
|
True if the value was set successfully, False otherwise.
|
|
"""
|
|
if not hasattr(config, config_key):
|
|
logger.warning("Config key '%s' does not exist in configuration", config_key)
|
|
return False
|
|
|
|
try:
|
|
# Apply type conversion using env_converter if available
|
|
# Skip for extra_config which has special handling below
|
|
if hasattr(config, "_config_definitions") and config_key != "extra_config":
|
|
original_value = value
|
|
value = _apply_env_converter_safely(
|
|
config._config_definitions, config_key, value
|
|
)
|
|
# If original value was not None but conversion returned None,
|
|
# it means conversion failed
|
|
if original_value is not None and value is None:
|
|
# _apply_env_converter_safely already logged warning
|
|
return False
|
|
|
|
# Convert string to dict for extra_config
|
|
if config_key == "extra_config" and isinstance(value, str):
|
|
value = json.loads(value) if value else None
|
|
|
|
# Handle extra_config special logic
|
|
if config_key == "extra_config":
|
|
current_value = getattr(config, config_key, None)
|
|
|
|
# Handle None or empty value when override=False
|
|
if not override and (value is None or value == ""):
|
|
# Keep current value
|
|
logger.info(
|
|
"Keeping current extra_config (override=False, value is None/empty)"
|
|
)
|
|
return True
|
|
|
|
if override:
|
|
# Full override: replace current value with new value
|
|
setattr(config, config_key, value)
|
|
logger.info("Overridden extra_config")
|
|
return True
|
|
else:
|
|
# Merge mode (override=False)
|
|
if value is not None and isinstance(value, dict):
|
|
if current_value is not None and isinstance(current_value, dict):
|
|
# Merge: current values preserved, new values override
|
|
merged_value = {**current_value, **value}
|
|
setattr(config, config_key, merged_value)
|
|
logger.info("Merged extra_config")
|
|
else:
|
|
# Current value is None or not a dict, set new value
|
|
setattr(config, config_key, value)
|
|
logger.info("Set extra_config")
|
|
return True
|
|
|
|
# For non-extra_config keys: skip if override=False and key user-set
|
|
if not override:
|
|
user_set_keys: set[str] = getattr(config, "_user_set_keys", set())
|
|
if config_key in user_set_keys:
|
|
current_value = getattr(config, config_key, None)
|
|
logger.info(
|
|
"Skipping config %s (override=False, user-set value=%s)",
|
|
config_key,
|
|
current_value,
|
|
)
|
|
# Return True to indicate operation completed (kept existing)
|
|
return True
|
|
|
|
setattr(config, config_key, value)
|
|
logger.info("Set config item '%s'", config_key)
|
|
return True
|
|
except Exception as e:
|
|
logger.error(
|
|
"Failed to set config item '%s' with value %s: %s",
|
|
config_key,
|
|
value,
|
|
e,
|
|
)
|
|
return False
|
|
|
|
|
|
def fetch_remote_config(
|
|
remote_config_url: str,
|
|
app_id: Optional[str],
|
|
config: Any,
|
|
timeout: int = 10,
|
|
) -> Optional[dict]:
|
|
"""Fetch configuration from remote config service.
|
|
|
|
The config server protocol:
|
|
- Request: POST with JSON body containing 'current_config' and 'env_variables'
|
|
- Query parameter: 'appId' if app_id is provided
|
|
- Response: JSON with 'configs' array, each item has 'key', 'value', 'override'
|
|
|
|
See examples/remote_config_server/ for a reference implementation.
|
|
|
|
Args:
|
|
remote_config_url: URL of the remote config service.
|
|
app_id: Optional app ID to send to the config service.
|
|
config: Current LMCacheEngineConfig to send to the config service.
|
|
timeout: Request timeout in seconds.
|
|
|
|
Returns:
|
|
Parsed JSON response from the config service, or None if failed.
|
|
"""
|
|
try:
|
|
# Build request payload with current config and env variables
|
|
payload: dict[str, Any] = {
|
|
"current_config": config.to_dict(),
|
|
"env_variables": {k: v for k, v in os.environ.items()},
|
|
}
|
|
|
|
# Build URL with app_id query parameter if provided
|
|
params = {"appId": app_id} if app_id else None
|
|
|
|
# Send POST request with JSON payload
|
|
response = requests.get(
|
|
remote_config_url,
|
|
json=payload,
|
|
params=params,
|
|
headers={"Content-Type": "application/json"},
|
|
timeout=timeout,
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
except requests.RequestException as e:
|
|
logger.warning(
|
|
"Failed to fetch remote config from %s: %s", remote_config_url, e
|
|
)
|
|
return None
|
|
except json.JSONDecodeError as e:
|
|
logger.warning("Failed to parse remote config response: %s", e)
|
|
return None
|
|
except Exception as e:
|
|
logger.warning("Unexpected error fetching remote config: %s", e)
|
|
return None
|
|
|
|
|
|
def apply_remote_configs(config: Any, remote_response: dict) -> Any:
|
|
"""Apply remote configuration to LMCacheEngineConfig.
|
|
|
|
This function extracts the 'configs' field from the remote response
|
|
and applies each config item to the LMCacheEngineConfig instance.
|
|
|
|
The expected format of remote_response['configs'] is:
|
|
[
|
|
{"override": true, "key": "config_key", "value": "config_value"},
|
|
...
|
|
]
|
|
|
|
Args:
|
|
config: LMCacheEngineConfig instance to update.
|
|
remote_response: Response from the remote config service.
|
|
|
|
Returns:
|
|
Updated LMCacheEngineConfig instance.
|
|
"""
|
|
configs = remote_response.get("configs", [])
|
|
if not configs:
|
|
logger.info("No configs found in remote response")
|
|
return config
|
|
|
|
applied_count = 0
|
|
for config_item in configs:
|
|
if not isinstance(config_item, dict):
|
|
logger.warning("Invalid config item format: %s", config_item)
|
|
continue
|
|
|
|
key = config_item.get("key")
|
|
value = config_item.get("value")
|
|
override = config_item.get("override", True)
|
|
|
|
if not key:
|
|
logger.warning("Config item missing 'key': %s", config_item)
|
|
continue
|
|
|
|
# Try to convert value to appropriate type
|
|
if validate_and_set_config_value(config, key, value, override=override):
|
|
logger.info("Applied remote config: %s=%s", key, value)
|
|
applied_count += 1
|
|
else:
|
|
logger.warning(
|
|
"Failed to apply remote config %s=%s. Using default value.",
|
|
key,
|
|
value,
|
|
)
|
|
|
|
logger.info("Applied %d remote configuration items", applied_count)
|
|
return config
|