This commit is contained in:
woodser 2021-05-04 20:20:01 -04:00
commit 8a38081c04
2800 changed files with 344130 additions and 0 deletions

View file

@ -0,0 +1,52 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.network.p2p.network.TorMode;
import org.berndpruenster.netlayer.tor.Tor;
import java.io.File;
/**
* This class uses an already defined Tor via <code>Tor.getDefault()</code>
*
* @author Florian Reimair
*
*/
public class AvailableTor extends TorMode {
private final String hiddenServiceDirectory;
public AvailableTor(File torWorkingDirectory, String hiddenServiceDirectory) {
super(torWorkingDirectory);
this.hiddenServiceDirectory = hiddenServiceDirectory;
}
@Override
public Tor getTor() {
return Tor.getDefault();
}
@Override
public String getHiddenServiceDirectory() {
return hiddenServiceDirectory;
}
}

View file

@ -0,0 +1,74 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import java.util.Properties;
/**
* Does some pre-computation for a configurable class.
*
* @author Florian Reimair
*/
public abstract class Configurable {
protected Properties configuration = new Properties();
private String name;
/**
* Filters all java properties starting with {@link Configurable#getName()} of
* the class and makes them available. Does <em>NOT</em> parse the content of
* the properties!
* <p>
* For example, if the implementing class sets its name (using
* {@link Configurable#setName(String)}) to <code>MyName</code>, the list of
* properties is scanned for properties starting with <code>MyName</code>.
* Matching lines are made available to the class without the prefix. For
* example, a property <code>MyName.answer=42</code> is made available as
* <code>configuration.getProperty("answer")</code> resulting in
* <code>42</code>.
*
* @param properties a set of configuration properties
*/
public void configure(final Properties properties) {
// only configure the Properties which belong to us
final Properties myProperties = new Properties();
properties.forEach((k, v) -> {
String key = (String) k;
if (key.startsWith(getName()))
myProperties.put(key.substring(key.indexOf(".") + 1), v);
});
// configure all properties that belong to us
this.configuration = myProperties;
}
protected String getName() {
return name;
}
/**
* Set the name used to filter through configuration properties. See
* {@link Configurable#configure(Properties)}.
*
* @param name the name of the configurable
*/
protected void setName(String name) {
this.name = name;
}
}

View file

@ -0,0 +1,147 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.common.app.Version;
import bisq.common.util.Utilities;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import static bisq.common.config.Config.BASE_CURRENCY_NETWORK;
/**
* Starts a Metric (in its own {@link Thread}), manages its properties and shuts
* it down gracefully. Furthermore, configuration updates and execution are done
* in a thread-save manner. Implementing classes only have to implement the
* {@link Metric#execute()} method.
*
* @author Florian Reimair
*/
@Slf4j
public abstract class Metric extends Configurable implements Runnable {
private static final String INTERVAL = "run.interval";
private static ScheduledExecutorService executor;
protected final Reporter reporter;
private ScheduledFuture<?> scheduler;
/**
* disable execution
*/
private void disable() {
if (scheduler != null)
scheduler.cancel(false);
}
/**
* enable execution
*/
private void enable() {
scheduler = executor.scheduleWithFixedDelay(this, new Random().nextInt(60),
Long.parseLong(configuration.getProperty(INTERVAL)), TimeUnit.SECONDS);
}
/**
* Constructor.
*/
protected Metric(Reporter reporter) {
this.reporter = reporter;
setName(this.getClass().getSimpleName());
if (executor == null) {
executor = new ScheduledThreadPoolExecutor(6);
}
}
boolean enabled() {
if (scheduler != null)
return !scheduler.isCancelled();
else
return false;
}
@Override
public void configure(final Properties properties) {
synchronized (this) {
log.info("{} (re)loading config...", getName());
super.configure(properties);
reporter.configure(properties);
Version.setBaseCryptoNetworkId(Integer.parseInt(properties.getProperty("System." + BASE_CURRENCY_NETWORK, "1"))); // defaults to BTC_TESTNET
// decide whether to enable or disable the task
if (configuration.isEmpty() || !configuration.getProperty("enabled", "false").equals("true")
|| !configuration.containsKey(INTERVAL)) {
disable();
// some informative log output
if (configuration.isEmpty())
log.error("{} is not configured at all. Will not run.", getName());
else if (!configuration.getProperty("enabled", "false").equals("true"))
log.info("{} is deactivated. Will not run.", getName());
else if (!configuration.containsKey(INTERVAL))
log.error("{} is missing mandatory '" + INTERVAL + "' property. Will not run.", getName());
else
log.error("{} is mis-configured. Will not run.", getName());
} else if (!enabled() && configuration.getProperty("enabled", "false").equals("true")) {
// check if this Metric got activated after being disabled.
// if so, resume execution
enable();
log.info("{} got activated. Starting up.", getName());
}
}
}
@Override
public void run() {
try {
Thread.currentThread().setName("Metric: " + getName());
// execute all the things
synchronized (this) {
log.info("{} started", getName());
execute();
log.info("{} done", getName());
}
} catch (Throwable e) {
log.error("A metric misbehaved!", e);
}
}
/**
* Gets scheduled repeatedly.
*/
protected abstract void execute();
/**
* initiate an orderly shutdown on all metrics. Blocks until all metrics are
* shut down or after one minute.
*/
public static void haltAllMetrics() {
Utilities.shutdownAndAwaitTermination(executor, 2, TimeUnit.MINUTES);
}
}

View file

@ -0,0 +1,182 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.monitor.metric.MarketStats;
import bisq.monitor.metric.P2PMarketStats;
import bisq.monitor.metric.P2PNetworkLoad;
import bisq.monitor.metric.P2PRoundTripTime;
import bisq.monitor.metric.P2PSeedNodeSnapshot;
import bisq.monitor.metric.PriceNodeStats;
import bisq.monitor.metric.TorHiddenServiceStartupTime;
import bisq.monitor.metric.TorRoundTripTime;
import bisq.monitor.metric.TorStartupTime;
import bisq.monitor.reporter.ConsoleReporter;
import bisq.monitor.reporter.GraphiteReporter;
import bisq.common.app.Capabilities;
import bisq.common.app.Capability;
import org.berndpruenster.netlayer.tor.NativeTor;
import org.berndpruenster.netlayer.tor.Tor;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import sun.misc.Signal;
/**
* Monitor executable for the Bisq network.
*
* @author Florian Reimair
*/
@Slf4j
public class Monitor {
public static final File TOR_WORKING_DIR = new File("monitor/work/monitor-tor");
private static String[] args = {};
public static void main(String[] args) throws Throwable {
Monitor.args = args;
new Monitor().start();
}
/**
* A list of all active {@link Metric}s
*/
private final List<Metric> metrics = new ArrayList<>();
/**
* Starts up all configured Metrics.
*
* @throws Throwable in case something goes wrong
*/
private void start() throws Throwable {
// start Tor
Tor.setDefault(new NativeTor(TOR_WORKING_DIR, null, null, false));
//noinspection deprecation,deprecation,deprecation,deprecation,deprecation,deprecation,deprecation,deprecation
Capabilities.app.addAll(Capability.TRADE_STATISTICS,
Capability.TRADE_STATISTICS_2,
Capability.ACCOUNT_AGE_WITNESS,
Capability.ACK_MSG,
Capability.PROPOSAL,
Capability.BLIND_VOTE,
Capability.DAO_STATE,
Capability.BUNDLE_OF_ENVELOPES,
Capability.REFUND_AGENT,
Capability.MEDIATION,
Capability.TRADE_STATISTICS_3);
// assemble Metrics
// - create reporters
Reporter graphiteReporter = new GraphiteReporter();
// only use ConsoleReporter if requested (for debugging for example)
Properties properties = getProperties();
if ("true".equals(properties.getProperty("System.useConsoleReporter", "false")))
graphiteReporter = new ConsoleReporter();
// - add available metrics with their reporters
metrics.add(new TorStartupTime(graphiteReporter));
metrics.add(new TorRoundTripTime(graphiteReporter));
metrics.add(new TorHiddenServiceStartupTime(graphiteReporter));
metrics.add(new P2PRoundTripTime(graphiteReporter));
metrics.add(new P2PNetworkLoad(graphiteReporter));
metrics.add(new P2PSeedNodeSnapshot(graphiteReporter));
metrics.add(new P2PMarketStats(graphiteReporter));
metrics.add(new PriceNodeStats(graphiteReporter));
metrics.add(new MarketStats(graphiteReporter));
// prepare configuration reload
// Note that this is most likely only work on Linux
Signal.handle(new Signal("USR1"), signal -> {
try {
configure();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
// configure Metrics
// - which also starts the metrics if appropriate
configure();
// exit Metrics gracefully on shutdown
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
// set the name of the Thread for debugging purposes
log.info("system shutdown initiated");
log.info("shutting down active metrics...");
Metric.haltAllMetrics();
try {
log.info("shutting down tor...");
Tor tor = Tor.getDefault();
checkNotNull(tor, "tor must not be null");
tor.shutdown();
} catch (Throwable ignore) {
}
log.info("system halt");
}, "Monitor Shutdown Hook ")
);
}
/**
* Reload the configuration from disk.
*
* @throws Exception if something goes wrong
*/
private void configure() throws Exception {
Properties properties = getProperties();
for (Metric current : metrics)
current.configure(properties);
}
/**
* Overloads a default set of properties with a file if given
*
* @return a set of properties
* @throws Exception in case something goes wrong
*/
private Properties getProperties() throws Exception {
Properties result = new Properties();
// if we have a config file load the config file, else, load the default config
// from the resources
if (args.length > 0)
result.load(new FileInputStream(args[0]));
else
result.load(Monitor.class.getClassLoader().getResourceAsStream("metrics.properties"));
return result;
}
}

View file

@ -0,0 +1,47 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.network.p2p.NodeAddress;
import java.net.MalformedURLException;
import java.net.URL;
/**
* Helper for parsing and pretty printing onion addresses.
*
* @author Florian Reimair
*/
public class OnionParser {
public static NodeAddress getNodeAddress(final String current) throws MalformedURLException {
String nodeAddress = current.trim();
if (!nodeAddress.startsWith("http://"))
nodeAddress = "http://" + nodeAddress;
URL tmp = new URL(nodeAddress);
return new NodeAddress(tmp.getHost(), tmp.getPort() > 0 ? tmp.getPort() : 80);
}
public static String prettyPrint(final NodeAddress host) {
return host.getHostNameWithoutPostFix();
}
public static String prettyPrint(String host) throws MalformedURLException {
return prettyPrint(getNodeAddress(host));
}
}

