仅有趋同是不够的
Convergence is not enough

原始链接: https://www.inkandswitch.com/livelymerge/notebook/lm-02/

Livelymerge (LM) 项目探索了一个大胆的概念:构建一个实时系统,使整个堆(对象、类和方法)成为一个 Automerge 文档。虽然这实现了无缝的多用户协作,但也带来了一个重大挑战:Automerge 虽然能确保状态收敛,却无法保证所得状态符合应用程序的逻辑不变性。 作者以链表的并发修改为例,展示了 Automerge 对指针写入的“盲目”重放如何导致结构损坏,例如产生循环或链表截断。由于 Automerge 记录的是底层属性写入而非高层编程意图,它无法从本质上保护复杂的数据结构。 团队认为,未来的方向在于“合并感知型数据类型”(merge-aware datatypes)。通过将对原始指针操作的合并转向对语义操作的合并(例如,使用“在之后插入”而非“设置 next 指针”),系统可以在构建时强制执行不变性。尽管他们承认目前尚无完美的解决方案,但他们指出了在可交换操作和像 Coln 这样的基于约束的数据库系统领域中,存在一些有前景的研究。目前,该团队在继续攻克这一架构挑战的同时,主要依赖严谨的编程和 Automerge 的内置类型来维护系统完整性。

Hacker News | 最新 | 过往 | 评论 | 提问 | 展示 | 招聘 | 提交 | 登录 收敛是不够的 (inkandswitch.com) 19 分,zdw 发布于 2 小时前 | 隐藏 | 过往 | 收藏 | 1 条评论 帮助 alexisread 5 分钟前 [–] 关于格类型 (Lattice types) 的相关论文 (BloomL): https://dsf.berkeley.edu/papers/UCB-lattice-tr.pdf 回复 考虑申请 YC 2026 年秋季批次!申请截止日期为 7 月 27 日。 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

Introduction

In the Livelymerge project, Dan Ingalls, Peter Van Hardenberg, and I (Alex Warth) are building a Lively Kernel-like system whose heap — every object, class, and method — is an Automerge document. The pitch, from the first note in this series, was that this would give us merges “for free”: multiple users share the same object memory, work on it concurrently (even offline), and Automerge reconciles everything.

I also wrote that merging the state of a live system is a nontrivial problem, and that there’s no way to guarantee that objects’ invariants won’t be violated. This note looks that problem straight in the eye, with a concrete example. We don’t have a solution yet — this is an acknowledgment, not a victory lap — but I’ll sketch one possible solution that we find promising, and we’d love to hear your ideas, too.

Exhibit A: a linked list

Automerge’s promise is convergence: after two clients exchange their changes, they are guaranteed to arrive at the same state. But that state is not guaranteed to be one your program can live with. To be fair, for lots of applications — documents, todo lists, sketches — Automerge’s merge does just what you’d want. But in LM we’re doing something weirder: asking it to merge the heap of a running program, pointers and all. That’s well outside Automerge’s comfort zone, and this note is about what happens out there.

Consider a humble linked list containing 1 → 2 → 3 → 4, built the way any programmer would build it in LM: each node has a next property that points to another node. Now suppose two clients modify it concurrently:

  • Client A swaps 2 and 3, by writing 1.next ← 3, 2.next ← 4, and 3.next ← 2. Their list now reads 1, 3, 2, 4.
  • Client B swaps 3 and 4, by writing 2.next ← 4, 3.next ← null, and 4.next ← 3. Their list reads 1, 2, 4, 3.

Now let’s see what happens when these changes sync. Here’s how Automerge merges them: each client’s change is a transaction, and the merge behaves as if one transaction’s writes were applied and then the other’s — Automerge deterministically picks one of the two orders, and both clients get the same one. (So arbitrary interleavings of the individual writes are not possible, which limits the number of states we can end up in.) A write that the later transaction didn’t touch survives from the earlier one; where both transactions wrote, the later one wins.

Let’s see what the two possible orders actually produce. 1.next comes from A either way (B never wrote it), 4.next comes from B either way, but 3.next — which both transactions wrote — goes to whichever came second:

  • A then B (3.next = null): traversing from 1 yields “1, 3” — the list has been truncated, and nodes 2 and 4 are stranded off to the side.
  • B then A (3.next = 2): traversing from 1 yields “1, 3, 2, 4, 3, 2, 4, …” — the list now contains a cycle, and any code that walks it will never terminate.

Let me emphasize: Automerge did nothing wrong here. Both clients converge on the same result, deterministically, exactly as promised. The trouble is that replaying B’s writes after A’s is not the same as performing B’s intent after A’s. Client B computed those three writes by looking at the original list, 1 → 2 → 3 → 4 — a state that, post-merge, no longer exists. If B’s “swap 3 and 4” had actually run after A’s change, it would have produced different writes and a perfectly good list. The merge replays effects, not intents, and the programmer’s invariants — every node appears exactly once, no cycles, the list ends — were never written down anywhere that Automerge could see. Convergence is not enough.

