View Javadoc

1   /*
2    * Copyright (c) 2004 UNINETT FAS
3    *
4    * This program is free software; you can redistribute it and/or modify it
5    * under the terms of the GNU General Public License as published by the Free
6    * Software Foundation; either version 2 of the License, or (at your option)
7    * any later version.
8    *
9    * This program is distributed in the hope that it will be useful, but WITHOUT
10   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11   * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12   * more details.
13   *
14   * You should have received a copy of the GNU General Public License along with
15   * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
16   * Place - Suite 330, Boston, MA 02111-1307, USA.
17   *
18   * $Id: TicketTTLEvictionPolicy.java,v 1.11 2005/03/14 14:26:47 bos Exp $
19   */
20  
21  package no.feide.moria.store;
22  
23  import java.net.InetAddress;
24  import java.util.ArrayList;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.Timer;
30  import java.util.Vector;
31  
32  import no.feide.moria.log.MessageLogger;
33  
34  import org.jboss.cache.CacheException;
35  import org.jboss.cache.Fqn;
36  import org.jboss.cache.Node;
37  import org.jboss.cache.TreeCache;
38  import org.jboss.cache.eviction.EvictionAlgorithm;
39  import org.jboss.cache.eviction.EvictionTimerTask;
40  import org.jboss.cache.eviction.LRUPolicy;
41  import org.jboss.cache.eviction.Region;
42  import org.jboss.cache.eviction.RegionManager;
43  import org.jboss.cache.eviction.RegionNameConflictException;
44  import org.jboss.cache.lock.LockingException;
45  import org.jboss.cache.lock.TimeoutException;
46  import org.jgroups.MergeView;
47  import org.jgroups.View;
48  import org.jgroups.stack.IpAddress;
49  import org.w3c.dom.Element;
50  import org.w3c.dom.NodeList;
51  import org.w3c.dom.Text;
52  
53  /***
54   * This eviction policy evicts tickets after a fixed period, aka Time To Live.
55   *
56   * @author Bjørn Ola Smievoll <b.o.smievoll@conduct.no>
57   * @version $Revision: 1.11 $
58   */
59  
60  public final class TicketTTLEvictionPolicy extends LRUPolicy {
61  
62      /*** The logger used by this class. */
63      private MessageLogger messageLogger = new MessageLogger(TicketTTLEvictionPolicy.class);
64  
65      /*** Manages the regions for each ticket type. */
66      private RegionManager regionManager;
67  
68      /***  The root node. */
69      private static final Fqn ROOT = new Fqn("");
70  
71      /*** Default interval value for running the evictions. */
72      private static final int WAKEUP_INTERVAL_DEFAULT = 5;
73  
74      /*** Default value for max nodes in each region. */
75      private static final int MAX_NODES_DEFAULT = 0x186a0;
76  
77      /*** Constant representing the region name key. */
78      private static final String REGION_NAME = "region";
79  
80      /*** Constant representing the attribute name key. */
81      private static final String ATTRIBUTE_NAME = "attribute";
82  
83      /*** Constant representing the name attribute key. */
84      private static final String NAME_ATTRIBUTE_NAME = "name";
85  
86      /*** Constant representing the wakeup interval key. */
87      private static final String WAKEUP_INTERVAL_NAME = "wakeUpIntervalSeconds";
88  
89      /*** Constant representing the max nodes key. */
90      private static final String MAX_NODES_NAME = "maxNodes";
91  
92      /*** Constant representing the ttl attribute key. */
93      private static final String TTL_ATTRIBUTE_NAME = "timeToLive";
94  
95      /*** Constant holding default error message. */
96      private static final String OPERATION_FAILED_MESSAGE = "Operation failed";
97  
98      /*** Contains the different regions registered for this policy. */
99      private RegionValue[] regionValues;
100 
101     /*** The interval for the eviction threads to run. */
102     private int wakeUpIntervalSeconds;
103 
104     /*** Maximal number of nodes in a region. Not really used, should be set high. */
105     private int maxNodes;
106 
107     /*** The timer responsible for scheduling the eviction threads. */
108     private Timer evictionTimer;
109 
110     /*** The cache the evictions are done on. */
111     private TreeCache cache;
112 
113     /***
114      * Creates a new instance.
115      */
116     public TicketTTLEvictionPolicy() {
117     }
118 
119     /***
120      * @see org.jboss.cache.eviction.EvictionPolicy#configure(org.jboss.cache.TreeCache)
121      */
122     public void configure(final TreeCache cache) {
123         parseConfig(cache.getEvictionPolicyConfig());
124         regionManager = new RegionManager(this);
125         this.cache = cache;
126 
127         for (int i = 0; i < regionValues.length; i++) {
128             EvictionAlgorithm algorithm = new TicketTTLEvictionAlgorithm();
129 
130             try {
131                 Region region = regionManager.createRegion(ROOT.toString() + regionValues[i].regionName, algorithm);
132                 region.setMaxNodes(maxNodes);
133                 region.setTimeToLiveSeconds(wakeUpIntervalSeconds);
134             } catch (RegionNameConflictException e) {
135                 messageLogger.logWarn("Name conflict in region naming.", e);
136                 throw new EvictionConfigurationException("Unable to create region " + regionValues[i].regionName, e);
137             }
138         }
139 
140     }
141 
142     /***
143      * @see org.jboss.cache.eviction.EvictionPolicy#getRegions()
144      */
145     public Region[] getRegions() {
146         return regionManager.getRegions();
147     }
148 
149     /***
150      * @see org.jboss.cache.eviction.EvictionPolicy#evict(org.jboss.cache.Fqn)
151      */
152     public void evict(final Fqn fqn)
153             throws Exception {
154         cache.evict(fqn);
155     }
156 
157     /***
158      * @see org.jboss.cache.eviction.EvictionPolicy#getChildrenNames(org.jboss.cache.Fqn)
159      */
160     public Set getChildrenNames(final Fqn fqn) {
161         /* Here the API forces us to use RuntimeException. */
162         try {
163             return cache.getChildrenNames(fqn);
164         } catch (LockingException le) {
165             messageLogger.logWarn(OPERATION_FAILED_MESSAGE, le);
166             throw new RuntimeException(le);
167         } catch (TimeoutException te) {
168             messageLogger.logWarn(OPERATION_FAILED_MESSAGE, te);
169             throw new RuntimeException(te);
170         } catch (CacheException ce) {
171             messageLogger.logWarn(OPERATION_FAILED_MESSAGE, ce);
172             throw new RuntimeException(ce);
173         }
174     }
175 
176     /***
177      * @see org.jboss.cache.eviction.EvictionPolicy#hasChild(org.jboss.cache.Fqn)
178      */
179     public boolean hasChild(final Fqn fqn) {
180         return cache.hasChild(fqn);
181     }
182 
183     /***
184      * @see org.jboss.cache.eviction.EvictionPolicy#getCacheData(org.jboss.cache.Fqn, java.lang.Object)
185      */
186     public Object getCacheData(final Fqn fqn, final Object key) {
187         /* Here the API forces us to use RuntimeException. */
188         try {
189             return cache.get(fqn, key);
190         } catch (LockingException le) {
191             messageLogger.logWarn(OPERATION_FAILED_MESSAGE, le);
192             throw new RuntimeException(le);
193         } catch (TimeoutException te) {
194             messageLogger.logWarn(OPERATION_FAILED_MESSAGE, te);
195             throw new RuntimeException(te);
196         } catch (CacheException ce) {
197             messageLogger.logWarn(OPERATION_FAILED_MESSAGE, ce);
198             throw new RuntimeException(ce);
199         }
200     }
201 
202     /***
203      * @see org.jboss.cache.eviction.EvictionPolicy#getWakeupIntervalSeconds()
204      */
205     public int getWakeupIntervalSeconds() {
206         return wakeUpIntervalSeconds;
207     }
208 
209     /***
210      * @see org.jboss.cache.TreeCacheListener#nodeAdded(org.jboss.cache.Fqn)
211      */
212     public void nodeCreated(final Fqn fqn) {
213         if (fqn.equals(ROOT))
214             return;
215 
216         if (regionManager != null) {
217             Region region = regionManager.getRegion(fqn.toString());
218 
219             if (region != null) {
220                 region.setAddedNode(fqn);
221                 messageLogger.logDebug("Node created: " + fqn);
222             } else {
223                 messageLogger.logInfo("No region returned for created node: " + fqn);
224             }
225         } else {
226             messageLogger.logWarn("regionManager is null");
227         }
228     }
229 
230     /***
231      * @see org.jboss.cache.TreeCacheListener#nodeLoaded(org.jboss.cache.Fqn)
232      */
233     public void nodeLoaded(final Fqn fqn) {
234         if (fqn.equals(ROOT))
235             return;
236 
237         if (regionManager != null) {
238             Region region = regionManager.getRegion(fqn.toString());
239 
240             if (region != null) {
241                 region.setAddedNode(fqn);
242                 messageLogger.logDebug("Node loaded: " + fqn);
243             } else {
244                 messageLogger.logInfo("No region returned for loaded node: " + fqn);
245             }
246         } else {
247             messageLogger.logWarn("regionManager is null");
248         }
249     }
250 
251     /***
252      * @see org.jboss.cache.TreeCacheListener#nodeRemoved(org.jboss.cache.Fqn)
253      */
254     public void nodeRemoved(final Fqn fqn) {
255         if (fqn.equals(ROOT))
256             return;
257 
258         if (regionManager != null) {
259             Region region = regionManager.getRegion(fqn.toString());
260 
261             if (region != null) {
262                 region.setRemovedNode(fqn);
263                 messageLogger.logDebug("Listener got node removed: " + fqn);
264             } else {
265                 messageLogger.logInfo("No region returned for removed node: " + fqn);
266             }
267         } else {
268             messageLogger.logWarn("regionManager is null");
269         }
270     }
271 
272     /***
273      * @see org.jboss.cache.TreeCacheListener#nodeEvicted(org.jboss.cache.Fqn)
274      */
275     public void nodeEvicted(final Fqn fqn) {
276         if (fqn.equals(ROOT))
277             return;
278 
279         messageLogger.logDebug("Listener got node evicted: " + fqn);
280     }
281 
282     /***
283      * @see org.jboss.cache.TreeCacheListener#nodeModified(org.jboss.cache.Fqn)
284      */
285     public void nodeModified(final Fqn fqn) {
286         if (fqn.equals(ROOT))
287             return;
288 
289         if (regionManager != null) {
290             Region region = regionManager.getRegion(fqn.toString());
291 
292             if (region != null) {
293                 region.setVisitedNode(fqn);
294                 messageLogger.logDebug("Listener got node modified: " + fqn);
295             } else {
296                 messageLogger.logInfo("No region returned for modified node: " + fqn);
297             }
298         } else {
299             messageLogger.logWarn("regionManager is null");
300         }
301     }
302 
303     /***
304      * @see org.jboss.cache.TreeCacheListener#nodeVisited(org.jboss.cache.Fqn)
305      */
306     public void nodeVisited(final Fqn fqn) {
307         if (fqn.equals(ROOT))
308             return;
309 
310         if (regionManager != null) {
311             Region region = regionManager.getRegion(fqn.toString());
312 
313             if (region != null) {
314                 region.setVisitedNode(fqn);
315                 messageLogger.logDebug("Listener got node visited: " + fqn);
316             } else {
317                 messageLogger.logInfo("No region returned for visited node: " + fqn);
318             }
319         } else {
320             messageLogger.logWarn("regionManager is null");
321         }
322     }
323 
324     /***
325      * @see org.jboss.cache.TreeCacheListener#cacheStarted(org.jboss.cache.TreeCache)
326      */
327     public void cacheStarted(final TreeCache cache) {
328         messageLogger.logInfo("Starting eviction policy using provider: " + this.getClass().getName());
329 
330         if (!this.cache.equals(cache)) {
331             messageLogger.logWarn("Cache instance given on start not equal to configured cache.");
332         }
333 
334         evictionTimer = new Timer();
335         evictionTimer.schedule(new EvictionTimerTask(this), 1000, wakeUpIntervalSeconds * 1000);
336     }
337 
338     /***
339      * @see org.jboss.cache.TreeCacheListener#cacheStopped(org.jboss.cache.TreeCache)
340      */
341     public void cacheStopped(final TreeCache cache) {
342 
343         if (!this.cache.equals(cache)) {
344             messageLogger.logWarn("Cache instance given on stop not equal to configured cache.");
345         }
346 
347         if (evictionTimer != null) {
348             evictionTimer.cancel();
349             messageLogger.logInfo("Stopped eviction policy timer.");
350         } else {
351             messageLogger.logInfo("Cache stop called with uninitialized eviction timer.");
352         }
353     }
354 
355     /***
356      * @see org.jboss.cache.TreeCacheListener#viewChange(org.jgroups.View)
357      */
358     public void viewChange(final View view) {
359         messageLogger.logDebug("Listener got a view change.");
360 
361         Vector subGroupMembers = null;
362         final Object myAddress = cache.getLocalAddress();
363 
364         /* Exit unless this is a merge. */
365         if (view instanceof MergeView) {
366             messageLogger.logDebug("View is a MergeView!");
367             MergeView mergeView = (MergeView) view;
368 
369             Vector subgroups = mergeView.getSubgroups();
370 
371             for (int v = 0; v < subgroups.size(); v++) {
372                 subGroupMembers = ((View) subgroups.get(v)).getMembers();
373                 /* We run until we find a the subgroup we were coordinating. */
374                 if (subGroupMembers.get(0).equals(myAddress)) {
375                     break;
376                 } else {
377                     subGroupMembers = null;
378                 }
379             }
380         } else {
381             return;
382         }
383 
384         /* If we're one of the old coords we get workin'. Basically we
385          * recommit all nodes that belonged to us. */
386         if (subGroupMembers != null) {
387             messageLogger.logDebug("This was previously a coordinator.");
388 
389             /* Identify our group members. */
390             ArrayList memberIds = new ArrayList();
391 
392             for (Iterator i = subGroupMembers.iterator(); i.hasNext();) {
393                 /* Create ticket id prefix. */
394                 String nodeId;
395 
396                 Object memberAddress = i.next();
397 
398                 if (memberAddress instanceof IpAddress) {
399                     IpAddress ipAddress = (IpAddress) memberAddress;
400                     InetAddress inetAddress = ipAddress.getIpAddress();
401                     nodeId = inetAddress.getHostAddress() + ":" + ipAddress.getPort();
402                 } else {
403                     messageLogger.logWarn("Skipping a group member. Address not an IpAddress: " + memberAddress);
404                     continue;
405                 }
406 
407                 memberIds.add(RandomId.pseudoBase64Encode(RandomId.nodeIdToByteArray(nodeId)));
408             }
409 
410             /* Get data. */
411 
412             List branches = MoriaTicketType.TICKET_TYPES;
413 
414             int ticketCount = 0;
415 
416             for (Iterator i = branches.iterator(); i.hasNext();) {
417 
418                 Node branch;
419                 Map tickets;
420 
421                 try {
422                     branch = cache.get(new Fqn(i.next()));
423                 } catch (LockingException le) {
424                     messageLogger.logCritical("Locking cache failed.", le);
425                     continue;
426                 } catch (TimeoutException te) {
427                     messageLogger.logCritical("Got timeout from cache.", te);
428                     continue;
429                 } catch (CacheException ce) {
430                     messageLogger.logCritical("Got exception from cache.", ce);
431                     continue;
432                 }
433 
434                 if (branch != null) {
435                     tickets = branch.getChildren();
436                 } else {
437                     continue;
438                 }
439 
440                 /* In case a null value is returned instead of a empty Map. */
441                 if (tickets == null)
442                     continue;
443 
444                 for (Iterator j = tickets.keySet().iterator(); j.hasNext();) {
445                     Object ticketId = j.next();
446                     String nodeId = ticketId.toString().substring(0, 7);
447 
448                     for (Iterator k = memberIds.iterator(); k.hasNext();) {
449                         String memberId = (String) k.next();
450 
451                         if (nodeId.equals(memberId)) {
452                             Node ticket = (Node) tickets.get(ticketId);
453 
454                             if (ticket != null) {
455                                 try {
456                                     cache.put(ticket.getFqn(), ticket.getData());
457                                     ticketCount++;
458                                 } catch (RuntimeException re) {
459                                     /* We do this to weed out RuntimeExceptions from the catch below. */
460                                     throw re;
461                                 } catch (Exception e) {
462                                     messageLogger.logCritical("Insertion into store failed. [" + ticket.getName() + "]", e);
463                                     continue;
464                                 }
465 
466                                 /* Throttle redistribution somewhat. */
467                                 try {
468                                     Thread.sleep(30);
469                                 } catch (InterruptedException ie) {
470                                     messageLogger.logWarn("Thread sleep interrupted", ie);
471                                 }
472                             }
473                         }
474                     }
475                 }
476             }
477             messageLogger.logDebug("Number of redistributed tickets: " + ticketCount);
478         } else {
479             messageLogger.logDebug("Got a MergeView, but this wasn't a coordinator before.");
480             return;
481         }
482     }
483 
484     /***
485      * Parses the config retrieved from TreeCache.getEvictionPolicyConfig(). Populates
486      * the regionValues array.
487      *
488      * @param config configuration for this eviction policy
489      */
490     synchronized void parseConfig(final Element config) {
491 
492         if (config == null)
493             throw new IllegalArgumentException("config cannot be null");
494 
495         /* Get wakeup interval from config. */
496         wakeUpIntervalSeconds = Integer.parseInt(getAttribute(config, WAKEUP_INTERVAL_NAME));
497 
498         if (wakeUpIntervalSeconds < 1) {
499             wakeUpIntervalSeconds = 5;
500             messageLogger.logWarn("Illegal value for config parameter wakeUpIntervalSeconds. Using default: "
501                                   + wakeUpIntervalSeconds);
502         }
503 
504         /* Get max nodes from config. */
505         maxNodes = Integer.parseInt(getAttribute(config, MAX_NODES_NAME));
506 
507         if (maxNodes < 1) {
508             maxNodes = MAX_NODES_DEFAULT;
509             messageLogger.logWarn("Illegal value for config parameter maxNodes. Using default: " + maxNodes);
510         }
511 
512         /* Get and create regions. */
513         NodeList regions = config.getElementsByTagName(REGION_NAME);
514         regionValues = new RegionValue[regions.getLength()];
515 
516         for (int i = 0; i < regionValues.length; i++) {
517             RegionValue regionValue = new RegionValue();
518             org.w3c.dom.Node node = regions.item(i);
519 
520             if (node.getNodeType() != 1)
521                 continue;
522 
523             Element region = (Element) node;
524             regionValue.regionName = region.getAttribute(NAME_ATTRIBUTE_NAME);
525             regionValue.timeToLive = Long.parseLong(getAttribute(region, TTL_ATTRIBUTE_NAME)) * 1000L;
526 
527             if (regionValue.regionName == null || regionValue.regionName.equals(""))
528                 throw new EvictionConfigurationException("Illegal value for region name: " + regionValue.regionName);
529 
530             if (regionValue.timeToLive < 1000L)
531                 throw new EvictionConfigurationException("Illegal value for time to live: " + regionValue.timeToLive);
532 
533             regionValues[i] = regionValue;
534             messageLogger.logDebug("Added region: " + regionValue);
535         }
536     }
537 
538     /***
539      * Retrieves the value of the first occurance of the given
540      * attribute in the given element.
541      *
542      * @param element the element containing an named attribute.
543      * @param attributeName the name of the requested attribute.
544      * @return the value of the requested attribute, null if no attribute exists in element.
545      */
546     String getAttribute(final Element element, final String attributeName) {
547         NodeList nodes = element.getElementsByTagName(ATTRIBUTE_NAME);
548 
549         for (int i = 0; i < nodes.getLength(); i++) {
550             org.w3c.dom.Node node = nodes.item(i);
551 
552             if (node.getNodeType() != org.w3c.dom.Node.ELEMENT_NODE)
553                 continue;
554 
555             Element subElement = (Element) node;
556             String name = subElement.getAttribute(NAME_ATTRIBUTE_NAME);
557 
558             if (name.equals(attributeName))
559                 return getElementContent(subElement, true);
560         }
561 
562         return null;
563     }
564 
565     /***
566      * Retrieves the textual content of an xml element.
567      *
568      * @param element The element containing the data to be retrieved.
569      * @param trim Whether or not to trim whitespace before returing.
570      * @return A concatenated string of the text of the child nodes of the element.
571      */
572     String getElementContent(final Element element, boolean trim) {
573         NodeList nodes = element.getChildNodes();
574         String attributeText = "";
575 
576         for (int i = 0; i < nodes.getLength(); i++) {
577             org.w3c.dom.Node node = nodes.item(i);
578             if (node instanceof Text)
579                 attributeText = attributeText + ((Text) node).getData();
580         }
581 
582         if (trim)
583             attributeText = attributeText.trim();
584 
585         return attributeText;
586     }
587 
588     /***
589      * Gets the region values.
590      *
591      * @return Array containing the region values.
592      */
593     RegionValue[] getRegionValues() {
594         return regionValues;
595     }
596 
597     /***
598      * Gets a region value.
599      *
600      * @param fqn Fully quailified name.
601      * @return A region value.
602      */
603     final RegionValue getRegionValue(final String fqn) {
604 
605         for (int i = 0; i < regionValues.length; i++) {
606             if (fqn.startsWith(ROOT.toString() + regionValues[i].regionName))
607                 return regionValues[i];
608         }
609 
610         return null;
611     }
612 
613     /***
614      * Simple data container for region values.
615      */
616     static final class RegionValue {
617 
618         /*** Region name. */
619         private String regionName;
620 
621         /*** Time to live. */
622         private long timeToLive;
623 
624         /***
625          * Returns a string representation.
626          * @return The string representation.
627          */
628         public String toString() {
629             return "[" + regionName + ", " + timeToLive + "]";
630         }
631 
632         /***
633          * Gets region name.
634          * @return The region name.
635          */
636         String getRegionName() {
637             return regionName;
638         }
639 
640         /***
641          * Gets time to live.
642          * @return Time to live.
643          */
644         long getTimeToLive() {
645             return timeToLive;
646         }
647     }
648 }