Limit bandwidth used by a group of torrents (#2761)

* Add support for bandwidth groups. i.e. Bandwidth limit for a (user specified) group of torrents,
This commit is contained in:
Matan Ziv-Av 2022-03-18 15:11:59 +02:00 committed by GitHub
parent 6cb0498486
commit c07bac4e19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 372 additions and 3 deletions

View File

@ -144,6 +144,7 @@ Request arguments:
| `downloadLimited` | boolean | true if `downloadLimit` is honored
| `files-unwanted` | array | indices of file(s) to not download
| `files-wanted` | array | indices of file(s) to download
| `group` | string | The name of this torrent's bandwidth group
| `honorsSessionLimits` | boolean | true if session upload limits are honored
| `ids` | array | torrent list, as described in 3.1
| `labels` | array | array of string labels
@ -228,6 +229,7 @@ The 'source' column here corresponds to the data structure there.
| `file-count` | number | tr_info
| `files`| array (see below)| n/a
| `fileStats`| array (see below)| n/a
| `group`| string| n/a
| `hashString`| string| tr_torrent_view
| `haveUnchecked`| number| tr_stat
| `haveValid`| number| tr_stat
@ -709,6 +711,50 @@ Response arguments:
| `size-bytes` | number | the size, in bytes, of the free space in that directory
| `total_size` | number | the total capacity, in bytes, of that directory
### 4.8 Bandwidth groups
#### 4.8.1 Bandwidth group Mutator: `group-set`
Method name: `group-set`
Request parameters:
| Key | Value type | Description
|:--|:--|:--
| `honorsSessionLimits` | boolean | true if session upload limits are honored
| `name` | string | Bandwidth group name
| `speed-limit-down-enabled` | boolean | true means enabled
| `speed-limit-down` | number | max global download speed (KBps)
| `speed-limit-up-enabled` | boolean | true means enabled
| `speed-limit-up` | number | max global upload speed (KBps)
Response arguments: none
#### 4.8.2 Bandwidth group Accessor: `group-get`
Method name: `group-get`
Request arguments: An optional argument `group`.
`group` is either a string naming the bandwidth group,
or a list of such strings.
If `group` is omitted, all bandwidth groups are used.
Response arguments:
| Key | Value type | Description
|:--|:--|:--
|`group`| array | A list of bandwidth group description objects
A bandwidth group description object has:
| Key | Value type | Description
|:--|:--|:--
| `honorsSessionLimits` | boolean | true if session upload limits are honored
| `name` | string | Bandwidth group name
| `speed-limit-down-enabled` | boolean | true means enabled
| `speed-limit-down` | number | max global download speed (KBps)
| `speed-limit-up-enabled` | boolean | true means enabled
| `speed-limit-up` | number | max global upload speed (KBps)
## 5. Protocol Versions
@ -949,13 +995,17 @@ Transmission 4.0.0 (`rpc-version-semver` 5.3.0, `rpc-version`: 17)
| `session-get` | new arg `script-torrent-done-seeding-filename`
| `torrent-add` | new arg `labels`
| `torrent-get` | new arg `file-count`
| `torrent-get` | new arg `group`
| `torrent-get` | new arg `percentComplete`
| `torrent-get` | new arg `primary-mime-type`
| `torrent-get` | new arg `tracker.sitename`
| `torrent-get` | new arg `trackerStats.sitename`
| `torrent-get` | new arg `trackerList`
| `torrent-set` | new arg `group`
| `torrent-set` | new arg `trackerList`
| `torrent-set` | **DEPRECATED** `trackerAdd`. Use `trackerList` instead.
| `torrent-set` | **DEPRECATED** `trackerRemove`. Use `trackerList` instead.
| `torrent-set` | **DEPRECATED** `trackerReplace`. Use `trackerList` instead.
| `group-set` | new method
| `group-get` | new method

View File

@ -5,6 +5,7 @@
#include <algorithm>
#include <vector>
#include <cstring>
#include <fmt/core.h>
@ -12,10 +13,15 @@
#include "bandwidth.h"
#include "crypto-utils.h" /* tr_rand_int_weak() */
#include "error.h"
#include "log.h"
#include "peer-io.h"
#include "platform.h"
#include "quark.h"
#include "session.h"
#include "tr-assert.h"
#include "utils.h"
#include "variant.h"
/***
****
@ -336,3 +342,25 @@ void Bandwidth::notifyBandwidthConsumed(tr_direction dir, size_t byte_count, boo
this->parent_->notifyBandwidthConsumed(dir, byte_count, is_piece_data, now);
}
}
/***
****
***/
tr_bandwidth_limits Bandwidth::getLimits() const
{
tr_bandwidth_limits limits;
limits.up_limit_KBps = tr_toSpeedKBps(this->getDesiredSpeedBytesPerSecond(TR_UP));
limits.down_limit_KBps = tr_toSpeedKBps(this->getDesiredSpeedBytesPerSecond(TR_DOWN));
limits.up_limited = this->isLimited(TR_UP);
limits.down_limited = this->isLimited(TR_DOWN);
return limits;
}
void Bandwidth::setLimits(tr_bandwidth_limits const* limits)
{
this->setDesiredSpeedBytesPerSecond(TR_UP, tr_toSpeedBytes(limits->up_limit_KBps));
this->setDesiredSpeedBytesPerSecond(TR_DOWN, tr_toSpeedBytes(limits->down_limit_KBps));
this->setLimited(TR_UP, limits->up_limited);
this->setLimited(TR_DOWN, limits->down_limited);
}

View File

@ -12,6 +12,7 @@
#include <array>
#include <cstddef> // size_t
#include <vector>
#include <string>
#include "transmission.h"
@ -24,6 +25,14 @@ class tr_peerIo;
* @{
*/
struct tr_bandwidth_limits
{
bool up_limited;
unsigned int up_limit_KBps;
bool down_limited;
unsigned int down_limit_KBps;
};
/**
* Bandwidth is an object for measuring and constraining bandwidth speeds.
*
@ -228,6 +237,9 @@ public:
bool honor_parent_limits_;
};
tr_bandwidth_limits getLimits() const;
void setLimits(tr_bandwidth_limits const* limits);
private:
static unsigned int getSpeedBytesPerSecond(RateControl& r, unsigned int interval_msec, uint64_t now);

View File

@ -18,7 +18,7 @@ using namespace std::literals;
namespace
{
auto constexpr my_static = std::array<std::string_view, 390>{ ""sv,
auto constexpr my_static = std::array<std::string_view, 391>{ ""sv,
"activeTorrentCount"sv,
"activity-date"sv,
"activityDate"sv,
@ -131,6 +131,7 @@ auto constexpr my_static = std::array<std::string_view, 390>{ ""sv,
"fromLtep"sv,
"fromPex"sv,
"fromTracker"sv,
"group"sv,
"hasAnnounced"sv,
"hasScraped"sv,
"hashString"sv,

View File

@ -134,6 +134,7 @@ enum
TR_KEY_fromLtep,
TR_KEY_fromPex,
TR_KEY_fromTracker,
TR_KEY_group,
TR_KEY_hasAnnounced,
TR_KEY_hasScraped,
TR_KEY_hashString,

View File

@ -133,6 +133,27 @@ static auto loadLabels(tr_variant* dict, tr_torrent* tor)
****
***/
static void saveGroup(tr_variant* dict, tr_torrent const* tor)
{
tr_variantDictAddStrView(dict, TR_KEY_group, tor->group);
}
static auto loadGroup(tr_variant* dict, tr_torrent* tor)
{
std::string_view groupName;
if (tr_variantDictFindStrView(dict, TR_KEY_group, &groupName) && !groupName.empty())
{
tor->setGroup(groupName);
return tr_resume::Group;
}
return tr_resume::fields_t{};
}
/***
****
***/
static void saveDND(tr_variant* dict, tr_torrent const* tor)
{
auto const n = tor->fileCount();
@ -819,6 +840,11 @@ static auto loadFromFile(tr_torrent* tor, tr_resume::fields_t fieldsToLoad, bool
fields_loaded |= loadLabels(&top, tor);
}
if ((fieldsToLoad & tr_resume::Group) != 0)
{
fields_loaded |= loadGroup(&top, tor);
}
/* loading the resume file triggers of a lot of changes,
* but none of them needs to trigger a re-saving of the
* same resume information... */
@ -930,6 +956,7 @@ void save(tr_torrent* tor)
saveFilenames(&top, tor);
saveName(&top, tor);
saveLabels(&top, tor);
saveGroup(&top, tor);
if (auto const err = tr_variantToFile(&top, TR_VARIANT_FMT_BENC, tor->resumeFile()); err != 0)
{

View File

@ -42,6 +42,7 @@ auto inline constexpr TimeDownloading = fields_t{ 1 << 19 };
auto inline constexpr Filenames = fields_t{ 1 << 20 };
auto inline constexpr Name = fields_t{ 1 << 21 };
auto inline constexpr Labels = fields_t{ 1 << 22 };
auto inline constexpr Group = fields_t{ 1 << 23 };
auto inline constexpr All = ~fields_t{ 0 };

View File

@ -562,6 +562,10 @@ static void initField(tr_torrent const* const tor, tr_stat const* const st, tr_v
addFileStats(tor, initme);
break;
case TR_KEY_group:
tr_variantInitStrView(initme, tor->group);
break;
case TR_KEY_hashString:
tr_variantInitStrView(initme, tor->infoHashString());
break;
@ -1157,6 +1161,11 @@ static char const* torrentSet(
}
}
if (std::string_view group; tr_variantDictFindStrView(args_in, TR_KEY_group, &group))
{
tor->setGroup(group);
}
if (errmsg == nullptr && tr_variantDictFindList(args_in, TR_KEY_labels, &tmp_variant))
{
errmsg = setLabels(tor, tmp_variant);
@ -1695,6 +1704,96 @@ static char const* torrentAdd(tr_session* session, tr_variant* args_in, tr_varia
****
***/
static char const* groupGet(tr_session* s, tr_variant* args_in, tr_variant* args_out, struct tr_rpc_idle_data* /*idle_data*/)
{
std::set<std::string_view> names;
if (std::string_view one_name; tr_variantDictFindStrView(args_in, TR_KEY_name, &one_name))
{
names.insert(one_name);
}
else if (tr_variant* namesList = nullptr; tr_variantDictFindList(args_in, TR_KEY_name, &namesList))
{
int names_count = tr_variantListSize(namesList);
for (int i = 0; i < names_count; i++)
{
tr_variant* v = tr_variantListChild(namesList, i);
if (std::string_view l; tr_variantIsString(v) && tr_variantGetStrView(v, &l))
{
names.insert(l);
}
}
}
tr_variant* list = tr_variantDictAddList(args_out, TR_KEY_group, 1);
for (auto const& [name, group] : s->bandwidth_groups)
{
if (names.empty() || names.count(name) > 0)
{
tr_variant* dict = tr_variantListAddDict(list, 5);
auto limits = group->getLimits();
tr_variantDictAddStrView(dict, TR_KEY_name, name);
tr_variantDictAddBool(dict, TR_KEY_uploadLimited, limits.up_limited);
tr_variantDictAddInt(dict, TR_KEY_uploadLimit, limits.up_limit_KBps);
tr_variantDictAddBool(dict, TR_KEY_downloadLimited, limits.down_limited);
tr_variantDictAddInt(dict, TR_KEY_downloadLimit, limits.down_limit_KBps);
tr_variantDictAddBool(dict, TR_KEY_honorsSessionLimits, group->areParentLimitsHonored(TR_UP));
}
}
return nullptr;
}
static char const* groupSet(
tr_session* session,
tr_variant* args_in,
tr_variant* /*args_out*/,
struct tr_rpc_idle_data* /*idle_data*/)
{
std::string_view name;
if (!tr_variantDictFindStrView(args_in, TR_KEY_name, &name))
{
return "No group name given";
}
Bandwidth* group = session->bandwidthGroupFind(name);
if (group == nullptr)
{
return "No such group";
}
auto limits = group->getLimits();
tr_variantDictFindBool(args_in, TR_KEY_speed_limit_down_enabled, &limits.down_limited);
int64_t intVal = 0;
if (tr_variantDictFindInt(args_in, TR_KEY_speed_limit_down, &intVal))
{
limits.down_limit_KBps = intVal;
}
tr_variantDictFindBool(args_in, TR_KEY_speed_limit_up_enabled, &limits.up_limited);
if (tr_variantDictFindInt(args_in, TR_KEY_speed_limit_up, &intVal))
{
limits.up_limit_KBps = intVal;
}
group->setLimits(&limits);
bool honors = false;
if (tr_variantDictFindBool(args_in, TR_KEY_honorsSessionLimits, &honors))
{
group->honorParentLimits(TR_UP, honors);
group->honorParentLimits(TR_DOWN, honors);
}
return nullptr;
}
/***
****
***/
static char const* sessionSet(
tr_session* session,
tr_variant* args_in,
@ -2346,9 +2445,11 @@ struct rpc_method
handler func;
};
static auto constexpr Methods = std::array<rpc_method, 22>{ {
static auto constexpr Methods = std::array<rpc_method, 24>{ {
{ "blocklist-update"sv, false, blocklistUpdate },
{ "free-space"sv, true, freeSpace },
{ "group-get"sv, true, groupGet },
{ "group-set"sv, true, groupSet },
{ "port-test"sv, false, portTest },
{ "queue-move-bottom"sv, true, queueMoveBottom },
{ "queue-move-down"sv, true, queueMoveDown },

View File

@ -78,6 +78,9 @@ static auto constexpr DefaultPrefetchEnabled = bool{ true };
#endif
static auto constexpr SaveIntervalSecs = int{ 360 };
static void bandwidthGroupRead(tr_session* session, char const* configDir);
static int bandwidthGroupWrite(tr_session* session, char const* configDir);
static tr_port getRandomPort(tr_session const* s)
{
return tr_port(tr_rand_int_weak(s->randomPortHigh - s->randomPortLow + 1) + s->randomPortLow);
@ -536,6 +539,9 @@ void tr_sessionSaveSettings(tr_session* session, char const* config_dir, tr_vari
/* cleanup */
tr_variantFree(&settings);
/* Write bandwidth groups limits to file */
bandwidthGroupWrite(session, config_dir);
}
/***
@ -596,6 +602,7 @@ tr_session* tr_sessionInit(char const* config_dir, bool messageQueuingEnabled, t
session->session_id = tr_session_id_new();
session->bandwidth = new Bandwidth(nullptr);
session->removed_torrents.clear();
bandwidthGroupRead(session, config_dir);
/* nice to start logging at the very beginning */
if (auto i = int64_t{}; tr_variantDictFindInt(clientSettings, TR_KEY_message_level, &i))
@ -2267,6 +2274,26 @@ void tr_sessionSetDefaultTrackers(tr_session* session, char const* trackers)
****
***/
Bandwidth* tr_session::bandwidthGroupFind(std::string_view name)
{
std::string str_name{ name };
if (name.empty())
{
return nullptr;
}
if (bandwidth_groups[str_name] == nullptr)
{
Bandwidth* bw = new Bandwidth(bandwidth);
bandwidth_groups[str_name] = bw;
}
return bandwidth_groups[str_name];
}
/***
****
***/
struct port_forwarding_data
{
bool enabled;
@ -2845,3 +2872,78 @@ int tr_sessionCountQueueFreeSlots(tr_session* session, tr_direction dir)
return max - active_count;
}
static void bandwidthGroupRead(tr_session* session, char const* configDir)
{
tr_variant group_list;
auto const filename = tr_strvPath(configDir, "bandwidthGroups");
if (!tr_variantFromFile(&group_list, TR_VARIANT_PARSE_JSON, filename, nullptr) || !tr_variantIsList(&group_list))
{
return;
}
int n = tr_variantListSize(&group_list);
for (int i = 0; i < n; i++)
{
tr_variant* dict = tr_variantListChild(&group_list, i);
std::string_view name;
if (!tr_variantDictFindStrView(dict, TR_KEY_name, &name) || name.empty())
{
continue;
}
Bandwidth* group = session->bandwidthGroupFind(name);
if (group == nullptr)
{
continue;
}
int64_t val = 0;
tr_bandwidth_limits limits;
tr_variantDictFindBool(dict, TR_KEY_uploadLimited, &limits.up_limited);
if (tr_variantDictFindInt(dict, TR_KEY_uploadLimit, &val))
{
limits.up_limit_KBps = val;
}
tr_variantDictFindBool(dict, TR_KEY_downloadLimited, &limits.down_limited);
if (tr_variantDictFindInt(dict, TR_KEY_downloadLimit, &val))
{
limits.down_limit_KBps = val;
}
group->setLimits(&limits);
bool honors = false;
if (tr_variantDictFindBool(dict, TR_KEY_honorsSessionLimits, &honors))
{
group->honorParentLimits(TR_UP, honors);
group->honorParentLimits(TR_DOWN, honors);
}
}
tr_variantFree(&group_list);
}
static int bandwidthGroupWrite(tr_session* session, char const* configDir)
{
tr_variant group_list;
int n = session->bandwidth_groups.size();
tr_variantInitList(&group_list, n);
for (auto const& [name, group] : session->bandwidth_groups)
{
tr_variant* dict = tr_variantListAddDict(&group_list, 5);
auto limits = group->getLimits();
tr_variantDictAddStr(dict, TR_KEY_name, name);
tr_variantDictAddBool(dict, TR_KEY_uploadLimited, limits.up_limited);
tr_variantDictAddInt(dict, TR_KEY_uploadLimit, limits.up_limit_KBps);
tr_variantDictAddBool(dict, TR_KEY_downloadLimited, limits.down_limited);
tr_variantDictAddInt(dict, TR_KEY_downloadLimit, limits.down_limit_KBps);
tr_variantDictAddBool(dict, TR_KEY_honorsSessionLimits, group->areParentLimitsHonored(TR_UP));
}
auto const filename = tr_strvPath(configDir, "bandwidthGroups");
auto const ret = tr_variantToFile(&group_list, TR_VARIANT_FMT_JSON, filename);
tr_variantFree(&group_list);
return ret;
}

View File

@ -26,6 +26,7 @@
#include "transmission.h"
#include "announce-list.h"
#include "bandwidth.h"
#include "net.h" // tr_socket_t
#include "quark.h"
#include "torrents.h"
@ -404,6 +405,9 @@ public:
// Only session.cc should use this.
int peer_socket_tos_ = *tr_netTosFromName(TR_DEFAULT_PEER_SOCKET_TOS_STR);
Bandwidth* bandwidthGroupFind(std::string_view name);
std::map<std::string, Bandwidth*> bandwidth_groups;
private:
static std::recursive_mutex session_mutex_;

View File

@ -1902,6 +1902,21 @@ void tr_torrentSetLabels(tr_torrent* tor, tr_labels_t&& labels)
****
***/
void tr_torrent::setGroup(std::string_view groupName)
{
auto const lock = this->unique_lock();
this->group.assign(groupName);
Bandwidth* bw = this->session->bandwidthGroupFind(group);
this->bandwidth->setParent(bw != nullptr ? bw : this->session->bandwidth);
this->setDirty();
}
/***
****
***/
tr_priority_t tr_torrentGetPriority(tr_torrent const* tor)
{
TR_ASSERT(tr_isTorrent(tor));

View File

@ -717,6 +717,10 @@ public:
tr_labels_t labels;
std::string group;
/* Set the bandwidth group the torrent belongs to */
void setGroup(std::string_view groupName);
static auto constexpr MagicNumber = int{ 95549 };
tr_file_piece_map fpm_ = tr_file_piece_map{ metainfo_ };

View File

@ -243,7 +243,7 @@ enum
****
***/
static auto constexpr Options = std::array<tr_option, 89>{
static auto constexpr Options = std::array<tr_option, 91>{
{ { 'a', "add", "Add torrent files by filename or URL", "a", false, nullptr },
{ 970, "alt-speed", "Use the alternate Limits", "as", false, nullptr },
{ 971, "no-alt-speed", "Don't use the alternate Limits", "AS", false, nullptr },
@ -258,6 +258,8 @@ static auto constexpr Options = std::array<tr_option, 89>{
{ 'c', "incomplete-dir", "Where to store new torrents until they're complete", "c", true, "<dir>" },
{ 'C', "no-incomplete-dir", "Don't store incomplete torrents in a different location", "C", false, nullptr },
{ 'b', "debug", "Print debugging information", "b", false, nullptr },
{ 730, "bandwidth-group", "Set the current torrents' bandwidth group", "bwg", true, "<group>" },
{ 731, "no-bandwidth-group", "Reset the current torrents' bandwidth group", "nwg", false, nullptr },
{ 'd',
"downlimit",
"Set the max download speed in " SPEED_K_STR " for the current torrent(s) or globally",
@ -481,6 +483,8 @@ static int getOptMode(int val)
case 900: /* file priority-high */
case 901: /* file priority-normal */
case 902: /* file priority-low */
case 730: /* set bandwidth group */
case 731: /* reset bandwidth group */
return MODE_TORRENT_SET | MODE_TORRENT_ADD;
case 961: /* find */
@ -673,6 +677,11 @@ static void addLabels(tr_variant* args, std::string_view comma_delimited_labels)
}
}
static void setGroup(tr_variant* args, std::string_view group)
{
tr_variantDictAddStrView(args, TR_KEY_group, group);
}
static void addFiles(tr_variant* args, tr_quark const key, char const* arg)
{
tr_variant* files = tr_variantDictAddList(args, key, 100);
@ -716,6 +725,7 @@ static tr_quark const details_keys[] = {
TR_KEY_error,
TR_KEY_errorString,
TR_KEY_eta,
TR_KEY_group,
TR_KEY_hashString,
TR_KEY_haveUnchecked,
TR_KEY_haveValid,
@ -979,6 +989,11 @@ static void printDetails(tr_variant* top)
printf("\n");
}
if (tr_variantDictFindStrView(t, TR_KEY_group, &sv) && !sv.empty())
{
printf(" Bandwidth group: %" TR_PRIsv "\n", TR_PRIsv_ARG(sv));
}
printf("\n");
printf("TRANSFER\n");
@ -2785,6 +2800,14 @@ static int processArgs(char const* rpcurl, int argc, char const* const* argv)
addLabels(args, optarg ? optarg : "");
break;
case 730:
setGroup(args, optarg ? optarg : "");
break;
case 731:
setGroup(args, "");
break;
case 900:
addFiles(args, TR_KEY_priority_high, optarg);
break;