Currently in testing: the GitHub code will be opened once complete.

Ticketing Java / Kotlin Library

GitHub Maven Central

A single TicketBroker offers three calling styles together — Kotlin coroutines use acquire, Java async uses acquireAsync (CompletableFuture), and Java blocking uses acquireBlocking. Whichever you use, it's the same broker and the same connections.

Repository

xml
kts

Example

Basic Example

Create one broker at application startup and share it. wait=0 is one immediate attempt without a queue and the maximum wait is 255 seconds; lease is 1–250 seconds. The final minimum-work budget is required and may be zero. Clients round sub-second values up and reject out-of-range values or a budget greater than the normalized lease before sending; they never clamp.

Kotlin (coroutines):

kotlin
val broker = TicketBroker.connect("127.0.0.1:5225")
broker.waitReady(Duration.ofSeconds(5))

val ticket = broker.acquire("key", Duration.ofSeconds(5), Duration.ofSeconds(30), Duration.ofSeconds(2))
val token = ticket.token
// In the same DB transaction: verify/update token high-water and perform the business write.
ticket.release()

Java (blocking):

java
TicketBroker broker = TicketBroker.connect("127.0.0.1:5225");
broker.waitReadyBlocking(Duration.ofSeconds(5));

try (Ticket ticket = broker.acquireBlocking(
        "key", Duration.ofSeconds(5), Duration.ofSeconds(30), Duration.ofSeconds(2))) {
    long token = ticket.getToken();
    // In the same DB transaction: verify/update token high-water and perform the business write.
}

Automatic close/drop is a bounded best-effort release. Use the explicit release API when its result matters. The token must fence the protected database write in the same transaction.

Good to Know (Behavior)

  • Once any request byte may have been sent, losing the definitive response yields Indeterminate. The client never automatically resends A with the same owner, and the caller must not enter the critical section.
  • Cancellation before send is unsent; cancellation after possible send closes that session. If a grant token was parsed concurrently, the client attempts a bounded compensating exact-token release.
  • M, every E, and malformed/oversized/unknown responses are session-fatal. Unresolved possible-send acquires become Indeterminate.
  • B is a definitive capacity rejection and is returned immediately. There is no internal retry; the caller may later start a new acquire with a new owner and application-level backoff.
  • Explicit and compensating release retry the exact token only within an absolute 5-second deadline from call/enqueue. R is success, N means already gone or not current, and no final response is an error—not assumed success.
  • A Ticket is returned only with positive conservative time remaining and enough time for the required work budget. Work over 250 seconds is unsupported before send.
  • The token is mandatory database fencing: in the same DB transaction, reject token <= stored_high_water, update the high-water mark, and perform the business write before commit/rollback and release.

Security Options (Token · TLS)

Every option is optional. token should match the server's client_tokens, and TLS has four modes: off / system trust store / a specified CA / skip verification (test only).

kotlin
val broker = TicketBroker.builder()
    .addrs("10.0.0.1:5225", "10.0.0.2:5225", "10.0.0.3:5225")
    .token("123")
    .tls(TlsMode.SystemRoots)
    // .tls(TlsMode.Ca("ca.crt"))
    // .tls(TlsMode.InsecureSkipVerify)
    .connect()

In Java, use the static factories TlsMode.systemRoots() / TlsMode.ca("ca.crt") / TlsMode.insecureSkipVerify().

You can generate the server-side token, TLS, and cluster configuration on the Ticketing Server Deployment page.

Spring Virtual Threads

On Spring MVC with virtual threads, use the *Blocking calls — in Java and in Kotlin alike. A non-suspending controller cannot call acquire, so acquireBlocking is the normal path even in Kotlin. They never go through coroutines and only park on the reply, so the carrier thread stays free.

kotlin
@RestController
class OrderController(private val broker: TicketBroker) {

    @PostMapping("/orders/{id}")
    fun place(@PathVariable id: String): String {
        broker.acquireBlocking("order-$id", Duration.ofSeconds(5), Duration.ofSeconds(30)).use {
            // critical section
        }
        return "ok"
    }
}

The background release behind close() (try-with-resources / use) runs on a virtual-thread executor by default, so releases never queue behind a fixed-size pool. Pass your own executor to use Spring's instead.

kotlin
TicketBroker.builder()
    .addrs("127.0.0.1:5225")
    .executor(applicationTaskExecutor)
    .connect()

With WebFlux or coroutine controllers, keep using the suspend calls.