EMDIS-ECS

 view release on metacpan or  search on metacpan

lib/EMDIS/ECS/LockedHash.pm  view on Meta::CPAN


# ----------------------------------------------------------------------
# Constructor.  Requires name of the database file and name of lock file
# as parameters.  Also accepts optional lock timeout parameter.
sub new {
    my $class = shift;
    my $dbfile = shift;
    my $lockfile = shift;
    my $lock_timeout = shift;

    # validate aaarghs
    if(!defined $dbfile) {
        warn "EMDIS::ECS::LockedHash::new() failed: missing database file name.";
        return undef;
    }
    if(!defined $lockfile) {
        warn "EMDIS::ECS::LockedHash::new() failed: missing lock file name.";
        return undef;
    }
    $lock_timeout = 10 unless defined $lock_timeout;

    # define this object
    my $this = {};
    bless $this, $class;
    $this->{dbfile} = $dbfile;
    $this->{lockfile} = $lockfile;
    $this->{lock_timeout} = $lock_timeout;
    $this->{ERROR} = '';
    $this->{LOCK} = 0;
    $this->{TIED} = '';

    # open lock file and retain file handle
    if(!sysopen($this->{FH_LOCK}, $this->{lockfile}, O_RDWR|O_CREAT)) {
        warn "EMDIS::ECS::LockedHash::new() failed: unable to access lock file " .
            "'$this->{lockfile}': $!";
        return undef;
    }

    # tie/untie db file, to test whether it's accessible
    if(!$this->_tie()) {
        warn "EMDIS::ECS::LockedHash::new() failed: " . $this->ERROR;
        return undef;
    }
    $this->_untie;

    return $this;
}

# ----------------------------------------------------------------------
# set/get error description
sub ERROR {
    my $this = shift;
    my $err = shift;
    if(defined $err) {
        $this->{ERROR} = $err;
    }
    return $this->{ERROR};
}

# ----------------------------------------------------------------------
# set/get locked status indicator
sub LOCK {
    my $this = shift;
    my $status = shift;
    if(defined $status) {
        $this->{LOCK} = $status;
    }
    return $this->{LOCK};
}

# ----------------------------------------------------------------------
# set/get tied status indicator
sub TIED {
    my $this = shift;
    my $status = shift;
    if(defined $status) {
        $this->{TIED} = $status;
    }
    return $this->{TIED};
}

# ----------------------------------------------------------------------
# Read one key-value from the database under a shared lock
sub read {
    my $this = shift;
    my $key = shift;
    my $value = undef;

    $this->ERROR('');  # reset error status
    # check lock status
    if($this->LOCK != LOCK_SH and $this->LOCK != LOCK_EX) {
        $this->ERROR(
            "EMDIS::ECS::LockedHash::read() requires shared or exclusive lock.");
        return undef;
    }
    # read value from hash
    $value = undef;
    $value = $this->{hash}->{$key} if exists $this->{hash}->{$key};
    if(defined($value) and ($value =~ /^\$\w+\s*=\s*\{.*\}\s*\;\s*$/s)) {
        # convert Dumper() string to hash ref
        $value =~ s/^\$\w+/\$value/;  # convert "$VAR1 = ..." to "$value = ..."
        eval($value);                 # eval "$value = ..." string
    }
    return $value;
}

# ----------------------------------------------------------------------
# Write one key-value to the database under an exclusive lock
sub write {
    my $this = shift;
    my $key = shift;
    my $value = shift;

    $this->ERROR('');  # reset error status
    # check lock status
    if($this->LOCK != LOCK_EX) {
        $this->ERROR("EMDIS::ECS::LockedHash::write() requires exclusive lock.");
        return '';
    }
    # write value to hash
    if(ref $value) {
        local $Data::Dumper::Indent = 0;
        $value = Dumper($value);   # convert ref to Dumper() string
    }
    $this->{hash}->{$key} = $value;
    return 1;  # successful
}

# ----------------------------------------------------------------------
# Delete a key-value under an exclusive lock
sub delete {
    my $this = shift;
    my $key = shift;
    my $value = undef;

    $this->ERROR('');  # reset error status
    # check lock status
    if($this->LOCK != LOCK_EX) {
        $this->ERROR("EMDIS::ECS::LockedHash::delete() requires exclusive lock.");
        return '';
    }
    # delete value from hash
    $value = delete $this->{hash}->{$key};
    return 1;  # successful
}

# ----------------------------------------------------------------------
# Return a list of key values under a shared lock
sub keys {
    my $this = shift;
    my @ks = ();

    $this->ERROR('');  # reset error status
    # check lock status
    if($this->LOCK != LOCK_SH and $this->LOCK != LOCK_EX) {
        $this->ERROR(
            "EMDIS::ECS::LockedHash::keys() requires shared or exclusive lock.");
        return '';
    }
    # get keys from hash
    @ks = keys %{$this->{hash}};
    return @ks;
}

# ----------------------------------------------------------------------
# Obtain (advisory) lock and tie internal hash to db file.
sub lock {
    my $this = shift;
    my $lock_type = shift;
    my $oldlock = $this->LOCK;
    $lock_type = LOCK_EX unless $lock_type;   # default = LOCK_EX
    $this->ERROR('');   # reset error status
    return 1 if $oldlock == $lock_type;       # already locked
    my $locked = 0;
    my $attempt = 0;
    while(!$locked and $attempt++ < 5) {
        sleep 2 if $attempt > 1;
        $this->ERROR('');   # reset error status
        $locked = $this->_lock($lock_type);
    }
    if(!$locked) {
        $this->ERROR("EMDIS::ECS::LockedHash::lock() failed: " . $this->ERROR);
        return '';
    }
    if(!$this->TIED and !$this->_tie()) {
        $this->ERROR("EMDIS::ECS::LockedHash::lock() failed: " . $this->ERROR);
        return '';
    }
    return 1;  # successful
}

