Skip to content

Commit d105d52

Browse files
authored
feat: support transaction timeouts (GoogleCloudPlatform#3831)
PGAdapter now supports setting a transaction timeout for read/write transactions. Execute the following SQL statement, or set the option in the connection string, to enable transaction timeouts: ```sql -- Timeout in milliseconds set spanner.transaction_timeout=10000; begin; insert into my_table (id, value) values (1, 'One'); insert into my_table (id, value) values (2, 'Two'); commit; ```
1 parent 92f6ef2 commit d105d52

4 files changed

Lines changed: 70 additions & 2 deletions

File tree

docs/faq.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,22 @@ from large_table
175175
order by my_col;
176176
```
177177

178+
179+
## How can I set a transaction timeout?
180+
You can set the transaction timeout that a connection should use for read/write transactions by
181+
executing a `set spanner.transaction_timeout=<timeout>` statement. The timeout is specified in
182+
milliseconds.
183+
184+
Example:
185+
186+
```sql
187+
-- Use a 10 second (10,000 milliseconds) transaction timeout.
188+
set spanner.transaction_timeout=10000;
189+
begin;
190+
-- Execute transaction statements. If the total time needed for these statements exceed
191+
-- the transaction timeout, then the transaction will fail.
192+
insert into my_table (id, value) values (1, 'One');
193+
...
194+
insert into my_table (id, value) values (10000, 'Ten thousand');
195+
commit;
196+
```

src/main/java/com/google/cloud/spanner/pgadapter/error/PGExceptionFactory.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ public static PGException newQueryCancelledException() {
8585
return newPGException("Query cancelled", SQLState.QueryCanceled);
8686
}
8787

88+
private static PGException newQueryCancelledDueToTimeoutException(SpannerException exception) {
89+
return PGException.newBuilder("canceling statement due to statement timeout")
90+
.setSQLState(SQLState.QueryCanceled)
91+
.setSeverity(Severity.ERROR)
92+
.setCause(exception)
93+
.build();
94+
}
95+
8896
/**
8997
* Creates a new exception that indicates that the current transaction is in the aborted state.
9098
*/
@@ -145,6 +153,8 @@ public static PGException toPGException(SpannerException spannerException) {
145153
.setHints(
146154
"Execute 'set spanner.support_drop_cascade=true' to enable 'drop {table|schema} cascade' statements.")
147155
.build();
156+
} else if (spannerException.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED) {
157+
return newQueryCancelledDueToTimeoutException(spannerException);
148158
}
149159
return newPGException(extractMessage(spannerException));
150160
}

src/main/java/com/google/cloud/spanner/pgadapter/wireoutput/ErrorResponse.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class ErrorResponse extends WireOutput {
3737
private static final byte MESSAGE_FLAG = 'M';
3838
private static final byte SEVERITY_FLAG = 'S';
3939
private static final byte HINT_FLAG = 'H';
40+
private static final byte DETAIL_FLAG = 'D';
4041
private static final byte NULL_TERMINATOR = 0;
4142

4243
private final PGException pgException;
@@ -63,6 +64,10 @@ static int calculateLength(PGException pgException, WellKnownClient client) {
6364
+ convertMessageToWireProtocol(pgException).length
6465
+ NULL_TERMINATOR_LENGTH
6566
+ NULL_TERMINATOR_LENGTH;
67+
byte[] detail = convertDetailToWireProtocol(pgException);
68+
if (detail.length > 0) {
69+
length += FIELD_IDENTIFIER_LENGTH + detail.length + NULL_TERMINATOR_LENGTH;
70+
}
6671
byte[] hints = convertHintsToWireProtocol(pgException, client);
6772
if (hints.length > 0) {
6873
length += FIELD_IDENTIFIER_LENGTH + hints.length + NULL_TERMINATOR_LENGTH;
@@ -82,6 +87,19 @@ static byte[] convertMessageToWireProtocol(PGException pgException) {
8287
return pgException.getMessage().getBytes(StandardCharsets.UTF_8);
8388
}
8489

90+
static byte[] convertDetailToWireProtocol(PGException pgException) {
91+
if (pgException.getCause() != null
92+
&& !Strings.isNullOrEmpty(pgException.getCause().getMessage())) {
93+
if (pgException.getCause().getMessage().equals(pgException.getMessage())) {
94+
// Do not repeat the same error message in both the 'message' and 'detail' field if they
95+
// are equal.
96+
return EMPTY_BYTE_ARRAY;
97+
}
98+
return pgException.getCause().getMessage().getBytes(StandardCharsets.UTF_8);
99+
}
100+
return EMPTY_BYTE_ARRAY;
101+
}
102+
85103
static byte[] convertHintsToWireProtocol(PGException pgException, WellKnownClient client) {
86104
if (Strings.isNullOrEmpty(pgException.getHints())
87105
&& client.getErrorHints(pgException).isEmpty()) {
@@ -112,6 +130,12 @@ protected void sendPayload() throws IOException {
112130
this.outputStream.writeByte(MESSAGE_FLAG);
113131
this.outputStream.write(convertMessageToWireProtocol(pgException));
114132
this.outputStream.writeByte(NULL_TERMINATOR);
133+
byte[] detail = convertDetailToWireProtocol(pgException);
134+
if (detail.length > 0) {
135+
this.outputStream.writeByte(DETAIL_FLAG);
136+
this.outputStream.write(detail);
137+
this.outputStream.writeByte(NULL_TERMINATOR);
138+
}
115139
byte[] hints = convertHintsToWireProtocol(pgException, client);
116140
if (hints.length > 0) {
117141
this.outputStream.writeByte(HINT_FLAG);

src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6237,8 +6237,6 @@ public void testIsolationLevelForOneTransaction() throws SQLException {
62376237
ExecuteSqlRequest request = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
62386238
assertTrue(request.hasTransaction());
62396239
assertTrue(request.getTransaction().hasBegin());
6240-
// TODO: Change this when https://github.com/googleapis/java-spanner/pull/3718 has been
6241-
// released and merged into PGAdapter.
62426240
assertEquals(
62436241
translateIsolationLevel(isolation),
62446242
request.getTransaction().getBegin().getIsolationLevel());
@@ -6249,6 +6247,23 @@ public void testIsolationLevelForOneTransaction() throws SQLException {
62496247
}
62506248
}
62516249

6250+
@Test
6251+
public void testTransactionTimeout() throws SQLException {
6252+
try (Connection connection = DriverManager.getConnection(createUrl())) {
6253+
connection.setAutoCommit(false);
6254+
connection.createStatement().execute("set spanner.transaction_timeout='1ns'");
6255+
PSQLException exception =
6256+
assertThrows(
6257+
PSQLException.class,
6258+
() -> connection.createStatement().execute(UPDATE_STATEMENT.getSql()));
6259+
assertTrue(
6260+
exception.getMessage(),
6261+
exception.getMessage().startsWith("ERROR: canceling statement due to statement timeout"));
6262+
assertTrue(exception.getMessage(), exception.getMessage().contains("DEADLINE_EXCEEDED"));
6263+
assertEquals(SQLState.QueryCanceled.toString(), exception.getSQLState());
6264+
}
6265+
}
6266+
62526267
private static IsolationLevel translateIsolationLevel(int jdbcLevel) {
62536268
switch (jdbcLevel) {
62546269
case Connection.TRANSACTION_SERIALIZABLE:

0 commit comments

Comments
 (0)