001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019package org.apache.oozie.dependency.hcat; 020 021import java.net.URI; 022import java.net.URISyntaxException; 023import java.net.URL; 024import java.util.ArrayList; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.HashSet; 028import java.util.Iterator; 029import java.util.Map; 030import java.util.Map.Entry; 031import java.util.Set; 032import java.util.concurrent.ConcurrentHashMap; 033import java.util.concurrent.ConcurrentMap; 034 035import net.sf.ehcache.Cache; 036import net.sf.ehcache.CacheException; 037import net.sf.ehcache.CacheManager; 038import net.sf.ehcache.Ehcache; 039import net.sf.ehcache.Element; 040import net.sf.ehcache.config.CacheConfiguration; 041import net.sf.ehcache.event.CacheEventListener; 042 043import org.apache.hadoop.conf.Configuration; 044import org.apache.oozie.service.ConfigurationService; 045import org.apache.oozie.service.HCatAccessorService; 046import org.apache.oozie.service.PartitionDependencyManagerService; 047import org.apache.oozie.service.Services; 048import org.apache.oozie.util.HCatURI; 049import org.apache.oozie.util.XLog; 050 051public class EhcacheHCatDependencyCache implements HCatDependencyCache, CacheEventListener { 052 053 private static XLog LOG = XLog.getLog(EhcacheHCatDependencyCache.class); 054 private static String TABLE_DELIMITER = "#"; 055 private static String PARTITION_DELIMITER = ";"; 056 057 public static String CONF_CACHE_NAME = PartitionDependencyManagerService.CONF_PREFIX + "cache.ehcache.name"; 058 059 private CacheManager cacheManager; 060 061 private boolean useCanonicalHostName = false; 062 063 /** 064 * Map of server to EhCache which has key as db#table#pk1;pk2#val;val2 and value as WaitingActions (list of 065 * WaitingAction) which is Serializable (for overflowToDisk) 066 */ 067 private ConcurrentMap<String, Cache> missingDepsByServer; 068 069 private CacheConfiguration cacheConfig; 070 /** 071 * Map of server#db#table - sorted part key pattern - count of different partition values (count 072 * of elements in the cache) still missing for a partition key pattern. This count is used to 073 * quickly determine if there are any more missing dependencies for a table. When the count 074 * becomes 0, we unregister from notifications as there are no more missing dependencies for 075 * that table. 076 */ 077 private ConcurrentMap<String, ConcurrentMap<String, SettableInteger>> partKeyPatterns; 078 /** 079 * Map of actionIDs and collection of available URIs 080 */ 081 private ConcurrentMap<String, Collection<String>> availableDeps; 082 083 @Override 084 public void init(Configuration conf) { 085 String cacheName = conf.get(CONF_CACHE_NAME); 086 URL cacheConfigURL; 087 if (cacheName == null) { 088 cacheConfigURL = this.getClass().getClassLoader().getResource("ehcache-default.xml"); 089 cacheName = "dependency-default"; 090 } 091 else { 092 cacheConfigURL = this.getClass().getClassLoader().getResource("ehcache.xml"); 093 } 094 if (cacheConfigURL == null) { 095 throw new IllegalStateException("ehcache.xml is not found in classpath"); 096 } 097 cacheManager = CacheManager.newInstance(cacheConfigURL); 098 final Cache specifiedCache = cacheManager.getCache(cacheName); 099 if (specifiedCache == null) { 100 throw new IllegalStateException("Cache " + cacheName + " configured in " + CONF_CACHE_NAME 101 + " is not found"); 102 } 103 cacheConfig = specifiedCache.getCacheConfiguration(); 104 missingDepsByServer = new ConcurrentHashMap<String, Cache>(); 105 partKeyPatterns = new ConcurrentHashMap<String, ConcurrentMap<String, SettableInteger>>(); 106 availableDeps = new ConcurrentHashMap<String, Collection<String>>(); 107 useCanonicalHostName = ConfigurationService.getBoolean(SimpleHCatDependencyCache.USE_CANONICAL_HOSTNAME); 108 109 } 110 111 @Override 112 public void addMissingDependency(HCatURI hcatURI, String actionID) { 113 String serverName = canonicalizeHostname(hcatURI.getServer()); 114 // Create cache for the server if we don't have one 115 Cache missingCache = missingDepsByServer.get(serverName); 116 if (missingCache == null) { 117 CacheConfiguration clonedConfig = cacheConfig.clone(); 118 clonedConfig.setName(serverName); 119 missingCache = new Cache(clonedConfig); 120 Cache exists = missingDepsByServer.putIfAbsent(serverName, missingCache); 121 if (exists == null) { 122 cacheManager.addCache(missingCache); 123 missingCache.getCacheEventNotificationService().registerListener(this); 124 } 125 else { 126 missingCache.dispose(); //discard 127 } 128 } 129 130 // Add hcat uri into the missingCache 131 SortedPKV sortedPKV = new SortedPKV(hcatURI.getPartitionMap()); 132 String partKeys = sortedPKV.getPartKeys(); 133 String missingKey = hcatURI.getDb() + TABLE_DELIMITER + hcatURI.getTable() + TABLE_DELIMITER 134 + partKeys + TABLE_DELIMITER + sortedPKV.getPartVals(); 135 boolean newlyAdded = true; 136 synchronized (missingCache) { 137 Element element = missingCache.get(missingKey); 138 if (element == null) { 139 WaitingActions waitingActions = new WaitingActions(); 140 element = new Element(missingKey, waitingActions); 141 Element exists = missingCache.putIfAbsent(element); 142 if (exists != null) { 143 newlyAdded = false; 144 waitingActions = (WaitingActions) exists.getObjectValue(); 145 } 146 waitingActions.add(new WaitingAction(actionID, hcatURI.toURIString())); 147 } 148 else { 149 newlyAdded = false; 150 WaitingActions waitingActions = (WaitingActions) element.getObjectValue(); 151 waitingActions.add(new WaitingAction(actionID, hcatURI.toURIString())); 152 } 153 } 154 155 // Increment count for the partition key pattern 156 if (newlyAdded) { 157 String tableKey = canonicalizeHostname(hcatURI.getServer()) + TABLE_DELIMITER + hcatURI.getDb() + TABLE_DELIMITER 158 + hcatURI.getTable(); 159 synchronized (partKeyPatterns) { 160 ConcurrentMap<String, SettableInteger> patternCounts = partKeyPatterns.get(tableKey); 161 if (patternCounts == null) { 162 patternCounts = new ConcurrentHashMap<String, SettableInteger>(); 163 partKeyPatterns.put(tableKey, patternCounts); 164 } 165 SettableInteger count = patternCounts.get(partKeys); 166 if (count == null) { 167 patternCounts.put(partKeys, new SettableInteger(1)); 168 } 169 else { 170 count.increment(); 171 } 172 } 173 } 174 } 175 176 @Override 177 public boolean removeMissingDependency(HCatURI hcatURI, String actionID) { 178 179 Cache missingCache = missingDepsByServer.get(canonicalizeHostname(hcatURI.getServer())); 180 if (missingCache == null) { 181 LOG.warn("Remove missing dependency - Missing cache entry for server - uri={0}, actionID={1}", 182 hcatURI.toURIString(), actionID); 183 return false; 184 } 185 SortedPKV sortedPKV = new SortedPKV(hcatURI.getPartitionMap()); 186 String partKeys = sortedPKV.getPartKeys(); 187 String missingKey = hcatURI.getDb() + TABLE_DELIMITER + hcatURI.getTable() + TABLE_DELIMITER + 188 partKeys + TABLE_DELIMITER + sortedPKV.getPartVals(); 189 boolean decrement = false; 190 boolean removed = false; 191 synchronized (missingCache) { 192 Element element = missingCache.get(missingKey); 193 if (element == null) { 194 LOG.warn("Remove missing dependency - Missing cache entry - uri={0}, actionID={1}", 195 hcatURI.toURIString(), actionID); 196 return false; 197 } 198 Collection<WaitingAction> waitingActions = ((WaitingActions) element.getObjectValue()).getWaitingActions(); 199 removed = waitingActions.remove(new WaitingAction(actionID, hcatURI.toURIString())); 200 if (!removed) { 201 LOG.warn("Remove missing dependency - Missing action ID - uri={0}, actionID={1}", 202 hcatURI.toURIString(), actionID); 203 } 204 if (waitingActions.isEmpty()) { 205 missingCache.remove(missingKey); 206 decrement = true; 207 } 208 } 209 // Decrement partition key pattern count if the cache entry is removed 210 if (decrement) { 211 String tableKey = canonicalizeHostname(hcatURI.getServer()) + TABLE_DELIMITER + hcatURI.getDb() + TABLE_DELIMITER 212 + hcatURI.getTable(); 213 decrementPartKeyPatternCount(tableKey, partKeys, hcatURI.toURIString()); 214 } 215 return removed; 216 } 217 218 @Override 219 public Collection<String> getWaitingActions(HCatURI hcatURI) { 220 Collection<String> actionIDs = null; 221 Cache missingCache = missingDepsByServer.get(canonicalizeHostname(hcatURI.getServer())); 222 if (missingCache != null) { 223 SortedPKV sortedPKV = new SortedPKV(hcatURI.getPartitionMap()); 224 String missingKey = hcatURI.getDb() + TABLE_DELIMITER + hcatURI.getTable() + TABLE_DELIMITER 225 + sortedPKV.getPartKeys() + TABLE_DELIMITER + sortedPKV.getPartVals(); 226 Element element = missingCache.get(missingKey); 227 if (element != null) { 228 WaitingActions waitingActions = (WaitingActions) element.getObjectValue(); 229 actionIDs = new ArrayList<String>(); 230 URI uri = hcatURI.getURI(); 231 String uriString = null; 232 try { 233 uriString = new URI(uri.getScheme(), canonicalizeHostname(uri.getAuthority()), uri.getPath(), 234 uri.getQuery(), uri.getFragment()).toString(); 235 } 236 catch (URISyntaxException e) { 237 uriString = hcatURI.toURIString(); 238 } 239 for (WaitingAction action : waitingActions.getWaitingActions()) { 240 if (action.getDependencyURI().equals(uriString)) { 241 actionIDs.add(action.getActionID()); 242 } 243 } 244 } 245 } 246 return actionIDs; 247 } 248 249 @Override 250 public Collection<String> markDependencyAvailable(String server, String db, String table, 251 Map<String, String> partitions) { 252 String tableKey = canonicalizeHostname(server) + TABLE_DELIMITER + db + TABLE_DELIMITER + table; 253 synchronized (partKeyPatterns) { 254 Map<String, SettableInteger> patternCounts = partKeyPatterns.get(tableKey); 255 if (patternCounts == null) { 256 LOG.warn("Got partition available notification for " + tableKey 257 + ". Unexpected as no matching partition keys. Unregistering topic"); 258 unregisterFromNotifications(server, db, table); 259 return null; 260 } 261 Cache missingCache = missingDepsByServer.get(server); 262 if (missingCache == null) { 263 LOG.warn("Got partition available notification for " + tableKey 264 + ". Unexpected. Missing server entry in cache. Unregistering topic"); 265 partKeyPatterns.remove(tableKey); 266 unregisterFromNotifications(server, db, table); 267 return null; 268 } 269 Collection<String> actionsWithAvailDep = new HashSet<String>(); 270 StringBuilder partValSB = new StringBuilder(); 271 // If partition patterns are date, date;country and date;country;state, 272 // construct the partition values for each pattern and for the matching value in the 273 // missingCache, get the waiting actions and mark it as available. 274 for (Entry<String, SettableInteger> entry : patternCounts.entrySet()) { 275 String[] partKeys = entry.getKey().split(PARTITION_DELIMITER); 276 partValSB.setLength(0); 277 for (String key : partKeys) { 278 partValSB.append(partitions.get(key)).append(PARTITION_DELIMITER); 279 } 280 partValSB.setLength(partValSB.length() - 1); 281 String missingKey = db + TABLE_DELIMITER + table + TABLE_DELIMITER + entry.getKey() + TABLE_DELIMITER 282 + partValSB.toString(); 283 boolean removed = false; 284 Element element = null; 285 synchronized (missingCache) { 286 element = missingCache.get(missingKey); 287 if (element != null) { 288 missingCache.remove(missingKey); 289 removed = true; 290 } 291 } 292 if (removed) { 293 decrementPartKeyPatternCount(tableKey, entry.getKey(), server + TABLE_DELIMITER + missingKey); 294 // Add the removed entry to available dependencies 295 Collection<WaitingAction> wActions = ((WaitingActions) element.getObjectValue()) 296 .getWaitingActions(); 297 for (WaitingAction wAction : wActions) { 298 String actionID = wAction.getActionID(); 299 actionsWithAvailDep.add(actionID); 300 Collection<String> depURIs = availableDeps.get(actionID); 301 if (depURIs == null) { 302 depURIs = new ArrayList<String>(); 303 Collection<String> existing = availableDeps.putIfAbsent(actionID, depURIs); 304 if (existing != null) { 305 depURIs = existing; 306 } 307 } 308 synchronized (depURIs) { 309 depURIs.add(wAction.getDependencyURI()); 310 availableDeps.put(actionID, depURIs); 311 } 312 } 313 } 314 } 315 return actionsWithAvailDep; 316 } 317 } 318 319 @Override 320 public Collection<String> getAvailableDependencyURIs(String actionID) { 321 Collection<String> available = availableDeps.get(actionID); 322 if (available != null) { 323 // Return a copy 324 available = new ArrayList<String>(available); 325 } 326 return available; 327 } 328 329 @Override 330 public boolean removeAvailableDependencyURIs(String actionID, Collection<String> dependencyURIs) { 331 if (!availableDeps.containsKey(actionID)) { 332 return false; 333 } 334 else { 335 Collection<String> availList = availableDeps.get(actionID); 336 if (!availList.removeAll(dependencyURIs)) { 337 return false; 338 } 339 synchronized (availList) { 340 if (availList.isEmpty()) { 341 availableDeps.remove(actionID); 342 } 343 } 344 } 345 return true; 346 } 347 348 @Override 349 public void destroy() { 350 availableDeps.clear(); 351 cacheManager.shutdown(); 352 } 353 354 @Override 355 public Object clone() throws CloneNotSupportedException { 356 throw new CloneNotSupportedException(); 357 } 358 359 @Override 360 public void dispose() { 361 } 362 363 @Override 364 public void notifyElementExpired(Ehcache cache, Element element) { 365 // Invoked when timeToIdleSeconds or timeToLiveSeconds is met 366 String missingDepKey = (String) element.getObjectKey(); 367 LOG.info("Cache entry [{0}] of cache [{1}] expired", missingDepKey, cache.getName()); 368 onExpiryOrEviction(cache, element, missingDepKey); 369 } 370 371 @Override 372 public void notifyElementPut(Ehcache arg0, Element arg1) throws CacheException { 373 374 } 375 376 @Override 377 public void notifyElementRemoved(Ehcache arg0, Element arg1) throws CacheException { 378 } 379 380 @Override 381 public void notifyElementUpdated(Ehcache arg0, Element arg1) throws CacheException { 382 } 383 384 @Override 385 public void notifyRemoveAll(Ehcache arg0) { 386 } 387 388 @Override 389 public void notifyElementEvicted(Ehcache cache, Element element) { 390 // Invoked when maxElementsInMemory is met 391 String missingDepKey = (String) element.getObjectKey(); 392 LOG.info("Cache entry [{0}] of cache [{1}] evicted", missingDepKey, cache.getName()); 393 onExpiryOrEviction(cache, element, missingDepKey); 394 } 395 396 private void onExpiryOrEviction(Ehcache cache, Element element, String missingDepKey) { 397 int partValIndex = missingDepKey.lastIndexOf(TABLE_DELIMITER); 398 int partKeyIndex = missingDepKey.lastIndexOf(TABLE_DELIMITER, partValIndex - 1); 399 // server#db#table. Name of the cache is that of the server. 400 String tableKey = cache.getName() + TABLE_DELIMITER + missingDepKey.substring(0, partKeyIndex); 401 String partKeys = missingDepKey.substring(partKeyIndex + 1, partValIndex); 402 decrementPartKeyPatternCount(tableKey, partKeys, missingDepKey); 403 } 404 405 /** 406 * Decrement partition key pattern count, once a hcat URI is removed from the cache 407 * 408 * @param tableKey key identifying the table - server#db#table 409 * @param partKeys partition key pattern 410 * @param hcatURI URI with the partition key pattern 411 */ 412 private void decrementPartKeyPatternCount(String tableKey, String partKeys, String hcatURI) { 413 synchronized (partKeyPatterns) { 414 Map<String, SettableInteger> patternCounts = partKeyPatterns.get(tableKey); 415 if (patternCounts == null) { 416 LOG.warn("Removed dependency - Missing cache entry - uri={0}. " 417 + "But no corresponding pattern key table entry", hcatURI); 418 } 419 else { 420 SettableInteger count = patternCounts.get(partKeys); 421 if (count == null) { 422 LOG.warn("Removed dependency - Missing cache entry - uri={0}. " 423 + "But no corresponding pattern key entry", hcatURI); 424 } 425 else { 426 count.decrement(); 427 if (count.getValue() == 0) { 428 patternCounts.remove(partKeys); 429 } 430 if (patternCounts.isEmpty()) { 431 partKeyPatterns.remove(tableKey); 432 String[] tableDetails = tableKey.split(TABLE_DELIMITER); 433 unregisterFromNotifications(tableDetails[0], tableDetails[1], tableDetails[2]); 434 } 435 } 436 } 437 } 438 } 439 440 private void unregisterFromNotifications(String server, String db, String table) { 441 // Close JMS session. Stop listening on topic 442 HCatAccessorService hcatService = Services.get().get(HCatAccessorService.class); 443 hcatService.unregisterFromNotification(server, db, table); 444 } 445 446 private static class SortedPKV { 447 private StringBuilder partKeys; 448 private StringBuilder partVals; 449 450 public SortedPKV(Map<String, String> partitions) { 451 this.partKeys = new StringBuilder(); 452 this.partVals = new StringBuilder(); 453 ArrayList<String> keys = new ArrayList<String>(partitions.keySet()); 454 Collections.sort(keys); 455 for (String key : keys) { 456 this.partKeys.append(key).append(PARTITION_DELIMITER); 457 this.partVals.append(partitions.get(key)).append(PARTITION_DELIMITER); 458 } 459 this.partKeys.setLength(partKeys.length() - 1); 460 this.partVals.setLength(partVals.length() - 1); 461 } 462 463 public String getPartKeys() { 464 return partKeys.toString(); 465 } 466 467 public String getPartVals() { 468 return partVals.toString(); 469 } 470 471 } 472 473 private static class SettableInteger { 474 private int value; 475 476 public SettableInteger(int value) { 477 this.value = value; 478 } 479 480 public int getValue() { 481 return value; 482 } 483 484 public void increment() { 485 value++; 486 } 487 488 public void decrement() { 489 value--; 490 } 491 } 492 493 @Override 494 public void removeNonWaitingCoordActions(Set<String> staleActions) { 495 for (Entry<String, Cache> entry : missingDepsByServer.entrySet()) { 496 Cache missingCache = entry.getValue(); 497 498 if (missingCache == null) { 499 continue; 500 } 501 502 synchronized (missingCache) { 503 for (Object key : missingCache.getKeys()) { 504 Element element = missingCache.get(key); 505 if (element == null) { 506 continue; 507 } 508 Collection<WaitingAction> waitingActions = ((WaitingActions) element.getObjectValue()) 509 .getWaitingActions(); 510 Iterator<WaitingAction> wactionItr = waitingActions.iterator(); 511 HCatURI hcatURI = null; 512 while(wactionItr.hasNext()) { 513 WaitingAction waction = wactionItr.next(); 514 if(staleActions.contains(waction.getActionID())) { 515 try { 516 hcatURI = new HCatURI(waction.getDependencyURI()); 517 wactionItr.remove(); 518 } 519 catch (URISyntaxException e) { 520 continue; 521 } 522 } 523 } 524 if (waitingActions.isEmpty() && hcatURI != null) { 525 missingCache.remove(key); 526 // Decrement partition key pattern count if the cache entry is removed 527 SortedPKV sortedPKV = new SortedPKV(hcatURI.getPartitionMap()); 528 String partKeys = sortedPKV.getPartKeys(); 529 String tableKey = canonicalizeHostname(hcatURI.getServer()) + TABLE_DELIMITER + hcatURI.getDb() 530 + TABLE_DELIMITER + hcatURI.getTable(); 531 String hcatURIStr = hcatURI.toURIString(); 532 decrementPartKeyPatternCount(tableKey, partKeys, hcatURIStr); 533 } 534 } 535 } 536 } 537 } 538 539 @Override 540 public void removeCoordActionWithDependenciesAvailable(String coordAction) { 541 // to be implemented when reverse-lookup data structure for purging is added 542 } 543 544 public String canonicalizeHostname(String name) { 545 return SimpleHCatDependencyCache.canonicalizeHostname(name, useCanonicalHostName); 546 } 547 548}