File Coverage

blib/lib/Redis/LeaderBoardMulti.pm
Criterion Covered Total %
statement 56 291 19.2
branch 20 118 16.9
condition 9 69 13.0
subroutine 10 46 21.7
pod 0 13 0.0
total 95 537 17.6


line stmt bran cond sub pod time code
1             package Redis::LeaderBoardMulti;
2 12     12   588597 use 5.010;
  12         28  
3 12     12   42 use strict;
  12         12  
  12         195  
4 12     12   32 use warnings;
  12         19  
  12         391  
5              
6             our $VERSION = "0.02";
7              
8 12     12   3543 use Redis::LeaderBoardMulti::Member;
  12         16  
  12         292  
9 12     12   4362 use Redis::Transaction qw/multi_exec watch_multi_exec/;
  12         4746  
  12         528  
10 12     12   4259 use Redis::Script;
  12         38388  
  12         483  
11 12     12   50 use Carp;
  12         12  
  12         32620  
12              
13             our $SUPPORT_64BIT = eval { unpack('q>', "\x00\x00\x00\x00\x00\x00\x00\x01") };
14              
15             sub new {
16 70     70 0 13278 my ($class, %args) = @_;
17 70         224 my $self = bless {
18             use_hash => 1,
19             use_script => 1,
20             use_evalsha => 1,
21             order => ['desc'],
22             %args,
23             }, $class;
24              
25 70   33     251 $self->{hash_key} ||= $self->{key} . ":score";
26              
27 70         54 my $mask = "";
28 70 50       102 unless (ref $self->{order}) {
29 0         0 $self->{order} = [$self->{order}];
30             }
31 70         45 for my $order (@{$self->{order}}) {
  70         110  
32 74         43 my $m = "\x80\x00\x00\x00\x00\x00\x00\x00";
33 74 100       106 if ($order eq 'asc') {
    50          
34             # do nothing
35             } elsif ($order eq 'desc') {
36 30         27 $m = ~$m;
37             } else {
38 0         0 die "invalid order: $order";
39             }
40 74         91 $mask .= $m;
41             }
42 70         63 $self->{_mask} = $mask;
43 70 100       70 $self->{_pack_pattern} = ($SUPPORT_64BIT ? "q>" : "l>l>") x scalar @{$self->{order}};
  70         129  
44 70 100       68 $self->{_unpack_pattern} = ($SUPPORT_64BIT ? "q>" : "l>l>") x scalar @{$self->{order}};
  70         68  
45              
46 70         110 return $self;
47             }
48              
49             sub set_score {
50 0     0 0 0 my $self = shift;
51 0         0 for (my $i = 0; $i < @_; $i+=2) {
52 0         0 $self->_set_score($_[$i], $_[$i+1]);
53             }
54             }
55              
56             sub _set_score {
57 0     0   0 my ($self, $member, $scores) = @_;
58 0         0 my $redis = $self->{redis};
59 0         0 my $key = $self->{key};
60 0         0 my $packed_score = $self->_pack_scores($scores);
61              
62 0 0       0 if ($self->{use_hash}) {
63 0         0 my $hash_key = $self->{hash_key};
64 0 0       0 if ($self->{use_script}) {
65             my $script = $self->{_set_score_hash_script} ||= Redis::Script->new(
66             use_evalsha => $self->{use_evalsha},
67 0   0     0 script => <
68             local s=redis.call('HGET',KEYS[2],ARGV[1])
69             if s then
70             redis.call('ZREM',KEYS[1],s..ARGV[1])
71             end
72             redis.call('ZADD',KEYS[1],0,ARGV[2]..ARGV[1])
73             redis.call('HSET',KEYS[2],ARGV[1],ARGV[2])
74             EOS
75             );
76 0         0 $script->eval($redis, [$key, $hash_key], [$member, $packed_score]);
77             } else {
78             watch_multi_exec $redis, [$hash_key], 10, sub {
79 0     0   0 return $redis->hget($hash_key, $member);
80             }, sub {
81 0     0   0 my (undef, $old_packed_score) = @_;
82 0 0       0 $redis->zrem($key, "$old_packed_score$member", sub {}) if $old_packed_score;
83 0         0 $redis->zadd($key, 0, "$packed_score$member", sub {});
84 0         0 $redis->hset($hash_key, $member, $packed_score, sub {});
85 0         0 };
86             }
87             } else {
88 0         0 my $sub_sort_key = "$key:$member";
89 0 0       0 if ($self->{use_script}) {
90             my $script = $self->{_set_score_script} ||= Redis::Script->new(
91             use_evalsha => $self->{use_evalsha},
92 0   0     0 script => <
93             local s=redis.call('GET',KEYS[2])
94             if s then
95             redis.call('ZREM',KEYS[1],s..ARGV[1])
96             end
97             redis.call('ZADD',KEYS[1],0,ARGV[2]..ARGV[1])
98             redis.call('SET',KEYS[2],ARGV[2])
99             EOS
100             );
101 0         0 $script->eval($redis, [$key, $sub_sort_key], [$member, $packed_score]);
102             } else {
103             watch_multi_exec $redis, [$sub_sort_key], 10, sub {
104 0     0   0 return $redis->get($sub_sort_key);
105             }, sub {
106 0     0   0 my (undef, $old_packed_score) = @_;
107 0 0       0 $redis->zrem($key, "$old_packed_score$member", sub {}) if $old_packed_score;
108 0         0 $redis->zadd($key, 0, "$packed_score$member", sub {});
109 0         0 $redis->set($sub_sort_key, $packed_score, sub {});
110 0         0 };
111             }
112             }
113 0         0 $self->_set_expire_and_limit($member);
114             }
115              
116             sub get_score {
117 0     0 0 0 my ($self, $member) = @_;
118 0         0 my $redis = $self->{redis};
119 0         0 my $key = $self->{key};
120             my $packed_score = $self->{use_hash}
121 0 0       0 ? $redis->hget($self->{hash_key}, $member)
122             : $redis->get("$key:$member");
123 0 0       0 return unless $packed_score;
124 0         0 return $self->_unpack_scores($packed_score);
125             }
126              
127             sub incr_score {
128 0     0 0 0 my ($self, $member, $scores) = @_;
129 0         0 my $redis = $self->{redis};
130 0         0 my $key = $self->{key};
131 0         0 my $order = $self->{order};
132 0         0 my @new_scores;
133              
134 0   0     0 $scores ||= [1];
135 0 0       0 unless (ref $scores) {
136 0         0 $scores = [$scores];
137             }
138              
139 0 0       0 if ($self->{use_hash}) {
140 0         0 my $hash_key = $self->{hash_key};
141 0 0       0 if ($self->{use_script}) {
142             my $script = $self->{_incr_score_hash_script} ||= Redis::Script->new(
143             use_evalsha => $self->{use_evalsha},
144 0   0     0 script => <
145             local s=redis.call('HGET',KEYS[2],ARGV[1]) or ''
146             if s~=ARGV[3] then
147             return 0
148             end
149             if s~='' then
150             redis.call('ZREM',KEYS[1],s..ARGV[1])
151             end
152             redis.call('ZADD',KEYS[1],0,ARGV[2]..ARGV[1])
153             redis.call('HSET',KEYS[2],ARGV[1],ARGV[2])
154             return 1
155             EOS
156             );
157 0         0 for (1..10) {
158 0         0 my $old_packed_score = $redis->hget($hash_key, $member);
159 0         0 my @old_scores;
160 0 0       0 if ($old_packed_score) {
161 0         0 @old_scores = $self->_unpack_scores($old_packed_score);
162 0     0   0 $redis->zrem($key, "$old_packed_score$member", sub {});
163             }
164              
165 0         0 for my $i (0..scalar(@$order)-1) {
166 0   0     0 push @new_scores, ($old_scores[$i] || 0) + ($scores->[$i] || 0);
      0        
167             }
168 0         0 my $packed_score = $self->_pack_scores(\@new_scores);
169 0 0 0     0 if ($script->eval($redis, [$key, $hash_key], [$member, $packed_score, $old_packed_score || ''])) {
170 0         0 last;
171             }
172             }
173             } else {
174             watch_multi_exec $redis, [$hash_key], 10, sub {
175 0     0   0 return $redis->hget($hash_key, $member);
176             }, sub {
177 0     0   0 my (undef, $old_packed_score) = @_;
178 0         0 my @old_scores;
179 0 0       0 if ($old_packed_score) {
180 0         0 @old_scores = $self->_unpack_scores($old_packed_score);
181 0         0 $redis->zrem($key, "$old_packed_score$member", sub {});
182             }
183              
184 0         0 for my $i (0..scalar(@$order)-1) {
185 0   0     0 push @new_scores, ($old_scores[$i] || 0) + ($scores->[$i] || 0);
      0        
186             }
187 0         0 my $packed_score = $self->_pack_scores(\@new_scores);
188 0         0 $redis->zadd($key, 0, "$packed_score$member", sub {});
189 0         0 $redis->hset($hash_key, $member, $packed_score, sub {});
190 0         0 };
191             }
192             } else {
193 0         0 my $sub_sort_key = "$key:$member";
194             watch_multi_exec $redis, [$sub_sort_key], 10, sub {
195 0     0   0 return $redis->get($sub_sort_key);
196             }, sub {
197 0     0   0 my (undef, $old_packed_score) = @_;
198 0         0 my @old_scores;
199 0 0       0 if ($old_packed_score) {
200 0         0 @old_scores = $self->_unpack_scores($old_packed_score);
201 0         0 $redis->zrem($key, "$old_packed_score$member", sub {});
202             }
203              
204 0         0 for my $i (0..scalar(@$order)-1) {
205 0   0     0 push @new_scores, ($old_scores[$i] || 0) + ($scores->[$i] || 0);
      0        
206             }
207 0         0 my $packed_score = $self->_pack_scores(\@new_scores);
208 0         0 $redis->zadd($key, 0, "$packed_score$member", sub {});
209 0         0 $redis->set($sub_sort_key, $packed_score, sub {});
210 0         0 };
211             }
212 0         0 $self->_set_expire_and_limit($member);
213              
214 0 0       0 return wantarray ? @new_scores : $new_scores[0];
215             }
216              
217             sub decr_score {
218 0     0 0 0 my ($self, $member, $scores) = @_;
219 0   0     0 $scores ||= [1];
220 0 0       0 unless (ref $scores) {
221 0         0 $scores = [$scores];
222             }
223 0         0 for my $i (0..scalar(@$scores)-1) {
224 0         0 $scores->[$i] *= -1;
225             }
226 0         0 return $self->incr_score($member, $scores);
227             }
228              
229             sub _set_expire_and_limit {
230 0     0   0 my ($self, $member) = @_;
231 0         0 my $redis = $self->{redis};
232 0 0       0 if (my $expire_at = $self->{expire_at}) {
233 0         0 $redis->expireat($self->{key}, $expire_at);
234 0 0       0 if ($self->{use_hash}) {
235 0         0 $redis->expireat($self->{hash_key}, $expire_at);
236             } else {
237 0         0 $redis->expireat($self->{key}.":".$member, $expire_at);
238             }
239             }
240              
241 0 0       0 if ($self->{limit}) {
242 0         0 $self->_set_limit;
243             }
244             }
245              
246             sub _set_limit {
247 0     0   0 my $self = shift;
248 0         0 my $redis = $self->{redis};
249              
250 0         0 my $limit = $self->{limit};
251 0         0 my $key = $self->{key};
252 0         0 my $scorelen = @{$self->{order}}*8;
  0         0  
253 0 0       0 if ($self->{use_hash}) {
254 0         0 my $hash_key = $self->{hash_key};
255 0 0       0 if ($self->{use_script}) {
256             my $script = $self->{_limit_script} ||= Redis::Script->new(
257             use_evalsha => $self->{use_evalsha},
258 0   0     0 script => <
259             local k=KEYS[1]
260             local l=ARGV[1]
261             local s=redis.call('ZRANGE',k,l,-1)
262             if #s==0 then
263             return
264             end
265             for i=1,#s do
266             s[i]=string.sub(s[i],ARGV[2])
267             end
268             redis.call('HDEL',KEYS[2],unpack(s))
269             redis.call('ZREMRANGEBYRANK',k,l,-1)
270             EOS
271             );
272 0         0 $script->eval($redis, [$key, $hash_key], [$limit, $scorelen]);
273             } else {
274             watch_multi_exec $redis, [$key], 10, sub {
275 0     0   0 return $redis->zrange($key, $limit, -1);
276             }, sub {
277 0     0   0 shift; #ignore $redis
278 0 0       0 return unless @_;
279 0         0 $redis->hdel($hash_key, (map { substr $_, $scorelen } @_), sub {});
  0         0  
280 0         0 $redis->zremrangebyrank($key, $limit, -1, sub {});
281 0         0 };
282             }
283             } else {
284 0 0       0 if ($self->{use_script}) {
285             my $script = $self->{_limit_script} ||= Redis::Script->new(
286             use_evalsha => $self->{use_evalsha},
287 0   0     0 script => <
288             local k=KEYS[1]
289             local l=ARGV[1]
290             local s=redis.call('ZRANGE',k,l,-1)
291             if #s==0 then
292             return
293             end
294             for i=1,#s do
295             s[i]=k..":"..string.sub(s[i],ARGV[2])
296             end
297             redis.call('DEL',unpack(s))
298             redis.call('ZREMRANGEBYRANK',k,l,-1)
299             EOS
300             );
301 0         0 $script->eval($redis, [$key], [$limit, $scorelen]);
302             } else {
303             watch_multi_exec $redis, [$key], 10, sub {
304 0     0   0 return $redis->zrange($key, $limit, -1);
305             }, sub {
306 0     0   0 shift; #ignore $redis
307 0 0       0 return unless @_;
308 0         0 $redis->del((map { $key.":".substr($_, $scorelen) } @_), sub {});
  0         0  
309 0         0 $redis->zremrangebyrank($key, $limit, -1, sub {});
310 0         0 };
311             }
312             }
313             }
314              
315             sub remove {
316 0     0 0 0 my ($self, $member) = @_;
317 0         0 my $redis = $self->{redis};
318 0         0 my $key = $self->{key};
319              
320 0 0       0 if ($self->{use_hash}) {
321 0         0 my $hash_key = $self->{hash_key};
322 0 0       0 if ($self->{use_script}) {
323             my $script = $self->{_remove_hash_script} ||= Redis::Script->new(
324             use_evalsha => $self->{use_evalsha},
325 0   0     0 script => <
326             local s=redis.call('HGET',KEYS[2],ARGV[1])
327             if s then
328             redis.call('ZREM',KEYS[1],s..ARGV[1])
329             redis.call('HDEL',KEYS[2],ARGV[1])
330             end
331             EOS
332             );
333 0         0 $script->eval($redis, [$key, $hash_key], [$member]);
334             } else {
335             watch_multi_exec $redis, [$hash_key], 10, sub {
336 0     0   0 return $redis->hget($hash_key, $member);
337             }, sub {
338 0     0   0 my (undef, $packed_score) = @_;
339 0 0       0 if ($packed_score) {
340 0         0 $redis->zrem($key, "$packed_score$member");
341 0         0 $redis->hdel($hash_key, $member);
342             }
343 0         0 };
344             }
345             } else {
346 0         0 my $sub_sort_key = "$key:$member";
347 0 0       0 if ($self->{use_script}) {
348             my $script = $self->{_remove_script} ||= Redis::Script->new(
349             use_evalsha => $self->{use_evalsha},
350 0   0     0 script => <
351             local s=redis.call('GET',KEYS[2])
352             if s then
353             redis.call('ZREM',KEYS[1],s..ARGV[1])
354             redis.call('DEL',KEYS[2])
355             end
356             EOS
357             );
358 0         0 $script->eval($redis, [$key, $sub_sort_key], [$member]);
359             } else {
360             watch_multi_exec $redis, [$sub_sort_key], 10, sub {
361 0     0   0 return $redis->get($sub_sort_key);
362             }, sub {
363 0     0   0 my (undef, $packed_score) = @_;
364 0 0       0 if ($packed_score) {
365 0         0 $redis->zrem($key, "$packed_score$member");
366 0         0 $redis->del($sub_sort_key);
367             }
368 0         0 };
369             }
370             }
371             }
372              
373             sub get_sorted_order {
374 0     0 0 0 my ($self, $member) = @_;
375 0         0 my $redis = $self->{redis};
376 0         0 my $key = $self->{key};
377 0         0 my $order;
378              
379 0 0       0 if ($self->{use_hash}) {
380 0         0 my $hash_key = $self->{hash_key};
381 0 0       0 if ($self->{use_script}) {
382             my $script = $self->{_get_sort_order_hash_script} ||= Redis::Script->new(
383             use_evalsha => $self->{use_evalsha},
384 0   0     0 script => <
385             local s=redis.call('HGET',KEYS[2],ARGV[1])
386             return redis.call('ZRANK',KEYS[1],s..ARGV[1])
387             EOS
388             );
389 0         0 $order = $script->eval($redis, [$key, $hash_key], [$member]);
390             } else {
391 0         0 my $packed_score = $redis->hget($hash_key, $member);
392 0         0 $order = $redis->zrank($key, "$packed_score$member");
393             }
394             } else {
395 0         0 my $sub_sort_key = "$key:$member";
396 0 0       0 if ($self->{use_script}) {
397             my $script = $self->{_get_sort_order_script} ||= Redis::Script->new(
398             use_evalsha => $self->{use_evalsha},
399 0   0     0 script => <
400             local s=redis.call('GET',KEYS[2])
401             return redis.call('ZRANK',KEYS[1],s..ARGV[1])
402             EOS
403             );
404 0         0 $order = $script->eval($redis, [$key, $sub_sort_key], [$member]);
405             } else {
406             ($order) = watch_multi_exec $redis, [$sub_sort_key], 10, sub {
407 0     0   0 return $redis->get($sub_sort_key);
408             }, sub {
409 0     0   0 my (undef, $packed_score) = @_;
410 0         0 $redis->zrank($key, "$packed_score$member", sub {});
411 0         0 };
412             }
413             }
414 0         0 return $order;
415             }
416              
417             sub get_rank {
418 0     0 0 0 my ($self, $member) = @_;
419 0         0 my ($rank) = $self->get_rank_with_score($member);
420 0         0 return $rank;
421             }
422              
423             sub get_rank_with_score {
424 0     0 0 0 my ($self, $member) = @_;
425 0         0 my $redis = $self->{redis};
426 0         0 my $key = $self->{key};
427 0         0 my $sub_sort_key = "$key:$member";
428              
429 0         0 my $rank;
430             my $packed_score;
431 0 0       0 if ($self->{use_hash}) {
432 0         0 my $hash_key = $self->{hash_key};
433 0 0       0 if ($self->{use_script}) {
434             my $script = $self->{_get_rank_with_score_hash_script} ||= Redis::Script->new(
435             use_evalsha => $self->{use_evalsha},
436 0   0     0 script => <
437             local s=redis.call('HGET',KEYS[2],ARGV[1])
438             if not s then
439             return {nil, nil}
440             end
441             return {s,redis.call('ZLEXCOUNT',KEYS[1],'-','('..s)}
442             EOS
443             );
444 0         0 ($packed_score, $rank) = $script->eval($redis, [$key, $hash_key], [$member]);
445             } else {
446 0         0 $packed_score = $redis->hget($hash_key, $member);
447 0 0       0 $rank = $redis->zlexcount($key, '-', "($packed_score") if $packed_score;
448             }
449             } else {
450 0         0 my $sub_sort_key = "$key:$member";
451 0 0       0 if ($self->{use_script}) {
452             my $script = $self->{_get_rank_with_score_script} ||= Redis::Script->new(
453             use_evalsha => $self->{use_evalsha},
454 0   0     0 script => <
455             local s=redis.call('GET',KEYS[2])
456             if not s then
457             return {nil, nil}
458             end
459             return {s,redis.call('ZLEXCOUNT',KEYS[1],'-','('..s)}
460             EOS
461             );
462 0         0 ($packed_score, $rank) = $script->eval($redis, [$key, $sub_sort_key], []);
463             } else {
464             ($rank) = watch_multi_exec $redis, [$sub_sort_key], 10, sub {
465 0     0   0 $packed_score = $redis->get($sub_sort_key);
466             }, sub {
467 0 0   0   0 $redis->zlexcount($key, '-', "($packed_score") if $packed_score;
468 0         0 };
469             }
470             }
471              
472 0 0 0     0 return if !defined $rank or !defined $packed_score;
473 0         0 return $rank + 1, $self->_unpack_scores($packed_score);
474             }
475              
476             sub get_rank_by_score {
477 0     0 0 0 my ($self, $scores) = @_;
478 0         0 my $redis = $self->{redis};
479 0         0 my $key = $self->{key};
480              
481 0         0 my $packed_score = $self->_pack_scores($scores);
482 0         0 my $rank = $redis->zlexcount($key, '-', "[$packed_score");
483              
484 0         0 return $rank + 1;
485             }
486              
487             sub member_count {
488 0     0 0 0 my ($self, $from, $to) = @_;
489              
490 0 0 0     0 if (!$from && !$to) {
491 0         0 $self->{redis}->zcard($self->{key});
492             }
493             else {
494 0 0       0 $from = defined $from ? $from : '-inf';
495 0 0       0 $to = defined $to ? $to : 'inf';
496 0         0 $self->{redis}->zcount($self->{key}, $from, $to);
497             }
498             }
499              
500             sub rankings {
501 0     0 0 0 my ($self, %args) = @_;
502 0 0       0 my $limit = exists $args{limit} ? $args{limit} : $self->member_count;
503 0 0       0 my $offset = exists $args{offset} ? $args{offset} : 0;
504              
505 0         0 my $members_with_scores = $self->{redis}->zrange($self->{key}, $offset, $offset + $limit - 1);
506 0 0       0 return [] unless @$members_with_scores;
507              
508 0         0 my @rankings;
509 0         0 my ($current_rank, $current_target_scores, $same_score_members);
510 0         0 for my $member_with_score (@$members_with_scores) {
511 0         0 my $scores = [$self->_unpack_scores($member_with_score)];
512 0         0 my $member = substr $member_with_score, 8*@$scores;
513              
514 0 0       0 if (!$current_rank) {
    0          
515 0         0 $current_rank = $self->get_rank_by_score($scores);
516 0         0 $same_score_members = $offset - $current_rank + 2;
517 0         0 $current_target_scores = $scores;
518             }
519 0         0 elsif (!grep { $scores->[$_] != $current_target_scores->[$_] } 0..@$scores-1) {
520 0         0 $same_score_members++;
521             }
522             else {
523 0         0 $current_target_scores = $scores;
524 0         0 $current_rank = $current_rank + $same_score_members;
525 0         0 $same_score_members = 1;
526             }
527 0         0 push @rankings, +{
528             member => $member,
529             score => $scores->[0],
530             rank => $current_rank,
531             scores => $scores,
532             };
533             }
534              
535 0         0 \@rankings;
536             }
537              
538             sub find_member {
539 0     0 0 0 my ($self, $member) = @_;
540              
541 0         0 Redis::LeaderBoardMulti::Member->new(
542             member => $member,
543             leader_board => $self,
544             );
545             }
546              
547             sub _pack_scores {
548 54     54   103 my ($self, $scores) = @_;
549 54 50       70 unless(ref $scores) {
550 0         0 $scores = [$scores];
551             }
552 54         39 my $num = scalar @$scores;
553 54         41 my $order = $self->{order};
554 54 50       66 die "the number of scores is illegal" if $num != scalar @$order;
555 54 100       55 if ($SUPPORT_64BIT) {
556 49         261 return pack($self->{_pack_pattern}, @$scores) ^ $self->{_mask};
557             } else {
558             return pack(
559             $self->{_pack_pattern},
560             # sign extension
561 5 100       27 map { ($_<0?-1:0), $_ } @$scores
562 5         6 ) ^ $self->{_mask};
563             }
564             }
565              
566             sub _unpack_scores {
567 16     16   35 my ($self, $packed_score) = @_;
568 16         42 my @scores = unpack($self->{_unpack_pattern}, $packed_score ^ $self->{_mask});
569 16 100       19 if ($SUPPORT_64BIT) {
570 12 50       39 return wantarray ? @scores : $scores[0];
571             } else {
572 4         2 my @s;
573 4         9 for (my $i = 0; $i < @scores; $i += 2) {
574             # check overflow
575 4 100 100     25 if (($scores[$i+1]>=0&&$scores[$i]!=0) || ($scores[$i+1]<0&&$scores[$i]!=-1)) {
      100        
      66        
576 2         29 carp "[Redis::LeaderBoardMulti] score overflow";
577             }
578 4         666 push @s, $scores[$i+1];
579             }
580 4 50       22 return wantarray ? @s : $s[0];
581             }
582             }
583              
584              
585             1;
586             __END__