# ----------------------------------------------------------------------
# Release (advisory) lock and untie internal hash.
sub unlock {
    my $this = shift;
    $this->_untie();
    $this->_unlock();
}

# ----------------------------------------------------------------------
# Quickly delete all key-values under an exclusive lock
sub undef {
    my $this = shift;

    $this->ERROR('');  # reset error status
    # check lock status
    if($this->LOCK != LOCK_EX) {
        $this->ERROR("EMDIS::ECS::LockedHash::undef() requires exclusive lock.");
        return '';
    }
    # delete everything from hash
    undef %{$this->{hash}};
    return 1;  # successful
}


# ----------------------------------------------------------------------
# untie hash and close lock file when perl object passes out of scope
sub DESTROY {
    my $this = shift;
    $this->_untie();
    close($this->{FH_LOCK})
        if defined $this->{FH_LOCK};
}

# ----------------------------------------------------------------------
# Select UNIX or Win32 version of _lock
sub _lock
{
    $^O =~ /MSWin32/ ? _lock_win32(@_) : _lock_unix(@_);
}

# ----------------------------------------------------------------------
# Internal subroutine:  obtain (advisory) lock, using time limit to
# avoid indefinite blocking.  Returns true if able to obtain lock within
# time limit;  otherwise returns false.
sub _lock_unix {
    my $this = shift;
    my $lock_type = shift;
    $lock_type = LOCK_EX unless defined $lock_type;
    my $result = 1;

    # set up "local" SIG_ALRM handler
    # (Note:  not using "local $SIG{PIPE}" because it ignores die())
    my $oldsigalrm = $SIG{ALRM};
    $SIG{ALRM} = sub {
        die "timeout - $this->{lock_timeout} second time limit exceeded\n";
    };

    # attempt to obtain lock, with time limit
    eval {
        alarm($this->{lock_timeout});   # set alarm
        die "flock() failed: $!\n"
            unless flock($this->{FH_LOCK}, $lock_type);
# File::lockf -- potential alternate locking method:
#        my $status = File::lockf::lock($this->{FH_LOCK}, 0);
#        die "lockf failed: $status\n"
#            if $status != 0;
        alarm(0);  # turn off alarm
    };
    if($@) {
        alarm(0);  # turn off alarm
        $this->ERROR("EMDIS::ECS::LockedHash::_lock_unix() failed: $@");
        $this->LOCK(0);  # reset status indicator
        $result = '';
    }
    # restore previous SIG_ALRM handler
    if(defined $oldsigalrm) { $SIG{ALRM} = $oldsigalrm; }
    else                    { delete $SIG{ALRM}; }
    $this->LOCK($lock_type)   # set status indicator
        if $result;
    return $result;  # successful
}

# ----------------------------------------------------------------------
# Internal subroutine:  obtain (advisory) lock, using time limit to
# avoid indefinite blocking.  Returns true if able to obtain lock within
# time limit;  otherwise returns false.
sub _lock_win32 {
    my $this = shift;
    my $lock_type = shift;
    $lock_type = LOCK_EX unless defined $lock_type;
    my $result = 1;

    # attempt to obtain lock, with time limit
    # (uses polling method to obtain lock -- somewhat more crude than
    # the unix method, which uses blocking with SIGALRM to enforce timeout)
    my $timeoutCount = 0;
    my $locked;
    while (!($locked = flock($this->{FH_LOCK}, $lock_type | LOCK_NB)) and 
        ($timeoutCount++ <= $this->{lock_timeout})) {
	sleep 1;
    }

    if(!$locked) {
        $this->ERROR("EMDIS::ECS::LockedHash::_lock_win32() failed: $@");
        $this->LOCK(0);  # reset status indicator
        $result = '';
    }
    $this->LOCK($lock_type)   # set status indicator
        if $result;
    return $result;  # successful
}

# ----------------------------------------------------------------------
# Internal subroutine:  tie hash to db file
sub _tie {
    my $this = shift;
    $this->{db_obj} = tie(%{$this->{hash}}, 'SDBM_File', $this->{dbfile},
        O_CREAT|O_RDWR, (defined $EMDIS::ECS::FILEMODE ? $EMDIS::ECS::FILEMODE : 0664))
        or $this->ERROR(
            "EMDIS::ECS::LockedHash::_tie() failed ($this->{dbfile}): $!");
    if($this->{db_obj}) {
        $this->TIED(1);     # set status indicator
    } else {
        $this->TIED('');    # reset status indicator
    }
    return $this->TIED;
}

# ----------------------------------------------------------------------
# Internal subroutine:  release (advisory) lock
sub _unlock {
    my $this = shift;
    flock($this->{FH_LOCK}, LOCK_UN);
# File::lockf -- potential alternate locking method:
#    my $status = File::lockf::ulock($this->{FH_LOCK}, 0);
    $this->LOCK(0);      # reset status indicator
}

# ----------------------------------------------------------------------
# Internal subroutine:  untie hash from db file
# (mainly, insure that output is flushed to disk)
sub _untie {
    my $this = shift;
    untie $this->{hash}
        if exists $this->{hash};
    delete $this->{hash};
    delete $this->{db_obj};
    $this->TIED('');        # reset status indicator
}

1;

__DATA__

# embedded POD documentation
# for more info:  man perlpod

=head1 NAME

EMDIS::ECS::LockedHash - tied hash (SDBM) with simple locking protocol

=head1 SYNOPSIS



( run in 0.835 second using v1.01-cache-2.11-cpan-5a3173703d6 )