package org.radargun.service; import java.io.IOException; import java.lang.reflect.Type; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.spy.memcached.DefaultConnectionFactory; import net.spy.memcached.FailureMode; import net.spy.memcached.MemcachedClient; import net.spy.memcached.compat.log.Log4JLogger; import org.radargun.Service; import org.radargun.config.Converter; import org.radargun.config.Property; import org.radargun.logging.Log; import org.radargun.logging.LogFactory; import org.radargun.traits.Lifecycle; import org.radargun.traits.ProvidesTrait; import org.radargun.utils.TimeConverter; /** * @author Radim Vansa <rvansa@redhat.com> */ @Service(doc = "SpyMemcached client") public class SpyMemcachedService implements Lifecycle { private static final Log log = LogFactory.getLog(SpyMemcachedService.class); private static final int DEFAULT_PORT = 11211; private static final String PATTERN_STRING = "(\\[([0-9A-Fa-f:]+)\\]|([^:/?#]*))(?::(\\d*))?"; private static final Pattern ADDRESS_PATTERN = Pattern.compile(PATTERN_STRING); @Property(doc = "Expected cache name. Requests for other caches will fail. Defaults to null.") protected String cacheName; @Property(doc = "Semicolon-separated list of server addresses.", converter = AddressListConverter.class) protected List<InetSocketAddress> servers; @Property(doc = "Failure mode for the client. Default is 'Redistribute' (continue with next living node).") protected FailureMode failureMode = FailureMode.Redistribute; @Property(doc = "Timeout for operation (request will throw exception after this timeout). Default is 15 seconds.", converter = TimeConverter.class) protected long operationTimeout = 15000; @Property(doc = "Number of memcached client instances (each keeps only single connection to each server). Default is 100.") protected int poolSize = 100; protected MemcachedClient[] memcachedClients; protected AtomicInteger nextClient = new AtomicInteger(0); @ProvidesTrait public SpyMemcachedOperations createOperations() { return new SpyMemcachedOperations(this); } @ProvidesTrait public Lifecycle getLifecycle() { return this; } @Override public synchronized void start() { if (memcachedClients != null) { log.warn("Service already started"); return; } // TODO: route that through Radargun logging (to support log4j2) System.setProperty("net.spy.log.LoggerImpl", Log4JLogger.class.getName()); try { memcachedClients = new MemcachedClient[poolSize]; for (int i = 0; i < poolSize; ++i) { memcachedClients[i] = new MemcachedClient(new DefaultConnectionFactory() { @Override public FailureMode getFailureMode() { return failureMode; } @Override public long getOperationTimeout() { return operationTimeout; } }, servers); } } catch (IOException e) { throw new RuntimeException("Failed to start SpyMemcachedService", e); } } @Override public synchronized void stop() { if (memcachedClients == null) { log.warn("Service not started"); return; } for (MemcachedClient memcachedClient : memcachedClients) { memcachedClient.shutdown(); } memcachedClients = null; } @Override public synchronized boolean isRunning() { return memcachedClients != null; } /** * Distributes clients between cache instances. * @return Initialized memcached client, possibly shared. */ public MemcachedClient nextClient() { return memcachedClients[(nextClient.getAndIncrement() & Integer.MAX_VALUE) % poolSize]; } private static class AddressListConverter implements Converter<List<InetSocketAddress>> { @Override public List<InetSocketAddress> convert(String servers, Type type) { List<InetSocketAddress> addresses = new ArrayList<InetSocketAddress>(); for (String server : servers.split(";")) { Matcher matcher = ADDRESS_PATTERN.matcher(server.trim()); if (matcher.matches()) { String v6host = matcher.group(2); String v4host = matcher.group(3); String host = v6host != null ? v6host.trim() : v4host.trim(); String portString = matcher.group(4); int port = portString == null ? DEFAULT_PORT : Integer.parseInt(portString.trim()); addresses.add(new InetSocketAddress(host, port)); } else { throw new IllegalArgumentException("Cannot parse host:port from " + server); } } return addresses; } @Override public String convertToString(List<InetSocketAddress> value) { StringBuilder sb = new StringBuilder(); for (InetSocketAddress address : value) { if (sb.length() != 0) sb.append(';'); sb.append(address.getHostString()); InetAddress inetAddr = address.getAddress(); if (inetAddr != null) { sb.append('=').append(inetAddr.getCanonicalHostName()); sb.append('=').append(Arrays.toString(inetAddr.getAddress())); } else { sb.append("(not resolved)"); } sb.append(':').append(address.getPort()); } return sb.toString(); } @Override public String allowedPattern(Type type) { return PATTERN_STRING; } } }