ErisPulse.Core.config 模块
模块概述
ErisPulse 配置中心
集中管理所有配置项,避免循环导入问题
提供自动补全缺失配置项的功能
添加内存缓存和延迟写入机制以提高性能
基于 tomlkit 实现注释保留写入:配置文件中的注释与键顺序在任何框架写入后均不丢失
配置写入采用"唯一临时文件 + fsync + 原子替换"(os.replace):
进程被杀(OOM / SIGKILL)或断电时磁盘上的配置文件要么是完整旧内容、要么是完整新内容,
不会出现空文件或半写状态;临时文件名按进程唯一,多实例共享配置目录时不再互相争抢踩踏
提示
- 使用 getConfig(key) / setConfig(key, value) 读写配置
- 配置变更可通过生命周期钩子监听: @lifecycle.on("config.set")
函数列表
parse_bool_config(value: Any)
解析配置中的布尔值
- value (
Any): 配置值(可以是 bool, int, str 等) 返回值 (bool): 解析后的布尔值
提示 支持的值:
- True: True, 1, "true", "True", "1", "yes", "Yes", "on", "On"
- False: False, 0, "false", "False", "0", "no", "No", "off", "Off"
json_safe(value: Any, _depth: int = 0)
递归将任意结构转换为可直接 JSON 序列化的等价结构
供 get_topology 等面向 WebUI 的聚合方法保证输出可序列化:
dict / list / tuple / set 递归处理;类对象(type)取
__name__;其余不可序列化对象退化为 str() 表示。
- value (
任意值): - _depth (internal-use): 递归深度保护 返回值 (可被):json.dumps序列化的等价结构
类列表
class ConfigManager
ConfigManager 类提供相关功能。
方法列表
__init__(config_file: str = DEFAULT_CONFIG_FILE_PATH)
初始化配置管理器
- config_file (
str): 配置文件路径 (默认: "config/config.toml")
_start_config_watcher()
启动后台线程定期检查配置文件变化
当用户手动编辑 config.toml 时,后台线程检测到 mtime 变化后
自动重载缓存并发射 config.updated 生命周期事件。
内部方法
_watch_config_file()
记录配置文件的当前 mtime,用于后续检测外部修改
内部方法
_migrate_config()
迁移旧配置文件到新位置
从项目根目录的 config.toml 迁移到 config/config.toml
内部方法
_load_config()
从文件加载配置到缓存
对加载失败按三种状态分别给出可操作的诊断信息:
- 文件缺失:正常首次启动,静默使用空配置
- TOML 语法错误:输出出错行号/列号与原因,保留上次有效缓存(不擦除)
- 权限/其他错误:输出明确原因,保留上次有效缓存(不擦除)
返回值 (bool): 加载成功(含文件缺失)返回 True;解析/权限等错误返回 False
内部方法
_drop_redundant_dirty_keys()
丢弃与新配置文件内容一致的待写键
配置重载(外部修改)后,部分待写键的值可能已与文件一致, 继续保留会在下次 flush 时用陈旧快照覆盖用户热更新。 仅当待写值已反映在缓存(即文件)中时才丢弃;真正未落盘的写入仍保留。
内部方法
_log_config_error(message: str, level: str = 'error')
将配置加载诊断信息写入日志
内部方法 统一处理 logger 尚未就绪的早期场景,失败时静默忽略。
- message (
str): 日志消息 - level (
str): 日志级别(error/warning/debug)
_malformed_sentinel_path()
跨进程告警冷却哨兵文件路径
位于配置文件同级目录下的隐藏文件,通过其 mtime 实现跨进程去重:
无论 epsdk run 子进程、python main.py 直跑、还是多实例场景,
所有进程共享同一文件系统,自然协调告警频率。
内部方法
_acquire_instance_lock()
尝试以独占方式锁定配置目录的实例锁文件,检测多实例共享配置目录
锁文件位于配置文件同级目录(:data:~.constants.CONFIG_LOCK_FILE_NAME),
进程持锁后全生命周期不释放,由 OS 在进程退出时自动归还——
无需清理逻辑,进程被强杀也不会留下"幽灵锁"。
锁被占用说明另一个 ErisPulse 实例正在使用同一配置目录,
此时仅记录告警(并发写入可能互相覆盖),不阻塞框架启动。
内部方法
_atomic_write_text(text: str)
原子写入配置文件(唯一临时文件 + fsync + os.replace)
旧实现使用固定名 <config>.tmp 并在 rename 前不落盘,存在两类丢文件场景:
多实例共享配置目录时同名临时文件被对端 truncate / rename(ENOENT 争抢);
进程被杀或断电时 rename 元数据先于数据块落盘(ext4 延迟分配),留下空文件。
现改为:同目录 mkstemp 生成进程唯一临时文件 → 写毕 flush + fsync
强制数据落盘 → os.replace 原子替换目标(POSIX / Windows 均原子);
POSIX 下额外 fsync 配置目录,尽力保证断电后替换结果不回退。
- text (
str): 待写入的完整文件内容 异常:OSError- 临时文件创建、写入或替换失败时抛出,由调用方按写失败处理
内部方法
_set_doc_path(doc: Any, keys: list[str], value: Any)
在 tomlkit 文档树中按点分路径写入值
中间层节点缺失或非表时以空表替换(与 dict 语义一致); 叶子写入保留既有注释与顺序,新键追加至所在节末尾。
- doc (
tomlkit): 文档/表对象 - keys (
点分路径拆分后的键列表): - value: 待写入的值(plain dict 会转换为标准 table)
内部方法
_doc_to_plain_dict(doc: Any)
将 tomlkit 文档转为 plain dict 缓存
经 body 低层插入的条目不进入容器索引,直接 unwrap() 会丢失;
渲染后重新解析可保证缓存与文件内容严格一致。
- doc (
tomlkit): 文档对象 返回值 (dict): 纯字典形式的配置内容
内部方法
_flush_config()
将待写入的配置刷新到文件
使用文件锁确保多线程环境下的原子性操作。 基于 tomlkit 在解析出的文档树上做增量修改后整体回写, 文件中已有的注释与键顺序不因框架写入而丢失或重排。
内部方法
_register_atexit()
注册 atexit 钩子,确保进程退出时未持久化的配置被 flush
内部方法
_flush_on_exit()
atexit 回调:进程退出时强制刷新所有脏配置,并清理哨兵文件
内部方法 哨兵文件(
.flush_malformed_cooldown)是运行时跨进程去重的临时标记,
_schedule_write()
安排延迟写入
内部方法
_check_cache_validity()
检查缓存有效性,必要时重新加载
同时检测配置文件是否被外部修改(手动编辑磁盘文件),
若文件 mtime 变化则自动重载。更新内容会在下一次
getConfig 调用时生效,无需重启程序。
内部方法
_check_file_change()
检测配置文件是否被外部程序或用户手动编辑
对比记录的 mtime 与当前文件 mtime,若不一致说明文件已被外部修改。
若变化后的 mtime 与框架自身最后一次刷盘的 mtime(_last_self_write_mtime)
一致,则判定为框架自身的写入而非外部修改,返回 False。
返回值 (bool): 文件是否被外部修改
内部方法
_emit_config_updated(old_config: dict[str, Any])
发射 config.updated 生命周期事件,通知适配器/模块配置已变更
用户手动编辑 config.toml 后,下一次 getConfig 调用会自动检测
到文件变更并触发此事件。适配器通过 on_config_update(old, new) 响应。
- old_config (
变更前的配置快照): > 内部方法
getConfig(key: str, default: Any = None)
获取配置项
支持点分隔符路径(如 "module.sub.key")。当存在待写入队列
(延迟刷盘未落盘的 setConfig)时,读取结果会叠加待写值,
保证"写后立读"一致性:
查询键精确命中待写队列 → 直接返回待写值
待写键是查询键的祖先 → 在待写值子树内继续解析
待写键是查询键的后代 → 以待写值深合并覆盖缓存子树
key (
str): 配置键, 支持点分隔符如 "module.sub.key"default (
Any): 默认值 (默认: None) 返回值 (Any): 配置值
示例:
>>> value = sdk.config.getConfig("ErisPulse.server.port", 8000)
_walk_cache(key: str, default: Any)
内部方法 在缓存树中按点分路径取值;路径缺失或中间节点非字典时返回 default
- key (
点分配置键): - default: 路径缺失时的默认值 返回值 (缓存中的值或): default
_dirty_overlay(keys: list[str])
内部方法 收集以待查键为前缀的待写键,构建叠加子树
- keys (
查询键的路径段列表): 返回值: 叠加子树(无匹配时为空字典)
_deep_merge(base: dict[str, Any], override: dict[str, Any])
内部方法 深合并字典(override 优先,仅 dict 值递归合并)
- base (
基础字典(不修改原对象)): - override: 覆盖字典 返回值: 合并后的新字典
setConfig(key: str, value: Any, immediate: bool = False)
设置配置项
- key (
str): 配置键, 支持点分隔符如 "module.sub.key" - value (
Any): 配置值 - immediate (
bool): 是否立即写入磁盘 (默认: False, 延迟写入) 返回值 (bool): 操作是否成功
示例:
>>> sdk.config.setConfig("ErisPulse.server.port", 9000)
>>> sdk.config.setConfig("ErisPulse.server.port", 9000, immediate=True)
force_save()
强制立即保存所有待写入的配置到磁盘
提示 注意!除非您知道您在干什么,否则请勿直接强制保存!
reload()
重新从磁盘加载配置,丢弃所有未保存的更改
提示 reload 时,未持久化的配置项会被丢弃,并重新从配置文件中加载
async agetConfig(key: str, default: Any = None)
异步获取配置项
- key (
str): 配置键, 支持点分隔符 - default (
Any): 默认值 返回值 (Any): 配置值
async asetConfig(key: str, value: Any, immediate: bool = False)
异步设置配置项
- key (
str): 配置键 - value (
Any): 配置值 - immediate (
bool): 是否立即写入磁盘 返回值 (bool): 操作是否成功
setConfigTemplate(key: str, toml_text: str, immediate: bool = True)
以带注释的 TOML 模板文本写入指定配置节
用于适配器/模块首次生成配置模板:模板中的字段注释原样落盘。 目标节已存在时不覆盖(由调用方保证仅在配置缺失时调用); 文件其余内容与注释不受影响。
- key (
str): 配置节键(支持点分路径,如"MyAdapter") - toml_text (
str): 模板 TOML 文本(仅键值与注释,不含节头) - immediate (
bool): 是否立即写入磁盘 (默认: True) 返回值 (bool): 是否写入成功
示例:
>>> sdk.config.setConfigTemplate("MyAdapter", '# API 令牌\ntoken = ""')
async aforce_save()
异步强制保存所有待写入的配置到磁盘
async areload()
异步重新从磁盘加载配置