This document describes how to use Ledger for synchronized application storage in a client application. The goal of the document is to explain the key concepts and the most common operations, not necessarily to cover the entire API surface.
The Ledger API is exposed through a FIDL interface.
Access to Ledger is vended from the fuchsia::modular::ComponentContext
of the running application. Each application component has a separate private data store for each user on behalf of which it runs.
Data stored in Ledger is grouped into separate independent key-value stores called pages.
Pages are identified by ids of 16 bytes each. When creating a page, the application can pick its id (for example, to create/access a page with a name that is fixed and known in advance), or can request a new unique id to be generated by Ledger (for example, to create pages that correspond 1-1 to business logic objects). Ledger also offers a convenience method GetRootPage()
which retrieves the page with id [0, 0 ... , 0]
.
Each page exposes a key-value store API.
The state of each page changes in commits which form a DAG. This DAG is not directly exposed to the developer through the Ledger API, but we mention it as it helps to understand how the page operations described below behave.
Values can be written/modified using mutation methods of the Page interface.
There are two ways to write a value into a page:
Put()
and PutWithPriority()
methods). This is handy, but note that the developer needs to ensure that the value does not exceed the maximum payload size of a FIDL message.CreateReferenceFromSocket()
, CreateReferenceFromBuffer()
and PutReference()
. Note that the created reference is temporary and not persisted through app restarts - it must be set through PutReference()
, or discarded.In order to ensure that reads across multiple entries (key-value pairs) are consistent, read operations are exposed through a snapshot API. Snapshot of the page can be obtained:
GetSnapshot()
), yielding a snapshot corresponding to the state of the page tracked by this page connectionOnce the snapshot is obtained, data can be read using the GetEntries()
method which retrieves multiple entries, or the Get()
method which retrieves the value associated with the particular key.
The GetEntries()
method takes a key_start
argument, allowing the app to perform a range query. In order to retrieve all entries between two keys, we need to call GetEntries()
with the first key passed as key_start
, and continue reading the paginated response as long as the returned values are in the desired range.
PutWithPriority()
and PutReference()
methods allow the app to write a value as a lazy value. Compared with the default eager values, lazy values are not guaranteed to be proactively fetched to the local device when the Page syncs to the state that contains them. The keys of the lazy values are always synced eagerly, but the value itself might be missing.
To ensure predictable performance of read operations, Get()
and GetEntries
methods never retrieve the missing lazy values from the network - these must be explicitly requested through a dedicated Fetch()
method.
The client application can register a watcher to be notified of changes to the state tracked by the local page connection. As typically we are interested in retrieving the initial base state at the moment of registering the watcher, the watchers are registered using the GetSnapshot()
method.
C++ watcher example, Dart watcher example.
GetSnapshot()
takes an optional key_prefix
parameter, which allows the client app to register specifically for change notifications within a particular prefix of keys.
Transactions allow the application to make a set of changes that are guaranteed to be synced and surfaced atomically. A client application would typically:
GetSnapshot()
or a watcher (see below), and read the dataOnce a transaction is started, the state of the page tracked on the page connection is pinned and won't advance until the transaction is either committed or aborted - this ensures that the transaction writes affect precisely the state visible on the page snapshot.
There can be only one transaction in progress per Page
connection, but clients can create multiple connections to the same page. Each of these connections can run a transaction independently from the others.
Watch and transactions: As starting a transaction pins the state of the page visible on the particular page connection, the client app won't receive any watch notifications (for watchers registered on this page connection) while the transaction is in progress.
Conversely, on a page connection with registered watchers, a StartTransaction()
call will only return when the app finishes processing all pending change notification. This ensures the app knows the base state of the page when performing a transaction.
If the page history has diverged and has more than one head, conflict resolution will be invoked to merge the heads.
By default, a key-by-key last-one-wins policy is used to merge divergent commits, based on best-effort, not-guaranteed-to-be-right, timestamps of each commit.
The client might opt for a different merge policy by implementing the ConflictResolverFactory
interface and setting it through a Ledger connection.
Available conflict resolution policies:
last-one-wins (the default): LAST_ONE_WINS
For each entry modified on both sides, LAST_ONE_WINS
takes the entry from the most recent commit; for each entry modified only on one side (e.g.: when adding a new key, or when modifying an existing one), the modification is carried over. The most recent commit is determined using timestamps assigned to the commits by the devices that created them. Note, however that these timestamps are not guaranteed to be accurate.
custom, invoked only for conflicting (modified on both branches being merged) entries: AUTOMATIC_WITH_FALLBACK
For all modified keys, if either they are modified only on one side, or changed in the same way (added/updated with the same value, or deleted) in both sides, AUTOMATIC_WITH_FALLBACK
applies the change automatically. If, however any key is modified on both sides with a non identical change, the app gets a callback with all changed entries from both sides (not just the conflicting ones).
custom, invoked for all merges (even if the entries modified by each branch are non-conflicting): CUSTOM
Conflicting changes are ignored and there are no merges performed.
See also Conflict Resolution.
Synchronization is configured at the Ledger level as described in the user guide. Once configured, the Ledger will upload local changes, and download changes made by other devices. These operations will happen automatically in the background, and clients do not have to manage them.