Advanced Topics
As TCStore API is a storage API targeted for distributed datasets, the picture wouldn't be complete without addressing ...
Consistency
Durability
Atomicity guarantees
Asynchronicity
Visibility and Durability Guarantees
The following section discusses consistency and durability guaranteed provided by TCStore API and highlights their flexibility.
Mutating Operations
When persisting data to a cluster, we want to be able to control how fast the update needs to be available to other participants (readers or writers). Another aspect is how durable we want this write to be in case of failure.
TCStore API lets you express the first concern using WriteVisibility encapsulated in a WriteSettings. For the second one, TCStore will always give you strong durability.
Visibility
The visibility comes in two distinct settings:
EVENTUAL | Writes are ... ... eventually visible to ROUTINE readers ...immediately visible to DEFINITIVE readers See "Read Operations" below |
IMMEDIATE | Writes are immediately visible to all readers, regardless of reader setting. |
Concretely, depending on the use case where the data is used, you may want to raise or lower the visibility guarantees of your mutative operation.
For example, decreasing or increasing an availableStock counter may be more or less important to be propagated depending on the new value (e.g. if the value reaches 0, meaning that the stock has become unavailable).
Failure Tolerance
TCStore API will always aim for maximum durability. The exact meaning of this depends on the configuration of the store itself:
For non-persistent, non-replicated stores, it means the data made it to the server.
For persistent, non-replicated stores, it means the data made it to the disk of the server.
For persistent, replicated stores, it means the data made it to the disk of the servers and all replicas.
Read Operations
Read operations are simpler, as they only need to express the Visibility expectations that a given read operation has:
ROUTINE | Provides standard visibility as dictated by the write operation |
DEFINITIVE | Provides immediate visibility of previous writes regardless of their write mode |
Again, depending on the use case, a user could use ROUTINE to read the current availableStock for listing items currently available matching a given user query, but would probably want to raise the visibility expectations to DEFINITIVE when actually fulfilling an order.
Atomicity
Beside the previously discussed read and write settings, TCStore API makes a Record the atomic unit of work.
So when you build a Stream<Record<?>> where you filter and then mutate (a bulk update), it will make sure you never mutate a Record<?> that doesn’t pass that Predicate you used to filter, even if some other writer mutated the given Record<?> concurrently.
Also, if you mutate two Cell instances of one Record, depending on the write and read settings either both new or old values would be observed, but a reader would never be able observe one old and one new value for that given write.
The underlying principle is that a Record instances are effectively immutable.
Asynchronicity
As mentioned previously, TCStore API is targeted at distributed deployments. As such, it is inherently asynchronous. Although all the API examples we used in this document are synchronous, TCStore exposes an asynchronous API for you to use:
Asynchronous Operations
AsyncDatasetWriterReader<String> asyncAccess =
counterAccess.async(); // <1>
Operation<Boolean> addOp =
asyncAccess.add("counter10", counterCell.newCell(10L)); // <2>
Operation<Optional<Record<String>>> getOp =
addOp.thenCompose((b) -> asyncAccess.get("counter10")); // <3>
Operation<Void> acceptOp = getOp.thenAccept(or -> or.ifPresent( // <4>
r -> System.out.println("The record with key " + r.getKey() +
" was added")));
try {
acceptOp.get(); // <5>
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
1. Retrieves an asynchronous accessor to the Dataset in the form of AsyncDatasetWriterReader.
2. Schedule a write a new Record, identified by the key counter10 with an initial value of counterCell as 10. This returns an Operation instance which can then be waited upon (as a java.util.concurrent.Future) or combined with other operations (as a java.util.concurrent.CompletionStage).
3. Chain a get, to be performed once the write is complete, to the write operation.
This does NOT affect the write nor does it combine the write and the get into an atomic unit.
The result of this combination is also an Operation instance.
4. To the get Operation instance, chain a Consumer that processes the Optional<Record<String>> returned from the get. The result of this chain construction returns an Operation<Void>. Consumer returns no result.
5. As with addOp and getOp, the acceptOp Operation is a handle to an operation scheduled for background completion. This operation sequence will complete on its own but, to await completion, one of the Future.get methods must be called.
The default API is synchronous, as it is probably more obvious for everyone to get started with. But the power of the asynchronous API is exposed as well.
Note: When using the asynchronous API, operation sequencing is not maintained. If, in the example above, the get was not chained to the write, the get could be executed before the write even though the Operation instances were created in the opposite order. Mind, that two asynchronous operations are independent.