All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.manoelcampos.gossipsimulator.GossipSimulator Maven / Gradle / Ivy

There is a newer version: 0.2.0
Show newest version
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 Set> nodes;
    private int cycles;

    public GossipSimulator(final GossipConfig config, final RealDistribution random) {
        this.config = Objects.requireNonNull(config);
        this.random = Objects.requireNonNull(random);
        this.nodes = new HashSet<>();
    }

    /**
     * Gets the set of all known {@link GossipNode}s.
     * @return
     */
    public Set> getNodes(){
        return Collections.unmodifiableSet(nodes);
    }

    public int getNodesCount(){
        return nodes.size();
    }

    final void addNode(final GossipNode neighbour) {
        nodes.add(Objects.requireNonNull(neighbour));
    }

    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(
                    "No message was sent by the {} nodes because there is no infected node or their neighbourhood is empty.",
                    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()}.
     *
     * @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(randomNodes(nodes, 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 a set.
     * 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 availableNodes the set to randomly select nodes from
     * @param count the number of random nodes to select
     * @return the collection of randomly selected nodes
     */
    Collection> randomNodes(final Set> availableNodes, final int count) {
        if(count >= availableNodes.size()){
            LOGGER.debug(
                    "It was requested the selection of {} random nodes but there are only {} available. Selecting all available ones.",
                    count, availableNodes.size());
            return availableNodes;
        }

        /*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;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy