File Coverage

blib/lib/RateLimitations.pm
Criterion Covered Total %
statement 70 110 63.6
branch 5 16 31.2
condition 5 17 29.4
subroutine 14 20 70.0
pod 6 6 100.0
total 100 169 59.1


line stmt bran cond sub pod time code
1             package RateLimitations;
2              
3 2     2   112158 use strict;
  2         4  
  2         48  
4 2     2   6 use warnings;
  2         2  
  2         56  
5             our $VERSION = '0.04';
6              
7 2     2   6 use Carp;
  2         2  
  2         82  
8              
9 2     2   802 use Cache::RedisDB;
  2         120752  
  2         58  
10 2     2   866 use Config::Onion;
  2         67514  
  2         68  
11 2     2   16 use List::Util qw(all);
  2         2  
  2         280  
12 2     2   1062 use Time::Duration::Concise;
  2         8238  
  2         84  
13              
14 2     2   10 use base qw( Exporter );
  2         2  
  2         220  
15             our @EXPORT_OK = qw(
16             all_service_consumers
17             flush_all_service_consumers
18             rate_limited_services
19             rate_limits_for_service
20             verify_rate_limitations_config
21             within_rate_limits
22             );
23 2     2   8 use constant KEYSPACE => 'RATELIMITATIONS'; # Everything will fall under this
  2         4  
  2         88  
24 2     2   24 use constant SEPARATOR => '::'; # How to join strings together
  2         2  
  2         336  
25              
26             my $rates_file_content;
27             my %limits;
28              
29             BEGIN {
30 2     2   16 my $cfg = Config::Onion->new;
31 2         2308 $cfg->set_default(
32             rl_internal_testing => {
33             '10s' => 2,
34             '5m' => 6
35             });
36 2         634 $cfg->load('/etc/perl_rate_limitations', '/etc/rmg/perl_rate_limitations');
37 2         40294 $rates_file_content = $cfg->get;
38 2         1066 foreach my $svc (sort keys %$rates_file_content) {
39 2         4 my @service_limits;
40 2         2 foreach my $time (keys %{$rates_file_content->{$svc}}) {
  2         10  
41 4         144 my $ti = Time::Duration::Concise->new(interval => $time);
42 4         136 my $count = $rates_file_content->{$svc}->{$time};
43 4         14 push @service_limits, [$ti->seconds, $count];
44             }
45 2         44 @service_limits = sort { $a->[0] <=> $b->[0] } @service_limits;
  2         6  
46 2         1452 $limits{$svc} = {
47             rates => \@service_limits,
48             seconds => $service_limits[-1]->[0],
49             entries => $service_limits[-1]->[1],
50             };
51             }
52             }
53              
54             sub verify_rate_limitations_config {
55 1     1 1 335578 my $proper = 1; # Assume it is proper until we find a bad entry
56 1         23 foreach my $svc (sort keys %$rates_file_content) {
57 1         2 my @service_limits;
58 1         3 foreach my $time (keys %{$rates_file_content->{$svc}}) {
  1         11  
59 2         34 my $ti = Time::Duration::Concise->new(interval => $time);
60 2         91 my $count = $rates_file_content->{$svc}->{$time};
61 2         12 push @service_limits, [$ti->seconds, $count, $count / $ti->seconds, undef, $time];
62             }
63 1         15 @service_limits = sort { $a->[0] <=> $b->[0] } @service_limits;
  1         6  
64              
65 1         2 my @final_limits;
66 1         55 while (my $this_limit = shift @service_limits) {
67 2         6 my ($improper, $index) = ($this_limit->[3], $#service_limits);
68 2   66     32 while (not $improper and $index > -1) {
69 1         2 my $that_limit = $service_limits[$index];
70             # This one is improper if that longer period has the same or smaller count
71 1 50       3 $improper = 'count should be lower than ' . $that_limit->[4] . ' count' if ($that_limit->[1] <= $this_limit->[1]);
72             # That one is improper if this shorter period has the smaller rate
73 1 50 33     9 $service_limits[$index]->[3] = 'rate should be lower than ' . $this_limit->[4] . ' rate'
74             if (not $improper and $this_limit->[2] < $that_limit->[2]);
75 1         4 $index--;
76             }
77 2 50       9 if ($improper) {
78             # If any entry is improper we will fail and warn.
79             # We still check the rest for completeness
80 0         0 $proper = 0;
81 0         0 carp $svc . ' - ' . $this_limit->[4] . ' entry improper: ' . $improper;
82             }
83             }
84             }
85 1         18 return $proper;
86             }
87              
88             sub within_rate_limits {
89 0     0 1 0 my $args = shift;
90              
91 0 0       0 croak 'Must supply args as a hash reference' unless ref $args eq 'HASH';
92 0         0 my ($service, $consumer) = @{$args}{'service', 'consumer'};
  0         0  
93 0 0   0   0 croak 'Must supply both "service" and "consumer" arguments' unless all { defined } ($service, $consumer);
  0         0  
94 0         0 my $limit = $limits{$service};
95 0 0       0 croak 'Unknown service supplied: ' . $service unless $limit;
96              
97 0         0 my $redis = Cache::RedisDB->redis;
98 0         0 my $key = _make_key($service, $consumer);
99 0         0 my $within_limits = 1;
100 0         0 my $now = time;
101 0         0 $redis->lpush($key, $now); # We push first so that we hit limits more often in heavy (DoS) conditions
102 0         0 $redis->ltrim($key, 0, $limit->{entries}); # Our new entry is now in index 0.. we keep 1 extra entry.
103 0         0 $redis->expire($key, $limit->{seconds});
104 0         0 foreach my $rate (@{$limit->{rates}}) {
  0         0  
105 0 0 0     0 if (($redis->lindex($key, $rate->[1]) // 0) > $now - $rate->[0]) {
106 0         0 $within_limits = 0;
107 0         0 last;
108             }
109             }
110              
111 0         0 return $within_limits;
112             }
113              
114             sub flush_all_service_consumers {
115 0     0 1 0 my $redis = Cache::RedisDB->redis;
116 0         0 my $count = 0;
117              
118 0         0 foreach my $key (_all_keys($redis)) {
119 0         0 $count += $redis->del($key);
120             }
121              
122 0         0 return $count;
123             }
124              
125 0   0 0   0 sub _all_keys { my $redis = shift // Cache::RedisDB->redis; return @{$redis->keys(_make_key('*', '*')) // []}; }
  0   0     0  
  0         0  
126              
127 1     1 1 987 sub rate_limited_services { return (sort keys %limits); }
128              
129             sub rate_limits_for_service {
130 2   100 2 1 1573 my $service = shift // 'undef';
131 2         5 my $svc_limits = $limits{$service};
132 2 100       18 croak 'Unknown service supplied: ' . $service unless $svc_limits;
133              
134 1         2 return @{$svc_limits->{rates}};
  1         18  
135             }
136              
137             sub all_service_consumers {
138              
139 0     0 1   my %consumers;
140              
141 0           foreach my $pair (map { [(split SEPARATOR, $_)[-2, -1]] } _all_keys()) {
  0            
142 0   0       $consumers{$pair->[0]} //= [];
143 0           push @{$consumers{$pair->[0]}}, $pair->[1];
  0            
144             }
145              
146 0           return \%consumers;
147             }
148              
149             sub _make_key {
150 0     0     my ($service, $consumer) = @_;
151              
152 0           return join(SEPARATOR, KEYSPACE, $service, $consumer);
153             }
154              
155             1;
156             __END__