Skip to content

Commit b10177d

Browse files
committed
Add exponential backoff retry for datastore transaction api. Update related doc.
1 parent f08c779 commit b10177d

File tree

2 files changed

+78
-22
lines changed

2 files changed

+78
-22
lines changed

api/src/main/java/com/google/appengine/api/datastore/DatastoreServiceImpl.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.appengine.api.datastore.FutureHelper.quietGet;
2020

21+
import com.google.apphosting.api.ApiProxy;
2122
import java.util.Collection;
2223
import java.util.List;
2324
import java.util.Map;
@@ -30,6 +31,8 @@
3031
final class DatastoreServiceImpl implements DatastoreService {
3132

3233
private final AsyncDatastoreServiceInternal async;
34+
static final long BEGIN_TXN_RETRY_DELAY_MS = 100;
35+
private static final int MAX_RETRIES = Integer.getInteger("appengine.datastore.retries", 1);
3336

3437
public DatastoreServiceImpl(AsyncDatastoreServiceInternal async) {
3538
this.async = async;
@@ -137,12 +140,33 @@ public KeyRangeState allocateIdRange(KeyRange range) {
137140

138141
@Override
139142
public Transaction beginTransaction() {
140-
return quietGet(async.beginTransaction());
143+
return beginTransaction(TransactionOptions.Builder.withDefaults());
141144
}
142145

143146
@Override
144147
public Transaction beginTransaction(TransactionOptions options) {
145-
return quietGet(async.beginTransaction(options));
148+
int retries = 0;
149+
long delay = BEGIN_TXN_RETRY_DELAY_MS;
150+
while (true) {
151+
try {
152+
Transaction tx = quietGet(async.beginTransaction(options));
153+
tx.getId(); // Force handle resolution
154+
return tx;
155+
} catch (DatastoreFailureException
156+
| DatastoreTimeoutException
157+
| ApiProxy.RPCFailedException e) {
158+
if (++retries > MAX_RETRIES) {
159+
throw e;
160+
}
161+
try {
162+
Thread.sleep(delay);
163+
} catch (InterruptedException ie) {
164+
Thread.currentThread().interrupt();
165+
throw e;
166+
}
167+
delay *= 2;
168+
}
169+
}
146170
}
147171

148172
@Override

runtime/runtime_impl_jetty121/API_CLIENTS.md

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,17 @@ requests are queued until a connection becomes available.
2424

2525
### Configuration
2626

27-
You can configure the Jetty client using the following environment variables:
27+
You can configure the Jetty client using the following environment variables
28+
and system properties:
2829

29-
* **`APPENGINE_API_MAX_CONNECTIONS`**: Sets the maximum number of concurrent
30-
connections for API calls. If you observe API call latency or
31-
connection-related errors under high load, you might consider adjusting this
32-
value.
30+
* **`APPENGINE_API_MAX_CONNECTIONS`** (Environment Variable): Sets the
31+
maximum number of concurrent connections for API calls. If you observe API
32+
call latency or connection-related errors under high load, you might
33+
consider adjusting this value.
3334
* Default: `100`
34-
* **`APPENGINE_API_CALLS_IDLE_TIMEOUT_MS`**: Sets the idle timeout in
35-
milliseconds for connections in the connection pool. Connections that are
36-
idle for longer than this duration may be closed.
35+
* **`APPENGINE_API_CALLS_IDLE_TIMEOUT_MS`** (Environment Variable): Sets
36+
the idle timeout in milliseconds for connections in the connection pool.
37+
Connections that are idle for longer than this duration may be closed.
3738
* Default: `58000` (58 seconds)
3839

3940
## JDK HTTP Client
@@ -50,7 +51,8 @@ To use the JDK client instead of the Jetty client, set the
5051

5152
### Configuration
5253

53-
When using the JDK client, you can configure its threading behavior:
54+
When using the JDK client, you can configure its behavior with the following
55+
settings:
5456

5557
* **`appengine.api.use.virtualthreads`** (Java System Property): If set to `true`,
5658
the JDK client will use Java Virtual Threads (when available on the JVM) to
@@ -63,28 +65,58 @@ If `appengine.api.use.virtualthreads` is `false` or not set, the JDK client uses
6365
traditional thread pool model. In this mode, you can control the thread pool
6466
size using an environment variable:
6567

66-
* **`APPENGINE_API_MAX_CONNECTIONS`**: Sets the maximum number of threads in
67-
the pool for handling API calls. This limits the number of concurrent API
68-
calls, similar to how it works for the Jetty client. This variable is
69-
**ignored** if `appengine.api.use.virtualthreads` is set to `true`.
68+
* **`APPENGINE_API_MAX_CONNECTIONS`** (Environment Variable): Sets the
69+
maximum number of threads in the pool for handling API calls. This limits
70+
the number of concurrent API calls, similar to how it works for the Jetty
71+
client. This variable is **ignored** if `appengine.api.use.virtualthreads`
72+
is set to `true`.
7073
* Default: `100`
7174

75+
## Datastore-Specific Configuration
7276

77+
### `beginTransaction` Retries
7378

74-
### Example
79+
In addition to the HTTP client settings, the Datastore client library includes
80+
specific retry logic for `beginTransaction()` calls. These calls can fail with
81+
transient errors such as `DatastoreFailureException`, `DatastoreTimeoutException`,
82+
or `ApiProxy.RPCFailedException`, especially under high contention when
83+
multiple transactions attempt to access the same entity group simultaneously.
7584

76-
To change the API call path to use the JDK client instead of the Jetty client,
77-
and to enable virtual threads for this client, add the following environment
78-
variable and system property to `appengine-web.xml` and redeploy your
79-
application:
85+
To handle this, `DatastoreService.beginTransaction()` automatically retries
86+
failed attempts with exponential backoff, starting at 100ms. You can
87+
configure the number of retry attempts using a system property:
88+
89+
* **`appengine.datastore.retries`** (Java System Property): The maximum
90+
number of times to retry a `beginTransaction` call if it fails with
91+
`DatastoreFailureException`, `DatastoreTimeoutException`, or
92+
`ApiProxy.RPCFailedException`. This retry logic applies only to
93+
`beginTransaction` calls; other Datastore operations are not retried
94+
by this mechanism.
95+
* Default: `1`
96+
97+
98+
```xml
99+
<system-properties>
100+
<property name="appengine.datastore.retries" value="3" />
101+
</system-properties>
102+
```
103+
104+
## Configuring via `appengine-web.xml`
105+
106+
You can set these options by adding `<env-variables>` and
107+
`<system-properties>` sections to your `appengine-web.xml` file.
108+
109+
For example, to switch to the JDK client, enable virtual threads, increase
110+
the connection limit to 200, and set Datastore `beginTransaction` retries to 3,
111+
you would add:
80112

81113
```xml
82114
<env-variables>
83115
<env-var name="APPENGINE_API_CALLS_USING_JDK_CLIENT" value="true" />
84-
<env-var name="APPENGINE_API_MAX_CONNECTIONS" value="100" />
85-
<env-var name="APPENGINE_API_CALLS_IDLE_TIMEOUT_MS" value="58" />
116+
<env-var name="APPENGINE_API_MAX_CONNECTIONS" value="200" />
86117
</env-variables>
87118
<system-properties>
88119
<property name="appengine.api.use.virtualthreads" value="true" />
120+
<property name="appengine.datastore.retries" value="3" />
89121
</system-properties>
90122
```

0 commit comments

Comments
 (0)