This is not just about linked lists

You can dodge this particular example by not building linked lists out of next pointers. Automerge has built-in datatypes with well-behaved merge semantics — its arrays (which our object model exposes directly) merge concurrent insertions and deletions the way you’d hope, and maps of various flavors are easy to represent. We lean on this hard in practice in Morphic, the graphical framework at the heart of our system. (Morphic originated in the Self programming language — see Maloney et al. — and was later used in Squeak and Dan’s Lively Kernel. In Morphic, everything you see on screen is a morph: an object that can contain other morphs, all the way down to buttons and text.) Each morph’s submorphs list is an Automerge array, and concurrent adds from two users interleave just fine.

But all bets are off when you compose these datatypes — and this sort of thing happens very often in programming! The merge orders whole transactions, but it still replays their writes blindly, so any invariant that spans more than one property or object is invisible to it:

  • A doubly-linked list (next and prev must mirror each other).
  • A tree (Morphic itself: every morph’s owner must agree with its owner’s submorphs — two users concurrently reparenting the same morph can break this today).
  • A cached count or index that must agree with the collection it summarizes.
  • An “each element appears exactly once” constraint on anything.

In a system like LM — where the heap is the document, and users are encouraged to build whatever data structures they like — this isn’t a corner case. It’s a problem we’re actively thinking about.

That said, it has bitten us less often than we anticipated. With some careful programming — leaning on Automerge’s built-in datatypes wherever possible, and avoiding redundant representations that can be made to disagree — we’ve been able to build a system that holds up well in day-to-day multi-user use. (Careful programming isn’t a solution, of course; it’s what you do while you’re waiting for one.)

A direction we like: merge-aware datatypes

Today, merging happens at the level of the raw object graph, below the abstractions the programmer actually cares about. Given the diagnosis above, the natural move is to merge the intents instead of the writes they compiled down to. And notice that the intent isn’t “set 3.next to null” — it isn’t even “swap 3 and 4”, which is still implementation, just one level up. The intent is what the programmer would say at the level of the abstract type: “remove this value from the list”, “insert this value after that one”.

Automerge already works exactly this way — but only for its built-in types. A change to an Automerge list is recorded in the document’s history as insert and delete operations (that’s the actual Automerge term), not as pointer writes, and that’s precisely why concurrent edits to those lists merge so well. So here’s an idea we find intriguing: what if Automerge exposed a notion of types — list-like, map-like, counter-like — that programmer-defined data structures could declare themselves to be? The representation inside the document would be up to the programmer, but reads and writes would go through the type’s interface, and what gets recorded as operations would be the type’s higher-level vocabulary. Merging would then mean merging those operations, with semantics defined once per type. Even better if programmers could define entirely new types, not just adopt the built-in ones. There’s encouraging precedent that this can be done rigorously for a nontrivial type: Kleppmann et al.'s move operation for replicated trees bakes “reparenting never creates a cycle” directly into the merge. The technique underneath that algorithm generalizes surprisingly far: replay everyone’s operations in a single deterministic order (e.g., by timestamp), checking each operation against the invariant as you go and skipping the ones that would violate it. Since every client replays the same operations in the same order, convergence comes for free, and the invariant holds by construction. (ECRO is another system built on this idea.) It’s not a perfect answer, though: an operation that was valid when it ran can be retroactively invalidated when an operation with a lower timestamp arrives late, so users can watch previously-accepted work get rolled back. And the broader open question for user-defined types remains: a type’s operations have to commute, or come with a deterministic way to resolve the cases where they don’t, and it’s not obvious how a system would help programmers meet (or even check) that bar.

We’re not the only ones circling this territory. Martin Kleppmann, Vincent Liu, Owen Lynch, and their collaborators are tackling the problem head-on in Coln, a mergeable database with an expressive language for schemas, queries, and migrations. In Coln, you can declare constraints on your data — “this is a doubly-linked list”, say — and it takes a refreshingly strict line on violations: if merging would break a constraint, the merge is simply refused, and it’s up to the user (or an AI) to get the data back into a state that satisfies the constraints before the merge is allowed. Martin’s Convergence reading list is also a good entry point to the connection between invariants and coordination. If you’ve thought about this problem — or you’re sitting on a better solution — let’s talk!

Next time

In the next note, we’ll describe the object model that makes all of this concrete: how we make an Automerge document look and feel like an ordinary JavaScript heap — object table, proxies, garbage collection and all.

联系我们 contact @ memedata.com