File Coverage

blib/script/narada-mysqldump
Criterion Covered Total %
statement 83 222 37.3
branch 0 52 0.0
condition 0 18 0.0
subroutine 28 42 66.6
pod n/a
total 111 334 33.2


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2 2     2   1563 use 5.010001;
  2         5  
3 2     2   8 use warnings;
  2         2  
  2         68  
4 2     2   8 use strict;
  2         2  
  2         40  
5 2     2   6 use utf8;
  2         2  
  2         13  
6              
7             our $VERSION = 'v2.3.7';
8              
9 2     2   95 use FindBin;
  2         4  
  2         145  
10 2     2   1006 use lib "$FindBin::Bin/../lib/perl5";
  2         1074  
  2         12  
11 2     2   229 use Narada;
  2         2  
  2         43  
12 2     2   1004 use Narada::Lock qw( exclusive_lock unlock_new unlock );;
  2         4  
  2         9  
13 2     2   2626 use Narada::Config qw( get_config get_config_line get_db_config );
  2         3  
  2         13  
14 2     2   223 use DBI;
  2         2  
  2         68  
15 2     2   957 use Time::Local;
  2         2819  
  2         119  
16 2     2   14 use List::Util qw( max );
  2         2  
  2         125  
17 2     2   8 use File::Temp qw( tempfile );
  2         2  
  2         73  
18 2     2   8 use Fcntl qw(F_SETFD);
  2         2  
  2         75  
19              
20 2     2   8 use constant IS_NARADA => Narada::detect() eq 'narada';
  2         3  
  2         11  
21 2     2   8 use constant CONFIG_DIR => IS_NARADA ? 'mysql' : 'db';
  2         3  
  2         103  
22 2     2   9 use constant SQL_DIR => IS_NARADA ? 'var/mysql' : 'var/sql';
  2         3  
  2         111  
23 2     2   9 use constant SCHEME => SQL_DIR.'/db.scheme.sql';
  2         3  
  2         91  
24 2     2   7 use constant REQUIRED_TABLE => 0;
  2         6  
  2         74  
25 2     2   7 use constant OPTIONAL_TABLE => 1;
  2         2  
  2         73  
26 2     2   7 use constant TIMELOCAL_MONTH=> 4;
  2         2  
  2         82  
27 2     2   6 use constant STAT_MTIME => 9;
  2         3  
  2         76  
28 2     2   6 use constant DESC_FIELD => 0;
  2         28  
  2         110  
29 2     2   59 use constant DESC_TYPE => 1;
  2         2  
  2         78  
30 2     2   6 use constant DESC_NULL => 2;
  2         2  
  2         68  
31 2     2   45 use constant DESC_KEY => 3;
  2         4  
  2         83  
32 2     2   6 use constant DESC_DEFAULT => 4;
  2         2  
  2         69  
33 2     2   7 use constant DESC_EXTRA => 5;
  2         2  
  2         3379  
