Link Search Menu Expand Document
Start for Free

Transactions

This page discusses transactions in Stardog.

Page Contents
  1. ACID Transactions
    1. Atomicity
    2. Consistency
    3. Isolation
    4. Durability
  2. Begin Transaction
  3. Commit Transaction
  4. Rollback Transaction
  5. List All Open Transactions on a Database
    1. Transaction Logging

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_CONFLICT detects conflicts per statement and aborts the losing transaction with Operation aborted: Write Conflict detected (error code QEWCE2). 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 subsequent COMMIT fails 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 begin command:

    $ stardog tx begin myDatabase
    65e93faa-b26a-43ab-b39d-c5392eb93859
    

    A 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 --tx option for query execute.

  • curl -u username:password -X POST http://localhost:5820/myDatabase/transaction/begin
    

    Output :

    d2c91a85-5b33-4993-8b67-7d95db815377
    

    A 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#begin for more information.

Commit Transaction

To commit a transaction:

  • Provide the transaction id to the tx commit command 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#commit for more information.

Rollback Transaction

To rollback a transaction:

  • Provide the transaction id to the tx rollback command 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#rollback for 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 myDatabase
    

    Sample 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/transaction
    

    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()) {
    
            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#transactions for 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.