package gobblin.cluster;
import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import org.apache.helix.Criteria;
import org.apache.helix.HelixDataAccessor;
import org.apache.helix.HelixManager;
import org.apache.helix.HelixProperty;
import org.apache.helix.InstanceType;
import org.apache.helix.messaging.CriteriaEvaluator;
import org.apache.helix.messaging.DefaultMessagingService;
import org.apache.helix.messaging.ZNRecordRow;
import org.apache.helix.PropertyKey;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.helix.model.LiveInstance;
import org.apache.helix.model.Message;
/**
* #HELIX-0.6.7-WORKAROUND
* The GobblinHelixMessagingService is a temporary workaround for missing messaging support for INSTANCES in helix 0.6.7
*/
public class GobblinHelixMessagingService extends DefaultMessagingService {
private GobblinHelixCriteriaEvaluator _gobblinHelixCriteriaEvaluator;
private HelixManager _manager;
public GobblinHelixMessagingService(HelixManager manager) {
super(manager);
_manager = manager;
_gobblinHelixCriteriaEvaluator = new GobblinHelixCriteriaEvaluator();
}
private List<Message> generateMessagesForController(Message message) {
List<Message> messages = new ArrayList<Message>();
String id = UUID.randomUUID().toString();
Message newMessage = new Message(message.getRecord(), id);
newMessage.setMsgId(id);
newMessage.setSrcName(_manager.getInstanceName());
newMessage.setTgtName("Controller");
messages.add(newMessage);
return messages;
}
@Override
public Map<InstanceType, List<Message>> generateMessage(final Criteria recipientCriteria,
final Message message) {
Map<InstanceType, List<Message>> messagesToSendMap = new HashMap<InstanceType, List<Message>>();
InstanceType instanceType = recipientCriteria.getRecipientInstanceType();
if (instanceType == InstanceType.CONTROLLER) {
List<Message> messages = generateMessagesForController(message);
messagesToSendMap.put(InstanceType.CONTROLLER, messages);
// _dataAccessor.setControllerProperty(PropertyType.MESSAGES,
// newMessage.getRecord(), CreateMode.PERSISTENT);
} else if (instanceType == InstanceType.PARTICIPANT) {
List<Message> messages = new ArrayList<Message>();
List<Map<String, String>> matchedList =
_gobblinHelixCriteriaEvaluator.evaluateCriteria(recipientCriteria, _manager);
if (!matchedList.isEmpty()) {
Map<String, String> sessionIdMap = new HashMap<String, String>();
if (recipientCriteria.isSessionSpecific()) {
HelixDataAccessor accessor = _manager.getHelixDataAccessor();
PropertyKey.Builder keyBuilder = accessor.keyBuilder();
List<LiveInstance> liveInstances = accessor.getChildValues(keyBuilder.liveInstances());
for (LiveInstance liveInstance : liveInstances) {
sessionIdMap.put(liveInstance.getInstanceName(), liveInstance.getSessionId());
}
}
for (Map<String, String> map : matchedList) {
String id = UUID.randomUUID().toString();
Message newMessage = new Message(message.getRecord(), id);
String srcInstanceName = _manager.getInstanceName();
String tgtInstanceName = map.get("instanceName");
// Don't send message to self
if (recipientCriteria.isSelfExcluded()
&& srcInstanceName.equalsIgnoreCase(tgtInstanceName)) {
continue;
}
newMessage.setSrcName(srcInstanceName);
newMessage.setTgtName(tgtInstanceName);
newMessage.setResourceName(map.get("resourceName"));
newMessage.setPartitionName(map.get("partitionName"));
if (recipientCriteria.isSessionSpecific()) {
newMessage.setTgtSessionId(sessionIdMap.get(tgtInstanceName));
}
messages.add(newMessage);
}
messagesToSendMap.put(InstanceType.PARTICIPANT, messages);
}
}
return messagesToSendMap;
}
public static class GobblinHelixCriteriaEvaluator extends CriteriaEvaluator {
/**
* Examine persisted data to match wildcards in {@link Criteria}
* @param recipientCriteria Criteria specifying the message destinations
* @param manager connection to the persisted data
* @return map of evaluated criteria
*/
public List<Map<String, String>> evaluateCriteria(Criteria recipientCriteria, HelixManager manager) {
// get the data
HelixDataAccessor accessor = manager.getHelixDataAccessor();
PropertyKey.Builder keyBuilder = accessor.keyBuilder();
List<HelixProperty> properties;
properties = accessor.getChildValues(keyBuilder.liveInstances());
// flatten the data
List<ZNRecordRow> allRows = ZNRecordRow.flatten(HelixProperty.convertToList(properties));
// save the matches
Set<String> liveParticipants = accessor.getChildValuesMap(keyBuilder.liveInstances()).keySet();
List<ZNRecordRow> result = Lists.newArrayList();
for (ZNRecordRow row : allRows) {
// The participant instance name is stored in the return value of either getRecordId() or getMapSubKey()
if (rowMatches(recipientCriteria, row) &&
(liveParticipants.contains(row.getRecordId()) || liveParticipants.contains(row.getMapSubKey()))) {
result.add(row);
}
}
Set<Map<String, String>> selected = Sets.newHashSet();
// deduplicate and convert the matches into the required format
for (ZNRecordRow row : result) {
Map<String, String> resultRow = new HashMap<String, String>();
resultRow.put("instanceName", !recipientCriteria.getInstanceName().equals("") ?
(!Strings.isNullOrEmpty(row.getMapSubKey()) ? row.getMapSubKey() : row.getRecordId()) : "");
resultRow.put("resourceName", !recipientCriteria.getResource().equals("") ? row.getRecordId() : "");
resultRow.put("partitionName", !recipientCriteria.getPartition().equals("") ? row.getMapKey() : "");
resultRow.put("partitionState", !recipientCriteria.getPartitionState().equals("") ? row.getMapValue() : "");
selected.add(resultRow);
}
return Lists.newArrayList(selected);
}
/**
* Check if a given row matches the specified criteria
* @param criteria the criteria
* @param row row of currently persisted data
* @return true if it matches, false otherwise
*/
private boolean rowMatches(Criteria criteria, ZNRecordRow row) {
String instanceName = normalizePattern(criteria.getInstanceName());
String resourceName = normalizePattern(criteria.getResource());
String partitionName = normalizePattern(criteria.getPartition());
String partitionState = normalizePattern(criteria.getPartitionState());
return stringMatches(instanceName, row.getMapSubKey()) && stringMatches(resourceName, row.getRecordId())
&& stringMatches(partitionName, row.getMapKey()) && stringMatches(partitionState, row.getMapValue());
}
/**
* Convert an SQL like expression into a Java matches expression
* @param pattern SQL like match pattern (i.e. contains '%'s and '_'s)
* @return Java matches expression (i.e. contains ".*?"s and '.'s)
*/
private String normalizePattern(String pattern) {
if (pattern == null || pattern.equals("") || pattern.equals("*")) {
pattern = "%";
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < pattern.length(); i++) {
char ch = pattern.charAt(i);
if ("[](){}.*+?$^|#\\".indexOf(ch) != -1) {
// escape any reserved characters
builder.append("\\");
}
// append the character
builder.append(ch);
}
pattern = builder.toString().toLowerCase().replace("_", ".").replace("%", ".*?");
return pattern;
}
/**
* Check if a string matches a pattern
* @param pattern pattern allowed by Java regex matching
* @param value the string to check
* @return true if they match, false otherwise
*/
private boolean stringMatches(String pattern, String value) {
Pattern p = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
return p.matcher(value).matches();
}
}
}