From 40f1c550ea52362374c384382440ac991de26e1b Mon Sep 17 00:00:00 2001 From: dormando Date: Thu, 20 Jan 2011 00:48:00 -0800 Subject: [PATCH] new cachable objects + factories device/host aren't finished yet: - need save to db and friends - more tests - some missing interfaces MogFactory needs to be renamed to MogileFS::Factory and the other factories moved underneath. Most tests pass as of this commit. --- lib/MogileFS/Factory.pm | 96 +++++++++++++ lib/MogileFS/Factory/Class.pm | 75 +++++++++++ lib/MogileFS/Factory/Device.pm | 14 ++ lib/MogileFS/Factory/Domain.pm | 14 ++ lib/MogileFS/Factory/Host.pm | 14 ++ lib/MogileFS/NewClass.pm | 54 ++++++++ lib/MogileFS/NewDevice.pm | 296 +++++++++++++++++++++++++++++++++++++++++ lib/MogileFS/NewDomain.pm | 41 ++++++ lib/MogileFS/NewHost.pm | 104 +++++++++++++++ lib/MogileFS/Store.pm | 48 +++++++ t/01-domain-class.t | 153 +++++++++++++++++++++ t/02-host-device.t | 119 +++++++++++++++++ 12 files changed, 1028 insertions(+) create mode 100644 lib/MogileFS/Factory.pm create mode 100644 lib/MogileFS/Factory/Class.pm create mode 100644 lib/MogileFS/Factory/Device.pm create mode 100644 lib/MogileFS/Factory/Domain.pm create mode 100644 lib/MogileFS/Factory/Host.pm create mode 100644 lib/MogileFS/NewClass.pm create mode 100644 lib/MogileFS/NewDevice.pm create mode 100644 lib/MogileFS/NewDomain.pm create mode 100644 lib/MogileFS/NewHost.pm create mode 100644 t/01-domain-class.t create mode 100644 t/02-host-device.t diff --git a/lib/MogileFS/Factory.pm b/lib/MogileFS/Factory.pm new file mode 100644 index 0000000..df896ae --- /dev/null +++ b/lib/MogileFS/Factory.pm @@ -0,0 +1,96 @@ +package MogileFS::Factory; +use strict; +use warnings; + +=head1 + +MogileFS::MogFactory - singleton class for holding some common objects. + +=head1 ABOUT + +This module holds a singleton for caching objects which are common but +relatively low in number. Such as devices, compared to fids. + +This singleton is to be maintained by the parent process, and inherited to +children during fork. Post-fork, the cache is updated by natural commands, or +a monitor process pushing changes through the parent. + +The purpose is to provide a fresh cache, without forcing new children to +wait for a monitoring run before becoming useful. It also should greatly +reduce the number of simple DB queries, as those should only happen +periodically directly from the monitor job. + +=cut + +my %singleton; + +# Rename to new maybe? +sub get_factory { + my $class = shift; + if (!exists $singleton{$class}) { + $singleton{$class} = bless { + by_id => {}, + by_name => {}, + }, $class; + } + return $singleton{$class}; +} + +# because 'add' means bail if already exists. +sub set { + my $self = shift; + my $obj = shift; + + $self->{by_id}->{$obj->id} = $obj; + $self->{by_name}->{$obj->name} = $obj; + return $obj; +} + +sub remove { + my $self = shift; + my $obj = shift; + + if (exists $self->{by_id}->{$obj->id}) { + delete $self->{by_id}->{$obj->id}; + delete $self->{by_name}->{$obj->name}; + } +} + +sub get_by_id { + my ($self, $id) = @_; + return $self->{by_id}->{$id}; +} + +sub get_by_name { + my ($self, $name) = @_; + return $self->{by_name}->{$name}; +} + +sub get_ids { + my $self = shift; + return keys %{$self->{by_id}}; +} + +sub get_names { + my $self = shift; + return keys %{$self->{by_name}}; +} + +sub get_all { + my $self = shift; + return values %{$self->{by_id}}; +} + +sub map_by_id { + my $self = shift; + my $set = $self->{by_id}; + return { map { $_ => $set->{$_} } keys %{$set} }; +} + +sub map_by_name { + my $self = shift; + my $set = $self->{by_name}; + return { map { $_ => $set->{$_} } keys %{$set} }; +} + +1; diff --git a/lib/MogileFS/Factory/Class.pm b/lib/MogileFS/Factory/Class.pm new file mode 100644 index 0000000..a4ec368 --- /dev/null +++ b/lib/MogileFS/Factory/Class.pm @@ -0,0 +1,75 @@ +package MogileFS::Factory::Class; +use strict; +use warnings; +use base 'MogileFS::Factory'; + +use MogileFS::NewClass; + +# This class is a reimplementation since classids and classnames +# are not globally unique... uses the same interface. +# Stupid/wasteful. +sub set { + my ($self, $domain, $args) = @_; + my $domain_factory = MogileFS::Factory::Domain->get_factory; + # FIXME: Inject the dmid into the class somehow. + my $class = MogileFS::NewClass->new_from_args($args, $domain_factory); + $self->{by_id}->{$domain->id}->{$class->id} = $class; + $self->{by_name}->{$domain->id}->{$class->name} = $class; + return $class; +} + +# Example of what we could use for testing. +# Test creates the object, injects its own factory, then hands it to us. +sub set_from_obj { + my ($self, $obj) = @_; +} + +sub remove { + my $self = shift; + my $class = shift; + my $domid = $class->domain->id; + my $clsid = $class->id; + if (exists $self->{by_id}->{$domid}->{$clsid}) { + delete $self->{by_id}->{$domid}->{$clsid}; + delete $self->{by_name}->{$domid}->{$class->name}; + } +} + +sub get_by_id { + my ($self, $domain, $id) = @_; + return $self->{by_id}->{$domain->id}->{$id}; +} + +sub get_by_name { + my ($self, $domain, $name) = @_; + return $self->{by_name}->{$domain->id}->{$name}; +} + +sub get_ids { + my ($self, $domain) = @_; + return keys %{$self->{by_id}->{$domain->id}}; +} + +sub get_names { + my ($self, $domain) = @_; + return keys %{$self->{by_name}->{$domain->id}}; +} + +sub get_all { + my ($self, $domain) = @_; + return values %{$self->{by_id}->{$domain->id}}; +} + +sub map_by_id { + my ($self, $domain) = @_; + my $set = $self->{by_id}->{$domain->id}; + return { map { $_ => $set->{$_} } keys %{$set} }; +} + +sub map_by_name { + my ($self, $domain) = @_; + my $set = $self->{by_name}->{$domain->id}; + return { map { $_ => $set->{$_} } keys %{$set} }; +} + +1; diff --git a/lib/MogileFS/Factory/Device.pm b/lib/MogileFS/Factory/Device.pm new file mode 100644 index 0000000..91b3af9 --- /dev/null +++ b/lib/MogileFS/Factory/Device.pm @@ -0,0 +1,14 @@ +package MogileFS::Factory::Device; +use strict; +use warnings; +use base 'MogileFS::Factory'; + +use MogileFS::NewDevice; + +sub set { + my ($self, $args) = @_; + my $hostfactory = MogileFS::Factory::Host->get_factory; + return $self->SUPER::set(MogileFS::NewDevice->new_from_args($args, $hostfactory)); +} + +1; diff --git a/lib/MogileFS/Factory/Domain.pm b/lib/MogileFS/Factory/Domain.pm new file mode 100644 index 0000000..92fc60e --- /dev/null +++ b/lib/MogileFS/Factory/Domain.pm @@ -0,0 +1,14 @@ +package MogileFS::Factory::Domain; +use strict; +use warnings; +use base 'MogileFS::Factory'; + +use MogileFS::NewDomain; + +sub set { + my ($self, $args) = @_; + my $classfactory = MogileFS::Factory::Class->get_factory; + return $self->SUPER::set(MogileFS::NewDomain->new_from_args($args, $classfactory)); +} + +1; diff --git a/lib/MogileFS/Factory/Host.pm b/lib/MogileFS/Factory/Host.pm new file mode 100644 index 0000000..08b6554 --- /dev/null +++ b/lib/MogileFS/Factory/Host.pm @@ -0,0 +1,14 @@ +package MogileFS::Factory::Host; +use strict; +use warnings; +use base 'MogileFS::Factory'; + +use MogileFS::NewHost; + +sub set { + my ($self, $args) = @_; + my $devfactory = MogileFS::Factory::Device->get_factory; + return $self->SUPER::set(MogileFS::NewHost->new_from_args($args, $devfactory)); +} + +1; diff --git a/lib/MogileFS/NewClass.pm b/lib/MogileFS/NewClass.pm new file mode 100644 index 0000000..5bcf050 --- /dev/null +++ b/lib/MogileFS/NewClass.pm @@ -0,0 +1,54 @@ +package MogileFS::NewClass; +use strict; +use warnings; +use MogileFS::Util qw(throw); + +=head1 + +MogileFS::NewClass - Class class. + +=cut + +sub new_from_args { + my ($class, $args, $domain_factory) = @_; + return bless { + domain_factory => $domain_factory, + mindevcount => 2, + %{$args}, + }, $class; +} + +# Instance methods: + +sub id { $_[0]{classid} } +sub name { $_[0]{classname} } +sub mindevcount { $_[0]{mindevcount} } + +sub repl_policy_string { + my $self = shift; + return $self->{replpolicy} ? $self->{replpolicy} + : 'MultipleHosts()'; +} + +sub repl_policy_obj { + my $self = shift; + if (! $self->{_repl_policy_obj}) { + my $polstr = $self->repl_policy_string; + # Parses the string. + $self->{_repl_policy_obj} = + MogileFS::ReplicationPolicy->new_from_policy_string($polstr); + } + return $self->{_repl_policy_obj}; +} + +sub domain { + my $self = shift; + return $self->{domain_factory}->get_by_id($self->{dmid}); +} + +sub has_files { + my $self = shift; + return Mgd::get_store()->class_has_files($self->{dmid}, $self->id); +} + +1; diff --git a/lib/MogileFS/NewDevice.pm b/lib/MogileFS/NewDevice.pm new file mode 100644 index 0000000..bdae2eb --- /dev/null +++ b/lib/MogileFS/NewDevice.pm @@ -0,0 +1,296 @@ +package MogileFS::NewDevice; +use strict; +use warnings; +use Carp qw/croak/; +use MogileFS::Util qw(throw); +use MogileFS::Util qw(okay_args device_state error); + +=head1 + +MogileFS::NewDevice - device class + +=cut + +BEGIN { + my $testing = $ENV{TESTING} ? 1 : 0; + eval "sub TESTING () { $testing }"; +} + +my @fields = qw/hostid status weight observed_state mb_total mb_used mb_asof +utilization devid/; + +sub new_from_args { + my ($class, $args, $host_factory) = @_; + my $self = bless { + host_factory => $host_factory, + %{$args}, + }, $class; + + $self->host || die "No host for $self->{devid} (host $self->{hostid})"; + + croak "invalid device observed state '$self->{observed_state}', valid: writeable, readable, unreachable" + if $self->{observed_state} !~ /^(?:writeable|readable|unreachable)$/; + + return $self; +} + +# Instance methods + +sub id { return $_[0]{devid} } +sub name { return $_[0]{devid} } +sub status { return $_[0]{status} } +sub weight { return $_[0]{weight} } +sub hostid { return $_[0]{hostid} } + +# FIXME: This shouldn't be necessary anymore? +sub t_init { + my ($self, $hostid, $state) = @_; + + my $dstate = device_state($state) or + die "Bogus state"; + + $self->{hostid} = $hostid; + $self->{status} = $state; + $self->{observed_state} = "writeable"; + + # say it's 10% full, of 1GB + $self->{mb_total} = 1000; + $self->{mb_used} = 100; +} + +sub host { + my $self = shift; + return $self->{host_factory}->get_by_id($self->{hostid}); +} + +# returns 0 if not known, else [0,1] +sub percent_free { + my $self = shift; + return 0 unless $self->{mb_total} && defined $self->{mb_used}; + return 1 - ($self->{mb_used} / $self->{mb_total}); +} + +# returns undef if not known, else [0,1] +sub percent_full { + my $self = shift; + return undef unless $self->{mb_total} && defined $self->{mb_used}; + return $self->{mb_used} / $self->{mb_total}; +} + +# FIXME: $self->mb_free? +sub fields { + my $self = shift; + my @tofetch = @_ ? @_ : @fields; + my $ret = { map { $_ => $self->{$_} } @tofetch }; + return $ret; +} + +sub observed_utilization { + my $self = shift; + + if (TESTING) { + my $weight_varname = 'T_FAKE_IO_DEV' . $self->id; + return $ENV{$weight_varname} if defined $ENV{$weight_varname}; + } + + return $self->{utilization}; +} + +sub observed_writeable { + my $self = shift; + return 0 unless $self->{observed_state} && $self->{observed_state} eq 'writeable'; + my $host = $self->host or return 0; + return 0 unless $host->observed_reachable; + return 1; +} + +sub observed_readable { + my $self = shift; + return $self->{observed_state} && $self->{observed_state} eq 'readable'; +} + +sub observed_unreachable { + my $self = shift; + return $self->{observed_state} && $self->{observed_state} eq 'unreachable'; +} + +# FIXME: This pattern is weird. Store the object on new? +sub dstate { + my $ds = device_state($_[0]->status); + return $ds if $ds; + error("dev$_[0]->{devid} has bogus status '$_[0]->{status}', pretending 'down'"); + return device_state("down"); +} + +sub can_delete_from { + return $_[0]->dstate->can_delete_from; +} + +sub can_read_from { + return $_[0]->dstate->can_read_from; +} + +# FIXME: Is there a (unrelated to this code) bug where new files aren't tested +# against the free space limit before being stored or replicated somewhere? +sub should_get_new_files { + my $self = shift; + my $dstate = $self->dstate; + + return 0 unless $dstate->should_get_new_files; + return 0 unless $self->observed_writeable; + return 0 unless $self->host->should_get_new_files; + # have enough disk space? (default: 100MB) + my $min_free = MogileFS->config("min_free_space"); + return 0 if $self->{mb_total} && + $self->mb_free < $min_free; + + return 1; +} + +sub mb_free { + my $self = shift; + return $self->{mb_total} - $self->{mb_used}; +} + +sub mb_used { + return $_[0]->{mb_used}; +} + +# currently the same policy, but leaving it open for differences later. +sub should_get_replicated_files { + return $_[0]->should_get_new_files; +} + +sub not_on_hosts { + my ($self, @hosts) = @_; + my @hostids = map { ref($_) ? $_->hostid : $_ } @hosts; + my $my_hostid = $self->hostid; + return (grep { $my_hostid == $_ } @hostids) ? 0 : 1; +} + +# "cached" by nature of the monitor worker testing this. +sub doesnt_know_mkcol { + return $_[0]->{no_mkcol}; +} + +# Gross class-based singleton cache. +my %dir_made; # /dev/path -> $time +my $dir_made_lastclean = 0; +# returns 1 on success, 0 on failure +sub create_directory { + my ($self, $uri) = @_; + return 1 if $self->doesnt_know_mkcol; + + # rfc2518 says we "should" use a trailing slash. Some servers + # (nginx) appears to require it. + $uri .= '/' unless $uri =~ m/\/$/; + + return 1 if $dir_made{$uri}; + + my $hostid = $self->hostid; + my $host = $self->host; + my $hostip = $host->ip or return 0; + my $port = $host->http_port or return 0; + my $peer = "$hostip:$port"; + + my $sock = IO::Socket::INET->new(PeerAddr => $peer, Timeout => 1) + or return 0; + + print $sock "MKCOL $uri HTTP/1.0\r\n". + "Content-Length: 0\r\n\r\n"; + + my $ans = <$sock>; + + # if they don't support this method, remember that + if ($ans && $ans =~ m!HTTP/1\.[01] (400|501)!) { + $self->{no_mkcol} = 1; + # TODO: move this into method in *monitor* worker + return 1; + } + + return 0 unless $ans && $ans =~ m!^HTTP/1.[01] 2\d\d!; + + my $now = time(); + $dir_made{$uri} = $now; + + # cleanup %dir_made occasionally. + my $clean_interval = 300; # every 5 minutes. + if ($dir_made_lastclean < $now - $clean_interval) { + $dir_made_lastclean = $now; + foreach my $k (keys %dir_made) { + delete $dir_made{$k} if $dir_made{$k} < $now - 3600; + } + } + return 1; +} + +sub fid_list { + my ($self, %opts) = @_; + my $limit = delete $opts{limit}; + croak("No limit specified") unless $limit && $limit =~ /^\d+$/; + croak("Unknown options to fid_list") if %opts; + + my $sto = Mgd::get_store(); + my $fidids = $sto->get_fidids_by_device($self->devid, $limit); + return map { + MogileFS::FID->new($_) + } @{$fidids || []}; +} + +sub fid_chunks { + my ($self, %opts) = @_; + + my $sto = Mgd::get_store(); + # storage function does validation. + my $fidids = $sto->get_fidid_chunks_by_device(devid => $self->devid, %opts); + return map { + MogileFS::FID->new($_) + } @{$fidids || []}; +} + +sub forget_about { + my ($self, $fid) = @_; + Mgd::get_store()->remove_fidid_from_devid($fid->id, $self->id); + return 1; +} + +sub usage_url { + my $self = shift; + my $host = $self->host; + my $get_port = $host->http_get_port; + my $hostip = $host->ip; + return "http://$hostip:$get_port/dev$self->{devid}/usage"; +} + +sub can_change_to_state { + my ($self, $newstate) = @_; + # don't allow dead -> alive transitions. (yes, still possible + # to go dead -> readonly -> alive to bypass this, but this is + # all more of a user-education thing than an absolute policy) + return 0 if $self->dstate->is_perm_dead && $newstate eq 'alive'; + return 1; +} + +sub vivify_directories { + my ($self, $path) = @_; + + # $path is something like: + # http://10.0.0.26:7500/dev2/0/000/148/0000148056.fid + + # three directories we'll want to make: + # http://10.0.0.26:7500/dev2/0 + # http://10.0.0.26:7500/dev2/0/000 + # http://10.0.0.26:7500/dev2/0/000/148 + + croak "non-HTTP mode no longer supported" unless $path =~ /^http/; + return 0 unless $path =~ m!/dev(\d+)/(\d+)/(\d\d\d)/(\d\d\d)/\d+\.fid$!; + my ($devid, $p1, $p2, $p3) = ($1, $2, $3, $4); + + die "devid mismatch" unless $self->id == $devid; + + $self->create_directory("/dev$devid/$p1"); + $self->create_directory("/dev$devid/$p1/$p2"); + $self->create_directory("/dev$devid/$p1/$p2/$p3"); +} + +1; diff --git a/lib/MogileFS/NewDomain.pm b/lib/MogileFS/NewDomain.pm new file mode 100644 index 0000000..2011675 --- /dev/null +++ b/lib/MogileFS/NewDomain.pm @@ -0,0 +1,41 @@ +package MogileFS::NewDomain; +use strict; +use warnings; +use MogileFS::Util qw(throw); + +=head1 + +MogileFS::NewDomain - domain class. + +=cut + +sub new_from_args { + my ($class, $args, $class_factory) = @_; + return bless { + class_factory => $class_factory, + %{$args}, + }, $class; +} + +# Instance methods: + +sub id { $_[0]{dmid} } +sub name { $_[0]{namespace} } + +sub has_files { + my $self = shift; + return 1 if $Mgd::_T_DOM_HAS_FILES; + return Mgd::get_store()->domain_has_files($self->id); +} + +sub classes { + my $self = shift; + return $self->{class_factory}->get_all($self); +} + +sub class { + my $self = shift; + return $self->{class_factory}->get_by_name($self, $_[0]); +} + +1; diff --git a/lib/MogileFS/NewHost.pm b/lib/MogileFS/NewHost.pm new file mode 100644 index 0000000..a39646c --- /dev/null +++ b/lib/MogileFS/NewHost.pm @@ -0,0 +1,104 @@ +package MogileFS::NewHost; +use strict; +use warnings; +use MogileFS::Util qw(throw); +use Net::Netmask; +use Carp qw(croak); +use MogileFS::Connection::Mogstored; + +=head1 + +MogileFS::NewHost - host class + +=cut + +# Centralized here instead of three places. +my @fields = qw/hostid hostname hostip status http_port http_get_port altip altmask/; + +# TODO: Validate a few things: state, observed state. +sub new_from_args { + my ($class, $args, $dev_factory) = @_; + my $self = bless { + dev_factory => $dev_factory, + %{$args}, + }, $class; + + $self->{mask} = ($self->{altip} && $self->{altmask}) ? + Net::Netmask->new2($self->{altmask}) : undef; + + return $self; +} + +sub valid_state { + my ($class, $state) = @_; + return $state && $state =~ /^alive|dead|down$/; +} + +# Instance methods: + +sub id { $_[0]{hostid} } +sub name { $_[0]{hostname} } +sub hostname { $_[0]{hostname} } +sub hostip { $_[0]{hostip} } +sub status { $_[0]{status} } +sub http_port { $_[0]{http_port} } + +sub http_get_port { + return $_[0]->{http_get_port} || $_[0]->{http_port}; +} + +sub ip { + my $self = shift; + if ($self->{mask} && $self->{altip} && + ($MogileFS::REQ_altzone || ($MogileFS::REQ_client_ip && + $self->{mask}->match($MogileFS::REQ_client_ip)))) { + return $self->{altip}; + } else { + return $self->{hostip}; + } +} + +sub fields { + my $self = shift; + my @tofetch = @_ ? @_ : @fields; + return { map { $_ => $self->{$_} } @tofetch }; +} + +sub should_get_new_files { + return $_[0]->status eq 'alive'; +} + +sub t_init { + my $self = shift; + my $status = shift; + $self->{status} = $status; + $self->{observed_state} = "reachable"; +} + +sub observed_reachable { + my $self = shift; + return $self->{observed_state} && $self->{observed_state} eq 'reachable'; +} + +sub observed_unreachable { + my $self = shift; + return $self->{observed_state} && $self->{observed_state} eq 'unreachable'; +} + +# returns/creates a MogileFS::Connection::Mogstored object to the +# host's mogstored management/side-channel port (which starts +# unconnected, and only connects when you ask it to, with its sock +# method) +sub mogstored_conn { + my $self = shift; + return $self->{mogstored_conn} ||= + MogileFS::Connection::Mogstored->new($self->ip, $self->sidechannel_port); +} + +sub sidechannel_port { + # TODO: let this be configurable per-host? currently it's configured + # once for all machines. + MogileFS->config("mogstored_stream_port"); +} + +1; diff --git a/lib/MogileFS/Store.pm b/lib/MogileFS/Store.pm index a1d1883..3283841 100644 --- a/lib/MogileFS/Store.pm +++ b/lib/MogileFS/Store.pm @@ -341,6 +341,7 @@ sub conddup { my ($self, $code) = @_; my $rv = eval { $code->(); }; throw("dup") if $self->was_duplicate_error; + croak($@) if $@; return $rv; } @@ -735,6 +736,8 @@ sub delete_host { # return true if deleted, 0 if didn't exist, exception if error sub delete_domain { my ($self, $dmid) = @_; + throw("has_files") if $self->domain_has_files($dmid); + throw("has_classes") if $self->domain_has_classes($dmid); return $self->dbh->do("DELETE FROM domain WHERE dmid = ?", undef, $dmid); } @@ -745,6 +748,13 @@ sub domain_has_files { return $has_a_fid ? 1 : 0; } +sub domain_has_classes { + my ($self, $dmid) = @_; + my $has_a_class = $self->dbh->selectrow_array('SELECT classid FROM class WHERE dmid = ? LIMIT 1', + undef, $dmid); + return $has_a_class ? 1 : 0; +} + sub class_has_files { my ($self, $dmid, $clid) = @_; my $has_a_fid = $self->dbh->selectrow_array('SELECT fid FROM file WHERE dmid = ? AND classid = ? LIMIT 1', @@ -1042,6 +1052,18 @@ sub create_device { return 1; } +sub update_device { + my ($self, $devid, $to_update) = @_; + my @keys = sort keys %$to_update; + return unless @keys; + $self->conddup(sub { + $self->dbh->do("UPDATE device SET " . join('=?, ', @keys) + . "=? WHERE devid=?", undef, (map { $to_update->{$_} } @keys), + $devid); + }); + return 1; +} + sub update_device_usage { my $self = shift; my %arg = $self->_valid_params([qw(mb_total mb_used devid)], @_); @@ -1052,6 +1074,19 @@ sub update_device_usage { $self->condthrow; } +# This is unimplemented at the moment as we must verify: +# - no file_on rows exist +# - nothing in file_to_queue is going to attempt to use it +# - nothing in file_to_replicate is going to attempt to use it +# - it's already been marked dead +# - that all trackers are likely to know this :/ +# - ensure the devid can't be reused +# IE; the user can't mark it dead then remove it all at once and cause their +# cluster to implode. +sub delete_device { + die "Unimplemented; needs further testing"; +} + sub mark_fidid_unreachable { my ($self, $fidid) = @_; die "Your database does not support REPLACE! Reimplement mark_fidid_unreachable!" unless $self->can_replace; @@ -1077,6 +1112,7 @@ sub set_device_state { sub delete_class { my ($self, $dmid, $cid) = @_; + throw("has_files") if $self->class_has_files($dmid, $cid); eval { $self->dbh->do("DELETE FROM class WHERE dmid = ? AND classid = ?", undef, $dmid, $cid); }; @@ -1441,6 +1477,18 @@ sub create_domain { die "failed to make domain"; # FIXME: the above is racy. } +sub update_host { + my ($self, $hid, $to_update) = @_; + my @keys = sort keys %$to_update; + return unless @keys; + $self->conddup(sub { + $self->dbh->do("UPDATE host SET " . join('=?, ', @keys) + . "=? WHERE hostid=?", undef, (map { $to_update->{$_} } @keys), + $hid); + }); + return 1; +} + sub update_host_property { my ($self, $hostid, $col, $val) = @_; $self->conddup(sub { diff --git a/t/01-domain-class.t b/t/01-domain-class.t new file mode 100644 index 0000000..69336da --- /dev/null +++ b/t/01-domain-class.t @@ -0,0 +1,153 @@ +# -*-perl-*- + +use strict; +use warnings; +use Test::More; +use FindBin qw($Bin); + +use MogileFS::Server; +use MogileFS::Util qw(error_code); +use MogileFS::Test; +use MogileFS::Factory; +use MogileFS::Factory::Domain; +use MogileFS::Factory::Class; +use MogileFS::NewDomain; +use MogileFS::NewClass; + +use Data::Dumper qw/Dumper/; + +my $sto = eval { temp_store(); }; +if ($sto) { + plan tests => 33; +} else { + plan skip_all => "Can't create temporary test database: $@"; + exit 0; +} + +# Fetch the factories. +my $domfac = MogileFS::Factory::Domain->get_factory; +ok($domfac, "got a domain factory"); +my $classfac = MogileFS::Factory::Class->get_factory; +ok($classfac, "got a class factory"); + +# Ensure the inherited singleton is good. +ok($domfac != $classfac, "factories are not the same singleton"); + +{ + # Add in a test domain. + my $dom = $domfac->set({ dmid => 1, namespace => 'toast'}); + ok($dom, "made a new domain object"); + is($dom->id, 1, "domain id is 1"); + is($dom->name, 'toast', 'domain namespace is toast'); + + # Add in a test class. + my $cls = $classfac->set($dom, { classid => 1, dmid => 1, mindevcount => 3, + replpolicy => '', classname => 'fried'}); + ok($cls, "got a class object"); + is($cls->id, 1, "class id is 1"); + is($cls->name, 'fried', 'class name is fried'); + is(ref($cls->domain), 'MogileFS::NewDomain', + 'class can find a domain object'); +} + +# Add a few more classes and domains. +{ + my $dom2 = $domfac->set({ dmid => 2, namespace => 'harro' }); + $classfac->set($dom2, { classid => 1, dmid => 2, mindevcount => 2, + replpolicy => '', classname => 'red' }); + $classfac->set($dom2, { classid => 2, dmid => 2, mindevcount => 3, + replpolicy => 'MultipleHosts(2)', classname => 'green' }); + $classfac->set($dom2, { classid => 3, dmid => 2, mindevcount => 4, + replpolicy => 'MultipleHosts(5)', classname => 'blue' }); +} + +# Ensure the select and remove factory methods work. +{ + my $dom = $domfac->get_by_id(1); + is($dom->name, 'toast', 'got the right domain from get_by_id'); +} + +{ + my $dom = $domfac->get_by_name('harro'); + is($dom->id, 2, 'got the right domain from get_by_name'); +} + +{ + my @doms = $domfac->get_all; + is(scalar(@doms), 2, 'got two domains back from get_all'); + for (@doms) { + is(ref($_), 'MogileFS::NewDomain', 'and both are domains'); + } + isnt($doms[0]->id, $doms[1]->id, 'and both are not the same'); +} + +{ + my $dom = $domfac->get_by_name('harro'); + my $clsmap = $classfac->map_by_id($dom); + is(ref($clsmap), 'HASH', 'got a mapped class hash'); + is($clsmap->{2}->name, 'green', 'got the right class set'); + + $classfac->remove($clsmap->{2}); + + my $cls = $classfac->get_by_name($dom, 'green'); + ok(!$cls, "class removed from factory"); +} + +# Test the domain routines harder. +{ + my $dom = $domfac->get_by_name('harro'); + my @classes = $dom->classes; + is(scalar(@classes), 2, 'found two classes'); + + ok($dom->class('blue'), 'found the blue class'); + ok(!$dom->class('fried'), 'did not find the fried class'); +} + +# Test the class routines harder. +{ + my $dom = $domfac->get_by_name('harro'); + my $cls = $dom->class('blue'); + my $polobj = $cls->repl_policy_obj; + ok($polobj, 'class can create policy object'); +} + +# Add a domain and two classes to the DB. +{ + my $domid = $sto->create_domain('foo'); + ok($domid, 'new domain stored in database: ' . $domid); + + my $clsid1 = $sto->create_class($domid, 'bar'); + my $clsid2 = $sto->create_class($domid, 'baz'); + is($clsid1, 1, 'new class1 stored in database'); + is($clsid2, 2, 'new class2 stored in database'); + + ok($sto->update_class_mindevcount(dmid => $domid, classid => $clsid2, + mindevcount => 3), 'can set mindevcount'); + ok($sto->update_class_replpolicy(dmid => $domid, classid => $clsid2, + replpolicy => 'MultipleHosts(6)'), 'can set replpolicy'); + ok($sto->update_class_name(dmid => $domid, classid => $clsid2, + classname => 'boo'), 'can rename class'); +} + +{ + # Reload from the DB and confirm they came back the way they went in. + my %domains = $sto->get_all_domains; + ok(exists $domains{foo}, 'domain foo exists'); + is($domains{foo}, 1, 'and the id is 1'); + my @classes = $sto->get_all_classes; + is_deeply($classes[0], { + 'replpolicy' => undef, + 'dmid' => '1', + 'classid' => '1', + 'mindevcount' => '2', + 'classname' => 'bar' + }, 'class bar came back'); + # We edited class2 a bunch, make sure that all stuck. + is_deeply($classes[1], { + 'replpolicy' => 'MultipleHosts(6)', + 'dmid' => '1', + 'classid' => '2', + 'mindevcount' => '3', + 'classname' => 'boo' + }, 'class baz came back as boo'); +} diff --git a/t/02-host-device.t b/t/02-host-device.t new file mode 100644 index 0000000..bc2f157 --- /dev/null +++ b/t/02-host-device.t @@ -0,0 +1,119 @@ +# -*-perl-*- + +use strict; +use warnings; +use Test::More; +use FindBin qw($Bin); + +use MogileFS::Server; +use MogileFS::Util qw(error_code); +use MogileFS::Test; +use MogileFS::Factory; +use MogileFS::Factory::Host; +use MogileFS::Factory::Device; +use MogileFS::NewHost; +use MogileFS::NewDevice; + +use Data::Dumper qw/Dumper/; + +my $sto = eval { temp_store(); }; +if ($sto) { + plan tests => 21; +} else { + plan skip_all => "Can't create temporary test database: $@"; + exit 0; +} + +# Fetch the factories. +my $hostfac = MogileFS::Factory::Host->get_factory; +ok($hostfac, "got a host factory"); +my $devfac = MogileFS::Factory::Device->get_factory; +ok($devfac, "got a device factory"); + +MogileFS::Config->set_config_no_broadcast("min_free_space", 100); + +# Ensure the inherited singleton is good. +ok($hostfac != $devfac, "factories are not the same singleton"); + +{ + # Test host. + my $host = $hostfac->set({ hostid => 1, hostname => 'foo', hostip => +'127.0.0.5', status => 'alive', http_port => 7500, observed_state => +'reachable'}); + ok($host, 'made a new host object'); + is($host->id, 1, 'host id is 1'); + is($host->name, 'foo', 'host name is foo'); + + # Test device. + my $dev = $devfac->set({ devid => 1, hostid => 1, status => 'alive', +weight => 100, mb_total => 5000, mb_used => 300, mb_asof => 1295217165, +observed_state => 'writeable'}); + ok($dev, 'made a new dev object'); + is($dev->id, 1, 'dev id is 1'); + is($dev->host->name, 'foo', 'name of devs host is foo'); + ok($dev->can_delete_from, 'can_delete_from works'); + ok($dev->can_read_from, 'can_read_from works'); + ok($dev->should_get_new_files, 'should_get_new_files works'); + + $hostfac->remove($host); + $devfac->remove($dev); +} + +# Might be able to skip the factory tests, as domain/class cover those. + +{ + # Add a host and two devices to the DB. + my $hostid = $sto->create_host('foo', '127.0.0.7'); + is($hostid, 1, 'new host got id 1'); + + # returns 1 instead of the devid :( + # since this it the only place which doesn't autogenerate its id. + ok($sto->create_device(1, $hostid, 'alive'), 'created dev1'); + ok($sto->create_device(2, $hostid, 'down'), 'created dev2'); + + # Update host details to DB and ensure they stick. + ok($sto->update_host($hostid, { http_port => 6500, http_get_port => 6501 }), + 'updated host DB entry'); + # Update device details in DB and ensure they stick. + ok($sto->update_device(1, { mb_total => 150, mb_used => 8 }), + 'updated dev1 DB entry'); + ok($sto->update_device(2, { mb_total => 100, mb_used => 3, + status => 'dead' }), 'updated dev2 DB entry'); +} + +{ + # Reload from DB and confirm they match what we had before. + my @hosts = $sto->get_all_hosts; + my @devs = $sto->get_all_devices; + + is_deeply($hosts[0], { + 'http_get_port' => 6501, + 'status' => 'down', + 'http_port' => '6500', + 'hostip' => '127.0.0.7', + 'hostname' => 'foo', + 'hostid' => '1', + 'altip' => undef, + 'altmask' => undef + }, 'host is as expected'); + + is_deeply($devs[0], { + 'mb_total' => 150, + 'mb_used' => 8, + 'status' => 'alive', + 'devid' => '1', + 'weight' => '100', + 'mb_asof' => undef, + 'hostid' => '1' + }, 'dev1 is as expected'); + is_deeply($devs[1], { + 'mb_total' => 100, + 'mb_used' => 3, + 'status' => 'dead', + 'devid' => '2', + 'weight' => '100', + 'mb_asof' => undef, + 'hostid' => '1' + }, 'dev2 is as expected'); +} + -- 2.11.4.GIT