![JAR search and dependency download from the Maven repository](/logo.png)
com.manoelcampos.gossipsimulator.GossipSimulator Maven / Gradle / Ivy
package com.manoelcampos.gossipsimulator;
import ch.qos.logback.classic.Level;
import org.apache.commons.math3.distribution.RealDistribution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.stream.IntStream;
import static java.util.stream.Collectors.*;
/**
* Simulates the dissemination of data across a
* network of Gossip nodes.
*
* @param the type of the data the node shares
* @see #run()
*/
public class GossipSimulator {
public static final Logger LOGGER = LoggerFactory.getLogger(GossipSimulator.class.getSimpleName());
private final GossipConfig config;
private final RealDistribution random;
private final List> nodes;
private int cycles;
private int lastNodeId;
public GossipSimulator(final GossipConfig config, final RealDistribution random) {
this.config = Objects.requireNonNull(config);
this.random = Objects.requireNonNull(random);
this.nodes = new LinkedList<>();
}
/**
* Gets the last node id and increments its value.
* @return
*/
int nextNodeId(){
return lastNodeId++;
}
/**
* Gets the list of all known {@link GossipNode}s.
* It's not ensured the list has only unique nodes.
* You have to ensure that by not creating nodes
* with the same id.
*
*
* A {@link Set} is not used here to reduce time
* complexity if you want to randomly select nodes
* from the {@link #getNodes() list}.
* Selecting random nodes from that list can be used,
* for instance, if you want to infect multiple
* nodes randomly before simulation startup.
*
* @return
*/
public List> getNodes(){
return Collections.unmodifiableList(nodes);
}
public int getNodesCount(){
return nodes.size();
}
/**
* Adds a node to the simulator.
* You have to ensure not duplicated node is inserted.
* @param node the node to add
*/
final void addNode(final GossipNode node) {
nodes.add(Objects.requireNonNull(node));
}
public long getInfectedNodesNumber(){
return nodes.stream().filter(GossipNode::isInfected).count();
}
public GossipConfig getConfig() {
return config;
}
/**
* Runs a {@link #getCycles() cycle} of the Gossip transmissions,
* making all infected nodes to send the data to their neighbours.
* If you want to run multiple cycles, you need to call this method
* inside a loop with the stop condition you want.
* For instance, you may want to run a fixed number of cycles
* or while there are non-infected nodes.
* @throws IllegalStateException
* @see #getInfectedNodesNumber()
*/
public void run() {
if(cycles++ == 0){
if(nodes.size() <= config.getMaxNeighbours()) {
throw new IllegalStateException(
String.format(
"The number of existing nodes (%d) must be higher than the number of neighbours by node (%d).",
nodes.size(), config.getMaxNeighbours()));
}
nodes.forEach(this::addRandomNeighbours);
}
LOGGER.info("Running simulation cycle {}", cycles);
final long messagesSent = nodes.stream()
.filter(GossipNode::isInfected)
.filter(GossipNode::sendMessage)
.count();
if(messagesSent == 0) {
LOGGER.warn(
"Cycle {}: No message was sent by any of the {} nodes, because there is no infected node or their neighbourhood is empty.",
cycles, nodes.size());
} else LOGGER.info(
"Number of infected nodes 🐞 after sending messages to {} nodes: {} of {} (cycle {})",
messagesSent, getInfectedNodesNumber(), nodes.size(), cycles);
}
/**
* Adds randomly selected neighbours to a source node,
* according to the {@link GossipConfig#getMaxNeighbours()},
* to create the initial neighborhood.
*
* @param source the node to add neighbours to
*/
private void addRandomNeighbours(final GossipNode source) {
final int prevSize = source.getNeighbourhoodSize();
final int count = rand(config.getMaxNeighbours()+1);
source.addNeighbours(getRandomNodes(count));
LOGGER.debug(
"Added {} neighbours to {} from the max of {} configured.",
source.getNeighbourhoodSize()-prevSize, source, config.getMaxNeighbours());
}
/**
* Randomly selects a given number of nodes from the
* list of all available nodes.
* If the requested number is greater or equal to the number of available nodes,
* there is not need to randomly select them and all available nodes are returned.
*
* @param count the number of random nodes to select
* @return the collection of randomly selected nodes
*/
public Collection> getRandomNodes(final int count) {
return getRandomNodes(nodes, count);
}
/**
* Randomly selects a given number of nodes from a collection.
* If the requested number is greater or equal to the number of available nodes,
* there is not need to randomly select them and all available nodes are returned.
*
* @param sourceNodes the collection to randomly select nodes from
* @param count the number of random nodes to select
* @return the collection of randomly selected nodes
*/
public Collection> getRandomNodes(final Collection> sourceNodes, final int count) {
if(count >= sourceNodes.size()){
LOGGER.debug(
"It was requested the selection of {} random nodes but there are only {} available. Selecting all available ones.",
count, sourceNodes.size());
return sourceNodes;
}
if(sourceNodes instanceof List> list){
return randomNodesFromList(list, count);
}
return randomNodesFromCollection(sourceNodes, count);
}
private List> randomNodesFromList(final List> list, final int count) {
return IntStream.range(0, count)
.map(i -> rand(list.size()))
.mapToObj(list::get)
.collect(toList());
}
private List> randomNodesFromCollection(final Collection> availableNodes, final int count) {
/*An ordered set containing the indexes of the nodes selected randomly
* from the collection of available nodes. */
final Set orderedIndexSet = IntStream.range(0, count)
.mapToObj(i -> rand(availableNodes.size()))
.collect(toCollection(TreeSet::new));
int i = 0;
final List> selectedNodes = new ArrayList<>(count);
final Iterator> it = availableNodes.iterator();
/* Since the availableNodes set doesn't allow direct (indexed) access,
* it's used an additional loop to get the next item
* until we reach the i'th item inside that set. */
for (final int selectedIndex : orderedIndexSet) {
while(it.hasNext()){
final GossipNode node = it.next();
if(selectedIndex == i++) {
selectedNodes.add(node);
break;
}
}
if(!it.hasNext()){
return selectedNodes;
}
}
return selectedNodes;
}
/**
* Returns a random double value between [0..1[.
* @return
*/
public double rand() {
return random.sample();
}
/**
* Returns a random int value between [0..max[.
* @return
*/
public int rand(final int max) {
if(max <= 0){
throw new IllegalArgumentException("Max must be greater than 0.");
}
return (int)Math.floor(random.sample() * max);
}
public static void setLoggerLevel(final Level level){
final Logger root = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
((ch.qos.logback.classic.Logger) root).setLevel(level);
}
/**
* Gets the number of cycles up to now.
* @return
* @see #run()
*/
public int getCycles() {
return cycles;
}
/**
* Checks if the simulation has already started,
* that is: if it has already run any cycle.
* @return
*/
public boolean isStarted(){
return cycles > 0;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy