/* * Copyright (c) 2014-2015 Spotify AB * * 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.spotify.folsom; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.net.HostAndPort; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.spotify.dns.DnsSrvResolver; import com.spotify.dns.DnsSrvResolvers; import com.spotify.folsom.client.NoopMetrics; import com.spotify.folsom.client.ascii.DefaultAsciiMemcacheClient; import com.spotify.folsom.client.binary.DefaultBinaryMemcacheClient; import com.spotify.folsom.ketama.AddressAndClient; import com.spotify.folsom.ketama.KetamaMemcacheClient; import com.spotify.folsom.ketama.SrvKetamaClient; import com.spotify.folsom.reconnect.ReconnectingClient; import com.spotify.folsom.retry.RetryingClient; import com.spotify.folsom.roundrobin.RoundRobinMemcacheClient; import com.spotify.folsom.transcoder.ByteArrayTranscoder; import com.spotify.folsom.transcoder.SerializableObjectTranscoder; import com.spotify.folsom.transcoder.StringTranscoder; import java.io.Serializable; import java.nio.charset.Charset; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; public class MemcacheClientBuilder<V> { private static final int DEFAULT_MAX_SET_LENGTH = 1024 * 1024; private static final int DEFAULT_MAX_OUTSTANDING = 1000; private static final String DEFAULT_HOSTNAME = "127.0.0.1"; private static final int DEFAULT_PORT = 11211; /** * Lazily instantiated singleton default executor. */ private static class DefaultExecutor { private static final Executor INSTANCE = new ForkJoinPool( Runtime.getRuntime().availableProcessors(), ForkJoinPool.defaultForkJoinWorkerThreadFactory, new UncaughtExceptionHandler(), true); } /** * Lazily instantiated singleton default srvResolver. */ private static class DefaultDnsResolver { private static final DnsSrvResolver INSTANCE = DnsSrvResolvers.newBuilder() .cachingLookups(true) .retainingDataOnFailures(true) .build(); } /** * Lazily instantiated singleton default scheduled executor. */ private static class DefaultScheduledExecutor { private static final ScheduledExecutorService INSTANCE = Executors.newSingleThreadScheduledExecutor( new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat("folsom-default-scheduled-executor") .build()); } private List<HostAndPort> addresses = null; private int maxOutstandingRequests = DEFAULT_MAX_OUTSTANDING; private final Transcoder<V> valueTranscoder; private Metrics metrics = NoopMetrics.INSTANCE; private BackoffFunction backoffFunction = new ExponentialBackoff(10L, 60 * 1000L, 2.5); private int connections = 1; private boolean retry = true; private Executor executor; private Charset charset = Charsets.UTF_8; private DnsSrvResolver srvResolver; private String srvRecord; private long dnsRefreshPeriod = 60 * 1000L; private long shutdownDelay = 60 * 1000L; private long timeoutMillis = 3000; private int maxSetLength = DEFAULT_MAX_SET_LENGTH; /** * Create a client builder for byte array values. * @return The builder */ public static MemcacheClientBuilder<byte[]> newByteArrayClient() { return new MemcacheClientBuilder<>(ByteArrayTranscoder.INSTANCE); } /** * Create a client builder with a basic string transcoder using the UTF-8 * Charset. * * @return The builder */ public static MemcacheClientBuilder<String> newStringClient() { return newStringClient(Charsets.UTF_8); } /** * Create a client builder with a basic string transcoder using the supplied * Charset. * * @param charset the Charset to encode and decode String objects with. * @return The builder */ public static MemcacheClientBuilder<String> newStringClient(Charset charset) { return new MemcacheClientBuilder<>(new StringTranscoder(charset)); } /** * Create a client builder for serializable object values. * @return The builder */ public static MemcacheClientBuilder<Serializable> newSerializableObjectClient() { return new MemcacheClientBuilder<>(SerializableObjectTranscoder.INSTANCE); } /** * Create a client builder with the provided value transcoder. * @param valueTranscoder the transcoder to use to encode/decode values. */ public MemcacheClientBuilder(final Transcoder<V> valueTranscoder) { this.valueTranscoder = valueTranscoder; } /** * Define the charset encoding for keys. * Note that some charsets may not be compatible with the memcache protocol which requires * that keys are a string of 8-bit characters with the exclusion of characters in the range * [0x00, 0x20]. * This is a problem in charsets such as UTF-16 which uses two bytes and one of them * could easily be invalid for memcache purposes. * * UTF-8 and most single-byte charsets should be fine though. * * @param charset The charset encoding for keys. The default is UTF-8. * @return itself */ public MemcacheClientBuilder<V> withKeyCharset(final Charset charset) { this.charset = checkNotNull(charset); return this; } /** * Define which memcache server to connect to. * @param hostname a server, using the default memcached port (11211). * @return itself */ public MemcacheClientBuilder<V> withAddress(final String hostname) { return withAddress(HostAndPort.fromParts(hostname, DEFAULT_PORT)); } /** * Define which memcache server to connect to. * @param address a server. * @return itself */ public MemcacheClientBuilder<V> withAddress(HostAndPort address) { this.addresses = ImmutableList.of(address); return this; } /** * Define which memcache servers to connect to. * If more than one address is given, Ketama will be used to distribute requests. * @param addresses a list of servers. * @return itself */ public MemcacheClientBuilder<V> withAddresses(final List<HostAndPort> addresses) { checkArgument(!addresses.isEmpty()); this.addresses = ImmutableList.copyOf(checkNotNull(addresses)); return this; } /** * Use SRV to lookup nodes instead of a fixed set of addresses. * This means that the set of nodes can change dynamically over time. * @param srvRecord the SRV record to use. * @return itself */ public MemcacheClientBuilder<V> withSRVRecord(final String srvRecord) { this.srvRecord = checkNotNull(srvRecord); return this; } /** * This is only used for the SRV based ketama client. This is the maximum * time DNS should be queried for updates. * It can be shorter, depending the ttl values in the DNS lookup result * * @param periodMillis time in milliseonds. The default is 60 seconds. * @return itself */ public MemcacheClientBuilder<V> withSRVRefreshPeriod(final long periodMillis) { this.dnsRefreshPeriod = periodMillis; return this; } /** * This is only used for the SRV based ketama client. When the SRV record has changed, * the old client will be shutdown after this much time has passed, in order to complete * pending requests. * @param shutdownDelay time in milliseconds. The default is 60 seconds. * @return itself */ public MemcacheClientBuilder<V> withSRVShutdownDelay(final long shutdownDelay) { this.shutdownDelay = shutdownDelay; return this; } /** * Use a specific SRV resolver. * @param srvResolver the resolver to use. Default is a caching resolver from * {@link com.spotify.dns.DnsSrvResolvers} * @return itself */ public MemcacheClientBuilder<V> withSrvResolver(final DnsSrvResolver srvResolver) { this.srvResolver = checkNotNull(srvResolver, "srvResolver"); return this; } /** * Specify how to collect metrics. * @param metrics Default is NoopMetrics - which doesn't collect anything. * @return itself */ public MemcacheClientBuilder<V> withMetrics(final Metrics metrics) { this.metrics = metrics; return this; } /** * Specify the maximum number of requests in the queue per server connection. * If this is set too low, requests will fail with * {@link com.spotify.folsom.MemcacheOverloadedException}. * If this is set too high, there is a risk of having high latency requests and delaying the * time to notice that the system is malfunctioning. * @param maxOutstandingRequests the maximum number of requests that can be in queue. * Default is 1000. * @return itself */ public MemcacheClientBuilder<V> withMaxOutstandingRequests(final int maxOutstandingRequests) { this.maxOutstandingRequests = maxOutstandingRequests; return this; } /** * Specify how long the client should wait between reconnects. * @param backoffFunction A custom backoff function. Default is exponential backoff. * @return itself. */ public MemcacheClientBuilder<V> withBackoff(final BackoffFunction backoffFunction) { this.backoffFunction = backoffFunction; return this; } /** * Specify if the client should retry once if the connection is closed. * This typically only has an effect when one of the ketama nodes disconnect while the * request is sent. * @param retry Default is true * @return itself */ public MemcacheClientBuilder<V> withRetry(final boolean retry) { this.retry = retry; return this; } /** * Specify an executor to execute all replies on. Default is a shared {@link ForkJoinPool} in * async mode with one thread per processor. * @param executor the executor to use. * @return itself */ public MemcacheClientBuilder<V> withReplyExecutor(final Executor executor) { this.executor = Preconditions.checkNotNull(executor); return this; } /** * Use multiple connections to each memcache server. * This is likely not a useful thing in practice, * unless the IO connection really becomes a bottleneck. * @param connections Number of connections, must be 1 or greater. The default is 1 * @return itself */ public MemcacheClientBuilder<V> withConnections(final int connections) { if (connections < 1) { throw new IllegalArgumentException("connections must be at least 1"); } this.connections = connections; return this; } /** * Enforce a timeout for requests to complete, closing the connection and reconnecting if the * timeout is exceeded. * @param timeoutMillis The timeout in milliseconds. The default is 3000 ms. * @return itself */ public MemcacheClientBuilder<V> withRequestTimeoutMillis(final long timeoutMillis) { this.timeoutMillis = timeoutMillis; return this; } /** * Set the maximum value size for set requests. * If the limit is exceeded the set operation will fast-fail with a VALUE_TOO_LARGE status. * * If this limit is set higher than the actual limit in the memcache service, * the memcache service may return a SERVER_ERROR * which will close the connection to prevent any corrupted state. * * The default value is 1 MiB * * @param maxSetLength The maximum size in bytes * @return itself */ public MemcacheClientBuilder<V> withMaxSetLength(final int maxSetLength) { this.maxSetLength = maxSetLength; return this; } /** * Create a client that uses the binary memcache protocol. * @return a {@link com.spotify.folsom.BinaryMemcacheClient} */ public BinaryMemcacheClient<V> connectBinary() { return new DefaultBinaryMemcacheClient<>(connectRaw(true), metrics, valueTranscoder, charset); } /** * Create a client that uses the ascii memcache protocol. * @return a {@link com.spotify.folsom.AsciiMemcacheClient} */ public AsciiMemcacheClient<V> connectAscii() { return new DefaultAsciiMemcacheClient<>(connectRaw(false), metrics, valueTranscoder, charset); } /** * Connect a raw memcached client without any protocol specific methods. * This should rarely be needed. * @param binary whether to use the binary protocol or not. * @return A raw memcached client. */ protected RawMemcacheClient connectRaw(boolean binary) { List<HostAndPort> addresses = this.addresses; RawMemcacheClient client; if (srvRecord != null) { if (addresses != null) { throw new IllegalStateException("You may not specify both srvRecord and addresses"); } client = createSRVClient(binary); } else { if (addresses == null) { addresses = ImmutableList.of( HostAndPort.fromParts(DEFAULT_HOSTNAME, DEFAULT_PORT)); } final List<RawMemcacheClient> clients = createClients(addresses, binary); if (addresses.size() > 1) { checkState(clients.size() == addresses.size()); final List<AddressAndClient> aac = Lists.newArrayListWithCapacity(clients.size()); for (int i = 0; i < clients.size(); i++) { final HostAndPort address = addresses.get(i); aac.add(new AddressAndClient(address, clients.get(i))); } client = new KetamaMemcacheClient(aac); } else { client = clients.get(0); } } if (retry) { return new RetryingClient(client); } return client; } private List<RawMemcacheClient> createClients(List<HostAndPort> addresses, boolean binary) { final List<RawMemcacheClient> clients = Lists.newArrayListWithCapacity(addresses.size()); for (final HostAndPort address : addresses) { clients.add(createClient(address, binary)); } return clients; } private RawMemcacheClient createSRVClient(final boolean binary) { DnsSrvResolver resolver = srvResolver; if (resolver == null) { resolver = DefaultDnsResolver.INSTANCE; } SrvKetamaClient client = new SrvKetamaClient(srvRecord, resolver, DefaultScheduledExecutor.INSTANCE, dnsRefreshPeriod, TimeUnit.MILLISECONDS, new SrvKetamaClient.Connector() { @Override public RawMemcacheClient connect(HostAndPort input) { return createClient(input, binary); } }, shutdownDelay, TimeUnit.MILLISECONDS); client.start(); return client; } private RawMemcacheClient createClient(final HostAndPort address, boolean binary) { if (connections == 1) { return createReconnectingClient(address, binary); } final List<RawMemcacheClient> clients = Lists.newArrayList(); for (int i = 0; i < connections; i++) { clients.add(createReconnectingClient(address, binary)); } return new RoundRobinMemcacheClient(clients); } private RawMemcacheClient createReconnectingClient(final HostAndPort address, boolean binary) { final Executor executor = this.executor != null ? this.executor : DefaultExecutor.INSTANCE; return new ReconnectingClient( backoffFunction, ReconnectingClient.singletonExecutor(), address, maxOutstandingRequests, binary, executor, timeoutMillis, charset, metrics, maxSetLength); } }