Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,22 @@

package com.google.cloud.spanner.adapter;

import static com.google.cloud.spanner.adapter.util.ErrorMessageUtils.serverErrorResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.api.gax.rpc.ApiCallContext;
import com.google.api.gax.rpc.ServerStream;
import static com.google.cloud.spanner.adapter.util.ErrorMessageUtils.serverErrorResponse;
import static com.google.cloud.spanner.adapter.util.ErrorMessageUtils.unavailableErrorResponse;
import com.google.protobuf.ByteString;
import com.google.spanner.adapter.v1.AdaptMessageRequest;
import com.google.spanner.adapter.v1.AdaptMessageResponse;
import com.google.spanner.adapter.v1.AdapterClient;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Wraps an {@link AdapterClient} to manage gRPC communication with the Adapter service. */
final class AdapterClientWrapper {
Expand Down Expand Up @@ -85,8 +87,8 @@ ByteString sendGrpcRequest(
}
} catch (RuntimeException e) {
LOG.error("Error executing AdaptMessage request: ", e);
// Any error in getting the AdaptMessageResponse should be reported back to the client.
return serverErrorResponse(streamId, e.getMessage());
// Any grpc exception should be reported back to the client as a retryable unavailable error.
return unavailableErrorResponse(streamId, e.getMessage());
}

if (collectedPayloads.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,23 @@

package com.google.cloud.spanner.adapter;

import static com.google.cloud.spanner.adapter.util.ErrorMessageUtils.serverErrorResponse;
import static com.google.cloud.spanner.adapter.util.ErrorMessageUtils.unpreparedResponse;
import static com.google.cloud.spanner.adapter.util.StringUtils.startsWith;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.datastax.oss.driver.internal.core.protocol.ByteBufPrimitiveCodec;
import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting;
Expand All @@ -32,27 +46,15 @@
import com.google.api.gax.grpc.GrpcCallContext;
import com.google.api.gax.rpc.ApiCallContext;
import com.google.cloud.spanner.adapter.metrics.BuiltInMetricsRecorder;
import static com.google.cloud.spanner.adapter.util.ErrorMessageUtils.serverErrorResponse;
import static com.google.cloud.spanner.adapter.util.ErrorMessageUtils.unpreparedResponse;
import static com.google.cloud.spanner.adapter.util.StringUtils.startsWith;
import com.google.common.collect.ImmutableMap;
import com.google.protobuf.ByteString;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Handles the connection from a driver, translating TCP data to gRPC requests and vice versa. */
final class DriverConnectionHandler implements Runnable {
Expand Down Expand Up @@ -171,7 +173,7 @@ private void processRequestsLoop(InputStream inputStream, OutputStream outputStr
}
} catch (RuntimeException e) {
// 5. Handle any error during payload construction or attachment processing.
// Create a server error response to send back to the client.
// Create an server error response to send back to the client to trigger retry.
LOG.error("Error processing request: ", e);
response =
serverErrorResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

package com.google.cloud.spanner.adapter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.datastax.oss.driver.api.core.ConsistencyLevel;
import com.datastax.oss.driver.api.core.context.DriverContext;
import com.datastax.oss.driver.api.core.retry.RetryDecision;
Expand All @@ -27,8 +30,6 @@
import com.datastax.oss.driver.api.core.session.Request;
import com.datastax.oss.driver.internal.core.retry.DefaultRetryPolicy;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A custom retry policy for Cloud Spanner's Cassandra API.
Expand Down Expand Up @@ -136,7 +137,10 @@ public RetryDecision onWriteTimeout(
@Override
public RetryDecision onUnavailable(
Request request, ConsistencyLevel cl, int required, int alive, int retryCount) {
return delegate.onUnavailable(request, cl, required, alive, retryCount);
// Original default behavior is RETRY_NEXT, which won't work for Spanner server that appears as
// a single node.
RetryDecision decision = (retryCount == 0) ? RetryDecision.RETRY_SAME : RetryDecision.RETHROW;
return decision;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,22 @@

package com.google.cloud.spanner.adapter.util;

import java.util.Collections;

import com.datastax.oss.driver.internal.core.protocol.ByteBufPrimitiveCodec;
import com.datastax.oss.protocol.internal.Compressor;
import com.datastax.oss.protocol.internal.Frame;
import com.datastax.oss.protocol.internal.FrameCodec;
import com.datastax.oss.protocol.internal.ProtocolConstants;
import com.datastax.oss.protocol.internal.ProtocolConstants.ErrorCode;
import com.datastax.oss.protocol.internal.response.Error;
import com.datastax.oss.protocol.internal.response.error.Unavailable;
import com.datastax.oss.protocol.internal.response.error.Unprepared;
import com.google.api.core.InternalApi;
import com.google.protobuf.ByteString;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import java.util.Collections;

/**
* Utility class for creating specific types of error response frames used in the server protocol,
Expand Down Expand Up @@ -75,6 +79,18 @@ public static ByteString serverErrorResponse(int streamId, String message) {
return errorResponse(streamId, errorMsg);
}

/**
* Creates a server error message response.
*
* @param streamId The stream id of the message.
* @param message The error message.
* @return A {@link ByteString} representing the server error response.
*/
public static ByteString unavailableErrorResponse(int streamId, String message) {
Error errorMsg = new Unavailable(message, ProtocolConstants.ConsistencyLevel.QUORUM, 1, 1);
return errorResponse(streamId, errorMsg);
}

/**
* Creates an error response frame and converts it to a byte array.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.google.cloud.spanner.adapter;

import static com.google.cloud.spanner.adapter.util.ErrorMessageUtils.serverErrorResponse;
import static com.google.cloud.spanner.adapter.util.ErrorMessageUtils.unavailableErrorResponse;;
import static com.google.cloud.spanner.adapter.util.ErrorMessageUtils.unpreparedResponse;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,17 @@

package com.google.cloud.spanner.adapter;

import java.net.InetSocketAddress;

import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.datastax.oss.driver.api.core.ConsistencyLevel;
import com.datastax.oss.driver.api.core.context.DriverContext;
import com.datastax.oss.driver.api.core.metadata.EndPoint;
import com.datastax.oss.driver.api.core.metadata.Node;
Expand All @@ -28,11 +35,6 @@
import com.datastax.oss.driver.api.core.servererrors.ReadFailureException;
import com.datastax.oss.driver.api.core.servererrors.WriteFailureException;
import com.datastax.oss.driver.api.core.session.Request;
import java.net.InetSocketAddress;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class SpannerCqlRetryPolicyTest {
Expand All @@ -52,6 +54,12 @@ public void setUp() {
when(endpoint.resolve()).thenReturn(new InetSocketAddress("localhost", 9042));
}

@Test
public void testOnUnavailable() {
RetryDecision decision = policy.onUnavailable(request, ConsistencyLevel.LOCAL_QUORUM, 3, 2, 0);
assertEquals(RetryDecision.RETRY_SAME, decision);
}

@Test
public void testOnErrorResponse_writeFailure_retryable() {
WriteFailureException e = mock(WriteFailureException.class);
Expand Down
Loading