Skip to content

Commit

Permalink
stop torrent when target ratio is reached
Browse files Browse the repository at this point in the history
  • Loading branch information
anthonyraymond committed Oct 24, 2023
1 parent 31ebcb7 commit 49814ac
Show file tree
Hide file tree
Showing 14 changed files with 79 additions and 38 deletions.
2 changes: 1 addition & 1 deletion src/main/java/org/araymond/joal/core/SeedManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public void startSeeding() throws IOException {
.withAppConfiguration(appConfiguration)
.withTorrentFileProvider(this.torrentFileProvider)
.withBandwidthDispatcher(this.bandwidthDispatcher)
.withAnnouncerFactory(new AnnouncerFactory(announceDataAccessor, httpClient))
.withAnnouncerFactory(new AnnouncerFactory(announceDataAccessor, httpClient, appConfiguration))
.withEventPublisher(this.publisher)
.withDelayQueue(new DelayQueue<>())
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,23 @@ public class AppConfiguration {
private final String client;
@JsonProperty("keepTorrentWithZeroLeechers")
private final boolean keepTorrentWithZeroLeechers;
private final float uploadRatioTarget;

@JsonCreator
public AppConfiguration(
@JsonProperty(value = "minUploadRate", required = true) final Long minUploadRate,
@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;

validate();
}
Expand Down Expand Up @@ -64,5 +67,9 @@ private void validate() {
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)");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ public void onNoMorePeers(final InfoHash infoHash) {
}
}

public void onUploadRatioLimitReached(final InfoHash infoHash) {
this.torrentFileProvider.moveToArchiveFolder(infoHash);
}

public void onTorrentHasStopped(final Announcer stoppedAnnouncer) {
if (this.stop) {
this.currentlySeedingAnnouncer.remove(stoppedAnnouncer);
Expand Down Expand Up @@ -218,4 +222,5 @@ public List<AnnouncerFacade> getCurrentlySeedingAnnouncer() {
this.lock.readLock().unlock();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@ public class Announcer implements AnnouncerFacade {
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) {
Expand Down Expand Up @@ -71,6 +74,7 @@ public SuccessAnnounceResponse announce(final RequestEvent event) throws Announc
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();
Expand Down Expand Up @@ -123,6 +127,14 @@ public InfoHash getTorrentInfoHash() {
return this.getTorrent().getTorrentInfoHash();
}

public boolean hasReachedUploadRatioLimit() {
if (uploadRatioTarget == -1f) {
return false;
}
final float bytesToUploadTarget = (uploadRatioTarget * (float) this.getTorrentSize());
return reportedUploadBytes >= bytesToUploadTarget;
}

@Override
public boolean equals(final Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@

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;

@RequiredArgsConstructor
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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ public String getHttpRequestQueryForTorrent(final InfoHash infoHash, final Reque
public List<Map.Entry<String, String>> getHttpHeadersForTorrent() {
return this.bitTorrentClient.createRequestHeaders();
}

public long getUploaded(final InfoHash infoHash) {
return this.bandwidthDispatcher.getSeedStatForTorrent(infoHash).getUploaded();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public void onAnnounceStartFails(final Announcer announcer, final Throwable thro
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());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ public class ConfigIncomingMessage {
private final Integer simultaneousSeed;
private final String client;
private final boolean keepTorrentWithZeroLeechers;
private final Float uploadRatioTarget;

@JsonCreator
ConfigIncomingMessage(
@JsonProperty(value = "minUploadRate", required = true) final Long minUploadRate,
@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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ public void shouldFailToDeserializeIfKeepTorrentWithZeroLeechersIsNotDefined() t

@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,91 +11,91 @@
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);
}

@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);
}

@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);
Expand All @@ -105,15 +105,15 @@ public void shouldBuild() {

@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());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public class JoalConfigProviderTest {
190L,
5,
"azureus-5.7.5.0.client",
false
false,
1f
);

@Test
Expand Down Expand Up @@ -106,7 +107,8 @@ public void shouldWriteConfigurationFile() throws IOException {
rand.longs(201, 400).findFirst().getAsLong(),
rand.ints(1, 5).findFirst().getAsInt(),
RandomStringUtils.random(60),
false
false,
1f
);

provider.saveNewConf(newConf);
Expand Down
Original file line number Diff line number Diff line change
@@ -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.junit.jupiter.api.Test;
Expand All @@ -14,7 +15,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);
final Announcer announcer = announcerFactory.create(torrent);
Expand Down
Loading

0 comments on commit 49814ac

Please sign in to comment.