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   277 use strict;
  49         74  
  49         1628  
4 49     49   213 use warnings;
  49         70  
  49         3144  
5             our $VERSION = '0.1702';
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   220 use Moo;
  49         80  
  49         346  
27 49     49   18082 use Carp;
  49         98  
  49         3753  
28 49     49   298 use Scalar::Util qw( blessed refaddr reftype weaken );
  49         76  
  49         2943  
29 49     49   229 use Module::Load qw( load );
  49         165  
  49         558  
30 49     49   25548 use namespace::clean;
  49         704899  
  49         379  
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 350484 my ($self, $args) = @_;
50              
51 75   66     370 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         420 $spec->run_pending_checks;
57              
58 71         375 $self->{-spec} = $spec;
59 71         450 $self->{-pid} = $$;
60              
61 71         428 $self->_silo_do_override($args);
62              
63 71         232 $active_instances{ refaddr $self } = $self;
64 71         530 weaken $active_instances{ refaddr $self };
65             };
66              
67             sub DEMOLISH {
68 56     56 0 7297622 my $self = shift;
69 56         359 delete $active_instances{ refaddr $self };
70             $self->ctl->cleanup
71 56 100       1086 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   90566 foreach my $container (values %active_instances) {
78 19 50       85 next unless $container;
79 19         91 $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 7701589 my $self = shift;
108 117         2184 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   351 my ($self, $name, $arg) = @_;
117              
118 163 100       1337 croak "Illegal resource name '$name'"
119             unless $name =~ $ID_REX;
120              
121 161         417 my $spec = $self->{-spec}{resource}{$name};
122 161   100     652 $arg //= '';
123              
124 161 100       341 croak "Attempting to fetch nonexistent resource '$name'"
125             unless $spec;
126 160 100       347 croak "Argument for resource '$name' must be a scalar"
127             if ref $arg;
128             croak "Illegal argument for resource '$name': '$arg'"
129 159 100       536 unless $spec->{argument}->($arg);
130              
131             croak "Attempting to initialize resource '$name' during cleanup"
132 157 100       495 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     470 and !$self->{-override}{$name};
      100        
137              
138             # Detect circular dependencies
139 155 100       469 my $key = $name . (length $arg ? "/$arg" : '');
140 155 100       430 if ($self->{-pending}{$key}) {
141 1         2 my $loop = join ', ', sort keys %{ $self->{-pending} };
  1         5  
142 1         11 croak "Circular dependency detected for resource $key: {$loop}";
143             };
144              
145             # Try loading modules
146 154         216 foreach my $mod (@{ $spec->{require} }) {
  154         397  
147 6 100       8 eval { load $mod; 1 }
  6         23  
  5         6681  
148             or croak "resource '$name': failed to load '$mod': $@";
149             };
150              
151             # Finally set the temporary flags
152 153         402 local $self->{-onbehalf} = $name; # should we use a stack instead?
153 153         330 local $self->{-pending}{$key} = 1;
154 153         332 local $self->{-allow} = $spec->{allowdeps};
155              
156 153   66     587 my $init = $self->{-override}{$name} // $spec->{init};
157 153         520 my $entity = $init->($self, $name, $arg);
158 142 100       872 if (!defined $entity) {
159 8 100       25 return $entity if ($spec->{nullable});
160 5         48 croak "Instantiating resource '$key' $spec->{origin} returned undef for no apparent reason";
161             }
162 134 100       286 if ($spec->{coerce}) {
163 4         10 $entity = $spec->{coerce}->($self, $entity);
164 4 100 66     59 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       313 if $spec->{check};
169 131         531 return $entity;
170             };
171              
172             # use instead of delete $self->{-cache}{$name}
173             sub _silo_cleanup_res {
174 146     146   494 my ($self, $name, %opt) = @_;
175              
176             # TODO Do we need to validate arguments here?
177 146         456 my $spec = $self->{-spec}{resource}{$name};
178              
179 146 100 100     1045 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         298 my $action;
184 137 100       599 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       956 $action = $opt{fork} ? $spec->{fork_cleanup} : $spec->{cleanup};
188             };
189 137         343 my $known = $self->{-cache}{$name};
190              
191 137         632 my @list = keys %$known;
192              
193 137 100       550 if ($action) {
194 59         231 foreach my $arg (@list) {
195 58         864 local $@; # don't pollute $@ if we're in destructor after an exception
196             eval {
197 58         577 $action->($known->{$arg});
198 55         801 1;
199 58 100       150 } or do {
200 3         593 my $err = $@;
201 3         26 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         2317 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   281 my ($name, $spec) = @_;
215              
216             return sub {
217 181     181   2178970 my ($self, $arg) = @_;
218              
219             # If there was a fork, flush cache
220 181 100       1162 if ($self->{-pid} != (my $pid = $$)) {
221 5         780 $self->ctl->_cleanup( fork => 1 );
222 5         96 $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     600 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     575 my $key = defined $arg && !ref $arg ? $arg : '';
232 176 100       531 if (!exists $self->{-cache}{$name}{$key}) {
233 154         719 $self->{-cache}{$name}{$key} = $self->_silo_instantiate_res($name, $arg);
234             };
235 153         550 return $self->{-cache}{$name}{$key};
236 120         693 };
237             };
238              
239             sub _silo_check_overrides {
240 11     11   113 my ($self, $subst) = @_;
241              
242 11         151 my $known = $self->{-spec}{resource};
243 11         286 my @bad = grep { !$known->{$_} } keys %$subst;
  10         143  
244             croak "Attempt to override unknown resource(s): "
245 11 100       166 .join ", ", map { "'$_'" } @bad
  3         28  
246             if @bad;
247             };
248              
249             sub _silo_do_override {
250 79     79   173 my ($self, $subst) = @_;
251              
252 79         171 my $known = $self->{-spec}{resource};
253              
254 79         282 foreach my $name (keys %$subst) {
255             # Just skip over unknown resources if we're in constructor
256 13 100       76 next unless $known->{$name};
257 12         41 my $init = $subst->{$name};
258              
259             # Finalize existing values in cache, just in case
260             # BEFORE setting up override
261 12         55 $self->_silo_cleanup_res($name);
262              
263 12 100       43 if (defined $init) {
264             $self->{-override}{$name} = (reftype $init // '') eq 'CODE'
265             ? $init
266 11 100 100 6   251 : sub { $init };
  6         14  
267             } else {
268 1         2 delete $self->{-override}{$name};
269             };
270             };
271             }
272              
273             sub _silo_unexpected_dep {
274 5     5   16 my ($self, $name) = @_;
275 5         10 my $spec = $self->{-spec}{resource}{$name};
276              
277             my $explain = $spec->{autodeps}
278 5 50       11 ? ". Use explicit 'dependencies'"
279             : " but is not listed in its dependencies";
280 5         68 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         3 my $class = ref $self;
287              
288 2         4 my @queue = $class;
289 2         9 while (defined( my $next = shift @queue )) {
290 4         8 my $meta = $Resource::Silo::metadata{$next};
291 4 100       10 return $meta if $meta;
292 49     49   97088 no strict 'refs'; ## no critic strictures
  49         96  
  49         6220  
293 3         43 push @queue, @{ "${next}::ISA" };
  3         16  
294             };
295              
296 1         16 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   316 use Carp;
  49         75  
  49         3464  
313 49     49   342 use Scalar::Util qw( reftype );
  49         86  
  49         31930  
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   271 my ($self, %subst) = @_;
341              
342 11         850 $$self->_silo_check_overrides(\%subst);
343             $self->_cleanup( fork => 1 )
344 8 100       370 if $$ != $$self->{-pid};
345 8         90 $$self->_silo_do_override(\%subst);
346              
347 8         30 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   5 my ($self) = @_;
361 2         12 $$self->{-locked} = 1;
362 2         6 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   1 my $self = shift;
373 1         2 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         12 $meta->preload;
396              
397 2         4 my $list = $meta->{preload};
398 2         4 for my $name (@$list) {
399 2         3 for my $arg (@{ $meta->{resource}{$name}{preload} }) {
  2         4  
400 3         8 my $unused = $$self->$name($arg);
401             }
402             };
403 2         10 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   220 my $self = shift;
417             # Don't give the user access to options (yet)
418              
419 75         183 my @opt;
420 75 100       1107 push @opt, fork => 1, force => 1 if $$ != $$self->{-pid};
421              
422 75         376 $self->_cleanup(@opt);
423             }
424              
425             sub _cleanup {
426 85     85   293 my $self = ${ +shift };
  85         522  
427 85         579 my %opt = @_;
428 85         550 local $self->{-cleanup} = 1; # This is stronger than lock.
429              
430             # NOTE Be careful! cleanup must never ever die!
431              
432 85         363 my $spec = $self->{-spec}{resource};
433             my @order = sort {
434 86         567 $spec->{$a}{cleanup_order} <=> $spec->{$b}{cleanup_order};
435 85         279 } keys %{ $self->{-cache} };
  85         1542  
436              
437              
438 85         366 foreach my $name (@order) {
439 134         1087 $self->_silo_cleanup_res($name, %opt);
440             };
441              
442 85         1953 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   34 return ${+shift}->_silo_instantiate_res(@_);
  9         41  
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   9 my $cache = ${+shift}->{-cache};
  5         17  
475 5         8 my @out;
476 5         22 foreach my $service (sort keys %$cache) {
477 4         5 foreach my $arg (sort keys %{ $cache->{$service} }) {
  4         9  
478 6 100       18 push @out, length $arg ? "$service/$arg" : $service;
479             };
480             };
481 5 100       39 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         94  
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;