package com.limegroup.gnutella;
import java.lang.ref.WeakReference;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.inject.EagerSingleton;
import org.limewire.io.Connectable;
import org.limewire.io.GUID;
import org.limewire.io.IpPort;
import org.limewire.io.IpPortSet;
import org.limewire.io.NetworkInstanceUtils;
import org.limewire.lifecycle.ServiceScheduler;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.limegroup.gnutella.dht.db.SearchListener;
import com.limegroup.gnutella.uploader.HTTPHeaderUtils;
@EagerSingleton
class PushEndpointCacheImpl implements PushEndpointCache {
private static final Log LOG = LogFactory.getLog(PushEndpointCacheImpl.class);
/**
* A mapping from GUID to a CachedPushEndpoint. This is used to ensure
* that all PE's will have access to the same PushProxies, even if
* multiple PE's exist for a single GUID. Because access to the proxies
* is referenced from this GUID_PROXY_MAP, the PE will always receive the
* most up-to-date set of proxies.
*
* Insertion to this map must be manually performed, to allow for temporary
* PE objects that are used to update pre-existing ones.
*
* There is no explicit removal from the map -- the Weak nature of it will
* automatically remove the key/value pairs when the key is garbage
* collected. For this reason, all PEs must reference the exact GUID
* object that is stored in the map -- to ensure that the map will not GC
* the GUID while it is still in use by a PE.
*
* The value is a CachedPushEndpoint SetWrapper (containing a WeakReference to the
* GUID key as well as the Set of proxies) so that subsequent PEs can
* reference the true key object. A WeakReference is used to allow
* GC'ing to still work and the map to ultimately remove unused keys.
*/
private final Map<GUID, CachedPushEndpoint> GUID_PROXY_MAP =
Collections.synchronizedMap(new WeakHashMap<GUID, CachedPushEndpoint>());
private final HTTPHeaderUtils httpHeaderUtils;
private final NetworkInstanceUtils networkInstanceUtils;
@Inject
PushEndpointCacheImpl(HTTPHeaderUtils httpHeaderUtils, NetworkInstanceUtils networkInstanceUtils) {
this.httpHeaderUtils = httpHeaderUtils;
this.networkInstanceUtils = networkInstanceUtils;
}
@Inject
public void register(final @Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor, ServiceScheduler serviceScheduler) {
serviceScheduler.scheduleWithFixedDelay("PushEndpointCacheImpl WeakHashMap Cleaner", new WeakCleaner(), 30, 30, TimeUnit.SECONDS, backgroundExecutor);
}
/**
* Overwrites the current known push proxies for the host specified
* by the GUID, using the the proxies as written in the httpString.
*
* @param guid the guid whose proxies to overwrite
* @param httpString comma-separated list of proxies and possible proxy features
*/
public void overwriteProxies(byte [] guid, String httpString) {
Set<? extends Connectable> newSet = httpHeaderUtils.decodePushProxies(httpString, ",");
overwriteProxies(guid, newSet);
}
/**
* Overwrites any stored proxies for the host specified by the guid.
*
* @param guid the guid whose proxies to overwrite
* @param newSet the proxies to overwrite with
*/
public void overwriteProxies(byte[] guid, Set<? extends IpPort> newSet) {
GUID g = new GUID(guid);
CachedPushEndpoint wrapper ;
synchronized(GUID_PROXY_MAP) {
wrapper = GUID_PROXY_MAP.get(g);
if (wrapper==null) {
wrapper = new CachedPushEndpoint(g, newSet);
GUID_PROXY_MAP.put(g, wrapper);
} else {
wrapper.overwriteProxies(newSet);
}
}
}
public void removePushProxy(byte[] bytes, IpPort pushProxy) {
if (LOG.isDebugEnabled()) {
LOG.debug("Removing push proxy: " + pushProxy + " for " + new GUID(bytes));
}
GUID guid = new GUID(bytes);
CachedPushEndpoint cachedPushEndpoint = GUID_PROXY_MAP.get(guid);
if (cachedPushEndpoint != null) {
cachedPushEndpoint.removePushProxy(pushProxy);
}
}
/**
* updates the external address of all PushEndpoints for the given guid
*/
public void setAddr(byte [] guid, IpPort addr) {
GUID g = new GUID(guid);
CachedPushEndpoint current = getCached(g);
if (current!=null)
current.setIpPort(addr);
}
/**
* Sets the fwt version supported for all PEs pointing to the
* given client guid.
*/
public void setFWTVersionSupported(byte[] guid,int version){
GUID g = new GUID(guid);
CachedPushEndpoint current = getCached(g);
if (current!=null)
current.setFWTVersion(version);
}
public CachedPushEndpoint getCached(GUID guid) {
return GUID_PROXY_MAP.get(guid);
}
public PushEndpoint getPushEndpoint(GUID guid) {
CachedPushEndpoint cached = GUID_PROXY_MAP.get(guid);
return cached != null ? cached.createClone() : null;
}
public void findPushEndpoint(GUID guid, SearchListener<PushEndpoint> listener) {
PushEndpoint pushEndpoint = getPushEndpoint(guid);
if (pushEndpoint != null) {
listener.handleResult(pushEndpoint);
} else {
listener.searchFailed();
}
}
public GUID updateProxiesFor(GUID guid, PushEndpoint pushEndpoint, boolean valid) {
if (LOG.isDebugEnabled()) {
LOG.debug("Updating proxies for: " + guid + " with: " + pushEndpoint + ", valid: " + valid);
}
CachedPushEndpoint existing;
GUID guidRef = null;
synchronized(GUID_PROXY_MAP) {
existing = GUID_PROXY_MAP.get(guid);
// try to get a hard ref so that the mapping won't expire
if (existing!=null)
guidRef=existing.getGuid();
// if we do not have a mapping for this guid, or it just expired,
// add a new one atomically
// (we don't care about the proxies of the expired mapping)
if (existing == null || guidRef==null) {
existing = new CachedPushEndpoint(guid, pushEndpoint.getFeatures(), pushEndpoint.getFWTVersion(),
valid ? pushEndpoint.getProxies() : IpPort.EMPTY_SET);
GUID_PROXY_MAP.put(guid, existing);
return guid;
}
}
// if we got here, means we did have a mapping. no need to
// hold the map mutex when updating just the set
existing.updateProxies(pushEndpoint.getProxies(), valid);
return guidRef;
}
public void clear() {
GUID_PROXY_MAP.clear();
}
private final class WeakCleaner implements Runnable {
public void run() {
GUID_PROXY_MAP.size();
}
}
class CachedPushEndpoint extends AbstractPushEndpoint {
private final WeakReference<GUID> _guidRef;
/**
* Class invariant: never null
*/
private Set<IpPort> _proxies;
private byte _features;
private int _fwtVersion;
private IpPort _externalAddr;
private final byte[] guid;
CachedPushEndpoint(GUID guid, Set<? extends IpPort> proxies) {
this(guid, (byte)0, 0, proxies);
}
CachedPushEndpoint(GUID guid, byte features, int version, Set<? extends IpPort> proxies) {
this.guid = guid.bytes();
_guidRef = new WeakReference<GUID>(guid);
_features=features;
_fwtVersion=version;
overwriteProxies(proxies);
}
public synchronized void removePushProxy(IpPort pushProxy) {
// It's imortant to use IPortSet instead of Collections.singleton for
// comparator correctness based on IpPort.COMPARATOR
updateProxies(new IpPortSet(pushProxy), false);
}
public synchronized void updateProxies(Set<? extends IpPort> s, boolean add){
Set<IpPort> existing = new IpPortSet(_proxies);
if (add)
existing.addAll(s);
else
existing.removeAll(s);
_proxies = Collections.unmodifiableSet(existing);
}
public synchronized void overwriteProxies(Set<? extends IpPort> proxies) {
_proxies = Collections.unmodifiableSet(new IpPortSet(proxies));
}
public synchronized Set<IpPort> getProxies() {
return _proxies;
}
public synchronized byte getFeatures() {
return _features;
}
public synchronized int getFWTVersion() {
return _fwtVersion;
}
public synchronized void setFeatures(byte features) {
_features=features;
}
public synchronized void setFWTVersion(int version){
_fwtVersion=version;
}
public synchronized void setIpPort(IpPort addr) {
_externalAddr = addr;
}
public synchronized IpPort getIpPort() {
return _externalAddr;
}
public GUID getGuid() {
return _guidRef.get();
}
public synchronized PushEndpoint createClone() {
return new PushEndpointImpl(guid, getProxies(),
getFeatures(), getFWTVersion(),
getValidExternalAddress(), PushEndpointCacheImpl.this, networkInstanceUtils);
}
public byte[] getClientGUID() {
return guid;
}
public synchronized int getPort() {
IpPort address = _externalAddr;
return address != null ? address.getPort() : 6346;
}
public synchronized IpPort getValidExternalAddress() {
return _externalAddr;
}
public boolean isLocal() {
return false;
}
public synchronized void updateProxies(boolean good) {
if (!good) {
_proxies = Collections.emptySet();
}
}
public synchronized String getAddress() {
IpPort address = _externalAddr;
return address != null ? _externalAddr.getAddress() : RemoteFileDesc.BOGUS_IP;
}
@Override
public String getAddressDescription() {
IpPort addr = getValidExternalAddress();
return addr == null ? null : addr.getAddress();
}
public InetAddress getInetAddress() {
return null;
}
public InetSocketAddress getInetSocketAddress() {
return null;
}
}
}