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

com.ocadotechnology.event.scheduling.BusyLoopEventScheduler Maven / Gradle / Ivy

There is a newer version: 16.6.21
Show newest version
/*
 * Copyright © 2017-2023 Ocado (Ocava)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.ocadotechnology.event.scheduling;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Consumer;

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

import com.ocadotechnology.ThreadManager;
import com.ocadotechnology.event.EventUtil;
import com.ocadotechnology.event.RecoverableException;
import com.ocadotechnology.event.scheduling.BusyLoopQueue.BusyLoopQueueType;
import com.ocadotechnology.time.TimeProvider;

public class BusyLoopEventScheduler extends TypedEventScheduler {
    private static final Logger logger = LoggerFactory.getLogger(BusyLoopEventScheduler.class);
    private final BusyLoopQueue busyLoopQueue;
    private final TimeProvider timeProvider;
    private final String name;
    private final Set> failureListeners = new HashSet<>();
    private final Set> recoverableFailureListeners = new HashSet<>();
    private final Set onShutDowns = new HashSet<>();
    private final ThreadManager threadManager;
    private final boolean heartbeatMonitor;

    private final long parkDurationNanos;
    private final boolean useLowLatencyRunner;

    private volatile boolean shouldStop = false;
    private volatile long threadId;

    /**
     * @param parkDurationNanos an experimental setting to park the thread for a number of nanoseconds when idle to
     *                          prevent an idle thread from overheating the core.  It is expected that a value of ~10ns
     *                          should result in a reduction in idle core heating of 80-90% for an acceptably small
     *                          reduction in responsiveness.  However, in practice, this has been observed to create
     *                          unacceptably large latency spikes.
     * */
    public BusyLoopEventScheduler(
            TimeProvider timeProvider,
            String name,
            EventSchedulerType type,
            ThreadManager threadManager,
            boolean heartbeatMonitor,
            BusyLoopQueueType busyLoopQueueType,
            int size,
            long parkDurationNanos,
            boolean useLowLatencyRunner) {
        super(type);
        this.timeProvider = timeProvider;
        this.name = name;
        this.heartbeatMonitor = heartbeatMonitor;
        this.busyLoopQueue = BusyLoopQueue.constructQueue(busyLoopQueueType, timeProvider, size);
        this.threadManager = threadManager;

        this.parkDurationNanos = parkDurationNanos;
        this.useLowLatencyRunner = useLowLatencyRunner;
    }

    public BusyLoopEventScheduler(TimeProvider timeProvider, String name, EventSchedulerType type, ThreadManager threadManager, boolean heartbeatMonitor, long parkDurationNanos, boolean useLowLatencyRunner) {
        this(timeProvider, name, type, threadManager, heartbeatMonitor, BusyLoopQueueType.SwitchingQueue, 0, parkDurationNanos, useLowLatencyRunner);
    }

    public BusyLoopEventScheduler(TimeProvider timeProvider, String name, EventSchedulerType type) {
        this(timeProvider, name, type, () -> {}, false, BusyLoopQueueType.SwitchingQueue, 0, 0, false);
    }

    @Override
    public void cancel(Event event) {
        busyLoopQueue.remove(event);
    }

    @Override
    public TimeProvider getTimeProvider() {
        return timeProvider;
    }

    @Override
    public Cancelable doNow(Runnable r, String description) {
        Event event = new Event(0, description, r, this, false);
        busyLoopQueue.addToNow(event);
        return event;
    }

    @Override
    public Cancelable doAt(double time, Runnable r, String description, boolean isDaemon) {
        Event event = new Event(time, description, r, this, isDaemon);
        busyLoopQueue.addToSchedule(event);
        return event;
    }

    public void start() {
        shouldStop = false;
        if (heartbeatMonitor) {
            addHeartbeatMonitor();
        }
        Thread thread = new Thread(this::threadStart, "BusyLoopScheduler-" + name);
        threadId = thread.getId();
        thread.start();
    }

    private void threadStart() {
        threadManager.manage();

        if (useLowLatencyRunner) {
            runLowLatencyLoop();
        } else {
            runThroughputLoop();
        }
    }

    private void runThroughputLoop() {
        // Tight loop:
        // It doesn't matter that we always call getTime (system call):
        //     if we have an event, we need to call it anyway;
        //     if we don't, then the delay is irrelevant
        // Micro optimisations:
        // 1. Using continue is faster
        // 2. Catching here is faster
        while (!shouldStop) {
            Event e = busyLoopQueue.getNextEvent();
            if (e == null) {
                continue;
            }
            try {
                e.execute();
            } catch (Throwable t) {
                threadExceptionHandler(e, t);
            }
        }
    }

    private void runLowLatencyLoop() {
        while (!shouldStop) {
            runLowLatencyNowLoop();
            runLowLatencyScheduledLoop();
        }
    }

    private void runLowLatencyNowLoop() {
        // See optimisations as for runThroughputLoop
        // 3. Separating now events from timed events is faster
        while (!shouldStop) {
            Event e = busyLoopQueue.getNextNowEvent();
            if (e == null) {
                return;
            }
            try {
                e.execute();
            } catch (Throwable t) {
                threadExceptionHandler(e, t);
            }
        }
    }

    private void runLowLatencyScheduledLoop() {
        // See optimisations as for runThroughputLoop
        while (!shouldStop && busyLoopQueue.isEmptyNow()) {
            double now = timeProvider.getTime();
            Event e = busyLoopQueue.getNextScheduledEvent(now);
            if (e != null) {
                try {
                    e.execute();
                    continue;
                } catch (Throwable t) {
                    threadExceptionHandler(e, t);
                }
            }
            LockSupport.parkNanos(parkDurationNanos);  // Does not park the thread at all if parkDurationNanos == 0
        }
    }

    private void threadExceptionHandler(Event lastEvent, Throwable t) {
        if (t instanceof IllegalStateException) {
            Throwable cause = t.getCause();
            while (cause != null) {
                if (cause instanceof RecoverableException) {
                    String message = String.format("Scheduler %s attempting to recover at %s from failure processing %s", name, EventUtil.logTime(timeProvider.getTime()), lastEvent);
                    logger.error(message, cause);  // required method is error(message, throwable).  error(message, args, throwable) doesn't exist
                    RecoverableException exception = (RecoverableException) cause;
                    try {
                        recoverableFailureListeners.forEach(l -> l.accept(exception));
                    } catch (Throwable inner) {
                        fail(lastEvent, inner);
                    }
                    break;
                }
                cause = cause.getCause();
            }
            if (cause == null) {
                fail(lastEvent, t);
            }
        } else {
            fail(lastEvent, t);
        }
    }

    private void fail(Event event, Throwable t) {
        // Defensive programming required: Things have gone arbitrarily wrong, so assume the worst:
        try {
            // It's most important to get to the call.  The args matter less
            String message = "Scheduler %s failed at %s whilst processing %s";
            try {
                double time = timeProvider.getTime();
                message = String.format(message, name, EventUtil.logTime(time), event);
            } catch (Throwable ignoreMe) {
                // ignore
            }
            // The logger method we need is error(String, Throwable).  There is no error(String, Args, Throwable)
            logger.error(message, t);
        } finally {
            try {
                failureListeners.forEach(l -> l.accept(t));
            } finally {
                onStop();
            }
        }
    }

    @Override
    public boolean hasOnlyDaemonEvents() {
        return busyLoopQueue.hasOnlyDaemonEvents();
    }

    public void registerOnShutDown(Runnable onShutDown) {
        onShutDowns.add(onShutDown);
    }

    public void registerFailureListener(Consumer l) {
        failureListeners.add(l);
    }

    public void registerRecoverableFailureListener(Consumer l) {
        recoverableFailureListeners.add(l);
    }

    @Override
    public void prepareToStop() {
        // We could add 'graceful' stop to this scheduler as a future improvement (see SimpleDiscreteEventScheduler)
        // nop
    }

    @Override
    public void stop() {
        // Defensive programming required: Who know why we've called stop.  Could be because the logger has failed...
        try {
            // We are throwing an exception here to see who was responsible for stopping the scheduler.
            // info method we need is info(message, throwable).  There is no method info(message, args, throwable)
            logger.info("Scheduler " + name + " stopping", new Exception("DUMMY"));
        } catch (Throwable ignoreMe) {
            // ignore
        }
        try {
            onStop();
        } finally {
            logger.info("Scheduler " + name + " was stopped");
        }
    }

    private void onStop() {
        try {
            onShutDowns.forEach(Runnable::run);
        } finally {
            shouldStop = true;
        }
    }

    public boolean isStopping() {
        return false;
    }

    @Override
    public long getThreadId() {
        return threadId;
    }

    public int getQueueSize() {
        return busyLoopQueue.size();
    }

    private void addHeartbeatMonitor() {
        double now = timeProvider.getTime();
        long heartbeat = 1_000;
        doAt(
                now + heartbeat,
                () -> {
                    logger.info("Scheduler {} heartbeat was executed with delay {}", name, timeProvider.getTime() - now - heartbeat);
                    addHeartbeatMonitor();
                },
                "Heart Beat Monitor"
        );
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy