Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions timeless-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@
<artifactId>quarkus-langchain4j-openai</artifactId>
<version>${quarkus-langchain4j-openai.version}</version>
</dependency>
<dependency>
<groupId>io.quarkiverse.amazonservices</groupId>
<artifactId>quarkus-messaging-amazon-sqs</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.amazonservices</groupId>
<artifactId>quarkus-amazon-sqs</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package dev.matheuscruz.infra.outbox;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
import java.util.UUID;

@Entity
@Table(name = "outbox_messages")
public class OutboxMessage {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

@Column(nullable = false, columnDefinition = "TEXT")
private String payload;

@Column(name = "queue_url", nullable = false)
private String queueUrl;

@Column(name = "message_group_id", nullable = false)
private String messageGroupId;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private OutboxStatus status;

@Column(name = "created_at", nullable = false)
private Instant createdAt;

@Column(name = "processed_at")
private Instant processedAt;

@Column(name = "retry_count", nullable = false)
private int retryCount;

protected OutboxMessage() {
}

public OutboxMessage(String payload, String queueUrl, String messageGroupId) {
this.payload = payload;
this.queueUrl = queueUrl;
this.messageGroupId = messageGroupId;
this.status = OutboxStatus.PENDING;
this.createdAt = Instant.now();
this.retryCount = 0;
}

public UUID getId() {
return id;
}

public String getPayload() {
return payload;
}

public String getQueueUrl() {
return queueUrl;
}

public String getMessageGroupId() {
return messageGroupId;
}

public OutboxStatus getStatus() {
return status;
}

public Instant getCreatedAt() {
return createdAt;
}

public Instant getProcessedAt() {
return processedAt;
}

public int getRetryCount() {
return retryCount;
}

public void markAsSent() {
this.status = OutboxStatus.SENT;
this.processedAt = Instant.now();
}

public void markAsFailed() {
this.status = OutboxStatus.FAILED;
this.processedAt = Instant.now();
}

public void incrementRetryCount() {
this.retryCount++;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package dev.matheuscruz.infra.outbox;

import io.quarkus.narayana.jta.QuarkusTransaction;
import io.quarkus.scheduler.Scheduled;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import org.jboss.logging.Logger;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.SendMessageRequest;

@ApplicationScoped
public class OutboxMessageRelay {

private static final int MAX_RETRIES = 10;

private final OutboxMessageRepository outboxMessageRepository;
private final SqsClient sqsClient;
private final Logger logger = Logger.getLogger(OutboxMessageRelay.class);

public OutboxMessageRelay(OutboxMessageRepository outboxMessageRepository, SqsClient sqsClient) {
this.outboxMessageRepository = outboxMessageRepository;
this.sqsClient = sqsClient;
}

@Scheduled(every = "5s")
public void processOutbox() {
List<OutboxMessage> pending = outboxMessageRepository.findPendingMessages();

for (OutboxMessage message : pending) {
if (message.getRetryCount() >= MAX_RETRIES) {
failMessage(message);
continue;
}

try {
sqsClient.sendMessage(SendMessageRequest.builder().queueUrl(message.getQueueUrl())
.messageBody(message.getPayload()).messageGroupId(message.getMessageGroupId()).build());

updateMessageStatus(message, true);
} catch (Exception e) {
logger.errorf(e, "Failed to send outbox message %s to queue %s", message.getId(),
message.getQueueUrl());
updateMessageStatus(message, false);
}
}
}

private void updateMessageStatus(OutboxMessage message, boolean success) {
QuarkusTransaction.requiringNew().run(() -> {
OutboxMessage managed = outboxMessageRepository.findById(message.getId());
if (managed != null) {
if (success) {
managed.markAsSent();
} else {
managed.incrementRetryCount();
}
}
});
}

private void failMessage(OutboxMessage message) {
QuarkusTransaction.requiringNew().run(() -> {
OutboxMessage managed = outboxMessageRepository.findById(message.getId());
if (managed != null) {
managed.markAsFailed();
logger.errorf("Outbox message %s has exceeded max retries, marking as FAILED", message.getId());
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.matheuscruz.infra.outbox;

import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.UUID;

@ApplicationScoped
public class OutboxMessageRepository implements PanacheRepositoryBase<OutboxMessage, UUID> {

private static final int MAX_RETRIES = 10;
private static final int BATCH_SIZE = 20;

public List<OutboxMessage> findPendingMessages() {
return find("status = ?1 and retryCount < ?2 order by createdAt", OutboxStatus.PENDING, MAX_RETRIES)
.range(0, BATCH_SIZE - 1).list();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dev.matheuscruz.infra.outbox;

public enum OutboxStatus {
PENDING, SENT, FAILED
}
Loading
Loading