Adjust grpc & core services to new takeoffer error handling

- GrpcErrorMessageHandler  A new ErrorMessageHandler implementation
  to get around the api specific problem of having to use Task
  ErrorMessageHandlers that build task error messages for the UI.

- GrpcExceptionHandler  A new method for working with the
  ErrorMessageHandler interface.

- GrpcTradesService, CoreApi, CoreTradesService:  Ajdusted takeoffer
  error handling to give a failure reason provided by the new
  GrpcErrorMessageHandler.
This commit is contained in:
ghubstan 2021-03-10 14:16:15 -03:00
parent 067a5c7a08
commit d0590d93a9
No known key found for this signature in database
GPG Key ID: E35592D6800A861E
5 changed files with 153 additions and 22 deletions

View File

@ -33,6 +33,7 @@ import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.common.app.Version;
import bisq.common.config.Config;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import org.bitcoinj.core.Coin;
@ -224,12 +225,14 @@ public class CoreApi {
public void takeOffer(String offerId,
String paymentAccountId,
String takerFeeCurrencyCode,
Consumer<Trade> resultHandler) {
Consumer<Trade> resultHandler,
ErrorMessageHandler errorMessageHandler) {
Offer offer = coreOffersService.getOffer(offerId);
coreTradesService.takeOffer(offer,
paymentAccountId,
takerFeeCurrencyCode,
resultHandler);
resultHandler,
errorMessageHandler);
}
public void confirmPaymentStarted(String tradeId) {

View File

@ -32,6 +32,8 @@ import bisq.core.trade.protocol.SellerProtocol;
import bisq.core.user.User;
import bisq.core.util.validation.BtcAddressValidator;
import bisq.common.handlers.ErrorMessageHandler;
import org.bitcoinj.core.Coin;
import javax.inject.Inject;
@ -86,7 +88,8 @@ class CoreTradesService {
void takeOffer(Offer offer,
String paymentAccountId,
String takerFeeCurrencyCode,
Consumer<Trade> resultHandler) {
Consumer<Trade> resultHandler,
ErrorMessageHandler errorMessageHandler) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
@ -114,10 +117,7 @@ class CoreTradesService {
useSavingsWallet,
coreContext.isApiUser(),
resultHandler::accept,
errorMessage -> {
log.error(errorMessage);
throw new IllegalStateException(errorMessage);
}
errorMessageHandler
);
}

View File

@ -0,0 +1,104 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daemon.grpc;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.proto.grpc.TakeOfferReply;
import protobuf.AvailabilityResult;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import lombok.Getter;
import static bisq.proto.grpc.TradesGrpc.getTakeOfferMethod;
/**
* An implementation of bisq.common.handlers.ErrorMessageHandler that avoids
* an exception loop with the UI's bisq.common.taskrunner framework.
*
* The legacy ErrorMessageHandler is for reporting error messages only to the UI, but
* some core api tasks (takeoffer) require one. This implementation works around
* the problem of Task ErrorMessageHandlers not throwing exceptions to the gRPC client.
*
* Extra care is needed because exceptions thrown by an ErrorMessageHandler inside
* a Task may be thrown back to the GrpcService object, and if a gRPC ErrorMessageHandler
* responded by throwing another exception, the loop may only stop after the gRPC
* stream is closed.
*
* A unique instance should be used for a single gRPC call.
*/
public class GrpcErrorMessageHandler implements ErrorMessageHandler {
@Getter
private boolean isErrorHandled = false;
private final String fullMethodName;
private final StreamObserver<?> responseObserver;
private final GrpcExceptionHandler exceptionHandler;
private final Logger log;
public GrpcErrorMessageHandler(String fullMethodName,
StreamObserver<?> responseObserver,
GrpcExceptionHandler exceptionHandler,
Logger log) {
this.fullMethodName = fullMethodName;
this.exceptionHandler = exceptionHandler;
this.responseObserver = responseObserver;
this.log = log;
}
@Override
public void handleErrorMessage(String errorMessage) {
// A task runner may call handleErrorMessage(String) more than once.
// Throw only one exception if that happens, to avoid looping until the
// grpc stream is closed
if (!isErrorHandled) {
this.isErrorHandled = true;
log.error(errorMessage);
if (isTakeOfferError()) {
handleTakeOfferError();
} else {
exceptionHandler.handleErrorMessage(log,
errorMessage,
responseObserver);
}
}
}
private void handleTakeOfferError() {
// Send the AvailabilityResult to the client instead of throwing an exception.
// The client should look at the grpc reply object's AvailabilityResult
// field if reply.hasTrade = false, and use it give the user a human readable msg.
StreamObserver<TakeOfferReply> takeOfferResponseObserver = (StreamObserver<TakeOfferReply>) responseObserver;
var availabilityResult = AvailabilityResult.valueOf("MAKER_DENIED_API_USER");
var reply = TakeOfferReply.newBuilder()
.setAvailabilityResult(availabilityResult)
.build();
takeOfferResponseObserver.onNext(reply);
takeOfferResponseObserver.onCompleted();
}
private boolean isTakeOfferError() {
return fullMethodName.equals(getTakeOfferMethod().getFullMethodName());
}
}

View File

@ -24,6 +24,7 @@ import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.function.Function;
import java.util.function.Predicate;
import org.slf4j.Logger;
@ -68,6 +69,18 @@ class GrpcExceptionHandler {
throw grpcStatusRuntimeException;
}
public void handleErrorMessage(Logger log,
String errorMessage,
StreamObserver<?> responseObserver) {
// This is used to wrap Task errors from the ErrorMessageHandler
// interface, an interface that is not allowed to throw exceptions.
log.error(errorMessage);
var grpcStatusRuntimeException = new StatusRuntimeException(
UNKNOWN.withDescription(cliStyleErrorMessage.apply(errorMessage)));
responseObserver.onError(grpcStatusRuntimeException);
throw grpcStatusRuntimeException;
}
private StatusRuntimeException wrapException(Throwable t) {
// We want to be careful about what kinds of exception messages we send to the
// client. Expected core exceptions should be wrapped in an IllegalStateException
@ -87,6 +100,12 @@ class GrpcExceptionHandler {
}
}
private final Function<String, String> cliStyleErrorMessage = (e) -> {
String[] line = e.split("\\r?\\n");
int lastLine = line.length;
return line[lastLine - 1].toLowerCase();
};
private Status mapGrpcErrorStatus(Throwable t, String description) {
// We default to the UNKNOWN status, except were the mapping of a core api
// exception to a gRPC Status is obvious. If we ever use a gRPC reverse-proxy

View File

@ -90,21 +90,26 @@ class GrpcTradesService extends TradesImplBase {
@Override
public void takeOffer(TakeOfferRequest req,
StreamObserver<TakeOfferReply> responseObserver) {
try {
coreApi.takeOffer(req.getOfferId(),
req.getPaymentAccountId(),
req.getTakerFeeCurrencyCode(),
trade -> {
TradeInfo tradeInfo = toTradeInfo(trade);
var reply = TakeOfferReply.newBuilder()
.setTrade(tradeInfo.toProtoMessage())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
GrpcErrorMessageHandler errorMessageHandler =
new GrpcErrorMessageHandler(getTakeOfferMethod().getFullMethodName(),
responseObserver,
exceptionHandler,
log);
coreApi.takeOffer(req.getOfferId(),
req.getPaymentAccountId(),
req.getTakerFeeCurrencyCode(),
trade -> {
TradeInfo tradeInfo = toTradeInfo(trade);
var reply = TakeOfferReply.newBuilder()
.setTrade(tradeInfo.toProtoMessage())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
},
errorMessage -> {
if (!errorMessageHandler.isErrorHandled())
errorMessageHandler.handleErrorMessage(errorMessage);
});
}
@Override