/* * Copyright (c) 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.ketama; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import com.google.common.net.HostAndPort; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.spotify.dns.DnsSrvResolver; import com.spotify.dns.LookupResult; import com.spotify.folsom.AbstractRawMemcacheClient; import com.spotify.folsom.ConnectFuture; import com.spotify.folsom.ConnectionChangeListener; import com.spotify.folsom.ObservableClient; import com.spotify.folsom.RawMemcacheClient; import com.spotify.folsom.client.NotConnectedClient; import com.spotify.folsom.client.Request; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; public class SrvKetamaClient extends AbstractRawMemcacheClient { private static final Logger log = LoggerFactory.getLogger(SrvKetamaClient.class); public static final int MIN_DNS_WAIT_TIME = 10; public static final int MAX_DNS_WAIT_TIME = 3600; private final ScheduledExecutorService executor; private final String srvRecord; private final DnsSrvResolver srvResolver; private final long period; private final TimeUnit periodUnit; private final Connector connector; private final long shutdownDelay; private final TimeUnit shutdownUnit; private final MyConnectionChangeListener listener = new MyConnectionChangeListener(); private final Runnable refreshRunnable = new Runnable() { @Override public void run() { updateDNS(); } }; private ScheduledFuture<?> refreshJob; private final Object sync = new Object(); private volatile List<HostAndPort> addresses = Collections.emptyList(); private volatile RawMemcacheClient currentClient; private volatile RawMemcacheClient pendingClient = null; private boolean shutdown = false; public SrvKetamaClient(final String srvRecord, DnsSrvResolver srvResolver, ScheduledExecutorService executor, long period, TimeUnit periodUnit, final Connector connector, long shutdownDelay, TimeUnit shutdownUnit) { this.srvRecord = srvRecord; this.srvResolver = srvResolver; this.period = period; this.periodUnit = periodUnit; this.connector = connector; this.shutdownDelay = shutdownDelay; this.shutdownUnit = shutdownUnit; this.executor = executor; this.currentClient = NotConnectedClient.INSTANCE; this.currentClient.registerForConnectionChanges(listener); } public void start() { if (refreshJob != null) { throw new RuntimeException("You may only start this once"); } refreshJob = this.executor.schedule(refreshRunnable, 0, TimeUnit.MILLISECONDS); } public void updateDNS() { synchronized (sync) { if (shutdown) { return; } long ttl = TimeUnit.SECONDS.convert(period, periodUnit); try { List<LookupResult> lookupResults = srvResolver.resolve(srvRecord); List<HostAndPort> hosts = Lists.newArrayListWithCapacity(lookupResults.size()); for (LookupResult lookupResult : lookupResults) { hosts.add(HostAndPort.fromParts(lookupResult.host(), lookupResult.port())); ttl = Math.min(ttl, lookupResult.ttl()); } List<HostAndPort> newAddresses = Ordering.from(HostAndPortComparator.INSTANCE) .sortedCopy(hosts); if (!newAddresses.equals(addresses)) { addresses = newAddresses; log.info("Connecting to " + newAddresses); List<AddressAndClient> addressAndClients = getAddressesAndClients(newAddresses); setPendingClient(addressAndClients); } } finally { long delay = clamp(MIN_DNS_WAIT_TIME, MAX_DNS_WAIT_TIME, ttl); refreshJob = this.executor.schedule(refreshRunnable, delay, TimeUnit.SECONDS); } } } private long clamp(int min, int max, long value) { return Math.max(min, Math.min(max, value)); } private List<AddressAndClient> getAddressesAndClients(List<HostAndPort> newAddresses) { List<AddressAndClient> res = Lists.newArrayListWithCapacity(newAddresses.size()); for (HostAndPort address : newAddresses) { res.add(new AddressAndClient(address, connector.connect(address))); } return res; } @Override public <T> ListenableFuture<T> send(Request<T> request) { return currentClient.send(request); } @Override public void shutdown() { if (refreshJob != null) { refreshJob.cancel(false); } final RawMemcacheClient pending; synchronized (sync) { shutdown = true; pending = pendingClient; pendingClient = null; currentClient.shutdown(); } if (pending != null) { pending.unregisterForConnectionChanges(listener); pending.shutdown(); } } @Override public boolean isConnected() { return currentClient.isConnected(); } @Override public int numTotalConnections() { return currentClient.numTotalConnections(); } @Override public int numActiveConnections() { return currentClient.numActiveConnections(); } public interface Connector { RawMemcacheClient connect(HostAndPort input); } private void setPendingClient(List<AddressAndClient> addressAndClients) { final RawMemcacheClient newPending = new KetamaMemcacheClient(addressAndClients); newPending.registerForConnectionChanges(listener); final RawMemcacheClient oldPending; synchronized (sync) { oldPending = pendingClient; pendingClient = newPending; } ListenableFuture<Void> future = ConnectFuture.connectFuture(newPending); Futures.addCallback(future, new FutureCallback<Void>() { @Override public void onSuccess(@Nullable Void result) { final RawMemcacheClient oldClient; synchronized (sync) { if (newPending != pendingClient) { // We don't care about this event if it's not the expected client return; } oldClient = currentClient; currentClient = pendingClient; pendingClient = null; } notifyConnectionChange(); executor.schedule(new ShutdownJob(oldClient), shutdownDelay, shutdownUnit); } @Override public void onFailure(Throwable t) { throw new RuntimeException("Programmer bug - this is unreachable code"); } }); if (oldPending != null) { oldPending.unregisterForConnectionChanges(listener); oldPending.shutdown(); } } private static class HostAndPortComparator implements Comparator<HostAndPort> { private static final HostAndPortComparator INSTANCE = new HostAndPortComparator(); @Override public int compare(HostAndPort o1, HostAndPort o2) { int cmp = o1.getHostText().compareTo(o2.getHostText()); if (cmp != 0) { return cmp; } return Integer.compare(o1.getPort(), o2.getPort()); } } private class ShutdownJob implements Runnable { private final RawMemcacheClient oldClient; public ShutdownJob(RawMemcacheClient oldClient) { this.oldClient = oldClient; } @Override public void run() { oldClient.unregisterForConnectionChanges(listener); oldClient.shutdown(); } } private class MyConnectionChangeListener implements ConnectionChangeListener { @Override public void connectionChanged(ObservableClient client) { notifyConnectionChange(); } } }