Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: docs

on:
push:
branches:
- main
paths:
- 'docs/**'
- '.github/workflows/docs.yml'
pull_request:
paths:
- 'docs/**'
- '.github/workflows/docs.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: pages-${{ github.ref }}
cancel-in-progress: true

jobs:
pr-build-check:
name: Docs build check (PR)
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install zensical and plugins
run: pip install -r requirements.txt
working-directory: ./docs
- name: Build site
run: zensical build --clean
working-directory: ./docs

deploy:
name: Deploy docs
if: github.event_name != 'pull_request'
permissions:
pages: write
id-token: write
contents: read
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install zensical and plugins
run: pip install -r requirements.txt
working-directory: ./docs
- name: Build site
run: zensical build --clean
working-directory: ./docs
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v4
with:
path: docs/site
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ benchmarking/target/
.DS_Store
*.iml

/bin/
/bin/

# Zensical docs build output
docs/site/
62 changes: 45 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,26 @@ its MVCC (MultiVersion Concurrency Control) capability.
<dependency>
<groupId>com.phonepe</groupId>
<artifactId>DLM</artifactId>
<version>1.0.0</version>
<version>${dlm.version}</version>
</dependency>
```

> Replace `${dlm.version}` with the latest version from [Maven Central](https://central.sonatype.com/artifact/com.phonepe/DLM) or [GitHub Releases](https://github.com/PhonePe/DLM/releases).


### Usage

#### Initializing Distributed Lock Manager

##### With Aerospike as lock base

``` java
```java
DistributedLockManager lockManager = DistributedLockManager.builder()
.clientId("CLIENT_ID")
.farmId("FA1")
.lockBase(AerospikeLockBase.builder()
.lockBase(LockBase.builder()
.mode(LockMode.EXCLUSIVE)
.store(AerospikeStore.builder()
.lockStore(AerospikeStore.builder()
.aerospikeClient(aerospikeClient)
.namespace("NAMESPACE")
.setSuffix("distributed_lock")
Expand All @@ -44,13 +46,13 @@ lockManager.initialize();

##### With HBase as lock base

``` java
```java
DistributedLockManager lockManager = DistributedLockManager.builder()
.clientId("CLIENT_ID")
.farmId("FA1")
.lockBase(HBaseLockBase.builder()
.lockBase(LockBase.builder()
.mode(LockMode.EXCLUSIVE)
.store(HBaseStore.builder()
.lockStore(HBaseStore.builder()
.connection(connection) // HBase connection reference
.tableName("table_name")
.build())
Expand Down Expand Up @@ -88,40 +90,47 @@ This library offers various methods for acquiring and releasing locks on critica
final Lock lock = lockManager.getLockInstance("LOCK_ID", LockLevel.DC);
try {
lockManager.tryAcquireLock(lock); // Attempts to acquire the lock for the default duration of 90 seconds
// OR lockManager.tryAcquireLock(lock, 120); // Tries to acquire the lock for 120 seconds
// OR lockManager.tryAcquireLock(lock, Duration.ofSeconds(120)); // Tries to acquire the lock for 120 seconds

// Perform actions once the lock is successfully acquired.

} catch (DLSException e) {
if (ErrorCode.LOCK_UNAVAILABLE.equals(e.getErrorCode)) {
} catch (DLMException e) {
if (ErrorCode.LOCK_UNAVAILABLE.equals(e.getErrorCode())) {
// Actions to take if the lock can't be acquired.
}
} finally {
// Verify if the lock was released successfully.
boolean released = lockManager.release(lock);
boolean released = lockManager.releaseLock(lock);
}
```

```java
// Vulnerable entity represented by LOCK_ID
// Representing a vulnerable entity by LOCK_ID
final Lock lock = lockManager.getLockInstance("LOCK_ID", LockLevel.DC);
try {
lockManager.acquireLock(lock); // Attempts to acquire the lock for the default duration of 90 seconds and waits for 90 seconds
// OR lockManager.acquireLock(lock, 30); // Tries to acquire the lock for 30 seconds, waiting for 90 seconds
// OR lockManager.acquireLock(lock, 30, 30); // Tries to acquire the lock for 30 seconds, waiting for 30 seconds
// OR lockManager.acquireLock(lock, Duration.ofSeconds(30)); // Tries to acquire the lock for 30 seconds, waiting for 90 seconds
// OR lockManager.acquireLock(lock, Duration.ofSeconds(30), Duration.ofSeconds(30)); // Tries to acquire the lock for 30 seconds, waiting for 30 seconds

// Perform actions once the lock is successfully acquired.
} catch (DLSException e) {
if (ErrorCode.LOCK_UNAVAILABLE.equals(e.getErrorCode)) {
} catch (DLMException e) {
if (ErrorCode.LOCK_UNAVAILABLE.equals(e.getErrorCode())) {
// Actions to take if the lock can't be acquired.
}
} finally {
// Verify if the lock was released successfully.
boolean released = lockManager.release(lock);
boolean released = lockManager.releaseLock(lock);
}
```

#### Cleanup

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

```java
lockManager.destroy();
```

#### Lock Levels
* DC - Acquiring/releasing lock within a DC
* XDC - Acquiring/releasing lock across DCs.
Expand Down Expand Up @@ -175,3 +184,22 @@ lockManager.initialize();

> **Backward compatibility**: omitting `lockConfiguration(...)` from the builder is fully supported
> and produces identical behaviour to all previous library versions.

## Documentation Site (Zensical)

This repository now includes Zensical-based docs under `docs/`.

