Skip to content

Commit

Permalink
disposable part 2
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Nov 30, 2023
1 parent dcaf14a commit 1c8183d
Show file tree
Hide file tree
Showing 2 changed files with 23 additions and 38 deletions.
57 changes: 21 additions & 36 deletions zh-CN/cookbook/design/disposable.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,65 +13,50 @@ Cordis 的名字来源于拉丁语的心。我希望它能成为未来软件 (

作为一个元框架,Cordis 并不耦合任何具体的领域或场景。它所提供的能力是大多数框架都不足为奇的——插件系统,但在这个系统背后却是大多数框架都没有达成的目标:可逆性。

## 软件的可逆性缺失
## 可逆性

### 引子:软件文明在退步吗?

我时常会觉得现代软件相比于曾经的软件存在着某种退步。在过去,我们用 C 这样的语言编写程序时,我知道 `open()` 会返回一个 `fd`,我知道 `malloc()` 会返回一个 `ptr`,我知道 `fork()` 会返回一个 `pid`。这些东西通常被称为 **资源 (Resource)**。我也知道,为了编写可靠的程序,我应当在使用完这些资源后,调用对应的函数来回收它们。

而在如今,当我使用 Koa 时,我可以使用 `app.use()` 来注册一个中间件;当我使用 Vue 时,我可以使用 `app.component()` 来注册一个组件;当我使用 Node.js 时,我可以直接导入 `.node` 文件来加载使用 C++ 编写的模块。但很遗憾的是,Koa 不会教你如何取消这个中间件,Vue 不会教你如何卸载这个组件,Node.js 甚至会永久占用这个 `.node` 文件。
而在如今,当我使用 Koa 时,我可以使用 `app.use()` 来注册一个中间件;当我使用 Vue 时,我可以使用 `app.component()` 来注册一个组件;当我使用 Node.js 时,我可以直接导入 `.node` 文件来加载使用 C++ 编写的模块。但很遗憾的是,Koa 不会告诉你如何取消这个中间件,Vue 不会告诉你如何卸载这个组件,Node.js 甚至会永久占用这个 `.node` 文件。

你当然可以说这是软件发展的结果:底层 API 被妥善地封装了,开发者不再需要关心这些细节。但被封装后的资源仍然是资源,它们仍然有着被回收的需求。或许对于每一个具体的场景,我们都可以找到一个解决方案,或者给出我们不需要回收资源的理由,但面对一个复杂的、未知的应用,如果你想要回收资源而它又没有提供相应的 API,最好的办法就只有重启了。

事实上,封装也根本不是导致这种现象的原因。面对不当使用指针引发的内存安全问题,无论是 C++ 的智能指针、Java 的垃圾回收机制,还是 Rust 的所有权系统,都提供了对指针的封装。这些封装不仅不会导致内存泄露,反而通过提高易用性减少了开发者的心智负担。

有了正面的例子,我们就可以知道,这种退步实际上只是特定领域中呈现出的趋势 (在 JavaScript 和 Python 这种高级语言中尤为明显)。或许是人们认为重启过于方便了,因此一些框架的开发者们已经完全不考虑回收资源的需求了。但现代软件就如同摩天大楼,一旦某一层缺失了支撑,在其上的一切都会变得摇摇欲坠。好在我们或许有办法改善这一切。

### 可逆性的优势

## 定义

可逆的 Koishi 是指,对于任何一个 Koishi 实例,任意进行加载和卸载插件操作后,最终行为仅与最终启用的插件相关;与中间是否重复加载过插件、插件之间的加载或卸载顺序都无关。你也可以简单理解为「路径无关」。这里的相关和无关具体包括:

- 任意次加载并卸载一个插件后,内存占用不会增加
- 任意次加载并卸载一个插件后,不会残留对其他插件的影响
- 如果插件之间有依赖关系,依赖的插件会自动在被依赖的插件之后加载,并自动在被依赖的插件之前卸载,即确保插件的生命周期由依赖关系而非加载顺序决定

## 设计动机

实现了「可逆的 Koishi」的项目将会获得以下优点。

### 热重载

由于插件的副作用会在卸载时回收,Koishi 的所有插件都将可以在运行时加载、卸载和重载。这显著降低了用户的开发和更新成本,并大幅提高了 Koishi 应用的 SLA。
**可逆性 (Disposability)**,即回收资源的能力,可以为软件带来以下好处:

### 异步加载
**可组合性 (Composibility)**。很多软件很喜欢用「模块化」「插件」这样的词,这显然是来自现实世界的概念。然而现实中的模块也应当是可拆卸的,现实中的插件也应当是可以拔出的。不可逆的软件即便进行了模块化,也只会随着时间推移而变得更加臃肿。此外,可逆性可以让我们更好地理解模块之间的依赖关系,从而更好地促成解耦。这一点我们稍后会进一步讨论。

由于插件的加载顺序由依赖关系决定,因此插件的代码可以被异步地加载,而不需要担心加载顺序对可用性的影响。这将显著提高 Koishi 的启动速度
**可靠性 (Reliability)**。当软件的规模增加时,可逆性可以确保软件所使用的内存和其他资源都在可控范围内 (内存安全其实是资源安全的一种特殊情况)。同时,由于可逆性也意味着可追踪性,即便某个模块出现了资源泄露,我们也可以快速定位错误的来源

### 可追踪
**可访问性 (Availability)**。一个拥有众多功能的软件,如果没有提供可逆的 API,那替换任何一个组件都意味着整个重启。重启期间,那些本可以不受影响的服务也被迫下线。但如果其中的每个组件都是可逆的,我们就可以在保证其他功能持续运行的情况下替换掉任何一个组件,甚至可以滚动更新整个程序自身。实现了可逆性后,软件将显著降低由于故障和更新带来的额外开销。

可逆性意味着由 Koishi 插件注册的指令和中间件、监听的事件、提供的本地化、扩展的页面、抛出的错误都可以被明确地追踪来源。这有利于在大型项目中快速定位问题。
### 可逆的 Koishi

## 生态现状
相比上面这些可能有些晦涩的概念,以 Koishi 作为更具体的例子或许更有说服力。

目前的 Koishi 生态普遍依赖此模式。

### 依赖服务的插件

Koishi 存在大量依赖服务的插件。任何插件可以声明自身依赖某些服务,由 Koishi 确保插件只在服务加载完成后加载,并在卸载开始前卸载。

### @koishijs/plugin-config

@koishijs/plugin-config 提供了「插件管理」页面,允许用户在运行时启用、停用、修改插件配置,而不用重启 Koishi。这些功能直接与 Cordis 的底层 API 交互,确保了所有操作的可逆性。

### @koishijs/plugin-hmr
可逆的 Koishi 是指,对于任何一个 Koishi 实例,任意进行加载和卸载插件操作后,最终行为仅与最终启用的插件相关;与中间是否重复加载过插件、插件之间的加载或卸载顺序都无关。你也可以简单理解为「路径无关」。这里的相关和无关具体包括:

@koishijs/plugin-hmr 允许用户在开发过程中直接通过保存源文件来按需重载插件源码和配置。这是非常少见的后端 HMR (Hot Module Replacement,模块热替换) 实现。
- 任意次加载并卸载一个插件后,内存占用不会增加。
- 任意次加载并卸载一个插件后,不会残留对其他插件的影响。
- 如果插件之间有依赖关系,依赖的插件会自动在被依赖的插件之后加载,并自动在被依赖的插件之前卸载,即确保插件的生命周期由依赖关系而非加载顺序决定。

### @koishijs/client
实现了可逆性的 Koishi 项目将获得以下优点:

Koishi 的控制台前端由 @koishijs/client 提供,这个包同样依赖了 Cordis。这意味着 Koishi 的前端插件也是可重载的。此两者共同确保了 Koishi 控制台插件的可逆性。
- **热重载**:由于插件的副作用会在卸载时回收,Koishi 的所有插件都将可以在运行时加载、卸载和重载。这显著降低了用户的开发和更新成本,并大幅提高了 Koishi 应用的 SLA。
- **异步加载**:由于插件的加载顺序由依赖关系决定,因此插件的代码可以被异步地加载,而不需要担心加载顺序对可用性的影响。这将显著提高 Koishi 的启动速度。
- **可追踪**:由 Koishi 插件注册的指令和中间件、监听的事件、提供的本地化、扩展的页面、抛出的错误都可以被明确地追踪来源。这有利于在大型项目中快速定位问题。

## 实现原理

说了这么多好处,可逆性真的可以实现吗?答案是肯定的。在这一节中,我们将会从数学的角度来探讨可逆性的实现原理。你会发现,任何语言都可以实现自己的 Cordis。

### 可逆的副作用

函数式编程中有着纯函数的概念——给定相同的输入总是给出相同的输出。然而,现实中的程序往往要与各种各样的副作用打交道。对于这种情况,我们可以对函数进行“纯化”——将它的副作用转化为参数和返回值的一部分即可。考虑下面的函数:
Expand Down
4 changes: 2 additions & 2 deletions zh-CN/cookbook/design/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@

### 零占用的 Koishi

零占用模式是指,给定一个正在运行的 Koishi 实例,移除该实例目录下的所有内容,实例应按照预期的方式保持工作。具体而言,实例内的所有模块,包括 Koishi 本体及所有插件,均在设计时对此情况做了考虑,并编写了相应的处理逻辑:
零占用的 Koishi 是指,给定一个正在运行的 Koishi 实例,移除该实例目录下的所有内容,实例应按照预期的方式保持工作。具体而言,实例内的所有模块,包括 Koishi 本体及所有插件,均在设计时对此情况做了考虑,并编写了相应的处理逻辑:

- Koishi 本体在启动时读取了配置文件。在启动后删除该文件,Koishi 保持工作。
- 存储大文件的插件在要求时加载、解析大文件并返回结果给用户。删除该文件后,插件无法顺利解析,但插件返回可读的错误文本或输出可读的错误日志,不会造成 Koishi 奔溃。
- 外部程序包装插件依赖外部的可执行文件进行工作。可执行文件在运行时无法解除占用,故应当预先被转移至实例目录之外。实例运行时,实例目录内不存在被占用的可执行文件。

实现了零占用模式的 Koishi 项目将会获得以下优点
实现了零占用模式的 Koishi 项目将获得以下优点

- **自更新**:可以通过插件更新 Koishi 及其依赖。在更新依赖的整个过程中,Koishi 及所有插件仍保持可用。目前已有 [market](../../plugins/console/market.md) 插件实现了此特性。
- **健壮性**:文件暂时无法访问不会导致 Koishi 崩溃。这对实例目录使用网络映射的场景更友好。
Expand Down

0 comments on commit 1c8183d

Please sign in to comment.