From dd09a4718ffbfd36b94f841ccd45d816f3b7edcc Mon Sep 17 00:00:00 2001
From: i-shm app.set)
而在单个Domain中 OCaml 的 runtime 是通过在safe point期间半抢占式的切换线程。也就是说,线程切换只会发生在safe point期间。例如内存分配就是是safe point。这意味着在没有safe point的代码块内,可以在Domain内原子性地进行多次读写或访问操作,因为线程没被切换。
-OCaml 编译器提供了一个名为 [@poll error] 的annotation,可以在函数中使用它来确保该函数不包含safe point。
所以通过使用 [@poll error] 就可以创建在Domain内原子性执行的函数,也就是说,基于此特性可以实现单个Domain内线程安全的数据结构,例如 thread-table 便是使用这个特性实现的 Hash Table。可以看看它的 add 函数的实现:
let[@poll error] add_atomically t buckets n i before after = |
相比使用 Stdlib.Mutex,这种无锁实现会有更好的性能(特别是对于只读操作),并且还允许例如信号处理之类的上下文操作。
]]>而在单个Domain中 OCaml 的 runtime 是通过在safe point期间半抢占式的切换线程。也就是说,线程切换只会发生在safe point期间。例如内存分配就是是safe point。这意味着在没有safe point的代码块内,可以在Domain内原子性地进行多次读写或访问操作,因为线程没被切换。
+OCaml 编译器提供了一个名为 [@poll error] 的annotation,可以在函数中使用它来确保该函数不包含safe point。
所以通过使用 [@poll error] 就可以创建在Domain内原子性执行的函数,也就是说,基于此特性可以实现单个Domain内线程安全的数据结构,例如 thread-table 便是使用这个特性实现的 Hash Table。可以看看它的 add 函数的实现:
let[@poll error] add_atomically t buckets n i before after = |
相比使用 Stdlib.Mutex,这种无锁实现会有更好的性能(特别是对于只读操作),并且还允许例如信号处理之类的上下文操作。
+]]>药化团队合成了一批化合物。生物团队上传活性、选择性、ADME、早期毒理等实验结果。项目负责人看完某个化合物的数据包后,要做一个决策:
+提交这个决策时,系统通常要做这些事:
+这些写入必须一起成功。不能出现决策已经提交,但化合物阶段没变, 或是候选药物档案已创建,但数据包没有锁定等问题。
+最直接的实现,是在 Command Handler 里注入 Prisma,然后开事务:
+class SubmitCompoundPromotionDecisionHandler { |
但它把几个层次混在了一起。
+Command Handler 本来应该编排业务流程,但现在它编排了数据库表的更新顺序。
+化合物阶段变化本来应该由 Compound 聚合判断。比如只有 lead_optimization 阶段的化合物才能推进到 candidate,已经暂停的化合物不能直接推进。现在这条规则可能散落在 tx.compound.update 前后的 if 判断里。
事务边界本来属于用例。现在它由具体 ORM 的 $transaction 暴露在 Command Handler 里。换 ORM、拆 outbox、改审计事件写法,Command Handler 都要动。
如果把这段逻辑挪到 repository 里:
+compoundRepository.submitPromotionDecision(command); |
这看起来把 Prisma 从 Command Handler 里拿掉了。但如果 compoundRepository 内部仍然创建决策、更新化合物、创建候选药物档案、关闭责任、写审计事件,那只是把混乱换了一个地方,因为 Repository 的核心职责应该只是持久化聚合,例如:
compoundRepository.findById(id); |
如果一个 CompoundRepository 开始保存 CompoundDecision、DevelopmentCandidate、ReviewAssignment、AuditEvent,它就不再只是 Compound 的 repository。它变成了一个跨模块事务服务,只是名字还叫 repository。
所以这里我的解决方案之一是在 application 层定义一个 Unit of Work port,它表达的是一个具体业务动作:提交候选化合物推进决策。
+export interface SubmitCompoundPromotionDecisionUnitOfWork { |
Command Handler 依赖这个端口,而不是依赖 Prisma。
+class SubmitCompoundPromotionDecisionHandler { |
这段代码没有事务细节。它只转交命令意图。
+具体事务放在 infrastructure:
+class PrismaSubmitCompoundPromotionDecisionUnitOfWork |
这样就把“业务事务边界”和“命令入口”分开,Command Handler 不需要知道用 Prisma 还是别的 ORM。它也不需要知道事务里要调用哪些表。它只知道这个命令由一个原子用例完成。
+但聚合仍然要负责状态规则,Unit of Work 不能变成新的数据库脚本,在事务内部,仍然应该先读取聚合,让聚合执行状态变化,再保存聚合。
+例如 Compound 聚合可以这样表达规则:
class Compound { |
Unit of Work 在事务里调用这些方法:
+await prisma.$transaction(async (tx) => { |
而为了让事务里的代码复用聚合持久化逻辑,我们需要一种 tx-bound repository 或 store。
+最粗暴的写法是在领域 repository interface 里加一个可选事务参数:
+interface CompoundRepository { |
但它又把 Prisma 带进了领域层。就算把 Prisma.TransactionClient 包成 TransactionClient 类型别名,领域接口仍然在为基础设施妥协。
我更倾向于把 tx-bound 能力留在 infrastructure,领域层只定义干净的 repository port:
+export interface CompoundRepository { |
infrastructure 层拆一个 store,接收最小持久化客户端:
+type CompoundStoreClient = { |
普通 repository 可以继承这个 store,传完整 Prisma client:
+class PrismaCompoundRepository extends PrismaCompoundStore { |
Unit of Work 在事务里传 tx:
+await prisma.$transaction(async (tx) => { |