Skip to content

simba-jdbc Module

The simba-jdbc module provides a JDBC-based distributed mutex backend using MySQL. It uses optimistic locking (a version column) to ensure safe concurrent updates to the simba_mutex table, and polls the database on a ScheduledThreadPoolExecutor to detect ownership changes.

Schema DDL

The MySQL schema is defined in simba-jdbc/src/init-script/init-simba-mysql.sql:

sql
CREATE DATABASE IF NOT EXISTS simba_db;
USE simba_db;

CREATE TABLE IF NOT EXISTS simba_mutex (
    mutex         VARCHAR(66)    NOT NULL PRIMARY KEY COMMENT 'mutex name',
    acquired_at   BIGINT UNSIGNED NOT NULL,
    ttl_at        BIGINT UNSIGNED NOT NULL,
    transition_at BIGINT UNSIGNED NOT NULL,
    owner_id      CHAR(32)       NOT NULL,
    version       INT UNSIGNED   NOT NULL
);

ER Diagram

mermaid
erDiagram
    SIMBA_MUTEX {
        varchar mutex PK "mutex name (max 66 chars)"
        bigint acquired_at "epoch millis -- when lock was acquired"
        bigint ttl_at "epoch millis -- TTL expiry"
        bigint transition_at "epoch millis -- grace period end"
        char owner_id "contender ID of current owner"
        int version "optimistic lock version"
    }

    note for SIMBA_MUTEX "Each row represents one distributed mutex.<br>Optimistic locking via version column prevents<br>concurrent ownership conflicts."

Column Semantics

ColumnTypeDescription
mutexVARCHAR(66) PKThe logical mutex name. Primary key -- one row per mutex.
acquired_atBIGINT UNSIGNEDEpoch millis when the current owner acquired the lock. 0 when no owner.
ttl_atBIGINT UNSIGNEDEpoch millis when the TTL expires. After this, other contenders may attempt acquisition.
transition_atBIGINT UNSIGNEDEpoch millis when the grace period ends. Equals acquired_at + ttl + transition.
owner_idCHAR(32)The contenderId of the current owner. Empty string when no owner.
versionINT UNSIGNEDIncremented on every acquire/release for optimistic concurrency control.

Key Classes

MutexOwnerRepository Interface

Source: simba-jdbc/.../MutexOwnerRepository.kt:23

kotlin
interface MutexOwnerRepository {
    fun initMutex(mutex: String): Boolean
    fun tryInitMutex(mutex: String): Boolean
    fun getOwner(mutex: String): MutexOwnerEntity
    fun acquire(mutex: String, contenderId: String, ttl: Long, transition: Long): Boolean
    fun acquireAndGetOwner(mutex: String, contenderId: String, ttl: Long, transition: Long): MutexOwnerEntity
    fun release(mutex: String, contenderId: String): Boolean
    fun ensureOwner(mutex: String): MutexOwnerEntity
}
MethodDescription
initMutexInserts the mutex row. Throws SQLIntegrityConstraintViolationException if already exists.
tryInitMutexSafe wrapper: returns false on any exception.
getOwnerReads the current owner. Throws NotFoundMutexOwnerException if the row does not exist.
acquireAttempts to acquire the mutex via UPDATE ... WHERE. Returns true if affected rows > 0.
acquireAndGetOwnerAtomic transaction: acquires the mutex and reads back the full owner state.
releaseReleases the mutex by resetting the row. Only succeeds if owner_id matches.
ensureOwnerGets the owner, auto-initializing the row if it does not exist.

JdbcMutexOwnerRepository

Source: simba-jdbc/.../JdbcMutexOwnerRepository.kt:27

kotlin
class JdbcMutexOwnerRepository(private val dataSource: DataSource) : MutexOwnerRepository

ACQUIRE SQL Logic

The acquisition uses a conditional UPDATE:

sql
UPDATE simba_mutex
SET acquired_at = NOW_MILLIS,
    ttl_at = NOW_MILLIS + ?,
    transition_at = NOW_MILLIS + ?,
    owner_id = ?,
    version = version + 1
WHERE mutex = ?
  AND (
    transition_at < NOW_MILLIS                    -- no active owner (transition expired)
    OR
    (owner_id = ? AND transition_at > NOW_MILLIS) -- same owner renewing within transition
  );

This ensures:

  1. No active owner: transition_at has passed -- any contender can acquire.
  2. Same owner renewing: The current owner can renew even during the transition period (grace period for leadership stability).

MutexOwnerEntity

Source: simba-jdbc/.../MutexOwnerEntity.kt:22

Extends MutexOwner with JDBC-specific fields:

