View Javadoc

1   package org.lsst.ccs.localdb.statusdb;
2   
3   import java.lang.reflect.Field;
4   import java.util.ArrayList;
5   import java.util.List;
6   import java.util.Map;
7   import java.util.Properties;
8   import java.util.Queue;
9   import java.util.concurrent.ConcurrentHashMap;
10  import java.util.concurrent.ConcurrentLinkedQueue;
11  import org.hibernate.LockMode;
12  import org.hibernate.Query;
13  import org.hibernate.Session;
14  import org.hibernate.SessionFactory;
15  import org.hibernate.Transaction;
16  import org.lsst.ccs.bus.BusMessage;
17  import org.lsst.ccs.bus.DataValueNotification;
18  import org.lsst.ccs.bus.EncodedDataStatus;
19  import org.lsst.ccs.bus.KVList;
20  import org.lsst.ccs.bus.KeyData;
21  import org.lsst.ccs.bus.KeyValueStatusListener;
22  import org.lsst.ccs.bus.MetadataStatus;
23  import org.lsst.ccs.bus.ObjectNType;
24  import org.lsst.ccs.bus.StatusListener;
25  import org.lsst.ccs.bus.TrendingStatus;
26  import org.lsst.ccs.bus.ValueNotification;
27  import org.lsst.ccs.bus.trending.TrendingData;
28  import org.lsst.ccs.localdb.Trending;
29  import org.lsst.ccs.localdb.statusdb.model.DataDesc;
30  import org.lsst.ccs.localdb.statusdb.model.DataMetaData;
31  import org.lsst.ccs.localdb.statusdb.model.RawData;
32  import org.lsst.ccs.localdb.statusdb.model.StatData;
33  import org.lsst.ccs.localdb.statusdb.model.StatDesc;
34  import org.lsst.ccs.localdb.statusdb.utils.StatusdbUtils;
35  import org.lsst.ccs.utilities.beanutils.Optional;
36  import org.lsst.ccs.utilities.logging.Logger;
37  
38  /**
39   * StatusPersister object. Implements StatusListener and updates the db.
40   * <p/>
41   * Can be run inside a StatusPersisterSubsystem wrapper.
42   * <p/>
43   * Deprecated use: inside a Message Driver Bean (tied to JMS implementation)
44   * 
45   * @author aubourg
46   */
47  public class StatusPersister implements StatusListener, KeyValueStatusListener {
48  
49      /*
50       * public static AnnotationConfiguration ac = new AnnotationConfiguration();
51       * public static Configuration cfg = ac.configure("hibernate-tm.cfg.xml");
52       * public static SessionFactory fac = cfg.buildSessionFactory();
53       */
54  
55      static {
56  //        Logger.configure();
57      }
58      static Logger log = Logger.getLogger("org.lsst.ccs.localdb");
59      private static SessionFactory fac;
60  
61      public static synchronized void init(Properties p) {
62          fac = StatusdbUtils.getSessionFactory(p);
63      }
64  
65      Map<String, DataDesc> map = new ConcurrentHashMap<>();
66      protected DataWriter writer = new DataWriter();
67  
68      // TODO : force a re-read of the database
69      // or re-read the database periodically?
70      
71      // TODO : clean code and change method names. Too much history there...
72      
73      @SuppressWarnings("unchecked")
74      public StatusPersister(Properties p) {
75          if (p == null) {
76              p = new Properties();
77          }
78          init(p);
79          log.info("Starting StatusPersister");
80          Session sess = fac.openSession();
81          List<DataDesc> l = sess.createQuery("from DataDesc").list();
82          for (DataDesc dd : l) {
83              String key = dd.getSrcSubsystem() + "/" + dd.getSrcName();
84              map.put(key, dd);
85              log.info("storing " + key);
86          }
87          sess.close();
88  
89          // TODO  use executor or scheduler?
90          
91          new Thread(writer).start();
92  
93          // TODO stop cleanly the writer thread?
94      }
95  
96  
97      // TODO handle overflows, use blocking queue?
98      protected Queue<Object[]> rq = new ConcurrentLinkedQueue<>();
99  
100     public class DataWriter implements Runnable {
101 
102         @Override
103         public void run() {
104             ArrayList<Object[]> workingList = new ArrayList<>(1000);
105             long sleepTime = 1000L;
106             long lastSleep = -1L; // last time we slept after doing real work
107 
108             while (true) {
109 
110                 // get at most 1000 objets from the queue
111                 for (int i = 0; i < 1000; i++) {
112                     Object[] d = rq.poll();
113                     if (d == null) break;
114                     workingList.add(d);
115                 }
116 
117                 // see if queue is empty, time since last run, and sleep
118                 // accordingly
119                 // else persist
120                 if (workingList.isEmpty()) {
121                     log.info("empty list, sleeping");
122                     // TODO adjust sleep time dynamically
123                     try {
124                         Thread.sleep(sleepTime);
125                     } catch (InterruptedException e) {
126                         e.printStackTrace();
127                     }
128                 } else {
129                     log.info("batch persisting " + workingList.size()
130                             + " objects");
131                     Session sess = fac.openSession();
132                     Transaction tx = sess.beginTransaction();
133                     try {
134                         for (Object[] o : workingList) {
135                             if (o[0] instanceof RawData) {
136                                 persistData((RawData)o[0], (String)o[1], sess);
137                             } else {
138                                 persistMetadataStatus((MetadataStatus)o[0],
139                                                       sess);
140                             }
141                         }
142                     } catch (Exception e) {
143                         log.error(e.toString());
144                     }
145                     tx.commit();
146                     sess.close();
147                     workingList.clear();
148                 }
149             }
150 
151         }
152     }
153 
154 
155     @Override
156     public void onStatus(BusMessage s) {
157         if (s instanceof  EncodedDataStatus) {
158             queueEncodedData((EncodedDataStatus)s);
159         } else if (s instanceof TrendingStatus) {
160             queueTrendingStatus((TrendingStatus)s);
161         } else if (s instanceof MetadataStatus) {
162             queueMetadataStatus((MetadataStatus)s);
163         } else {
164             log.info("can't persist message " + s + " of class " + s.getClass());
165         }
166     }
167 
168 
169     @Override
170     public void onKeyValueStatusDecomposition(String source, long timeStamp,
171                                               String key, Object value,
172                                               int commonID) {
173         //TODO: will this work with "well-known" types that are not precisely
174         //  scalars?  arrays for instance
175         queueImmediateScalar(timeStamp, source+'/'+key, value);
176     }
177 
178 
179     public void queueTrendingStatus(TrendingStatus pv) {
180         queueData(pv.getOrigin(), pv.getTimeStamp(), pv.getValue());
181     }
182 
183 
184     /**
185      * persists A status with Encoded Data (list of keyValue pairs)
186      * @param encodedDataStatus
187      */
188     @Deprecated //KeyValueStatusListener takes care of that see method below
189     public void queueEncodedData(EncodedDataStatus encodedDataStatus) {
190         //todo: get rid of this code
191         for (EncodedDataStatus dataStatus : encodedDataStatus) {
192             KVList list = dataStatus.getContent();
193             for (KeyData keyData : list) {
194                 long timeStamp =  dataStatus.getDataTimestamp() ;
195                 List<KeyData> detailsList = keyData.getContentAsList();
196                 for(KeyData detaileddata : detailsList) {
197                     String key = detaileddata.getKey() ;
198                     Optional<Object> optional = detaileddata.getValue() ;
199                     if(optional.isPresent()) {
200                         queueImmediateScalar(timeStamp, key, optional.get());
201                     }
202                 }
203             }
204         }
205     }
206 
207 
208     // Trending data can be published in three ways
209     // - a single ValueNotification object, i.e. name, Object, timestamp
210     // - a list of ValueNotification objects for publishing a whole bunch of
211     // data with different timestamps (like in the case of the shutter movement)
212     // - to publish a lot of values with the same timestamp a structure is
213     // published. All the member variables annotated with @Trending will be
214     // added to the
215     // trending database with the name of the variable.
216     public void queueData(String origin, long ts, Object p) {
217         if (p instanceof DataValueNotification[]) {
218             DataValueNotification[] vv = (DataValueNotification[]) p;
219             for (DataValueNotification v : vv) {
220                 queueData(origin, v.gettStamp(), v);
221             }
222         } else if (p instanceof List<?> && !((List<?>) p).isEmpty()
223                 && ((List<?>) p).get(0) instanceof DataValueNotification) {
224             @SuppressWarnings("unchecked")
225             List<DataValueNotification> vv = (List<DataValueNotification>) p;
226             for (DataValueNotification v : vv) {
227                 queueData(origin, v.gettStamp(), v);
228             }
229 
230         } else if (p instanceof DataValueNotification) {
231             DataValueNotification dvNotification = (DataValueNotification) p;
232             queueDataValueNotification(origin, dvNotification);
233         } else if (p instanceof ValueNotification[]) {
234             ValueNotification[] vv = (ValueNotification[]) p;
235             for (ValueNotification v : vv) {
236                 queueData(origin, v.gettStamp(), v);
237             }
238         } else if (p instanceof List<?> && !((List<?>) p).isEmpty()
239                 && ((List<?>) p).get(0) instanceof ValueNotification) {
240             @SuppressWarnings("unchecked")
241             List<ValueNotification> vv = (List<ValueNotification>) p;
242             for (ValueNotification v : vv) {
243                 queueData(origin, v.gettStamp(), v);
244             }
245         } else if (p instanceof ValueNotification) {
246             ValueNotification dt = (ValueNotification) p;
247             Object data = dt.getData();
248             ts = dt.gettStamp();
249             if (data instanceof Number || data instanceof String) {
250                 String key = origin + "/" + dt.getName();
251                 queueImmediateScalar(ts, key, data);
252             } else {
253                 Class<? extends Object> dataClass = data.getClass();
254                 log.debug("will try to persist class " + dataClass);
255                 boolean saveAll = dataClass.isAnnotationPresent(Trending.class);
256                 Field[] fields = dataClass.getDeclaredFields();
257                 for (Field f : fields) {
258                     if (saveAll || f.isAnnotationPresent(Trending.class)) {
259                         try {
260                             log.debug("persisting field " + f.getName());
261                             Object d = f.get(data);
262                             String key = origin + "/" + dt.getName() + "/"
263                                     + f.getName();
264                             queueImmediateScalar(ts, key, d);
265                         } catch (IllegalArgumentException
266                                   | IllegalAccessException ex) {
267                             log.error("pb reading field", ex);
268                         }
269                     }
270 
271                 }
272             }
273         } else {
274             if (p instanceof List) {
275                 Object x = ((List) p).get(0);
276                 log.info("can't persist a list of " + x + " class " + x != null ? p
277                         .getClass() : "null");
278             } else {
279                 log.info("can't persist " + p + " class " + p.getClass());
280             }
281             // this is not worth an exception. Or is it?
282             // or should we try to persist stuff not encapsulated in
283             // ValueNotification?
284         }
285     }
286 
287 
288     public void queueDataValueNotification(String origin,
289                                            DataValueNotification dv) {
290         if (dv.isOfWellKnownType()) {
291             String name = origin + "/" + dv.getName();
292             queueDescribedScalar(dv.gettStamp(), name, dv.getObjectNType());
293         } else {
294             Map<String, ObjectNType> mapDesc = dv.getTrendingMap();
295             if (mapDesc != null) {
296                 for (Map.Entry<String, ObjectNType> entry : mapDesc.entrySet()) {
297                     String name = origin + "/" + entry.getKey();
298                     queueDescribedScalar(dv.gettStamp(), name, entry.getValue());
299                 }
300             } else {
301                 Object o = dv.getData();
302                 queueObject(origin + "/" + dv.getName(), dv.gettStamp(), o);
303             }
304         }
305     }
306 
307 
308     // This saves an object, either all fields, or fields with @Trending
309     // annotation.
310     // the new @Trendable annotation should be handled via maps in crystallized
311     // objects.
312     // TODO we might want to save some objects as BLOBs ?
313     private void queueObject(String name, long ts, Object data) {
314         Class<? extends Object> dataClass = data.getClass();
315         log.debug("will try to persist class " + dataClass);
316         boolean saveAll = dataClass.isAnnotationPresent(Trending.class);
317         Field[] fields = dataClass.getDeclaredFields();
318         for (Field f : fields) {
319             if (saveAll || f.isAnnotationPresent(Trending.class)
320                     || f.isAnnotationPresent(TrendingData.class)) {
321                 try {
322                     log.debug("persisting field " + f.getName());
323                     Object d = f.get(data);
324                     String key = name + "/" + f.getName();
325                     queueImmediateScalar(ts, key, d);
326                 } catch (IllegalArgumentException | IllegalAccessException ex) {
327                     log.error("pb reading field", ex);
328                 }
329             }
330         }
331     }
332 
333 
334     // TODO: this is copied from queueImmediateScalar!
335     public void queueDescribedScalar(long tStamp, String name,
336                                      ObjectNType descriptionAndValue) {
337         log.debug("got update " + name);
338 
339         // BEGIN COPIED
340         RawData data = new RawData();
341         data.setTstamp(tStamp);
342         // END COPIED
343         String className = descriptionAndValue.getClassName();
344         boolean useString = false;
345         Object value = null;
346         try {
347             value = descriptionAndValue.getData();
348         } catch (ClassNotFoundException e) {
349             // should not happen!
350             log.error("impossible deserialization", e);
351             return;
352         }
353         if (descriptionAndValue.isOfPrimitiveType()) {
354             if ("char".equals(className)) {
355                 useString = true;
356             }
357         } else {
358             Class dataClass = null;
359             try {
360                 dataClass = Class.forName(className);
361             } catch (ClassNotFoundException e) {
362                 log.error("impossible deserialization", e);
363                 return;
364             }
365             if (Number.class.isAssignableFrom(dataClass)) {
366                 useString = false;
367             } else {
368                 useString = true;
369             }
370         }
371         if (useString) {
372             data.setStringData(String.valueOf(value));
373         } else {
374             data.setDoubleData(((Number) value).doubleValue());
375         }
376         addToQueue(data, name);
377     }
378 
379 
380     public void queueImmediateScalar(long tStamp, String name, Object d) {
381  
382         log.debug("got update " + name);
383 
384         RawData data = new RawData();
385         data.setTstamp(tStamp);
386         if (d instanceof Double) {
387             data.setDoubleData((Double) d);
388         } else if (d instanceof Float) {
389             data.setDoubleData((Double) (double) (Float) d);
390         } else if (d instanceof Integer) {
391             data.setDoubleData((Double) (double) (Integer) d);
392         } else if (d instanceof Short) {
393             data.setDoubleData((Double) (double) (Short) d);
394         } else {
395 		//TODO: if array do an Array.toString
396             data.setStringData(String.valueOf(d));
397         }
398         addToQueue(data, name);
399     }
400 
401 
402     public void queueMetadataStatus(MetadataStatus mst) {
403         addToQueue(mst, null);
404     }
405 
406 
407     public void addToQueue(Object data, String name) {
408         rq.add(new Object[]{data, name});
409     }
410 
411 
412     private void persistData(RawData data, String name, Session sess) {
413 
414         DataDesc dd = getDataDescription(name, sess);
415         if (dd == null) return;
416 
417         data.setDescr(dd);
418         sess.lock(dd, LockMode.NONE);
419         sess.persist(data);
420 
421         List<StatDesc> stats = dd.getDerived();
422         // TODO use named queries
423         Query q = sess.createQuery("from StatData s where s.descr = :d "
424                                    + "order by s.tstampFirst desc");
425         q.setLockMode("s", LockMode.UPGRADE);
426         for (StatDesc stat : stats) {
427             q.setEntity("d", stat);
428             q.setMaxResults(1);
429             StatData sd = (StatData) q.uniqueResult();
430             if (sd == null) {
431                 sd = new StatData(stat, data);
432                 sess.persist(sd);
433             } else if (data.getTstamp() > sd.getTstampFirst()
434                     + stat.getTimeBinWidth()) {
435                 sd = new StatData(stat, data);
436                 sess.persist(sd);
437             } else {
438                 sd.accumulate(data);
439             }
440         }
441     }
442 
443 
444     private void persistMetadataStatus(MetadataStatus mst, Session sess) {
445 
446         String dataName = mst.getOrigin() + "/" + mst.getDataName();
447         DataDesc dd = getDataDescription(dataName, sess);
448         if (dd == null) return;
449 
450         sess.lock(dd, LockMode.NONE);
451 
452         DataMetaData metadata = new DataMetaData();
453         metadata.setName(mst.getMetadataName());
454         metadata.setRawDescr(dd);
455         metadata.setValue(mst.getMetadataValue());
456         metadata.setTstart(mst.getTimeStamp());
457 
458         // First update the tstop of a previously existing record for this
459         // metadata
460         Query q = sess.createQuery("from DataMetaData md "
461                                    + "where rawDescr_id = :id "
462                                    + "and name = :n and tstopmillis <= 0");
463         q.setParameter("id", dd.getId());
464         q.setParameter("n", metadata.getName());
465         DataMetaData oldMetaData = (DataMetaData) q.uniqueResult();
466         if (oldMetaData != null) {
467             oldMetaData.setTstop(metadata.getTstart());
468             sess.update(oldMetaData);
469         }
470         // Now commit the new value of the metadata
471         sess.persist(metadata);
472     }
473 
474 
475     private DataDesc getDataDescription(String key, Session sess) {
476 
477         log.debug("Looking for data description in map " + map + " " + key);
478         DataDesc dd = map.get(key);
479 
480         // TODO we should not do that in production.
481         // this piece of code will make ALL data passing on the bus be
482         // persisted by creating some default data description for it.
483         // in preliminary version, datadesc should be populated by hand
484         // TODO create GUI to allow user to select what should be persisted
485 
486         if (dd == null) {
487             log.debug("Adding default Data Description for " + key);
488             dd = new DataDesc();
489             dd.setDataType("a");
490             dd.setName(key);
491             int ind = key.indexOf("/");
492             dd.setSrcName(key.substring(ind + 1));
493             dd.setSrcSubsystem(key.substring(0, ind));
494             sess.persist(dd);
495             map.put(key, dd);
496         }
497 
498         return dd;
499     }
500 
501 }