File Coverage

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


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