- Config: `docs/zensical.toml`
- Content: `docs/docs/`
- Python dependencies: `docs/requirements.txt`
- GitHub Pages workflow: `.github/workflows/docs.yml`

Build docs locally:

```bash
cd docs
pip install -r requirements.txt
zensical build --clean
```

Generated site output is available at `docs/site`.
82 changes: 82 additions & 0 deletions docs/docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Getting Started

## Requirements

- **Java 17** or later.
- One of the supported storage backends:
- An **Aerospike** cluster reachable from application nodes, or
- An **HBase** cluster reachable from application nodes.

## Add dependency

```xml
<dependency>
<groupId>com.phonepe</groupId>
<artifactId>DLM</artifactId>
<version>${dlm.version}</version>
</dependency>
```

Replace `${dlm.version}` with the latest version from [Maven Central](https://central.sonatype.com/artifact/com.phonepe/DLM) or [GitHub Releases](https://github.com/PhonePe/DLM/releases).

## Build locally

```bash
git clone https://github.com/PhonePe/DLM.git
cd DLM
mvn clean install
```

To run the tests:

```bash
mvn clean test
```

!!! note
Some integration tests require Docker (via Testcontainers) for Aerospike.
Make sure the Docker daemon is running before executing the full test suite.

## Minimal example

```java
// 1. Build the lock manager
DistributedLockManager lockManager = DistributedLockManager.builder()
.clientId("order-service")
.farmId("dc1")
.lockBase(LockBase.builder()
.mode(LockMode.EXCLUSIVE)
.lockStore(AerospikeStore.builder()
.aerospikeClient(aerospikeClient)
.namespace("locks")
.setSuffix("distributed_lock")
.build())
.build())
.build();

// 2. Initialize (creates tables / validates connectivity)
lockManager.initialize();

// 3. Acquire → work → release
Lock lock = lockManager.getLockInstance("order-123", LockLevel.DC);
try {
lockManager.acquireLock(lock);
// critical section
} finally {
lockManager.releaseLock(lock);
}

// 4. Shutdown
lockManager.destroy();
```

!!! tip
The example above uses all default timing values (90s TTL, 90s wait, 1s retry).
To customise these, pass a `LockConfiguration` to the `LockBase` builder.
See [Configuring lock timing](usage.md#configuring-lock-timing).

## What's next

- [Usage](usage.md) — initialization, lock timing configuration, API overloads, error handling.
- [Locking Semantics](locking.md) — defaults, retry behavior, lock levels.
- [Storage Backends](storages/aerospike.md) — Aerospike and HBase details.
71 changes: 71 additions & 0 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Distributed Lock Manager

Distributed Lock Manager (DLM) is a lightweight Java library for coordinating lock acquisition and release across multiple application instances in a distributed environment.

## Why DLM?

In service-oriented architectures, concurrent access to shared resources is inevitable. DLM provides a simple, pluggable distributed locking mechanism that protects critical entities for a specified duration — without requiring a dedicated lock server.

## Key features

- **Exclusive locking** — only one holder at a time per lock identity.
- **Lock levels** — `DC` (single data center) and `XDC` (cross data center).
- **Pluggable storage backends** — Aerospike and HBase out of the box.
- **Blocking and non-blocking acquisition** — choose between immediate-fail (`tryAcquireLock`) or wait-with-timeout (`acquireLock`).
- **Automatic TTL** — every lock has a time-to-live; the lock expires even if the holder crashes.
- **Configurable lock timing** — tune TTL, wait timeout, and retry interval per `LockBase` via `LockConfiguration`.
- **Built-in retry** — configurable retry with backoff on transient storage failures.

## How it works

```mermaid
sequenceDiagram
participant App as Application
participant DLM as DistributedLockManager
participant LB as LockBase
participant Store as ILockStore (Aerospike / HBase)

App->>DLM: getLockInstance("order-123", DC)
DLM-->>App: Lock

App->>DLM: acquireLock(lock, duration, timeout)
DLM->>LB: acquireLock(lock, duration, timeout)

loop Until acquired or timeout
LB->>Store: write(lockId, lockLevel, farmId, ttl)
alt Lock available
Store-->>LB: success
LB-->>DLM: acquired
else Lock held by another holder
Store-->>LB: LOCK_UNAVAILABLE
LB->>LB: sleep 1s, retry
end
end

DLM-->>App: void (acquired)

App->>DLM: releaseLock(lock)
DLM->>LB: releaseLock(lock)
LB->>Store: remove(lockId, lockLevel, farmId)
LB-->>DLM: true
DLM-->>App: true
```

### Lock identity scoping

Each lock identity is scoped to a **client**. Internally the lock ID is stored as `clientId#lockId`, so two different clients can independently lock the same logical entity without conflict.

### Lock lifecycle

1. **Initialize** — `lockManager.initialize()` prepares the storage backend (e.g. creates the HBase table).
2. **Get lock instance** — `lockManager.getLockInstance(id, level)` creates a `Lock` object.
3. **Acquire** — `tryAcquireLock` / `acquireLock` writes a record to the store with a TTL.
4. **Release** — `releaseLock` removes the record from the store.
5. **Destroy** — `lockManager.destroy()` closes the underlying storage connection.

## What to read next

- [Getting Started](getting-started.md) — dependency setup, prerequisites, building locally.
- [Usage](usage.md) — initialization, acquisition, release, cleanup.
- [Locking Semantics](locking.md) — API reference, defaults, retry behavior, error codes.
- [Storage Backends](storages/aerospike.md) — Aerospike and HBase internals.
Loading
Loading