使用 SQLAlchemy Engine 事件进行错误包装¶
https://blueprints.launchpad.net/oslo.db/+spec/use-events-for-error-wrapping
oslo.db 提供的 SQLAlchemy 执行时错误转换系统,包括 DBAPI 和相关错误,应该得到改进,使其更简单、更具可移植性、更向前兼容、更易扩展和更易测试。
问题描述¶
oslo.db 提供了一个系统,通过该系统拦截 SQLAlchemy 中引发的常见异常,并通常将其转换为 oslo.db.exception 中的 oslo 特定异常。这种机制涉及在关键 ORM Session 方法周围放置一个包装函数,以便在调用这些方法并引发异常时,异常会被一个处理函数捕获,该函数会立即对异常做出一些决策,然后可能将其传递给一系列“异常匹配”函数。oslo.db 测试套件包含针对此系统的各种测试,针对不同类型的异常有单独的测试套件。
当前实现中此系统的一些关键问题包括
为了在语句执行的地方放置错误拦截器,oslo.db 提供了一个
Session子类,然后列出调用数据库查询的关键方法,并使用名为@_wrap_db_error的包装器对其进行包装。这种方法遗漏了许多 ORM 级别的方法,例如Query对象之上的所有方法(有关此问题,请参阅 https://review.openstack.org/#/c/92002/),未加载或过期的映射属性发出的延迟加载,以及整个 Core 级别系统,即使在 ORM 级别,在Session.execute()的形式下,结果对象也暴露了它,并且在行访问时也可能引发数据库级别异常。以当前的形式,扩展此系统以涵盖所有可能的访问方式非常困难,尤其是在 ORM 仪器化属性方面。即使可以将此系统扩展到涵盖每种可能的方法,它也与未来可能从不同起点调用语句执行的新系统不兼容。错误拦截器本身提供了一种命令式(即:大量的条件语句)系统,用于拦截异常并将它们路由到不同的决策路径。一些异常,例如
UnicodeEncodeError到DBInvalidUnicodeParameter,直接在@_wrap_db_error内部处理。其他异常,例如死锁和重复行,被传递到其他函数,这些函数本身包含相当多的命令式逻辑,特别是“重复行”处理程序。添加对新异常场景的支持需要向这些系统中添加新的条件语句,从而使所有系统的逻辑变得不稳定,因为它们都是相互关联的。单元测试异常过滤器的系统是不一致且不完整的。
_wrap_db_error()函数本身只有 27 行代码中的 14 行被测试覆盖;大约一半的潜在错误场景没有得到充分测试。此外,测试,例如TestDBDeadlocked,测试了错误过滤器函数_raise_if_deadlock_error(),但没有在完整的_wrap_db_error()函数上下文中进行测试。如果另一个过滤器函数,例如_raise_if_db_connection_lost(),无意中捕获了此错误,则该场景没有被测试。由于测试系统对不同的异常采取不同的方法,因此为新的错误场景添加测试并不容易。
提议的变更¶
oslo.db 将利用 SQLAlchemy Connection 级别钩子,该钩子会在 SQLAlchemy 本身拦截的 DBAPI 生成的所有异常时被调用,包括语句执行和结果获取阶段。虽然 SQLAlchemy 自 0.7 起就支持这种钩子 dbapi_error(),但它将被一个新的钩子 handle_error() 取代,该钩子经过完善,以确保它被调用以处理 SQLAlchemy 本身处理的所有可能的错误,而不仅仅是 DBAPI 级别异常,并且它支持将给定的异常转换为新的异常。
将在 SQLAlchemy 0.9.7 中发布的 handle_error() 钩子的机制,将通过将事件直接回移植到 oslo.db 中,使用自定义 Connection 子类,SQLAlchemy 支持通过用作其工厂的 Engine 来替换它,从而在 requirements.txt 中列出的所有先前版本的 SQLAlchemy 中可用。此更改的方面类似于当前子类化 Session 的方法,从技术上讲,它可以单独满足未来的功能;但是,允许 SQLAlchemy 从 0.9.7 开始原生提供此事件意味着 oslo.db 的兼容层只需要针对已经发布的 SQLAlchemy 版本,而不会冒 Connection 子类方法在未来版本中发生更改的风险,因为它不会用于未来的版本。
在 handle_error() 侦听器中,_wrap_db_error() 的逻辑将被展开为一种声明式系统,函数可以声明为提供特定数据库/异常/正则表达式组合的过滤器。这些过滤器将由一个通用系统解释,该系统确保为给定的异常输入调用正确的过滤器。现有的规则,特别是正则表达式和有关它们的说明,可以维护并移植到新系统,而无需更改,因此基于实际数据库观察所做的现有工作得以保留。
一个这样的过滤器示例如下
@filters("mysql", sqla_exc.OperationalError, r"^.*\(1213, 'Deadlock.*")
@filters("postgresql", sqla_exc.OperationalError, r"^.*deadlock detected.*")
def _deadlock_error(operational_error, match, engine_name, is_disconnect):
raise exception.DBDeadlock(operational_error)
在上述方法中,可以添加对新的数据库后端和新拦截异常的支持,而不会影响现有的规则。
在测试方面,一个类似框架将允许使用一致的系统构建异常过滤器测试,该系统从 SQLAlchemy 的语句执行点运行到过滤例程的最终结果。该系统的关键是模拟当前引擎的方言的 Dialect.do_execute() 方法,以便可以引发特定的异常,包括模拟 DBAPI 级别异常。所有异常场景都易于在此注入,并且过滤器和操作的系统可以被 100% 覆盖。
备选方案¶
错误可以在更高级别的框架中处理。如果所有 oslo.db 应用程序都将数据库代码用于一些通用包装器中,例如普遍使用的“事务”包装器,那么一致的异常处理也可以在该级别发生。我们没有这样做,因为当前数据库方法尚未达到这种一致性水平,并且它还会对所有代码施加限制,如果未遵守该限制,则会导致意外和未处理的异常被引发。
对于异常路由,可以使用更具数据驱动性的系统,例如使用名称表达异常过滤和处理的规则引擎。我们没有这样做,因为它过于繁重;正如目前的情况,我们在异常过滤级别采用数据驱动的方法,然后在处理现在拦截的异常时转为编程方式。这似乎是一种利用 Python 将函数视为数据结构的能力的良好方法。
Impact on Existing APIs¶
此更改不应影响 oslo.db 之外的任何内容,除非其他系统正在显式使用 _wrap_db_error()。该函数被下划线标记为私有,因此不应该出现这种情况,但是如果出现这种情况,我们可以提供一个什么也不做的 _wrap_db_error() 方法,因为新的过滤系统发生在更低的层级。
安全影响¶
无。
性能影响¶
handle_error() 事件和过滤系统使用的额外比较和迭代量与当前系统相比微不足道。两种系统都仅在引发异常后才被调用,这已经是 Python 中非性能分支,因此任何轻微的性能差异实际上在任何情况下都没有影响。
Configuration Impact¶
无。
开发人员影响¶
oslo.db.exceptions 系统现在将对所有 SQL 操作生效。当前未被现有系统覆盖的现有系统现在将调用新的行为。
实现¶
负责人¶
- 主要负责人
zzzeek
- 其他贡献者
无
里程碑¶
初始原型实现已完成,并将作为与此蓝图一起提交的 Gerrit。
工作项¶
在 SQLAlchemy 中实现
handle_error()事件和测试,并为 0.9.7 版本做准备。在 oslo.db 中实现
handle_error()兼容层,并将 SQLAlchemy 更改中的逻辑和测试的选定元素移植过来。这将发生在新的子包oslo.db.sqlalchemy.compat中,各种 SQLAlchemy 向后兼容系统将从这里开始。确保 oslo.db 层在 SQLAlchemy 的 0.7、0.8 和 0.9 系列上工作。
将
_wrap_db_error()和子函数重构为声明式过滤器系统,并在oslo.db.sqlalchemy.session.create_engine()工厂中安装过滤器。找到 tests/ 中所有测试异常集成系统的不同部分的测试套件,并将它们转换为使用新系统。
孵化¶
无。
采用¶
无。
库¶
oslo.db
预计 API 稳定¶
无。
文档影响¶
如果 oslo.db 中有关于异常处理的文档,则应提及此处理安装在 Engine 的核心级别。
依赖项¶
此更改依赖于将 SQLAlchemy 0.7、0.8、0.9 版本集成到 tox.ini 套件中,该套件已被合并。
参考资料¶
此功能在 SQLAlchemy 和 OpenStack wiki 页面上讨论过,网址为 https://wiki.openstack.org/wiki/OpenStack_and_SQLAlchemy#Exception_Rewriting。
注意
本作品采用知识共享署名 3.0 非移植许可协议授权。 http://creativecommons.org/licenses/by/3.0/legalcode