View file

@ -0,0 +1,74 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import java.util.Map;
/**
* Reports findings to a specific service/file/place using the proper means to
* do so.
*
* @author Florian Reimair
*/
public abstract class Reporter extends Configurable {
protected Reporter() {
setName(this.getClass().getSimpleName());
}
/**
* Report our findings.
*
* @param value the value to report
*/
public abstract void report(long value);
/**
* Report our findings
*
* @param value the value to report
* @param prefix a common prefix to be included in the tag name
*/
public abstract void report(long value, String prefix);
/**
* Report our findings.
*
* @param values Map<metric name, metric value>
*/
public abstract void report(Map<String, String> values);
/**
* Report our findings.
*
* @param values Map<metric name, metric value>
* @param prefix for example "torStartupTime"
*/
public abstract void report(Map<String, String> values, String prefix);
/**
* Report our findings one by one.
*
* @param key the metric name
* @param value the value to report
* @param timestamp a unix timestamp in milliseconds
* @param prefix for example "torStartupTime"
*/
public abstract void report(String key, String value, String timestamp, String prefix);
}

View file

@ -0,0 +1,70 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.LongSummaryStatistics;
import java.util.Map;
/**
* Calculates average, max, min, p25, p50, p75 off of a list of samples and
* throws in the sample size for good measure.
*
* @author Florian Reimair
*/
public class StatisticsHelper {
public static Map<String, String> process(Collection<Long> input) {
List<Long> samples = new ArrayList<>(input);
// aftermath
Collections.sort(samples);
// - average, max, min , sample size
LongSummaryStatistics statistics = samples.stream().mapToLong(val -> val).summaryStatistics();
Map<String, String> results = new HashMap<>();
results.put("average", String.valueOf(Math.round(statistics.getAverage())));
results.put("max", String.valueOf(statistics.getMax()));
results.put("min", String.valueOf(statistics.getMin()));
results.put("sampleSize", String.valueOf(statistics.getCount()));
// - p25, median, p75
Integer[] percentiles = new Integer[] { 25, 50, 75 };
for (Integer percentile : percentiles) {
double rank = statistics.getCount() * percentile / 100.0;
Long percentileValue;
if (samples.size() <= rank + 1)
percentileValue = samples.get(samples.size() - 1);
else if (Math.floor(rank) == rank)
percentileValue = samples.get((int) rank);
else
percentileValue = Math.round(samples.get((int) Math.floor(rank))
+ (samples.get((int) (Math.floor(rank) + 1)) - samples.get((int) Math.floor(rank)))
/ (rank - Math.floor(rank)));
results.put("p" + percentile, String.valueOf(percentileValue));
}
return results;
}
}

View file

@ -0,0 +1,81 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
/**
* Gate pattern to help with thread synchronization
*
* @author Florian Reimair
*/
@Slf4j
public class ThreadGate {
private CountDownLatch lock = new CountDownLatch(0);
/**
* Make everyone wait until the gate is open again.
*/
public void engage() {
lock = new CountDownLatch(1);
}
/**
* Make everyone wait until the gate is open again.
*
* @param numberOfLocks how often the gate has to be unlocked until the gate
* opens.
*/
public void engage(int numberOfLocks) {
lock = new CountDownLatch(numberOfLocks);
}
/**
* Wait for the gate to be opened. Blocks until the gate is open again. Returns
* immediately if the gate is already open.
*/
public synchronized void await() {
while (lock.getCount() > 0)
try {
if (!lock.await(60, TimeUnit.SECONDS)) {
log.warn("timeout occurred!");
break; // break the loop
}
} catch (InterruptedException ignore) {
}
}
/**
* Open the gate and let everyone proceed with their execution.
*/
public void proceed() {
lock.countDown();
}
/**
* Open the gate with no regards on how many locks are still in place.
*/
public void unlock() {
while (lock.getCount() > 0)
lock.countDown();
}
}

View file

@ -0,0 +1,100 @@
/*
* This file is part of Bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metric;
import bisq.monitor.Metric;
import bisq.monitor.Reporter;
import java.net.URL;
import java.net.URLConnection;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
/**
* Uses the markets API to retrieve market volume data.
*
* @author Florian Reimair
*
*/
@Slf4j
public class MarketStats extends Metric {
private static final String MARKETS_BISQ_NETWORK = "https://markets.bisq.network";
// poor mans JSON parser
private final Pattern marketPattern = Pattern.compile("\"market\" ?: ?\"([a-z_]+)\"");
private final Pattern amountPattern = Pattern.compile("\"amount\" ?: ?\"([\\d\\.]+)\"");
private final Pattern volumePattern = Pattern.compile("\"volume\" ?: ?\"([\\d\\.]+)\"");
private final Pattern timestampPattern = Pattern.compile("\"trade_date\" ?: ?([\\d]+)");
private Long lastRun = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(15));
public MarketStats(Reporter reporter) {
super(reporter);
}
@Override
protected void execute() {
try {
// for each configured host
Map<String, String> result = new HashMap<>();
// assemble query
long now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
String query = "/api/trades?format=json&market=all&timestamp_from=" + lastRun + "&timestamp_to=" + now;
lastRun = now; // thought about adding 1 second but what if a trade is done exactly in this one second?
// connect
URLConnection connection = new URL(MARKETS_BISQ_NETWORK + query).openConnection();
// prepare to receive data
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line, all = "";
while ((line = in.readLine()) != null)
all += ' ' + line;
in.close();
Arrays.stream(all.substring(0, all.length() - 2).split("}")).forEach(trade -> {
Matcher market = marketPattern.matcher(trade);
Matcher amount = amountPattern.matcher(trade);
Matcher timestamp = timestampPattern.matcher(trade);
market.find();
if (market.group(1).endsWith("btc")) {
amount = volumePattern.matcher(trade);
}
amount.find();
timestamp.find();
reporter.report("volume." + market.group(1), amount.group(1), timestamp.group(1), getName());
});
} catch (IllegalStateException ignore) {
// no match found
} catch (IOException e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,283 @@
/*
* This file is part of Bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metric;
import bisq.monitor.Reporter;
import bisq.core.offer.OfferPayload;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.peers.getdata.messages.GetDataResponse;
import bisq.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest;
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
import bisq.network.p2p.storage.payload.ProtectedStoragePayload;
import bisq.common.proto.network.NetworkEnvelope;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Demo Stats metric derived from the OfferPayload messages we get from the seed nodes
*
* @author Florian Reimair
*/
@Slf4j
public class P2PMarketStats extends P2PSeedNodeSnapshotBase {
final Map<NodeAddress, Statistics<Aggregator>> versionBucketsPerHost = new ConcurrentHashMap<>();
final Map<NodeAddress, Statistics<Aggregator>> offerVolumeBucketsPerHost = new ConcurrentHashMap<>();
final Map<NodeAddress, Statistics<List<Long>>> offerVolumeDistributionBucketsPerHost = new ConcurrentHashMap<>();
final Map<NodeAddress, Statistics<Map<NodeAddress, Aggregator>>> offersPerTraderBucketsPerHost = new ConcurrentHashMap<>();
final Map<NodeAddress, Statistics<Map<NodeAddress, Aggregator>>> volumePerTraderBucketsPerHost = new ConcurrentHashMap<>();
/**
* Efficient way to aggregate numbers.
*/
private static class Aggregator {
private long value = 0;
synchronized long value() {
return value;
}
synchronized void increment() {
value++;
}
synchronized void add(long amount) {
value += amount;
}
}
private abstract static class OfferStatistics<T> extends Statistics<T> {
@Override
public synchronized void log(Object message) {
if (message instanceof OfferPayload) {
OfferPayload currentMessage = (OfferPayload) message;
// For logging different data types
String market = currentMessage.getDirection() + "." + currentMessage.getBaseCurrencyCode() + "_" + currentMessage.getCounterCurrencyCode();
process(market, currentMessage);
}
}
abstract void process(String market, OfferPayload currentMessage);
}
private class OfferCountStatistics extends OfferStatistics<Aggregator> {
@Override
void process(String market, OfferPayload currentMessage) {
buckets.putIfAbsent(market, new Aggregator());
buckets.get(market).increment();
}
}
private class OfferVolumeStatistics extends OfferStatistics<Aggregator> {
@Override
void process(String market, OfferPayload currentMessage) {
buckets.putIfAbsent(market, new Aggregator());
buckets.get(market).add(currentMessage.getAmount());
}
}
private class OfferVolumeDistributionStatistics extends OfferStatistics<List<Long>> {
@Override
void process(String market, OfferPayload currentMessage) {
buckets.putIfAbsent(market, new ArrayList<>());
buckets.get(market).add(currentMessage.getAmount());
}
}
private class OffersPerTraderStatistics extends OfferStatistics<Map<NodeAddress, Aggregator>> {
@Override
void process(String market, OfferPayload currentMessage) {
buckets.putIfAbsent(market, new HashMap<>());
buckets.get(market).putIfAbsent(currentMessage.getOwnerNodeAddress(), new Aggregator());
buckets.get(market).get(currentMessage.getOwnerNodeAddress()).increment();
}
}
private class VolumePerTraderStatistics extends OfferStatistics<Map<NodeAddress, Aggregator>> {
@Override
void process(String market, OfferPayload currentMessage) {
buckets.putIfAbsent(market, new HashMap<>());
buckets.get(market).putIfAbsent(currentMessage.getOwnerNodeAddress(), new Aggregator());
buckets.get(market).get(currentMessage.getOwnerNodeAddress()).add(currentMessage.getAmount());
}
}
private class VersionsStatistics extends Statistics<Aggregator> {
@Override
public void log(Object message) {
if (message instanceof OfferPayload) {
OfferPayload currentMessage = (OfferPayload) message;
String version = "v" + currentMessage.getId().substring(currentMessage.getId().lastIndexOf("-") + 1);
buckets.putIfAbsent(version, new Aggregator());
buckets.get(version).increment();
}
}
}
public P2PMarketStats(Reporter graphiteReporter) {
super(graphiteReporter);
}
@Override
protected List<NetworkEnvelope> getRequests() {
List<NetworkEnvelope> result = new ArrayList<>();
Random random = new Random();
result.add(new PreliminaryGetDataRequest(random.nextInt(), hashes));
return result;
}
protected void createHistogram(List<Long> input, String market, Map<String, String> report) {
int numberOfBins = 5;
// - get biggest offer
double max = input.stream().max(Long::compareTo).map(value -> value * 1.01).orElse(0.0);
// - create histogram
input.stream().collect(
Collectors.groupingBy(aLong -> aLong == max ? numberOfBins - 1 : (int) Math.floor(aLong / (max / numberOfBins)), Collectors.counting())).
forEach((integer, integer2) -> report.put(market + ".bin_" + integer, String.valueOf(integer2)));
report.put(market + ".number_of_bins", String.valueOf(numberOfBins));
report.put(market + ".max", String.valueOf((int) max));
}
@Override
protected void report() {
Map<String, String> report = new HashMap<>();
bucketsPerHost.values().stream().findFirst().ifPresent(nodeAddressStatisticsEntry -> nodeAddressStatisticsEntry.values().forEach((market, numberOfOffers) -> report.put(market, String.valueOf(((Aggregator) numberOfOffers).value()))));
reporter.report(report, getName() + ".offerCount");
// do offerbook volume statistics
report.clear();
offerVolumeBucketsPerHost.values().stream().findFirst().ifPresent(aggregatorStatistics -> aggregatorStatistics.values().forEach((market, numberOfOffers) -> report.put(market, String.valueOf(numberOfOffers.value()))));
reporter.report(report, getName() + ".volume");
// do the offer vs volume histogram
report.clear();
// - get a data set
offerVolumeDistributionBucketsPerHost.values().stream().findFirst().ifPresent(listStatistics -> listStatistics.values().forEach((market, offers) -> {
createHistogram(offers, market, report);
}));
reporter.report(report, getName() + ".volume-per-offer-distribution");
// do offers per trader
report.clear();
// - get a data set
offersPerTraderBucketsPerHost.values().stream().findFirst().ifPresent(mapStatistics -> mapStatistics.values().forEach((market, stuff) -> {
List<Long> offerPerTrader = stuff.values().stream().map(Aggregator::value).collect(Collectors.toList());
createHistogram(offerPerTrader, market, report);
}));
reporter.report(report, getName() + ".traders_by_number_of_offers");
// do volume per trader
report.clear();
// - get a data set
volumePerTraderBucketsPerHost.values().stream().findFirst().ifPresent(mapStatistics -> mapStatistics.values().forEach((market, stuff) -> {
List<Long> volumePerTrader = stuff.values().stream().map(Aggregator::value).collect(Collectors.toList());
createHistogram(volumePerTrader, market, report);
}));
reporter.report(report, getName() + ".traders_by_volume");
// do version statistics
report.clear();
Optional<Statistics<Aggregator>> optionalStatistics = versionBucketsPerHost.values().stream().findAny();
optionalStatistics.ifPresent(aggregatorStatistics -> aggregatorStatistics.values()
.forEach((version, numberOfOccurrences) -> report.put(version, String.valueOf(numberOfOccurrences.value()))));
reporter.report(report, "versions");
}
protected boolean treatMessage(NetworkEnvelope networkEnvelope, Connection connection) {
checkNotNull(connection.getPeersNodeAddressProperty(),
"although the property is nullable, we need it to not be null");
if (networkEnvelope instanceof GetDataResponse) {
Statistics offerCount = new OfferCountStatistics();
Statistics offerVolume = new OfferVolumeStatistics();
Statistics offerVolumeDistribution = new OfferVolumeDistributionStatistics();
Statistics offersPerTrader = new OffersPerTraderStatistics();
Statistics volumePerTrader = new VolumePerTraderStatistics();
Statistics versions = new VersionsStatistics();
GetDataResponse dataResponse = (GetDataResponse) networkEnvelope;
final Set<ProtectedStorageEntry> dataSet = dataResponse.getDataSet();
dataSet.forEach(e -> {
final ProtectedStoragePayload protectedStoragePayload = e.getProtectedStoragePayload();
if (protectedStoragePayload == null) {
log.warn("StoragePayload was null: {}", networkEnvelope.toString());
return;
}
offerCount.log(protectedStoragePayload);
offerVolume.log(protectedStoragePayload);
offerVolumeDistribution.log(protectedStoragePayload);
offersPerTrader.log(protectedStoragePayload);
volumePerTrader.log(protectedStoragePayload);
versions.log(protectedStoragePayload);
});
dataResponse.getPersistableNetworkPayloadSet().forEach(persistableNetworkPayload -> {
// memorize message hashes
//Byte[] bytes = new Byte[persistableNetworkPayload.getHash().length];
//Arrays.setAll(bytes, n -> persistableNetworkPayload.getHash()[n]);
//hashes.add(bytes);
hashes.add(persistableNetworkPayload.getHash());
});
bucketsPerHost.put(connection.getPeersNodeAddressProperty().getValue(), offerCount);
offerVolumeBucketsPerHost.put(connection.getPeersNodeAddressProperty().getValue(), offerVolume);
offerVolumeDistributionBucketsPerHost.put(connection.getPeersNodeAddressProperty().getValue(), offerVolumeDistribution);
offersPerTraderBucketsPerHost.put(connection.getPeersNodeAddressProperty().getValue(), offersPerTrader);
volumePerTraderBucketsPerHost.put(connection.getPeersNodeAddressProperty().getValue(), volumePerTrader);
versionBucketsPerHost.put(connection.getPeersNodeAddressProperty().getValue(), versions);
return true;
}
return false;
}
}

View file

@ -0,0 +1,250 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metric;
import bisq.monitor.AvailableTor;
import bisq.monitor.Metric;
import bisq.monitor.Monitor;
import bisq.monitor.Reporter;
import bisq.monitor.ThreadGate;
import bisq.core.network.p2p.seed.DefaultSeedNodeRepository;
import bisq.core.proto.network.CoreNetworkProtoResolver;
import bisq.core.proto.persistable.CorePersistenceProtoResolver;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.MessageListener;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.network.SetupListener;
import bisq.network.p2p.network.TorNetworkNode;
import bisq.network.p2p.peers.PeerManager;
import bisq.network.p2p.peers.keepalive.KeepAliveManager;
import bisq.network.p2p.peers.peerexchange.PeerExchangeManager;
import bisq.network.p2p.storage.messages.BroadcastMessage;
import bisq.common.ClockWatcher;
import bisq.common.config.Config;
import bisq.common.file.CorruptedStorageFileHandler;
import bisq.common.persistence.PersistenceManager;
import bisq.common.proto.network.NetworkEnvelope;
import bisq.common.proto.network.NetworkProtoResolver;
import java.time.Clock;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.slf4j.Slf4j;
/**
* Contacts a list of hosts and asks them for all the data we do not have. The
* answers are then compiled into buckets of message types. Based on these
* buckets, the Metric reports (for each host) the message types observed and
* their number along with a relative comparison between all hosts.
*
* @author Florian Reimair
*
*/
@Slf4j
public class P2PNetworkLoad extends Metric implements MessageListener, SetupListener {
private static final String TOR_PROXY_PORT = "run.torProxyPort";
private static final String MAX_CONNECTIONS = "run.maxConnections";
private static final String HISTORY_SIZE = "run.historySize";
private NetworkNode networkNode;
private final File torHiddenServiceDir = new File("metric_" + getName());
private final ThreadGate hsReady = new ThreadGate();
private final Map<String, Counter> buckets = new ConcurrentHashMap<>();
/**
* Buffers the last X message we received. New messages will only be logged in case
* the message isn't already in the history. Note that the oldest message hashes are
* dropped to record newer hashes.
*/
private Map<Integer, Object> history;
private long lastRun = 0;
/**
* History implementation using a {@link LinkedHashMap} and its
* {@link LinkedHashMap#removeEldestEntry(Map.Entry)} option.
*/
private static class FixedSizeHistoryTracker<K, V> extends LinkedHashMap<K, V> {
final int historySize;
FixedSizeHistoryTracker(int historySize) {
super(historySize, 10, true);
this.historySize = historySize;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > historySize;
}
}
@Override
protected void execute() {
// in case we do not have a NetworkNode up and running, we create one
if (null == networkNode) {
// prepare the gate
hsReady.engage();
// start the network node
networkNode = new TorNetworkNode(Integer.parseInt(configuration.getProperty(TOR_PROXY_PORT, "9053")),
new CoreNetworkProtoResolver(Clock.systemDefaultZone()), false,
new AvailableTor(Monitor.TOR_WORKING_DIR, torHiddenServiceDir.getName()), null);
networkNode.start(this);
// wait for the HS to be published
hsReady.await();
// boot up P2P node
try {
Config config = new Config();
CorruptedStorageFileHandler corruptedStorageFileHandler = new CorruptedStorageFileHandler();
int maxConnections = Integer.parseInt(configuration.getProperty(MAX_CONNECTIONS, "12"));
NetworkProtoResolver networkProtoResolver = new CoreNetworkProtoResolver(Clock.systemDefaultZone());
CorePersistenceProtoResolver persistenceProtoResolver = new CorePersistenceProtoResolver(null,
networkProtoResolver);
DefaultSeedNodeRepository seedNodeRepository = new DefaultSeedNodeRepository(config);
PeerManager peerManager = new PeerManager(networkNode, seedNodeRepository, new ClockWatcher(),
new PersistenceManager<>(torHiddenServiceDir, persistenceProtoResolver, corruptedStorageFileHandler), maxConnections);
// init file storage
peerManager.readPersisted(() -> {
});
PeerExchangeManager peerExchangeManager = new PeerExchangeManager(networkNode, seedNodeRepository,
peerManager);
// updates the peer list every now and then as well
peerExchangeManager
.requestReportedPeersFromSeedNodes(seedNodeRepository.getSeedNodeAddresses().iterator().next());
KeepAliveManager keepAliveManager = new KeepAliveManager(networkNode, peerManager);
keepAliveManager.start();
networkNode.addMessageListener(this);
} catch (Throwable e) {
e.printStackTrace();
}
}
// report
Map<String, String> report = new HashMap<>();
if (lastRun != 0 && System.currentTimeMillis() - lastRun != 0) {
// - normalize to data/minute
double perMinuteFactor = 60000.0 / (System.currentTimeMillis() - lastRun);
// - get snapshot so we do not loose data
Set<String> keys = new HashSet<>(buckets.keySet());
// - transfer values to report
keys.forEach(key -> {
int value = buckets.get(key).getAndReset();
if (value != 0) {
report.put(key, String.format("%.2f", value * perMinuteFactor));
}
});
// - report
reporter.report(report, getName());
}
// - reset last run
lastRun = System.currentTimeMillis();
}
public P2PNetworkLoad(Reporter reporter) {
super(reporter);
}
@Override
public void configure(Properties properties) {
super.configure(properties);
history = Collections.synchronizedMap(new FixedSizeHistoryTracker<>(Integer.parseInt(configuration.getProperty(HISTORY_SIZE, "200"))));
}
/**
* Efficient way to count message occurrences.
*/
private static class Counter {
private int value = 1;
/**
* atomic get and reset
*
* @return the current value
*/
synchronized int getAndReset() {
try {
return value;
} finally {
value = 0;
}
}
synchronized void increment() {
value++;
}
}
@Override
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
if (networkEnvelope instanceof BroadcastMessage) {
try {
if (history.get(networkEnvelope.hashCode()) == null) {
history.put(networkEnvelope.hashCode(), null);
buckets.get(networkEnvelope.getClass().getSimpleName()).increment();
}
} catch (NullPointerException e) {
// use exception handling because we hardly ever need to add a fresh bucket
buckets.put(networkEnvelope.getClass().getSimpleName(), new Counter());
}
}
}
@Override
public void onTorNodeReady() {
}
@Override
public void onHiddenServicePublished() {
// open the gate
hsReady.proceed();
}
@Override
public void onSetupFailed(Throwable throwable) {
}
@Override
public void onRequestCustomBridges() {
}
}

View file

@ -0,0 +1,111 @@
/*
* This file is part of Bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metric;
import bisq.monitor.OnionParser;
import bisq.monitor.Reporter;
import bisq.monitor.StatisticsHelper;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.CloseConnectionReason;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.peers.keepalive.messages.Ping;
import bisq.network.p2p.peers.keepalive.messages.Pong;
import bisq.common.proto.network.NetworkEnvelope;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import static com.google.common.base.Preconditions.checkNotNull;
public class P2PRoundTripTime extends P2PSeedNodeSnapshotBase {
private static final String SAMPLE_SIZE = "run.sampleSize";
private final Map<Integer, Long> sentAt = new HashMap<>();
private Map<NodeAddress, Statistics> measurements = new HashMap<>();
public P2PRoundTripTime(Reporter reporter) {
super(reporter);
}
/**
* Use a counter to do statistics.
*/
private class Statistics {
private final List<Long> samples = new ArrayList<>();
public synchronized void log(Object message) {
Pong pong = (Pong) message;
Long start = sentAt.get(pong.getRequestNonce());
if (start != null)
samples.add(System.currentTimeMillis() - start);
}
public List<Long> values() {
return samples;
}
}
@Override
protected List<NetworkEnvelope> getRequests() {
List<NetworkEnvelope> result = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < Integer.parseInt(configuration.getProperty(SAMPLE_SIZE, "1")); i++)
result.add(new Ping(random.nextInt(), 42));
return result;
}
@Override
protected void aboutToSend(NetworkEnvelope message) {
sentAt.put(((Ping) message).getNonce(), System.currentTimeMillis());
}
@Override
protected boolean treatMessage(NetworkEnvelope networkEnvelope, Connection connection) {
if (networkEnvelope instanceof Pong) {
checkNotNull(connection.getPeersNodeAddressProperty(),
"although the property is nullable, we need it to not be null");
measurements.putIfAbsent(connection.getPeersNodeAddressProperty().getValue(), new Statistics());
measurements.get(connection.getPeersNodeAddressProperty().getValue()).log(networkEnvelope);
connection.shutDown(CloseConnectionReason.APP_SHUT_DOWN);
return true;
}
return false;
}
@Override
void report() {
// report
measurements.forEach(((nodeAddress, samples) ->
reporter.report(StatisticsHelper.process(samples.values()),
getName() + "." + OnionParser.prettyPrint(nodeAddress))
));
// clean up for next round
measurements = new HashMap<>();
}
}

View file

@ -0,0 +1,292 @@
/*
* This file is part of Bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metric;
import bisq.monitor.OnionParser;
import bisq.monitor.Reporter;
import bisq.core.dao.monitoring.model.StateHash;
import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.GetStateHashesResponse;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.peers.getdata.messages.GetDataResponse;
import bisq.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest;
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
import bisq.network.p2p.storage.payload.ProtectedStoragePayload;
import bisq.common.proto.network.NetworkEnvelope;
import java.net.MalformedURLException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Contacts a list of hosts and asks them for all the data excluding persisted messages. The
* answers are then compiled into buckets of message types. Based on these
* buckets, the Metric reports (for each host) the message types observed and
* their number.
*
* Furthermore, since the DAO is a thing now, the consistency of the DAO state held by each host is assessed and reported.
*
* @author Florian Reimair
*
*/
@Slf4j
public class P2PSeedNodeSnapshot extends P2PSeedNodeSnapshotBase {
final Map<NodeAddress, Statistics<Set<Integer>>> bucketsPerHost = new ConcurrentHashMap<>();
private int daostateheight = 594000;
private int proposalheight = daostateheight;
private int blindvoteheight = daostateheight;
/**
* Use a counter to do statistics.
*/
private static class MyStatistics extends Statistics<Set<Integer>> {
@Override
public synchronized void log(Object message) {
// For logging different data types
String className = message.getClass().getSimpleName();
buckets.putIfAbsent(className, new HashSet<>());
buckets.get(className).add(message.hashCode());
}
}
public P2PSeedNodeSnapshot(Reporter reporter) {
super(reporter);
}
protected List<NetworkEnvelope> getRequests() {
List<NetworkEnvelope> result = new ArrayList<>();
Random random = new Random();
result.add(new PreliminaryGetDataRequest(random.nextInt(), hashes));
result.add(new GetDaoStateHashesRequest(daostateheight, random.nextInt()));
result.add(new GetProposalStateHashesRequest(proposalheight, random.nextInt()));
result.add(new GetBlindVoteStateHashesRequest(blindvoteheight, random.nextInt()));
return result;
}
/**
* Report all the stuff. Uses the configured reporter directly.
*/
void report() {
// report
Map<String, String> report = new HashMap<>();
// - assemble histograms
bucketsPerHost.forEach((host, statistics) -> statistics.values().forEach((type, set) -> report
.put(OnionParser.prettyPrint(host) + ".numberOfMessages." + type, Integer.toString(set.size()))));
// - assemble diffs
// - transfer values
Map<String, Statistics<Set<Integer>>> messagesPerHost = new HashMap<>();
bucketsPerHost.forEach((host, value) -> messagesPerHost.put(OnionParser.prettyPrint(host), value));
// - pick reference seed node and its values
String referenceHost = "overall_number_of_unique_messages";
Map<String, Set<Object>> referenceValues = new HashMap<>();
messagesPerHost.forEach((host, statistics) -> statistics.values().forEach((type, set) -> {
referenceValues.putIfAbsent(type, new HashSet<>());
referenceValues.get(type).addAll(set);
}));
// - calculate diffs
messagesPerHost.forEach(
(host, statistics) -> {
statistics.values().forEach((messageType, set) -> {
try {
report.put(OnionParser.prettyPrint(host) + ".relativeNumberOfMessages." + messageType,
String.valueOf(set.size() - referenceValues.get(messageType).size()));
} catch (MalformedURLException | NullPointerException e) {
log.error("we should never have gotten here", e);
}
});
try {
report.put(OnionParser.prettyPrint(host) + ".referenceHost", referenceHost);
} catch (MalformedURLException ignore) {
log.error("we should never got here");
}
});
// cleanup for next run
bucketsPerHost.forEach((host, statistics) -> statistics.reset());
// when our hash cache exceeds a hard limit, we clear the cache and start anew
if (hashes.size() > 150000)
hashes.clear();
// - report
reporter.report(report, getName());
// - assemble dao report
Map<String, String> daoreport = new HashMap<>();
// - transcode
Map<String, Map<NodeAddress, Tuple>> perType = new HashMap<>();
daoData.forEach((nodeAddress, daostatistics) -> daostatistics.values().forEach((type, tuple) -> {
perType.putIfAbsent(type, new HashMap<>());
perType.get(type).put(nodeAddress, tuple);
}));
// - process dao data
perType.forEach((type, nodeAddressTupleMap) -> {
// - find head
int head = nodeAddressTupleMap.values().stream().max(Comparator.comparingLong(Tuple::getHeight))
.map(value -> (int) value.height)
.orElse(0);
int oldest = nodeAddressTupleMap.values().stream().min(Comparator.comparingLong(Tuple::getHeight))
.map(value -> (int) value.height)
.orElse(0);
// - update queried height
if (type.contains("DaoState"))
daostateheight = oldest - 20;
else if (type.contains("Proposal"))
proposalheight = oldest - 20;
else
blindvoteheight = oldest - 20;
// - calculate diffs
nodeAddressTupleMap.forEach((nodeAddress, tuple) -> daoreport.put(type + "." + OnionParser.prettyPrint(nodeAddress) + ".head", Long.toString(tuple.height - head)));
// - memorize hashes
Map<ByteBuffer, Integer> hitcount = new HashMap<>();
nodeAddressTupleMap.forEach((nodeAddress, tuple) -> {
ByteBuffer hash = ByteBuffer.wrap(tuple.hash);
if (hitcount.containsKey(hash)) {
hitcount.put(hash, hitcount.get(hash) + 1);
} else
hitcount.put(hash, 1);
});
hitcount.clear();
nodeAddressTupleMap.forEach((nodeAddress, tuple) ->
daoreport.put(type + "." + OnionParser.prettyPrint(nodeAddress) + ".hash",
Integer.toString(Arrays.asList(hitcount.entrySet().stream()
.sorted((o1, o2) -> o2.getValue().compareTo(o1.getValue()))
.map(Map.Entry::getKey).toArray()).indexOf(ByteBuffer
.wrap(tuple.hash)))));
// - report reference head
daoreport.put(type + ".referenceHead", Integer.toString(head));
});
daoData.clear();
// - report
reporter.report(daoreport, "DaoStateSnapshot");
}
private static class Tuple {
@Getter
private final long height;
private final byte[] hash;
Tuple(long height, byte[] hash) {
this.height = height;
this.hash = hash;
}
}
private class DaoStatistics extends Statistics<Tuple> {
@Override
public void log(Object message) {
// get last entry
StateHash last = (StateHash) ((GetStateHashesResponse) message).getStateHashes().get(((GetStateHashesResponse) message).getStateHashes().size() - 1);
// For logging different data types
String className = last.getClass().getSimpleName();
buckets.putIfAbsent(className, new Tuple(last.getHeight(), last.getHash()));
}
}
private final Map<NodeAddress, Statistics<Tuple>> daoData = new ConcurrentHashMap<>();
protected boolean treatMessage(NetworkEnvelope networkEnvelope, Connection connection) {
checkNotNull(connection.getPeersNodeAddressProperty(),
"although the property is nullable, we need it to not be null");
if (networkEnvelope instanceof GetDataResponse) {
Statistics result = new MyStatistics();
GetDataResponse dataResponse = (GetDataResponse) networkEnvelope;
final Set<ProtectedStorageEntry> dataSet = dataResponse.getDataSet();
dataSet.forEach(e -> {
final ProtectedStoragePayload protectedStoragePayload = e.getProtectedStoragePayload();
if (protectedStoragePayload == null) {
log.warn("StoragePayload was null: {}", networkEnvelope.toString());
return;
}
result.log(protectedStoragePayload);
});
dataResponse.getPersistableNetworkPayloadSet().forEach(persistableNetworkPayload -> {
// memorize message hashes
//Byte[] bytes = new Byte[persistableNetworkPayload.getHash().length];
//Arrays.setAll(bytes, n -> persistableNetworkPayload.getHash()[n]);
//hashes.add(bytes);
hashes.add(persistableNetworkPayload.getHash());
});
bucketsPerHost.put(connection.getPeersNodeAddressProperty().getValue(), result);
return true;
} else if (networkEnvelope instanceof GetStateHashesResponse) {
daoData.putIfAbsent(connection.getPeersNodeAddressProperty().getValue(), new DaoStatistics());
daoData.get(connection.getPeersNodeAddressProperty().getValue()).log(networkEnvelope);
return true;
}
return false;
}
}

