From 50ef80e12473f630285209038a2ba4d68314a2c3 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 5 Nov 2026 00:34:53 -0800 Subject: [PATCH] Preserve commit notifications when shutdown rejects submission Catch RejectedExecutionException around submission and run the same notification task synchronously. Add a regression test that verifies the persist listener is notified when submission is rejected. Fixes #2565 --- .../transaction/TransactionManager.java | 9 +++- .../TransactionManagerNotifyOfCommitTest.java | 44 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 ebean-core/src/test/java/io/ebeaninternal/server/transaction/TransactionManagerNotifyOfCommitTest.java diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java index 18d392ea60..fa504d2a29 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java @@ -40,6 +40,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.RejectedExecutionException; import static java.lang.System.Logger.Level.DEBUG; import static java.lang.System.Logger.Level.ERROR; @@ -336,7 +337,13 @@ public final void notifyOfCommit(SpiTransaction transaction) { try { PostCommitProcessing postCommit = new PostCommitProcessing(clusterManager, this, transaction); postCommit.notifyLocalCache(); - backgroundExecutor.execute(postCommit.backgroundNotify()); + Runnable backgroundNotify = postCommit.backgroundNotify(); + try { + backgroundExecutor.execute(backgroundNotify); + } catch (RejectedExecutionException ex) { + // Preserve notifications when shutdown has stopped the background executor. + backgroundNotify.run(); + } } catch (Exception ex) { log.log(ERROR, "NotifyOfCommit failed. L2 Cache potentially not notified.", ex); } diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/transaction/TransactionManagerNotifyOfCommitTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/transaction/TransactionManagerNotifyOfCommitTest.java new file mode 100644 index 0000000000..c2f2b9b2de --- /dev/null +++ b/ebean-core/src/test/java/io/ebeaninternal/server/transaction/TransactionManagerNotifyOfCommitTest.java @@ -0,0 +1,44 @@ +package io.ebeaninternal.server.transaction; + +import io.ebean.BackgroundExecutor; +import io.ebean.cache.ServerCacheNotify; +import io.ebean.config.DatabaseConfig; +import io.ebean.config.dbplatform.DatabasePlatform; +import io.ebean.plugin.SpiServer; +import io.ebeaninternal.api.SpiLogManager; +import io.ebeaninternal.api.SpiProfileHandler; +import io.ebeaninternal.api.SpiTransaction; +import io.ebeaninternal.api.TransactionEvent; +import io.ebeaninternal.server.cluster.ClusterManager; +import io.ebeaninternal.server.core.PersistRequestBean; +import io.ebeaninternal.server.deploy.BeanDescriptorManager; +import io.ebeanservice.docstore.api.DocStoreUpdateProcessor; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.RejectedExecutionException; + +import static org.mockito.Mockito.*; + +class TransactionManagerNotifyOfCommitTest { + + @Test + void rejectedSubmission_notifiesListenerSynchronously() { + var executor = mock(BackgroundExecutor.class); + doThrow(new RejectedExecutionException()).when(executor).execute(any(Runnable.class)); + var config = new DatabaseConfig(); + config.setDatabasePlatform(new DatabasePlatform()); + var manager = new TransactionManager(new TransactionManagerOptions(mock(SpiServer.class), true, config, + mock(TransactionScopeManager.class), mock(ClusterManager.class), executor, + mock(DocStoreUpdateProcessor.class), mock(BeanDescriptorManager.class), mock(DataSourceSupplier.class), + mock(SpiProfileHandler.class), mock(SpiLogManager.class), mock(TableModState.class), mock(ServerCacheNotify.class))); + var listener = mock(PersistRequestBean.class); + var event = new TransactionEvent(); + event.addListenerNotify(listener); + var transaction = mock(SpiTransaction.class); + when(transaction.event()).thenReturn(event); + + manager.notifyOfCommit(transaction); + + verify(listener).notifyLocalPersistListener(); + } +}