File Coverage

blib/lib/Resource/Silo/Container.pm
Criterion Covered Total %
statement 177 177 100.0
branch 64 66 96.9
condition 26 28 92.8
subroutine 32 32 100.0
pod 1 3 33.3
total 300 306 98.0


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