Compare commits

...

4 Commits

Author SHA1 Message Date
catbref
7db96c672f Bump to v1.3.3 2020-08-13 14:16:50 +01:00
catbref
f8725d6313 Modify ApplyUpdate to pass JVM options to Windows launcher EXE
ApplyUpdate is the 2nd-stage of the auto-update system, called
after core has downloaded the update.

As old versions of the Windows launcher EXE selects a 'client'
JVM mode, heap memory could be limited to only 256MB.

Until users upgrade via Windows installer, which replaces the EXE
with 'server' JVM mode baked-in, then a work-around is to
pass -XX:MaxRAMFraction=4 to the new JVM in order to emulate
heap size in 'server' JVM mode.
2020-08-13 14:08:47 +01:00
catbref
2165c87b9d Fix race condition between Network.start() and Controller calling Network.prunePeers()
Modified synchronized Lists to be final.
Moved some initializers out of constructor.
2020-08-13 13:45:06 +01:00
catbref
f61e320230 Fix API call GET /crosschain/trades (get completed trades) due to poorly performing SQL query.
Added "minimumTimestamp" param to same API call to allow fetching results for scenarios like:
* completed trades since midnight
* completed trades within last 24 hours

Added corresponding tests for above API call, including checking call response times.
2020-08-13 11:56:08 +01:00
8 changed files with 227 additions and 48 deletions

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>1.3.2</version>
<version>1.3.3</version>
<packaging>jar</packaging>
<properties>
<bitcoinj.version>0.15.5</bitcoinj.version>

View File