kotlin
class MutexOwnerEntity(
    val mutex: String,
    ownerId: String, acquiredAt: Long, ttlAt: Long, transitionAt: Long
) : MutexOwner(ownerId, acquiredAt, ttlAt, transitionAt) {
    var version: Int = 0
    var currentDbAt: Long = 0
}
FieldDescription
versionThe optimistic lock version from the database. Used for concurrency control.
currentDbAtThe database server's current timestamp, used to prevent clock skew issues between application servers.

JdbcMutexContendService

Source: simba-jdbc/.../JdbcMutexContendService.kt:32

kotlin
class JdbcMutexContendService(
    mutexContender: MutexContender,
    handleExecutor: Executor,
    private val mutexOwnerRepository: MutexOwnerRepository,
    private val initialDelay: Duration,
    private val ttl: Duration,
    private val transition: Duration
) : AbstractMutexContendService(mutexContender, handleExecutor)
ParameterDescription
mutexContenderThe contender bound to this service
handleExecutorExecutor for async owner notification callbacks
mutexOwnerRepositoryThe JDBC repository for mutex state
initialDelayDelay before the first contention attempt
ttlLock TTL -- how long before the owner must renew
transitionGrace period after TTL where the current owner can preferentially renew

JdbcMutexContendServiceFactory

Source: simba-jdbc/.../JdbcMutexContendServiceFactory.kt:27

kotlin
class JdbcMutexContendServiceFactory(
    private val mutexOwnerRepository: MutexOwnerRepository,
    private val handleExecutor: Executor = ForkJoinPool.commonPool(),
    private val initialDelay: Duration,
    private val ttl: Duration,
    private val transition: Duration
) : MutexContendServiceFactory

Sequence Diagram -- Polling Contention

mermaid
sequenceDiagram
autonumber
    participant Service as JdbcMutexContendService
    participant Repo as JdbcMutexOwnerRepository
    participant DB as MySQL
    participant Contender as MutexContender
    participant Executor as handleExecutor

    Service->>Service: startContend()
    Service->>Service: create ScheduledThreadPoolExecutor

    loop Polling Cycle (each ttl interval)
        Service->>Repo: acquireAndGetOwner(mutex, contenderId, ttl, transition)
        Repo->>DB: BEGIN TRANSACTION
        Repo->>DB: UPDATE simba_mutex SET ... WHERE transition_at < NOW OR (owner_id = self AND ...)
        Repo->>DB: SELECT ... FROM simba_mutex WHERE mutex = ?
        Repo->>DB: COMMIT
        DB-->>Repo: MutexOwnerEntity
        Repo-->>Service: MutexOwnerEntity

        Service->>Service: notifyOwner(mutexOwner)
        Service->>Executor: runAsync(safeNotifyOwner)
        Executor->>Contender: onAcquired(mutexState) or onReleased(mutexState)

        Service->>Service: contendPeriod.ensureNextDelay(mutexOwner)
        Note over Service: Owner: renew before TTL<br>Non-owner: wait until transition + jitter
        Service->>Service: schedule next contend
    end

Properties

When using simba-spring-boot-starter, the JDBC backend is configured via application.yml:

yaml
simba:
  enabled: true          # Global Simba enable (default: true)
  jdbc:
    enabled: true        # JDBC backend enable (default: true)
    initial-delay: 0s    # Delay before first contention
    ttl: 10s             # Lock TTL
    transition: 6s       # Grace period after TTL

Source: simba-spring-boot-starter/.../JdbcProperties.kt:25

PropertyDefaultDescription
simba.enabledtrueGlobal enable switch for all Simba backends
simba.jdbc.enabledtrueEnable the JDBC backend
simba.jdbc.initial-delay0sDelay before first contention attempt
simba.jdbc.ttl10sLock TTL -- how long the lock is held before requiring renewal
simba.jdbc.transition6sGrace period after TTL for preferential owner renewal

Error Handling

SituationBehavior
Mutex row does not existNotFoundMutexOwnerException thrown; use tryInitMutex() or ensureOwner() to auto-create
Concurrent acquisition conflictOptimistic locking via version column; the UPDATE returns 0 affected rows
SQL error during contendLogged at ERROR level; next contend scheduled after ttl period
Transaction rollbackacquireAndGetOwner rolls back on any exception and wraps in SimbaException

Dependencies

simba-jdbc
  ├── simba-core
  └── javax.sql.DataSource (provided by the application)

The module does not bundle a JDBC driver. The application must provide the MySQL connector (e.g., com.mysql:mysql-connector-j).

See Also

Released under the Apache License 2.0.