mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-30 21:51:26 +00:00
Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7db96c672f | ||
|
f8725d6313 | ||
|
2165c87b9d | ||
|
f61e320230 |
2
pom.xml
2
pom.xml
@@ -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>
|
||||
|
@@ -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()));
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
38
src/test/java/org/qortal/test/api/CrossChainApiTests.java
Normal file
38
src/test/java/org/qortal/test/api/CrossChainApiTests.java
Normal 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));
|
||||
}
|
||||
|
||||
}
|
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user