diff --git a/README.md b/README.md index 81ae77b..e48489a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
-![version 0.3.8](https://img.shields.io/badge/version-0.3.8-black?labelColor=black&style=flat-square) ![jdk 17](https://img.shields.io/badge/minimum_jdk-17-orange?labelColor=black&style=flat-square) ![load-test](https://img.shields.io/badge/load%20test%2010%2C000%2C000-success-brightgreen?labelColor=black&style=flat-square) +![version 0.3.9](https://img.shields.io/badge/version-0.3.9-black?labelColor=black&style=flat-square) ![jdk 17](https://img.shields.io/badge/minimum_jdk-17-orange?labelColor=black&style=flat-square) ![load-test](https://img.shields.io/badge/load%20test%2010%2C000%2C000-success-brightgreen?labelColor=black&style=flat-square) ![redis--stream](https://img.shields.io/badge/-redis--stream-da2020?style=flat-square&logo=Redis&logoColor=white) Redis-Stream을 지원하는 Saga frame work 입니다. @@ -14,18 +14,18 @@ Redis-Stream을 지원하는 Saga frame work 입니다. 1. 동기 API와 비동기[Reactor](https://projectreactor.io/) API 지원 2. 함수형 Orchestrator 방식과 Event 기반 Choreograph 방식 지원 -3. 처리되지 않은 트랜잭션을 찾아 자동으로 재실행 -4. Backpressure 지원으로 노드별 처리가능한 트랜잭션 수 조절 -5. 여러 노드가 중복 트랜잭션 이벤트를 수신하는 문제 방지 +3. 설정한 주기 마다 처리되지 않은 이벤트를 찾아 자동으로 재실행 +4. Backpressure 지원으로 노드별 처리가능한 이벤트 수 조절 +5. 같은 그룹의 여러 노드가 이벤트를 중복 수신하는 문제 방지 6. `At Least Once` 방식의 메시지 전달 보장 ## How to use -Netx는 스프링 환경에서 사용할 수 있으며, 아래와 같이 `@EnableDistributedTransaciton` 어노테이션을 붙이는것으로 손쉽게 사용할 수 있습니다. +Netx는 스프링 환경에서 사용할 수 있으며, 아래와 같이 `@EnableSaga` 어노테이션을 붙이는것으로 손쉽게 구성할 수 있습니다. ```kotlin +@EnableSaga @SpringBootApplication -@EnableDistributedTransaciton class Application { companion object { @@ -37,32 +37,32 @@ class Application { } ``` -`@EnableDistributedTransaciton` 어노테이션으로 자동 구성할 경우 netx는 아래 프로퍼티를 사용해 메시지 큐와 커넥션을 맺습니다. +`@EnableSaga` 어노테이션으로 자동 구성할 경우 netx는 아래 프로퍼티를 사용해 이벤트 스트림 서비스와 커넥션을 맺습니다. #### Properties -| KEY | EXAMPLE | DESCRIPTION | DEFAULT | -|-------------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| -| **netx.mode** | redis | 트랜잭션 관리에 사용할 메시지 큐 구현체의 mode 입니다. | | -| **netx.host** | localhost | 트랜잭션 관리에 사용할 메시지 큐 의 host url 입니다. (ex. redis host) | | -| **netx.password** | 0000 | 트랜잭션 관리에 사용할 메시지큐에 접속하는데 사용하는 password 입니다. 설정하지 않을시 0000이 비밀번호로 매핑됩니다. | 0000 | -| **netx.port** | 6379 | 트랜잭션 관리에 사용할 메시지 큐의 port 입니다. | | -| **netx.group** | pay-group | 분산 노드의 그룹입니다. 트랜잭션 이벤트는 같은 그룹내 하나의 노드로만 전송됩니다. | | -| **netx.node-id** | 1 | id 생성에 사용될 식별자입니다. 모든 서버는 반드시 다른 id를 할당받아야 하며, 1~256 만큼의 id를 설정할 수 있습니다. _`중복된 id 생성을 방지하기위해 twitter snowflake 알고리즘으로 id를 생성합니다.`_ | | -| **netx.node-name** | pay-1 | _`netx.group`_ 에 참여할 서버의 이름입니다. 같은 그룹내에 중복된 이름이 존재하면 안됩니다. | | -| **netx.recovery-milli** | 1000 | _`netx.recovery-milli`_ 마다 _`netx.orphan-milli`_ 동안 처리 되지 않는 트랜잭션을 찾아 재실행합니다. | 1000 | -| **netx.orphan-milli** | 60000 | PENDING 상태가된 트랜잭션 중, orphan-milli가 지나도 ACK 상태가 되지 않은 트랜잭션을 찾아 재시작합니다. | 60000 | -| **netx.backpressure** | 40 | 한번에 수신가능한 트랜잭션 수를 조절합니다. **너무 높게설정하면 서버에 부하가 올 수 있고, 낮게 설정하면 성능이 낮아질 수 있습니다.** 이 설정은 다른 서버가 발행한 트랜잭션 수신량과 처리에 실패한 트랜잭션 수신량에 영향을 미칩니다. 수신되지 못하거나, drop된 트랜잭션은 자동으로 retry 대기열에 들어갑니다. | 40 | -| **netx.logging.level** | info | logging level을 지정합니다. 선택가능한 value는 다음과 같습니다. "info", "warn", "off" | "off" | -| **netx.pool-size** | 40 | 커넥션을 계속해서 맺어야할때, 최대 커넥션 수를 조절하는데 사용됩니다. | 10 | +| KEY | EXAMPLE | DESCRIPTION | DEFAULT | +|-------------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| +| **netx.mode** | redis | Saga 관리에 사용할 메시지 큐 구현체의 mode 입니다. | | +| **netx.host** | localhost | Saga 관리에 사용할 메시지 큐 의 host url 입니다. (ex. redis host) | | +| **netx.password** | 0000 | Saga 관리에 사용할 메시지큐에 접속하는데 사용하는 password 입니다. 설정하지 않을시 0000이 비밀번호로 매핑됩니다. | 0000 | +| **netx.port** | 6379 | Saga 관리에 사용할 메시지 큐의 port 입니다. | | +| **netx.group** | pay-group | 분산 노드의 그룹입니다. Saga 이벤트는 같은 그룹내 하나의 노드로만 전송됩니다. | | +| **netx.node-id** | 1 | id 생성에 사용될 식별자입니다. 모든 서버는 반드시 다른 id를 할당받아야 하며, 1~256 만큼의 id를 설정할 수 있습니다. _`중복된 id 생성을 방지하기위해 twitter snowflake 알고리즘으로 id를 생성합니다.`_ | | +| **netx.node-name** | pay-1 | _`netx.group`_ 에 참여할 서버의 이름입니다. 같은 그룹내에 중복된 이름이 존재하면 안됩니다. | | +| **netx.recovery-milli** | 1000 | _`netx.recovery-milli`_ 마다 _`netx.orphan-milli`_ 동안 처리 되지 않는 Saga를 찾아 재실행합니다. | 1000 | +| **netx.orphan-milli** | 60000 | PENDING 상태가된 이벤트 중, orphan-milli가 지나도 ACK 상태가 되지 않은 이벤트르 찾아 재시작합니다. | 60000 | +| **netx.backpressure** | 40 | 한번에 수신가능한 이벤트 수를 조절합니다. **너무 높게설정하면 서버에 부하가 올 수 있고, 낮게 설정하면 성능이 낮아질 수 있습니다.** 이 설정은 다른 서버가 발행한 이벤트 수신량과 처리에 실패한 이벤트 수신량에 영향을 미칩니다. 수신되지 못하거나, drop된 이벤트는 자동으로 재시도 대기열에 들어갑니다. | 40 | +| **netx.logging.level** | info | logging level을 지정합니다. 선택가능한 value는 다음과 같습니다. "info", "warn", "off" | "off" | +| **netx.pool-size** | 40 | 커넥션을 계속해서 맺어야할때, 최대 커넥션 수를 조절하는데 사용됩니다. | 10 | ### Usage example #### Orchestrator-example. > [!TIP] -> Orchestrator 사용시, Transactional Message Pattern이 자동 적용됩니다. -> 메시지 유실에대한 retry 단위는 Orchestrator의 각 연산(하나의 function) 단위이며, 모든 체인이 성공하거나 rollback이 호출됩니다. +> Orchestrator 사용시, `Transactional messaging pattern` 이 자동 적용됩니다. +> 이벤트 유실에대한 retry 단위는 Orchestrator의 각 연산(하나의 function) 단위이며, 모든 체인이 성공하거나 rollback이 호출됩니다. ```kotlin // Use Orchestrator @@ -70,11 +70,9 @@ class Application { class OrderService(private val orderOrchestrator: Orchestrator) { fun order(orderRequest: Order): OrderResult { - val result = orderOrchestrator.transactionSync(orderRequest) - if (!result.isSuccess) { - result.throwError() - } - return result.decodeResult(OrderResult::class) + val result = orderOrchestrator.sagaSync(orderRequest) + + result.decodeResultOrThrow(OrderResult::class) // If success get result or else throw exception } } @@ -89,7 +87,7 @@ class OrchestratorConfigurer( return orchestratorFactory.create("orderOrchestrator") .start( orchestrate = { order -> // its order type - // Start Transaction with your bussiness logic + // Do your bussiness logic // something like ... "Check valid seller" return@start user }, @@ -123,149 +121,153 @@ class OrchestratorConfigurer( } ``` +#### Events-Scenario0. Handle saga event + +다른 분산서버가 (혹은 자기자신이) sagaManager를 통해서 saga를 시작하거나 saga의 상태를 변경했을때, 상태에 맞는 핸들러를 호출합니다. +이 핸들러를 구현함으로써, saga 상태별 로직을 구현할 수 있습니다. +각 핸들러에서 에러가 던져지면, 자동으로 rollback 이 호출되며, 핸들러가 종료되면, 어노테이션에 설정된 successWith 상태가 자동으로 호출니다. + > [!WARNING] -> Event사용시 Transactional Message Pattern을 직접 적용해줘야합니다. -> 아래 이벤트 사용 예시는 적용된 예시가 아니며, 적용을 위해서는 transactionManager 를 호출하는 부분과 트랜잭션 이벤트를 받는부분을 분리해야합니다. -> 비즈니스로직을 모두 @Transaction...Listener 안으로 이동함으로써 손쉽게 적용할 수 있습니다. +> Saga 핸들러는 반드시 핸들러에 맞는 `Saga...Event` **하나**만을 파라미터로 받아야 합니다. +> Event사용시 `Transactional messaging pattern` 을 직접 적용해줘야합니다. +> 아래 예시와 같이, 비즈니스로직을 모두 @Saga...Listener 안으로 이동함으로써 손쉽게 적용할 수 있습니다. + +```kotlin + +@SagaHandler +class SagaHandler( + private val sagaManager: SagaManager, +) { + + fun start() { + val foo = Foo("...") + sagaManager.startSync(foo) // it will call + } + + @SagaStartListener(event = Foo::class, successWith = SuccessWith.PUBLISH_JOIN) // Receive saga event when event can be mapped to Foo.class + fun handleSagaStartEvent(event: SagaStartEvent) { + val foo: Foo = event.decodeEvent(Foo::class) // Get event field to Foo.class + // ... + event.setNextEvent(nextFoo) // When this handler terminates and calls the next event or rollback, the event set here is published together. + } + + @SagaJoinListener(successWith = SuccessWith.PUBLISH_COMMIT) // Receive all saga event when no type is defined. And, when terminated this function, publish commit state + fun handleSagaJoinEvent(event: SagaJoinEvent) { + // ... + } + + @SagaCommitListener( + event = Foo::class, + noRollbackFor = [IllegalArgumentException::class] // Don't rollback when throw IllegalArgumentException. *Rollback if throw Throwable or IllegalArgumentException's super type* + ) + fun handleSagaCommitEvent(event: SagaCommitEvent): Mono { // In Webflux framework, publisher must be returned. + throw IllegalArgumentException("Ignore this exception") + // ... + } + + @SagaRollbackListener(Foo::class) + fun handleSagaRollbackEvent(event: SagaRollbackEvent) { // In Mvc framework, publisher must not returned. + val undo: Foo = event.decodeUndo(Foo::class) // Get event field to Foo.class + } +} +``` -#### Event-example. Start pay transaction +#### Events-Scenario1. Start pay saga ```kotlin // Sync fun pay(param: Any): Any { - val transactionId = - transactionManager.syncStart(Pay(id = 1L, paid = 1000L)) // start transaction + val sagaId = sagaManager.syncStart(Pay(id = 1L, paid = 1000L)) // start saga runCatching { // Do your bussiness logic }.fold( - onSuccess = { transactionManager.syncCommit(transactionId) }, // commit transaction + onSuccess = { sagaManager.syncCommit(sagaId) }, // commit saga onFailure = { - transactionManager.syncRollback( - transactionId, + sagaManager.syncRollback( + sagaId, it.message ) - } // rollback transaction + } // rollback saga ) } // Async fun pay(param: Any): Mono { - return transactionManager.start( + return sagaManager.start( Pay( id = 1L, paid = 1000L ) - ) // Start distributed transaction and publish transaction start event - .flatMap { transactionId -> + ) // Start distributed saga and publish saga start event + .flatMap { sagaId -> service.pay(param) .doOnError { throwable -> - transactionManager.rollback( - transactionId, + sagaManager.rollback( + sagaId, throwable.message - ) // Publish rollback event to all transaction joined node + ) // Publish rollback event to all saga joined node } - }.doOnSuccess { transactionId -> - transactionManager.commit(transactionId) // Publish commit event to all transaction joined node + }.doOnSuccess { sagaId -> + sagaManager.commit(sagaId) // Publish commit event to all saga joined node } } ``` -#### Events-Scenario2. Join order transaction +#### Events-Scenario2. Join order saga ```kotlin //Sync fun order(param: Any): Any { - val transactionId = transactionManager.syncJoin( - param.transactionId, + val sagaId = sagaManager.syncJoin( + param.saganId, Order(id = 1L, state = PENDING) - ) // join transaction + ) // join saga runCatching { // This is kotlin try catch, not netx library spec // Do your bussiness logic }.fold( - onSuccess = { transactionManager.syncCommit(transactionId) }, // commit transaction + onSuccess = { sagaManager.syncCommit(sagaId) }, // commit saga onFailure = { - transactionManager.syncRollback( - transactionId, + sagaManager.syncRollback( + sagaId, it.message ) - } // rollback transaction + } // rollback saga ) } // Async fun order(param: Any): Mono { - return transactionManager.join( - param.transactionId, + return sagaManager.join( + param.sagaId, Order(id = 1L, state = PENDING) - ) // join exists distributed transaction and publish transaction join event - .flatMap { transactionId -> + ) // join exists distributed saga and publish saga join event + .flatMap { sagaId -> service.order(param) .doOnError { throwable -> - transactionManager.rollback(transactionId, throwable.message) + sagaManager.rollback(sagaId, throwable.message) } - }.doOnSuccess { transactionId -> - transactionManager.commit(transactionId) + }.doOnSuccess { sagaId -> + sagaManager.commit(sagaId) } } ``` -#### Events-Scenario3. Check exists transaction +#### Events-Scenario3. Check exists saga ```kotlin // Sync fun exists(param: Any): Any { - return transactionManager.syncExists(param.transactionId) + return sagaManager.syncExists(param.sagaId) } // Async fun exists(param: Any): Mono { - return transactionManager.exists(param.transactionId) // Find any transaction has ever been started + return sagaManager.exists(param.sagaId) // Find any saga has ever been started } ``` -#### Events-Scenario4. Handle transaction event - -다른 분산서버가 (혹은 자기자신이) transactionManager를 통해서 트랜잭션을 시작하거나 트랜잭션 상태를 변경했을때, 트랜잭션 상태에 맞는 핸들러를 호출합니다. -이 핸들러를 구현함으로써, 트랜잭션 상태별 로직을 구현할 수 있습니다. -각 핸들러에서 에러가 던져지면, 자동으로 rollback 이 호출됩니다. - -> [!WARNING] -> 트랜잭션 핸들러는 반드시 핸들러에 맞는 `TransactionEvent` **하나**만을 파라미터로 받아야 합니다. - -```kotlin - -@TransactionHandler -class TransactionHandler { - - @TransactionStartListener(event = Foo::class) // Receive transaction event when event can be mapped to Foo.class - fun handleTransactionStartEvent(event: TransactionStartEvent) { - val foo: Foo = event.decodeEvent(Foo::class) // Get event field to Foo.class - // ... - event.setNextEvent(nextFoo) // When this handler terminates and calls the next event or rollback, the event set here is published together. - } - - @TransactionJoinListener(successWith = SuccessWith.PUBLISH_COMMIT) // Receive all transaction event when no type is defined. And, when terminated this function, publish commit state - fun handleTransactionJoinEvent(event: TransactionJoinEvent) { - // ... - } - - @TransactionCommitListener( - event = Foo::class, - noRollbackFor = [IllegalArgumentException::class] // Dont rollback when throw IllegalArgumentException. *Rollback if throw Throwable or IllegalArgumentException's super type* - ) - fun handleTransactionCommitEvent(event: TransactionCommitEvent): Mono { // In Webflux framework, publisher must be returned. - throw IllegalArgumentException("Ignore this exception") - // ... - } - - @TransactionRollbackListener(Foo::class) - fun handleTransactionRollbackEvent(event: TransactionRollbackEvent) { // In Mvc framework, publisher must not returned. - val undo: Foo = event.decodeUndo(Foo::class) // Get event field to Foo.class - } -} -``` ## Download diff --git a/gradle.properties b/gradle.properties index 5320a89..5cb3c9d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ kotlin.code.style=official ### Project ### group=org.rooftop.netx -version=0.3.8 +version=0.3.9 compatibility=17 ### Sonarcloud ### diff --git a/src/main/kotlin/org/rooftop/netx/api/Exceptions.kt b/src/main/kotlin/org/rooftop/netx/api/Exceptions.kt index 892854f..4e6b9c3 100644 --- a/src/main/kotlin/org/rooftop/netx/api/Exceptions.kt +++ b/src/main/kotlin/org/rooftop/netx/api/Exceptions.kt @@ -4,14 +4,14 @@ class EncodeException(message: String, throwable: Throwable) : RuntimeException( class DecodeException(message: String, throwable: Throwable) : RuntimeException(message, throwable) -open class TransactionException(message: String) : RuntimeException(message) +open class SagaException(message: String) : RuntimeException(message) -class AlreadyCommittedTransactionException(transactionId: String, state: String) : - TransactionException("Cannot join transaction cause, transaction \"$transactionId\" already \"$state\"") +class AlreadyCommittedSagaException(id: String, state: String) : + SagaException("Cannot join saga cause, saga \"$id\" already \"$state\"") class NotFoundDispatchFunctionException(message: String) : RuntimeException(message) -class FailedAckTransactionException(message: String) : RuntimeException(message) +class FailedAckSagaException(message: String) : RuntimeException(message) class ResultTimeoutException(message: String, throwable: Throwable) : RuntimeException(message, throwable) diff --git a/src/main/kotlin/org/rooftop/netx/api/Orchestrator.kt b/src/main/kotlin/org/rooftop/netx/api/Orchestrator.kt index 045ecec..6e23352 100644 --- a/src/main/kotlin/org/rooftop/netx/api/Orchestrator.kt +++ b/src/main/kotlin/org/rooftop/netx/api/Orchestrator.kt @@ -4,19 +4,19 @@ import reactor.core.publisher.Mono interface Orchestrator { - fun transaction(request: T): Mono> + fun saga(request: T): Mono> - fun transaction(timeoutMillis: Long, request: T): Mono> + fun saga(timeoutMillis: Long, request: T): Mono> - fun transaction(request: T, context: MutableMap): Mono> + fun saga(request: T, context: MutableMap): Mono> - fun transaction(timeoutMillis: Long, request: T, context: MutableMap): Mono> + fun saga(timeoutMillis: Long, request: T, context: MutableMap): Mono> - fun transactionSync(request: T): Result + fun sagaSync(request: T): Result - fun transactionSync(timeoutMillis: Long, request: T): Result + fun sagaSync(timeoutMillis: Long, request: T): Result - fun transactionSync(request: T, context: MutableMap): Result + fun sagaSync(request: T, context: MutableMap): Result - fun transactionSync(timeoutMillis: Long, request: T, context: MutableMap): Result + fun sagaSync(timeoutMillis: Long, request: T, context: MutableMap): Result } diff --git a/src/main/kotlin/org/rooftop/netx/api/SagaCommitEvent.kt b/src/main/kotlin/org/rooftop/netx/api/SagaCommitEvent.kt new file mode 100644 index 0000000..d17827f --- /dev/null +++ b/src/main/kotlin/org/rooftop/netx/api/SagaCommitEvent.kt @@ -0,0 +1,13 @@ +package org.rooftop.netx.api + +class SagaCommitEvent internal constructor( + id: String, + nodeName: String, + group: String, + event: String?, + codec: Codec, +) : SagaEvent(id, nodeName, group, event, codec) { + + override fun copy(): SagaCommitEvent = + SagaCommitEvent(id, nodeName, group, event, codec) +} diff --git a/src/main/kotlin/org/rooftop/netx/api/TransactionCommitListener.kt b/src/main/kotlin/org/rooftop/netx/api/SagaCommitListener.kt similarity index 84% rename from src/main/kotlin/org/rooftop/netx/api/TransactionCommitListener.kt rename to src/main/kotlin/org/rooftop/netx/api/SagaCommitListener.kt index 239a8a9..e59fcc3 100644 --- a/src/main/kotlin/org/rooftop/netx/api/TransactionCommitListener.kt +++ b/src/main/kotlin/org/rooftop/netx/api/SagaCommitListener.kt @@ -4,7 +4,7 @@ import kotlin.reflect.KClass @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) -annotation class TransactionCommitListener( +annotation class SagaCommitListener( val event: KClass<*> = Any::class, val noRollbackFor: Array> = [], ) diff --git a/src/main/kotlin/org/rooftop/netx/api/TransactionEvent.kt b/src/main/kotlin/org/rooftop/netx/api/SagaEvent.kt similarity index 84% rename from src/main/kotlin/org/rooftop/netx/api/TransactionEvent.kt rename to src/main/kotlin/org/rooftop/netx/api/SagaEvent.kt index 661179d..02012d9 100644 --- a/src/main/kotlin/org/rooftop/netx/api/TransactionEvent.kt +++ b/src/main/kotlin/org/rooftop/netx/api/SagaEvent.kt @@ -2,8 +2,8 @@ package org.rooftop.netx.api import kotlin.reflect.KClass -sealed class TransactionEvent( - val transactionId: String, +sealed class SagaEvent( + val id: String, val nodeName: String, val group: String, internal val event: String?, @@ -24,5 +24,5 @@ sealed class TransactionEvent( type ) - internal abstract fun copy(): TransactionEvent + internal abstract fun copy(): SagaEvent } diff --git a/src/main/kotlin/org/rooftop/netx/api/SagaJoinEvent.kt b/src/main/kotlin/org/rooftop/netx/api/SagaJoinEvent.kt new file mode 100644 index 0000000..9f3f21f --- /dev/null +++ b/src/main/kotlin/org/rooftop/netx/api/SagaJoinEvent.kt @@ -0,0 +1,13 @@ +package org.rooftop.netx.api + +class SagaJoinEvent internal constructor( + id: String, + nodeName: String, + group: String, + event: String?, + codec: Codec, +) : SagaEvent(id, nodeName, group, event, codec) { + + override fun copy(): SagaJoinEvent = + SagaJoinEvent(id, nodeName, group, event, codec) +} diff --git a/src/main/kotlin/org/rooftop/netx/api/TransactionJoinListener.kt b/src/main/kotlin/org/rooftop/netx/api/SagaJoinListener.kt similarity index 87% rename from src/main/kotlin/org/rooftop/netx/api/TransactionJoinListener.kt rename to src/main/kotlin/org/rooftop/netx/api/SagaJoinListener.kt index 243b103..d888dae 100644 --- a/src/main/kotlin/org/rooftop/netx/api/TransactionJoinListener.kt +++ b/src/main/kotlin/org/rooftop/netx/api/SagaJoinListener.kt @@ -4,7 +4,7 @@ import kotlin.reflect.KClass @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) -annotation class TransactionJoinListener( +annotation class SagaJoinListener( val event: KClass<*> = Any::class, val noRollbackFor: Array> = [], val successWith: SuccessWith = SuccessWith.PUBLISH_JOIN, diff --git a/src/main/kotlin/org/rooftop/netx/api/SagaManager.kt b/src/main/kotlin/org/rooftop/netx/api/SagaManager.kt new file mode 100644 index 0000000..bf558ea --- /dev/null +++ b/src/main/kotlin/org/rooftop/netx/api/SagaManager.kt @@ -0,0 +1,43 @@ +package org.rooftop.netx.api + +import reactor.core.publisher.Mono + +interface SagaManager { + + fun start(): Mono + + fun start(event: T): Mono + + fun syncStart(): String + + fun syncStart(event: T): String + + fun join(id: String): Mono + + fun join(id: String, event: T): Mono + + fun syncJoin(id: String): String + + fun syncJoin(id: String, event: T): String + + fun exists(id: String): Mono + + fun syncExists(id: String): String + + fun commit(id: String): Mono + + fun commit(id: String, event: T): Mono + + fun syncCommit(id: String): String + + fun syncCommit(id: String, event: T): String + + fun rollback(id: String, cause: String): Mono + + fun rollback(id: String, cause: String, event: T): Mono + + fun syncRollback(id: String, cause: String): String + + fun syncRollback(id: String, cause: String, event: T): String + +} diff --git a/src/main/kotlin/org/rooftop/netx/api/SagaRollbackEvent.kt b/src/main/kotlin/org/rooftop/netx/api/SagaRollbackEvent.kt new file mode 100644 index 0000000..998739c --- /dev/null +++ b/src/main/kotlin/org/rooftop/netx/api/SagaRollbackEvent.kt @@ -0,0 +1,14 @@ +package org.rooftop.netx.api + +class SagaRollbackEvent internal constructor( + id: String, + nodeName: String, + group: String, + event: String?, + val cause: String, + codec: Codec, +) : SagaEvent(id, nodeName, group, event, codec) { + + override fun copy(): SagaRollbackEvent = + SagaRollbackEvent(id, nodeName, group, event, cause, codec) +} diff --git a/src/main/kotlin/org/rooftop/netx/api/TransactionRollbackListener.kt b/src/main/kotlin/org/rooftop/netx/api/SagaRollbackListener.kt similarity index 79% rename from src/main/kotlin/org/rooftop/netx/api/TransactionRollbackListener.kt rename to src/main/kotlin/org/rooftop/netx/api/SagaRollbackListener.kt index 78b89b7..3f1c2c4 100644 --- a/src/main/kotlin/org/rooftop/netx/api/TransactionRollbackListener.kt +++ b/src/main/kotlin/org/rooftop/netx/api/SagaRollbackListener.kt @@ -4,6 +4,6 @@ import kotlin.reflect.KClass @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) -annotation class TransactionRollbackListener( +annotation class SagaRollbackListener( val event: KClass<*> = Any::class, ) diff --git a/src/main/kotlin/org/rooftop/netx/api/SagaStartEvent.kt b/src/main/kotlin/org/rooftop/netx/api/SagaStartEvent.kt new file mode 100644 index 0000000..2286c74 --- /dev/null +++ b/src/main/kotlin/org/rooftop/netx/api/SagaStartEvent.kt @@ -0,0 +1,13 @@ +package org.rooftop.netx.api + +class SagaStartEvent internal constructor( + id: String, + nodeName: String, + group: String, + event: String?, + codec: Codec, +) : SagaEvent(id, nodeName, group, event, codec) { + + override fun copy(): SagaStartEvent = + SagaStartEvent(id, nodeName, group, event, codec) +} diff --git a/src/main/kotlin/org/rooftop/netx/api/TransactionStartListener.kt b/src/main/kotlin/org/rooftop/netx/api/SagaStartListener.kt similarity index 87% rename from src/main/kotlin/org/rooftop/netx/api/TransactionStartListener.kt rename to src/main/kotlin/org/rooftop/netx/api/SagaStartListener.kt index 0ca54a4..92407d9 100644 --- a/src/main/kotlin/org/rooftop/netx/api/TransactionStartListener.kt +++ b/src/main/kotlin/org/rooftop/netx/api/SagaStartListener.kt @@ -4,7 +4,7 @@ import kotlin.reflect.KClass @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) -annotation class TransactionStartListener( +annotation class SagaStartListener( val event: KClass<*> = Any::class, val noRollbackFor: Array> = [], val successWith: SuccessWith = SuccessWith.PUBLISH_JOIN, diff --git a/src/main/kotlin/org/rooftop/netx/api/TransactionCommitEvent.kt b/src/main/kotlin/org/rooftop/netx/api/TransactionCommitEvent.kt deleted file mode 100644 index ba47177..0000000 --- a/src/main/kotlin/org/rooftop/netx/api/TransactionCommitEvent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.rooftop.netx.api - -class TransactionCommitEvent internal constructor( - transactionId: String, - nodeName: String, - group: String, - event: String?, - codec: Codec, -): TransactionEvent(transactionId, nodeName, group, event, codec) { - - override fun copy(): TransactionCommitEvent = - TransactionCommitEvent(transactionId, nodeName, group, event, codec) -} diff --git a/src/main/kotlin/org/rooftop/netx/api/TransactionJoinEvent.kt b/src/main/kotlin/org/rooftop/netx/api/TransactionJoinEvent.kt deleted file mode 100644 index 9a31120..0000000 --- a/src/main/kotlin/org/rooftop/netx/api/TransactionJoinEvent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.rooftop.netx.api - -class TransactionJoinEvent internal constructor( - transactionId: String, - nodeName: String, - group: String, - event: String?, - codec: Codec, -) : TransactionEvent(transactionId, nodeName, group, event, codec) { - - override fun copy(): TransactionJoinEvent = - TransactionJoinEvent(transactionId, nodeName, group, event, codec) -} diff --git a/src/main/kotlin/org/rooftop/netx/api/TransactionManager.kt b/src/main/kotlin/org/rooftop/netx/api/TransactionManager.kt deleted file mode 100644 index 25679e3..0000000 --- a/src/main/kotlin/org/rooftop/netx/api/TransactionManager.kt +++ /dev/null @@ -1,43 +0,0 @@ -package org.rooftop.netx.api - -import reactor.core.publisher.Mono - -interface TransactionManager { - - fun start(): Mono - - fun start(event: T): Mono - - fun syncStart(): String - - fun syncStart(event: T): String - - fun join(transactionId: String): Mono - - fun join(transactionId: String, event: T): Mono - - fun syncJoin(transactionId: String): String - - fun syncJoin(transactionId: String, event: T): String - - fun exists(transactionId: String): Mono - - fun syncExists(transactionId: String): String - - fun commit(transactionId: String): Mono - - fun commit(transactionId: String, event: T): Mono - - fun syncCommit(transactionId: String): String - - fun syncCommit(transactionId: String, event: T): String - - fun rollback(transactionId: String, cause: String): Mono - - fun rollback(transactionId: String, cause: String, event: T): Mono - - fun syncRollback(transactionId: String, cause: String): String - - fun syncRollback(transactionId: String, cause: String, event: T): String - -} diff --git a/src/main/kotlin/org/rooftop/netx/api/TransactionRollbackEvent.kt b/src/main/kotlin/org/rooftop/netx/api/TransactionRollbackEvent.kt deleted file mode 100644 index ced0e88..0000000 --- a/src/main/kotlin/org/rooftop/netx/api/TransactionRollbackEvent.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.rooftop.netx.api - -class TransactionRollbackEvent internal constructor( - transactionId: String, - nodeName: String, - group: String, - event: String?, - val cause: String, - codec: Codec, -) : TransactionEvent(transactionId, nodeName, group, event, codec) { - - override fun copy(): TransactionRollbackEvent = - TransactionRollbackEvent(transactionId, nodeName, group, event, cause, codec) -} diff --git a/src/main/kotlin/org/rooftop/netx/api/TransactionStartEvent.kt b/src/main/kotlin/org/rooftop/netx/api/TransactionStartEvent.kt deleted file mode 100644 index 8e1831d..0000000 --- a/src/main/kotlin/org/rooftop/netx/api/TransactionStartEvent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.rooftop.netx.api - -class TransactionStartEvent internal constructor( - transactionId: String, - nodeName: String, - group: String, - event: String?, - codec: Codec, -) : TransactionEvent(transactionId, nodeName, group, event, codec) { - - override fun copy(): TransactionStartEvent = - TransactionStartEvent(transactionId, nodeName, group, event, codec) -} diff --git a/src/main/kotlin/org/rooftop/netx/engine/AbstractDispatchFunction.kt b/src/main/kotlin/org/rooftop/netx/engine/AbstractDispatchFunction.kt index e842ecf..43b788d 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/AbstractDispatchFunction.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/AbstractDispatchFunction.kt @@ -1,7 +1,7 @@ package org.rooftop.netx.engine -import org.rooftop.netx.api.TransactionEvent -import org.rooftop.netx.api.TransactionManager +import org.rooftop.netx.api.SagaEvent +import org.rooftop.netx.api.SagaManager import reactor.core.scheduler.Schedulers import kotlin.reflect.KClass import kotlin.reflect.KFunction @@ -11,12 +11,12 @@ internal sealed class AbstractDispatchFunction( protected val function: KFunction, protected val handler: Any, private val noRollbackFor: Array>, - private val nextState: NextTransactionState, - private val transactionManager: TransactionManager, + private val nextState: NextSagaState, + private val sagaManager: SagaManager, ) { fun name(): String = function.name - abstract fun call(transactionEvent: TransactionEvent): T + abstract fun call(sagaEvent: SagaEvent): T protected fun isNoRollbackFor(throwable: Throwable): Boolean { return noRollbackFor.isNotEmpty() && throwable.cause != null && noRollbackFor.contains( @@ -24,35 +24,35 @@ internal sealed class AbstractDispatchFunction( ) } - protected fun isProcessable(transactionEvent: TransactionEvent): Boolean { + protected fun isProcessable(sagaEvent: SagaEvent): Boolean { return runCatching { - transactionEvent.decodeEvent(eventType) + sagaEvent.decodeEvent(eventType) }.onFailure { return it is NullPointerException && eventType == Any::class }.isSuccess } - protected fun rollback(transactionEvent: TransactionEvent, throwable: Throwable) { - transactionEvent.nextEvent?.let { - transactionManager.rollback(transactionEvent.transactionId, throwable.getCause(), it) + protected fun rollback(sagaEvent: SagaEvent, throwable: Throwable) { + sagaEvent.nextEvent?.let { + sagaManager.rollback(sagaEvent.id, throwable.getCause(), it) .subscribeOn(Schedulers.parallel()) .subscribe() - } ?: transactionManager.rollback(transactionEvent.transactionId, throwable.getCause()) + } ?: sagaManager.rollback(sagaEvent.id, throwable.getCause()) .subscribeOn(Schedulers.parallel()) .subscribe() } - protected fun publishNextTransaction(transactionEvent: TransactionEvent) { + protected fun publishNextSaga(sagaEvent: SagaEvent) { when (nextState) { - NextTransactionState.JOIN -> transactionEvent.nextEvent?.let { - transactionManager.join(transactionEvent.transactionId, it) - } ?: transactionManager.join(transactionEvent.transactionId) + NextSagaState.JOIN -> sagaEvent.nextEvent?.let { + sagaManager.join(sagaEvent.id, it) + } ?: sagaManager.join(sagaEvent.id) - NextTransactionState.COMMIT -> transactionEvent.nextEvent?.let { - transactionManager.commit(transactionEvent.transactionId, it) - } ?: transactionManager.commit(transactionEvent.transactionId) + NextSagaState.COMMIT -> sagaEvent.nextEvent?.let { + sagaManager.commit(sagaEvent.id, it) + } ?: sagaManager.commit(sagaEvent.id) - NextTransactionState.END -> return + NextSagaState.END -> return }.subscribeOn(Schedulers.parallel()) .subscribe() } @@ -61,7 +61,7 @@ internal sealed class AbstractDispatchFunction( return this.message ?: this.cause?.message ?: this::class.java.name } - internal enum class NextTransactionState { + internal enum class NextSagaState { JOIN, COMMIT, END diff --git a/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionDispatcher.kt b/src/main/kotlin/org/rooftop/netx/engine/AbstractSagaDispatcher.kt similarity index 50% rename from src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionDispatcher.kt rename to src/main/kotlin/org/rooftop/netx/engine/AbstractSagaDispatcher.kt index d84d411..6a30025 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionDispatcher.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/AbstractSagaDispatcher.kt @@ -2,8 +2,8 @@ package org.rooftop.netx.engine import jakarta.annotation.PostConstruct import org.rooftop.netx.api.* -import org.rooftop.netx.engine.core.Transaction -import org.rooftop.netx.engine.core.TransactionState +import org.rooftop.netx.engine.core.Saga +import org.rooftop.netx.engine.core.SagaState import org.rooftop.netx.engine.logging.info import org.rooftop.netx.engine.logging.warningOnError import reactor.core.publisher.Flux @@ -13,106 +13,106 @@ import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.reflect.full.declaredMemberFunctions -internal abstract class AbstractTransactionDispatcher( +internal abstract class AbstractSagaDispatcher( private val codec: Codec, - private val transactionManager: TransactionManager, + private val sagaManager: SagaManager, ) { private val functions = - mutableMapOf>>() + mutableMapOf>>() - fun dispatch(transaction: Transaction, messageId: String): Mono { - return Flux.fromIterable(functions[transaction.state] ?: listOf()) + fun dispatch(saga: Saga, messageId: String): Mono { + return Flux.fromIterable(functions[saga.state] ?: listOf()) .flatMap { function -> when (function) { is MonoDispatchFunction -> { - mapToTransactionEvent(transaction.copy()) + mapSagaEvent(saga.copy()) .callMono(function) - .warningOnError("Error occurred in TransactionHandler function \"${function.name()}\" with transaction id ${transaction.id}") + .warningOnError("Error occurred in SagaHandler function \"${function.name()}\" with saga id ${saga.id}") } is NotPublishDispatchFunction -> { - mapToTransactionEvent(transaction.copy()) + mapSagaEvent(saga.copy()) .callNotPublish(function) - .warningOnError("Error occurred in TransactionHandler function \"${function.name()}\" with transaction id ${transaction.id}") + .warningOnError("Error occurred in SagaHandler function \"${function.name()}\" with saga id ${saga.id}") } is OrchestrateDispatchFunction -> { - mapToTransactionEvent(transaction.copy()) + mapSagaEvent(saga.copy()) .callOrchestrate(function) - .warningOnError("Error occurred in TransactionHandler function \"${function.name()}\" with transaction id ${transaction.id}") + .warningOnError("Error occurred in SagaHandler function \"${function.name()}\" with saga id ${saga.id}") } } } .subscribeOn(Schedulers.boundedElastic()) - .ackWhenComplete(transaction, messageId) + .ackWhenComplete(saga, messageId) .then(Mono.just(DISPATCHED)) } private fun Flux<*>.ackWhenComplete( - transaction: Transaction, + saga: Saga, messageId: String ): Flux<*> = this.doOnComplete { - Mono.just(transaction to messageId) - .info("Ack transaction \"${transaction.id}\"") + Mono.just(saga to messageId) + .info("Ack saga \"${saga.id}\"") .flatMap { - ack(transaction, messageId) - .warningOnError("Fail to ack transaction \"${transaction.id}\"") + ack(saga, messageId) + .warningOnError("Fail to ack saga \"${saga.id}\"") } .subscribeOn(Schedulers.parallel()) .subscribe() } - private fun mapToTransactionEvent(transaction: Transaction): Mono { - return when (transaction.state) { - TransactionState.START -> Mono.just( - TransactionStartEvent( - transactionId = transaction.id, - nodeName = transaction.serverId, - group = transaction.group, - event = extractEvent(transaction), + private fun mapSagaEvent(saga: Saga): Mono { + return when (saga.state) { + SagaState.START -> Mono.just( + SagaStartEvent( + id = saga.id, + nodeName = saga.serverId, + group = saga.group, + event = extractEvent(saga), codec = codec, ) ) - TransactionState.COMMIT -> Mono.just( - TransactionCommitEvent( - transactionId = transaction.id, - nodeName = transaction.serverId, - group = transaction.group, - event = extractEvent(transaction), + SagaState.COMMIT -> Mono.just( + SagaCommitEvent( + id = saga.id, + nodeName = saga.serverId, + group = saga.group, + event = extractEvent(saga), codec = codec ) ) - TransactionState.JOIN -> Mono.just( - TransactionJoinEvent( - transactionId = transaction.id, - nodeName = transaction.serverId, - group = transaction.group, - event = extractEvent(transaction), + SagaState.JOIN -> Mono.just( + SagaJoinEvent( + id = saga.id, + nodeName = saga.serverId, + group = saga.group, + event = extractEvent(saga), codec = codec, ) ) - TransactionState.ROLLBACK -> + SagaState.ROLLBACK -> Mono.just( - TransactionRollbackEvent( - transactionId = transaction.id, - nodeName = transaction.serverId, - group = transaction.group, - event = extractEvent(transaction), - cause = transaction.cause - ?: throw NullPointerException("Null value on TransactionRollbackEvent's cause field"), + SagaRollbackEvent( + id = saga.id, + nodeName = saga.serverId, + group = saga.group, + event = extractEvent(saga), + cause = saga.cause + ?: throw NullPointerException("Null value on SagaRollbackEvent's cause field"), codec = codec, ) ) } } - private fun extractEvent(transaction: Transaction): String? { - return when (transaction.event != null) { - true -> transaction.event + private fun extractEvent(saga: Saga): String? { + return when (saga.event != null) { + true -> saga.event false -> null } } @@ -131,23 +131,23 @@ internal abstract class AbstractTransactionDispatcher( function.annotations .forEach { annotation -> runCatching { - val transactionState = getMatchedTransactionState(annotation) + val sagaState = getMatchedSagaState(annotation) val eventType = getEventType(annotation) val noRollbackFor = getNoRollbackFor(annotation) - val nextState = getNextTransactionState(annotation) - functions.putIfAbsent(transactionState, mutableListOf()) - functions[transactionState]?.add( + val nextState = getNextSagaState(annotation) + functions.putIfAbsent(sagaState, mutableListOf()) + functions[sagaState]?.add( OrchestrateDispatchFunction( eventType, function as KFunction>, handler, noRollbackFor, nextState, - transactionManager, + sagaManager, ) ) }.onFailure { - throw IllegalStateException("Cannot add Mono TransactionHandler", it) + throw IllegalStateException("Cannot add Mono SagaHandler", it) } } } @@ -155,9 +155,9 @@ internal abstract class AbstractTransactionDispatcher( @PostConstruct fun initHandler() { - val transactionHandler = findHandlers() - initMonoFunctions(transactionHandler) - initNotPublisherFunctions(transactionHandler) + val sagaHandlers = findHandlers() + initMonoFunctions(sagaHandlers) + initNotPublisherFunctions(sagaHandlers) functions.forEach { (_, function) -> val functionName = function.map { it.name() }.toList() info("Register functions names : \"${functionName}\"") @@ -176,23 +176,23 @@ internal abstract class AbstractTransactionDispatcher( function.annotations .forEach { annotation -> runCatching { - val transactionState = getMatchedTransactionState(annotation) + val sagaState = getMatchedSagaState(annotation) val eventType = getEventType(annotation) val noRollbackFor = getNoRollbackFor(annotation) - val nextState = getNextTransactionState(annotation) - functions.putIfAbsent(transactionState, mutableListOf()) - functions[transactionState]?.add( + val nextState = getNextSagaState(annotation) + functions.putIfAbsent(sagaState, mutableListOf()) + functions[sagaState]?.add( MonoDispatchFunction( eventType, function as KFunction>, handler, noRollbackFor, nextState, - transactionManager, + sagaManager, ) ) }.onFailure { - throw IllegalStateException("Cannot add Mono TransactionHandler", it) + throw IllegalStateException("Cannot add Mono SagaHandler", it) } } } @@ -211,23 +211,23 @@ internal abstract class AbstractTransactionDispatcher( function.annotations .forEach { annotation -> runCatching { - val transactionState = getMatchedTransactionState(annotation) + val sagaState = getMatchedSagaState(annotation) val eventType = getEventType(annotation) val noRollbackFor = getNoRollbackFor(annotation) - val nextState = getNextTransactionState(annotation) - functions.putIfAbsent(transactionState, mutableListOf()) - functions[transactionState]?.add( + val nextState = getNextSagaState(annotation) + functions.putIfAbsent(sagaState, mutableListOf()) + functions[sagaState]?.add( NotPublishDispatchFunction( eventType, function, handler, noRollbackFor, nextState, - transactionManager, + sagaManager, ) ) }.onFailure { - throw IllegalStateException("Cannot add TransactionHandler", it) + throw IllegalStateException("Cannot add SagaHandler", it) } } } @@ -238,59 +238,59 @@ internal abstract class AbstractTransactionDispatcher( private fun getEventType(annotation: Annotation): KClass<*> { return when (annotation) { - is TransactionStartListener -> annotation.event - is TransactionCommitListener -> annotation.event - is TransactionJoinListener -> annotation.event - is TransactionRollbackListener -> annotation.event - else -> throw notMatchedTransactionHandlerException + is SagaStartListener -> annotation.event + is SagaCommitListener -> annotation.event + is SagaJoinListener -> annotation.event + is SagaRollbackListener -> annotation.event + else -> throw notMatchedSagaHandlerException } } private fun getNoRollbackFor(annotation: Annotation): Array> { return when (annotation) { - is TransactionStartListener -> annotation.noRollbackFor - is TransactionCommitListener -> annotation.noRollbackFor - is TransactionJoinListener -> annotation.noRollbackFor - is TransactionRollbackListener -> emptyArray() - else -> throw notMatchedTransactionHandlerException + is SagaStartListener -> annotation.noRollbackFor + is SagaCommitListener -> annotation.noRollbackFor + is SagaJoinListener -> annotation.noRollbackFor + is SagaRollbackListener -> emptyArray() + else -> throw notMatchedSagaHandlerException } } - private fun getMatchedTransactionState(annotation: Annotation): TransactionState { + private fun getMatchedSagaState(annotation: Annotation): SagaState { return when (annotation) { - is TransactionStartListener -> TransactionState.START - is TransactionCommitListener -> TransactionState.COMMIT - is TransactionJoinListener -> TransactionState.JOIN - is TransactionRollbackListener -> TransactionState.ROLLBACK - else -> throw notMatchedTransactionHandlerException + is SagaStartListener -> SagaState.START + is SagaCommitListener -> SagaState.COMMIT + is SagaJoinListener -> SagaState.JOIN + is SagaRollbackListener -> SagaState.ROLLBACK + else -> throw notMatchedSagaHandlerException } } - private fun getNextTransactionState(annotation: Annotation): AbstractDispatchFunction.NextTransactionState { + private fun getNextSagaState(annotation: Annotation): AbstractDispatchFunction.NextSagaState { return when (annotation) { - is TransactionStartListener -> annotation.successWith.toNextTransactionState() - is TransactionJoinListener -> annotation.successWith.toNextTransactionState() - else -> AbstractDispatchFunction.NextTransactionState.END + is SagaStartListener -> annotation.successWith.toNextSagaState() + is SagaJoinListener -> annotation.successWith.toNextSagaState() + else -> AbstractDispatchFunction.NextSagaState.END } } - private fun SuccessWith.toNextTransactionState(): AbstractDispatchFunction.NextTransactionState { + private fun SuccessWith.toNextSagaState(): AbstractDispatchFunction.NextSagaState { return when (this) { - SuccessWith.PUBLISH_JOIN -> AbstractDispatchFunction.NextTransactionState.JOIN - SuccessWith.PUBLISH_COMMIT -> AbstractDispatchFunction.NextTransactionState.COMMIT - SuccessWith.END -> AbstractDispatchFunction.NextTransactionState.END + SuccessWith.PUBLISH_JOIN -> AbstractDispatchFunction.NextSagaState.JOIN + SuccessWith.PUBLISH_COMMIT -> AbstractDispatchFunction.NextSagaState.COMMIT + SuccessWith.END -> AbstractDispatchFunction.NextSagaState.END } } protected abstract fun ack( - transaction: Transaction, + saga: Saga, messageId: String - ): Mono> + ): Mono> private companion object { private const val DISPATCHED = "dispatched" - private val notMatchedTransactionHandlerException = - NotFoundDispatchFunctionException("Cannot find matched Transaction handler") + private val notMatchedSagaHandlerException = + NotFoundDispatchFunctionException("Cannot find matched Saga handler") } } diff --git a/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionListener.kt b/src/main/kotlin/org/rooftop/netx/engine/AbstractSagaListener.kt similarity index 65% rename from src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionListener.kt rename to src/main/kotlin/org/rooftop/netx/engine/AbstractSagaListener.kt index a511773..b038e0a 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionListener.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/AbstractSagaListener.kt @@ -1,6 +1,6 @@ package org.rooftop.netx.engine -import org.rooftop.netx.engine.core.Transaction +import org.rooftop.netx.engine.core.Saga import org.rooftop.netx.engine.logging.info import org.rooftop.netx.engine.logging.warningOnError import reactor.core.publisher.BufferOverflowStrategy @@ -8,9 +8,9 @@ import reactor.core.publisher.Flux import reactor.core.publisher.Mono import reactor.core.scheduler.Schedulers -internal abstract class AbstractTransactionListener( +internal abstract class AbstractSagaListener( private val backpressureSize: Int, - private val transactionDispatcher: AbstractTransactionDispatcher, + private val sagaDispatcher: AbstractSagaDispatcher, ) { fun subscribeStream() { @@ -18,18 +18,18 @@ internal abstract class AbstractTransactionListener( .publishOn(Schedulers.boundedElastic()) .onBackpressureBuffer(backpressureSize, BufferOverflowStrategy.DROP_LATEST) .doOnNext { - info("Listen transaction ${it.first}\nmessageId \"${it.second}\"") + info("Listen saga ${it.first}\nmessageId \"${it.second}\"") } - .flatMap { (transaction, messageId) -> - transactionDispatcher.dispatch(transaction, messageId) - .warningOnError("Error occurred when listen transaction ${transaction.id}") + .flatMap { (saga, messageId) -> + sagaDispatcher.dispatch(saga, messageId) + .warningOnError("Error occurred when listen saga id ${saga.id}") } .onErrorResume { Mono.empty() } .restartWhenTerminated() .subscribe() } - protected abstract fun receive(): Flux> + protected abstract fun receive(): Flux> private fun Flux.restartWhenTerminated(): Flux { return this.doAfterTerminate { diff --git a/src/main/kotlin/org/rooftop/netx/engine/AbstractSagaManager.kt b/src/main/kotlin/org/rooftop/netx/engine/AbstractSagaManager.kt new file mode 100644 index 0000000..7724ef2 --- /dev/null +++ b/src/main/kotlin/org/rooftop/netx/engine/AbstractSagaManager.kt @@ -0,0 +1,231 @@ +package org.rooftop.netx.engine + +import org.rooftop.netx.api.AlreadyCommittedSagaException +import org.rooftop.netx.api.Codec +import org.rooftop.netx.api.SagaException +import org.rooftop.netx.api.SagaManager +import org.rooftop.netx.engine.core.Saga +import org.rooftop.netx.engine.core.SagaState +import org.rooftop.netx.engine.logging.info +import org.rooftop.netx.engine.logging.infoOnError +import org.rooftop.netx.engine.logging.warningOnError +import reactor.core.publisher.Mono + +internal abstract class AbstractSagaManager( + private val codec: Codec, + private val nodeGroup: String, + private val nodeName: String, + private val sagaIdGenerator: SagaIdGenerator, +) : SagaManager { + + override fun syncStart(): String { + return start().block() + ?: throw SagaException("Cannot start saga") + } + + final override fun syncStart(event: T): String { + return start(event).block() + ?: throw SagaException("Cannot start saga \"$event\"") + } + + override fun syncJoin(id: String): String { + return join(id).block() + ?: throw SagaException("Cannot join saga \"$id\"") + } + + final override fun syncJoin(id: String, event: T): String { + return join(id, event).block() + ?: throw SagaException("Cannot join saga \"$id\", \"$event\"") + } + + final override fun syncExists(id: String): String { + return exists(id).block() + ?: throw SagaException("Cannot exists saga \"$id\"") + } + + final override fun syncCommit(id: String): String { + return commit(id).block() + ?: throw SagaException("Cannot commit saga \"$id\"") + } + + override fun syncCommit(id: String, event: T): String { + return commit(id, event).block() + ?: throw SagaException("Cannot commit saga \"$id\" \"$event\"") + } + + final override fun syncRollback(id: String, cause: String): String { + return rollback(id, cause).block() + ?: throw SagaException("Cannot rollback saga \"$id\", \"$cause\"") + } + + override fun syncRollback(id: String, cause: String, event: T): String { + return rollback(id, cause, event).block() + ?: throw SagaException("Cannot rollback saga \"$id\", \"$cause\" \"$event\"") + } + + override fun start(): Mono { + return startSaga(null) + .info("Start saga") + .contextWrite { it.put(CONTEXT_TX_KEY, sagaIdGenerator.generate()) } + } + + final override fun start(event: T): Mono { + return Mono.fromCallable { codec.encode(event) } + .flatMap { encodedEvent -> + startSaga(encodedEvent) + .info("Start saga event \"$event\"") + } + .contextWrite { it.put(CONTEXT_TX_KEY, sagaIdGenerator.generate()) } + } + + private fun startSaga(event: String?): Mono { + return Mono.deferContextual { Mono.just(it[CONTEXT_TX_KEY]) } + .flatMap { id -> + publishSaga( + id, Saga( + id = id, + serverId = nodeName, + group = nodeGroup, + state = SagaState.START, + event = event, + ) + ) + } + } + + override fun join(id: String): Mono { + return getAnySaga(id) + .map { + if (it == SagaState.COMMIT) { + throw AlreadyCommittedSagaException(id, it.name) + } + id + } + .warningOnError("Cannot join saga") + .flatMap { + joinSaga(id, null) + .info("Join saga id \"$id\"") + } + } + + override fun join(id: String, event: T): Mono { + return getAnySaga(id) + .map { + if (it == SagaState.COMMIT) { + throw AlreadyCommittedSagaException(id, it.name) + } + id + } + .warningOnError("Cannot join saga") + .map { codec.encode(event) } + .flatMap { + joinSaga(id, it) + .info("Join saga id \"$id\", event \"$event\"") + } + } + + private fun joinSaga(id: String, event: String?): Mono { + return publishSaga( + id, Saga( + id = id, + serverId = nodeName, + group = nodeGroup, + state = SagaState.JOIN, + event = event, + ) + ) + } + + final override fun rollback(id: String, cause: String): Mono { + return exists(id) + .infoOnError("Cannot rollback saga cause, saga \"$id\" is not exists") + .flatMap { + rollbackSaga(id, cause, null) + } + .info("Rollback saga \"$id\"") + .contextWrite { it.put(CONTEXT_TX_KEY, id) } + } + + override fun rollback(id: String, cause: String, event: T): Mono { + return exists(id) + .infoOnError("Cannot rollback saga cause, saga \"$id\" is not exists") + .map { codec.encode(event) } + .flatMap { encodedEvent -> + rollbackSaga(id, cause, encodedEvent) + } + .info("Rollback saga \"$id\"") + .contextWrite { it.put(CONTEXT_TX_KEY, id) } + } + + private fun rollbackSaga( + id: String, + cause: String, + event: String? + ): Mono { + return publishSaga( + id, Saga( + id = id, + serverId = nodeName, + group = nodeGroup, + state = SagaState.ROLLBACK, + cause = cause, + event = event + ) + ) + } + + final override fun commit(id: String): Mono { + return exists(id) + .infoOnError("Cannot commit saga cause, saga \"$id\" is not exists") + .flatMap { commitSaga(id, null) } + .info("Commit saga \"$id\"") + .contextWrite { it.put(CONTEXT_TX_KEY, id) } + } + + override fun commit(id: String, event: T): Mono { + return exists(id) + .infoOnError("Cannot commit saga cause, saga \"$id\" is not exists") + .map { codec.encode(event) } + .flatMap { encodedEvent -> + commitSaga(id, encodedEvent) + } + .info("Commit saga \"$id\"") + .contextWrite { it.put(CONTEXT_TX_KEY, id) } + } + + private fun commitSaga(id: String, event: String?): Mono { + return publishSaga( + id, Saga( + id = id, + serverId = nodeName, + group = nodeGroup, + state = SagaState.COMMIT, + event = event, + ) + ) + } + + final override fun exists(id: String): Mono { + return getAnySaga(id) + .infoOnError("There is no saga corresponding to id \"$id\"") + .mapSagaId() + .contextWrite { it.put(CONTEXT_TX_KEY, id) } + } + + protected abstract fun getAnySaga(id: String): Mono + + private fun Mono<*>.mapSagaId(): Mono { + return this.flatMap { + Mono.deferContextual { Mono.just(it[CONTEXT_TX_KEY]) } + } + } + + protected abstract fun publishSaga( + id: String, + saga: Saga, + ): Mono + + private companion object { + private const val CONTEXT_TX_KEY = "sagaId" + } +} diff --git a/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionRetrySupporter.kt b/src/main/kotlin/org/rooftop/netx/engine/AbstractSagaRetrySupporter.kt similarity index 67% rename from src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionRetrySupporter.kt rename to src/main/kotlin/org/rooftop/netx/engine/AbstractSagaRetrySupporter.kt index e1876d2..c99f1af 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionRetrySupporter.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/AbstractSagaRetrySupporter.kt @@ -1,7 +1,7 @@ package org.rooftop.netx.engine import jakarta.annotation.PreDestroy -import org.rooftop.netx.engine.core.Transaction +import org.rooftop.netx.engine.core.Saga import org.rooftop.netx.engine.logging.info import org.rooftop.netx.engine.logging.warningOnError import reactor.core.publisher.Flux @@ -12,35 +12,35 @@ import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit -internal abstract class AbstractTransactionRetrySupporter( +internal abstract class AbstractSagaRetrySupporter( private val backpressureSize: Int, private val recoveryMilli: Long, - private val transactionDispatcher: AbstractTransactionDispatcher, + private val sagaDispatcher: AbstractSagaDispatcher, ) { private lateinit var executor: ScheduledExecutorService; private lateinit var scheduledFuture: ScheduledFuture<*>; - fun watchOrphanTransaction() { + fun watchOrphanSaga() { this.executor = Executors.newSingleThreadScheduledExecutor() this.scheduledFuture = executor.scheduleWithFixedDelay( - handleOrphanTransaction(), + handleOrphanSaga(), 0, recoveryMilli, TimeUnit.MILLISECONDS, ) } - private fun handleOrphanTransaction(): Runnable { + private fun handleOrphanSaga(): Runnable { return Runnable { - claimOrphanTransaction(backpressureSize) + claimOrphanSaga(backpressureSize) .doOnNext { - info("Retry orphan transaction ${it.first}\nmessageId \"${it.second}\"") + info("Retry orphan saga ${it.first}\nmessageId \"${it.second}\"") } - .flatMap { (transaction, messageId) -> - transactionDispatcher.dispatch(transaction, messageId) - .warningOnError("Error occurred when retry orphan transaction \"${transaction.id}\"") + .flatMap { (saga, messageId) -> + sagaDispatcher.dispatch(saga, messageId) + .warningOnError("Error occurred when retry orphan saga \"${saga.id}\"") } .onErrorResume { Mono.empty() } .subscribeOn(Schedulers.immediate()) @@ -48,7 +48,7 @@ internal abstract class AbstractTransactionRetrySupporter( } } - protected abstract fun claimOrphanTransaction(backpressureSize: Int): Flux> + protected abstract fun claimOrphanSaga(backpressureSize: Int): Flux> @PreDestroy private fun shutdownGracefully() { @@ -58,14 +58,14 @@ internal abstract class AbstractTransactionRetrySupporter( if (!executor.awaitTermination(10, TimeUnit.MINUTES)) { executor.shutdownNow() if (!executor.awaitTermination(1, TimeUnit.MINUTES)) { - org.rooftop.netx.engine.logging.error("Cannot shutdown TransactionRetrySupporter thread") + org.rooftop.netx.engine.logging.error("Cannot shutdown SagaRetrySupporter thread") } } }.onFailure { executor.shutdownNow() Thread.currentThread().interrupt() }.onSuccess { - info("Shutdown TransactionRetrySupporter gracefully") + info("Shutdown SagaRetrySupporter gracefully") } } } diff --git a/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionManager.kt b/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionManager.kt deleted file mode 100644 index 05b7468..0000000 --- a/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionManager.kt +++ /dev/null @@ -1,231 +0,0 @@ -package org.rooftop.netx.engine - -import org.rooftop.netx.api.AlreadyCommittedTransactionException -import org.rooftop.netx.api.Codec -import org.rooftop.netx.api.TransactionException -import org.rooftop.netx.api.TransactionManager -import org.rooftop.netx.engine.core.Transaction -import org.rooftop.netx.engine.core.TransactionState -import org.rooftop.netx.engine.logging.info -import org.rooftop.netx.engine.logging.infoOnError -import org.rooftop.netx.engine.logging.warningOnError -import reactor.core.publisher.Mono - -internal abstract class AbstractTransactionManager( - private val codec: Codec, - private val nodeGroup: String, - private val nodeName: String, - private val transactionIdGenerator: TransactionIdGenerator, -) : TransactionManager { - - override fun syncStart(): String { - return start().block() - ?: throw TransactionException("Cannot start transaction") - } - - final override fun syncStart(event: T): String { - return start(event).block() - ?: throw TransactionException("Cannot start transaction \"$event\"") - } - - override fun syncJoin(transactionId: String): String { - return join(transactionId).block() - ?: throw TransactionException("Cannot join transaction \"$transactionId\"") - } - - final override fun syncJoin(transactionId: String, event: T): String { - return join(transactionId, event).block() - ?: throw TransactionException("Cannot join transaction \"$transactionId\", \"$event\"") - } - - final override fun syncExists(transactionId: String): String { - return exists(transactionId).block() - ?: throw TransactionException("Cannot exists transaction \"$transactionId\"") - } - - final override fun syncCommit(transactionId: String): String { - return commit(transactionId).block() - ?: throw TransactionException("Cannot commit transaction \"$transactionId\"") - } - - override fun syncCommit(transactionId: String, event: T): String { - return commit(transactionId, event).block() - ?: throw TransactionException("Cannot commit transaction \"$transactionId\" \"$event\"") - } - - final override fun syncRollback(transactionId: String, cause: String): String { - return rollback(transactionId, cause).block() - ?: throw TransactionException("Cannot rollback transaction \"$transactionId\", \"$cause\"") - } - - override fun syncRollback(transactionId: String, cause: String, event: T): String { - return rollback(transactionId, cause, event).block() - ?: throw TransactionException("Cannot rollback transaction \"$transactionId\", \"$cause\" \"$event\"") - } - - override fun start(): Mono { - return startTransaction(null) - .info("Start transaction") - .contextWrite { it.put(CONTEXT_TX_KEY, transactionIdGenerator.generate()) } - } - - final override fun start(event: T): Mono { - return Mono.fromCallable { codec.encode(event) } - .flatMap { encodedEvent -> - startTransaction(encodedEvent) - .info("Start transaction event \"$event\"") - } - .contextWrite { it.put(CONTEXT_TX_KEY, transactionIdGenerator.generate()) } - } - - private fun startTransaction(event: String?): Mono { - return Mono.deferContextual { Mono.just(it[CONTEXT_TX_KEY]) } - .flatMap { transactionId -> - publishTransaction( - transactionId, Transaction( - id = transactionId, - serverId = nodeName, - group = nodeGroup, - state = TransactionState.START, - event = event, - ) - ) - } - } - - override fun join(transactionId: String): Mono { - return getAnyTransaction(transactionId) - .map { - if (it == TransactionState.COMMIT) { - throw AlreadyCommittedTransactionException(transactionId, it.name) - } - transactionId - } - .warningOnError("Cannot join transaction") - .flatMap { - joinTransaction(transactionId, null) - .info("Join transaction transactionId \"$transactionId\"") - } - } - - override fun join(transactionId: String, event: T): Mono { - return getAnyTransaction(transactionId) - .map { - if (it == TransactionState.COMMIT) { - throw AlreadyCommittedTransactionException(transactionId, it.name) - } - transactionId - } - .warningOnError("Cannot join transaction") - .map { codec.encode(event) } - .flatMap { - joinTransaction(transactionId, it) - .info("Join transaction transactionId \"$transactionId\", event \"$event\"") - } - } - - private fun joinTransaction(transactionId: String, event: String?): Mono { - return publishTransaction( - transactionId, Transaction( - id = transactionId, - serverId = nodeName, - group = nodeGroup, - state = TransactionState.JOIN, - event = event, - ) - ) - } - - final override fun rollback(transactionId: String, cause: String): Mono { - return exists(transactionId) - .infoOnError("Cannot rollback transaction cause, transaction \"$transactionId\" is not exists") - .flatMap { - rollbackTransaction(transactionId, cause, null) - } - .info("Rollback transaction \"$transactionId\"") - .contextWrite { it.put(CONTEXT_TX_KEY, transactionId) } - } - - override fun rollback(transactionId: String, cause: String, event: T): Mono { - return exists(transactionId) - .infoOnError("Cannot rollback transaction cause, transaction \"$transactionId\" is not exists") - .map { codec.encode(event) } - .flatMap { encodedEvent -> - rollbackTransaction(transactionId, cause, encodedEvent) - } - .info("Rollback transaction \"$transactionId\"") - .contextWrite { it.put(CONTEXT_TX_KEY, transactionId) } - } - - private fun rollbackTransaction( - transactionId: String, - cause: String, - event: String? - ): Mono { - return publishTransaction( - transactionId, Transaction( - id = transactionId, - serverId = nodeName, - group = nodeGroup, - state = TransactionState.ROLLBACK, - cause = cause, - event = event - ) - ) - } - - final override fun commit(transactionId: String): Mono { - return exists(transactionId) - .infoOnError("Cannot commit transaction cause, transaction \"$transactionId\" is not exists") - .flatMap { commitTransaction(transactionId, null) } - .info("Commit transaction \"$transactionId\"") - .contextWrite { it.put(CONTEXT_TX_KEY, transactionId) } - } - - override fun commit(transactionId: String, event: T): Mono { - return exists(transactionId) - .infoOnError("Cannot commit transaction cause, transaction \"$transactionId\" is not exists") - .map { codec.encode(event) } - .flatMap { encodedEvent -> - commitTransaction(transactionId, encodedEvent) - } - .info("Commit transaction \"$transactionId\"") - .contextWrite { it.put(CONTEXT_TX_KEY, transactionId) } - } - - private fun commitTransaction(transactionId: String, event: String?): Mono { - return publishTransaction( - transactionId, Transaction( - id = transactionId, - serverId = nodeName, - group = nodeGroup, - state = TransactionState.COMMIT, - event = event, - ) - ) - } - - final override fun exists(transactionId: String): Mono { - return getAnyTransaction(transactionId) - .infoOnError("There is no transaction corresponding to transactionId \"$transactionId\"") - .mapTransactionId() - .contextWrite { it.put(CONTEXT_TX_KEY, transactionId) } - } - - protected abstract fun getAnyTransaction(transactionId: String): Mono - - private fun Mono<*>.mapTransactionId(): Mono { - return this.flatMap { - Mono.deferContextual { Mono.just(it["transactionId"]) } - } - } - - protected abstract fun publishTransaction( - transactionId: String, - transaction: Transaction, - ): Mono - - private companion object { - private const val CONTEXT_TX_KEY = "transactionId" - } -} diff --git a/src/main/kotlin/org/rooftop/netx/engine/DefaultOrchestrateChain.kt b/src/main/kotlin/org/rooftop/netx/engine/DefaultOrchestrateChain.kt index 08e4ffa..bc06ca8 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/DefaultOrchestrateChain.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/DefaultOrchestrateChain.kt @@ -44,7 +44,7 @@ class DefaultOrchestrateChain private constru function: TypeReified, ) = JoinOrchestrateListener( codec = chainContainer.codec, - transactionManager = chainContainer.transactionManager, + sagaManager = chainContainer.sagaManager, orchestratorId = orchestratorId, orchestrateSequence = orchestrateSequence + 1, orchestrateCommand = OrchestrateCommand( @@ -113,7 +113,7 @@ class DefaultOrchestrateChain private constru function: TypeReified, ) = MonoJoinOrchestrateListener( codec = chainContainer.codec, - transactionManager = chainContainer.transactionManager, + sagaManager = chainContainer.sagaManager, orchestratorId = orchestratorId, orchestrateSequence = orchestrateSequence + 1, monoOrchestrateCommand = MonoOrchestrateCommand( @@ -172,7 +172,7 @@ class DefaultOrchestrateChain private constru function: TypeReified, ) = CommitOrchestrateListener( codec = chainContainer.codec, - transactionManager = chainContainer.transactionManager, + sagaManager = chainContainer.sagaManager, orchestratorId = orchestratorId, orchestrateSequence = orchestrateSequence + 1, orchestrateCommand = OrchestrateCommand(commandType, chainContainer.codec, function), @@ -187,7 +187,7 @@ class DefaultOrchestrateChain private constru ) = rollback?.let { RollbackOrchestrateListener( codec = chainContainer.codec, - transactionManager = chainContainer.transactionManager, + sagaManager = chainContainer.sagaManager, orchestratorId = orchestratorId, orchestrateSequence = orchestrateSequence + 1, rollbackCommand = RollbackCommand(commandType, chainContainer.codec, it), @@ -214,7 +214,7 @@ class DefaultOrchestrateChain private constru val firstOrchestrators = nextDefaultOrchestrateChain.initOrchestrateListeners() return@cache OrchestratorManager( - transactionManager = chainContainer.transactionManager, + sagaManager = chainContainer.sagaManager, codec = chainContainer.codec, orchestratorId = orchestratorId, resultHolder = chainContainer.resultHolder, @@ -266,7 +266,7 @@ class DefaultOrchestrateChain private constru val firstOrchestrators = nextDefaultOrchestrateChain.initOrchestrateListeners() return@cache OrchestratorManager( - transactionManager = chainContainer.transactionManager, + sagaManager = chainContainer.sagaManager, codec = chainContainer.codec, orchestratorId = orchestratorId, resultHolder = chainContainer.resultHolder, @@ -373,8 +373,8 @@ class DefaultOrchestrateChain private constru private fun addDispatcher(orchestrateListeners: List, AbstractOrchestrateListener?>>) { orchestrateListeners.forEach { (listener, rollbackListener) -> - chainContainer.transactionDispatcher.addOrchestrate(listener) - rollbackListener?.let { chainContainer.transactionDispatcher.addOrchestrate(it) } + chainContainer.sagaDispatcher.addOrchestrate(listener) + rollbackListener?.let { chainContainer.sagaDispatcher.addOrchestrate(it) } } } @@ -383,7 +383,7 @@ class DefaultOrchestrateChain private constru function: TypeReified, ) = MonoCommitOrchestrateListener( codec = chainContainer.codec, - transactionManager = chainContainer.transactionManager, + sagaManager = chainContainer.sagaManager, orchestratorId = orchestratorId, orchestrateSequence = orchestrateSequence + 1, monoOrchestrateCommand = MonoOrchestrateCommand( @@ -402,7 +402,7 @@ class DefaultOrchestrateChain private constru ) = rollback?.let { MonoRollbackOrchestrateListener( codec = chainContainer.codec, - transactionManager = chainContainer.transactionManager, + sagaManager = chainContainer.sagaManager, orchestratorId = orchestratorId, orchestrateSequence = orchestrateSequence + 1, monoRollbackCommand = MonoRollbackCommand( @@ -418,8 +418,8 @@ class DefaultOrchestrateChain private constru internal class Pre internal constructor( private val orchestratorId: String, - private val transactionManager: TransactionManager, - private val transactionDispatcher: AbstractTransactionDispatcher, + private val sagaManager: SagaManager, + private val sagaDispatcher: AbstractSagaDispatcher, private val codec: Codec, private val resultHolder: ResultHolder, private val requestHolder: RequestHolder, @@ -467,7 +467,7 @@ class DefaultOrchestrateChain private constru function: TypeReified, ) = StartOrchestrateListener( codec = codec, - transactionManager = transactionManager, + sagaManager = sagaManager, orchestratorId = orchestratorId, orchestrateSequence = 0, orchestrateCommand = OrchestrateCommand( @@ -486,7 +486,7 @@ class DefaultOrchestrateChain private constru ) = rollback?.let { RollbackOrchestrateListener( codec = codec, - transactionManager = transactionManager, + sagaManager = sagaManager, orchestratorId = orchestratorId, orchestrateSequence = 0, rollbackCommand = RollbackCommand( @@ -541,7 +541,7 @@ class DefaultOrchestrateChain private constru function: TypeReified, ) = MonoStartOrchestrateListener( codec = codec, - transactionManager = transactionManager, + sagaManager = sagaManager, orchestratorId = orchestratorId, orchestrateSequence = 0, monoOrchestrateCommand = MonoOrchestrateCommand( @@ -560,7 +560,7 @@ class DefaultOrchestrateChain private constru ) = rollback?.let { MonoRollbackOrchestrateListener( codec = codec, - transactionManager = transactionManager, + sagaManager = sagaManager, orchestratorId = orchestratorId, orchestrateSequence = 0, monoRollbackCommand = MonoRollbackCommand( @@ -575,8 +575,8 @@ class DefaultOrchestrateChain private constru } private fun getStreamContainer(): ChainContainer = ChainContainer( - transactionManager, - transactionDispatcher, + sagaManager, + sagaDispatcher, codec, resultHolder, requestHolder, @@ -589,8 +589,8 @@ class DefaultOrchestrateChain private constru } private data class ChainContainer( - val transactionManager: TransactionManager, - val transactionDispatcher: AbstractTransactionDispatcher, + val sagaManager: SagaManager, + val sagaDispatcher: AbstractSagaDispatcher, val codec: Codec, val resultHolder: ResultHolder, val requestHolder: RequestHolder, diff --git a/src/main/kotlin/org/rooftop/netx/engine/MonoDispatchFunction.kt b/src/main/kotlin/org/rooftop/netx/engine/MonoDispatchFunction.kt index 2fefc4f..6444c7b 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/MonoDispatchFunction.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/MonoDispatchFunction.kt @@ -1,13 +1,13 @@ package org.rooftop.netx.engine -import org.rooftop.netx.api.TransactionEvent -import org.rooftop.netx.api.TransactionManager +import org.rooftop.netx.api.SagaEvent +import org.rooftop.netx.api.SagaManager import org.rooftop.netx.engine.logging.info import reactor.core.publisher.Mono import kotlin.reflect.KClass import kotlin.reflect.KFunction -internal fun Mono.callMono(function: MonoDispatchFunction): Mono<*> { +internal fun Mono.callMono(function: MonoDispatchFunction): Mono<*> { return this.flatMap { function.call(it) } @@ -18,34 +18,34 @@ internal class MonoDispatchFunction( function: KFunction>, handler: Any, noRetryFor: Array>, - nextState: NextTransactionState, - transactionManager: TransactionManager, + nextState: NextSagaState, + sagaManager: SagaManager, ) : AbstractDispatchFunction>( eventType, function, handler, noRetryFor, nextState, - transactionManager, + sagaManager, ) { - override fun call(transactionEvent: TransactionEvent): Mono<*> { - return Mono.just(transactionEvent) - .filter { isProcessable(transactionEvent) } - .map { transactionEvent.copy() } - .flatMap { function.call(handler, transactionEvent) } - .info("Call Mono TransactionHandler \"${name()}\" with transactionId \"${transactionEvent.transactionId}\"") + override fun call(sagaEvent: SagaEvent): Mono<*> { + return Mono.just(sagaEvent) + .filter { isProcessable(sagaEvent) } + .map { sagaEvent.copy() } + .flatMap { function.call(handler, sagaEvent) } + .info("Call Mono SagaHandler \"${name()}\" with id \"${sagaEvent.id}\"") .switchIfEmpty(`continue`) .doOnNext { - if (isProcessable(transactionEvent)) { - publishNextTransaction(transactionEvent) + if (isProcessable(sagaEvent)) { + publishNextSaga(sagaEvent) } } .onErrorResume { if (isNoRollbackFor(it)) { return@onErrorResume noRollbackFor } - rollback(transactionEvent, it) + rollback(sagaEvent, it) `continue` } } diff --git a/src/main/kotlin/org/rooftop/netx/engine/NotPublishDispatchFunction.kt b/src/main/kotlin/org/rooftop/netx/engine/NotPublishDispatchFunction.kt index cd263d2..6a4163b 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/NotPublishDispatchFunction.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/NotPublishDispatchFunction.kt @@ -1,13 +1,13 @@ package org.rooftop.netx.engine -import org.rooftop.netx.api.TransactionEvent -import org.rooftop.netx.api.TransactionManager +import org.rooftop.netx.api.SagaEvent +import org.rooftop.netx.api.SagaManager import org.rooftop.netx.engine.logging.info import reactor.core.publisher.Mono import kotlin.reflect.KClass import kotlin.reflect.KFunction -internal fun Mono.callNotPublish(function: NotPublishDispatchFunction): Mono<*> { +internal fun Mono.callNotPublish(function: NotPublishDispatchFunction): Mono<*> { return this.map { function.call(it) } } @@ -16,29 +16,29 @@ internal class NotPublishDispatchFunction( function: KFunction<*>, handler: Any, noRollbackFor: Array>, - nextState: NextTransactionState, - transactionManager: TransactionManager, + nextState: NextSagaState, + sagaManager: SagaManager, ) : AbstractDispatchFunction( eventType, function, handler, noRollbackFor, nextState, - transactionManager, + sagaManager, ) { - override fun call(transactionEvent: TransactionEvent): Any { - if (isProcessable(transactionEvent)) { + override fun call(sagaEvent: SagaEvent): Any { + if (isProcessable(sagaEvent)) { return runCatching { - function.call(handler, transactionEvent) - info("Call NotPublisher TransactionHandler \"${name()}\" with transactionId \"${transactionEvent.transactionId}\"") + function.call(handler, sagaEvent) + info("Call NotPublisher SagaHandler \"${name()}\" with id \"${sagaEvent.id}\"") }.fold( - onSuccess = { publishNextTransaction(transactionEvent) }, + onSuccess = { publishNextSaga(sagaEvent) }, onFailure = { if (isNoRollbackFor(it)) { return@fold NO_ROLLBACK_FOR } - rollback(transactionEvent, it) + rollback(sagaEvent, it) }, ) } diff --git a/src/main/kotlin/org/rooftop/netx/engine/OrchestrateDispatchFunction.kt b/src/main/kotlin/org/rooftop/netx/engine/OrchestrateDispatchFunction.kt index 6c88835..77ae940 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/OrchestrateDispatchFunction.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/OrchestrateDispatchFunction.kt @@ -1,13 +1,13 @@ package org.rooftop.netx.engine -import org.rooftop.netx.api.TransactionEvent -import org.rooftop.netx.api.TransactionManager +import org.rooftop.netx.api.SagaEvent +import org.rooftop.netx.api.SagaManager import org.rooftop.netx.engine.logging.info import reactor.core.publisher.Mono import kotlin.reflect.KClass import kotlin.reflect.KFunction -internal fun Mono.callOrchestrate(function: OrchestrateDispatchFunction): Mono<*> { +internal fun Mono.callOrchestrate(function: OrchestrateDispatchFunction): Mono<*> { return this.flatMap { function.call(it) } @@ -18,25 +18,25 @@ internal class OrchestrateDispatchFunction( function: KFunction>, handler: Any, noRetryFor: Array>, - nextState: NextTransactionState, - transactionManager: TransactionManager, + nextState: NextSagaState, + sagaManager: SagaManager, ) : AbstractDispatchFunction>( eventType, function, handler, noRetryFor, nextState, - transactionManager, + sagaManager, ) { - override fun call(transactionEvent: TransactionEvent): Mono<*> { - return Mono.just(transactionEvent) - .filter { isProcessable(transactionEvent) } - .map { transactionEvent.copy() } - .flatMap { function.call(handler, transactionEvent) } - .info("Call OrchestrateHandler \"${name()}\" with transactionId \"${transactionEvent.transactionId}\"") + override fun call(sagaEvent: SagaEvent): Mono<*> { + return Mono.just(sagaEvent) + .filter { isProcessable(sagaEvent) } + .map { sagaEvent.copy() } + .flatMap { function.call(handler, sagaEvent) } + .info("Call OrchestrateHandler \"${name()}\" with id \"${sagaEvent.id}\"") .map { - publishNextTransaction(transactionEvent) + publishNextSaga(sagaEvent) it } .switchIfEmpty(`continue`) diff --git a/src/main/kotlin/org/rooftop/netx/engine/OrchestratorFactory.kt b/src/main/kotlin/org/rooftop/netx/engine/OrchestratorFactory.kt index 4a0b43f..3c90546 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/OrchestratorFactory.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/OrchestratorFactory.kt @@ -4,8 +4,8 @@ import org.rooftop.netx.api.* import org.rooftop.netx.api.OrchestratorFactory class OrchestratorFactory internal constructor( - private val transactionManager: TransactionManager, - private val transactionDispatcher: AbstractTransactionDispatcher, + private val sagaManager: SagaManager, + private val sagaDispatcher: AbstractSagaDispatcher, private val codec: Codec, private val resultHolder: ResultHolder, private val requestHolder: RequestHolder, @@ -19,8 +19,8 @@ class OrchestratorFactory internal constructor( override fun create(orchestratorId: String): OrchestrateChain.Pre { return DefaultOrchestrateChain.Pre( orchestratorId = orchestratorId, - transactionManager = transactionManager, - transactionDispatcher = transactionDispatcher, + sagaManager = sagaManager, + sagaDispatcher = sagaDispatcher, codec = codec, resultHolder = resultHolder, requestHolder = requestHolder, diff --git a/src/main/kotlin/org/rooftop/netx/engine/OrchestratorManager.kt b/src/main/kotlin/org/rooftop/netx/engine/OrchestratorManager.kt index c4f6888..002ab8c 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/OrchestratorManager.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/OrchestratorManager.kt @@ -1,11 +1,12 @@ package org.rooftop.netx.engine import org.rooftop.netx.api.* +import org.rooftop.netx.engine.listen.AbstractOrchestrateListener import reactor.core.publisher.Mono import kotlin.time.Duration.Companion.milliseconds class OrchestratorManager internal constructor( - private val transactionManager: TransactionManager, + private val sagaManager: SagaManager, private val codec: Codec, private val orchestratorId: String, private val resultHolder: ResultHolder, @@ -13,43 +14,43 @@ class OrchestratorManager internal constructor( private val rollbackOrchestrateListener: AbstractOrchestrateListener?, ) : Orchestrator { - override fun transactionSync(request: T): Result { - return transaction(request).block() - ?: throw TransactionException("Cannot start transaction \"$request\"") + override fun sagaSync(request: T): Result { + return saga(request).block() + ?: throw SagaException("Cannot start saga \"$request\"") } - override fun transactionSync(timeoutMillis: Long, request: T): Result { - return transaction(timeoutMillis, request).block() - ?: throw TransactionException("Cannot start transaction \"$request\"") + override fun sagaSync(timeoutMillis: Long, request: T): Result { + return saga(timeoutMillis, request).block() + ?: throw SagaException("Cannot start saga \"$request\"") } - override fun transactionSync(request: T, context: MutableMap): Result { - return transaction(request, context).block() - ?: throw TransactionException("Cannot start transaction \"$request\"") + override fun sagaSync(request: T, context: MutableMap): Result { + return saga(request, context).block() + ?: throw SagaException("Cannot start saga \"$request\"") } - override fun transactionSync( + override fun sagaSync( timeoutMillis: Long, request: T, context: MutableMap ): Result { - return transaction(timeoutMillis, request, context).block() - ?: throw TransactionException("Cannot start transaction \"$request\"") + return saga(timeoutMillis, request, context).block() + ?: throw SagaException("Cannot start saga \"$request\"") } - override fun transaction(request: T): Mono> { - return transaction(TEN_SECONDS_TO_TIME_OUT, request, mutableMapOf()) + override fun saga(request: T): Mono> { + return saga(TEN_SECONDS_TO_TIME_OUT, request, mutableMapOf()) } - override fun transaction(timeoutMillis: Long, request: T): Mono> { - return transaction(timeoutMillis, request, mutableMapOf()) + override fun saga(timeoutMillis: Long, request: T): Mono> { + return saga(timeoutMillis, request, mutableMapOf()) } - override fun transaction(request: T, context: MutableMap): Mono> { - return transaction(TEN_SECONDS_TO_TIME_OUT, request, context) + override fun saga(request: T, context: MutableMap): Mono> { + return saga(TEN_SECONDS_TO_TIME_OUT, request, context) } - override fun transaction( + override fun saga( timeoutMillis: Long, request: T, context: MutableMap @@ -66,7 +67,7 @@ class OrchestratorManager internal constructor( context = codec.encode(context.mapValues { codec.encode(it.value) }) ) } - .flatMap { transactionManager.start(it) } + .flatMap { sagaManager.start(it) } .flatMap { resultHolder.getResult(timeoutMillis.milliseconds, it) } } diff --git a/src/main/kotlin/org/rooftop/netx/engine/ResultHolder.kt b/src/main/kotlin/org/rooftop/netx/engine/ResultHolder.kt index 8efe09d..2c8037c 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/ResultHolder.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/ResultHolder.kt @@ -6,9 +6,9 @@ import kotlin.time.Duration internal interface ResultHolder { - fun getResult(timeout: Duration, transactionId: String): Mono> + fun getResult(timeout: Duration, id: String): Mono> - fun setSuccessResult(transactionId: String, result: T): Mono + fun setSuccessResult(id: String, result: T): Mono - fun setFailResult(transactionId: String, result: T) : Mono + fun setFailResult(id: String, result: T) : Mono } diff --git a/src/main/kotlin/org/rooftop/netx/engine/TransactionIdGenerator.kt b/src/main/kotlin/org/rooftop/netx/engine/SagaIdGenerator.kt similarity index 89% rename from src/main/kotlin/org/rooftop/netx/engine/TransactionIdGenerator.kt rename to src/main/kotlin/org/rooftop/netx/engine/SagaIdGenerator.kt index 968a13a..6327639 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/TransactionIdGenerator.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/SagaIdGenerator.kt @@ -2,7 +2,7 @@ package org.rooftop.netx.engine import com.github.f4b6a3.tsid.TsidFactory -class TransactionIdGenerator( +class SagaIdGenerator( nodeId: Int, private val tsidFactory: TsidFactory = TsidFactory.newInstance256(nodeId), ) { diff --git a/src/main/kotlin/org/rooftop/netx/engine/core/Transaction.kt b/src/main/kotlin/org/rooftop/netx/engine/core/Saga.kt similarity index 72% rename from src/main/kotlin/org/rooftop/netx/engine/core/Transaction.kt rename to src/main/kotlin/org/rooftop/netx/engine/core/Saga.kt index dce8ef3..84c4c18 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/core/Transaction.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/core/Saga.kt @@ -1,10 +1,10 @@ package org.rooftop.netx.engine.core -internal data class Transaction( +internal data class Saga( val id: String, val serverId: String, val group: String, - val state: TransactionState, + val state: SagaState, val cause: String? = null, val event: String? = null, ) diff --git a/src/main/kotlin/org/rooftop/netx/engine/core/TransactionState.kt b/src/main/kotlin/org/rooftop/netx/engine/core/SagaState.kt similarity index 70% rename from src/main/kotlin/org/rooftop/netx/engine/core/TransactionState.kt rename to src/main/kotlin/org/rooftop/netx/engine/core/SagaState.kt index f5697df..d5f4d81 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/core/TransactionState.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/core/SagaState.kt @@ -1,6 +1,6 @@ package org.rooftop.netx.engine.core -internal enum class TransactionState { +internal enum class SagaState { JOIN, COMMIT, ROLLBACK, diff --git a/src/main/kotlin/org/rooftop/netx/engine/AbstractOrchestrateListener.kt b/src/main/kotlin/org/rooftop/netx/engine/listen/AbstractOrchestrateListener.kt similarity index 81% rename from src/main/kotlin/org/rooftop/netx/engine/AbstractOrchestrateListener.kt rename to src/main/kotlin/org/rooftop/netx/engine/listen/AbstractOrchestrateListener.kt index 1ef3baa..43decd5 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/AbstractOrchestrateListener.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/listen/AbstractOrchestrateListener.kt @@ -1,6 +1,9 @@ -package org.rooftop.netx.engine +package org.rooftop.netx.engine.listen import org.rooftop.netx.api.* +import org.rooftop.netx.engine.OrchestrateEvent +import org.rooftop.netx.engine.RequestHolder +import org.rooftop.netx.engine.ResultHolder import reactor.core.publisher.Mono import reactor.core.scheduler.Schedulers import kotlin.reflect.KClass @@ -9,7 +12,7 @@ internal abstract class AbstractOrchestrateListener internal c private val orchestratorId: String, internal val orchestrateSequence: Int, private val codec: Codec, - private val transactionManager: TransactionManager, + private val sagaManager: SagaManager, private val requestHolder: RequestHolder, private val resultHolder: ResultHolder, private val typeReference: TypeReference?, @@ -48,28 +51,28 @@ internal abstract class AbstractOrchestrateListener internal c } } - protected fun orchestrate(transactionEvent: TransactionEvent): Mono { - return transactionEvent.startWithOrchestrateEvent() + protected fun orchestrate(sagaEvent: SagaEvent): Mono { + return sagaEvent.startWithOrchestrateEvent() .filter { it.orchestrateSequence == orchestrateSequence && it.orchestratorId == orchestratorId } .mapReifiedRequest() .flatMap { (request, event) -> - holdRequestIfRollbackable(request, transactionEvent.transactionId) + holdRequestIfRollbackable(request, sagaEvent.id) .map { it to event } } .flatMap { (request, event) -> command(request, event) } .setNextCastableType() .doOnError { rollback( - transactionEvent.transactionId, + sagaEvent.id, it, - transactionEvent.decodeEvent(OrchestrateEvent::class) + sagaEvent.decodeEvent(OrchestrateEvent::class) ) } .toOrchestrateEvent() .map { - transactionEvent.setNextEvent(it) + sagaEvent.setNextEvent(it) } } @@ -86,9 +89,9 @@ internal abstract class AbstractOrchestrateListener internal c } } - protected fun Mono.getHeldRequest(transactionEvent: TransactionEvent): Mono> { + protected fun Mono.getHeldRequest(sagaEvent: SagaEvent): Mono> { return this.flatMap { event -> - val key = "${transactionEvent.transactionId}:$orchestrateSequence" + val key = "${sagaEvent.id}:$orchestrateSequence" if (typeReference == null) { return@flatMap requestHolder.getRequest(key, getCastableType()) .map { it to event } @@ -97,12 +100,12 @@ internal abstract class AbstractOrchestrateListener internal c } } - protected fun holdRequestIfRollbackable(request: T, transactionId: String): Mono { + protected fun holdRequestIfRollbackable(request: T, id: String): Mono { if (!isRollbackable) { Mono.just(request) } return requestHolder.setRequest( - "$transactionId:$orchestrateSequence", + "$id:$orchestrateSequence", request ) } @@ -129,7 +132,7 @@ internal abstract class AbstractOrchestrateListener internal c } ?: throw NullPointerException("Cannot cast \"$data\" cause, castableType is null") } - protected fun TransactionEvent.startWithOrchestrateEvent(): Mono = + protected fun SagaEvent.startWithOrchestrateEvent(): Mono = Mono.just(this.decodeEvent(OrchestrateEvent::class)) private fun Throwable.toEmptyStackTrace(): Throwable { @@ -138,7 +141,7 @@ internal abstract class AbstractOrchestrateListener internal c } protected fun rollback( - transactionId: String, + id: String, throwable: Throwable, orchestrateEvent: OrchestrateEvent, ) { @@ -149,18 +152,18 @@ internal abstract class AbstractOrchestrateListener internal c "", orchestrateEvent.context, ) - holdFailResult(transactionId, throwable) + holdFailResult(id, throwable) .flatMap { - transactionManager.rollback( - transactionId = transactionId, + sagaManager.rollback( + id = id, cause = throwable.message ?: throwable.localizedMessage, event = rollbackOrchestrateEvent ) }.subscribeOn(Schedulers.parallel()).subscribe() } - private fun holdFailResult(transactionId: String, throwable: Throwable): Mono { - return resultHolder.setFailResult(transactionId, throwable.toEmptyStackTrace()) + private fun holdFailResult(id: String, throwable: Throwable): Mono { + return resultHolder.setFailResult(id, throwable.toEmptyStackTrace()) } open fun withAnnotated(): AbstractOrchestrateListener { diff --git a/src/main/kotlin/org/rooftop/netx/engine/listen/CommitOrchestrateListener.kt b/src/main/kotlin/org/rooftop/netx/engine/listen/CommitOrchestrateListener.kt index 248e4aa..1e17cb0 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/listen/CommitOrchestrateListener.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/listen/CommitOrchestrateListener.kt @@ -1,7 +1,6 @@ package org.rooftop.netx.engine.listen import org.rooftop.netx.api.* -import org.rooftop.netx.engine.AbstractOrchestrateListener import org.rooftop.netx.engine.OrchestrateEvent import org.rooftop.netx.engine.RequestHolder import org.rooftop.netx.engine.ResultHolder @@ -9,7 +8,7 @@ import reactor.core.publisher.Mono internal class CommitOrchestrateListener internal constructor( codec: Codec, - transactionManager: TransactionManager, + sagaManager: SagaManager, private val orchestratorId: String, orchestrateSequence: Int, private val orchestrateCommand: OrchestrateCommand, @@ -20,19 +19,19 @@ internal class CommitOrchestrateListener internal constructor( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, ) { - @TransactionCommitListener(OrchestrateEvent::class) - fun listenCommitOrchestrateEvent(transactionCommitEvent: TransactionCommitEvent): Mono { - return transactionCommitEvent.startWithOrchestrateEvent() + @SagaCommitListener(OrchestrateEvent::class) + fun listenCommitOrchestrateEvent(sagaCommitEvent: SagaCommitEvent): Mono { + return sagaCommitEvent.startWithOrchestrateEvent() .filter { it.orchestrateSequence == orchestrateSequence && it.orchestratorId == orchestratorId } .mapReifiedRequest() .flatMap { (request, event) -> - holdRequestIfRollbackable(request, transactionCommitEvent.transactionId) + holdRequestIfRollbackable(request, sagaCommitEvent.id) .map { it to event } } .map { (request, event) -> @@ -40,13 +39,13 @@ internal class CommitOrchestrateListener internal constructor( } .doOnError { rollback( - transactionCommitEvent.transactionId, + sagaCommitEvent.id, it, - transactionCommitEvent.decodeEvent(OrchestrateEvent::class) + sagaCommitEvent.decodeEvent(OrchestrateEvent::class) ) } .flatMap { (response, _) -> - resultHolder.setSuccessResult(transactionCommitEvent.transactionId, response) + resultHolder.setSuccessResult(sagaCommitEvent.id, response) } } } diff --git a/src/main/kotlin/org/rooftop/netx/engine/listen/JoinOrchestrateListener.kt b/src/main/kotlin/org/rooftop/netx/engine/listen/JoinOrchestrateListener.kt index 566ffde..211f058 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/listen/JoinOrchestrateListener.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/listen/JoinOrchestrateListener.kt @@ -1,7 +1,6 @@ package org.rooftop.netx.engine.listen import org.rooftop.netx.api.* -import org.rooftop.netx.engine.AbstractOrchestrateListener import org.rooftop.netx.engine.OrchestrateEvent import org.rooftop.netx.engine.RequestHolder import org.rooftop.netx.engine.ResultHolder @@ -9,7 +8,7 @@ import reactor.core.publisher.Mono internal class JoinOrchestrateListener( private val codec: Codec, - private val transactionManager: TransactionManager, + private val sagaManager: SagaManager, private val orchestratorId: String, orchestrateSequence: Int, private val orchestrateCommand: OrchestrateCommand, @@ -20,7 +19,7 @@ internal class JoinOrchestrateListener( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, @@ -39,17 +38,17 @@ internal class JoinOrchestrateListener( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, ) { - @TransactionJoinListener( + @SagaJoinListener( event = OrchestrateEvent::class, successWith = SuccessWith.PUBLISH_JOIN ) - fun handleTransactionJoinEvent(transactionJoinEvent: TransactionJoinEvent): Mono { - return orchestrate(transactionJoinEvent) + fun handleSagaJoinEvent(sagaJoinEvent: SagaJoinEvent): Mono { + return orchestrate(sagaJoinEvent) } override fun command(request: T, event: OrchestrateEvent): Mono> { @@ -63,17 +62,17 @@ internal class JoinOrchestrateListener( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, ) { - @TransactionJoinListener( + @SagaJoinListener( event = OrchestrateEvent::class, successWith = SuccessWith.PUBLISH_COMMIT ) - fun handleTransactionJoinEvent(transactionJoinEvent: TransactionJoinEvent): Mono { - return orchestrate(transactionJoinEvent) + fun handleSagaJoinEvent(sagaJoinEvent: SagaJoinEvent): Mono { + return orchestrate(sagaJoinEvent) } override fun command(request: T, event: OrchestrateEvent): Mono> { diff --git a/src/main/kotlin/org/rooftop/netx/engine/listen/MonoCommitOrchestrateListener.kt b/src/main/kotlin/org/rooftop/netx/engine/listen/MonoCommitOrchestrateListener.kt index 70fbadf..f40f410 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/listen/MonoCommitOrchestrateListener.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/listen/MonoCommitOrchestrateListener.kt @@ -1,7 +1,6 @@ package org.rooftop.netx.engine.listen import org.rooftop.netx.api.* -import org.rooftop.netx.engine.AbstractOrchestrateListener import org.rooftop.netx.engine.OrchestrateEvent import org.rooftop.netx.engine.RequestHolder import org.rooftop.netx.engine.ResultHolder @@ -9,7 +8,7 @@ import reactor.core.publisher.Mono internal class MonoCommitOrchestrateListener internal constructor( codec: Codec, - transactionManager: TransactionManager, + sagaManager: SagaManager, private val orchestratorId: String, orchestrateSequence: Int, private val monoOrchestrateCommand: MonoOrchestrateCommand, @@ -20,18 +19,18 @@ internal class MonoCommitOrchestrateListener internal construc orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, ) { - @TransactionCommitListener(OrchestrateEvent::class) - fun listenCommitOrchestrateEvent(transactionCommitEvent: TransactionCommitEvent): Mono { - return transactionCommitEvent.startWithOrchestrateEvent() + @SagaCommitListener(OrchestrateEvent::class) + fun listenCommitOrchestrateEvent(sagaCommitEvent: SagaCommitEvent): Mono { + return sagaCommitEvent.startWithOrchestrateEvent() .filter { it.orchestrateSequence == orchestrateSequence && it.orchestratorId == orchestratorId } .mapReifiedRequest() .flatMap { (request, event) -> - holdRequestIfRollbackable(request, transactionCommitEvent.transactionId) + holdRequestIfRollbackable(request, sagaCommitEvent.id) .map { it to event } } .flatMap { (request, event) -> @@ -39,13 +38,13 @@ internal class MonoCommitOrchestrateListener internal construc } .doOnError { rollback( - transactionCommitEvent.transactionId, + sagaCommitEvent.id, it, - transactionCommitEvent.decodeEvent(OrchestrateEvent::class) + sagaCommitEvent.decodeEvent(OrchestrateEvent::class) ) } .flatMap { (response, _) -> - resultHolder.setSuccessResult(transactionCommitEvent.transactionId, response) + resultHolder.setSuccessResult(sagaCommitEvent.id, response) } } } diff --git a/src/main/kotlin/org/rooftop/netx/engine/listen/MonoJoinOrchestrateListener.kt b/src/main/kotlin/org/rooftop/netx/engine/listen/MonoJoinOrchestrateListener.kt index 8bb0192..f7f8ffa 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/listen/MonoJoinOrchestrateListener.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/listen/MonoJoinOrchestrateListener.kt @@ -1,7 +1,6 @@ package org.rooftop.netx.engine.listen import org.rooftop.netx.api.* -import org.rooftop.netx.engine.AbstractOrchestrateListener import org.rooftop.netx.engine.OrchestrateEvent import org.rooftop.netx.engine.RequestHolder import org.rooftop.netx.engine.ResultHolder @@ -9,7 +8,7 @@ import reactor.core.publisher.Mono internal class MonoJoinOrchestrateListener( private val codec: Codec, - private val transactionManager: TransactionManager, + private val sagaManager: SagaManager, private val orchestratorId: String, orchestrateSequence: Int, private val monoOrchestrateCommand: MonoOrchestrateCommand, @@ -20,7 +19,7 @@ internal class MonoJoinOrchestrateListener( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, @@ -39,17 +38,17 @@ internal class MonoJoinOrchestrateListener( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, ) { - @TransactionJoinListener( + @SagaJoinListener( event = OrchestrateEvent::class, successWith = SuccessWith.PUBLISH_JOIN ) - fun handleTransactionJoinEvent(transactionJoinEvent: TransactionJoinEvent): Mono { - return orchestrate(transactionJoinEvent) + fun handleSagaJoinEvent(sagaJoinEvent: SagaJoinEvent): Mono { + return orchestrate(sagaJoinEvent) } override fun command(request: T, event: OrchestrateEvent): Mono> { @@ -63,17 +62,17 @@ internal class MonoJoinOrchestrateListener( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, ) { - @TransactionJoinListener( + @SagaJoinListener( event = OrchestrateEvent::class, successWith = SuccessWith.PUBLISH_COMMIT ) - fun handleTransactionJoinEvent(transactionJoinEvent: TransactionJoinEvent): Mono { - return orchestrate(transactionJoinEvent) + fun handleSagaJoinEvent(sagaJoinEvent: SagaJoinEvent): Mono { + return orchestrate(sagaJoinEvent) } override fun command(request: T, event: OrchestrateEvent): Mono> { diff --git a/src/main/kotlin/org/rooftop/netx/engine/listen/MonoRollbackOrchestrateListener.kt b/src/main/kotlin/org/rooftop/netx/engine/listen/MonoRollbackOrchestrateListener.kt index f395809..180f98f 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/listen/MonoRollbackOrchestrateListener.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/listen/MonoRollbackOrchestrateListener.kt @@ -1,7 +1,6 @@ package org.rooftop.netx.engine.listen import org.rooftop.netx.api.* -import org.rooftop.netx.engine.AbstractOrchestrateListener import org.rooftop.netx.engine.OrchestrateEvent import org.rooftop.netx.engine.RequestHolder import org.rooftop.netx.engine.ResultHolder @@ -11,7 +10,7 @@ internal class MonoRollbackOrchestrateListener( private val codec: Codec, private val orchestratorId: String, orchestrateSequence: Int, - private val transactionManager: TransactionManager, + private val sagaManager: SagaManager, private val monoRollbackCommand: MonoRollbackCommand, requestHolder: RequestHolder, resultHolder: ResultHolder, @@ -20,36 +19,36 @@ internal class MonoRollbackOrchestrateListener( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, ) { - @TransactionRollbackListener(OrchestrateEvent::class) - fun listenRollbackOrchestrateEvent(transactionRollbackEvent: TransactionRollbackEvent): Mono { - return transactionRollbackEvent.startWithOrchestrateEvent() + @SagaRollbackListener(OrchestrateEvent::class) + fun listenRollbackOrchestrateEvent(sagaRollbackEvent: SagaRollbackEvent): Mono { + return sagaRollbackEvent.startWithOrchestrateEvent() .filter { it.orchestratorId == orchestratorId && it.orchestrateSequence == orchestrateSequence } - .getHeldRequest(transactionRollbackEvent) + .getHeldRequest(sagaRollbackEvent) .flatMap { (request, event) -> monoRollbackCommand.command(request, event.context) } - .cascadeRollback(transactionRollbackEvent) + .cascadeRollback(sagaRollbackEvent) } - private fun Mono>.cascadeRollback(transactionRollbackEvent: TransactionRollbackEvent): Mono { + private fun Mono>.cascadeRollback(sagaRollbackEvent: SagaRollbackEvent): Mono { return this.filter { !isFirst } .flatMap { (_, context) -> - val orchestrateEvent = transactionRollbackEvent.decodeEvent(OrchestrateEvent::class) + val orchestrateEvent = sagaRollbackEvent.decodeEvent(OrchestrateEvent::class) val nextOrchestrateEvent = OrchestrateEvent( orchestrateEvent.orchestratorId, beforeRollbackOrchestrateSequence, orchestrateEvent.clientEvent, codec.encode(context.contexts), ) - transactionManager.rollback( - transactionRollbackEvent.transactionId, - transactionRollbackEvent.cause, + sagaManager.rollback( + sagaRollbackEvent.id, + sagaRollbackEvent.cause, nextOrchestrateEvent ) }.map { } diff --git a/src/main/kotlin/org/rooftop/netx/engine/listen/MonoStartOrchestrateListener.kt b/src/main/kotlin/org/rooftop/netx/engine/listen/MonoStartOrchestrateListener.kt index 71f8630..192698d 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/listen/MonoStartOrchestrateListener.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/listen/MonoStartOrchestrateListener.kt @@ -1,7 +1,6 @@ package org.rooftop.netx.engine.listen import org.rooftop.netx.api.* -import org.rooftop.netx.engine.AbstractOrchestrateListener import org.rooftop.netx.engine.OrchestrateEvent import org.rooftop.netx.engine.RequestHolder import org.rooftop.netx.engine.ResultHolder @@ -9,7 +8,7 @@ import reactor.core.publisher.Mono internal class MonoStartOrchestrateListener( private val codec: Codec, - private val transactionManager: TransactionManager, + private val sagaManager: SagaManager, private val orchestratorId: String, orchestrateSequence: Int, private val monoOrchestrateCommand: MonoOrchestrateCommand, @@ -20,7 +19,7 @@ internal class MonoStartOrchestrateListener( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, @@ -39,17 +38,17 @@ internal class MonoStartOrchestrateListener( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, ) { - @TransactionStartListener( + @SagaStartListener( event = OrchestrateEvent::class, successWith = SuccessWith.PUBLISH_JOIN ) - fun handleTransactionStartEvent(transactionStartEvent: TransactionStartEvent): Mono { - return orchestrate(transactionStartEvent) + fun handleSagaStartEvent(sagaStartEvent: SagaStartEvent): Mono { + return orchestrate(sagaStartEvent) } override fun command(request: T, event: OrchestrateEvent): Mono> { @@ -63,17 +62,17 @@ internal class MonoStartOrchestrateListener( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, ) { - @TransactionStartListener( + @SagaStartListener( event = OrchestrateEvent::class, successWith = SuccessWith.PUBLISH_COMMIT ) - fun handleTransactionStartEvent(transactionStartEvent: TransactionStartEvent): Mono { - return orchestrate(transactionStartEvent) + fun handleSagaStartEvent(sagaStartEvent: SagaStartEvent): Mono { + return orchestrate(sagaStartEvent) } override fun command(request: T, event: OrchestrateEvent): Mono> { diff --git a/src/main/kotlin/org/rooftop/netx/engine/listen/RollbackOrchestrateListener.kt b/src/main/kotlin/org/rooftop/netx/engine/listen/RollbackOrchestrateListener.kt index 5d698de..ff8693a 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/listen/RollbackOrchestrateListener.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/listen/RollbackOrchestrateListener.kt @@ -1,7 +1,6 @@ package org.rooftop.netx.engine.listen import org.rooftop.netx.api.* -import org.rooftop.netx.engine.AbstractOrchestrateListener import org.rooftop.netx.engine.OrchestrateEvent import org.rooftop.netx.engine.RequestHolder import org.rooftop.netx.engine.ResultHolder @@ -11,7 +10,7 @@ internal class RollbackOrchestrateListener( private val codec: Codec, private val orchestratorId: String, orchestrateSequence: Int, - private val transactionManager: TransactionManager, + private val sagaManager: SagaManager, private val rollbackCommand: RollbackCommand, requestHolder: RequestHolder, resultHolder: ResultHolder, @@ -20,17 +19,17 @@ internal class RollbackOrchestrateListener( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, ) { - @TransactionRollbackListener(OrchestrateEvent::class) - fun listenRollbackOrchestrateEvent(transactionRollbackEvent: TransactionRollbackEvent): Mono { - return transactionRollbackEvent.startWithOrchestrateEvent() + @SagaRollbackListener(OrchestrateEvent::class) + fun listenRollbackOrchestrateEvent(sagaRollbackEvent: SagaRollbackEvent): Mono { + return sagaRollbackEvent.startWithOrchestrateEvent() .filter { it.orchestratorId == orchestratorId && it.orchestrateSequence == orchestrateSequence } - .getHeldRequest(transactionRollbackEvent) + .getHeldRequest(sagaRollbackEvent) .map { (request, event) -> rollbackCommand.command(request, event.context) } @@ -40,22 +39,22 @@ internal class RollbackOrchestrateListener( } it } - .cascadeRollback(transactionRollbackEvent) + .cascadeRollback(sagaRollbackEvent) } - private fun Mono>.cascadeRollback(transactionRollbackEvent: TransactionRollbackEvent): Mono { + private fun Mono>.cascadeRollback(sagaRollbackEvent: SagaRollbackEvent): Mono { return this.filter { !isFirst } .flatMap { (_, context) -> - val orchestrateEvent = transactionRollbackEvent.decodeEvent(OrchestrateEvent::class) + val orchestrateEvent = sagaRollbackEvent.decodeEvent(OrchestrateEvent::class) val nextOrchestrateEvent = OrchestrateEvent( orchestrateEvent.orchestratorId, beforeRollbackOrchestrateSequence, orchestrateEvent.clientEvent, codec.encode(context.contexts), ) - transactionManager.rollback( - transactionRollbackEvent.transactionId, - transactionRollbackEvent.cause, + sagaManager.rollback( + sagaRollbackEvent.id, + sagaRollbackEvent.cause, nextOrchestrateEvent ) }.map { } diff --git a/src/main/kotlin/org/rooftop/netx/engine/listen/StartOrchestrateListener.kt b/src/main/kotlin/org/rooftop/netx/engine/listen/StartOrchestrateListener.kt index 8ee6d9c..91e8404 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/listen/StartOrchestrateListener.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/listen/StartOrchestrateListener.kt @@ -1,7 +1,6 @@ package org.rooftop.netx.engine.listen import org.rooftop.netx.api.* -import org.rooftop.netx.engine.AbstractOrchestrateListener import org.rooftop.netx.engine.OrchestrateEvent import org.rooftop.netx.engine.RequestHolder import org.rooftop.netx.engine.ResultHolder @@ -9,7 +8,7 @@ import reactor.core.publisher.Mono internal class StartOrchestrateListener( private val codec: Codec, - private val transactionManager: TransactionManager, + private val sagaManager: SagaManager, private val orchestratorId: String, orchestrateSequence: Int, private val orchestrateCommand: OrchestrateCommand, @@ -20,7 +19,7 @@ internal class StartOrchestrateListener( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, @@ -39,17 +38,17 @@ internal class StartOrchestrateListener( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, ) { - @TransactionStartListener( + @SagaStartListener( event = OrchestrateEvent::class, successWith = SuccessWith.PUBLISH_JOIN ) - fun handleTransactionStartEvent(transactionStartEvent: TransactionStartEvent): Mono { - return orchestrate(transactionStartEvent) + fun handleSagaStartEvent(sagaStartEvent: SagaStartEvent): Mono { + return orchestrate(sagaStartEvent) } override fun command(request: T, event: OrchestrateEvent): Mono> { @@ -63,17 +62,17 @@ internal class StartOrchestrateListener( orchestratorId, orchestrateSequence, codec, - transactionManager, + sagaManager, requestHolder, resultHolder, typeReference, ) { - @TransactionStartListener( + @SagaStartListener( event = OrchestrateEvent::class, successWith = SuccessWith.PUBLISH_COMMIT ) - fun handleTransactionStartEvent(transactionStartEvent: TransactionStartEvent): Mono { - return orchestrate(transactionStartEvent) + fun handleSagaStartEvent(sagaStartEvent: SagaStartEvent): Mono { + return orchestrate(sagaStartEvent) } override fun command(request: T, event: OrchestrateEvent): Mono> { diff --git a/src/main/kotlin/org/rooftop/netx/meta/EnableDistributedTransaction.kt b/src/main/kotlin/org/rooftop/netx/meta/EnableSaga.kt similarity index 51% rename from src/main/kotlin/org/rooftop/netx/meta/EnableDistributedTransaction.kt rename to src/main/kotlin/org/rooftop/netx/meta/EnableSaga.kt index b36845c..7391d5a 100644 --- a/src/main/kotlin/org/rooftop/netx/meta/EnableDistributedTransaction.kt +++ b/src/main/kotlin/org/rooftop/netx/meta/EnableSaga.kt @@ -1,9 +1,9 @@ package org.rooftop.netx.meta -import org.rooftop.netx.redis.RedisTransactionConfigurer +import org.rooftop.netx.redis.RedisSagaConfigurer import org.springframework.context.annotation.Import @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) -@Import(RedisTransactionConfigurer::class) -annotation class EnableDistributedTransaction +@Import(RedisSagaConfigurer::class) +annotation class EnableSaga diff --git a/src/main/kotlin/org/rooftop/netx/meta/TransactionHandler.kt b/src/main/kotlin/org/rooftop/netx/meta/SagaHandler.kt similarity index 81% rename from src/main/kotlin/org/rooftop/netx/meta/TransactionHandler.kt rename to src/main/kotlin/org/rooftop/netx/meta/SagaHandler.kt index fc0f6b3..294b29e 100644 --- a/src/main/kotlin/org/rooftop/netx/meta/TransactionHandler.kt +++ b/src/main/kotlin/org/rooftop/netx/meta/SagaHandler.kt @@ -5,4 +5,4 @@ import org.springframework.stereotype.Component @Component @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) -annotation class TransactionHandler +annotation class SagaHandler diff --git a/src/main/kotlin/org/rooftop/netx/redis/RedisResultHolder.kt b/src/main/kotlin/org/rooftop/netx/redis/RedisResultHolder.kt index 316225b..3465595 100644 --- a/src/main/kotlin/org/rooftop/netx/redis/RedisResultHolder.kt +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisResultHolder.kt @@ -28,10 +28,10 @@ class RedisResultHolder( override fun getResult( timeout: Duration, - transactionId: String + id: String ): Mono> { return pool.withPoolable { - it.leftPop("Netx:Result:$transactionId", timeout.toJavaDuration()) + it.leftPop("Netx:Result:$id", timeout.toJavaDuration()) .switchIfEmpty(Mono.error { ResultTimeoutException( "Cannot get result in \"$timeout\" time", @@ -61,18 +61,18 @@ class RedisResultHolder( } override fun setSuccessResult( - transactionId: String, + id: String, result: T ): Mono { return reactiveRedisTemplate.opsForList() .leftPush( - "Netx:Result:$transactionId", + "Netx:Result:$id", "$SUCCESS:${objectMapper.writeValueAsString(result)}" ).map { result } .doOnNext { info("Set success result $it") } } - override fun setFailResult(transactionId: String, result: T): Mono { + override fun setFailResult(id: String, result: T): Mono { val error = Error( objectMapper.writeValueAsString(result::class.java), objectMapper.writeValueAsString(result) @@ -80,7 +80,7 @@ class RedisResultHolder( val encodedError = objectMapper.writeValueAsString(error) return reactiveRedisTemplate.opsForList() .leftPush( - "Netx:Result:$transactionId", + "Netx:Result:$id", "$FAIL:$encodedError" ).map { result } .doOnNext { info("Set fail result $it") } diff --git a/src/main/kotlin/org/rooftop/netx/redis/RedisTransactionConfigurer.kt b/src/main/kotlin/org/rooftop/netx/redis/RedisSagaConfigurer.kt similarity index 73% rename from src/main/kotlin/org/rooftop/netx/redis/RedisTransactionConfigurer.kt rename to src/main/kotlin/org/rooftop/netx/redis/RedisSagaConfigurer.kt index 3950327..e905552 100644 --- a/src/main/kotlin/org/rooftop/netx/redis/RedisTransactionConfigurer.kt +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisSagaConfigurer.kt @@ -5,9 +5,9 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.paramnames.ParameterNamesModule -import org.rooftop.netx.api.TransactionManager +import org.rooftop.netx.api.SagaManager import org.rooftop.netx.engine.* -import org.rooftop.netx.engine.core.Transaction +import org.rooftop.netx.engine.core.Saga import org.rooftop.netx.engine.logging.LoggerFactory import org.rooftop.netx.engine.logging.info import org.rooftop.netx.engine.logging.logger @@ -27,7 +27,7 @@ import org.springframework.data.redis.serializer.StringRedisSerializer @Configuration -internal class RedisTransactionConfigurer( +internal class RedisSagaConfigurer( @Value("\${netx.host}") private val host: String, @Value("\${netx.port}") private val port: String, @Value("\${netx.password:0000}") private val password: String, @@ -49,8 +49,8 @@ internal class RedisTransactionConfigurer( @Bean @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") fun redisStreamOrchestratorFactory(): OrchestratorFactory = OrchestratorFactory( - transactionManager = redisStreamTransactionManager(), - transactionDispatcher = redisStreamTransactionDispatcher(), + sagaManager = redisStreamSagaManager(), + sagaDispatcher = redisStreamSagaDispatcher(), codec = jsonCodec(), resultHolder = redisResultHolder(), requestHolder = redisRequestHolder(), @@ -58,35 +58,35 @@ internal class RedisTransactionConfigurer( @Bean @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") - fun redisStreamTransactionManager(): TransactionManager = - RedisStreamTransactionManager( + fun redisStreamSagaManager(): SagaManager = + RedisStreamSagaManager( nodeName = nodeName, nodeGroup = nodeGroup, - reactiveRedisTemplate = transactionReactiveRedisTemplate(), + reactiveRedisTemplate = sagaReactiveRedisTemplate(), codec = jsonCodec(), - transactionIdGenerator = tsidTransactionIdGenerator(), + sagaIdGenerator = tsidSagaIdGenerator(), objectMapper = netxObjectMapper(), ).also { - info("RedisStreamTransactionManager connect to host : \"$host\" port : \"$port\" nodeName : \"$nodeName\" nodeGroup : \"$nodeGroup\"") + info("RedisStreamSagaManager connect to host : \"$host\" port : \"$port\" nodeName : \"$nodeName\" nodeGroup : \"$nodeGroup\"") } @Bean @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") - fun tsidTransactionIdGenerator(): TransactionIdGenerator = TransactionIdGenerator(nodeId) + fun tsidSagaIdGenerator(): SagaIdGenerator = SagaIdGenerator(nodeId) @Bean @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") - fun redisStreamTransactionListener(): RedisStreamTransactionListener = - RedisStreamTransactionListener( + fun redisStreamSagaListener(): RedisStreamSagaListener = + RedisStreamSagaListener( backpressureSize = backpressureSize, - transactionDispatcher = redisStreamTransactionDispatcher(), + sagaDispatcher = redisStreamSagaDispatcher(), connectionFactory = reactiveRedisConnectionFactory(), nodeGroup = nodeGroup, nodeName = nodeName, - reactiveRedisTemplate = transactionReactiveRedisTemplate(), + reactiveRedisTemplate = sagaReactiveRedisTemplate(), objectMapper = netxObjectMapper(), ).also { - info("RedisStreamTransactionListener connect to host : \"$host\" port : \"$port\" nodeName : \"$nodeName\" nodeGroup : \"$nodeGroup\" backpressureSize : \"$backpressureSize\"") + info("RedisStreamSagaListener connect to host : \"$host\" port : \"$port\" nodeName : \"$nodeName\" nodeGroup : \"$nodeGroup\" backpressureSize : \"$backpressureSize\"") it.subscribeStream() } @@ -121,42 +121,42 @@ internal class RedisTransactionConfigurer( @Bean @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") - fun redisTransactionRetrySupporter(): RedisTransactionRetrySupporter = - RedisTransactionRetrySupporter( + fun redisSagaRetrySupporter(): RedisSagaRetrySupporter = + RedisSagaRetrySupporter( nodeGroup = nodeGroup, nodeName = nodeName, - reactiveRedisTemplate = transactionReactiveRedisTemplate(), - transactionDispatcher = redisStreamTransactionDispatcher(), + reactiveRedisTemplate = sagaReactiveRedisTemplate(), + sagaDispatcher = redisStreamSagaDispatcher(), orphanMilli = orphanMilli, recoveryMilli = recoveryMilli, backpressureSize = backpressureSize, objectMapper = netxObjectMapper(), ).also { - info("RedisTransactionRetrySupporter connect to host : \"$host\" port : \"$port\" nodeName : \"$nodeName\" nodeGroup : \"$nodeGroup\" orphanMilli : \"$orphanMilli\" recoveryMilli : \"$recoveryMilli\" backpressureSize : \"$backpressureSize\"") - it.watchOrphanTransaction() + info("RedisSagaRetrySupporter connect to host : \"$host\" port : \"$port\" nodeName : \"$nodeName\" nodeGroup : \"$nodeGroup\" orphanMilli : \"$orphanMilli\" recoveryMilli : \"$recoveryMilli\" backpressureSize : \"$backpressureSize\"") + it.watchOrphanSaga() } @Bean @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") - internal fun redisStreamTransactionDispatcher(): RedisStreamTransactionDispatcher = - RedisStreamTransactionDispatcher( + internal fun redisStreamSagaDispatcher(): RedisStreamSagaDispatcher = + RedisStreamSagaDispatcher( applicationContext = applicationContext, - reactiveRedisTemplate = transactionReactiveRedisTemplate(), + reactiveRedisTemplate = sagaReactiveRedisTemplate(), nodeGroup = nodeGroup, codec = jsonCodec(), - transactionManager = redisStreamTransactionManager(), + sagaManager = redisStreamSagaManager(), ).also { - info("RedisStreamTransactionDispatcher connect to host : \"$host\" port : \"$port\" nodeName : \"$nodeName\" nodeGroup : \"$nodeGroup\"") + info("RedisStreamSagaDispatcher connect to host : \"$host\" port : \"$port\" nodeName : \"$nodeName\" nodeGroup : \"$nodeGroup\"") } @Bean @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") - internal fun transactionReactiveRedisTemplate(): ReactiveRedisTemplate { + internal fun sagaReactiveRedisTemplate(): ReactiveRedisTemplate { val keySerializer = StringRedisSerializer() val valueSerializer = - Jackson2JsonRedisSerializer(netxObjectMapper(), Transaction::class.java) + Jackson2JsonRedisSerializer(netxObjectMapper(), Saga::class.java) - val builder: RedisSerializationContext.RedisSerializationContextBuilder = + val builder: RedisSerializationContext.RedisSerializationContextBuilder = RedisSerializationContext.newSerializationContext(keySerializer) val context = builder.value(valueSerializer).build() diff --git a/src/main/kotlin/org/rooftop/netx/redis/RedisTransactionRetrySupporter.kt b/src/main/kotlin/org/rooftop/netx/redis/RedisSagaRetrySupporter.kt similarity index 73% rename from src/main/kotlin/org/rooftop/netx/redis/RedisTransactionRetrySupporter.kt rename to src/main/kotlin/org/rooftop/netx/redis/RedisSagaRetrySupporter.kt index 5b762ff..2804b2c 100644 --- a/src/main/kotlin/org/rooftop/netx/redis/RedisTransactionRetrySupporter.kt +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisSagaRetrySupporter.kt @@ -1,26 +1,26 @@ package org.rooftop.netx.redis import com.fasterxml.jackson.databind.ObjectMapper -import org.rooftop.netx.engine.AbstractTransactionDispatcher -import org.rooftop.netx.engine.AbstractTransactionRetrySupporter -import org.rooftop.netx.engine.core.Transaction +import org.rooftop.netx.engine.AbstractSagaDispatcher +import org.rooftop.netx.engine.AbstractSagaRetrySupporter +import org.rooftop.netx.engine.core.Saga import org.springframework.data.domain.Range import org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions import org.springframework.data.redis.core.ReactiveRedisTemplate import reactor.core.publisher.Flux -internal class RedisTransactionRetrySupporter( +internal class RedisSagaRetrySupporter( recoveryMilli: Long, backpressureSize: Int, - transactionDispatcher: AbstractTransactionDispatcher, + sagaDispatcher: AbstractSagaDispatcher, private val nodeGroup: String, private val nodeName: String, - private val reactiveRedisTemplate: ReactiveRedisTemplate, + private val reactiveRedisTemplate: ReactiveRedisTemplate, private val orphanMilli: Long, private val objectMapper: ObjectMapper, -) : AbstractTransactionRetrySupporter(backpressureSize, recoveryMilli, transactionDispatcher) { +) : AbstractSagaRetrySupporter(backpressureSize, recoveryMilli, sagaDispatcher) { - override fun claimOrphanTransaction(backpressureSize: Int): Flux> { + override fun claimOrphanSaga(backpressureSize: Int): Flux> { return reactiveRedisTemplate.opsForStream() .pending(STREAM_KEY, nodeGroup, Range.closed("-", "+"), backpressureSize.toLong()) .filter { it.get().toList().isNotEmpty() } @@ -37,7 +37,7 @@ internal class RedisTransactionRetrySupporter( .map { objectMapper.readValue( it.value["data"], - Transaction::class.java + Saga::class.java ) to it.id.toString() } } diff --git a/src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionDispatcher.kt b/src/main/kotlin/org/rooftop/netx/redis/RedisStreamSagaDispatcher.kt similarity index 51% rename from src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionDispatcher.kt rename to src/main/kotlin/org/rooftop/netx/redis/RedisStreamSagaDispatcher.kt index 20753e4..5683997 100644 --- a/src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionDispatcher.kt +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisStreamSagaDispatcher.kt @@ -1,37 +1,37 @@ package org.rooftop.netx.redis import org.rooftop.netx.api.Codec -import org.rooftop.netx.api.FailedAckTransactionException -import org.rooftop.netx.api.TransactionManager -import org.rooftop.netx.engine.AbstractTransactionDispatcher -import org.rooftop.netx.engine.core.Transaction -import org.rooftop.netx.meta.TransactionHandler +import org.rooftop.netx.api.FailedAckSagaException +import org.rooftop.netx.api.SagaManager +import org.rooftop.netx.engine.AbstractSagaDispatcher +import org.rooftop.netx.engine.core.Saga +import org.rooftop.netx.meta.SagaHandler import org.springframework.context.ApplicationContext import org.springframework.data.redis.core.ReactiveRedisTemplate import reactor.core.publisher.Mono -internal class RedisStreamTransactionDispatcher( +internal class RedisStreamSagaDispatcher( codec: Codec, - transactionManager: TransactionManager, + sagaManager: SagaManager, private val applicationContext: ApplicationContext, - private val reactiveRedisTemplate: ReactiveRedisTemplate, + private val reactiveRedisTemplate: ReactiveRedisTemplate, private val nodeGroup: String, -) : AbstractTransactionDispatcher(codec, transactionManager) { +) : AbstractSagaDispatcher(codec, sagaManager) { override fun findHandlers(): List { - return applicationContext.getBeansWithAnnotation(TransactionHandler::class.java) + return applicationContext.getBeansWithAnnotation(SagaHandler::class.java) .entries.asSequence() .map { it.value } .toList() } - override fun ack(transaction: Transaction, messageId: String): Mono> { + override fun ack(saga: Saga, messageId: String): Mono> { return reactiveRedisTemplate.opsForStream() .acknowledge(STREAM_KEY, nodeGroup, messageId) - .map { transaction to messageId } + .map { saga to messageId } .switchIfEmpty( Mono.error { - throw FailedAckTransactionException("Fail to ack transaction transactionId \"${transaction.id}\" messageId \"$messageId\"") + throw FailedAckSagaException("Fail to ack saga id \"${saga.id}\" messageId \"$messageId\"") } ) } diff --git a/src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionListener.kt b/src/main/kotlin/org/rooftop/netx/redis/RedisStreamSagaListener.kt similarity index 82% rename from src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionListener.kt rename to src/main/kotlin/org/rooftop/netx/redis/RedisStreamSagaListener.kt index 71efe1e..fc999e6 100644 --- a/src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionListener.kt +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisStreamSagaListener.kt @@ -2,9 +2,9 @@ package org.rooftop.netx.redis import com.fasterxml.jackson.databind.ObjectMapper import io.lettuce.core.RedisBusyException -import org.rooftop.netx.engine.AbstractTransactionDispatcher -import org.rooftop.netx.engine.AbstractTransactionListener -import org.rooftop.netx.engine.core.Transaction +import org.rooftop.netx.engine.AbstractSagaDispatcher +import org.rooftop.netx.engine.AbstractSagaListener +import org.rooftop.netx.engine.core.Saga import org.rooftop.netx.engine.logging.info import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory import org.springframework.data.redis.connection.stream.Consumer @@ -17,15 +17,15 @@ import reactor.core.publisher.Mono import kotlin.time.Duration.Companion.hours import kotlin.time.toJavaDuration -internal class RedisStreamTransactionListener( +internal class RedisStreamSagaListener( backpressureSize: Int, - transactionDispatcher: AbstractTransactionDispatcher, + sagaDispatcher: AbstractSagaDispatcher, connectionFactory: ReactiveRedisConnectionFactory, private val nodeGroup: String, private val nodeName: String, - private val reactiveRedisTemplate: ReactiveRedisTemplate, + private val reactiveRedisTemplate: ReactiveRedisTemplate, private val objectMapper: ObjectMapper, -) : AbstractTransactionListener(backpressureSize, transactionDispatcher) { +) : AbstractSagaListener(backpressureSize, sagaDispatcher) { private val options = StreamReceiver.StreamReceiverOptions.builder() .pollTimeout(1.hours.toJavaDuration()) @@ -33,7 +33,7 @@ internal class RedisStreamTransactionListener( private val receiver = StreamReceiver.create(connectionFactory, options) - override fun receive(): Flux> { + override fun receive(): Flux> { return createGroupIfNotExists() .flatMap { receiver.receive( @@ -42,7 +42,7 @@ internal class RedisStreamTransactionListener( ).map { objectMapper.readValue( it.value["data"], - Transaction::class.java, + Saga::class.java, ) to it.id.value } } diff --git a/src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManager.kt b/src/main/kotlin/org/rooftop/netx/redis/RedisStreamSagaManager.kt similarity index 51% rename from src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManager.kt rename to src/main/kotlin/org/rooftop/netx/redis/RedisStreamSagaManager.kt index c58a376..c6608fb 100644 --- a/src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManager.kt +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisStreamSagaManager.kt @@ -2,56 +2,56 @@ package org.rooftop.netx.redis import com.fasterxml.jackson.databind.ObjectMapper import org.rooftop.netx.api.Codec -import org.rooftop.netx.api.TransactionException -import org.rooftop.netx.engine.AbstractTransactionManager -import org.rooftop.netx.engine.TransactionIdGenerator -import org.rooftop.netx.engine.core.Transaction -import org.rooftop.netx.engine.core.TransactionState +import org.rooftop.netx.api.SagaException +import org.rooftop.netx.engine.AbstractSagaManager +import org.rooftop.netx.engine.SagaIdGenerator +import org.rooftop.netx.engine.core.Saga +import org.rooftop.netx.engine.core.SagaState import org.springframework.data.redis.connection.stream.Record import org.springframework.data.redis.core.ReactiveRedisTemplate import reactor.core.publisher.Mono -internal class RedisStreamTransactionManager( +internal class RedisStreamSagaManager( codec: Codec, nodeName: String, - transactionIdGenerator: TransactionIdGenerator, + sagaIdGenerator: SagaIdGenerator, private val nodeGroup: String, - private val reactiveRedisTemplate: ReactiveRedisTemplate, + private val reactiveRedisTemplate: ReactiveRedisTemplate, private val objectMapper: ObjectMapper, -) : AbstractTransactionManager( +) : AbstractSagaManager( nodeName = nodeName, nodeGroup = nodeGroup, codec = codec, - transactionIdGenerator = transactionIdGenerator + sagaIdGenerator = sagaIdGenerator ) { - override fun getAnyTransaction(transactionId: String): Mono { + override fun getAnySaga(id: String): Mono { return reactiveRedisTemplate - .opsForHash()[transactionId, STATE_KEY] + .opsForHash()[id, STATE_KEY] .switchIfEmpty( Mono.error { - throw TransactionException("Cannot find exists transaction id \"$transactionId\"") + throw SagaException("Cannot find exists saga by id \"$id\"") } ) - .map { TransactionState.valueOf(it) } + .map { SagaState.valueOf(it) } } - override fun publishTransaction(transactionId: String, transaction: Transaction): Mono { + override fun publishSaga(id: String, saga: Saga): Mono { return reactiveRedisTemplate.opsForHash() .putAll( - transactionId, mapOf( - STATE_KEY to transaction.state.name, + id, mapOf( + STATE_KEY to saga.state.name, ) ) - .map { objectMapper.writeValueAsString(transaction) } + .map { objectMapper.writeValueAsString(saga) } .flatMap { - reactiveRedisTemplate.opsForStream() + reactiveRedisTemplate.opsForStream() .add( Record.of(mapOf(DATA to it)) .withStreamKey(STREAM_KEY) ) } - .map { transactionId } + .map { id } } private companion object { diff --git a/src/main/resources/META-INF/spring/spring.factories b/src/main/resources/META-INF/spring/spring.factories index 007d7c1..28f0db6 100644 --- a/src/main/resources/META-INF/spring/spring.factories +++ b/src/main/resources/META-INF/spring/spring.factories @@ -1,2 +1,2 @@ -org.rooftop.netx.meta.EnableDistributedTransaction=\ -org.rooftop.netx.redis.RedisTransactionConfigurer +org.rooftop.netx.meta.EnableSaga=\ +org.rooftop.netx.redis.RedisSagaConfigurer diff --git a/src/test/java/org/rooftop/netx/javasupports/NetxJavaSupportsTest.java b/src/test/java/org/rooftop/netx/javasupports/NetxJavaSupportsTest.java index b6dea63..6068fdc 100644 --- a/src/test/java/org/rooftop/netx/javasupports/NetxJavaSupportsTest.java +++ b/src/test/java/org/rooftop/netx/javasupports/NetxJavaSupportsTest.java @@ -8,20 +8,20 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.rooftop.netx.api.Orchestrator; -import org.rooftop.netx.api.TransactionManager; -import org.rooftop.netx.meta.EnableDistributedTransaction; +import org.rooftop.netx.api.SagaManager; +import org.rooftop.netx.meta.EnableSaga; import org.rooftop.netx.redis.RedisContainer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; -@EnableDistributedTransaction +@EnableSaga @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = { RedisContainer.class, NetxJavaSupportsTest.class, - TransactionEventListeners.class, + SagaEventListeners.class, OrchestratorConfigurer.class, }) @DisplayName("NetxJavaSupportsTest") @@ -32,49 +32,49 @@ class NetxJavaSupportsTest { private static final Event POSITIVE_EVENT = new Event(1L); @Autowired - private TransactionManager transactionManager; + private SagaManager sagaManager; @Autowired - private TransactionEventListeners transactionEventListeners; + private SagaEventListeners sagaEventListeners; @Autowired private Orchestrator orchestrator; @BeforeEach void clear() { - transactionEventListeners.clear(); + sagaEventListeners.clear(); } @Test @DisplayName("Scenario-1. Start -> Join -> Commit") void Scenario1_Start_Join_Commit() { - String transactionId = transactionManager.syncStart(POSITIVE_EVENT); + String id = sagaManager.syncStart(POSITIVE_EVENT); Awaitility.waitAtMost(5, TimeUnit.SECONDS) .untilAsserted(() -> { - transactionEventListeners.assertTransactionCount("START", 1); - transactionEventListeners.assertTransactionCount("JOIN", 1); - transactionEventListeners.assertTransactionCount("COMMIT", 1); + sagaEventListeners.assertSagaCount("START", 1); + sagaEventListeners.assertSagaCount("JOIN", 1); + sagaEventListeners.assertSagaCount("COMMIT", 1); }); } @Test @DisplayName("Scenario-2. Start -> Join -> Rollback") - void Transaction_Start_Join_Rollback() { - String transactionId = transactionManager.syncStart(NEGATIVE_EVENT); + void Scenario2_Start_Join_Rollback() { + String id = sagaManager.syncStart(NEGATIVE_EVENT); Awaitility.waitAtMost(5, TimeUnit.SECONDS) .untilAsserted(() -> { - transactionEventListeners.assertTransactionCount("START", 1); - transactionEventListeners.assertTransactionCount("JOIN", 1); - transactionEventListeners.assertTransactionCount("ROLLBACK", 1); + sagaEventListeners.assertSagaCount("START", 1); + sagaEventListeners.assertSagaCount("JOIN", 1); + sagaEventListeners.assertSagaCount("ROLLBACK", 1); }); } @Test @DisplayName("Scenario-3. Orchestrator add 3 number") void Orchestrator_Add_Three_Number() { - var result = orchestrator.transactionSync(0); + var result = orchestrator.sagaSync(0); Assertions.assertThat(result.isSuccess()).isTrue(); Assertions.assertThat(result.decodeResult(Integer.class)).isEqualTo(3); diff --git a/src/test/java/org/rooftop/netx/javasupports/SagaEventListeners.java b/src/test/java/org/rooftop/netx/javasupports/SagaEventListeners.java new file mode 100644 index 0000000..7551415 --- /dev/null +++ b/src/test/java/org/rooftop/netx/javasupports/SagaEventListeners.java @@ -0,0 +1,73 @@ +package org.rooftop.netx.javasupports; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.assertj.core.api.Assertions; +import org.rooftop.netx.api.SuccessWith; +import org.rooftop.netx.api.SagaCommitEvent; +import org.rooftop.netx.api.SagaCommitListener; +import org.rooftop.netx.api.SagaJoinEvent; +import org.rooftop.netx.api.SagaJoinListener; +import org.rooftop.netx.api.SagaRollbackEvent; +import org.rooftop.netx.api.SagaRollbackListener; +import org.rooftop.netx.api.SagaStartEvent; +import org.rooftop.netx.api.SagaStartListener; +import org.rooftop.netx.meta.SagaHandler; +import reactor.core.publisher.Mono; + +@SagaHandler +public class SagaEventListeners { + + private final Map receivedSagas = new ConcurrentHashMap<>(); + + public void clear() { + receivedSagas.clear(); + } + + public void assertSagaCount(String sagaState, int count) { + Assertions.assertThat(receivedSagas.getOrDefault(sagaState, 0)) + .isEqualTo(count); + } + + @SagaStartListener( + event = Event.class, + noRollbackFor = IllegalArgumentException.class, + successWith = SuccessWith.PUBLISH_JOIN + ) + public void listenSagaStartEvent(SagaStartEvent sagaStartEvent) { + incrementSaga("START"); + Event event = sagaStartEvent.decodeEvent(Event.class); + sagaStartEvent.setNextEvent(event); + } + + @SagaJoinListener( + event = Event.class, + successWith = SuccessWith.PUBLISH_COMMIT + ) + public void listenSagaJoinEvent(SagaJoinEvent sagaJoinEvent) { + incrementSaga("JOIN"); + Event event = sagaJoinEvent.decodeEvent(Event.class); + sagaJoinEvent.setNextEvent(event); + if (event.event() < 0) { + throw new IllegalArgumentException(); + } + } + + @SagaCommitListener + public Mono listenSagaCommitEvent(SagaCommitEvent sagaCommitEvent) { + incrementSaga("COMMIT"); + return Mono.just(1L); + } + + @SagaRollbackListener(event = Event.class) + public String listenSagaRollbackEvent(SagaRollbackEvent sagaRollbackEvent) { + incrementSaga("ROLLBACK"); + sagaRollbackEvent.decodeEvent(Event.class); + return "listenSagaRollbackEvent"; + } + + private void incrementSaga(String sagaState) { + receivedSagas.put(sagaState, + receivedSagas.getOrDefault(sagaState, 0) + 1); + } +} diff --git a/src/test/java/org/rooftop/netx/javasupports/TransactionEventListeners.java b/src/test/java/org/rooftop/netx/javasupports/TransactionEventListeners.java deleted file mode 100644 index 5bd7950..0000000 --- a/src/test/java/org/rooftop/netx/javasupports/TransactionEventListeners.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.rooftop.netx.javasupports; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import org.assertj.core.api.Assertions; -import org.rooftop.netx.api.SuccessWith; -import org.rooftop.netx.api.TransactionCommitEvent; -import org.rooftop.netx.api.TransactionCommitListener; -import org.rooftop.netx.api.TransactionJoinEvent; -import org.rooftop.netx.api.TransactionJoinListener; -import org.rooftop.netx.api.TransactionRollbackEvent; -import org.rooftop.netx.api.TransactionRollbackListener; -import org.rooftop.netx.api.TransactionStartEvent; -import org.rooftop.netx.api.TransactionStartListener; -import org.rooftop.netx.meta.TransactionHandler; -import reactor.core.publisher.Mono; - -@TransactionHandler -public class TransactionEventListeners { - - private final Map receivedTransactions = new ConcurrentHashMap<>(); - - public void clear() { - receivedTransactions.clear(); - } - - public void assertTransactionCount(String transactionState, int count) { - Assertions.assertThat(receivedTransactions.getOrDefault(transactionState, 0)) - .isEqualTo(count); - } - - @TransactionStartListener( - event = Event.class, - noRollbackFor = IllegalArgumentException.class, - successWith = SuccessWith.PUBLISH_JOIN - ) - public void listenTransactionStartEvent(TransactionStartEvent transactionStartEvent) { - incrementTransaction("START"); - Event event = transactionStartEvent.decodeEvent(Event.class); - transactionStartEvent.setNextEvent(event); - } - - @TransactionJoinListener( - event = Event.class, - successWith = SuccessWith.PUBLISH_COMMIT - ) - public void listenTransactionJoinEvent(TransactionJoinEvent transactionJoinEvent) { - incrementTransaction("JOIN"); - Event event = transactionJoinEvent.decodeEvent(Event.class); - transactionJoinEvent.setNextEvent(event); - if (event.event() < 0) { - throw new IllegalArgumentException(); - } - } - - @TransactionCommitListener - public Mono listenTransactionCommitEvent(TransactionCommitEvent transactionCommitEvent) { - incrementTransaction("COMMIT"); - return Mono.just(1L); - } - - @TransactionRollbackListener(event = Event.class) - public String listenTransactionRollbackEvent(TransactionRollbackEvent transactionRollbackEvent) { - incrementTransaction("ROLLBACK"); - transactionRollbackEvent.decodeEvent(Event.class); - return "listenTransactionRollbackEvent"; - } - - private void incrementTransaction(String transactionState) { - receivedTransactions.put(transactionState, - receivedTransactions.getOrDefault(transactionState, 0) + 1); - } -} diff --git a/src/test/kotlin/org/rooftop/netx/client/NetxLoadTest.kt b/src/test/kotlin/org/rooftop/netx/client/NetxLoadTest.kt index 388adac..2c21d25 100644 --- a/src/test/kotlin/org/rooftop/netx/client/NetxLoadTest.kt +++ b/src/test/kotlin/org/rooftop/netx/client/NetxLoadTest.kt @@ -5,8 +5,8 @@ import io.kotest.core.annotation.DisplayName import io.kotest.core.spec.style.FunSpec import io.kotest.data.forAll import io.kotest.data.row -import org.rooftop.netx.api.TransactionManager -import org.rooftop.netx.meta.EnableDistributedTransaction +import org.rooftop.netx.api.SagaManager +import org.rooftop.netx.meta.EnableSaga import org.rooftop.netx.redis.RedisContainer import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.TestPropertySource @@ -17,40 +17,41 @@ import kotlin.time.Duration.Companion.minutes classes = [ RedisContainer::class, LoadRunner::class, - TransactionReceiveStorage::class, + OrchestratorConfigurer::class, + SagaReceiveStorage::class, ] ) -@EnableDistributedTransaction +@EnableSaga @TestPropertySource("classpath:fast-recover-mode.properties") internal class NetxLoadTest( - private val transactionManager: TransactionManager, + private val sagaManager: SagaManager, private val loadRunner: LoadRunner, - private val transactionReceiveStorage: TransactionReceiveStorage, + private val sagaReceiveStorage: SagaReceiveStorage, ) : FunSpec({ test("Netx는 부하가 가중되어도, 결과적 일관성을 보장한다.") { forAll( row(1, 1), - row(10, 10), - row(100, 100), - row(1_000, 1_000), - row(10_000, 10_000), +// row(10, 10), +// row(100, 100), +// row(1_000, 1_000), +// row(10_000, 10_000), ) { commitLoadCount, rollbackLoadCount -> - transactionReceiveStorage.clear() + sagaReceiveStorage.clear() loadRunner.load(commitLoadCount) { - transactionManager.start(LoadTestEvent(NO_ROLLBACK)).block()!! + sagaManager.start(LoadTestEvent(NO_ROLLBACK)).block()!! } loadRunner.load(rollbackLoadCount) { - transactionManager.start(LoadTestEvent(ROLLBACK)).block()!! + sagaManager.start(LoadTestEvent(ROLLBACK)).block()!! } eventually(3.minutes) { - transactionReceiveStorage.startCountShouldBeGreaterThanOrEqual(commitLoadCount + rollbackLoadCount) - transactionReceiveStorage.joinCountShouldBeGreaterThanOrEqual(commitLoadCount + rollbackLoadCount) - transactionReceiveStorage.commitCountShouldBeGreaterThanOrEqual(commitLoadCount + rollbackLoadCount) - transactionReceiveStorage.rollbackCountShouldBeGreaterThanOrEqual(rollbackLoadCount) + sagaReceiveStorage.startCountShouldBeGreaterThanOrEqual(commitLoadCount + rollbackLoadCount) + sagaReceiveStorage.joinCountShouldBeGreaterThanOrEqual(commitLoadCount + rollbackLoadCount) + sagaReceiveStorage.commitCountShouldBeGreaterThanOrEqual(commitLoadCount + rollbackLoadCount) + sagaReceiveStorage.rollbackCountShouldBeGreaterThanOrEqual(rollbackLoadCount) } } } diff --git a/src/test/kotlin/org/rooftop/netx/client/NetxOrchestratorLoadTest.kt b/src/test/kotlin/org/rooftop/netx/client/NetxOrchestratorLoadTest.kt index afe3ccc..e9e1e69 100644 --- a/src/test/kotlin/org/rooftop/netx/client/NetxOrchestratorLoadTest.kt +++ b/src/test/kotlin/org/rooftop/netx/client/NetxOrchestratorLoadTest.kt @@ -7,7 +7,7 @@ import io.kotest.data.forAll import io.kotest.data.row import io.kotest.matchers.equals.shouldBeEqual import org.rooftop.netx.api.Orchestrator -import org.rooftop.netx.meta.EnableDistributedTransaction +import org.rooftop.netx.meta.EnableSaga import org.rooftop.netx.redis.RedisContainer import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.TestPropertySource @@ -23,7 +23,7 @@ import kotlin.time.Duration.Companion.minutes OrchestratorConfigurer::class, ] ) -@EnableDistributedTransaction +@EnableSaga @TestPropertySource("classpath:fast-recover-mode.properties") class NetxOrchestratorLoadTest( private val loadRunner: LoadRunner, @@ -42,7 +42,7 @@ class NetxOrchestratorLoadTest( val atomicInt = AtomicInteger(0) loadRunner.load(count) { - orchestrator.transaction(THREE_MINUTES_MILLIS, atomicInt.getAndIncrement()) + orchestrator.saga(THREE_MINUTES_MILLIS, atomicInt.getAndIncrement()) .map { resultStorage.add(it.decodeResult(Int::class)) } .subscribe() } diff --git a/src/test/kotlin/org/rooftop/netx/client/OrchestratorConfigurer.kt b/src/test/kotlin/org/rooftop/netx/client/OrchestratorConfigurer.kt index 53cac1b..9ab9e68 100644 --- a/src/test/kotlin/org/rooftop/netx/client/OrchestratorConfigurer.kt +++ b/src/test/kotlin/org/rooftop/netx/client/OrchestratorConfigurer.kt @@ -13,9 +13,13 @@ class OrchestratorConfigurer( @Bean fun sum3Orchestrator(): Orchestrator { return orchestratorFactory.create("sum3Orchestrator") - .start(IntOrchestrator) - .join(IntOrchestrator) - .commit(IntOrchestrator) + .startReactive(MonoIntOrchestrator, rollback = { Mono.fromCallable { it - 1 } }) + .joinReactive(MonoIntOrchestrator, rollback = { Mono.fromCallable { it - 1 } }) + .commitReactiveWithContext({ _, request -> + Mono.fromCallable { + request + 1 + } + }, contextRollback = { _, request -> Mono.fromCallable { request - 1 } }) } object IntOrchestrator : Orchestrate { diff --git a/src/test/kotlin/org/rooftop/netx/client/SagaReceiveStorage.kt b/src/test/kotlin/org/rooftop/netx/client/SagaReceiveStorage.kt new file mode 100644 index 0000000..72f1845 --- /dev/null +++ b/src/test/kotlin/org/rooftop/netx/client/SagaReceiveStorage.kt @@ -0,0 +1,77 @@ +package org.rooftop.netx.client + +import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual +import org.rooftop.netx.api.* +import org.rooftop.netx.meta.SagaHandler +import reactor.core.publisher.Mono +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap +import java.util.concurrent.CopyOnWriteArrayList + +@SagaHandler +class SagaReceiveStorage( + private val storage: ConcurrentMap> = ConcurrentHashMap(), +) { + + fun clear() { + storage.clear() + } + + fun joinCountShouldBeGreaterThanOrEqual(count: Int) { + (storage["JOIN"]?.size ?: 0) shouldBeGreaterThanOrEqual count + } + + fun startCountShouldBeGreaterThanOrEqual(count: Int) { + (storage["START"]?.size ?: 0) shouldBeGreaterThanOrEqual count + } + + fun commitCountShouldBeGreaterThanOrEqual(count: Int) { + (storage["COMMIT"]?.size ?: 0) shouldBeGreaterThanOrEqual count + } + + fun rollbackCountShouldBeGreaterThanOrEqual(count: Int) { + (storage["ROLLBACK"]?.size ?: 0) shouldBeGreaterThanOrEqual count + } + + @SagaStartListener( + event = NetxLoadTest.LoadTestEvent::class, + successWith = SuccessWith.PUBLISH_JOIN + ) + fun listenStart(sagaStartEvent: SagaStartEvent): Mono { + return Mono.fromCallable { saveSaga("START", sagaStartEvent) } + .map { sagaStartEvent.decodeEvent(NetxLoadTest.LoadTestEvent::class) } + .map { sagaStartEvent.setNextEvent(it) } + } + + @SagaJoinListener( + event = NetxLoadTest.LoadTestEvent::class, + successWith = SuccessWith.PUBLISH_COMMIT + ) + fun listenJoin(sagaJoinEvent: SagaJoinEvent): Mono { + return Mono.fromCallable { saveSaga("JOIN", sagaJoinEvent) } + .map { sagaJoinEvent.decodeEvent(NetxLoadTest.LoadTestEvent::class) } + .map { sagaJoinEvent.setNextEvent(it) } + } + + @SagaRollbackListener(event = NetxLoadTest.LoadTestEvent::class) + fun listenRollback(sagaRollbackEvent: SagaRollbackEvent): Mono { + return Mono.fromCallable { saveSaga("ROLLBACK", sagaRollbackEvent) } + } + + @SagaCommitListener(event = NetxLoadTest.LoadTestEvent::class) + fun listenCommit(sagaCommitEvent: SagaCommitEvent): Mono { + return Mono.fromCallable { saveSaga("COMMIT", sagaCommitEvent) } + .map { sagaCommitEvent.decodeEvent(NetxLoadTest.LoadTestEvent::class) } + .map { + sagaCommitEvent.setNextEvent(it) + if (it.load == "-") { + throw IllegalArgumentException("Rollback cause \"-\"") + } + } + } + + private fun saveSaga(key: String, sagaEvent: SagaEvent) { + storage.putIfAbsent(key, CopyOnWriteArrayList()) + storage[key]?.add(sagaEvent) + } +} diff --git a/src/test/kotlin/org/rooftop/netx/client/TransactionReceiveStorage.kt b/src/test/kotlin/org/rooftop/netx/client/TransactionReceiveStorage.kt deleted file mode 100644 index d62843c..0000000 --- a/src/test/kotlin/org/rooftop/netx/client/TransactionReceiveStorage.kt +++ /dev/null @@ -1,77 +0,0 @@ -package org.rooftop.netx.client - -import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual -import org.rooftop.netx.api.* -import org.rooftop.netx.meta.TransactionHandler -import reactor.core.publisher.Mono -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentMap -import java.util.concurrent.CopyOnWriteArrayList - -@TransactionHandler -class TransactionReceiveStorage( - private val storage: ConcurrentMap> = ConcurrentHashMap(), -) { - - fun clear() { - storage.clear() - } - - fun joinCountShouldBeGreaterThanOrEqual(count: Int) { - (storage["JOIN"]?.size ?: 0) shouldBeGreaterThanOrEqual count - } - - fun startCountShouldBeGreaterThanOrEqual(count: Int) { - (storage["START"]?.size ?: 0) shouldBeGreaterThanOrEqual count - } - - fun commitCountShouldBeGreaterThanOrEqual(count: Int) { - (storage["COMMIT"]?.size ?: 0) shouldBeGreaterThanOrEqual count - } - - fun rollbackCountShouldBeGreaterThanOrEqual(count: Int) { - (storage["ROLLBACK"]?.size ?: 0) shouldBeGreaterThanOrEqual count - } - - @TransactionStartListener( - event = NetxLoadTest.LoadTestEvent::class, - successWith = SuccessWith.PUBLISH_JOIN - ) - fun listenStart(transaction: TransactionStartEvent): Mono { - return Mono.fromCallable { saveTransaction("START", transaction) } - .map { transaction.decodeEvent(NetxLoadTest.LoadTestEvent::class) } - .map { transaction.setNextEvent(it) } - } - - @TransactionJoinListener( - event = NetxLoadTest.LoadTestEvent::class, - successWith = SuccessWith.PUBLISH_COMMIT - ) - fun listenJoin(transaction: TransactionJoinEvent): Mono { - return Mono.fromCallable { saveTransaction("JOIN", transaction) } - .map { transaction.decodeEvent(NetxLoadTest.LoadTestEvent::class) } - .map { transaction.setNextEvent(it) } - } - - @TransactionRollbackListener(event = NetxLoadTest.LoadTestEvent::class) - fun listenRollback(transaction: TransactionRollbackEvent): Mono { - return Mono.fromCallable { saveTransaction("ROLLBACK", transaction) } - } - - @TransactionCommitListener(event = NetxLoadTest.LoadTestEvent::class) - fun listenCommit(transaction: TransactionCommitEvent): Mono { - return Mono.fromCallable { saveTransaction("COMMIT", transaction) } - .map { transaction.decodeEvent(NetxLoadTest.LoadTestEvent::class) } - .map { - transaction.setNextEvent(it) - if (it.load == "-") { - throw IllegalArgumentException("Rollback cause \"-\"") - } - } - } - - private fun saveTransaction(key: String, transaction: TransactionEvent) { - storage.putIfAbsent(key, CopyOnWriteArrayList()) - storage[key]?.add(transaction) - } -} diff --git a/src/test/kotlin/org/rooftop/netx/engine/NetxEventSupportsTest.kt b/src/test/kotlin/org/rooftop/netx/engine/NetxEventSupportsTest.kt index cb1c7c0..84bfcd5 100644 --- a/src/test/kotlin/org/rooftop/netx/engine/NetxEventSupportsTest.kt +++ b/src/test/kotlin/org/rooftop/netx/engine/NetxEventSupportsTest.kt @@ -4,141 +4,141 @@ import io.kotest.core.annotation.DisplayName import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.equality.shouldBeEqualUsingFields import io.kotest.matchers.equals.shouldBeEqual -import org.rooftop.netx.api.TransactionManager -import org.rooftop.netx.meta.EnableDistributedTransaction +import org.rooftop.netx.api.SagaManager +import org.rooftop.netx.meta.EnableSaga import org.rooftop.netx.redis.RedisContainer import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.TestPropertySource -@EnableDistributedTransaction +@EnableSaga @ContextConfiguration( classes = [ RedisContainer::class, - TransactionReceiveStorage::class, + SagaReceiveStorage::class, ] ) @DisplayName("NetxEventSupports") @TestPropertySource("classpath:application.properties") class NetxEventSupportsTest( - private val transactionManager: TransactionManager, - private val transactionReceiveStorage: TransactionReceiveStorage, + private val sagaManager: SagaManager, + private val sagaReceiveStorage: SagaReceiveStorage, ) : StringSpec({ beforeEach { - transactionReceiveStorage.clear() + sagaReceiveStorage.clear() } - "event로 객체가 주어지면, TransactionRollbackEvent에서 해당 객체를 decode 할 수 있다." { + "event로 객체가 주어지면, SagaRollbackEvent에서 해당 객체를 decode 할 수 있다." { // given val expected = Foo("hello", 1.1234567891234568) - transactionManager.syncStart(expected) + sagaManager.syncStart(expected) Thread.sleep(1000) // when - val startEvent = transactionReceiveStorage.pollStart() + val startEvent = sagaReceiveStorage.pollStart() // then startEvent.decodeEvent(Foo::class) shouldBeEqualUsingFields expected } - "event로 Map이 주어지면, TransactionRollbackEvent에서 해당 객체를 decode할 수 있다." { + "event로 Map이 주어지면, SagaRollbackEvent에서 해당 객체를 decode할 수 있다." { // given val expected = mapOf("name" to "hello") - val transactionId = transactionManager.syncStart() - transactionManager.syncJoin(transactionId, expected) + val id = sagaManager.syncStart() + sagaManager.syncJoin(id, expected) Thread.sleep(1000) // when - val joinEvent = transactionReceiveStorage.pollJoin() + val joinEvent = sagaReceiveStorage.pollJoin() val result = joinEvent.decodeEvent(Map::class) // then result["name"]!! shouldBeEqual expected["name"]!! } - "event로 Int가 주어지면, TransactionRollbackEvent에서 해당 객체를 decode할 수 있다." { + "event로 Int가 주어지면, SagaRollbackEvent에서 해당 객체를 decode할 수 있다." { // given val expected = 1 - val transactionId = transactionManager.syncStart() - transactionManager.syncCommit(transactionId, expected) + val id = sagaManager.syncStart() + sagaManager.syncCommit(id, expected) Thread.sleep(1000) // when - val result = transactionReceiveStorage.pollCommit().decodeEvent(Int::class) + val result = sagaReceiveStorage.pollCommit().decodeEvent(Int::class) // then result shouldBeEqual expected } - "event로 Long이 주어지면, TransactionRollbackEvent에서 해당 객체를 decode할 수 있다." { + "event로 Long이 주어지면, SagaRollbackEvent에서 해당 객체를 decode할 수 있다." { // given val expected = 1L - val transactionId = transactionManager.syncStart() - transactionManager.syncRollback(transactionId, "cause", expected) + val id = sagaManager.syncStart() + sagaManager.syncRollback(id, "cause", expected) Thread.sleep(1000) // when - val result = transactionReceiveStorage.pollRollback().decodeEvent(Long::class) + val result = sagaReceiveStorage.pollRollback().decodeEvent(Long::class) // then result shouldBeEqual expected } - "event로 String이 주어지면, TransactionRollbackEvent에서 해당 객체를 decode할 수 있다." { + "event로 String이 주어지면, SagaRollbackEvent에서 해당 객체를 decode할 수 있다." { // given val expected = "string" - transactionManager.syncStart(expected) + sagaManager.syncStart(expected) Thread.sleep(1000) // when - val result = transactionReceiveStorage.pollStart().decodeEvent(String::class) + val result = sagaReceiveStorage.pollStart().decodeEvent(String::class) // then result shouldBeEqual expected } - "event로 char이 주어지면, TransactionRollbackEvent에서 해당 객체를 decode할 수 있다." { + "event로 char이 주어지면, SagaRollbackEvent에서 해당 객체를 decode할 수 있다." { // given val expected = 'c' - transactionManager.syncStart(expected) + sagaManager.syncStart(expected) Thread.sleep(1000) // when - val result = transactionReceiveStorage.pollStart().decodeEvent(Char::class) + val result = sagaReceiveStorage.pollStart().decodeEvent(Char::class) // then result shouldBeEqual expected } - "event로 Boolean이 주어지면, TransactionRollbackEvent에서 해당 객체를 decode할 수 있다." { + "event로 Boolean이 주어지면, SagaRollbackEvent에서 해당 객체를 decode할 수 있다." { // given val expected = true - transactionManager.syncStart(expected) + sagaManager.syncStart(expected) Thread.sleep(1000) // when - val result = transactionReceiveStorage.pollStart().decodeEvent(Boolean::class) + val result = sagaReceiveStorage.pollStart().decodeEvent(Boolean::class) // then result shouldBeEqual expected } - "event로 Unit이 주어지면, TransactionRollbackEvent에서 해당 객체를 decode할 수 있다." { + "event로 Unit이 주어지면, SagaRollbackEvent에서 해당 객체를 decode할 수 있다." { // given val expected = Unit - transactionManager.syncStart(expected) + sagaManager.syncStart(expected) Thread.sleep(1000) // when - val result = transactionReceiveStorage.pollStart().decodeEvent(Unit::class) + val result = sagaReceiveStorage.pollStart().decodeEvent(Unit::class) // then result shouldBeEqual expected diff --git a/src/test/kotlin/org/rooftop/netx/engine/NetxEventTypedDispatherTest.kt b/src/test/kotlin/org/rooftop/netx/engine/NetxEventTypedDispatherTest.kt index 4bb4dae..f419d01 100644 --- a/src/test/kotlin/org/rooftop/netx/engine/NetxEventTypedDispatherTest.kt +++ b/src/test/kotlin/org/rooftop/netx/engine/NetxEventTypedDispatherTest.kt @@ -3,106 +3,106 @@ package org.rooftop.netx.engine import io.kotest.assertions.nondeterministic.eventually import io.kotest.core.annotation.DisplayName import io.kotest.core.spec.style.StringSpec -import org.rooftop.netx.api.TransactionManager -import org.rooftop.netx.meta.EnableDistributedTransaction +import org.rooftop.netx.api.SagaManager +import org.rooftop.netx.meta.EnableSaga import org.rooftop.netx.redis.RedisContainer import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.TestPropertySource import kotlin.time.Duration.Companion.seconds -@EnableDistributedTransaction +@EnableSaga @ContextConfiguration( classes = [ RedisContainer::class, - TransactionTypedReceiveStorage::class, + SagaTypedReceiveStorage::class, ] ) @DisplayName("NetxEventTypedDispatherTest") @TestPropertySource("classpath:application.properties") class NetxEventTypedDispatherTest( - private val transactionManager: TransactionManager, - private val transactionTypedReceiveStorage: TransactionTypedReceiveStorage, + private val sagaManager: SagaManager, + private val sagaTypedReceiveStorage: SagaTypedReceiveStorage, ) : StringSpec({ beforeEach { - transactionTypedReceiveStorage.clear() + sagaTypedReceiveStorage.clear() } - "event로 Foo 타입의 클래스가 주어지면, Any::class, Foo::class의 모든 핸들러에게 트랜잭션이 전파된다." { - transactionManager.syncStart(Foo("xb")) + "event로 Foo 타입의 클래스가 주어지면, Any::class, Foo::class의 모든 핸들러에게 saga event가 전파된다." { + sagaManager.syncStart(Foo("xb")) eventually(5.seconds) { - transactionTypedReceiveStorage.handlerShouldBeEqual(Any::class, 1) - transactionTypedReceiveStorage.handlerShouldBeEqual(Foo::class, 1) - transactionTypedReceiveStorage.handlerShouldBeEqual(String::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(Long::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(Unit::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(Boolean::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Any::class, 1) + sagaTypedReceiveStorage.handlerShouldBeEqual(Foo::class, 1) + sagaTypedReceiveStorage.handlerShouldBeEqual(String::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Long::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Unit::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Boolean::class, 0) } } - "event로 String 타입의 클래스가 주어지면, Any::class, String::class의 모든 핸들러에게 트랜잭션이 전파된다." { - transactionManager.syncStart("String") + "event로 String 타입의 클래스가 주어지면, Any::class, String::class의 모든 핸들러에게 saga event가 전파된다." { + sagaManager.syncStart("String") eventually(5.seconds) { - transactionTypedReceiveStorage.handlerShouldBeEqual(Any::class, 1) - transactionTypedReceiveStorage.handlerShouldBeEqual(Foo::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(String::class, 1) - transactionTypedReceiveStorage.handlerShouldBeEqual(Long::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(Unit::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(Boolean::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Any::class, 1) + sagaTypedReceiveStorage.handlerShouldBeEqual(Foo::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(String::class, 1) + sagaTypedReceiveStorage.handlerShouldBeEqual(Long::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Unit::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Boolean::class, 0) } } - "event로 Long 타입의 클래스가 주어지면, Any::class, Long::class, String::class, Boolean::class 의 모든 핸들러에게 트랜잭션이 전파된다." { - transactionManager.syncStart(1000L) + "event로 Long 타입의 클래스가 주어지면, Any::class, Long::class, String::class, Boolean::class 의 모든 핸들러에게 saga event가 전파된다." { + sagaManager.syncStart(1000L) eventually(5.seconds) { - transactionTypedReceiveStorage.handlerShouldBeEqual(Any::class, 1) - transactionTypedReceiveStorage.handlerShouldBeEqual(Foo::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(String::class, 1) - transactionTypedReceiveStorage.handlerShouldBeEqual(Long::class, 1) - transactionTypedReceiveStorage.handlerShouldBeEqual(Unit::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(Boolean::class, 1) + sagaTypedReceiveStorage.handlerShouldBeEqual(Any::class, 1) + sagaTypedReceiveStorage.handlerShouldBeEqual(Foo::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(String::class, 1) + sagaTypedReceiveStorage.handlerShouldBeEqual(Long::class, 1) + sagaTypedReceiveStorage.handlerShouldBeEqual(Unit::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Boolean::class, 1) } } - "event로 Boolean 타입의 클래스가 주어지면, Any::class, Boolean::class, String::class 의 모든 핸들러에게 트랜잭션이 전파된다." { - transactionManager.syncStart(true) + "event로 Boolean 타입의 클래스가 주어지면, Any::class, Boolean::class, String::class 의 모든 핸들러에게 saga event가 전파된다." { + sagaManager.syncStart(true) eventually(5.seconds) { - transactionTypedReceiveStorage.handlerShouldBeEqual(Any::class, 1) - transactionTypedReceiveStorage.handlerShouldBeEqual(Foo::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(String::class, 1) - transactionTypedReceiveStorage.handlerShouldBeEqual(Long::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(Unit::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(Boolean::class, 1) + sagaTypedReceiveStorage.handlerShouldBeEqual(Any::class, 1) + sagaTypedReceiveStorage.handlerShouldBeEqual(Foo::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(String::class, 1) + sagaTypedReceiveStorage.handlerShouldBeEqual(Long::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Unit::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Boolean::class, 1) } } - "event로 어떠한것도 전달되지 않으면, Any::class의 모든 핸들러에게 트랜잭션이 전파된다." { - transactionManager.syncStart() + "event로 어떠한것도 전달되지 않으면, Any::class의 모든 핸들러에게 saga event가 전파된다." { + sagaManager.syncStart() eventually(5.seconds) { - transactionTypedReceiveStorage.handlerShouldBeEqual(Any::class, 1) - transactionTypedReceiveStorage.handlerShouldBeEqual(Foo::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(String::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(Long::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(Unit::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(Boolean::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Any::class, 1) + sagaTypedReceiveStorage.handlerShouldBeEqual(Foo::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(String::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Long::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Unit::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Boolean::class, 0) } } - "event로 Unit이 주어지면, Unit::class의 핸들러에게 트랜잭션이 전파된다." { - transactionManager.syncStart(Unit) + "event로 Unit이 주어지면, Unit::class의 핸들러에게 saga event가 전파된다." { + sagaManager.syncStart(Unit) eventually(5.seconds) { - transactionTypedReceiveStorage.handlerShouldBeEqual(Any::class, 1) - transactionTypedReceiveStorage.handlerShouldBeEqual(Foo::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(String::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(Long::class, 0) - transactionTypedReceiveStorage.handlerShouldBeEqual(Unit::class, 1) - transactionTypedReceiveStorage.handlerShouldBeEqual(Boolean::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Any::class, 1) + sagaTypedReceiveStorage.handlerShouldBeEqual(Foo::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(String::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Long::class, 0) + sagaTypedReceiveStorage.handlerShouldBeEqual(Unit::class, 1) + sagaTypedReceiveStorage.handlerShouldBeEqual(Boolean::class, 0) } } diff --git a/src/test/kotlin/org/rooftop/netx/engine/OrcChoCompatibleTest.kt b/src/test/kotlin/org/rooftop/netx/engine/OrcChoCompatibleTest.kt new file mode 100644 index 0000000..5d81a93 --- /dev/null +++ b/src/test/kotlin/org/rooftop/netx/engine/OrcChoCompatibleTest.kt @@ -0,0 +1,9 @@ +package org.rooftop.netx.engine + +import io.kotest.core.spec.style.DescribeSpec + +internal class OrcChoCompatibleTest: DescribeSpec({ + + describe("EventHandler와 Choreography가 같이 등록되어 있어도,") +}) { +} diff --git a/src/test/kotlin/org/rooftop/netx/engine/OrchestratorFactoryTest.kt b/src/test/kotlin/org/rooftop/netx/engine/OrchestratorFactoryTest.kt index 3655d2d..27569b7 100644 --- a/src/test/kotlin/org/rooftop/netx/engine/OrchestratorFactoryTest.kt +++ b/src/test/kotlin/org/rooftop/netx/engine/OrchestratorFactoryTest.kt @@ -6,12 +6,12 @@ import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.equals.shouldBeEqual import io.kotest.matchers.equals.shouldNotBeEqual import org.rooftop.netx.api.Orchestrator -import org.rooftop.netx.meta.EnableDistributedTransaction +import org.rooftop.netx.meta.EnableSaga import org.rooftop.netx.redis.RedisContainer import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.TestPropertySource -@EnableDistributedTransaction +@EnableSaga @DisplayName("OrchestratorFactory 클래스의") @ContextConfiguration(classes = [RedisContainer::class]) @TestPropertySource("classpath:application.properties") diff --git a/src/test/kotlin/org/rooftop/netx/engine/OrchestratorTest.kt b/src/test/kotlin/org/rooftop/netx/engine/OrchestratorTest.kt index 101cdf5..2fce645 100644 --- a/src/test/kotlin/org/rooftop/netx/engine/OrchestratorTest.kt +++ b/src/test/kotlin/org/rooftop/netx/engine/OrchestratorTest.kt @@ -8,7 +8,7 @@ import io.kotest.matchers.equality.shouldBeEqualToComparingFields import io.kotest.matchers.equals.shouldBeEqual import org.rooftop.netx.api.Orchestrator import org.rooftop.netx.api.TypeReference -import org.rooftop.netx.meta.EnableDistributedTransaction +import org.rooftop.netx.meta.EnableSaga import org.rooftop.netx.redis.RedisContainer import org.springframework.beans.factory.annotation.Qualifier import org.springframework.test.context.ContextConfiguration @@ -16,7 +16,7 @@ import org.springframework.test.context.TestPropertySource import java.time.Instant import kotlin.time.Duration.Companion.seconds -@EnableDistributedTransaction +@EnableSaga @ContextConfiguration( classes = [ RedisContainer::class, @@ -40,9 +40,9 @@ class OrchestratorTest( ) : DescribeSpec({ describe("numberOrchestrator 구현채는") { - context("transaction 메소드가 호출되면,") { + context("saga 메소드가 호출되면,") { it("처음 입력받은 숫자에 orchestrate만큼의 수를 더한다.") { - val result = numberOrchestrator.transactionSync(3) + val result = numberOrchestrator.sagaSync(3) result.isSuccess shouldBeEqual true result.decodeResult(Int::class) shouldBeEqual 7 @@ -56,10 +56,10 @@ class OrchestratorTest( mutableListOf(Person("Mother"), Person("Father"), Person("Son")) ) - context("transaction 메소드가 호출되면,") { + context("saga 메소드가 호출되면,") { it("처음 입력 받은 Home에 Mother, Father, Son을 추가한다.") { val result = - homeOrchestrator.transaction(Home("Korea, Seoul, Gangnam", mutableListOf())) + homeOrchestrator.saga(Home("Korea, Seoul, Gangnam", mutableListOf())) .block() result!!.isSuccess shouldBeEqual true @@ -71,9 +71,9 @@ class OrchestratorTest( describe("instantOrchestrator 구현채는") { val expected = InstantWrapper(Instant.now()) - context("transaction 메소드가 호출되면,") { + context("saga 메소드가 호출되면,") { it("처음 입력받은 instantWrapper를 그대로 반환한다.") { - val result = instantOrchestrator.transactionSync(expected) + val result = instantOrchestrator.sagaSync(expected) result.isSuccess shouldBeEqual true result.decodeResult(InstantWrapper::class) shouldBeEqualToComparingFields expected @@ -84,9 +84,9 @@ class OrchestratorTest( describe("manyTypeOrchestrator 구현채는") { val expected = Home("HOME", mutableListOf()) - context("transaction메소드가 호출되면,") { + context("saga 메소드가 호출되면,") { it("처음 Home을 반환한다.") { - val result = manyTypeOrchestrator.transactionSync(1) + val result = manyTypeOrchestrator.sagaSync(1) result.isSuccess shouldBeEqual true result.decodeResult(Home::class) shouldBeEqualToComparingFields expected @@ -97,9 +97,9 @@ class OrchestratorTest( describe("rollbackOrchestrator 구현채는") { val expected = listOf("1", "2", "3", "4", "-4", "-3", "-1") - context("transaction 메소드가 호출되면,") { + context("saga 메소드가 호출되면,") { it("실패한 부분부터 위로 거슬러 올라가며 롤백한다") { - val result = rollbackOrchestrator.transactionSync("") + val result = rollbackOrchestrator.sagaSync("") result.isSuccess shouldBeEqual false shouldThrowWithMessage("Rollback") { @@ -116,7 +116,7 @@ class OrchestratorTest( val expected = listOf("1", "2", "3", "4", "-3", "-1") it("호출할 rollback function이 없으면, 가장 가까운 상단의 rollback을 호출한다.") { - val result = upChainRollbackOrchestrator.transactionSync("") + val result = upChainRollbackOrchestrator.sagaSync("") result.isSuccess shouldBeEqual false shouldThrowWithMessage("Rollback for test") { @@ -129,11 +129,11 @@ class OrchestratorTest( } describe("monoRollbackOrchestrator 구현채는") { - context("transaction 메소드가 호출되면,") { + context("saga 메소드가 호출되면,") { val expected = listOf("1", "2", "3", "4", "-3", "-1") it("실패한 부분부터 위로 거슬러 올라가며 롤백한다.") { - val result = monoRollbackOrchestrator.transactionSync("") + val result = monoRollbackOrchestrator.sagaSync("") result.isSuccess shouldBeEqual false shouldThrowWithMessage("Rollback for test") { @@ -147,11 +147,11 @@ class OrchestratorTest( } describe("contextOrchestrator 구현채는") { - context("transaction 메소드가 호출되면,") { + context("saga 메소드가 호출되면,") { val expected = listOf("0", "1", "2", "r3", "r2") it("context 에서 아이템을 교환하며 Saga를 진행한다.") { - val result = contextOrchestrator.transactionSync("0") + val result = contextOrchestrator.sagaSync("0") result.isSuccess shouldBeEqual false shouldThrowWithMessage("Rollback") { @@ -165,9 +165,9 @@ class OrchestratorTest( } describe("pairOrchestrator 구현채는") { - context("transaction 메소드가 호출되면,") { + context("saga 메소드가 호출되면,") { it("입력받은 파라미터를 name 으로 갖는 Foo pair 를 반환한다. ") { - val result = pairOrchestrator.transactionSync("james") + val result = pairOrchestrator.sagaSync("james") result.isSuccess shouldBeEqual false shouldThrowWithMessage("Rollback") { @@ -178,9 +178,9 @@ class OrchestratorTest( } describe("startWithContextOrchestrator 구현채는") { - context("context와 함께 transaction 메소드가 호출되면,") { + context("context와 함께 saga 메소드가 호출되면,") { it("key에 해당하는 context를 반환한다.") { - val result = startWithContextOrchestrator.transactionSync( + val result = startWithContextOrchestrator.sagaSync( "ignored request", mutableMapOf("key" to "hello") ) @@ -191,7 +191,7 @@ class OrchestratorTest( } describe("fooContextOrchestrator 구현채는") { - context("context 와 함께 transaction 메소드가 호출되면,") { + context("context 와 함께 saga 메소드가 호출되면,") { val expected = listOf( Foo("startSync"), Foo("startWithContext"), @@ -199,7 +199,7 @@ class OrchestratorTest( ) it("0,1,2 Foo가 들어있는 Foo list를 반환한다.") { - val result = fooContextOrchestrator.transactionSync( + val result = fooContextOrchestrator.sagaSync( "", mutableMapOf("0" to Foo("startSync")) ) diff --git a/src/test/kotlin/org/rooftop/netx/engine/SagaReceiveStorage.kt b/src/test/kotlin/org/rooftop/netx/engine/SagaReceiveStorage.kt new file mode 100644 index 0000000..9f657a3 --- /dev/null +++ b/src/test/kotlin/org/rooftop/netx/engine/SagaReceiveStorage.kt @@ -0,0 +1,50 @@ +package org.rooftop.netx.engine + +import org.rooftop.netx.api.* +import org.rooftop.netx.meta.SagaHandler +import java.util.* + +@SagaHandler +class SagaReceiveStorage { + + private val startEvents: Queue = LinkedList() + private val joinEvents: Queue = LinkedList() + private val rollbackEvents: Queue = LinkedList() + private val commitEvents: Queue = LinkedList() + + fun clear(){ + startEvents.clear() + joinEvents.clear() + rollbackEvents.clear() + commitEvents.clear() + } + + @SagaStartListener(successWith = SuccessWith.END) + fun handleStart(sagaStartEvent: SagaStartEvent) { + startEvents.add(sagaStartEvent) + } + + @SagaJoinListener(successWith = SuccessWith.END) + fun handleJoin(sagaJoinEvent: SagaJoinEvent) { + joinEvents.add(sagaJoinEvent) + } + + @SagaRollbackListener + fun handleRollback(sagaRollbackEvent: SagaRollbackEvent) { + rollbackEvents.add(sagaRollbackEvent) + } + + @SagaCommitListener + fun handleCommit(sagaCommitEvent: SagaCommitEvent) { + commitEvents.add(sagaCommitEvent) + } + + fun pollStart(): SagaStartEvent = startEvents.poll() + + fun pollJoin(): SagaJoinEvent = joinEvents.poll() + + fun pollRollback(): SagaRollbackEvent = rollbackEvents.poll() + + fun pollCommit(): SagaCommitEvent = commitEvents.poll() + +} diff --git a/src/test/kotlin/org/rooftop/netx/engine/SagaTypedReceiveStorage.kt b/src/test/kotlin/org/rooftop/netx/engine/SagaTypedReceiveStorage.kt new file mode 100644 index 0000000..cd016cd --- /dev/null +++ b/src/test/kotlin/org/rooftop/netx/engine/SagaTypedReceiveStorage.kt @@ -0,0 +1,65 @@ +package org.rooftop.netx.engine + +import io.kotest.matchers.equals.shouldBeEqual +import org.rooftop.netx.api.SuccessWith +import org.rooftop.netx.api.SagaEvent +import org.rooftop.netx.api.SagaStartEvent +import org.rooftop.netx.api.SagaStartListener +import org.rooftop.netx.meta.SagaHandler +import reactor.core.publisher.Mono +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.reflect.KClass + +@SagaHandler +class SagaTypedReceiveStorage { + + private val storage: ConcurrentMap, CopyOnWriteArrayList> = + ConcurrentHashMap() + + fun clear() { + storage.clear() + } + + fun handlerShouldBeEqual(key: KClass<*>, count: Int) { + (storage[key]?.size ?: 0) shouldBeEqual count + } + + @SagaStartListener(successWith = SuccessWith.END) + fun any(sagaStartEvent: SagaStartEvent): Mono { + return Mono.fromCallable { log(Any::class, sagaStartEvent) } + } + + @SagaStartListener(NetxEventTypedDispatherTest.Foo::class, successWith = SuccessWith.END) + fun foo(sagaStartEvent: SagaStartEvent): Mono { + return Mono.fromCallable { log(NetxEventTypedDispatherTest.Foo::class, sagaStartEvent) } + } + + @SagaStartListener(String::class, successWith = SuccessWith.END) + fun string(sagaStartEvent: SagaStartEvent) { + log(String::class, sagaStartEvent) + } + + @SagaStartListener(Long::class, successWith = SuccessWith.END) + fun long(sagaStartEvent: SagaStartEvent): Long { + log(Long::class, sagaStartEvent) + return 1L + } + + @SagaStartListener(Unit::class, successWith = SuccessWith.END) + fun unit(sagaStartEvent: SagaStartEvent) { + log(Unit::class, sagaStartEvent) + } + + @SagaStartListener(Boolean::class, successWith = SuccessWith.END) + fun boolean(sagaStartEvent: SagaStartEvent) { + log(Boolean::class, sagaStartEvent) + } + + private fun log(key: KClass<*>, sagaEvent: SagaEvent) { + storage.putIfAbsent(key, CopyOnWriteArrayList()) + storage[key]?.add(sagaEvent) + } + +} diff --git a/src/test/kotlin/org/rooftop/netx/engine/TransactionReceiveStorage.kt b/src/test/kotlin/org/rooftop/netx/engine/TransactionReceiveStorage.kt deleted file mode 100644 index c85db90..0000000 --- a/src/test/kotlin/org/rooftop/netx/engine/TransactionReceiveStorage.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.rooftop.netx.engine - -import org.rooftop.netx.api.* -import org.rooftop.netx.meta.TransactionHandler -import java.util.* - -@TransactionHandler -class TransactionReceiveStorage { - - private val startEvents: Queue = LinkedList() - private val joinEvents: Queue = LinkedList() - private val rollbackEvents: Queue = LinkedList() - private val commitEvents: Queue = LinkedList() - - fun clear(){ - startEvents.clear() - joinEvents.clear() - rollbackEvents.clear() - commitEvents.clear() - } - - @TransactionStartListener(successWith = SuccessWith.END) - fun handleStart(transactionStartEvent: TransactionStartEvent) { - startEvents.add(transactionStartEvent) - } - - @TransactionJoinListener(successWith = SuccessWith.END) - fun handleJoin(transactionJoinEvent: TransactionJoinEvent) { - joinEvents.add(transactionJoinEvent) - } - - @TransactionRollbackListener - fun handleRollback(transactionRollbackEvent: TransactionRollbackEvent) { - rollbackEvents.add(transactionRollbackEvent) - } - - @TransactionCommitListener - fun handleCommit(transactionCommitEvent: TransactionCommitEvent) { - commitEvents.add(transactionCommitEvent) - } - - fun pollStart(): TransactionStartEvent = startEvents.poll() - - fun pollJoin(): TransactionJoinEvent = joinEvents.poll() - - fun pollRollback(): TransactionRollbackEvent = rollbackEvents.poll() - - fun pollCommit(): TransactionCommitEvent = commitEvents.poll() - -} diff --git a/src/test/kotlin/org/rooftop/netx/engine/TransactionTypedReceiveStorage.kt b/src/test/kotlin/org/rooftop/netx/engine/TransactionTypedReceiveStorage.kt deleted file mode 100644 index f3f5976..0000000 --- a/src/test/kotlin/org/rooftop/netx/engine/TransactionTypedReceiveStorage.kt +++ /dev/null @@ -1,65 +0,0 @@ -package org.rooftop.netx.engine - -import io.kotest.matchers.equals.shouldBeEqual -import org.rooftop.netx.api.SuccessWith -import org.rooftop.netx.api.TransactionEvent -import org.rooftop.netx.api.TransactionStartEvent -import org.rooftop.netx.api.TransactionStartListener -import org.rooftop.netx.meta.TransactionHandler -import reactor.core.publisher.Mono -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentMap -import java.util.concurrent.CopyOnWriteArrayList -import kotlin.reflect.KClass - -@TransactionHandler -class TransactionTypedReceiveStorage { - - private val storage: ConcurrentMap, CopyOnWriteArrayList> = - ConcurrentHashMap() - - fun clear() { - storage.clear() - } - - fun handlerShouldBeEqual(key: KClass<*>, count: Int) { - (storage[key]?.size ?: 0) shouldBeEqual count - } - - @TransactionStartListener(successWith = SuccessWith.END) - fun any(transaction: TransactionStartEvent): Mono { - return Mono.fromCallable { log(Any::class, transaction) } - } - - @TransactionStartListener(NetxEventTypedDispatherTest.Foo::class, successWith = SuccessWith.END) - fun foo(transaction: TransactionStartEvent): Mono { - return Mono.fromCallable { log(NetxEventTypedDispatherTest.Foo::class, transaction) } - } - - @TransactionStartListener(String::class, successWith = SuccessWith.END) - fun string(transaction: TransactionStartEvent) { - log(String::class, transaction) - } - - @TransactionStartListener(Long::class, successWith = SuccessWith.END) - fun long(transaction: TransactionStartEvent): Long { - log(Long::class, transaction) - return 1L - } - - @TransactionStartListener(Unit::class, successWith = SuccessWith.END) - fun unit(transaction: TransactionStartEvent) { - log(Unit::class, transaction) - } - - @TransactionStartListener(Boolean::class, successWith = SuccessWith.END) - fun boolean(transaction: TransactionStartEvent) { - log(Boolean::class, transaction) - } - - private fun log(key: KClass<*>, transaction: TransactionEvent) { - storage.putIfAbsent(key, CopyOnWriteArrayList()) - storage[key]?.add(transaction) - } - -} diff --git a/src/test/kotlin/org/rooftop/netx/redis/AbstractTransactionHandlerAssertions.kt b/src/test/kotlin/org/rooftop/netx/redis/AbstractSagaHandlerAssertions.kt similarity index 93% rename from src/test/kotlin/org/rooftop/netx/redis/AbstractTransactionHandlerAssertions.kt rename to src/test/kotlin/org/rooftop/netx/redis/AbstractSagaHandlerAssertions.kt index 834c9ef..3edc2d7 100644 --- a/src/test/kotlin/org/rooftop/netx/redis/AbstractTransactionHandlerAssertions.kt +++ b/src/test/kotlin/org/rooftop/netx/redis/AbstractSagaHandlerAssertions.kt @@ -2,7 +2,7 @@ package org.rooftop.netx.redis import io.kotest.matchers.shouldBe -abstract class AbstractTransactionHandlerAssertions { +abstract class AbstractSagaHandlerAssertions { private val methodInvocationCounts = mutableMapOf() diff --git a/src/test/kotlin/org/rooftop/netx/redis/MonoSagaHandlerAssertions.kt b/src/test/kotlin/org/rooftop/netx/redis/MonoSagaHandlerAssertions.kt new file mode 100644 index 0000000..2febdd2 --- /dev/null +++ b/src/test/kotlin/org/rooftop/netx/redis/MonoSagaHandlerAssertions.kt @@ -0,0 +1,34 @@ +package org.rooftop.netx.redis + +import org.rooftop.netx.api.* +import org.rooftop.netx.meta.SagaHandler +import reactor.core.publisher.Mono + +@SagaHandler +class MonoSagaHandlerAssertions : AbstractSagaHandlerAssertions() { + + @SagaRollbackListener + fun handleRollback(event: SagaRollbackEvent): Mono { + put("ROLLBACK") + return Mono.just(Unit) + } + + @SagaCommitListener + fun handleCommit(event: SagaCommitEvent): Mono { + put("COMMIT") + return Mono.just(Unit) + } + + @SagaStartListener(successWith = SuccessWith.END) + fun handleStart(event: SagaStartEvent): Mono { + put("START") + return Mono.just(Unit) + } + + @SagaJoinListener(successWith = SuccessWith.END) + fun handleJoin(event: SagaJoinEvent): Mono { + put("JOIN") + return Mono.just(Unit) + } + +} diff --git a/src/test/kotlin/org/rooftop/netx/redis/MonoTransactionHandlerAssertions.kt b/src/test/kotlin/org/rooftop/netx/redis/MonoTransactionHandlerAssertions.kt deleted file mode 100644 index 08c161e..0000000 --- a/src/test/kotlin/org/rooftop/netx/redis/MonoTransactionHandlerAssertions.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.rooftop.netx.redis - -import org.rooftop.netx.api.* -import org.rooftop.netx.meta.TransactionHandler -import reactor.core.publisher.Mono - -@TransactionHandler -class MonoTransactionHandlerAssertions : AbstractTransactionHandlerAssertions() { - - @TransactionRollbackListener - fun handleRollback(event: TransactionRollbackEvent): Mono { - put("ROLLBACK") - return Mono.just(Unit) - } - - @TransactionCommitListener - fun handleCommit(event: TransactionCommitEvent): Mono { - put("COMMIT") - return Mono.just(Unit) - } - - @TransactionStartListener(successWith = SuccessWith.END) - fun handleStart(event: TransactionStartEvent): Mono { - put("START") - return Mono.just(Unit) - } - - @TransactionJoinListener(successWith = SuccessWith.END) - fun handleJoin(event: TransactionJoinEvent): Mono { - put("JOIN") - return Mono.just(Unit) - } - -} diff --git a/src/test/kotlin/org/rooftop/netx/redis/NoPublisherSagaHandlerAssertions.kt b/src/test/kotlin/org/rooftop/netx/redis/NoPublisherSagaHandlerAssertions.kt new file mode 100644 index 0000000..a1b9181 --- /dev/null +++ b/src/test/kotlin/org/rooftop/netx/redis/NoPublisherSagaHandlerAssertions.kt @@ -0,0 +1,34 @@ +package org.rooftop.netx.redis + +import org.rooftop.netx.api.* +import org.rooftop.netx.meta.SagaHandler + +@SagaHandler +class NoPublisherSagaHandlerAssertions : AbstractSagaHandlerAssertions() { + + @SagaRollbackListener + fun handleRollback(event: SagaRollbackEvent): Long { + put("ROLLBACK") + return Long.MIN_VALUE + } + + @SagaCommitListener + fun handleCommit(event: SagaCommitEvent) { + put("COMMIT") + } + + @SagaStartListener(successWith = SuccessWith.END) + fun handleStart(event: SagaStartEvent): Foo { + put("START") + return Foo("START") + } + + @SagaJoinListener(successWith = SuccessWith.END) + fun handleJoin(event: SagaJoinEvent): Any { + put("JOIN") + return Any::class + } + + class Foo(name: String) + +} diff --git a/src/test/kotlin/org/rooftop/netx/redis/NoPublisherTransactionHandlerAssertions.kt b/src/test/kotlin/org/rooftop/netx/redis/NoPublisherTransactionHandlerAssertions.kt deleted file mode 100644 index c841d50..0000000 --- a/src/test/kotlin/org/rooftop/netx/redis/NoPublisherTransactionHandlerAssertions.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.rooftop.netx.redis - -import org.rooftop.netx.api.* -import org.rooftop.netx.meta.TransactionHandler - -@TransactionHandler -class NoPublisherTransactionHandlerAssertions : AbstractTransactionHandlerAssertions() { - - @TransactionRollbackListener - fun handleRollback(event: TransactionRollbackEvent): Long { - put("ROLLBACK") - return Long.MIN_VALUE - } - - @TransactionCommitListener - fun handleCommit(event: TransactionCommitEvent) { - put("COMMIT") - } - - @TransactionStartListener(successWith = SuccessWith.END) - fun handleStart(event: TransactionStartEvent): Foo { - put("START") - return Foo("START") - } - - @TransactionJoinListener(successWith = SuccessWith.END) - fun handleJoin(event: TransactionJoinEvent): Any { - put("JOIN") - return Any::class - } - - class Foo(name: String) - -} diff --git a/src/test/kotlin/org/rooftop/netx/redis/RedisAssertions.kt b/src/test/kotlin/org/rooftop/netx/redis/RedisAssertions.kt index 822d2d0..418a9df 100644 --- a/src/test/kotlin/org/rooftop/netx/redis/RedisAssertions.kt +++ b/src/test/kotlin/org/rooftop/netx/redis/RedisAssertions.kt @@ -1,7 +1,7 @@ package org.rooftop.netx.redis import io.kotest.matchers.shouldBe -import org.rooftop.netx.engine.core.Transaction +import org.rooftop.netx.engine.core.Saga import org.springframework.beans.factory.annotation.Value import org.springframework.boot.test.context.TestComponent import org.springframework.data.domain.Range @@ -9,7 +9,7 @@ import org.springframework.data.redis.core.ReactiveRedisOperations @TestComponent internal class RedisAssertions( - private val reactiveRedisOperations: ReactiveRedisOperations, + private val reactiveRedisOperations: ReactiveRedisOperations, @Value("\${netx.group}") private val nodeGroup: String, ) { @@ -22,12 +22,12 @@ internal class RedisAssertions( pendingMessageCount shouldBe count } - fun retryTransactionShouldBeNotExists(transactionId: String) { - val retryTransaction = reactiveRedisOperations.opsForSet() + fun retrySagaShouldBeNotExists(sagaId: String) { + val retrySaga = reactiveRedisOperations.opsForSet() .members(nodeGroup) - .any { it.id == transactionId } + .any { it.id == sagaId } .block() - retryTransaction shouldBe false + retrySaga shouldBe false } } diff --git a/src/test/kotlin/org/rooftop/netx/redis/RedisDispatcherNoRollbackForTest.kt b/src/test/kotlin/org/rooftop/netx/redis/RedisStreamSagaDispatcherNoRollbackForTest.kt similarity index 53% rename from src/test/kotlin/org/rooftop/netx/redis/RedisDispatcherNoRollbackForTest.kt rename to src/test/kotlin/org/rooftop/netx/redis/RedisStreamSagaDispatcherNoRollbackForTest.kt index 3bc5c5a..a225ab9 100644 --- a/src/test/kotlin/org/rooftop/netx/redis/RedisDispatcherNoRollbackForTest.kt +++ b/src/test/kotlin/org/rooftop/netx/redis/RedisStreamSagaDispatcherNoRollbackForTest.kt @@ -3,55 +3,55 @@ package org.rooftop.netx.redis import io.kotest.assertions.nondeterministic.eventually import io.kotest.core.annotation.DisplayName import io.kotest.core.spec.style.StringSpec -import org.rooftop.netx.api.TransactionManager -import org.rooftop.netx.meta.EnableDistributedTransaction +import org.rooftop.netx.api.SagaManager +import org.rooftop.netx.meta.EnableSaga import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.TestPropertySource import kotlin.time.Duration.Companion.seconds -@EnableDistributedTransaction +@EnableSaga @ContextConfiguration( classes = [ RedisContainer::class, - TransactionNoRollbackForStorage::class, - MonoTransactionHandlerAssertions::class, + SagaNoRollbackForStorage::class, + MonoSagaHandlerAssertions::class, ] ) -@DisplayName("RedisDispatcherNoRollbackForTest") +@DisplayName("RedisStreamSagaDispatcherNoRollbackForTest") @TestPropertySource("classpath:application.properties") -internal class RedisDispatcherNoRollbackForTest( - private val transactionAssertions: MonoTransactionHandlerAssertions, - private val transactionManager: TransactionManager, +internal class RedisStreamSagaDispatcherNoRollbackForTest( + private val sagaAssertions: MonoSagaHandlerAssertions, + private val sagaManager: SagaManager, ) : StringSpec({ beforeEach { - transactionAssertions.clear() + sagaAssertions.clear() } "noRollbackFor로 IllegalArgumentException이 걸려있으면, 해당 예외가 발생해도 rollback 하지 않는다." { - transactionManager.syncStart(IllegalArgumentExceptionEvent("illegal")) + sagaManager.syncStart(IllegalArgumentExceptionEvent("illegal")) eventually(5.seconds) { - transactionAssertions.startCountShouldBe(1) - transactionAssertions.rollbackCountShouldBe(0) + sagaAssertions.startCountShouldBe(1) + sagaAssertions.rollbackCountShouldBe(0) } } "noRollbackFor로 UnSupportedOperationException이 걸려있으면, 해당 예외가 발생해도 rollback 하지않는다." { - transactionManager.syncStart(UnSupportedOperationExceptionEvent("unsupports")) + sagaManager.syncStart(UnSupportedOperationExceptionEvent("unsupports")) eventually(5.seconds) { - transactionAssertions.startCountShouldBe(1) - transactionAssertions.rollbackCountShouldBe(0) + sagaAssertions.startCountShouldBe(1) + sagaAssertions.rollbackCountShouldBe(0) } } "noRollbackFor에 설정되지 않은 예외가 발생하면, rollback을 수행한다." { - transactionManager.syncStart(NoSuchElementExceptionEvent("noSuchElement")) + sagaManager.syncStart(NoSuchElementExceptionEvent("noSuchElement")) eventually(5.seconds) { - transactionAssertions.startCountShouldBe(1) - transactionAssertions.rollbackCountShouldBe(1) + sagaAssertions.startCountShouldBe(1) + sagaAssertions.rollbackCountShouldBe(1) } } }) { diff --git a/src/test/kotlin/org/rooftop/netx/redis/RedisStreamSagaManagerTest.kt b/src/test/kotlin/org/rooftop/netx/redis/RedisStreamSagaManagerTest.kt new file mode 100644 index 0000000..2b0cf21 --- /dev/null +++ b/src/test/kotlin/org/rooftop/netx/redis/RedisStreamSagaManagerTest.kt @@ -0,0 +1,274 @@ +package org.rooftop.netx.redis + +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.assertions.throwables.shouldThrowMessage +import io.kotest.core.annotation.DisplayName +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import org.rooftop.netx.api.* +import org.rooftop.netx.meta.EnableSaga +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource +import reactor.test.StepVerifier +import kotlin.time.Duration.Companion.seconds + +@EnableSaga +@ContextConfiguration( + classes = [ + RedisContainer::class, + MonoSagaHandlerAssertions::class, + NoPublisherSagaHandlerAssertions::class, + ] +) +@DisplayName("RedisStreamSagaManager 클래스의") +@TestPropertySource("classpath:application.properties") +internal class RedisStreamSagaManagerTest( + private val sagaManager: SagaManager, + private val monoSagaHandlerAssertions: MonoSagaHandlerAssertions, + private val noPublisherSagaHandlerAssertions: NoPublisherSagaHandlerAssertions, +) : DescribeSpec({ + + beforeEach { + monoSagaHandlerAssertions.clear() + noPublisherSagaHandlerAssertions.clear() + } + + describe("start 메소드는") { + context("어떤 event도 없이 호출되면,") { + it("Saga 를 시작하고 saga-id를 반환한다.") { + sagaManager.start().subscribe() + + eventually(5.seconds) { + monoSagaHandlerAssertions.startCountShouldBe(1) + noPublisherSagaHandlerAssertions.startCountShouldBe(1) + } + } + } + + context("서로 다른 id의 사가가 여러번 시작되어도") { + it("모두 읽을 수 있다.") { + sagaManager.start().block() + sagaManager.start().block() + + eventually(5.seconds) { + monoSagaHandlerAssertions.startCountShouldBe(2) + noPublisherSagaHandlerAssertions.startCountShouldBe(2) + } + } + } + } + + describe("syncStart 메소드는") { + context("어떤 event도 없이 호출되면,") { + it("Saga 를 시작하고 saga-id를 반환한다.") { + sagaManager.syncStart() + + eventually(5.seconds) { + monoSagaHandlerAssertions.startCountShouldBe(1) + noPublisherSagaHandlerAssertions.startCountShouldBe(1) + } + } + } + + context("서로 다른 id의 사가가 여러번 시작되어도") { + it("모두 읽을 수 있다.") { + sagaManager.syncStart() + sagaManager.syncStart() + + eventually(5.seconds) { + monoSagaHandlerAssertions.startCountShouldBe(2) + noPublisherSagaHandlerAssertions.startCountShouldBe(2) + } + } + } + } + + describe("join 메소드는") { + context("존재하는 sagaId를 입력받으면,") { + val sagaId = sagaManager.start().block()!! + + it("Saga 에 참여한다.") { + sagaManager.join(sagaId).subscribe() + + eventually(5.seconds) { + monoSagaHandlerAssertions.joinCountShouldBe(1) + noPublisherSagaHandlerAssertions.joinCountShouldBe(1) + } + } + } + + context("존재하지 않는 sagaId를 입력받으면,") { + it("SagaException 을 던진다.") { + val result = sagaManager.join(NOT_EXIST_TX_ID) + + StepVerifier.create(result) + .verifyErrorMessage("Cannot find exists saga by id \"$NOT_EXIST_TX_ID\"") + } + } + } + + describe("syncJoin 메소드는") { + context("존재하는 sagaId를 입력받으면,") { + val sagaId = sagaManager.syncStart() + + it("Saga 에 참여한다.") { + sagaManager.syncJoin(sagaId) + + eventually(5.seconds) { + monoSagaHandlerAssertions.joinCountShouldBe(1) + noPublisherSagaHandlerAssertions.joinCountShouldBe(1) + } + } + } + + context("존재하지 않는 sagaId를 입력받으면,") { + it("SagaException 을 던진다.") { + shouldThrowMessage("Cannot find exists saga by id \"$NOT_EXIST_TX_ID\"") { + sagaManager.syncJoin(NOT_EXIST_TX_ID) + } + } + } + } + + describe("exists 메소드는") { + context("존재하는 sagaId를 입력받으면,") { + val sagaId = sagaManager.start().block()!! + + it("saga id를 반환한다.") { + val result = sagaManager.exists(sagaId) + + StepVerifier.create(result) + .expectNext(sagaId) + .verifyComplete() + } + } + + context("존재하지 않는 sagaId를 입력받으면,") { + it("SagaException 을 던진다.") { + val result = sagaManager.exists(NOT_EXIST_TX_ID) + + StepVerifier.create(result) + .verifyErrorMessage("Cannot find exists saga by id \"$NOT_EXIST_TX_ID\"") + } + } + } + + describe("syncExists 메소드는") { + context("존재하는 sagaId를 입력받으면,") { + val sagaId = sagaManager.syncStart() + + it("saga id를 반환한다.") { + val result = sagaManager.syncExists(sagaId) + + result shouldBe sagaId + } + } + + context("존재하지 않는 sagaId를 입력받으면,") { + it("SagaException 을 던진다.") { + shouldThrowMessage("Cannot find exists saga by id \"$NOT_EXIST_TX_ID\"") { + sagaManager.syncExists(NOT_EXIST_TX_ID) + } + } + } + } + + describe("commit 메소드는") { + context("존재하는 sagaId를 입력받으면,") { + val sagaId = sagaManager.start().block()!! + + it("commit 메시지를 publish 한다") { + sagaManager.commit(sagaId).block() + + eventually(5.seconds) { + monoSagaHandlerAssertions.commitCountShouldBe(1) + noPublisherSagaHandlerAssertions.commitCountShouldBe(1) + } + } + } + + context("존재하지 않는 sagaId를 입력받으면,") { + it("SagaException 을 던진다.") { + val result = sagaManager.commit(NOT_EXIST_TX_ID) + + StepVerifier.create(result) + .verifyErrorMessage("Cannot find exists saga by id \"$NOT_EXIST_TX_ID\"") + } + } + } + + describe("syncCommit 메소드는") { + context("존재하는 sagaId를 입력받으면,") { + val sagaId = sagaManager.syncStart() + + it("commit 메시지를 publish 한다") { + sagaManager.syncCommit(sagaId) + + eventually(5.seconds) { + monoSagaHandlerAssertions.commitCountShouldBe(1) + noPublisherSagaHandlerAssertions.commitCountShouldBe(1) + } + } + } + + context("존재하지 않는 sagaId를 입력받으면,") { + it("SagaException 을 던진다.") { + shouldThrowMessage("Cannot find exists saga by id \"$NOT_EXIST_TX_ID\"") { + sagaManager.syncCommit(NOT_EXIST_TX_ID) + } + } + } + } + + describe("rollback 메소드는") { + context("존재하는 sagaId를 입력받으면,") { + val sagaId = sagaManager.start().block()!! + + it("rollback 메시지를 publish 한다") { + sagaManager.rollback(sagaId, "rollback for test").block() + + eventually(5.seconds) { + monoSagaHandlerAssertions.rollbackCountShouldBe(1) + noPublisherSagaHandlerAssertions.rollbackCountShouldBe(1) + } + } + } + + context("존재하지 않는 sagaId를 입력받으면,") { + it("SagaException 을 던진다.") { + val result = sagaManager.rollback(NOT_EXIST_TX_ID, "rollback for test") + + StepVerifier.create(result) + .verifyErrorMessage("Cannot find exists saga by id \"$NOT_EXIST_TX_ID\"") + } + } + } + + describe("syncRollback 메소드는") { + context("존재하는 sagaId를 입력받으면,") { + val sagaId = sagaManager.syncStart() + + it("rollback 메시지를 publish 한다") { + sagaManager.syncRollback(sagaId, "rollback for test") + + eventually(5.seconds) { + monoSagaHandlerAssertions.rollbackCountShouldBe(1) + noPublisherSagaHandlerAssertions.rollbackCountShouldBe(1) + } + } + } + + context("존재하지 않는 sagaId를 입력받으면,") { + it("SagaException 을 던진다.") { + shouldThrowMessage("Cannot find exists saga by id \"$NOT_EXIST_TX_ID\"") { + sagaManager.syncRollback(NOT_EXIST_TX_ID, "rollback for test") + } + } + } + } +}) { + + private companion object { + private const val NOT_EXIST_TX_ID = "NOT_EXISTS_TX_ID" + } +} diff --git a/src/test/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManagerTest.kt b/src/test/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManagerTest.kt deleted file mode 100644 index 737015a..0000000 --- a/src/test/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManagerTest.kt +++ /dev/null @@ -1,274 +0,0 @@ -package org.rooftop.netx.redis - -import io.kotest.assertions.nondeterministic.eventually -import io.kotest.assertions.throwables.shouldThrowMessage -import io.kotest.core.annotation.DisplayName -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe -import org.rooftop.netx.api.* -import org.rooftop.netx.meta.EnableDistributedTransaction -import org.springframework.test.context.ContextConfiguration -import org.springframework.test.context.TestPropertySource -import reactor.test.StepVerifier -import kotlin.time.Duration.Companion.seconds - -@EnableDistributedTransaction -@ContextConfiguration( - classes = [ - RedisContainer::class, - MonoTransactionHandlerAssertions::class, - NoPublisherTransactionHandlerAssertions::class, - ] -) -@DisplayName("RedisStreamTransactionManager 클래스의") -@TestPropertySource("classpath:application.properties") -internal class RedisStreamTransactionManagerTest( - private val transactionManager: TransactionManager, - private val monoTransactionHandlerAssertions: MonoTransactionHandlerAssertions, - private val noPublisherTransactionHandlerAssertions: NoPublisherTransactionHandlerAssertions, -) : DescribeSpec({ - - beforeEach { - monoTransactionHandlerAssertions.clear() - noPublisherTransactionHandlerAssertions.clear() - } - - describe("start 메소드는") { - context("어떤 event도 없이 호출되면,") { - it("트랜잭션을 시작하고 transaction-id를 반환한다.") { - transactionManager.start().subscribe() - - eventually(5.seconds) { - monoTransactionHandlerAssertions.startCountShouldBe(1) - noPublisherTransactionHandlerAssertions.startCountShouldBe(1) - } - } - } - - context("서로 다른 id의 트랜잭션이 여러번 시작되어도") { - it("모두 읽을 수 있다.") { - transactionManager.start().block() - transactionManager.start().block() - - eventually(5.seconds) { - monoTransactionHandlerAssertions.startCountShouldBe(2) - noPublisherTransactionHandlerAssertions.startCountShouldBe(2) - } - } - } - } - - describe("syncStart 메소드는") { - context("어떤 event도 없이 호출되면,") { - it("트랜잭션을 시작하고 transaction-id를 반환한다.") { - transactionManager.syncStart() - - eventually(5.seconds) { - monoTransactionHandlerAssertions.startCountShouldBe(1) - noPublisherTransactionHandlerAssertions.startCountShouldBe(1) - } - } - } - - context("서로 다른 id의 트랜잭션이 여러번 시작되어도") { - it("모두 읽을 수 있다.") { - transactionManager.syncStart() - transactionManager.syncStart() - - eventually(5.seconds) { - monoTransactionHandlerAssertions.startCountShouldBe(2) - noPublisherTransactionHandlerAssertions.startCountShouldBe(2) - } - } - } - } - - describe("join 메소드는") { - context("존재하는 transactionId를 입력받으면,") { - val transactionId = transactionManager.start().block()!! - - it("트랜잭션에 참여한다.") { - transactionManager.join(transactionId).subscribe() - - eventually(5.seconds) { - monoTransactionHandlerAssertions.joinCountShouldBe(1) - noPublisherTransactionHandlerAssertions.joinCountShouldBe(1) - } - } - } - - context("존재하지 않는 transactionId를 입력받으면,") { - it("TransactionException 을 던진다.") { - val result = transactionManager.join(NOT_EXIST_TX_ID) - - StepVerifier.create(result) - .verifyErrorMessage("Cannot find exists transaction id \"$NOT_EXIST_TX_ID\"") - } - } - } - - describe("syncJoin 메소드는") { - context("존재하는 transactionId를 입력받으면,") { - val transactionId = transactionManager.syncStart() - - it("트랜잭션에 참여한다.") { - transactionManager.syncJoin(transactionId) - - eventually(5.seconds) { - monoTransactionHandlerAssertions.joinCountShouldBe(1) - noPublisherTransactionHandlerAssertions.joinCountShouldBe(1) - } - } - } - - context("존재하지 않는 transactionId를 입력받으면,") { - it("TransactionException 을 던진다.") { - shouldThrowMessage("Cannot find exists transaction id \"$NOT_EXIST_TX_ID\"") { - transactionManager.syncJoin(NOT_EXIST_TX_ID) - } - } - } - } - - describe("exists 메소드는") { - context("존재하는 transactionId를 입력받으면,") { - val transactionId = transactionManager.start().block()!! - - it("트랜잭션 id를 반환한다.") { - val result = transactionManager.exists(transactionId) - - StepVerifier.create(result) - .expectNext(transactionId) - .verifyComplete() - } - } - - context("존재하지 않는 transactionId를 입력받으면,") { - it("TransactionException 을 던진다.") { - val result = transactionManager.exists(NOT_EXIST_TX_ID) - - StepVerifier.create(result) - .verifyErrorMessage("Cannot find exists transaction id \"$NOT_EXIST_TX_ID\"") - } - } - } - - describe("syncExists 메소드는") { - context("존재하는 transactionId를 입력받으면,") { - val transactionId = transactionManager.syncStart() - - it("트랜잭션 id를 반환한다.") { - val result = transactionManager.syncExists(transactionId) - - result shouldBe transactionId - } - } - - context("존재하지 않는 transactionId를 입력받으면,") { - it("TransactionException 을 던진다.") { - shouldThrowMessage("Cannot find exists transaction id \"$NOT_EXIST_TX_ID\"") { - transactionManager.syncExists(NOT_EXIST_TX_ID) - } - } - } - } - - describe("commit 메소드는") { - context("존재하는 transactionId를 입력받으면,") { - val transactionId = transactionManager.start().block()!! - - it("commit 메시지를 publish 한다") { - transactionManager.commit(transactionId).block() - - eventually(5.seconds) { - monoTransactionHandlerAssertions.commitCountShouldBe(1) - noPublisherTransactionHandlerAssertions.commitCountShouldBe(1) - } - } - } - - context("존재하지 않는 transactionId를 입력받으면,") { - it("TransactionException 을 던진다.") { - val result = transactionManager.commit(NOT_EXIST_TX_ID) - - StepVerifier.create(result) - .verifyErrorMessage("Cannot find exists transaction id \"$NOT_EXIST_TX_ID\"") - } - } - } - - describe("syncCommit 메소드는") { - context("존재하는 transactionId를 입력받으면,") { - val transactionId = transactionManager.syncStart() - - it("commit 메시지를 publish 한다") { - transactionManager.syncCommit(transactionId) - - eventually(5.seconds) { - monoTransactionHandlerAssertions.commitCountShouldBe(1) - noPublisherTransactionHandlerAssertions.commitCountShouldBe(1) - } - } - } - - context("존재하지 않는 transactionId를 입력받으면,") { - it("TransactionException 을 던진다.") { - shouldThrowMessage("Cannot find exists transaction id \"$NOT_EXIST_TX_ID\"") { - transactionManager.syncCommit(NOT_EXIST_TX_ID) - } - } - } - } - - describe("rollback 메소드는") { - context("존재하는 transactionId를 입력받으면,") { - val transactionId = transactionManager.start().block()!! - - it("rollback 메시지를 publish 한다") { - transactionManager.rollback(transactionId, "rollback for test").block() - - eventually(5.seconds) { - monoTransactionHandlerAssertions.rollbackCountShouldBe(1) - noPublisherTransactionHandlerAssertions.rollbackCountShouldBe(1) - } - } - } - - context("존재하지 않는 transactionId를 입력받으면,") { - it("TransactionException 을 던진다.") { - val result = transactionManager.rollback(NOT_EXIST_TX_ID, "rollback for test") - - StepVerifier.create(result) - .verifyErrorMessage("Cannot find exists transaction id \"$NOT_EXIST_TX_ID\"") - } - } - } - - describe("syncRollback 메소드는") { - context("존재하는 transactionId를 입력받으면,") { - val transactionId = transactionManager.syncStart() - - it("rollback 메시지를 publish 한다") { - transactionManager.syncRollback(transactionId, "rollback for test") - - eventually(5.seconds) { - monoTransactionHandlerAssertions.rollbackCountShouldBe(1) - noPublisherTransactionHandlerAssertions.rollbackCountShouldBe(1) - } - } - } - - context("존재하지 않는 transactionId를 입력받으면,") { - it("TransactionException 을 던진다.") { - shouldThrowMessage("Cannot find exists transaction id \"$NOT_EXIST_TX_ID\"") { - transactionManager.syncRollback(NOT_EXIST_TX_ID, "rollback for test") - } - } - } - } -}) { - - private companion object { - private const val NOT_EXIST_TX_ID = "NOT_EXISTS_TX_ID" - } -} diff --git a/src/test/kotlin/org/rooftop/netx/redis/SagaNoRollbackForStorage.kt b/src/test/kotlin/org/rooftop/netx/redis/SagaNoRollbackForStorage.kt new file mode 100644 index 0000000..42a73e2 --- /dev/null +++ b/src/test/kotlin/org/rooftop/netx/redis/SagaNoRollbackForStorage.kt @@ -0,0 +1,34 @@ +package org.rooftop.netx.redis + +import org.rooftop.netx.api.SagaStartEvent +import org.rooftop.netx.api.SagaStartListener +import org.rooftop.netx.meta.SagaHandler +import reactor.core.publisher.Mono + +@SagaHandler +class SagaNoRollbackForStorage { + + @SagaStartListener( + RedisStreamSagaDispatcherNoRollbackForTest.IllegalArgumentExceptionEvent::class, + noRollbackFor = [IllegalArgumentException::class] + ) + fun noRollbackForIllegalArgumentException(sagaStartEvent: SagaStartEvent) { + throw IllegalArgumentException("No Rollback for IllegalArgumentExceptionEvent") + } + + @SagaStartListener( + RedisStreamSagaDispatcherNoRollbackForTest.UnSupportedOperationExceptionEvent::class, + noRollbackFor = [UnsupportedOperationException::class] + ) + fun noRollbackForUnSupportedOperationException(sagaStartEvent: SagaStartEvent): Mono { + throw UnsupportedOperationException("No Rollback for UnSupportedOperationExceptionEvent") + } + + @SagaStartListener( + RedisStreamSagaDispatcherNoRollbackForTest.NoSuchElementExceptionEvent::class, + noRollbackFor = [UnsupportedOperationException::class] + ) + fun doRollbackCauseThrowNoSuchElementException(sagaStartEvent: SagaStartEvent) { + throw NoSuchElementException("Rollback cause throw NoSuchElementExceptionEvent") + } +} diff --git a/src/test/kotlin/org/rooftop/netx/redis/TransactionNoRollbackForStorage.kt b/src/test/kotlin/org/rooftop/netx/redis/TransactionNoRollbackForStorage.kt deleted file mode 100644 index 0b3d2c5..0000000 --- a/src/test/kotlin/org/rooftop/netx/redis/TransactionNoRollbackForStorage.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.rooftop.netx.redis - -import org.rooftop.netx.api.TransactionStartEvent -import org.rooftop.netx.api.TransactionStartListener -import org.rooftop.netx.meta.TransactionHandler -import reactor.core.publisher.Mono - -@TransactionHandler -class TransactionNoRollbackForStorage { - - @TransactionStartListener( - RedisDispatcherNoRollbackForTest.IllegalArgumentExceptionEvent::class, - noRollbackFor = [IllegalArgumentException::class] - ) - fun noRollbackForIllegalArgumentException(transactionStartEvent: TransactionStartEvent) { - throw IllegalArgumentException("No Rollback for IllegalArgumentExceptionEvent") - } - - @TransactionStartListener( - RedisDispatcherNoRollbackForTest.UnSupportedOperationExceptionEvent::class, - noRollbackFor = [UnsupportedOperationException::class] - ) - fun noRollbackForUnSupportedOperationException(transactionStartEvent: TransactionStartEvent): Mono { - throw UnsupportedOperationException("No Rollback for UnSupportedOperationExceptionEvent") - } - - @TransactionStartListener( - RedisDispatcherNoRollbackForTest.NoSuchElementExceptionEvent::class, - noRollbackFor = [UnsupportedOperationException::class] - ) - fun doRollbackCauseThrowNoSuchElementException(transactionStartEvent: TransactionStartEvent) { - throw NoSuchElementException("Rollback cause throw NoSuchElementExceptionEvent") - } -}