/* * Seldon -- open source prediction engine * ======================================= * * Copyright 2011-2015 Seldon Technologies Ltd and Rummble Ltd (http://www.seldon.io/) * * ******************************************************************************************** * * 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 io.seldon.api.state.zk; import io.seldon.api.state.ClientConfigHandler; import io.seldon.api.state.ClientConfigUpdateListener; import io.seldon.api.state.NewClientListener; import io.seldon.api.state.ZkSubscriptionHandler; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.lang3.StringUtils; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.recipes.cache.ChildData; import org.apache.curator.framework.recipes.cache.TreeCacheEvent; import org.apache.curator.framework.recipes.cache.TreeCacheListener; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; /** * @author firemanphil * Date: 26/11/14 * Time: 13:51 */ @Component public class ZkClientConfigHandler implements TreeCacheListener, ClientConfigHandler { private static Logger logger = Logger.getLogger(ZkClientConfigHandler.class.getName()); private final ZkSubscriptionHandler handler; private Map<String,Map<String,String>> clientsWithInitialConfig; private final Set<ClientConfigUpdateListener> listeners; private final ArrayList<NewClientListener> newClientListeners; private static final String CLIENT_LIST_LOCATION = "all_clients"; private ObjectMapper jsonMapper = new ObjectMapper(); private boolean initalized = false; @Autowired public ZkClientConfigHandler(ZkSubscriptionHandler handler){ this.handler = handler; this.newClientListeners = new ArrayList<>(); this.listeners = new HashSet<>(); this.clientsWithInitialConfig = new ConcurrentHashMap<>(); } private boolean isClientPath(String path) { // i.e. a path like /all_clients/testclient is one but /all_clients/testclient/mf is not return StringUtils.countMatches(path,"/") == 2; } @Override public Map<String, String> requestCacheDump(String client){ if(initalized) { return handler.getChildrenValues("/" + CLIENT_LIST_LOCATION + "/" + client); } else { return Collections.emptyMap(); } } @Override public synchronized void addListener(ClientConfigUpdateListener listener) { logger.info("Adding client config listener, current clients are " + StringUtils.join(clientsWithInitialConfig.keySet(),',')); listeners.add(listener); } @Override public synchronized void addNewClientListener(NewClientListener listener, boolean notifyExistingClients) { newClientListeners.add(listener); if(notifyExistingClients){ for(String client : clientsWithInitialConfig.keySet()) listener.clientAdded(client, clientsWithInitialConfig.get(client)); } } @Override public synchronized void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception { if(event == null || event.getType() == null) { logger.warn("Event received was null somewhere"); return; } if(!initalized && event.getType() != TreeCacheEvent.Type.INITIALIZED) { logger.debug ("Ignore event as we are not in an initialised state: " + event); return; } if(event.getType()== TreeCacheEvent.Type.NODE_ADDED || event.getType() == TreeCacheEvent.Type.NODE_UPDATED) logger.info("Message received from ZK : " + event.toString()); switch (event.getType()){ case NODE_ADDED: if( event.getData() == null || event.getData().getPath()==null) { logger.warn("Event received was null somewhere"); return; } String path = event.getData().getPath(); if(isClientPath(path)){ String clientName = retrieveClientName(path); logger.info("Found new client : " + clientName); Map<String, String> initialConfig = new HashMap<>(); try { if(event.getData().getData()!=null) { initialConfig = jsonMapper.readValue( event.getData().getData(), new TypeReference<Map<String, String>>() { }); } } catch (IOException e){ logger.warn("Couldn't read JSON at " + path + ", ignoring"); } clientsWithInitialConfig.put(clientName, initialConfig); for (NewClientListener listener : newClientListeners) { listener.clientAdded(clientName, initialConfig); } break; } //purposeful cascade as the below deals with the rest of the cases case NODE_UPDATED: String location = event.getData().getPath(); boolean foundAMatch = false; String[] clientAndNode = location.replace("/" + CLIENT_LIST_LOCATION + "/", "").split("/",2); if(clientAndNode !=null && clientAndNode.length==2){ for(ClientConfigUpdateListener listener: listeners){ foundAMatch = true; byte[] data = event.getData().getData(); String dataString = data == null ? "" : new String(data); listener.configUpdated(clientAndNode[0], clientAndNode[1],dataString); } } else { logger.warn("Couldn't process message for node : " + location + " data : " + new String(event.getData().getData())); } if (!foundAMatch) logger.warn("Received message for node " + location +" : " + event.getType() + " but found no interested listeners"); break; case NODE_REMOVED: path = event.getData().getPath(); String[] clientAndNode2 = path.replace("/" + CLIENT_LIST_LOCATION + "/", "").split("/"); if(clientAndNode2 !=null && clientAndNode2.length==2){ for(ClientConfigUpdateListener listener: listeners){ listener.configRemoved(clientAndNode2[0], clientAndNode2[1]); } } if(isClientPath(path)){ String clientName = retrieveClientName(path); clientsWithInitialConfig.keySet().remove(clientName); logger.warn("Deleted client : " + clientName+" - presently resources will not be released"); //for (NewClientListener listener: newClientListeners) // listener.clientDeleted(clientName); //jdofactory.clientDeleted(clientName); // ensure called last in case other client removal listeners need db } break; case INITIALIZED: initalized = true; logger.info("Finished building '/all_clients' tree cache. "); afterCacheBuilt(); } } public static String retrieveClientName(String path) { return path.replace("/"+CLIENT_LIST_LOCATION+"/","").split("/")[0]; } private void afterCacheBuilt() throws Exception { // first get the clients Collection<ChildData> clientChildrenData = handler.getImmediateChildren("/" + CLIENT_LIST_LOCATION); logger.info("Found " +clientChildrenData.size() + " clients on start up."); for(ChildData clientChildData : clientChildrenData) { childEvent(null, new TreeCacheEvent(TreeCacheEvent.Type.NODE_ADDED, clientChildData)); // then the children of clients Collection<ChildData> furtherChildren = handler.getChildren(clientChildData.getPath()); logger.info("Found " +furtherChildren.size() + " children for client "+ retrieveClientName(clientChildData.getPath())+" on startup"); for (ChildData child : furtherChildren){ childEvent(null, new TreeCacheEvent(TreeCacheEvent.Type.NODE_ADDED, child)); } } } public void contextIntialised() throws Exception { handler.addSubscription("/" + CLIENT_LIST_LOCATION, this); } }