Distributed Data Structures over a Shared Log
A system for building highly available metadata services where the key abstraction is a Tango object, a class of in-memory data structures built over a durable, fault-tolerant shared log

Tango can be viewed as a synthesis of SMR, log-structured storage, and history-based systems
Its design is enabled by the existence of fast, decentralized shared log implementations that can scale to millions of appends and reads per second; the papers implementation runs over a modified version of CORFU
OIDs and versions
- An OID is a unique integer for each Tango object. The runtime uses it to tag the object’s entries in the shared log, so many objects can share one log
- A version is the last log offset that modified the object
- As for versioning granularity, one version per object is fine for registers and counters, but for maps and trees it could abort transactions that touch unrelated parts
- In these cases, a structure can provide its own scheme and upcalls to check and update versions
Anatomy of a Tango object
- A Tango object is an in-memory data structure (the view) whose only source of truth is the shared log. It has three parts
- the view: ordinary in-memory state, like a map, a list, or a register’s one integer
- an
applyupcall, which the Tango runtime calls with new log entries. The view may only change throughapply, never from application threads - mutators and accessors, which are the object’s external interface (
get/put,read/write) - The runtime gives the object two calls
updateHelper(record, oid)appends an opaque update record to the logqueryHelper(oid)plays new log entries up to the current tail, callingapplyon each
- By deriving views from the log, it inherits a few properties for free:
- Consistency: all writes go through the shared history and synchronize on reads
- Durability: clients can recover views after crashes by playing back the history in the shared log
- History: previous state can be accessed by instantiating a new view from a prefix of the history
- Elasticity: the aggregate throughput of linearizable reads can be scaled simply by adding new views
class TangoRegister {
private state = 0 // the view
constructor(private oid: number, private runtime: TangoRuntime) {}
// upcall: only the runtime calls this, with entries read from the log
// all records are just bytes
apply(record: Uint8Array) {
this.state = decodeInt(record)
}
// mutator: append an update record, don't touch the view
write(value: number) {
this.runtime.updateHelper(encodeInt(value), this.oid)
}
// accessor: catch up to the log tail, then read the view
read(): number {
this.runtime.queryHelper(this.oid)
return this.state
}
}Transactions
- A transaction is optimistic: it runs without locks and is validated at commit
beginTXandendTXbracket ordinary accessor and mutator calls- While it runs
beginTXcreates a transaction context in thread-local storagequeryHelpernow just adds the object’s(oid, version)to the read setupdateHelpernow just buffers writes and nothing reaches the log
endTXflushes the buffered writes to the log as speculative entries and appends a commit record carrying the read set- Clients replaying these skip speculative entries.
- When any client’s replay reaches a commit record, it compares each version in the read set with the object’s current version at that point in the log
- none changed: commit. The speculative writes are applied through each object’s
applyupcall - any changed: abort. The speculative writes are never applied
- none changed: commit. The speculative writes are applied through each object’s
Streams
- A stream is the part of the shared log that belongs to one object (identified by its OID). A client only hosts some objects, so it should only have to read those streams and not the whole log
- That needs a way to find a stream’s next entry without reading the entries in between.
readnextreturns it, andmultiappendputs one entry on several streams - CORFU does this with backpointers: each entry’s header lists the previous K entries of every stream it belongs to. They point backward because log pages are write-once, so an old entry can’t be updated with a forward pointer, and the sequencer already knows each stream’s last K offsets when it hands out the next position
offset 0 1 2 3 4 5 6 7
stream A A B A A B B A
▲───▲───────────┘ entry 7 points to 4 and 3
▲───────▲───┘ entry 4 points to 3 and 1
▲───▲───────┘ entry 3 points to 1 and 0
▲───┘ entry 1 points to 0- Stream A above is entries 0, 1, 3, 4 and 7, with K = 2. Each row starts at an entry and the arrows point at the entries its backpointers refer to. To catch up, a client asks the sequencer for the stream’s last entry and walks backward: read entry 7, then jump to entry 3 (its Kth pointer), and it has the whole list after two reads instead of four. In general N entries cost about N/K reads. It then keeps the offsets in memory, and
readnextwalks that list forward- K > 1 also keeps the chain alive when an entry is junk, since a hole filled with junk has no backpointers
- A transaction across several objects multiappends its commit record to all their streams, so a client hosting any of them sees it