Skip to content

Commit a16cb88

Browse files
fix: handle recovery failures during stream reframing failure (#46)
* fix: handle recovery failures during stream reframing failure This was discovered while debugging another issue. While deflaking ReadRowRetryTest, this issue came up preventing me from seeing the underlying issue. ReframingResponseObserver#deliverUnsafe() should never fail. However if does, it will try to cancel the upstream stream and notify the downstream observer. However canceling the upstream can throw an exception and prevent the downstram observer from being notified of any error. This fix will catch cancellation errors and add them as suppressed exceptions to the original failure * add test * format
1 parent 4ca7e2f commit a16cb88

File tree

2 files changed

+65
-1
lines changed

2 files changed

+65
-1
lines changed

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/reframing/ReframingResponseObserver.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,14 @@ private void deliver() {
244244
// purposefully leaving the lock non-zero and notifying the outerResponseObserver of the
245245
// error. Care must be taken to avoid calling close twice in case the first invocation threw
246246
// an error.
247-
innerController.cancel();
247+
try {
248+
innerController.cancel();
249+
} catch (Throwable cancelError) {
250+
t.addSuppressed(
251+
new IllegalStateException(
252+
"Failed to cancel upstream while recovering from an unexpected error",
253+
cancelError));
254+
}
248255
if (!finished) {
249256
outerResponseObserver.onError(t);
250257
}

google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/gaxx/reframing/ReframingResponseObserverTest.java

+57
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.google.cloud.bigtable.gaxx.reframing;
1717

18+
import com.google.api.gax.rpc.StreamController;
1819
import com.google.cloud.bigtable.gaxx.testing.FakeStreamingApi.ServerStreamingStashCallable;
1920
import com.google.cloud.bigtable.gaxx.testing.FakeStreamingApi.ServerStreamingStashCallable.StreamControllerStash;
2021
import com.google.cloud.bigtable.gaxx.testing.MockStreamingApi.MockResponseObserver;
@@ -41,6 +42,7 @@
4142
import org.junit.Test;
4243
import org.junit.runner.RunWith;
4344
import org.junit.runners.JUnit4;
45+
import org.mockito.Mockito;
4446

4547
@RunWith(JUnit4.class)
4648
public class ReframingResponseObserverTest {
@@ -374,6 +376,61 @@ public String pop() {
374376
Truth.assertThat(lastCall.getNumDelivered()).isEqualTo(2);
375377
}
376378

379+
/**
380+
* Test the scenario where the reframer throws an exception on incoming data and the upstream
381+
* throws an exception during cleanup when cancel is called.
382+
*/
383+
@Test
384+
public void testFailedRecoveryHandling() {
385+
MockResponseObserver<String> outerObserver = new MockResponseObserver<>(true);
386+
final RuntimeException fakeReframerError = new RuntimeException("fake reframer error");
387+
388+
Reframer<String, String> brokenReframer =
389+
new Reframer<String, String>() {
390+
@Override
391+
public void push(String ignored) {
392+
throw fakeReframerError;
393+
}
394+
395+
@Override
396+
public boolean hasFullFrame() {
397+
return false;
398+
}
399+
400+
@Override
401+
public boolean hasPartialFrame() {
402+
return false;
403+
}
404+
405+
@Override
406+
public String pop() {
407+
throw new IllegalStateException("should not be called");
408+
}
409+
};
410+
ReframingResponseObserver<String, String> middleware =
411+
new ReframingResponseObserver<>(outerObserver, brokenReframer);
412+
413+
// Configure the mock inner controller to fail cancellation.
414+
StreamController mockInnerController = Mockito.mock(StreamController.class);
415+
RuntimeException fakeCancelError = new RuntimeException("fake cancel error");
416+
Mockito.doThrow(fakeCancelError).when(mockInnerController).cancel();
417+
418+
// Jumpstart a call & feed it data
419+
middleware.onStartImpl(mockInnerController);
420+
middleware.onResponseImpl("1");
421+
422+
// Make sure that the outer observer was notified with the reframer, which contains a suppressed
423+
// cancellation error.
424+
Throwable finalError = outerObserver.getFinalError();
425+
Truth.assertThat(finalError).isSameInstanceAs(fakeReframerError);
426+
Truth.assertThat(ImmutableList.of(finalError.getSuppressed())).hasSize(1);
427+
Truth.assertThat(finalError.getSuppressed()[0]).isInstanceOf(IllegalStateException.class);
428+
Truth.assertThat(finalError.getSuppressed()[0])
429+
.hasMessageThat()
430+
.isEqualTo("Failed to cancel upstream while recovering from an unexpected error");
431+
Truth.assertThat(finalError.getSuppressed()[0].getCause()).isSameInstanceAs(fakeCancelError);
432+
}
433+
377434
/**
378435
* A simple implementation of a {@link Reframer}. The input string is split by dash, and the
379436
* output is concatenated by dashes. The test can verify M:N behavior by adjusting the

0 commit comments

Comments
 (0)