# vim: set cindent expandtab ts=4 sw=4:
#
# Copyright (c) 1998-2005 Chi-Keung Ho. All rights reserved.
#
# This programe is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# Extmail - a high-performance webmail to maildir
# $Id$
package Ext::DB;
use strict;

sub new {
    my $class = shift;
    my %opt = @_;

    my $self = {
        try1null    => $opt{try1null},
        use_flock    => ($opt{no_flock} ? 0 : 1)
        # XXX see INTERNAL_LOCK in include/sys_defs.h
    };
    bless $self, $class;

    return $self->tie(@_) if defined $opt{file}; # file path
    $self;
}

sub tie {
    my ($self, %opt) = @_;

    if ($opt{file} !~ /^(\w+):(.+)$/) {
        $self->{error} = "Cannot parse database name $opt{file}";
        return undef;
    }
    my $dbtype = $1;
    $opt{file} = $2; # this is trick

    $self = "Ext::DB::$dbtype"->new(%opt);
    if(!$self) {
        $self->{error} = "Database format $dbtype not implemented";
        return undef;
    }
    $self;
}

# perfomance tips: under high concurrency application, flocking is
# a must to advoid race condition, but it will hurt the performance,
# the current implemention does use_flock checking every time calling
# flock(), this is much more expensive!
#
# Under PIII450, 50,000 times calling insert(), without flock invokes
# in insert(), which simpliy comment it, use only 4-5 wall seconds.
#
# If we uncomment the flock() calling, time will raise up to 8 seconds!
# So please wait for better implemention :-)
#
# He zhiqiang (Chi-Keung Ho) <hzqbbc@hzqbbc.com>
sub flock {
    my ($self, $flags) = @_;

    return if not $self->{handle};

    if ($self->{use_flock}) {
        flock($self->{handle}, $flags) or die "flock: $!";
    } else {
        return ; # should log: "No locking implemented!\n";
    }
}

sub error {
    my ($self) = @_;
    return $self->{error};
}

package Ext::DB::DB_File;
use vars qw(@ISA);
@ISA = qw(Ext::DB);

use DB_File; # db-1.8.x
use Fcntl ':flock';
use Symbol;

sub new {
    my ($class, %opt) = @_;

    my $self = $class->SUPER::new(%opt);

    return $self->tie(%opt) if scalar keys %opt;
    $self;
}

sub setup_locking {
    my ($self) = @_;

    my $fd = $self->{db}->fd;
    $self->{handle} = gensym;
    open($self->{handle}, "+<&=$fd");
}

sub lookup {
    my ($self, $key) = @_;
    my $value;

    $self->flock(LOCK_SH);
    my $r = $self->{db}->get($key.($self->{try1null} ? "\0" : ''), $value);
    # die $r if $r and $r !~ /^DB_NOTFOUND/; # XXX this is BDB perl pkg does
    # DB_File only return 1 if record not found , so ignore it and return
    # undef, don't call die() and advoid up-level programe crash, :-)
    warn "$key not found\n" if $r;
    $self->flock(LOCK_UN);

    return $r ? undef : $value;
}

# an alias to insert. in DB level, update is a certain kind of
# insert.
sub update {
    shift->insert(@_);
}

sub insert {
    my ($self, $key, $value) = @_;

    $self->flock(LOCK_EX);
    my $r = $self->{db}->put($key.($self->{try1null} ? "\0" : ''), $value);
    die "Insert $key error\n" if $r;
    $self->flock(LOCK_UN);
}

# XXX update bunch of records, useful for a lot of insert operation, it
# will reduce sync calling times.
sub update_s {
    shift->insert_s(@_);
}

sub insert_s {
    my ($self, $ref) = @_;
    my $r = 0;

    $self->flock(LOCK_EX);
    foreach(keys %$ref) {
        $r = $self->{db}->put($_, $ref->{$_});
        die "Insert_s $_ error\n" if $r;
    }
    $self->flock(LOCK_UN);
}

sub delete {
    my ($self, $key) = @_;

    $self->flock(LOCK_EX);
    my $r = $self->{db}->del($key.($self->{try1null} ? "\0" : ''));
    $self->flock(LOCK_UN);
}

package Ext::DB::Hash;
use vars qw(@ISA);
@ISA = qw(Ext::DB::DB_File);
use DB_File;

sub tie {
    my ($self, %opt) = @_;
    if (not defined $opt{flags}) {
        $opt{flags} = O_RDONLY;
    }elsif(lc $opt{flags} eq 'write') {
        $opt{flags} = O_CREAT|O_RDWR;
    }else {
        $opt{flags} = O_RDONLY;
    }

    $self->{db} = tie my %h, "DB_File", $opt{file}, 
        $opt{flags}, 0666, $DB_HASH;

    if (not defined $self->{db}) {
        $self->{error} = "Cannot open $opt{file}: $!\n";
        return undef;
    }

    $self->{hash} = \%h;
    # no locking provide, see DB_File(3), use BerkeleyDB instead
    # $self->setup_locking;
    $self;
}

package Ext::DB::Btree;
use vars qw(@ISA);
@ISA = qw(Ext::DB::DB_File);
use DB_File;

sub tie {
    my ($self, %opt) = @_;
    if (not defined $opt{flags}) {
        $opt{flags} = O_RDONLY;
    }elsif(lc $opt{flags} eq 'write') {
        $opt{flags} = O_CREAT|O_RDWR;
    }else {
        $opt{flags} = O_RDONLY;
    }

    $self->{db} = tie my %h, "DB_File", $opt{file}, 
        $opt{flags}, 0666, $DB_BTREE;

    if (not defined $self->{db}) {
        $self->{error} = "Cannot open $opt{file}: $!\n";
        return undef;
    }

    $self->{hash} = \%h;
    # no locking provide, see DB_File(3), use BerkeleyDB instead
    # $self->setup_locking;
    $self;
}

1;


syntax highlighted by Code2HTML, v. 0.9.1