Wednesday, July 8, 2026

From Stuck to Fixed: Debugging an LRA Heuristic with an AI Assistant

 

Distributed sagas are hard to debug. When a participant service fails mid-transaction, the

coordinator enters a *heuristic* state — a stuck, partially-compensated mess that requires

manual operator action. Finding out *which* participant failed, *why*, and *what to do about

it* normally means grepping logs across services and reading coordinator state dumps.


This post walks through a live example using the Narayana LRA coordinator and the LRA AI

dashboard — a Quarkus application that lets you chat with the coordinator state in plain English.

We will deliberately break a saga, watch it enter a heuristic state, and then fix it through

a conversation with the AI assistant.


---


## What Is a Heuristic Outcome?


In the LRA (Long Running Actions) model, when you cancel a transaction the coordinator asks

every enrolled participant to *compensate* — roll back its local changes. If a participant

service is down or returns an error, the coordinator retries periodically. When it gives up

after exhausting retries, the LRA enters **FailedToCancel** and the participant is marked

**FailedToCompensate**. At that point automatic recovery is no longer possible and an operator

must intervene.


This is the heuristic outcome: the saga is partially compensated, state is inconsistent, and

a human needs to decide what to do.


---


## Setup


You need three things running before starting:


| Component | Port |

|-----------|------|

| LRA coordinator | 8080 |

| LRA AI assistant | 8082 |

| Mock participant (bad) | 9999 |


**1 — LRA coordinator**


```bash

podman run --network host quay.io/jbosstm/lra-coordinator:latest

```


`--network host` is required so the coordinator can call back to the mock participant running on the host (`localhost:9999`).


**2 — AI assistant**


```bash

git clone https://github.com/marcosgopen/lra-ai-dashboard.git

cd lra-ai-dashboard

mvn quarkus:dev -Dquarkus.http.port=8082

```


Open **http://localhost:8082** — you should see the chat UI.

 

Note: the quarkus application should start a container with the llama3.1 model, if you want to install ollama by yourself you can see the guide here https://github.com/marcosgopen/lra-ai-dashboard#install-and-start-ollama

 

**3 — A deliberately broken participant service**


This one-liner starts an HTTP server on port 9999 that returns `500 FailedToCompensate`.

The body is critical: a bare 500 with no body is treated as a transient error and retried

every 120 seconds indefinitely — the LRA would stay stuck in `Cancelling` forever. The

coordinator only moves the LRA to `FailedToCancel` when the response body is a valid

`ParticipantStatus` enum value (`FailedToCompensate` or `FailedToComplete`).


```bash

python3 -c "

import http.server


class AlwaysFail(http.server.BaseHTTPRequestHandler):

def do_PUT(self):

body = b'FailedToCompensate'

self.send_response(500)

self.send_header('Content-Type', 'text/plain')

self.send_header('Content-Length', str(len(body)))

self.end_headers()

self.wfile.write(body)

print(f' [{self.command}] {self.path} -> 500 FailedToCompensate')

def log_message(self, *a): pass


print('Bad participant listening on :9999')

http.server.HTTPServer(('', 9999), AlwaysFail).serve_forever()

"

```


---


## Creating the LRA and Enrolling the Bad Participant


Open a new terminal. The steps below use `curl` to drive the coordinator REST API directly.


**Start a new LRA:**


```bash

LRA_ID=$(curl -s -X POST \

"http://localhost:8080/lra-coordinator/start?ClientID=payment-saga&TimeLimit=0" \

-H "Accept: text/plain")


echo "LRA ID: $LRA_ID"

# → http://localhost:8080/lra-coordinator/0_ffff7f000001_b07b_68401234_0

```


**Enroll the bad participant:**


The coordinator records participant callback URLs from the `Link` header. It will call

`compensate` on cancel and `complete` on close.


```bash

curl -s -X PUT "$LRA_ID" \

-H "Link: <http://localhost:9999/compensate>;rel=compensate, <http://localhost:9999/complete>;rel=complete, <http://localhost:9999/forget>;rel=forget" \

-H "Long-Running-Action: $LRA_ID" \

-H "Accept: text/plain"

```


A successful enrolment returns a recovery URL — save it, you may need it later:


```

http://localhost:8080/lra-coordinator/recovery/0_ffff7f000001_b07b_68401234_0/1

```


