Add preferred-blockchain filtering to /websockets/crosschain/tradeoffers via foreignBlockchain query param

This commit is contained in:
catbref 2020-12-10 16:00:01 +00:00
parent 31fa916156
commit 68e3d3b989
2 changed files with 149 additions and 93 deletions

View File

@ -3,6 +3,8 @@ package org.qortal.api.websocket;
import java.io.IOException; import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -42,18 +44,23 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
private static final Logger LOGGER = LogManager.getLogger(TradeOffersWebSocket.class); private static final Logger LOGGER = LogManager.getLogger(TradeOffersWebSocket.class);
private static final Map<String, AcctMode> previousAtModes = new HashMap<>(); private static class CachedOfferInfo {
public final Map<String, AcctMode> previousAtModes = new HashMap<>();
// OFFERING // OFFERING
private static final Map<String, CrossChainOfferSummary> currentSummaries = new HashMap<>(); public final Map<String, CrossChainOfferSummary> currentSummaries = new HashMap<>();
// REDEEMED/REFUNDED/CANCELLED // REDEEMED/REFUNDED/CANCELLED
private static final Map<String, CrossChainOfferSummary> historicSummaries = new HashMap<>(); public final Map<String, CrossChainOfferSummary> historicSummaries = new HashMap<>();
}
// Manual synchronization
private static final Map<String, CachedOfferInfo> cachedInfoByBlockchain = new HashMap<>();
private static final Predicate<CrossChainOfferSummary> isHistoric = offerSummary private static final Predicate<CrossChainOfferSummary> isHistoric = offerSummary
-> offerSummary.getMode() == AcctMode.REDEEMED -> offerSummary.getMode() == AcctMode.REDEEMED
|| offerSummary.getMode() == AcctMode.REFUNDED || offerSummary.getMode() == AcctMode.REFUNDED
|| offerSummary.getMode() == AcctMode.CANCELLED; || offerSummary.getMode() == AcctMode.CANCELLED;
private static final Map<Session, String> sessionBlockchain = Collections.synchronizedMap(new HashMap<>());
@Override @Override
public void configure(WebSocketServletFactory factory) { public void configure(WebSocketServletFactory factory) {
@ -79,7 +86,6 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
BlockData blockData = ((Controller.NewBlockEvent) event).getBlockData(); BlockData blockData = ((Controller.NewBlockEvent) event).getBlockData();
// Process any new info // Process any new info
List<CrossChainOfferSummary> crossChainOfferSummaries = new ArrayList<>();
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
// Find any new/changed trade ATs since this block // Find any new/changed trade ATs since this block
@ -88,8 +94,14 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
final Long expectedValue = null; final Long expectedValue = null;
final Integer minimumFinalHeight = blockData.getHeight(); final Integer minimumFinalHeight = blockData.getHeight();
// Loop for all different types of trade offer? for (SupportedBlockchain blockchain : SupportedBlockchain.values()) {
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getAcctMap(); Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(blockchain);
List<CrossChainOfferSummary> crossChainOfferSummaries = new ArrayList<>();
synchronized (cachedInfoByBlockchain) {
CachedOfferInfo cachedInfo = cachedInfoByBlockchain.computeIfAbsent(blockchain.name(), k -> new CachedOfferInfo());
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) { for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
byte[] codeHash = acctInfo.getKey().value; byte[] codeHash = acctInfo.getKey().value;
ACCT acct = acctInfo.getValue().get(); ACCT acct = acctInfo.getValue().get();
@ -100,67 +112,94 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
crossChainOfferSummaries.addAll(produceSummaries(repository, acct, atStates, blockData.getTimestamp())); crossChainOfferSummaries.addAll(produceSummaries(repository, acct, atStates, blockData.getTimestamp()));
} }
} catch (DataException e) {
// No output this time
return;
}
synchronized (previousAtModes) {
// Remove any entries unchanged from last time // Remove any entries unchanged from last time
crossChainOfferSummaries.removeIf(offerSummary -> previousAtModes.get(offerSummary.getQortalAtAddress()) == offerSummary.getMode()); crossChainOfferSummaries.removeIf(offerSummary -> cachedInfo.previousAtModes.get(offerSummary.getQortalAtAddress()) == offerSummary.getMode());
// Don't send anything if no results // Skip to next blockchain if nothing has changed (for this blockchain)
if (crossChainOfferSummaries.isEmpty()) if (crossChainOfferSummaries.isEmpty())
return; continue;
// Update // Update
for (CrossChainOfferSummary offerSummary : crossChainOfferSummaries) { for (CrossChainOfferSummary offerSummary : crossChainOfferSummaries) {
previousAtModes.put(offerSummary.qortalAtAddress, offerSummary.getMode()); cachedInfo.previousAtModes.put(offerSummary.qortalAtAddress, offerSummary.getMode());
LOGGER.trace(() -> String.format("Block height: %d, AT: %s, mode: %s", blockData.getHeight(), offerSummary.qortalAtAddress, offerSummary.getMode().name())); LOGGER.trace(() -> String.format("Block height: %d, AT: %s, mode: %s", blockData.getHeight(), offerSummary.qortalAtAddress, offerSummary.getMode().name()));
switch (offerSummary.getMode()) { switch (offerSummary.getMode()) {
case OFFERING: case OFFERING:
currentSummaries.put(offerSummary.qortalAtAddress, offerSummary); cachedInfo.currentSummaries.put(offerSummary.qortalAtAddress, offerSummary);
historicSummaries.remove(offerSummary.qortalAtAddress); cachedInfo.historicSummaries.remove(offerSummary.qortalAtAddress);
break; break;
case REDEEMED: case REDEEMED:
case REFUNDED: case REFUNDED:
case CANCELLED: case CANCELLED:
currentSummaries.remove(offerSummary.qortalAtAddress); cachedInfo.currentSummaries.remove(offerSummary.qortalAtAddress);
historicSummaries.put(offerSummary.qortalAtAddress, offerSummary); cachedInfo.historicSummaries.put(offerSummary.qortalAtAddress, offerSummary);
break; break;
case TRADING: case TRADING:
currentSummaries.remove(offerSummary.qortalAtAddress); cachedInfo.currentSummaries.remove(offerSummary.qortalAtAddress);
historicSummaries.remove(offerSummary.qortalAtAddress); cachedInfo.historicSummaries.remove(offerSummary.qortalAtAddress);
break; break;
} }
} }
// Remove any historic offers that are over 24 hours old // Remove any historic offers that are over 24 hours old
final long tooOldTimestamp = NTP.getTime() - 24 * 60 * 60 * 1000L; final long tooOldTimestamp = NTP.getTime() - 24 * 60 * 60 * 1000L;
historicSummaries.values().removeIf(historicSummary -> historicSummary.getTimestamp() < tooOldTimestamp); cachedInfo.historicSummaries.values().removeIf(historicSummary -> historicSummary.getTimestamp() < tooOldTimestamp);
} }
// Notify sessions // Notify sessions
for (Session session : getSessions()) for (Session session : getSessions()) {
// Only send if this session has this/no preferred blockchain
String preferredBlockchain = sessionBlockchain.get(session);
if (preferredBlockchain == null || preferredBlockchain.equals(blockchain.name()))
sendOfferSummaries(session, crossChainOfferSummaries); sendOfferSummaries(session, crossChainOfferSummaries);
} }
}
} catch (DataException e) {
// No output this time
}
}
@OnWebSocketConnect @OnWebSocketConnect
@Override @Override
public void onWebSocketConnect(Session session) { public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap(); Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
final boolean includeHistoric = queryParams.get("includeHistoric") != null; final boolean includeHistoric = queryParams.get("includeHistoric") != null;
List<String> foreignBlockchains = queryParams.get("foreignBlockchain");
final String foreignBlockchain = foreignBlockchains == null ? null : foreignBlockchains.get(0);
// Make sure blockchain (if any) is valid
if (foreignBlockchain != null && SupportedBlockchain.fromString(foreignBlockchain) == null) {
session.close(4003, "unknown blockchain: " + foreignBlockchain);
return;
}
// save session's preferred blockchain (if any)
sessionBlockchain.put(session, foreignBlockchain);
List<CrossChainOfferSummary> crossChainOfferSummaries = new ArrayList<>(); List<CrossChainOfferSummary> crossChainOfferSummaries = new ArrayList<>();
synchronized (previousAtModes) { synchronized (cachedInfoByBlockchain) {
crossChainOfferSummaries.addAll(currentSummaries.values()); Collection<CachedOfferInfo> cachedInfos;
if (foreignBlockchain == null)
// No preferred blockchain, so iterate through all of them
cachedInfos = cachedInfoByBlockchain.values();
else
cachedInfos = Collections.singleton(cachedInfoByBlockchain.computeIfAbsent(foreignBlockchain, k -> new CachedOfferInfo()));
for (CachedOfferInfo cachedInfo : cachedInfos) {
crossChainOfferSummaries.addAll(cachedInfo.currentSummaries.values());
if (includeHistoric) if (includeHistoric)
crossChainOfferSummaries.addAll(historicSummaries.values()); crossChainOfferSummaries.addAll(cachedInfo.historicSummaries.values());
}
} }
if (!sendOfferSummaries(session, crossChainOfferSummaries)) { if (!sendOfferSummaries(session, crossChainOfferSummaries)) {
@ -174,6 +213,9 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
@OnWebSocketClose @OnWebSocketClose
@Override @Override
public void onWebSocketClose(Session session, int statusCode, String reason) { public void onWebSocketClose(Session session, int statusCode, String reason) {
// clean up
sessionBlockchain.remove(session);
super.onWebSocketClose(session, statusCode, reason); super.onWebSocketClose(session, statusCode, reason);
} }
@ -208,7 +250,11 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
Long expectedValue = (long) AcctMode.OFFERING.value; Long expectedValue = (long) AcctMode.OFFERING.value;
Integer minimumFinalHeight = null; Integer minimumFinalHeight = null;
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getAcctMap(); for (SupportedBlockchain blockchain : SupportedBlockchain.values()) {
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(blockchain);
CachedOfferInfo cachedInfo = cachedInfoByBlockchain.computeIfAbsent(blockchain.name(), k -> new CachedOfferInfo());
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) { for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
byte[] codeHash = acctInfo.getKey().value; byte[] codeHash = acctInfo.getKey().value;
ACCT acct = acctInfo.getValue().get(); ACCT acct = acctInfo.getValue().get();
@ -222,13 +268,14 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
throw new DataException("Couldn't fetch current trades from repository"); throw new DataException("Couldn't fetch current trades from repository");
// Save initial AT modes // Save initial AT modes
previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> AcctMode.OFFERING))); cachedInfo.previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> AcctMode.OFFERING)));
// Convert to offer summaries // Convert to offer summaries
currentSummaries.putAll(produceSummaries(repository, acct, initialAtStates, null).stream() cachedInfo.currentSummaries.putAll(produceSummaries(repository, acct, initialAtStates, null).stream()
.collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, offerSummary -> offerSummary))); .collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, offerSummary -> offerSummary)));
} }
} }
}
private static void populateHistoricSummaries(Repository repository) throws DataException { private static void populateHistoricSummaries(Repository repository) throws DataException {
// We want REDEEMED/REFUNDED/CANCELLED trades over the last 24 hours // We want REDEEMED/REFUNDED/CANCELLED trades over the last 24 hours
@ -243,7 +290,11 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
Long expectedValue = null; Long expectedValue = null;
++minimumFinalHeight; // because height is just *before* timestamp ++minimumFinalHeight; // because height is just *before* timestamp
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getAcctMap(); for (SupportedBlockchain blockchain : SupportedBlockchain.values()) {
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(blockchain);
CachedOfferInfo cachedInfo = cachedInfoByBlockchain.computeIfAbsent(blockchain.name(), k -> new CachedOfferInfo());
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) { for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
byte[] codeHash = acctInfo.getKey().value; byte[] codeHash = acctInfo.getKey().value;
ACCT acct = acctInfo.getValue().get(); ACCT acct = acctInfo.getValue().get();
@ -262,10 +313,11 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
continue; continue;
// Add summary to initial burst // Add summary to initial burst
historicSummaries.put(historicOfferSummary.getQortalAtAddress(), historicOfferSummary); cachedInfo.historicSummaries.put(historicOfferSummary.getQortalAtAddress(), historicOfferSummary);
// Save initial AT mode // Save initial AT mode
previousAtModes.put(historicOfferSummary.getQortalAtAddress(), historicOfferSummary.getMode()); cachedInfo.previousAtModes.put(historicOfferSummary.getQortalAtAddress(), historicOfferSummary.getMode());
}
} }
} }
} }

View File

@ -67,6 +67,10 @@ public enum SupportedBlockchain {
return supportedAcctsByCodeHash; return supportedAcctsByCodeHash;
} }
public static SupportedBlockchain fromString(String name) {
return blockchainsByName.get(name);
}
public static Map<ByteArray, Supplier<ACCT>> getFilteredAcctMap(SupportedBlockchain blockchain) { public static Map<ByteArray, Supplier<ACCT>> getFilteredAcctMap(SupportedBlockchain blockchain) {
if (blockchain == null) if (blockchain == null)
return getAcctMap(); return getAcctMap();