@@ -35,6 +35,8 @@ public class ApplyUpdate {
private static final String JAR_FILENAME = AutoUpdate.JAR_FILENAME;
private static final String NEW_JAR_FILENAME = AutoUpdate.NEW_JAR_FILENAME;
private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe";
private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS";
private static final String JAVA_TOOL_OPTIONS_VALUE = "-XX:MaxRAMFraction=4";
private static final long CHECK_INTERVAL = 10 * 1000L; // ms
private static final int MAX_ATTEMPTS = 12;
@@ -65,17 +67,19 @@ public class ApplyUpdate {
}
private static boolean shutdownNode() {
String BASE_URI = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
LOGGER.info(String.format("Shutting down node using API via %s", BASE_URI));
String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
LOGGER.info(() -> String.format("Shutting down node using API via %s", baseUri));
int attempt;
for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
LOGGER.info(String.format("Attempt #%d out of %d to shutdown node", attempt + 1, MAX_ATTEMPTS));
String response = ApiRequest.perform(BASE_URI + "admin/stop", null);
final int attemptForLogging = attempt;
LOGGER.info(() -> String.format("Attempt #%d out of %d to shutdown node", attemptForLogging + 1, MAX_ATTEMPTS));
String response = ApiRequest.perform(baseUri + "admin/stop", null);
if (response == null)
break;
// No response - consider node shut down
return true;
LOGGER.info(String.format("Response from API: %s", response));
LOGGER.info(() -> String.format("Response from API: %s", response));
try {
Thread.sleep(CHECK_INTERVAL);
@@ -99,19 +103,20 @@ public class ApplyUpdate {
Path newJar = Paths.get(NEW_JAR_FILENAME);
if (!Files.exists(newJar)) {
LOGGER.warn(String.format("Replacement JAR '%s' not found?", newJar));
LOGGER.warn(() -> String.format("Replacement JAR '%s' not found?", newJar));
return;
}
int attempt;
for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
LOGGER.info(String.format("Attempt #%d out of %d to replace JAR", attempt + 1, MAX_ATTEMPTS));
final int attemptForLogging = attempt;
LOGGER.info(() -> String.format("Attempt #%d out of %d to replace JAR", attemptForLogging + 1, MAX_ATTEMPTS));
try {
Files.copy(newJar, realJar, StandardCopyOption.REPLACE_EXISTING);
break;
} catch (IOException e) {
LOGGER.info(String.format("Unable to replace JAR: %s", e.getMessage()));
LOGGER.info(() -> String.format("Unable to replace JAR: %s", e.getMessage()));
// Try again
}
@@ -119,6 +124,7 @@ public class ApplyUpdate {
try {
Thread.sleep(CHECK_INTERVAL);
} catch (InterruptedException e) {
LOGGER.warn("Ignoring interrupt...");
// Doggedly retry
}
}
@@ -129,13 +135,13 @@ public class ApplyUpdate {
private static void restartNode(String[] args) {
String javaHome = System.getProperty("java.home");
LOGGER.info(String.format("Java home: %s", javaHome));
LOGGER.info(() -> String.format("Java home: %s", javaHome));
Path javaBinary = Paths.get(javaHome, "bin", "java");
LOGGER.info(String.format("Java binary: %s", javaBinary));
LOGGER.info(() -> String.format("Java binary: %s", javaBinary));
Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER);
LOGGER.info(String.format("Windows EXE launcher: %s", exeLauncher));
LOGGER.info(() -> String.format("Windows EXE launcher: %s", exeLauncher));
List<String> javaCmd;
if (Files.exists(exeLauncher)) {
@@ -156,9 +162,16 @@ public class ApplyUpdate {
}
try {
LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
LOGGER.info(() -> String.format("Restarting node with: %s", String.join(" ", javaCmd)));
new ProcessBuilder(javaCmd).start();
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
if (Files.exists(exeLauncher)) {
LOGGER.info(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE));
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
}
processBuilder.start();
} catch (IOException e) {
LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage()));
}

View File

@@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
@@ -1223,6 +1224,10 @@ public class CrossChainResource {
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public List<CrossChainTradeSummary> getCompletedTrades(
@Parameter(
description = "Only return trades that completed on/after this timestamp (milliseconds since epoch)",
example = "1597310000000"
) @QueryParam("minimumTimestamp") Long minimumTimestamp,
@Parameter( ref = "limit") @QueryParam("limit") Integer limit,
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
@Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
@@ -1230,10 +1235,27 @@ public class CrossChainResource {
if (limit != null && limit > 100)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// minimumTimestamp (if given) needs to be positive
if (minimumTimestamp != null && minimumTimestamp <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
final Boolean isFinished = Boolean.TRUE;
final Integer minimumFinalHeight = null;
try (final Repository repository = RepositoryManager.getRepository()) {
Integer minimumFinalHeight = null;
if (minimumTimestamp != null) {
minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(minimumTimestamp);
if (minimumFinalHeight == 0)
// We don't have any blocks since minimumTimestamp, let alone trades, so nothing to return
return Collections.emptyList();
// height returned from repository is for block BEFORE timestamp
// but we want trades AFTER timestamp so bump height accordingly
minimumFinalHeight++;
}
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
isFinished,
BTCACCT.MODE_BYTE_OFFSET, (long) BTCACCT.Mode.REDEEMED.value,

View File

@@ -50,7 +50,6 @@ import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.transform.Transformer;
import org.qortal.utils.ExecuteProduceConsume;
// import org.qortal.utils.ExecutorDumper;
import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot;
@@ -91,15 +90,16 @@ public class Network {
public static final int MAX_SIGNATURES_PER_REPLY = 500;
public static final int MAX_BLOCK_SUMMARIES_PER_REPLY = 500;
private final Ed25519PrivateKeyParameters edPrivateKeyParams;
private final Ed25519PublicKeyParameters edPublicKeyParams;
private final String ourNodeId;
// Generate our node keys / ID
private final Ed25519PrivateKeyParameters edPrivateKeyParams = new Ed25519PrivateKeyParameters(new SecureRandom());
private final Ed25519PublicKeyParameters edPublicKeyParams = edPrivateKeyParams.generatePublicKey();
private final String ourNodeId = Crypto.toNodeAddress(edPublicKeyParams.getEncoded());
private final int maxMessageSize;
private List<PeerData> allKnownPeers;
private List<Peer> connectedPeers;
private List<PeerAddress> selfPeers;
private final List<PeerData> allKnownPeers = new ArrayList<>();
private final List<Peer> connectedPeers = new ArrayList<>();
private final List<PeerAddress> selfPeers = new ArrayList<>();
private ExecuteProduceConsume networkEPC;
private Selector channelSelector;
@@ -108,39 +108,21 @@ public class Network {
private int minOutboundPeers;
private int maxPeers;
private long nextConnectTaskTimestamp;
private long nextConnectTaskTimestamp = 0L; // ms - try first connect once NTP syncs
private ExecutorService broadcastExecutor;
private long nextBroadcastTimestamp;
private ExecutorService broadcastExecutor = Executors.newCachedThreadPool();
private long nextBroadcastTimestamp = 0L; // ms - try first broadcast once NTP syncs
private Lock mergePeersLock;
private final Lock mergePeersLock = new ReentrantLock();
// Constructors
private Network() {
connectedPeers = new ArrayList<>();
selfPeers = new ArrayList<>();
// Generate our ID
byte[] seed = new byte[Transformer.PRIVATE_KEY_LENGTH];
new SecureRandom().nextBytes(seed);
edPrivateKeyParams = new Ed25519PrivateKeyParameters(seed, 0);
edPublicKeyParams = edPrivateKeyParams.generatePublicKey();
ourNodeId = Crypto.toNodeAddress(edPublicKeyParams.getEncoded());
maxMessageSize = 4 + 1 + 4 + BlockChain.getInstance().getMaxBlockSize();
minOutboundPeers = Settings.getInstance().getMinOutboundPeers();
maxPeers = Settings.getInstance().getMaxPeers();
nextConnectTaskTimestamp = 0; // First connect once NTP syncs
broadcastExecutor = Executors.newCachedThreadPool();
nextBroadcastTimestamp = 0; // First broadcast once NTP syncs
mergePeersLock = new ReentrantLock();
// We'll use a cached thread pool but with more aggressive timeout.
ExecutorService networkExecutor = new ThreadPoolExecutor(1,
Settings.getInstance().getMaxNetworkThreadPoolSize(),
@@ -177,7 +159,9 @@ public class Network {
// Load all known peers from repository
try (final Repository repository = RepositoryManager.getRepository()) {
allKnownPeers = repository.getNetworkRepository().getAllPeers();
synchronized (this.allKnownPeers) {
this.allKnownPeers.addAll(repository.getNetworkRepository().getAllPeers());
}
}
// Start up first networking thread

View File

@@ -301,7 +301,6 @@ public class HSQLDBATRepository implements ATRepository {
+ "WHERE ATStates.AT_address = ATs.AT_address "
+ "ORDER BY height DESC "
+ "LIMIT 1 "
+ "USING INDEX"
+ ") AS FinalATStates "
+ "WHERE code_hash = ? ");
@@ -309,7 +308,7 @@ public class HSQLDBATRepository implements ATRepository {
bindParams.add(codeHash);
if (isFinished != null) {
sql.append("AND is_finished = ?");
sql.append("AND is_finished = ? ");
bindParams.add(isFinished);
}

View File

@@ -0,0 +1,38 @@
package org.qortal.test.api;
import org.junit.Before;
import org.junit.Test;
import org.qortal.api.ApiError;
import org.qortal.api.resource.CrossChainResource;
import org.qortal.test.common.ApiCommon;
public class CrossChainApiTests extends ApiCommon {
private CrossChainResource crossChainResource;
@Before
public void buildResource() {
this.crossChainResource = (CrossChainResource) ApiCommon.buildResource(CrossChainResource.class);
}
@Test
public void testGetTradeOffers() {
assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getTradeOffers(limit, offset, reverse));
}
@Test
public void testGetCompletedTrades() {
assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getCompletedTrades(System.currentTimeMillis() /*minimumTimestamp*/, limit, offset, reverse));
}
@Test
public void testInvalidGetCompletedTrades() {
Integer limit = null;
Integer offset = null;
Boolean reverse = null;
assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(-1L /*minimumTimestamp*/, limit, offset, reverse));
assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(0L /*minimumTimestamp*/, limit, offset, reverse));
}
}

View File

@@ -0,0 +1,63 @@
package org.qortal.test.apps;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class LaunchExeWIthJvmOptions {
private static final String JAR_FILENAME = "qortal.jar";
private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe";
private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS";
private static final String JAVA_TOOL_OPTIONS_VALUE = "-XX:MaxRAMFraction=4";
public static void main(String[] args) {
String javaHome = System.getProperty("java.home");
System.out.println(String.format("Java home: %s", javaHome));
Path javaBinary = Paths.get(javaHome, "bin", "java");
System.out.println(String.format("Java binary: %s", javaBinary));
Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER);
System.out.println(String.format("Windows EXE launcher: %s", exeLauncher));
List<String> javaCmd;
if (Files.exists(exeLauncher)) {
javaCmd = Arrays.asList(exeLauncher.toString());
} else {
javaCmd = new ArrayList<>();
// Java runtime binary itself
javaCmd.add(javaBinary.toString());
// JVM arguments
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
// Call mainClass in JAR
javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME));
// Add saved command-line args
javaCmd.addAll(Arrays.asList(args));
}
try {
System.out.println(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
if (Files.exists(exeLauncher)) {
System.out.println(String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE));
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
}
processBuilder.start();
} catch (IOException e) {
System.err.println(String.format("Failed to restart node (BAD): %s", e.getMessage()));
}
}
}

View File

@@ -1,16 +1,30 @@
package org.qortal.test.common;
import static org.junit.Assert.*;
import java.lang.reflect.Field;
import org.eclipse.jetty.server.Request;
import org.junit.Before;
import org.qortal.api.ApiError;
import org.qortal.api.ApiException;
import org.qortal.repository.DataException;
public class ApiCommon extends Common {
public static final long MAX_API_RESPONSE_PERIOD = 2_000L; // ms
public static final Boolean[] ALL_BOOLEAN_VALUES = new Boolean[] { null, true, false };
public static final Boolean[] TF_BOOLEAN_VALUES = new Boolean[] { true, false };
public static final Integer[] SAMPLE_LIMIT_VALUES = new Integer[] { null, 0, 1, 20 };
public static final Integer[] SAMPLE_OFFSET_VALUES = new Integer[] { null, 0, 1, 5 };
@FunctionalInterface
public interface SlicedApiCall {
public abstract void call(Integer limit, Integer offset, Boolean reverse);
}
public static class FakeRequest extends Request {
public FakeRequest() {
super(null, null);
@@ -48,4 +62,50 @@ public class ApiCommon extends Common {
}
}
public static void assertApiError(ApiError expectedApiError, Runnable apiCall, Long maxResponsePeriod) {
try {
long beforeTimestamp = System.currentTimeMillis();
apiCall.run();
if (maxResponsePeriod != null) {
long responsePeriod = System.currentTimeMillis() - beforeTimestamp;
if (responsePeriod > maxResponsePeriod)
fail(String.format("API call response period %d ms greater than max allowed (%d ms)", responsePeriod, maxResponsePeriod));
}
} catch (ApiException e) {
ApiError actualApiError = ApiError.fromCode(e.error);
assertEquals(expectedApiError, actualApiError);
}
}
public static void assertApiError(ApiError expectedApiError, Runnable apiCall) {
assertApiError(expectedApiError, apiCall, MAX_API_RESPONSE_PERIOD);
}
public static void assertNoApiError(Runnable apiCall, Long maxResponsePeriod) {
try {
long beforeTimestamp = System.currentTimeMillis();
apiCall.run();
if (maxResponsePeriod != null) {
long responsePeriod = System.currentTimeMillis() - beforeTimestamp;
if (responsePeriod > maxResponsePeriod)
fail(String.format("API call response period %d ms greater than max allowed (%d ms)", responsePeriod, maxResponsePeriod));
}
} catch (ApiException e) {
fail("ApiException unexpected");
}
}
public static void assertNoApiError(Runnable apiCall) {
assertNoApiError(apiCall, MAX_API_RESPONSE_PERIOD);
}
public static void assertNoApiError(SlicedApiCall apiCall) {
for (Integer limit : SAMPLE_LIMIT_VALUES)
for (Integer offset : SAMPLE_OFFSET_VALUES)
for (Boolean reverse : ALL_BOOLEAN_VALUES)
assertNoApiError(() -> apiCall.call(limit, offset, reverse));
}
}