/*
* Data Hub Service (DHuS) - For Space data distribution.
* Copyright (C) 2013,2014,2015 GAEL Systems
*
* This file is part of DHuS software sources.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fr.gael.dhus.sync;
import fr.gael.dhus.database.object.SynchronizerConf;
import java.text.ParseException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.quartz.CronExpression;
/**
* Synchronizer executor.
*
* Creates a thread to run {@link Synchronizer#synchronize()} in a loop every
* time the given cron expression is satisfied.
*
* Uses the Round-Robin algorithm if there is more than one Synchronizer.
*/
public final class ExecutorImpl implements Executor
{
private static final Logger LOGGER = LogManager.getLogger(ExecutorImpl.class);
/** Stores the synchronizers. */
private final Map<CronSchedule, List<StatSync>> synchronizers = new HashMap<> ();
/** The instance of {@link Executor.Runner}. */
private final Runner instance;
/**
* A Lock for thread synchronization purposes.
* Protects operations on the `synchronizers` Map.
* This lock MUST NOT protect blocking/time consuming operations.
*/
private final ReentrantLock lockSyncMap = new ReentrantLock ();
/** Reference to the synchronizer being run. */
private final AtomicReference<StatSync> runningSyncer = new AtomicReference<> ();
/** For the {@link #stop()} method. */
private final AtomicBoolean mustStop = new AtomicBoolean (false);
/** Enables the Batch synchronization mode. */
private final AtomicBoolean batchEnabled = new AtomicBoolean (false);
/** The current thread running the {@link Executor.Runner}. */
private Thread thread = null;
/** Constructor. */
public ExecutorImpl ()
{
this.instance = new Runner ();
}
/**
* Adds a {@link Synchronizer}.
* @param s a non null reference to a class extending {@link Synchronizer}.
* @return {@code true} if has been successfully added.
*/
@Override
public boolean addSynchronizer (Synchronizer s)
{
Objects.requireNonNull (s, "Param must not be null");
try
{
CronSchedule cron = new CronSchedule(s.getCronExpression ());
StatSync ss = new StatSync(s);
ss.status = SynchronizerStatus.makePendingStatus (cron.cronExpression);
this.lockSyncMap.lock ();
try
{
List<StatSync> sync_l = this.synchronizers.get (cron);
if (sync_l == null)
{
sync_l = new LinkedList<> ();
sync_l.add (ss);
this.synchronizers.put (cron, sync_l);
}
else if (!sync_l.contains (ss))
{
sync_l.add (ss);
}
// Wakes up the Runner
synchronized (this.instance)
{
this.instance.notify ();
}
}
finally
{
this.lockSyncMap.unlock ();
}
}
catch (ParseException | NullPointerException e)
{
LOGGER.debug ("failed to add a Synchronizer", e);
return false;
}
return true;
}
/**
* Removes the given {@ling Synchronizer}.
* Removes a synchronizer z such that {@code z.equals(s)}.
* THIS METHOD MIGHT BLOCK! if the synchronizer to remove is being run.
* @param s a non null reference to the DBO of the synchronizer to remove.
* @return the removed instance (you probably want to store its configuration
* back in the database) or {@code null} if not found.
*/
@Override
public Synchronizer removeSynchronizer (SynchronizerConf s)
{
Objects.requireNonNull (s, "Param must not be null");
Synchronizer res = null;
try
{
CronSchedule cron = new CronSchedule(s.getCronExpression ());
this.lockSyncMap.lock ();
try
{
List<StatSync> sync_l = this.synchronizers.get (cron);
if (sync_l != null && !sync_l.isEmpty ())
{
// Finds and remove `s` from the StatSyncList.
Iterator<StatSync> sync_it = sync_l.iterator();
while (sync_it.hasNext())
{
StatSync ss = sync_it.next();
if (ss.syncConf.getId() == s.getId())
{
res = ss;
sync_it.remove();
break;
}
}
// If found and removed
if (res != null)
{
// Terminate the Executor if `res` is being run
StatSync current = this.runningSyncer.get ();
if (current != null && current.equals (res))
{
this.thread.interrupt ();
// Block until res.synchronize is finished
synchronized (res)
{
res.getId ();
}
}
}
}
// If sync_l is empty, it must be removed from the Synchronizers map.
if (sync_l != null && sync_l.isEmpty())
{
this.synchronizers.remove(cron);
}
}
finally
{
this.lockSyncMap.unlock ();
}
}
catch (ParseException | NullPointerException e)
{
LOGGER.debug ("failed to remove a Synchronizer", e);
}
return res;
}
/**
* Removes all the synchronizers, does not stop the Executor.
*/
@Override
public void removeAllSynchronizers ()
{
this.lockSyncMap.lock ();
try
{
for (List<StatSync> v: this.synchronizers.values ())
{
// Here we don't have to check if removed synchronizers are being run
// because they are not returned by this method.
v.clear ();
}
this.synchronizers.clear();
}
finally
{
this.lockSyncMap.unlock ();
}
}
/**
* Returns {@code true} if the Executor is Running.
* @return {@code true} if the Executor is Running.
*/
@Override
public boolean isRunning ()
{
return this.thread != null &&
this.thread.isAlive () &&
!this.thread.isInterrupted ();
}
/**
* The Executor can run in batch mode, which means every time the schedule
* awakes the Executor, it will loop on the synchronizers until there is
* nothing more to synchronize.
*
* @param enabled {@code true} to enable the batch mode.
*/
@Override
public void enableBatchMode (boolean enabled)
{
this.batchEnabled.set (enabled);
}
/**
* Tells whether the batch mode is enabled.
* @return true if the the batch mode is enabled.
*/
@Override
public boolean isBatchModeEnabled ()
{
return this.batchEnabled.get ();
}
/**
* Starts the synchronization.
* The synchronization is done in a thread, this method returns immediately.
*
* @param start_now will start periodic synchronizers, and synchronizers
* scheduled in the past immediately.
*/
@Override
public void start (final boolean start_now)
{
// Do not use this.lock here, this method does not modify fields protected by this.lock
synchronized (this.instance)
{
this.mustStop.set (false);
if (this.thread == null || !this.thread.isAlive ())
{
Runnable rable = new Runnable ()
{
@Override
public void run ()
{
instance.runSynchronization (start_now);
}
};
this.thread = new Thread(rable, "SyncExecutor");
this.thread.start ();
}
}
}
/**
* Stops the synchronization after the current pass, then the inner thread
* will be stopped.
* Use {@link #start()} to restart the synchronization.
*/
@Override
public void stop ()
{
this.mustStop.set (true);
}
/**
* Calls {@link Thread#interrupt()} on the inner thread.
* The current synchronization pass will be abandoned and the inner thread
* will be stopped.
* Use {@link #start()} to restart the synchronization.
*/
@Override
public void terminate ()
{
this.mustStop.set (true);
if (this.thread!=null) this.thread.interrupt ();
}
/**
* Returns the status of a synchronizer.
* @param sc synchronizerConf of the synchronizer to query.
* @return an instance of SynchronizerStatus or null if not found.
*/
@Override
public SynchronizerStatus getSynchronizerStatus (SynchronizerConf sc)
{
try
{
List<StatSync> lss = this.synchronizers.get (new CronSchedule (sc.getCronExpression ()));
if (lss == null || lss.isEmpty ())
{
return null;
}
StatSync ss = null;
this.lockSyncMap.lock ();
try
{
for (StatSync ls: lss)
{
if (ls.getId () == sc.getId ())
{
ss = ls;
break;
}
}
}
finally
{
this.lockSyncMap.unlock ();
}
if (ss != null)
{
return ss.status;
}
return null;
}
catch (ParseException ex)
{
return null;
}
}
/** Decorator class adding status informations to {@link Synchronizer}s. */
private static class StatSync extends Synchronizer
{
/** Decorated instance. */
public Synchronizer sync;
/** Current status. */
public SynchronizerStatus status;
/** Constructor. */
public StatSync (Synchronizer sync)
{
super (sync.getSynchronizerConf ());
this.sync = sync;
}
/// Delegation.
@Override
public boolean synchronize () throws InterruptedException
{
return this.sync.synchronize ();
}
@Override
public long getId ()
{
return this.sync.getId ();
}
@Override
public String getCronExpression ()
{
return this.sync.getCronExpression ();
}
@Override
public SynchronizerConf getSynchronizerConf ()
{
return this.sync.getSynchronizerConf ();
}
}
/** A CronExpression with proper equals and hashcode methods. */
private static class CronSchedule
{
/** Cron expression as String. */
public final String cronString;
/** Cron expression as CronExpression. */
public final CronExpression cronExpression;
/** Constructor. */
public CronSchedule (String cron_string) throws ParseException
{
Objects.requireNonNull (cron_string);
this.cronString = cron_string;
this.cronExpression = new CronExpression (cron_string);
this.cronExpression.setTimeZone (TimeZone.getTimeZone ("UTC"));
}
@Override
public boolean equals (Object obj)
{
if (obj == null || !(obj instanceof CronSchedule))
{
return false;
}
CronSchedule other = (CronSchedule) obj;
return other.cronString.equals (cronString);
}
@Override
public int hashCode ()
{
return cronString.hashCode ();
}
}
/**
* An inner class to start {@link Synchronizer#synchronize()} in a
* thread.
* Will run {@link Synchronizer#synchronize()} every time the cron
* expression is satisfied.
*/
private class Runner
{
private Date lastRun;
/**
* Returns the next time a Synchronizer has to be executed.
* Returns {@code new Date (Long.MAX_VALUE)} if there is no synchronizer.
* @return the next wake up date.
*/
private Date getNextWakeUp () throws InterruptedException
{
Date res = new Date (Long.MAX_VALUE);
lockSyncMap.lockInterruptibly ();
try
{
for (CronSchedule cron: synchronizers.keySet ())
{
Date cmp = cron.cronExpression.getNextValidTimeAfter (lastRun);
if (cmp.before (res))
{
res = cmp;
}
}
}
finally
{
lockSyncMap.unlock ();
}
return res;
}
/**
* Returns a {@link List} of {@link Synchronizer} whose cron expression is
* triggered between the given parameters.
* @param start the /start/ Date (exclusive).
* @param end the /end/ Date (inclusive).
* @return a {@link List} of {@link Synchronizer}.
*/
private List<StatSync> getToSync (Date start, Date end) throws InterruptedException
{
List<StatSync> res = new LinkedList<> ();
lockSyncMap.lockInterruptibly ();
try
{
for (Map.Entry<CronSchedule, List<StatSync>> e: synchronizers.entrySet ())
{
Date next = e.getKey ().cronExpression.getNextValidTimeAfter (start);
if (next.before (end) || next.equals (end))
{
res.addAll (e.getValue ());
}
}
}
finally
{
lockSyncMap.unlock ();
}
return res;
}
/**
* Locks {@code synchronizers} and call {@link Synchronizer#synchronize()}
* if the given synchronizer still exist in the synchronizers map.
* @param s to synchronize.
* @return the value returned by {@link Synchronizer#synchronize()} or
* false if the synchronizer has'n been executed.
* @throws InterruptedException if the thread must stop.
*/
private boolean lockAndSynchronize (StatSync s) throws InterruptedException
{
boolean res = false;
try
{
CronSchedule cron = new CronSchedule (s.getCronExpression ());
lockSyncMap.lockInterruptibly ();
try
{
if (synchronizers.get (cron).contains (s))
{
runningSyncer.set (s);
synchronized (s)
{
// lockSyncMap is release after `s` has been held.
lockSyncMap.unlock ();
s.status = SynchronizerStatus.makeRunningStatus ();
long delta = System.currentTimeMillis ();
res = s.synchronize ();
delta = System.currentTimeMillis () - delta;
LOGGER.debug ("Synchronizer#" + s.getId () + " done in " + delta + "ms");
s.status = SynchronizerStatus.makePendingStatus (cron.cronExpression);
}
runningSyncer.set (null);
}
}
finally
{
// This test because unlock throws IllegalMonitorStateException if not locked
if (lockSyncMap.isHeldByCurrentThread ())
{
lockSyncMap.unlock ();
}
}
}
catch (ParseException e)
{
LOGGER.warn ("Unexpected exception");
}
return res;
}
/**
* Runs {@link Synchronizer#synchronize()} in a loop.
* @param start_now will start periodic synchronizers immediately.
*/
public void runSynchronization (boolean start_now)
{
if (start_now)
{
this.lastRun = new Date (0L);
}
else
{
this.lastRun = new Date ();
}
// Sync loop, broken when scheduleEnabled == false, or is interrupted.
for (;;)
{
try
{
// Schedule pace
Date lr_next = getNextWakeUp ();
Date now;
while ((now = new Date ()).before (lr_next))
{
synchronized (instance)
{
instance.wait (lr_next.getTime () - now.getTime ());
}
lr_next = getNextWakeUp ();
}
// Exit condition
if (mustStop.get () == true)
{
break;
}
// Batch mode
boolean has_more_sync = false;
// Synchronize
List<StatSync> sync_l = getToSync (lastRun, now);
if (sync_l.isEmpty ())
{ // can happen because the lock was released
continue;
}
ListIterator<StatSync> sync = sync_l.listIterator ();
for (;;)
{
StatSync ss = null;
try
{
if (!sync.hasNext ())
{
if (batchEnabled.get () && has_more_sync)
{
now = new Date ();
sync_l = getToSync (lastRun, now);
sync = sync_l.listIterator ();
}
else
{
break;
}
has_more_sync = false;
}
ss = sync.next ();
boolean more = lockAndSynchronize (ss);
has_more_sync = has_more_sync || more;
}
catch (InterruptedException e)
{
// Rethrows to break the loops
throw e;
}
catch (Exception e)
{
if (ss != null)
{
LOGGER.error ("Synchronizer#" + ss.getSynchronizerConf ().getId () +
" has thrown an exception", e);
ss.status = SynchronizerStatus.makeErrorStatus (e.getMessage ());
}
else
{
LOGGER.error ("A Synchronizer has thrown an exception", e);
}
// Expected behaviour: continue
}
if (Thread.interrupted ())
{
// Throws an InterruptedException to break the loops
throw new InterruptedException ();
}
}
lastRun = now;
}
catch (InterruptedException e)
{
if (mustStop.get () == true)
{
break; // Expected behaviour: quit the loop.
}
}
}
}
}
}