stop torrent when target ratio is reached

This commit is contained in:
Anthony RAYMOND 2023-10-24 23:13:01 +02:00
parent 04c10b1c99
commit dd94b478e2
14 changed files with 79 additions and 38 deletions

View file

@ -132,7 +132,7 @@ public class SeedManager {
.withAppConfiguration(appConfig)
.withTorrentFileProvider(this.torrentFileProvider)
.withBandwidthDispatcher(this.bandwidthDispatcher)
.withAnnouncerFactory(new AnnouncerFactory(announceDataAccessor, httpClient))
.withAnnouncerFactory(new AnnouncerFactory(announceDataAccessor, httpClient, appConfig))
.withEventPublisher(this.appEventPublisher)
.withDelayQueue(new DelayQueue<>())
.build();

View file

@ -20,6 +20,7 @@ public class AppConfiguration {
private final int simultaneousSeed;
private final String client;
private final boolean keepTorrentWithZeroLeechers;
private final float uploadRatioTarget;
@JsonCreator
public AppConfiguration(
@ -27,13 +28,15 @@ public class AppConfiguration {
@JsonProperty(value = "maxUploadRate", required = true) final long maxUploadRate,
@JsonProperty(value = "simultaneousSeed", required = true) final int simultaneousSeed,
@JsonProperty(value = "client", required = true) final String client,
@JsonProperty(value = "keepTorrentWithZeroLeechers", required = true) final boolean keepTorrentWithZeroLeechers
@JsonProperty(value = "keepTorrentWithZeroLeechers", required = true) final boolean keepTorrentWithZeroLeechers,
@JsonProperty(value = "uploadRatioTarget", defaultValue = "-1.0", required = false) final float uploadRatioTarget
) {
this.minUploadRate = minUploadRate;
this.maxUploadRate = maxUploadRate;
this.simultaneousSeed = simultaneousSeed;
this.client = client;
this.keepTorrentWithZeroLeechers = keepTorrentWithZeroLeechers;
this.uploadRatioTarget = uploadRatioTarget;
validate();
}
@ -56,5 +59,9 @@ public class AppConfiguration {
if (StringUtils.isBlank(client)) {
throw new AppConfigurationIntegrityException("client is required, no file name given");
}
if (uploadRatioTarget < 0f && uploadRatioTarget != -1f){
throw new AppConfigurationIntegrityException("uploadRatioTarget must be greater than 0 (or equal to -1)");
}
}
}

View file

@ -181,6 +181,10 @@ public class Client implements TorrentFileChangeAware, ClientFacade {
}
}
public void onUploadRatioLimitReached(final InfoHash infoHash) {
this.torrentFileProvider.moveToArchiveFolder(infoHash);
}
public void onTorrentHasStopped(final Announcer stoppedAnnouncer) {
if (this.stop) {
this.currentlySeedingAnnouncers.remove(stoppedAnnouncer);
@ -242,4 +246,5 @@ public class Client implements TorrentFileChangeAware, ClientFacade {
lock.unlock();
}
}
}

View file

@ -35,11 +35,14 @@ public class Announcer implements AnnouncerFacade {
@Getter private final MockedTorrent torrent;
private TrackerClient trackerClient;
private final AnnounceDataAccessor announceDataAccessor;
private long reportedUploadBytes = 0L;
private final float uploadRatioTarget;
Announcer(final MockedTorrent torrent, final AnnounceDataAccessor announceDataAccessor, final HttpClient httpClient) {
Announcer(final MockedTorrent torrent, final AnnounceDataAccessor announceDataAccessor, final HttpClient httpClient, final float uploadRatioTarget) {
this.torrent = torrent;
this.trackerClient = this.buildTrackerClient(torrent, httpClient);
this.announceDataAccessor = announceDataAccessor;
this.uploadRatioTarget = uploadRatioTarget;
}
private TrackerClient buildTrackerClient(final MockedTorrent torrent, HttpClient httpClient) {
@ -67,6 +70,7 @@ public class Announcer implements AnnouncerFacade {
log.info("{} has announced successfully. Response: {} seeders, {} leechers, {}s interval",
this.torrent.getTorrentInfoHash().getHumanReadable(), responseMessage.getSeeders(), responseMessage.getLeechers(), responseMessage.getInterval());
this.reportedUploadBytes = announceDataAccessor.getUploaded(this.torrent.getTorrentInfoHash());
this.lastKnownInterval = responseMessage.getInterval();
this.lastKnownLeechers = responseMessage.getLeechers();
this.lastKnownSeeders = responseMessage.getSeeders();
@ -116,6 +120,14 @@ public class Announcer implements AnnouncerFacade {
return this.getTorrent().getTorrentInfoHash();
}
public boolean hasReachedUploadRatioLimit() {
if (uploadRatioTarget == -1f) {
return false;
}
final float bytesToUploadTarget = (uploadRatioTarget * (float) this.getTorrentSize());
return reportedUploadBytes >= bytesToUploadTarget;
}
/**
* Make sure to keep {@code torrentInfoHash} as the only input.
*/

View file

@ -2,6 +2,7 @@ package org.araymond.joal.core.ttorrent.client.announcer;
import lombok.RequiredArgsConstructor;
import org.apache.http.client.HttpClient;
import org.araymond.joal.core.config.AppConfiguration;
import org.araymond.joal.core.torrent.torrent.MockedTorrent;
import org.araymond.joal.core.ttorrent.client.announcer.request.AnnounceDataAccessor;
@ -9,8 +10,9 @@ import org.araymond.joal.core.ttorrent.client.announcer.request.AnnounceDataAcce
public class AnnouncerFactory {
private final AnnounceDataAccessor announceDataAccessor;
private final HttpClient httpClient;
private final AppConfiguration appConfiguration;
public Announcer create(final MockedTorrent torrent) {
return new Announcer(torrent, this.announceDataAccessor, httpClient);
return new Announcer(torrent, this.announceDataAccessor, httpClient, appConfiguration.getUploadRatioTarget());
}
}

View file

@ -24,4 +24,8 @@ public class AnnounceDataAccessor {
public Set<Map.Entry<String, String>> getHttpHeadersForTorrent() {
return this.bitTorrentClient.getHeaders();
}
public long getUploaded(final InfoHash infoHash) {
return this.bandwidthDispatcher.getSeedStatForTorrent(infoHash).getUploaded();
}
}

View file

@ -34,6 +34,10 @@ public class ClientNotifier implements AnnounceResponseHandler {
public void onAnnounceRegularSuccess(final Announcer announcer, final SuccessAnnounceResponse result) {
if (result.getSeeders() < 1 || result.getLeechers() < 1) {
this.client.onNoMorePeers(announcer.getTorrentInfoHash());
return;
}
if (announcer.hasReachedUploadRatioLimit()) {
this.client.onUploadRatioLimitReached(announcer.getTorrentInfoHash());
}
}

View file

@ -18,6 +18,7 @@ public class ConfigIncomingMessage {
private final Integer simultaneousSeed;
private final String client;
private final boolean keepTorrentWithZeroLeechers;
private final Float uploadRatioTarget;
@JsonCreator
ConfigIncomingMessage(
@ -25,16 +26,18 @@ public class ConfigIncomingMessage {
@JsonProperty(value = "maxUploadRate", required = true) final Long maxUploadRate,
@JsonProperty(value = "simultaneousSeed", required = true) final Integer simultaneousSeed,
@JsonProperty(value = "client", required = true) final String client,
@JsonProperty(value = "keepTorrentWithZeroLeechers", required = true) final boolean keepTorrentWithZeroLeechers
@JsonProperty(value = "keepTorrentWithZeroLeechers", required = true) final boolean keepTorrentWithZeroLeechers,
@JsonProperty(value = "uploadRatioTarget", defaultValue = "-1.0", required = false) final Float uploadRatioTarget
) {
this.minUploadRate = minUploadRate;
this.maxUploadRate = maxUploadRate;
this.simultaneousSeed = simultaneousSeed;
this.client = client;
this.keepTorrentWithZeroLeechers = keepTorrentWithZeroLeechers;
this.uploadRatioTarget = uploadRatioTarget;
}
public AppConfiguration toAppConfiguration() throws AppConfigurationIntegrityException {
return new AppConfiguration(this.minUploadRate, this.maxUploadRate, this.simultaneousSeed, this.client, keepTorrentWithZeroLeechers);
return new AppConfiguration(this.minUploadRate, this.maxUploadRate, this.simultaneousSeed, this.client, keepTorrentWithZeroLeechers, this.uploadRatioTarget);
}
}

View file

@ -54,8 +54,8 @@ public class AppConfigurationSerializationTest {
@Test
public void shouldSerialize() throws JsonProcessingException {
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false);
assertThat(mapper.writeValueAsString(config)).isEqualTo("{\"minUploadRate\":180,\"maxUploadRate\":190,\"simultaneousSeed\":2,\"client\":\"azureus.client\",\"keepTorrentWithZeroLeechers\":false}");
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false, 1f);
assertThat(mapper.writeValueAsString(config)).isEqualTo("{\"minUploadRate\":180,\"maxUploadRate\":190,\"simultaneousSeed\":2,\"client\":\"azureus.client\",\"keepTorrentWithZeroLeechers\":false,\"uploadRatioTarget\":1.0}");
}
@Test

View file

@ -11,40 +11,40 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class AppConfigurationTest {
public static AppConfiguration createOne() {
return new AppConfiguration(30L, 150L, 2, "azureus", true);
return new AppConfiguration(30L, 150L, 2, "azureus", true, 1f);
}
@Test
public void shouldNotBuildIfMinUploadRateIsLessThanZero() {
assertThatThrownBy(() -> new AppConfiguration(-1L, 190L, 2, "azureus.client", false))
assertThatThrownBy(() -> new AppConfiguration(-1L, 190L, 2, "azureus.client", false, 1f))
.isInstanceOf(AppConfigurationIntegrityException.class)
.hasMessageContaining("minUploadRate must be at least 0");
}
@Test
public void shouldBuildIfMinUploadRateEqualsZero() {
final AppConfiguration config = new AppConfiguration(0L, 190L, 2, "azureus.client", false);
final AppConfiguration config = new AppConfiguration(0L, 190L, 2, "azureus.client", false, 1f);
assertThat(config.getMinUploadRate()).isEqualTo(0);
}
@Test
public void shouldBuildIfMinUploadRateEqualsOne() {
final AppConfiguration config = new AppConfiguration(0L, 1L, 2, "azureus.client", false);
final AppConfiguration config = new AppConfiguration(0L, 1L, 2, "azureus.client", false, 1f);
assertThat(config.getMaxUploadRate()).isEqualTo(1);
}
@Test
public void shouldNotBuildIfMaxUploadRateIsLessThanZero() {
assertThatThrownBy(() -> new AppConfiguration(180L, -1L, 2, "azureus.client", false))
assertThatThrownBy(() -> new AppConfiguration(180L, -1L, 2, "azureus.client", false, 1f))
.isInstanceOf(AppConfigurationIntegrityException.class)
.hasMessageContaining("maxUploadRate must greater or equal to 0");
}
@Test
public void shouldBuildIfMinRateAndMaxRateEqualsZero() {
final AppConfiguration conf = new AppConfiguration(0L, 0L, 2, "azureus.client", false);
final AppConfiguration conf = new AppConfiguration(0L, 0L, 2, "azureus.client", false, 1f);
assertThat(conf.getMinUploadRate()).isEqualTo(0L);
assertThat(conf.getMaxUploadRate()).isEqualTo(0L);
@ -52,14 +52,14 @@ public class AppConfigurationTest {
@Test
public void shouldNotBuildIfMaxRateIsLesserThanMinRate() {
assertThatThrownBy(() -> new AppConfiguration(180L, 179L, 2, "azureus.client", false))
assertThatThrownBy(() -> new AppConfiguration(180L, 179L, 2, "azureus.client", false, 1f))
.isInstanceOf(AppConfigurationIntegrityException.class)
.hasMessageContaining("maxUploadRate must be greater or equal to minUploadRate");
}
@Test
public void shouldBuildIfMaxRateEqualsMinRate() {
final AppConfiguration conf = new AppConfiguration(180L, 180L, 2, "azureus.client", false);
final AppConfiguration conf = new AppConfiguration(180L, 180L, 2, "azureus.client", false, 1f);
assertThat(conf.getMinUploadRate()).isEqualTo(180L);
assertThat(conf.getMaxUploadRate()).isEqualTo(180L);
@ -67,35 +67,35 @@ public class AppConfigurationTest {
@Test
public void shouldNotBuildIfSimultaneousSeedIsLessThanOne() {
assertThatThrownBy(() -> new AppConfiguration(180L, 190L, 0, "azureus.client", false))
assertThatThrownBy(() -> new AppConfiguration(180L, 190L, 0, "azureus.client", false, 1f))
.isInstanceOf(AppConfigurationIntegrityException.class)
.hasMessageContaining("simultaneousSeed must be greater than 0");
}
@Test
public void shouldCreateIfSimultaneousSeedIsOne() {
final AppConfiguration config = new AppConfiguration(180L, 190L, 1, "azureus.client", false);
final AppConfiguration config = new AppConfiguration(180L, 190L, 1, "azureus.client", false, 1f);
assertThat(config.getSimultaneousSeed()).isEqualTo(1);
}
@Test
public void shouldNotBuildIfClientIsNull() {
assertThatThrownBy(() -> new AppConfiguration(180L, 190L, 2, null, false))
assertThatThrownBy(() -> new AppConfiguration(180L, 190L, 2, null, false, 1f))
.isInstanceOf(AppConfigurationIntegrityException.class)
.hasMessageContaining("client is required, no file name given");
}
@Test
public void shouldNotBuildIfClientIsEmpty() {
assertThatThrownBy(() -> new AppConfiguration(180L, 190L, 2, " ", false))
assertThatThrownBy(() -> new AppConfiguration(180L, 190L, 2, " ", false, 1f))
.isInstanceOf(AppConfigurationIntegrityException.class)
.hasMessageContaining("client is required, no file name given");
}
@Test
public void shouldBuild() {
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false);
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false, 1f);
assertThat(config.getMinUploadRate()).isEqualTo(180);
assertThat(config.getMaxUploadRate()).isEqualTo(190);
@ -105,15 +105,15 @@ public class AppConfigurationTest {
@Test
public void shouldBeEqualsByProperties() {
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false);
final AppConfiguration config2 = new AppConfiguration(180L, 190L, 2, "azureus.client", false);
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false, 1f);
final AppConfiguration config2 = new AppConfiguration(180L, 190L, 2, "azureus.client", false, 1f);
assertThat(config).isEqualTo(config2);
}
@Test
public void shouldHaveSameHashCodeWithSameProperties() {
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false);
final AppConfiguration config2 = new AppConfiguration(180L, 190L, 2, "azureus.client", false);
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false, 1f);
final AppConfiguration config2 = new AppConfiguration(180L, 190L, 2, "azureus.client", false, 1f);
assertThat(config.hashCode()).isEqualTo(config2.hashCode());
}

View file

@ -32,7 +32,8 @@ public class JoalConfigProviderTest {
190L,
5,
"azureus-5.7.5.0.client",
false
false,
1f
);
@Test
@ -108,7 +109,8 @@ public class JoalConfigProviderTest {
rand.longs(201, 400).findFirst().getAsLong(),
rand.ints(1, 5).findFirst().getAsInt(),
RandomStringUtils.random(60),
false
false,
1f
);
provider.saveNewConf(newConf);

View file

@ -1,6 +1,7 @@
package org.araymond.joal.core.ttorrent.client.announcer;
import org.apache.http.client.HttpClient;
import org.araymond.joal.core.config.AppConfiguration;
import org.araymond.joal.core.torrent.torrent.MockedTorrent;
import org.araymond.joal.core.ttorrent.client.announcer.request.AnnounceDataAccessor;
import org.araymond.joal.core.ttorrent.client.announcer.tracker.NoMoreUriAvailableException;
@ -20,7 +21,7 @@ public class AnnouncerFactoryTest {
@Test
public void shouldCreate() {
final AnnounceDataAccessor announceDataAccessor = mock(AnnounceDataAccessor.class);
final AnnouncerFactory announcerFactory = new AnnouncerFactory(announceDataAccessor, Mockito.mock(HttpClient.class));
final AnnouncerFactory announcerFactory = new AnnouncerFactory(announceDataAccessor, Mockito.mock(HttpClient.class), mock(AppConfiguration.class));
final MockedTorrent torrent = mock(MockedTorrent.class);
given(torrent.getAnnounceList()).willReturn(list(list(URI.create("http://localhost"))));

View file

@ -28,7 +28,7 @@ public class AnnouncerTest {
@Test
public void shouldProvideRequiredInfoForAnnouncerFacade() {
final MockedTorrent torrent = MockedTorrentTest.createOneMock();
final AnnouncerFacade facade = new Announcer(torrent, null, Mockito.mock(HttpClient.class));
final AnnouncerFacade facade = new Announcer(torrent, null, Mockito.mock(HttpClient.class), 1f);
assertThat(facade.getConsecutiveFails()).isEqualTo(0);
assertThat(facade.getLastKnownInterval()).isEqualTo(5);
@ -47,7 +47,7 @@ public class AnnouncerTest {
doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class));
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class), 1f);
announcer.setTrackerClient(trackerClient);
//noinspection Duplicates
@ -80,7 +80,7 @@ public class AnnouncerTest {
doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class));
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class), 1f);
announcer.setTrackerClient(trackerClient);
//noinspection Duplicates
@ -116,7 +116,7 @@ public class AnnouncerTest {
doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class));
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class), 1f);
announcer.setTrackerClient(trackerClient);
assertThat(announcer.getLastAnnouncedAt()).isEmpty();
@ -155,7 +155,7 @@ public class AnnouncerTest {
doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class));
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class), 1f);
announcer.setTrackerClient(trackerClient);
assertThat(announcer.getLastKnownInterval()).isEqualTo(5);
@ -194,11 +194,11 @@ public class AnnouncerTest {
@Test
public void shouldBeEqualsByInfoHash() {
final MockedTorrent torrent1 = MockedTorrentTest.createOneMock("abcd");
final Announcer announcer1 = new Announcer(torrent1, null, Mockito.mock(HttpClient.class));
final Announcer announcer1 = new Announcer(torrent1, null, Mockito.mock(HttpClient.class), 1f);
final MockedTorrent torrent2 = MockedTorrentTest.createOneMock("abcd");
final Announcer announcer2 = new Announcer(torrent2, null, Mockito.mock(HttpClient.class));
final Announcer announcer2 = new Announcer(torrent2, null, Mockito.mock(HttpClient.class), 1f);
assertThat(announcer1).isEqualTo(announcer2);
}
@ -207,11 +207,11 @@ public class AnnouncerTest {
@Test
public void shouldNotBeEqualsWithDifferentInfoHash() {
final MockedTorrent torrent1 = MockedTorrentTest.createOneMock("abcd");
final Announcer announcer1 = new Announcer(torrent1, null, Mockito.mock(HttpClient.class));
final Announcer announcer1 = new Announcer(torrent1, null, Mockito.mock(HttpClient.class), 1f);
final MockedTorrent torrent2 = MockedTorrentTest.createOneMock("abcdefgh");
final Announcer announcer2 = new Announcer(torrent2, null, Mockito.mock(HttpClient.class));
final Announcer announcer2 = new Announcer(torrent2, null, Mockito.mock(HttpClient.class), 1f);
assertThat(announcer1).isNotEqualTo(announcer2);
}

View file

@ -3,5 +3,6 @@
"maxUploadRate": 190,
"simultaneousSeed": 5,
"client": "azureus-5.7.5.0.client",
"keepTorrentWithZeroLeechers": false
"keepTorrentWithZeroLeechers": false,
"uploadRatioTarget": 1.0
}