From 88b94515fea9707e3aab5a35a674f3745ebf5e88 Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Mon, 5 Feb 2024 15:04:43 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20Netx=EA=B0=80=20undo=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A5=BC=20=EB=94=B0=EB=A1=9C=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 48 +++++++----- gradle.properties | 3 + gradle/core.gradle | 2 + idl | 2 +- .../rooftop/netx/api/TransactionManager.kt | 6 +- .../netx/api/TransactionRollbackEvent.kt | 2 +- .../engine/AbstractTransactionDispatcher.kt | 18 +++-- .../netx/engine/AbstractTransactionManager.kt | 21 +++-- .../org/rooftop/netx/engine/UndoManager.kt | 13 ++++ .../redis/AutoConfigureRedisTransaction.kt | 8 -- .../redis/RedisStreamTransactionDispatcher.kt | 4 +- .../redis/RedisStreamTransactionManager.kt | 9 ++- .../netx/redis/RedisTransactionConfigurer.kt | 5 +- .../rooftop/netx/redis/RedisUndoConfigurer.kt | 40 ++++++++++ .../rooftop/netx/redis/RedisUndoManager.kt | 36 +++++++++ .../META-INF/spring/spring.factories | 3 + .../org/rooftop/netx/redis/RedisContainer.kt | 5 ++ .../RedisStreamTransactionManagerTest.kt | 1 + .../netx/redis/RedisUndoManagerTest.kt | 76 +++++++++++++++++++ src/test/resources/application.properties | 2 + 20 files changed, 254 insertions(+), 50 deletions(-) create mode 100644 src/main/kotlin/org/rooftop/netx/engine/UndoManager.kt delete mode 100644 src/main/kotlin/org/rooftop/netx/redis/AutoConfigureRedisTransaction.kt create mode 100644 src/main/kotlin/org/rooftop/netx/redis/RedisUndoConfigurer.kt create mode 100644 src/main/kotlin/org/rooftop/netx/redis/RedisUndoManager.kt create mode 100644 src/test/kotlin/org/rooftop/netx/redis/RedisUndoManagerTest.kt diff --git a/README.md b/README.md index 0fe8778..c8f98d0 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,16 @@ > Distributed transaction library based on Choreography
- -![version 0.1.1](https://img.shields.io/badge/version-0.1.0-black?labelColor=black&style=flat-square) ![jdk 17](https://img.shields.io/badge/jdk-17-orange?labelColor=black&style=flat-square) + +![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) Choreography 방식으로 구현된 분산 트랜잭션 라이브러리 입니다. -`Netx` 는 다음 기능을 제공합니다. +`Netx` 는 다음 기능을 제공합니다. + 1. [Reactor](https://projectreactor.io/) 기반의 완전한 비동기 트랜잭션 관리 -2. Redis-stream 기반의 트랜잭션 관리 +2. Redis-stream 기반의 트랜잭션 관리 3. 여러 노드가 중복 트랜잭션 이벤트를 수신하는 문제 방지 4. `At Least Once` 방식의 메시지 전달 보장 @@ -34,17 +35,19 @@ class Application { } ``` -`@AutoconfigureRedisTransaction` 어노테이션으로 자동 구성할 경우 netx는 아래 프로퍼티를 사용해 메시지 큐와 커넥션을 맺습니다. +`@AutoconfigureRedisTransaction` 어노테이션으로 자동 구성할 경우 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.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.host** | localhost | 트랜잭션 관리에 사용할 메시지 큐 의 host url 입니다. (ex. redis host) | +| **netx.undo.port** | 6380 | 트랜잭션 관리에 사용할 메시지 큐의 port 입니다. | ### Usage example @@ -55,21 +58,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 +88,7 @@ fun order(param: Any): Mono { } } ``` - + #### Scenario3. Check exists transaction ```kotlin @@ -90,7 +99,8 @@ fun exists(param: Any): Mono { #### Scenario4. Handle transaction event -다른 분산서버가 (혹은 자기자신이) transactionManager를 통해서 트랜잭션을 시작하거나 트랜잭션 상태를 변경했을때, 호출한 메소드에 맞는 트랜잭션 이벤트를 발행합니다. +다른 분산서버가 (혹은 자기자신이) transactionManager를 통해서 트랜잭션을 시작하거나 트랜잭션 상태를 변경했을때, 호출한 메소드에 맞는 트랜잭션 이벤트를 +발행합니다. 이 이벤트들을 핸들링 함으로써, 다른서버에서 발생한 에러등을 수신하고 롤백할 수 있습니다. ```kotlin diff --git a/gradle.properties b/gradle.properties index d66db86..857598c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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/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..272409c 100644 --- a/src/main/kotlin/org/rooftop/netx/redis/RedisTransactionConfigurer.kt +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisTransactionConfigurer.kt @@ -1,6 +1,7 @@ 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.context.ApplicationEventPublisher @@ -20,15 +21,16 @@ 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 fun redisStreamTransactionManager(): TransactionManager = RedisStreamTransactionManager( nodeId, nodeName, applicationEventPublisher, + undoManager, reactiveRedisTemplate() ) @@ -37,6 +39,7 @@ class RedisTransactionConfigurer( RedisStreamTransactionDispatcher( applicationEventPublisher, reactiveRedisConnectionFactory(), + undoManager, group, nodeName, reactiveRedisTemplate() 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..603770c --- /dev/null +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisUndoConfigurer.kt @@ -0,0 +1,40 @@ +package org.rooftop.netx.redis + +import org.springframework.beans.factory.annotation.Value +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 + fun redisUndoManager(): RedisUndoManager = RedisUndoManager(group, redisUndoServer()) + + @Bean + fun redisUndoServer(): ReactiveRedisTemplate { + val stringRedisSerializer = StringRedisSerializer() + + val context = + RedisSerializationContext.newSerializationContext(stringRedisSerializer) + .value(stringRedisSerializer) + .build() + + return ReactiveRedisTemplate(undoServerConnectionFactory(), context) + } + + @Bean + 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 index 18fe703..0a97812 100644 --- a/src/main/resources/META-INF/spring/spring.factories +++ b/src/main/resources/META-INF/spring/spring.factories @@ -1,2 +1,5 @@ org.rooftop.netx.redis.AutoConfigureRedisTransaction=\ org.rooftop.netx.redis.RedisTransactionConfigurer + +org.rooftop.netx.redis.AutoConfigureRedisUndo=\ +org.rooftop.netx.redis.RedisUndoConfigurer 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..6f1c9f7 100644 --- a/src/test/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManagerTest.kt +++ b/src/test/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManagerTest.kt @@ -10,6 +10,7 @@ import org.springframework.test.context.TestPropertySource import reactor.test.StepVerifier import kotlin.time.Duration.Companion.minutes +@AutoConfigureRedisUndo @AutoConfigureRedisTransaction @ContextConfiguration( classes = [ 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..fe4c287 --- /dev/null +++ b/src/test/kotlin/org/rooftop/netx/redis/RedisUndoManagerTest.kt @@ -0,0 +1,76 @@ +package org.rooftop.netx.redis + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import org.rooftop.netx.engine.UndoManager +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource +import reactor.test.StepVerifier + +@AutoConfigureRedisUndo +@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..8f5095a 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -3,3 +3,5 @@ netx.port=6379 netx.group=netx-group netx.node-id=1 netx.node-name=netx-node +netx.undo.host=localhost +netx.undo.port=6379 From 0cd9c2440b88a7d524deaa6aead7a9f2e21af99c Mon Sep 17 00:00:00 2001 From: devxb Date: Tue, 6 Feb 2024 15:36:34 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=202=EA=B0=9C=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=EB=90=9C=20AutoConfigure=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=EC=9D=84=20=ED=95=98=EB=82=98?= =?UTF-8?q?=EB=A1=9C=20=ED=95=A9=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++-- gradle.properties | 2 +- .../autoconfig/AutoConfigureDistributedTransaction.kt | 10 ++++++++++ .../rooftop/netx/redis/RedisTransactionConfigurer.kt | 6 ++++++ .../org/rooftop/netx/redis/RedisUndoConfigurer.kt | 4 ++++ src/main/resources/META-INF/spring/spring.factories | 5 ----- .../netx/redis/RedisStreamTransactionManagerTest.kt | 4 ++-- .../org/rooftop/netx/redis/RedisUndoManagerTest.kt | 3 ++- src/test/resources/application.properties | 2 ++ 9 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 src/main/kotlin/org/rooftop/netx/autoconfig/AutoConfigureDistributedTransaction.kt delete mode 100644 src/main/resources/META-INF/spring/spring.factories diff --git a/README.md b/README.md index c8f98d0..954c942 100644 --- a/README.md +++ b/README.md @@ -41,13 +41,15 @@ class Application { | 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.host** | localhost | 트랜잭션 관리에 사용할 메시지 큐 의 host url 입니다. (ex. redis host) | -| **netx.undo.port** | 6380 | 트랜잭션 관리에 사용할 메시지 큐의 port 입니다. | +| **netx.undo.mode** | redis | 트랜잭션 undo 상태 저장에 사용할 저장소 구현체의 mode 입니다. | +| **netx.undo.host** | localhost | 트랜잭션 undo 상태 저장에 사용할 저장소의 host url 입니다. | +| **netx.undo.port** | 6380 | 트랜잭션 undo 상태 저장에 사용할 저장소의 port 입니다. | ### Usage example diff --git a/gradle.properties b/gradle.properties index 857598c..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 ### 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/redis/RedisTransactionConfigurer.kt b/src/main/kotlin/org/rooftop/netx/redis/RedisTransactionConfigurer.kt index 272409c..376852f 100644 --- a/src/main/kotlin/org/rooftop/netx/redis/RedisTransactionConfigurer.kt +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisTransactionConfigurer.kt @@ -4,6 +4,7 @@ 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 @@ -25,6 +26,7 @@ class RedisTransactionConfigurer( ) { @Bean + @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") fun redisStreamTransactionManager(): TransactionManager = RedisStreamTransactionManager( nodeId, @@ -35,6 +37,7 @@ class RedisTransactionConfigurer( ) @Bean + @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") fun redisStreamTransactionDispatcher(): RedisStreamTransactionDispatcher = RedisStreamTransactionDispatcher( applicationEventPublisher, @@ -46,6 +49,7 @@ class RedisTransactionConfigurer( ) @Bean + @ConditionalOnProperty(prefix = "netx", name = ["mode"], havingValue = "redis") fun reactiveRedisTemplate(): ReactiveRedisTemplate { val builder = RedisSerializationContext.newSerializationContext( StringRedisSerializer() @@ -57,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 index 603770c..5d807b2 100644 --- a/src/main/kotlin/org/rooftop/netx/redis/RedisUndoConfigurer.kt +++ b/src/main/kotlin/org/rooftop/netx/redis/RedisUndoConfigurer.kt @@ -1,6 +1,7 @@ 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 @@ -17,9 +18,11 @@ class RedisUndoConfigurer( ) { @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() @@ -32,6 +35,7 @@ class RedisUndoConfigurer( } @Bean + @ConditionalOnProperty(prefix = "netx.undo", name = ["mode"], havingValue = "redis") fun undoServerConnectionFactory(): ReactiveRedisConnectionFactory { val undoServerPort: String = System.getProperty("netx.undo.port") ?: netxUndoPort 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 0a97812..0000000 --- a/src/main/resources/META-INF/spring/spring.factories +++ /dev/null @@ -1,5 +0,0 @@ -org.rooftop.netx.redis.AutoConfigureRedisTransaction=\ -org.rooftop.netx.redis.RedisTransactionConfigurer - -org.rooftop.netx.redis.AutoConfigureRedisUndo=\ -org.rooftop.netx.redis.RedisUndoConfigurer diff --git a/src/test/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManagerTest.kt b/src/test/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManagerTest.kt index 6f1c9f7..a81ecef 100644 --- a/src/test/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManagerTest.kt +++ b/src/test/kotlin/org/rooftop/netx/redis/RedisStreamTransactionManagerTest.kt @@ -5,13 +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 -@AutoConfigureRedisUndo -@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 index fe4c287..268f976 100644 --- a/src/test/kotlin/org/rooftop/netx/redis/RedisUndoManagerTest.kt +++ b/src/test/kotlin/org/rooftop/netx/redis/RedisUndoManagerTest.kt @@ -2,12 +2,13 @@ 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 -@AutoConfigureRedisUndo +@AutoConfigureDistributedTransaction @ContextConfiguration(classes = [RedisContainer::class]) @TestPropertySource("classpath:application.properties") internal class RedisUndoManagerTest( diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 8f5095a..91f67be 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,7 +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