// $Id$
//
// Copyright 2007-2008 Cisco Systems Inc.
//
// 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.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
namespace Etch.Util
{
///
/// The alarm manager is used to implement alarms.
/// A listener desiring a wakeup call should add itself to the manager
/// with the specified delay in milliseconds. The listener may be removed
/// at any time. When the alarm goes off, the listener may reset or remove
/// the alarm.
///
public class AlarmManager : AbstractStartable
{
///
/// Adds the listener to the set of those getting wakeup calls.
///
/// the target of the wakeup call.
/// a bit of state for the listener.
/// the positive delay in milliseconds before the wakeup call.
public static void staticAdd(AlarmListener listener, Object state, int delay)
{
GetAlarmManager(true).Add(listener, state, delay);
}
///
/// Removes the listener from the set of those getting wakeup calls.
///
/// the target of the wakeup call.
public static void staticRemove(AlarmListener listener)
{
AlarmManager am = GetAlarmManager(false);
if (am != null)
am.Remove(listener);
}
///
/// Gets the statically configured alarm manager, creating one if
/// specified and none exists.
///
/// if true, we really need one and if there isn't
/// one then one should be started, otherwise one should not be started
/// he statically configured alarm manager, or a newly
/// created one if there is none.
public static AlarmManager GetAlarmManager(bool startIt)
{
if (alarmManager == null)
{
lock (alarmManagerSync)
{
if (alarmManager == null)
{
if (!startIt)
return null;
AlarmManager am = new AlarmManager();
am.Start();
alarmManager = am;
}
}
}
return alarmManager;
}
///
/// Sets the statically configured alarm manager, returning the old one
/// if any. Don't forget to stop the old one.
///
/// a new AlarmManager to be statically available
/// the old statically available AlarmManager.
public static AlarmManager SetAlarmManager(AlarmManager newAlarmManager)
{
lock (alarmManagerSync)
{
AlarmManager oldAlarmManager = alarmManager;
alarmManager = newAlarmManager;
return oldAlarmManager;
}
}
private static AlarmManager alarmManager;
private readonly static Object alarmManagerSync = new Object();
///
/// Constructs the AlarmManager.
///
/// the number of worker threads to create to process
/// wakeups. Must be > 0 and < 100.
public AlarmManager(int nWorkers)
{
if (nWorkers <= 0 || nWorkers >= 100)
throw new ArgumentException("nWorkers <= 0 || nWorkers >= 100");
this.nWorkers = nWorkers;
}
///
/// onstructs the AlarmManager with the default number of workers.
///
public AlarmManager()
: this(DEFAULT_NWORKERS)
{
}
private readonly int nWorkers;
///
/// The default number of worker threads (1).
///
public const int DEFAULT_NWORKERS = 1;
protected override void Start0()
{
ClearAlarms();
ClearQueue();
worker = new Thread[nWorkers];
for (int i = 0; i < nWorkers; i++)
{
worker[i] = new Thread(new ThreadStart(run));
worker[i].IsBackground = true;
worker[i].Start();
}
}
private Thread[] worker;
protected override void Stop0()
{
ClearAlarms();
ClearQueue();
System.Threading.Monitor.PulseAll(this);
for (int i = 0; i < nWorkers; i++)
{
if (worker[i] != null)
{
worker[i].Join();
worker[i] = null;
}
}
}
///
/// Adds the listener to the set of those getting wakeup calls. If the
/// listener is already scheduled for a wakeup call, the schedule is
/// adjusted. There can only be one outstanding wakeup call per listener.
/// This method is thread safe.
///
/// the target of the wakeup call.
/// a bit of state for the listener.
/// the positive delay in milliseconds before the wakeup call.
/// throws ArgumentException if the delay is less
/// than or equal to 0
[MethodImpl(MethodImplOptions.Synchronized)]
public void Add(AlarmListener listener, Object state, int delay)
{
if (listener == null)
throw new NullReferenceException("listener == null");
if (delay <= 0)
throw new ArgumentException("delay <= 0");
CheckIsStarted();
long due = HPTimer.Now() + delay * HPTimer.NS_PER_MILLISECOND;
Alarm alarm = GetAlarm(listener);
if (alarm != null)
{
// schedule is being adjusted
Dequeue(alarm);
alarm.setDue(due);
alarm.setState(state);
Enqueue(alarm);
}
else
{
alarm = new Alarm(listener, state, due);
AddAlarm(listener, alarm);
Enqueue(alarm);
}
NotifyWorker("add");
}
[MethodImpl(MethodImplOptions.Synchronized)]
private void Update(Alarm alarm, int delay)
{
long due = delay > 0
? alarm.getDue() + delay * HPTimer.NS_PER_MILLISECOND
: HPTimer.Now() - delay * HPTimer.NS_PER_MILLISECOND;
alarm.setDue(due);
Enqueue(alarm);
NotifyWorker("update");
}
///
/// Removes any scheduled wakeup call for this listener.
/// This method is thread safe.
///
/// the target of the wakeup call.
[MethodImpl(MethodImplOptions.Synchronized)]
public void Remove(AlarmListener listener)
{
CheckIsStarted();
Alarm alarm = RemoveAlarm(listener);
if (alarm != null)
Dequeue(alarm);
NotifyWorker("remove");
}
[MethodImpl(MethodImplOptions.Synchronized)]
private void Remove(Alarm alarm)
{
RemoveAlarm(alarm.listener);
}
private void Wakeup(Alarm alarm)
{
try
{
int delay = alarm.listener.Wakeup(this, alarm.getState(), alarm.getDue());
if (delay != 0)
Update(alarm, delay);
else
Remove(alarm);
}
catch (Exception)
{
Remove(alarm);
// Log.report( "wakeup", "who", alarm.listener, Log.EXCP, e );
}
}
private Alarm getNextDueAlarm()
{
// ok, the worker needs to get the next due alarm and
// then wait until its due time, then return it. if alerted
// by notifyWorker, it should refresh the next due alarm.
// one trick will be in excluding multiple workers from
// coming in here at the same time.
lock (getNextDueAlarmSync)
{
lock (this)
{
while (true)
{
//
if (!IsStarted())
return null;
Alarm alarm = getFirst();
if (alarm == null)
{
try
{
//Console.WriteLine(" Waiting in getNextDueAlarm ");
System.Threading.Monitor.Wait(this, Int32.MaxValue);
//Console.WriteLine(" Done Waiting in getNextDueAlarm ");
continue;
}
catch (Exception e)
{
Console.WriteLine(e);
return null;
}
}
long delayNs = alarm.getDue() - HPTimer.Now();
if (delayNs > 0)
{
try
{
long delay = delayNs / HPTimer.NS_PER_MILLISECOND;
if (delay > 0)
{
int d = (int)delay;
System.Threading.Monitor.Wait(this, d);
}
continue;
}
catch (Exception e)
{
Console.WriteLine(e);
return null;
}
}
// the alarm being returned has not been removed
// from alarmsByListener. it is presumed that the
// alarm will be set again. if not, it should be
// removed.
Dequeue(alarm);
return alarm;
}
}
}
}
private readonly Object getNextDueAlarmSync = new Object();
private void NotifyWorker(String reason)
{
// Log.report( "notify", "who", this, "reason", reason, "where", new Throwable() );
// the set of alarms has changed.
System.Threading.Monitor.Pulse(this);
}
public void run()
{
try
{
Alarm alarm;
while ((alarm = getNextDueAlarm()) != null)
{
Wakeup(alarm);
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
private Alarm GetAlarm(AlarmListener listener)
{
Alarm alarm;
return alarmsByListener.TryGetValue(listener, out alarm) ? alarm : null;
}
private void AddAlarm(AlarmListener listener, Alarm alarm)
{
alarmsByListener.Add(listener, alarm);
}
private Alarm RemoveAlarm(AlarmListener listener)
{
Alarm alarm = GetAlarm(listener);
if (alarm != null)
alarmsByListener.Remove(listener);
return alarm;
}
private void ClearAlarms()
{
alarmsByListener.Clear();
}
private readonly Dictionary alarmsByListener = new Dictionary();
////////////////////////
// ALARMS BY DUE TIME //
////////////////////////
private Alarm getFirst()
{
if (alarms.Count == 0)
return null;
return alarms[0];
}
private void Enqueue(Alarm alarm)
{
alarms.Add(alarm);
alarms.Sort();
}
private void Dequeue(Alarm alarm)
{
alarms.Remove(alarm);
}
private void ClearQueue()
{
alarms.Clear();
}
private readonly List alarms = new List();
private class Alarm : IComparable
{
///
///
///
/// the target of the wakeup call.
/// a bit of state for the listener.
/// he absolute due time for the alarm.
public Alarm(AlarmListener listener, Object state, long due)
{
this.listener = listener;
this.state = state;
this.due = due;
}
///
///
///
/// the state for the listener.
public Object getState()
{
return state;
}
///
///
///
/// a new bit of state for the listener./param>
public void setState(Object state)
{
this.state = state;
}
///
///
///
/// the time the alarm is due.
public long getDue()
{
return due;
}
///
///
///
///
public void setDue(long due)
{
this.due = due;
}
public override int GetHashCode()
{
return (int)((due ^ (due >> 32)) ^ (seq ^ (seq >> 32)));
}
public override bool Equals(Object obj)
{
if (obj == this)
return true;
if (obj == null)
return false;
if (obj.GetType() != typeof(Alarm))
return false;
Alarm other = (Alarm)obj;
return due == other.due && seq == other.seq;
}
public int CompareTo(Alarm other)
{
if (due < other.due)
return -1;
if (due > other.due)
return 1;
// due time is the same for both, now we need to
// compare the seq.
if (seq < other.seq)
return -1;
if (seq > other.seq)
return 1;
return 0;
}
///
/// The listener for wakeup events.
///
public readonly AlarmListener listener;
///
/// Just a bit of state for the listener.
///
private Object state;
///
/// The time the alarm is due.
///
private long due;
///
/// A unique for all reasonable time sequence number.
///
private readonly long seq = idGen.Next();
private readonly static IdGenerator idGen = new IdGenerator();
}
///
/// Shuts down the default alarm manager if there is one.
///
public static void shutdown()
{
AlarmManager am = SetAlarmManager(null);
if (am != null)
am.Stop();
}
}
}