diff --git a/README.md b/README.md index 2204dda..d21c5b5 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,15 @@ > Distributed transaction library based on Choreography
- -![version 0.1.1](https://img.shields.io/badge/version-0.1.1-black?labelColor=black&style=flat-square) ![jdk 17](https://img.shields.io/badge/jdk-17-orange?labelColor=black&style=flat-square) +![version 0.1.2](https://img.shields.io/badge/version-0.1.2-black?labelColor=black&style=flat-square) ![jdk 17](https://img.shields.io/badge/jdk-17-orange?labelColor=black&style=flat-square) Choreography 방식으로 구현된 분산 트랜잭션 라이브러리 입니다. -`Netx` 는 다음 기능을 제공합니다. +`Netx` 는 다음 기능을 제공합니다. + 1. [Reactor](https://projectreactor.io/) 기반의 완전한 비동기 트랜잭션 관리 -2. Redis-stream 기반의 트랜잭션 관리 +2. Redis-stream 기반의 트랜잭션 관리 3. 여러 노드가 중복 트랜잭션 이벤트를 수신하는 문제 방지 4. `At Least Once` 방식의 메시지 전달 보장 @@ -21,7 +21,7 @@ Netx는 스프링 환경에서 사용할 수 있으며, 아래와 같이 `@AutoC ```kotlin @SpringBootApplication -@AutoConfigureRedisTransaction +@AutoConfigureDistributedTransaction @EnableAutoConfiguration(exclude = [RedisReactiveAutoConfiguration::class]) class Application { @@ -34,17 +34,21 @@ class Application { } ``` -`@AutoconfigureRedisTransaction` 어노테이션으로 자동 구성할 경우 netx는 아래 프로퍼티를 사용해 메시지 큐와 커넥션을 맺습니다. +`@AutoConfigureDistributedTransaction` 어노테이션으로 자동 구성할 경우 netx는 아래 프로퍼티를 사용해 메시지 큐와 커넥션을 맺습니다. #### Properties -| key | example | description | -|------------------|----------|------------------------------------------------------------------------------------------------------------------------------------| -| **netx.host** | localhost | 트랜잭션 관리에 사용할 메시지 큐 의 host url 입니다. (ex. redis host) | -| **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`_ 에 참여할 서버의 이름입니다. 같은 그룹내에 중복된 이름이 존재하면 안됩니다. | +| key | example | description | +|--------------------|-----------|------------------------------------------------------------------------------------------------------------------------------------| +| **netx.mode** | redis | 트랜잭션 관리에 사용할 메시지 큐 구현체의 mode 입니다. | +| **netx.host** | localhost | 트랜잭션 관리에 사용할 메시지 큐 의 host url 입니다. (ex. redis host) | +| **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.undo.mode** | redis | 트랜잭션 undo 상태 저장에 사용할 저장소 구현체의 mode 입니다. | +| **netx.undo.host** | localhost | 트랜잭션 undo 상태 저장에 사용할 저장소의 host url 입니다. | +| **netx.undo.port** | 6380 | 트랜잭션 undo 상태 저장에 사용할 저장소의 port 입니다. | ### Usage example @@ -55,21 +59,27 @@ fun pay(param: Any): Mono { return transactionManager.start("paid=1000") // Start distributed transaction and publish transaction start event .flatMap { transactionId -> service.pay(param) - .doOnError { throwable -> - transactionManager.rollback(transactionId, throwable.message) // Publish rollback event to all transaction joined node + .doOnError { throwable -> + transactionManager.rollback( + transactionId, + throwable.message + ) // Publish rollback event to all transaction joined node } }.doOnSuccess { transactionId -> transactionManager.commit(transactionId) // Publish commit event to all transaction joined node } } ``` - + #### Scenario2. Join order transaction ```kotlin fun order(param: Any): Mono { - return transactionManager.join(param.transactionId, "orderId=1:state=PENDING") // join exists distributed transaction and publish transaction join event - .flatMap { transactionId -> + return transactionManager.join( + param.transactionId, + "orderId=1:state=PENDING" + ) // join exists distributed transaction and publish transaction join event + .flatMap { transactionId -> service.order(param) .doOnError { throwable -> transactionManager.rollback(transactionId, throwable.message) @@ -79,7 +89,7 @@ fun order(param: Any): Mono { } } ``` - + #### Scenario3. Check exists transaction ```kotlin @@ -90,7 +100,8 @@ fun exists(param: Any): Mono { #### Scenario4. Handle transaction event -다른 분산서버가 (혹은 자기자신이) transactionManager를 통해서 트랜잭션을 시작하거나 트랜잭션 상태를 변경했을때, 호출한 메소드에 맞는 트랜잭션 이벤트를 발행합니다. +다른 분산서버가 (혹은 자기자신이) transactionManager를 통해서 트랜잭션을 시작하거나 트랜잭션 상태를 변경했을때, 호출한 메소드에 맞는 트랜잭션 이벤트를 +발행합니다. 이 이벤트들을 핸들링 함으로써, 다른서버에서 발생한 에러등을 수신하고 롤백할 수 있습니다. ```kotlin diff --git a/gradle.properties b/gradle.properties index d66db86..0a8618e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ kotlin.code.style=official ### Project ### group=org.rooftop.netx -version=0.1.0 +version=0.1.2 compatibility=17 ### Protobuf ### @@ -36,3 +36,6 @@ lettuceVersion=6.3.0.RELEASE ### TestContainer ### testContainerVersion=1.19.3 + +### Jackson ### +jacksonVersion=2.16.1 diff --git a/gradle/core.gradle b/gradle/core.gradle index 433383f..71fa511 100644 --- a/gradle/core.gradle +++ b/gradle/core.gradle @@ -1,3 +1,5 @@ dependencies { implementation "com.github.f4b6a3:tsid-creator:${snowflakeVersion}" + implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + implementation "com.fasterxml.jackson.module:jackson-module-parameter-names:${jacksonVersion}" } diff --git a/idl b/idl index e1f481d..d0ea8e0 160000 --- a/idl +++ b/idl @@ -1 +1 @@ -Subproject commit e1f481d6f34e879b82487603c11cb7a57cc6a6ab +Subproject commit d0ea8e0b64253d3e7966e2b6d95a751f40fe27de diff --git a/src/main/kotlin/org/rooftop/netx/api/TransactionManager.kt b/src/main/kotlin/org/rooftop/netx/api/TransactionManager.kt index 74fc6d7..cc0b9d2 100644 --- a/src/main/kotlin/org/rooftop/netx/api/TransactionManager.kt +++ b/src/main/kotlin/org/rooftop/netx/api/TransactionManager.kt @@ -4,11 +4,11 @@ import reactor.core.publisher.Mono interface TransactionManager { - fun start(replay: String): Mono + fun start(undo: String): Mono - fun exists(transactionId: String): Mono + fun join(transactionId: String, undo: String): Mono - fun join(transactionId: String, replay: String): Mono + fun exists(transactionId: String): Mono fun commit(transactionId: String): Mono diff --git a/src/main/kotlin/org/rooftop/netx/api/TransactionRollbackEvent.kt b/src/main/kotlin/org/rooftop/netx/api/TransactionRollbackEvent.kt index ffa9d2a..addfdce 100644 --- a/src/main/kotlin/org/rooftop/netx/api/TransactionRollbackEvent.kt +++ b/src/main/kotlin/org/rooftop/netx/api/TransactionRollbackEvent.kt @@ -2,7 +2,7 @@ package org.rooftop.netx.api data class TransactionRollbackEvent( val transactionId: String, - val replay: String, val nodeName: String, val cause: String?, + val undoState: String, ) diff --git a/src/main/kotlin/org/rooftop/netx/autoconfig/AutoConfigureDistributedTransaction.kt b/src/main/kotlin/org/rooftop/netx/autoconfig/AutoConfigureDistributedTransaction.kt new file mode 100644 index 0000000..093beb9 --- /dev/null +++ b/src/main/kotlin/org/rooftop/netx/autoconfig/AutoConfigureDistributedTransaction.kt @@ -0,0 +1,10 @@ +package org.rooftop.netx.autoconfig + +import org.rooftop.netx.redis.RedisTransactionConfigurer +import org.rooftop.netx.redis.RedisUndoConfigurer +import org.springframework.boot.autoconfigure.ImportAutoConfiguration + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@ImportAutoConfiguration(RedisUndoConfigurer::class, RedisTransactionConfigurer::class) +annotation class AutoConfigureDistributedTransaction diff --git a/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionDispatcher.kt b/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionDispatcher.kt index b15af24..3450aac 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionDispatcher.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionDispatcher.kt @@ -11,12 +11,14 @@ import reactor.core.publisher.Flux import reactor.core.publisher.Mono abstract class AbstractTransactionDispatcher( + private val undoManager: UndoManager, private val eventPublisher: EventPublisher, ) { @EventListener(SubscribeTransactionEvent::class) fun subscribeStream(event: SubscribeTransactionEvent): Flux { - return receive(event).dispatch() + return receive(event) + .dispatch() } protected abstract fun receive(event: SubscribeTransactionEvent): Flux @@ -50,18 +52,20 @@ abstract class AbstractTransactionDispatcher( .doOnNext { eventPublisher.publish(TransactionCommitEvent(it.id, it.serverId)) } } - private fun publishRollback(it: Transaction): Mono { - return Mono.just(it) + private fun publishRollback(transaction: Transaction): Mono { + return undoManager.find(transaction.id) .doOnNext { eventPublisher.publish( TransactionRollbackEvent( - it.id, - it.replay, - it.serverId, - it.cause, + transaction.id, + transaction.serverId, + transaction.cause, + it ) ) } + .flatMap { undoManager.delete(transaction.id) } + .map { transaction } } private fun publishStart(it: Transaction): Mono { diff --git a/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionManager.kt b/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionManager.kt index 160a0d7..0a77b6b 100644 --- a/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionManager.kt +++ b/src/main/kotlin/org/rooftop/netx/engine/AbstractTransactionManager.kt @@ -11,39 +11,40 @@ abstract class AbstractTransactionManager( private val nodeName: String, private val eventPublisher: EventPublisher, private val transactionIdGenerator: TransactionIdGenerator = TransactionIdGenerator(nodeId), + private val undoManager: UndoManager, ) : TransactionManager { - final override fun start(replay: String): Mono { - return startTransaction(replay) + final override fun start(undo: String): Mono { + return startTransaction() .subscribeTransaction() + .saveUndoState(undo) .contextWrite { it.put(CONTEXT_TX_KEY, transactionIdGenerator.generate()) } } - private fun startTransaction(replay: String): Mono { + private fun startTransaction(): Mono { return Mono.deferContextual { Mono.just(it[CONTEXT_TX_KEY]) } .flatMap { transactionId -> publishTransaction(transactionId, transaction { id = transactionId serverId = nodeName - this.replay = replay this.state = TransactionState.TRANSACTION_STATE_START }) } } - final override fun join(transactionId: String, replay: String): Mono { + final override fun join(transactionId: String, undo: String): Mono { return exists(transactionId) - .joinTransaction(replay) + .joinTransaction() .subscribeTransaction() + .saveUndoState(undo) .contextWrite { it.put(CONTEXT_TX_KEY, transactionId) } } - private fun Mono.joinTransaction(replay: String): Mono { + private fun Mono.joinTransaction(): Mono { return flatMap { transactionId -> publishTransaction(transactionId, transaction { id = transactionId serverId = nodeName - this.replay = replay state = TransactionState.TRANSACTION_STATE_JOIN }) } @@ -55,6 +56,10 @@ abstract class AbstractTransactionManager( } } + private fun Mono.saveUndoState(undo: String): Mono { + return this.flatMap { undoManager.save(it, undo) } + } + final override fun rollback(transactionId: String, cause: String): Mono { return exists(transactionId) .publishTransaction(transaction { diff --git a/src/main/kotlin/org/rooftop/netx/engine/UndoManager.kt b/src/main/kotlin/org/rooftop/netx/engine/UndoManager.kt new file mode 100644 index 0000000..ab12d9e --- /dev/null +++ b/src/main/kotlin/org/rooftop/netx/engine/UndoManager.kt @@ -0,0 +1,13 @@ +package org.rooftop.netx.engine + +import reactor.core.publisher.Mono + +interface UndoManager { + + fun save(transactionId: String, undo: String): Mono + + fun find(transactionId: String): Mono + + fun delete(transactionId: String): Mono + +} diff --git a/src/main/kotlin/org/rooftop/netx/redis/AutoConfigureRedisTransaction.kt b/src/main/kotlin/org/rooftop/netx/redis/AutoConfigureRedisTransaction.kt deleted file mode 100644 index 12cefa0..0000000 --- a/src/main/kotlin/org/rooftop/netx/redis/AutoConfigureRedisTransaction.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.rooftop.netx.redis - -import org.springframework.boot.autoconfigure.ImportAutoConfiguration - -@ImportAutoConfiguration(RedisTransactionConfigurer::class) -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -annotation class AutoConfigureRedisTransaction diff --git a/src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionDispatcher.kt b/src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionDispatcher.kt index 3390a3c..fc5e7d7 100644 --- a/src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionDispatcher.kt +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionDispatcher.kt @@ -2,6 +2,7 @@ package org.rooftop.netx.redis import org.rooftop.netx.engine.AbstractTransactionDispatcher import org.rooftop.netx.engine.SubscribeTransactionEvent +import org.rooftop.netx.engine.UndoManager import org.rooftop.netx.idl.Transaction import org.springframework.context.ApplicationEventPublisher import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory @@ -18,10 +19,11 @@ import kotlin.time.toJavaDuration class RedisStreamTransactionDispatcher( eventPublisher: ApplicationEventPublisher, connectionFactory: ReactiveRedisConnectionFactory, + undoManager: UndoManager, private val streamGroup: String, private val nodeName: String, private val reactiveRedisTemplate: ReactiveRedisTemplate, -) : AbstractTransactionDispatcher(SpringEventPublisher(eventPublisher)) { +) : AbstractTransactionDispatcher(undoManager, SpringEventPublisher(eventPublisher)) { private val options = StreamReceiver.StreamReceiverOptions.builder() .pollTimeout(1.hours.toJavaDuration()) diff --git a/src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManager.kt b/src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManager.kt index 8d0c31f..2e37ef6 100644 --- a/src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManager.kt +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManager.kt @@ -1,6 +1,7 @@ package org.rooftop.netx.redis import org.rooftop.netx.engine.AbstractTransactionManager +import org.rooftop.netx.engine.UndoManager import org.rooftop.netx.idl.Transaction import org.springframework.context.ApplicationEventPublisher import org.springframework.data.domain.Range @@ -12,8 +13,14 @@ class RedisStreamTransactionManager( nodeId: Int, nodeName: String, applicationEventPublisher: ApplicationEventPublisher, + undoManager: UndoManager, private val reactiveRedisTemplate: ReactiveRedisTemplate, -) : AbstractTransactionManager(nodeId, nodeName, SpringEventPublisher(applicationEventPublisher)) { +) : AbstractTransactionManager( + nodeId, + nodeName, + SpringEventPublisher(applicationEventPublisher), + undoManager = undoManager +) { override fun findAnyTransaction(transactionId: String): Mono { return reactiveRedisTemplate.opsForStream() diff --git a/src/main/kotlin/org/rooftop/netx/redis/RedisTransactionConfigurer.kt b/src/main/kotlin/org/rooftop/netx/redis/RedisTransactionConfigurer.kt index eb16a91..376852f 100644 --- a/src/main/kotlin/org/rooftop/netx/redis/RedisTransactionConfigurer.kt +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisTransactionConfigurer.kt @@ -1,8 +1,10 @@ package org.rooftop.netx.redis import org.rooftop.netx.api.TransactionManager +import org.rooftop.netx.engine.UndoManager import org.rooftop.pay.infra.transaction.ByteArrayRedisSerializer import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.context.ApplicationEventPublisher import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -20,29 +22,34 @@ class RedisTransactionConfigurer( @Value("\${netx.node-id}") private val nodeId: Int, @Value("\${netx.node-name}") private val nodeName: String, private val applicationEventPublisher: ApplicationEventPublisher, + private val undoManager: UndoManager, ) { - @Bean + @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") fun redisStreamTransactionManager(): TransactionManager = RedisStreamTransactionManager( nodeId, nodeName, applicationEventPublisher, + undoManager, reactiveRedisTemplate() ) @Bean + @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") fun redisStreamTransactionDispatcher(): RedisStreamTransactionDispatcher = RedisStreamTransactionDispatcher( applicationEventPublisher, reactiveRedisConnectionFactory(), + undoManager, group, nodeName, reactiveRedisTemplate() ) @Bean + @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") fun reactiveRedisTemplate(): ReactiveRedisTemplate { val builder = RedisSerializationContext.newSerializationContext( StringRedisSerializer() @@ -54,11 +61,13 @@ class RedisTransactionConfigurer( } @Bean + @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") fun byteArrayRedisSerializer(): ByteArrayRedisSerializer { return ByteArrayRedisSerializer() } @Bean + @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") fun reactiveRedisConnectionFactory(): ReactiveRedisConnectionFactory { val port: String = System.getProperty("netx.port") ?: port diff --git a/src/main/kotlin/org/rooftop/netx/redis/RedisUndoConfigurer.kt b/src/main/kotlin/org/rooftop/netx/redis/RedisUndoConfigurer.kt new file mode 100644 index 0000000..5d807b2 --- /dev/null +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisUndoConfigurer.kt @@ -0,0 +1,44 @@ +package org.rooftop.netx.redis + +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory +import org.springframework.data.redis.core.ReactiveRedisTemplate +import org.springframework.data.redis.serializer.RedisSerializationContext +import org.springframework.data.redis.serializer.StringRedisSerializer + +@Configuration +class RedisUndoConfigurer( + @Value("\${netx.group}") private val group: String, + @Value("\${netx.undo.host}") private val netxUndoHost: String, + @Value("\${netx.undo.port}") private val netxUndoPort: String, +) { + + @Bean + @ConditionalOnProperty(prefix = "netx.undo", name = ["mode"], havingValue = "redis") + fun redisUndoManager(): RedisUndoManager = RedisUndoManager(group, redisUndoServer()) + + @Bean + @ConditionalOnProperty(prefix = "netx.undo", name = ["mode"], havingValue = "redis") + fun redisUndoServer(): ReactiveRedisTemplate { + val stringRedisSerializer = StringRedisSerializer() + + val context = + RedisSerializationContext.newSerializationContext(stringRedisSerializer) + .value(stringRedisSerializer) + .build() + + return ReactiveRedisTemplate(undoServerConnectionFactory(), context) + } + + @Bean + @ConditionalOnProperty(prefix = "netx.undo", name = ["mode"], havingValue = "redis") + fun undoServerConnectionFactory(): ReactiveRedisConnectionFactory { + val undoServerPort: String = System.getProperty("netx.undo.port") ?: netxUndoPort + + return LettuceConnectionFactory(netxUndoHost, undoServerPort.toInt()) + } +} diff --git a/src/main/kotlin/org/rooftop/netx/redis/RedisUndoManager.kt b/src/main/kotlin/org/rooftop/netx/redis/RedisUndoManager.kt new file mode 100644 index 0000000..11d18c5 --- /dev/null +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisUndoManager.kt @@ -0,0 +1,36 @@ +package org.rooftop.netx.redis + +import org.rooftop.netx.engine.UndoManager +import org.springframework.data.redis.core.ReactiveRedisTemplate +import reactor.core.publisher.Mono + +class RedisUndoManager( + private val group: String, + private val reactiveRedisTemplate: ReactiveRedisTemplate, +) : UndoManager { + + override fun find(transactionId: String): Mono { + return reactiveRedisTemplate.opsForValue()["$group:$transactionId"] + .switchIfEmpty( + Mono.error { + throw IllegalStateException("Cannot find undo state \"$group:$transactionId\"") + } + ) + } + + override fun delete(transactionId: String): Mono { + return reactiveRedisTemplate.opsForValue().delete("$group:$transactionId") + } + + override fun save(transactionId: String, undo: String): Mono { + return reactiveRedisTemplate.opsForValue() + .set("$group:$transactionId", undo) + .flatMap { + when (it) { + true -> Mono.just(it) + false -> Mono.error { throw IllegalStateException("Error occurred during the undo process.") } + } + } + .map { transactionId } + } +} diff --git a/src/main/resources/META-INF/spring/spring.factories b/src/main/resources/META-INF/spring/spring.factories deleted file mode 100644 index 18fe703..0000000 --- a/src/main/resources/META-INF/spring/spring.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.rooftop.netx.redis.AutoConfigureRedisTransaction=\ -org.rooftop.netx.redis.RedisTransactionConfigurer diff --git a/src/test/kotlin/org/rooftop/netx/redis/RedisContainer.kt b/src/test/kotlin/org/rooftop/netx/redis/RedisContainer.kt index 29e5625..8329778 100644 --- a/src/test/kotlin/org/rooftop/netx/redis/RedisContainer.kt +++ b/src/test/kotlin/org/rooftop/netx/redis/RedisContainer.kt @@ -1,5 +1,6 @@ package org.rooftop.netx.redis +import io.mockk.InternalPlatformDsl.toStr import org.springframework.boot.test.context.TestConfiguration import org.testcontainers.containers.GenericContainer import org.testcontainers.utility.DockerImageName @@ -16,5 +17,9 @@ class RedisContainer { "netx.port", redis.getMappedPort(6379).toString() ) + System.setProperty( + "netx.undo.port", + redis.getMappedPort(6379).toString() + ) } } diff --git a/src/test/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManagerTest.kt b/src/test/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManagerTest.kt index 5272ee1..a81ecef 100644 --- a/src/test/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManagerTest.kt +++ b/src/test/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManagerTest.kt @@ -5,12 +5,13 @@ 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.autoconfig.AutoConfigureDistributedTransaction import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.TestPropertySource import reactor.test.StepVerifier import kotlin.time.Duration.Companion.minutes -@AutoConfigureRedisTransaction +@AutoConfigureDistributedTransaction @ContextConfiguration( classes = [ EventCapture::class, diff --git a/src/test/kotlin/org/rooftop/netx/redis/RedisUndoManagerTest.kt b/src/test/kotlin/org/rooftop/netx/redis/RedisUndoManagerTest.kt new file mode 100644 index 0000000..268f976 --- /dev/null +++ b/src/test/kotlin/org/rooftop/netx/redis/RedisUndoManagerTest.kt @@ -0,0 +1,77 @@ +package org.rooftop.netx.redis + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import org.rooftop.netx.autoconfig.AutoConfigureDistributedTransaction +import org.rooftop.netx.engine.UndoManager +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource +import reactor.test.StepVerifier + +@AutoConfigureDistributedTransaction +@ContextConfiguration(classes = [RedisContainer::class]) +@TestPropertySource("classpath:application.properties") +internal class RedisUndoManagerTest( + private val undoManager: UndoManager, +) : DescribeSpec({ + + describe("find 메소드는") { + context("transactionId를 받으면,") { + + val transactionId = "TX-1" + val undoState = "id:1" + undoManager.save(transactionId, undoState).block() + + it("저장된 UndoState를 반환한다.") { + val result = undoManager.find(transactionId) + + StepVerifier.create(result) + .assertNext { + it shouldBe undoState + } + .verifyComplete() + } + } + + context("존재하지 않는 transactionId를 받으면,") { + val transactionId = "UNKNOWN_TX" + + it("IllegalStateException 을 던진다.") { + val result = undoManager.find(transactionId) + + StepVerifier.create(result) + .verifyErrorMessage("Cannot find undo state \"netx-group:UNKNOWN_TX\"") + } + } + } + + describe("delete 메소드는") { + context("transactionId를 받으면,") { + + val transactionId = "TX-2" + val undoState = "id:2" + undoManager.save(transactionId, undoState).block() + + it("undoState 를 삭제하고 true를 반환한다.") { + val result = undoManager.delete(transactionId) + + StepVerifier.create(result) + .expectNext(true) + .verifyComplete() + } + } + + context("존재하지 않는 transactionId를 받으면") { + + val transactionId = "UNKNOWN_TX" + + it("false 를 반환한다.") { + val result = undoManager.delete(transactionId) + + StepVerifier.create(result) + .expectNext(false) + .verifyComplete() + } + } + } +}) diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 8b3384d..91f67be 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,5 +1,9 @@ +netx.mode=redis netx.host=localhost netx.port=6379 netx.group=netx-group netx.node-id=1 netx.node-name=netx-node +netx.undo.mode=redis +netx.undo.host=localhost +netx.undo.port=6379