Skip to content

simba-jdbc Module

The simba-jdbc module provides a JDBC-based distributed mutex backend using MySQL. It serializes concurrent updates to the simba_mutex table with atomic conditional UPDATEs guarded by owner/transition predicates, and polls the database on a ScheduledThreadPoolExecutor to detect ownership changes.

MySQL version requirement: the backend reads database time through UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)). On 64-bit MySQL 8.0.28+ the returned value is valid through year 3001; on older versions the valid range ends at 2038-01-19 UTC, where out-of-range calls return 0 and owner timestamps silently fall back to JVM time. Plan an upgrade before 2038 if you run an older MySQL.

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      VARCHAR(128)   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 "state change counter"
    }

    note for SIMBA_MUTEX "Each row represents one distributed mutex.<br>Conditional UPDATE + InnoDB row lock 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_idVARCHAR(128)The contenderId of the current owner. Empty string when no owner.
versionINT UNSIGNEDIncremented on every acquire/release as a state-change counter. Not compared in WHERE — concurrency is guarded by the owner/transition predicates.

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
versionState-change counter from the database. Not used for concurrency decisions.
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 conflictAtomic conditional UPDATE guarded by owner/transition predicates; the loser's UPDATE affects 0 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.