Documentation tweak.
[irspy-moved-to-github.git] / lib / ZOOM / Pod.pm
1 # $Id: Pod.pm,v 1.11 2006-06-06 16:27:56 mike Exp $
2
3 package ZOOM::Pod;
4
5 use strict;
6 use warnings;
7
8 use ZOOM;
9
10 BEGIN {
11     # Just register the name
12     ZOOM::Log::mask_str("pod");
13     ZOOM::Log::mask_str("pod_unhandled");
14 }
15
16 =head1 NAME
17
18 ZOOM::Pod - Perl extension for handling pods of concurrent ZOOM connections
19
20 =head1 SYNOPSIS
21
22  use ZOOM::Pod;
23
24  $pod = new ZOOM::Pod("bagel.indexdata.com/gils",
25                       "bagel.indexdata.com/marc");
26  $pod->callback(ZOOM::Event::RECV_SEARCH, \&completed_search);
27  $pod->callback(ZOOM::Event::RECV_RECORD, \&got_record);
28  $pod->search_pqf("the");
29  $err = $pod->wait();
30  die "$pod->wait() failed with error $err" if $err;
31
32  sub completed_search {
33      ($conn, undef, $rs) = @_;
34      print $conn->option("host"), ": found ", $rs->size(), " records\n";
35      $rs->records(0, 1, 0); # Queues a request for the record
36      return 0;
37  }
38
39  sub got_record {
40      ($conn, undef, $rs) = @_;
41      $rec = $rs->record(0);
42      print $conn->option("host"), ": got $rec = '", $rec->render(), "'\n";
43      return 0;
44  }
45
46 =head1 DESCRIPTION
47
48 C<ZOOM:Pod> provides an API that simplifies asynchronous programming
49 using ZOOM.  A pod is a collection of asynchronous connections that
50 are run simultaneously to achieve broadcast searching and retrieval.
51 When a pod is created, a set of connections (or target-strings to
52 connect to) are specified.  Thereafter, they are treated as a unit,
53 and methods for searching, option-setting, etc. that are invoked on
54 the pod are delegated to each of its members.
55
56 The key method on a pod is C<wait()>, which enters a loop accepting
57 and dispatching events occurring on any of the connections in the pod.
58 Unless interrupted,the loop runs until there are no more events left,
59 i.e. no searches are outstanding and no requested records have still
60 to be received.
61
62 Event dispatching is done by means of callback functions, which can be
63 registered for each event.  A registered callback is invoked whenever
64 a corresponding event occurs.  A special callback can be nominated to
65 handle errors.
66
67 =head1 METHODS
68
69 =head2 new()
70
71  $pod = new ZOOM::Pod($conn1, $conn2, $conn3);
72  $pod = new ZOOM::Pod("bagel.indexdata.com/gils",
73                       "bagel.indexdata.com/marc");
74
75 Creates a new pod containing one or more connections.  Each connection
76 may be specified either by an existing C<ZOOM::Connection> object,
77 which I<must> be asynchronous; or by a ZOOM target string, in which
78 case the pod module will make the connection object itself.
79
80 If the constructor's first argument is a number, then it is taken as a
81 limit on the number of connections to handle at any one time.  In this
82 case, the pod initially multiplexes between the first I<n>
83 connections, and brings further connections into the active subset
84 whenever already-active connections are closed.
85
86 Returns the new pod.
87
88 =cut
89
90 sub new {
91     my $class = shift();
92     my(@conn) = @_;
93
94     die "$class with no connections" if @conn == 0;
95     my @state; # Hashrefs with application state associated with connections
96     foreach my $conn (@conn) {
97         if (!ref $conn) {
98             $conn = new ZOOM::Connection($conn, 0, async => 1);
99             # The $conn object is always made, even if no there's no
100             # server.  Such errors are caught later, by the _check()
101             # call in wait(). 
102         }
103         push @state, {};
104     }
105
106     return bless {
107         conn => \@conn,
108         state => \@state,
109         rs => [],
110         callback => {},
111     }, $class;
112 }
113
114 =head2 option()
115
116  $oldElemSet = $pod->option("elementSetName");
117  $pod->option(elementSetName => "b");
118
119 Sets a specified option in all the connections in a pod.  Returns the
120 old value that the option had in first of the connections in the pod:
121 be aware that this value was not necessarily shared by all the members
122 of the pod ... but that is true often enough to be useful.
123
124 =cut
125
126 sub option {
127     my $this = shift();
128     my($key, $value) = @_;
129
130     my $old = $this->{conn}->[0]->option($key);
131     foreach my $conn (@{ $this->{conn} }) {
132         $conn->option($key, $value);
133     }
134
135     return $old;
136 }
137
138 =head2 callback()
139
140  $pod->callback(ZOOM::Event::RECV_SEARCH, \&completed_search);
141  $pod->callback("exception", sub { print "never mind: $@\n"; return 0 } );
142
143 Registers a callback to be invoked by the pod when an event happens.
144 Callback functions are invoked by C<wait()> (q.v.).
145
146 When registering a callback, the first argument is an event-code - one
147 of those defined in the C<ZOOM::Event> enumeration - and the second is
148 a function reference, or equivalently an inline code-fragment.  It is
149 acceptable to nominate the same function as the callback for multiple
150 events, by multiple invocations of C<callback()>.
151
152 When an event occurs during the execution of C<wait()>, the relevant
153 callback function is called with four arguments: the connection that the
154 event happened on; a state hash-reference associated with the
155 connection; the result-set associated with the connection; and the
156 event-type (so that a single function that handles events of multiple
157 types can switch on the code where necessary).  The callback function
158 can handle the event as it wishes, finishing up by returning an
159 integer.  If this is zero, then C<wait()> continues as normal; if it
160 is anything else, then that value is immediately returned from
161 C<wait()>.
162
163 So a simple event-handler might look like this:
164
165  sub got_event {
166       ($conn, $state, $rs, $event) = @_;
167       print "event $event on connection ", $conn->option("host"), "\n";
168       print "Found ", $rs->size(), " records\n"
169           if $event == ZOOM::Event::RECV_SEARCH;
170       return 0;
171  }
172
173 In addition to the event-type callbacks discussed above, there is a
174 special callback, C<"exception">, which is invoked if an exception
175 occurs.  This will nearly always be a ZOOM error, but this can be
176 tested using C<$exception-E<gt>isa("ZOOM::Exception")>.  This callback is
177 invoked with the same arguments as described above, except that
178 instead of the event-type, the fourth argument is a copy of the
179 exception, C<$@>.  Exception-handling callbacks may of course re-throw
180 the exception using C<die $exception>.
181
182 So a simple error-handler might look like this:
183
184  sub got_error {
185       ($conn, $state, $rs, $exception) = @_;
186       if ($exception->isa("ZOOM::Exception")) {
187           print "Caught error $exception - continuing";
188           return 0;
189       }
190       die $exception;
191  }
192
193 The C<$state> argument is a reference to an initially empty hash,
194 which the application can use as it sees fit, to store its own
195 connection-relation information.  For example, an application might
196 use C<$state-E<gt>{last}> to keep a record of which was the last record
197 retrieved from the associated connection.  The pod module itself does
198 not use the state hash at all, and applications are also welcome to
199 ignore it if they do not need it.
200
201 =cut
202
203 sub callback {
204     my $this = shift();
205     my($event, $sub) = @_;
206
207     my $old = $this->{callback}->{$event};
208     $this->{callback}->{$event} = $sub
209         if defined $sub;
210
211     return $old;
212 }
213
214 =head2 search_pqf()
215
216  $pod->search_pqf("@attr 1=1003 wedel");
217
218 Submits the specified query to each of the connections in a pod,
219 delegating to the same-named method of the C<ZOOM::Connection> class
220 and storing each result in a result-set object associated with the
221 connection that generated it.  Returns no value: success or failure
222 must subsequently be detected by inspecting the events and exceptions
223 generated by C<wait()>ing on the pod.
224
225 B<WARNING!>
226 An important simplifying assumption is that each connection can only
227 have one search active on it at a time: this allows the pod to
228 maintain the one-to-one mapping between connections and result-sets.
229 Submitting a new search on a connection before the old one has
230 completed will result in a total failure in the nature of causality,
231 and the spontaneous existence-failure of the universe.  Do not do
232 this.
233
234 =cut
235
236 sub search_pqf {
237     my $this = shift();
238     my($pqf) = @_;
239
240     foreach my $i (0..@{ $this->{conn} }-1) {
241         $this->{rs}->[$i] = $this->{conn}->[$i]->search_pqf($pqf);
242     }
243 }
244
245 =head2 wait()
246
247  $err = $pod->wait();
248  die "$pod->wait() failed with error $err" if $err;
249
250 Waits for events on the connections that make up the pod, usually
251 continuing until there are no more events left and then returning
252 zero.  Whenever an event occurs, a callback function is dispatched as
253 described above; if
254 that function returns a non-zero value, then C<wait()> terminates
255 immediately, whether or not any events remain, and returns that value.
256
257 If an error occurs on one of the connection in the pod, then it is
258 normally thrown as a C<ZOOM::Exception>.  If, however, there is a
259 special C<"exception"> callback registered, then the exception object
260 is passed to this instead.  As usual, the return value of the callback
261 indicates whether C<wait()> should continue (return-value 0) or return
262 immediately (any other value).  Exception-handling callbacks may of
263 course re-throw the exception.
264
265 =cut
266
267 sub wait {
268     my $this = shift();
269     my $res = 0;
270
271     while ((my $i = ZOOM::event($this->{conn})) != 0) {
272         my $conn = $this->{conn}->[$i-1];
273         my $ev = $conn->last_event();
274         my $evstr = ZOOM::event_str($ev);
275         ZOOM::Log::log("pod", "connection ", $i-1, ": event $ev ($evstr)");
276
277         eval {
278             $conn->_check();
279         }; if ($@) {
280             my $sub = $this->{callback}->{exception};
281             die $@ if !defined $sub;
282             $res = &$sub($conn, $this->{state}->[$i-1],
283                          $this->{rs}->[$i-1], $@);
284             last if $res != 0;
285             next;
286         }
287
288         my $sub = $this->{callback}->{$ev};
289         if (defined $sub) {
290             $res = &$sub($conn, $this->{state}->[$i-1],
291                          $this->{rs}->[$i-1], $ev);
292             last if $res != 0;
293         } else {
294             ZOOM::Log::log("pod_unhandled", "connection ", $i-1, ": unhandled event $ev ($evstr)");
295         }
296     }
297
298     return $res;
299 }
300
301
302 =head1 LOGGING
303
304 This module generates logging messages using C<ZOOM::Log::log()>,
305 which in turn relies on the YAZ logging facilities.  It uses two
306 logging levels:
307
308 =over 4
309
310 =item pod
311
312 Logs all events.
313
314 =item pod_unhandled
315
316 Logs unhandled events, i.e. events of types for which no callback has
317 been registered.
318
319 =back
320
321 These logging levels can be turned on by setting the C<YAZ_LOG>
322 environment variable to C<pod,pod_unhandled>.
323
324 =head1 SEE ALSO
325
326 The underlying
327 C<ZOOM>
328 module (part of the
329 C<Net::Z3950::ZOOM>
330 distribution).
331
332 =head1 AUTHOR
333
334 Mike Taylor, E<lt>mike@indexdata.comE<gt>
335
336 =head1 COPYRIGHT AND LICENCE
337
338 Copyright (C) 2006 by Index Data.
339
340 This library is free software; you can redistribute it and/or modify
341 it under the same terms as Perl itself, either Perl version 5.8.4 or,
342 at your option, any later version of Perl 5 you may have available.
343
344 =cut
345
346
347 1;