improve slave handling code
[MogileFS-Server.git] / lib / MogileFS / Store / MySQL.pm
blobbb2229a17135b768aa6fbb81463bfa951cd2e54e
1 package MogileFS::Store::MySQL;
2 use strict;
3 use warnings;
4 use DBI 1.44;
5 use DBD::mysql;
6 use MogileFS::Util qw(throw);
7 use base 'MogileFS::Store';
9 # --------------------------------------------------------------------------
10 # Package methods we override
11 # --------------------------------------------------------------------------
13 sub dsn_of_dbhost {
14 my ($class, $dbname, $host, $port) = @_;
15 return "DBI:mysql:$dbname;host=$host" . ($port ? ";port=$port" : "");
18 sub dsn_of_root {
19 my ($class, $dbname, $host, $port) = @_;
20 return $class->dsn_of_dbhost('mysql', $host, $port);
23 # --------------------------------------------------------------------------
24 # Store-related things we override
25 # --------------------------------------------------------------------------
27 sub init {
28 my $self = shift;
29 $self->SUPER::init;
30 $self->{lock_depth} = 0;
31 $self->{slave_next_check} = 0;
34 sub post_dbi_connect {
35 my $self = shift;
36 $self->SUPER::post_dbi_connect;
37 $self->{lock_depth} = 0;
40 sub was_deadlock_error {
41 my $self = shift;
42 my $dbh = $self->dbh;
43 return 0 unless $dbh->err;
44 # 1205 is "lock wait timeout", but we should bomb out if we've
45 # alerady hung for that long.
46 return 1 if ($dbh->err == 1213);
49 sub was_duplicate_error {
50 my $self = shift;
51 my $dbh = $self->dbh;
52 return 0 unless $dbh->err;
53 return 1 if $dbh->err == 1062 || $dbh->errstr =~ /duplicate/i;
56 sub table_exists {
57 my ($self, $table) = @_;
58 return eval {
59 my $sth = $self->dbh->prepare("DESCRIBE $table");
60 $sth->execute;
61 my $rec = $sth->fetchrow_hashref;
62 return $rec ? 1 : 0;
66 sub can_replace { 1 }
67 sub can_insertignore { 1 }
68 sub can_insert_multi { 1 }
69 sub unix_timestamp { "UNIX_TIMESTAMP()" }
71 sub filter_create_sql {
72 my ($self, $sql) = @_;
73 return $sql unless $self->fid_type eq "BIGINT";
74 $sql =~ s!\bfid\s+INT\b!fid BIGINT!i;
75 return $sql;
78 sub can_do_slaves { 1 }
80 sub check_slave {
81 my $self = shift;
83 return 0 unless $self->{slave};
85 my $next_check = \$self->{slave}->{next_check};
87 if ($$next_check > time()) {
88 return 1;
91 #my $slave_status = eval { $self->{slave}->dbh->selectrow_hashref("SHOW SLAVE STATUS") };
92 #warn "Error thrown: '$@' while trying to get slave status." if $@;
94 # TODO: Check show slave status *unless* a server setting is present to
95 # tell us to ignore it (like in a multi-DC setup).
96 eval { $self->{slave}->dbh };
97 return 0 if $@;
99 # call time() again here because SQL blocks.
100 $$next_check = time() + 5;
102 return 1;
105 # attempt to grab a lock of lockname, and timeout after timeout seconds.
106 # returns 1 on success and 0 on timeout
107 sub get_lock {
108 my ($self, $lockname, $timeout) = @_;
109 die "Lock recursion detected (grabbing $lockname, had $self->{last_lock}). Bailing out." if $self->{lock_depth};
111 my $lock = $self->dbh->selectrow_array("SELECT GET_LOCK(?, ?)", undef, $lockname, $timeout);
112 if ($lock) {
113 $self->{lock_depth} = 1;
114 $self->{last_lock} = $lockname;
116 return $lock;
119 # attempt to release a lock of lockname.
120 # returns 1 on success and 0 if no lock we have has that name.
121 sub release_lock {
122 my ($self, $lockname) = @_;
123 my $rv = $self->dbh->selectrow_array("SELECT RELEASE_LOCK(?)", undef, $lockname);
124 $self->{lock_depth} = 0;
125 return $rv;
128 sub lock_queue {
129 my ($self, $type) = @_;
130 my $lock = $self->get_lock('mfsd:' . $type, 30);
131 return $lock ? 1 : 0;
134 sub unlock_queue {
135 my ($self, $type) = @_;
136 my $lock = $self->release_lock('mfsd:' . $type);
137 return $lock ? 1 : 0;
140 # clears everything from the fsck_log table
141 # return 1 on success. die otherwise.
142 # Under MySQL 4.1+ this is actually fast.
143 sub clear_fsck_log {
144 my $self = shift;
145 $self->dbh->do("TRUNCATE TABLE fsck_log");
146 return 1;
149 # --------------------------------------------------------------------------
150 # Functions specific to Store::MySQL subclass. Not in parent.
151 # --------------------------------------------------------------------------
153 sub fid_type {
154 my $self = shift;
155 return $self->{_fid_type} if $self->{_fid_type};
157 # let people force bigint mode with environment.
158 if ($ENV{MOG_FIDSIZE} && $ENV{MOG_FIDSIZE} eq "big") {
159 return $self->{_fid_type} = "BIGINT";
162 # else, check a maybe-existing table and see if we're in bigint
163 # mode already.
164 my $dbh = $self->dbh;
165 my @create = eval { $dbh->selectrow_array("SHOW CREATE TABLE file") };
166 if (@create && $create[0] eq 'file') {
167 if ($create[1] =~ /\bfid\b.+\bbigint\b/i) {
168 return $self->{_fid_type} = "BIGINT";
169 } else {
170 return $self->{_fid_type} = "INT";
174 # Used to default to 32bit ints, but this always bites people
175 # a few years down the road. So default to 64bit.
176 return $self->{_fid_type} = "BIGINT";
179 sub column_type {
180 my ($self, $table, $col) = @_;
181 my $sth = $self->dbh->prepare("DESCRIBE $table");
182 $sth->execute;
183 while (my $rec = $sth->fetchrow_hashref) {
184 if ($rec->{Field} eq $col) {
185 $sth->finish;
186 return $rec->{Type};
189 return undef;
192 # --------------------------------------------------------------------------
193 # Test suite things we override
194 # --------------------------------------------------------------------------
196 sub new_temp {
197 my $self = shift;
198 my %args = @_;
199 my $dbname = $args{dbname} || "tmp_mogiletest";
200 my $host = $args{dbhost} || 'localhost';
201 my $port = $args{dbport} || 3306;
202 my $user = $args{dbuser} || 'root';
203 my $pass = $args{dbpass} || '';
204 my $rootuser = $args{dbrootuser} || $args{dbuser} || 'root';
205 my $rootpass = $args{dbrootpass} || $args{dbpass} || '';
206 my $sto =
207 MogileFS::Store->new_from_dsn_user_pass("DBI:mysql:database=$dbname;host=$host;port=$port",
208 $rootuser, $rootpass);
210 my $dbh = $sto->dbh;
211 _create_mysql_db($dbh, $dbname);
213 # allow MyISAM in the test suite.
214 $ENV{USE_UNSAFE_MYSQL} = 1 unless defined $ENV{USE_UNSAFE_MYSQL};
216 my @args = ("$FindBin::Bin/../mogdbsetup", "--yes",
217 "--dbname=$dbname", "--type=MySQL",
218 "--dbhost=$host", "--dbport=$port",
219 "--dbrootuser=$rootuser",
220 "--dbuser=$user", );
221 push @args, "--dbpass=$pass" unless $pass eq '';
222 push @args, "--dbrootpass=$rootpass" unless $rootpass eq '';
223 system(@args)
224 and die "Failed to run mogdbsetup (".join(' ',map { "'".$_."'" } @args).").";
226 if($user ne $rootuser) {
227 $sto = MogileFS::Store->new_from_dsn_user_pass(
228 "DBI:mysql:database=$dbname;host=$host;port=$port",
229 $user, $pass);
230 $dbh = $sto->dbh;
233 $dbh->do("use $dbname");
234 return $sto;
237 sub _create_mysql_db {
238 my $dbh = shift;
239 my $dbname = shift;
240 _drop_mysql_db($dbh, $dbname);
241 $dbh->do("CREATE DATABASE $dbname");
244 sub _drop_mysql_db {
245 my $dbh = shift;
246 my $dbname = shift;
247 $dbh->do("DROP DATABASE IF EXISTS $dbname");
250 # --------------------------------------------------------------------------
251 # Database creation time things we override
252 # --------------------------------------------------------------------------
254 sub create_table {
255 my $self = shift;
256 my ($table) = @_;
258 my $dbh = $self->dbh;
259 my $errmsg =
260 "InnoDB backend is unavailable for use, force creation of tables " .
261 "by setting USE_UNSAFE_MYSQL=1 in your environment and run this " .
262 "command again.";
264 unless ($ENV{USE_UNSAFE_MYSQL}) {
265 my $engines = eval { $dbh->selectall_hashref("SHOW ENGINES", "Engine"); };
266 if ($@ && $dbh->err == 1064) {
267 # syntax error? for MySQL 4.0.x.
268 # who cares. we'll catch it below on the double-check.
269 } else {
270 die $errmsg
271 unless ($engines->{InnoDB} and
272 $engines->{InnoDB}->{Support} =~ m/^(YES|DEFAULT)$/i);
276 my $existed = $self->table_exists($table);
278 $self->SUPER::create_table(@_);
279 return if $ENV{USE_UNSAFE_MYSQL};
281 # don't alter an existing table up to InnoDB from MyISAM...
282 # could be costly. but on new tables, no problem...
283 unless ($existed) {
284 $dbh->do("ALTER TABLE $table ENGINE=InnoDB");
285 warn "DBI reported an error of: '" . $dbh->errstr . "' when trying to " .
286 "alter table type of $table to InnoDB\n" if $dbh->err;
289 # but in any case, let's see if it's already InnoDB or not.
290 my $table_status = $dbh->selectrow_hashref("SHOW TABLE STATUS LIKE '$table'");
292 # if not, either die or warn.
293 unless (($table_status->{Engine} || $table_status->{Type} || "") eq "InnoDB") {
294 if ($existed) {
295 warn "WARNING: MySQL table that isn't InnoDB: $table\n";
296 } else {
297 die "MySQL didn't change table type to InnoDB as requested.\n\n$errmsg"
303 # --------------------------------------------------------------------------
304 # Data-access things we override
305 # --------------------------------------------------------------------------
307 # update the device count for a given fidid
308 sub update_devcount_atomic {
309 my ($self, $fidid) = @_;
310 my $lockname = "mgfs:fid:$fidid";
312 my $lock = eval { $self->get_lock($lockname, 10) };
314 # Check to make sure the lock didn't timeout, then we want to bail.
315 return 0 if defined $lock && $lock == 0;
317 # Checking $@ is pointless for the time because we just want to plow ahead
318 # even if the get_lock trapped a recursion and threw a fatal error.
320 $self->update_devcount($fidid);
322 # Don't release the lock if we never got it.
323 $self->release_lock($lockname) if $lock;
324 return 1;
327 sub should_begin_replicating_fidid {
328 my ($self, $fidid) = @_;
329 my $lockname = "mgfs:fid:$fidid:replicate";
330 return 1 if $self->get_lock($lockname, 1);
331 return 0;
334 sub note_done_replicating {
335 my ($self, $fidid) = @_;
336 my $lockname = "mgfs:fid:$fidid:replicate";
337 $self->release_lock($lockname);
340 sub upgrade_add_host_getport {
341 my $self = shift;
342 # see if they have the get port, else update it
343 unless ($self->column_type("host", "http_get_port")) {
344 $self->dowell("ALTER TABLE host ADD COLUMN http_get_port MEDIUMINT UNSIGNED AFTER http_port");
348 sub upgrade_add_host_altip {
349 my $self = shift;
350 unless ($self->column_type("host", "altip")) {
351 $self->dowell("ALTER TABLE host ADD COLUMN altip VARCHAR(15) AFTER hostip");
352 $self->dowell("ALTER TABLE host ADD COLUMN altmask VARCHAR(18) AFTER altip");
353 $self->dowell("ALTER TABLE host ADD UNIQUE altip (altip)");
357 sub upgrade_add_device_asof {
358 my $self = shift;
359 unless ($self->column_type("device", "mb_asof")) {
360 $self->dowell("ALTER TABLE device ADD COLUMN mb_asof INT(10) UNSIGNED AFTER mb_used");
364 sub upgrade_add_device_weight {
365 my $self = shift;
366 unless ($self->column_type("device", "weight")) {
367 $self->dowell("ALTER TABLE device ADD COLUMN weight MEDIUMINT DEFAULT 100 AFTER status");
372 sub upgrade_add_device_readonly {
373 my $self = shift;
374 unless ($self->column_type("device", "status") =~ /readonly/) {
375 $self->dowell("ALTER TABLE device MODIFY COLUMN status ENUM('alive', 'dead', 'down', 'readonly')");
379 sub upgrade_add_device_drain {
380 my $self = shift;
381 unless ($self->column_type("device", "status") =~ /drain/) {
382 $self->dowell("ALTER TABLE device MODIFY COLUMN status ENUM('alive', 'dead', 'down', 'readonly', 'drain')");
386 sub upgrade_modify_server_settings_value {
387 my $self = shift;
388 unless ($self->column_type("server_settings", "value") =~ /text/i) {
389 $self->dowell("ALTER TABLE server_settings MODIFY COLUMN value TEXT");
393 sub upgrade_add_file_to_queue_arg {
394 my $self = shift;
395 unless ($self->column_type("file_to_queue", "arg")) {
396 $self->dowell("ALTER TABLE file_to_queue ADD COLUMN arg TEXT");
400 sub upgrade_modify_device_size {
401 my $self = shift;
402 for my $col ('mb_total', 'mb_used') {
403 if ($self->column_type("device", $col) =~ m/mediumint/i) {
404 $self->dowell("ALTER TABLE device MODIFY COLUMN $col INT UNSIGNED");
409 sub pre_daemonize_checks {
410 my $self = shift;
411 # Jay Buffington, from the mailing lists, writes:
413 # > > Is your DBI version at least 1.43? The Makefile.PL of DBD::mysql shows
414 # > > that code for last_insert_it is compiled in only if DBD::mysql is built
415 # > > with DBI 1.43 or newer.
416 #> Yes, I have 1.53.
417 #> jay@webdev:~$ perl -MDBI -le 'print $DBI::VERSION'
418 #> 1.53
420 #> BUT I just re-installed 2.9006 while researching this and my test
421 #> script started working. I just reran the mogile server test suite and
422 #> all test passed!
424 #> Problem solved!
426 #> The original DBD::mysql 2.9006 was installed from a RPM. I bet that
427 #> it was built against a DBI older than 1.43, so it didn't support
428 #> LAST_INSERT_ID.
430 # So...
431 # since we don't know what version of DBI their DBD::mysql was built against,
432 # let's just test that last_insert_id works.
434 my $id = eval {
435 $self->register_tempfile(dmid => 99,
436 key => "_server_startup_test");
438 unless ($id) {
439 die "MySQL self-tests failed. Your DBD::mysql might've been built against an old DBI version.\n";
446 __END__
448 =head1 NAME
450 MogileFS::Store::MySQL - MySQL data storage for MogileFS
452 =head1 SEE ALSO
454 L<MogileFS::Store>