diff --git a/docs/src/main/asciidoc/datasource.adoc b/docs/src/main/asciidoc/datasource.adoc index a56046cb64efd..8c40a26fe3580 100644 --- a/docs/src/main/asciidoc/datasource.adoc +++ b/docs/src/main/asciidoc/datasource.adoc @@ -517,6 +517,84 @@ public class MyProducer { ---- ==== +[[datasource-multiple-single-transaction]] +=== Use multiple datasources in a single transaction + +By default, XA support on datasources is disabled, +and thus a transaction may include at most one datasource. +Attempting to access multiple non-XA datasources in the same transaction +would result in an exception similar to this: + +[source] +---- +... +Caused by: java.sql.SQLException: Exception in association of connection to existing transaction + at io.agroal.narayana.NarayanaTransactionIntegration.associate(NarayanaTransactionIntegration.java:130) + ... +Caused by: java.sql.SQLException: Unable to enlist connection to existing transaction + at io.agroal.narayana.NarayanaTransactionIntegration.associate(NarayanaTransactionIntegration.java:121) + ... +---- + +To allow using multiple JDBC datasources in the same transaction: + +. Make sure your JDBC driver supports XA. +All <>, +but <> might not. +. Make sure your database server is configured to enable XA. +. Enable XA support explicitly for each relevant datasource by setting +<> to `xa`. + +Using XA, a rollback in one datasource will trigger a rollback in every other datasource enrolled in the transaction. + +[NOTE] +==== +XA transactions on reactive datasources are not supported at the moment. +==== + +[NOTE] +==== +If your transaction involves other, non-datasource resources, +keep in mind *those* resources might not support XA transactions, +or might require additional configuration. +==== + +If XA cannot be enabled for one of your datasources: + +* Be aware that enabling XA for all datasources _except one_ (and only one) is still supported +through https://www.narayana.io/docs/project/index.html#d5e857[Last Resource Commit Optimization (LRCO)]. +* If you do not need a rollback for one datasource to trigger a rollback for other datasources, +consider splitting your code into multiple transactions. +To that end, use xref:transaction.adoc#programmatic-approach[`QuarkusTransaction.requiringNew()`]/xref:transaction.adoc#declarative-approach[`@Transactional(REQUIRES_NEW)`] (preferably) +or xref:transaction.adoc#legacy-api-approach[`UserTransaction`] (for more complex use cases). + +[CAUTION] +==== +As a last resort, and for compatibility with Quarkus 3.8 and earlier, +you may allow unsafe transaction handling across multiple non-XA datasources +by setting `quarkus.transaction-manager.unsafe-multiple-last-resources` to `allow`. + +With this property set to `allow`, a transaction rollback +could possibly be applied to only some of the non-XA datasources, +with other non-XA datasources having already committed their changes, +leaving your overall system in an inconsistent state. + +Alternatively, you can allow the same unsafe behavior, +but with warnings when it is taken advantage of: + +* setting the property to `warn-each` +would result in logging a warning on *each* offending transaction. +* setting the property to `warn-first` +would result in logging a warning on the *first* offending transaction. + +We do not recommend using this configuration property, +and we plan to remove it in the future, +so you should plan fixing your application accordingly. +If you think your use case of this feature is valid and this option should be kept around, +open an issue in the https://github.com/quarkusio/quarkus/issues/new?assignees=&labels=kind%2Fenhancement&projects=&template=feature_request.yml[Quarkus tracker] +explaining why. +==== + == Datasource integrations === Datasource health check diff --git a/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java b/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java index 396279af870c0..51cce765f1fa9 100644 --- a/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java +++ b/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java @@ -1,6 +1,7 @@ package io.quarkus.narayana.jta.deployment; import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; +import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; import java.util.List; import java.util.Map; @@ -47,6 +48,8 @@ import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; import io.quarkus.deployment.IsTest; import io.quarkus.deployment.annotations.BuildProducer; @@ -56,16 +59,21 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.NativeImageFeatureBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import io.quarkus.gizmo.ClassCreator; import io.quarkus.narayana.jta.runtime.NarayanaJtaProducers; import io.quarkus.narayana.jta.runtime.NarayanaJtaRecorder; +import io.quarkus.narayana.jta.runtime.TransactionManagerBuildTimeConfig; +import io.quarkus.narayana.jta.runtime.TransactionManagerBuildTimeConfig.UnsafeMultipleLastResourcesMode; import io.quarkus.narayana.jta.runtime.TransactionManagerConfiguration; import io.quarkus.narayana.jta.runtime.context.TransactionContext; +import io.quarkus.narayana.jta.runtime.graal.DisableLoggingFeature; import io.quarkus.narayana.jta.runtime.interceptor.TestTransactionInterceptor; import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorMandatory; import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorNever; @@ -93,7 +101,8 @@ public void build(NarayanaJtaRecorder recorder, BuildProducer reflectiveClass, BuildProducer runtimeInit, BuildProducer feature, - TransactionManagerConfiguration transactions, ShutdownContextBuildItem shutdownContextBuildItem) { + TransactionManagerConfiguration transactions, TransactionManagerBuildTimeConfig transactionManagerBuildTimeConfig, + ShutdownContextBuildItem shutdownContextBuildItem) { recorder.handleShutdown(shutdownContextBuildItem, transactions); feature.produce(new FeatureBuildItem(Feature.NARAYANA_JTA)); additionalBeans.produce(new AdditionalBeanBuildItem(NarayanaJtaProducers.class)); @@ -137,6 +146,12 @@ public void build(NarayanaJtaRecorder recorder, builder.addBeanClass(TransactionalInterceptorNotSupported.class); additionalBeans.produce(builder.build()); + transactionManagerBuildTimeConfig.unsafeMultipleLastResources.ifPresent(mode -> { + if (!mode.equals(UnsafeMultipleLastResourcesMode.FAIL)) { + recorder.logUnsafeMultipleLastResourcesOnStartup(mode); + } + }); + //we want to force Arjuna to init at static init time Properties defaultProperties = PropertiesFactory.getDefaultProperties(); //we don't want to store the system properties here @@ -144,6 +159,7 @@ public void build(NarayanaJtaRecorder recorder, for (Object i : System.getProperties().keySet()) { defaultProperties.remove(i); } + recorder.setDefaultProperties(defaultProperties); // This must be done before setNodeName as the code in setNodeName will create a TSM based on the value of this property recorder.disableTransactionStatusManager(); @@ -152,6 +168,50 @@ public void build(NarayanaJtaRecorder recorder, recorder.setConfig(transactions); } + @BuildStep + @Record(STATIC_INIT) + public void allowUnsafeMultipleLastResources(NarayanaJtaRecorder recorder, + TransactionManagerBuildTimeConfig transactionManagerBuildTimeConfig, + Capabilities capabilities, BuildProducer logCleanupFilters, + BuildProducer nativeImageFeatures) { + switch (transactionManagerBuildTimeConfig.unsafeMultipleLastResources + .orElse(UnsafeMultipleLastResourcesMode.DEFAULT)) { + case ALLOW -> { + recorder.allowUnsafeMultipleLastResources(capabilities.isPresent(Capability.AGROAL), true); + // we will handle the warnings ourselves at runtime init when the option is set explicitly + logCleanupFilters.produce( + new LogCleanupFilterBuildItem("com.arjuna.ats.arjuna", "ARJUNA012139", "ARJUNA012141", "ARJUNA012142")); + } + case WARN_FIRST -> { + recorder.allowUnsafeMultipleLastResources(capabilities.isPresent(Capability.AGROAL), true); + // we will handle the warnings ourselves at runtime init when the option is set explicitly + // but we still want Narayana to produce a warning on the first offending transaction + logCleanupFilters.produce( + new LogCleanupFilterBuildItem("com.arjuna.ats.arjuna", "ARJUNA012139", "ARJUNA012142")); + } + case WARN_EACH -> { + recorder.allowUnsafeMultipleLastResources(capabilities.isPresent(Capability.AGROAL), false); + // we will handle the warnings ourselves at runtime init when the option is set explicitly + // but we still want Narayana to produce one warning per offending transaction + logCleanupFilters.produce( + new LogCleanupFilterBuildItem("com.arjuna.ats.arjuna", "ARJUNA012139", "ARJUNA012142")); + } + case FAIL -> { // No need to do anything, this is the default behavior of Narayana + } + } + } + + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) + public void nativeImageFeature(TransactionManagerBuildTimeConfig transactionManagerBuildTimeConfig, + BuildProducer nativeImageFeatures) { + switch (transactionManagerBuildTimeConfig.unsafeMultipleLastResources + .orElse(UnsafeMultipleLastResourcesMode.DEFAULT)) { + case ALLOW, WARN_FIRST, WARN_EACH -> { + nativeImageFeatures.produce(new NativeImageFeatureBuildItem(DisableLoggingFeature.class)); + } + } + } + @BuildStep @Record(RUNTIME_INIT) @Consume(NarayanaInitBuildItem.class) diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java index 83d9f1b865cfa..156dbd6d1c865 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java @@ -29,6 +29,7 @@ import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.runtime.util.StringUtil; @Recorder public class NarayanaJtaRecorder { @@ -110,6 +111,30 @@ public void setConfig(final TransactionManagerConfiguration transactions) { .setXaResourceOrphanFilterClassNames(transactions.xaResourceOrphanFilters); } + /** + * This should be removed in the future. + */ + @Deprecated(forRemoval = true) + public void allowUnsafeMultipleLastResources(boolean agroalPresent, boolean disableMultipleLastResourcesWarning) { + arjPropertyManager.getCoreEnvironmentBean().setAllowMultipleLastResources(true); + arjPropertyManager.getCoreEnvironmentBean().setDisableMultipleLastResourcesWarning(disableMultipleLastResourcesWarning); + if (agroalPresent) { + jtaPropertyManager.getJTAEnvironmentBean() + .setLastResourceOptimisationInterfaceClassName("io.agroal.narayana.LocalXAResource"); + } + } + + /** + * This should be removed in the future. + */ + @Deprecated(forRemoval = true) + public void logUnsafeMultipleLastResourcesOnStartup( + TransactionManagerBuildTimeConfig.UnsafeMultipleLastResourcesMode mode) { + log.warnf( + "Setting quarkus.transaction-manager.unsafe-multiple-last-resources to '%s' makes adding multiple resources to the same transaction unsafe.", + StringUtil.hyphenate(mode.name()).replace('_', '-')); + } + private void setObjectStoreDir(String name, TransactionManagerConfiguration config) { BeanPopulator.getNamedInstance(ObjectStoreEnvironmentBean.class, name).setObjectStoreDir(config.objectStore.directory); } diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerBuildTimeConfig.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerBuildTimeConfig.java new file mode 100644 index 0000000000000..dced5709b63cc --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerBuildTimeConfig.java @@ -0,0 +1,66 @@ +package io.quarkus.narayana.jta.runtime; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public final class TransactionManagerBuildTimeConfig { + /** + * Define the behavior when using multiple XA unaware resources in the same transactional demarcation. + *

+ * Defaults to {@code fail}. + * {@code warn} and {@code allow} are UNSAFE and should only be used for compatibility. + * Either use XA for all resources if you want consistency, or split the code into separate + * methods with separate transactions. + *

+ * Note that using a single XA unaware resource together with XA aware resources, known as + * the Last Resource Commit Optimization (LRCO), is different from using multiple XA unaware + * resources. Although LRCO allows most transactions to complete normally, some errors can + * cause an inconsistent transaction outcome. Using multiple XA unaware resources is not + * recommended since the probability of inconsistent outcomes is significantly higher and + * much harder to recover from than LRCO. For this reason, use LRCO as a last resort. + *

+ * We do not recommend using this configuration property, and we plan to remove it in the future, + * so you should plan fixing your application accordingly. + * If you think your use case of this feature is valid and this option should be kept around, + * open an issue in our tracker explaining why. + * + * @deprecated This property is planned for removal in a future version. + */ + @Deprecated(forRemoval = true) + @ConfigItem(defaultValueDocumentation = "fail") + public Optional unsafeMultipleLastResources; + + public enum UnsafeMultipleLastResourcesMode { + /** + * Allow using multiple XA unaware resources in the same transactional demarcation. + *

+ * This will log a warning once on application startup, + * but not on each use of multiple XA unaware resources in the same transactional demarcation. + */ + ALLOW, + /** + * Allow using multiple XA unaware resources in the same transactional demarcation, + * but log a warning on the first occurrence. + */ + WARN_FIRST, + /** + * Allow using multiple XA unaware resources in the same transactional demarcation, + * but log a warning on each occurrence. + */ + WARN_EACH, + /** + * Allow using multiple XA unaware resources in the same transactional demarcation, + * but log a warning on each occurrence. + */ + FAIL; + + // The default is WARN in Quarkus 3.8, FAIL in Quarkus 3.9+ + // Make sure to update defaultValueDocumentation on unsafeMultipleLastResources when changing this. + public static final UnsafeMultipleLastResourcesMode DEFAULT = FAIL; + } + +} diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/graal/DisableLoggingFeature.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/graal/DisableLoggingFeature.java new file mode 100644 index 0000000000000..1e32c4ec2a9d7 --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/graal/DisableLoggingFeature.java @@ -0,0 +1,43 @@ +package io.quarkus.narayana.jta.runtime.graal; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.graalvm.nativeimage.hosted.Feature; + +/** + * Disables logging during the analysis phase + */ +public class DisableLoggingFeature implements Feature { + + private static final String[] CATEGORIES = { + "com.arjuna.ats.arjuna" + }; + + private final Map categoryMap = new HashMap<>(CATEGORIES.length); + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + for (String category : CATEGORIES) { + Logger logger = Logger.getLogger(category); + categoryMap.put(category, logger.getLevel()); + logger.setLevel(Level.SEVERE); + } + } + + @Override + public void afterAnalysis(AfterAnalysisAccess access) { + for (String category : CATEGORIES) { + Level level = categoryMap.remove(category); + Logger logger = Logger.getLogger(category); + logger.setLevel(level); + } + } + + @Override + public String getDescription() { + return "Disables INFO and WARN logging during the analysis phase"; + } +}