Transactions
This page discusses transactions in Stardog.
Page Contents
ACID Transactions
This section discusses Stardog’s transactional semantics and guarantees. Generally speaking, Stardog supports ACID transactions. A good overview of transaction isolation in the context of J2EE is this beginner’s guide.
Atomicity
Databases may guarantee atomicity – groups of database actions (i.e. mutations) are irreducible and indivisible: either all the changes happen or none of them happens. Stardog’s transacted writes are atomic.
Stardog does not support nested transactions.
Consistency
Data stored should be valid according to the data model (in this case, RDF) and to the guarantees offered by the database, as well as to any application-specific integrity constraints that may exist. Stardog’s transactions are guaranteed not to violate integrity constraints during execution. A transaction that would leave a database in an inconsistent or invalid state is aborted.
See the Data Quality Constraints section for a more detailed consideration of Stardog’s integrity constraint mechanism.
Isolation
A Stardog connection will run in SNAPSHOT isolation level if it has not started an explicit transaction. The isolation level can be set to SNAPSHOT or SERIALIZABLE with the database configuration option transaction.isolation. In either of these modes, uncommitted changes will only be visible to the connection that made the changes: no other connection can see those values before they are committed. Thus, “dirty reads” can never occur. Additionally, a transaction will only see changes which were committed before the transaction began, so there are no “non-repeatable reads”.
SNAPSHOT isolation does suffer from the write skew anomaly, which poses a problem when operating under external logical constraints. We illustrate this with the following example, where the database initially has two triples :a :val 100 and :b :val 50, and the application imposes the constraint that the total can never be less than 0.
Example of write-skew anomaly:
| Time | Connection 1 | Connection 2 | Connection 3 |
|---|---|---|---|
0 | *BEGIN TX* | *BEGIN TX* | |
1 | SELECT ?val {:a :val ?val} <= 100 | SELECT ?val {:b :val ?val} <= 50 | |
2 | INSERT {:a :val 0} | INSERT {:b :val 0} | |
3 | *COMMIT* | *COMMIT* | |
4 | *BEGIN TX* | ||
5 | SELECT ?val {?a :val ?val} <= 0 | ||
6 | SELECT ?val {?b :val ?val} <= 0 |
At the end of this scenario, Connection 1 believes the state of the database to be :a :val 0 and :b :val 50, so the constraint is not violated. Similarly, Connection 2 believes the state of the database to be :a :val 100 and :b :val 0, which also does not violate the constraint. However, Connection 3 sees :a :val 0 and :b :val 0, which violates the logical constraint.
No locks are taken for concurrent transactions at the SNAPSHOT isolation level. Whether conflicting writes are detected depends on the database’s transaction.write.conflict.strategy option, which is fixed at creation time:
LAST_COMMIT_WINS(the default) performs no conflict detection. Every writer commits, and for a given statement the transaction with the highest commit timestamp (functionally, the one which committed “last”) determines whether that statement is present.ABORT_ON_CONFLICTdetects conflicts per statement and aborts the losing transaction withOperation aborted: Write Conflict detected(error codeQEWCE2). A write to a statement conflicts if another transaction also wrote that statement and either is still open, or committed after this transaction began. In the second case the other transaction’s write is absent from this transaction’s snapshot yet collides with it. A transaction that rolled back never causes a conflict. Once a conflict is detected the transaction is cancelled and can no longer be committed: a subsequentCOMMITfails rather than persisting the writes the transaction had already made. Retrying therefore means beginning a new transaction and replaying the work, not re-issuing the failed statement inside the same one. Databases created with edge properties enabled always use this strategy.
Either way, results may be unexpected, since every transaction reads from a snapshot created when it began.
Consider the following query being executed by two concurrent threads at the SNAPSHOT isolation level against a database having the triple :counter :val 1 initially:
INSERT {
:counter :val ?newValue
}
DELETE {
:counter :val ?oldValue
}
WHERE {
:counter :val ?oldValue
BIND (?oldValue+1 AS ?newValue)
}
Since each transaction will read the current value from its snapshot, it is possible that both transactions will read the value 1 and insert the value 2 even though we expect the final value to be 3.
Under LAST_COMMIT_WINS a read-modify-write of this shape can also leave both new values in the database, not just one. Because both transactions computed the same ?newValue above, this is not visible in that example; it becomes visible as soon as the two transactions write different values. Suppose one transaction adds 1 and a concurrent one adds 10, both reading :counter :val 1 from their snapshots:
| Time | Connection 1 | Connection 2 |
|---|---|---|
0 | *BEGIN TX* | *BEGIN TX* |
1 | DELETE {:counter :val 1} | DELETE {:counter :val 1} |
2 | INSERT {:counter :val 2} | INSERT {:counter :val 11} |
3 | *COMMIT* | *COMMIT* |
Both deletions agree that :counter :val 1 should be gone, so it is. But the two insertions are of different statements, and neither transaction deleted the other’s, so both survive: the database ends up holding :counter :val 2 and :counter :val 11. An application that assumes :counter has a single :val will now read two. This is the practical reason to reach for one of the remedies below whenever an update’s correctness depends on a value it just read.
The SERIALIZABLE isolation level can be used to avoid all of the situations above, including the write skew anomaly. In SERIALIZABLE mode, an exclusive lock needs to be acquired before a transaction begins. This ensures concurrent updates cannot interfere with each other. However, update throughput will decrease since only one transaction can run at a time.
The ABORT_ON_CONFLICT write conflict strategy is the cheaper option for the two counter examples specifically: it lets transactions run concurrently and fails the losing one, which the client can retry. In both of those examples the two transactions write the statement :counter :val 1, so the second one to reach it is aborted rather than allowed to add a second value. It does not address the write skew anomaly, however, since there the two transactions write different statements and so never conflict. Only SERIALIZABLE prevents that.
Durability
By default, Stardog’s transacted writes are durable, and no other actions are required.
Begin Transaction
To begin a transaction:
-
Use the
tx begincommand:$ stardog tx begin myDatabase 65e93faa-b26a-43ab-b39d-c5392eb93859A transaction id (e.g.
65e93faa-b26a-43ab-b39d-c5392eb93859) will be returned. This id can be provided to many CLI commands to indicate that the operation should take place within the transaction. For example, see the--txoption forquery execute. curl -u username:password -X POST http://localhost:5820/myDatabase/transaction/beginOutput :
d2c91a85-5b33-4993-8b67-7d95db815377A transaction id is returned which can then be provided to multiple different operations. For example, see the API to generate a SHACL a report within a transaction.
try (Connection aConn = ConnectionConfiguration .to("myDatabase") // the name of the db to connect to .server("http://localhost:5820") //server url .credentials("admin", "admin") // credentials to use while connecting .connect()) { // begin transaction aConn.begin(); // perform other operations and eventually commit/rollback the current transaction }It is required to begin a new transaction before making any updates through the connection. See
com.complexible.stardog.api.Connection#beginfor more information.
Commit Transaction
To commit a transaction:
-
Provide the transaction id to the
tx commitcommand to be committed:stardog tx commit myDatabase <tx-id> -
Provide the transaction id for the transaction to be committed.
curl -u username:password -X POST http://localhost:5820/myDatabase/transaction/commit/{txid}See the HTTP API for more information.
try (Connection aConn = ConnectionConfiguration .to("myDatabase") // the name of the db to connect to .server("http://localhost:5820") //server url .credentials("admin", "admin") // credentials to use while connecting .connect()) { // begin transaction aConn.begin(); //create some data and add to a named graph Collection<Statement> aGraph = Collections.singleton( Values.statement(Values.iri("urn:subj"), Values.iri("urn:pred"), Values.iri("urn:obj"))); Resource aContext = Values.iri("urn:graph"); aConn.add().graph(aGraph, aContext); //commit the current transaction aConn.commit() }See
com.complexible.stardog.api.Connection#commitfor more information.
Rollback Transaction
To rollback a transaction:
-
Provide the transaction id to the
tx rollbackcommand to be committed:stardog tx rollback myDatabase <tx-id> -
Provide the transaction id for the transaction to be rolled back.
curl -u username:password -X POST http://localhost:5820/myDatabase/transaction/rollback/{txid}See the HTTP API for more information.
try (Connection aConn = ConnectionConfiguration .to("myDatabase") // the name of the db to connect to .server("http://localhost:5820") //server url .credentials("admin", "admin") // credentials to use while connecting .connect()) { // begin transaction aConn.begin(); //create some data and add to a named graph Collection<Statement> aGraph = Collections.singleton( Values.statement(Values.iri("urn:subj"), Values.iri("urn:pred"), Values.iri("urn:obj"))); Resource aContext = Values.iri("urn:graph"); aConn.add().graph(aGraph, aContext); //rollback the current transaction aConn.rollback() }See
com.complexible.stardog.api.Connection#rollbackfor more information.
List All Open Transactions on a Database
Only superusers can see all open transactions on a database. If a non-superuser attempts to list all open transactions, they will only see the ones they have opened. To list all open transactions on a database:
stardog tx list myDatabaseSample Output:
+--------------------------------------+-------+-------------------------------+ | Tx ID | User | Start Time | +--------------------------------------+-------+-------------------------------+ | 4923b094-e1c4-4804-8e3a-d0e8e36134c8 | admin | 2021-07-21T16:03:24.894-04:00 | +--------------------------------------+-------+-------------------------------+ 1 transaction(s)curl -u username:password -X GET http://localhost:5820/myDatabase/transactionSee the HTTP API for more information.
try (Connection aConn = ConnectionConfiguration .to("myDatabase") // the name of the db to connect to .server("http://localhost:5820") //server url .credentials("admin", "admin") // credentials to use while connecting .connect()) { for (TxInfo tx : aConn.transactions()){ System.out.println("TX ID: " + tx.getID()); System.out.println("USER: " + tx.getUser()); System.out.println("START TIME: " + tx.getStartTime()); } }See
com.complexible.stardog.api.Connection#transactionsfor more information.
Transaction Logging
Stardog can optionally log all transactions to disk, enabling point-in-time recovery and transaction inspection. When enabled via the transaction.logging database option, all committed transactions are recorded in a sequential log that can be exported and replayed.
See Transaction Logs for details on enabling, inspecting, and replaying transaction logs.