File Coverage

blib/lib/BusyBird/StatusStorage/Memory.pm
Criterion Covered Total %
statement 166 207 80.1
branch 81 98 82.6
condition 23 26 88.4
subroutine 24 31 77.4
pod 6 6 100.0
total 300 368 81.5


line stmt bran cond sub pod time code
1             package BusyBird::StatusStorage::Memory;
2 2     2   82392 use strict;
  2         3  
  2         67  
3 2     2   8 use warnings;
  2         2  
  2         50  
4 2     2   8 use parent ('BusyBird::StatusStorage');
  2         3  
  2         12  
5 2     2   98 use BusyBird::Util qw(set_param sort_statuses);
  2         3  
  2         131  
6 2     2   9 use BusyBird::Log qw(bblog);
  2         2  
  2         82  
7 2     2   666 use BusyBird::StatusStorage::Common qw(contains ack_statuses get_unacked_counts);
  2         4  
  2         121  
8 2     2   10 use BusyBird::DateTime::Format;
  2         2  
  2         35  
9 2     2   595 use Storable qw(dclone);
  2         2755  
  2         108  
10 2     2   9 use Carp;
  2         2  
  2         86  
11 2     2   8 use List::Util qw(min);
  2         2  
  2         137  
12 2     2   604 use JSON;
  2         9120  
  2         10  
13 2     2   212 use Try::Tiny;
  2         3  
  2         479  
