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