/* * Licensed to the Apache Software Foundation (ASF) under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional information regarding * copyright ownership. The ASF licenses this file to You 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 org.apache.geode.internal; import static org.apache.geode.distributed.ConfigurationProperties.*; import org.apache.geode.admin.internal.InetAddressUtil; import org.apache.geode.cache.Cache; import org.apache.geode.cache.CacheFactory; import org.apache.geode.cache.Region; import org.apache.geode.distributed.DistributedSystem; import org.apache.geode.distributed.internal.DistributionConfig; import org.apache.geode.distributed.internal.InternalDistributedSystem; import org.apache.geode.internal.cache.GemFireCacheImpl; import org.apache.geode.internal.i18n.LocalizedStrings; import java.io.*; import java.net.*; import java.util.Enumeration; import java.util.Iterator; import java.util.Properties; import static org.apache.geode.distributed.ConfigurationProperties.LOCATORS; import static org.apache.geode.distributed.ConfigurationProperties.MCAST_PORT; /** * MigrationServer creates a cache using a supplied cache.xml and then opens a server socket that a * MigrationClient connects to and requests the data from a Region. MigrationServer sends the data * to the MigrationClient using normal java serialization in order to allow migration from * incompatible versions of DataSerializer. Keys and values stored in the cache must serialize and * deserialize correctly. * <p> * Command line arguments are<br> *   cache-xml-file-name (required)<br> *   listen port (defaults to 10553)<br> *   bind address (defaults to listing on all interfaces)<br> * <p> * Both the MigrationClient and MigrationServer must be configured to have the appropriate domain * classes in their CLASSPATH, or errors will be encountered during deserialization. * <P> * Details of the transfers can be viewed by setting the system property Migration.VERBOSE=true. * <p> * For example, * * <pre> * java -cp $MYCLASSES:migration.jar:$GEMFIRE/lib/geode-dependencies.jar \ * org.apache.geode.internal.MigrationServer cacheDescription.xml * </pre> * <p> * Where the cacheDescription.xml file might look like this: * * <pre> * <!DOCTYPE cache PUBLIC "-//GemStone Systems, Inc.//GemFire Declarative Caching 5.7//EN" "http://www.gemstone.com/dtd/cache5_7.dtd"> <cache is-server="false"> <region name="root"> <region-attributes scope="distributed-no-ack"> </region-attributes> <region name="Test"> <region-attributes data-policy="persistent-replicate"> <disk-write-attributes> <synchronous-writes/> </disk-write-attributes> <disk-dirs> <disk-dir>diskfiles</disk-dir> </disk-dirs> <eviction-attributes> <lru-memory-size maximum="100" action="overflow-to-disk"/> </eviction-attributes> </region-attributes> </region> <!-- Test region --> </region> <!-- root region --> </cache> * * </pre> * <p> * The client is then run with a different cache description having different disk-dirs to hold the * migrated information. * * @since GemFire 6.0.1 */ public class MigrationServer { final static boolean VERBOSE = Boolean.getBoolean("Migration.VERBOSE"); final static int VERSION = 551; // version for backward communications compatibility protected static final int CODE_ERROR = 0; protected static final int CODE_ENTRY = 1; /* serialized key, serialized value */ protected static final int CODE_COMPLETED = 2; public static void main(String[] args) throws Exception { int argIdx = 0; String cacheXmlFileName = "cache.xml"; String bindAddressName = null; int listenPort = 10533; if (args.length > 0) { cacheXmlFileName = args[argIdx++]; } else { System.err.println("MigrationServer cache-xml-file [server-address] [server-port]"); } if (args.length > argIdx) { listenPort = Integer.parseInt(args[argIdx++]); } if (args.length > argIdx) { bindAddressName = args[argIdx++]; } MigrationServer instance = null; try { instance = new MigrationServer(cacheXmlFileName, bindAddressName, listenPort); } catch (IllegalArgumentException e) { System.err.println(e.getMessage()); System.exit(1); } instance.createDistributedSystem(); instance.createCache(); instance.serve(); } private InetAddress bindAddress; private int listenPort; private ServerSocket serverSocket; private DistributedSystem distributedSystem; private File cacheXmlFile; private Cache cache; /** * Create a MigrationServer to be used with a DistributedSystem and Cache that are created using * GemFire APIs * * @param bindAddressName the NIC to bind to, or null to use all interfaces * @param listenPort the port to listen on */ public MigrationServer(String bindAddressName, int listenPort) { this.listenPort = listenPort; if (bindAddressName != null) { if (!isLocalHost(bindAddressName)) { throw new IllegalArgumentException( "Error - bind address is not an address of this machine: '" + bindAddressName + "'"); } try { this.bindAddress = InetAddress.getByName(bindAddressName); } catch (IOException e) { throw new IllegalArgumentException( "Error - bind address cannot be resolved: '" + bindAddressName + "'"); } } try { if (this.bindAddress != null) { this.serverSocket = new ServerSocket(); SocketAddress addr = new InetSocketAddress(this.bindAddress, listenPort); this.serverSocket.bind(addr); } else { this.serverSocket = new ServerSocket(listenPort); } if (VERBOSE) { System.out.println("created server socket " + serverSocket); } } catch (IOException e) { throw new IllegalArgumentException("Port is already in use", e); } } /** * this is for use by main() * * @param cacheXmlFileName the name of the xml file describing the cache, or null * @param bindAddressName the name of the NIC to bind to, or null * @param listenPort the port to listen on (must not be zero) */ private MigrationServer(String cacheXmlFileName, String bindAddressName, int listenPort) { this(bindAddressName, listenPort); this.cacheXmlFile = new File(cacheXmlFileName); if (!this.cacheXmlFile.exists()) { // in 6.x this should be localizable System.err.println("Warning - file not found in local directory: '" + cacheXmlFileName + "'"); } } /** * Create a distributed system. If this method is not invoked before running the MigrationServer, * an existing distributed system must exist for the server to use. * * @throws Exception if there are any problems */ private void createDistributedSystem() throws Exception { Properties dsProps = new Properties(); // if no discovery information has been explicitly given, use a loner ds if (System.getProperty(DistributionConfig.GEMFIRE_PREFIX + MCAST_PORT) == null && System.getProperty(DistributionConfig.GEMFIRE_PREFIX + LOCATORS) == null) { dsProps.put(MCAST_PORT, "0"); } dsProps.put(LOG_FILE, "migrationServer.log"); if (this.cacheXmlFile != null) { dsProps.put(CACHE_XML_FILE, this.cacheXmlFile.getName()); } this.distributedSystem = DistributedSystem.connect(dsProps); if (VERBOSE) { System.out.println("created distributed system " + this.distributedSystem); } } /** * create the cache to be used by this migration server * * @throws Exception if there are any problems */ private void createCache() throws Exception { if (this.distributedSystem == null) { this.distributedSystem = InternalDistributedSystem.getConnectedInstance(); } this.cache = CacheFactory.create(this.distributedSystem); if (VERBOSE) { System.out.println("created cache " + this.cache); } } /** * This locates the distributed system and cache, if they have not been created by this server, * and then listens for requests from MigrationClient processes. * * @throws IllegalStateException if an attempt is made to reuse a server that has been stopped */ public void serve() throws Exception { if (this.serverSocket == null || this.serverSocket.isClosed()) { throw new IllegalStateException("This server has been closed and cannot be reused"); } try { if (this.distributedSystem == null) { this.distributedSystem = InternalDistributedSystem.getConnectedInstance(); } if (this.cache == null) { this.cache = GemFireCacheImpl.getInstance(); } if (this.bindAddress != null) { System.out.println("Migration server on port " + this.listenPort + " bound to " + this.bindAddress + " is ready for client requets"); } else { System.out.println( "Migration server on port " + this.listenPort + " is ready for client requests"); } for (;;) { if (Thread.interrupted() || this.serverSocket.isClosed()) { return; } Socket clientSocket; try { clientSocket = this.serverSocket.accept(); } catch (java.net.SocketException e) { return; } (new RequestHandler(clientSocket)).serveClientRequest(); } } finally { System.out.println("Closing migration server"); try { this.serverSocket.close(); } catch (Exception e) { this.serverSocket = null; } } } /** * this causes the migration server to stop serving after it finishes dispatching any in-process * requests * * @throws IOException if there is a problem closing the server socket */ public void stop() throws IOException { if (this.serverSocket != null && !this.serverSocket.isClosed()) { this.serverSocket.close(); } } /** * get the cache being used by this migration server * * @return the cache, or null if a cache has not yet been associated with this server */ public Cache getCache() { return this.cache; } /** * get the distributed system being used by this migration server * * @return the distributed system, or null if a system has not yet been associated with this * server */ public DistributedSystem getDistributedSystem() { return this.distributedSystem; } // copied from 6.0 SocketCreator public static boolean isLocalHost(Object host) { if (host instanceof InetAddress) { if (InetAddressUtil.LOCALHOST.equals(host)) { return true; } else { try { Enumeration en = NetworkInterface.getNetworkInterfaces(); while (en.hasMoreElements()) { NetworkInterface i = (NetworkInterface) en.nextElement(); for (Enumeration en2 = i.getInetAddresses(); en2.hasMoreElements();) { InetAddress addr = (InetAddress) en2.nextElement(); if (host.equals(addr)) { return true; } } } return false; } catch (SocketException e) { throw new IllegalArgumentException( LocalizedStrings.InetAddressUtil_UNABLE_TO_QUERY_NETWORK_INTERFACE .toLocalizedString(), e); } } } else { return isLocalHost(toInetAddress(host.toString())); } } // copied from 6.0 SocketCreator public static InetAddress toInetAddress(String host) { if (host == null || host.length() == 0) { return null; } try { if (host.indexOf("/") > -1) { return InetAddress.getByName(host.substring(host.indexOf("/") + 1)); } else { return InetAddress.getByName(host); } } catch (java.net.UnknownHostException e) { throw new IllegalArgumentException(e.getMessage()); } } // R E Q U E S T H A N D L E R class RequestHandler implements Runnable { Socket clientSocket; DataInputStream dis; DataOutputStream dos; RequestHandler(Socket clientSocket) throws IOException { this.clientSocket = clientSocket; dos = new DataOutputStream(this.clientSocket.getOutputStream()); dis = new DataInputStream(this.clientSocket.getInputStream()); } // for now this is a blocking operation - multithread later if necessary void serveClientRequest() { try { run(); } finally { if (!this.clientSocket.isClosed()) { try { this.clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } } public void run() { try { // first exchange version information so we can communicate correctly dos.writeShort(VERSION); int version = dis.readShort(); handleRequest(version); } catch (IOException e) { System.err.println("Trouble dispatching request: " + e.getMessage()); return; } finally { try { this.clientSocket.close(); } catch (IOException e) { System.err.println("Trouble closing client socket: " + e.getMessage()); } } } /** * read and dispatch a single request on client socket * * @param clientVersion */ private void handleRequest(int clientVersion) { // for now we ignore the client version in the server. The client // is typically of a later release than the server, and this information // is given to the server in case a situation arises where it's needed try { ClientRequest req = ClientRequest.readRequest(this.clientSocket, dis, dos); if (req != null) { System.out.println( "Processing " + req + " from " + this.clientSocket.getInetAddress().getHostAddress()); req.process(MigrationServer.this); dos.flush(); } } catch (IOException e) { e.printStackTrace(); } } } // R E Q U E S T C L A S S E S static abstract class ClientRequest { Socket clientSocket; DataInputStream dsi; DataOutputStream dso; final static int REGION_REQUEST = 1; /** * Use readRequest to create a new request object, not this constructor. Subclasses may refine * this constructor to perform other initialization * * @param dsi socket's input stream * @param dso socket's output stream * @throws IOException if there are any problems reading initialization information */ ClientRequest(Socket clientSocket, DataInputStream dsi, DataOutputStream dso) throws IOException { this.clientSocket = clientSocket; this.dsi = dsi; this.dso = dso; } /** * Read and return a request from a client * * @param clientSocket * @param dsi socket input stream * @param dso socket output stream * @return the new request * @throws IOException */ static ClientRequest readRequest(Socket clientSocket, DataInputStream dsi, DataOutputStream dso) throws IOException { int requestType = dsi.readShort(); switch (requestType) { case REGION_REQUEST: return new RegionRequest(clientSocket, dsi, dso); } String errorMessage = "Type of request is not implemented in this server"; dso.writeShort(CODE_ERROR); dso.writeUTF(errorMessage); System.err.println("Migration server received unknown type of request (" + requestType + ") from " + clientSocket.getInetAddress().getHostAddress()); return null; } void writeErrorResponse(String message) throws IOException { this.dso.writeShort(CODE_ERROR); this.dso.writeUTF(message); } abstract void process(MigrationServer server) throws IOException; } /** * RegionRequest represents a request for the keys and values of a Region from a client. */ static class RegionRequest extends ClientRequest { String regionName; RegionRequest(Socket clientSocket, DataInputStream dsi, DataOutputStream dso) throws IOException { super(clientSocket, dsi, dso); regionName = dsi.readUTF(); } @Override public String toString() { return "request for contents of region '" + this.regionName + "'"; } @Override void process(MigrationServer server) throws IOException { Cache cache = server.getCache(); Region region = null; try { region = cache.getRegion(regionName); if (region == null) { String errorMessage = "Error: region " + this.regionName + " not found in cache"; System.err.println(errorMessage); writeErrorResponse(errorMessage); } } catch (IllegalArgumentException e) { String errorMessage = "Error: malformed region name"; System.err.println(errorMessage); writeErrorResponse(errorMessage); } try { for (Iterator it = region.entrySet().iterator(); it.hasNext();) { sendEntry((Region.Entry) it.next()); } this.dso.writeShort(CODE_COMPLETED); } catch (Exception e) { writeErrorResponse(e.getMessage()); } } private void sendEntry(Region.Entry entry) throws Exception { Object key = entry.getKey(); Object value = entry.getValue(); if (!(key instanceof Serializable)) { throw new IOException("Could not serialize entry for '" + key + "'"); } if (!(value instanceof Serializable)) { throw new IOException("Could not serialize entry for '" + key + "'"); } if (VERBOSE) { System.out.println("Sending " + key); } dso.writeShort(CODE_ENTRY); (new ObjectOutputStream(clientSocket.getOutputStream())).writeObject(key); (new ObjectOutputStream(clientSocket.getOutputStream())).writeObject(value); } } }