
bt.peer.lan.LocalServiceDiscoveryService 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)
/*
* Copyright (c) 2016—2017 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.peer.lan;
import bt.event.Event;
import bt.event.EventSource;
import bt.event.TorrentStartedEvent;
import bt.event.TorrentStoppedEvent;
import bt.metainfo.TorrentId;
import bt.service.IRuntimeLifecycleBinder;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
public class LocalServiceDiscoveryService implements ILocalServiceDiscoveryService {
private static final Logger LOGGER = LoggerFactory.getLogger(LocalServiceDiscoveryService.class);
private final IRuntimeLifecycleBinder lifecycleBinder;
private final LocalServiceDiscoveryConfig config;
private final LinkedHashSet announceQueue;
private final BlockingQueue events;
private final Collection announcers;
private final AtomicBoolean scheduled; // true, if periodic announce has been scheduled
@Inject
public LocalServiceDiscoveryService(Cookie cookie,
ILocalServiceDiscoveryInfo info,
Collection groupChannels,
EventSource eventSource,
IRuntimeLifecycleBinder lifecycleBinder,
LocalServiceDiscoveryConfig config) {
this.lifecycleBinder = lifecycleBinder;
this.config = config;
this.announceQueue = new LinkedHashSet<>();
this.events = new LinkedBlockingQueue<>();
this.announcers = createAnnouncers(groupChannels, cookie, info.getLocalPorts());
this.scheduled = new AtomicBoolean(false);
// do not enable LSD if there are no groups to announce to
if (groupChannels.size() > 0) {
eventSource.onTorrentStarted(this::onTorrentStarted);
eventSource.onTorrentStopped(this::onTorrentStopped);
}
}
private Collection createAnnouncers(
Collection groupChannels,
Cookie cookie,
Set localPorts) {
return groupChannels.stream()
.map(channel -> new LocalServiceDiscoveryAnnouncer(channel, cookie, localPorts, config))
.collect(Collectors.toList());
}
private void schedulePeriodicAnnounce() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "lsd-announcer"));
long intervalMillis = config.getLocalServiceDiscoveryAnnounceInterval().toMillis();
executor.scheduleWithFixedDelay(this::announce, 0, intervalMillis, TimeUnit.MILLISECONDS);
lifecycleBinder.onShutdown(executor::shutdownNow);
}
// TODO: using synchronized for now, because this method is available from the public API
// (however, it's unlikely to be called from anywhere other than tests)
@Override
public synchronized void announce() {
if (announceQueue.isEmpty() && events.isEmpty()) {
return;
}
try {
Map statusChanges = foldStartStopEvents(events);
Collection idsToAnnounce = collectNextTorrents(statusChanges);
if (idsToAnnounce.size() > 0) {
announce(idsToAnnounce);
}
} catch (Exception e) {
LOGGER.error("Failed to announce", e);
}
}
private enum StatusChange {
STARTED, STOPPED
}
/**
* Folds started/stopped events into a map of status changes
*/
private Map foldStartStopEvents(BlockingQueue events) {
int k = events.size(); // decide on the number of events to process upfront
Map statusChanges = new HashMap<>(k * 2);
Event event;
while (--k >= 0 && (event = events.poll()) != null) {
if (event instanceof TorrentStartedEvent) {
statusChanges.put(((TorrentStartedEvent) event).getTorrentId(), StatusChange.STARTED);
} else if (event instanceof TorrentStoppedEvent) {
statusChanges.put(((TorrentStoppedEvent) event).getTorrentId(), StatusChange.STOPPED);
} else {
LOGGER.warn("Unexpected event type: " + event.getClass().getName() + ". Skipping...");
}
}
return statusChanges;
}
/**
* Collect next few IDs to announce and additionally remove all inactive IDs from the queue.
*/
private Collection collectNextTorrents(Map statusChanges) {
int k = config.getLocalServiceDiscoveryMaxTorrentsPerAnnounce();
List ids = new ArrayList<>(k * 2);
Iterator iter = announceQueue.iterator();
while (iter.hasNext()) {
TorrentId id = iter.next();
StatusChange statusChange = statusChanges.get(id);
if (statusChange == null) {
if (ids.size() < k) {
iter.remove(); // temporary remove from the queue
ids.add(id);
announceQueue.add(id); // add to the end of the queue
}
} else if (statusChange == StatusChange.STOPPED) {
// remove inactive
iter.remove();
}
// last case is that the torrent has been stopped and started in between the announces,
// which means that we should leave it in the announce queue
}
// add all new started torrents to the announce queue
statusChanges.forEach((id, statusChange) -> {
if (statusChange == StatusChange.STARTED) {
announceQueue.add(id);
if (ids.size() < k) {
ids.add(id);
}
}
});
return ids;
}
private void announce(Collection ids) {
// TODO: announce in parallel?
announcers.forEach(a -> {
try {
a.announce(ids);
} catch (IOException e) {
LOGGER.error("Failed to announce to group: " + a.getGroup().getAddress(), e);
}
});
}
private void onTorrentStarted(TorrentStartedEvent event) {
events.add(event);
if (scheduled.compareAndSet(false, true)) {
// schedule periodic announce immediately after the first torrent has been started
// TODO: immediately announce each time a torrent is started (but no more than 1 announce per minute)
schedulePeriodicAnnounce();
}
}
private void onTorrentStopped(TorrentStoppedEvent event) {
events.add(event);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy