View Javadoc

1   package org.lsst.ccs.bus;
2   
3   import java.util.ArrayList;
4   import java.util.HashMap;
5   import java.util.List;
6   import java.util.Map;
7   import java.util.Map.Entry;
8   import java.util.NoSuchElementException;
9   import java.util.concurrent.ConcurrentHashMap;
10  import java.util.concurrent.ConcurrentLinkedQueue;
11  import java.util.regex.Pattern;
12  
13  //This service will listen to the status bus and keep track in memory of selected status data. 
14  //
15  //It should be configurable: 
16  //- list of status data to record 
17  //- for some, keep some history for a defined duration 
18  //- for some, keep track of min, max, average, and other relevant aggregate data, for a defined window 
19  //
20  //It could be used 
21  //- by the FITS image writer to include data in header/tables 
22  //- by the MCM 
23  
24  // TODO configure from regular expression.
25  
26  // TODO keep some values for a number of samples and not a duration
27  
28  // TODO get a key/value map of all "last" values, or all "average" values.
29  
30  /**
31   * A <TT>StatusAggregator</TT> listens to the status bus and keeps track in
32   * memory of selected status data.
33   * 
34   * <p>
35   * It will keep at least the last value of each status data it is configured to
36   * monitor.
37   * </p>
38   * <p>
39   * It can also keep a history during a given time, and accumulate statistics
40   * over a given time.
41   * </p>
42   * 
43   * @author aubourg
44   * 
45   */
46  
47  public class StatusAggregator implements KeyValueStatusListener {
48  
49      Map<String, StatusAggregateConfig> config = new ConcurrentHashMap<String, StatusAggregateConfig>();
50  
51      Map<String, StatusAggregateConfig> patternConfig = new ConcurrentHashMap<String, StatusAggregateConfig>();
52  
53      Map<String, ConcurrentLinkedQueue<TimedValue>> history = new ConcurrentHashMap<String, ConcurrentLinkedQueue<TimedValue>>();
54      Map<String, TimedValue> last = new ConcurrentHashMap<String, TimedValue>();
55      Map<String, TimedValueStats> stats = new ConcurrentHashMap<String, TimedValueStats>();
56  
57      List<Pattern> patterns = new ArrayList<Pattern>();
58  
59      public StatusAggregator() {
60      }
61  
62      /**
63       * Configures the <tt>StatusAgregator</tt> to monitor a given data.
64       * 
65       * @param key
66       *            the name ("subsystem/key") of the data to be monitored
67       * 
68       * @param historyDuration
69       *            duration (in ms) for which to keep history. -1 not to keep
70       *            history.
71       * @param aggregateWindow
72       *            duration (in ms) for which to compute aggregates and stats. -1
73       *            not to compute.
74       */
75      public void setAggregate(String key, int historyDuration,
76              int aggregateWindow) {
77          config.put(key, new StatusAggregateConfig(key, historyDuration,
78                  aggregateWindow));
79          if (historyDuration > 0 || aggregateWindow > 0) {
80              history.put(key,
81                      new ConcurrentLinkedQueue<StatusAggregator.TimedValue>());
82          }
83  
84          if (aggregateWindow > 0) {
85              stats.put(key, new TimedValueStats());
86          }
87  
88      }
89  
90      public void setAggregatePattern(String pattern, int historyDuration,
91              int aggregateWindow) {
92          setAggregatePattern(Pattern.compile(pattern), historyDuration,
93                  aggregateWindow);
94      }
95  
96      public void setAggregatePattern(Pattern pattern, int historyDuration,
97              int aggregateWindow) {
98          StatusAggregateConfig c = new StatusAggregateConfig(pattern,
99                  historyDuration, aggregateWindow);
100         patternConfig.put(c.name, c);
101     }
102 
103     /**
104      * stop monitoring a given data.
105      * 
106      * @param key
107      *            the name ("subsystem/key") of the data to be cleared
108      * 
109      */
110     public void clearAggregate(String key) {
111         config.remove(key);
112         history.remove(key);
113         stats.remove(key);
114         last.remove(key);
115     }
116 
117     protected StatusAggregateConfig getConfig(String key) {
118         StatusAggregateConfig c = config.get(key);
119         if (c != null)
120             return c;
121 
122         for (Entry<String, StatusAggregateConfig> e : patternConfig.entrySet()) {
123             c = e.getValue();
124             if (!c.pattern.matcher(key).matches())
125                 continue;
126             c = new StatusAggregateConfig(key, c.historyDuration,
127                     c.aggregateWindow);
128             config.put(key, c);
129             if (c.historyDuration > 0 || c.aggregateWindow > 0) {
130                 history.put(
131                         key,
132                         new ConcurrentLinkedQueue<StatusAggregator.TimedValue>());
133             }
134 
135             if (c.aggregateWindow > 0) {
136                 stats.put(key, new TimedValueStats());
137             }
138 
139             return c;
140         }
141 
142         return null;
143     }
144 
145     @Override
146     public void onKeyValueStatusDecomposition(String source, long timeStamp,
147             String key, Object value, int commonID) {
148         String k = source + '/' + key;
149 
150         StatusAggregateConfig c = getConfig(k);
151         if (c == null)
152             return;
153 
154         last.put(k, new TimedValue(k, timeStamp, value));
155 
156         int kept = c.historyDuration > c.aggregateWindow ? c.historyDuration
157                 : c.aggregateWindow;
158         if (kept < 0)
159             return;
160 
161         // System.out.println("keeping "+kept+" for "+k);
162 
163         // remove old samples from aggregate and history
164 
165         long aggregateBound = c.aggregateWindow < 0 ? Long.MAX_VALUE
166                 : timeStamp - c.aggregateWindow;
167         // long historyBound = c.historyDuration < 0 ? Long.MAX_VALUE :
168         // timeStamp - c.historyDuration;
169         long stopBound = timeStamp - kept; // assuming timeStamp is upper bound
170                                            // : use current time ??
171 
172         ConcurrentLinkedQueue<TimedValue> q = history.get(k);
173 
174         if (q == null) {
175             q = new ConcurrentLinkedQueue<TimedValue>();
176             history.put(k, q);
177         }
178 
179         synchronized (q) {
180             q.add(new TimedValue(k, timeStamp, value));
181             if (c.aggregateWindow > 0) {
182                 TimedValueStats st = stats.get(k);
183                 if (st == null) {
184                     st = new TimedValueStats();
185                     stats.put(k, st);
186                 }
187                 if (value instanceof Number)
188                     st.add(((Number) value).doubleValue());
189 
190                 // iterate on queue and remove from aggregate
191                 if (aggregateBound > st.firstSample) {
192                     long firstRemaining = Long.MAX_VALUE;
193                     for (TimedValue v : q) {
194                         if (v.tStamp < aggregateBound
195                                 && v.tStamp >= st.firstSample
196                                 && v.value instanceof Number) {
197                             st.remove(((Number) v.value).doubleValue(), q,
198                                     aggregateBound);
199                         } else {
200                             if (v.tStamp < firstRemaining
201                                     && v.tStamp >= aggregateBound)
202                                 firstRemaining = v.tStamp;
203                         }
204                     }
205                     st.firstSample = firstRemaining;
206                 }
207             }
208             // poll from history
209             while (true) {
210                 TimedValue v = q.peek();
211                 if (v != null && v.tStamp < stopBound) {
212                     // System.out.println("removing from history " + v.name +
213                     // " "
214                     // + v.tStamp + " added " + timeStamp);
215                     q.poll();
216                 } else
217                     break;
218 
219             }
220         }
221 
222     }
223 
224     /**
225      * returns the last value seen for a given key (which has to be monitored)
226      * 
227      * @param key
228      * @return the last value seen on the status bus.
229      */
230 
231     public Object getLast(String key) {
232         TimedValue v = last.get(key);
233         return (v == null) ? null : v.value;
234     }
235 
236     public TimedValue getLastTV(String key) {
237         return last.get(key);
238     }
239 
240     /**
241      * returns the average (over the configured duration) for a given key.
242      * 
243      * The data has to be a number.
244      * 
245      * @param key
246      * @return the average, as a double
247      */
248 
249     public double getAverage(String key) {
250         TimedValueStats st = stats.get(key);
251         if (st == null)
252             throw new NoSuchElementException();
253         if (st.n == 0)
254             return 0;
255         // throw new NoSuchElementException();
256 
257         return st.average();
258     }
259 
260     // TODO distinguish between "not enough data" and
261     // "not configured to compute this" ?
262 
263     /**
264      * returns the stddev (over the configured duration) for a given key.
265      * 
266      * The data has to be a number.
267      * 
268      * @param key
269      * @return the stddev, as a double
270      */
271 
272     public double getStdDev(String key) {
273         TimedValueStats st = stats.get(key);
274         if (st == null)
275             throw new NoSuchElementException();
276         if (st.n == 0)
277             return 0;
278         // throw new NoSuchElementException();
279         return st.stddev();
280     }
281 
282     /**
283      * returns the min (over the configured duration) for a given key.
284      * 
285      * The data has to be a number.
286      * 
287      * @param key
288      * @return the min, as a double
289      */
290 
291     public double getMin(String key) {
292         TimedValueStats st = stats.get(key);
293         if (st == null)
294             throw new NoSuchElementException();
295         if (st.n == 0)
296             return 0;
297         // throw new NoSuchElementException();
298         return st.min;
299     }
300 
301     /**
302      * returns the max (over the configured duration) for a given key.
303      * 
304      * The data has to be a number.
305      * 
306      * @param key
307      * @return the max, as a double
308      */
309 
310     public double getMax(String key) {
311         TimedValueStats st = stats.get(key);
312         if (st == null)
313             throw new NoSuchElementException();
314         if (st.n == 0)
315             return 0;
316         // throw new NoSuchElementException();
317         return st.max;
318     }
319 
320     /**
321      * returns the history (over the configured duration) for a given key.
322      * 
323      * 
324      * @param key
325      * @return the history
326      */
327 
328     public List<TimedValue> getHistory(String key) {
329 
330         ConcurrentLinkedQueue<TimedValue> hh = history.get(key);
331         if (hh == null)
332             return null;
333 
334         ArrayList<TimedValue> l = new ArrayList<TimedValue>();
335         StatusAggregateConfig cf = config.get(key);
336         long lastSample = last.get(key).tStamp;
337         long firstSample = lastSample - cf.historyDuration;
338 
339         for (TimedValue tv : hh) {
340             if (tv.tStamp >= firstSample)
341                 l.add(tv);
342         }
343 
344         return l.size() == 0 ? null : l;
345     }
346 
347     public Statistics getStatistics(String key) {
348         ConcurrentLinkedQueue<TimedValue> q = history.get(key);
349         Statistics s;
350         synchronized (q) {
351             s = new Statistics(getMax(key), getMax(key), getAverage(key),
352                     getStdDev(key));
353         }
354         return s;
355     }
356 
357     public Map<String, Object> getAllLast() {
358         HashMap<String, Object> m = new HashMap<String, Object>();
359 
360         for (String k : stats.keySet()) {
361             m.put(k, getLast(k));
362         }
363 
364         return m;
365     }
366 
367     public Map<String, TimedValue> getAllLastTV() {
368         HashMap<String, TimedValue> m = new HashMap<String, TimedValue>();
369 
370         for (String k : stats.keySet()) {
371             m.put(k, getLastTV(k));
372         }
373 
374         return m;
375     }
376 
377     public Map<String, Statistics> getAllStatistics() {
378         HashMap<String, Statistics> m = new HashMap<String, Statistics>();
379 
380         for (String k : stats.keySet()) {
381             m.put(k, getStatistics(k));
382         }
383 
384         return m;
385     }
386 
387     public static class Statistics {
388 
389         public Statistics(double min, double max, double average, double stddev) {
390             super();
391             this.min = min;
392             this.max = max;
393             this.average = average;
394             this.stddev = stddev;
395         }
396 
397         public double getMin() {
398             return min;
399         }
400 
401         public double getMax() {
402             return max;
403         }
404 
405         public double getAverage() {
406             return average;
407         }
408 
409         public double getStddev() {
410             return stddev;
411         }
412 
413         private double min, max, average, stddev;
414     }
415 
416     private static class StatusAggregateConfig {
417 
418         public StatusAggregateConfig(String name, int historyDuration,
419                 int aggregateWindow) {
420             this.name = name;
421             this.historyDuration = historyDuration;
422             this.aggregateWindow = aggregateWindow;
423         }
424 
425         public StatusAggregateConfig(Pattern pattern, int historyDuration,
426                 int aggregateWindow) {
427             this.pattern = pattern;
428             this.name = pattern.toString();
429             this.historyDuration = historyDuration;
430             this.aggregateWindow = aggregateWindow;
431         }
432 
433         public String name; // "subsystem/name", key in map
434         public Pattern pattern; // if this is a regex config
435         public int historyDuration; // ms, <0 : do not keep
436         public int aggregateWindow; // ms, <0 : do not keep
437     }
438 
439     public static class TimedValue {
440 
441         public TimedValue(String name, long tStamp, Object value) {
442             super();
443             this.name = name;
444             this.tStamp = tStamp;
445             this.value = value;
446         }
447 
448         public String getName() {
449             return name;
450         }
451 
452         public long gettStamp() {
453             return tStamp;
454         }
455 
456         public Object getValue() {
457             return value;
458         }
459 
460         private String name;
461         private long tStamp;
462         private Object value;
463     }
464 
465     private static class TimedValueStats {
466         public String name;
467         public int n = 0;
468         public double sum = 0;
469         public double sum2 = 0;
470         public double min = Double.MAX_VALUE;
471         public double max = Double.MIN_NORMAL;
472 
473         public long firstSample;
474 
475         public double average() {
476             return (n > 0) ? (sum / n) : 0;
477         }
478 
479         public double stddev() {
480             if (n == 0)
481                 return 0;
482             return Math.sqrt(sum2 / n - sum * sum / (n * n));
483         }
484 
485         public void add(double value) {
486             sum += value;
487             sum2 += value * value;
488             n++;
489             updateMinMaxIn(value);
490         }
491 
492         public void remove(double value, ConcurrentLinkedQueue<TimedValue> h,
493                 long bound) {
494             sum -= value;
495             sum2 -= value * value;
496             n--;
497             updateMinMaxOut(value, h, bound);
498         }
499 
500         private void updateMinMaxIn(double newValue) {
501             if (newValue < min)
502                 min = newValue;
503             if (newValue > max)
504                 max = newValue;
505         }
506 
507         private void updateMinMaxOut(double removedValue,
508                 ConcurrentLinkedQueue<TimedValue> h, long bound) {
509             if (removedValue <= min || removedValue >= max) {
510                 min = Double.MAX_VALUE;
511                 max = Double.MIN_NORMAL;
512                 for (TimedValue v : h) {
513                     if (v.tStamp >= bound && v.value instanceof Number) {
514                         double x = ((Number) v.value).doubleValue();
515                         if (x > max)
516                             max = x;
517                         if (x < min)
518                             min = x;
519                     }
520                 }
521             }
522         }
523 
524     }
525 
526 }