At this point the LRA is `Active` with one enrolled participant. Now switch entirely to the

AI assistant — all remaining steps happen through the chat UI.


---


## Step 1 — AI Lists the Active Transactions


Open the chat UI at **http://localhost:8082** and ask:


> **You:** Show me all active transactions.


The assistant calls `listAllLRAs()` and responds with something like:


> **Assistant:** There is one active LRA:

>

> — **LRA** `http://localhost:8080/lra-coordinator/0_ffff7f000001_b07b_68401234_0`

> — **Status:** Active

> — **ClientID:** payment-saga

>

> This transaction is in progress and has not been closed or cancelled yet.


The AI can see the LRA because it is still in the active store. The coordinator API returns

LRA-level metadata (ID, clientId, status, timestamps) but does not expose participant details

such as participant URLs or per-participant status.


---


## Step 2 — AI Cancels the Saga


Ask the assistant to cancel the transaction:


> **You:** Cancel the payment-saga LRA.


The assistant calls `cancelLRA()` with the LRA ID. The coordinator processes the cancel

synchronously: it calls `PUT http://localhost:9999/compensate`, gets back

`500 FailedToCompensate`, immediately transitions the LRA to **FailedToCancel**, and moves it

to the failed store — all before returning the response. The AI sees the result and explains:


> **Assistant:** I cancelled LRA

> `http://localhost:8080/lra-coordinator/0_ffff7f000001_b07b_68401234_0`.

>

> The cancel completed, but the result is **FailedToCancel** — at least one participant

> returned a permanent failure (`FailedToCompensate`) during compensation. The coordinator

> has given up and moved this LRA to the failed store. There is no automatic retry.

>

> To resolve this you will need to:

> 1. Check the coordinator logs to identify which participant failed and why.

> 2. Fix the failing service.

> 3. Call the participant's compensate endpoint directly — the coordinator will not retry.

> 4. Once done, tell me and I will remove the failed record from the coordinator.


Watch the bad participant terminal — you will see the single compensate call:


```

Bad participant listening on :9999

[PUT] /compensate -> 500 FailedToCompensate

```


---


## Step 3 — Fixing the Participant


In this demo you already know the failing participant is `localhost:9999` because you enrolled

it yourself. In a production system you would identify it from the coordinator logs as the

assistant suggested.


**Restore the participant service**


Stop the broken server (Ctrl-C in its terminal) and start a fixed version that returns `200`:


```bash

python3 -c "

import http.server


class AlwaysOK(http.server.BaseHTTPRequestHandler):

def do_PUT(self):

body = b'Compensated'

self.send_response(200)

self.send_header('Content-Type', 'text/plain')

self.send_header('Content-Length', str(len(body)))

self.end_headers()

self.wfile.write(body)

print(f' [{self.command}] {self.path} -> 200 Compensated')

def log_message(self, *a): pass


print('Fixed participant listening on :9999')

http.server.HTTPServer(('', 9999), AlwaysOK).serve_forever()

"

```


**Manually drive the compensation**


The coordinator will not call the participant again — you must do it directly:


```bash

curl -s -X PUT http://localhost:9999/compensate

# → 200 Compensated

```


This is the actual business compensation: the participant rolls back its local state. In a real

system this is the step where you restore a database row, reverse a payment, release a booking,

and so on.


---


## Step 4 — AI Cleans Up the Failed Record


With the participant compensated, tell the assistant and ask it to close the books:


> **You:** I have manually compensated the participant. Please clean up the failed LRA record.


The assistant calls `listFailedLRAs()` to confirm the record is still there, then calls

`deleteFailedLRA()` to remove it:


> **Assistant:**

> I've deleted the failed LRA record. The participant has been successfully compensated and the transaction can now be considered complete.

>

> **Result:** HTTP 204. The failed LRA record has been removed from the coordinator's

> ObjectStore. The saga is fully resolved — all business-level compensation has been confirmed

> and the coordinator no longer tracks this transaction.


---


## Summary


| Step | What happened |

|------|---------------|

| Create LRA | Coordinator created a new saga in `Active` state |

| Enroll bad participant | Coordinator recorded callback URLs for `localhost:9999` |

| AI: list LRAs | Assistant found the Active LRA via `listAllLRAs()` |

