Skip to content

Commit cbd7dc7

Browse files
authored
fix: honor read.partial.row.timeout.ms as the readRows watchdog timeout (#4629)
google.bigtable.grpc.read.partial.row.timeout.ms was handed to gax as the retry settings' rpcTimeout, but the read path always put a timeout on the ApiCallContext, and gax only applies rpcTimeout when the context carries none of its own (ServerStreamingAttemptCallable#call). The key was silently discarded and had no effect on any read. Wire it to readRowsSettings.setWaitTimeout() instead, which is the timeout it was named for: how long a stream may go without receiving a response before it is cancelled and retried, reset by every response. This lets a sparse filtered scan survive long stretches of non-matching rows, which is what prompted the change -- a large export hit the 5 minute default watchdog on ten consecutive attempts and failed the job. Applied raise-only: a value below the 5 minute client default is ignored. Because the key never reached the wire before, honoring a short value outright would newly cancel reads for anyone who happens to have one set. The attempt timeout moves off the ApiCallContext and onto the retry settings, where it no longer suppresses rpcTimeout. The gRPC deadline stays: on the scan path PaginatedRowResultScanner reuses a single context across segment fetches and Deadline.after() is absolute, so it bounds the scanner's whole lifetime rather than one ReadRows. Verified non-breaking by differential testing over 30 configurations. The deadline observed server-side is identical before and after in all of them except bigtable.read.rpc.attempt.timeout.ms=0, where the old code let the partial row timeout through as the attempt deadline and the new code sets none. That direction is permissive only, and the read stays bounded by the total timeout and the watchdog. Also exposes --bigtableReadPartialRowTimeoutMs on the SequenceFile export template. Change-Id: Ic39d1572503786b083f38b9b5dd2ddaa0e512508
1 parent 49d419a commit cbd7dc7

6 files changed

Lines changed: 188 additions & 36 deletions

File tree

bigtable-client-core-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/BigtableOptionsFactory.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,22 @@ public class BigtableOptionsFactory {
163163
public static final String MAX_ELAPSED_BACKOFF_MILLIS_KEY =
164164
"google.bigtable.grpc.retry.max.elapsed.backoff.ms";
165165

166-
/** Key to set the amount of time to wait when reading a partial row. */
166+
/**
167+
* Key to set how long a read may go without receiving a response before the stream is cancelled
168+
* and retried. This is the gap between consecutive responses, reset every time the server sends
169+
* something; it is not a deadline for the attempt as a whole. Raise it for scans that can
170+
* legitimately go a long time without producing a row, such as a filtered scan over a large
171+
* table. Defaults to 5 minutes.
172+
*
173+
* <p>An attempt is separately bounded by {@link #BIGTABLE_READ_RPC_ATTEMPT_TIMEOUT_MS_KEY}, 10
174+
* minutes by default, so raising this beyond 10 minutes accomplishes nothing on its own — the
175+
* attempt deadline would fire first. Raise that key too if you need a longer gap than that.
176+
*
177+
* <p>This can only raise the watchdog, never lower it: values below the 5 minute default are
178+
* ignored. The key used to be handed to gax as the rpc timeout, where gax discarded it because
179+
* the read path always put a timeout on the call context, so it never had any effect. Clamping it
180+
* to the default keeps a previously ignored short value from suddenly cancelling reads.
181+
*/
167182
public static final String READ_PARTIAL_ROW_TIMEOUT_MS =
168183
"google.bigtable.grpc.read.partial.row.timeout.ms";
169184

bigtable-client-core-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/wrappers/veneer/BigtableHBaseVeneerSettings.java

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -338,10 +338,8 @@ private BigtableDataSettings buildBigtableDataSettings(ClientOperationTimeouts c
338338
// Configure metrics
339339
configureMetricsBridge(dataBuilder);
340340

341-
// Configure RPCs - this happens in two parts:
342-
// - most of the timeouts are defined here
343-
// - attempt timeouts for readRows is set in DataClientVeneerApi to workaround lack of attempt
344-
// timeouts for streaming RPCs
341+
// Configure RPCs. All of the timeouts are defined here; DataClientVeneerApi only sets the
342+
// gRPC deadline for the overall operation.
345343
// Complex RPC method settings
346344
configureBulkMutationSettings(
347345
dataBuilder.stubSettings().bulkMutateRowsSettings(),
@@ -748,8 +746,9 @@ private void configureReadRowsSettings(
748746
OperationTimeouts operationTimeouts) {
749747

750748
// Configure retries
751-
// NOTE: that similar but not the same as unary retry settings: per attempt timeouts don't
752-
// exist, instead we use READ_PARTIAL_ROW_TIMEOUT_MS as the intra-row timeout
749+
// NOTE: similar but not the same as unary retry settings: responseTimeout is the watchdog
750+
// wait timeout, separate from the per attempt timeout, and the attempt deadline goes on the
751+
// retry settings rather than the ApiCallContext. See below.
753752
if (!configuration.getBoolean(ENABLE_GRPC_RETRIES_KEY, true)) {
754753
// user explicitly disabled retries, treat it as a non-idempotent method
755754
readRowsSettings.setRetryableCodes(Collections.emptySet());
@@ -780,16 +779,32 @@ private void configureReadRowsSettings(
780779
configuration.getInt(MAX_SCAN_TIMEOUT_RETRIES, MAX_CONSECUTIVE_SCAN_ATTEMPTS));
781780
}
782781

783-
// Per response timeouts (note: gax maps rpcTimeouts to response timeouts for streaming rpcs)
782+
// The watchdog wait timeout: how long the stream may go without receiving a response before
783+
// it is cancelled and retried. Reset by every response, so it bounds the gap between them
784+
// rather than the attempt as a whole.
785+
//
786+
// This used to be handed to gax as the rpcTimeout, where it was silently discarded because
787+
// the ApiCallContext already carried a timeout, so the key never had any effect. Honoring it
788+
// outright would newly cancel reads for anyone who had set a short value, so it is only
789+
// allowed to raise the watchdog, never lower it. Values below the client default are ignored.
784790
if (operationTimeouts.getResponseTimeout().isPresent()) {
791+
Duration defaultWaitTimeout = readRowsSettings.getWaitTimeout();
792+
Duration responseTimeout = operationTimeouts.getResponseTimeout().get();
793+
if (defaultWaitTimeout == null || responseTimeout.compareTo(defaultWaitTimeout) > 0) {
794+
readRowsSettings.setWaitTimeout(responseTimeout);
795+
}
796+
}
797+
798+
// Attempt timeouts. gax applies rpcTimeout as the deadline for a single attempt, but only if
799+
// the ApiCallContext doesn't already carry a timeout of its own (see
800+
// ServerStreamingAttemptCallable#call), so DataClientVeneerApi deliberately leaves it unset.
801+
if (operationTimeouts.getAttemptTimeout().isPresent()) {
785802
readRowsSettings
786803
.retrySettings()
787-
.setInitialRpcTimeout(operationTimeouts.getResponseTimeout().get())
788-
.setMaxRpcTimeout(operationTimeouts.getResponseTimeout().get());
804+
.setInitialRpcTimeout(operationTimeouts.getAttemptTimeout().get())
805+
.setMaxRpcTimeout(operationTimeouts.getAttemptTimeout().get());
789806
}
790807

791-
// Attempt timeouts are set in DataClientVeneerApi
792-
793808
// overall timeout
794809
if (operationTimeouts.getOperationTimeout().isPresent()) {
795810
readRowsSettings
@@ -1004,9 +1019,9 @@ static class OperationTimeouts {
10041019
new OperationTimeouts(
10051020
Optional.<Duration>absent(), Optional.<Duration>absent(), Optional.<Duration>absent());
10061021

1007-
// responseTimeouts are only relevant to streaming RPCs, they limit the amount of timeout a
1008-
// stream will wait for the next response message. This is synonymous with attemptTimeouts in
1009-
// unary RPCs since they receive a single response (so its ignored).
1022+
// responseTimeouts are only relevant to streaming RPCs, they limit how long a stream will
1023+
// wait for the next response message. Unary RPCs receive a single response, so it's ignored
1024+
// there.
10101025
private final Optional<Duration> responseTimeout;
10111026
private final Optional<Duration> attemptTimeout;
10121027
private final Optional<Duration> operationTimeout;

bigtable-client-core-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/wrappers/veneer/DataClientVeneerApi.java

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import com.google.cloud.bigtable.data.v2.models.Row;
3434
import com.google.cloud.bigtable.data.v2.models.RowMutation;
3535
import com.google.cloud.bigtable.hbase.adapters.Adapters;
36-
import com.google.cloud.bigtable.hbase.util.Logger;
3736
import com.google.cloud.bigtable.hbase.wrappers.BulkMutationWrapper;
3837
import com.google.cloud.bigtable.hbase.wrappers.BulkReadWrapper;
3938
import com.google.cloud.bigtable.hbase.wrappers.DataClientWrapper;
@@ -60,14 +59,11 @@
6059
import org.apache.hadoop.hbase.client.AbstractClientScanner;
6160
import org.apache.hadoop.hbase.client.Result;
6261
import org.apache.hadoop.hbase.client.ResultScanner;
63-
import org.threeten.bp.Duration;
6462

6563
/** For internal use only - public for technical reasons. */
6664
@InternalApi("For internal usage only")
6765
public class DataClientVeneerApi implements DataClientWrapper {
6866

69-
private final Logger LOG = new Logger(DataClientVeneerApi.class);
70-
7167
private static final RowResultAdapter RESULT_ADAPTER = new RowResultAdapter();
7268

7369
private final BigtableDataClient delegate;
@@ -174,12 +170,16 @@ private ApiCallContext createReadRowCallContext() {
174170
GrpcCallContext ctx = GrpcCallContext.createDefault();
175171
OperationTimeouts callSettings = clientOperationTimeouts.getUnaryTimeouts();
176172

177-
if (callSettings.getAttemptTimeout().isPresent()) {
178-
ctx = ctx.withTimeout(callSettings.getAttemptTimeout().get());
179-
}
180-
// TODO: remove this after fixing it in veneer/gax
181-
// If the attempt timeout was overridden, it disables overall timeout limiting
182-
// Fix it by settings the underlying grpc deadline
173+
// NOTE: the attempt timeout is deliberately not set here. gax applies the retry settings'
174+
// rpcTimeout as the attempt deadline, but only when the context has no timeout of its own,
175+
// so setting one here would suppress it. See BigtableHBaseVeneerSettings, which puts the
176+
// attempt timeout on the retry settings instead.
177+
178+
// Kept for backward compatibility: this context is built fresh per call, so the grpc deadline
179+
// below bounds a single readRow. gax now clamps each attempt's rpcTimeout to the time left on
180+
// the total timeout (ExponentialRetryAlgorithm#createNextAttempt), and readRowSettings already
181+
// carries that total timeout, so this deadline is very likely redundant. Dropping it isn't
182+
// provably behavior neutral though, so it stays.
183183
if (callSettings.getOperationTimeout().isPresent()) {
184184
ctx =
185185
ctx.withCallOptions(
@@ -191,10 +191,17 @@ private ApiCallContext createReadRowCallContext() {
191191
return ctx;
192192
}
193193

194-
// Support 2 bigtable-hbase features not directly available in veneer:
195-
// - per attempt deadlines - vener doesn't implement deadlines for attempts. To workaround this,
196-
// the timeouts are set per call in the ApiCallContext. However this creates a separate issue of
197-
// over running the operation deadline, so gRPC deadline is also set.
194+
// Kept for backward compatibility: veneer has no operation deadline for streaming RPCs, so the
195+
// grpc deadline below stands in for one. Most callers build a context per call, but
196+
// PaginatedRowResultScanner holds onto the one it is handed and passes it to every segment
197+
// fetch, and Deadline.after() is absolute, so on that path the deadline bounds the scanner's
198+
// whole lifetime rather than a single ReadRows. Removing it in favor of gax's per operation
199+
// total timeout would hand each segment its own fresh budget, which is a real behavior change.
200+
//
201+
// The attempt deadline is deliberately *not* set here. gax applies the retry settings'
202+
// rpcTimeout as the attempt deadline, but only when the context carries no timeout of its own
203+
// (ServerStreamingAttemptCallable#call), so setting one here would suppress it. The attempt
204+
// timeout goes on the retry settings in BigtableHBaseVeneerSettings instead.
198205
private GrpcCallContext createScanCallContext() {
199206
GrpcCallContext ctx = GrpcCallContext.createDefault();
200207
OperationTimeouts callSettings = clientOperationTimeouts.getScanTimeouts();
@@ -206,11 +213,6 @@ private GrpcCallContext createScanCallContext() {
206213
Deadline.after(
207214
callSettings.getOperationTimeout().get().toMillis(), TimeUnit.MILLISECONDS)));
208215
}
209-
if (callSettings.getAttemptTimeout().isPresent()) {
210-
Duration attemptTimeout = callSettings.getAttemptTimeout().get();
211-
LOG.info("effective attempt timeout for scan is %s", attemptTimeout);
212-
ctx = ctx.withTimeout(attemptTimeout);
213-
}
214216

215217
return ctx;
216218
}

bigtable-client-core-parent/bigtable-hbase/src/test/java/com/google/cloud/bigtable/hbase/wrappers/veneer/TestBigtableHBaseVeneerSettings.java

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,13 @@
5555
import com.google.api.gax.core.NoCredentialsProvider;
5656
import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
5757
import com.google.api.gax.retrying.RetrySettings;
58+
import com.google.api.gax.rpc.ServerStreamingCallSettings;
5859
import com.google.api.gax.rpc.UnaryCallSettings;
5960
import com.google.auth.Credentials;
6061
import com.google.cloud.bigtable.admin.v2.BigtableTableAdminSettings;
6162
import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
63+
import com.google.cloud.bigtable.data.v2.models.Query;
64+
import com.google.cloud.bigtable.data.v2.models.Row;
6265
import com.google.cloud.bigtable.hbase.BigtableConfiguration;
6366
import com.google.cloud.bigtable.hbase.BigtableHBaseVersion;
6467
import com.google.cloud.bigtable.hbase.BigtableOptionsFactory;
@@ -294,10 +297,15 @@ public void testTimeoutBeingPassed() throws IOException {
294297
RetrySettings readRowsRetrySettings =
295298
dataSettings.getStubSettings().readRowsSettings().getRetrySettings();
296299
assertEquals(initialElapsedMs, readRowsRetrySettings.getInitialRetryDelay().toMillis());
297-
assertEquals(perRowTimeoutMs, readRowsRetrySettings.getInitialRpcTimeout().toMillis());
298-
assertEquals(perRowTimeoutMs, readRowsRetrySettings.getMaxRpcTimeout().toMillis());
300+
assertEquals(
301+
readRowStreamAttemptTimeout, readRowsRetrySettings.getInitialRpcTimeout().toMillis());
302+
assertEquals(readRowStreamAttemptTimeout, readRowsRetrySettings.getMaxRpcTimeout().toMillis());
299303
assertEquals(maxAttempt, readRowsRetrySettings.getMaxAttempts());
300304
assertEquals(readRowStreamTimeout, readRowsRetrySettings.getTotalTimeout().toMillis());
305+
// The per row timeout is the watchdog wait timeout, not an attempt deadline. 1001ms is below
306+
// the 5 minute client default, and the key can only raise the watchdog, so it is ignored.
307+
assertEquals(
308+
Duration.ofMinutes(5), dataSettings.getStubSettings().readRowsSettings().getWaitTimeout());
301309

302310
RetrySettings sampleRowKeysRetrySettings =
303311
dataSettings.getStubSettings().sampleRowKeysSettings().getRetrySettings();
@@ -307,6 +315,100 @@ public void testTimeoutBeingPassed() throws IOException {
307315
assertEquals(rpcAttemptTimeoutMs, sampleRowKeysRetrySettings.getMaxRpcTimeout().toMillis());
308316
}
309317

318+
@Test
319+
public void testReadRowsWaitTimeout() throws IOException {
320+
BigtableDataSettings defaultSettings =
321+
((BigtableHBaseVeneerSettings) BigtableHBaseVeneerSettings.create(configuration))
322+
.getDataSettings();
323+
324+
// READ_PARTIAL_ROW_TIMEOUT_MS is the watchdog wait timeout: what cancels a sparse scan that
325+
// goes 5 minutes without a row. Its default matches the veneer default, so wiring it through
326+
// does not change the out of the box behavior. See also
327+
// testPartialRowTimeoutBelowTheDefaultIsIgnored.
328+
assertEquals(
329+
Duration.ofMinutes(5),
330+
defaultSettings.getStubSettings().readRowsSettings().getWaitTimeout());
331+
332+
configuration.set(READ_PARTIAL_ROW_TIMEOUT_MS, "540000");
333+
BigtableDataSettings dataSettings =
334+
((BigtableHBaseVeneerSettings) BigtableHBaseVeneerSettings.create(configuration))
335+
.getDataSettings();
336+
337+
assertEquals(
338+
Duration.ofMinutes(9), dataSettings.getStubSettings().readRowsSettings().getWaitTimeout());
339+
// The wait timeout is independent of the idle timeout and of the attempt deadline.
340+
assertEquals(
341+
defaultSettings.getStubSettings().readRowsSettings().getIdleTimeout(),
342+
dataSettings.getStubSettings().readRowsSettings().getIdleTimeout());
343+
assertEquals(
344+
defaultSettings.getStubSettings().readRowsSettings().getRetrySettings().toString(),
345+
dataSettings.getStubSettings().readRowsSettings().getRetrySettings().toString());
346+
}
347+
348+
@Test
349+
public void testReadRowsAttemptTimeoutIsOnTheRetrySettings() throws IOException {
350+
// The attempt deadline has to live on the retry settings rather than the ApiCallContext: gax
351+
// only applies rpcTimeout when the context carries no timeout of its own. See
352+
// TestReadRowsTimeoutSemantics.
353+
BigtableDataSettings defaultSettings =
354+
((BigtableHBaseVeneerSettings) BigtableHBaseVeneerSettings.create(configuration))
355+
.getDataSettings();
356+
assertEquals(
357+
Duration.ofMinutes(10),
358+
defaultSettings.getStubSettings().readRowsSettings().getRetrySettings().getMaxRpcTimeout());
359+
360+
configuration.set(BIGTABLE_READ_RPC_ATTEMPT_TIMEOUT_MS_KEY, "900000");
361+
BigtableHBaseVeneerSettings settings =
362+
(BigtableHBaseVeneerSettings) BigtableHBaseVeneerSettings.create(configuration);
363+
364+
ServerStreamingCallSettings<Query, Row> readRows =
365+
settings.getDataSettings().getStubSettings().readRowsSettings();
366+
assertEquals(Duration.ofMinutes(15), readRows.getRetrySettings().getInitialRpcTimeout());
367+
assertEquals(Duration.ofMinutes(15), readRows.getRetrySettings().getMaxRpcTimeout());
368+
assertEquals(
369+
Optional.of(Duration.ofMinutes(15)),
370+
settings.getClientTimeouts().getScanTimeouts().getAttemptTimeout());
371+
}
372+
373+
@Test
374+
public void testReadRowsWaitTimeoutBeyondTheAttemptTimeoutNeedsBothRaised() throws IOException {
375+
// A 15 minute wait timeout can never fire against the default 10 minute attempt deadline.
376+
configuration.set(READ_PARTIAL_ROW_TIMEOUT_MS, "900000");
377+
BigtableHBaseVeneerSettings settings =
378+
(BigtableHBaseVeneerSettings) BigtableHBaseVeneerSettings.create(configuration);
379+
380+
ServerStreamingCallSettings<Query, Row> readRows =
381+
settings.getDataSettings().getStubSettings().readRowsSettings();
382+
assertEquals(Duration.ofMinutes(15), readRows.getWaitTimeout());
383+
assertEquals(Duration.ofMinutes(10), readRows.getRetrySettings().getMaxRpcTimeout());
384+
385+
configuration.set(BIGTABLE_READ_RPC_ATTEMPT_TIMEOUT_MS_KEY, "900000");
386+
readRows =
387+
((BigtableHBaseVeneerSettings) BigtableHBaseVeneerSettings.create(configuration))
388+
.getDataSettings()
389+
.getStubSettings()
390+
.readRowsSettings();
391+
assertEquals(Duration.ofMinutes(15), readRows.getWaitTimeout());
392+
assertEquals(Duration.ofMinutes(15), readRows.getRetrySettings().getMaxRpcTimeout());
393+
}
394+
395+
@Test
396+
public void testPartialRowTimeoutBelowTheDefaultIsIgnored() throws IOException {
397+
// The key is raise only. It never reached the wire before (gax discarded it because the call
398+
// context carried a timeout), so honoring a short value now would newly cancel reads that
399+
// used to survive. It is still parsed into the client timeouts, just not applied.
400+
configuration.set(READ_PARTIAL_ROW_TIMEOUT_MS, "1000");
401+
BigtableHBaseVeneerSettings settings =
402+
(BigtableHBaseVeneerSettings) BigtableHBaseVeneerSettings.create(configuration);
403+
404+
assertEquals(
405+
Optional.of(Duration.ofMillis(1000)),
406+
settings.getClientTimeouts().getScanTimeouts().getResponseTimeout());
407+
assertEquals(
408+
Duration.ofMinutes(5),
409+
settings.getDataSettings().getStubSettings().readRowsSettings().getWaitTimeout());
410+
}
411+
310412
@Test
311413
public void testWhenRetriesAreDisabled() throws IOException {
312414
configuration.setBoolean(ENABLE_GRPC_RETRIES_KEY, false);

bigtable-dataflow-parent/bigtable-beam-import/src/main/java/com/google/cloud/bigtable/beam/TemplateUtils.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ public static CloudBigtableScanConfiguration buildExportConfig(ExportOptions opt
117117
BigtableOptionsFactory.BIGTABLE_READ_RPC_ATTEMPT_TIMEOUT_MS_KEY,
118118
options.getBigtableReadRpcAttemptTimeoutMs());
119119
}
120+
if (options.getBigtableReadPartialRowTimeoutMs() != null) {
121+
configBuilder.withConfiguration(
122+
BigtableOptionsFactory.READ_PARTIAL_ROW_TIMEOUT_MS,
123+
options.getBigtableReadPartialRowTimeoutMs());
124+
}
120125
if (options.getBigtableMaxAttempts() != null) {
121126
configBuilder.withConfiguration(
122127
BigtableOptionsFactory.MAX_SCAN_TIMEOUT_RETRIES, options.getBigtableMaxAttempts());

bigtable-dataflow-parent/bigtable-beam-import/src/main/java/com/google/cloud/bigtable/beam/sequencefiles/ExportJob.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,19 @@ public interface ExportOptions extends GcpOptions, GcsOptions {
197197

198198
@SuppressWarnings("unused")
199199
void setBigtableMaxAttempts(ValueProvider<String> maxAttempts);
200+
201+
@Description(
202+
"How long a scan may go without receiving a response, in milliseconds, before it is "
203+
+ "cancelled and retried. This is the gap between consecutive responses, not a "
204+
+ "deadline for the attempt, so raise it for a filtered scan that can traverse a lot "
205+
+ "of non-matching rows between results. Defaults to 300000 (5 minutes); lower "
206+
+ "values are ignored. A single attempt is separately capped by "
207+
+ "--bigtableReadRpcAttemptTimeoutMs (10 minutes by default), so raise that as well "
208+
+ "if you need a gap longer than that.")
209+
ValueProvider<String> getBigtableReadPartialRowTimeoutMs();
210+
211+
@SuppressWarnings("unused")
212+
void setBigtableReadPartialRowTimeoutMs(ValueProvider<String> partialRowTimeoutMs);
200213
}
201214

202215
public static void main(String[] args) {

0 commit comments

Comments
 (0)