34              
35             # GLOBALS: $::dbh, $::mycnf, $::MYSQLDUMP
36              
37              
38             main(@ARGV) if !caller;
39              
40              
41 0     0     sub err { die "narada-mysqldump: @_\n" }
42              
43             sub main {
44 0 0   0     die "Usage: narada-mysqldump\n" if @_;
45              
46 0 0         init_globals() or return;
47              
48 0           exclusive_lock();
49 0           unlock_new();
50              
51 0           my ($full, $incremental, $ignore) = list_tables();
52 0           my $unchanged = detect_unchanged($full);
53              
54 0           del_dumps_except($incremental, $unchanged);
55 0           dump_scheme_except($ignore);
56 0           dump_full($full, $unchanged);
57 0           dump_incremental($incremental);
58              
59             # To correctly detect_unchanged() we should guarantee next table's
60             # Update_time after executing narada-mysqldump will be at least 1
61             # second later than previous (which kept in dump file's mtime).
62 0           sleep 1;
63              
64 0           unlock();
65              
66 0           return;
67             }
68              
69             sub init_globals {
70 0 0   0     my $db = get_db_config() or return;
71             $::dbh = DBI->connect($db->{dsn}, $db->{login}, $db->{pass},
72 0 0         {RaiseError=>1}) or err DBI->errstr;
73 0           $::mycnf = tempfile(DIR=>'tmp');
74 0           print {$::mycnf} "[client]\n";
  0            
75 0           print {$::mycnf} "user = $db->{login}\n";
  0            
76 0 0         print {$::mycnf} "password = $db->{pass}\n" if length $db->{pass}; ## no critic (ProhibitPostfixControls)
  0            
77 0 0         print {$::mycnf} "host = $db->{host}\n" if length $db->{host}; ## no critic (ProhibitPostfixControls)
  0            
78 0 0         print {$::mycnf} "port = $db->{port}\n" if length $db->{port}; ## no critic (ProhibitPostfixControls)
  0            
79 0           $::mycnf->flush;
80 0 0         sysseek $::mycnf, 0, 0 or err "sysseek: $!";
81 0 0         fcntl $::mycnf, F_SETFD, 0 or err "fcntl: $!";
82 0           my $fd = fileno $::mycnf;
83 0           $::MYSQLDUMP = "mysqldump --defaults-file=/proc/self/fd/\Q$fd\E \Q$db->{db}\E";
84 0           return 1;
85             }
86              
87             sub list_tables {
88 0     0     my $incremental = load_db_conf('dump/incremental', REQUIRED_TABLE);
89 0           my $empty = load_db_conf('dump/empty', REQUIRED_TABLE);
90 0           my $ignore = load_db_conf('dump/ignore', OPTIONAL_TABLE);
91 0           my %other = map {$_=>1} @{$incremental}, @{$empty}, @{$ignore};
  0            
  0            
  0            
  0            
92 0           my @full;
93 0           for my $table (@{ $::dbh->selectcol_arrayref('SHOW TABLES') }) {
  0            
94 0 0         next if $other{$table};
95 0           push @full, $table;
96             }
97 0           return ([sort @full], $incremental, $ignore);
98             }
99              
100             sub load_db_conf {
101 0     0     my ($conf, $required) = @_;
102 0           my @conf = eval { split /\s*\n/xms, get_config(CONFIG_DIR."/$conf") };
  0            
103 0           my @tables;
104 0           for my $table (@conf) {
105 0           local ($::dbh->{RaiseError}, $::dbh->{PrintError});
106 0           my $desc = $::dbh->selectall_arrayref('DESC '.$table);
107 0 0         if ($desc) {
    0          
108 0           push @tables, $table;
109             }
110             elsif ($required == REQUIRED_TABLE) {
111 0           err "Table $table listed in ".CONFIG_DIR."/$conf does not exists\n";
112             }
113             }
114 0           return [sort @tables];
115             }
116              
117             sub detect_unchanged {
118 0     0     my ($full) = @_;
119 0           my @unchanged;
120 0           for my $table (@{$full}) {
  0            
121 0           my $file = SQL_DIR."/$table.sql";
122 0 0 0       if (-f $file && mtime($file) == get_table_status($table, 'Update_time')) {
123 0           push @unchanged, $table;
124             }
125             }
126 0           return \@unchanged;
127             }
128              
129             sub del_dumps_except {
130 0     0     my ($incremental, $unchanged) = @_;
131 0           my %incremental = map {$_=>1} @{$incremental};
  0            
  0            
132 0           my %unchanged = map {$_=>1} @{$unchanged};
  0            
  0            
133 0           for my $file (glob SQL_DIR.'/*.sql') {
134 0 0         if ($file =~ m{\A\Q${\SQL_DIR}\E/([^.]*)[.]sql\z}xms) {
  0 0          
135 0           my $table = $1;
136 0 0         next if $unchanged{$table};
137             }
138 0           elsif ($file =~ m{\A\Q${\SQL_DIR}\E/([^.]*)[.]\d+-(\d+)[.]sql\z}xms) {
139 0           my $table = $1;
140 0 0 0       next if $incremental{$table}
141             && mtime($file) >= get_table_status($table, 'Create_time');
142             }
143 0 0         unlink $file or err "unlink($file): $!\n";
144             }
145 0           return;
146             }
147              
148             sub dump_scheme_except {
149 0     0     my ($ignore) = @_;
150 0           my $db = get_config_line(CONFIG_DIR.'/db');
151 0           my $tables = join q{ }, map {"--ignore-table=\Q$db\E.\Q$_\E"} @{$ignore};
  0            
  0            
152 0           mysqldump("--opt -d $tables", SCHEME, time);
153 0           return;
154             }
155              
156             sub dump_full {
157 0     0     my ($full, $unchanged) = @_;
158 0           my %unchanged = map {$_=>1} @{$unchanged};
  0            
  0            
159 0           for my $table (@{$full}) {
  0            
160 0 0         next if $unchanged{$table};
161 0           my $file = SQL_DIR."/$table.sql";
162 0           my $t = get_table_status($table, 'Update_time');
163 0           mysqldump("--opt -t \Q$table\E", $file, $t);
164             }
165 0           return;
166             }
167              
168             sub dump_incremental {
169 0     0     my ($incremental) = @_;
170 0           for my $table (@{$incremental}) {
  0            
171 0           my $key = get_key($table);
172 0           my $prev = max(0, map {m/-(\d+)[.]sql\z/xms} glob SQL_DIR."/\Q$table\E.*.sql");
  0            
173 0           my $next = get_table_status($table, 'Auto_increment');
174 0 0         if ($prev < $next-1) {
175 0           my $from = $prev+1;
176 0           my $to = $next-1;
177 0           my $file = SQL_DIR."/$table.$from-$to.sql";
178 0           my $where= "$key>=$from AND $key<=$to";
179 0           my $t = get_table_status($table, 'Update_time');
180 0           mysqldump("--opt -t -w \Q$where\E \Q$table\E", $file, $t);
181             }
182             }
183 0           return;
184             }
185              
186             ###
187              
188             sub get_table_status {
189 0     0     my ($table, $field) = @_;
190             my $val = $::dbh->selectrow_hashref('SHOW TABLE STATUS LIKE ?',
191 0           undef, $table)->{$field};
192 0 0         if ($field =~ /_time\z/xms) {
193 0 0         if (!defined $val) {
194 0           $val = 0;
195             }
196             else {
197 0           my @datetime = reverse split /\D+/xms, $val;
198 0           $datetime[TIMELOCAL_MONTH]--;
199 0           $val = timelocal(@datetime);
200             }
201             }
202 0           return $val;
203             }
204              
205             sub get_key {
206 0     0     my ($table) = @_;
207 0           my $desc = $::dbh->selectall_arrayref('DESC '.$table);
208             err "First field in table $table must be: INT AUTO_INCREMENT PRIMARY KEY\n"
209 0           if !(@{ $desc }
210             && $desc->[0][DESC_TYPE]=~/\A\w*int\b/xms
211             && $desc->[0][DESC_KEY] eq 'PRI'
212             && $desc->[0][DESC_EXTRA] eq 'auto_increment'
213 0 0 0       && 1 == grep {$_->[DESC_KEY] eq 'PRI'} @{$desc});
  0   0        
  0   0        
      0        
214 0           return $desc->[0][DESC_FIELD];
215             }
216              
217             sub mtime {
218 0     0     my ($file) = @_;
219 0           return (stat $file)[STAT_MTIME];
220             }
221              
222             sub mysqldump {
223 0     0     my ($opt, $file, $t) = @_;
224 0 0         system("$::MYSQLDUMP $opt > \Q$file\E.tmp")
225             == 0 or err "system($::MYSQLDUMP $opt > $file.tmp): $?";
226 0 0         rename "$file.tmp", $file or err "rename($file.tmp, $file): $!";
227 0 0         utime $t, $t, $file or err "utime($file): $!";
228 0           return;
229             }
230              
231              
232             1; # Magic true value required at end of module
233             __END__