diff --git a/docs/src/main/asciidoc/transaction.adoc b/docs/src/main/asciidoc/transaction.adoc index efed7ffa0cff6..919ca73b26ef3 100644 --- a/docs/src/main/asciidoc/transaction.adoc +++ b/docs/src/main/asciidoc/transaction.adoc @@ -307,6 +307,14 @@ The node name identifier needs to be unique per transaction manager deployment. And the node identifier needs to be stable over the transaction manager restarts. The node name identifier may be configured via the property `quarkus.transaction-manager.node-name`. +[NOTE] +==== +The node-name property has a length limitation, it cannot be longer than 28 bytes. + +The length limitation can possibly be solved by enabling the property 'quarkus.transaction-manager.shortenNodeNameIfNecessary' which, when enabled, will shorten names longer than 28 bytes. +The shortening process is done by applying the 'SHA-224' algorithm, encoding to Base64 and then truncating the result. Therefore this may result in the node-name ending as an unpredictable duplicate string and this breaks the uniqueness that is mandatory for safe transaction usage. +For more information about the potential for conflict see https://github.com/quarkusio/quarkus/issues/30491 +==== [[transaction-scope]] == Using `@TransactionScoped` to bind CDI beans to the transaction lifecycle diff --git a/extensions/narayana-jta/runtime/pom.xml b/extensions/narayana-jta/runtime/pom.xml index fdd82659db7e4..a6b76942406b0 100644 --- a/extensions/narayana-jta/runtime/pom.xml +++ b/extensions/narayana-jta/runtime/pom.xml @@ -83,6 +83,17 @@ org.jboss.narayana.jts narayana-jts-integration + + + io.quarkus + quarkus-junit5-internal + test + + + io.quarkus + quarkus-junit5 + test + 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 156dbd6d1c865..3363fb1276547 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 @@ -5,6 +5,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Map; @@ -60,7 +61,15 @@ private static void shortenNodeName(TransactionManagerConfiguration transactions HASH_ALGORITHM_FOR_SHORTENING); final byte[] nodeNameAsBytes = originalNodeName.getBytes(); MessageDigest messageDigest224 = MessageDigest.getInstance(HASH_ALGORITHM_FOR_SHORTENING); - transactions.nodeName = new String(messageDigest224.digest(nodeNameAsBytes), StandardCharsets.UTF_8); + byte[] hashedByteArray = messageDigest224.digest(nodeNameAsBytes); + + //Encode the byte array in Base64 + //encoding the array might result in a longer array + byte[] base64Result = Base64.getEncoder().encode(hashedByteArray); + //truncate the array + byte[] slice = Arrays.copyOfRange(base64Result, 0, 28); + + transactions.nodeName = new String(slice, StandardCharsets.UTF_8); log.warnf("New node name is \"%s\"", transactions.nodeName); } diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerConfiguration.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerConfiguration.java index 6719ea8e1d128..cad532cc6d9a4 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerConfiguration.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerConfiguration.java @@ -27,7 +27,7 @@ public final class TransactionManagerConfiguration { * Whether the node name should be shortened if necessary. * The node name must not exceed a length of 28 bytes. If this property is set to {@code true}, and the node name exceeds 28 * bytes, the node name is shortened by calculating the SHA-224 hash, - * which has a length of 28 bytes. + * which has a length of 28 bytes, encoded to Base64 format and then shorten to 28 bytes. * * @see #nodeName */ diff --git a/extensions/narayana-jta/runtime/src/test/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorderTest.java b/extensions/narayana-jta/runtime/src/test/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorderTest.java new file mode 100644 index 0000000000000..f10bb28768cef --- /dev/null +++ b/extensions/narayana-jta/runtime/src/test/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorderTest.java @@ -0,0 +1,53 @@ +package io.quarkus.narayana.jta.runtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +public class NarayanaJtaRecorderTest { + + //this string has been chosen as when hashed and Base64 encoded the resulted byte array will have a length > 28, so it will be trimmed too. + public static final String NODE_NAME_TO_SHORTEN = "dfe2420d-b12e-4ec3-92c0-ee7c4"; + + @Test + void testByteLengthWithLongerString() { + TransactionManagerConfiguration transactions = new TransactionManagerConfiguration(); + transactions.shortenNodeNameIfNecessary = true; + // create nodeNames larger than 28 bytes + assertTrue(NODE_NAME_TO_SHORTEN.getBytes(StandardCharsets.UTF_8).length > 28); + NarayanaJtaRecorder r = new NarayanaJtaRecorder(); + transactions.nodeName = NODE_NAME_TO_SHORTEN; + r.setNodeName(transactions); + int numberOfBytes = transactions.nodeName.getBytes(StandardCharsets.UTF_8).length; + assertEquals(28, numberOfBytes, + "node name bytes was not 28 bytes limit, number of bytes is " + numberOfBytes); + assertFalse(numberOfBytes > 28, + "node name bytes still exceeded 28 bytes limit, number of bytes is " + numberOfBytes); + } + + @Test + void testPredictableConversion() { + TransactionManagerConfiguration transactions = new TransactionManagerConfiguration(); + transactions.shortenNodeNameIfNecessary = true; + assertTrue(NODE_NAME_TO_SHORTEN.getBytes(StandardCharsets.UTF_8).length > 28); + NarayanaJtaRecorder r = new NarayanaJtaRecorder(); + transactions.nodeName = NODE_NAME_TO_SHORTEN; + r.setNodeName(transactions); + int numberOfBytes = transactions.nodeName.getBytes(StandardCharsets.UTF_8).length; + assertEquals(28, numberOfBytes, + "node name bytes was not 28 bytes limit, number of bytes is " + numberOfBytes); + String firstConversion = transactions.nodeName; + transactions.nodeName = NODE_NAME_TO_SHORTEN; + r.setNodeName(transactions); + String secondConversion = transactions.nodeName; + numberOfBytes = transactions.nodeName.getBytes(StandardCharsets.UTF_8).length; + assertEquals(28, numberOfBytes, + "node name bytes was not 28 bytes limit, number of bytes is " + numberOfBytes); + assertEquals(firstConversion, secondConversion, + "Node names were shortened differently: " + firstConversion + " " + secondConversion); + } +}