Lots of the comments here seem to be about why do you need CRDT when so many solutions can solve collaborative text editing.
The biggest bang is for de-centralization or edge based storage similar idea to @josephg (personal computing). Imagine taking multiple hours provisioning your personal compute with all your data and stream the diff across your devices and having an internet that runs off your devices. This would finally allow for a multi device local-first experience (https://www.inkandswitch.com/local-first/) where your phone streams the changes to your data while you were on your flight to your desktop and at a bigger scale entire facebook/google data store is a streaming system with auto-merge instead of a focus on ACID DB structures and coherence.
I've read a couple of CRDT papers but never used them in anger. (Zero interest in collaborative text editing, but a lot of interests in multi-master systems involving local databases that lose internet connectivity).
Anyone have any good war stories? The general impression I get is that anything more than a state delta two-phase set is an open research problem to implement efficiently - the papers make it sound pretty straight forward but there's a whole garbage collection problem to deal with, there was another research paper dealing with just that IIRC.
It's interesting to me that people still doubt that CRDTs can be used in practice. The fact is that they are already being used in practice by many companies with millions of users for applications like rich-text editing and state management in 3d applications. See https://github.com/yjs/yjs#who-is-using-yjs
In fact, Yjs - a CRDT implementation- is the most downloaded solution for collaborative applications. Even more than ShareDB (the most popular OT-based solution).
I implemented and deployed a state synchronization protocol based on CRDTs at my last job. It replaced a protocol that needed a fully connected mesh to function properly, and the entire cluster would suffer a grey failure if a connection was broken or slow. The new protocol was much more resilient, and was able to take advantage of the transitive property of state-based CRDT operations.
My solution for garbage collection was pretty boring. Occasionally, a JSON file was rsynced to each service node with the 'true' state of the system at some time in the past, and the CRDT maintained the new state on top of that JSON file, until the next file got rsynced.
For context, one of the main motivations for CRDT was multiple users simultaneously editing some document. E.G. Google Docs.
I was researching these approaches when I was implementing my own collaborative text editor. CRDT was too mathematically steep for me. Another approach is differential syncing. I think Google Docs was originally implemented with differential syncing and then switched to CRDT.
I think the tradeoff and why I leaned towards differential syncing was that the implementation was easier. There are some gaps / unclear implementation details in this white paper's algorithm that you kind of have to rediscover yourself, because if you followed the algorithm listed here it will not work. I forgot the details. I'm still not sure if that was intentional. I hope now the literature on CRDT has gotten more mainstream and digestible for non-mathematicians because that seems like the future.
They adopted Google Wave's algorithm which was taken from Jupiter UI system [1]: operational transformations [2] with conflict resolution on the server side.
When the server does ordering operation, operational transformations became a piece of cake to implement and reason about. If you add hierarchical addressing like "book/chapter/section/subsection/paragraph/char", they are also quite conflict free.
But you need a central authority. In case of Google, it is easy and fits the profit model.
Is anyone working on a CRDT system using pijul patches and merge strategy?
The "Replicated Data Type" part of CRDTs seems to be a solved problem, making "Conflict-Free" give results that are as close to what the user would expect as is theoretically possible would appear to be the hard part.
It also barfed on my wife's Mac (which has not been loaded with a mish-mash of development stuff), so I give up. If nobody else is reporting the same problems as I'm having then I guess it's just that I'm having a bad hair year.
The story I have in my head for CRDTs is basically all math and theory. I'd love a quick perspective on how CRDTs integrate into real app infrastructure in the real world. Do they interact with disk storage? Are there dedicated DB solutions that don't ruin their mathematical guarantees?
https://www.antidotedb.eu is an example of a CRDT database that very much hits disk and preserves the mathematical guarantees. It's based on transactional causal+ consistency, which is formally proven to be the strongest consistency mode for an AP database. Other great examples are https://automerge.org and https://www.macrometa.com
(Disclaimer: building a database system on top of Antidote at https://vaxine.io).
You can move CRDT data over whatever data layer you have on hand, and insert them into whatever data stores you want. Care for the special semantics of CRDT are only needed when merging CRDTs, storage and transportation is like any other encoded format, like moving a JPEG encoded image over HTTP, or storing a JPEG as a byte stream in a database. Neither the HTTP client or the database need to know how to turn the JPEG bytes into screen pixels.
Here’s a possible design for a CRDT editor system:
Browser client composes a CRDT locally in-memory, and saves the CRDT data into IndexedDB periodically as a Blob of bytes.
When the client is online, it periodically performs a sync with the server using HTTP, with this protocol:
1. HTTP GET /doc/:id/stateVector - returns the latest state vector on the central server. A little analogous for checking the refs of a git remote.
2. HTTP POST /doc/:id/push - send the difference between the state vector returned from (1) and the client’s local state to the server. This is kinda like `git push`.
3. The server responds to (2) with the missing state on the client. It can compute this from the data the client sent.
Inside the server, writes are handled like this:
1. Start a RW transaction on a Postgres row containing the CRDT data.
2. Read the data from the DB and merge it with the incoming data in the client post.
3. Compute the update delta for the client
4. Write the merged data back into the DB
5. Commit.
6. Respond with the delta computed in (3).
This scheme is not optimally efficient, but shows that you can put together a CRDT system with a normal database and a bit of HTTP.
Yep. I've done something very similar on top of Diamond Types for a little personal wiki. This page[1] is synced between all users who have the page open. Its a remarkably small piece of code, outside of the CRDT library itself (which is in rust via wasm). The way it works is:
- On page load, the server sends the whole CRDT document to the browser, and the server streams changes from that point onwards.
- When a change happens in the browser, it makes that change locally then and sends anything the server doesn't know about upstream.
- Whenever the server finds out about a new change, it re-broadcasts that change to any subscribed browser streams.
I'm using the braid HTTP protocol for changes - but we could easily switch to a SSE or websocket solution. It doesn't really matter.
At the moment I'm just using flat files for storage, but there's nothing stopping you using a database instead, except that its a bit awkward to use efficient CRDT packing techniques in a database.
> I'd love a quick perspective on how CRDTs integrate into real app infrastructure in the real world.
There just aren't a lot of good integrations with databases yet. There's nothing technical stopping that from happening - but I don't think we even know CRDTs could be fast enough to be practically useful until last year. So its going to take a few years before this stuff bleeds into databases.
A list CRDT like diamond types or Automerge is just a set of changes. Changes are like git commits - they have an ID, parents, and details of the change. Its like if you wanted to store a git commit per edit / per keystroke a user makes. The algorithms / mathematical guarantees don't care how you store and send that data set.
Aside from some clever algorithms, an (operation based) CRDT is essentially an append-only set of operations, that needs to be replicated between peers. There's nothing magic about them, and any infrastructure which can do that can support CRDTs.
So you could just store each change as an entry in a database table - and actually, that'd work fine in practice[1]. The problem is that you'd end up using up a lot of space for a document, because there are so many changes. Type a 10 page paper? That'll be 260k database entries! There are plenty of tricks to storing this information compactly. Simple RLE encoding decreases size by an order of magnitude. (Thanks to Martin Kleppmann for this work). But lots of compact binary representation tricks don't play that nice with modern databases. And you don't want to have to load & re-save the data each time you change something.
One long term answer would be to bake CRDT compaction techniques into databases themselves, but obviously this will be idiosyncratic. And there's lots of shorter term solutions, like using multiple tables + run-length encoding values. But I think the CRDT libraries themselves (Yjs, Automerge, Diamond Types, etc) need to mature some more. I don't see much point hacking up postgres when the set of fields & semantics are still (somewhat) in flux. (Well, yjs might be more ready for this work - but its probably still a good idea to wait until yjs's rust port (yrs) is done).
[1] It'd work fine the way my own CRDT (diamond types) is designed, since DT just needs to store the "origin positions" of each change. In Yjs and Automerge, changes store references to the ID of the item to the immediate left. In order to generate those changes, both automerge and yjs need to look up those persistent IDs - which in practice means they need to load the document state into memory first.
...but it's important to note that CRDTs as used by Redis (and as defined by, for example, https://arxiv.org/pdf/1806.10254.pdf) really just means that you've defined a way to consistently resolve conflicts - it doesn't necessarily mean the all-singing-all-dancing collaborative text editing that a lot of people seem to be assuming. For example, as defined in the paper above, simple "last write wins" is a CRDT.
Well, I should rephrase: I didn't know CRDTs could be fast enough to be practical. At least in the context of collaborative text editing. I've been in the field I was arguing against using CRDTs for over a decade.
I ran a bunch of optimization experiments 18 months ago[1] and realised I was wrong. I demonstrated (at least to my own satisfaction) that all the problems that CRDTs had (memory usage, disk usage, CPU usage) seemed to be finally solvable. Last year I put out a blog post[2] talking about how to get insane performance. Both of these posts were discussed in detail here on HN.
One result of this is that automerge (an excellent CRDT) has started optimizing for performance. I hear their performance on this benchmark has improved from 5 minutes to ~2 seconds. Yjs can do the same thing in 1 second. Diamond types (my own CRDT) is now about 6 times faster than it was when I wrote that post last year. I can run the same benchmark in ~10ms (0.01 second). Memory usage has dropped from automerge's 880mb, to 2mb in diamond types. And I think we can get it much lower. Disk size for the same data set has dropped from 4mb of JSON down to 100kb while still storing the full edit history. (And this is for a 100kb document!)
There's a lot of tricks involved, and plenty more in the tank. But in short, modern list / string CRDTs are now close to the speed of their equivalent native data structures. With a text CRDT in rust, more CPU gets wasted in wasm overhead than is spent maintaining the CRDT itself.
Thats not something we knew was even possible a year or two ago. So, thats whats changed.
We have a log processing system that performs entity resolution and merging of data. It does this without synchronization, out of order, in parallel.
What that means is we may get N records referring to the same 1 entity. We incrementally build that entity as records come in. Regardless of the ordering of the updates, we always end up with the same information in that entity.
In our case this is for security analytics. This lets us process data from arbitrary sources, with arbitrary ingest cadences, and build a single graph of entities and relationships. Every property has a merge strategy.
The exception to this is that we also have immutable properties. These are contract based - we store the first write and assume that the value will never change, and it's up to the client to only mark immutable data as immutable when that is correct.
How do CRDTs work with denormalized data, or more generally, data where some fields have invariants that must be maintained between them? Let's say we have an object with an array of strings and a count of that array:
{
"words": ["a", "b", "c"],
"count": 3
}
Now suppose users A and B edit the document so that one word is deleted, and two words are added, respectively:
What probably should happen now is that we reach the state where `words` is `["a", "b", "d", "e"]` and `count` is `4`, but is that really what would happen? That would require the CRDT to understand changes to `count` as increment/decrement operations, but aren't there also situations where you'd simply want one of the two values to win?
Yes, it would work if you implement the schema correctly: among the standard value types there are also counter types. See for example https://automerge.org/docs/types/counters/
I think with CRDT's you wouldn't just have an integer type. You would have to choose your int type acording to the operations you would want done for it. So it could be declared as a "register" crdt, in which case the semantics would be of set and override, or it could be declared as a counter, with semantics of increment/decrement.
the crux of crdts is for solutions that don't want to use central always online sync backends, have a need to merge deep history not just pick a winner of a global latest state but are ok with losing some conflicting detailed information stored inside your data.
- if you can just pick a global winner for the object/document after diverging histories/ conflicts its much simpler to just pick a latest win strategy especially for simple data
- if you cannot lose conflicting detail information, crdts will not solve your problems and you most probably need to implement application specific conflict resolution. an example is someone renaming a todo from "create invoice summary 2019" to "create latest invoice summary 2023" and at the same time someone else setting the "create invoice summary 2019" as done. a crdt can consistently merge the changes but you might end up with a todo that is set to done and is called "create latest invoice summary 2023" which is a wrong result
> Usually people find that there are simpler/better ways to get the job done.
CRDTs can do one magic trick no other technology solutions can do: They can let users collaboratively edit without needing a centralized server. And this works between users, between devices or between processes.
Its just, most of the time that doesn't matter. Servers are cheap, and big companies frankly seem to love it when we're dependent on their infrastructure.
I want CRDTs for more "personal computing". I want my data to be shared between my devices seamlessly. And I want that to keep working even when I can't contact google's computers. I also really like something about Git - which is the fact that my computers are first class citizens in a repository. Unlike Google Docs, if github ever goes down, I don't lose any data and I can still get my work done, and collaborate with my teammates.
All software should work like that. I'm fighting to make CRDTs work well so we can bring that future within reach.
To OP's point, which I secretly wished I had the courage to post when CRDTs came up a couple times last week: I've been seeing these posts, and your comment, for what feels like my entire 13 years on HN. (That can't be true. But I distinctly recall someone saying "if they can do text this fast hey'll be able to do arbitrary JSON soon!" at least 5 years ago)
At the beginning of that period I made and sold a startup on having a dead simple sync strategy, by myself, while I watched 3 funded competitors try to build sync, fail and go under. I had no funding and no CS education, I was a waiter.
I don't quite understand what's going on then because I swear I just read in one of those threads last week that even the best text automerger still needs manual intervention to avoid creating nonsense...which, to be honest, sounded off because I thought this whole thing was popularized by Google Docs? Wave? and it didn't have that issue
Can you link the other comment? Its hard to know what you're talking about without that context.
In any case, you can't use a text "automerger" to collaboratively edit JSON text. (Whats an "automerger"?)
To understand why, suppose we had an empty (JSON) list: []. Two users concurrently inserted two items, "a" and "b". If those inserts get translated into string inserts, you end up with a JSON string containing ["a""b"] - which is a syntax error!
The trick to making this work correctly is that the CRDT / OT system needs to understand that you're editing a list. Don't collaboratively edit the JSON string. Collaboratively edit a list then JSON.stringify the result.
> the best text automerger still needs manual intervention to avoid creating nonsense
AFAIK the best production-ready CRDT solution for text is Yjs [0], and in some specific cases, the result of the merge might be something unexpected (one example [1]). However, there is research-quality CRDT called Peritext [2] which is specifically designed to handle styled text in an intuitive way, so the merge is more predictable.
> Servers are cheap, and big companies frankly seem to love it when we're dependent on their infrastructure.
I've worked on a few different sync engines for mobile apps over the years, it's also a matter of user expectations in consumer apps. Users expect to seamlessly pick up where they left off on another device and the absence of an intermediary server makes that difficult.
I am. I guess thats something I really like about git + github - you get the best of both worlds. Github is fantastic, and you can use github all day long. But you don't depend on it. If github had an outage like atlassian is having at the moment, there's no downtime. There's no risk of data loss. You can keep working and if you need to you can always move to gitlab or self host.
The downside is you sort of have the worst of both worlds in terms of complexity. You need a working, performant CRDT and a working centralized server. But I think its a great model that we (as an industry) should explore more.
Yeah I agree it's great from that perspective but the business goals very rarely make it a priority given the added complexity. Even supporting offline-first style workflows is step up as you can tolerate more downtime on the backend as your app doesn't become completely useless.
Usually people find that there are simpler/better ways to get the job done.
Here's all the ways I know for dealing with write conflicts in a distributed systems:
- Last write wins, AKA I'm going to pretend I've solved the problem and just randomly lose data
- Record all versions of the data, pick an arbitrary winner, store a revision, and surface it to the user if there's a conflict (the CouchDB strategy, I don't know anything else using this)
- Model your data so it's append only (events, maybe) then merge just becomes a set union of all the different nodes[0]
So including CRDTs, that's my general taxonomy of solutions to this problem. Are there more?
[0] Working on a prototype for this right now, if anyone has done similar work I'd love to pick your brain, email in profile.
CRDTs don’t quite fit in this list, as these are all valid merge strategies that a CRDT can use. The fundamental idea of a CRDT is that all of these schemes have something in common: They’re based on iteratively merging a bunch of versions together with a merge operator that fulfills a few basic properties (where + means merge:
- Associativity: (A + B) + C == A + (B + C)
- Commutativity: A + B == B + A
- Idempotency: A + A = A
- Closure: Every merge result is a valid merge argument
- (perhaps a few others; it’s been a while since I read the original papers)
As long as your merge operator has all of these properties, you get the strong eventual consistency result that makes CRDTs a useful abstraction: Any two users that have received versions including the same updates will reconstruct the document in the same way, even if the actual set of versions seen differs.
That part is clear to me. What's not immediately obvious is that the DT in a CRDT can be entire database. Certainly the focus seems to be on individual data structures within a data store.
Might have to re-read the papers with this thought in mind. It did seem like there was a duality there but I kind of dismissed it.
A CRDT is still just a data structure. Most of my work lately has been on building an operation based CRDT for lists. Storage wise, you basically just store the set of all the operations you know about. (Though you can trim that set down if you want, like a shallow git clone).
You can store that set of operations just as easily in a file or in a database table. (Though the table will take a bit more space on disk because you can't be quite as clever).
To generate the data value itself, you can either replay all the operations from scratch (which is not as bad as it sounds), or store a cached copy of the data at some version and use that. You can think about it as, the operations give you history, tell you how to merge in remote changes and contain the data remote peers need to sync. The data itself is the value you read out & display.
There's nothing special about the data itself. You can store all that data in a flat file, or a database table, or in a few arrays in memory. The fancy part is (usually) how you merge changes together.
The conditions referenced in GP seem to imply that the most general state-based CRDT is simply a set of all reported outcomes. Something commutative, associative and idempotent is basically describing set union. So whether it's easy to go from one to the other would seem to depend on what constraints you're enforcing on your database.
Operation-based CRDT's are a bit different, these require commutativity and associativity. So you would be dealing with a multiset of reported operations.
Slightly pedantic, but I think you'll actually find that all of these are CRDTs. If you read "Conflict-free Replicated Data Types: An Overview" (https://arxiv.org/pdf/1806.10254.pdf), it refers to both last write wins and "record all versions of the data" as CRDTs in 2.1.1.
My understanding is that "CRDT" really just means that you've defined how you're going to resolve conflicts consistently (see criteria in s2.2 of the paper above).
Yeah it had occurred to me that what I'm doing was just a GSet writ large. In my mind a CRDT was always a single record, not a collection of them - still not convinced that doesn't change it.
But if CRDTs really are a kind of a meta-language for resolving conflicts in a sane way - whether the CRDT is a single record or a whole data store - that's a nice thought. The CouchDB CRDT could be defined.
The pessimist in my brain keeps waiting for someone to prove mathematically that they’re impossible. NP complete might be fine for many uses, since the branching factor for code for example is pretty low. But for some people NP complete is a magical force field that shuts off all rational thought.
That agrees with my intuition for the most part, although it might carve out some space for hope, in the sense that a paper about proof of A that doesn't mention !A means someone spent time in there and didn't find a bigger problem. The more times someone carves out a small amount of negative space in a problem domain, the higher the odds are that there's an asymptote that is nonzero.
What I'm more worried about is that we will prove that 'interesting text merges' are not all commutative. Though one trick we keep doing in spaces where things are proven impossible is that we cheat by creating tools that disallow or discourage the intractable scenarios. That could, for instance, lead to a generation of programming languages with a strange file structure that makes most workflows commutative.
That may sound strange or terrible to some people, but it's been my opinion for a number of years now that most of our coding conventions boil down to issues of avoiding merge conflicts, so a language that enforced a stronger form of conflict avoidance would be an 'opinionated' language but within the realm of the sorts of things we already put up with. Maybe no stranger than pure functional or Actor based languages, for instance.
The biggest bang is for de-centralization or edge based storage similar idea to @josephg (personal computing). Imagine taking multiple hours provisioning your personal compute with all your data and stream the diff across your devices and having an internet that runs off your devices. This would finally allow for a multi device local-first experience (https://www.inkandswitch.com/local-first/) where your phone streams the changes to your data while you were on your flight to your desktop and at a bigger scale entire facebook/google data store is a streaming system with auto-merge instead of a focus on ACID DB structures and coherence.
Hands down Martin Kleppmann (Author of data intensive applications) is the biggest fan of CRDTs and this page has a lot of great info https://martin.kleppmann.com/2020/07/06/crdt-hard-parts-hydr...