/Bucardo.pm

https://github.com/burbon/bucardo · Perl · 9316 lines · 6879 code · 1227 blank · 1210 comment · 752 complexity · 2e8c16c2166d1fbc83b8982baecb7727 MD5 · raw file

  1. #!perl
  2. # -*-mode:cperl; indent-tabs-mode: nil; cperl-indent-level: 4-*-
  3. ## The main Bucardo program
  4. ##
  5. ## This script should only be called via the 'bucardo' program
  6. ##
  7. ## Copyright 2006-2013 Greg Sabino Mullane <greg@endpoint.com>
  8. ##
  9. ## Please visit http://bucardo.org for more information
  10. package Bucardo;
  11. use 5.008003;
  12. use strict;
  13. use warnings;
  14. use utf8;
  15. our $VERSION = '4.99.7';
  16. use DBI 1.51; ## How Perl talks to databases
  17. use DBD::Pg 2.0 qw( :async ); ## How Perl talks to Postgres databases
  18. use DBIx::Safe '1.2.4'; ## Filter out what DB calls customcode may use
  19. use sigtrap qw( die normal-signals ); ## Call die() on HUP, INT, PIPE, or TERM
  20. use Config qw( %Config ); ## Used to map signal names
  21. use File::Spec qw( ); ## For portable file operations
  22. use Data::Dumper qw( Dumper ); ## Used to dump information in email alerts
  23. use POSIX qw( strftime strtod ); ## For grabbing the local timezone, and forcing to NV
  24. use Sys::Hostname qw( hostname ); ## Used for host safety check, and debugging/mail sending
  25. use IO::Handle qw( autoflush ); ## Used to prevent stdout/stderr buffering
  26. use Sys::Syslog qw( openlog syslog ); ## In case we are logging via syslog()
  27. use Net::SMTP qw( ); ## Used to send out email alerts
  28. use boolean qw( true false ); ## Used to send truthiness to MongoDB
  29. use List::Util qw( first ); ## Better than grep
  30. use MIME::Base64 qw( encode_base64
  31. decode_base64 ); ## For making text versions of bytea primary keys
  32. use Time::HiRes
  33. qw(sleep gettimeofday tv_interval); ## For better resolution than the built-in sleep
  34. ## and for timing of events
  35. ## Formatting of Dumper() calls:
  36. $Data::Dumper::Varname = 'BUCARDO';
  37. $Data::Dumper::Indent = 1;
  38. ## Common variables we don't want to declare over and over:
  39. use vars qw($SQL %SQL $sth %sth $count $info);
  40. ## Logging verbosity control
  41. ## See also the 'log_level_number' inside the config hash
  42. use constant {
  43. LOG_WARN => 0, ## Always shown
  44. LOG_TERSE => 1, ## Bare minimum
  45. LOG_NORMAL => 2, ## Normal messages
  46. LOG_VERBOSE => 3, ## Many more details
  47. LOG_DEBUG => 4, ## Firehose: rarely needed
  48. };
  49. ## Map system signal numbers to standard names
  50. ## This allows us to say kill $signumber{HUP} => $pid
  51. my $x = 0;
  52. my %signumber;
  53. for (split(' ', $Config{sig_name})) {
  54. $signumber{$_} = $x++;
  55. }
  56. ## Prevent buffering of output:
  57. *STDOUT->autoflush(1);
  58. *STDERR->autoflush(1);
  59. ## Configuration of DBIx::Safe
  60. ## Specify exactly what database handles are allowed to do within custom code
  61. ## Here, 'strict' means 'inside the main transaction that Bucardo uses to make changes'
  62. my $strict_allow = 'SELECT INSERT UPDATE DELETE quote quote_identifier';
  63. my $nostrict_allow = "$strict_allow COMMIT ROLLBACK NOTIFY SET pg_savepoint pg_release pg_rollback_to";
  64. my %dbix = (
  65. source => {
  66. strict => {
  67. allow_command => $strict_allow,
  68. allow_attribute => '',
  69. allow_regex => '', ## Must be qr{} if not empty
  70. deny_regex => '',
  71. },
  72. notstrict => {
  73. allow_command => $nostrict_allow,
  74. allow_attribute => 'RaiseError PrintError',
  75. allow_regex => [qr{CREATE TEMP TABLE},qr{CREATE(?: UNIQUE)? INDEX}],
  76. deny_regex => '',
  77. },
  78. },
  79. target => {
  80. strict => {
  81. allow_command => $strict_allow,
  82. allow_attribute => '',
  83. allow_regex => '', ## Must be qr{} if not empty
  84. deny_regex => '',
  85. },
  86. notstrict => {
  87. allow_command => $nostrict_allow,
  88. allow_attribute => 'RaiseError PrintError',
  89. allow_regex => [qr{CREATE TEMP TABLE}],
  90. deny_regex => '',
  91. },
  92. }
  93. );
  94. ## Grab our full and shortened host name:
  95. ## Used for the host_safety_check as well as for emails
  96. my $hostname = hostname;
  97. my $shorthost = $hostname;
  98. $shorthost =~ s/^(.+?)\..*/$1/;
  99. ## Items pulled from bucardo_config and shared everywhere:
  100. our %config;
  101. our %config_about;
  102. ## Sequence columns we care about and how to change them via ALTER:
  103. my @sequence_columns = (
  104. ['last_value' => ''],
  105. ['start_value' => 'START WITH'],
  106. ['increment_by' => 'INCREMENT BY'],
  107. ['max_value' => 'MAXVALUE'],
  108. ['min_value' => 'MINVALUE'],
  109. ['is_cycled' => 'BOOL CYCLE'],
  110. ['is_called' => ''],
  111. );
  112. my $sequence_columns = join ',' => map { $_->[0] } @sequence_columns;
  113. ## Output messages per language
  114. our %msg = (
  115. 'en' => {
  116. 'time-day' => q{day},
  117. 'time-days' => q{days},
  118. 'time-hour' => q{hour},
  119. 'time-hours' => q{hours},
  120. 'time-minute' => q{minute},
  121. 'time-minutes' => q{minutes},
  122. 'time-month' => q{month},
  123. 'time-months' => q{months},
  124. 'time-second' => q{second},
  125. 'time-seconds' => q{seconds},
  126. 'time-week' => q{week},
  127. 'time-weeks' => q{weeks},
  128. 'time-year' => q{year},
  129. 'time-years' => q{years},
  130. },
  131. 'fr' => {
  132. 'time-day' => q{jour},
  133. 'time-days' => q{jours},
  134. 'time-hour' => q{heure},
  135. 'time-hours' => q{heures},
  136. 'time-minute' => q{minute},
  137. 'time-minutes' => q{minutes},
  138. 'time-month' => q{mois},
  139. 'time-months' => q{mois},
  140. 'time-second' => q{seconde},
  141. 'time-seconds' => q{secondes},
  142. 'time-week' => q{semaine},
  143. 'time-weeks' => q{semaines},
  144. 'time-year' => q{année},
  145. 'time-years' => q{années},
  146. },
  147. 'de' => {
  148. 'time-day' => q{Tag},
  149. 'time-days' => q{Tag},
  150. 'time-hour' => q{Stunde},
  151. 'time-hours' => q{Stunden},
  152. 'time-minute' => q{Minute},
  153. 'time-minutes' => q{Minuten},
  154. 'time-month' => q{Monat},
  155. 'time-months' => q{Monate},
  156. 'time-second' => q{Sekunde},
  157. 'time-seconds' => q{Sekunden},
  158. 'time-week' => q{Woche},
  159. 'time-weeks' => q{Woche},
  160. 'time-year' => q{Jahr},
  161. 'time-years' => q{Jahr},
  162. },
  163. 'es' => {
  164. 'time-day' => q{día},
  165. 'time-days' => q{días},
  166. 'time-hour' => q{hora},
  167. 'time-hours' => q{horas},
  168. 'time-minute' => q{minuto},
  169. 'time-minutes' => q{minutos},
  170. 'time-month' => q{mes},
  171. 'time-months' => q{meses},
  172. 'time-second' => q{segundo},
  173. 'time-seconds' => q{segundos},
  174. 'time-week' => q{semana},
  175. 'time-weeks' => q{semanas},
  176. 'time-year' => q{año},
  177. 'time-years' => q{años},
  178. },
  179. );
  180. ## use critic
  181. ## Figure out which language to use for output
  182. our $lang = $ENV{LC_ALL} || $ENV{LC_MESSAGES} || $ENV{LANG} || 'en';
  183. $lang = substr($lang,0,2);
  184. ##
  185. ## Everything else is subroutines
  186. ##
  187. sub new {
  188. ## Create a new Bucardo object and return it
  189. ## Takes a hashref of options as the only argument
  190. my $class = shift;
  191. my $params = shift || {};
  192. ## The hash for this object, with default values:
  193. my $self = {
  194. created => scalar localtime,
  195. mcppid => $$,
  196. verbose => 1,
  197. logdest => ['/var/log/bucardo'],
  198. warning_file => '',
  199. logseparate => 0,
  200. logextension => '',
  201. logclean => 0,
  202. dryrun => 0,
  203. sendmail => 1,
  204. extraname => '',
  205. logprefix => 'BC!',
  206. version => $VERSION,
  207. listening => {},
  208. pidmap => {},
  209. exit_on_nosync => 0,
  210. sqlprefix => "/* Bucardo $VERSION */",
  211. };
  212. ## Add any passed-in parameters to our hash:
  213. for (keys %$params) {
  214. $self->{$_} = $params->{$_};
  215. }
  216. ## Transform our hash into a genuine 'Bucardo' object:
  217. bless $self, $class;
  218. ## Remove any previous log files if requested
  219. if ($self->{logclean} && (my @dirs = grep {
  220. $_ !~ /^(?:std(?:out|err)|none|syslog)/
  221. } @{ $self->{logdest} }) ) {
  222. ## If the dir does not exists, silently proceed
  223. for my $dir (@dirs) {
  224. opendir my $dh, $dir or next;
  225. ## We look for any files that start with 'log.bucardo' plus another dot
  226. for my $file (grep { /^log\.bucardo\./ } readdir $dh) {
  227. my $fullfile = File::Spec->catfile( $dir => $file );
  228. unlink $fullfile or warn qq{Could not remove "$fullfile": $!\n};
  229. }
  230. closedir $dh or warn qq{Could not closedir "$dir": $!\n};
  231. }
  232. }
  233. ## Zombie stopper
  234. $SIG{CHLD} = 'IGNORE';
  235. ## Basically, dryrun does a rollback instead of a commit at the final sync step
  236. ## This is not 100% safe, if (for example) you have custom code that reaches
  237. ## outside the database to do things.
  238. if (exists $ENV{BUCARDO_DRYRUN}) {
  239. $self->{dryrun} = 1;
  240. }
  241. if ($self->{dryrun}) {
  242. $self->glog(q{** DRYRUN - Syncs will not be committed! **}, LOG_WARN);
  243. }
  244. ## This gets appended to the process description ($0)
  245. if ($self->{extraname}) {
  246. $self->{extraname} = " ($self->{extraname})";
  247. }
  248. ## Connect to the main Bucardo database
  249. $self->{masterdbh} = $self->connect_database();
  250. ## Load in the configuration information
  251. $self->reload_config_database();
  252. ## Figure out if we are writing emails to a file
  253. $self->{sendmail_file} = $ENV{BUCARDO_EMAIL_DEBUG_FILE} || $config{email_debug_file} || '';
  254. ## Where to store our PID:
  255. $self->{pidfile} = File::Spec->catfile( $config{piddir} => 'bucardo.mcp.pid' );
  256. ## The file to ask all processes to stop:
  257. $self->{stopfile} = File::Spec->catfile( $config{piddir} => $config{stopfile} );
  258. ## Send all log lines starting with "Warning" to a separate file
  259. $self->{warning_file} ||= $config{warning_file};
  260. ## Make sure we are running where we are supposed to be
  261. ## This prevents items in bucardo.db that reference production
  262. ## systems from getting run on QA!
  263. ## ...or at least makes sure people have to work a lot harder
  264. ## to shoot themselves in the foot.
  265. if (length $config{host_safety_check}) {
  266. my $safe = $config{host_safety_check};
  267. my $osafe = $safe;
  268. my $ok = 0;
  269. ## Regular expression
  270. if ($safe =~ s/^~//) {
  271. $ok = 1 if $hostname =~ qr{$safe};
  272. }
  273. ## Set of choices
  274. elsif ($safe =~ s/^=//) {
  275. for my $string (split /,/ => $safe) {
  276. if ($hostname eq $string) {
  277. $ok=1;
  278. last;
  279. }
  280. }
  281. }
  282. ## Simple string
  283. elsif ($safe eq $hostname) {
  284. $ok = 1;
  285. }
  286. if (! $ok) {
  287. warn qq{Cannot start: configured to only run on "$osafe". This is "$hostname"\n};
  288. warn qq{ This is usually done to prevent a configured Bucardo from running\n};
  289. warn qq{ on the wrong host. Please verify the 'db' settings by doing:\n};
  290. warn qq{bucardo list dbs\n};
  291. warn qq{ Once you are sure the bucardo.db table has the correct values,\n};
  292. warn qq{ you can adjust the 'host_safety_check' value\n};
  293. exit 2;
  294. }
  295. }
  296. return $self;
  297. } ## end of new
  298. sub start_mcp {
  299. ## Start the Bucardo daemon. Called by bucardo after setsid()
  300. ## Arguments: one
  301. ## 1. Arrayref of command-line options.
  302. ## Returns: never (exit 0 or exit 1)
  303. my ($self, $opts) = @_;
  304. ## Store the original invocation string, then modify it
  305. my $old0 = $0;
  306. ## May not work on all platforms, of course, but we're gonna try
  307. $0 = "Bucardo Master Control Program v$VERSION.$self->{extraname}";
  308. ## Prefix all lines in the log file with this TLA (until overriden by a forked child)
  309. $self->{logprefix} = 'MCP';
  310. ## If the standard pid file [from new()] already exists, cowardly refuse to run
  311. if (-e $self->{pidfile}) {
  312. ## Grab the PID from the file if we can for better output
  313. my $extra = '';
  314. my $fh;
  315. ## Failing to open is not fatal here, just means no PID shown
  316. if (open ($fh, '<', $self->{pidfile})) {
  317. if (<$fh> =~ /(\d+)/) {
  318. $extra = " (PID=$1)";
  319. }
  320. close $fh or warn qq{Could not close "$self->{pidfile}": $!\n};
  321. }
  322. ## Output to the logfile, to STDERR, then exit
  323. my $msg = qq{File "$self->{pidfile}" already exists$extra: cannot run until it is removed};
  324. $self->glog($msg, LOG_WARN);
  325. warn $msg;
  326. exit 1;
  327. }
  328. ## We also refuse to run if the global stop file exists
  329. if (-e $self->{stopfile}) {
  330. my $msg = qq{Cannot run while this file exists: "$self->{stopfile}"};
  331. $self->glog($msg, LOG_WARN);
  332. warn $msg;
  333. ## Failure to open this file is not fatal
  334. if (open my $fh, '<', $self->{stopfile}) {
  335. ## Read in up to 10 lines from the stopfile and output them
  336. while (<$fh>) {
  337. $msg = "Line $.: $_";
  338. $self->glog($msg, LOG_WARN);
  339. warn $msg;
  340. last if $. > 10;
  341. }
  342. close $fh or warn qq{Could not close "$self->{stopfile}": $!\n};
  343. }
  344. exit 1;
  345. }
  346. ## We are clear to start. Output a quick hello and version to the logfile
  347. $self->glog("Starting Bucardo version $VERSION", LOG_WARN);
  348. $self->glog("Log level: $config{log_level}", LOG_WARN);
  349. ## Close unused file handles.
  350. unless (grep { $_ eq 'stderr' } @{ $self->{logdest} }) {
  351. close STDERR or warn "Could not close STDERR\n";
  352. }
  353. unless (grep { $_ eq 'stdout' } @{ $self->{logdest} }) {
  354. close STDOUT or warn "Could not close STDOUT\n";
  355. }
  356. ## Create a new (temporary) PID file
  357. ## We will overwrite later with a new PID once we do the initial fork
  358. open my $pidfh, '>', $self->{pidfile}
  359. or die qq{Cannot write to $self->{pidfile}: $!\n};
  360. ## Inside our newly created PID file, print out PID on the first line
  361. ## - print how the script was originally invoked on the second line (old $0),
  362. ## - print the current time on the third line
  363. my $now = scalar localtime;
  364. print {$pidfh} "$$\n$old0\n$now\n";
  365. close $pidfh or warn qq{Could not close "$self->{pidfile}": $!\n};
  366. ## Create a pretty Dumped version of the current $self object, with the password elided
  367. ## This is used in the body of emails that may be sent later
  368. ## Squirrel away the old password
  369. my $oldpass = $self->{dbpass};
  370. ## Set to something else
  371. $self->{dbpass} = '<not shown>';
  372. ## Dump the entire object with Data::Dumper (with custom config variables)
  373. my $dump = Dumper $self;
  374. ## Put the password back in place
  375. $self->{dbpass} = $oldpass;
  376. ## Prepare to send an email letting people know we have started up
  377. my $body = qq{
  378. Master Control Program $$ was started on $hostname
  379. Args: $old0
  380. Version: $VERSION
  381. };
  382. my $subject = qq{Bucardo $VERSION started on $shorthost};
  383. ## If someone left a message in the reason file, append it, then delete the file
  384. my $reason = get_reason('delete');
  385. if ($reason) {
  386. $body .= "Reason: $reason\n";
  387. $subject .= " ($reason)";
  388. }
  389. ## Strip leading whitespace from the body (from the qq{} above)
  390. $body =~ s/^\s+//gsm;
  391. ## Send out the email (if sendmail or sendmail_file is enabled)
  392. $self->send_mail({ body => "$body\n\n$dump", subject => $subject });
  393. ## Drop the existing database connection, fork, and get a new one
  394. ## This self-fork helps ensure our survival
  395. my $disconnect_ok = 0;
  396. eval {
  397. ## This connection was set in new()
  398. $self->{masterdbh}->disconnect();
  399. $disconnect_ok = 1;
  400. };
  401. $disconnect_ok or $self->glog("Warning! Disconnect failed $@", LOG_WARN);
  402. my $seeya = fork;
  403. if (! defined $seeya) {
  404. die q{Could not fork mcp!};
  405. }
  406. ## Immediately close the child process (one side of the fork)
  407. if ($seeya) {
  408. exit 0;
  409. }
  410. ## Now that we've forked, overwrite the PID file with our new value
  411. open $pidfh, '>', $self->{pidfile} or die qq{Cannot write to $self->{pidfile}: $!\n};
  412. ## Same format as before: PID, then invocation line, then timestamp
  413. $now = scalar localtime;
  414. print {$pidfh} "$$\n$old0\n$now\n";
  415. close $pidfh or warn qq{Could not close "$self->{pidfile}": $!\n};
  416. ## Reconnect to the master database
  417. ($self->{mcp_backend}, $self->{masterdbh}) = $self->connect_database();
  418. my $masterdbh = $self->{masterdbh};
  419. ## Let any listeners know we have gotten this far
  420. ## (We do this nice and early for impatient watchdog programs)
  421. $self->db_notify($masterdbh, 'boot', 1);
  422. ## Store the function to use to generate clock timestamps
  423. ## We greatly prefer clock_timestamp,
  424. ## but fallback to timeofday() for 8.1 and older
  425. $self->{mcp_clock_timestamp} =
  426. $masterdbh->{pg_server_version} >= 80200
  427. ? 'clock_timestamp()'
  428. : 'timeofday()::timestamptz';
  429. ## Start outputting some interesting things to the log
  430. $self->show_db_version_and_time($masterdbh, 'Master DB ');
  431. $self->glog("PID: $$", LOG_WARN);
  432. $self->glog("Postgres backend PID: $self->{mcp_backend}", LOG_WARN);
  433. $self->glog('Postgres library version: ' . $masterdbh->{pg_lib_version}, LOG_WARN);
  434. $self->glog("bucardo: $old0", LOG_WARN);
  435. $self->glog('Bucardo.pm: ' . $INC{'Bucardo.pm'}, LOG_WARN);
  436. $self->glog((sprintf 'OS: %s Perl: %s %vd', $^O, $^X, $^V), LOG_WARN);
  437. ## Get an integer version of the DBD::Pg version, for later comparisons
  438. if ($DBD::Pg::VERSION !~ /(\d+)\.(\d+)\.(\d+)/) {
  439. die "Could not parse the DBD::Pg version: was $DBD::Pg::VERSION\n";
  440. }
  441. $self->{dbdpgversion} = int (sprintf '%02d%02d%02d', $1,$2,$3);
  442. $self->glog((sprintf 'DBI version: %s DBD::Pg version: %s (%d) DBIx::Safe version: %s',
  443. $DBI::VERSION,
  444. $DBD::Pg::VERSION,
  445. $self->{dbdpgversion},
  446. $DBIx::Safe::VERSION),
  447. LOG_WARN);
  448. ## Store some PIDs for later debugging use
  449. $self->{pidmap}{$$} = 'MCP';
  450. $self->{pidmap}{$self->{mcp_backend}} = 'Bucardo DB';
  451. ## Again with the password trick
  452. $self->{dbpass} = '<not shown>'; ## already saved as $oldpass above
  453. my $objdump = "Bucardo object:\n";
  454. ## Get the maximum key length for pretty formatting
  455. my $maxlen = 5;
  456. for (keys %$self) {
  457. $maxlen = length($_) if length($_) > $maxlen;
  458. }
  459. ## Print each object, aligned, and show 'undef' for undefined values
  460. ## Yes, this prints things like HASH(0x8fbfc84), but we're okay with that
  461. for (sort keys %$self) {
  462. $objdump .= sprintf " %-*s => %s\n", $maxlen, $_, (defined $self->{$_}) ? qq{'$self->{$_}'} : 'undef';
  463. }
  464. $self->glog($objdump, LOG_TERSE);
  465. ## Restore the password
  466. $self->{dbpass} = $oldpass;
  467. ## Dump all configuration variables to the log
  468. $self->log_config();
  469. ## Any other files we find in the piddir directory should be considered old
  470. ## Thus, we can remove them
  471. my $piddir = $config{piddir};
  472. opendir my $dh, $piddir or die qq{Could not opendir "$piddir": $!\n};
  473. ## Nothing else should really be in here, but we will limit with a regex anyway
  474. my @pidfiles = grep { /^bucardo.*\.pid$/ } readdir $dh;
  475. closedir $dh or warn qq{Could not closedir "$piddir" $!\n};
  476. ## Loop through and remove each file found, making a note in the log
  477. for my $pidfile (sort @pidfiles) {
  478. my $fullfile = File::Spec->catfile( $piddir => $pidfile );
  479. ## Do not erase our own file
  480. next if $fullfile eq $self->{pidfile};
  481. ## Everything else can get removed
  482. if (-e $fullfile) {
  483. if (unlink $fullfile) {
  484. $self->glog("Warning: removed old pid file $fullfile", LOG_VERBOSE);
  485. }
  486. else {
  487. ## This will cause problems, but we will drive on
  488. $self->glog("Warning: failed to remove pid file $fullfile", LOG_TERSE);
  489. }
  490. }
  491. }
  492. ## From this point forward, we want to die gracefully
  493. ## We setup our own subroutine to catch any die signals
  494. local $SIG{__DIE__} = sub {
  495. ## Arguments: one
  496. ## 1. The error message
  497. ## Returns: never (exit 1 or exec new process)
  498. my $msg = shift;
  499. my $line = (caller)[2];
  500. $self->glog("Warning: Killed (line $line): $msg", LOG_WARN);
  501. ## The error message determines if we try to resurrect ourselves or not
  502. my $respawn = (
  503. $msg =~ /DBI connect/ ## From DBI
  504. or $msg =~ /Ping failed/ ## Set below
  505. ) ? 1 : 0;
  506. ## Sometimes we don't want to respawn at all (e.g. during some tests)
  507. if (! $config{mcp_dbproblem_sleep}) {
  508. $self->glog('Database problem, but will not attempt a respawn due to mcp_dbproblem_sleep=0', LOG_TERSE);
  509. $respawn = 0;
  510. }
  511. ## Create some output for the mail message
  512. my $diesubject = "Bucardo MCP $$ was killed";
  513. my $diebody = "MCP $$ was killed: $msg";
  514. ## Most times we *do* want to respawn
  515. if ($respawn) {
  516. $self->glog("Database problem, will respawn after a short sleep: $config{mcp_dbproblem_sleep}", LOG_TERSE);
  517. $diebody .= " (will attempt respawn in $config{mcp_dbproblem_sleep} seconds)";
  518. $diesubject .= ' (respawning)';
  519. }
  520. ## Callers can prevent an email being sent by setting this before they die
  521. if (! $self->{clean_exit}) {
  522. $self->send_mail({ body => $diebody, subject => $diesubject });
  523. }
  524. ## Kill kids, remove pidfile, update tables, etc.
  525. $self->cleanup_mcp("Killed: $msg");
  526. ## If we are not respawning, simply exit right now
  527. exit 1 if ! $respawn;
  528. ## We will attempt a restart, but sleep a while first to avoid constant restarts
  529. $self->glog("Sleep time: $config{mcp_dbproblem_sleep}", LOG_TERSE);
  530. sleep($config{mcp_dbproblem_sleep});
  531. ## We assume this is bucardo, and that we are in same directory as when called
  532. my $RUNME = $old0;
  533. ## Check to see if $RUNME is executable as is, before we assume we're in the same directory
  534. if (! -x $RUNME) {
  535. $RUNME = "./$RUNME" if index ($RUNME,'.') != 0;
  536. }
  537. my $reason = 'Attempting automatic respawn after MCP death';
  538. $self->glog("Respawn attempt: $RUNME @{ $opts } start '$reason'", LOG_TERSE);
  539. ## Replace ourselves with a new process running this command
  540. { exec $RUNME, @{ $opts }, 'start', $reason };
  541. $self->glog("Could not exec $RUNME: $!", LOG_WARN);
  542. }; ## end SIG{__DIE_} handler sub
  543. ## This resets listeners, kills kids, and loads/activates syncs
  544. my $active_syncs = $self->reload_mcp();
  545. if (!$active_syncs && $self->{exit_on_nosync}) {
  546. ## No syncs means no reason for us to hang around, so we exit
  547. $self->glog('No active syncs were found, so we are exiting', LOG_WARN);
  548. $self->db_notify($masterdbh, 'nosyncs', 1);
  549. $self->cleanup_mcp('No active syncs');
  550. exit 1;
  551. }
  552. ## Report which syncs are active
  553. $self->glog("Active syncs: $active_syncs", LOG_TERSE);
  554. ## We want to reload everything if someone HUPs us
  555. local $SIG{HUP} = sub {
  556. $self->reload_mcp();
  557. };
  558. ## We need KIDs to tell us their PID so we can deregister them
  559. $self->{kidpidlist} = {};
  560. ## Let any listeners know we have gotten this far
  561. $self->db_notify($masterdbh, 'started', 1);
  562. ## For optimization later on, we need to know which syncs are 'fullcopy'
  563. for my $syncname (keys %{ $self->{sync} }) {
  564. my $s = $self->{sync}{$syncname};
  565. ## Skip inactive syncs
  566. next unless $s->{mcp_active};
  567. ## Walk through each database and check the roles, discarding inactive dbs
  568. my %rolecount;
  569. for my $db (values %{ $s->{db} }) {
  570. next if $db->{status} ne 'active';
  571. $rolecount{$db->{role}}++;
  572. }
  573. ## Default to being fullcopy
  574. $s->{fullcopy} = 1;
  575. ## We cannot be a fullcopy sync if:
  576. if ($rolecount{'target'} ## there are any target dbs
  577. or $rolecount{'source'} > 1 ## there is more than one source db
  578. or ! $rolecount{'fullcopy'}) { ## there are no fullcopy dbs
  579. $s->{fullcopy} = 0;
  580. }
  581. }
  582. ## Because a sync may have gotten a notice while we were down,
  583. ## we auto-kick all eligible syncs
  584. ## We also need to see if we can prevent the VAC daemon from running,
  585. ## if there are no databases with bucardo schemas
  586. $self->{needsvac} = 0;
  587. for my $syncname (keys %{ $self->{sync} }) {
  588. my $s = $self->{sync}{$syncname};
  589. ## Default to starting in a non-kicked mode
  590. $s->{kick_on_startup} = 0;
  591. ## Skip inactive syncs
  592. next unless $s->{mcp_active};
  593. ## Skip fullcopy syncs
  594. next if $s->{fullcopy};
  595. ## Right now, the vac daemon is only useful for source Postgres databases
  596. ## Of course, it is not needed for fullcopy syncs
  597. for my $db (values %{ $s->{db} }) {
  598. if ($db->{status} eq 'active'
  599. and $db->{dbtype} eq 'postgres'
  600. and $db->{role} eq 'source') {
  601. $db->{needsvac}++;
  602. $self->{needsvac} = 1;
  603. }
  604. }
  605. ## Skip if autokick is false
  606. next if ! $s->{autokick};
  607. ## Kick it!
  608. $s->{kick_on_startup} = 1;
  609. }
  610. ## Start the main loop
  611. $self->mcp_main();
  612. die 'We should never reach this point!';
  613. ##
  614. ## Everything from this point forward in start_mcp is subroutines
  615. ##
  616. return; ## no critic
  617. } ## end of start_mcp
  618. sub mcp_main {
  619. ## The main MCP process
  620. ## Arguments: none
  621. ## Returns: never (exit 0 or exit 1)
  622. my $self = shift;
  623. my $maindbh = $self->{masterdbh};
  624. my $sync = $self->{sync};
  625. ## Used to gather up and handle any notices received via the listen/notify system
  626. my $notice;
  627. ## Used to keep track of the last time we pinged the databases
  628. my $lastpingcheck = 0;
  629. ## Keep track of how long since we checked on the VAC daemon
  630. my $lastvaccheck = 0;
  631. $self->glog('Entering main loop', LOG_TERSE);
  632. MCP: {
  633. ## Bail if the stopfile exists
  634. if (-e $self->{stopfile}) {
  635. $self->glog(qq{Found stopfile "$self->{stopfile}": exiting}, LOG_WARN);
  636. my $msg = 'Found stopfile';
  637. ## Grab the reason, if it exists, so we can propagate it onward
  638. my $mcpreason = get_reason(0);
  639. if ($mcpreason) {
  640. $msg .= ": $mcpreason";
  641. }
  642. ## Stop controllers, disconnect, remove PID file, etc.
  643. $self->cleanup_mcp("$msg\n");
  644. $self->glog('Exiting', LOG_WARN);
  645. exit 0;
  646. }
  647. ## Startup the VAC daemon as needed
  648. ## May be off via user configuration, or because of no valid databases
  649. if ($config{bucardo_vac} and $self->{needsvac}) {
  650. ## Check on it occasionally (different than the running time)
  651. if (time() - $lastvaccheck >= $config{mcp_vactime}) {
  652. ## Is it alive? If not, spawn
  653. my $pidfile = "$config{piddir}/bucardo.vac.pid";
  654. if (! -e $pidfile) {
  655. $self->fork_vac();
  656. }
  657. $lastvaccheck = time();
  658. } ## end of time to check vac
  659. } ## end if bucardo_vac
  660. ## Every once in a while, make sure our database connections are still there
  661. if (time() - $lastpingcheck >= $config{mcp_pingtime}) {
  662. ## This message must have "Ping failed" to match the $respawn above
  663. $maindbh->ping or die qq{Ping failed for main database!\n};
  664. ## Check each (pingable) remote database in undefined order
  665. for my $dbname (keys %{ $self->{sdb} }) {
  666. $x = $self->{sdb}{$dbname};
  667. next if $x->{dbtype} =~ /flat|mongo|redis/o;
  668. if (! $x->{dbh}->ping) {
  669. ## Database is not reachable, so we'll try and reconnect
  670. $self->glog("Ping failed for database $dbname, trying to reconnect", LOG_NORMAL);
  671. ## Sleep a hair so we don't reloop constantly
  672. sleep 0.5;
  673. ($x->{backend}, $x->{dbh}) = $self->connect_database($dbname);
  674. if (defined $x->{backend}) {
  675. $self->glog(qq{Database "$dbname" backend PID: $x->{backend}}, LOG_VERBOSE);
  676. $self->show_db_version_and_time($x->{dbh}, qq{Database "$dbname" });
  677. }
  678. else {
  679. $self->glog("Unable to reconnect to database $dbname!", LOG_WARN);
  680. ## We may want to throw an exception if this keeps happening
  681. ## We may also want to adjust lastpingcheck so we check more often
  682. }
  683. }
  684. }
  685. ## Reset our internal counter to 'now'
  686. $lastpingcheck = time();
  687. } ## end of checking database connections
  688. ## Add in any messages from the main database and reset the notice hash
  689. ## Ignore things we may have sent ourselves
  690. my $notice = $self->db_get_notices($maindbh, $self->{mcp_backend});
  691. ## Add in any messages from each remote database
  692. for my $dbname (keys %{ $self->{sdb} }) {
  693. $x = $self->{sdb}{$dbname};
  694. next if $x->{dbtype} ne 'postgres';
  695. ## Start listening for KIDs if we have not done so already
  696. if (! exists $self->{kidpidlist}{$dbname}) {
  697. $self->{kidpidlist}{$dbname} = 1;
  698. $self->db_listen($x->{dbh}, 'kid_pid_start', $dbname, 1);
  699. $self->db_listen($x->{dbh}, 'kid_pid_stop', $dbname, 1);
  700. $x->{dbh}->commit();
  701. }
  702. my $nlist = $self->db_get_notices($x->{dbh});
  703. $x->{dbh}->rollback();
  704. for my $name (keys %{ $nlist } ) {
  705. if (! exists $notice->{$name}) {
  706. $notice->{$name} = $nlist->{$name};
  707. }
  708. else {
  709. for my $pid (keys %{ $nlist->{$name}{pid} }) {
  710. $notice->{$name}{pid}{$pid}++;
  711. }
  712. }
  713. }
  714. }
  715. ## Handle each notice one by one
  716. for my $name (sort keys %{ $notice }) {
  717. my $npid = $notice->{$name}{firstpid};
  718. ## Request to stop everything
  719. if ('mcp_fullstop' eq $name) {
  720. $self->glog("Received full stop notice from PID $npid, leaving", LOG_TERSE);
  721. $self->cleanup_mcp("Received stop NOTICE from PID $npid");
  722. exit 0;
  723. }
  724. ## Request that a named sync get kicked
  725. elsif ($name =~ /^kick_sync_(.+)/o) {
  726. my $syncname = $1;
  727. ## Prepare to send some sort of log message
  728. my $msg = '';
  729. ## We will not kick if this sync does not exist or it is inactive
  730. if (! exists $self->{sync}{$syncname}) {
  731. $msg = qq{Warning: Unknown sync to be kicked: "$syncname"\n};
  732. }
  733. elsif (! $self->{sync}{$syncname}{mcp_active}) {
  734. $msg = qq{Cannot kick inactive sync "$syncname"};
  735. }
  736. ## We also won't kick if this was created by a kid
  737. ## This can happen as our triggerkicks may be set to 'always'
  738. elsif (exists $self->{kidpidlist}{$npid}) {
  739. $self->glog(qq{Not kicking sync "$syncname" as it came from KID $npid}, LOG_DEBUG);
  740. }
  741. else {
  742. ## Kick it!
  743. $sync->{$syncname}{kick_on_startup} = 1;
  744. }
  745. if ($msg) {
  746. $self->glog($msg, LOG_TERSE);
  747. ## As we don't want people to wait around for a syncdone...
  748. $self->db_notify($maindbh, "syncerror_$syncname", 1);
  749. }
  750. }
  751. ## A sync has finished
  752. elsif ($name =~ /^syncdone_(.+)/o) {
  753. my $syncdone = $1;
  754. $self->glog("Sync $syncdone has finished", LOG_DEBUG);
  755. ## Echo out to anyone listening
  756. $self->db_notify($maindbh, $name, 1);
  757. ## If this was a onetimecopy sync, flip it off
  758. $sync->{$syncdone}{onetimecopy} = 0;
  759. }
  760. ## A sync has been killed
  761. elsif ($name =~ /^synckill_(.+)/o) {
  762. my $syncdone = $1;
  763. $self->glog("Sync $syncdone has been killed", LOG_DEBUG);
  764. ## Echo out to anyone listening
  765. $self->db_notify($maindbh, $name, 1);
  766. }
  767. ## Request to reload the configuration file
  768. elsif ('reload_config' eq $name) {
  769. $self->glog('Reloading configuration table', LOG_TERSE);
  770. $self->reload_config_database();
  771. ## Output all values to the log file again
  772. $self->log_config();
  773. ## We need to reload ourself as well
  774. $self->reload_mcp();
  775. ## Let anyone listening know we are done
  776. $self->db_notify($maindbh, 'reload_config_finished', 1);
  777. }
  778. ## Request to reload the MCP
  779. elsif ('mcp_reload' eq $name) {
  780. $self->glog('Reloading MCP', LOG_TERSE);
  781. $self->reload_mcp();
  782. ## Let anyone listening know we are done
  783. $self->db_notify($maindbh, 'reloaded_mcp', 1);
  784. }
  785. ## Request for a ping via listen/notify
  786. elsif ('mcp_ping' eq $name) {
  787. $self->glog("Got a ping from PID $npid, issuing pong", LOG_DEBUG);
  788. $self->db_notify($maindbh, 'mcp_pong', 1);
  789. }
  790. ## Request that we parse and empty the log message table
  791. elsif ('log_message' eq $name) {
  792. $self->glog('Checking for log messages', LOG_DEBUG);
  793. $SQL = 'SELECT msg,cdate FROM bucardo_log_message ORDER BY cdate';
  794. my $sth = $maindbh->prepare_cached($SQL);
  795. $count = $sth->execute();
  796. if ($count ne '0E0') {
  797. for my $row (@{$sth->fetchall_arrayref()}) {
  798. $self->glog("MESSAGE ($row->[1]): $row->[0]", LOG_VERBOSE);
  799. }
  800. $maindbh->do('TRUNCATE TABLE bucardo_log_message');
  801. $maindbh->commit();
  802. }
  803. else {
  804. $sth->finish();
  805. }
  806. }
  807. ## Request that a named sync get reloaded
  808. elsif ($name =~ /^reload_sync_(.+)/o) {
  809. my $syncname = $1;
  810. ## Skip if the sync does not exist or is inactive
  811. if (! exists $sync->{$syncname}) {
  812. $self->glog(qq{Invalid sync reload: "$syncname"}, LOG_TERSE);
  813. }
  814. elsif (!$sync->{$syncname}{mcp_active}) {
  815. $self->glog(qq{Cannot reload: sync "$syncname" is not active}, LOG_TERSE);
  816. }
  817. else {
  818. $self->glog(qq{Deactivating sync "$syncname"}, LOG_TERSE);
  819. $self->deactivate_sync($sync->{$syncname});
  820. ## Reread from the database
  821. $SQL = q{SELECT *, }
  822. . q{COALESCE(EXTRACT(epoch FROM checktime),0) AS checksecs, }
  823. . q{COALESCE(EXTRACT(epoch FROM lifetime),0) AS lifetimesecs }
  824. . q{FROM bucardo.sync WHERE name = ?};
  825. my $sth = $maindbh->prepare($SQL);
  826. $count = $sth->execute($syncname);
  827. if ($count eq '0E0') {
  828. $sth->finish();
  829. $self->glog(qq{Warning! Cannot reload sync "$syncname": no longer in the database!}, LOG_WARN);
  830. $maindbh->commit();
  831. next; ## Handle the next notice
  832. }
  833. ## XXX: Actually do a full disconnect and redo all the items in here
  834. my $info = $sth->fetchall_arrayref({})->[0];
  835. $maindbh->commit();
  836. ## Only certain things can be changed "on the fly"
  837. for my $val (qw/checksecs stayalive deletemethod status autokick
  838. analyze_after_copy vacuum_after_copy targetgroup targetdb
  839. onetimecopy lifetimesecs maxkicks rebuild_index/) {
  840. $sync->{$syncname}{$val} = $self->{sync}{$syncname}{$val} = $info->{$val};
  841. }
  842. ## XXX: Todo: Fix those double assignments
  843. ## Empty all of our custom code arrays
  844. for my $key (grep { /^code_/ } sort keys %{ $self->{sync}{$syncname} }) {
  845. $sync->{$syncname}{$key} = $self->{sync}{$syncname}{$key} = [];
  846. }
  847. sleep 2; ## XXX TODO: Actually wait somehow, perhaps fork
  848. $self->glog("Reactivating sync $syncname", LOG_TERSE);
  849. $sync->{$syncname}{mcp_active} = 0;
  850. if (! $self->activate_sync($sync->{$syncname})) {
  851. $self->glog(qq{Warning! Reactivation of sync "$syncname" failed}, LOG_WARN);
  852. }
  853. else {
  854. ## Let anyone listening know the sync is now ready
  855. $self->db_notify($maindbh, "reloaded_sync_$syncname", 1);
  856. }
  857. $maindbh->commit();
  858. }
  859. }
  860. ## Request that a named sync get activated
  861. elsif ($name =~ /^activate_sync_(.+)/o) {
  862. my $syncname = $1;
  863. if (! exists $sync->{$syncname}) {
  864. $self->glog(qq{Invalid sync activation: "$syncname"}, LOG_TERSE);
  865. }
  866. elsif ($sync->{$syncname}{mcp_active}) {
  867. $self->glog(qq{Sync "$syncname" is already activated}, LOG_TERSE);
  868. $self->db_notify($maindbh, "activated_sync_$syncname", 1);
  869. }
  870. else {
  871. if ($self->activate_sync($sync->{$syncname})) {
  872. $sync->{$syncname}{mcp_active} = 1;
  873. }
  874. }
  875. }
  876. ## Request that a named sync get deactivated
  877. elsif ($name =~ /^deactivate_sync_(.+)/o) {
  878. my $syncname = $1;
  879. if (! exists $sync->{$syncname}) {
  880. $self->glog(qq{Invalid sync "$syncname"}, LOG_TERSE);
  881. }
  882. elsif (! $sync->{$syncname}{mcp_active}) {
  883. $self->glog(qq{Sync "$syncname" is already deactivated}, LOG_TERSE);
  884. $self->db_notify($maindbh, "deactivated_sync_$syncname", 1);
  885. }
  886. elsif ($self->deactivate_sync($sync->{$syncname})) {
  887. $sync->{$syncname}{mcp_active} = 0;
  888. }
  889. }
  890. # Serialization/deadlock problems; now the child is gonna sleep.
  891. elsif ($name =~ /^syncsleep_(.+)/o) {
  892. my $syncname = $1;
  893. $self->glog("Sync $syncname could not serialize, will sleep", LOG_DEBUG);
  894. ## Echo out to anyone listening
  895. $self->db_notify($maindbh, $name, 1);
  896. }
  897. ## A kid reporting in. We just store the PID
  898. elsif ('kid_pid_start') {
  899. for my $lpid (keys %{ $notice->{$name}{pid} }) {
  900. $self->{kidpidlist}{$lpid} = 1;
  901. }
  902. }
  903. ## A kid leaving. We remove the stored PID.
  904. elsif ('kid_pid_stop') {
  905. for my $lpid (keys %{ $notice->{$name}{pid} }) {
  906. delete $self->{kidpidlist}{$lpid};
  907. }
  908. }
  909. ## Should not happen, but let's at least log it
  910. else {
  911. $self->glog("Warning: received unknown message $name from $npid!", LOG_TERSE);
  912. }
  913. } ## end each notice
  914. $maindbh->commit();
  915. ## Just in case this changed behind our back:
  916. $sync = $self->{sync};
  917. ## Startup controllers for all eligible syncs
  918. SYNC: for my $syncname (keys %$sync) {
  919. ## Skip if this sync has not been activated
  920. next unless $sync->{$syncname}{mcp_active};
  921. my $s = $sync->{$syncname};
  922. ## If this is not a stayalive, AND is not being kicked, skip it
  923. next if ! $s->{stayalive} and ! $s->{kick_on_startup};
  924. ## If this is a fullcopy sync, skip unless it is being kicked
  925. next if $s->{fullcopy} and ! $s->{kick_on_startup};
  926. ## If this is a previous stayalive, see if it is active, kick if needed
  927. if ($s->{stayalive} and $s->{controller}) {
  928. $count = kill 0 => $s->{controller};
  929. ## If kill 0 returns nothing, the controller is gone, so create a new one
  930. if (! $count) {
  931. $self->glog("Could not find controller $s->{controller}, will create a new one. Kicked is $s->{kick_on_startup}", LOG_TERSE);
  932. $s->{controller} = 0;
  933. }
  934. else { ## Presume it is alive and listening to us, restart and kick as needed
  935. if ($s->{kick_on_startup}) {
  936. ## See if controller needs to be killed, because of time limit or job count limit
  937. my $restart_reason = '';
  938. ## We can kill and restart a controller after a certain number of kicks
  939. if ($s->{maxkicks} > 0 and $s->{ctl_kick_counts} >= $s->{maxkicks}) {
  940. $restart_reason = "Total kicks ($s->{ctl_kick_counts}) >= limit ($s->{maxkicks})";
  941. }
  942. ## We can kill and restart a controller after a certain amount of time
  943. elsif ($s->{lifetimesecs} > 0) {
  944. my $thistime = time();
  945. my $timediff = $thistime - $s->{start_time};
  946. if ($thistime - $s->{start_time} > $s->{lifetimesecs}) {
  947. $restart_reason = "Time is $timediff, limit is $s->{lifetimesecs} ($s->{lifetime})";
  948. }
  949. }
  950. if ($restart_reason) {
  951. ## Kill and restart controller
  952. $self->glog("Restarting controller for sync $syncname. $restart_reason", LOG_TERSE);
  953. kill $signumber{USR1} => $s->{controller};
  954. ## Create a new controller
  955. $self->fork_controller($s, $syncname);
  956. }
  957. else {
  958. ## Perform the kick
  959. my $notify = "ctl_kick_$syncname";
  960. $self->db_notify($maindbh, $notify);
  961. $self->glog(qq{Sent a kick to controller $s->{controller} for sync "$syncname"}, LOG_VERBOSE);
  962. }
  963. ## Reset so we don't kick the next round
  964. $s->{kick_on_startup} = 0;
  965. ## Track how many times we've kicked
  966. $s->{ctl_kick_counts}++;
  967. }
  968. next SYNC;
  969. }
  970. }
  971. ## At this point, we are either:
  972. ## 1. Not a stayalive
  973. ## 2. A stayalive that has not been run yet
  974. ## 3. A stayalive that has been run but is not responding
  975. ## Make sure there is nothing out there already running
  976. my $syncname = $s->{name};
  977. my $pidfile = "$config{piddir}/bucardo.ctl.sync.$syncname.pid";
  978. if ($s->{mcp_changed}) {
  979. $self->glog(qq{Checking for existing controllers for sync "$syncname"}, LOG_VERBOSE);
  980. }
  981. if (-e $pidfile and ! $s->{mcp_problemchild}) {
  982. $self->glog("File exists staylive=$s->{stayalive} controller=$s->{controller}", LOG_TERSE);
  983. my $pid;
  984. if (!open $pid, '<', $pidfile) {
  985. $self->glog(qq{Warning: Could not open file "$pidfile": $!}, LOG_WARN);
  986. $s->{mcp_problemchild} = 1;
  987. next SYNC;
  988. }
  989. my $oldpid = <$pid>;
  990. chomp $oldpid;
  991. close $pid or warn qq{Could not close "$pidfile": $!\n};
  992. ## We don't need to know about this every time
  993. if ($s->{mcp_changed}) {
  994. $self->glog(qq{Found previous controller $oldpid from "$pidfile"}, LOG_TERSE);
  995. }
  996. if ($oldpid !~ /^\d+$/) {
  997. $self->glog(qq{Warning: Invalid pid found inside of file "$pidfile" ($oldpid)}, LOG_WARN);
  998. $s->{mcp_changed} = 0;
  999. $s->{mcp_problemchild} = 2;
  1000. next SYNC;
  1001. }
  1002. ## Is it still alive?
  1003. $count = kill 0 => $oldpid;
  1004. if ($count==1) {
  1005. if ($s->{mcp_changed}) {
  1006. $self->glog(qq{Skipping sync "$syncname", seems to be already handled by $oldpid}, LOG_VERBOSE);
  1007. ## Make sure this kid is still running
  1008. $count = kill 0 => $oldpid;
  1009. if (!$count) {
  1010. $self->glog(qq{Warning! PID $oldpid was not found. Removing PID file}, LOG_WARN);
  1011. unlink $pidfile or $self->glog("Warning! Failed to unlink $pidfile", LOG_WARN);
  1012. $s->{mcp_problemchild} = 3;
  1013. next SYNC;
  1014. }
  1015. $s->{mcp_changed} = 0;
  1016. }
  1017. if (! $s->{stayalive}) {
  1018. $self->glog(qq{Non stayalive sync "$syncname" still active - sending it a notify}, LOG_NORMAL);
  1019. }
  1020. my $notify = "ctl_kick_$syncname";
  1021. $self->db_notify($maindbh, $notify);
  1022. $s->{kick_on_startup} = 0;
  1023. next SYNC;
  1024. }
  1025. $self->glog("No active pid $oldpid found. Killing just in case, and removing file", LOG_TERSE);
  1026. $self->kill_bucardo_pid($oldpid => 'normal');
  1027. unlink $pidfile or $self->glog("Warning! Failed to unlink $pidfile", LOG_WARN);
  1028. $s->{mcp_changed} = 1;
  1029. } ## end if pidfile found for this sync
  1030. ## We may have found an error in the pid file detection the first time through
  1031. $s->{mcp_problemchild} = 0;
  1032. ## Fork off the controller, then clean up the $s hash
  1033. $self->{masterdbh}->commit();
  1034. $self->fork_controller($s, $syncname);
  1035. $s->{kick_on_startup} = 0;
  1036. $s->{mcp_changed} = 1;
  1037. } ## end each sync
  1038. sleep $config{mcp_loop_sleep};
  1039. redo MCP;
  1040. } ## end of MCP loop
  1041. return;
  1042. } ## end of mcp_main
  1043. sub start_controller {
  1044. ## For a particular sync, does all the listening and creation of KIDs
  1045. ## aka the CTL process
  1046. ## Arguments: one
  1047. ## 1. Hashref of sync information
  1048. ## Returns: never
  1049. our ($self,$sync) = @_;
  1050. $self->{ctlpid} = $$;
  1051. $self->{syncname} = $sync->{name};
  1052. ## Prefix all log lines with this TLA (was MCP)
  1053. $self->{logprefix} = 'CTL';
  1054. ## Extract some of the more common items into local vars
  1055. my ($syncname,$kidsalive,$dbinfo, $kicked,) = @$sync{qw(
  1056. name kidsalive dbs kick_on_startup)};
  1057. ## Set our process name
  1058. $0 = qq{Bucardo Controller.$self->{extraname} Sync "$syncname" for relgroup "$sync->{herd}" to dbs "$sync->{dbs}"};
  1059. ## Upgrade any specific sync configs to global configs
  1060. if (exists $config{sync}{$syncname}) {
  1061. while (my ($setting, $value) = each %{$config{sync}{$syncname}}) {
  1062. $config{$setting} = $value;
  1063. $self->glog("Set sync-level config setting $setting: $value", LOG_TERSE);
  1064. }
  1065. }
  1066. ## Store our PID into a file
  1067. ## Save the complete returned name for later cleanup
  1068. $self->{ctlpidfile} = $self->store_pid( "bucardo.ctl.sync.$syncname.pid" );
  1069. ## Start normal log output for this controller: basic facts
  1070. my $msg = qq{New controller for sync "$syncname". Relgroup is "$sync->{herd}", dbs is "$sync->{dbs}". PID=$$};
  1071. $self->glog($msg, LOG_TERSE);
  1072. ## Log some startup information, and squirrel some away for later emailing
  1073. my $mailmsg = "$msg\n";
  1074. $msg = qq{ stayalive: $sync->{stayalive} checksecs: $sync->{checksecs} kicked: $kicked};
  1075. $self->glog($msg, LOG_NORMAL);
  1076. $mailmsg .= "$msg\n";
  1077. $msg = sprintf q{ kidsalive: %s onetimecopy: %s lifetimesecs: %s (%s) maxkicks: %s},
  1078. $kidsalive,
  1079. $sync->{onetimecopy},
  1080. $sync->{lifetimesecs},
  1081. $sync->{lifetime} || 'NULL',
  1082. $sync->{maxkicks};
  1083. $self->glog($msg, LOG_NORMAL);
  1084. $mailmsg .= "$msg\n";
  1085. ## Allow the MCP to signal us (request to exit)
  1086. local $SIG{USR1} = sub {
  1087. ## Do not change this message: looked for in the controller DIE sub
  1088. die "MCP request\n";
  1089. };
  1090. ## From this point forward, we want to die gracefully
  1091. local $SIG{__DIE__} = sub {
  1092. ## Arguments: one
  1093. ## 1. Error message
  1094. ## Returns: never (exit 0)
  1095. my ($diemsg) = @_;
  1096. ## Store the line that did the actual exception
  1097. my $line = (caller)[2];
  1098. ## Don't issue a warning if this was simply a MCP request
  1099. my $warn = $diemsg =~ /MCP request/ ? '' : 'Warning! ';
  1100. $self->glog(qq{${warn}Controller for "$syncname" was killed at line $line: $diemsg}, LOG_WARN);
  1101. ## We send an email if it's enabled
  1102. if ($self->{sendmail} or $self->{sendmail_file}) {
  1103. ## Never email passwords
  1104. my $oldpass = $self->{dbpass};
  1105. $self->{dbpass} = '???';
  1106. ## Create a text version of our $self to email out
  1107. my $dump = Dumper $self;
  1108. my $body = qq{
  1109. Controller $$ has been killed at line $line
  1110. Host: $hostname
  1111. Sync name: $syncname
  1112. Relgroup: $sync->{herd}
  1113. Databases: $sync->{dbs}
  1114. Error: $diemsg
  1115. Parent process: $self->{mcppid}
  1116. Stats page: $config{stats_script_url}?sync=$syncname
  1117. Version: $VERSION
  1118. };
  1119. ## Whitespace cleanup
  1120. $body =~ s/^\s+//gsm;
  1121. ## Give some hints in the subject lines for known types of errors
  1122. my $moresub = '';
  1123. if ($diemsg =~ /Found stopfile/) {
  1124. $moresub = ' (stopfile)';
  1125. }
  1126. elsif ($diemsg =~ /could not serialize access/) {
  1127. $moresub = ' (serialization)';
  1128. }
  1129. elsif ($diemsg =~ /deadlock/) {
  1130. $moresub = ' (deadlock)';
  1131. }
  1132. elsif ($diemsg =~ /could not connect/) {
  1133. $moresub = ' (no connection)';
  1134. }
  1135. ## Send the mail, but not for a normal shutdown
  1136. if ($moresub !~ /stopfile/) {
  1137. my $subject = qq{Bucardo "$syncname" controller killed on $shorthost$moresub};
  1138. $self->send_mail({ body => "$body\n", subject => $subject });
  1139. }
  1140. ## Restore the password for the final cleanup connection
  1141. $self->{dbpass} = $oldpass;
  1142. } ## end sending email
  1143. ## Cleanup the controller by killing kids, cleaning database tables and removing the PID file.
  1144. $self->cleanup_controller(0, $diemsg);
  1145. exit 0;
  1146. }; ## end SIG{__DIE_} handler sub
  1147. ## Connect to the master database
  1148. ($self->{master_backend}, $self->{masterdbh}) = $self->connect_database();
  1149. my $maindbh = $self->{masterdbh};
  1150. $self->glog("Bucardo database backend PID: $self->{master_backend}", LOG_VERBOSE);
  1151. ## Map the PIDs to common names for better log output
  1152. $self->{pidmap}{$$} = 'CTL';
  1153. $self->{pidmap}{$self->{master_backend}} = 'Bucardo DB';
  1154. ## Listen for kick requests from the MCP for this sync
  1155. my $kicklisten = "kick_$syncname";
  1156. $self->db_listen($maindbh, "ctl_$kicklisten");
  1157. ## Listen for a controller ping request
  1158. my $pinglisten = "${$}_ping";
  1159. $self->db_listen($maindbh, "ctl_$pinglisten");
  1160. ## Commit so we start listening right away
  1161. $maindbh->commit();
  1162. ## SQL to update the syncrun table's status only
  1163. ## This is currently unused, but no harm in leaving it in place.
  1164. ## It would be nice to syncrun the before_sync and after_sync
  1165. ## custom codes. If we reintroduce the multi-kid 'gang' concept,
  1166. ## that changes things radically as well.
  1167. $SQL = q{
  1168. UPDATE bucardo.syncrun
  1169. SET status=?
  1170. WHERE sync=?
  1171. AND ended IS NULL
  1172. };
  1173. $sth{ctl_syncrun_update_status} = $maindbh->prepare($SQL);
  1174. ## SQL to update the syncrun table on startup
  1175. ## Returns the insert (start) time
  1176. $SQL = q{
  1177. UPDATE bucardo.syncrun
  1178. SET ended=now(), status=?
  1179. WHERE sync=?
  1180. AND ended IS NULL
  1181. RETURNING started
  1182. };
  1183. $sth{ctl_syncrun_end_now} = $maindbh->prepare($SQL);
  1184. ## At this point, this controller must be authoritative for its sync
  1185. ## Thus, we want to stop/kill any other CTL or KID processes that exist for this sync
  1186. ## The first step is to send a friendly notice asking them to leave gracefully
  1187. my $stopsync = "stopsync_$syncname";
  1188. ## This will commit after the notify:
  1189. $self->db_notify($maindbh, "kid_$stopsync");
  1190. ## We also want to force other controllers of this sync to leave
  1191. $self->db_notify($maindbh, "ctl_$stopsync");
  1192. ## Now we can listen for it ourselves in case the MCP requests it
  1193. $self->db_listen($maindbh, "ctl_$stopsync");
  1194. ## Now we look for any PID files for this sync and send them a HUP
  1195. $count = $self->send_signal_to_PID( {sync => $syncname} );
  1196. ## Next, we want to interrupt any long-running queries a kid may be in the middle of
  1197. ## If they are, they will not receive the message above until done, but we can't wait
  1198. ## If we stopped anyone, sleep a bit to allow them to exit and remove their PID files
  1199. $self->terminate_old_goats($syncname) and sleep 1;
  1200. ## Clear out any old entries in the syncrun table
  1201. $sth = $sth{ctl_syncrun_end_now};
  1202. $count = $sth->execute("Old entry ended (CTL $$)", $syncname);
  1203. if (1 == $count) {
  1204. $info = $sth->fetchall_arrayref()->[0][0];
  1205. $self->glog("Ended old syncrun entry, start time was $info", LOG_NORMAL);
  1206. }
  1207. else {
  1208. $sth->finish();
  1209. }
  1210. ## Count the number of gangs in use by this sync
  1211. my %gang;
  1212. for my $dbname (sort keys %{ $sync->{db} }) {
  1213. $x = $sync->{db}{$dbname};
  1214. ## Makes no sense to specify gangs for source databases!
  1215. next if $x->{role} eq 'source';
  1216. $gang{$x->{gang}}++;
  1217. }
  1218. $sync->{numgangs} = keys %gang;
  1219. ## Listen for a kid letting us know the sync has finished
  1220. my $syncdone = "syncdone_$syncname";
  1221. $self->db_listen($maindbh, "ctl_$syncdone");
  1222. ## Determine the last time this sync fired, if we are using "checksecs"
  1223. if ($sync->{checksecs}) {
  1224. ## The handy syncrun table tells us the time of the last good run
  1225. $SQL = q{
  1226. SELECT CEIL(EXTRACT(epoch FROM ended))
  1227. FROM bucardo.syncrun
  1228. WHERE sync=?
  1229. AND lastgood IS TRUE
  1230. OR lastempty IS TRUE
  1231. };
  1232. $sth = $maindbh->prepare($SQL);
  1233. $count = $sth->execute($syncname);
  1234. ## Got a match? Use that
  1235. if (1 == $count) {
  1236. $sync->{lastheardfrom} = $sth->fetchall_arrayref()->[0][0];
  1237. }
  1238. else {
  1239. ## We default to "now" if we cannot find an earlier time
  1240. $sth->finish();
  1241. $sync->{lastheardfrom} = time();
  1242. }
  1243. $maindbh->commit();
  1244. }
  1245. ## If running an after_sync customcode, we need a timestamp
  1246. if (exists $sync->{code_after_sync}) {
  1247. $SQL = 'SELECT now()';
  1248. $sync->{starttime} = $maindbh->selectall_arrayref($SQL)->[0][0];
  1249. ## Rolling back as all we did was the SELECT
  1250. $maindbh->rollback();
  1251. }
  1252. ## Reconnect to all databases we care about: overwrites existing dbhs
  1253. for my $dbname (sort keys %{ $sync->{db} }) {
  1254. $x = $sync->{db}{$dbname};
  1255. if ($x->{dbtype} =~ /flat/o) {
  1256. $self->glog(qq{Not connecting to flatfile database "$dbname"}, LOG_NORMAL);
  1257. next;
  1258. }
  1259. ## Do not need non-Postgres handles for the controller
  1260. next if $x->{dbtype} ne 'postgres';
  1261. ## Establish a new database handle
  1262. ($x->{backend}, $x->{dbh}) = $self->connect_database($dbname);
  1263. $self->glog(qq{Database "$dbname" backend PID: $x->{backend}}, LOG_NORMAL);
  1264. $self->{pidmap}{$x->{backend}} = "DB $dbname";
  1265. }
  1266. ## Adjust the target table names as needed and store in the goat hash
  1267. ## New table name regardless of syncs or databases
  1268. $SQL = 'SELECT newname FROM bucardo.customname WHERE goat=? AND db IS NULL and sync IS NULL';
  1269. my $sth_custom1 = $maindbh->prepare($SQL);
  1270. ## New table name for this sync only
  1271. $SQL = 'SELECT newname FROM bucardo.customname WHERE goat=? AND sync=? AND db IS NULL';
  1272. my $sth_custom2 = $maindbh->prepare($SQL);
  1273. ## New table name for a specific database only
  1274. $SQL = 'SELECT newname FROM bucardo.customname WHERE goat=? AND db=? AND sync IS NULL';
  1275. my $sth_custom3 = $maindbh->prepare($SQL);
  1276. ## New table name for this sync and a specific database
  1277. $SQL = 'SELECT newname FROM bucardo.customname WHERE goat=? AND sync=? AND db=?';
  1278. my $sth_custom4 = $maindbh->prepare($SQL);
  1279. ## Adjust the target table columns as needed and store in the goat hash
  1280. ## New table cols regardless of syncs or databases
  1281. $SQL = 'SELECT clause FROM bucardo.customcols WHERE goat=? AND db IS NULL and sync IS NULL';
  1282. my $sth_customc1 = $maindbh->prepare($SQL);
  1283. ## New table cols for this sync only
  1284. $SQL = 'SELECT clause FROM bucardo.customcols WHERE goat=? AND sync=? AND db IS NULL';
  1285. my $sth_customc2 = $maindbh->prepare($SQL);
  1286. ## New table cols for a specific database only
  1287. $SQL = 'SELECT clause FROM bucardo.customcols WHERE goat=? AND db=? AND sync IS NULL';
  1288. my $sth_customc3 = $maindbh->prepare($SQL);
  1289. ## New table cols for this sync and a specific database
  1290. $SQL = 'SELECT clause FROM bucardo.customcols WHERE goat=? AND sync=? AND db=?';
  1291. my $sth_customc4 = $maindbh->prepare($SQL);
  1292. for my $g (@{ $sync->{goatlist} }) {
  1293. ## We only transform tables for now
  1294. next if $g->{reltype} ne 'table';
  1295. my ($S,$T) = ($g->{safeschema},$g->{safetable});
  1296. ## See if we have any custom names or columns. Each level overrides the last
  1297. my $customname = '';
  1298. my $customcols = '';
  1299. ## Just this goat
  1300. $count = $sth_custom1->execute($g->{id});
  1301. if ($count < 1) {
  1302. $sth_custom1->finish();
  1303. }
  1304. else {
  1305. $customname = $sth_custom1->fetchall_arrayref()->[0][0];
  1306. }
  1307. $count = $sth_customc1->execute($g->{id});
  1308. if ($count < 1) {
  1309. $sth_customc1->finish();
  1310. }
  1311. else {
  1312. $customcols = $sth_customc1->fetchall_arrayref()->[0][0];
  1313. }
  1314. ## Just this goat and this sync
  1315. $count = $sth_custom2->execute($g->{id}, $syncname);
  1316. if ($count < 1) {
  1317. $sth_custom2->finish();
  1318. }
  1319. else {
  1320. $customname = $sth_custom2->fetchall_arrayref()->[0][0];
  1321. }
  1322. $count = $sth_customc2->execute($g->{id}, $syncname);
  1323. if ($count < 1) {
  1324. $sth_customc2->finish();
  1325. }
  1326. else {
  1327. $customcols = $sth_customc2->fetchall_arrayref()->[0][0];
  1328. }
  1329. ## Need to pick one source at random to extract the list of columns from
  1330. my $saved_sourcedbh = '';
  1331. ## Set for each target db
  1332. $g->{newname}{$syncname} = {};
  1333. $g->{newcols}{$syncname} = {};
  1334. for my $dbname (sort keys %{ $sync->{db} }) {
  1335. $x = $sync->{db}{$dbname};
  1336. my $type= $x->{dbtype};
  1337. my $cname;
  1338. my $ccols = '';
  1339. ## We only ever change table names (or cols) for true targets
  1340. if ($x->{role} ne 'source') {
  1341. ## Save local copies for this database only
  1342. $cname = $customname;
  1343. $ccols = $customcols;
  1344. ## Anything for this goat and this database?
  1345. $count = $sth_custom3->execute($g->{id}, $dbname);
  1346. if ($count < 1) {
  1347. $sth_custom3->finish();
  1348. }
  1349. else {
  1350. $cname = $sth_custom3->fetchall_arrayref()->[0][0];
  1351. }
  1352. $count = $sth_customc3->execute($g->{id}, $dbname);
  1353. if ($count < 1) {
  1354. $sth_customc3->finish();
  1355. }
  1356. else {
  1357. $ccols = $sth_customc3->fetchall_arrayref()->[0][0];
  1358. }
  1359. ## Anything for this goat, this sync, and this database?
  1360. $count = $sth_custom4->execute($g->{id}, $syncname, $dbname);
  1361. if ($count < 1) {
  1362. $sth_custom4->finish();
  1363. }
  1364. else {
  1365. $cname = $sth_custom4->fetchall_arrayref()->[0][0];
  1366. }
  1367. $count = $sth_customc4->execute($g->{id}, $syncname, $dbname);
  1368. if ($count < 1) {
  1369. $sth_customc4->finish();
  1370. }
  1371. else {
  1372. $ccols = $sth_customc4->fetchall_arrayref()->[0][0];
  1373. }
  1374. }
  1375. ## Got a new name match? Just use that for everything
  1376. if (defined $cname and $cname) {
  1377. $g->{newname}{$syncname}{$dbname} = $cname;
  1378. }
  1379. ## Only a few use schemas:
  1380. elsif ($x->{dbtype} eq 'postgres'
  1381. or $x->{dbtype} eq 'flatpg') {
  1382. $g->{newname}{$syncname}{$dbname} = "$S.$T";
  1383. }
  1384. ## Some always get the raw table name
  1385. elsif ($x->{dbtype} eq 'redis') {
  1386. $g->{newname}{$syncname}{$dbname} = $g->{tablename};
  1387. }
  1388. else {
  1389. $g->{newname}{$syncname}{$dbname} = $T;
  1390. }
  1391. ## Set the columns for this combo: empty for no change
  1392. $g->{newcols}{$syncname}{$dbname} = $ccols;
  1393. ## If we do not have a source database handle yet, grab one
  1394. if (! $saved_sourcedbh) {
  1395. for my $dbname (sort keys %{ $sync->{db} }) {
  1396. next if $sync->{db}{$dbname}{role} ne 'source';
  1397. ## All we need is the handle, nothing more
  1398. $saved_sourcedbh = $sync->{db}{$dbname}{dbh};
  1399. ## Leave this loop, we got what we came for
  1400. last;
  1401. }
  1402. }
  1403. ## We either get the specific columns, or use a '*' if no customcols
  1404. my $SELECT = $ccols || 'SELECT *';
  1405. ## Run a dummy query against the source to pull back the column names
  1406. ## This is particularly important for customcols of course!
  1407. $sth = $saved_sourcedbh->prepare("SELECT * FROM ($SELECT FROM $S.$T LIMIT 0) AS foo LIMIT 0");
  1408. $sth->execute();
  1409. ## Store the arrayref of column names for this goat and this select clause
  1410. $g->{tcolumns}{$SELECT} = $sth->{NAME};
  1411. $sth->finish();
  1412. $saved_sourcedbh->rollback();
  1413. ## Make sure none of them are un-named, which Postgres outputs as ?column?
  1414. if (grep { /\?/ } @{ $g->{tcolumns}{$SELECT} }) {
  1415. die "Invalid customcols given: must give an alias to all columns!\n";
  1416. }
  1417. }
  1418. }
  1419. ## Set to true if we determine the kid(s) should make a run
  1420. ## Can be set by:
  1421. ## kick notice from the MCP for this sync
  1422. ## 'checksecs' timeout
  1423. ## if we are just starting up (now)
  1424. my $kick_request = 1;
  1425. ## How long it has been since we checked on our kids
  1426. my $kidchecktime = 0;
  1427. ## For custom code:
  1428. our $input = {}; ## XXX still needed?
  1429. ## We are finally ready to enter the main loop
  1430. CONTROLLER: {
  1431. ## Bail if the stopfile exists
  1432. if (-e $self->{stopfile}) {
  1433. $self->glog(qq{Found stopfile "$self->{stopfile}": exiting}, LOG_TERSE);
  1434. ## Do not change this message: looked for in the controller DIE sub
  1435. my $stopmsg = 'Found stopfile';
  1436. ## Grab the reason, if it exists, so we can propagate it onward
  1437. my $ctlreason = get_reason(0);
  1438. if ($ctlreason) {
  1439. $stopmsg .= ": $ctlreason";
  1440. }
  1441. ## This exception is caught by the controller's __DIE__ sub above
  1442. die "$stopmsg\n";
  1443. }
  1444. ## Process any notifications from the main database
  1445. ## Ignore things we may have sent ourselves
  1446. my $nlist = $self->db_get_notices($maindbh, $self->{master_backend});
  1447. NOTICE: for my $name (sort keys %{ $nlist }) {
  1448. my $npid = $nlist->{$name}{firstpid};
  1449. ## Strip prefix so we can easily use both pre and post 9.0 versions
  1450. $name =~ s/^ctl_//o;
  1451. ## Kick request from the MCP?
  1452. if ($name eq $kicklisten) {
  1453. $kick_request = 1;
  1454. next NOTICE;
  1455. }
  1456. ## Request for a ping via listen/notify
  1457. if ($name eq $pinglisten) {
  1458. $self->glog('Got a ping, issuing pong', LOG_DEBUG);
  1459. $self->db_notify($maindbh, "ctl_${$}_pong");
  1460. next NOTICE;
  1461. }
  1462. ## Another controller has asked us to leave as we are no longer The Man
  1463. if ($name eq $stopsync) {
  1464. $self->glog('Got a stop sync request, so exiting', LOG_TERSE);
  1465. die 'Stop sync request';
  1466. }
  1467. ## A kid has just finished syncing
  1468. if ($name eq $syncdone) {
  1469. $self->{syncdone} = time;
  1470. $self->glog("Kid $npid has reported that sync $syncname is done", LOG_DEBUG);
  1471. ## If this was a onetimecopy sync, flip the bit (which should be done in the db already)
  1472. if ($sync->{onetimecopy}) {
  1473. $sync->{onetimecopy} = 0;
  1474. }
  1475. next NOTICE;
  1476. }
  1477. ## Someone else's sync is getting kicked, finishing up, or stopping
  1478. next NOTICE if
  1479. (index($name, 'kick_') == 0)
  1480. or
  1481. (index($name, 'syncdone_') == 0)
  1482. or
  1483. (index($name, 'stopsync_') == 0);
  1484. ## Should not happen, but let's at least log it
  1485. $self->glog("Warning: received unknown message $name from $npid!", LOG_TERSE);
  1486. } ## end of each notification
  1487. ## To ensure we can receive new notifications next time:
  1488. $maindbh->commit();
  1489. if ($self->{syncdone}) {
  1490. ## Reset the notice
  1491. $self->{syncdone} = 0;
  1492. ## Run all after_sync custom codes
  1493. if (exists $sync->{code_after_sync}) {
  1494. for my $code (@{$sync->{code_after_sync}}) {
  1495. #$sth{ctl_syncrun_update_status}->execute("Code after_sync (CTL $$)", $syncname);
  1496. $maindbh->commit();
  1497. my $result = $self->run_ctl_custom_code($sync,$input,$code, 'nostrict');
  1498. $self->glog("End of after_sync $code->{id}", LOG_VERBOSE);
  1499. } ## end each custom code
  1500. }
  1501. ## Let anyone listening know that this sync is complete. Global message
  1502. my $notifymsg = "syncdone_$syncname";
  1503. $self->db_notify($maindbh, $notifymsg);
  1504. ## If we are not a stayalive, this is a good time to leave
  1505. if (! $sync->{stayalive} and ! $kidsalive) {
  1506. $self->cleanup_controller(1, 'Kids are done');
  1507. exit 0;
  1508. }
  1509. ## XXX: re-examine
  1510. # If we ran an after_sync and grabbed rows, reset the time
  1511. # if (exists $rows_for_custom_code->{source}) {
  1512. # $SQL = "SELECT $self->{mcp_clock_timestamp}";
  1513. # $sync->{starttime} = $maindbh->selectall_arrayref($SQL)->[0][0];
  1514. # }
  1515. } ## end if sync done
  1516. ## If we are using checksecs, possibly force a kick
  1517. if ($sync->{checksecs}) {
  1518. ## Already being kicked? Reset the clock
  1519. if ($kick_request) {
  1520. $sync->{lastheardfrom} = time();
  1521. }
  1522. elsif (time() - $sync->{lastheardfrom} >= $sync->{checksecs}) {
  1523. if ($sync->{onetimecopy}) {
  1524. $self->glog(qq{Timed out, but in onetimecopy mode, so not kicking, for "$syncname"}, LOG_DEBUG);
  1525. }
  1526. else {
  1527. $self->glog(qq{Timed out - force a sync for "$syncname"}, LOG_VERBOSE);
  1528. $kick_request = 1;
  1529. }
  1530. ## Reset the clock
  1531. $sync->{lastheardfrom} = time();
  1532. }
  1533. }
  1534. ## XXX What about non stayalive kids?
  1535. ## XXX This is called too soon - recently created kids are not there yet!
  1536. ## Check that our kids are alive and healthy
  1537. ## XXX Skip if we know the kids are busy? (cannot ping/pong!)
  1538. ## XXX Maybe skip this entirely and just check on a kick?
  1539. if ($sync->{stayalive} ## CTL must be persistent
  1540. and $kidsalive ## KID must be persistent
  1541. and $self->{kidpid} ## KID must have been created at least once
  1542. and time() - $kidchecktime >= $config{ctl_checkonkids_time}) {
  1543. my $pidfile = "$config{piddir}/bucardo.kid.sync.$syncname.pid";
  1544. ## If we find a problem, set this to true
  1545. my $resurrect = 0;
  1546. ## Make sure the PID file exists
  1547. if (! -e $pidfile) {
  1548. $self->glog("PID file missing: $pidfile", LOG_DEBUG);
  1549. $resurrect = 1;
  1550. }
  1551. else {
  1552. ## Make sure that a kill 0 sees it
  1553. ## XXX Use ping/pong?
  1554. my $pid = $self->{kidpid};
  1555. $count = kill 0 => $pid;
  1556. if ($count != 1) {
  1557. $self->glog("Warning: Kid $pid is not responding, will respawn", LOG_TERSE);
  1558. $resurrect = 2;
  1559. }
  1560. }
  1561. ## At this point, the PID file does not exist or the kid is not responding
  1562. if ($resurrect) {
  1563. ## XXX Try harder to kill it?
  1564. $self->glog("Resurrecting kid $syncname, resurrect was $resurrect", LOG_DEBUG);
  1565. $self->{kidpid} = $self->create_newkid($sync);
  1566. ## Sleep a little here to prevent runaway kid creation
  1567. sleep $config{kid_restart_sleep};
  1568. }
  1569. ## Reset the time
  1570. $kidchecktime = time();
  1571. } ## end of time to check on our kid's health
  1572. ## Redo if we are not kicking but are stayalive and the queue is clear
  1573. if (! $kick_request and $sync->{stayalive}) {
  1574. sleep $config{ctl_sleep};
  1575. redo CONTROLLER;
  1576. }
  1577. ## Reset the kick_request for the next run
  1578. $kick_request = 0;
  1579. ## At this point, we know we are about to run a sync
  1580. ## We will either create the kid(s), or signal the existing one(s)
  1581. ## XXX If a custom code handler needs a database handle, create one
  1582. our ($cc_sourcedbh,$safe_sourcedbh);
  1583. ## Run all before_sync code
  1584. ## XXX Move to kid? Do not want to run over and over if something is queued
  1585. if (exists $sync->{code_before_sync}) {
  1586. #$sth{ctl_syncrun_update_status}->execute("Code before_sync (CTL $$)", $syncname);
  1587. $maindbh->commit();
  1588. for my $code (@{$sync->{code_before_sync}}) {
  1589. my $result = $self->run_ctl_custom_code($sync,$input,$code, 'nostrict');
  1590. if ($result eq 'redo') {
  1591. redo CONTROLLER;
  1592. }
  1593. }
  1594. }
  1595. $maindbh->commit();
  1596. if ($self->{kidpid}) {
  1597. ## Tell any listening kids to go ahead and start
  1598. $self->db_notify($maindbh, "kid_run_$syncname");
  1599. }
  1600. else {
  1601. ## Create any kids that do not exist yet (or have been killed, as detected above)
  1602. $self->glog("Creating a new kid for sync $syncname", LOG_VERBOSE);
  1603. $self->{kidpid} = $self->create_newkid($sync);
  1604. }
  1605. sleep $config{ctl_sleep};
  1606. redo CONTROLLER;
  1607. } ## end CONTROLLER
  1608. die 'How did we reach outside of the main controller loop?';
  1609. } ## end of start_controller
  1610. sub start_kid {
  1611. ## A single kid, in charge of doing a sync between two or more databases
  1612. ## aka the KID process
  1613. ## Arguments: one
  1614. ## 1. Hashref of sync information
  1615. ## Returns: never (exits)
  1616. my ($self,$sync) = @_;
  1617. ## Prefix all log lines with this TLA
  1618. $self->{logprefix} = 'KID';
  1619. ## Extract some of the more common items into local vars
  1620. my ($syncname, $goatlist, $kidsalive, $dbs, $kicked) = @$sync{qw(
  1621. name goatlist kidsalive dbs kick_on_startup)};
  1622. ## Adjust the process name, start logging
  1623. $0 = qq{Bucardo Kid.$self->{extraname} Sync "$syncname"};
  1624. my $extra = $sync->{onetimecopy} ? "OTC: $sync->{onetimecopy}" : '';
  1625. $self->glog(qq{New kid, sync "$syncname" alive=$kidsalive Parent=$self->{ctlpid} PID=$$ kicked=$kicked $extra}, LOG_TERSE);
  1626. ## Store our PID into a file
  1627. ## Save the complete returned name for later cleanup
  1628. $self->{kidpidfile} = $self->store_pid( "bucardo.kid.sync.$syncname.pid" );
  1629. ## Establish these early so the DIE block can use them
  1630. my ($S,$T,$pkval) = ('?','?','?');
  1631. ## Keep track of how many times this kid has done work
  1632. my $kidloop = 0;
  1633. ## Catch USR1 errors as a signal from the parent CTL process to exit right away
  1634. local $SIG{USR1} = sub {
  1635. ## Mostly so we do not send an email:
  1636. $self->{clean_exit} = 1;
  1637. die "CTL request\n";
  1638. };
  1639. ## Set up some common groupings of the databases inside sync->{db}
  1640. ## Also setup common attributes
  1641. my (@dbs, @dbs_source, @dbs_target, @dbs_delta, @dbs_fullcopy,
  1642. @dbs_connectable, @dbs_dbi, @dbs_write, @dbs_non_fullcopy,
  1643. @dbs_postgres, @dbs_drizzle, @dbs_mongo, @dbs_mysql, @dbs_oracle,
  1644. @dbs_redis, @dbs_sqlite);
  1645. ## Used to weed out all but one source if in onetimecopy mode
  1646. my $found_first_source = 0;
  1647. for my $dbname (sort keys %{ $sync->{db} }) {
  1648. $x = $sync->{db}{$dbname};
  1649. ## First, do some exclusions
  1650. ## If this is a onetimecopy sync, the fullcopy targets are dead to us
  1651. next if $sync->{onetimecopy} and $x->{role} eq 'fullcopy';
  1652. ## If this is a onetimecopy sync, we only need to connect to a single source
  1653. if ($sync->{onetimecopy} and $x->{role} eq 'source') {
  1654. next if $found_first_source;
  1655. $found_first_source = 1;
  1656. }
  1657. ## Now set the default attributes
  1658. ## Is this a SQL database?
  1659. $x->{does_sql} = 0;
  1660. ## Can it do truncate?
  1661. $x->{does_truncate} = 0;
  1662. ## Can it do savepoints (and roll them back)?
  1663. $x->{does_savepoints} = 0;
  1664. ## Does it support truncate cascade?
  1665. $x->{does_cascade} = 0;
  1666. ## Does it support a LIMIT clause?
  1667. $x->{does_limit} = 0;
  1668. ## Can it be queried?
  1669. $x->{does_append_only} = 0;
  1670. ## List of tables in this database that do makedelta
  1671. $x->{is_makedelta} = {};
  1672. ## Start clumping into groups and adjust the attributes
  1673. ## Postgres
  1674. if ('postgres' eq $x->{dbtype}) {
  1675. push @dbs_postgres => $dbname;
  1676. $x->{does_sql} = 1;
  1677. $x->{does_truncate} = 1;
  1678. $x->{does_savepoints} = 1;
  1679. $x->{does_cascade} = 1;
  1680. $x->{does_limit} = 1;
  1681. }
  1682. ## Drizzle
  1683. if ('drizzle' eq $x->{dbtype}) {
  1684. push @dbs_drizzle => $dbname;
  1685. $x->{does_sql} = 1;
  1686. $x->{does_truncate} = 1;
  1687. $x->{does_savepoints} = 1;
  1688. $x->{does_limit} = 1;
  1689. }
  1690. ## MongoDB
  1691. if ('mongo' eq $x->{dbtype}) {
  1692. push @dbs_mongo => $dbname;
  1693. }
  1694. ## MySQL (and MariaDB)
  1695. if ('mysql' eq $x->{dbtype} or 'mariadb' eq $x->{dbtype}) {
  1696. push @dbs_mysql => $dbname;
  1697. $x->{does_sql} = 1;
  1698. $x->{does_truncate} = 1;
  1699. $x->{does_savepoints} = 1;
  1700. $x->{does_limit} = 1;
  1701. }
  1702. ## Oracle
  1703. if ('oracle' eq $x->{dbtype}) {
  1704. push @dbs_oracle => $dbname;
  1705. $x->{does_sql} = 1;
  1706. $x->{does_truncate} = 1;
  1707. $x->{does_savepoints} = 1;
  1708. }
  1709. ## Redis
  1710. if ('redis' eq $x->{dbtype}) {
  1711. push @dbs_redis => $dbname;
  1712. }
  1713. ## SQLite
  1714. if ('sqlite' eq $x->{dbtype}) {
  1715. push @dbs_sqlite => $dbname;
  1716. $x->{does_sql} = 1;
  1717. $x->{does_truncate} = 1;
  1718. $x->{does_savepoints} = 1;
  1719. $x->{does_limit} = 1;
  1720. }
  1721. ## Flat files
  1722. if ($x->{dbtype} =~ /flat/) {
  1723. $x->{does_append_only} = 1;
  1724. }
  1725. ## Everyone goes into this bucket
  1726. push @dbs => $dbname;
  1727. ## Databases we read data from
  1728. push @dbs_source => $dbname
  1729. if $x->{role} eq 'source';
  1730. ## Target databases
  1731. push @dbs_target => $dbname
  1732. if $x->{role} ne 'source';
  1733. ## Databases that (potentially) get written to
  1734. ## This is all of them, unless we are a source
  1735. ## and a fullcopy sync or in onetimecopy mode
  1736. push @dbs_write => $dbname
  1737. if (!$sync->{fullcopy} and !$sync->{onetimecopy})
  1738. or $x->{role} ne 'source';
  1739. ## Databases that get deltas
  1740. ## If in onetimecopy mode, this is always forced to be empty
  1741. ## Likewise, no point in populating if this is a fullcopy sync
  1742. push @dbs_delta => $dbname
  1743. if $x->{role} eq 'source'
  1744. and ! $sync->{onetimecopy}
  1745. and ! $sync->{fullcopy};
  1746. ## Databases that get the full monty
  1747. ## In normal mode, this means a role of 'fullcopy'
  1748. ## In onetimecopy mode, this means a role of 'target'
  1749. push @dbs_fullcopy => $dbname
  1750. if ($sync->{onetimecopy} and $x->{role} eq 'target')
  1751. or ($sync->{fullcopy} and $x->{role} eq 'fullcopy');
  1752. ## Non-fullcopy databases. Basically dbs_source + dbs_target
  1753. push @dbs_non_fullcopy => $dbname
  1754. if $x->{role} ne 'fullcopy';
  1755. ## Databases with Perl DBI support
  1756. push @dbs_dbi => $dbname
  1757. if $x->{dbtype} eq 'postgres'
  1758. or $x->{dbtype} eq 'drizzle'
  1759. or $x->{dbtype} eq 'mariadb'
  1760. or $x->{dbtype} eq 'mysql'
  1761. or $x->{dbtype} eq 'oracle'
  1762. or $x->{dbtype} eq 'sqlite';
  1763. push @dbs_connectable => $dbname
  1764. if $x->{dbtype} !~ /flat/;
  1765. }
  1766. ## Connect to the main database
  1767. ($self->{master_backend}, $self->{masterdbh}) = $self->connect_database();
  1768. ## Set a shortcut for this handle, and log the details
  1769. my $maindbh = $self->{masterdbh};
  1770. $self->glog("Bucardo database backend PID: $self->{master_backend}", LOG_VERBOSE);
  1771. ## Setup mapping so we can report in the log which things came from this backend
  1772. $self->{pidmap}{$self->{master_backend}} = 'Bucardo DB';
  1773. ## SQL to enter a new database in the dbrun table
  1774. $SQL = q{
  1775. INSERT INTO bucardo.dbrun(sync,dbname,pgpid)
  1776. VALUES (?,?,?)
  1777. };
  1778. $sth{dbrun_insert} = $maindbh->prepare($SQL);
  1779. ## SQL to remove a database from the dbrun table
  1780. $SQL{dbrun_delete} = q{
  1781. DELETE FROM bucardo.dbrun
  1782. WHERE sync = ? AND dbname = ?
  1783. };
  1784. $sth{dbrun_delete} = $maindbh->prepare($SQL{dbrun_delete});
  1785. ## Disable the CTL exception handler.
  1786. local $SIG{__DIE__};
  1787. ## Fancy exception handler to clean things up before leaving.
  1788. my $err_handler = sub {
  1789. ## Arguments: one
  1790. ## 1. Error message
  1791. ## Returns: never (exit 1)
  1792. ## Trim whitespace from our message
  1793. my ($msg) = @_;
  1794. $msg =~ s/\s+$//g;
  1795. ## Where did we die?
  1796. my $line = (caller)[2];
  1797. $msg .= "\nLine: $line";
  1798. ## Subject line tweaking later on
  1799. my $moresub = '';
  1800. ## Find any error messages/states for all databases
  1801. if ($msg =~ /DBD::Pg/) {
  1802. $msg .= "\nMain DB state: " . ($maindbh->state || '?');
  1803. $msg .= ' Error: ' . ($maindbh->err || 'none');
  1804. for my $dbname (@dbs_dbi) {
  1805. $x = $sync->{db}{$dbname};
  1806. my $dbh = $x->{dbh};
  1807. my $state = $dbh->state || '?';
  1808. $msg .= "\nDB $dbname state: $state";
  1809. $msg .= ' Error: ' . ($dbh->err || 'none');
  1810. ## If this was a deadlock problem, try and gather more information
  1811. if ($state eq '40P01' and $x->{dbtype} eq 'postgres') {
  1812. $msg .= $self->get_deadlock_details($dbh, $msg);
  1813. $moresub = ' (deadlock)';
  1814. last;
  1815. }
  1816. }
  1817. }
  1818. $msg .= "\n";
  1819. ## Drop connection to the main database, then reconnect
  1820. if (defined $maindbh and $maindbh) {
  1821. $maindbh->rollback;
  1822. $_->finish for values %{ $maindbh->{CachedKids} };
  1823. $maindbh->disconnect;
  1824. }
  1825. my ($finalbackend, $finaldbh) = $self->connect_database();
  1826. $self->glog("Final database backend PID: $finalbackend", LOG_VERBOSE);
  1827. $sth{dbrun_delete} = $finaldbh->prepare($SQL{dbrun_delete});
  1828. ## Drop all open database connections, clear out the dbrun table
  1829. for my $dbname (@dbs_dbi) {
  1830. $x = $sync->{db}{$dbname};
  1831. my $dbh = $x->{dbh} or do {
  1832. $self->glog("Missing $dbname database handle", LOG_WARN);
  1833. next;
  1834. };
  1835. $dbh->rollback();
  1836. ## Deregister ourself with the MCP
  1837. $self->db_notify($dbh, 'kid_pid_stop', 1);
  1838. $self->glog("Disconnecting from database $dbname", LOG_DEBUG);
  1839. $_->finish for values %{ $dbh->{CachedKids} };
  1840. $dbh->disconnect();
  1841. ## Clear out the entry from the dbrun table
  1842. $sth = $sth{dbrun_delete};
  1843. $sth->execute($syncname, $dbname);
  1844. $finaldbh->commit();
  1845. }
  1846. ## If using semaphore tables, mark the status as 'failed'
  1847. ## At least in the Mongo case, it's pretty safe to do this,
  1848. ## as it is unlikely the error came from Mongo Land
  1849. if ($config{semaphore_table}) {
  1850. my $tname = $config{semaphore_table};
  1851. for my $dbname (@dbs_connectable) {
  1852. $x = $sync->{db}{$dbname};
  1853. if ($x->{dbtype} eq 'mongo') {
  1854. my $collection = $x->{dbh}->get_collection($tname);
  1855. my $object = {
  1856. sync => $syncname,
  1857. status => 'failed',
  1858. endtime => scalar gmtime,
  1859. };
  1860. $collection->update
  1861. (
  1862. {sync => $syncname},
  1863. $object,
  1864. { upsert => 1, safe => 1 }
  1865. );
  1866. }
  1867. }
  1868. }
  1869. (my $flatmsg = $msg) =~ s/\n/ /g;
  1870. ## Mark this syncrun as aborted if needed, replace the 'lastbad'
  1871. my $status = "Failed : $flatmsg (KID $$)";
  1872. $self->end_syncrun($finaldbh, 'bad', $syncname, $status);
  1873. $finaldbh->commit();
  1874. ## Update the dbrun table as needed
  1875. $SQL = q{DELETE FROM bucardo.dbrun WHERE sync = ?};
  1876. $sth = $finaldbh->prepare($SQL);
  1877. $sth->execute($syncname);
  1878. ## Let anyone listening know that this target sync aborted. Global message.
  1879. $self->db_notify($finaldbh, "synckill_${syncname}");
  1880. ## Done with database cleanups, so disconnect
  1881. $finaldbh->disconnect();
  1882. if ($msg =~ /DBD::Pg/) {
  1883. $self->glog($flatmsg, LOG_TERSE);
  1884. }
  1885. ## Send an email as needed (never for clean exit)
  1886. if (! $self->{clean_exit} and $self->{sendmail} or $self->{sendmail_file}) {
  1887. my $warn = $msg =~ /CTL.+request/ ? '' : 'Warning! ';
  1888. $self->glog(qq{${warn}Child for sync "$syncname" was killed at line $line: $msg}, LOG_WARN);
  1889. ## Never display the database passwords
  1890. for (values %{$self->{dbs}}) {
  1891. $_->{dbpass} = '???';
  1892. }
  1893. $self->{dbpass} = '???';
  1894. ## Create the body of the message to be mailed
  1895. my $dump = Dumper $self;
  1896. my $body = qq{
  1897. Kid $$ has been killed at line $line
  1898. Error: $msg
  1899. Possible suspects: $S.$T: $pkval
  1900. Host: $hostname
  1901. Sync name: $syncname
  1902. Stats page: $config{stats_script_url}?sync=$syncname
  1903. Parent process: $self->{mcppid} -> $self->{ctlpid}
  1904. Rows set to aborted: $count
  1905. Version: $VERSION
  1906. Loops: $kidloop
  1907. };
  1908. $body =~ s/^\s+//gsm;
  1909. if ($msg =~ /Found stopfile/) {
  1910. $moresub = ' (stopfile)';
  1911. }
  1912. elsif ($msg =~ /could not connect/) {
  1913. $moresub = ' (no connection)';
  1914. }
  1915. my $subject = qq{Bucardo kid for "$syncname" killed on $shorthost$moresub};
  1916. $self->send_mail({ body => "$body\n", subject => $subject });
  1917. } ## end sending email
  1918. my $extrainfo = sprintf '%s%s%s',
  1919. qq{Sync "$syncname"},
  1920. $S eq '?' ? '' : " $S.$T",
  1921. $pkval eq '?' ? '' : " pk: $pkval";
  1922. $self->cleanup_kid($flatmsg, $extrainfo);
  1923. exit 1;
  1924. }; ## end $err_handler
  1925. my $stop_sync_request = "stopsync_$syncname";
  1926. ## Tracks how long it has been since we last ran a ping against our databases
  1927. my $lastpingcheck = 0;
  1928. ## Row counts from the delta tables:
  1929. my %deltacount;
  1930. ## Count of changes made (inserts,deletes,truncates,conflicts handled):
  1931. my %dmlcount;
  1932. my $did_setup = 0;
  1933. local $@;
  1934. eval {
  1935. ## Listen for the controller asking us to go again if persistent
  1936. if ($kidsalive) {
  1937. $self->db_listen( $maindbh, "kid_run_$syncname" );
  1938. }
  1939. ## Listen for a kid ping, even if not persistent
  1940. my $kidping = "${$}_ping";
  1941. $self->db_listen( $maindbh, "kid_$kidping" );
  1942. ## Listen for a sync-wide exit signal
  1943. $self->db_listen( $maindbh, "kid_$stop_sync_request" );
  1944. ## Prepare all of our SQL
  1945. ## Note that none of this is actually 'prepared' until the first execute
  1946. ## SQL to add a new row to the syncrun table
  1947. $SQL = 'INSERT INTO bucardo.syncrun(sync,status) VALUES (?,?)';
  1948. $sth{kid_syncrun_insert} = $maindbh->prepare($SQL);
  1949. ## SQL to update the syncrun table's status only
  1950. $SQL = q{
  1951. UPDATE bucardo.syncrun
  1952. SET status=?
  1953. WHERE sync=?
  1954. AND ended IS NULL
  1955. };
  1956. $sth{kid_syncrun_update_status} = $maindbh->prepare($SQL);
  1957. ## SQL to set the syncrun table as ended once complete
  1958. $SQL = q{
  1959. UPDATE bucardo.syncrun
  1960. SET deletes=deletes+?, inserts=inserts+?, truncates=truncates+?,
  1961. conflicts=?, details=?, status=?
  1962. WHERE sync=?
  1963. AND ended IS NULL
  1964. };
  1965. $sth{kid_syncrun_end} = $maindbh->prepare($SQL);
  1966. ## Connect to all (connectable) databases we are responsible for
  1967. ## This main list has already been pruned by the controller as needed
  1968. for my $dbname (@dbs_connectable) {
  1969. $x = $sync->{db}{$dbname};
  1970. ($x->{backend}, $x->{dbh}) = $self->connect_database($dbname);
  1971. $self->glog(qq{Database "$dbname" backend PID: $x->{backend}}, LOG_VERBOSE);
  1972. ## Register ourself with the MCP (if we are Postgres)
  1973. if ($x->{dbtype} eq 'postgres') {
  1974. $self->db_notify($x->{dbh}, 'kid_pid_start', 1);
  1975. }
  1976. }
  1977. ## Set the maximum length of the $dbname.$S.$T string.
  1978. ## Used for logging output
  1979. $self->{maxdbname} = 1;
  1980. for my $dbname (keys %{ $sync->{db} }) {
  1981. $self->{maxdbname} = length $dbname if length $dbname > $self->{maxdbname};
  1982. }
  1983. my $maxst = 3;
  1984. for my $g (@$goatlist) {
  1985. next if $g->{reltype} ne 'table';
  1986. ($S,$T) = ($g->{safeschema},$g->{safetable});
  1987. $maxst = length "$S.$T" if length "$S.$T" > $maxst;
  1988. }
  1989. $self->{maxdbstname} = $self->{maxdbname} + 1 + $maxst;
  1990. ## If we are using delta tables, prepare all relevant SQL
  1991. if (@dbs_delta) {
  1992. ## Prepare the SQL specific to each table
  1993. for my $g (@$goatlist) {
  1994. ## Only tables get all this fuss: sequences are easy
  1995. next if $g->{reltype} ne 'table';
  1996. ## This is the main query: grab all unique changed primary keys since the last sync
  1997. $SQL{delta}{$g} = qq{
  1998. SELECT DISTINCT $g->{pklist}
  1999. FROM bucardo.$g->{deltatable} d
  2000. WHERE NOT EXISTS (
  2001. SELECT 1
  2002. FROM bucardo.$g->{tracktable} t
  2003. WHERE d.txntime = t.txntime
  2004. AND t.target = TARGETNAME::text
  2005. )
  2006. };
  2007. ## Mark all unclaimed visible delta rows as done in the track table
  2008. $SQL{track}{$g} = qq{
  2009. INSERT INTO bucardo.$g->{tracktable} (txntime,target)
  2010. SELECT DISTINCT txntime, TARGETNAME::text
  2011. FROM bucardo.$g->{deltatable} d
  2012. WHERE NOT EXISTS (
  2013. SELECT 1
  2014. FROM bucardo.$g->{tracktable} t
  2015. WHERE d.txntime = t.txntime
  2016. AND t.target = TARGETNAME::text
  2017. );
  2018. };
  2019. ## The same thing, but to the staging table instead, as we have to
  2020. ## wait for all targets to succesfully commit in multi-source situations
  2021. $SQL{stage}{$g} = qq{
  2022. INSERT INTO bucardo.$g->{stagetable} (txntime,target)
  2023. SELECT DISTINCT txntime, TARGETNAME::text
  2024. FROM bucardo.$g->{deltatable} d
  2025. WHERE NOT EXISTS (
  2026. SELECT 1
  2027. FROM bucardo.$g->{tracktable} t
  2028. WHERE d.txntime = t.txntime
  2029. AND t.target = TARGETNAME::text
  2030. );
  2031. };
  2032. } ## end each table
  2033. ## For each source database, prepare the queries above
  2034. for my $dbname (@dbs_source) {
  2035. $x = $sync->{db}{$dbname};
  2036. ## Set the TARGETNAME for each database: the bucardo.track_* target entry
  2037. ## Unless we start using gangs again, just use the dbgroup
  2038. $x->{TARGETNAME} = "dbgroup $dbs";
  2039. for my $g (@$goatlist) {
  2040. next if $g->{reltype} ne 'table';
  2041. ($S,$T) = ($g->{safeschema},$g->{safetable});
  2042. ## Replace with the target name for source delta querying
  2043. ($SQL = $SQL{delta}{$g}) =~ s/TARGETNAME/'$x->{TARGETNAME}'/o;
  2044. ## As these can be expensive, make them asynchronous
  2045. $sth{getdelta}{$dbname}{$g} = $x->{dbh}->prepare($SQL, {pg_async => PG_ASYNC});
  2046. ## We need to update either the track table or the stage table
  2047. ## There is no way to know beforehand which we will need, so we prepare both
  2048. ## Replace with the target name for source track updating
  2049. ($SQL = $SQL{track}{$g}) =~ s/TARGETNAME/'$x->{TARGETNAME}'/go;
  2050. ## Again, async as they may be slow
  2051. $sth{track}{$dbname}{$g} = $x->{dbh}->prepare($SQL, {pg_async => PG_ASYNC});
  2052. ## Same thing for stage
  2053. ($SQL = $SQL{stage}{$g}) =~ s/TARGETNAME/'$x->{TARGETNAME}'/go;
  2054. $sth{stage}{$dbname}{$g} = $x->{dbh}->prepare($SQL, {pg_async => PG_ASYNC});
  2055. ## Set the per database/per table makedelta setting now
  2056. if (defined $g->{makedelta}) {
  2057. if ($g->{makedelta} eq 'on' or $g->{makedelta} =~ /\b$dbname\b/) {
  2058. $x->{is_makedelta}{$S}{$T} = 1;
  2059. $self->glog("Set table $dbname.$S.$T to makedelta", LOG_NORMAL);
  2060. }
  2061. }
  2062. } ## end each table
  2063. } ## end each source database
  2064. } ## end if delta databases
  2065. ## We disable and enable triggers and rules in one of two ways
  2066. ## For old, pre 8.3 versions of Postgres, we manipulate pg_class
  2067. ## This is not ideal, as we don't lock pg_class and thus risk problems
  2068. ## because the system catalogs are not strictly MVCC. However, there is
  2069. ## no other way to disable rules, which we must do.
  2070. ## If we are 8.3 or higher, we simply use session_replication_role,
  2071. ## which is completely safe, and faster (thanks Jan!)
  2072. ##
  2073. ## We also see if the version is modern enough to use COPY with subselects
  2074. ##
  2075. ## Note that each database within the same sync may have different methods,
  2076. ## so we need to see if anyone is doing things the old way
  2077. my $anyone_does_pgclass = 0;
  2078. for my $dbname (@dbs_write) {
  2079. $x = $sync->{db}{$dbname};
  2080. next if $x->{dbtype} ne 'postgres';
  2081. my $ver = $x->{dbh}{pg_server_version};
  2082. if ($ver >= 80300) {
  2083. $x->{disable_trigrules} = 'replica';
  2084. }
  2085. else {
  2086. $x->{disable_trigrules} = 'pg_class';
  2087. $anyone_does_pgclass = 1;
  2088. }
  2089. ## If 8.2 or higher, we can use COPY (SELECT *)
  2090. $x->{modern_copy} = $ver >= 80200 ? 1 : 0;
  2091. }
  2092. ## We don't bother building these statements unless we need to
  2093. if ($anyone_does_pgclass) {
  2094. ## TODO: Ideally, we would also adjust for "newname"
  2095. ## For now, we do not and thus have a restriction of no
  2096. ## customnames on Postgres databases older than 8.2
  2097. ## The SQL to disable all triggers and rules for the tables in this sync
  2098. $SQL = q{
  2099. UPDATE pg_class
  2100. SET reltriggers = 0, relhasrules = false
  2101. WHERE (
  2102. };
  2103. $SQL .= join "OR\n"
  2104. => map { "(oid = '$_->{safeschema}.$_->{safetable}'::regclass)" }
  2105. grep { $_->{reltype} eq 'table' }
  2106. @$goatlist;
  2107. $SQL .= ')';
  2108. ## We are adding all tables together in a single multi-statement query
  2109. $SQL{disable_trigrules} = $SQL;
  2110. my $setclause =
  2111. ## no critic (RequireInterpolationOfMetachars)
  2112. q{reltriggers = }
  2113. . q{(SELECT count(*) FROM pg_catalog.pg_trigger WHERE tgrelid = pg_catalog.pg_class.oid),}
  2114. . q{relhasrules = }
  2115. . q{CASE WHEN (SELECT COUNT(*) FROM pg_catalog.pg_rules WHERE schemaname=SNAME AND tablename=TNAME) > 0 }
  2116. . q{THEN true ELSE false END};
  2117. ## use critic
  2118. ## The SQL to re-enable rules and triggers
  2119. ## for each table in this sync
  2120. $SQL{etrig} = qq{
  2121. UPDATE pg_class
  2122. SET $setclause
  2123. WHERE oid = 'SCHEMANAME.TABLENAME'::regclass
  2124. };
  2125. $SQL = join ";\n"
  2126. => map {
  2127. my $sql = $SQL{etrig};
  2128. $sql =~ s/SNAME/$_->{safeschemaliteral}/g;
  2129. $sql =~ s/TNAME/$_->{safetableliteral}/g;
  2130. $sql =~ s/SCHEMANAME/$_->{safeschema}/g;
  2131. $sql =~ s/TABLENAME/$_->{safetable}/g;
  2132. $sql;
  2133. }
  2134. grep { $_->{reltype} eq 'table' }
  2135. @$goatlist;
  2136. $SQL{enable_trigrules} .= $SQL;
  2137. } ## end anyone using pg_class to turn off triggers and rules
  2138. ## Common settings for the database handles. Set before passing to DBIx::Safe below
  2139. ## These persist through all subsequent transactions
  2140. ## First, things that are common to databases, irrespective of read/write:
  2141. for my $dbname (@dbs) {
  2142. $x = $sync->{db}{$dbname};
  2143. my $xdbh = $x->{dbh};
  2144. if ($x->{dbtype} eq 'postgres') {
  2145. ## We never want to timeout
  2146. $xdbh->do('SET statement_timeout = 0');
  2147. ## Using the same time zone everywhere keeps us sane
  2148. $xdbh->do(q{SET TIME ZONE 'UTC'});
  2149. ## Rare, but allow for tcp fiddling
  2150. if ($config{tcp_keepalives_idle}) { ## e.g. not 0, should always exist
  2151. $xdbh->do("SET tcp_keepalives_idle = $config{tcp_keepalives_idle}");
  2152. $xdbh->do("SET tcp_keepalives_interval = $config{tcp_keepalives_interval}");
  2153. $xdbh->do("SET tcp_keepalives_count = $config{tcp_keepalives_count}");
  2154. }
  2155. $xdbh->commit();
  2156. } ## end postgres
  2157. elsif ($x->{dbtype} eq 'mysql' or $x->{dbtype} eq 'mariadb') {
  2158. ## Serialize for this session
  2159. $xdbh->do('SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE');
  2160. ## ANSI mode: mostly because we want ANSI_QUOTES
  2161. $xdbh->do(q{SET sql_mode = 'ANSI'});
  2162. ## Use the same time zone everywhere
  2163. $xdbh->do(q{SET time_zone = '+0:00'});
  2164. $xdbh->commit();
  2165. } ## end mysql/mariadb
  2166. }
  2167. ## Now things that apply only to databases we are writing to:
  2168. for my $dbname (@dbs_write) {
  2169. $x = $sync->{db}{$dbname};
  2170. my $xdbh = $x->{dbh};
  2171. if ($x->{dbtype} eq 'postgres') {
  2172. ## Note: no need to turn these back to what they were: we always want to stay in replica mode
  2173. ## If doing old school pg_class hackery, we defer until much later
  2174. if ($x->{disable_trigrules} eq 'replica') {
  2175. $xdbh->do(q{SET session_replication_role = 'replica'});
  2176. $xdbh->commit();
  2177. }
  2178. } ## end postgres
  2179. elsif ($x->{dbtype} eq 'mysql' or $x->{dbtype} eq 'mariadb') {
  2180. ## No foreign key checks, please
  2181. $xdbh->do('SET foreign_key_checks = 0');
  2182. $xdbh->commit();
  2183. } ## end mysql/mariadb
  2184. }
  2185. ## Create safe versions of the database handles if we are going to need them
  2186. if ($sync->{need_safe_dbh_strict} or $sync->{need_safe_dbh}) {
  2187. for my $dbname (@dbs_postgres) {
  2188. $x = $sync->{db}{$dbname};
  2189. my $darg;
  2190. if ($sync->{need_safe_dbh_strict}) {
  2191. for my $arg (sort keys %{ $dbix{ $x->{role} }{strict} }) {
  2192. next if ! length $dbix{ $x->{role} }{strict}{$arg};
  2193. $darg->{$arg} = $dbix{ $x->{role} }{strict}{$arg};
  2194. }
  2195. $darg->{dbh} = $x->{dbh};
  2196. $self->{safe_dbh_strict}{$dbname} = DBIx::Safe->new($darg);
  2197. }
  2198. if ($sync->{need_safe_dbh}) {
  2199. undef $darg;
  2200. for my $arg (sort keys %{ $dbix{ $x->{role} }{notstrict} }) {
  2201. next if ! length $dbix{ $x->{role} }{notstrict}{$arg};
  2202. $darg->{$arg} = $dbix{ $x->{role} }{notstrict}{$arg};
  2203. }
  2204. $darg->{dbh} = $x->{dbh};
  2205. $self->{safe_dbh}{$dbname} = DBIx::Safe->new($darg);
  2206. }
  2207. }
  2208. } ## end DBIX::Safe creations
  2209. $did_setup = 1;
  2210. };
  2211. $err_handler->($@) if !$did_setup;
  2212. ## Begin the main KID loop
  2213. my $didrun = 0;
  2214. my $runkid = sub {
  2215. KID: {
  2216. ## Leave right away if we find a stopfile
  2217. if (-e $self->{stopfile}) {
  2218. $self->glog(qq{Found stopfile "$self->{stopfile}": exiting}, LOG_WARN);
  2219. last KID;
  2220. }
  2221. ## Should we actually do something this round?
  2222. my $dorun = 0;
  2223. ## If we were just created or kicked, go ahead and start a run.
  2224. if ($kicked) {
  2225. $dorun = 1;
  2226. $kicked = 0;
  2227. }
  2228. ## If persistent, listen for messages and do an occasional ping of all databases
  2229. if ($kidsalive) {
  2230. my $nlist = $self->db_get_notices($maindbh);
  2231. for my $name (sort keys %{ $nlist }) {
  2232. my $npid = $nlist->{$name}{firstpid};
  2233. ## Strip the prefix
  2234. $name =~ s/^kid_//o;
  2235. ## The controller wants us to exit
  2236. if ( $name eq $stop_sync_request ) {
  2237. $self->glog('Got a stop sync request, so exiting', LOG_TERSE);
  2238. die 'Stop sync request';
  2239. }
  2240. ## The controller has told us we are clear to go
  2241. elsif ($name eq "run_$syncname") {
  2242. $dorun = 1;
  2243. }
  2244. ## Got a ping? Respond with a pong.
  2245. elsif ($name eq "${$}_ping") {
  2246. $self->glog('Got a ping, issuing pong', LOG_DEBUG);
  2247. $self->db_notify($maindbh, "kid_${$}_pong");
  2248. }
  2249. ## Someone else's sync is running
  2250. elsif (index($name, 'run_') == 0) {
  2251. }
  2252. ## Someone else's sync is stopping
  2253. elsif (index($name, 'stopsync_') == 0) {
  2254. }
  2255. ## Someone else's kid is getting pinged
  2256. elsif (index($name, '_ping') > 0) {
  2257. }
  2258. ## Should not happen, but let's at least log it
  2259. else {
  2260. $self->glog("Warning: received unknown message $name from $npid!", LOG_TERSE);
  2261. }
  2262. } ## end each notice
  2263. ## Now that we've read in any notices, simply rollback
  2264. $maindbh->rollback();
  2265. ## Periodically verify connections to all databases
  2266. if (time() - $lastpingcheck >= $config{kid_pingtime}) {
  2267. ## If this fails, simply have the CTL restart it
  2268. ## Other things match on the exception wording below, so change carefully
  2269. $maindbh->ping or die qq{Ping failed for main database\n};
  2270. for my $dbname (@dbs_dbi) {
  2271. $x = $sync->{db}{$dbname};
  2272. $x->{dbh}->ping or die qq{Ping failed for database "$dbname"\n};
  2273. $x->{dbh}->rollback();
  2274. }
  2275. $lastpingcheck = time();
  2276. }
  2277. } ## end if kidsalive
  2278. ## If we are not doing anything this round, sleep and start over
  2279. ## We will only ever hit this on the second go around, as kids
  2280. ## start as autokicked
  2281. if (! $dorun) {
  2282. sleep $config{kid_sleep};
  2283. redo KID;
  2284. }
  2285. ## From this point on, we are a live kid that is expected to run the sync
  2286. ## Used to report on total times for the long-running parts, e.g. COPY
  2287. my $kid_start_time = [gettimeofday];
  2288. ## Create an entry in the syncrun table to let people know we've started
  2289. $sth{kid_syncrun_insert}->execute($syncname, "Started (KID $$)");
  2290. ## Increment our count of how many times we have been here before
  2291. $kidloop++;
  2292. ## Reset the numbers to track total bucardo_delta matches
  2293. undef %deltacount;
  2294. $deltacount{all} = 0;
  2295. ## Reset our counts of total inserts, deletes, truncates, and conflicts
  2296. undef %dmlcount;
  2297. $dmlcount{deletes} = 0;
  2298. $dmlcount{inserts} = 0;
  2299. $dmlcount{truncates} = 0;
  2300. $dmlcount{conflicts} = 0;
  2301. ## Reset all of our truncate stuff
  2302. $self->{has_truncation} = 0;
  2303. delete $self->{truncateinfo};
  2304. ## Reset some things at the per-database level
  2305. for my $dbname (keys %{ $sync->{db} }) {
  2306. ## This must be set, as it is used by the conflict_strategy below
  2307. $deltacount{$dbname} = 0;
  2308. $dmlcount{allinserts}{$dbname} = 0;
  2309. $dmlcount{alldeletes}{$dbname} = 0;
  2310. $x = $sync->{db}{$dbname};
  2311. delete $x->{truncatewinner};
  2312. }
  2313. ## Reset things at the goat level
  2314. for my $g (@$goatlist) {
  2315. delete $g->{truncatewinner};
  2316. }
  2317. ## Run all 'before_txn' code
  2318. if (exists $sync->{code_before_txn}) {
  2319. ## Let external people know where we are
  2320. $sth{kid_syncrun_update_status}->execute("Code before_txn (KID $$)", $syncname);
  2321. $maindbh->commit();
  2322. for my $code (@{$sync->{code_before_txn}}) {
  2323. ## Check if the code has asked us to skip other before_txn codes
  2324. last if 'last' eq $self->run_kid_custom_code($sync, $code);
  2325. }
  2326. }
  2327. ## Populate the dbrun table so others know we are using these databases
  2328. for my $dbname (@dbs_connectable) {
  2329. $x = $sync->{db}{$dbname};
  2330. $sth{dbrun_insert}->execute($syncname, $dbname, $x->{backend});
  2331. $maindbh->commit();
  2332. }
  2333. ## Add a note to the syncrun table
  2334. $sth{kid_syncrun_update_status}->execute("Begin txn (KID $$)", $syncname);
  2335. ## Figure out our isolation level. Only used for Postgres
  2336. ## All others are hard-coded as 'serializable'
  2337. my $isolation_level = defined $sync->{isolation_level} ? $sync->{isolation_level} :
  2338. $config{isolation_level} || 'serializable';
  2339. ## Commit so our dbrun and syncrun stuff is visible to others
  2340. ## This should be done just before we start transactions on all dbs
  2341. $maindbh->commit();
  2342. ## Start the main transactions by setting isolation levels.
  2343. ## From here on out, speed is important.
  2344. ## Note that all database handles are currently not in a txn
  2345. ## (last action was commit or rollback)
  2346. for my $dbname (@dbs_dbi) {
  2347. $x = $sync->{db}{$dbname};
  2348. ## Just in case:
  2349. $x->{dbh}->rollback();
  2350. if ($x->{dbtype} eq 'postgres') {
  2351. $x->{dbh}->do(qq{SET TRANSACTION ISOLATION LEVEL $isolation_level READ WRITE});
  2352. $self->glog(qq{Set database "$dbname" to serializable read write}, LOG_DEBUG);
  2353. }
  2354. if ($x->{dbtype} eq 'mysql' or $x->{dbtype} eq 'mariadb') {
  2355. $x->{dbh}->do('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE');
  2356. $self->glog(qq{Set database "$dbname" to serializable}, LOG_DEBUG);
  2357. }
  2358. if ($x->{dbtype} eq 'drizzle') {
  2359. ## Drizzle does not appear to have anything to control this yet
  2360. }
  2361. if ($x->{dbtype} eq 'oracle') {
  2362. $x->{dbh}->do('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE');
  2363. ## READ WRITE - can we set serializable and read write at the same time??
  2364. $self->glog(qq{Set database "$dbname" to serializable and read write}, LOG_DEBUG);
  2365. }
  2366. if ($x->{dbtype} eq 'sqlite') {
  2367. ## Nothing needed here, the default seems okay
  2368. }
  2369. if ($x->{dbtype} eq 'redis') {
  2370. ## Implement MULTI, when the driver supports it
  2371. ##$x->{dbh}->multi();
  2372. }
  2373. }
  2374. ## We may want to lock all the tables. Use sparingly
  2375. my $lock_table_mode = '';
  2376. my $force_lock_file = "/tmp/bucardo-force-lock-$syncname";
  2377. ## If the file exists, pull the mode from inside it
  2378. if (-e $force_lock_file) {
  2379. $lock_table_mode = 'EXCLUSIVE';
  2380. if (-s _ and (open my $fh, '<', "$force_lock_file")) {
  2381. my $newmode = <$fh>;
  2382. close $fh or warn qq{Could not close "$force_lock_file": $!\n};
  2383. if (defined $newmode) {
  2384. chomp $newmode;
  2385. ## Quick sanity check: only set if looks like normal words
  2386. $lock_table_mode = $newmode if $newmode =~ /^\s*\w[ \w]+\s*$/o;
  2387. }
  2388. }
  2389. $self->glog(qq{Found lock control file "$force_lock_file". Mode: $lock_table_mode}, LOG_TERSE);
  2390. }
  2391. if ($lock_table_mode) {
  2392. $self->glog("Locking all tables in $lock_table_mode MODE", LOG_TERSE);
  2393. for my $g (@$goatlist) {
  2394. next if $g->{reltype} ne 'table';
  2395. for my $dbname (@dbs_write) {
  2396. $x = $sync->{db}{$dbname};
  2397. ## Figure out which table name to use
  2398. my $tname = $g->{newname}{$syncname}{$dbname};
  2399. if ('postgres' eq $x->{dbtype}) {
  2400. my $com = "$tname IN $lock_table_mode MODE";
  2401. $self->glog("Database $dbname: Locking table $com", LOG_TERSE);
  2402. $x->{dbh}->do("LOCK TABLE $com");
  2403. }
  2404. elsif ('mysql' eq $x->{dbtype} or 'drizzle' eq $x->{dbtype} or 'mariadb' eq $x->{dbtype}) {
  2405. my $com = "$tname WRITE";
  2406. $self->glog("Database $dbname: Locking table $com", LOG_TERSE);
  2407. $x->{dbh}->do("LOCK TABLE $com");
  2408. }
  2409. elsif ('oracle' eq $x->{dbtype}) {
  2410. my $com = "$tname IN EXCLUSIVE MODE";
  2411. $self->glog("Database $dbname: Locking table $com", LOG_TERSE);
  2412. $x->{dbh}->do("LOCK TABLE $com");
  2413. }
  2414. elsif ('sqlite' eq $x->{dbtype}) {
  2415. ## BEGIN EXCLUSIVE? May not be needed...
  2416. }
  2417. }
  2418. }
  2419. }
  2420. ## Run all 'before_check_rows' code
  2421. if (exists $sync->{code_before_check_rows}) {
  2422. $sth{kid_syncrun_update_status}->execute("Code before_check_rows (KID $$)", $syncname);
  2423. $maindbh->commit();
  2424. for my $code (@{$sync->{code_before_check_rows}}) {
  2425. ## Check if the code has asked us to skip other before_check_rows codes
  2426. last if 'last' eq $self->run_kid_custom_code($sync, $code);
  2427. }
  2428. }
  2429. ## Do all the delta (non-fullcopy) targets
  2430. if (@dbs_delta) {
  2431. ## We will never reach this while in onetimecopy mode as @dbs_delta is emptied
  2432. ## Check if any tables were truncated on all source databases
  2433. ## If so, set $self->{has_truncation}; store results in $self->{truncateinfo}
  2434. ## First level keys are schema then table name
  2435. ## Third level is maxtime and maxdb, showing the "winner" for each table
  2436. $SQL = 'SELECT quote_ident(sname), quote_ident(tname), MAX(EXTRACT(epoch FROM cdate))'
  2437. . ' FROM bucardo.bucardo_truncate_trigger '
  2438. . ' WHERE sync = ? AND replicated IS NULL GROUP BY 1,2';
  2439. for my $dbname (@dbs_source) {
  2440. $x = $sync->{db}{$dbname};
  2441. ## Grab the latest truncation time for each table, for this source database
  2442. $self->glog(qq{Checking truncate_trigger table on database "$dbname"}, LOG_VERBOSE);
  2443. $sth = $x->{dbh}->prepare($SQL);
  2444. $self->{has_truncation} += $sth->execute($syncname);
  2445. for my $row (@{ $sth->fetchall_arrayref() }) {
  2446. my ($s,$t,$time) = @{ $row };
  2447. ## Store if this is the new winner
  2448. if (! exists $self->{truncateinfo}{$s}{$t}{maxtime}
  2449. or $time > $self->{truncateinfo}{$s}{$t}{maxtime}) {
  2450. $self->{truncateinfo}{$s}{$t}{maxtime} = $time;
  2451. $self->{truncateinfo}{$s}{$t}{maxdb} = $dbname;
  2452. }
  2453. }
  2454. } ## end each source database, checking for truncations
  2455. ## Now go through and mark the winner within the "x" hash, for easy skipping later on
  2456. if ($self->{has_truncation}) {
  2457. for my $s (keys %{ $self->{truncateinfo} }) {
  2458. for my $t (keys %{ $self->{truncateinfo}{$s} }) {
  2459. my $dbname = $self->{truncateinfo}{$s}{$t}{maxdb};
  2460. $x = $sync->{db}{$dbname};
  2461. $x->{truncatewinner}{$s}{$t} = 1;
  2462. $self->glog("Truncate winner for $s.$t is database $dbname", LOG_DEBUG);
  2463. }
  2464. }
  2465. ## Set the truncate count
  2466. my $number = @dbs_non_fullcopy; ## not the best estimate: corner cases
  2467. $dmlcount{truncate} = $number - 1;
  2468. ## Now map this back to our goatlist
  2469. for my $g (@$goatlist) {
  2470. next if $g->{reltype} ne 'table';
  2471. ($S,$T) = ($g->{safeschema},$g->{safetable});
  2472. if (exists $self->{truncateinfo}{$S}{$T}) {
  2473. $g->{truncatewinner} = $self->{truncateinfo}{$S}{$T}{maxdb};
  2474. }
  2475. }
  2476. }
  2477. ## Next, handle all the sequences
  2478. for my $g (@$goatlist) {
  2479. next if $g->{reltype} ne 'sequence';
  2480. ($S,$T) = ($g->{safeschema},$g->{safetable});
  2481. ## Grab the sequence information from each database
  2482. ## Figure out which source one is the highest
  2483. ## Right now, this is the only sane option.
  2484. ## In the future, we might consider coupling tables and sequences and
  2485. ## then copying sequences based on the 'winning' underlying table
  2486. $SQL = "SELECT * FROM $S.$T";
  2487. my $maxvalue = -1;
  2488. for my $dbname (@dbs_non_fullcopy) {
  2489. $x = $sync->{db}{$dbname};
  2490. next if $x->{dbtype} ne 'postgres';
  2491. $sth = $x->{dbh}->prepare($SQL);
  2492. $sth->execute();
  2493. my $info = $sth->fetchall_arrayref({})->[0];
  2494. $g->{sequenceinfo}{$dbname} = $info;
  2495. ## Only the source databases matter for the max value comparison
  2496. next if $x->{role} ne 'source';
  2497. if ($info->{last_value} > $maxvalue) {
  2498. $maxvalue = $info->{last_value};
  2499. $g->{winning_db} = $dbname;
  2500. }
  2501. }
  2502. $self->glog("Sequence $S.$T from db $g->{winning_db} is the highest", LOG_DEBUG);
  2503. ## Now that we have a winner, apply the changes to every other (non-fullcopy) PG database
  2504. for my $dbname (@dbs_non_fullcopy) {
  2505. $x = $sync->{db}{$dbname};
  2506. next if $x->{dbtype} ne 'postgres';
  2507. $x->{adjustsequence} = 1;
  2508. }
  2509. $deltacount{sequences} += $self->adjust_sequence($g, $sync, $S, $T, $syncname);
  2510. } ## end of handling sequences
  2511. ## We want to line up all the delta count numbers in the logs,
  2512. ## so this tracks the largest number returned
  2513. my $maxcount = 0;
  2514. ## Grab the delta information for each table from each source database
  2515. ## While we could do this as per-db/per-goat instead of per-goat/per-db,
  2516. ## we want to take advantage of the async requests as much as possible,
  2517. ## and we'll get the best benefit by hitting each db in turn
  2518. for my $g (@$goatlist) {
  2519. ## Again, this is only for tables
  2520. next if $g->{reltype} ne 'table';
  2521. ## Populate the global vars
  2522. ($S,$T) = ($g->{safeschema},$g->{safetable});
  2523. ## This is the meat of Bucardo:
  2524. for my $dbname (@dbs_source) {
  2525. ## If we had a truncation, we only get deltas from the "winning" source
  2526. ## We still need these, as we want to respect changes made after the truncation!
  2527. next if exists $g->{truncatewinner} and $g->{truncatewinner} ne $dbname;
  2528. ## Gets all relevant rows from bucardo_deltas: runs asynchronously
  2529. $sth{getdelta}{$dbname}{$g}->execute();
  2530. }
  2531. ## Grab all results as they finish.
  2532. ## Order does not really matter here, except for consistency in the logs
  2533. for my $dbname (@dbs_source) {
  2534. ## Skip if truncating and this one is not the winner
  2535. next if exists $g->{truncatewinner} and $g->{truncatewinner} ne $dbname;
  2536. $x = $sync->{db}{$dbname};
  2537. ## pg_result tells us to wait for the query to finish
  2538. $count = $x->{dbh}->pg_result();
  2539. ## Call finish() and change the ugly 0E0 to a true zero
  2540. $sth{getdelta}{$dbname}{$g}->finish() if $count =~ s/0E0/0/o;
  2541. ## Store counts globally (per sync), per DB, per table, and per table/DB
  2542. $deltacount{all} += $count;
  2543. $deltacount{db}{$dbname} += $count;
  2544. $deltacount{table}{$S}{$T} += $count;
  2545. $deltacount{dbtable}{$dbname}{$S}{$T} = $count; ## NOT a +=
  2546. ## Special versions for FK checks below
  2547. if ($count) {
  2548. $deltacount{tableoid}{$g->{oid}}{$dbname} = $count;
  2549. }
  2550. ## For our pretty output below
  2551. $maxcount = $count if $count > $maxcount;
  2552. } ## end each database
  2553. } ## end each table (deltacount)
  2554. ## Output the counts, now that we know the widths
  2555. for my $g (@$goatlist) {
  2556. ## Only for tables
  2557. next if $g->{reltype} ne 'table';
  2558. ## Populate the global vars
  2559. ($S,$T) = ($g->{safeschema},$g->{safetable});
  2560. for my $dbname (@dbs_source) {
  2561. ## Skip if truncating and this one is not the winner
  2562. next if exists $g->{truncatewinner} and $g->{truncatewinner} ne $dbname;
  2563. $x = $sync->{db}{$dbname};
  2564. $self->glog((sprintf q{Delta count for %-*s : %*d},
  2565. $self->{maxdbstname},
  2566. "$dbname.$S.$T",
  2567. length $maxcount,
  2568. $deltacount{dbtable}{$dbname}{$S}{$T}),
  2569. $deltacount{dbtable}{$dbname}{$S}{$T} ? LOG_NORMAL : LOG_VERBOSE);
  2570. } ## end each db
  2571. } ## end each table
  2572. ## Report on the total number of deltas found
  2573. $self->glog("Total delta count: $deltacount{all}", LOG_VERBOSE);
  2574. ## Reset our list of possible FK issues
  2575. $sync->{fkcheck} = {};
  2576. ## If more than one total source db, break it down at that level
  2577. ## We also check for foreign key dependencies here
  2578. if (keys %{ $deltacount{db} } > 1) {
  2579. ## Figure out the width for the per-db breakdown below
  2580. my $maxdbcount = 0;
  2581. for my $dbname (sort keys %{ $sync->{db} }) {
  2582. $maxdbcount = $deltacount{db}{$dbname}
  2583. if exists $deltacount{db}{$dbname}
  2584. and $deltacount{db}{$dbname} > $maxdbcount;
  2585. }
  2586. for my $dbname (@dbs_source) {
  2587. ## Skip if truncating and deltacount is thus not set
  2588. next if ! exists $deltacount{db}{$dbname};
  2589. $self->glog((sprintf q{Delta count for %-*s: %*d},
  2590. $self->{maxdbname} + 2,
  2591. qq{"$dbname"},
  2592. length $maxdbcount,
  2593. $deltacount{db}{$dbname}), LOG_VERBOSE);
  2594. }
  2595. ## Since we have changes appearing on more than one database,
  2596. ## we need to see if any of the database-spanning tables involved
  2597. ## are linked via foreign keys. If they are, we may have to
  2598. ## change our replication strategy so that the foreign keys are
  2599. ## still intact at the end of our operation.
  2600. ## If we find tables that need to be checked, we add them to $self->{fkcheck}
  2601. ## Walk through each table with changes
  2602. for my $toid (sort keys %{ $deltacount{tableoid} }) {
  2603. my $t1 = $deltacount{tableoid}{$toid};
  2604. my $tname1 = $sync->{tableoid}{$toid}{name};
  2605. ## Find all tables that this table references
  2606. my $info = $sync->{tableoid}{$toid};
  2607. ## Note that we really only need to check one of references or referencedby
  2608. REFFER: for my $reftable (sort keys %{ $info->{references} } ) {
  2609. ## Skip if it has no changes
  2610. next if ! exists $deltacount{tableoid}{$reftable};
  2611. ## At this point, we know that both linked tables have at
  2612. ## least one source change. We also know that at least two
  2613. ## source databases are involved in this sync.
  2614. my $t2 = $deltacount{tableoid}{$reftable};
  2615. my $tname2 = $sync->{tableoid}{$reftable}{name};
  2616. ## The danger is if the changes come from different databases
  2617. ## If this happens, the foreign key relationship may be violated
  2618. ## when we push the changes both ways.
  2619. ## Check if any of the dbs are mismatched. If so, instant FK marking
  2620. for my $db1 (sort keys %$t1) {
  2621. if (! exists $t2->{$db1}) {
  2622. $self->glog("Table $tname1 and $tname2 may have FK issues", LOG_DEBUG);
  2623. $sync->{fkcheck}{$tname1}{$tname2} = 1;
  2624. next REFFER;
  2625. }
  2626. }
  2627. ## So both tables have changes on the same source databases.
  2628. ## Now the only danger is if either has more than one source
  2629. if (keys %$t1 > 1 or keys %$t2 > 1) {
  2630. $self->glog("Table $tname1 and $tname2 may have FK issues", LOG_DEBUG);
  2631. $sync->{fkcheck}{$tname1}{$tname2} = 1;
  2632. $sync->{fkcheck}{$tname2}{$tname1} = 2;
  2633. }
  2634. } ## end each reffed table
  2635. } ## end each changed table
  2636. } ## end if more than one source database has changes
  2637. ## If there were no changes on any sources, rollback all databases,
  2638. ## update the syncrun and dbrun tables, notify listeners,
  2639. ## then either re-loop or leave
  2640. if (! $deltacount{all} and ! $self->{has_truncation}) {
  2641. ## If we modified the bucardo_sequences table, save the change
  2642. if ($deltacount{sequences}) {
  2643. #die "fixme";
  2644. #$sourcedbh->commit();
  2645. }
  2646. ## Just to be safe, rollback everything
  2647. for my $dbname (@dbs_dbi) {
  2648. $x = $sync->{db}{$dbname};
  2649. $x->{dbh}->rollback();
  2650. }
  2651. ## Clear out the entries from the dbrun table
  2652. for my $dbname (@dbs_connectable) {
  2653. $x = $sync->{db}{$dbname};
  2654. ## We never do native fullcopy targets here
  2655. next if $x->{role} eq 'fullcopy';
  2656. $sth = $sth{dbrun_delete};
  2657. $sth->execute($syncname, $dbname);
  2658. $maindbh->commit();
  2659. }
  2660. ## Clear the syncrun table
  2661. my $msg = "No delta rows found (KID $$)";
  2662. $self->end_syncrun($maindbh, 'empty', $syncname, $msg);
  2663. $maindbh->commit();
  2664. ## Let the CTL know we are done
  2665. $self->db_notify($maindbh, "ctl_syncdone_${syncname}");
  2666. $maindbh->commit();
  2667. ## Sleep a hair
  2668. sleep $config{kid_nodeltarows_sleep};
  2669. $self->glog('No changes made this round', LOG_DEBUG);
  2670. redo KID if $kidsalive;
  2671. last KID;
  2672. } ## end no deltas
  2673. ## Only need to turn off triggers and rules once via pg_class
  2674. my $disabled_via_pg_class = 0;
  2675. ## The overall winning database for conflicts
  2676. delete $self->{conflictwinner};
  2677. ## Do each goat in turn
  2678. PUSHDELTA_GOAT: for my $g (@$goatlist) {
  2679. ## No need to proceed unless we're a table
  2680. next if $g->{reltype} ne 'table';
  2681. ## Skip if we've already handled this via fullcopy
  2682. next if $g->{source}{needstruncation};
  2683. ($S,$T) = ($g->{safeschema},$g->{safetable});
  2684. ## Skip this table if no source rows have changed
  2685. ## However, we still need to go on in the case of a truncation
  2686. next if ! $deltacount{table}{$S}{$T} and ! exists $g->{truncatewinner};
  2687. ## How many times this goat has handled an exception?
  2688. $g->{exceptions} ||= 0;
  2689. ## The list of primary key columns
  2690. if (! $g->{pkeycols}) { ## only do this once
  2691. $g->{pkeycols} = '';
  2692. $x=0;
  2693. for my $qpk (@{$g->{qpkey}}) {
  2694. $g->{pkeycols} .= sprintf '%s,', $g->{binarypkey}{$x} ? qq{ENCODE($qpk,'base64')} : $qpk;
  2695. $x++;
  2696. }
  2697. chop $g->{pkeycols};
  2698. $g->{numpkcols} > 1 and $g->{pkeycols} = "($g->{pkeycols})";
  2699. ## Example: id
  2700. ## Example MCPK: (id,"space bar",cdate)
  2701. ## Store a raw version for some non-Postgres targets
  2702. $g->{pkeycolsraw} = join ',' => @{ $g->{pkey} };
  2703. }
  2704. ## How many times have we done the loop below?
  2705. my $delta_attempts = 0;
  2706. ## For each source database, grab all distinct pks for this table
  2707. ## from bucardo_delta (that have not already been pushed to the targetname)
  2708. ## We've already executed and got a count from these queries:
  2709. ## it's now time to gather the actual data
  2710. my %deltabin;
  2711. for my $dbname (@dbs_source) {
  2712. $x = $sync->{db}{$dbname};
  2713. ## Skip if we are truncating and this is not the winner
  2714. next if exists $g->{truncatewinner} and $g->{truncatewinner} ne $dbname;
  2715. ## If this is a truncation, we always want the deltabin to exist, even if empty!
  2716. if (exists $g->{truncatewinner}) {
  2717. $deltabin{$dbname} = {};
  2718. }
  2719. ## Skip if we know we have no rows - and thus have issued a finish()
  2720. next if ! $deltacount{dbtable}{$dbname}{$S}{$T};
  2721. ## Create an empty hash to hold the primary key information
  2722. $deltabin{$dbname} = {};
  2723. while (my $y = $sth{getdelta}{$dbname}{$g}->fetchrow_arrayref()) {
  2724. ## Join all primary keys together with \0, put into hash as key
  2725. ## XXX: Using \0 is not unique for binaries
  2726. if (!$g->{hasbinarypk}) {
  2727. $deltabin{$dbname}{join "\0" => @$y} = 1;
  2728. }
  2729. else {
  2730. my $decodename = '';
  2731. my @pk;
  2732. for my $row (@$y) {
  2733. push @pk => $row;
  2734. }
  2735. $deltabin{$dbname}{join "\0" => @pk} = 1;
  2736. }
  2737. }
  2738. } ## end getting pks from each db for this table
  2739. ## Walk through and make sure we have only one source for each primary key
  2740. ## Simple map of what we've already compared:
  2741. my %seenpair;
  2742. ## Hash indicating which databases have conflicts:
  2743. $self->{db_hasconflict} = {};
  2744. ## Hash of all conflicts for this goat
  2745. ## Key is the primary key value
  2746. ## Value is a list of all databases containing this value
  2747. my %conflict;
  2748. for my $dbname1 (sort keys %deltabin) {
  2749. for my $dbname2 (sort keys %deltabin) {
  2750. ## Don't compare with ourselves
  2751. next if $dbname1 eq $dbname2;
  2752. ## Skip if we've already handled this pair the reverse way
  2753. next if exists $seenpair{$dbname2}{$dbname1};
  2754. $seenpair{$dbname1}{$dbname2} = 1;
  2755. ## Loop through all rows from database 1 and see if they exist on 2
  2756. ## If they do, it's a conflict, and one of them must win
  2757. ## Store in the conflict hash for processing below
  2758. for my $key (keys %{ $deltabin{$dbname1} }) {
  2759. next if ! exists $deltabin{$dbname2}{$key};
  2760. ## Got a conflict! Same pkey updated on both sides
  2761. $conflict{$key}{$dbname1} = 1;
  2762. $conflict{$key}{$dbname2} = 1;
  2763. ## Build a list of which databases have conflicts
  2764. $self->{db_hasconflict}{$dbname1} = 1;
  2765. $self->{db_hasconflict}{$dbname2} = 1;
  2766. }
  2767. }
  2768. }
  2769. ## If we had any conflicts, handle them now
  2770. $count = keys %conflict;
  2771. if ($count) {
  2772. ## Increment count across all tables
  2773. $dmlcount{conflicts} += $count;
  2774. $self->glog("Conflicts for $S.$T: $count", LOG_NORMAL);
  2775. ## If we have a custom conflict handler for this goat, invoke it
  2776. if ($g->{code_conflict}) {
  2777. $self->glog('Starting code_conflict', LOG_VERBOSE);
  2778. ## We pass it %conflict, and assume it will modify all the values therein
  2779. my $code = $g->{code_conflict};
  2780. $code->{info}{conflicts} = \%conflict;
  2781. $self->run_kid_custom_code($sync, $code);
  2782. ## Loop through and make sure the conflict handler has done its job
  2783. while (my ($key, $winner) = each %conflict) {
  2784. if (! defined $winner or ref $winner) {
  2785. ($pkval = $key) =~ s/\0/\|/go;
  2786. die "Conflict handler failed to provide a winner for $S.$T.$pkval";
  2787. }
  2788. if (! exists $deltabin{$winner}) {
  2789. ($pkval = $key) =~ s/\0/\|/go;
  2790. die "Conflict handler provided an invalid winner for $S.$T.$pkval: $winner";
  2791. }
  2792. }
  2793. }
  2794. ## If conflict_strategy is abort, simply die right away
  2795. elsif ('bucardo_abort' eq $g->{conflict_strategy}) {
  2796. die "Aborting sync due to conflict of $S.$T";
  2797. }
  2798. ## If we require a custom code, also die
  2799. elsif ('bucardo_custom' eq $g->{conflict_strategy}) {
  2800. die "Aborting sync due to lack of custom conflict handler for $S.$T";
  2801. }
  2802. ## If we are grabbing the 'latest', figure out which it is
  2803. ## For this handler, we want to treat all the tables in the sync
  2804. ## as deeply linked to each other, and this we have one winning
  2805. ## database for *all* tables in the sync.
  2806. ## Thus, the only things arriving from other databases will be inserts
  2807. elsif ('bucardo_latest' eq $g->{conflict_strategy}) {
  2808. ## We only need to figure out the winning database once
  2809. ## The winner is the latest one to touch any of our tables
  2810. ## In theory, this is a little crappy.
  2811. ## In practice, it works out quite well. :)
  2812. $self->glog(q{Starting 'bucardo_latest' conflict strategy}, LOG_VERBOSE);
  2813. if (! exists $self->{conflictwinner}) {
  2814. for my $dbname (@dbs_delta) {
  2815. $x = $sync->{db}{$dbname};
  2816. ## Start by assuming this DB has no changes
  2817. $x->{lastmod} = 0;
  2818. for my $g (@$goatlist) {
  2819. ## This only makes sense for tables
  2820. next if $g->{reltype} ne 'table';
  2821. ## Prep our SQL: find the epoch of the latest transaction for this table
  2822. if (!exists $g->{sql_max_delta}) {
  2823. $SQL = qq{SELECT extract(epoch FROM MAX(txntime)) FROM bucardo.$g->{deltatable} };
  2824. $g->{sql_max_delta} = $SQL;
  2825. }
  2826. $sth = $x->{dbh}->prepare($g->{sql_max_delta});
  2827. $sth->execute();
  2828. ## Keep in mind we don't really care which table this is
  2829. my $epoch = $sth->fetchall_arrayref()->[0][0];
  2830. ## May be undefined if no rows in the table yet: MAX forces a row back
  2831. if (defined $epoch and $epoch > $x->{lastmod}) {
  2832. $x->{lastmod} = $epoch;
  2833. }
  2834. } ## end checking each table in the sync
  2835. } ## end checking each source database
  2836. ## Now we declare the overall winner
  2837. ## We sort the database names so even in the (very!) unlikely
  2838. ## chance of a tie, the same database always wins
  2839. my $highest = -1;
  2840. for my $dbname (sort @dbs_delta) {
  2841. $x = $sync->{db}{$dbname};
  2842. $self->glog("Conflict check lastmod for $dbname is $x->{lastmod}", LOG_DEBUG);
  2843. if ($x->{lastmod} > $highest) {
  2844. $highest = $x->{lastmod};
  2845. $self->{conflictwinner} = $dbname;
  2846. }
  2847. }
  2848. ## We now have a winning database inside self -> conflictwinner
  2849. ## This means we do not need to update %conflict at all
  2850. $self->glog("Conflict winner is $self->{conflictwinner} with $highest", LOG_VERBOSE);
  2851. } ## end conflictwinner not set yet
  2852. }
  2853. else {
  2854. ## Use the standard conflict: a list of database names
  2855. ## Basically, we use the first valid one we find
  2856. ## The only reason *not* to use an entry is if it had
  2857. ## no updates at all for this run. Note: this does not
  2858. ## mean no conflicts, it means no insert/update/delete
  2859. $self->glog(q{Starting default conflict strategy}, LOG_VERBOSE);
  2860. if (! exists $self->{conflictwinner}) {
  2861. ## Optimize for a single database name
  2862. my $sc = $g->{conflict_strategy};
  2863. if (index($sc, ' ') < 1) {
  2864. ## Sanity check
  2865. if (! exists $deltacount{$sc}) {
  2866. die "Invalid conflict_strategy '$sc' used for $S.$T";
  2867. }
  2868. $self->{conflictwinner} = $sc;
  2869. }
  2870. else {
  2871. ## Have more than one, so figure out the best one to use
  2872. my @dbs = split / +/ => $sc;
  2873. ## Make sure they all exist
  2874. for my $dbname (@dbs) {
  2875. if (! exists $deltacount{$dbname}) {
  2876. die qq{Invalid database "$dbname" found in standard conflict for $S.$T};
  2877. }
  2878. }
  2879. ## Check each candidate in turn
  2880. ## It wins, unless it has no changes at all
  2881. for my $dbname (@dbs) {
  2882. my $found_delta = 0;
  2883. ## Walk through but stop at the first found delta
  2884. for my $g (@$goatlist) {
  2885. ## This only makes sense for tables
  2886. next if $g->{reltype} ne 'table';
  2887. ## Prep our SQL: find the epoch of the latest transaction for this table
  2888. if (!exists $g->{sql_got_delta}) {
  2889. ## We need to know if any have run since the last time we ran this sync
  2890. ## In other words, any deltas newer than the highest track entry
  2891. $SQL = qq{SELECT COUNT(*) FROM bucardo.$g->{deltatable} d }
  2892. . qq{WHERE d.txntime > }
  2893. . qq{(SELECT MAX(txntime) FROM bucardo.$g->{tracktable} }
  2894. . qq{WHERE target = '$x->{TARGETNAME}')};
  2895. $g->{sql_got_delta} = $SQL;
  2896. }
  2897. $sth = $x->{dbh}->prepare($g->{sql_got_delta});
  2898. $count = $sth->execute();
  2899. $sth->finish();
  2900. if ($count >= 1) {
  2901. $found_delta = 1;
  2902. last;
  2903. }
  2904. }
  2905. if (! $found_delta) {
  2906. $self->glog("No rows changed, so discarding conflict winner '$dbname'", LOG_VERBOSE);
  2907. next;
  2908. }
  2909. $self->{conflictwinner} = $dbname;
  2910. last;
  2911. }
  2912. ## No match at all? Must be a non-inclusive list
  2913. if (! exists $self->{conflictwinner}) {
  2914. die qq{Invalid standard conflict '$sc': no matching database found!};
  2915. }
  2916. }
  2917. } ## end conflictwinner not set yet
  2918. } ## end standard conflict
  2919. ## At this point, conflictwinner should be set, OR
  2920. ## %conflict should hold the winning database per key
  2921. ## Walk through and apply to the %deltabin hash
  2922. ## We want to walk through each primary key for this table
  2923. ## We figure out who the winning database is
  2924. ## Then we remove all rows for all databases with this key
  2925. ## Finally, we add the winning databases/key combo to deltabin
  2926. ## We do it this way as we cannot be sure that the combo existed.
  2927. ## It could be the case that the winning database made
  2928. ## no changes to this table!
  2929. for my $key (keys %conflict) {
  2930. my $winner = $self->{conflictwinner} || $conflict{$key};
  2931. ## Delete everyone for this primary key
  2932. for my $dbname (keys %deltabin) {
  2933. delete $deltabin{$dbname}{$key};
  2934. }
  2935. ## Add (or re-add) the winning one
  2936. $deltabin{$winner}{$key} = 1;
  2937. }
  2938. $self->glog('Conflicts have been resolved', LOG_NORMAL);
  2939. } ## end if have conflicts
  2940. ## At this point, %deltabin should contain a single copy of each primary key
  2941. ## It may even be empty if we are truncating
  2942. ## We need to figure out how many sources we have for some later optimizations
  2943. my $numsources = keys %deltabin;
  2944. ## Figure out which databases are getting written to
  2945. ## If there is only one source, then it will *not* get written to
  2946. ## If there is more than one source, then everyone gets written to!
  2947. for my $dbname (keys %{ $sync->{db} }) {
  2948. $x = $sync->{db}{$dbname};
  2949. ## Again: everyone is written to unless there is a single source
  2950. ## A truncation source may have an empty deltabin, but it will exist
  2951. $x->{writtento} = (1==$numsources and exists $deltabin{$dbname}) ? 0 : 1;
  2952. next if ! $x->{writtento};
  2953. next if $x->{dbtype} ne 'postgres';
  2954. ## Should we use the stage table for this database?
  2955. $x->{trackstage} = ($numsources > 1 and exists $deltabin{$dbname}) ? 1 : 0;
  2956. ## Disable triggers and rules the 'old way'
  2957. if ($x->{disable_trigrules} eq 'pg_class' and ! $disabled_via_pg_class) {
  2958. ## Run all 'before_trigger_disable' code
  2959. if (exists $sync->{code_before_trigger_disable}) {
  2960. $sth{kid_syncrun_update_status}->execute("Code before_trigger_disable (KID $$)", $syncname);
  2961. $maindbh->commit();
  2962. for my $code (@{$sync->{code_before_trigger_disable}}) {
  2963. last if 'last' eq $self->run_kid_custom_code($sync, $code);
  2964. }
  2965. }
  2966. $self->glog(qq{Disabling triggers and rules on db "$dbname" via pg_class}, LOG_VERBOSE);
  2967. $x->{dbh}->do($SQL{disable_trigrules});
  2968. ## Run all 'after_trigger_disable' code
  2969. if (exists $sync->{code_after_trigger_disable}) {
  2970. $sth{kid_syncrun_update_status}->execute("Code after_trigger_disable (KID $$)", $syncname);
  2971. $maindbh->commit();
  2972. for my $code (@{$sync->{code_after_trigger_disable}}) {
  2973. last if 'last' eq $self->run_kid_custom_code($sync, $code);
  2974. }
  2975. }
  2976. ## Because this disables all tables in this sync, we only want to do it once
  2977. $disabled_via_pg_class = 1;
  2978. }
  2979. ## If we are rebuilding indexes, disable them for this table now
  2980. ## XXX Do all of these at once as per above? Maybe even combine the call?
  2981. ## XXX No sense in updating pg_class yet another time
  2982. ## XXX Although the relhasindex is important...but may not hurt since we are updating
  2983. ## XXX the row anyway...
  2984. if ($g->{rebuild_index} == 2) { ## XXX why the 2?
  2985. ## No index means no manipulation
  2986. ## We don't cache the value, but simply set index_disabled below
  2987. $SQL = "SELECT relhasindex FROM pg_class WHERE oid = '$S.$T'::regclass";
  2988. if ($x->{dbh}->selectall_arrayref($SQL)->[0][0]) {
  2989. $self->glog("Turning off indexes for $dbname.$S.$T", LOG_NORMAL);
  2990. $SQL = "UPDATE pg_class SET relhasindex = 'f' WHERE oid = '$S.$T'::regclass";
  2991. $x->{dbh}->do($SQL);
  2992. $x->{index_disabled} = 1;
  2993. }
  2994. }
  2995. } ## end setting up each database
  2996. ## Create filehandles for any flatfile databases
  2997. for my $dbname (keys %{ $sync->{db} }) {
  2998. $x = $sync->{db}{$dbname};
  2999. next if $x->{dbtype} !~ /flat/o;
  3000. ## Figure out and set the filename
  3001. my $date = strftime('%Y%m%d_%H%M%S', localtime());
  3002. $x->{filename} = "$config{flatfile_dir}/bucardo.flatfile.$self->{syncname}.$date.sql";
  3003. ## Does this already exist? It's possible we got so quick the old one exists
  3004. ## Since we want the names to be unique, come up with a new name
  3005. if (-e $x->{filename}) {
  3006. my $tmpfile;
  3007. my $extension = 1;
  3008. {
  3009. $tmpfile = "$x->{filename}.$extension";
  3010. last if -e $tmpfile;
  3011. $extension++;
  3012. redo;
  3013. }
  3014. $x->{filename} = $tmpfile;
  3015. }
  3016. $x->{filename} .= '.tmp';
  3017. open $x->{filehandle}, '>>', $x->{filename}
  3018. or die qq{Could not open flatfile "$x->{filename}": $!\n};
  3019. }
  3020. ## Populate the semaphore table if the setting is non-empty
  3021. if ($config{semaphore_table}) {
  3022. my $tname = $config{semaphore_table};
  3023. for my $dbname (@dbs_connectable) {
  3024. $x = $sync->{db}{$dbname};
  3025. if ($x->{dbtype} eq 'mongo') {
  3026. my $collection = $x->{dbh}->get_collection($tname);
  3027. my $object = {
  3028. sync => $syncname,
  3029. status => 'started',
  3030. starttime => scalar gmtime,
  3031. };
  3032. $collection->update
  3033. (
  3034. {sync => $syncname},
  3035. $object,
  3036. { upsert => 1, safe => 1 }
  3037. );
  3038. }
  3039. }
  3040. }
  3041. ## This is where we want to 'rewind' to on a handled exception
  3042. PUSH_SAVEPOINT: {
  3043. $delta_attempts++;
  3044. ## From here on out, we're making changes that may trigger an exception
  3045. ## Thus, if we have exception handling code, we create savepoints to rollback to
  3046. if ($g->{has_exception_code}) {
  3047. for my $dbname (keys %{ $sync->{db} }) {
  3048. $x = $sync->{db}{$dbname};
  3049. ## No need to rollback if we didn't make any changes
  3050. next if ! $x->{writtento};
  3051. $self->glog(qq{Creating savepoint on database "$dbname" for exception handler(s)}, LOG_DEBUG);
  3052. $x->{dbh}->do("SAVEPOINT bucardo_$$")
  3053. or die qq{Savepoint creation failed for bucardo_$$};
  3054. }
  3055. }
  3056. ## This var gets set to true at the end of the eval
  3057. ## Safety check as $@ alone is not enough
  3058. my $evaldone = 0;
  3059. ## This label is solely to localize the DIE signal handler
  3060. LOCALDIE: {
  3061. $sth{kid_syncrun_update_status}->execute("Sync $S.$T (KID $$)", $syncname);
  3062. $maindbh->commit();
  3063. ## Everything before this point should work, so we delay the eval until right before
  3064. ## our first actual data change on a target
  3065. eval {
  3066. ## Walk through each database in %deltabin, and push its contents
  3067. ## to all other databases for this sync
  3068. for my $dbname1 (sort keys %deltabin) {
  3069. ## If we are doing a truncate, delete everything from all other dbs!
  3070. if (exists $g->{truncatewinner}) {
  3071. for my $dbnamet (@dbs) {
  3072. ## Exclude ourselves, which should be the only thing in deltabin!
  3073. next if $dbname1 eq $dbnamet;
  3074. ## Grab the real target name
  3075. my $tname = $g->{newname}{$syncname}{$dbnamet};
  3076. $x = $sync->{db}{$dbnamet};
  3077. my $do_cascade = 0;
  3078. $self->truncate_table($x, $tname, $do_cascade);
  3079. }
  3080. ## We keep going, in case the source has post-truncation items
  3081. }
  3082. ## How many rows are we pushing around? If none, we done!
  3083. my $rows = keys %{ $deltabin{$dbname1} };
  3084. $self->glog("Rows to push from $dbname1.$S.$T: $rows", LOG_VERBOSE);
  3085. ## This also exits us if we are a truncate with no source rows
  3086. next if ! $rows;
  3087. ## Build the list of target databases we are pushing to
  3088. my @pushdbs;
  3089. for my $dbname2 (@dbs_non_fullcopy) {
  3090. ## Don't push to ourselves!
  3091. next if $dbname1 eq $dbname2;
  3092. ## No %seenpair is needed: this time we *do* go both ways (A->B, then B->A)
  3093. push @pushdbs => $sync->{db}{$dbname2};
  3094. }
  3095. my $sdbh = $sync->{db}{$dbname1}{dbh};
  3096. ## Here's the real action: delete/truncate from target, then copy from source to target
  3097. ## For this table, delete all rows that may exist on the target(s)
  3098. $dmlcount{deletes} += $self->delete_rows(
  3099. $deltabin{$dbname1}, $S, $T, $g, $sync, \@pushdbs);
  3100. ## For this table, copy all rows from source to target(s)
  3101. $dmlcount{inserts} += $self->push_rows(
  3102. $deltabin{$dbname1}, $S, $T, $g, $sync, $sdbh, $dbname1, \@pushdbs);
  3103. } ## end source database
  3104. ## Go through each database and as needed:
  3105. ## - turn indexes back on
  3106. ## - run a REINDEX
  3107. ## - release the savepoint
  3108. for my $dbname (sort keys %{ $sync->{db} }) {
  3109. $x = $sync->{db}{$dbname};
  3110. next if ! $x->{writtento};
  3111. if ($x->{index_disabled}) {
  3112. $self->glog("Re-enabling indexes for $dbname.$S.$T", LOG_NORMAL);
  3113. $SQL = "UPDATE pg_class SET relhasindex = 't' WHERE oid = '$S.$T'::regclass";
  3114. $x->{dbh}->do($SQL);
  3115. $self->glog("Reindexing table $dbname.$S.$T", LOG_NORMAL);
  3116. ## We do this asynchronously so we don't wait on each db
  3117. $x->{dbh}->do( "REINDEX TABLE $S.$T", {pg_async => PG_ASYNC} );
  3118. }
  3119. }
  3120. ## Wait for all REINDEXes to finish
  3121. for my $dbname (sort keys %{ $sync->{db} }) {
  3122. $x = $sync->{db}{$dbname};
  3123. next if ! $x->{writtento};
  3124. if ($x->{index_disabled}) {
  3125. $x->{dbh}->pg_result();
  3126. $x->{index_disabled} = 0;
  3127. }
  3128. }
  3129. ## If this table has a possible FK problem,
  3130. ## we need to check things out
  3131. ## Cannot do anything until both pairs have reported in!
  3132. if (exists $sync->{fkcheck}{"$S.$T"}) {
  3133. }
  3134. ## We set this as we cannot rely on $@ alone
  3135. $evaldone = 1;
  3136. }; ## end of eval
  3137. } ## end of LOCALDIE
  3138. ## Got exception handlers, but no exceptions, so reset the count:
  3139. if ($evaldone) {
  3140. $g->{exceptions} = 0;
  3141. }
  3142. ## Did we fail the eval?
  3143. else {
  3144. chomp $@;
  3145. (my $err = $@) =~ s/\n/\\n/g;
  3146. ## If we have no exception code, we simply die to pass control to $err_handler.
  3147. ## XXX If no handler, we want to rewind and try again ourselves
  3148. ## XXX But this time, we want to enter a more aggressive conflict resolution mode
  3149. ## XXX Specifically, we need to ensure that a single database "wins" and that
  3150. ## XXX all table changes therein come from that database.
  3151. ## XXX No need if we only have a single table, of course, or if there were
  3152. ## XXX no possible conflicting changes.
  3153. ## XXX Finally, we skip if the first run already had a canonical winner
  3154. if (!$g->{has_exception_code}) {
  3155. $self->glog("Warning! Aborting due to exception for $S.$T:$pkval Error was $err", LOG_WARN);
  3156. die "$err\n";
  3157. }
  3158. ## We have an exception handler
  3159. $self->glog("Exception caught: $err", LOG_WARN);
  3160. ## Bail if we've already tried to handle this goat via an exception
  3161. if ($g->{exceptions}++ > 1) {
  3162. ## XXX Does this get properly reset on a redo?
  3163. $self->glog("Warning! Exception custom code did not work for $S.$T:$pkval", LOG_WARN);
  3164. die qq{Error: too many exceptions to handle for $S.$T:$pkval};
  3165. }
  3166. ## Time to let the exception handling custom code do its work
  3167. ## First, we rollback to our savepoint on all databases that are using them
  3168. for my $dbname (keys %{ $sync->{db} }) {
  3169. $x = $sync->{db}{$dbname};
  3170. next if ! $x->{writtento};
  3171. $self->glog("Rolling back to savepoint on database $dbname", LOG_DEBUG);
  3172. $x->{dbh}->do("ROLLBACK TO SAVEPOINT bucardo_$$");
  3173. }
  3174. ## Prepare information to pass to the handler about this run
  3175. my $codeinfo = {
  3176. schemaname => $S,
  3177. tablename => $T,
  3178. error_string => $err,
  3179. deltabin => \%deltabin,
  3180. attempts => $delta_attempts,
  3181. };
  3182. ## Set if any handlers think we should try again
  3183. my $runagain = 0;
  3184. for my $code (@{$g->{code_exception}}) {
  3185. $self->glog("Trying exception code $code->{id}: $code->{name}", LOG_TERSE);
  3186. ## Pass in the information above about the current state
  3187. $code->{info} = $codeinfo;
  3188. my $result = $self->run_kid_custom_code($sync, $code);
  3189. ## A request to run the same goat again.
  3190. if ('retry' eq $result) {
  3191. $self->glog('Exception handler thinks we can try again', LOG_NORMAL);
  3192. $runagain = 1;
  3193. last;
  3194. }
  3195. ## Request to skip any other codes
  3196. last if $result eq 'last';
  3197. $self->glog('Going to next available exception code', LOG_VERBOSE);
  3198. next;
  3199. }
  3200. ## If not running again, we simply give up and throw an exception to the kid
  3201. if (!$runagain) {
  3202. $self->glog('No exception handlers were able to help, so we are bailing out', LOG_WARN);
  3203. die qq{No exception handlers were able to help, so we are bailing out\n};
  3204. }
  3205. ## The custom code wants to try again
  3206. ## XXX Should probably reset session_replication_role
  3207. ## Make sure the Postgres database connections are still clean
  3208. for my $dbname (@dbs_postgres) {
  3209. $x = $sync->{db}{$dbname};
  3210. my $ping = $sync->{db}{$dbname}{dbh}->ping();
  3211. if ($ping !~ /^[123]$/o) {
  3212. $self->glog("Warning! Ping on database $dbname after exception handler was $ping", LOG_WARN);
  3213. }
  3214. }
  3215. ## Now jump back and try this goat again!
  3216. redo PUSH_SAVEPOINT;
  3217. } ## end of handled exception
  3218. } ## end of PUSH_SAVEPOINT
  3219. } ## end each goat
  3220. $self->glog("Totals: deletes=$dmlcount{deletes} inserts=$dmlcount{inserts} conflicts=$dmlcount{conflicts}",
  3221. ($dmlcount{deletes} or $dmlcount{inserts} or $dmlcount{conflicts}) ? LOG_NORMAL : LOG_VERBOSE);
  3222. ## Update bucardo_track table so that the bucardo_delta rows we just processed
  3223. ## are marked as "done" and ignored by subsequent runs
  3224. ## Reset our pretty-printer count
  3225. $maxcount = 0;
  3226. for my $g (@$goatlist) {
  3227. next if $g->{reltype} ne 'table';
  3228. ($S,$T) = ($g->{safeschema},$g->{safetable});
  3229. delete $g->{rateinfo};
  3230. ## Gather up our rate information - just store for now, we can write it after the commits
  3231. ## XX Redo with sourcename etc.
  3232. if ($deltacount{source}{$S}{$T} and $sync->{track_rates}) {
  3233. $self->glog('Gathering source rate information', LOG_VERBOSE);
  3234. my $sth = $sth{source}{$g}{deltarate};
  3235. $count = $sth->execute();
  3236. $g->{rateinfo}{source} = $sth->fetchall_arrayref();
  3237. }
  3238. for my $dbname (@dbs_source) {
  3239. if ($deltacount{dbtable}{$dbname}{$S}{$T} and $sync->{track_rates}) {
  3240. $self->glog('Gathering target rate information', LOG_VERBOSE);
  3241. my $sth = $sth{target}{$g}{deltarate};
  3242. $count = $sth->execute();
  3243. $g->{rateinfo}{target} = $sth->fetchall_arrayref();
  3244. }
  3245. }
  3246. ## For each database that had delta changes, insert rows to bucardo_track
  3247. ## We also need to consider makedelta:
  3248. ## For all tables that are marked as makedelta, we need to ensure
  3249. ## that we call the SQL below for each dbs_source in which
  3250. ## the deltacount for *any* other source dbname is non-zero
  3251. for my $dbname (@dbs_source) {
  3252. $x = $sync->{db}{$dbname};
  3253. $x->{needs_track} = 0;
  3254. if ($deltacount{dbtable}{$dbname}{$S}{$T}) {
  3255. $x->{needs_track} = 1;
  3256. }
  3257. elsif (exists $x->{is_makedelta}{$S}{$T}) { ## XXX set this earlier!
  3258. ## We know that this particular table in this database is makedelta
  3259. ## See if any of the other sources had deltas
  3260. ## If they did, then rows were inserted here, so we need a track update
  3261. my $found = 0;
  3262. for my $dbname2 (@dbs_source) {
  3263. if ($deltacount{dbtable}{$dbname}{$S}{$T}) {
  3264. $found = 1;
  3265. last;
  3266. }
  3267. }
  3268. }
  3269. }
  3270. ## Kick off the track or stage update asynchronously
  3271. for my $dbname (@dbs_source) {
  3272. $x = $sync->{db}{$dbname};
  3273. if ($x->{needs_track}) {
  3274. ## This is async:
  3275. if ($x->{trackstage}) {
  3276. $sth{stage}{$dbname}{$g}->execute();
  3277. }
  3278. else {
  3279. $sth{track}{$dbname}{$g}->execute();
  3280. }
  3281. }
  3282. }
  3283. ## Loop through again and let everyone finish
  3284. for my $dbname (@dbs_source) {
  3285. $x = $sync->{db}{$dbname};
  3286. if ($x->{needs_track}) {
  3287. ($count = $x->{dbh}->pg_result()) =~ s/0E0/0/o;
  3288. $self->{insertcount}{dbname}{$S}{$T} = $count;
  3289. $maxcount = $count if $count > $maxcount;
  3290. }
  3291. }
  3292. } ## end each goat
  3293. ## Pretty print the number of rows per db/table
  3294. for my $g (@$goatlist) {
  3295. next if $g->{reltype} ne 'table';
  3296. ($S,$T) = ($g->{safeschema},$g->{safetable});
  3297. for my $dbname (keys %{ $sync->{db} }) {
  3298. $x = $sync->{db}{$dbname};
  3299. if ($deltacount{dbtable}{$dbname}{$S}{$T}) {
  3300. $count = $self->{insertcount}{dbname}{$S}{$T};
  3301. $self->glog((sprintf 'Rows inserted to bucardo_%s for %-*s: %*d',
  3302. $x->{trackstage} ? 'stage' : 'track',
  3303. $self->{maxdbstname},
  3304. "$dbname.$S.$T",
  3305. length $maxcount,
  3306. $count),
  3307. LOG_DEBUG);
  3308. }
  3309. } ## end each db
  3310. } ## end each table
  3311. } ## end if dbs_delta
  3312. ## Handle all the fullcopy targets
  3313. if (@dbs_fullcopy) {
  3314. ## We only need one of the sources, so pull out the first one
  3315. ## (dbs_source should only have a single entry anyway)
  3316. my ($sourcename, $sourcedbh, $sourcex);
  3317. for my $dbname (@dbs_source) {
  3318. $x = $sync->{db}{$dbname};
  3319. $sourcename = $dbname;
  3320. $sourcedbh = $x->{dbh};
  3321. $sourcex = $x;
  3322. $self->glog(qq{For fullcopy, we are using source database "$sourcename"}, LOG_VERBOSE);
  3323. last;
  3324. }
  3325. ## Temporary hash to store onetimecopy information
  3326. $sync->{otc} = {};
  3327. ## Walk through and handle each goat
  3328. GOAT: for my $g (@$goatlist) {
  3329. ($S,$T) = ($g->{safeschema},$g->{safetable});
  3330. ## Handle sequences first
  3331. ## We always do these, regardless of onetimecopy
  3332. if ($g->{reltype} eq 'sequence') {
  3333. $SQL = "SELECT * FROM $S.$T";
  3334. $sth = $sourcedbh->prepare($SQL);
  3335. $sth->execute();
  3336. $g->{sequenceinfo}{$sourcename} = $sth->fetchall_arrayref({})->[0];
  3337. $g->{winning_db} = $sourcename;
  3338. ## We want to modify all fullcopy targets only
  3339. for my $dbname (@dbs_fullcopy) {
  3340. $sync->{db}{$dbname}{adjustsequence} = 1;
  3341. }
  3342. $self->adjust_sequence($g, $sync, $S, $T, $syncname);
  3343. next;
  3344. }
  3345. ## Some tables exists just to be examined but not pushed to
  3346. if ($g->{ghost}) {
  3347. $self->glog("Skipping ghost table $S.$T", LOG_VERBOSE);
  3348. next;
  3349. }
  3350. ## If doing a one-time-copy and using empty mode, skip this table if it has rows
  3351. ## This is done on a per table / per target basis
  3352. if (2 == $sync->{onetimecopy}) {
  3353. ## Also make sure we have at least one row on the source
  3354. my $tname = $g->{newname}{$syncname}{$sourcename};
  3355. if (! $self->table_has_rows($sourcex, $tname)) {
  3356. $self->glog(qq{Source table "$sourcename.$S.$T" has no rows and we are in onetimecopy if empty mode, so we will not COPY}, LOG_NORMAL);
  3357. ## No sense in going any further
  3358. next GOAT;
  3359. }
  3360. ## Check each fullcopy target to see if it is empty and thus ready to COPY
  3361. my $have_targets = 0;
  3362. for my $dbname (@dbs_fullcopy) {
  3363. $x = $sync->{db}{$dbname};
  3364. my $tname = $g->{newname}{$syncname}{$dbname};
  3365. ## If this target table has rows, skip it
  3366. if ($self->table_has_rows($x, $tname)) {
  3367. $sync->{otc}{skip}{$dbname} = 1;
  3368. $self->glog(qq{Target table "$dbname.$tname" has rows and we are in onetimecopy if empty mode, so we will not COPY}, LOG_NORMAL);
  3369. }
  3370. else {
  3371. $have_targets = 1;
  3372. }
  3373. }
  3374. ## If we have no valid targets at all, skip this goat
  3375. next GOAT if ! $have_targets;
  3376. } ## end onetimecopy of 2
  3377. ## The list of targets we will be fullcopying to
  3378. ## This is a subset of dbs_fullcopy, and may be less due
  3379. ## to the target having rows and onetimecopy being set
  3380. my @dbs_copytarget;
  3381. for my $dbname (@dbs_fullcopy) {
  3382. $x = $sync->{db}{$dbname};
  3383. ## Skip if onetimecopy was two and this target had rows
  3384. next if exists $sync->{otc}{skip}{$dbname};
  3385. push @dbs_copytarget => $dbname;
  3386. }
  3387. ## If requested, disable the indexes before we copy
  3388. if ($g->{rebuild_index}) {
  3389. for my $dbname (@dbs_copytarget) {
  3390. $x = $sync->{db}{$dbname};
  3391. ## Grab the actual target table name
  3392. my $tname = $g->{newname}{$syncname}{$dbname};
  3393. if ($x->{dbtype} eq 'postgres') {
  3394. ## TODO: Cache this information earlier
  3395. $SQL = "SELECT relhasindex FROM pg_class WHERE oid = '$tname'::regclass";
  3396. if ($x->{dbh}->selectall_arrayref($SQL)->[0][0]) {
  3397. $self->glog("Turning off indexes for $tname on $dbname", LOG_NORMAL);
  3398. ## Do this without pg_class manipulation when Postgres supports that
  3399. $SQL = "UPDATE pg_class SET relhasindex = 'f' WHERE oid = '$S.$T'::regclass";
  3400. $x->{dbh}->do($SQL);
  3401. $x->{index_disabled} = 1;
  3402. }
  3403. }
  3404. if ($x->{dbtype} eq 'mysql' or $x->{dbtype} eq 'mariadb') {
  3405. $SQL = "ALTER TABLE $tname DISABLE KEYS";
  3406. $self->glog("Disabling keys for $tname on $dbname", LOG_NORMAL);
  3407. $x->{dbh}->do($SQL);
  3408. $x->{index_disabled} = 1;
  3409. }
  3410. if ($x->{dbtype} eq 'sqlite') {
  3411. ## May be too late to do this here
  3412. $SQL = q{PRAGMA foreign_keys = OFF};
  3413. $self->glog("Disabling foreign keys on $dbname", LOG_NORMAL);
  3414. $x->{dbh}->do($SQL);
  3415. $x->{index_disabled} = 1;
  3416. }
  3417. }
  3418. }
  3419. ## Only need to turn off triggers and rules once via pg_class
  3420. my $disabled_via_pg_class = 0;
  3421. ## Truncate the table on all target databases, and fallback to delete if that fails
  3422. for my $dbname (@dbs_copytarget) {
  3423. $x = $sync->{db}{$dbname};
  3424. ## Nothing to do here for flatfiles
  3425. next if $x->{dbtype} =~ /flat/;
  3426. ## Grab the real target name
  3427. my $tname = $g->{newname}{$syncname}{$dbname};
  3428. if ('postgres' eq $x->{dbtype}) {
  3429. ## Disable triggers and rules the 'old way'
  3430. if ($x->{disable_trigrules} eq 'pg_class' and ! $disabled_via_pg_class) {
  3431. ## Run all 'before_trigger_disable' code
  3432. if (exists $sync->{code_before_trigger_disable}) {
  3433. $sth{kid_syncrun_update_status}->execute("Code before_trigger_disable (KID $$)", $syncname);
  3434. $maindbh->commit();
  3435. for my $code (@{$sync->{code_before_trigger_disable}}) {
  3436. last if 'last' eq $self->run_kid_custom_code($sync, $code);
  3437. }
  3438. }
  3439. $self->glog(qq{Disabling triggers and rules on db "$dbname" via pg_class}, LOG_VERBOSE);
  3440. $x->{dbh}->do($SQL{disable_trigrules});
  3441. ## Run all 'after_trigger_disable' code
  3442. if (exists $sync->{code_after_trigger_disable}) {
  3443. $sth{kid_syncrun_update_status}->execute("Code after_trigger_disable (KID $$)", $syncname);
  3444. $maindbh->commit();
  3445. for my $code (@{$sync->{code_after_trigger_disable}}) {
  3446. last if 'last' eq $self->run_kid_custom_code($sync, $code);
  3447. }
  3448. }
  3449. ## Because this disables all tables in this sync, we only want to do it once
  3450. $disabled_via_pg_class = 1;
  3451. }
  3452. } ## end postgres
  3453. $self->glog(qq{Emptying out $dbname.$tname using $sync->{deletemethod}}, LOG_VERBOSE);
  3454. my $use_delete = 1;
  3455. ## By hook or by crook, empty this table
  3456. if ($sync->{deletemethod} =~ /truncate/io) {
  3457. my $do_cascade = $sync->{deletemethod} =~ /cascade/io ? 1 : 0;
  3458. if ($self->truncate_table($x, $tname, $do_cascade)) {
  3459. $self->glog("Truncated table $tname", LOG_VERBOSE);
  3460. $use_delete = 0;
  3461. }
  3462. else {
  3463. $self->glog("Truncation of table $tname failed, so we will try a delete", LOG_VERBOSE);
  3464. }
  3465. }
  3466. if ($use_delete) {
  3467. ## This may take a while, so we update syncrun
  3468. $sth{kid_syncrun_update_status}->execute("DELETE $tname (KID $$)", $syncname);
  3469. $maindbh->commit();
  3470. ## Note: even though $tname is the actual name, we still track stats with $S.$T
  3471. $dmlcount{D}{target}{$S}{$T} = $self->delete_table($x, $tname);
  3472. $dmlcount{alldeletes}{target} += $dmlcount{D}{target}{$S}{$T};
  3473. $self->glog("Rows deleted from $tname: $dmlcount{D}{target}{$S}{$T}", LOG_VERBOSE);
  3474. }
  3475. } ## end each database to be truncated/deleted
  3476. ## For this table, copy all rows from source to target(s)
  3477. $dmlcount{inserts} += $dmlcount{I}{target}{$S}{$T} = $self->push_rows(
  3478. 'fullcopy', $S, $T, $g, $sync, $sourcedbh, $sourcename,
  3479. ## We need an array of database objects here:
  3480. [ map { $sync->{db}{$_} } @dbs_copytarget ]);
  3481. ## Add to our cross-table tally
  3482. $dmlcount{allinserts}{target} += $dmlcount{I}{target}{$S}{$T};
  3483. ## Restore the indexes and run REINDEX where needed
  3484. for my $dbname (@dbs_copytarget) {
  3485. $x = $sync->{db}{$dbname};
  3486. next if ! $x->{index_disabled};
  3487. my $tname = $g->{newname}{$syncname}{$dbname};
  3488. ## May be slow, so update syncrun
  3489. $sth{kid_syncrun_update_status}->execute("REINDEX $tname (KID $$)", $syncname);
  3490. $maindbh->commit();
  3491. if ($x->{dbtype} eq 'postgres') {
  3492. $SQL = "UPDATE pg_class SET relhasindex = 't' WHERE oid = '$tname'::regclass";
  3493. $x->{dbh}->do($SQL);
  3494. ## Do the reindex, and time how long it takes
  3495. my $t0 = [gettimeofday];
  3496. $self->glog("Reindexing table $dbname.$tname", LOG_NORMAL);
  3497. $x->{dbh}->do("REINDEX TABLE $tname");
  3498. $self->glog((sprintf(q{(OTC: %s) REINDEX TABLE %s},
  3499. $self->pretty_time(tv_interval($t0), 'day'), $tname)), LOG_NORMAL);
  3500. }
  3501. if ($x->{dbtype} eq 'mysql' or $x->{dbtype} eq 'mariadb') {
  3502. $SQL = "ALTER TABLE $tname ENABLE KEYS";
  3503. $self->glog("Enabling keys for $tname on $dbname", LOG_NORMAL);
  3504. $x->{dbh}->do($SQL);
  3505. }
  3506. if ($x->{dbtype} eq 'sqlite') {
  3507. $SQL = q{PRAGMA foreign_keys = ON};
  3508. $self->glog("Enabling keys on $dbname", LOG_NORMAL);
  3509. $x->{dbh}->do($SQL);
  3510. }
  3511. $x->{index_disabled} = 0;
  3512. } ## end each target to be reindexed
  3513. ## TODO: logic to clean out delta rows is this was a onetimecopy
  3514. } ## end each goat
  3515. if ($sync->{deletemethod} ne 'truncate') {
  3516. $self->glog("Total target rows deleted: $dmlcount{alldeletes}{target}", LOG_NORMAL);
  3517. }
  3518. $self->glog("Total target rows copied: $dmlcount{allinserts}{target}", LOG_NORMAL);
  3519. } ## end have some fullcopy targets
  3520. ## Close filehandles for any flatfile databases
  3521. for my $dbname (keys %{ $sync->{db} }) {
  3522. $x = $sync->{db}{$dbname};
  3523. next if $x->{dbtype} !~ /flat/o;
  3524. close $x->{filehandle}
  3525. or warn qq{Could not close flatfile "$x->{filename}": $!\n};
  3526. ## Atomically rename it so other processes can pick it up
  3527. (my $newname = $x->{filename}) =~ s/\.tmp$//;
  3528. rename $x->{filename}, $newname;
  3529. ## Remove the old ones, just in case
  3530. delete $x->{filename};
  3531. delete $x->{filehandle};
  3532. }
  3533. ## If using semaphore tables, mark the status as 'complete'
  3534. if ($config{semaphore_table}) {
  3535. my $tname = $config{semaphore_table};
  3536. for my $dbname (@dbs_connectable) {
  3537. $x = $sync->{db}{$dbname};
  3538. if ($x->{dbtype} eq 'mongo') {
  3539. my $collection = $x->{dbh}->get_collection($tname);
  3540. my $object = {
  3541. sync => $syncname,
  3542. status => 'complete',
  3543. endtime => scalar gmtime,
  3544. };
  3545. $collection->update
  3546. (
  3547. {sync => $syncname},
  3548. $object,
  3549. { upsert => 1, safe => 1 }
  3550. );
  3551. }
  3552. }
  3553. }
  3554. ## If doing truncate, do some cleanup
  3555. if (exists $self->{truncateinfo}) {
  3556. ## For each source database that had a truncate entry, mark them all as done
  3557. $SQL = 'UPDATE bucardo.bucardo_truncate_trigger SET replicated = now() WHERE sync = ?';
  3558. for my $dbname (@dbs_source) {
  3559. $x = $sync->{db}{$dbname};
  3560. $x->{sth} = $x->{dbh}->prepare($SQL, {pg_async => PG_ASYNC});
  3561. $x->{sth}->execute($syncname);
  3562. }
  3563. for my $dbname (@dbs_source) {
  3564. $x = $sync->{db}{$dbname};
  3565. $x->{dbh}->pg_result();
  3566. }
  3567. }
  3568. ## Run all 'before_trigger_enable' code
  3569. if (exists $sync->{code_before_trigger_enable}) {
  3570. $sth{kid_syncrun_update_status}->execute("Code before_trigger_enable (KID $$)", $syncname);
  3571. $maindbh->commit();
  3572. for my $code (@{$sync->{code_before_trigger_enable}}) {
  3573. last if 'last' eq $self->run_kid_custom_code($sync, $code);
  3574. }
  3575. }
  3576. ## Bring the db back to normal
  3577. for my $dbname (@dbs_write) {
  3578. $x = $sync->{db}{$dbname};
  3579. next if ! $x->{writtento};
  3580. ## Turn triggers and rules back on if using old-school pg_class hackery
  3581. if ($x->{dbtype} eq 'postgres') {
  3582. next if $x->{disable_trigrules} ne 'pg_class';
  3583. $self->glog(qq{Enabling triggers and rules on $dbname via pg_class}, LOG_VERBOSE);
  3584. $x->{dbh}->do($SQL{enable_trigrules});
  3585. }
  3586. elsif ($x->{dbtype} eq 'mysql' or $x->{dbtype} eq 'mariadb') {
  3587. $self->glog(qq{Turning foreign key checks back on for $dbname}, LOG_VERBOSE);
  3588. $x->{dbh}->do('SET foreign_key_checks = 1');
  3589. }
  3590. }
  3591. ## Run all 'after_trigger_enable' code
  3592. if (exists $sync->{code_after_trigger_enable}) {
  3593. $sth{kid_syncrun_update_status}->execute("Code after_trigger_enable (KID $$)", $syncname);
  3594. $maindbh->commit();
  3595. for my $code (@{$sync->{code_after_trigger_enable}}) {
  3596. last if 'last' eq $self->run_kid_custom_code($sync, $code);
  3597. }
  3598. }
  3599. if ($self->{dryrun}) {
  3600. $self->glog('Dryrun, rolling back...', LOG_TERSE);
  3601. for my $dbname (@dbs_dbi) {
  3602. $sync->{db}{$dbname}{dbh}->rollback();
  3603. }
  3604. for my $dbname (@dbs_redis) {
  3605. ## Implement DISCARD when the client supports it
  3606. ##$sync->{db}{$dbname}{dbh}->discard();
  3607. }
  3608. $maindbh->rollback();
  3609. }
  3610. else {
  3611. $self->glog(q{Issuing final commit for all databases}, LOG_VERBOSE);
  3612. ## This is a tricky bit: all writeable databases *must* go first
  3613. ## If we only have a single source, this ensures we don't mark rows as done
  3614. ## in the track tables before everyone has reported back
  3615. for my $dbname (@dbs_dbi) {
  3616. next if ! $x->{writtento};
  3617. $sync->{db}{$dbname}{dbh}->commit();
  3618. }
  3619. ## Now we can commit anyone else
  3620. for my $dbname (@dbs_dbi) {
  3621. next if $x->{writtento};
  3622. $sync->{db}{$dbname}{dbh}->commit();
  3623. }
  3624. for my $dbname (@dbs_redis) {
  3625. ## Implement EXEC when the client supports it
  3626. ## $sync->{db}{$dbname}{dbh}->exec();
  3627. }
  3628. $self->glog(q{All databases committed}, LOG_VERBOSE);
  3629. }
  3630. ## If we used a staging table for the tracking info, do the final inserts now
  3631. ## This is the safest way to ensure we never miss any changes
  3632. for my $dbname (@dbs_dbi) {
  3633. $x = $sync->{db}{$dbname};
  3634. next if ! $x->{trackstage};
  3635. my $dbh = $sync->{db}{$dbname}{dbh};
  3636. for my $g (@$goatlist) {
  3637. next if $g->{reltype} ne 'table';
  3638. $SQL = "INSERT INTO bucardo.$g->{tracktable} SELECT * FROM bucardo.$g->{stagetable}";
  3639. $dbh->do($SQL);
  3640. $SQL = "TRUNCATE TABLE bucardo.$g->{stagetable}";
  3641. $dbh->do($SQL);
  3642. $self->glog("Populated $dbname.$g->{tracktable}", LOG_DEBUG);
  3643. }
  3644. $dbh->commit();
  3645. }
  3646. ## Capture the current time. now() is good enough as we just committed or rolled back
  3647. ## XXX used for track below
  3648. #my $source_commit_time = $sourcedbh->selectall_arrayref('SELECT now()')->[0][0];
  3649. #my $target_commit_time = $targetdbh->selectall_arrayref('SELECT now()')->[0][0];
  3650. #$sourcedbh->commit();
  3651. #$targetdbh->commit();
  3652. my ($source_commit_time, $target_commit_time);
  3653. ## Update the syncrun table, including the delete and insert counts
  3654. my $reason = "Finished (KID $$)";
  3655. my $details = '';
  3656. $count = $sth{kid_syncrun_end}->execute(
  3657. $dmlcount{deletes}, $dmlcount{inserts}, $dmlcount{truncates}, $dmlcount{conflicts},
  3658. $details, $reason, $syncname);
  3659. ## Change this row to the latest good or empty
  3660. my $action = ($dmlcount{deletes} or $dmlcount{inserts} or $dmlcount{truncates})
  3661. ? 'good' : 'empty';
  3662. $self->end_syncrun($maindbh, $action, $syncname, "Complete (KID $$)");
  3663. $maindbh->commit();
  3664. ## Just in case, report on failure to update
  3665. if ($count != 1) {
  3666. $self->glog("Unable to correctly update syncrun table! (count was $count)", LOG_TERSE);
  3667. }
  3668. ## Put a note in the logs for how long this took
  3669. my $synctime = sprintf '%.2f', tv_interval($kid_start_time);
  3670. $self->glog((sprintf 'Total time for sync "%s" (%s rows): %s%s',
  3671. $syncname,
  3672. $dmlcount{inserts},
  3673. pretty_time($synctime),
  3674. $synctime < 120 ? '' : " ($synctime seconds)",),
  3675. ## We don't want to output a "finished" if no changes made unless verbose
  3676. $dmlcount{allinserts}{target} ? LOG_NORMAL : LOG_VERBOSE);
  3677. ## Update our rate information as needed
  3678. if ($sync->{track_rates}) {
  3679. $SQL = 'INSERT INTO bucardo_rate(sync,goat,target,mastercommit,slavecommit,total) VALUES (?,?,?,?,?,?)';
  3680. $sth = $maindbh->prepare($SQL);
  3681. for my $g (@$goatlist) {
  3682. next if ! exists $g->{rateinfo} or $g->{reltype} ne 'table';
  3683. ($S,$T) = ($g->{safeschema},$g->{safetable});
  3684. if ($deltacount{source}{$S}{$T}) {
  3685. for my $time (@{$g->{rateinfo}{source}}) {
  3686. #$sth->execute($syncname,$g->{id},$targetname,$time,$source_commit_time,$deltacount{source}{$S}{$T});
  3687. }
  3688. }
  3689. if ($deltacount{target}{$S}{$T}) {
  3690. for my $time (@{$g->{rateinfo}{target}}) {
  3691. # fixme
  3692. #$sth->execute($syncname,$g->{id},$sourcename,$time,$source_commit_time,$deltacount{target}{$S}{$T});
  3693. }
  3694. }
  3695. }
  3696. $maindbh->commit();
  3697. } ## end of track_rates
  3698. if (@dbs_fullcopy and !$self->{dryrun}) {
  3699. if ($sync->{vacuum_after_copy}) {
  3700. ## May want to break this output down by table
  3701. $sth{kid_syncrun_update_status}->execute("VACUUM (KID $$)", $syncname);
  3702. $maindbh->commit();
  3703. for my $dbname (@dbs_fullcopy) {
  3704. $x = $sync->{db}{$dbname};
  3705. for my $g (@$goatlist) {
  3706. next if ! $g->{vacuum_after_copy} or $g->{reltype} ne 'table';
  3707. my $tablename = $g->{newname}{$syncname}{$dbname};
  3708. $self->vacuum_table($kid_start_time, $x->{dbtype}, $x->{dbh}, $x->{name}, $tablename);
  3709. }
  3710. }
  3711. }
  3712. if ($sync->{analyze_after_copy}) {
  3713. $sth{kid_syncrun_update_status}->execute("ANALYZE (KID $$)", $syncname);
  3714. $maindbh->commit();
  3715. for my $dbname (@dbs_fullcopy) {
  3716. $x = $sync->{db}{$dbname};
  3717. for my $g (@$goatlist) {
  3718. next if ! $g->{analyze_after_copy} or $g->{reltype} ne 'table';
  3719. if ($g->{onetimecopy_ifempty}) {
  3720. $g->{onetimecopy_ifempty} = 0;
  3721. next;
  3722. }
  3723. my $tablename = $g->{newname}{$syncname}{$dbname};
  3724. $self->analyze_table($kid_start_time, $x->{dbtype}, $x->{dbh}, $x->{name}, $tablename);
  3725. }
  3726. }
  3727. }
  3728. }
  3729. my $total_time = sprintf '%.2f', tv_interval($kid_start_time);
  3730. ## Remove lock file if we used it
  3731. if ($lock_table_mode and -e $force_lock_file) {
  3732. $self->glog("Removing lock control file $force_lock_file", LOG_VERBOSE);
  3733. unlink $force_lock_file or $self->glog("Warning! Failed to unlink $force_lock_file", LOG_WARN);
  3734. }
  3735. ## Run all 'after_txn' code
  3736. if (exists $sync->{code_after_txn}) {
  3737. $sth{kid_syncrun_update_status}->execute("Code after_txn (KID $$)", $syncname);
  3738. $maindbh->commit();
  3739. for my $code (@{$sync->{code_after_txn}}) {
  3740. last if 'last' eq $self->run_kid_custom_code($sync, $code);
  3741. }
  3742. }
  3743. ## Clear out the entries from the dbrun table
  3744. for my $dbname (@dbs_connectable) {
  3745. $sth = $sth{dbrun_delete};
  3746. $sth->execute($syncname, $dbname);
  3747. $maindbh->commit();
  3748. }
  3749. ## Notify the parent that we are done
  3750. $self->db_notify($maindbh, "ctl_syncdone_${syncname}");
  3751. $maindbh->commit();
  3752. ## If this was a onetimecopy, leave so we don't have to rebuild dbs_fullcopy etc.
  3753. if ($sync->{onetimecopy}) {
  3754. $self->glog('Turning onetimecopy back to 0', LOG_VERBOSE);
  3755. $SQL = 'UPDATE sync SET onetimecopy=0 WHERE name = ?';
  3756. $sth = $maindbh->prepare($SQL);
  3757. $sth->execute($syncname);
  3758. $maindbh->commit();
  3759. ## This gets anything loaded from scratch from this point
  3760. ## The CTL knows to switch onetimecopy off because it gets a syncdone signal
  3761. last KID;
  3762. }
  3763. if (! $kidsalive) {
  3764. $self->glog('Kid is not kidsalive, so exiting', LOG_DEBUG);
  3765. last KID;
  3766. }
  3767. redo KID;
  3768. } ## end KID
  3769. ## Disconnect from all the databases used in this sync
  3770. for my $dbname (@dbs_dbi) {
  3771. my $dbh = $sync->{db}{$dbname}{dbh};
  3772. $dbh->rollback();
  3773. $_->finish for values %{ $dbh->{CachedKids} };
  3774. $dbh->disconnect();
  3775. }
  3776. if ($sync->{onetimecopy}) {
  3777. ## XXX
  3778. ## We need the MCP and CTL to pick up the new setting. This is the
  3779. ## easiest way: First we sleep a second, to make sure the CTL has
  3780. ## picked up the syncdone signal. It may resurrect a kid, but it
  3781. ## will at least have the correct onetimecopy
  3782. #sleep 1;
  3783. #$maindbh->do("NOTIFY reload_sync_$syncname");
  3784. #$maindbh->commit();
  3785. }
  3786. ## Disconnect from the main database
  3787. $maindbh->disconnect();
  3788. $self->cleanup_kid('Normal exit', '');
  3789. $didrun = 1;
  3790. }; ## end $runkid
  3791. ## Do the actual work.
  3792. RUNKID: {
  3793. $didrun = 0;
  3794. eval { $runkid->() };
  3795. exit 0 if $didrun;
  3796. my $err = $@;
  3797. ## Bail out unless this error came from DBD::Pg
  3798. $err_handler->($err) if $err !~ /DBD::Pg/;
  3799. ## We only do special things for certain errors, so check for those.
  3800. my ($sleeptime,$payload_detail) = (0,'');
  3801. my @states = map { $sync->{db}{$_}{dbh}->state } @dbs_dbi;
  3802. if (first { $_ eq '40001' } @states) {
  3803. $sleeptime = $config{kid_serial_sleep};
  3804. ## If set to -1, this means we never try again
  3805. if ($sleeptime < 0) {
  3806. $self->glog('Could not serialize, will not retry', LOG_VERBOSE);
  3807. $err_handler->($err);
  3808. }
  3809. elsif ($sleeptime) {
  3810. $self->glog((sprintf "Could not serialize, will sleep for %s %s",
  3811. $sleeptime, 1==$sleeptime ? 'second' : 'seconds'), LOG_NORMAL);
  3812. }
  3813. else {
  3814. $self->glog('Could not serialize, will try again', LOG_NORMAL);
  3815. }
  3816. $payload_detail = "Serialization failure. Sleep=$sleeptime";
  3817. }
  3818. elsif (first { $_ eq '40P01' } @states) {
  3819. $sleeptime = $config{kid_deadlock_sleep};
  3820. ## If set to -1, this means we never try again
  3821. if ($sleeptime < 0) {
  3822. $self->glog('Encountered a deadlock, will not retry', LOG_VERBOSE);
  3823. $err_handler->($err);
  3824. }
  3825. elsif ($sleeptime) {
  3826. $self->glog((sprintf "Encountered a deadlock, will sleep for %s %s",
  3827. $sleeptime, 1==$sleeptime ? 'second' : 'seconds'), LOG_NORMAL);
  3828. }
  3829. else {
  3830. $self->glog('Encountered a deadlock, will try again', LOG_NORMAL);
  3831. }
  3832. $payload_detail = "Deadlock detected. Sleep=$sleeptime";
  3833. ## TODO: Get more information via gett_deadlock_details()
  3834. }
  3835. else {
  3836. $err_handler->($err);
  3837. }
  3838. ## Roll everyone back
  3839. for my $dbname (@dbs_dbi) {
  3840. my $dbh = $sync->{db}{$dbname}{dbh};
  3841. $dbh->pg_cancel if $dbh->{pg_async_status} > 0;
  3842. $dbh->rollback;
  3843. }
  3844. $maindbh->rollback;
  3845. ## Tell listeners we are about to sleep
  3846. ## TODO: Add some sweet payload information: sleep time, which dbs/tables failed, etc.
  3847. $self->db_notify($maindbh, "syncsleep_${syncname}", 0, $payload_detail);
  3848. $maindbh->commit;
  3849. ## Sleep and try again.
  3850. sleep $sleeptime if $sleeptime;
  3851. $kicked = 1;
  3852. redo RUNKID;
  3853. }
  3854. } ## end of start_kid
  3855. sub connect_database {
  3856. ## Connect to the given database
  3857. ## Arguments: one
  3858. ## 1. The id of the database
  3859. ## If the database id is blank or zero, we return the main database
  3860. ## Returns:
  3861. ## - the database handle and the backend PID
  3862. ## OR
  3863. ## - the string 'inactive' if set as such in the db table
  3864. ## OR
  3865. ## - the string 'flat' if this is a flatfile 'database'
  3866. my $self = shift;
  3867. my $id = shift || 0;
  3868. my ($dsn,$dbh,$user,$pass,$ssp);
  3869. my $dbtype = 'postgres';
  3870. ## If id is 0, connect to the main database
  3871. if (!$id) {
  3872. $dsn = "dbi:Pg:dbname=$self->{dbname}";
  3873. defined $self->{dbport} and length $self->{dbport} and $dsn .= ";port=$self->{dbport}";
  3874. defined $self->{dbhost} and length $self->{dbhost} and $dsn .= ";host=$self->{dbhost}";
  3875. defined $self->{dbconn} and length $self->{dbconn} and $dsn .= ";$self->{dbconn}";
  3876. $user = $self->{dbuser};
  3877. $pass = $self->{dbpass};
  3878. $ssp = 1;
  3879. }
  3880. else {
  3881. my $db = $self->get_dbs;
  3882. exists $db->{$id} or die qq{Invalid database id!: $id\n};
  3883. my $d = $db->{$id};
  3884. $dbtype = $d->{dbtype};
  3885. if ($d->{status} ne 'active') {
  3886. return 0, 'inactive';
  3887. }
  3888. ## Flat files do not actually get connected to, of course
  3889. if ($dbtype =~ /flat/o) {
  3890. return 0, 'flat';
  3891. }
  3892. if ('postgres' eq $dbtype) {
  3893. $dsn = "dbi:Pg:dbname=$d->{dbname}";
  3894. }
  3895. elsif ('drizzle' eq $dbtype) {
  3896. $dsn = "dbi:drizzle:database=$d->{dbname}";
  3897. }
  3898. elsif ('mongo' eq $dbtype) {
  3899. my $dsn = {};
  3900. for my $name (qw/ dbhost dbport dbuser dbpass /) {
  3901. defined $d->{$name} and length $d->{$name} and $dsn->{$name} = $d->{$name};
  3902. }
  3903. ## For now, we simply require it
  3904. require MongoDB;
  3905. my $conn = MongoDB::Connection->new($dsn); ## no critic
  3906. $dbh = $conn->get_database($d->{dbname});
  3907. my $backend = 0;
  3908. return $backend, $dbh;
  3909. }
  3910. elsif ('mysql' eq $dbtype or 'mariadb' eq $dbtype) {
  3911. $dsn = "dbi:mysql:database=$d->{dbname}";
  3912. }
  3913. elsif ('oracle' eq $dbtype) {
  3914. $dsn = "dbi:Oracle:dbname=$d->{dbname}";
  3915. $d->{dbhost} ||= ''; $d->{dbport} ||= ''; $d->{conn} ||= '';
  3916. defined $d->{dbhost} and length $d->{dbhost} and $dsn .= ";host=$d->{dbhost}";
  3917. defined $d->{dbport} and length $d->{dbport} and $dsn .= ";port=$d->{dbport}";
  3918. defined $d->{dbconn} and length $d->{dbconn} and $dsn .= ";$d->{dbconn}";
  3919. }
  3920. elsif ('redis' eq $dbtype) {
  3921. my $dsn = {};
  3922. for my $name (qw/ dbhost dbport dbuser dbpass /) {
  3923. defined $d->{$name} and length $d->{$name} and $dsn->{$name} = $d->{$name};
  3924. }
  3925. my @dsn;
  3926. my $server = '';
  3927. if (defined $d->{host} and length $d->{host}) {
  3928. $server = $d->{host};
  3929. }
  3930. if (defined $d->{port} and length $d->{port}) {
  3931. $server = ":$d->{port}";
  3932. }
  3933. if ($server) {
  3934. push @dsn => 'server', $server;
  3935. }
  3936. ## For now, we simply require it
  3937. require Redis;
  3938. $dbh = Redis->new(@dsn);
  3939. my $backend = 0;
  3940. return $backend, $dbh;
  3941. }
  3942. elsif ('sqlite' eq $dbtype) {
  3943. $dsn = "dbi:SQLite:dbname=$d->{dbname}";
  3944. }
  3945. else {
  3946. die qq{Cannot handle databases of type "$dbtype"\n};
  3947. }
  3948. defined $d->{dbport} and length $d->{dbport} and $dsn .= ";port=$d->{dbport}";
  3949. defined $d->{dbhost} and length $d->{dbhost} and $dsn .= ";host=$d->{dbhost}";
  3950. length $d->{dbconn} and $dsn .= ";$d->{dbconn}";
  3951. $user = $d->{dbuser};
  3952. $pass = $d->{dbpass} || '';
  3953. $ssp = $d->{server_side_prepares};
  3954. }
  3955. $dbh = DBI->connect
  3956. (
  3957. $dsn,
  3958. $user,
  3959. $pass,
  3960. {AutoCommit=>0, RaiseError=>1, PrintError=>0}
  3961. );
  3962. ## Register this database in our global list
  3963. ## Note that we only worry about DBI-backed databases here,
  3964. ## as there is no particular cleanup needed (e.g. InactiveDestroy)
  3965. ## for other types.
  3966. $self->{dbhlist}{$dbh} = $dbh;
  3967. if ($dbtype ne 'postgres') {
  3968. return 0, $dbh;
  3969. }
  3970. ## Set the application name if we can
  3971. if ($dbh->{pg_server_version} >= 90000) {
  3972. $dbh->do(q{SET application_name='bucardo'});
  3973. $dbh->commit();
  3974. }
  3975. ## If we are using something like pgbouncer, we need to tell Bucardo not to
  3976. ## use server-side prepared statements, as they will not span commits/rollbacks.
  3977. if (! $ssp) {
  3978. $self->glog('Turning off server-side prepares for this database connection', LOG_TERSE);
  3979. $dbh->{pg_server_prepare} = 0;
  3980. }
  3981. ## Grab the backend PID for this Postgres process
  3982. ## Also a nice check that everything is working properly
  3983. $SQL = 'SELECT pg_backend_pid()';
  3984. my $backend = $dbh->selectall_arrayref($SQL)->[0][0];
  3985. $dbh->rollback();
  3986. ## If the main database, prepend 'bucardo' to the search path
  3987. if (!$id) {
  3988. $dbh->do(q{SELECT pg_catalog.set_config('search_path', 'bucardo,' || current_setting('search_path'), false)});
  3989. $dbh->commit();
  3990. }
  3991. return $backend, $dbh;
  3992. } ## end of connect_database
  3993. sub reload_config_database {
  3994. ## Reload the %config and %config_about hashes from the bucardo_config table
  3995. ## Calls commit on the masterdbh
  3996. ## Arguments: none
  3997. ## Returns: undef
  3998. my $self = shift;
  3999. undef %config;
  4000. undef %config_about;
  4001. my %log_level_number = (
  4002. WARN => 1, ## Yes, this is correct. Should not be able to set lower than 1
  4003. TERSE => 1,
  4004. NORMAL => 2,
  4005. VERBOSE => 3,
  4006. DEBUG => 4,
  4007. );
  4008. $SQL = 'SELECT name,setting,about,type,name FROM bucardo_config';
  4009. $sth = $self->{masterdbh}->prepare($SQL);
  4010. $sth->execute();
  4011. for my $row (@{$sth->fetchall_arrayref({})}) {
  4012. ## Things from an rc file can override the value in the db
  4013. my $setting = exists $self->{$row->{name}} ? $self->{$row->{name}} : $row->{setting};
  4014. if ($row->{name} eq 'log_level') {
  4015. my $newvalue = $log_level_number{uc $setting};
  4016. if (! defined $newvalue) {
  4017. die "Invalid log_level!\n";
  4018. }
  4019. $config{log_level_number} = $newvalue;
  4020. }
  4021. if (defined $row->{type}) {
  4022. $config{$row->{type}}{$row->{name}}{$row->{setting}} = $setting;
  4023. $config_about{$row->{type}}{$row->{name}}{$row->{setting}} = $row->{about};
  4024. }
  4025. else {
  4026. $config{$row->{name}} = $setting;
  4027. $config_about{$row->{name}} = $row->{about};
  4028. }
  4029. }
  4030. $self->{masterdbh}->commit();
  4031. return;
  4032. } ## end of reload_config_database
  4033. sub log_config {
  4034. ## Write the current contents of the config hash to the log
  4035. ## Arguments: none
  4036. ## Returns: undef
  4037. my $self = shift;
  4038. my $msg = "Bucardo config:\n";
  4039. ## Figure out the longest key name for pretty formatting
  4040. my $maxlen = 5;
  4041. for (keys %config) {
  4042. $maxlen = length($_) if length($_) > $maxlen;
  4043. }
  4044. ## Print each config name and setting in alphabetic order
  4045. for (sort keys %config) {
  4046. $msg .= sprintf " %-*s => %s\n", $maxlen, $_, (defined $config{$_}) ? qq{'$config{$_}'} : 'undef';
  4047. }
  4048. $self->glog($msg, LOG_WARN);
  4049. return;
  4050. } ## end of log_config
  4051. sub _logto {
  4052. my $self = shift;
  4053. if ($self->{logpid} && $self->{logpid} != $$) {
  4054. # We've forked! Get rid of any existing handles.
  4055. delete $self->{logcodes};
  4056. }
  4057. return @{ $self->{logcodes} } if $self->{logcodes};
  4058. # Do no logging if any destination is "none".
  4059. return @{ $self->{logcodes} = [] }
  4060. if grep { $_ eq 'none' } @{ $self->{logdest} };
  4061. $self->{logpid} = $$;
  4062. my %code_for;
  4063. for my $dest (@{ $self->{logdest}} ) {
  4064. next if $code_for{$dest};
  4065. if ($dest eq 'syslog') {
  4066. openlog 'Bucardo', 'pid nowait', $config{syslog_facility};
  4067. ## Ignore the header argument for syslog output.
  4068. $code_for{syslog} = sub { shift; syslog 'info', @_ };
  4069. }
  4070. elsif ($dest eq 'stderr') {
  4071. $code_for{stderr} = sub { print STDERR @_, $/ };
  4072. }
  4073. elsif ($dest eq 'stdout') {
  4074. $code_for{stdout} = sub { print STDOUT @_, $/ };
  4075. }
  4076. else {
  4077. my $fn = File::Spec->catfile($dest, 'log.bucardo');
  4078. $fn .= ".$self->{logextension}" if length $self->{logextension};
  4079. ## If we are writing each process to a separate file,
  4080. ## append the prefix and the PID to the file name
  4081. $fn .= "$self->{logprefix}.$$" if $self->{logseparate};
  4082. open my $fh, '>>', $fn or die qq{Could not append to "$fn": $!\n};
  4083. ## Turn off buffering on this handle
  4084. $fh->autoflush(1);
  4085. $code_for{$dest} = sub { print {$fh} @_, $/ };
  4086. }
  4087. }
  4088. return @{ $self->{logcodes} = [ values %code_for ] };
  4089. }
  4090. sub glog { ## no critic (RequireArgUnpacking)
  4091. ## Reformat and log internal messages to the correct place
  4092. ## Arguments: two
  4093. ## 1. the log message
  4094. ## 2. the log level (defaults to 0)
  4095. ## Returns: undef
  4096. ## Quick shortcut if verbose is 'off' (which is not recommended!)
  4097. return if ! $_[0]->{verbose};
  4098. my $self = shift;
  4099. my $msg = shift;
  4100. ## Grab the log level: defaults to 0 (LOG_WARN)
  4101. my $loglevel = shift || 0;
  4102. ## Return and do nothing, if we have not met the minimum log level
  4103. return if $loglevel > $config{log_level_number};
  4104. ## Just return if there is no place to log to.
  4105. my @logs = $self->_logto;
  4106. return unless @logs || ($loglevel == LOG_WARN && $self->{warning_file});
  4107. ## Remove newline from the end of the message, in case it has one
  4108. chomp $msg;
  4109. ## We should always have a prefix, either BC!, MCP, CTL, KID, or VAC
  4110. ## Prepend it to our message
  4111. my $prefix = $self->{logprefix} || '???';
  4112. $msg = "$prefix $msg";
  4113. ## We may also show other optional things: log level, PID, timestamp, line we came from
  4114. ## Optionally show the current time in some form
  4115. my $showtime = '';
  4116. if ($config{log_showtime}) {
  4117. my ($sec,$msec) = gettimeofday;
  4118. $showtime =
  4119. 1 == $config{log_showtime} ? $sec
  4120. : 2 == $config{log_showtime} ? (scalar gmtime($sec))
  4121. : 3 == $config{log_showtime} ? (scalar localtime($sec))
  4122. : '';
  4123. if ($config{log_microsecond}) {
  4124. $showtime =~ s/(:\d\d) /"$1." . substr($msec,0,3) . ' '/oe;
  4125. }
  4126. }
  4127. ## Optionally show the PID (and set the time from above)
  4128. ## Show which line we came from as well
  4129. my $header = sprintf '%s%s%s',
  4130. ($config{log_showpid} ? "($$) " : ''),
  4131. ($showtime ? "[$showtime] " : ''),
  4132. $config{log_showline} ? (sprintf '#%04d ', (caller)[2]) : '';
  4133. ## Prepend the loglevel to the message
  4134. if ($config{log_showlevel}) {
  4135. $header = "$loglevel $header";
  4136. }
  4137. ## Warning messages may also get written to a separate file
  4138. ## Note that a 'warning message' is simply anything starting with "Warning"
  4139. if ($self->{warning_file} and $loglevel == LOG_WARN) {
  4140. my $file = $self->{warning_file};
  4141. open my $fh, , '>>', $file or die qq{Could not append to "$file": $!\n};
  4142. print {$fh} "$header$msg\n";
  4143. close $fh or warn qq{Could not close "$file": $!\n};
  4144. }
  4145. # Send it to all logs.
  4146. $_->($header, $msg) for @logs;
  4147. return;
  4148. } ## end of glog
  4149. sub conflict_log {
  4150. ## Log a message to the conflict log file at config{log_conflict_file}
  4151. ## Arguments: one
  4152. ## 1. the log message
  4153. ## Returns: undef
  4154. my $self = shift;
  4155. my $msg = shift;
  4156. chomp $msg;
  4157. my $cfile = $config{log_conflict_file};
  4158. my $clog;
  4159. if (! open $clog, '>>', $cfile) {
  4160. warn qq{Could not append to file "$cfile": $!};
  4161. return;
  4162. }
  4163. print {$clog} "$msg\n";
  4164. close $clog or warn qq{Could not close "$cfile": $!\n};
  4165. return;
  4166. } ## end of conflict_log
  4167. sub show_db_version_and_time {
  4168. ## Output the time, timezone, and version information to the log
  4169. ## Arguments: two
  4170. ## 1. Database handle
  4171. ## 2. A string indicating which database this is
  4172. ## Returns: undef
  4173. my ($self,$ldbh,$prefix) = @_;
  4174. return if ! defined $ldbh;
  4175. return if ref $ldbh ne 'DBI::db';
  4176. return if $ldbh->{Driver}{Name} ne 'Pg';
  4177. ## Get the databases epoch, timestamp, and timezone
  4178. $SQL = q{SELECT extract(epoch FROM now()), now(), current_setting('timezone')};
  4179. my $sth = $ldbh->prepare($SQL);
  4180. ## Get the system's time
  4181. my $systemtime = Time::HiRes::time();
  4182. ## Do the actual database call as close as possible to the system one
  4183. $sth->execute();
  4184. my $dbtime = $sth->fetchall_arrayref()->[0];
  4185. $self->glog("${prefix}Local epoch: $systemtime DB epoch: $dbtime->[0]", LOG_WARN);
  4186. $systemtime = scalar localtime ($systemtime);
  4187. $self->glog("${prefix}Local time: $systemtime DB time: $dbtime->[1]", LOG_WARN);
  4188. $systemtime = strftime('%Z (%z)', localtime());
  4189. $self->glog("${prefix}Local timezone: $systemtime DB timezone: $dbtime->[2]", LOG_WARN);
  4190. $self->glog("${prefix}Postgres version: " . $ldbh->{pg_server_version}, LOG_WARN);
  4191. $self->glog("${prefix}Database port: " . $ldbh->{pg_port}, LOG_WARN);
  4192. return;
  4193. } ## end of show_db_version_and_time
  4194. sub get_dbs {
  4195. ## Fetch a hashref of everything in the db table
  4196. ## Used by connect_database()
  4197. ## Calls commit on the masterdbh
  4198. ## Arguments: none
  4199. ## Returns: hashref
  4200. my $self = shift;
  4201. $SQL = 'SELECT * FROM bucardo.db';
  4202. $sth = $self->{masterdbh}->prepare($SQL);
  4203. $sth->execute();
  4204. my $info = $sth->fetchall_hashref('name');
  4205. $self->{masterdbh}->commit();
  4206. return $info;
  4207. } ## end of get_dbs
  4208. sub get_goats {
  4209. ## Fetch a hashref of everything in the goat table
  4210. ## Used by find_goats()
  4211. ## Calls commit on the masterdbh
  4212. ## Arguments: none
  4213. ## Returns: hashref
  4214. my $self = shift;
  4215. $SQL = 'SELECT * FROM bucardo.goat';
  4216. $sth = $self->{masterdbh}->prepare($SQL);
  4217. $sth->execute();
  4218. my $info = $sth->fetchall_hashref('id');
  4219. $self->{masterdbh}->commit();
  4220. return $info;
  4221. } ## end of get_goats
  4222. sub find_goats {
  4223. ## Given a herd, return an arrayref of goats
  4224. ## Used by validate_sync()
  4225. ## Calls commit on the masterdbh
  4226. ## Arguments: none
  4227. ## Returns: hashref
  4228. my ($self,$herd) = @_;
  4229. my $goats = $self->get_goats();
  4230. $SQL = q{
  4231. SELECT goat
  4232. FROM bucardo.herdmap
  4233. WHERE herd = ?
  4234. ORDER BY priority DESC, goat ASC
  4235. };
  4236. $sth = $self->{masterdbh}->prepare($SQL);
  4237. $sth->execute($herd);
  4238. my $newgoats = [];
  4239. for (@{$sth->fetchall_arrayref()}) {
  4240. push @$newgoats, $goats->{$_->[0]};
  4241. }
  4242. $self->{masterdbh}->commit();
  4243. return $newgoats;
  4244. } ## end of find_goats
  4245. sub get_syncs {
  4246. ## Fetch a hashref of everything in the sync table
  4247. ## Used by reload_mcp()
  4248. ## Calls commit on the masterdbh
  4249. ## Arguments: none
  4250. ## Returns: hashref
  4251. my $self = shift;
  4252. ## Grab all fields plus some computed ones from the sync table
  4253. $SQL = q{
  4254. SELECT *,
  4255. COALESCE(EXTRACT(epoch FROM checktime),0) AS checksecs,
  4256. COALESCE(EXTRACT(epoch FROM lifetime),0) AS lifetimesecs
  4257. FROM bucardo.sync
  4258. };
  4259. $sth = $self->{masterdbh}->prepare($SQL);
  4260. $sth->execute();
  4261. ## Turn it into a hash based on the sync name, then return the ref
  4262. my $info = $sth->fetchall_hashref('name');
  4263. $self->{masterdbh}->commit();
  4264. return $info;
  4265. } ## end of get_syncs
  4266. sub get_reason {
  4267. ## Returns the current string (if any) in the reason file
  4268. ## Arguments: one
  4269. ## 1. Optional boolean: if true, the reason file is removed
  4270. ## Returns: string
  4271. my $delete = shift || 0;
  4272. ## String to return
  4273. my $reason = '';
  4274. ## If we can't open the file, we simply return an empty string
  4275. if (open my $fh, '<', $config{reason_file}) {
  4276. ## Everything after the pipe is the reason. If no match, return empty string
  4277. if (<$fh> =~ /\|\s*(.+)/o) {
  4278. $reason = $1;
  4279. }
  4280. close $fh or warn qq{Could not close "$config{reason_file}": $!\n};
  4281. ## Optionally delete the file after we've opened and closed it
  4282. $delete and unlink $config{reason_file};
  4283. }
  4284. return $reason;
  4285. } ## end of get_reason
  4286. sub db_listen {
  4287. ## Listen for specific messages. Does not commit.
  4288. ## Arguments: two, three, or four
  4289. ## 1. Database handle
  4290. ## 2. String to listen for
  4291. ## 3. Short name of the database (optional, for debug output, default to 'bucardo')
  4292. ## 4. Whether to skip payloads. Optional boolean, defaults to false
  4293. ## Returns: undef
  4294. my $self = shift;
  4295. my $ldbh = shift;
  4296. my $string = shift;
  4297. my $name = shift || 'bucardo';
  4298. my $skip_payload = shift || 0;
  4299. if (! ref $ldbh) {
  4300. my $line = (caller)[2];
  4301. $self->glog("Call to db_listen from an invalid database handle for $name, line $line", LOG_WARN);
  4302. return;
  4303. }
  4304. ## If using payloads, we only need to listen for one thing
  4305. if ($ldbh->{pg_server_version} >= 90000 and ! $skip_payload) {
  4306. ## Do nothing if we are already listening
  4307. return if $self->{listen_payload}{$ldbh};
  4308. ## Mark this process as listening to this database.
  4309. ## Get implicitly reset post-fork as new database handles are created
  4310. $self->{listen_payload}{$ldbh} = 1;
  4311. ## We use 'bucardo', 'bucardo_ctl', or 'bucardo_kid'
  4312. my $suffix = $self->{logprefix} =~ /KID|CTL/ ? ('_' . lc $self->{logprefix}) : '';
  4313. $string = "bucardo$suffix";
  4314. }
  4315. elsif (exists $self->{listening}{$ldbh}{$string}) {
  4316. ## Using old-style direct names and already listening? Just return
  4317. return;
  4318. }
  4319. else {
  4320. ## Mark it as already done
  4321. $self->{listening}{$ldbh}{$string} = 1;
  4322. }
  4323. $string = "bucardo_$string" if index($string, 'bucardo');
  4324. ## If log level low enough, show which line this call came from
  4325. if ($config{log_level_number} <= LOG_DEBUG) {
  4326. my $line = (caller)[2];
  4327. $self->glog(qq{LISTEN for "$string" on "$name" (line $line)}, LOG_DEBUG);
  4328. }
  4329. $ldbh->do(qq{LISTEN "$string"})
  4330. or die qq{LISTEN "$string" failed!\n};
  4331. return;
  4332. } ## end of db_listen
  4333. sub db_unlisten {
  4334. ## Stop listening for specific messages
  4335. ## Arguments: four
  4336. ## 1. Database handle
  4337. ## 2. String to stop listening to
  4338. ## 3. Short name of the database (for debug output)
  4339. ## 4. Whether to skip payloads. Optional boolean, defaults to false
  4340. ## Returns: undef
  4341. my $self = shift;
  4342. my $ldbh = shift;
  4343. my $string = shift;
  4344. my $name = shift || 'bucardo';
  4345. my $skip_payload = shift || 0;
  4346. ## If we are 9.0 or greater, we never stop listening
  4347. if ($ldbh->{pg_server_version} >= 90000 and ! $skip_payload) {
  4348. return;
  4349. }
  4350. my $original_string = $string;
  4351. $string = "bucardo_$string";
  4352. ## If log level low enough, show which line this call came from
  4353. if ($config{log_level_number} <= LOG_DEBUG) {
  4354. my $line = (caller)[2];
  4355. $self->glog(qq{UNLISTEN for "$string" on "$name" (line $line)}, LOG_DEBUG);
  4356. }
  4357. ## We'll unlisten even if the hash indicates we are not
  4358. $ldbh->do(qq{UNLISTEN "$string"});
  4359. delete $self->{listening}{$ldbh}{$original_string};
  4360. return;
  4361. } ## end of db_unlisten
  4362. sub db_unlisten_all {
  4363. ## Stop listening to everything important
  4364. ## Arguments: one
  4365. ## 1. Database handle
  4366. ## Returns: undef
  4367. my $self = shift;
  4368. my $ldbh = shift;
  4369. ## If the log level is low enough, show the line that called this
  4370. if ($config{log_level_number} <= LOG_DEBUG) {
  4371. my $line = (caller)[2];
  4372. $self->glog(qq{UNLISTEN * (line $line)}, LOG_DEBUG);
  4373. }
  4374. ## Do the deed
  4375. $ldbh->do('UNLISTEN *');
  4376. delete $self->{listening}{$ldbh};
  4377. return;
  4378. } ## end of db_unlisten_all
  4379. sub db_notify {
  4380. ## Send an asynchronous notification into the DB aether, then commit
  4381. ## 1. Database handle
  4382. ## 2. The string to send
  4383. ## 3. Whether to skip payloads. Optional boolean, defaults to false
  4384. ## Returns: undef
  4385. my ($self, $ldbh, $string, $skip_payload) = @_;
  4386. ## We make some exceptions to the payload system, mostly for early MCP notices
  4387. ## This is because we don't want to complicate external clients with payload decisions
  4388. $skip_payload = 0 if ! defined $skip_payload;
  4389. ## XXX TODO: We should make this log level test more generic and apply it elsewhere
  4390. ## Basically, there is no reason to invoke caller() if we are not going to use it
  4391. if ($config{log_level_number} <= LOG_DEBUG) {
  4392. my $line = (caller)[2];
  4393. $self->glog(qq{Sending NOTIFY "$string" (line $line)}, LOG_DEBUG);
  4394. }
  4395. if ($ldbh->{pg_server_version} < 90000 or $skip_payload) {
  4396. ## Old-school notification system. Simply send the given string
  4397. ## ...but prepend a 'bucardo_' to it first
  4398. $string = "bucardo_$string";
  4399. $ldbh->do(qq{NOTIFY "$string"})
  4400. or $self->glog(qq{Warning: NOTIFY failed for "$string"}, LOG_DEBUG);
  4401. }
  4402. else {
  4403. ## New-style notification system. The string becomes the payload
  4404. ## The channel is always 'bucardo' based.
  4405. my $channel = 'bucardo';
  4406. ## Going to ctl?
  4407. $channel = 'bucardo_ctl' if $string =~ s/^ctl_//o;
  4408. ## Going to kid
  4409. $channel = 'bucardo_kid' if $string =~ s/^kid_//o;
  4410. $ldbh->do(qq{NOTIFY $channel, '$string'})
  4411. or $self->glog(qq{Warning: NOTIFY failed for bucardo, '$string'}, LOG_DEBUG);
  4412. }
  4413. $ldbh->commit();
  4414. return;
  4415. } ## end of db_notify
  4416. sub db_get_notices {
  4417. ## Gather up and return a list of asynchronous notices received since the last check
  4418. ## Arguments: one or two
  4419. ## 1. Database handle
  4420. ## 2. PID that can be ignored (optional)
  4421. ## Returns: hash of notices, with the key as the name and then another hash with:
  4422. ## count: total number received
  4423. ## firstpid: the first PID for this notice
  4424. ## pids: hashref of all pids
  4425. ## If using 9.0 or greater, the payload becomes the name
  4426. my ($self, $ldbh, $selfpid) = @_;
  4427. my ($n, %notice);
  4428. while ($n = $ldbh->func('pg_notifies')) {
  4429. my ($name, $pid, $payload) = @$n;
  4430. ## Ignore certain PIDs (e.g. from ourselves!)
  4431. next if defined $selfpid and $pid == $selfpid;
  4432. if ($ldbh->{pg_server_version} >= 90000 and $payload) {
  4433. $name = $payload; ## presto!
  4434. }
  4435. else {
  4436. $name =~ s/^bucardo_//o;
  4437. }
  4438. if (exists $notice{$name}) {
  4439. $notice{$name}{count}++;
  4440. $notice{$name}{pid}{$pid}++;
  4441. }
  4442. else {
  4443. $notice{$name}{count} = 1;
  4444. $notice{$name}{pid}{$pid} = 1;
  4445. $notice{$name}{firstpid} = $pid;
  4446. }
  4447. }
  4448. ## Return right now if we had no notices,
  4449. ## or if don't need lots of logging detail
  4450. if (! keys %notice or $config{log_level_number} > LOG_DEBUG) {
  4451. return \%notice;
  4452. }
  4453. ## TODO: Return if this was sent from us (usually PID+1)
  4454. ## Always want to write the actual line these came from
  4455. my $line = (caller)[2];
  4456. ## Walk the list and show each unique message received
  4457. for my $name (sort keys %notice) {
  4458. my $pid = $notice{$name}{firstpid};
  4459. my $prettypid = (exists $self->{pidmap}{$pid} ? "$pid ($self->{pidmap}{$pid})" : $pid);
  4460. my $extra = '';
  4461. my $pcount = keys %{ $notice{$name}{pid} };
  4462. $pcount--; ## Not the firstpid please
  4463. if ($pcount > 1) {
  4464. $extra = sprintf ' (and %d other %s)',
  4465. $pcount, 1 == $pcount ? 'PID' : 'PIDs';
  4466. }
  4467. my $times = '';
  4468. $count = $notice{$name}{count};
  4469. if ($count > 1) {
  4470. $times = " $count times";
  4471. }
  4472. my $msg = sprintf 'Got NOTICE %s%s from %s%s (line %d)',
  4473. $name, $times, $prettypid, $extra, $line;
  4474. $self->glog($msg, LOG_DEBUG);
  4475. }
  4476. return \%notice;
  4477. } ## end of db_get_notices
  4478. sub send_signal_to_PID {
  4479. ## Send a USR1 to one or more PIDs
  4480. ## Arguments: one
  4481. ## 1. Hashref of info, including:
  4482. ## sync => name of a sync to filter PID files with
  4483. ## Returns: number of signals sucessfully sent
  4484. my ($self, $arg) = @_;
  4485. my $total = 0;
  4486. ## Slurp in all the files from the PID directory
  4487. my $piddir = $config{piddir};
  4488. opendir my $dh, $piddir or die qq{Could not opendir "$piddir" $!\n};
  4489. my @pidfiles = grep { /^bucardo.*\.pid$/ } readdir $dh;
  4490. closedir $dh or warn qq{Could not closedir "$piddir": $!\n};
  4491. ## Send a signal to the ones we care about
  4492. for my $pidfile (sort @pidfiles) {
  4493. next if $arg->{sync} and $pidfile !~ /\bsync\.$arg->{sync}\b/;
  4494. my $pfile = File::Spec->catfile( $piddir => $pidfile );
  4495. if (open my $fh, '<', $pfile) {
  4496. my $pid = <$fh>;
  4497. close $fh or warn qq{Could not close "$pfile": $!\n};
  4498. if (! defined $pid or $pid !~ /^\d+$/) {
  4499. $self->glog("Warning: No PID found in file, so removing $pfile", LOG_TERSE);
  4500. unlink $pfile;
  4501. }
  4502. elsif ($pid == $$) {
  4503. }
  4504. else {
  4505. $total += kill $signumber{'USR1'} => $pid;
  4506. $self->glog("Sent USR1 signal to process $pid", LOG_VERBOSE);
  4507. }
  4508. }
  4509. else {
  4510. $self->glog("Warning: Could not open file, so removing $pfile", LOG_TERSE);
  4511. unlink $pfile;
  4512. }
  4513. }
  4514. return $total;
  4515. } ## end of send_signal_to_PID
  4516. sub validate_sync {
  4517. ## Check each database a sync needs to use, and validate all tables and columns
  4518. ## This also populates the all important $self->{sdb} hash
  4519. ## We use sdb to prevent later accidental mixing with $sync->{db}
  4520. ## Arguments: one
  4521. ## 1. Hashref of sync information
  4522. ## Returns: boolean success/failure
  4523. my ($self,$s) = @_;
  4524. my $syncname = $s->{name};
  4525. $self->glog(qq{Running validate_sync on "$s->{name}"}, LOG_NORMAL);
  4526. ## Populate $s->{db} with all databases in this sync
  4527. $SQL = 'SELECT db.*, m.role, m.priority, m.gang FROM dbmap m JOIN db ON (db.name = m.db) WHERE m.dbgroup = ?';
  4528. $sth = $self->{masterdbh}->prepare($SQL);
  4529. $count = $sth->execute($s->{dbs});
  4530. $s->{db} = $sth->fetchall_hashref('name');
  4531. ## Figure out what role each database will play in this sync
  4532. my %role = ( source => 0, target => 0, fullcopy => 0);
  4533. ## Establish a connection to each database used
  4534. ## We also populate the "source" database as the first source we come across
  4535. my ($sourcename,$srcdbh);
  4536. for my $dbname (sort keys %{ $s->{db} }) {
  4537. ## Helper var so we don't have to type this out all the time
  4538. my $d = $s->{db}{$dbname};
  4539. ## Check for inactive databases
  4540. if ($d->{status} ne 'active') {
  4541. ## Source databases are never allowed to be inactive
  4542. if ($d->{role} eq 'source') {
  4543. $self->glog("Source database $dbname is not active, cannot run this sync", LOG_WARN);
  4544. die "Source database $dbname is not active";
  4545. }
  4546. ## Warn about non-source ones, but allow the sync to proceed
  4547. $self->glog("Database $dbname is not active, so it will not be used", LOG_WARN);
  4548. ## No sense in connecting to it
  4549. next;
  4550. }
  4551. ## If we've not already populated sdb, do so now
  4552. if (! exists $self->{sdb}{$dbname}) {
  4553. $x = $self->{sdb}{$dbname} = $d;
  4554. my $role = $x->{role};
  4555. if ($x->{dbtype} =~ /flat/o) {
  4556. $self->glog(qq{Skipping flatfile database "$dbname"}, LOG_NORMAL);
  4557. next;
  4558. }
  4559. $self->glog(qq{Connecting to database "$dbname" ($role)}, LOG_TERSE);
  4560. ($x->{backend}, $x->{dbh}) = $self->connect_database($dbname);
  4561. if (defined $x->{backend}) {
  4562. $self->glog(qq{Database "$dbname" backend PID: $x->{backend}}, LOG_VERBOSE);
  4563. }
  4564. $self->show_db_version_and_time($x->{dbh}, qq{DB "$dbname" });
  4565. }
  4566. ## Help figure out source vs target later on
  4567. $role{$d->{role}}++;
  4568. ## We want to grab the first source we find and populate $sourcename and $srcdbh
  4569. if (! defined $sourcename and $s->{db}{$dbname}{role} eq 'source') {
  4570. $sourcename = $dbname;
  4571. $srcdbh = $self->{sdb}{$dbname}{dbh};
  4572. }
  4573. } ## end each database
  4574. ## If we have more than one source, then everyone is a target
  4575. ## Otherwise, only non-source databases are
  4576. for my $dbname (keys %{ $s->{db} }) {
  4577. $x = $s->{db}{$dbname};
  4578. $x->{istarget} =
  4579. ($x->{role} ne 'source' or $role{source} > 1) ? 1 : 0;
  4580. $x->{issource} = $x->{role} eq 'source' ? 1 : 0;
  4581. }
  4582. ## Grab the authoritative list of goats in this herd
  4583. $s->{goatlist} = $self->find_goats($s->{herd});
  4584. ## Call validate_sync: checks tables, columns, sets up supporting
  4585. ## schemas, tables, functions, and indexes as needed
  4586. $self->{masterdbh}->do("SELECT validate_sync('$syncname')");
  4587. ## Prepare some SQL statements for immediate and future use
  4588. my %SQL;
  4589. ## Given a schema and table name, return safely quoted names
  4590. $SQL{checktable} = q{
  4591. SELECT c.oid, quote_ident(n.nspname), quote_ident(c.relname), quote_literal(n.nspname), quote_literal(c.relname)
  4592. FROM pg_class c, pg_namespace n
  4593. WHERE c.relnamespace = n.oid
  4594. AND c.oid = ?::regclass
  4595. };
  4596. $sth{checktable} = $srcdbh->prepare($SQL{checktable});
  4597. ## Given a table, return detailed column information
  4598. $SQL{checkcols} = q{
  4599. SELECT attname, quote_ident(attname) AS qattname, atttypid, format_type(atttypid, atttypmod) AS ftype,
  4600. attnotnull, atthasdef, attnum,
  4601. (SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid=attrelid
  4602. AND adnum=attnum AND atthasdef) AS def
  4603. FROM pg_attribute
  4604. WHERE attrelid = ?::regclass AND attnum > 0 AND NOT attisdropped
  4605. ORDER BY attnum
  4606. };
  4607. $sth{checkcols} = $srcdbh->prepare($SQL{checkcols});
  4608. ## Reset custom code related counters for this sync
  4609. $s->{need_rows} = $s->{need_safe_dbh} = $s->{need_safe_dbh_strict} = 0;
  4610. ## Empty out any existing lists of code types
  4611. for my $key (grep { /^code_/ } sort keys %$s) {
  4612. $s->{$key} = [];
  4613. }
  4614. ## Validate all (active) custom codes for this sync
  4615. my $goatlistcodes = join ',' => map { $_->{id} } @{$s->{goatlist}};
  4616. my $goatclause = length $goatlistcodes ? "OR m.goat IN ($goatlistcodes)" : '';
  4617. $SQL = qq{
  4618. SELECT c.src_code, c.id, c.whenrun, c.getdbh, c.name, COALESCE(c.about,'?') AS about,
  4619. c.status, m.active, m.priority, COALESCE(m.goat,0) AS goat
  4620. FROM customcode c, customcode_map m
  4621. WHERE c.id=m.code AND m.active IS TRUE
  4622. AND (m.sync = ? $goatclause)
  4623. ORDER BY m.priority ASC
  4624. };
  4625. $sth = $self->{masterdbh}->prepare($SQL);
  4626. $sth->execute($syncname);
  4627. ## Loop through all customcodes for this sync
  4628. for my $c (@{$sth->fetchall_arrayref({})}) {
  4629. if ($c->{status} ne 'active') {
  4630. $self->glog(qq{ Skipping custom code $c->{id} ($c->{name}): not active }. LOG_NORMAL);
  4631. next;
  4632. }
  4633. $self->glog(qq{ Validating custom code $c->{id} ($c->{whenrun}) (goat=$c->{goat}): $c->{name}}, LOG_WARN);
  4634. ## Carefully compile the code and catch complications
  4635. TRY: {
  4636. local $@;
  4637. local $_;
  4638. $c->{coderef} = eval qq{
  4639. package Bucardo::CustomCode;
  4640. sub { $c->{src_code} }
  4641. }; ## no critic (ProhibitStringyEval)
  4642. if ($@) {
  4643. $self->glog(qq{Warning! Custom code $c->{id} ($c->{name}) for sync "$syncname" did not compile: $@}, LOG_WARN);
  4644. return 0;
  4645. };
  4646. }
  4647. ## If this code is run at the goat level, push it to each goat's list of code
  4648. if ($c->{goat}) {
  4649. my ($goat) = grep { $_->{id}==$c->{goat} } @{$s->{goatlist}};
  4650. push @{$goat->{"code_$c->{whenrun}"}}, $c;
  4651. if ($c->{whenrun} eq 'exception') {
  4652. $goat->{has_exception_code}++;
  4653. }
  4654. }
  4655. else {
  4656. push @{$s->{"code_$c->{whenrun}"}}, $c;
  4657. ## Every goat gets this code
  4658. for my $g ( @{$s->{goatlist}} ) {
  4659. push @{$g->{"code_$c->{whenrun}"}}, $c;
  4660. $g->{has_exception_code}++ if $c->{whenrun} eq 'exception';
  4661. }
  4662. }
  4663. ## Some custom code needs database handles - if so, gets one of two types
  4664. if ($c->{getdbh}) {
  4665. if ($c->{whenrun} eq 'before_txn'
  4666. or $c->{whenrun} eq 'after_txn'
  4667. or $c->{whenrun} eq 'before_sync'
  4668. or $c->{whenrun} eq 'after_sync') {
  4669. $s->{need_safe_dbh} = 1;
  4670. }
  4671. else {
  4672. $s->{need_safe_dbh_strict} = 1;
  4673. }
  4674. }
  4675. } ## end checking each custom code
  4676. ## Go through each goat in this sync, adjusting items and possibly bubbling up info to sync
  4677. for my $g (@{$s->{goatlist}}) {
  4678. ## None of this applies to non-tables
  4679. next if $g->{reltype} ne 'table';
  4680. ## If we didn't find exception custom code above, set it to 0 for this goat
  4681. $g->{has_exception_code} ||= 0;
  4682. ## If goat.rebuild_index is null, use the sync's value
  4683. if (!defined $g->{rebuild_index}) {
  4684. $g->{rebuild_index} = $s->{rebuild_index};
  4685. }
  4686. } ## end each goat
  4687. ## There are things that a fullcopy sync does not do
  4688. if ($s->{fullcopy}) {
  4689. $s->{track_rates} = 0;
  4690. }
  4691. ## Build our customname hash for use below when checking remote database tables
  4692. my %customname;
  4693. $SQL = q{SELECT goat,newname,db,COALESCE(db,'') AS db, COALESCE(sync,'') AS sync FROM bucardo.customname};
  4694. my $maindbh = $self->{masterdbh};
  4695. $sth = $maindbh->prepare($SQL);
  4696. $sth->execute();
  4697. for my $row (@{$sth->fetchall_arrayref({})}) {
  4698. ## Ignore if this is for some other sync
  4699. next if length $row->{sync} and $row->{sync} ne $syncname;
  4700. $customname{$row->{goat}}{$row->{db}} = $row->{newname};
  4701. }
  4702. ## Go through each table and make sure it exists and matches everywhere
  4703. for my $g (@{$s->{goatlist}}) {
  4704. ## TODO: refactor with work in validate_sync()
  4705. $self->glog(qq{ Inspecting source $g->{reltype} "$g->{schemaname}.$g->{tablename}" on database "$sourcename"}, LOG_NORMAL);
  4706. ## Check the source table, save escaped versions of the names
  4707. $sth = $sth{checktable};
  4708. $count = $sth->execute(qq{"$g->{schemaname}"."$g->{tablename}"});
  4709. if ($count != 1) {
  4710. $sth->finish();
  4711. my $msg = qq{Could not find $g->{reltype} "$g->{schemaname}"."$g->{tablename}"\n};
  4712. $self->glog($msg, LOG_WARN);
  4713. warn $msg;
  4714. return 0;
  4715. }
  4716. ## Store oid and quoted names for this relation
  4717. ($g->{oid},$g->{safeschema},$g->{safetable},$g->{safeschemaliteral},$g->{safetableliteral})
  4718. = @{$sth->fetchall_arrayref()->[0]};
  4719. my ($S,$T) = ($g->{safeschema},$g->{safetable});
  4720. ## Plunk the oid into a hash for easy lookup below when saving FK information
  4721. $s->{tableoid}{$g->{oid}}{name} = "$S.$T";
  4722. ## Determine the conflict method for each goat
  4723. ## Use the syncs if it has one, otherwise the default
  4724. $g->{conflict_strategy} = $s->{conflict_strategy} || $config{default_conflict_strategy};
  4725. ## We do this even if g->{code_conflict} exists so it can fall through
  4726. my $colinfo;
  4727. if ($g->{reltype} eq 'table') {
  4728. ## Save information about each column in the primary key
  4729. if (!defined $g->{pkey} or !defined $g->{qpkey}) {
  4730. die "Table $g->{safetable} has no pkey or qpkey - do you need to run validate_goat() on it?\n";
  4731. }
  4732. ## Much of this is used later on, for speed of performing the sync
  4733. $g->{pkey} = [split /\|/o => $g->{pkey}];
  4734. $g->{qpkey} = [split /\|/o => $g->{qpkey}];
  4735. $g->{pkeytype} = [split /\|/o => $g->{pkeytype}];
  4736. $g->{numpkcols} = @{$g->{pkey}};
  4737. $g->{hasbinarypk} = 0; ## Not used anywhere?
  4738. $x=0;
  4739. for (@{$g->{pkey}}) {
  4740. $g->{binarypkey}{$x++} = 0;
  4741. }
  4742. ## All pks together for the main delta query
  4743. ## We change bytea to base64 so we don't have to declare binary args anywhere
  4744. $g->{pklist} = '';
  4745. for ($x = 0; defined $g->{pkey}[$x]; $x++) {
  4746. $g->{pklist} .= sprintf '%s,',
  4747. $g->{pkeytype}[$x] eq 'bytea'
  4748. ? qq{ENCODE("$g->{pkey}[$x]", 'base64')}
  4749. : qq{"$g->{pkey}[$x]"};
  4750. }
  4751. ## Remove the final comma:
  4752. chop $g->{pklist};
  4753. ## The name of the delta and track tables for this table
  4754. $SQL = 'SELECT bucardo.bucardo_tablename_maker(?)';
  4755. $sth = $self->{masterdbh}->prepare($SQL);
  4756. $sth->execute($S.'_'.$T);
  4757. $g->{makername} = $sth->fetchall_arrayref()->[0][0];
  4758. if ($g->{makername} =~ s/"//g) {
  4759. $g->{deltatable} = qq{"delta_$g->{makername}"};
  4760. $g->{tracktable} = qq{"track_$g->{makername}"};
  4761. $g->{stagetable} = qq{"stage_$g->{makername}"};
  4762. }
  4763. else {
  4764. $g->{deltatable} = "delta_$g->{makername}";
  4765. $g->{tracktable} = "track_$g->{makername}";
  4766. $g->{stagetable} = "stage_$g->{makername}";
  4767. }
  4768. ## Turn off the search path, to help the checks below match up
  4769. $srcdbh->do('SET LOCAL search_path = pg_catalog');
  4770. ## Check the source columns, and save them
  4771. $sth = $sth{checkcols};
  4772. $sth->execute(qq{"$g->{schemaname}"."$g->{tablename}"});
  4773. $colinfo = $sth->fetchall_hashref('attname');
  4774. ## Allow for 'dead' columns in the attnum ordering
  4775. $x=1;
  4776. for (sort { $colinfo->{$a}{attnum} <=> $colinfo->{$b}{attnum} } keys %$colinfo) {
  4777. $colinfo->{$_}{realattnum} = $x++;
  4778. }
  4779. $g->{columnhash} = $colinfo;
  4780. ## Build lists of columns
  4781. $x = 1;
  4782. $g->{cols} = [];
  4783. $g->{safecols} = [];
  4784. COL: for my $colname (sort { $colinfo->{$a}{attnum} <=> $colinfo->{$b}{attnum} } keys %$colinfo) {
  4785. ## Skip if this column is part of the primary key
  4786. for my $pk (@{$g->{pkey}}) {
  4787. next COL if $pk eq $colname;
  4788. }
  4789. push @{$g->{cols}}, $colname;
  4790. push @{$g->{safecols}}, $colinfo->{$colname}{qattname};
  4791. $colinfo->{$colname}{order} = $x++;
  4792. }
  4793. ## Stringified versions of the above lists, for ease later on
  4794. $g->{columnlist} = join ',' => @{$g->{cols}};
  4795. $g->{safecolumnlist} = join ',' => @{$g->{safecols}};
  4796. ## Note which columns are bytea
  4797. BCOL: for my $colname (keys %$colinfo) {
  4798. my $c = $colinfo->{$colname};
  4799. next if $c->{atttypid} != 17; ## Yes, it's hardcoded, no sweat
  4800. $x = 0;
  4801. for my $pk (@{$g->{pkey}}) {
  4802. if ($colname eq $pk) {
  4803. $g->{binarypkey}{$x} = 1;
  4804. $g->{hasbinarypk} = 1;
  4805. next BCOL;
  4806. }
  4807. $x++;
  4808. }
  4809. ## This is used to bind_param these as binary during inserts and updates
  4810. push @{$g->{binarycols}}, $colinfo->{$colname}{order};
  4811. }
  4812. $srcdbh->do('RESET search_path');
  4813. } ## end if reltype is table
  4814. my $sourceseq = 1;
  4815. #$g->{reltype} eq 'sequence'
  4816. # ? $self->get_sequence_info($srcdbh, $S, $T)
  4817. # : {};
  4818. next if $g->{reltype} ne 'table';
  4819. ## Verify sequences or tables+columns on remote databases
  4820. for my $dbname (sort keys %{ $self->{sdb} }) {
  4821. ## Only ones for this sync, please
  4822. next if ! exists $s->{db}{$dbname};
  4823. $x = $self->{sdb}{$dbname};
  4824. next if $x->{role} eq 'source';
  4825. ## Flat files are obviously skipped as we create them de novo
  4826. next if $x->{dbtype} =~ /flat/o;
  4827. ## Mongo is skipped because it can create schemas on the fly
  4828. next if $x->{dbtype} =~ /mongo/o;
  4829. ## Redis is skipped because we can create keys on the fly
  4830. next if $x->{dbtype} =~ /redis/o;
  4831. ## MySQL/MariaDB/Drizzle/Oracle/SQLite is skipped for now, but should be added later
  4832. next if $x->{dbtype} =~ /mysql|mariadb|drizzle|oracle|sqlite/o;
  4833. ## Respond to ping here and now for very impatient watchdog programs
  4834. $maindbh->commit();
  4835. my $nlist = $self->db_get_notices($maindbh);
  4836. for my $name (keys %{ $nlist }) {
  4837. my $npid = $nlist->{$name}{firstpid};
  4838. if ($name eq 'mcp_fullstop') {
  4839. $self->glog("Received full stop notice from PID $npid, leaving", LOG_WARN);
  4840. $self->cleanup_mcp("Received stop NOTICE from PID $npid");
  4841. exit 0;
  4842. }
  4843. if ($name eq 'mcp_ping') {
  4844. $self->glog("Got a ping from PID $npid, issuing pong", LOG_DEBUG);
  4845. $self->db_notify($maindbh, 'mcp_pong');
  4846. }
  4847. }
  4848. ## Get a handle for the remote database
  4849. my $dbh = $x->{dbh};
  4850. ## If a sequence, verify the information and move on
  4851. if ($g->{reltype} eq 'sequenceSKIP') {
  4852. my $targetseq = $self->get_sequence_info($dbh, $S, $T);
  4853. for my $key (sort keys %$targetseq) {
  4854. if (! exists $sourceseq->{$key}) {
  4855. $self->glog(qq{Warning! Sequence on target has item $key, but source does not!}, LOG_WARN);
  4856. next;
  4857. }
  4858. if ($targetseq->{$key} ne $sourceseq->{$key}) {
  4859. $self->glog("Warning! Sequence mismatch. Source $key=$sourceseq->{$key}, target is $targetseq->{$key}", LOG_WARN);
  4860. next;
  4861. }
  4862. }
  4863. next;
  4864. } ## end if sequence
  4865. ## Turn off the search path, to help the checks below match up
  4866. $dbh->do('SET LOCAL search_path = pg_catalog');
  4867. ## Grab column information about this table
  4868. $sth = $dbh->prepare($SQL{checkcols});
  4869. ## Change to the customname if needed
  4870. my ($RS,$RT) = ($S,$T);
  4871. ## We don't need to check if this is a source: this is already targets only
  4872. my $using_customname = 0;
  4873. if (exists $customname{$g->{id}}) {
  4874. ## If there is an entry for this particular database, use that
  4875. ## Otherwise, use the default one
  4876. if (exists $customname{$g->{id}}{$dbname} or exists $customname{$g->{id}}{''}) {
  4877. $RT = $customname{$g->{id}}{$dbname} || $customname{$g->{id}}{''};
  4878. $using_customname = 1;
  4879. ## If this has a dot, change the schema as well
  4880. ## Otherwise, we simply use the existing schema
  4881. if ($RT =~ s/(.+)\.//) {
  4882. $RS = $1;
  4883. }
  4884. }
  4885. }
  4886. $self->glog(qq{ Inspecting target $g->{reltype} "$RS.$RT" on database "$dbname"}, LOG_NORMAL);
  4887. $sth->execute("$RS.$RT");
  4888. my $targetcolinfo = $sth->fetchall_hashref('attname');
  4889. ## Allow for 'dead' columns in the attnum ordering
  4890. $x=1;
  4891. for (sort { $targetcolinfo->{$a}{attnum} <=> $targetcolinfo->{$b}{attnum} } keys %$targetcolinfo) {
  4892. $targetcolinfo->{$_}{realattnum} = $x++;
  4893. }
  4894. $dbh->do('RESET search_path');
  4895. $dbh->rollback();
  4896. my $t = "$g->{schemaname}.$g->{tablename}";
  4897. ## We'll state no problems until we are proved wrong
  4898. my $column_problems = 0;
  4899. ## Check each column in alphabetic order
  4900. for my $colname (sort keys %$colinfo) {
  4901. ## Simple var mapping to make the following code sane
  4902. my $fcol = $targetcolinfo->{$colname};
  4903. my $scol = $colinfo->{$colname};
  4904. $self->glog(qq{ Column on target database "$dbname": "$colname" ($scol->{ftype})}, LOG_DEBUG);
  4905. ## Always fatal: column on source but not target
  4906. if (! exists $targetcolinfo->{$colname}) {
  4907. $column_problems = 2;
  4908. my $msg = qq{Source database for sync "$syncname" has column "$colname" of table "$t", but target database "$dbname" does not};
  4909. $self->glog("Warning: $msg", LOG_WARN);
  4910. warn $msg;
  4911. next;
  4912. }
  4913. ## Almost always fatal: types do not match up
  4914. if ($scol->{ftype} ne $fcol->{ftype}) {
  4915. ## Carve out some known exceptions (but still warn about them)
  4916. ## Allowed: varchar == text
  4917. ## Allowed: timestamp* == timestamp*
  4918. if (
  4919. ($scol->{ftype} eq 'character varying' and $fcol->{ftype} eq 'text')
  4920. or
  4921. ($scol->{ftype} eq 'text' and $fcol->{ftype} eq 'character varying')
  4922. or
  4923. ($scol->{ftype} =~ /^timestamp/ and $fcol->{ftype} =~ /^timestamp/)
  4924. ) {
  4925. my $msg = qq{Source database for sync "$syncname" has column "$colname" of table "$t" as type "$scol->{ftype}", but target database "$dbname" has a type of "$fcol->{ftype}". You should really fix that.};
  4926. $self->glog("Warning: $msg", LOG_WARN);
  4927. }
  4928. else {
  4929. $column_problems = 2;
  4930. my $msg = qq{Source database for sync "$syncname" has column "$colname" of table "$t" as type "$scol->{ftype}", but target database "$dbname" has a type of "$fcol->{ftype}"};
  4931. $self->glog("Warning: $msg", LOG_WARN);
  4932. next;
  4933. }
  4934. }
  4935. ## Fatal in strict mode: NOT NULL mismatch
  4936. if ($scol->{attnotnull} != $fcol->{attnotnull}) {
  4937. $column_problems ||= 1; ## Don't want to override a setting of "2"
  4938. my $msg = sprintf q{Source database for sync "%s" has column "%s" of table "%s" set as %s, but target database "%s" has column set as %s},
  4939. $syncname,
  4940. $colname,
  4941. $t,
  4942. $scol->{attnotnull} ? 'NOT NULL' : 'NULL',
  4943. $dbname,
  4944. $scol->{attnotnull} ? 'NULL' : 'NOT NULL';
  4945. $self->glog("Warning: $msg", LOG_WARN);
  4946. warn $msg;
  4947. }
  4948. ## Fatal in strict mode: DEFAULT existence mismatch
  4949. if ($scol->{atthasdef} != $fcol->{atthasdef}) {
  4950. $column_problems ||= 1; ## Don't want to override a setting of "2"
  4951. my $msg = sprintf q{Source database for sync "%s" has column "%s" of table "%s" %s, but target database "%s" %s},
  4952. $syncname,
  4953. $colname,
  4954. $t,
  4955. $scol->{atthasdef} ? 'with a DEFAULT value' : 'has no DEFAULT value',
  4956. $dbname,
  4957. $scol->{atthasdef} ? 'has none' : 'does';
  4958. $self->glog("Warning: $msg", LOG_WARN);
  4959. warn $msg;
  4960. }
  4961. ## Fatal in strict mode: DEFAULT exists but does not match
  4962. if ($scol->{atthasdef} and $fcol->{atthasdef} and $scol->{def} ne $fcol->{def}) {
  4963. ## Make an exception for Postgres versions returning DEFAULT parenthesized or not
  4964. ## e.g. as "-5" in 8.2 or as "(-5)" in 8.3
  4965. my $scol_def = $scol->{def};
  4966. my $fcol_def = $fcol->{def};
  4967. for ($scol_def, $fcol_def) {
  4968. s/\A\(//;
  4969. s/\)\z//;
  4970. s/\)::/::/;
  4971. }
  4972. my $msg;
  4973. if ($scol_def eq $fcol_def) {
  4974. $msg = q{Postgres version mismatch leads to this difference, which is being tolerated: };
  4975. }
  4976. else {
  4977. $column_problems ||= 1; ## Don't want to override a setting of "2"
  4978. $msg = '';
  4979. }
  4980. $msg .= qq{Source database for sync "$syncname" has column "$colname" of table "$t" with a DEFAULT of "$scol->{def}", but target database "$dbname" has a DEFAULT of "$fcol->{def}"};
  4981. $self->glog("Warning: $msg", LOG_WARN);
  4982. warn $msg;
  4983. }
  4984. ## Fatal in strict mode: order of columns does not match up
  4985. if ($scol->{realattnum} != $fcol->{realattnum}) {
  4986. $column_problems ||= 1; ## Don't want to override a setting of "2"
  4987. my $msg = qq{Source database for sync "$syncname" has column "$colname" of table "$t" at position $scol->{realattnum} ($scol->{attnum}), but target database "$dbname" has it in position $fcol->{realattnum} ($fcol->{attnum})};
  4988. $self->glog("Warning: $msg", LOG_WARN);
  4989. warn $msg;
  4990. }
  4991. } ## end each column to be checked
  4992. ## Fatal in strict mode: extra columns on the target side
  4993. for my $colname (sort keys %$targetcolinfo) {
  4994. next if exists $colinfo->{$colname};
  4995. $column_problems ||= 1; ## Don't want to override a setting of "2"
  4996. my $msg = qq{Target database has column "$colname" on table "$t", but source database does not};
  4997. $self->glog("Warning: $msg", LOG_WARN);
  4998. warn $msg;
  4999. }
  5000. ## Real serious problems always bail out
  5001. return 0 if $column_problems >= 2;
  5002. ## If this is a minor problem, and we are using a customname,
  5003. ## allow it to pass
  5004. $column_problems = 0 if $using_customname;
  5005. ## If other problems, only bail if strict checking is on both sync and goat
  5006. ## This allows us to make a sync strict, but carve out exceptions for goats
  5007. return 0 if $column_problems and $s->{strict_checking} and $g->{strict_checking};
  5008. } ## end each target database
  5009. } ## end each goat
  5010. ## Generate mapping of foreign keys
  5011. ## This helps us with conflict resolution later on
  5012. my $oidlist = join ',' => map { $_->{oid} } @{ $s->{goatlist} };
  5013. if ($oidlist) {
  5014. $SQL = qq{SELECT conname,
  5015. conrelid, conrelid::regclass,
  5016. confrelid, confrelid::regclass,
  5017. array_agg(a.attname), array_agg(z.attname)
  5018. FROM pg_constraint c
  5019. JOIN pg_attribute a ON (a.attrelid = conrelid AND a.attnum = ANY(conkey))
  5020. JOIN pg_attribute z ON (z.attrelid = confrelid AND z.attnum = ANY (confkey))
  5021. WHERE contype = 'f'
  5022. AND (conrelid IN ($oidlist) OR confrelid IN ($oidlist))
  5023. GROUP BY 1,2,3,4,5
  5024. };
  5025. ## We turn off search_path to get fully-qualified relation names
  5026. $srcdbh->do('SET LOCAL search_path = pg_catalog');
  5027. for my $row (@{ $srcdbh->selectall_arrayref($SQL) }) {
  5028. my ($conname, $oid1,$t1, $oid2,$t2, $c1,$c2) = @$row;
  5029. ## The referenced table is not being tracked in this sync
  5030. if (! exists $s->{tableoid}{$oid2}) {
  5031. ## Nothing to do except report this problem and move on
  5032. $self->glog("Table $t1 references $t2, which is not part of this sync!", LOG_NORMAL);
  5033. next;
  5034. }
  5035. ## A table referencing us is not being tracked in this sync
  5036. if (! exists $s->{tableoid}{$oid1}) {
  5037. ## Nothing to do except report this problem and move on
  5038. $self->glog("Table $t2 is referenced by $t1, which is not part of this sync!", LOG_NORMAL);
  5039. next;
  5040. }
  5041. ## Both exist, so tie them together
  5042. $s->{tableoid}{$oid1}{references}{$oid2} = [$conname,$c1,$c2];
  5043. $s->{tableoid}{$oid2}{referencedby}{$oid1} = [$conname,$c1,$c2];
  5044. }
  5045. $srcdbh->do('RESET search_path');
  5046. $srcdbh->commit();
  5047. }
  5048. ## If autokick, listen for a triggerkick on all source databases
  5049. if ($s->{autokick}) {
  5050. my $l = "kick_sync_$syncname";
  5051. for my $dbname (sort keys %{ $self->{sdb} }) {
  5052. $x = $self->{sdb}{$dbname};
  5053. next if $x->{role} ne 'source';
  5054. $self->db_listen($x->{dbh}, $l, $dbname, 0);
  5055. $x->{dbh}->commit();
  5056. }
  5057. }
  5058. ## Success!
  5059. return 1;
  5060. } ## end of validate_sync
  5061. sub activate_sync {
  5062. ## We've got a new sync to be activated (but not started)
  5063. ## Arguments: one
  5064. ## 1. Hashref of sync information
  5065. ## Returns: boolean success/failure
  5066. my ($self,$s) = @_;
  5067. my $maindbh = $self->{masterdbh};
  5068. my $syncname = $s->{name};
  5069. ## Connect to each database used by this sync and validate tables
  5070. if (! $self->validate_sync($s)) {
  5071. $self->glog("Validation of sync $s->{name} FAILED", LOG_WARN);
  5072. $s->{mcp_active} = 0;
  5073. return 0;
  5074. }
  5075. ## If the kids stay alive, the controller must too
  5076. if ($s->{kidsalive} and !$s->{stayalive}) {
  5077. $s->{stayalive} = 1;
  5078. $self->glog('Warning! Setting stayalive to true because kidsalive is true', LOG_WARN);
  5079. }
  5080. ## Mark this sync as active: used in sync kicks/reloads later on
  5081. $self->{sync}{$syncname}{mcp_active} = 1;
  5082. ## Let any listeners know we are done
  5083. $self->db_notify($maindbh, "activated_sync_$syncname", 1);
  5084. ## We don't need to listen for activation requests anymore
  5085. $self->db_unlisten($maindbh, "activate_sync_$syncname", '', 1);
  5086. ## But we do need to listen for deactivate and kick requests
  5087. $self->db_listen($maindbh, "deactivate_sync_$syncname", '', 1);
  5088. $self->db_listen($maindbh, "kick_sync_$syncname", '', 1);
  5089. $maindbh->commit();
  5090. ## Redo our process name to include an updated list of active syncs
  5091. my @activesyncs;
  5092. for my $syncname (sort keys %{ $self->{sync} }) {
  5093. next if ! $self->{sync}{$syncname}{mcp_active};
  5094. push @activesyncs, $syncname;
  5095. }
  5096. ## Change our process name to show all active syncs
  5097. $0 = "Bucardo Master Control Program v$VERSION.$self->{extraname} Active syncs: ";
  5098. $0 .= join ',' => @activesyncs;
  5099. return 1;
  5100. } ## end of activate_sync
  5101. sub deactivate_sync {
  5102. ## We need to turn off a running sync
  5103. ## Arguments: one
  5104. ## 1. Hashref of sync information
  5105. ## Returns: boolean success/failure
  5106. my ($self,$s) = @_;
  5107. my $maindbh = $self->{masterdbh};
  5108. my $syncname = $s->{name};
  5109. ## Kill the controller
  5110. my $ctl = $s->{controller};
  5111. if (!$ctl) {
  5112. $self->glog('Warning! Controller not found', LOG_WARN);
  5113. }
  5114. else {
  5115. $count = kill $signumber{USR1} => $ctl;
  5116. $self->glog("Sent kill USR1 to CTL process $ctl. Result: $count", LOG_NORMAL);
  5117. }
  5118. $s->{controller} = 0;
  5119. $self->{sync}{$syncname}{mcp_active} = 0;
  5120. ## Let any listeners know we are done
  5121. $self->db_notify($maindbh, "deactivated_sync_$syncname");
  5122. ## We don't need to listen for deactivation or kick requests
  5123. $self->db_unlisten($maindbh, "deactivate_sync_$syncname", '', 1);
  5124. $self->db_unlisten($maindbh, "kick_sync_$syncname", '', 1);
  5125. ## But we do need to listen for an activation request
  5126. $self->db_listen($maindbh, "activate_sync_$syncname", '', 1);
  5127. $maindbh->commit();
  5128. ## If we are listening for kicks on the source, stop doing so
  5129. for my $dbname (sort keys %{ $self->{sdb} }) {
  5130. $x = $self->{sdb}{$dbname};
  5131. next if $x->{dbtype} ne 'postgres';
  5132. next if $x->{role} ne 'source';
  5133. $x->{dbh} ||= $self->connect_database($dbname);
  5134. $x->{dbh}->commit();
  5135. if ($s->{autokick}) {
  5136. my $l = "kick_sync_$syncname";
  5137. $self->db_unlisten($x->{dbh}, $l, $dbname, 0);
  5138. $x->{dbh}->commit();
  5139. }
  5140. }
  5141. ## Redo our process name to include an updated list of active syncs
  5142. my @activesyncs;
  5143. for my $syncname (keys %{ $self->{sync} }) {
  5144. push @activesyncs, $syncname;
  5145. }
  5146. $0 = "Bucardo Master Control Program v$VERSION.$self->{extraname} Active syncs: ";
  5147. $0 .= join ',' => @activesyncs;
  5148. return 1;
  5149. } ## end of deactivate_sync
  5150. sub fork_controller {
  5151. ## Fork off a controller process
  5152. ## Arguments: two
  5153. ## 1. Hashref of sync information
  5154. ## 2. The name of the sync
  5155. ## Returns: undef
  5156. my ($self, $s, $syncname) = @_;
  5157. my $newpid = $self->fork_and_inactivate('CTL');
  5158. if ($newpid) { ## We are the parent
  5159. $self->glog(qq{Created controller $newpid for sync "$syncname". Kick is $s->{kick_on_startup}}, LOG_NORMAL);
  5160. $s->{controller} = $newpid;
  5161. $self->{pidmap}{$newpid} = 'CTL';
  5162. ## Reset counters for ctl restart via maxkicks and lifetime settings
  5163. $s->{ctl_kick_counts} = 0;
  5164. $s->{start_time} = time();
  5165. return;
  5166. }
  5167. ## We are the kid, aka the new CTL process
  5168. ## Sleep a hair so the MCP can finish the items above first
  5169. sleep 0.05;
  5170. ## No need to keep information about other syncs around
  5171. $self->{sync} = $s;
  5172. $self->start_controller($s);
  5173. exit 0;
  5174. } ## end of fork_controller
  5175. sub fork_and_inactivate {
  5176. ## Call fork, and immediately inactivate open database handles
  5177. ## Arguments: one
  5178. ## 1. Type of thing we are forking (VAC, CTL, KID)
  5179. ## Returns: nothing
  5180. my $self = shift;
  5181. my $type = shift || '???';
  5182. my $newpid = fork;
  5183. if (!defined $newpid) {
  5184. die qq{Warning: Fork for $type failed!\n};
  5185. }
  5186. if ($newpid) { ## Parent
  5187. ## Very slight sleep to increase the chance of something happening to the kid
  5188. ## before InactiveDestroy is set
  5189. sleep 0.1;
  5190. }
  5191. else { ## Kid
  5192. ## Walk through the list of all known DBI databases
  5193. ## Inactivate each one, then undef it
  5194. ## It is probably still referenced elsewhere, so handle that - how?
  5195. for my $iname (keys %{ $self->{dbhlist} }) {
  5196. my $ldbh = $self->{dbhlist}{$iname};
  5197. $self->glog("Inactivating dbh $iname post-fork", LOG_DEBUG);
  5198. $ldbh->{InactiveDestroy} = 1;
  5199. delete $self->{dbhlist}{$iname};
  5200. }
  5201. ## Now go through common shared database handle locations, and delete them
  5202. delete $self->{masterdbh};
  5203. ## Clear the 'sdb' structure of any existing database handles
  5204. if (exists $self->{sdb}) {
  5205. for my $dbname (keys %{ $self->{sdb} }) {
  5206. for my $item (qw/ dbh backend kicked /) {
  5207. delete $self->{sdb}{$dbname}{$item};
  5208. }
  5209. }
  5210. }
  5211. ## Clear any sync-specific database handles
  5212. if (exists $self->{sync}) {
  5213. if (exists $self->{sync}{name}) { ## This is a controller/kid with a single sync
  5214. for my $dbname (sort keys %{ $self->{sync}{db} }) {
  5215. $self->glog("Removing reference to database $dbname", LOG_DEBUG);
  5216. for my $item (qw/ dbh backend kicked /) {
  5217. delete $self->{sync}{db}{$dbname}{$item};
  5218. }
  5219. }
  5220. }
  5221. else {
  5222. for my $syncname (keys %{ $self->{sync} }) {
  5223. for my $dbname (sort keys %{ $self->{sync}{$syncname}{db} }) {
  5224. $self->glog("Removing reference to database $dbname in sync $syncname", LOG_DEBUG);
  5225. for my $item (qw/ dbh backend kicked /) {
  5226. delete $self->{sync}{$syncname}{db}{$dbname}{$item};
  5227. }
  5228. }
  5229. }
  5230. }
  5231. }
  5232. }
  5233. return $newpid;
  5234. } ## end of fork_and_inactivate
  5235. sub fork_vac {
  5236. ## Fork off a VAC process
  5237. ## Arguments: none
  5238. ## Returns: undef
  5239. my $self = shift;
  5240. ## Fork it off
  5241. my $newpid = $self->fork_and_inactivate('VAC');
  5242. ## Parent MCP just makes a note in the logs and returns
  5243. if ($newpid) { ## We are the parent
  5244. $self->glog(qq{Created VAC $newpid}, LOG_NORMAL);
  5245. $self->{vacpid} = $newpid;
  5246. return;
  5247. }
  5248. ## Prefix all log lines with this TLA (was MCP)
  5249. $self->{logprefix} = 'VAC';
  5250. ## Set our process name
  5251. $0 = qq{Bucardo VAC.$self->{extraname}};
  5252. ## Store our PID into a file
  5253. ## Save the complete returned name for later cleanup
  5254. $self->{vacpidfile} = $self->store_pid( 'bucardo.vac.pid' );
  5255. ## Start normal log output for this controller: basic facts
  5256. my $msg = qq{New VAC daemon. PID=$$};
  5257. $self->glog($msg, LOG_NORMAL);
  5258. ## Allow the MCP to signal us (request to exit)
  5259. local $SIG{USR1} = sub {
  5260. ## Do not change this message: looked for in the controller DIE sub
  5261. die "MCP request\n";
  5262. };
  5263. ## From this point forward, we want to die gracefully
  5264. local $SIG{__DIE__} = sub {
  5265. ## Arguments: one
  5266. ## 1. Error message
  5267. ## Returns: never (exit 0)
  5268. my ($diemsg) = @_;
  5269. ## Store the line that did the actual exception
  5270. my $line = (caller)[2];
  5271. ## Don't issue a warning if this was simply a MCP request
  5272. my $warn = ($diemsg =~ /MCP request|Not needed/ ? '' : 'Warning! ');
  5273. $self->glog(qq{${warn}VAC was killed at line $line: $diemsg}, $warn ? LOG_WARN :LOG_VERBOSE);
  5274. ## Not a whole lot of cleanup to do on this one: just shut database connections and leave
  5275. $self->{masterdbh}->disconnect() if exists $self->{masterdbhvac};
  5276. ## Remove our pid file
  5277. unlink $self->{vacpidfile} or $self->glog("Warning! Failed to unlink $self->{vacpidfile}", LOG_WARN);
  5278. exit 0;
  5279. }; ## end SIG{__DIE_} handler sub
  5280. ## Connect to the master database
  5281. ($self->{master_backend}, $self->{masterdbh}) = $self->connect_database();
  5282. $self->{masterdbhvac} = 1;
  5283. my $maindbh = $self->{masterdbh};
  5284. $self->glog("Bucardo database backend PID: $self->{master_backend}", LOG_VERBOSE);
  5285. ## Map the PIDs to common names for better log output
  5286. $self->{pidmap}{$$} = 'VAC';
  5287. $self->{pidmap}{$self->{master_backend}} = 'Bucardo DB';
  5288. ## Listen for an exit request from the MCP
  5289. my $exitrequest = 'stop_vac';
  5290. $self->db_listen($maindbh, $exitrequest, '', 1); ## No payloads please
  5291. ## Commit so we start listening right away
  5292. $maindbh->commit();
  5293. ## Reconnect to all databases we care about
  5294. for my $dbname (keys %{ $self->{sdb} }) {
  5295. $x = $self->{sdb}{$dbname};
  5296. ## We looped through all the syncs earlier to determine which databases
  5297. ## really need to be vacuumed. The criteria:
  5298. ## not a fullcopy sync, dbtype is postgres, role is source
  5299. next if ! $x->{needsvac};
  5300. ## Establish a new database handle
  5301. ($x->{backend}, $x->{dbh}) = $self->connect_database($dbname);
  5302. $self->glog(qq{Connected to database "$dbname" with backend PID of $x->{backend}}, LOG_NORMAL);
  5303. $self->{pidmap}{$x->{backend}} = "DB $dbname";
  5304. }
  5305. ## Track how long since we last came to life for vacuuming
  5306. my $lastvacrun = 0;
  5307. ## The main loop
  5308. VAC: {
  5309. ## Bail if the stopfile exists
  5310. if (-e $self->{stopfile}) {
  5311. $self->glog(qq{Found stopfile "$self->{stopfile}": exiting}, LOG_TERSE);
  5312. ## Do not change this message: looked for in the controller DIE sub
  5313. my $stopmsg = 'Found stopfile';
  5314. ## Grab the reason, if it exists, so we can propagate it onward
  5315. my $vacreason = get_reason(0);
  5316. if ($vacreason) {
  5317. $stopmsg .= ": $vacreason";
  5318. }
  5319. ## This exception is caught by the controller's __DIE__ sub above
  5320. die "$stopmsg\n";
  5321. }
  5322. ## Process any notifications from the main database
  5323. ## Ignore things we may have sent ourselves
  5324. my $nlist = $self->db_get_notices($maindbh, $self->{master_backend});
  5325. NOTICE: for my $name (sort keys %{ $nlist }) {
  5326. my $npid = $nlist->{$name}{firstpid};
  5327. ## Strip prefix so we can easily use both pre and post 9.0 versions
  5328. $name =~ s/^vac_//o;
  5329. ## Exit request from the MCP?
  5330. if ($name eq $exitrequest) {
  5331. die "Process $npid requested we exit\n";
  5332. }
  5333. ## Just ignore everything else
  5334. } ## end of each notification
  5335. ## To ensure we can receive new notifications next time:
  5336. $maindbh->commit();
  5337. ## Should we attempt a vacuum?
  5338. if (time() - $lastvacrun >= $config{vac_run}) {
  5339. $lastvacrun = time();
  5340. ## If there are no valid backends, we want to stop running entirely
  5341. my $valid_backends = 0;
  5342. ## Kick each one off async
  5343. for my $dbname (sort keys %{ $self->{sdb}} ) {
  5344. $x = $self->{sdb}{$dbname};
  5345. next if ! $x->{needsvac};
  5346. my $xdbh = $x->{dbh};
  5347. ## Safety check: if the bucardo schema is not there, we don't want to vacuum
  5348. if (! exists $x->{hasschema}) {
  5349. $SQL = q{SELECT count(*) FROM pg_namespace WHERE nspname = 'bucardo'};
  5350. $x->{hasschema} = $xdbh->selectall_arrayref($SQL)->[0][0];
  5351. if (! $x->{hasschema} ) {
  5352. $self->glog("Warning! Cannot vacuum db $dbname unless we have a bucardo schema", LOG_WARN);
  5353. }
  5354. }
  5355. ## No schema? We've already complained, so skip it silently
  5356. next if ! $x->{hasschema};
  5357. $valid_backends++;
  5358. ## Async please
  5359. $self->glog(qq{Running bucardo_purge_delta on database "$dbname"}, LOG_VERBOSE);
  5360. $SQL = q{SELECT bucardo.bucardo_purge_delta('45 seconds')};
  5361. $sth{"vac_$dbname"} = $xdbh->prepare($SQL, { pg_async => PG_ASYNC } );
  5362. $sth{"vac_$dbname"}->execute();
  5363. } ## end each source database
  5364. ## If we found no backends, we can leave right away, and not run again
  5365. if (! $valid_backends) {
  5366. $self->glog('No valid backends, so disabling the VAC daemon', LOG_VERBOSE);
  5367. $config{bucardo_vac} = 0;
  5368. ## Caught by handler above
  5369. die "Not needed";
  5370. }
  5371. ## Finish each one up
  5372. for my $dbname (sort keys %{ $self->{sdb}} ) {
  5373. $x = $self->{sdb}{$dbname};
  5374. ## As above, skip if not a source or no schema available
  5375. next if ! $x->{needsvac};
  5376. next if ! $x->{hasschema};
  5377. my $xdbh = $x->{dbh};
  5378. $self->glog(qq{Finish and fetch bucardo_purge_delta on database "$dbname"}, LOG_DEBUG);
  5379. $count = $sth{"vac_$dbname"}->pg_result();
  5380. my $info = $sth{"vac_$dbname"}->fetchall_arrayref()->[0][0];
  5381. $xdbh->commit();
  5382. $self->glog(qq{Purge on db "$dbname" gave: $info}, LOG_VERBOSE);
  5383. } ## end each source database
  5384. } ## end of attempting to vacuum
  5385. sleep $config{vac_sleep};
  5386. redo VAC;
  5387. } ## end of main VAC loop
  5388. exit 0;
  5389. } ## end of fork_vac
  5390. sub reset_mcp_listeners {
  5391. ## Unlisten everything, the relisten to specific entries
  5392. ## Used by reload_mcp()
  5393. ## Arguments: none
  5394. ## Returns: undef
  5395. my $self = shift;
  5396. my $maindbh = $self->{masterdbh};
  5397. ## Unlisten everything
  5398. $self->db_unlisten_all($maindbh);
  5399. ## Need to commit here to work around Postgres bug!
  5400. $maindbh->commit();
  5401. ## Listen for MCP specific items
  5402. for my $l
  5403. (
  5404. 'mcp_fullstop',
  5405. 'mcp_reload',
  5406. 'reload_config',
  5407. 'log_message',
  5408. 'mcp_ping',
  5409. ) {
  5410. $self->db_listen($maindbh, $l, '', 1);
  5411. }
  5412. ## Listen for sync specific items
  5413. for my $syncname (keys %{ $self->{sync} }) {
  5414. for my $l
  5415. (
  5416. 'activate_sync',
  5417. 'deactivate_sync',
  5418. 'reload_sync',
  5419. 'kick_sync',
  5420. ) {
  5421. ## If the sync is inactive, no sense in listening for anything but activate/reload requests
  5422. if ($self->{sync}{$syncname}{status} ne 'active') {
  5423. next if $l eq 'deactivate_sync' or $l eq 'kick_sync';
  5424. }
  5425. else {
  5426. ## If sync is active, no need to listen for an activate request
  5427. next if $l eq 'activate_sync';
  5428. }
  5429. my $listen = "${l}_$syncname";
  5430. $self->db_listen($maindbh, $listen, '', 1);
  5431. }
  5432. ## Listen for controller telling us the sync is done
  5433. $self->db_listen($maindbh, "syncdone_$syncname");
  5434. }
  5435. $maindbh->commit();
  5436. return;
  5437. } ## end of reset_mcp_listeners
  5438. sub reload_mcp {
  5439. ## Reset listeners, kill kids, load and activate syncs
  5440. ## Arguments: none
  5441. ## Returns: number of syncs we activated
  5442. my $self = shift;
  5443. ## Grab a list of all the current syncs from the database and store as objects
  5444. $self->{sync} = $self->get_syncs();
  5445. ## This unlistens any old syncs
  5446. $self->reset_mcp_listeners();
  5447. ## Stop any kids that currently exist
  5448. ## First, we loop through the PID directory and signal all CTL processes
  5449. ## These should in turn remove their kids
  5450. $self->signal_pid_files('ctl');
  5451. ## Next, we signal any KID processes that are still around
  5452. $self->signal_pid_files('kid');
  5453. ## Next we use dbrun to see if any database connections are still active
  5454. ## First, a brief sleep to allow things to catch up
  5455. sleep 0.5;
  5456. $self->terminate_old_goats();
  5457. my $maindbh = $self->{masterdbh};
  5458. ## At this point, we are authoritative, so we can safely clean out the syncrun table
  5459. $SQL = q{
  5460. UPDATE bucardo.syncrun
  5461. SET status=?, ended=now()
  5462. WHERE ended IS NULL
  5463. };
  5464. $sth = $maindbh->prepare($SQL);
  5465. my $cleanmsg = "Old entry ended (MCP $$)";
  5466. $count = $sth->execute($cleanmsg);
  5467. $maindbh->commit();
  5468. if ($count >= 1) {
  5469. $self->glog("Entries cleaned from the syncrun table: $count", LOG_NORMAL);
  5470. }
  5471. $SQL = q{TRUNCATE TABLE bucardo.dbrun};
  5472. $maindbh->do($SQL);
  5473. $self->glog(('Loading sync table. Rows=' . (scalar (keys %{ $self->{sync} }))), LOG_VERBOSE);
  5474. ## Load each sync in alphabetical order
  5475. my @activesyncs;
  5476. for (sort keys %{ $self->{sync} }) {
  5477. my $s = $self->{sync}{$_};
  5478. my $syncname = $s->{name};
  5479. ## Note that the mcp has changed this sync
  5480. $s->{mcp_changed} = 1;
  5481. ## Reset some boolean flags for this sync
  5482. $s->{mcp_active} = $s->{kick_on_startup} = $s->{controller} = 0;
  5483. ## If this sync is active, don't bother going any further
  5484. if ($s->{status} ne 'active') {
  5485. $self->glog(qq{Skipping sync "$syncname": status is "$s->{status}"}, LOG_TERSE);
  5486. next;
  5487. }
  5488. ## If we are doing specific syncs, check the name
  5489. if (exists $self->{dosyncs}) {
  5490. if (! exists $self->{dosyncs}{$syncname}) {
  5491. $self->glog(qq{Skipping sync "$syncname": not explicitly named}, LOG_VERBOSE);
  5492. next;
  5493. }
  5494. $self->glog(qq{Activating sync "$syncname": explicitly named}, LOG_VERBOSE);
  5495. }
  5496. else {
  5497. $self->glog(qq{Activating sync "$syncname"}, LOG_NORMAL);
  5498. }
  5499. ## Activate this sync!
  5500. $s->{mcp_active} = 1;
  5501. if (! $self->activate_sync($s)) {
  5502. $s->{mcp_active} = 0;
  5503. }
  5504. # If it was successfully activated, push it on the queue
  5505. push @activesyncs, $syncname if $s->{mcp_active};
  5506. } ## end each sync
  5507. ## Change our process name, and list all active syncs
  5508. $0 = "Bucardo Master Control Program v$VERSION.$self->{extraname} Active syncs: ";
  5509. $0 .= join ',' => @activesyncs;
  5510. my $count = @activesyncs;
  5511. return $count;
  5512. } ## end of reload_mcp
  5513. sub cleanup_mcp {
  5514. ## MCP is shutting down, so we:
  5515. ## - disconnect from the database
  5516. ## - attempt to kill any controller kids
  5517. ## - send a final NOTIFY
  5518. ## - remove our own PID file
  5519. ## Arguments: one
  5520. ## 1. String with a reason for exiting
  5521. ## Returns: undef
  5522. my ($self,$exitreason) = @_;
  5523. ## Rollback and disconnect from the master database if needed
  5524. if ($self->{masterdbh}) {
  5525. $self->{masterdbh}->rollback();
  5526. $self->{masterdbh}->disconnect();
  5527. }
  5528. ## Reconnect to the master database for some final cleanups
  5529. my ($finalbackend,$finaldbh) = $self->connect_database();
  5530. $self->glog("Final database backend PID: $finalbackend", LOG_VERBOSE);
  5531. ## Sleep a bit to let the processes clean up their own pid files
  5532. sleep 1.5;
  5533. ## We know we are authoritative for all pid files in the piddir
  5534. ## Use those to kill any open processes that we think are still bucardo related
  5535. my $piddir = $config{piddir};
  5536. opendir my $dh, $piddir or die qq{Could not opendir "$piddir" $!\n};
  5537. ## As before, we only worry about certain files,
  5538. ## even though nothing else should be in there
  5539. my @pidfiles2 = grep { /^bucardo.*\.pid$/ } readdir $dh;
  5540. closedir $dh or warn qq{Could not closedir "$piddir": $!\n};
  5541. ## For each file, attempt to kill the process it refers to
  5542. for my $pidfile (sort @pidfiles2) {
  5543. next if $pidfile eq 'bucardo.mcp.pid'; ## That's us!
  5544. my $pfile = File::Spec->catfile( $piddir => $pidfile );
  5545. if (-e $pfile) {
  5546. $self->glog("Trying to kill stale PID file $pidfile", LOG_DEBUG);
  5547. my $result = $self->kill_bucardo_pidfile($pfile);
  5548. if ($result == -4) { ## kill 0 indicates that PID is no more
  5549. $self->glog("PID from $pidfile is gone, removing file", LOG_NORMAL);
  5550. unlink $pfile;
  5551. }
  5552. }
  5553. }
  5554. ## Gather system and database timestamps, output them to the logs
  5555. my $end_systemtime = scalar localtime;
  5556. my $end_dbtime = eval { $finaldbh->selectcol_arrayref('SELECT now()')->[0] } || 'unknown';
  5557. $self->glog(qq{End of cleanup_mcp. Sys time: $end_systemtime. Database time: $end_dbtime}, LOG_TERSE);
  5558. ## Let anyone listening know we have stopped
  5559. $self->db_notify($finaldbh, 'stopped', 1) if $end_dbtime ne 'unknown';
  5560. $finaldbh->disconnect();
  5561. ## For the very last thing, remove our own PID file
  5562. if (unlink $self->{pidfile}) {
  5563. $self->glog(qq{Removed pid file "$self->{pidfile}"}, LOG_DEBUG);
  5564. }
  5565. else {
  5566. $self->glog("Warning! Failed to remove pid file $self->{pidfile}", LOG_WARN);
  5567. }
  5568. return;
  5569. } ## end of cleanup_mcp
  5570. sub terminate_old_goats {
  5571. ## Uses the dbrun table to see if any existing connections are still active
  5572. ## This can happen if a KID is killed but a large COPY is still going on
  5573. ## Arguments: one
  5574. ## 1. Optional sync name to limit the reaping to
  5575. ## Returns: number of backends successfully terminated
  5576. my $self = shift;
  5577. my $sync = shift || '';
  5578. my $maindbh = $self->{masterdbh};
  5579. ## Grab all backends in the tbale
  5580. $SQL = 'SELECT * FROM bucardo.dbrun WHERE pgpid IS NOT NULL';
  5581. ## Just for one sync if that was passed in
  5582. if ($sync) {
  5583. $SQL .= ' AND sync = ' . $maindbh->quote($sync);
  5584. }
  5585. $sth = $maindbh->prepare($SQL);
  5586. $sth->execute();
  5587. ## Create a hash with the names of the databases as the first-level keys,
  5588. ## and the process ids as the second-level keys.
  5589. my %dbpid;
  5590. for my $row (@{ $sth->fetchall_arrayref({}) }) {
  5591. $dbpid{$row->{dbname}}{$row->{pgpid}} = $row->{started};
  5592. }
  5593. ## Use pg_stat_activity to find a match, then terminate it
  5594. my $pidcol = $maindbh->{pg_server_version} >= 90200 ? 'pid' : 'procpid';
  5595. $SQL = "SELECT 1 FROM pg_stat_activity WHERE $pidcol = ? AND query_start = ?";
  5596. my $SQLC = 'SELECT pg_cancel_backend(?)';
  5597. my $total = 0;
  5598. for my $dbname (sort keys %{ $self->{sdb} }) {
  5599. $x = $self->{sdb}{$dbname};
  5600. ## All of this is very Postgres specific
  5601. next if $x->{dbtype} ne 'postgres';
  5602. ## Loop through each backend PID found for this database
  5603. for my $pid (sort keys %{ $dbpid{$dbname} }) {
  5604. my $time = $dbpid{$dbname}{$pid};
  5605. $sth = $x->{dbh}->prepare($SQL);
  5606. ## See if the process is still around by matching PID and query_start time
  5607. $count = $sth->execute($pid, $time);
  5608. $sth->finish();
  5609. ## If no match, silently move on
  5610. next if $count < 1;
  5611. ## If we got a match, try and kill it
  5612. $sth = $x->{dbh}->prepare($SQLC);
  5613. $count = $sth->execute($pid);
  5614. my $res = $count < 1 ? 'failed' : 'ok';
  5615. $self->glog("Attempted to kill backend $pid on db $dbname, started $time. Result: $res", LOG_NORMAL);
  5616. ## We are going to count both failed and ok as the same for the return number
  5617. $total += $count;
  5618. }
  5619. }
  5620. return $total;
  5621. } ## end of terminate_old_goats
  5622. sub kill_bucardo_pidfile {
  5623. ## Given a file, extract the PID and kill it
  5624. ## Arguments: 2
  5625. ## 1. File to be checked
  5626. ## 2. String either 'strict' or not. Strict does TERM and KILL in addition to USR1
  5627. ## Returns: same as kill_bucardo_pid, plus:
  5628. ## -100: File not found
  5629. ## -101: Could not open the file
  5630. ## -102: No PID found in the file
  5631. my ($self,$file,$strength) = @_;
  5632. ## Make sure the file supplied exists!
  5633. if (! -e $file) {
  5634. $self->glog(qq{Failed to find PID file "$file"}, LOG_VERBOSE);
  5635. return -100;
  5636. }
  5637. ## Try and open the supplied file
  5638. my $fh;
  5639. if (! open $fh, '<', $file) {
  5640. $self->glog(qq{Failed to open PID file "$file": $!}, LOG_VERBOSE);
  5641. return -101;
  5642. }
  5643. ## Try and extract the numeric PID from inside of it
  5644. ## Should be the only thing on the first line
  5645. if (<$fh> !~ /(\d+)/) {
  5646. $self->glog(qq{Failed to find a PID in the file PID "$file"}, LOG_TERSE);
  5647. close $fh or warn qq{Could not close "$file": $!};
  5648. return -102;
  5649. }
  5650. ## Close the file and call another method to do the dirty work
  5651. close $fh or warn qq{Could not close "$file": $!};
  5652. return $self->kill_bucardo_pid($1 => $strength);
  5653. } ## end of kill_bucardo_pidfile
  5654. sub kill_bucardo_pid {
  5655. ## Send a kill signal to a specific process
  5656. ## Arguments: two
  5657. ## 1. PID to be killed
  5658. ## 2. String either 'strict' or not. Strict does KILL and TERM in addition to USR1
  5659. ## Returns: 1 on successful kill, < 0 otherwise
  5660. ## 0: no such PID or not a 'bucardo' PID
  5661. ## +1 : successful TERM
  5662. ## -1: Failed to signal with USR1
  5663. ## +2: Successful KILL
  5664. ## -2: Failed to signal with TERM and KILL
  5665. ## -3: Invalid PID (non-numeric)
  5666. ## -4: PID does not exist
  5667. my ($self,$pid,$nice) = @_;
  5668. $self->glog("Attempting to kill PID $pid", LOG_VERBOSE);
  5669. ## We want to confirm this is still a Bucardo process
  5670. ## The most portable way at the moment is a plain ps -p
  5671. ## Windows users are on their own
  5672. ## If the PID is not numeric, throw a warning and return
  5673. if ($pid !~ /^\d+$/o) {
  5674. $self->glog("Warning: invalid PID supplied to kill_bucardo_pid: $pid", LOG_WARN);
  5675. return -3;
  5676. }
  5677. ## Make sure the process is still around
  5678. ## If not, log it and return
  5679. if (! kill(0 => $pid) ) {
  5680. $self->glog("Process $pid did not respond to a kill 0", LOG_NORMAL);
  5681. return -4;
  5682. }
  5683. ## It's nice to do some basic checks when possible that these are Bucardo processes
  5684. ## For non Win32 boxes, we can try a basic ps
  5685. ## If no header line, drive on
  5686. ## If command is not perl, skip it!
  5687. ## If args is not perl or bucardo, skip it
  5688. if ($^O !~ /Win/) {
  5689. my $COM = "ps -p $pid -o comm,args";
  5690. my $info = qx{$COM};
  5691. if ($info !~ /^COMMAND/) {
  5692. $self->glog(qq{Could not determine ps information for pid $pid}, LOG_VERBOSE);
  5693. }
  5694. elsif ($info !~ /\bbucardo\s+/o) {
  5695. $self->glog(qq{Will not kill process $pid: ps args is not 'Bucardo', got: $info}, LOG_TERSE);
  5696. return 0;
  5697. }
  5698. } ## end of trying ps because not Windows
  5699. ## At this point, we've done due diligence and can start killing this pid
  5700. ## Start with a USR1 signal
  5701. $self->glog("Sending signal $signumber{USR1} to pid $pid", LOG_DEBUG);
  5702. $count = kill $signumber{USR1} => $pid;
  5703. if ($count >= 1) {
  5704. $self->glog("Successfully signalled pid $pid with kill USR1", LOG_DEBUG);
  5705. return 1;
  5706. }
  5707. ## If we are not strict, we are done
  5708. if ($nice ne 'strict') {
  5709. $self->glog("Failed to USR1 signal pid $pid", LOG_TERSE);
  5710. return -1;
  5711. }
  5712. $self->glog("Sending signal $signumber{TERM} to pid $pid", LOG_DEBUG);
  5713. $count = kill $signumber{TERM} => $pid;
  5714. if ($count >= 1) {
  5715. $self->glog("Successfully signalled pid $pid with kill TERM", LOG_DEBUG);
  5716. return 1;
  5717. }
  5718. $self->glog("Failed to TERM signal pid $pid", LOG_TERSE);
  5719. ## Raise the stakes and issue a KILL signal
  5720. $self->glog("Sending signal $signumber{KILL} to pid $pid", LOG_DEBUG);
  5721. $count = kill $signumber{KILL} => $pid;
  5722. if ($count >= 1) {
  5723. $self->glog("Successfully signalled pid $pid with kill KILL", LOG_DEBUG);
  5724. return 2;
  5725. }
  5726. $self->glog("Failed to KILL signal pid $pid", LOG_TERSE);
  5727. return -2;
  5728. } ## end of kill_bucardo_pid
  5729. sub signal_pid_files {
  5730. ## Finds the pid in all matching pid files, and signals with USR1
  5731. ## Arguments: 1
  5732. ## 1. String to match the file inside the PID directory with
  5733. ## Returns: number successfully signalled
  5734. my ($self,$string) = @_;
  5735. my $signalled = 0;
  5736. ## Open the directory that contains our PID files
  5737. my $piddir = $config{piddir};
  5738. opendir my $dh, $piddir or die qq{Could not opendir "$piddir": $!\n};
  5739. my ($name, $fh);
  5740. while (defined ($name = readdir($dh))) {
  5741. ## Skip unless it's a matched file
  5742. next unless index($name, $string) >= 0;
  5743. $self->glog(qq{Attempting to signal PID from file "$name"}, LOG_TERSE);
  5744. ## File must be readable
  5745. my $cfile = File::Spec->catfile( $piddir => $name );
  5746. if (! open $fh, '<', $cfile) {
  5747. $self->glog(qq{Could not open $cfile: $!}, LOG_WARN);
  5748. next;
  5749. }
  5750. ## File must contain a number (the PID)
  5751. if (<$fh> !~ /(\d+)/) {
  5752. $self->glog(qq{Warning! File "$cfile" did not contain a PID!}, LOG_WARN);
  5753. next;
  5754. }
  5755. my $pid = $1; ## no critic (ProhibitCaptureWithoutTest)
  5756. close $fh or warn qq{Could not close "$cfile": $!\n};
  5757. ## No sense in doing deeper checks that this is still a Bucardo process,
  5758. ## as a USR1 should be a pretty harmless signal
  5759. $count = kill $signumber{USR1} => $pid;
  5760. if ($count != 1) {
  5761. $self->glog(qq{Failed to signal $pid with USR1}, LOG_WARN);
  5762. }
  5763. else {
  5764. $signalled++;
  5765. }
  5766. } ## end each file in the pid directory
  5767. closedir $dh or warn qq{Warning! Could not closedir "$piddir": $!\n};
  5768. return $signalled;
  5769. } ## end of signal_pid_files
  5770. sub cleanup_controller {
  5771. ## Controller is shutting down
  5772. ## Disconnect from the database
  5773. ## Attempt to kill any kids
  5774. ## Remove our PID file
  5775. ## Arguments: two
  5776. ## 1. Exited normally? (0 or 1)
  5777. ## 2. Reason for leaving
  5778. ## Return: undef
  5779. my ($self,$normalexit,$reason) = @_;
  5780. if (exists $self->{cleanexit}) {
  5781. $reason = 'Normal exit';
  5782. }
  5783. ## Ask all kids to exit as well
  5784. my $exitname = "kid_stopsync_$self->{syncname}";
  5785. $self->{masterdbh}->rollback();
  5786. $self->db_notify($self->{masterdbh}, $exitname);
  5787. ## Disconnect from the master database
  5788. if ($self->{masterdbh}) {
  5789. # Quick debug to find active statement handles
  5790. # for my $s (@{$self->{masterdbh}{ChildHandles}}) {
  5791. # next if ! ref $s or ! $s->{Active};
  5792. # $self->glog(Dumper $s->{Statement}, LOG_NORMAL);
  5793. #}
  5794. $self->{masterdbh}->rollback();
  5795. $self->{masterdbh}->disconnect();
  5796. }
  5797. ## Sleep a bit to let the processes clean up their own pid files
  5798. sleep 0.5;
  5799. ## Kill any kids who have a pid file for this sync
  5800. ## By kill, we mean "send a friendly USR1 signal"
  5801. my $piddir = $config{piddir};
  5802. opendir my $dh, $piddir or die qq{Could not opendir "$piddir" $!\n};
  5803. my @pidfiles = readdir $dh;
  5804. closedir $dh or warn qq{Could not closedir "$piddir": $!\n};
  5805. for my $pidfile (sort @pidfiles) {
  5806. my $sname = $self->{syncname};
  5807. next unless $pidfile =~ /^bucardo\.kid\.sync\.$sname\.?.*\.pid$/;
  5808. my $pfile = File::Spec->catfile( $piddir => $pidfile );
  5809. if (open my $fh, '<', $pfile) {
  5810. my $pid = <$fh>;
  5811. close $fh or warn qq{Could not close "$pfile": $!\n};
  5812. if (! defined $pid or $pid !~ /^\d+$/) {
  5813. $self->glog("Warning: no PID found in file, so removing $pfile", LOG_TERSE);
  5814. unlink $pfile;
  5815. }
  5816. else {
  5817. kill $signumber{USR1} => $pid;
  5818. $self->glog("Sent USR1 signal to kid process $pid", LOG_VERBOSE);
  5819. }
  5820. }
  5821. else {
  5822. $self->glog("Warning: could not open file, so removing $pfile", LOG_TERSE);
  5823. unlink $pfile;
  5824. }
  5825. }
  5826. $self->glog("Controller $$ exiting at cleanup_controller. Reason: $reason", LOG_TERSE);
  5827. ## Remove the pid file
  5828. if (unlink $self->{ctlpidfile}) {
  5829. $self->glog(qq{Removed pid file "$self->{ctlpidfile}"}, LOG_DEBUG);
  5830. }
  5831. else {
  5832. $self->glog("Warning! Failed to remove pid file $self->{ctlpidfile}", LOG_WARN);
  5833. }
  5834. ## Reconnect and clean up the syncrun table
  5835. my ($finalbackend, $finaldbh) = $self->connect_database();
  5836. $self->glog("Final database backend PID: $finalbackend", LOG_VERBOSE);
  5837. ## Need to make this one either lastgood or lastbad
  5838. ## In theory, this will never set lastgood
  5839. $self->end_syncrun($finaldbh, $normalexit ? 'good' : 'bad',
  5840. $self->{syncname}, "Ended (CTL $$)");
  5841. $finaldbh->commit();
  5842. $finaldbh->disconnect();
  5843. $self->glog('Made final adjustment to the syncrun table', LOG_DEBUG);
  5844. return;
  5845. } ## end of cleanup_controller
  5846. sub end_syncrun {
  5847. ## End the current syncrun entry, and adjust lastgood/lastbad/lastempty as needed
  5848. ## If there is no null ended for this sync, does nothing
  5849. ## Does NOT commit
  5850. ## Arguments: four
  5851. ## 1. The database handle to use
  5852. ## 2. How did we exit ('good', 'bad', or 'empty')
  5853. ## 3. The name of the sync
  5854. ## 4. The new status to put
  5855. ## Returns: undef
  5856. my ($self, $ldbh, $exitmode, $syncname, $status) = @_;
  5857. ## Which column are we changing?
  5858. my $lastcol =
  5859. $exitmode eq 'good' ? 'lastgood' :
  5860. $exitmode eq 'bad' ? 'lastbad' :
  5861. $exitmode eq 'empty' ? 'lastempty' :
  5862. die qq{Invalid exitmode "$exitmode"};
  5863. ## Make sure we have something to update
  5864. $SQL = q{
  5865. SELECT ctid
  5866. FROM bucardo.syncrun
  5867. WHERE sync = ?
  5868. AND ended IS NULL};
  5869. $sth = $ldbh->prepare($SQL);
  5870. $count = $sth->execute($syncname);
  5871. if ($count < 1) {
  5872. $sth->finish();
  5873. return;
  5874. }
  5875. if ($count > 1) {
  5876. $self->glog("Expected one row from end_syncrun, but got $count", LOG_NORMAL);
  5877. }
  5878. my $ctid = $sth->fetchall_arrayref()->[0][0];
  5879. ## Remove the previous 'last' entry, if any
  5880. $SQL = qq{
  5881. UPDATE bucardo.syncrun
  5882. SET $lastcol = 'false'
  5883. WHERE $lastcol IS TRUE
  5884. AND sync = ?
  5885. };
  5886. $sth = $ldbh->prepare($SQL);
  5887. $sth->execute($syncname);
  5888. ## End the current row, and elevate it to a 'last' position
  5889. $SQL = qq{
  5890. UPDATE bucardo.syncrun
  5891. SET $lastcol = 'true', ended=now(), status=?
  5892. WHERE ctid = ?
  5893. };
  5894. $sth = $ldbh->prepare($SQL);
  5895. $sth->execute($status, $ctid);
  5896. return;
  5897. } ## end of end_syncrun
  5898. sub run_ctl_custom_code {
  5899. ## Arguments: four
  5900. ## 1. Sync object
  5901. ## 2. Input object
  5902. ## 2. Hashref of customcode information
  5903. ## 3. Strictness boolean, defaults to false
  5904. ## 4. Number of attempts, defaults to 0
  5905. ## Returns: string indicating what to do, one of:
  5906. ## 'next'
  5907. ## 'redo'
  5908. ## 'normal'
  5909. my $self = shift;
  5910. my $sync = shift;
  5911. my $input = shift;
  5912. my $c = shift;
  5913. my $strictness = shift || '';
  5914. my $attempts = shift || 0;
  5915. $self->glog("Running $c->{whenrun} controller custom code $c->{id}: $c->{name}", LOG_NORMAL);
  5916. my $cc_sourcedbh;
  5917. if (!defined $sync->{safe_sourcedbh}) {
  5918. $cc_sourcedbh = $self->connect_database($sync->{sourcedb});
  5919. my $darg;
  5920. for my $arg (sort keys %{ $dbix{source}{notstrict} }) {
  5921. next if ! length $dbix{source}{notstrict}{$arg};
  5922. $darg->{$arg} = $dbix{source}{notstrict}{$arg};
  5923. }
  5924. $darg->{dbh} = $cc_sourcedbh;
  5925. $sync->{safe_sourcedbh} = DBIx::Safe->new($darg);
  5926. }
  5927. $input = {
  5928. sourcedbh => $sync->{safe_sourcedbh},
  5929. syncname => $sync->{name},
  5930. goatlist => $sync->{goatlist},
  5931. rellist => $sync->{goatlist},
  5932. sourcename => $sync->{sourcedb},
  5933. targetname => '',
  5934. message => '',
  5935. warning => '',
  5936. error => '',
  5937. nextcode => '',
  5938. endsync => '',
  5939. };
  5940. $self->{masterdbh}->{InactiveDestroy} = 1;
  5941. $cc_sourcedbh->{InactiveDestroy} = 1;
  5942. local $_ = $input;
  5943. $c->{coderef}->($input);
  5944. $self->{masterdbh}->{InactiveDestroy} = 0;
  5945. $cc_sourcedbh->{InactiveDestroy} = 0;
  5946. $self->glog("Finished custom code $c->{id}", LOG_VERBOSE);
  5947. if (length $input->{message}) {
  5948. $self->glog("Message from $c->{whenrun} code $c->{id}: $input->{message}", LOG_TERSE);
  5949. }
  5950. if (length $input->{warning}) {
  5951. $self->glog("Warning! Code $c->{whenrun} $c->{id}: $input->{warning}", LOG_WARN);
  5952. }
  5953. if (length $input->{error}) {
  5954. $self->glog("Warning! Code $c->{whenrun} $c->{id}: $input->{error}", LOG_WARN);
  5955. die "Code $c->{whenrun} $c->{id} error: $input->{error}";
  5956. }
  5957. if (length $input->{nextcode}) { ## Mostly for conflict handlers
  5958. return 'next';
  5959. }
  5960. if (length $input->{endsync}) {
  5961. $self->glog("Code $c->{whenrun} requests a cancellation of the rest of the sync", LOG_TERSE);
  5962. ## before_txn and after_txn only should commit themselves
  5963. $cc_sourcedbh->rollback();
  5964. $self->{masterdbh}->commit();
  5965. sleep $config{endsync_sleep};
  5966. return 'redo';
  5967. }
  5968. return 'normal';
  5969. } ## end of run_ctl_custom_code
  5970. sub create_newkid {
  5971. ## Fork and create a KID process
  5972. ## Arguments: one
  5973. ## 1. Hashref of sync information ($self->{sync}{$syncname})
  5974. ## Returns: PID of new process
  5975. my ($self, $kidsync) = @_;
  5976. ## Just in case, ask any existing kid processes to exit
  5977. $self->db_notify($self->{masterdbh}, "kid_stopsync_$self->{syncname}");
  5978. ## Fork off a new process which will become the KID
  5979. my $newkid = $self->fork_and_inactivate('KID');
  5980. if ($newkid) { ## We are the parent
  5981. my $msg = sprintf q{Created new kid %s for sync "%s"},
  5982. $newkid, $self->{syncname};
  5983. $self->glog($msg, LOG_NORMAL);
  5984. ## Map this PID to a name for CTL use elsewhere
  5985. $self->{pidmap}{$newkid} = 'KID';
  5986. sleep $config{ctl_createkid_time};
  5987. return $newkid;
  5988. }
  5989. ## Create the kid process
  5990. $self->start_kid($kidsync);
  5991. exit 0;
  5992. } ## end of create_newkid
  5993. sub get_deadlock_details {
  5994. ## Given a database handle, extract deadlock details from it
  5995. ## Arguments: two
  5996. ## 1. Database handle
  5997. ## 2. Database error string
  5998. ## Returns: detailed string, or an empty one
  5999. my ($self, $dldbh, $dlerr) = @_;
  6000. return '' unless $dlerr =~ /Process \d+ waits for /;
  6001. return '' unless defined $dldbh and $dldbh;
  6002. $dldbh->rollback();
  6003. my $pid = $dldbh->{pg_pid};
  6004. while ($dlerr =~ /Process (\d+) waits for (.+) on relation (\d+) of database (\d+); blocked by process (\d+)/g) {
  6005. next if $1 == $pid;
  6006. my ($process,$locktype,$relation) = ($1,$2,$3);
  6007. ## Fetch the relation name
  6008. my $getname = $dldbh->prepare(q{SELECT nspname||'.'||relname FROM pg_class c, pg_namespace n ON (n.oid=c.relnamespace) WHERE c.oid = ?});
  6009. $getname->execute($relation);
  6010. my $relname = $getname->fetchall_arrayref()->[0][0];
  6011. my $clock_timestamp = $dldbh->{pg_server_version} >= 80200
  6012. ? 'clock_timestamp()' : 'timeofday()::timestamptz';
  6013. ## Fetch information about the conflicting process
  6014. my $pidcol = $dldbh->{pg_server_version} >= 90200 ? 'pid' : 'procpid';
  6015. my $queryinfo =$dldbh->prepare(qq{
  6016. SELECT
  6017. current_query AS query,
  6018. datname AS database,
  6019. TO_CHAR($clock_timestamp, 'HH24:MI:SS (YYYY-MM-DD)') AS current_time,
  6020. TO_CHAR(backend_start, 'HH24:MI:SS (YYYY-MM-DD)') AS backend_started,
  6021. TO_CHAR($clock_timestamp - backend_start, 'HH24:MI:SS') AS backend_age,
  6022. CASE WHEN query_start IS NULL THEN '?' ELSE
  6023. TO_CHAR(query_start, 'HH24:MI:SS (YYYY-MM-DD)') END AS query_started,
  6024. CASE WHEN query_start IS NULL THEN '?' ELSE
  6025. TO_CHAR($clock_timestamp - query_start, 'HH24:MI:SS') END AS query_age,
  6026. COALESCE(host(client_addr)::text,''::text) AS ip,
  6027. CASE WHEN client_port <= 0 THEN 0 ELSE client_port END AS port,
  6028. usename AS user
  6029. FROM pg_stat_activity
  6030. WHERE $pidcol = ?
  6031. });
  6032. $queryinfo->execute($process);
  6033. my $q = $queryinfo->fetchall_arrayref({})->[0];
  6034. my $ret = qq{Deadlock on "$relname"\nLocktype: $locktype\n};
  6035. if (defined $q) {
  6036. $ret .= qq{Blocker PID: $process $q->{ip} Database: $q->{database} User: $q->{user}\n}.
  6037. qq{Query: $q->{query}\nQuery started: $q->{query_started} Total time: $q->{query_age}\n}.
  6038. qq{Backend started: $q->{backend_started} Total time: $q->{backend_age}\n};
  6039. }
  6040. return $ret;
  6041. }
  6042. return;
  6043. } ## end of get_deadlock_details
  6044. sub cleanup_kid {
  6045. ## Kid is shutting down
  6046. ## Remove our PID file
  6047. ## Arguments: two
  6048. ## 1. Reason for leaving
  6049. ## 2. Extra information
  6050. ## Returns: undef
  6051. my ($self,$reason,$extrainfo) = @_;
  6052. $self->glog("Kid $$ exiting at cleanup_kid. $extrainfo Reason: $reason", LOG_TERSE);
  6053. ## Remove the pid file, but only if it has our PID in it!
  6054. my $file = $self->{kidpidfile};
  6055. my $fh;
  6056. if (! open my $fh, '<', $file) {
  6057. $self->glog("Warning! Could not find pid file $file", LOG_WARN);
  6058. }
  6059. elsif (<$fh> !~ /(\d+)/) {
  6060. $self->glog("Warning! File $file did not contain a PID", LOG_WARN);
  6061. }
  6062. else {
  6063. my $oldpid = $1;
  6064. if ($$ !~ $oldpid) {
  6065. $self->glog("File $file contained foreign PID $oldpid, so will not remove", LOG_WARN);
  6066. }
  6067. elsif (unlink $file) {
  6068. $self->glog(qq{Removed pid file $file}, LOG_DEBUG);
  6069. }
  6070. else {
  6071. $self->glog("Warning! Failed to remove pid file $file", LOG_WARN);
  6072. }
  6073. }
  6074. return;
  6075. } ## end of cleanup_kid
  6076. sub store_pid {
  6077. ## Store the PID of the current process somewhere (e.g. local disk)
  6078. ## Arguments: one
  6079. ## 1. Name of the file
  6080. ## Returns: complete name of the file, with directory
  6081. my $self = shift;
  6082. my $file = shift or die;
  6083. ## Put this file into our pid directory
  6084. my $pidfile = File::Spec->catfile( $config{piddir} => $file );
  6085. ## Check for any remove old processes
  6086. my $oldpid = '?';
  6087. if (-e $pidfile) {
  6088. ## Send the PID in the file a USR1. If we did so, sleep a littel bit
  6089. ## to allow that process to clean itself up
  6090. $self->signal_pid_files($pidfile) and sleep 1;
  6091. if (-e $pidfile) {
  6092. $self->glog("Overwriting $pidfile: old process was $oldpid", LOG_NORMAL);
  6093. }
  6094. }
  6095. ## Overwrite anything that is already there
  6096. open my $pidfh, '>', $pidfile or die qq{Cannot write to $pidfile: $!\n};
  6097. print {$pidfh} "$$\n";
  6098. close $pidfh or warn qq{Could not close "$pidfile": $!\n};
  6099. $self->glog("Created $pidfile", LOG_DEBUG);
  6100. return $pidfile;
  6101. } ## end of store_pid
  6102. sub table_has_rows {
  6103. ## See if the given table has any rows or not
  6104. ## Arguments: two
  6105. ## 1. Target database object (contains dbtype and possibly dbh)
  6106. ## 2. Name of the table
  6107. ## Returns: true or false
  6108. my ($self,$x,$tname) = @_;
  6109. ## Some types do not have a count
  6110. return 0 if $x->{does_append_only};
  6111. if ($x->{does_limit}) {
  6112. $SQL = "SELECT 1 FROM $tname LIMIT 1";
  6113. $sth = $x->{dbh}->prepare($SQL);
  6114. $sth->execute();
  6115. $count = $sth->rows();
  6116. $sth->finish();
  6117. return $count >= 1 ? 1 : 0;
  6118. }
  6119. elsif ('mongo' eq $x->{dbtype}) {
  6120. my $collection = $x->{dbh}->get_collection($tname);
  6121. $count = $collection->count({});
  6122. return $count >= 1 ? 1 : 0;
  6123. }
  6124. elsif ('oracle' eq $x->{dbtype}) {
  6125. $SQL = "SELECT 1 FROM $tname WHERE rownum > 1";
  6126. $sth = $x->{dbh}->prepare($SQL);
  6127. $sth->execute();
  6128. $count = $sth->rows();
  6129. $sth->finish();
  6130. return $count >= 1 ? 1 : 0;
  6131. }
  6132. elsif ('redis' eq $x->{dbtype}) {
  6133. ## No sense in returning anything here
  6134. return 0;
  6135. }
  6136. else {
  6137. die "Cannot handle database type $x->{dbtype} yet!";
  6138. }
  6139. return 0;
  6140. } ## end of table_has_rows
  6141. sub get_sequence_info {
  6142. ## Get sequence information
  6143. ## Not technically MVCC but good enough for our purposes
  6144. ## Arguments: five
  6145. ## 1. Database handle
  6146. ## 2. Schema name
  6147. ## 3. Sequence name
  6148. ## 4. (optional) Name of the sync
  6149. ## 5. (optional) Target database name
  6150. ## Returns: hashref of information
  6151. ## If five arguments are given, look up the "old" information in bucardo_sequences
  6152. ## With only three arguments, pull directly from the sequence
  6153. return; ## XXX sequence work
  6154. my ($self,$ldbh,$schemaname,$seqname,$syncname,$targetname) = @_;
  6155. if (defined $syncname) {
  6156. ## Pull "old" sequence information. May be empty.
  6157. $SQL = "SELECT $sequence_columns FROM bucardo.bucardo_sequences "
  6158. . ' WHERE schemaname=? AND seqname = ? AND syncname=? AND targetname=?';
  6159. $sth = $ldbh->prepare($SQL);
  6160. $sth->execute($schemaname,$seqname, $syncname, $targetname);
  6161. }
  6162. else {
  6163. ## Pull directly from a named sequence
  6164. $SQL = "SELECT $sequence_columns FROM $schemaname.$seqname";
  6165. $sth = $ldbh->prepare($SQL);
  6166. $sth->execute();
  6167. }
  6168. return $sth->fetchall_arrayref({})->[0];
  6169. } ## end of get_sequence_info
  6170. sub adjust_sequence {
  6171. ## Adjusts all sequences as needed using a "winning" source database sequence
  6172. ## If changed, update the bucardo_sequences table
  6173. ## Arguments: four
  6174. ## 1. goat object (which contains 'winning_db' and 'sequenceinfo')
  6175. ## 2. sync object
  6176. ## 2. Schema name
  6177. ## 3. Sequence name
  6178. ## 4. Name of the current sync
  6179. ## Returns: number of changes made for this sequence
  6180. my ($self,$g,$sync,$S,$T,$syncname) = @_;
  6181. ## Total changes made across all databases
  6182. my $changes = 0;
  6183. my $winner = $g->{winning_db};
  6184. my $sourceinfo = $g->{sequenceinfo}{$winner};
  6185. ## Walk through all Postgres databases and set the sequence
  6186. for my $dbname (sort keys %{ $sync->{db} }) {
  6187. next if $dbname eq $winner; ## Natch
  6188. $x = $sync->{db}{$dbname};
  6189. next if $x->{dbtype} ne 'postgres';
  6190. next if ! $x->{adjustsequence};
  6191. ## Reset the flag in case this sub is called more than once
  6192. $x->{adjustsequence} = 0;
  6193. my $targetinfo = $g->{sequenceinfo}{$dbname} || {};
  6194. ## First, change things up via SETVAL if needed
  6195. if (! exists $targetinfo->{last_value}
  6196. or
  6197. $sourceinfo->{last_value} != $targetinfo->{last_value}
  6198. or
  6199. $sourceinfo->{is_called} != $targetinfo->{is_called}) {
  6200. $self->glog("Set sequence $dbname.$S.$T to $sourceinfo->{last_value} (is_called to $sourceinfo->{is_called})",
  6201. LOG_DEBUG);
  6202. $SQL = qq{SELECT setval('$S.$T', $sourceinfo->{last_value}, '$sourceinfo->{is_called}')};
  6203. $x->{dbh}->do($SQL);
  6204. $changes++;
  6205. }
  6206. ## Then, change things up via ALTER SEQUENCE if needed
  6207. my @alter;
  6208. for my $col (@sequence_columns) {
  6209. my ($name,$syntax) = @$col;
  6210. ## Skip things not set by ALTER SEQUENCE
  6211. next if ! $syntax;
  6212. ## Skip if these items are the exact same
  6213. next if exists $targetinfo->{last_value} and $sourceinfo->{$name} eq $targetinfo->{$name};
  6214. ## Fullcopy will not have this, and we won't report it
  6215. if (exists $targetinfo->{last_value}) {
  6216. $self->glog("Sequence $S.$T has a different $name value: was $targetinfo->{$name}, now $sourceinfo->{$name}", LOG_VERBOSE);
  6217. }
  6218. ## If this is a boolean setting, we want to simply prepend a 'NO' for false
  6219. if ($syntax =~ s/BOOL //) {
  6220. push @alter => sprintf '%s%s',
  6221. $sourceinfo->{$name} ? '' : 'NO ',
  6222. $syntax;
  6223. }
  6224. else {
  6225. push @alter => "$syntax $sourceinfo->{$name}";
  6226. }
  6227. $changes++;
  6228. } ## end each sequence column
  6229. if (@alter) {
  6230. $SQL = "ALTER SEQUENCE $S.$T ";
  6231. $SQL .= join ' ' => @alter;
  6232. $self->glog("Running on target $dbname: $SQL", LOG_DEBUG);
  6233. $x->{dbh}->do($SQL);
  6234. }
  6235. } ## end each database
  6236. return $changes;
  6237. } ## end of adjust_sequence
  6238. sub run_kid_custom_code {
  6239. ## Prepare and then run the custom code subroutine
  6240. ## Arguments: two
  6241. ## 1. Sync information
  6242. ## 2. This code information
  6243. ## Returns: status code, one of 'redo', 'last', 'retry', or 'normal'
  6244. ## May also throw an exception if the calling code requests it
  6245. my $self = shift;
  6246. my $sync = shift;
  6247. my $c = shift;
  6248. $self->glog("Running $c->{whenrun} custom code $c->{id}: $c->{name}", LOG_NORMAL);
  6249. ## Create a hash of information common to all customcodes
  6250. my $info = {
  6251. syncname => $sync->{name},
  6252. version => $self->{version}, ## Version of Bucardo
  6253. sourcename => $sync->{sourcedb},
  6254. targetname => $sync->{targetname},
  6255. message => '', ## Allows the code to send a message to the logs
  6256. warning => '', ## Allows a warning to be thrown by the code
  6257. error => '', ## Allows an exception to be thrown by the code
  6258. lastcode => '', ## Tells the caller to skip any other codes of this type
  6259. endsync => '', ## Tells the caller to cancel the whole sync
  6260. sendmail => sub { $self->send_mail(@_) },
  6261. };
  6262. ## Add in any items custom to this code
  6263. if (exists $c->{info}) {
  6264. for my $key (keys %{ $c->{info} }) {
  6265. $info->{$key} = $c->{info}{$key};
  6266. }
  6267. delete $c->{info};
  6268. }
  6269. ## Make a copy of what we send them, so we can safely pull back info later
  6270. my $infocopy = {};
  6271. for (keys %$info) {
  6272. $infocopy->{$_} = $info->{$_};
  6273. }
  6274. ## If they need database handles, provide them
  6275. if ($c->{getdbh}) {
  6276. my $strict = ($c->{whenrun} eq 'before_txn' or $c->{whenrun} eq 'after_txn') ? 1 : 0;
  6277. for my $dbname (keys %{ $sync->{db} }) {
  6278. $info->{dbh}{$dbname} = $strict ? $self->{safe_dbh}{$dbname}
  6279. : $self->{safe_dbh_strict}{$dbname};
  6280. }
  6281. }
  6282. ## Set all databases' InactiveDestroy to on, so the customcode doesn't mess things up
  6283. for my $dbname (keys %{ $sync->{db} }) {
  6284. $sync->{db}{$dbname}{dbh}->{InactiveDestroy} = 1;
  6285. }
  6286. ## Run the actual code!
  6287. local $_ = $info;
  6288. $c->{coderef}->($info);
  6289. $self->glog("Finished custom code $c->{id}", LOG_VERBOSE);
  6290. for my $dbname (keys %{ $sync->{db} }) {
  6291. $sync->{db}{$dbname}{dbh}->{InactiveDestroy} = 0;
  6292. }
  6293. ## Check for any messages set by the custom code
  6294. if (length $info->{message}) {
  6295. $self->glog("Message from $c->{whenrun} code $c->{id}: $info->{message}", LOG_TERSE);
  6296. }
  6297. ## Check for any warnings set by the custom code
  6298. if (length $info->{warning}) {
  6299. $self->glog("Warning! Code $c->{whenrun} $c->{id}: $info->{warning}", LOG_WARN);
  6300. }
  6301. ## Check for any errors set by the custom code. Throw an exception if found.
  6302. if (length $info->{error}) {
  6303. $self->glog("Warning! Code $c->{whenrun} $c->{id}: $info->{error}", LOG_WARN);
  6304. die "Code $c->{whenrun} $c->{id} error: $info->{error}";
  6305. }
  6306. ## Check for a request to end the sync.
  6307. ## If found, rollback, adjust the Q, and redo the kid
  6308. if (length $info->{endsync}) {
  6309. $self->glog("Code $c->{whenrun} requests a cancellation of the rest of the sync", LOG_TERSE);
  6310. ## before_txn and after_txn should commit themselves
  6311. for my $dbname (keys %{ $sync->{db} }) {
  6312. $sync->{db}{$dbname}{dbh}->rollback();
  6313. }
  6314. my $syncname = $infocopy->{syncname};
  6315. my $targetname = $infocopy->{targetname};
  6316. $sth{qend}->execute(0,0,0,$syncname,$targetname,$$);
  6317. my $notify = "bucardo_syncdone_${syncname}_$targetname";
  6318. my $maindbh = $self->{masterdbh};
  6319. $self->db_notify($maindbh, $notify);
  6320. sleep $config{endsync_sleep};
  6321. redo KID;
  6322. }
  6323. ## The custom code has requested we retry this sync (exception code only)
  6324. if (exists $info->{retry} and $info->{retry}) {
  6325. return 'retry';
  6326. }
  6327. ## The custom code has requested we don't call any other codes of the same type
  6328. if (length $info->{lastcode}) {
  6329. return 'last';
  6330. }
  6331. ## Default action, which usually means the next code in the list, if any
  6332. return 'normal';
  6333. } ## end of run_kid_custom_code
  6334. sub custom_conflict {
  6335. ## Arguments: one
  6336. ## 1. Hashref of info about the state of affairs
  6337. ## - table: hashref of info about the current goat
  6338. ## - schema, table
  6339. ## - key: null-joined primary key causing the problem
  6340. ## - sourcedbh, targetdbh
  6341. ## Returns: action -1=nobody wins 1=source wins 2=target wins
  6342. my ($self,$arg) = @_;
  6343. my $ginfo = $arg->{table};
  6344. my $sync = $arg->{sync};
  6345. for my $code (@{$ginfo->{code_conflict}}) {
  6346. my $result = $self->run_kid_custom_code($sync, $code);
  6347. if ($result eq 'next') {
  6348. $self->glog('Going to next available conflict code', LOG_DEBUG);
  6349. next;
  6350. }
  6351. $self->glog("Conflict handler action: $result", LOG_DEBUG);
  6352. return $result;
  6353. }
  6354. return 0; ## Will fail, as we should not get here!
  6355. } ## end of custom_conflict
  6356. sub truncate_table {
  6357. ## Given a table, attempt to truncate it
  6358. ## Arguments: three
  6359. ## 1. Database object
  6360. ## 2. Table name
  6361. ## 3. Boolean if we should CASCADE the truncate or not
  6362. ## Returns: true if the truncate succeeded without error, false otherwise
  6363. my ($self, $x, $tname, $cascade) = @_;
  6364. ## Override any existing handlers so we can cleanly catch the eval
  6365. local $SIG{__DIE__} = sub {};
  6366. if ($x->{does_sql}) {
  6367. if ($x->{does_savepoints}) {
  6368. $x->{dbh}->do('SAVEPOINT truncate_attempt');
  6369. }
  6370. $SQL = sprintf 'TRUNCATE TABLE %s%s',
  6371. $tname,
  6372. ($cascade and $x->{does_cascade}) ? ' CASCADE' : '';
  6373. my $truncate_ok = 0;
  6374. eval {
  6375. $x->{dbh}->do($SQL);
  6376. $truncate_ok = 1;
  6377. };
  6378. if (! $truncate_ok) {
  6379. $x->{does_savepoints} and $x->{dbh}->do('ROLLBACK TO truncate_attempt');
  6380. $self->glog("Truncate error for db $x->{name}.$x->{dbname}.$tname: $@", LOG_NORMAL);
  6381. return 0;
  6382. }
  6383. else {
  6384. $x->{does_savepoints} and $x->{dbh}->do('RELEASE truncate_attempt');
  6385. return 1;
  6386. }
  6387. }
  6388. if ('mongo' eq $x->{dbtype}) {
  6389. my $collection = $x->{dbh}->get_collection($tname);
  6390. $collection->remove({}, { safe => 1} );
  6391. return 1;
  6392. }
  6393. elsif ('redis' eq $x->{dbtype}) {
  6394. ## No real equivalent here, as we do not map tables 1:1 to redis keys
  6395. ## In theory, we could walk through all keys and delete ones that match the table
  6396. ## We will hold off until someone actually needs that, however :)
  6397. return 1;
  6398. }
  6399. return undef;
  6400. } ## end of truncate_table
  6401. sub delete_table {
  6402. ## Given a table, attempt to unconditionally delete rows from it
  6403. ## Arguments: two
  6404. ## 1. Database object
  6405. ## 2. Table name
  6406. ## Returns: number of rows deleted
  6407. my ($self, $x, $tname) = @_;
  6408. my $count = 0;
  6409. if ($x->{does_sql}) {
  6410. ($count = $x->{dbh}->do("DELETE FROM $tname")) =~ s/0E0/0/o;
  6411. }
  6412. elsif ('mongo' eq $x->{dbtype}) {
  6413. ## Same as truncate, really, except we return the number of rows
  6414. my $collection = $x->{dbh}->get_collection($tname);
  6415. my $res = $collection->remove({}, { safe => 1} );
  6416. $count = $res->{n};
  6417. }
  6418. elsif ('redis' eq $x->{dbtype}) {
  6419. ## Nothing relevant here, as the table is only part of the key name
  6420. }
  6421. else {
  6422. die "Do not know how to delete a dbtype of $x->{dbtype}";
  6423. }
  6424. return $count;
  6425. } ## end of delete_table
  6426. sub delete_rows {
  6427. ## Given a list of rows, delete them from a database
  6428. ## Arguments: six
  6429. ## 1. Hash of rows, where the key is \0 joined pkeys
  6430. ## 2. Schema name
  6431. ## 3. Table name
  6432. ## 4. Goat object
  6433. ## 5. Sync object
  6434. ## 6. Target database object, or arrayref of the same
  6435. ## Returns: number of rows deleted
  6436. my ($self,$rows,$S,$T,$goat,$sync,$deldb) = @_;
  6437. my $syncname = $sync->{name};
  6438. my $pkcols = $goat->{pkeycols};
  6439. my $pkcolsraw = $goat->{pkeycolsraw};
  6440. my $numpks = $goat->{numpkcols};
  6441. ## Keep track of exact number of rows deleted from each target
  6442. my %count;
  6443. ## Allow for non-arrays by forcing to an array
  6444. if (ref $deldb ne 'ARRAY') {
  6445. $deldb = [$deldb];
  6446. }
  6447. my $newname = $goat->{newname}{$self->{syncname}};
  6448. ## Have we already truncated this table? If yes, skip and reset the flag
  6449. if (exists $goat->{truncatewinner}) {
  6450. return 0;
  6451. }
  6452. ## Are we truncating?
  6453. if (exists $self->{truncateinfo}{$S}{$T}) {
  6454. ## Try and truncate each target
  6455. for my $t (@$deldb) {
  6456. my $type = $t->{dbtype};
  6457. my $tname = $newname->{$t->{name}};
  6458. ## Postgres is a plain and simple TRUNCATE, with an async flag
  6459. ## TRUNCATE CASCADE is not needed as everything should be in one
  6460. ## sync (herd), and we have turned all FKs off
  6461. if ('postgres' eq $type) {
  6462. my $tdbh = $t->{dbh};
  6463. $tdbh->do("TRUNCATE table $tname", { pg_async => PG_ASYNC });
  6464. } ## end postgres database
  6465. ## For all other SQL databases, we simply truncate
  6466. elsif ($x->{does_sql}) {
  6467. $t->{dbh}->do("TRUNCATE TABLE $tname");
  6468. }
  6469. ## For MongoDB, we simply remove everything from the collection
  6470. ## This keeps the indexes around (which is why we don't "drop")
  6471. elsif ('mongo' eq $type) {
  6472. $self->{collection} = $t->{dbh}->get_collection($tname);
  6473. $self->{collection}->remove({}, { safe => 1} );
  6474. next;
  6475. }
  6476. ## For Redis, do nothing
  6477. elsif ('redis' eq $type) {
  6478. next;
  6479. }
  6480. ## For flatfiles, write out a basic truncate statement
  6481. elsif ($type =~ /flat/o) {
  6482. printf {$t->{filehandle}} qq{TRUNCATE TABLE %S;\n\n},
  6483. 'flatpg' eq $type ? $tname : $tname;
  6484. $self->glog(qq{Appended to flatfile "$t->{filename}"}, LOG_VERBOSE);
  6485. }
  6486. } ## end each database to be truncated
  6487. ## Final cleanup for each target
  6488. for my $t (@$deldb) {
  6489. my $type = $t->{dbtype};
  6490. if ('postgres' eq $type) {
  6491. ## Wrap up all the async truncate call
  6492. $t->{dbh}->pg_result();
  6493. }
  6494. }
  6495. return 0;
  6496. } ## end truncation
  6497. ## The number of items before we break it into a separate statement
  6498. ## This is inexact, as we don't know how large each key is,
  6499. ## but should be good enough as long as not set too high.
  6500. my $chunksize = $config{statement_chunk_size} || 10_000;
  6501. ## Setup our deletion SQL as needed
  6502. my %SQL;
  6503. for my $t (@$deldb) {
  6504. my $type = $t->{dbtype};
  6505. ## No special preparation for mongo or redis
  6506. next if $type =~ /mongo|redis/;
  6507. ## Set the type of SQL we are using: IN vs ANY
  6508. my $sqltype = '';
  6509. if ('postgres' eq $type) {
  6510. $sqltype = (1 == $numpks) ? 'ANY' : 'IN';
  6511. }
  6512. elsif ('mysql' eq $type or 'drizzle' eq $type or 'mariadb' eq $type) {
  6513. $sqltype = 'MYIN';
  6514. }
  6515. elsif ('oracle' eq $type) {
  6516. $sqltype = 'IN';
  6517. }
  6518. elsif ('sqlite' eq $type) {
  6519. $sqltype = 'IN';
  6520. }
  6521. elsif ($type =~ /flatpg/o) {
  6522. ## XXX Worth the trouble to allow building an ANY someday for flatpg?
  6523. $sqltype = 'IN';
  6524. }
  6525. elsif ($type =~ /flat/o) {
  6526. $sqltype = 'IN';
  6527. }
  6528. my $tname = $newname->{$t->{name}};
  6529. ## We may want to break this up into separate rounds if large
  6530. my $round = 0;
  6531. ## Internal counter of how many items we've processed this round
  6532. my $roundtotal = 0;
  6533. ## Postgres-specific optimization for a single primary key:
  6534. if ($sqltype eq 'ANY') {
  6535. $SQL{ANY}{$tname} ||= "$self->{sqlprefix}DELETE FROM $tname WHERE $pkcols = ANY(?)";
  6536. ## The array where we store each chunk
  6537. my @SQL;
  6538. for my $key (keys %$rows) {
  6539. push @{$SQL[$round]} => length $key ? ([split '\0', $key, -1]) : [''];
  6540. if (++$roundtotal >= $chunksize) {
  6541. $roundtotal = 0;
  6542. $round++;
  6543. }
  6544. }
  6545. $SQL{ANYargs} = \@SQL;
  6546. }
  6547. ## Normal DELETE call with IN() clause
  6548. elsif ($sqltype eq 'IN') {
  6549. $SQL = sprintf '%sDELETE FROM %s WHERE %s IN (',
  6550. $self->{sqlprefix},
  6551. $tname,
  6552. $pkcols;
  6553. ## The array where we store each chunk
  6554. my @SQL;
  6555. for my $key (keys %$rows) {
  6556. my $inner = length $key
  6557. ? (join ',' => map { s/\'/''/go; s{\\}{\\\\}; qq{'$_'}; } split '\0', $key, -1)
  6558. : q{''};
  6559. $SQL[$round] .= "($inner),";
  6560. if (++$roundtotal >= $chunksize) {
  6561. $roundtotal = 0;
  6562. $round++;
  6563. }
  6564. }
  6565. ## Cleanup
  6566. for (@SQL) {
  6567. chop;
  6568. $_ = "$SQL $_)";
  6569. }
  6570. $SQL{IN} = \@SQL;
  6571. }
  6572. ## MySQL IN clause
  6573. elsif ($sqltype eq 'MYIN') {
  6574. (my $safepk = $pkcols) =~ s/\"/`/go;
  6575. $SQL = sprintf '%sDELETE FROM %s WHERE %s IN (',
  6576. $self->{sqlprefix},
  6577. $tname,
  6578. $safepk;
  6579. ## The array where we store each chunk
  6580. my @SQL;
  6581. ## Quick workaround for a more standard timestamp
  6582. if ($goat->{pkeytype}[0] =~ /timestamptz/) {
  6583. for my $key (keys %$rows) {
  6584. my $inner = length $key
  6585. ? (join ',' => map { s/\'/''/go; s{\\}{\\\\}; s/\+\d\d$//; qq{'$_'}; } split '\0', $key, -1)
  6586. : q{''};
  6587. $SQL[$round] .= "($inner),";
  6588. if (++$roundtotal >= $chunksize) {
  6589. $roundtotal = 0;
  6590. $round++;
  6591. }
  6592. }
  6593. }
  6594. else {
  6595. for my $key (keys %$rows) {
  6596. my $inner = length $key
  6597. ? (join ',' => map { s/\'/''/go; s{\\}{\\\\}; qq{'$_'}; } split '\0', $key, -1)
  6598. : q{''};
  6599. $SQL[$round] .= "($inner),";
  6600. if (++$roundtotal >= $chunksize) {
  6601. $roundtotal = 0;
  6602. $round++;
  6603. }
  6604. }
  6605. }
  6606. ## Cleanup
  6607. for (@SQL) {
  6608. chop;
  6609. $_ = "$SQL $_)";
  6610. }
  6611. $SQL{MYIN} = \@SQL;
  6612. }
  6613. }
  6614. ## Do each target in turn
  6615. for my $t (@$deldb) {
  6616. my $type = $t->{dbtype};
  6617. my $tname = $newname->{$t->{name}};
  6618. if ('postgres' eq $type) {
  6619. my $tdbh = $t->{dbh};
  6620. ## Only the last will be async
  6621. ## In most cases, this means always async
  6622. my $count = 1==$numpks ? @{ $SQL{ANYargs} } : @{ $SQL{IN} };
  6623. for my $loop (1..$count) {
  6624. my $async = $loop==$count ? PG_ASYNC : 0;
  6625. my $pre = $count > 1 ? "/* $loop of $count */ " : '';
  6626. if (1 == $numpks) {
  6627. $t->{deletesth} = $tdbh->prepare("$pre$SQL{ANY}{$tname}", { pg_async => $async });
  6628. my $res = $t->{deletesth}->execute($SQL{ANYargs}->[$loop-1]);
  6629. $count{$t} += $res unless $async;
  6630. }
  6631. else {
  6632. $count{$t} += $tdbh->do($pre.$SQL{IN}->[$loop-1], { pg_direct => 1, pg_async => $async });
  6633. $t->{deletesth} = 0;
  6634. }
  6635. }
  6636. next;
  6637. } ## end postgres database
  6638. if ('mongo' eq $type) {
  6639. ## Grab the collection name and store it
  6640. $self->{collection} = $t->{dbh}->get_collection($tname);
  6641. ## Because we may have multi-column primary keys, and each key may need modifying,
  6642. ## we have to put everything into an array of arrays.
  6643. ## The first level is the primary key number, the next is the actual values
  6644. my @delkeys = [];
  6645. ## The pkcolsraw variable is a simple comma-separated list of PK column names
  6646. ## The rows variable is a hash with the PK values as keys (the values can be ignored)
  6647. ## Binary PKs are easy: all we have to do is decode
  6648. ## We can assume that binary PK means not a multi-column PK
  6649. if ($goat->{hasbinarypkey}) {
  6650. @{ $delkeys[0] } = map { decode_base64($_) } keys %$rows;
  6651. }
  6652. else {
  6653. ## Break apart the primary keys into an array of arrays
  6654. my @fullrow = map { length($_) ? [split '\0', $_, -1] : [''] } keys %$rows;
  6655. ## Which primary key column we are currently using
  6656. my $pknum = 0;
  6657. ## Walk through each column making up the primary key
  6658. for my $realpkname (split /,/, $pkcolsraw, -1) {
  6659. ## Grab what type this column is
  6660. ## We need to map non-strings to correct types as best we can
  6661. my $type = $goat->{columnhash}{$realpkname}{ftype};
  6662. ## For integers, we simply force to a Perlish int
  6663. if ($type =~ /smallint|integer|bigint/o) {
  6664. @{ $delkeys[$pknum] } = map { int $_->[$pknum] } @fullrow;
  6665. }
  6666. ## Non-integer numbers get set via the strtod command from the 'POSIX' module
  6667. elsif ($type =~ /real|double|numeric/o) {
  6668. @{ $delkeys[$pknum] } = map { strtod $_->[$pknum] } @fullrow;
  6669. }
  6670. ## Boolean becomes true Perlish booleans via the 'boolean' module
  6671. elsif ($type eq 'boolean') {
  6672. @{ $delkeys[$pknum] } = map { $_->[$pknum] eq 't' ? true : false } @fullrow;
  6673. }
  6674. ## Everything else gets a direct mapping
  6675. else {
  6676. @{ $delkeys[$pknum] } = map { $_->[$pknum] } @fullrow;
  6677. }
  6678. $pknum++;
  6679. }
  6680. } ## end of multi-column PKs
  6681. ## How many items we end up actually deleting
  6682. $count{$t} = 0;
  6683. ## We may need to batch these to keep the total message size reasonable
  6684. my $max = keys %$rows;
  6685. $max--;
  6686. ## The bottom of our current array slice
  6687. my $bottom = 0;
  6688. ## This loop limits the size of our delete requests to mongodb
  6689. MONGODEL: {
  6690. ## Calculate the current top of the array slice
  6691. my $top = $bottom + $chunksize;
  6692. ## Stop at the total number of rows
  6693. $top = $max if $top > $max;
  6694. ## If we have a single key, we can use the '$in' syntax
  6695. if ($numpks <= 1) {
  6696. my @newarray = @{ $delkeys[0] }[$bottom..$top];
  6697. my $result = $self->{collection}->remove(
  6698. {$pkcolsraw => { '$in' => \@newarray }}, { safe => 1 });
  6699. $count{$t} += $result->{n};
  6700. }
  6701. else {
  6702. ## For multi-column primary keys, we cannot use '$in', sadly.
  6703. ## Thus, we will just call delete once per row
  6704. ## Put the names into an easy to access array
  6705. my @realpknames = split /,/, $pkcolsraw, -1;
  6706. my @find;
  6707. ## Which row we are currently processing
  6708. my $numrows = scalar keys %$rows;
  6709. for my $rownumber (0..$numrows-1) {
  6710. for my $pknum (0..$numpks-1) {
  6711. push @find => $realpknames[$pknum], $delkeys[$pknum][$rownumber];
  6712. }
  6713. }
  6714. my $result = $self->{collection}->remove(
  6715. { '$and' => \@find }, { safe => 1 });
  6716. $count{$t} += $result->{n};
  6717. ## We do not need to loop, as we just went 1 by 1 through the whole list
  6718. last MONGODEL;
  6719. }
  6720. ## Bail out of the loop if we've hit the max
  6721. last MONGODEL if $top >= $max;
  6722. ## Assign the bottom of our array slice to be above the current top
  6723. $bottom = $top + 1;
  6724. redo MONGODEL;
  6725. }
  6726. $self->glog("Mongo objects removed from $tname: $count{$t}", LOG_VERBOSE);
  6727. next;
  6728. }
  6729. if ('mysql' eq $type or 'drizzle' eq $type or 'mariadb' eq $type) {
  6730. my $tdbh = $t->{dbh};
  6731. for (@{ $SQL{MYIN} }) {
  6732. ($count{$t} += $tdbh->do($_)) =~ s/0E0/0/o;
  6733. }
  6734. next;
  6735. }
  6736. if ('oracle' eq $type) {
  6737. my $tdbh = $t->{dbh};
  6738. for (@{ $SQL{IN} }) {
  6739. ($count{$t} += $tdbh->do($_)) =~ s/0E0/0/o;
  6740. }
  6741. next;
  6742. }
  6743. if ('redis' eq $type) {
  6744. ## We need to remove the entire tablename:pkey:column for each column we know about
  6745. my $cols = $goat->{cols};
  6746. for my $pk (keys %$rows) {
  6747. ## If this is a multi-column primary key, change our null delimiter to a colon
  6748. if ($goat->{numpkcols} > 1) {
  6749. $pk =~ s{\0}{:}go;
  6750. }
  6751. $count = $t->{dbh}->del("$tname:$pk");
  6752. }
  6753. next;
  6754. }
  6755. if ('sqlite' eq $type) {
  6756. my $tdbh = $t->{dbh};
  6757. for (@{ $SQL{IN} }) {
  6758. ($count{$t} += $tdbh->do($_)) =~ s/0E0/0/o;
  6759. }
  6760. next;
  6761. }
  6762. if ($type =~ /flat/o) { ## same as flatpg for now
  6763. for (@{ $SQL{IN} }) {
  6764. print {$t->{filehandle}} qq{$_;\n\n};
  6765. }
  6766. $self->glog(qq{Appended to flatfile "$t->{filename}"}, LOG_VERBOSE);
  6767. next;
  6768. }
  6769. die qq{No support for database type "$type" yet!};
  6770. }
  6771. ## Final cleanup as needed (e.g. process async results)
  6772. for my $t (@$deldb) {
  6773. my $type = $t->{dbtype};
  6774. if ('postgres' eq $type) {
  6775. my $tdbh = $t->{dbh};
  6776. ## Wrap up all the async queries
  6777. ($count{$t} += $tdbh->pg_result()) =~ s/0E0/0/o;
  6778. ## Call finish if this was a statement handle (as opposed to a do)
  6779. if ($t->{deletesth}) {
  6780. $t->{deletesth}->finish();
  6781. }
  6782. delete $t->{deletesth};
  6783. }
  6784. }
  6785. $count = 0;
  6786. for my $t (@$deldb) {
  6787. ## We do not delete from certain types of targets
  6788. next if $t->{dbtype} =~ /mongo|flat|redis/o;
  6789. my $tname = $newname->{$t->{name}};
  6790. $count += $count{$t};
  6791. $self->glog(qq{Rows deleted from $t->{name}.$tname: $count{$t}}, LOG_VERBOSE);
  6792. }
  6793. return $count;
  6794. } ## end of delete_rows
  6795. sub push_rows {
  6796. ## Copy rows from one database to another
  6797. ## Arguments: eight
  6798. ## 1. Hash of rows, where the key is \0 joined pkeys
  6799. ## 2. Schema name
  6800. ## 3. Table name
  6801. ## 4. Goat object
  6802. ## 5. Sync object
  6803. ## 6. Database handle we are copying from
  6804. ## 7. Database name we are copying from
  6805. ## 8. Target database object, or arrayref of the same
  6806. ## Returns: number of rows copied
  6807. my ($self,$rows,$S,$T,$goat,$sync,$fromdbh,$fromname,$todb) = @_;
  6808. my $syncname = $sync->{name};
  6809. my $pkcols = $goat->{pkeycols};
  6810. my $numpks = $goat->{numpkcols};
  6811. ## This may be a fullcopy. If it is, $rows will not be a hashref
  6812. ## If it is fullcopy, flip it to a dummy hashref
  6813. my $fullcopy = 0;
  6814. if (! ref $rows) {
  6815. if ($rows eq 'fullcopy') {
  6816. $fullcopy = 1;
  6817. $self->glog('Setting push_rows to fullcopy mode', LOG_DEBUG);
  6818. }
  6819. else {
  6820. die "Invalid rows passed to push_rows: $rows\n";
  6821. }
  6822. $rows = {};
  6823. }
  6824. ## This will be zero for fullcopy of course
  6825. my $total = keys %$rows;
  6826. ## Total number of rows written
  6827. $count = 0;
  6828. my $newname = $goat->{newname}{$self->{syncname}};
  6829. ## As with delete, we may break this into more than one step
  6830. ## Should only be a factor for very large numbers of keys
  6831. my $chunksize = $config{statement_chunk_size} || 10_000;
  6832. ## Build a list of all PK values to feed to IN clauses
  6833. my @pkvals;
  6834. my $round = 0;
  6835. my $roundtotal = 0;
  6836. for my $key (keys %$rows) {
  6837. my $inner = length $key
  6838. ? (join ',' => map { s{\'}{''}go; s{\\}{\\\\}go; qq{'$_'}; } split '\0', $key, -1)
  6839. : q{''};
  6840. push @{ $pkvals[$round] ||= [] } => $numpks > 1 ? "($inner)" : $inner;
  6841. if (++$roundtotal >= $chunksize) {
  6842. $roundtotal = 0;
  6843. $round++;
  6844. }
  6845. }
  6846. ## Example: 1234, 221
  6847. ## Example MCPK: ('1234','Don''t Stop','2008-01-01'),('221','foobar','2008-11-01')
  6848. ## Allow for non-arrays by forcing to an array
  6849. if (ref $todb ne 'ARRAY') {
  6850. $todb = [$todb];
  6851. }
  6852. ## This can happen if we truncated but had no delta activity
  6853. return 0 if (! $pkvals[0] or ! length $pkvals[0]->[0] ) and ! $fullcopy;
  6854. ## Get ready to export from the source
  6855. ## This may have multiple versions depending on the customcols table
  6856. my $newcols = $goat->{newcols}{$syncname} || {};
  6857. ## Walk through and grab which SQL is needed for each target
  6858. ## Cache this earlier on - controller?
  6859. my %srccmd;
  6860. for my $t (@$todb) {
  6861. ## The SELECT clause we use (may be empty)
  6862. my $clause = $newcols->{$t->{name}};
  6863. ## Associate this target with this clause
  6864. push @{$srccmd{$clause}} => $t;
  6865. }
  6866. ## Loop through each source command and push it out to all targets
  6867. ## that are associated with it
  6868. for my $clause (sort keys %srccmd) {
  6869. ## Build the clause (cache) and kick it off
  6870. my $SELECT = $clause || 'SELECT *';
  6871. ## Prepare each target in turn
  6872. for my $t (@{ $srccmd{$clause} }) {
  6873. ## Internal name of this target
  6874. my $targetname = $t->{name};
  6875. ## Name of the table we are pushing to on this target
  6876. my $tname = $newname->{$targetname};
  6877. ## The columns we are pushing to, both as an arrayref and a CSV:
  6878. my $cols = $goat->{tcolumns}{$SELECT};
  6879. my $columnlist = $t->{does_sql} ?
  6880. ('(' . (join ',', map { $t->{dbh}->quote_identifier($_) } @$cols) . ')')
  6881. : ('(' . (join ',', map { $_ } @$cols) . ')');
  6882. my $type = $t->{dbtype};
  6883. ## Use columnlist below so we never have to worry about the order
  6884. ## of the columns on the target
  6885. if ('postgres' eq $type) {
  6886. my $tgtcmd = "$self->{sqlprefix}COPY $tname$columnlist FROM STDIN";
  6887. $t->{dbh}->do($tgtcmd);
  6888. }
  6889. elsif ('flatpg' eq $type) {
  6890. print {$t->{filehandle}} "COPY $tname$columnlist FROM STDIN;\n";
  6891. $self->glog(qq{Appended to flatfile "$t->{filename}"}, LOG_VERBOSE);
  6892. }
  6893. elsif ('flatsql' eq $type) {
  6894. print {$t->{filehandle}} "INSERT INTO $tname$columnlist VALUES\n";
  6895. $self->glog(qq{Appended to flatfile "$t->{filename}"}, LOG_VERBOSE);
  6896. }
  6897. elsif ('mongo' eq $type) {
  6898. $self->{collection} = $t->{dbh}->get_collection($tname);
  6899. }
  6900. elsif ('redis' eq $type) {
  6901. ## No prep needed, other than to reset our count of changes
  6902. $t->{redis} = 0;
  6903. }
  6904. elsif ('mysql' eq $type or 'drizzle' eq $type or 'mariadb' eq $type) {
  6905. my $tgtcmd = "INSERT INTO $tname$columnlist VALUES (";
  6906. $tgtcmd .= '?,' x @$cols;
  6907. $tgtcmd =~ s/,$/)/o;
  6908. $t->{sth} = $t->{dbh}->prepare($tgtcmd);
  6909. }
  6910. elsif ('oracle' eq $type) {
  6911. my $tgtcmd = "INSERT INTO $tname$columnlist VALUES (";
  6912. $tgtcmd .= '?,' x @$cols;
  6913. $tgtcmd =~ s/,$/)/o;
  6914. $t->{sth} = $t->{dbh}->prepare($tgtcmd);
  6915. }
  6916. elsif ('sqlite' eq $type) {
  6917. my $tgtcmd = "INSERT INTO $tname$columnlist VALUES (";
  6918. $tgtcmd .= '?,' x @$cols;
  6919. $tgtcmd =~ s/,$/)/o;
  6920. $t->{sth} = $t->{dbh}->prepare($tgtcmd);
  6921. }
  6922. else {
  6923. die qq{No support for database type "$type" yet!};
  6924. }
  6925. } ## end preparing each target for this clause
  6926. ## Put dummy data into @pkvals if using fullcopy
  6927. if ($fullcopy) {
  6928. push @pkvals => ['fullcopy'];
  6929. }
  6930. my $loop = 1;
  6931. my $pcount = @pkvals;
  6932. ## Loop through each chunk of primary keys to copy over
  6933. for my $pk_values (@pkvals) {
  6934. my $pkvs = join ',' => @{ $pk_values };
  6935. ## Message to prepend to the statement if chunking
  6936. my $pre = $pcount <= 1 ? '' : "/* $loop of $pcount */";
  6937. $loop++;
  6938. ## Kick off the copy on the source
  6939. my $srccmd = sprintf '%s%sCOPY (%s FROM %s.%s%s) TO STDOUT%s',
  6940. $pre,
  6941. $self->{sqlprefix},
  6942. $SELECT,
  6943. $S,
  6944. $T,
  6945. $fullcopy ? '' : " WHERE $pkcols IN ($pkvs)",
  6946. $sync->{copyextra} ? " $sync->{copyextra}" : '';
  6947. $fromdbh->do($srccmd);
  6948. my $buffer = '';
  6949. $self->glog(qq{Copying from $fromname.$S.$T}, LOG_VERBOSE);
  6950. ## Loop through all changed rows on the source, and push to the target(s)
  6951. my $multirow = 0;
  6952. ## If in fullcopy mode, we don't know how many rows will get copied,
  6953. ## so we count as we go along
  6954. if ($fullcopy) {
  6955. $total = 0;
  6956. }
  6957. ## Loop through each row output from the source, storing it in $buffer
  6958. while ($fromdbh->pg_getcopydata($buffer) >= 0) {
  6959. $total++ if $fullcopy;
  6960. ## For each target using this particular COPY statement
  6961. for my $t (@{ $srccmd{$clause} }) {
  6962. my $type = $t->{dbtype};
  6963. my $cols = $goat->{tcolumns}{$SELECT};
  6964. my $tname = $newname->{$t->{name}};
  6965. chomp $buffer;
  6966. ## For Postgres, we simply do COPY to COPY
  6967. if ('postgres' eq $type) {
  6968. $t->{dbh}->pg_putcopydata("$buffer\n");
  6969. }
  6970. ## For flat files destined for Postgres, just do a tab-delimited dump
  6971. elsif ('flatpg' eq $type) {
  6972. print {$t->{filehandle}} "$buffer\n";
  6973. }
  6974. ## For other flat files, make a standard VALUES list
  6975. elsif ('flatsql' eq $type) {
  6976. if ($multirow++) {
  6977. print {$t->{filehandle}} ",\n";
  6978. }
  6979. print {$t->{filehandle}} '(' .
  6980. (join ',' => map { $self->{masterdbh}->quote($_) } split /\t/, $buffer, -1) . ')';
  6981. }
  6982. ## For Mongo, do some mongomagic
  6983. elsif ('mongo' eq $type) {
  6984. ## Have to map these values back to their names
  6985. my @cols = map { $_ = undef if $_ eq '\\N'; $_; } split /\t/, $buffer, -1;
  6986. ## Our object consists of the primary keys, plus all other fields
  6987. my $object = {};
  6988. for my $cname (@{ $cols }) {
  6989. $object->{$cname} = shift @cols;
  6990. }
  6991. ## Coerce non-strings into different objects
  6992. for my $key (keys %$object) {
  6993. ## Since mongo is schemaless, don't set null columns in the mongo doc
  6994. if (!defined($object->{$key})) {
  6995. delete $object->{$key};
  6996. }
  6997. elsif ($goat->{columnhash}{$key}{ftype} =~ /smallint|integer|bigint/o) {
  6998. $object->{$key} = int $object->{$key};
  6999. }
  7000. elsif ($goat->{columnhash}{$key}{ftype} eq 'boolean') {
  7001. if (defined $object->{$key}) {
  7002. $object->{$key} = $object->{$key} eq 't' ? true : false;
  7003. }
  7004. }
  7005. elsif ($goat->{columnhash}{$key}{ftype} =~ /real|double|numeric/o) {
  7006. $object->{$key} = strtod($object->{$key});
  7007. }
  7008. }
  7009. $self->{collection}->insert($object, { safe => 1 });
  7010. }
  7011. ## For MySQL, MariaDB, Drizzle, Oracle, and SQLite, do some basic INSERTs
  7012. elsif ('mysql' eq $type
  7013. or 'mariadb' eq $type
  7014. or 'drizzle' eq $type
  7015. or 'oracle' eq $type
  7016. or 'sqlite' eq $type) {
  7017. my @cols = map { $_ = undef if $_ eq '\\N'; $_; } split /\t/, $buffer, -1;
  7018. for my $cindex (0..@cols) {
  7019. next unless defined $cols[$cindex];
  7020. if ($goat->{columnhash}{$cols->[$cindex]}{ftype} eq 'boolean') {
  7021. # BOOLEAN support is inconsistent, but almost everyone will coerce 1/0 to TRUE/FALSE
  7022. $cols[$cindex] = ( $cols[$cindex] =~ /^[1ty]/i )? 1 : 0;
  7023. }
  7024. }
  7025. $count += $t->{sth}->execute(@cols);
  7026. }
  7027. elsif ('redis' eq $type) {
  7028. ## We are going to set a Redis hash, in which the key is "tablename:pkeyvalue"
  7029. my @colvals = map { $_ = undef if $_ eq '\\N'; $_; } split /\t/, $buffer, -1;
  7030. my @pkey;
  7031. for (1 .. $goat->{numpkcols}) {
  7032. push @pkey => shift @colvals;
  7033. }
  7034. my $pkeyval = join ':' => @pkey;
  7035. ## Build a list of non-null key/value pairs to set in the hash
  7036. my @add;
  7037. my $x = $goat->{numpkcols} - 1;
  7038. for my $val (@colvals) {
  7039. $x++;
  7040. next if ! defined $val;
  7041. push @add, $cols->[$x], $val;
  7042. }
  7043. $t->{dbh}->hmset("$tname:$pkeyval", @add);
  7044. $count++;
  7045. $t->{redis}++;
  7046. }
  7047. } ## end each target
  7048. } ## end each row pulled from the source
  7049. } ## end each pklist
  7050. ## Workaround for DBD::Pg bug
  7051. ## Once we require a minimum version of 2.18.1 or better, we can remove this!
  7052. if ($self->{dbdpgversion} < 21801) {
  7053. $fromdbh->do('SELECT 1');
  7054. }
  7055. ## Perform final cleanups for each target
  7056. for my $t (@{ $srccmd{$clause} }) {
  7057. my $type = $t->{dbtype};
  7058. my $tname = $newname->{$t->{name}};
  7059. if ('postgres' eq $type) {
  7060. my $dbh = $t->{dbh};
  7061. $dbh->pg_putcopyend();
  7062. ## Same bug as above
  7063. if ($self->{dbdpgversion} < 21801) {
  7064. $dbh->do('SELECT 1');
  7065. }
  7066. $self->glog(qq{Rows copied to $t->{name}.$tname: $total}, LOG_VERBOSE);
  7067. $count += $total;
  7068. ## If this table is set to makedelta, add rows to bucardo.delta to simulate the
  7069. ## normal action of a trigger and add a row to bucardo.track to indicate that
  7070. ## it has already been replicated here.
  7071. my $dbinfo = $sync->{db}{ $t->{name} };
  7072. if (!$fullcopy and exists $dbinfo->{is_makedelta}{$S}{$T}) {
  7073. my ($cols, $vals);
  7074. if ($numpks == 1) {
  7075. $cols = "($pkcols)";
  7076. $vals = join ',', map { "($_)" } map { @{ $_ } } @pkvals;
  7077. } else {
  7078. $cols = $pkcols;
  7079. $vals = join ',', map { @{ $_ } } @pkvals;
  7080. }
  7081. $dbh->do(qq{
  7082. INSERT INTO bucardo.$goat->{deltatable} $cols
  7083. VALUES $vals
  7084. });
  7085. # Make sure we track it!
  7086. $dbh->do(qq{
  7087. INSERT INTO bucardo.$goat->{tracktable}
  7088. VALUES (NOW(), ?)
  7089. }, undef, $t->{TARGETNAME});
  7090. }
  7091. }
  7092. elsif ('flatpg' eq $type) {
  7093. print {$t->{filehandle}} "\\\.\n\n";
  7094. }
  7095. elsif ('flatsql' eq $type) {
  7096. print {$t->{filehandle}} ";\n\n";
  7097. }
  7098. elsif ('redis' eq $type) {
  7099. $self->glog(qq{Rows copied to Redis $t->{name}.$tname:<pkeyvalue>: $t->{redis}}, LOG_VERBOSE);
  7100. }
  7101. }
  7102. } ## end of each clause in the source command list
  7103. return $count;
  7104. } ## end of push_rows
  7105. sub vacuum_table {
  7106. ## Compact and/or optimize the table in the target database
  7107. ## Argument: five
  7108. ## 1. Starting time for the kid, so we can output cumulative times
  7109. ## 2. Database type
  7110. ## 3. Database handle
  7111. ## 4. Database name
  7112. ## 5. Table name (may be in schema.table format)
  7113. ## Returns: undef
  7114. my ($self, $start_time, $dbtype, $ldbh, $dbname, $tablename) = @_;
  7115. ## XXX Return output from vacuum/optimize as a LOG_VERBOSE or LOG_DEBUG?
  7116. if ('postgres' eq $dbtype) {
  7117. ## Do a normal vacuum of the table
  7118. $ldbh->commit();
  7119. $ldbh->{AutoCommit} = 1;
  7120. $self->glog("Vacuuming $dbname.$tablename", LOG_VERBOSE);
  7121. $ldbh->do("VACUUM $tablename");
  7122. $ldbh->{AutoCommit} = 0;
  7123. my $total_time = sprintf '%.2f', tv_interval($start_time);
  7124. $self->glog("Vacuum complete. Time: $total_time", LOG_VERBOSE);
  7125. }
  7126. elsif ('mysql' eq $dbtype or 'drizzle' eq $dbtype or 'mariadb' eq $dbtype) {
  7127. ## Optimize the table
  7128. $self->glog("Optimizing $tablename", LOG_VERBOSE);
  7129. $ldbh->do("OPTIMIZE TABLE $tablename");
  7130. $ldbh->commit();
  7131. my $total_time = sprintf '%.2f', tv_interval($start_time);
  7132. $self->glog("Optimization complete. Time: $total_time", LOG_VERBOSE);
  7133. }
  7134. elsif ('sqlite' eq $dbtype) {
  7135. # Note the SQLite command vacuums the entire database.
  7136. # Should probably avoid multi-vacuuming if several tables have changed.
  7137. $self->glog('Vacuuming the database', LOG_VERBOSE);
  7138. $ldbh->do('VACUUM');
  7139. my $total_time = sprintf '%.2f', tv_interval($start_time);
  7140. $self->glog("Vacuum complete. Time: $total_time", LOG_VERBOSE);
  7141. }
  7142. elsif ('redis' eq $dbtype) {
  7143. # Nothing to do, really
  7144. }
  7145. elsif ('mongodb' eq $dbtype) {
  7146. # Use db.repairDatabase() ?
  7147. }
  7148. else {
  7149. ## Do nothing!
  7150. }
  7151. return;
  7152. } ## end of vacuum_table
  7153. sub analyze_table {
  7154. ## Update table statistics in the target database
  7155. ## Argument: five
  7156. ## 1. Starting time for the kid, so we can output cumulative times
  7157. ## 2. Database type
  7158. ## 3. Database handle
  7159. ## 4. Database name
  7160. ## 5. Table name (may be in schema.table format)
  7161. ## Returns: undef
  7162. my ($self, $start_time, $dbtype, $ldbh, $dbname, $tablename) = @_;
  7163. ## XXX Return output from analyze as a LOG_VERBOSE or LOG_DEBUG?
  7164. if ('postgres' eq $dbtype) {
  7165. $ldbh->do("ANALYZE $tablename");
  7166. my $total_time = sprintf '%.2f', tv_interval($start_time);
  7167. $self->glog("Analyze complete for $dbname.$tablename. Time: $total_time", LOG_VERBOSE);
  7168. $ldbh->commit();
  7169. }
  7170. elsif ('sqlite' eq $dbtype) {
  7171. $ldbh->do("ANALYZE $tablename");
  7172. my $total_time = sprintf '%.2f', tv_interval($start_time);
  7173. $self->glog("Analyze complete for $dbname.$tablename. Time: $total_time", LOG_VERBOSE);
  7174. $ldbh->commit();
  7175. }
  7176. elsif ('mysql' eq $dbtype or 'drizzle' eq $dbtype or 'mariadb' eq $dbtype) {
  7177. $ldbh->do("ANALYZE TABLE $tablename");
  7178. my $total_time = sprintf '%.2f', tv_interval($start_time);
  7179. $self->glog("Analyze complete for $tablename. Time: $total_time", LOG_VERBOSE);
  7180. $ldbh->commit();
  7181. }
  7182. else {
  7183. ## Nothing to do here
  7184. }
  7185. return undef;
  7186. } ## end of analyze_table
  7187. sub msg { ## no critic
  7188. my $name = shift || '?';
  7189. my $msg = '';
  7190. if (exists $msg{$lang}{$name}) {
  7191. $msg = $msg{$lang}{$name};
  7192. }
  7193. elsif (exists $msg{'en'}{$name}) {
  7194. $msg = $msg{'en'}{$name};
  7195. }
  7196. else {
  7197. my $line = (caller)[2];
  7198. die qq{Invalid message "$name" from line $line\n};
  7199. }
  7200. my $x=1;
  7201. {
  7202. my $val = $_[$x-1];
  7203. $val = '?' if ! defined $val;
  7204. last unless $msg =~ s/\$$x/$val/g;
  7205. $x++;
  7206. redo;
  7207. }
  7208. return $msg;
  7209. } ## end of msg
  7210. sub pretty_time {
  7211. ## Transform number of seconds to a more human-readable format
  7212. ## First argument is number of seconds
  7213. ## Second optional arg is highest transform: s,m,h,d,w
  7214. ## If uppercase, it indicates to "round that one out"
  7215. my $sec = shift;
  7216. my $tweak = shift || '';
  7217. ## Round to two decimal places, then trim the rest
  7218. $sec = sprintf '%.2f', $sec;
  7219. $sec =~ s/0+$//o;
  7220. $sec =~ s/\.$//o;
  7221. ## Just seconds (< 2:00)
  7222. if ($sec < 120 or $tweak =~ /s/) {
  7223. return sprintf "$sec %s", $sec==1 ? msg('time-second') : msg('time-seconds');
  7224. }
  7225. ## Minutes and seconds (< 60:00)
  7226. if ($sec < 60*60 or $tweak =~ /m/) {
  7227. my $min = int $sec / 60;
  7228. $sec %= 60;
  7229. my $ret = sprintf "$min %s", $min==1 ? msg('time-minute') : msg('time-minutes');
  7230. $sec and $tweak !~ /S/ and $ret .= sprintf " $sec %s", $sec==1 ? msg('time-second') : msg('time-seconds');
  7231. return $ret;
  7232. }
  7233. ## Hours, minutes, and seconds (< 48:00:00)
  7234. if ($sec < 60*60*24*2 or $tweak =~ /h/) {
  7235. my $hour = int $sec / (60*60);
  7236. $sec -= ($hour*60*60);
  7237. my $min = int $sec / 60;
  7238. $sec -= ($min*60);
  7239. my $ret = sprintf "$hour %s", $hour==1 ? msg('time-hour') : msg('time-hours');
  7240. $min and $tweak !~ /M/ and $ret .= sprintf " $min %s", $min==1 ? msg('time-minute') : msg('time-minutes');
  7241. $sec and $tweak !~ /[SM]/ and $ret .= sprintf " $sec %s", $sec==1 ? msg('time-second') : msg('time-seconds');
  7242. return $ret;
  7243. }
  7244. ## Days, hours, minutes, and seconds (< 28 days)
  7245. if ($sec < 60*60*24*28 or $tweak =~ /d/) {
  7246. my $day = int $sec / (60*60*24);
  7247. $sec -= ($day*60*60*24);
  7248. my $our = int $sec / (60*60);
  7249. $sec -= ($our*60*60);
  7250. my $min = int $sec / 60;
  7251. $sec -= ($min*60);
  7252. my $ret = sprintf "$day %s", $day==1 ? msg('time-day') : msg('time-days');
  7253. $our and $tweak !~ /H/ and $ret .= sprintf " $our %s", $our==1 ? msg('time-hour') : msg('time-hours');
  7254. $min and $tweak !~ /[HM]/ and $ret .= sprintf " $min %s", $min==1 ? msg('time-minute') : msg('time-minutes');
  7255. $sec and $tweak !~ /[HMS]/ and $ret .= sprintf " $sec %s", $sec==1 ? msg('time-second') : msg('time-seconds');
  7256. return $ret;
  7257. }
  7258. ## Weeks, days, hours, minutes, and seconds (< 28 days)
  7259. my $week = int $sec / (60*60*24*7);
  7260. $sec -= ($week*60*60*24*7);
  7261. my $day = int $sec / (60*60*24);
  7262. $sec -= ($day*60*60*24);
  7263. my $our = int $sec / (60*60);
  7264. $sec -= ($our*60*60);
  7265. my $min = int $sec / 60;
  7266. $sec -= ($min*60);
  7267. my $ret = sprintf "$week %s", $week==1 ? msg('time-week') : msg('time-weeks');
  7268. $day and $tweak !~ /D/ and $ret .= sprintf " $day %s", $day==1 ? msg('time-day') : msg('time-days');
  7269. $our and $tweak !~ /[DH]/ and $ret .= sprintf " $our %s", $our==1 ? msg('time-hour') : msg('time-hours');
  7270. $min and $tweak !~ /[DHM]/ and $ret .= sprintf " $min %s", $min==1 ? msg('time-minute') : msg('time-minutes');
  7271. $sec and $tweak !~ /[DHMS]/ and $ret .= sprintf " $sec %s", $sec==1 ? msg('time-second') : msg('time-seconds');
  7272. return $ret;
  7273. } ## end of pretty_time
  7274. sub send_mail {
  7275. ## Send out an email message
  7276. ## Arguments: one
  7277. ## 1. Hashref with mandatory args 'body' and 'subject'. Optional 'to'
  7278. ## Returns: undef
  7279. my $self = shift;
  7280. ## Return right away if sendmail and sendmail_file are false
  7281. return if ! $self->{sendmail} and ! $self->{sendmail_file};
  7282. ## Hashref of args
  7283. my $arg = @_;
  7284. ## If 'default_email_from' is not set, we default to currentuser@currenthost
  7285. my $from = $config{default_email_from} || (getpwuid($>) . '@' . $hostname);
  7286. ## Who is the email going to? We usually use the default.
  7287. $arg->{to} ||= $config{default_email_to};
  7288. ## We should always pass in a subject, but just in case:
  7289. $arg->{subject} ||= 'Bucardo Mail!';
  7290. ## Like any good murder mystery, a body is mandatory
  7291. if (! $arg->{body}) {
  7292. $self->glog('Warning: Cannot send mail, no body message', LOG_WARN);
  7293. return;
  7294. }
  7295. ## Where do we connect to?
  7296. my $smtphost = $config{default_email_host} || 'localhost';
  7297. ## Send normal email
  7298. ## Do not send it if the 'example.com' default value is still in place
  7299. if ($self->{sendmail} and $arg->{to} ne 'nobody@example.com') {
  7300. ## Wrap the whole call in an eval so we can report errors
  7301. my $evalworked = 0;
  7302. eval {
  7303. my $smtp = Net::SMTP->new(
  7304. Host => $smtphost,
  7305. Hello => $hostname,
  7306. Timeout => 15
  7307. );
  7308. $smtp->mail($from);
  7309. $smtp->to($arg->{to});
  7310. $smtp->data();
  7311. $smtp->datasend("From: $from\n");
  7312. $smtp->datasend("To: $arg->{to}\n");
  7313. $smtp->datasend("Subject: $arg->{subject}\n");
  7314. $smtp->datasend("\n");
  7315. $smtp->datasend($arg->{body});
  7316. $smtp->dataend;
  7317. $smtp->quit;
  7318. $evalworked = 1;
  7319. };
  7320. if (! $evalworked) {
  7321. my $error = $@ || '???';
  7322. $self->glog("Warning: Error sending email to $arg->{to}: $error", LOG_WARN);
  7323. }
  7324. else {
  7325. $self->glog("Sent an email to $arg->{to} from $from: $arg->{subject}", LOG_NORMAL);
  7326. }
  7327. }
  7328. ## Write the mail to a file
  7329. if ($self->{sendmail_file}) {
  7330. my $fh;
  7331. ## This happens rare enough to not worry about caching the file handle
  7332. if (! open $fh, '>>', $self->{sendmail_file}) {
  7333. $self->glog(qq{Warning: Could not open sendmail file "$self->{sendmail_file}": $!}, LOG_WARN);
  7334. return;
  7335. }
  7336. my $now = scalar localtime;
  7337. print {$fh} qq{
  7338. ==========================================
  7339. To: $arg->{to}
  7340. From: $from
  7341. Subject: $arg->{subject}
  7342. Date: $now
  7343. $arg->{body}
  7344. };
  7345. close $fh or warn qq{Could not close "$self->{sendmail_file}": $!\n};
  7346. }
  7347. return;
  7348. } ## end of send_mail
  7349. 1;
  7350. __END__
  7351. =pod
  7352. =head1 NAME
  7353. Bucardo - Postgres multi-master replication system
  7354. =head1 VERSION
  7355. This document describes version 4.99.7 of Bucardo
  7356. =head1 WEBSITE
  7357. The latest news and documentation can always be found at:
  7358. http://bucardo.org/
  7359. =head1 DESCRIPTION
  7360. Bucardo is a Perl module that replicates Postgres databases using a combination
  7361. of Perl, a custom database schema, Pl/Perlu, and Pl/Pgsql.
  7362. Bucardo is unapologetically extremely verbose in its logging.
  7363. Full documentation can be found on the website, or in the files that came with
  7364. this distribution. See also the documentation for the bucardo program.
  7365. =head1 DEPENDENCIES
  7366. =over
  7367. =item * DBI (1.51 or better)
  7368. =item * DBD::Pg (2.0.0 or better)
  7369. =item * Sys::Hostname
  7370. =item * Sys::Syslog
  7371. =item * DBIx::Safe ## Try 'yum install perl-DBIx-Safe' or visit bucardo.org
  7372. =item * boolean
  7373. =back
  7374. =head1 BUGS
  7375. Bugs should be reported to bucardo-general@bucardo.org. A list of bugs can be found at
  7376. http://bucardo.org/bugs.html
  7377. =head1 CREDITS
  7378. Bucardo was originally developed and funded by Backcountry.com, who have been using versions
  7379. of it in production since 2002. Jon Jensen <jon@endpoint.com> wrote the original version.
  7380. =head1 AUTHOR
  7381. Greg Sabino Mullane <greg@endpoint.com>
  7382. =head1 LICENSE AND COPYRIGHT
  7383. Copyright (c) 2005-2013 Greg Sabino Mullane <greg@endpoint.com>.
  7384. This software is free to use: see the LICENSE file for details.
  7385. =cut