-
Notifications
You must be signed in to change notification settings - Fork 0
Transaction Handling
Every graph operation in Titan occurs within the context of a transaction. According to the Blueprints’ specification, each thread opens its own transaction against the graph database with the first operation (i.e. retrieval or mutation) on the graph.
TitanGraph g = TitanFactory.open("/tmp/titan");
Vertex juno = g.addVertex(null); //Automatically opens a new transaction
juno.setProperty("name", "juno");
g.commit(); //Commits transaction
In this example, a local Titan graph database is opened. Adding the vertex “juno” is the first operation (in this thread) which automatically opens a new transaction. All subsequent operations occur in the context of that same transaction until the transaction is explicitly stopped or the graph database shutdown()
which commits all currently running transactions.
All graph elements (vertices, edges, and types) are associated with the transactional scope in which they were retrieved or created. Under Blueprint’s default transactional semantics, transactions are automatically created with the first operation on the graph and closed explicitly using commit()
or rollback()
. Once the transaction is closed, all graph elements associated with that transaction become stale and unavailable. However, Titan will automatically transition vertices and types into the new transactional scope as shown in this example:
TitanGraph g = TitanFactory.open("/tmp/titan");
Vertex juno = g.addVertex(null); //Automatically opens a new transaction
g.commit(); //Ends transaction
juno.setProperty("name", "juno"); //Vertex is automatically transitioned
Edges, on the other hand, are not automatically transitioned and cannot be accessed outside their original transaction. They must be explicitly transitioned.
Edge e = juno.addEdge("knows",g.addVertex(null));
g.commit(); //Ends transaction
e = g.getEdge(e); //Need to refresh edge
e.setProperty("time", 99);
When committing a transaction, Titan will attempt to persist all changes to the storage backend. This might not always be successful due to IO exceptions, network errors, machine crashes or resource unavailability. Hence, transactions can fail. In fact, transactions will eventually fail in sufficiently large systems. Therefore, we highly recommend that your code expects and accommodates such failures.
try {
if (g.getVertices("name",name).iterator().hasNext())
throw new IllegalArgumentException("Username already taken: " + name);
Vertex user = g.addVertex(null);
user.setProperty("name", name);
g.commit();
} catch (TitanException e) {
//Recover, retry, or return error message
}
The example above demonstrates a simplified user signup implementation where name
is the name of the user who wishes to register. First, it is checked whether a user with that name already exists. If not, a new user vertex is created and the name assigned. Finally, the transaction is committed.
If the transaction fails, a TitanException
is thrown. There are a variety of reasons why a transaction may fail. Titan differentiates between potentially temporary and permanent failures.
Potentially temporary failures are those related to resource unavailability and IO hickups (e.g. network timeouts). Titan automatically tries to recover from temporary failures by retrying to persist the transactional state after some delay. The number of retry attempts and the retry delay can be configured through the Titan graph configuration.
Permanent failures can be caused by complete connection loss, hardware failure or lock contention. To understand the cause of lock contention, consider the signup example above and suppose a user tries to signup with username “juno”. That username may still be available at the beginning of the transaction but by the time the transaction is committed, another user might have concurrently registered with “juno” as well and that transaction holds the lock on the username therefore causing the other transaction to fail. It some cases one can recover from a lock contention failure by re-running the entire transaction.
- Transactions are started automatically with the first operation executed against the graph. One does NOT have to start a transaction manually. The method
newTransaction
is used to start multi threaded transactions only.
- Transactions are automatically started under the Blueprints semantics but not automatically terminated. Transactions have to be terminated manually with
g.commit()
if successful org.rollback()
if not. Manual termination of transactions is necessary because only the user knows the transactional boundary.
A transaction will attempt to maintain its state from the beginning of the transaction. This might lead to unexpected behavior in multi-threaded applications as illustrated in the following artificial example:
v = g.v(4) //Retrieve vertex, first action automatically starts transaction
v.bothE
>> returns nothing, v has no edges
//thread is idle for a few seconds, another thread adds edges to v
v.bothE
>> still returns nothing because the transactional state from the beginning is maintained
Such unexpected behavior is likely to occur in client-server applications where the server maintains multiple threads to answer client requests. It is therefore important to terminate the transaction after a unit of work (e.g. code snippet, query, etc). For instance, Rexster Graph Server manages the transactional boundary for each gremlin query. So, the example above should be:
v = g.v(4) //Retrieve vertex, first action automatically starts transaction
v.bothE
g.commit()
//thread is idle for a few seconds, another thread adds edges to v
v.bothE
>> returns the newly added edge
g.commit()
- Read more about Blueprints Transactions
- Learn how to speed up transactions with multiple threads