Skip to content

Usage

Builder parameters

Parameter Type Required Description
clientId String Yes Unique identifier for the calling service. Lock IDs are scoped as clientId#lockId, so different clients can lock the same entity independently.
farmId String Yes Data center / farm identifier. Used in key construction for DC-level locks.
lockBase LockBase Yes The lock engine — wraps the storage backend and lock mode.

Initialize lock manager

DistributedLockManager lockManager = DistributedLockManager.builder()
    .clientId("CLIENT_ID")
    .farmId("FA1")
    .lockBase(LockBase.builder()
        .mode(LockMode.EXCLUSIVE)
        .lockStore(AerospikeStore.builder()
            .aerospikeClient(aerospikeClient)
            .namespace("NAMESPACE")
            .setSuffix("distributed_lock")
            .build())
        .build())
    .build();

lockManager.initialize();
DistributedLockManager lockManager = DistributedLockManager.builder()
    .clientId("CLIENT_ID")
    .farmId("FA1")
    .lockBase(LockBase.builder()
        .mode(LockMode.EXCLUSIVE)
        .lockStore(HBaseStore.builder()
            .connection(connection)
            .tableName("table_name")
            .build())
        .build())
    .build();

lockManager.initialize();
LockConfiguration config = LockConfiguration.builder()
    .lockTtl(Duration.ofSeconds(30))
    .waitForLock(Duration.ofSeconds(10))
    .sleepBetweenRetries(Duration.ofMillis(500))
    .build();

DistributedLockManager lockManager = DistributedLockManager.builder()
    .clientId("CLIENT_ID")
    .farmId("FA1")
    .lockBase(LockBase.builder()
        .mode(LockMode.EXCLUSIVE)
        .lockConfiguration(config)
        .lockStore(AerospikeStore.builder()
            .aerospikeClient(aerospikeClient)
            .namespace("NAMESPACE")
            .setSuffix("distributed_lock")
            .build())
        .build())
    .build();

lockManager.initialize();

Warning

Always call lockManager.initialize() before any lock operations. For HBase, this creates the table if it does not exist.

Configuring lock timing

By default, LockBase uses the library's built-in timing constants from LockConfiguration. You can override any or all of them by supplying a custom LockConfiguration to the LockBase builder.

LockConfiguration parameters

Parameter Type Default Description
lockTtl Duration 90 seconds How long the lock is held before the storage layer expires it automatically.
waitForLock Duration 90 seconds Maximum time a blocking acquireLock call waits for a contended lock.
sleepBetweenRetries Duration 1000 ms Sleep interval between successive acquisition attempts when a lock is unavailable.

Default configuration

Omitting lockConfiguration(...) from the builder is fully supported — LockBase internally creates a default LockConfiguration with all defaults applied:

// These two are equivalent:
LockBase.builder().mode(LockMode.EXCLUSIVE).lockStore(store).build();
LockBase.builder().mode(LockMode.EXCLUSIVE).lockStore(store)
    .lockConfiguration(LockConfiguration.builder().build())  // all defaults
    .build();

Custom configuration examples

LockConfiguration config = LockConfiguration.builder()
    .lockTtl(Duration.ofSeconds(30))           // short-lived locks
    .waitForLock(Duration.ofSeconds(10))        // fail fast on contention
    .sleepBetweenRetries(Duration.ofMillis(500)) // poll twice as fast
    .build();
LockConfiguration config = LockConfiguration.builder()
    .lockTtl(Duration.ofMinutes(10))           // hold lock for batch duration
    .waitForLock(Duration.ofMinutes(5))         // willing to wait longer
    .sleepBetweenRetries(Duration.ofSeconds(5)) // poll less frequently
    .build();
// waitForLock and sleepBetweenRetries keep their defaults (90s and 1000ms)
LockConfiguration config = LockConfiguration.builder()
    .lockTtl(Duration.ofSeconds(60))
    .build();

How timing flows through the API

When you call acquireLock(lock) without explicit duration/timeout arguments, the values are read from LockConfiguration:

  • tryAcquireLock(lock) → uses lockConfiguration.getLockTtl() as TTL.
  • acquireLock(lock) → uses lockConfiguration.getLockTtl() as TTL and lockConfiguration.getWaitForLock() as timeout.
  • acquireLock(lock, duration) → uses the explicit duration but lockConfiguration.getWaitForLock() as timeout.
  • acquireLock(lock, duration, timeout) → uses both explicit values; config is not consulted.
  • Retry sleep always uses lockConfiguration.getSleepBetweenRetries().

