diff options
author | Yorhel <git@yorhel.nl> | 2009-11-07 12:33:38 +0100 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2009-11-07 12:33:38 +0100 |
commit | 4b73f8b3d33344432f464dc6d8f8258d3dea5295 (patch) | |
tree | 6391979a551dc7d9c49209d63f4646b06f4b9bef | |
parent | da73ec4715f6bdb67833ed482ad09e8735c29977 (diff) |
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.
-rw-r--r-- | data/docs/11 | 17 | ||||
-rw-r--r-- | 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.</p> <li>5 connections per IP. All connections that are opened after reaching this limit will be immediately closed.</li> <li>3 connections per user. The login command will reply with a 'sesslimit' error when reaching this limit.</li> <li>Each command currently returns at most 10 results. <i>TODO: make configurable?</i></li> - <li><i>more to come...</i></li> + <li>30 commands per minute per user. Server will reply with a 'throttled' error (type="cmd") when reaching this limit.</li> + <li> + 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. + </li> </ul> <br /> @@ -421,6 +428,14 @@ however still required.<br /> <dt>missing</dt><dd>A JSON object argument is missing a required member. The name of which is given in the additional "field" member.</dd> <dt>badarg</dt><dd>A JSON value is of the wrong type or in the wrong format. The name of the incorrect field is given in a "field" member.</dd> <dt>needlogin</dt><dd>Need to be logged in to issue this command.</dd> + <dt>throttled</td><dd> + 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. + </dd> <dt>auth</dt><dd>(login) Incorrect username/password combination.</dd> <dt>loggedin</dt><dd>(login) Already logged in. Only one successful login command can be issues on one connection.</dd> <dt>sesslimit</dt><dd>(login) Too many open sessions for the current user.</dd> 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}}; |