001: /*
002: * <copyright>
003: *
004: * Copyright 1997-2004 BBNT Solutions, LLC
005: * under sponsorship of the Defense Advanced Research Projects
006: * Agency (DARPA).
007: *
008: * You can redistribute this software and/or modify it under the
009: * terms of the Cougaar Open Source License as published on the
010: * Cougaar Open Source Website (www.cougaar.org).
011: *
012: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
013: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
014: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
015: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
016: * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
017: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
018: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
019: * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
020: * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
021: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
022: * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
023: *
024: * </copyright>
025: */
026:
027: package org.cougaar.core.agent.service.alarm;
028:
029: import java.text.DateFormat;
030: import java.text.ParseException;
031: import java.text.SimpleDateFormat;
032: import java.util.Calendar;
033: import java.util.Date;
034: import java.util.GregorianCalendar;
035:
036: import org.cougaar.bootstrap.SystemProperties;
037: import org.cougaar.util.log.Logger;
038: import org.cougaar.util.log.Logging;
039:
040: /**
041: * A Timer for execution time (artificial "execution" or "scenario"
042: * time), which is a modifiable offset from real time with a rate
043: * multiplier.
044: * <p>
045: * Control the advancement of Execution time. Execution time is a
046: * monotonically increasing value whose rate can be controlled through
047: * the API implemented in this class. Execution time is nominally the
048: * same everywhere in the society. Small descrepancies may exist when
049: * the parameters of the exeuction time advancement are altered.
050: * Execution time is represented by a rate of advancement and an
051: * offset. The rate of advancment may be zero (time stands still) or
052: * any positive value. Excessively high rates may lead to anomalous
053: * behavior.
054: * <p>
055: * The equation of time is Te = Ts * Km + Ko where:<pre>
056: * Te is Execution time
057: * Ts is system time (System.currentTimeMillis())
058: * Km is the (positive) rate of advancement
059: * Ko is the offset
060: * </pre>
061: * <p>
062: * The System.currentTimeMillis() is presumed to be in sync in all
063: * agents within a few milliseconds by using NTP (Network Time
064: * Protocol). The maximum offset of the system clocks limits the
065: * maximum value that Km can have without introducing serious
066: * anomalies.
067: * <p>
068: * It is necessary for execution time to be monotonic. Monotonicity
069: * must be achieved even in the face of delays in propagating changes
070: * in the parameters of execution time advancement. The message that
071: * is used to alter the execution time advancement parameters
072: * specifically allows for the change to occur at some future time and
073: * it is expected that that will be the norm, but if the message
074: * transmission is delayed or if insufficient time is allowed, the
075: * equation of time must be altered to assure monotonicity.
076: * Furthermore, a succession of time advancement must ultimately
077: * result in all agents using the same time parameters. This is
078: * achieved by defining a sorting order on the time parameters that
079: * establishes a dominance relation between all such parameter sets.
080: * <p>
081: * Time change messages contain:<pre>
082: * New rate
083: * New offset
084: * Changeover (real) time
085: * </pre>
086: * <p>
087: * The changeover time is the first order sorting factor. If the changeover
088: * times are equal, the parameters yielding the highest execution time value
089: * at the changeover time dominate. If the execution times at the
090: * changeover time are equal, the change with the highest offset
091: * dominates.
092: * <p>
093: * The changeover time in this message is used as an offset from the
094: * current system time --- at current system time + changeover the new parameters
095: * will take effect. Normally, though, an absolute real time sync point should be
096: * specified to ensure that all nodes change parameters at the same time (again,
097: * assuming all host clocks are synchronized using NTP). An absolute real time
098: * change time can be specified using the ExecutionTimer.Parameters.create method,
099: * and setting changeIsAbsolute = true.
100: * <p>
101: * We want the execution timer to have the ability to initialize the natural-time
102: * clock to a particular time. This is
103: * problematic since we represent execution time as an offset from
104: * system time. Computing that offset consistently across all
105: * agents means that we have to compute a time (in the past) at
106: * which the parameters became effective and then compute the offset
107: * relative to that.
108: * <p>
109: * If org.cougaar.core.society.startTime is provided, we can
110: * synchronize from a common baseline point to reasonable accuracy.
111: * Otherwise, there is no way for all agents to reliably
112: * compute the same value, so we'll assume simultaneous starts.
113: *
114: * @property org.cougaar.core.agent.startTime The date to use as the
115: * start of execution for demonstration purposes. Accepts date/time
116: * in the form of <em>MM/dd/yyy_H:mm:ss</em>, <em>MM/dd/yyy H:mm:ss</em>,
117: * or <em>MM/dd/yyy</em>. The time sequence is optional and defaults to
118: * midnight on the specified date.
119: * agentStartTime must be in GMT. Note that if society.startTime is
120: * not fully specified, a multi-node society can have significant
121: * natural-time clock skew across the members.
122: *
123: * @property org.cougaar.core.society.startTime The real date-time stamp
124: * when the society was started. If supplied, can be used to synchronize
125: * the execution times of nodes which were started at different real times.
126: * society.startTime must be in GMT and ought to be generally
127: * slightly in the past. Format example: "09/12/2003 13:00:00"
128: *
129: * @property org.cougaar.core.society.timeOffset Specify an offset (in milliseconds)
130: * from real time to use as execution time. This is an alternative to
131: * using agent.startTime and society.startTime (if timeOffset is specified, then
132: * these other properties are ignored). Typical usage would be to specify
133: * the execution-time offset when (re)starting a node to match the other nodes
134: * in the society.
135: */
136: public class ExecutionTimer extends Timer {
137: private static final Logger logger = Logging
138: .getLogger("org.cougaar.core.agent.service.alarm.ExecutionTimer");
139:
140: public static final long DEFAULT_CHANGE_DELAY = 10000L;
141:
142: public static class Parameters implements Comparable,
143: java.io.Serializable {
144: public final long theOffset; // The offset (Ko)
145: public final double theRate; // The advancement rate (Km)
146: public final long theChangeTime; // The changeover time
147:
148: public Parameters(double aRate, long anOffset, long aChangeTime) {
149: theRate = aRate;
150: theOffset = anOffset;
151: theChangeTime = aChangeTime;
152: }
153:
154: public long computeTime(long now) {
155: return (long) (now * theRate) + theOffset;
156: }
157:
158: public int compareTo(Object o) {
159: Parameters other = (Parameters) o;
160: long diff = this .theChangeTime - other.theChangeTime;
161: if (diff < 0L)
162: return -1;
163: if (diff > 0L)
164: return 1;
165: diff = this .computeTime(this .theChangeTime)
166: - other.computeTime(other.theChangeTime);
167: if (diff < 0L)
168: return -1;
169: if (diff > 0L)
170: return 1;
171: diff = this .theOffset - other.theOffset;
172: if (diff < 0L)
173: return -1;
174: if (diff > 0L)
175: return 1;
176: return 0;
177: }
178:
179: public String toString() {
180: return ("Time = "
181: + new Date(computeTime(theChangeTime)).toString()
182: + "*" + theRate + "@" + new Date(theChangeTime)
183: .toString());
184: }
185: }
186:
187: /**
188: * Description of a Parameters change (relative to some implicit Parameters)
189: */
190: public static class Change {
191: public final long theOffsetDelta; // Step in the offset
192: public final double theRate; // New rate is absolute
193: public final long theChangeTimeDelta; // Change time relative to some other Parameters
194:
195: public Change(double aRate, long anOffsetDelta,
196: long aChangeTimeDelta) {
197: theOffsetDelta = anOffsetDelta;
198: theRate = aRate;
199: theChangeTimeDelta = aChangeTimeDelta;
200: }
201: }
202:
203: /**
204: * An array of Parameters. The array is sorted in the order in which
205: * they are to be applied. The 0-th element is the current setting.
206: */
207: Parameters[] theParameters = new Parameters[] {
208: new Parameters(1.0, 0L, 0L), null, // Allow up to five new parameters
209: null, null, null, };
210: private int theParameterCount = 1;
211:
212: long theCurrentExecutionTime; // This assures monotonicity
213:
214: /**
215: * Create Parameters that jump time by a specified amount and
216: * continue at a new rate thereafter.
217: */
218: public Parameters create(long millis, boolean millisIsAbsolute,
219: double newRate) {
220: synchronized (sem) {
221: return create(millis, millisIsAbsolute, newRate, false,
222: DEFAULT_CHANGE_DELAY);
223: }
224: }
225:
226: /**
227: * Create Parameters that jump time by a specified amount and
228: * continue at a new rate thereafter. The new rate is 0.0 if running
229: * is false, the current rate if running is true and the current
230: * rate is greater than 0.0 or 1.0 if running is true and the
231: * current rate is stopped.
232: */
233: public Parameters create(long millis, boolean millisIsAbsolute,
234: double newRate, boolean forceRunning) {
235: synchronized (sem) {
236: return create(millis, millisIsAbsolute, newRate,
237: forceRunning, DEFAULT_CHANGE_DELAY);
238: }
239: }
240:
241: /**
242: * Create Parameters that jump time by a specified amount and
243: * continue at a new rate thereafter. The new rate is 0.0 if running
244: * is false, the current rate if running is true and the current
245: * rate is greater than 0.0 or 1.0 if running is true and the
246: * current rate is stopped.
247: * @deprecated Use the version that allows specifying absolute change time instead
248: */
249: public Parameters create(long millis, boolean millisIsAbsolute,
250: double newRate, boolean forceRunning, long changeDelay) {
251: synchronized (sem) {
252: long changeTime = getNow() + changeDelay;
253: return create(millis, millisIsAbsolute, newRate,
254: forceRunning, changeTime, theParameters[0]);
255: }
256: }
257:
258: /**
259: * Create Parameters that jump time by a specified amount and
260: * continue at a new rate thereafter. The new rate is 0.0 if running
261: * is false, the current rate if running is true and the current
262: * rate is greater than 0.0 or 1.0 if running is true and the
263: * current rate is stopped.
264: * The new parameters can go into effect at a time relative to
265: * the current system time (changeIsAbsolute == false), or at a
266: * specific system time (changeIsAbsolute == true)
267: */
268: public Parameters create(long millis, boolean millisIsAbsolute,
269: double newRate, boolean forceRunning, long changeTime,
270: boolean changeIsAbsolute) {
271: synchronized (sem) {
272: if (!changeIsAbsolute)
273: changeTime = getNow() + changeTime;
274: return create(millis, millisIsAbsolute, newRate,
275: forceRunning, changeTime, theParameters[0]);
276: }
277: }
278:
279: private Parameters create(long millis, boolean millisIsAbsolute,
280: double newRate, boolean forceRunning, long changeTime,
281: Parameters relativeTo) {
282: long valueAtChangeTime = relativeTo.computeTime(changeTime);
283: if (Double.isNaN(newRate))
284: newRate = relativeTo.theRate;
285: if (Double.isInfinite(newRate)) {
286: throw new IllegalArgumentException("Illegal infinite rate");
287: }
288: if (newRate < 0.0) {
289: throw new IllegalArgumentException(
290: "Illegal negative rate: " + newRate);
291: }
292: if (forceRunning) {
293: if (newRate == 0.0) {
294: newRate = relativeTo.theRate;
295: }
296: if (newRate == 0.0) {
297: newRate = 1.0;
298: }
299: }
300: if (millisIsAbsolute) {
301: millis = millis - valueAtChangeTime;
302: }
303: if (millis < 0L) {
304: throw new IllegalArgumentException(
305: "Illegal negative advancement:" + millis);
306: }
307: long newOffset = valueAtChangeTime + millis
308: - (long) (changeTime * newRate);
309: return new Parameters(newRate, newOffset, changeTime);
310: }
311:
312: /**
313: * Creates a series of changes. The first change is relative to the
314: * current Parameters, the subsequent changes are relative to the
315: * previous change.
316: */
317: public Parameters[] create(Change[] changes) {
318: synchronized (sem) {
319: Parameters[] result = new Parameters[changes.length];
320: Parameters prev = theParameters[0];
321: long changeTime = getNow();
322: for (int i = 0; i < changes.length; i++) {
323: Change change = changes[i];
324: if (change.theChangeTimeDelta <= 0.0) {
325: throw new IllegalArgumentException(
326: "Illegal non-positive change time delta: "
327: + change.theChangeTimeDelta);
328: }
329: changeTime += change.theChangeTimeDelta;
330: result[i] = create(change.theOffsetDelta, false,
331: change.theRate, false, changeTime, prev);
332: prev = result[i];
333: }
334: return result;
335: }
336: }
337:
338: /**
339: * Get the current real time and insure that the current parameters
340: * are compatible with the real time being returned. The pending
341: * parameters become current if their time of applicability has been
342: * reached.
343: */
344: private long getNow() {
345: long now = System.currentTimeMillis();
346: while (theParameterCount > 1
347: && theParameters[1].theChangeTime <= now) {
348: System.arraycopy(theParameters, 1, theParameters, 0,
349: --theParameterCount);
350: theParameters[theParameterCount] = null;
351: }
352: return now;
353: }
354:
355: /**
356: * Get the current execution time in millis.
357: */
358: public long currentTimeMillis() {
359: synchronized (sem) {
360: long now = getNow();
361: long newTime = theParameters[0].computeTime(now);
362: if (newTime > theCurrentExecutionTime) {
363: theCurrentExecutionTime = newTime; // Only advance time, never decreases
364: }
365: return theCurrentExecutionTime;
366: }
367: }
368:
369: /**
370: * Insert new Parameters into theParameters. If the new parameters
371: * apply before the last element of theParameters, ignore them.
372: * Otherwise, append the new parameters overwriting the last parameters if necessary.
373: */
374: public void setParameters(Parameters parameters) {
375: synchronized (sem) {
376: getNow(); // Bring parameters up-to-now
377: if (parameters
378: .compareTo(theParameters[theParameterCount - 1]) > 0) {
379: if (theParameterCount < theParameters.length) {
380: if (logger.isInfoEnabled()) {
381: logger.info("Setting parameters "
382: + theParameterCount + " to "
383: + parameters);
384: }
385: theParameters[theParameterCount++] = parameters;
386: } else {
387: if (logger.isInfoEnabled()) {
388: logger.info("Setting parameters "
389: + (theParameterCount - 1) + " to "
390: + parameters);
391: }
392: theParameters[theParameterCount - 1] = parameters;
393: }
394: }
395: }
396: requestRun();
397: }
398:
399: protected long getMaxWait() {
400: if (theParameterCount > 1) {
401: return theParameters[1].theChangeTime
402: - System.currentTimeMillis();
403: } else {
404: return 100000000L;
405: }
406: }
407:
408: public double getRate() {
409: synchronized (sem) {
410: getNow(); // Bring parameters up-to-now
411: return theParameters[0].theRate;
412: }
413: }
414:
415: /**
416: */
417: public ExecutionTimer() {
418: long offset = computeInitialOffset();
419: if (offset != 0L) {
420: if (logger.isWarnEnabled()) {
421: logger.warn("Starting Time set to "
422: + new Date(System.currentTimeMillis() + offset)
423: + " offset=" + offset + "ms");
424: }
425: theParameters[0] = new Parameters(1.0, offset, 0L);
426: }
427: }
428:
429: private long computeInitialOffset() {
430: long offset = 0L;
431:
432: // society.timeOffset wins if specified
433: {
434: offset = SystemProperties.getLong(
435: "org.cougaar.core.society.timeOffset", DATE_ERROR);
436: if (offset != DATE_ERROR) {
437: if (logger.isInfoEnabled()) {
438: logger
439: .info("Exact time set by society.timeOffset ("
440: + offset + ")");
441: }
442: return offset;
443: }
444: }
445: offset = 0L; // reset so not == DATE_ERROR
446:
447: long now = System.currentTimeMillis();
448: ParsedPropertyDate adt = new ParsedPropertyDate(
449: "org.cougaar.core.agent.startTime");
450: ParsedPropertyDate sdt = new ParsedPropertyDate(
451: "org.cougaar.core.society.startTime");
452:
453: long target = adt.time;
454: if (target == DATE_ERROR) {
455: if (adt.date != DATE_ERROR) {
456: if (logger.isWarnEnabled()) {
457: logger
458: .warn("Inexact agent.startTime specified: Will default to midnight.");
459: }
460: target = adt.date;
461: }
462: }
463:
464: if (target != DATE_ERROR) { // fully-specified agent start time?
465: if (sdt.time != DATE_ERROR) { // fully-specified society start time?
466: // then we can compute exact offset
467: offset = target - sdt.time;
468: if (logger.isInfoEnabled()) {
469: logger
470: .info("Exact time set by agent-society times ("
471: + offset + ")");
472: }
473: } else {
474: if (sdt.date != DATE_ERROR) {
475: // useless: only partially-specified society start!
476: logger
477: .error("Ignoring partially-specified society.startTime "
478: + new Date(sdt.date));
479: // fall through...
480: }
481:
482: // no useful society.startTime: accept the skew, but complain
483: offset = target - now;
484: if (logger.isWarnEnabled()) { // check in case someone turns it off
485: logger
486: .warn("Multi-node societies will have execution-time clock skew: Set org.cougaar.core.society.startTime or society.timeOffset to avoid this problem.");
487: }
488: }
489: }
490: return offset;
491: }
492:
493: private static final long DATE_ERROR = Long.MIN_VALUE;
494:
495: private static class ParsedPropertyDate {
496: String name;
497: String value;
498: long time = DATE_ERROR;
499: long date = DATE_ERROR;
500:
501: ParsedPropertyDate(String propertyName) {
502: this .name = propertyName;
503: value = SystemProperties.getProperty(name);
504:
505: if (value != null) {
506: try {
507: DateFormat f;
508: try {
509: // try full date with "_" separator
510: f = (new SimpleDateFormat("MM/dd/yyy_H:mm:ss"));
511: time = f.parse(value).getTime();
512: } catch (ParseException e1) {
513: // try full date with " " separator
514: f = (new SimpleDateFormat("MM/dd/yyy H:mm:ss"));
515: time = f.parse(value).getTime();
516: }
517: // get midnight of specified date
518: Calendar c = f.getCalendar();
519: c.setTimeInMillis(time);
520: c.set(Calendar.HOUR, 0);
521: c.set(Calendar.MINUTE, 0);
522: c.set(Calendar.SECOND, 0);
523: c.set(Calendar.MILLISECOND, 0);
524: date = c.getTimeInMillis();
525: } catch (ParseException e) {
526: // try with just the date
527: try {
528: DateFormat f = (new SimpleDateFormat(
529: "MM/dd/yyy"));
530: time = f.parse(value).getTime();
531: } catch (ParseException e1) {
532: if (logger.isDebugEnabled())
533: logger.debug("Failed to parse property "
534: + propertyName
535: + " as date+time or just time: "
536: + value, e1);
537: }
538: }
539: }
540: }
541: }
542:
543: protected String getName() {
544: return "ExecutionTimer";
545: }
546:
547: /* ///////////////////////////////////////////////////////
548:
549: // point test
550:
551: public static void main(String args[]) {
552: // create a timer
553: ExecutionTimer timer = new ExecutionTimer();
554: timer.start();
555:
556: System.err.println("currentTimeMillis() = "+timer.currentTimeMillis());
557: // test running advance
558: timer.addAlarm(timer.createTestAlarm(60*60*1000)); // 60 min
559: timer.addAlarm(timer.createTestAlarm(60*60*1000+30*1000)); // 60min+30sec
560: timer.addAlarm(timer.createTestAlarm(30*60*1000)); // 30 min
561: timer.addAlarm(timer.createTestAlarm(5*1000));// 5 sec
562: timer.addAlarm(timer.createTestAlarm(10*1000)); // 10 sec
563: timer.addAlarm(timer.createTestAlarm(10*1000)); // 10 sec (again)
564: timer.sleep(15*1000); // wait 10 seconds
565: System.err.println("advancing running time 60 minutes");
566: timer.advanceRunningOffset(60*60*1000);
567: timer.sleep(20*1000); // wait 20sec
568: System.err.println("done waiting for running time.");
569:
570: // stopped tests
571: System.err.println("Trying stopped tests:");
572: long t = timer.currentTimeMillis()+10*1000;
573: timer.advanceStoppedTime(t);
574: System.err.println("currentTimeMillis() = "+timer.currentTimeMillis());
575: timer.addAlarm(timer.createTestAlarm(5*1000));// 5 sec
576: timer.addAlarm(timer.createTestAlarm(10*1000)); // 10 sec
577: timer.addAlarm(timer.createTestAlarm(30*60*1000)); // 30 min
578: timer.addAlarm(timer.createTestAlarm(60*60*1000)); // 60 min
579: timer.addAlarm(timer.createTestAlarm(60*60*1000+30*1000)); // 60min+30sec
580: timer.sleep(15*1000); // wait 10 seconds
581: System.err.println("advancing stopped time 5 seconds");
582: timer.advanceStoppedTime(t+5*1000);
583: timer.sleep(1*1000); // sleep a second
584: System.err.println("advancing stopped time to 10 seconds");
585: timer.advanceStoppedTime(t+10*1000);
586: timer.sleep(1*1000); // sleep a second
587: System.err.println("advancing stopped time to 60 minutes");
588: timer.advanceStoppedTime(t+60*60*1000);
589: timer.sleep(1*1000); // wait 20sec
590: System.err.println("starting clock");
591: timer.startRunning();
592: timer.sleep(20*1000); // wait 20sec
593: System.err.println("done waiting for running time.");
594:
595:
596: System.exit(0);
597: }
598:
599: public void sleep(long millis) {
600: try {
601: synchronized(this) {
602: this.wait(millis);
603: }
604: } catch (InterruptedException ie) {}
605: }
606:
607:
608: Alarm createTestAlarm(long delta) {
609: return new TestAlarm(delta);
610: }
611: private class TestAlarm implements Alarm {
612: long exp;
613: public TestAlarm(long delta) { this.exp = currentTimeMillis()+delta; }
614: public long getExpirationTime() {return exp;}
615: public void expire() { System.err.println("Alarm "+exp+" expired.");}
616: public String toString() { return "<"+exp+">";}
617: public boolean cancel() {} // doesn't support cancel
618: }
619:
620: /* */
621: }
|