Get a lock instance

Lock lock = lockManager.getLockInstance("order-123", LockLevel.DC);

The returned Lock object contains:

  • lockId — composed as clientId#order-123.
  • lockLevelDC or XDC.
  • farmId — inherited from the manager.
  • acquiredStatus — an AtomicBoolean tracking whether this instance currently holds the lock.

Info

The Lock object is lightweight and does not perform any I/O on creation. Actual storage interaction happens only on acquire / release.

Acquiring locks

Non-blocking — tryAcquireLock

Attempts to acquire immediately. Throws DLMException with ErrorCode.LOCK_UNAVAILABLE if the lock is already held.

// Default TTL (90 seconds)
lockManager.tryAcquireLock(lock);

// Custom TTL
lockManager.tryAcquireLock(lock, Duration.ofSeconds(120));

Blocking — acquireLock

Retries in a loop (1-second intervals) until the lock is acquired or the timeout expires.

// Default TTL (90s) and default timeout (90s)
lockManager.acquireLock(lock);

// Custom TTL, default timeout (90s)
lockManager.acquireLock(lock, Duration.ofSeconds(30));

// Custom TTL and custom timeout
lockManager.acquireLock(lock, Duration.ofSeconds(30), Duration.ofSeconds(10));

Default values

These defaults come from LockConfiguration and can be overridden per LockBase instance. See Configuring lock timing for details.

Constant Default
DEFAULT_LOCK_TTL 90 seconds
DEFAULT_WAIT_FOR_LOCK 90 seconds
DEFAULT_SLEEP_BETWEEN_RETRIES 1000 ms

Releasing locks

boolean released = lockManager.releaseLock(lock);
  • Returns true if the lock was held and successfully released.
  • Returns false if the lock was not held by this instance (i.e. acquiredStatus was already false).

Warning

Always release in a finally block to avoid lock leaks.

Error handling

All lock operations throw DLMException. Use getErrorCode() to distinguish failure reasons:

Lock lock = lockManager.getLockInstance("order-123", LockLevel.DC);
try {
    lockManager.tryAcquireLock(lock, Duration.ofSeconds(60));
    // critical section
} catch (DLMException e) {
    switch (e.getErrorCode()) {
        case LOCK_UNAVAILABLE -> log.warn("Lock held by another holder");
        case CONNECTION_ERROR -> log.error("Storage backend unreachable", e);
        case RETRIES_EXHAUSTED -> log.error("All retry attempts failed", e);
        default -> log.error("Unexpected error", e);
    }
} finally {
    lockManager.releaseLock(lock);
}

See Error Codes for the full list.

Complete lifecycle example

// ── Setup (application startup) ──
DistributedLockManager lockManager = DistributedLockManager.builder()
    .clientId("payment-service")
    .farmId("dc1")
    .lockBase(LockBase.builder()
        .mode(LockMode.EXCLUSIVE)
        .lockStore(AerospikeStore.builder()
            .aerospikeClient(aerospikeClient)
            .namespace("locks")
            .setSuffix("distributed_lock")
            .build())
        .build())
    .build();
lockManager.initialize();

// ── Use (request handling) ──
Lock lock = lockManager.getLockInstance("txn-456", LockLevel.DC);
try {
    lockManager.acquireLock(lock, Duration.ofSeconds(30), Duration.ofSeconds(10));
    processPayment("txn-456");
} catch (DLMException e) {
    if (e.getErrorCode() == ErrorCode.LOCK_UNAVAILABLE) {
        // another instance is processing this transaction
    }
} finally {
    lockManager.releaseLock(lock);
}

// ── Teardown (application shutdown) ──
lockManager.destroy();

Cleanup

When the application shuts down, call destroy() to close the underlying store connection and release resources:

lockManager.destroy();
  • For Aerospike, this calls aerospikeClient.close().
  • For HBase, this calls connection.close().

Danger

Failing to call destroy() may leave dangling connections to the storage backend. Invoke it in a shutdown hook or your framework's lifecycle callback.