--- deliantra/Deliantra-Client/DC/DB.pm 2008/01/10 23:02:19 1.31 +++ deliantra/Deliantra-Client/DC/DB.pm 2010/04/03 02:58:24 1.45 @@ -14,54 +14,131 @@ package DC::DB; -use strict; -use utf8; +use common::sense; +use File::Path (); use Carp (); use Storable (); +use AnyEvent::Util (); use Config; use BDB; +use Fcntl (); use DC; -our $ODBDIR = "cfplus-" . BDB::VERSION . "-$Config{archname}"; -our $DBDIR = "client-" . BDB::VERSION . "-$Config{archname}"; +our $ODBDIR = "cfplus-" . BDB::VERSION_MAJOR . "." . BDB::VERSION_MINOR . "-$Config{archname}"; +our $DBDIR = "client-" . BDB::VERSION_MAJOR . "." . BDB::VERSION_MINOR . "-$Config{archname}"; our $DB_HOME = "$Deliantra::VARDIR/$DBDIR"; -if (!-e $DB_HOME and -e "$Deliantra::VARDIR/$ODBDIR") { - rename "$Deliantra::VARDIR/$ODBDIR", $DB_HOME; - print STDERR "INFO: moved old database from $Deliantra::VARDIR/$ODBDIR to $DB_HOME\n"; -} +sub FIRST_TILE_ID () { 64 } -if (!-e $DB_HOME and -e "$Deliantra::OLDDIR/$ODBDIR") { - rename "$Deliantra::OLDDIR/$DBDIR", $DB_HOME; - print STDERR "INFO: moved old database from $Deliantra::OLDDIR/$ODBDIR to $DB_HOME\n"; +unless (-d $DB_HOME) { + if (-d "$Deliantra::VARDIR/$ODBDIR") { + rename "$Deliantra::VARDIR/$ODBDIR", $DB_HOME; + print STDERR "INFO: moved old database from $Deliantra::VARDIR/$ODBDIR to $DB_HOME\n"; + } elsif (-d "$Deliantra::OLDDIR/$ODBDIR") { + rename "$Deliantra::OLDDIR/$DBDIR", $DB_HOME; + print STDERR "INFO: moved old database from $Deliantra::OLDDIR/$ODBDIR to $DB_HOME\n"; + } else { + File::Path::mkpath [$DB_HOME] + or die "unable to create database directory $DB_HOME: $!"; + } } BDB::max_poll_time 0.03; +BDB::max_parallel 1; our $DB_ENV; +our $DB_ENV_FH; our $DB_STATE; our %DB_TABLE; +our $TILE_SEQ; -sub open_db { - mkdir $DB_HOME, 0777; +sub all_databases { + opendir my $fh, $DB_HOME + or return; + + grep !/^(?:\.|log\.|_)/, readdir $fh +} + +sub try_verify_env($) { + my ($env) = @_; + + open my $lock, "+>$DB_HOME/__lock" + or die "__lock: $!"; - $DB_ENV = db_env_create; + flock $lock, &Fcntl::LOCK_EX + or die "flock: $!"; - $DB_ENV->set_errfile (\*STDERR); - $DB_ENV->set_msgfile (\*STDERR); - $DB_ENV->set_verbose (-1, 1); + # we look at the __db.register env file that has been created by now + # and check for the number of registered processes - if there is + # only one, we verify all databases, otherwise we skip this + # we MUST NOT close the filehandle as longa swe keep the env open, as + # this destroys the record locks on it. + open $DB_ENV_FH, "<$DB_HOME/__db.register" + or die "__db.register: $!"; - $DB_ENV->set_flags (BDB::AUTO_COMMIT | BDB::LOG_AUTOREMOVE | BDB::TXN_WRITE_NOSYNC); - $DB_ENV->set_cachesize (0, 2048 * 1024, 0); + # __db.register contains one record per process, with X signifying + # empty records (of course, this is completely private to bdb...) + my $count = grep /^[^X]/, <$DB_ENV_FH>; - db_env_open $DB_ENV, $DB_HOME, + if ($count == 1) { + # if any databases are corrupted, we simply delete all of them + + for (all_databases) { + my $dbh = db_create $env + or last; + + # a failed verify will panic the environment, which is fine with us + db_verify $dbh, "$DB_HOME/$_"; + + return if $!; # nuke database and recreate if verification failure + } + + } + + # close probably cleans those up, but we also want to run on windows, + # so better be safe. + flock $lock, &Fcntl::LOCK_UN + or die "funlock: $!"; + + 1 +} + +sub try_open_db { + File::Path::mkpath [$DB_HOME]; + + undef $DB_ENV; + undef $DB_ENV_FH; + + my $env = db_env_create; + + $env->set_errfile (\*STDERR); + $env->set_msgfile (\*STDERR); + $env->set_verbose (-1, 1); + + $env->set_flags (BDB::AUTO_COMMIT | BDB::REGION_INIT); + $env->set_flags (&BDB::LOG_AUTOREMOVE ) if BDB::VERSION v0, v4.7; + $env->log_set_config (&BDB::LOG_AUTO_REMOVE) if BDB::VERSION v4.7; + + $env->set_timeout (3, BDB::SET_TXN_TIMEOUT); + $env->set_timeout (3, BDB::SET_LOCK_TIMEOUT); + + $env->set_cachesize (0, 2048 * 1024, 0); + + db_env_open $env, $DB_HOME, BDB::CREATE | BDB::REGISTER | BDB::RECOVER | BDB::INIT_MPOOL | BDB::INIT_LOCK | BDB::INIT_TXN, 0666; $! and die "cannot open database environment $DB_HOME: " . BDB::strerror; + # now we go through the registered processes, if there is only one, we verify all files + # to make sure windows didn'T corrupt them (as windows does....) + try_verify_env $env + or die "database environment failed verification"; + + $DB_ENV = $env; + 1 } @@ -71,6 +148,9 @@ $table =~ s/([^a-zA-Z0-9_\-])/sprintf "=%x=", ord $1/ge; + $DB_ENV#d# + or return ::clienterror ("trying to create table $_[0] with empty db_env $DB_ENV" => 1);#d# + my $db = db_create $DB_ENV; $db->set_flags (BDB::CHKSUM); @@ -85,20 +165,9 @@ ############################################################################# -unless (eval { open_db }) { - warn "$@";#d# - eval { File::Path::rmtree $DB_HOME }; - open_db; -} - -our $WATCHER = EV::io BDB::poll_fileno, EV::READ, \&BDB::poll_cb; - -our $SYNC = EV::timer_ns 0, 60, sub { - $_[0]->stop; - db_env_txn_checkpoint $DB_ENV, 0, 0, 0, sub { }; -}; - -our $tilemap; +our $WATCHER; +our $SYNC; +our $facemap; sub exists($$$) { my ($db, $key, $cb) = @_; @@ -150,34 +219,30 @@ my $table = table "facemap"; my $id; - db_get $table, undef, $name, $id, 0; - return $cb->($id) unless $!; - - for (1..100) { - my $txn = $DB_ENV->txn_begin; - db_get $table, $txn, id => $id, 0; - - $id = 64 if $id < 64; + db_get $table, undef, $name => $id, 0; + $! or return $cb->($id); - ++$id; - - db_put $table, $txn, id => $id, 0; - db_txn_finish $txn; - - $SYNC->again unless $SYNC->is_active; + unless ($TILE_SEQ) { + $TILE_SEQ = $table->sequence; + $TILE_SEQ->initial_value (FIRST_TILE_ID); + $TILE_SEQ->set_cachesize (0); + db_sequence_open $TILE_SEQ, undef, "id", BDB::CREATE; + } - return $cb->($id) unless $!; + db_sequence_get $TILE_SEQ, undef, 1, my $id; - select undef, undef, undef, 0.01 * rand; - } + die "unable to allocate tile id: $!" + if $!; + + db_put $table, undef, $name => $id, 0; + $cb->($id); - die "maximum number of transaction retries reached - database problems?"; } sub get_tile_id_sync($) { my ($name) = @_; - $tilemap->{$name} ||= do { + $facemap->{$name} ||= do { my $id; do_get_tile_id $name, sub { $id = $_[0]; @@ -222,22 +287,9 @@ ############################################################################# -# fetch the full face table first -unless ($tilemap) { - do_table facemap => sub { - $tilemap = $_[0]; - delete $tilemap->{id}; - my %maptile = reverse %$tilemap;#d# - if ((scalar keys %$tilemap) != (scalar keys %maptile)) {#d# - $tilemap = { };#d# - DC::error "FATAL: facemap is not a 1:1 mapping, please report this and delete your $DB_HOME directory!\n";#d# - }#d# - }; -} - package DC::DB::Server; -use strict; +use common::sense; use EV (); use Fcntl; @@ -366,7 +418,8 @@ } sub run { - ($FH, my $fh) = DC::socketpipe; + ($FH, my $fh) = AnyEvent::Util::portable_socketpair + or die "unable to create database socketpair: $!"; my $oldfh = select $FH; $| = 1; select $oldfh; my $oldfh = select $fh; $| = 1; select $oldfh; @@ -423,6 +476,52 @@ close $FH; } +package DC::DB; + +sub nuke_db { + undef $DB_ENV; + undef $DB_ENV_FH; + + File::Path::mkpath [$DB_HOME]; + eval { File::Path::rmtree $DB_HOME }; +} + +sub open_db { + unless (eval { try_open_db }) { + warn "$@";#d# + eval { nuke_db }; + try_open_db; + } + + # fetch the full face table first + unless ($facemap) { + do_table facemap => sub { + $facemap = $_[0]; + delete $facemap->{id}; + my %maptile = reverse %$facemap;#d# + if ((scalar keys %$facemap) != (scalar keys %maptile)) {#d# + $facemap = { };#d# + DC::error "FATAL: facemap is not a 1:1 mapping, please report this and delete your $DB_HOME directory!\n";#d# + }#d# + }; + } + + $WATCHER = EV::io BDB::poll_fileno, EV::READ, \&BDB::poll_cb; + $SYNC = EV::timer_ns 0, 60, sub { + $_[0]->stop; + db_env_txn_checkpoint $DB_ENV, 0, 0, 0, sub { }; + }; +} + +END { + db_env_txn_checkpoint $DB_ENV, 0, 0, 0 + if $DB_ENV; + + undef $TILE_SEQ; + %DB_TABLE = (); + undef $DB_ENV; +} + 1; =back