File Coverage

blib/lib/Async/Blackboard.pm
Criterion Covered Total %
statement 103 105 98.1
branch 20 24 83.3
condition 3 5 60.0
subroutine 25 25 100.0
pod 14 15 93.3
total 165 174 94.8


line stmt bran cond sub pod time code
1             package Async::Blackboard;
2              
3             =head1 NAME
4              
5             Async::Blackboard - A simple blackboard database and dispatcher.
6              
7             =head1 SYNOPSIS
8              
9             my $blackboard = Async::Blackboard->new();
10              
11             $blackboard->watch([qw( foo bar )], [ $object, "found_foobar" ]);
12             $blackboard->watch(foo => [ $object, "found_foo" ]);
13              
14             $blackboard->put(foo => "First dispatch");
15             # $object->found_foo("First dispatch") is called
16             $blackboard->put(bar => "Second dispatch");
17             # $object->found_foobar("First dispatch", "Second dispatch") is called
18              
19             $blackboard->clear;
20              
21             $blackboard->put(bar => "Future Dispatch");
22             # No dispatch is called...
23             # but $blackboard->get("bar") eq "Future Dispatch"
24              
25             $blackboard->put(foo => "Another dispatch");
26              
27             # Order of the following is undefined, but both are called:
28             #
29             # $object->found_foo("Future dispatch")
30             # $object->found_foobar("Future Dispatch", "Another dispatch")
31              
32             $blackboard->hangup;
33              
34             =head1 DESCRIPTION
35              
36             Async::Blackboard provides a mechanism for describing the a parallizable
37             workflow as a series of merge points. An instance of a given workflow is
38             associated with a blackboard, which might be cloned from a prototype
39             blackboard. The blackboard is a key value store which contains the data
40             necessary to complete the task in question.
41              
42             The description of the workflow is in the form of a type of event listener,
43             which is notified when values are associated with a given set of keys. Once
44             values have been published to the blackboard for all of the keys a given
45             listener is interested in, the listener is invoked given the values. That
46             listener then has the opportunity to provide more values. Used in an
47             asynchornous I/O bound application, this allows the application workflow to be
48             intrinsically optimized for parallelism.
49              
50             =cut
51              
52 3     3   93387 use strict;
  3         10  
  3         253  
53 3     3   18 use warnings FATAL => "all";
  3         5  
  3         150  
54 3     3   15 use Carp qw( confess );
  3         11  
  3         244  
55 3     3   16 use Scalar::Util ();
  3         13  
  3         4608  
