com.couchbase.client.core.service.kv.ReplicaHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core-io Show documentation
Show all versions of core-io Show documentation
The official Couchbase JVM Core IO Library
/*
* Copyright 2021 Couchbase, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.couchbase.client.core.service.kv;
import com.couchbase.client.core.Core;
import com.couchbase.client.core.CoreContext;
import com.couchbase.client.core.Reactor;
import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.cnc.RequestSpan;
import com.couchbase.client.core.cnc.TracingIdentifiers;
import com.couchbase.client.core.cnc.events.request.IndividualReplicaGetFailedEvent;
import com.couchbase.client.core.config.BucketConfig;
import com.couchbase.client.core.config.CouchbaseBucketConfig;
import com.couchbase.client.core.env.CoreEnvironment;
import com.couchbase.client.core.error.CommonExceptions;
import com.couchbase.client.core.error.CouchbaseException;
import com.couchbase.client.core.error.DocumentUnretrievableException;
import com.couchbase.client.core.error.context.AggregateErrorContext;
import com.couchbase.client.core.error.context.ErrorContext;
import com.couchbase.client.core.error.context.ReducedKeyValueErrorContext;
import com.couchbase.client.core.io.CollectionIdentifier;
import com.couchbase.client.core.msg.kv.GetRequest;
import com.couchbase.client.core.msg.kv.GetResponse;
import com.couchbase.client.core.msg.kv.ReplicaGetRequest;
import com.couchbase.client.core.retry.RetryStrategy;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.couchbase.client.core.error.DefaultErrorUtil.keyValueStatusToException;
import static com.couchbase.client.core.util.Validators.notNullOrEmpty;
import static java.util.Objects.requireNonNull;
@Stability.Internal
public class ReplicaHelper {
private ReplicaHelper() {
throw new AssertionError("not instantiable");
}
public static class GetReplicaResponse {
private final GetResponse response;
private final boolean fromReplica;
public GetReplicaResponse(GetResponse response, boolean fromReplica) {
this.response = requireNonNull(response);
this.fromReplica = fromReplica;
}
public boolean isFromReplica() {
return fromReplica;
}
public GetResponse getResponse() {
return response;
}
}
/**
* @param clientContext (nullable)
* @param parentSpan (nullable)
*/
public static Mono getAnyReplicaReactive(
final Core core,
final CollectionIdentifier collectionIdentifier,
final String documentId,
final Duration timeout,
final RetryStrategy retryStrategy,
Map clientContext,
RequestSpan parentSpan
) {
RequestSpan getAnySpan = core.context().environment().requestTracer()
.requestSpan(TracingIdentifiers.SPAN_GET_ANY_REPLICA, parentSpan);
return getAllReplicasReactive(core, collectionIdentifier, documentId, timeout, retryStrategy, clientContext, getAnySpan)
.next()
.doFinally(signalType -> getAnySpan.end());
}
/**
* @param clientContext (nullable)
* @param parentSpan (nullable)
*/
public static Flux getAllReplicasReactive(
final Core core,
final CollectionIdentifier collectionIdentifier,
final String documentId,
final Duration timeout,
final RetryStrategy retryStrategy,
Map clientContext,
RequestSpan parentSpan
) {
notNullOrEmpty(documentId, "Id", () -> ReducedKeyValueErrorContext.create(documentId, collectionIdentifier));
CoreEnvironment env = core.context().environment();
RequestSpan getAllSpan = env.requestTracer().requestSpan(TracingIdentifiers.SPAN_GET_ALL_REPLICAS, parentSpan);
getAllSpan.attribute(TracingIdentifiers.ATTR_SYSTEM, TracingIdentifiers.ATTR_SYSTEM_COUCHBASE);
return Reactor
.toMono(() -> getAllReplicasRequests(core, collectionIdentifier, documentId, clientContext, retryStrategy, timeout, getAllSpan))
.flux()
.flatMap(Flux::fromStream)
.flatMap(request -> Reactor
.wrap(request, get(core, request), true)
.onErrorResume(t -> {
env.eventBus().publish(new IndividualReplicaGetFailedEvent(request.context()));
return Mono.empty(); // Swallow any errors from individual replicas
})
.map(response -> new GetReplicaResponse(response, request instanceof ReplicaGetRequest))
)
.doFinally(signalType -> getAllSpan.end());
}
/**
* Reads from replicas or the active node based on the options and returns the results as a list
* of futures that might complete or fail.
*
* @param clientContext (nullable)
* @param parentSpan (nullable)
* @param responseMapper converts the GetReplicaResponse to the client's native result type
* @return a list of results from the active and the replica.
*/
public static CompletableFuture>> getAllReplicasAsync(
final Core core,
final CollectionIdentifier collectionIdentifier,
final String documentId,
final Duration timeout,
final RetryStrategy retryStrategy,
final Map clientContext,
final RequestSpan parentSpan,
final Function responseMapper
) {
CoreEnvironment env = core.context().environment();
RequestSpan getAllSpan = env.requestTracer().requestSpan(TracingIdentifiers.SPAN_GET_ALL_REPLICAS, parentSpan);
getAllSpan.attribute(TracingIdentifiers.ATTR_SYSTEM, TracingIdentifiers.ATTR_SYSTEM_COUCHBASE);
return getAllReplicasRequests(core, collectionIdentifier, documentId, clientContext, retryStrategy, timeout, getAllSpan)
.thenApply(stream ->
stream.map(request ->
get(core, request)
.thenApply(response -> new GetReplicaResponse(response, request instanceof ReplicaGetRequest))
.thenApply(responseMapper)
).collect(Collectors.toList()))
.whenComplete((completableFutures, throwable) -> {
final AtomicInteger toComplete = new AtomicInteger(completableFutures.size());
for (CompletableFuture cf : completableFutures) {
cf.whenComplete((a, b) -> {
if (toComplete.decrementAndGet() == 0) {
getAllSpan.end();
}
});
}
});
}
/**
* @param clientContext (nullable)
* @param parentSpan (nullable)
* @param responseMapper converts the GetReplicaResponse to the client's native result type
*/
public static CompletableFuture getAnyReplicaAsync(
final Core core,
final CollectionIdentifier collectionIdentifier,
final String documentId,
final Duration timeout,
final RetryStrategy retryStrategy,
final Map clientContext,
final RequestSpan parentSpan,
final Function responseMapper) {
RequestSpan getAnySpan = core.context().environment().requestTracer()
.requestSpan(TracingIdentifiers.SPAN_GET_ANY_REPLICA, parentSpan);
CompletableFuture>> listOfFutures = getAllReplicasAsync(
core, collectionIdentifier, documentId, timeout, retryStrategy, clientContext, getAnySpan, responseMapper
);
// Aggregating the futures here will discard the individual errors, which we don't need
CompletableFuture anyReplicaFuture = new CompletableFuture<>();
listOfFutures.whenComplete((futures, throwable) -> {
if (throwable != null) {
anyReplicaFuture.completeExceptionally(throwable);
}
final AtomicBoolean successCompleted = new AtomicBoolean(false);
final AtomicInteger totalCompleted = new AtomicInteger(0);
final List nestedContexts = Collections.synchronizedList(new ArrayList<>());
futures.forEach(individual -> individual.whenComplete((result, error) -> {
int completed = totalCompleted.incrementAndGet();
if (error != null) {
if (error instanceof CompletionException && error.getCause() instanceof CouchbaseException) {
nestedContexts.add(((CouchbaseException) error.getCause()).context());
}
}
if (result != null && successCompleted.compareAndSet(false, true)) {
anyReplicaFuture.complete(result);
}
if (!successCompleted.get() && completed == futures.size()) {
anyReplicaFuture.completeExceptionally(new DocumentUnretrievableException(new AggregateErrorContext(nestedContexts)));
}
}));
});
return anyReplicaFuture.whenComplete((getReplicaResult, throwable) -> getAnySpan.end());
}
/**
* Helper method to assemble a stream of requests to the active and all replicas
*
* @param core the core to execute the request
* @param collectionIdentifier the collection containing the document
* @param documentId the ID of the document
* @param clientContext (nullable) client context info
* @param retryStrategy the retry strategy to use
* @param timeout the timeout until we need to stop the get all replicas
* @param parent the "get all/any replicas" request span
* @return a stream of requests.
*/
public static CompletableFuture> getAllReplicasRequests(
final Core core,
final CollectionIdentifier collectionIdentifier,
final String documentId,
final Map clientContext,
final RetryStrategy retryStrategy,
final Duration timeout,
final RequestSpan parent
) {
notNullOrEmpty(documentId, "Id");
final CoreContext coreContext = core.context();
final CoreEnvironment environment = coreContext.environment();
final BucketConfig config = core.clusterConfig().bucketConfig(collectionIdentifier.bucket());
if (config instanceof CouchbaseBucketConfig) {
int numReplicas = ((CouchbaseBucketConfig) config).numberOfReplicas();
List requests = new ArrayList<>(numReplicas + 1);
RequestSpan span = environment.requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_GET, parent);
GetRequest activeRequest = new GetRequest(documentId, timeout, coreContext, collectionIdentifier, retryStrategy, span);
activeRequest.context().clientContext(clientContext);
requests.add(activeRequest);
for (short replica = 1; replica <= numReplicas; replica++) {
RequestSpan replicaSpan = environment.requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_GET_REPLICA, parent);
ReplicaGetRequest replicaRequest = new ReplicaGetRequest(
documentId, timeout, coreContext, collectionIdentifier, retryStrategy, replica, replicaSpan
);
replicaRequest.context().clientContext(clientContext);
requests.add(replicaRequest);
}
return CompletableFuture.completedFuture(requests.stream());
} else if (config == null) {
// no bucket config found, it might be in-flight being opened so we need to reschedule the operation until
// the timeout fires!
final Duration retryDelay = Duration.ofMillis(100);
final CompletableFuture> future = new CompletableFuture<>();
coreContext.environment().timer().schedule(() -> {
getAllReplicasRequests(core, collectionIdentifier, documentId, clientContext, retryStrategy, timeout.minus(retryDelay), parent).whenComplete((getRequestStream, throwable) -> {
if (throwable != null) {
future.completeExceptionally(throwable);
} else {
future.complete(getRequestStream);
}
});
}, retryDelay);
return future;
} else {
final CompletableFuture> future = new CompletableFuture<>();
future.completeExceptionally(CommonExceptions.getFromReplicaNotCouchbaseBucket());
return future;
}
}
private static CompletableFuture get(final Core core, final GetRequest request) {
core.send(request);
return request
.response()
.thenApply(response -> {
if (!response.status().success()) {
throw keyValueStatusToException(request, response);
}
return response;
})
.whenComplete((t, e) -> request.context().logicallyComplete());
}
}