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

io.windmill.core.CPU Maven / Gradle / Ivy

There is a newer version: 0.2
Show newest version
package io.windmill.core;

import java.net.InetSocketAddress;
import java.nio.channels.Selector;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import io.windmill.core.tasks.*;
import io.windmill.core.Status.Flag;
import io.windmill.disk.File;
import io.windmill.disk.IOService;
import io.windmill.disk.IOTask;
import io.windmill.disk.PageRef;
import io.windmill.net.Channel;
import io.windmill.net.Network;
import io.windmill.utils.IOUtils;

import com.github.benmanes.caffeine.cache.Cache;

import com.lmax.disruptor.BusySpinWaitStrategy;
import com.lmax.disruptor.EventPoller;
import com.lmax.disruptor.EventPoller.PollState;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.ProducerType;

import net.openhft.affinity.AffinitySupport;
import net.openhft.affinity.CpuLayout;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Windmill's primary execution abstraction: the per-core run loop,
 * network selector, and io scheduler.
 *
 * All work in windmill is scheduled on a {@link CPU}, using any of the
 * functions defined on it, such as {@link #schedule(Task0)}, {@link #open(java.io.File, String)},
 * {@link #connect(InetSocketAddress)}, {@link #listen(InetSocketAddress, VoidTask1, VoidTask1)},
 * or {@link #repeat(Task1)} functions, or by performing operations on the {@link Future}s
 * returned by these operations.
 *
 * The number of IO threads used by each windmill CPU is configurable via the
 * {@code windmill.cpu.io_threads} system property.
 */
public class CPU
{
    private static final Logger logger = LoggerFactory.getLogger(CPU.class);

    // default number of I/O threads per CPU
    private static final int DEFAULT_IO_THREADS = Integer.getInteger("windmill.cpu.io_threads", 4);

    private static final EventPoller.Handler HANDLER = (event, sequence, endOfBatch) -> {
        event.run();
        return false;
    };

    private volatile boolean isHalted = false;

    protected final CpuLayout layout;
    protected final int id;
    protected final CPUSet.Socket socket;
    protected final RingBuffer runQueue;
    protected final IOService io;
    protected final Network network;
    protected final DelayQueue timers;

    CPU(CpuLayout layout, int cpuId, CPUSet.Socket socket, Cache pageTracker)
    {
        this.layout = layout;
        this.id = cpuId;
        this.socket = socket;
        this.runQueue = RingBuffer.create(ProducerType.MULTI, WorkEvent::new, 1 << 20, new BusySpinWaitStrategy());
        this.io = new IOService(this, pageTracker, DEFAULT_IO_THREADS);
        this.network = new Network(this);
        this.timers = new DelayQueue<>();
    }

    /**
     * @return the id for this CPU
     */
    public int getId()
    {
        return id;
    }

    /**
     * @return the {@link CpuLayout} this {@link CPU} belongs to
     */
    public CpuLayout getLayout()
    {
        return layout;
    }

    /**
     * Bind a socket to a given {@link InetSocketAddress} and listen for incoming connections
     *
     * @param address the address to listen for connections on
     * @param onAccept executed when each new connection is accepted, the {@link Channel}
     *                 object can be used to perform operations on the accepted connection
     * @param onFailure executed if there was an exception while binding to the given address
     */
    public void listen(InetSocketAddress address, VoidTask1 onAccept, VoidTask1 onFailure)
    {
        network.listen(address, onAccept, onFailure);
    }

    /**
     * Try to make a connection to the given {@link InetSocketAddress}.
     *
     * @param address the address to connect to
     * @return a {@link Future} that represents the eventual connection or failure,
     * the {@link Channel} object can be used to perform further operations on the connection.
     */
    public Future connect(InetSocketAddress address)
    {
        return network.connect(address);
    }

    /**
     * Schedule arbitrary work, which does not return a value, on this CPU
     *
     * @param task the work to execute
     * @return a {@link Future} that can be used to interact with the completion of the work or
     * any exceptions that occur during its execution.
     */
    public Future schedule(VoidTask0 task)
    {
        return schedule(() -> { task.compute(); return null; });
    }

    /**
     * Schedule arbitrary work on this CPU
     *
     * @param task the work to execute
     * @param  the type of value the work returns
     * @return a {@link Future} that can be used to schedule more work based on the result, or to handle
     * any exceptions that occurred during execution.
     */
    public  Future schedule(Task0 task)
    {
        return schedule(new Promise<>(this, task));
    }

    /**
     * Schedule work on one of the IO threads managed by this CPU. This is intended for work that
     * performs IO, enabling asynchronous IO
     *
     * @param task the work to execute
     * @param  the type of value the work returns
     * @return a {@link Future} that can be used to schedule more work based on the result, or to handle
     * any exceptions that occurred during execution.
     */
    public  Future scheduleIO(IOTask task)
    {
        return io.schedule(task);
    }

    /**
     * Perform a side-effecting task continuously - executing the next iteration once the previous has completed. Tasks
     * can indicate whether or not to continue via the {@link Status} in the returned future.
     *
     * @param task the work to perform repeatedly
     */
    public void repeat(Task1>> task)
    {
        repeat((Task2>>) (cpu, previous) -> task.compute(cpu));
    }

    /**
     * Perform a task continuously - executing the next iteration once the previous has completed. Tasks
     * can indicate whether or not to continue, and propagate the previous result, via the {@link Status} in the returned future.
     *
     * @param task the work to perform repeatedly
     * @param  the type of value the work produces
     */
    public  void repeat(Task2>> task)
    {
        repeat(task, null);
    }

    private  void repeat(Task2>> task, O previous)
    {
        schedule(() -> task.compute(this, previous).onSuccess((status) -> {
            if (status.flag == Flag.CONTINUE)
                repeat(task, status.value);
        }));
    }

    /**
     * Given a list of asynchronous work, all returning the same type of value, {@code I},
     * return a single {@link Future} representing the successful completion of all of
     * the work, or one or more failures
     *
     * @param futures the list of work to sequence
     * @param  the type of value returned by every task in the list
     * @return a {@link Future} representing the successful completion of all of
     * the work, or one or more failures
     */
    public  Future> sequence(List> futures)
    {
        Future> promise = new Future<>(this);
        schedule(() -> {
            AtomicInteger counter = new AtomicInteger(0);
            List results = new ArrayList<>(futures.size());

            for (int i = 0; i < futures.size(); i++)
            {
                int currentIndex = i;
                Future f = futures.get(i);

                // pre-allocate the space for the elements being added out of order
                results.add(null);

                // both success and failure handling
                // should be done based on the correct CPU context
                f.onSuccess((v) -> schedule(() -> {
                    results.set(currentIndex, v);

                    // collect all of the results before
                    // marking flattened future as success
                    if (counter.incrementAndGet() >= futures.size())
                        promise.setValue(results);
                }));

                // if at least one of the futures fails, fail flattened promise.
                f.onFailure((e) -> schedule(() -> promise.setFailure(e)));
            }

        });

        return promise;
    }

    /**
     * Perform arbitrary work after a given delay
     *
     * @param duration the amount to delay for
     * @param unit the unit of {@code duration}
     * @param then the work to execute after the given delay
     * @param  the type of value the work returns
     * @return a {@link Future} allow work to be scheduled based on the result
     * of the work executing after the given delay
     */
    public  Future sleep(long duration, TimeUnit unit, Task0 then)
    {
        Promise promise = new Promise<>(this, then);
        timers.add(new TimerTask<>(unit.toNanos(duration), promise));
        return promise.getFuture();
    }

    /**
     * Open a file asynchronously
     *
     * @param path the absolute or relative path to the file
     * @param mode see {@link java.io.RandomAccessFile} for a description of this argument
     * @return a {@link Future} that can be used to perform operations on the file,
     * if successfully opened, or handle any exceptions that may have occurred
     */
    public Future open(String path, String mode)
    {
        return io.open(path, mode);
    }

    /**
     * Open a file asynchronously
     *
     * @param file the file to open
     * @param mode see {@link java.io.RandomAccessFile} for a description of this argument
     * @return a {@link Future} that can be used to perform operations on the file,
     * if successfully opened, or handle any exceptions that may have occurred
     */
    public Future open(java.io.File file, String mode)
    {
        return io.open(file.getAbsolutePath(), mode);
    }

    protected  Future schedule(Promise promise)
    {
        long sequence = runQueue.next();

        try
        {
            WorkEvent event = runQueue.get(sequence);
            event.setWork(promise);
        }
        finally
        {
            runQueue.publish(sequence);
        }

        return promise.getFuture();
    }

    /**
     * @return the {@link io.windmill.core.CPUSet.Socket} this {@link CPU} belongs to
     */
    public CPUSet.Socket getSocket()
    {
        return socket;
    }

    protected Selector getSelector()
    {
        return network.getSelector();
    }

    /**
     * Starts the thread that this CPU runs on. This must be called before any work
     * is scheduled on this CPU.
     */
    public void start()
    {
        Thread thread = new Thread(this::run);
        thread.setName(id + "-app");
        thread.start();
    }

    protected void run()
    {
        setAffinity();

        EventPoller poller = runQueue.newPoller();

        while (!isHalted)
        {
            try
            {
                if (poller.poll(HANDLER) != PollState.PROCESSING)
                    network.poll();

                processTimers();
            }
            catch (Exception e)
            {
                logger.error("task failed", e);
            }
        }

        IOUtils.closeQuietly(io);
        IOUtils.closeQuietly(network);
    }

    protected void processTimers()
    {
        for (;;)
        {
            TimerTask task = timers.poll();
            if (task == null)
                break;

            schedule(task.promise);
        }
    }

    /**
     * Eventually stop the thread that is executing work scheduled on this {@link CPU}.
     * This stops any future work from being performed by this {@link CPU}.
     */
    public void halt()
    {
        isHalted = true;
    }

    public void setAffinity()
    {
        try
        {
            if (layout != null)
                AffinitySupport.setAffinity(1L << id);
        }
        catch (IllegalStateException e)
        {
            logger.warn("failed to set affinity for CPU {}, ignoring...", id, e);
        }
    }

    private class WorkEvent implements Runnable
    {
        private Promise promise;

        public void setWork(Promise promise)
        {
            this.promise = promise;
        }

        public void run()
        {
            promise.fulfil();
            promise = null; // release a reference to already processed promise
        }
    }

    private static class TimerTask implements Delayed
    {
        private final long startTime;
        private final Promise promise;

        public TimerTask(long delayNanos, Promise promise)
        {
            this.startTime = System.nanoTime() + delayNanos;
            this.promise = promise;
        }

        @Override
        public long getDelay(TimeUnit unit)
        {
            return unit.convert(startTime - System.nanoTime(), TimeUnit.NANOSECONDS);
        }

        @Override
        public int compareTo(Delayed other)
        {
            if (other == null || !(other instanceof TimerTask))
                return -1;

            if (other == this)
                return 0;

            return Long.compare(startTime, ((TimerTask) other).startTime);
        }
    }
}