Skip to content
Snippets Groups Projects
Commit 9a1b7c38 authored by Melanie Bruns's avatar Melanie Bruns
Browse files

Merge branch 'merge-improvements-into-v2' into 'release-v2-routing'

Merge improvements into v2

See merge request !142
parents 8ef95c77 4095c61f
No related branches found
No related tags found
1 merge request!142Merge improvements into v2
Pipeline #
Showing
with 1384 additions and 83 deletions
# Add this file as argument if you want to use DisasterRouter
Group.router = DisasterRouter
DisasterRouter.powerThreshold = 0.1
# Parameters for rating mechanisms
......@@ -21,8 +22,10 @@ UtilityMessageChooser.prophetPlusWeight = 0.5
UtilityMessageChooser.rdWeight = 0.35
UtilityMessageChooser.evWeight = 0.15
UtilityMessageChooser.messageUtilityThreshold = 0.45
# TODO: Comment in once implemented (v3)
# UtilityMessageChooser.powerThreshold = 0.1
UtilityMessageChooser.powerThreshold = 0.1
RescueModeMessageChooser.powerThreshold = 0.1
RescueModeMessageChooser.shortTimespanThreshold = 1800
# Parameters for prioritization
DisasterPrioritization.headStartThreshold = 300
......@@ -30,6 +33,5 @@ DisasterPrioritization.priorityThreshold = 5
DisasterPrioritization.dpWeight = 0.5
# Parameters for buffer management
# ! TODO: Uncomment this once buffer management is implemented !
# DisasterBufferComparator.hopThreshold = 5
# DisasterBufferComparator.ageThreshold = 300
\ No newline at end of file
DisasterBufferComparator.hopThreshold = 6
DisasterBufferComparator.ageThreshold = 300
\ No newline at end of file
......@@ -22,6 +22,9 @@ Group.bufferSize = 50M
Group.nrofInterfaces = 1
Group.interface1 = wInterface
Group.net.scanInterval = 30
#After which time messages get dropped (in minutes)
#360 minutes = 6 hours
Group.msgTtl = 360
#How often nodes in the group will reorder the messages in their buffer
Group.MessageOrderingInterval=2.0
# Time the node will help at a disaster site (in seconds)
......@@ -191,7 +194,7 @@ MapBasedMovement.mapFile2 = data/paderborn_pedestrians_and_cars.wkt
MapBasedMovement.mapFile3 = data/paderborn_cars_only.wkt
## Reports - all report names have to be valid report classes
Report.nrofReports = 7
Report.nrofReports = 8
Report.reportDir = reports/
# length of the warm up period (simulated seconds)
Report.warmup = 0
......@@ -204,6 +207,8 @@ Report.report6 = DataSyncReport
DataSyncReport.precision=2
Report.report7 = EnergyLevelReport
EnergyLevelReport.granularity = 600
Report.report8 = BufferOccupancyReport
BufferOccupancyReport.occupancyInterval = 300
## Optimization settings -- these affect the speed of the simulation
## see World class for details.
......
......@@ -253,15 +253,54 @@ public class DatabaseApplication extends Application implements DisasterDataCrea
}
}
// Then create data messages out of the data items.
return this.createDataMessagePrototypes(interestingData);
}
/**
* Creates database synchronization messages from existing useful data that has been modified recently.
*
* @param databaseOwner The DTNHost this instance of the application is attached to.
* @param maximumNumberSecondsSinceModification The maximum number of seconds since last modification.
* @return The created messages. They don't have a receiver yet, so {@link Message#getTo()} will return null.
*/
public List<DataMessage> wrapRecentUsefulDataIntoMessages(
DTNHost databaseOwner, int maximumNumberSecondsSinceModification) {
// If we don't know who the application is attached to yet, use the new knowledge for initialization.
if (!this.isInitialized()) {
this.initialize(databaseOwner);
}
// Find all interesting data which has been modified recently.
List<Tuple<DisasterData, Double>> recentData = new ArrayList<>();
for (Tuple<DisasterData, Double> dataWithUtility :
this.database.getAllNonMapDataWithMinimumUtility(this.utilityThreshold)) {
if (SimClock.getTime() - dataWithUtility.getKey().getCreation() <= maximumNumberSecondsSinceModification) {
recentData.add(dataWithUtility);
}
}
// Then create data messages out of the data items.
return this.createDataMessagePrototypes(recentData);
}
/**
* Creates a {@link DataMessage} prototype out of every {@link #itemsPerMessage} data items, i.e. a data message
* without a receiver. The data items are sorted by utility to group them into messages.
*
* @param data The data to wrap.
* @return The created messages.
*/
private List<DataMessage> createDataMessagePrototypes(List<Tuple<DisasterData, Double>> data) {
// Sort data items by utility.
interestingData.sort(Comparator.comparingDouble(t -> (-1) * t.getValue()));
data.sort(Comparator.comparingDouble(t -> (-1) * t.getValue()));
// Then create a message out of every x data items.
List<DataMessage> messages = new ArrayList<>(data.size());
DTNHost unknownReceiver = null;
List<DataMessage> messages = new ArrayList<>(interestingData.size());
for (int i = 0; i < interestingData.size(); i += itemsPerMessage) {
int firstIndexNotToSent = Math.min(i + itemsPerMessage, interestingData.size());
List<Tuple<DisasterData, Double>> subsetToSent = interestingData.subList(i, firstIndexNotToSent);
for (int i = 0; i < data.size(); i += this.itemsPerMessage) {
int firstIndexNotToSent = Math.min(i + this.itemsPerMessage, data.size());
List<Tuple<DisasterData, Double>> subsetToSent = data.subList(i, firstIndexNotToSent);
DataMessage message = new DataMessage(
this.host, unknownReceiver,
this.createMessageId(subsetToSent), subsetToSent,
......@@ -269,8 +308,6 @@ public class DatabaseApplication extends Application implements DisasterDataCrea
message.setAppID(APP_ID);
messages.add(message);
}
// And return all messages.
return messages;
}
......@@ -427,5 +464,4 @@ public class DatabaseApplication extends Application implements DisasterDataCrea
public Map<DisasterData.DataType, Double> getRatioOfItemsPerDataType(){
return database.getRatioOfItemsPerDataType();
}
}
......@@ -100,6 +100,13 @@ public class LocalDatabase {
dataIterator.remove();
}
}
// Make sure to set used size to 0 if database is empty. Without this check, an empty database could use up a
// positive amount of size due to rounding errors.
// This behavior is important when (indirectly) using this field to check whether the database is empty.
if (this.data.isEmpty()) {
this.usedSize = 0;
}
}
/**
......
......@@ -42,7 +42,7 @@ public class Message implements Comparable<Message> {
private double timeReceived;
/** The time when this message was created */
private double timeCreated;
/** Initial TTL of the message */
/** Initial TTL of the message in minutes */
private int initTtl;
/**
......
package core;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
/**
* Message which should be delivered to a certain group of nodes
*
......@@ -12,6 +17,11 @@ public class MulticastMessage extends Message {
*/
private Group group;
/**
* Recipients that have been reached on this message copy's path.
*/
private HashSet<Integer> reachedRecipients = new HashSet<>();
/**
* Creates a new Message.
*
......@@ -42,6 +52,7 @@ public class MulticastMessage extends Message {
" but host "+ from + " is not " + to);
}
this.group = to;
this.reachedRecipients.add(from.getAddress());
}
/**
......@@ -63,6 +74,21 @@ public class MulticastMessage extends Message {
return group;
}
/**
* Gets the addresses of all group members that haven't been passed on this message copy's path so far.
*
* @return Addresses of all group members that haven't been reached so far.
*/
public Collection<Integer> getRemainingRecipients() {
List<Integer> remainingRecipients = new ArrayList<>();
for (Integer address : this.group.getMembers()) {
if (!this.reachedRecipients.contains(address)) {
remainingRecipients.add(address);
}
}
return remainingRecipients;
}
/**
* Determines whether the provided node is a final recipient of the message.
* @param host Node to check.
......@@ -80,7 +106,33 @@ public class MulticastMessage extends Message {
*/
@Override
public boolean completesDelivery(DTNHost receiver) {
return false;
// Check whether all hosts have already been reached.
int groupSize = this.group.getMembers().length;
if (this.reachedRecipients.size() == groupSize) {
return true;
}
// Then check if only the current receiver is missing.
return this.reachedRecipients.size() == groupSize - 1
&& this.isFinalRecipient(receiver)
&& !this.reachedRecipients.contains(receiver.getAddress());
}
/**
* Adds a new node on the list of nodes this message has passed
*
* @param node The node to add
*/
@Override
public void addNodeOnPath(DTNHost node) {
super.addNodeOnPath(node);
// Only add a reached recipient if we are not in initialization.
// We cannot add reached recipients in initialization because the necessary fields are not yet set. However,
// adding the single host this message was created by is handled in constructor.
if (this.reachedRecipients != null && this.isFinalRecipient(node)) {
this.reachedRecipients.add(node.getAddress());
}
}
/**
......@@ -111,6 +163,7 @@ public class MulticastMessage extends Message {
public void copyFrom(MulticastMessage m){
super.copyFrom(m);
this.group = m.group;
this.reachedRecipients = new HashSet<>(m.reachedRecipients);
}
/**
......
......@@ -7,7 +7,7 @@ package report;
/**
* Records the average buffer occupancy and its variance with format:
* <p>
* [Simulation time] [average buffer occupancy % [0..100] ] [variance]
* [Simulation time] [average buffer occupancy % [0..100] ] [variance] [min] [max]
* </p>
*
* <p>
......@@ -71,19 +71,23 @@ public class BufferOccupancyReport extends Report implements UpdateListener {
private void printLine(List<DTNHost> hosts) {
double bufferOccupancy = 0.0;
double bo2 = 0.0;
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for (DTNHost h : hosts) {
double tmp = h.getBufferOccupancy();
tmp = (tmp<=100.0)?(tmp):(100.0);
bufferOccupancy += tmp;
bo2 += (tmp*tmp)/100.0;
min = Math.min(min, tmp);
max = Math.max(max, tmp);
}
double E_X = bufferOccupancy / hosts.size();
double Var_X = bo2 / hosts.size() - (E_X*E_X)/100.0;
String output = format(SimClock.getTime()) + " " + format(E_X) + " " +
format(Var_X);
format(Var_X) + " " + format(min) + " " + format(max);
write(output);
}
......
......@@ -5,8 +5,8 @@ import core.DTNHost;
import core.Message;
import core.MessageListener;
import core.Settings;
import core.SettingsError;
import core.SimClock;
import routing.choosers.EpidemicMessageChooser;
import routing.choosers.UtilityMessageChooser;
import routing.prioritizers.DisasterPrioritizationStrategy;
import routing.prioritizers.PrioritySorter;
......@@ -29,6 +29,15 @@ import java.util.List;
* Created by Britta Heymann on 19.05.2017.
*/
public class DisasterRouter extends ActiveRouter {
/** Namespace for all general disaster router settings. */
public static final String DISASTER_ROUTER_NS = "DisasterRouter";
/**
* If the host's relative power is below this threshold, it will change into a rescue mode. -setting id ({@value}).
* In resuce mode, the host tries to save all its messages and recent data from deletion by sending them out.
*/
public static final String POWER_THRESHOLD = "powerThreshold";
/* Comparators to sort direct messages. */
private Comparator<Message> directMessageComparator;
private Comparator<Tuple<Message, Connection>> directMessageTupleComparator;
......@@ -54,6 +63,26 @@ public class DisasterRouter extends ActiveRouter {
*/
private List<Tuple<Message, Connection>> cachedNonDirectMessages = new ArrayList<>();
/**
* If the {@link DTNHost}'s relative power is below this threshold, it will change into a rescue mode in which
* it tries to save all its messages and recent data from deletion by sending them out.
*/
private double powerThreshold;
/**
* Number of tuples of messages/hosts which are remembered, such that they are not sent again
*/
private static final int MESSAGE_HISTORY_SIZE = 1000;
/**
* Constant indicating that a message is not sent because it is contained in the history
*/
private static final int DENIED_IN_HISTORY = -110;
/**
* List storing the last x message IDs and host IDs that are not sent again. The size of the list is restricted to {@link #MESSAGE_HISTORY_SIZE}.
*/
private List<Tuple<String, Integer>> messageSentToHostHistory = new ArrayList<>();
/**
* Initializes a new instance of the {@link DisasterRouter} class.
* @param s Settings to use.
......@@ -73,6 +102,14 @@ public class DisasterRouter extends ActiveRouter {
this.messagePrioritizer = new DisasterPrioritizationStrategy(this);
this.directMessageComparator = new PrioritySorter();
this.directMessageTupleComparator = new PriorityTupleSorter();
// Read power threshold from settings.
s.setNameSpace(DISASTER_ROUTER_NS);
this.powerThreshold = s.getDouble(POWER_THRESHOLD);
if (this.powerThreshold < 0 || this.powerThreshold > 1) {
throw new SettingsError("Power threshold should be in [0, 1], but is " + this.powerThreshold + "!");
}
s.restoreNameSpace();
}
/**
......@@ -93,6 +130,22 @@ public class DisasterRouter extends ActiveRouter {
this.messagePrioritizer = router.messagePrioritizer.replicate(this);
this.directMessageComparator = router.directMessageComparator;
this.directMessageTupleComparator = router.directMessageTupleComparator;
// Copy power threshold.
this.powerThreshold = router.powerThreshold;
}
/**
* Checks if the router is a {@link DisasterRouter} and throws an {@link IllegalArgumentException} if it isn't.
* @param router Router to check
*/
public static void checkRouterIsDisasterRouter(MessageRouter router) {
if (router == null) {
throw new IllegalArgumentException("Router is null!");
}
if (!(router instanceof DisasterRouter)) {
throw new IllegalArgumentException("Cannot handle routers of type " + router.getClass() + "!");
}
}
/**
......@@ -185,6 +238,17 @@ public class DisasterRouter extends ActiveRouter {
this.tryOtherMessages();
}
/**
* Method is called just before a transfer is finalized
* at {@link #update()}.
* Subclasses that are interested of the event may want to override this.
* @param con The connection whose transfer was finalized
*/
@Override
protected void transferDone(Connection con) {
addMessageAndHostToHistory(con.getMessage(), con.getOtherNode(getHost()));
}
/**
* Checks whether this router has anything to send out.
*
......@@ -253,6 +317,22 @@ public class DisasterRouter extends ActiveRouter {
return super.removeFromMessages(id);
}
/**
* Adds a message / host pair to the message history. Also deletes messages until the list has reached its
* maximum size
* @param message: Message to be added
* @param host: Host to be added
*/
private void addMessageAndHostToHistory(Message message, DTNHost host) {
Tuple<String, Integer> historyItem = new Tuple<>(message.getId(), host.getAddress());
while (this.messageSentToHostHistory.size() >= MESSAGE_HISTORY_SIZE) {
this.messageSentToHostHistory.remove(this.messageSentToHostHistory.size() - 1);
}
this.messageSentToHostHistory.add(0, historyItem);
}
/**
* Computes a ratio between the encounter value of this router and the one of the provided router.
* A ratio less than 0.5 signifies that the other host is less social than this one, a
......@@ -313,4 +393,38 @@ public class DisasterRouter extends ActiveRouter {
messages.sort(this.directMessageComparator);
return messages;
}
@Override
protected int startTransfer(Message m, Connection con) {
if (messageSentToHostHistory.contains(new Tuple<String, Integer>(m.getId(),
con.getOtherNode(getHost()).getAddress()))) {
return DENIED_IN_HISTORY;
}
return super.startTransfer(m, con);
}
/**
* Returns the power threshold.
* @return The power threshold.
*/
public double getPowerThreshold() {
return this.powerThreshold;
}
/**
* Returns the last {@link #MESSAGE_HISTORY_SIZE} messages that were already sent by this host
* @return message history
*/
public List<Tuple<String, Integer>> getMessageSentToHostHistory() {
return new ArrayList<>(messageSentToHostHistory);
}
/**
* Returns the number of messages in the history
* @return size of the history
*/
public static int getMessageHistorySize() {
return MESSAGE_HISTORY_SIZE;
}
}
package routing.choosers;
import core.Connection;
import core.DTNHost;
import core.Message;
import core.Settings;
import core.SettingsError;
import routing.DisasterRouter;
import routing.MessageChoosingStrategy;
import routing.MessageRouter;
import routing.util.DatabaseApplicationUtil;
import util.Tuple;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* A host with very low power enters a "RESCUE"-mode: To prevent data-loss it will try to get rid of all messages it is
* transporting. To save the messages they will be given to any other encountered node regardless of whether it is a
* good or bad relay.
*
* Created by Britta Heymann on 23.07.2017.
*/
public class RescueModeMessageChooser implements MessageChoosingStrategy{
/** Namespace for all rescue mode message chooser settings. */
public static final String RESCUE_MODE_MESSAGE_CHOOSER_NS = "RescueModeMessageChooser";
/**
* If a neighbor's relative power level is below this threshold, no messages will be sent. -setting id ({@value}).
*/
public static final String POWER_THRESHOLD = "powerThreshold";
/**
* The number of seconds a data item modification counts as recent. -setting id ({@value}).
*/
public static final String SHORT_TIMESPAN_THRESHOLD = "shortTimespanThreshold";
/** If a neighbor's relative power level is below this threshold, no messages will be sent. */
private double powerThreshold;
/** The number of seconds a data item modification counts as recent. */
private int shortTimespanThreshold;
/** Host choosing the messages. */
private DTNHost attachedHost;
/**
* Initializes a new instance of the {@link RescueModeMessageChooser} class.
*/
public RescueModeMessageChooser() {
// Read settings:
Settings s = new Settings(RESCUE_MODE_MESSAGE_CHOOSER_NS);
this.powerThreshold = s.getDouble(POWER_THRESHOLD);
this.shortTimespanThreshold = s.getInt(SHORT_TIMESPAN_THRESHOLD);
s.restoreNameSpace();
// Check thresholds are valid.
if (this.powerThreshold < 0 || this.powerThreshold > 1) {
throw new SettingsError("Power threshold must be in [0, 1], but is " + this.powerThreshold + "!");
}
if (this.shortTimespanThreshold < 0) {
throw new SettingsError(
"Short timespan threshold must be natural, but is " + this.shortTimespanThreshold + "!");
}
}
/** Copy constructor. */
private RescueModeMessageChooser(RescueModeMessageChooser chooser) {
this.powerThreshold = chooser.powerThreshold;
this.shortTimespanThreshold = chooser.shortTimespanThreshold;
}
/**
* Chooses non-direct messages to send.
*
* @param messages All messages in buffer.
* @param connections All connections the host has.
* @return Which messages should be send to which neighbors.
*/
@Override
public Collection<Tuple<Message, Connection>> chooseNonDirectMessages(
Collection<Message> messages, List<Connection> connections) {
Collection<Tuple<Message, Connection>> chosenMessages = new ArrayList<>();
List<Connection> availableConnections = new ArrayList<>();
// Add ordinary messages: Send everything to all available connections.
for (Connection con : connections) {
DTNHost neighbor = con.getOtherNode(this.attachedHost);
DisasterRouter.checkRouterIsDisasterRouter(neighbor.getRouter());
DisasterRouter neighborRouter = (DisasterRouter)neighbor.getRouter();
if (neighborRouter.isTransferring() || neighborRouter.remainingEnergyRatio() < this.powerThreshold) {
continue;
}
availableConnections.add(con);
for (Message m : messages) {
if (!m.isFinalRecipient(neighbor) && !neighborRouter.hasMessage(m.getId())) {
chosenMessages.add(new Tuple<>(m, con));
}
}
}
// Wrap useful data stored at host which has been modified recently into data messages to available neighbors
// and add them to the messages to sent.
chosenMessages.addAll(DatabaseApplicationUtil.wrapRecentUsefulDataIntoMessages(
this.attachedHost, availableConnections, this.shortTimespanThreshold));
return chosenMessages;
}
/**
* Creates a replicate of this message choosing strategy. The replicate has the same settings as this message
* choosing strategy but is attached to the provided router and has no attached host.
*
* @param attachedRouter Router choosing the messages.
* @return The replicate.
*/
@Override
public MessageChoosingStrategy replicate(MessageRouter attachedRouter) {
return new RescueModeMessageChooser(this);
}
/**
* Sets the attached host.
*
* @param host host choosing the messages.
*/
@Override
public void setAttachedHost(DTNHost host) {
this.attachedHost = host;
}
/**
* Gets the power threshold.
* @return The power threshold.
*/
public double getPowerThreshold() {
return powerThreshold;
}
/**
* Gets the short timespan threshold.
* @return The short timespan threshold.
*/
public int getShortTimespanThreshold() {
return shortTimespanThreshold;
}
}
......@@ -73,6 +73,11 @@ public class UtilityMessageChooser implements MessageChoosingStrategy {
*/
public static final String UTILITY_THRESHOLD = "messageUtilityThreshold";
/**
* If a neighbor's relative power level is below this threshold, no messages will be sent. -setting id ({@value}).
*/
public static final String POWER_THRESHOLD = "powerThreshold";
/** Acceptable difference of weight sums to 1. */
private static final double SUM_EQUALS_ONE_DELTA = 0.00001;
......@@ -85,6 +90,9 @@ public class UtilityMessageChooser implements MessageChoosingStrategy {
/** Utility threshold above which messages are sent. */
private double utilityThreshold;
/** If a neighbor's relative power level is below this threshold, no messages will be sent. */
private double powerThreshold;
/** Router choosing the messages. */
private DisasterRouter attachedRouter;
/** Host choosing the messages. */
......@@ -112,16 +120,22 @@ public class UtilityMessageChooser implements MessageChoosingStrategy {
// Read thresholds.
this.utilityThreshold = s.getDouble(UTILITY_THRESHOLD);
// TODO: power threshold (protocol v3).
this.powerThreshold = s.getDouble(POWER_THRESHOLD);
// Restore old settings namespace.
s.restoreNameSpace();
// Check all values are valid.
this.validateWeights();
if (this.utilityThreshold < 0 || this.utilityThreshold > 1) {
throw new SettingsError("Utility threshold must be in [0, 1], but is " + this.utilityThreshold + "!");
}
if (this.powerThreshold < 0 || this.powerThreshold > 1) {
throw new SettingsError("Power threshold must be in [0, 1], but is " + this.powerThreshold + "!");
}
// Then set attached router.
UtilityMessageChooser.checkRouterIsDisasterRouter(attachedRouter);
DisasterRouter.checkRouterIsDisasterRouter(attachedRouter);
this.attachedRouter = (DisasterRouter)attachedRouter;
}
......@@ -136,9 +150,9 @@ public class UtilityMessageChooser implements MessageChoosingStrategy {
this.replicationsDensityWeight = chooser.replicationsDensityWeight;
this.encounterValueWeight = chooser.encounterValueWeight;
this.utilityThreshold = chooser.utilityThreshold;
// TODO: power threshold (protocol v3)
this.powerThreshold = chooser.powerThreshold;
UtilityMessageChooser.checkRouterIsDisasterRouter(attachedRouter);
DisasterRouter.checkRouterIsDisasterRouter(attachedRouter);
this.attachedRouter = (DisasterRouter)attachedRouter;
}
......@@ -172,20 +186,6 @@ public class UtilityMessageChooser implements MessageChoosingStrategy {
}
}
/**
* Checks if the router is a {@link DisasterRouter} and throws an {@link IllegalArgumentException} if it isn't.
* @param router Router to check
*/
private static void checkRouterIsDisasterRouter(MessageRouter router) {
if (router == null) {
throw new IllegalArgumentException("Router is null!");
}
if (!(router instanceof DisasterRouter)) {
throw new IllegalArgumentException(
"Utility message chooser cannot handle routers of type " + router.getClass() + "!");
}
}
/**
* Sets the attached host.
*
......@@ -207,12 +207,17 @@ public class UtilityMessageChooser implements MessageChoosingStrategy {
public Collection<Tuple<Message, Connection>> chooseNonDirectMessages(
Collection<Message> messages, List<Connection> connections) {
Collection<Tuple<Message, Connection>> chosenMessages = new ArrayList<>();
List<Connection> relevantConnections = new ArrayList<>();
// Add ordinary messages.
for (Connection con : connections) {
DTNHost neighbor = con.getOtherNode(this.attachedHost);
DisasterRouter.checkRouterIsDisasterRouter(neighbor.getRouter());
DisasterRouter neighborRouter = (DisasterRouter)neighbor.getRouter();
relevantConnections.add(con);
for (Message m : messages) {
if (!m.isFinalRecipient(neighbor) && this.shouldBeSent(m, neighbor)) {
if (!m.isFinalRecipient(neighbor) && this.shouldBeSent(m, neighborRouter)) {
chosenMessages.add(new Tuple<>(m, con));
}
}
......@@ -220,7 +225,7 @@ public class UtilityMessageChooser implements MessageChoosingStrategy {
// Wrap useful data stored at host in data messages to neighbors and add them to the messages to sent.
chosenMessages.addAll(DatabaseApplicationUtil.wrapUsefulDataIntoMessages(
this.attachedHost.getRouter(), this.attachedHost, connections));
this.attachedHost.getRouter(), this.attachedHost, relevantConnections));
return chosenMessages;
}
......@@ -239,20 +244,15 @@ public class UtilityMessageChooser implements MessageChoosingStrategy {
}
/**
* Determines whether the provided message should be sent to the provided host right now.
* This is only the case if the host is not transferring, does not know the message yet, and the message - host
* Determines whether the provided message should be sent to the provided router right now.
* This is only the case if the router does not know the message yet, and the message - host
* pair's utility is sufficiently high.
* @param m Message to check.
* @param otherHost Host to check.
* @param otherRouter Router to check.
* @return True iff the message should be sent.
*/
private boolean shouldBeSent(Message m, DTNHost otherHost) {
UtilityMessageChooser.checkRouterIsDisasterRouter(otherHost.getRouter());
DisasterRouter otherRouter = (DisasterRouter)otherHost.getRouter();
return !otherRouter.isTransferring()
&& !otherRouter.hasMessage(m.getId())
&& this.computeUtility(m, otherRouter) > this.utilityThreshold;
// TODO: also check other's energy (routing protocol v3)
private boolean shouldBeSent(Message m, DisasterRouter otherRouter) {
return !otherRouter.hasMessage(m.getId()) && this.computeUtility(m, otherRouter) > this.utilityThreshold;
}
/**
......@@ -295,6 +295,14 @@ public class UtilityMessageChooser implements MessageChoosingStrategy {
return utilityThreshold;
}
/**
* Gets the power threshold.
* @return The power threshold.
*/
public double getPowerThreshold() {
return powerThreshold;
}
/**
* Creates a replicate of this message choosing strategy. The replicate has the same settings as this message
* choosing strategy but is attached to the provided router and has no attached host.
......
......@@ -80,7 +80,7 @@ public class DisasterPrioritizationStrategy implements MessagePrioritizationStra
"Priority threshold must be non-negative, but is " + this.priorityThreshold + "!");
}
DisasterPrioritizationStrategy.checkRouterIsDisasterRouter(attachedRouter);
DisasterRouter.checkRouterIsDisasterRouter(attachedRouter);
this.nonHeadStartPrioritization = new DisasterPrioritization(s, (DisasterRouter)attachedRouter);
}
......@@ -93,25 +93,11 @@ public class DisasterPrioritizationStrategy implements MessagePrioritizationStra
this.headStartThreshold = strategy.headStartThreshold;
this.priorityThreshold = strategy.priorityThreshold;
DisasterPrioritizationStrategy.checkRouterIsDisasterRouter(attachedRouter);
DisasterRouter.checkRouterIsDisasterRouter(attachedRouter);
this.nonHeadStartPrioritization =
new DisasterPrioritization(strategy.nonHeadStartPrioritization, (DisasterRouter)attachedRouter);
}
/**
* Checks if the router is a {@link DisasterRouter} and throws an {@link IllegalArgumentException} if it isn't.
* @param router Router to check
*/
private static void checkRouterIsDisasterRouter(MessageRouter router) {
if (router == null) {
throw new IllegalArgumentException("Router is null!");
}
if (!(router instanceof DisasterRouter)) {
throw new IllegalArgumentException(
"Disaster prioritization strategy cannot handle routers of type " + router.getClass() + "!");
}
}
/**
* Sets the attached host.
*
......
......@@ -57,6 +57,46 @@ public final class DatabaseApplicationUtil {
List<DataMessage> messagePrototypes = application.wrapUsefulDataIntoMessages(host);
// ... and create real instances for each of them:
return DatabaseApplicationUtil.wrapPrototypesIntoConcreteMessages(messagePrototypes, connections, host);
}
/**
* Fetches database synchronization messages from the provided host which are made up of data items which have been
* modified recently.
*
* @param host The {@link DTNHost} to do this for.
* @param connections Connections to find data messages for.
* @param maximumNumberSecondsSinceModification The maximum number of seconds since the modification of the wrapped
* data items.
* @return The created messages and the connection they should be sent over.
*/
public static List<Tuple<Message, Connection>> wrapRecentUsefulDataIntoMessages(
DTNHost host, List<Connection> connections, int maximumNumberSecondsSinceModification) {
// First find out if we even have a database application.
DatabaseApplication application = DatabaseApplicationUtil.findDatabaseApplication(host.getRouter());
if (application == null) {
return new ArrayList<>(0);
}
// Then fetch prototypes of messages containing recently modified useful data items...
List<DataMessage> messagePrototypes =
application.wrapRecentUsefulDataIntoMessages(host, maximumNumberSecondsSinceModification);
// ... and create real instances for each of them:
return DatabaseApplicationUtil.wrapPrototypesIntoConcreteMessages(messagePrototypes, connections, host);
}
/**
* Creates real messages out of each {@link DataMessage} prototype by instantiating them for every receiver provided
* by the provided connections.
*
* @param messagePrototypes The {@link DataMessage} prototypes.
* @param connections All connections for which messages should be generated.
* @param host The sending {@link DTNHost}.
* @return Instantiated messages with explicitly given connection.
*/
private static List<Tuple<Message, Connection>> wrapPrototypesIntoConcreteMessages(
List<DataMessage> messagePrototypes, List<Connection> connections, DTNHost host) {
List<Tuple<Message, Connection>> messages = new ArrayList<>(messagePrototypes.size() * connections.size());
for (DataMessage dataMessage : messagePrototypes) {
// For each receiver ...
......@@ -69,7 +109,6 @@ public final class DatabaseApplicationUtil {
}
}
// Finally return all messages.
return messages;
}
......
......@@ -254,7 +254,7 @@ public class DeliveryPredictabilityStorage extends AbstractIntervalRatingMechani
return this.getDeliveryPredictability(message.getTo());
case MULTICAST:
MulticastMessage multicast = (MulticastMessage)message;
return this.getMaximumDeliveryPredictability(multicast.getGroup().getMembers());
return this.getMaximumDeliveryPredictability(multicast.getRemainingRecipients());
default:
throw new IllegalArgumentException(
"No delivery predictability for messages of type " + message.getType() + " defined!");
......@@ -266,7 +266,7 @@ public class DeliveryPredictabilityStorage extends AbstractIntervalRatingMechani
* @param addresses The addresses to check the delivery predictability for.
* @return The maximum delivery predictability.
*/
private double getMaximumDeliveryPredictability(Integer[] addresses) {
private double getMaximumDeliveryPredictability(Collection<Integer> addresses) {
double maxDeliveryPred = 0;
for (int address : addresses) {
maxDeliveryPred = Math.max(maxDeliveryPred, this.getDeliveryPredictability(address));
......
package routing.util;
import core.BroadcastMessage;
import core.Message;
import core.Settings;
import core.SimClock;
import routing.DisasterRouter;
import routing.MessageRouter;
import java.util.Comparator;
import java.util.HashMap;
/**
* Compares two messages deciding which one should be deleted first, if the need occurs.
*
* Created by Britta Heymann on 21.07.2017.
*/
// Suppress warning about unserializable comparator because the rule's assumption that the overhead to make it
// serializable is low does not fit for this one (routers are not serializable!).
@SuppressWarnings({"squid:S2063"})
public class DisasterBufferComparator implements Comparator<Message> {
/** Namespace for all disaster buffer comparator settings. */
public static final String DISASTER_BUFFER_NS = "DisasterBufferComparator";
/**
* Exclusive maximum number of hops for which a message may be assigned a high rank -setting id ({@value}).
* Messages which meet this threshold and the age threshold {@link #AGE_THRESHOLD_S} are deleted only after the
* messages not meeting one of these two thresholds.
*/
public static final String HOP_THRESHOLD_S = "hopThreshold";
/**
* Exclusive maximum time span for which a message may be assigned a high rank -setting id ({@value}.
* Messages which meet this threshold and the hop threshold {@link #HOP_THRESHOLD_S} are deleted only after the
* messages not meeting one of these two thresholds.
*/
public static final String AGE_THRESHOLD_S = "ageThreshold";
/**
* Exclusive maximum number of hops for which a message may be assigned a high rank.
* Messages which meet this threshold and the age threshold {@link #ageThreshold} are deleted only after the
* messages not meeting one of these two thresholds.
*/
private int hopThreshold;
/**
* Exclusive maximum time span for which a message may be assigned a high rank -setting id ({@value}.
* Messages which meet this threshold and the hop threshold {@link #hopThreshold} are deleted only after the
* messages not meeting one of these two thresholds.
*/
private double ageThreshold;
/**
* The router handling this buffer.
*/
private DisasterRouter attachedRouter;
/**
* Comparator used between messages which have a high rank / should be deleted later than others because they
* have not traveled far in the network and have been in the host's buffer only for a short time.
*
* These messages are first sorted by hop count, then by time spent in the buffer. Messages which have been
* in the buffer for a longer time and have a higher hop count are deleted faster.
*/
private Comparator<Message> highRankMessageComparator =
Comparator.<Message> comparingInt(m -> (-1) * m.getHopCount()).thenComparing(Message::getReceiveTime);
/**
* Caches deletion rank values for messages not having a high rank. Very useful because comparators may be called
* multiple times for each item.
* Invalidated every timestep to ensure correct value.
*/
private HashMap<Message, Double> deletionRankCache = new HashMap<>();
/**
* The simulation time the current {@link #deletionRankCache} is for.
*/
private double cacheTime;
/**
* Initializes a new instance of the {@link DisasterBufferComparator} class.
* @param attachedRouter Router deleting the messages from buffer.
*/
public DisasterBufferComparator(MessageRouter attachedRouter) {
Settings s = new Settings(DISASTER_BUFFER_NS);
this.hopThreshold = s.getInt(HOP_THRESHOLD_S);
this.ageThreshold = s.getDouble(AGE_THRESHOLD_S);
DisasterBufferComparator.checkRouterIsDisasterRouter(attachedRouter);
this.attachedRouter = (DisasterRouter)attachedRouter;
}
/**
* Copy constructor.
* @param bufferComparator Original {@link DisasterBufferComparator} to copy settings from.
* @param attachedRouter Router prioritizing the messages.
*/
public DisasterBufferComparator(DisasterBufferComparator bufferComparator, MessageRouter attachedRouter) {
this.hopThreshold = bufferComparator.hopThreshold;
this.ageThreshold = bufferComparator.ageThreshold;
DisasterBufferComparator.checkRouterIsDisasterRouter(attachedRouter);
this.attachedRouter = (DisasterRouter)attachedRouter;
}
/**
* Checks if the router is a {@link DisasterRouter} and throws an {@link IllegalArgumentException} if it isn't.
* @param router Router to check
*/
private static void checkRouterIsDisasterRouter(MessageRouter router) {
if (router == null) {
throw new IllegalArgumentException("Router is null!");
}
if (!(router instanceof DisasterRouter)) {
throw new IllegalArgumentException(
"Disaster routing buffer management cannot handle routers of type " + router.getClass() + "!");
}
}
/**
* Compares two messages.
* @param m1 the first message to be compared.
* @param m2 the second message to be compared.
* @return a negative integer, zero, or a positive integer as the
* first message should be deleted before, at the same time, or after the second message.
*/
@Override
public int compare(Message m1, Message m2) {
// If both messages have a high rank or both don't, use respective sorting mechanisms.
if (this.hasHighRank(m1) && this.hasHighRank(m2)) {
return this.highRankMessageComparator.compare(m1, m2);
}
if (!this.hasHighRank(m1) && !this.hasHighRank(m2)) {
return Double.compare(this.computeDeletionRankValue(m1), this.computeDeletionRankValue(m2));
}
// Else, return a value depending on which message has a high rank.
return Boolean.compare(this.hasHighRank(m1), this.hasHighRank(m2));
}
/**
* Returns whether the provided message has a high rank / should be deleted later than others because it
* has not traveled far in the network and has been in the host's buffer only for a short time.
* @param m The message to check.
* @return Whether the provided message has a high rank.
*/
private boolean hasHighRank(Message m) {
return m.getHopCount() < this.hopThreshold && (SimClock.getTime() - m.getReceiveTime()) < this.ageThreshold;
}
/**
* Computes a value indicating how useful it is to keep the provided message in the host's buffer. In other words,
* high values mean that the message should be deleted later.
* @param m Message to compute the value for.
* @return Value indicating how useful the message is in the host's buffer.
*/
private double computeDeletionRankValue(Message m) {
// Invalidate cache if required.
this.possiblyInvalidateCache();
// Then: If we already have the deletion rank cached, don't compute it.
Double cachedValue = this.deletionRankCache.get(m);
if (cachedValue != null) {
return cachedValue;
}
// Else: Compute the value...
Double deletionRank;
if (m instanceof BroadcastMessage) {
deletionRank = 1 - this.attachedRouter.getReplicationsDensity(m);
} else {
deletionRank = this.attachedRouter.getDeliveryPredictability(m);
}
// ...and cache it before returning.
this.deletionRankCache.put(m, deletionRank);
return deletionRank;
}
/**
* Invalidates the {@link #deletionRankCache} if it is obsolete.
*/
private void possiblyInvalidateCache() {
double currentTime = SimClock.getTime();
if (currentTime > this.cacheTime) {
this.deletionRankCache.clear();
this.cacheTime = currentTime;
}
}
/**
* Returns the used hop count threshold.
* @return The hop count threshold.
*/
public int getHopThreshold() {
return hopThreshold;
}
/**
* Returns the used age threshold.
* @return The age threshold.
*/
public double getAgeThreshold() {
return ageThreshold;
}
}
package test;
import core.CBRConnection;
import core.Connection;
import core.DTNHost;
import core.Message;
import core.SimClock;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import routing.MessageChoosingStrategy;
import routing.MessageRouter;
import util.Tuple;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Base class for all test classes testing classes implementing {@link routing.MessageChoosingStrategy}.
*
* Created by Britta Heymann on 26.07.2017.
*/
public abstract class AbstractMessageChoosingStrategyTest {
/** Some values needed in tests. */
protected static final int TWO_MESSAGES = 2;
protected static final double SMALL_POWER_DIFFERENCE = 0.01;
/** Error messages. */
protected static final String UNEXPECTED_NUMBER_OF_CHOSEN_MESSAGES =
"Expected different number of chosen messages.";
/** The maximum delta when comparing for double equality. */
protected static final double DOUBLE_COMPARISON_DELTA = 0.00001;
protected TestUtils utils;
protected SimClock clock = SimClock.getInstance();
protected TestSettings settings;
protected MessageChoosingStrategy chooser;
protected DTNHost attachedHost;
protected DTNHost neighbor1;
protected DTNHost neighbor2;
@Before
public void setUp() {
this.settings = new TestSettings();
this.addNecessarySettings();
this.utils = new TestUtils(new ArrayList<>(), new ArrayList<>(), this.settings);
this.utils.setMessageRouterProto(this.createMessageRouterPrototype());
this.attachedHost = this.utils.createHost();
this.neighbor1 = this.utils.createHost();
this.neighbor2 = this.utils.createHost();
this.attachedHost.update(true);
this.neighbor1.update(true);
this.neighbor2.update(true);
this.chooser = this.createMessageChooser();
this.chooser.setAttachedHost(this.attachedHost);
}
@After
public void cleanUp() {
SimClock.reset();
DTNHost.reset();
}
/**
* Adds all necessary settings to the settings object {@link #settings}.
*/
protected abstract void addNecessarySettings();
/**
* Creates the router to use for all hosts.
* @return A prototype of the router.
*/
protected abstract MessageRouter createMessageRouterPrototype();
/**
* Creates the message chooser to test.
* @return The chooser to test.
*/
protected abstract MessageChoosingStrategy createMessageChooser();
/**
* Checks that {@link MessageChoosingStrategy#replicate(MessageRouter)} returns a message choosing strategy of the
* correct type.
*/
@Test
public abstract void testReplicateReturnsCorrectType();
/**
* Checks that {@link MessageChoosingStrategy#replicate(MessageRouter)} copies all settings.
*/
@Test
public abstract void testReplicateCopiesSettings();
/**
* Checks that {@link MessageChoosingStrategy#chooseNonDirectMessages(Collection, List)} does not return any
* (message, connection) tuples for which the receiving host would be a final recipient of the message.
*/
@Test
public void testChooseNonDirectMessagesDoesNotReturnDirectMessages() {
// Create message to neighbor 1.
Message m = new Message(this.attachedHost, neighbor1, "M1", 0);
this.attachedHost.createNewMessage(m);
// Call chooseNonDirectMessages with two connections, one of them to neighbor 1.
List<Connection> connections = new ArrayList<>();
connections.add(AbstractMessageChoosingStrategyTest.createConnection(this.attachedHost, neighbor1));
connections.add(AbstractMessageChoosingStrategyTest.createConnection(this.attachedHost, neighbor2));
Collection<Tuple<Message, Connection>> messages =
this.chooser.chooseNonDirectMessages(Collections.singletonList(m), connections);
// Make sure the direct message was not returned.
Assert.assertEquals(UNEXPECTED_NUMBER_OF_CHOSEN_MESSAGES, 1, messages.size());
Assert.assertFalse(
"Direct message should not have been returned.",
this.messageToHostsExists(messages, m.getId(), neighbor1));
Assert.assertTrue(
"Message to second neighbor expected.", this.messageToHostsExists(messages, m.getId(), neighbor2));
}
/**
* Checks that {@link MessageChoosingStrategy#chooseNonDirectMessages(Collection, List)} does not return any
* (message, connection) tuples for which the receiving host already knows the message.
*/
@Test
public void testChooseNonDirectMessagesDoesNotReturnKnownMessages() {
// Create message which is known by neighbor 1.
Message m = new Message(this.attachedHost, this.utils.createHost(), "M1", 0);
this.attachedHost.createNewMessage(m);
this.neighbor1.createNewMessage(m);
// Call chooseNonDirectMessages with two connections, one of them to neighbor 1.
List<Connection> connections = new ArrayList<>();
connections.add(UtilityMessageChooserTest.createConnection(this.attachedHost, neighbor1));
connections.add(UtilityMessageChooserTest.createConnection(this.attachedHost, neighbor2));
Collection<Tuple<Message, Connection>> messages =
this.chooser.chooseNonDirectMessages(Collections.singletonList(m), connections);
// Make sure the known message was not returned.
Assert.assertEquals(UNEXPECTED_NUMBER_OF_CHOSEN_MESSAGES, 1, messages.size());
Assert.assertFalse(
"Known message should not have been returned.",
this.messageToHostsExists(messages, m.getId(), neighbor1));
Assert.assertTrue(
"Message to second neighbor expected.", this.messageToHostsExists(messages, m.getId(), neighbor2));
}
/**
* Creates a {@link Connection} object.
* @return The created connection object.
*/
protected static Connection createConnection(DTNHost from, DTNHost to) {
return new CBRConnection(from, from.getInterfaces().get(0), to, to.getInterfaces().get(0), 1);
}
/**
* Checks the provided message-connection tuple list for the existence of a tuple mapping a message with the
* provided ID to a connection where the host which is not {@link #attachedHost} is the provided host.
*
* @param messages List to check.
* @param id Message ID to look for.
* @param host Host to look for.
* @return True if such a message can be found.
*/
protected boolean messageToHostsExists(Collection<Tuple<Message, Connection>> messages, String id, DTNHost host) {
for (Tuple<Message, Connection> tuple : messages) {
if (tuple.getKey().getId().equals(id) && tuple.getValue().getOtherNode(this.attachedHost).equals(host)) {
return true;
}
}
return false;
}
}
......@@ -31,6 +31,9 @@ public abstract class AbstractReportTest {
*/
@Before
public void setUp() throws IOException {
// Set locale for periods instead of commas in doubles.
java.util.Locale.setDefault(java.util.Locale.US);
this.outputFile = File.createTempFile("reportTest", ".tmp");
String reportName = this.getReportClass().getSimpleName();
......
......@@ -11,6 +11,7 @@ import core.Coord;
import core.DTNHost;
import core.MessageListener;
import core.NetworkInterface;
import core.Settings;
import core.SimClock;
import junit.framework.TestCase;
import routing.MessageRouter;
......@@ -22,7 +23,7 @@ import routing.MessageRouter;
public abstract class AbstractRouterTest extends TestCase {
protected MessageChecker mc;
protected TestUtils utils;
protected static TestSettings ts = new TestSettings();
protected TestSettings ts = new TestSettings();
protected static final int BUFFER_SIZE = 100;
protected static final int TRANSMIT_SPEED = 10;
......@@ -58,11 +59,11 @@ public abstract class AbstractRouterTest extends TestCase {
List<MessageListener> ml = new ArrayList<MessageListener>();
ml.add(mc);
ts.setNameSpace(TestUtils.IFACE_NS);
ts.putSetting(NetworkInterface.TRANSMIT_SPEED_S, ""+TRANSMIT_SPEED);
ts.putSetting(NetworkInterface.TRANSMIT_RANGE_S, "1");
this.ts.setNameSpace(TestUtils.IFACE_NS);
this.ts.putSetting(NetworkInterface.TRANSMIT_SPEED_S, ""+TRANSMIT_SPEED);
this.ts.putSetting(NetworkInterface.TRANSMIT_RANGE_S, "1");
this.utils = new TestUtils(null,ml,ts);
this.utils = new TestUtils(null,ml,this.ts);
this.utils.setMessageRouterProto(routerProto);
core.NetworkInterface.reset();
core.DTNHost.reset();
......@@ -75,6 +76,16 @@ public abstract class AbstractRouterTest extends TestCase {
this.h6 = utils.createHost(c0, "h6");
}
/**
* Tears down the fixture, for example, close a network connection.
* This method is called after a test is executed.
*/
@Override
protected void tearDown() throws Exception {
super.tearDown();
Settings.init(null);
}
protected void setRouterProto(MessageRouter r) {
this.routerProto = r;
}
......
package test;
import core.BroadcastMessage;
import core.DTNHost;
import core.Settings;
import core.SimClock;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import report.BufferOccupancyReport;
import report.Report;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.DoubleSummaryStatistics;
import java.util.List;
/**
* Contains tests for the {@link report.BufferOccupancyReport} class.
* Created by Britta Heymann on 11.08.2017.
*/
public class BufferOccupancyReportTest extends AbstractReportTest {
/* Time spans needed for tests. */
private static final int REPORT_INTERVAL = WARM_UP_TIME / 2;
private static final int SECOND_REPORT_TIME = 2 * REPORT_INTERVAL;
private static final double SMALL_TIME_DIFFERENCE = 0.01;
/* Message sizes needed for tests. */
private static final int SMALL_MESSAGE_SIZE = 10;
private static final int LARGER_MESSAGE_SIZE = 5000;
/* Indices of certain values in a report line. */
private static final int TIME_INDEX = 0;
private static final int AVERAGE_INDEX = 1;
private static final int VARIANCE_INDEX = 2;
private static final int MINIMUM_INDEX = 3;
private static final int MAXIMUM_INDEX = 4;
/* Further constants. */
private static final double EXPONENT_TO_SQUARE = 2;
private static final String UNEXPECTED_REPORT_TIME = "Expected report line at different simulator time point.";
/** The accepted difference when comparing doubles for equality. */
private static final double DOUBLE_COMPARISON_DELTA = 0.0001;
private BufferOccupancyReport report;
private SimClock clock = SimClock.getInstance();
// Hosts with buffers.
private DTNHost hostWithSmallMessage;
private DTNHost hostWithLargerMessage;
private List<DTNHost> allHosts;
public BufferOccupancyReportTest() {
// Empty constructor for "Classes and enums with private members should hava a constructor" (S1258).
// This is dealt with by the setUp method.
}
@Before
@Override
public void setUp() throws IOException {
// Let base do the basic report setup.
super.setUp();
// Add warm up time.
this.settings.setNameSpace(this.getReportClass().getSimpleName());
this.settings.putSetting(Report.WARMUP_S, Integer.toString(WARM_UP_TIME));
this.settings.putSetting(BufferOccupancyReport.BUFFER_REPORT_INTERVAL, Integer.toString(REPORT_INTERVAL));
this.settings.restoreNameSpace();
// Set clock to 0.
this.clock.setTime(0);
// Create report.
this.report = new BufferOccupancyReport();
// Create hosts.
TestUtils utils = new TestUtils(null, new ArrayList<>(), this.settings);
this.hostWithSmallMessage = utils.createHost();
this.hostWithLargerMessage = utils.createHost();
this.allHosts = Arrays.asList(this.hostWithLargerMessage, this.hostWithSmallMessage);
// Make sure hosts have messages in buffer.
this.hostWithSmallMessage.createNewMessage(
new BroadcastMessage(this.hostWithSmallMessage, "M", SMALL_MESSAGE_SIZE));
this.hostWithLargerMessage.createNewMessage(
new BroadcastMessage(this.hostWithLargerMessage, "M", LARGER_MESSAGE_SIZE));
}
/**
* Checks that the report correctly handles the warm up time as set by the {@link Report#WARMUP_S} setting.
*/
@Override
public void testReportCorrectlyHandlesWarmUpTime() throws IOException {
Assert.assertTrue("Report interval should be smaller than warm up time for test that makes sense.",
REPORT_INTERVAL < WARM_UP_TIME);
// Check report works even before warm up time expired.
this.clock.setTime(REPORT_INTERVAL);
this.report.updated(this.allHosts);
this.report.done();
try(BufferedReader reader = this.createBufferedReader()) {
Assert.assertEquals(
UNEXPECTED_REPORT_TIME,
REPORT_INTERVAL,
BufferOccupancyReportTest.getTimeFromLine(reader.readLine()),
DOUBLE_COMPARISON_DELTA);
Assert.assertNull("Expected only one line.", reader.readLine());
}
}
/**
* Checks that the {@link BufferOccupancyReport} correctly prints all statistics.
* @throws IOException
*/
@Test
public void testBufferStatisticsAreCorrect() throws IOException {
// Write report.
this.clock.setTime(REPORT_INTERVAL);
this.report.updated(Arrays.asList(this.hostWithSmallMessage, this.hostWithLargerMessage));
this.report.done();
// Check statistics.
DoubleSummaryStatistics statistics = new DoubleSummaryStatistics();
statistics.accept(this.hostWithLargerMessage.getBufferOccupancy());
statistics.accept(this.hostWithSmallMessage.getBufferOccupancy());
double variance = 0;
variance += Math.pow(
this.hostWithLargerMessage.getBufferOccupancy() - statistics.getAverage(), EXPONENT_TO_SQUARE);
variance += Math.pow(
this.hostWithSmallMessage.getBufferOccupancy() - statistics.getAverage(), EXPONENT_TO_SQUARE);
try(BufferedReader reader = this.createBufferedReader()) {
String line = reader.readLine();
Assert.assertEquals(
UNEXPECTED_REPORT_TIME,
REPORT_INTERVAL, BufferOccupancyReportTest.getTimeFromLine(line),
DOUBLE_COMPARISON_DELTA);
Assert.assertEquals(
"Expected different value as average.",
statistics.getAverage(), BufferOccupancyReportTest.getAverageFromLine(line),
DOUBLE_COMPARISON_DELTA);
Assert.assertEquals(
"Expected different value as variance.",
variance, BufferOccupancyReportTest.getVarianceFromLine(line),
DOUBLE_COMPARISON_DELTA);
Assert.assertEquals(
"Expected different value as minimum.",
statistics.getMin(), BufferOccupancyReportTest.getMinimumFromLine(line),
DOUBLE_COMPARISON_DELTA);
Assert.assertEquals(
"Expected different value as average.",
statistics.getMax(), BufferOccupancyReportTest.getMaximumFromLine(line),
DOUBLE_COMPARISON_DELTA);
}
}
@Test
public void testCorrectReportIntervalGetsUsed() throws IOException {
// Update report at multiple time points.
this.clock.setTime(REPORT_INTERVAL - SMALL_TIME_DIFFERENCE);
this.report.updated(this.allHosts);
this.clock.setTime(REPORT_INTERVAL);
this.report.updated(this.allHosts);
this.clock.setTime(SECOND_REPORT_TIME);
this.report.updated(this.allHosts);
// Finish report.
this.report.done();
// Check it was printed at correct times.
try(BufferedReader reader = this.createBufferedReader()) {
Assert.assertEquals(
UNEXPECTED_REPORT_TIME,
REPORT_INTERVAL,
BufferOccupancyReportTest.getTimeFromLine(reader.readLine()),
DOUBLE_COMPARISON_DELTA);
Assert.assertEquals(
UNEXPECTED_REPORT_TIME,
SECOND_REPORT_TIME,
BufferOccupancyReportTest.getTimeFromLine(reader.readLine()),
DOUBLE_COMPARISON_DELTA);
Assert.assertNull("Expected only two lines.", reader.readLine());
}
}
@Test
public void testDefaultReportIntervalIsUsedIfNonSpecified() throws IOException {
// Use report without any special settings.
Settings.init(null);
super.setUp();
this.report = new BufferOccupancyReport();
// Update report after default time.
this.clock.setTime(BufferOccupancyReport.DEFAULT_BUFFER_REPORT_INTERVAL);
this.report.updated(this.allHosts);
this.report.done();
// Check line was written at correct time.
try(BufferedReader reader = this.createBufferedReader()) {
Assert.assertEquals(
UNEXPECTED_REPORT_TIME,
BufferOccupancyReport.DEFAULT_BUFFER_REPORT_INTERVAL,
BufferOccupancyReportTest.getTimeFromLine(reader.readLine()),
DOUBLE_COMPARISON_DELTA);
Assert.assertNull("Expected only one line.", reader.readLine());
}
}
/**
* Gets the report class to test.
*
* @return The report class to test.
*/
@Override
protected Class getReportClass() {
return BufferOccupancyReport.class;
}
/**
* Gets the time from a line with format "TIME something else".
* @param line The line to get the time from.
* @return The parsed time.
*/
private static double getTimeFromLine(String line) {
return BufferOccupancyReportTest.parseWordFromLineAsDouble(line, TIME_INDEX);
}
/**
* Gets the reported average from a line with format "something AVERAGE something else".
* @param line The line to get the average from.
* @return The parsed average.
*/
private static double getAverageFromLine(String line) {
return BufferOccupancyReportTest.parseWordFromLineAsDouble(line, AVERAGE_INDEX);
}
/**
* Gets the reported variance from a line with format "something something VARIANCE something else".
* @param line The line to get the variance from.
* @return The parsed variance.
*/
private static double getVarianceFromLine(String line) {
return BufferOccupancyReportTest.parseWordFromLineAsDouble(line, VARIANCE_INDEX);
}
/**
* Gets the reported minimum from a line with format "something something something MINIMUM something else".
* @param line The line to get the minimum from.
* @return The parsed minimum.
*/
private static double getMinimumFromLine(String line) {
return BufferOccupancyReportTest.parseWordFromLineAsDouble(line, MINIMUM_INDEX);
}
/**
* Gets the reported maximum from a line with format "something something something something MAXIMUM".
* @param line The line to get the maximum from.
* @return The parsed maximum.
*/
private static double getMaximumFromLine(String line) {
return BufferOccupancyReportTest.parseWordFromLineAsDouble(line, MAXIMUM_INDEX);
}
/**
* Reads the word with the provided index from the provided line and parses it as a double.
* @param line Line to read the value from.
* @param index Index of word to parse.
* @return The parsed double.
*/
private static double parseWordFromLineAsDouble(String line, int index) {
return Double.parseDouble(line.split(" ")[index]);
}
}
......@@ -49,6 +49,7 @@ public class DatabaseApplicationTest {
/** Small time difference used for tests about map sending. */
private static final double SMALL_TIME_DIFF = 0.1;
private static final int TIME_IN_DISTANT_FUTURE = 600_000;
private static final int THREE_MINUTES = 3 * 60;
/** Used to check that some database sizes are completely in the interval, not on the border. */
private static final int DISTANCE_FROM_BORDER = 10;
......@@ -265,6 +266,14 @@ public class DatabaseApplicationTest {
TestCase.assertNotNull(EXPECTED_INITIALIZED_APPLICATION, uninitializedApp.getDatabaseSize());
}
@Test
public void testApplicationIsInitializedAfterFirstWrapRecentUsefulDataIntoMessages() {
DatabaseApplication uninitializedApp = new DatabaseApplication(this.settings);
DatabaseApplicationTest.checkAppIsNotInitialized(uninitializedApp);
uninitializedApp.wrapRecentUsefulDataIntoMessages(this.utils.createHost(), 1);
TestCase.assertNotNull(EXPECTED_INITIALIZED_APPLICATION, uninitializedApp.getDatabaseSize());
}
@Test
public void testHandleMessageStoresNewDisasterData() {
/* Send data message to app. */
......@@ -393,7 +402,7 @@ public class DatabaseApplicationTest {
}
@Test
public void testCreateDataMessagesGroupsDataByUtility() {
public void testWrapUsefulDataIntoMessagesGroupsDataByUtility() {
// Create app sending out everything.
this.settings.putSetting(DatabaseApplication.UTILITY_THRESHOLD, "0");
// Use copy constructor to subscribe as data listener.
......@@ -467,6 +476,117 @@ public class DatabaseApplicationTest {
TestCase.assertEquals("Not all maps have been returned.", mapData.size(), mapsInMessages.size());
}
@Test
public void testWrapRecentUsefulDataIntoMessagesCreatesCorrectMessageForInterestingDataItems() {
/* Create data. */
Coord currLocation = this.hostAttachedToApp.getLocation();
double currTime = SimClock.getTime();
DisasterData marker = new DisasterData(DisasterData.DataType.MARKER, 0, currTime, currLocation);
DisasterData resource = new DisasterData(DisasterData.DataType.RESOURCE, 0, currTime, currLocation);
DisasterData skill = new DisasterData(DisasterData.DataType.SKILL, 0, currTime, currLocation);
/* Add to database. */
DisasterDataNotifier.dataCreated(this.hostAttachedToApp, marker);
DisasterDataNotifier.dataCreated(this.hostAttachedToApp, resource);
DisasterDataNotifier.dataCreated(this.hostAttachedToApp, skill);
/* Check all data items are returned as messages. */
List<DataMessage> messages = this.app.wrapRecentUsefulDataIntoMessages(this.hostAttachedToApp, 1);
TestCase.assertEquals(UNEXPECTED_NUMBER_DATA_MESSAGES,
(int)Math.ceil((double)THREE_DATA_ITEMS / ITEMS_PER_MESSAGE), messages.size());
TestCase.assertTrue(
"Expected marker to be in a message.",
messages.stream().anyMatch(msg -> msg.getData().contains(marker)));
TestCase.assertTrue(
"Expected resource to be in a message.",
messages.stream().anyMatch(msg -> msg.getData().contains(resource)));
TestCase.assertTrue(
"Expected skill to be in a message.",
messages.stream().anyMatch(msg -> msg.getData().contains(skill)));
}
@Test
public void testWrapRecentUsefulDataIntoMessagesOnlySendsOutInterestingData() {
this.clock.setTime(TIME_IN_DISTANT_FUTURE);
DisasterData usefulData =
DatabaseApplicationTest.createUsefulData(DisasterData.DataType.SKILL, this.hostAttachedToApp);
DisasterData uselessData = new DisasterData(DisasterData.DataType.RESOURCE, 0, 0, new Coord(0, 0));
DisasterDataNotifier.dataCreated(this.hostAttachedToApp, usefulData);
DisasterDataNotifier.dataCreated(this.hostAttachedToApp, uselessData);
List<DataMessage> messages = this.app.wrapRecentUsefulDataIntoMessages(this.hostAttachedToApp, 1);
TestCase.assertEquals(UNEXPECTED_NUMBER_DATA_MESSAGES, 1, messages.size());
TestCase.assertTrue(
"Expected useful data to be in a message.",
messages.stream().anyMatch(msg -> msg.getData().contains(usefulData)));
TestCase.assertFalse(
"Did not expect useless data to be in a message..",
messages.stream().anyMatch(msg -> msg.getData().contains(uselessData)));
}
@Test
public void testWrapRecentUsefulDataIntoMessagesOnlySendsOutRecentData() {
// Create one data item exactly 3 minutes in the past and another one 3 minutes and 1 second.
DisasterData oldData =
DatabaseApplicationTest.createUsefulData(DisasterData.DataType.SKILL, this.hostAttachedToApp);
DisasterDataNotifier.dataCreated(this.hostAttachedToApp, oldData);
this.clock.advance(1);
DisasterData newerData =
DatabaseApplicationTest.createUsefulData(DisasterData.DataType.SKILL, this.hostAttachedToApp);
DisasterDataNotifier.dataCreated(this.hostAttachedToApp, newerData);
this.clock.advance(THREE_MINUTES);
// Ask for data created at most 3 minutes in the past.
List<DataMessage> messages = this.app.wrapRecentUsefulDataIntoMessages(this.hostAttachedToApp, THREE_MINUTES);
TestCase.assertTrue(
"Expected newer data to be in a message.",
messages.stream().anyMatch(msg -> msg.getData().contains(newerData)));
TestCase.assertFalse(
"Did not expect old data to be in a message..",
messages.stream().anyMatch(msg -> msg.getData().contains(oldData)));
}
@Test
public void testWrapRecentUsefulDataIntoMessagesGroupsDataByUtility() {
// Create app sending out everything.
this.settings.putSetting(DatabaseApplication.UTILITY_THRESHOLD, "0");
// Use copy constructor to subscribe as data listener.
DatabaseApplication floodingApp = new DatabaseApplication(new DatabaseApplication(this.settings));
floodingApp.update(this.hostAttachedToApp);
// Add useful and not that useful data items.
this.clock.setTime(TIME_IN_DISTANT_FUTURE);
this.hostAttachedToApp.setLocation(new Coord(1, 1));
DisasterData[] usefulData = new DisasterData[ITEMS_PER_MESSAGE];
DisasterData[] uselessData = new DisasterData[ITEMS_PER_MESSAGE];
for (int i = 0; i < ITEMS_PER_MESSAGE; i++) {
usefulData[i] =
DatabaseApplicationTest.createUsefulData(DisasterData.DataType.SKILL, this.hostAttachedToApp);
uselessData[i] = new DisasterData(DisasterData.DataType.RESOURCE, 0, SimClock.getTime(), new Coord(0, 0));
DisasterDataNotifier.dataCreated(this.hostAttachedToApp, usefulData[i]);
DisasterDataNotifier.dataCreated(this.hostAttachedToApp, uselessData[i]);
}
// Create messages.
List<DataMessage> messages = floodingApp.wrapRecentUsefulDataIntoMessages(this.hostAttachedToApp, 1);
// Check for correct number messages.
TestCase.assertEquals(UNEXPECTED_NUMBER_DATA_MESSAGES, TWO_DATA_MESSAGES, messages.size());
// Check first message consists of useful data.
boolean allUsefulDataInFirstMessage = messages.get(0).getData().containsAll(Arrays.asList(usefulData));
boolean noOtherDataInFirstMessage = messages.get(0).getData().size() == ITEMS_PER_MESSAGE;
TestCase.assertTrue("Expected all useful data in one message.",
allUsefulDataInFirstMessage && noOtherDataInFirstMessage);
// Check second message consists of useless data.
boolean allUselessDataInSecondMessage = messages.get(1).getData().containsAll(Arrays.asList(uselessData));
boolean noOtherDataInSecondMessage = messages.get(1).getData().size() == ITEMS_PER_MESSAGE;
TestCase.assertTrue("Expected all useless data in one message.",
allUselessDataInSecondMessage && noOtherDataInSecondMessage);
}
@Test
public void testSameSeedAndSameHostLeadsToSameDatabaseSize() {
DatabaseApplication app1 = new DatabaseApplication(this.settings);
......
......@@ -8,8 +8,10 @@ import core.DTNHost;
import core.DataMessage;
import core.DisasterData;
import core.Message;
import core.SimClock;
import input.DisasterDataNotifier;
import junit.framework.TestCase;
import org.junit.After;
import org.junit.Test;
import routing.EpidemicRouter;
import routing.MessageRouter;
......@@ -37,8 +39,22 @@ public class DatabaseApplicationUtilTest {
/** The maximum number of database items per message. */
private static final int ITEMS_PER_MESSAGE = 2;
/* Some more constants needed for tests. */
private static final Coord FAR_AWAY = new Coord(10_000, 20_000);
private static final int POSITIVE_TIMESPAN = 100;
private static final String UNEXPECTED_NUMBER_MESSAGES = "Expected different number of data messages.";
private static final String UNEXPECTED_RECEIVER = "Expected different receiver.";
private static final String EXPECTED_EMPTY_LIST = "No data messages should have been returned.";
private TestSettings testSettings = new TestSettings();
private TestUtils utils = new TestUtils(new ArrayList<>(), new ArrayList<>(), this.testSettings);
private SimClock clock = SimClock.getInstance();
@After
public void cleanUp() {
SimClock.reset();
}
@Test
public void testHasNoMessagesToSendReturnsFalseIfDatabaseApplicationExists() {
......@@ -76,7 +92,7 @@ public class DatabaseApplicationUtilTest {
List<Tuple<Message, Connection>> dataMessages =
DatabaseApplicationUtil.wrapUsefulDataIntoMessages(host.getRouter(), host, host.getConnections());
TestCase.assertEquals("No data messages should have been returned.", 0, dataMessages.size());
TestCase.assertEquals(EXPECTED_EMPTY_LIST, 0, dataMessages.size());
}
@Test
......@@ -87,7 +103,7 @@ public class DatabaseApplicationUtilTest {
List<Tuple<Message, Connection>> dataMessages =
DatabaseApplicationUtil.wrapUsefulDataIntoMessages(host.getRouter(), host, host.getConnections());
TestCase.assertEquals("No data messages should have been returned.", 0, dataMessages.size());
TestCase.assertEquals(EXPECTED_EMPTY_LIST, 0, dataMessages.size());
}
@Test
......@@ -119,12 +135,12 @@ public class DatabaseApplicationUtilTest {
List<Tuple<Message, Connection>> dataMessages =
DatabaseApplicationUtil.wrapUsefulDataIntoMessages(host.getRouter(), host, host.getConnections());
TestCase.assertEquals("Expected different number of data messages.", TWO_MESSAGES, dataMessages.size());
TestCase.assertEquals(UNEXPECTED_NUMBER_MESSAGES, TWO_MESSAGES, dataMessages.size());
DataMessage message1 = (DataMessage)dataMessages.get(0).getKey();
DataMessage message2 = (DataMessage)dataMessages.get(1).getKey();
TestCase.assertEquals("Expected different receiver.", message1.getTo(), neighbor1);
TestCase.assertEquals("Expected different receiver.", message2.getTo(), neighbor2);
TestCase.assertEquals(UNEXPECTED_RECEIVER, message1.getTo(), neighbor1);
TestCase.assertEquals(UNEXPECTED_RECEIVER, message2.getTo(), neighbor2);
TestCase.assertEquals("Data should be the same for both messages.", message1.getData(), message2.getData());
TestCase.assertEquals(
"Utility should be the same for both messages.", message1.getUtility(), message2.getUtility());
......@@ -148,13 +164,99 @@ public class DatabaseApplicationUtilTest {
List<Tuple<Message, Connection>> dataMessages =
DatabaseApplicationUtil.wrapUsefulDataIntoMessages(host.getRouter(), host, host.getConnections());
TestCase.assertEquals("Expected different number of data messages.", 1, dataMessages.size());
TestCase.assertEquals(UNEXPECTED_NUMBER_MESSAGES, 1, dataMessages.size());
DataMessage message = (DataMessage)dataMessages.get(0).getKey();
TestCase.assertEquals("Expected different number of data items.", ITEMS_PER_MESSAGE, message.getData().size());
TestCase.assertTrue("Marker should have been returned.", message.getData().contains(MARKER_DATA));
TestCase.assertTrue("Resource should have been returned.", message.getData().contains(RESOURCE_DATA));
}
@Test
public void testWrapRecentUsefulDataIntoMessagesReturnsEmptyListForNoDatabaseApplication() {
DTNHost host = this.utils.createHost();
DisasterDataNotifier.dataCreated(host, MARKER_DATA);
DTNHost connected = this.utils.createHost();
host.connect(connected);
List<Tuple<Message, Connection>> dataMessages =
DatabaseApplicationUtil.wrapRecentUsefulDataIntoMessages(host, host.getConnections(), 1);
TestCase.assertEquals(EXPECTED_EMPTY_LIST, 0, dataMessages.size());
}
@Test
public void testWrapRecentUsefulDataIntoMessagesReturnsEmptyListIfNoConnectionsAvailable() {
this.addDatabaseApplication();
DTNHost host = this.utils.createHost();
DisasterDataNotifier.dataCreated(host, MARKER_DATA);
List<Tuple<Message, Connection>> dataMessages =
DatabaseApplicationUtil.wrapRecentUsefulDataIntoMessages(host, host.getConnections(), 1);
TestCase.assertEquals(EXPECTED_EMPTY_LIST, 0, dataMessages.size());
}
@Test
public void testWrapRecentUsefulDataIntoMessagesReturnsEachMessageForEachNeighbor() {
this.addDatabaseApplication();
DTNHost host = this.utils.createHost();
DisasterDataNotifier.dataCreated(host, MARKER_DATA);
DTNHost neighbor1 = this.utils.createHost();
DTNHost neighbor2 = this.utils.createHost();
host.connect(neighbor1);
host.connect(neighbor2);
List<Tuple<Message, Connection>> dataMessages =
DatabaseApplicationUtil.wrapRecentUsefulDataIntoMessages(host, host.getConnections(), 1);
TestCase.assertEquals(UNEXPECTED_NUMBER_MESSAGES, TWO_MESSAGES, dataMessages.size());
DataMessage message1 = (DataMessage)dataMessages.get(0).getKey();
DataMessage message2 = (DataMessage)dataMessages.get(1).getKey();
TestCase.assertEquals(UNEXPECTED_RECEIVER, message1.getTo(), neighbor1);
TestCase.assertEquals(UNEXPECTED_RECEIVER, message2.getTo(), neighbor2);
TestCase.assertEquals("Data should be the same for both messages.", message1.getData(), message2.getData());
TestCase.assertEquals(
"Utility should be the same for both messages.", message1.getUtility(), message2.getUtility());
TestCase.assertEquals(
"Creation should be the same for both messages.",
message1.getCreationTime(),
message2.getCreationTime());
TestCase.assertEquals("Size should be the same for both messages", message1.getSize(), message2.getSize());
TestCase.assertEquals("Sender should be the same for both messages", message1.getFrom(), message2.getFrom());
}
@Test
public void testWrapRecentUsefulDataIntoMessagesReturnsRecentInterestingData() {
// Prepare a setting where it's easy to provide old and useless data.
this.addDatabaseApplication();
this.clock.setTime(POSITIVE_TIMESPAN);
DTNHost host = this.utils.createHost();
host.setLocation(FAR_AWAY);
// Prepare three data items: One useful, but not recent; one recent, but not useful; and one useful and recent.
DisasterData notRecent = new DisasterData(DisasterData.DataType.MARKER, 0, 0, host.getLocation());
DisasterData notUseful = new DisasterData(DisasterData.DataType.MARKER, 0, SimClock.getTime(), new Coord(0, 0));
DisasterData recentAndUseful =
new DisasterData(DisasterData.DataType.MARKER, 0, SimClock.getTime(), host.getLocation());
DisasterDataNotifier.dataCreated(host, notRecent);
DisasterDataNotifier.dataCreated(host, notUseful);
DisasterDataNotifier.dataCreated(host, recentAndUseful);
// Meet with neighbor.
DTNHost neighbor = this.utils.createHost();
host.connect(neighbor);
// Check only useful and recent item is returned.
List<Tuple<Message, Connection>> dataMessages =
DatabaseApplicationUtil.wrapRecentUsefulDataIntoMessages(host, host.getConnections(), 1);
TestCase.assertEquals(UNEXPECTED_NUMBER_MESSAGES, 1, dataMessages.size());
DataMessage message = (DataMessage)dataMessages.get(0).getKey();
TestCase.assertFalse("Old data item should not have been returned.", message.getData().contains(notRecent));
TestCase.assertFalse("Useless data item should not have been returned.", message.getData().contains(notUseful));
TestCase.assertTrue("Expected recent useful data item to be in message.",
message.getData().contains(recentAndUseful));
}
private void addDatabaseApplication() {
DatabaseApplicationUtilTest.addDummyValuesForDatabaseApplication(this.testSettings);
this.testSettings.putSetting(DatabaseApplication.ITEMS_PER_MESSAGE, Integer.toString(ITEMS_PER_MESSAGE));
......@@ -165,7 +267,7 @@ public class DatabaseApplicationUtilTest {
}
private static void addDummyValuesForDatabaseApplication(TestSettings s) {
s.putSetting(DatabaseApplication.UTILITY_THRESHOLD, "0.0");
s.putSetting(DatabaseApplication.UTILITY_THRESHOLD, "0.1");
s.putSetting(DatabaseApplication.SIZE_RANDOMIZER_SEED, "0");
s.putSetting(DatabaseApplication.DATABASE_SIZE_RANGE, "0,0");
s.putSetting(DatabaseApplication.MIN_INTERVAL_MAP_SENDING, "0");
......
  • Contributor

    SonarQube analysis reported no issues.

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment