File Coverage

blib/lib/Resource/Silo/Container.pm
Criterion Covered Total %
statement 183 183 100.0
branch 68 70 97.1
condition 31 34 91.1
subroutine 32 32 100.0
pod 1 3 33.3
total 315 322 97.8


line stmt bran cond sub pod time code
1             package Resource::Silo::Container;
2              
3 49     49   366 use strict;
  49         97  
  49         2074  
4 49     49   275 use warnings;
  49         93  
  49         4201  
5             our $VERSION = '0.1703';
6              
7             =head1 NAME
8              
9             Resource::Silo::Container - base resource container class for L.
10              
11             =head1 DESCRIPTION
12              
13             L isolates resources by storing them
14             inside a container object.
15              
16             The methods of such an object are generated on the fly and stored either
17             in a special virtual package, or the calling module.
18              
19             This class provides some common functionality that allows to access resources,
20             as well as a doorway into a fine-grained control interface.
21              
22             =head1 METHODS
23              
24             =cut
25              
26 49     49   297 use Moo;
  49         119  
  49         421  
27 49     49   24443 use Carp;
  49         114  
  49         4964  
28 49     49   464 use Scalar::Util qw( blessed refaddr reftype weaken );
  49         129  
  49         3909  
29 49     49   312 use Module::Load qw( load );
  49         246  
  49         1066  
30 49     49   33629 use namespace::clean;
  49         922461  
  49         384  
31              
32             my $ID_REX = qr/^[a-z][a-z_0-9]*$/i;
33              
34             =head2 new( resource => $override, ... )
35              
36             Create a new container (also available as Cnew>).
37              
38             If arguments are given, they will be passed to the
39             L method (see below).
40              
41             =cut
42              
43             # NOTE to the editor. As we want to stay compatible with Moo/Moose,
44             # please make sure all internal fields start with a hyphen ("-").
45              
46             my %active_instances;
47              
48             sub BUILD {
49 75     75 0 500854 my ($self, $args) = @_;
50              
51 75   66     512 my $spec = $Resource::Silo::metadata{ref $self}
52             // _silo_find_metaclass($self);
53              
54             # TODO Carp doesn't seem to work correctly from constructor with either Moo or Moose,
55             # but at least we now generate pointers to where the offending resources are declared
56 74         507 $spec->run_pending_checks;
57              
58 71         479 $self->{-spec} = $spec;
59 71         640 $self->{-pid} = $$;
60              
61 71         593 $self->_silo_do_override($args);
62              
63 71         323 $active_instances{ refaddr $self } = $self;
64 71         805 weaken $active_instances{ refaddr $self };
65             };
66              
67             sub DEMOLISH {
68 56     56 0 11174266 my $self = shift;
69 56         615 delete $active_instances{ refaddr $self };
70             $self->ctl->cleanup
71 56 100       1452 if $self->{-spec};
72             };
73              
74             # As container instances inside the silo() function will be available forever,
75             # we MUST enforce freeing the resources before program ends
76             END {
77             # don't iterate over a hash while simultaneously deleting from it.
78             # sorting is useless because keys are mem addresses.
79 49     49   125982 my @list = keys %active_instances;
80 49         1403 foreach my $id (@list) {
81 19 50       134 next unless $active_instances{$id};
82 19         142 $active_instances{$id}->ctl->cleanup;
83             };
84             };
85              
86             =head2 C
87              
88             As the container class may contain arbitrary resource names and
89             user-defined methods to boot, we intend to declare as few public methods
90             as possible.
91              
92             Instead, we create a facade object that has access to container's internals
93             and can perform fine-grained management operations.
94             See L below.
95              
96             Example:
97              
98             # Somewhere in a test file
99             use Test::More;
100             use My::App qw(silo);
101              
102             silo->ctl->override( dbh => $fake_database_connection );
103             silo->ctl->lock; # forbid instantiating new resources
104              
105             Returns a facade object referencing the original container.
106              
107             =cut
108              
109             sub ctl {
110 117     117 1 11507646 my $self = shift;
111 117         2995 return bless \$self, 'Resource::Silo::Container::Dashboard';
112             # 'Clever' weaken-ing code was here
113             # Please don't do this again, it's unnecessary
114             };
115              
116             # Instantiate resource $name with argument $argument.
117             # This is what a silo->resource_name calls after checking the cache.
118             sub _silo_instantiate_res {
119 163     163   512 my ($self, $name, $arg) = @_;
120              
121 163 100       2140 croak "Illegal resource name '$name'"
122             unless $name =~ $ID_REX;
123              
124 161         602 my $spec = $self->{-spec}{resource}{$name};
125 161   100     3082 $arg //= '';
126              
127 161 100       428 croak "Attempting to fetch nonexistent resource '$name'"
128             unless $spec;
129 160 100       482 croak "Argument for resource '$name' must be a scalar"
130             if ref $arg;
131             croak "Illegal argument for resource '$name': '$arg'"
132 159 100       1021 unless $spec->{argument}->($arg);
133              
134             croak "Attempting to initialize resource '$name' during cleanup"
135 157 100       595 if $self->{-cleanup};
136             croak "Attempting to initialize resource '$name' in locked mode"
137             if $self->{-locked}
138             and !$spec->{derived}
139 156 100 100     805 and !$self->{-override}{$name};
      100        
140              
141             # Detect circular dependencies
142 155 100       647 my $key = $name . (length $arg ? "/$arg" : '');
143 155 100       566 if ($self->{-pending}{$key}) {
144 1         3 my $loop = join ', ', sort keys %{ $self->{-pending} };
  1         8  
145 1         36 croak "Circular dependency detected for resource $key: {$loop}";
146             };
147              
148             # Try loading modules
149 154         301 foreach my $mod (@{ $spec->{require} }) {
  154         642  
150 6 100       29 eval { load $mod; 1 }
  6         30  
  5         9664  
151             or croak "resource '$name': failed to load '$mod': $@";
152             };
153              
154             # Finally set the temporary flags
155 153         524 local $self->{-onbehalf} = $name; # should we use a stack instead?
156 153         472 local $self->{-pending}{$key} = 1;
157 153         459 local $self->{-allow} = $spec->{allowdeps};
158              
159 153   66     930 my $init = $self->{-override}{$name} // $spec->{init};
160 153         775 my $entity = $init->($self, $name, $arg);
161 142 100       1056 if (!defined $entity) {
162 8 100       31 return $entity if ($spec->{nullable});
163 5         51 croak "Instantiating resource '$key' $spec->{origin} returned undef for no apparent reason";
164             }
165 134 100       415 if ($spec->{coerce}) {
166 4         15 $entity = $spec->{coerce}->($self, $entity);
167 4 100 66     89 croak "Coercing resource '$key' $spec->{origin} returned an empty value"
      100        
168             if !defined $entity || (ref $entity eq '' && $entity eq '');
169             }
170             $spec->{check}->($self, $entity, $name, $arg)
171 132 100       431 if $spec->{check};
172 131         938 return $entity;
173             };
174              
175             # use instead of delete $self->{-cache}{$name}
176             sub _silo_cleanup_res {
177 146     146   621 my ($self, $name, %opt) = @_;
178              
179             # TODO Do we need to validate arguments here?
180 146         794 my $spec = $self->{-spec}{resource}{$name};
181              
182 146 100 100     1589 return if $opt{fork} and not $opt{force} and $spec->{fork_safe};
      100        
183              
184             # NOTE Be careful! cleanup must never ever die!
185              
186 137         367 my $action;
187 137 100       1116 if (!$self->{-override}{$name}) {
188             # 1) skip resources that have overrides
189             # 2) if we're in "no pid" mode, use fork_cleanup if available
190 130 100       967 $action = $opt{fork} ? $spec->{fork_cleanup} : $spec->{cleanup};
191             };
192 137         424 my $known = $self->{-cache}{$name};
193              
194 137         710 my @list = keys %$known;
195              
196 137 100       553 if ($action) {
197 59         300 foreach my $arg (@list) {
198 58         1348 local $@; # don't pollute $@ if we're in destructor after an exception
199             eval {
200 58         879 $action->($known->{$arg});
201 55         3467 1;
202 58 100       170 } or do {
203 3         574 my $err = $@;
204 3         43 Carp::cluck "Failed to cleanup resource '$name/$arg', but trying to continue: $err";
205             };
206             };
207             };
208              
209             # This will trigger the normal destructor(s) on resource instances, if any
210 137         2684 delete $self->{-cache}{$name};
211             };
212              
213             # We must create resource accessors in this package
214             # so that errors get attributed correctly
215             # (+ This way no other classes need to know our internal structure)
216             sub _silo_make_accessor {
217 120     120   341 my ($name, $spec) = @_;
218              
219             return sub {
220 181     181   3707762 my ($self, $arg) = @_;
221              
222             # If there was a fork, flush cache
223 181 100       1612 if ($self->{-pid} != (my $pid = $$)) {
224 5         1533 $self->ctl->_cleanup( fork => 1 );
225 5         171 $self->{-pid} = $pid;
226             };
227              
228             # We must check dependencies even before going to the cache
229             $self->_silo_unexpected_dep($name)
230 181 100 100     855 if ($self->{-allow} && !$self->{-allow}{$name});
231              
232             # Stringify $arg ASAP, we'll validate it inside _silo_instantiate_res().
233             # The cache entry for an invalid argument will never get populated.
234 176 100 100     997 my $key = defined $arg && !ref $arg ? $arg : '';
235 176 100       878 if (!exists $self->{-cache}{$name}{$key}) {
236 154         1037 $self->{-cache}{$name}{$key} = $self->_silo_instantiate_res($name, $arg);
237             };
238 153         768 return $self->{-cache}{$name}{$key};
239 120         1417 };
240             };
241              
242             sub _silo_check_overrides {
243 11     11   143 my ($self, $subst) = @_;
244              
245 11         290 my $known = $self->{-spec}{resource};
246 11         471 my @bad = grep { !$known->{$_} } keys %$subst;
  10         106  
247             croak "Attempt to override unknown resource(s): "
248 11 100       229 .join ", ", map { "'$_'" } @bad
  3         26  
249             if @bad;
250             };
251              
252             sub _silo_do_override {
253 79     79   285 my ($self, $subst) = @_;
254              
255 79         258 my $known = $self->{-spec}{resource};
256              
257 79         517 foreach my $name (keys %$subst) {
258             # Just skip over unknown resources if we're in constructor
259 13 100       97 next unless $known->{$name};
260 12         36 my $init = $subst->{$name};
261              
262             # Finalize existing values in cache, just in case
263             # BEFORE setting up override
264 12         101 $self->_silo_cleanup_res($name);
265              
266 12 100       74 if (defined $init) {
267             $self->{-override}{$name} = (reftype $init // '') eq 'CODE'
268             ? $init
269 11 100 100 6   395 : sub { $init };
  6         14  
270             } else {
271 1         4 delete $self->{-override}{$name};
272             };
273             };
274             }
275              
276             sub _silo_unexpected_dep {
277 5     5   14 my ($self, $name) = @_;
278 5         13 my $spec = $self->{-spec}{resource}{$name};
279              
280             my $explain = $spec->{autodeps}
281 5 50       71 ? ". Use explicit 'dependencies'"
282             : " but is not listed in its dependencies";
283 5         104 croak "Resource '$name' was unexpectedly required by"
284             ." '$self->{-onbehalf}'$explain";
285             }
286              
287             sub _silo_find_metaclass {
288 2     2   5 my $self = shift;
289 2         6 my $class = ref $self;
290              
291 2         6 my @queue = $class;
292 2         77 while (defined( my $next = shift @queue )) {
293 4         10 my $meta = $Resource::Silo::metadata{$next};
294 4 100       15 return $meta if $meta;
295 49     49   126147 no strict 'refs'; ## no critic strictures
  49         124  
  49         7778  
296 3         5 push @queue, @{ "${next}::ISA" };
  3         22  
297             };
298              
299 1         13 croak "Failed to locate \$Resource::Silo::metadata for class $class";
300             };
301              
302             =head1 CONTROL INTERFACE
303              
304             The below methods are all accessible via
305             C<$container-Ectl-E$method_name>.
306              
307             =cut
308              
309             # We're declaring a different package in the same file because
310             # 1) it must have access to the internals anyway and
311             # 2) we want to keep the documentation close to the implementation.
312             package
313             Resource::Silo::Container::Dashboard;
314              
315 49     49   436 use Carp;
  49         102  
  49         4419  
316 49     49   394 use Scalar::Util qw( reftype );
  49         107  
  49         42334  
317              
318             =head2 override( %substitutes )
319              
320             Provide a set of overrides for some of the resources.
321              
322             This can be used e.g. in tests to mock certain external facilities.
323              
324             %substitutes values are interpreted as follows:
325              
326             =over
327              
328             =item * C - use this code instead of the resource's C;
329              
330             =item * C - erase the override for given resource;
331              
332             =item * anything else is coerced into an initializer:
333             $value => sub { return $value }.
334              
335             =back
336              
337             Setting overrides has the side effect of clearing cache
338             for the affected resources.
339              
340             =cut
341              
342             sub override {
343 11     11   321 my ($self, %subst) = @_;
344              
345 11         924 $$self->_silo_check_overrides(\%subst);
346             $self->_cleanup( fork => 1 )
347 8 100       477 if $$ != $$self->{-pid};
348 8         79 $$self->_silo_do_override(\%subst);
349              
350 8         48 return $self;
351             }
352              
353             =head2 lock
354              
355             Forbid initializing new resources.
356              
357             The cached ones instantiated so far, the ones that have been overridden,
358             and the ones with the C flag will still be returned.
359              
360             =cut
361              
362             sub lock {
363 2     2   8 my ($self) = @_;
364 2         18 $$self->{-locked} = 1;
365 2         9 return $self;
366             };
367              
368             =head2 unlock
369              
370             Remove the lock set by C.
371              
372             =cut
373              
374             sub unlock {
375 1     1   2 my $self = shift;
376 1         3 delete $$self->{-locked};
377 1         4 return $self;
378             };
379              
380             =head2 preload()
381              
382             Try loading all the resources that have C flag set.
383              
384             May be useful if e.g. a server-side application is starting and must
385             check its database connection(s) before it starts handling any clients.
386              
387             In addition, self-check will be called and all declared C'd
388             modules will be loaded, even if they are not required by preloaded resources.
389              
390             =cut
391              
392             sub preload {
393 3     3   5 my $self = shift;
394             # TODO allow specifying resources to load
395              
396 3         31 my $meta = $$self->{-spec};
397              
398 3         16 $meta->preload;
399              
400 2         4 my $list = $meta->{preload};
401 2         5 for my $name (@$list) {
402 2         4 for my $arg (@{ $meta->{resource}{$name}{preload} }) {
  2         8  
403 3         13 my $unused = $$self->$name($arg);
404             }
405             };
406 2         13 return $self;
407             };
408              
409             =head2 cleanup
410              
411             Cleanup all resources.
412             Once the cleanup is started, no more resources can be created,
413             and trying to do so will result in exception.
414             Typically only useful for destruction.
415              
416             =cut
417              
418             sub cleanup {
419 75     75   211 my $self = shift;
420             # Don't give the user access to options (yet)
421              
422 75         213 my @opt;
423 75 100       1459 push @opt, fork => 1, force => 1 if $$ != $$self->{-pid};
424              
425 75         438 $self->_cleanup(@opt);
426             }
427              
428             sub _cleanup {
429 85     85   350 my $self = ${ +shift };
  85         669  
430 85         1365 my %opt = @_;
431 85         476 local $self->{-cleanup} = 1; # This is stronger than lock.
432              
433             # NOTE Be careful! cleanup must never ever die!
434              
435 85         454 my $spec = $self->{-spec}{resource};
436             my @order = sort {
437 87         783 $spec->{$a}{cleanup_order} <=> $spec->{$b}{cleanup_order};
438 85         307 } keys %{ $self->{-cache} };
  85         1926  
439              
440              
441 85         569 foreach my $name (@order) {
442 134         1170 $self->_silo_cleanup_res($name, %opt);
443             };
444              
445 85         2611 return $_[0];
446             };
447              
448             =head2 fresh( $resource_name [, $argument ] )
449              
450             Instantiate resource and return it, ignoring cached value, if any.
451             This may be useful if the resource's state is going to be modified
452             in a manner incompatible with its other consumers within the same process.
453              
454             E.g. performing a Big Evil SQL Transaction while other parts of the application
455             are happily using L.
456              
457             B Use with caution.
458             Resorting to this method frequently may be a sign of a broader
459             architectural problem.
460              
461             =cut
462              
463             sub fresh {
464 9     9   20 return ${+shift}->_silo_instantiate_res(@_);
  9         45  
465             };
466              
467             =head2 list_cached
468              
469             Return list of services that are currently present in the cache as strings
470             of form C<$service_name> or C<$service_name/$argument> if argument is present.
471              
472             Useful for debugging.
473              
474             =cut
475              
476             sub list_cached {
477 5     5   8 my $cache = ${+shift}->{-cache};
  5         18  
478 5         8 my @out;
479 5         19 foreach my $service (sort keys %$cache) {
480 4         8 foreach my $arg (sort keys %{ $cache->{$service} }) {
  4         13  
481 6 100       39 push @out, length $arg ? "$service/$arg" : $service;
482             };
483             };
484 5 100       42 return wantarray ? @out : \@out;
485             };
486              
487             =head2 meta
488              
489             Get resource metadata object (a L).
490              
491             =cut
492              
493             sub meta {
494 7     7   17 return ${+shift}->{-spec};
  7         59  
495             };
496              
497             =head1 COPYRIGHT AND LICENSE
498              
499             Copyright (c) 2023-2026, Konstantin Uvarin, C<< >>
500              
501             This program is free software.
502             You can redistribute it and/or modify it under the terms of either:
503             the GNU General Public License as published by the Free Software Foundation,
504             or the Artistic License.
505              
506             See L for more information.
507              
508             =cut
509              
510             1;