/**
* Copyright 2012 Comcast Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.comcast.cns.persistence;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import org.apache.log4j.Logger;
import com.comcast.cmb.common.persistence.PersistenceFactory;
import com.comcast.cmb.common.util.CMBErrorCodes;
import com.comcast.cmb.common.util.CMBException;
import com.comcast.cmb.common.util.ExpiringCache;
import com.comcast.cmb.common.util.ExpiringCache.CacheFullException;
import com.comcast.cns.model.CNSEndpointPublishJob;
import com.comcast.cns.model.CNSMessage;
import com.comcast.cns.model.CNSSubscription;
import com.comcast.cns.model.CNSSubscription.CnsSubscriptionProtocol;
import com.comcast.cns.util.Util;
/**
* This class supports caching of the subinfo in memory
* The static cache uses topicArn as the key for the cache. The value is a concurrent-hashmap of subArn->subInfo
* {{topicArn, exp}->{subArn->{subInfo}}}
*
* @author aseem
*
* Class is thread-safe
*/
public class CNSCachedEndpointPublishJob extends CNSEndpointPublishJob {
private static Logger logger = Logger.getLogger(CNSCachedEndpointPublishJob.class);
private static final ExpiringCache<String, LinkedHashMap<String, CNSCachedEndpointSubscriptionInfo>> cache = new ExpiringCache<String, LinkedHashMap<String,CNSCachedEndpointSubscriptionInfo>>(1000);
/**
*
* @param topicArn
* @return THh list of sub-infos
* @throws Exception
*/
public static List<CNSCachedEndpointSubscriptionInfo> getSubInfos(String topicArn) throws Exception {
LinkedHashMap<String, CNSCachedEndpointSubscriptionInfo> arnToSubInfo;
try {
arnToSubInfo = cache.getAndSetIfNotPresent(topicArn, new CachePopulator(topicArn), 60000);
} catch (CacheFullException e) {
arnToSubInfo = new CachePopulator(topicArn).call();
} catch(IllegalStateException e) {
if ((e.getCause() instanceof ExecutionException) &&
(e.getCause().getCause() instanceof TopicNotFoundException)) {
throw new TopicNotFoundException("Topic Not Found:" + topicArn);
} else {
throw e;
}
}
return new LinkedList<CNSCachedEndpointSubscriptionInfo>(arnToSubInfo.values());
}
public static class CNSCachedEndpointSubscriptionInfo extends CNSEndpointSubscriptionInfo {
public CNSCachedEndpointSubscriptionInfo(CnsSubscriptionProtocol protocol, String endpoint, String subArn, boolean rawDelivery) {
super(protocol, endpoint, subArn, rawDelivery);
}
public CNSCachedEndpointSubscriptionInfo(CNSEndpointSubscriptionInfo info) {
super(info.protocol, info.endpoint, info.subArn, info.rawDelivery);
}
@Override
public String serialize() {
return subArn;
}
public static CNSCachedEndpointSubscriptionInfo parseInstance(String str) {
return new CNSCachedEndpointSubscriptionInfo(CNSEndpointSubscriptionInfo.parseInstance(str));
}
/**
*
* @param arr array of serialized CachedSubInfo objects (or its identifiers)
* @param idx start index
* @param count num to read
* @return list of fully populated SubInfos in random order
* @throws Exception
*/
public static List<CNSCachedEndpointSubscriptionInfo> parseInstances(String topicArn, String []arr, int idx, int count, boolean useCache) throws Exception {
if (count == 0) {
return Collections.emptyList();
}
List<CNSCachedEndpointSubscriptionInfo> infos = new LinkedList<CNSCachedEndpointPublishJob.CNSCachedEndpointSubscriptionInfo>();
if (useCache) {
HashMap<String, CNSCachedEndpointSubscriptionInfo> arnToSubInfo;
try {
arnToSubInfo = cache.getAndSetIfNotPresent(topicArn, new CachePopulator(topicArn), 60000);
} catch (CacheFullException e) {
arnToSubInfo = new CachePopulator(topicArn).call();
} catch(IllegalStateException e) {
if ((e.getCause() instanceof ExecutionException) &&
(e.getCause().getCause() instanceof TopicNotFoundException)) {
logger.warn("event=topic_not_found topic_arn=" + topicArn);
throw new TopicNotFoundException("Topic Not Found:" + topicArn);
} else {
throw e;
}
}
for (int i = idx; i < idx + count ; i++) {
String subArn = arr[i];
CNSCachedEndpointSubscriptionInfo subInfo = arnToSubInfo.get(subArn);
if (subInfo == null) {
logger.warn("event=subscription_info_not_found arn=" + subArn + " topic_arn=" + topicArn);
} else {
infos.add(subInfo);
}
}
} else {
//get from Cassandra
ICNSSubscriptionPersistence subscriptionPersistence = PersistenceFactory.getSubscriptionPersistence();
for (int i = idx; i < idx + count ; i++) {
String subArn = arr[i];
CNSSubscription sub = subscriptionPersistence.getSubscription(subArn);
//TODO: store raw delivery flag as part of the subscription for better performance
if (sub == null) {
logger.warn("event=subscription_info_not_found arn=" + subArn + " topic_arn=" + topicArn);
} else {
infos.add(new CNSCachedEndpointSubscriptionInfo(sub.getProtocol(), sub.getEndpoint(), sub.getArn(), sub.getRawMessageDelivery()));
}
}
}
return infos;
}
}
public CNSCachedEndpointPublishJob(CNSMessage message, List<? extends CNSEndpointSubscriptionInfo> subInfos) {
super(message, subInfos);
}
/**
* Class responsible for returning the contents of the cache if cache miss occurs
*/
private static class CachePopulator implements Callable<LinkedHashMap<String,CNSCachedEndpointSubscriptionInfo>> {
final String topicArn;
public CachePopulator(String topicArn) {
this.topicArn = topicArn;
}
@Override
public LinkedHashMap<String, CNSCachedEndpointSubscriptionInfo> call() throws Exception {
long ts1 = System.currentTimeMillis();
ICNSSubscriptionPersistence subscriptionPersistence = PersistenceFactory.getSubscriptionPersistence();
List<CNSSubscription> subs = null;
String nextToken = null;
LinkedHashMap<String, CNSCachedEndpointSubscriptionInfo> val = new LinkedHashMap<String, CNSCachedEndpointPublishJob.CNSCachedEndpointSubscriptionInfo>();
while (true) {
//get subscription by page
subs = subscriptionPersistence.listSubscriptionsByTopic(nextToken, topicArn, null, 1000);
if (subs == null || subs.size() == 0) {
break;
}
for (CNSSubscription sub : subs) {
if (!sub.getArn().equals("PendingConfirmation")) {
val.put(sub.getArn(), new CNSCachedEndpointSubscriptionInfo(sub.getProtocol(), sub.getEndpoint(), sub.getArn(), sub.getRawMessageDelivery()));
nextToken = sub.getArn();
}
}
}
long ts2 = System.currentTimeMillis();
logger.debug("event=populated_subscription_cache topic_arn=" + topicArn + " res_ts=" + (ts2 - ts1));
return val;
}
}
/**
* Construct a CNSEndpointPublishJob given its string representation
* @param str
* @return
* @throws CMBException
*/
public static CNSEndpointPublishJob parseInstance(String str) throws CMBException {
String arr[] = str.split("\n");
if (arr.length < 2) {
throw new IllegalArgumentException("Expected at least two tokens in CNSEndpointPublishJob serial representation. Expect4ed <num-subinfos>\n[<sub-info>\n<sub-info>...\n]<CNSPublishJob>. Got:" + str);
}
int numSubInfos = Integer.parseInt(arr[0]);
int idx = 1;
//Note: We assume that topic-ARN is part of the sub-arn
String topicArn = null;
boolean useCache = true;
if (numSubInfos > 0) {
topicArn = Util.getCnsTopicArn(arr[idx]);
}
List<CNSCachedEndpointSubscriptionInfo> subInfos;
try {
subInfos = CNSCachedEndpointSubscriptionInfo.parseInstances(topicArn, arr, idx, numSubInfos, useCache);
} catch (TopicNotFoundException e) {
logger.error("event=parse_publish_job error_code=topic_not_found topic_arn=" + topicArn, e);
throw e;
} catch (Exception e) {
logger.error("event=parse_publish_job", e);
throw new CMBException(CMBErrorCodes.InternalError, e.getMessage());
}
idx += numSubInfos;
StringBuffer sb = new StringBuffer();
for (int j = idx; j < arr.length; j++) {
if (j != idx) {
sb.append("\n");
}
sb.append(arr[j]);
}
CNSMessage message = null;
try {
message = CNSMessage.parseInstance(sb.toString());
} catch(Exception e) {
logger.error("event=parse_publish_job cnsmessage_serialized=" + sb.toString(), e);
throw new CMBException(CMBErrorCodes.InternalError, e.getMessage());
}
return new CNSCachedEndpointPublishJob(message, subInfos);
}
}