]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/support-scripts/pingest.pl
efdb2ed5aaa2dd9d184a0349b9c6518385e662ab
[Evergreen.git] / Open-ILS / src / support-scripts / pingest.pl
1 #!/usr/bin/perl
2 # ---------------------------------------------------------------
3 # Copyright © 2013,2014 Merrimack Valley Library Consortium
4 # Jason Stephenson <jstephenson@mvlc.org>
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 # ---------------------------------------------------------------
16 # TODO: Document with POD.
17 # This guy parallelizes a reingest.
18 use strict;
19 use warnings;
20 use DBI;
21 use Getopt::Long;
22
23 # Globals for the command line options: --
24
25 # You will want to adjust the next two based on your database size,
26 # i.e. number of bib records as well as the number of cores on your
27 # database server.  Using roughly number of cores/2 doesn't seem to
28 # have much impact in off peak times.
29 my $batch_size = 10000; # records processed per batch
30 my $max_child  = 8;     # max number of parallel worker processes
31
32 my $skip_browse;  # Skip the browse reingest.
33 my $skip_attrs;   # Skip the record attributes reingest.
34 my $skip_search;  # Skip the search reingest.
35 my $skip_facets;  # Skip the facets reingest.
36 my $skip_display; # Skip the display reingest.
37 my $rebuild_rmsr; # Rebuild reporter.materialized_simple_record.
38 my $start_id;     # start processing at this bib ID.
39 my $end_id;       # stop processing when this bib ID is reached.
40 my $max_duration; # max processing duration in seconds
41 my $help;         # show help text
42 my $opt_pipe;     # Read record ids from STDIN.
43 my $record_attrs; # Record attributes for metabib.reingest_record_attributes.
44
45 # Database connection options with defaults:
46 my $db_user = $ENV{PGUSER} || 'evergreen';
47 my $db_host = $ENV{PGHOST} || 'localhost';
48 my $db_db = $ENV{PGDATABASE} || 'evergreen';
49 my $db_password = $ENV{PGPASSWORD} || 'evergreen';
50 my $db_port = $ENV{PGPORT} || 5432;
51
52 GetOptions(
53     'user=s'         => \$db_user,
54     'host=s'         => \$db_host,
55     'db=s'           => \$db_db,
56     'password=s'     => \$db_password,
57     'port=i'         => \$db_port,
58     'batch-size=i'   => \$batch_size,
59     'max-child=i'    => \$max_child,
60     'skip-browse'    => \$skip_browse,
61     'skip-attrs'     => \$skip_attrs,
62     'skip-search'    => \$skip_search,
63     'skip-facets'    => \$skip_facets,
64     'skip-display'   => \$skip_display,
65     'rebuild-rmsr'   => \$rebuild_rmsr,
66     'start-id=i'     => \$start_id,
67     'end-id=i'       => \$end_id,
68     'pipe'           => \$opt_pipe,
69     'max-duration=i' => \$max_duration,
70     'attr=s@'        => \$record_attrs,
71     'help'           => \$help
72 );
73
74 sub help {
75     print <<HELP;
76
77     $0 --batch-size $batch_size --max-child $max_child \
78         --start-id 1 --end-id 500000 --max-duration 14400
79
80     --batch-size
81         Number of records to process per batch
82
83     --max-child
84         Max number of worker processes
85
86     --skip-browse
87     --skip-attrs
88     --skip-search
89     --skip-facets
90     --skip-display
91         Skip the selected reingest component
92
93     --attr
94         Specify a record attribute for ingest
95         This option can be used more than once to specify multiple
96         attributes to ingest.
97         This option is ignored if --skip-attrs is also given.
98     --rebuild-rmsr
99         Rebuild the reporter.materialized_simple_record table.
100
101     --start-id
102         Start processing at this record ID.
103
104     --end-id
105         Stop processing when this record ID is reached
106
107     --pipe
108         Read record IDs to reingest from standard input.
109         This option conflicts with --start-id and/or --end-id.
110
111     --max-duration
112         Stop processing after this many total seconds have passed.
113
114     --help
115         Show this help text.
116
117 HELP
118     exit;
119 }
120
121 help() if $help;
122
123 # Check for mutually exclusive options:
124 if ($opt_pipe && ($start_id || $end_id)) {
125     warn('Mutually exclusive options');
126     help();
127 }
128
129 my $where = "WHERE deleted = 'f'";
130 if ($start_id && $end_id) {
131     $where .= " AND id BETWEEN $start_id AND $end_id";
132 } elsif ($start_id) {
133     $where .= " AND id >= $start_id";
134 } elsif ($end_id) {
135     $where .= " AND id <= $end_id";
136 }
137
138 # "Gimme the keys!  I'll drive!"
139 my $q = <<END_OF_Q;
140 SELECT id
141 FROM biblio.record_entry
142 $where
143 ORDER BY id ASC
144 END_OF_Q
145
146 # Stuffs needed for looping, tracking how many lists of records we
147 # have, storing the actual list of records, and the list of the lists
148 # of records.
149 my ($count, $lists, $records) = (0,0,[]);
150 my @lol = ();
151 # To do the browse-only ingest:
152 my @blist = ();
153
154 my $start_epoch = time;
155
156 sub duration_expired {
157     return 1 if $max_duration && (time - $start_epoch) >= $max_duration;
158     return 0;
159 }
160
161 # All of the DBI->connect() calls in this file assume that you have
162 # configured the PGHOST, PGPORT, PGDATABASE, PGUSER, and PGPASSWORD
163 # variables in your execution environment.  If you have not, you have
164 # two options:
165 #
166 # 1) configure them
167 #
168 # 2) edit the DBI->connect() calls in this program so that it can
169 # connect to your database.
170
171 # Get the input records from either standard input or the database.
172 my @input;
173 if ($opt_pipe) {
174     while (<STDIN>) {
175         # Assume any string of digits is an id.
176         if (my @subs = /([0-9]+)/g) {
177             push(@input, @subs);
178         }
179     }
180 } else {
181     my $dbh = DBI->connect("DBI:Pg:database=$db_db;host=$db_host;port=$db_port;application_name=pingest",
182                            $db_user, $db_password);
183     @input = @{$dbh->selectcol_arrayref($q)};
184     $dbh->disconnect();
185 }
186
187 foreach my $record (@input) {
188     push(@blist, $record); # separate list of browse-only ingest
189     push(@$records, $record);
190     if (++$count == $batch_size) {
191         $lol[$lists++] = $records;
192         $count = 0;
193         $records = [];
194     }
195 }
196 $lol[$lists++] = $records if ($count); # Last batch is likely to be
197                                        # small.
198
199 # We're going to reuse $count to keep track of the total number of
200 # batches processed.
201 $count = 0;
202
203 # @running keeps track of the running child processes.
204 my @running = ();
205
206 # We start the browse-only ingest before starting the other ingests.
207 browse_ingest(@blist) unless ($skip_browse);
208
209 # We loop until we have processed all of the batches stored in @lol
210 # or the maximum processing duration has been reached.
211 while ($count < $lists) {
212     my $duration_expired = duration_expired();
213
214     if (scalar(@lol) && scalar(@running) < $max_child && !$duration_expired) {
215         # Reuse $records for the lulz.
216         $records = shift(@lol);
217         if ($skip_search && $skip_facets && $skip_attrs && $skip_display) {
218             $count++;
219         } else {
220             reingest($records);
221         }
222     } else {
223         my $pid = wait();
224         if (grep {$_ == $pid} @running) {
225             @running = grep {$_ != $pid} @running;
226             $count++;
227             print "$count of $lists processed\n";
228         }
229     }
230
231     if ($duration_expired && scalar(@running) == 0) {
232         warn "Exiting on max_duration ($max_duration)\n";
233         exit(0);
234     }
235 }
236
237 # Rebuild reporter.materialized_simple_record after the ingests.
238 rmsr_rebuild() if ($rebuild_rmsr);
239
240 # This subroutine forks a process to do the browse-only ingest on the
241 # @blist above.  It cannot be parallelized, but can run in parrallel
242 # to the other ingests.
243 sub browse_ingest {
244     my @list = @_;
245     my $pid = fork();
246     if (!defined($pid)) {
247         die "failed to spawn child";
248     } elsif ($pid > 0) {
249         # Add our browser to the list of running children.
250         push(@running, $pid);
251         # Increment the number of lists, because this list was not
252         # previously counted.
253         $lists++;
254     } elsif ($pid == 0) {
255         my $dbh = DBI->connect("DBI:Pg:database=$db_db;host=$db_host;port=$db_port;application_name=pingest",
256                                $db_user, $db_password);
257         my $sth = $dbh->prepare('SELECT metabib.reingest_metabib_field_entries(bib_id := ?, skip_facet := TRUE, skip_browse := FALSE, skip_search := TRUE, skip_display := TRUE)');
258         foreach (@list) {
259             if ($sth->execute($_)) {
260                 my $crap = $sth->fetchall_arrayref();
261             } else {
262                 warn ("Browse ingest failed for record $_");
263             }
264             if (duration_expired()) {
265                 warn "browse_ingest() stopping on record $_ ".
266                     "after max duration reached\n";
267                 last;
268             }
269         }
270         $dbh->disconnect();
271         exit(0);
272     }
273 }
274
275 # Fork a child to do the other reingests:
276
277 sub reingest {
278     my $list = shift;
279     my $pid = fork();
280     if (!defined($pid)) {
281         die "Failed to spawn a child";
282     } elsif ($pid > 0) {
283         push(@running, $pid);
284     } elsif ($pid == 0) {
285         my $dbh = DBI->connect("DBI:Pg:database=$db_db;host=$db_host;port=$db_port;application_name=pingest",
286                                $db_user, $db_password);
287         reingest_attributes($dbh, $list) unless ($skip_attrs);
288         reingest_field_entries($dbh, $list)
289             unless ($skip_facets && $skip_search && $skip_display);
290         $dbh->disconnect();
291         exit(0);
292     }
293 }
294
295 # Reingest metabib field entries on a list of records.
296 sub reingest_field_entries {
297     my $dbh = shift;
298     my $list = shift;
299     my $sth = $dbh->prepare('SELECT metabib.reingest_metabib_field_entries(bib_id := ?, skip_facet := ?, skip_browse := TRUE, skip_search := ?, skip_display := ?)');
300     # Because reingest uses "skip" options we invert the logic of do variables.
301     $sth->bind_param(2, ($skip_facets) ? 1 : 0);
302     $sth->bind_param(3, ($skip_search) ? 1 : 0);
303     $sth->bind_param(4, ($skip_display) ? 1: 0);
304     foreach (@$list) {
305         $sth->bind_param(1, $_);
306         if ($sth->execute()) {
307             my $crap = $sth->fetchall_arrayref();
308         } else {
309             warn ("metabib.reingest_metabib_field_entries failed for record $_");
310         }
311     }
312 }
313
314 # Reingest record attributes on a list of records.
315 sub reingest_attributes {
316     my $dbh = shift;
317     my $list = shift;
318     my $sth = $dbh->prepare(<<END_OF_INGEST
319 SELECT metabib.reingest_record_attributes(rid := id, prmarc := marc, pattr_list := ?)
320 FROM biblio.record_entry
321 WHERE id = ?
322 END_OF_INGEST
323     );
324     $sth->bind_param(1, $record_attrs);
325     foreach (@$list) {
326         $sth->bind_param(2, $_);
327         if ($sth->execute()) {
328             my $crap = $sth->fetchall_arrayref();
329         } else {
330             warn ("metabib.reingest_record_attributes failed for record $_");
331         }
332     }
333 }
334
335 # Rebuild/refresh reporter.materialized_simple_record
336 sub rmsr_rebuild {
337     print("Rebuilding reporter.materialized_simple_record\n");
338     my $dbh = DBI->connect("DBI:Pg:database=$db_db;host=$db_host;port=$db_port;application_name=pingest",
339                            $db_user, $db_password);
340     $dbh->selectall_arrayref("SELECT reporter.refresh_materialized_simple_record();");
341     $dbh->disconnect();
342 }