Commit 7275b02d authored by Georg Mittendorfer's avatar Georg Mittendorfer

Fix rate limiting to use number of operations contained in command instead of number of commands.

parent d90f68f4
Pipeline #59947557 passed with stage
in 3 minutes and 16 seconds
......@@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.collections4.CollectionUtils;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
......@@ -81,4 +82,9 @@ public class AttachToTangle extends IriCommand {
return false;
}
@JsonIgnore
@Override
public int getRateLimitCount() {
return CollectionUtils.size(trytes);
}
}
......@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.collections4.CollectionUtils;
import javax.validation.constraints.NotEmpty;
import java.util.List;
......@@ -53,4 +54,10 @@ public class BroadcastTransactions extends IriCommand {
return false;
}
@JsonIgnore
@Override
public int getRateLimitCount() {
return CollectionUtils.size(trytes);
}
}
......@@ -19,15 +19,15 @@
package com.mio.piri.commands;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.collections4.CollectionUtils;
import javax.validation.constraints.NotEmpty;
import java.util.List;
// not in api docs (yet).
// For more details see https://iota.stackexchange.com/questions/799/in-the-iota-wallet-what-does-the-promote-button-do/801#801
@Getter
@Setter
@ToString
......@@ -36,4 +36,10 @@ public class CheckConsistency extends IriCommand {
@NotEmpty
public List<String> tails; // list of transactions
@JsonIgnore
@Override
public int getRateLimitCount() {
return CollectionUtils.size(tails);
}
}
......@@ -19,10 +19,12 @@
package com.mio.piri.commands;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.collections4.CollectionUtils;
import java.util.List;
......@@ -45,4 +47,12 @@ public class FindTransactions extends IriCommand {
@JsonInclude(value=NON_NULL)
private List<String> approvees;
@JsonIgnore
@Override
public int getRateLimitCount() {
// the result only contains the intersection of these lists but IRI queries them all before intersecting.
// the effort is retrieving all of them plus intersection, therefore we sum them.
return CollectionUtils.size(addresses) + CollectionUtils.size(bundles) + CollectionUtils.size(tags) + CollectionUtils.size(approvees);
}
}
......@@ -19,10 +19,12 @@
package com.mio.piri.commands;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.collections4.CollectionUtils;
import javax.validation.constraints.NotNull;
import java.util.List;
......@@ -45,5 +47,10 @@ public class GetBalances extends IriCommand {
@JsonInclude(value=NON_NULL)
private List<String> tips;
@JsonIgnore
@Override
public int getRateLimitCount() {
return CollectionUtils.size(addresses);
}
}
......@@ -19,10 +19,12 @@
package com.mio.piri.commands;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.collections4.CollectionUtils;
import javax.validation.constraints.NotNull;
import java.util.List;
......@@ -42,4 +44,10 @@ public class GetInclusionStates extends IriCommand {
@NotNull
private List<String> tips;
@JsonIgnore
@Override
public int getRateLimitCount() {
return CollectionUtils.size(transactions);
}
}
......@@ -19,10 +19,12 @@
package com.mio.piri.commands;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.collections4.CollectionUtils;
import javax.validation.constraints.NotNull;
import java.util.List;
......@@ -43,4 +45,10 @@ public class GetTrytes extends IriCommand {
@JsonInclude(value=NON_NULL)
private List<String> hashes;
@JsonIgnore
@Override
public int getRateLimitCount() {
return CollectionUtils.size(hashes);
}
}
......@@ -98,6 +98,11 @@ public abstract class IriCommand {
return true;
}
@JsonIgnore
public int getRateLimitCount() {
return 1; // default
}
@JsonIgnore
private final List<String> executionHistory = new ArrayList<>();
......
......@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.collections4.CollectionUtils;
import javax.validation.constraints.NotEmpty;
import java.util.List;
......@@ -41,4 +42,10 @@ public class StoreTransactions extends IriCommand {
return false;
}
@JsonIgnore
@Override
public int getRateLimitCount() {
return CollectionUtils.size(trytes);
}
}
......@@ -20,9 +20,11 @@
package com.mio.piri.commands;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.collections4.CollectionUtils;
import javax.validation.constraints.NotEmpty;
import java.util.List;
......@@ -35,4 +37,10 @@ public class WereAddressesSpentFrom extends IriCommand {
@NotEmpty
private List<String> addresses;
@JsonIgnore
@Override
public int getRateLimitCount() {
return CollectionUtils.size(addresses);
}
}
......@@ -120,6 +120,8 @@ public class IriApiHandler {
if (rateLimitOperations.isIpRateLimited(ip)) {
RateLimiter rateLimiter = rateLimitOperations.rateLimiter(ip, command.getCommand());
RateLimiter globalLimiter = rateLimitOperations.globalRateLimiter();
rateLimitOperations.applyContentSpecificRateLimitWeight(command, rateLimiter);
rateLimitOperations.applyContentSpecificRateLimitWeight(command, globalLimiter);
responseEntityMono = responseEntityMono
.transform(RateLimiterOperator.of(rateLimiter))
.transform(RateLimiterOperator.of(globalLimiter));
......@@ -181,7 +183,7 @@ public class IriApiHandler {
} else {
// successful requests might count multiple times. this is a workaround for not being able
// to extend the rate limit dynamically within one rate limiting period.
rateLimitOperations.applyRateLimitWeight(command.getIp(), command.getCommand());
rateLimitOperations.applySuccessRateLimitWeight(command.getIp(), command.getCommand());
}
return responseEntity;
});
......@@ -261,7 +263,7 @@ public class IriApiHandler {
public ResponseEntity handleRateLimitReached(RuntimeException re) {
rejectedCommandCounter("limit").increment();
logger.info("Too many requests. {}", re.getMessage());
throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, "Rate limit reached or no node available. Try again later.");
throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, "Rate limit reached or all nodes are busy. Try again later.");
}
@InitBinder
......
......@@ -19,6 +19,7 @@
package com.mio.piri.tolerance;
import com.mio.piri.commands.IriCommand;
import io.github.resilience4j.ratelimiter.RateLimiter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
......@@ -27,6 +28,7 @@ import org.springframework.core.env.Environment;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
......@@ -46,7 +48,7 @@ public class RateLimitOperations {
rateLimitedCommands = rateLimiterFactory.getListOfRateLimitedCommands();
// get maximum dynamic limit (limit that can be increased, e.g. for ignoring erroneous responses).
rateLimitedCommands.forEach(str -> commandsSuccessWeight.put(str,
getRateLimitWeight(env, "ip." + str)));
initRateLimitWeight(env, "ip." + str)));
String noLimitPattern = StringUtils.stripToNull(env.getProperty("piri.rate.ip.unlimited.pattern"));
if (noLimitPattern == null) {
......@@ -71,31 +73,50 @@ public class RateLimitOperations {
return noRateLimitForPattern == null || StringUtils.isBlank(ip) || !noRateLimitForPattern.matcher(ip).matches();
}
public void applyRateLimitWeight(final String ip, final String command) {
public void applyContentSpecificRateLimitWeight(final IriCommand command, RateLimiter rateLimiter) {
Objects.requireNonNull(rateLimiter, "No rate limiter. Command: " + command.getCommand() + "");
if (rateLimitedCommands.contains(command.getCommand())) {
int weight = command.getRateLimitCount();
if (weight > 1) {
applyRateLimitWeight(rateLimiter, weight);
}
}
}
public void applySuccessRateLimitWeight(final String ip, final String command) {
// only apply if command is rate limit and weight > 1
if (rateLimitedCommands.contains(command) && commandsSuccessWeight.getOrDefault(command, 1) > 1) {
RateLimiter rateLimiter = rateLimiterFactory.rateLimiter(ip, command);
int weight = commandsSuccessWeight.get(command);
int processed = 1;
long waitTime = 0;
while (processed < weight && waitTime >= 0) {
// negative wait time means there is no reservation possible
// zero means reserved
// positive means reservation possible within timeout
waitTime = rateLimiter.reservePermission(rateLimiter.getRateLimiterConfig().getTimeoutDuration());
processed++;
if (waitTime != 0) {
logger.debug("Reservation wait time for rate limiter [{}]: [{}] nanos.", rateLimiter.getName(), waitTime);
}
applyRateLimitWeight(rateLimiter, commandsSuccessWeight.get(command));
}
}
private void applyRateLimitWeight(final RateLimiter rateLimiter, int weight) {
assert rateLimiter != null : "Rate limiter must not be null.";
int processed = 1;
long waitTime = 0;
while (processed < weight && waitTime >= 0) {
// negative wait time means there is no reservation possible
// zero means reserved
// positive means reservation possible within timeout
waitTime = rateLimiter.reservePermission(rateLimiter.getRateLimiterConfig().getTimeoutDuration());
processed++;
if (waitTime != 0) {
logger.debug("Reservation wait time for rate limiter [{}]: [{}] nanos.", rateLimiter.getName(), waitTime);
}
logger.debug("Applied rate limit weight of [{}] for rate limiter [{}].", processed, rateLimiter.getName());
} // else do nothing
}
logger.debug("Applied rate limit weight of [{}] for rate limiter [{}].", processed, rateLimiter.getName());
}
private int getRateLimitWeight(Environment env, String type) {
int value = env.getProperty("piri.rate." + type + ".weight", Integer.class, 1);
logger.info("Configured [{}] weight: [{}].", type, value);
private int initRateLimitWeight(Environment env, String commandName) {
int value = env.getProperty("piri.rate." + commandName + ".weight", Integer.class, 1);
logger.info("Configured [{}] weight: [{}].", commandName, value);
return value;
}
......
/*
* Copyright (c) 2019.
*
* This file is part of Piri.
*
* Piri is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* Piri is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with Piri. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mio.piri.commands;
import org.junit.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
public class AttachToTangleTest {
@Test
public void whenGetRateLimitCountThenReturnTrytesCount() {
AttachToTangle att = new AttachToTangle();
att.setTrytes(Arrays.asList("trytes", "trytes", "trytes"));
assertThat(att.getRateLimitCount()).isEqualTo(3);
}
}
\ No newline at end of file
/*
* Copyright (c) 2019.
*
* This file is part of Piri.
*
* Piri is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* Piri is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with Piri. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mio.piri.commands;
import org.junit.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
public class BroadcastTransactionsTest {
@Test
public void whenGetRateLimitCountThenReturnTrytesCount() {
BroadcastTransactions command = new BroadcastTransactions();
command.setTrytes(Arrays.asList("trytes", "trytes", "trytes"));
assertThat(command.getRateLimitCount()).isEqualTo(3);
}
}
\ No newline at end of file
/*
* Copyright (c) 2019.
*
* This file is part of Piri.
*
* Piri is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* Piri is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with Piri. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mio.piri.commands;
import org.junit.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
public class CheckConsistencyTest {
@Test
public void whenGetRateLimitCountThenReturnTailsCount() {
CheckConsistency command = new CheckConsistency();
command.setTails(Arrays.asList("a", "b", "c"));
assertThat(command.getRateLimitCount()).isEqualTo(3);
}
}
\ No newline at end of file
package com.mio.piri.commands;
import org.junit.Test;
import java.util.Arrays;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
public class FindTransactionsTest {
@Test
public void whenGetRateLimitCountThenReturnAddressesCount() {
FindTransactions command = new FindTransactions();
command.setAddresses(Arrays.asList("a", "b", "c"));
assertThat(command.getRateLimitCount()).isEqualTo(3);
}
@Test
public void whenGetRateLimitCountThenReturnTagsCount() {
FindTransactions command = new FindTransactions();
command.setTags(Arrays.asList("a", "b", "c"));
assertThat(command.getRateLimitCount()).isEqualTo(3);
}
@Test
public void whenGetRateLimitCountThenReturnApproveesCount() {
FindTransactions command = new FindTransactions();
command.setApprovees(Arrays.asList("a", "b", "c"));
assertThat(command.getRateLimitCount()).isEqualTo(3);
}
@Test
public void whenGetRateLimitCountThenReturnBundlesCount() {
FindTransactions command = new FindTransactions();
command.setBundles(Arrays.asList("a", "b", "c"));
assertThat(command.getRateLimitCount()).isEqualTo(3);
}
@Test
public void whenGetRateLimitCountThenReturnCombinedCount() {
FindTransactions command = new FindTransactions();
command.setAddresses(Collections.singletonList("a"));
command.setTags(Arrays.asList("a", "b"));
command.setBundles(Arrays.asList("a", "b", "c"));
command.setApprovees(Arrays.asList("a", "b", "c", "d"));
assertThat(command.getRateLimitCount()).isEqualTo(10);
}
}
\ No newline at end of file
/*
* Copyright (c) 2019.
*
* This file is part of Piri.
*
* Piri is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* Piri is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with Piri. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mio.piri.commands;
import org.junit.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
public class GetBalancesTest {
@Test
public void whenGetRateLimitCountThenReturnAddressesCount() {
GetBalances command = new GetBalances();
command.setAddresses(Arrays.asList("a", "b", "c"));
assertThat(command.getRateLimitCount()).isEqualTo(3);
}
}
\ No newline at end of file
/*
* Copyright (c) 2019.
*
* This file is part of Piri.
*
* Piri is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* Piri is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with Piri. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mio.piri.commands;
import org.junit.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
public class GetInclusionStatesTest {
@Test
public void whenGetRateLimitCountThenReturnTransactionsCount() {
GetInclusionStates command = new GetInclusionStates();
command.setTransactions(Arrays.asList("a", "b", "c"));
assertThat(command.getRateLimitCount()).isEqualTo(3);
}
}
\ No newline at end of file
/*
* Copyright (c) 2019.
*
* This file is part of Piri.
*
* Piri is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* Piri is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with Piri. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mio.piri.commands;
import org.junit.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
public class GetTrytesTest {
@Test
public void whenGetRateLimitCountThenReturnHashesCount() {
GetTrytes command = new GetTrytes();
command.setHashes(Arrays.asList("a", "b", "c"));
assertThat(command.getRateLimitCount()).isEqualTo(3);
}
}
\ No newline at end of file
/*
* Copyright (c) 2019.
*
* This file is part of Piri.
*
* Piri is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* Piri is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with Piri. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mio.piri.commands;
import org.junit.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
public class StoreTransactionsTest {
@Test
public void whenGetRateLimitCountThenReturnTrytesCount() {
StoreTransactions command = new StoreTransactions();
command.setTrytes(Arrays.asList("trytes", "trytes", "trytes"));
assertThat(command.getRateLimitCount()).isEqualTo(3);
}
}
\ No newline at end of file
/*
* Copyright (c) 2019.
*
* This file is part of Piri.
*
* Piri is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* Piri is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with Piri. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mio.piri.commands;
import org.junit.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
public class WereAddressesSpentFromTest {
@Test
public void whenGetRateLimitCountThenReturnAddressesCount() {
WereAddressesSpentFrom command = new WereAddressesSpentFrom();
command.setAddresses(Arrays.asList("a", "b", "c"));
assertThat(command.getRateLimitCount()).isEqualTo(3);