{"id":27598382,"url":"https://github.com/devxb/netx","last_synced_at":"2025-04-22T14:13:57.052Z","repository":{"id":220454996,"uuid":"751298974","full_name":"devxb/Netx","owner":"devxb","description":"Saga framework / Supports redis stream and blocking, reactive.","archived":false,"fork":false,"pushed_at":"2025-04-07T16:19:35.000Z","size":444,"stargazers_count":17,"open_issues_count":12,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-04-22T14:13:42.747Z","etag":null,"topics":["choreography","distributed-database","distributed-systems","microservice","microservices-architecture","orchestration","redis","redis-stream","saga","saga-pattern","transaction","transaction-manager"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devxb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-02-01T10:31:04.000Z","updated_at":"2025-04-07T16:19:39.000Z","dependencies_parsed_at":"2024-02-02T06:28:04.238Z","dependency_job_id":"88238334-3a18-4b96-9b5b-0e4bc4dc35e0","html_url":"https://github.com/devxb/Netx","commit_stats":null,"previous_names":["rooftop-msa/netx","devxb/netx"],"tags_count":36,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devxb%2FNetx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devxb%2FNetx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devxb%2FNetx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devxb%2FNetx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devxb","download_url":"https://codeload.github.com/devxb/Netx/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250255776,"owners_count":21400410,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["choreography","distributed-database","distributed-systems","microservice","microservices-architecture","orchestration","redis","redis-stream","saga","saga-pattern","transaction","transaction-manager"],"created_at":"2025-04-22T14:13:56.373Z","updated_at":"2025-04-22T14:13:57.041Z","avatar_url":"https://github.com/devxb.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Netx \u003cimg src=\"https://avatars.githubusercontent.com/u/149151221?s=200\u0026v=4\" height = 100 align = left\u003e\n\n\u003e Saga framework / Supports redis-stream and blocking, reactive paradigms.\n\n\u003cbr\u003e\n\n![version 0.5.0](https://img.shields.io/badge/version-0.4.9-black?labelColor=black\u0026style=flat-square) ![jdk 17](https://img.shields.io/badge/minimum_jdk-17-orange?labelColor=black\u0026style=flat-square) ![load-test](https://img.shields.io/badge/load%20test%2010%2C000%2C000-success-brightgreen?labelColor=black\u0026style=flat-square)    \n![redis--stream](https://img.shields.io/badge/-redis--stream-da2020?style=flat-square\u0026logo=Redis\u0026logoColor=white)\n\n**TPS(6,000)** on my Macbook air m2(default options). _[link](#Test1-TPS)_ \n\nNetx is a Saga framework, that provides following features.\n\n1. Supports redis-stream.\n2. Supports synchronous API and asynchronous [Reactor](https://projectreactor.io/) API.\n3. Supports both Orchestration and Choreograph.\n4. Automatically reruns loss events.\n5. Automatically applies **`Transactional messaging pattern`**.\n6. 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.\n7. Supports backpressure to control the number of events that can be processed per node.\n8. Prevents multiple nodes in the same group from receiving duplicate events.\n9. Ensures message delivery using the `At Least Once` approach.\n\nYou can see the test results [here](#Test).\n\n## Table of Contents\n- [Download](#download)\n- [How to use](#how-to-use)\n    - [Orchestrator-example.](#orchestrator-example)\n    - [Events-Example. Handle saga event](#events-example-handle-saga-event)\n    - [Events-Example. Start pay saga](#events-example-start-pay-saga)\n    - [Events-Example. Join order saga](#events-example-join-order-saga)\n    - [Events-Example. Check exists saga](#events-example-check-exists-saga)\n- [Rollback DeadLetter](#rollback-deadletter)\n    - [Example. relay deadLetter](#example-relay-deadletter)\n    - [Example. handle deadLetter message](#example-handle-deadletter-message)\n- [Test](#test)\n    - [Test1-TPS](#test1-tps)\n    - [Test2-Rollback](#test2-rollback)\n\n## Download\n\n```groovy\ndependencies {\n    implementation \"org.rooftopmsa:netx:${version}\"\n}\n```\n\n## How to use\n\nNetx can be used in Spring environments, and it can be easily configured by adding the `@EnableSaga` annotation as follows:\n\n```kotlin\n@EnableSaga\n@SpringBootApplication\nclass Application {\n\n    companion object {\n        @JvmStatic\n        fun main(vararg args: String) {\n            SpringApplication.run(Application::class.java, *args)\n        }\n    }\n}\n```\n\nWhen configured automatically with the `@EnableSaga` annotation, netx uses the following properties to establish connections with event stream services:\n\n#### Properties\n\n| KEY                     | EXAMPLE   | DESCRIPTION                                                                                                                                                                   | DEFAULT |\n|-------------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|\n| **netx.mode**           | redis     | Specifies the mode implementation used for Saga management. Currently, only redis is available as an option.                                                                                                                                            |         |\n| **netx.host**           | localhost | The host URL of the event stream used for Saga management. (e.g., redis host)                                                                                                                           |         |\n| **netx.password**       | 0000      | The password used to connect to the event stream used for Saga management. If not set, 0000 is mapped as the password.                                                                                                       | 0000    |\n| **netx.port**           | 6379      | The port of the message queue used for Saga management.                                                                                                                                                 |         |\n| **netx.group**          | pay-group | The group of distributed nodes. Saga events are sent to only one node within the same group.                                                                                                                                |         |\n| **netx.node-id**        | 1         | The identifier used for id generation. Each server must be assigned a different id, and ids can be set from 1 to 256. _`Ids are generated using the Twitter Snowflake algorithm to prevent duplicate id generation.`_                                            |         |\n| **netx.node-name**      | pay-1     | The name of the server participating in the _`netx.group`_. There should be no duplicate names within the same group.                                                                                                                    |         |\n| **netx.recovery-milli** | 1000      | Finds and reruns Sagas not processed for _`netx.orphan-milli`_ milliseconds every _`netx.recovery-milli`_ milliseconds.                                                                                                 | 1000    |\n| **netx.orphan-milli**   | 60000     | Finds events in the PENDING state that have not become ACK state even after _`netx.orphan-milli`_ milliseconds and restarts them.                                                                                                          | 60000   |\n| **netx.backpressure**   | 40        | Adjusts the number of events that can be received at once. **Setting this too high can cause server load, and setting it too low can reduce performance.** This setting affects the amount of events received from other servers and the amount of events failed to be processed. Unreceived or dropped events are automatically put into the retry queue. | 40      |\n| **netx.logging.level**  | info      | Specifies the logging level. Possible values are: \"info\", \"warn\", \"off\"                                                                                                            | \"off\"   |\n| **netx.pool-size**      | 40        | Used to adjust the maximum number of connections when connections need to be continuously established.                                                                                                                                       | 10      |\n\n### Usage example\n\n#### Orchestrator-example.\n\n\u003e [!TIP]   \n\u003e When using Orchestrator, `Transactional messaging pattern` is automatically applied.   \n\u003e The retry unit for event loss is each operation (one function) of the Orchestrator, and either all chains succeed or rollback is called.\n\n```kotlin\n// Use Orchestrator\n@Service\nclass OrderService(private val orderOrchestrator: Orchestrator\u003cOrder, OrderResponse\u003e) {\n\n    fun order(orderRequest: Order): OrderResult {\n        val result = orderOrchestrator.sagaSync(orderRequest)\n        \n        result.decodeResultOrThrow(OrderResult::class) // If success get result or else throw exception \n    }\n}\n\n// Register Orchestrator\n@Configurer\nclass OrchestratorConfigurer(\n    private val orchestratorFactory: OrchestratorFactory,\n) {\n\n    @Bean\n    fun orderOrchestartor(): Orchestrator\u003cOrder, OrderResponse\u003e { // \u003cFirst Request, Last Response\u003e\n        return orchestratorFactory.create\u003cOrder\u003e(\"orderOrchestrator\")\n            .start(\n                orchestrate = { order -\u003e // its order type\n                    // Do your bussiness logic \n                    // something like ... \"Check valid seller\"\n                    return@start user\n                },\n                rollback = { order -\u003e\n                    // do rollback logic\n                }\n            )\n            .joinReactive(\n                orchestrate = { user -\u003e // Before operations response type \"User\" flow here \n                    // Webflux supports, should return Mono type.\n                },\n                // Can skip rollback operation, if you want\n            )\n            .joinWithContext(\n                contextOrchestrate = { context, request -\u003e\n                    context.set(\"key\", request) // save data on context\n                    context.decode(\"foo\", Foo::class) // The context set in the upstream chain can be retrieved.\n                },\n            )\n            .commit(\n                orchestrate = { request -\u003e\n                    // If a rollback occurs here, all the above rollback functions will be executed sequentially.\n                    throw IllegalArgumentException(\"Oops! Something went wrong..\")\n                }\n            )\n    }\n}\n```\n\n#### Events-Example. Handle saga event\n\nWhen another distributed server (or itself) starts or changes the state of a saga through the sagaManager, the appropriate handler is called based on the state.    \nBy implementing this handler, you can implement logic for each saga state. If an error is thrown in each handler, rollback is automatically called, and when the handler is terminated, the state set in the annotation successWith is automatically called.\n\n\u003e [!WARNING]   \n\u003e Saga handlers must accept only **one** `Saga...Event` that corresponds to the handler.    \n\u003e When using Event, `Transactional messaging pattern` must be applied directly.    \n\u003e You can easily apply it by moving all business logic into the @Saga...Listener as shown below.\n\n```kotlin\n\n@SagaHandler\nclass SagaHandler(\n    private val sagaManager: SagaManager,\n) {\n    \n    fun start() {\n        val foo = Foo(\"...\")\n        sagaManager.startSync(foo) // it will call \n    }\n\n    @SagaStartListener(event = Foo::class, successWith = SuccessWith.PUBLISH_JOIN) // Receive saga event when event can be mapped to Foo.class\n    fun handleSagaStartEvent(event: SagaStartEvent) {\n        val foo: Foo = event.decodeEvent(Foo::class) // Get event field to Foo.class\n        // ...\n        event.setNextEvent(nextFoo) // When this handler terminates and calls the next event or rollback, the event set here is published together.\n    }\n\n    @SagaJoinListener(successWith = SuccessWith.PUBLISH_COMMIT) // Receive all saga event when no type is defined. And, when terminated this function, publish commit state\n    fun handleSagaJoinEvent(event: SagaJoinEvent) {\n        // ...\n    }\n\n    @SagaCommitListener(\n        event = Foo::class,\n        noRollbackFor = [IllegalArgumentException::class] // Don't rollback when throw IllegalArgumentException. *Rollback if throw Throwable or IllegalArgumentException's super type* \n    )\n    fun handleSagaCommitEvent(event: SagaCommitEvent): Mono\u003cString\u003e { // In Webflux framework, publisher must be returned.\n        throw IllegalArgumentException(\"Ignore this exception\")\n        // ...\n    }\n\n    @SagaRollbackListener(Foo::class)\n    fun handleSagaRollbackEvent(event: SagaRollbackEvent) { // In Mvc framework, publisher must not returned.\n        val undo: Foo = event.decodeUndo(Foo::class) // Get event field to Foo.class\n    }\n}\n```\n\n#### Events-Example. Start pay saga\n\n```kotlin\n// Sync\nfun pay(param: Any): Any {\n    val sagaId = sagaManager.syncStart(Pay(id = 1L, paid = 1000L)) // start saga\n\n    runCatching {\n        // Do your bussiness logic\n    }.fold(\n        onSuccess = { sagaManager.syncCommit(sagaId) }, // commit saga\n        onFailure = {\n            sagaManager.syncRollback(\n                sagaId,\n                it.message\n            )\n        } // rollback saga\n    )\n}\n\n// Async\nfun pay(param: Any): Mono\u003cAny\u003e {\n    return sagaManager.start(\n        Pay(\n            id = 1L,\n            paid = 1000L\n        )\n    ) // Start distributed saga and publish saga start event\n        .flatMap { sagaId -\u003e\n            service.pay(param)\n                .doOnError { throwable -\u003e\n                    sagaManager.rollback(\n                        sagaId,\n                        throwable.message\n                    ) // Publish rollback event to all saga joined node\n                }\n        }.doOnSuccess { sagaId -\u003e\n            sagaManager.commit(sagaId) // Publish commit event to all saga joined node\n        }\n}\n```\n\n#### Events-Example. Join order saga\n\n```kotlin\n//Sync\nfun order(param: Any): Any {\n    val sagaId = sagaManager.syncJoin(\n        param.saganId,\n        Order(id = 1L, state = PENDING)\n    ) // join saga\n\n    runCatching { // This is kotlin try catch, not netx library spec\n        // Do your bussiness logic\n    }.fold(\n        onSuccess = { sagaManager.syncCommit(sagaId) }, // commit saga\n        onFailure = {\n            sagaManager.syncRollback(\n                sagaId,\n                it.message\n            )\n        } // rollback saga\n    )\n}\n\n// Async\nfun order(param: Any): Mono\u003cAny\u003e {\n    return sagaManager.join(\n        param.sagaId,\n        Order(id = 1L, state = PENDING)\n    ) // join exists distributed saga and publish saga join event\n        .flatMap { sagaId -\u003e\n            service.order(param)\n                .doOnError { throwable -\u003e\n                    sagaManager.rollback(sagaId, throwable.message)\n                }\n        }.doOnSuccess { sagaId -\u003e\n            sagaManager.commit(sagaId)\n        }\n}\n```\n\n#### Events-Example. Check exists saga\n\n```kotlin\n// Sync\nfun exists(param: Any): Any {\n    return sagaManager.syncExists(param.sagaId)\n}\n\n// Async\nfun exists(param: Any): Mono\u003cAny\u003e {\n    return sagaManager.exists(param.sagaId) // Find any saga has ever been started \n}\n```\n\n### Rollback DeadLetter\n\nIn certain scenarios, an exception may occur during a rollback event.    \nWhen this happens, the event should not be retried. To accommodate this behavior, Netx automatically acknowledges (ACK) any rollback event that triggers an exception, rather than attempting a retry.   \nThe exception-causing event is then transferred to a dead-letter stream, enabling a manual retry process at a later time if needed.\n\n#### Example. relay deadLetter\n\n```kotlin\n\n@Component\nclass SomeClass(\n    private val deadLetterRelay: DeadLetterRelay,\n) {\n    \n    fun example() {\n        // Relay latest dead letter\n        deadLetterRelay.relay()\n            .subscribe()\n        \n        // Alternatively, you can use the …Sync method in a synchronous environment.\n        deadLetterRelay.relaySync()\n        \n        // Relay specific dead letter by deadLetterId \n        deadLetterRelay.relaySync(\"12345-01\")\n    }\n} \n\n```\n\n#### Example. handle deadLetter message\n\n```kotlin \n\n@Configuration\nclass SomeClass(\n    private val deadLetterRegistry: DeadLetterRegistry,\n) {\n    \n    fun example() {\n        deadLetterRegistry.addListener { deadLetterId, sagaEvent -\u003e\n            // do handle\n        }\n    }\n}\n```\n\n## Test\n\n### Test1-TPS\n\n\u003e **How to test?**    \n\u003e For 333,333 tests, the sequence proceeds as follows: saga start -\u003e saga join -\u003e saga commit.   \n\u003e For 444,444 tests, the sequence proceeds as follows: saga start -\u003e saga join -\u003e saga commit -\u003e saga rollback.   \n\u003e The combined test, consisting of both sequences, took a total of 2 minutes and 10 seconds.\n   \n\u003cimg width=\"700\" alt=\"Netx load test 777,777\" src=\"https://github.com/devxb/Netx/assets/62425964/2935f194-f246-40de-b9b3-be0505b19446\"\u003e\n\n\n### Test2-Rollback\n\n\u003e **How to test?**   \n\u003e Pending order -\u003e Pending payment -\u003e Successful payment -\u003e Successful order -\u003e Inventory deduction failure -\u003e Order failure -\u003e Payment failure\n\n\u003cimg src = \"https://github.com/rooftop-MSA/Netx/assets/62425964/08ed9050-1923-42b5-803f-5b7ea37a263f\"/\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevxb%2Fnetx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevxb%2Fnetx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevxb%2Fnetx/lists"}