File Coverage

blib/lib/Resource/Silo.pm
Criterion Covered Total %
statement 53 53 100.0
branch 2 2 100.0
condition 4 6 66.6
subroutine 12 12 100.0
pod n/a
total 71 73 97.2


line stmt bran cond sub pod time code
1             package Resource::Silo;
2              
3 29     29   1629696 use 5.010;
  29         374  
4 29     29   153 use strict;
  29         62  
  29         640  
5 29     29   146 use warnings;
  29         60  
  29         3124  
6              
7             our $VERSION = '0.11';
8              
9             =head1 NAME
10              
11             Resource::Silo - lazy declarative resource container for Perl.
12              
13             =head1 DESCRIPTION
14              
15             We assume the following setup:
16              
17             =over
18              
19             =item * The application needs to access multiple resources, such as
20             configuration files, databases, queues, service endpoints, credentials, etc.
21              
22             =item * The application has helper scripts that don't need to initialize
23             all the resources at once, as well as a test suite where accessing resources
24             is undesirable unless a fixture or mock is provided.
25              
26             =item * The resource management has to be decoupled from the application
27             logic where possible.
28              
29             =back
30              
31             And we propose the following solution:
32              
33             =over
34              
35             =item * All available resources are declared in one place
36             and encapsulated within a single container.
37              
38             =item * Such container is equipped with methods to access resources,
39             as well as an exportable prototyped function for obtaining the one and true
40             instance of it (a.k.a. optional singleton).
41              
42             =item * Every class or script in the project accesses resources
43             through this container and only through it.
44              
45             =back
46              
47             =head1 SYNOPSIS
48              
49             The default mode is to create a one-off container for all resources
50             and export if into the calling class via C function.
51              
52             package My::App;
53             use Resource::Silo;
54              
55             use DBI;
56             use YAML::LoadFile;
57             ...
58              
59             resource config => sub { LoadFile( ... ) };
60             resource dbh => sub {
61             my $self = shift;
62             my $conf = $self->config->{dbh};
63             DBI->connect( $conf->{dsn}, $conf->{user}, $conf->{pass}, { RaiseError => 1 } );
64             };
65             resource queue => sub { My::Queue->new( ... ) };
66             ...
67              
68             my $statement = silo->dbh->prepare( $sql );
69             my $queue = silo->queue;
70              
71             For more complicated projects, it may make more sense
72             to create a dedicated class for resource management:
73              
74             # in the container class
75             package My::Project::Res;
76             use Resource::Silo -class; # resource definitions will now create
77             # eponymous methods in My::Project::Res
78              
79             resource foo => sub { ... }; # declare resources as in the above example
80             resource bar => sub { ... };
81              
82             1;
83              
84             # in all other modules/packages/scripts:
85              
86             package My::Project;
87             use My::Project::Res qw(silo);
88              
89             silo->foo; # obtain resources
90             silo->bar;
91              
92             My::Project::Res->new; # separate empty resource container
93              
94             =head1 EXPORT
95              
96             The following functions will be exported into the calling module,
97             unconditionally:
98              
99             =over
100              
101             =item * silo - a singleton function returning the resource container.
102             Note that this function will be created separately for every calling module,
103             and needs to be re-exported to be shared.
104              
105             =item * resource - a DSL for defining resources, their initialization
106             and properties. See below.
107              
108             =back
109              
110             Additionally, if the C<-class> argument was added to the use line,
111             the following things happen:
112              
113             =over
114              
115             =item * L and L are added to C<@ISA>;
116              
117             =item * C function is added to C<@EXPORT> and thus becomes re-exported
118             by default;
119              
120             =item * calling C creates a corresponding method in this package.
121              
122             =back
123              
124             =head2 resource
125              
126             resource 'name' => sub { ... };
127             resource 'name' => %options;
128              
129             %options may include:
130              
131             =head3 init => sub { $self, $name, [$argument] }
132              
133             A coderef to obtain the resource.
134             Required, unless C or C are specified.
135              
136             If the number of arguments is odd,
137             the last one is popped and considered to be the init function.
138              
139             =head3 literal => $value
140              
141             Replace initializer with C.
142              
143             In addition, C flag is set,
144             and an empty C list is implied.
145              
146             =head3 argument => C || C
147              
148             If specified, assume that the resource in question may have several instances,
149             distinguished by a string argument. Such argument will be passed as the 3rd
150             parameter to the C function.
151              
152             Only one resource instance will be cached per argument value.
153              
154             This may be useful e.g. for L result sets,
155             or for L.
156              
157             A regular expression will always be anchored to match I.
158             A function must return true for the parameter to be valid.
159              
160             If the argument is omitted, it is assumed to be an empty string.
161              
162             See L below.
163              
164             =head3 derived => 1 | 0
165              
166             Assume that resource can be derived from its dependencies,
167             or that it introduces no extra side effects compared to them.
168              
169             This also naturally applies to resources with pure initializers,
170             i.e. those having no dependencies and adding no side effects on top.
171              
172             Examples may be L built on top of a L handle
173             or L built on top of L connection.
174              
175             Derivative resources may be instantiated even in locked mode,
176             as they would only initialize if their dependencies have already been
177             initialized or overridden.
178              
179             See L.
180              
181             =head3 ignore_cache => 1 | 0
182              
183             If set, don't cache resource, always create a fresh one instead.
184             See also L.
185              
186             =head3 preload => 1 | 0
187              
188             If set, try loading the resource when Cctl-Epreload> is called.
189             Useful if you want to throw errors when a service is starting,
190             not during request processing.
191              
192             =head3 cleanup => sub { $resource_instance }
193              
194             Undo the init procedure.
195             Usually it is assumed that the resource will do it by itself in the destructor,
196             e.g. that's what a L connection would do.
197             However, if it's not the case, or resources refer circularly to one another,
198             a manual "destructor" may be specified.
199              
200             It only accepts the resource itself as an argument and will be called before
201             erasing the object from the cache.
202              
203             See also C.
204              
205             =head3 cleanup_order => $number
206              
207             The higher the number, the later the resource will get destroyed.
208              
209             The default is 0, negative numbers are also valid, if that makes sense for
210             you application
211             (e.g. destroy C<$my_service_main_object> before the resources it consumes).
212              
213             =head3 fork_cleanup => sub { $resource_instance }
214              
215             Like C, but only in case a change in process ID was detected.
216             See L
217              
218             This may be useful if cleanup is destructive and shouldn't be performed twice.
219              
220             =head3 dependencies => \@list
221              
222             List other resources that may be requested in the initializer.
223             Unless C is specified (see below),
224             the dependencies I be declared I the dependant.
225              
226             A resource with parameter may also depend on itself.
227              
228             The default is all eligible resources known so far.
229              
230             B This behavior was different prior to v.0.09
231             and may be change again in the near future.
232              
233             This parameter has a different structure
234             if C parameter is in action (see below).
235              
236             =head3 loose_deps => 1|0
237              
238             Allow dependencies that have not been declared yet.
239              
240             Not specifying the C parameter would now mean
241             there are no restrictions whatsoever.
242              
243             B Having to resort to this flag may be
244             a sign of a deeper architectural problem.
245              
246             =head3 class => 'Class::Name'
247              
248             Turn on Spring-style dependency injection.
249             This forbids C and C parameters
250             and requires C to be a hash.
251              
252             The dependencies' keys become the arguments to Cnew>,
253             and the values format is as follows:
254              
255             =over
256              
257             =item * argument_name => resource_name
258              
259             Use a resource without parameter;
260              
261             =item * argument_name => [ resource_name => argument ]
262              
263             Use a parametric resource;
264              
265             =item * resource_name => 1
266              
267             Shorthand for C resource_name>;
268              
269             =item * name => \$literal_value
270              
271             Pass $literal_value to the constructor as is.
272              
273             =back
274              
275             So this:
276              
277             resource foo =>
278             class => 'My::Foo',
279             dependencies => {
280             dbh => 1,
281             redis => [ redis => 'session' ],
282             version => \3.14,
283             };
284              
285             Is roughly equivalent to:
286              
287             resource foo =>
288             dependencies => [ 'dbh', 'redis' ],
289             init => sub {
290             my $c = shift;
291             require My::Foo;
292             My::Foo->new(
293             dbh => $c->dbh,
294             redis => $c->redis('session'),
295             version => 3.14,
296             );
297             };
298              
299             =head3 require => 'Module::Name' || \@module_list
300              
301             Load module(s) specified before calling the initializer.
302              
303             This is exactly the same as calling require 'Module::Name' in the initializer
304             itself except that it's more explicit.
305              
306             =head2 silo
307              
308             A re-exportable singleton function returning
309             one and true L instance
310             associated with the class where the resources were declared.
311              
312             B Calling C from a different module will
313             create a I container instance. You'll have to re-export
314             (or otherwise provide access to) this function.
315              
316             I
317             within the same interpreter without interference.>
318              
319             Cnew> will create a new instance of the I container class.
320              
321             =cut
322              
323 29     29   231 use Carp;
  29         72  
  29         1891  
