Skip to content

Transactions#

By default, every Concourse operation runs in autocommit mode — each individual read or write is immediately committed. For operations that must succeed or fail as a group, Concourse provides full ACID transactions with serializable isolation.

Staging a Transaction#

Call stage to begin a transaction. All subsequent operations are buffered and not visible to other clients until you commit.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Java
concourse.stage();
try {
    concourse.set("balance", 90, 1);
    concourse.set("balance", 110, 2);
    concourse.commit();
}
catch (TransactionException e) {
    concourse.abort();
}
1
2
3
4
5
// CaSH
stage
set "balance", 90, 1
set "balance", 110, 2
commit

Lambda Syntax#

For convenience, you can pass a lambda to stage. The transaction is automatically committed if the lambda completes without exception, or aborted if an exception is thrown.

1
2
3
4
5
// Java
concourse.stage(() -> {
    concourse.set("balance", 90, 1);
    concourse.set("balance", 110, 2);
});

Committing and Aborting#

commit#

The commit method attempts to apply all staged operations atomically. It returns true if the commit succeeds, or false if a conflict is detected. A failed commit automatically aborts the transaction.

1
2
// Java
boolean success = concourse.commit();

abort#

The abort method discards all staged operations without applying them. Always abort a transaction if an error occurs during staging.

1
2
// Java
concourse.abort();

Error Handling Pattern#

Always wrap transactions in a try/catch block to ensure proper cleanup:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Java
concourse.stage();
try {
    // Perform operations
    concourse.add("key", "value", record);
    boolean success = concourse.commit();
    if(!success) {
        // Handle conflict
    }
}
catch (TransactionException e) {
    concourse.abort();
}

Serializable Isolation#

Concourse transactions provide serializable isolation, the strongest isolation level. Each transaction behaves as if it were the only operation running against the database, even when many transactions execute concurrently.

Specifically, Concourse guarantees:

  • Atomicity: All operations in a transaction succeed together or fail together. There is no partial application.
  • Consistency: The database always transitions from one valid state to another.
  • Isolation: Concurrent transactions do not interfere with each other. Staged operations are invisible to other clients.
  • Durability: Once committed, data survives server restarts.

If two concurrent transactions conflict (e.g., both try to modify the same field), one will succeed and the other will fail at commit time. The failed transaction must be retried.

Just-in-Time Locking#

Concourse uses a just-in-time (JIT) locking strategy to maximize concurrency. Instead of acquiring all locks at the start of a transaction, locks are acquired only when each operation executes. This means:

  • Transactions that touch different data never conflict.
  • Lock contention is minimized because locks are held for the shortest possible duration.
  • The system automatically detects conflicts at commit time and fails one of the conflicting transactions cleanly.

From a user’s perspective, JIT locking is transparent. You write transaction code normally, and Concourse handles conflict detection and resolution automatically.

Atomic Operations#

In addition to full transactions, Concourse provides several operations that execute atomically without requiring an explicit stage/commit cycle. These are useful when you need atomicity for a single compound operation.

verifyAndSwap#

Atomically replace a value if and only if the expected value is currently stored. This is the classic compare-and-swap (CAS) pattern.

1
2
3
// Java
boolean swapped = concourse.verifyAndSwap(
    "balance", 100, 1, 90);

verifyOrSet#

Atomically verify that a field contains exactly one specific value, or set it. Unlike set, this creates no revisions if the field already contains only the specified value.

1
2
// Java
concourse.verifyOrSet("status", "active", 1);

findOrAdd / findOrInsert#

Atomically find a unique record matching a condition, or create one if none exists. See Writing Data for details.

reconcile#

Atomically synchronize a field to contain exactly the specified collection of values, computing and applying the minimal diff.

1
2
3
// Java
concourse.reconcile("tags", 1,
    Lists.newArrayList("v1", "stable", "release"));

Transactions in CaSH#

The Concourse Shell supports transactions via commands and via prepare mode.

Direct Commands#

1
2
3
4
5
// CaSH
stage
add "name", "Jeff", 1
add "age", 30, 1
commit

If something goes wrong, use abort to discard:

1
2
3
4
// CaSH
stage
add "name", "Jeff", 1
abort

Prepare Mode#

CaSH offers a prepare mode that queues commands for batch submission. Enter prepare mode with :prepare, queue your commands, then submit them all at once with :submit or discard with :discard.

1
2
3
4
5
6
7
// CaSH
:prepare
stage
add "name", "Jeff", 1
add "age", 30, 1
commit
:submit

Prepare mode validates transaction semantics as you queue commands, warning you about issues like unmatched commit without a preceding stage.

See Concourse Shell for more details on prepare mode.