All Downloads are FREE. Search and download functionalities are using the official Maven repository.

bt.net.PeerConnectionPool Maven / Gradle / Ivy

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.event.EventSink;
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.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 *

Note that this class implements a service. * Hence, is not a part of the public API and is a subject to change.

*/ public class PeerConnectionPool implements IPeerConnectionPool { private static final Logger LOGGER = LoggerFactory.getLogger(PeerConnectionPool.class); private Config config; private EventSink eventSink; private ScheduledExecutorService cleaner; private Connections connections; private ReentrantLock connectionLock; private Duration peerConnectionInactivityThreshold; @Inject public PeerConnectionPool( EventSink eventSink, IRuntimeLifecycleBinder lifecycleBinder, Config config) { this.config = config; this.eventSink = eventSink; this.peerConnectionInactivityThreshold = config.getPeerConnectionInactivityThreshold(); this.connections = new Connections(); this.connectionLock = new ReentrantLock(); String cleanerThreadName = String.format("%d.bt.net.pool.cleaner", config.getAcceptorPort()); this.cleaner = Executors.newScheduledThreadPool(1, r -> new Thread(r, cleanerThreadName)); lifecycleBinder.onStartup("Schedule periodic cleanup of stale peer connections", () -> cleaner.scheduleAtFixedRate(new Cleaner(), 1, 1, TimeUnit.SECONDS)); lifecycleBinder.onShutdown("Shutdown connection pool", this::shutdown); } @Override public PeerConnection getConnection(Peer peer, TorrentId torrentId) { return connections.get(peer, peer.getPort(), torrentId); } @Override public PeerConnection getConnection(ConnectionKey key) { return connections.get(key); } @Override public void visitConnections(TorrentId torrentId, Consumer visitor) { connections.visitConnections(torrentId, visitor); } @Override public int size() { return connections.count(); } @Override public PeerConnection addConnectionIfAbsent(PeerConnection newConnection) { PeerConnection existingConnection = null; ConnectionKey connectionKey = new ConnectionKey(newConnection.getRemotePeer(), newConnection.getRemotePort(), newConnection.getTorrentId()); connectionLock.lock(); try { if (connections.count() >= config.getMaxPeerConnections()) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Closing newly created connection with {} due to exceeding of connections limit", newConnection.getRemotePeer()); } newConnection.closeQuietly(); } else { List connectionsWithSameAddress = getConnectionsForAddress(newConnection.getTorrentId(), newConnection.getRemotePeer()); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Checking duplicate connections for newly established" + " connection with peer: {} (established remote port: {})." + " All connections:\n{}", newConnection.getRemotePeer(), newConnection.getRemotePort(), connectionsWithSameAddress.stream() .map(Object::toString) .collect(Collectors.joining("\n"))); } for (PeerConnection connection : connectionsWithSameAddress) { if (!connection.getRemotePeer().isPortUnknown() && connection.getRemotePeer().getPort() == newConnection.getRemotePeer().getPort()) { existingConnection = connection; break; } } if (existingConnection == null) { if (connections.putIfAbsent(connectionKey, newConnection) != null) { throw new IllegalStateException(); } } } } finally { connectionLock.unlock(); } if (existingConnection != null) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Connection already exists for peer {} (established remote port {})", newConnection.getRemotePeer(), existingConnection.getRemotePort()); } return existingConnection; } else { eventSink.firePeerConnected(connectionKey); return newConnection; } } @Override public void checkDuplicateConnections(TorrentId torrentId, Peer peer) { connectionLock.lock(); try { List connectionsWithSameAddress = getConnectionsForAddress(torrentId, peer); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Checking duplicate connections for newly discovered peer: {}." + " All connections:\n{}", peer, connectionsWithSameAddress.stream() .map(Object::toString) .collect(Collectors.joining("\n"))); } PeerConnection outgoingConnection = null; PeerConnection incomingConnection = null; for (PeerConnection connection : connectionsWithSameAddress) { if (connection.getRemotePort() == peer.getPort()) { outgoingConnection = connection; } else if (connection.getRemotePeer().getPort() == peer.getPort()) { incomingConnection = connection; } if (outgoingConnection != null && incomingConnection != null) { break; } } if (outgoingConnection != null && incomingConnection != null) { // always prefer to keep outgoing connections if (LOGGER.isDebugEnabled()) { LOGGER.debug("Closing duplicate incoming connection with {}:{}" + " (established remote port {})", incomingConnection.getRemotePeer().getInetAddress(), outgoingConnection.getRemotePort(), incomingConnection.getRemotePort()); } incomingConnection.closeQuietly(); } } finally { connectionLock.unlock(); } } private List getConnectionsForAddress(TorrentId torrentId, Peer peer) { List connectionsWithSameAddress = new ArrayList<>(); connections.visitConnections(torrentId, connection -> { if (connection.isClosed()) { return; } Peer connectionPeer = connection.getRemotePeer(); if (connectionPeer.getInetAddress().equals(peer.getInetAddress())) { connectionsWithSameAddress.add(connection); } }); return connectionsWithSameAddress; } private class Cleaner implements Runnable { @Override public void run() { if (connections.count() == 0) { return; } connectionLock.lock(); try { connections.visitConnections(connection -> { if (connection.isClosed()) { purgeConnection(connection); } else if (System.currentTimeMillis() - connection.getLastActive() >= peerConnectionInactivityThreshold.toMillis()) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Removing inactive peer connection: {}", connection.getRemotePeer()); } purgeConnection(connection); } // can send keep-alives here based on lastActiveTime }); } finally { connectionLock.unlock(); } } } private void purgeConnection(PeerConnection connection) { ConnectionKey connectionKey = new ConnectionKey(connection.getRemotePeer(), connection.getRemotePort(), connection.getTorrentId()); connections.remove(connectionKey, connection); connection.closeQuietly(); eventSink.firePeerDisconnected(connectionKey); } private void shutdown() { shutdownCleaner(); connections.visitConnections(PeerConnection::closeQuietly); } private void shutdownCleaner() { cleaner.shutdown(); try { cleaner.awaitTermination(1, TimeUnit.SECONDS); } catch (InterruptedException e) { LOGGER.warn("Interrupted while waiting for the cleaner's shutdown"); } if (!cleaner.isShutdown()) { cleaner.shutdownNow(); } } } class Connections { private ConcurrentMap connections; private ConcurrentMap> connectionsByTorrent; Connections() { this.connections = new ConcurrentHashMap<>(); this.connectionsByTorrent = new ConcurrentHashMap<>(); } int count() { return connections.size(); } synchronized boolean remove(ConnectionKey key, PeerConnection connection) { Objects.requireNonNull(connection); PeerConnection removed = connections.remove(key); boolean success = (removed == connection); if (success) { Collection torrentConnections = connectionsByTorrent.get(key.getTorrentId()); torrentConnections.remove(removed); if (torrentConnections.isEmpty()) { connectionsByTorrent.remove(key.getTorrentId()); } } return success; } synchronized PeerConnection putIfAbsent(ConnectionKey key, PeerConnection connection) { Objects.requireNonNull(connection); PeerConnection existing = connections.putIfAbsent(key, connection); if (existing == null) { connectionsByTorrent.computeIfAbsent(key.getTorrentId(), id -> ConcurrentHashMap.newKeySet()) .add(connection); } return existing; } PeerConnection get(Peer peer, int remotePort, TorrentId torrentId) { return get(new ConnectionKey(peer, remotePort, torrentId)); } PeerConnection get(ConnectionKey key) { return connections.get(key); } void visitConnections(Consumer visitor) { connections.values().forEach(visitor::accept); } void visitConnections(TorrentId torrentId, Consumer visitor) { Collection connections = connectionsByTorrent.get(torrentId); if (connections != null) { connections.forEach(visitor::accept); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy