package MogileFS::Device; use strict; use warnings; use Carp qw(croak); use MogileFS::Config qw(DEVICE_SUMMARY_CACHE_TIMEOUT); use MogileFS::Util qw(okay_args device_state error); BEGIN { my $testing = $ENV{TESTING} ? 1 : 0; eval "sub TESTING () { $testing }"; } my %singleton; # devid -> instance my $last_load = 0; # unixtime we last reloaded devices from database my $all_loaded = 0; # bool: have we loaded all the devices? # throws "dup" on duplicate devid. returns new MogileFS::Device object on success. # %args include devid, hostid, and status (in (alive, down, readonly)) sub create { my ($pkg, %args) = @_; okay_args(\%args, qw(devid hostid status)); my $devid = Mgd::get_store()->create_device(@args{qw(devid hostid status)}); MogileFS::Device->invalidate_cache; return $pkg->of_devid($devid); } sub of_devid { my ($class, $devid) = @_; croak("Invalid devid") unless $devid; return $singleton{$devid} ||= bless { devid => $devid, no_mkcol => 0, _loaded => 0, }, $class; } sub t_wipe_singletons { %singleton = (); $last_load = time(); # fake it } sub t_init { my ($self, $hostid, $state) = @_; $self->{_loaded} = 1; 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 from_devid_and_hostname { my ($class, $devid, $hostname) = @_; my $dev = MogileFS::Device->of_devid($devid) or return undef; return undef unless $dev->exists; my $host = $dev->host; return undef unless $host && $host->exists && $host->hostname eq $hostname; return $dev; } sub vivify_directories { my ($class, $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); my $dev = MogileFS::Device->of_devid($devid); return 0 unless $dev->exists; $dev->create_directory("/dev$devid/$p1"); $dev->create_directory("/dev$devid/$p1/$p2"); $dev->create_directory("/dev$devid/$p1/$p2/$p3"); } # returns array of all MogileFS::Device objects sub devices { my $class = shift; MogileFS::Device->check_cache; return values %singleton; } # returns hashref of devid -> $device_obj # you're allowed to mess with this returned hashref sub map { my $class = shift; my $ret = {}; foreach my $d (MogileFS::Device->devices) { $ret->{$d->id} = $d; } return $ret; } sub reload_devices { my $class = shift; # mark them all invalid for now, until they're reloaded foreach my $dev (values %singleton) { $dev->{_loaded} = 0; } MogileFS::Host->check_cache; my $sto = Mgd::get_store(); foreach my $row ($sto->get_all_devices) { my $dev = MogileFS::Device->of_devid($row->{devid}); $dev->absorb_dbrow($row); } # get rid of ones that could've gone away: foreach my $devid (keys %singleton) { my $dev = $singleton{$devid}; delete $singleton{$devid} unless $dev->{_loaded} } $all_loaded = 1; $last_load = time(); } sub invalidate_cache { my $class = shift; # so next time it's invalid and won't be used old $last_load = 0; $all_loaded = 0; $_->{_loaded} = 0 foreach values %singleton; if (my $worker = MogileFS::ProcManager->is_child) { $worker->invalidate_meta("device"); } } sub check_cache { my $class = shift; my $now = $Mgd::nowish || time(); return if $last_load > $now - DEVICE_SUMMARY_CACHE_TIMEOUT; MogileFS::Device->reload_devices; } # -------------------------------------------------------------------------- sub devid { return $_[0]{devid} } sub id { return $_[0]{devid} } sub absorb_dbrow { my ($dev, $hashref) = @_; foreach my $k (qw(hostid mb_total mb_used mb_asof status weight)) { $dev->{$k} = $hashref->{$k}; } $dev->{$_} ||= 0 foreach qw(mb_total mb_used mb_asof); my $host = MogileFS::Host->of_hostid($dev->{hostid}); if ($host && $host->exists) { my $host_status = $host->status; die "No status" unless $host_status =~ /^\w+$/; # FIXME: not sure I like this, changing the in-memory version # of the configured status is. I'd rather this be calculated # in an accessor. if ($dev->{status} eq 'alive' && $host_status ne 'alive') { $dev->{status} = "down" } } else { if ($dev->{status} eq "dead") { # ignore dead devices without hosts. not a big deal. } else { die "No host for dev $dev->{devid} (host $dev->{hostid})"; } } $dev->{_loaded} = 1; } # returns 0 if not known, else [0,1] sub percent_free { my $dev = shift; $dev->_load; return 0 unless $dev->{mb_total} && defined $dev->{mb_used}; return 1 - ($dev->{mb_used} / $dev->{mb_total}); } # returns undef if not known, else [0,1] sub percent_full { my $dev = shift; $dev->_load; return undef unless $dev->{mb_total} && defined $dev->{mb_used}; return $dev->{mb_used} / $dev->{mb_total}; } our $util_no_broadcast = 0; sub set_observed_utilization { my ($dev, $util) = @_; $dev->{utilization} = $util; my $devid = $dev->id; return if $util_no_broadcast; my $worker = MogileFS::ProcManager->is_child or return; $worker->send_to_parent(":set_dev_utilization $devid $util"); } sub observed_utilization { my ($dev) = @_; if (TESTING) { my $weight_varname = 'T_FAKE_IO_DEV' . $dev->id; return $ENV{$weight_varname} if defined $ENV{$weight_varname}; } return $dev->{utilization}; } sub set_observed_state { my ($dev, $state) = @_; croak "set_observed_state() with invalid device state '$state', valid: writeable, readable, unreachable" if $state !~ /^(?:writeable|readable|unreachable)$/; $dev->{observed_state} = $state; } sub observed_writeable { my $dev = shift; return 0 unless $dev->{observed_state} && $dev->{observed_state} eq "writeable"; my $host = $dev->host or return 0; return 0 unless $host->observed_reachable; return 1; } sub observed_readable { my $dev = shift; return $dev->{observed_state} && $dev->{observed_state} eq "readable"; } sub observed_unreachable { my $dev = shift; return $dev->{observed_state} && $dev->{observed_state} eq "unreachable"; } # returns status as a string (SEE ALSO: dstate, returns DeviceState object, # which knows the traits/capabilities of that named state) sub status { my $dev = shift; $dev->_load; return $dev->{status}; } sub weight { my $dev = shift; $dev->_load; return $dev->{weight}; } 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 { my $self = shift; return $self->dstate->can_delete_from; } sub can_read_from { my $self = shift; return $self->dstate->can_read_from; } sub should_get_new_files { my $dev = shift; my $dstate = $dev->dstate; return 0 unless $dstate->should_get_new_files; return 0 unless $dev->observed_writeable; return 0 unless $dev->host->should_get_new_files; # have enough disk space? (default: 100MB) my $min_free = MogileFS->config("min_free_space"); return 0 if $dev->{mb_total} && $dev->mb_free < $min_free; return 1; } sub mb_free { my $self = shift; return $self->{mb_total} - $self->{mb_used}; } # currently the same policy, but leaving it open for differences later. sub should_get_replicated_files { my $dev = shift; return $dev->should_get_new_files; } sub not_on_hosts { my ($dev, @hosts) = @_; my @hostids = map { ref($_) ? $_->hostid : $_ } @hosts; my $my_hostid = $dev->hostid; return (grep { $my_hostid == $_ } @hostids) ? 0 : 1; } sub exists { my $dev = shift; $dev->_try_load; return $dev->{_loaded}; } sub host { my $dev = shift; return MogileFS::Host->of_hostid($dev->hostid); } sub hostid { my $dev = shift; $dev->_load; return $dev->{hostid}; } sub doesnt_know_mkcol { my $self = shift; # TODO: forget this periodically? maybe whenever host/device is observed down? # in case webserver changes. return $self->{no_mkcol}; } 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; 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|405|501)!) { $self->{no_mkcol} = 1; # TODO: move this into method on device, which propogates to parent # and also receive from parent. so all query workers share this knowledge 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 forget_about { my ($dev, $fid) = @_; Mgd::get_store()->remove_fidid_from_devid($fid->id, $dev->id); return 1; } sub usage_url { my $dev = shift; my $host = $dev->host; my $get_port = $host->http_get_port; my $hostip = $host->ip; return "http://$hostip:$get_port/dev$dev->{devid}/usage"; } sub overview_hashref { my $dev = shift; $dev->_load; my $ret = {}; foreach my $k (qw(devid hostid status weight observed_state mb_total mb_used mb_asof utilization)) { $ret->{$k} = $dev->{$k}; } $ret->{mb_free} = $dev->mb_free; return $ret; } sub set_weight { my ($dev, $weight) = @_; my $sto = Mgd::get_store(); $sto->set_device_weight($dev->id, $weight); MogileFS::Device->invalidate_cache; } sub set_state { my ($dev, $state) = @_; my $dstate = device_state($state) or die "Bogus state"; my $sto = Mgd::get_store(); $sto->set_device_state($dev->id, $state); MogileFS::Device->invalidate_cache; # wake a reaper process up from sleep to get started as soon as possible # on re-replication MogileFS::ProcManager->wake_a("reaper") if $dstate->should_wake_reaper; } # given the current state, can this device transition into the provided $newstate? 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 _load { return if $_[0]{_loaded}; MogileFS::Device->reload_devices; return if $_[0]{_loaded}; my $dev = shift; croak "Device $dev->{devid} doesn't exist.\n"; } sub _try_load { return if $_[0]{_loaded}; MogileFS::Device->reload_devices; } 1;