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

com.distelli.monitor.impl.MonitorImpl Maven / Gradle / Ivy

package com.distelli.monitor.impl;

import com.distelli.jackson.transform.TransformModule;
import com.distelli.monitor.Monitor;
import com.distelli.monitor.MonitorInfo;
import com.distelli.monitor.Monitored;
import com.distelli.monitor.ProductVersion;
import com.distelli.monitor.TaskContext;
import com.distelli.monitor.TaskInfo;
import com.distelli.monitor.TaskManager;
import com.distelli.persistence.AttrType;
import com.distelli.persistence.Index;
import com.distelli.persistence.IndexDescription;
import com.distelli.persistence.PageIterator;
import com.distelli.persistence.TableDescription;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.persistence.RollbackException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Singleton
public class MonitorImpl implements Monitor {
    private static final Logger LOG = LoggerFactory.getLogger(MonitorImpl.class);
    private static final int HEARTBEAT_INTERVAL_MS = 10000;
    private static final int REAP_INTERVALS = 6;

    @Inject
    private ScheduledExecutorService _executor;
    @Inject
    private TaskManager _taskManager;
    @Inject
    private ProductVersion _productVersion;
    @Inject
    private ReapMonitorTask _reapMonitorTask;

    private Index _monitors;
    private final ObjectMapper _om = new ObjectMapper();

    // synchronize(_heartbeats):
    private Map _heartbeats = new HashMap<>();

    // synchronize(this):
    private MonitorInfoImpl _activeMonitorInfo;
    // synchronize(this):
    private ScheduledFuture _reaper;
    // synchronize(this):
    private ScheduledFuture _heartbeat;
    // synchronize(this):
    private Set _monitorsToShutdown = new HashSet<>();
    // synchronize(this):
    private boolean _shuttingDown = false;

    public static TableDescription getTableDescription() {
        return TableDescription.builder()
            .tableName("monitors")
            .index((idx) -> idx
                   .hashKey("id", AttrType.STR)
                   .build())
            .build();
    }

    private TransformModule createTransforms(TransformModule module) {
        module.createTransform(MonitorInfoImpl.class)
            .put("id", String.class, "monitorId")
            .put("nam", String.class, "nodeName")
            .put("ver", String.class, "version")
            .put("hb", Long.class, "heartbeat");
        return module;
    }

    @Inject
    protected MonitorImpl() {}

    @Inject
    protected void init(Index.Factory indexFactory) {
        _om.registerModule(createTransforms(new TransformModule()));

        _monitors = indexFactory.create(MonitorInfoImpl.class)
            .withNoEncrypt("hb")
            .withTableDescription(getTableDescription())
            .withConvertValue(_om::convertValue)
            .build();
    }

    private synchronized void scheduleHeartbeat() {
        if ( _shuttingDown ) throw new ShuttingDownException();
        if ( null == _reaper ) {
            long reapInterval = HEARTBEAT_INTERVAL_MS * REAP_INTERVALS;
            _reaper = _executor.scheduleAtFixedRate(
                this::reaper,
                ThreadLocalRandom.current().nextLong(reapInterval),
                reapInterval,
                TimeUnit.MILLISECONDS);
        }
        if ( null == _heartbeat ) {
            _heartbeat = _executor.scheduleAtFixedRate(
                this::heartbeat,
                ThreadLocalRandom.current().nextLong(HEARTBEAT_INTERVAL_MS),
                HEARTBEAT_INTERVAL_MS,
                TimeUnit.MILLISECONDS);
        }
    }

    @Override
    public MonitorInfo getMonitorInfo(String monitorId) {
        return _monitors.getItem(monitorId);
    }

    @Override
    public void monitor(Monitored task) {
        MonitorInfoImpl monitor = null;
        MonitorInfoImpl shutdownMonitor = null;
        synchronized ( this ) {
            scheduleHeartbeat();
            monitor = _activeMonitorInfo;

            if ( null == monitor || monitor.hasFailedHeartbeat() ) {
                monitor = new MonitorInfoImpl(
                    _productVersion.toString(),
                    HEARTBEAT_INTERVAL_MS * (REAP_INTERVALS - 1));
                shutdownMonitor = _activeMonitorInfo;
                _monitorsToShutdown.add(monitor);
                if ( LOG.isDebugEnabled() ) {
                    LOG.debug("activeMonitor="+_activeMonitorInfo+" new="+monitor);
                }
                _monitors.putItem(monitor);
                _activeMonitorInfo = monitor;
            } else {
                monitor = _activeMonitorInfo;
            }
        }
        if ( null != shutdownMonitor && ! shutdownMonitor.isRunningInMonitoredThread() ) {
            shutdown(shutdownMonitor, true);
        }
        try {
            monitor.captureRunningThread();
            task.run(monitor);
        } finally {
            if ( monitor.releaseRunningThread() &&
                 monitor.hasFailedHeartbeat() )
            {
                // Nothing should be running, so set mayInterruptIfRunning=false
                shutdown(monitor, false);
            }
        }
    }

    @Override
    public synchronized boolean isActiveMonitor(MonitorInfo monitorInfo) {
        return _activeMonitorInfo == monitorInfo;
    }

    private static long milliTime() {
        return TimeUnit.MILLISECONDS.convert(System.nanoTime(), TimeUnit.NANOSECONDS);
    }

