patroni-4.0.2的源码分析
1. patroni文件夹
__init__.py
:导包初始化代码。__main__.py
:主函数,程序入口。version.py
:保存版本信息。-
dcs
文件夹: dynamic_loader.py
:存放查找包中特定抽象接口实现的辅助函数。request.py
:处理与Patroni的REST API通信的工具。daemon.py
:config_generator.py
:存放配置生成器的文件。validator.py
:
1.1 __init__.py
位置patroni
-> __init__.py
,__init__.py
文件的存在使得 Python 能够识别一个目录为一个包,并提供了包级别的初始化逻辑和自动导入子模块的功能。即使 __init__.py
文件为空,也表明该目录是一个包。然而,通过在这个文件中添加代码,可以增强包的功能和可维护性。
parse_version
函数:将version
从人类可读的格式转换为整数元组。(外部调用api)_parse_version
函数:将人类可读版本字符串的每个部分生成为整数。(内部实现)
1.1.1 主体
包括导入、环境变量定义和parse_version()
以及其的内置函数_parse_version()
,核心代码如下。
"""为:mod: ` patroni `定义通用变量和函数。
:var PATRONI_ENV_PREFIX: Patroni相关配置环境变量的前缀。
:var KUBERNETES_ENV_PREFIX: Kubernetes相关配置环境变量的前缀。
:var MIN_PSYCOPG2:最低版本的:mod: ` psycopg2 `需要由Patroni工作。
:mod: ` psycopg `的最低版本需要由Patroni工作。
"""
from typing import Iterator, Tuple
PATRONI_ENV_PREFIX = `PATRONI_`
KUBERNETES_ENV_PREFIX = `KUBERNETES_`
MIN_PSYCOPG2 = (2, 5, 4)
MIN_PSYCOPG3 = (3, 0, 0)
def parse_version(version: str) -> Tuple[int, ...]:
"""将*version*从人类可读的格式转换为整数元组。
..注意::
为方便比较Python中的软件版本而设计。
参数版本:人类可读的软件版本,例如:` ` 2.5.4.dev1 (dt dec pq3 ext lo64) ` `。
:返回:由*版本*部分组成的元组,每个部分都是整数。
例子:
>>> parse_version(`2.5.4.dev1 (dt dec pq3 ext lo64)`)
(2, 5, 4)
"""
def _parse_version(version: str) -> Iterator[int]:
"""将人类可读版本字符串的每个部分生成为整数。
参数版本:人类可读的软件版本,例如:` ` 2.5.4.dev1 ` `。
:将*version*的每个部分生成为整数。
例子:
> > >元组(_parse_version (2.5.4.dev1))
(2, 5, 4)
"""
for e in version.split(`.`):
try:
yield int(e)
except ValueError:
break
# 调用 _parse_version 函数,并传递 version 字符串的第一个单词(忽略空格之后的部分),返回一个整数元组
return tuple(_parse_version(version.split(` `)[0]))
1.1.2 作用
-
转换版本号:
parse_version
函数的作用是将人类可读的软件版本号转换为一个整数元组,使得版本号便于在 Python 中进行比较。例如,版本号 "2.5.4.dev1 (dt dec pq3 ext lo64)" 将被转换为(2, 5, 4)
。 -
提供对外接口:
parse_version
和_parse_version
这样的函数名通常暗示了一个是公开的 API,而另一个则是内部使用的私有函数。
1.2 __main__.py
位置patroni
-> __main__.py
1.2.1 main()
main函数的主体如下,root目录下的patroni.py
文件中的mian方法调用的即为patroni
-> __main__.py
-> def mian()
的mian方法。这个函数主要是依赖检查,处理传入的命令行参数,如果当前进程是容器初始化的时候充当init进程(设置信号处理量,启动守护进程),如果不是则执行守护进程patroni_main()
函数,
1.2.1.2 主体
def main() -> None:
"""mod: `patron.__main__` 的主入口点。
处理命令行参数,确保 :mod: `psycopg2`(或 :mod: `psycopg`)满足先决条件并开始 `patroni` 守护进程。
注意::
如果运行在 Docker 容器中,让主进程负责 init 进程的任务并运行 `patroni` 守护进程作为另一个进程。在这种情况下,主进程接收并转发相关信号到守护进程的“patroni”。
"""
from multiprocessing import freeze_support
# PyInstaller 创建的可执行文件被冻结,因此我们需要启用冻结支持
# :mod: `multiprocessing` 以避免 :class: `RuntimeError` 异常。
freeze_support()
# 检查是否安装了 psycopg2 或 psycopg,并且版本是否满足要求
check_psycopg()
# 处理命令行参数
args = process_arguments()
# 如果当前进程不是 PID=1,则直接运行 patroni_main 函数
if os.getpid() != 1:
return patroni_main(args.configfile)
# Patroni 从 PID=1 开始,看起来我们在容器中
from types import FrameType
pid = 0
# 看起来我们在一个 Docker 容器中,所以我们将像 init 一样操作
def sigchld_handler(signo: int, stack_frame: Optional[FrameType]) -> None:
"""处理主进程在守护进程终止时从守护进程接收到的 SIGCHLD。
:参数 signo: 信号量。
:param stack_frame: 当前栈帧。
"""
try:
# 记录所有子进程的退出代码,并在没有子进程时中断循环
while True:
ret = os.waitpid(-1, os.WNOHANG)
if ret == (0, 0):
break
elif ret[0] != pid:
logger.info(`Reaped pid=%s, exit status=%s`, *ret)
except OSError:
pass
def passtochild(signo: int, stack_frame: Optional[FrameType]) -> None:
"""从主进程向子进程转发一个信号。
:参数 signo: 信号量。
:param stack_frame: 当前栈帧。
"""
if pid:
os.kill(pid, signo)
# 设置信号处理器
if os.name != `nt`:
signal.signal(signal.SIGCHLD, sigchld_handler)
signal.signal(signal.SIGHUP, passtochild)
signal.signal(signal.SIGQUIT, passtochild)
signal.signal(signal.SIGUSR1, passtochild)
signal.signal(signal.SIGUSR2, passtochild)
signal.signal(signal.SIGINT, passtochild)
signal.signal(signal.SIGABRT, passtochild)
signal.signal(signal.SIGTERM, passtochild)
# 导入 multiprocessing 模块
import multiprocessing
# 创建一个新的进程,用于运行 patroni 主程序
patroni = multiprocessing.Process(target=patroni_main, args=(args.configfile,))
# 启动新进程
patroni.start()
# 记录子进程的 PID
pid = patroni.pid
# 等待子进程结束
patroni.join()
1.2.1.2 作用
- 处理命令行参数:
- 通过
process_arguments()
函数处理命令行参数,并获取配置文件路径等必要信息。
- 通过
- 检查数据库适配器库:
- 通过调用
check_psycopg()
函数确保环境中至少安装了一个符合要求版本的psycopg2
或psycopg
库。
- 通过调用
- 支持冻结的应用程序:
- 如果应用程序是通过 PyInstaller 等工具打包的,那么
freeze_support()
函数会确保multiprocessing
模块可以正常工作。
- 如果应用程序是通过 PyInstaller 等工具打包的,那么
- 根据进程ID区分行为:
- 如果当前进程的 PID 是 1(通常是容器内的初始化进程),那么它将扮演类似于传统 Unix/Linux 系统中
init
进程的角色。 - 如果当前进程的 PID 不是 1,那么直接运行
patroni_main
函数。
- 如果当前进程的 PID 是 1(通常是容器内的初始化进程),那么它将扮演类似于传统 Unix/Linux 系统中
- 设置信号处理器:
- 为各种信号(如
SIGCHLD
,SIGINT
,SIGTERM
等)设置处理器,确保主进程可以接收并转发这些信号到守护进程。
- 为各种信号(如
- 启动守护进程:
- 使用
multiprocessing
模块创建一个新的子进程来运行patroni_main
函数,并等待这个子进程结束。
- 使用
1.2.2 freeze_support()
主要是针对windows系统,保证多进程在windows上的正确运行和解析参数。
1.2.2.1 主体
def freeze_support():
'''
如果进程对象不是主进程,则运行该进程对象的代码
'''
if is_forking(sys.argv):
kwds = {}
for arg in sys.argv[2:]:
name, value = arg.split(`=`)
if value == `None`:
kwds[name] = None
else:
kwds[name] = int(value)
# 运行通过管道接收到的数据指定的代码
spawn_main(**kwds)
sys.exit()
1.2.2.2 作用
- 处理多进程:
- 这个函数的作用是在非主进程中正确运行代码。当程序被打包成
.exe
文件并在 Windows 上运行时,多进程的支持需要特别处理。用于支持多进程程序在 Windows 系统上的正确运行。
- 这个函数的作用是在非主进程中正确运行代码。当程序被打包成
- 参数解析:
- 函数从命令行参数
sys.argv
中解析额外的参数,并将这些参数传递给spawn_main
函数。
- 函数从命令行参数
1.2.3 check_psycopg()
检查依赖,优先选择psycopg2
。
1.2.3.1 主体
def check_psycopg() -> None:
"""确保环境中至少有一个 :mod: `psycopg2` 或 :mod: `psycopg` 库可用。
注意::
如果可能的话,Patroni 会选择 :mod: `psycopg2` 而不是 :mod: `psycopg`。
如果没有找到符合要求的内容,则退出并显示一条致命消息。
"""
# 将最小版本号转换为字符串格式
min_psycopg2_str = `.`.join(map(str, MIN_PSYCOPG2))
min_psycopg3_str = `.`.join(map(str, MIN_PSYCOPG3))
# 存储可用版本号的列表
available_versions: List[str] = []
# 尝试导入 psycopg2
try:
from psycopg2 import __version__ # 导入 psycopg2 的版本信息
# 检查版本是否满足要求
if parse_version(__version__) >= MIN_PSYCOPG2:
return # 如果满足要求,直接返回
# 如果不满足要求,记录可用版本号
available_versions.append(`psycopg2=={0}`.format(__version__.split(` `)[0]))
except ImportError:
logger.debug(`psycopg2 module is not available`) # 如果无法导入 psycopg2,记录调试信息
# 尝试导入 psycopg3
try:
from psycopg import __version__ # 导入 psycopg 的版本信息
# 检查版本是否满足要求
if parse_version(__version__) >= MIN_PSYCOPG3:
return # 如果满足要求,直接返回
# 如果不满足要求,记录可用版本号
available_versions.append(`psycopg=={0}`.format(__version__.split(` `)[0]))
except ImportError:
logger.debug(`psycopg module is not available`) # 如果无法导入 psycopg,记录调试信息
# 构造错误信息
error = f`FATAL: Patroni requires psycopg2>={min_psycopg2_str}, psycopg2-binary, or psycopg>={min_psycopg3_str}`
# 如果有可用版本,补充错误信息
if available_versions:
error += `, but only {0} {1} available`.format(
`and`.join(available_versions),
`is` if len(available_versions) == 1 else `are`)
sys.exit(error) # 如果没有符合条件的库,退出并显示错误信息
1.2.3.2 作用
- 版本转换:
- 将
MIN_PSYCOPG2
和MIN_PSYCOPG3
的版本号转换为字符串格式,用于后续的版本比较。
- 将
- 尝试导入
psycopg2
:- 尝试导入
psycopg2
的版本信息。 - 检查版本是否满足要求(
>= MIN_PSYCOPG2
)。 - 如果不满足要求,记录可用的版本号。
- 尝试导入
- 尝试导入
psycopg3
:- 尝试导入
psycopg
的版本信息。 - 检查版本是否满足要求(
>= MIN_PSYCOPG3
)。 - 如果不满足要求,记录可用的版本号。
- 尝试导入
- 构造错误信息:
- 如果没有符合条件的库,构造一个错误信息。
- 退出程序并显示错误信息:
- 如果没有符合条件的库,使用
sys.exit()
退出程序并显示错误信息。
- 如果没有符合条件的库,使用
1.2.4 process_arguments()
处理命令行参数。
1.2.4.1 主体
- 函数返回一个命名空间对象,即包含所有命令行参数的对象
def process_arguments() -> Namespace:
"""处理命令行参数。
通过:func:`~patroni.daemon创建一个基本的命令行解析器。Get_base_arg_parser `,扩展其功能
添加这些标志并解析命令行参数:
* ``--validate-config`` -- 用于验证Patroni配置文件
* ``--generate-config`` -- 用于从运行中的PostgreSQL实例生成Patroni配置
* ``--generate-sample-config`` -- 用于生成一个样本Patroni配置
* ``--ignore-listen-port`` | ``-i`` -- 用于忽略已经被使用的 `listen` 端口。
只能与 `--validate-config` 一起使用。
.. 注意::
如果使用`——generate-config ` `, `——generate-sample-config ` `或`——validate-flag ` `运行将退出
在生成或验证配置之后。
:返回:解析后的参数,如果没有使用``——validate-config``标志运行。
"""
from patroni.config_generator import generate_config
# 获取基础的命令行解析器。
parser = get_base_arg_parser()
# 创建一个互斥组,意味着组内的选项只能选择一个
group = parser.add_mutually_exclusive_group()
# 向互斥组内添加 --validate-config 选项,如果设置则存储为 True 并退出程序
group.add_argument(`--validate-config`, action=`store_true`, help=`Run config validator and exit`)
# 向互斥组内添加 --generate-sample-config 选项,如果设置则存储为 True 并生成一个样本Patroni YAML配置文件
group.add_argument(`--generate-sample-config`, action=`store_true`,
help=`Generate a sample Patroni yaml configuration file`)
# 向互斥组内添加 --generate-config 选项,如果设置则存储为 True 并为运行中的实例生成一个Patroni YAML配置文件
group.add_argument(`--generate-config`, action=`store_true`,
help=`Generate a Patroni yaml configuration file for a running instance`)
# 添加 --dsn 选项,用于指定用于生成配置的数据源实例的DSN字符串。要求超级用户连接
parser.add_argument(`--dsn`, help=`Optional DSN string of the instance to be used as a source \
for config generation. Superuser connection is required.`)
# 添加 --ignore-listen-port | -i 选项,如果设置则存储为 True,表示忽略已经被使用的 listen 端口。仅能与 --validate-config 一起使用
parser.add_argument(`--ignore-listen-port`, `-i`, action=`store_true`,
help=`Ignore `listen` ports already in use.\
Can only be used with --validate-config`)
# 解析命令行参数,并将其存储到 args 变量中
args = parser.parse_args()
# 根据 args 的值调用 generate_config 函数或者验证配置文件,然后退出程序
if args.generate_sample_config:
generate_config(args.configfile, True, None)
sys.exit(0)
elif args.generate_config:
generate_config(args.configfile, False, args.dsn)
sys.exit(0)
elif args.validate_config:
from patroni.config import Config, ConfigParseError
from patroni.validator import populate_validate_params, schema
populate_validate_params(ignore_listen_port=args.ignore_listen_port)
try:
Config(args.configfile, validator=schema)
sys.exit()
except ConfigParseError as e:
sys.exit(e.value)
return args
1.2.4.2 作用
- 处理命令行参数
- 此函数的作用是处理命令行参数,并根据提供的标志执行特定操作,如生成配置文件、生成示例配置文件或验证现有配置文件。如果命令行参数指示生成或验证配置,则函数会在执行相应操作后终止进程。如果没有这些标志,则函数返回解析后的命令行参数。
1.2.5 patroni_main()
启动守护进程,abstract_main()
方法是一个通用的守护进程启动方法,传入的Patroni
为实际要启动的守护进程的类,configfile
为配置文件。
1.2.5.1 主体
def patroni_main(configfile: str) -> None:
"""配置并启动` ` patroni ` `主守护进程。
:param configfile: Patroni配置文件路径。
"""
abstract_main(Patroni, configfile)
1.2.5.2 作用
- 传入配置文件
- 启动patroni主守护进程
1.2.6 abstract_main()
启动守护进程通用方法。
1.2.6.1 主体
def abstract_main(cls: Type[AbstractPatroniDaemon], configfile: str) -> None:
"""创建指定守护进程的主要入口点。
:参数 cls: 应该继承自 :class:`AbstractPatroniDaemon` 的类。
:参数 configfile: 配置文件的路径。
"""
# 导入必要的模块
from .config import Config, ConfigParseError
# 读取配置文件
try:
config = Config(configfile)
except ConfigParseError as e:
# 如果配置文件解析失败,退出程序并显示错误信息
sys.exit(e.value)
# 创建守护进程控制器实例
controller = cls(config)
# 运行守护进程
try:
controller.run()
except KeyboardInterrupt:
# 如果用户中断了程序(例如按下 Ctrl+C),则捕获 KeyboardInterrupt 异常
pass
finally:
# 无论是否发生异常,都会执行 shutdown 方法来清理资源
controller.shutdown()
1.2.6.2 作用
- 加载配置文件:
- 从提供的配置文件路径 (
configfile
) 加载配置信息。 - 如果配置文件无法正确解析,则会抛出异常并退出程序。
- 从提供的配置文件路径 (
- 创建守护进程控制器:
- 创建一个
cls
类型的对象,这个类应该继承自AbstractPatroniDaemon
。 - 通过构造函数将配置信息传递给这个对象。
- 创建一个
- 运行守护进程:
- 调用守护进程控制器的
run
方法来启动守护进程。 - 如果在运行过程中用户中断了程序(比如通过键盘中断
Ctrl+C
),则捕获KeyboardInterrupt
异常并继续执行。
- 调用守护进程控制器的
- 清理资源:
- 在
finally
子句中调用守护进程控制器的shutdown
方法来确保释放所有资源,即使在出现异常或用户中断的情况下也是如此。
- 在
1.2.7 类:Patroni
守护进程实际运行的类,即守护进程控制器。继承了AbstractPatroniDaemon
(守护进程)和Tags
类。
version
属性: Patroni 版本。dcs
属性: DCS 对象。watchdog
属性: 如果配置了使用 watchdog,则为 watchdog 处理器。postgresql
属性: 管理的 Postgres 实例。api
属性: 此节点的 REST API 服务器实例。request
属性: 用于执行 HTTP 请求的包装器。ha
属性: HA 处理器。next_run
属性: 下一次 HA 循环运行的时间。scheduled_restart
属性: 如果计划了重启,则包含两个键:schedule
: 计划重启的时间戳;postmaster_start_time
: Postgres 最后一次启动的时间戳。
__init__
函数:创建一个:class:Patroni
实例,带有给定的config
,获取DCS
连接,配置watchdog
(如果需要),设置与Postgres
的Patroni
接口,配置HA
循环并启动REST API
。ensure_dcs_access
函数:持续尝试从 DCS 中检索集群,并延迟一定时间。apply_dynamic_configuration
函数:应用 Patroni 动态配置。如果 DCS 中存在/config
键,则应用动态配置,否则回退到配置文件中的bootstrap.dcs
部分。ensure_unique_name
函数:帮助方法,防止由于操作员命名错误导致的splitbrain
。_get_tags
函数:获取此节点配置的标签(如果有)。reload_config
函数:应用新的配置值给patroni
守护进程。tags
函数:配置给此节点的标签(如果有)。schedule_next_run
函数:安排下一次patroni
守护进程主循环的运行。下次运行基于上次运行加上 DCS 中的loop_wait
配置值。如果已经超过了这个值,则立即运行下一个周期。run
函数:运行patroni
守护进程主循环的一个周期。运行一次 HA 循环并安排下次循环运行。如果检测到任何动态配置更改请求,则应用更改并将新的动态配置值缓存到patroni.dynamic.json
文件中。_run_cycle
函数:运行patroni
守护进程主循环的一个周期。运行一次 HA 循环并安排下次循环运行。如果检测到任何动态配置更改请求,则应用更改并将新的动态配置值缓存到patroni.dynamic.json
文件中。_shutdown
函数:执行patroni
守护进程的关闭操作。关闭 REST API 和 HA 处理器。
1.2.7.1 主体
class Patroni(AbstractPatroniDaemon, Tags):
""" 实现 ``patroni`` 命令守护进程。
:ivar version: Patroni 版本。
:ivar dcs: DCS 对象。
:ivar watchdog: 如果配置了使用 watchdog,则为 watchdog 处理器。
:ivar postgresql: 管理的 Postgres 实例。
:ivar api: 此节点的 REST API 服务器实例。
:ivar request: 用于执行 HTTP 请求的包装器。
:ivar ha: HA 处理器。
:ivar next_run: 下一次 HA 循环运行的时间。
:ivar scheduled_restart: 如果计划了重启,则包含两个键:
* ``schedule``: 计划重启的时间戳;
* ``postmaster_start_time``: Postgres 最后一次启动的时间戳。
"""
def __init__(self, config: `Config`) -> None:
"""创建一个 :class:`Patroni` 实例,带有给定的 *config*。
获取 DCS 连接,配置 watchdog(如果需要),设置与 Postgres 的 Patroni 接口,配置 HA 循环并启动 REST API。
.. note::
预期通过 :func:`~patroni.daemon.abstract_main` 实例化并运行。
:param config: Patroni 配置。
"""
from patroni.api import RestApiServer
from patroni.dcs import get_dcs
from patroni.ha import Ha
from patroni.postgresql import Postgresql
from patroni.request import PatroniRequest
from patroni.version import __version__
from patroni.watchdog import Watchdog
# 调用父类构造函数
super(Patroni, self).__init__(config)
# 初始化成员变量
self.version = __version__
self.dcs = get_dcs(self.config)
self.request = PatroniRequest(self.config, True)
# 确保 DCS 访问
cluster = self.ensure_dcs_access()
self.ensure_unique_name(cluster)
# 配置 watchdog 和动态配置
self.watchdog = Watchdog(self.config)
self.apply_dynamic_configuration(cluster)
# 初始化 PostgreSQL 实例和 REST API 服务器
self.postgresql = Postgresql(self.config[`postgresql`], self.dcs.mpp)
self.api = RestApiServer(self, self.config[`restapi`])
self.ha = Ha(self)
# 初始化其他成员变量
self._tags = self._get_tags()
self.next_run = time.time()
self.scheduled_restart: Dict[str, Any] = {}
def ensure_dcs_access(self, sleep_time: int = 5) -> `Cluster`:
"""持续尝试从 DCS 中检索集群,并延迟一定时间。
:param sleep_time: 在 DCS 连接引发 :exc:`DCSError` 后重试之间等待的秒数。
:returns: 一个 PostgreSQL 或 MPP 实现的 :class:`Cluster`。
"""
from patroni.exceptions import DCSError
# 持续尝试获取集群信息,如果失败,记录警告信息并休眠一段时间后重试
while True:
try:
return self.dcs.get_cluster()
except DCSError:
logger.warning(`Can not get cluster from dcs`)
time.sleep(sleep_time)
def apply_dynamic_configuration(self, cluster: `Cluster`) -> None:
"""应用 Patroni 动态配置。
如果 DCS 中存在 `/config` 键,则应用动态配置,否则回退到配置文件中的 `bootstrap.dcs` 部分。
.. note::
此方法仅在 Patroni 启动时调用一次。
:param cluster: 一个 PostgreSQL 或 MPP 实现的 :class:`Cluster`。
"""
# 从 DCS 获取配置,如果集群配置存在,则设置动态配置,并重新加载 DCS 和 watchdog 配置
if cluster and cluster.config and cluster.config.data:
if self.config.set_dynamic_configuration(cluster.config):
self.dcs.reload_config(self.config)
self.watchdog.reload_config(self.config)
# 从 bootstrap 部分获取配置,并重新加载 DCS 和 watchdog 配置
elif not self.config.dynamic_configuration and `bootstrap` in self.config:
if self.config.set_dynamic_configuration(self.config[`bootstrap`][`dcs`]):
self.dcs.reload_config(self.config)
self.watchdog.reload_config(self.config)
def ensure_unique_name(self, cluster: `Cluster`) -> None:
"""帮助方法,防止由于操作员命名错误导致的 splitbrain。
:param cluster: 一个 PostgreSQL 或 MPP 实现的 :class:`Cluster`。
"""
from patroni.dcs import Member
# 检查集群是否存在
if not cluster:
return
# 检查节点名称唯一性
member = cluster.get_member(self.config[`name`], False)
if not isinstance(member, Member):
return
# 检查节点是否已存在
try:
# 抑制在快速重启 Patroni 时的烦人警告信息
self.logger.update_loggers({`urllib3.connectionpool`: `ERROR`})
_ = self.request(member, endpoint="/liveness", timeout=3)
logger.fatal("Can`t start; there is already a node named `%s` running", self.config[`name`])
sys.exit(1)
except Exception:
self.logger.update_loggers({})
def _get_tags(self) -> Dict[str, Any]:
"""获取此节点配置的标签(如果有)。
:returns: 一个包含此节点标签的字典。
"""
return self._filter_tags(self.config.get(`tags`, {}))
def reload_config(self, sighup: bool = False, local: Optional[bool] = False) -> None:
"""应用新的配置值给 ``patroni`` 守护进程。
重新加载:
* 缓存的标签;
* 请求包装器配置;
* REST API 配置;
* watchdog 配置;
* PostgreSQL 配置;
* DCS 配置。
:param sighup: 如果与 SIGHUP 信号有关。
:param local: 如果本地配置文件发生了变化。
"""
# 调用父类的重新加载配置方法
try:
super(Patroni, self).reload_config(sighup, local)
if local:
self._tags = self._get_tags()
self.request.reload_config(self.config)
if local or sighup and self.api.reload_local_certificate():
self.api.reload_config(self.config[`restapi`])
self.watchdog.reload_config(self.config)
self.postgresql.reload_config(self.config[`postgresql`], sighup)
self.dcs.reload_config(self.config)
except Exception:
logger.exception(`Failed to reload config_file=%s`, self.config.config_file)
@property
def tags(self) -> Dict[str, Any]:
"""配置给此节点的标签(如果有)。"""
return self._tags
def schedule_next_run(self) -> None:
"""安排下一次 ``patroni`` 守护进程主循环的运行。
下次运行基于上次运行加上 DCS 中的 ``loop_wait`` 配置值。如果已经超过了这个值,则立即运行下一个周期。
"""
# 计算下次运行时间
self.next_run += self.dcs.loop_wait
current_time = time.time()
nap_time = self.next_run - current_time
if nap_time <= 0:
self.next_run = current_time
# 释放 GIL,以免阻塞那些等待 async_executor 锁的操作。
time.sleep(0.001)
# 警告用户 Patroni 无法跟上
logger.warning("Loop time exceeded, rescheduling immediately.")
elif self.ha.watch(nap_time):
self.next_run = time.time()
def run(self) -> None:
"""运行 ``patroni`` 守护进程主循环。
启动 REST API 并每隔 ``loop_wait`` 秒运行一次 HA 循环。
"""
# 启动 REST API 并运行父类的 run 方法
self.api.start()
self.next_run = time.time()
super(Patroni, self).run()
def _run_cycle(self) -> None:
"""运行 ``patroni`` 守护进程主循环的一个周期。
运行一次 HA 循环并安排下次循环运行。如果检测到任何动态配置更改请求,则应用更改并将新的动态配置值缓存到 ``patroni.dynamic.json`` 文件中。
"""
# 运行 HA 循环
logger.info(self.ha.run_cycle())
# 应用动态配置
if self.dcs.cluster and self.dcs.cluster.config and self.dcs.cluster.config.data \
and self.config.set_dynamic_configuration(self.dcs.cluster.config):
self.reload_config()
# 保存缓存
if self.postgresql.role != `uninitialized`:
self.config.save_cache()
# 安排下次运行
self.schedule_next_run()
def _shutdown(self) -> None:
"""执行 ``patroni`` 守护进程的关闭操作。
关闭 REST API 和 HA 处理器。
"""
# 关闭 REST API 和 HA 处理器
try:
self.api.shutdown()
except Exception:
logger.exception(`Exception during RestApi.shutdown`)
try:
self.ha.shutdown()
except Exception:
logger.exception(`Exception during Ha.shutdown`)
1.2.7.2 作用
Patroni
类的作用是实现一个具体的 Patroni
守护进程,它继承自 AbstractPatroniDaemon
和 Tags
,并实现了以下功能:
- 初始化配置:
- 从配置文件加载配置信息。
- 创建 DCS、watchdog、PostgreSQL、REST API 和 HA 处理器实例。
- 确保集群访问:
- 确保能够访问 DCS,并获取集群信息。
- 确保节点名称唯一性。
- 应用动态配置:
- 应用从 DCS 获取的动态配置。
- 重新加载配置:
- 支持重新加载配置文件中的各项配置信息。
- 运行守护进程:
- 启动 REST API 服务器。
- 运行 HA 循环,并根据需要重新调度运行时间。
- 关闭守护进程:
- 关闭 REST API 服务器和 HA 处理器。
1.3 version.py
位置patroni
-> version.py
。__main__.py
文件中的patroni
类的__init__
初始化方法中,将值赋值给类中属性。
__version__
属性:当前的Patroni版
1.3.1 主体
"""此模块指定当前的Patroni版本。
:var __version__:当前的Patroni版本。
"""
__version__ = `4.0.2`
1.3.2 作用
- 保存版本信息
1.4 dcs文件夹
1.4.1 __init__.py
"""用于分布式配置存储的抽象类。"""
dsc文件夹的初始化方法代码。
get_dcs
函数:从已知的 DCS 实现中动态地加载一个分布式配置存储模块。iter_dcs_classes
函数 :根据提供的配置信息尝试导入并返回所有可用的 DCS 模块。AbstractDCS
类 :抽象表示 DCS 模块。get_cluster
函数:获取一个最新的集群视图,并进行适当的缓存以减少对底层 REST API 的请求开销。is_mpp_coordinator
函数:确定当前节点是否为 MPP 系统中的协调器节点。_get_mpp_cluster
函数:从分布式协调服务(DCS)中加载 MPP 集群的信息,并组织成一个Cluster
实例返回。_load_cluster
函数:从分布式协调服务(DCS)中加载集群信息,并通过指定的加载器方法来构建Cluster
实例。_mpp_cluster_loader
函数:从分布式协调服务(DCS)中加载单个 MPP(大规模并行处理)集群中的所有 PostgreSQL 集群信息,并构建出这些集群的Cluster
对象。__get_postgresql_cluster
函数:从指定的路径(默认为通过self.client_path
获取的路径)所在的 DCS 后端加载一个Cluster
对象。client_path
函数:根据当前的配置构造一个适合 DCS 使用的绝对键名。_postgresql_cluster_loader
函数:抽象基类中声明一个方法,作用是从 DCS 中加载并构建表示单个 PostgreSQL 集群的Cluster
对象。_load_cluster
函数:抽象基类中声明一个方法,作用是从 DCS 中加载Cluster
实例或一组Cluster
实例。reset_cluster
函数:清除缓存中的集群状态信息,并确保任何后续的操作都会重新从 DCS 加载最新的集群状态。reload_config
函数:从配置信息中重新加载并设置相关的属性值。_set_loop_wait
函数:设置一个新的loop_wait
值。set_ttl
函数:抽象基类中声明一个方法,设置 DCS 中键的新的ttl
值。set_retry_timeout
函数:抽象基类中声明一个方法,设置新的retry_timeout
值。mpp
函数:提供一个只读接口来访问实例变量_mpp
,该变量通常保存了一个AbstractMPP
类型的对象,代表某种多点处理(Multi-Point Processing, MPP)机制或接口。__init__
函数:初始化一个对象的成员变量。get_mpp_coordinator
函数:从分布式一致性系统中加载与 MPP 协调者组 ID 相关联的 PostgreSQL 集群。_get_mpp_cluster
函数:从分布式一致性系统(DCS)中加载 MPP(Massively Parallel Processing,大规模并行处理)集群的信息。reset_cluster
函数:清除与 DCS(Distributed Consistency System,分布式一致性系统)相关的缓存状态。_write_leader_optime
函数:将当前的 WAL(Write-Ahead Logging,预写式日志)LSN(Log Sequence Number,日志序列号)写入到 DCS(Distributed Consistency System,分布式一致性系统)中指定的键下。write_leader_optime
函数:将当前的 WAL(Write-Ahead Logging,预写式日志)LSN(Log Sequence Number,日志序列号)写入到 DCS(Distributed Consistency System,分布式一致性系统)中指定的键下。_write_status
函数:抽象基类中声明一个方法,将当前的状态信息写入到 DCS(Distributed Consistency System,分布式一致性系统)中指定的键下。write_status
函数:将当前的状态信息写入到 DCS(Distributed Consistency System,分布式一致性系统)中指定的键下,并处理一些兼容性问题。_write_failsafe
函数:抽象基类中声明一个方法,在 DCS(Distributed Consistency System,分布式一致性系统)中存储当前集群的拓扑结构,以便在故障安全机制启用时使用。write_failsafe
函数:在 DCS(Distributed Consistency System,分布式一致性系统)中存储当前集群成员的信息,这些信息包括成员的名称和 API URL,以便在故障安全机制启用时使用。_build_retain_slots
函数:构建和维护一个成员复制槽的列表,这些槽在成员暂时离线时会被保留,以防止成员重新上线时丢失重要的 WAL(Write-Ahead Logging)段。update_leader
函数:更新集群中的leader
密钥(或会话)的 TTL,并根据需要更新集群的状态信息和故障安全信息。attempt_to_acquire_leader
函数:抽象基类中声明一个方法,在分布式一致性系统(DCS)中尝试获取领导者的锁,并返回一个布尔值表示操作是否成功。set_failover_value
函数:抽象基类中声明一个方法,在分布式一致性系统(DCS)中创建或更新/failover
键,并返回一个布尔值表示操作是否成功。manual_failover
函数:在分布式一致性系统(DCS)中手动触发故障转移,并返回一个布尔值表示操作是否成功。set_config_value
函数:抽象基类中声明一个方法,在分布式一致性系统(DCS)中创建或更新/config
键,并返回一个布尔值表示操作是否成功。touch_member
函数:抽象基类中声明一个方法,在分布式一致性系统(DCS)中创建或更新成员信息,并返回一个布尔值表示操作是否成功。take_leader
函数:抽象基类中声明一个方法,在分布式一致性系统(DCS)中建立一个新的领导者,并返回一个布尔值表示操作是否成功。initialize
函数:抽象基类中声明一个方法,在分布式一致性系统(DCS)中进行集群初始化,并返回一个布尔值表示操作是否成功。_delete_leader
函数:抽象基类中声明一个方法,在分布式协调系统(DCS)中删除 leader 键。delete_leader
函数:抽象基类中声明一个方法,在分布式一致性系统(DCS)中删除领导者键,并返回一个布尔值表示操作是否成功。cancel_initialization
函数:抽象基类中声明一个方法,在分布式一致性系统(DCS)中取消集群初始化,并返回一个布尔值表示操作是否成功。delete_cluster
函数:抽象基类中声明一个方法,在分布式一致性系统(DCS)中删除与集群相关的所有数据,并返回一个布尔值表示操作是否成功。sync_state
函数:静态方法,根据提供的参数构建一个sync_state
字典,该字典描述了集群中的同步状态信息。_write_leader_optime
函数:在分布式一致性系统(DCS)中写入新的同步状态,并返回一个SyncState
对象或None
表示操作的结果。set_history_value
函数:抽象基类中声明一个方法,在分布式一致性系统(DCS)中设置history
键的值,并返回一个布尔值表示操作是否成功。set_sync_state_value
函数:抽象基类中声明一个方法,在分布式一致性系统(DCS)中设置同步状态,并返回新对象的版本号或布尔值False
表示操作是否成功。delete_sync_state
函数:抽象基类中声明一个方法,从分布式一致性系统(DCS)中删除同步状态,并返回一个布尔值表示操作是否成功。watch
函数:在分布式系统中监视领导者键的状态变化,并决定是否需要重新调度下一个高可用(HA)周期的运行。
Cluster
类 :用于表示一个 PostgreSQL 或 MPP 集群的状态。empty
函数:生成一个没有任何具体信息填充的Cluster
实例。__new__
函数:在创建类的新实例时,处理workers
参数的缺失情况。get_member
函数:通过成员名称来获取一个Member
对象。如果找不到对应的成员,并且fallback_to_leader
参数为True
,则返回当前的Leader
对象。is_empty
函数:验证当前Cluster
实例的所有属性是否都没有被填充或初始化。__len__
函数:实现对Cluster
实例的长度评估功能,以便能够在布尔上下文中方便地判断Cluster
实例是否为空。is_unlocked
函数:检查集群是否没有领导者。has_member
函数:检查给定的成员名称是否存在于集群的成员列表中。get_member
函数:根据给定的成员名称来获取对应的Member
对象,或者如果找不到成员且fallback_to_leader
参数为True
,则返回Leader
对象。get_clone_member
函数:从集群中选择一个合适的成员作为克隆源。is_physical_slot
函数:检查给定的配置是否表示一个永久物理复制槽位。is_logical_slot
函数:检查给定的配置是否表示一个永久逻辑复制槽位。get_replication_slots
函数:获取并整合集群中配置的复制槽位信息。_merge_permanent_slots
函数:合并成员的复制槽位信息与永久槽位信息。_get_permanent_slots
函数:从配置中获取永久复制槽位信息。_get_members_slots
函数:获取特定成员的物理复制槽位配置。has_permanent_slots
函数:检查一个给定的节点是否配置了永久复制槽位。maybe_filter_permanent_slots
函数:从给定的一组槽位中筛选出永久性的槽位。_has_permanent_logical_slots
函数:检查给定的成员节点是否配置了任何永久的逻辑复制槽位。should_enforce_hot_standby_feedback
函数:决定是否应该为给定的成员节点启用hot_standby_feedback
功能。get_slot_name_on_primary
函数:获取当前节点在其主节点上的物理复制槽位名称。
Status
类 :用于封装数据库状态信息的不可变对象。empty
函数:生成一个没有任何具体信息填充的Status
实例。is_empty
函数:检查当前Status
实例的所有属性是否都没有具体的值,也就是是否处于“空”的状态。from_node
函数:从给定的值value
中解析出一个Status
对象。
SyncState
类 :用于封装同步复制状态信息的不可变对象。empty
函数:生成一个没有任何具体信息填充的SyncState
实例,除了版本号可以由用户指定。from_node
函数:从给定的值value
中解析出一个SyncState
对象。is_empty
函数:检查SyncState
实例是否有领导者成员。_str_to_list
函数:从一个逗号分隔的字符串中提取出所有非空的子字符串,并返回这些子字符串组成的列表。voters
函数:获取SyncState
实例中的sync_standby
字段,并将其转换为一个列表形式。members
函数:返回SyncState
实例中的所有成员名称(包括领导者和同步备用节点),如果实例被认为为空(即没有领导者),则返回一个空列表。matches
函数:检查给定的节点名称是否存在于同步状态中leader_matches
函数:检查给定的名称是否与SyncState
实例中的领导者名称相匹配。
parse_connection_string
函数:将输入的连接字符串拆分成两个部分:连接 URL (conn_url
) 和 API URL (api_url
)。slot_name_from_member_name
函数:将输入的member_name
转换成一个有效的 PostgreSQL 复制槽名称。dcs_modules
函数:获取当前包下的所有 DCS(Distributed Consistency System,分布式一致性系统)模块的名称。catch_return_false_exception
函数:在被装饰的函数抛出ReturnFalseException
异常时,能够捕获这个异常,并且代替抛出异常的行为返回False
。ReturnFalseException
类 :提供一种标准化的方式来处理函数中可能需要返回False
的情况,抛出自定义异常。TimelineHistory
类 :用于表示和存储 PostgreSQL 时间线历史文件的信息。from_node
函数:从一个 JSON 序列化的字符串中解析出时间线历史行,并将其封装成一个TimelineHistory
对象。
ClusterConfig
类 :在应用程序中表示集群的配置信息。from_node
函数:从一个 JSON 序列化的字符串中解析出配置信息,并将其封装成一个ClusterConfig
对象。
Failover
类 :在应用程序中表示故障转移(failover)或切换(switchover)的配置信息。from_node
函数:从一个 JSON 序列化的字符串、配置信息的字典或者是冒号分隔的字符串中解析出故障转移配置信息,并将其封装成一个Failover
对象。__len__
函数:定义如何计算Failover
实例的“长度”。
Leader
类 :在应用程序中表示集群中的领导者信息。conn_kwargs
函数:获取连接所需的关键词参数。
RemoteMember
类 :表示一个备用集群中的远程成员,通常是主节点。__new__
函数:在创建RemoteMember
实例时初始化该实例。__getattr__
函数:在尝试访问RemoteMember
实例上不存在的属性时,模拟字典的键查找行为。
Member
类 :表示 PostgreSQL 集群中的单个成员。from_node
函数:从给定的参数中创建Member
实例。conn_kwargs
函数:从当前成员对象中提取或构建用于连接 PostgreSQL 数据库的关键字参数。get_endpoint_url
函数:根据成员对象中的api_url
属性和提供的endpoint
参数构建完整的 REST API URL。
1.4.1.1 get_dsc()
- 定义了一个函数
get_dcs
,它接受一个config
参数,该参数可以是Config
类型的对象或字典类型的对象。 - 函数的返回类型是一个
AbstractDCS
类型的对象。
def get_dcs(config: Union[`Config`, Dict[str, Any]]) -> `AbstractDCS`:
"""尝试从已知可用的实现中加载一个分布式配置存储(Distributed Configuration Store,简称 DCS)
注意事项:使用由 iter_dcs_classes 函数返回的可用 DCS 类列表,动态实例化实现了 DCS 的类,这些类继承自抽象类 AbstractDCS。
从 config 中检索的基本顶级配置参数会在传递给模块的 DCS 类之前传播到特定于 DCS 的配置部分。
如果没有找到满足配置的模块,则报告并记录一个错误。这将导致 Patroni 退出。
异常说明:如果尝试加载所有可用的 DCS 模块均未成功,则引发 PatroniFatalException 异常。
参数说明:config 是一个包含 Patroni 配置的对象或字典。这通常表示 Patroni 的主要配置
返回值说明:返回第一个成功加载的实现了 AbstractDCS 的 DCS 模块。
"""
# 遍历 iter_dcs_classes 函数返回的 DCS 类列表。
for name, dcs_class in iter_dcs_classes(config):
# 将一些参数从定义的配置顶层传播到特定于DCS的配置部分。
config[name].update({
p: config[p] for p in (`namespace`, `name`, `scope`, `loop_wait`,
`patronictl`, `ttl`, `retry_timeout`)
if p in config})
from patroni.postgresql.mpp import get_mpp
# 返回 dcs_class 的实例,传入更新后的配置和通过 get_mpp 函数获取的 MPP(大规模并行处理)配置。
return dcs_class(config[name], get_mpp(config))
# 如果没有找到合适的 DCS 实现,则构造一个包含所有可用 DCS 实现名称的字符串。
available_implementations = `, `.join(sorted([n for n, _ in iter_dcs_classes()]))
# 抛出 PatroniFatalException 异常,指明无法找到合适的 DCS 配置,并列出所有可用的实现
raise PatroniFatalException("Can not find suitable configuration of distributed configuration store\n"
f"Available implementations: {available_implementations}")
作用:
get_dcs
函数的作用是从已知的 DCS 实现中动态地加载一个分布式配置存储模块。具体来说:
- 动态加载 DCS 模块:
- 通过
iter_dcs_classes
函数获取所有可用的 DCS 类。 - 遍历这些类,并尝试实例化它们,传入经过调整的配置参数。
- 通过
- 传播配置参数:
- 将一些基本的顶级配置参数(如
namespace
,name
,scope
等)传播到特定于 DCS 的配置部分,以确保这些配置参数能够在 DCS 模块中生效。
- 将一些基本的顶级配置参数(如
- 返回 DCS 实例=:
- 成功实例化 DCS 类后,返回该类的实例。
- 如果所有尝试都失败,则抛出
PatroniFatalException
异常,并指出所有可用的 DCS 实现,以便用户了解问题所在。
通过这种方式,get_dcs
函数确保了 Patroni 应用程序能够根据配置动态选择并加载正确的 DCS 模块,从而实现集群管理所需的分布式协调功能。
1.4.1.2 iter_dcs_classes()
- 定义了一个函数
iter_dcs_classes
,它接受一个config
参数,该参数可以是Config
类型的对象或字典类型的对象,默认值为None
。 - 函数的返回类型是一个迭代器,每个迭代项是一个元组,元组包含两个元素:模块的
name
和导入的 DCS 类对象。
def iter_dcs_classes(
config: Optional[Union[`Config`, Dict[str, Any]]] = None
) -> Iterator[Tuple[str, Type[`AbstractDCS`]]]:
"""尝试导入存在于给定配置中的 DCS 模块。
注意事项:如果一个模块成功导入,我们可以假定其所有的依赖都已经安装。
参数说明:config 是一个配置信息,其中可能包含 DCS 名称作为键。如果提供了 config,则仅尝试导入配置中定义的 DCS 模块。否则,如果为 None,则尝试导入任何支持的 DCS 模块。
返回值说明:返回一个迭代器,每个迭代项是一个包含模块名称和导入的 DCS 类对象的元组。
"""
if TYPE_CHECKING: # pragma: no cover
assert isinstance(__package__, str)
return iter_classes(__package__, AbstractDCS, config)
- 这段代码仅在类型检查时执行。
TYPE_CHECKING
是一个标志,在运行时通常为False
,但在类型检查时为True
。 - 检查
__package__
是否为字符串类型。这一步是为了确保类型安全,但在实际运行时不执行(# pragma: no cover
表示代码覆盖率工具应忽略此行) - 返回
iter_classes
函数的结果,该函数接受三个参数:包名(__package__
)、抽象基类AbstractDCS
以及配置信息config
。
作用:
iter_dcs_classes
函数的作用是根据提供的配置信息尝试导入并返回所有可用的 DCS 模块。具体来说:
- 参数处理:
- 函数接收一个可选的
config
参数,该参数包含了可能的 DCS 名称作为键。 - 如果
config
为None
,则尝试导入所有支持的 DCS 模块;否则,只尝试导入配置中定义的 DCS 模块。
- 函数接收一个可选的
- 模块导入:
- 通过调用
iter_classes
函数来尝试导入指定包中的所有 DCS 模块。 iter_classes
函数负责实际的模块导入,并返回一个迭代器,该迭代器中的每个元素都是一个元组,包含了模块的名称和导入的 DCS 类对象。
- 通过调用
- 类型检查:
- 为了确保类型安全,函数中包含了一段仅在类型检查时执行的代码,用于验证
__package__
是否为字符串类型。
- 为了确保类型安全,函数中包含了一段仅在类型检查时执行的代码,用于验证
通过这种方式,iter_dcs_classes
函数可以动态地发现并导入所有可用的 DCS 模块,这对于构建高度灵活的应用程序是非常有用的,尤其是那些需要根据配置动态选择不同 DCS 实现的应用程序。这样可以确保应用程序可以根据实际情况使用最适合的 DCS 模块,同时保持代码的简洁性和可维护性。
1.4.1.3 类:AbstractDCS
抽象类。
"""抽象表示 DCS 模块。
使用适当的后端客户端接口实现具体的 DCS 类必须包含以下方法和属性。
在其定时至关重要的功能性方法,要求在 ``retry_timeout`` 期间内完成,以防止 DCS 被视为不可访问,每个方法执行复杂数据对象的 构造:
* :meth:`~AbstractDCS._postgresql_cluster_loader`:
处理存储在 DCS 中的数据结构的方法,用于构建包含所有相关联数据的 :class:`Cluster` 对象。
* :meth:`~AbstractDCS._mpp_cluster_loader`:
与上面类似,但特别表示 MPP 组和工作节点信息。
* :meth:`~AbstractDCS._load_cluster`:
主要方法,用于调用特定的 ``loader`` 方法来构建表示集群状态和拓扑结构的 :class:`Cluster` 对象。
在其定时至关重要的功能性方法,并且在编写时必须考虑到 ACID 事务属性:
* :meth:`~AbstractDCS.attempt_to_acquire_leader`:
在领导者选举中用于尝试通过在 DCS 中创建领导者键来获取领导者锁的方法,如果领导者键不存在的话。
* :meth:`~AbstractDCS._update_leader`:
用于更新 DCS 中的 ``leader`` 键的方法。依赖于 Compare-And-Set 以确保更新主锁键。
如果在此方法未能在 ``retry_timeout`` 窗口内更新,则主节点将被降级。
依赖于 Compare-And-Create 来确保只有单一成员创建相关键的功能性方法:
* :meth:`~AbstractDCS.initialize`:
用于集群初始化竞赛的方法,它会在 DCS 中创建 ``initialize`` 键。
DCS 后端的 getter 和 setter 方法及属性:
* :meth:`~AbstractDCS.take_leader`: 用于在 DCS 中创建新的领导者键的方法。
* :meth:`~AbstractDCS.set_ttl`: 用于设置 DCS 中 TTL 值的方法。
* :meth:`~AbstractDCS.ttl`: 返回当前 TTL 的属性。
* :meth:`~AbstractDCS.set_retry_timeout`: 用于设置 DCS 后端中的 ``retry_timeout`` 的方法。
* :meth:`~AbstractDCS._write_leader_optime`: 用于向 DCS 写入 WAL LSN 的兼容性方法。
* :meth:`~AbstractDCS._write_status`: 用于向 DCS 写入 WAL LSN 以供槽位使用的的方法。
* :meth:`~AbstractDCS._write_failsafe`: 用于写入集群拓扑到 DCS 的方法,被 failsafe 机制所使用。
* :meth:`~AbstractDCS.touch_member`: 用于更新 DCS 中单个成员键的方法。
* :meth:`~AbstractDCS.set_history_value`: 用于设置 DCS 中的 ``history`` 键的方法。
使用 Compare-And-Set 的 DCS setter 方法,尽管重要,但如果它们失败,可以重试尝试或可能导致警告日志消息:
* :meth:`~AbstractDCS.set_failover_value`: 用于在 DCS 中创建和/或更新 ``failover`` 键的方法。
* :meth:`~AbstractDCS.set_config_value`: 用于在 DCS 中创建和/或更新 ``failover`` 键的方法。
* :meth:`~AbstractDCS.set_sync_state_value`: 用于设置 DCS 中同步状态 ``sync`` 键的方法。
DCS 数据和键删除方法:
* :meth:`~AbstractDCS.delete_sync_state`:
同样,用于从 DCS 中移除同步状态 ``sync`` 键的方法。
* :meth:`~AbstractDCS.delete_cluster`:
用于从 DCS 中移除集群信息的方法。仅从 `patronictl` 使用。
* :meth:`~AbstractDCS._delete_leader`:
依赖于 CAS 的方法,由当前领导者成员使用,以从 DCS 中移除 ``leader`` 键。
* :meth:`~AbstractDCS.cancel_initialization`:
用于从 DCS 中移除集群的 ``initialize`` 键的方法。
如果 `sync_state` 设置或删除方法中的任何一个失败,尽管不关键,但这可能导致记录 ``Synchronous replication key updated by someone else`` 消息。
应当注意查阅每个抽象方法的任何附加信息和要求,例如在某些条件下应该引发的预期异常以及方法和属性的参数和返回对象类型。
"""
_INITIALIZE = 'initialize'
_CONFIG = 'config'
_LEADER = 'leader'
_FAILOVER = 'failover'
_HISTORY = 'history'
_MEMBERS = 'members/'
_OPTIME = 'optime'
_STATUS = 'status' # JSON, contains "leader_lsn" and confirmed_flush_lsn of logical "slots" on the leader
_LEADER_OPTIME = _OPTIME + '/' + _LEADER # legacy
_SYNC = 'sync'
_FAILSAFE = 'failsafe'
1.4.1.3.1 get_cluster()
- 定义了一个名为
get_cluster
的方法,该方法属于某个类,并返回一个Cluster
类型的对象。此方法的作用是获取一个新鲜的 DCS(Distributed Coordination Service)视图。
def get_cluster(self) -> Cluster:
"""获取一个新鲜的 DCS 视图。
.. note::
存储时间、状态和 failsafe 值的副本,以便在 DCS 更新决策中进行比较。
缓存是必需的,以避免对 REST API 造成负担。
根据可用性,返回 PostgreSQL 或 MPP 实现的 :class:`Cluster` 类型。
:returns: 新鲜的 DCS 集群视图。
"""
# 尝试获取集群视图
try:
cluster = self._get_mpp_cluster() if self.is_mpp_coordinator() else self.__get_postgresql_cluster()
except Exception:
self.reset_cluster()
raise
# 如果在获取集群视图的过程中发生了异常,则首先调用 reset_cluster 方法重置集群状态,然后重新抛出异常
with self._cluster_thread_lock:
self._cluster = cluster
self._cluster_valid_till = time.time() + self.ttl
self._last_seen = int(time.time())
self._last_status = {self._OPTIME: cluster.status.last_lsn, 'retain_slots': cluster.status.retain_slots}
if cluster.status.slots:
self._last_status['slots'] = cluster.status.slots
self._last_failsafe = cluster.failsafe
return cluster
- 尝试获取集群视图:
- 如果当前节点是一个 MPP 协调器(通过
is_mpp_coordinator
方法判断),则调用_get_mpp_cluster
方法获取 MPP 集群视图; - 否则,调用
__get_postgresql_cluster
方法获取 PostgreSQL 集群视图。
- 如果当前节点是一个 MPP 协调器(通过
- 如果在获取集群视图的过程中发生了异常,则首先调用
reset_cluster
方法重置集群状态,然后重新抛出异常。 - 使用
_cluster_thread_lock
锁保护对_cluster
及相关属性的访问,以确保线程安全:- 将获取到的
cluster
对象赋值给_cluster
。 - 设置
_cluster_valid_till
为当前时间加上ttl
时间,这表示集群视图的有效期。 - 更新
_last_seen
为当前时间戳。 - 复制集群的状态信息到
_last_status
中,包括最新 LSN (last_lsn
) 和是否保留 slots (retain_slots
)。 - 如果集群状态中有 slots,则也将其复制到
_last_status
中。 - 复制集群的
failsafe
状态到_last_failsafe
。 - 返回获取到的
cluster
对象。
- 将获取到的
作用:
这个函数的作用是获取一个最新的集群视图,并进行适当的缓存以减少对底层 REST API 的请求开销。具体来说:
- 获取集群视图:
- 根据当前节点的角色(MPP 协调器还是 PostgreSQL 节点)选择合适的集群获取方法。
- 异常处理:
- 在获取集群视图的过程中,如果发生任何异常,会先重置集群状态,然后重新抛出异常,确保异常不会被忽略。
- 线程安全地更新状态:
- 使用锁来保护对集群状态的更新操作,确保在多线程环境中不会发生数据竞争。
- 更新集群状态的相关信息,包括有效期、最后一次看到的时间、状态信息和 failsafe 状态。
- 返回集群视图:
- 最终返回一个最新的集群视图,供后续操作使用。
通过这种方式,get_cluster
方法不仅提供了最新的集群视图,还通过缓存机制减少了不必要的 API 请求,提高了系统的性能和响应速度。同时,通过锁机制保证了线程安全性,防止了并发操作可能导致的数据不一致问题。
1.4.1.3.2 is_mpp_coordinator()
- 定义了一个名为
is_mpp_coordinator
的方法,该方法属于某个类,并返回一个布尔值。此方法的作用是判断当前节点是否作为一个 MPP(Massively Parallel Processing)协调器运行。
def is_mpp_coordinator(self) -> bool:
""":class:`Cluster` 实例具有一个协调器组 ID。
:returns: 如果给定的节点作为 MPP 协调器运行,则返回 ``True``。
"""
# 返回 _mpp 属性的 is_coordinator 方法的结果
return self._mpp.is_coordinator()
作用:
这个函数的作用是确定当前节点是否为 MPP 系统中的协调器节点。具体来说:
- 判断 MPP 协调器:
- 通过调用
_mpp.is_coordinator()
方法来判断当前节点是否为 MPP 系统中的协调器节点。
- 通过调用
- 返回结果:
- 如果当前节点是 MPP 系统中的协调器,则返回
True
;否则返回False
。
- 如果当前节点是 MPP 系统中的协调器,则返回
通过这个方法,可以方便地在代码中判断当前节点的角色,从而执行相应的逻辑。这对于 MPP 数据库系统尤为重要,因为协调器节点和普通的 worker 节点在行为上有显著的区别,例如,协调器节点负责调度查询任务给 worker 节点执行,并收集结果。因此,根据节点的角色来执行不同的操作是非常必要的。
1.4.1.3.3 _get_mpp_cluster()
- 定义了一个名为
_get_mpp_cluster
的私有方法,该方法返回一个Cluster
类型的对象。此方法的作用是从分布式协调服务(DCS)中加载 MPP(大规模并行处理)集群。
def _get_mpp_cluster(self) -> Cluster:
"""从分布式协调服务 (DCS) 加载 MPP 集群。
:returns: 一个 MPP :class:`Cluster` 实例,其中包含了协调器及其工作节点集群在 `Cluster.workers` 字典中。
"""
# 从 DCS 中加载集群信息
groups = self._load_cluster(self._base_path + '/', self._mpp_cluster_loader)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(groups, dict)
# 移除协调器组的信息,并将其赋值给 cluster 变量
cluster = groups.pop(self._mpp.coordinator_group_id, Cluster.empty())
# 将 groups 字典中剩余的工作节点集群信息添加到 cluster.workers 字典中
cluster.workers.update(groups)
return cluster
作用:
这个函数的作用是从分布式协调服务(DCS)中加载 MPP 集群的信息,并组织成一个 Cluster
实例返回。具体来说:
- 加载集群信息:
- 通过
_load_cluster
方法加载集群信息,并将结果存储在groups
变量中。
- 通过
- 类型检查:
- 如果处于类型检查模式,则确保
groups
是一个字典,以帮助类型检查工具进行静态分析。
- 如果处于类型检查模式,则确保
- 提取协调器信息:
- 从
groups
中提取协调器的信息,并创建一个Cluster
实例。 - 如果协调器组不存在,则创建一个空的
Cluster
实例。
- 从
- 添加工作节点信息:
- 将
groups
中剩余的工作节点信息添加到Cluster
实例的workers
字典中。
- 将
- 返回结果:
- 返回构造好的
Cluster
实例,该实例包含了协调器及其工作节点的信息。
- 返回构造好的
通过这种方式,_get_mpp_cluster
方法能够从 DCS 中加载完整的 MPP 集群信息,并以 Cluster
实例的形式返回,便于后续处理和管理 MPP 集群。
1.4.1.3.4 _load_cluster()
- 定义了一个名为
_load_cluster
的方法,该方法属于某个类,并返回Union[Cluster, Dict[int, Cluster]]
类型。此方法有两个参数:path
和loader
。
def _load_cluster(
self, path: str, loader: Callable[[Any], Union[Cluster, Dict[int, Cluster]]]
) -> Union[Cluster, Dict[int, Cluster]]:
"""实现加载 :class:`Cluster` 实例的主要抽象方法。
.. note::
内部,此方法应调用 *loader* 方法,该方法将构建表示 DCS 中集群当前状态和拓扑结构的 :class:`Cluster` 对象。
预期此方法仅由 :meth:`~AbstractDCS.get_cluster` 方法调用。
:param path: 在 DCS 中加载 Cluster(s) 的路径。
:param loader: 为 :meth:`~AbstractDCS._postgresql_cluster_loader` 或
:meth:`~AbstractDCS._mpp_cluster_loader` 中的一个。
:raise: 在与 DCS 通信出现问题的情况下引发 :exc:`~DCSError`。
如果当前节点正在作为主节点运行并且引发了异常,则实例将被降级。
"""
作用:
这个函数的作用是从分布式协调服务(DCS)中加载集群信息,并通过指定的加载器方法来构建 Cluster
实例。具体来说:
- 加载集群信息:
path
参数指定了在 DCS 中加载集群信息的路径。loader
参数是一个函数或方法,用于实际加载并构建Cluster
对象。
- 调用加载器:
- 内部调用
loader
方法来构建表示 DCS 中集群当前状态和拓扑结构的Cluster
对象。
- 内部调用
- 预期调用场景:
- 此方法预期仅由
get_cluster
方法调用,以确保在正确的上下文中加载集群信息。
- 此方法预期仅由
- 异常处理:
- 如果在与 DCS 通信时出现问题,则会引发
DCSError
异常。 - 如果当前节点正在作为主节点运行,并且在此过程中引发了异常,则会导致该节点被降级。
- 如果在与 DCS 通信时出现问题,则会引发
1.4.1.3.5 _mpp_cluster_loader()
- 定义了一个名为
_mpp_cluster_loader
的抽象方法,该方法属于某个类,并且返回类型为Dict[int, Cluster]
。此方法有一个参数path
,类型为Any
。
@abc.abstractmethod
def _mpp_cluster_loader(self, path: Any) -> Dict[int, Cluster]:
"""加载并构建来自单个 MPP 集群的所有 PostgreSQL 集群。
:param path: 在 DCS 中加载 Cluster(s) 的路径。
:returns: 所有的 MPP 组作为一个 :class:`dict`,其中组 ID 作为键,:class:`Cluster` 对象作为值。
"""
作用:
这个函数的作用是从分布式协调服务(DCS)中加载单个 MPP(大规模并行处理)集群中的所有 PostgreSQL 集群信息,并构建出这些集群的 Cluster
对象。具体来说:
- 加载集群信息:
- 通过
path
参数指定在 DCS 中加载集群信息的位置。
- 通过
- 构建集群对象:
- 加载完成后,构建出包含所有 MPP 组的字典,其中组 ID 作为键,对应的
Cluster
对象作为值。
- 加载完成后,构建出包含所有 MPP 组的字典,其中组 ID 作为键,对应的
1.4.1.3.6 __get_postgresql_cluster()
- 定义了一个名为
__get_postgresql_cluster
的实例方法,该方法接受一个可选的字符串参数path
,并返回一个Cluster
类型的实例。
def __get_postgresql_cluster(self, path: Optional[str] = None) -> Cluster:
"""底层方法,从 DCS 加载一个 :class:`Cluster` 对象。
:param path: 可选的客户端路径,在 DCS 后端加载。
:returns: 加载的 :class:`Cluster` 实例。
"""
# 从分布式一致性存储(DCS)中加载一个 Cluster 对象
if path is None:
path = self.client_path('')
# 加载 Cluster 对象
cluster = self._load_cluster(path, self._postgresql_cluster_loader)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(cluster, Cluster)
return cluster
作用:
这个方法的作用是从指定的路径(默认为通过 self.client_path
获取的路径)所在的 DCS 后端加载一个 Cluster
对象。具体来说:
- 路径检查:如果
path
参数没有提供,则使用self.client_path
方法获取路径。 - 加载 Cluster 对象:使用
_load_cluster
方法,并传入路径和加载器_postgresql_cluster_loader
来加载Cluster
对象。 - 类型断言:如果当前环境处于类型检查模式,则断言加载得到的对象必须是
Cluster
类型的。 - 返回结果:返回加载得到的
Cluster
对象。
1.4.1.3.7 client_path()
- 定义了一个名为
client_path
的实例方法,该方法接受一个字符串参数path
,并返回一个字符串。
def client_path(self, path: str) -> str:
"""从适当的部分构造绝对键名,以适应 DCS 类型。
:param path: 当前 Patroni 集群中的键名。
:returns: 当前 Patroni 集群的绝对键名。
"""
components = [self._base_path]
if self._mpp.is_enabled():
components.append(str(self._mpp.group))
components.append(path.lstrip('/'))
return '/'.join(components)
作用:
这个方法的作用是根据当前的配置构造一个适合 DCS 使用的绝对键名。具体来说:
- 初始化路径组件:首先创建一个列表
components
,并加入_base_path
。 - 附加多主处理组标识:如果多主处理(
_mpp
)已启用,则将多主处理的组标识添加到路径组件中。 - 附加相对路径:移除传入的
path
前面的斜杠(如果有的话),然后将其添加到路径组件列表中。 - 构造绝对键名:使用斜杠
/
来连接路径组件列表中的所有元素,并返回构造好的绝对键名。
1.4.1.3.8 _postgresql_cluster_loader()
- 定义了一个名为
_postgresql_cluster_loader
的抽象方法,该方法接受一个参数path
,并返回一个Cluster
类型的实例。
@abc.abstractmethod
def _postgresql_cluster_loader(self, path: Any) -> Cluster:
"""从 DCS 加载并构建表示单个 PostgreSQL 集群的 :class:`Cluster` 对象。
:param path: 在 DCS 中加载 :class:`Cluster` 的路径。
:returns: :class:`Cluster` 实例。
"""
作用:
这个抽象方法的作用是在抽象基类中声明一个方法 _postgresql_cluster_loader
,该方法应该从 DCS 中加载并构建表示单个 PostgreSQL 集群的 Cluster
对象。由于这是一个抽象方法,它本身并没有实现具体的逻辑,而是要求所有继承自该抽象基类的子类必须提供具体的实现。
1.4.1.3.9 _load_cluster()
- 定义了一个名为
_load_cluster
的抽象方法,该方法接受两个参数:path
和loader
,并返回一个Cluster
类型的实例或包含Cluster
实例的字典。
@abc.abstractmethod
def _load_cluster(
self, path: str, loader: Callable[[Any], Union[Cluster, Dict[int, Cluster]]]
) -> Union[Cluster, Dict[int, Cluster]]:
"""Main abstract method that implements the loading of :class:`Cluster` instance.
.. note::
Internally this method should call the *loader* method that will build :class:`Cluster` object which
represents current state and topology of the cluster in DCS. This method supposed to be called only by
the :meth:`~AbstractDCS.get_cluster` method.
:param path: the path in DCS where to load Cluster(s) from.
:param loader: one of :meth:`~AbstractDCS._postgresql_cluster_loader` or
:meth:`~AbstractDCS._mpp_cluster_loader`.
:raise: :exc:`~DCSError` in case of communication problems with DCS. If the current node was running as a
primary and exception raised, instance would be demoted.
"""
作用:
这个抽象方法的作用是在抽象基类中声明一个方法 _load_cluster
,该方法应该从 DCS 中加载 Cluster
实例或一组 Cluster
实例。此方法需要在具体的子类中实现具体的加载逻辑。具体来说:
- 加载路径:提供一个 DCS 中的路径,用于加载集群数据。
- 加载器:提供一个
loader
函数,该函数负责根据提供的路径加载并构建Cluster
对象。 - 返回类型:根据
loader
的实现返回一个Cluster
对象或包含多个Cluster
对象的字典。 - 异常处理:在与 DCS 的通信出现问题时,应该抛出
DCSError
异常。如果当前节点是主节点并且发生了异常,节点会被降级。
1.4.1.3.10 reset_cluster()
- 定义了一个名为
reset_cluster
的实例方法,该方法不需要任何参数,并且没有返回值(返回类型为None
)。
def reset_cluster(self) -> None:
"""清除 DCS 的缓存状态。"""
# 使用 self._cluster_thread_lock 锁来确保线程安全,即在同一时间只有一个线程可以执行下面的代码块。
with self._cluster_thread_lock:
self._cluster = None
self._cluster_valid_till = 0
作用:
这个方法的作用是清除缓存中的集群状态信息,并确保任何后续的操作都会重新从 DCS 加载最新的集群状态。具体来说:
- 锁定资源:使用线程锁来确保在同一时间内只有一个线程可以执行下面的操作,以避免并发访问导致的问题。
- 清空
_cluster
:将_cluster
属性设置为None
,表示当前缓存的集群状态被清除。 - 设置
_cluster_valid_till
:将_cluster_valid_till
属性设置为0
,表示当前缓存的集群状态不再有效,后续的请求需要重新加载最新的集群状态。
1.4.1.3.11 reload_config()
- 定义了一个名为
reload_config
的实例方法,该方法接受一个类型为Union['Config', Dict[str, Any]]
的参数config
,并返回None
。
def reload_config(self, config: Union['Config', Dict[str, Any]]) -> None:
"""从配置加载并设置相关值。
设置 loop_wait、ttl 和 retry_timeout 属性。
:param config: 加载的配置信息对象或键值对的字典。
"""
self._set_loop_wait(config['loop_wait'])
self.set_ttl(config['ttl'])
self.set_retry_timeout(config['retry_timeout'])
作用:
这个方法的作用是从配置信息中重新加载并设置相关的属性值。具体来说:
- 设置循环等待时间 (
loop_wait
):- 从配置中获取
loop_wait
的值。 - 调用
self._set_loop_wait
方法来设置loop_wait
的新值。
- 从配置中获取
- 设置生存时间 (
ttl
):- 从配置中获取
ttl
的值。 - 调用
self.set_ttl
方法来设置ttl
的新值。
- 从配置中获取
- 设置重试超时时间 (
retry_timeout
):- 从配置中获取
retry_timeout
的值。 - 调用
self.set_retry_timeout
方法来设置retry_timeout
的新值。
- 从配置中获取
1.4.1.3.12 _set_loop_wait()
- 定义了一个名为
_set_loop_wait
的实例方法,该方法接受一个类型为int
的参数loop_wait
,并返回None
。
def _set_loop_wait(self, loop_wait: int) -> None:
"""设置新的 loop_wait 值。
:param loop_wait: 要设置的值。
"""
self._loop_wait = loop_wait
作用:
这个方法的作用是设置一个新的 loop_wait
值。具体来说:
- 接收参数:接收一个整数类型的参数
loop_wait
。 - 设置值:将传入的
loop_wait
值赋给实例变量self._loop_wait
。
1.4.1.3.13 set_ttl()
- 定义了一个名为
set_ttl
的抽象方法(abstract method),该方法接受一个类型为int
的参数ttl
,并返回类型为Optional[bool]
的值。
@abc.abstractmethod
def set_ttl(self, ttl: int) -> Optional[bool]:
"""设置 DCS keys 的新的 ttl 值。"""
此方法用于设置分布式协调服务(DCS, Distributed Coordination Service)中键的新的 ttl
(time-to-live,存活时间)值。
作用:
这个抽象方法的作用是设置 DCS 中键的新的 ttl
值。具体来说:
- 接收参数:接收一个整数类型的参数
ttl
,表示新的ttl
值。 - 返回类型:返回类型为
Optional[bool]
的值,意味着该方法可以返回一个布尔值或None
。
1.4.1.3.14 set_retry_timeout()
- 定义了一个名为
set_retry_timeout
的抽象方法(abstract method),该方法接受一个类型为int
的参数retry_timeout
,并返回None
。
@abc.abstractmethod
def set_retry_timeout(self, retry_timeout: int) -> None:
"""设置新的 retry_timeout 值。"""
此方法用于设置新的 retry_timeout
值。
作用:
这个抽象方法的作用是设置新的 retry_timeout
值。具体来说:
- 接收参数:接收一个整数类型的参数
retry_timeout
,表示新的retry_timeout
值。 - 返回类型:返回
None
,表示该方法执行完后不需要返回任何值。
由于这是一个抽象方法,它在基类中并没有具体实现,而是要求所有继承自该基类的子类必须实现这个方法。这意味着所有继承自该基类的子类都必须提供具体的 set_retry_timeout
方法实现,以便能够正确地设置 retry_timeout
的值。
1.4.1.3.15 mpp()
- 使用
@property
装饰器声明一个名为mpp
的属性,该属性将作为一个只读属性来访问。 - 定义一个名为
mpp
的方法,该方法没有参数,并返回类型为AbstractMPP
的对象。
@property
def mpp(self) -> 'AbstractMPP':
"""获取有效的底层 MPP(如果有配置的话)。"""
return self._mpp
作用:
这个属性的作用是提供一个只读接口来访问实例变量 _mpp
,该变量通常保存了一个 AbstractMPP
类型的对象,代表某种多点处理(Multi-Point Processing, MPP)机制或接口。具体来说:
- 只读属性:通过
@property
装饰器声明,使mpp
成为一个只读属性,可以通过instance.mpp
的方式访问,而不需要像普通方法那样加上括号。 - 获取底层 MPP:返回配置的底层 MPP 对象,如果有的话。通常
_mpp
应该已经在其他地方被正确地初始化了。
1.4.1.3.16 __init__()
-
定义了一个构造函数
__init__
,该构造函数接受两个参数:config: Dict[str, Any]
:一个字典类型的参数,表示选定的 DCS(Distributed Consistency System,分布式一致性系统)的配置节。mpp: AbstractMPP
:一个实现了AbstractMPP
接口的对象。
构造函数返回
None
,表示它是用来初始化对象状态的。
def __init__(self, config: Dict[str, Any], mpp: 'AbstractMPP') -> None:
"""准备 DCS 路径、MPP 对象、状态信息的初始值和处理依赖项。
:param config: :class:`dict`,指向选定 DCS 的配置节的引用。
例如:对于 ZooKeeper 使用 `zookeeper`,对于 Etcd 使用 `etcd` 等等。
:param mpp: 实现了 :class:`AbstractMPP` 接口的对象。
"""
self._mpp = mpp
self._name = config['name']
# 使用正则表达式 re.sub 清理路径中的多余斜杠
self._base_path = re.sub('/+', '/', '/'.join(['', config.get('namespace', 'service'), config['scope']]))
# 根据 config 中的 loop_wait 字段设置循环等待的时间间隔
self._set_loop_wait(config.get('loop_wait', 10))
# 从 config 中提取 patronictl 字段,并将其转换为布尔值
self._ctl = bool(config.get('patronictl', False))
self._cluster: Optional[Cluster] = None
self._cluster_valid_till: float = 0
self._cluster_thread_lock = Lock()
self._last_lsn: int = 0
self._last_seen: int = 0
self._last_status: Dict[str, Any] = {'retain_slots': []}
self._last_retain_slots: Dict[str, float] = {}
self._last_failsafe: Optional[Dict[str, str]] = {}
# 初始化 event 成员变量为一个事件对象
self.event = Event()
作用:
__init__
函数的具体作用是初始化一个对象的成员变量。具体来说:
- 初始化 MPP 对象:
- 将传入的
mpp
对象保存到_mpp
成员变量中,这可能是与某种特定的分布式一致性系统交互的对象。
- 将传入的
- 设置 DCS 路径:
- 根据配置中的
namespace
和scope
设置_base_path
,用于后续的操作。
- 根据配置中的
- 初始化状态信息:
- 设置
_name
成员变量为配置中的name
字段。 - 设置循环等待时间
_set_loop_wait
。 - 设置
_ctl
成员变量为配置中的patronictl
字段的布尔值。 - 初始化
_cluster
为None
,表示集群信息尚未加载。 - 初始化
_cluster_valid_till
为0
,表示集群信息无效。 - 初始化线程锁
_cluster_thread_lock
,用于同步对集群信息的访问。 - 初始化其他状态变量,如
_last_lsn
、_last_seen
、_last_status
、_last_retain_slots
和_last_failsafe
。
- 设置
- 设置同步机制:
- 创建一个事件对象
event
,用于同步异步操作。
- 创建一个事件对象
1.4.1.3.17 get_mpp_coordinator()
- 定义了一个名为
get_mpp_coordinator
的实例方法,该方法没有参数,并返回一个可选的Cluster
类型对象。
def get_mpp_coordinator(self) -> Optional[Cluster]:
"""加载 MPP 协调者的 PostgreSQL 集群。
.. note::
此方法仅在工作节点上执行以查找协调者。
:returns: 选择与 MPP 协调者组 ID 关联的 :class:`Cluster` 实例。
"""
try:
# 调用私有方法 __get_postgresql_cluster 加载与 MPP 协调者组 ID 相关联的 PostgreSQL 集群
return self.__get_postgresql_cluster(f'{self._base_path}/{self._mpp.coordinator_group_id}/')
except Exception as e:
logger.error('Failed to load %s coordinator cluster from %s: %r',
self._mpp.type, self.__class__.__name__, e)
return None
作用:
get_mpp_coordinator
函数的具体作用是从分布式一致性系统中加载与 MPP 协调者组 ID 相关联的 PostgreSQL 集群。具体来说:
- 加载协调者集群:
- 该方法尝试通过调用私有方法
__get_postgresql_cluster
来加载 MPP 协调者的集群信息。路径是由_base_path
和_mpp.coordinator_group_id
组合而成的。
- 该方法尝试通过调用私有方法
- 异常处理:
- 如果在加载过程中发生任何异常,该方法会捕获异常,并通过日志记录器记录一条错误信息。这有助于调试和监控系统的健康状况。
1.4.1.3.18 _get_mpp_cluster()
- 定义了一个名为
_get_mpp_cluster
的实例方法,该方法没有参数,并返回一个Cluster
类型的对象。
def _get_mpp_cluster(self) -> Cluster:
"""从分布式一致性系统(DCS)加载 MPP 集群。
:returns: 一个 MPP :class:`Cluster` 实例,其中包含协调者的集群信息,并且在 `Cluster.workers` 字典中存储了工作节点集群的信息。
"""
# 加载指定路径下的集群组信息
groups = self._load_cluster(self._base_path + '/', self._mpp_cluster_loader)
# 进行类型检查
if TYPE_CHECKING: # pragma: no cover
assert isinstance(groups, dict)
# 从 groups 字典中移除协调者组,并将其赋值给 cluster。如果协调者组 ID 在 groups 字典中不存在,则使用 Cluster.empty() 方法创建一个新的空集群对象
cluster = groups.pop(self._mpp.coordinator_group_id, Cluster.empty())
# 将 groups 字典中剩余的工作节点集群信息添加到 cluster.workers 字典中
cluster.workers.update(groups)
# 返回最终构建的 cluster 对象。
return cluster
作用:
_get_mpp_cluster
函数的具体作用是从分布式一致性系统(DCS)中加载 MPP(Massively Parallel Processing,大规模并行处理)集群的信息。具体来说:
- 加载集群组:
- 调用
_load_cluster
方法来加载_base_path
下的集群组信息。
- 调用
- 类型检查:
- 当使用类型检查工具时,确保
groups
是字典类型。
- 当使用类型检查工具时,确保
- 获取协调者集群:
- 从加载的集群组中移除协调者组,并将其赋值给
cluster
。如果协调者组不存在,则创建一个新的空集群对象。
- 从加载的集群组中移除协调者组,并将其赋值给
- 更新工作节点集群:
- 将剩余的集群组信息更新到
cluster.workers
字典中,其中包含了所有工作节点的集群信息。
- 将剩余的集群组信息更新到
- 返回集群对象:
- 返回最终构建的
cluster
对象,该对象包含了协调者集群及其对应的工作节点集群信息。
- 返回最终构建的
1.4.1.3.19 reset_cluster()
- 定义了一个名为
reset_cluster
的实例方法,该方法没有参数,并且不返回任何值(返回类型为None
)。
def reset_cluster(self) -> None:
"""清除 DCS 的缓存状态。"""
# 使用 with 语句获取 _cluster_thread_lock 互斥锁,确保在多线程环境下对 _cluster 和 _cluster_valid_till 的修改是原子性的
with self._cluster_thread_lock:
self._cluster = None
self._cluster_valid_till = 0
作用:
reset_cluster
函数的具体作用是清除与 DCS(Distributed Consistency System,分布式一致性系统)相关的缓存状态。具体来说:
- 获取互斥锁:
- 使用
with
语句获取_cluster_thread_lock
互斥锁,确保在多线程环境下对_cluster
和_cluster_valid_till
的修改不会出现竞态条件。
- 使用
- 清除集群状态:
- 将
_cluster
属性设置为None
,表示清除当前缓存的集群状态。 - 将
_cluster_valid_till
属性设置为0
,表示清除有效时间标记,使得下次请求时需要重新加载集群状态。
- 将
- 释放互斥锁:
- 当
with
语句块执行完毕后,会自动释放_cluster_thread_lock
互斥锁。
- 当
1.4.1.3.20 _write_leader_optime()
- 定义了一个名为
_write_leader_optime
的抽象方法,该方法接受一个str
类型的参数last_lsn
,并返回一个bool
类型的值。此方法前加了@abc.abstractmethod
装饰器,表明这是一个抽象基类中的抽象方法,任何继承此类的子类都必须实现这个方法。
@abc.abstractmethod
def _write_leader_optime(self, last_lsn: str) -> bool:
"""将当前的 WAL LSN 写入 DCS 中的 ``/optime/leader`` 键。
:param last_lsn: 绝对的 WAL LSN(日志序列号)以字节为单位。
:returns: 如果成功提交到 DCS,则返回 ``True``。
"""
作用:
_write_leader_optime
函数的具体作用是将当前的 WAL(Write-Ahead Logging,预写式日志)LSN(Log Sequence Number,日志序列号)写入到 DCS(Distributed Consistency System,分布式一致性系统)中指定的键下。具体来说:
- 参数接收:
- 接受一个
last_lsn
参数,表示要写入的最新的 WAL LSN。
- 接受一个
- 写入 DCS:
- 将
last_lsn
写入到 DCS 中的/optime/leader
键下。
- 将
- 返回值:
- 如果成功写入 DCS,则返回
True
表示操作成功; - 如果写入失败,则应返回
False
,虽然在实际代码中未明确表示,但在实现时应该考虑到这种情况。
- 如果成功写入 DCS,则返回
1.4.1.3.21 write_leader_optime()
- 定义了一个名为
write_leader_optime
的实例方法,该方法接受一个int
类型的参数last_lsn
,并且不返回任何值(返回类型为None
)。
def write_leader_optime(self, last_lsn: int) -> None:
"""将 WAL LSN 的值写入 DCS 中的 ``optime/leader`` 键。
.. note::
本方法抽象了 :meth:`~Cluster.write_status` 所需的数据结构,因此在调用者处不需要。但是,
``optime/leader`` 只有在集群中有足够老版本的 Patroni 成员时才会在 :meth:`~Cluster.write_status` 中写入
(即旧版的 Patroni 不理解新的格式)。
:param last_lsn: 绝对的 WAL LSN(日志序列号)以字节为单位。
"""
self.write_status({self._OPTIME: last_lsn})
作用:
write_leader_optime
函数的具体作用是将当前的 WAL(Write-Ahead Logging,预写式日志)LSN(Log Sequence Number,日志序列号)写入到 DCS(Distributed Consistency System,分布式一致性系统)中指定的键下。具体来说:
- 参数接收:
- 接受一个
last_lsn
参数,表示要写入的最新的 WAL LSN。
- 接受一个
- 写入状态:
- 将
last_lsn
的值通过self.write_status
方法写入到 DCS 中,该方法负责将状态信息写入 DCS。通过构造一个字典{self._OPTIME: last_lsn}
,将last_lsn
与一个标识符_OPTIME
关联起来。
- 将
- 兼容性考虑:
- 如注释中提到的,
optime/leader
只有在集群中有足够老版本的 Patroni 成员时才需要写入。这是因为旧版本的 Patroni 可能不理解新的格式,所以需要保持向后兼容性。
- 如注释中提到的,
1.4.1.3.22 _write_status()
- 定义了一个名为
_write_status
的抽象方法,该方法接受一个str
类型的参数value
,并返回一个bool
类型的值。此方法前加了@abc.abstractmethod
装饰器,表明这是一个抽象基类中的抽象方法,任何继承此类的子类都必须实现这个方法。
@abc.abstractmethod
def _write_status(self, value: str) -> bool:
"""将当前的 WAL LSN 和永久槽位的 `confirmed_flush_lsn` 写入 DCS 中的 ``/status`` 键。
:param value: 以 JSON 格式序列化的状态。
:returns: 如果成功提交到 DCS,则返回 ``True``。
"""
作用:
_write_status
函数的具体作用是将当前的状态信息写入到 DCS(Distributed Consistency System,分布式一致性系统)中指定的键下。具体来说:
- 参数接收:
- 接受一个
value
参数,表示要写入的状态信息,该信息是以 JSON 格式序列化的字符串。
- 接受一个
- 写入 DCS:
- 将状态信息写入到 DCS 中的
/status
键下。
- 将状态信息写入到 DCS 中的
- 返回值:
- 如果成功写入 DCS,则返回
True
表示操作成功; - 如果写入失败,则应返回
False
,虽然在实际代码中未明确表示,但在实现时应该考虑到这种情况。
- 如果成功写入 DCS,则返回
1.4.1.3.23 write_status()
- 定义了一个名为
write_status
的实例方法,该方法接受一个Dict[str, Any]
类型的参数value
,并且不返回任何值(返回类型为None
)。
def write_status(self, value: Dict[str, Any]) -> None:
"""如果状态发生变化,则将集群状态写入 DCS。
.. note::
DCS 键 ``/status`` 在 Patroni 版本 2.1.0 中引入。在此之前,最后已知领导者的 LSN 存储在 ``optime/leader`` 中。此方法具有检测功能,以支持旧版本成员的向后兼容性。
:param value: 包含当前 WAL LSN 和永久槽位的 ``confirmed_flush_lsn`` 的可 JSON 序列化的字典。
"""
# 这个方法总是带optime键调用,其他键是可选的。
# 如果我们知道旧的值(存储在self._last_status中),我们将复制它们。
for name in ('slots', 'retain_slots'):
if name not in value and self._last_status.get(name):
value[name] = self._last_status[name]
# 如果键存在,但值为None,则不写入这样的对。
# 创建一个新的字典,其中不包含键值对中值为 None 的条目
value = {k: v for k, v in value.items() if v is not None}
# 如果当前状态 value 与上次记录的状态 self._last_status 不同,并且成功地将状态写入到 DCS,则更新 self._last_status 为当前状态 value
if not deep_compare(self._last_status, value) and self._write_status(json.dumps(value, separators=(',', ':'))):
self._last_status = value
# 获取当前集群的最小版本号 min_version
cluster = self.cluster
min_version = cluster and cluster.min_version
# 如果集群中最老的 Patroni 版本小于 2.1.0,并且当前的 optime 值与上次记录的不同,则更新 _last_lsn 为当前的 optime 值,并调用 _write_leader_optime 方法将 optime 写入到 DCS 中
if min_version and min_version < (2, 1, 0) and self._last_lsn != value[self._OPTIME]:
self._last_lsn = value[self._OPTIME]
self._write_leader_optime(str(value[self._OPTIME]))
作用:
write_status
函数的具体作用是将当前的状态信息写入到 DCS(Distributed Consistency System,分布式一致性系统)中指定的键下,并处理一些兼容性问题。具体来说:
- 参数接收:
- 接受一个
value
参数,表示要写入的状态信息,该信息是一个字典,包含当前的 WAL LSN 和永久槽位的confirmed_flush_lsn
。
- 接受一个
- 复制额外的状态信息:
- 如果
value
中缺少某些键,而这些键在self._last_status
中存在,则将这些键的值复制到value
中。
- 如果
- 过滤
None
值:- 创建一个新的字典,移除键值对中值为
None
的条目。
- 创建一个新的字典,移除键值对中值为
- 检查状态是否改变:
- 如果当前状态与上次记录的状态不同,则将状态信息写入到 DCS,并更新
self._last_status
。
- 如果当前状态与上次记录的状态不同,则将状态信息写入到 DCS,并更新
- 兼容性处理:
- 如果集群中最老的 Patroni 版本小于 2.1.0,并且当前的
optime
值与上次记录的不同,则更新_last_lsn
并将optime
写入到 DCS 中。
- 如果集群中最老的 Patroni 版本小于 2.1.0,并且当前的
1.4.1.3.24 _write_failsafe()
- 定义了一个名为
_write_failsafe
的抽象方法,该方法接受一个str
类型的参数value
,并返回一个bool
类型的值。此方法前加了@abc.abstractmethod
装饰器,表明这是一个抽象基类中的抽象方法,任何继承此类的子类都必须实现这个方法。
@abc.abstractmethod
def _write_failsafe(self, value: str) -> bool:
"""将当前的集群拓扑写入 DCS,该拓扑将被故障安全机制(如果启用的话)使用。
:param value: 以 JSON 格式序列化的故障安全拓扑。
:returns: 如果成功提交到 DCS,则返回 ``True``。
"""
作用:
_write_failsafe
函数的具体作用是在 DCS(Distributed Consistency System,分布式一致性系统)中存储当前集群的拓扑结构,以便在故障安全机制启用时使用。具体来说:
- 参数接收:
- 接受一个
value
参数,表示要写入 DCS 的当前集群拓扑信息,该信息是以 JSON 格式序列化的字符串。
- 接受一个
- 写入 DCS:
- 将当前集群拓扑信息写入 DCS 中的一个特定键下,这个键通常用于故障安全机制的启动或恢复过程。
- 返回值:
- 如果成功写入 DCS,则返回
True
表示操作成功; - 如果写入失败,则应返回
False
,虽然在实际代码中未明确表示,但在实现时应该考虑到这种情况。
- 如果成功写入 DCS,则返回
1.4.1.3.25 write_failsafe()
- 定义了一个名为
write_failsafe
的实例方法,该方法接受一个Dict[str, str]
类型的参数value
,并且不返回任何值(返回类型为None
)。
def write_failsafe(self, value: Dict[str, str]) -> None:
"""将 ``/failsafe`` 键写入 DCS。
:param value: 要设置的字典值,由成员的 ``name`` 和 ``api_url`` 组成。
"""
# 这段代码首先检查当前传入的 value 是否与之前记录的 self._last_failsafe 相同
if not (isinstance(self._last_failsafe, dict) and deep_compare(self._last_failsafe, value)) \
and self._write_failsafe(json.dumps(value, separators=(',', ':'))):
self._last_failsafe = value
作用:
write_failsafe
函数的具体作用是在 DCS(Distributed Consistency System,分布式一致性系统)中存储当前集群成员的信息,这些信息包括成员的名称和 API URL,以便在故障安全机制启用时使用。具体来说:
- 参数接收:
- 接受一个
value
参数,表示要写入 DCS 的当前集群成员信息,该信息是一个字典,包含成员的name
和api_url
。
- 接受一个
- 检查状态是否改变:
- 如果当前传入的
value
与之前记录的self._last_failsafe
不相同,则继续执行写入操作。
- 如果当前传入的
- 序列化值:
- 使用
json.dumps
方法将value
字典序列化为 JSON 字符串,并使用逗号,
和冒号:
作为分隔符,使 JSON 字符串更加紧凑。
- 使用
- 写入 DCS:
- 调用
_write_failsafe
方法将序列化后的value
写入到 DCS 中。
- 调用
- 更新状态:
- 如果成功写入 DCS,则更新
self._last_failsafe
为当前的value
。
- 如果成功写入 DCS,则更新
1.4.1.3.26 _build_retain_slots()
- 定义了一个名为
_build_retain_slots
的私有方法,该方法接受一个Cluster
类型的参数cluster
和一个可选的Dict[str, int]
类型的参数slots
,返回一个Optional[List[str]]
类型的值,即可能返回一个字符串列表或者None
。
def _build_retain_slots(self, cluster: Cluster, slots: Optional[Dict[str, int]]) -> Optional[List[str]]:
"""处理集群成员物理复制槽的保留策略。
当成员密钥缺失时,我们希望保留其复制槽一段时间,以便当它重新上线时 WAL 段不会已经消失。这通过在 ``/status`` 键的 ``retain_slots`` 字段中存储代表成员的复制槽列表来解决。
该方法通过在内存中保持这样的复制槽列表,并在它们被观察到的时间超过 ``member_slots_ttl`` 时删除名称来处理保留策略。
:param cluster: 包含当前集群状态信息的 :class:`Cluster` 对象。
:param slots: 存在于领导者节点上的槽名称及其 LSN 值,包括成员槽和永久复制槽。
:returns: 要写入 ``/status`` 键的复制槽列表或 ``None``。
"""
# 获取当前的时间戳
timestamp = time.time()
# DCS是真相的来源,因此我们从中提取缺失的值
self._last_retain_slots.update({name: timestamp for name in self._last_status['retain_slots']
if (not slots or name not in slots) and name not in self._last_retain_slots})
# 这段代码检查 slots 是否非空
if slots: # 意味着当前运行的是 PostgreSQL 11+ 的版本
# 如果 slots 非空,那么遍历 cluster.members 列表,将所有非 nostream 的成员添加到 members 集合中
members: Set[str] = set()
found_self = False
for member in cluster.members:
found_self = member.name == self._name
if not member.nostream:
members.add(slot_name_from_member_name(member.name))
# 如果当前节点没有找到自己(即 found_self 仍然为 False),则将当前节点的名字也添加到 members 集合中。
if not found_self:
# 可能是我们节点的成员键不在DCS中,我们无法检查tag .nostream。
# 在这种情况下,我们的名字会错误地出现在‘ retain_slots ’中,但只是暂时的。
members.add(slot_name_from_member_name(self._name))
# 获取集群中的永久物理槽,并更新 self._last_retain_slots,只保留那些属于成员而非永久槽的槽
permanent_slots = cluster.permanent_physical_slots
# 我们希望在‘ ’ retain_slots ' ‘ ’中只包含非常任理事国的位置
self._last_retain_slots.update({name: timestamp for name in slots
if name in members and name not in permanent_slots})
# retention
# 遍历 self._last_retain_slots,如果某个槽的时间戳加上 global_config.member_slots_ttl 小于当前时间戳,则认为该槽已过期,并从 self._last_retain_slots 中删除该槽
for name, value in list(self._last_retain_slots.items()):
if value + global_config.member_slots_ttl <= timestamp:
logger.info("Replication slot '%s' for absent cluster member is expired after %d sec.",
name, global_config.member_slots_ttl)
del self._last_retain_slots[name]
# 返回排序后的 self._last_retain_slots 的键列表,如果没有保留的槽,则返回 None
return list(sorted(self._last_retain_slots.keys())) or None
作用:
_build_retain_slots
函数的具体作用是构建和维护一个成员复制槽的列表,这些槽在成员暂时离线时会被保留,以防止成员重新上线时丢失重要的 WAL(Write-Ahead Logging)段。具体来说:
- 参数接收:
- 接受一个
Cluster
类型的参数cluster
,包含当前集群状态信息。 - 接受一个可选的
Dict[str, int]
类型的参数slots
,表示当前存在的复制槽及其 LSN 值。
- 接受一个
- 获取当前时间戳:
- 获取当前时间戳,用于判断槽的保留时间。
- 更新保留槽:
- 从 DCS 获取缺失的槽,并更新
self._last_retain_slots
。
- 从 DCS 获取缺失的槽,并更新
- 处理成员槽:
- 构建一个集合
members
,包含所有非nostream
的成员槽。
- 构建一个集合
- 处理永久槽:
- 从
members
中排除永久槽。
- 从
- 处理过期的槽:
- 删除过期的槽。
- 返回结果:
- 返回排序后的保留槽列表,如果没有保留的槽,则返回
None
。
- 返回排序后的保留槽列表,如果没有保留的槽,则返回
1.4.1.3.27 update_leader()
-
定义了一个名为
update_leader
的实例方法,该方法接受以下参数:cluster
:Cluster
类型的对象,包含当前集群状态的信息。last_lsn
: 可选的整数类型参数,表示绝对的 WAL(Write-Ahead Logging)LSN(Log Sequence Number)位置,单位为字节。slots
: 可选的字典类型参数,包含永久槽的confirmed_flush_lsn
。failsafe
: 可选的字典类型参数,如果定义了,则传递给write_failsafe
方法。
方法返回一个布尔值,表示是否成功更新了
leader
密钥(或会话)的 TTL。
def update_leader(self,
cluster: Cluster,
last_lsn: Optional[int],
slots: Optional[Dict[str, int]] = None,
failsafe: Optional[Dict[str, str]] = None) -> bool:
"""更新 `leader` 密钥(或会话)的 TTL,以及 `/status` 和 `/failsafe` 密钥。
:param cluster: 包含当前集群状态信息的 :class:`Cluster` 对象。
:param last_lsn: 绝对的 WAL LSN,以字节为单位。
:param slots: 包含永久槽 `confirmed_flush_lsn` 的字典。
:param failsafe: 如果定义了,则传递给 :meth:`~AbstractDCS.write_failsafe` 的字典。
:returns: 如果 `leader` 密钥(或会话)成功更新,则返回 `True`。
"""
if TYPE_CHECKING: # pragma: no cover
assert isinstance(cluster.leader, Leader)
# 调用 _update_leader 方法更新 leader 密钥(或会话)的 TTL
ret = self._update_leader(cluster.leader)
# 如果 ret 为真,并且 last_lsn 不为空,则构建一个新的字典 status
if ret and last_lsn:
status: Dict[str, Any] = {self._OPTIME: last_lsn, 'slots': slots or None,
'retain_slots': self._build_retain_slots(cluster, slots)}
self.write_status(status)
# 如果 ret 为真,并且 failsafe 参数不为空,则调用 write_failsafe 方法更新故障安全信息
if ret and failsafe is not None:
self.write_failsafe(failsafe)
return ret
作用:
update_leader
函数的具体作用是更新集群中的 leader
密钥(或会话)的 TTL,并根据需要更新集群的状态信息和故障安全信息。具体来说:
- 更新 leader 密钥:
- 调用
_update_leader
方法更新leader
密钥(或会话)的 TTL。
- 调用
- 更新状态信息:
- 如果提供了
last_lsn
,则构建一个新的状态字典,并使用write_status
方法更新状态信息。状态信息包括最新的optime
、永久槽的confirmed_flush_lsn
以及需要保留的槽列表。
- 如果提供了
- 更新故障安全信息:
- 如果提供了
failsafe
参数,则调用write_failsafe
方法更新故障安全信息。
- 如果提供了
1.4.1.3.28 attempt_to_acquire_leader()
- 定义了一个抽象方法
attempt_to_acquire_leader
,该方法是一个抽象基类(Abstract Base Class, ABC)的一部分,意味着子类必须实现此方法。方法没有参数,并返回一个布尔值。
@abc.abstractmethod
def attempt_to_acquire_leader(self) -> bool:
"""尝试获取领导者的锁。
.. note::
此方法应该创建键 ``/leader``,其值为 :attr:`~AbstractDCS._name`。
必须原子地创建该键。如果该键已经存在,则不应覆盖它,并且必须返回 `False`。
如果由于 DCS 不可访问或无法处理请求(希望是暂时的)而导致键创建失败,则应抛出 :exc:`DCSError` 异常。
:returns: 如果键成功创建,则返回 `True`。
"""
作用:
attempt_to_acquire_leader
方法是一个抽象方法,它的具体作用是在分布式一致性系统(DCS)中尝试获取领导者的锁,并返回一个布尔值表示操作是否成功。具体来说:
- 尝试获取领导者的锁:
- 该方法应当在具体的子类中实现,以尝试创建 DCS 中的
/leader
键。 - 键的值应该是当前节点的标识(
_name
),表示当前节点正在尝试成为领导者。 - 创建操作应该是原子性的,这意味着要么完全成功,要么完全失败,中间不能有部分完成的状态。
- 如果键已经存在,表明已经有另一个节点成为了领导者,则不应覆盖现有的键,并返回
False
。 - 如果由于 DCS 不可访问或无法处理请求而导致键创建失败,则应该抛出
DCSError
异常,表明这是一个暂时性的错误。
- 该方法应当在具体的子类中实现,以尝试创建 DCS 中的
- 返回结果:
- 如果成功创建
/leader
键,则返回True
。否则,返回False
。
- 如果成功创建
1.4.1.3.29 set_failover_value()
-
定义了一个抽象方法
set_failover_value
,该方法是一个抽象基类(Abstract Base Class, ABC)的一部分,意味着子类必须实现此方法。方法接受两个参数:value
: 类型为str
的参数,表示要设置的值。version
: 可选参数,类型为Any
,用于有条件地更新键/对象。
方法返回一个布尔值,表示是否成功提交到 DCS(Distributed Consistency System)。
@abc.abstractmethod
def set_failover_value(self, value: str, version: Optional[Any] = None) -> bool:
"""创建或更新 `/failover` 键。
:param value: 要设置的值。
:param version: 用于有条件地更新键/对象。
:returns: 如果成功提交到 DCS,则返回 `True`。
"""
作用:
set_failover_value
方法是一个抽象方法,它的具体作用是在分布式一致性系统(DCS)中创建或更新 /failover
键,并返回一个布尔值表示操作是否成功。具体来说:
- 创建或更新
/failover
键:- 该方法应当在具体的子类中实现,以创建或更新 DCS 中的
/failover
键。 value
参数指定要设置的值,通常是与故障转移相关的数据。version
参数允许有条件地更新键/对象,这对于避免竞态条件和实现原子更新非常有用。
- 该方法应当在具体的子类中实现,以创建或更新 DCS 中的
- 返回结果:
- 如果成功提交到 DCS,则返回
True
。否则,返回False
。
- 如果成功提交到 DCS,则返回
1.4.1.3.30 manual_failover()
-
定义了一个名为
manual_failover
的实例方法,该方法接受以下参数:leader
: 可选的字符串类型参数,表示要设置为领导者的值。candidate
: 可选的字符串类型参数,表示要设置为候选成员的值。scheduled_at
: 可选的datetime.datetime
类型参数,表示计划的时间,会被转换为 ISO 日期格式。version
: 可选的参数,类型为Any
,用于有条件地更新键/对象。
方法返回一个布尔值,表示是否成功提交到 DCS。
def manual_failover(self, leader: Optional[str], candidate: Optional[str],
scheduled_at: Optional[datetime.datetime] = None, version: Optional[Any] = None) -> bool:
"""准备包含给定值的字典,并在 DCS 中设置 `/failover` 键。
:param leader: 用于设置 `leader` 的值。
:param candidate: 用于设置 `member` 的值。
:param scheduled_at: 被转换为 ISO 日期格式的值,用于 `scheduled_at`。
:param version: 用于有条件地更新键/对象。
:returns: 如果成功提交到 DCS,则返回 `True`。
"""
# 创建一个空字典 failover_value,用于存储故障转移所需的信息
failover_value = {}
if leader:
failover_value['leader'] = leader
if candidate:
failover_value['member'] = candidate
if scheduled_at:
failover_value['scheduled_at'] = scheduled_at.isoformat()
return self.set_failover_value(json.dumps(failover_value, separators=(',', ':')), version)
作用:
manual_failover
方法的具体作用是在分布式一致性系统(DCS)中手动触发故障转移,并返回一个布尔值表示操作是否成功。具体来说:
- 构建故障转移信息:
- 根据提供的参数
leader
、candidate
和scheduled_at
构建一个字典failover_value
,该字典包含故障转移所需的信息。
- 根据提供的参数
- 序列化故障转移信息:
- 使用
json.dumps
方法将构建好的字典序列化为 JSON 字符串。
- 使用
- 设置
/failover
键:- 调用
set_failover_value
方法来设置 DCS 中的/failover
键,并传递序列化的故障转移信息及版本信息(如果有的话)。
- 调用
- 返回结果:
- 返回
set_failover_value
方法的结果,即如果成功提交到 DCS,则返回True
。否则返回False
。
- 返回
1.4.1.3.31 set_config_value()
-
定义了一个抽象方法
set_config_value
,该方法是一个抽象基类(Abstract Base Class, ABC)的一部分,意味着子类必须实现此方法。方法接受两个参数:value
: 类型为str
的参数,表示要设置的新值。version
: 可选参数,类型为Any
,用于有条件地更新键/对象。
方法返回一个布尔值,表示是否成功提交到 DCS(Distributed Consistency System)。
@abc.abstractmethod
def set_config_value(self, value: str, version: Optional[Any] = None) -> bool:
"""创建或更新 DCS 中的 `/config` 键。
:param value: 要在 `config` 键中设置的新值。
:param version: 用于有条件地更新键/对象。
:returns: 如果成功提交到 DCS,则返回 `True`。
"""
作用:
set_config_value
方法是一个抽象方法,它的具体作用是在分布式一致性系统(DCS)中创建或更新 /config
键,并返回一个布尔值表示操作是否成功。具体来说:
- 创建或更新
/config
键:- 该方法应当在具体的子类中实现,以创建或更新 DCS 中的
/config
键。 value
参数指定要设置的新值。version
参数允许有条件地更新键/对象,这对于避免竞态条件和实现原子更新非常有用。
- 该方法应当在具体的子类中实现,以创建或更新 DCS 中的
- 返回结果:
- 如果成功提交到 DCS,则返回
True
。否则,返回False
。
- 如果成功提交到 DCS,则返回
1.4.1.3.32 touch_member()
-
定义了一个抽象方法
touch_member
,该方法是一个抽象基类(Abstract Base Class, ABC)的一部分,意味着子类必须实现此方法。方法接受一个参数:data
: 类型为Dict[str, Any]
的参数,表示关于实例的信息(包括连接字符串等)。
方法返回一个布尔值,表示是否成功提交到 DCS(Distributed Consistency System)。
@abc.abstractmethod
def touch_member(self, data: Dict[str, Any]) -> bool:
"""在 DCS 中更新成员键。
.. note::
此方法应该在给定的 DCS 中创建或更新键名由 ``/members/`` 加上 :attr:`~AbstractDCS._name` 构成,并且键的值为 *data*。
:param data: 关于实例的信息(包括连接字符串)。
:returns: 如果成功提交到 DCS,则返回 `True`。
"""
作用:
touch_member
方法是一个抽象方法,它的具体作用是在分布式一致性系统(DCS)中创建或更新成员信息,并返回一个布尔值表示操作是否成功。具体来说:
- 创建或更新成员键:
- 该方法应当在具体的子类中实现,以创建或更新 DCS 中的成员信息。
- 成员键的名字是由
"/members/"
加上当前节点的名称(_name
属性)构成的。 - 键的值为提供的
data
字典,其中包含关于实例的信息,例如连接字符串等。
- 返回结果:
- 如果成功提交到 DCS,则返回
True
。否则,返回False
。
- 如果成功提交到 DCS,则返回
1.4.1.3.33 take_leader()
- 定义了一个抽象方法
take_leader
,该方法是一个抽象基类(Abstract Base Class, ABC)的一部分,意味着子类必须实现此方法。方法没有参数,并返回一个布尔值,表示是否成功提交到 DCS(Distributed Consistency System)。
@abc.abstractmethod
def take_leader(self) -> bool:
"""在 DCS 中建立一个新的领导者。
.. note::
此方法应该创建领导者键,键值为 :attr:`~AbstractDCS._name`,并且键的生存时间(TTL)为 :attr:`~AbstractDCS.ttl`。
由于它可能仅在初始集群引导时被调用,因此它可以不管现有情况创建此键,必要时覆盖键。
:returns: 如果成功提交到 DCS,则返回 `True`。
"""
作用:
take_leader
方法是一个抽象方法,它的具体作用是在分布式一致性系统(DCS)中建立一个新的领导者,并返回一个布尔值表示操作是否成功。具体来说:
- 建立新的领导者:
- 该方法应当在具体的子类中实现,以在 DCS 中建立一个新的领导者。
- 领导者键的值应该是当前节点的名称(
_name
属性)。 - 领导者键的生存时间(TTL)应该是
ttl
属性所定义的时间长度。 - 由于此方法可能仅在初始集群引导时被调用,因此即使键已存在,也应覆盖它以确保当前节点成为领导者。
- 返回结果:
- 如果成功提交到 DCS,则返回
True
。否则,返回False
。
- 如果成功提交到 DCS,则返回
1.4.1.3.34 initialize()
-
定义了一个抽象方法
initialize
,该方法是一个抽象基类(Abstract Base Class, ABC)的一部分,意味着子类必须实现此方法。方法接受两个参数:create_new
: 类型为bool
的参数,默认值为True
,表示如果键应该不存在并且需要创建新键的话。sysid
: 类型为str
的参数,默认值为空字符串,表示 PostgreSQL 集群系统标识符,如果指定了该参数,则将其写入键中。
方法返回一个布尔值,表示是否成功创建了初始化键。
@abc.abstractmethod
def initialize(self, create_new: bool = True, sysid: str = "") -> bool:
"""争夺集群初始化。
此方法应该原子地创建 `initialize` 键并返回 `True`,
否则应该返回 `False`。
:param create_new: 如果键应该已经存在(在设置系统标识符的情况下),则为 `False`。
:param sysid: 如果指定了 PostgreSQL 集群系统标识符,则将其写入键中。
:returns: 如果键成功创建,则返回 `True`。
"""
作用:
initialize
方法是一个抽象方法,它的具体作用是在分布式一致性系统(DCS)中进行集群初始化,并返回一个布尔值表示操作是否成功。具体来说:
- 争夺集群初始化:
- 该方法应当在具体的子类中实现,以在 DCS 中原子地创建
initialize
键。 - 如果
create_new
参数为True
,则表示键不应该已经存在,此时方法应尝试创建该键。 - 如果
create_new
参数为False
,则表示键应该已经存在,此时方法不应创建键,而是用于设置系统标识符sysid
。 - 如果
sysid
参数被指定,则将其值写入initialize
键中。
- 该方法应当在具体的子类中实现,以在 DCS 中原子地创建
- 返回结果:
- 如果成功创建
initialize
键,则返回True
。否则,返回False
。
- 如果成功创建
1.4.1.3.35 _delete_leader()
- 定义了一个抽象方法
_delete_leader
,该方法属于某个类的一部分,假设这个类是一个抽象基类(Abstract Base Class,简称 ABC)。方法接收一个参数leader
,类型为Leader
,返回类型为bool
。
@abc.abstractmethod
def _delete_leader(self, leader: Leader) -> bool:
"""从 DCS 中移除 leader 键。
该方法应该在当前实例是 leader 时移除 leader 键。
:param leader: 包含有关 leader 信息的 :class:`Leader` 对象。
:returns: 如果成功提交到 DCS,则返回 ``True``。
"""
作用:
_delete_leader
方法的具体作用是在分布式协调系统(Distributed Coordination System,简称 DCS)中删除 leader 键。这个方法的设计目的是为了实现一个通用的接口,用于在当前实例成为 leader 之后能够从 DCS 中删除代表 leader 的键值对。这样做的目的是为了在 leader 退出或者不再担任 leader 角色时,能够及时更新 DCS 中的状态信息,确保集群的一致性。
1.4.1.3.36 delete_leader()
-
定义了一个抽象方法
_delete_leader
,该方法是一个抽象基类(Abstract Base Class, ABC)的一部分,意味着子类必须实现此方法。方法接受一个参数:leader
: 类型为Leader
的对象,包含有关领导者的详细信息。
方法返回一个布尔值,表示是否成功提交到 DCS(Distributed Consistency System)。
def delete_leader(self, leader: Optional[Leader], last_lsn: Optional[int] = None) -> bool:
"""从 DCS 中移除领导者键。
此方法应该移除领导者键,如果当前实例是领导者。
:param leader: 包含领导者信息的 :class:`Leader` 对象。
:returns: 如果成功提交到 DCS,则返回 `True`。
"""
if last_lsn:
self.write_status({self._OPTIME: last_lsn})
return bool(leader) and self._delete_leader(leader)
作用:
_delete_leader
方法是一个抽象方法,它的具体作用是在分布式一致性系统(DCS)中删除领导者键,并返回一个布尔值表示操作是否成功。具体来说:
- 移除领导者键:
- 该方法应当在具体的子类中实现,以移除 DCS 中的领导者键。
- 如果当前实例是领导者,则应该移除领导者键。
leader
参数是一个Leader
类型的对象,包含了关于领导者的信息,如领导者的名字或其他标识信息。
- 返回结果:
- 如果成功移除领导者键,并且成功提交到 DCS,则返回
True
。否则,返回False
。
- 如果成功移除领导者键,并且成功提交到 DCS,则返回
1.4.1.3.37 cancel_initialization()
- 定义了一个抽象方法
cancel_initialization
,该方法是一个抽象基类(Abstract Base Class, ABC)的一部分,意味着子类必须实现此方法。方法没有参数,并返回一个布尔值,表示是否成功提交到 DCS(Distributed Consistency System)。
@abc.abstractmethod
def cancel_initialization(self) -> bool:
"""移除集群的 `initialize` 键。
:returns: 如果成功提交到 DCS,则返回 `True`。
"""
作用:
cancel_initialization
方法是一个抽象方法,它的具体作用是在分布式一致性系统(DCS)中取消集群初始化,并返回一个布尔值表示操作是否成功。具体来说:
- 移除
initialize
键:- 该方法应当在具体的子类中实现,以移除 DCS 中的
initialize
键。 initialize
键通常用于记录集群是否已被初始化,移除该键意味着取消集群的初始化操作。
- 该方法应当在具体的子类中实现,以移除 DCS 中的
- 返回结果:
- 如果成功移除
initialize
键,并且成功提交到 DCS,则返回True
。否则,返回False
。
- 如果成功移除
1.4.1.3.38 delete_cluster()
- 定义了一个抽象方法
delete_cluster
,该方法是一个抽象基类(Abstract Base Class, ABC)的一部分,意味着子类必须实现此方法。方法没有参数,并返回一个布尔值,表示是否成功提交到 DCS(Distributed Consistency System)。
@abc.abstractmethod
def delete_cluster(self) -> bool:
"""从 DCS 中删除集群。
:returns: 如果成功提交到 DCS,则返回 `True`。
"""
作用:
delete_cluster
方法是一个抽象方法,它的具体作用是在分布式一致性系统(DCS)中删除与集群相关的所有数据,并返回一个布尔值表示操作是否成功。具体来说:
- 删除集群相关数据:
- 该方法应当在具体的子类中实现,以删除 DCS 中与特定集群相关的所有数据。
- 这可能涉及到删除多个键或路径,具体取决于 DCS 的设计和集群数据的组织方式。
- 返回结果:
- 如果成功删除集群数据,并且成功提交到 DCS,则返回
True
。否则,返回False
。
- 如果成功删除集群数据,并且成功提交到 DCS,则返回
1.4.1.3.39 sync_state()
-
定义了一个静态方法
sync_state
,该方法不依赖于类或实例的状态,因此使用@staticmethod
装饰器。方法接受三个参数:leader
: 类型为Optional[str]
的参数,表示管理/sync
键的领导者节点的名称。sync_standby
: 类型为Optional[Collection[str]]
的参数,表示当前已知的同步备用节点名称的集合。quorum
: 类型为Optional[int]
的参数,表示如果sync_standby
列表中的节点正在进行领导者选举,那么它应该至少看到quorum
数量的其他节点(来自sync_standby
+leader
列表)。
方法返回一个字典,该字典可以稍后序列化为 JSON 或直接保存到 DCS。
@staticmethod
def sync_state(leader: Optional[str], sync_standby: Optional[Collection[str]],
quorum: Optional[int]) -> Dict[str, Any]:
"""构建 `sync_state` 字典。
:param leader: 管理 `/sync` 键的领导者节点的名称。
:param sync_standby: 当前已知的同步备用节点名称的集合。
:param quorum: 如果 `sync_standby` 列表中的节点正在进行领导者选举,那么它应该至少看到 `quorum` 数量的其他节点(来自 `sync_standby` + `leader` 列表)。
:returns: 一个稍后可以序列化为 JSON 或直接保存到 DCS 的字典。
"""
return {'leader': leader, 'quorum': quorum,
'sync_standby': ','.join(sorted(sync_standby)) if sync_standby else None}
作用:
sync_state
方法是一个静态方法,它的具体作用是根据提供的参数构建一个 sync_state
字典,该字典描述了集群中的同步状态信息。具体来说:
- 构建字典:
- 方法接收
leader
、sync_standby
和quorum
参数,并根据这些参数构建一个字典。 - 如果
sync_standby
参数不是None
,则将其转换为一个排序后的逗号分隔字符串,然后作为sync_standby
字典项的值。 - 如果
sync_standby
参数为None
,则sync_standby
字典项的值也为None
。
- 方法接收
- 返回字典:
- 返回的字典包含
leader
、quorum
和sync_standby
三个键。 - 这个字典可以被序列化为 JSON 格式或者直接存储到 DCS 中,用于记录集群的同步状态。
- 返回的字典包含
1.4.1.3.40 write_sync_state()
-
定义了一个名为
write_sync_state
的方法,该方法属于某个类的一个实例方法,因为它使用了self
参数。方法接受四个参数:leader
: 类型为Optional[str]
的参数,表示管理/sync
键的领导者节点的名称。sync_standby
: 类型为Optional[Collection[str]]
的参数,表示当前已知的同步备用节点名称的集合。quorum
: 类型为Optional[int]
的参数,表示如果sync_standby
列表中的节点正在进行领导者选举,那么它应该至少看到quorum
数量的其他节点(来自sync_standby
+leader
列表)。version
: 类型为Optional[Any]
的参数,默认值为None
,表示用于条件性更新键/对象的版本号。
方法返回类型为
Optional[SyncState]
,即返回一个SyncState
对象或None
。
def write_sync_state(self, leader: Optional[str], sync_standby: Optional[Collection[str]],
quorum: Optional[int], version: Optional[Any] = None) -> Optional[SyncState]:
"""将新的同步状态写入 DCS。
调用 :meth:`~AbstractDCS.sync_state` 来构建一个字典,然后调用特定于 DCS 的 :meth:`~AbstractDCS.set_sync_state_value` 方法。
:param leader: 管理 `/sync` 键的领导者节点的名称。
:param sync_standby: 当前已知的同步备用节点名称的集合。
:param version: 用于条件性更新键/对象的版本号。
:param quorum: 如果 `sync_standby` 列表中的节点正在进行领导者选举,那么它应该至少看到 `quorum` 数量的其他节点(来自 `sync_standby` + `leader` 列表)。
:returns: 新的 :class:`SyncState` 对象或 `None`。
"""
# 调用 self.sync_state 方法(假设是该类的一个方法)来构建一个同步状态字典
sync_value = self.sync_state(leader, sync_standby, quorum)
# 将构建好的同步状态字典序列化为 JSON 格式的字符串,并调用 self.set_sync_state_value 方法来将该字符串写入到 DCS 中。separators 参数用于减少 JSON 输出的空白字符,使字符串更紧凑。version 参数用于条件性更新
ret = self.set_sync_state_value(json.dumps(sync_value, separators=(',', ':')), version)
if not isinstance(ret, bool):
return SyncState.from_node(ret, sync_value)
return None
作用:
write_sync_state
方法是一个实例方法,它的具体作用是在分布式一致性系统(DCS)中写入新的同步状态,并返回一个 SyncState
对象或 None
表示操作的结果。具体来说:
- 构建同步状态字典:
- 该方法首先调用
sync_state
方法来构建一个同步状态字典,该字典包含了领导者名称、同步备用节点列表和法定人数信息。
- 该方法首先调用
- 序列化同步状态字典:
- 将构建好的同步状态字典序列化为 JSON 格式的字符串。
- 写入 DCS:
- 调用
set_sync_state_value
方法将序列化后的字符串写入到 DCS 中。version
参数允许进行条件性更新,即只有在键/对象的版本匹配时才进行更新。
- 调用
- 返回结果:
- 如果
set_sync_state_value
方法返回的不是一个布尔值,这意味着返回的是一个节点的状态,使用SyncState.from_node
方法来创建一个新的SyncState
对象,并返回该对象。 - 如果返回的是布尔值,则返回
None
,表示没有创建新的SyncState
对象。
- 如果
1.4.1.3.41 set_history_value()
- 在 DCS 中设置
history
的值。 :param value:history
键/对象的新值。 :returns: 如果成功提交到 DCS,则返回True
。在 DCS 中设置history
的值。 :param value:history
键/对象的新值。 :returns: 如果成功提交到 DCS,则返回True
。
@abc.abstractmethod
def set_history_value(self, value: str) -> bool:
"""在 DCS 中设置 `history` 的值。
:param value: `history` 键/对象的新值。
:returns: 如果成功提交到 DCS,则返回 `True`。
"""
作用:
set_history_value
方法是一个抽象方法,它的具体作用是在分布式一致性系统(DCS)中设置 history
键的值,并返回一个布尔值表示操作是否成功。具体来说:
- 设置历史值:
- 该方法应当在具体的子类中实现,以在 DCS 中设置
history
键的值。 value
参数是一个字符串,代表history
键的新值。
- 该方法应当在具体的子类中实现,以在 DCS 中设置
- 返回结果:
- 如果成功设置
history
键的值,并且成功提交到 DCS,则返回True
。否则,返回False
。
- 如果成功设置
1.4.1.3.42 set_sync_state_value()
-
定义了一个抽象方法
set_sync_state_value
,该方法是一个抽象基类(Abstract Base Class, ABC)的一部分,意味着子类必须实现此方法。方法接受两个参数:value
: 类型为str
的参数,表示/sync
键的新值。version
: 类型为Optional[Any]
的参数,默认值为None
,表示用于条件性更新键/对象的版本号。
方法返回类型为
Union[Any, bool]
,即返回任意类型的版本号或布尔值False
。
@abc.abstractmethod
def set_sync_state_value(self, value: str, version: Optional[Any] = None) -> Union[Any, bool]:
"""在 DCS 中设置同步状态。
:param value: `/sync` 键的新值。
:param version: 用于条件性更新键/对象的版本号。
:returns: 新对象的 *version* 或者在错误情况下返回 `False`。
"""
作用:
set_sync_state_value
方法是一个抽象方法,它的具体作用是在分布式一致性系统(DCS)中设置同步状态,并返回新对象的版本号或布尔值 False
表示操作是否成功。具体来说:
- 设置同步状态:
- 该方法应当在具体的子类中实现,以在 DCS 中设置
/sync
键的值。 value
参数是一个字符串,代表/sync
键的新值。version
参数是一个可选参数,用于条件性更新。如果提供了版本号,那么更新操作仅在键/对象的当前版本匹配时才会执行。
- 该方法应当在具体的子类中实现,以在 DCS 中设置
- 返回结果:
- 如果成功设置
/sync
键的值,并且成功提交到 DCS,则返回新对象的版本号。 - 如果设置失败,例如由于版本不匹配或其他错误,则返回
False
。
- 如果成功设置
1.4.1.3.43 delete_sync_state()
-
定义了一个抽象方法
delete_sync_state
,该方法是一个抽象基类(Abstract Base Class, ABC)的一部分,意味着子类必须实现此方法。方法接受一个参数:version
: 类型为Optional[Any]
的参数,默认值为None
,表示用于条件性删除键/对象的版本号。
方法返回一个布尔值,表示删除操作是否成功。
@abc.abstractmethod
def delete_sync_state(self, version: Optional[Any] = None) -> bool:
"""从 DCS 中删除同步状态。
:param version: 用于条件性删除键/对象的版本号。
:returns: 如果删除成功,则返回 `True`。
"""
作用:
delete_sync_state
方法是一个抽象方法,它的具体作用是从分布式一致性系统(DCS)中删除同步状态,并返回一个布尔值表示操作是否成功。具体来说:
- 删除同步状态:
- 该方法应当在具体的子类中实现,以从 DCS 中删除与同步状态相关的键或对象。
version
参数是一个可选参数,用于条件性删除。如果提供了版本号,那么删除操作仅在键/对象的当前版本匹配时才会执行。
- 返回结果:
- 如果成功删除同步状态键/对象,并且成功提交到 DCS,则返回
True
。 - 如果删除失败,例如由于版本不匹配或其他错误,则返回
False
。
- 如果成功删除同步状态键/对象,并且成功提交到 DCS,则返回
1.4.1.3.44 watch()
-
定义了一个名为
watch
的方法,该方法属于某个类的一个实例方法,因为它使用了self
参数。方法接受两个参数:leader_version
: 类型为Optional[Any]
的参数,表示领导者键的版本。timeout
: 类型为float
的参数,表示等待超时的时间(秒)。
方法返回一个布尔值,表示是否需要重新调度下一个高可用(HA)周期的运行。
def watch(self, leader_version: Optional[Any], timeout: float) -> bool:
"""如果当前节点是领导者,则休眠;否则,监视领导者键的变化,等待给定的 *timeout* 时间。
:param leader_version: 领导者键的版本。
:param timeout: 超时时间(秒)。
:returns: 如果为 `True`,则重新调度下一个 HA 周期的运行。
"""
_ = leader_version
self.event.wait(timeout)
return self.event.is_set()
作用:
watch
方法是一个实例方法,它的具体作用是在分布式系统中监视领导者键的状态变化,并决定是否需要重新调度下一个高可用(HA)周期的运行。具体来说:
- 监视领导者状态:
- 如果当前节点是领导者,则该方法可能会让当前线程休眠,避免不必要的计算或操作。
- 如果当前节点不是领导者,则监视领导者键的状态变化,等待一个指定的时间(
timeout
)。
- 返回结果:
- 如果在指定时间内领导者键的状态发生变化(即
self.event
被设置),则返回True
,这表明需要重新调度下一个 HA 周期的运行。 - 如果在指定时间内没有变化,则返回
False
。
- 如果在指定时间内领导者键的状态发生变化(即
1.4.1.4 类:Cluster
-
定义了一个名为
Cluster
的类,继承自NamedTuple
。这个类表示一个 PostgreSQL 或 MPP 集群的不可变对象。Cluster
类包含了多个字段,这些字段定义了集群的各种状态信息 -
Cluster
类是一个不可变对象,用于表示一个 PostgreSQL 或 MPP 集群的状态。它包含了一系列关于集群的信息,如初始化状态、配置、领导者、成员列表、故障转移信息、同步状态、历史记录、故障安全信息以及 MPP 集群的工作节点信息。通过这个类,可以方便地管理和表示集群的各种状态,并且由于它是不可变的,所以在并发环境下更容易处理。 -
此外,通过使用
NamedTuple
,可以利用 Python 的序列化机制方便地保存和恢复集群状态,同时也提供了更好的类型检查和调试支持。
class Cluster(NamedTuple('Cluster',
[('initialize', Optional[str]),
('config', Optional[ClusterConfig]),
('leader', Optional[Leader]),
('status', Status),
('members', List[Member]),
('failover', Optional[Failover]),
('sync', SyncState),
('history', Optional[TimelineHistory]),
('failsafe', Optional[Dict[str, str]]),
('workers', Dict[int, 'Cluster'])])):
"""不可变对象(命名元组),表示 PostgreSQL 或 MPP 集群。
.. note::
我们在这里使用旧式属性声明,因为否则无法重写 `__new__` 方法。
如果没有它,*workers* 默认总是会得到相同的 :class:`dict` 对象,这可能会被修改。
由以下字段组成:
:ivar initialize: 显示该集群是否在 DC 中存储了初始化密钥。
:ivar config: 全局动态配置,指向 `ClusterConfig` 对象的引用。
:ivar leader: :class:`Leader` 对象,表示集群的当前领导者。
:ivar status: :class:`Status` 对象,表示 `/status` 键。
:ivar members: :class:`Member` 对象的列表,包括所有 PostgreSQL 集群成员(包括领导者)。
:ivar failover: 指向 :class:`Failover` 对象的引用。
:ivar sync: 指向 :class:`SyncState` 对象的引用,最后观察到的同步复制状态。
:ivar history: 指向 `TimelineHistory` 对象的引用。
:ivar failsafe: 保护拓扑。只有当节点的名称在此列表中时,才能成为领导者。
:ivar workers: MPP 集群的工作节点字典(可选)。每个键表示组,相应的值是一个 :class:`Cluster` 实例。
"""
1.4.1.4.1 empty()
- 定义了一个名为
empty
的静态方法,该方法返回类型为Cluster
。
@staticmethod
def empty() -> 'Cluster':
"""生成一个空的 :class:`Cluster` 实例。"""
return Cluster(None, None, None, Status.empty(), [], None, SyncState.empty(), None, None, {})
作用:
这个函数的作用是生成一个没有任何具体信息填充的 Cluster
实例。具体来说:
- 初始化
Cluster
实例:- 创建一个
Cluster
实例,并将其所有字段设置为默认值或空值。
- 创建一个
- 返回实例:
- 返回创建的空
Cluster
实例。
- 返回创建的空
1.4.1.4.2 __new__
()
- 定义了一个名为
empty
的静态方法,该方法返回类型为Cluster
。
def __new__(cls, *args: Any, **kwargs: Any):
"""使 workers 参数成为可选,并设置为一个空字典对象。"""
# 如果位置参数的数量小于类字段的数量,并且关键字参数中没有 workers
if len(args) < len(cls._fields) and 'workers' not in kwargs:
kwargs['workers'] = {}
return super(Cluster, cls).__new__(cls, *args, **kwargs)
作用:
这个方法的作用是在创建类的新实例时,处理 workers
参数的缺失情况。具体来说:
- 检查参数:首先检查传入的位置参数
args
的数量是否少于类的字段数量,并且关键字参数kwargs
中没有workers
键。 - 设置默认值:如果上述条件成立,则在
kwargs
中为workers
设置一个默认值为空字典{}
。 - 调用父类构造函数:无论是否设置了
workers
的默认值,最终都会调用父类的__new__
方法来创建新的实例,并将处理过的args
和kwargs
传递给父类构造函数。
1.4.1.4.3 get_member()
- 定义了一个名为
get_member
的实例方法,该方法接受两个参数:member_name
和fallback_to_leader
,并返回Member
、Leader
类型的实例或None
。
def get_member(self, member_name: str, fallback_to_leader: bool = True) -> Union[Member, Leader, None]:
"""通过名称获取 :class:`Member` 对象或 :class:`Leader`。
:param member_name: 要检索的成员名称。
:param fallback_to_leader: 如果为 ``True``,则在找不到成员时返回 :class:`Leader`。
:returns: 如果找到则返回 :class:`Member` 或 :class:`Leader` 对象。
"""
return next((m for m in self.members if m.name == member_name),
self.leader if fallback_to_leader else None)
作用:
这个方法的作用是通过成员名称来获取一个 Member
对象。如果找不到对应的成员,并且 fallback_to_leader
参数为 True
,则返回当前的 Leader
对象。具体来说:
- 查找成员:遍历
self.members
列表,寻找第一个名称与member_name
相匹配的Member
对象。 - 提供备选项:如果没有找到匹配的成员,并且
fallback_to_leader
参数为True
,则返回当前的Leader
对象;否则返回None
。
1.4.1.4.4 is_empty()
- 定义了一个名为
is_empty
的方法,该方法属于某个类的一个实例方法,因为它使用了self
参数。这个方法没有额外的输入参数。
def is_empty(self):
"""验证当前 :class:`Cluster` 实例的所有属性是否为空。
:returns: 如果当前 :class:`Cluster` 的所有属性都未填充,则返回 `True`。
"""
return all((self.initialize is None, self.config is None, self.leader is None, self.status.is_empty(),
self.members == [], self.failover is None, self.sync.version is None,
self.history is None, self.failsafe is None, self.workers == {}))
这一行代码检查了 Cluster
类实例的多个属性是否为空或未初始化。具体来说:
self.initialize is None
: 检查initialize
属性是否为None
。self.config is None
: 检查config
属性是否为None
。self.leader is None
: 检查leader
属性是否为None
。self.status.is_empty()
: 调用status
属性上的is_empty
方法,假设status
是另一个对象,也有is_empty
方法来判断其是否为空。self.members == []
: 检查members
属性是否为空列表。self.failover is None
: 检查failover
属性是否为None
。self.sync.version is None
: 检查sync
属性下的version
是否为None
。self.history is None
: 检查history
属性是否为None
。self.failsafe is None
: 检查failsafe
属性是否为None
。self.workers == {}
: 检查workers
属性是否为空字典。
all
函数会返回 True
,只有在所有传入的布尔表达式都为 True
的时候。如果任何一个条件不满足(即任何一个属性不为空或已初始化),则 all
函数返回 False
。
作用:
is_empty
方法是一个实例方法,它的具体作用是验证当前 Cluster
实例的所有属性是否都没有被填充或初始化。具体来说:
- 检查属性状态:
- 方法遍历了
Cluster
类实例中的多个属性,包括但不限于initialize
,config
,leader
,status
,members
,failover
,sync.version
,history
,failsafe
,workers
。 - 对于每个属性,检查它们是否为空或未初始化。
- 方法遍历了
- 返回结果:
- 如果所有属性都是未填充或未初始化的,则返回
True
。 - 如果有任何一个属性已被填充或初始化,则返回
False
。
- 如果所有属性都是未填充或未初始化的,则返回
1.4.1.4.5 __len__()
- 定义了一个名为
__len__
的特殊方法(魔法方法),该方法属于某个类的一个实例方法,因为它使用了self
参数。这个方法没有额外的输入参数,并且返回一个整数。
def __len__(self) -> int:
"""实现 `len` 函数的功能。
.. note::
此魔法方法有助于评估 `Cluster` 实例的“空”状态。例如:
>>> cluster = Cluster.empty()
>>> len(cluster)
0
>>> assert bool(cluster) is False
>>> status = Status(0, None, [])
>>> cluster = Cluster(None, None, None, status, [1, 2, 3], None, SyncState.empty(), None, None, {})
>>> len(cluster)
1
>>> assert bool(cluster) is True
这使得编写 `if cluster` 比较长的语句更容易。
"""
return int(not self.is_empty())
作用:
__len__
方法是一个实例方法,它的具体作用是实现对 Cluster
实例的长度评估功能,以便能够在布尔上下文中方便地判断 Cluster
实例是否为空。具体来说:
- 评估实例状态:
- 方法通过调用
is_empty
方法来评估当前Cluster
实例是否为空。 - 如果
is_empty
返回True
,表示实例为空,__len__
返回0
。 - 如果
is_empty
返回False
,表示实例非空,__len__
返回1
。
- 方法通过调用
- 返回结果:
- 返回的结果可以用来方便地判断实例是否为空。例如,在布尔上下文中,如果
__len__
返回0
,则实例在布尔上下文中被评估为False
;如果__len__
返回1
,则实例在布尔上下文中被评估为True
。
- 返回的结果可以用来方便地判断实例是否为空。例如,在布尔上下文中,如果
1.4.1.4.6 is_unlocked()
- 定义了一个名为
is_unlocked
的方法,该方法是一个类的实例方法,因为使用了self
参数。该方法没有其他参数,并且返回一个布尔值。
def is_unlocked(self) -> bool:
"""检查集群是否没有领导者。
:returns: 如果领导者名称未定义,则返回 `True`。
"""
return not self.leader_name
作用:
is_unlocked
方法是一个实例方法,它的具体作用是检查集群是否没有领导者。具体来说:
- 检查领导者状态:
- 方法通过检查
self.leader_name
是否为空或者未定义来判断集群是否有领导者。 - 如果
self.leader_name
为空或者未定义,则认为集群没有领导者。
- 方法通过检查
- 返回结果:
- 如果集群没有领导者,则返回
True
。 - 如果集群有领导者,则返回
False
。
- 如果集群没有领导者,则返回
1.4.1.4.7 has_member()
- 定义了一个名为
has_member
的方法,该方法是一个类的实例方法,因为使用了self
参数。该方法接受一个参数member_name
,类型为str
,并且返回一个布尔值。
def has_member(self, member_name: str) -> bool:
"""检查给定的成员名称是否存在于集群中。
:param member_name: 在 :attr:`~Cluster.members` 中查找的名称。
:returns: 如果找到成员名称,则返回 `True`。
"""
return any(m for m in self.members if m.name == member_name)
作用:
has_member
方法是一个实例方法,它的具体作用是检查给定的成员名称是否存在于集群的成员列表中。具体来说:
- 查找成员名称:
- 方法通过遍历
self.members
列表来查找是否存在一个成员的name
属性与给定的member_name
相等。 - 如果找到了至少一个这样的成员,则认为该成员存在于集群中。
- 方法通过遍历
- 返回结果:
- 如果找到至少一个匹配的成员名称,则返回
True
。 - 如果没有任何匹配的成员名称,则返回
False
。
- 如果找到至少一个匹配的成员名称,则返回
1.4.1.4.8 get_member()
-
定义了一个名为
get_member
的方法,该方法是一个类的实例方法,因为使用了self
参数。该方法接受两个参数:member_name
: 类型为str
,是要检索的成员的名称。fallback_to_leader
: 类型为bool
,默认值为True
,表示如果找不到成员,则返回Leader
对象。
方法返回类型为
Union[Member, Leader, None]
,即返回Member
类型的对象、Leader
类型的对象或者None
。
def get_member(self, member_name: str, fallback_to_leader: bool = True) -> Union[Member, Leader, None]:
"""通过名称获取 `Member` 对象或 `Leader` 对象。
:param member_name: 要检索的成员的名称。
:param fallback_to_leader: 如果为 `True`,则如果找不到成员,则返回 `Leader` 对象。
:returns: 如果找到,则返回 `Member` 对象或 `Leader` 对象。
"""
return next((m for m in self.members if m.name == member_name),
self.leader if fallback_to_leader else None)
作用:
get_member
方法是一个实例方法,它的具体作用是根据给定的成员名称来获取对应的 Member
对象,或者如果找不到成员且 fallback_to_leader
参数为 True
,则返回 Leader
对象。具体来说:
- 查找成员对象:
- 方法通过遍历
self.members
列表来查找是否存在一个成员的name
属性与给定的member_name
相等。 - 如果找到了至少一个这样的成员,则返回该成员对象。
- 方法通过遍历
- 返回默认值:
- 如果没有找到符合名称的成员,并且
fallback_to_leader
参数为True
,则返回self.leader
对象。 - 如果没有找到符合名称的成员,并且
fallback_to_leader
参数为False
,则返回None
。
- 如果没有找到符合名称的成员,并且
1.4.1.4.9 get_clone_member()
- 定义了一个名为
get_clone_member
的方法,该方法是一个类的实例方法,因为使用了self
参数。该方法接受一个参数exclude_name
,类型为str
,并且返回类型为Union[Member, Leader, None]
的对象,即返回Member
类型的对象、Leader
类型的对象或者None
。
def get_clone_member(self, exclude_name: str) -> Union[Member, Leader, None]:
"""获取要用于克隆源的成员或领导者对象。
:param exclude_name: 要排除的成员名称。
:returns: 从可用的正在运行并且配置为有效的克隆源(配置中有 `clonefrom` 标签)的成员中随机选择的候选成员。如果没有合适的成员,则使用当前领导者。
"""
exclude = [exclude_name] + ([self.leader.name] if self.leader else [])
# 使用列表推导式来创建一个候选成员列表 candidates
candidates = [m for m in self.members if m.clonefrom and m.is_running and m.name not in exclude]
return candidates[randint(0, len(candidates) - 1)] if candidates else self.leader
作用:
get_clone_member
方法是一个实例方法,它的具体作用是从集群中选择一个合适的成员作为克隆源。具体来说:
- 排除某些成员:
- 方法首先创建一个
exclude
列表,该列表包含了exclude_name
以及领导者的名字(如果领导者存在的话)。
- 方法首先创建一个
- 筛选候选成员:
- 方法遍历
self.members
列表中的每一个成员,并筛选出那些配置了clonefrom
标签、当前处于运行状态,并且名字不在exclude
列表中的成员。这些成员成为候选成员。
- 方法遍历
- 返回结果:
- 如果存在候选成员,则从候选成员中随机选择一个作为克隆源并返回。
- 如果没有候选成员,则返回当前领导者作为克隆源。
1.4.1.4.10 is_physical_slot()
- 定义了一个静态方法
is_physical_slot
,该方法不需要访问类或实例的任何状态,因此使用了@staticmethod
装饰器。该方法接受一个参数value
,类型为Union[Any, Dict[str, Any]]
(可以是任意类型或字典类型),并且返回一个布尔值。
@staticmethod
def is_physical_slot(value: Union[Any, Dict[str, Any]]) -> bool:
"""检查提供的配置是否为永久物理复制槽位。
:param value: 永久复制槽位的配置。
:returns: 如果 *value* 是物理复制槽位,则返回 `True`,否则返回 `False`。
"""
return not value \
or (isinstance(value, dict) and not Cluster.is_logical_slot(value)
and value.get('type', 'physical') == 'physical')
作用:
is_physical_slot
方法是一个静态方法,它的具体作用是检查给定的配置是否表示一个永久物理复制槽位。具体来说:
- 检查配置类型:
- 方法首先检查
value
是否为None
或者是假值,如果是,则认为是物理复制槽位。 - 如果
value
是字典类型,则进一步检查。
- 方法首先检查
- 排除逻辑复制槽位:
- 方法调用
Cluster.is_logical_slot(value)
来排除逻辑复制槽位。如果value
表示的是逻辑复制槽位,则返回False
。
- 方法调用
- 确认物理复制槽位:
- 如果
value
是字典类型,并且不是逻辑复制槽位,并且value
中type
字段的值为'physical'
,则认为这是一个物理复制槽位。
- 如果
- 返回结果:
- 如果满足上述条件之一,则返回
True
;否则返回False
。
- 如果满足上述条件之一,则返回
1.4.1.4.11 is_logical_slot()
- 定义了一个静态方法
is_logical_slot
,该方法不需要访问类或实例的任何状态,因此使用了@staticmethod
装饰器。该方法接受一个参数value
,类型为Union[Any, Dict[str, Any]]
(可以是任意类型或字典类型),并且返回一个布尔值。
@staticmethod
def is_logical_slot(value: Union[Any, Dict[str, Any]]) -> bool:
"""检查提供的配置是否为永久逻辑复制槽位。
:param value: 永久复制槽位的配置。
:returns: 如果 *value* 是逻辑复制槽位,则返回 `True`,否则返回 `False`。
"""
return isinstance(value, dict) \
and value.get('type', 'logical') == 'logical' \
and bool(value.get('database') and value.get('plugin'))
作用:
is_logical_slot
方法是一个静态方法,它的具体作用是检查给定的配置是否表示一个永久逻辑复制槽位。具体来说:
- 检查配置类型:
- 方法首先检查
value
是否为字典类型,如果不是字典类型,则返回False
。
- 方法首先检查
- 确认逻辑复制槽位类型:
- 方法检查
value
字典中type
字段的值是否为'logical'
,如果不是,则返回False
。
- 方法检查
- 验证必要字段的存在:
- 方法进一步检查
value
字典中是否同时存在database
和plugin
字段,并且这两个字段都有有效值。如果缺少任何一个字段或字段值无效,则返回False
。
- 方法进一步检查
- 返回结果:
- 如果以上所有条件都满足,则返回
True
;否则返回False
。
- 如果以上所有条件都满足,则返回
1.4.1.4.12 get_replication_slots()
-
定义了一个名为
get_replication_slots
的方法,该方法是一个类的实例方法,因为使用了self
参数。该方法接受四个参数:postgresql
: 类型为Postgresql
,指的是一个Postgresql
对象的引用。member
: 类型为Tags
,指的是实现了Tags
接口的对象的引用。role
: 类型为Optional[str]
,可选参数,表示节点的角色,如果不设置则从postgresql
中获取。show_error
: 类型为bool
,默认值为False
,表示是否显示错误报告。
方法返回类型为
Dict[str, Dict[str, Any]]
,即返回一个嵌套字典。
def get_replication_slots(self, postgresql: 'Postgresql', member: Tags, *,
role: Optional[str] = None, show_error: bool = False) -> Dict[str, Dict[str, Any]]:
"""在 DCS 中查找配置的槽位名称,报告发现的问题,并与永久槽位合并。
如果发生以下情况,则记录错误:
* 由于版本兼容性问题,任何逻辑槽位被禁用,并且 *show_error* 为 `True`。
:param postgresql: `Postgresql` 对象的引用。
:param member: 实现了 `Tags` 接口的对象的引用。
:param role: 节点的角色,如果未设置则从 *postgresql* 中获取。
:param show_error: 如果为 `True`,则报告发现的任何禁用的逻辑槽位或冲突的槽位名称错误。
:returns: 最终的槽位名称字典,在与永久槽位合并并执行合理性检查之后。
"""
# 这里初始化了两个变量 name 和 role
name = member.name if isinstance(member, Member) else postgresql.name
role = role or postgresql.role
# 调用 _get_members_slots 方法来获取成员槽位
slots: Dict[str, Dict[str, Any]] = self._get_members_slots(name, role,
member.nofailover, postgresql.can_advance_slots)
# 调用 _get_permanent_slots 方法来获取永久槽位
permanent_slots: Dict[str, Any] = self._get_permanent_slots(postgresql, member, role)
# 调用 _merge_permanent_slots 方法来合并槽位信息
disabled_permanent_logical_slots: List[str] = self._merge_permanent_slots(
slots, permanent_slots, name, role, postgresql.can_advance_slots)
# 如果存在被禁用的永久逻辑槽位,并且 show_error 为 True,则记录一条错误日志
if disabled_permanent_logical_slots and show_error:
logger.error("Permanent logical replication slots supported by Patroni only starting from PostgreSQL 11. "
"Following slots will not be created: %s.", disabled_permanent_logical_slots)
return slots
作用:
get_replication_slots
方法是一个实例方法,它的具体作用是获取并整合集群中配置的复制槽位信息。具体来说:
- 获取槽位信息:
- 方法首先从 DCS 中获取成员槽位信息,并存储在
slots
字典中。 - 方法接着获取永久槽位信息,并存储在
permanent_slots
字典中。
- 方法首先从 DCS 中获取成员槽位信息,并存储在
- 合并槽位信息:
- 方法通过
_merge_permanent_slots
方法将永久槽位信息合并进成员槽位信息中,并得到一个可能包含禁用的永久逻辑槽位名称的列表disabled_permanent_logical_slots
。
- 方法通过
- 错误处理:
- 如果存在禁用的永久逻辑槽位,并且
show_error
参数为True
,则记录一条错误日志,指出这些槽位由于版本兼容性问题无法创建。
- 如果存在禁用的永久逻辑槽位,并且
- 返回结果:
- 返回最终的槽位信息字典
slots
,这个字典包含了所有经过合并和检查后的槽位信息。
- 返回最终的槽位信息字典
1.4.1.4.13 _merge_permanent_slots()
-
定义了一个名为
_merge_permanent_slots
的私有方法,该方法是一个类的实例方法,因为使用了self
参数。该方法接受五个参数:slots
: 类型为Dict[str, Dict[str, Any]]
,包含已知的槽位名称及其属性。permanent_slots
: 类型为Dict[str, Any]
,包含槽位名称键和槽位信息值。name
: 类型为str
,表示当前节点的名称。role
: 类型为str
,表示节点的角色,可以是primary
,standby_leader
或replica
。can_advance_slots
: 类型为bool
,表示是否支持pg_replication_slot_advance()
函数。
方法返回类型为
List[str]
,即返回一个字符串列表。
def _merge_permanent_slots(self, slots: Dict[str, Dict[str, Any]], permanent_slots: Dict[str, Any],
name: str, role: str, can_advance_slots: bool) -> List[str]:
"""合并成员的复制 *slots* 和 *permanent_slots*。
对配置的永久槽位名称进行验证,跳过无效的名称。
将根据槽位的类型(`physical` 或 `logical`)以及节点名称更新 *slots*。
如果槽位值中没有存储属性,则假设类型为 `physical`。
:param slots: 包含现有属性的槽位名称(如果已知)。
:param name: 当前节点的名称。
:param role: 节点的角色 -- `primary`, `standby_leader` 或 `replica`。
:param permanent_slots: 包含槽位名称键和槽位信息值的字典。
:param can_advance_slots: 如果 `pg_replication_slot_advance()` 函数可用,则为 `True`,否则为 `False`。
:returns: 如果 PostgreSQL 版本小于 11,则返回禁用的永久逻辑槽位名称列表。
"""
# 这里初始化了两个变量 name 和 topology
name = slot_name_from_member_name(name)
topology = {slot_name_from_member_name(m.name): m.replicatefrom and slot_name_from_member_name(m.replicatefrom)
for m in self.members}
# 用于存放禁用的永久逻辑槽位名称
disabled_permanent_logical_slots: List[str] = []
# 遍历 permanent_slots 字典中的每个槽位名称和其对应的值
for slot_name, value in permanent_slots.items():
if not slot_name_re.match(slot_name):
logger.error("Invalid permanent replication slot name '%s'", slot_name)
logger.error("Slot name may only contain lower case letters, numbers, and the underscore chars")
continue
value = deepcopy(value) if value else {'type': 'physical'}
if isinstance(value, dict):
if 'type' not in value:
value['type'] = 'logical' if value.get('database') and value.get('plugin') else 'physical'
# 如果槽位类型为 physical,并且槽位名称不存在于 slots 中且不等于当前节点名称,则根据节点角色和拓扑信息设置 expected_active 字段,并将槽位信息添加到 slots 中
if value['type'] == 'physical':
# 不要尝试为自己创建永久的物理复制槽
if slot_name not in slots and slot_name != name:
# 在leader上,我们希望有永久槽位活动,除非它是一个槽位
# 表示级联副本。让我们考虑一个配置,其中C是一个永久槽。在这个
# 情况下,我们应该有如下:A(B:活性,C:非活性)<- B (C:活性)<- C
# 我们不考虑节点B上的相同情况,因为如果节点C不存在,我们将不考虑
# 能够知道它的‘ replicatefrom ’标签值。
expected_active = not topology.get(slot_name) and role in ('primary', 'standby_leader')
slots[slot_name] = {**value, 'expected_active': expected_active}
continue
# 如果槽位类型为 logical,则检查 can_advance_slots 是否为 True,如果为 False 则将槽位名称添加到禁用列表中;如果为 True 且槽位名称已存在于 slots 中,则记录一条错误日志;否则将槽位信息添加到 slots 中
if self.is_logical_slot(value):
if not can_advance_slots:
disabled_permanent_logical_slots.append(slot_name)
elif slot_name in slots:
logger.error("Permanent logical replication slot {'%s': %s} is conflicting with"
" physical replication slot for cluster member", slot_name, value)
else:
slots[slot_name] = value
continue
logger.error("Bad value for slot '%s' in permanent_slots: %s", slot_name, permanent_slots[slot_name])
# 返回禁用的永久逻辑槽位名称列表
return disabled_permanent_logical_slots
作用:
_merge_permanent_slots
方法是一个实例方法,它的具体作用是合并成员的复制槽位信息与永久槽位信息。具体来说:
- 验证槽位名称:
- 方法首先检查每个永久槽位名称是否符合正则表达式规则,不符合规则的槽位名称会被忽略。
- 更新槽位信息:
- 方法根据槽位的类型(
physical
或logical
)以及节点名称来更新slots
字典。 - 对于物理槽位,如果槽位名称不存在于
slots
中且不等于当前节点名称,则根据节点角色和拓扑信息设置expected_active
字段,并将槽位信息添加到slots
中。 - 对于逻辑槽位,如果 PostgreSQL 版本支持
pg_replication_slot_advance()
函数,则将槽位信息添加到slots
中;否则将槽位名称添加到禁用列表中。
- 方法根据槽位的类型(
- 错误处理:
- 如果槽位名称不符合规则或槽位信息格式错误,则记录一条错误日志。
- 返回结果:
- 返回一个列表
disabled_permanent_logical_slots
,其中包含了由于 PostgreSQL 版本不支持而禁用的永久逻辑槽位名称。
- 返回一个列表
1.4.1.4.14 _get_permanent_slots()
-
定义了一个名为
_get_permanent_slots
的私有方法,该方法是一个类的实例方法,因为使用了self
参数。该方法接受三个参数:postgresql
: 类型为Postgresql
,指的是一个Postgresql
对象的引用。tags
: 类型为Tags
,指的是实现了Tags
接口的对象的引用。role
: 类型为str
,表示节点的角色,可以是primary
,standby_leader
或replica
。
方法返回类型为
Dict[str, Any]
,即返回一个字典。
def _get_permanent_slots(self, postgresql: 'Postgresql', tags: Tags, role: str) -> Dict[str, Any]:
"""获取配置的永久复制槽位。
.. note::
只有当 `use_slots` 配置启用时才会考虑永久复制槽位。
不应该成为领导者(*nofailover*)的节点不会拥有永久复制槽位。
此外,流复制禁用(*nostream*)的节点及其级联跟随者由于缺乏从节点到主节点的反馈,不能拥有永久逻辑槽位,这使得它们使用起来不安全。
在备用集群中,我们只支持物理复制槽位。
对于非备用集群返回的字典总是包含永久逻辑复制槽位,以便在 PostgreSQL 版本低于 11 时不支持逻辑槽位时发出警告。
:param postgresql: `Postgresql` 对象的引用。
:param tags: 实现了 `Tags` 接口的对象的引用。
:param role: 节点的角色 -- `primary`, `standby_leader` 或 `replica`。
:returns: 永久槽位名称映射到属性的字典。
"""
# 如果全局配置中的 use_slots 未启用或者 tags.nofailover 为 True,则直接返回空字典
if not global_config.use_slots or tags.nofailover:
return {}
# 如果当前集群是备用集群 (global_config.is_standby_cluster) 或者在主节点上找不到对应槽位名称
if global_config.is_standby_cluster or self.get_slot_name_on_primary(postgresql.name, tags) is None:
return self.permanent_physical_slots if postgresql.can_advance_slots or role == 'standby_leader' else {}
return self.__permanent_slots if postgresql.can_advance_slots or role == 'primary' \
else self.__permanent_logical_slots
作用:
_get_permanent_slots
方法是一个实例方法,它的具体作用是从配置中获取永久复制槽位信息。具体来说:
- 检查全局配置:
- 方法首先检查全局配置
use_slots
是否启用。如果没有启用,则返回空字典。 - 方法还检查当前节点是否设置了
nofailover
标记。如果设置了nofailover
标记,则返回空字典。
- 方法首先检查全局配置
- 判断备用集群:
- 如果当前集群是备用集群,则只返回物理永久槽位信息。
- 如果在主节点上找不到对应槽位名称,则也根据
postgresql.can_advance_slots
或者role
决定返回物理永久槽位还是空字典。
- 返回槽位信息:
- 如果
postgresql.can_advance_slots
为True
或者role
为primary
,则返回永久槽位信息。 - 否则返回逻辑永久槽位信息。
- 如果
1.4.1.4.15 _get_members_slots()
-
定义了一个名为
_get_members_slots
的私有方法,该方法是一个类的实例方法,因为使用了self
参数。该方法接受四个参数:name
: 类型为str
,表示当前节点的名称。role
: 类型为str
,表示当前节点的角色,可以是primary
,standby_leader
或replica
。nofailover
: 类型为bool
,表示是否标记为不可切换。can_advance_slots
: 类型为bool
,表示是否支持pg_replication_slot_advance()
函数。
方法返回类型为
Dict[str, Dict[str, Any]]
,即返回一个字典。
def _get_members_slots(self, name: str, role: str, nofailover: bool,
can_advance_slots: bool) -> Dict[str, Dict[str, Any]]:
"""获取给定成员的物理复制槽位配置。
以下情况可能发生:
* 如果成员设置了 `nostream` 标签 - 我们不应该在当前主节点或其他成员上为它创建复制槽位,
即使设置了 `replicatefrom`,因为 `nostream` 禁用了 WAL 流式复制。
* PostgreSQL 版本为 11 或更高版本,并且配置允许保留成员的复制槽位。在这种情况下,
我们希望为每个成员创建复制槽位,除了设置了 `nofailover` 标签的情况。
* PostgreSQL 版本低于 11 或配置不允许保留成员的复制槽位。在这种情况下我们希望:
* 在主节点上为所有没有 `replicatefrom` 标签指向现有成员的成员创建复制槽位。
* 在副本节点上仅创建 `replicatefrom` 标签指向我们的成员的复制槽位。
如果发生以下情况之一,将会记录一条错误日志:
* 成员之间存在冲突的槽位名称
:param name: 当前节点的名称。
:param role: 当前节点的角色,`primary`, `standby_leader`, 或 `replica`。
:param nofailover: 如果此节点被标记为不可作为切换候选,则为 `True`,否则为 `False`。
:param can_advance_slots: 如果 `pg_replication_slot_advance()` 函数可用,则为 `True`,否则为 `False`。
:returns: 应该存在于给定节点上的物理复制槽位的字典。
"""
# 如果全局配置 use_slots 未启用,则返回空字典
if not global_config.use_slots:
return {}
# 我们总是想从列表中排除具有我们名字的成员,
# 也排除禁用WAL流的成员
members = filter(lambda m: m.name != name and not m.nostream, self.members)
# 用于检查提供的成员是否应该从当前节点复制数据,当当前节点运行为主节点时
def leader_filter(member: Member) -> bool:
"""检查提供的 member 在当前节点作为领导者运行时是否应该进行复制。
:param member: 一个 :class:`Member` 对象。
:returns: 如果提供的 member 应该从当前节点进行复制,则返回 ``True``,否则返回 ``False``。
"""
return member.replicatefrom is None or\
member.replicatefrom == name or\
not self.has_member(member.replicatefrom)
def replica_filter(member: Member) -> bool:
"""检查提供的 member 在当前节点作为副本运行时是否应该进行复制。
..note:: 我们仅考虑与我们名称匹配的具有 replicatefrom 标签的成员,并始终排除领导者。
:param member: 一个 :class:`Member` 对象。
:returns: 如果提供的 member 应该从当前节点进行复制,则返回 ``True``,否则返回 ``False``。
"""
return member.replicatefrom == name and member.name != self.leader_name
# 在可以保留复制插槽的情况下,将使用 expected_active 函数来确定复制插槽是否预期处于活动状态。否则,它将用于查找当前节点上应存在的复制插槽。
expected_active = leader_filter if role in ('primary', 'standby_leader') else replica_filter
if can_advance_slots and global_config.member_slots_ttl > 0:
# 如果该节点仅进行级联并且无法成为领导者,我们只希望为能够连接到它的成员保留插槽。
members = [m for m in members if not nofailover or m.replicatefrom == name]
else:
members = [m for m in members if expected_active(m)]
# 遍历成员列表,构造每个成员的复制槽位信息
slots: Dict[str, int] = self.slots
ret: Dict[str, Dict[str, Any]] = {}
for member in members:
slot_name = slot_name_from_member_name(member.name)
lsn = slots.get(slot_name, 0)
if member.replicatefrom:
# /status 键由领导者维护,但 member 可能连接到其他节点。在这种情况下,领导者中的插槽处于非活动状态并且不会前进,因此我们使用成员报告的 LSN 来推进复制插槽的 LSN。max 只是一个后备选项,所以当没有来自成员的反馈时,我们从插槽中获取 LSN。
lsn = max(member.lsn or 0, lsn)
ret[slot_name] = {'type': 'physical', 'lsn': lsn, 'expected_active': expected_active(member)}
slot_name = slot_name_from_member_name(name)
ret.update({slot: {'type': 'physical'} for slot in self.status.retain_slots
if not nofailover and slot not in ret and slot != slot_name})
if len(ret) < len(members):
# 查找哪些名称存在冲突,以便提供更友好的错误消息。
slot_conflicts: Dict[str, List[str]] = defaultdict(list)
for member in members:
slot_conflicts[slot_name_from_member_name(member.name)].append(member.name)
logger.error("Following cluster members share a replication slot name: %s",
"; ".join(f"{', '.join(v)} map to {k}"
for k, v in slot_conflicts.items() if len(v) > 1))
return ret
作用:
_get_members_slots
方法是一个实例方法,它的具体作用是获取特定成员的物理复制槽位配置。具体来说:
- 过滤成员:
- 方法首先过滤掉当前节点名称相同的成员以及禁用了 WAL 流式复制的成员。
- 根据角色选择过滤器:
- 根据当前节点的角色选择合适的过滤器函数
expected_active
,用于确定哪些成员应该从当前节点复制数据。
- 根据当前节点的角色选择合适的过滤器函数
- 构造槽位信息:
- 遍历过滤后的成员列表,构造每个成员的复制槽位信息,并将其添加到结果字典
ret
中。
- 遍历过滤后的成员列表,构造每个成员的复制槽位信息,并将其添加到结果字典
- 处理槽位冲突:
- 如果发现槽位名称冲突,则记录一条错误日志。
- 返回槽位配置:
- 返回构造好的槽位配置字典。
1.4.1.4.16 has_permanent_slots()
-
定义了一个名为
has_permanent_slots
的实例方法。该方法接收两个参数:postgresql
: 类型为Postgresql
,指的是一个Postgresql
对象的引用。member
: 类型为Tags
,指的是实现了Tags
接口的对象的引用。
方法返回类型为
bool
,即返回一个布尔值。
def has_permanent_slots(self, postgresql: 'Postgresql', member: Tags) -> bool:
"""检查我们的节点是否配置了永久复制槽位。
:param postgresql: `Postgresql` 对象的引用。
:param member: 用于检查永久逻辑复制槽位的节点,实现了 `Tags` 接口的对象的引用。
:returns: 如果配置了永久复制槽位则返回 `True`,否则返回 `False`。
"""
role = 'replica'
# 调用 _get_members_slots 方法来获取成员槽位信息
members_slots: Dict[str, Dict[str, str]] = self._get_members_slots(postgresql.name, role,
member.nofailover,
postgresql.can_advance_slots)
# 调用 _get_permanent_slots 方法来获取永久槽位信息
permanent_slots: Dict[str, Any] = self._get_permanent_slots(postgresql, member, role)
# 使用 deepcopy 方法创建 members_slots 的深拷贝
slots = deepcopy(members_slots)
# 调用 _merge_permanent_slots 方法来合并永久槽位信息到 slots 变量中
self._merge_permanent_slots(slots, permanent_slots, postgresql.name, role, postgresql.can_advance_slots)
return len(slots) > len(members_slots) or any(self.is_physical_slot(v) for v in permanent_slots.values())
作用:
has_permanent_slots
方法的作用是检查一个给定的节点是否配置了永久复制槽位。具体步骤如下:
- 初始化角色:
- 将
role
初始化为'replica'
,代表当前节点的角色为副本节点。
- 将
- 获取成员槽位:
- 通过调用
_get_members_slots
方法来获取当前节点作为副本节点时的成员槽位信息。
- 通过调用
- 获取永久槽位:
- 通过调用
_get_permanent_slots
方法来获取当前节点的永久槽位信息。
- 通过调用
- 合并槽位信息:
- 使用
deepcopy
创建members_slots
的深拷贝,并将permanent_slots
合并到拷贝中。
- 使用
- 判断是否有永久槽位:
- 最后,通过比较合并后的槽位信息与原始成员槽位信息的长度,以及检查
permanent_slots
中是否存在物理槽位,来确定是否有永久槽位配置。
- 最后,通过比较合并后的槽位信息与原始成员槽位信息的长度,以及检查
- 返回布尔值:
- 根据上述条件返回
True
或False
,表示是否有永久槽位配置。
- 根据上述条件返回
1.4.1.4.17 maybe_filter_permanent_slots()
-
定义了一个名为
maybe_filter_permanent_slots
的实例方法。该方法接收两个参数:postgresql
: 类型为Postgresql
,指的是一个Postgresql
对象的引用。slots
: 类型为Dict[str, int]
,指的是包含槽位名称及其 LSN 值的字典。
方法返回类型为
Dict[str, int]
,即返回一个字典。
def maybe_filter_permanent_slots(self, postgresql: 'Postgresql', slots: Dict[str, int]) -> Dict[str, int]:
"""从提供的 *slots* 字典中过滤掉所有非永久槽位。
.. note::
如果启用了成员复制槽位的保留,则不会执行任何过滤操作,因为我们需要发布成员复制槽位的LSN值,
以便其他节点可以使用这些值来推进LSN,就像它们对永久槽位所做的那样。
:param postgresql: `Postgresql` 对象的引用。
:param slots: 包含槽位名称及其 LSN 值的字典。
:returns: 包含已知为永久槽位的字典。
"""
if global_config.member_slots_ttl > 0:
return slots
# 调用 _get_permanent_slots 方法来获取永久槽位信息
permanent_slots: Dict[str, Any] = self._get_permanent_slots(postgresql, RemoteMember('', {}), 'replica')
# 创建一个集合 members_slots,包含所有成员的槽位名称
members_slots = {slot_name_from_member_name(m.name) for m in self.members}
return {name: value for name, value in slots.items() if name in permanent_slots
and (self.is_physical_slot(permanent_slots[name])
or self.is_logical_slot(permanent_slots[name]) and name not in members_slots)}
作用:
maybe_filter_permanent_slots
方法的作用是从给定的一组槽位中筛选出永久性的槽位。具体步骤如下:
- 检查配置:
- 如果全局配置
member_slots_ttl
大于零(意味着成员槽位的保留被启用),那么不需要做任何过滤,直接返回输入的slots
字典。
- 如果全局配置
- 获取永久槽位信息:
- 通过调用
_get_permanent_slots
方法来获取永久槽位信息。
- 通过调用
- 获取成员槽位名称集合:
- 创建一个集合
members_slots
,包含所有成员的槽位名称。
- 创建一个集合
- 构建永久槽位字典:
- 构建一个新的字典,该字典只包含那些在
permanent_slots
中,并且是物理槽位或逻辑槽位但不属于成员槽位集合members_slots
的槽位。
- 构建一个新的字典,该字典只包含那些在
- 返回结果:
- 返回新的字典。
1.4.1.4.18 _has_permanent_logical_slots()
-
定义了一个名为
_has_permanent_logical_slots
的实例方法。该方法接收两个参数:postgresql
: 类型为Postgresql
,指的是一个Postgresql
对象的引用。member
: 类型为Tags
,指的是一个实现了Tags
接口的对象,用于标识我们正在检查其永久逻辑复制槽位的节点。
方法返回类型为布尔值
bool
,即返回True
或False
。
def _has_permanent_logical_slots(self, postgresql: 'Postgresql', member: Tags) -> bool:
"""检查给定的成员节点是否配置了永久的“逻辑”复制槽位。
:param postgresql: `Postgresql` 对象的引用。
:param member: 实现了 `Tags` 接口的对象,用于标识我们正在检查其永久逻辑复制槽位的节点。
:returns: 如果检测到的任何复制槽位是“逻辑”的,则返回 `True`,否则返回 `False`。
"""
# 调用 get_replication_slots 方法来获取指定角色(这里是 'replica')的复制槽位信息
slots = self.get_replication_slots(postgresql, member, role='replica').values()
# 使用列表推导式结合 any 函数来检查是否存在逻辑类型的复制槽位
return any(v for v in slots if v.get("type") == "logical")
作用:
_has_permanent_logical_slots
方法的作用是检查给定的成员节点是否配置了任何永久的逻辑复制槽位。具体步骤如下:
- 获取复制槽位信息:
- 调用
get_replication_slots
方法,传入postgresql
对象和member
对象以及角色'replica'
,得到所有复制槽位的信息。
- 调用
- 检查逻辑槽位:
- 遍历所有槽位信息,检查是否存在类型为
"logical"
的槽位。
- 遍历所有槽位信息,检查是否存在类型为
- 返回结果:
- 如果找到至少一个逻辑槽位,则返回
True
,表示存在永久逻辑复制槽位; - 否则返回
False
,表示没有找到逻辑槽位。
- 如果找到至少一个逻辑槽位,则返回
1.4.1.4.19 should_enforce_hot_standby_feedback()
-
定义了一个名为
should_enforce_hot_standby_feedback
的实例方法。该方法接收两个参数:postgresql
: 类型为Postgresql
,指的是一个Postgresql
对象的引用。member
: 类型为Tags
,指的是一个实现了Tags
接口的对象,用于标识我们正在检查其永久逻辑复制槽位的节点。
方法返回类型为布尔值
bool
,即返回True
或False
。
def should_enforce_hot_standby_feedback(self, postgresql: 'Postgresql', member: Tags) -> bool:
"""确定是否应为给定的成员启用 `hot_standby_feedback`。
如果当前副本具有 `logical` 槽位,或者它作为另一个具有 `logical` 槽位节点的级联副本工作,则必须启用 `hot_standby_feedback`。
:param postgresql: `Postgresql` 对象的引用。
:param member: 实现了 `Tags` 接口的对象,用于标识我们正在检查其永久逻辑复制槽位的节点。
:returns: 如果当前节点或从当前节点复制的任何成员具有永久逻辑槽位,则返回 `True`,否则返回 `False`。
"""
# 首先检查当前节点是否具有永久逻辑槽位
if self._has_permanent_logical_slots(postgresql, member):
return True
# 如果全局配置中启用了槽位 (global_config.use_slots 为真),则根据 member 是否为 Member 类型来确定 name,通常是节点的名字
if global_config.use_slots:
name = member.name if isinstance(member, Member) else postgresql.name
# 检查主节点上是否有对应的槽位名称
if not self.get_slot_name_on_primary(name, member):
return False
# 获取所有从当前节点复制的成员(排除领导者节点),然后递归地调用自身方法
members = [m for m in self.members if m.replicatefrom == name and m.name != self.leader_name]
return any(self.should_enforce_hot_standby_feedback(postgresql, m) for m in members)
return False
作用:
should_enforce_hot_standby_feedback
方法的作用是决定是否应该为给定的成员节点启用 hot_standby_feedback
功能。具体步骤如下:
- 检查当前节点:
- 如果当前节点拥有永久逻辑槽位,则直接返回
True
。
- 如果当前节点拥有永久逻辑槽位,则直接返回
- 检查全局配置:
- 如果全局配置开启了槽位机制(
use_slots
为真),则继续下一步检查。
- 如果全局配置开启了槽位机制(
- 确定节点名称:
- 根据
member
是否为Member
类型来确定name
。
- 根据
- 检查主节点上的槽位:
- 检查主节点上是否有对应节点的槽位名称。如果没有,则返回
False
。
- 检查主节点上是否有对应节点的槽位名称。如果没有,则返回
- 检查级联副本:
- 如果当前节点是一个级联副本的源头,则检查所有从当前节点复制的成员是否需要启用
hot_standby_feedback
。如果任何一个成员需要启用,则返回True
。
- 如果当前节点是一个级联副本的源头,则检查所有从当前节点复制的成员是否需要启用
- 返回默认值:
- 如果以上条件都不满足,则返回
False
。
- 如果以上条件都不满足,则返回
1.4.1.4.20 get_slot_name_on_primary()
-
定义了一个名为
get_slot_name_on_primary
的实例方法。该方法接收两个参数:name
: 类型为str
,指的是要检查的成员节点的名称。tags
: 类型为Tags
,指的是实现了Tags
接口的对象。
方法返回类型为
Optional[str]
,即返回一个可选的字符串,可能为空。
def get_slot_name_on_primary(self, name: str, tags: Tags) -> Optional[str]:
"""获取当前节点在主节点上的物理复制槽位名称。
.. note::
在级联复制的情况下,我们必须检查的不是我们的物理槽位,而是连接到主节点的那个副本的槽位。
:param name: 要检查的成员节点的名称。
:param tags: 实现了 `Tags` 接口的对象的引用。
:returns: 在主节点上用于当前节点物理复制的槽位名称。
"""
# 初始化一个集合 seen_nodes 用来记录已经访问过的节点名称
seen_nodes: Set[str] = set()
# 进入一个无限循环,并将当前节点名称添加到已访问节点的集合中
while True:
seen_nodes.add(name)
# 如果 tags 对象的 nostream 属性为 True,则表明没有流复制
if tags.nostream:
return None
# 尝试获取当前节点所复制的节点(replicatefrom)
replicatefrom = self.get_member(tags.replicatefrom, False) \
if tags.replicatefrom and tags.replicatefrom != name else None
# 检查 replicatefrom 是否是一个 Member 类型的实例
if not isinstance(replicatefrom, Member):
return slot_name_from_member_name(name)
# 检查复制源节点名称是否已经在 seen_nodes 集合中。如果是,则表明存在环路,返回 None
if replicatefrom.name in seen_nodes:
return None
# 更新 name 和 tags 为复制源节点的名称和标签,以便在下一次迭代中继续向上追溯
name, tags = replicatefrom.name, replicatefrom
作用:
get_slot_name_on_primary
方法的作用是获取当前节点在其主节点上的物理复制槽位名称。具体步骤如下:
- 初始化已访问节点集合:
- 创建一个集合
seen_nodes
用来记录已经访问过的节点名称。
- 创建一个集合
- 无限循环:
- 进入一个无限循环,用于追踪复制关系链。
- 检查流复制状态:
- 如果当前节点没有流复制,则返回
None
。
- 如果当前节点没有流复制,则返回
- 获取复制源节点:
- 尝试获取当前节点所复制的节点。
- 检查复制源类型:
- 如果复制源不是一个
Member
类型的实例,则返回当前节点名称转换得到的槽位名称。
- 如果复制源不是一个
- 检查环路:
- 检查复制源节点名称是否已经在
seen_nodes
集合中,以防止无限循环。如果有环路,则返回None
。
- 检查复制源节点名称是否已经在
- 向上追溯:
- 更新当前节点为复制源节点,并继续向上追溯,直到找到主节点上的物理复制槽位名称或遇到返回条件。
- 返回结果:
- 当找到物理复制槽位名称或满足某个返回条件时,跳出循环并返回结果。
1.4.1.5 类:Status
- 定义了一个名为
Status
的类,继承自NamedTuple
。这个类表示一个不可变的对象,用于封装与数据库状态相关的信息,特别是与/status
相关的信息。 Status
类是一个用于封装数据库状态信息的不可变对象。它通过NamedTuple
来定义,提供了对数据库/status
键相关数据的封装。该类包含了三个字段:last_lsn
、slots
和retain_slots
,分别用于表示最新领导者 LSN 位置、主节点上的永久复制槽状态以及存在于集群中的成员的物理复制槽列表。这个类的使用可以简化数据库状态的管理和表示,同时由于其不可变性,增强了代码的安全性和可维护性。
class Status(NamedTuple):
"""不可变对象(命名元组),表示 `/status` 键。
Consists of the following fields:
:ivar last_lsn: :class:`int` 对象,包含已知的最新领导者 LSN 位置。
:ivar slots: 主节点上永久复制槽的状态,格式为:``{"slot_name": int}``。
:ivar retain_slots: 列出存在于集群中的成员的物理复制槽。
"""
last_lsn: int
slots: Optional[Dict[str, int]]
retain_slots: List[str]
1.4.1.5.1 empty()
- 定义了一个名为
empty
的静态方法,该方法返回类型为Status
。
@staticmethod
def empty() -> 'Status':
"""构造一个空的 :class:`Status` 实例。
:returns: 空的 :class:`Status` 对象。
"""
return Status(0, None, [])
作用:
这个函数的作用是生成一个没有任何具体信息填充的 Status
实例。具体来说:
- 初始化
Status
实例:- 创建一个
Status
实例,并将其所有字段设置为默认值或空值。
- 创建一个
- 返回实例:
- 返回创建的空
Status
实例。
- 返回创建的空
1.4.1.5.2 is_empty()
- 定义了一个名为
is_empty
的实例方法,该方法没有参数,除了隐含的self
参数。
def is_empty(self):
"""验证这个 :class:`Status` 实例的所有属性是否已被定义。
:returns: 如果当前 :class:`Status` 的所有属性都没有填充,则返回 ``True``。
"""
return self.last_lsn == 0 and self.slots is None and not self.retain_slots
作用:
这个函数的作用是检查当前 Status
实例的所有属性是否都没有具体的值,也就是是否处于“空”的状态。具体来说:
- 检查
last_lsn
属性:- 如果
last_lsn
的值为0
,表示没有记录的 LSN 位置,这通常意味着还没有任何实际的数据记录。
- 如果
- 检查
slots
属性:- 如果
slots
的值为None
,表示没有永久复制槽的状态信息,这通常意味着还没有设置任何复制槽。
- 如果
- 检查
retain_slots
属性:- 如果
retain_slots
是一个空列表[]
,表示没有需要保留的复制槽,这意味着还没有任何成员需要保留复制槽。
- 如果
如果这三个条件都满足,则认为当前的 Status
实例是“空”的。
1.4.1.5.3 from_node()
- 定义了一个名为
from_node
的静态方法,该方法接收一个类型为Union[str, Dict[str, Any], None]
的参数value
,并返回一个Status
类型的对象。
@staticmethod
def from_node(value: Union[str, Dict[str, Any], None]) -> 'Status':
"""工厂方法,用于将 *value* 解析为 :class:`Status` 对象。
:param value: JSON 序列化的字符串或 :class:`dict` 对象。
:returns: 构造的 :class:`Status` 对象。
"""
# 试将 value 转换为字典对象
try:
if isinstance(value, str):
value = json.loads(value)
except Exception:
return Status.empty()
# 检查 value 是否为整数类型
if isinstance(value, int): # legacy
return Status(value, None, [])
# 如果 value 不是字典类型,则返回一个空的 Status 对象
if not isinstance(value, dict):
return Status.empty()
# 尝试从 value 字典中获取 optime 键对应的值,并将其转换为整数类型。如果转换失败
try:
last_lsn = int(value.get('optime', ''))
except Exception:
last_lsn = 0
# 从 value 字典中获取 slots 键对应的值
slots: Union[str, Dict[str, int], None] = value.get('slots')
if isinstance(slots, str):
try:
slots = json.loads(slots)
except Exception:
slots = None
if not isinstance(slots, dict):
slots = None
# 从 value 字典中获取 retain_slots 键对应的值
retain_slots: Union[str, List[str], None] = value.get('retain_slots')
if isinstance(retain_slots, str):
try:
retain_slots = json.loads(retain_slots)
except Exception:
retain_slots = []
if not isinstance(retain_slots, list):
retain_slots = []
return Status(last_lsn, slots, retain_slots)
作用:
这个函数的作用是从给定的值 value
中解析出一个 Status
对象。value
可以是 JSON 格式的字符串,也可以是字典对象。根据 value
的类型和内容,函数会进行适当的解析,并构造出一个 Status
对象。如果解析过程中遇到任何问题,例如 value
不能被正确解析为字典,或者某些关键字段缺失或格式错误,函数会返回一个空的 Status
对象。
1.4.1.6 类:SyncState
SyncState
类是一个用于封装同步复制状态信息的不可变对象。它通过NamedTuple
来定义,提供了对同步复制状态的封装。该类包含了四个字段:version
、leader
、sync_standby
和quorum
,分别用于表示同步状态的版本、当前领导者、同步备用列表以及法定数量。这个类的使用可以简化同步复制状态的管理和表示,同时由于其不可变性,增强了代码的安全性和可维护性。
class SyncState(NamedTuple):
"""不可变对象(命名元组),表示最后观察到的同步复制状态。
:ivar version: 同步键在配置存储中的修改版本。
:ivar leader: 指向当前领导者成员的引用。
:ivar sync_standby: 最后同步到领导者的同步备用列表(逗号分隔)。
:ivar quorum: 如果 :attr:`~SyncState.sync_standby` 列表中的节点正在进行领导者选举,它应该至少看到 :attr:`~SyncState.quorum` 个其他节点,
包括 :attr:`~SyncState.sync_standby` + :attr:`~SyncState.leader` 列表中的节点。
"""
version: Optional[_Version]
leader: Optional[str]
sync_standby: Optional[str]
quorum: int
1.4.1.6.1 empty()
- 定义了一个名为
empty
的静态方法,该方法返回类型为SyncState
,并且接受一个可选的参数version
,类型为Optional[_Version]
。
@staticmethod
def empty(version: Optional[_Version] = None) -> 'SyncState':
"""构造一个空的 :class:`SyncState` 实例。
:param version: 可选的版本号。
:returns: 空的同步状态对象。
"""
return SyncState(version, None, None, 0)
作用:
这个函数的作用是生成一个没有任何具体信息填充的 SyncState
实例,除了版本号可以由用户指定。具体来说:
- 初始化
SyncState
实例:- 创建一个
SyncState
实例,并将其所有字段设置为默认值或空值,除了版本号可以由用户指定。
- 创建一个
- 返回实例:
- 返回创建的空
SyncState
实例。
- 返回创建的空
1.4.1.6.2 from_node()
- 定义了一个名为
from_node
的静态方法,该方法返回类型为SyncState
,并且接受两个参数:version
和value
。
@staticmethod
def from_node(version: Optional[_Version], value: Union[str, Dict[str, Any], None]) -> 'SyncState':
"""工厂方法,用于将 *value* 解析为同步状态信息。
:param version: 可选的 *version* 编号。
:param value: (可选 JSON 序列化)的同步状态信息
:returns: 构造的 :class:`SyncState` 对象。
:Example:
>>> SyncState.from_node(1, None).leader is None
True
>>> SyncState.from_node(1, '{}').leader is None
True
>>> SyncState.from_node(1, '{').leader is None
True
>>> SyncState.from_node(1, '[]').leader is None
True
>>> SyncState.from_node(1, '{"leader": "leader"}').leader == "leader"
True
>>> SyncState.from_node(1, {"leader": "leader"}).leader == "leader"
True
"""
# 尝试将 value 转换为字典对象
try:
if value and isinstance(value, str):
value = json.loads(value)
assert isinstance(value, dict)
leader = value.get('leader')
quorum = value.get('quorum')
return SyncState(version, leader, value.get('sync_standby'), int(quorum) if leader and quorum else 0)
except (AssertionError, TypeError, ValueError):
return SyncState.empty(version)
作用:
这个函数的作用是从给定的值 value
中解析出一个 SyncState
对象。value
可以是 JSON 格式的字符串,也可以是字典对象。根据 value
的类型和内容,函数会进行适当的解析,并构造出一个 SyncState
对象。如果解析过程中遇到任何问题,例如 value
不能被正确解析为字典,或者某些关键字段缺失或格式错误,函数会返回一个空的 SyncState
对象。
1.4.1.6.3 is_empty()
- 定义了一个名为
is_empty
的属性方法,返回一个布尔值。
@property
def is_empty(self) -> bool:
"""``True`` 如果 ``/sync`` 键无效(没有领导者)。"""
return not self.leader
作用:
这个属性方法的作用是检查 SyncState
实例是否有领导者成员。如果没有领导者成员,则返回 True
,表明该 SyncState
实例是无效的;如果有领导者成员,则返回 False
,表明该 SyncState
实例是有效的。
1.4.1.6.4 _str_to_list()
- 定义了一个名为
_str_to_list
的静态方法,该方法接受一个字符串参数value
并返回一个字符串列表。
@staticmethod
def _str_to_list(value: str) -> List[str]:
"""按逗号分割字符串并返回字符串列表。
:param value: 逗号分隔的字符串。
:returns: 将输入值按逗号分割后的非空字符串列表。
"""
return list(filter(lambda a: a, [s.strip() for s in value.split(',')]))
使用列表推导式来分割字符串 value
,并通过 strip()
方法去除每个子字符串两端的空白字符。接着,使用 filter()
函数过滤掉所有空字符串(即去除空白字符后长度为零的字符串),并将过滤结果转换为列表返回。
作用:
这个函数的作用是从一个逗号分隔的字符串中提取出所有非空的子字符串,并返回这些子字符串组成的列表。具体来说:
- 分割字符串:使用
split(',')
方法将输入的字符串按照逗号分割成多个子字符串。 - 去除空白字符:使用
strip()
方法去除每个子字符串两端的空白字符。 - 过滤空字符串:使用
filter()
函数过滤掉所有去除空白字符后为空的字符串。 - 返回列表:将过滤后的字符串集合转换为列表并返回。
1.4.1.6.5 voters()
- 定义了一个名为
voters
的属性方法,该方法不需要任何参数,并返回一个字符串列表。
@property
def voters(self) -> List[str]:
""":attr:`~SyncState.sync_standby` 作为列表,或者如果未定义或对象被认为是 ``empty`` 则返回空列表。"""
return self._str_to_list(self.sync_standby) if not self.is_empty and self.sync_standby else []
如果当前 SyncState
实例不是空的 (not self.is_empty
) 并且 sync_standby
字段存在 (self.sync_standby
),则调用 _str_to_list
方法将 sync_standby
字段转换为一个列表;否则返回一个空列表 []
。
作用:
这个属性方法的作用是获取 SyncState
实例中的 sync_standby
字段,并将其转换为一个列表形式。如果 sync_standby
字段存在并且当前实例不是空的,那么返回的是一个包含所有同步备用节点名称的列表。如果 sync_standby
字段不存在或者当前实例被认为是空的,则返回一个空列表。
1.4.1.6.6 members()
- 定义了一个名为
members
的属性方法,该方法不需要任何参数,并返回一个字符串列表。
@property
def members(self) -> List[str]:
""":attr:`~SyncState.sync_standby` 和 :attr:`~SyncState.leader` 作为列表
或者如果对象被认为是 ``empty`` 则返回空列表。
"""
return [] if not self.leader else [self.leader] + self.voters
作用:
这个属性方法的作用是返回 SyncState
实例中的所有成员名称(包括领导者和同步备用节点),如果实例被认为为空(即没有领导者),则返回一个空列表。具体来说:
- 检查实例是否为空:通过
not self.leader
检查当前SyncState
实例是否有领导者,如果实例被认为是空的,则直接返回空列表。 - 构建成员列表:如果实例不是空的,则构建一个成员列表,首先将领导者名称添加到列表中,然后追加由
voters
属性方法返回的所有同步备用节点名称。
1.4.1.6.7 matches()
- 定义了一个名为
matches
的实例方法,该方法接受两个参数:name
和check_leader
,并且返回一个布尔值。
def matches(self, name: Optional[str], check_leader: bool = False) -> bool:
"""检查节点是否在 ``/sync`` 状态中呈现。
由于 PostgreSQL 对 synchronous_standby_name 进行不区分大小写的检查,我们也这样做。
:param name: 节点名称。
:param check_leader: 默认情况下只在成员中搜索 *name* ,`True` 将包括领导者到列表。
:returns: 如果 ``/sync`` 键不是 :func:`is_empty` 并且给定的 *name* 在同步状态中呈现,则返回 ``True``。
:Example:
>>> s = SyncState(1, 'foo', 'bar,zoo', 0)
>>> s.matches('foo')
False
>>> s.matches('fOo', True)
True
>>> s.matches('Bar')
True
>>> s.matches('zoO')
True
>>> s.matches('baz')
False
>>> s.matches(None)
False
>>> SyncState.empty(1).matches('foo')
False
"""
# 初始化返回值 ret 为 False
ret = False
if name and not self.is_empty:
# 构建一个用于搜索的字符串 search_str
search_str = (self.sync_standby or '') + (',' + (self.leader or '') if check_leader else '')
ret = name.lower() in self._str_to_list(search_str.lower())
return ret
作用:
这个方法的作用是检查给定的节点名称是否存在于同步状态中。具体来说:
- 检查实例是否为空:如果当前
SyncState
实例被认为是空的,则直接返回False
。 - 构建搜索字符串:如果
check_leader
为True
,则将领导者名称也加入到搜索字符串中。 - 不区分大小写的搜索:将搜索字符串和节点名称都转换为小写形式,以便进行不区分大小写的比较。
- 返回结果:如果转换后的搜索字符串列表中包含节点名称的小写形式,则返回
True
;否则返回False
。
1.4.1.6.8 leader_matches()
- 定义了一个名为
leader_matches
的实例方法,该方法接受一个参数name
,并且返回一个布尔值。
def leader_matches(self, name: Optional[str]) -> bool:
"""比较给定的 *name* 与存储的领导者值。
:returns: 如果 *name* 与 :attr:`~SyncState.leader` 值匹配,则返回 ``True``。
"""
return bool(name and not self.is_empty and name.lower() == (self.leader or '').lower())
作用:
这个方法的作用是检查给定的名称是否与 SyncState
实例中的领导者名称相匹配。具体来说:
- 检查
name
是否存在:如果name
为None
或者空字符串,则直接返回False
。 - 检查实例是否为空:如果当前
SyncState
实例被认为是空的(没有领导者),则直接返回False
。 - 比较名称:如果前两个条件都满足,则比较给定的
name
与存储的领导者名称,忽略大小写差异。如果它们匹配,则返回True
;否则返回False
。
1.4.1.7 parse_connection_string()
- 定义了一个名为
parse_connection_string
的函数,该函数接受一个字符串类型的参数value
,并返回一个元组,元组的第一个元素是字符串类型,第二个元素可能是字符串或None
。
def parse_connection_string(value: str) -> Tuple[str, Union[str, None]]:
"""拆分并重组一个 URL 字符串为连接 URL 和 API URL。
.. note::
原始 Governor 存储每个集群成员的连接字符串如下格式:
postgres://{username}:{password}@{connect_address}/postgres
由于我们的每个 Patroni 实例提供了自己的 REST API 端点,因此最好将此信息与 PostgreSQL 连接字符串一起存储在 DCS 中。为了不引入新的键并与原始 Governor 兼容,我们决定以以下方式扩展原始连接字符串:
postgres://{username}:{password}@{connect_address}/postgres?application_name={api_url}
这样,原始 Governor 可以照原样使用这样的连接字符串,因为 `libpq` 库有此特性。
:param value: 要拆分的 URL 字符串。
:returns: 存储在 DCS 中的连接字符串拆分为两部分,`conn_url` 和 `api_url`。
"""
# 使用 urlparse 函数解析 value 字符串
scheme, netloc, path, params, query, fragment = urlparse(value)
# 使用 urlunparse 函数重新构建连接 URL,忽略原始查询字符串,将其替换为空字符串
conn_url = urlunparse((scheme, netloc, path, params, '', fragment))
# 使用 parse_qsl 函数解析查询字符串
api_url = ([v for n, v in parse_qsl(query) if n == 'application_name'] or [None])[0]
return conn_url, api_url
作用:
parse_connection_string
函数的具体作用是将输入的连接字符串拆分成两个部分:连接 URL (conn_url
) 和 API URL (api_url
)。具体来说:
- 解析 URL:
- 使用
urlparse
函数解析输入的value
字符串,获取各个组成部分。
- 使用
- 重建连接 URL:
- 使用
urlunparse
函数重新构造连接 URL,保留原始的协议、网络位置、路径和片段,但将查询字符串替换为空字符串。
- 使用
- 提取 API URL:
- 使用
parse_qsl
函数解析查询字符串,找出application_name
键对应的值作为 API URL。如果没有找到application_name
键,则返回None
。
- 使用
1.4.1.8 slot_name_from_member_name()
- 定义了一个名为
slot_name_from_member_name
的函数,该函数接受一个字符串类型的参数member_name
,并返回一个字符串。- 定义了一个内部函数
replace_char
,该函数接受一个匹配对象match
,并返回一个字符串。该函数的作用是:c = match.group(0)
:从匹配对象中提取实际匹配到的字符。return '_' if c in '-.' else f"u{ord(c):04d}"
:如果匹配到的字符是破折号或点,则返回下划线;否则,返回该字符的 Unicode 代码点,格式化为形如uXXXX
的字符串。
- 定义了一个内部函数
def slot_name_from_member_name(member_name: str) -> str:
"""将成员名称转换为有效的 PostgreSQL 复制槽名称。
.. note::
PostgreSQL 的复制槽名称必须是有效的 PostgreSQL 名称。此函数将更广泛的成员名称映射到有效的 PostgreSQL 名称。名称会被转换成小写,常见的主机名中的破折号和点会被下划线替换,其他字符会被编码为其 Unicode 代码点。名称会被截断为 64 个字符。多个不同的成员名称可能会映射到同一个槽名称。
:param member_name: 要转换为槽名称的字符串。
:returns: 使用上述规则转换后的字符串。
"""
def replace_char(match: Any) -> str:
c = match.group(0)
return '_' if c in '-.' else f"u{ord(c):04d}"
slot_name = re.sub('[^a-z0-9_]', replace_char, member_name.lower())
return slot_name[0:63]
作用:
slot_name_from_member_name
函数的具体作用是将输入的 member_name
转换成一个有效的 PostgreSQL 复制槽名称。具体来说:
- 转换成小写:
- 将输入的
member_name
转换成小写,以符合 PostgreSQL 名称规范。
- 将输入的
- 替换非法字符:
- 使用
replace_char
函数将破折号-
和点.
替换为下划线_
,将其他非法字符替换为其 Unicode 代码点的形式uXXXX
。
- 使用
- 截取字符串:
- 截取转换后的字符串为前 63 个字符,以确保名称长度不超过 PostgreSQL 的限制。
1.4.1.9 dcs_modules()
- 定义了一个名为
dcs_modules
的函数,该函数没有参数,并返回一个字符串列表。
def dcs_modules() -> List[str]:
"""根据执行环境获取 DCS 模块的名称。
:returns: 已知模块名称的列表,带有绝对的 Python 模块路径命名空间,例如 `patroni.dcs.etcd`。
"""
# 进行类型检查
if TYPE_CHECKING: # pragma: no cover
assert isinstance(__package__, str)
return iter_modules(__package__)
作用:
dcs_modules
函数的具体作用是获取当前包下的所有 DCS(Distributed Consistency System,分布式一致性系统)模块的名称。具体来说:
- 类型检查:
- 当使用类型检查工具时,确认
__package__
是字符串类型。
- 当使用类型检查工具时,确认
- 获取模块名称:
- 使用
iter_modules
函数从当前包__package__
下迭代出所有子模块的名称,并返回这些名称。
- 使用
1.4.1.10 catch_return_false_exception()
- 定义了一个名为
catch_return_false_exception
的装饰器函数,该装饰器接收一个泛型函数func
作为参数,并返回一个同样泛型的结果。- 定义了一个内部函数
wrapper
,该函数接受任意数量的位置参数*args
和关键字参数**kwargs
,这样它就可以透明地传递任何类型的参数给被装饰的函数func
。
- 定义了一个内部函数
def catch_return_false_exception(func: Callable[..., Any]) -> Any:
"""捕获那些可能抛出 `ReturnFalseException` 异常的函数的装饰器函数。
:param func: 被包装的函数。
:returns: 包装后的函数。
"""
def wrapper(*args: Any, **kwargs: Any):
try:
return func(*args, **kwargs)
except ReturnFalseException:
return False
return wrapper
作用:
catch_return_false_exception
函数是一个装饰器,它的具体作用是在被装饰的函数抛出 ReturnFalseException
异常时,能够捕获这个异常,并且代替抛出异常的行为返回 False
。
1.4.1.11 类:ReturnFalseException
- 定义了一个名为
ReturnFalseException
的异常类,它继承自 Python 内置的Exception
基类。
class ReturnFalseException(Exception):
"""由 :func:`catch_return_false_exception` 装饰器捕获的异常。"""
作用:
这个 ReturnFalseException
类的具体作用在于提供一种标准化的方式来处理函数中可能需要返回 False
的情况,特别是在使用了 catch_return_false_exception
装饰器的场景下。通常来说,Python 中的函数通过直接返回值来传达执行的结果,但是有时候可能需要在函数内部通过抛出异常的方式来进行控制流的操作。
例如,在某些框架或库的设计中,可能会有这样的需求:当一个函数执行失败时,不希望它抛出真正的错误(这可能会中断程序的执行流程),而是希望能够返回一个特定的值(在这里可能是 False
)来表示失败,同时允许上层代码能够优雅地处理这种情况。
通过定义 ReturnFalseException
并配合装饰器使用,可以实现这样的模式:
- 抛出自定义异常:在函数内部,当需要返回
False
时,可以抛出ReturnFalseException
。 - 捕获异常并返回
False
:装饰器catch_return_false_exception
可以捕获ReturnFalseException
并返回False
给调用者。
这样做的好处是可以统一处理逻辑,并且让代码更加清晰易懂。同时,也避免了在多个地方重复编写相同的错误处理逻辑。
1.4.1.12 类:TimelineHistory
- 定义了一个名为
TimelineHistory
的类,它继承自NamedTuple
。NamedTuple
是 Python 中的一个容器类,提供了创建简单类的方法,其中包含一些命名字段。
class TimelineHistory(NamedTuple):
"""代表 PostgreSQL 时间线历史文件的对象。
.. note::
*lines* 中的内容是从 *value* 中反序列化出来的,它们是从 PostgreSQL 时间线历史文件中解析出来的行,
包括时间线编号、时间线分割处的位置(LSN)以及文件中存在的任何其他字符串。这些文件是由 :func:`~patroni.postgresql.misc.parse_history` 函数解析的。
:ivar version: 文件的版本号。
:ivar value: 由从历史文件中解析出来的行组成的原始 JSON 序列化数据。
:ivar lines: 从历史文件中解析出来的行的 ``List``,每个元素都是一个 ``Tuple``。
"""
version: _Version
value: Any
lines: List[_HistoryTuple]
作用:
TimelineHistory
类的具体作用是在 Patroni 或类似的 PostgreSQL 集群管理工具中,用于表示和存储 PostgreSQL 时间线历史文件的信息。具体来说:
- 存储文件版本信息:
version
字段存储了文件的版本号,这对于跟踪文件的变化和兼容性非常重要。
- 存储原始序列化数据:
value
字段存储了从历史文件中解析出来的行的原始 JSON 序列化数据。这可能是为了方便后续处理或备份。
- 存储解析后的行数据:
lines
字段存储了从历史文件中解析出来的行数据,每一行都是一个_HistoryTuple
类型的元组。这使得数据更易于处理和查询。
1.4.1.12.1 from_node()
-
定义了一个名为
from_node
的静态方法。该方法接收两个参数:version
: 类型为_Version
,代表版本号。value
: 类型为str
,代表一个 JSON 序列化的字符串,该字符串包含从 PostgreSQL 时间线历史文件中解析出来的行。
方法返回类型为
TimelineHistory
,即返回一个TimelineHistory
类的实例。
@staticmethod
def from_node(version: _Version, value: str) -> 'TimelineHistory':
"""解析给定的 JSON 序列化的字符串为一系列时间线历史行。
:param version: 版本号
:param value: JSON 序列化的字符串,包含从 PostgreSQL 时间线历史文件中解析出来的行,
详见 :class:`TimelineHistory`。
:returns: 使用解析后的行组成的时间线历史对象。
:Example:
如果传递的 *value* 参数未被解析,则返回空的行列表:
>>> h = TimelineHistory.from_node(1, 2)
>>> h.lines
[]
"""
# 尝试使用 json.loads() 方法将 value 字符串反序列化为 Python 列表
try:
lines = json.loads(value)
assert isinstance(lines, list)
except (AssertionError, TypeError, ValueError):
lines: List[_HistoryTuple] = []
# 使用提供的 version 和 value 以及处理后的 lines 来创建并返回一个新的 TimelineHistory 实例
return TimelineHistory(version, value, lines)
作用:
from_node
方法的具体作用是从一个 JSON 序列化的字符串中解析出时间线历史行,并将其封装成一个 TimelineHistory
对象。该方法主要用于从节点获取时间线历史数据,并将其转换为 TimelineHistory
类型的对象,以便进一步处理和使用。
1.4.1.13 类:ClusterConfig
- 定义了一个名为
ClusterConfig
的类,它继承自NamedTuple
。NamedTuple
是 Python 标准库中的一个容器类,它创建的是不可变的对象,并且拥有命名属性,适合用来创建只读的数据结构。
class ClusterConfig(NamedTuple):
"""不可变的对象(namedtuple),表示集群配置。
:ivar version: 对象的版本号。
:ivar data: 配置信息的字典。
:ivar modify_version: 修改版本号。
"""
version: _Version
data: Dict[str, Any]
modify_version: _Version
作用:
ClusterConfig
类的具体作用是在应用程序中表示集群的配置信息。它作为一个不可变的对象,确保了配置信息一旦被创建就不能被改变,这对于并发环境下的数据一致性非常重要。
具体来说:
- 存储版本信息:
version
字段存储了配置对象的版本号,这对于追踪配置的变更历史非常有用。
- 存储配置数据:
data
字段是一个字典,包含了所有配置项及其对应的值。这种形式非常适合用来存储键值对形式的配置信息。
- 存储修改版本号:
modify_version
字段存储了配置最后一次被修改的版本号,这对于检测配置是否已被更改或者用于乐观锁机制非常有用。
1.4.1.13.1 from_node()
-
定义了一个名为
from_node
的静态方法。该方法接收三个参数:version
: 类型为_Version
,代表对象的版本号。value
: 类型为str
,代表一个 JSON 序列化的字符串,该字符串包含配置信息。modify_version
: 类型为Optional[_Version]
,代表修改版本号,默认值为None
。
方法返回类型为
ClusterConfig
,即返回一个ClusterConfig
类的实例。
@staticmethod
def from_node(version: _Version, value: str, modify_version: Optional[_Version] = None) -> 'ClusterConfig':
"""工厂方法,用于将 *value* 解析为配置信息。
:param version: 对象的版本号。
:param value: 原始 JSON 序列化的数据,如果无法解析则替换为空字典。
:param modify_version: 可选的修改版本号,如果没有提供则使用 *version*。
:returns: 构建的 :class:`ClusterConfig` 实例。
:Example:
>>> ClusterConfig.from_node(1, '{') is None
False
"""
# 尝试使用 json.loads() 方法将 value 字符串反序列化为 Python 字典
try:
data = json.loads(value)
assert isinstance(data, dict)
except (AssertionError, TypeError, ValueError):
data: Dict[str, Any] = {}
modify_version = 0
# 使用提供的 version、处理后的 data 以及处理后的 modify_version 来创建并返回一个新的 ClusterConfig 实例
return ClusterConfig(version, data, version if modify_version is None else modify_version)
作用:
from_node
方法的具体作用是从一个 JSON 序列化的字符串中解析出配置信息,并将其封装成一个 ClusterConfig
对象。该方法主要用于从节点获取配置数据,并将其转换为 ClusterConfig
类型的对象,以便进一步处理和使用。
1.4.1.14 类:Failover
- 定义了一个名为
Failover
的类,它继承自NamedTuple
。NamedTuple
是 Python 标准库中的一个容器类,它创建的是不可变的对象,并且拥有命名属性,适合用来创建只读的数据结构。
class Failover(NamedTuple):
"""不可变的对象(namedtuple),表示用于故障转移/切换功能所需的配置信息。
:ivar version: 对象的版本号。
:ivar leader: 领导者的名称。如果值非空,则视为从指定节点进行切换。
:ivar candidate: 被考虑作为故障转移候选者的成员节点的名称。
:ivar scheduled_at: 在进行切换的情况下,用于执行计划切换的 :class:`~datetime.datetime` 对象。
:Example:
>>> 'Failover' in str(Failover.from_node(1, '{"leader": "cluster_leader"}'))
True
>>> 'Failover' in str(Failover.from_node(1, {"leader": "cluster_leader"}))
True
>>> 'Failover' in str(Failover.from_node(1, '{"leader": "cluster_leader", "member": "cluster_candidate"}'))
True
>>> Failover.from_node(1, 'null') is None
False
>>> n = '''{"leader": "cluster_leader", "member": "cluster_candidate",
... "scheduled_at": "2016-01-14T10:09:57.1394Z"}'''
>>> 'tzinfo=' in str(Failover.from_node(1, n))
True
>>> Failover.from_node(1, None) is None
False
>>> Failover.from_node(1, '{}') is None
False
>>> 'abc' in Failover.from_node(1, 'abc:def')
True
"""
version: _Version
leader: Optional[str]
candidate: Optional[str]
scheduled_at: Optional[datetime.datetime]
作用:
Failover
类的具体作用是在应用程序中表示故障转移(failover)或切换(switchover)的配置信息。它作为一个不可变的对象,确保了配置信息一旦被创建就不能被改变,这对于并发环境下的数据一致性非常重要。
具体来说:
- 存储版本信息:
version
字段存储了配置对象的版本号,这对于追踪配置的变更历史非常有用。
- 存储领导者信息:
leader
字段存储了领导者的名称。如果值非空,则表示为从指定节点进行切换。
- 存储候选者信息:
candidate
字段存储了被考虑作为故障转移候选者的成员节点的名称。
- 存储计划切换时间:
scheduled_at
字段存储了在进行切换的情况下,用于执行计划切换的时间。
1.4.1.14.1 from_node()
-
定义了一个名为
from_node
的静态方法。该方法接收两个参数:version
: 类型为_Version
,代表对象的版本号。value
: 类型为Union[str, Dict[str, str]]
,代表 JSON 序列化的字符串或配置信息的字典。它也可以是一个冒号(:
)分隔的领导者名称和候选者名称的列表(遗留格式)。
方法返回类型为
Failover
,即返回一个Failover
类的实例。
@staticmethod
def from_node(version: _Version, value: Union[str, Dict[str, str]]) -> 'Failover':
"""工厂方法,用于将 *value* 解析为故障转移配置。
:param version: 对象的版本号。
:param value: JSON 序列化的数据或配置信息的字典。
也可以是一个冒号(``:``)分隔的领导者名称,后面跟着候选者名称(遗留格式)。
如果定义了 ``scheduled_at`` 键,则其值将通过 :func:`dateutil.parser.parse` 解析。
:returns: 构建的 :class:`Failover` 信息对象
"""
if isinstance(value, dict):
data: Dict[str, Any] = value
elif value:
try:
data = json.loads(value)
assert isinstance(data, dict)
except AssertionError:
data = {}
except ValueError:
t = [a.strip() for a in value.split(':')]
leader = t[0]
candidate = t[1] if len(t) > 1 else None
return Failover(version, leader, candidate, None)
else:
data = {}
if data.get('scheduled_at'):
data['scheduled_at'] = dateutil.parser.parse(data['scheduled_at'])
return Failover(version, data.get('leader'), data.get('member'), data.get('scheduled_at'))
作用:
from_node
方法的具体作用是从一个 JSON 序列化的字符串、配置信息的字典或者是冒号分隔的字符串中解析出故障转移配置信息,并将其封装成一个 Failover
对象。该方法主要用于从节点获取故障转移配置数据,并将其转换为 Failover
类型的对象,以便进一步处理和使用。
1.4.1.14.2 __len__()
- 定义了一个特殊方法
__len__
,该方法定义了如何计算Failover
实例的长度。这个方法返回一个整数,表示对象的“长度”。
def __len__(self) -> int:
"""实现 ``len`` 函数的能力。
.. note::
这个魔术方法有助于评估一个 :class:`Failover` 实例的“空”状态。例如:
>>> failover = Failover.from_node(1, None)
>>> len(failover)
0
>>> assert bool(failover) is False
>>> failover = Failover.from_node(1, {"leader": "cluster_leader"})
>>> len(failover)
1
>>> assert bool(failover) is True
这使得可以更容易地写 ``if cluster.failover`` 而不是写更长的语句。
"""
return int(bool(self.leader)) + int(bool(self.candidate))
作用:
__len__
方法的具体作用是定义如何计算 Failover
实例的“长度”。在这个上下文中,“长度”实际上是指 Failover
实例中非空属性的数量。具体来说:
- 计算非空属性数量:如果
leader
或candidate
有一个或两个都不为空,则__len__
方法返回的长度将是 1 或 2。 - 支持
len()
函数:这个方法使得可以直接使用内置的len()
函数来计算Failover
实例的有效属性数量,从而简化了判断实例是否为空的逻辑。 - 支持布尔上下文:由于 Python 中
bool(x)
和len(x)
之间存在联系(即如果len(x)
返回 0,则bool(x)
返回False
),因此这个方法还支持在布尔上下文中使用Failover
实例,如条件判断中。
1.4.1.15 类:Leader
- 定义了一个名为
Leader
的类,它继承自NamedTuple
。NamedTuple
是 Python 标准库中的一个容器类,它创建的是不可变的对象,并且拥有命名属性,适合用来创建只读的数据结构。
class Leader(NamedTuple):
"""表示领导者键的不可变对象(namedtuple)。
它包含以下字段:
:ivar version: 配置存储中领导者键的修改版本号。
:ivar session: 会话 ID 或只是 TTL(秒)
:ivar member: 指向表示当前领导者的 :class:`Member` 对象的引用(参见 :attr:`Cluster.members`)。
"""
version: _Version
session: _Session
member: Member
作用:
Leader
类的具体作用是在应用程序中表示集群中的领导者信息。它作为一个不可变的对象,确保了领导者信息一旦被创建就不能被改变,这对于并发环境下的数据一致性非常重要。
具体来说:
- 存储版本信息:
version
字段存储了配置存储中领导者键的修改版本号,这对于追踪配置的变更历史非常有用。
- 存储会话信息:
session
字段存储了会话 ID 或 TTL(生存时间)。在集群管理中,会话 ID 通常用于标识领导者选举的当前会话,而 TTL 则用于标识领导者心跳的超时时间。
- 存储成员信息:
member
字段存储了指向当前领导者的Member
对象的引用。这意味着领导者信息与集群成员信息相关联,可以通过成员信息来获取领导者的具体详情。
1.4.1.15.1 conn_kwargs()
- 定义了一个名为
conn_kwargs
的方法。该方法属于Leader
类,接收一个可选的参数auth
,类型为Optional[Dict[str, str]]
,默认值为None
。方法返回一个字典,类型为Dict[str, str]
。
def conn_kwargs(self, auth: Optional[Dict[str, str]] = None) -> Dict[str, str]:
"""连接关键字参数。
:param auth: 一个可选的字典,包含身份验证信息。
:returns: 调用 :meth:`Member.conn_kwargs` 方法的结果。
"""
return self.member.conn_kwargs(auth)
作用:
conn_kwargs
方法的具体作用是获取连接所需的关键词参数。具体来说:
- 传递身份验证信息:如果提供了
auth
参数,则将其传递给Member
对象的conn_kwargs
方法。这允许在连接时携带身份验证信息,例如用户名和密码。 - 返回连接参数:方法最终返回的是
Member
对象的conn_kwargs
方法的结果。这意味着实际的连接参数是由Member
对象提供的,而不是直接由Leader
类本身定义。
1.4.1.16 类:RemoteMember
- 定义了一个名为
RemoteMember
的类,继承自Member
类。
class RemoteMember(Member):
"""代表备用集群中的远程成员(通常是主节点)。
:cvar ALLOWED_KEYS: 控制对存储在 :attr:`~RemoteMember.data` 中的相关键名的访问。
"""
ALLOWED_KEYS: Tuple[str, ...] = (
'primary_slot_name',
'create_replica_methods',
'restore_command',
'archive_cleanup_command',
'recovery_min_apply_delay',
'no_replication_slot'
)
作用:
RemoteMember
类继承自 Member
类,其主要目的是为了表示一个备用集群中的远程成员,通常是主节点。此类中的 ALLOWED_KEYS
变量定义了一组特定的键名,这些键名可以用来控制对 RemoteMember
对象数据属性的访问权限。这有助于确保只有预期的数据能够被存取,从而增加了系统的安全性和数据的一致性。
1.4.1.16.1 __new__()
-
定义了一个名为
__new__
的类方法。该方法是 Python 中的一个特殊方法,用于创建一个新的实例。它接收两个参数:name
: 类型为str
,代表远程成员的名称。data
: 类型为Dict[str, Any]
,代表成员的信息字典,其中可以包含RemoteMember.ALLOWED_KEYS
中的键,以及成员连接信息api_url
和conn_kwargs
,还有槽位信息。
方法返回类型为
RemoteMember
的新实例。
def __new__(cls, name: str, data: Dict[str, Any]) -> 'RemoteMember':
"""工厂方法,用于从给定的 *name* 和 *data* 构造实例。
:param name: 远程成员的名称。
:param data: 成员信息的字典,可以包含 :const:`~RemoteMember.ALLOWED_KEYS` 中的键,
也可以包含成员连接信息 `api_url` 和 `conn_kwargs`,以及槽位信息。
:returns: 使用提供的参数构造的实例。
"""
return super(RemoteMember, cls).__new__(cls, -1, name, None, data)
作用:
__new__
方法的具体作用是在创建 RemoteMember
实例时初始化该实例。这个方法是 __init__
方法之前调用的构造方法,用于创建一个新的实例对象。在这个例子中,__new__
方法通过调用父类的 __new__
方法来创建一个新的实例,并传入默认的版本号 -1
、成员名称 name
、默认的会话信息 None
以及成员信息字典 data
。
1.4.1.16.2 __getattr__()
- 定义了一个名为
__getattr__
的方法。该方法是一个特殊的魔术方法,用于当试图访问类的实例上不存在的属性时的行为。它接收一个参数name
,类型为str
,并且返回类型为Any
的值。
def __getattr__(self, name: str) -> Any:
"""字典风格的键查找。
:param name: 要查找的键。
:returns: 如果键 *name* 在 :cvar:`~RemoteMember.ALLOWED_KEYS` 中,则返回 :attr:`~RemoteMember.data` 中 *name* 键的值,否则返回 ``None``。
"""
return self.data.get(name) if name in RemoteMember.ALLOWED_KEYS else None
作用:
__getattr__
方法的具体作用是在尝试访问 RemoteMember
实例上不存在的属性时,模拟字典的键查找行为。具体来说:
- 键查找:当尝试访问一个未定义的属性时,
__getattr__
方法会被调用,它会在data
字典中查找对应的键名。 - 权限控制:只有当键名存在于
ALLOWED_KEYS
中时,才会返回相应的值。这相当于一种权限控制机制,确保只能访问预先定义的键名。 - 返回值:如果找到了键名,则返回对应的值;如果没有找到,则返回
None
。
1.4.1.17 类:Member
- 定义了一个名为
Member
的类,该类继承自Tags
和NamedTuple
。NamedTuple
的定义采用了旧式的语法,使用元组来定义每个字段及其类型。
class Member(Tags, NamedTuple('Member',
[('version', _Version),
('name', str),
('session', _Session),
('data', Dict[str, Any])])):
"""表示 PostgreSQL 集群单个成员的不可变对象(namedtuple)。
.. note::
我们在这里使用了旧式的属性声明方式,因为如果不这样做的话,在 :class:`RemoteMember` 类中就无法重写 ``__new__`` 方法。
.. note::
数据中的这两个键总是被写入到 DCS 中,但在读取数据时会注意保持一致性和弹性:
``conn_url``: 包含主机、用户和密码的连接字符串,可用于访问该成员。
``api_url``: Patroni 实例的 REST API URL。
它包含以下字段:
:ivar version: 给定成员键在配置存储中的修改版本号。
:ivar name: PostgreSQL 集群成员的名称。
:ivar session: 会话 ID 或仅仅是 TTL(秒)。
:ivar data: 包含任意数据的字典,例如 ``conn_url``、``api_url``、``xlog_location``、``state``、``role``、``tags`` 等。
"""
作用:
Member
类的具体作用是表示 PostgreSQL 集群中的单个成员。它是一个不可变的对象,使用 NamedTuple
来创建,包含以下几个属性:
- 版本信息 (
version
):表示配置存储中给定成员键的修改版本号。这有助于跟踪成员信息的变化历史。 - 成员名称 (
name
):PostgreSQL 集群成员的名称,用于唯一标识每个成员。 - 会话信息 (
session
):可以是会话 ID 或者仅仅是 TTL(生存时间),用于表示会话的有效期。 - 数据 (
data
):包含任意数据的字典,这些数据可以包括连接字符串 (conn_url
)、REST API URL (api_url
)、日志位置 (xlog_location
)、状态 (state
)、角色 (role
)、标签 (tags
) 等。
1.4.1.17.1 from_node()
-
定义了一个名为
from_node
的静态方法,该方法接收四个参数:version
: 类型为_Version
,表示给定成员键在配置存储中的修改版本号。name
: 类型为str
,表示 PostgreSQL 集群成员的名称。session
: 类型为_Session
,表示会话 ID 或仅仅是 TTL(秒)。value
: 类型为str
,表示 JSON 编码的字符串,包含任意数据,如conn_url
、api_url
、xlog_location
、state
、role
、tags
等,或者是一个以postgres://
开头的连接 URL。
方法返回类型为
Member
的实例。
@staticmethod
def from_node(version: _Version, name: str, session: _Session, value: str) -> 'Member':
"""工厂方法,用于从 JSON 序列化的字符串或对象实例化 :class:`Member`。
:param version: 给定成员键在配置存储中的修改版本号。
:param name: PostgreSQL 集群成员的名称。
:param session: 会话 ID 或仅仅是 TTL(秒)。
:param value: 包含任意数据的 JSON 编码字符串,如 `conn_url`、`api_url`、`xlog_location`、`state`、`role`、`tags` 等,或者是一个以 `postgres://` 开头的连接 URL。
:returns: 使用给定参数构建的 :class:`Member` 实例。
:Example:
>>> Member.from_node(-1, '', '', '{"conn_url": "postgres://foo@bar/postgres"}') is not None
True
>>> Member.from_node(-1, '', '', '{')
Member(version=-1, name='', session='', data={})
"""
if value.startswith('postgres'):
conn_url, api_url = parse_connection_string(value)
data = {'conn_url': conn_url, 'api_url': api_url}
else:
try:
data = json.loads(value)
assert isinstance(data, dict)
except (AssertionError, TypeError, ValueError):
data: Dict[str, Any] = {}
return Member(version, name, session, data)
作用:
from_node
方法的具体作用是从给定的参数中创建 Member
实例。这个方法可以处理两种类型的输入:
- JSON 字符串:如果
value
是一个 JSON 字符串,那么它会被解析成字典,并从中提取数据来创建Member
实例。 - 连接字符串:如果
value
是一个以postgres://
开头的连接字符串,那么它会被解析成连接 URL 和 API URL,并创建一个包含这些信息的字典来构建Member
实例。
1.4.1.17.2 conn_kwargs()
- 定义了一个名为
conn_kwargs
的方法。该方法属于某个类(假设是Member
类),它接收一个可选的auth
参数,类型为Union[Any, Dict[str, Any], None]
,默认值为None
。方法返回类型为Dict[str, Any]
的字典。
def conn_kwargs(self, auth: Union[Any, Dict[str, Any], None] = None) -> Dict[str, Any]:
"""提供用于 PostgreSQL 连接设置的关键字参数。
:param auth: 认证属性 - 可以定义为 `psycopg2` 或 `psycopg` 模块支持的任何内容。
如果提供了 `username` 键,则将其转换为 `user` 键。
:returns: 包含默认参数键 `host`、`port` 和 `dbname` 与 `Member.data` 中 `conn_kwargs` 键内容合并的字典。
如果这些未定义,则将从 `Member.conn_url` 解析并重组连接参数。这两个属性之一需要有定义才能构造输出字典。
最后,*auth* 参数在返回前与字典合并。
"""
# 定义了一个包含默认连接参数的字典 defaults
defaults = {
"host": None,
"port": None,
"dbname": None
}
ret: Optional[Dict[str, Any]] = self.data.get('conn_kwargs')
# 如果 ret 有值(即 conn_kwargs 已经定义),则更新 defaults 字典,并将结果赋值给 ret
if ret:
defaults.update(ret)
ret = defaults
else:
conn_url = self.conn_url
if not conn_url:
return {} # due to the invalid conn_url we don't care about authentication parameters
r = urlparse(conn_url)
ret = {
'host': r.hostname,
'port': r.port or 5432,
'dbname': r.path[1:]
}
self.data['conn_kwargs'] = ret.copy()
# 应用任何剩余的认证参数。
# 如果 auth 参数存在且为字典类型,则将其与 ret 合并,并处理 username 键,将其转换为 user 键
if auth and isinstance(auth, dict):
ret.update({k: v for k, v in auth.items() if v is not None})
if 'username' in auth:
ret['user'] = ret.pop('username')
return ret
作用:
conn_kwargs
方法的具体作用是从当前成员对象中提取或构建用于连接 PostgreSQL 数据库的关键字参数。这个方法的逻辑如下:
- 默认参数初始化:首先定义了包含默认连接参数的字典
defaults
。 - 获取已存在的连接参数:尝试从
self.data
中获取已经定义的conn_kwargs
,如果有,则将其与默认参数合并。 - 从连接字符串解析参数:如果没有预定义的
conn_kwargs
,则从self.conn_url
解析连接参数,并保存到conn_kwargs
中。 - 应用认证参数:如果提供了
auth
参数,则将其与现有的连接参数合并,并处理username
键,将其转换为user
键。 - 返回最终的连接参数:返回构造好的关键字参数字典。
1.4.1.17.3 get_endpoint_url()
- 定义了一个名为
get_endpoint_url
的方法。该方法属于某个类(假设是Member
类),它接收一个可选的endpoint
参数,默认值为None
。方法返回类型为str
的字符串。
def get_endpoint_url(self, endpoint: Optional[str] = None) -> str:
"""从成员的 :attr:`~Member.api_url` 和指定的端点获取 URL。
:param endpoint: REST API 的 URL 路径。
:returns: 完整的 REST API URL。
"""
url = self.api_url or ''
if endpoint:
scheme, netloc, _, _, _, _ = urlparse(url)
url = urlunparse((scheme, netloc, endpoint, '', '', ''))
return url
作用:
get_endpoint_url
方法的具体作用是根据成员对象中的 api_url
属性和提供的 endpoint
参数构建完整的 REST API URL。这个方法的主要逻辑如下:
- 获取基础 URL:从成员对象中获取 REST API 的基础 URL(
api_url
)。 - 检查端点:如果提供了
endpoint
参数,则使用提供的端点路径替换基础 URL 中的路径部分。 - 返回完整 URL:返回构造好的完整 URL。
1.4.2 consul.py
1.4.2.1 get_dsc()
作用:
1.4.2.2 get_dsc()
作用:
1.4.2.3 get_dsc()
作用:
1.4.2.4 类:AbstractDCS
抽象类。
1.4.2.4.1 get_cluster()
作用:
1.4.2.5 类:AbstractDCS
抽象类。
1.4.2.4.1 get_cluster()
作用:
1.4.2.6 类:AbstractDCS
抽象类。
1.4.2.4.1 get_cluster()
作用:
1.4.2.7 类:AbstractDCS
抽象类。
1.4.2.4.1 get_cluster()
作用:
1.4.2.8 类:AbstractDCS
抽象类。
1.4.2.4.1 get_cluster()
作用:
1.4.2.9 类:AbstractDCS
抽象类。
1.4.2.4.1 get_cluster()
作用:
1.4.2.10 类:AbstractDCS
抽象类。
1.4.2.4.1 get_cluster()
作用:
1.4.2.11 类:AbstractDCS
抽象类。
1.4.2.4.1 get_cluster()
作用:
1.5 dynamic_loader.py
"""用于在一个包中搜索特定抽象接口实现的辅助函数。"""
存放查找包中特定抽象接口实现的辅助函数。其中包括三个函数iter_classes
、iter_modules
和find_class_in_module
。
iter_classes
函数:根据提供的包名和类类型,尝试导入并返回所有实现了指定类类型的模块。iter_modules
函数:根据提供的包名,动态地获取该包下所有模块的名称。find_class_in_module
函数:在给定的模块中查找一个特定类型的类,并且该类的名称与模块的名称相匹配。
1.5.1 iter_classes()
1.5.1.1 主体
- 定义了一个函数
iter_classes
,它接受三个参数:package
:一个字符串,表示要搜索模块的包名,例如"patroni.dcs"
。cls_type
:一个类类型,表示我们正在寻找的类类型。config
:一个可选参数,可以是Config
类型的对象或字典类型的对象,默认值为None
。
- 函数的返回类型是一个迭代器,每个迭代项是一个元组,元组包含两个元素:模块的
name
和导入的类对象。
def iter_classes(
package: str, cls_type: Type[ClassType],
config: Optional[Union[`Config`, Dict[str, Any]]] = None
) -> Iterator[Tuple[str, Type[ClassType]]]:
"""尝试导入存在于给定配置中的模块,并查找实现了 cls_type 的类。
注意事项:如果一个模块成功导入,我们可以假定其所有的依赖都已经安装。
package:一个包名,用于搜索模块,例如 patroni.dcs。
cls_type:我们要寻找的类类型。
config:配置信息,其中可能包含模块名称作为键。如果提供了 config,则仅尝试导入配置中定义的模块。否则,如果为 None,则尝试导入任何支持的模块。
:生成器说明:返回一个元组,包含模块的 name 和导入的类对象。
"""
for mod_name in iter_modules(package):
name = mod_name.rpartition(`.`)[2]
if config is None or name in config:
try:
# 尝试导入模块
module = importlib.import_module(mod_name)
module_cls = find_class_in_module(module, cls_type)
if module_cls:
yield name, module_cls
except ImportError:
logger.log(logging.DEBUG if config is not None else logging.INFO,
`Failed to import %s`, mod_name)
流程说明:
- 遍历
iter_modules
函数返回的所有模块名称,该函数用于获取指定包下的所有模块名称。 - 获取模块名称,去除包路径部分,保留最后一个部分作为模块名称。
- 如果
config
为None
或者模块名称在config
中出现,则继续处理。- 尝试导入模块。
- 使用
find_class_in_module
函数查找模块中实现了cls_type
的类。 - 如果找到了符合条件的类,则返回一个元组,包含模块名称和该类对象。
- 如果导入模块失败,则记录一条调试信息或信息级别日志,取决于
config
是否为None
。
1.5.1.2 作用
iter_classes
函数的作用是根据提供的包名和类类型,尝试导入并返回所有实现了指定类类型的模块。具体来说:
- 模块发现:
- 通过
iter_modules
函数获取指定包下的所有模块名称。
- 通过
- 模块筛选:
- 如果提供了
config
,则仅考虑那些名称出现在config
中的模块。 - 如果
config
为None
,则尝试导入所有模块。
- 如果提供了
- 模块导入:
- 使用
importlib.import_module
动态导入模块。 - 如果导入失败,记录相应的日志信息。
- 使用
- 类查找:
- 使用
find_class_in_module
函数查找模块中实现了cls_type
的类。 - 如果找到了符合条件的类,则返回一个元组,包含模块名称和该类对象。
- 使用
通过这种方式,iter_classes
函数可以动态地发现并导入所有实现了指定类类型的模块,这对于构建高度灵活的应用程序非常有用,特别是那些需要根据配置动态选择不同模块实现的应用程序。这样可以确保应用程序可以根据实际情况使用最适合的模块实现,同时保持代码的简洁性和可维护性。
1.5.2 iter_modules()
1.5.2.1 主体
- 定义了一个函数
iter_modules
,它接受一个字符串参数package
,表示要搜索模块的包名,例如"patroni.dcs"
。 - 函数的返回类型是一个字符串列表,每个字符串代表一个模块的完整路径。
def iter_modules(package: str) -> List[str]:
"""根据执行环境获取来自指定包的模块名称。
注意事项:如果使用 PyInstaller 打包,模块不能通过扫描源目录动态发现,因为 importlib.machinery.FrozenImporter 不实现 iter_modules 方法。但是,仍然可以通过遍历 toc(包含所有“冻结”资源的列表)来找到所有潜在的模块。
参数说明:package 是一个包名,用于搜索模块,例如 patroni.dcs。
返回值说明:返回一个字符串列表,其中每个字符串代表一个已知模块的名称,并带有绝对的 Python 模块路径命名空间,例如 patroni.dcs.etcd。
"""
module_prefix = package + `.`
if getattr(sys, `frozen`, False):
toc: Set[str] = set()
# dirname可能包含一些点,这会导致pkgutil.iter_importer ()
# 将路径误解为包名。这是可以避免的
# 完全不传递路径,因为PyInstaller的
# FrozenImporter是一个单例,注册为顶级查找器。
for importer in pkgutil.iter_importers():
if hasattr(importer, `toc`):
toc |= getattr(importer, `toc`)
dots = module_prefix.count(`.`) # 只搜索同一级别的模块
return [module for module in toc if module.startswith(module_prefix) and module.count(`.`) == dots]
# 这里我们假设调用这个函数的包已经被导入
pkg_file = sys.modules[package].__file__
if TYPE_CHECKING: # pragma: no cover
assert isinstance(pkg_file, str)
return [name for _, name, is_pkg in pkgutil.iter_modules([os.path.dirname(pkg_file)], module_prefix) if not is_pkg]
流程说明:
- 初始化模块前缀,用于后续拼接模块完整路径。
- 检查是否在使用 PyInstaller 打包的应用环境中运行。
sys.frozen
属性在使用 PyInstaller 打包的应用程序中为True
。- 初始化一个集合
toc
来存储所有“冻结”的资源名称。 - 避免由于路径中包含多个点而导致
pkgutil.iter_importers()
错误地将路径解释为包名。不传递路径,因为 PyInstaller 的FrozenImporter
是单例,并且注册为顶层查找器。 - 遍历导入器,并检查是否有
toc
属性,如果有,则将toc
中的内容合并到toc
集合中。 - 计算模块前缀中的点的数量,以确定模块所在的层级。
- 返回一个列表,其中包含所有以
module_prefix
开头且层级相同的模块名称。
- 初始化一个集合
- 假设调用此函数的包已经被导入。
- 获取包的文件路径。
- 类型检查时确保
pkg_file
是字符串类型。
- 返回一个列表,其中包含所有位于
pkg_file
所在目录下的非包模块,并且模块名称以module_prefix
开头。
1.5.2.2 作用
iter_modules
函数的作用是根据提供的包名,动态地获取该包下所有模块的名称。具体来说:
- 环境检测:
- 如果程序是使用 PyInstaller 打包的,则使用不同的策略来获取模块列表,因为打包后的程序不会像普通 Python 程序那样拥有动态发现模块的能力。
- 模块名称获取:
- 如果不是打包程序,则通过
pkgutil.iter_modules
获取指定目录下的所有模块名称。 - 如果是打包程序,则通过访问
FrozenImporter
的toc
属性来获取所有“冻结”的资源名称,并从中筛选出所需层级的模块名称。
- 如果不是打包程序,则通过
- 返回结果:
- 最终返回一个列表,其中包含所有符合条件的模块名称,并带有绝对的 Python 模块路径命名空间。
通过这种方式,iter_modules
函数能够适应不同的执行环境,无论是普通的 Python 环境还是使用 PyInstaller 打包后的环境,都能正确地获取到包内的模块列表。这对于动态加载模块或需要根据环境变化调整模块加载策略的应用程序来说非常重要。
1.5.3 find_class_in_module()
1.5.3.1 主体
- 定义了一个函数
find_class_in_module
,它接受两个参数:module
:一个已经导入的模块。cls_type
:我们要查找的类类型。
- 函数的返回类型是一个类类型,如果找到,则返回该类型;如果没有找到,则返回
None
。
def find_class_in_module(module: ModuleType, cls_type: Type[ClassType]) -> Optional[Type[ClassType]]:
"""尝试在指定的模块中查找与模块名称匹配的 cls_type 类接口的实现。
module:已经导入的模块。
cls_type:我们要查找的类类型。
返回值说明:返回一个类,该类名称与模块名称匹配并且实现了 cls_type;如果没有找到,则返回 None。
"""
module_name = module.__name__.rpartition(`.`)[2]
return next(
(obj for obj_name, obj in module.__dict__.items()
if (obj_name.lower() == module_name
and inspect.isclass(obj) and issubclass(obj, cls_type))),
None)
流程说明:
- 获取模块名称的最后一部分,去除包路径部分。例如,如果模块全路径为
patroni.dcs.etcd
,则module_name
将为etcd
。 - 使用生成器表达式遍历模块的
__dict__
属性中的所有项,其中__dict__
包含模块的所有属性。 - 对于每一对
(obj_name, obj)
,检查以下条件:obj_name.lower() == module_name
:对象名称(转为小写)是否与模块名称相同。inspect.isclass(obj)
:对象是否为类。issubclass(obj, cls_type)
:对象是否为cls_type
的子类。
- 如果找到满足上述条件的对象,则返回该对象;如果没有找到,则返回
None
。
1.5.3.2 作用
find_class_in_module
函数的作用是在给定的模块中查找一个特定类型的类,并且该类的名称与模块的名称相匹配。具体来说:
- 模块名称提取:
- 获取模块名称的最后一部分,以便与模块中的类名称进行比较。
- 类查找:
- 遍历模块的
__dict__
属性中的所有项。 - 对于每个项,检查其名称是否与模块名称匹配,并且它是一个类,并且是
cls_type
的子类。 - 如果找到符合条件的类,则返回该类。
- 遍历模块的
- 返回结果:
- 如果找到了符合条件的类,则返回该类。
- 如果没有找到符合条件的类,则返回
None
。
通过这种方式,find_class_in_module
函数可以帮助动态地发现模块中的特定类型类,并确保该类的名称与模块名称一致,这对于需要动态加载特定实现的情况非常有用,尤其是在需要根据配置文件或环境动态选择模块中的类时。
1.6 request.py
"""处理与 Patroni 的 REST API 通信的设施。"""
处理与Patroni的REST API通信的工具。
PatroniRequest
类:它是用于向 Patroni 的 REST API 发送请求的封装类。在执行请求之前,会根据配置设置准备请求管理器。_insecure
属性:如何处理 SSL 证书验证。- 如果为 True,则执行 REST API 请求时不验证 SSL 证书;
- 如果为 False,则执行 REST API 请求时验证 SSL 证书;
- 如果为 None,则根据 ctl.insecure 配置项的行为决定;
- 如果以上都不适用,则默认为 False。
_pool
属性:PatroniPoolManager
实例,设置连接池数量和最大连接数__init__
函数:构造函数初始化 PatroniRequest 实例,并接受配置信息 config 和 SSL 证书验证选项 insecure。_get_ctl_value
函数:从配置的ctl
部分获取指定名称的设置值。_get_restapi_value
函数:从配置的restapi
部分获取指定名称的设置值。_apply_pool_param
函数:在请求管理器中配置指定的参数及其值。_apply_ssl_file_param
函数:应用与 SSL 相关的参数到请求管理器。reload_config
:根据配置信息重新加载请求管理器request
函数:执行 HTTP 请求。__call__
函数:将PatroniRequest
类转换为可调用对象。
PatroniRequest
类:__init__
函数:调用父类构造函数,覆盖连接池类映射。
PatroniRequest
类:_validate_conn
函数:重写父类中的相应方法,以抑制有关未启用证书验证的请求的警告信息。
get
函数:执行一个HTTP GET请求。
1.6.1 类:PatroniRequest
1.6.1.1 主体
class PatroniRequest(object):
"""定义了一个名为 PatroniRequest 的类,它是用于向 Patroni 的 REST API 发送请求的封装类。
在执行请求之前,会根据配置设置准备请求管理器。
"""
def __init__(self, config: Union[Config, Dict[str, Any]], insecure: Optional[bool] = None) -> None:
"""构造函数初始化 PatroniRequest 实例,并接受配置信息 config 和 SSL 证书验证选项 insecure。
config:Patroni 的 YAML 配置。
insecure:如何处理 SSL 证书验证:
如果为 True,则执行 REST API 请求时不验证 SSL 证书;
如果为 False,则执行 REST API 请求时验证 SSL 证书;
如果为 None,则根据 ctl.insecure 配置项的行为决定;
如果以上都不适用,则默认为 False。
"""
# 初始化 _insecure 变量。
# 创建 PatroniPoolManager 实例,设置连接池数量和最大连接数。
# 调用 reload_config 方法,根据配置信息重新加载请求管理器
self._insecure = insecure
self._pool = PatroniPoolManager(num_pools=10, maxsize=10)
self.reload_config(config)
@staticmethod
def _get_ctl_value(config: Union[Config, Dict[str, Any]], name: str, default: Any = None) -> Optional[Any]:
"""静态方法 _get_ctl_value 从配置的 ctl 部分获取指定名称的设置值。
config:Patroni 的 YAML 配置。
name:要检索的设置值的名称。
返回 ctl.*name* 的值,如果不存在则返回 None。
"""
# # 获取配置中 ctl 字段的 name 设置值,如果不存在则返回默认值
return config.get(`ctl`, {}).get(name, default)
@staticmethod
def _get_restapi_value(config: Union[Config, Dict[str, Any]], name: str) -> Optional[Any]:
"""静态方法 _get_restapi_value 从配置的 restapi 部分获取指定名称的设置值。
config:Patroni 的 YAML 配置。
name:要检索的设置值的名称。
返回 restapi -> *name* 的值,如果不存在则返回 None。
"""
#获取配置中 restapi 字段的 name 设置值,如果不存在则返回 None
return config.get(`restapi`, {}).get(name)
def _apply_pool_param(self, param: str, value: Any) -> None:
"""方法 _apply_pool_param 在请求管理器中配置指定的参数及其值。
param:要更改的设置名称。
value:新的参数值。如果为 None、0、False 等类似值,则显式声明的参数被移除,此时采用默认值(如果有)。
"""
# 如果 value 存在,则设置请求管理器中的参数值;否则,从请求管理器中删除该参数
if value:
self._pool.connection_pool_kw[param] = value
else:
self._pool.connection_pool_kw.pop(param, None)
def _apply_ssl_file_param(self, config: Union[Config, Dict[str, Any]], name: str) -> Union[str, None]:
"""方法 _apply_ssl_file_param 应用与 SSL 相关的参数到请求管理器。
config:Patroni 的 YAML 配置。
name:Patroni SSL 相关设置名称的前缀。目前支持:
cert:转换为 certfile
key:转换为 keyfile
尝试首先从 ctl 部分获取请求的密钥。
返回 ctl.*name*file 的值,如果不存在则返回 None。
"""
# 获取 ctl 部分的 SSL 文件设置值,并应用到请求管理器中
value = self._get_ctl_value(config, name + `file`)
self._apply_pool_param(name + `_file`, value)
return value
def reload_config(self, config: Union[Config, Dict[str, Any]]) -> None:
"""方法 reload_config 根据配置信息重新加载请求管理器。
配置 HTTP 请求头部:
authorization:基于 Patroni 的 CTL 或 REST API 认证配置;
user-agent:基于 patroni.utils.USER_AGENT。
配置 SSL 相关设置:
如果 ctl.cacert 或 restapi.cafile 可用,则配置 ca_certs;
如果 ctl.certfile 可用,则配置 cert、key 和 key_password。
config:Patroni 的 YAML 配置。
"""
# ``ctl -> auth`` 相当于 ``ctl -> authentication -> username`` + ``:`` +
# ``ctl -> authentication -> password``. ``restapi -> auth``也是一样
# 获取基本身份验证信息 basic_auth,优先从 ctl 配置获取,如果没有则从 restapi 配置获取
basic_auth = self._get_ctl_value(config, `auth`) or self._get_restapi_value(config, `auth`)
# 设置请求管理器的 headers 属性,使用 urllib3.make_headers 方法根据 basic_auth 和 USER_AGENT 生成 HTTP 头
self._pool.headers = urllib3.make_headers(basic_auth=basic_auth, user_agent=USER_AGENT)
# 设置 SSL 证书要求为 CERT_REQUIRED,表示请求时需要验证服务器证书
self._pool.connection_pool_kw[`cert_reqs`] = `CERT_REQUIRED`
# 获取是否启用不安全模式的配置
insecure = self._insecure if isinstance(self._insecure, bool)\
else self._get_ctl_value(config, `insecure`, False)
# 检查是否应用了 SSL 证书文件配置
# 如果应用了证书文件配置 (`cert`)
if self._apply_ssl_file_param(config, `cert`):
if insecure: # The assert_hostname = False helps to silence warnings
# 如果启用了不安全模式,则设置 assert_hostname = False 以减少警告信息
self._pool.connection_pool_kw[`assert_hostname`] = False
# 应用密钥文件配置 (`key`)
self._apply_ssl_file_param(config, `key`)
# 获取密钥文件密码,并应用到请求管理器
password = self._get_ctl_value(config, `keyfile_password`)
self._apply_pool_param(`key_password`, password)
else:
if insecure:
# 如果禁用了证书验证,则禁用服务器证书验证。
self._pool.connection_pool_kw[`cert_reqs`] = `CERT_NONE`
# 移除 assert_hostname 和 key_file 配置项(如果存在)
self._pool.connection_pool_kw.pop(`assert_hostname`, None)
self._pool.connection_pool_kw.pop(`key_file`, None)
# 获取 CA 证书路径配置,优先从 ctl 配置获取,如果没有则从 restapi 配置获取
cacert = self._get_ctl_value(config, `cacert`) or self._get_restapi_value(config, `cafile`)
# 应用 CA 证书路径配置到请求管理器
self._apply_pool_param(`ca_certs`, cacert)
def request(self, method: str, url: str, body: Optional[Any] = None,
**kwargs: Any) -> urllib3.response.HTTPResponse:
"""方法 request 执行 HTTP 请求。
method:要使用的 HTTP 方法,例如 GET。
url:要请求的 URL。
body:用作请求正文的任何内容。
kwargs:传递给 urllib3.PoolManager.request 的关键字参数。
返回请求返回的响应。
"""
# 如果 body 不为空且不是字符串,则将其序列化为 JSON 字符串。
if body is not None and not isinstance(body, str):
body = json.dumps(body)
# 使用请求管理器执行 HTTP 请求,并返回响应。
return self._pool.request(method.upper(), url, body=body, **kwargs)
def __call__(self, member: Member, method: str = `GET`, endpoint: Optional[str] = None,
data: Optional[Any] = None, **kwargs: Any) -> urllib3.response.HTTPResponse:
"""方法 __call__ 将 PatroniRequest 类转换为可调用对象。
当被调用时,通过请求管理器执行请求。
member:DCS 成员,可以从其中获取配置的基本 URL 用于 REST API。
method:要使用的 HTTP 方法,例如 GET。
endpoint:此请求的 URL 路径,例如 switchover。
data:用作请求正文的任何内容。
返回请求返回的响应。
"""
# 获取请求的完整 URL,并执行请求,返回响应
url = member.get_endpoint_url(endpoint)
return self.request(method, url, data, **kwargs)
1.6.1.2 作用
PatroniRequest
类的作用是封装对 Patroni REST API 的请求操作。具体来说:
- 初始化:
- 接受配置信息和 SSL 证书验证选项,并初始化请求管理器。
- 配置加载:
- 从配置文件中读取必要的信息,并根据这些信息配置请求管理器,包括设置 SSL 证书、认证信息等。
- 请求执行:
- 提供
request
方法来执行具体的 HTTP 请求,并返回响应。 - 通过
__call__
方法使类实例可调用,简化请求过程。
- 提供
通过这种方式,PatroniRequest
类提供了一个统一的接口来发送请求到 Patroni 的 REST API,并根据配置自动处理 SSL 证书验证、认证等细节,使得外部代码可以更简单地与 Patroni 交互。
1.6.2 类:PatroniPoolManager
1.6.2.1 主体
该类继承自 urllib3.PoolManager
。
class PatroniPoolManager(urllib3.PoolManager):
def __init__(self, *args: Any, **kwargs: Any) -> None:
# 调用父类 urllib3.PoolManager 的构造函数,传入 *args 和 **kwargs,完成基础构造。
super(PatroniPoolManager, self).__init__(*args, **kwargs)
# 初始化 pool_classes_by_scheme 字典,用于指定不同协议对应的连接池类:
# http 协议对应 urllib3.HTTPConnectionPool 类。
# https 协议对应 HTTPSConnectionPool 类。
self.pool_classes_by_scheme = {`http`: urllib3.HTTPConnectionPool, `https`: HTTPSConnectionPool}
1.6.2.2 作用
PatroniPoolManager
类的具体作用如下:
- 扩展
urllib3.PoolManager
类:PatroniPoolManager
继承自urllib3.PoolManager
,这意味着它可以使用urllib3.PoolManager
提供的所有功能,并在此基础上进行扩展。
- 初始化连接池类映射:
- 在构造函数中,
PatroniPoolManager
通过覆盖pool_classes_by_scheme
字典来指定不同的协议对应的连接池类。 - 这样做的目的是为了能够更灵活地管理和配置 HTTP 和 HTTPS 连接池,特别是在 Patroni 应用程序中有特定的需求时。
- 在构造函数中,
- 协议特定的连接池管理:
- 通过指定
http
和https
协议对应的连接池类,可以在处理不同类型的请求时使用适当的连接池。 - 这对于优化网络请求性能和安全性非常重要,特别是在处理 SSL/TLS 加密的 HTTPS 请求时。
- 通过指定
1.6.3 类:HTTPSConnectionPool
HTTPSConnectionPool
类的作用是扩展urllib3
库中的HTTPSConnectionPool
类,并重写_validate_conn
方法来抑制有关未启用证书验证的请求的警告信息。具体来说:- 扩展
HTTPSConnectionPool
:HTTPSConnectionPool
是urllib3
库中的一个类,用于管理与特定主机的 HTTPS 连接。通过继承这个类,可以利用其所有的功能,并在此基础上增加新的行为或覆盖已有的方法。
- 重写
_validate_conn
方法:_validate_conn
方法原本是用来验证连接是否有效。在这里,我们重写了这个方法,目的是在不进行证书验证的情况下,抑制可能出现的相关警告信息。这样做的常见场景是当开发者在开发或测试环境中关闭了SSL证书验证,但又不希望每次请求时都出现警告信息。
- 扩展
class HTTPSConnectionPool(urllib3.HTTPSConnectionPool):
1.6.3.1 _validate_conn()
- 定义了一个名为
_validate_conn
的实例方法,该方法接受任意数量的位置参数*args
和关键字参数**kwargs
,并且没有返回值。
def _validate_conn(self, *args: Any, **kwargs: Any) -> None:
"""重写父类的方法来抑制关于未启用证书验证的请求的警告。"""
作用:
_validate_conn
方法的具体作用是重写父类中的相应方法,以抑制有关未启用证书验证的请求的警告信息。具体来说:
- 重写父类方法:
- 该方法覆盖了父类中的
_validate_conn
方法。这意味着它改变了父类中原有的行为。
- 该方法覆盖了父类中的
- 抑制警告信息:
- 根据文档字符串描述,该方法的主要目的是为了消除因请求时未启用证书验证而产生的警告信息。这通常是在某些情况下,例如出于测试目的或特定的安全策略选择禁用了SSL证书验证时,可能会遇到的情况。
1.6.4 get()
-
定义了一个名为
get
的函数,该函数接受以下参数:url: str
:一个字符串类型的参数,代表要请求的完整URL。verify: bool = True
:一个布尔类型的参数,默认值为True
,指示是否在处理请求时验证SSL证书。**kwargs: Any
:接受任意数量的关键字参数,通常用于传递额外的HTTP请求选项。
该函数返回一个
urllib3.response.HTTPResponse
类型的对象,即HTTP响应。
def get(url: str, verify: bool = True, **kwargs: Any) -> urllib3.response.HTTPResponse:
"""执行一个HTTP GET请求。
.. note::
它使用了 :class:`PatroniRequest`,所以在处理请求之前会应用所有相关的配置。
:param url: 此GET请求的完整URL。
:param verify: 是否在处理请求时验证SSL证书。
:returns: 请求返回的响应。
"""
http = PatroniRequest({}, not verify)
return http.request('GET', url, **kwargs)
作用:
get
函数的具体作用是执行一个HTTP GET请求。具体来说:
- 创建请求对象:
- 使用
PatroniRequest
类创建一个请求对象,并传递一个空字典作为配置,以及一个布尔值not verify
,该布尔值决定是否验证SSL证书。
- 使用
- 发送请求:
- 使用创建的请求对象的
request
方法发送一个HTTP GET请求到指定的url
,并将**kwargs
中的任意关键字参数传递给请求方法。
- 使用创建的请求对象的
- 返回响应:
- 返回请求的响应结果。
1.7 daemon.py
get_base_arg_parser
函数:创建一个基本的命令行参数解析器abstract_main
函数:AbstractPatroniDaemon
类:
1.7.1 get_base_arg_parser()
1.7.1.1 主体
- 定义一个名为
get_base_arg_parser
的函数,该函数返回一个argparse.ArgumentParser
对象。
def get_base_arg_parser() -> argparse.ArgumentParser:
"""函数的文档字符串描述了该函数的作用:
创建一个基本的命令行参数解析器,包含用于 Patroni 和 Raft 控制器守护进程的基本参数。
返回一个 argparse.ArgumentParser 对象。
"""
from .config import Config
from .version import __version__
# 创建一个 argparse.ArgumentParser 对象,用于解析命令行参数
parser = argparse.ArgumentParser()
# 添加 --version 命令行参数
parser.add_argument(`--version`, action=`version`, version=`%(prog)s {0}`.format(__version__))
# 添加 configfile 命令行参数
parser.add_argument(`configfile`, nargs=`?`, default=``,
help=`Patroni may also read the configuration from the {0} environment variable`
.format(Config.PATRONI_CONFIG_VARIABLE))
return parser
1.7.1.2 作用
这个 get_base_arg_parser
函数的作用是创建一个基本的命令行参数解析器,用于处理以下两个主要任务:
- 版本信息:
- 当用户传递
--version
参数时,自动打印版本信息并退出。
- 当用户传递
- 配置文件路径:
- 允许用户通过命令行参数指定配置文件的路径。如果用户没有提供配置文件路径,则使用默认值(空字符串)。此外,还允许从环境变量读取配置文件路径。
这个函数可以被其他模块调用,以便在启动 Patroni 或 Raft 控制器守护进程时解析基本的命令行参数。通过这种方式,可以简化命令行工具的使用,并提供一致的参数处理方式。
1.7.2 abstract_main()
1.7.2.1 主体
- 定义了一个名为
abstract_main
的函数,接受两个参数:cls
和configfile
。cls
参数应该是继承自AbstractPatroniDaemon
的类,configfile
是配置文件的路径。该函数没有返回值(返回类型为None
)。
def abstract_main(cls: Type[AbstractPatroniDaemon], configfile: str) -> None:
"""创建给定守护进程的主要入口点。
cls: 应该继承自 :class:AbstractPatroniDaemon 的类。
configfile:
"""
from .config import Config, ConfigParseError
try:
# 试使用提供的 configfile 路径创建一个 Config 实例。
config = Config(configfile)
except ConfigParseError as e:
sys.exit(e.value)
# 使用提供的配置创建 cls 类的一个实例
controller = cls(config)
try:
# 尝试调用 controller 实例的 run 方法来启动守护进程。
controller.run()
except KeyboardInterrupt:
pass
finally:
# controller 实例的 shutdown 方法来关闭守护进程。
controller.shutdown()
1.7.2.2 作用
这个函数的作用是作为给定守护进程的主要入口点。它的主要功能如下:
- 配置文件解析:
- 使用提供的配置文件路径创建
Config
实例。如果配置文件解析失败,则通过sys.exit()
退出程序,并输出错误信息。
- 使用提供的配置文件路径创建
- 守护进程实例化:
- 使用
Config
实例创建一个继承自AbstractPatroniDaemon
的类的实例。
- 使用
- 守护进程启动:
- 调用守护进程实例的
run
方法来启动守护进程。如果过程中接收到键盘中断,则捕获异常但不做进一步处理。
- 调用守护进程实例的
- 守护进程关闭:
- 无论守护进程是否正常运行完毕,都会调用守护进程实例的
shutdown
方法来关闭守护进程,确保资源得到释放。
- 无论守护进程是否正常运行完毕,都会调用守护进程实例的
通过这个函数,可以简化启动一个守护进程的过程,使得守护进程可以从配置文件加载配置,并在异常情况下能够优雅地关闭,保证程序的健壮性和可用性。
1.7.3 类:AbstractPatroniDaemon
1.7.3.1 主体
该类继承自 urllib3.PoolManager
。
__init__
函数:构造函数,用于初始化信号处理程序、日志处理器和配置。sighup_handler
函数:用于处理 SIGHUP 信号,并标记守护进程已收到 SIGHUP 信号。api_sigterm
函数:保证只有一个 SIGTERM 正在处理中,并通过锁定机制标记守护进程已收到 SIGTERM 信号。sigterm_handler
函数:用于处理 SIGTERM 信号,并通过api_sigterm
方法终止守护进程。setup_signal_handlers
函数:用于设置守护进程的信号处理程序,包括 SIGHUP 和 SIGTERM 信号。received_sigterm
函数:用于检查守护进程是否收到了 SIGTERM 信号。reload_config
函数:用于重新加载配置。如果local
参数为True
,则重新加载本地配置文件中的日志配置。_run_cycle
函数:抽象方法,该方法应在每次执行周期中被调用,并定义守护进程在每次执行周期中应执行的操作。run
函数:于启动守护进程,并在收到 SIGTERM 信号之前持续执行执行周期。如果收到 SIGHUP 信号,则重新加载配置。_shutdown
函数:抽象方法,该方法应在守护进程关闭时定义要执行的操作。shutdown
函数:用于在收到 SIGTERM 信号时关闭守护进程,并关闭日志线程。
class AbstractPatroniDaemon(abc.ABC):
"""一个 Patroni 守护进程。
.. note::
当继承自 :class:`AbstractPatroniDaemon` 时,你应当定义方法 :func:`_run_cycle`
来决定每次执行周期应该做什么,以及定义方法 :func:`_shutdown` 来决定关闭时应该做什么。
:ivar logger: 此守护进程使用的日志处理器。
:ivar config: 此守护进程的配置选项。
"""
def __init__(self, config: `Config`) -> None:
"""设置信号处理程序、日志处理器和配置。
:param config: 此守护进程的配置选项。
"""
from patroni.log import PatroniLogger
self.setup_signal_handlers()
self.logger = PatroniLogger()
self.config = config
AbstractPatroniDaemon.reload_config(self, local=True)
def sighup_handler(self, *_: Any) -> None:
"""处理 SIGHUP 信号。
标记守护进程为“收到 SIGHUP
"""
self._received_sighup = True
def api_sigterm(self) -> bool:
"""保证只有一个 SIGTERM 正在处理中。
标记守护进程已收到 SIGTERM 信号。
:returns: 如果守护进程被标记为 "SIGTERM 收到" 则返回 ``True``。
"""
ret = False
with self._sigterm_lock:
if not self._received_sigterm:
self._received_sigterm = True
ret = True
return ret
def sigterm_handler(self, *_: Any) -> None:
"""处理 SIGTERM 信号。
通过 :func:`api_sigterm` 终止守护进程。
"""
if self.api_sigterm():
sys.exit()
def setup_signal_handlers(self) -> None:
"""设置守护进程的信号处理程序。
设置 SIGHUP 和 SIGTERM 信号处理程序。
.. note::
SIGHUP 仅在非 Windows 环境中处理。
"""
self._received_sighup = False
self._sigterm_lock = Lock()
self._received_sigterm = False
if os.name != `nt`:
signal.signal(signal.SIGHUP, self.sighup_handler)
signal.signal(signal.SIGTERM, self.sigterm_handler)
@property
def received_sigterm(self) -> bool:
"""如果守护进程收到了 SIGTERM 信号。"""
with self._sigterm_lock:
return self._received_sigterm
def reload_config(self, sighup: bool = False, local: Optional[bool] = False) -> None:
"""重新加载配置。
:param sighup: 如果它与 SIGHUP 信号有关。
sighup 参数可以在子类中重写的方法中使用。
:param local: 如果本地配置文件中有更改,则为 ``True``。
"""
if local:
self.logger.reload_config(self.config.get(`log`, {}))
@abc.abstractmethod
def _run_cycle(self) -> None:
"""定义守护进程在每次执行周期中应做什么。
在守护进程的主循环中持续被调用,直到守护进程最终被终止。
"""
def run(self) -> None:
"""运行守护进程。
启动日志线程,并在收到 SIGTERM 信号之前持续运行执行周期。收到 SIGHUP 后也重新加载配置。
"""
self.logger.start()
while not self.received_sigterm:
if self._received_sighup:
self._received_sighup = False
self.reload_config(True, self.config.reload_local_configuration())
self._run_cycle()
@abc.abstractmethod
def _shutdown(self) -> None:
"""定义守护进程在关闭时应做什么。"""
def shutdown(self) -> None:
"""当收到 SIGTERM 信号时关闭守护进程。
关闭守护进程和日志线程。
"""
with self._sigterm_lock:
self._received_sigterm = True
self._shutdown()
self.logger.shutdown()
1.7.3.2 作用
这个类的作用是提供一个抽象的基础框架来构建 Patroni 守护进程。它包含了一系列的方法,用于处理信号、设置日志、加载配置等基础任务,并且要求子类实现具体的 _run_cycle
和 _shutdown
方法来定义守护进程在每次执行周期中以及关闭时的行为。
通过继承 AbstractPatroniDaemon
类,子类可以专注于实现特定的业务逻辑,而不需要关心信号处理、日志配置等通用任务。这使得代码更易于维护和扩展。
1.8 config_generator.py
"""Patroni --generate-config 机制。"""
存放配置生成器的文件。
generate_config
函数:根据提供的参数生成 Patroni 的配置文件。get_address
函数:创建一个基本的命令行参数解析器。AbstractConfigGenerator
类:对象,表示生成的Patroni配置。__init__
函数:函数的文档字符串描述了该构造方法的作用:设置输出文件(如果提供了的话)、辅助变量以及最小的配置结构。get_template_config
函数:设置输出文件(如果提供了的话)、辅助变量以及最小的配置结构。generate
函数:生成配置,并将其存储在 :attr:~AbstractConfigGenerator.config
属性中_format_block
函数:格式化单个 YAML 块。_format_config_section
函数:格式化并生成当前 :attr:~AbstractConfigGenerator.config
的单个节。_format_config
函数:格式化当前的 :attr:~AbstractConfigGenerator.config 并添加一些注释。_write_config_to_fd
函数:格式化并将当前的 :attr:~AbstractConfigGenerator.config 写入提供的文件描述符。write_config
函数:如果提供了输出文件,则将当前的 :attr:~AbstractConfigGenerator.config 写入输出文件,否则写入 stdout。
SampleConfigGenerator
类:表示生成的示例 Patroni 配置的对象。get_auth_method
函数:返回针对特定 PostgreSQL 版本的首选认证方法(如果提供),否则返回默认值md5
。_get_int_major_version
函数:从 PostgreSQL 二进制文件中获取主要 PostgreSQL 版本作为整数。generate
函数:使用一些合理的默认值生成示例配置,并更新 :attr:~AbstractConfigGenerator.config
。
RunningClusterConfigGenerator
类:表示使用来自正在运行的实例的信息生成的 Patroni 配置的对象。__init__
函数:另外存储传递的 DSN(如果有的话),并以原始和解析的形式存储,并运行配置生成。_get_hba_conn_types
函数:返回允许的连接类型。_required_pg_params
函数:必须始终存在于生成的配置中的 PostgreSQL 配置参数。_get_bin_dir_from_running_instance
函数:使用 postmaster 的 PID 可执行文件定义 PostgreSQL 二进制文件所在的目录。_get_connection_cursor
函数:基于存储的信息建立 PG 连接并获取游标。_set_pg_params
函数:扩展:attr: ~ RunningClusterConfigGenerator
。使用实际的PG GUCs值。_set_su_params
函数:扩展:attr: ~ RunningClusterConfigGenerator
。配置超级用户授权信息。_set_conf_files
函数:扩展RunningClusterConfigGenerator.config
。使用pg_hba.conf
和pg_ident.conf
文件的内容扩展 config。_enrich_config_from_running_instance
函数:扩展:attr: ~ RunningClusterConfigGenerator
。使用从正在运行的实例中收集的值配置generate
函数:使用从指定的正在运行的 PostgreSQL 实例收集的信息生成配置。
1.8.1 generate_config()
- 定义一个名为
generate_config
的函数,该函数接受三个参数,并不返回任何值。
def generate_config(output_file: str, sample: bool, dsn: Optional[str]) -> None:
"""生成 Patroni 配置文件。
参数 output_file:要使用的配置文件的完整路径。如果不提供,则结果将发送到标准输出(stdout)
参数 sample:可选标志。如果设置,则不使用任何来源实例——使用一些合理的默认值生成配置
参数 dsn:用于从本地实例获取 GUC 值的可选 DSN 字符串
"""
try:
# 根据 sample 参数的值决定创建哪种类型的配置生成器
if sample:
config_generator = SampleConfigGenerator(output_file)
else:
config_generator = RunningClusterConfigGenerator(output_file, dsn)
# 调用配置生成器的 write_config 方法来生成配置文件
config_generator.write_config()
except PatroniException as e:
sys.exit(str(e))
except Exception as e:
sys.exit(f`Unexpected exception: {e}`)
作用:
这个 generate_config
函数的作用是根据提供的参数生成 Patroni 的配置文件。具体来说:
- 生成配置文件:
- 根据
output_file
参数指定的路径生成配置文件。如果output_file
为空,则输出到标准输出流(stdout
)。
- 根据
- 处理不同的配置生成需求:
- 如果
sample
参数为True
,则使用默认值生成配置文件,不从任何实例获取数据。 - 如果
sample
参数为False
,则从指定的 Postgres 数据库实例(通过dsn
提供)获取必要的配置信息,并生成配置文件。
- 如果
- 错误处理:
- 尝试执行配置生成操作,并捕获可能出现的异常。
- 如果发生
PatroniException
异常,输出异常信息并退出程序。 - 如果发生其他类型的异常,输出一条包含异常信息的消息并退出程序。
通过这个函数,用户可以通过提供必要的参数来生成 Patroni 的配置文件,从而简化配置过程并确保生成的配置文件满足所需的要求。
1.8.2 get_address()
- 定义一个名为
generate_config
的函数,该函数接受三个参数,并不返回任何值。
def get_address() -> Tuple[str, str]:
"""Try to get hostname and the ip address for it returned by :func:`~socket.gethostname`.
.. note::
Can also return local ip.
:returns: tuple consisting of the hostname returned by :func:`~socket.gethostname`
and the first element in the sorted list of the addresses returned by :func:`~socket.getaddrinfo`.
Sorting guarantees it will prefer IPv4.
If an exception occured, hostname and ip values are equal to :data:`~patroni.config_generator.NO_VALUE_MSG`.
"""
hostname = None
try:
hostname = socket.gethostname()
return hostname, sorted(socket.getaddrinfo(hostname, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0),
key=lambda x: x[0])[0][4][0]
except Exception as err:
logging.warning(`Failed to obtain address: %r`, err)
return NO_VALUE_MSG, NO_VALUE_MSG
作用:
这个 get_base_arg_parser
函数的作用是创建一个基本的命令行参数解析器,用于处理以下两个主要任务:
- 版本信息:
- 当用户传递
--version
参数时,自动打印版本信息并退出。
- 当用户传递
- 配置文件路径:
- 允许用户通过命令行参数指定配置文件的路径。如果用户没有提供配置文件路径,则使用默认值(空字符串)。此外,还允许从环境变量读取配置文件路径。
这个函数可以被其他模块调用,以便在启动 Patroni 或 Raft 控制器守护进程时解析基本的命令行参数。通过这种方式,可以简化命令行工具的使用,并提供一致的参数处理方式。
1.8.3 类:AbstractConfigGenerator
__init__
函数:函数的文档字符串描述了该构造方法的作用:设置输出文件(如果提供了的话)、辅助变量以及最小的配置结构。get_template_config
函数:设置输出文件(如果提供了的话)、辅助变量以及最小的配置结构。generate
函数:生成配置,并将其存储在 :attr:~AbstractConfigGenerator.config
属性中_format_block
函数:格式化单个 YAML 块。_format_config_section
函数:格式化并生成当前 :attr:~AbstractConfigGenerator.config
的单个节。_format_config
函数:格式化当前的 :attr:~AbstractConfigGenerator.config 并添加一些注释。_write_config_to_fd
函数:格式化并将当前的 :attr:~AbstractConfigGenerator.config 写入提供的文件描述符。write_config
函数:如果提供了输出文件,则将当前的 :attr:~AbstractConfigGenerator.config 写入输出文件,否则写入 stdout。
1.8.3.1 主体
- 定义一个名为
generate_config
的函数,该函数接受三个参数,并不返回任何值。
class AbstractConfigGenerator(abc.ABC):
"""对象,表示生成的Patroni配置。
ivar output_file:要使用的输出文件的完整路径。
:ivar pg_major: PostgreSQL主要版本的整数表示。
:ivar config:用于生成配置存储的字典。
"""
def __init__(self, output_file: Optional[str]) -> None:
"""设置输出文件(如果提供了的话)、辅助变量以及最小的配置结构。
参数 output_file:要使用的输出文件的完整路径
"""
# 初始化成员信息
self.output_file = output_file
self.pg_major = 0
# 获取一个模板配置
self.config = self.get_template_config()
# 生成配置文件
self.generate()
@classmethod
def get_template_config(cls) -> Dict[str, Any]:
"""函数的文档字符串描述了该方法的作用:
生成一个模板配置,用于进一步扩展(例如,在继承的类中)。
返回一个字典,包含从 Patroni 环境收集的值,期望定义的主机名和 IP 地址(否则设置为 NO_VALUE_MSG),以及一些合理的默认值。
"""
# 获取主机名和 IP 地址
_HOSTNAME, _IP = get_address()
# 初始化字典
template_config: Dict[str, Any] = {
`scope`: NO_VALUE_MSG,
`name`: _HOSTNAME,
`restapi`: {
`connect_address`: _IP + `:8008`,
`listen`: _IP + `:8008`
},
`log`: {
`type`: PatroniLogger.DEFAULT_TYPE,
`level`: PatroniLogger.DEFAULT_LEVEL,
`traceback_level`: PatroniLogger.DEFAULT_TRACEBACK_LEVEL,
`format`: PatroniLogger.DEFAULT_FORMAT,
`max_queue_size`: PatroniLogger.DEFAULT_MAX_QUEUE_SIZE
},
`postgresql`: {
`data_dir`: NO_VALUE_MSG,
`connect_address`: _IP + `:5432`,
`listen`: _IP + `:5432`,
`bin_dir`: ``,
`authentication`: {
`superuser`: {
`username`: `postgres`,
`password`: NO_VALUE_MSG
},
`replication`: {
`username`: `replicator`,
`password`: NO_VALUE_MSG
}
}
},
`tags`: {
`failover_priority`: 1,
`noloadbalance`: False,
`clonefrom`: True,
`nosync`: False,
`nostream`: False,
}
}
# 获取默认配置
dynamic_config = Config.get_default_config()
# 转换为普通的字典,以便稍后将CaseInsensitiveDict正确转储为YAML
dynamic_config[`postgresql`][`parameters`] = dict(dynamic_config[`postgresql`][`parameters`])
# 创建一个 Config 实例,并获取环境中的配置值
config = Config(``, None).local_configuration
# 设置字段
config.setdefault(`bootstrap`, {})[`dcs`] = dynamic_config
config.setdefault(`postgresql`, {})
# 删除项
del config[`bootstrap`][`dcs`][`standby_cluster`]
# 合并 template_config 和 config
patch_config(template_config, config)
return template_config
@abc.abstractmethod
def generate(self) -> None:
"""生成配置,并将其存储在 :attr:`~AbstractConfigGenerator.config` 属性中"""
@staticmethod
def _format_block(block: Any, line_prefix: str = ``) -> str:
"""格式化单个 YAML 块。
.. note::
可选地,格式化的块可以用 *line_prefix* 进行缩进。
:param block: 应该被格式化为 YAML 的对象。
:param line_prefix: 用于缩进。
:returns: 一个格式化且缩进的 *block*。
"""
return line_prefix + yaml.safe_dump(block, default_flow_style=False, line_break=`\n`,
allow_unicode=True, indent=2).strip().replace(`\n`, `\n` + line_prefix)
def _format_config_section(self, section_name: str) -> Iterator[str]:
"""格式化并生成当前 :attr:`~AbstractConfigGenerator.config` 的单个节。
.. note::
如果节是一个 :class:`dict` 对象,我们在它前面放置一个空行。
:param section_name: 当前 :attr:`~AbstractConfigGenerator.config` 中的一个节名称。
:yields: 如果该节存在于 :attr:`~AbstractConfigGenerator.config` 中,则生成一个格式化的节。
"""
# 检查 section_name 是否存在于 self.config 中
if section_name in self.config:
if isinstance(self.config[section_name], dict):
yield ``
yield self._format_block({section_name: self.config[section_name]})
def _format_config(self) -> Iterator[str]:
"""格式化当前的 :attr:~AbstractConfigGenerator.config 并添加一些注释。
:yields: 表示 YAML 文档文本输出的格式化行或块。
"""
# 遍历一组配置项名称,对于每个名称,使用 _format_config_section 方法格式化相应的配置节并生成
for name in (`scope`, `namespace`, `name`, `log`, `restapi`, `ctl`, `citus`,
`consul`, `etcd`, `etcd3`, `exhibitor`, `kubernetes`, `raft`, `zookeeper`):
yield from self._format_config_section(name)
# 如果 bootstrap 存在于 self.config 中,则首先添加一些关于 bootstrap 配置的注释,然后格式化 bootstrap.dcs 节点下的特定项,并将它们逐个生成
if `bootstrap` in self.config:
yield `\n# The bootstrap configuration. Works only when the cluster is not yet initialized.`
yield `# If the cluster is already initialized, all changes in the `bootstrap` section are ignored!`
yield `bootstrap:`
if `dcs` in self.config[`bootstrap`]:
yield ` # 在初始化'之后,该部分将写入<dcs>:/<namespace>/<scope>/config中。
yield ` # 新集群和所有其他集群成员将使用它作为“全局配置”。
yield ` # 警告!如果您想更改已设置的任何参数
yield ` # 通过引导。请使用“patronictl edit-config”!”
yield ` dcs:`
for name in (`loop_wait`, `retry_timeout`, `ttl`):
if name in self.config[`bootstrap`][`dcs`]:
yield self._format_block({name: self.config[`bootstrap`][`dcs`].pop(name)}, ` `)
for name, value in self.config[`bootstrap`][`dcs`].items():
yield self._format_block({name: value}, ` `)
# 遍历剩余的配置项名称,对于每个名称,使用 _format_config_section 方法格式化相应的配置节并生成
for name in (`postgresql`, `watchdog`, `tags`):
yield from self._format_config_section(name)
def _write_config_to_fd(self, fd: TextIO) -> None:
"""格式化并将当前的 :attr:~AbstractConfigGenerator.config 写入提供的文件描述符。
fd: 配置文件的写入位置。可以是 sys.stdout 或实际的文件。
"""
fd.write(`\n`.join(self._format_config()))
def write_config(self) -> None:
"""如果提供了输出文件,则将当前的 :attr:~AbstractConfigGenerator.config 写入输出文件,否则写入 stdout。"""
if self.output_file:
dir_path = os.path.dirname(self.output_file)
if dir_path and not os.path.isdir(dir_path):
os.makedirs(dir_path)
with open(self.output_file, `w`, encoding=`UTF-8`) as output_file:
self._write_config_to_fd(output_file)
else:
self._write_config_to_fd(sys.stdout)
作用:
1.8.4 类:SampleConfigGenerator
该类继承了AbstractConfigGenerator
。
get_auth_method
函数:返回针对特定 PostgreSQL 版本的首选认证方法(如果提供),否则返回默认值md5
。_get_int_major_version
函数:从 PostgreSQL 二进制文件中获取主要 PostgreSQL 版本作为整数。generate
函数:使用一些合理的默认值生成示例配置,并更新 :attr:~AbstractConfigGenerator.config
。
1.8.4.1 主体
- 定义一个名为
generate_config
的函数,该函数接受三个参数,并不返回任何值。
class SampleConfigGenerator(AbstractConfigGenerator):
"""表示生成的示例 Patroni 配置的对象。
根据收集到的 PostgreSQL 版本使用合理默认值
"""
@property
def get_auth_method(self) -> str:
"""返回针对特定 PostgreSQL 版本的首选认证方法(如果提供),否则返回默认值 ``md5``。
:returns: 用于首选认证方法的 :class:`str` 值。
"""
return `scram-sha-256` if self.pg_major and self.pg_major >= 100000 else `md5`
def _get_int_major_version(self) -> int:
"""从 PostgreSQL 二进制文件中获取主要 PostgreSQL 版本作为整数。
:returns: 从 PostgreSQL 二进制文件中收集的主要 PostgreSQL 版本的整数表示。
参见 :func:`~patroni.postgresql.misc.postgres_major_version_to_int` 和
:func:`~patroni.utils.get_major_version`。
"""
postgres_bin = ((self.config.get(`postgresql`)
or EMPTY_DICT).get(`bin_name`) or EMPTY_DICT).get(`postgres`, `postgres`)
return postgres_major_version_to_int(get_major_version(self.config[`postgresql`].get(`bin_dir`), postgres_bin))
def generate(self) -> None:
"""使用一些合理的默认值生成示例配置,并更新 :attr:`~AbstractConfigGenerator.config`"""
# 获取主要 PostgreSQL 版本号
self.pg_major = self._get_int_major_version()
# 更新 self.config
self.config[`postgresql`][`parameters`] = {`password_encryption`: self.get_auth_method}
username = self.config["postgresql"]["authentication"]["replication"]["username"]
self.config[`postgresql`][`pg_hba`] = [
f`host all all all {self.get_auth_method}`,
f`host replication {username} all {self.get_auth_method}`
]
# 添加版本特定的配置
wal_keep_param = `wal_keep_segments` if self.pg_major < 130000 else `wal_keep_size`
self.config[`bootstrap`][`dcs`][`postgresql`][`parameters`][wal_keep_param] = \
ConfigHandler.CMDLINE_OPTIONS[wal_keep_param][0]
# 根据 PostgreSQL 的版本,设置 wal_level 参数
wal_level = `hot_standby` if self.pg_major < 90600 else `replica`
self.config[`bootstrap`][`dcs`][`postgresql`][`parameters`][`wal_level`] = wal_level
# 根据 wal_log_hints 参数的值设置 use_pg_rewind 参数
self.config[`bootstrap`][`dcs`][`postgresql`][`use_pg_rewind`] = \
parse_bool(self.config[`bootstrap`][`dcs`][`postgresql`][`parameters`][`wal_log_hints`]) is True
# 如果 PostgreSQL 版本大于等于 110000,则设置 rewind 用户名和密码
if self.pg_major >= 110000:
self.config[`postgresql`][`authentication`].setdefault(
`rewind`, {`username`: `rewind_user`}).setdefault(`password`, NO_VALUE_MSG)
1.8.4.2 作用
这个 SampleConfigGenerator
类的作用是生成 Patroni 的示例配置,并根据 PostgreSQL 的版本使用合理的默认值。具体来说:
- 获取 PostgreSQL 版本:
- 通过
_get_int_major_version
方法获取 PostgreSQL 的主要版本号,并存储在self.pg_major
中。
- 通过
- 设置认证方法:
- 根据 PostgreSQL 的版本选择合适的密码加密方法,并设置到
self.config
中。
- 根据 PostgreSQL 的版本选择合适的密码加密方法,并设置到
- 设置
pg_hba.conf
文件条目:- 根据选择的密码加密方法,设置
pg_hba.conf
文件中的条目。
- 根据选择的密码加密方法,设置
- 添加版本特定的配置:
- 根据 PostgreSQL 的版本,设置
wal_keep_segments
或wal_keep_size
参数。 - 根据 PostgreSQL 的版本,设置
wal_level
参数。
- 根据 PostgreSQL 的版本,设置
- 设置
use_pg_rewind
参数:- 根据
wal_log_hints
参数的值设置use_pg_rewind
参数。
- 根据
- 设置
rewind
用户名和密码:- 如果 PostgreSQL 版本大于等于
110000
,则设置rewind
用户名和密码。
- 如果 PostgreSQL 版本大于等于
通过这个类及其 generate
方法,可以生成一个包含合理默认值的 Patroni 配置示例,这个配置示例可以根据 PostgreSQL 的不同版本进行适当调整,从而确保生成的配置适用于不同版本的 PostgreSQL 数据库。
1.8.5 类:RunningClusterConfigGenerator
该类继承了AbstractConfigGenerator
。
__init__
函数:另外存储传递的 DSN(如果有的话),并以原始和解析的形式存储,并运行配置生成。_get_hba_conn_types
函数:返回允许的连接类型。_required_pg_params
函数:必须始终存在于生成的配置中的 PostgreSQL 配置参数。_get_bin_dir_from_running_instance
函数:使用 postmaster 的 PID 可执行文件定义 PostgreSQL 二进制文件所在的目录。_get_connection_cursor
函数:基于存储的信息建立 PG 连接并获取游标。_set_pg_params
函数:扩展:attr: ~ RunningClusterConfigGenerator
。使用实际的PG GUCs值。_set_su_params
函数:扩展:attr: ~ RunningClusterConfigGenerator
。配置超级用户授权信息。_set_conf_files
函数:扩展RunningClusterConfigGenerator.config
。使用pg_hba.conf
和pg_ident.conf
文件的内容扩展 config。_enrich_config_from_running_instance
函数:扩展:attr: ~ RunningClusterConfigGenerator
。使用从正在运行的实例中收集的值配置generate
函数:使用从指定的正在运行的 PostgreSQL 实例收集的信息生成配置。
1.8.5.1 主体
- 定义一个名为
generate_config
的函数,该函数接受三个参数,并不返回任何值。
class RunningClusterConfigGenerator(AbstractConfigGenerator):
"""表示使用来自正在运行的实例的信息生成的 Patroni 配置的对象
:ivar dsn:用于从本地实例获取 GUC 值的 DSN 字符串(如果提供了的话)
:ivar parsed_dsn:解析成字典形式的 DSN 字符串(参见 ~patroni.postgresql.config.parse_dsn 函数)
"""
def __init__(self, output_file: Optional[str] = None, dsn: Optional[str] = None) -> None:
"""另外存储传递的 DSN(如果有的话),并以原始和解析的形式存储,并运行配置生成
:param output_file:要使用的输出文件的完整路径
:param dsn:用于从本地实例获取 GUC 值的 DSN 字符串.
:raises:
:exc:`~patroni.exceptions.PatroniException`: 如果 DSN 解析失败
"""
self.dsn = dsn
self.parsed_dsn = {}
super().__init__(output_file)
@property
def _get_hba_conn_types(self) -> Tuple[str, ...]:
"""返回允许的连接类型
如果 RunningClusterConfigGenerator.pg_major 已定义,则为 PostgreSQL 版本 >=16 添加额外的参数
:返回之后: 允许的连接方法组成的元组
"""
# 初始化一个包含基本连接类型的元组
allowed_types = (`local`, `host`, `hostssl`, `hostnossl`, `hostgssenc`, `hostnogssenc`)
# 检查 self.pg_major 是否已定义,并且其值是否大于等于 160000(表示 PostgreSQL 版本 16 或更高)
if self.pg_major and self.pg_major >= 160000:
allowed_types += (`include`, `include_if_exists`, `include_dir`)
return allowed_types
@property
def _required_pg_params(self) -> List[str]:
"""必须始终存在于生成的配置中的 PostgreSQL 配置参数
返回:参数名称的列表
"""
return [`hba_file`, `ident_file`, `config_file`, `data_directory`] + \
list(ConfigHandler.CMDLINE_OPTIONS.keys())
def _get_bin_dir_from_running_instance(self) -> str:
"""使用 postmaster 的 PID 可执行文件定义 PostgreSQL 二进制文件所在的目录
返回:PostgreSQL 二进制文件目录的路径
抛出异常:
如果:
无法从 postmaster.pid 文件中获得 PID;
在处理 postmaster.pid 文件时发生 OSError;
获得的 postmaster PID 不存在
"""
# 初始化 postmaster_pid 为 None
postmaster_pid = None
# 获取 PostgreSQL 数据目录的路径
data_dir = self.config[`postgresql`][`data_dir`]
try:
# 尝试以只读模式打开 postmaster.pid 文件
with open(f"{data_dir}/postmaster.pid", `r`) as pid_file:
# 读取文件中的第一行,即 postmaster 的 PID
postmaster_pid = pid_file.readline()
# 如果 postmaster_pid 为空,则抛出异常
if not postmaster_pid:
raise PatroniException(`Failed to obtain postmaster pid from postmaster.pid file`)
postmaster_pid = int(postmaster_pid.strip())
except OSError as err:
raise PatroniException(f`Error while reading postmaster.pid file: {err}`)
# 尝试获取 postmaster 进程的可执行文件路径,并返回其父目录路径
try:
return os.path.dirname(psutil.Process(postmaster_pid).exe())
except psutil.NoSuchProcess:
raise PatroniException("Obtained postmaster pid doesn`t exist.")
@contextmanager
def _get_connection_cursor(self) -> Iterator[Union[`cursor`, `Cursor[Any]`]]:
"""基于存储的信息建立 PG 连接并获取游标
:raises:
:exc:如果发生 psycopg.Error,则抛出 PatroniException
"""
try:
# 尝试使用 psycopg.connect 方法建立一个到 PostgreSQL 数据库的连接
conn = psycopg.connect(dsn=self.dsn,
password=self.config[`postgresql`][`authentication`][`superuser`][`password`])
# 获取连接的游标对象
with conn.cursor() as cur:
yield cur
conn.close()
except psycopg.Error as e:
raise PatroniException(f`Failed to establish PostgreSQL connection: {e}`)
def _set_pg_params(self, cur: Union[`cursor`, `Cursor[Any]`]) -> None:
"""扩展:attr: ~ RunningClusterConfigGenerator。使用实际的PG GUCs值
设置以下GUC值:
* 非内部的且来源为配置文件、postmaster 命令行或环境变量的值
* 总是需要的参数列表(参见 _required_pg_params 方法)
:参数 cur: 要使用的连接游标.
"""
# 执行 SQL 查询,获取特定条件下的 GUC 参数及其当前设置值
cur.execute("SELECT name, pg_catalog.current_setting(name) FROM pg_catalog.pg_settings "
"WHERE context <> `internal` "
"AND source IN (`configuration file`, `command line`, `environment variable`) "
"AND category <> `Write-Ahead Log / Recovery Target` "
"AND setting <> `(disabled)` "
"OR name = ANY(%s)", (self._required_pg_params,))
# 初始化一个包含 `port` 和 `listen_addresses` 的字典
helper_dict = dict.fromkeys([`port`, `listen_addresses`])
# 设置或获取 parameters 字典
self.config[`postgresql`].setdefault(`parameters`, {})
# 遍历查询结果中的每一对参数及其值
for param, value in cur.fetchall():
if param == `data_directory`:
self.config[`postgresql`][`data_dir`] = value
elif param == `cluster_name` and value:
self.config[`scope`] = value
elif param in (`archive_command`, `restore_command`,
`archive_cleanup_command`, `recovery_end_command`,
`ssl_passphrase_command`, `hba_file`,
`ident_file`, `config_file`):
# 出于安全考虑,将命令写入本地配置
# write hba/ident/config_file到本地配置,以确保它们不会被删除
self.config[`postgresql`][`parameters`][param] = value
elif param in helper_dict:
helper_dict[param] = value
else:
self.config[`bootstrap`][`dcs`][`postgresql`][`parameters`][param] = value
# 从 connect_address 中提取 IP 地址,获取连接端口,然后更新 connect_address 和 listen 的值
connect_ip = self.config[`postgresql`][`connect_address`].rsplit(`:`)[0]
connect_port = self.parsed_dsn.get(`port`, os.getenv(`PGPORT`, helper_dict[`port`]))
self.config[`postgresql`][`connect_address`] = f`{connect_ip}:{connect_port}`
self.config[`postgresql`][`listen`] = f`{helper_dict["listen_addresses"]}:{helper_dict["port"]}`
def _set_su_params(self) -> None:
"""扩展:attr: ~ RunningClusterConfigGenerator。配置超级用户授权信息
信息集基于用于连接的选项
"""
# 存放超级用户的认证信息
su_params: Dict[str, str] = {}
for conn_param, env_var in _AUTH_ALLOWED_PARAMETERS_MAPPING.items():
val = self.parsed_dsn.get(conn_param, os.getenv(env_var))
if val:
su_params[conn_param] = val
# 获取超级用户的用户名和密码
patroni_env_su_username = ((self.config.get(`authentication`)
or EMPTY_DICT).get(`superuser`) or EMPTY_DICT).get(`username`)
patroni_env_su_pwd = ((self.config.get(`authentication`)
or EMPTY_DICT).get(`superuser`) or EMPTY_DICT).get(`password`)
# because we use "username" in the config for some reason
# 将 su_params 中的 user 键替换为 username,如果 user 键不存在,则使用 patroni_env_su_username 或者当前登录用户的用户名
su_params[`username`] = su_params.pop(`user`, patroni_env_su_username) or getuser()
su_params[`password`] = su_params.get(`password`, patroni_env_su_pwd) or \
getpass(`Please enter the user password:`)
# 将构建好的 su_params 添加到 self.config[`postgresql`][`authentication`] 中,并设置复制用户的用户名和密码为 NO_VALUE_MSG
self.config[`postgresql`][`authentication`] = {
`superuser`: su_params,
`replication`: {`username`: NO_VALUE_MSG, `password`: NO_VALUE_MSG}
}
def _set_conf_files(self) -> None:
"""扩展 RunningClusterConfigGenerator.config:使用 pg_hba.conf 和 pg_ident.conf 文件的内容扩展 config。
注意:
此函数仅在 hba_file 和 ident_file 被设置为默认路径时定义 postgresql.pg_hba 和 postgresql.pg_ident。
这些文件可能位于 PGDATA 外部,Patroni 可能没有对它们的写权限。
:raises:
如果在处理配置文件时发生 OSError,则抛出 PatroniException
"""
# 计算默认的 pg_hba.conf 文件路径
default_hba_path = os.path.join(self.config[`postgresql`][`data_dir`], `pg_hba.conf`)
# 如果 hba_file 的路径与默认路径相同,则尝试读取 pg_hba.conf 文件的内容,并将符合条件的行添加到 self.config[`postgresql`][`pg_hba`] 中。
if self.config[`postgresql`][`parameters`][`hba_file`] == default_hba_path:
try:
self.config[`postgresql`][`pg_hba`] = list(
filter(lambda i: i and i.split()[0] in self._get_hba_conn_types, read_stripped(default_hba_path)))
except OSError as err:
raise PatroniException(f`Failed to read pg_hba.conf: {err}`)
# 计算默认的 pg_ident.conf 文件路径
default_ident_path = os.path.join(self.config[`postgresql`][`data_dir`], `pg_ident.conf`)
# 如果 ident_file 的路径与默认路径相同,则尝试读取 pg_ident.conf 文件的内容,并将非注释行添加到 self.config[`postgresql`][`pg_ident`] 中
if self.config[`postgresql`][`parameters`][`ident_file`] == default_ident_path:
try:
self.config[`postgresql`][`pg_ident`] = [i for i in read_stripped(default_ident_path)
if i and not i.startswith(`#`)]
except OSError as err:
raise PatroniException(f`Failed to read pg_ident.conf: {err}`)
if not self.config[`postgresql`][`pg_ident`]:
del self.config[`postgresql`][`pg_ident`]
def _enrich_config_from_running_instance(self) -> None:
"""扩展:attr: ~ RunningClusterConfigGenerator。使用从正在运行的实例中收集的值配置
从正在运行的PostgreSQL实例中检索以下信息:
* 超级用户认证参数(参见 _set_su_params 方法);
* 一些 GUC 值(参见 _set_pg_params 方法);
* postgresql.connect_address 和 postgresql.listen;
* postgresql.pg_hba 和 postgresql.pg_ident(参见 _set_conf_files 方法)
重新定义 scope:如果设置了 cluster_name GUC 值,则重新定义 scope
:raises:
:exc:`~patroni.exceptions.PatroniException`: 如果提供的用户没有超级用户的权限
"""
# 设置超级用户的认证参数
self._set_su_params()
# 获取一个连接游标
with self._get_connection_cursor() as cur:
# 从连接中获取服务器版本号
self.pg_major = getattr(cur.connection, `server_version`, 0)
# 检查连接的 is_superuser 参数状态是否为 True
if not parse_bool(getattr(cur.connection, `get_parameter_status`)(`is_superuser`)):
raise PatroniException(`The provided user does not have superuser privilege`)
# 设置一些 GUC 值
self._set_pg_params(cur)
# 设置 postgresql.pg_hba 和 postgresql.pg_ident 文件
self._set_conf_files()
def generate(self) -> None:
"""使用从指定的正在运行的 PostgreSQL 实例收集的信息生成配置
结果写入 RunningClusterConfigGenerator.config 属性
"""
# 检查 self.dsn 是否存在
if self.dsn:
# 存储结果
self.parsed_dsn = parse_dsn(self.dsn) or {}
if not self.parsed_dsn:
# 解析失败
raise PatroniException(`Failed to parse DSN string`)
# 丰富配置信息
self._enrich_config_from_running_instance()
# 设置bin_dir
self.config[`postgresql`][`bin_dir`] = self._get_bin_dir_from_running_instance()
1.8.5.2 作用
RunningClusterConfigGenerator
类的主要目的是从一个正在运行的 PostgreSQL 实例中收集配置信息,并生成一个新的配置文件。该类提供了多种方法来完成这一任务,确保生成的配置文件包含必要的参数和信息,以便新集群能够按照现有集群的配置运行。
- 初始化 (
__init__
):- 接受一个 DSN(Data Source Name)作为参数,并将其解析为
parsed_dsn
。 - 初始化配置对象
self.config
。 - 设置
dsn
和parsed_dsn
属性,并开始配置生成流程。
- 接受一个 DSN(Data Source Name)作为参数,并将其解析为
- 设置超级用户认证参数 (
_set_su_params
):- 收集超级用户的认证信息(如用户名和密码),并将这些信息存储在
su_params
字典中。 - 从配置中获取超级用户的用户名和密码。
- 如果存在
user
键,则将其替换为username
,如果没有user
键,则使用环境变量或当前用户的用户名。 - 将构建好的
su_params
添加到self.config[
postgresql][
authentication]
中,并设置复制用户的用户名和密码为NO_VALUE_MSG
。
- 收集超级用户的认证信息(如用户名和密码),并将这些信息存储在
- 设置配置文件内容 (
_set_conf_files
):- 计算默认的
pg_hba.conf
文件路径,并检查hba_file
是否指向此路径。 - 如果
hba_file
指向默认路径,则读取pg_hba.conf
文件的内容,并筛选出符合条件的行(即连接类型),然后存储在self.config[
postgresql][
pg_hba]
中。 - 计算默认的
pg_ident.conf
文件路径,并检查ident_file
是否指向此路径。 - 如果
ident_file
指向默认路径,则读取pg_ident.conf
文件的内容,并筛选出非注释行,然后存储在self.config[
postgresql][
pg_ident]
中。
- 计算默认的
- 从运行实例中丰富配置 (
_enrich_config_from_running_instance
):- 设置超级用户的认证参数(调用
_set_su_params
方法)。 - 获取一个连接游标,并从中获取服务器版本号。
- 检查连接的用户是否具有超级用户权限。
- 设置一些 GUC 值(调用
_set_pg_params
方法)。 - 设置
postgresql.pg_hba
和postgresql.pg_ident
文件(调用_set_conf_files
方法)。
- 设置超级用户的认证参数(调用
- 生成配置 (
generate
):- 解析提供的 DSN,并存储解析结果。
- 如果 DSN 解析失败,则抛出异常。
- 从运行的 PostgreSQL 实例中丰富配置信息(调用
_enrich_config_from_running_instance
方法)。 - 设置 PostgreSQL 二进制文件目录(调用
_get_bin_dir_from_running_instance
方法)
1.9 validator.py
"""Patroni 配置验证助手。
这个模块包含用于验证 Patroni 进程配置的工具。
:var schema: 由 “patroni” 命令启动的守护进程的配置模式。
"""
-
populate_validate_params
函数:配置用于验证 Patroni 配置文件的参数。 -
validate_log_field
函数:对传入的日志字段进行验证,确保其符合规定的格式。 -
validate_log_format
函数:对传入的日志格式进行验证,确保其符合规定的格式。 -
data_directory_empty
函数:检查指定的 PostgreSQL 数据目录是否为空。 -
validate_host_port
函数:验证传入的主机和端口是否符合规定的格式,并且检查主机和端口是否可用。 -
validate_host_port_list
函数:对一个包含主机和端口信息的列表进行验证。 -
comma_separated_host_port
函数:从一个逗号分隔的字符串中提取出一系列的主机和端口项,并验证这些项的有效性。 -
validate_host_port_listen
函数:验证一个指定的主机和端口是否有效,并且这个端口是否可用以供服务绑定。 -
validate_host_port_listen_multiple_hosts
函数:证一个或多个指定的主机和端口是否有效,并且这个端口是否可用以供服务绑定。 -
is_ipv4_address
函数:验证一个给定的字符串ip
是否代表了一个有效的 IPv4 地址。 -
is_ipv6_address
函数:验证一个给定的字符串ip
是否代表了一个有效的 IPv6 地址。 -
get_bin_name
函数:从配置中获取与特定二进制文件名相关的配置值。 -
validate_data_dir
函数:验证一个给定的数据目录路径data_dir
是否适合用于 PostgreSQL 数据目录。 -
validate_binary_name
函数:验证一个给定的二进制文件名bin_name
是否是有效的。 -
_get_type_name
函数:获取给定 Python 类型的一个用户友好的名称。 -
assert_
函数:在开发过程中用来检查某个条件是否为真。如果条件不满足,则会抛出一个异常,并附带一个错误信息。 -
validate_watchdog_mode
函数:验证一个给定的值value
是否适合作为watchdog.mode
配置项的有效值。 -
Result
类:具体作用是表示一次配置验证的结果。__init__
函数:构造方法。__repr__
函数:用于返回一个表示对象的字符串。
-
Case
类:具体作用是定义如何验证一组配置选项。__init__
函数:构造方法。
-
Or
类:具体作用是定义一组可能的选项或验证规则。__init__
函数:构造方法。__repr__
函数:
-
AtMostOne
类:具体作用是定义一组配置选项中的最多只能有一个被设置。__init__
函数:构造方法。
-
Optional
类:具体作用是定义一个配置选项为可选的,并提供一个默认值。__init__
函数:构造方法。
-
Directory
类:具体作用是对一个给定的目录进行验证,确保该目录包含预期的文件和可执行文件。__init__
函数:构造方法。_check_executables
函数:检查contains_executable
列表中的所有可执行文件是否存在于指定路径或环境变量PATH
中。validate
函数:检查指定目录是否包含预期的路径和可执行文件。
-
BinDirectory
类:具体作用是对一个 Postgres 二进制目录进行验证,确保该目录包含一组特定的可执行文件。validate
函数:检查在 name 二进制目录下是否能找到预期的可执行文件。
-
Schema
类:具体作用是定义一个配置模式(schema),这个模式描述了配置项的结构和验证规则。__init__
函数:构造方法。__call__
函数:在给定的配置数据上执行验证,依据在Schema
实例中定义的验证规则,该方法使得Schema
类的实例可以像函数一样被调用。validate
函数:对给定的数据进行验证,根据在Schema
实例中定义的验证规则。iter
函数:处理可迭代类型的验证逻辑,例如字典、列表以及特定的Directory
或Or
类型。iter_dict
函数:处理字典类型的验证逻辑,确保data
中的键和值符合validator
中定义的规则。iter_or
函数:处理Or
类型的验证逻辑,确保data
中的值符合validator
中定义的一种或多种规则。_data_key
函数:根据validator
中定义的键类型来确定在data
字典中应使用哪些键来访问相应的值。
-
IntValidator
类:具体作用是提供一种机制来验证一个整数值是否满足一定的条件。__init__
函数:构造方法。__call__
函数:根据IntValidator
实例定义的规则来验证一个值是否有效,它使得IntValidator
实例可以像函数一样被调用。
-
EnumValidator
类:具体作用是提供一种机制来验证一个值是否属于一组预定义的允许值中。__init__
函数:构造方法。__call__
函数:根据EnumValidator
实例定义的规则来验证一个值是否有效,它使得EnumValidator
实例可以像函数一样被调用。
1.9.1 populate_validate_params()
- 定义一个名为
populate_validate_params
的函数,该函数接受一个布尔类型的参数ignore_listen_port
,默认值为False
,并且不返回任何值。
def populate_validate_params(ignore_listen_port: bool = False) -> None:
"""函数的文档字符串描述了该函数的作用:填充用于微调 Patroni 配置验证的参数。
参数 ignore_listen_port:忽略标记为 listen 的端口绑定失败。
"""
_validation_params[`ignore_listen_port`] = ignore_listen_port
作用:
这个 populate_validate_params
函数的作用是配置用于验证 Patroni 配置文件的参数。具体来说:
- 参数配置:
- 该函数接受一个布尔类型的参数
ignore_listen_port
,表示是否忽略在listen
配置中指定的端口绑定失败。
- 该函数接受一个布尔类型的参数
- 参数存储:
- 函数将
ignore_listen_port
参数的值存储到_validation_params
字典中,键为ignore_listen_port
。
- 函数将
通过这个函数,可以动态地配置 Patroni 配置验证时的行为。例如,如果设置了 ignore_listen_port=True
,那么在验证配置文件时,将会忽略那些在配置中被标记为 listen
的端口绑定失败的情况。这在某些情况下可能是有用的,比如当开发者希望检查配置文件的其他方面而暂时忽略端口冲突时。
1.9.2 validate_log_field()
- 定义了一个名为
validate_log_field
的函数,该函数接受一个类型为Union[str, Dict[str, Any], Any]
的参数field
,并且返回一个布尔类型的值。
def validate_log_field(field: Union[str, Dict[str, Any], Any]) -> bool:
"""检查日志字段是否有效。
:param field: 要验证的日志字段。
:returns: 如果字段是一个字符串或一个恰好有一个键且其值为字符串的字典,则返回 ``True``,否则返回 ``False``。
"""
if isinstance(field, str):
return True
elif isinstance(field, dict):
return len(field) == 1 and isinstance(next(iter(field.values())), str)
return False
作用:
validate_log_field
函数的具体作用是对传入的日志字段进行验证,确保其符合规定的格式。具体来说:
- 验证字符串类型:
- 如果
field
是一个字符串,则认为它是有效的,并返回True
。
- 如果
- 验证字典型:
- 如果
field
是一个字典,并且字典恰好有一个键,并且该键对应的值是一个字符串,则认为它是有效的,并返回True
。 - 否则,返回
False
。
- 如果
- 默认情况:
- 如果
field
既不是字符串也不是字典,则返回False
。
- 如果
1.9.3 validate_log_format()
- 定义了一个名为
validate_log_format
的函数,该函数接受一个类型为type_logformat
的参数logformat
,并返回一个布尔类型的值。
def validate_log_format(logformat: type_logformat) -> bool:
"""检查日志格式是否有效。
:param logformat: 要验证的日志格式。
:returns: 如果日志格式是一个字符串或一个由有效的日志字段组成的列表,则返回 ``True``。
:raises:
:exc:`~patroni.exceptions.ConfigParseError`:
* 如果日志格式不是字符串或列表;或
* 如果日志格式是一个空列表;或
* 如果日志格式是一个列表,并且其中的值无法通过使用 :func:`validate_log_field` 进行验证。
"""
if isinstance(logformat, str):
return True
# 如果 logformat 是一个列表,则继续进行验证
elif isinstance(logformat, list):
if len(logformat) == 0:
# 空列表
raise ConfigParseError('should contain at least one item')
# 如果 logformat 是一个列表,并且其中任何一个元素无法通过 validate_log_field 函数的验证,则抛出 ConfigParseError 异常。
if not all(map(validate_log_field, logformat)):
raise ConfigParseError('each item should be a string or a dictionary with string values')
return True
else:
# 既不是字符串也不是列表
raise ConfigParseError('Should be a string or a list')
作用:
validate_log_format
函数的具体作用是对传入的日志格式进行验证,确保其符合规定的格式。具体来说:
- 验证字符串类型:
- 如果
logformat
是一个字符串,则认为它是有效的,并返回True
。
- 如果
- 验证列表类型:
- 如果
logformat
是一个列表,并且该列表不为空,并且列表中的每个元素都通过validate_log_field
函数的验证,则认为它是有效的,并返回True
。 - 否则,如果列表为空,或者列表中的元素未能通过验证,则抛出
ConfigParseError
异常。
- 如果
- 默认情况:
- 如果
logformat
既不是字符串也不是列表,则抛出ConfigParseError
异常。
- 如果
1.9.4 data_directory_empty()
- 定义了一个名为
data_directory_empty
的函数,该函数接受一个类型为str
的参数data_dir
,并返回一个布尔类型的值。
def data_directory_empty(data_dir: str) -> bool:
"""检查 PostgreSQL 数据目录是否为空。
:param data_dir: 要检查的 PostgreSQL 数据目录的路径。
:returns: 如果数据目录为空,则返回 ``True``。
"""
if os.path.isfile(os.path.join(data_dir, "global", "pg_control")):
return False
return data_directory_is_empty(data_dir)
作用:
data_directory_empty
函数的具体作用是检查指定的 PostgreSQL 数据目录是否为空。具体来说:
- 检查
pg_control
文件是否存在:- 如果
data_dir
下存在global/pg_control
文件,则认为数据目录不为空,并返回False
。
- 如果
- 进一步检查:
- 如果
global/pg_control
文件不存在,则通过调用data_directory_is_empty
函数来进一步确认数据目录是否为空,并返回其结果。
- 如果
1.9.5 validate_log_field()
- 定义了一个名为
validate_connect_address
的函数,该函数接受一个类型为str
的参数address
,并返回一个布尔类型的值。
def validate_connect_address(address: str) -> bool:
"""检查与连接地址相关的选项是否正确配置。
:param address: 要验证的地址,格式为 ``host:ip``。
:returns: 如果地址有效,则返回 ``True``。
:raises:
:class:`~patroni.exceptions.ConfigParseError`:
* 如果地址不符合预期的格式;或
* 如果主机设置为不允许的值(``127.0.0.1``, ``0.0.0.0``, ``*``, ``::1``, 或 ``localhost``)。
"""
# 试将 address 分割成 host 和 ip
try:
host, _ = split_host_port(address, 1)
except (AttributeError, TypeError, ValueError):
raise ConfigParseError("contains a wrong value")
# 如果 host 设置为不允许的值
if host in ["127.0.0.1", "0.0.0.0", "*", "::1", "localhost"]:
raise ConfigParseError('must not contain "127.0.0.1", "0.0.0.0", "*", "::1", "localhost"')
return True
作用:
validate_connect_address
函数的具体作用是验证传入的连接地址是否符合规定的格式,并且检查主机地址是否为允许使用的值。具体来说:
- 格式验证:
- 尝试从
address
中提取主机名host
和端口号ip
。 - 如果提取失败(即
address
不符合host:ip
的格式),则抛出ConfigParseError
异常。
- 尝试从
- 主机地址验证:
- 检查
host
是否为不允许使用的值(如127.0.0.1
,0.0.0.0
,*
,::1
,localhost
)。 - 如果
host
是不允许使用的值,则抛出ConfigParseError
异常。
- 检查
- 返回结果:
- 如果
address
符合预期格式,并且host
是允许使用的值,则返回True
。
- 如果
1.9.6 validate_host_port()
- 定义了一个名为
validate_host_port
的函数,该函数接受三个参数host_port
、listen
和multiple_hosts
,并返回一个布尔类型的值。
def validate_host_port(host_port: str, listen: bool = False, multiple_hosts: bool = False) -> bool:
"""检查主机和端口是否有效并且可用于使用。
:param host_port: 要验证的主机和端口。它可以是以下格式之一:
* ``host:ip``,如果 *multiple_hosts* 是 ``False``;或
* ``host_1,host_2,...,host_n:port``,如果 *multiple_hosts* 是 ``True``。
:param listen: 如果期望地址可用以绑定。 ``False`` 表示它期望连接到该地址,而 ``True`` 表示它期望绑定到该地址。
:param multiple_hosts: 如果 *host_port* 可以包含多个主机。
:returns: 如果主机和端口有效,则返回 ``True``。
:raises:
:class:`~patroni.exceptions.ConfigParseError`:
* 如果 *host_port* 不符合预期的格式;或
* 如果 *host_port* 中指定了 ``*`` 以及更多主机;或
* 如果我们期望绑定到一个已经使用的地址;或
* 如果我们无法连接到我们期望连接的地址;或
* 如果尝试连接给定地址时,socket 模块抛出 :class:`~socket.gaierror`。
"""
# 尝试从 host_port 中提取主机和端口
try:
hosts, port = split_host_port(host_port, 1)
except (ValueError, TypeError):
raise ConfigParseError("contains a wrong value")
else:
# 根据 multiple_hosts 参数决定如何处理 hosts
if multiple_hosts:
hosts = hosts.split(",")
else:
hosts = [hosts]
if "*" in hosts:
if len(hosts) != 1:
raise ConfigParseError("expecting '*' alone")
# 如果主机设置为 *,获取所有主机名和/或 IP 地址,该主机能够监听这些地址
hosts = [p[-1][0] for p in socket.getaddrinfo(None, port, 0, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)]
for host in hosts:
# 对于每一个 host ,检查是否使用了 "socket.IF_INET" 或 "socket.IF_INET6" ,并根据识别的协议实例化一个 socket 。
# protocol
proto = socket.getaddrinfo(host, None, 0, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
s = socket.socket(proto[0][0], socket.SOCK_STREAM)
try:
if s.connect_ex((host, port)) == 0:
# 如果ignore_listen_port设置为True,则不会引发异常。
# 表示端口已被占用
if listen and not _validation_params.get('ignore_listen_port', False):
raise ConfigParseError("Port {} is already in use.".format(port))
# 如果连接失败,表示地址不可达
elif not listen:
raise ConfigParseError("{} is not reachable".format(host_port))
except socket.gaierror as e:
raise ConfigParseError(e)
finally:
s.close()
return True
作用:
validate_host_port
函数的具体作用是验证传入的主机和端口是否符合规定的格式,并且检查主机和端口是否可用。具体来说:
- 格式验证:
- 尝试从
host_port
中提取主机和端口。 - 如果提取失败(即
host_port
不符合host:port
或host_1,host_2,...,host_n:port
的格式),则抛出ConfigParseError
异常。
- 尝试从
- 主机地址验证:
- 根据
multiple_hosts
参数处理hosts
,如果是True
则按逗号分隔;否则,放入列表中。 - 如果
hosts
包含*
,并且hosts
的长度不等于1
,则抛出ConfigParseError
异常。 - 如果主机设置为
*
,获取所有主机名和/或 IP 地址,该主机能够监听这些地址。
- 根据
- 连接和绑定验证:
- 对于每一个
host
,创建 socket 并尝试连接到(host, port)
。 - 如果连接成功,并且
listen
为True
,则抛出ConfigParseError
异常,表示端口已被占用。 - 如果连接失败,并且
listen
为False
,则抛出ConfigParseError
异常,表示地址不可达。 - 如果尝试连接时抛出
socket.gaierror
,则抛出ConfigParseError
异常。
- 对于每一个
- 返回结果:
- 如果
host_port
符合预期格式,并且hosts
和port
通过了验证,则返回True
。
- 如果
1.9.7 validate_host_port_list()
- 定义了一个名为
validate_host_port_list
的函数,该函数接受一个类型为List[str]
的参数value
,并返回一个布尔类型的值。
def validate_host_port_list(value: List[str]) -> bool:
"""验证主机和端口项的列表。
使用 *value* 中的每一项调用 :func:`validate_host_port` 函数。
:param value: 要验证的主机和端口项的列表。
:returns: 如果所有项都有效,则返回 ``True``。
"""
assert all([validate_host_port(v) for v in value]), "didn't pass the validation"
return True
作用:
validate_host_port_list
函数的具体作用是对一个包含主机和端口信息的列表进行验证。具体来说:
- 遍历列表:
- 函数遍历传入的
value
列表中的每一个元素。
- 函数遍历传入的
- 调用验证函数:
- 对于每一个元素
v
,调用validate_host_port
函数来验证该元素是否符合预期的格式,并且检查主机和端口是否可用。
- 对于每一个元素
- 验证结果:
- 如果所有元素都通过了
validate_host_port
函数的验证,那么validate_host_port_list
函数将返回True
。 - 如果任何一个元素没有通过验证,那么
validate_host_port_list
函数将触发断言错误。
- 如果所有元素都通过了
1.9.8 comma_separated_host_port()
- 定义了一个名为
comma_separated_host_port
的函数,该函数接受一个类型为str
的参数string
,并返回一个布尔类型的值。
def comma_separated_host_port(string: str) -> bool:
"""验证主机和端口项的列表。
使用由 CSV *string* 表示的列表调用 :func:`validate_host_port_list` 函数。
:param string: 由逗号分隔的主机和端口项列表。
:returns: 如果 CSV 字符串中的所有项都有效,则返回 ``True``。
"""
return validate_host_port_list([s.strip() for s in string.split(",")])
作用:
comma_separated_host_port
函数的具体作用是从一个逗号分隔的字符串中提取出一系列的主机和端口项,并验证这些项的有效性。具体来说:
- 字符串分割:
- 将输入的字符串
string
按照逗号,
进行分割,得到一个包含各个主机和端口项的列表。
- 将输入的字符串
- 去除空白字符:
- 对于列表中的每一项,去除该项两端的空白字符。
- 调用验证函数:
- 将处理后的列表传递给
validate_host_port_list
函数,进行验证。
- 将处理后的列表传递给
- 返回验证结果:
- 如果所有项都通过了验证,则返回
True
; - 如果有任何一项未能通过验证,则
validate_host_port_list
会返回False
,从而导致整个函数返回False
。
- 如果所有项都通过了验证,则返回
1.9.9 validate_host_port_listen()
- 定义了一个名为
validate_host_port_listen
的函数,该函数接受一个类型为str
的参数host_port
,并返回一个布尔类型的值。
def validate_host_port_listen(host_port: str) -> bool:
"""检查主机和端口是否有效并且可用于绑定。
使用 *listen* 设置为 ``True`` 调用 :func:`validate_host_port` 函数。
:param host_port: 要验证的主机和端口。必须是 ``host:ip`` 的格式。
:returns: 如果主机和端口有效并且可用于绑定,则返回 ``True``。
"""
return validate_host_port(host_port, listen=True)
作用:
validate_host_port_listen
函数的具体作用是验证一个指定的主机和端口是否有效,并且这个端口是否可用以供服务绑定。具体来说:
- 格式验证:
- 输入的
host_port
必须符合host:ip
的格式。
- 输入的
- 有效性验证:
- 通过
validate_host_port
函数来验证主机和端口是否符合预期的格式。
- 通过
- 端口绑定验证:
- 使用
listen=True
来指示validate_host_port
函数去检查这个端口是否已经被占用,即是否可以用于绑定新的服务。
- 使用
- 返回验证结果:
- 如果主机和端口有效,并且端口可用以绑定新的服务,则返回
True
; - 如果有任何一项验证失败,则
validate_host_port
函数可能会抛出异常或者返回False
,从而导致整个函数返回False
。
- 如果主机和端口有效,并且端口可用以绑定新的服务,则返回
1.9.10 validate_host_port_listen_multiple_hosts()
- 定义了一个名为
validate_host_port_listen_multiple_hosts
的函数,该函数接受一个类型为str
的参数host_port
,并返回一个布尔类型的值。
def validate_host_port_listen_multiple_hosts(host_port: str) -> bool:
"""检查主机(们)和端口是否有效并且可用于绑定。
使用 *listen* 和 *multiple_hosts* 设置为 ``True`` 调用 :func:`validate_host_port` 函数。
:param host_port: 要验证的主机(们)和端口。它可以是以下格式之一:
* ``host:ip``;或者
* ``host_1,host_2,...,host_n:port``
:returns: 如果主机(们)和端口有效并且可用于绑定,则返回 ``True``。
"""
# 调用 validate_host_port 函数,并设置 listen 参数为 True 和 multiple_hosts 参数为 True,这表示期望这个地址可用于绑定(即服务监听),并且支持多个主机绑定到同一个端口。
return validate_host_port(host_port, listen=True, multiple_hosts=True)
作用:
validate_host_port_listen_multiple_hosts
函数的具体作用是验证一个或多个指定的主机和端口是否有效,并且这个端口是否可用以供服务绑定。具体来说:
- 格式验证:
- 输入的
host_port
可以是单一主机和端口组合host:ip
的格式,也可以是多个主机共享一个端口的格式host_1,host_2,...,host_n:port
。
- 输入的
- 有效性验证:
- 通过
validate_host_port
函数来验证主机和端口是否符合预期的格式。
- 通过
- 端口绑定验证:
- 使用
listen=True
来指示validate_host_port
函数去检查这个端口是否已经被占用,即是否可以用于绑定新的服务。 - 使用
multiple_hosts=True
来指示validate_host_port
函数去验证多个主机是否可以绑定到同一个端口。
- 使用
- 返回验证结果:
- 如果主机(们)和端口有效,并且端口可用以绑定新的服务,则返回
True
; - 如果有任何一项验证失败,则
validate_host_port
函数可能会抛出异常或者返回False
,从而导致整个函数返回False
。
- 如果主机(们)和端口有效,并且端口可用以绑定新的服务,则返回
1.9.11 is_ipv4_address()
- 定义了一个名为
is_ipv4_address
的函数,该函数接受一个类型为str
的参数ip
,并返回一个布尔类型的值。
def is_ipv4_address(ip: str) -> bool:
"""检查 *ip* 是否是一个有效的 IPv4 地址。
:param ip: 要检查的 IP 地址。
:returns: 如果 IP 是一个 IPv4 地址,则返回 ``True``。
:raises:
:class:`~patroni.exceptions.ConfigParseError`: 如果 *ip* 不是一个有效的 IPv4 地址,则抛出此异常。
"""
try:
socket.inet_aton(ip)
except Exception:
raise ConfigParseError("Is not a valid ipv4 address")
return True
作用:
is_ipv4_address
函数的具体作用是验证一个给定的字符串 ip
是否代表了一个有效的 IPv4 地址。具体来说:
- IP 地址验证:
- 使用 Python 的
socket
模块中的inet_aton
方法来检查ip
是否是一个有效的 IPv4 地址。inet_aton
方法会将有效的 IPv4 地址转换成一个打包的 32 位二进制格式,如果输入不是一个合法的 IPv4 地址,则会抛出异常。
- 使用 Python 的
- 异常处理:
- 当
inet_aton
方法抛出异常时,函数会捕获这个异常,并重新抛出一个ConfigParseError
异常,附带错误信息表明输入的ip
不是一个有效的 IPv4 地址。
- 当
- 返回验证结果:
- 如果
ip
是有效的 IPv4 地址,则返回True
; - 如果
ip
不是有效的 IPv4 地址,则不会到达返回语句,因为会抛出异常。
- 如果
1.9.12 is_ipv6_address()
- 定义了一个名为
is_ipv6_address
的函数,该函数接受一个类型为str
的参数ip
,并返回一个布尔类型的值。
def is_ipv6_address(ip: str) -> bool:
"""检查 *ip* 是否是一个有效的 IPv6 地址。
:param ip: 要检查的 IP 地址。
:returns: 如果 IP 是一个 IPv6 地址,则返回 ``True``。
:raises:
:class:`~patroni.exceptions.ConfigParseError`: 如果 *ip* 不是一个有效的 IPv6 地址,则抛出此异常。
"""
try:
socket.inet_pton(socket.AF_INET6, ip)
except Exception:
raise ConfigParseError("Is not a valid ipv6 address")
return True
作用:
is_ipv6_address
函数的具体作用是验证一个给定的字符串 ip
是否代表了一个有效的 IPv6 地址。具体来说:
- IP 地址验证:
- 使用 Python 的
socket
模块中的inet_pton
方法,并指定地址族为AF_INET6
来检查ip
是否是一个有效的 IPv6 地址。inet_pton
方法会将有效的 IPv6 地址转换成一个打包的 128 位二进制格式,如果输入不是一个合法的 IPv6 地址,则会抛出异常。
- 使用 Python 的
- 异常处理:
- 当
inet_pton
方法抛出异常时,函数会捕获这个异常,并重新抛出一个ConfigParseError
异常,附带错误信息表明输入的ip
不是一个有效的 IPv6 地址。
- 当
- 返回验证结果:
- 如果
ip
是有效的 IPv6 地址,则返回True
; - 如果
ip
不是有效的 IPv6 地址,则不会到达返回语句,因为会抛出异常。
- 如果
1.9.13 get_bin_name()
- 定义了一个名为
get_bin_name
的函数,该函数接受一个类型为str
的参数bin_name
,并返回一个字符串类型的结果。
def get_bin_name(bin_name: str) -> str:
"""获取 ``postgresql.bin_name[*bin_name*]`` 配置选项的值。
:param bin_name: 一个从 ``postgresql.bin_name`` 配置中检索的键。
:returns: 如果存在,则返回 ``postgresql.bin_name[*bin_name*]`` 的值,否则返回 *bin_name*。
"""
if TYPE_CHECKING: # pragma: no cover
assert isinstance(schema.data, dict)
return (schema.data.get('postgresql', {}).get('bin_name', {}) or EMPTY_DICT).get(bin_name, bin_name)
作用:
get_bin_name
函数的具体作用是从配置中获取与特定二进制文件名相关的配置值。具体来说:
- 获取配置值:
- 尝试从
schema.data
中获取postgresql
字段。 - 如果
postgresql
存在,则进一步获取其中的bin_name
子字段。 - 如果
bin_name
字段存在,则从中获取与传入的bin_name
键对应的值。
- 尝试从
- 处理缺失情况:
- 如果任何一个步骤中相应的字段不存在,则返回一个空字典(
EMPTY_DICT
)。 - 最终如果没有找到对应的配置值,则返回传入的
bin_name
值本身。
- 如果任何一个步骤中相应的字段不存在,则返回一个空字典(
1.9.14 validate_data_dir()
- 定义了一个名为
validate_data_dir
的函数,该函数接受一个类型为str
的参数data_dir
,并返回一个布尔类型的值。
def validate_data_dir(data_dir: str) -> bool:
"""验证 ``postgresql.data_dir`` 配置选项的值。
它要求 ``postgresql.data_dir`` 被设置并且满足以下条件之一:
* 指向一个尚不存在的路径;或
* 指向一个空目录;或
* 指向一个似乎包含有效 PostgreSQL 数据目录的非空目录。
:param data_dir: ``postgresql.data_dir`` 配置选项的值。
:returns: 如果 PostgreSQL 数据目录有效,则返回 ``True``。
:raises:
:class:`~patroni.exceptions.ConfigParseError`:
* 如果没有给出 *data_dir*;或
* 如果 *data_dir* 是一个文件而不是一个目录;或
* 如果 *data_dir* 是一个非空目录,并且:
* 目录中没有 ``PG_VERSION`` 文件
* 目录中没有 ``pg_wal``/``pg_xlog`` 目录
* ``PG_VERSION`` 内容与 ``postgres --version`` 报告的主要版本不匹配
"""
# 如果 data_dir 是空字符串
if not data_dir:
raise ConfigParseError("is an empty string")
# 如果 data_dir 存在并且不是目录
elif os.path.exists(data_dir) and not os.path.isdir(data_dir):
raise ConfigParseError("is not a directory")
# 如果 data_dir 不是空目录,则继续检查
elif not data_directory_empty(data_dir):
# 如果 data_dir 中不存在 PG_VERSION 文件
if not os.path.exists(os.path.join(data_dir, "PG_VERSION")):
raise ConfigParseError("doesn't look like a valid data directory")
# 否则,读取 data_dir 中的 PG_VERSION 文件内容,并去除首尾空白字符
else:
with open(os.path.join(data_dir, "PG_VERSION"), "r") as version:
pgversion = version.read().strip()
# 根据 pgversion 的值确定 waldir 的名称
waldir = ("pg_wal" if float(pgversion) >= 10 else "pg_xlog")
if not os.path.isdir(os.path.join(data_dir, waldir)):
raise ConfigParseError("data dir for the cluster is not empty, but doesn't contain"
" \"{}\" directory".format(waldir))
# 如果启用了类型检查(TYPE_CHECKING 为 True)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(schema.data, dict)
# 获取 postgresql.bin_dir 的值
bin_dir = schema.data.get("postgresql", {}).get("bin_dir", None)
major_version = get_major_version(bin_dir, get_bin_name('postgres'))
if pgversion != major_version:
raise ConfigParseError("data_dir directory postgresql version ({}) doesn't match with "
"'postgres --version' output ({})".format(pgversion, major_version))
return True
作用:
validate_data_dir
函数的具体作用是验证一个给定的数据目录路径 data_dir
是否适合用于 PostgreSQL 数据目录。具体来说:
- 检查路径是否存在:
- 如果路径不存在,那么它被认为是可以用于初始化一个新的 PostgreSQL 数据目录。
- 检查路径是否为空目录:
- 如果路径指向的是一个空目录,那么它被认为是可以用于初始化一个新的 PostgreSQL 数据目录。
- 检查路径是否为有效的 PostgreSQL 数据目录:
- 如果路径指向的是一个非空目录,则需要进一步检查:
- 目录中是否存在
PG_VERSION
文件。 - 目录中是否存在
pg_wal
或pg_xlog
目录,取决于 PostgreSQL 的版本。 PG_VERSION
文件中的版本号是否与通过postgres --version
命令获得的主要版本号相匹配。
- 目录中是否存在
- 如果路径指向的是一个非空目录,则需要进一步检查:
- 返回验证结果:
- 如果所有检查均通过,则返回
True
; - 如果任何一项检查未通过,则抛出
ConfigParseError
异常,并提供详细的错误信息。
- 如果所有检查均通过,则返回
1.9.15 validate_binary_name()
- 定义了一个名为
validate_binary_name
的函数,该函数接受一个类型为str
的参数bin_name
,并返回一个布尔类型的值。
def validate_binary_name(bin_name: str) -> bool:
"""验证 ``postgresql.binary_name[*bin_name*]`` 配置选项的值。
如果设置了 ``postgresql.bin_dir`` 并且 *bin_name* 的值满足以下条件:
* ``postgresql.bin_dir`` 加上 *bin_name* 的路径存在;并且
* 上述路径可执行
如果未设置 ``postgresql.bin_dir``,则验证 *bin_name* 的值满足以下条件:
* 使用 ``which`` 在系统 PATH 中找到 *bin_name*
:param bin_name: ``postgresql.bin_name[*bin_name*]`` 的值。
:returns: 如果条件为真,则返回 ``True``。
:raises:
:class:`~patroni.exceptions.ConfigParseError` 如果:
* *bin_name* 未设置;或者
* ``postgresql.bin_dir`` 加上 *bin_name* 的路径不存在;或者
* 上述路径不可执行;或者
* *bin_name* 在系统 PATH 中找不到
"""
# 如果 bin_name 是空字符串
if not bin_name:
raise ConfigParseError("is an empty string")
if TYPE_CHECKING: # pragma: no cover
assert isinstance(schema.data, dict)
bin_dir = schema.data.get('postgresql', {}).get('bin_dir', None)
if not shutil.which(bin_name, path=bin_dir):
raise ConfigParseError(f"does not contain '{bin_name}' in '{bin_dir or '$PATH'}'")
return True
作用:
validate_binary_name
函数的具体作用是验证一个给定的二进制文件名 bin_name
是否是有效的。具体来说:
- 检查二进制文件名是否为空:
- 如果
bin_name
是空字符串,则直接抛出异常。
- 如果
- 检查二进制文件是否存在且可执行:
- 如果设置了
postgresql.bin_dir
,则检查bin_name
是否存在于bin_dir
中,并且是否可执行。 - 如果未设置
postgresql.bin_dir
,则检查bin_name
是否存在于系统的 PATH 中,并且是否可执行。
- 如果设置了
- 返回验证结果:
- 如果所有检查均通过,则返回
True
; - 如果任何一项检查未通过,则抛出
ConfigParseError
异常,并提供详细的错误信息。
- 如果所有检查均通过,则返回
1.9.16 _get_type_name()
- 定义了一个名为
_get_type_name
的函数,该函数接受一个类型为Any
的参数python_type
,并返回一个字符串类型的结果。
def _get_type_name(python_type: Any) -> str:
"""获取给定 Python 类型的用户友好的名称。
:param python_type: Python 类型,应该获取其用户友好的名称。
:returns: 给定 Python 类型的用户友好的名称。
"""
# 定义了一个字典 types ,用于存储一些常见 Python 类型及其对应的用户友好的名称。
types: Dict[Any, str] = {str: 'a string', int: 'an integer', float: 'a number',
bool: 'a boolean', list: 'an array', dict: 'a dictionary'}
return types.get(python_type, getattr(python_type, __name__, "unknown type"))
作用:
_get_type_name
函数的具体作用是获取给定 Python 类型的一个用户友好的名称。具体来说:
- 映射常见类型:
- 对于一些常见的 Python 类型如
str
,int
,float
,bool
,list
,dict
,函数会返回一个用户更易理解的名字,比如'a string'
,'an integer'
,'a number'
,'a boolean'
,'an array'
,'a dictionary'
。
- 对于一些常见的 Python 类型如
- 处理未知类型:
- 如果给定的类型不在预定义的映射中,函数会尝试获取该类型的名称属性 (
__name__
)。 - 如果类型没有名称属性或者名称属性无法获取,则返回一个默认的描述
"unknown type"
。
- 如果给定的类型不在预定义的映射中,函数会尝试获取该类型的名称属性 (
1.9.17 assert_()
- 定义了一个名为
assert_
的函数,该函数接受两个参数:一个是布尔类型的condition
,另一个是字符串类型的message
,默认值为"Wrong value"
。该函数没有返回值(返回类型为None
)。
def assert_(condition: bool, message: str = "Wrong value") -> None:
"""断言给定的条件为 ``True``。
如果断言失败,则抛出一条消息。
:param condition: 要断言的条件的结果。
:param message: 如果条件为 ``False`` 时要抛出的消息。
"""
assert condition, message
作用:
assert_
函数的具体作用是在开发过程中用来检查某个条件是否为真。如果条件不满足,则会抛出一个异常,并附带一个错误信息。具体来说:
- 断言条件:
- 当调用
assert_
函数时,它接收一个布尔表达式condition
作为参数,并检查这个表达式是否为True
。
- 当调用
- 错误处理:
- 如果
condition
为True
,则函数什么也不做,正常退出。 - 如果
condition
为False
,则函数会抛出一个AssertionError
,并附带一个可选的错误消息message
。
- 如果
1.9.18 validate_watchdog_mode()
- 定义了一个名为
validate_watchdog_mode
的函数,该函数接受一个类型为Any
的参数value
,并返回None
。
def validate_watchdog_mode(value: Any) -> None:
"""验证 ``watchdog.mode`` 配置选项。
:param value: 要验证的 ``watchdog.mode`` 的值。
"""
assert_(isinstance(value, (str, bool)), "expected type is not a string")
assert_(value in (False, "off", "automatic", "required"))
作用:
validate_watchdog_mode
函数的具体作用是验证一个给定的值 value
是否适合作为 watchdog.mode
配置项的有效值。具体来说:
- 类型验证:
- 检查
value
是否为字符串类型或布尔类型。如果不是这两种类型之一,则抛出异常。
- 检查
- 值验证:
- 检查
value
是否为预定义的几个合法值之一:False
、"off"
、"automatic"
或"required"
。如果value
不是这些合法值之一,则抛出异常。
- 检查
- 返回结果:
- 如果所有验证均通过,则函数正常退出,不返回任何值;
- 如果任何一项验证未通过,则抛出
AssertionError
异常,并附带相应的错误信息。
1.9.19 类:Result
- 定义了一个名为
Result
的类,继承自object
。 Result
类的具体作用是表示一次配置验证的结果。具体来说:- 属性初始化:
status
: 表示验证是否成功。path
: 配置项在 YAML 树中的路径。data
: 配置项的值。level
: 如果有错误发生,表示错误的严重程度。error
: 如果验证失败,表示具体的错误信息。
- 属性逻辑处理:
- 在初始化时,只有当
status
为False
时,error
属性才会被设置。 - 如果
status
为True
,则error
属性被设置为None
。
- 在初始化时,只有当
- 对象表示:
- 通过
__repr__
方法,可以方便地查看验证结果,包括配置项路径、值以及可能的错误信息。
- 通过
- 属性初始化:
class Result(object):
"""表示一次给定验证的结果。
:ivar status: 如果验证成功。
:ivar path: 配置选项的 YAML 树路径。
:ivar data: 配置选项的值。
:ivar level: 错误级别,在出现错误时。
:ivar error: 如果验证失败的错误消息,否则为 ``None``。
1.9.19.1 __init__()
- 定义了类的构造方法
__init__
,接受多个参数以初始化一个Result
对象。
def __init__(self, status: bool, error: OptionalType[str] = "didn't pass validation", level: int = 0,
path: str = "", data: Any = "") -> None:
"""基于给定的参数创建一个 :class:`Result` 对象。
.. note::
``error`` 属性仅在 *status* 失败时设置。
:param status: 如果验证成功。
:param error: 如果验证失败,与所执行的验证相关的错误消息。
:param level: 错误级别,在出现错误时。
:param path: 配置选项的 YAML 树路径。
:param data: 配置选项的值。
"""
self.status = status
self.path = path
self.data = data
self.level = level
self._error = error
if not self.status:
self.error = error
else:
self.error = None
作用:
__init__
是一个特殊的方法,也称为构造函数或初始化器,在 Python 中用于初始化新创建的对象。当创建一个新的类实例时,__init__
方法会被自动调用。具体来说:
- 初始化对象属性:
status
:表示验证是否成功。path
:配置项在 YAML 树中的路径。data
:配置项的值。level
:如果验证失败,表示错误的严重程度。error
:如果验证失败,存储具体的错误信息。
- 逻辑处理:
- 只有当
status
为False
时,error
属性才会被设置为传入的error
参数。 - 如果
status
为True
,则error
属性被设置为None
。
- 只有当
- 封装私有属性:
- 使用
_error
来存储原始错误信息,而实际使用的error
属性则根据status
来决定是否赋值。
- 使用
1.9.19.2 __repr__()
- 定义了类的
__repr__
方法,该方法返回一个表示该对象的字符串。
def __repr__(self) -> str:
"""显示配置路径和值。如果验证失败,也显示错误消息。"""
return str(self.path) + (" " + str(self.data) + " " + str(self._error) if self.error else "")
作用:
__repr__
是一个特殊的方法,用于返回一个表示对象的字符串,这个字符串通常是可读的,并且尽可能地是独一无二的。这个方法返回的字符串通常可以用来重新创建对象。
- 返回对象的描述字符串:
- 返回一个字符串,该字符串包含了配置路径和值。
- 如果验证失败,则还包含错误信息。
- 提高调试便利性:
- 通过打印
Result
对象,可以快速查看验证的结果,包括配置项的位置、配置项的值以及可能存在的错误信息。
- 通过打印
1.9.20 类:Case
- 定义了一个名为
Case
的类,继承自object
。 Case
类的具体作用是定义如何验证一组配置选项。具体来说:- 模式定义:
schema
字典定义了如何验证配置选项。字典的键是配置项的名字,值是验证函数或预期的数据类型。
- 验证逻辑:
Case
对象根据传入的schema
来确定如何对配置选项进行验证。- 当配置项存在时,将使用对应的验证函数或类型检查来验证配置项的值。
- 模式定义:
class Case(object):
"""映射如何验证一组可用的配置选项。
.. note::
它应该与一个 :class:`Or` 对象一起使用。:class:`Or` 对象将在给定上下文中定义可能的配置选项列表,
而 :class:`Case` 对象将规定如何验证它们(如果它们被设置了的话)。
"""
1.9.20.1 __init__()
- 定义了类的构造方法
__init__
,接受一个字典schema
作为参数,以初始化一个Case
对象。
def __init__(self, schema: Dict[str, Any]) -> None:
"""创建一个 :class:`Case` 对象。
:param schema: 用于验证一组可能在配置中存在的属性的模式。
每个键是给定范围内可用并且应该被验证的配置,
相关的值是验证函数或预期的类型。
:Example:
.. code-block:: python
Case({
"host": validate_host_port,
"url": str,
})
这将会检查 ``host`` 配置(如果给出),是否基于 :func:`validate_host_port` 有效,
并且还会检查 ``url`` 配置(如果给出),是否是一个 ``str`` 实例。
"""
self._schema = schema
作用:
在 Case
类中,__init__
函数的主要作用如下:
- 接收初始化参数:
- 接受一个名为
schema
的字典作为参数。这个字典定义了如何验证一组配置选项。
- 接受一个名为
- 存储初始化参数:
- 将传入的
schema
字典存储到实例的一个私有属性_schema
中。这意味着每个Case
对象都有自己的schema
,用于后续的配置验证操作。
- 将传入的
1.9.21 类:Or
- 定义了一个名为
Or
的类,继承自object
。 Or
类的具体作用是定义一组可能的选项或验证规则。具体来说:- 选项定义:
Or
类可以表示一组配置选项,这些选项在某个特定的上下文内是有效的。- 它也可以表示一组验证函数或预期类型,这些用于验证某个特定配置选项的有效性。
- 验证逻辑:
Or
对象可以被用来定义多个可能的配置选项,意味着这些选项在给定的上下文中可以选择其中一个。- 同样,对于某个配置选项,
Or
对象可以定义多个验证规则,只要满足其中一个即可认为配置项是有效的。
- 选项定义:
class Or(object):
"""表示可用的一系列选项。
它可以表示给定范围内可用的一系列配置选项,或者是给定配置选项的一系列验证函数和/或预期类型。
"""
1.9.21.1 __init__()
- 定义了类的构造方法
__init__
,接受任意数量的参数args
作为位置参数,并初始化一个Or
对象。
def __init__(self, *args: Any) -> None:
"""创建一个 :class:`Or` 对象。
:param `*args`: 任何调用者希望存储在这个 :class:`Or` 对象中的参数。
:Example:
.. code-block:: python
Or("host", "hosts"): Case({
"host": validate_host_port,
"hosts": Or(comma_separated_host_port, [validate_host_port]),
})
外部的 :class:`Or` 用于定义 ``host`` 和 ``hosts`` 是此范围内的可能选项。
内部的 :class`Or` 在 ``hosts`` 键值中用于定义 ``hosts`` 选项如果通过 :func:`comma_separated_host_port` 或 :func:`validate_host_port` 中的任何一个验证即为有效。
"""
self.args = args
作用:
以下是 __init__
函数的具体功能描述:
- 接收参数:
- 接收不定数量的位置参数
*args
。这允许我们在创建Or
对象时传递任意数量的参数。
- 接收不定数量的位置参数
- 存储参数:
- 将接收到的所有参数存储到实例变量
self.args
中。这意味着每个Or
对象都会记住它在创建时所接收到的参数。
- 将接收到的所有参数存储到实例变量
1.9.22 类:AtMostOne
- 定义了一个名为
AtMostOne
的类,继承自object
。 AtMostOne
类的具体作用是定义一组配置选项中的最多只能有一个被设置。具体来说:- 选项限制:
AtMostOne
类可以表示一组配置选项,这些选项在某个特定的上下文内最多只能有一个被设置。- 这意味着如果有多个选项,那么这些选项之间是互斥的,即不能同时设置。
- 验证逻辑:
AtMostOne
对象可以被用来定义多个可能的配置选项,并且这些选项在配置过程中只能选择其中一个。- 通常与
Case
对象一起使用,用于指定哪些配置项是互斥的。
- 选项限制:
class AtMostOne(object):
"""标记最多只能从一个 :class:`Case` 中提供一个选项。
表示给定范围内可能的配置选项列表,其中最多只能实际提供一个。
.. note::
应该与一个 :class:`Case` 对象一起使用。
"""
1.9.22.1 __init__()
- 定义了类的构造方法
__init__
,接受任意数量的字符串参数args
,并初始化一个AtMostOne
对象。
def __init__(self, *args: str) -> None:
"""创建一个 :class:`AtMostOne` 对象。
:param `*args`: 任何调用者希望存储在这个 :class:`AtMostOne` 对象中的参数。
:Example:
.. code-block:: python
AtMostOne("nofailover", "failover_priority"): Case({
"nofailover": bool,
"failover_priority": IntValidator(min=0, raise_assert=True),
})
:class`AtMostOne` 对象用于定义最多只能提供 ``nofailover`` 和 ``failover_priority`` 中的一个。
"""
self.args = args
作用:
以下是 __init__
函数的具体功能描述:
- 接收参数:
- 接收不定数量的字符串参数
*args
。这允许我们在创建AtMostOne
对象时传递任意数量的字符串参数。
- 接收不定数量的字符串参数
- 存储参数:
- 将接收到的所有字符串参数存储到实例变量
self.args
中。这意味着每个AtMostOne
对象都会记住它在创建时所接收到的字符串参数。
- 将接收到的所有字符串参数存储到实例变量
1.9.23 类:Optional
- 定义了一个名为
Optional
的类,继承自object
。 Optional
类的具体作用是定义一个配置选项为可选的,并提供一个默认值。具体来说:- 配置选项标记:
Optional
类可以用来标记一个配置选项是可选的,这意味着如果用户没有提供这个配置项,系统仍然可以正常工作,并使用一个预设的默认值。
- 默认值设定:
- 当配置选项未被用户提供时,
Optional
类允许设置一个默认值,这个值将在配置选项缺失时被使用。
- 当配置选项未被用户提供时,
- 配置选项标记:
class Optional(object):
"""标记一个配置选项为可选的。
:ivar name: 配置选项的名称。
:ivar default: 如果配置选项没有明确提供,则设置的值。
"""
1.9.23.1 __init__()
- 定义了类的构造方法
__init__
,接受两个参数name
和default
,其中name
是配置项的名称,default
是如果配置项没有被明确提供时的默认值,默认为None
。
def __init__(self, name: str, default: OptionalType[Any] = None) -> None:
"""创建一个 :class:`Optional` 对象。
:param name: 配置选项的名称。
:param default: 如果配置选项没有明确提供,则设置的值。
"""
self.name = name
self.default = default
作用:
以下是 __init__
函数的具体功能描述:
- 接收参数:
- 接收两个参数:
name
和default
。name
:配置选项的名称,类型为str
。default
:如果配置选项没有被明确提供,则使用的默认值,默认为None
。
- 接收两个参数:
- 存储参数:
- 将接收到的
name
和default
分别存储到实例变量self.name
和self.default
中。
- 将接收到的
1.9.24 类:Directory
- 定义了一个名为
Directory
的类,继承自object
。 Directory
类的具体作用是对一个给定的目录进行验证,确保该目录包含预期的文件和可执行文件。具体来说:- 配置选项的标识:
Directory
类可以用来标记一个目录应该包含哪些文件和可执行文件。
- 验证逻辑:
- 当需要验证一个目录是否符合预期时,
Directory
类允许指定一组必须存在的文件路径和一组必须存在的可执行文件路径。 - 使用
validate
方法可以对目录进行验证,检查是否包含预期的文件和可执行文件。
- 当需要验证一个目录是否符合预期时,
- 配置选项的标识:
class Directory(object):
"""检查一个目录是否包含预期的文件。
此类对象的属性由其 :func:`validate` 方法使用。
:param contains: 应该存在于给定目录下的路径列表。
:param contains_executable: 应该直接存在于给定目录下的可执行文件列表。
"""
1.9.24.1 __init__()
- 定义了类的构造方法
__init__
,接受两个参数contains
和contains_executable
,均为可选参数,默认值为None
。
def __init__(self, contains: OptionalType[List[str]] = None,
contains_executable: OptionalType[List[str]] = None) -> None:
"""创建一个 :class:`Directory` 对象。
:param contains: 应该存在于给定目录下的路径列表。
:param contains_executable: 应该直接存在于给定目录下的可执行文件列表。
"""
self.contains = contains
self.contains_executable = contains_executable
作用:
- 初始化
Directory
对象。 - 接受两个参数
contains
和contains_executable
,分别表示应存在于目录中的文件路径列表以及应存在于目录中的可执行文件列表。 - 将这两个参数分别存储在实例变量
self.contains
和self.contains_executable
中。
1.9.24.2 _check_executables()
- 定义了一个私有方法
_check_executables
,接受一个可选参数path
,默认值为None
。
def _check_executables(self, path: OptionalType[str] = None) -> Iterator[Result]:
"""检查contains_executable列表中的所有可执行文件是否存在于给定目录或‘ PATH ’中。
:param path: 可选的路径到基准目录,其中可执行文件将被验证。
如果未提供,则在 ``PATH`` 中检查。
:yields: 如果任何检查失败,则产生包含可执行文件名称的错误消息的对象。
"""
for program in self.contains_executable or []:
if not shutil.which(program, path=path):
yield Result(False, f"does not contain '{program}' in '{(path or '$PATH')}'")
作用:
- 检查
contains_executable
列表中的所有可执行文件是否存在于指定路径或环境变量PATH
中。 - 如果某个可执行文件不在
path
或PATH
中,则生成一个Result
对象,表示检查失败,并带有错误信息。
1.9.24.3 validate()
- 定义了一个方法
validate
,接受一个参数name
,代表要验证的基准目录路径。
def validate(self, name: str) -> Iterator[Result]:
"""检查是否可以在*name*目录下找到预期的路径和可执行文件。
:param name: 路径到基准目录,其中路径和可执行文件将被验证。
如果 name 未提供,则针对 ``PATH`` 进行检查。
:yields: 如果任何检查失败,则产生包含相关错误消息的对象。
"""
# 检查基准目录是否存在,并且是否为一个目录
if not name:
yield from self._check_executables()
elif not os.path.exists(name):
yield Result(False, "Directory '{}' does not exist.".format(name))
elif not os.path.isdir(name):
yield Result(False, "'{}' is not a directory.".format(name))
else:
if self.contains:
for path in self.contains:
if not os.path.exists(os.path.join(name, path)):
yield Result(False, "'{}' does not contain '{}'".format(name, path))
yield from self._check_executables(path=name)
作用:
- 检查指定目录是否包含预期的路径和可执行文件。
- 如果目录不存在、不是目录、不包含预期的文件路径或不包含预期的可执行文件,则生成
Result
对象表示检查失败,并带有相应的错误信息。
1.9.25 类:BinDirectory
- 定义了一个名为
Or
的类,继承自object
。 BinDirectory
类的具体作用是对一个 Postgres 二进制目录进行验证,确保该目录包含一组特定的可执行文件。具体来说:- 继承关系:
BinDirectory
继承自Directory
类,因此它可以利用Directory
类提供的验证逻辑。
- 扩展功能:
- 它扩展了父类的功能,特别指定了 Postgres 二进制目录应该包含哪些可执行文件。
- 根据配置的
postgresql.bin_name
,可以对BINARIES
列表进行翻译处理,以适应不同的配置需求。
- 继承关系:
class BinDirectory(Directory):
"""检查一个 Postgres 二进制目录是否包含预期的文件。
它是 :class:`Directory` 的子类,具有扩展能力:根据配置的 ``postgresql.bin_name`` 翻译 ``BINARIES``(如果有配置)。
:cvar BINARIES: 应该直接存在于给定 Postgres 二进制目录下的可执行文件列表。
"""
# ``pg_rewind`` 不在此列表中,因为 Patroni 使用它是可选的。此外,它默认情况下在 Patroni 支持的 Postgres 9.3 和 9.4 版本中不可用。
BINARIES = ["pg_ctl", "initdb", "pg_controldata", "pg_basebackup", "postgres", "pg_isready"]
1.9.25.1 validate()
- 定义了一个名为
validate
的方法,它接受一个字符串参数name
并返回一个Result
对象的迭代器(Iterator[Result]
)。
def validate(self, name: str) -> Iterator[Result]:
"""检查在 *name* 二进制目录下是否能找到预期的可执行文件。
:param name: 要验证可执行文件的基准目录路径。如果 *name* 未提供,则针对 PATH 进行检查。
:yields: 如果任何检查失败,则产生包含与失败相关的错误消息的对象。
"""
self.contains_executable: List[str] = [get_bin_name(binary) for binary in self.BINARIES]
yield from super().validate(name)
作用:
BinDirectory
类中的 validate
方法的具体作用是对一个给定的 Postgres 二进制目录进行验证,确保该目录包含一组特定的可执行文件,并且这些可执行文件的名称可能根据特定的配置进行了调整。
- 可执行文件名称的调整:
- 在调用父类的
validate
方法之前,BinDirectory
的validate
方法会先根据BINARIES
列表中的每个元素,通过get_bin_name
函数来获取实际的可执行文件名称。这样做的目的是为了支持不同的配置或操作系统环境下的可执行文件命名差异。
- 在调用父类的
- 调用父类验证逻辑:
- 使用调整后的
self.contains_executable
列表,调用父类Directory
的validate
方法来进行验证。父类的validate
方法会检查目录是否存在,是否为目录,以及是否包含指定的可执行文件。
- 使用调整后的
1.9.26 类:Schema
- 定义了一个名为
Schema
的类,继承自 Python 内置的object
类。 Schema
类的具体作用是定义一个配置模式(schema),这个模式描述了配置项的结构和验证规则。具体来说:- 配置项定义:
Schema
类允许定义一个配置模式,该模式包含所有可用的配置选项及其验证规则。
- 验证执行:
- 当
Schema
对象被调用或其validate
方法被调用时,将执行定义的验证规则。
- 当
- 验证器类型:
validator
变量可以是多种类型之一:- 字符串类型(
str
):表示需要一个字符串值; - 类型对象(
type
):表示需要一个给定类型的值; - 可调用对象(
callable
):表示需要按照可调用对象中定义的代码进行验证。如果可调用对象有expected_type
属性,则会在调用其代码之前检查配置值的类型; - 列表类型(
list
):表示需要一个或多个值的列表; - 字典类型(
dict
):表示需要一个代表 YAML 配置树的字典。
- 字符串类型(
- 配置项定义:
class Schema(object):
"""定义一个配置模式。
它包含每个范围内可用的所有配置选项,包括应该针对每个选项执行的验证。每当调用 :class:`Schema` 对象或其 :func:`validate` 方法时,都会执行这些验证。
:ivar validator: 配置模式的验证器。可以是以下类型之一:
* :class:`str`:定义需要一个字符串值;或
* :class:`type`:任何 :class:`type` 的子类,定义需要给定类型的值;或
* ``callable``:任何可调用对象,定义验证将遵循可调用对象中定义的代码。如果可调用对象包含一个 ``expected_type`` 属性,则会在调用可调用对象的代码之前检查配置值是否为预期类型;或
* :class:`list`:列表代表配置中的一个或多个值;或
* :class:`dict`:字典代表 YAML 配置树。
"""
1.9.26.1 __init__()
- 定义了一个构造函数,它接受一个名为
validator
的参数,该参数可以是dict
、list
或者任意类型。
def __init__(self, validator: Union[Dict[Any, Any], List[Any], Any]) -> None:
"""创建一个 :class:`Schema` 对象。
.. note::
预期这个类最初实例化时会有一个基于 :class:`dict` 的 *validator* 参数。想法是这个字典代表了配置选项的完整 YAML 树。:func:`validate` 方法随后会递归地遍历配置树,在新的“基路径”上创建新的 :class:`Schema` 实例,以验证树的结构和叶节点的值。递归在叶节点停止,此时会执行实际设置值的检查。
:param validator: 配置模式的验证器。可以是以下类型之一:
* :class:`str`:定义需要一个字符串值;或
* :class:`type`:任何 :class:`type` 的子类,定义需要给定类型的值;或
* ``callable``:任何可调用对象,定义验证将遵循可调用对象中定义的代码。如果可调用对象包含一个 ``expected_type`` 属性,则会在调用可调用对象的代码之前检查配置值是否为预期类型;或
* :class:`list`:列表代表配置中的一个或多个值;或
* :class:`dict`:字典代表 YAML 配置树。
上述列表中的前三个项目在这里称为“基本验证器”,它们导致递归停止。
如果 *validator* 是一个 :class:`dict`,那么你应该遵循这些规则:
* 对于键,它可以是:
* 一个 :class:`str` 实例。它将是配置选项的名称;或
* 一个 :class:`Optional` 实例。该对象的 ``name`` 属性将是配置选项的名称,这个类使配置选项对于用户来说是可选的,允许其不在 YAML 中指定;或
* 一个 :class:`Or` 实例。该对象的 ``args`` 属性将包含一个配置选项名称的元组。至少其中一个应该由用户在 YAML 中指定;
* 对于值,它可以是:
* 一个新的 :class:`dict` 实例。它将代表 YAML 配置树中的新层级;或
* 一个 :class:`Case` 实例。这是必需的,如果这个值的键是一个 :class:`Or` 实例,并且 :class:`Case` 实例用于将 :class:`Or` 中的每个 ``args`` 映射到 :class:`Case` 中相应的基本验证器;或
* 一个包含一个或多个基本验证器的 :class:`Or` 实例;或
* 一个包含单个基本验证器的 :class:`list` 实例;或
* 一个基本验证器。
:Example:
.. code-block:: python
Schema({
"application_name": str,
"bind": {
"host": validate_host,
"port": int,
},
"aliases": [str],
Optional("data_directory"): "/var/lib/myapp",
Or("log_to_file", "log_to_db"): Case({
"log_to_file": bool,
"log_to_db": bool,
}),
"version": Or(int, float),
})
这个示例模式定义了你的 YAML 配置应遵循这些规则:
* 必须包含一个 ``application_name`` 项,其值应为 :class:`str` 实例;
* 必须包含一个 ``bind.host`` 项,其值应按函数 ``validate_host`` 有效;
* 必须包含一个 ``bind.port`` 项,其值应为 :class:`int` 实例;
* 必须包含一个 ``aliases`` 项,其值应为 :class:`list` 的 :class:`str` 实例;
* 可以选择包含一个 ``data_directory`` 项,其值应为字符串;
* 必须包含 ``log_to_file`` 或 ``log_to_db`` 中的一个,其值应为 :class:`bool` 实例;
* 必须包含一个 ``version`` 项,其值应为 :class:`int` 或 :class:`float` 实例。
"""
self.validator = validator
作用:
1.9.26.2 __call__()
- 定义了一个
__call__
方法,该方法使得Schema
类的实例可以像函数一样被调用。它接受一个名为data
的参数,该参数可以是任何类型的数据,并返回一个字符串列表,其中包含了验证过程中发现的错误信息。
def __call__(self, data: Any) -> List[str]:
"""使用在此模式中定义的规则对数据进行验证。
:param data: 要根据 `validator` 验证的配置。
:returns: 在验证 *data* 时识别出的错误列表(如果有)。
"""
errors: List[str] = []
# 遍历 self.validate(data) 返回的结果
for i in self.validate(data):
if not i.status:
errors.append(str(i))
return errors
作用:
__call__
方法的具体作用是在给定的配置数据上执行验证,依据在 Schema
实例中定义的验证规则。具体来说:
- 执行验证:
- 当
Schema
实例被当作函数调用时,__call__
方法会被触发。 - 它使用内部的
validator
对传入的data
进行验证。
- 当
- 收集错误:
- 在验证过程中,如果发现任何不符合验证规则的地方,就会记录下错误信息。
- 所有的错误信息最终会被收集到一个列表中。
- 返回结果:
- 方法最终返回一个字符串列表,其中包含了所有在验证过程中发现的错误信息。
- 如果没有错误发生,则返回一个空列表。
1.9.26.3 validate()
- 定义了一个
validate
方法,它接受一个名为data
的参数,该参数可以是dict
或任何类型的数据,并返回一个Result
对象的迭代器。每个Result
对象包含了验证过程中的结果信息。
def validate(self, data: Union[Dict[Any, Any], Any]) -> Iterator[Result]:
"""根据给定的配置执行模式中的所有验证。
它首先检查 *data* 参数类型是否与 `validator` 属性的类型一致。
此外:
* 如果 `validator` 属性是一个可调用对象,则调用它来验证 *data* 参数。在此之前,如果 `validator` 包含一个 `expected_type` 属性,则检查 *data* 参数是否符合预期类型。
* 如果 `validator` 属性是一个可迭代对象(如 :class:`dict`、:class:`list`、:class:`Directory` 或 :class:`Or`),则遍历它以验证 *data* 参数中对应的每一项。
:param data: 要根据 `validator` 验证的配置。
:yields: 如果任何检查失败,则产生带有相关失败信息的对象。
"""
# 将传入的 data 赋值给 self.data,以便后续在方法内部使用
self.data = data
# 新的‘ Schema ’对象可以在验证给定的‘ Schema ’时创建,这取决于它的结构。第一个
# 3 IF语句处理的是我们已经到达“Schema”结构中的叶节点的情况
# 我们正在处理一个实际的值验证。该方法中的其余逻辑用于遍历
# iterable对象,直到我们最终到达一个叶节点来验证它的值。
# 如果 self.validator 是一个字符串,那么检查 self.data 是否也是一个字符串
if isinstance(self.validator, str):
yield Result(isinstance(self.data, str), "is not a string", level=1, data=self.data)
# 如果 self.validator 是一个类型(type 的实例),那么检查 self.data 是否为该类型,并产生一个 Result 对象
elif isinstance(self.validator, type):
yield Result(isinstance(self.data, self.validator),
"is not {}".format(_get_type_name(self.validator)), level=1, data=self.data)
# 如果 self.validator 是一个可调用对象
elif callable(self.validator):
if hasattr(self.validator, "expected_type"):
if not isinstance(data, self.validator.expected_type):
yield Result(False, "is not {}"
.format(_get_type_name(self.validator.expected_type)), level=1, data=self.data)
return
try:
self.validator(data)
yield Result(True, data=self.data)
except Exception as e:
yield Result(False, "didn't pass validation: {}".format(e), data=self.data)
# 如果 self.validator 是一个字典,那么检查 self.data 是否也是一个字典,并产生一个 Result 对象
elif isinstance(self.validator, dict):
if not isinstance(self.data, dict):
yield Result(isinstance(self.data, dict), "is not a dictionary", level=1, data=self.data)
# 如果 self.validator 是一个列表,那么检查 self.data 是否也是一个列表,并产生一个 Result 对象
elif isinstance(self.validator, list):
if not isinstance(self.data, list):
yield Result(isinstance(self.data, list), "is not a list", level=1, data=self.data)
return
yield from self.iter()
作用:
validate
方法的具体作用是对给定的数据进行验证,根据在 Schema
实例中定义的验证规则。具体来说:
- 类型检查:
- 对于基础类型的验证(如字符串、整型等),直接检查数据类型是否符合预期。
- 对于可调用对象,先检查数据类型是否符合预期类型(如果定义了
expected_type
),然后调用该对象进行验证。 - 对于字典和列表,检查数据是否也是相同类型的容器。
- 递归验证:
- 如果
validator
是一个字典或列表,则递归地对每个子项进行验证。 - 这种递归行为允许验证复杂的、嵌套的数据结构。
- 如果
- 错误报告:
- 每次验证失败都会产生一个
Result
对象,其中包含了失败的原因。 - 这些
Result
对象通过迭代器的方式返回,允许外部调用者逐个获取验证结果。
- 每次验证失败都会产生一个
1.9.26.4 iter()
- 定义了一个
iter
方法,它返回一个Result
对象的迭代器,这些对象包含了验证过程中的结果信息。
def iter(self) -> Iterator[Result]:
"""如果 ``validator`` 是一个可迭代对象,则遍历它以验证 ``data`` 中对应的条目。
只有 :class:`dict`、:class:`list`、:class:`Directory` 和 :class:`Or` 对象被认为是可迭代对象。
:yields: 如果任何检查失败,则产生带有相关失败信息的对象。
"""
# 如果 self.validator 是一个字典,那么检查 self.data 是否也是一个字典
if isinstance(self.validator, dict):
if not isinstance(self.data, dict):
yield Result(False, "is not a dictionary.", level=1)
else:
yield from self.iter_dict()
elif isinstance(self.validator, list):
if len(self.data) == 0:
yield Result(False, "is an empty list", data=self.data)
if self.validator:
for key, value in enumerate(self.data):
# 尽管配置中的值(data)期望包含一个或多个条目,但在 validator 属性列表中定义的第一个验证器将被使用。 validator 中定义为 list 只是为了逻辑能够理解 data 属性中的值应该是一个 list。例如:validator 属性中 的 "pg_hba": [str] 表明 data 属性中的 "pg_hba" 应该包含一个或多个 str 条目。
for v in Schema(self.validator[0]).validate(value):
yield Result(v.status, v.error,
path=(str(key) + ("." + v.path if v.path else "")), level=v.level, data=value)
elif isinstance(self.validator, Directory) and isinstance(self.data, str):
yield from self.validator.validate(self.data)
elif isinstance(self.validator, Or):
yield from self.iter_or()
作用:
iter
方法的具体作用是处理可迭代类型的验证逻辑,例如字典、列表以及特定的 Directory
或 Or
类型。具体来说:
- 字典验证:
- 如果
validator
是一个字典,那么首先检查data
是否也是一个字典。 - 如果
data
是字典,则进一步调用iter_dict
方法来处理字典的验证逻辑。
- 如果
- 列表验证:
- 如果
validator
是一个列表,那么检查data
是否是一个非空列表。 - 如果
data
是列表,则使用validator
中的第一个元素作为验证器来验证data
中的每个元素。
- 如果
- Directory 类型验证:
- 如果
validator
是Directory
类型且data
是字符串,则调用validator
的validate
方法来验证data
。
- 如果
- Or 类型验证:
- 如果
validator
是Or
类型,则调用iter_or
方法来处理Or
类型的验证逻辑。
- 如果
1.9.26.5 iter_dict()
- 定义了一个
iter_dict
方法,它返回一个Result
对象的迭代器,这些对象包含了验证过程中的结果信息。
def iter_dict(self) -> Iterator[Result]:
"""遍历基于 :class:`dict` 的 `validator` 以验证 `data` 中对应的条目。
:yields: 如果任何检查失败,则产生带有相关失败信息的对象。
"""
# validator属性(‘ key ’变量)中的一个键可以映射到‘ data ’属性(‘ d ’变量)中的一个或多个键
# variable),取决于“key”的类型。
if TYPE_CHECKING: # pragma: no cover
assert isinstance(self.validator, dict)
assert isinstance(self.data, dict)
# 遍历 self.validator 字典的所有键
for key in self.validator.keys():
if isinstance(key, AtMostOne) and len(list(self._data_key(key))) > 1:
yield Result(False, f"Multiple of {key.args} provided")
continue
# 遍历 self.data 中与 key 匹配的所有键
for d in self._data_key(key):
# 如果 d 不在 self.data 中,并且 key 不是 Optional 类型
if d not in self.data and not isinstance(key, Optional):
yield Result(False, "is not defined.", path=d)
elif d not in self.data and isinstance(key, Optional) and key.default is None:
continue
else:
if d not in self.data and isinstance(key, Optional):
self.data[d] = key.default
# 获取 validator 中对应 key 的验证器
validator = self.validator[key]
# 如果 key 是 Or 或 AtMostOne 类型,并且 validator 是 Case 类型,则从 _schema 中选择对应 d 的验证器。
if isinstance(key, (Or, AtMostOne)) and isinstance(self.validator[key], Case):
validator = self.validator[key]._schema[d]
# 在这个循环中,我们可能在树的中间节点上调用一个新的“Schema”,或者
# 在叶子节点上。在后一种情况下,给定路径中的递归调用将完成。
for v in Schema(validator).validate(self.data[d]):
yield Result(v.status, v.error,
path=(d + ("." + v.path if v.path else "")), level=v.level, data=v.data)
作用:
iter_dict
方法的具体作用是处理字典类型的验证逻辑,确保 data
中的键和值符合 validator
中定义的规则。具体来说:
- 键的存在性验证:
- 如果
key
不是Optional
类型且d
不在self.data
中,则会产生一个错误。 - 如果
key
是Optional
类型且d
不在self.data
中,则会使用默认值填充self.data[d]
(如果定义了默认值的话)。
- 如果
- 键的唯一性验证:
- 如果
key
是AtMostOne
类型,并且self.data
中有多个键与之匹配,则会产生一个错误。
- 如果
- 键的递归验证:
- 对于每个匹配的键
d
,使用validator
中对应的验证器创建新的Schema
实例进行验证。 - 如果验证器是
Case
类型,则选择正确的子验证器进行验证。
- 对于每个匹配的键
- 路径跟踪:
- 在每次验证结果中添加路径信息,这样可以准确知道错误发生在哪个键上。
1.9.26.6 iter_or()
- 定义了一个
iter_or
方法,它返回一个Result
对象的迭代器,这些对象包含了验证过程中的结果信息。
def iter_or(self) -> Iterator[Result]:
"""执行由 :class:`Or` 对象定义的所有验证,针对给定的配置选项。
此方法只能对配置树中的叶节点调用。定义在 ``validator`` 键中的 :class:`Or` 对象将由 :func:`iter_dict` 方法处理。
:yields: 如果任何检查失败,则产生带有相关失败信息的对象。
"""
if TYPE_CHECKING: # pragma: no cover
assert isinstance(self.validator, Or)
results: List[Result] = []
# 遍历 self.validator 中所有的参数 a,并初始化一个列表 r 来存储针对单个参数的验证结果
for a in self.validator.args:
r: List[Result] = []
# 对于 Or 验证器中的每一个验证器,创建一个新的 Schema 实例来验证 self.data,并将所有验证结果添加到 r 列表中
for v in Schema(a).validate(self.data):
r.append(v)
if any([x.status for x in r]) and not all([x.status for x in r]):
results += [x for x in r if not x.status]
else:
results += r
# 没有一个“Or”验证器成功地验证了“数据”,所以我们将问题报告回来。
if not any([x.status for x in results]):
max_level = 3
for v in sorted(results, key=lambda x: x.level):
if v.level > max_level:
break
max_level = v.level
yield Result(v.status, v.error, path=v.path, level=v.level, data=v.data)
作用:
iter_or
方法的具体作用是处理 Or
类型的验证逻辑,确保 data
中的值符合 validator
中定义的一种或多种规则。具体来说:
- 验证多个规则:
- 对于
Or
类型的每一个参数a
,创建一个新的Schema
实例来验证data
。 - 收集所有验证结果,并根据验证结果的状态决定是否将失败的结果添加到最终的
results
列表中。
- 对于
- 处理部分成功的情况:
- 如果存在至少一个验证通过的结果,但是不是所有验证都通过,则只记录失败的结果。
- 如果所有验证都通过,则记录所有结果。
- 处理完全失败的情况:
- 如果没有一个验证器验证通过
data
,则按验证结果的级别排序,并只报告级别不超过max_level
的验证失败情况。
- 如果没有一个验证器验证通过
1.9.26.7 _data_key()
- 定义了一个
_data_key
方法,它接受一个key
参数,该参数可以是str
、Optional
、Or
或AtMostOne
类型,并返回一个迭代器,用于产生data
字典中应使用的键。
def _data_key(self, key: Union[str, Optional, Or, AtMostOne]) -> Iterator[str]:
"""将 `validator` 字典中的键映射到 `data` 字典中的相应键。
:param key: `validator` 属性中的键。
:yields: 应用于访问 `data` 属性中对应值的键。
"""
# 如果 key 是 str 类型,并且 self.data 是 dict 类型,则直接返回该 key
if isinstance(self.data, dict) and isinstance(key, str):
yield key
# 如果 key 是 Optional 类型,则返回 key 的名称作为 data 字典的键
elif isinstance(key, Optional):
yield key.name
# 如果 key 是 Or 类型,并且 self.data 是 dict 类型,则根据 Or 对象中的参数来判断
elif isinstance(key, Or) and isinstance(self.data, dict):
# 如果 data 字典中至少有一个 Or 入口,则返回所有找到的入口,以便调用者方法可以验证它们
if any([item in self.data for item in key.args]):
for item in key.args:
if item in self.data:
yield item
# 如果 data 字典中没有 Or 入口,则返回所有入口,以便调用者方法可以报告它们全部缺失。
else:
for item in key.args:
yield item
# 如果 key 是 AtMostOne 类型,并且 self.data 是 dict 类型,则返回 data 字典中的所有入口,每个入口都将被验证,然后统计以告知我们是否提供了过多的入口。
elif isinstance(key, AtMostOne) and isinstance(self.data, dict):
# Yield back all of the entries from the `data` dictionary, each will be validated and then counted
# to inform us if we've provided too many
for item in key.args:
if item in self.data:
yield item
作用:
_data_key
方法的具体作用是根据 validator
中定义的键类型来确定在 data
字典中应使用哪些键来访问相应的值。具体来说:
- 字符串类型的键:
- 如果
key
是str
类型,则直接返回该键作为data
字典中的键。
- 如果
- 可选类型的键:
- 如果
key
是Optional
类型,则返回其名称作为data
字典中的键。
- 如果
- 或类型的键:
- 如果
key
是Or
类型,则根据data
字典中是否存在Or
的入口来决定返回哪些键。 - 如果存在至少一个
Or
入口,则返回所有找到的入口。 - 如果不存在任何一个
Or
入口,则返回所有入口,以便报告它们全部缺失。
- 如果
- 最多一个类型的键:
- 如果
key
是AtMostOne
类型,则返回data
字典中的所有入口,并统计这些入口的数量,以确保提供的数量不超过一个。
- 如果
1.9.27 类:IntValidator
- 定义了一个名为
IntValidator
的类,继承自object
类。这是一个用于验证整数设置的类。 IntValidator
类的具体作用是提供一种机制来验证一个整数值是否满足一定的条件。具体来说:- 类型验证:
- 确保传入的值是整数类型。这可以通过
expected_type
变量来实现,它默认为int
类型。
- 确保传入的值是整数类型。这可以通过
- 数值范围验证:
- 确保整数值在一个指定的范围内。通过
min
和max
变量来设置允许的最小值和最大值。
- 确保整数值在一个指定的范围内。通过
- 单位转换:
- 如果需要,可以在验证之前将整数值转换为基本单位。这通过
base_unit
变量来实现。
- 如果需要,可以在验证之前将整数值转换为基本单位。这通过
- 断言测试:
- 根据
raise_assert
变量的设置,可以选择是否执行assert
断言来检查值是否符合预期类型和有效的范围。
- 根据
- 类型验证:
class IntValidator(object):
"""验证一个整数设置。
:ivar min: 设置允许的最小值,如果有。
:ivar max: 设置允许的最大值,如果有。
:ivar base_unit: 在检查值是否在 *min* 和 *max* 范围内之前,转换值的基本单位。
:ivar expected_type: 期望的 Python 类型。
:ivar raise_assert: 是否应该执行有关期望类型和有效范围的 ``assert`` 测试。
"""
1.9.27.1 __init__()
-
定义了
IntValidator
类的构造函数__init__
,它接收以下参数:min
: 最小允许值,默认为None
。max
: 最大允许值,默认为None
。base_unit
: 基本单位,在检查值是否在min
和max
范围内之前,用于转换值的基本单位,默认为None
。expected_type
: 期望的 Python 类型,默认为None
。raise_assert
: 一个布尔值,指示是否执行关于期望类型和有效范围的assert
测试,默认为False
。
构造函数的返回类型被指定为
None
。
def __init__(self, min: OptionalType[int] = None, max: OptionalType[int] = None,
base_unit: OptionalType[str] = None, expected_type: Any = None, raise_assert: bool = False) -> None:
"""使用给定的规则创建一个 :class:`IntValidator` 对象。
:param min: 设置允许的最小值,如果有。
:param max: 设置允许的最大值,如果有。
:param base_unit: 在检查值是否在 *min* 和 *max* 范围内之前,转换值的基本单位。
:param expected_type: 期望的 Python 类型。
:param raise_assert: 是否应该执行有关期望类型和有效范围的 ``assert`` 测试。
"""
self.min = min
self.max = max
self.base_unit = base_unit
if expected_type:
self.expected_type = expected_type
self.raise_assert = raise_assert
作用:
__init__
函数的具体作用是在创建 IntValidator
类的实例时,根据传入的参数初始化该实例的属性。具体来说:
- 初始化范围限制:
min
和max
分别表示验证的最小值和最大值。如果设置了这些值,则在验证过程中会检查目标值是否在这个范围内。
- 初始化单位转换:
base_unit
表示在进行范围检查前,是否需要将值转换为特定的基本单位。这对于处理具有单位的数值非常有用,比如从毫秒转换为秒等。
- 初始化类型检查:
expected_type
用于指定期望的类型。如果设置了该值,则在验证过程中会检查目标值是否为指定类型。
- 初始化断言开关:
raise_assert
是一个布尔标志,用于控制是否启用assert
断言检查。如果设为True
,则在验证过程中会使用assert
来确保类型和范围的有效性。
1.9.27.2 __call__()
- 定义了一个
__call__
方法,它使得IntValidator
实例可以像函数一样被调用。该方法接受一个value
参数,并返回一个布尔值。
def __call__(self, value: Any) -> bool:
"""检查 *value* 是否是一个有效的整数,并且在预期的范围内。
.. note::
如果 ``raise_assert`` 为 ``True`` 并且 *value* 无效,则会触发 :class:`AssertionError`。
:param value: 要与为此 :class:`IntValidator` 实例定义的规则进行比较的值。
:returns: 如果 *value* 有效且在预期范围内,则返回 ``True``。
"""
# 首先调用 parse_int 函数来解析 value,并根据 self.base_unit 进行转换。这里的 parse_int 函数应该是一个外部定义的函数,用于将输入值转换成整数,并根据需要应用单位转换。
value = parse_int(value, self.base_unit)
ret = isinstance(value, int)\
and (self.min is None or value >= self.min)\
and (self.max is None or value <= self.max)
if self.raise_assert:
assert_(ret)
return ret
作用:
__call__
方法的具体作用是根据 IntValidator
实例定义的规则来验证一个值是否有效。具体来说:
- 解析输入值:
- 使用
parse_int
函数将输入值转换为整数,并根据定义的base_unit
进行单位转换。
- 使用
- 类型验证:
- 检查转换后的值是否为整数类型。
- 范围验证:
- 检查转换后的值是否在定义的最小值和最大值之间(如果定义了的话)。
- 断言验证:
- 如果
raise_assert
为True
,则使用assert
来确保值满足上述条件,否则抛出AssertionError
。
- 如果
- 返回验证结果:
- 返回一个布尔值,表示输入值是否有效。
1.9.28 类:EnumValidator
- 定义了一个名为
EnumValidator
的类,继承自object
类。这是一个用于验证枚举设置的类。 EnumValidator
类的具体作用是提供一种机制来验证一个值是否属于一组预定义的允许值中。具体来说:- 枚举值验证:
- 确保传入的值是属于允许的枚举值集合中的一个值。这通过
allowed_values
变量来实现,它是一个包含所有允许值的集合。
- 确保传入的值是属于允许的枚举值集合中的一个值。这通过
- 断言测试:
- 根据
raise_assert
变量的设置,可以选择是否执行assert
断言来检查值是否符合期望类型和有效的范围。
- 根据
- 枚举值验证:
class EnumValidator(object):
"""验证枚举设置。
:ivar allowed_values: 包含允许的枚举值的一个 ``set`` 或 ``CaseInsensitiveSet`` 对象。
:ivar raise_assert: 是否应该执行有关期望类型和有效范围的 ``assert`` 调用。
"""
1.9.28.1 __init__()
-
定义了
EnumValidator
类的构造函数__init__
,它接收以下参数:allowed_values
: 允许的枚举值组成的元组。case_sensitive
: 布尔值,如果为True
则进行大小写敏感的比较,默认为False
。raise_assert
: 布尔值,如果为True
则执行assert
断言来验证值是否符合预期,默认为False
。
构造函数的返回类型被指定为
None
。
def __init__(self, allowed_values: Tuple[str, ...],
case_sensitive: bool = False, raise_assert: bool = False) -> None:
"""使用给定的允许值创建一个 :class:`EnumValidator` 对象。
:param allowed_values: 包含允许的枚举值的一个元组。
:param case_sensitive: 设置为 ``True`` 以进行大小写敏感的比较。
:param raise_assert: 如果应执行 `assert` 调用来验证预期的值。
"""
self.allowed_values = set(allowed_values) if case_sensitive else CaseInsensitiveSet(allowed_values)
self.raise_assert = raise_assert
作用:
__init__
函数的具体作用是在创建 EnumValidator
类的实例时,根据传入的参数初始化该实例的属性。具体来说:
- 初始化允许的值集合:
allowed_values
用于存储一组允许的枚举值。这些值可以是任何类型的字符串,但是通常用于表示枚举类型的名称或标识符。- 通过
case_sensitive
参数可以控制比较时是否区分大小写。
- 初始化断言开关:
raise_assert
是一个布尔标志,用于控制是否启用assert
断言检查。如果设为True
,则在验证过程中会使用assert
来确保值的有效性。
1.9.28.2 __call__()
- 定义了一个
__call__
方法,它使得EnumValidator
实例可以像函数一样被调用。该方法接受一个value
参数,并返回一个布尔值。
def __call__(self, value: Any) -> bool:
"""检查提供的 *value* 是否存在于 *allowed_values* 中。
.. note::
如果 ``raise_assert`` 为 ``True`` 并且 *value* 无效,则会触发 ``AssertionError``。
:param value: 要检查的值。
:returns: 如果 *value* 存在于 *allowed_values* 中,则返回 ``True``。
"""
ret = isinstance(value, str) and value in self.allowed_values
if self.raise_assert:
assert_(ret)
return ret
作用:
__call__
方法的具体作用是根据 EnumValidator
实例定义的规则来验证一个值是否有效。具体来说:
- 类型验证:
- 检查输入值
value
是否为字符串类型。
- 检查输入值
- 枚举值验证:
- 检查输入值
value
是否存在于self.allowed_values
集合中。
- 检查输入值
- 断言验证:
- 如果
raise_assert
为True
,则使用assert
来确保值满足上述条件,否则抛出AssertionError
。
- 如果
- 返回验证结果:
- 返回一个布尔值,表示输入值是否有效。
1.10 config.py
"""与 Patroni 配置相关的工具。"""
default_validator
函数:对传入的配置进行基本的验证,确保配置不是空的。Config
类:处理和管理集群的配置信息。
1.10.1 default_validator()
- 定义了一个名为
default_validator
的函数,它接收一个类型为Dict[str, Any]
的参数conf
,并返回类型为List[str]
的值。
def default_validator(conf: Dict[str, Any]) -> List[str]:
"""确保 *conf* 不为空。
设计为在没有提供特定验证器时作为 :class:`Config` 对象的默认验证器。
:param conf: 要验证的配置。
:returns: 一个空列表 -- :class:`Config` 期望验证器返回一个包含0个或更多在验证配置过程中发现的问题的列表。
:raises:
:class:`ConfigParseError`: 如果 *conf* 为空。
"""
if not conf:
raise ConfigParseError("Config is empty.")
return []
作用:
default_validator
函数的具体作用是对传入的配置进行基本的验证,确保配置不是空的。具体来说:
- 验证配置:
- 检查传入的配置字典
conf
是否为空。
- 检查传入的配置字典
- 返回结果:
- 如果配置为空,则抛出
ConfigParseError
异常。 - 如果配置不为空,则返回一个空列表,表示没有发现任何验证问题。
- 如果配置为空,则抛出
1.10.2 类:Config
__init__
函数:创建一个新的Config
类实例,并使用validator
验证加载的配置。config_file
函数:光顾配置文件的路径,如果有,则为' ' None ' '。dynamic_configuration
函数:缓存的Patroni动态配置的深度副本。local_configuration
函数:缓存的Patroni本地配置的深度副本。get_default_config
函数:深拷贝默认配置。_load_config_path
函数:从path
加载 Patroni 配置文件。_load_config_file
函数:加载来自文件系统的配置文件,并应用那些通过环境变量设置的值。_load_cache
函数:从patroni.dynamic.json
文件加载动态配置。save_cache
函数: 将动态配置保存到 Postgres 数据目录下的patroni.dynamic.json
文件中。__get_and_maybe_adjust_int_value
函数:获取、验证并可能调整配置中的某个参数值,以确保它符合最小值要求。_validate_and_adjust_timeouts
函数:验证并调整loop_wait
、retry_timeout
和ttl
的值(如有必要)。set_dynamic_configuration
函数:使用给定的configuration
设置动态配置值。reload_local_configuration
函数:从配置文件中重新加载配置值。_process_postgresql_parameters
函数:处理 Postgresparameters
。_safe_copy_dynamic_configuration
函数:创建dynamic_configuration
的安全副本。_build_environment_configuration
函数:获取通过环境变量指定的本地配置设置。_popenv
函数:获取环境变量name
(传入的参数)的值。_fix_log_env
函数:规范化日志相关的环境变量。_set_section_values
函数:获取与 section 相关的 params 环境变量的值。_parse_list
函数:用于将 YAML 格式的字符串解析为列表。_parse_dict
函数:用于将 YAML 格式的字符串解析为字典。_get_auth
函数:从环境变量中获取与授权相关的参数值。
_build_effective_configuration
函数:通过合并dynamic_configuration
和local_configuration
构建有效的配置。get
函数:从 Patroni 配置的根获取 key 设置的有效值。__contains__
函数:检查有效配置中是否存在设置 key。__getitem__
函数:从有效配置中获取设置 key 的值。copy
函数:获取有效的 Patroni 配置的深拷贝。_validate_failover_tags
函数:检查配置中的nofailover
和failover_priority
标签是否矛盾,并在发现矛盾时发出警告。
1.10.2.1 主体
config
类的主要作用是处理和管理集群的配置信息。它包含了多个方法来加载、设置、验证和调整配置项,以确保配置的有效性和一致性。这个类对于集群成员的正常运作至关重要,因为它确保了各个成员在执行操作时使用的配置是正确无误的。特别是在涉及到故障转移(failover)相关的配置时,它可以有效地帮助避免由于配置错误而导致的问题。
class Config(object):
"""处理Patroni配置。
这个类负责:
1)建立并访问` ` effecve_configuration ` ` from:
*“配置。__DEFAULT_CONFIG ` `——一些相同的默认值;
* ` ` dynamic_configuration ` `——配置存储在DCS中;
* ``local_configuration``——config. conf ``Yml `或环境。
2)保存并加载` ` dynamic_configuration ` `到`patroni.dynamic. ` `json的文件
位于local_configuration[`postgresql`][`data_dir`]目录中。
这对于能够恢复` ` dynamic_configuration ` `是必要的。
如果DCS不小心被删除了
3)加载旧格式的配置文件并将其转换为新格式。
4)模仿一些“字典”接口使之成为可能
像使用旧的“config”对象一样使用它。
:cvar patron_config_variable:可用于从中加载Patroni配置的环境变量的名称。
:cvar __CACHE_FILENAME:用于缓存Postgres data目录下动态配置的文件名。
:cvar __DEFAULT_CONFIG:一些Patroni设置的默认配置值。
"""
PATRONI_CONFIG_VARIABLE = PATRONI_ENV_PREFIX + `CONFIGURATION`
__CACHE_FILENAME = `patroni.dynamic.json`
__DEFAULT_CONFIG: Dict[str, Any] = {
`ttl`: 30, `loop_wait`: 10, `retry_timeout`: 10,
`standby_cluster`: {
`create_replica_methods`: ``,
`host`: ``,
`port`: ``,
`primary_slot_name`: ``,
`restore_command`: ``,
`archive_cleanup_command`: ``,
`recovery_min_apply_delay`: ``
},
`postgresql`: {
`use_slots`: True,
`parameters`: CaseInsensitiveDict({p: v[0] for p, v in ConfigHandler.CMDLINE_OPTIONS.items()
if v[0] is not None and p not in (`wal_keep_segments`, `wal_keep_size`)})
}
}
def __init__(self, configfile: str,
validator: Optional[Callable[[Dict[str, Any]], List[str]]] = default_validator) -> None:
"""创建一个新的 Config 类实例,并使用 validator 验证加载的配置。
.. 注意:
Patroni 将按照以下顺序读取配置:
如果存在并且可以解析通过命令行参数传递的文件或目录路径(configfile),否则
如果通过环境变量传递的 YAML 文件存在并且可以解析(参见 :attr:PATRONI_CONFIG_VARIABLE),否则
从作为环境变量定义的配置值中读取(参见 :meth:~Config._build_environment_configuration)。
:configfile:Patroni 配置文件的路径。
:validator:用于验证 Patroni 配置的函数。它应该接收一个字典,代表 Patroni 的配置,并根据验证返回一个零个或多个错误消息的列表
抛出异常:
ConfigParseError:如果 validator 报告任何问题
"""
# 初始化版本修改标志为 -1,动态配置为空字典
self._modify_version = -1
self._dynamic_configuration = {}
# 从环境变量构建配置信息
self.__environment_configuration = self._build_environment_configuration()
# 如果 configfile 存在并且是一个有效的路径,则将其赋值给 _config_file
self._config_file = configfile if configfile and os.path.exists(configfile) else None
if self._config_file:
# 从文件加载配置信息
self._local_configuration = self._load_config_file()
else:
# 从环境变量加载配置信息
config_env = os.environ.pop(self.PATRONI_CONFIG_VARIABLE, None)
self._local_configuration = config_env and yaml.safe_load(config_env) or self.__environment_configuration
# 如果提供了 validator 函数
if validator:
# 使用该函数验证 _local_configuration
errors = validator(self._local_configuration)
if errors:
raise ConfigParseError("\n".join(errors))
# 构建有效的配置,并从中提取 PostgreSQL 数据目录的路径
self.__effective_configuration = self._build_effective_configuration({}, self._local_configuration)
self._data_dir = self.__effective_configuration.get(`postgresql`, {}).get(`data_dir`, "")
# 计算缓存文件的路径
self._cache_file = os.path.join(self._data_dir, self.__CACHE_FILENAME)
if validator: # patronictl使用validator=None
# 加载缓存并验证故障转移标签
self._load_cache() # 我们不想为ctl从本地缓存加载任何东西
self._validate_failover_tags() # ctl
# 始化缓存是否需要保存的状态为 False
self._cache_needs_saving = False
@property
def config_file(self) -> Optional[str]:
"""光顾配置文件的路径,如果有,则为' ' None ' '。"""
return self._config_file
@property
def dynamic_configuration(self) -> Dict[str, Any]:
"""缓存的Patroni动态配置的深度副本。"""
return deepcopy(self._dynamic_configuration)
@property
def local_configuration(self) -> Dict[str, Any]:
"""缓存的Patroni本地配置的深度副本。
:返回:attr: ' ~Config._local_configuration '的副本
"""
return deepcopy(dict(self._local_configuration))
@classmethod
def get_default_config(cls) -> Dict[str, Any]:
"""深拷贝默认配置。
:returns: :attr:`~Config.__DEFAULT_CONFIG` 的副本。
"""
# 这一行返回 cls.__DEFAULT_CONFIG 的深拷贝
return deepcopy(cls.__DEFAULT_CONFIG)
def _load_config_path(self, path: str) -> Dict[str, Any]:
"""从 *path* 加载 Patroni 配置文件。
如果 *path* 是一个文件,加载由 *path* 指定的 YAML 文件。
如果 *path* 是一个目录,按字母顺序加载该目录中的所有 YAML 文件。
:param path: 路径指向一个 YAML 配置文件,或者包含 YAML 配置文件的文件夹。
:returns: 从 *path* 读取配置文件后的配置。
:raises:
:class:`ConfigParseError`: 如果 *path* 无效。
"""
# 如果 path 指向的是一个文件,那么将其存储在一个列表 files 中
if os.path.isfile(path):
files = [path]
# 如果 path 是一个目录,那么列出该目录下所有的 YAML 文件,并按字母顺序排序,然后将这些文件路径存储在 files 列表中
elif os.path.isdir(path):
files = [os.path.join(path, f) for f in sorted(os.listdir(path))
if (f.endswith(`.yml`) or f.endswith(`.yaml`)) and os.path.isfile(os.path.join(path, f))]
else:
# 既不是一个文件也不是一个目录
logger.error(`config path %s is neither directory nor file`, path)
raise ConfigParseError(`invalid config path`)
# 用于存储合并后的配置信息
overall_config: Dict[str, Any] = {}
for fname in files:
with open(fname) as f:
# 打开文件并使用 yaml.safe_load 解析其内容
config = yaml.safe_load(f)
# 将解析后的配置信息合并到 overall_config 中
patch_config(overall_config, config)
return overall_config
def _load_config_file(self) -> Dict[str, Any]:
"""加载来自文件系统的配置文件,并应用那些通过环境变量设置的值。
:returns: 在合并配置文件和环境变量之后的最终配置。
"""
# 检查是否正在进行类型检查
if TYPE_CHECKING: # pragma: no cover
assert self.config_file is not None
# 加载指定的配置文件
config = self._load_config_path(self.config_file)
# 更新 config
patch_config(config, self.__environment_configuration)
return config
def _load_cache(self) -> None:
"""从 ``patroni.dynamic.json`` 文件加载动态配置。"""
# 检查 self._cache_file 是否是一个存在的文件
if os.path.isfile(self._cache_file):
try:
# 如果文件存在,尝试打开文件,并使用 json.load() 方法将文件内容解析为 JSON 对象,然后调用 self.set_dynamic_configuration 方法设置动态配置
with open(self._cache_file) as f:
self.set_dynamic_configuration(json.load(f))
except Exception:
logger.exception(`Exception when loading file: %s`, self._cache_file)
def save_cache(self) -> None:
"""将动态配置保存到 Postgres 数据目录下的 `patroni.dynamic.json` 文件中。
.. 注意:
`patroni.dynamic.jsonXXXXXX` 会先作为一个临时文件创建,然后重命名为 `patroni.dynamic.json`,其中 `XXXXXX` 是一个随机后缀。
"""
if self._cache_needs_saving:
tmpfile = fd = None
try:
pg_perm.set_permissions_from_data_directory(self._data_dir)
(fd, tmpfile) = tempfile.mkstemp(prefix=self.__CACHE_FILENAME, dir=self._data_dir)
with os.fdopen(fd, `w`) as f:
fd = None
json.dump(self.dynamic_configuration, f)
tmpfile = shutil.move(tmpfile, self._cache_file)
os.chmod(self._cache_file, pg_perm.file_create_mode)
self._cache_needs_saving = False
except Exception:
logger.exception(`Exception when saving file: %s`, self._cache_file)
if fd:
try:
os.close(fd)
except Exception:
logger.error(`Can not close temporary file %s`, tmpfile)
if tmpfile and os.path.exists(tmpfile):
try:
os.remove(tmpfile)
except Exception:
logger.error(`Can not remove temporary file %s`, tmpfile)
def __get_and_maybe_adjust_int_value(self, config: Dict[str, Any], param: str, min_value: int) -> int:
"""从 *config* :class:`dict` 中获取、验证并可能调整 *param* 的整数值。
.. note:
如果值小于提供的 *min_value*,我们更新 *config*。
如果值不是 :class:`int` 或者不能转换为 :class:`int`,此方法可能会抛出异常。
:param config: 包含新全局配置的 :class:`dict` 对象。
:param param: 我们想要读取/验证/调整的配置参数名称。
:param min_value: 给定 *param* 可能拥有的最小值。
:returns: 对应于提供的 *param* 的整数值。
"""
# 获取配置参数 param 的值。如果 config 中不存在该参数,则使用默认配置中的值,并将结果转换为整数。
value = int(config.get(param, self.__DEFAULT_CONFIG[param]))
# 如果转换后的值小于 min_value,则记录一条警告日志,并将 config 中的 param 值调整为 min_value。
if value < min_value:
logger.warning("%s=%d can`t be smaller than %d, adjusting...", param, value, min_value)
value = config[param] = min_value
return value
def _validate_and_adjust_timeouts(self, config: Dict[str, Any]) -> None:
"""验证并调整 `loop_wait`、`retry_timeout` 和 `ttl` 的值(如有必要)。
最小值:
* `loop_wait`:1 秒;
* `retry_timeout`:3 秒;
* `ttl`:20 秒;
最大值:
如果值不符合以下规则,则减少 `retry_timeout` 和 `loop_wait` 以满足规则:
.. code-block:: python
loop_wait + 2 * retry_timeout <= ttl
注意:
我们倾向于减少 `loop_wait`,只有当 `loop_wait` 已经设置为可能的最小值时才会减少 `retry_timeout`。
:param config: 包含新全局配置的 :class:`dict` 对象。
"""
# 义 min_loop_wait 为 1,然后分别获取并可能调整 loop_wait、retry_timeout 和 ttl 的值
min_loop_wait = 1
loop_wait = self. __get_and_maybe_adjust_int_value(config, `loop_wait`, min_loop_wait)
retry_timeout = self. __get_and_maybe_adjust_int_value(config, `retry_timeout`, 3)
ttl = self. __get_and_maybe_adjust_int_value(config, `ttl`, 20)
# 如果最小 loop_wait 加上两倍的 retry_timeout 大于 ttl,则调整 loop_wait 为最小值,并调整 retry_timeout 以满足规则,同时记录一条警告日志
if min_loop_wait + 2 * retry_timeout > ttl:
config[`loop_wait`] = min_loop_wait
config[`retry_timeout`] = (ttl - min_loop_wait) // 2
logger.warning(`Violated the rule "loop_wait + 2*retry_timeout <= ttl", where ttl=%d. `
`Adjusting loop_wait from %d to %d and retry_timeout from %d to %d`,
ttl, loop_wait, min_loop_wait, retry_timeout, config[`retry_timeout`])
# 如果 loop_wait 加上两倍的 retry_timeout 大于 ttl,则调整 loop_wait 以满足规则,并记录一条警告日志
elif loop_wait + 2 * retry_timeout > ttl:
config[`loop_wait`] = ttl - 2 * retry_timeout
logger.warning(`Violated the rule "loop_wait + 2*retry_timeout <= ttl", where ttl=%d and retry_timeout=%d.`
` Adjusting loop_wait from %d to %d`, ttl, retry_timeout, loop_wait, config[`loop_wait`])
# 配置可以是ClusterConfig或dict
def set_dynamic_configuration(self, configuration: Union[ClusterConfig, Dict[str, Any]]) -> bool:
"""使用给定的 *configuration* 设置动态配置值。
:param configuration: 新的动态配置值。支持 :class:`dict` 以兼容旧版本。
:returns: 如果检测到当前动态配置和新的动态 *configuration* 之间存在变化,则返回 ``True``,否则返回 ``False``。
"""
# 检查传入的 configuration 是否是 ClusterConfig 类型
if isinstance(configuration, ClusterConfig):
# 如果当前的 _modify_version 与 configuration 的 modify_version 相等,则说明配置未更改
if self._modify_version == configuration.modify_version:
return False # 如果版本未改变,则无需做任何事情
# 如果版本号发生了变化,则更新 _modify_version,并将 configuration 替换为其数据部分 configuration.data
self._modify_version = configuration.modify_version
configuration = configuration.data
# 使用 deep_compare 函数比较当前的 _dynamic_configuration 和新的 configuration 是否相同
if not deep_compare(self._dynamic_configuration, configuration):
try:
self._validate_and_adjust_timeouts(configuration)
self.__effective_configuration = self._build_effective_configuration(configuration,
self._local_configuration)
self._dynamic_configuration = configuration
self._cache_needs_saving = True
return True
except Exception:
logger.exception(`Exception when setting dynamic_configuration`)
return False
def reload_local_configuration(self) -> Optional[bool]:
"""从配置文件中重新加载配置值。
.. 注意: 设计用于用户对配置文件进行更改时,以便 Patroni 可以通过重新加载而不是重启来使用新值。
:returns: 如果在当前本地配置之间检测到更改,则返回 True。
"""
if self.config_file:
try:
configuration = self._load_config_file()
if not deep_compare(self._local_configuration, configuration):
new_configuration = self._build_effective_configuration(self._dynamic_configuration, configuration)
self._local_configuration = configuration
self.__effective_configuration = new_configuration
self._validate_failover_tags()
return True
else:
logger.info(`No local configuration items changed.`)
except Exception:
logger.exception(`Exception when reloading local configuration from %s`, self.config_file)
@staticmethod
def _process_postgresql_parameters(parameters: Dict[str, Any], is_local: bool = False) -> Dict[str, Any]:
"""处理 Postgres *parameters*。
.. note::
如果 *is_local* 配置则丢弃 *parameters* 中列出的任何设置,这些设置应仅通过动态配置来设置。
当通过动态配置设置 :attr:`~patroni.postgresql.config.ConfigHandler.CMDLINE_OPTIONS` 中的参数时,其值将
根据该属性条目中定义的验证器进行验证。如果给定的值无法验证,则记录警告,并使用 GUC 的默认值。
即使不是 *is_local* 配置,:attr:`~patroni.postgresql.config.ConfigHandler.CMDLINE_OPTIONS` 中的一些
参数也不能设置:
* `listen_addresses`:从 `postgresql.listen` 本地配置或 `PATRONI_POSTGRESQL_LISTEN` 环境变量推
断;
* `port`:从 `postgresql.listen` 本地配置或 `PATRONI_POSTGRESQL_LISTEN` 环境变量推断;
* `cluster_name`:通过 `scope` 本地配置或 `PATRONI_SCOPE` 环境变量设置;
* `hot_standby`:总是启用;
:param parameters: 需要处理的 Postgres 参数。应该是 `postgresql.parameters` 配置的解析后的 YAML 值,可以来自
本地配置也可以来自动态配置。
:param is_local: 如果 *parameters* 指向本地配置,则应为 `True`,否则为 `False`。
:returns: 在处理和验证 *parameters* 后 `postgresql.parameters` 的新值。
"""
# 始化一个新的空字典 pg_params,用于存储处理后的参数
pg_params: Dict[str, Any] = {}
# 遍历 parameters 字典中的每一项
for name, value in (parameters or {}).items():
# 如果参数名不在 CMDLINE_OPTIONS 中,则将该参数添加到 pg_params 中
if name not in ConfigHandler.CMDLINE_OPTIONS:
pg_params[name] = value
# 如果不是本地配置 (is_local 为 False),则获取相应的验证器,并使用验证器验证参数值。如果验证通过,则根据验证器的类型将参数值转换为整数或将原值放入 pg_params 中;如果验证失败,则记录警告,并使用默认值
elif not is_local:
validator = ConfigHandler.CMDLINE_OPTIONS[name][1]
if validator(value):
int_val = parse_int(value) if isinstance(validator, IntValidator) else None
pg_params[name] = int_val if isinstance(int_val, int) else value
else:
logger.warning("postgresql parameter %s=%s failed validation, defaulting to %s",
name, value, ConfigHandler.CMDLINE_OPTIONS[name][0])
return pg_params
def _safe_copy_dynamic_configuration(self, dynamic_configuration: Dict[str, Any]) -> Dict[str, Any]:
"""创建 *dynamic_configuration* 的副本。
将 *dynamic_configuration* 与 :attr:`__DEFAULT_CONFIG` 合并(*dynamic_configuration* 优先),并在存在的情况下通过 :func:`_process_postgresql_parameters` 处理 `postgresql.parameters`。
.. note::
以下设置不允许出现在 `postgresql` 部分,因为它们被设计为本地配置,并在出现时被移除:
* `connect_address`;
* `proxy_address`;
* `listen`;
* `config_dir`;
* `data_dir`;
* `pgpass`;
* `authentication`;
此外,任何存在于 *dynamic_configuration* 但不存在于 :attr:`__DEFAULT_CONFIG` 中的设置都将被丢弃。
:param dynamic_configuration: Patroni 动态配置。
:returns: *dynamic_configuration* 的副本,合并了默认动态配置,并对其执行了一些合理性检查。
"""
# 获取默认配置
config = self.get_default_config()
# 遍历 dynamic_configuration 中的所有项
for name, value in dynamic_configuration.items():
if name == `postgresql`:
for name, value in (value or EMPTY_DICT).items():
if name == `parameters`:
config[`postgresql`][name].update(self._process_postgresql_parameters(value))
# 如果子项名不属于某些预定义的关键字(这些关键字被认为是本地配置),则将该子项的值深拷贝到 config 中
elif name not in (`connect_address`, `proxy_address`, `listen`,
`config_dir`, `data_dir`, `pgpass`, `authentication`):
config[`postgresql`][name] = deepcopy(value)
elif name == `standby_cluster`:
for name, value in (value or EMPTY_DICT).items():
if name in self.__DEFAULT_CONFIG[`standby_cluster`]:
config[`standby_cluster`][name] = deepcopy(value)
# 如果项名存在于 config 中(意味着它也在默认配置中),则将该值转换为整数,并赋值给 config 中对应的项
elif name in config: # 只有存在于__DEFAULT_CONFIG中的变量才允许从DCS中重写
config[name] = int(value)
return config
@staticmethod
def _build_environment_configuration() -> Dict[str, Any]:
"""获取通过环境变量指定的本地配置设置。
返回:包含找到的环境变量及其值的字典,遵循预期的 Patroni 配置结构。
"""
# 初始化一个空的 defaultdict
ret: Dict[str, Any] = defaultdict(dict)
def _popenv(name: str) -> Optional[str]:
"""获取环境变量 name 的值。
注意:
当在环境中搜索时,name 会被加上 PATRONI_ENV_PREFIX 前缀。
此外,在读取其值后,相应的环境变量也会从环境中移除。
参数:name:环境变量的名称。
返回:如果 name 在环境中存在,则返回其值,否则返回 None
"""
# 从环境中获取指定名称的值,并移除该环境变量
return os.environ.pop(PATRONI_ENV_PREFIX + name.upper(), None)
# 循环遍历 name、namespace 和 scope,获取它们的值,并将非空值存储在 ret 字典中
for param in (`name`, `namespace`, `scope`):
value = _popenv(param)
if value:
ret[param] = value
def _fix_log_env(name: str, oldname: str) -> None:
"""规范化日志相关的环境变量。
注意:
Patroni 过去支持不同的日志相关环境变量名称。随着环境变量的重命名,此函数负责映射和规范化环境。
name 在环境中搜索时会被加上 PATRONI_ENV_PREFIX 和 LOG 前缀。
oldname 在环境中搜索时会被加上 PATRONI_ENV_PREFIX 前缀。
如果 name 和 oldname 都在环境中设置,则 name 优先
name:新的日志相关环境变量名称。
oldname:原来的日志相关环境变量名称
:type oldname: str
"""
# 获取旧的环境变量值,并将其映射到新的环境变量名称上
value = _popenv(oldname)
name = PATRONI_ENV_PREFIX + `LOG_` + name.upper()
if value and name not in os.environ:
os.environ[name] = value
# 循环遍历日志相关的名称和旧名称,并调用 _fix_log_env 函数进行规范化
for name, oldname in ((`level`, `loglevel`), (`format`, `logformat`), (`dateformat`, `log_datefmt`)):
_fix_log_env(name, oldname)
def _set_section_values(section: str, params: List[str]) -> None:
"""获取与 section 相关的 params 环境变量的值
注意:
这些值是从环境中检索的,并直接更新到 :func:_build_environment_configuration` 函数返回的字典中。
参数:
section:params 所属的配置节。
params:Patroni 设置的名称
"""
# 循环遍历 params 列表,获取每个参数的值
for param in params:
value = _popenv(section + `_` + param)
if value:
ret[section][param] = value
# 调用 _set_section_values 函数,设置 restapi 节的相关参数值。
_set_section_values(`restapi`, [`listen`, `connect_address`, `certfile`, `keyfile`, `keyfile_password`,
`cafile`, `ciphers`, `verify_client`, `http_extra_headers`,
`https_extra_headers`, `allowlist`, `allowlist_include_members`,
`request_queue_size`])
# 设置 ctl 节的相关参数值
_set_section_values(`ctl`, [`insecure`, `cacert`, `certfile`, `keyfile`, `keyfile_password`])
# 设置 postgresql 节的相关参数值
_set_section_values(`postgresql`, [`listen`, `connect_address`, `proxy_address`,
`config_dir`, `data_dir`, `pgpass`, `bin_dir`])
# 设置 log 节的相关参数值
_set_section_values(`log`, [`type`, `level`, `traceback_level`, `format`, `dateformat`, `static_fields`,
`max_queue_size`, `dir`, `mode`, `file_size`, `file_num`, `loggers`])
# 设置 raft 节的相关参数值
_set_section_values(`raft`, [`data_dir`, `self_addr`, `partner_addrs`, `password`, `bind_addr`])
# 循环遍历 PostgreSQL 相关的二进制文件名,获取它们的路径
for binary in (`pg_ctl`, `initdb`, `pg_controldata`, `pg_basebackup`, `postgres`, `pg_isready`, `pg_rewind`):
value = _popenv(`POSTGRESQL_BIN_` + binary)
if value:
ret[`postgresql`].setdefault(`bin_name`, {})[binary] = value
# parse all values retrieved from the environment as Python objects, according to the expected type
# 将从环境变量中获取的某些值解析为 Python 对象,例如布尔值
for first, second in ((`restapi`, `allowlist_include_members`), (`ctl`, `insecure`)):
value = ret.get(first, {}).pop(second, None)
if value:
value = parse_bool(value)
if value is not None:
ret[first][second] = value
# 将从环境变量中获取的某些值解析为整数
for first, params in ((`restapi`, (`request_queue_size`,)),
(`log`, (`max_queue_size`, `file_size`, `file_num`, `mode`))):
for second in params:
value = ret.get(first, {}).pop(second, None)
if value:
value = parse_int(value)
if value is not None:
ret[first][second] = value
def _parse_list(value: str) -> Optional[List[str]]:
"""将 YAML 格式的字符串解析为列表
参数值:字符串形式的YAML列表。
返回值:value :class: ` list `。
"""
# 如果传入的字符串不是以 - 开头或包含 [,则将其包装成 YAML 列表格式,然后尝试解析为列表
if not (value.strip().startswith(`-`) or `[` in value):
value = `[{0}]`.format(value)
try:
return yaml.safe_load(value)
except Exception:
logger.exception(`Exception when parsing list %s`, value)
return None
# 将某些特定参数解析为列表
for first, second in ((`raft`, `partner_addrs`), (`restapi`, `allowlist`)):
value = ret.get(first, {}).pop(second, None)
if value:
value = _parse_list(value)
if value:
ret[first][second] = value
# 如果 logformat 不包含 % 格式化字符,则将其解析为列表
logformat = ret.get(`log`, {}).get(`format`)
if logformat and not re.search(r`%\(\w+\)`, logformat):
logformat = _parse_list(logformat)
if logformat:
ret[`log`][`format`] = logformat
def _parse_dict(value: str) -> Optional[Dict[str, Any]]:
"""将YAML字典*值*解析为:class: ` dict `。
参数值:字符串形式的YAML字典。
返回:value 作为:class: ` dict `。
"""
# 如果传入的字符串不是以 { 开头,则将其包装成 YAML 字典格式,然后尝试解析为字典
if not value.strip().startswith(`{`):
value = `{{{0}}}`.format(value)
try:
return yaml.safe_load(value)
except Exception:
logger.exception(`Exception when parsing dict %s`, value)
return None
# 将某些特定参数解析为字典
dict_configs = (
(`restapi`, (`http_extra_headers`, `https_extra_headers`)),
(`log`, (`static_fields`, `loggers`))
)
for first, params in dict_configs:
for second in params:
value = ret.get(first, {}).pop(second, None)
if value:
value = _parse_dict(value)
if value:
ret[first][second] = value
def _get_auth(name: str, params: Collection[str] = _AUTH_ALLOWED_PARAMETERS[:2]) -> Dict[str, str]:
"""获取与授权相关的环境变量 params,来自于 name 部分。
name: 可能包含授权 params 的配置部分名称。
params: 可能在 name 部分下设置的授权设置。
:returns: 包含部分 name 的授权 params 的环境值的字典。
"""
# 从环境变量中获取与授权相关的参数值
ret: Dict[str, str] = {}
for param in params:
value = _popenv(name + `_` + param)
if value:
ret[param] = value
return ret
# 获取 ctl 和 restapi 节点的认证信息
for section in (`ctl`, `restapi`):
auth = _get_auth(section)
if auth:
ret[section][`authentication`] = auth
authentication = {}
# 获取 replication、superuser 和 rewind 用户类型的认证信息
for user_type in (`replication`, `superuser`, `rewind`):
entry = _get_auth(user_type, _AUTH_ALLOWED_PARAMETERS)
if entry:
authentication[user_type] = entry
if authentication:
ret[`postgresql`][`authentication`] = authentication
# 遍历环境变量,根据前缀过滤出与 Patroni 相关的环境变量,并根据变量后缀的不同类型对值进行适当的转换(如整型、布尔值等),然后存储到 ret 字典中
for param in list(os.environ.keys()):
if param.startswith(PATRONI_ENV_PREFIX):
# PATRONI_(ETCD|CONSUL|ZOOKEEPER|EXHIBITOR|...)_(HOSTS?|PORT|..)
name, suffix = (param[len(PATRONI_ENV_PREFIX):].split(`_`, 1) + [``])[:2]
if suffix in (`HOST`, `HOSTS`, `PORT`, `USE_PROXIES`, `PROTOCOL`, `SRV`, `SRV_SUFFIX`, `URL`, `PROXY`,
`CACERT`, `CERT`, `KEY`, `VERIFY`, `TOKEN`, `CHECKS`, `DC`, `CONSISTENCY`,
`REGISTER_SERVICE`, `SERVICE_CHECK_INTERVAL`, `SERVICE_CHECK_TLS_SERVER_NAME`,
`SERVICE_TAGS`, `NAMESPACE`, `CONTEXT`, `USE_ENDPOINTS`, `SCOPE_LABEL`, `ROLE_LABEL`,
`POD_IP`, `PORTS`, `LABELS`, `BYPASS_API_SERVICE`, `RETRIABLE_HTTP_CODES`, `KEY_PASSWORD`,
`USE_SSL`, `SET_ACLS`, `GROUP`, `DATABASE`, `LEADER_LABEL_VALUE`, `FOLLOWER_LABEL_VALUE`,
`STANDBY_LEADER_LABEL_VALUE`, `TMP_ROLE_LABEL`, `AUTH_DATA`) and name:
value = os.environ.pop(param)
if name == `CITUS`:
if suffix == `GROUP`:
value = parse_int(value)
elif suffix != `DATABASE`:
continue
elif suffix == `PORT`:
value = value and parse_int(value)
elif suffix in (`HOSTS`, `PORTS`, `CHECKS`, `SERVICE_TAGS`, `RETRIABLE_HTTP_CODES`):
value = value and _parse_list(value)
elif suffix in (`LABELS`, `SET_ACLS`, `AUTH_DATA`):
value = _parse_dict(value)
elif suffix in (`USE_PROXIES`, `REGISTER_SERVICE`, `USE_ENDPOINTS`, `BYPASS_API_SERVICE`, `VERIFY`):
value = parse_bool(value)
if value is not None:
ret[name.lower()][suffix.lower()] = value
# 获取 etcd 和 etcd3 节点的认证信息,并更新到 ret 字典中
for dcs in (`etcd`, `etcd3`):
if dcs in ret:
ret[dcs].update(_get_auth(dcs))
return ret
def _build_effective_configuration(self, dynamic_configuration: Dict[str, Any],
local_configuration: Dict[str, Union[Dict[str, Any], Any]]) -> Dict[str, Any]:
"""通过合并 *dynamic_configuration* 和 *local_configuration* 构建有效的配置。
.. note::
如果在两者中都定义了相同的设置,则 *local_configuration* 的优先级高于 *dynamic_configuration*。
:param dynamic_configuration: Patroni 动态配置。
:param local_configuration: Patroni 本地配置。
:returns: 合并后的有效配置。
"""
# 创建安全副本
config = self._safe_copy_dynamic_configuration(dynamic_configuration)
# 遍历 local_configuration 中的所有项
for name, value in local_configuration.items():
if name == `citus`: # 移除无效的 citus 配置
if isinstance(value, dict) and isinstance(value.get(`group`), int)\
and isinstance(value.get(`database`), str):
config[name] = value
# 进一步处理 postgresql 下的子项
elif name == `postgresql`:
for name, value in (value or {}).items():
if name == `parameters`:
config[`postgresql`][name].update(self._process_postgresql_parameters(value, True))
elif name != `use_slots`: # replication slots 必须全局启用或禁用
config[`postgresql`][name] = deepcopy(value)
# 如果项名不在 config 中,或者项名为 watchdog,则将该项的值复制到 config 中
elif name not in config or name in [`watchdog`]:
config[name] = deepcopy(value) if value else {}
# restapi 服务器期望接收如下的格式 restapi.auth = 'username:password' 同样适用于 `ctl`
for section in (`ctl`, `restapi`):
if section in config and `authentication` in config[section]:
config[section][`auth`] = `{username}:{password}`.format(**config[section][`authentication`])
# 对旧版配置的特殊处理
# `exhibitor` inside `zookeeper`:
# 如果配置中有 zookeeper 并且 zookeeper 下有 exhibitor,则将 exhibitor 提升至顶层,并移除 zookeeper
if `zookeeper` in config and `exhibitor` in config[`zookeeper`]:
config[`exhibitor`] = config[`zookeeper`].pop(`exhibitor`)
config.pop(`zookeeper`)
# 如果 postgresql 下没有 authentication,则创建一个 authentication 字段,并从中提取 replication 和 superuser 的配置
pg_config = config[`postgresql`]
# postgresql 中没有 'authentication',但是有 'replication' 和 'superuser'
if `authentication` not in pg_config:
pg_config[`use_pg_rewind`] = `pg_rewind` in pg_config
pg_config[`authentication`] = {u: pg_config[u] for u in (`replication`, `superuser`) if u in pg_config}
# postgresql.authentication 中没有 'superuser',但是有 'pg_rewind'
if `superuser` not in pg_config[`authentication`] and `pg_rewind` in pg_config:
pg_config[`authentication`][`superuser`] = pg_config[`pg_rewind`]
# handle设置可能可用的附加连接参数
# 在配置文件中,如SSL连接参数
for name, value in pg_config[`authentication`].items():
pg_config[`authentication`][name] = {n: v for n, v in value.items() if n in _AUTH_ALLOWED_PARAMETERS}
# no `name` in config
if `name` not in config and `name` in pg_config:
config[`name`] = pg_config[`name`]
# 当引导新的 Citus 集群(协调者/工作者)时,在全局配置中启用同步复制
# 如果配置中有 citus,则在 bootstrap.dcs 中设置 synchronous_mode 为 quorum
if `citus` in config:
bootstrap = config.setdefault(`bootstrap`, {})
dcs = bootstrap.setdefault(`dcs`, {})
dcs.setdefault(`synchronous_mode`, `quorum`)
updated_fields = (
`name`,
`scope`,
`retry_timeout`,
`citus`
)
# 更新 postgresql 配置,将 updated_fields 中的字段从顶层配置移到 postgresql 中
pg_config.update({p: config[p] for p in updated_fields if p in config})
return config
def get(self, key: str, default: Optional[Any] = None) -> Any:
"""从 Patroni 配置的根获取 ``key`` 设置的有效值。
设计为与 :func:`dict.get` 工作方式相同。
:param key: 设置的名称。
:param default: 如果 *key* 不在有效配置中,则使用的默认值。
:returns: 如果 *key* 在有效配置中,则返回 *key* 的值,否则返回 *default*。
"""
return self.__effective_configuration.get(key, default)
def __contains__(self, key: str) -> bool:
"""检查有效配置中是否存在设置 *key*。
设计上与 `dict.__contains__` 的工作方式相同。
:param key: 要检查的设置名称。
:returns: 如果设置 *key* 在有效配置中存在,则返回 ``True``;否则返回 ``False``。
"""
return key in self.__effective_configuration
def __getitem__(self, key: str) -> Any:
"""从有效配置中获取设置 key 的值。
设计上与 dict.__getitem__ 的工作方式相同。
key: 设置的名称。
:returns: 设置 key 的值。
:raises: :class:KeyError: 如果 key 在有效配置中不存在。
"""
return self.__effective_configuration[key]
def copy(self) -> Dict[str, Any]:
"""获取有效的 Patroni 配置的深拷贝。
:returns: Patroni 配置的深拷贝。
"""
return deepcopy(self.__effective_configuration)
def _validate_failover_tags(self) -> None:
"""检查 ``nofailover``/``failover_priority`` 配置,并在它们相互矛盾时警告用户。
.. note::
为了保持逻辑清晰(并保留向后兼容性),``nofailover`` 标签将继续存在。相互矛盾的配置是指:
- ``nofailover`` 为 ``True`` 但 ``failover_priority > 0``,
- 或者 ``nofailover`` 为 ``False`` 但 ``failover_priority <= 0``。
实质上,``nofailover`` 和 ``failover_priority`` 表达的是不同的意图。
这里检查这种边缘情况(即用户的错误配置),并向用户发出警告。
其行为就像没有提供 ``failover_priority`` 一样(即 ``nofailover`` 是基本的真实来源)。
"""
# 获取 self 对象中的 tags 字典,默认为空字典 {}
tags = self.get(`tags`, {})
# 检查 tags 字典中是否存在 nofailover 键
if `nofailover` not in tags:
return
# 获取 tags 字典中的 nofailover 和 failover_priority 值
nofailover_tag = tags.get(`nofailover`)
failover_priority_tag = parse_int(tags.get(`failover_priority`))
if failover_priority_tag is not None \
and (bool(nofailover_tag) is True and failover_priority_tag > 0
or bool(nofailover_tag) is False and failover_priority_tag <= 0):
logger.warning(`Conflicting configuration between nofailover: %s and failover_priority: %s. `
`Defaulting to nofailover: %s`, nofailover_tag, failover_priority_tag, nofailover_tag)
1.10.2.2 作用
config
类似乎是一个用来管理和验证集群配置的对象。这个类包含了一些方法来处理集群成员的标签(如 nofailover
和 failover_priority
)、动态配置的加载和设置,以及配置中涉及的时间间隔的验证和调整。
- 动态配置管理:
_load_cache
方法用来从一个 JSON 文件中加载动态配置,并设置到当前对象中。set_dynamic_configuration
方法用来设置动态配置,并检测配置的变化。如果检测到配置发生变化,则会更新内部状态,并标记缓存需要保存。
- 配置验证和调整:
_validate_and_adjust_timeouts
方法用来验证和调整与超时相关的配置项,如loop_wait
、retry_timeout
和ttl
,确保它们满足一定的约束条件。__get_and_maybe_adjust_int_value
方法是一个辅助方法,用来获取配置中的整数值,并在必要时进行调整以符合最小值要求。
- 标签配置验证:
_validate_failover_tags
方法用来验证nofailover
和failover_priority
标签之间的关系,如果这两个标签配置相互矛盾,则会记录一条警告日志。
1.11 utils.py
patch_config
函数:合并配置项。parse_bool
函数:各种形式的表示真/假的值转换为 Python 的布尔值。read_stripped
函数:迭代给定文件中的去空白行。parse_int
函数:将value
(传入的参数)解析为int
。convert_to_base_unit
函数:将一个特定单位下的数值转换为另一个指定的基本单位下的等效数值。strtol
函数:从一个字符串开头提取一个可能存在的长整数,并返回这个整数以及剩余的字符串部分。get_conversion_table
函数:根据传入的基本单位base_unit
返回相应的转换表。deep_compare
函数:递归比较两个字典,以检查它们的键和值是否相等。
1.11.1 patch_config()
1.11.1.1 主体
- 定义一个名为
patch_config
的函数,该函数接受两个参数:一个是要更新的配置字典config
,另一个是包含新配置值的字典data
。该函数返回一个布尔值,表示config
是否发生了变化。
def patch_config(config: Dict[Any, Union[Any, Dict[Any, Any]]], data: Dict[Any, Union[Any, Dict[Any, Any]]]) -> bool:
"""更新并追加字典 config 中来自 data 的覆盖项。
注意事项:
如果 data 中某个键的值为 None,则移除 config 中对应的键;
如果 data 中存在某个键,但 config 中不存在,则添加该键及其相应的值到 config;
对于两边都存在的键,比较相应值的字符串表示形式,如果不匹配,则覆盖 config 中的值
参数 config:要更新的配置
参数 data:新的配置值,用于更新 config
返回值:如果 config 发生了变化,则返回 True
"""
# 是否发现变化标志
is_changed = False
# 遍历 data 字典中的键值对
for name, value in data.items():
# 如果 value 是 None,则尝试从 config 中移除对应的键。如果成功移除,则标记 config 已发生变化
if value is None:
if config.pop(name, None) is not None:
is_changed = True
# 如果 name 存在于 config 中
elif name in config:
if isinstance(value, dict):
if isinstance(config[name], dict):
# 如果 value 是字典类型,并且 config[name] 也是字典类型,则递归调用 patch_config 方法来更新 config[name]。如果递归调用返回 True,则标记 config 已发生变化
if patch_config(config[name], value):
is_changed = True
else:
# 如果 value 是字典类型,但 config[name] 不是字典类型,则直接用 value 覆盖 config[name] 并标记 config 已发生变化
config[name] = value
is_changed = True
# 如果 value 不是字典类型,并且 config[name] 与 value 的字符串表示形式不相等,则用 value 覆盖 config[name] 并标记 config 已发生变化
elif str(config[name]) != str(value):
config[name] = value
is_changed = True
# 如果 name 不存在于 config 中,则添加该键及其相应的值到 config 中,并标记 config 已发生变化
else:
config[name] = value
is_changed = True
return is_changed
1.11.1.2 作用
这个 patch_config
函数的作用是对一个配置字典 config
进行更新,使其符合另一个字典 data
中的配置值。具体来说:
- 移除配置项:
- 如果
data
中某个键的值为None
,则从config
中移除该键。
- 如果
- 添加配置项:
- 如果
data
中存在某个键,但config
中不存在,则将该键及其值添加到config
中。
- 如果
- 更新配置项:
- 对于两边都存在的键,比较相应值的字符串表示形式,如果不匹配,则用
data
中的值覆盖config
中的值。 - 如果
value
是字典类型,并且config[name]
也是字典类型,则递归调用patch_config
方法来更新内部的字典。
- 对于两边都存在的键,比较相应值的字符串表示形式,如果不匹配,则用
- 返回是否发生变化:
- 最终返回一个布尔值,表示
config
是否因为这次更新而发生了变化。
- 最终返回一个布尔值,表示
1.11.2 parse_bool()
1.11.2.1 主体
- 定义一个名为
parse_bool
的函数,该函数接受任意类型的参数value
,返回布尔值或None
。
def parse_bool(value: Any) -> Union[bool, None]:
"""将给定的值解析为 bool 对象
.. note::
解析过程不区分大小写,并考虑以下值:
* ``on``, ``true``, ``yes``, and ``1`` as ``True``.
* ``off``, ``false``, ``no``, and ``0`` as ``False``.
:参数 value: 要解析为布尔值的值.
:返回值: 解析后的值。如果无法解析,则返回 None
:Example:
>>> parse_bool(1)
True
>>> parse_bool(`off`)
False
>>> parse_bool(`foo`)
"""
value = str(value).lower()
if value in (`on`, `true`, `yes`, `1`):
return True
if value in (`off`, `false`, `no`, `0`):
return False
1.11.2.2 作用
这个 parse_bool
函数用于将各种形式的表示真/假的值转换为 Python 的布尔值。它能够处理常见的文本表示形式(如 on
、true
、yes
、1
代表 True
,off
、false
、no
、0
代表 False
),并且不区分大小写。这样的函数在处理配置文件、命令行参数或其他需要将非布尔类型的值转换为布尔值的场景中非常有用。如果输入值不符合预设的规则,则函数返回 None
,这有助于处理不明确的输入情况。主要作用是:
- 解析布尔值:
- 将给定的值转换为布尔值,支持多种表示方式。
- 支持的值包括:
on
、true
、yes
和1
作为True
。off
、false
、no
和0
作为False
。
- 不区分大小写:
- 在解析过程中不区分大小写,即不论输入值的字母是大写还是小写,都能正确解析。
- 返回解析结果:
- 如果能解析为布尔值,则返回相应的布尔值(
True
或False
)。 - 如果无法解析,则返回
None
。
- 如果能解析为布尔值,则返回相应的布尔值(
1.11.3 read_stripped()
1.11.3.1 主体
- 定义一个名为
read_stripped
的函数,该函数接受一个字符串参数file_path
,返回一个迭代器,该迭代器中的元素为字符串类型。
def read_stripped(file_path: str) -> Iterator[str]:
"""迭代给定文件中的去空白行
参数:file_path:要从中读取的文件的路径
:yields: 返回:每一行从给定文件中去除前后空白字符后的字符串
"""
with open(file_path) as f:
for line in f:
yield line.strip()
1.11.3.2 作用
这个 read_stripped
函数用于读取指定路径的文件,并返回一个迭代器,该迭代器中的每个元素都是从文件中读取的一行,且去除了该行的前后空白字符。这种方法非常适合用于处理文本文件,特别是当文件很大时,使用生成器可以节省内存,因为不需要一次性将整个文件加载到内存中。此外,使用 with
语句确保了文件在处理完毕后会被正确关闭,防止了文件句柄泄露的问题。主要作用是:
- 读取文件:
- 使用
with open(file_path)
语句打开指定路径的文件。
- 使用
- 去除空白字符:
- 遍历文件中的每一行,并使用
strip()
方法去除每行字符串的前后空白字符。
- 遍历文件中的每一行,并使用
- 生成迭代器:
- 使用
yield
语句返回处理后的每一行,使得函数成为一个生成器,可以按需生成每一行。
- 使用
1.11.4 parse_int()
1.11.4.1 主体
-
定义一个
parse_int
函数,该函数接收两个参数:value
和base_unit
。value
:可以是任何可以由strtol
或strtod
处理的值。如果value
包含单位,则必须给出base_unit
。base_unit
:可选的基本单位,用于通过convert_to_base_unit
函数转换value
。如果value
不包含单位,则不会使用base_unit
。
函数返回一个整数,如果能够解析的话;否则返回
None
。
def parse_int(value: Any, base_unit: Optional[str] = None) -> Optional[int]:
"""将*value*解析为:class: ` int `。
:param value:任何可以由:func: ` strtol `或:func: ` strtod `处理的值。如果*value*包含
单位,则必须给出*base_unit*。
:param base_unit:通过:func: ` convert_to_base_unit `转换*值*的可选基本单位。不使用if
*value*不包含单位。
如果能够解析,则返回解析后的值。否则返回“None”。
:Example:
>>> parse_int(`1`) == 1
True
>>> parse_int(` 0x400 MB `, `16384kB`) == 64
True
>>> parse_int(`1MB`, `kB`) == 1024
True
>>> parse_int(`1000 ms`, `s`) == 1
True
>>> parse_int(`1TB`, `GB`) is None
True
>>> parse_int(50, None) == 50
True
>>> parse_int("51", None) == 51
True
>>> parse_int("nonsense", None) == None
True
>>> parse_int("nonsense", "kB") == None
True
>>> parse_int("nonsense") == None
True
>>> parse_int(0) == 0
True
>>> parse_int(`6GB`, `16MB`) == 384
True
>>> parse_int(`4097.4kB`, `kB`) == 4097
True
>>> parse_int(`4097.5kB`, `kB`) == 4098
True
"""
val, unit = strtol(value)
if val is None and unit.startswith(`.`) or unit and unit[0] in (`.`, `e`, `E`):
val, unit = strtod(value)
if val is not None:
unit = unit.strip()
if not unit:
return round(val)
val = convert_to_base_unit(val, unit, base_unit)
if val is not None:
return round(val)
1.11.4.2 作用
arse_int
函数的作用是从给定的字符串 value
中解析出一个整数。此外,该函数还支持处理带有空格和其它空白字符的输入,并且在无法解析的情况下返回 None
。因此,这个函数非常适合用于需要灵活解析不同类型输入的场景,尤其是在处理配置文件或用户输入时。该函数支持以下几种情况:
- 纯数字:如果输入是一个简单的数字或浮点数,函数会尝试将其转换为整数。
- 带单位的数字:如果输入是一个带有单位的字符串,比如
"1000 ms"
或"1MB"
,函数会尝试将这些单位转换为基本单位(如果提供了base_unit
)。 - 科学计数法:如果输入是一个以科学计数法表示的数字,例如
"1.2e3"
,函数也会尝试解析它。
1.11.5 convert_to_base_unit()
1.11.5.1 主体
- 定义一个
convert_to_base_unit
函数,接受三个参数:value
、unit
和base_unit
。value
:要转换到基本单位的值。unit
:value
的单位。接受以下单位(大小写敏感):- 返回:
value
在unit
单位下转换为base_unit
单位的结果。如果unit
或base_unit
无效,则返回None
。
def convert_to_base_unit(value: Union[int, float], unit: str, base_unit: Optional[str]) -> Union[int, float, None]:
"""将*value*作为计算信息或时间的*单位*转换为*base_unit*。
:param value:要转换为基本单位的值。
:参数单位:*值*的单位。接受以下单位(区分大小写):
*空间:``B``,``kB ` `, ` ` m ` `, ` ` g ` `,或“结核”;
*时间:`` d ``, ` ` h ` `, ` `分钟` `,` ` s ` `, ` `, ` `,或` `我们` `。
:param base_unit:转换后的目标单位。可能包含有关联值的目标单元,例如
“512 mb的。接受以下单位(区分大小写):
*对于空间:` ` B ` `, ` ` kB ` `,或` MB ` `;
*表示时间:“ms”、“s”或“min”。
:将*unit*中的*值*转换为*base_unit*。如果*unit*或*base_unit*无效,则返回` None `。
:Example:
>>> convert_to_base_unit(1, `GB`, `256MB`)
4
>>> convert_to_base_unit(1, `GB`, `MB`)
1024
>>> convert_to_base_unit(1, `gB`, `512MB`) is None
True
>>> convert_to_base_unit(1, `GB`, `512 MB`) is None
True
"""
base_value, base_unit = strtol(base_unit, False)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(base_value, int)
convert_tbl = get_conversion_table(base_unit)
# {`TB`: `GB`, `GB`: `MB`, ...}
round_order = dict(zip(convert_tbl, itertools.islice(convert_tbl, 1, None)))
if unit in convert_tbl and base_unit in convert_tbl[unit]:
value *= convert_tbl[unit][base_unit] / float(base_value)
if unit in round_order:
multiplier = convert_tbl[round_order[unit]][base_unit]
value = round(value / float(multiplier)) * multiplier
return value
1.11.5.2 作用
convert_to_base_unit
函数的作用是将一个特定单位下的数值转换为另一个指定的基本单位下的等效数值。此函数可以用于数据容量的转换(如从 GB 到 MB),也可以用于时间单位的转换(如从小时到分钟)。函数首先解析 base_unit
,然后根据提供的转换表来进行单位之间的转换,并在必要时应用四舍五入逻辑以确保结果的准确性。如果输入的单位不符合预定义的标准,则函数将返回 None
。
1.11.6 strtol()
1.11.6.1 主体
- 定义:定义一个
strtol
函数,接受两个参数:value
和strict
。value
:可以从其中提取长整数的任意值。strict
:决定当strtol
不能在value
中找到长整数时,返回元组的第一个元素应如何设置。如果strict
为True
,则第一个元素将是None
,否则为1
。
- 返回值:返回的元组的第一个元素是从 value 中提取的长整数,第二个元素是 value 的剩余部分。如果不能在 value 中匹配到长整数,则第一个元素将是 None 或 1(取决于 strict 参数),第二个元素将是原始的 value。
def strtol(value: Any, strict: Optional[bool] = True) -> Tuple[Union[int, None], str]:
"""从表示配置值的字符串开头提取长整数部分。
尽可能接近` ` strtol(3) ` ` C函数(base=0), postgres使用它来解析
参数值。
考虑以十六进制、八进制或十进制格式表示的数字。
:param value:要从中提取长整数的任何值。
:param strict:指示当:func: ` strtol `无法找到一个参数时如何设置返回元组中的第一项
*value*中的长整数。如果*strict*为` True `,那么第一项将为` None `,否则将为` 1 `。
:返回:第一项是从*value*中提取的长整数,第二项是*value*的剩余字符串
* *价值。如果无法匹配*value*中的长整数,则第一项将为` ` None ` `或` ` 1 ` `
(取决于*strict*参数),第二项将是原始的*值*。
举例:
>>> strtol(0) == (0, ``)
True
>>> strtol(1) == (1, ``)
True
>>> strtol(9) == (9, ``)
True
>>> strtol(` +0x400MB`) == (1024, `MB`)
True
>>> strtol(` -070d`) == (-56, `d`)
True
>>> strtol(` d `) == (None, `d`)
True
>>> strtol(` 1 d `) == (1, ` d`)
True
>>> strtol(`9s`, False) == (9, `s`)
True
>>> strtol(` s `, False) == (1, `s`)
True
"""
value = str(value).strip()
for regex, base in ((HEX_RE, 16), (OCT_RE, 8), (DEC_RE, 10)):
match = regex.match(value)
if match:
end = match.end()
return int(value[:end], base), value[end:]
return (None if strict else 1), value
1.11.6.2 作用
strtol
函数的主要作用是从一个字符串开头提取一个可能存在的长整数,并返回这个整数以及剩余的字符串部分。该函数考虑了十六进制、八进制和十进制三种格式的数字,并且可以根据 strict
参数控制未找到有效数字时的行为。此函数的设计灵感来源于 C 语言中的 strtol()
函数,并且在 PostgreSQL 数据库系统中用来解析参数值。
1.11.7 get_conversion_table()
1.11.7.1 主体
- 定义:定义一个
get_conversion_table
函数,接受一个参数base_unit
。base_unit
:用于选择转换表的单位。
- 返回值:返回一个有序字典对象。
def get_conversion_table(base_unit: str) -> Dict[str, Dict[str, Union[int, float]]]:
"""获取指定基本单位的转换表。
如果传递的单元不存在转换表,则返回空的:class: ' OrderedDict '。
:param base_unit:选择转换表的单位。
:返回::class: ' OrderedDict '对象。
"""
memory_unit_conversion_table: Dict[str, Dict[str, Union[int, float]]] = OrderedDict([
('TB', {'B': 1024**4, 'kB': 1024**3, 'MB': 1024**2}),
('GB', {'B': 1024**3, 'kB': 1024**2, 'MB': 1024}),
('MB', {'B': 1024**2, 'kB': 1024, 'MB': 1}),
('kB', {'B': 1024, 'kB': 1, 'MB': 1024**-1}),
('B', {'B': 1, 'kB': 1024**-1, 'MB': 1024**-2})
])
time_unit_conversion_table: Dict[str, Dict[str, Union[int, float]]] = OrderedDict([
('d', {'ms': 1000 * 60**2 * 24, 's': 60**2 * 24, 'min': 60 * 24}),
('h', {'ms': 1000 * 60**2, 's': 60**2, 'min': 60}),
('min', {'ms': 1000 * 60, 's': 60, 'min': 1}),
('s', {'ms': 1000, 's': 1, 'min': 60**-1}),
('ms', {'ms': 1, 's': 1000**-1, 'min': 1 / (1000 * 60)}),
('us', {'ms': 1000**-1, 's': 1000**-2, 'min': 1 / (1000**2 * 60)})
])
if base_unit in ('B', 'kB', 'MB'):
return memory_unit_conversion_table
elif base_unit in ('ms', 's', 'min'):
return time_unit_conversion_table
return OrderedDict()
1.11.7.2 作用
get_conversion_table
函数的作用是根据传入的基本单位 base_unit
返回相应的转换表。转换表是一个有序字典,其中包含了不同单位之间转换所需的系数。函数支持两种类型的单位转换:
- 存储单位:包括
'TB'
,'GB'
,'MB'
,'kB'
,'B'
。 - 时间单位:包括
'd'
(天),'h'
(小时),'min'
(分钟),'s'
(秒),'ms'
(毫秒),'us'
(微秒)。
函数根据 base_unit
的类型返回对应的转换表,如果没有找到匹配的单位,则返回一个空的有序字典。这使得函数可以在需要进行单位转换的地方使用,例如在进行存储空间或时间单位的转换时。
1.11.8 deep_compare()
1.11.8.1 主体
- 定义了一个名为
deep_compare
的函数,它接收两个参数obj1
和obj2
,类型均为Dict[Any, Union[Any, Dict[Any, Any]]]
,返回类型为bool
。
def deep_compare(obj1: Dict[Any, Union[Any, Dict[Any, Any]]], obj2: Dict[Any, Union[Any, Dict[Any, Any]]]) -> bool:
"""递归比较两个字典,以检查它们的键和值是否相等。
.. note::
值是基于它们的字符串表示形式来进行比较的。
:param obj1: 要与 *obj2* 比较的字典。
:param obj2: 要与 *obj1* 比较的字典。
:returns: 如果两个字典中的所有键和值都匹配,则返回 ``True``。
:Example:
>>> deep_compare({'1': None}, {})
False
>>> deep_compare({'1': {}}, {'1': None})
False
>>> deep_compare({'1': [1]}, {'1': [2]})
False
>>> deep_compare({'1': 2}, {'1': '2'})
True
>>> deep_compare({'1': {'2': [3, 4]}}, {'1': {'2': [3, 4]}})
True
"""
# 如果 obj1 和 obj2 的键集不相等,则直接返回 False
if set(list(obj1.keys())) != set(list(obj2.keys())): # Objects have different sets of keys
return False
# 遍历 obj1 中的所有键值对
for key, value in obj1.items():
# 果当前值 value 是字典类型,则检查 obj2 中对应的值是否也是字典,并递归调用 deep_compare 函数进行比较
if isinstance(value, dict):
if not (isinstance(obj2[key], dict) and deep_compare(value, obj2[key])):
return False
# 如果当前值 value 不是字典类型,则将其转换为字符串并与 obj2 中对应的值的字符串表示形式进行比较
elif str(value) != str(obj2[key]):
return False
return True
1.11.8.2 作用
deep_compare
函数特别适用于需要深入比较嵌套字典的情况,尤其是在配置文件或复杂的对象结构中需要确定两个对象是否完全相等时非常有用。通过将值转换为字符串进行比较,可以处理不同类型但字符串表示形式相同的值,例如数字 2
和字符串 '2'
被视为相等。主要作用是递归地比较两个字典是否相等。具体来说:
- 键集比较:首先比较两个字典的键集合是否完全一致。
- 值比较:
- 如果值是字典类型,则递归调用
deep_compare
进行比较。 - 如果值不是字典类型,则将值转换为字符串后进行比较。
- 如果值是字典类型,则递归调用
- 返回结果:如果所有的键值对都匹配,则返回
True
;否则返回False
。
1.11.9 类:Retry
- 定义了一个名为
Retry
的类,继承自object
。
class Retry(object):
"""重试辅助类,在遇到可重试异常时重试方法。
:ivar max_tries: 重试命令的次数。
:ivar delay: 初始重试延迟。
:ivar backoff: 重试之间的回退倍增因子。
:ivar max_jitter: 在重试之间等待的附加最大抖动周期,以避免服务器负载过大。
:ivar max_delay: 最大延迟秒数,不管其他回退设置如何。
:ivar sleep_func: 用于引入人为延迟的函数。
:ivar deadline: 重试操作的超时时间。
:ivar retry_exceptions: 单个异常或异常元组。
"""
1.11.9.1 __init__()
- 定义了
Retry
类的构造函数__init__
,该构造函数接受多个可选参数,并且没有返回值。
def __init__(self, max_tries: Optional[int] = 1, delay: float = 0.1, backoff: int = 2,
max_jitter: float = 0.8, max_delay: int = 3600,
sleep_func: Callable[[Union[int, float]], None] = _sleep,
deadline: Optional[Union[int, float]] = None,
retry_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = PatroniException) -> None:
"""创建一个 :class:`Retry` 实例用于重试函数调用。
:param max_tries: 重试命令的次数。``-1`` 表示无限次重试。
:param delay: 初始重试延迟。
:param backoff: 重试之间的回退倍增因子。默认为 ``2`` 用于指数回退。
:param max_jitter: 在重试之间等待的附加最大抖动周期,以避免服务器负载过大。
:param max_delay: 最大延迟秒数,不管其他回退设置如何。
:param sleep_func: 用于引入人为延迟的函数。
:param deadline: 重试操作的超时时间。
:param retry_exceptions: 单个异常或异常元组。
"""
self.max_tries = max_tries
self.delay = delay
self.backoff = backoff
self.max_jitter = int(max_jitter * 100)
self.max_delay = float(max_delay)
self._attempts = 0
self._cur_delay = delay
self.deadline = deadline
self._cur_stoptime = None
self.sleep_func = sleep_func
self.retry_exceptions = retry_exceptions
作用:
__init__
方法的作用是在创建 Retry
类的实例时初始化必要的属性。具体来说:
- 初始化重试配置:设置重试的最大次数、初始延迟、回退倍增因子、最大抖动周期、最大延迟时间等。
- 初始化计数器和状态变量:设置当前尝试次数为
0
,当前延迟时间为初始延迟时间,当前停止时间为None
。 - 设置延迟函数和异常类型:设置用于引入延迟的函数以及需要重试的异常类型。
1.11.10 is_subpath()
- 定义:定义一个
strtol
函数,接受两个参数:value
和strict
。value
:可以从其中提取长整数的任意值。strict
:决定当strtol
不能在value
中找到长整数时,返回元组的第一个元素应如何设置。如果strict
为True
,则第一个元素将是None
,否则为1
。
- 返回值:返回的元组的第一个元素是从 value 中提取的长整数,第二个元素是 value 的剩余部分。如果不能在 value 中匹配到长整数,则第一个元素将是 None 或 1(取决于 strict 参数),第二个元素将是原始的 value。
def is_subpath(d1: str, d2: str) -> bool:
"""检查文件系统路径 *d2* 是否在解析符号链接后位于 *d1* 内。
.. note::
它不会检查路径实际上是否存在,它只会扩展路径并解析任何发现的符号链接。
:param d1: 一个目录的路径。
:param d2: 需要检查是否位于 *d1* 内的路径。
:returns: 如果 *d1* 是 *d2* 的子路径,则返回 ``True``。
"""
# 获取 d1 路径的真实路径,并在其末尾加上路径分隔符
real_d1 = os.path.realpath(d1) + os.path.sep
# 将 d2 与 real_d1 路径合并,并获取合并后的路径的真实路径
real_d2 = os.path.realpath(os.path.join(real_d1, d2))
# 计算 real_d1 和 real_d2 的公共前缀,并判断该公共前缀是否等于 real_d1
return os.path.commonprefix([real_d1, real_d2 + os.path.sep]) == real_d1
作用:
is_subpath
函数的作用是检查一个路径 d2
是否是另一个路径 d1
的子路径。具体来说:
- 路径规范化:
- 获取
d1
和d2
的真实路径(解析符号链接)。
- 获取
- 路径合并:
- 将
d2
与d1
的真实路径合并,并获取合并后路径的真实路径。
- 将
- 公共前缀比较:
- 计算
d1
和合并后d2
路径的公共前缀。 - 判断公共前缀是否等于
d1
的真实路径,如果是,则说明d2
是d1
的子路径。
- 计算
通过这些步骤,is_subpath
函数能够准确地判断一个路径是否位于另一个路径的目录树内,这对于确保路径的安全性和正确性非常重要,尤其是在处理文件系统操作时。
1.11.11 strtod()
- 定义了一个名为
strtod
的函数,它接受一个任意类型的参数value
,并返回一个元组,其中第一个元素是浮点数或None
,第二个元素是字符串。
def strtod(value: Any) -> Tuple[Union[float, None], str]:
"""从表示配置值的字符串开头提取双精度浮点数部分。
尽可能接近等同于 C 函数 ``strtod(3)``,该函数被 postgres 用来解析参数值。
:param value: 从中提取双精度浮点数的任意值。
:returns: 第一个元素是从 *value* 中提取的双精度浮点数,第二个元素是 *value* 的剩余字符串。如果不能在 *value* 中匹配到 双精度浮点数,则第一个元素将是 ``None``,第二个元素将是原始的 *value*。
:Example:
>>> strtod(' A ') == (None, 'A')
True
>>> strtod('1 A ') == (1.0, ' A')
True
>>> strtod('1.5A') == (1.5, 'A')
True
>>> strtod('8.325e-10A B C') == (8.325e-10, 'A B C')
True
"""
# 将 value 转换为字符串,并去除首尾空白字符
value = str(value).strip()
# 使用正则表达式 DBL_RE 匹配 value 的开头部分,以查找是否包含一个双精度浮点数
match = DBL_RE.match(value)
# 如果匹配成功,获取匹配部分的结束位置 end,然后返回从字符串开头到结束位置的浮点数值,以及结束位置之后的剩余字符串
if match:
end = match.end()
return float(value[:end]), value[end:]
return None, value
作用:
strtod
函数的具体作用是从给定的字符串开头提取双精度浮点数值,并返回提取的结果以及剩余的字符串部分。具体来说:
- 字符串处理:
- 将输入的
value
转换为字符串,并去除首尾空白字符。
- 将输入的
- 正则表达式匹配:
- 使用预定义的正则表达式
DBL_RE
来匹配字符串开头的双精度浮点数值。
- 使用预定义的正则表达式
- 结果返回:
- 如果匹配成功,返回提取的浮点数值和剩余字符串;
- 如果匹配失败,返回
None
和原字符串。
这个函数的设计目的是为了模仿 C 语言中的 strtod
函数的行为,该函数通常用于从字符串中解析双精度浮点数值。在 PostgreSQL 中,这样的功能可能会用于解析配置文件或命令行参数中的数值。通过这个函数,可以确保以一种与 PostgreSQL 相兼容的方式从字符串中提取双精度浮点数值。
1.11.12 split_host_port()
- 定义了一个名为
split_host_port
的函数,它接受两个参数:一个字符串value
和一个可选的整数default_port
,返回一个包含两个元素的元组,分别是字符串和整数。
def split_host_port(value: str, default_port: Optional[int]) -> Tuple[str, int]:
"""从 *value* 中提取主机(s)和端口。
:param value: 从中提取主机(s)和端口的字符串。接受以下格式之一:
* ``host:port``;或
* ``host1,host2,...,hostn:port``。
每个 *value* 中的 ``host`` 部分可以是:
* 完全限定域名(FQDN);或
* IPv4 地址;或
* IPv6 地址,带或不带方括号。
:param default_port: 如果 *param* 中找不到端口,则使用 *default_port* 。
:returns: 第一个元素由 *value* 中的 CSV 列表组成,第二个元素是 *value* 中的端口或 *default_port* 。
:Example:
>>> split_host_port('127.0.0.1', 5432)
('127.0.0.1', 5432)
>>> split_host_port('127.0.0.1:5400', 5432)
('127.0.0.1', 5400)
>>> split_host_port('127.0.0.1,192.168.0.101:5400', 5432)
('127.0.0.1,192.168.0.101', 5400)
>>> split_host_port('127.0.0.1,www.mydomain.com,[fe80:0:0:0:213:72ff:fe3c:21bf], 0:0:0:0:0:0:0:0:5400', 5432)
('127.0.0.1,www.mydomain.com,fe80:0:0:0:213:72ff:fe3c:21bf,0:0:0:0:0:0:0:0', 5400)
"""
# 使用 rsplit 方法从右侧开始分割 value,只分割一次,得到一个列表 t
t = value.rsplit(':', 1)
# 如果 *value* 包含 ``:`` 我们认为它是一个 IPv6 地址,所以我们尝试移除可能的方括号。
if ':' in t[0]:
t[0] = ','.join([h.strip().strip('[]') for h in t[0].split(',')])
# 将 default_port 转换为字符串,并追加到列表 t 的末尾。
t.append(str(default_port))
return t[0], int(t[1])
作用:
split_host_port
函数的具体作用是从给定的字符串 value
中提取主机名或 IP 地址列表和端口号。具体来说:
- 分割主机和端口:
- 使用
rsplit
方法从右侧开始分割字符串value
,最多分割一次,得到一个包含两部分的列表t
。
- 使用
- 处理 IPv6 地址:
- 如果
value
中包含:
,则认为这是一个 IPv6 地址,并移除可能存在的方括号。
- 如果
- 补充默认端口:
- 如果
value
中没有明确的端口号,使用default_port
补充,并将其转换为字符串后追加到列表t
中。
- 如果
- 返回结果:
- 返回由处理后的主机名或 IP 地址列表
t[0]
和端口号int(t[1])
组成的元组。
- 返回由处理后的主机名或 IP 地址列表
1.11.13 compare_values()
- 定义了一个名为
compare_values
的函数,它接受四个参数:vartype
(字符串)、unit
(可选字符串)、settings_value
(任意类型)和config_value
(任意类型),并返回一个布尔值。
def compare_values(vartype: str, unit: Optional[str], settings_value: Any, config_value: Any) -> bool:
"""检查来自 ``pg_settings`` 和来自 Patroni 配置的值在解析为 *vartype* 后是否等价。
:param vartype: 解析 *settings_value* 和 *config_value* 之前的预期类型。
接受以下类型之一(区分大小写):
* ``bool``: 使用 :func:`parse_bool` 解析值;或
* ``integer``: 使用 :func:`parse_int` 解析值;或
* ``real``: 使用 :func:`parse_real` 解析值;或
* ``enum``: 解析值为小写字符串;或
* ``string``: 解析值为字符串。如果传递给 *vartype* 的不是有效值,则使用此选项作为默认值。
:param unit: 当调用 :func:`parse_int` 或 :func:`parse_real` 解析 *config_value* 时使用的基准单位。
:param settings_value: 与 *config_value* 进行比较的值。
:param config_value: 与 *settings_value* 进行比较的值。
:returns: 如果 *settings_value* 和 *config_value* 解析为 *vartype* 后等价,则返回 ``True``。
:Example:
>>> compare_values('enum', None, 'remote_write', 'REMOTE_WRITE')
True
>>> compare_values('string', None, 'remote_write', 'REMOTE_WRITE')
False
>>> compare_values('real', None, '1e-06', 0.000001)
True
>>> compare_values('integer', 'MB', '6GB', '6GB')
False
>>> compare_values('integer', None, '6GB', '6GB')
False
>>> compare_values('integer', '16384kB', '64', ' 0x400 MB ')
True
>>> compare_values('integer', '2MB', 524288, '1TB')
True
>>> compare_values('integer', 'MB', 1048576, '1TB')
True
>>> compare_values('integer', 'kB', 4098, '4097.5kB')
True
"""
# 定义了一个字典 converters,映射不同的 vartype 到对应的转换函数。每个转换函数负责将输入值转换为目标类型
converters: Dict[str, Callable[[str, Optional[str]], Union[None, bool, int, float, str]]] = {
'bool': lambda v1, v2: parse_bool(v1),
'integer': parse_int,
'real': parse_real,
'enum': lambda v1, v2: str(v1).lower(),
'string': lambda v1, v2: str(v1)
}
# 根据 vartype 获取对应的转换函数,如果 vartype 不合法,则默认使用 string 类型的转换函数
converter = converters.get(vartype) or converters['string']
old_converted = converter(settings_value, None)
new_converted = converter(config_value, unit)
return old_converted is not None and new_converted is not None and old_converted == new_converted
作用:
compare_values
函数的具体作用是比较两个值 settings_value
和 config_value
是否等价,这两个值首先会被解析为 vartype
指定的数据类型。具体来说:
- 解析类型:
- 根据
vartype
选择合适的转换函数,将settings_value
和config_value
分别转换为目标类型。
- 根据
- 比较转换后的值:
- 如果两个转换后的值都存在并且相等,则返回
True
,否则返回False
。
- 如果两个转换后的值都存在并且相等,则返回
1.11.14 uri()
- 定义了一个名为
uri
的函数,它接受四个参数:proto
(字符串类型)、netloc
(列表、元组或字符串类型)、path
(可选字符串类型,默认为空字符串)和user
(可选字符串类型,默认为None
),返回一个字符串。
def uri(proto: str, netloc: Union[List[str], Tuple[str, Union[int, str]], str], path: Optional[str] = '',
user: Optional[str] = None) -> str:
"""根据给定的参数构造 URI。
:param proto: URI 协议。
:param netloc: URI 主机(们)和端口。可以用以下任意一种方式指定:
* 一个 :class:`list` 或 :class:`tuple`。第二个项应该是端口,而第一个项应该由以下格式之一的主机组成:
* ``host``;或
* ``host1,host2,...,hostn``。
* 一个 :class:`str`,格式如下:
* ``host:port``;或
* ``host1,host2,...,hostn:port``。
在所有情况下,*netloc* 中的每个 ``host`` 部分可以是:
* 一个 FQDN;或
* 一个 IPv4 地址;或
* 一个 IPv6 地址,带或不带方括号。
:param path: URI 路径。
:param user: 如果有的话,认证用户。
:returns: 构造好的 URI。
"""
# 如果 netloc 是列表或元组,则直接将其拆分为 host 和 port
host, port = netloc if isinstance(netloc, (list, tuple)) else split_host_port(netloc, 0)
# 如果 host 包含 :,认为这是一个 IPv6 地址,如果缺少方括号,则添加方括号
if host and ':' in host and host[0] != '[' and host[-1] != ']':
host = '[{0}]'.format(host)
# 如果 port 存在,则格式化为 ':port',否则为空字符串
port = ':{0}'.format(port) if port else ''
# 如果 path 存在并且不以斜杠 / 开头,则在前面加上斜杠 /;否则保持原样
path = '/{0}'.format(path) if path and not path.startswith('/') else path
# 如果 user 存在,则格式化为 '{user}@$',否则为空字符串
user = '{0}@'.format(user) if user else ''
# 构造并返回最终的 URI 字符串
return '{0}://{1}{2}{3}{4}'.format(proto, user, host, port, path)
作用:
uri
函数的具体作用是根据给定的参数构建一个完整的 URI(统一资源标识符)。具体来说:
- 解析
netloc
参数:netloc
可以是列表、元组或字符串,根据其类型来解析出host
和port
。
- 处理 IPv6 地址:
- 如果
host
是 IPv6 地址,并且没有方括号,则添加方括号。
- 如果
- 格式化
port
:- 如果
port
存在,则格式化为':port'
。
- 如果
- 处理
path
:- 如果
path
存在且不以斜杠/
开始,则在其前加上斜杠/
。
- 如果
- 处理
user
:- 如果
user
存在,则格式化为{user}@
。
- 如果
- 构造完整 URI:
- 使用
proto
、user
、host
、port
和path
组装成完整的 URI 字符串并返回。
- 使用
1.11.15 data_directory_is_empty()
- 定义了一个名为
data_directory_is_empty
的函数,它接受一个字符串参数data_dir
,并返回一个布尔值。
def data_directory_is_empty(data_dir: str) -> bool:
"""检查 PostgreSQL 数据目录是否为空。
.. note::
在非 Windows 环境中,如果 *data_dir* 只包含隐藏文件和/或 ``lost+found`` 目录,也被认为是空的。
:param data_dir: 要检查的 PostgreSQL 数据目录。
:returns: 如果 *data_dir* 是空的,则返回 ``True``。
"""
if not os.path.exists(data_dir):
return True
return all(os.name != 'nt' and (n.startswith('.') or n == 'lost+found') for n in os.listdir(data_dir))
作用:
data_directory_is_empty
函数的具体作用是检查一个 PostgreSQL 数据目录是否为空。具体来说:
- 目录存在性检查:
- 如果给定的路径不存在,则认为目录为空。
- 内容检查:
- 如果目录存在,则检查目录中的内容是否仅包含隐藏文件或
lost+found
目录(在非 Windows 环境下)。
- 如果目录存在,则检查目录中的内容是否仅包含隐藏文件或
- 返回结果:
- 如果目录为空(按上述标准),则返回
True
;否则返回False
。
- 如果目录为空(按上述标准),则返回
1.12 postgresql文件夹
存放postgresql相关代码。
misc.py
:config.py
:mpp文件夹
:__init__.py
:citus.py
:
__init__.py
:connection.py
:bootstrap.py
:slots.py
:sync.py
:callback_executor.py
:cancellable.py
:
1.12.1 misc.py
- ``postgres_major_version_to_int`函数:将 PostgreSQL 的版本号转换为一个整数。
- ``postgres_major_version_to_int`函数:将 PostgreSQL 的版本号转换为一个整数。
1.12.1.1 postgres_major_version_to_int()
1.12.1.1.1 主体
- 定义一个名为
postgres_major_version_to_int
的函数,该函数接受一个字符串参数pg_version
,表示 PostgreSQL 的版本号,并返回一个整数,表示该版本号的整数表示形式。
def postgres_major_version_to_int(pg_version: str) -> int:
"""
>>> postgres_major_version_to_int(`10`)
100000
>>> postgres_major_version_to_int(`9.6`)
90600
"""
# 调用 postgres_version_to_int 函数,并将 pg_version 后面加上 .0 以确保版本号格式正确,然后返回转换后的整数值
return postgres_version_to_int(pg_version + `.0`)
1.12.1.1.2 作用
这个 postgres_major_version_to_int
函数的作用是将 PostgreSQL 的版本号转换为一个整数,以便更容易地进行版本号的比较和处理。具体来说:
- 输入参数:
pg_version
:一个字符串,表示 PostgreSQL 的版本号,例如10
或9.6
。
- 转换逻辑:
- 函数内部调用了
postgres_version_to_int
函数,并将pg_version
后面加上.0
以确保版本号格式正确,然后返回转换后的整数值。
- 函数内部调用了
- 返回值:
- 返回一个整数,表示 PostgreSQL 版本号的整数表示形式。例如:
10
转换成100000
。9.6
转换成90600
。
- 返回一个整数,表示 PostgreSQL 版本号的整数表示形式。例如:
1.12.1.2 postgres_major_version_to_int()
1.12.1.2.1 主体
- 定义一个名为
postgres_version_to_int
的函数,该函数接受一个字符串参数pg_version
,表示 PostgreSQL 的版本号,并返回一个整数,表示该版本号的整数表示形式。
def postgres_version_to_int(pg_version: str) -> int:
"""将server_version转换为整数
>>> postgres_version_to_int(`9.5.3`)
90503
>>> postgres_version_to_int(`9.3.13`)
90313
>>> postgres_version_to_int(`10.1`)
100001
>>> postgres_version_to_int(`10`) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
PostgresException: `Invalid PostgreSQL version format: X.Y or X.Y.Z is accepted: 10`
>>> postgres_version_to_int(`9.6`) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
PostgresException: `Invalid PostgreSQL version format: X.Y or X.Y.Z is accepted: 9.6`
>>> postgres_version_to_int(`a.b.c`) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
PostgresException: `Invalid PostgreSQL version: a.b.c`
"""
try:
# 尝试将 pg_version 按照点号分割,并将分割后的字符串转换为整数列表
components = list(map(int, pg_version.split(`.`)))
except ValueError:
# 字符串包含非数字字符
raise PostgresException(`Invalid PostgreSQL version: {0}`.format(pg_version))
# 检查 components 列表的长度
if len(components) < 2 or len(components) == 2 and components[0] < 10 or len(components) > 3:
# 版本号格式无效
raise PostgresException(`Invalid PostgreSQL version format: X.Y or X.Y.Z is accepted: {0}`.format(pg_version))
if len(components) == 2:
# 如果 components 列表的长度为 2,则插入一个 0 作为第二个元素,以确保版本号格式为 X.0.Y
# 新样式的版本号,即10.1变成100001
components.insert(1, 0)
# 将 components 列表中的每个元素转换为两位数的字符串形式,并拼接成一个完整的字符串
return int(``.join(`{0:02d}`.format(c) for c in components))
1.12.1.2.2 作用
这个 postgres_version_to_int
函数的作用是将 PostgreSQL 的版本号转换为一个整数,以便更容易地进行版本号的比较和处理。具体来说:
- 输入参数:
pg_version
:一个字符串,表示 PostgreSQL 的版本号,例如9.5.3
或10.1
。
- 转换逻辑:
- 函数首先检查版本号的有效性,确保其格式为
X.Y
或X.Y.Z
。 - 对于格式为
X.Y
的版本号,插入一个0
作为补丁版本号,使其格式为X.0.Y
。 - 将版本号的各个部分转换为两位数的字符串形式,并拼接成一个完整的字符串。
- 将拼接后的字符串转换为整数,并返回该整数。
- 函数首先检查版本号的有效性,确保其格式为
- 返回值:
- 返回一个整数,表示 PostgreSQL 版本号的整数表示形式。例如:
9.5.3
转换成90503
。9.3.13
转换成90313
。10.1
转换成100001
。
- 返回一个整数,表示 PostgreSQL 版本号的整数表示形式。例如:
1.12.2 config.py
parse_dsn
函数:解析dsn。conninfo_uri_parse
函数:解析 PostgreSQL 数据库的连接字符串(DSN URI)。conninfo_parse
函数:解析 PostgreSQL 数据库的连接字符串(DSN)。read_param_value
函数:用于解析 PostgreSQL 数据库连接字符串中的参数值部分。
1.12.2.1 parse_dsn()
1.12.2.1.1 主体
- 定义一个名为
parse_dsn
的函数,该函数接受一个字符串参数value
,返回一个字典或None
。
def parse_dsn(value: str) -> Optional[Dict[str, str]]:
"""
非常简单,相当于` psycopg2.extensions `。Parse_dsn `在2.7.0中引入。
为了与2.5.4+保持兼容,我们没有使用psycopg2函数。
虽然有一些小的区别,这个函数设置了` sslmode `, `gssencmode`,
和` channel_binding `到` prefer `,如果它们不存在于连接字符串中。
这对于简化新旧值的比较是必要的。
>>> r = parse_dsn(`postgresql://u%2Fse:pass@:%2f123,[::1]/db%2Fsdf?application_name=mya%2Fpp&ssl=true`)
>>> r == {`application_name`: `mya/pp`, `dbname`: `db/sdf`, `host`: `,::1`, `sslmode`: `require`,\
`password`: `pass`, `port`: `/123,`, `user`: `u/se`, `gssencmode`: `prefer`, `channel_binding`: `prefer`}
True
>>> r = parse_dsn(" host = `host` dbname = db\\\\ name requiressl=1 ")
>>> r == {`dbname`: `db name`, `host`: `host`, `sslmode`: `require`,\
`gssencmode`: `prefer`, `channel_binding`: `prefer`}
True
>>> parse_dsn(`requiressl = 0\\\\`) == {`sslmode`: `prefer`, `gssencmode`: `prefer`, `channel_binding`: `prefer`}
True
>>> parse_dsn("host=a foo = `") is None
True
>>> parse_dsn("host=a foo = ") is None
True
>>> parse_dsn("1") is None
True
"""
# 检查 DSN 是否以 postgres:// 或 postgresql:// 开头
if value.startswith(`postgres://`) or value.startswith(`postgresql://`):
ret = conninfo_uri_parse(value)
else:
ret = conninfo_parse(value)
# 检查解析结果是否有效
if ret:
if `sslmode` not in ret: # 允许sslmode优先于requiressl
requiressl = ret.pop(`requiressl`, None)
if requiressl == `1`:
ret[`sslmode`] = `require`
elif requiressl is not None:
ret[`sslmode`] = `prefer`
ret.setdefault(`sslmode`, `prefer`)
ret.setdefault(`gssencmode`, `prefer`)
ret.setdefault(`channel_binding`, `prefer`)
return ret
1.12.2.1.2 作用
这个 parse_dsn
函数用于解析 PostgreSQL 数据库的连接字符串(DSN),并将解析出的参数存放在字典中返回。该函数特别注意处理 SSL 相关的参数,并且在某些情况下会设置默认值以简化旧值和新值之间的比较。这样可以确保即使在缺少某些参数的情况下,也能得到一个一致的解析结果。函数的主要作用是:
- 解析 DSN:
- 接受一个字符串类型的 DSN,并尝试解析出其中的各个参数。
- 如果 DSN 以
postgres://
或postgresql://
开头,则使用conninfo_uri_parse
函数解析;否则使用conninfo_parse
函数解析。
- 处理 SSL 相关参数:
- 如果结果中没有
sslmode
,则根据requiressl
的值设置sslmode
。 - 如果结果中没有
gssencmode
和channel_binding
,则将它们设置为prefer
。
- 如果结果中没有
- 返回解析结果:
- 返回解析后的字典,如果没有成功解析,则返回
None
。
- 返回解析后的字典,如果没有成功解析,则返回
1.12.2.2 conninfo_uri_parse()
1.12.2.2.1 主体
- 定义一个名为
conninfo_uri_parse
的函数,该函数接受一个字符串参数dsn
,返回一个字典。
def conninfo_uri_parse(dsn: str) -> Dict[str, str]:
ret: Dict[str, str] = {}
r = urlparse(dsn)
if r.username:
ret[`user`] = r.username
if r.password:
ret[`password`] = r.password
if r.path[1:]:
ret[`dbname`] = r.path[1:]
hosts: List[str] = []
ports: List[str] = []
for netloc in r.netloc.split(`@`)[-1].split(`,`):
host = None
if `[` in netloc and `]` in netloc:
tmp = netloc.split(`]`) + [``]
host = tmp[0][1:]
netloc = `:`.join(tmp[:2])
tmp = netloc.rsplit(`:`, 1)
if host is None:
host = tmp[0]
hosts.append(host)
ports.append(tmp[1] if len(tmp) == 2 else ``)
if hosts:
ret[`host`] = `,`.join(hosts)
if ports:
ret[`port`] = `,`.join(ports)
ret = {name: unquote(value) for name, value in ret.items()}
ret.update({name: value for name, value in parse_qsl(r.query)})
if ret.get(`ssl`) == `true`:
del ret[`ssl`]
ret[`sslmode`] = `require`
return ret
1.12.2.2.2 作用
这个 conninfo_uri_parse
函数用于解析 PostgreSQL 数据库的连接字符串(DSN URI),并将解析出的参数存放在字典中返回。该函数能够正确处理各种不同的 DSN URI 格式,包括 IPv6 地址,并且能够处理查询字符串中的参数。这样可以确保从 DSN URI 中提取所有必要的连接信息,并以一致的格式返回。主要作用是:
- 解析 DSN URI:
- 接受一个字符串类型的 DSN URI,并尝试解析出其中的各个参数。
- 使用
urlparse
函数解析 DSN URI。
- 提取参数:
- 提取用户名、密码、数据库名称、主机地址和端口,并将它们存放在字典中。
- 特别处理 IPv6 地址的情况。
- 处理查询字符串:
- 将查询字符串部分解析成键值对,并更新到字典中。
- 处理 SSL 参数:
- 如果
ssl
键的值为true
,则删除该键,并设置sslmode
为require
。
- 如果
- 返回解析结果:
- 返回一个包含所有解析参数的字典。
1.12.2.3 conninfo_parse()
1.12.2.3.1 主体
- 定义一个名为
conninfo_parse
的函数,该函数接受一个字符串参数dsn
,返回一个字典或None
。
def conninfo_parse(dsn: str) -> Optional[Dict[str, str]]:
ret: Dict[str, str] = {}
length = len(dsn)
i = 0
while i < length:
if dsn[i].isspace():
i += 1
continue
param_match = PARAMETER_RE.match(dsn[i:])
if not param_match:
return
param = param_match.group(1)
i += param_match.end()
if i >= length:
return
# 调用 read_param_value 函数读取参数值,并获取读取的长度
value, end = read_param_value(dsn[i:])
if value is None or end is None:
return
i += end
ret[param] = value
return ret
1.12.2.3.2 作用
这个 conninfo_parse
函数用于解析 PostgreSQL 数据库的连接字符串(DSN),并将解析出的参数存放在字典中返回。该函数能够逐字符地遍历 DSN 字符串,处理空白字符,并使用正则表达式匹配参数名。通过调用 read_param_value
函数来读取参数值,并将所有有效的参数名和值存放在字典中返回。这样可以确保从 DSN 字符串中正确提取所有必要的连接信息,并以一致的格式返回。主要作用是:
- 解析 DSN 字符串:
- 接受一个字符串类型的 DSN,并尝试解析出其中的各个参数。
- 逐字符遍历 DSN 字符串,跳过空白字符。
- 匹配参数名:
- 使用正则表达式
PARAMETER_RE
匹配参数名。 - 如果匹配失败,则返回
None
。
- 使用正则表达式
- 读取参数值:
- 调用
read_param_value
函数读取参数值,并获取读取的长度。 - 如果读取失败,则返回
None
。
- 调用
- 保存参数:
- 更新索引
i
,并将参数名和对应的值添加到ret
字典中。
- 更新索引
- 返回解析结果:
- 返回一个包含所有解析参数的字典,如果在任何步骤中出现错误,则返回
None
。
- 返回一个包含所有解析参数的字典,如果在任何步骤中出现错误,则返回
1.12.2.4 read_param_value()
1.12.2.4.1 主体
- 定义一个名为
read_param_value
的函数,该函数接受一个字符串参数value
,返回一个元组,元组的第一个元素是字符串或None
,第二个元素是整数或None
。
def read_param_value(value: str) -> Union[Tuple[None, None], Tuple[str, int]]:
length = len(value)
ret = ``
is_quoted = value[0] == "`"
i = int(is_quoted)
while i < length:
if is_quoted:
if value[i] == "`":
return ret, i + 1
elif value[i].isspace():
break
if value[i] == `\\`:
i += 1
if i >= length:
break
ret += value[i]
i += 1
return (None, None) if is_quoted else (ret, i)
1.12.2.4.2 作用
这个 read_param_value
函数用于解析 PostgreSQL 数据库连接字符串中的参数值部分,并将解析出的值返回。该函数能够处理被单引号包围的字符串以及未被引号包围的字符串,并支持转义字符的处理。这样可以确保从参数值字符串中正确提取实际的参数值,并以一致的格式返回。主要作用是:
- 解析参数值:
- 接受一个字符串类型的参数值,并尝试解析出其真正的值。
- 根据第一个字符是否为单引号 ``` 来判断是否是被引号包围的字符串。
- 处理引号包围的字符串:
- 如果参数值是以单引号开始的,则查找结束的单引号,并返回中间的字符串。
- 如果找到了结束的单引号,则返回已解析的字符串和当前位置。
- 处理未引号包围的字符串:
- 如果参数值不是以单引号开始的,则查找第一个空白字符,并返回之前的字符串。
- 如果遇到了反斜杠
\
,则跳过下一个字符(转义字符)。
- 返回解析结果:
- 返回一个元组,元组的第一个元素是解析的字符串或
None
,第二个元素是当前位置或None
。 - 如果是被引号包围的字符串但没有找到结束的单引号,则返回
(None, None)
。
- 返回一个元组,元组的第一个元素是解析的字符串或
1.12.2.5 类:ConfigHandler
- 定义了一个名为
ConfigHandler
的类,继承自object
。 - 此帮助类用于管理从 Patroni 到 PostgreSQL 的命名连接。此类实例保留命名的
NamedConnection
对象及用于新连接的参数。
class ConfigHandler(object):
# 必须始终作为命令行选项传递给postmaster的参数列表,以防止使用 ALTERSYSTEM 更改它们。
# 这些参数只能全局更改,例如通过DCS。
# 注意:'listen_addresses' 和 'port' 添加到这里只是为了方便,标记为应始终通过命令行传递的参数。
# 格式:
# key - 参数名称
# value - 元组(默认值, 验证函数, 最小版本)
# 默认值 - - 一些合理的默认值
# 验证函数 - - 如果新值不正确,则必须返回!False
# 最小版本 - - 参数引入时的PostgreSQL主要版本
# 初始化 CMDLINE_OPTIONS 字典,该字典包含了一系列参数及其配置信息,包括默认值、验证函数以及这些参数适用的 PostgreSQL 最小版本。
CMDLINE_OPTIONS = CaseInsensitiveDict({
'listen_addresses': (None, _false_validator, 90100),
'port': (None, _false_validator, 90100),
'cluster_name': (None, _false_validator, 90500),
'wal_level': ('hot_standby', EnumValidator(('hot_standby', 'replica', 'logical')), 90100),
'hot_standby': ('on', _bool_is_true_validator, 90100),
'max_connections': (100, IntValidator(min=25), 90100),
'max_wal_senders': (10, IntValidator(min=3), 90100),
'wal_keep_segments': (8, IntValidator(min=1), 90100),
'wal_keep_size': ('128MB', IntValidator(min=16, base_unit='MB'), 130000),
'max_prepared_transactions': (0, IntValidator(min=0), 90100),
'max_locks_per_transaction': (64, IntValidator(min=32), 90100),
'track_commit_timestamp': ('off', _bool_validator, 90500),
'max_replication_slots': (10, IntValidator(min=4), 90400),
'max_worker_processes': (8, IntValidator(min=2), 90400),
'wal_log_hints': ('on', _bool_validator, 90400)
})
# 初始化 _RECOVERY_PARAMETERS 集合,包含所有恢复参数的键名
_RECOVERY_PARAMETERS = CaseInsensitiveSet(recovery_parameters.keys())
这个构造函数的作用是初始化一个 ConfigHandler
实例,并根据提供的 postgresql
和 config
参数设置各种配置文件路径和其他相关变量。具体来说:
- 存储 PostgreSQL 实例引用:构造函数接收一个指向
Postgresql
对象的引用,并存储在实例变量_postgresql
中。 - 配置文件路径:根据提供的配置或默认路径设置各种配置文件的路径。
- 初始化变量:初始化多个变量,包括配置文件的修改时间戳等。
- 检查 pgpass 文件:确保
pgpass
文件存在且为文件类型,否则抛出异常。 - 加载配置:调用
reload_config
方法来加载提供的配置信息。
1.12.2.5.1 __init__()
- 定义了一个构造函数
__init__
,该构造函数接受两个参数:postgresql
类型为Postgresql
,config
类型为Dict[str, Any]
,构造函数返回None
。
def __init__(self, postgresql: 'Postgresql', config: Dict[str, Any]) -> None:
# 将 postgresql 参数赋值给实例变量 _postgresql
self._postgresql = postgresql
# 获取配置目录路径
self._config_dir = os.path.abspath(config.get('config_dir', '') or postgresql.data_dir)
# 获取配置文件基础名称
config_base_name = config.get('config_base_name', 'postgresql')
# 构建 PostgreSQL 配置文件的完整路径
self._postgresql_conf = os.path.join(self._config_dir, config_base_name + '.conf')
# 初始化 _postgresql_conf_mtime 为 None,表示尚未记录 PostgreSQL 配置文件的修改时间
self._postgresql_conf_mtime = None
# 构建 PostgreSQL 基础配置文件的完整路径
self._postgresql_base_conf_name = config_base_name + '.base.conf'
self._postgresql_base_conf = os.path.join(self._config_dir, self._postgresql_base_conf_name)
# 分别为 pg_hba.conf 和 pg_ident.conf 文件构建完整路径
self._pg_hba_conf = os.path.join(self._config_dir, 'pg_hba.conf')
self._pg_ident_conf = os.path.join(self._config_dir, 'pg_ident.conf')
# 构建 recovery.conf 文件的完整路径
self._recovery_conf = os.path.join(postgresql.data_dir, 'recovery.conf')
self._recovery_conf_mtime = None
# 分别为 recovery.signal 和 standby.signal 文件构建完整路径
self._recovery_signal = os.path.join(postgresql.data_dir, 'recovery.signal')
self._standby_signal = os.path.join(postgresql.data_dir, 'standby.signal')
# 构建 postgresql.auto.conf 文件的完整路径
self._auto_conf = os.path.join(postgresql.data_dir, 'postgresql.auto.conf')
self._auto_conf_mtime = None
# 获取 pgpass 文件的绝对路径
self._pgpass = os.path.abspath(config.get('pgpass') or os.path.join(os.path.expanduser('~'), 'pgpass'))
# 检查 _pgpass 文件是否存在且为文件
if os.path.exists(self._pgpass) and not os.path.isfile(self._pgpass):
raise PatroniFatalException("'{0}' exists and it's not a file, check your `postgresql.pgpass` configuration"
.format(self._pgpass))
# 初始化 _passfile、_passfile_mtime 和 _postmaster_ctime 变量为 None
self._passfile = None
self._passfile_mtime = None
self._postmaster_ctime = None
# 始化 _current_recovery_params 为 None,_config 为一个空字典,_recovery_params 和 _server_parameters 为 CaseInsensitiveDict 实例
self._current_recovery_params: Optional[CaseInsensitiveDict] = None
self._config = {}
self._recovery_params = CaseInsensitiveDict()
self._server_parameters: CaseInsensitiveDict = CaseInsensitiveDict()
# 调用 reload_config 方法来重新加载配置
self.reload_config(config)
作用:
这个 __init__
函数的作用是初始化一个 ConfigHandler
类的实例,并设置与 PostgreSQL 数据库相关的各种配置文件路径以及其他重要的变量。具体来说,它具有以下几个主要作用:
- 存储 PostgreSQL 实例引用:接收一个指向
Postgresql
对象的引用,并存储在实例变量_postgresql
中。这允许ConfigHandler
在后续操作中与 PostgreSQL 实例进行交互。 - 配置文件路径设置:根据提供的配置或默认路径,设置多种与 PostgreSQL 配置相关的文件路径,如
postgresql.conf
、pg_hba.conf
、pg_ident.conf
、recovery.conf
等。 - 初始化时间戳变量:为多个配置文件初始化时间戳变量(如
_postgresql_conf_mtime
、_recovery_conf_mtime
、_auto_conf_mtime
),这些变量用于追踪配置文件的最后修改时间。 - 检查
pgpass
文件:确保pgpass
文件存在且为文件类型,如果不满足条件,则抛出异常。这有助于确保后续使用pgpass
文件进行认证的安全性和有效性。 - 初始化其他变量:初始化一些其他的变量,如
_passfile
、_passfile_mtime
、_postmaster_ctime
等,这些变量用于存储额外的信息或状态。 - 加载配置信息:最后,调用
reload_config
方法来加载提供的配置信息,确保ConfigHandler
实例的状态与提供的配置相一致。
1.12.2.5.2 reload_config()
- 定义了一个名为
reload_config
的方法,该方法有两个参数:config
类型为Dict[str, Any]
,sighup
类型为布尔值,默认为False
。方法没有返回值。
def reload_config(self, config: Dict[str, Any], sighup: bool = False) -> None:
self._superuser = config['authentication'].get('superuser', {})
# 调用 get_server_parameters 方法获取服务器参数
server_parameters = self.get_server_parameters(config)
params_skip_changes = CaseInsensitiveSet((*self._RECOVERY_PARAMETERS, 'hot_standby'))
conf_changed = hba_changed = ident_changed = local_connection_address_changed = False
param_diff = CaseInsensitiveDict()
# 如果 PostgreSQL 当前处于运行状态,那么创建一个字典 changes,包含 server_parameters 中除了 params_skip_changes 集合外的所有项
if self._postgresql.state == 'running':
changes = CaseInsensitiveDict({p: v for p, v in server_parameters.items()
if p not in params_skip_changes})
# 更新 changes 字典,添加 server_parameters 中不存在或在 params_skip_changes 中的键,并将它们的值设为 None
changes.update({p: None for p in self._server_parameters.keys()
if not (p in changes or p in params_skip_changes)})
# 如果 changes 字典不为空,并且包含 'wal_buffers' 键,计算默认值所需的参数并添加到 changes 中
if changes:
undef = []
if 'wal_buffers' in changes: # we need to calculate the default value of wal_buffers
undef = [p for p in ('shared_buffers', 'wal_segment_size', 'wal_block_size') if p not in changes]
changes.update({p: None for p in undef})
# XXX: query can raise an exception
# 获取 changes 中参数的旧值,并存储在 old_values 中
old_values = self._get_pg_settings(changes.keys())
# 处理 wal_buffers 的特殊逻辑,并从 changes 中删除不需要的键
if 'wal_buffers' in changes:
self._handle_wal_buffers(old_values, changes)
for p in undef:
del changes[p]
# 遍历 old_values 的值,检查是否有变化,并根据变化情况更新配置文件标志
for r in old_values.values():
if r[4] != 'internal' and r[0] in changes:
new_value = changes.pop(r[0])
if new_value is None or not compare_values(r[3], r[2], r[1], new_value):
conf_changed = True
if r[4] == 'postmaster':
param_diff[r[0]] = get_param_diff(r[1], new_value, r[3], r[2])
logger.info("Changed %s from '%s' to '%s' (restart might be required)",
r[0], param_diff[r[0]]['old_value'], new_value)
if config.get('use_unix_socket') and r[0] == 'unix_socket_directories'\
or r[0] in ('listen_addresses', 'port'):
local_connection_address_changed = True
else:
logger.info("Changed %s from '%s' to '%s'",
r[0], maybe_convert_from_base_unit(r[1], r[3], r[2]), new_value)
elif r[0] in self._server_parameters \
and not compare_values(r[3], r[2], r[1], self._server_parameters[r[0]]):
# Check if any parameter was set back to the current pg_settings value
# We can use pg_settings value here, as it is proved to be equal to new_value
logger.info("Changed %s from '%s' to '%s'", r[0], self._server_parameters[r[0]], new_value)
conf_changed = True
# 检查用户定义的参数是否有变化,并根据变化情况更新配置文件标志
for param, value in changes.items():
if '.' in param:
# 检查用户定义的参数是否已更改(名称中带有句点的参数)
if value is None or param not in self._server_parameters \
or str(value) != str(self._server_parameters[param]):
logger.info("Changed %s from '%s' to '%s'",
param, self._server_parameters.get(param), value)
conf_changed = True
elif param in server_parameters:
logger.warning('Removing invalid parameter `%s` from postgresql.parameters', param)
server_parameters.pop(param)
# 检查 pg_hba 配置是否有变化,并更新 hba_changed 标志
if (not server_parameters.get('hba_file') or server_parameters['hba_file'] == self._pg_hba_conf) \
and config.get('pg_hba'):
hba_changed = self._config.get('pg_hba', []) != config['pg_hba']
# 检查 pg_ident 配置是否有变化,并更新 ident_changed 标志
if (not server_parameters.get('ident_file') or server_parameters['ident_file'] == self._pg_hba_conf) \
and config.get('pg_ident'):
ident_changed = self._config.get('pg_ident', []) != config['pg_ident']
# 更新 _config 和 _server_parameters 为新的配置
self._config = config
self._server_parameters = server_parameters
# 调整恢复参数
self._adjust_recovery_parameters()
# 设置 Kerberos 服务名
self._krbsrvname = config.get('krbsrvname')
# for not so obvious connection attempts that may happen outside of pyscopg2
# 设置环境变量 PGKRBSRVNAME 以便支持外部连接尝试
if self._krbsrvname:
os.environ['PGKRBSRVNAME'] = self._krbsrvname
# 如果本地连接地址没有改变,则解析连接地址
if not local_connection_address_changed:
self.resolve_connection_addresses()
# 设置代理地址
proxy_addr = config.get('proxy_address')
self._postgresql.proxy_url = uri('postgres', proxy_addr, self._postgresql.database) if proxy_addr else None
# 如果配置文件发生变化,则写入 PostgreSQL 配置文件
if conf_changed:
self.write_postgresql_conf()
# 如果 pg_hba 文件发生变化,则替换 pg_hba 文件
if hba_changed:
self.replace_pg_hba()
# 如果 pg_ident 文件发生变化,则替换 pg_ident 文件
if ident_changed:
self.replace_pg_ident()
# 如果需要重载配置,则执行重载,并检查是否有外部修改的配置
if sighup or conf_changed or hba_changed or ident_changed:
logger.info('Reloading PostgreSQL configuration.')
self._postgresql.reload()
if self._postgresql.major_version >= 90500:
time.sleep(1)
try:
settings_diff: CaseInsensitiveDict = CaseInsensitiveDict()
for param, value, unit, vartype in self._postgresql.query(
'SELECT name, pg_catalog.current_setting(name), unit, vartype FROM pg_catalog.pg_settings'
' WHERE pg_catalog.lower(name) != ALL(%s) AND pending_restart',
[n.lower() for n in params_skip_changes]):
new_value = self._postgresql.get_guc_value(param)
new_value = '?' if new_value is None else new_value
settings_diff[param] = get_param_diff(value, new_value, vartype, unit)
external_change = {param: value for param, value in settings_diff.items()
if param not in param_diff or value != param_diff[param]}
if external_change:
logger.info("PostgreSQL configuration parameters requiring restart"
" (%s) seem to be changed bypassing Patroni config."
" Setting 'Pending restart' flag", ', '.join(external_change))
param_diff = settings_diff
except Exception as e:
logger.warning('Exception %r when running query', e)
else:
logger.info('No PostgreSQL configuration items changed, nothing to reload.')
# 设置 PostgreSQL 是否需要重启的原因
self._postgresql.set_pending_restart_reason(param_diff)
作用:
reload_config
方法的主要作用是根据传入的新配置信息 config
更新现有的配置,并根据需要重载 PostgreSQL 的配置。具体来说:
- 参数比较:比较当前配置与新配置之间的差异,确定哪些配置项需要更改。
- 配置更新:如果检测到配置文件需要更新,比如
postgresql.conf
、pg_hba.conf
或pg_ident.conf
,则更新相应的文件。 - 重载配置:如果配置文件已经更新,或者接收到信号
sighup
,则重载 PostgreSQL 的配置。 - 检查外部修改:在重载配置之后,检查是否有外部程序修改了 PostgreSQL 的配置,如果有的话,记录下来。
- 设置重启原因:根据配置的变化情况,设置 PostgreSQL 需要重启的原因。
通过这种方式,reload_config
方法可以确保 PostgreSQL 的配置始终保持最新,并且可以根据需要动态地调整配置。这有助于在不停机的情况下更新配置,提高系统的可用性和灵活性。
1.12.2.5.3 check_directories()
- 定义了一个名为
check_directories
的方法,该方法没有参数也没有返回值。
def check_directories(self) -> None:
# 检查 self._server_parameters 字典中是否存在 "unix_socket_directories" 键
if "unix_socket_directories" in self._server_parameters:
for d in self._server_parameters["unix_socket_directories"].split(","):
self.try_to_create_dir(d.strip(), "'{}' is defined in unix_socket_directories, {}")
# 检查 self._server_parameters 字典中是否存在 "stats_temp_directory" 键
if "stats_temp_directory" in self._server_parameters:
self.try_to_create_dir(self._server_parameters["stats_temp_directory"],
"'{}' is defined in stats_temp_directory, {}")
if not self._krbsrvname:
self.try_to_create_dir(os.path.dirname(self._pgpass),
"'{}' is defined in `postgresql.pgpass`, {}")
作用:
check_directories
方法的作用是在 PostgreSQL 启动之前检查和准备必要的目录结构。具体来说:
- Unix Socket 目录:根据
server_parameters
中unix_socket_directories
的设置,确保 Unix socket 文件能够放置在指定的目录下。这对于通过 Unix socket 连接到 PostgreSQL 数据库非常重要。 - 统计临时目录:如果
server_parameters
中设置了stats_temp_directory
,则确保此目录存在。这是为了 PostgreSQL 能够在其内部进行某些统计工作时使用临时文件。 - Pgpass 文件目录:如果设置了
krbsrvname
(通常用于 Kerberos 认证),则不需要处理pgpass
文件。否则,尝试创建pgpass
文件所在的目录。这是因为pgpass
文件通常用来存储认证信息,以简化连接过程。
1.12.2.5.4 load_current_server_parameters()
- 定义了一个名为
load_current_server_parameters
的方法,它没有参数,并且没有返回值。
def load_current_server_parameters(self) -> None:
"""当 Patroni 加入一个已经在运行的 PostgreSQL 实例时,从 `pg_settings` 中读取 GUC(运行时配置变量)的值。"""
# 创建一个列表 exclude
exclude = [name.lower() for name, value in self.CMDLINE_OPTIONS.items() if value[1] == _false_validator]
# 创建一个字典 keep_values
keep_values = {k: self._server_parameters[k] for k in exclude}
# 从 PostgreSQL 的 pg_settings 表中查询当前配置变量的值
server_parameters = CaseInsensitiveDict({r[0]: r[1] for r in self._postgresql.query(
"SELECT name, pg_catalog.current_setting(name) FROM pg_catalog.pg_settings"
" WHERE (source IN ('command line', 'environment variable') OR sourcefile = %s"
" OR pg_catalog.lower(name) = ANY(%s)) AND pg_catalog.lower(name) != ALL(%s)",
self._postgresql_conf, [n.lower() for n in self.CMDLINE_OPTIONS.keys()], exclude)})
# 从 server_parameters 中提取出所有属于恢复相关的配置项
recovery_params = CaseInsensitiveDict({k: server_parameters.pop(k) for k in self._RECOVERY_PARAMETERS
if k in server_parameters})
# 我们还想加载恢复参数的当前设置,包括primary_conninfo
# 和primary_slot_name,否则patronictl restart将更新postgresql.conf
# 并删除它们,在最坏的情况下会导致重新启动。
# 我们只在PostgresSQL v12以后做这个,因为旧版本还有recovery.conf
if not self._postgresql.is_primary() and self._postgresql.major_version >= 120000:
# Primary_conninfo预计是一个字典,因此我们需要解析它
recovery_params['primary_conninfo'] = parse_dsn(recovery_params.pop('primary_conninfo', '')) or {}
self._recovery_params = recovery_params
self._server_parameters = CaseInsensitiveDict({**server_parameters, **keep_values})
作用:
load_current_server_parameters
方法的作用是从 PostgreSQL 的 pg_settings
表中读取当前配置变量的值,并更新内部状态,以便在 Patroni 加入一个已经在运行的 PostgreSQL 实例时,可以获取最新的配置信息。具体来说:
- 排除不需要加载的配置项:
- 创建一个
exclude
列表,包含所有由_false_validator
验证的配置项名称的小写形式。
- 创建一个
- 保存需要保留的配置项值:
- 创建一个
keep_values
字典,包含exclude
列表中所有配置项的当前值。
- 创建一个
- 查询当前配置变量的值:
- 从
pg_settings
表中查询当前配置变量的值,并将结果存储到CaseInsensitiveDict
中。
- 从
- 提取恢复相关的配置项:
- 从
server_parameters
中提取出所有属于恢复相关的配置项,并存储到recovery_params
中。
- 从
- 处理恢复参数:
- 如果 PostgreSQL 版本为 12 及以上,并且当前实例不是主节点,则解析
primary_conninfo
为字典,并更新recovery_params
。
- 如果 PostgreSQL 版本为 12 及以上,并且当前实例不是主节点,则解析
- 更新配置信息:
- 最终更新
_server_parameters
为server_parameters
和keep_values
的组合。
- 最终更新
通过这种方式,load_current_server_parameters
方法确保了在 Patroni 加入一个已经在运行的 PostgreSQL 实例时,能够获取并保存最新的配置信息,这对于确保集群的一致性和正确性非常重要。
1.12.2.5.5 try_to_create_dir()
- 定义了一个名为
try_to_create_dir
的方法,该方法有两个参数:d
类型为str
,表示目录路径;msg
类型也为str
,表示一个格式化的字符串,用于构造输出信息。方法没有返回值。
def try_to_create_dir(self, d: str, msg: str) -> None:
# 将输入的目录路径 d 与 PostgreSQL 的数据目录路径 self._postgresql.data_dir 进行拼接,得到完整的目录路径
d = os.path.join(self._postgresql.data_dir, d)
# 检查拼接后的目录路径 d 是否是 PostgreSQL 数据目录的子路径,并且 PostgreSQL 数据目录是否为空
if (not is_subpath(self._postgresql.data_dir, d) or not self._postgresql.data_directory_empty()):
validate_directory(d, msg)
作用:
try_to_create_dir
方法的作用是在 PostgreSQL 数据目录内创建指定的目录,同时确保目录位于数据目录的子路径中,并且数据目录是空的。具体来说:
- 路径拼接:首先,该方法会将输入的相对路径与 PostgreSQL 数据目录路径拼接起来,形成绝对路径。
- 路径检查:然后,检查拼接后的路径是否位于 PostgreSQL 数据目录的子目录中,确保不会创建数据目录之外的路径。
- 目录状态检查:接着,检查 PostgreSQL 数据目录是否为空,这可能是为了防止在非初始状态下(例如,已经有数据存在时)创建目录,因为创建目录的操作可能只应在初始化阶段进行。
- 目录验证与创建:如果路径不在数据目录内或数据目录不为空,则调用
validate_directory
方法来验证和创建目录,并打印相应的消息。
1.12.2.5.6 replace_pg_hba()
- 定义了一个名为
replace_pg_hba
的方法,它没有参数,并返回一个可选的布尔值(Optional[bool]
),即可能返回True
、False
或None
。
def replace_pg_hba(self) -> Optional[bool]:
"""
如果配置文件中没有定义hba_file,请替换PGDATA中的pg_hba.conf content
postgresql。参数'和pg_hba是在' postgresql '配置部分定义的。
:返回:如果重写了pg_hba.conf,则返回True。
"""
# 当我们正在进行自定义引导时,我们假设不知道超级用户的密码,
# 为了能够更改它,我们需要从某个地址开放信任访问。
# 如果正在进行自定义引导
if self._postgresql.bootstrap.running_custom_bootstrap:
# 初始化 addresses 字典
# 根据操作系统名称设置 addresses 字典
addresses = {} if os.name == 'nt' else {'': 'local'} # windows doesn't yet support unix-domain sockets
# 如果 local_replication_address 字典中有 host 键并且该主机地址不是以 / 开头,则使用 socket.getaddrinfo 获取所有地址信息,并更新 addresses 字典,使得每个 IP 地址对应 'host' 类型的条目。
if 'host' in self.local_replication_address and not self.local_replication_address['host'].startswith('/'):
addresses.update({sa[0] + '/32': 'host' for _, _, _, _, sa in socket.getaddrinfo(
self.local_replication_address['host'], self.local_replication_address['port'],
0, socket.SOCK_STREAM, socket.IPPROTO_TCP)})
# 使用上下文管理器 self.config_writer(self._pg_hba_conf) 打开 pg_hba.conf 文件,并为每个地址写入两条信任访问规则:一条针对复制用户,另一条针对超级用户或所有用户
with self.config_writer(self._pg_hba_conf) as f:
for address, t in addresses.items():
f.writeline((
'{0}\treplication\t{1}\t{3}\ttrust\n'
'{0}\tall\t{2}\t{3}\ttrust'
).format(t, self.replication['username'], self._superuser.get('username') or 'all', address))
# 如果 hba_file 没有定义,并且配置中有 pg_hba 定义,则用配置中的内容覆盖 pg_hba.conf 文件,并返回 True
# 如果 hba_file 未定义并且在 postgresql 配置部分定义了 pg_hba,则使用上下文管理器 self.config_writer(self._pg_hba_conf) 打开 pg_hba.conf 文件,并写入 self._config['pg_hba'] 中的内容
elif not self.hba_file and self._config.get('pg_hba'):
with self.config_writer(self._pg_hba_conf) as f:
f.writelines(self._config['pg_hba'])
return True
作用:
replace_pg_hba
方法的具体作用是在满足一定条件下替换 PostgreSQL 的 pg_hba.conf
文件的内容。具体来说:
- 自定义引导时的处理:
- 如果正在进行自定义引导,并且本地复制地址配置存在且不是以
/
开头,则根据本地复制地址生成信任访问规则,并写入pg_hba.conf
文件。 - 这样做是为了在引导期间允许从特定地址的信任连接,以便可以更改超级用户的密码。
- 如果正在进行自定义引导,并且本地复制地址配置存在且不是以
- 常规情况下的处理:
- 如果
hba_file
未定义并且配置中有pg_hba
内容,则用self._config['pg_hba']
中的内容替换pg_hba.conf
文件的内容。 - 这样做是为了在
hba_file
未定义的情况下,使用配置中提供的pg_hba
规则来替代默认的pg_hba.conf
文件。
- 如果
1.12.2.5.7 replace_pg_ident()
- 定义了一个名为
replace_pg_ident
的方法,它没有参数,并且返回一个布尔值或者None
。
def replace_pg_ident(self) -> Optional[bool]:
"""
如果文件中没有定义ident_file,则替换PGDATA中的pg_ident.conf内容
'postgresql.parameter'和pg_ident是在' postgresql '部分定义的。
:返回:如果pg_ident.conf被重写,则返回True。
"""
# 使用上下文管理器打开 pg_ident.conf 文件
if not self.ident_file and self._config.get('pg_ident'):
with self.config_writer(self._pg_ident_conf) as f:
f.writelines(self._config['pg_ident'])
return True
作用:
replace_pg_ident
方法的主要作用是根据一定的条件来重写 PostgreSQL 的 pg_ident.conf
文件。具体来说:
- 条件检查:
- 如果
ident_file
没有在postgresql.parameters
中定义,并且在postgresql
配置部分定义了pg_ident
,那么就会执行重写操作。
- 如果
- 重写文件:
- 使用配置中的
pg_ident
内容来重写pg_ident.conf
文件。
- 使用配置中的
- 返回值:
- 如果文件被重写,方法返回
True
,否则返回None
。
- 如果文件被重写,方法返回
1.12.2.5.8 ident_file()
- 定义了一个名为
ident_file
的属性(property),该属性没有显式的参数,并返回一个可选的字符串(Optional[str]
),即可能返回一个字符串或None
。
@property
def ident_file(self) -> Optional[str]:
# 从实例的 _server_parameters 字典中获取 ident_file 的值
ident_file = self._server_parameters.get('ident_file')
# 如果 ident_file 的值等于 self._pg_ident_conf,则返回 None;否则返回 ident_file 的值
return None if ident_file == self._pg_ident_conf else ident_file
作用:
ident_file
属性的主要作用是提供对 ident_file
参数的读取,并根据一定的条件返回适当的值。具体来说:
- 获取参数值:
- 从
_server_parameters
字典中读取ident_file
的配置值。
- 从
- 条件判断:
- 如果
ident_file
的值与self._pg_ident_conf
相等,则返回None
。 - 否则,返回
ident_file
的原始值。
- 如果
这个属性的设计目的是为了在某些情况下返回 None
,可能是为了避免使用默认的 pg_ident.conf
文件路径,或者是出于某种特定的逻辑需求。例如,在某些配置下,如果 ident_file
设置成了默认的 pg_ident.conf
文件路径,那么实际上可以视为没有指定 ident_file
,因此返回 None
来表示这种情况。
1.12.2.5.9 config_writer()
- 定义了一个名为
config_writer
的上下文管理器方法,它接受一个字符串类型的参数filename
,并返回一个Iterator[ConfigWriter]
类型的对象。
@contextmanager
def config_writer(self, filename: str) -> Iterator[ConfigWriter]:
"""创建 :class:`ConfigWriter` 对象并对 *filename* 设置权限。
:param filename: 配置文件的路径。
:yields: :class:`ConfigWriter` 对象。
"""
# 使用 with 语句创建一个 ConfigWriter 对象,并将其绑定到变量 writer 上
with ConfigWriter(filename) as writer:
yield writer
# 在 with 块结束后,调用 self.set_file_permissions 方法设置 filename 的权限
self.set_file_permissions(filename)
作用:
config_writer
方法的具体作用是创建一个 ConfigWriter
对象,并为指定的配置文件设置适当的权限。具体来说:
- 创建
ConfigWriter
对象:- 通过
with ConfigWriter(filename) as writer:
创建一个ConfigWriter
实例,该实例负责处理指定路径filename
的配置文件。
- 通过
- 上下文管理:
yield writer
使得可以在with
语句块中使用writer
对象来进行文件写入等操作。在这个上下文内部,任何使用writer
对象的操作都会受到ConfigWriter
类定义的行为的影响。
- 设置文件权限:
- 在
with
块结束之后,通过self.set_file_permissions(filename)
调用设置文件权限。这一步骤通常是必要的,以确保配置文件的安全性和正确性,例如设置文件的读写权限。
- 在
这个方法的设计使得可以安全地管理和修改配置文件,同时确保文件的权限得到适当的设置。通过使用上下文管理器的方式,可以确保即使在处理文件的过程中发生异常,也能正确地清理资源(例如关闭文件句柄)并设置权限。这样可以提高代码的健壮性和安全性。
1.12.2.5.10 hba_file()
- 定义了一个名为
hba_file
的属性(property),该属性没有显式的参数,并返回一个可选的字符串(Optional[str]
),即可能返回一个字符串或None
。
@property
def hba_file(self) -> Optional[str]:
# 从实例的 _server_parameters 字典中获取 hba_file 的值
hba_file = self._server_parameters.get('hba_file')
# 如果 hba_file 的值等于 self._pg_hba_conf,则返回 None;否则返回 hba_file 的值
return None if hba_file == self._pg_hba_conf else hba_file
作用:
hba_file
属性的具体作用是提供对 hba_file
参数的读取,并根据一定的条件返回适当的值。具体来说:
- 获取参数值:
- 从
_server_parameters
字典中读取hba_file
的配置值。
- 从
- 条件判断:
- 如果
hba_file
的值与self._pg_hba_conf
相等,则返回None
。 - 否则,返回
hba_file
的原始值。
- 如果
这个属性的设计目的是为了在某些情况下返回 None
,可能是为了避免使用默认的 pg_hba.conf
文件路径,或者是出于某种特定的逻辑需求。例如,在某些配置下,如果 hba_file
设置成了默认的 pg_hba.conf
文件路径,那么实际上可以视为没有指定 hba_file
,因此返回 None
来表示这种情况。
1.12.2.5.11 set_file_permissions()
- 定义了一个名为 set_file_permissions 的方法,它接受一个字符串类型的参数 filename ,并返回 None 。
def set_file_permissions(self, filename: str) -> None:
"""设置文件 *filename* 的权限,如果该文件位于 PGDATA 目录下,则根据预期的权限进行设置。
.. note::
如果文件不在 PGDATA 下,则不做任何处理。
:param filename: 文件路径,该文件的权限可能需要调整。
"""
# 查 filename 是否是 self._postgresql.data_dir 的子路径
if is_subpath(self._postgresql.data_dir, filename):
# 设置数据目录下的权限
pg_perm.set_permissions_from_data_directory(self._postgresql.data_dir)
# 使用 os.chmod 方法来设置 filename 的权限
os.chmod(filename, pg_perm.file_create_mode)
作用:
set_file_permissions
方法的具体作用是根据 PostgreSQL 数据目录的要求来设置指定文件的权限。具体来说:
- 检查文件路径:
- 首先检查
filename
是否是 PostgreSQL 数据目录 (self._postgresql.data_dir
) 的子路径。
- 首先检查
- 设置目录权限:
- 如果
filename
位于数据目录内,调用pg_perm.set_permissions_from_data_directory
方法来设置数据目录内的权限。这通常涉及到设置目录的属主、属组以及权限位,以确保数据库文件的安全性。
- 如果
- 设置文件权限:
- 使用
os.chmod
方法来设置filename
的权限,确保文件具有正确的权限位,这些权限位通常是由pg_perm.file_create_mode
指定的标准模式。
- 使用
这个方法的设计目的是为了确保 PostgreSQL 数据目录内的文件具有正确的权限设置,这对于数据库的安全性至关重要。只有当文件位于 PostgreSQL 的数据目录内时,才会进行权限设置,这样可以避免意外地修改其他位置的文件权限。
1.12.2.5.12 get_server_parameters()
- 定义了一个名为
get_server_parameters
的方法,它接受一个字典类型的参数config
,并返回一个CaseInsensitiveDict
类型的对象。
def get_server_parameters(self, config: Dict[str, Any]) -> CaseInsensitiveDict:
# 从 config 字典中获取 parameters 子项,并创建一个副本
parameters = config['parameters'].copy()
# 从 config 字典中获取 listen_addresses 和 port
listen_addresses, port = split_host_port(config['listen'], 5432)
# 更新 parameters 字典
parameters.update(cluster_name=self._postgresql.scope, listen_addresses=listen_addresses, port=str(port))
# 如果 global_config 中的同步模式被启用,则根据不同的条件更新或移除 synchronous_standby_names 的值
if global_config.is_synchronous_mode:
synchronous_standby_names = self._server_parameters.get('synchronous_standby_names')
if synchronous_standby_names is None:
if global_config.is_synchronous_mode_strict\
and self._postgresql.role in ('primary', 'promoted'):
parameters['synchronous_standby_names'] = '*'
else:
parameters.pop('synchronous_standby_names', None)
else:
parameters['synchronous_standby_names'] = synchronous_standby_names
# Handle hot_standby <-> replica rename
# 处理 hot_standby 和 replica 的重命名问题
if parameters.get('wal_level') == ('hot_standby' if self._postgresql.major_version >= 90600 else 'replica'):
parameters['wal_level'] = 'replica' if self._postgresql.major_version >= 90600 else 'hot_standby'
# 尝试重新计算wal_keep_segments <-> wal_keep_size,假设典型的wal_segment_size为16MB。
# 真实的段大小可以从pg_control中估计出来,但我们并不真正关心,因为唯一的目标是
# 这个练习提高了跨版本兼容性,用户必须在配置中设置正确的参数。
# 根据 PostgreSQL 版本的不同,尝试重新计算 wal_keep_segments 和 wal_keep_size 的关系
if self._postgresql.major_version >= 130000:
wal_keep_segments = parameters.pop('wal_keep_segments', self.CMDLINE_OPTIONS['wal_keep_segments'][0])
parameters.setdefault('wal_keep_size', str(int(wal_keep_segments) * 16) + 'MB')
elif self._postgresql.major_version:
wal_keep_size = parse_int(parameters.pop('wal_keep_size', self.CMDLINE_OPTIONS['wal_keep_size'][0]), 'MB')
parameters.setdefault('wal_keep_segments', int(((wal_keep_size or 0) + 8) / 16))
# 调用 mpp_handler 的 adjust_postgres_gucs 方法来调整 parameters
self._postgresql.mpp_handler.adjust_postgres_gucs(parameters)
# 创建一个新的 CaseInsensitiveDict,包含那些适用于当前 PostgreSQL 版本的参数
ret = CaseInsensitiveDict({k: v for k, v in parameters.items() if not self._postgresql.major_version
or self._postgresql.major_version >= self.CMDLINE_OPTIONS.get(k, (0, 1, 90100))[2]})
# 如果 ret 中包含 hba_file 或 ident_file 键,则将其值更新为 self._config_dir 下的相应路径
ret.update({k: os.path.join(self._config_dir, ret[k]) for k in ('hba_file', 'ident_file') if k in ret})
return ret
作用:
get_server_parameters
方法的具体作用是根据传入的配置 config
来生成适用于 PostgreSQL 服务的一组参数。具体来说:
- 初始化参数:
- 从
config
中提取parameters
并创建一个副本。
- 从
- 设置监听地址和端口:
- 解析
listen
配置,并设置listen_addresses
和port
。
- 解析
- 同步模式配置:
- 根据全局配置中的同步模式设置
synchronous_standby_names
。
- 根据全局配置中的同步模式设置
- 处理
wal_level
名称变更:- 根据 PostgreSQL 版本处理
hot_standby
和replica
名称变更。
- 根据 PostgreSQL 版本处理
- 调整
wal_keep_segments
和wal_keep_size
:- 根据版本差异调整这两个参数之间的转换。
- 调整 PostgreSQL GUCs:
- 调用
mpp_handler
的方法来进一步调整参数。
- 调用
- 筛选适用的参数:
- 创建一个
CaseInsensitiveDict
,包含那些适用于当前 PostgreSQL 版本的参数。
- 创建一个
- 设置配置文件路径:
- 如果参数中包含
hba_file
或ident_file
,则设置其绝对路径。
- 如果参数中包含
这个方法的设计目的是为了根据当前的配置和 PostgreSQL 的版本生成一组适合当前环境的参数集合。这样可以确保服务启动时使用的是正确的参数设置,避免由于版本差异导致的问题。
1.12.2.5.13 _get_pg_settings()
- 定义了一个名为
_get_pg_settings
的方法,它接受一个Collection[str]
类型的参数names
,并返回一个字典,字典的键和值类型可以是任意类型,但根据上下文推断,键应该是字符串类型,值是一个元组。
def _get_pg_settings(self, names: Collection[str]) -> Dict[Any, Tuple[Any, ...]]:
return {r[0]: r for r in self._postgresql.query(('SELECT name, setting, unit, vartype, context, sourcefile'
+ ' FROM pg_catalog.pg_settings '
+ ' WHERE pg_catalog.lower(name) = ANY(%s)'),
[n.lower() for n in names])}
作用:
_get_pg_settings
函数的具体作用是从 PostgreSQL 数据库中获取特定配置项的详细信息。具体来说:
- 参数处理:
- 输入参数
names
是一个字符串集合,表示需要查询的配置项名称。 - 将每个名称转换为小写形式,以进行不区分大小写的比较。
- 输入参数
- 执行数据库查询:
- 使用
self._postgresql.query
方法执行 SQL 查询,从pg_catalog.pg_settings
表中获取指定配置项的信息。 - SQL 查询语句选择了
name
,setting
,unit
,vartype
,context
,sourcefile
这六个字段,并通过WHERE
子句过滤出名称在给定列表中的记录。
- 使用
- 结果处理:
- 构造一个字典,其中键是查询结果中每条记录的第一个字段(配置项名称),值是整个查询结果的元组。
通过这个函数,可以方便地从 PostgreSQL 数据库中获取指定配置项的详细信息,包括配置项的名称、设置值、单位、变量类型、上下文以及源文件等信息。这对于需要动态查询数据库配置的应用程序非常有用。
1.12.2.5.14 _handle_wal_buffers()
- 定义了一个名为
_handle_wal_buffers
的静态方法,它接受两个参数:一个字典old_values
和一个CaseInsensitiveDict
类型的changes
,并返回None
。
@staticmethod
def _handle_wal_buffers(old_values: Dict[Any, Tuple[Any, ...]], changes: CaseInsensitiveDict) -> None:
# 从 old_values 中获取 wal_block_size 的设置值,并尝试将其转换为整数。如果没有成功转换,则默认值为 8192
wal_block_size = parse_int(old_values['wal_block_size'][1]) or 8192
# 从 old_values 中获取 wal_segment_size 的设置值
wal_segment_size = old_values['wal_segment_size']
# 从 wal_segment_size 的第三个元素(单位)中获取单位,并尝试将其转换为整数值
wal_segment_unit = parse_int(wal_segment_size[2], 'B') or 8192 \
if wal_segment_size[2] is not None and wal_segment_size[2][0].isdigit() else 1
# 从 wal_segment_size 的第二个元素(大小)中获取大小,并尝试将其转换为整数值
wal_segment_size = parse_int(wal_segment_size[1]) or (16777216 if wal_segment_size[2] is None else 2048)
# 计算调整后的 wal_segment_size
wal_segment_size *= wal_segment_unit / wal_block_size
# 计算默认的 wal_buffers 大小
default_wal_buffers = min(max((parse_int(old_values['shared_buffers'][1]) or 16384) / 32, 8), wal_segment_size)
# 从 old_values 中获取 wal_buffers 的设置值
wal_buffers = old_values['wal_buffers']
new_value = str(changes['wal_buffers'] or -1)
# 如果新的 wal_buffers 值为 -1,则将其设置为默认的 wal_buffers 值
new_value = default_wal_buffers if new_value == '-1' else parse_int(new_value, wal_buffers[2])
# 如果旧的 wal_buffers 值为 -1,则将其设置为默认的 wal_buffers 值
old_value = default_wal_buffers if wal_buffers[1] == '-1' else parse_int(*wal_buffers[1:3])
# 如果新的 wal_buffers 值等于旧的 wal_buffers 值,则从 changes 中删除 wal_buffers 键
if new_value == old_value:
del changes['wal_buffers']
作用:
_handle_wal_buffers
函数的具体作用是处理 wal_buffers
配置项的变化。具体来说:
- 获取相关配置项的值:
- 获取
wal_block_size
、wal_segment_size
和shared_buffers
的值。
- 获取
- 计算默认的
wal_buffers
大小:- 根据
shared_buffers
的大小计算默认的wal_buffers
大小。
- 根据
- 处理新的
wal_buffers
值:- 如果新的
wal_buffers
值为-1
,则使用默认的wal_buffers
大小。 - 否则,将新的
wal_buffers
值转换为整数。
- 如果新的
- 处理旧的
wal_buffers
值:- 如果旧的
wal_buffers
值为-1
,则使用默认的wal_buffers
大小。 - 否则,将旧的
wal_buffers
值转换为整数。
- 如果旧的
- 比较新旧值并删除未更改的项:
- 如果新的
wal_buffers
值与旧的wal_buffers
值相同,则从changes
字典中删除wal_buffers
键。
- 如果新的
通过这个函数,可以确保在更改 wal_buffers
时,不会因为新旧值相同而导致不必要的配置更新,从而优化了配置处理过程。
1.12.2.5.15 _adjust_recovery_parameters()
- 定义了一个名为
_adjust_recovery_parameters
的方法,它是一个实例方法,接受一个self
参数,并且返回类型为None
。
def _adjust_recovery_parameters(self) -> None:
# 这不是严格必要的,但可以使 Patroni 的配置与所有版本的 PostgreSQL 兼容。
# 从 self._server_parameters 中筛选出与恢复相关的参数,并将它们放入一个新的字典 recovery_conf 中
recovery_conf = {n: v for n, v in self._server_parameters.items() if n.lower() in self._RECOVERY_PARAMETERS}
# 如果 recovery_conf 不为空(即存在恢复相关的参数),则将其添加到 self._config 的 recovery_conf 键下
if recovery_conf:
self._config['recovery_conf'] = recovery_conf
# 查 self 是否已经有一个 recovery_conf 的键存在
if self.get('recovery_conf'):
# 从 self._config['recovery_conf'] 中移除名为 _triggerfile_wrong_name 的参数(如果存在),并将它的值赋给 value 变量
value = self._config['recovery_conf'].pop(self._triggerfile_wrong_name, None)
# 如果 _config['recovery_conf'] 中不存在名为 self.triggerfile_good_name 的参数,并且 value 不为空,则将 value 添加到 _config['recovery_conf'] 中,并使用 self.triggerfile_good_name 作为键名
if self.triggerfile_good_name not in self._config['recovery_conf'] and value:
self._config['recovery_conf'][self.triggerfile_good_name] = value
作用:
_adjust_recovery_parameters
方法的具体作用是对恢复相关的参数进行处理,以确保 Patroni 的配置能够兼容不同版本的 PostgreSQL,并且能够正确地处理触发文件(trigger file)的命名问题。具体来说:
- 筛选恢复参数:
- 从服务器参数中筛选出所有与恢复相关的参数,并创建一个新的字典
recovery_conf
。
- 从服务器参数中筛选出所有与恢复相关的参数,并创建一个新的字典
- 存储恢复参数:
- 如果存在恢复相关的参数,则将它们存储到
self._config
的recovery_conf
键下。
- 如果存在恢复相关的参数,则将它们存储到
- 处理触发文件命名:
- 如果发现使用了错误的触发文件名称
_triggerfile_wrong_name
,则将其替换为正确的名称self.triggerfile_good_name
。
- 如果发现使用了错误的触发文件名称
1.12.2.5.16 resolve_connection_addresses()
- 定义了一个名为
resolve_connection_addresses
的方法,它是一个实例方法,接受一个self
参数,并且返回类型为None
。
def resolve_connection_addresses(self) -> None:
"""计算并设置本地和远程连接的 URL 和选项。
该方法设置了以下属性:
* :attr:`Postgresql.connection_string <patroni.postgresql.Postgresql.connection_string>` 属性,稍后会将该属性写入 DCS 中的成员键作为 ``conn_url``。
* :attr:`ConfigHandler.local_replication_address` 属性,用于建立到本地 PostgreSQL 的复制连接。
* :attr:`ConnectionPool.conn_kwargs <patroni.postgresql.connection.ConnectionPool.conn_kwargs>` 属性,用于建立到本地 PostgreSQL 的超级用户连接。
.. note::
如果 Patroni 配置中的 ``postgresql.parameters.unix_socket_directories`` 中存在有效的目录,并且 ``postgresql.use_unix_socket`` 和/或 ``postgresql.use_unix_socket_repl`` 设置为 ``True``,我们将分别使用 Unix 套接字建立超级用户和复制连接到本地 PostgreSQL。
如果需要使用 Unix 套接字,但在 ``postgresql.parameters.unix_socket_directories`` 中没有设置任何值,我们在连接参数中省略 ``host``,依赖于 ``libpq`` 能够通过某个默认的 Unix 套接字目录连接。
如果没有请求使用 Unix 套接字,我们“切换”到 TCP,如果可以推断出 PostgreSQL 正监听本地接口地址,则优先使用 ``localhost``。
否则,我们只是使用在 ``listen_addresses`` GUC 中指定的第一个地址。
"""
# 获取 PostgreSQL 的端口号,并确定本地 TCP 地址
port = self._server_parameters['port']
tcp_local_address = self._get_tcp_local_address()
# 据配置中的 connect_address 或者默认的 TCP 地址来构建网络位置信息 netloc
netloc = self._config.get('connect_address') or tcp_local_address + ':' + port
# 初始化 unix_local_address 字典,包含端口号
unix_local_address = {'port': port}
unix_socket_directories = self._server_parameters.get('unix_socket_directories')
# 如果配置中有 unix_socket_directories,则尝试获取适合的 Unix 套接字地址,如果没有找到合适的地址,则回退使用 TCP 地址
if unix_socket_directories is not None:
# 如果设置了unix_socket_directories,则退回到TCP,但没有合适的值
unix_local_address['host'] = self._get_unix_local_address(unix_socket_directories) or tcp_local_address
# 构建包含主机名和端口号的 TCP 地址字典
tcp_local_address = {'host': tcp_local_address, 'port': port}
# 根据配置决定是否使用 Unix 套接字地址还是 TCP 地址作为本地复制地址
self.local_replication_address = unix_local_address\
if self._config.get('use_unix_socket_repl') else tcp_local_address
# 设置 connection_string 属性,用于建立数据库连接
self._postgresql.connection_string = uri('postgres', netloc, self._postgresql.database)
# 根据配置决定是否使用 Unix 套接字地址还是 TCP 地址作为本地地址
local_address = unix_local_address if self._config.get('use_unix_socket') else tcp_local_address
# 构建本地连接的参数字典
local_conn_kwargs = {
**local_address,
**self._superuser,
'dbname': self._postgresql.database,
'fallback_application_name': 'Patroni',
'connect_timeout': 3,
'options': '-c statement_timeout=2000'
}
# 如果连接参数中存在 username,则将其替换为 user,因为 PostgreSQL 使用 user 参数
if 'username' in local_conn_kwargs:
local_conn_kwargs['user'] = local_conn_kwargs.pop('username')
# 通知connection_pool新的本地连接地址
# 更新连接池中的连接参数,使其使用新的本地连接地址
self._postgresql.connection_pool.conn_kwargs = local_conn_kwargs
作用:
resolve_connection_addresses
方法的具体作用是根据 Patroni 的配置动态计算并设置 PostgreSQL 数据库连接所需的各项参数,包括连接字符串、本地复制地址以及超级用户的连接参数。具体来说:
- 确定连接方式:
- 根据配置判断是否使用 Unix 套接字连接或 TCP 连接。
- 设置连接字符串:
- 计算并设置用于建立数据库连接的字符串。
- 设置本地复制地址:
- 根据配置选择合适的本地复制地址。
- 设置超级用户连接参数:
- 构建超级用户的连接参数,并通知连接池更新连接参数。
1.12.2.5.17 _get_tcp_local_address()
- 定义了一个名为
_get_tcp_local_address
的方法,它是一个实例方法,接受一个self
参数,并且返回类型为str
。
def _get_tcp_local_address(self) -> str:
# 从 self._server_parameters 字典中获取 listen_addresses 参数,并按逗号分割成列表
listen_addresses = self._server_parameters['listen_addresses'].split(',')
for la in listen_addresses:
if la.strip().lower() in ('*', '0.0.0.0', '127.0.0.1', 'localhost'): # we are listening on '*' or localhost
return 'localhost' # connection via localhost is preferred
return listen_addresses[0].strip() # can't use localhost, take first address from listen_addresses
作用:
_get_tcp_local_address
方法的具体作用是从 PostgreSQL 的 listen_addresses
参数中找出最适合用于本地连接的地址。具体来说:
- 解析监听地址:
- 从 PostgreSQL 的配置参数
listen_addresses
中获取监听地址列表。
- 从 PostgreSQL 的配置参数
- 查找本地地址:
- 检查监听地址列表中是否存在通配符
'*'
或本地地址'0.0.0.0'
、'127.0.0.1'
、'localhost'
。
- 检查监听地址列表中是否存在通配符
- 优先使用本地地址:
- 如果存在上述地址之一,则优先返回
'localhost'
作为本地连接地址。
- 如果存在上述地址之一,则优先返回
- 使用首个监听地址:
- 如果没有找到上述地址,则返回
listen_addresses
列表中的第一个地址作为本地连接地址。
- 如果没有找到上述地址,则返回
1.12.2.5.18 _get_unix_local_address()
- 定义了一个名为
_get_unix_local_address
的静态方法,它接受一个字符串参数unix_socket_directories
,并返回一个字符串。
@staticmethod
def _get_unix_local_address(unix_socket_directories: str) -> str:
# 将传入的 unix_socket_directories 字符串按照逗号分割成多个目录,并遍历这些目录
for d in unix_socket_directories.split(','):
d = d.strip()
if d.startswith('/'): # 只有绝对路径可以用来通过 Unix 套接字连接。
return d
return ''
作用:
_get_unix_local_address
函数的具体作用是从 PostgreSQL 的 unix_socket_directories
参数中找出适合用于 Unix 套接字连接的绝对路径目录。具体来说:
- 解析 Unix 套接字目录:
- 接受一个包含多个 Unix 套接字目录的字符串,并按逗号分割成多个目录。
- 查找绝对路径目录:
- 遍历分割后的目录列表,检查是否有绝对路径目录。
- 返回适合的目录:
- 如果找到适合的绝对路径目录,则返回该目录;如果没有找到,则返回空字符串。
1.12.2.5.19 write_postgresql_conf()
- 定义了一个名为
write_postgresql_conf
的方法,它接受一个可选的CaseInsensitiveDict
类型的参数configuration
,默认为None
,返回类型为None
。
def write_postgresql_conf(self, configuration: Optional[CaseInsensitiveDict] = None) -> None:
# 如果当前配置中没有 custom_conf 项,并且基本配置文件 _postgresql_base_conf 不存在,则重命名原始配置文件 _postgresql_conf 到 _postgresql_base_conf
if 'custom_conf' not in self._config and not os.path.exists(self._postgresql_base_conf):
os.rename(self._postgresql_conf, self._postgresql_base_conf)
# 如果 configuration 参数未提供,则使用 _server_parameters 的副本
configuration = configuration or self._server_parameters.copy()
# 由于配置了永久逻辑复制槽,我们必须启用 hot_standby_feedback 。
if self._postgresql.enforce_hot_standby_feedback:
configuration['hot_standby_feedback'] = 'on'
# 使用上下文管理器 config_writer 打开配置文件
with self.config_writer(self._postgresql_conf) as f:
include = self._config.get('custom_conf') or self._postgresql_base_conf_name
f.writeline("include '{0}'\n".format(ConfigWriter.escape(include)))
# 获取 PostgreSQL 版本,并迭代排序后的配置项
version = self.pg_version
for name, value in sorted((configuration).items()):
# 根据 PostgreSQL 版本和参数名称转换配置值
value = transform_postgresql_parameter_value(version, name, value)
# 如果转换后的值有效,并且参数不是 hba_file 或者当前不是自定义引导模式,则写入配置参数及其值
if value is not None and\
(name != 'hba_file' or not self._postgresql.bootstrap.running_custom_bootstrap):
f.write_param(name, value)
# 当我们在做自定义引导时,我们假定我们不知道超级用户的密码,
# 为了能够更改它,我们正在从某个地址开放信任访问,
# 因此我们需要确保 hba_file 不会被覆盖,
# 在更改超级用户密码之后,我们将“撤销”所有这些“更改”。
if self._postgresql.bootstrap.running_custom_bootstrap or 'hba_file' not in self._server_parameters:
f.write_param('hba_file', self._pg_hba_conf)
# 如果配置中没有 ident_file 参数,则写入 ident_file 的路径
if 'ident_file' not in self._server_parameters:
f.write_param('ident_file', self._pg_ident_conf)
# 如果 PostgreSQL 的主版本号大于等于 12,则如果有恢复参数,则写入恢复参数
if self._postgresql.major_version >= 120000:
if self._recovery_params:
f.writeline('\n# recovery.conf')
self._write_recovery_params(f, self._recovery_params)
if not self._postgresql.bootstrap.keep_existing_recovery_conf:
self._sanitize_auto_conf()
作用:
write_postgresql_conf
方法的具体作用是写入 PostgreSQL 的配置文件。具体来说:
- 备份原有配置:
- 如果当前配置中没有
custom_conf
项,并且基本配置文件_postgresql_base_conf
不存在,则重命名原始配置文件_postgresql_conf
到_postgresql_base_conf
。
- 如果当前配置中没有
- 准备配置数据:
- 如果没有提供新的配置,则使用当前服务参数的副本。
- 如果逻辑复制槽已配置,则强制启用
hot_standby_feedback
。
- 写入配置文件:
- 在配置文件中加入
include
行,指向自定义配置或基本配置。 - 根据 PostgreSQL 版本和参数名称转换配置值,并写入配置文件。
- 在自定义引导模式下,确保
hba_file
的路径不会被覆盖。 - 写入
ident_file
的路径。 - 如果 PostgreSQL 版本大于等于 12,则写入恢复参数,并清理自动配置文件。
- 在配置文件中加入
1.12.2.5.20 _write_recovery_params()
- 定义一个名为
_write_recovery_params
的方法,接受三个参数:self
(实例本身),fd
(一个类型为ConfigWriter
的文件描述符,用于写入配置信息),以及recovery_params
(一个不区分大小写的字典,包含恢复参数)。
def _write_recovery_params(self, fd: ConfigWriter, recovery_params: CaseInsensitiveDict) -> None:
# 检查 PostgreSQL 的主版本号是否大于或等于 9.5
if self._postgresql.major_version >= 90500:
pause_at_recovery_target = parse_bool(recovery_params.pop('pause_at_recovery_target', None))
if pause_at_recovery_target is not None:
recovery_params.setdefault('recovery_target_action', 'pause' if pause_at_recovery_target else 'promote')
# 如果 PostgreSQL 的版本低于 9.5
else:
if str(recovery_params.pop('recovery_target_action', None)).lower() == 'promote':
recovery_params.setdefault('pause_at_recovery_target', 'false')
# 遍历排序后的 recovery_params 字典项
for name, value in sorted(recovery_params.items()):
# 检查当前键名是否为 primary_conninfo
if name == 'primary_conninfo':
# 如果 PostgreSQL 的版本是 10 或更高,并且 write_pgpass 方法返回的结果包含 'PGPASSFILE',那么进行一些额外的处理
if self._postgresql.major_version >= 100000 and 'PGPASSFILE' in self.write_pgpass(value):
# 更新 value 字典中的 passfile 键,并记录密码文件的修改时间
value['passfile'] = self._passfile = self._pgpass
self._passfile_mtime = mtime(self._pgpass)
# 调用 format_dsn 方法来格式化连接字符串
value = self.format_dsn(value)
# 否则,根据 PostgreSQL 的主版本号,键名,和值来转换恢复参数的值
else:
value = transform_recovery_parameter_value(self._postgresql.major_version, name, value)
if value is None:
continue
# 写入参数名称和值到配置文件中
fd.write_param(name, value)
作用:
这个函数的主要作用是根据当前使用的 PostgreSQL 版本来调整恢复参数,并将这些参数写入配置文件中。它负责处理不同版本之间恢复参数的不同之处,例如在不同版本中的 pause_at_recovery_target
和 recovery_target_action
的交互方式,以及如何处理 primary_conninfo
中的密码文件信息。这有助于确保恢复参数被正确地配置,以便在数据库恢复期间可以正确地应用。
1.12.2.5.21 format_dsn()
- 定义了一个名为
format_dsn
的方法,它接受一个字典类型的参数params
,其中键为字符串,值可以是任意类型,并返回一个字符串。
def format_dsn(self, params: Dict[str, Any]) -> str:
"""根据连接参数格式化连接字符串。
.. note::
只考虑下面列表中的参数,并且值会被转义。
:param params: 包含连接参数的 :class:`dict` 对象。
:returns: 格式化的连接字符串,格式为 "key1=value1 key2=value2"。
"""
# 可以在conninfo字符串中找到的关键字列表。遵循libpq可接受的内容
# 定义了一个包含关键字的列表 keywords
keywords = ('dbname', 'user', 'passfile' if params.get('passfile') else 'password', 'host', 'port',
'sslmode', 'sslcompression', 'sslcert', 'sslkey', 'sslpassword', 'sslrootcert', 'sslcrl',
'sslcrldir', 'application_name', 'krbsrvname', 'gssencmode', 'channel_binding',
'target_session_attrs')
# 定义了一个名为 escape 的内部函数,用于转义连接参数的值
def escape(value: Any) -> str:
return re.sub(r'([\'\\ ])', r'\\\1', str(value))
return ' '.join('{0}={1}'.format(kw, escape(params[kw])) for kw in keywords if params.get(kw) is not None)
作用:
format_dsn
方法的具体作用是根据给定的连接参数字典 params
,生成一个符合 libpq 规范的连接字符串。具体来说:
- 选择关键字:
- 从预定义的
keywords
列表中选择有效的关键字。
- 从预定义的
- 转义值:
- 对于每个有效的关键字,使用
escape
函数来转义其值,以确保生成的连接字符串是安全的。
- 对于每个有效的关键字,使用
- 构造连接字符串:
- 将所有有效的关键字及其转义后的值构造成
"key=value"
形式的字符串,并用空格分隔,最终形成完整的连接字符串。
- 将所有有效的关键字及其转义后的值构造成
1.12.2.5.22 write_pgpass()
- 定义了一个名为
write_pgpass
的方法,它接受一个字典类型的参数record
,其中键为字符串,值可以是任意类型,并返回一个字符串键值对的字典。
def write_pgpass(self, record: Dict[str, Any]) -> Dict[str, str]:
"""基于连接参数可能创建 :attr:`_passfile`。
:param record: 包含连接参数的 :class:`dict` 对象。
:returns: 包含环境变量的副本,如果文件被写入,则将包括 ``PGPASSFILE``。
"""
# 调用 _pgpass_content 方法来生成 .pgpass 文件的内容
content = self._pgpass_content(record)
# 如果生成的内容为空,则返回操作系统环境变量的副本
if not content:
return os.environ.copy()
# 使用 with 语句打开 _pgpass 文件进行写入操作
with open(self._pgpass, 'w') as f:
# 设置 .pgpass 文件的权限为可读可写
os.chmod(self._pgpass, stat.S_IWRITE | stat.S_IREAD)
f.write(content)
return {**os.environ, 'PGPASSFILE': self._pgpass}
作用:
write_pgpass
方法的具体作用是根据给定的连接参数生成并写入一个 .pgpass
文件,并更新环境变量以包含该文件的位置。具体来说:
- 生成内容:
- 通过
_pgpass_content
方法生成.pgpass
文件的内容。
- 通过
- 检查内容:
- 如果生成的内容为空,则直接返回环境变量的副本。
- 写入文件:
- 如果内容非空,则写入
.pgpass
文件,并设置文件权限。
- 如果内容非空,则写入
- 更新环境变量:
- 将
.pgpass
文件的路径添加到环境变量中,以使 PostgreSQL 客户端工具能够读取此文件。
- 将
1.12.2.5.23 _pgpass_content()
- 定义了一个名为
_pgpass_content
的静态方法,它接受一个字典类型的参数record
,其中键为字符串,值可以是任意类型,并返回一个字符串或None
。
@staticmethod
def _pgpass_content(record: Dict[str, Any]) -> Optional[str]:
"""根据连接参数生成 `pgpassfile` 的内容。
.. note::
如果 ``host`` 是由逗号分隔的字符串,我们将为每个主机生成一行。
:param record: 包含连接参数的 :class:`dict` 对象。
:returns: 生成的 `pgpassfile` 内容的字符串,如果没有 ``password`` 则返回 ``None``。
"""
# 检查 record 字典中是否包含 password 键
if 'password' in record:
# 定义了一个名为 escape 的内部函数,用于转义连接参数的值
def escape(value: Any) -> str:
return re.sub(r'([:\\])', r'\\\1', str(value))
# 处理 host 键的值,如果它是逗号分隔的字符串,那么将为每个主机生成一行
hosts = [escape(host) for host in filter(None, map(str.strip,
(record.get('host', '') or '*').split(',')))] # pyright: ignore [reportUnknownArgumentType]
# 检查是否有任何主机名是以 / 开头的,并且 hosts 列表中没有 localhost,则添加 localhost
if any(host.startswith('/') for host in hosts) and 'localhost' not in hosts:
hosts.append('localhost')
record = {n: escape(record.get(n) or '*') for n in ('port', 'user', 'password')}
return ''.join('{host}:{port}:*:{user}:{password}\n'.format(**record, host=host) for host in hosts)
作用:
_pgpass_content
方法的具体作用是根据给定的连接参数生成一个适合 .pgpass
文件的内容。具体来说:
- 检查密码:
- 检查
record
中是否包含password
键,如果没有,则返回None
。
- 检查
- 转义值:
- 对于每个需要的连接参数(如
host
、port
、user
和password
),使用escape
函数来转义值,确保生成的内容是安全的。
- 对于每个需要的连接参数(如
- 生成内容:
- 如果
host
是逗号分隔的多个值,为每个主机生成一行内容。 - 如果有以
/
开头的主机,并且没有localhost
,则添加localhost
。 - 构造格式化的字符串,并用换行符连接起来。
- 如果
1.12.2.5.24 _sanitize_auto_conf()
- 定义了一个名为
_sanitize_auto_conf
的方法,它没有参数并且返回None
。
def _sanitize_auto_conf(self) -> None:
# 初始化两个变量
overwrite = False
lines: List[str] = []
# 检查 _auto_conf 文件是否存在
if os.path.exists(self._auto_conf):
# 如果文件存在,尝试打开并读取文件内容
try:
with open(self._auto_conf) as f:
for raw_line in f:
line = raw_line.strip()
match = PARAMETER_RE.match(line)
if match and match.group(1).lower() in self._RECOVERY_PARAMETERS:
overwrite = True
else:
lines.append(raw_line)
except Exception:
logger.info('Failed to read %s', self._auto_conf)
# 检查是否需要重写文件
if overwrite:
# 如果需要重写文件,则尝试打开文件进行写入操作。首先设置文件权限,然后遍历 lines 列表并将每一行写入文件
try:
with open(self._auto_conf, 'w') as f:
self.set_file_permissions(self._auto_conf)
for raw_line in lines:
f.write(raw_line)
except Exception:
logger.exception('Failed to remove some unwanted parameters from %s', self._auto_conf)
作用:
_sanitize_auto_conf
方法的具体作用是从 PostgreSQL 的自动配置文件(通常为 postgresql.auto.conf
)中移除与恢复相关的参数。具体来说:
- 读取文件:
- 读取自动配置文件的内容,并检查每一行是否包含恢复相关的参数。
- 标记重写:
- 如果发现恢复相关的参数,则标记需要重写文件。
- 重写文件:
- 如果标记为需要重写,则打开文件进行写入,并跳过包含恢复相关参数的行。
- 记录日志:
- 如果在读取或写入过程中出现任何异常,则记录相应的日志信息。
1.12.2.5.25 save_configuration_files()
- 定义了一个名为
save_configuration_files
的方法,该方法接受一个可选的布尔参数check_custom_bootstrap
(默认为False
),并返回一个布尔值。
def save_configuration_files(self, check_custom_bootstrap: bool = False) -> bool:
"""
将 postgresql.conf 复制为 postgresql.conf.backup ,以便能够检索配置文件。
- 最初存储为符号链接,这些文件通常会被 pg_basebackup 忽略。
- 在 WAL-E basebackup 的情况下(参见 http://comments.gmane.org/gmane.comp.db.postgresql.wal-e/239)
"""
# 检查 check_custom_bootstrap 是否为 True 并且 _postgresql.bootstrap.running_custom_bootstrap 也为 True
if not (check_custom_bootstrap and self._postgresql.bootstrap.running_custom_bootstrap):
try:
# 遍历 _configuration_to_save 集合中的每个配置文件名
for f in self._configuration_to_save:
# 构建配置文件的完整路径
config_file = os.path.join(self._config_dir, f)
# 构建备份文件的完整路径
backup_file = os.path.join(self._postgresql.data_dir, f + '.backup')
# 如果配置文件存在,则将其复制到备份位置,并设置备份文件的权限
if os.path.isfile(config_file):
shutil.copy(config_file, backup_file)
self.set_file_permissions(backup_file)
except IOError:
logger.exception('unable to create backup copies of configuration files')
return True
作用:
save_configuration_files
方法的作用是备份 PostgreSQL 的配置文件。具体来说:
- 条件判断:
- 如果
check_custom_bootstrap
为True
并且_postgresql.bootstrap.running_custom_bootstrap
也为True
,则跳过备份操作。
- 如果
- 备份配置文件:
- 遍历
_configuration_to_save
集合中的配置文件名,并构建配置文件和备份文件的路径。 - 如果配置文件存在,则将其复制到 PostgreSQL 数据目录下的备份位置,并设置备份文件的权限。
- 遍历
- 异常处理:
- 如果在复制过程中发生
IOError
,则记录一条异常日志。
- 如果在复制过程中发生
- 返回值:
- 无论备份操作是否成功,都返回
True
。
- 无论备份操作是否成功,都返回
通过这些步骤,save_configuration_files
方法能够确保在 PostgreSQL 数据库集群中重要的配置文件得到妥善备份。这在恢复数据或者调试问题时是非常有用的,尤其是在使用 pg_basebackup
或者 WAL-E
进行备份时,这些工具可能会忽略符号链接指向的配置文件。
1.12.2.5.26 _configuration_to_save()
- 定义了一个名为
_configuration_to_save
的属性(property),该属性返回一个字符串列表。
@property
def _configuration_to_save(self) -> List[str]:
# 初始化 configuration 列表
configuration = [os.path.basename(self._postgresql_conf)]
# 如果 _config 字典中没有 'custom_conf' 键
if 'custom_conf' not in self._config:
configuration.append(os.path.basename(self._postgresql_base_conf_name))
# 没有自定义的 pg_hba.conf 文件
if not self.hba_file:
configuration.append('pg_hba.conf')
# 没有自定义的 pg_ident.conf 文件
if not self.ident_file:
configuration.append('pg_ident.conf')
return configuration
作用:
_configuration_to_save
属性的作用是动态生成一个需要备份的配置文件列表。具体来说:
- 初始化配置文件列表:
- 初始化
configuration
列表,并将 PostgreSQL 主配置文件的名字添加到列表中。
- 初始化
- 检查基础配置文件:
- 如果
_config
字典中没有'custom_conf'
键,则认为需要备份基础配置文件,并将基础配置文件的名字添加到列表中。
- 如果
- 检查
pg_hba.conf
文件:- 如果
hba_file
属性为False
或None
,则认为需要备份默认的pg_hba.conf
文件,并将文件名字添加到列表中。
- 如果
- 检查
pg_ident.conf
文件:- 如果
ident_file
属性为False
或None
,则认为需要备份默认的pg_ident.conf
文件,并将文件名字添加到列表中。
- 如果
- 返回配置文件列表:
- 返回最终的
configuration
列表。
- 返回最终的
通过这种方式,_configuration_to_save
属性能够根据当前配置动态确定哪些配置文件需要被备份,确保只有必要的文件被处理,从而提高了备份过程的灵活性和效率。
1.12.2.6 类:ConfigWriter
ConfigWriter
类提供了一种机制,允许用户以结构化的方式写入配置文件。通过使用 Python 的上下文管理协议(即 __enter__
和 __exit__
方法),这个类能够在 with
语句中自动打开和关闭文件,同时提供了一些辅助方法来简化写入过程。
__init__
:初始化方法,设置要写入的文件名,并初始化文件描述符 _fd
为 None
。
__enter__
:定义了进入上下文管理时的操作。这个方法打开指定的文件用于写入,并写入两条警告注释,然后返回自身实例,允许在 with
语句中使用。
__exit__
:定义了退出上下文管理时的操作。这个方法检查文件描述符 _fd
是否存在,如果存在则关闭文件。
writeline
:这个方法将单行文本写入文件,并在其后加上换行符。它首先检查文件描述符 _fd
是否有效,然后执行写入操作。
writelines
:这个方法接收一个字符串列表作为参数,并逐行写入这些字符串到文件中。对于列表中的每一个元素,如果它是字符串,则调用 writeline
方法写入该行。
escape
:这个静态方法用于转义字符串中的单引号和反斜杠。它接收任意类型的值,先将其转换为字符串,然后使用正则表达式将所有出现的单引号和反斜杠重复一次。
write_param
:这个方法用于写入参数及其值到文件中。它接受一个参数名称和一个参数值,先调用 escape
方法对值进行转义处理,然后构造出类似 "param_name = 'escaped_value'"
的格式,并调用 writeline
方法写入到文件中。
- 定义了一个名为
ConfigWriter
的类,继承自object
。
class ConfigWriter(object):
ConfigWriter
类会具备以下功能:
- 打开文件:在进入上下文管理器时打开文件。
- 写入文件:提供方法来写入文件内容。
- 关闭文件:在离开上下文管理器时关闭文件,并处理可能出现的异常。
1.12.2.6.1 __init__()
- 定义了类的构造方法
__init__
,它接受一个字符串类型的参数filename
,并返回None
。
def __init__(self, filename: str) -> None:
# 这通常用于保存文件的路径
self._filename = filename
# 文件描述符
self._fd = None
作用:
ConfigWriter
类的具体作用是提供一个用于写入配置文件的工具类。具体来说:
- 初始化文件路径:
- 在构造函数中接收一个文件路径
filename
,并将其存储在_filename
属性中。这使得ConfigWriter
实例可以记住它将要操作的文件。
- 在构造函数中接收一个文件路径
- 准备文件描述符:
- 初始化
_fd
属性为None
。在后续的上下文管理协议实现中,这个属性会被设置成打开的文件对象,这样就可以通过这个对象来写入文件。
- 初始化
1.12.2.6.2 __enter__()
- 这一行定义了
__enter__
方法,它是一个特殊的方法,用于在with
语句中初始化一个对象。这里指定了方法的返回类型为'ConfigWriter'
,意味着这个方法将返回当前类的一个实例。
def __enter__(self) -> 'ConfigWriter':
self._fd = open(self._filename, 'w')
self.writeline('# Do not edit this file manually!\n# It will be overwritten by Patroni!')
return self
作用:
这个 __enter__
方法的作用是在使用 with ConfigWriter(...) as writer:
这样的语法时,自动打开指定的文件并写入两条警告注释。当 with
块执行完毕后,Python 的上下文管理器会自动调用 __exit__
方法,通常用于关闭文件或者释放其他资源。这样做可以确保即使在处理文件期间发生错误,文件也会被正确关闭。然而,从提供的代码片段来看,__exit__
方法没有给出,实际使用中应该也有相应的实现来处理资源的释放。
1.12.2.6.3 __exit__()
- 定义了
__exit__
方法,它接受三个参数:exc_type
、exc_val
和exc_tb
,这些参数分别代表异常类型、异常值和异常的追踪信息。这些参数允许我们在with
语句块中捕获并处理异常。方法的返回类型声明为None
。
def __exit__(self, exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]) -> None:
if self._fd:
self._fd.close()
作用:
__exit__
方法的作用是在 with
语句块结束时进行必要的清理工作。在这个特定的例子中,它的主要功能是关闭在 __enter__
方法中打开的文件。无论 with
语句块内的代码是否正常完成还是抛出了异常,__exit__
方法都会被调用,因此它可以保证文件被正确关闭,防止资源泄露。这种机制在处理像文件这样的外部资源时非常有用,因为即使发生了错误,也可以确保资源得到适当的释放。
1.12.2.6.4 writeline()
- 定义了一个方法
writeline
,它接受一个类型为str
的参数line
,并且明确指出该方法的返回类型为None
。
def writeline(self, line: str) -> None:
if self._fd:
self._fd.write(line)
self._fd.write('\n')
作用:
writeline
方法的作用是在给定的文件中写入一行文本,并在其后自动添加换行符,从而确保每次调用该方法时都能在同一文件中的新行开始写入新的内容。这种方法常用于生成配置文件或其他需要逐行写入信息的场景。如果文件描述符 _fd
无效(可能是因为文件未打开或已关闭),则不会执行写入操作。
1.12.2.6.5 writelines()
- 定义了一个方法
writelines
,它接受一个类型为List[Optional[str]]
的参数lines
,其中Optional[str]
表示列表中的元素可以是字符串类型或者None
。该方法的返回类型为None
。
def writelines(self, lines: List[Optional[str]]) -> None:
for line in lines:
# 这行代码检查当前迭代到的 line 是否是一个字符串类型的对象。如果是字符串,那么就执行写入操作
if isinstance(line, str):
self.writeline(line)
作用:
writelines
方法的作用是从一个列表中读取每一行字符串,并调用 writeline
方法将它们逐行写入到一个已经打开的文件中。这个方法假设传入的列表中的每个元素都是字符串或者 None
,对于 None
元素,该方法不会尝试写入任何内容。如果列表中的元素不是字符串,也不会引发错误,只是简单地跳过该元素。
这个方法可以用于批量写入多行文本到文件中,非常适合于需要一次性写入大量文本行的情况。例如,在生成配置文件、日志文件或者数据文件时可能会用到这个方法。
1.12.2.6.6 escape()
- 这是一个装饰器,表明
escape
方法是一个静态方法。静态方法不需要访问类或实例的状态,因此不需要传递self
或cls
参数。 - 定义了一个名为
escape
的静态方法,它接受一个类型为Any
的参数value
并返回一个字符串类型的结果。注释部分解释了这个方法的功能:转义(通过重复)给定字符串中的任何单引号或反斜杠。
@staticmethod
def escape(value: Any) -> str: # Escape (by doubling) any single quotes or backslashes in given string
return re.sub(r'([\'\\])', r'\1\1', str(value))
作用:
escape
函数的具体作用是对给定的字符串中的单引号 ('
) 和反斜杠 (\
) 进行转义。所谓“转义”,指的是将这些字符重复一次,以便在某些上下文中(如字符串字面量、配置文件等)能够正确解析这些字符。例如,当字符串需要嵌入到另一个字符串中时,避免单引号和反斜杠被误解析为字符串的终止符号或其他意义。这在处理包含特殊字符的数据时非常有用,尤其是在生成需要严格格式控制的文本文件(如配置文件、SQL查询等)时。
1.12.2.6.7 write_param()
- 定义了一个方法
write_param
,它接受两个参数:一个是类型为str
的param
,代表参数名称;另一个是类型为Any
的value
,代表参数值。该方法的返回类型为None
。
def write_param(self, param: str, value: Any) -> None:
self.writeline("{0} = '{1}'".format(param, self.escape(value)))
作用:
write_param
方法的作用是将一个参数及其对应的值按照特定格式写入文件中,并确保值中的特殊字符(如单引号和反斜杠)被适当地转义。具体来说,这个方法会构造一条形如 "param = 'value'"
的配置项,并将其写入到文件中,确保文件中的配置项格式正确且值中的特殊字符不会导致解析错误。
这种方法常用于生成配置文件或者其他需要按特定格式保存键值对的场景。通过调用 escape
方法,可以确保写入的值不会包含会导致解析问题的特殊字符。
1.12.2.7 get_param_diff()
- 定义了一个名为
get_param_diff
的函数,它接受四个参数:old_value
(任意类型)、new_value
(任意类型)、vartype
(可选字符串,默认为None
)、unit
(可选字符串,默认为None
),并返回一个字典,字典的键为字符串,值也为字符串。
def get_param_diff(old_value: Any, new_value: Any,
vartype: Optional[str] = None, unit: Optional[str] = None) -> Dict[str, str]:
"""获取代表单个 PG 参数值差异的字典。
:param old_value: 当前的 :class:`str` 参数值。
:param new_value: 参数重启后的 :class:`str` 值。
:param vartype: 解析 old/new_value 的目标类型。参见 :func:`~patroni.utils.maybe_convert_from_base_unit` 的 ``vartype`` 参数。
:param unit: *old/new_value* 的单位。参见 :func:`~patroni.utils.maybe_convert_from_base_unit` 的 ``base_unit`` 参数。
:returns: 包含两个键 ``old_value`` 和 ``new_value`` 的 :class:`dict` 对象,其值被转换为 :class:`str` 并从基本单位转换(如果可能的话)。
"""
str_value: Callable[[Any], str] = lambda x: '' if x is None else str(x)
# 构造并返回一个字典,包含两个键 old_value 和 new_value ,其值分别为 old_value 和 new_value 转换后的字符串表示。如果提供了 vartype ,则使用 maybe_convert_from_base_unit 函数从基本单位进行转换,否则直接使用 str_value 函数进行字符串化。
return {
'old_value': (maybe_convert_from_base_unit(str_value(old_value), vartype, unit)
if vartype else str_value(old_value)),
'new_value': (maybe_convert_from_base_unit(str_value(new_value), vartype, unit)
if vartype else str_value(new_value))
}
作用:
get_param_diff
函数的具体作用是获取一个 PostgreSQL 参数值的变化,并以字典的形式返回变化前后的值。具体来说:
- 参数处理:
old_value
:表示参数当前的值。new_value
:表示参数在重启后的新值。vartype
:用于指定old_value
和new_value
的目标类型,以便进行相应的转换。unit
:用于指定old_value
和new_value
的单位,以便进行从基本单位的转换。
- 值转换:
- 如果提供了
vartype
,则使用maybe_convert_from_base_unit
函数对old_value
和new_value
进行转换,使其从基本单位转换为更易于理解的形式。 - 如果没有提供
vartype
,则直接将old_value
和new_value
转换为字符串。
- 如果提供了
- 结果构造:
- 构造一个包含两个键
old_value
和new_value
的字典,其值为转换后的字符串表示。
- 构造一个包含两个键
1.12.2.8 maybe_convert_from_base_unit()
- 定义了一个名为
maybe_convert_from_base_unit
的函数,它接受三个参数:base_value
(字符串类型)、vartype
(字符串类型)和base_unit
(可选字符串类型),并返回一个字符串。
def maybe_convert_from_base_unit(base_value: str, vartype: str, base_unit: Optional[str]) -> str:
"""尝试将整数或实数类型的值从基本单位转换为人类可读的单位。
值以字符串形式传递。如果解析或后续转换失败,则返回原始值。
:param base_value: 需要从基本单位转换的值。
:param vartyp e: 解析 *base_value* 之前的目标类型(期望 ``integer`` 或 ``real`` 类型,其他类型将导致返回值等于 *base_value* 字符串)。
:param base_unit: *value* 的单位。应为基本单位之一(区分大小写):
* 对于空间:``B``、``kB``、``MB``;
* 对于时间:``ms``、``s``、``min``。
:returns: :class:`str` 类型的值,表示 *base_value* 已从 *base_unit* 转换为尽可能大的人类友好单位,或者如果转换失败则返回 *base_value* 字符串。
:Example:
>>> maybe_convert_from_base_unit('5', 'integer', 'ms')
'5ms'
>>> maybe_convert_from_base_unit('4.2', 'real', 'ms')
'4200us'
>>> maybe_convert_from_base_unit('on', 'bool', None)
'on'
>>> maybe_convert_from_base_unit('', 'integer', '256MB')
''
"""
# 定义了一个字典 converters,映射不同的 vartype 到对应的解析函数和转换函数
converters: Dict[str, Tuple[Callable[[str, Optional[str]], Union[int, float, str, None]],
Callable[[Any, Optional[str]], Optional[str]]]] = {
'integer': (parse_int, convert_int_from_base_unit),
'real': (parse_real, convert_real_from_base_unit),
'default': (lambda v, _: v, lambda v, _: v)
}
# 根据 vartype 从 converters 字典中获取对应的解析函数和转换函数
parser, converter = converters.get(vartype, converters['default'])
# 使用解析函数 parser 尝试将 base_value 解析为目标类型
parsed_value = parser(base_value, None)
if parsed_value:
return converter(parsed_value, base_unit) or base_value
return base_value
作用:
maybe_convert_from_base_unit
函数的具体作用是尝试将一个字符串表示的值从其基本单位转换为更容易理解的人类可读单位。具体来说:
- 解析输入值:
- 根据
vartype
选择适当的解析函数,尝试将base_value
解析为目标类型(整数或实数)。
- 根据
- 转换单位:
- 如果解析成功,根据
base_unit
选择适当的转换函数,尝试将解析后的值转换为更易读的单位。
- 如果解析成功,根据
- 返回结果:
- 如果转换成功,返回转换后的字符串表示。
- 如果解析或转换失败,返回原始的
base_value
。
1.12.2.9 mtime()
- 定义了一个名为
mtime
的函数,接受一个字符串类型的参数filename
,返回值类型为可选的浮点数类型(Optional[float]
),即可能返回一个浮点数或者None
。
def mtime(filename: str) -> Optional[float]:
# 试使用 os.stat() 函数获取指定文件的信息
try:
return os.stat(filename).st_mtime
except OSError:
return None
作用:
mtime
函数的具体作用是获取指定文件的最后修改时间。具体来说:
- 文件信息获取:
- 使用
os.stat()
函数来获取文件的状态信息。
- 使用
- 提取修改时间:
- 从状态信息中提取
st_mtime
,这是文件最后一次修改的时间戳。
- 从状态信息中提取
- 异常处理:
- 如果文件不存在或由于其他原因导致
os.stat()
抛出OSError
异常,则返回None
。
- 如果文件不存在或由于其他原因导致
1.12.3 mpp文件夹
1.12.3.1 __init__.py
- 类:AbstractMPP
- 定义了一个名为
AbstractMPP
的抽象基类(Abstract Base Class, ABC),继承自abc.ABC
。
class AbstractMPP(abc.ABC):
"""一个应该传递给 :class:`AbstractDCS` 的抽象类。
.. note::
我们创建了 :class:`AbstractMPP` 和 :class:`AbstractMPPHandler` 来解决初始化时的鸡生蛋蛋生鸡问题。
当初始化 DCS 时,我们动态创建一个实现了 :class:`AbstractMPP` 的对象,之后该对象被用来实例化一个实现了 :class:`AbstractMPPHandler` 的对象。
"""
group_re: Any # re.Pattern[str]
作用:
这个类的作用是作为一个抽象基类,提供一个框架来解决初始化时的相互依赖问题。具体来说:
- 抽象基类:
AbstractMPP
是一个抽象基类,意味着它不能直接被实例化,而是应该被子类继承,并实现其抽象方法。 - 解决初始化问题:通过创建
AbstractMPP
和AbstractMPPHandler
,解决了在初始化过程中可能出现的相互依赖问题,即初始化时需要先有对象 A 才能创建对象 B,但对象 A 的创建又依赖于 B 的存在。 - 正则表达式属性:类中定义了一个类型为
Any
的属性group_re
,实际上预期类型为re.Pattern[str]
,可能用于匹配某些特定模式的字符串。
1.12.3.1.1 is_coordinator()
- 定义了一个名为
is_coordinator
的方法,该方法属于某个类,并返回一个布尔值。此方法的作用是检查当前节点是否在一个作为协调器的 PostgreSQL 集群中运行。
def is_coordinator(self) -> bool:
"""检查当前节点是否在一个作为协调器的 PostgreSQL 集群中运行。
:returns: 如果 MPP 已启用并且当前节点的组 ID 与 :attr:`coordinator_group_id` 匹配,则返回 ``True``,否则返回 ``False``。
"""
return self.is_enabled() and self.group == self.coordinator_group_id
- 返回
self.is_enabled()
与self.group == self.coordinator_group_id
的逻辑与运算结果:self.is_enabled()
:检查 MPP 是否被启用。self.group == self.coordinator_group_id
:检查当前节点的组 ID 是否与协调器组 ID 匹配。
作用:
这个函数的作用是确定当前节点是否为 MPP 系统中的协调器节点。具体来说:
- 检查 MPP 是否启用:
- 通过
self.is_enabled()
方法检查 MPP 功能是否已经启用。
- 通过
- 检查组 ID 是否匹配:
- 如果 MPP 已启用,进一步检查当前节点的组 ID (
self.group
) 是否与协调器组 ID (self.coordinator_group_id
) 匹配。
- 如果 MPP 已启用,进一步检查当前节点的组 ID (
- 返回结果:
- 如果 MPP 已启用并且当前节点的组 ID 与协调器组 ID 匹配,则返回
True
,表明当前节点是一个 MPP 协调器。 - 否则,返回
False
。
- 如果 MPP 已启用并且当前节点的组 ID 与协调器组 ID 匹配,则返回
通过这个方法,可以方便地在代码中判断当前节点的角色。对于 MPP 数据库系统而言,协调器节点负责接收客户端的查询请求,并将这些请求分发到不同的工作节点上执行,然后再将结果汇总返回给客户端。因此,确定当前节点是否为协调器对于执行正确的逻辑流程非常重要。
1.12.3.1.2 is_enabled()
- 定义了一个名为
is_enabled
的方法,该方法属于某个类,并返回一个布尔值。此方法的作用是检查给定的 MPP(大规模并行处理)是否已被启用。
def is_enabled(self) -> bool:
"""检查给定的 MPP 是否已被启用。
.. note::
我们只是检查 :attr:`_config` 对象是否为空,并期望它仅在 :class:`Null` 情况下为空。
:returns: 如果 MPP 已启用,则返回 ``True``,否则返回 ``False``。
"""
return bool(self._config)
- 返回
self._config
转换为布尔值的结果:bool(self._config)
:如果_config
对象不是空的(即包含有效配置),则返回True
;否则返回False
。
作用:
这个函数的作用是确定 MPP 是否已经被启用。具体来说:
- 检查配置:
- 通过检查
_config
对象是否为空来判断 MPP 是否已被启用。 - 如果
_config
不为空,则认为 MPP 已启用,返回True
。 - 如果
_config
为空,则认为 MPP 未启用,返回False
。
- 通过检查
- 返回结果:
- 返回 MPP 是否启用的布尔值结果。
通过这个方法,可以很容易地判断 MPP 功能是否已经配置并启用,这对于后续逻辑处理非常有用,尤其是在需要区分是否启用 MPP 场景下的不同行为时。如果 _config
存在且非空,那么可以认为 MPP 已经启用,否则 MPP 尚未启用或配置不完整。
1.12.3.1.3 group()
- 定义了一个名为
group
的抽象属性方法,该方法不需要任何参数,并声明返回类型为Any
。
@property
@abc.abstractmethod
def group(self) -> Any:
"""给定的 MPP 实现的组。"""
作用:
这个抽象属性方法的作用是在抽象基类中声明一个属性 group
,该属性应该返回一个与特定 MPP 实现相关的组标识。由于这是一个抽象方法,所以它本身并没有实现具体的逻辑,而是要求所有继承自该抽象基类的子类必须提供具体的实现。
1.12.3.1.4 coordinator_group_id()
- 定义了一个名为
coordinator_group_id
的属性,该属性是一个抽象方法(@abc.abstractmethod
),并且返回类型为Any
。这个属性代表了协调器 PostgreSQL 集群的组 ID。
@property
@abc.abstractmethod
def coordinator_group_id(self) -> Any:
"""协调器 PostgreSQL 集群的组 ID。"""
作用:
这个属性的作用是在子类中定义协调器 PostgreSQL 集群的组 ID。由于它是一个抽象属性(@abc.abstractmethod
),因此在任何继承自当前类的子类中都必须具体实现这个属性。
1.12.3.1.5 get_handler_impl()
- 定义了一个名为
get_handler_impl
的实例方法,该方法接受一个类型为Postgresql
的参数postgresql
,并返回类型为AbstractMPPHandler
的对象。
def get_handler_impl(self, postgresql: 'Postgresql') -> 'AbstractMPPHandler':
"""查找并实例化此对象的 Handler 实现。
:param postgresql: 指向 Postgresql 对象的引用。
:raises:
:exc:`PatroniException`: 如果未找到 Handler 类。
:returns: 实现了此对象 Handler 的实例化类。
"""
# 遍历 _get_handler_cls 方法返回的类列表
for cls in self._get_handler_cls():
# 对于每一个类 cls,使用提供的 postgresql 和 _config 参数实例化,并立即返回该实例
return cls(postgresql, self._config)
raise PatroniException(f'Failed to initialize {self.__class__.__name__}Handler object')
作用:
这个方法的作用是根据当前对象找到合适的 Handler
实现,并实例化该 Handler
。具体来说:
- 获取 Handler 类:通过调用
_get_handler_cls
方法获取候选的 Handler 类。 - 实例化 Handler:对于每一个候选类,使用提供的
postgresql
和_config
参数实例化,并立即返回该实例。
1.12.3.1.6 get_handler_impl()
- 定义了一个名为
_get_handler_cls
的实例方法,该方法没有参数,并返回一个类型为Iterator
的对象,其中包含类型为Type['AbstractMPPHandler']
的元素。
def _get_handler_cls(self) -> Iterator[Type['AbstractMPPHandler']]:
"""查找继承自此对象类类型的 Handler 类。
:yields: 此对象的 Handler 类。
"""
# 使用 for 循环遍历当前类的所有子类
for cls in self.__class__.__subclasses__():
# 对于每一个子类 cls,检查是否继承自 AbstractMPPHandler 并且类名是以当前类的类名开头的
if issubclass(cls, AbstractMPPHandler) and cls.__name__.startswith(self.__class__.__name__):
yield cls
作用:
这个方法的作用是查找继承自当前对象类类型的 Handler
类,并返回一个生成器,该生成器产生这些 Handler
类。具体来说:
- 获取子类:通过
self.__class__.__subclasses__()
获取当前类的所有子类。 - 检查子类:对于每一个子类,检查它是否继承自
AbstractMPPHandler
并且类名是以当前类的类名开头。 - 生成类:如果条件满足,则使用
yield
语句返回该类。
1.12.3.2 citus.py
1.12.4 __init__.py
Postgresql
类:
1.12.4.1 类:Postgresql
- 定义了一个名为
Postgresql
的类,继承自object
。 - 这个类的作用主要是定义了一些用于 PostgreSQL 数据库操作的常量和 SQL 表达式。具体来说:
- POSTMASTER_START_TIME:定义了一个用于获取 PostgreSQL 后端进程启动时间的常量。
- TL_LSN:定义了一个 SQL 表达式,用于获取 PostgreSQL 数据库中多个关键的 LSN 信息,包括主时间线编号、当前 WAL LSN、最近的重放 LSN 和最近的接收 LSN,以及重放是否被暂停的状态。
class Postgresql(object):
POSTMASTER_START_TIME = "pg_catalog.pg_postmaster_start_time()"
TL_LSN = ("CASE WHEN pg_catalog.pg_is_in_recovery() THEN 0 "
"ELSE ('x' || pg_catalog.substr(pg_catalog.pg_{0}file_name("
"pg_catalog.pg_current_{0}_{1}()), 1, 8))::bit(32)::int END, " # primary timeline
"CASE WHEN pg_catalog.pg_is_in_recovery() THEN 0 ELSE "
"pg_catalog.pg_{0}_{1}_diff(pg_catalog.pg_current_{0}{2}_{1}(), '0/0')::bigint END, " # wal(_flush)?_lsn
"pg_catalog.pg_{0}_{1}_diff(pg_catalog.pg_last_{0}_replay_{1}(), '0/0')::bigint, "
"pg_catalog.pg_{0}_{1}_diff(COALESCE(pg_catalog.pg_last_{0}_receive_{1}(), '0/0'), '0/0')::bigint, "
"pg_catalog.pg_is_in_recovery() AND pg_catalog.pg_is_{0}_replay_paused()")
1.12.4.1.1 __init__()
- 定义了一个名为
__init__
的构造函数,该函数接受一个类型为Dict[str, Any]
的参数config
和一个类型为AbstractMPP
的参数mpp
,并返回None
。
# 此构造函数用于初始化 PostgreSQL 对象,并设置其属性。
def __init__(self, config: Dict[str, Any], mpp: AbstractMPP) -> None:
# 从配置中获取值
self.name: str = config['name']
self.scope: str = config['scope']
self._data_dir: str = config['data_dir']
self._database = config.get('database', 'postgres')
# 获取 PostgreSQL 版本文件的路径
self._version_file = os.path.join(self._data_dir, 'PG_VERSION')
# 获取 PostgreSQL 控制文件的路径
self._pg_control = os.path.join(self._data_dir, 'global', 'pg_control')
self.connection_string: str
self.proxy_url: Optional[str]
self._major_version = self.get_major_version()
# 创建一个锁对象,并设置初始状态为 'stopped'
self._state_lock = Lock()
self.set_state('stopped')
# 创建一个不区分大小写的字典
self._pending_restart_reason = CaseInsensitiveDict()
# 创建一个连接池,并从中获取一个名为 'heartbeat' 的连接
self.connection_pool = ConnectionPool()
self._connection = self.connection_pool.get('heartbeat')
# 从 mpp 获取处理程序实现
self.mpp_handler = mpp.get_handler_impl(self)
# 从配置中获取 bin_dir 值
self._bin_dir = config.get('bin_dir') or ''
# 创建一个配置处理器实例,并从配置中检查目录的存在性
self.config = ConfigHandler(self, config)
self.config.check_directories()
# 创建一个引导程序实例,并设置 bootstrapping 为 False,同时获取当前线程的标识符
self.bootstrap = Bootstrap(self)
self.bootstrapping = False
self.__thread_ident = current_thread().ident
# 创建一个插槽处理器实例和一个同步处理器实例
self.slots_handler = SlotsHandler(self)
self.sync_handler = SyncHandler(self)
# 创建一个回调执行器实例,并初始化两个与回调相关的标志
self._callback_executor = CallbackExecutor()
self.__cb_called = False
self.__cb_pending = None
# 创建一个可取消的子进程实例
self.cancellable = CancellableSubprocess()
# 初始化系统标识符,并创建一个重试策略实例
self._sysid = ''
self.retry = Retry(max_tries=-1, deadline=config['retry_timeout'] / 2.0, max_delay=1,
retry_exceptions=PostgresConnectionException)
# 创建一个仅重试一次的重试策略实例
self._is_leader_retry = Retry(max_tries=1, deadline=config['retry_timeout'] / 2.0, max_delay=1,
retry_exceptions=PostgresConnectionException)
# 创建一个锁对象,并设置 PostgreSQL 的角色
self._role_lock = Lock()
self.set_role(self.get_postgres_role_from_data_directory())
# 初始化状态进入时间戳
self._state_entry_timestamp = 0
# 初始化集群信息状态、查询插槽标志、热备反馈强制标志和副本时间线缓存
self._cluster_info_state = {}
self._should_query_slots = True
self._enforce_hot_standby_feedback = False
self._cached_replica_timeline = None
# 初始化最后一次已知的运行中的进程引用
self._postmaster_proc = None
# 如果 PostgreSQL 正在运行,则设置状态为 'starting' 并检查启动状态是否改变
if self.is_running():
# 如果我们发现postmaster进程,我们需要弄清楚postgres是否正在接受连接
self.set_state('starting')
self.check_startup_state_changed()
# 正在加入一个已经处于运行状态的 PostgreSQL 实例
if self.state == 'running': # 我们正在“加入”已经在运行的postgres
# 我们知道PostgreSQL正在接受连接,并且可以从pg_settings中读取一些GUC
self.config.load_current_server_parameters()
# 根据是否为主节点设置角色为 'primary' 或 'replica'
self.set_role('primary' if self.is_primary() else 'replica')
# 替换 pg_hba.conf 和 pg_ident.conf 文件,并记录是否进行了替换
hba_saved = self.config.replace_pg_hba()
ident_saved = self.config.replace_pg_ident()
# 如果 PostgreSQL 主要版本小于 12 或者角色为主节点,则重新加载配置。如果 hba_saved 或 ident_saved 为 True,则重新加载
if self.major_version < 120000 or self.role == 'primary':
# 如果PostgreSQL作为主数据库运行,或者我们运行的PostgreSQL版本大于12,我们可以
# 再次调用reload_config()(第一次调用发生在ConfigHandler构造函数中),
# 以便它可以确定是否应该更新配置文件并执行pg_ctl reload。
self.config.reload_config(config, sighup=bool(hba_saved or ident_saved))
elif hba_saved or ident_saved:
self.reload()
# 如果 PostgreSQL 不在运行并且当前角色为主节点,则将角色设置为 'demoted'
elif not self.is_running() and self.role == 'primary':
self.set_role('demoted')
作用:
这个构造函数的作用是初始化一个 PostgreSQL 对象,并根据提供的配置设置其属性。具体来说:
- 初始化属性:根据配置设置名称、范围、数据目录、数据库等属性。
- 获取版本信息:获取 PostgreSQL 的主要版本号。
- 设置状态:初始化状态锁,并设置初始状态为
'stopped'
。 - 创建连接池:创建一个连接池,并从中获取一个名为
'heartbeat'
的连接。 - 初始化处理程序:创建多个处理程序实例,如 MPP 处理程序、配置处理器、引导程序、插槽处理器、同步处理器等。
- 初始化回调执行器:创建回调执行器实例,并初始化回调相关的标志。
- 初始化子进程:创建一个可取消的子进程实例。
- 设置角色和重试策略:设置 PostgreSQL 角色,并创建重试策略实例。
- 初始化时间戳和集群信息:初始化状态进入时间戳,并设置集群信息状态。
- 初始化最后一个已知的运行中的进程:初始化最后一个已知的运行中的进程引用。
- 检查运行状态:如果 PostgreSQL 正在运行,则进一步检查其状态,并根据需要加载当前服务器参数和替换配置文件。
- 设置角色:根据 PostgreSQL 的运行状态设置其角色。
通过这些步骤,构造函数确保了 PostgreSQL 对象在创建时已经根据配置进行了适当的初始化,并准备好了后续的操作。
1.12.4.1.2 get_major_version()
- 定义了一个名为
get_major_version
的实例方法,该方法没有参数,并返回一个整数类型的结果。
def get_major_version(self) -> int:
"""从 PG_VERSION 文件读取主要版本。
:returns: 以整数格式返回的主要 PostgreSQL 版本号,或者在文件缺失或发生错误时返回 0。"""
# 检查版本文件是否存
if self._version_file_exists():
# 如果版本文件存在,则尝试打开文件,并读取其内容,去除两端空白字符后,转换为主要版本号的整数形式并返回
try:
with open(self._version_file) as f:
return postgres_major_version_to_int(f.read().strip())
except Exception:
logger.exception('Failed to read PG_VERSION from %s', self._data_dir)
return 0
作用:
这个方法的作用是从 PostgreSQL 数据目录中的 PG_VERSION
文件读取 PostgreSQL 的主要版本号,并返回该版本号的整数形式。具体来说:
- 检查文件:首先检查
PG_VERSION
文件是否存在。 - 读取文件:如果文件存在,则打开文件并读取其内容,去除空白字符后转换为主要版本号的整数形式。
- 异常处理:如果在读取文件的过程中发生任何异常,则记录异常信息并通过返回
0
来指示失败。 - 返回默认值:如果文件不存在或者读取失败,则返回
0
作为默认值。
1.12.4.1.3 _version_file_exists()
- 定义了一个名为
_version_file_exists
的实例方法,该方法没有参数,并返回一个布尔值。
def _version_file_exists(self) -> bool:
return not self.data_directory_empty() and os.path.isfile(self._version_file)
作用:
这个方法的作用是检查 PostgreSQL 数据目录中的 PG_VERSION
文件是否存在。具体来说:
- 检查数据目录:通过调用
self.data_directory_empty()
方法确定数据目录是否为空。 - 检查文件存在性:使用
os.path.isfile()
方法检查PG_VERSION
文件是否存在。 - 返回结果:只有当数据目录不为空并且
PG_VERSION
文件存在时,该方法才会返回True
,否则返回False
。
1.12.4.1.4 pg_control_exists()
- 定义了一个名为
pg_control_exists
的实例方法,该方法没有参数,并返回一个布尔值。
def pg_control_exists(self) -> bool:
return os.path.isfile(self._pg_control)
作用:
这个方法的作用是检查 PostgreSQL 数据目录中的 pg_control
文件是否存在。具体来说:
- 检查文件存在性:使用
os.path.isfile()
方法检查_pg_control
文件是否存在。 - 返回结果:如果
_pg_control
文件存在,则返回True
,否则返回False
。
1.12.4.1.5 set_state()
- 定义了一个名为
set_state
的实例方法,该方法接受一个类型为str
的参数value
,并返回None
。 - 此方法用于设置实例的状态,并记录状态进入的时间戳。
def set_state(self, value: str) -> None:
# 使用 with 语句获得对 _state_lock 锁的独占访问权,确保在修改状态时不会发生竞态条件
with self._state_lock:
self._state = value
self._state_entry_timestamp = time.time()
作用:
这个方法的作用是设置实例的状态,并记录状态进入的时间戳。具体来说:
- 获取锁:首先通过
_state_lock
锁确保在设置状态时不会与其他线程冲突。 - 设置状态:将实例的状态设置为传入的
value
。 - 记录时间戳:记录状态进入的时间戳为当前时间。
1.12.4.1.6 data_directory_is_empty()
- 定义了一个名为
data_directory_is_empty
的函数,该函数接受一个类型为str
的参数data_dir
,并返回一个布尔值。
def data_directory_is_empty(data_dir: str) -> bool:
"""检查 PostgreSQL 数据目录是否为空。
.. note::
在非 Windows 环境中,如果 *data_dir* 只包含隐藏文件和/或 "lost+found" 目录,也被认为是空的。
:param data_dir: 要检查的 PostgreSQL 数据目录。
:returns: 如果 *data_dir* 是空的,则返回 True。
"""
if not os.path.exists(data_dir):
return True
return all(os.name != 'nt' and (n.startswith('.') or n == 'lost+found') for n in os.listdir(data_dir))
作用:
这个函数的作用是检查指定的 PostgreSQL 数据目录是否为空。具体来说:
- 检查目录存在性:首先检查提供的数据目录是否存在。
- 返回目录不存在的情况:如果目录不存在,则直接返回
True
,表示该目录为空。 - 检查目录内容:如果目录存在,则检查目录内的内容。
- 对于非 Windows 环境,如果目录内只有隐藏文件和/或
lost+found
目录,则认为该目录为空。 - 对于 Windows 环境,只检查目录是否存在,而不考虑其内容。
- 对于非 Windows 环境,如果目录内只有隐藏文件和/或
1.12.4.1.7 get_postgres_role_from_data_directory()
- 定义了一个名为
get_postgres_role_from_data_directory
的方法,该方法没有参数(除了隐式的self
参数),并且返回一个字符串。
def get_postgres_role_from_data_directory(self) -> str:
# 数据目录是否为空,或者数据目录中的 controldata 是否不存在或无效
if self.data_directory_empty() or not self.controldata():
# 数据库尚未初始化
return 'uninitialized'
# 数据目录不是空的,并且存在恢复配置文件
elif self.config.recovery_conf_exists():
# 备节点
return 'replica'
# 都不满足
else:
# 主节点
return 'primary'
作用:
get_postgres_role_from_data_directory
方法的作用是通过检查 PostgreSQL 数据目录的状态来确定当前 PostgreSQL 实例的角色。具体来说:
- 检查数据目录是否为空或未初始化:
- 如果数据目录为空(
data_directory_empty
返回True
),或者数据目录中的controldata
无效(controldata
返回False
),则认为当前实例还未初始化,返回'uninitialized'
。
- 如果数据目录为空(
- 检查是否存在恢复配置:
- 如果数据目录不是空的,并且存在恢复配置文件(
recovery_conf_exists
返回True
),则认为当前实例是一个备节点,返回'replica'
。
- 如果数据目录不是空的,并且存在恢复配置文件(
- 默认为主节点:
- 如果数据目录既不是空的,也没有恢复配置文件的存在,则认为当前实例是一个主节点,返回
'primary'
。
- 如果数据目录既不是空的,也没有恢复配置文件的存在,则认为当前实例是一个主节点,返回
1.12.4.1.8 data_dir()
- 定义了一个名为
data_dir
的属性(property),该属性返回一个字符串。
@property
def data_dir(self) -> str:
return self._data_dir
作用:
data_dir
属性的作用是提供对内部状态 _data_dir
的只读访问。具体来说:
- 封装内部状态:
- 使用
@property
装饰器来封装_data_dir
的访问逻辑,使得外部代码可以像访问普通属性一样使用data_dir
。
- 使用
- 提供只读访问:
- 由于
data_dir
是一个只读属性,因此不允许直接修改它的值。这有助于保护内部状态不受外部修改的影响。
- 由于
- 返回数据目录路径:
- 返回 PostgreSQL 数据目录的实际路径,该路径对于许多数据库操作都是必需的,例如启动、停止数据库服务,备份或恢复数据等。
通过这种方式,data_dir
属性提供了一种简单、安全的方式来访问 PostgreSQL 数据目录的位置,同时保持了封装性,隐藏了具体的实现细节。
1.12.4.1.9 data_directory_empty()
- 定义了一个名为
data_directory_empty
的方法,该方法没有参数(除了隐式的self
参数),并且返回一个布尔值。
def data_directory_empty(self) -> bool:
# 查 PostgreSQL 数据目录中是否存在 pg_control 文件
if self.pg_control_exists():
return False
return data_directory_is_empty(self._data_dir)
作用:
data_directory_empty
方法的作用是检查 PostgreSQL 数据目录是否为空。具体来说:
- 检查
pg_control
文件是否存在:- 通过调用
pg_control_exists
方法检查数据目录中是否存在pg_control
文件。 - 如果
pg_control
文件存在,说明数据目录中有至少一个重要的控制文件,因此数据目录不为空,返回False
。
- 通过调用
- 进一步检查数据目录是否为空:
- 如果
pg_control
文件不存在,则调用data_directory_is_empty
方法来进一步检查数据目录是否为空。 - 返回
data_directory_is_empty
方法的结果,如果数据目录为空,则返回True
;否则返回False
。
- 如果
1.12.4.1.10 controldata()
- 定义了一个名为
controldata
的方法,该方法没有参数(除了隐式的self
参数),并且返回一个字典类型的数据。
def controldata(self) -> Dict[str, str]:
""" 返回 pg_controldata 的内容,如果调用 pg_controldata 失败则返回非真值。 """
# 在备份恢复期间不要尝试调用 pg_controldata。
if self._version_file_exists() and self.state != 'creating replica':
try:
# 尝试调用 pg_controldata 命令,并传递当前的数据目录路径
# 设置环境变量 LANG 和 LC_ALL 为 C ,以确保输出格式一致。
env = {**os.environ, 'LANG': 'C', 'LC_ALL': 'C'}
data = subprocess.check_output([self.pgcommand('pg_controldata'), self._data_dir], env=env)
# 如果 data 非空,则解码 data 并按行分割
if data:
data = filter(lambda e: ':' in e, data.decode('utf-8').splitlines())
# pg_controldata output depends on major version. Some of parameters are prefixed by 'Current '
return {k.replace('Current ', '', 1): v.strip() for k, v in map(lambda e: e.split(':', 1), data)}
except subprocess.CalledProcessError:
logger.exception("Error when calling pg_controldata")
return {}
作用:
controldata
方法的作用是从 PostgreSQL 数据目录中提取 pg_controldata
的输出内容,并将其转换为一个字典,便于后续处理。具体来说:
- 检查状态和版本文件:
- 确认当前不是在创建副本的状态,并且版本文件存在。
- 调用
pg_controldata
命令:- 使用
subprocess.check_output
方法调用pg_controldata
命令,并传递数据目录路径。 - 设置环境变量
LANG
和LC_ALL
为C
,以确保输出的一致性。
- 使用
- 解析输出结果:
- 解码命令的输出,并按行分割。
- 过滤出包含冒号
:
的行,表示有效的键值对。 - 将每行分割成键值对,并去除键前面的 "Current " 字样(如果存在),最终形成一个字典。
- 异常处理:
- 如果调用
pg_controldata
出错,则捕获异常并记录错误信息。 - 如果没有达到调用
pg_controldata
的条件或者发生其他错误,则返回一个空字典。
- 如果调用
1.12.4.1.11 recovery_conf_exists()
- 定义了一个名为
recovery_conf_exists
的方法,该方法没有参数(除了隐式的self
参数),并且返回一个布尔值。
def recovery_conf_exists(self) -> bool:
# PostgreSQL 12 或更高版本
if self._postgresql.major_version >= 120000:
# 如果 PostgreSQL 版本为 12 或更高,那么检查 standby.signal 或 recovery.signal 文件是否存在
return os.path.exists(self._standby_signal) or os.path.exists(self._recovery_signal)
# 则直接检查 _recovery_conf 文件是否存在
return os.path.exists(self._recovery_conf)
作用:
recovery_conf_exists
方法的作用是检查 PostgreSQL 数据目录中是否存在恢复配置相关的文件。具体来说:
- 版本检查:
- 首先,检查 PostgreSQL 的主版本号是否大于或等于
120000
(即 PostgreSQL 12 或更高版本)。
- 首先,检查 PostgreSQL 的主版本号是否大于或等于
- 检查恢复信号文件:
- 如果 PostgreSQL 版本为 12 或更高,那么检查
standby.signal
或recovery.signal
文件是否存在。这两个文件用于指示 PostgreSQL 应该从恢复模式进入正常模式或从备用模式进入主模式。 - 如果任一信号文件存在,则返回
True
,表示存在恢复配置;否则返回False
。
- 如果 PostgreSQL 版本为 12 或更高,那么检查
- 检查恢复配置文件:
- 如果 PostgreSQL 版本低于 12,则直接检查
_recovery_conf
文件是否存在。该文件包含了恢复过程中的配置信息。 - 如果
_recovery_conf
文件存在,则返回True
,表示存在恢复配置;否则返回False
。
- 如果 PostgreSQL 版本低于 12,则直接检查
1.12.4.1.12 pgcommand()
- 定义了一个名为
pgcommand
的方法,该方法接受一个字符串参数cmd
,并且返回一个字符串。
def pgcommand(self, cmd: str) -> str:
"""返回指定的 PostgreSQL 命令的路径。
.. note::
如果用户配置了 ``postgresql.bin_name.*cmd*``,则使用该二进制文件名,否则使用默认的二进制文件名 *cmd*。
:param cmd: 要获取路径的 Postgres 二进制文件名。
:returns: Postgres 二进制文件 *cmd* 的路径。
"""
return os.path.join(self._bin_dir, (self.config.get('bin_name', {}) or EMPTY_DICT).get(cmd, cmd))
作用:
pgcommand
方法的作用是返回指定 PostgreSQL 命令的完整路径。具体来说:
- 获取二进制文件路径:
- 该方法接收一个
cmd
参数,代表 PostgreSQL 的某个命令名称,如pg_dump
、pg_controldata
等。
- 该方法接收一个
- 查找命令别名:
- 检查配置项
self.config.get('bin_name', {})
是否为该命令cmd
提供了别名。如果有别名,则使用别名;如果没有,则使用默认的命令名cmd
。
- 检查配置项
- 组合路径:
- 使用
os.path.join
方法组合二进制文件所在的目录路径self._bin_dir
和命令名称,生成完整的命令路径。
- 使用
1.12.4.1.12 is_running()
- 定义了一个名为
is_running
的方法,该方法没有参数(除了隐式的self
参数),并且返回一个Optional[PostmasterProcess]
类型的对象,即可能返回一个PostmasterProcess
对象,也可能返回None
。
def is_running(self) -> Optional[PostmasterProcess]:
"""如果数据目录上有一个正在运行的 postmaster 进程,则返回 PostmasterProcess 对象,否则返回 None。如果最近看 到的进程仍在运行,则根据 pid 文件更新缓存的进程。"""
# 检查 _postmaster_proc 是否已经存在
if self._postmaster_proc:
if self._postmaster_proc.is_running():
return self._postmaster_proc
self._postmaster_proc = None
# 如果注意到 postgres 重启了,则强制同步复制槽并检查逻辑槽。
self.slots_handler.schedule()
# 创建一个新的 PostmasterProcess 对象,该对象是从数据目录中的 PID 文件中读取的
self._postmaster_proc = PostmasterProcess.from_pidfile(self._data_dir)
return self._postmaster_proc
作用:
is_running
方法的作用是检查 PostgreSQL 的 postmaster
进程是否正在运行,并返回相应的 PostmasterProcess
对象。具体来说:
- 检查缓存的
postmaster
进程:- 首先检查
_postmaster_proc
是否已经有缓存的PostmasterProcess
对象。 - 如果缓存的进程还在运行,则直接返回该进程对象。
- 如果缓存的进程不再运行,则将
_postmaster_proc
设为None
,表示需要重新检查。
- 首先检查
- 处理复制槽和逻辑槽:
- 如果注意到 PostgreSQL 重启了,则强制同步复制槽(replication slots)并检查逻辑槽(logical slots)。
- 从 PID 文件读取进程信息:
- 如果缓存的
postmaster
进程不再存在或不再运行,则从数据目录中的 PID 文件创建一个新的PostmasterProcess
对象。 - 创建完成后,将新的
PostmasterProcess
对象赋值给_postmaster_proc
,并返回这个对象。
- 如果缓存的
1.12.4.1.13 check_startup_state_changed()
- 定义了一个名为
check_startup_state_changed
的方法,该方法没有参数(除了隐式的self
参数),并且返回一个布尔值。
def check_startup_state_changed(self) -> bool:
"""检查 PostgreSQL 是否已完成启动、失败或仍在启动中。
仅当状态为 'starting' 时应调用此方法。
:returns 如果状态已从 'starting' 改变,则返回 True 。
"""
# 调用 pg_isready 方法来检查 PostgreSQL 的状态
ready = self.pg_isready()
# 正在启动或停止
if ready == STATE_REJECT:
return False
# 没有响应
elif ready == STATE_NO_RESPONSE:
ret = not self.is_running()
if ret:
self.set_state('start failed')
self.slots_handler.schedule(False) # TODO: can remove this?
self.config.save_configuration_files(True) # TODO: maybe remove this?
return ret
# 检查是否为 STATE_RUNNING
else:
if ready != STATE_RUNNING:
# 错误的配置或意外的操作系统错误。不知道PostgreSQL的状态。
# 让运行周期的主循环清理混乱。
logger.warning("%s status returned from pg_isready",
"Unknown" if ready == STATE_UNKNOWN else "Invalid")
# 如果 ready 状态为 STATE_RUNNING,则设置状态为 'running',安排复制槽的同步,并保存配置文件
self.set_state('running')
self.slots_handler.schedule()
self.config.save_configuration_files(True)
# PostgreSQL自动重启后 TODO: __cb_pending可以设置为None。我们要调用回调吗?
#以前我们甚至没有注意到。
action = self.__cb_pending or CallbackAction.ON_START
self.call_nowait(action)
self.__cb_pending = None
return True
作用:
check_startup_state_changed
方法的作用是检查 PostgreSQL 的启动状态是否发生了改变。具体来说:
- 调用
pg_isready
方法:- 通过调用
pg_isready
方法来检查 PostgreSQL 的当前状态。
- 通过调用
- 根据状态进行处理:
- 如果状态为
STATE_REJECT
,则返回False
表示状态尚未改变。 - 如果状态为
STATE_NO_RESPONSE
,则检查是否有PostmasterProcess
在运行。如果没有运行,则设置状态为 'start failed',并返回True
表示状态发生了变化;否则返回False
表示状态没有变化。 - 如果状态为
STATE_RUNNING
或其他有效状态,则设置状态为 'running',安排复制槽的同步,并保存配置文件。调用启动回调,并返回True
表示状态已从 'starting' 改变为 'running'。
- 如果状态为
- 异常处理:
- 如果
ready
状态既不是STATE_REJECT
也不是STATE_NO_RESPONSE
且不是STATE_RUNNING
,则记录警告日志,表示 PostgreSQL 的状态未知或无效。
- 如果
1.12.4.1.14 pg_isready()
- 定义了一个名为
pg_isready
的方法,该方法没有参数(除了隐式的self
参数),并且返回一个字符串。
def pg_isready(self) -> str:
"""运行 pg_isready 来查看 PostgreSQL 是否接受连接。
:returns 如果 PostgreSQL 已启动,则返回 'ok';如果正在启动中,则返回 'reject';如果没有启动,则返回 'no_response'。"""
# 从 connection_pool 中获取连接参数
r = self.connection_pool.conn_kwargs
# 构建 pg_isready 命令的基本参数
cmd = [self.pgcommand('pg_isready'), '-p', r['port'], '-d', self._database]
# 如果我们通过默认的unix套接字连接,则不设置主机
# 如果连接参数中包含 host,则添加 -h 参数和对应的主机地址
if 'host' in r:
cmd.extend(['-h', r['host']])
# 我们只需要用户名,因为pg_isready不尝试进行身份验证
# 如果连接参数中包含 user,则添加 -U 参数和对应的用户名
if 'user' in r:
cmd.extend(['-U', r['user']])
# 使用 subprocess.call 执行构建好的 pg_isready 命令,并获取返回码
ret = subprocess.call(cmd)
# 定义一个映射表,将 pg_isready 命令的返回码映射到相应的状态字符串
return_codes = {0: STATE_RUNNING,
1: STATE_REJECT,
2: STATE_NO_RESPONSE,
3: STATE_UNKNOWN}
return return_codes.get(ret, STATE_UNKNOWN)
作用:
pg_isready
方法的作用是检查 PostgreSQL 数据库是否已经准备好接受连接。具体来说:
- 构建
pg_isready
命令:- 根据连接参数构建
pg_isready
命令,包括数据库名称、端口、主机地址(如果存在)、用户名(如果存在)。
- 根据连接参数构建
- 执行
pg_isready
命令:- 使用
subprocess.call
执行构建好的命令,并获取返回码。
- 使用
- 解析返回码:
- 根据
pg_isready
命令的返回码映射到相应的状态字符串,并返回该状态字符串。
- 根据
通过这些步骤,pg_isready
方法能够有效地检测 PostgreSQL 数据库的状态,并根据不同的状态返回相应的标识。这对于监控数据库服务的健康状况非常有用,特别是在需要确认数据库是否可以接受连接的情况下。如果过程中出现了任何问题,该方法也会返回一个默认的未知状态。
1.12.4.1.15 call_nowait()
- 定义了一个名为
call_nowait
的方法,它接受一个CallbackAction
类型的参数cb_type
,并且没有返回值。
def call_nowait(self, cb_type: CallbackAction) -> None:
"""选择一个回调命令并调用它而不等待其完成。 """
# 如果 bootstrapping 属性为 True,则直接返回
if self.bootstrapping:
return
# 如果 cb_type 是 ON_START、ON_STOP、ON_RESTART 或 ON_ROLE_CHANGE 中的一个,则将 __cb_called 设置为 True
if cb_type in (CallbackAction.ON_START, CallbackAction.ON_STOP,
CallbackAction.ON_RESTART, CallbackAction.ON_ROLE_CHANGE):
self.__cb_called = True
# 如果 callback 属性不为 None 并且 cb_type 存在于 callback 字典中,则继续执行回调命令
if self.callback and cb_type in self.callback:
# 获取 callback 字典中对应 cb_type 的命令,并根据当前的角色(role)设置其值
cmd = self.callback[cb_type]
role = 'primary' if self.role == 'promoted' else self.role
try:
# 尝试将命令拆分为列表,并追加 cb_type、role 和 scope 到命令列表的末尾
cmd = shlex.split(self.callback[cb_type]) + [cb_type, role, self.scope]
# 使用 _callback_executor 执行命令
self._callback_executor.call(cmd)
except Exception:
logger.exception('callback %s %r %s %s failed', cmd, cb_type, role, self.scope)
作用:
call_nowait
方法的作用是在指定的情况下执行回调命令,并且不等待命令执行完毕。具体来说:
- 检查引导状态:
- 如果当前正在引导(
bootstrapping
),则不执行任何回调。
- 如果当前正在引导(
- 标记回调已调用:
- 如果
cb_type
表示的是启动、停止、重启或角色变更,则标记__cb_called
为True
,表明回调已被调用。
- 如果
- 检查回调配置:
- 如果存在有效的回调配置,并且
cb_type
已经在配置中注册,则准备执行回调命令。
- 如果存在有效的回调配置,并且
- 执行回调命令:
- 将回调命令拆解成命令列表,并追加回调类型、当前角色以及作用域信息。
- 使用
_callback_executor
来执行命令。 - 如果执行过程中出现异常,则记录异常信息。
通过这种方式,call_nowait
方法能够在特定条件下执行预定义的回调命令,这对于在 PostgreSQL 集群的状态发生变化时触发外部脚本或程序非常有用。这种方法还避免了因等待回调命令执行完毕而导致的阻塞,提高了系统的响应速度。
1.12.4.1.16 callback()
- 定义了一个名为
callback
的属性(property),该属性返回一个字典,字典的键为字符串,值也为字符串。
@property
def callback(self) -> Dict[str, str]:
return self.config.get('callbacks', {}) or {}
作用:
callback
属性的作用是从配置中获取回调命令的字典。具体来说:
- 封装配置访问:
- 使用
@property
装饰器来封装对配置中'callbacks'
键的访问逻辑,使得外部代码可以像访问普通属性一样使用callback
。
- 使用
- 提供默认值:
- 如果配置中不存在
'callbacks'
键或者该键对应的值为None
,则返回一个空字典{}
,确保不会因为配置缺失而导致程序错误。
- 如果配置中不存在
- 返回回调命令字典:
- 返回配置中存储的回调命令字典,该字典包含了不同类型的回调命令及其对应的命令字符串。
通过这种方式,callback
属性提供了一种简单、安全的方式来访问配置中的回调命令,同时保持了封装性,隐藏了具体的实现细节。这有助于在需要执行回调命令时,从配置中快速获取相应的命令,并确保即使配置缺失也不会影响程序的正常运行。
1.12.4.1.17 query()
- 定义了一个名为
query
的方法,它接受一个 SQL 语句sql
、可变数量的参数params
以及一个布尔型参数retry
(默认为True
),并返回一个列表,列表中的每个元素都是一个包含查询结果的元组。
def query(self, sql: str, *params: Any, retry: bool = True) -> List[Tuple[Any, ...]]:
"""执行 *sql* 查询语句,并可选地返回结果。
:param sql: 要执行的 SQL 语句。
:param params: 要传递的参数。
:param retry: 查询失败时是否应该重试或立即放弃。
:returns: 如果有结果,返回一个作为元组列表的查询响应。
:raises:
:exc:`~psycopg.Error` 如果在执行 *sql* 时出现问题。
:exc:`~patroni.exceptions.PostgresConnectionException` 如果在连接数据库时出现问题。
:exc:`~patroni.utils.RetryFailedError` 如果检测到连接/查询失败是由于 PostgreSQL 重启造成的,或重试截止时 间已过。
"""
# 如果 retry 为 False,则直接调用 _query 方法执行 SQL 语句,并返回结果
if not retry:
return self._query(sql, *params)
# 如果 retry 为 True,则尝试使用 retry 方法来执行 _query 方法,这样可以处理查询失败的情况,并在必要时重试
try:
return self.retry(self._query, sql, *params)
except RetryFailedError as exc:
raise PostgresConnectionException(str(exc)) from exc
作用:
query
方法的作用是执行 SQL 查询,并返回查询结果。此外,该方法提供了重试机制,以应对可能发生的临时性故障或网络问题。具体来说:
- 直接执行查询:
- 如果
retry
为False
,则直接执行 SQL 查询,并返回结果。
- 如果
- 重试执行查询:
- 如果
retry
为True
,则使用retry
方法来执行 SQL 查询,这意味着如果查询失败,将会尝试重新执行。 - 如果重试仍然失败,并且达到了重试的上限或检测到由于 PostgreSQL 重启导致的失败,则抛出
RetryFailedError
。
- 如果
- 异常处理:
- 如果在重试过程中遇到
RetryFailedError
,则转换为PostgresConnectionException
并抛出,以便上层逻辑可以统一处理数据库连接相关的异常。
- 如果在重试过程中遇到
通过这种方式,query
方法不仅执行了 SQL 查询,而且还提供了一种机制来处理查询失败的情况,确保了应用程序的健壮性和可用性。
1.12.4.1.18 _query()
- 定义了一个名为
_query
的私有方法,它接受一个 SQL 语句sql
和可变数量的参数params
,并返回一个列表,列表中的每个元素都是一个包含查询结果的元组。
def _query(self, sql: str, *params: Any) -> List[Tuple[Any, ...]]:
"""执行 *sql* 查询语句,并可选地返回结果。
:param sql: 要执行的 SQL 语句。
:param params: 要传递的参数。
:returns: 如果有结果,返回一个作为元组列表的查询响应。
:raises:
:exc:`~psycopg.Error` 如果在执行 *sql* 时出现问题。
:exc:`~patroni.exceptions.PostgresConnectionException` 如果在连接数据库时出现问题。
:exc:`~patroni.utils.RetryFailedError` 如果检测到连接/查询失败是由于 PostgreSQL 重启造成的。
"""
# 尝试使用 _connection 对象执行 SQL 查询,并传递参数 params。如果查询成功,则返回查询结果
try:
return self._connection.query(sql, *params)
except PostgresConnectionException as exc:
if self.state == 'restarting':
raise RetryFailedError('cluster is being restarted') from exc
raise
作用:
_query
方法的作用是执行 SQL 查询,并返回查询结果。此外,该方法处理了执行查询时可能遇到的异常情况。具体来说:
- 执行查询:
- 尝试使用
_connection
对象执行 SQL 查询,并传递参数params
。 - 如果查询成功,则返回查询结果。
- 尝试使用
- 处理异常:
- 如果在执行查询时遇到了
PostgresConnectionException
,则检查当前对象的状态。 - 如果状态为
'restarting'
,则认为当前集群正在重启,此时抛出RetryFailedError
异常,表明不应该再尝试重试查询。 - 如果状态不是
'restarting'
,则直接重新抛出捕获到的异常,让调用者处理。
- 如果在执行查询时遇到了
通过这种方式,_query
方法不仅执行了 SQL 查询,而且还处理了执行查询时可能出现的异常情况,确保了查询逻辑的健壮性。
1.12.4.1.19 is_primary()
- 定义了一个名为
is_primary
的方法,它没有参数,并返回一个布尔值。
def is_primary(self) -> bool:
try:
# 尝试从当前连接中获取有关集群的信息,并通过检查 timeline 属性来判断当前节点是否为主节点
return bool(self._cluster_info_state_get('timeline'))
except PostgresConnectionException:
logger.warning('Failed to determine PostgreSQL state from the connection, falling back to cached role')
return bool(self.is_running() and self.role == 'primary')
作用:
is_primary
方法的作用是确定当前数据库实例是否为主节点。如果能够成功获取到集群的 timeline
信息,则认为当前节点为主节点。如果获取 timeline
失败,则会使用缓存的角色信息来判断是否为主节点。具体来说:
- 尝试确定主节点:
- 尝试通过
_cluster_info_state_get('timeline')
获取集群信息,并通过检查timeline
属性来判断是否为主节点。
- 尝试通过
- 异常处理:
- 如果在获取集群信息时出现
PostgresConnectionException
异常,记录一条警告日志。 - 使用缓存的角色信息来判断是否为主节点。这涉及到检查实例是否正在运行 (
self.is_running()
) 并且角色为primary
(self.role == 'primary'
)。
- 如果在获取集群信息时出现
通过这种方式,is_primary
方法不仅尝试从当前连接中确定主节点身份,还在连接不可靠的情况下提供了回退机制,保证了判断逻辑的健壮性。
1.12.4.1.20 _cluster_info_state_get()
- 定义了一个名为
_cluster_info_state_get
的方法,它接受一个字符串类型的参数name
,用于指定要获取的集群状态信息的名称,并返回一个可选项Optional[Any]
。
def _cluster_info_state_get(self, name: str) -> Optional[Any]:
# 检查 _cluster_info_state 是否为空或未初始化
if not self._cluster_info_state:
# 如果 _cluster_info_state 没有数据,则尝试通过 _is_leader_retry 方法执行查询,并获取集群信息
try:
result = self._is_leader_retry(self._query, self.cluster_info_query)[0]
# 使用查询结果创建一个字典
cluster_info_state = dict(zip(['timeline', 'wal_position', 'replayed_location',
'received_location', 'replay_paused', 'pg_control_timeline',
'received_tli', 'slot_name', 'conninfo', 'receiver_state',
'restore_command', 'slots', 'synchronous_commit',
'synchronous_standby_names', 'pg_stat_replication'], result))
# 如果需要查询 slots 并且可以推进 slots,则更新 cluster_info_state 中的 slots 字段
if self._should_query_slots and self.can_advance_slots:
cluster_info_state['slots'] =\
self.slots_handler.process_permanent_slots(cluster_info_state['slots'])
# 将创建的 cluster_info_state 字典赋值给 _cluster_info_state
self._cluster_info_state = cluster_info_state
except RetryFailedError as e: # 查询失败两次
self._cluster_info_state = {'error': str(e)}
if not self.is_starting() and self.pg_isready() == STATE_REJECT:
self.set_state('starting')
# 检查 _cluster_info_state 是否包含错误信息
if 'error' in self._cluster_info_state:
raise PostgresConnectionException(self._cluster_info_state['error'])
return self._cluster_info_state.get(name)
作用:
_cluster_info_state_get
方法的作用是从当前的集群信息状态中获取指定名称的数据项。具体来说:
- 获取集群状态信息:
- 如果
_cluster_info_state
尚未初始化,则尝试从数据库中获取相关信息,并存储在_cluster_info_state
中。 - 如果获取过程中发生错误,则记录错误信息,并可能改变当前实例的状态。
- 如果
- 异常处理:
- 如果
_cluster_info_state
中包含错误信息,则抛出异常。
- 如果
- 返回数据项:
- 从
_cluster_info_state
中获取请求的数据项并返回。
- 从
通过这种方式,_cluster_info_state_get
方法不仅提供了获取特定集群状态的方法,还在数据获取失败时提供了错误处理机制,确保了逻辑的健壮性。
1.12.4.1.21 is_starting()
- 定义了一个名为
is_starting
的方法,它没有参数,并且返回一个布尔值。
def is_starting(self) -> bool:
return self.state == 'starting'
作用:
这个函数的作用是确定当前实例是否处于启动 (starting
) 状态。具体来说:
- 状态检查:
- 当调用
is_starting
方法时,它会检查实例的当前状态self.state
。 - 如果
self.state
的值是'starting'
,则返回True
,表示实例正处于启动过程中。 - 否则,返回
False
,表示实例不处于启动状态。
- 当调用
- 应用背景:
- 这种类型的方法通常用于监控服务或者组件的状态,尤其是在分布式系统或数据库管理系统中。
- 在这些系统中,了解某个实例是否正在启动是非常重要的,因为它可以帮助其他组件做出决策,如等待该实例启动完成再进行下一步操作,或者避免在实例启动期间执行一些可能会干扰启动过程的操作。
通过这种方式,is_starting
方法提供了一个简洁的方式来判断实例是否处于启动状态,这对于系统的管理和协调有着重要的意义。
1.12.4.1.22 reload()
- 定义了一个名为
reload
的方法,它接受一个布尔类型的参数block_callbacks
(默认值为False
),并返回一个布尔值。
def reload(self, block_callbacks: bool = False) -> bool:
# 调用 self.pg_ctl 方法,传入 'reload' 作为参数
ret = self.pg_ctl('reload')
# 如果 pg_ctl 的返回值为 True 并且 block_callbacks 为 False
if ret and not block_callbacks:
self.call_nowait(CallbackAction.ON_RELOAD)
return ret
作用:
reload
方法的主要作用是重新加载 PostgreSQL 服务的配置文件,并在重载成功后触发相应的回调。
具体来说:
- 重载配置:
- 调用
pg_ctl
方法发送一个重新加载配置的命令。如果命令执行成功,ret
变量将为True
;否则为False
。
- 调用
- 触发回调:
- 如果
block_callbacks
参数为False
,并且pg_ctl
成功执行了重载命令(即ret
为True
),那么将调用call_nowait
方法,并传入CallbackAction.ON_RELOAD
,这会触发一个关于配置重载成功的回调事件。
- 如果
- 返回结果:
- 最终返回
pg_ctl
方法的结果,表明重载配置是否成功。
- 最终返回
这个方法通常在需要动态更新 PostgreSQL 配置而不需要重启整个服务的情况下使用。例如,更改了 postgresql.conf
文件中的某些参数之后,可以通过调用此方法来使新的配置生效。同时,如果需要在配置重载后执行某些动作(如通知其他服务或组件),可以通过设置 block_callbacks
参数为 False
来允许触发回调事件。
1.12.4.1.23 pg_ctl()
- 定义了一个名为
pg_ctl
的方法,它接受三个参数:cmd
:一个字符串类型的参数,代表pg_ctl
命令要执行的操作。*args
:可变数量的位置参数,代表传递给pg_ctl
命令的额外参数。**kwargs
:关键字参数,代表传递给subprocess.call
方法的额外选项。
def pg_ctl(self, cmd: str, *args: str, **kwargs: Any) -> bool:
"""构建并执行pg_ctl命令
返回:”!True '当return_code == 0时,否则' !假的"""
# 构建 pg_ctl 命令的基本部分,包括 pg_ctl 命令本身及其子命令 cmd
pg_ctl = [self.pgcommand('pg_ctl'), cmd]
return subprocess.call(pg_ctl + ['-D', self._data_dir] + list(args), **kwargs) == 0
作用:
pg_ctl
方法的主要作用是构建并执行 pg_ctl
命令来控制 PostgreSQL 数据库服务。具体来说:
- 构建命令:
- 根据传入的
cmd
参数构建pg_ctl
命令的基本形式。 - 添加
-D
参数来指定 PostgreSQL 的数据目录self._data_dir
。 - 将任意数量的位置参数
args
添加到命令行中。
- 根据传入的
- 执行命令:
- 使用
subprocess.call
来执行构建好的pg_ctl
命令,并传递任何关键字参数kwargs
给subprocess.call
。 subprocess.call
返回命令的退出码。
- 使用
- 返回结果:
- 如果
subprocess.call
的返回值(即命令的退出码)为0
,则表示命令执行成功,返回True
。 - 否则,如果退出码不为
0
,表示命令执行失败,返回False
。
- 如果
这个方法使得可以方便地通过 Python 脚本来控制 PostgreSQL 服务,比如启动、停止、重载配置等操作。通过这种方法,可以简化对 PostgreSQL 服务的管理,并且能够在脚本中灵活地处理命令执行的结果。
1.12.4.1.24 pgcommand()
- 定义了一个名为
pgcommand
的方法,它接受一个字符串类型的参数cmd
,并返回一个字符串。
def pgcommand(self, cmd: str) -> str:
"""返回指定的 PostgreSQL 命令的路径。
.. note::
如果用户配置了 ``postgresql.bin_name.*cmd*``,则使用该二进制文件名,否则使用默认的二进制文件名 *cmd*。
:param cmd: 要获取路径的 Postgres 二进制文件名。
:returns: Postgres 二进制文件 *cmd* 的路径。
"""
return os.path.join(self._bin_dir, (self.config.get('bin_name', {}) or EMPTY_DICT).get(cmd, cmd))
构造并返回 cmd
对应的 PostgreSQL 二进制命令的完整路径。
self.config.get('bin_name', {})
:从配置中获取bin_name
子配置,如果不存在则返回一个空字典。(self.config.get('bin_name', {}) or EMPTY_DICT).get(cmd, cmd)
:如果bin_name
子配置是空字典,则使用EMPTY_DICT
代替,并从中获取cmd
的值,如果不存在则返回cmd
本身。os.path.join(self._bin_dir, ...)
:使用os.path.join
方法将路径部件组合起来,形成完整的文件路径。
作用:
pgcommand
方法的具体作用是返回指定 PostgreSQL 命令的完整路径。具体来说:
- 确定二进制文件名:
- 如果用户在配置中指定了
bin_name
子配置项,并且其中包含了cmd
的值,则使用该值作为二进制文件名。 - 如果没有指定,或者
bin_name
子配置项为空字典,则直接使用cmd
作为二进制文件名。
- 如果用户在配置中指定了
- 构造路径:
- 使用
os.path.join
方法将self._bin_dir
(PostgreSQL 二进制文件所在的目录)与前面确定的二进制文件名组合起来,形成完整的路径。
- 使用
这个方法的设计目的是为了方便地获取 PostgreSQL 命令的路径,特别是当用户需要定制安装路径或二进制文件名时,可以通过配置来灵活地调整路径。这样可以提高代码的灵活性和适应性,使得在不同环境中运行时能够正确找到所需的 PostgreSQL 命令。
1.12.5 connection.py
ConnectionPool
类:
1.12.5.1 类:ConnectionPool
- 定义了一个名为
ConnectionPool
的类。 - 此帮助类用于管理从 Patroni 到 PostgreSQL 的命名连接。此类实例保留命名的
NamedConnection
对象及用于新连接的参数。
class ConnectionPool:
"""帮助类用于管理 Patroni 到 PostgreSQL 的命名连接。
此类实例保留命名的 NamedConnection 对象及用于新连接的参数。
"""
1.12.5.1.1 __init__()
- 定义了一个构造函数
__init__
,该构造函数没有参数,并返回None
。
def __init__(self) -> None:
"""创建一个 ConnectionPool 类的实例。"""
# 初始化一个锁 _lock,用于同步对连接池的操作
self._lock = Lock()
# 初始化一个字典 _connections,用于存储命名的连接对象
self._connections: Dict[str, NamedConnection] = {}
# 初始化一个字典 _conn_kwargs,用于存储创建新连接所需的参数
self._conn_kwargs: Dict[str, Any] = {}
作用:
这个类的作用是管理从 Patroni 到 PostgreSQL 的命名连接。具体来说:
- 初始化锁:创建一个锁
_lock
,用于确保在并发环境下对连接池的操作是线程安全的。 - 初始化连接字典:创建一个字典
_connections
,用于存储命名的NamedConnection
对象。 - 初始化连接参数字典:创建一个字典
_conn_kwargs
,用于存储创建新连接所需的参数。
1.12.5.1.2 get()
- 定义了一个名为
get
的实例方法,该方法接受两个参数:name
类型为str
,kwargs_override
类型为可选的Dict[str, Any]
,默认值为None
。该方法返回类型为NamedConnection
的对象。
def get(self, name: str, kwargs_override: Optional[Dict[str, Any]] = None) -> NamedConnection:
"""从连接池中获取一个新的命名的 :class:`NamedConnection` 对象。
.. note::
如果连接池中尚不存在该对象,则创建一个新的 :class:`NamedConnection` 对象。
:param name: 连接的名称。
:param kwargs_override: 包含连接参数的 :class:`dict` 对象,这些参数应不同于由 :attr:`conn_kwargs` 提供的默认 值。
:returns: :class:`NamedConnection` 对象。
"""
# 获得对 _lock 锁的独占访问权
with self._lock:
# 检查 name 是否已经存在于 _connections 字典中
if name not in self._connections:
self._connections[name] = NamedConnection(self, name, kwargs_override)
return self._connections[name]
作用:
这个方法的作用是从连接池中获取一个命名的 NamedConnection
对象。具体来说:
- 获取锁:首先通过
_lock
锁确保在检查和创建连接时不会与其他线程冲突。 - 检查连接存在性:检查请求的连接名
name
是否已经存在于_connections
字典中。 - 创建连接:如果不存在,则创建一个新的
NamedConnection
对象,并将其添加到_connections
字典中。 - 返回连接:最终返回
_connections
字典中对应name
的NamedConnection
对象。
1.12.5.2 类:NamedConnection
- 定义了一个名为
NamedConnection
的类。 - 这是一个帮助类,用于管理从 Patroni 到 PostgreSQL 的
psycopg
连接。类有一个实例变量server_version
,表示连接到的 PostgreSQL 版本号。
class NamedConnection:
"""Helper class to manage ``psycopg`` connections from Patroni to PostgreSQL.
:ivar server_version: PostgreSQL version in integer format where we are connected to.
"""
server_version: int
1.12.5.2.1 __init__()
- 定义了一个构造函数
__init__
,该构造函数接受三个参数:pool
类型为ConnectionPool
,name
类型为str
,kwargs_override
类型为可选的Dict[str, Any]
。构造函数返回None
。
def __init__(self, pool: 'ConnectionPool', name: str, kwargs_override: Optional[Dict[str, Any]]) -> None:
"""创建一个 NamedConnection 类的实例。
:param pool: 指向 ConnectionPool 对象的引用。
:param name: 连接的名称。
:param kwargs_override: 包含连接参数的 dict 对象,这些参数应不同于由连接 pool 提供的默认值。
"""
self._pool = pool
self._name = name
self._kwargs_override = kwargs_override or {}
self._lock = Lock() # used to make sure that only one connection to postgres is established
self._connection = None
作用:
这个类的作用是管理从 Patroni 到 PostgreSQL 的命名连接。具体来说:
- 初始化锁:创建一个锁
_lock
,用于确保在并发环境下对连接池的操作是线程安全的。 - 初始化连接字典:创建一个字典
_connections
,用于存储命名的NamedConnection
对象。 - 初始化连接参数字典:创建一个字典
_conn_kwargs
,用于存储创建新连接所需的参数。
1.12.5.2.2 query()
- 定义了一个名为
query
的方法,它接受一个 SQL 语句sql
和可变数量的参数params
,并返回一个列表,列表中的每个元素都是一个包含查询结果的元组。
def query(self, sql: str, *params: Any) -> List[Tuple[Any, ...]]:
"""执行带有参数的查询,并可选地返回结果。
:param sql: 要执行的 SQL 语句。
:param params: 要传递的参数。
:returns: 如果有结果,返回一个作为元组列表的查询响应。
:raises:
:exc:`~psycopg.Error` 如果在执行 *sql* 时出现问题。
:exc:`~patroni.exceptions.PostgresConnectionException` 如果在连接数据库时出现问题。
"""
cursor = None
# 使用上下文管理器 with 来确保 cursor 在使用后会被正确关闭
try:
with self.get().cursor() as cursor:
# 执行 SQL 查询,并传递参数 params
cursor.execute(sql.encode('utf-8'), params or None)
# 如果查询返回了结果,则返回所有结果作为一个列表,否则返回一个空列表
return cursor.fetchall() if cursor.rowcount and cursor.rowcount > 0 else []
except psycopg.Error as exc:
# 检查 cursor 是否存在,并且 cursor.connection.closed 的值为 0(意味着连接仍然打开)
if cursor and cursor.connection.closed == 0:
# 当通过 UNIX 套接字连接时,psycopg2 无法识别“连接丢失”,并将
# `self._connection.closed` 设置为 0,但会引发通用异常。继续使用现有连接没有意义,
# 因此我们将关闭它,以避免其被重用。
# 如果异常类型是 DatabaseError 或 OperationalError,则关闭连接;否则,重新抛出异常
if type(exc) in (psycopg.DatabaseError, psycopg.OperationalError):
self.close()
else:
raise exc
raise PostgresConnectionException('connection problems') from exc
作用:
query
方法的作用是执行 SQL 查询,并返回查询结果。此外,该方法处理了执行查询时可能遇到的异常情况。具体来说:
- 执行查询:
- 尝试使用
self.get().cursor()
获取游标并执行 SQL 查询,并传递参数params
。 - 如果查询成功且有结果,则返回查询结果。
- 尝试使用
- 处理异常:
- 如果在执行查询时遇到了
psycopg.Error
类型的异常,首先检查cursor
是否存在,并且连接是否仍然打开。 - 如果异常类型为
DatabaseError
或OperationalError
,则认为连接有问题,关闭连接。 - 如果异常类型不属于上述情况,则重新抛出异常。
- 如果上述条件都不满足,则抛出
PostgresConnectionException
异常,表明连接存在问题。
- 如果在执行查询时遇到了
通过这种方式,query
方法不仅执行了 SQL 查询,而且还处理了执行查询时可能出现的异常情况,确保了查询逻辑的健壮性。
1.12.6 bootstrap.py
Bootstrap
类:
1.12.6.1 类:Bootstrap
- 定义了一个名为
Bootstrap
的类,此类继承了object
。
class Bootstrap(object):
1.12.6.1.1 __init__()
- 定义了一个名为
__init__
的构造函数,它是类的一个特殊方法,用于在创建类的实例时初始化对象。该构造函数接受一个类型为Postgresql
的参数postgresql
,并且没有返回值。
def __init__(self, postgresql: 'Postgresql') -> None:
self._postgresql = postgresql
self._running_custom_bootstrap = False
作用:
__init__
方法的作用是在创建类的实例时初始化必要的属性。具体来说:
- 存储 PostgreSQL 引用:接收一个
Postgresql
类型的对象,并将其保存为实例变量_postgresql
。这使得类的方法可以在后续操作中访问到这个 PostgreSQL 实例。 - 初始化自定义引导标志:设置
_running_custom_bootstrap
标志为False
,表示目前没有正在进行的自定义引导过程。这个标志可能在某些特定的操作中会被设置为True
,用来标识是否正在进行特殊的引导流程。
1.12.6.1.2 running_custom_bootstrap()
- 定义了一个名为
running_custom_bootstrap
的属性(property),该属性返回一个布尔值。
@property
def running_custom_bootstrap(self) -> bool:
return self._running_custom_bootstrap
作用:
running_custom_bootstrap
属性的作用是提供对内部状态 _running_custom_bootstrap
的只读访问。具体来说:
- 使用
@property
装饰器:- 将方法
running_custom_bootstrap
转换为一个属性,这样可以通过类似属性的方式访问这个方法的结果,而不需要显式地调用方法(即不需要加括号)。
- 将方法
- 返回内部状态:
- 返回
_running_custom_bootstrap
的值,该值是一个布尔类型,用于指示当前是否正在进行自定义引导(bootstrap)操作。
- 返回
通过这种方式,外部代码可以通过访问 running_custom_bootstrap
属性来了解当前对象是否处于自定义引导的过程中,而不需要直接访问私有变量 _running_custom_bootstrap
。这样做有助于封装内部实现细节,并提供了更安全的数据访问方式。
1.12.7 slots.py
SlotsHandler
类:
1.12.7.1 类:SlotsHandler
- 定义了一个名为
SlotsHandler
的类。
class SlotsHandler:
"""PostgreSQL中用于管理和存储复制槽信息的处理程序。
:ivar pg_replslot_dir: PostgreSQL复制槽位的系统位置路径。
:ivar _logical_slots_processing_queue:主端待处理的逻辑复制槽位
"""
1.12.7.1.1 __init__()
- 定义了
SlotsHandler
类的构造函数__init__
,该构造函数接受一个类型为Postgresql
的参数postgresql
,并且没有返回值。
def __init__(self, postgresql: 'Postgresql') -> None:
"""为复制槽创建一个具有存储属性的实例,并安排第一次同步。
:param postgresql:调用类实例提供接口到postgresql。
"""
self._force_readiness_check = False
self._schedule_load_slots = False
self._postgresql = postgresql
self._advance = None
self._replication_slots: Dict[str, Dict[str, Any]] = {} # already existing replication slots
self._logical_slots_processing_queue: Dict[str, Optional[int]] = {}
self.pg_replslot_dir = os.path.join(self._postgresql.data_dir, 'pg_replslot')
# 调用 schedule 方法来安排第一次同步
self.schedule()
作用:
__init__
方法的作用是在创建 SlotsHandler
类的实例时初始化必要的属性,并安排第一次同步操作。具体来说:
- 存储 PostgreSQL 引用:接收一个
Postgresql
类型的对象,并将其保存为实例变量_postgresql
。这使得类的方法可以在后续操作中访问到这个 PostgreSQL 实例。 - 初始化状态标志:设置
_force_readiness_check
和_schedule_load_slots
标志为False
,这些标志可能用于控制复制槽管理过程中的某些状态。 - 初始化复制槽信息:初始化
_replication_slots
和_logical_slots_processing_queue
变量,分别用于存储现有的复制槽信息和主节点上的逻辑复制槽队列。 - 设置复制槽目录:根据 PostgreSQL 数据目录构造复制槽的目录路径,并存储为
pg_replslot_dir
。 - 安排同步:调用
schedule
方法来安排第一次同步,这可能是为了确保在实例初始化后立即开始管理复制槽的工作。
1.12.7.1.2 schedule()
- 定义了一个名为
schedule
的方法,该方法有一个可选参数value
,默认值为None
,并且没有返回值。
def schedule(self, value: Optional[bool] = None) -> None:
"""调度从数据库加载槽位信息。
:param value:可选值,如果设置为“False”或强制为“True”,可用于取消调度。
如果省略该选项,如果PostgreSQL节点支持槽位复制,则该值为“True”。
"""
# 检查 value 是否为 None
if value is None:
value = self._postgresql.major_version >= 90400
# 将 value 赋值给 _schedule_load_slots 和 _force_readiness_check 两个变量
self._schedule_load_slots = self._force_readiness_check = value
作用:
schedule
方法的作用是安排从数据库加载复制槽信息的时间点。具体来说:
- 参数处理:如果提供了
value
参数,则直接使用该参数的值。如果没有提供,则检查当前使用的 PostgreSQL 版本是否支持复制槽功能(即版本号是否大于等于9.4
)。如果支持,则value
设为True
,否则设为False
。 - 状态标记设置:根据
value
的值来设置_schedule_load_slots
和_force_readiness_check
两个状态标记。如果value
为True
,则这两个标记都会被设置为True
,表示需要加载复制槽信息,并且可能需要强制进行准备状态检查。
1.12.8 sync.py
SyncHandler
类:
1.12.8.1 类:SyncHandler
- 定义了一个名为
SyncHandler
的类。
class SyncHandler(object):
"""类负责处理' synchronous_standby_names '。
同步备用是根据它们在' pg_stat_replication '中的状态选择的。
当' synchronous_standby_names '更改时,我们记住' _primary_flush_lsn '
并且' current_state() '方法将只在以下情况下将新添加的名称计数为“sync”
它们达到了记忆LSN,并通过pg_stat_replication报告为“sync”。"""
1.12.8.1.1 __init__()
- 定义了
SyncHandler
类的构造函数__init__
,该构造函数接受一个类型为Postgresql
的参数postgresql
,并且没有返回值。
def __init__(self, postgresql: 'Postgresql') -> None:
self._postgresql = postgresql
# 初始化一个名为 _synchronous_standby_names 的实例变量
self._synchronous_standby_names = '' # synchronous_standby_names最近的已知值
# 初始化一个名为 _ssn_data 的实例变量
self._ssn_data = deepcopy(_EMPTY_SSN)
self._primary_flush_lsn = 0
# “同步”复制连接,已验证达到self。_primary_flush_lsn
self._ready_replicas = CaseInsensitiveDict({}) # 键:成员名,值:连接id
作用:
__init__
方法的作用是在创建 SyncHandler
类的实例时初始化必要的属性。具体来说:
- 存储 PostgreSQL 引用:接收一个
Postgresql
类型的对象,并将其保存为实例变量_postgresql
。这使得类的方法可以在后续操作中访问到这个 PostgreSQL 实例。 - 初始化状态变量:设置
_synchronous_standby_names
为最新的已知值(初始为空字符串),_ssn_data
为同步备用名的默认或空数据结构的深拷贝,_primary_flush_lsn
为主节点的 flush LSN(初始为0
),以及_ready_replicas
为已经确认达到_primary_flush_lsn
的备节点连接的字典。
1.12.9 callback_executor.py
CallbackExecutor
类:OnReloadExecutor
类:
1.12.9.1 类:CallbackExecutor
- 定义了一个名为
CallbackExecutor
的类,该类继承自CancellableExecutor
和Thread
。
class CallbackExecutor(CancellableExecutor, Thread):
1.12.9.1.1 __init__()
- 定义了
CallbackExecutor
类的构造函数__init__
,该构造函数没有参数(除了隐式的self
参数),并且没有返回值。
def __init__(self):
# 调用父类 CancellableExecutor 和 Thread 的构造函数来初始化父类
CancellableExecutor.__init__(self)
Thread.__init__(self)
# 将线程的守护标志设置为 True
self.daemon = True
self._on_reload_executor = OnReloadExecutor()
self._cmd = None
# 初始化一个名为 _condition 的实例变量,并将其设置为一个新的 Condition 对象
self._condition = Condition()
# 调用 Thread 类的 start 方法来启动线程
self.start()
作用:
__init__
方法的作用是在创建 CallbackExecutor
类的实例时初始化必要的属性,并启动一个新的线程。具体来说:
- 父类初始化:调用父类
CancellableExecutor
和Thread
的构造函数来初始化父类的行为。 - 守护线程设置:将线程设置为守护线程,这样当主线程退出时,该线程也会随之退出。
- 初始化任务执行器:创建一个
_on_reload_executor
实例,可能用于处理重新加载事件。 - 初始化命令变量:初始化
_cmd
变量为None
,用于存储待执行的命令。 - 初始化同步条件变量:创建一个
_condition
对象,用于线程间的同步和通信。 - 启动线程:启动线程,使
CallbackExecutor
在单独的线程中执行任务。
1.12.9.2 类:OnReloadExecutor
- 定义了一个名为
OnReloadExecutor
的类,继承自CancellableSubprocess
。
class OnReloadExecutor(CancellableSubprocess):
1.12.9.2.1 call_nowait()
- 定义了一个名为
call_nowait
的方法,该方法接受一个类型为List[str]
的参数cmd
,并且没有返回值。
def call_nowait(self, cmd: List[str]) -> None:
"""最多运行一个 `on_reload` 回调。
为了实现这一点,我们总是杀死已经运行的命令,包括子进程。"""
# 调用 cancel 方法来取消当前正在运行的任何命令
self.cancel(kill=True)
# 调用 _kill_children 方法来杀死当前进程的所有子进程
self._kill_children()
with self._lock:
started = self._start_process(cmd, close_fds=True)
# 如果进程成功启动,并且 _process 变量不是 None,则创建一个新的线程来等待进程的完成
if started and self._process is not None:
Thread(target=self._process.wait).start()
作用:
call_nowait
方法的作用是运行一个 on_reload
回调命令,并确保在同一时间只能有一个 on_reload
回调命令在运行。具体来说:
- 终止现有命令:首先,通过调用
cancel
方法来终止任何正在运行的命令,并设置kill=True
来确保命令被彻底终止。 - 清理子进程:接着,调用
_kill_children
方法来杀死所有子进程,确保没有任何遗留的子进程继续运行。 - 启动新命令:在获取锁
_lock
的情况下,调用_start_process
方法来启动新的命令,并关闭除标准文件描述符外的所有文件描述符。 - 等待新命令完成:如果新命令成功启动,并且
_process
变量不是None
,则创建一个新的线程来等待该进程的完成。
1.12.10 cancellable.py
CancellableExecutor
类:CancellableSubprocess
类:
1.12.10.1 类:CancellableExecutor
- 定义了一个名为
CancellableExecutor
的类,继承自object
。
class CancellableExecutor(object):
"""
必须只有一个这样的进程,以便AsyncExecutor可以轻松地取消它。
"""
1.12.10.1.1 __init__()
- 定义了
CancellableExecutor
类的构造函数__init__
,该构造函数没有参数(除了隐式的self
参数),并且没有返回值。
def __init__(self) -> None:
self._process = None
self._process_cmd = None
self._process_children: List[psutil.Process] = []
self._lock = Lock()
作用:
__init__
方法的作用是在创建 CancellableExecutor
类的实例时初始化必要的属性。具体来说:
- 初始化进程变量:设置
_process
和_process_cmd
为None
,表示当前没有正在执行的进程或命令行参数。 - 初始化子进程列表:初始化
_process_children
为一个空列表,用于存储当前进程可能有的子进程。 - 初始化锁对象:创建一个
_lock
对象,用于保护对上述变量的访问,以确保在多线程环境下的安全性和一致性。
1.12.10.1.2 call()
- 定义了一个名为
call
的方法,它接受一个字符串列表cmd
作为参数,并且没有返回值。
def call(self, cmd: List[str]) -> None:
"""一次执行一个回调。
已经运行的命令会被杀死(包括子进程)。
如果不能被杀死,我们会等待它完成。
:param cmd: 要执行的命令"""
#根据 Python 版本的不同,设置 kwargs 字典,用于传递给日志记录器。如果 Python 版本大于等于 3.8,则 kwargs 包含 stacklevel 参数
kwargs: Dict[str, Any] = {'stacklevel': 3} if sys.version_info >= (3, 8) else {}
logger.debug('CallbackExecutor.call(%s)', cmd, **kwargs)
# 检查命令列表中的第三个元素是否为 CallbackAction.ON_RELOAD
if cmd[-3] == CallbackAction.ON_RELOAD:
return self._on_reload_executor.call_nowait(cmd)
# 调用 _kill_process 方法来终止当前正在运行的进程(如果有的话)
self._kill_process()
# 使用 with 语句进入 _condition 上下文管理器,设置 _cmd 为传入的 cmd,然后通知等待在 _condition 上的线程
with self._condition:
self._cmd = cmd
self._condition.notify()
作用:
call
方法的作用是在一个同步的环境中执行一个回调命令,并确保一次只有一个回调命令在运行。具体来说:
- 记录调试日志:
- 记录一个调试级别的日志,显示即将执行的命令。
- 处理重载回调:
- 如果命令是
ON_RELOAD
类型的回调,则委托给_on_reload_executor.call_nowait
方法执行命令,并立即返回。
- 如果命令是
- 终止当前进程:
- 调用
_kill_process
方法来终止当前正在运行的任何进程,如果无法终止,则等待其自然结束。
- 调用
- 设置命令并通知线程:
- 使用互斥锁(
_condition
)来保证命令设置的一致性,并通知其他可能等待的线程,告知它们新的命令已经被设置。
- 使用互斥锁(
通过这种方式,call
方法确保了每次只有一个回调命令被执行,并且在执行新的命令之前会清除任何正在运行的旧命令。这有助于防止并发执行多个回调命令可能导致的问题,确保了回调命令的顺序执行。
1.12.10.1.3 _kill_process()
- 定义了一个名为
_kill_process
的私有方法,该方法没有参数,并且没有返回值。
def _kill_process(self) -> None:
# 使用 with 语句进入 _lock 上下文管理器,确保在同一时间内只有一个线程可以执行以下代码块,防止并发修改
with self._lock:
# 检查 _process 是否存在、是否正在运行,并且 _process_children 是否为空
if self._process is not None and self._process.is_running() and not self._process_children:
# 尝试暂停进程,在获取子进程列表之前先挂起进程
try:
self._process.suspend() # 在获得子列表之前暂停该进程
except psutil.Error as e:
logger.info('Failed to suspend the process: %s', e.msg)
# 尝试获取当前进程的所有子进程,递归查找
try:
self._process_children = self._process.children(recursive=True)
except psutil.Error:
pass
# 尝试终止进程,并记录终止的信息
try:
self._process.kill()
logger.warning('Killed %s because it was still running', self._process_cmd)
except psutil.NoSuchProcess:
pass
except psutil.AccessDenied as e:
logger.warning('Failed to kill the process: %s', e.msg)
作用:
_kill_process
方法的作用是在确保没有并发修改的前提下,终止一个正在运行的进程及其所有子进程。具体来说:
- 检查进程状态:
- 检查
_process
是否存在、是否正在运行,并且_process_children
是否为空。
- 检查
- 暂停进程:
- 在获取子进程列表之前,尝试暂停当前进程,以防子进程在获取过程中发生变化。
- 获取子进程列表:
- 尝试获取当前进程的所有子进程,并递归查找所有的后代进程。
- 终止进程:
- 尝试终止当前进程,并记录相应的日志信息。
- 处理异常情况:
- 如果在尝试挂起进程、获取子进程列表或终止进程时遇到错误,则根据错误类型进行相应的处理,并记录相关信息。
通过这种方式,_kill_process
方法确保了在执行新的命令之前,已经彻底清理了任何可能存在的旧命令及其子进程,防止了资源泄露和并发问题。
1.12.10.2 类:CancellableSubprocess
- 定义了一个名为
CancellableSubprocess
的类,继承自CancellableExecutor
。
class CancellableSubprocess(CancellableExecutor):
1.12.10.2.1 __init__()
- 定义了
CancellableSubprocess
类的构造函数__init__
,该构造函数没有参数(除了隐式的self
参数),并且没有返回值。
def __init__(self) -> None:
super(CancellableSubprocess, self).__init__()
# 标记当前的子进程是否已被取消
self._is_cancelled = False
作用:
__init__
方法的作用是在创建 CancellableSubprocess
类的实例时初始化必要的属性。具体来说:
- 父类初始化:通过调用父类
CancellableExecutor
的构造函数来初始化父类的行为,确保继承自CancellableExecutor
的所有属性和方法都被正确设置。 - 初始化取消标记:设置
_is_cancelled
为False
,表示当前没有取消请求。这个标记用于后续的操作中判断是否需要取消当前正在执行的子进程。
1.12.10.3 类:OnReloadExecutor
- 定义了一个名为
CancellableSubprocess
的类,继承自CancellableExecutor
。
class OnReloadExecutor(CancellableSubprocess):
1.12.10.3.1 call_nowait()
- 定义了一个名为
call_nowait
的方法,它接受一个字符串列表cmd
作为参数,并且没有返回值。
def call_nowait(self, cmd: List[str]) -> None:
"""最多运行一个 `on_reload` 回调。
为了实现这一点,我们总是终止已经运行的命令,包括子进程。"""
# 调用 cancel 方法,并设置 kill 参数为 True,以取消当前正在执行的任务,如果有必要的话,终止进程
self.cancel(kill=True)
# 调用 _kill_children 方法来终止所有子进程
self._kill_children()
# 使用 with 语句进入 _lock 上下文管理器,确保在同一时间内只有一个线程可以执行 _start_process 方法。设置 close_fds 参数为 True,开始执行命令 cmd,并返回是否成功启动
with self._lock:
started = self._start_process(cmd, close_fds=True)
# 如果命令成功启动并且 _process 不为 None,则启动一个新的线程来等待 _process 的完成
if started and self._process is not None:
Thread(target=self._process.wait).start()
作用:
call_nowait
方法的作用是在一个非阻塞的方式下执行一个 on_reload
类型的回调命令,并确保一次最多只有一个这样的命令在运行。具体来说:
- 取消当前任务:
- 调用
cancel
方法并设置kill=True
,以确保当前正在执行的任何命令被取消,并且如果有必要的话,终止相关进程。
- 调用
- 终止子进程:
- 调用
_kill_children
方法来终止所有子进程,确保没有遗留的子进程影响新的命令执行。
- 调用
- 启动新命令:
- 使用互斥锁(
_lock
)来保证命令启动的一致性,防止并发修改。 - 设置
close_fds=True
,这通常用于防止子进程继承父进程的文件描述符,从而减少潜在的安全隐患。 - 尝试启动命令
cmd
,并记录是否成功启动。
- 使用互斥锁(
- 非阻塞等待:
- 如果命令成功启动,并且
_process
对象不是None
,则在一个新的线程中等待_process
的完成,这样主线程不会因为等待_process
而被阻塞。
- 如果命令成功启动,并且
通过这种方式,call_nowait
方法确保了每次只能有一个 on_reload
类型的回调命令在运行,并且在启动新的命令之前会清理掉任何可能存在的旧命令及其子进程。这有助于防止并发执行多个 on_reload
命令可能导致的问题,并且保证了回调命令的非阻塞性执行。
1.12.11 postmaster.py
PostmasterProcess
类:CancellableSubprocess
类:
1.12.11.1 类:PostmasterProcess
- 定义了一个名为
PostmasterProcess
的类,继承自psutil.Process
。
class PostmasterProcess(psutil.Process):
1.12.11.1.1 __init__()
- 定义了
PostmasterProcess
类的构造函数__init__
,该构造函数接受一个整数类型的pid
参数,并且没有返回值。
def __init__(self, pid: int) -> None:
# 初始化一个名为 _postmaster_pid 的实例变量
self._postmaster_pid: Dict[str, str]
# 是否处于单用户模式
self.is_single_user = False
# 检查传入的 pid 是否小于 0。如果是,将 pid 取正值
if pid < 0:
pid = -pid
self.is_single_user = True
# 调用父类 psutil.Process 的构造函数来初始化父类的行为
super(PostmasterProcess, self).__init__(pid)
作用:
__init__
方法的作用是在创建 PostmasterProcess
类的实例时初始化必要的属性。具体来说:
- 初始化私有变量:
- 初始化
_postmaster_pid
变量,但没有赋值。这里可能是开发者预留的变量,打算后续使用。
- 初始化
- 初始化单用户模式标志:
- 初始化
is_single_user
变量,并默认设置为False
。如果传入的pid
是负数,则表示进程处于单用户模式,此时会将pid
转为正数,并将is_single_user
设置为True
。
- 初始化
- 调用父类构造函数:
- 通过调用
psutil.Process
类的构造函数来完成父类的初始化,确保PostmasterProcess
具有psutil.Process
类的所有功能。
- 通过调用
1.12.11.1.2 from_pidfile()
- 定义了一个名为
from_pidfile
的静态方法,该方法接受一个字符串参数data_dir
,并且返回一个可选的PostmasterProcess
类型的对象,即可能返回一个PostmasterProcess
对象,也可能返回None
。
@staticmethod
def from_pidfile(data_dir: str) -> Optional['PostmasterProcess']:
# 尝试从指定的数据目录 data_dir 中读取 PID 文件
try:
proc = PostmasterProcess._from_pidfile(data_dir)
return proc if proc and proc._is_postmaster_process() else None
except psutil.NoSuchProcess:
return None
作用:
from_pidfile
方法的作用是从 PostgreSQL 数据目录中的 PID 文件读取 postmaster
进程的信息,并返回一个 PostmasterProcess
对象。具体来说:
- 读取 PID 文件:
- 通过
_from_pidfile
方法从指定的数据目录data_dir
中读取 PID 文件,并获取postmaster
进程的信息。
- 通过
- 验证
postmaster
进程:- 使用
_is_postmaster_process
方法来验证获取到的进程是否确实是一个postmaster
进程。 - 如果验证成功,则返回
PostmasterProcess
对象;否则返回None
。
- 使用
- 异常处理:
- 如果在读取 PID 文件或获取进程信息的过程中遇到
psutil.NoSuchProcess
异常,则返回None
。
- 如果在读取 PID 文件或获取进程信息的过程中遇到
1.12.11.1.3 _from_pidfile()
- 定义了一个名为
_from_pidfile
的类方法,该方法接受一个字符串参数data_dir
,并且返回一个可选的PostmasterProcess
类型的对象,即可能返回一个PostmasterProcess
对象,也可能返回None
。
@classmethod
def _from_pidfile(cls, data_dir: str) -> Optional['PostmasterProcess']:
# 从指定的数据目录 data_dir 中读取 postmaster 的 PID 文件
postmaster_pid = PostmasterProcess._read_postmaster_pidfile(data_dir)
try:
# 试将从 PID 文件中读取到的 pid 值转换为整数
pid = int(postmaster_pid.get('pid', 0))
if pid:
proc = cls(pid)
proc._postmaster_pid = postmaster_pid
return proc
except ValueError:
return None
作用:
_from_pidfile
方法的作用是从 PostgreSQL 数据目录中的 PID 文件读取 postmaster
进程的信息,并创建一个 PostmasterProcess
对象。具体来说:
- 读取 PID 文件:
- 通过
_read_postmaster_pidfile
方法从指定的数据目录data_dir
中读取 PID 文件,并获取其中的pid
信息。
- 通过
- 转换 PID 值:
- 尝试将从 PID 文件中读取到的
pid
值转换为整数,并检查是否为非零值。
- 尝试将从 PID 文件中读取到的
- 创建
PostmasterProcess
对象:- 如果
pid
是有效的(非零),则创建一个新的PostmasterProcess
对象,并将从 PID 文件中读取的信息保存到_postmaster_pid
属性中。 - 如果
pid
不是有效的整数,则返回None
。
- 如果
- 异常处理:
- 如果在读取 PID 文件或转换
pid
值的过程中遇到任何错误(如ValueError
),则返回None
。
- 如果在读取 PID 文件或转换
1.12.11.1.4 _read_postmaster_pidfile()
- 定义了一个名为
_read_postmaster_pidfile
的静态方法,该方法接受一个字符串参数data_dir
,并且返回一个字典类型的数据,字典的键和值都是字符串类型。
@staticmethod
def _read_postmaster_pidfile(data_dir: str) -> Dict[str, str]:
"""从数据目录中读取并解析 postmaster.pid 文件。
:returns 如果成功,则返回包含值的字典,否则返回空字典。
"""
pid_line_names = ['pid', 'data_dir', 'start_time', 'port', 'socket_dir', 'listen_addr', 'shmem_key']
try:
# 尝试打开指定数据目录下的 postmaster.pid 文件,并读取文件内容
with open(os.path.join(data_dir, 'postmaster.pid')) as f:
return {name: line.rstrip('\n') for name, line in zip(pid_line_names, f)}
except IOError:
return {}
作用:
_read_postmaster_pidfile
方法的作用是从 PostgreSQL 数据目录中的 postmaster.pid
文件读取 postmaster
进程的相关信息,并将其解析成一个字典。具体来说:
- 定义数据项名称:
- 定义了一个列表
pid_line_names
,包含了postmaster.pid
文件中每一行对应的数据项名称。
- 定义了一个列表
- 打开并读取文件:
- 尝试打开指定数据目录下的
postmaster.pid
文件,并读取其内容。 - 使用
with
语句确保文件正确关闭。
- 尝试打开指定数据目录下的
- 解析文件内容:
- 通过
zip
函数将pid_line_names
列表与文件的每一行进行配对。 - 创建一个字典,字典的键是
pid_line_names
中的名字,值是文件中对应行的内容(去除了行尾的换行符)。
- 通过
- 异常处理:
- 如果在打开或读取文件的过程中遇到任何 I/O 错误(如文件不存在或无法访问),则返回一个空字典。
1.12.11.1.5 _is_postmaster_process()
- 定义了一个名为
_is_postmaster_process
的方法,该方法没有参数(除了隐式的self
参数),并且返回一个布尔值。
def _is_postmaster_process(self) -> bool:
try:
# 尝试将 self._postmaster_pid 字典中的 start_time 值转换为整数
start_time = int(self._postmaster_pid.get('start_time', 0))
# 如果 start_time 不为 0,并且 postmaster 进程的实际启动时间与 PID 文件记录的启动时间之差大于 3 秒,则认为这不是 postmaster 进程
if start_time and abs(self.create_time() - start_time) > 3:
logger.info('Process %s is not postmaster, too much difference between PID file start time %s and '
'process start time %s', self.pid, start_time, self.create_time())
return False
except ValueError:
logger.warning('Garbage start time value in pid file: %r', self._postmaster_pid.get('start_time'))
# 额外的安全检查。这个过程不可能是我们自己,我们的父母或我们的直系子女。
# 额外的安全检查。如果进程 ID (self.pid) 是当前进程的 ID (os.getpid())、当前进程的父进程 ID (os.getppid()) 或者当前进程是该进程的直接子进程,则认为这不是 postmaster 进程
if self.pid == os.getpid() or self.pid == os.getppid() or self.ppid() == os.getpid():
logger.info('Patroni (pid=%s, ppid=%s), "fake postmaster" (pid=%s, ppid=%s)',
os.getpid(), os.getppid(), self.pid, self.ppid())
return False
return True
作用:
_is_postmaster_process
方法的作用是检查当前进程是否为 PostgreSQL 的 postmaster
进程。具体来说:
- 检查启动时间:
- 验证当前进程的实际启动时间和 PID 文件记录的启动时间是否接近。如果差异超过 3 秒,则认为当前进程不是
postmaster
进程。
- 验证当前进程的实际启动时间和 PID 文件记录的启动时间是否接近。如果差异超过 3 秒,则认为当前进程不是
- 异常处理:
- 如果在转换
start_time
时遇到值错误,则记录警告日志,并继续执行后续检查。
- 如果在转换
- 安全性检查:
- 检查当前进程是否是自身、父进程或直接子进程。如果符合任何一种情况,则认为当前进程不是
postmaster
进程。
- 检查当前进程是否是自身、父进程或直接子进程。如果符合任何一种情况,则认为当前进程不是
- 返回结果:
- 如果上述所有检查均未否定当前进程是
postmaster
进程,则返回True
,表明当前进程被认为是postmaster
进程;否则返回False
。
- 如果上述所有检查均未否定当前进程是
1.12.12 global_config.py
GlobalConfig
类:
1.12.12.1 类:GlobalConfig
- 定义了一个名为
GlobalConfig
的类,继承自types.ModuleType
。
class GlobalConfig(types.ModuleType):
"""一个包装全局配置并提供方便的方法来访问/检查值的类。"""
__file__ = __file__ # just to make unittest and pytest happy
1.12.12.1.1 __init__()
- 定义了一个名为
__init__
的构造方法,它没有参数,并返回None
。
def __init__(self) -> None:
"""初始化 GlobalConfig 对象。"""
# 调用父类的构造方法,传递当前模块的名字作为参数
super().__init__(__name__)
self.__config = {}
作用:
__init__
方法的主要作用包括:
-
设置模块标识:
- 通过设置
__file__
属性,使得该类在单元测试框架中能够被正确识别。
- 通过设置
-
初始化配置存储:
- 创建一个空字典
__config
,为后续存储配置信息做好准备。
- 创建一个空字典
-
继承行为初始化:
- 调用父类构造方法,确保
GlobalConfig
类具备ModuleType
类的所有特性和行为。
- 调用父类构造方法,确保
1.12.12.1.2 is_synchronous_mode()
- 定义了一个名为
is_synchronous_mode
的属性(property),该属性没有显式的参数,并返回一个布尔值。
@property
def is_synchronous_mode(self) -> bool:
"""``True`` 如果请求了同步复制并且这不是一个备用集群配置。"""
return (self.check_mode('synchronous_mode') is True or self.is_quorum_commit_mode) \
and not self.is_standby_cluster
作用:
is_synchronous_mode
属性的具体作用是确定当前集群配置是否处于同步复制模式。具体来说:
- 检查同步模式:
- 通过调用
self.check_mode('synchronous_mode')
检查是否请求了同步复制模式。 - 如果
self.is_quorum_commit_mode
为True
,也认为是同步模式的一种形式。
- 通过调用
- 排除备用集群:
- 通过检查
self.is_standby_cluster
是否为False
来确定这不是一个备用集群配置。
- 通过检查
只有当以上两个条件都满足时,才会返回 True
,表示当前集群配置为同步复制模式。
1.12.12.1.3 is_synchronous_mode_strict()
- 定义了一个名为
is_synchronous_mode_strict
的属性(property),该属性没有显式的参数,并返回一个布尔值。
@property
def is_synchronous_mode_strict(self) -> bool:
"""``True`` 如果至少需要一个同步节点。"""
return self.check_mode('synchronous_mode_strict')
作用:
1.12.13 validator.py
Bool
类:transform()
1.12.13.1 类:Bool
- 定义了一个名为
Bool
的类,它继承自_Transformable
类。假设_Transformable
是一个基类,它可能定义了某些通用的行为或者接口,而Bool
类则是对_Transformable
的具体实现之一。
class Bool(_Transformable):
1.12.13.1.1 transform()
- 定义了一个名为
transform
的方法,它接受两个参数:一个类型为str
的name
和一个类型为Any
的value
。该方法的返回类型为Optional[Any]
,这意味着它可以返回任何类型的值,也可能是None
。
def transform(self, name: str, value: Any) -> Optional[Any]:
# 行代码检查 value 是否可以通过 parse_bool 函数转换为布尔值
if parse_bool(value) is not None:
return value
logger.warning('Removing bool parameter=%s from the config due to the invalid value=%s', name, value)
作用:
transform
方法的作用是对给定的参数名称 name
和参数值 value
进行验证,确保 value
可以被解析为布尔值。如果 value
可以被正确解析为布尔值,则返回该值;如果不能,则记录一条警告日志,并且不返回任何值(隐式返回 None
)。
这个方法可以用于配置文件的解析过程中,特别是在需要验证配置项是否为有效的布尔值的情况下。当配置文件中的布尔值不符合预期的格式时,这个方法可以帮助识别并记录问题,同时防止无效的配置项影响系统的运行。
1.12.13.2 transform_postgresql_parameter_value()
- 定义了一个名为
transform_postgresql_parameter_value
的函数,它接受三个参数:version
(整数类型,PostgreSQL 版本)、name
(字符串类型,PostgreSQL GUC 参数名)和value
(任何类型,PostgreSQL GUC 参数值),返回值类型为可选的任何类型。
def transform_postgresql_parameter_value(version: int, name: str, value: Any) -> Optional[Any]:
"""验证 GUC *name* 的 *value* 对于 Postgres *version* 是否有效,使用 ``parameters`` 进行验证。
:param version: 验证 GUC 的 Postgres 版本。
:param name: Postgres GUC 的名称。
:param value: Postgres GUC 的值。
:returns: 返回值可能是以下之一:
* 如果 *name* 似乎是一个扩展 GUC(包含一个点 '.'),则返回原始的 *value*;或
* 如果 **name** 是一个恢复 GUC,则返回 ``None``;或
* 使用在 ``parameters`` 中定义的验证器,将 GUC *name* 在 Postgres *version* 中转换为预期的格式。也可以返回 ``None``。详见 :func:`_transform_parameter_value`。
"""
# 如果 name 包含点 . 并且不在 parameters 中,则认为是一个扩展 GUC
if '.' in name and name not in parameters:
# 可能是一个扩展 GUC,因此直接返回。否则,如果 `name` 在 `parameters` 中,则可能是来自自定义 PostgreSQL 构建的命名空间 GUC,我们对此进行特别处理而不是使用常规验证手段。
return value
# 如果 name 是一个恢复 GUC
if name in recovery_parameters:
return None
# 返回 name 在 version 版本下的转换值
return _transform_parameter_value(parameters, version, name, value)
作用:
transform_postgresql_parameter_value
函数的具体作用是对给定的 PostgreSQL GUC 参数进行验证和转换,确保其值符合特定版本的 PostgreSQL 的要求。具体来说:
- 处理扩展 GUC:
- 如果参数名称包含点
.
并且不在预定义的parameters
字典中,则认为是扩展 GUC,直接返回原始值。
- 如果参数名称包含点
- 处理恢复 GUC:
- 如果参数名称在预定义的
recovery_parameters
列表中,则返回None
,表示不进行处理。
- 如果参数名称在预定义的
- 转换和验证 GUC 参数:
- 对于非扩展和非恢复的 GUC 参数,调用
_transform_parameter_value
函数进行转换,该函数会根据 PostgreSQL 的版本和参数名称使用parameters
中定义的验证规则来进行转换。
- 对于非扩展和非恢复的 GUC 参数,调用
1.12.13.3 _transform_parameter_value()
- 定义了一个名为
_transform_parameter_value
的函数,它接受四个参数:validators
(可变映射类型,包含了针对不同 PostgreSQL 版本的所有 GUC 参数的验证规则)、version
(整数类型,PostgreSQL 版本)、name
(字符串类型,PostgreSQL GUC 参数名)和value
(任何类型,PostgreSQL GUC 参数值),返回值类型为可选的任何类型。
def _transform_parameter_value(validators: MutableMapping[str, Tuple[_Transformable, ...]],
version: int, name: str, value: Any) -> Optional[Any]:
"""使用定义的 *validators* 验证 Postgres *version* 中 GUC *name* 的 *value*。
:param validators: 包含所有 PostgreSQL 版本的所有 GUC 参数的字典。每个键是 PostgreSQL GUC 的名称,对应的值是一个 :class:`_Transformable` 类型的可变长度元组。每个项目都是针对给定范围内的 PostgreSQL 版本的 GUC 的验证规则。应该只包含恢复 GUC 或一般 GUC,但不能同时包含两者。
:param version: 验证 GUC 的 PostgreSQL 版本。
:param name: PostgreSQL GUC 的名称。
:param value: PostgreSQL GUC 的值。
* 不允许向 ``postgresql.conf``(或 ``recovery.conf``)写入在 PostgreSQL *version* 中不存在的 GUC;
* 如果 *name* 在 *validators* 中没有验证器,但在 PostgreSQL *version* 中是一个有效的 GUC,则避免忽略 GUC *name*。
:returns: 返回值可能是以下之一:
* 如果 *name* 在 *validators* 中有一个针对相应 PostgreSQL *version* 的验证器,则返回转换为预期格式的 *value*;或
* 如果 *name* 在 *validators* 中没有验证器,则返回 ``None``。
"""
# 从 validators 字典中获取 name 的验证规则
for validator in validators.get(name, ()) or ():
# 检查当前 version 是否在验证器的有效版本范围内
if version >= validator.version_from and\
(validator.version_till is None or version < validator.version_till):
# 如果版本匹配,则调用验证器的 transform 方法对 value 进行转换,并返回结果
return validator.transform(name, value)
logger.warning('Removing unexpected parameter=%s value=%s from the config', name, value)
作用:
_transform_parameter_value
函数的具体作用是对给定的 PostgreSQL GUC 参数进行验证和转换,确保其值符合特定版本的 PostgreSQL 的要求。具体来说:
- 查找验证规则:
- 从
validators
字典中查找与name
相关的验证规则。
- 从
- 版本匹配检查:
- 检查当前 PostgreSQL 版本是否在验证规则的有效版本范围内。
- 转换 GUC 值:
- 如果版本匹配,则使用验证规则提供的
transform
方法对 GUC 的值进行转换。
- 如果版本匹配,则使用验证规则提供的
- 处理无效 GUC:
- 如果没有找到匹配的验证规则,则记录一条警告信息,并返回
None
,表示该 GUC 将被移除或忽略。
- 如果没有找到匹配的验证规则,则记录一条警告信息,并返回
1.12.13.4 类:_Transformable
- 定义了一个名为
_Transformable
的类,继承自abc.ABC
(抽象基类)。这表明_Transformable
是一个抽象类,可能包含抽象方法。
class _Transformable(abc.ABC):
1.12.13.4.1 __init__()
- 定义了一个构造函数
__init__
,接受两个参数:version_from
(整数类型,指定版本范围的起始版本号)和version_till
(可选整数类型,默认为None
,指定版本范围的结束版本号)。 - 定义了一个名为
transform
的方法,它接受两个参数:name
(字符串类型,GUC 的名称)和value
(任何类型,GUC 的值),返回值类型为可选的任何类型。
def __init__(self, version_from: int, version_till: Optional[int] = None) -> None:
self.__version_from = version_from
self.__version_till = version_till
作用:
- 初始化对象属性:
- 设置
_Transformable
对象的版本范围属性__version_from
和__version_till
。 - 这些属性用于后续的版本检查和验证操作。
- 设置
- 存储版本信息:
version_from
参数指定了验证器适用的 PostgreSQL 版本的起始点。version_till
参数指定了验证器适用的 PostgreSQL 版本的结束点(如果提供了的话)。
1.12.13.4.2 transform()
- 使用装饰器
@abc.abstractmethod
定义了一个抽象方法transform
。这意味着任何继承_Transformable
的子类都必须实现这个方法。
@abc.abstractmethod
def transform(self, name: str, value: Any) -> Optional[Any]:
"""验证提供的值是否有效。
:param name: GUC 的名称
:param value: GUC 的值
:returns: 如果值有效,则返回该值(有时会被限制在一定范围内),如果值无效,则返回 ``None``
"""
作用:
transform
方法的具体作用是对给定的 PostgreSQL GUC 参数进行验证和转换,确保其值符合特定的要求。具体来说:
- 参数验证:
- 接受 GUC 的名称
name
和值value
。 - 验证提供的值是否有效。
- 接受 GUC 的名称
- 值的转换:
- 如果值有效,则返回该值(有时会被限制在一定范围内)。
- 如果值无效,则返回
None
。
1.12.13.5 transform_postgresql_parameter_value()
- 定义了一个名为
transform_recovery_parameter_value
的函数,接受三个参数:version
(整数类型,PostgreSQL 版本)、name
(字符串类型,PostgreSQL 恢复 GUC 的名称)、value
(任意类型,PostgreSQL 恢复 GUC 的值),返回值类型为可选的任何类型(Optional[Any]
),即可能返回一个值或None
。
def transform_recovery_parameter_value(version: int, name: str, value: Any) -> Optional[Any]:
"""使用 ``recovery_parameters`` 验证 Postgres *version* 中 GUC *name* 的 *value*。
:param version: 验证恢复 GUC 的 PostgreSQL 版本。
:param name: PostgreSQL 恢复 GUC 的名称。
:param value: PostgreSQL 恢复 GUC 的值。
:returns: 使用 ``recovery_parameters`` 中定义的验证器将恢复 GUC *name* 在 Postgres *version* 中转换为期望的格式。也可以返回 ``None``。参见 :func:`_transform_parameter_value`。
"""
return _transform_parameter_value(recovery_parameters, version, name, value)
作用:
transform_recovery_parameter_value
函数的具体作用是对给定的 PostgreSQL 恢复 GUC 参数进行验证和转换,确保其值符合特定版本的 PostgreSQL 的要求。具体来说:
- 参数验证:
- 验证提供的值是否有效,并确保其符合特定版本的要求。
- 值的转换:
- 如果值有效,则返回转换后的值。
- 如果值不符合要求,则返回
None
。
1.12.13.6 _transform_parameter_value()
- 定义了一个名为
_transform_parameter_value
的函数,它接受四个参数:validators
(一个字典,其中键为字符串,值为_Transformable
类型的元组)、version
(整数类型,PostgreSQL 版本)、name
(字符串类型,PostgreSQL GUC 的名称)、value
(任意类型,PostgreSQL GUC 的值)。该函数返回一个可选项Optional[Any]
,即可能是任意类型的值或None
。
def _transform_parameter_value(validators: MutableMapping[str, Tuple[_Transformable, ...]],
version: int, name: str, value: Any) -> Optional[Any]:
"""使用定义的 *validators* 验证 Postgres *version* 中 GUC *name* 的 *value*。
:param validators: 所有版本的 PostgreSQL GUC 的字典。每个键是一个 PostgreSQL GUC 的名称,相应的值是一个长度可变的 :class:`_Transformable` 元组。每个项目是给定版本范围内 GUC 的验证规则。应该只包含恢复 GUC 或一般 GUC,而不是两者。
:param version: 验证 GUC 的 PostgreSQL 版本。
:param name: PostgreSQL GUC 的名称。
:param value: PostgreSQL GUC 的值。
* 不允许写入不在 Postgres *version* 中存在的 GUC 到 ``postgresql.conf``(或 ``recovery.conf``);
* 如果 *name* 在 *validators* 中没有验证器,但在 Postgres *version* 中是一个有效的 GUC,则避免忽略 GUC *name*。
:returns: 返回值可能是以下之一:
* 如果 *name* 在 *validators* 中有对应 Postgres *version* 的验证器,则返回转换为预期格式的 GUC *name* 的 *value*;或
* 如果 *name* 在 *validators* 中没有验证器,则返回 ``None``。
"""
# 从 validators 字典中获取与 name 相关的验证器元组
for validator in validators.get(name, ()) or ():
# 检查当前版本 version 是否在验证器 validator 的版本范围内
if version >= validator.version_from and\
(validator.version_till is None or version < validator.version_till):
# 如果版本匹配,则调用验证器的 transform 方法来转换 value
return validator.transform(name, value)
logger.warning('Removing unexpected parameter=%s value=%s from the config', name, value)
作用:
_transform_parameter_value
函数的具体作用是根据给定的 PostgreSQL 版本、GUC 名称和值,使用定义的验证器来验证并可能转换 GUC 的值。具体来说:
- 查找验证器:
- 从
validators
字典中查找与给定 GUC 名称相关的验证器。
- 从
- 检查版本范围:
- 检查当前版本是否在验证器的版本范围内。
- 转换值:
- 如果版本匹配,则使用验证器的
transform
方法来转换 GUC 的值,并返回转换后的结果。
- 如果版本匹配,则使用验证器的
- 记录警告:
- 如果没有找到匹配的验证器,则记录一条警告日志,并返回
None
。
- 如果没有找到匹配的验证器,则记录一条警告日志,并返回
1.12.14 rewind.py
Rewind
类:transform()
1.12.14.1 类:Rewind
- 定义了一个名为
Rewind
的类,它继承自object
类。
class Rewind(object):
1.12.14.1.1 __init__()
- 定义了一个名为
__init__
的初始化方法,它接受一个类型为Postgresql
的参数postgresql
,并且返回类型为None
。
def __init__(self, postgresql: Postgresql) -> None:
self._postgresql = postgresql
# 创建一个 Lock 对象
self._checkpoint_task_lock = Lock()
# 调用 self.reset_state() 方法来重置类的状态
self.reset_state()
作用:
__init__
方法的具体作用是初始化一个 Rewind
类的实例,并设置与重放操作相关的内部状态。具体来说:
- 初始化 PostgreSQL 接口:
- 接收一个
Postgresql
类型的对象引用,这可能是对 PostgreSQL 数据库的封装或接口,用于后续的操作。
- 接收一个
- 创建任务锁:
- 创建一个锁对象
self._checkpoint_task_lock
,用于同步与重放相关的任务,防止并发修改状态导致的问题。
- 创建一个锁对象
- 重置状态:
- 调用
reset_state()
方法来初始化或重置类的状态,这可能是为了准备进行重放操作,或者是清除之前的状态以便开始新的操作。
- 调用
1.12.14.1.2 reset_state()
- 定义了一个名为
reset_state
的方法,它属于类的一个实例,并且该方法的返回类型为None
。
def reset_state(self) -> None:
self._state = REWIND_STATUS.INITIAL
# 确保在修改 _checkpoint_task 时的线程安全
with self._checkpoint_task_lock:
self._checkpoint_task = None
作用:
reset_state
方法的具体作用是对 Rewind
类的实例状态进行重置。具体来说:
- 设置初始状态:
- 将
self._state
设置为REWIND_STATUS.INITIAL
,表示重放状态的初始值。
- 将
- 确保线程安全:
- 使用
with self._checkpoint_task_lock:
语句块来确保在多线程环境下修改_checkpoint_task
属性时的一致性和安全性。
- 使用
- 清空任务引用:
- 将
_checkpoint_task
设置为None
,表明目前没有正在进行的检查点任务,或者之前的任务已经结束。
- 将
1.13 exceptions.py
存放自定义异常的文件。实现高层次的 Patroni 异常。更具体的异常可以在其他模块中找到,作为本模块中定义的任何异常的子类。
PatroniException
类:这是所有 Patroni 异常的基类。PatroniFatalException
类:表示一种灾难性的异常,使得 Patroni 无法执行其工作。PostgresException
类:表示与 PostgreSQL 管理相关的任何异常。DCSError
类:所有与分布式协调服务(DCS)相关的异常的父类。PostgresConnectionException
类:表示连接到 PostgreSQL 实例时遇到的任何问题。WatchdogError
类:表示在管理 watchdog 设备时遇到的任何问题。ConfigParseError
类:表示在加载或验证 Patroni 配置时识别到的任何问题。
1.13.1 类:PatroniException
- 定义了一个名为
PatroniException
的类,继承自 Python 的内置Exception
类。这是所有 Patroni 异常的基类。
class PatroniException(Exception):
"""所有类型的 Patroni 异常的父类。
:ivar value: 异常的描述。
"""
def __init__(self, value: Any) -> None:
"""Create a new instance of :class:`PatroniException` with the given description.
:param value: description of the exception.
"""
self.value = value
1.13.2 类:PatroniFatalException
- 定义了一个名为
PatroniFatalException
的类,继承自PatroniException
。这种异常表示一种灾难性的异常,使得 Patroni 无法执行其工作。
class PatroniFatalException(PatroniException):
"""灾难性的异常,阻止 Patroni 执行其工作。"""
pass
1.13.3 类:PostgresException
- 定义了一个名为
PostgresException
的类,继承自PatroniException
。这种异常表示与 PostgreSQL 管理相关的任何异常。
class PostgresException(PatroniException):
"""与 PostgreSQL 管理相关的任何异常。"""
pass
1.13.4 类:DCSError
- 定义了一个名为
DCSError
的类,继承自PatroniException
。这是所有与分布式协调服务(DCS)相关的异常的父类。
class DCSError(PatroniException):
"""所有与 DCS 相关的异常的父类。"""
pass
1.13.5 类:PostgresConnectionException
- 定义了一个名为
PostgresConnectionException
的类,继承自PostgresException
。这种异常表示连接到 PostgreSQL 实例时遇到的任何问题。
class PostgresConnectionException(PostgresException):
"""连接到 PostgreSQL 实例时遇到的任何问题。"""
pass
1.13.6 类:WatchdogError
- 定义了一个名为
WatchdogError
的类,继承自PatroniException
。这种异常表示在管理 watchdog 设备时遇到的任何问题。
class WatchdogError(PatroniException):
"""管理 watchdog 设备时遇到的任何问题。"""
pass
1.13.7 类:ConfigParseError
- 定义了一个名为
ConfigParseError
的类,继承自PatroniException
。这种异常表示在加载或验证 Patroni 配置时识别到的任何问题。
class ConfigParseError(PatroniException):
"""在加载或验证 Patroni 配置时识别到的任何问题。"""
pass
1.14 log.py
存放自定义异常的文件。实现高层次的 Patroni 异常。更具体的异常可以在其他模块中找到,作为本模块中定义的任何异常的子类。
PatroniLogger
类:实现一个异步的日志记录机制,它主要用于 Patroni 守护进程中。ProxyHandler
类:代替尚未准备好的日志处理器来处理日志记录。
1.14.1 类:PatroniLogger
- 定义了一个名为
PatroniLogger
的类,该类继承自Thread
类。 - 这个类的作用是实现一个异步的日志记录机制,它主要用于 Patroni 守护进程中。具体来说:
- 日志消息排队:日志消息首先会被存入内存队列中。
- 异步刷新:由专门的日志线程异步地将队列中的日志消息刷新到最终的日志存储位置。
- 配置管理:提供了一系列的默认配置值,包括日志格式、日志级别、队列大小等。
- 线程安全:使用锁来保护对
log_handler
的修改,保证多线程环境下的安全性。
class PatroniLogger(Thread):
"""Patroni 守护进程的日志线程。
这是一个两步的日志记录方法。任何时候发出一条日志消息,它最初会在内存中排队,然后由日志线程异步刷新到最终目的地。
.. seealso::
:class:`QueueHandler`: 用于在内存中排队消息的对象。
:cvar DEFAULT_TYPE: 默认的日志格式类型(``plain``)。
:cvar DEFAULT_LEVEL: 默认的日志级别(``INFO``)。
:cvar DEFAULT_TRACEBACK_LEVEL: 默认的追踪日志级别(``ERROR``)。
:cvar DEFAULT_FORMAT: 默认的日志消息格式(``%(asctime)s %(levelname)s: %(message)s``)。
:cvar NORMAL_LOG_QUEUE_SIZE: 在正常情况下每个 HA 循环期望的日志消息数量。
:cvar DEFAULT_MAX_QUEUE_SIZE: 默认的最大队列大小,用于保存待刷新的日志消息的积压。
:cvar LOGGING_BROKEN_EXIT_CODE: 如果检测到日志记录故障时使用的退出码(``5``)。
:ivar log_handler: 当前线程当前使用的日志处理器。
:ivar log_handler_lock: 用于修改 ``log_handler`` 的锁。
"""
DEFAULT_TYPE = 'plain'
DEFAULT_LEVEL = 'INFO'
DEFAULT_TRACEBACK_LEVEL = 'ERROR'
DEFAULT_FORMAT = '%(asctime)s %(levelname)s: %(message)s'
NORMAL_LOG_QUEUE_SIZE = 2 # When everything goes normal Patroni writes only 2 messages per HA loop
DEFAULT_MAX_QUEUE_SIZE = 1000
LOGGING_BROKEN_EXIT_CODE = 5
1.14.1.1 __init__()
- 定义了一个名为
__init__
的实例方法,这是类的构造函数,该方法不需要任何参数,并且没有返回值(返回类型为None
)。
def __init__(self) -> None:
"""准备日志队列和代理处理器,当守护进程启动时它们会准备好。
.. note::
在 Patroni 启动期间,它保持 ``DEBUG`` 日志级别,并通过代理处理器写入日志消息。
一旦日志线程最终启动,它就会从代理处理器切换到基于队列的日志记录器,并应用配置的日志设置。
这种切换用于避免在日志线程正确启动之前发生的任何问题导致日志线程阻止 Patroni 关闭。
"""
# 调用父类 Thread 的构造函数来初始化线程的基础属性
super(PatroniLogger, self).__init__()
# 创建一个 QueueHandler 实例,该实例用于将日志消息放入队列中
self._queue_handler = QueueHandler()
# 获取根日志记录器,该记录器是 Python 日志模块中的全局日志记录器
self._root_logger = logging.getLogger()
# 初始化 _config 属性为 None,该属性将用来存储日志配置信息
self._config: Optional[Dict[str, Any]] = None
# 用来存储实际的日志处理器
self.log_handler = None
# 修改 log_handler 属性时保证线程安全
self.log_handler_lock = Lock()
# 用来存储旧的日志处理器
self._old_handlers: List[logging.Handler] = []
# 初始设置日志级别为 DEBUG,因为在日志线程尚未开始运行。
# 守护进程稍后会根据用户配置文件提供的信息调整所有与日志相关的设置。
self.reload_config({'level': 'DEBUG'})
# 我们只有在线程启动时才会切换到 QueueHandler。
# 这是必要的,以防止 Patroni 构造失败的情况,此时 PatroniLogger 线程仍然运行并阻止关闭。
self._proxy_handler = ProxyHandler(self)
self._root_logger.addHandler(self._proxy_handler)
作用:
这个构造函数的作用是在 Patroni 启动时初始化日志记录的相关组件,并设置初始的日志级别为 DEBUG
。具体来说:
- 初始化日志队列处理器:创建一个
QueueHandler
实例。 - 获取根日志记录器:获取全局的日志记录器。
- 初始化配置信息:设置
_config
为None
,等待后续配置。 - 初始化日志处理器:设置
log_handler
为None
。 - 创建锁对象:创建一个锁对象来保护
log_handler
的修改。 - 初始化旧处理器列表:创建一个空列表来存储旧的日志处理器。
- 设置初始日志级别:设置初始的日志级别为
DEBUG
。 - 设置代理处理器:创建一个
ProxyHandler
实例,并将其添加到根日志记录器中。
__init__
方法是 PatroniLogger
类的构造函数,用于初始化日志记录所需的各个组件,并设置初始的日志级别为 DEBUG
。这样做是为了确保在日志线程完全启动之前,可以记录详细的调试信息,并且在日志线程未能正确启动的情况下不会阻碍 Patroni 的关闭。通过这种方式,构造函数帮助实现了日志记录的初始化,并为后续的日志配置和处理做好了准备。
1.14.1.2 update_loggers()
- 定义了一个名为
update_loggers
的实例方法,该方法接受一个字典类型的参数config
,并返回None
。
def update_loggers(self, config: Dict[str, Any]) -> None:
"""配置自定义日志记录器的日志级别。
.. note::
它会创建尚未在日志管理器中定义的日志记录器对象。
:param config: :class:`dict` 对象,包含自定义日志记录器的配置,该配置可以从以下来源设置:
* Patroni 配置中的 ``log.loggers`` 部分;或者
* 试图确保节点名称不重复的方法(以消除烦人的 ``urllib3`` 警告)。
:Example:
.. code-block:: python
update_loggers({'urllib3.connectionpool': 'WARNING'})
"""
# 使用 deepcopy 创建 config 的深拷贝
loggers = deepcopy(config)
# 遍历根日志记录器管理器中的所有日志记录器
for name, logger in self._root_logger.manager.loggerDict.items():
# ``Placeholder`` 是日志管理器中的一个节点,表示尚未定义的日志记录器。我们只关心那些已经定义的。
if not isinstance(logger, logging.PlaceHolder):
# 如果这个日志记录器存在于 *config* 中,使用配置的日志级别,否则
# 使用 ``logging.NOTSET``,这意味着它将继承从任何父节点到根节点的日志级别。
level = loggers.pop(name, logging.NOTSET)
logger.setLevel(level)
# 定义尚未存在的日志记录器,并设置配置中的级别。
for name, level in loggers.items():
logger = self._root_logger.manager.getLogger(name)
logger.setLevel(level)
作用:
这个方法的作用是根据提供的配置更新日志记录器的日志级别。具体来说:
- 创建配置副本:创建
config
的深拷贝,以防止修改原始配置。 - 更新已存在的日志记录器:遍历所有已定义的日志记录器,如果它们在提供的配置中存在,则设置相应级别的日志级别;如果不存在,则设置为
NOTSET
,表示继承父节点的日志级别。 - 创建并设置新日志记录器:对于配置中存在的但尚未定义的日志记录器,创建它们,并设置相应的日志级别。
1.14.1.3 reload_config()
- 定义了一个名为
reload_config
的方法,它属于PatroniLogger
类的一个实例,并且该方法的返回类型为None
。该方法接收一个参数config
,类型为字典。
def reload_config(self, config: Dict[str, Any]) -> None:
"""应用与日志相关的配置。
.. note::
它也能处理运行时的配置变化。
:param config: 来自 Patroni 配置的 ``log`` 部分。
"""
# 检查当前配置 self._config 是否为空或与新配置 config 不同
if self._config is None or not deep_compare(self._config, config):
# 在队列锁的上下文中,设置队列的最大尺寸为配置中指定的 max_queue_size 值
with self._queue_handler.queue.mutex:
self._queue_handler.queue.maxsize = config.get('max_queue_size', self.DEFAULT_MAX_QUEUE_SIZE)
# 设置根日志记录器的日志级别为配置中指定的 level 值
self._root_logger.setLevel(config.get('level', PatroniLogger.DEFAULT_LEVEL))
# 根据配置中指定的 traceback_level 来决定是否以 DEBUG 级别显示堆栈跟踪
if config.get('traceback_level', PatroniLogger.DEFAULT_TRACEBACK_LEVEL).lower() == 'debug':
# 仅当'DEBUG '日志时显示堆栈跟踪。traceback_level ' ' ' is ' ' DEBUG ' '
logging.Logger.exception = debug_exception
else:
# 将堆栈跟踪显示为“ERROR”日志消息
logging.Logger.exception = error_exception
handler = self.log_handler
# 检查配置中是否存在 dir 键
if 'dir' in config:
# 如果配置中存在 dir 键,并且当前处理器不是 PatroniFileHandler 类型,则创建一个新的 PatroniFileHandler 实例。设置日志文件模式、最大文件大小以及备份文件数量
mode = parse_int(config.get('mode'))
if not isinstance(handler, PatroniFileHandler):
handler = PatroniFileHandler(os.path.join(config['dir'], __name__), mode)
handler.set_log_file_mode(mode)
max_file_size = int(config.get('file_size', 25000000))
handler.maxBytes = max_file_size # pyright: ignore [reportAttributeAccessIssue]
handler.backupCount = int(config.get('file_num', 4))
# 我们不能在下面使用if not isinstance(handler, logging.StreamHandler)
# 因为RotatingFileHandler和PatroniFileHandler是StreamHandler的子类!!
# 如果处理器为空或者是一个 PatroniFileHandler 类型,则创建一个新的 StreamHandler 实例作为处理器
elif handler is None or isinstance(handler, PatroniFileHandler):
handler = logging.StreamHandler()
# 检查新创建的处理器 handler 是否与当前处理器不同
is_new_handler = handler != self.log_handler
# 如果配置发生了改变或者处理器发生了改变,并且处理器存在,则获取一个格式化器并设置到处理器中
if (self._is_config_changed(config) or is_new_handler) and handler:
formatter = self._get_formatter(config)
handler.setFormatter(formatter)
# 如果处理器发生了改变,则在锁的上下文中替换当前的日志处理器为新的处理器,并将旧的处理器保存到_old_handlers列表中
if is_new_handler:
with self.log_handler_lock:
if self.log_handler:
self._old_handlers.append(self.log_handler)
self.log_handler = handler
# 将新的配置复制到 _config 变量,并更新其他日志记录器
self._config = config.copy()
self.update_loggers(config.get('loggers') or {})
作用:
reload_config
方法的具体作用是重新加载日志相关的配置。具体来说:
- 队列配置更新:
- 根据新的配置更新日志消息队列的最大尺寸。
- 日志级别更新:
- 根据新的配置更新根日志记录器的日志级别。
- 堆栈跟踪级别更新:
- 根据新的配置确定是否以
DEBUG
级别显示堆栈跟踪信息。
- 根据新的配置确定是否以
- 日志处理器更新:
- 根据新的配置创建或更新日志处理器,并设置相关属性如日志文件路径、模式、大小限制等。
- 日志格式化器更新:
- 如果配置或处理器发生改变,则创建新的格式化器并应用到日志处理器上。
- 日志记录器更新:
- 更新其他日志记录器的配置。
1.14.1.4 _is_config_changed()
- 定义了一个名为
_is_config_changed
的方法,它属于PatroniLogger
类的一个实例,并且该方法的返回类型为bool
。
def _is_config_changed(self, config: Dict[str, Any]) -> bool:
"""检查给定的配置是否与当前配置不同。
:param config: 来自 Patroni 配置的 ``log`` 部分。
:returns: 如果配置改变了则返回 ``True``,否则返回 ``False``。
"""
# 如果 _config 不存在,则将其赋值为空字典
old_config = self._config or {}
# 从旧配置 old_config 和新配置 config 中获取 type 字段的值
oldlogtype = old_config.get('type', PatroniLogger.DEFAULT_TYPE)
logtype = config.get('type', PatroniLogger.DEFAULT_TYPE)
# 从旧配置 old_config 和新配置 config 中获取 format 字段的值
oldlogformat: type_logformat = old_config.get('format', PatroniLogger.DEFAULT_FORMAT)
logformat: type_logformat = config.get('format', PatroniLogger.DEFAULT_FORMAT)
# 从旧配置 old_config 和新配置 config 中获取 dateformat 字段的值
olddateformat = old_config.get('dateformat') or None
dateformat = config.get('dateformat') or None # Convert empty string to `None`
# 从旧配置 old_config 和新配置 config 中获取 static_fields 字段的值
old_static_fields = old_config.get('static_fields', {})
static_fields = config.get('static_fields', {})
# 构建一个包含旧配置相关信息的新字典 old_log_config
old_log_config = {
'type': oldlogtype,
'format': oldlogformat,
'dateformat': olddateformat,
'static_fields': old_static_fields
}
# # 构建一个包含旧配置相关信息的新字典 old_log_config
log_config = {
'type': logtype,
'format': logformat,
'dateformat': dateformat,
'static_fields': static_fields
}
# 比较 old_log_config 和 log_config 是否相同
return not deep_compare(old_log_config, log_config)
作用:
_is_config_changed
方法的具体作用是检查给定的日志配置是否与当前正在使用的日志配置有所不同。具体来说:
- 提取配置项:
- 从新配置
config
和当前配置_config
中提取关键的配置项,如type
,format
,dateformat
,static_fields
。
- 从新配置
- 构建配置字典:
- 构建两个字典
old_log_config
和log_config
分别存放旧配置和新配置的关键项。
- 构建两个字典
- 比较配置项:
- 使用
deep_compare
方法来比较两个配置字典是否完全相同。如果不同,则返回True
表示配置有所改变;如果相同,则返回False
表示配置没有改变。
- 使用
1.14.1.5 _get_formatter()
- 定义了一个名为
_get_formatter
的方法,它属于PatroniLogger
类的一个实例,并且该方法的返回类型为logging.Formatter
。
def _get_formatter(self, config: Dict[str, Any]) -> logging.Formatter:
"""基于给定配置中的 logger 类型返回一个日志格式化器。
:param config: 来自 Patroni 配置的 ``log`` 部分。
:returns: 一个可以用来格式化日志记录的 :class:`logging.Formatter` 对象。
"""
# 从配置 config 中获取 type, format, dateformat, static_fields 字段的值
logtype = config.get('type', PatroniLogger.DEFAULT_TYPE)
logformat: type_logformat = config.get('format', PatroniLogger.DEFAULT_FORMAT)
dateformat = config.get('dateformat') or None # Convert empty string to `None`
static_fields = config.get('static_fields', {})
# 如果 dateformat 不是 str 类型,则发出警告,并将 dateformat 设为 None
if dateformat is not None and not isinstance(dateformat, str):
_LOGGER.warning('Expected log dateformat to be a string, but got "%s"', _type(dateformat))
dateformat = None
# 根据 logtype 的值选择不同的格式化器创建方式
if logtype == 'json':
formatter = self._get_json_formatter(logformat, dateformat, static_fields)
else:
formatter = self._get_plain_formatter(logformat, dateformat)
return formatter
作用:
_get_formatter
方法的具体作用是根据给定的日志配置信息创建一个 logging.Formatter
对象。具体来说:
- 提取配置项:
- 从配置
config
中提取关键的配置项,如type
,format
,dateformat
,static_fields
。
- 从配置
- 验证日期格式:
- 确认
dateformat
是否为字符串类型,如果不是,则发出警告并将其设为None
。
- 确认
- 选择格式化器:
- 根据
logtype
的值决定创建 JSON 格式的格式化器还是普通的文本格式化器。
- 根据
1.14.1.6 _get_json_formatter()
- 定义了一个名为
_get_json_formatter
的方法,它属于PatroniLogger
类的一个实例,并且该方法的返回类型为logging.Formatter
。
def _get_json_formatter(self, logformat: type_logformat, dateformat: Optional[str],
static_fields: Dict[str, Any]) -> logging.Formatter:
"""返回一个输出 JSON 格式消息的日志格式化器。
.. note::
如果 :mod:`pythonjsonlogger` 库没有安装,打印错误信息并返回一个普通的日志格式化器。
:param logformat: 指定 JSON 日志消息中的日志字段及其键名。
:param dateformat: 日志消息中时间戳的格式。
:param static_fields: 一个静态字段的字典,这些字段将添加到每条日志消息中。
:returns: 一个可以用来将日志记录格式化为 JSON 字符串的日志格式化器对象。
"""
# 如果 logformat 是一个字符串,则直接将其赋值给 jsonformat
if isinstance(logformat, str):
jsonformat = logformat
rename_fields = {}
# 如果 logformat 是一个列表,则初始化两个列表 log_fields 和字典 rename_fields
elif isinstance(logformat, list):
log_fields: List[str] = []
rename_fields: Dict[str, str] = {}
# 遍历 logformat 列表中的每一项,如果是字符串,则添加到 log_fields;如果是字典,则提取原始字段名和重命名后的字段名,并检查重命名后的字段名是否为字符串
for field in logformat:
if isinstance(field, str):
log_fields.append(field)
elif isinstance(field, dict):
for original_field, renamed_field in field.items():
if isinstance(renamed_field, str):
log_fields.append(original_field)
rename_fields[original_field] = renamed_field
else:
_LOGGER.warning(
'Expected renamed log field to be a string, but got "%s"',
_type(renamed_field)
)
# 如果 logformat 列表中的某一项既不是字符串也不是字典,则发出警告
else:
_LOGGER.warning(
'Expected each item of log format to be a string or dictionary, but got "%s"',
_type(field)
)
# 如果 log_fields 非空,则拼接成一个字符串 jsonformat;否则,使用默认格式
if len(log_fields) > 0:
jsonformat = ' '.join([f'%({field})s' for field in log_fields])
else:
jsonformat = PatroniLogger.DEFAULT_FORMAT
# 如果 logformat 既不是字符串也不是列表,则使用默认格式,并发出警告
else:
jsonformat = PatroniLogger.DEFAULT_FORMAT
rename_fields = {}
_LOGGER.warning('Expected log format to be a string or a list, but got "%s"', _type(logformat))
# 尝试导入 pythonjsonlogger 库,并兼容 Python 3.12 版本新增的 taskName 属性
try:
from pythonjsonlogger import jsonlogger
if hasattr(jsonlogger, 'RESERVED_ATTRS') \
and 'taskName' not in jsonlogger.RESERVED_ATTRS: # pyright: ignore [reportUnnecessaryContains]
# compatibility with python 3.12, that added a new attribute to LogRecord
jsonlogger.RESERVED_ATTRS += ('taskName',)
# 返回一个 JsonFormatter 对象
return jsonlogger.JsonFormatter(
jsonformat,
dateformat,
rename_fields=rename_fields,
static_fields=static_fields
)
except ImportError as e:
# 捕获 ImportError 异常,如果 pythonjsonlogger 库未能成功导入
_LOGGER.error('Failed to import "python-json-logger" library: %r. Falling back to the plain logger', e)
except Exception as e:
# 捕获其他异常,如果 JsonFormatter 初始化失败,则记录错误信息,并回退到普通的日志格式化器
_LOGGER.error('Failed to initialize JsonFormatter: %r. Falling back to the plain logger', e)
# 如果上述任何一步出现错误,则返回一个普通的日志格式化器
return self._get_plain_formatter(jsonformat, dateformat)
作用:
_get_json_formatter
方法的具体作用是根据给定的日志配置信息创建一个 JsonFormatter
对象,该对象能够输出 JSON 格式的消息。具体来说:
- 解析
logformat
参数:- 如果
logformat
是字符串,则直接使用; - 如果
logformat
是列表,则解析每个元素,支持字段重命名; - 如果
logformat
既不是字符串也不是列表,则使用默认格式。
- 如果
- 创建
JsonFormatter
对象:- 尝试导入
pythonjsonlogger
库,并创建JsonFormatter
实例; - 如果导入或初始化失败,则回退到普通的日志格式化器。
- 尝试导入
1.14.1.7 _get_plain_formatter()
- 定义了一个名为
_get_plain_formatter
的方法,它属于PatroniLogger
类的一个实例,并且该方法的返回类型为logging.Formatter
。
def _get_plain_formatter(self, logformat: type_logformat, dateformat: Optional[str]) -> logging.Formatter:
"""返回一个具有指定格式和日期格式的日志格式化器。
.. note::
如果日志格式不是一个字符串,打印警告信息并使用默认的日志格式。
:param logformat: 日志消息的格式。
:param dateformat: 日志消息中时间戳的格式。
:returns: 一个可以用来格式化日志记录的日志格式化器对象。
"""
# 如果 logformat 不是一个字符串,则发出警告
if not isinstance(logformat, str):
_LOGGER.warning('Expected log format to be a string when log type is plain, but got "%s"', _type(logformat))
logformat = PatroniLogger.DEFAULT_FORMAT
# 返回一个 logging.Formatter 对象
return logging.Formatter(logformat, dateformat)
作用:
_get_plain_formatter
方法的具体作用是根据给定的日志配置信息创建一个 logging.Formatter
对象,该对象能够按照指定的格式输出普通的文本日志消息。具体来说:
- 验证
logformat
参数:- 如果
logformat
不是字符串,则使用默认格式,并发出警告信息。
- 如果
- 创建
Formatter
对象:- 使用验证后的
logformat
和dateformat
创建logging.Formatter
对象。
- 使用验证后的
1.14.1.8 update_loggers()
- 定义了一个名为
update_loggers
的方法,它属于PatroniLogger
类的一个实例,并且该方法不返回任何值。
def update_loggers(self, config: Dict[str, Any]) -> None:
"""配置自定义记录器的日志级别。
.. note::
它会创建日志管理器中尚未定义的日志记录器对象。
:param config: 包含自定义日志记录器配置的 :class:`dict` 对象,其设置来源于:
* Patroni 配置中的 ``log.loggers`` 部分;或者
* 来自于试图确保节点名称不重复的方法(以消除烦人的 ``urllib3`` 警告)。
:Example:
.. code-block:: python
update_loggers({'urllib3.connectionpool': 'WARNING'})
"""
# 复制 config 到一个新的字典 loggers,避免修改原始配置
loggers = deepcopy(config)
# 遍历根日志记录器管理器中的所有日志记录器
for name, logger in self._root_logger.manager.loggerDict.items():
# ``Placeholder`` 是日志管理器中的一个节点,对于该节点没有定义日志记录器。我们只对那些已经定义的感兴趣。
if not isinstance(logger, logging.PlaceHolder):
# 如果这个日志记录器存在于 *config* 中,则使用配置的级别,否则使用 ``logging.NOTSET`` ,这意味着它将继承从任何父节点到已定义日志级别的根节点的级别。
level = loggers.pop(name, logging.NOTSET)
logger.setLevel(level)
# 定义那些尚不存在的日志记录器,并根据配置设置级别
for name, level in loggers.items():
logger = self._root_logger.manager.getLogger(name)
logger.setLevel(level)
作用:
update_loggers
方法的具体作用是根据给定的日志配置信息更新日志记录器的日志级别。具体来说:
- 复制配置:
- 复制传入的配置字典
config
到新的字典loggers
。
- 复制传入的配置字典
- 遍历现有的日志记录器:
- 遍历日志管理器中的所有日志记录器,并且对于每一个实际存在的日志记录器(非占位符),如果在配置字典
loggers
中存在相应的级别设置,则设置其日志级别,否则保持默认设置。
- 遍历日志管理器中的所有日志记录器,并且对于每一个实际存在的日志记录器(非占位符),如果在配置字典
- 定义并设置未存在的日志记录器:
- 对于配置字典
loggers
中剩余的日志记录器名称,创建这些日志记录器,并设置相应的日志级别。
- 对于配置字典
1.14.2 类:ProxyHandler
- 定义了一个名为
ProxyHandler
的类,它继承自 Python 的logging.Handler
类。
class ProxyHandler(logging.Handler):
"""代替尚未准备好的日志处理器来处理日志记录。
.. note::
这是在日志线程还未启动时用来处理日志消息的情况,在这种情况下基于队列的处理器还未启动。
:ivar patroni_logger: 日志线程。
"""
1.14.2.1 __init__()
- 定义了一个名为
__init__
的实例方法,这是类的构造函数,该方法不需要任何参数,并且没有返回值(返回类型为None
)。
def __init__(self, patroni_logger: 'PatroniLogger') -> None:
"""Create a new :class:`ProxyHandler` instance.
:param patroni_logger: the logger thread.
"""
super().__init__()
self.patroni_logger = patroni_logger
作用:
ProxyHandler
类的具体作用是在日志线程还未启动时处理日志记录。当应用程序开始运行并且日志记录功能还没有完全初始化时,ProxyHandler
类可以作为一个临时的日志处理器来捕捉日志记录。具体来说:
- 替代处理:
- 在实际的日志处理器(通常是基于队列的日志处理器)还未启动之前,
ProxyHandler
会处理日志记录。
- 在实际的日志处理器(通常是基于队列的日志处理器)还未启动之前,
- 持有日志线程引用:
patroni_logger
变量用于持有PatroniLogger
类型的日志线程的引用。当实际的日志处理器准备好后,可以通过这个变量来传递日志记录到正式的日志处理器中去。
1.14.3 类:QueueHandler
- 定义了一个名为
QueueHandler
的类,它继承自 Python 的logging.Handler
类。
class QueueHandler(logging.Handler):
"""基于队列的日志处理器。
:ivar queue: 用于保存待刷新到最终目的地的日志消息的队列。
"""
1.14.3.1 __init__()
- 定义了
QueueHandler
类的构造函数__init__
,它没有返回值(None
类型)。
def __init__(self) -> None:
"""初始化队列并建立初始的丢失记录计数。"""
super().__init__()
self.queue: Queue[Union[logging.LogRecord, None]] = Queue()
# 用于记录由于队列满等原因导致的日志记录丢失的数量
self._records_lost = 0
作用:
__init__
方法的具体作用是初始化 QueueHandler
类的一个实例,包括:
- 初始化队列:
- 创建一个队列
self.queue
用于存放日志记录。
- 创建一个队列
- 初始化丢失记录计数:
- 设置
_records_lost
初始值为 0,用于跟踪由于队列满等原因未能成功入队的日志记录数量。
- 设置
1.14.4 类:PatroniFileHandler
- 定义了一个名为
PatroniFileHandler
的类,它继承自 Python 的RotatingFileHandler
类。
class PatroniFileHandler(RotatingFileHandler):
"""RotatingFileHandler 的包装器,用于处理日志文件的权限。 """
1.14.4.1 __init__()
- 定义了
PatroniFileHandler
类的构造函数__init__
,该函数接受两个参数:filename
和mode
,并返回None
。
def __init__(self, filename: str, mode: Optional[int]) -> None:
"""创建一个新的 PatroniFileHandler 实例。
:param filename: 日志文件的基本名称。
:param mode: 日志文件的权限。
"""
# 设置日志文件的权限
self.set_log_file_mode(mode)
# 调用父类 RotatingFileHandler 的构造函数来初始化父类的部分
super(PatroniFileHandler, self).__init__(filename)
作用:
__init__
方法的具体作用是初始化 PatroniFileHandler
类的一个实例,包括:
- 设置日志文件权限:
- 在构造函数中首先调用
set_log_file_mode
方法来设置日志文件的权限。
- 在构造函数中首先调用
- 初始化父类:
- 调用父类
RotatingFileHandler
的构造函数来初始化父类的部分,并传入日志文件的基本名称filename
。
- 调用父类
1.14.4.2 set_log_file_mode()
- 定义了
PatroniFileHandler
类的方法set_log_file_mode
,该方法接受一个可选的整数参数mode
,并返回None
。
def set_log_file_mode(self, mode: Optional[int]) -> None:
"""设置 Patroni 日志文件的模式。
:param mode: 日志文件的权限。
.. note::
如果 mode 未指定,我们将从 `umask` 值计算它。
"""
# 如果 mode 参数没有指定,则从原始的 umask 值计算出权限,并将其设置为 _log_file_mode;如果 mode 参数有指定,则直接将 mode 赋值给 _log_file_mode
self._log_file_mode = 0o666 & ~pg_perm.orig_umask if mode is None else mode
作用:
set_log_file_mode
方法的具体作用是设置日志文件的权限。具体来说:
- 设置日志文件权限:
- 该方法接收一个
mode
参数,表示要设置的日志文件权限。 - 如果
mode
参数未提供,则根据当前系统的umask
值来计算权限。通常umask
默认值为0022
或者0000
,这决定了新创建的文件默认权限。 - 计算权限的方式是使用
0o666 & ~umask
,这会得到一个与umask
相关的权限值,通常表示所有者可读写,而其他用户根据umask
的设置有不同的权限。 - 如果
mode
参数提供了具体的权限值,则直接使用提供的值。
- 该方法接收一个
1.15 watchdog文件夹
1.15.1 __init__.py
watchdog文件夹的初始化方法代码。
from patroni.watchdog.base import Watchdog, WatchdogError
__all__ = ['WatchdogError', 'Watchdog']
作用
- 导入必需的类和异常:从
patroni.watchdog.base
模块导入Watchdog
类和WatchdogError
异常,使得当前模块可以使用这两个类或异常。 - 定义公开符号:通过
__all__
列表定义当前模块对外公开的类或异常的名称,使得其他模块可以通过import *
的方式导入这些类或异常。
1.15.2 base.py
parse_mode
函数:解析传入的mode
参数,并根据其值返回一个标准化的模式字符串。Watchdog
类:提供一个统一的接口来管理不同类型的看门狗实现,并处理配置变化。WatchdogConfig
类:这个类的作用是用来存储看门狗配置的一个快照。NullWatchdog
类:这个类的作用是在不支持看门狗功能的环境中提供一个空的实现。WatchdogBase
类:定义了看门狗的基本行为和接口规范。
1.15.2.1 parse_mode()
- 定义了一个名为
parse_mode
的函数,该函数接受一个类型为Union[bool, str]
的参数mode
,并返回一个字符串类型的值。
def parse_mode(mode: Union[bool, str]) -> str:
# 如果 mode 的值是 False,则返回 MODE_OFF
if mode is False:
return MODE_OFF
# 转换为小写
mode = str(mode).lower()
if mode in ['require', 'required']:
return MODE_REQUIRED
elif mode in ['auto', 'automatic']:
return MODE_AUTOMATIC
else:
if mode not in ['off', 'disable', 'disabled']:
logger.warning("Watchdog mode {0} not recognized, disabling watchdog".format(mode))
return MODE_OFF
作用:
这个函数的作用是解析传入的 mode
参数,并根据其值返回一个标准化的模式字符串。具体来说:
- 处理布尔值:如果
mode
是False
,则直接返回MODE_OFF
。 - 转换为小写字符串:将
mode
转换成字符串,并转换成小写形式,以便于比较。 - 识别模式:
- 如果
mode
的值是require
或required
,则返回MODE_REQUIRED
。 - 如果
mode
的值是auto
或automatic
,则返回MODE_AUTOMATIC
。 - 如果
mode
的值是off
、disable
或disabled
,则返回MODE_OFF
。 - 如果
mode
的值不属于以上情况,则记录一条警告信息,并返回MODE_OFF
。
- 如果
1.15.2.2 类:Watchdog
- 定义了一个名为
Watchdog
的类,该类继承自object
。 - 这个类的作用是提供一个统一的接口来管理不同类型的看门狗实现,并处理配置变化。具体来说:
- 外观模式:作为一个外观类,
Watchdog
提供了一个简单的接口来管理多种不同的看门狗实现。 - 配置变更处理:当配置发生变化时,
Watchdog
会根据新的配置来调整看门狗的行为。 - 容错机制:如果激活看门狗失败,
Watchdog
会切换到一个空实现(Null
实现),该实现在不执行任何实际操作的同时,避免了因不断尝试激活而导致的日志垃圾信息。
- 外观模式:作为一个外观类,
class Watchdog(object):
"""用于动态管理看门狗实现并处理配置更改的外观类。
当激活失败时,底层实现将切换到一个空实现。为了避免日志垃圾信息,只有在看门狗配置更改时才会重试激活。"""
1.15.2.2.1 __init__()
- 定义了一个名为
__init__
的构造函数,该函数接受一个类型为Config
的参数config
,并返回None
。
def __init__(self, config: Config) -> None:
# 将传入的配置 config 转换为 WatchdogConfig 对象
self.config = WatchdogConfig(config)
# 将当前活动的配置初始化为与 self.config 相同的 WatchdogConfig 对象
self.active_config: WatchdogConfig = self.config
# 创建一个可重入锁(RLock)实例
self.lock = RLock()
# 初始化 self.active 标志为 False,表示当前看门狗还未激活
self.active = False
# 根据配置中的模式来选择看门狗的实现
if self.config.mode == MODE_OFF:
self.impl = NullWatchdog()
else:
self.impl = self.config.get_impl()
if self.config.mode == MODE_REQUIRED and self.impl.is_null:
logger.error("Configuration requires a watchdog, but watchdog is not supported on this platform.")
sys.exit(1)
作用:
这个构造函数的作用是初始化 Watchdog
类的实例,并根据提供的配置来设置看门狗的实现方式。具体来说:
- 配置初始化:将传入的配置转换为
WatchdogConfig
对象,并分别存储为当前配置和活动配置。 - 锁初始化:创建一个可重入锁,用于保护并发访问时的数据一致性。
- 激活标志初始化:初始化激活标志为
False
,表示看门狗尚未被激活。 - 看门狗实现初始化:
- 如果配置要求禁用看门狗(
MODE_OFF
),则使用一个空实现。 - 否则,根据配置获取相应的看门狗实现。
- 如果配置要求必须启用看门狗(
MODE_REQUIRED
),但是当前平台不支持看门狗,则记录错误日志并终止程序。
- 如果配置要求必须启用看门狗(
- 如果配置要求禁用看门狗(
1.15.2.3 类:WatchdogConfig
- 定义了一个名为
WatchdogConfig
的类,该类继承自object
。 - 这个类的作用是用来存储看门狗配置的一个快照。具体来说:
- 配置封装:将看门狗的配置信息封装在一个对象中,便于管理和访问。
- 配置快照:提供一个配置的快照,可以在需要时读取配置而不影响当前的配置状态。
- 辅助功能:作为辅助类,它可以帮助简化配置管理的过程,使看门狗类可以更容易地使用配置信息。
class WatchdogConfig(object):
"""Helper to contain a snapshot of configuration"""
1.15.2.3.1 __init__()
- 定义了一个名为
__init__
的构造函数,该函数接受一个类型为Config
的参数config
,并返回None
。
def __init__(self, config: Config) -> None:
# 从传入的配置对象 config 中获取 watchdog 子配置
watchdog_config = config.get("watchdog") or {'mode': 'automatic'}
# 根据 watchdog_config 中的 mode 字段获取模式
self.mode = parse_mode(watchdog_config.get('mode', 'automatic'))
self.ttl = config['ttl']
self.loop_wait = config['loop_wait']
self.safety_margin = watchdog_config.get('safety_margin', 5)
self.driver = watchdog_config.get('driver', 'default')
# 创建一个新的字典 self.driver_config
self.driver_config = dict((k, v) for k, v in watchdog_config.items()
if k not in ['mode', 'safety_margin', 'driver'])
作用:
这个构造函数的作用是初始化 WatchdogConfig
类的实例,并根据传入的配置对象 config
设置各个属性的值。具体来说:
- 初始化模式:根据
watchdog
子配置中的mode
字段初始化self.mode
,如果不存在则默认为'automatic'
,并通过parse_mode
函数转换为标准模式字符串。 - 设置 TTL 和 Loop Wait 时间:从
config
中获取ttl
和loop_wait
字段的值,并赋值给相应的实例变量。 - 初始化安全边际:从
watchdog
子配置中获取safety_margin
字段的值,并赋值给self.safety_margin
,如果该字段不存在,则默认为5
。 - 初始化驱动:从
watchdog
子配置中获取driver
字段的值,并赋值给self.driver
,如果该字段不存在,则默认为'default'
。 - 初始化驱动配置:创建一个新字典
self.driver_config
,该字典包含了watchdog
子配置中除了mode
、safety_margin
和driver
字段之外的所有配置项。
1.15.2.3.2 get_impl()
- 定义了一个名为
get_impl
的实例方法,该方法返回一个WatchdogBase
的子类实例。
def get_impl(self) -> 'WatchdogBase':
if self.driver == 'testing': # pragma: no cover
from patroni.watchdog.linux import TestingWatchdogDevice
return TestingWatchdogDevice.from_config(self.driver_config)
elif platform.system() == 'Linux' and self.driver == 'default':
from patroni.watchdog.linux import LinuxWatchdogDevice
return LinuxWatchdogDevice.from_config(self.driver_config)
else:
return NullWatchdog()
作用:
这个方法的作用是根据当前配置选择合适的看门狗实现,并返回相应的实例。具体来说:
- 测试模式:如果配置中的
driver
字段值为'testing'
,则返回一个TestingWatchdogDevice
实例。 - Linux 默认模式:如果当前系统为 Linux 且配置中的
driver
字段值为'default'
,则返回一个LinuxWatchdogDevice
实例。 - 其他情况:在以上两种情况以外,返回一个
NullWatchdog
实例,表示不使用实际的看门狗实现。
1.15.2.3.3 reload_config()
- 定义了一个名为
reload_config
的实例方法,该方法接受一个类型为Config
的参数config
,并返回None
。该方法使用了@synchronized
装饰器,确保该方法在一个多线程环境下是同步执行的。 - 此方法用于重新加载配置信息,并根据新的配置信息调整当前看门狗的状态。
@synchronized
def reload_config(self, config: Config) -> None:
# 将传入的配置信息 config 转换为 WatchdogConfig 类型的对象,并赋值给实例变量 self.config。
self.config = WatchdogConfig(config)
# 关闭看门狗总是可以立即执行。
# 查新的配置模式是否为关闭模式 (MODE_OFF)。如果是关闭模式,则立即执行关闭操作
if self.config.mode == MODE_OFF:
# 如果当前看门狗是活跃状态 (self.active 为 True),则调用 _disable 方法关闭看门狗。
if self.active:
self._disable()
# 更新 active_config 为新的配置,并将 impl 设置为 NullWatchdog 实例。
self.active_config = self.config
self.impl = NullWatchdog()
# 如果看门狗不是活跃状态,我们可以立即应用配置以尽早显示任何警告。
# 否则我们需要延迟直到下次发送 keepalive 信号时,以确保超时时间和领导者键更新匹配。
if not self.active:
# 如果新的配置中的 driver 或 driver_config 与当前活动配置不同,则更新 impl 为新的配置所对应的实现。
if self.config.driver != self.active_config.driver or \
self.config.driver_config != self.active_config.driver_config:
self.impl = self.config.get_impl()
self.active_config = self.config
作用:
这个方法的作用是根据新的配置信息重新加载看门狗的配置,并根据配置信息调整当前看门狗的状态。具体来说:
- 更新配置信息:将传入的配置信息转换为
WatchdogConfig
类型,并保存在self.config
中。 - 关闭看门狗:如果新的配置模式为关闭模式 (
MODE_OFF
),则关闭当前活跃的看门狗,并将实现设置为NullWatchdog
。 - 非活跃状态下的配置更新:如果看门狗当前不是活跃状态,则直接应用新的配置信息,并更新
impl
为新的配置所对应的实现。 - 活跃状态下的配置更新:如果看门狗当前是活跃状态,则记录新的配置信息,并延迟到下次发送
keepalive
信号时再应用新的配置信息。
1.15.2.3.4 _disable()
- 定义了一个名为
_disable
的实例方法,该方法没有参数,并返回None
。 - 此方法用于禁用当前激活的看门狗。
def _disable(self) -> None:
try:
# 检查当前看门狗实现是否正在运行,并且是否不能被禁用
if self.impl.is_running and not self.impl.can_be_disabled:
# 给予系统管理员一些额外的时间来清理工作。
self.impl.keepalive()
logger.warning("Watchdog implementation can't be disabled. System will reboot after "
"{0} seconds when watchdog times out.".format(self.impl.get_timeout()))
# 关闭当前的看门狗实现
self.impl.close()
except WatchdogError as e:
logger.error("Error while disabling watchdog: %s", e)
作用:
这个方法的作用是在禁用看门狗时执行一系列的安全措施,确保系统能够安全地关闭看门狗。具体来说:
- 检查状态:首先检查当前看门狗是否正在运行,并且是否可以被禁用。
- 重置计时器:如果看门狗不能被禁用,则重置看门狗计时器,并记录一条警告日志信息。
- 关闭看门狗:关闭当前的看门狗实现。
- 异常处理:如果在关闭看门狗的过程中发生了异常,则记录一条错误日志信息。
1.15.2.4 类:NullWatchdog
- 定义了一个名为
NullWatchdog
的类,该类继承自WatchdogBase
。 - 这个类的作用是在不支持看门狗功能的环境中提供一个空的实现。具体来说:
- 空实现:提供了一个没有任何实际操作的方法实现,包括
open
、close
和keepalive
方法,这些方法都不执行任何动作。 - 超长时间间隔:提供了一个
get_timeout
方法,该方法返回一个非常大的数值,表示一个足够长的时间间隔,使得在不支持看门狗的情况下,这个时间间隔不会对系统产生实质性的影响。
- 空实现:提供了一个没有任何实际操作的方法实现,包括
class NullWatchdog(WatchdogBase):
"""当不支持看门狗时使用的空实现。"""
is_null = True
1.15.2.4.1 open()
- 定义了一个名为
open
的方法,该方法没有执行任何操作,直接返回。
def open(self) -> None:
return
1.15.2.4.2 close()
- 定义了一个名为
close
的方法,该方法没有执行任何操作,直接返回。
def close(self) -> None:
return
1.15.2.4.3 keepalive()
- 定义了一个名为
keepalive
的方法,该方法没有执行任何操作,直接返回。
def keepalive(self) -> None:
return
1.15.2.4.4 get_timeout()
- 定义了一个名为
get_timeout
的方法,该方法返回一个非常大的整数(10亿),表示一个足够长的时间间隔,使得在不支持看门狗的环境中,这个时间间隔实际上不会产生影响。
def get_timeout(self) -> int:
# 一个大到无关紧要的数字
return 1000000000
1.15.2.5 类:WatchdogBase
- 定义了一个名为
WatchdogBase
的抽象基类(Abstract Base Class,简称 ABC),该类继承自abc.ABC
。 - 此类定义了看门狗的基本行为和接口规范。
WatchdogBase
类定义了看门狗的基本行为和接口规范,具体来说:- 抽象方法:定义了四个抽象方法
open
、close
、keepalive
和get_timeout
,它们需要在子类中实现具体的逻辑。 - 属性:定义了三个属性
is_running
、is_healthy
和can_be_disabled
,它们用于描述看门狗的状态。 - 非抽象方法:定义了两个非抽象方法
has_set_timeout
和set_timeout
,它们用于检查和设置超时时间。 - 描述方法:定义了一个方法
describe
用于返回人类可读的设备名称。 - 类方法:定义了一个类方法
from_config
用于从配置创建看门狗实例。
- 抽象方法:定义了四个抽象方法
class WatchdogBase(abc.ABC):
"""一个打开后的看门狗对象需要周期性的调用 keepalive。
如果在超时时间内没有调用 keepalive,系统将会终止。"""
# 定义了一个类变量 is_null 并设置其值为 False,表示这不是一个空实现。
is_null = False
1.15.2.5.1 is_running()
- 定义了一个名为
is_running
的只读属性,该属性返回一个布尔值,表示看门狗是否处于激活状态并且能够执行任务。默认情况下返回False
。
@property
def is_running(self) -> bool:
"""当看门狗被激活并且能够执行其任务时返回 True。"""
return False
1.15.2.5.2 is_healthy()
- 定义了一个名为
is_healthy
的只读属性,该属性返回一个布尔值,表示调用open()
是否已知会失败。默认情况下返回False
。
@property
def is_healthy(self) -> bool:
"""当已知调用 open() 会失败时返回 False。"""
return False
1.15.2.5.3 can_be_disabled()
- 定义了一个名为
can_be_disabled
的只读属性,该属性返回一个布尔值,表示通过调用close()
是否可以关闭看门狗。一些看门狗设备一旦激活就会一直运行,无论之后发生什么。如果没有先调用open()
就调用该属性可能会引发WatchdogError
。默认情况下返回True
。
@property
def can_be_disabled(self) -> bool:
"""当通过调用 close() 可以关闭看门狗时返回 True。一些看门狗设备一旦激活就会一直运行,无论之后发生什么。
如果没有先调用 open() 就调用该属性可能会引发 WatchdogError。"""
return True
1.15.2.5.4 open()
- 定义了一个名为
open
的抽象方法,该方法用于打开看门狗设备。如果看门狗设备成功打开,则不返回任何值;如果无法打开设备,则抛出WatchdogError
。
@abc.abstractmethod
def open(self) -> None:
"""打开看门狗设备。
当看门狗设备被打开后必须调用 keepalive。如果成功则不返回任何值,或者如果无法打开设备则抛出 WatchdogError。"""
1.15.2.5.5 close()
- 定义了一个名为
close
的抽象方法,该方法用于优雅地关闭看门狗设备。
@abc.abstractmethod
def close(self) -> None:
"""优雅地关闭看门狗设备。"""
1.15.2.5.6 is_running()
- 定义了一个名为
keepalive
的抽象方法,该方法用于重置看门狗计时器。只有在看门狗设备被打开的情况下才能调用keepalive
。
@abc.abstractmethod
def keepalive(self) -> None:
"""重置看门狗计时器。
当调用 keepalive 时看门狗必须是打开状态。"""
1.15.2.5.7 get_timeout()
- 定义了一个名为
get_timeout
的抽象方法,该方法用于返回当前有效的 keepalive 超时时间(秒)。
@abc.abstractmethod
def get_timeout(self) -> int:
"""返回当前有效的 keepalive 超时时间。"""
1.15.2.5.8 has_set_timeout()
- 定义了一个名为
has_set_timeout
的非抽象方法,该方法用于检查是否支持设置超时时间。默认情况下返回False
。
def has_set_timeout(self) -> bool:
"""如果支持设置超时时间则返回 True。"""
return False
1.15.2.5.9 set_timeout()
- 定义了一个名为
set_timeout
的方法,该方法用于设置看门狗计时器的超时时间。默认实现中抛出WatchdogError
,表明不支持设置超时时间。
def set_timeout(self, timeout: int) -> None:
"""设置看门狗计时器的超时时间。
:param timeout: 看门狗超时时间(秒)"""
raise WatchdogError("Setting timeout is not supported on {0}".format(self.describe()))
1.15.2.5.10 describe()
- 定义了一个名为
describe
的方法,该方法用于返回一个人类可读的设备名称。默认返回当前类的名称。
def describe(self) -> str:
"""返回一个人类可读的设备名称。"""
return self.__class__.__name__
1.15.2.5.11 from_config()
- 定义了一个名为
from_config
的类方法,该方法接受一个配置字典,并返回一个WatchdogBase
的实例。默认实现直接返回当前类的实例。
@classmethod
def from_config(cls, config: Dict[str, Any]) -> 'WatchdogBase':
return cls()
1.15.3 linux.py
parse_mode
函数:解析传入的mode
参数,并根据其值返回一个标准化的模式字符串。Watchdog
类:提供一个统一的接口来管理不同类型的看门狗实现,并处理配置变化。WatchdogConfig
类:这个类的作用是用来存储看门狗配置的一个快照。NullWatchdog
类:这个类的作用是在不支持看门狗功能的环境中提供一个空的实现。WatchdogBase
类:定义了看门狗的基本行为和接口规范。
1.15.3.1 类:LinuxWatchdogDevice
- 定义了一个名为
LinuxWatchdogDevice
的类,该类继承自WatchdogBase
。 - 此类提供了针对 Linux 系统的看门狗设备的具体实现。
class LinuxWatchdogDevice(WatchdogBase):
DEFAULT_DEVICE = '/dev/watchdog'
1.15.3.1.1 __init__()
- 定义了一个名为
__init__
的构造函数,该函数接受一个类型为str
的参数device
,并返回None
。
def __init__(self, device: str) -> None:
self.device = device
# 用于缓存看门狗是否支持的信息
self._support_cache = None
# 用于存储文件描述符
self._fd = None
作用:
这个 __init__
函数的作用是对 LinuxWatchdogDevice
类的实例进行初始化。具体来说,它的作用包括:
- 初始化设备路径:
- 接受一个字符串参数
device
,这个参数通常是看门狗设备的路径,例如/dev/watchdog
。 - 将这个路径保存在实例变量
self.device
中,这样在后续的方法中就可以使用这个路径来操作看门狗设备。
- 接受一个字符串参数
- 初始化支持信息缓存:
- 创建一个名为
_support_cache
的实例变量,并将其初始值设为None
。 - 这个变量用于缓存看门狗设备是否支持当前操作的信息,避免每次都需要检查看门狗设备的支持情况。
- 创建一个名为
- 初始化文件描述符:
- 创建一个名为
_fd
的实例变量,并将其初始值设为None
。 - 这个变量将在后续的看门狗设备操作中用来存储文件描述符,例如当看门狗设备被打开时,文件描述符会被存储在这里。
- 创建一个名为
1.15.3.1.2 from_config()
- 定义了一个名为
from_config
的类方法,该方法接受一个类型为Dict[str, Any]
的参数config
,并返回一个LinuxWatchdogDevice
的实例。 - 此类方法通常用于从配置信息创建类的实例。
@classmethod
def from_config(cls, config: Dict[str, Any]) -> 'LinuxWatchdogDevice':
# 从 config 字典中获取 device 键的值,如果该键不存在,则使用类变量 DEFAULT_DEVICE 的值作为默认值。
device = config.get('device', cls.DEFAULT_DEVICE)
# 使用获取到的 device 值创建并返回一个 cls 类的实例
return cls(device)
作用:
这个类方法的作用是从配置信息中创建一个 LinuxWatchdogDevice
的实例。具体来说:
- 获取设备路径:从提供的配置字典
config
中获取device
键的值。如果没有提供device
键,则使用类变量DEFAULT_DEVICE
的值作为默认的设备路径。 - 创建实例:使用获取到的设备路径创建一个
LinuxWatchdogDevice
的实例,并返回该实例。
1.15.3.2 类:TestingWatchdogDevice
- 定义了一个名为
TestingWatchdogDevice
的类,该类继承自LinuxWatchdogDevice
。 TestingWatchdogDevice
类的作用是提供一个用于测试的看门狗设备实现。具体来说:- 模拟真实设备:通过继承
LinuxWatchdogDevice
类,它可以模拟真实的 Linux 看门狗设备的行为。 - 通过命名管道通信:它通过命名管道而不是实际的硬件设备来进行通信,这样可以方便地在测试环境中拦截和控制看门狗设备的行为。
- 支持功能声明:它声明了支持的功能,如
MAGICCLOSE
和SETTIMEOUT
。 - 设置和获取超时时间:提供方法来设置和获取超时时间,以模拟看门狗设备的行为。
- 模拟真实设备:通过继承
class TestingWatchdogDevice(LinuxWatchdogDevice): # pragma: no cover
"""将超时 ioctl 调用转换为可以通过命名管道拦截的常规写入。"""
timeout = 60
1.15.3.2.1 get_support()
- 定义了一个名为
get_support
的方法,该方法返回一个WatchdogInfo
实例,表示该测试看门狗设备支持的功能。这里设置了支持MAGICCLOSE
和SETTIMEOUT
功能,并附带一个描述字符串"Watchdog test harness"
。
def get_support(self) -> WatchdogInfo:
return WatchdogInfo(WDIOF['MAGICCLOSE'] | WDIOF['SETTIMEOUT'], 0, "Watchdog test harness")
1.15.3.2.2 set_timout()
- 定义了一个名为
set_timeout
的方法,该方法接受一个整数参数timeout
,用于设置看门狗的超时时间。
def set_timeout(self, timeout: int) -> None:
if self._fd is None:
raise WatchdogError("Watchdog device is closed")
buf = "Ctimeout={0}\n".format(timeout).encode('utf8')
while len(buf):
buf = buf[os.write(self._fd, buf):]
self.timeout = timeout
该方法首先检查文件描述符 _fd
是否存在,如果不存在,则抛出 WatchdogError
。然后构建一个包含超时信息的字符串缓冲区 buf
,并通过 os.write
方法将该缓冲区的内容写入文件描述符 _fd
。最后更新实例变量 self.timeout
为新的超时时间。
1.15.3.2.3 get_timeout()
- 定义了一个名为
get_timeout
的方法,该方法返回当前设置的超时时间。
def get_timeout(self) -> int:
return self.timeout
1.16 collections.py
"""Patroni 的自定义对象类型在某种程度上类似于collections模块。
提供了一种不区分大小写的dict和set对象类型,以及EMPTY_DICT(一个不可变的字典对象)。
"""
CaseInsensitiveSet
类:一个不区分大小写的类似字典(dict)的对象。__init__
函数:初始化一个新的CaseInsensitiveDict
实例,并可以从提供的初始数据创建。__repr__
函数:返回一个字符串,该字符串描述了当前对象的状态,并且可以用来重新创建该对象。__str__
函数:返回一个字符串,该字符串描述了当前对象的状态,通常用于输出或显示目的。__contains__
函数:检查给定的值value
是否存在于当前集合中,而且这个检查是不区分大小写的。__iter__
函数:返回一个迭代器,允许外部代码通过迭代协议遍历集合中的每个元素。__len__
函数:返回一个整数,表示当前集合中元素的数量。add
函数:向集合中添加一个新值value
。discard
函数:从集合中移除一个指定的值value
。issubset
函数:检查当前集合 (self
) 是否是另一个集合 (other
) 的子集。
CaseInsensitiveDict
类:一个不区分大小写的 :class:set
类似对象。__init__
函数:初始化一个新的CaseInsensitiveSet
实例,并可以从提供的初始数据创建。__setitem__
函数:在字典中设置或更新一个键值对。__getitem__
函数:从字典中获取对应于给定键key
的值。__delitem__
函数:从_FrozenDict
实例中根据给定的键key
移除相应的键值对。__iter__
函数:返回一个迭代器,允许外部代码通过迭代协议遍历字典中的每个键。__len__
函数:返回一个整数,表示当前字典中键的数量。copy
函数:创建一个当前字典的浅拷贝。keys
函数:返回一个KeysView
对象,该对象提供了一个对字典中所有键的视图。__repr__
函数:返回一个字符串,该字符串提供了有关当前字典的信息,包括它的类名、内容(键和值)以及内存地址。
_FrozenDict
类:冻结的字典对象。__init__
函数:初始化一个新的_FrozenDict
实例,并使用提供的数据填充内部的字典。__iter__
函数:返回一个迭代器,允许外部代码通过迭代协议遍历_FrozenDict
中的每个键。__len__
函数:返回一个整数,表示当前_FrozenDict
实例中键的数量。__getitem__
函数:从_FrozenDict
实例中根据给定的键key
获取相应的值。copy
函数:创建一个_FrozenDict
实例的深拷贝。
1.16.1 类:CaseInsensitiveSet
- 定义了一个名为
CaseInsensitiveSet
的类,继承自MutableSet[str]
。
class CaseInsensitiveSet(MutableSet[str]):
"""一个不区分大小写的 :class:`set` 类似对象。
实现了 :class:`~typing.MutableSet` 的所有方法和操作。所有值预期都是字符串。
该结构记住最后一次设置的值的大小写,然而,包含测试是不区分大小写的。
"""
1.16.1.1 __init__()
- 定义了一个名为
__init__
的构造方法,它接受一个可选的Collection[str]
参数values
,并返回None
。
def __init__(self, values: Optional[Collection[str]] = None) -> None:
"""使用给定的 *values* 创建一个新的 :class:`CaseInsensitiveSet` 实例。
:param values: 要添加到集合中的值。
"""
self._values: Dict[str, str] = {}
for v in values or ():
self.add(v)
作用:
__init__
方法的主要作用如下:
- 初始化内部存储结构:
- 初始化一个字典
_values
,用于存储集合中的元素。字典的键是经过转换的小写字符串,值是原始的字符串。
- 初始化一个字典
- 添加初始值:
- 如果在创建实例时提供了初始值
values
,则遍历这些值并将它们添加到集合中。
- 如果在创建实例时提供了初始值
1.16.1.2 __repr__()
- 定义了一个名为
__repr__
的实例方法,该方法不接受任何参数(除了隐含的self
参数),并返回一个字符串。
def __repr__(self) -> str:
"""获取集合的字符串表示。
提供一种有助于重新创建集合的方式。
:returns: 表示集合及其值的字符串。
:Example:
>>> repr(CaseInsensitiveSet(('1', 'test', 'Test', 'TESt', 'test2'))) # doctest: +ELLIPSIS
"<CaseInsensitiveSet('1', 'TESt', 'test2') at ...>"
"""
return '<{0}{1} at {2:x}>'.format(type(self).__name__, tuple(self._values.values()), id(self))
作用:
__repr__
方法的具体作用是返回一个字符串,该字符串描述了当前对象的状态,并且可以用来重新创建该对象。具体来说:
- 类名获取:
- 使用
type(self).__name__
获取当前对象所属类的名称。
- 使用
- 值获取:
- 使用
self._values.values()
获取存储在_values
字典中的值,并将其转换为元组。
- 使用
- 内存地址获取:
- 使用
id(self)
获取当前对象的内存地址,并以十六进制形式表示。
- 使用
- 字符串格式化:
- 使用字符串格式化方法构造出一个字符串,该字符串包含了类名、值列表以及对象的内存地址。
1.16.1.3 __str__()
- 定义了一个名为
__str__
的实例方法,该方法不接受任何参数(除了隐含的self
参数),并返回一个字符串。
def __str__(self) -> str:
"""获取用于打印的集合值。
:returns: 以字符串格式表示的集合值。
:Example:
>>> str(CaseInsensitiveSet(('1', 'test', 'Test', 'TESt', 'test2'))) # doctest: +SKIP
"{'TESt', 'test2', '1'}"
"""
return str(set(self._values.values()))
作用:
__str__
方法的具体作用是返回一个字符串,该字符串描述了当前对象的状态,通常用于输出或显示目的。具体来说:
- 获取值:
- 使用
self._values.values()
获取存储在_values
字典中的值。
- 使用
- 去重:
- 使用
set(...)
将这些值转换为一个新的集合,这样可以去除重复的元素。
- 使用
- 字符串化:
- 使用
str(...)
将集合转换为字符串。
- 使用
- 返回字符串:
- 返回转换后的字符串。
1.16.1.4 __contains__()
- 定义了一个名为
__contains__
的实例方法,它接受一个参数value
并返回一个布尔值。
def __contains__(self, value: object) -> bool:
"""检查集合是否包含 *value*。
检查是以不区分大小写的方式进行的。
:param value: 要检查的值。
:returns: 如果 *value* 已经在集合中,则返回 ``True``;否则返回 ``False``。
"""
return isinstance(value, str) and value.lower() in self._values
作用:
__contains__
方法的具体作用是检查给定的值 value
是否存在于当前集合中,而且这个检查是不区分大小写的。具体来说:
- 参数解析:
- 接收一个参数
value
,表示要检查的值。
- 接收一个参数
- 类型检查:
- 使用
isinstance(value, str)
检查value
是否为字符串类型。如果不是字符串,则返回False
。
- 使用
- 值转换:
- 如果
value
是字符串,则使用value.lower()
将其转换为小写形式。
- 如果
- 值查找:
- 使用
value.lower() in self._values
查找转换后的小写字符串是否存在于_values
字典的键中。
- 使用
- 返回结果:
- 如果找到匹配的键,则返回
True
;否则返回False
。
- 如果找到匹配的键,则返回
1.16.1.5 __iter__()
- 定义了一个名为
__iter__
的实例方法,该方法没有接受任何参数(除了隐含的self
参数),并返回一个迭代器,该迭代器会返回字符串类型的元素。
def __iter__(self) -> Iterator[str]:
"""遍历此集合中的值。
:yields: 集合中的值。
"""
return iter(self._values.values())
作用:
__iter__
方法的具体作用是返回一个迭代器,允许外部代码通过迭代协议遍历集合中的每个元素。具体来说:
- 获取值:
- 使用
self._values.values()
获取存储在_values
字典中的值。
- 使用
- 创建迭代器:
- 使用
iter(...)
方法创建一个迭代器,该迭代器可以按顺序访问这些值。
- 使用
- 返回迭代器:
- 返回创建的迭代器对象。
1.16.1.6 __len__()
- 定义了一个名为
__len__
的实例方法,该方法不接受任何参数(除了隐含的self
参数),并返回一个整数。
def __len__(self) -> int:
"""获取此集合的长度。
:returns: 集合中的值的数量。
:Example:
>>> len(CaseInsensitiveSet(('1', 'test', 'Test', 'TESt', 'test2')))
3
"""
return len(self._values)
作用:
__len__
方法的具体作用是返回一个整数,表示当前集合中元素的数量。具体来说:
- 获取字典长度:
- 使用
len(self._values)
计算_values
字典中的键的数量。由于_values
存储的是不区分大小写的元素,因此它应该已经去除了重复的项。
- 使用
- 返回长度:
- 返回计算得到的长度值。
1.16.1.7 add()
- 定义了一个名为
add
的实例方法,它接受一个字符串参数value
,并返回None
。
def add(self, value: str) -> None:
"""向此集合添加 *value*。
搜索是以不区分大小写的方式进行的。如果 *value* 已经在集合中,则用 *value* 覆盖现有的值,以便记住 *value* 的最后一个出现的大小写形式。
:param value: 要添加到集合中的值。
"""
self._values[value.lower()] = value
作用:
add
方法的具体作用是向集合中添加一个新值 value
。具体来说:
- 参数解析:
- 接收一个字符串参数
value
,表示要添加到集合中的值。
- 接收一个字符串参数
- 值转换:
- 使用
value.lower()
将value
转换为小写形式,以确保搜索时不区分大小写。
- 使用
- 值存储:
- 使用转换后的小写形式作为键,在
_values
字典中存储value
的原始形式。这保证了即使同一个值的不同大小写形式被多次添加,集合中只会保留最后一次添加的值的原始大小写形式。
- 使用转换后的小写形式作为键,在
1.16.1.8 discard()
- 定义了一个名为
discard
的实例方法,它接受一个字符串参数value
,并返回None
。
def discard(self, value: str) -> None:
"""从集合中移除 *value*。
搜索是以不区分大小写的方式进行的。如果 *value* 不在集合中,则不抛出异常。
:param value: 要从集合中移除的值。
"""
self._values.pop(value.lower(), None)
作用:
discard
方法的具体作用是从集合中移除一个指定的值 value
。具体来说:
- 参数解析:
- 接收一个字符串参数
value
,表示要从集合中移除的值。
- 接收一个字符串参数
- 值转换:
- 使用
value.lower()
将value
转换为小写形式,以确保搜索时不区分大小写。
- 使用
- 值移除:
- 使用转换后的小写形式作为键,在
_values
字典中尝试移除对应的条目。如果该键存在,则移除条目;如果键不存在,则返回None
并且不抛出异常。
- 使用转换后的小写形式作为键,在
1.16.1.9 issubset()
- 定义了一个名为
issubset
的实例方法,它接受一个参数other
,该参数类型为CaseInsensitiveSet
,并返回一个布尔值。
def issubset(self, other: 'CaseInsensitiveSet') -> bool:
"""检查此集合是否是 *other* 的子集。
:param other: 用于与此集合比较的另一个集合。
:returns: 如果此集合是 *other* 的子集,则返回 ``True``;否则返回 ``False``。
"""
return self <= other
作用:
issubset
方法的具体作用是检查当前集合 (self
) 是否是另一个集合 (other
) 的子集。具体来说:
- 参数解析:
- 接收一个参数
other
,该参数类型为CaseInsensitiveSet
,表示要与当前集合进行比较的另一个集合。
- 接收一个参数
- 子集判断:
- 使用
<=
运算符来判断当前集合 (self
) 是否是另一个集合 (other
) 的子集。这里的运算符重载实现了不区分大小写的子集判断逻辑。
- 使用
- 返回结果:
- 如果当前集合是另一个集合的子集,则返回
True
;否则返回False
。
- 如果当前集合是另一个集合的子集,则返回
1.16.2 类:CaseInsensitiveDict
- 定义了一个名为
CaseInsensitiveDict
的类,继承自MutableMapping[str, Any]
,表示这是一个键为字符串的可变映射,允许存储任意类型的值。 - 这个类的作用是实现一个不区分大小写的字典类。具体来说:
- 键的存储:所有的键都会被存储为字符串,并且在内部会记住每个键最后一次被设置时的原始大小写形式。
- 键的迭代:在进行迭代时(如使用
iter
、keys
、items
、iterkeys
、iteritems
方法),返回的键会保留它们的原始大小写形式。 - 键的查找:在查找键或测试键是否存在时,键的比较是不区分大小写的,这意味着无论你使用何种大小写形式来查找键,都能找到对应的项。
class CaseInsensitiveDict(MutableMapping[str, Any]):
"""一个不区分大小写的类似字典(dict)的对象。
实现了 typing.MutableMapping 的所有方法和操作,以及 dict 的 copy 方法。所有键都应该是字符串。该结构记住最后一个被设置 的键的大小写,并且 iter、dict.keys、dict.items、dict.iterkeys 和 dict.iteritems 将包含区分大小写的键。然而,查询和 包含测试是不区分大小写的。
"""
1.16.2.1 __init__()
- 定义了一个名为
__init__
的构造函数,该函数接受一个类型为Optional[Dict[str, Any]]
的参数data
,默认值为None
,并返回None
。
def __init__(self, data: Optional[Dict[str, Any]] = None) -> None:
"""使用给定的 *data* 创建一个新的 :class:`CaseInsensitiveDict` 实例。
:param data: 用于创建 :class:`CaseInsensitiveDict` 的初始字典。
"""
self._values: OrderedDict[str, Any] = OrderedDict()
self.update(data or {})
作用:
这个构造函数的作用是初始化一个新的 CaseInsensitiveDict
实例,并可以从提供的初始数据创建。具体来说:
- 初始化
_values
:- 创建一个
OrderedDict
实例,并将其赋值给_values
属性,这确保了字典中元素的插入顺序得以保留。
- 创建一个
- 更新字典:
- 使用构造函数传入的
data
参数来更新CaseInsensitiveDict
。如果data
为None
,则使用一个空字典{}
作为默认值。
- 使用构造函数传入的
1.16.2.2 __setitem__()
- 定义了一个名为
__setitem__
的实例方法,它接受两个参数:key
和value
,分别对应字典的键和值,并且该方法没有返回值。
def __setitem__(self, key: str, value: Any) -> None:
"""在字典中为 *key* 分配 *value*。
*key* 在字典中是以不区分大小写的方式进行搜索/存储的。字典中相应的值是一个包含以下内容的元组:
* 原始的 *key*;
* *value*。
:param key: 要在字典中创建或更新的键。
:param value: 对应于 *key* 的值。
"""
self._values[key.lower()] = (key, value)
作用:
__setitem__
方法的具体作用是在字典中设置或更新一个键值对。具体来说:
- 参数解析:
- 接收两个参数:
key
和value
,分别对应字典的键和值。
- 接收两个参数:
- 键转换:
- 使用
key.lower()
将key
转换为小写形式,以确保搜索时不区分大小写。
- 使用
- 值存储:
- 使用转换后的小写形式作为键,在
_values
字典中存储包含原始key
和value
的元组。这意味着即使一个键的不同大小写形式被多次设置,字典中只会保留最后一次设置的值的原始大小写形式和对应的值。
- 使用转换后的小写形式作为键,在
1.16.2.3 __getitem__()
- 定义了一个名为
__getitem__
的实例方法,它接受一个字符串参数key
,并返回任意类型的值。
def __getitem__(self, key: str) -> Any:
"""获取对应于 *key* 的值。
*key* 在字典中是以不区分大小写的方式进行搜索的。
.. note::
如果 *key* 不在字典中,将触发 :class:`KeyError`。
:param key: 要在字典中搜索的键。
:returns: 对应于 *key* 的值。
"""
return self._values[key.lower()][1]
作用:
__getitem__
方法的具体作用是从字典中获取对应于给定键 key
的值。具体来说:
- 参数解析:
- 接收一个字符串参数
key
,表示要获取其对应值的键。
- 接收一个字符串参数
- 键转换:
- 使用
key.lower()
将key
转换为小写形式,以确保搜索时不区分大小写。
- 使用
- 值检索:
- 使用转换后的小写形式作为键,在
_values
字典中获取对应的值。这里的_values
字典中存储的是包含原始键和实际值的元组。 - 从上述元组中提取实际的值(即元组的第二个元素)。
- 使用转换后的小写形式作为键,在
- 返回结果:
- 返回提取的实际值。
1.16.2.4 __delitem__()
- 定义了一个名为
__delitem__
的实例方法,该方法接受一个字符串类型的键key
作为参数,并没有返回值。
def __delitem__(self, key: str) -> None:
"""从字典中移除 *key*。
*key* 在字典中是以不区分大小写的方式进行搜索的。
.. note:
如果 *key* 不在字典中,则会触发 :class:`KeyError`。
:param key: 要从字典中移除的键。
"""
del self._values[key.lower()]
作用:
__delitem__
方法的具体作用是从 _FrozenDict
实例中根据给定的键 key
移除相应的键值对。具体来说:
- 转换键为小写:
- 使用
key.lower()
将输入的键转换为小写形式,以便在内部字典中进行不区分大小写的查找。
- 使用
- 删除键值对:
- 使用
del self._values[key.lower()]
从_FrozenDict
实例的内部字典self._values
中删除指定键的条目。如果键不存在,则会抛出KeyError
。
- 使用
1.16.2.5 __iter__()
- 定义了一个名为
__iter__
的实例方法,该方法没有接受任何参数(除了隐含的self
参数),并返回一个迭代器,该迭代器会返回字符串类型的元素(键)。
def __iter__(self) -> Iterator[str]:
"""遍历此字典中的键。
:yields: 字典中存在的每个键。每个键都以其最后被存储的大小写形式返回。
"""
return iter(key for key, _ in self._values.values()
作用:
__iter__
方法的具体作用是返回一个迭代器,允许外部代码通过迭代协议遍历字典中的每个键。具体来说:
- 获取值:
- 使用
self._values.values()
获取_values
字典中的所有值。这里假设_values
是存储字典元素的字典,其中的值是一个包含原始键和对应值的元组。
- 使用
- 提取键:
- 使用生成器表达式
key for key, _ in self._values.values()
从每个元组中提取原始键。
- 使用生成器表达式
- 创建迭代器:
- 使用
iter(...)
方法创建一个迭代器,该迭代器可以按顺序访问这些键。
- 使用
- 返回迭代器:
- 返回创建的迭代器对象。
1.16.2.6 __len__()
- 定义了一个名为
__len__
的实例方法,该方法没有接受任何参数(除了隐含的self
参数),并返回一个整数。
def __len__(self) -> int:
"""获取此字典的长度。
:returns: 字典中的键的数量。
:Example:
>>> len(CaseInsensitiveDict({'a': 'b', 'A': 'B', 'c': 'd'}))
2
"""
return len(self._values)
作用:
__len__
方法的具体作用是返回一个整数,表示当前字典中键的数量。具体来说:
- 获取字典长度:
- 使用
len(self._values)
计算_values
字典中的键的数量。由于_values
存储的是不区分大小写的键,因此它应该已经去除了重复的项。
- 使用
- 返回长度:
- 返回计算得到的长度值。
1.16.2.7 copy()
- 定义了一个名为
copy
的实例方法,该方法没有接受任何参数(除了隐含的self
参数),并返回一个CaseInsensitiveDict
类型的对象。
def copy(self) -> 'CaseInsensitiveDict':
"""创建此字典的一个副本。
:return: 一个新的字典对象,具有与此字典相同的键和值。
"""
return CaseInsensitiveDict({v[0]: v[1] for v in self._values.values()})
作用:
copy
方法的具体作用是创建一个当前字典的浅拷贝。具体来说:
- 创建字典:
- 使用字典推导式
{v[0]: v[1] for v in self._values.values()}
从_values
字典中提取每个原始键及其对应的值。
- 使用字典推导式
- 初始化新实例:
- 使用创建的新字典来初始化一个新的
CaseInsensitiveDict
实例。
- 使用创建的新字典来初始化一个新的
- 返回新实例:
- 返回初始化的新
CaseInsensitiveDict
实例。
- 返回初始化的新
1.16.2.8 keys()
- 定义了一个名为
keys
的实例方法,该方法没有接受任何参数(除了隐含的self
参数),并返回一个KeysView
对象,其中包含字符串类型的键。
def keys(self) -> KeysView[str]:
"""返回字典键的一个新的视图。
:returns: 提供对字典键视图的一个类似集合的对象。
"""
return self._values.keys()
作用:
keys
方法的具体作用是返回一个 KeysView
对象,该对象提供了一个对字典中所有键的视图。具体来说:
- 获取键视图:
- 使用
_values.keys()
方法获取_values
字典中的所有键的视图。这里_values
字典存储的是不区分大小写的键(小写形式)和对应的值(原始键和实际值的元组)。
- 使用
- 返回视图:
- 返回获取的
KeysView
对象。
- 返回获取的
1.16.2.9 __repr__()
- 定义了一个名为
__repr__
的实例方法,该方法没有接受任何参数(除了隐含的self
参数),并返回一个字符串。
def __repr__(self) -> str:
"""获取字典的字符串表示。
提供一种有助于重新创建字典的方法。
:returns: 显示字典的键和值的表示。
:Example:
>>> repr(CaseInsensitiveDict({'a': 'b', 'A': 'B', 'c': 'd'})) # doctest: +ELLIPSIS
"<CaseInsensitiveDict{'A': 'B', 'c': 'd'} at ...>"
"""
return '<{0}{1} at {2:x}>'.format(type(self).__name__, dict(self.items()), id(self))
作用:
__repr__
方法的具体作用是返回一个字符串,该字符串提供了有关当前字典的信息,包括它的类名、内容(键和值)以及内存地址。具体来说:
- 获取类名:
- 使用
type(self).__name__
获取当前实例所属类的名字。
- 使用
- 获取字典表示:
- 使用
dict(self.items())
将字典项转换为标准字典格式,展示字典的键和值。
- 使用
- 获取内存地址:
- 使用
id(self)
获取当前实例的内存地址。
- 使用
- 格式化输出:
- 使用格式化字符串
'<{0}{1} at {2:x}>'.format(...)
构建最终的字符串表示。
- 使用格式化字符串
- 返回字符串:
- 返回构建好的字符串。
1.16.3 类:_FrozenDict
- 定义了一个名为
_FrozenDict
的类,该类继承自Mapping[str, Any]
。Mapping
是一个抽象基类(ABC),用于定义映射类型的行为。这里的_FrozenDict
限制键为字符串类型,值可以是任意类型。 _FrozenDict
类的具体作用是创建一个不可变(即冻结的)字典对象。具体来说:- 继承 Mapping:
_FrozenDict
继承自Mapping[str, Any]
,这意味着它实现了映射接口,可以像普通的字典一样使用,但是它是一个不可变的映射,一旦创建就不能修改。
- 不可变性:
- 作为一个“冻结”的字典,它不允许在创建之后添加、删除或修改条目。这使得
_FrozenDict
对象非常适合用于那些需要不可变数据结构的地方,比如配置文件、常量映射等。
- 作为一个“冻结”的字典,它不允许在创建之后添加、删除或修改条目。这使得
- 继承 Mapping:
class _FrozenDict(Mapping[str, Any]):
"""冻结的字典对象。"""
1.16.3.1 __init__()
- 定义了一个名为
__init__
的实例方法,它是_FrozenDict
类的构造函数。该方法接受任意数量的位置参数*args
和关键字参数**kwargs
,并且没有返回值。
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""使用给定的数据创建一个新的 :class:`_FrozenDict` 实例。"""
self.__values: Dict[str, Any] = dict(*args, **kwargs)
作用:
__init__
方法的具体作用是初始化一个新的 _FrozenDict
实例,并使用提供的数据填充内部的字典。具体来说:
- 接收参数:
- 接受任意数量的位置参数
*args
和关键字参数**kwargs
。这使得__init__
方法可以灵活地处理不同的构造方式,如直接传入一个字典、多个键值对元组,或者其他可迭代对象。
- 接受任意数量的位置参数
- 合并参数:
- 使用
dict(*args, **kwargs)
将传入的所有位置参数和关键字参数合并成一个字典。
- 使用
- 初始化内部状态:
- 将合并后的字典赋值给
self.__values
属性,这个属性保存了_FrozenDict
实例的内部数据。
- 将合并后的字典赋值给
1.16.3.2 __iter__()
- 定义了一个名为
__iter__
的实例方法,该方法没有接受任何参数(除了隐含的self
参数),并返回一个迭代器,该迭代器会返回字符串类型的元素(键)。
def __iter__(self) -> Iterator[str]:
"""遍历此字典中的键。
:yields: 字典中存在的每个键。每个键都以其最后被存储的大小写形式返回。
"""
return iter(self.__values)
作用:
__iter__
方法的具体作用是返回一个迭代器,允许外部代码通过迭代协议遍历 _FrozenDict
中的每个键。具体来说:
- 获取迭代器:
- 使用
iter(self.__values)
获取一个迭代器,该迭代器可以遍历_FrozenDict
内部存储的键。这里self.__values
是一个字典,存储了_FrozenDict
的所有键值对。
- 使用
- 返回迭代器:
- 返回创建的迭代器对象。
1.16.3.3 __len__()
- 定义了一个名为
__len__
的实例方法,该方法没有接受任何参数(除了隐含的self
参数),并返回一个整数。
def __len__(self) -> int:
"""获取此字典的长度。
:returns: 字典中的键的数量。
:Example:
>>> len(_FrozenDict())
0
"""
return len(self.__values)
作用:
__len__
方法的具体作用是返回一个整数,表示当前 _FrozenDict
实例中键的数量。具体来说:
- 计算长度:
- 使用
len(self.__values)
计算_FrozenDict
实例内部存储的键的数量。这里self.__values
是一个字典,存储了_FrozenDict
的所有键值对。
- 使用
- 返回长度:
- 返回计算得到的长度值。
1.16.3.4 __getitem__()
- 定义了一个名为
__getitem__
的实例方法,该方法接受一个字符串类型的键key
作为参数,并返回任意类型的值Any
。
def __getitem__(self, key: str) -> Any:
"""获取对应于 *key* 的值。
:returns: 对应于 *key* 的值。
"""
return self.__values[key]
作用:
__getitem__
方法的具体作用是从 _FrozenDict
实例中根据给定的键 key
获取相应的值。具体来说:
- 获取值:
- 使用
self.__values[key]
从_FrozenDict
实例的内部字典self.__values
中获取对应的值。这里self.__values
是一个字典,存储了_FrozenDict
的所有键值对。
- 使用
- 返回值:
- 返回从内部字典中找到的值。
1.16.3.5 copy()
- 定义了一个名为
copy
的实例方法,该方法没有接受任何参数(除了隐含的self
参数),并返回一个字典类型Dict[str, Any]
的对象。
def copy(self) -> Dict[str, Any]:
"""创建此字典的一个副本。
:return: 一个新的字典对象,具有与此字典相同的键和值。
"""
return deepcopy(self.__values)
作用:
copy
方法的具体作用是创建一个 _FrozenDict
实例的深拷贝。具体来说:
- 创建深拷贝:
- 使用
deepcopy
函数来创建self.__values
字典的一个深拷贝。deepcopy
会递归地复制字典中的所有对象,确保新字典与原字典没有任何共享的对象引用。
- 使用
- 返回新字典:
- 返回创建的深拷贝字典。
1.17 file_perm.py
__FilePermissions
类:
1.17.1 类:__FilePermissions
- 定义了一个名为
__FilePermissions
的类。双下划线开头表明这是一个私有类,不应该在类的外部直接使用。
class __FilePermissions:
"""用于管理 PGDATA 下目录和文件权限的帮助类。
执行 :meth:`set_permissions_from_data_directory` 方法来确定基于 PGDATA 根目录权限,
应该为 PGDATA 下的文件和目录使 用哪些权限。
"""
# 数据目录权限的模式掩码,只允许所有者
# 读/写目录和文件——mask 077。
__PG_MODE_MASK_OWNER = stat.S_IRWXG | stat.S_IRWXO
# 允许组读/执行的数据目录权限的模式掩码——mask 027。
__PG_MODE_MASK_GROUP = stat.S_IWGRP | stat.S_IRWXO
# 创建目录的默认模式——mode 700。
__PG_DIR_MODE_OWNER = stat.S_IRWXU
# 允许组读/执行的目录创建模式——Mode 750。
__PG_DIR_MODE_GROUP = stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP
# 创建文件的默认模式——mode 600。
__PG_FILE_MODE_OWNER = stat.S_IRUSR | stat.S_IWUSR
# 允许组读的文件创建模式——Mode 640。
__PG_FILE_MODE_GROUP = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP
1.17.1.1 __init__()
- 定义了
__FilePermissions
类的构造函数__init__
,它没有接受任何参数(除了隐式的self
),并且没有返回值。
def __init__(self) -> None:
"""创建一个 :class:`__FilePermissions` 对象并设置默认的权限。"""
# 调用私有方法 __set_owner_permissions 来设置所有者权限
self.__set_owner_permissions()
# 调用私有方法 __set_umask 来设置文件创建时的默认掩码(umask)
self.__orig_umask = self.__set_umask()
作用:
__init__
方法的作用是在创建 __FilePermissions
类的实例时,初始化一些默认的权限设置。具体来说:
- 初始化所有者权限:
- 调用
__set_owner_permissions
方法来设置默认的所有者权限。虽然我们看不到__set_owner_permissions
方法的具体实现,但是我们可以推测它可能涉及设置某些默认权限,以确保数据目录下的文件和目录对于所有者来说具有适当的读写权限。
- 调用
- 设置文件创建掩码(umask):
- 调用
__set_umask
方法来设置默认的 umask 值,这个值决定了新创建的文件和目录的默认权限。同时,原始的 umask 值被存储在__orig_umask
属性中,这样可以在需要的时候恢复到原来的 umask 设置。
- 调用
通过这些步骤,构造函数确保了在创建 __FilePermissions
类的实例时,已经根据预设的规则设置了适当的权限掩码和默认权限,从而确保 PostgreSQL 数据目录下的文件和目录具有正确的访问控制。
1.17.1.2 file_create_mode()
- 定义了一个名为
file_create_mode
的属性(property),该属性返回一个整数,表示文件创建时的权限模式。
@property
def file_create_mode(self) -> int:
"""文件权限"""
return self.__pg_file_create_mode
作用:
file_create_mode
属性的作用是提供对内部状态 __pg_file_create_mode
的只读访问。具体来说:
- 封装内部状态:
- 使用
@property
装饰器来封装__pg_file_create_mode
的访问逻辑,使得外部代码可以像访问普通属性一样使用file_create_mode
。
- 使用
- 提供只读访问:
- 由于
file_create_mode
是一个只读属性,因此不允许直接修改它的值。这有助于保护内部状态不受外部修改的影响。
- 由于
- 返回文件创建权限模式:
- 返回 PostgreSQL 数据目录下新创建文件时应使用的权限模式,这个模式对于确保数据的安全性和访问控制非常重要。
通过这种方式,file_create_mode
属性提供了一种简单、安全的方式来访问新创建文件时应该使用的权限模式,同时保持了封装性,隐藏了具体的实现细节。这有助于在创建新的文件时,自动应用正确的权限设置,从而保证系统的安全性。
1.17.1.3 set_permissions_from_data_directory()
- 定义了一个名为 set_permissions_from_data_directory 的方法,它接受一个字符串类型的参数 data_dir ,并返回 None 。
def set_permissions_from_data_directory(self, data_dir: str) -> None:
"""根据提供的 *data_dir* 设置新的权限。
:param data_dir: 用于计算权限的数据目录 PGDATA 的引用。
"""
# 尝试获取 data_dir 的状态信息
try:
st = os.stat(data_dir)
# 检查 data_dir 的权限位
if (st.st_mode & self.__PG_DIR_MODE_GROUP) == self.__PG_DIR_MODE_GROUP:
self.__set_group_permissions()
else:
self.__set_owner_permissions()
except Exception as e:
logger.error('Can not check permissions on %s: %r', data_dir, e)
else:
# 设置 umask
self.__set_umask()
作用:
set_permissions_from_data_directory
方法的具体作用是根据提供的数据目录 data_dir
来设置新的权限。具体来说:
- 获取状态信息:
- 尝试使用
os.stat()
获取data_dir
的状态信息。
- 尝试使用
- 检查权限位:
- 检查
data_dir
的权限位是否符合特定模式self.__PG_DIR_MODE_GROUP
。 - 如果符合,则调用
self.__set_group_permissions()
方法来设置权限。 - 如果不符合,则调用
self.__set_owner_permissions()
方法来设置权限。
- 检查
- 异常处理:
- 如果在获取状态信息时发生异常,则记录一条错误日志,并输出异常信息。
- 设置 umask:
- 如果没有异常发生,则调用
self.__set_umask()
方法来设置umask
。
- 如果没有异常发生,则调用
1.17.1.4 __set_group_permissions()
- 定义了一个名为
__set_group_permissions
的私有方法(private method),它没有参数,并返回None
。
def __set_group_permissions(self) -> None:
"""使目录/文件对所有者可访问,并对组可读。"""
self.__pg_dir_create_mode = self.__PG_DIR_MODE_GROUP
self.__pg_file_create_mode = self.__PG_FILE_MODE_GROUP
self.__pg_mode_mask = self.__PG_MODE_MASK_GROUP
作用:
__set_group_permissions
方法的具体作用是设置目录和文件的权限,使其对所有者可访问,并对组可读。具体来说:
- 设置目录权限:
- 将
self.__pg_dir_create_mode
设置为self.__PG_DIR_MODE_GROUP
,这意味着新创建的目录将具有特定的权限,通常是对所有者可读写执行,对组可读执行,对其他人没有任何权限。
- 将
- 设置文件权限:
- 将
self.__pg_file_create_mode
设置为self.__PG_FILE_MODE_GROUP
,这意味着新创建的文件将具有特定的权限,通常是对所有者可读写,对组可读,对其他人没有任何权限。
- 将
- 设置权限掩码:
- 将
self.__pg_mode_mask
设置为self.__PG_MODE_MASK_GROUP
,这通常是一个掩码值,用于在创建文件或目录时确定哪些权限位应该被清除(即不允许设置)。
- 将
1.17.1.5 __set_owner_permissions()
- 定义了一个名为
__set_owner_permissions
的私有方法(private method),它没有参数,并返回None
。
def __set_owner_permissions(self) -> None:
"""使目录/文件仅对所有者可访问。"""
self.__pg_dir_create_mode = self.__PG_DIR_MODE_OWNER
self.__pg_file_create_mode = self.__PG_FILE_MODE_OWNER
self.__pg_mode_mask = self.__PG_MODE_MASK_OWNER
作用:
__set_owner_permissions
方法的具体作用是设置目录和文件的权限,使其仅对所有者可访问。具体来说:
- 设置目录权限:
- 将
self.__pg_dir_create_mode
设置为self.__PG_DIR_MODE_OWNER
,这意味着新创建的目录将具有特定的权限,通常是对所有者可读写执行,对组和其他人没有任何权限。
- 将
- 设置文件权限:
- 将
self.__pg_file_create_mode
设置为self.__PG_FILE_MODE_OWNER
,这意味着新创建的文件将具有特定的权限,通常是对所有者可读写,对组和其他人没有任何权限。
- 将
- 设置权限掩码:
- 将
self.__pg_mode_mask
设置为self.__PG_MODE_MASK_OWNER
,这通常是一个掩码值,用于在创建文件或目录时确定哪些权限位应该被清除(即不允许设置),以确保只有所有者可以访问。
- 将
这些设置确保了新创建的文件和目录具有适当的安全级别,只允许所有者对其进行访问,而不允许组内的其他成员或任何人访问。这对于 PostgreSQL 数据目录的安全性尤为重要,因为不正确的权限可能导致数据泄露或其他安全问题。
1.17.1.6 __set_umask()
- 定义了一个名为
__set_umask
的私有方法(private method),它没有参数,并返回一个整数。
def __set_umask(self) -> int:
"""基于计算设置 umask 值。
.. note::
应该只在调用了 __set_owner_permissions 或 __set_group_permissions 方法之一之后再调用此方法。
:returns: 之前的 umask 值,如果设置 umask 失败,则返回 ``0022``。
"""
try:
# 尝试设置当前进程的 umask 为 self.__pg_mode_mask 的值,并返回之前的 umask 值
return os.umask(self.__pg_mode_mask)
except Exception as e:
logger.error('Can not set umask to %03o: %r', self.__pg_mode_mask, e)
return 0o22
作用:
__set_umask
方法的具体作用是根据之前设置的权限掩码来设置当前进程的 umask
值。具体来说:
- 设置 umask:
- 使用
os.umask()
方法将当前进程的umask
设置为self.__pg_mode_mask
的值。 - 这个方法通常是在调用了
__set_owner_permissions
或__set_group_permissions
方法之后调用,因为这些方法已经计算出了适当的权限掩码值。
- 使用
- 异常处理:
- 如果在设置
umask
时发生异常,则记录一条错误日志,并返回默认的umask
值0o22
。
- 如果在设置
- 返回值:
- 正常情况下,返回设置
umask
之前的旧umask
值。 - 如果设置失败,则返回默认值
0o22
。
- 正常情况下,返回设置
这个方法的设计目的是为了确保在创建新的文件或目录时,默认的新文件权限能够根据先前计算出的权限掩码来正确设置。通过设置 umask
,可以控制新创建的文件或目录的默认权限,确保它们具有适当的安全级别。例如,如果 self.__pg_mode_mask
被设置为一个值,使得新创建的文件或目录只能由所有者访问,那么设置 umask
可以确保这一点。
1.17.1.7 file_create_mode()
- 定义了一个名为
file_create_mode
的属性(property),该属性没有显式的参数,并返回一个整数。
@property
def file_create_mode(self) -> int:
"""文件权限。"""
return self.__pg_file_create_mode
作用:
file_create_mode
属性的具体作用是提供对用于创建新文件时所使用的权限掩码的读取。具体来说:
- 获取权限掩码:
- 返回
self.__pg_file_create_mode
的值,这个值决定了新创建的文件将具有什么样的默认权限。
- 返回
这个属性的设计目的是为了方便地获取当前设置的文件创建权限掩码。通过这个属性,可以在需要的地方查询当前系统对于新创建的文件将应用什么样的默认权限设置,从而确保文件的安全性和一致性。
1.17.1.8 orig_umask()
- 装饰器
@property
将下面的方法转换成一个只读属性,允许我们像访问属性一样访问这个方法的结果,而不需要调用括号。 - 定义了一个名为
orig_umask
的方法,该方法属于某个类的一个实例,并且该方法的返回类型为int
。
@property
def orig_umask(self) -> int:
"""原始的 umask 值。"""
return self.__orig_umask
作用:
orig_umask
是一个只读属性,其具体作用是从对象内部获取并返回原始的 umask
值。具体来说:
- 属性封装:
- 通过
@property
装饰器,orig_umask
方法变成了一个只读属性,可以直接通过instance.orig_umask
访问,而无需调用instance.orig_umask()
。
- 通过
- 获取原始
umask
值:- 返回对象内部存储的原始
umask
值,这个值通常是对象在初始化时记录下来的系统的umask
值。
- 返回对象内部存储的原始
1.18 api.py
RestApiServer
类:
1.18.1 类:RestApiServer
- 定义了一个名为
RestApiServer
的类,继承自ThreadingMixIn
、HTTPServer
和Thread
。这意味着这是一个基于多线程的 HTTP 服务器,并且作为一个独立的线程运行。
class RestApiServer(ThreadingMixIn, HTTPServer, Thread):
"""Patroni REST API 服务器。
一个基于线程的异步 HTTP 服务器。
"""
# 设置了类属性 daemon_threads 为 True,表示所有的子线程都是守护线程。这样做的目的是防止服务器关闭时由于等待非守护线程结束而造成的阻塞,从而防止内存泄漏
# 在 Python 3.7 及以上版本中, ThreadingMixIn 会在服务器关闭时收集所有非守护线程以便于等待它们结束。
daemon_threads = True # 让工作线程“触发后就忘记”,以防止内存泄漏。
1.18.1.1 __init__()
- 定义了一个名为
__init__
的初始化方法,它接受两个参数:patroni
(类型为Patroni
)和config
(类型为字典Dict[str, Any]
),并返回None
。
def __init__(self, patroni: Patroni, config: Dict[str, Any]) -> None:
"""建立用于 REST API 守护进程的 Patroni 配置。
创建一个 :class:`RestApiServer` 实例。
:param patroni: Patroni 守护进程。
:param config: Patroni 配置中的 ``restapi`` 部分。
"""
self.connection_string: str
self.__auth_key = None
self.__allowlist_include_members: Optional[bool] = None
self.__allowlist: Tuple[Union[IPv4Network, IPv6Network], ...] = ()
self.http_extra_headers: Dict[str, str] = {}
self.patroni = patroni
self.__listen = None
self.request_queue_size = int(config.get('request_queue_size', 5))
self.__ssl_options: Dict[str, Any] = {}
self.__ssl_serial_number = None
self._received_new_cert = False
self.reload_config(config)
# 将实例变量 daemon 设置为 True,表示这个守护线程应该在主线程退出时也退出
self.daemon = True
作用:
__init__
初始化方法的具体作用是为 RestApiServer
类的实例设置初始状态,并加载配置。具体来说:
- 初始化变量:
- 初始化多个实例变量,包括私有的和公开的,用于存储与 REST API 服务器相关的配置和状态。
- 设置守护线程标志:
- 设置
daemon
标志为True
,确保在主线程退出时,这个守护线程也会一并退出。
- 设置
- 加载配置:
- 通过调用
reload_config
方法加载传入的config
配置项,并根据配置项更新实例的状态。
- 通过调用
- 配置请求队列大小:
- 从配置中获取请求队列大小,并确保即使配置项不存在也有一个默认值。
1.18.1.2 reload_config()
- 定义了一个名为
reload_config
的方法,它接受一个类型为Dict[str, Any]
的参数config
,并返回None
。
def reload_config(self, config: Dict[str, Any]) -> None:
"""重新加载 REST API 配置。
:param config: 表示 ``restapi`` 配置部分的值的字典。
:raises:
:class:`ValueError`: 如果 ``listen`` 键不在 *config* 中。
"""
# 检查 config 字典中是否包含 listen 键
if 'listen' not in config: # changing config in runtime
raise ValueError('Can not find "restapi.listen" config')
# 根据 config 中的 allowlist 键的值构建 __allowlist,并将其转换为元组
self.__allowlist = tuple(self._build_allowlist(config.get('allowlist')))
self.__allowlist_include_members = config.get('allowlist_include_members')
# 从 config 中提取 SSL 相关的配置项
ssl_options = {n: config[n] for n in ('certfile', 'keyfile', 'keyfile_password',
'cafile', 'ciphers') if n in config}
# 设置 http_extra_headers 为 config 中的 http_extra_headers 的值
self.http_extra_headers = config.get('http_extra_headers') or {}
self.http_extra_headers.update((config.get('https_extra_headers') or {}) if ssl_options.get('certfile') else {})
# 如果 config 中的 verify_client 是字符串类型
if isinstance(config.get('verify_client'), str):
ssl_options['verify_client'] = config['verify_client'].lower()
# 检查 listen 地址是否变化或者 SSL 选项是否变化,或者是否收到了新的证书
if self.__listen != config['listen'] or self.__ssl_options != ssl_options or self._received_new_cert:
self.__initialize(config['listen'], ssl_options)
# 如果 config 中有 auth 键,则将其编码为 UTF-8 字节串,再进行 Base64 编码后赋值给 __auth_key
self.__auth_key = base64.b64encode(config['auth'].encode('utf-8')) if 'auth' in config else None
# 这是一个类型检查的断言,确保在类型检查期间 __listen 是字符串类型。实际运行时不执行此代码。
# through :func:`__initialize`.
if TYPE_CHECKING: # pragma: no cover
assert isinstance(self.__listen, str)
# 构建 connection_string,使用 uri 函数
self.connection_string = uri(self.__protocol, config.get('connect_address') or self.__listen, 'patroni')
作用:
reload_config
方法的具体作用是对 REST API 服务器的配置进行重新加载,并根据新的配置更新服务器的状态。具体来说:
- 验证配置项:
- 检查
listen
地址是否存在于配置中,如果不存在则抛出异常。
- 检查
- 更新配置项:
- 更新
allowlist
和allowlist_include_members
。 - 提取 SSL 相关配置项,并根据需要更新
verify_client
。 - 更新
http_extra_headers
。 - 如果需要,调用
__initialize
方法初始化服务器。 - 更新认证密钥
__auth_key
。 - 构建连接字符串
connection_string
。
- 更新
- 初始化服务器:
- 当监听地址或 SSL 选项发生变化时,重新初始化服务器。
1.18.1.3 _build_allowlist()
- 定义了一个名为
_build_allowlist
的方法,它接受一个类型为Optional[List[str]]
的参数value
,并返回一个迭代器Iterator[Union[IPv4Network, IPv6Network]]
。
def _build_allowlist(self, value: Optional[List[str]]) -> Iterator[Union[IPv4Network, IPv6Network]]:
"""解析 *value* 中的每一项为 IP 网络对象。
:param value: 包含在 ``restapi.allowlist`` 设置中的 IP 地址和/或网络列表。每一项可以是一个主机名、IP 地址或 CIDR 格式的网络。
:yields: 解析后的 *host* + *port* 形成的 IP 网络。
"""
# 检查 value 是否为列表类型
if isinstance(value, list):
for v in value:
# 检查 v 是否包含斜杠 /,如果是,则认为 v 是带有子网掩码的网络地址
if '/' in v: # netmask
# 尝试将 v 解析为 ip_network 对象,并通过迭代器返回
try:
yield ip_network(v, False)
except Exception as e:
logger.error('Invalid value "%s" in the allowlist: %r', v, e)
# 果 v 不包含斜杠 /,则认为 v 是 IP 地址或主机名,尝试解析它
else:
for ip in self.__resolve_ips(v, 8080):
yield ip
作用:
_build_allowlist
方法的具体作用是将 restapi.allowlist
设置中的值解析为 IP 网络对象,从而生成一个包含合法 IP 网络的迭代器。具体来说:
- 解析网络地址:
- 如果
value
中的项包含斜杠/
,则直接尝试将其解析为ip_network
对象。 - 如果
value
中的项不包含斜杠/
,则认为是 IP 地址或主机名,调用__resolve_ips
方法来解析它。
- 如果
- 处理错误:
- 如果在解析过程中遇到错误,记录错误日志,并继续处理下一个项。
- 生成 IP 网络:
- 通过迭代器返回解析后的 IP 网络对象。
1.18.1.4 __resolve_ips()
- 定义了一个名为
__resolve_ips
的静态方法,它接受两个参数:host
(类型为str
)和port
(类型为int
),并返回一个迭代器Iterator[Union[IPv4Network, IPv6Network]]
。
@staticmethod
def __resolve_ips(host: str, port: int) -> Iterator[Union[IPv4Network, IPv6Network]]:
"""解析 *host* + *port* 为一个或多个 IP 网络。
:param host: 要检查的主机名。
:param port: 要检查的端口。
:yields: 解析后的 *host* + *port* 形成的 IP 网络。
"""
# 尝试使用 socket.getaddrinfo 方法来解析主机名 host 和端口 port,获取所有可能的地址信息
try:
for _, _, _, _, sa in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP):
yield ip_network(sa[0], False)
except Exception as e:
logger.error('Failed to resolve %s: %r', host, e)
作用:
__resolve_ips
方法的具体作用是将给定的主机名和端口号解析为一个或多个 IP 网络对象。具体来说:
- 解析主机名:
- 使用
socket.getaddrinfo
方法来获取主机名对应的 IP 地址信息。
- 使用
- 生成 IP 网络:
- 对于每一个解析出来的 IP 地址,使用
ip_network
函数将其转换为 IP 网络对象,并通过迭代器返回。
- 对于每一个解析出来的 IP 地址,使用
- 处理异常:
- 如果解析过程中遇到任何异常,记录错误日志,并继续处理其他地址信息。
1.19 ha.py
Ha
类:
1.19.1 类:Ha
- 声明了一个名为
Ha
的类,并且指定了父类为object
class Ha(object):
1.19.1.1 __init__()
- 定义了一个名为
__init__
的初始化方法,它接受一个类型为Patroni
的参数patroni
。
def __init__(self, patroni: Patroni):
self.patroni = patroni
self.state_handler = patroni.postgresql
# 创建一个 Rewind 类的实例
self._rewind = Rewind(self.state_handler)
self.dcs = patroni.dcs
# 创建一个空的 Cluster 实例
self.cluster = Cluster.empty()
# 创建一个空的 Cluster 实例
self.old_cluster = Cluster.empty()
self._leader_expiry = 0
# 创建一个 RLock 实例
self._leader_expiry_lock = RLock()
# 创建一个 Failsafe 类的实例
self._failsafe = Failsafe(patroni.dcs)
self._was_paused = False
self._promote_timestamp = 0
self._leader_timeline = None
self.recovering = False
# 创建一个 CriticalTask 类的实例
self._async_response = CriticalTask()
self._crash_recovery_started = 0
self._start_timeout = None
# 创建一个 AsyncExecutor 类的实例
self._async_executor = AsyncExecutor(self.state_handler.cancellable, self.wakeup)
self.watchdog = patroni.watchdog
# 每个成员使用 touch_member 向 DCS 发布各种信息。这个锁保护状态和发布过程,以确保一致的顺序并避免发布过时的值。
# 创建一个 RLock 实例
self._member_state_lock = RLock()
# 当前接收/刷新/重放 LSN 的最后已知值。
# 我们从 update_lock() 和 touch_member() 方法更新这个值,因为它们本来就会获取它。
# 这个值用于在 failsafe_mode 活动时通知领导者,而无需执行任何查询
# 初始化一个实例变量 self._last_wal_lsn 并设置其值为 None。这个变量存储了当前接收/刷新/重放 LSN 的最后一个已知值,用于在 failsafe_mode 激活时通知主节点,而不需要执行任何查询
self._last_wal_lsn = None
# 同步禁用请求的并发计数。值大于零表示我们不想成为同步备用。
# 变更受到 _member_state_lock 的保护。
# 初始化一个实例变量 self._disable_sync 并设置其值为 0。这个计数器记录了并发的同步禁用请求的数量,大于零的值表示我们不想成为同步备用。变化受 _member_state_lock 保护
self._disable_sync = 0
# 记住上一个写入 DCS 的成员角色和状态,以便通知 MPP 协调器
# 初始化一个实例变量 self._last_state 并设置其值为 None。这个变量记录了写入 DCS 的最后一个已知成员角色和状态,以便通知 MPP 协调器
self._last_state = None
# 我们需要以下属性,以避免当 Patroni 加入已经作为副本运行的 postgres 时,由于 DCS 中的集群未初始化而导致的关机。
# 初始化一个实例变量 self._join_aborted 并设置其值为 False。这个属性用于避免在加入 PostgreSQL 时由于集群未在 DCS 中初始化而导致加入副本失败的情况
self._join_aborted = False
# 仅在失败 pre_promote 脚本后进行重试时使用
# 初始化一个实例变量 self._released_leader_key_timestamp并设置其值为0。这个变量仅在预提升脚本失败后的回退中使用
self._released_leader_key_timestamp = 0
# 初始化全局配置
global_config.update(None, self.patroni.config.dynamic_configuration)
作用:
__init__
方法的具体作用是初始化一个 Ha
类的实例,并设置与集群管理相关的各种属性和状态。具体来说:
- 初始化变量:
- 初始化多个实例变量,包括与 PostgreSQL 状态、分布式一致性存储(DCS)、集群状态等相关的信息。
- 创建实例:
- 创建多个辅助类的实例,如
Rewind
、Failsafe
、CriticalTask
和AsyncExecutor
,用于实现高可用性和故障恢复等功能。
- 创建多个辅助类的实例,如
- 设置锁:
- 创建多个锁对象(如
RLock
),用于保护并发操作的一致性,避免数据竞争和不一致的问题。
- 创建多个锁对象(如
- 更新配置:
- 更新全局配置,使用传入的
patroni
对象中的动态配置信息。
- 更新全局配置,使用传入的
1.19.2 类:Failsafe
- 定义了一个名为
Failsafe
的类,并且显式地指定了父类为object
。
class Failsafe(object):
"""表示集群的故障安全状态。"""
1.19.2.1 __init__()
- 定义了一个名为
__init__
的初始化方法,它接受一个类型为AbstractDCS
的参数dcs
,并且该方法的返回类型为None
。
def __init__(self, dcs: AbstractDCS) -> None:
"""初始化 Failsafe 对象。
:param dcs: 当前的 DCS 对象,仅用于获取当前的 ``ttl`` 值。
"""
self._lock = RLock()
self._dcs = dcs
# 调用 self._reset_state() 方法来初始化或重置类的状态
self._reset_state()
作用:
__init__
方法的具体作用是初始化一个 Failsafe
类的实例,并设置与故障安全模式相关的内部状态。具体来说:
- 初始化锁对象:
- 创建一个
RLock
对象,用于同步访问共享资源,确保多线程环境下的安全性。
- 创建一个
- 保存 DCS 对象:
- 将传入的
dcs
参数赋值给实例变量self._dcs
,用于获取当前的ttl
值。ttl
通常是指时间生存期,用于控制在分布式一致性存储(DCS)中的数据过期时间。
- 将传入的
- 重置状态:
- 调用
self._reset_state()
方法来初始化或重置类的状态。这可能是为了准备进行故障安全模式的相关操作,或者是清除之前的状态以便开始新的操作。
- 调用
1.19.2.2 _reset_state()
- 定义了一个名为
_reset_state
的方法,它属于类的一个实例,并且该方法的返回类型为None
。方法名前面有一个下划线,表示这是一个内部使用的方法,不是外部应该直接调用的公共方法。
def _reset_state(self) -> None:
"""重置 Failsafe 对象的状态。"""
# 将实例变量 self._last_update 设置为 0,表示故障安全模式最后一次被触发的时间信息
self._last_update = 0 # 保存上次触发 failsafe 的时间信息。
self._name = '' # 集群领导者的名称
self._conn_url = None # 领导者的 PostgreSQL 连接 URL
self._api_url = None # 领导者的 Patroni REST API URL
self._slots = None # 领导者上复制槽的状态
作用:
_reset_state
方法的具体作用是对 Failsafe
类的实例状态进行重置。具体来说:
- 重置时间戳:
- 将
self._last_update
设置为0
,表示没有触发过故障安全模式。
- 将
- 清空领导者信息:
- 将
self._name
设置为空字符串''
,表示当前没有领导者名称。 - 将
self._conn_url
设置为None
,表示当前没有领导者的 PostgreSQL 连接 URL。 - 将
self._api_url
设置为None
,表示当前没有领导者的 Patroni REST API URL。
- 将
- 清空复制槽位状态:
- 将
self._slots
设置为None
,表示当前没有领导者的复制槽位状态。
- 将
1.20 async_executor.py
CriticalTask
类:AsyncExecutor
类:
1.20.1 类:CriticalTask
- 声明了一个名为
CriticalTask
的类,并且指定了父类为object
。
class CriticalTask(object):
"""表示一个在后台进程中需要取消或获取结果的关键任务。
访问该对象的字段时必须持有锁。为了执行关键任务,后台线程必须在持有该对象的锁的同时检查 ``is_cancelled`` 标志,运行任务,并使用 :func:`complete` 标记任务为完成。
主线程必须持有异步锁以防止任务完成,同时持有关键任务对象的锁,调用 :func:`cancel`。如果任务已经完成, :func:`cancel` 将返回 ``False``,并且 ``result`` 字段将包含任务的结果。当 :func:`cancel` 返回 ``True`` 时,可以保证后台任务会注意到 ``is_cancelled`` 标志。
:ivar is_cancelled: 如果关键任务已被取消。
:ivar result: 如果任务已完成,则包含任务的结果。
"""
1.20.1.1 __init__()
- 定义了一个名为
__init__
的初始化方法,它属于CriticalTask
类的一个实例,并且该方法的返回类型为None
。
def __init__(self) -> None:
"""创建一个 :class:`CriticalTask` 的新实例。
实例化锁和任务控制属性。
"""
self._lock = Lock()
# 表示当前任务未被取消
self.is_cancelled = False
# 表示当前任务的结果尚未确定或尚未计算出来
self.result = None
作用:
__init__
方法的具体作用是初始化一个 CriticalTask
类的实例,并设置与关键任务相关的内部状态。具体来说:
- 初始化锁对象:
- 创建一个
Lock
对象,用于同步访问共享资源,确保多线程环境下的安全性。
- 创建一个
- 设置任务取消标志:
- 将
self.is_cancelled
设置为False
,表示当前任务未被取消。这可以在任务执行过程中用来检查是否需要取消任务。
- 将
- 设置任务结果:
- 将
self.result
设置为None
,表示当前任务的结果尚未确定或尚未计算出来。这可以在任务完成后用来存储结果。
- 将
1.20.2 类:AsyncExecutor
- 声明了一个名为
AsyncExecutor
的类,并且指定了父类为object
。
class AsyncExecutor(object):
"""异步执行(长)任务的执行器。
:ivar critical_task: 一个 :class:`CriticalTask` 实例,用于处理关键后台任务的执行。
"""
1.20.2.1 __init__()
- 定义了一个名为
__init__
的初始化方法,它接受两个参数:cancellable
和ha_wakeup
,并且该方法的返回类型为None
。
def __init__(self, cancellable: CancellableSubprocess, ha_wakeup: Callable[..., None]) -> None:
"""创建一个 :class:`AsyncExecutor` 的新实例。
配置给定的 *cancellable* 和 *ha_wakeup*,初始化控制属性,并实例化锁和事件对象,这些对象用于访问属性和管理线程之间的通信。
:param cancellable: 支持被取消的子进程。
:param ha_wakeup: 用于唤醒 HA 循环的函数。
"""
self._cancellable = cancellable
self._ha_wakeup = ha_wakeup
self._thread_lock = RLock()
self._scheduled_action: Optional[str] = None
self._scheduled_action_lock = RLock()
self._is_cancelled = False
self._finish_event = Event()
self.critical_task = CriticalTask()
作用:
__init__
方法的具体作用是初始化一个 AsyncExecutor
类的实例,并设置与异步执行相关的内部状态。具体来说:
- 配置取消支持的子进程:
- 将
cancellable
参数赋值给self._cancellable
,表示支持被取消的子进程。
- 将
- 配置 HA 唤醒函数:
- 将
ha_wakeup
参数赋值给self._ha_wakeup
,表示用于唤醒 HA 循环的函数。
- 将
- 初始化锁对象:
- 创建一个
RLock
对象self._thread_lock
,用于同步访问共享资源。 - 创建一个
RLock
对象self._scheduled_action_lock
,用于同步访问self._scheduled_action
变量。
- 创建一个
- 设置任务控制标志:
- 将
self._scheduled_action
设置为None
,表示当前没有计划执行的动作。 - 将
self._is_cancelled
设置为False
,表示当前任务未被取消。
- 将
- 初始化事件对象:
- 创建一个
Event
对象self._finish_event
,用于管理线程间的通信。
- 创建一个
- 创建关键任务实例:
- 创建一个
CriticalTask
类的实例self.critical_task
,用于管理关键任务的执行。
- 创建一个
1.21 global_config.py
"""实现了全局配置的工具。
在导入时实例化 :class:GlobalConfig 对象,并在 :data:sys.modules 中替换 patroni.global_config 模块,这使得可以像使用模块变量和函数一样使用其属性和方法。
"""
GlobalConfig
类:一个包装全局配置的类,并提供便捷的方法来访问/检查值。__init__
函数:初始化一个GlobalConfig
类的实例,并设置与全局配置相关的内部状态。update
函数:从Cluster
对象中获取新的全局配置信息,并更新GlobalConfig
类实例中的配置。_cluster_has_valid_config
函数:检查提供的cluster
对象是否包含有效的全局配置信息。from_cluster
函数:从提供的Cluster
对象中提取全局配置信息,并根据这些信息创建或更新GlobalConfig
对象。get
函数:从全局配置中通过名称获取配置值。check_mode
函数:检查全局配置中某个特定的模式参数是否被启用。get_standby_cluster_config
函数:从全局配置中获取standby_cluster
的配置,并返回一个该配置的深拷贝。get_int
函数:从全局配置中获取指定名称的参数值,并将其转换为整数。
__getattr__
函数:在动态属性访问时提供一个备选方案,特别是在使用静态类型检查工具(如 pyright)时避免对未声明的模块成员进行警告。
1.21.1 类:GlobalConfig
- 声明了一个名为
GlobalConfig
的类,并且指定了父类为types.ModuleType
。
class GlobalConfig(types.ModuleType):
"""一个包装全局配置的类,并提供便捷的方法来访问/检查值。"""
__file__ = __file__ # 仅用于使 unittest 和 pytest 感到满意
1.21.1.1 __init__()
- 定义了一个名为
__init__
的初始化方法,它属于GlobalConfig
类的一个实例,并且该方法的返回类型为None
。
def __init__(self) -> None:
"""初始化 :class:`GlobalConfig` 对象。"""
super().__init__(__name__)
# 将实例变量 self.__config 初始化为一个空字典,用于存储全局配置信息
self.__config = {}
作用:
__init__
方法的具体作用是初始化一个 GlobalConfig
类的实例,并设置与全局配置相关的内部状态。具体来说:
- 调用父类初始化方法:
- 通过调用
super().__init__(__name__)
,初始化继承自父类的属性或行为。这里的__name__
是当前模块的名字,通常用于日志记录或其他初始化工作。
- 通过调用
- 初始化配置字典:
- 将实例变量
self.__config
初始化为一个空字典,用于存储全局配置信息。这意味着GlobalConfig
类的一个实例将拥有一个用于存放配置项的字典。
- 将实例变量
1.21.1.2 update()
- 定义了一个名为
update
的方法,它属于GlobalConfig
类的一个实例,并且该方法的返回类型为None
。该方法接收两个参数:cluster
和default
。
def update(self, cluster: Optional['Cluster'], default: Optional[Dict[str, Any]] = None) -> None:
"""使用来自 :class:`Cluster` 对象视图的新全局配置进行更新。
.. note::
更新是在原地发生的,并且仅由主心跳线程执行。
:param cluster: 当前已知的来自 DCS 的集群状态。
:param default: 默认配置,在没有有效的 *cluster.config* 时使用。
"""
# 尝试保护 DCS 被清除的情况
if self._cluster_has_valid_config(cluster):
self.__config = cluster.config.data # pyright: ignore [reportOptionalMemberAccess]
elif default:
self.__config = default
作用:
update
方法的具体作用是从 Cluster
对象中获取新的全局配置信息,并更新 GlobalConfig
类实例中的配置。具体来说:
- 验证配置的有效性:
- 通过
_cluster_has_valid_config
方法检查cluster
对象是否包含有效的配置信息。
- 通过
- 更新配置:
- 如果
cluster
包含有效的配置,则将cluster.config.data
的内容赋值给self.__config
。 - 如果
cluster
不包含有效的配置,并且提供了default
参数,则使用default
中的配置值来更新self.__config
。
- 如果
- 保护机制:
- 在
pyright: ignore [reportOptionalMemberAccess]
注释的帮助下,避免静态类型检查工具因为可能的None
访问而发出警告。
- 在
1.21.1.3 _cluster_has_valid_config()
- 声明
has_valid_config
方法为静态方法。静态方法不需要实例化类即可被调用,并且不强制传入self
参数。 - 定义了一个名为
_cluster_has_valid_config
的静态方法,它接受一个类型为可选Cluster
的参数cluster
,并返回一个布尔值。
@staticmethod
def _cluster_has_valid_config(cluster: Optional['Cluster']) -> bool:
"""检查提供的 cluster 对象是否有有效的全局配置。
:param cluster: 当前已知的来自 DCS 的集群状态。
:returns: 如果提供的 cluster 对象有有效的全局配置则返回 ``True``,否则返回 ``False``。
"""
return bool(cluster and cluster.config and cluster.config.modify_version)
作用:
_cluster_has_valid_config
函数的具体作用是检查提供的 cluster
对象是否包含有效的全局配置信息。具体来说:
- 检查
cluster
是否非None
:- 如果
cluster
为None
,则认为没有有效的配置。
- 如果
- 检查
cluster.config
是否非None
:- 如果
cluster.config
为None
,则认为cluster
对象没有配置信息。
- 如果
- 检查
modify_version
是否存在:- 如果
cluster.config.modify_version
存在,则认为cluster
对象包含了有效的配置信息。
- 如果
1.21.1.4 from_cluster()
- 定义了一个名为
from_cluster
的实例方法,它接受一个类型为可选Cluster
的参数cluster
,并返回一个GlobalConfig
类型的对象。
def from_cluster(self, cluster: Optional['Cluster']) -> 'GlobalConfig':
"""从提供的 :class:`Cluster` 对象视图返回一个 :class:`GlobalConfig` 实例。
.. note::
如果提供的 *cluster* 对象没有有效的全局配置,我们将返回 :class:`GlobalConfig` 对象的上一次已知有效状态。
当我们需要拥有全局配置中最新的值,但又不想更新全局对象时,会使用此方法。
:param cluster: 从 DCS(分布式一致性系统)获取的当前已知集群状态。
:returns: :class:`GlobalConfig` 对象。
"""
# 检查 cluster 对象是否具有有效的全局配置
if not self._cluster_has_valid_config(cluster):
return self
ret = GlobalConfig()
ret.update(cluster)
return ret
作用:
from_cluster
方法的具体作用是从提供的 Cluster
对象中提取全局配置信息,并根据这些信息创建或更新 GlobalConfig
对象。具体来说:
- 有效性检查:
- 首先检查
cluster
对象是否具有有效的全局配置信息。如果无效,则返回当前GlobalConfig
对象的实例。
- 首先检查
- 创建新的配置对象:
- 如果
cluster
对象具有有效的全局配置信息,则创建一个新的GlobalConfig
实例。
- 如果
- 更新配置信息:
- 使用
update
方法将cluster
对象中的配置信息更新到新的GlobalConfig
实例中。
- 使用
- 返回新的配置对象:
- 最终返回更新后的
GlobalConfig
实例。
- 最终返回更新后的
1.21.1.5 get()
- 定义了一个名为
get
的实例方法,它接受一个字符串参数name
,并返回任意类型的结果。
def get(self, name: str) -> Any:
"""通过 *name* 获取全局配置值。
:param name: 参数名称。
:returns: 配置值,如果缺失则返回 ``None``。
"""
return self.__config.get(name)
作用:
get
方法的具体作用是从全局配置中通过名称获取配置值。具体来说:
- 参数解析:
- 接收一个字符串参数
name
,表示要获取的配置项的名称。
- 接收一个字符串参数
- 配置值获取:
- 使用
self.__config.get(name)
方法从配置对象中获取对应名称的配置值。
- 使用
- 返回配置值:
- 如果配置项存在,则返回该配置项的值。
- 如果配置项不存在,则返回
None
。
1.21.1.6 check_mode()
- 定义了一个名为
check_mode
的实例方法,它接受一个字符串参数mode
,并返回一个布尔值。
def check_mode(self, mode: str) -> bool:
"""检查某个特定参数是否启用。
:param mode: 参数名称,例如 ``synchronous_mode``,``failsafe_mode``,``pause``,``check_timeline`` 等等。
:returns: 如果参数 *mode* 在全局配置中被启用,则返回 ``True``。
"""
return bool(parse_bool(self.__config.get(mode)))
作用:
check_mode
方法的具体作用是检查全局配置中某个特定的模式参数是否被启用。具体来说:
- 参数解析:
- 接收一个字符串参数
mode
,表示要检查的配置项的名称。
- 接收一个字符串参数
- 配置值获取:
- 使用
self.__config.get(mode)
方法从配置对象中获取对应名称的配置值。
- 使用
- 配置值转换:
- 使用
parse_bool
函数将配置值转换成布尔值。假设parse_bool
函数能够处理多种类型的配置值(如字符串 "true"/"false" 或者 "yes"/"no" 等)。
- 使用
- 返回布尔结果:
- 如果配置项存在且被启用,则返回
True
; - 如果配置项不存在或未被启用,则返回
False
。
- 如果配置项存在且被启用,则返回
1.21.1.7 get_standby_cluster_config()
- 定义了一个名为
get_standby_cluster_config
的实例方法,该方法不接受额外的参数,并返回一个字典或任意类型的值。
def get_standby_cluster_config(self) -> Union[Dict[str, Any], Any]:
"""获取 ``standby_cluster`` 配置。
:returns: 返回 ``standby_cluster`` 配置的一个副本。
"""
return deepcopy(self.get('standby_cluster'))
作用:
get_standby_cluster_config
方法的具体作用是从全局配置中获取 standby_cluster
的配置,并返回一个该配置的深拷贝。具体来说:
- 获取配置:
- 使用
self.get('standby_cluster')
方法从配置对象中获取standby_cluster
的配置值。
- 使用
- 创建配置副本:
- 使用
deepcopy
函数创建配置值的深拷贝,确保返回的配置不会受到后续对原始配置修改的影响。
- 使用
- 返回配置副本:
- 返回
standby_cluster
配置的一个深拷贝。
- 返回
1.21.1.8 get_int()
- 定义了一个名为
get_int
的实例方法,它接受三个参数:name
、default
和base_unit
,并返回一个整数。
def get_int(self, name: str, default: int = 0, base_unit: Optional[str] = None) -> int:
"""从全局配置中获取 *name* 的当前值,并尝试将其作为 :class:`int` 类型返回。
:param name: 参数的名称。
:param default: 如果 *name* 没有在配置中或无效时的默认值。
:param base_unit: 可选的基本单位,用于转换 *name* 参数的值。
如果值不包含单位,则不使用此参数。
:returns: 如果 *name* 设置并且有效,则返回其当前配置值;否则返回 *default*。
"""
ret = parse_int(self.get(name), base_unit)
return default if ret is None else ret
作用:
get_int
方法的具体作用是从全局配置中获取指定名称的参数值,并将其转换为整数。具体来说:
- 参数解析:
- 接收一个字符串参数
name
,表示要获取的配置项的名称。 - 接收一个整数参数
default
,表示如果配置项不存在或无法转换为整数时的默认值。 - 接收一个可选字符串参数
base_unit
,用于在配置项的值包含单位时进行转换。
- 接收一个字符串参数
- 配置值获取与转换:
- 使用
self.get(name)
方法从配置对象中获取对应名称的配置值。 - 使用
parse_int
函数尝试将配置值转换为整数。如果配置值包含单位,base_unit
参数可用于转换。
- 使用
- 返回整数值:
- 如果转换成功,则返回转换后的整数值。
- 如果转换失败或配置项不存在,则返回
default
参数指定的默认值。
1.21.2 __getattr__()
- 定义了一个名为
__getattr__
的函数,它接受两个参数:mod
和name
,并返回任意类型的值。
def __getattr__(mod: types.ModuleType, name: str) -> Any:
"""此函数的存在只是为了使 pyright 满意。
如果没有它,pyright 会在访问 global_config 模块的未知成员时抱怨。
"""
return getattr(sys.modules[__name__], name) # pragma: no cover
作用:
__getattr__
函数的具体作用是在动态属性访问时提供一个备选方案,特别是在使用静态类型检查工具(如 pyright)时避免对未声明的模块成员进行警告。
具体来说:
- 处理静态类型检查工具的警告:
- 当使用静态类型检查工具(如 pyright)时,如果尝试访问模块中未明确声明的属性,可能会收到警告或错误提示。
- 此函数作为一个兜底方法,确保在访问模块的未知属性时能够顺利获取到该属性,从而避免静态类型检查工具的警告。
- 动态属性访问:
- 在某些情况下,模块可能需要动态地添加属性,或者依赖于运行时动态生成的属性。
__getattr__
提供了一种机制来处理这些动态属性的访问。
- 兼容性和灵活性:
- 通过
getattr
函数从当前模块的全局命名空间中获取属性name
的值,增加了代码的兼容性和灵活性。
- 通过
1.22 tags.py
Tags
类:
1.22.1 类:Tags
- 定义了一个名为
Tags
的抽象基类(Abstract Base Class,简称 ABC),继承自abc.ABC
。
class Tags(abc.ABC):
"""一个封装了所有 ``tags`` 逻辑的抽象类。
希望使用所提供功能的子类必须实现 ``tags`` 抽象属性。
.. note::
由于向后兼容的原因,旧标签可能比新标签有更宽松的类型转换规则。
"""
1.22.1.1 _filter_tags()
- 定义了一个名为
__init__
的初始化方法,它属于GlobalConfig
类的一个实例,并且该方法的返回类型为None
。
@staticmethod
def _filter_tags(tags: Dict[str, Any]) -> Dict[str, Any]:
"""Get tags configured for this node, if any.
Handle both predefined Patroni tags and custom defined tags.
.. note::
A custom tag is any tag added to the configuration ``tags`` section that is not one of ``clonefrom``,
``nofailover``, ``noloadbalance``,``nosync`` or ``nostream``.
For most of the Patroni predefined tags, the returning object will only contain them if they are enabled as
they all are boolean values that default to disabled.
However ``nofailover`` tag is always returned if ``failover_priority`` tag is defined. In this case, we need
both values to see if they are contradictory and the ``nofailover`` value should be used.
:returns: a dictionary of tags set for this node. The key is the tag name, and the value is the corresponding
tag value.
"""
return {tag: value for tag, value in tags.items()
if any((tag not in ('clonefrom', 'nofailover', 'noloadbalance', 'nosync', 'nostream'),
value,
tag == 'nofailover' and 'failover_priority' in tags))}
作用:
1. 注解
1.1 @property
@property
是 Python 中的一个装饰器(decorator),用于将一个方法变成一个属性(attribute)。在 Python 中,@property
通常用于定义 getter 方法,这样就可以像访问属性一样访问这个方法的结果,而不需要显式地调用方法。
@property
的作用
- 简化属性访问:
- 通过
@property
,可以像访问普通属性一样访问方法的结果,而不必显式地调用方法。 - 例如,你可以写
obj.name
而不是obj.get_name()
。
- 通过
- 封装和验证:
- 可以在 getter 方法中加入逻辑,例如验证数据的有效性、转换数据格式等。
- 也可以在 setter 方法中加入逻辑,例如在赋值前进行验证或修改值。
- 保持面向对象风格:
- 使用
@property
可以保持面向对象编程的风格,让类看起来更像是一个数据模型,而不是一堆方法。
- 使用
使用
class MyClass:
def __init__(self, value):
self._value = value
@property
def value(self):
"""返回内部存储的值。"""
return self._value
@value.setter
def value(self, new_value):
"""设置内部存储的值,但在设置之前进行验证。"""
if not isinstance(new_value, int):
raise ValueError("Value must be an integer.")
self._value = new_value
@value.deleter
def value(self):
"""删除内部存储的值。"""
del self._value
1.2 @staticmethod
@staticmethod
是 Python 中的一个装饰器,用于定义静态方法。静态方法并不直接与类的状态相关联,也就是说,它们并不访问类的实例变量或类变量。
@staticmethod
的主要作用
- 不依赖实例状态:
- 静态方法不需要访问类的状态,因此它们不具有
self
或cls
参数。这意味着静态方法并不依赖于类或其实例的存在。 - 这种方法通常用于那些功能上独立于类本身的方法,但逻辑上又与类相关联的情况。
- 静态方法不需要访问类的状态,因此它们不具有
- 命名空间组织:
- 静态方法可以作为一种组织工具,用来将一些功能相关的函数放在同一个类中,而不是让它们散落在全局命名空间中。
- 这有助于提高代码的可读性和可维护性。
- 代码重用:
- 如果有一个函数经常在类的不同实例中使用,那么可以将其定义为静态方法,这样就不必在每个实例中都重复定义该函数。
- 实例无关:
- 因为静态方法不依赖于类的实例状态,所以可以在没有创建类的实例的情况下调用它们。
使用
class MathUtils:
@staticmethod
def add(a, b):
"""Add two numbers."""
return a + b
@staticmethod
def multiply(a, b):
"""Multiply two numbers."""
return a * b
# 调用静态方法
result = MathUtils.add(5, 3)
print(result) # 输出 8
result = MathUtils.multiply(5, 3)
print(result) # 输出 15
在这个例子中,add
和 multiply
方法都是静态方法,它们不依赖于 MathUtils
类的实例。你可以直接通过类名来调用它们,而无需创建类的实例。
注意事项
尽管静态方法在某些情况下非常有用,但在设计类的时候应该谨慎使用静态方法,特别是当方法逻辑上与类的状态紧密相关时。在这种情况下,应该考虑使用实例方法或类方法,而不是静态方法。
- 实例方法:需要一个
self
参数(通常是第一个参数),用于访问实例数据。 - 类方法:使用
@classmethod
装饰器定义,第一个参数通常命名为cls
,用于访问和修改类的状态。
1.3 @abc.abstractmethod
@abc.abstractmethod
是一个装饰器,用于标记一个方法为抽象方法。这个装饰器通常用在定义抽象基类(Abstract Base Class, ABC)的时候。它来源于 Python 的 abc
模块,该模块提供了创建抽象基类的能力。
装饰器的作用
- 强制子类实现:
- 当一个类继承了一个包含
@abc.abstractmethod
方法的抽象基类时,这个类必须实现所有的抽象方法,否则它自身也会成为一个抽象类,不能被实例化。
- 当一个类继承了一个包含
- 定义接口:
- 抽象方法定义了一个类的公共接口,告诉其他开发者或用户,任何继承自该抽象基类的类都需要提供这些方法的具体实现。
- 确保一致性:
- 使用抽象方法可以帮助确保所有派生类都遵循相同的行为规范,从而提高代码的一致性和可维护性。
使用
import abc
class MyAbstractBaseClass(metaclass=abc.ABCMeta):
@abc.abstractmethod
def do_something(self):
"""子类必须实现这个方法"""
pass
class MyClass(MyAbstractBaseClass):
def do_something(self):
print("实现了抽象方法")
# 下面的代码会引发 TypeError,因为 MyClass 没有实现 do_something 方法
# class IncorrectImplementation(MyAbstractBaseClass):
# pass
# 创建一个 MyClass 的实例
instance = MyClass()
instance.do_something() # 输出 "实现了抽象方法"
应用场景
抽象方法通常用于以下几种情况:
- 框架设计:
- 在开发框架时,可以定义一个包含抽象方法的基类,要求使用者实现这些方法来完成特定的功能。
- 插件系统:
- 在设计插件系统时,可以通过定义包含抽象方法的基类来规定插件应该提供的功能。
- API 设计:
- 在设计 API 时,可以通过抽象方法来定义 API 的契约,确保所有实现类都遵循相同的接口规范。
1.4 @contextmanager
@contextmanager
是一个装饰器,它来自 Python 的标准库模块 contextlib
,用于简化上下文管理器的创建。通常,实现上下文管理器需要定义一个类,并在类中实现 __enter__()
和 __exit__()
方法。然而,有时候我们只需要创建一个临时性的、简单的上下文管理器,这时使用 @contextmanager
可以更加方便和简洁。
工作原理
@contextmanager
允许你使用生成器来创建上下文管理器。生成器函数应该使用 yield
语句来标记进入上下文的点。在 yield
之前执行的操作相当于 __enter__()
方法,在 yield
之后执行的操作相当于 __exit__()
方法。
使用
from contextlib import contextmanager
@contextmanager
def my_open(filename, mode=`r`):
print("Opening file")
file = open(filename, mode)
try:
yield file # 相当于 __enter__()
finally:
print("Closing file")
file.close() # 相当于 __exit__()
# 使用 my_open 作为上下文管理器
with my_open(`example.txt`, `w`) as f:
f.write(`Hello, world!\n`)
my_open
是一个生成器函数,它使用@contextmanager
装饰器。- 当进入
with
语句的上下文时,my_open
函数被调用,直到yield
表达式为止,所有执行的操作都会在__enter__()
方法中执行。 yield file
表达式返回file
对象,并暂停函数的执行。此时,控制权转移到with
语句块。- 当
with
语句块执行完毕后,生成器恢复执行,并执行yield
后面的代码,这部分相当于__exit__()
方法。
作用
- 简化上下文管理器的创建:不需要定义类和实现
__enter__()
和__exit__()
方法。 - 更加灵活:可以轻松地创建一次性的上下文管理器,特别是在需要临时处理某些资源的时候。
- 保持代码清晰:使得上下文管理逻辑更加明确和易于理解。
1.5 @classmethod
@classmethod
是 Python 中的一个装饰器,用于定义类方法。类方法的第一个参数通常是 cls
,它代表类本身而不是类的实例。这意味着类方法可以直接通过类名来访问类的属性,而不需要创建类的实例。
使用场景
- 当方法不需要访问实例状态:如果一个方法不需要访问实例变量,只需要操作类变量或提供一些与类相关的行为,那么可以使用类方法。
- 工厂模式:有时候,我们希望提供替代构造器来创建类的不同变体或状态。这时可以使用类方法作为工厂方法来创建类的实例。
- 类的状态管理:如果有一些状态需要在类级别维护,比如计数器或者缓存,那么可以在类方法中实现这些功能。
使用
class MyClass:
counter = 0
def __init__(self):
MyClass.counter += 1
@classmethod
def increment_counter(cls):
cls.counter += 1
@classmethod
def get_counter(cls):
return cls.counter
# 使用类方法
MyClass.increment_counter()
print(MyClass.get_counter()) # 输出: 1
obj = MyClass()
print(MyClass.get_counter()) # 输出: 2
在这个示例中,increment_counter
和 get_counter
都是类方法,可以直接通过类名调用,而不必创建类的实例。
作用
- 类级别的操作:
- 类方法可以用来执行与类相关的操作,而不是与类的实例相关。它主要用于处理类级别的状态或数据,不需要依赖于类实例的状态。
- 替代构造器:
- 类方法常被用作“工厂方法”或“替代构造器”,用于创建类的不同实例。这在某些情况下很有用,比如根据不同的输入创建不同类型的对象。
- 减少内存消耗:
- 由于类方法不需要实例化对象就可以调用,因此可以节省内存资源。特别是在创建对象之前需要做一些准备工作的时候,使用类方法可以避免不必要的实例化。
- 清晰的接口定义:
- 使用类方法可以明确地表明某个方法是与类相关的,而不是与类的实例相关的。这有助于提高代码的可读性和可维护性。
2. 命令风格
2.1 函数使用命名设计
parse_version
和 _parse_version
这样的函数名通常暗示了一个是公开的 API,而另一个则是内部使用的私有函数。这种命名约定在很多编程语言中都很常见,包括 Python。
parse_version
:- 这通常是一个设计用于外部使用的函数,意味着它是 API 的一部分,用户可以依赖其稳定性和行为。
parse_version
函数通常是用来解析版本字符串并将其转换成一种更容易比较的形式,例如将 "1.2.3" 解析成一个版本对象,可以用来比较不同的版本。
- 这通常是一个设计用于外部使用的函数,意味着它是 API 的一部分,用户可以依赖其稳定性和行为。
_parse_version
:- 带有下划线前缀的
_parse_version
通常表示这是一个内部使用的函数,即它不是设计给外部使用的。这种函数可能是某个更大功能的一部分,它可能被其他内部函数或者类所调用,但是不应该被外部代码直接调用。这个约定是告诉开发者们这个函数的实现细节可能会改变,不应该依赖于它的存在或者具体的行为。
- 带有下划线前缀的
3. 关键字
3.1 with
Python 中的 with
关键字用于简化资源管理和确保清理工作(如关闭文件、释放内存、断开网络连接等)的完成,即使在处理过程中发生异常也是如此。with
语句通常与实现了上下文管理协议的对象一起使用。上下文管理协议指的是对象实现了 __enter__()
和 __exit__()
方法,这两个方法定义了进入和退出上下文管理器时的行为。
示例
f = open(`example.txt`, `w`)
f.write(`Hello, world!\n`)
f.close()
with open(`example.txt`, `w`) as f:
f.write(`Hello, world!\n`)
with
语句会负责在代码块执行完成后自动调用 close()
方法关闭文件,即使在写入期间发生了异常。这里 open()
函数返回的文件对象默认实现了 __enter__()
和 __exit__()
方法,因此可以与 with
语句一起使用。
主要用途
- 简化资源管理:它使得代码更简洁,不需要显式地调用资源释放方法。
- 确保清理工作:即使在代码执行过程中发生异常,也能确保资源被正确地释放。
- 提高代码可读性:使用
with
语句可以使代码更容易理解,因为它明确了资源管理和清理的意图。