14              
15             sub new {
16 3     3 1 1411 my ($class, %options) = @_;
17 3         13 my $self = bless {
18             timelines => {}, ## timelines should always be sorted.
19             }, $class;
20 3         17 $self->set_param(\%options, 'max_status_num', 2000);
21 3 50       13 if($self->{max_status_num} <= 0) {
22 0         0 croak "max_status_num option must be bigger than 0.";
23             }
24 3         10 return $self;
25             }
26              
27             sub _log {
28 0     0   0 my ($self, $level, $msg) = @_;
29 0         0 bblog($level, $msg);
30             }
31              
32             sub _index {
33 1057     1057   1334 my ($self, $timeline, $id) = @_;
34 1057 100       2259 return -1 if not defined($self->{timelines}{$timeline});
35 1012         1134 my $tl = $self->{timelines}{$timeline};
36 1012         2321 my @ret = grep { $tl->[$_]{id} eq $id } 0..$#$tl;
  25080         29019  
37 1012 50       2364 confess "multiple IDs in timeline $timeline." if int(@ret) >= 2;
38 1012 100       2113 return int(@ret) == 0 ? -1 : $ret[0];
39             }
40              
41             sub _acked {
42 4930     4930   3814 my ($self, $status) = @_;
43 2     2   10 no autovivification;
  2         3  
  2         9  
44 4930         14357 return $status->{busybird}{acked_at};
45             }
46              
47             sub save {
48 0     0 1 0 my ($self, $filepath) = @_;
49 0 0       0 if(not defined($filepath)) {
50 0         0 croak '$filepath is not specified.';
51             }
52 0         0 my $file;
53 0 0       0 if(!open $file, ">", $filepath) {
54 0         0 $self->_log("error", "Cannot open $filepath to write.");
55 0         0 return 0;
56             }
57 0         0 my $success;
58             try {
59 0     0   0 print $file encode_json($self->{timelines});
60 0         0 $success = 1;
61             }catch {
62 0     0   0 my $e = shift;
63 0         0 $self->_log("error", "Error while saving: $e");
64 0         0 $success = 0;
65 0         0 };
66 0         0 close $file;
67 0         0 return $success;
68             }
69              
70             sub load {
71 0     0 1 0 my ($self, $filepath) = @_;
72 0 0       0 if(not defined($filepath)) {
73 0         0 croak '$filepath is not specified.';
74             }
75 0         0 my $file;
76 0 0       0 if(!open $file, "<", $filepath) {
77 0         0 $self->_log("notice", "Cannot open $filepath to read");
78 0         0 return 0;
79             }
80 0         0 my $success;
81             try {
82 0     0   0 my $text = do { local $/; <$file> };
  0         0  
  0         0  
83 0         0 $self->{timelines} = decode_json($text);
84 0         0 $success = 1;
85             }catch {
86 0     0   0 my $e = shift;
87 0         0 $self->_log("error", "Error while loading: $e");
88 0         0 $success = 0;
89 0         0 };
90 0         0 close $file;
91 0         0 return $success;
92             }
93              
94             sub _is_timestamp_format_ok {
95 1799     1799   2224 my ($timestamp_str) = @_;
96 1799 100       4093 return 1 if not defined $timestamp_str;
97            
98             ## It is very inefficient to parse $timestamp_str to check its
99             ## format, because creating a DateTime object takes long time. We
100             ## do it because BB::SS::Memory is just a reference
101             ## implementation.
102 1318         3175 return defined(BusyBird::DateTime::Format->parse_datetime($timestamp_str));
103             }
104              
105             sub put_statuses {
106 121     121 1 20815 my ($self, %args) = @_;
107 121 100       454 croak 'timeline arg is mandatory' if not defined $args{timeline};
108 120         215 my $timeline = $args{timeline};
109 120 100 100     802 if(!defined($args{mode}) ||
      66        
      66        
110             ($args{mode} ne 'insert'
111             && $args{mode} ne 'update' && $args{mode} ne 'upsert')) {
112 1         85 croak 'mode arg must be insert/update/upsert';
113             }
114 119         201 my $mode = $args{mode};
115 119         157 my $statuses;
116 119 100       627 if(!defined($args{statuses})) {
    100          
    50          
117 1         86 croak 'statuses arg is mandatory';
118             }elsif(ref($args{statuses}) eq 'HASH') {
119 28         59 $statuses = [ $args{statuses} ];
120             }elsif(ref($args{statuses}) eq 'ARRAY') {
121 90         184 $statuses = $args{statuses};
122             }else {
123 0         0 croak 'statuses arg must be STATUS/ARRAYREF_OF_STATUSES';
124             }
125 118         222 foreach my $s (@$statuses) {
126 2     2   920 no autovivification;
  2         2  
  2         34  
127 919 100       313464 croak "{id} field is mandatory in statuses" if not defined $s->{id};
128 904 100 100     3667 croak "{busybird} field must be a hash-ref if present" if defined($s->{busybird}) && ref($s->{busybird}) ne "HASH";
129 901 100       1706 croak "{created_at} field must be parsable by BusyBird::DateTime::Format" if !_is_timestamp_format_ok($s->{created_at});
130 898         696590 my $acked_at = $s->{busybird}{acked_at}; ## avoid autovivification
131 898 100       1852 croak "{busybird}{acked_at} field must be parsable by BusyBird::DateTime::Format" if !_is_timestamp_format_ok($acked_at);
132             }
133 94         25759 my $put_count = 0;
134 94         297 foreach my $status_index (reverse 0 .. $#$statuses) {
135 880         943 my $s = $statuses->[$status_index];
136 880         1807 my $tl_index = $self->_index($timeline, $s->{id});
137 880         876 my $existent = ($tl_index >= 0);
138 880 100 100     4162 next if ($mode eq 'insert' && $existent) || ($mode eq 'update' && !$existent);
      100        
      66        
139 869         863 my $is_insert = ($mode eq 'insert');
140 869 100       1344 if($mode eq 'upsert') {
141 24         23 $is_insert = (!$existent);
142             }
143 869 100       1064 if($is_insert) {
144 548         400 unshift(@{$self->{timelines}{$timeline}}, dclone($s));
  548         7125  
145             }else {
146             ## update
147 321         4609 $self->{timelines}{$timeline}[$tl_index] = dclone($s);
148             }
149 869         1564 $put_count++;
150             }
151 94 100       276 if($put_count > 0) {
152 90         440 $self->{timelines}{$timeline} = sort_statuses($self->{timelines}{$timeline});
153 90 100       223 if(int(@{$self->{timelines}{$timeline}}) > $self->{max_status_num}) {
  90         407  
154 3         7 splice(@{$self->{timelines}{$timeline}}, -(int(@{$self->{timelines}{$timeline}}) - $self->{max_status_num}));
  3         11  
  3         28  
155             }
156             }
157 94 50       313 if($args{callback}) {
158 94         234 @_ = (undef, $put_count);
159 94         507 goto $args{callback};
160             }
161             }
162              
163             sub delete_statuses {
164 58     58 1 12274 my ($self, %args) = @_;
165 58 100       270 croak 'timeline arg is mandatory' if not defined $args{timeline};
166 57 100       253 croak 'ids arg is mandatory' if not exists $args{ids};
167 56         125 my $timeline = $args{timeline};
168 56         86 my $ids = $args{ids};
169 56 100       143 if(defined($ids)) {
170 7 100       27 if(!ref($ids)) {
    50          
171 2         4 $ids = [$ids];
172             }elsif(ref($ids) eq 'ARRAY') {
173 5 100       12 croak "ids arg array must not contain undef" if grep { !defined($_) } @$ids;
  14         111  
174             }else {
175 0         0 croak "ids must be undef/ID/ARRAYREF_OF_IDS";
176             }
177             }
178 55 100       209 if(!$self->{timelines}{$timeline}) {
179 14 50       43 if($args{callback}) {
180 14         37 @_ = (undef, 0);
181 14         58 goto $args{callback};
182             }
183 0         0 return;
184             }
185 41         63 my $delete_num = 0;
186 41 100       89 if(defined($ids)) {
187 5         10 foreach my $id (@$ids) {
188 8         18 my $tl_index = $self->_index($timeline, $id);
189 8 100       14 last if $tl_index < 0;
190 7         10 splice(@{$self->{timelines}{$timeline}}, $tl_index, 1);
  7         16  
191 7         19 $delete_num++;
192             }
193             }else {
194 36 50       120 if(defined($self->{timelines}{$timeline})) {
195 36         43 $delete_num = @{$self->{timelines}{$timeline}};
  36         82  
196 36         400 delete $self->{timelines}{$timeline};
197             }
198             }
199 41 50       132 if($args{callback}) {
200 41         93 @_ = (undef, $delete_num);
201 41         142 goto $args{callback};
202             }
203             }
204              
205             sub get_statuses {
206 538     538 1 21589 my ($self, %args) = @_;
207 538 100       1524 croak 'timeline arg is mandatory' if not defined $args{timeline};
208 537 100       1149 croak 'callback arg is mandatory' if not defined $args{callback};
209 536         651 my $timeline = $args{timeline};
210 536 100       1566 if(!$self->{timelines}{$timeline}) {
211 68         140 @_ = (undef, []);
212 68         207 goto $args{callback};
213             }
214 468   100     1087 my $ack_state = $args{ack_state} || 'any';
215 468         529 my $max_id = $args{max_id};
216 468 50       960 my $count = defined($args{count}) ? $args{count} : 20;
217             my $ack_test = $ack_state eq 'unacked' ? sub {
218 2866     2866   3191 !$self->_acked(shift);
219             } : $ack_state eq 'acked' ? sub {
220 2064     2064   2446 $self->_acked(shift);
221 468 100   3121   1940 } : sub { 1 };
  3121 100       7364  
222 468         602 my $start_index;
223 468 100       991 if(defined($max_id)) {
224 169         344 my $tl_index = $self->_index($timeline, $max_id);
225 169 100       318 if($tl_index < 0) {
226 23         48 @_ = (undef, []);
227 23         109 goto $args{callback};
228             }
229 146         242 my $s = $self->{timelines}{$timeline}[$tl_index];
230 146 100       278 if(!$ack_test->($s)) {
231 40         257 @_ = (undef, []);
232 40         212 goto $args{callback};
233             }
234 106         162 $start_index = $tl_index;
235             }
236             my @indice = grep {
237 7905 100 100     10484 if(!$ack_test->($self->{timelines}{$timeline}[$_])) {
  405 100       1151  
238 2283         2793 0;
239             }elsif(defined($start_index) && $_ < $start_index) {
240 726         824 0;
241             }else {
242 4896         5446 1;
243             }
244 405         530 } 0 .. $#{$self->{timelines}{$timeline}};
245 405 100       1112 $count = int(@indice) if $count eq 'all';
246 405         1037 $count = min($count, int(@indice));
247 4382         41246 my $result_statuses = $count <= 0 ? [] : [ map {
248 405 100       1284 dclone($self->{timelines}{$timeline}[$_])
249             } @indice[0 .. ($count-1)] ];
250              
251 405         1072 @_ = (undef, $result_statuses);
252 405         2351 goto $args{callback};
253             }
254              
255             1;
256              
257             __END__