bt.peerexchange.PeerExchangePeerSourceFactory 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.peerexchange;
import bt.BtException;
import bt.event.EventSource;
import bt.metainfo.TorrentId;
import bt.net.ConnectionKey;
import bt.net.Peer;
import bt.peer.PeerSource;
import bt.peer.PeerSourceFactory;
import bt.protocol.Message;
import bt.protocol.extended.ExtendedHandshake;
import bt.runtime.Config;
import bt.service.IRuntimeLifecycleBinder;
import bt.torrent.annotation.Consumes;
import bt.torrent.annotation.Produces;
import bt.torrent.messaging.MessageContext;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
/**
* Note that this class implements a service.
* Hence, is not a part of the public API and is a subject to change.
*/
public class PeerExchangePeerSourceFactory implements PeerSourceFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(PeerExchangePeerSourceFactory.class);
private static final Duration CLEANER_INTERVAL = Duration.ofSeconds(37);
private Map peerSources;
private Map> peerEvents;
private ReentrantReadWriteLock rwLock;
private Set peers;
private Map lastSentPEXMessage;
private Duration minMessageInterval;
private Duration maxMessageInterval;
private int minEventsPerMessage;
private int maxEventsPerMessage;
@Inject
public PeerExchangePeerSourceFactory(EventSource eventSource,
IRuntimeLifecycleBinder lifecycleBinder,
PeerExchangeConfig pexConfig,
Config config) {
this.peerSources = new ConcurrentHashMap<>();
this.peerEvents = new ConcurrentHashMap<>();
this.rwLock = new ReentrantReadWriteLock();
this.peers = ConcurrentHashMap.newKeySet();
this.lastSentPEXMessage = new ConcurrentHashMap<>();
if (pexConfig.getMaxMessageInterval().compareTo(pexConfig.getMinMessageInterval()) < 0) {
throw new IllegalArgumentException("Max message interval is greater than min interval");
}
this.minMessageInterval = pexConfig.getMinMessageInterval();
this.maxMessageInterval = pexConfig.getMaxMessageInterval();
this.minEventsPerMessage = pexConfig.getMinEventsPerMessage();
this.maxEventsPerMessage = pexConfig.getMaxEventsPerMessage();
eventSource.onPeerConnected(null, e -> onPeerConnected(e.getConnectionKey()))
.onPeerDisconnected(null, e -> onPeerDisconnected(e.getConnectionKey()));
String threadName = String.format("%d.bt.peerexchange.cleaner", config.getAcceptorPort());
ScheduledExecutorService executor =
Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, threadName));
lifecycleBinder.onStartup("Schedule periodic cleanup of PEX messages", () -> executor.scheduleAtFixedRate(
new Cleaner(), CLEANER_INTERVAL.toMillis(), CLEANER_INTERVAL.toMillis(), TimeUnit.MILLISECONDS));
lifecycleBinder.onShutdown("Shutdown PEX cleanup scheduler", executor::shutdownNow);
}
private void onPeerConnected(ConnectionKey connectionKey) {
getPeerEvents(connectionKey.getTorrentId())
.add(PeerEvent.added(connectionKey.getPeer()));
}
private void onPeerDisconnected(ConnectionKey connectionKey) {
getPeerEvents(connectionKey.getTorrentId())
.add(PeerEvent.dropped(connectionKey.getPeer()));
peers.remove(connectionKey);
lastSentPEXMessage.remove(connectionKey);
}
private Queue getPeerEvents(TorrentId torrentId) {
Queue events = peerEvents.get(torrentId);
if (events == null) {
events = new PriorityBlockingQueue<>();
Queue existing = peerEvents.putIfAbsent(torrentId, events);
if (existing != null) {
events = existing;
}
}
return events;
}
@Override
public PeerSource getPeerSource(TorrentId torrentId) {
return getOrCreatePeerSource(torrentId);
}
private PeerExchangePeerSource getOrCreatePeerSource(TorrentId torrentId) {
PeerExchangePeerSource peerSource = peerSources.get(torrentId);
if (peerSource == null) {
peerSource = new PeerExchangePeerSource();
PeerExchangePeerSource existing = peerSources.putIfAbsent(torrentId, peerSource);
if (existing != null) {
peerSource = existing;
}
}
return peerSource;
}
@Consumes
public void consume(ExtendedHandshake handshake, MessageContext messageContext) {
if (handshake.getSupportedMessageTypes().contains("ut_pex")) {
// TODO: peer may eventually turn off the PEX extension
// moreover the extended handshake message type map is additive,
// so we can't learn about the peer turning off extensions solely from the message
peers.add(messageContext.getConnectionKey());
}
}
@Consumes
public void consume(PeerExchange message, MessageContext messageContext) {
getOrCreatePeerSource(messageContext.getTorrentId()).addMessage(message);
}
@Produces
public void produce(Consumer messageConsumer, MessageContext messageContext) {
ConnectionKey connectionKey = messageContext.getConnectionKey();
long currentTime = System.currentTimeMillis();
long lastSentPEXMessageToPeer = lastSentPEXMessage.getOrDefault(connectionKey, 0L);
if (peers.contains(connectionKey) && (currentTime - lastSentPEXMessageToPeer) >= minMessageInterval.toMillis()) {
List events = new ArrayList<>();
rwLock.readLock().lock();
try {
Queue torrentPeerEvents = getPeerEvents(messageContext.getTorrentId());
for (PeerEvent event : torrentPeerEvents) {
if (event.getInstant() - lastSentPEXMessageToPeer >= 0) {
Peer exchangedPeer = event.getPeer();
// don't send PEX message if anything of the above is true:
// - we don't know the listening port of the event's peer yet
// - we don't know the listening port of the current connection's peer yet
// - event's peer and connection's peer are the same
if (!exchangedPeer.isPortUnknown()
&& !connectionKey.getPeer().isPortUnknown()
&& !exchangedPeer.getInetAddress().equals(connectionKey.getPeer().getInetAddress())
&& exchangedPeer.getPort() != connectionKey.getRemotePort()) {
events.add(event);
}
} else {
break;
}
if (events.size() >= maxEventsPerMessage) {
break;
}
}
} finally {
rwLock.readLock().unlock();
}
if (events.size() >= minEventsPerMessage ||
(!events.isEmpty() && (currentTime - lastSentPEXMessageToPeer >= maxMessageInterval.toMillis()))) {
lastSentPEXMessage.put(connectionKey, currentTime);
PeerExchange.Builder messageBuilder = PeerExchange.builder();
events.forEach(event -> {
switch (event.getType()) {
case ADDED: {
messageBuilder.added(event.getPeer());
break;
}
case DROPPED: {
messageBuilder.dropped(event.getPeer());
break;
}
default: {
throw new BtException("Unknown event type: " + event.getType().name());
}
}
});
messageConsumer.accept(messageBuilder.build());
}
}
}
private class Cleaner implements Runnable {
@Override
public void run() {
rwLock.writeLock().lock();
try {
long lruEventTime = lastSentPEXMessage.values().stream()
.reduce(Long.MAX_VALUE, (a, b) -> (a < b) ? a : b);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Prior to cleaning events. LRU event time: {}, peer events: {}", lruEventTime, peerEvents);
}
PeerEvent event;
for (Queue events : peerEvents.values()) {
while ((event = events.peek()) != null && event.getInstant() <= lruEventTime) {
events.poll();
}
}
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("After cleaning events. Peer events: {}", peerEvents);
}
} finally {
rwLock.writeLock().unlock();
}
}
}
}