AppDaemonService.java

/*
 * Copyright (c) 2002-2022, City of Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.portal.service.daemon;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import fr.paris.lutece.portal.service.datastore.DatastoreService;
import fr.paris.lutece.portal.service.init.LuteceInitException;
import fr.paris.lutece.portal.service.spring.SpringContextService;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;

/**
 * this class provides methods to manage daemons services
 */
public final class AppDaemonService
{
    private static final String PROPERTY_MAX_INITIAL_START_DELAY = "daemon.maxInitialStartDelay";
    private static final String PROPERTY_DAEMON_ON_STARTUP = ".onStartUp";
    private static final String PROPERTY_DAEMON_INTERVAL = ".interval";
    private static final String KEY_DAEMON = "daemon.";
    private static final String KEY_DAEMON_PREFIX = "core." + KEY_DAEMON;
    private static final Map<String, DaemonEntry> _mapDaemonEntries = new HashMap<>( );
    private static final Random _random = new Random( );
    private static boolean _bInit;
    private static IDaemonScheduler _executor;

    /** Private constructor */
    private AppDaemonService( )
    {
    }

    /**
     * Performs initialization of the DaemonFactory. Note that this should return right away so that processing can continue (IE thread off everything)
     * 
     * @throws LuteceInitException
     *             If an error occurred
     */
    public static synchronized void init( )
    {
        // already initialized
        if ( _bInit )
        {
            return;
        }

        _executor = SpringContextService.getBean( IDaemonScheduler.BEAN_NAME );

        if ( _mapDaemonEntries.size( ) > 0 )
        {
            // Unsynchronized daemon start
            int nInitialDaemon = 0;

            for ( DaemonEntry entry : _mapDaemonEntries.values( ) )
            {
                if ( entry.onStartup( ) )
                {
                    nInitialDaemon++;
                }
            }

            int nDelay = AppPropertiesService.getPropertyInt( PROPERTY_MAX_INITIAL_START_DELAY, 30 );

            if ( nInitialDaemon > 0 )
            {
                nDelay = nDelay / nInitialDaemon;
            }

            int nInitialDelay = 0;

            // Register daemons
            for ( DaemonEntry entry : _mapDaemonEntries.values( ) )
            {
                // starts any daemon declared as startup daemons
                if ( entry.onStartup( ) )
                {
                    nInitialDelay += nDelay;

                    scheduleThread( entry, nInitialDelay );
                }
            }
        }

        _bInit = true;
    }

    /**
     * Register a daemon by its entry
     * 
     * @param entry
     *            The daemon entry
     * @throws LuteceInitException
     *             If an error occurred
     */
    public static void registerDaemon( DaemonEntry entry ) throws LuteceInitException
    {
        if ( _mapDaemonEntries.containsKey( entry.getId( ) ) )
        {
            AppLogService.error( "Ignoring attempt to register already registered daemon {}", entry.getId( ) );
            return;
        }
        String strIntervalKey = getIntervalKey( entry.getId( ) );
        String strIntervalKeyDefaultValue = null;

        // init interval value if no exists
        if ( !DatastoreService.existsInstanceKey( strIntervalKey ) )
        {
            strIntervalKeyDefaultValue = AppPropertiesService.getProperty( KEY_DAEMON + entry.getId( ) + PROPERTY_DAEMON_INTERVAL, "10" );
            DatastoreService.setInstanceDataValue( strIntervalKey, strIntervalKeyDefaultValue );
        }

        String strIntervalKeyValue = DatastoreService.getInstanceDataValue( strIntervalKey, strIntervalKeyDefaultValue );

        long lInterval = Long.parseLong( strIntervalKeyValue );

        String strOnStartupKey = getOnStartupKey( entry.getId( ) );
        String strOnStartupDefaultValue = null;

        // init onStartup value if no exists
        if ( !DatastoreService.existsInstanceKey( strOnStartupKey ) )
        {
            strOnStartupDefaultValue = AppPropertiesService.getProperty( KEY_DAEMON + entry.getId( ) + ".onstartup", "0" ).equals( "1" )
                    ? DatastoreService.VALUE_TRUE
                    : DatastoreService.VALUE_FALSE;
            DatastoreService.setInstanceDataValue( strOnStartupKey, strOnStartupDefaultValue );
        }

        String strOnStarupvalue = DatastoreService.getInstanceDataValue( strOnStartupKey, strOnStartupDefaultValue );
        boolean bOnStartup = Boolean.parseBoolean( strOnStarupvalue );

        entry.setInterval( lInterval );
        entry.setOnStartUp( bOnStartup );

        try
        {
            entry.loadDaemon( );
        }
        catch( IllegalAccessException | InstantiationException | ClassNotFoundException e )
        {
            throw new LuteceInitException( "Couldn't instantiate daemon: " + entry.getId( ), e );
        }

        // Add plugin name to Daemon class
        if ( entry.getPluginName( ) != null )
        {
            entry.getDaemon( ).setPluginName( entry.getPluginName( ) );
        }

        _mapDaemonEntries.put( entry.getId( ), entry );

        AppLogService.info( "New Daemon registered : {} ", entry.getId( ) );
    }

    /**
     * Unregister a daemon
     * 
     * @param strDaemonKey
     *            The daemon key
     */
    public static void unregisterDaemon( String strDaemonKey )
    {
        unScheduleThread( _mapDaemonEntries.get( strDaemonKey ) );
        _mapDaemonEntries.remove( strDaemonKey );
    }

    /**
     * Starts a daemon
     * 
     * @param strDaemonKey
     *            The daemon key
     */
    public static void startDaemon( String strDaemonKey )
    {
        scheduleThread( _mapDaemonEntries.get( strDaemonKey ) );
    }

    /**
     * Stops a daemon
     * 
     * @param strDaemonKey
     *            The daemon key
     */
    public static void stopDaemon( String strDaemonKey )
    {
        unScheduleThread( _mapDaemonEntries.get( strDaemonKey ) );
    }

    /**
     * Signal a daemon for execution in the immediate future.
     * <p>
     * This can fail is resources are limited, which should be exceptional.
     *
     * @param strDaemonKey
     *            the daemon key
     * @return <code>true</code> if the daemon was successfully signaled, <code>false</code> otherwise
     * @since 6.0.0
     */
    public static boolean signalDaemon( String strDaemonKey )
    {
        return signalDaemon( strDaemonKey, 0L, TimeUnit.MILLISECONDS );
    }

    /**
     * Signal a daemon for execution in the immediate future.
     * <p>
     * This can fail is resources are limited, which should be exceptional.
     *
     * @param strDaemonKey
     *            the daemon key
     * @param nDelay
     *            the delay before execution
     * @param unit
     *            the unit of <code>nDelay</code> argument
     * @return <code>true</code> if the daemon was successfully signaled, <code>false</code> otherwise
     * @since 6.0.0
     */
    public static boolean signalDaemon( String strDaemonKey, long nDelay, TimeUnit unit )
    {
        return _executor.enqueue( _mapDaemonEntries.get( strDaemonKey ), nDelay, unit );
    }

    /**
     * modify daemon interval
     * 
     * @param strDaemonKey
     *            The daemon key
     * @param strDaemonInterval
     *            the daemon interval
     */
    public static void modifyDaemonInterval( String strDaemonKey, String strDaemonInterval )
    {
        DaemonEntry entry = _mapDaemonEntries.get( strDaemonKey );

        if ( entry != null )
        {
            entry.setInterval( Long.valueOf( strDaemonInterval ) );
            DatastoreService.setInstanceDataValue( getIntervalKey( entry.getId( ) ), strDaemonInterval );
        }
    }

    /**
     * Add daemon to schedule's queue
     * 
     * @param entry
     *            The DaemonEntry
     */
    private static void scheduleThread( DaemonEntry entry )
    {
        scheduleThread( entry, _random.nextInt( AppPropertiesService.getPropertyInt( PROPERTY_MAX_INITIAL_START_DELAY, 30 ) ) );
    }

    /**
     * Add daemon to schedule's queue
     * 
     * @param entry
     *            The DaemonEntry
     * @param nInitialDelay
     *            Initial start delay
     */
    private static void scheduleThread( DaemonEntry entry, int nInitialDelay )
    {
        AppLogService.info( "Scheduling daemon {} ; first run in {} seconds", entry.getId( ), nInitialDelay );
        entry.setIsRunning( true );
        _executor.schedule( entry, nInitialDelay, TimeUnit.SECONDS );
        // update onStartup property
        DatastoreService.setInstanceDataValue( getOnStartupKey( entry.getId( ) ), DatastoreService.VALUE_TRUE );
    }

    /**
     * Remove daemon from schedule's queue
     * 
     * @param entry
     *            The DaemonEntry
     */
    private static void unScheduleThread( DaemonEntry entry )
    {
        cancelScheduledThread( entry.getId( ) );
        entry.setIsRunning( false );
        // update onStartup property
        DatastoreService.setInstanceDataValue( getOnStartupKey( entry.getId( ) ), DatastoreService.VALUE_FALSE );
        AppLogService.info( "Stopping daemon '{}'", entry.getId( ) );
    }

    /**
     * Cancel scheduled thread (don't interrupt if it is running )
     * 
     * @param strEntryId
     *            The DaemonEntry Id
     */
    protected static void cancelScheduledThread( String strEntryId )
    {
        _executor.unSchedule( _mapDaemonEntries.get( strEntryId ) );
    }

    /**
     * Get the current known DaemonEntries within the DaemonFactory
     *
     * @return the entries list of daemons declaration
     */
    public static Collection<DaemonEntry> getDaemonEntries( )
    {
        return _mapDaemonEntries.values( );
    }

    /**
     * Performs the shutdown of the DaemonFactory.
     */
    public static void shutdown( )
    {
        _executor.shutdown( );
    }

    /**
     * Gets a daemon object from its key name
     *
     * @param strDaemonKey
     *            The daemon key
     * @return The daemon
     */
    public static Daemon getDaemon( String strDaemonKey )
    {
        DaemonEntry entry = _mapDaemonEntries.get( strDaemonKey );

        return entry.getDaemon( );
    }

    /**
     * return the OnStartup key link to the daemon
     * 
     * @param strDaemonKey
     *            The daemon key
     * @return The key
     */
    private static String getOnStartupKey( String strDaemonKey )
    {
        return KEY_DAEMON_PREFIX + strDaemonKey + PROPERTY_DAEMON_ON_STARTUP;
    }

    /**
     * return the Interval key link to the daemon
     * 
     * @param strDaemonKey
     *            The daemon key
     * @return The key
     */
    private static String getIntervalKey( String strDaemonKey )
    {
        return KEY_DAEMON_PREFIX + strDaemonKey + PROPERTY_DAEMON_INTERVAL;
    }
}