Skip to content

Commit

Permalink
wallets: Fix bitcoind deadlock
Browse files Browse the repository at this point in the history
We parse stdout and wait for the "init message: Done loading" line to
detect when bitcoind is ready. After seeing this line, we stop reading
stdout causing the stdout buffer to fill up. When the stdout buffer
fills up, bitcoind waits until its parent process reads from it but this
unfortunately never happens leading to a deadlock.
  • Loading branch information
alvasw committed Sep 3, 2024
1 parent 2b5e36e commit dedc162
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -60,7 +61,7 @@ public void shutdown() {
invokeStopRpcCall();
}

private void waitUntilReady() {
protected void waitUntilReady() {
FutureTask<Boolean> waitingFuture = new FutureTask<>(this::waitUntilLogContainsLines);
Thread waitingThread = new Thread(waitingFuture);
waitingThread.start();
Expand All @@ -79,7 +80,9 @@ private void waitUntilReady() {
}
}

protected abstract Set<String> getIsSuccessfulStartUpLogLines();
protected Set<String> getIsSuccessfulStartUpLogLines() {
return Collections.emptySet();
}

public abstract void invokeStopRpcCall();

Expand All @@ -89,6 +92,7 @@ private Process createAndStartProcess() throws IOException {

var processBuilder = new ProcessBuilder(args);
processBuilder.redirectErrorStream(true);
processBuilder.redirectOutput(ProcessBuilder.Redirect.DISCARD);

Map<String, String> environment = processBuilder.environment();
environment.putAll(processConfig.getEnvironmentVars());
Expand All @@ -99,7 +103,9 @@ private Process createAndStartProcess() throws IOException {

public abstract ProcessConfig createProcessConfig();

protected abstract LogScanner getLogScanner();
protected LogScanner getLogScanner() {
return null;
}

protected boolean waitUntilLogContainsLines() {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@

package bisq.wallets.regtest.bitcoind;

import bisq.common.file.InputStreamScanner;
import bisq.common.file.LogScanner;
import bisq.common.util.NetworkUtils;
import bisq.wallets.bitcoind.rpc.BitcoindDaemon;
import bisq.wallets.json_rpc.JsonRpcClient;
Expand All @@ -30,20 +28,25 @@
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.net.ConnectException;
import java.nio.file.Path;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.Set;

@Slf4j
public class BitcoindRegtestProcess extends DaemonProcess {

@Getter
protected final RpcConfig rpcConfig;
private final BitcoindDaemon bitcoindDaemon;

public BitcoindRegtestProcess(RpcConfig rpcConfig, Path dataDir) {
super(dataDir);
this.rpcConfig = rpcConfig;
JsonRpcClient rpcClient = RpcClientFactory.createDaemonRpcClient(rpcConfig);
bitcoindDaemon = new BitcoindDaemon(rpcClient);
}

@Override
Expand Down Expand Up @@ -74,16 +77,39 @@ public ProcessConfig createProcessConfig() {
}

@Override
protected LogScanner getLogScanner() {
return new InputStreamScanner(
getIsSuccessfulStartUpLogLines(),
process.getInputStream()
);
protected void waitUntilReady() {
Instant timeoutInstant = Instant.now().plus(2, ChronoUnit.MINUTES);
int failedAttempts = 0;
while (true) {
try {
bitcoindDaemon.listWallets();
log.info("Connected to Bitcoin Core.");
break;
} catch (RpcCallFailureException e) {
if (e.getCause() instanceof ConnectException) {
if (isAfterTimeout(timeoutInstant)) {
throw new IllegalStateException("Bitcoin Core isn't ready after 2 minutes. Giving up.");
}

failedAttempts++;
double msToWait = Math.min(300 * failedAttempts, 10_000);
log.info("Bitcoind RPC isn't ready yet. Trying again in {}ms.", msToWait);
sleepForMs(msToWait);
}
}
}
}

@Override
protected Set<String> getIsSuccessfulStartUpLogLines() {
return Set.of("init message: Done loading");
private boolean isAfterTimeout(Instant timeoutInstant) {
return Instant.now().isAfter(timeoutInstant);
}

private void sleepForMs(double ms) {
try {
Thread.sleep((long) ms);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
}

@Override
Expand Down

0 comments on commit dedc162

Please sign in to comment.