56              
57             our $VERSION = "0.3.14";
58              
59             =head1 CONSTRUCTORS
60              
61             =over 4
62              
63             =item new
64              
65             The new constructor takes no arguments. If you wish to initialize a blackboard
66             which is prepopulated, try using the ``build'' constructor, or cloning a
67             blackboard in a partially run state using the ``clone'' method.
68              
69             This is done to maintain the guarantee that each listener is notifed once and
70             only once upon its dependencies being satisifed.
71              
72             =cut
73              
74             sub new {
75 100407     100407 1 177257 my ($class) = @_;
76              
77 100407         579624 bless {
78             -watchers => {},
79             -interests => {},
80             -objects => {},
81             -hungup => 0,
82             }, $class;
83             }
84              
85             =item build watchers => [ ... ]
86              
87             =item build values => [ ... ]
88              
89             =item build watchers => [ ... ], values => [ ... ]
90              
91             Build and return a blackboard prototype, it takes a balanced list of keys and
92             array references, with the keys specifying the method to call and the array
93             reference specifying the argument list. This is a convenience method which is
94             short hand explained by the following example:
95              
96             my $blackboard = Async::Blackboard->new();
97              
98             $blackboard->watch(@$watchers);
99             $blackboard->put(@$values);
100              
101             # This is equivalent to
102             my $blackboard = Async::Blackboard->build(
103             watchers => $watchers,
104             values => $values
105             );
106              
107             =cut
108              
109             sub build {
110 94 50   94 1 3337 confess "Build requires a balanced list of arguments" unless @_ % 2;
111              
112 94         191 my ($class, %args) = @_;
113              
114 94         164 my ($watchers, $values) = @args{qw( watchers values )};
115              
116 94         170 my $blackboard = $class->new();
117              
118 94 100       326 $blackboard->watch(@$watchers) if $watchers;
119 94 50       177 $blackboard->put(@$values) if $values;
120              
121 94         261 return $blackboard;
122             }
123              
124             =back
125              
126             =head1 METHODS
127              
128             =over 4
129              
130             =item hungup
131              
132             Determine whether or not the blackboard has been hung up. A blackboard which
133             has been hung up will stop accepting values and release all watcher references.
134              
135             =cut
136              
137 201401     201401 1 532245 sub hungup { shift->{-hungup} }
138              
139             =item has KEY
140              
141             Returns true if the blackboard has a value for the given key, false otherwise.
142              
143             =cut
144              
145             sub has {
146 101711     101711 1 137570 my ($self, $key) = @_;
147              
148 101711         461189 return exists $self->{-objects}->{$key};
149             }
150              
151             =item get KEY [, KEY .. ]
152              
153             Fetch the value of a key. If given a list of keys and in list context, return
154             the value of each key supplied as a list.
155              
156             =cut
157              
158             sub get {
159 124     124 1 404 my ($self, @keys) = @_;
160              
161 124 50 33     335 if (@keys > 1 && wantarray) {
162 0         0 return map $self->{-objects}->{$_}, @keys;
163             }
164             else {
165 124         439 return $self->{-objects}->{$keys[0]};
166             }
167             }
168              
169             =item watcher KEY
170              
171             =item watcher KEYS
172              
173             Given a key or an array reference of keys, return all watchers interested in
174             the given key.
175              
176             =cut
177              
178             sub watchers {
179 1     1 1 2 my ($self, $keys) = @_;
180              
181 1 50       7 $keys = [ $keys ] unless ref $keys;
182              
183 1         3 return map @{ $self->{-watchers}->{$_} }, @$keys;
  1         10  
184             }
185              
186             =item watched
187              
188             Return a list of all keys currently being watched.
189              
190             =cut
191              
192             sub watched {
193 2     2 1 9 my ($self) = @_;
194              
195 2         5 return keys %{ $self->{-watchers} };
  2         11  
196             }
197              
198             =item watch KEYS, WATCHER
199              
200             =item watch KEY, WATCHER
201              
202             Given an array ref of keys (or a single key as a string) and an array ref
203             describing a watcher, register the watcher for a dispatch when the given data
204             elements are provided. The watcher may be either an array reference to a tuple
205             of [ $object, $method_name ] or a subroutine reference.
206              
207             In the instance that a value has already been provided for this key, the
208             dispatch will happen immediately.
209              
210             Returns a reference to self so the builder pattern can be used.
211              
212             =cut
213              
214             # Create a callback subref from a tuple.
215             sub _callback {
216 96     96   136 my ($self, $object, $method) = @_;
217              
218             return sub {
219 189     189   548 $object->$method(@_);
220 96         345 };
221              
222 0         0 return $self;
223             }
224              
225             # Verify that a watcher has all interests.
226             sub _can_dispatch {
227 903     903   1093 my ($self, $watcher) = @_;
228              
229 903         1828 my $interests = $self->{-interests}->{$watcher};
230              
231 903         1900 return @$interests == grep $self->has($_), @$interests;
232             }
233              
234             # Dispatch this watcher if it's _interests are all available.
235             sub _dispatch {
236 437     437   484 my ($self, $watcher) = @_;
237              
238 437         919 my $interests = $self->{-interests}->{$watcher};
239              
240             # Determine if all _interests for this watcher have defined keys (some
241             # kind of value, including undef).
242 437         497 $watcher->(@{ $self->{-objects} }{@$interests});
  437         1297  
243             }
244              
245             # Add the actual listener.
246             sub _watch {
247 346     346   410 my ($self, $keys, $watcher) = @_;
248              
249 346 100       576 return if $self->hungup;
250              
251 345 100       761 if (ref $watcher eq "ARRAY") {
252 96         278 $watcher = $self->_callback(@$watcher);
253             }
254              
255 345         522 for my $key (@$keys) {
256 408   100     388 push @{ $self->{-watchers}->{$key} ||= [] }, $watcher;
  408         2315  
257             }
258              
259 345         2312 $self->{-interests}->{$watcher} = $keys;
260              
261 345 100       600 $self->_dispatch($watcher) if $self->_can_dispatch($watcher);
262             }
263              
264             sub watch {
265 315     315 1 2562 my ($self, @args) = @_;
266              
267 315         840 while (@args) {
268 346         570 my ($keys, $watcher) = splice @args, 0, 2;
269              
270 346 100       787 unless (ref $keys) {
271 221         399 $keys = [ $keys ];
272             }
273              
274 346         642 $self->_watch($keys, $watcher);
275             }
276             }
277              
278             sub _found {
279 100466     100466   138955 my ($self, $key) = @_;
280              
281 100466         176984 my $watchers = $self->{-watchers}->{$key};
282 100466         198480 my @ready_watchers = grep $self->_can_dispatch($_), @$watchers;
283              
284 100466         2862939 for my $watcher (@ready_watchers)
285             {
286 434         726 $self->_dispatch($watcher);
287              
288             # Break out of the loop if hangup was invoked during dispatching.
289 434 100       9588 last if $self->hungup;
290             }
291             }
292              
293             =item put KEY, VALUE [, KEY, VALUE .. ]
294              
295             Put the given keys in the blackboard and notify all watchers of those keys that
296             the objects have been found, if and only if the value has not already been
297             placed in the blackboard.
298              
299             =cut
300              
301             sub put {
302 100435     100435 1 231187 my ($self, %found) = @_;
303              
304 100435         114658 my @keys;
305              
306 100435         271690 for my $key (grep not($self->has($_)), keys %found) {
307             # Unfortunately, because this API was built this API to accept multiple
308             # values in a single method invocation, it has to check the value of
309             # hangup before every dispatch for hangup to work properly.
310 100466 100       205996 unless ($self->hungup) {
311 100435         277449 $self->{-objects}->{$key} = $found{$key};
312              
313 100435         234763 $self->_found($key);
314             }
315             }
316             }
317              
318             =item weaken KEY
319              
320             Weaken the reference to KEY.
321              
322             When the value placed on the blackboard should *not* have a strong reference
323             (for instance, a circular reference to the blackboard), use this method to
324             weaken the value reference to the value associated with the key.
325              
326             =cut
327              
328             sub weaken {
329 30     30 1 112 my ($self, $key) = @_;
330              
331 30         189 Scalar::Util::weaken $self->{-objects}->{$key};
332             }
333              
334             =item delete KEY [, KEY ...]
335              
336             Given a list of keys, remove them from the blackboard. This method should be
337             used with I, since watchers are not notified that the values are
338             removed but they will be re-notified when a new value is provided.
339              
340             =cut
341              
342             sub remove {
343 31     31 0 119 my ($self, @keys) = @_;
344              
345 31         34 delete @{$self->{-objects}}{@keys};
  31         107  
346             }
347              
348             =item replace KEY, VALUE [, KEY, VALUE .. ]
349              
350             Given a list of key value pairs, replace those values on the blackboard.
351             Replacements have special semantics, unlike calling `remove` and `put` on a
352             single key in succession, calling `replace` will not notify any watchers of the
353             given keys on this blackboard. But watchers waiting for more than one key who
354             have not yet been notified, will get the newer value. Further, replace will
355             dispatch the found event if the key is new.
356              
357             =cut
358              
359             sub replace {
360 62     62 1 375 my ($self, %found) = @_;
361              
362 62         296 my @new_keys;
363              
364 62         114 for my $key (keys %found) {
365 62 100       107 push @new_keys, $key unless $self->has($key);
366              
367 62         235 $self->{-objects}->{$key} = $found{$key};
368             }
369              
370 62         182 $self->_found($_) for @new_keys;
371             }
372              
373             =item clear
374              
375             Clear the blackboard of all values.
376              
377             =cut
378              
379             sub clear {
380 92     92 1 292 my ($self) = @_;
381              
382 92         253 $self->{-objects} = {};
383             }
384              
385             =item hangup
386              
387             Clear all watchers, and stop accepting new values on the blackboard.
388              
389             Once hangup has been called, the blackboard workflow is finished.
390              
391             =cut
392              
393             sub hangup {
394 123     123 1 418 my ($self) = @_;
395              
396 123         238 $self->{-watchers} = {};
397 123         663 $self->{-hungup} = 1;
398             }
399              
400             =item clone
401              
402             Create a clone of this blackboard. This will not dispatch any events, even if
403             the blackboard is prepopulated.
404              
405             The clone is two levels, and the two blackboards will operate independently of
406             one another, but any references stored as values on the blackboard will be
407             shared between the two instances.
408              
409             =cut
410              
411             sub clone {
412 100124     100124 1 2353280 my ($self) = @_;
413              
414 100124         148387 my $class = ref $self;
415              
416 100124         117388 my $objects = { %{ $self->{-objects} } };
  100124         268445  
417 100124         137863 my $watchers = { %{ $self->{-watchers} } };
  100124         218654  
418 100124         134144 my $interests = { %{ $self->{-interests} } };
  100124         222367  
419 100124         218248 my $hangup = $self->hungup;
420              
421 100124         301707 $interests->{$_} = [ @{ $interests->{$_} } ] for keys %$interests;
  124         387  
422 100124         234253 $watchers->{$_} = [ @{ $watchers->{$_} } ] for keys %$watchers;
  124         338  
423              
424 100124         262429 my $clone = $class->new();
425              
426 100124         320700 @$clone{qw( -objects -watchers -interests -hungup )} = ( $objects,
427             $watchers, $interests, $hangup );
428              
429 100124         350912 return $clone;
430             }
431              
432             return __PACKAGE__;
433              
434             =back
435              
436             =head1 BUGS
437              
438             None known, but please submit them to
439             https://github.com/ssmccoy/Async-Blackboard/issues if any are found, or CPAN
440             RT.
441              
442             =head1 LICENSE
443              
444             Copyright (C) 2011, 2012, 2013 Say Media.
445              
446             Distributed under the Artistic License, 2.0.
447              
448             =cut