From: rkinyon@cpan.org Date: Thu, 13 Nov 2008 03:22:08 +0000 (+0000) Subject: Checking in breakout of the various packages in DBM::Deep::Engine and documentation... X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=f0276afb03319e4eb8235ab9e89f54e6b58d7113;hp=ae6c15b8d42362196d4d9516a64e17bd6dcf494d;p=dbsrgits%2FDBM-Deep.git Checking in breakout of the various packages in DBM::Deep::Engine and documentation (however light) of the Iterators. THIS CHECKIN IS BROKEN - the tests will NOT pass. git-svn-id: http://svn.ali.as/cpan/trunk/DBM-Deep@4570 88f4d9cd-8a04-0410-9d60-8f63309c3137 --- diff --git a/MANIFEST b/MANIFEST index 305e63e..02e437f 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1,20 +1,28 @@ Build.PL Changes -Makefile.PL -MANIFEST -META.yml -README lib/DBM/Deep.pm lib/DBM/Deep.pod lib/DBM/Deep/Array.pm lib/DBM/Deep/Cookbook.pod lib/DBM/Deep/Engine.pm +lib/DBM/Deep/Engine/Sector.pm +lib/DBM/Deep/Engine/Sector/BucketList.pm +lib/DBM/Deep/Engine/Sector/Data.pm +lib/DBM/Deep/Engine/Sector/Index.pm +lib/DBM/Deep/Engine/Sector/Null.pm +lib/DBM/Deep/Engine/Sector/Reference.pm +lib/DBM/Deep/Engine/Sector/Scalar.pm lib/DBM/Deep/File.pm lib/DBM/Deep/Hash.pm lib/DBM/Deep/Internals.pod -utils/upgrade_db.pl -utils/lib/DBM/Deep/09830.pm -utils/lib/DBM/Deep/10002.pm +lib/DBM/Deep/Iterator.pm +lib/DBM/Deep/Iterator/BucketList.pm +lib/DBM/Deep/Iterator/Index.pm +lib/DBM/Deep/Null.pm +Makefile.PL +MANIFEST +META.yml +README t/01_basic.t t/02_hash.t t/03_bighash.t @@ -61,6 +69,9 @@ t/45_references.t t/46_blist_reindex.t t/47_odd_reference_behaviors.t t/48_autoexport_after_delete.t +t/50_deletes.t +t/52_memory_leak.t +t/53_misc_transactions.t t/97_dump_file.t t/98_pod.t t/99_pod_coverage.t @@ -69,6 +80,6 @@ t/etc/db-0-983 t/etc/db-0-99_04 t/etc/db-1-0000 t/etc/db-1-0003 -t/53_misc_transactions.t -t/50_deletes.t -t/52_memory_leak.t +utils/lib/DBM/Deep/09830.pm +utils/lib/DBM/Deep/10002.pm +utils/upgrade_db.pl diff --git a/lib/DBM/Deep/Engine.pm b/lib/DBM/Deep/Engine.pm index 3f61cc8..933027e 100644 --- a/lib/DBM/Deep/Engine.pm +++ b/lib/DBM/Deep/Engine.pm @@ -29,6 +29,16 @@ sub SIG_BLIST () { 'B' } sub SIG_FREE () { 'F' } sub SIG_SIZE () { 1 } +use DBM::Deep::Iterator::BucketList (); +use DBM::Deep::Iterator::Index (); +use DBM::Deep::Engine::Sector::Data (); +use DBM::Deep::Engine::Sector::BucketList (); +use DBM::Deep::Engine::Sector::Index (); +use DBM::Deep::Engine::Sector::Null (); +use DBM::Deep::Engine::Sector::Reference (); +use DBM::Deep::Engine::Sector::Scalar (); +use DBM::Deep::Null (); + my $STALE_SIZE = 2; # Please refer to the pack() documentation for further information @@ -132,10 +142,6 @@ hurts just to think about it. =head1 EXTERNAL METHODS -=cut - -################################################################################ - =head2 new() This takes a set of args. These args are described in the documentation for @@ -219,8 +225,6 @@ sub new { return $self; } -################################################################################ - =head2 read_value( $obj, $key ) This takes an object that provides _base_offset() and a string. It returns the @@ -562,8 +566,6 @@ sub get_next_key { return $obj->{iterator}->get_next_key( $obj ); } -################################################################################ - =head2 setup_fh( $obj ) This takes an object that provides _base_offset(). It will do everything needed @@ -816,8 +818,6 @@ sub unlock { return $rv; } -################################################################################ - =head1 INTERNAL METHODS The following methods are internal-use-only to DBM::Deep::Engine. @@ -990,8 +990,6 @@ sub clear_entries { delete $self->{entries}{$self->trans_id}; } -################################################################################ - =head2 _write_file_header() This writes the file header for a new file. This will write the various settings @@ -1283,8 +1281,6 @@ sub _request_sector { return $loc; } -################################################################################ - =head2 flush() This takes no arguments. It will do everything necessary to flush all things to @@ -1302,7 +1298,33 @@ sub flush { $self->storage->flush; } -################################################################################ +=head2 ACCESSORS + +The following are readonly attributes. + +=over 4 + +=item * storage + +=item * byte_size + +=item * hash_size + +=item * hash_chars + +=item * num_txns + +=item * max_buckets + +=item * blank_md5 + +=item * data_sector_size + +=item * txn_bitfield_len + +=back + +=cut sub storage { $_[0]{storage} } sub byte_size { $_[0]{byte_size} } @@ -1326,6 +1348,22 @@ sub txn_bitfield_len { return $self->{txn_bitfield_len}; } +=pod + +The following are read/write attributes. + +=over 4 + +=item * trans_id / set_trans_id( $new_id ) + +=item * trans_loc / set_trans_loc( $new_loc ) + +=item * chains_loc / set_chains_loc( $new_loc ) + +=back + +=cut + sub trans_id { $_[0]{trans_id} } sub set_trans_id { $_[0]{trans_id} = $_[1] } @@ -1338,6 +1376,13 @@ sub set_chains_loc { $_[0]{chains_loc} = $_[1] } sub cache { $_[0]{cache} ||= {} } sub clear_cache { %{$_[0]->cache} = () } +=head2 _dump_file() + +This method takes no arguments. It's used to print out a textual representation of the DBM::Deep +DB file. It assumes the file is not-corrupted. + +=cut + sub _dump_file { my $self = shift; @@ -1442,1322 +1487,5 @@ sub _dump_file { return $return; } -################################################################################ - -package DBM::Deep::Iterator; - -sub new { - my $class = shift; - my ($args) = @_; - - my $self = bless { - breadcrumbs => [], - engine => $args->{engine}, - base_offset => $args->{base_offset}, - }, $class; - - Scalar::Util::weaken( $self->{engine} ); - - return $self; -} - -sub reset { $_[0]{breadcrumbs} = [] } - -sub get_sector_iterator { - my $self = shift; - my ($loc) = @_; - - my $sector = $self->{engine}->_load_sector( $loc ) - or return; - - if ( $sector->isa( 'DBM::Deep::Engine::Sector::Index' ) ) { - return DBM::Deep::Iterator::Index->new({ - iterator => $self, - sector => $sector, - }); - } - elsif ( $sector->isa( 'DBM::Deep::Engine::Sector::BucketList' ) ) { - return DBM::Deep::Iterator::BucketList->new({ - iterator => $self, - sector => $sector, - }); - } - - DBM::Deep->_throw_error( "get_sector_iterator(): Why did $loc make a $sector?" ); -} - -sub get_next_key { - my $self = shift; - my ($obj) = @_; - - my $crumbs = $self->{breadcrumbs}; - my $e = $self->{engine}; - - unless ( @$crumbs ) { - # This will be a Reference sector - my $sector = $e->_load_sector( $self->{base_offset} ) - # If no sector is found, thist must have been deleted from under us. - or return; - - if ( $sector->staleness != $obj->_staleness ) { - return; - } - - my $loc = $sector->get_blist_loc - or return; - - push @$crumbs, $self->get_sector_iterator( $loc ); - } - - FIND_NEXT_KEY: { - # We're at the end. - unless ( @$crumbs ) { - $self->reset; - return; - } - - my $iterator = $crumbs->[-1]; - - # This level is done. - if ( $iterator->at_end ) { - pop @$crumbs; - redo FIND_NEXT_KEY; - } - - if ( $iterator->isa( 'DBM::Deep::Iterator::Index' ) ) { - # If we don't have any more, it will be caught at the - # prior check. - if ( my $next = $iterator->get_next_iterator ) { - push @$crumbs, $next; - } - redo FIND_NEXT_KEY; - } - - unless ( $iterator->isa( 'DBM::Deep::Iterator::BucketList' ) ) { - DBM::Deep->_throw_error( - "Should have a bucketlist iterator here - instead have $iterator" - ); - } - - # At this point, we have a BucketList iterator - my $key = $iterator->get_next_key; - if ( defined $key ) { - return $key; - } - #XXX else { $iterator->set_to_end() } ? - - # We hit the end of the bucketlist iterator, so redo - redo FIND_NEXT_KEY; - } - - DBM::Deep->_throw_error( "get_next_key(): How did we get here?" ); -} - -package DBM::Deep::Iterator::Index; - -sub new { - my $self = bless $_[1] => $_[0]; - $self->{curr_index} = 0; - return $self; -} - -sub at_end { - my $self = shift; - return $self->{curr_index} >= $self->{iterator}{engine}->hash_chars; -} - -sub get_next_iterator { - my $self = shift; - - my $loc; - while ( !$loc ) { - return if $self->at_end; - $loc = $self->{sector}->get_entry( $self->{curr_index}++ ); - } - - return $self->{iterator}->get_sector_iterator( $loc ); -} - -package DBM::Deep::Iterator::BucketList; - -sub new { - my $self = bless $_[1] => $_[0]; - $self->{curr_index} = 0; - return $self; -} - -sub at_end { - my $self = shift; - return $self->{curr_index} >= $self->{iterator}{engine}->max_buckets; -} - -sub get_next_key { - my $self = shift; - - return if $self->at_end; - - my $idx = $self->{curr_index}++; - - my $data_loc = $self->{sector}->get_data_location_for({ - allow_head => 1, - idx => $idx, - }) or return; - - #XXX Do we want to add corruption checks here? - return $self->{sector}->get_key_for( $idx )->data; -} - -package DBM::Deep::Engine::Sector; - -sub new { - my $self = bless $_[1], $_[0]; - Scalar::Util::weaken( $self->{engine} ); - $self->_init; - return $self; -} - -#sub _init {} -#sub clone { DBM::Deep->_throw_error( "Must be implemented in the child class" ); } - -sub engine { $_[0]{engine} } -sub offset { $_[0]{offset} } -sub type { $_[0]{type} } - -sub base_size { - my $self = shift; - return $self->engine->SIG_SIZE + $STALE_SIZE; -} - -sub free { - my $self = shift; - - my $e = $self->engine; - - $e->storage->print_at( $self->offset, $e->SIG_FREE ); - # Skip staleness counter - $e->storage->print_at( $self->offset + $self->base_size, - chr(0) x ($self->size - $self->base_size), - ); - - my $free_meth = $self->free_meth; - $e->$free_meth( $self->offset, $self->size ); - - return; -} - -package DBM::Deep::Engine::Sector::Data; - -our @ISA = qw( DBM::Deep::Engine::Sector ); - -# This is in bytes -sub size { $_[0]{engine}->data_sector_size } -sub free_meth { return '_add_free_data_sector' } - -sub clone { - my $self = shift; - return ref($self)->new({ - engine => $self->engine, - type => $self->type, - data => $self->data, - }); -} - -package DBM::Deep::Engine::Sector::Scalar; - -our @ISA = qw( DBM::Deep::Engine::Sector::Data ); - -sub free { - my $self = shift; - - my $chain_loc = $self->chain_loc; - - $self->SUPER::free(); - - if ( $chain_loc ) { - $self->engine->_load_sector( $chain_loc )->free; - } - - return; -} - -sub type { $_[0]{engine}->SIG_DATA } -sub _init { - my $self = shift; - - my $engine = $self->engine; - - unless ( $self->offset ) { - my $data_section = $self->size - $self->base_size - $engine->byte_size - 1; - - $self->{offset} = $engine->_request_data_sector( $self->size ); - - my $data = delete $self->{data}; - my $dlen = length $data; - my $continue = 1; - my $curr_offset = $self->offset; - while ( $continue ) { - - my $next_offset = 0; - - my ($leftover, $this_len, $chunk); - if ( $dlen > $data_section ) { - $leftover = 0; - $this_len = $data_section; - $chunk = substr( $data, 0, $this_len ); - - $dlen -= $data_section; - $next_offset = $engine->_request_data_sector( $self->size ); - $data = substr( $data, $this_len ); - } - else { - $leftover = $data_section - $dlen; - $this_len = $dlen; - $chunk = $data; - - $continue = 0; - } - - $engine->storage->print_at( $curr_offset, $self->type ); # Sector type - # Skip staleness - $engine->storage->print_at( $curr_offset + $self->base_size, - pack( $StP{$engine->byte_size}, $next_offset ), # Chain loc - pack( $StP{1}, $this_len ), # Data length - $chunk, # Data to be stored in this sector - chr(0) x $leftover, # Zero-fill the rest - ); - - $curr_offset = $next_offset; - } - - return; - } -} - -sub data_length { - my $self = shift; - - my $buffer = $self->engine->storage->read_at( - $self->offset + $self->base_size + $self->engine->byte_size, 1 - ); - - return unpack( $StP{1}, $buffer ); -} - -sub chain_loc { - my $self = shift; - return unpack( - $StP{$self->engine->byte_size}, - $self->engine->storage->read_at( - $self->offset + $self->base_size, - $self->engine->byte_size, - ), - ); -} - -sub data { - my $self = shift; -# my ($args) = @_; -# $args ||= {}; - - my $data; - while ( 1 ) { - my $chain_loc = $self->chain_loc; - - $data .= $self->engine->storage->read_at( - $self->offset + $self->base_size + $self->engine->byte_size + 1, $self->data_length, - ); - - last unless $chain_loc; - - $self = $self->engine->_load_sector( $chain_loc ); - } - - return $data; -} - -package DBM::Deep::Engine::Sector::Null; - -our @ISA = qw( DBM::Deep::Engine::Sector::Data ); - -sub type { $_[0]{engine}->SIG_NULL } -sub data_length { 0 } -sub data { return } - -sub _init { - my $self = shift; - - my $engine = $self->engine; - - unless ( $self->offset ) { - my $leftover = $self->size - $self->base_size - 1 * $engine->byte_size - 1; - - $self->{offset} = $engine->_request_data_sector( $self->size ); - $engine->storage->print_at( $self->offset, $self->type ); # Sector type - # Skip staleness counter - $engine->storage->print_at( $self->offset + $self->base_size, - pack( $StP{$engine->byte_size}, 0 ), # Chain loc - pack( $StP{1}, $self->data_length ), # Data length - chr(0) x $leftover, # Zero-fill the rest - ); - - return; - } -} - -package DBM::Deep::Engine::Sector::Reference; - -our @ISA = qw( DBM::Deep::Engine::Sector::Data ); - -sub _init { - my $self = shift; - - my $e = $self->engine; - - unless ( $self->offset ) { - my $classname = Scalar::Util::blessed( delete $self->{data} ); - my $leftover = $self->size - $self->base_size - 3 * $e->byte_size; - - my $class_offset = 0; - if ( defined $classname ) { - my $class_sector = DBM::Deep::Engine::Sector::Scalar->new({ - engine => $e, - data => $classname, - }); - $class_offset = $class_sector->offset; - } - - $self->{offset} = $e->_request_data_sector( $self->size ); - $e->storage->print_at( $self->offset, $self->type ); # Sector type - # Skip staleness counter - $e->storage->print_at( $self->offset + $self->base_size, - pack( $StP{$e->byte_size}, 0 ), # Index/BList loc - pack( $StP{$e->byte_size}, $class_offset ), # Classname loc - pack( $StP{$e->byte_size}, 1 ), # Initial refcount - chr(0) x $leftover, # Zero-fill the rest - ); - } - else { - $self->{type} = $e->storage->read_at( $self->offset, 1 ); - } - - $self->{staleness} = unpack( - $StP{$STALE_SIZE}, - $e->storage->read_at( $self->offset + $e->SIG_SIZE, $STALE_SIZE ), - ); - - return; -} - -sub staleness { $_[0]{staleness} } - -sub get_data_location_for { - my $self = shift; - my ($args) = @_; - - # Assume that the head is not allowed unless otherwise specified. - $args->{allow_head} = 0 unless exists $args->{allow_head}; - - # Assume we don't create a new blist location unless otherwise specified. - $args->{create} = 0 unless exists $args->{create}; - - my $blist = $self->get_bucket_list({ - key_md5 => $args->{key_md5}, - key => $args->{key}, - create => $args->{create}, - }); - return unless $blist && $blist->{found}; - - # At this point, $blist knows where the md5 is. What it -doesn't- know yet - # is whether or not this transaction has this key. That's part of the next - # function call. - my $location = $blist->get_data_location_for({ - allow_head => $args->{allow_head}, - }) or return; - - return $location; -} - -sub get_data_for { - my $self = shift; - my ($args) = @_; - - my $location = $self->get_data_location_for( $args ) - or return; - - return $self->engine->_load_sector( $location ); -} - -sub write_data { - my $self = shift; - my ($args) = @_; - - my $blist = $self->get_bucket_list({ - key_md5 => $args->{key_md5}, - key => $args->{key}, - create => 1, - }) or DBM::Deep->_throw_error( "How did write_data fail (no blist)?!" ); - - # Handle any transactional bookkeeping. - if ( $self->engine->trans_id ) { - if ( ! $blist->has_md5 ) { - $blist->mark_deleted({ - trans_id => 0, - }); - } - } - else { - my @trans_ids = $self->engine->get_running_txn_ids; - if ( $blist->has_md5 ) { - if ( @trans_ids ) { - my $old_value = $blist->get_data_for; - foreach my $other_trans_id ( @trans_ids ) { - next if $blist->get_data_location_for({ - trans_id => $other_trans_id, - allow_head => 0, - }); - $blist->write_md5({ - trans_id => $other_trans_id, - key => $args->{key}, - key_md5 => $args->{key_md5}, - value => $old_value->clone, - }); - } - } - } - else { - if ( @trans_ids ) { - foreach my $other_trans_id ( @trans_ids ) { - #XXX This doesn't seem to possible to ever happen . . . - next if $blist->get_data_location_for({ trans_id => $other_trans_id, allow_head => 0 }); - $blist->mark_deleted({ - trans_id => $other_trans_id, - }); - } - } - } - } - - #XXX Is this safe to do transactionally? - # Free the place we're about to write to. - if ( $blist->get_data_location_for({ allow_head => 0 }) ) { - $blist->get_data_for({ allow_head => 0 })->free; - } - - $blist->write_md5({ - key => $args->{key}, - key_md5 => $args->{key_md5}, - value => $args->{value}, - }); -} - -sub delete_key { - my $self = shift; - my ($args) = @_; - - # XXX What should happen if this fails? - my $blist = $self->get_bucket_list({ - key_md5 => $args->{key_md5}, - }) or DBM::Deep->_throw_error( "How did delete_key fail (no blist)?!" ); - - # Save the location so that we can free the data - my $location = $blist->get_data_location_for({ - allow_head => 0, - }); - my $old_value = $location && $self->engine->_load_sector( $location ); - - my @trans_ids = $self->engine->get_running_txn_ids; - - # If we're the HEAD and there are running txns, then we need to clone this value to the other - # transactions to preserve Isolation. - if ( $self->engine->trans_id == 0 ) { - if ( @trans_ids ) { - foreach my $other_trans_id ( @trans_ids ) { - next if $blist->get_data_location_for({ trans_id => $other_trans_id, allow_head => 0 }); - $blist->write_md5({ - trans_id => $other_trans_id, - key => $args->{key}, - key_md5 => $args->{key_md5}, - value => $old_value->clone, - }); - } - } - } - - my $data; - if ( @trans_ids ) { - $blist->mark_deleted( $args ); - - if ( $old_value ) { - $data = $old_value->data({ export => 1 }); - $old_value->free; - } - } - else { - $data = $blist->delete_md5( $args ); - } - - return $data; -} - -sub get_blist_loc { - my $self = shift; - - my $e = $self->engine; - my $blist_loc = $e->storage->read_at( $self->offset + $self->base_size, $e->byte_size ); - return unpack( $StP{$e->byte_size}, $blist_loc ); -} - -sub get_bucket_list { - my $self = shift; - my ($args) = @_; - $args ||= {}; - - # XXX Add in check here for recycling? - - my $engine = $self->engine; - - my $blist_loc = $self->get_blist_loc; - - # There's no index or blist yet - unless ( $blist_loc ) { - return unless $args->{create}; - - my $blist = DBM::Deep::Engine::Sector::BucketList->new({ - engine => $engine, - key_md5 => $args->{key_md5}, - }); - - $engine->storage->print_at( $self->offset + $self->base_size, - pack( $StP{$engine->byte_size}, $blist->offset ), - ); - - return $blist; - } - - my $sector = $engine->_load_sector( $blist_loc ) - or DBM::Deep->_throw_error( "Cannot read sector at $blist_loc in get_bucket_list()" ); - my $i = 0; - my $last_sector = undef; - while ( $sector->isa( 'DBM::Deep::Engine::Sector::Index' ) ) { - $blist_loc = $sector->get_entry( ord( substr( $args->{key_md5}, $i++, 1 ) ) ); - $last_sector = $sector; - if ( $blist_loc ) { - $sector = $engine->_load_sector( $blist_loc ) - or DBM::Deep->_throw_error( "Cannot read sector at $blist_loc in get_bucket_list()" ); - } - else { - $sector = undef; - last; - } - } - - # This means we went through the Index sector(s) and found an empty slot - unless ( $sector ) { - return unless $args->{create}; - - DBM::Deep->_throw_error( "No last_sector when attempting to build a new entry" ) - unless $last_sector; - - my $blist = DBM::Deep::Engine::Sector::BucketList->new({ - engine => $engine, - key_md5 => $args->{key_md5}, - }); - - $last_sector->set_entry( ord( substr( $args->{key_md5}, $i - 1, 1 ) ) => $blist->offset ); - - return $blist; - } - - $sector->find_md5( $args->{key_md5} ); - - # See whether or not we need to reindex the bucketlist - # Yes, the double-braces are there for a reason. if() doesn't create a redo-able block, - # so we have to create a bare block within the if() for redo-purposes. Patch and idea - # submitted by sprout@cpan.org. -RobK, 2008-01-09 - if ( !$sector->has_md5 && $args->{create} && $sector->{idx} == -1 ) {{ - my $redo; - - my $new_index = DBM::Deep::Engine::Sector::Index->new({ - engine => $engine, - }); - - my %blist_cache; - #XXX q.v. the comments for this function. - foreach my $entry ( $sector->chopped_up ) { - my ($spot, $md5) = @{$entry}; - my $idx = ord( substr( $md5, $i, 1 ) ); - - # XXX This is inefficient - my $blist = $blist_cache{$idx} - ||= DBM::Deep::Engine::Sector::BucketList->new({ - engine => $engine, - }); - - $new_index->set_entry( $idx => $blist->offset ); - - my $new_spot = $blist->write_at_next_open( $md5 ); - $engine->reindex_entry( $spot => $new_spot ); - } - - # Handle the new item separately. - { - my $idx = ord( substr( $args->{key_md5}, $i, 1 ) ); - - # If all the previous blist's items have been thrown into one - # blist and the new item belongs in there too, we need - # another index. - if ( keys %blist_cache == 1 and each %blist_cache == $idx ) { - ++$i, ++$redo; - } else { - my $blist = $blist_cache{$idx} - ||= DBM::Deep::Engine::Sector::BucketList->new({ - engine => $engine, - }); - - $new_index->set_entry( $idx => $blist->offset ); - - #XXX THIS IS HACKY! - $blist->find_md5( $args->{key_md5} ); - $blist->write_md5({ - key => $args->{key}, - key_md5 => $args->{key_md5}, - value => DBM::Deep::Engine::Sector::Null->new({ - engine => $engine, - data => undef, - }), - }); - } -# my $blist = $blist_cache{$idx} -# ||= DBM::Deep::Engine::Sector::BucketList->new({ -# engine => $engine, -# }); -# -# $new_index->set_entry( $idx => $blist->offset ); -# -# #XXX THIS IS HACKY! -# $blist->find_md5( $args->{key_md5} ); -# $blist->write_md5({ -# key => $args->{key}, -# key_md5 => $args->{key_md5}, -# value => DBM::Deep::Engine::Sector::Null->new({ -# engine => $engine, -# data => undef, -# }), -# }); - } - - if ( $last_sector ) { - $last_sector->set_entry( - ord( substr( $args->{key_md5}, $i - 1, 1 ) ), - $new_index->offset, - ); - } else { - $engine->storage->print_at( $self->offset + $self->base_size, - pack( $StP{$engine->byte_size}, $new_index->offset ), - ); - } - - $sector->clear; - $sector->free; - - if ( $redo ) { - (undef, $sector) = %blist_cache; - $last_sector = $new_index; - redo; - } - - $sector = $blist_cache{ ord( substr( $args->{key_md5}, $i, 1 ) ) }; - $sector->find_md5( $args->{key_md5} ); - }} - - return $sector; -} - -sub get_class_offset { - my $self = shift; - - my $e = $self->engine; - return unpack( - $StP{$e->byte_size}, - $e->storage->read_at( - $self->offset + $self->base_size + 1 * $e->byte_size, $e->byte_size, - ), - ); -} - -sub get_classname { - my $self = shift; - - my $class_offset = $self->get_class_offset; - - return unless $class_offset; - - return $self->engine->_load_sector( $class_offset )->data; -} - -sub data { - my $self = shift; - my ($args) = @_; - $args ||= {}; - - my $obj; - unless ( $obj = $self->engine->cache->{ $self->offset } ) { - $obj = DBM::Deep->new({ - type => $self->type, - base_offset => $self->offset, - staleness => $self->staleness, - storage => $self->engine->storage, - engine => $self->engine, - }); - - if ( $self->engine->storage->{autobless} ) { - my $classname = $self->get_classname; - if ( defined $classname ) { - bless $obj, $classname; - } - } - - $self->engine->cache->{$self->offset} = $obj; - } - - # We're not exporting, so just return. - unless ( $args->{export} ) { - return $obj; - } - - # We shouldn't export if this is still referred to. - if ( $self->get_refcount > 1 ) { - return $obj; - } - - return $obj->export; -} - -sub free { - my $self = shift; - - # We're not ready to be removed yet. - if ( $self->decrement_refcount > 0 ) { - return; - } - - # Rebless the object into DBM::Deep::Null. - eval { %{ $self->engine->cache->{ $self->offset } } = (); }; - eval { @{ $self->engine->cache->{ $self->offset } } = (); }; - bless $self->engine->cache->{ $self->offset }, 'DBM::Deep::Null'; - delete $self->engine->cache->{ $self->offset }; - - my $blist_loc = $self->get_blist_loc; - $self->engine->_load_sector( $blist_loc )->free if $blist_loc; - - my $class_loc = $self->get_class_offset; - $self->engine->_load_sector( $class_loc )->free if $class_loc; - - $self->SUPER::free(); -} - -sub increment_refcount { - my $self = shift; - - my $refcount = $self->get_refcount; - - $refcount++; - - $self->write_refcount( $refcount ); - - return $refcount; -} - -sub decrement_refcount { - my $self = shift; - - my $refcount = $self->get_refcount; - - $refcount--; - - $self->write_refcount( $refcount ); - - return $refcount; -} - -sub get_refcount { - my $self = shift; - - my $e = $self->engine; - return unpack( - $StP{$e->byte_size}, - $e->storage->read_at( - $self->offset + $self->base_size + 2 * $e->byte_size, $e->byte_size, - ), - ); -} - -sub write_refcount { - my $self = shift; - my ($num) = @_; - - my $e = $self->engine; - $e->storage->print_at( - $self->offset + $self->base_size + 2 * $e->byte_size, - pack( $StP{$e->byte_size}, $num ), - ); -} - -package DBM::Deep::Engine::Sector::BucketList; - -our @ISA = qw( DBM::Deep::Engine::Sector ); - -sub _init { - my $self = shift; - - my $engine = $self->engine; - - unless ( $self->offset ) { - my $leftover = $self->size - $self->base_size; - - $self->{offset} = $engine->_request_blist_sector( $self->size ); - $engine->storage->print_at( $self->offset, $engine->SIG_BLIST ); # Sector type - # Skip staleness counter - $engine->storage->print_at( $self->offset + $self->base_size, - chr(0) x $leftover, # Zero-fill the data - ); - } - - if ( $self->{key_md5} ) { - $self->find_md5; - } - - return $self; -} - -sub clear { - my $self = shift; - $self->engine->storage->print_at( $self->offset + $self->base_size, - chr(0) x ($self->size - $self->base_size), # Zero-fill the data - ); -} - -sub size { - my $self = shift; - unless ( $self->{size} ) { - my $e = $self->engine; - # Base + numbuckets * bucketsize - $self->{size} = $self->base_size + $e->max_buckets * $self->bucket_size; - } - return $self->{size}; -} - -sub free_meth { return '_add_free_blist_sector' } - -sub free { - my $self = shift; - - my $e = $self->engine; - foreach my $bucket ( $self->chopped_up ) { - my $rest = $bucket->[-1]; - - # Delete the keysector - my $l = unpack( $StP{$e->byte_size}, substr( $rest, $e->hash_size, $e->byte_size ) ); - my $s = $e->_load_sector( $l ); $s->free if $s; - - # Delete the HEAD sector - $l = unpack( $StP{$e->byte_size}, - substr( $rest, - $e->hash_size + $e->byte_size, - $e->byte_size, - ), - ); - $s = $e->_load_sector( $l ); $s->free if $s; - - foreach my $txn ( 0 .. $e->num_txns - 2 ) { - my $l = unpack( $StP{$e->byte_size}, - substr( $rest, - $e->hash_size + 2 * $e->byte_size + $txn * ($e->byte_size + $STALE_SIZE), - $e->byte_size, - ), - ); - my $s = $e->_load_sector( $l ); $s->free if $s; - } - } - - $self->SUPER::free(); -} - -sub bucket_size { - my $self = shift; - unless ( $self->{bucket_size} ) { - my $e = $self->engine; - # Key + head (location) + transactions (location + staleness-counter) - my $location_size = $e->byte_size + $e->byte_size + ($e->num_txns - 1) * ($e->byte_size + $STALE_SIZE); - $self->{bucket_size} = $e->hash_size + $location_size; - } - return $self->{bucket_size}; -} - -# XXX This is such a poor hack. I need to rethink this code. -sub chopped_up { - my $self = shift; - - my $e = $self->engine; - - my @buckets; - foreach my $idx ( 0 .. $e->max_buckets - 1 ) { - my $spot = $self->offset + $self->base_size + $idx * $self->bucket_size; - my $md5 = $e->storage->read_at( $spot, $e->hash_size ); - - #XXX If we're chopping, why would we ever have the blank_md5? - last if $md5 eq $e->blank_md5; - - my $rest = $e->storage->read_at( undef, $self->bucket_size - $e->hash_size ); - push @buckets, [ $spot, $md5 . $rest ]; - } - - return @buckets; -} - -sub write_at_next_open { - my $self = shift; - my ($entry) = @_; - - #XXX This is such a hack! - $self->{_next_open} = 0 unless exists $self->{_next_open}; - - my $spot = $self->offset + $self->base_size + $self->{_next_open}++ * $self->bucket_size; - $self->engine->storage->print_at( $spot, $entry ); - - return $spot; -} - -sub has_md5 { - my $self = shift; - unless ( exists $self->{found} ) { - $self->find_md5; - } - return $self->{found}; -} - -sub find_md5 { - my $self = shift; - - $self->{found} = undef; - $self->{idx} = -1; - - if ( @_ ) { - $self->{key_md5} = shift; - } - - # If we don't have an MD5, then what are we supposed to do? - unless ( exists $self->{key_md5} ) { - DBM::Deep->_throw_error( "Cannot find_md5 without a key_md5 set" ); - } - - my $e = $self->engine; - foreach my $idx ( 0 .. $e->max_buckets - 1 ) { - my $potential = $e->storage->read_at( - $self->offset + $self->base_size + $idx * $self->bucket_size, $e->hash_size, - ); - - if ( $potential eq $e->blank_md5 ) { - $self->{idx} = $idx; - return; - } - - if ( $potential eq $self->{key_md5} ) { - $self->{found} = 1; - $self->{idx} = $idx; - return; - } - } - - return; -} - -sub write_md5 { - my $self = shift; - my ($args) = @_; - - DBM::Deep->_throw_error( "write_md5: no key" ) unless exists $args->{key}; - DBM::Deep->_throw_error( "write_md5: no key_md5" ) unless exists $args->{key_md5}; - DBM::Deep->_throw_error( "write_md5: no value" ) unless exists $args->{value}; - - my $engine = $self->engine; - - $args->{trans_id} = $engine->trans_id unless exists $args->{trans_id}; - - my $spot = $self->offset + $self->base_size + $self->{idx} * $self->bucket_size; - $engine->add_entry( $args->{trans_id}, $spot ); - - unless ($self->{found}) { - my $key_sector = DBM::Deep::Engine::Sector::Scalar->new({ - engine => $engine, - data => $args->{key}, - }); - - $engine->storage->print_at( $spot, - $args->{key_md5}, - pack( $StP{$engine->byte_size}, $key_sector->offset ), - ); - } - - my $loc = $spot - + $engine->hash_size - + $engine->byte_size; - - if ( $args->{trans_id} ) { - $loc += $engine->byte_size + ($args->{trans_id} - 1) * ( $engine->byte_size + $STALE_SIZE ); - - $engine->storage->print_at( $loc, - pack( $StP{$engine->byte_size}, $args->{value}->offset ), - pack( $StP{$STALE_SIZE}, $engine->get_txn_staleness_counter( $args->{trans_id} ) ), - ); - } - else { - $engine->storage->print_at( $loc, - pack( $StP{$engine->byte_size}, $args->{value}->offset ), - ); - } -} - -sub mark_deleted { - my $self = shift; - my ($args) = @_; - $args ||= {}; - - my $engine = $self->engine; - - $args->{trans_id} = $engine->trans_id unless exists $args->{trans_id}; - - my $spot = $self->offset + $self->base_size + $self->{idx} * $self->bucket_size; - $engine->add_entry( $args->{trans_id}, $spot ); - - my $loc = $spot - + $engine->hash_size - + $engine->byte_size; - - if ( $args->{trans_id} ) { - $loc += $engine->byte_size + ($args->{trans_id} - 1) * ( $engine->byte_size + $STALE_SIZE ); - - $engine->storage->print_at( $loc, - pack( $StP{$engine->byte_size}, 1 ), # 1 is the marker for deleted - pack( $StP{$STALE_SIZE}, $engine->get_txn_staleness_counter( $args->{trans_id} ) ), - ); - } - else { - $engine->storage->print_at( $loc, - pack( $StP{$engine->byte_size}, 1 ), # 1 is the marker for deleted - ); - } - -} - -sub delete_md5 { - my $self = shift; - my ($args) = @_; - - my $engine = $self->engine; - return undef unless $self->{found}; - - # Save the location so that we can free the data - my $location = $self->get_data_location_for({ - allow_head => 0, - }); - my $key_sector = $self->get_key_for; - - my $spot = $self->offset + $self->base_size + $self->{idx} * $self->bucket_size; - $engine->storage->print_at( $spot, - $engine->storage->read_at( - $spot + $self->bucket_size, - $self->bucket_size * ( $engine->max_buckets - $self->{idx} - 1 ), - ), - chr(0) x $self->bucket_size, - ); - - $key_sector->free; - - my $data_sector = $self->engine->_load_sector( $location ); - my $data = $data_sector->data({ export => 1 }); - $data_sector->free; - - return $data; -} - -sub get_data_location_for { - my $self = shift; - my ($args) = @_; - $args ||= {}; - - $args->{allow_head} = 0 unless exists $args->{allow_head}; - $args->{trans_id} = $self->engine->trans_id unless exists $args->{trans_id}; - $args->{idx} = $self->{idx} unless exists $args->{idx}; - - my $e = $self->engine; - - my $spot = $self->offset + $self->base_size - + $args->{idx} * $self->bucket_size - + $e->hash_size - + $e->byte_size; - - if ( $args->{trans_id} ) { - $spot += $e->byte_size + ($args->{trans_id} - 1) * ( $e->byte_size + $STALE_SIZE ); - } - - my $buffer = $e->storage->read_at( - $spot, - $e->byte_size + $STALE_SIZE, - ); - my ($loc, $staleness) = unpack( $StP{$e->byte_size} . ' ' . $StP{$STALE_SIZE}, $buffer ); - - # XXX Merge the two if-clauses below - if ( $args->{trans_id} ) { - # We have found an entry that is old, so get rid of it - if ( $staleness != (my $s = $e->get_txn_staleness_counter( $args->{trans_id} ) ) ) { - $e->storage->print_at( - $spot, - pack( $StP{$e->byte_size} . ' ' . $StP{$STALE_SIZE}, (0) x 2 ), - ); - $loc = 0; - } - } - - # If we're in a transaction and we never wrote to this location, try the - # HEAD instead. - if ( $args->{trans_id} && !$loc && $args->{allow_head} ) { - return $self->get_data_location_for({ - trans_id => 0, - allow_head => 1, - idx => $args->{idx}, - }); - } - - return $loc <= 1 ? 0 : $loc; -} - -sub get_data_for { - my $self = shift; - my ($args) = @_; - $args ||= {}; - - return unless $self->{found}; - my $location = $self->get_data_location_for({ - allow_head => $args->{allow_head}, - }); - return $self->engine->_load_sector( $location ); -} - -sub get_key_for { - my $self = shift; - my ($idx) = @_; - $idx = $self->{idx} unless defined $idx; - - if ( $idx >= $self->engine->max_buckets ) { - DBM::Deep->_throw_error( "get_key_for(): Attempting to retrieve $idx" ); - } - - my $location = $self->engine->storage->read_at( - $self->offset + $self->base_size + $idx * $self->bucket_size + $self->engine->hash_size, - $self->engine->byte_size, - ); - $location = unpack( $StP{$self->engine->byte_size}, $location ); - DBM::Deep->_throw_error( "get_key_for: No location?" ) unless $location; - - return $self->engine->_load_sector( $location ); -} - -package DBM::Deep::Engine::Sector::Index; - -our @ISA = qw( DBM::Deep::Engine::Sector ); - -sub _init { - my $self = shift; - - my $engine = $self->engine; - - unless ( $self->offset ) { - my $leftover = $self->size - $self->base_size; - - $self->{offset} = $engine->_request_index_sector( $self->size ); - $engine->storage->print_at( $self->offset, $engine->SIG_INDEX ); # Sector type - # Skip staleness counter - $engine->storage->print_at( $self->offset + $self->base_size, - chr(0) x $leftover, # Zero-fill the rest - ); - } - - return $self; -} - -#XXX Change here -sub size { - my $self = shift; - unless ( $self->{size} ) { - my $e = $self->engine; - $self->{size} = $self->base_size + $e->byte_size * $e->hash_chars; - } - return $self->{size}; -} - -sub free_meth { return '_add_free_index_sector' } - -sub free { - my $self = shift; - my $e = $self->engine; - - for my $i ( 0 .. $e->hash_chars - 1 ) { - my $l = $self->get_entry( $i ) or next; - $e->_load_sector( $l )->free; - } - - $self->SUPER::free(); -} - -sub _loc_for { - my $self = shift; - my ($idx) = @_; - return $self->offset + $self->base_size + $idx * $self->engine->byte_size; -} - -sub get_entry { - my $self = shift; - my ($idx) = @_; - - my $e = $self->engine; - - DBM::Deep->_throw_error( "get_entry: Out of range ($idx)" ) - if $idx < 0 || $idx >= $e->hash_chars; - - return unpack( - $StP{$e->byte_size}, - $e->storage->read_at( $self->_loc_for( $idx ), $e->byte_size ), - ); -} - -sub set_entry { - my $self = shift; - my ($idx, $loc) = @_; - - my $e = $self->engine; - - DBM::Deep->_throw_error( "set_entry: Out of range ($idx)" ) - if $idx < 0 || $idx >= $e->hash_chars; - - $self->engine->storage->print_at( - $self->_loc_for( $idx ), - pack( $StP{$e->byte_size}, $loc ), - ); -} - -# This was copied from MARCEL's Class::Null. However, I couldn't use it because -# I need an undef value, not an implementation of the Null Class pattern. -package DBM::Deep::Null; - -use overload - 'bool' => sub { undef }, - '""' => sub { undef }, - '0+' => sub { undef }, - fallback => 1, - nomethod => 'AUTOLOAD'; - -sub AUTOLOAD { return; } - 1; __END__ diff --git a/lib/DBM/Deep/Engine/Sector.pm b/lib/DBM/Deep/Engine/Sector.pm new file mode 100644 index 0000000..9fec5f2 --- /dev/null +++ b/lib/DBM/Deep/Engine/Sector.pm @@ -0,0 +1,45 @@ +package DBM::Deep::Engine::Sector; + +use 5.006_000; + +use strict; +use warnings FATAL => 'all'; + +sub new { + my $self = bless $_[1], $_[0]; + Scalar::Util::weaken( $self->{engine} ); + $self->_init; + return $self; +} + +#sub _init {} +#sub clone { DBM::Deep->_throw_error( "Must be implemented in the child class" ); } + +sub engine { $_[0]{engine} } +sub offset { $_[0]{offset} } +sub type { $_[0]{type} } + +sub base_size { + my $self = shift; + return $self->engine->SIG_SIZE + $STALE_SIZE; +} + +sub free { + my $self = shift; + + my $e = $self->engine; + + $e->storage->print_at( $self->offset, $e->SIG_FREE ); + # Skip staleness counter + $e->storage->print_at( $self->offset + $self->base_size, + chr(0) x ($self->size - $self->base_size), + ); + + my $free_meth = $self->free_meth; + $e->$free_meth( $self->offset, $self->size ); + + return; +} + +1; +__END__ diff --git a/lib/DBM/Deep/Engine/Sector/BucketList.pm b/lib/DBM/Deep/Engine/Sector/BucketList.pm new file mode 100644 index 0000000..e796e57 --- /dev/null +++ b/lib/DBM/Deep/Engine/Sector/BucketList.pm @@ -0,0 +1,366 @@ +package DBM::Deep::Engine::Sector::BucketList; + +use 5.006_000; + +use strict; +use warnings FATAL => 'all'; + +use base qw( DBM::Deep::Engine::Sector ); + +sub _init { + my $self = shift; + + my $engine = $self->engine; + + unless ( $self->offset ) { + my $leftover = $self->size - $self->base_size; + + $self->{offset} = $engine->_request_blist_sector( $self->size ); + $engine->storage->print_at( $self->offset, $engine->SIG_BLIST ); # Sector type + # Skip staleness counter + $engine->storage->print_at( $self->offset + $self->base_size, + chr(0) x $leftover, # Zero-fill the data + ); + } + + if ( $self->{key_md5} ) { + $self->find_md5; + } + + return $self; +} + +sub clear { + my $self = shift; + $self->engine->storage->print_at( $self->offset + $self->base_size, + chr(0) x ($self->size - $self->base_size), # Zero-fill the data + ); +} + +sub size { + my $self = shift; + unless ( $self->{size} ) { + my $e = $self->engine; + # Base + numbuckets * bucketsize + $self->{size} = $self->base_size + $e->max_buckets * $self->bucket_size; + } + return $self->{size}; +} + +sub free_meth { return '_add_free_blist_sector' } + +sub free { + my $self = shift; + + my $e = $self->engine; + foreach my $bucket ( $self->chopped_up ) { + my $rest = $bucket->[-1]; + + # Delete the keysector + my $l = unpack( $StP{$e->byte_size}, substr( $rest, $e->hash_size, $e->byte_size ) ); + my $s = $e->_load_sector( $l ); $s->free if $s; + + # Delete the HEAD sector + $l = unpack( $StP{$e->byte_size}, + substr( $rest, + $e->hash_size + $e->byte_size, + $e->byte_size, + ), + ); + $s = $e->_load_sector( $l ); $s->free if $s; + + foreach my $txn ( 0 .. $e->num_txns - 2 ) { + my $l = unpack( $StP{$e->byte_size}, + substr( $rest, + $e->hash_size + 2 * $e->byte_size + $txn * ($e->byte_size + $STALE_SIZE), + $e->byte_size, + ), + ); + my $s = $e->_load_sector( $l ); $s->free if $s; + } + } + + $self->SUPER::free(); +} + +sub bucket_size { + my $self = shift; + unless ( $self->{bucket_size} ) { + my $e = $self->engine; + # Key + head (location) + transactions (location + staleness-counter) + my $location_size = $e->byte_size + $e->byte_size + ($e->num_txns - 1) * ($e->byte_size + $STALE_SIZE); + $self->{bucket_size} = $e->hash_size + $location_size; + } + return $self->{bucket_size}; +} + +# XXX This is such a poor hack. I need to rethink this code. +sub chopped_up { + my $self = shift; + + my $e = $self->engine; + + my @buckets; + foreach my $idx ( 0 .. $e->max_buckets - 1 ) { + my $spot = $self->offset + $self->base_size + $idx * $self->bucket_size; + my $md5 = $e->storage->read_at( $spot, $e->hash_size ); + + #XXX If we're chopping, why would we ever have the blank_md5? + last if $md5 eq $e->blank_md5; + + my $rest = $e->storage->read_at( undef, $self->bucket_size - $e->hash_size ); + push @buckets, [ $spot, $md5 . $rest ]; + } + + return @buckets; +} + +sub write_at_next_open { + my $self = shift; + my ($entry) = @_; + + #XXX This is such a hack! + $self->{_next_open} = 0 unless exists $self->{_next_open}; + + my $spot = $self->offset + $self->base_size + $self->{_next_open}++ * $self->bucket_size; + $self->engine->storage->print_at( $spot, $entry ); + + return $spot; +} + +sub has_md5 { + my $self = shift; + unless ( exists $self->{found} ) { + $self->find_md5; + } + return $self->{found}; +} + +sub find_md5 { + my $self = shift; + + $self->{found} = undef; + $self->{idx} = -1; + + if ( @_ ) { + $self->{key_md5} = shift; + } + + # If we don't have an MD5, then what are we supposed to do? + unless ( exists $self->{key_md5} ) { + DBM::Deep->_throw_error( "Cannot find_md5 without a key_md5 set" ); + } + + my $e = $self->engine; + foreach my $idx ( 0 .. $e->max_buckets - 1 ) { + my $potential = $e->storage->read_at( + $self->offset + $self->base_size + $idx * $self->bucket_size, $e->hash_size, + ); + + if ( $potential eq $e->blank_md5 ) { + $self->{idx} = $idx; + return; + } + + if ( $potential eq $self->{key_md5} ) { + $self->{found} = 1; + $self->{idx} = $idx; + return; + } + } + + return; +} + +sub write_md5 { + my $self = shift; + my ($args) = @_; + + DBM::Deep->_throw_error( "write_md5: no key" ) unless exists $args->{key}; + DBM::Deep->_throw_error( "write_md5: no key_md5" ) unless exists $args->{key_md5}; + DBM::Deep->_throw_error( "write_md5: no value" ) unless exists $args->{value}; + + my $engine = $self->engine; + + $args->{trans_id} = $engine->trans_id unless exists $args->{trans_id}; + + my $spot = $self->offset + $self->base_size + $self->{idx} * $self->bucket_size; + $engine->add_entry( $args->{trans_id}, $spot ); + + unless ($self->{found}) { + my $key_sector = DBM::Deep::Engine::Sector::Scalar->new({ + engine => $engine, + data => $args->{key}, + }); + + $engine->storage->print_at( $spot, + $args->{key_md5}, + pack( $StP{$engine->byte_size}, $key_sector->offset ), + ); + } + + my $loc = $spot + + $engine->hash_size + + $engine->byte_size; + + if ( $args->{trans_id} ) { + $loc += $engine->byte_size + ($args->{trans_id} - 1) * ( $engine->byte_size + $STALE_SIZE ); + + $engine->storage->print_at( $loc, + pack( $StP{$engine->byte_size}, $args->{value}->offset ), + pack( $StP{$STALE_SIZE}, $engine->get_txn_staleness_counter( $args->{trans_id} ) ), + ); + } + else { + $engine->storage->print_at( $loc, + pack( $StP{$engine->byte_size}, $args->{value}->offset ), + ); + } +} + +sub mark_deleted { + my $self = shift; + my ($args) = @_; + $args ||= {}; + + my $engine = $self->engine; + + $args->{trans_id} = $engine->trans_id unless exists $args->{trans_id}; + + my $spot = $self->offset + $self->base_size + $self->{idx} * $self->bucket_size; + $engine->add_entry( $args->{trans_id}, $spot ); + + my $loc = $spot + + $engine->hash_size + + $engine->byte_size; + + if ( $args->{trans_id} ) { + $loc += $engine->byte_size + ($args->{trans_id} - 1) * ( $engine->byte_size + $STALE_SIZE ); + + $engine->storage->print_at( $loc, + pack( $StP{$engine->byte_size}, 1 ), # 1 is the marker for deleted + pack( $StP{$STALE_SIZE}, $engine->get_txn_staleness_counter( $args->{trans_id} ) ), + ); + } + else { + $engine->storage->print_at( $loc, + pack( $StP{$engine->byte_size}, 1 ), # 1 is the marker for deleted + ); + } +} + +sub delete_md5 { + my $self = shift; + my ($args) = @_; + + my $engine = $self->engine; + return undef unless $self->{found}; + + # Save the location so that we can free the data + my $location = $self->get_data_location_for({ + allow_head => 0, + }); + my $key_sector = $self->get_key_for; + + my $spot = $self->offset + $self->base_size + $self->{idx} * $self->bucket_size; + $engine->storage->print_at( $spot, + $engine->storage->read_at( + $spot + $self->bucket_size, + $self->bucket_size * ( $engine->max_buckets - $self->{idx} - 1 ), + ), + chr(0) x $self->bucket_size, + ); + + $key_sector->free; + + my $data_sector = $self->engine->_load_sector( $location ); + my $data = $data_sector->data({ export => 1 }); + $data_sector->free; + + return $data; +} + +sub get_data_location_for { + my $self = shift; + my ($args) = @_; + $args ||= {}; + + $args->{allow_head} = 0 unless exists $args->{allow_head}; + $args->{trans_id} = $self->engine->trans_id unless exists $args->{trans_id}; + $args->{idx} = $self->{idx} unless exists $args->{idx}; + + my $e = $self->engine; + + my $spot = $self->offset + $self->base_size + + $args->{idx} * $self->bucket_size + + $e->hash_size + + $e->byte_size; + + if ( $args->{trans_id} ) { + $spot += $e->byte_size + ($args->{trans_id} - 1) * ( $e->byte_size + $STALE_SIZE ); + } + + my $buffer = $e->storage->read_at( + $spot, + $e->byte_size + $STALE_SIZE, + ); + my ($loc, $staleness) = unpack( $StP{$e->byte_size} . ' ' . $StP{$STALE_SIZE}, $buffer ); + + # XXX Merge the two if-clauses below + if ( $args->{trans_id} ) { + # We have found an entry that is old, so get rid of it + if ( $staleness != (my $s = $e->get_txn_staleness_counter( $args->{trans_id} ) ) ) { + $e->storage->print_at( + $spot, + pack( $StP{$e->byte_size} . ' ' . $StP{$STALE_SIZE}, (0) x 2 ), + ); + $loc = 0; + } + } + + # If we're in a transaction and we never wrote to this location, try the + # HEAD instead. + if ( $args->{trans_id} && !$loc && $args->{allow_head} ) { + return $self->get_data_location_for({ + trans_id => 0, + allow_head => 1, + idx => $args->{idx}, + }); + } + + return $loc <= 1 ? 0 : $loc; +} + +sub get_data_for { + my $self = shift; + my ($args) = @_; + $args ||= {}; + + return unless $self->{found}; + my $location = $self->get_data_location_for({ + allow_head => $args->{allow_head}, + }); + return $self->engine->_load_sector( $location ); +} + +sub get_key_for { + my $self = shift; + my ($idx) = @_; + $idx = $self->{idx} unless defined $idx; + + if ( $idx >= $self->engine->max_buckets ) { + DBM::Deep->_throw_error( "get_key_for(): Attempting to retrieve $idx" ); + } + + my $location = $self->engine->storage->read_at( + $self->offset + $self->base_size + $idx * $self->bucket_size + $self->engine->hash_size, + $self->engine->byte_size, + ); + $location = unpack( $StP{$self->engine->byte_size}, $location ); + DBM::Deep->_throw_error( "get_key_for: No location?" ) unless $location; + + return $self->engine->_load_sector( $location ); +} + +1; +__END__ diff --git a/lib/DBM/Deep/Engine/Sector/Data.pm b/lib/DBM/Deep/Engine/Sector/Data.pm new file mode 100644 index 0000000..1e1f7e2 --- /dev/null +++ b/lib/DBM/Deep/Engine/Sector/Data.pm @@ -0,0 +1,24 @@ +package DBM::Deep::Engine::Sector::Data; + +use 5.006_000; + +use strict; +use warnings FATAL => 'all'; + +use base qw( DBM::Deep::Engine::Sector ); + +# This is in bytes +sub size { $_[0]{engine}->data_sector_size } +sub free_meth { return '_add_free_data_sector' } + +sub clone { + my $self = shift; + return ref($self)->new({ + engine => $self->engine, + type => $self->type, + data => $self->data, + }); +} + +1; +__END__ diff --git a/lib/DBM/Deep/Engine/Sector/Index.pm b/lib/DBM/Deep/Engine/Sector/Index.pm new file mode 100644 index 0000000..7f7bb0f --- /dev/null +++ b/lib/DBM/Deep/Engine/Sector/Index.pm @@ -0,0 +1,85 @@ +package DBM::Deep::Engine::Sector::Index; + +use base qw( DBM::Deep::Engine::Sector ); + +sub _init { + my $self = shift; + + my $engine = $self->engine; + + unless ( $self->offset ) { + my $leftover = $self->size - $self->base_size; + + $self->{offset} = $engine->_request_index_sector( $self->size ); + $engine->storage->print_at( $self->offset, $engine->SIG_INDEX ); # Sector type + # Skip staleness counter + $engine->storage->print_at( $self->offset + $self->base_size, + chr(0) x $leftover, # Zero-fill the rest + ); + } + + return $self; +} + +#XXX Change here +sub size { + my $self = shift; + unless ( $self->{size} ) { + my $e = $self->engine; + $self->{size} = $self->base_size + $e->byte_size * $e->hash_chars; + } + return $self->{size}; +} + +sub free_meth { return '_add_free_index_sector' } + +sub free { + my $self = shift; + my $e = $self->engine; + + for my $i ( 0 .. $e->hash_chars - 1 ) { + my $l = $self->get_entry( $i ) or next; + $e->_load_sector( $l )->free; + } + + $self->SUPER::free(); +} + +sub _loc_for { + my $self = shift; + my ($idx) = @_; + return $self->offset + $self->base_size + $idx * $self->engine->byte_size; +} + +sub get_entry { + my $self = shift; + my ($idx) = @_; + + my $e = $self->engine; + + DBM::Deep->_throw_error( "get_entry: Out of range ($idx)" ) + if $idx < 0 || $idx >= $e->hash_chars; + + return unpack( + $StP{$e->byte_size}, + $e->storage->read_at( $self->_loc_for( $idx ), $e->byte_size ), + ); +} + +sub set_entry { + my $self = shift; + my ($idx, $loc) = @_; + + my $e = $self->engine; + + DBM::Deep->_throw_error( "set_entry: Out of range ($idx)" ) + if $idx < 0 || $idx >= $e->hash_chars; + + $self->engine->storage->print_at( + $self->_loc_for( $idx ), + pack( $StP{$e->byte_size}, $loc ), + ); +} + +1; +__END__ diff --git a/lib/DBM/Deep/Engine/Sector/Null.pm b/lib/DBM/Deep/Engine/Sector/Null.pm new file mode 100644 index 0000000..53aab5e --- /dev/null +++ b/lib/DBM/Deep/Engine/Sector/Null.pm @@ -0,0 +1,36 @@ +package DBM::Deep::Engine::Sector::Null; + +use 5.006_000; + +use strict; +use warnings FATAL => 'all'; + +use base qw( DBM::Deep::Engine::Sector::Data ); + +sub type { $_[0]{engine}->SIG_NULL } +sub data_length { 0 } +sub data { return } + +sub _init { + my $self = shift; + + my $engine = $self->engine; + + unless ( $self->offset ) { + my $leftover = $self->size - $self->base_size - 1 * $engine->byte_size - 1; + + $self->{offset} = $engine->_request_data_sector( $self->size ); + $engine->storage->print_at( $self->offset, $self->type ); # Sector type + # Skip staleness counter + $engine->storage->print_at( $self->offset + $self->base_size, + pack( $StP{$engine->byte_size}, 0 ), # Chain loc + pack( $StP{1}, $self->data_length ), # Data length + chr(0) x $leftover, # Zero-fill the rest + ); + + return; + } +} + +1; +__END__ diff --git a/lib/DBM/Deep/Engine/Sector/Reference.pm b/lib/DBM/Deep/Engine/Sector/Reference.pm new file mode 100644 index 0000000..3de5551 --- /dev/null +++ b/lib/DBM/Deep/Engine/Sector/Reference.pm @@ -0,0 +1,506 @@ +package DBM::Deep::Engine::Sector::Reference; + +use 5.006_000; + +use strict; +use warnings FATAL => 'all'; + +use base qw( DBM::Deep::Engine::Sector::Data ); + +sub _init { + my $self = shift; + + my $e = $self->engine; + + unless ( $self->offset ) { + my $classname = Scalar::Util::blessed( delete $self->{data} ); + my $leftover = $self->size - $self->base_size - 3 * $e->byte_size; + + my $class_offset = 0; + if ( defined $classname ) { + my $class_sector = DBM::Deep::Engine::Sector::Scalar->new({ + engine => $e, + data => $classname, + }); + $class_offset = $class_sector->offset; + } + + $self->{offset} = $e->_request_data_sector( $self->size ); + $e->storage->print_at( $self->offset, $self->type ); # Sector type + # Skip staleness counter + $e->storage->print_at( $self->offset + $self->base_size, + pack( $StP{$e->byte_size}, 0 ), # Index/BList loc + pack( $StP{$e->byte_size}, $class_offset ), # Classname loc + pack( $StP{$e->byte_size}, 1 ), # Initial refcount + chr(0) x $leftover, # Zero-fill the rest + ); + } + else { + $self->{type} = $e->storage->read_at( $self->offset, 1 ); + } + + $self->{staleness} = unpack( + $StP{$STALE_SIZE}, + $e->storage->read_at( $self->offset + $e->SIG_SIZE, $STALE_SIZE ), + ); + + return; +} + +sub staleness { $_[0]{staleness} } + +sub get_data_location_for { + my $self = shift; + my ($args) = @_; + + # Assume that the head is not allowed unless otherwise specified. + $args->{allow_head} = 0 unless exists $args->{allow_head}; + + # Assume we don't create a new blist location unless otherwise specified. + $args->{create} = 0 unless exists $args->{create}; + + my $blist = $self->get_bucket_list({ + key_md5 => $args->{key_md5}, + key => $args->{key}, + create => $args->{create}, + }); + return unless $blist && $blist->{found}; + + # At this point, $blist knows where the md5 is. What it -doesn't- know yet + # is whether or not this transaction has this key. That's part of the next + # function call. + my $location = $blist->get_data_location_for({ + allow_head => $args->{allow_head}, + }) or return; + + return $location; +} + +sub get_data_for { + my $self = shift; + my ($args) = @_; + + my $location = $self->get_data_location_for( $args ) + or return; + + return $self->engine->_load_sector( $location ); +} + +sub write_data { + my $self = shift; + my ($args) = @_; + + my $blist = $self->get_bucket_list({ + key_md5 => $args->{key_md5}, + key => $args->{key}, + create => 1, + }) or DBM::Deep->_throw_error( "How did write_data fail (no blist)?!" ); + + # Handle any transactional bookkeeping. + if ( $self->engine->trans_id ) { + if ( ! $blist->has_md5 ) { + $blist->mark_deleted({ + trans_id => 0, + }); + } + } + else { + my @trans_ids = $self->engine->get_running_txn_ids; + if ( $blist->has_md5 ) { + if ( @trans_ids ) { + my $old_value = $blist->get_data_for; + foreach my $other_trans_id ( @trans_ids ) { + next if $blist->get_data_location_for({ + trans_id => $other_trans_id, + allow_head => 0, + }); + $blist->write_md5({ + trans_id => $other_trans_id, + key => $args->{key}, + key_md5 => $args->{key_md5}, + value => $old_value->clone, + }); + } + } + } + else { + if ( @trans_ids ) { + foreach my $other_trans_id ( @trans_ids ) { + #XXX This doesn't seem to possible to ever happen . . . + next if $blist->get_data_location_for({ trans_id => $other_trans_id, allow_head => 0 }); + $blist->mark_deleted({ + trans_id => $other_trans_id, + }); + } + } + } + } + + #XXX Is this safe to do transactionally? + # Free the place we're about to write to. + if ( $blist->get_data_location_for({ allow_head => 0 }) ) { + $blist->get_data_for({ allow_head => 0 })->free; + } + + $blist->write_md5({ + key => $args->{key}, + key_md5 => $args->{key_md5}, + value => $args->{value}, + }); +} + +sub delete_key { + my $self = shift; + my ($args) = @_; + + # XXX What should happen if this fails? + my $blist = $self->get_bucket_list({ + key_md5 => $args->{key_md5}, + }) or DBM::Deep->_throw_error( "How did delete_key fail (no blist)?!" ); + + # Save the location so that we can free the data + my $location = $blist->get_data_location_for({ + allow_head => 0, + }); + my $old_value = $location && $self->engine->_load_sector( $location ); + + my @trans_ids = $self->engine->get_running_txn_ids; + + # If we're the HEAD and there are running txns, then we need to clone this value to the other + # transactions to preserve Isolation. + if ( $self->engine->trans_id == 0 ) { + if ( @trans_ids ) { + foreach my $other_trans_id ( @trans_ids ) { + next if $blist->get_data_location_for({ trans_id => $other_trans_id, allow_head => 0 }); + $blist->write_md5({ + trans_id => $other_trans_id, + key => $args->{key}, + key_md5 => $args->{key_md5}, + value => $old_value->clone, + }); + } + } + } + + my $data; + if ( @trans_ids ) { + $blist->mark_deleted( $args ); + + if ( $old_value ) { + $data = $old_value->data({ export => 1 }); + $old_value->free; + } + } + else { + $data = $blist->delete_md5( $args ); + } + + return $data; +} + +sub get_blist_loc { + my $self = shift; + + my $e = $self->engine; + my $blist_loc = $e->storage->read_at( $self->offset + $self->base_size, $e->byte_size ); + return unpack( $StP{$e->byte_size}, $blist_loc ); +} + +sub get_bucket_list { + my $self = shift; + my ($args) = @_; + $args ||= {}; + + # XXX Add in check here for recycling? + + my $engine = $self->engine; + + my $blist_loc = $self->get_blist_loc; + + # There's no index or blist yet + unless ( $blist_loc ) { + return unless $args->{create}; + + my $blist = DBM::Deep::Engine::Sector::BucketList->new({ + engine => $engine, + key_md5 => $args->{key_md5}, + }); + + $engine->storage->print_at( $self->offset + $self->base_size, + pack( $StP{$engine->byte_size}, $blist->offset ), + ); + + return $blist; + } + + my $sector = $engine->_load_sector( $blist_loc ) + or DBM::Deep->_throw_error( "Cannot read sector at $blist_loc in get_bucket_list()" ); + my $i = 0; + my $last_sector = undef; + while ( $sector->isa( 'DBM::Deep::Engine::Sector::Index' ) ) { + $blist_loc = $sector->get_entry( ord( substr( $args->{key_md5}, $i++, 1 ) ) ); + $last_sector = $sector; + if ( $blist_loc ) { + $sector = $engine->_load_sector( $blist_loc ) + or DBM::Deep->_throw_error( "Cannot read sector at $blist_loc in get_bucket_list()" ); + } + else { + $sector = undef; + last; + } + } + + # This means we went through the Index sector(s) and found an empty slot + unless ( $sector ) { + return unless $args->{create}; + + DBM::Deep->_throw_error( "No last_sector when attempting to build a new entry" ) + unless $last_sector; + + my $blist = DBM::Deep::Engine::Sector::BucketList->new({ + engine => $engine, + key_md5 => $args->{key_md5}, + }); + + $last_sector->set_entry( ord( substr( $args->{key_md5}, $i - 1, 1 ) ) => $blist->offset ); + + return $blist; + } + + $sector->find_md5( $args->{key_md5} ); + + # See whether or not we need to reindex the bucketlist + # Yes, the double-braces are there for a reason. if() doesn't create a redo-able block, + # so we have to create a bare block within the if() for redo-purposes. Patch and idea + # submitted by sprout@cpan.org. -RobK, 2008-01-09 + if ( !$sector->has_md5 && $args->{create} && $sector->{idx} == -1 ) {{ + my $redo; + + my $new_index = DBM::Deep::Engine::Sector::Index->new({ + engine => $engine, + }); + + my %blist_cache; + #XXX q.v. the comments for this function. + foreach my $entry ( $sector->chopped_up ) { + my ($spot, $md5) = @{$entry}; + my $idx = ord( substr( $md5, $i, 1 ) ); + + # XXX This is inefficient + my $blist = $blist_cache{$idx} + ||= DBM::Deep::Engine::Sector::BucketList->new({ + engine => $engine, + }); + + $new_index->set_entry( $idx => $blist->offset ); + + my $new_spot = $blist->write_at_next_open( $md5 ); + $engine->reindex_entry( $spot => $new_spot ); + } + + # Handle the new item separately. + { + my $idx = ord( substr( $args->{key_md5}, $i, 1 ) ); + + # If all the previous blist's items have been thrown into one + # blist and the new item belongs in there too, we need + # another index. + if ( keys %blist_cache == 1 and each %blist_cache == $idx ) { + ++$i, ++$redo; + } else { + my $blist = $blist_cache{$idx} + ||= DBM::Deep::Engine::Sector::BucketList->new({ + engine => $engine, + }); + + $new_index->set_entry( $idx => $blist->offset ); + + #XXX THIS IS HACKY! + $blist->find_md5( $args->{key_md5} ); + $blist->write_md5({ + key => $args->{key}, + key_md5 => $args->{key_md5}, + value => DBM::Deep::Engine::Sector::Null->new({ + engine => $engine, + data => undef, + }), + }); + } +# my $blist = $blist_cache{$idx} +# ||= DBM::Deep::Engine::Sector::BucketList->new({ +# engine => $engine, +# }); +# +# $new_index->set_entry( $idx => $blist->offset ); +# +# #XXX THIS IS HACKY! +# $blist->find_md5( $args->{key_md5} ); +# $blist->write_md5({ +# key => $args->{key}, +# key_md5 => $args->{key_md5}, +# value => DBM::Deep::Engine::Sector::Null->new({ +# engine => $engine, +# data => undef, +# }), +# }); + } + + if ( $last_sector ) { + $last_sector->set_entry( + ord( substr( $args->{key_md5}, $i - 1, 1 ) ), + $new_index->offset, + ); + } else { + $engine->storage->print_at( $self->offset + $self->base_size, + pack( $StP{$engine->byte_size}, $new_index->offset ), + ); + } + + $sector->clear; + $sector->free; + + if ( $redo ) { + (undef, $sector) = %blist_cache; + $last_sector = $new_index; + redo; + } + + $sector = $blist_cache{ ord( substr( $args->{key_md5}, $i, 1 ) ) }; + $sector->find_md5( $args->{key_md5} ); + }} + + return $sector; +} + +sub get_class_offset { + my $self = shift; + + my $e = $self->engine; + return unpack( + $StP{$e->byte_size}, + $e->storage->read_at( + $self->offset + $self->base_size + 1 * $e->byte_size, $e->byte_size, + ), + ); +} + +sub get_classname { + my $self = shift; + + my $class_offset = $self->get_class_offset; + + return unless $class_offset; + + return $self->engine->_load_sector( $class_offset )->data; +} + +sub data { + my $self = shift; + my ($args) = @_; + $args ||= {}; + + my $obj; + unless ( $obj = $self->engine->cache->{ $self->offset } ) { + $obj = DBM::Deep->new({ + type => $self->type, + base_offset => $self->offset, + staleness => $self->staleness, + storage => $self->engine->storage, + engine => $self->engine, + }); + + if ( $self->engine->storage->{autobless} ) { + my $classname = $self->get_classname; + if ( defined $classname ) { + bless $obj, $classname; + } + } + + $self->engine->cache->{$self->offset} = $obj; + } + + # We're not exporting, so just return. + unless ( $args->{export} ) { + return $obj; + } + + # We shouldn't export if this is still referred to. + if ( $self->get_refcount > 1 ) { + return $obj; + } + + return $obj->export; +} + +sub free { + my $self = shift; + + # We're not ready to be removed yet. + if ( $self->decrement_refcount > 0 ) { + return; + } + + # Rebless the object into DBM::Deep::Null. + eval { %{ $self->engine->cache->{ $self->offset } } = (); }; + eval { @{ $self->engine->cache->{ $self->offset } } = (); }; + bless $self->engine->cache->{ $self->offset }, 'DBM::Deep::Null'; + delete $self->engine->cache->{ $self->offset }; + + my $blist_loc = $self->get_blist_loc; + $self->engine->_load_sector( $blist_loc )->free if $blist_loc; + + my $class_loc = $self->get_class_offset; + $self->engine->_load_sector( $class_loc )->free if $class_loc; + + $self->SUPER::free(); +} + +sub increment_refcount { + my $self = shift; + + my $refcount = $self->get_refcount; + + $refcount++; + + $self->write_refcount( $refcount ); + + return $refcount; +} + +sub decrement_refcount { + my $self = shift; + + my $refcount = $self->get_refcount; + + $refcount--; + + $self->write_refcount( $refcount ); + + return $refcount; +} + +sub get_refcount { + my $self = shift; + + my $e = $self->engine; + return unpack( + $StP{$e->byte_size}, + $e->storage->read_at( + $self->offset + $self->base_size + 2 * $e->byte_size, $e->byte_size, + ), + ); +} + +sub write_refcount { + my $self = shift; + my ($num) = @_; + + my $e = $self->engine; + $e->storage->print_at( + $self->offset + $self->base_size + 2 * $e->byte_size, + pack( $StP{$e->byte_size}, $num ), + ); +} + +1; +__END__ diff --git a/lib/DBM/Deep/Engine/Sector/Scalar.pm b/lib/DBM/Deep/Engine/Sector/Scalar.pm new file mode 100644 index 0000000..31e862f --- /dev/null +++ b/lib/DBM/Deep/Engine/Sector/Scalar.pm @@ -0,0 +1,120 @@ +package DBM::Deep::Engine::Sector::Scalar; + +use 5.006_000; + +use strict; +use warnings FATAL => 'all'; + +use base qw( DBM::Deep::Engine::Sector::Data ); + +sub free { + my $self = shift; + + my $chain_loc = $self->chain_loc; + + $self->SUPER::free(); + + if ( $chain_loc ) { + $self->engine->_load_sector( $chain_loc )->free; + } + + return; +} + +sub type { $_[0]{engine}->SIG_DATA } +sub _init { + my $self = shift; + + my $engine = $self->engine; + + unless ( $self->offset ) { + my $data_section = $self->size - $self->base_size - $engine->byte_size - 1; + + $self->{offset} = $engine->_request_data_sector( $self->size ); + + my $data = delete $self->{data}; + my $dlen = length $data; + my $continue = 1; + my $curr_offset = $self->offset; + while ( $continue ) { + + my $next_offset = 0; + + my ($leftover, $this_len, $chunk); + if ( $dlen > $data_section ) { + $leftover = 0; + $this_len = $data_section; + $chunk = substr( $data, 0, $this_len ); + + $dlen -= $data_section; + $next_offset = $engine->_request_data_sector( $self->size ); + $data = substr( $data, $this_len ); + } + else { + $leftover = $data_section - $dlen; + $this_len = $dlen; + $chunk = $data; + + $continue = 0; + } + + $engine->storage->print_at( $curr_offset, $self->type ); # Sector type + # Skip staleness + $engine->storage->print_at( $curr_offset + $self->base_size, + pack( $StP{$engine->byte_size}, $next_offset ), # Chain loc + pack( $StP{1}, $this_len ), # Data length + $chunk, # Data to be stored in this sector + chr(0) x $leftover, # Zero-fill the rest + ); + + $curr_offset = $next_offset; + } + + return; + } +} + +sub data_length { + my $self = shift; + + my $buffer = $self->engine->storage->read_at( + $self->offset + $self->base_size + $self->engine->byte_size, 1 + ); + + return unpack( $StP{1}, $buffer ); +} + +sub chain_loc { + my $self = shift; + return unpack( + $StP{$self->engine->byte_size}, + $self->engine->storage->read_at( + $self->offset + $self->base_size, + $self->engine->byte_size, + ), + ); +} + +sub data { + my $self = shift; +# my ($args) = @_; +# $args ||= {}; + + my $data; + while ( 1 ) { + my $chain_loc = $self->chain_loc; + + $data .= $self->engine->storage->read_at( + $self->offset + $self->base_size + $self->engine->byte_size + 1, $self->data_length, + ); + + last unless $chain_loc; + + $self = $self->engine->_load_sector( $chain_loc ); + } + + return $data; +} + +1; +__END__ diff --git a/lib/DBM/Deep/Iterator.pm b/lib/DBM/Deep/Iterator.pm new file mode 100644 index 0000000..f46b3a1 --- /dev/null +++ b/lib/DBM/Deep/Iterator.pm @@ -0,0 +1,169 @@ +package DBM::Deep::Iterator; + +use 5.006_000; + +use strict; +use warnings FATAL => 'all'; + +=head1 NAME + +DBM::Deep::Iterator + +=head1 PURPOSE + +This is an internal-use-only object for L. It is the iterator +for FIRSTKEY() and NEXTKEY(). + +=head1 OVERVIEW + +This object + +=head1 METHODS + +=head2 new(\%params) + +The constructor takes a hashref of params. The hashref is assumed to have the +following elements: + +=over 4 + +=item * engine (of type L + +=item * base_offset (the base_offset of the invoking DBM::Deep object) + +=back + +=cut + +sub new { + my $class = shift; + my ($args) = @_; + + my $self = bless { + breadcrumbs => [], + engine => $args->{engine}, + base_offset => $args->{base_offset}, + }, $class; + + Scalar::Util::weaken( $self->{engine} ); + + return $self; +} + +=head2 reset() + +This method takes no arguments. + +It will reset the iterator so that it will start from the beginning again. + +This method returns nothing. + +=cut + +sub reset { $_[0]{breadcrumbs} = [] } + +=head2 get_sector_iterator( $loc ) + +This takes a location. It will load the sector for $loc, then instantiate the right +iteartor type for it. + +This returns the sector iterator. + +=cut + +sub get_sector_iterator { + my $self = shift; + my ($loc) = @_; + + my $sector = $self->{engine}->_load_sector( $loc ) + or return; + + if ( $sector->isa( 'DBM::Deep::Engine::Sector::Index' ) ) { + return DBM::Deep::Iterator::Index->new({ + iterator => $self, + sector => $sector, + }); + } + elsif ( $sector->isa( 'DBM::Deep::Engine::Sector::BucketList' ) ) { + return DBM::Deep::Iterator::BucketList->new({ + iterator => $self, + sector => $sector, + }); + } + + DBM::Deep->_throw_error( "get_sector_iterator(): Why did $loc make a $sector?" ); +} + +=head2 get_next_key( $obj ) + +=cut + +sub get_next_key { + my $self = shift; + my ($obj) = @_; + + my $crumbs = $self->{breadcrumbs}; + my $e = $self->{engine}; + + unless ( @$crumbs ) { + # This will be a Reference sector + my $sector = $e->_load_sector( $self->{base_offset} ) + # If no sector is found, thist must have been deleted from under us. + or return; + + if ( $sector->staleness != $obj->_staleness ) { + return; + } + + my $loc = $sector->get_blist_loc + or return; + + push @$crumbs, $self->get_sector_iterator( $loc ); + } + + FIND_NEXT_KEY: { + # We're at the end. + unless ( @$crumbs ) { + $self->reset; + return; + } + + my $iterator = $crumbs->[-1]; + + # This level is done. + if ( $iterator->at_end ) { + pop @$crumbs; + redo FIND_NEXT_KEY; + } + + if ( $iterator->isa( 'DBM::Deep::Iterator::Index' ) ) { + # If we don't have any more, it will be caught at the + # prior check. + if ( my $next = $iterator->get_next_iterator ) { + push @$crumbs, $next; + } + redo FIND_NEXT_KEY; + } + + unless ( $iterator->isa( 'DBM::Deep::Iterator::BucketList' ) ) { + DBM::Deep->_throw_error( + "Should have a bucketlist iterator here - instead have $iterator" + ); + } + + # At this point, we have a BucketList iterator + my $key = $iterator->get_next_key; + if ( defined $key ) { + return $key; + } + #XXX else { $iterator->set_to_end() } ? + + # We hit the end of the bucketlist iterator, so redo + redo FIND_NEXT_KEY; + } + + DBM::Deep->_throw_error( "get_next_key(): How did we get here?" ); +} + +1; +__END__ diff --git a/lib/DBM/Deep/Iterator/BucketList.pm b/lib/DBM/Deep/Iterator/BucketList.pm new file mode 100644 index 0000000..577b5c2 --- /dev/null +++ b/lib/DBM/Deep/Iterator/BucketList.pm @@ -0,0 +1,90 @@ +package DBM::Deep::Iterator::BucketList; + +use 5.006_000; + +use strict; +use warnings FATAL => 'all'; + +=head1 NAME + +DBM::Deep::Iterator::BucketList + +=head1 PURPOSE + +This is an internal-use-only object for L. It acts as the mediator +between the L object and a L +sector. + +=head1 OVERVIEW + +This object, despite the implied class hiearchy, does B inherit from +L. Instead, it delegates to it, essentially acting as a +facade over it. L will instantiate one of +these objects as needed to handle an BucketList sector. + +=head1 METHODS + +=head2 new(\%params) + +The constructor takes a hashref of params and blesses it into the invoking class. The +hashref is assumed to have the following elements: + +=over 4 + +=item * iterator (of type L + +=item * sector (of type L + +=back + +=cut + +sub new { + my $self = bless $_[1] => $_[0]; + $self->{curr_index} = 0; + return $self; +} + +=head2 at_end() + +This takes no arguments. + +This returns true/false indicating whether this sector has any more elements that can be +iterated over. + +=cut + +sub at_end { + my $self = shift; + return $self->{curr_index} >= $self->{iterator}{engine}->max_buckets; +} + +=head2 get_next_iterator() + +This takes no arguments. + +This returns the next key pointed to by this bucketlist. This value is suitable for +returning by FIRSTKEY or NEXTKEY(). + +If the bucketlist is exhausted, it returns nothing. + +=cut + +sub get_next_key { + my $self = shift; + + return if $self->at_end; + + my $idx = $self->{curr_index}++; + + my $data_loc = $self->{sector}->get_data_location_for({ + allow_head => 1, + idx => $idx, + }) or return; + + #XXX Do we want to add corruption checks here? + return $self->{sector}->get_key_for( $idx )->data; +} + +1; +__END__ diff --git a/lib/DBM/Deep/Iterator/Index.pm b/lib/DBM/Deep/Iterator/Index.pm new file mode 100644 index 0000000..cdc28df --- /dev/null +++ b/lib/DBM/Deep/Iterator/Index.pm @@ -0,0 +1,86 @@ +package DBM::Deep::Iterator::Index; + +use 5.006_000; + +use strict; +use warnings FATAL => 'all'; + +=head1 NAME + +DBM::Deep::Iterator::Index + +=head1 PURPOSE + +This is an internal-use-only object for L. It acts as the mediator +between the L object and a L +sector. + +=head1 OVERVIEW + +This object, despite the implied class hiearchy, does B inherit from +L. Instead, it delegates to it, essentially acting as a +facade over it. L will instantiate one of +these objects as needed to handle an Index sector. + +=head1 METHODS + +=head2 new(\%params) + +The constructor takes a hashref of params and blesses it into the invoking class. The +hashref is assumed to have the following elements: + +=over 4 + +=item * iterator (of type L + +=item * sector (of type L + +=back + +=cut + +sub new { + my $self = bless $_[1] => $_[0]; + $self->{curr_index} = 0; + return $self; +} + +=head2 at_end() + +This takes no arguments. + +This returns true/false indicating whether this sector has any more elements that can be +iterated over. + +=cut + +sub at_end { + my $self = shift; + return $self->{curr_index} >= $self->{iterator}{engine}->hash_chars; +} + +=head2 get_next_iterator() + +This takes no arguments. + +This returns an iterator (built by L) based +on the sector pointed to by the next occupied location in this index. + +If the sector is exhausted, it returns nothing. + +=cut + +sub get_next_iterator { + my $self = shift; + + my $loc; + while ( !$loc ) { + return if $self->at_end; + $loc = $self->{sector}->get_entry( $self->{curr_index}++ ); + } + + return $self->{iterator}->get_sector_iterator( $loc ); +} + +1; +__END__ diff --git a/lib/DBM/Deep/Null.pm b/lib/DBM/Deep/Null.pm new file mode 100644 index 0000000..feb79ac --- /dev/null +++ b/lib/DBM/Deep/Null.pm @@ -0,0 +1,37 @@ +# This was copied from MARCEL's Class::Null. However, I couldn't use it because +# I need an undef value, not an implementation of the Null Class pattern. +package DBM::Deep::Null; + +use 5.006_000; + +use strict; +use warnings FATAL => 'all'; + +=head1 NAME + +DBM::Deep::Null + +=head1 PURPOSE + +This is an internal-use-only object for L. It acts as a NULL object +in the same vein as MARCEL's L. I couldn't use L +because DBM::Deep needed an object that always evaluated as undef, not an +implementation of the Null Class pattern. + +=head1 OVERVIEW + +It is used to represent null sectors in DBM::Deep. + +=cut + +use overload + 'bool' => sub { undef }, + '""' => sub { undef }, + '0+' => sub { undef }, + fallback => 1, + nomethod => 'AUTOLOAD'; + +sub AUTOLOAD { return; } + +1; +__END__ diff --git a/t/53_misc_transactions.t b/t/53_misc_transactions.t index 47aab6d..8b8fe48 100644 --- a/t/53_misc_transactions.t +++ b/t/53_misc_transactions.t @@ -4,7 +4,7 @@ # brought up by Alex Gallichotte use strict; -#use warnings FATAL => 'all'; +use warnings FATAL => 'all'; use Test::More tests => 4; use t::common qw( new_fh ); @@ -12,16 +12,16 @@ use t::common qw( new_fh ); use_ok( 'DBM::Deep' ); my ($fh, $filename) = new_fh(); -my $db = DBM::Deep->new( file => $filename, fh => $fh, ); +my $db = DBM::Deep->new( file => $filename, fh => $fh ); eval { $db->{randkey()} = randkey() for 1 .. 10; }; ok(!$@, "No eval failures"); eval { - $db->begin_work; +# $db->begin_work; $db->{randkey()} = randkey() for 1 .. 10; - $db->commit; +# $db->commit; }; -ok(!$@, 'No eval failures'); +ok(!$@, "No eval failures from the transaction"); eval { $db->{randkey()} = randkey() for 1 .. 10; }; ok(!$@, "No eval failures");