324 29     29   167 use Exporter;
  29         52  
  29         1001  
325 29     29   154 use Scalar::Util qw( set_prototype );
  29         59  
  29         1364  
326              
327 29     29   11841 use Resource::Silo::Metadata;
  29         81  
  29         1047  
328 29     29   12621 use Resource::Silo::Container;
  29         85  
  29         4708  
329              
330             # Store definitions here
331             our %metadata;
332              
333             sub import {
334 36     36   363 my ($self, @param) = @_;
335 36         110 my $caller = caller;
336 36         430 my $target;
337              
338 36         140 while (@param) {
339 15         30 my $flag = shift @param;
340 15 100       49 if ($flag eq '-class') {
341 14         40 $target = $caller;
342             } else {
343             # TODO if there's more than 3 elsifs, use jump table instead
344 1         155 croak "Unexpected parameter to 'use $self': '$flag'";
345             };
346             };
347              
348 35   66     244 $target ||= __PACKAGE__."::container::".$caller;
349              
350 35         213 my $spec = Resource::Silo::Metadata->new($target);
351 35         112 $metadata{$target} = $spec;
352              
353 35         61 my $instance;
354             my $silo = set_prototype {
355             # cannot instantiate target until the package is fully defined,
356             # thus go lazy
357 65   66 65   44868 $instance //= $target->new;
358 35         445 } '';
359              
360 29     29   202 no strict 'refs'; ## no critic
  29         54  
  29         921  
361 29     29   161 no warnings 'redefine', 'once'; ## no critic
  29         51  
  29         5320  
362              
363 35         81 push @{"${target}::ISA"}, 'Resource::Silo::Container';
  35         470  
364              
365 35         73 push @{"${caller}::ISA"}, 'Exporter';
  35         281  
366 35         76 push @{"${caller}::EXPORT"}, qw(silo);
  35         128  
367 35         151 *{"${caller}::resource"} = $spec->_make_dsl;
  35         180  
368 35         73 *{"${caller}::silo"} = $silo;
  35         43370  
369             };
370              
371             =head1 TESTING: LOCK AND OVERRIDES
372              
373             It's usually a bad idea to access real-world resources in one's test suite,
374             especially if it's e.g. a partner's endpoint.
375              
376             Now the #1 rule when it comes to mocks is to avoid mocks and instead design
377             the modules in such a way that they can be tested in isolation.
378             This however may not always be easily achievable.
379              
380             Thus, L provides a mechanism to substitute a subset of resources
381             with mocks and forbid the instantiation of the rest, thereby guarding against
382             unwanted side-effects.
383              
384             The C/C methods in L,
385             available via Cctl> frontend,
386             temporarily forbid instantiating new resources.
387             The resources already in cache will still be OK though.
388              
389             The C method allows to supply substitutes for resources or
390             their initializers.
391              
392             The C flag in the resource definition may be used to indicate
393             that a resource is safe to instantiate as long as its dependencies are
394             either instantiated or mocked, e.g. a L schema is probably fine
395             as long as the underlying database connection is taken care of.
396              
397             Here is an example:
398              
399             use Test::More;
400             use My::Project qw(silo);
401             silo->ctl->lock->override(
402             dbh => DBI->connect( 'dbi:SQLite:database=:memory:', '', '', { RaiseError => 1 ),
403             );
404              
405             silo->dbh; # a mocked database
406             silo->schema; # a DBIx::Class schema reliant on the dbh
407             silo->endpoint( 'partner' ); # an exception as endpoint wasn't mocked
408              
409             =head1 CAVEATS AND CONSIDERATIONS
410              
411             See L for resource container implementation.
412              
413             =head2 CACHING
414              
415             All resources are cached, the ones with arguments are cached together
416             with the argument.
417              
418             =head2 FORKING
419              
420             If the process forks, resources such as database handles may become invalid
421             or interfere with other processes' copies.
422             As of current, if a change in the process ID is detected,
423             the resource cache is reset altogether.
424              
425             This may change in the future as some resources
426             (e.g. configurations or endpoint URLs) are stateless and may be preserved.
427              
428             =head2 CIRCULAR DEPENDENCIES
429              
430             If a resource depends on other resources,
431             those will be simply created upon request.
432              
433             It is possible to make several resources depend on each other.
434             Trying to initialize such resource will cause an expection, however.
435              
436             =head2 COMPATIBILITY
437              
438             L uses L internally and is therefore compatible with
439             both L and L when in C<-class> mode:
440              
441             package My::App;
442              
443             use Moose;
444             use Resource::Silo -class;
445              
446             has path => is => 'ro', default => sub { '/dev/null' };
447             resource fd => sub {
448             my $self = shift;
449             open my $fd, "<", $self->path;
450             return $fd;
451             };
452              
453             Extending such mixed classes will also work.
454             However, the resource definitions will be taken
455             from the nearest ancestor that has them, using breadth first search.
456              
457             =head1 MORE EXAMPLES
458              
459             =head2 Resources with just the init
460              
461             package My::App;
462             use Resource::Silo;
463              
464             resource config => sub {
465             require YAML::XS;
466             YAML::XS::LoadFile( "/etc/myapp.yaml" );
467             };
468              
469             resource dbh => sub {
470             require DBI;
471             my $self = shift;
472             my $conf = $self->config->{database};
473             DBI->connect(
474             $conf->{dbi}, $conf->{username}, $conf->{password}, { RaiseError => 1 }
475             );
476             };
477              
478             resource user_agent => sub {
479             require LWP::UserAgent;
480             LWP::UserAgent->new();
481             # set your custom UserAgent header or SSL certificate(s) here
482             };
483              
484             Note that though lazy-loading the modules is not necessary,
485             it may speed up loading support scripts.
486              
487             =head2 Resources with extra options
488              
489             resource logger =>
490             cleanup_order => 9e9, # destroy as late as possible
491             require => [ 'Log::Any', 'Log::Any::Adapter' ],
492             init => sub {
493             Log::Any::Adapter->set( 'Stderr' );
494             # your rsyslog config could be here
495             Log::Any->get_logger;
496             };
497              
498             resource schema =>
499             derived => 1, # merely a frontend to dbi
500             require => 'My::App::Schema',
501             init => sub {
502             my $self = shift;
503             return My::App::Schema->connect( sub { $self->dbh } );
504             };
505              
506             =head2 Resource with parameter
507              
508             An useless but short example:
509              
510             #!/usr/bin/env perl
511              
512             use strict;
513             use warnings;
514             use Resource::Silo;
515              
516             resource fibonacci =>
517             argument => qr(\d+),
518             init => sub {
519             my ($self, $name, $arg) = @_;
520             $arg <= 1 ? $arg
521             : $self->fibonacci($arg-1) + $self->fibonacci($arg-2);
522             };
523              
524             print silo->fibonacci(shift);
525              
526             A more pragmatic one:
527              
528             package My::App;
529             use Resource::Silo;
530              
531             resource redis_conn => sub {
532             my $self = shift;
533             require Redis;
534             Redis->new( server => $self->config->{redis} );
535             };
536              
537             my %known_namespaces = (
538             lock => 1,
539             session => 1,
540             user => 1,
541             );
542              
543             resource redis =>
544             argument => sub { $known_namespaces{ $_ } },
545             require => 'Redis::Namespace',
546             init => sub {
547             my ($self, $name, $ns) = @_;
548             Redis::Namespace->new(
549             redis => $self->redis,
550             namespace => $ns,
551             );
552             };
553              
554             # later in the code
555             silo->redis; # nope!
556             silo->redis('session'); # get a prefixed namespace
557              
558             =head3 Overriding in test files
559              
560             use Test::More;
561             use My::App qw(silo);
562              
563             silo->ctl->override( dbh => $temp_sqlite_connection );
564             silo->ctl->lock;
565              
566             my $stuff = My::App::Stuff->new();
567             $stuff->frobnicate( ... ); # will only affect the sqlite instance
568              
569             $stuff->ping_partner_api(); # oops! the user_agent resource wasn't
570             # overridden, so there'll be an exception
571              
572             =head3 Fetching a dedicated resource instance
573              
574             use My::App qw(silo);
575             my $dbh = silo->ctl->fresh('dbh');
576              
577             $dbh->begin_work;
578             # Perform a Big Scary Update here
579             # Any operations on $dbh won't interfere with normal usage
580             # of silo->dbh by other application classes.
581              
582             =head1 SEE ALSO
583              
584             L - a more mature IoC / DI framework.
585              
586             =head1 ACKNOWLEDGEMENTS
587              
588             =over
589              
590             =item * This module was names after a building in the game
591             B>
592              
593             =item * This module was inspired in part by my work for
594             L.
595             That was a great time and I had great coworkers!
596              
597             =back
598              
599             =head1 BUGS
600              
601             This software is still in beta stage. Its interface is still evolving.
602              
603             Version 0.09 brings a breaking change that forbids forward dependencies.
604              
605             Please report bug reports and feature requests to
606             L
607             or via RT:
608             L.
609              
610             =head1 SUPPORT
611              
612             You can find documentation for this module with the perldoc command.
613              
614             perldoc Resource::Silo
615              
616             You can also look for information at:
617              
618             =over 4
619              
620             =item * RT: CPAN's request tracker (report bugs here)
621              
622             L
623              
624             =item * CPAN Ratings
625              
626             L
627              
628             =item * Search CPAN
629              
630             L
631              
632             =back
633              
634             =head1 COPYRIGHT AND LICENSE
635              
636             Copyright (c) 2023, Konstantin Uvarin, C<< >>
637              
638             This program is free software.
639             You can redistribute it and/or modify it under the terms of either:
640             the GNU General Public License as published by the Free Software Foundation,
641             or the Artistic License.
642              
643             See L for more information.
644              
645             =cut
646              
647             1; # End of Resource::Silo