fix bad race conditions in parallel schema upgrade, #29163
[freeside.git] / FS / bin / freeside-upgrade
1 #!/usr/bin/perl -w
2
3 use strict;
4 use vars qw( $opt_d $opt_s $opt_q $opt_v $opt_r $opt_c $opt_j $opt_a );
5 use vars qw( $DEBUG $DRY_RUN );
6 use Getopt::Std;
7 use DBD::Pg qw(:async); #for -a
8 use DBIx::DBSchema 0.31; #0.39
9 use FS::UID qw(adminsuidsetup checkeuid datasrc driver_name);
10 use FS::CurrentUser;
11 use FS::Schema qw( dbdef dbdef_dist reload_dbdef );
12 use FS::Misc::prune qw(prune_applications);
13 use FS::Conf;
14 use FS::Record qw(qsearch);
15 use FS::Upgrade qw(upgrade_schema upgrade_config upgrade upgrade_sqlradius);
16
17 my $start = time;
18
19 die "Not running uid freeside!" unless checkeuid();
20
21 getopts("dqrcsja");
22
23 $DEBUG = !$opt_q;
24 #$DEBUG = $opt_v;
25
26 $DRY_RUN = $opt_d;
27
28 my $user = shift or die &usage;
29 $FS::CurrentUser::upgrade_hack = 1;
30 $FS::UID::callback_hack = 1;
31 my $dbh = adminsuidsetup($user);
32 $FS::UID::callback_hack = 0;
33
34 # pass command line opts through to upgrade* routines
35 my %upgrade_opts = (
36   quiet   => $opt_q,
37   verbose => $opt_v,
38   queue   => $opt_j,
39   # others?
40 );
41
42 if ( driver_name =~ /^mysql/i ) { #until 0.39 is required above
43   eval "use DBIx::DBSchema 0.39;";
44   die $@ if $@;
45 }
46
47 #needs to match FS::Schema...
48 my $dbdef_file = "%%%FREESIDE_CONF%%%/dbdef.". datasrc;
49
50 dbdef_create($dbh, $dbdef_file);
51
52 delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload
53 reload_dbdef($dbdef_file);
54
55 warn "Upgrade startup completed in ". (time-$start). " seconds\n"; # if $DEBUG;
56 $start = time;
57
58 #$DBIx::DBSchema::DEBUG = $DEBUG;
59 #$DBIx::DBSchema::Table::DEBUG = $DEBUG;
60 #$DBIx::DBSchema::Index::DEBUG = $DEBUG;
61
62 my @bugfix = ();
63
64 if (dbdef->table('cust_main')->column('agent_custid') && ! $opt_s) { 
65   push @bugfix,
66     "UPDATE cust_main SET agent_custid = NULL where agent_custid = ''";
67
68   push @bugfix,
69     "UPDATE h_cust_main SET agent_custid = NULL where agent_custid = ''"
70       if (dbdef->table('h_cust_main')); 
71 }
72
73 if ( dbdef->table('cgp_rule_condition') &&
74      dbdef->table('cgp_rule_condition')->column('condition') 
75    )
76 {
77   push @bugfix,
78    "ALTER TABLE ${_}cgp_rule_condition RENAME COLUMN condition TO conditionname"
79       for '', 'h_';
80
81 }
82
83 if ( dbdef->table('areacode') and
84      dbdef->table('areacode')->primary_key eq 'code' )
85 {
86   if ( driver_name =~ /^mysql/i ) {
87     push @bugfix, 
88       'ALTER TABLE areacode DROP PRIMARY KEY',
89       'ALTER TABLE areacode ADD COLUMN (areanum int auto_increment primary key)';
90   }
91   else {
92     push @bugfix, 'ALTER TABLE areacode DROP CONSTRAINT areacode_pkey';
93   }
94 }
95
96 if ( dbdef->table('upgrade_journal') ) {
97   push @bugfix, "SELECT SETVAL( 'upgrade_journal_upgradenum_seq',
98                                 ( SELECT MAX(upgradenum) FROM upgrade_journal )
99                               )
100                 ";
101 }
102
103 if ( $DRY_RUN ) {
104   print
105     join(";\n", @bugfix ). ";\n";
106 } elsif ( @bugfix ) {
107
108   foreach my $statement ( @bugfix ) {
109     warn "$statement\n";
110     $dbh->do( $statement )
111       or die "Error: ". $dbh->errstr. "\n executing: $statement";
112   }
113
114   upgrade_schema(%upgrade_opts);
115
116   dbdef_create($dbh, $dbdef_file);
117   delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload
118   reload_dbdef($dbdef_file);
119
120 }
121
122 #you should have run fs-migrate-part_svc ages ago, when you upgraded
123 #from 1.3 to 1.4... if not, it needs to be hooked into -upgrade here or
124 #you'll lose all the part_svc settings it migrates to part_svc_column
125
126 my $conf = new FS::Conf;
127
128 my $dbdef_dist = dbdef_dist(
129   datasrc,
130   { 'queue-no_history' => $conf->exists('queue-no_history') },
131 );
132
133 my @statements = dbdef->sql_update_schema( $dbdef_dist,
134                                            $dbh,
135                                            { 'nullify_default' => 1, },
136                                          );
137
138 #### NEW CUSTOM FIELDS:
139 # 1. prevent new custom field columns from being dropped by upgrade
140 # 2. migrate old virtual fields to real fields (new custom fields)
141 ####
142 my $cfsth = $dbh->prepare("SELECT * FROM part_virtual_field") 
143                                                          or die $dbh->errstr;
144 $cfsth->execute or die $cfsth->errstr;
145 my $cf; 
146 while ( $cf = $cfsth->fetchrow_hashref ) {
147     my $tbl = $cf->{'dbtable'};
148     my $name = $cf->{'name'};
149     $name = lc($name) unless driver_name =~ /^mysql/i;
150
151     @statements = grep { $_ !~ /^\s*ALTER\s+TABLE\s+(h_|)$tbl\s+DROP\s+COLUMN\s+cf_$name\s*$/i }
152                                                                     @statements;
153     push @statements, 
154         "ALTER TABLE $tbl ADD COLUMN cf_$name varchar(".$cf->{'length'}.")"
155      unless (dbdef->table($tbl) && dbdef->table($tbl)->column("cf_$name"));
156     push @statements, 
157         "ALTER TABLE h_$tbl ADD COLUMN cf_$name varchar(".$cf->{'length'}.")"
158      unless (dbdef->table("h_$tbl") && dbdef->table("h_$tbl")->column("cf_$name"));
159 }
160 warn "Custom fields schema upgrade completed";
161
162 @statements = 
163   grep { $_ !~ /^CREATE +INDEX +h_queue/i } #useless, holds up queue insertion
164        @statements;
165
166 unless ( driver_name =~ /^mysql/i ) {
167   #not necessary under non-mysql, takes forever on big db
168   @statements =
169     grep { $_ !~ /^ *ALTER +TABLE +h_queue +ALTER +COLUMN +job +TYPE +varchar\(512\) *$/i }
170          @statements;
171 }
172
173 if ( $opt_c ) {
174
175   @statements =
176     grep { $_ !~ /^ *ALTER +TABLE +(h_)?cdr /i }
177          @statements;
178
179   @statements =
180     grep { $_ !~ /^ *CREATE +INDEX +(h_)?cdr\d+ /i }
181          @statements;
182
183 }
184
185 my $MAX_HANDLES; # undef for now, set it if you want a limit
186
187 if ( $DRY_RUN ) {
188   print
189     join(";\n", @statements ). ";\n";
190   exit;
191 } elsif ( $opt_a ) {
192
193   my @phases = map { [] } 0..4;
194   my $fsupgrade_idx = 1;
195   my %idx_map;
196   foreach (@statements) {
197     if ( /^ *(CREATE|ALTER) +TABLE/ ) {
198       # phase 0: CREATE TABLE, ALTER TABLE
199       push @{ $phases[0] }, $_;
200     } elsif ( /^ *ALTER +INDEX.* RENAME TO dbs_temp(\d+)/ ) {
201       # phase 1: rename index to dbs_temp%d
202       # (see DBIx::DBSchema::Table)
203       # but in this case, uniqueify all the dbs_temps.  This method only works
204       # because they are in the right order to begin with...
205       my $dbstemp_idx = $1;
206       s/dbs_temp$dbstemp_idx/fsupgrade_temp$fsupgrade_idx/;
207       $idx_map{ $dbstemp_idx } = $fsupgrade_idx;
208       push @{ $phases[1] }, $_;
209       $fsupgrade_idx++;
210     } elsif ( /^ *(CREATE|DROP)( +UNIQUE)? +INDEX/ ) {
211       # phase 2: create/drop indices
212       push @{ $phases[2] }, $_;
213     } elsif ( /^ *ALTER +INDEX +dbs_temp(\d+) +RENAME/ ) {
214       # phase 3: rename temp indices back to real ones
215       my $dbstemp_idx = $1;
216       my $mapped_idx = $idx_map{ $dbstemp_idx }
217         or die "unable to remap dbs_temp$1 RENAME statement";
218       s/dbs_temp$dbstemp_idx/fsupgrade_temp$mapped_idx/;
219       push @{ $phases[3] }, $_;
220     } else {
221       # phase 4: everything else (CREATE SEQUENCE, SELECT SETVAL, etc.)
222       push @{ $phases[4] }, $_;
223     }
224   }
225   my $i = 0;
226   my @busy = ();
227   my @free = ();
228   foreach my $phase (@phases) {
229     warn "Starting schema changes, phase $i...\n";
230     while (@$phase or @busy) {
231       # check status of all running tasks
232       my @newbusy;
233       my $failed_clone;
234       for my $clone (@busy) {
235         if ( $clone->pg_ready ) {
236           # then clean it up
237           my $rv = $clone->pg_result && $clone->commit;
238           $failed_clone = $clone if !$rv;
239           push @free, $clone;
240         } else {
241           push @newbusy, $clone;
242         }
243       }
244       if ( $failed_clone ) {
245         my $errstr = $failed_clone->errstr;
246         foreach my $clone (@newbusy, $failed_clone) {
247           $clone->pg_cancel if $clone->{pg_async_status} == 1;
248           $clone->disconnect;
249         }
250         die "$errstr\n";
251       }
252       @busy = @newbusy;
253       if (my $statement = $phase->[0]) {
254         my $clone;
255         if ( @free ) {
256           $clone = shift(@free);
257         } elsif ( !$MAX_HANDLES or 
258                   scalar(@free) + scalar(@busy) < $MAX_HANDLES ) {
259           $clone = $dbh->clone; # this will fail if over the server limit
260         }
261
262         if ( $clone ) {
263           my $rv = $clone->do($statement, {pg_async => PG_ASYNC});
264           if ( $rv ) {
265             warn "$statement\n";
266             shift @{ $phase }; # and actually take the statement off the queue
267             push @busy, $clone;
268           } # else I don't know, wait and retry
269         } # else too many handles, wait and retry
270       } elsif (@busy) {
271         # all statements are dispatched
272         warn "Waiting for phase $i to complete\n";
273         sleep 30;
274       }
275     } # while @$phase or @busy
276     $i++;
277   } # foreach $phase
278   warn "Schema changes complete.\n";
279
280 #  warn "Pre-schema change upgrades completed in ". (time-$start). " seconds\n"; # if $DEBUG;
281 #  $start = time;
282
283 #  dbdef->update_schema( dbdef_dist(datasrc), $dbh );
284 } else { # normal case, run statements sequentially
285   foreach my $statement ( @statements ) {
286     warn "$statement\n";
287     $dbh->do( $statement )
288       or die "Error: ". $dbh->errstr. "\n executing: $statement";
289   }
290 }
291
292 warn "Schema upgrade completed in ". (time-$start). " seconds\n"; # if $DEBUG;
293 $start = time;
294
295 my $hashref = {};
296 $hashref->{dry_run} = 1 if $DRY_RUN;
297 $hashref->{debug} = 1 if $DEBUG && $DRY_RUN;
298 prune_applications($hashref) unless $opt_s;
299
300 warn "Application pruning completed in ". (time-$start). " seconds\n"; # if $DEBUG;
301 $start = time;
302
303 print "\n" if $DRY_RUN;
304
305 if ( $dbh->{Driver}->{Name} =~ /^mysql/i && ! $opt_s ) {
306
307   foreach my $table (qw( svc_acct svc_phone )) {
308
309     my $sth = $dbh->prepare(
310       "SELECT COUNT(*) FROM duplicate_lock WHERE lockname = '$table'"
311     ) or die $dbh->errstr;
312
313     $sth->execute or die $sth->errstr;
314
315     unless ( $sth->fetchrow_arrayref->[0] ) {
316
317       $sth = $dbh->prepare(
318         "INSERT INTO duplicate_lock ( lockname ) VALUES ( '$table' )"
319       ) or die $dbh->errstr;
320
321       $sth->execute or die $sth->errstr;
322
323     }
324
325   }
326
327   warn "Duplication lock creation completed in ". (time-$start). " seconds\n"; # if $DEBUG;
328   $start = time;
329
330 }
331
332 $dbh->commit or die $dbh->errstr;
333
334 dbdef_create($dbh, $dbdef_file);
335
336 $dbh->disconnect or die $dbh->errstr;
337
338 delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload
339 $FS::UID::AutoCommit = 0;
340 $FS::UID::callback_hack = 1;
341 $dbh = adminsuidsetup($user);
342 $FS::UID::callback_hack = 0;
343 unless ( $DRY_RUN || $opt_s ) {
344   my $dir = "%%%FREESIDE_CONF%%%/conf.". datasrc;
345   if (!scalar(qsearch('conf', {}))) {
346     my $error = FS::Conf::init_config($dir);
347     if ($error) {
348       warn "CONFIGURATION UPGRADE FAILED\n";
349       $dbh->rollback or die $dbh->errstr;
350       die $error;
351     }
352   }
353 }
354 $dbh->commit or die $dbh->errstr;
355 $dbh->disconnect or die $dbh->errstr;
356
357 $FS::UID::AutoCommit = 1;
358
359 $dbh = adminsuidsetup($user);
360
361 warn "Re-initialization with updated schema completed in ". (time-$start). " seconds\n"; # if $DEBUG;
362 $start = time;
363
364 #### NEW CUSTOM FIELDS:
365 # 3. migrate old virtual field data to the new custom fields
366 ####
367 $cfsth = $dbh->prepare("SELECT * FROM virtual_field left join part_virtual_field using (vfieldpart)")
368                                                          or die $dbh->errstr;
369 $cfsth->execute or die $cfsth->errstr;
370 my @cfst;
371 while ( $cf = $cfsth->fetchrow_hashref ) {
372     my $tbl = $cf->{'dbtable'};
373     my $name = $cf->{'name'};
374     my $dtable = dbdef->table($tbl);
375     next unless $dtable && $dtable->primary_key; # XXX: warn first?
376     my $pkey = $dtable->primary_key;
377     next unless $dtable->column($pkey)->type =~ /int/i; # XXX: warn first?
378     push @cfst, "UPDATE $tbl set cf_$name = '".$cf->{'value'}."' WHERE $pkey = ".$cf->{'recnum'};
379     push @cfst, "DELETE FROM virtual_field WHERE vfieldnum = ".$cf->{'vfieldnum'};
380 }
381 foreach my $cfst ( @cfst ) {
382     warn "$cfst\n";
383     $dbh->do( $cfst )
384       or die "Error: ". $dbh->errstr. "\n executing: $cfst";
385 }
386 warn "Custom fields data upgrade completed";
387
388 upgrade_config(%upgrade_opts)
389   unless $DRY_RUN || $opt_s;
390
391 $dbh->commit or die $dbh->errstr;
392
393 warn "Config updates completed in ". (time-$start). " seconds\n"; # if $DEBUG;
394 $start = time;
395
396 upgrade(%upgrade_opts)
397   unless $DRY_RUN || $opt_s;
398
399 $dbh->commit or die $dbh->errstr;
400
401 warn "Table updates completed in ". (time-$start). " seconds\n"; # if $DEBUG;
402 $start = time;
403
404 upgrade_sqlradius(%upgrade_opts)
405   unless $DRY_RUN || $opt_s || $opt_r;
406
407 warn "SQL RADIUS updates completed in ". (time-$start). " seconds\n"; # if $DEBUG;
408 $start = time;
409
410 $dbh->commit or die $dbh->errstr;
411 $dbh->disconnect or die $dbh->errstr;
412
413 warn "Final commit and disconnection completed in ". (time-$start). " seconds; upgrade done!\n"; # if $DEBUG;
414
415 ###
416
417 sub dbdef_create { # reverse engineer the schema from the DB and save to file
418   my( $dbh, $file ) = @_;
419   my $dbdef = new_native DBIx::DBSchema $dbh;
420   $dbdef->save($file);
421 }
422
423 sub usage {
424   die "Usage:\n  freeside-upgrade [ -d ] [ -q | -v ] [ -r ] [ -c ] [ -s ] [ -j ] [ -a ] user\n"; 
425 }
426
427 =head1 NAME
428
429 freeside-upgrade - Upgrades database schema for new freeside verisons.
430
431 =head1 SYNOPSIS
432
433   freeside-upgrade [ -d ] [ -q | -v ] [ -r ] [ -c ] [ -s ] [ -j ] [ -a ]
434
435 =head1 DESCRIPTION
436
437 Reads your existing database schema and updates it to match the current schema,
438 adding any columns or tables necessary.
439
440 Also performs other upgrade functions:
441
442 =over 4
443
444 =item Calls FS:: Misc::prune::prune_applications (probably unnecessary every upgrade, but simply won't find any records to change)
445
446 =item If necessary, moves your configuration information from the filesystem in /usr/local/etc/freeside/conf.<datasrc> to the database.
447
448 =back
449
450   [ -d ]: Dry run; output SQL statements (to STDOUT) only, but do not execute
451           them.
452
453   [ -q ]: Run quietly.  This may become the default at some point.
454
455   [ -v ]: Run verbosely, sending debugging information to STDERR.  This is the
456           current default.
457
458   [ -s ]: Schema changes only.  Useful for Pg/slony slaves where the data
459
460   [ -r ]: Skip sqlradius updates.  Useful for occassions where the sqlradius
461           databases may be inaccessible.
462
463   [ -c ]: Skip cdr and h_cdr updates.
464
465           changes will be replicated from the Pg/slony master.
466
467   [ -j ]: Run certain upgrades asychronously from the job queue.  Currently 
468           used only for the 2.x -> 3.x cust_location, cust_pay and part_pkg
469           upgrades.  This may cause odd behavior before the upgrade is
470           complete, so it's recommended only for very large cust_main, cust_pay
471           and/or part_pkg tables that take too long to upgrade.
472
473   [ -a ]: Run schema changes in parallel (Pg only).  DBIx::DBSchema minimum 
474           version 0.41 recommended.  Recommended only for large databases and
475           powerful database servers, to reduce upgrade time.
476
477 =head1 SEE ALSO
478
479 =cut
480