| AI: cancel | Assistant called `cancelLRA()` → coordinator called `/compensate` → got `500 FailedToCompensate``FailedToCancel` |

| AI: diagnosis | Assistant explained the failure state and recommended checking coordinator logs for the failing participant |

| Fix participant | Operator restored service and called compensate endpoint directly |

| AI: cleanup | Assistant called `deleteFailedLRA` → coordinator record removed → saga resolved |


The AI assistant drove the entire workflow — monitoring, action, diagnosis, and cleanup — through

a natural-language conversation. The operator made the decisions; the assistant handled the

coordinator API and explained what was happening at each step.


---


## Going Further


- **Real participants:** Replace the Python mock with a Quarkus microservice using the

`@Compensate` and `@Complete` annotations from the MicroProfile LRA API.

- **Nested LRAs:** A parent LRA cancellation propagates to its children — ask the assistant

to trace the compensation cascade down the hierarchy.

- **Proactive alerting:** Add a Quarkus `@Scheduled` job that calls `listFailedLRAs()`

periodically and raises an alert when the count exceeds a threshold.


Thursday, October 2, 2025

Narayana LRA participation in Hacktoberfest 2025

 

Hi,

Hacktoberfest 2025 has just started and the Narayana LRA project is participating. Any contribution to the project is welcome!

Here are the GitHub repository, Contributing Guide, and Hacktoberfest Issues link. Please notice that now Narayana LRA project uses GitHub issues to track bugs, tasks and features! 

Reach out to us on our zulip hacktoberfest dedicated channel for any questions or help. 

Please register to the Hacktoberfest website in order to track all your contributions and discover the rewards for 2025.

For more information and to discover other interesting projects participating to the event I suggest you have a look at the nice WildFly blog post.

Good month of collaborations! 

Marco 

Wednesday, June 18, 2025

Narayana is now in Commonhaus Foundation!

Hi,

