Skip to content

Commit 326442b

Browse files
authored
feat: add LockHint feature (#3588)
* feat: add LockHint feature * fix typo * updated logic to make sure lockHint get propagated only for ReadWriteTransaction * add warning message when using lock hint with unsupported transaction
1 parent 7af512b commit 326442b

File tree

5 files changed

+163
-3
lines changed

5 files changed

+163
-3
lines changed

google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java

+11
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import java.util.Map;
5959
import java.util.concurrent.ThreadLocalRandom;
6060
import java.util.concurrent.atomic.AtomicLong;
61+
import java.util.logging.Logger;
6162
import javax.annotation.Nullable;
6263
import javax.annotation.concurrent.GuardedBy;
6364

@@ -67,6 +68,7 @@
6768
*/
6869
abstract class AbstractReadContext
6970
implements ReadContext, AbstractResultSet.Listener, SessionTransaction {
71+
private static final Logger logger = Logger.getLogger(AbstractReadContext.class.getName());
7072

7173
abstract static class Builder<B extends Builder<?, T>, T extends AbstractReadContext> {
7274
private SessionImpl session;
@@ -951,6 +953,15 @@ ResultSet readInternalWithOptions(
951953
} else if (defaultDirectedReadOptions != null) {
952954
builder.setDirectedReadOptions(defaultDirectedReadOptions);
953955
}
956+
if (readOptions.hasLockHint()) {
957+
if (isReadOnly()) {
958+
logger.warning(
959+
"Lock hint is only supported for ReadWrite transactions. "
960+
+ "Overriding lock hint to default unspecified.");
961+
} else {
962+
builder.setLockHint(readOptions.lockHint());
963+
}
964+
}
954965
final int prefetchChunks =
955966
readOptions.hasPrefetchChunks() ? readOptions.prefetchChunks() : defaultPrefetchChunks;
956967
ResumableStreamIterator stream =

google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java

+54-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.common.base.Preconditions;
2020
import com.google.spanner.v1.DirectedReadOptions;
21+
import com.google.spanner.v1.ReadRequest.LockHint;
2122
import com.google.spanner.v1.ReadRequest.OrderBy;
2223
import com.google.spanner.v1.RequestOptions.Priority;
2324
import java.io.Serializable;
@@ -75,6 +76,25 @@ public static RpcOrderBy fromProto(OrderBy proto) {
7576
}
7677
}
7778

79+
public enum RpcLockHint {
80+
UNSPECIFIED(LockHint.LOCK_HINT_UNSPECIFIED),
81+
SHARED(LockHint.LOCK_HINT_SHARED),
82+
EXCLUSIVE(LockHint.LOCK_HINT_EXCLUSIVE);
83+
84+
private final LockHint proto;
85+
86+
RpcLockHint(LockHint proto) {
87+
this.proto = Preconditions.checkNotNull(proto);
88+
}
89+
90+
public static RpcLockHint fromProto(LockHint proto) {
91+
for (RpcLockHint e : RpcLockHint.values()) {
92+
if (e.proto.equals(proto)) return e;
93+
}
94+
return RpcLockHint.UNSPECIFIED;
95+
}
96+
}
97+
7898
/** Marker interface to mark options applicable to both Read and Query operations */
7999
public interface ReadAndQueryOption extends ReadOption, QueryOption {}
80100

@@ -160,6 +180,10 @@ public static ReadOption orderBy(RpcOrderBy orderBy) {
160180
return new OrderByOption(orderBy);
161181
}
162182

183+
public static ReadOption lockHint(RpcLockHint orderBy) {
184+
return new LockHintOption(orderBy);
185+
}
186+
163187
/**
164188
* Specifying this will allow the client to prefetch up to {@code prefetchChunks} {@code
165189
* PartialResultSet} chunks for read and query. The data size of each chunk depends on the server
@@ -469,6 +493,7 @@ void appendToOptions(Options options) {
469493
private DirectedReadOptions directedReadOptions;
470494
private DecodeMode decodeMode;
471495
private RpcOrderBy orderBy;
496+
private RpcLockHint lockHint;
472497

473498
// Construction is via factory methods below.
474499
private Options() {}
@@ -605,6 +630,14 @@ OrderBy orderBy() {
605630
return orderBy == null ? null : orderBy.proto;
606631
}
607632

633+
boolean hasLockHint() {
634+
return lockHint != null;
635+
}
636+
637+
LockHint lockHint() {
638+
return lockHint == null ? null : lockHint.proto;
639+
}
640+
608641
@Override
609642
public String toString() {
610643
StringBuilder b = new StringBuilder();
@@ -661,6 +694,9 @@ public String toString() {
661694
if (orderBy != null) {
662695
b.append("orderBy: ").append(orderBy).append(' ');
663696
}
697+
if (lockHint != null) {
698+
b.append("lockHint: ").append(lockHint).append(' ');
699+
}
664700
return b.toString();
665701
}
666702

@@ -700,7 +736,8 @@ public boolean equals(Object o) {
700736
&& Objects.equals(withExcludeTxnFromChangeStreams(), that.withExcludeTxnFromChangeStreams())
701737
&& Objects.equals(dataBoostEnabled(), that.dataBoostEnabled())
702738
&& Objects.equals(directedReadOptions(), that.directedReadOptions())
703-
&& Objects.equals(orderBy(), that.orderBy());
739+
&& Objects.equals(orderBy(), that.orderBy())
740+
&& Objects.equals(lockHint(), that.lockHint());
704741
}
705742

706743
@Override
@@ -760,6 +797,9 @@ public int hashCode() {
760797
if (orderBy != null) {
761798
result = 31 * result + orderBy.hashCode();
762799
}
800+
if (lockHint != null) {
801+
result = 31 * result + lockHint.hashCode();
802+
}
763803
return result;
764804
}
765805

@@ -853,6 +893,19 @@ void appendToOptions(Options options) {
853893
}
854894
}
855895

896+
static class LockHintOption extends InternalOption implements ReadOption {
897+
private final RpcLockHint lockHint;
898+
899+
LockHintOption(RpcLockHint lockHint) {
900+
this.lockHint = lockHint;
901+
}
902+
903+
@Override
904+
void appendToOptions(Options options) {
905+
options.lockHint = lockHint;
906+
}
907+
}
908+
856909
static final class DataBoostQueryOption extends InternalOption implements ReadAndQueryOption {
857910

858911
private final Boolean dataBoostEnabled;

google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractReadContextTest.java

+16
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.google.spanner.v1.ExecuteSqlRequest.QueryMode;
3535
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
3636
import com.google.spanner.v1.ReadRequest;
37+
import com.google.spanner.v1.ReadRequest.LockHint;
3738
import com.google.spanner.v1.ReadRequest.OrderBy;
3839
import com.google.spanner.v1.RequestOptions;
3940
import com.google.spanner.v1.RequestOptions.Priority;
@@ -241,6 +242,21 @@ public void testGetReadRequestBuilderWithOrderBy() {
241242
assertEquals(OrderBy.ORDER_BY_NO_ORDER, request.getOrderBy());
242243
}
243244

245+
@Test
246+
public void testGetReadRequestBuilderWithLockHint() {
247+
ReadRequest request =
248+
ReadRequest.newBuilder()
249+
.setSession(
250+
SessionName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]").toString())
251+
.setTransaction(TransactionSelector.newBuilder().build())
252+
.setTable("table110115790")
253+
.setIndex("index100346066")
254+
.addAllColumns(new ArrayList<String>())
255+
.setLockHintValue(2)
256+
.build();
257+
assertEquals(LockHint.LOCK_HINT_EXCLUSIVE, request.getLockHint());
258+
}
259+
244260
@Test
245261
public void testGetExecuteBatchDmlRequestBuilderWithPriority() {
246262
ExecuteBatchDmlRequest.Builder request =

google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java

+49
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture;
5353
import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime;
5454
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
55+
import com.google.cloud.spanner.Options.RpcLockHint;
5556
import com.google.cloud.spanner.Options.RpcOrderBy;
5657
import com.google.cloud.spanner.Options.RpcPriority;
5758
import com.google.cloud.spanner.Options.TransactionOption;
@@ -90,6 +91,7 @@
9091
import com.google.spanner.v1.ExecuteSqlRequest.QueryMode;
9192
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
9293
import com.google.spanner.v1.ReadRequest;
94+
import com.google.spanner.v1.ReadRequest.LockHint;
9395
import com.google.spanner.v1.ReadRequest.OrderBy;
9496
import com.google.spanner.v1.RequestOptions.Priority;
9597
import com.google.spanner.v1.ResultSetMetadata;
@@ -1754,6 +1756,53 @@ public void testExecuteReadWithOrderByOption() {
17541756
assertEquals(OrderBy.ORDER_BY_NO_ORDER, request.getOrderBy());
17551757
}
17561758

1759+
@Test
1760+
public void testUnsupportedTransactionWithLockHintOption() {
1761+
DatabaseClient client =
1762+
spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
1763+
try (ResultSet resultSet =
1764+
client
1765+
.singleUse()
1766+
.read(
1767+
READ_TABLE_NAME,
1768+
KeySet.singleKey(Key.of(1L)),
1769+
READ_COLUMN_NAMES,
1770+
Options.lockHint(RpcLockHint.EXCLUSIVE))) {
1771+
consumeResults(resultSet);
1772+
}
1773+
1774+
List<ReadRequest> requests = mockSpanner.getRequestsOfType(ReadRequest.class);
1775+
assertThat(requests).hasSize(1);
1776+
ReadRequest request = requests.get(0);
1777+
// lock hint is only supported in ReadWriteTransaction
1778+
assertEquals(LockHint.LOCK_HINT_UNSPECIFIED, request.getLockHint());
1779+
}
1780+
1781+
@Test
1782+
public void testReadWriteTransactionWithLockHint() {
1783+
DatabaseClient client =
1784+
spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
1785+
1786+
TransactionRunner runner = client.readWriteTransaction();
1787+
runner.run(
1788+
transaction -> {
1789+
try (ResultSet resultSet =
1790+
transaction.read(
1791+
READ_TABLE_NAME,
1792+
KeySet.singleKey(Key.of(1L)),
1793+
READ_COLUMN_NAMES,
1794+
Options.lockHint(RpcLockHint.EXCLUSIVE))) {
1795+
consumeResults(resultSet);
1796+
}
1797+
return null;
1798+
});
1799+
1800+
List<ReadRequest> requests = mockSpanner.getRequestsOfType(ReadRequest.class);
1801+
assertThat(requests).hasSize(1);
1802+
ReadRequest request = requests.get(0);
1803+
assertEquals(LockHint.LOCK_HINT_EXCLUSIVE, request.getLockHint());
1804+
}
1805+
17571806
@Test
17581807
public void testExecuteReadWithDirectedReadOptions() {
17591808
DatabaseClient client =

google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java

+33-2
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525
import static org.junit.Assert.assertThrows;
2626
import static org.junit.Assert.assertTrue;
2727

28+
import com.google.cloud.spanner.Options.RpcLockHint;
2829
import com.google.cloud.spanner.Options.RpcOrderBy;
2930
import com.google.cloud.spanner.Options.RpcPriority;
3031
import com.google.spanner.v1.DirectedReadOptions;
3132
import com.google.spanner.v1.DirectedReadOptions.IncludeReplicas;
3233
import com.google.spanner.v1.DirectedReadOptions.ReplicaSelection;
34+
import com.google.spanner.v1.ReadRequest.LockHint;
3335
import com.google.spanner.v1.ReadRequest.OrderBy;
3436
import com.google.spanner.v1.RequestOptions.Priority;
3537
import org.junit.Test;
@@ -83,7 +85,8 @@ public void allOptionsPresent() {
8385
Options.prefetchChunks(1),
8486
Options.dataBoostEnabled(true),
8587
Options.directedRead(DIRECTED_READ_OPTIONS),
86-
Options.orderBy(RpcOrderBy.NO_ORDER));
88+
Options.orderBy(RpcOrderBy.NO_ORDER),
89+
Options.lockHint(Options.RpcLockHint.SHARED));
8790
assertThat(options.hasLimit()).isTrue();
8891
assertThat(options.limit()).isEqualTo(10);
8992
assertThat(options.hasPrefetchChunks()).isTrue();
@@ -92,6 +95,7 @@ public void allOptionsPresent() {
9295
assertTrue(options.dataBoostEnabled());
9396
assertTrue(options.hasDirectedReadOptions());
9497
assertTrue(options.hasOrderBy());
98+
assertTrue(options.hasLockHint());
9599
assertEquals(DIRECTED_READ_OPTIONS, options.directedReadOptions());
96100
}
97101

@@ -107,6 +111,7 @@ public void allOptionsAbsent() {
107111
assertThat(options.hasDataBoostEnabled()).isFalse();
108112
assertThat(options.hasDirectedReadOptions()).isFalse();
109113
assertThat(options.hasOrderBy()).isFalse();
114+
assertThat(options.hasLockHint()).isFalse();
110115
assertNull(options.withExcludeTxnFromChangeStreams());
111116
assertThat(options.toString()).isEqualTo("");
112117
assertThat(options.equals(options)).isTrue();
@@ -189,7 +194,8 @@ public void readOptionsTest() {
189194
Options.tag(tag),
190195
Options.dataBoostEnabled(true),
191196
Options.directedRead(DIRECTED_READ_OPTIONS),
192-
Options.orderBy(RpcOrderBy.NO_ORDER));
197+
Options.orderBy(RpcOrderBy.NO_ORDER),
198+
Options.lockHint(RpcLockHint.SHARED));
193199

194200
assertThat(options.toString())
195201
.isEqualTo(
@@ -207,11 +213,15 @@ public void readOptionsTest() {
207213
+ " "
208214
+ "orderBy: "
209215
+ RpcOrderBy.NO_ORDER
216+
+ " "
217+
+ "lockHint: "
218+
+ RpcLockHint.SHARED
210219
+ " ");
211220
assertThat(options.tag()).isEqualTo(tag);
212221
assertEquals(dataBoost, options.dataBoostEnabled());
213222
assertEquals(DIRECTED_READ_OPTIONS, options.directedReadOptions());
214223
assertEquals(OrderBy.ORDER_BY_NO_ORDER, options.orderBy());
224+
assertEquals(LockHint.LOCK_HINT_SHARED, options.lockHint());
215225
}
216226

217227
@Test
@@ -373,6 +383,14 @@ public void testReadOptionsOrderBy() {
373383
assertEquals("orderBy: " + orderBy + " ", options.toString());
374384
}
375385

386+
@Test
387+
public void testReadOptionsLockHint() {
388+
RpcLockHint lockHint = RpcLockHint.SHARED;
389+
Options options = Options.fromReadOptions(Options.lockHint(lockHint));
390+
assertTrue(options.hasLockHint());
391+
assertEquals("lockHint: " + lockHint + " ", options.toString());
392+
}
393+
376394
@Test
377395
public void testReadOptionsWithOrderByEquality() {
378396
Options optionsWithNoOrderBy1 = Options.fromReadOptions(Options.orderBy(RpcOrderBy.NO_ORDER));
@@ -383,6 +401,19 @@ public void testReadOptionsWithOrderByEquality() {
383401
assertFalse(optionsWithNoOrderBy1.equals(optionsWithPkOrder));
384402
}
385403

404+
@Test
405+
public void testReadOptionsWithLockHintEquality() {
406+
Options optionsWithSharedLockHint1 =
407+
Options.fromReadOptions(Options.lockHint(RpcLockHint.SHARED));
408+
Options optionsWithSharedLockHint2 =
409+
Options.fromReadOptions(Options.lockHint(RpcLockHint.SHARED));
410+
assertEquals(optionsWithSharedLockHint1, optionsWithSharedLockHint2);
411+
412+
Options optionsWithExclusiveLock =
413+
Options.fromReadOptions(Options.lockHint(RpcLockHint.EXCLUSIVE));
414+
assertNotEquals(optionsWithSharedLockHint1, optionsWithExclusiveLock);
415+
}
416+
386417
@Test
387418
public void testQueryOptionsPriority() {
388419
RpcPriority priority = RpcPriority.MEDIUM;

0 commit comments

Comments
 (0)