View file

@ -0,0 +1,241 @@
/*
* This file is part of Bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metric;
import bisq.monitor.AvailableTor;
import bisq.monitor.Metric;
import bisq.monitor.Monitor;
import bisq.monitor.OnionParser;
import bisq.monitor.Reporter;
import bisq.monitor.ThreadGate;
import bisq.core.account.witness.AccountAgeWitnessStore;
import bisq.core.proto.network.CoreNetworkProtoResolver;
import bisq.core.proto.persistable.CorePersistenceProtoResolver;
import bisq.core.trade.statistics.TradeStatistics3Store;
import bisq.network.p2p.CloseConnectionMessage;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.MessageListener;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.network.TorNetworkNode;
import bisq.common.app.Version;
import bisq.common.config.BaseCurrencyNetwork;
import bisq.common.persistence.PersistenceManager;
import bisq.common.proto.network.NetworkEnvelope;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import java.time.Clock;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
/**
* Contacts a list of hosts and asks them for all the data excluding persisted messages. The
* answers are then compiled into buckets of message types. Based on these
* buckets, the Metric reports (for each host) the message types observed and
* their number.
*
* @author Florian Reimair
*
*/
@Slf4j
public abstract class P2PSeedNodeSnapshotBase extends Metric implements MessageListener {
private static final String HOSTS = "run.hosts";
private static final String TOR_PROXY_PORT = "run.torProxyPort";
private static final String DATABASE_DIR = "run.dbDir";
final Map<NodeAddress, Statistics<?>> bucketsPerHost = new ConcurrentHashMap<>();
private final ThreadGate gate = new ThreadGate();
protected final Set<byte[]> hashes = new TreeSet<>(Arrays::compare);
/**
* Statistics Interface for use with derived classes.
*
* @param <T> the value type of the statistics implementation
*/
protected abstract static class Statistics<T> {
protected final Map<String, T> buckets = new HashMap<>();
abstract void log(Object message);
Map<String, T> values() {
return buckets;
}
void reset() {
buckets.clear();
}
}
public P2PSeedNodeSnapshotBase(Reporter reporter) {
super(reporter);
}
@Override
public void configure(Properties properties) {
super.configure(properties);
if (hashes.isEmpty() && configuration.getProperty(DATABASE_DIR) != null) {
File dir = new File(configuration.getProperty(DATABASE_DIR));
String networkPostfix = "_" + BaseCurrencyNetwork.values()[Version.getBaseCurrencyNetwork()].toString();
try {
CorePersistenceProtoResolver persistenceProtoResolver = new CorePersistenceProtoResolver(null, null);
//TODO will not work with historical data... should be refactored to re-use code for reading resource files
TradeStatistics3Store tradeStatistics3Store = new TradeStatistics3Store();
PersistenceManager<TradeStatistics3Store> tradeStatistics3PersistenceManager = new PersistenceManager<>(dir,
persistenceProtoResolver, null);
tradeStatistics3PersistenceManager.initialize(tradeStatistics3Store,
tradeStatistics3Store.getDefaultStorageFileName() + networkPostfix,
PersistenceManager.Source.NETWORK);
TradeStatistics3Store persistedTradeStatistics3Store = tradeStatistics3PersistenceManager.getPersisted();
if (persistedTradeStatistics3Store != null) {
tradeStatistics3Store.getMap().putAll(persistedTradeStatistics3Store.getMap());
}
hashes.addAll(tradeStatistics3Store.getMap().keySet().stream()
.map(byteArray -> byteArray.bytes).collect(Collectors.toSet()));
AccountAgeWitnessStore accountAgeWitnessStore = new AccountAgeWitnessStore();
PersistenceManager<AccountAgeWitnessStore> accountAgeWitnessPersistenceManager = new PersistenceManager<>(dir,
persistenceProtoResolver, null);
accountAgeWitnessPersistenceManager.initialize(accountAgeWitnessStore,
accountAgeWitnessStore.getDefaultStorageFileName() + networkPostfix,
PersistenceManager.Source.NETWORK);
AccountAgeWitnessStore persistedAccountAgeWitnessStore = accountAgeWitnessPersistenceManager.getPersisted();
if (persistedAccountAgeWitnessStore != null) {
accountAgeWitnessStore.getMap().putAll(persistedAccountAgeWitnessStore.getMap());
}
hashes.addAll(accountAgeWitnessStore.getMap().keySet().stream()
.map(byteArray -> byteArray.bytes).collect(Collectors.toSet()));
} catch (NullPointerException e) {
// in case there is no store file
log.error("There is no storage file where there should be one: {}", dir.getAbsolutePath());
}
}
}
@Override
protected void execute() {
// start the network node
final NetworkNode networkNode = new TorNetworkNode(Integer.parseInt(configuration.getProperty(TOR_PROXY_PORT, "9054")),
new CoreNetworkProtoResolver(Clock.systemDefaultZone()), false,
new AvailableTor(Monitor.TOR_WORKING_DIR, "unused"), null);
// we do not need to start the networkNode, as we do not need the HS
//networkNode.start(this);
// clear our buckets
bucketsPerHost.clear();
getRequests().forEach(getDataRequest -> send(networkNode, getDataRequest));
report();
}
protected abstract List<NetworkEnvelope> getRequests();
protected void send(NetworkNode networkNode, NetworkEnvelope message) {
ArrayList<Thread> threadList = new ArrayList<>();
// for each configured host
for (String current : configuration.getProperty(HOSTS, "").split(",")) {
threadList.add(new Thread(() -> {
try {
// parse Url
NodeAddress target = OnionParser.getNodeAddress(current);
// do the data request
aboutToSend(message);
SettableFuture<Connection> future = networkNode.sendMessage(target, message);
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(Connection connection) {
connection.addMessageListener(P2PSeedNodeSnapshotBase.this);
}
@Override
public void onFailure(@NotNull Throwable throwable) {
gate.proceed();
log.error(
"Sending {} failed. That is expected if the peer is offline.\n\tException={}", message.getClass().getSimpleName(), throwable.getMessage());
}
}, MoreExecutors.directExecutor());
} catch (Exception e) {
gate.proceed(); // release the gate on error
e.printStackTrace();
}
}, current));
}
gate.engage(threadList.size());
// start all threads and wait until they all finished. We do that so we can
// minimize the time between querying the hosts and therefore the chance of
// inconsistencies.
threadList.forEach(Thread::start);
gate.await();
}
protected void aboutToSend(NetworkEnvelope message) {
}
/**
* Report all the stuff. Uses the configured reporter directly.
*/
abstract void report();
@Override
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
if (treatMessage(networkEnvelope, connection)) {
gate.proceed();
} else if (networkEnvelope instanceof CloseConnectionMessage) {
gate.unlock();
} else {
log.warn("Got an unexpected message of type <{}>",
networkEnvelope.getClass().getSimpleName());
}
connection.removeMessageListener(this);
}
protected abstract boolean treatMessage(NetworkEnvelope networkEnvelope, Connection connection);
}

View file

