File Coverage

blib/lib/Resource/Silo/Container.pm
Criterion Covered Total %
statement 148 148 100.0
branch 53 60 88.3
condition 28 33 84.8
subroutine 27 27 100.0
pod 2 2 100.0
total 258 270 95.5


line stmt bran cond sub pod time code
1             package Resource::Silo::Container;
2              
3 29     29   210 use strict;
  29         67  
  29         967  
4 29     29   160 use warnings;
  29         78  
  29         1264  
5             our $VERSION = '0.10';
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 29     29   162 use Carp;
  29         62  
  29         1689  
27 29     29   181 use Scalar::Util qw( blessed refaddr reftype weaken );
  29         62  
  29         1598  
28 29     29   187 use Module::Load qw( load );
  29         69  
  29         651  
29              
30             my $ID_REX = qr/^[a-z][a-z_0-9]*$/i;
31              
32             =head2 new( resource => $override, ... )
33              
34             Create a new container (also available as Cnew>).
35              
36             If arguments are given, they will be passed to the
37             L method (see below).
38              
39             =cut
40              
41             # NOTE to the editor. As we want to stay compatible with Moo/Moose,
42             # please make sure all internal fields start with a hyphen ("-").
43              
44             my %active_instances;
45             my $not_once = \%Resource::Silo::metadata; # avoid once warning
46              
47             sub new {
48 48     48 1 28595 my $class = shift;
49 48 100       357 $class = ref $class if blessed $class;
50              
51 48 50       403 my $spec = $Resource::Silo::metadata{$class}
52             or croak "Failed to locate \$Resource::Silo::metadata for class $class";
53              
54 48         581 my $self = bless {
55             -pid => $$,
56             -spec => $spec,
57             }, $class;
58 48 100       189 if (@_) {
59 4 100       30 croak "Odd number of additional arguments in new()"
60             if @_ % 2;
61 3         36 $self->_override_resources({ @_ });
62             };
63 47         284 $active_instances{ refaddr $self } = $self;
64 47         270 weaken $active_instances{ refaddr $self };
65 47         356 return $self;
66             };
67              
68             sub DESTROY {
69 28     28   500817 my $self = shift;
70 28         220 delete $active_instances{ refaddr $self };
71 28         160 $self->ctl->cleanup;
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 29     29   45999 foreach my $container (values %active_instances) {
78 20 50       103 next unless $container;
79 20         114 $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.
103              
104             B Such object contains a weak reference to the parent object
105             and thus must not be saved anywhere, lest you be surprised.
106             Use it and discard immediately.
107              
108             =cut
109              
110             sub ctl {
111 79     79 1 3434 my $self = shift;
112 79         331 my $facade = bless \$self, 'Resource::Silo::Container::Dashboard';
113 79         445 weaken $$facade;
114 79 50       289 confess "Attempt to close over nonexistent value"
115             unless $$facade;
116 79         361 return $facade;
117             };
118              
119             # Instantiate resource $name with argument $argument.
120             # This is what a silo->resource_name calls after checking the cache.
121             sub _instantiate_resource {
122 102     102   289 my ($self, $name, $arg) = @_;
123              
124 102 100       825 croak "Illegal resource name '$name'"
125             unless $name =~ $ID_REX;
126              
127 100         315 my $spec = $self->{-spec}{resource}{$name};
128 100   100     440 $arg //= '';
129              
130 100 100       251 croak "Attempting to fetch nonexistent resource '$name'"
131             unless $spec;
132 99 100       259 croak "Argument for resource '$name' must be a scalar"
133             if ref $arg;
134             croak "Illegal argument for resource '$name': '$arg'"
135 98 100       365 unless $spec->{argument}->($arg);
136              
137             croak "Attempting to initialize resource '$name' during cleanup"
138 96 100       344 if $self->{-cleanup};
139             croak "Attempting to initialize resource '$name' in locked mode"
140             if $self->{-locked}
141             and !$spec->{derived}
142 95 100 100     338 and !$self->{-override}{$name};
      100        
143              
144             self->_unexpected_dependency($name)
145 94 50 66     345 if ($self->{-allow} && !$self->{-allow}{$name});
146              
147             # Detect circular dependencies
148 94 100       347 my $key = $name . (length $arg ? "\@$arg" : '');
149 94 100       267 if ($self->{-pending}{$key}) {
150 1         3 my $loop = join ', ', sort keys %{ $self->{-pending} };
  1         6  
151 1         15 croak "Circular dependency detected for resource $key: {$loop}";
152             };
153              
154             # Try loading modules
155 93         158 foreach my $mod (@{ $spec->{require} }) {
  93         266  
156 6 100       9 eval { load $mod; 1 }
  6         28  
  5         6858  
157             or croak "resource '$name': failed to load '$mod': $@";
158             };
159              
160             # Finally set the temporary flags
161 92         249 local $self->{-onbehalf} = $name; # should we use a stack instead?
162 92         246 local $self->{-pending}{$key} = 1;
163 92         219 local $self->{-allow} = $spec->{allowdeps};
164              
165 92   66     589 ($self->{-override}{$name} // $spec->{init})->($self, $name, $arg)
      66        
166             // croak "Instantiating resource '$key' failed for no apparent reason";
167             };
168              
169             # use instead of delete $self->{-cache}{$name}
170             sub _cleanup_resource {
171 76     76   242 my ($self, $name, @list) = @_;
172              
173             # TODO Do we need to validate arguments here?
174 76         179 my $spec = $self->{-spec}{resource}{$name};
175              
176 76         124 my $action;
177 76 100       272 if (!$self->{-override}{$name}) {
178             # 1) skip resources that have overrides
179             # 2) if we're in "no pid" mode, use fork_cleanup if available
180             $action = $self->{-pid} != $$
181             && $spec->{fork_cleanup}
182 72   100     601 || $spec->{cleanup};
183             };
184 76         161 my $known = $self->{-cache}{$name};
185              
186 76 50       345 @list = keys %$known
187             unless @list;
188              
189 76         218 foreach my $arg (@list) {
190 86   50     208 $arg //= '';
191 86 100       247 next unless defined $known->{$arg};
192 73 100       306 $action->($known->{$arg}) if $action;
193 72         259 delete $known->{$arg};
194             };
195             };
196              
197             # We must create resource accessors in this package
198             # so that errors get attributed correctly
199             # (+ This way no other classes need to know our internal structure)
200             sub _make_resource_accessor {
201 66     66   210 my ($name, $spec) = @_;
202              
203 66 100       255 if ($spec->{ignore_cache}) {
204             return sub {
205 3     3   19 my ($self, $arg) = @_;
206 3         16 return $self->_instantiate_resource($name, $arg);
207 1         8 };
208             };
209              
210             return sub {
211 111     111   78674 my ($self, $arg) = @_;
212              
213             # If there was a fork, flush cache
214 111 100       472 if ($self->{-pid} != $$) {
215 1         74 $self->ctl->cleanup;
216 1         36 $self->{-pid} = $$;
217             };
218              
219             # We must check dependencies even before going to the cache
220             $self->_unexpected_dependency($name)
221 111 100 100     425 if ($self->{-allow} && !$self->{-allow}{$name});
222              
223             # Stringify $arg ASAP, we'll validate it inside _instantiate_resource().
224             # The cache entry for an invalid argument will never get populated.
225 107 100 100     370 my $key = defined $arg && !ref $arg ? $arg : '';
226 107   100     908 $self->{-cache}{$name}{$key} //= $self->_instantiate_resource($name, $arg);
227 65         450 };
228             };
229              
230             sub _check_overrides {
231 5     5   17 my ($self, $subst) = @_;
232              
233 5         12 my $known = $self->{-spec}{resource};
234 5         18 my @bad = grep { !$known->{$_} } keys %$subst;
  5         21  
235             croak "Attempt to override unknown resource(s): "
236 5 100       28 .join ", ", map { "'$_'" } @bad
  3         33  
237             if @bad;
238             };
239              
240             sub _override_resources {
241 5     5   18 my ($self, $subst) = @_;
242              
243 5         17 my $known = $self->{-spec}{resource};
244              
245 5         22 foreach my $name (keys %$subst) {
246             # Just skip over unknown resources if we're in constructor
247 6 100       22 next unless $known->{$name};
248 5         10 my $init = $subst->{$name};
249              
250             # Finalize existing values in cache, just in case
251             # BEFORE setting up override
252 5         27 $self->_cleanup_resource($name);
253              
254 5 100       20 if (defined $init) {
255             $self->{-override}{$name} = (reftype $init // '') eq 'CODE'
256             ? $init
257 4 50 50 4   77 : sub { $init };
  4         51  
258             } else {
259 1         4 delete $self->{-override}{$name};
260             };
261             };
262             }
263              
264             sub _unexpected_dependency {
265 4     4   12 my ($self, $name) = @_;
266 4         10 my $spec = $self->{-spec}{resource}{$name};
267              
268             my $explain = $spec->{autodeps}
269 4 50       13 ? ". Use explicit 'dependencies' or the 'loose_deps' flag"
270             : " but is not listed in its dependencies";
271 4         48 croak "Resource '$name' was unexpectedly required by"
272             ." '$self->{-onbehalf}'$explain";
273             }
274              
275             =head1 CONTROL INTERFACE
276              
277             The below methods are all accessible via
278             C<$container-Ectl-E$method_name>.
279              
280             =cut
281              
282             # We're declaring a different package in the same file because
283             # 1) it must have access to the internals anyway and
284             # 2) we want to keep the documentation close to the implementation.
285             package
286             Resource::Silo::Container::Dashboard;
287              
288 29     29   48195 use Carp;
  29         74  
  29         2020  
289 29     29   225 use Scalar::Util qw( reftype );
  29         67  
  29         16166  
290              
291             =head2 override( %substitutes )
292              
293             Provide a set of overrides for some of the resources.
294              
295             This can be used e.g. in tests to mock certain external facilities.
296              
297             %substitutes values are interpreted as follows:
298              
299             =over
300              
301             =item * C - use this code instead of the resource's C;
302              
303             =item * C - erase the override for given resource;
304              
305             =item * anything else is coerced into an initializer:
306             $value => sub { return $value }.
307              
308             =back
309              
310             Setting overrides has the side effect of clearing cache
311             for the affected resources.
312              
313             =cut
314              
315             sub override {
316 5     5   27 my ($self, %subst) = @_;
317              
318 5         34 $$self->_check_overrides(\%subst);
319 2         17 $$self->_override_resources(\%subst);
320              
321 2         7 return $self;
322             }
323              
324             =head2 lock
325              
326             Forbid initializing new resources.
327              
328             The cached ones instantiated so far, the ones that have been overridden,
329             and the ones with the C flag will still be returned.
330              
331             =cut
332              
333             sub lock {
334 2     2   5 my ($self) = @_;
335 2         5 $$self->{-locked} = 1;
336 2         8 return $self;
337             };
338              
339             =head2 unlock
340              
341             Remove the lock set by C.
342              
343             =cut
344              
345             sub unlock {
346 1     1   3 my $self = shift;
347 1         2 delete $$self->{-locked};
348 1         3 return $self;
349             };
350              
351             =head2 preload()
352              
353             Try loading all the resources that have C flag set.
354              
355             May be useful if e.g. a server-side application is starting and must
356             check its database connection(s) before it starts handling any clients.
357              
358             In addition, self-check will be called and all declared C'd
359             modules will be loaded, even if they are not required by preloaded resources.
360              
361             =cut
362              
363             sub preload {
364 2     2   9 my $self = shift;
365             # TODO allow specifying resources to load
366             # but first come up with a way to specify arguments, too.
367              
368 2         5 my $meta = $$self->{-spec};
369              
370 2         21 $meta->self_check;
371              
372 1         6 my $list = $meta->{preload};
373 1         3 for my $name (@$list) {
374 1         3 my $unused = $$self->$name;
375             };
376 1         10 return $self;
377             };
378              
379             =head2 cleanup
380              
381             Cleanup all resources.
382             Once the cleanup is started, no more resources can be created,
383             and trying to do so will result in exception.
384             Typically only useful for destruction.
385              
386             =cut
387              
388             sub cleanup {
389 51     51   104 my $self = ${ $_[0] };
  51         136  
390 51         192 local $self->{-cleanup} = 1; # This is stronger than lock.
391              
392             # NOTE Be careful! cleanup must never ever die!
393              
394 51         148 my $spec = $self->{-spec}{resource};
395             my @order = sort {
396 45         157 $spec->{$a}{cleanup_order} <=> $spec->{$b}{cleanup_order};
397 51         113 } keys %{ $self->{-cache} };
  51         304  
398              
399 51         170 foreach my $name (@order) {
400 71         136 local $@; # don't pollute $@ if we're in destructor after an exception
401             eval {
402             # We cannot afford to die here as if we do
403             # a resource that causes exceptions in cleanup
404             # would be stuck in cache forever
405 71         320 $self->_cleanup_resource($name);
406 70         305 1;
407 71 100       136 } or do {
408 1         225 my $err = $@;
409 1         15 Carp::cluck "Failed to cleanup resource '$name', but trying to continue: $err";
410             };
411             };
412              
413 51         866 delete $self->{-cache};
414 51         1039 return $_[0];
415             };
416              
417             =head2 fresh( $resource_name [, $argument ] )
418              
419             Instantiate resource and return it, ignoring cached value, if any.
420             This may be useful if the resource's state is going to be modified
421             in a manner incompatible with its other consumers within the same process.
422              
423             E.g. performing a Big Evil SQL Transaction while other parts of the application
424             are happily using L.
425              
426             B Use with caution.
427             Resorting to this method frequently may be a sign of a broader
428             architectural problem.
429              
430             =cut
431              
432             sub fresh {
433 8     8   18 return ${+shift}->_instantiate_resource(@_);
  8         54  
434             };
435              
436             =head2 meta
437              
438             Get resource metadata object (a L).
439              
440             =cut
441              
442             sub meta {
443 11     11   21 return ${+shift}->{-spec};
  11         66  
444             };
445              
446             =head1 COPYRIGHT AND LICENSE
447              
448             Copyright (c) 2023, Konstantin Uvarin, C<< >>
449              
450             This program is free software.
451             You can redistribute it and/or modify it under the terms of either:
452             the GNU General Public License as published by the Free Software Foundation,
453             or the Artistic License.
454              
455             See L for more information.
456              
457             =cut
458              
459             1;