diff --git a/README.md b/README.md
index 6fd94cc..8809c11 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-  
+  

**TPS(6,000)** on my Macbook air m2(default options). _[link](#Test1-TPS)_
@@ -16,12 +16,28 @@ Netx is a Saga framework, that provides following features.
3. Supports both Orchestration and Choreograph.
4. Automatically reruns loss events.
5. Automatically applies **`Transactional messaging pattern`**.
-6. Supports backpressure to control the number of events that can be processed per node.
-7. Prevents multiple nodes in the same group from receiving duplicate events.
-8. Ensures message delivery using the `At Least Once` approach.
+6. Supports **`Rollback Dead letter`** relay. If an exception occurs during the rollback process, saga is stored in the Dead Letter Queue, and you can relay it using DeadLetterRelay.
+7. Supports backpressure to control the number of events that can be processed per node.
+8. Prevents multiple nodes in the same group from receiving duplicate events.
+9. Ensures message delivery using the `At Least Once` approach.
You can see the test results [here](#Test).
+## Table of Contents
+- [Download](#download)
+- [How to use](#how-to-use)
+ - [Orchestrator-example.](#orchestrator-example)
+ - [Events-Example. Handle saga event](#events-example-handle-saga-event)
+ - [Events-Example. Start pay saga](#events-example-start-pay-saga)
+ - [Events-Example. Join order saga](#events-example-join-order-saga)
+ - [Events-Example. Check exists saga](#events-example-check-exists-saga)
+- [Rollback DeadLetter](#rollback-deadletter)
+ - [Example. relay deadLetter](#example-relay-deadletter)
+ - [Example. handle deadLetter message](#example-handle-deadletter-message)
+- [Test](#test)
+ - [Test1-TPS](#test1-tps)
+ - [Test2-Rollback](#test2-rollback)
+
## Download
```groovy
@@ -274,6 +290,49 @@ fun exists(param: Any): Mono {
}
```
+### Rollback DeadLetter
+
+#### Example. relay deadLetter
+
+```kotlin
+
+@Component
+class SomeClass(
+ private val deadLetterRelay: DeadLetterRelay,
+) {
+
+ fun example() {
+ // Relay latest dead letter
+ deadLetterRelay.relay()
+ .subscribe()
+
+ // Alternatively, you can use the …Sync method in a synchronous environment.
+ deadLetterRelay.relaySync()
+
+ // Relay specific dead letter by deadLetterId
+ deadLetterRelay.relaySync("12345-01")
+ }
+}
+
+```
+
+#### Example. handle deadLetter message
+
+```kotlin
+
+@Configuration
+class SomeClass(
+ private val deadLetterRegistry: DeadLetterRegistry,
+) {
+
+ fun example() {
+ deadLetterRegistry.addListener { deadLetterId, sagaEvent ->
+ // do handle
+ }
+ }
+}
+```
+
## Test
### Test1-TPS
diff --git a/gradle.properties b/gradle.properties
index 7a57e5f..be4c05b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,7 +2,7 @@ kotlin.code.style=official
### Project ###
group=org.rooftopmsa
-version=0.4.8
+version=0.4.9
compatibility=17
### Sonarcloud ###
diff --git a/src/main/kotlin/org/rooftop/netx/api/DeadLetterRelay.kt b/src/main/kotlin/org/rooftop/netx/api/DeadLetterRelay.kt
new file mode 100644
index 0000000..3486699
--- /dev/null
+++ b/src/main/kotlin/org/rooftop/netx/api/DeadLetterRelay.kt
@@ -0,0 +1,37 @@
+package org.rooftop.netx.api
+
+import reactor.core.publisher.Mono
+
+/**
+ * An interface for relay dead letters, with support for recovering those that failed during rollback.
+ */
+interface DeadLetterRelay {
+
+ /**
+ * relay dead letter and return SagaEvent
+ *
+ * @return SagaEvent | A dead letter that was successfully processed
+ */
+ fun relay(): Mono
+
+ /**
+ * @see relay
+ * @return SagaEvent | A dead letter that was successfully processed
+ */
+ fun relaySync(): SagaEvent
+
+
+ /**
+ * relay dead letter and return SagaEvent by specific deadLetterId (not a same SagaEvent.id)
+ *
+ * @param deadLetterId
+ * @return SagaEvent | A dead letter that was successfully processed
+ */
+ fun relay(deadLetterId: String): Mono
+
+ /**
+ * @see relay
+ * @return SagaEvent | A dead letter that was successfully processed (not a same SagaEvent.id)
+ */
+ fun relaySync(deadLetterId: String): SagaEvent
+}
diff --git a/src/main/kotlin/org/rooftop/netx/api/Exceptions.kt b/src/main/kotlin/org/rooftop/netx/api/Exceptions.kt
index c29cf7a..2eb9b7b 100644
--- a/src/main/kotlin/org/rooftop/netx/api/Exceptions.kt
+++ b/src/main/kotlin/org/rooftop/netx/api/Exceptions.kt
@@ -18,5 +18,9 @@ class FailedAckSagaException(message: String) : RuntimeException(message)
class ResultTimeoutException(message: String, throwable: Throwable) :
RuntimeException(message, throwable)
+class DeadLetterTimeoutException(message: String): RuntimeException(message)
+
+class DeadLetterException(message: String): RuntimeException(message)
+
@JsonIgnoreProperties(ignoreUnknown = true)
class ResultException(message: String) : RuntimeException(message)
diff --git a/src/main/kotlin/org/rooftop/netx/api/SagaEvent.kt b/src/main/kotlin/org/rooftop/netx/api/SagaEvent.kt
index e07a2d7..0399b87 100644
--- a/src/main/kotlin/org/rooftop/netx/api/SagaEvent.kt
+++ b/src/main/kotlin/org/rooftop/netx/api/SagaEvent.kt
@@ -84,5 +84,28 @@ sealed class SagaEvent(
type
)
+ /**
+ * If you use Orchestrator and get SagaEvent that hold orchestrateEvent.
+ * you can get type class by using this function.
+ *
+ * @param type
+ * @param T
+ */
+ fun decodeOrchestrateEvent(type: Class): T = decodeOrchestrateEvent(type.kotlin)
+
+ /**
+ * @see decodeOrchestrateEvent
+ */
+ fun decodeOrchestrateEvent(type: KClass): T {
+ val orchestrateEvent = codec.decode(
+ event ?: throw NullPointerException("Cannot decode event cause event is null"),
+ object : TypeReference