Further to the earlier post about Narayana in a foundation (https://jbossts.blogspot.com/2025/03/request-for-input-narayana-in-foundation.html) I am pleased to share here that the Narayana project has successfully transferred to Commonhaus Foundation (as part of a move with WildFly - please see the WildFly announcement here https://www.wildfly.org/news/2025/04/30/WildFly-Commonhaus/).

You can find us now listed at: https://www.commonhaus.org/.

Please feel free to reach out on Zulip to chat about this: https://narayana.zulipchat.com/ 

Congratulations to the project on this new home!

Tom 

Monday, March 31, 2025

Request for input: Narayana in a foundation?

The Narayana project has been very successful for many, many years and we are very grateful for the contributions that our upstream community give to us through discussing, reporting issues, and providing code contributions to help the project thrive.

I would like to share an update with our community that we are considering to move the Narayana project to a vendor-neutral software foundation. By doing this we can hopefully further expand our community and continue to improve openness and transparency in the project.

Important Considerations

Moving to a foundation is not a trivial task, so it’s critical that the choice we make is a net benefit to our community. To help ensure this, there are a number of key factors we’re looking at when evaluating what foundation would be the best fit:

  1. Flexibility to continue shipping third-party components using a wide array of Open Source Initiative (OSI)-approved Open Source licences.
  2. Maintain as much as possible our current release processes.
  3. Retain independence in decision making, particularly on technical matters.
  4. Ensure Narayana remains visible and recognizable within a foundation’s potentially larger portfolio of projects.
  5. Ensure Narayana can make decisions based on technical merits, not foundation-imposed options.
  6. Provide flexibility in using Open Source Initiative (OSI)-approved Open Source licences for Narayana.

Support and Alignment with Red Hat Values

Red Hat business leaders are fully behind this move. Red Hat is dedicated to participating in and supporting vendor-neutral collaboration projects, such as the Linux kernel, Kubernetes, and OpenJDK. We seek the same for Narayana.

Please let us know what you think

We invite members of our Narayana community, inside and outside of Red Hat to join the discussion in the community either on the blog, in our users forum (https://groups.google.com/g/narayana-users) or over on Zulip (https://narayana.zulipchat.com/).


Monday, March 17, 2025

Narayana and its relationship to Red Hat middleware strategy



Hi everyone,


You might have already seen that Red Hat announced significant changes to its middleware strategy last month (if not, please do check out the relevant “Red Hat Blog” article: Evolving our middleware strategy [1]) and so I want to speak a little to the change and its relevance to our Narayana project.


As you may know, Narayana is a part of a number of Red Hat products, in particular Red Hat’s JBoss Enterprise Application Platform product and so this makes the strategic decision relevant to the Narayana project. That said, a key point in that article from the “Red Hat Blog” with regards to our Narayana project is that all transitioning Red Hat technology will remain open source and continue to follow an upstream-first development model. So as well as the technology still relying on being able to upstream-first (in projects like Narayana), it’s also that this upstream should remain open source (you can find what open source means at Red Hat over here [2]). Not only is the Narayana source code open source, but moreover its project operates in an open source manner, exhibiting the principles of open source and gratefully benefits from a healthy community of users and contributors. This will help us to keep innovating in the area of transactions as we move forwards.


I will also take this opportunity to add a “Thank you” for being part of our Narayana community - I am excited to see the results of what we achieve together next!



Tom Jenkinson


[1] https://www.redhat.com/en/blog/evolving-our-middleware-strategy

[2] https://www.redhat.com/en/about/open-source

Monday, January 6, 2025

Managing the availability of LRA participants

This post is a continuation of a series of jbosts blogs that discuss the MicroProfile LRA specification.

Services manage their workloads by providing endpoints to an LRA coordinator which in turn uses those endpoints to drive the LRA protocol forward thereby enabling the construction of reliable services. These endpoints may need to be modified over the long run so it ought to be possible to replace them with different ones in response to changes to the environment in which the service executes. Although the specification does not discuss how the endpoints can be replaced, the Narayana LRA REST API for the coordinator includes Microprofile OpenAPI documentation for replacing endpoints.

There are various administrative and management reasons for why the capability can be useful, such as controlling where termination handling is to take place, or to facilitate service replacement, etc. It may also be desirable for work completion, compensation, status reporting and clean up activities to be handled on different endpoints and at different times and this goal is facilitated via annotations including @Compensate, @Complete, @Status, @Forget and @AfterLRA.

When a participant does work in the context of a long running action, a “recovery URL” is created which services may use to associate their work with various management actions such as changing the participant endpoints as the action proceeds, after all a long running action can be of arbitrary duration and the needs of a service may change as the action evolves. The example I created for this post halts the JVM during “complete”, asks the user to send a curl request to the LRA coordinator to provide it with a new participant completion endpoint, restarts the participant on the new endpoint and waits for recovery to resend the completion callback to the new endpoint.

By leveraging the feature admins may proactively react to changing conditions (connectivity, throughput, functionality updates, etc) and be able to tune and or reconfigure the environment accordingly, perhaps bringing up a more reliability aware service that more intelligently operates within the more limited environment.

Build and start a coordinator on port 8080

Use the quarkus-maven-plugin to create a project for the coordinator, adding a dependency on maven artifact org.jboss.narayana.lra:lra-coordinator-jar:0.0.10.Final to the resulting pom. Also specify that the build should produce an uber jar so that the coordinator can run standalone:

    mvn io.quarkus:quarkus-maven-plugin:3.3.1:create -DprojectGroupId=org.acme -DprojectArtifactId=narayana-lra-coordinator -Dextensions="rest-jackson,rest-client"
    cd narayana-lra-coordinator
    rm -rf src/test src/main/java # the sources created by the example aren't required
    echo "quarkus.package.jar.type=uber-jar" > src/main/resources/application.properties
    # don't forget to add a dependency on maven artifact: org.jboss.narayana.lra:lra-coordinator-jar:0.0.10.Final
    ./mvnw clean package

and then start it on port 8080 by running the resulting jar

   java -jar target/narayana-lra-coordinator-1.0.0-SNAPSHOT-runner.jar &

Build and start a participant on port 8081 and run an LRA but halt the JVM before closing it

The service will be quite basic:

@Path("/halt")
public class MigratableResource {
    private static final AtomicBoolean halt = new AtomicBoolean(false);

    @LRA(value = LRA.Type.REQUIRED)
    @PUT
    public void doInTransaction() {
        halt.set(true); // halt when compensate or complete are called
        // when the business method finishes the LRA is closed and the complete endpoint will be called
    }

    @PUT
    @Path("/compensate")
    @Compensate
    public Response compensate() {
        return Response.ok().build();
    }

    @PUT
    @Path("/complete")
    @Complete
    public Response complete(@HeaderParam(LRA_HTTP_RECOVERY_HEADER) String recoveryUrl) {
        if (halt.get()) {
            int port = 8082;
            String completionUrl = String.format("http://localhost:%d/halt/complete", port);

            System.out.printf("Ask the coordinator to send the completion notification on a new endpoint using:%n");
            System.out.printf("curl -X PUT %s -d '<%s>; rel=complete'%n", recoveryUrl, completionUrl);
            Runtime.getRuntime().halt(1);
        }
        System.out.printf("completed%n");
        return Response.ok().build();
    }
}

The interesting part happens during completion where the JVM is halted. Notice that the curl command for migrating the completion endpoint is printed prior to halting.

Now build and run the participant on port 8081 - the maven project is available from the narayana artifacts maven repository.

cd <participant directory>
mvn clean package
java -Dquarkus.http.port=8081 -jar target/quarkus-app/quarkus-run.jar &

and then call the service method using the curl utility, or otherwise:

curl -X PUT -I http://localhost:8081/halt

The service method is annotated with just @LRA(value = LRA.Type.REQUIRED) so when it finishes the completion callback will be invoked by the coordinator. Make a note of the curl request printed by the completion callback just before it halts the JVM. An example is (the Uids will change on each run):

curl -X PUT http://localhost:8080/lra-coordinator/recovery/0_ffffc0a801c7_9d57_677ad0a4_2/0_ffffc0a801c7_9d57_677ad0a4_5 \
  -d '<http://localhost:8082/halt/complete>; rel=complete'

Notice that the payload of the HTTP PUT request includes the specification of the new completion callback, namely <http://localhost:8082/halt/complete>; rel=complete.

The new endpoint will be used on the next recovery pass which is every two minutes by default.

Finally restart the service on the new endpoint (port 8082):

java -Dquarkus.http.port=8082 -jar target/quarkus-app/quarkus-run.jar &

When the coordinator next runs a recovery scan it should use the new endpoint and the service will report that it has completed its' service work by printing the text “completed” when the completion endpoint is by the coordinator.

Monday, September 30, 2024

Coping with Failures during Long Running Actions

In this brief note I want to draw attention to some of the features in the LRA protocol that can help service writers manage failures. LRA is a transaction protocol that provides certain desirable properties for building reliable systems such as Atomicity, (eventual) Consistency and Durability. Providing this level of assurance is non trivial but the protocol provides a simple model that can help participants to easily play their part in enabling such systems.

LRA is not just for orchestrating services, it is as equally as important for managing failures. Apart from the specification I have not seen many posts, articles etc covering this important topic, and it is this deficit that I’d like to address in some posts. I had wanted to kick off with an article and demonstration of participant failover but I hit an issue while writing the demo which we need to release the fix for before I can showcase that. So instead, in this post I’ll just bring to the readers attention one or two, but by no means all, of the main features that service writers can use to help them to create more reliable microservices, a preview if you like, before going into more depth in a subsequent post.

Some remarkable items to consider include:

  1. Failing participants must be restarted. There is an option to change the callbacks on restart, any of the endpoints can be changed, even passing over responsibility for, say, the compensation to some other microservice. Likewise, failing coordinators must be restarted if progress of LRAs is to be made.
  2. There is an @Status annotation on participants that the coordinator can use to monitor participant progress and to enable participants to fully participate in the recovery protocol, in particular there is support for non-idempotent compensate endpoints; if there is an @Status endpoint and the compensate endpoint has previously returned a 202 Accepted HTTP status code, then it will periodically poll the status endpoint until the participant reports that it has reached an end state. The @Forget annotation is used by the coordinator to inform the participant that it is free to clean up.
  3. There are state transitions which participants use to notify the coordinator of failures (FailedToCompensate and FailedToComplete) and of transitory states (Compensating and Completing).
  4. Managing timeouts, although the actions supported by the protocol are long running careful choice of time limits for actions can bound failure windows and reduce the need for complicated recover procedures.
  5. And of course there is support for nested Long Running Actions which is a jewel in the toolkit for building reliable distributed systems.

That’s all for now - I’ve deliberately kept the ideas brief and high level so that they can be explored in greater depth later.