diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/XGoogSpannerRequestId.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/XGoogSpannerRequestId.java new file mode 100644 index 00000000000..4f6c0114750 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/XGoogSpannerRequestId.java @@ -0,0 +1,88 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed 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 com.google.cloud.spanner; + +import com.google.api.core.InternalApi; +import com.google.common.annotations.VisibleForTesting; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Objects; + +@InternalApi +public class XGoogSpannerRequestId { + // 1. Generate the random process Id singleton. + @VisibleForTesting + static final String RAND_PROCESS_ID = XGoogSpannerRequestId.generateRandProcessId(); + + @VisibleForTesting + static final long VERSION = 1; // The version of the specification being implemented. + + private final long nthClientId; + private final long nthChannelId; + private final long nthRequest; + private long attempt; + + XGoogSpannerRequestId(long nthClientId, long nthChannelId, long nthRequest, long attempt) { + this.nthClientId = nthClientId; + this.nthChannelId = nthChannelId; + this.nthRequest = nthRequest; + this.attempt = attempt; + } + + public static XGoogSpannerRequestId of( + long nthClientId, long nthChannelId, long nthRequest, long attempt) { + return new XGoogSpannerRequestId(nthClientId, nthChannelId, nthRequest, attempt); + } + + private static String generateRandProcessId() { + // Expecting to use 64-bits of randomness to avoid clashes. + BigInteger bigInt = new BigInteger(64, new SecureRandom()); + return String.format("%016x", bigInt); + } + + @Override + public String toString() { + return String.format( + "%d.%s.%d.%d.%d.%d", + XGoogSpannerRequestId.VERSION, + XGoogSpannerRequestId.RAND_PROCESS_ID, + this.nthClientId, + this.nthChannelId, + this.nthRequest, + this.attempt); + } + + @Override + public boolean equals(Object other) { + // instanceof for a null object returns false. + if (!(other instanceof XGoogSpannerRequestId)) { + return false; + } + + XGoogSpannerRequestId otherReqId = (XGoogSpannerRequestId) (other); + + return Objects.equals(this.nthClientId, otherReqId.nthClientId) + && Objects.equals(this.nthChannelId, otherReqId.nthChannelId) + && Objects.equals(this.nthRequest, otherReqId.nthRequest) + && Objects.equals(this.attempt, otherReqId.attempt); + } + + @Override + public int hashCode() { + return Objects.hash(this.nthClientId, this.nthChannelId, this.nthRequest, this.attempt); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/XGoogSpannerRequestIdTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/XGoogSpannerRequestIdTest.java new file mode 100644 index 00000000000..12c9213c7dc --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/XGoogSpannerRequestIdTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed 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 + * + * https://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 com.google.cloud.spanner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class XGoogSpannerRequestIdTest { + private static final Pattern REGEX_RAND_PROCESS_ID = + Pattern.compile("1.([0-9a-z]{16})(\\.\\d+){3}\\.(\\d+)$"); + + @Test + public void testEquals() { + XGoogSpannerRequestId reqID1 = XGoogSpannerRequestId.of(1, 1, 1, 1); + XGoogSpannerRequestId reqID2 = XGoogSpannerRequestId.of(1, 1, 1, 1); + assertEquals(reqID1, reqID2); + assertEquals(reqID1, reqID1); + assertEquals(reqID2, reqID2); + + XGoogSpannerRequestId reqID3 = XGoogSpannerRequestId.of(1, 1, 1, 2); + assertNotEquals(reqID1, reqID3); + assertNotEquals(reqID3, reqID1); + assertEquals(reqID3, reqID3); + } + + @Test + public void testEnsureHexadecimalFormatForRandProcessID() { + String str = XGoogSpannerRequestId.of(1, 2, 3, 4).toString(); + Matcher m = XGoogSpannerRequestIdTest.REGEX_RAND_PROCESS_ID.matcher(str); + assertTrue(m.matches()); + } +}