현재 테스트 중입니다: 완료되면 GitHub 코드를 오픈할 예정입니다.

Ticketing Java / Kotlin 라이브러리

GitHub Maven Central

하나의 TicketBroker가 세 가지 호출 방식을 함께 제공합니다 — Kotlin 코루틴은 acquire, Java 비동기는 acquireAsync(CompletableFuture), Java 블로킹은 acquireBlocking. 어느 쪽을 써도 같은 브로커, 같은 연결입니다.

리포지토리

xml
kts

예제

간단한 예제

브로커는 앱 시작 시 한 번 만들어 공유하면 됩니다. Duration.ZERO wait는 queue 없는 즉시 시도이고 최대 255초, lease는 1~250초입니다. 마지막 minWorkBudget은 필수이며 Duration.ZERO도 허용됩니다. 시간을 wire 초로 올린 뒤 범위 초과, lease 0, budget 250초 초과 또는 정규화한 lease 초과는 송신 전에 TicketException.Kind.INVALID_INPUT으로 거부하며 값을 잘라 맞추지 않습니다.

Kotlin (코루틴):

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
// 같은 DB transaction에서 token high-water 검사/갱신과 업무 write
ticket.release()

Java (블로킹, try-with-resources):

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();
    // 같은 DB transaction에서 token high-water 검사/갱신과 업무 write
}
Kotlin (suspend)Java Async (CompletableFuture)Java Blocking
acquireacquireAsyncacquireBlocking
waitReadywaitReadyAsyncwaitReadyBlocking
ticket.release()ticket.releaseAsync()ticket.releaseBlocking()

ticket.close()는 백그라운드 best-effort 반납이라 try-with-resources / use에 바로 넣을 수 있습니다.

알아 두면 좋은 동작

  • 송신 가능성 전 coroutine 취소·thread interrupt만 unsent입니다. possible-send 뒤 취소·interrupt나 확정 응답 유실은 TicketException.Kind.INDETERMINATE 이며, grant가 확인되면 Ticket을 공개하지 않고 정확한 token으로 보상 반납을 시도합니다. 같은 owner는 자동 재전송하지 않습니다.
  • M, 모든 E, malformed 응답은 session-fatal입니다. 아직 확정되지 않은 possible-send acquire는 INDETERMINATE입니다.
  • TicketException.Kind.BUSY 는 server capacity의 확정 미획득이며 즉시 반환됩니다.
  • 명시적·보상 반납은 호출 또는 queue 등록 시점부터 계산한 절대 5초 deadline 안에서만 정확한 token으로 재시도합니다. R은 성공, N은 이미 없거나 현재 token이 아님이 확정된 결과이고, deadline까지 최종 응답이 없으면 성공으로 가장하지 않고 오류를 반환합니다.
  • TicketException.Kind.INSUFFICIENT_LEASE면 알려진 정확한 token으로 보상 반납을 시도하고 Ticket을 전달하지 않습니다. ticket.conservativeRemaining()은 보수적인 local 보조값이며 fencing을 대신하지 않습니다.
  • ticket.token 은 unsigned-u64 wire 값을 담은 Long 펜싱 토큰입니다. Java/Kotlin 메모리에서 비교할 때는 Long.compareUnsigned를 사용합니다. DB high-water 검사·갱신과 업무 write를 같은 transaction에서 처리하고 DB 작업 종료가 확인된 뒤 반납합니다.

보안 옵션 (토큰 · TLS)

옵션은 모두 선택입니다. token은 서버의 client_tokens와 맞추고, TLS는 끔 / 시스템 신뢰 저장소 / CA 지정 / 검증 생략(테스트 전용) 네 가지 모드입니다.

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()

Java에서는 TlsMode.systemRoots() / TlsMode.ca("ca.crt") / TlsMode.insecureSkipVerify() 정적 팩토리를 쓰면 됩니다.

서버 쪽 토큰·TLS·클러스터 구성은 Ticketing 서버 배포에서 만들 수 있습니다.

Spring 가상 스레드

Spring MVC를 가상 스레드로 돌린다면 Java·Kotlin 모두 *Blocking 함수를 쓰면 됩니다. 논서스펜드 컨트롤러에서는 acquire(suspend)를 부를 수 없으므로, Kotlin에서도 acquireBlocking이 정상 경로입니다. 코루틴을 거치지 않고 응답에서만 park하므로 캐리어 스레드를 점유하지 않습니다.

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), Duration.ofSeconds(2)
        ).use {
            // 임계 구역
        }
        return "ok"
    }
}

close()(try-with-resources / use)의 백그라운드 반납은 기본적으로 가상 스레드 실행기에서 돌기 때문에 고정 크기 풀에 밀리지 않습니다. 스프링이 관리하는 실행기를 쓰려면 이렇게 넘기면 됩니다.

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

WebFlux나 코루틴 컨트롤러라면 지금처럼 suspend 함수를 그대로 쓰면 됩니다.