    @Override
    public void shutdownMonitor(boolean mayInterruptIfRunning) {
        synchronized ( this ) {
            _shuttingDown = true;
            _activeMonitorInfo = null;
            if ( null != _reaper ) {
                _reaper.cancel(false);
                _reaper = null;
            }
            if ( null != _heartbeat ) {
                _heartbeat.cancel(false);
                _heartbeat = null;
            }
        }
        while ( true ) {
            MonitorInfoImpl monitor = null;
            synchronized ( this ) {
                if ( ! _monitorsToShutdown.isEmpty() ) {
                    monitor = _monitorsToShutdown.iterator().next();
                }
            }
            if ( null == monitor ) break;
            shutdown(monitor, mayInterruptIfRunning);
        }
    }

    private void shutdown(MonitorInfoImpl monitor, boolean mayInterruptIfRunning) {
        monitor.forceHeartbeatFailure();
        synchronized ( this ) {
            if ( _activeMonitorInfo == monitor ) {
                // Make sure this is NOT the active monitor:
                _activeMonitorInfo = null;
            }
        }

        long waitTime = monitor.getLastHeartbeatMillis()
            + ( HEARTBEAT_INTERVAL_MS * REAP_INTERVALS )
            - milliTime();

        // Make sure we always have at least SOME wait time
        if ( waitTime < 200 ) {
            LOG.warn("Adjusting waitTime="+waitTime+"ms to 200ms when running shutdown("+monitor.getMonitorId()+")");
            waitTime = 200;
        }

        if ( ! monitor.interruptAndWaitForRunningThreads(waitTime, mayInterruptIfRunning) ) {
            String msg = "Failed to halt the following threads in "+waitTime+"ms. Halting the JVM:\n"+
                monitor.dumpThreads();
            LOG.error(msg);
            System.err.println(msg);
            Runtime.getRuntime().halt(-1);
        }

        Task task = new Task();
        task.entityId = monitor.getMonitorId();
        try {
            // Try to run the reaper inline so we can keep the DB tidy:
            TaskInfo taskInfo = _reapMonitorTask.run(new TaskContext() {
                    @Override
                    public TaskInfo getTaskInfo() {
                        return task;
                    }
                    @Override
                    public MonitorInfo getMonitorInfo() {
                        return monitor;
                    }
                    @Override
                    public byte[] getUpdateData() {
                        return null;
                    }
                    @Override
                    public void commitCheckpointData(byte[] checkpointData) {}
                });
            if ( null == taskInfo ) {
                _monitors.deleteItem(monitor.getMonitorId(), null);
            } // else: let the reaper take care of it...
        } catch ( Throwable ex ) {
            LOG.error("ReapMonitorTaskFailed: "+ex.getMessage(), ex);
        }
        synchronized ( this ) {
            _monitorsToShutdown.remove(monitor);
        }
    }

    private List listMonitors(PageIterator iter) {
        return _monitors.scanItems(iter);
    }

    private void reaper() {
        try {
            synchronized ( _heartbeats ) {
                Set monitorIds = new HashSet<>(_heartbeats.keySet());
                for ( PageIterator iter : new PageIterator().pageSize(100) ) {
                    for ( MonitorInfoImpl monitor : listMonitors(iter) ) {
                        String monitorId = monitor.getMonitorId();
                        monitorIds.remove(monitorId);
                        Long lastHB = _heartbeats.get(monitorId);
                        _heartbeats.put(monitorId, monitor.getHeartbeat());
                        // New monitor observed:
                        if ( null == lastHB ) {
                            continue;
                        }
                        // Heartbeats observed:
                        if ( monitor.getHeartbeat() != lastHB ) {
                            continue;
                        }
                        // Dead monitor observed:
                        reapMonitorId(monitor.getMonitorId());
                    }
                }
                // Keep the _heartbeats map tidy:
                for ( String monitorId : monitorIds ) {
                    _heartbeats.remove(monitorId);
                }
            }
        } catch ( Throwable ex ) {
            LOG.error(ex.getMessage(), ex);
        }
    }

    // public only for testing purposes:
    public TaskInfo reapMonitorId(String monitorId) {
        LOG.debug("Adding task to reap monitorId="+monitorId);
        // Add task to delete references:
        TaskInfo task =
            _reapMonitorTask.build(_taskManager.createTask(), monitorId);
        _taskManager.addTask(task);
        _monitors.deleteItem(monitorId, null);
        return task;
    }

    private void heartbeat() {
        try {
            MonitorInfoImpl monitor = null;
            try {
                synchronized ( this ) {
                    monitor = _activeMonitorInfo;
                }
                // We are already shutting down:
                if ( null == monitor || monitor.hasFailedHeartbeat() ) return;
                _monitors.updateItem(monitor.getMonitorId(), null)
                    .increment("hb", 1)
                    .when((expr) -> expr.exists("id"));
                monitor.heartbeatWasPerformed();
                return;
            } catch ( Throwable ex ) {
                if ( ex instanceof RollbackException ) {
                    // This could happen if the computer is put to sleep:
                    LOG.warn("Detected monitor deletion, forcing all tasks to stop (perhaps computer sleeped).");
                } else {
                    LOG.error(ex.getMessage(), ex);
                }
                if ( null != monitor ) {
                    shutdown(monitor, true);
                }
            }
        } catch ( Throwable ex ) {
            LOG.error(ex.getMessage(), ex);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy