bt.net.ConnectionSource Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of bt-core Show documentation
Show all versions of bt-core Show documentation
BitTorrent Client Library (Core)
The newest version!
/*
* Copyright (c) 2016—2021 Andrei Tomashpolskiy and individual contributors.
*
* 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 bt.net;
import bt.CountingThreadFactory;
import bt.metainfo.TorrentId;
import bt.runtime.Config;
import bt.service.IRuntimeLifecycleBinder;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConnectionSource implements IConnectionSource {
private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionSource.class);
private final IPeerConnectionFactory connectionFactory;
private final IPeerConnectionPool connectionPool;
private final ExecutorService connectionExecutor;
private final Config config;
private final Object lock = new Object();
private final ConcurrentMap> pendingConnections;
// TODO: weak map
private final ConcurrentMap unreachablePeers;
@Inject
public ConnectionSource(Set connectionAcceptors,
IPeerConnectionFactory connectionFactory,
IPeerConnectionPool connectionPool,
IRuntimeLifecycleBinder lifecycleBinder,
Config config) {
this.connectionFactory = connectionFactory;
this.connectionPool = connectionPool;
this.config = config;
String threadName = String.format("%d.bt.net.pool.connection-worker", config.getAcceptorPort());
this.connectionExecutor = Executors.newFixedThreadPool(
config.getMaxPendingConnectionRequests(),
CountingThreadFactory.daemonFactory(threadName));
lifecycleBinder.onShutdown("Shutdown connection workers", connectionExecutor::shutdownNow);
this.pendingConnections = new ConcurrentHashMap<>();
this.unreachablePeers = new ConcurrentHashMap<>();
IncomingConnectionListener incomingListener =
new IncomingConnectionListener(connectionAcceptors, connectionExecutor, connectionPool, config);
lifecycleBinder.onStartup("Initialize incoming connection acceptors", incomingListener::startup);
lifecycleBinder.onShutdown("Shutdown incoming connection acceptors", incomingListener::shutdown);
}
@Override
public ConnectionResult getConnection(Peer peer, TorrentId torrentId) {
try {
return getConnectionAsync(peer, torrentId).get();
} catch (InterruptedException e) {
return ConnectionResult.failure("Interrupted while waiting for connection", e);
} catch (ExecutionException e) {
return ConnectionResult.failure("Failed to establish connection due to error", e);
}
}
@Override
public CompletableFuture getConnectionAsync(Peer peer, TorrentId torrentId) {
ConnectionKey key = new ConnectionKey(peer, peer.getPort(), torrentId);
CompletableFuture result = validateNewConnPossible(peer, torrentId, key);
if (result != null) {
return result;
}
Long bannedAt = unreachablePeers.get(peer);
if (bannedAt != null) {
if (System.currentTimeMillis() - bannedAt >= config.getUnreachablePeerBanDuration().toMillis()) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Removing temporary ban for unreachable peer: {}", peer);
}
unreachablePeers.remove(peer);
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Will not attempt to establish connection to peer: {}. " +
"Reason: peer is unreachable. Torrent: {}", peer, torrentId);
}
return CompletableFuture.completedFuture(ConnectionResult.failure("Peer is unreachable"));
}
}
synchronized (lock) {
// synchronized double checking.
result = validateNewConnPossible(peer, torrentId, key);
if (result != null) {
return result;
} else {
CompletableFuture addedToPendingConnections = new CompletableFuture<>();
try {
result = createPendingConnFuture(peer, torrentId, key, addedToPendingConnections);
pendingConnections.put(key, result);
return result;
} finally {
// If an exception happens, make sure that a thread isn't deadlocked waiting for this to complete
addedToPendingConnections.complete(null);
}
}
}
}
private CompletableFuture createPendingConnFuture(Peer peer, TorrentId torrentId,
ConnectionKey key,
CompletableFuture addedToPendingConnections) {
return CompletableFuture.supplyAsync(() -> {
try {
ConnectionResult connectionResult =
connectionFactory.createOutgoingConnection(peer, torrentId);
if (connectionResult.isSuccess()) {
PeerConnection established = connectionResult.getConnection();
PeerConnection added = connectionPool.addConnectionIfAbsent(established);
if (added != established) {
established.closeQuietly();
}
return ConnectionResult.success(added);
} else {
return connectionResult;
}
} finally {
// ensure that we don't remove this key from the map before it is added.
addedToPendingConnections.join();
// The synchronize ensures a memory barrier that ensures the effects of connectionPool.addConnectionIfAbsent(established)
// are visible to any other thread that sees the removal.
// Unfortunately ConcurrentMap.remove() does not guarantee a happens before relationship. See:
// https://stackoverflow.com/questions/39341742/does-concurrentmap-remove-provide-a-happens-before-edge-before-get-returns-n
// When Java 11 features are enabled, this synchronize can be replaced with VarHandle.storeStoreFence().
synchronized (pendingConnections) {
pendingConnections.remove(key);
}
}
}, connectionExecutor).whenComplete((acquiredConnection, throwable) -> {
if (acquiredConnection == null || throwable != null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Peer is unreachable: {}. Will prevent further attempts to establish connection.",
peer);
}
unreachablePeers.putIfAbsent(peer, System.currentTimeMillis());
}
if (throwable != null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Failed to establish outgoing connection to peer: " + peer, throwable);
}
}
});
}
/**
* Checks whether a connection to this peer on the specified torrent id is possible. Returns a result if a new
* connection is not possible. This can happen if there is an existing pending connection, or we have reached
* {@link Config#getMaxPeerConnections()}
*
* @param peer the peer to connect to
* @param torrentId the torrent for the connection
* @param key the connection key
* @return a result if a new connection is not possible, null otherwise
*/
private CompletableFuture validateNewConnPossible(Peer peer, TorrentId torrentId,
ConnectionKey key) {
CompletableFuture connection = getExistingOrPendingConnection(key);
if (connection != null) {
if (connection.isDone() && LOGGER.isDebugEnabled()) {
LOGGER.debug("Returning existing connection for peer: {}. Torrent: {}", peer, torrentId);
}
return connection;
}
if (checkPeerConnectionsLimit(peer, torrentId)) {
return CompletableFuture.completedFuture(ConnectionResult.failure("Connections limit exceeded"));
}
return null;
}
private boolean checkPeerConnectionsLimit(Peer peer, TorrentId torrentId) {
if (connectionPool.size() >= config.getMaxPeerConnections()) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Will not attempt to establish connection to peer: {}. " +
"Reason: connections limit exceeded. Torrent: {}", peer, torrentId);
}
return true;
}
return false;
}
private CompletableFuture getExistingOrPendingConnection(ConnectionKey key) {
// When Java 11 features are enabled, this synchronize can be replaced with VarHandle.loadLoadFence() below the
// end of the synchronized block.
// See comment in createPendingConnFuture()
synchronized (pendingConnections) {
CompletableFuture pendingConnection = pendingConnections.get(key);
if (pendingConnection != null) {
return pendingConnection;
}
}
PeerConnection existingConnection = connectionPool.getConnection(key);
if (existingConnection != null) {
return CompletableFuture.completedFuture(ConnectionResult.success(existingConnection));
}
return null;
}
}