The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Mail::Decency::Helper::Database;

use Moose;

use version 0.74; our $VERSION = qv( "v0.1.4" );

use Data::Dumper;

use IPC::SysV qw/ IPC_PRIVATE IPC_CREAT S_IRWXU /;
use IPC::Semaphore;

use Time::HiRes qw/ usleep ualarm /;
use Carp qw/ confess /;

=head1 NAME

Mail::Decency::Helper::Database

=head1 DESCRIPTION

Base class for all databases

=head1 SYNPOSIS

Create a new datbaase like this:

    Mail::Decency::Helper::Database->create( MongoDB => $config_ref );


=head1 CLASS ATTRIBUTES

=head2 type : Str

The type of the database (DBD, MongoDB)

=cut

has type   => ( is => "rw", isa => "Str" );

=head2 logger : CodeRef

Log-Handler method

=cut

has logger => ( is => "rw", isa => "Mail::Decency::Helper::Logger" );

=head2 locker : IPC::Semaphore

=cut

has locker => ( is => "ro", predicate => "use_lock" );

=head2 locker_pid : Int

PID of process creating the semaphore

=cut

has locker_pid => ( is => "rw", isa => 'Int' );


=head1 METHODS


=head2 create $type, $args_ref

Returns a new instance of the create database object

    my $database = Mail::Decency::Helper::Database->create( DBD => $args_ref );

=over

=item * $type

Either DBD or MongoDB for now

=item * $args_ref

HashRef of constrauctions variabels for the module's new-method

=back

=cut

sub create {
    my ( $class, $type, $args_ref ) = @_;
    
    my $module = "Mail::Decency::Helper::Database::$type";
    my $ok = eval "use $module; 1";
    unless ( $ok ) {
        confess "Unsupported database '$type': $@\n";
    }
    
    # create locker
    # get a free share..
    my $locker = IPC::Semaphore->new( IPC_PRIVATE, 3, S_IRWXU | IPC_CREAT )
        or die "Cannot create IPC Semaphore for locking: $!\n";
    $locker->setall( (1) x 3 )
        or die "Cannot initial unlock semaphores: $!\n";
    
    # create and return instance
    my $obj;
    eval {
        $obj = $module->new(
            %$args_ref,
            type       => $type,
            locker     => $locker,
            locker_pid => $$
        );
    };
    die "Connection error for '$type': $@" if $@;
    return $obj;
}


=head2 DEMOLISH

Remove locker

=cut

sub DEMOLISH {
    my ( $self ) = @_;
    $self->locker->remove if $self->locker_pid == $$; # remove semaphore with parent process only
    $self->db->disconnect if $self->db;
    delete $self->{ db };
}


=head2 get $schema, $table, $search_ref

Searches and returns single entry from database

See parse_data method for return contexts.

=over

=item * $schema

The schema/context/prefix of the lookup.. eg "throttle" for throttle tables

=item * $table

The table/suffix of the lookup .. eg "sender_domain" for the "throttle_sender_domain" table

=item * $search_ref

HashRef of search attributes. Can be flat or nested

    $search_ref = { attribute => "value" }; # simple equals
    $search_ref = { attribute => { ">" => 123 } }; # complex "greater then"

=back


=head2 set $schema, $table, $search_ref, $data_ref

Writes to database. Could affect multiple entries.

=over

=item * $schema, $table, $search_ref

Set get method

=item * $data_ref

HashRef or scalar of the data to be saved. If scalar, it is will be converted into { data => "scalar" } 

=back


=head2 search $schema, $table, $search_ref

Returns a list of search results (in opposite to the get method). In scalar contexts it returns an ArrayRef instead

=over

=item * $schema, $table, $search_ref

Set get method

=back

=cut


=head2 update_data

Transforms flat (scalar) values into { data => $value } hashrefs

=cut

sub update_data {
    my ( $self, $data ) = @_;
    return $data if ref( $data );
    return { data => $data };
}

=head2 parse_data $data_ref

Transforms hashref values in an array context from { value => $value } to ( $value )

In array-context, it will return the content of the "data" field, if any

Can be modified in derived modules.

=cut

sub parse_data {
    my ( $self, $data ) = @_;
    return unless defined $data;
    return wantarray ? ( $data ) : { data => $data } unless ref( $data );
    return wantarray ? ( $data->{ data } ) : $data;
}



=head2 update_query $query_ref

Update method for search query. Can be overwritten/extended in derived modules.

=cut

sub update_query {
    my ( $self, $query_ref ) = @_;
    return $query_ref if ref( $query_ref );
    return { key => $query_ref };
}


=head2 do_lock

Locks via flock file

=cut

sub do_lock {
    my ( $self, $num ) = @_;
    $num ||= 0;
    
    my $locker = $self->locker;
    
    # !! ATTENTION !!
    #   the purpose of this locking is to ensure increments in multi-forking
    #   environment work. The purpose is NOT to assure absolute mutual
    #   exclusion. 
    #   worst case for data: some counter are not incremented
    #   worst case for process: slow response (not to speak of deadlock)
    #   the process needs overrule the (statistic) data needs.
    # !! ATTENTION !!
    my $deadlock = 1_500_000; # = 1.5 sec
    eval {
        $SIG{ ALRM } = sub {
            die "Deadlock timeout\n";
        };
        ualarm( $deadlock );
        $locker->op( $num, -1, 0 );
        ualarm( 0 );
    };
    if ( $@ ) {
        $locker->setval( $num, 0 );
        warn "Deadlock in $num blighted\n";
    }
}


=head2 do_unlock

Unlocks the flock

=cut

sub do_unlock {
    my ( $self, $num ) = @_;
    $num ||= 0;
    #$self->locker->write( 0, 0, 1 );
    $self->locker->op( $num, 1, 0 );
}

=head2 read_lock

Do read lock

=cut

sub read_lock {
    return shift->do_lock( 1 );
}

=head2 read_unlock

Do unlock read

=cut

sub read_unlock {
    return shift->do_unlock( 1 );
}



=head2 write_lock

Do read lock

=cut

sub write_lock {
    my ( $self ) = @_;
    $self->read_lock;
    $self->do_lock( 2 );
    return ;
}

=head2 write_unlock

Do unlock read

=cut

sub write_unlock {
    my ( $self ) = @_;
    $self->do_unlock( 2 );
    $self->read_unlock;
}

=head2 usr_lock

Custom locker

=cut

sub usr_lock {
    return shift->do_lock( 0 );
}

=head2 usr_lock

Custom locker

=cut

sub usr_unlock {
    return shift->do_unlock( 0 );
}



=head1 AUTHOR

Ulrich Kautz <uk@fortrabbit.de>

=head1 COPYRIGHT

Copyright (c) 2010 the L</AUTHOR> as listed above

=head1 LICENCSE

This library is free software and may be distributed under the same terms as perl itself.

=cut


1;