Compare commits

..

10 Commits

13 changed files with 128 additions and 25 deletions

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>1.4.0</version>
<version>1.4.1</version>
<packaging>jar</packaging>
<properties>
<skipTests>true</skipTests>

View File

@@ -21,18 +21,28 @@ public class HSQLDBPool extends JDBCPool {
public Connection tryConnection() throws SQLException {
for (int i = 0; i < states.length(); i++) {
if (states.compareAndSet(i, RefState.available, RefState.allocated)) {
return connections[i].getConnection();
JDBCPooledConnection pooledConnection = connections[i];
if (pooledConnection == null)
// Probably shutdown situation
return null;
return pooledConnection.getConnection();
}
if (states.compareAndSet(i, RefState.empty, RefState.allocated)) {
try {
JDBCPooledConnection connection = (JDBCPooledConnection) source.getPooledConnection();
JDBCPooledConnection pooledConnection = (JDBCPooledConnection) source.getPooledConnection();
connection.addConnectionEventListener(this);
connection.addStatementEventListener(this);
connections[i] = connection;
if (pooledConnection == null)
// Probably shutdown situation
return null;
return connections[i].getConnection();
pooledConnection.addConnectionEventListener(this);
pooledConnection.addStatementEventListener(this);
connections[i] = pooledConnection;
return pooledConnection.getConnection();
} catch (SQLException e) {
states.set(i, RefState.empty);
}

View File

@@ -118,6 +118,37 @@ public class CrossChainResource {
}
}
@GET
@Path("/trade/{ataddress}")
@Operation(
summary = "Show detailed trade info",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
implementation = CrossChainTradeData.class
)
)
)
}
)
@ApiErrors({ApiError.ADDRESS_UNKNOWN, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public CrossChainTradeData getTrade(@PathParam("ataddress") String atAddress) {
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
if (atData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
if (acct == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
return acct.populateTradeData(repository, atData);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/trades")
@Operation(

View File

@@ -146,7 +146,11 @@ public class QortalATAPI extends API {
String atAddress = this.atData.getATAddress();
int height = timestamp.blockHeight;
int sequence = timestamp.transactionSequence + 1;
int sequence = timestamp.transactionSequence;
if (state.getCurrentBlockHeight() < BlockChain.getInstance().getAtFindNextTransactionFixHeight())
// Off-by-one bug still in effect
sequence += 1;
ATRepository.NextTransactionInfo nextTransactionInfo;
try {

View File

@@ -70,6 +70,7 @@ public class BlockChain {
private GenesisBlock.GenesisInfo genesisInfo;
public enum FeatureTrigger {
atFindNextTransactionFix;
}
/** Map of which blockchain features are enabled when (height/timestamp) */
@@ -371,6 +372,10 @@ public class BlockChain {
// Convenience methods for specific blockchain feature triggers
public int getAtFindNextTransactionFixHeight() {
return this.featureTriggers.get(FeatureTrigger.atFindNextTransactionFix.name()).intValue();
}
// More complex getters for aspects that change by height or timestamp
public long getRewardAtHeight(int ourHeight) {

View File

@@ -377,7 +377,9 @@ public abstract class Bitcoiny implements ForeignBlockchain {
// Process new keys
} while (true);
return walletTransactions.stream().collect(Collectors.toList());
Comparator<BitcoinyTransaction> newestTimestampFirstComparator = Comparator.comparingInt((BitcoinyTransaction txn) -> txn.timestamp).reversed();
return walletTransactions.stream().sorted(newestTimestampFirstComparator).collect(Collectors.toList());
}
/**

View File

@@ -475,6 +475,8 @@ public class Peer {
ByteBuffer outputBuffer = ByteBuffer.wrap(message.toBytes());
synchronized (this.socketChannel) {
final long sendStart = System.currentTimeMillis();
while (outputBuffer.hasRemaining()) {
int bytesWritten = this.socketChannel.write(outputBuffer);
@@ -484,7 +486,7 @@ public class Peer {
message.getId(),
this));
if (bytesWritten == 0)
if (bytesWritten == 0) {
// Underlying socket's internal buffer probably full,
// so wait a short while for bytes to actually be transmitted over the wire
@@ -496,6 +498,11 @@ public class Peer {
* and connection loss.
*/
Thread.sleep(1L); //NOSONAR squid:S2276
if (System.currentTimeMillis() - sendStart > RESPONSE_TIMEOUT)
// We've taken too long to send this message
return false;
}
}
}
} catch (MessageException e) {

View File

@@ -276,7 +276,7 @@ public class HSQLDBRepository implements Repository {
return;
}
try (Statement stmt = this.connection.createStatement()) {
try {
assertEmptyTransaction("connection close");
// Assume we are not going to be GC'd for a while

View File

@@ -234,10 +234,11 @@ public class PresenceTransaction extends Transaction {
if (creatorsPresenceTransactions.isEmpty())
return;
// List should contain oldest transaction first, so remove all but last from repository.
creatorsPresenceTransactions.remove(creatorsPresenceTransactions.size() - 1);
for (TransactionData transactionData : creatorsPresenceTransactions) {
LOGGER.info(() -> String.format("Deleting older PRESENCE transaction %s", Base58.encode(transactionData.getSignature())));
if (transactionData.getTimestamp() >= this.transactionData.getTimestamp())
continue;
LOGGER.debug(() -> String.format("Deleting older PRESENCE transaction %s", Base58.encode(transactionData.getSignature())));
this.repository.getTransactionRepository().delete(transactionData);
}
}

View File

@@ -814,6 +814,23 @@ public abstract class Transaction {
return ValidationResult.OK;
} finally {
/*
* We call discardChanges() to restart repository 'transaction', discarding any
* transactional table locks, hence reducing possibility of deadlock or
* "serialization failure" with HSQLDB due to reads.
*
* "Serialization failure" most likely caused by existing transaction check above,
* where multiple threads are importing transactions
* and one thread finds existing an transaction, returns (unlocking blockchain lock),
* then another thread immediately obtains lock, tries to delete above existing transaction
* (e.g. older PRESENCE transaction) but can't because first thread's repository
* session still has row-lock on existing transaction and hasn't yet closed
* repository session. Deadlock caused by race condition.
*
* Hence we clear any repository-based locks before releasing blockchain lock.
*/
repository.discardChanges();
blockchainLock.unlock();
}
}

View File

@@ -125,8 +125,8 @@ public abstract class ExecuteProduceConsume implements Runnable {
// It's possible this might need to become a class instance private volatile
boolean canBlock = false;
while (true) {
final Task task;
while (!Thread.currentThread().isInterrupted()) {
Task task = null;
this.logger.trace(() -> String.format("[%d] waiting to produce...", Thread.currentThread().getId()));
@@ -142,7 +142,16 @@ public abstract class ExecuteProduceConsume implements Runnable {
Thread.currentThread().getId(), this.activeThreadCount, this.consumerCount, lambdaCanIdle));
final long beforeProduce = isLoggerTraceEnabled ? System.currentTimeMillis() : 0;
task = produceTask(canBlock);
try {
task = produceTask(canBlock);
} catch (InterruptedException e) {
// We're in shutdown situation so exit
Thread.currentThread().interrupt();
} catch (Exception e) {
this.logger.warn(() -> String.format("[%d] exception while trying to produce task", Thread.currentThread().getId()), e);
}
this.logger.trace(() -> String.format("[%d] producing took %dms", Thread.currentThread().getId(), System.currentTimeMillis() - beforeProduce));
}
@@ -155,7 +164,8 @@ public abstract class ExecuteProduceConsume implements Runnable {
--this.activeThreadCount;
this.logger.trace(() -> String.format("[%d] ending, activeThreadCount now: %d",
Thread.currentThread().getId(), this.activeThreadCount));
break;
return;
}
// We're the last surviving thread - producer can afford to block next round
@@ -192,7 +202,16 @@ public abstract class ExecuteProduceConsume implements Runnable {
}
this.logger.trace(() -> String.format("[%d] performing task...", Thread.currentThread().getId()));
task.perform(); // This can block for a while
try {
task.perform(); // This can block for a while
} catch (InterruptedException e) {
// We're in shutdown situation so exit
Thread.currentThread().interrupt();
} catch (Exception e) {
this.logger.warn(() -> String.format("[%d] exception while performing task", Thread.currentThread().getId()), e);
}
this.logger.trace(() -> String.format("[%d] finished task", Thread.currentThread().getId()));
synchronized (this) {
@@ -206,8 +225,6 @@ public abstract class ExecuteProduceConsume implements Runnable {
canBlock = false;
}
}
} catch (InterruptedException e) {
// We're in shutdown situation so exit
} finally {
if (this.isLoggerTraceEnabled)
Thread.currentThread().setName(this.className);

View File

@@ -48,6 +48,7 @@
"minutesPerBlock": 1
},
"featureTriggers": {
"atFindNextTransactionFix": 275000
},
"genesisInfo": {
"version": 4,

View File

@@ -77,16 +77,24 @@ public class GetNextTransactionTests extends Common {
BlockUtils.mintBlock(repository);
assertTimestamp(repository, atAddress, transaction);
// Mint a few blocks, then send non-AT message, followed by AT message
// Mint a few blocks, then send non-AT message, followed by two AT messages (in same block)
for (int i = 0; i < 5; ++i)
BlockUtils.mintBlock(repository);
sendMessage(repository, deployer, data, deployer.getAddress());
transaction = sendMessage(repository, deployer, data, atAddress);
Transaction transaction1 = sendMessage(repository, deployer, data, atAddress);
Transaction transaction2 = sendMessage(repository, deployer, data, atAddress);
BlockUtils.mintBlock(repository);
// Confirm AT finds message
// Confirm AT finds first message
BlockUtils.mintBlock(repository);
assertTimestamp(repository, atAddress, transaction);
assertTimestamp(repository, atAddress, transaction1);
// Confirm AT finds second message
BlockUtils.mintBlock(repository);
assertTimestamp(repository, atAddress, transaction2);
}
}