This is an automated email from the ASF dual-hosted git repository.

pefernan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-kogito-apps.git


The following commit(s) were added to refs/heads/main by this push:
     new a4b8aa613 [incubator-kie-issues#2311] Handle reconciliation of RETRY 
or ERROR jobs in a new transaction (#2333)
a4b8aa613 is described below

commit a4b8aa613b6fb9698b022e08d0f0f9537346e512
Author: Martin Weiler <[email protected]>
AuthorDate: Wed Jul 1 01:21:20 2026 -0600

    [incubator-kie-issues#2311] Handle reconciliation of RETRY or ERROR jobs in 
a new transaction (#2333)
    
    * [incubator-kie-issues#2311] Handle reconciliation of RETRY or ERROR jobs 
in a new transaction
    
    * Mark current transaction for rollback
    
    * Replace reflection code with platform specific implementations
    
    * Add test cases to verify transactional behavior with rollbacks
    
    * Ensure jobs are executed in parallel threads (required for Spring Boot)
    
    * Fix test failures
    
    * Review feedback
    
    * Review feedback, simplify logic around rescheduling
    
    * Fix usage of deprecated method
    
    * Simplification
---
 .../kogito/app/jobs/api/JobSchedulerBuilder.java   |   3 +
 .../kogito/app/jobs/impl/VertxJobScheduler.java    |  90 ++++++++++--
 .../ErrorHandlingJobTimeoutInterceptor.java        |   4 +-
 .../jobs/spi/NoOpTransactionRollbackMarker.java    |  38 ++---
 .../app/jobs/spi/TransactionRollbackMarker.java    |  40 ++++++
 .../app/jobs/quarkus/QuarkusJobsService.java       |   5 +
 .../quarkus/QuarkusTransactionRollbackMarker.java  |  67 +++++++++
 .../app/jobs/jpa/quarkus/MockDataRepository.java   | 115 +++++++++++++++
 .../quarkus/QuarkusTransactionRollbackTest.java    | 157 +++++++++++++++++++++
 .../app/jobs/jpa/quarkus/TestJobExecutor.java      |  10 ++
 .../SpringBootTransactionRollbackMarker.java       |  61 ++++++++
 .../app/jobs/springboot/SpringbootJobsService.java |   9 +-
 .../TransactionJobTimeoutInterceptor.java          |  17 +++
 .../app/jobs/springboot/MockDataRepository.java    |  98 +++++++++++++
 .../SpringBootTransactionRollbackTest.java         | 122 ++++++++++++++++
 .../app/jobs/springboot/TestJobExecutor.java       |  10 ++
 16 files changed, 808 insertions(+), 38 deletions(-)

diff --git 
a/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/api/JobSchedulerBuilder.java
 
b/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/api/JobSchedulerBuilder.java
index 6380da172..8376af625 100644
--- 
a/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/api/JobSchedulerBuilder.java
+++ 
b/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/api/JobSchedulerBuilder.java
@@ -22,6 +22,7 @@ import org.kie.kogito.app.jobs.impl.VertxJobScheduler;
 import org.kie.kogito.app.jobs.integrations.JobExceptionDetailsExtractor;
 import org.kie.kogito.app.jobs.spi.JobContextFactory;
 import org.kie.kogito.app.jobs.spi.JobStore;
+import org.kie.kogito.app.jobs.spi.TransactionRollbackMarker;
 import org.kie.kogito.event.EventPublisher;
 
 public interface JobSchedulerBuilder {
@@ -61,4 +62,6 @@ public interface JobSchedulerBuilder {
     JobSchedulerBuilder withJobDescriptorMergers(JobDescriptionMerger... 
jobDescriptionMergers);
 
     JobSchedulerBuilder 
withExceptionDetailsExtractor(JobExceptionDetailsExtractor 
exceptionDetailsExtractor);
+
+    JobSchedulerBuilder 
withTransactionRollbackMarker(TransactionRollbackMarker 
transactionRollbackMarker);
 }
diff --git 
a/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/impl/VertxJobScheduler.java
 
b/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/impl/VertxJobScheduler.java
index ebc38074a..d9ecceaff 100644
--- 
a/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/impl/VertxJobScheduler.java
+++ 
b/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/impl/VertxJobScheduler.java
@@ -47,6 +47,8 @@ import 
org.kie.kogito.app.jobs.integrations.UserTaskInstanceJobDescriptorMerger;
 import org.kie.kogito.app.jobs.spi.JobContext;
 import org.kie.kogito.app.jobs.spi.JobContextFactory;
 import org.kie.kogito.app.jobs.spi.JobStore;
+import org.kie.kogito.app.jobs.spi.NoOpTransactionRollbackMarker;
+import org.kie.kogito.app.jobs.spi.TransactionRollbackMarker;
 import org.kie.kogito.app.jobs.spi.memory.MemoryJobContextFactory;
 import org.kie.kogito.app.jobs.spi.memory.MemoryJobStore;
 import org.kie.kogito.event.DataEvent;
@@ -115,6 +117,8 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
 
     private JobExceptionDetailsExtractor exceptionDetailsExtractor;
 
+    private TransactionRollbackMarker transactionRollbackMarker;
+
     public class VertxJobSchedulerBuilder implements JobSchedulerBuilder {
 
         @Override
@@ -213,6 +217,12 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
             return this;
         }
 
+        @Override
+        public JobSchedulerBuilder 
withTransactionRollbackMarker(TransactionRollbackMarker 
transactionRollbackMarker) {
+            VertxJobScheduler.this.transactionRollbackMarker = 
transactionRollbackMarker;
+            return this;
+        }
+
     }
 
     public VertxJobScheduler() {
@@ -242,6 +252,7 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
         this.refreshJobsInterval = 1000L;
         this.retryInterval = 10 * 1000L; // ten seconds
         this.maxRefreshJobsIntervalWindow = 5 * 60 * 1000L; // every 5 minute
+        this.transactionRollbackMarker = new NoOpTransactionRollbackMarker();
     }
 
     @Override
@@ -390,7 +401,7 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
 
     private void timeout(Long timerId, String jobId) {
         LOG.debug("Executing timeout with timer Id {} and jobId {}", timerId, 
jobId);
-        workerExecutor.executeBlocking(newTimeoutTask(timerId, jobId));
+        workerExecutor.executeBlocking(newTimeoutTask(timerId, jobId), false);
     }
 
     private Callable<JobTimeoutExecution> newTimeoutTask(Long timerId, String 
jobId) {
@@ -421,6 +432,10 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
                     return new JobTimeoutExecution(nextJobDetails);
                 } catch (Exception exception) {
                     LOG.trace("Timeout {} with jobId {} will be retried if 
possible", timerId, jobId, exception);
+
+                    // Mark the current transaction for rollback
+                    markTransactionForRollbackWhenEnabled();
+
                     JobDetails nextJobDetails = doRetryOrError(jobDetails, 
exception);
                     reconcileScheduling(timerId, jobContext, nextJobDetails);
                     jobSchedulerListeners.forEach(l -> 
l.onFailure(jobDetails));
@@ -435,6 +450,55 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
         return current;
     }
 
+    /**
+     * Handles retry scheduling for RETRY and ERROR job statuses.
+     * If there's an active transaction that was marked for rollback, 
schedules the retry
+     * in a new transaction. Otherwise, persists the job immediately.
+     */
+    private void scheduleRetry(JobContext jobContext, JobDetails jobDetails) {
+        String jobId = jobDetails.getId();
+        JobStatus status = jobDetails.getStatus();
+
+        // Check if we need to execute in a new transaction
+        if (transactionRollbackMarker != null && 
transactionRollbackMarker.isTransactionActive()) {
+            // Current transaction will be rolled back, so schedule retry in a 
new transaction
+            LOG.trace("Job {} with status {} will be scheduled in new 
transaction", jobId, status);
+
+            JobContext newJobContext = jobContextFactory.newContext();
+
+            Callable<JobTimeoutExecution> newTransactionTask = () -> 
persistAndSchedule(newJobContext, jobDetails, status);
+
+            // Apply transaction interceptors to ensure this runs in a new 
transaction
+            for (JobTimeoutInterceptor interceptor : interceptors) {
+                newTransactionTask = 
interceptor.chainIntercept(newTransactionTask);
+            }
+
+            vertx.executeBlocking(newTransactionTask, false)
+                    .onFailure(e -> LOG.error("Async retry scheduling failed 
for job {}", jobId, e));
+        } else {
+            // No active transaction or transaction not marked for rollback
+            LOG.trace("Job {} with status {} will be scheduled", jobId, 
status);
+
+            try {
+                persistAndSchedule(jobContext, jobDetails, status);
+            } catch (Exception e) {
+                LOG.error("Failed to schedule retry for job {}", jobId, e);
+            }
+        }
+    }
+
+    private JobTimeoutExecution persistAndSchedule(JobContext jobContext, 
JobDetails jobDetails, JobStatus status) throws Exception {
+        jobStore.update(jobContext, jobDetails);
+        fireEvents(jobDetails);
+        // Schedule timer after persistence
+        if (status == JobStatus.RETRY) {
+            addOrUpdateTxTimer(jobDetails);
+        } else {
+            removeTxTimer(jobDetails);
+        }
+        return new JobTimeoutExecution(jobDetails);
+    }
+
     private void reconcileScheduling(Long timerId, JobContext jobContext, 
JobDetails nextJobDetails) {
         String jobId = nextJobDetails.getId();
         switch (nextJobDetails.getStatus()) {
@@ -445,15 +509,13 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
                 jobStore.remove(jobContext, jobId);
                 break;
             case SCHEDULED:
-            case RETRY:
                 LOG.trace("Timeout {} with jobId {} will be updated and 
scheduled", timerId, jobId);
                 addOrUpdateTxTimer(nextJobDetails);
                 jobStore.update(jobContext, nextJobDetails);
                 break;
+            case RETRY:
             case ERROR:
-                LOG.trace("Timeout {} with jobId {} will be set to error", 
timerId, jobId);
-                removeTxTimer(nextJobDetails);
-                jobStore.update(jobContext, nextJobDetails);
+                scheduleRetry(jobContext, nextJobDetails);
                 break;
             default:
                 LOG.trace("Timeout {} with jobId {} is RUNNING and should not 
happen", timerId, jobId);
@@ -553,7 +615,7 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
 
     private JobDetails doRetryOrError(JobDetails jobDetails, Exception 
exception) {
         Integer retryCounter = jobDetails.getRetries();
-        // First, create the next JobDetails (RETRY or ERROR) without firing 
events
+        // Create the next JobDetails (RETRY or ERROR)
         JobDetails nextJobDetails;
         if (retryCounter < this.maxNumberOfRetries) {
             LOG.trace("doRetryOrError: Job {} will be retried (retry {} of 
{})", jobDetails.getId(), retryCounter + 1, maxNumberOfRetries);
@@ -562,11 +624,10 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
             LOG.trace("doRetryOrError: Job {} exceeded max retries ({}) and 
will transition to ERROR", jobDetails.getId(), maxNumberOfRetries);
             nextJobDetails = createErrorJobDetails(jobDetails);
         }
-        // Then extract and set exception details BEFORE firing events
+        // Extract and set exception details
         extractAndSetExceptionDetails(nextJobDetails, exception);
         LOG.trace("doRetryOrError: Created {} with exception details: {}", 
nextJobDetails.getStatus(), nextJobDetails);
-        // Finally fire events with exception details already set
-        fireEvents(nextJobDetails);
+        // Events will be fired in scheduleRetry() after persistence
         return nextJobDetails;
     }
 
@@ -649,4 +710,15 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
         }
     }
 
+    /**
+     * Marks the current transaction for rollback using the injected 
TransactionRollbackMarker.
+     */
+    private void markTransactionForRollbackWhenEnabled() {
+        if (transactionRollbackMarker != null) {
+            transactionRollbackMarker.markForRollback();
+        } else {
+            LOG.warn("No TransactionRollbackMarker configured. Transaction 
rollback marking is not supported.");
+        }
+    }
+
 }
diff --git 
a/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/integrations/ErrorHandlingJobTimeoutInterceptor.java
 
b/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/integrations/ErrorHandlingJobTimeoutInterceptor.java
index 8e1308bab..897c70e04 100644
--- 
a/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/integrations/ErrorHandlingJobTimeoutInterceptor.java
+++ 
b/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/integrations/ErrorHandlingJobTimeoutInterceptor.java
@@ -41,7 +41,9 @@ public class ErrorHandlingJobTimeoutInterceptor implements 
JobTimeoutInterceptor
 
     @Override
     public Integer priority() {
-        return 50;
+        // Priority 5 ensures this runs OUTSIDE the transaction interceptor 
(priority 10)
+        // This allows error handling to occur after transaction rollback
+        return 5;
     }
 
     @Override
diff --git 
a/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/jpa/quarkus/TestJobExecutor.java
 
b/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/spi/NoOpTransactionRollbackMarker.java
similarity index 52%
copy from 
kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/jpa/quarkus/TestJobExecutor.java
copy to 
jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/spi/NoOpTransactionRollbackMarker.java
index 8cec79896..297dc7d51 100644
--- 
a/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/jpa/quarkus/TestJobExecutor.java
+++ 
b/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/spi/NoOpTransactionRollbackMarker.java
@@ -16,41 +16,27 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.kie.kogito.app.jobs.jpa.quarkus;
+package org.kie.kogito.app.jobs.spi;
 
-import org.kie.kogito.app.jobs.api.JobExecutor;
-import org.kie.kogito.jobs.service.model.JobDetails;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import jakarta.inject.Singleton;
-
-@Singleton
-public class TestJobExecutor implements JobExecutor {
+/**
+ * No-op implementation of TransactionRollbackMarker that does nothing.
+ */
+public class NoOpTransactionRollbackMarker implements 
TransactionRollbackMarker {
 
-    private Logger LOG = LoggerFactory.getLogger(TestJobExecutor.class);
-    private int numberOfFailures;
+    private static final Logger LOG = 
LoggerFactory.getLogger(NoOpTransactionRollbackMarker.class);
 
     @Override
-    public boolean accept(JobDetails jobDescription) {
-        return true;
+    public void markForRollback() {
+        LOG.warn("No platform-specific TransactionRollbackMarker 
implementation found. " +
+                "Transaction rollback marking is not supported. " +
+                "Please ensure a Quarkus or Spring Boot specific 
implementation is available.");
     }
 
     @Override
-    public void execute(JobDetails jobDescription) {
-        LOG.info("executing {}", jobDescription);
-        if (numberOfFailures > 0) {
-            --numberOfFailures;
-            throw new RuntimeException();
-        }
-    }
-
-    public void setNumberOfFailures(int numberOfFailures) {
-        this.numberOfFailures = numberOfFailures;
+    public boolean isTransactionActive() {
+        return false;
     }
-
-    public void reset() {
-        numberOfFailures = 0;
-    }
-
 }
diff --git 
a/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/spi/TransactionRollbackMarker.java
 
b/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/spi/TransactionRollbackMarker.java
new file mode 100644
index 000000000..5a10a5164
--- /dev/null
+++ 
b/jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/spi/TransactionRollbackMarker.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.kogito.app.jobs.spi;
+
+/**
+ * Interface for marking the current transaction for rollback.
+ * This is used when a job execution fails and needs to be retried, ensuring 
that
+ * any changes made during the failed execution are rolled back before the 
retry attempt.
+ */
+public interface TransactionRollbackMarker {
+
+    /**
+     * Marks the current transaction for rollback.
+     * If no transaction is active, this method should do nothing.
+     */
+    void markForRollback();
+
+    /**
+     * Checks if a transaction is currently active.
+     * 
+     * @return true if a transaction is active, false otherwise
+     */
+    boolean isTransactionActive();
+}
diff --git 
a/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/quarkus/QuarkusJobsService.java
 
b/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/quarkus/QuarkusJobsService.java
index 77be00aaa..801380061 100644
--- 
a/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/quarkus/QuarkusJobsService.java
+++ 
b/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/quarkus/QuarkusJobsService.java
@@ -31,6 +31,7 @@ import 
org.kie.kogito.app.jobs.integrations.UserTaskInstanceJobDescriptionJobIns
 import org.kie.kogito.app.jobs.quarkus.resource.RestApiConstants;
 import org.kie.kogito.app.jobs.spi.JobContextFactory;
 import org.kie.kogito.app.jobs.spi.JobStore;
+import org.kie.kogito.app.jobs.spi.TransactionRollbackMarker;
 import org.kie.kogito.event.EventPublisher;
 import org.kie.kogito.handler.ExceptionHandler;
 import org.kie.kogito.jobs.JobDescription;
@@ -94,6 +95,9 @@ public class QuarkusJobsService implements JobsService {
     @Inject
     WrappingConditionalJobExceptionDetailsExtractor exceptionDetailsExtractor;
 
+    @Inject
+    TransactionRollbackMarker transactionRollbackMarker;
+
     @PostConstruct
     public void init() {
         this.jobScheduler = JobSchedulerBuilder.newJobSchedulerBuilder()
@@ -114,6 +118,7 @@ public class QuarkusJobsService implements JobsService {
                         new TransactionJobTimeoutInterceptor(),
                         new 
ErrorHandlingJobTimeoutInterceptor(exceptionHandlers.stream().toList()))
                 .withExceptionDetailsExtractor(exceptionDetailsExtractor)
+                .withTransactionRollbackMarker(transactionRollbackMarker)
                 .withNumberOfWorkerThreads(numberOfWorkerThreads)
                 .withJobSynchronization(new JobSynchronization() {
 
diff --git 
a/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/quarkus/QuarkusTransactionRollbackMarker.java
 
b/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/quarkus/QuarkusTransactionRollbackMarker.java
new file mode 100644
index 000000000..ab03bdeee
--- /dev/null
+++ 
b/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/quarkus/QuarkusTransactionRollbackMarker.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.kogito.app.jobs.quarkus;
+
+import org.kie.kogito.app.jobs.spi.TransactionRollbackMarker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.transaction.Status;
+import jakarta.transaction.SystemException;
+import jakarta.transaction.TransactionManager;
+
+@ApplicationScoped
+public class QuarkusTransactionRollbackMarker implements 
TransactionRollbackMarker {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(QuarkusTransactionRollbackMarker.class);
+
+    private final TransactionManager transactionManager;
+
+    public QuarkusTransactionRollbackMarker(TransactionManager 
transactionManager) {
+        this.transactionManager = transactionManager;
+    }
+
+    @Override
+    public void markForRollback() {
+        try {
+            if (transactionManager != null && isTransactionActive()) {
+                transactionManager.setRollbackOnly();
+                LOG.debug("Marked transaction for rollback due to exception in 
job execution (Quarkus/Narayana)");
+            } else {
+                LOG.trace("No active transaction to mark for rollback");
+            }
+        } catch (SystemException e) {
+            LOG.warn("Failed to mark transaction for rollback", e);
+        }
+    }
+
+    @Override
+    public boolean isTransactionActive() {
+        try {
+            if (transactionManager != null) {
+                int status = transactionManager.getStatus();
+                return status == Status.STATUS_ACTIVE || status == 
Status.STATUS_MARKED_ROLLBACK;
+            }
+        } catch (SystemException e) {
+            LOG.trace("Failed to check transaction status", e);
+        }
+        return false;
+    }
+}
diff --git 
a/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/jpa/quarkus/MockDataRepository.java
 
b/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/jpa/quarkus/MockDataRepository.java
new file mode 100644
index 000000000..db04c5d11
--- /dev/null
+++ 
b/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/jpa/quarkus/MockDataRepository.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.kogito.app.jobs.jpa.quarkus;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.transaction.Status;
+import jakarta.transaction.Synchronization;
+import jakarta.transaction.TransactionManager;
+
+/**
+ * Mock repository that tracks data persistence attempts and simulates
+ * transactional behavior to verify that rollbacks prevent duplicate data.
+ */
+@ApplicationScoped
+public class MockDataRepository {
+
+    private final ConcurrentHashMap<String, AtomicInteger> committedData = new 
ConcurrentHashMap<>();
+    private final AtomicInteger totalPersistenceAttempts = new 
AtomicInteger(0);
+    private final AtomicInteger successfulCommits = new AtomicInteger(0);
+    private final AtomicInteger rolledBackAttempts = new AtomicInteger(0);
+
+    private TransactionManager transactionManager;
+
+    public void setTransactionManager(TransactionManager transactionManager) {
+        this.transactionManager = transactionManager;
+    }
+
+    /**
+     * Simulates persisting data within a transaction.
+     * If the transaction is rolled back, the data should not be committed.
+     */
+    public void persistData(String key, String value) {
+        totalPersistenceAttempts.incrementAndGet();
+
+        // Register a transaction synchronization to track commit/rollback
+        if (transactionManager != null) {
+            try {
+                if (transactionManager.getStatus() == Status.STATUS_ACTIVE) {
+                    
transactionManager.getTransaction().registerSynchronization(new 
Synchronization() {
+                        @Override
+                        public void beforeCompletion() {
+                            // Nothing to do before completion
+                        }
+
+                        @Override
+                        public void afterCompletion(int status) {
+                            if (status == Status.STATUS_COMMITTED) {
+                                // Only commit the data if transaction succeeds
+                                committedData.computeIfAbsent(key, k -> new 
AtomicInteger(0)).incrementAndGet();
+                                successfulCommits.incrementAndGet();
+                            } else if (status == Status.STATUS_ROLLEDBACK) {
+                                rolledBackAttempts.incrementAndGet();
+                            }
+                        }
+                    });
+                } else {
+                    // No active transaction - commit immediately (shouldn't 
happen in our tests)
+                    committedData.computeIfAbsent(key, k -> new 
AtomicInteger(0)).incrementAndGet();
+                    successfulCommits.incrementAndGet();
+                }
+            } catch (Exception e) {
+                // If we can't register synchronization, commit immediately
+                committedData.computeIfAbsent(key, k -> new 
AtomicInteger(0)).incrementAndGet();
+                successfulCommits.incrementAndGet();
+            }
+        } else {
+            // No transaction manager - commit immediately
+            committedData.computeIfAbsent(key, k -> new 
AtomicInteger(0)).incrementAndGet();
+            successfulCommits.incrementAndGet();
+        }
+    }
+
+    public int getCommittedCount(String key) {
+        AtomicInteger count = committedData.get(key);
+        return count != null ? count.get() : 0;
+    }
+
+    public int getTotalPersistenceAttempts() {
+        return totalPersistenceAttempts.get();
+    }
+
+    public int getSuccessfulCommits() {
+        return successfulCommits.get();
+    }
+
+    public int getRolledBackAttempts() {
+        return rolledBackAttempts.get();
+    }
+
+    public void reset() {
+        committedData.clear();
+        totalPersistenceAttempts.set(0);
+        successfulCommits.set(0);
+        rolledBackAttempts.set(0);
+    }
+}
diff --git 
a/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/jpa/quarkus/QuarkusTransactionRollbackTest.java
 
b/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/jpa/quarkus/QuarkusTransactionRollbackTest.java
new file mode 100644
index 000000000..26ad0f36f
--- /dev/null
+++ 
b/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/jpa/quarkus/QuarkusTransactionRollbackTest.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.kogito.app.jobs.jpa.quarkus;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.kie.kogito.app.jobs.quarkus.QuarkusJobsService;
+import org.kie.kogito.app.jobs.quarkus.QuarkusTransactionRollbackMarker;
+import org.kie.kogito.jobs.ExactExpirationTime;
+import org.kie.kogito.jobs.JobsService;
+import org.kie.kogito.jobs.descriptors.ProcessInstanceJobDescription;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+import jakarta.inject.Inject;
+import jakarta.transaction.Status;
+import jakarta.transaction.TransactionManager;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@QuarkusTest
+public class QuarkusTransactionRollbackTest {
+
+    @Inject
+    JobsService jobsService;
+
+    @Inject
+    TestJobSchedulerListener listener;
+
+    @Inject
+    TestJobExecutor testJobExecutor;
+
+    @Inject
+    TestExceptionHandler exceptionHandler;
+
+    @Inject
+    QuarkusTransactionRollbackMarker transactionRollbackMarker;
+
+    @Inject
+    TransactionManager transactionManager;
+
+    @Inject
+    MockDataRepository mockDataRepository;
+
+    @BeforeEach
+    public void init() {
+        ((QuarkusJobsService) jobsService).init();
+        testJobExecutor.reset();
+        exceptionHandler.reset();
+        mockDataRepository.reset();
+        mockDataRepository.setTransactionManager(transactionManager);
+    }
+
+    @AfterEach
+    public void cleanup() {
+        ((QuarkusJobsService) jobsService).destroy();
+    }
+
+    @Test
+    public void testTransactionRollbackMarkerInjected() {
+        assertThat(transactionRollbackMarker).isNotNull();
+        assertThat(transactionRollbackMarker.isTransactionActive()).isFalse();
+    }
+
+    @Test
+    public void testTransactionRollbackOnJobFailure() throws Exception {
+        // Configure the test executor to fail 4 times (initial + 3 retries)
+        testJobExecutor.setNumberOfFailures(4);
+
+        ProcessInstanceJobDescription jobDescription = new 
ProcessInstanceJobDescription(
+                "rollback-test-job",
+                "-1",
+                
ExactExpirationTime.of(Instant.now().plus(Duration.ofSeconds(2)).atZone(ZoneId.of("UTC"))),
+                5,
+                "test-process-instance",
+                null,
+                "test-process",
+                null,
+                "test-node-instance");
+
+        listener.setCount(4);
+        jobsService.scheduleJob(jobDescription);
+
+        // Wait for all retries to complete and error handler to be invoked
+        Awaitility.await()
+                .atMost(Duration.ofSeconds(10L))
+                .untilAsserted(() -> {
+                    assertThat(exceptionHandler.isError())
+                            .as("Error handler should be invoked after max 
retries")
+                            .isTrue();
+                });
+
+        assertThat(listener.await(1, java.util.concurrent.TimeUnit.SECONDS))
+                .as("All job executions should have been attempted")
+                .isTrue();
+
+        assertThat(mockDataRepository.getTotalPersistenceAttempts())
+                .as("Should have 4 persistence attempts (initial + 3 retries)")
+                .isEqualTo(4);
+
+        assertThat(mockDataRepository.getRolledBackAttempts())
+                .as("All failed attempts should have been rolled back")
+                .isEqualTo(4);
+
+        assertThat(mockDataRepository.getSuccessfulCommits())
+                .as("No data should be committed when all attempts fail")
+                .isEqualTo(0);
+
+        assertThat(mockDataRepository.getCommittedCount("rollback-test-job"))
+                .as("No duplicate data should be persisted due to rollback")
+                .isEqualTo(0);
+    }
+
+    @Test
+    public void testTransactionRollbackMarkerFunctionality() throws Exception {
+        assertThat(transactionRollbackMarker.isTransactionActive()).isFalse();
+
+        transactionManager.begin();
+        try {
+            
assertThat(transactionRollbackMarker.isTransactionActive()).isTrue();
+
+            transactionRollbackMarker.markForRollback();
+
+            // Verify the transaction is marked for rollback
+            assertThat(transactionManager.getStatus())
+                    .as("Transaction should be marked for rollback")
+                    .isEqualTo(Status.STATUS_MARKED_ROLLBACK);
+        } finally {
+            transactionManager.rollback();
+        }
+
+        assertThat(transactionRollbackMarker.isTransactionActive()).isFalse();
+    }
+
+}
diff --git 
a/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/jpa/quarkus/TestJobExecutor.java
 
b/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/jpa/quarkus/TestJobExecutor.java
index 8cec79896..e911fc590 100644
--- 
a/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/jpa/quarkus/TestJobExecutor.java
+++ 
b/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/jpa/quarkus/TestJobExecutor.java
@@ -23,6 +23,7 @@ import org.kie.kogito.jobs.service.model.JobDetails;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import jakarta.inject.Inject;
 import jakarta.inject.Singleton;
 
 @Singleton
@@ -31,6 +32,9 @@ public class TestJobExecutor implements JobExecutor {
     private Logger LOG = LoggerFactory.getLogger(TestJobExecutor.class);
     private int numberOfFailures;
 
+    @Inject
+    MockDataRepository mockDataRepository;
+
     @Override
     public boolean accept(JobDetails jobDescription) {
         return true;
@@ -39,6 +43,12 @@ public class TestJobExecutor implements JobExecutor {
     @Override
     public void execute(JobDetails jobDescription) {
         LOG.info("executing {}", jobDescription);
+
+        // Simulate data persistence (e.g., creating a user task)
+        if (mockDataRepository != null) {
+            mockDataRepository.persistData(jobDescription.getId(), 
"execution-data");
+        }
+
         if (numberOfFailures > 0) {
             --numberOfFailures;
             throw new RuntimeException();
diff --git 
a/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/SpringBootTransactionRollbackMarker.java
 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/SpringBootTransactionRollbackMarker.java
new file mode 100644
index 000000000..9c44b3f9b
--- /dev/null
+++ 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/SpringBootTransactionRollbackMarker.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.kogito.app.jobs.springboot;
+
+import org.kie.kogito.app.jobs.spi.TransactionRollbackMarker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.TransactionStatus;
+import 
org.springframework.transaction.support.TransactionSynchronizationManager;
+
+@Component
+public class SpringBootTransactionRollbackMarker implements 
TransactionRollbackMarker {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(SpringBootTransactionRollbackMarker.class);
+
+    // Thread-local to store the current TransactionStatus for 
TransactionTemplate usage
+    private static final ThreadLocal<TransactionStatus> 
CURRENT_TRANSACTION_STATUS = new ThreadLocal<>();
+
+    public static void setCurrentTransactionStatus(TransactionStatus status) {
+        CURRENT_TRANSACTION_STATUS.set(status);
+    }
+
+    public static void clearCurrentTransactionStatus() {
+        CURRENT_TRANSACTION_STATUS.remove();
+    }
+
+    @Override
+    public void markForRollback() {
+        if (isTransactionActive()) {
+            TransactionStatus status = CURRENT_TRANSACTION_STATUS.get();
+            if (status != null) {
+                status.setRollbackOnly();
+                LOG.debug("Transaction marked for rollback using 
TransactionStatus");
+            } else {
+                LOG.warn("No TransactionStatus available to mark for 
rollback");
+            }
+        }
+    }
+
+    @Override
+    public boolean isTransactionActive() {
+        return TransactionSynchronizationManager.isActualTransactionActive();
+    }
+}
diff --git 
a/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/SpringbootJobsService.java
 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/SpringbootJobsService.java
index 47c42cbea..b71251990 100644
--- 
a/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/SpringbootJobsService.java
+++ 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/SpringbootJobsService.java
@@ -32,6 +32,7 @@ import 
org.kie.kogito.app.jobs.integrations.ProcessJobDescriptionJobInstanceEven
 import 
org.kie.kogito.app.jobs.integrations.UserTaskInstanceJobDescriptionJobInstanceEventAdapter;
 import org.kie.kogito.app.jobs.spi.JobContextFactory;
 import org.kie.kogito.app.jobs.spi.JobStore;
+import org.kie.kogito.app.jobs.spi.TransactionRollbackMarker;
 import org.kie.kogito.app.jobs.springboot.resource.RestApiConstants;
 import org.kie.kogito.event.EventPublisher;
 import org.kie.kogito.handler.ExceptionHandler;
@@ -93,6 +94,9 @@ public class SpringbootJobsService implements JobsService {
     @Autowired
     protected WrappingConditionalJobExceptionDetailsExtractor 
exceptionDetailsExtractor;
 
+    @Autowired
+    protected TransactionRollbackMarker transactionRollbackMarker;
+
     @PostConstruct
     public void init() {
         this.jobScheduler = JobSchedulerBuilder.newJobSchedulerBuilder()
@@ -110,9 +114,10 @@ public class SpringbootJobsService implements JobsService {
                 .withMaxNumberOfRetries(maxNumberOfRetries)
                 .withRefreshJobsInterval(maxRefreshJobsIntervalWindow * 60 * 
1000L)
                 .withTimeoutInterceptor(
-                        new 
TransactionJobTimeoutInterceptor(transactionManager),
-                        new 
ErrorHandlingJobTimeoutInterceptor(ofNullable(exceptionHandlers).stream().toList()))
+                        new 
ErrorHandlingJobTimeoutInterceptor(ofNullable(exceptionHandlers).stream().toList()),
+                        new 
TransactionJobTimeoutInterceptor(transactionManager))
                 .withExceptionDetailsExtractor(exceptionDetailsExtractor)
+                .withTransactionRollbackMarker(transactionRollbackMarker)
                 .withNumberOfWorkerThreads(numberOfWorkerThreads)
                 .withJobSynchronization(new JobSynchronization() {
 
diff --git 
a/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/TransactionJobTimeoutInterceptor.java
 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/TransactionJobTimeoutInterceptor.java
index 58d4b562d..f2ef72f08 100644
--- 
a/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/TransactionJobTimeoutInterceptor.java
+++ 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/TransactionJobTimeoutInterceptor.java
@@ -23,6 +23,7 @@ import java.util.concurrent.Callable;
 import org.kie.kogito.app.jobs.api.JobTimeoutExecution;
 import org.kie.kogito.app.jobs.api.JobTimeoutInterceptor;
 import org.springframework.transaction.PlatformTransactionManager;
+import 
org.springframework.transaction.support.TransactionSynchronizationManager;
 import org.springframework.transaction.support.TransactionTemplate;
 
 public class TransactionJobTimeoutInterceptor implements JobTimeoutInterceptor 
{
@@ -41,9 +42,25 @@ public class TransactionJobTimeoutInterceptor implements 
JobTimeoutInterceptor {
             public JobTimeoutExecution call() throws Exception {
                 return transactionTemplate.execute(status -> {
                     try {
+                        // Initialize transaction synchronization if not 
already active
+                        // This is needed for 
TransactionSynchronizationManager callbacks to work
+                        if 
(!TransactionSynchronizationManager.isSynchronizationActive()) {
+                            
TransactionSynchronizationManager.initSynchronization();
+                        }
+
+                        // Store the transaction status in thread-local for 
SpringBootTransactionRollbackMarker
+                        
SpringBootTransactionRollbackMarker.setCurrentTransactionStatus(status);
+
                         return callable.call();
+                    } catch (RuntimeException e) {
+                        // Re-throw RuntimeException to trigger rollback
+                        throw e;
                     } catch (Exception e) {
+                        // Wrap checked exceptions in RuntimeException to 
trigger rollback
                         throw new RuntimeException(e);
+                    } finally {
+                        // Clear the transaction status from thread-local
+                        
SpringBootTransactionRollbackMarker.clearCurrentTransactionStatus();
                     }
                 });
             }
diff --git 
a/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/springboot/MockDataRepository.java
 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/springboot/MockDataRepository.java
new file mode 100644
index 000000000..e08991d75
--- /dev/null
+++ 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/springboot/MockDataRepository.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.kogito.app.jobs.springboot;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.support.TransactionSynchronization;
+import 
org.springframework.transaction.support.TransactionSynchronizationManager;
+
+/**
+ * Mock repository that tracks data persistence attempts and simulates
+ * transactional behavior to verify that rollbacks prevent duplicate data.
+ */
+@Component
+public class MockDataRepository {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(MockDataRepository.class);
+
+    private final ConcurrentHashMap<String, AtomicInteger> committedData = new 
ConcurrentHashMap<>();
+    private final AtomicInteger totalPersistenceAttempts = new 
AtomicInteger(0);
+    private final AtomicInteger successfulCommits = new AtomicInteger(0);
+    private final AtomicInteger rolledBackAttempts = new AtomicInteger(0);
+
+    /**
+     * Simulates persisting data within a transaction.
+     * If the transaction is rolled back, the data should not be committed.
+     */
+    public void persistData(String key, String value) {
+        totalPersistenceAttempts.incrementAndGet();
+
+        // Register a transaction synchronization to track commit/rollback
+        if (TransactionSynchronizationManager.isSynchronizationActive()) {
+            TransactionSynchronizationManager.registerSynchronization(new 
TransactionSynchronization() {
+                @Override
+                public void afterCommit() {
+                    // Only commit the data if transaction succeeds
+                    committedData.computeIfAbsent(key, k -> new 
AtomicInteger(0)).incrementAndGet();
+                    successfulCommits.incrementAndGet();
+                }
+
+                @Override
+                public void afterCompletion(int status) {
+                    if (status == STATUS_ROLLED_BACK) {
+                        rolledBackAttempts.incrementAndGet();
+                    }
+                }
+            });
+        } else {
+            // No transaction active - commit immediately (shouldn't happen in 
our tests)
+            committedData.computeIfAbsent(key, k -> new 
AtomicInteger(0)).incrementAndGet();
+            successfulCommits.incrementAndGet();
+        }
+    }
+
+    public int getCommittedCount(String key) {
+        AtomicInteger count = committedData.get(key);
+        return count != null ? count.get() : 0;
+    }
+
+    public int getTotalPersistenceAttempts() {
+        return totalPersistenceAttempts.get();
+    }
+
+    public int getSuccessfulCommits() {
+        return successfulCommits.get();
+    }
+
+    public int getRolledBackAttempts() {
+        return rolledBackAttempts.get();
+    }
+
+    public void reset() {
+        committedData.clear();
+        totalPersistenceAttempts.set(0);
+        successfulCommits.set(0);
+        rolledBackAttempts.set(0);
+    }
+}
diff --git 
a/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/springboot/SpringBootTransactionRollbackTest.java
 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/springboot/SpringBootTransactionRollbackTest.java
new file mode 100644
index 000000000..781bbc0fe
--- /dev/null
+++ 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/springboot/SpringBootTransactionRollbackTest.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.kogito.app.jobs.springboot;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.kie.kogito.jobs.ExactExpirationTime;
+import org.kie.kogito.jobs.JobsService;
+import org.kie.kogito.jobs.descriptors.ProcessInstanceJobDescription;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.transaction.PlatformTransactionManager;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest
+public class SpringBootTransactionRollbackTest {
+
+    @Autowired
+    JobsService jobsService;
+
+    @Autowired
+    TestJobSchedulerListener listener;
+
+    @Autowired
+    TestJobExecutor testJobExecutor;
+
+    @Autowired
+    TestExceptionHandler exceptionHandler;
+
+    @Autowired
+    SpringBootTransactionRollbackMarker transactionRollbackMarker;
+
+    @Autowired
+    PlatformTransactionManager transactionManager;
+
+    @Autowired
+    MockDataRepository mockDataRepository;
+
+    @BeforeEach
+    public void init() {
+        testJobExecutor.reset();
+        exceptionHandler.reset();
+        mockDataRepository.reset();
+    }
+
+    @Test
+    public void testTransactionRollbackMarkerInjected() {
+        assertThat(transactionRollbackMarker).isNotNull();
+        assertThat(transactionRollbackMarker.isTransactionActive()).isFalse();
+    }
+
+    @Test
+    public void testTransactionRollbackOnJobFailure() throws Exception {
+        // Configure the test executor to fail 4 times (initial + 3 retries)
+        testJobExecutor.setNumberOfFailures(4);
+
+        ProcessInstanceJobDescription jobDescription = new 
ProcessInstanceJobDescription(
+                "rollback-test-job",
+                "-1",
+                
ExactExpirationTime.of(Instant.now().plus(Duration.ofSeconds(2)).atZone(ZoneId.of("UTC"))),
+                5,
+                "test-process-instance",
+                null,
+                "test-process",
+                null,
+                "test-node-instance");
+
+        listener.setCount(4);
+        jobsService.scheduleJob(jobDescription);
+
+        // Wait for all retries to complete and error handler to be invoked
+        Awaitility.await()
+                .atMost(Duration.ofSeconds(10L))
+                .untilAsserted(() -> {
+                    assertThat(exceptionHandler.isError())
+                            .as("Error handler should be invoked after max 
retries")
+                            .isTrue();
+                });
+
+        assertThat(listener.await(1, java.util.concurrent.TimeUnit.SECONDS))
+                .as("All job executions should have been attempted")
+                .isTrue();
+
+        assertThat(mockDataRepository.getTotalPersistenceAttempts())
+                .as("Should have 4 persistence attempts (initial + 3 retries)")
+                .isEqualTo(4);
+
+        assertThat(mockDataRepository.getRolledBackAttempts())
+                .as("All failed attempts should have been rolled back by 
Spring")
+                .isEqualTo(4);
+
+        assertThat(mockDataRepository.getSuccessfulCommits())
+                .as("No data should be committed when all attempts fail")
+                .isEqualTo(0);
+
+        assertThat(mockDataRepository.getCommittedCount("rollback-test-job"))
+                .as("No duplicate data should be persisted due to automatic 
rollback")
+                .isEqualTo(0);
+    }
+}
diff --git 
a/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/springboot/TestJobExecutor.java
 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/springboot/TestJobExecutor.java
index 52ee0627b..5f08e03b4 100644
--- 
a/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/springboot/TestJobExecutor.java
+++ 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/test/java/org/kie/kogito/app/jobs/springboot/TestJobExecutor.java
@@ -22,6 +22,7 @@ import org.kie.kogito.app.jobs.api.JobExecutor;
 import org.kie.kogito.jobs.service.model.JobDetails;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.config.ConfigurableBeanFactory;
 import org.springframework.context.annotation.Scope;
 import org.springframework.stereotype.Component;
@@ -33,6 +34,9 @@ public class TestJobExecutor implements JobExecutor {
     private Logger LOG = LoggerFactory.getLogger(TestJobExecutor.class);
     private int numberOfFailures;
 
+    @Autowired(required = false)
+    private MockDataRepository mockDataRepository;
+
     @Override
     public boolean accept(JobDetails jobDescription) {
         return true;
@@ -41,6 +45,12 @@ public class TestJobExecutor implements JobExecutor {
     @Override
     public void execute(JobDetails jobDescription) {
         LOG.info("executing {}", jobDescription);
+
+        // Simulate data persistence (e.g., creating a user task)
+        if (mockDataRepository != null) {
+            mockDataRepository.persistData(jobDescription.getId(), 
"execution-data");
+        }
+
         if (numberOfFailures > 0) {
             --numberOfFailures;
             throw new RuntimeException();


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to