@ -0,0 +1,165 @@
/*
* This file is part of Bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metric;
import bisq.monitor.Metric;
import bisq.monitor.OnionParser;
import bisq.monitor.Reporter;
import bisq.asset.Asset;
import bisq.asset.AssetRegistry;
import bisq.network.p2p.NodeAddress;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import com.runjva.sourceforge.jsocks.protocol.SocksSocket;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Fetches fee and price data from the configured price nodes.
* Based on the work of HarryMcFinned.
*
* @author Florian Reimair
* @author HarryMcFinned
*
*/
@Slf4j
public class PriceNodeStats extends Metric {
private static final String HOSTS = "run.hosts";
private static final String IGNORE = "dashTxFee ltcTxFee dogeTxFee";
// poor mans JSON parser
private final Pattern stringNumberPattern = Pattern.compile("\"(.+)\" ?: ?(\\d+)");
private final Pattern pricePattern = Pattern.compile("\"price\" ?: ?([\\d.]+)");
private final Pattern currencyCodePattern = Pattern.compile("\"currencyCode\" ?: ?\"([A-Z]+)\"");
private final List<Object> assets = Arrays.asList(new AssetRegistry().stream().map(Asset::getTickerSymbol).toArray());
public PriceNodeStats(Reporter reporter) {
super(reporter);
}
@Override
protected void execute() {
try {
// fetch proxy
Tor tor = Tor.getDefault();
checkNotNull(tor, "tor must not be null");
Socks5Proxy proxy = tor.getProxy();
String[] hosts = configuration.getProperty(HOSTS, "").split(",");
Collections.shuffle(Arrays.asList(hosts));
// for each configured host
for (String current : hosts) {
Map<String, String> result = new HashMap<>();
// parse Url
NodeAddress tmp = OnionParser.getNodeAddress(current);
// connect
try {
SocksSocket socket = new SocksSocket(proxy, tmp.getHostName(), tmp.getPort());
// prepare to receive data
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// ask for fee data
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())));
out.println("GET /getFees/");
out.println();
out.flush();
// sift through the received lines and see if we got something json-like
String line;
while ((line = in.readLine()) != null) {
Matcher matcher = stringNumberPattern.matcher(line);
if (matcher.find())
if (!IGNORE.contains(matcher.group(1)))
result.put("fees." + matcher.group(1), matcher.group(2));
}
in.close();
out.close();
socket.close();
// connect
socket = new SocksSocket(proxy, tmp.getHostName(), tmp.getPort());
// prepare to receive data
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// ask for exchange rate data
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())));
out.println("GET /getAllMarketPrices/");
out.println();
out.flush();
String currencyCode = "";
while ((line = in.readLine()) != null) {
Matcher currencyCodeMatcher = currencyCodePattern.matcher(line);
Matcher priceMatcher = pricePattern.matcher(line);
if (currencyCodeMatcher.find()) {
currencyCode = currencyCodeMatcher.group(1);
if (!assets.contains(currencyCode))
currencyCode = "";
} else if (!"".equals(currencyCode) && priceMatcher.find())
result.put("price." + currencyCode, priceMatcher.group(1));
}
// close all the things
in.close();
out.close();
socket.close();
// report
reporter.report(result, getName());
// only ask for data as long as we got none
if (!result.isEmpty())
break;
} catch (IOException e) {
log.error("{} seems to be down. Trying next configured price node.", tmp.getHostName());
e.printStackTrace();
}
}
} catch (TorCtlException | IOException e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,82 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metric;
import bisq.monitor.Metric;
import bisq.monitor.Monitor;
import bisq.monitor.Reporter;
import bisq.monitor.ThreadGate;
import org.berndpruenster.netlayer.tor.HiddenServiceSocket;
import java.io.File;
import lombok.extern.slf4j.Slf4j;
/**
* A Metric to measure the startup time of a Tor Hidden Service on a already
* running Tor.
*
* @author Florian Reimair
*/
@Slf4j
public class TorHiddenServiceStartupTime extends Metric {
private static final String SERVICE_PORT = "run.servicePort";
private static final String LOCAL_PORT = "run.localPort";
private final String hiddenServiceDirectory = "metric_" + getName();
private final ThreadGate gate = new ThreadGate();
public TorHiddenServiceStartupTime(Reporter reporter) {
super(reporter);
}
@Override
protected void execute() {
// prepare settings. Fetch them every time we run the Metric so we do not have to
// restart on a config update
int localPort = Integer.parseInt(configuration.getProperty(LOCAL_PORT, "9998"));
int servicePort = Integer.parseInt(configuration.getProperty(SERVICE_PORT, "9999"));
// clear directory so we get a new onion address every time
new File(Monitor.TOR_WORKING_DIR + "/" + hiddenServiceDirectory).delete();
log.debug("creating the hidden service");
gate.engage();
// start timer - we do not need System.nanoTime as we expect our result to be in
// the range of tenth of seconds.
long start = System.currentTimeMillis();
HiddenServiceSocket hiddenServiceSocket = new HiddenServiceSocket(localPort, hiddenServiceDirectory,
servicePort);
hiddenServiceSocket.addReadyListener(socket -> {
// stop the timer and report
reporter.report(System.currentTimeMillis() - start, getName());
log.debug("the hidden service is ready");
gate.proceed();
return null;
});
gate.await();
log.debug("going to revoke the hidden service...");
hiddenServiceSocket.close();
log.debug("[going to revoke the hidden service...] done");
}
}

View file

@ -0,0 +1,92 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metric;
import bisq.monitor.Metric;
import bisq.monitor.OnionParser;
import bisq.monitor.Reporter;
import bisq.monitor.StatisticsHelper;
import bisq.network.p2p.NodeAddress;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import com.runjva.sourceforge.jsocks.protocol.SocksSocket;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* A Metric to measure the round-trip time to the Bisq seed nodes via plain tor.
*
* @author Florian Reimair
*/
public class TorRoundTripTime extends Metric {
private static final String SAMPLE_SIZE = "run.sampleSize";
private static final String HOSTS = "run.hosts";
public TorRoundTripTime(Reporter reporter) {
super(reporter);
}
@Override
protected void execute() {
SocksSocket socket;
try {
// fetch proxy
Tor tor = Tor.getDefault();
checkNotNull(tor, "tor must not be null");
Socks5Proxy proxy = tor.getProxy();
// for each configured host
for (String current : configuration.getProperty(HOSTS, "").split(",")) {
// parse Url
NodeAddress tmp = OnionParser.getNodeAddress(current);
List<Long> samples = new ArrayList<>();
while (samples.size() < Integer.parseInt(configuration.getProperty(SAMPLE_SIZE, "1"))) {
// start timer - we do not need System.nanoTime as we expect our result to be in
// seconds time.
long start = System.currentTimeMillis();
// connect
socket = new SocksSocket(proxy, tmp.getHostName(), tmp.getPort());
// by the time we get here, we are connected
samples.add(System.currentTimeMillis() - start);
// cleanup
socket.close();
}
// report
reporter.report(StatisticsHelper.process(samples), getName());
}
} catch (TorCtlException | IOException e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,88 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metric;
import bisq.monitor.Metric;
import bisq.monitor.Reporter;
import org.berndpruenster.netlayer.tor.NativeTor;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import org.berndpruenster.netlayer.tor.Torrc;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Properties;
/**
* A Metric to measure the deployment and startup time of the packaged Tor
* binaries.
*
* @author Florian Reimair
*/
public class TorStartupTime extends Metric {
private static final String SOCKS_PORT = "run.socksPort";
private final File torWorkingDirectory = new File("monitor/work/metric_torStartupTime");
private Torrc torOverrides;
public TorStartupTime(Reporter reporter) {
super(reporter);
}
@Override
public void configure(Properties properties) {
super.configure(properties);
synchronized (this) {
LinkedHashMap<String, String> overrides = new LinkedHashMap<>();
overrides.put("SOCKSPort", configuration.getProperty(SOCKS_PORT, "90500"));
try {
torOverrides = new Torrc(overrides);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
protected void execute() {
// cleanup installation
torWorkingDirectory.delete();
Tor tor = null;
// start timer - we do not need System.nanoTime as we expect our result to be in
// tenth of seconds time.
long start = System.currentTimeMillis();
try {
tor = new NativeTor(torWorkingDirectory, null, torOverrides);
// stop the timer and set its timestamp
reporter.report(System.currentTimeMillis() - start, getName());
} catch (TorCtlException e) {
e.printStackTrace();
} finally {
// cleanup
if (tor != null)
tor.shutdown();
}
}
}

View file

@ -0,0 +1,70 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.reporter;
import bisq.monitor.Reporter;
import bisq.common.app.Version;
import bisq.common.config.BaseCurrencyNetwork;
import java.util.HashMap;
import java.util.Map;
/**
* A simple console reporter.
*
* @author Florian Reimair
*/
public class ConsoleReporter extends Reporter {
@Override
public void report(long value, String prefix) {
HashMap<String, String> result = new HashMap<>();
result.put("", String.valueOf(value));
report(result, prefix);
}
@Override
public void report(long value) {
HashMap<String, String> result = new HashMap<>();
result.put("", String.valueOf(value));
report(result);
}
@Override
public void report(Map<String, String> values, String prefix) {
String timestamp = String.valueOf(System.currentTimeMillis());
values.forEach((key, value) -> {
report(key, value, timestamp, prefix);
});
}
@Override
public void report(String key, String value, String timestamp, String prefix) {
System.err.println("Report: bisq" + (Version.getBaseCurrencyNetwork() != 0 ? "-" + BaseCurrencyNetwork.values()[Version.getBaseCurrencyNetwork()].getNetwork() : "")
+ (prefix.isEmpty() ? "" : "." + prefix)
+ (key.isEmpty() ? "" : "." + key)
+ " " + value + " " + timestamp);
}
@Override
public void report(Map<String, String> values) {
report(values, "");
}
}

View file

@ -0,0 +1,106 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.reporter;
import bisq.monitor.OnionParser;
import bisq.monitor.Reporter;
import bisq.network.p2p.NodeAddress;
import bisq.common.app.Version;
import bisq.common.config.BaseCurrencyNetwork;
import org.berndpruenster.netlayer.tor.TorSocket;
import com.google.common.base.Charsets;
import java.net.Socket;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* Reports our findings to a graphite service.
*
* @author Florian Reimair
*/
public class GraphiteReporter extends Reporter {
@Override
public void report(long value, String prefix) {
HashMap<String, String> result = new HashMap<>();
result.put("", String.valueOf(value));
report(result, prefix);
}
@Override
public void report(long value) {
report(value, "");
}
@Override
public void report(Map<String, String> values, String prefix) {
String timestamp = String.valueOf(System.currentTimeMillis());
values.forEach((key, value) -> {
report(key, value, timestamp, prefix);
try {
// give Tor some slack
// TODO maybe use the pickle protocol?
// https://graphite.readthedocs.io/en/latest/feeding-carbon.html
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
@Override
public void report(String key, String value, String timeInMilliseconds, String prefix) {
// https://graphite.readthedocs.io/en/latest/feeding-carbon.html
String report = "bisq" + (Version.getBaseCurrencyNetwork() != 0 ? "-" + BaseCurrencyNetwork.values()[Version.getBaseCurrencyNetwork()].getNetwork() : "")
+ (prefix.isEmpty() ? "" : "." + prefix)
+ (key.isEmpty() ? "" : "." + key)
+ " " + value + " " + Long.parseLong(timeInMilliseconds) / 1000 + "\n";
try {
NodeAddress nodeAddress = OnionParser.getNodeAddress(configuration.getProperty("serviceUrl"));
Socket socket;
if (nodeAddress.getFullAddress().contains(".onion"))
socket = new TorSocket(nodeAddress.getHostName(), nodeAddress.getPort());
else
socket = new Socket(nodeAddress.getHostName(), nodeAddress.getPort());
socket.getOutputStream().write(report.getBytes(Charsets.UTF_8));
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public void report(Map<String, String> values) {
report(values, "");
}
}

View file

@ -0,0 +1,12 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.berndpruenster.netlayer.tor" level="INFO"/>
</configuration>

View file

@ -0,0 +1,79 @@
## System configuration
# true overwrites the reporters picked by the developers (for debugging for example) (defaults to false)
System.useConsoleReporter=true
# 0 -> BTC_MAINNET, 1 -> BTC_TESTNET (default)
System.baseCurrencyNetwork=0
## Each Metric is configured via a set of properties.
##
## The minimal set of properties required to run a Metric is:
##
## YourMetricName.enabled=true|false
## YourMetricName.run.interval=10 [seconds]
#Edit and uncomment the lines below to your liking
#TorStartupTime Metric
TorStartupTime.enabled=false
TorStartupTime.run.interval=100
TorStartupTime.run.socksPort=90500
TorRoundTripTime.enabled=false
TorRoundTripTime.run.interval=100
TorRoundTripTime.run.sampleSize=3
# torproject.org hidden service
TorRoundTripTime.run.hosts=http://expyuzz4wqqyqhjn.onion:80
#TorHiddenServiceStartupTime Metric
TorHiddenServiceStartupTime.enabled=false
TorHiddenServiceStartupTime.run.interval=100
TorHiddenServiceStartupTime.run.localPort=90501
TorHiddenServiceStartupTime.run.servicePort=90511
#P2PRoundTripTime Metric
P2PRoundTripTime.enabled=false
P2PRoundTripTime.run.interval=100
P2PRoundTripTime.run.sampleSize=5
P2PRoundTripTime.run.hosts=723ljisnynbtdohi.onion:8000, fl3mmribyxgrv63c.onion:8000
P2PRoundTripTime.run.torProxyPort=9060
#P2PNetworkLoad Metric
P2PNetworkLoad.enabled=false
P2PNetworkLoad.run.interval=100
P2PNetworkLoad.run.torProxyPort=9061
P2PNetworkLoad.run.historySize=200
#P2PSeedNodeSnapshotBase Metric
P2PSeedNodeSnapshot.enabled=true
P2PSeedNodeSnapshot.run.dbDir=bisq/p2p/build/resources/main/
P2PSeedNodeSnapshot.run.interval=24
P2PSeedNodeSnapshot.run.hosts=3f3cu2yw7u457ztq.onion:8000, 723ljisnynbtdohi.onion:8000, fl3mmribyxgrv63c.onion:8000
P2PSeedNodeSnapshot.run.torProxyPort=9062
#P2PMarketStats Metric
P2PMarketStats.enabled=false
P2PMarketStats.run.interval=37
P2PMarketStats.run.dbDir=bisq/p2p/build/resources/main/
P2PMarketStats.run.hosts=ef5qnzx6znifo3df.onion:8000
P2PMarketStats.run.torProxyPort=9063
#PriceNodeStats Metric
PriceNodeStats.enabled=false
PriceNodeStats.run.interval=42
PriceNodeStats.run.hosts=http://xc3nh4juf2hshy7e.onion, http://44mgyoe2b6oqiytt.onion, http://62nvujg5iou3vu3i.onion, http://ceaanhbvluug4we6.onion, http://gztmprecgqjq64zh.onion/
#MarketStats Metric
MarketStats.enabled=false
MarketStats.run.interval=191
#Another Metric
Another.run.interval=5
## Reporters are configured via a set of properties as well.
##
## In contrast to Metrics, Reporters do not have a minimal set of properties.
#GraphiteReporter
GraphiteReporter.serviceUrl=k6evlhg44acpchtc.onion:2003

View file

@ -0,0 +1,149 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.monitor.reporter.ConsoleReporter;
import java.util.HashMap;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
import org.junit.Assert;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@Disabled
public class MonitorInfrastructureTests {
/**
* A dummy metric for development purposes.
*/
public class Dummy extends Metric {
public Dummy() {
super(new ConsoleReporter());
}
public boolean active() {
return enabled();
}
@Override
protected void execute() {
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
@ParameterizedTest
@ValueSource(strings = {"empty", "no interval", "typo"})
public void basicConfigurationError(String configuration) {
HashMap<String, Properties> lut = new HashMap<>();
lut.put("empty", new Properties());
Properties noInterval = new Properties();
noInterval.put("Dummy.enabled", "true");
lut.put("no interval", noInterval);
Properties typo = new Properties();
typo.put("Dummy.enabled", "true");
//noinspection SpellCheckingInspection
typo.put("Dummy.run.inteval", "1");
lut.put("typo", typo);
Dummy DUT = new Dummy();
DUT.configure(lut.get(configuration));
Assert.assertFalse(DUT.active());
}
@Test
public void basicConfigurationSuccess() throws Exception {
Properties correct = new Properties();
correct.put("Dummy.enabled", "true");
correct.put("Dummy.run.interval", "1");
Dummy DUT = new Dummy();
DUT.configure(correct);
Assert.assertTrue(DUT.active());
// graceful shutdown
Metric.haltAllMetrics();
}
@Test
public void reloadConfig() throws InterruptedException, ExecutionException {
// our dummy
Dummy DUT = new Dummy();
// a second dummy to run as well
Dummy DUT2 = new Dummy();
DUT2.setName("Dummy2");
Properties dummy2Properties = new Properties();
dummy2Properties.put("Dummy2.enabled", "true");
dummy2Properties.put("Dummy2.run.interval", "1");
DUT2.configure(dummy2Properties);
// disable
DUT.configure(new Properties());
Assert.assertFalse(DUT.active());
Assert.assertTrue(DUT2.active());
// enable
Properties properties = new Properties();
properties.put("Dummy.enabled", "true");
properties.put("Dummy.run.interval", "1");
DUT.configure(properties);
Assert.assertTrue(DUT.active());
Assert.assertTrue(DUT2.active());
// disable again
DUT.configure(new Properties());
Assert.assertFalse(DUT.active());
Assert.assertTrue(DUT2.active());
// enable again
DUT.configure(properties);
Assert.assertTrue(DUT.active());
Assert.assertTrue(DUT2.active());
// graceful shutdown
Metric.haltAllMetrics();
}
@Test
public void shutdown() {
Dummy DUT = new Dummy();
DUT.setName("Dummy");
Properties dummyProperties = new Properties();
dummyProperties.put("Dummy.enabled", "true");
dummyProperties.put("Dummy.run.interval", "1");
DUT.configure(dummyProperties);
try {
Thread.sleep(2000);
Metric.haltAllMetrics();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,118 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.monitor.metric.P2PNetworkLoad;
import bisq.monitor.reporter.ConsoleReporter;
import org.berndpruenster.netlayer.tor.NativeTor;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import java.util.Map;
import java.util.Properties;
import org.junit.Assert;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Test the round trip time metric against the hidden service of tor project.org.
*
* @author Florian Reimair
*/
@Disabled
class P2PNetworkLoadTests {
/**
* A dummy Reporter for development purposes.
*/
private class DummyReporter extends ConsoleReporter {
private Map<String, String> results;
@Override
public void report(long value) {
Assert.fail();
}
Map<String, String> hasResults() {
return results;
}
@Override
public void report(Map<String, String> values) {
Assert.fail();
}
@Override
public void report(long value, String prefix) {
Assert.fail();
}
@Override
public void report(Map<String, String> values, String prefix) {
super.report(values, prefix);
results = values;
}
}
@BeforeAll
static void setup() throws TorCtlException {
// simulate the tor instance available to all metrics
Tor.setDefault(new NativeTor(Monitor.TOR_WORKING_DIR));
}
@Test
void run() throws Exception {
DummyReporter reporter = new DummyReporter();
// configure
Properties configuration = new Properties();
configuration.put("P2PNetworkLoad.enabled", "true");
configuration.put("P2PNetworkLoad.run.interval", "10");
configuration.put("P2PNetworkLoad.run.hosts",
"http://fl3mmribyxgrv63c.onion:8000, http://3f3cu2yw7u457ztq.onion:8000");
Metric DUT = new P2PNetworkLoad(reporter);
// start
DUT.configure(configuration);
// give it some time to start and then stop
while (!DUT.enabled())
Thread.sleep(500);
Thread.sleep(20000);
Metric.haltAllMetrics();
// observe results
Map<String, String> results = reporter.hasResults();
Assert.assertFalse(results.isEmpty());
}
@AfterAll
static void cleanup() {
Tor tor = Tor.getDefault();
checkNotNull(tor, "tor must not be null");
tor.shutdown();
}
}

View file

@ -0,0 +1,136 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.monitor.metric.P2PRoundTripTime;
import bisq.monitor.reporter.ConsoleReporter;
import org.berndpruenster.netlayer.tor.NativeTor;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import java.util.Map;
import java.util.Properties;
import org.junit.Assert;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Test the round trip time metric against the hidden service of tor project.org.
*
* @author Florian Reimair
*/
@Disabled
class P2PRoundTripTimeTests {
/**
* A dummy Reporter for development purposes.
*/
private class DummyReporter extends ConsoleReporter {
private Map<String, String> results;
@Override
public void report(long value) {
Assert.fail();
}
Map<String, String> hasResults() {
return results;
}
@Override
public void report(Map<String, String> values) {
Assert.fail();
}
@Override
public void report(long value, String prefix) {
Assert.fail();
}
@Override
public void report(Map<String, String> values, String prefix) {
super.report(values, prefix);
results = values;
}
}
@BeforeAll
static void setup() throws TorCtlException {
// simulate the tor instance available to all metrics
Tor.setDefault(new NativeTor(Monitor.TOR_WORKING_DIR));
}
@ParameterizedTest
@ValueSource(strings = {"default", "3", "4", "10"})
void run(String sampleSize) throws Exception {
DummyReporter reporter = new DummyReporter();
// configure
Properties configuration = new Properties();
configuration.put("P2PRoundTripTime.enabled", "true");
configuration.put("P2PRoundTripTime.run.interval", "2");
if (!"default".equals(sampleSize))
configuration.put("P2PRoundTripTime.run.sampleSize", sampleSize);
// torproject.org hidden service
configuration.put("P2PRoundTripTime.run.hosts", "http://fl3mmribyxgrv63c.onion:8000");
configuration.put("P2PRoundTripTime.run.torProxyPort", "9052");
Metric DUT = new P2PRoundTripTime(reporter);
// start
DUT.configure(configuration);
// give it some time to start and then stop
while (!DUT.enabled())
Thread.sleep(2000);
Metric.haltAllMetrics();
// observe results
Map<String, String> results = reporter.hasResults();
Assert.assertFalse(results.isEmpty());
Assert.assertEquals(results.get("sampleSize"), sampleSize.equals("default") ? "1" : sampleSize);
Integer p25 = Integer.valueOf(results.get("p25"));
Integer p50 = Integer.valueOf(results.get("p50"));
Integer p75 = Integer.valueOf(results.get("p75"));
Integer min = Integer.valueOf(results.get("min"));
Integer max = Integer.valueOf(results.get("max"));
Integer average = Integer.valueOf(results.get("average"));
Assert.assertTrue(0 < min);
Assert.assertTrue(min <= p25 && p25 <= p50);
Assert.assertTrue(p50 <= p75);
Assert.assertTrue(p75 <= max);
Assert.assertTrue(min <= average && average <= max);
}
@AfterAll
static void cleanup() {
Tor tor = Tor.getDefault();
checkNotNull(tor, "tor must not be null");
tor.shutdown();
}
}

View file

@ -0,0 +1,115 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.monitor.metric.PriceNodeStats;
import org.berndpruenster.netlayer.tor.NativeTor;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import java.io.File;
import java.util.Map;
import java.util.Properties;
import org.junit.Assert;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* @author Florian Reimair
*/
@Disabled
public class PriceNodeStatsTests {
private final static File torWorkingDirectory = new File("monitor/" + PriceNodeStatsTests.class.getSimpleName());
/**
* A dummy Reporter for development purposes.
*/
private class DummyReporter extends Reporter {
private Map<String, String> results;
@Override
public void report(long value) {
Assert.fail();
}
public Map<String, String> results() {
return results;
}
@Override
public void report(Map<String, String> values) {
results = values;
}
@Override
public void report(Map<String, String> values, String prefix) {
report(values);
}
@Override
public void report(String key, String value, String timestamp, String prefix) {
}
@Override
public void report(long value, String prefix) {
report(value);
}
}
@BeforeAll
public static void setup() throws TorCtlException {
// simulate the tor instance available to all metrics
Tor.setDefault(new NativeTor(torWorkingDirectory));
}
@Test
public void connect() {
DummyReporter reporter = new DummyReporter();
Metric DUT = new PriceNodeStats(reporter);
Properties configuration = new Properties();
configuration.put("PriceNodeStats.run.hosts", "http://5bmpx76qllutpcyp.onion");
DUT.configure(configuration);
DUT.execute();
Assert.assertNotNull(reporter.results());
Assert.assertTrue(reporter.results.size() > 0);
}
@AfterAll
public static void cleanup() {
Tor tor = Tor.getDefault();
checkNotNull(tor, "tor must not be null");
tor.shutdown();
torWorkingDirectory.delete();
}
}

View file

@ -0,0 +1,115 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.monitor.metric.TorHiddenServiceStartupTime;
import org.berndpruenster.netlayer.tor.NativeTor;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import java.io.File;
import java.util.Map;
import java.util.Properties;
import org.junit.Assert;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static com.google.common.base.Preconditions.checkNotNull;
@Disabled // Ignore for normal test runs as the tests take lots of time
public class TorHiddenServiceStartupTimeTests {
private final static File torWorkingDirectory = new File("monitor/" + TorHiddenServiceStartupTimeTests.class.getSimpleName());
/**
* A dummy Reporter for development purposes.
*/
private class DummyReporter extends Reporter {
private long result;
@Override
public void report(long value) {
result = value;
}
public long results() {
return result;
}
@Override
public void report(Map<String, String> values) {
report(Long.parseLong(values.values().iterator().next()));
}
@Override
public void report(Map<String, String> values, String prefix) {
report(values);
}
@Override
public void report(String key, String value, String timestamp, String prefix) {
}
@Override
public void report(long value, String prefix) {
report(value);
}
}
@BeforeAll
public static void setup() throws TorCtlException {
// simulate the tor instance available to all metrics
Tor.setDefault(new NativeTor(torWorkingDirectory));
}
@Test
public void run() throws Exception {
DummyReporter reporter = new DummyReporter();
// configure
Properties configuration = new Properties();
configuration.put("TorHiddenServiceStartupTime.enabled", "true");
configuration.put("TorHiddenServiceStartupTime.run.interval", "5");
Metric DUT = new TorHiddenServiceStartupTime(reporter);
// start
DUT.configure(configuration);
// give it some time and then stop
Thread.sleep(180 * 1000);
Metric.haltAllMetrics();
// observe results
Assert.assertTrue(reporter.results() > 0);
}
@AfterAll
public static void cleanup() {
Tor tor = Tor.getDefault();
checkNotNull(tor, "tor must not be null");
tor.shutdown();
torWorkingDirectory.delete();
}
}

View file

@ -0,0 +1,142 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.monitor.metric.TorRoundTripTime;
import org.berndpruenster.netlayer.tor.NativeTor;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import java.io.File;
import java.util.Map;
import java.util.Properties;
import org.junit.Assert;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Test the round trip time metric against the hidden service of tor project.org.
*
* @author Florian Reimair
*/
@Disabled // Ignore for normal test runs as the tests take lots of time
public class TorRoundTripTimeTests {
/**
* A dummy Reporter for development purposes.
*/
private class DummyReporter extends Reporter {
private Map<String, String> results;
@Override
public void report(long value) {
Assert.fail();
}
public Map<String, String> hasResults() {
return results;
}
@Override
public void report(Map<String, String> values) {
results = values;
}
@Override
public void report(Map<String, String> values, String prefix) {
report(values);
}
@Override
public void report(String key, String value, String timestamp, String prefix) {
}
@Override
public void report(long value, String prefix) {
report(value);
}
}
private static final File workingDirectory = new File(TorRoundTripTimeTests.class.getSimpleName());
@BeforeAll
public static void setup() throws TorCtlException {
// simulate the tor instance available to all metrics
Tor.setDefault(new NativeTor(workingDirectory));
}
@ParameterizedTest
@ValueSource(strings = {"default", "3", "4", "10"})
public void run(String sampleSize) throws Exception {
DummyReporter reporter = new DummyReporter();
// configure
Properties configuration = new Properties();
configuration.put("TorRoundTripTime.enabled", "true");
configuration.put("TorRoundTripTime.run.interval", "2");
if (!"default".equals(sampleSize))
configuration.put("TorRoundTripTime.run.sampleSize", sampleSize);
// torproject.org hidden service
configuration.put("TorRoundTripTime.run.hosts", "http://expyuzz4wqqyqhjn.onion:80");
Metric DUT = new TorRoundTripTime(reporter);
// start
DUT.configure(configuration);
// give it some time to start and then stop
Thread.sleep(100);
Metric.haltAllMetrics();
// observe results
Map<String, String> results = reporter.hasResults();
Assert.assertFalse(results.isEmpty());
Assert.assertEquals(results.get("sampleSize"), sampleSize.equals("default") ? "1" : sampleSize);
Integer p25 = Integer.valueOf(results.get("p25"));
Integer p50 = Integer.valueOf(results.get("p50"));
Integer p75 = Integer.valueOf(results.get("p75"));
Integer min = Integer.valueOf(results.get("min"));
Integer max = Integer.valueOf(results.get("max"));
Integer average = Integer.valueOf(results.get("average"));
Assert.assertTrue(0 < min);
Assert.assertTrue(min <= p25 && p25 <= p50);
Assert.assertTrue(p50 <= p75);
Assert.assertTrue(p75 <= max);
Assert.assertTrue(min <= average && average <= max);
}
@AfterAll
public static void cleanup() {
Tor tor = Tor.getDefault();
checkNotNull(tor, "tor must not be null");
tor.shutdown();
workingDirectory.delete();
}
}

View file

@ -0,0 +1,92 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.monitor.metric.TorStartupTime;
import java.util.Map;
import java.util.Properties;
import org.junit.Assert;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@Disabled // Ignore for normal test runs as the tests take lots of time
public class TorStartupTimeTests {
/**
* A dummy Reporter for development purposes.
*/
private class DummyReporter extends Reporter {
private long result;
@Override
public void report(long value) {
result = value;
}
public long results() {
return result;
}
@Override
public void report(Map<String, String> values) {
report(Long.parseLong(values.values().iterator().next()));
}
@Override
public void report(Map<String, String> values, String prefix) {
report(values);
}
@Override
public void report(String key, String value, String timestamp, String prefix) {
}
@Override
public void report(long value, String prefix) {
report(value);
}
}
@Test
public void run() throws Exception {
DummyReporter reporter = new DummyReporter();
// configure
Properties configuration = new Properties();
configuration.put("TorStartupTime.enabled", "true");
configuration.put("TorStartupTime.run.interval", "2");
configuration.put("TorStartupTime.run.socksPort", "9999");
Metric DUT = new TorStartupTime(reporter);
// start
DUT.configure(configuration);
// give it some time and then stop
Thread.sleep(15 * 1000);
Metric.haltAllMetrics();
// TODO Test fails due timing issue
// observe results
Assert.assertTrue(reporter.results() > 0);
}
}