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}