From 4b73f8b3d33344432f464dc6d8f8258d3dea5295 Mon Sep 17 00:00:00 2001
From: Yorhel
Date: Sat, 7 Nov 2009 12:33:38 +0100
Subject: API: Added commands/minute and sqltime/minute throttle
This is apparently a token bucket algorithm, though I learned about
that term after I wrote the implementation.
These limits shouldn't be very strict, in a normal situation client
applications won't have to worry about it.
---
data/docs/11 | 17 ++++++++++++++++-
lib/Multi/API.pm | 40 +++++++++++++++++++++++++++++++++++-----
2 files changed, 51 insertions(+), 6 deletions(-)
diff --git a/data/docs/11 b/data/docs/11
index 202d9024..70c5b2f5 100644
--- a/data/docs/11
+++ b/data/docs/11
@@ -53,7 +53,14 @@ server resources and prevent abuse of this service.
5 connections per IP. All connections that are opened after reaching this limit will be immediately closed.
3 connections per user. The login command will reply with a 'sesslimit' error when reaching this limit.
Each command currently returns at most 10 results. TODO: make configurable?
- more to come...
+ 30 commands per minute per user. Server will reply with a 'throttled' error (type="cmd") when reaching this limit.
+
+ 1 second of SQL time per minute per user. SQL time is the total time taken to
+ run the database queries for each command. This depends on both the command
+ (filters and get flags) and server load, and is thus not very predictable.
+ Server will reply with a 'throttled' error with type="sql" upon reaching
+ this limit.
+
@@ -421,6 +428,14 @@ however still required.
missingA JSON object argument is missing a required member. The name of which is given in the additional "field" member.
badargA JSON value is of the wrong type or in the wrong format. The name of the incorrect field is given in a "field" member.
needloginNeed to be logged in to issue this command.
+ throttled
+ You have used too many server resources within a short time, and need to wait
+ a bit before sending the next command. The type of throttle is given in the
+ "type" member, and the "minwait" and "fullwait" members tell you how long you
+ need to wait before sending the next command and when you can start bursting
+ again (this is the recommended waiting time), respectively. Both values are in
+ seconds, with one decimal after the point.
+
auth(login) Incorrect username/password combination.
loggedin(login) Already logged in. Only one successful login command can be issues on one connection.
sesslimit(login) Too many open sessions for the current user.
diff --git a/lib/Multi/API.pm b/lib/Multi/API.pm
index 3271e27b..7b36755d 100644
--- a/lib/Multi/API.pm
+++ b/lib/Multi/API.pm
@@ -13,6 +13,7 @@ use POE 'Wheel::SocketFactory', 'Wheel::ReadWrite';
use POE::Filter::VNDBAPI 'encode_filters';
use Digest::SHA 'sha256_hex';
use Encode 'encode_utf8';
+use Time::HiRes 'time'; # important for throttling
# not exported by Socket, taken from netinet/tcp.h (specific to Linux, AFAIK)
@@ -21,6 +22,11 @@ sub TCP_KEEPINTVL { 5 }
sub TCP_KEEPCNT { 6 }
+# Global throttle hash, key = username, value = [ cmd_time, sql_time ]
+# TODO: clean up items in this hash when username isn't connected anymore and throttle times < current time
+my %throttle;
+
+
sub spawn {
my $p = shift;
POE::Session->create(
@@ -37,6 +43,8 @@ sub spawn {
conn_per_ip => 5,
sess_per_user => 3,
tcp_keepalive => [ 120, 60, 3 ], # time, intvl, probes
+ throttle_cmd => [ 2, 30 ], # interval between each command, allowed burst
+ throttle_sql => [ 60, 1 ], # sql time multiplier, allowed burst (in sql time)
@_,
c => {},
},
@@ -247,13 +255,28 @@ sub client_input {
# when we're here, we can assume that $cmd contains a valid command
# and the arguments are syntactically valid
- # login
+ # handle login command
return $_[KERNEL]->yield(login => $c, @$arg) if $cmd eq 'login';
-
return cerr $c, needlogin => 'Not logged in.' if !$c->{username};
- # TODO: throttling
- # get
+ # update throttle array of the current user
+ my $time = time;
+ $_ < $time && ($_ = $time) for @{$c->{throttle}};
+
+ # check for thottle rule violation
+ my @limits = ('cmd', 'sql');
+ for (0..$#limits) {
+ my $threshold = $_[HEAP]{"throttle_$limits[$_]"}[0]*$_[HEAP]{"throttle_$limits[$_]"}[1];
+ return cerr $c, throttled => 'Throttle limit reached.', type => $limits[$_],
+ minwait => int(10*($c->{throttle}[$_]-$time-$threshold))/10+1,
+ fullwait => int(10*($c->{throttle}[$_]-$time))/10+1
+ if $c->{throttle}[$_]-$time > $threshold;
+ }
+
+ # update commands/second throttle
+ $c->{throttle}[0] += $_[HEAP]{throttle_cmd}[0];
+
+ # handle get command
return cerr $c, 'parse', "Unkown command '$cmd'" if $cmd ne 'get';
my $type = shift @$arg;
return cerr $c, 'gettype', "Unknown get type: '$type'" if $type ne 'vn';
@@ -293,8 +316,12 @@ sub login_res { # num, res, [ c, arg ]
my $encrypted = sha256_hex($VNDB::S{global_salt}.encode_utf8($arg->{password}).encode_utf8($res->[0]{salt}));
return cerr $c, auth => "Wrong password for user '$arg->{username}'" if lc($encrypted) ne lc($res->[0]{passwd});
- $c->{wheel}->put(['ok']);
+ # link this connection to the users' throttle array (create this if necessary)
+ $throttle{$arg->{username}} = [ time, time ] if !$throttle{$arg->{username}};
+ $c->{throttle} = $throttle{$arg->{username}};
+
$c->{username} = $arg->{username};
+ $c->{wheel}->put(['ok']);
$_[KERNEL]->yield(log => $c,
'Successful login by %s using client "%s" ver. %s', $arg->{username}, $arg->{client}, $arg->{clientver});
}
@@ -425,6 +452,9 @@ sub get_vn_res {
\@ids, 'get_vn_res', { %$get, type => 'relations' });
}
+ # update sql throttle
+ $get->{c}{throttle}[1] += $get->{time}*$_[HEAP]{throttle_sql}[0];
+
# send and log
delete $_->{latest} for @{$get->{list}};
$num = @{$get->{list}};
--
cgit v1.2.3