diff --git a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java index b992f4236a809f36e95dcf6811d6b80ad0675046..6083de9508e852ea2abe0d286a91b4d7ca245a68 100644 --- a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java +++ b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java @@ -23,22 +23,26 @@ */ package de.ozgcloud.common.binaryfile; -import com.google.protobuf.ByteString; -import de.ozgcloud.common.errorhandling.TechnicalException; -import io.grpc.stub.CallStreamObserver; -import lombok.Builder; -import lombok.extern.log4j.Log4j2; -import org.apache.commons.io.IOUtils; -import org.springframework.core.task.TaskExecutor; - import java.io.IOException; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; +import org.apache.commons.io.IOUtils; +import org.springframework.core.task.TaskExecutor; + +import com.google.protobuf.ByteString; + +import de.ozgcloud.common.errorhandling.TechnicalException; +import io.grpc.stub.CallStreamObserver; +import lombok.Builder; +import lombok.extern.log4j.Log4j2; + @Log4j2 public class GrpcBinaryFileServerDownloader<T> { @@ -49,10 +53,11 @@ public class GrpcBinaryFileServerDownloader<T> { private final Consumer<OutputStream> downloadConsumer; private final TaskExecutor taskExecutor; - private final byte[] buffer = new byte[GrpcBinaryFileServerDownloader.CHUNK_SIZE]; + private final byte[] buffer = new byte[CHUNK_SIZE]; private final AtomicBoolean started = new AtomicBoolean(false); private final AtomicBoolean downloadFinished = new AtomicBoolean(false); private final AtomicBoolean requestFinished = new AtomicBoolean(false); + private final AtomicReference<TechnicalException> downloadError = new AtomicReference<>(); private PipedInputStream inputStream; private PipedOutputStream outputStream; @@ -78,19 +83,36 @@ public class GrpcBinaryFileServerDownloader<T> { void doStart() { LOG.debug("Starting download."); - handleSafety(this::setupStreams); + safelySetupStreams(); taskExecutor.execute(this::startDownload); callObserver.setOnReadyHandler(this::sendChunks); } + void safelySetupStreams() { + try { + setupStreams(); + } catch (Exception e) { + closeOutputStream(); + closeInputStream(); + throw new TechnicalException("Error while setting up streams", e); + } + } + void setupStreams() throws IOException { outputStream = new PipedOutputStream(); - inputStream = new PipedInputStream(GrpcBinaryFileServerDownloader.CHUNK_SIZE); + inputStream = new PipedInputStream(CHUNK_SIZE); outputStream.connect(inputStream); } void startDownload() { - handleSafety(this::doDownload); + try { + doDownload(); + sendChunks(); + } catch (Exception e) { + downloadError.set(new TechnicalException("Error while downloading file contents", e)); + } finally { + closeOutputStream(); + } } void doDownload() { @@ -98,12 +120,14 @@ public class GrpcBinaryFileServerDownloader<T> { downloadConsumer.accept(outputStream); LOG.debug("Download completed."); downloadFinished.set(true); - closeOutputStream(); - sendChunks(); } synchronized void sendChunks() { - handleSafety(this::doSendChunks); + try { + doSendChunks(); + } catch (Exception e) { + completeRequestWithError(new TechnicalException("Error while sending chunks", e)); + } } void doSendChunks() throws IOException { @@ -111,7 +135,7 @@ public class GrpcBinaryFileServerDownloader<T> { return; } int bytesRead; - while (callObserver.isReady()) { + while (isReady()) { if ((bytesRead = inputStream.read(buffer)) == -1) { tryCompleteRequest(); break; @@ -121,30 +145,33 @@ public class GrpcBinaryFileServerDownloader<T> { } } + private boolean isReady() { + return callObserver.isReady(); + } + void tryCompleteRequest() { - if (shouldCompleteRequest()) { - completeRequest(); + if (Objects.nonNull(downloadError.get())) { + throw downloadError.get(); + } else if (downloadFinished.get()) { + completeRequestNormally(); } } - boolean shouldCompleteRequest() { - return downloadFinished.get() && requestFinished.compareAndSet(false, true); + void completeRequestWithError(TechnicalException e) { + LOG.debug("Complete download request with error"); + finishRequest(); + throw e; } - void completeRequest() { + void completeRequestNormally() { LOG.debug("Complete download request"); - closeInputStream(); + finishRequest(); callObserver.onCompleted(); } - void handleSafety(ExceptionalRunnable runnable) { - try { - runnable.run(); - } catch (Exception e) { - closeOutputStream(); - closeInputStream(); - throw new TechnicalException("Error occurred during downloading file content download.", e); - } + private void finishRequest() { + requestFinished.set(true); + closeInputStream(); } void closeOutputStream() { @@ -154,5 +181,4 @@ public class GrpcBinaryFileServerDownloader<T> { void closeInputStream() { IOUtils.closeQuietly(inputStream, e -> LOG.error("InputStream cannot be closed.", e)); } - } \ No newline at end of file diff --git a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java index 6050e6ba2d06a739e87ec479956dff4fecd707b8..8c3ad5a56eb8a52a3e8b079673406efd68b38420 100644 --- a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java +++ b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java @@ -23,20 +23,9 @@ */ package de.ozgcloud.common.binaryfile; -import com.google.protobuf.ByteString; -import de.ozgcloud.common.errorhandling.TechnicalException; -import de.ozgcloud.common.test.ReflectionTestUtils; -import io.grpc.Context; -import io.grpc.stub.CallStreamObserver; -import lombok.SneakyThrows; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.springframework.core.task.TaskExecutor; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; import java.io.IOException; import java.io.InputStream; @@ -45,12 +34,25 @@ import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.util.Random; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.springframework.core.task.TaskExecutor; + +import com.google.protobuf.ByteString; + +import de.ozgcloud.common.errorhandling.TechnicalException; +import de.ozgcloud.common.test.ReflectionTestUtils; +import io.grpc.stub.CallStreamObserver; +import lombok.SneakyThrows; class GrpcBinaryFileServerDownloaderTest { @@ -71,7 +73,6 @@ class GrpcBinaryFileServerDownloaderTest { .chunkBuilder(chunkBuilder).taskExecutor(taskExecutor).build()); } - @DisplayName("Start") @Nested class TestStart { @@ -117,25 +118,23 @@ class GrpcBinaryFileServerDownloaderTest { } } - @DisplayName("do start") @Nested class TestDoStart { @Captor private ArgumentCaptor<Runnable> runnableCaptor; - @Captor - private ArgumentCaptor<ExceptionalRunnable> setupStreamCaptor; + + @BeforeEach + void init() { + doNothing().when(downloader).safelySetupStreams(); + } @SneakyThrows @Test - void shouldCallSetupStreams() { - doNothing().when(downloader).handleSafety(any()); - + void shouldSafelySetupStreams() { downloader.doStart(); - verify(downloader).handleSafety(setupStreamCaptor.capture()); - setupStreamCaptor.getValue().run(); - verify(downloader).setupStreams(); + verify(downloader).safelySetupStreams(); } @Test @@ -159,28 +158,138 @@ class GrpcBinaryFileServerDownloaderTest { } } - @DisplayName("Start download") + @Nested + class TestSafelySetupStreams { + + @SneakyThrows + @Test + void shouldSetupStreams() { + doNothing().when(downloader).setupStreams(); + + safelySetupStreams(); + + verify(downloader).setupStreams(); + } + + @Nested + class OnException { + + @Mock + private PipedOutputStream outputStream; + @Mock + private PipedInputStream inputStream; + + private final IOException exception = new IOException(); + + @SneakyThrows + @BeforeEach + void init() { + setInputStreamField(inputStream); + setOutputStreamField(outputStream); + doThrow(exception).when(downloader).setupStreams(); + } + + @Test + void shouldThrowBinaryFileDownloadException() { + assertThatThrownBy(() -> downloader.safelySetupStreams()).isInstanceOf(TechnicalException.class).hasCause(exception); + } + + @SneakyThrows + @Test + void shouldCloseOutputStream() { + catchThrowable(TestSafelySetupStreams.this::safelySetupStreams); + + verify(outputStream).close(); + } + + @SneakyThrows + @Test + void shouldCloseInputStream() { + catchThrowable(TestSafelySetupStreams.this::safelySetupStreams); + + verify(inputStream).close(); + } + } + + private void safelySetupStreams() { + downloader.safelySetupStreams(); + } + } + @Nested class TestStartDownload { - @Captor - private ArgumentCaptor<ExceptionalRunnable> runnableCaptor; + @Mock + private PipedOutputStream outputStream; + + @BeforeEach + void init() { + setOutputStreamField(outputStream); + } - @SneakyThrows @Test - void shouldCallDoDownload() { - doNothing().when(downloader).handleSafety(any()); - doNothing().when(downloader).doDownload(); + void shouldErrorBeInitiallyNull() { + assertThat(getDownloadError()).isNull(); + } - downloader.startDownload(); + @Nested + class OnNoException { - verify(downloader).handleSafety(runnableCaptor.capture()); - runnableCaptor.getValue().run(); - verify(downloader).doDownload(); + @BeforeEach + void init() { + doNothing().when(downloader).doDownload(); + } + + @Test + void shouldDoDownload() { + downloader.startDownload(); + + verify(downloader).doDownload(); + } + + @SneakyThrows + @Test + void shouldCloseOutputStream() { + downloader.startDownload(); + + verify(outputStream).close(); + } + + @Test + void shouldSendChunks() { + downloader.startDownload(); + + verify(downloader).sendChunks(); + } + } + + @Nested + class OnException { + + private final TechnicalException exception = new TechnicalException("error"); + + @BeforeEach + void init() { + doThrow(exception).when(downloader).doDownload(); + } + + @Test + void shouldSetError() { + downloader.startDownload(); + + assertThat(getDownloadError()).isInstanceOf(TechnicalException.class).hasCause(exception); + } + + @SneakyThrows + @Test + void shouldCloseOutputStream() { + downloader.startDownload(); + + verify(outputStream).close(); + } } } - @DisplayName("do") @Nested class TestDoDownload { @@ -192,7 +301,6 @@ class GrpcBinaryFileServerDownloaderTest { setOutputStreamField(outputStream); } - @SneakyThrows @Test void shouldCallDownloadConsumer() { downloader.doDownload(); @@ -200,164 +308,207 @@ class GrpcBinaryFileServerDownloaderTest { verify(downloadConsumer).accept(outputStream); } - @SneakyThrows @Test - void shouldCloseOutputStream() { + void shouldDownloadFinishedBeInitiallyFalse() { + assertThat(getDownloadFinished()).isFalse(); + } + + @Test + void shouldSetDownloadFinished() { downloader.doDownload(); - verify(outputStream).close(); + assertThat(getDownloadFinished()).isTrue(); } } - @DisplayName("Send chunks") @Nested class TestSendChunks { - @SneakyThrows - @Test - void shouldCallHandleSafety() { - doNothing().when(downloader).doSendChunks(); - - downloader.sendChunks(); - - verify(downloader).handleSafety(any(ExceptionalRunnable.class)); - } + @Nested + class OnNoException { - @SneakyThrows - @Test - void shouldCallDoDownload() { - doNothing().when(downloader).doSendChunks(); + @SneakyThrows + @BeforeEach + void init() { + doNothing().when(downloader).doSendChunks(); + } - downloader.sendChunks(); + @SneakyThrows + @Test + void shouldDoSendChunks() { + downloader.sendChunks(); - verify(downloader).doSendChunks(); + verify(downloader).doSendChunks(); + } } - @DisplayName("do") @Nested - class TestDoSendChunks { + class OnException { - @Mock - private PipedInputStream inputStream; + private final IOException exception = new IOException(); @Captor - private ArgumentCaptor<ByteString> byteStringCaptor; - - private final int readBytes = 20; - private final byte[] buffer = new byte[readBytes]; - private final GrpcResponseDummy grpcResponseDummy = new GrpcResponseDummy(); + private ArgumentCaptor<TechnicalException> argumentCaptor; @SneakyThrows @BeforeEach - void mock() { - doNothing().when(downloader).tryCompleteRequest(); - when(callObserver.isReady()).thenReturn(true); - when(inputStream.read(any())).thenReturn(readBytes, -1); - setInputStreamField(inputStream); - new Random().nextBytes(buffer); - ReflectionTestUtils.setField(downloader, "buffer", buffer); + void init() { + doThrow(exception).when(downloader).doSendChunks(); + doNothing().when(downloader).completeRequestWithError(any()); } @Test - void shouldCallChunkBuilder() { - doSendChunks(); + void shouldCompleteRequestWithError() { + downloader.sendChunks(); - verify(chunkBuilder).apply(byteStringCaptor.capture()); - assertThat(byteStringCaptor.getValue().toByteArray()).isEqualTo(buffer); + verify(downloader).completeRequestWithError(argumentCaptor.capture()); + assertThat(argumentCaptor.getValue()).isInstanceOf(TechnicalException.class).hasCause(exception); } + } + } - @DisplayName("should send next chunk if callObserver is ready and stream already received data") - @Test - void shouldCallOnNext() { - when(chunkBuilder.apply(any())).thenReturn(grpcResponseDummy); + @Nested + class TestDoSendChunks { - doSendChunks(); + @Nested + class OnRequestFinished { - verify(callObserver).onNext(grpcResponseDummy); + @BeforeEach + void init() { + setRequestFinishedField(true); } - @DisplayName("should call complete grpc stream if download has finished and stream has no data left") @Test - void shouldCallCompleteDownload() { - setDownloadFinishedField(true); - + void shouldNotInteractWithCallObserver() { doSendChunks(); - verify(downloader).tryCompleteRequest(); + verifyNoInteractions(callObserver); } + } - @SneakyThrows - private void doSendChunks() { - downloader.doSendChunks(); + @Nested + class OnRequestNotFinished { + + @Nested + class OnNotReady { + + @BeforeEach + void init() { + when(callObserver.isReady()).thenReturn(false); + } + + @Test + void shouldOnlyCallIsReadyOnObserver() { + doSendChunks(); + + verify(callObserver).isReady(); + verifyNoMoreInteractions(callObserver); + } } - } - } - @Nested - class TestTryCompleteRequest { + @Nested + class OnReady { + + @Mock + private PipedInputStream inputStream; + @Captor + private ArgumentCaptor<ByteString> byteStringCaptor; + + private final int readBytes = 20; + private final byte[] buffer = new byte[readBytes]; + private final GrpcResponseDummy grpcResponseDummy = new GrpcResponseDummy(); + + @SneakyThrows + @BeforeEach + void mock() { + doNothing().when(downloader).tryCompleteRequest(); + when(callObserver.isReady()).thenReturn(true); + when(inputStream.read(any())).thenReturn(readBytes, -1); + setInputStreamField(inputStream); + new Random().nextBytes(buffer); + ReflectionTestUtils.setField(downloader, "buffer", buffer); + } - @Test - void shouldCallShouldCompleteRequest() { - downloader.tryCompleteRequest(); + @Test + void shouldCallChunkBuilder() { + doSendChunks(); - verify(downloader).shouldCompleteRequest(); - } + verify(chunkBuilder).apply(byteStringCaptor.capture()); + assertThat(byteStringCaptor.getValue().toByteArray()).isEqualTo(buffer); + } - @Test - void shouldCallCompleteRequest() { - doReturn(true).when(downloader).shouldCompleteRequest(); + @DisplayName("should send next chunk if callObserver is ready and stream already received data") + @Test + void shouldCallOnNext() { + when(chunkBuilder.apply(any())).thenReturn(grpcResponseDummy); - downloader.tryCompleteRequest(); + doSendChunks(); - verify(downloader).completeRequest(); - } + verify(callObserver).onNext(grpcResponseDummy); + } - @Test - void shouldNotCallCompleteRequest() { - doReturn(false).when(downloader).shouldCompleteRequest(); + @DisplayName("should call complete grpc stream if download has finished and stream has no data left") + @Test + void shouldTryCompleteRequest() { + setDownloadFinishedField(true); - downloader.tryCompleteRequest(); + doSendChunks(); - verify(downloader, never()).completeRequest(); + verify(downloader).tryCompleteRequest(); + } + } + } + + @SneakyThrows + private void doSendChunks() { + downloader.doSendChunks(); } } @Nested - class TestShouldCompleteRequest { + class TestTryCompleteRequest { @Nested - class TestWhenDownloadFinished { + class OnError { + + private final TechnicalException exception = new TechnicalException("error"); @BeforeEach void init() { - setDownloadFinishedField(true); + setDownloadErrorField(exception); } @Test - void shouldReturnTrue() { - var result = downloader.shouldCompleteRequest(); + void shouldThrowException() { + assertThatThrownBy(downloader::tryCompleteRequest).isSameAs(exception); + } + } + + @Nested + class OnDownloadFinished { - assertThat(result).isTrue(); + @BeforeEach + void init() { + setDownloadFinishedField(true); + doNothing().when(downloader).completeRequestNormally(); } @Test - void shouldReturnFalseIfRequestFinished() { - setRequestFinishedField(true); - - var result = downloader.shouldCompleteRequest(); + void shouldNotCompleteRequestWithError() { + downloader.tryCompleteRequest(); - assertThat(result).isFalse(); + verify(downloader, never()).completeRequestWithError(any()); } @Test - void shouldUpdateRequestFinished() { - downloader.shouldCompleteRequest(); + void shouldCompleteRequestNormally() { + downloader.tryCompleteRequest(); - assertThat(getRequestFinished()).isTrue(); + verify(downloader).completeRequestNormally(); } } @Nested - class TestWhenDownloadRunning { + class OnDownloadNotFinished { @BeforeEach void init() { @@ -365,116 +516,83 @@ class GrpcBinaryFileServerDownloaderTest { } @Test - void shouldReturnFalse() { - var result = downloader.shouldCompleteRequest(); + void shouldNotCompleteRequestNormally() { + downloader.tryCompleteRequest(); - assertThat(result).isFalse(); + verify(downloader, never()).completeRequestNormally(); } @Test - void shouldNotUpdateRequestFinished() { - downloader.shouldCompleteRequest(); + void shouldNotCompleteRequestWithError() { + downloader.tryCompleteRequest(); - assertThat(getRequestFinished()).isFalse(); + verify(downloader, never()).completeRequestWithError(any()); } } } @Nested - class TestCompleteRequest { - - @Mock - private PipedInputStream inputStream; + class TestCompleteRequestNormally { @BeforeEach - void mock() { - setRequestFinishedField(false); - setDownloadFinishedField(true); - setInputStreamField(inputStream); + void init() { + doNothing().when(downloader).closeInputStream(); } - @SneakyThrows @Test - void shouldCallCloseInputStream() { - downloader.completeRequest(); + void shouldSetRequestFinished() { + assertThat(getRequestFinished()).isFalse(); - verify(downloader).closeInputStream(); + downloader.completeRequestNormally(); + + assertThat(getRequestFinished()).isTrue(); } @Test - void shouldCallOnCompleted() { - downloader.completeRequest(); + void shouldCloseInputStream() { + downloader.completeRequestNormally(); - verify(callObserver).onCompleted(); + verify(downloader).closeInputStream(); } - @SneakyThrows - private boolean getRequestFinished() { - return ReflectionTestUtils.getField(downloader, "requestFinished", AtomicBoolean.class).get(); + @Test + void shouldNotifyObserver() { + downloader.completeRequestNormally(); + + verify(callObserver).onCompleted(); } } - @DisplayName("Handle safety") @Nested - class TestHandleSafety { + class TestCompleteRequestWithError { - @DisplayName("on exception") - @Nested - class TestOnException { + private final TechnicalException error = new TechnicalException("error"); - @Mock - private PipedOutputStream outputStream; - @Mock - private PipedInputStream inputStream; - - private final IOException exception = new IOException(); - - @SneakyThrows - @BeforeEach - void mock() { - setInputStreamField(inputStream); - setOutputStreamField(outputStream); - } - - @SneakyThrows - @Test - void shouldThrowTechnicalException() { - assertThatThrownBy(this::handleSafety).isInstanceOf(TechnicalException.class).extracting(Throwable::getCause).isEqualTo(exception); - } - - @SneakyThrows - @Test - void shouldCloseOutputStream() { - try { - handleSafety(); - } catch (Exception e) { - // do nothing - } + @BeforeEach + void init() { + doNothing().when(downloader).closeInputStream(); + } - verify(outputStream).close(); - } + @Test + void shouldSetRequestFinished() { + assertThat(getRequestFinished()).isFalse(); - @SneakyThrows - @Test - void shouldCloseInputStream() { - try { - handleSafety(); - } catch (Exception e) { - // do nothing - } + catchException(() -> downloader.completeRequestWithError(error)); - verify(inputStream).close(); - } + assertThat(getRequestFinished()).isTrue(); + } - private void handleSafety() { - downloader.handleSafety(this::dummyMethodThrowingException); - } + @Test + void shouldCloseInputStream() { + catchException(() -> downloader.completeRequestWithError(error)); - private void dummyMethodThrowingException() throws IOException { - throw exception; - } + verify(downloader).closeInputStream(); } + @Test + void shouldThrowException() { + assertThatThrownBy(() -> downloader.completeRequestWithError(error)).isSameAs(error); + } } private void setOutputStreamField(OutputStream outputStream) { @@ -485,10 +603,6 @@ class GrpcBinaryFileServerDownloaderTest { ReflectionTestUtils.setField(downloader, "inputStream", inputStream); } - private void setDownloadFinishedField(boolean downloadFinished) { - ReflectionTestUtils.setField(downloader, "downloadFinished", new AtomicBoolean(downloadFinished)); - } - private void setRequestFinishedField(boolean requestFinished) { ReflectionTestUtils.setField(downloader, "requestFinished", new AtomicBoolean(requestFinished)); } @@ -497,6 +611,22 @@ class GrpcBinaryFileServerDownloaderTest { return ReflectionTestUtils.getField(downloader, "requestFinished", AtomicBoolean.class).get(); } - static class GrpcResponseDummy { + private void setDownloadErrorField(TechnicalException error) { + ReflectionTestUtils.setField(downloader, "downloadError", new AtomicReference<>(error)); + } + + private TechnicalException getDownloadError() { + return (TechnicalException) ReflectionTestUtils.getField(downloader, "downloadError", AtomicReference.class).get(); + } + + private void setDownloadFinishedField(boolean downloadFinished) { + ReflectionTestUtils.setField(downloader, "downloadFinished", new AtomicBoolean(downloadFinished)); + } + + private boolean getDownloadFinished() { + return ReflectionTestUtils.getField(downloader, "downloadFinished", AtomicBoolean.class).get(); + } + + private static class GrpcResponseDummy { } } \ No newline at end of file