File Coverage

blib/lib/CHI/Cascade.pm
Criterion Covered Total %
statement 181 225 80.4
branch 74 124 59.6
condition 34 80 42.5
subroutine 26 31 83.8
pod 6 19 31.5
total 321 479 67.0


line stmt bran cond sub pod time code
1             package CHI::Cascade;
2              
3 15     15   1983279 use strict;
  15         86  
  15         441  
4 15     15   78 use warnings;
  15         31  
  15         572  
5              
6             our $VERSION = 0.300_004;
7              
8 15     15   84 use Carp;
  15         30  
  15         966  
9              
10 15     15   3631 use CHI::Cascade::Value ':state';
  15         46  
  15         2617  
11 15     15   6646 use CHI::Cascade::Rule;
  15         39  
  15         485  
12 15     15   6235 use CHI::Cascade::Target;
  15         42  
  15         416  
13 15     15   91 use Time::HiRes ();
  15         31  
  15         191  
14 15     15   7953 use POSIX ();
  15         128935  
  15         49689  
15              
16 0 0   0 0 0 sub min ($$) { $_[0] < $_[1] ? $_[0] : $_[1] }
17              
18             sub new {
19 1     1 1 131283 my ($class, %opts) = @_;
20              
21 1   33     48 my $self = bless {
22             %opts,
23             plain_targets => {},
24             qr_targets => [],
25             stats => { recompute => 0, run => 0, dependencies_lookup => 0 }
26              
27             }, ref($class) || $class;
28              
29 1   33     30 $self->{target_chi} ||= $self->{chi};
30              
31 1         6 $self;
32             }
33              
34             sub rule {
35 3     3 1 1277 my ($self, %opts) = @_;
36              
37 3         44 my $rule = CHI::Cascade::Rule->new( cascade => $self, %opts );
38              
39 3 100       15 if (ref($rule->{target}) eq 'Regexp') {
    50          
40 1         12 push @{ $self->{qr_targets} }, $rule;
  1         9  
41             }
42             elsif (! ref($rule->{target})) {
43 2         11 $self->{plain_targets}{$rule->{target}} = $rule;
44             }
45             else {
46 0         0 croak qq{The rule's target "$rule->{target}" is unknown type};
47             }
48             }
49              
50             sub target_computing {
51 29     29 0 43 my $trg_obj;
52              
53             ( $trg_obj = $_[0]->{target_chi}->get("t:$_[1]") )
54 29 50       126 ? ( ( ${ $_[2] } = $trg_obj->ttl ), $trg_obj->locked ? 1 : 0 )
  25 100       78  
55             : 0;
56             }
57              
58             sub target_is_actual {
59 6     6 0 17 my ( $self, $target, $actual_term ) = @_;
60              
61 6         10 my $trg_obj;
62              
63 6 100       39 ( $trg_obj = $self->{target_chi}->get("t:$target") )
64             ? $trg_obj->is_actual( $actual_term )
65             : 0;
66             }
67              
68             sub target_time {
69 40     40 0 76 my ($self, $target) = @_;
70              
71 40         64 my $trg_obj;
72              
73 40 100       136 return ( ( $trg_obj = $self->{target_chi}->get("t:$target") )
74             ? $trg_obj->time
75             : 0
76             );
77             }
78              
79             sub get_value {
80 11     11 0 25 my ($self, $target) = @_;
81              
82 11         40 my $value = $self->{chi}->get("v:$target");
83              
84 11 50       3662 return $value->state( CASCADE_FROM_CACHE )
85             if ($value);
86              
87 0         0 CHI::Cascade::Value->new( state => CASCADE_NO_CACHE );
88             }
89              
90             sub target_lock {
91 11     11 0 649 my ( $self, $rule ) = @_;
92              
93 11         34 my $target = $rule->target;
94              
95             # If target is already locked - a return
96             return
97 11 100       50 if ( $self->target_locked( $rule ) );
98              
99 8         18 my $trg_obj;
100             $trg_obj = CHI::Cascade::Target->new
101 8 100       29 unless ( ( $trg_obj = $self->{target_chi}->get("t:$target") ) );
102              
103 8         1180 $trg_obj->lock;
104 8         35 $self->{target_chi}->set( "t:$target", $trg_obj, $rule->target_expires( $trg_obj ) );
105              
106 8         7170 $rule->{run_instance}{target_locks}{$target} = 1;
107             }
108              
109             sub target_unlock {
110 8     8 0 17 my ( $self, $rule, $value ) = @_;
111              
112 8         19 my $target = $rule->target;
113              
114 8 50       63 if ( my $trg_obj = $self->{target_chi}->get( "t:$target" ) ) {
115 8         2502 $trg_obj->unlock;
116              
117 8 50 33     42 if ( $value && $value->state & CASCADE_RECOMPUTED ) {
118 8         34 $trg_obj->touch;
119             $trg_obj->actual_stamp
120 8 100 66     42 if $rule->{run_instance}{run_opts}{actual_term} && $rule->{run_instance}{orig_target} eq $target;
121             }
122              
123 8         30 $self->{target_chi}->set( "t:$target", $trg_obj, $rule->target_expires( $trg_obj ) );
124             }
125              
126 8         16798 delete $rule->{run_instance}{target_locks}{$target};
127             }
128              
129             sub target_actual_stamp {
130 0     0 0 0 my ( $self, $rule, $value ) = @_;
131              
132 0         0 my $target = $rule->target;
133              
134 0 0 0     0 if ( $value && $value->state & CASCADE_ACTUAL_VALUE && ( my $trg_obj = $self->{target_chi}->get( "t:$target" ) ) ) {
      0        
135 0         0 $trg_obj->actual_stamp;
136 0         0 $self->{target_chi}->set( "t:$target", $trg_obj, $rule->target_expires( $trg_obj ) );
137             }
138             }
139              
140             sub target_start_ttl {
141 0     0 0 0 my ( $self, $rule, $start_time ) = @_;
142              
143 0         0 my $target = $rule->target;
144              
145 0 0       0 if ( my $trg_obj = $self->{target_chi}->get( "t:$target" ) ) {
146 0         0 $trg_obj->ttl( $rule->ttl, $start_time );
147 0         0 $self->{target_chi}->set( "t:$target", $trg_obj, $rule->target_expires( $trg_obj ) );
148             }
149             }
150              
151             sub target_remove {
152 0     0 1 0 my ($self, $target) = @_;
153              
154 0         0 $self->{target_chi}->remove("t:$target");
155             }
156              
157             sub touch {
158 2     2 1 12 my ( $self, $target ) = @_;
159              
160 2 50       25 if ( my $trg_obj = $self->{target_chi}->get("t:$target") ) {
161 2         974 $trg_obj->touch;
162 2         14 $self->{target_chi}->set( "t:$target", $trg_obj, $self->find( $target )->target_expires( $trg_obj ) );
163             }
164             }
165              
166             sub target_locked {
167 91     91 0 199 my ( $self, $rule ) = @_;
168              
169 91         227 exists $rule->{run_instance}{target_locks}{ $rule->target };
170             }
171              
172             sub recompute {
173 8     8 0 33 my ( $self, $rule, $target, $dep_values) = @_;
174              
175             die CHI::Cascade::Value->new( state => CASCADE_DEFERRED )
176 8 50       27 if $rule->{run_instance}{run_opts}{defer};
177              
178 8         13 my $ret = eval { $rule->{code}->( $rule, $target, $rule->{dep_values} = $dep_values ) };
  8         36  
179              
180 8         71 $self->{stats}{recompute}++;
181              
182 8 50       19 if ($@) {
183 0         0 my $error = $@;
184 0 0       0 die( ( eval { $error->isa('CHI::Cascade::Value') } ) ? $error->thrown_from_code(1) : "CHI::Cascade: the target $target - error in the code: $error" );
  0         0  
185             }
186              
187 8         10 my $value;
188              
189             # For performance a value should not expire in anyway (only target marker if need)
190 8         41 $self->{chi}->set( "v:$target", $value = CHI::Cascade::Value->new->value($ret), 'never' );
191              
192 8         5586 $value->state( CASCADE_ACTUAL_VALUE | CASCADE_RECOMPUTED );
193              
194             $rule->{recomputed}->( $rule, $target, $value )
195 8 100       45 if ( ref $rule->{recomputed} eq 'CODE' );
196              
197 8         72 return $value;
198             }
199              
200             sub value_ref_if_recomputed {
201 29     29 0 70 my ( $self, $rule, $target, $only_from_cache ) = @_;
202              
203             return undef
204 29 50       63 unless defined $rule;
205              
206 29         46 my $run_instance = $rule->{run_instance};
207              
208 29         88 my @qr_params = $rule->qr_params;
209              
210 29         59 my ( $ret_state, $ttl, $should_be_recomputed ) = ( CASCADE_ACTUAL_VALUE );
211              
212 29 50       86 if ( $self->target_computing( $target, \$ttl ) ) {
213             # If we have any target as a being computed (dependencie/original)
214             # there is no need to compute anything - trying to return original target value
215 0         0 die CHI::Cascade::Value->new->state( CASCADE_COMPUTING );
216             }
217              
218 29         996 my ( %dep_values, $dep_name );
219              
220 29 100       66 if ( $only_from_cache ) {
221              
222             # Trying to get value from cache
223 9         25 my $value = $self->get_value($target);
224              
225 9 50       22 return $value
226             if $value->is_value;
227              
228             # If no in cache - we should recompute it again
229 0         0 $self->target_lock($rule);
230             }
231              
232 20         37 push @{ $run_instance->{target_stack} }, $target;
  20         52  
233              
234 20         32 my $ret = eval {
235 20         31 my $dep_target;
236              
237             my $catcher = sub {
238 17     17   29 my $sub = shift;
239              
240 17         69 my $ret = eval { $sub->() };
  17         32  
241              
242 17 50       45 if ($@) {
243 0         0 my $exception = $@;
244              
245             $rule->{depends_catch}->( $rule, $exception, $dep_values{$dep_target}->[0], $dep_target )
246             if ( exists $rule->{depends_catch}
247             && ref $rule->{depends_catch} eq 'CODE'
248 0 0 0     0 && eval { $exception->isa('CHI::Cascade::Value') }
  0   0     0  
      0        
249             && $exception->thrown_from_code );
250              
251 0         0 die $exception;
252             }
253              
254 17         57 return $ret;
255 20         110 };
256              
257 20 100       61 $self->target_lock($rule)
258             if ! $self->target_time($target);
259              
260 20         66 $should_be_recomputed = $self->target_locked($rule);
261              
262 20 50 33     77 if ( defined $ttl && $ttl > 0 && ! $should_be_recomputed ) {
      33        
263 0         0 $ret_state = CASCADE_TTL_INVOLVED;
264 0         0 $run_instance->{ttl} = $ttl;
265             }
266             else {
267             my (
268 20 50       51 $rule_ttl,
269             $circle_hash,
270             $start_time,
271             $min_start_time
272             ) = (
273             $rule->ttl,
274             $only_from_cache ? 'only_cache_chain' : 'chain'
275             );
276              
277 20         41 foreach my $depend (@{ $rule->depends }) {
  20         52  
278 11 50       27 $dep_target = ref($depend) eq 'CODE' ? $depend->( $rule, @qr_params ) : $depend;
279              
280 11         33 $dep_values{$dep_target}->[0] = $self->find( $dep_target, $rule->{run_instance} );
281              
282 0         0 die "Found circle dependencies (trace: " . join( '->', @{ $run_instance->{target_stack} }, $dep_target ) . ") - aborted!"
283 11 50       38 if ( exists $run_instance->{ $circle_hash }{$target}{$dep_target} );
284              
285 11         23 $run_instance->{ $circle_hash }{$target}{$dep_target} = 1;
286              
287             $catcher->( sub {
288 11 100 100 11   72 if ( ! $only_from_cache
      66        
289             && ( $start_time = ( $self->{stats}{dependencies_lookup}++,
290             ( $dep_values{$dep_target}->[1] = $self->value_ref_if_recomputed( $dep_values{$dep_target}->[0], $dep_target ) )->state & CASCADE_RECOMPUTED && Time::HiRes::time
291             || ( $start_time = $self->target_time($dep_target) ) > $self->target_time($target) && $start_time ) ) )
292             {
293 7 50 66     48 if ( ! $should_be_recomputed
      66        
      33        
      33        
294             && ! defined $ttl
295             && defined $rule_ttl
296             && $rule_ttl > 0
297             && ( $start_time + $rule_ttl ) > Time::HiRes::time )
298             {
299 0 0       0 $min_start_time = defined $min_start_time ? min( $start_time, $min_start_time ) : $start_time;
300             }
301             else {
302 7         23 $self->target_lock($rule);
303             }
304             }
305 11         84 } );
306              
307 11         102 delete $run_instance->{ $circle_hash }{$target}{$dep_target};
308             }
309              
310 20 50       58 if ( defined $min_start_time ) {
311 0         0 $ret_state = CASCADE_TTL_INVOLVED;
312 0         0 $self->target_start_ttl( $rule, $min_start_time );
313 0         0 $run_instance->{ttl} = $min_start_time + $rule_ttl - Time::HiRes::time;
314             }
315             }
316              
317 20 100       50 if ( $self->target_locked($rule) ) {
318             # We should recompute this target
319             # So we should recompute values for other dependencies
320 8         26 foreach $dep_target (keys %dep_values) {
321 7 100 66     45 if ( ! defined $dep_values{$dep_target}->[1]
322             || ! $dep_values{$dep_target}->[1]->is_value )
323             {
324 6         13 $self->{stats}{dependencies_lookup}++;
325             $catcher->( sub {
326 6 50   6   20 if ( ! ( $dep_values{$dep_target}->[1] = $self->value_ref_if_recomputed( $dep_values{$dep_target}->[0], $dep_target, 1 ) )->is_value ) {
327 0         0 $self->target_remove($dep_target);
328 0         0 return 1;
329             }
330 6         21 return 0;
331 6 50       35 } ) == 1 && return undef;
332             }
333             }
334             }
335              
336 20 100       52 return $self->recompute( $rule, $target, { map { $_ => $dep_values{$_}->[1]->value } keys %dep_values } )
  7         26  
337             if $self->target_locked($rule);
338              
339 12         73 return CHI::Cascade::Value->new( state => $ret_state );
340             };
341              
342 20         34 pop @{ $run_instance->{target_stack} };
  20         43  
343              
344 20         32 my $e = $@;
345              
346 20 100 66     49 if ( $self->target_locked($rule) ) {
    50 66        
347 8         28 $self->target_unlock( $rule, $ret );
348             }
349             elsif ( $run_instance->{run_opts}{actual_term} && ! $only_from_cache && $run_instance->{orig_target} eq $target ) {
350 0         0 $self->target_actual_stamp( $rule, $ret );
351             }
352              
353 20 50       61 die $e if $e;
354              
355 20   33     152 return $ret || CHI::Cascade::Value->new;
356             }
357              
358             sub stash {
359 0     0 1 0 warn "The Cascade::stash method is deprecated! Please use the Cascade::Rule::stash method for this!\n";
360 0 0 0     0 exists $_[0]->{stash} && $_[0]->{stash} || die "The stash method from outside run method!"
361             }
362              
363             sub run {
364 12     12 1 452 my ( $self, $target, %opts ) = @_;
365              
366 12         37 my $view_dependencies = 1;
367              
368             # The run's instance parameters. This instance to be needed to give ability to run the run method from deep inside run calls.
369             # It's for internal using.
370 12         25 my $run_instance = {};
371              
372 12   50     103 $run_instance->{stash} = $opts{stash} && ref $opts{stash} eq 'HASH' && $opts{stash} || {};
373              
374 12         29 $run_instance->{run_opts} = \%opts;
375 12         34 $run_instance->{ttl} = undef;
376 12         26 $run_instance->{target_locks} = {};
377              
378 12   100     56 $opts{actual_term} ||= $self->find($target)->{actual_term};
379 12         31 $self->{stats}{run}++;
380              
381             # FIXME to delete in future. Now it's deprecated
382 12         29 $self->{stash} = $run_instance->{stash};
383              
384             $view_dependencies = ! $self->target_is_actual( $target, $opts{actual_term} )
385 12 100       51 if ( $opts{actual_term} );
386              
387 12         206 my $res = $self->_run( ! $view_dependencies, $target, $run_instance );
388              
389             $res->state( CASCADE_ACTUAL_TERM )
390 12 100 100     57 if ( $opts{actual_term} && ! $view_dependencies );
391              
392 12 50 33     34 if ( defined $run_instance->{ttl} && $run_instance->{ttl} > 0 ) {
393 0         0 $res->state( CASCADE_TTL_INVOLVED );
394             }
395              
396 0         0 ${ $opts{ttl} } = $run_instance->{ttl}
397 12 50       29 if ( $opts{ttl} );
398              
399 6         13 ${ $opts{state} } = $res->state
400 12 100       34 if ( $opts{state} );
401              
402 12         31 $res->value;
403             }
404              
405             sub _run {
406 12     12   37 my ( $self, $only_from_cache, $target, $run_instance ) = @_;
407              
408 12 50       31 croak qq{The target ($target) for run should be string} if ref($target);
409 12 50       62 croak qq{The target for run is empty} if $target eq '';
410              
411 12         32 $run_instance->{chain} = {};
412 12         35 $run_instance->{only_cache_chain} = {};
413 12         29 $run_instance->{target_stack} = [];
414              
415 12         22 my $ret = eval {
416 12         37 $run_instance->{orig_target} = $target;
417              
418 12         50 return $self->value_ref_if_recomputed( $run_instance->{orig_rule} = $self->find( $target, $run_instance ), $target, $only_from_cache );
419             };
420              
421 12         25 my $terminated;
422              
423 12 50       27 if ( $terminated = $@ ) {
424 0         0 $ret = $@;
425              
426             die $ret
427 0 0       0 unless eval { $ret->isa('CHI::Cascade::Value') };
  0         0  
428              
429 0 0       0 $ret->state( CASCADE_CODE_EXCEPTION )
430             unless $ret->state;
431             }
432              
433 12 100       37 if ( ! $ret->is_value ) {
434 2         5 my $from_cache = $self->get_value( $target );
435              
436 2 50 33     18 return $from_cache->state( $ret->state )
437             if ( $terminated || $from_cache->is_value );
438              
439 0 0       0 return $self->_run( 1, $target, $run_instance )
440             if ! $only_from_cache;
441             }
442              
443 10         23 return $ret;
444             }
445              
446             sub find {
447 34     34 0 71 my ($self, $plain_target, $run_instance) = @_;
448              
449 34 50       83 die "CHI::Cascade::find : got empty target\n" if $plain_target eq '';
450              
451 34         45 my $new_rule;
452              
453             # If target is flat text
454 34 100       88 if (exists $self->{plain_targets}{$plain_target}) {
455 17 100       56 return $self->{plain_targets}{$plain_target}
456             unless $run_instance;
457 12         40 ( $new_rule = $self->{plain_targets}{$plain_target}->new( run_instance => $run_instance ) )->{matched_target} = $plain_target;
458 12         41 return $new_rule;
459             }
460              
461             # If rule's target is Regexp type
462 17         24 foreach my $rule (@{$self->{qr_targets}}) {
  17         55  
463 17         26 my @qr_params;
464              
465 17 50       163 if (@qr_params = $plain_target =~ $rule->{target}) {
466 17 100       60 return $rule
467             unless $run_instance;
468 11         50 ( $new_rule = $rule->new( run_instance => $run_instance ) )->qr_params(@qr_params);
469 11         22 $new_rule->{matched_target} = $plain_target;
470 11         52 return $new_rule;
471             }
472             }
473              
474 0           die "CHI::Cascade::find : cannot find the target $plain_target\n";
475             }
476              
477              
478             1;
479             __END__