/* * Copyright (C) 2018 Dgraph Labs, Inc. and Contributors * * 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 io.dgraph; import static java.util.Arrays.asList; import com.google.protobuf.InvalidProtocolBufferException; import io.dgraph.DgraphProto.Payload; import io.grpc.Context; import io.grpc.Metadata; import io.grpc.Status; import io.grpc.StatusRuntimeException; import io.grpc.stub.MetadataUtils; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Asynchronous implementation of a Dgraph client using grpc. * * <p>Queries, mutations, and most other types of admin tasks can be run from the client. * * @author Edgar Rodriguez-Diaz * @author Deepak Jois * @author Michail Klimenkov */ public class DgraphAsyncClient { private static final Logger LOG = LoggerFactory.getLogger(DgraphAsyncClient.class); private final List<DgraphGrpc.DgraphStub> stubs; private final ReadWriteLock jwtLock; private DgraphProto.Jwt jwt; /** * Creates a new client for interacting with a Dgraph store. * * <p>A single client is thread safe. * * @param stubs - an array of grpc stubs to be used by this client. The stubs to be used are * chosen at random per transaction. */ public DgraphAsyncClient(DgraphGrpc.DgraphStub... stubs) { this.stubs = asList(stubs); this.jwtLock = new ReentrantReadWriteLock(); } /** * login sends a LoginRequest to the server that contains the userid and password. If the * LoginRequest is processed successfully, the response returned by the server will contain an * access JWT and a refresh JWT, which will be stored in the jwt field of this class, and used for * authorizing all subsequent requests sent to the server. * * @param userid the id of the user who is trying to login, e.g. Alice * @param password the password of the user * @return a future which can be used to wait for completion of the login request */ public CompletableFuture<Void> login(String userid, String password) { Lock wlock = jwtLock.writeLock(); wlock.lock(); try { final DgraphGrpc.DgraphStub client = anyClient(); final DgraphProto.LoginRequest loginRequest = DgraphProto.LoginRequest.newBuilder().setUserid(userid).setPassword(password).build(); StreamObserverBridge<DgraphProto.Response> bridge = new StreamObserverBridge<>(); client.login(loginRequest, bridge); return bridge .getDelegate() .thenAccept( (DgraphProto.Response response) -> { try { // set the jwt field jwt = DgraphProto.Jwt.parseFrom(response.getJson()); } catch (InvalidProtocolBufferException e) { String errmsg = "error while parsing jwt from the response: "; LOG.error(errmsg, e); throw new RuntimeException(errmsg, e); } }); } finally { wlock.unlock(); } } protected CompletableFuture<Void> retryLogin() { Lock wlock = jwtLock.writeLock(); wlock.lock(); try { if (jwt.getRefreshJwt().isEmpty()) { CompletableFuture<Void> future = new CompletableFuture<>(); future.completeExceptionally(new Exception("refresh JWT should not be empty")); return future; } final DgraphGrpc.DgraphStub client = anyClient(); final DgraphProto.LoginRequest loginRequest = DgraphProto.LoginRequest.newBuilder().setRefreshToken(jwt.getRefreshJwt()).build(); StreamObserverBridge<DgraphProto.Response> bridge = new StreamObserverBridge<>(); client.login(loginRequest, bridge); return bridge .getDelegate() .thenAccept( (DgraphProto.Response response) -> { try { // set the jwt field jwt = DgraphProto.Jwt.parseFrom(response.getJson()); } catch (InvalidProtocolBufferException e) { LOG.error("error while parsing jwt from the response: ", e); } }); } finally { wlock.unlock(); } } /** * getStubWithJwt adds an AttachHeadersInterceptor to the stub, which will eventually attach a * header whose key is accessJwt and value is the access JWT stored in the current * DgraphAsyncClient object. * * @param stub the original stub that we should attach JWT to * @return the augmented stub with JWT */ protected DgraphGrpc.DgraphStub getStubWithJwt(DgraphGrpc.DgraphStub stub) { Lock readLock = jwtLock.readLock(); readLock.lock(); try { if (jwt != null && !jwt.getAccessJwt().isEmpty()) { Metadata metadata = new Metadata(); metadata.put( Metadata.Key.of("accessJwt", Metadata.ASCII_STRING_MARSHALLER), jwt.getAccessJwt()); return MetadataUtils.attachHeaders(stub, metadata); } return stub; } finally { readLock.unlock(); } } /** * runWithRetries takes a supplier of CompletableFuture, tries to get the result from it while * handling exceptions caused by access JWT expiration. If such an exception happens, * runWithRetries will retry login using the refresh JWT and retry the logic in the supplier. * * @param <T> The type of the supplier's returned CompletableFuture. If the supplier provides * logic to run queries, then the type T will be DgraphProto.Response. * @param operation the name of the operation * @param callable the callable returning a CompletableFuture, which encapsulates the logic to run * queries, mutations or alter operations * @return a completable future which can be used to get the result */ protected <T> CompletableFuture<T> runWithRetries( String operation, Callable<CompletableFuture<T>> callable) { final Callable<CompletableFuture<T>> ctxCallable = Context.current().wrap(callable); return CompletableFuture.supplyAsync( () -> { try { return ctxCallable.call().get(); } catch (InterruptedException e) { LOG.error("The " + operation + " got interrupted:", e); throw new RuntimeException(e); } catch (ExecutionException e) { if (ExceptionUtil.isJwtExpired(e.getCause())) { try { // retry the login retryLogin().get(); // retry the supplied logic return ctxCallable.call().get(); } catch (InterruptedException ie) { LOG.error("The retried " + operation + " got interrupted:", ie); throw new RuntimeException(ie); } catch (ExecutionException ie) { LOG.error("The retried " + operation + " encounters an execution exception:", ie); throw new RuntimeException(ie); } catch (Exception ie) { LOG.error("The retried " + operation + " encounters a completion exception:", ie); throw new CompletionException(ie); } } else if (e.getCause() instanceof StatusRuntimeException) { StatusRuntimeException ex1 = (StatusRuntimeException) e.getCause(); Status.Code code = ex1.getStatus().getCode(); String desc = ex1.getStatus().getDescription(); if (code.equals(Status.Code.ABORTED) || code.equals(Status.Code.FAILED_PRECONDITION)) { throw new CompletionException(new TxnConflictException(desc)); } } // Handle the case when the outer exception is not caused by JWT expiration throw new RuntimeException( "The " + operation + " encountered an execution exception:", e); } catch (Exception e) { throw new CompletionException(e); } }); } /** * Alter can be used to perform the following operations, by setting the right fields in the * protocol buffer Operation object. * * <p>- Modify a schema. * * <p>- Drop predicate. * * <p>- Drop the database. * * @param op a protocol buffer Operation object representing the operation being performed. * @return CompletableFuture with instance of Payload set as result */ public CompletableFuture<Payload> alter(DgraphProto.Operation op) { final DgraphGrpc.DgraphStub stub = anyClient(); return runWithRetries( "alter", () -> { StreamObserverBridge<Payload> observerBridge = new StreamObserverBridge<>(); DgraphGrpc.DgraphStub localStub = getStubWithJwt(stub); localStub.alter(op, observerBridge); return observerBridge.getDelegate(); }); } private DgraphGrpc.DgraphStub anyClient() { int index = ThreadLocalRandom.current().nextInt(stubs.size()); DgraphGrpc.DgraphStub rawStub = stubs.get(index); return getStubWithJwt(rawStub); } /** * Creates a new AsyncTransaction object. All operations performed by this transaction are * asynchronous. * * <p>A transaction lifecycle is as follows: * * <p>- Created using AsyncTransaction#newTransaction() * * <p>- Various AsyncTransaction#query() and AsyncTransaction#mutate() calls made. * * <p>- Commit using AsyncTransacation#commit() or Discard using AsyncTransaction#discard(). If * any mutations have been made, It's important that at least one of these methods is called to * clean up resources. Discard is a no-op if Commit has already been called, so it's safe to call * it after Commit. * * @return a new AsyncTransaction object. */ public AsyncTransaction newTransaction() { return new AsyncTransaction(this, this.anyClient()); } /** * Creates a new AsyncTransaction object that only allows queries. Any AsyncTransaction#mutate() * or AsyncTransaction#commit() call made to the read only transaction will result in * TxnReadOnlyException. All operations performed by this transaction are asynchronous. * * @return a new AsyncTransaction object */ public AsyncTransaction newReadOnlyTransaction() { return new AsyncTransaction(this, this.anyClient(), true); } }