From: rkinyon Date: Wed, 13 Dec 2006 06:07:02 +0000 (+0000) Subject: Transactions now seem to work, except for the rollback/commit bits X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=2432d6cc48de58e0ba24f7f2250d4dbbb58acae3;p=dbsrgits%2FDBM-Deep.git Transactions now seem to work, except for the rollback/commit bits --- diff --git a/lib/DBM/Deep/Engine2.pm b/lib/DBM/Deep/Engine2.pm index ff43781..2908c7e 100644 --- a/lib/DBM/Deep/Engine2.pm +++ b/lib/DBM/Deep/Engine2.pm @@ -222,9 +222,9 @@ sub write_value { } } else { - # If the HEAD isn't new, then we must take other transactions - # into account. If it is, then there can be no other transactions. - if ( !$key_tag->{is_new} ) { + if ( $key_tag->{is_new} ) { + } + else { my $old_value = $self->read_value( $trans_id, $base_offset, $key ); if ( my @transactions = $self->_storage->current_transactions ) { foreach my $other_trans_id ( @transactions ) { diff --git a/lib/DBM/Deep/Engine3.pm b/lib/DBM/Deep/Engine3.pm index 4e3177f..fb97a56 100644 --- a/lib/DBM/Deep/Engine3.pm +++ b/lib/DBM/Deep/Engine3.pm @@ -108,21 +108,22 @@ sub read_value { my $key_md5 = $self->_apply_digest( $key ); - # XXX What should happen if this fails? - my $blist = $sector->get_bucket_list({ - key_md5 => $key_md5, - create => 1, - }) or die "How did read_value fail (no blist)?!\n"; + my $value_sector = $sector->get_data_for({ + key_md5 => $key_md5, + allow_head => 1, + }); - my $value_sector = $blist->get_data_for( $key_md5, { allow_head => 1 } ); - if ( !$value_sector ) { - # Autovivification + unless ( $value_sector ) { $value_sector = DBM::Deep::Engine::Sector::Null->new({ engine => $self, data => undef, }); - $blist->write_md5( $key_md5, $key, $value_sector->offset ); + $sector->write_data({ + key_md5 => $key_md5, + key => $key, + value => $value_sector, + }); } return $value_sector->data; @@ -147,15 +148,13 @@ sub key_exists { my $sector = $self->_load_sector( $obj->_base_offset ) or die "How did key_exists fail (no sector for '$obj')?!\n"; - my $key_md5 = $self->_apply_digest( $key ); - - # XXX What should happen if this fails? - my $blist = $sector->get_bucket_list({ - key_md5 => $key_md5, - }) or die "How did key_exists fail (no blist)?!\n"; + my $data = $sector->get_data_for({ + key_md5 => $self->_apply_digest( $key ), + allow_head => 1, + }); # exists() returns 1 or '' for true/false. - return $blist->has_md5( $key_md5, { allow_head => 1 } ) ? 1 : ''; + return $data ? 1 : ''; } sub delete_key { @@ -165,6 +164,11 @@ sub delete_key { my $sector = $self->_load_sector( $obj->_base_offset ) or die "How did delete_key fail (no sector for '$obj')?!\n"; + return $sector->delete_key({ + key_md5 => $self->_apply_digest( $key ), + allow_head => 0, + }); + my $key_md5 = $self->_apply_digest( $key ); # XXX What should happen if this fails? @@ -179,18 +183,6 @@ sub write_value { my $self = shift; my ($obj, $key, $value) = @_; - # This will be a Reference sector - my $sector = $self->_load_sector( $obj->_base_offset ) - or die "How did write_value fail (no sector for '$obj')?!\n"; - - my $key_md5 = $self->_apply_digest( $key ); - - # XXX What should happen if this fails? - my $blist = $sector->get_bucket_list({ - key_md5 => $key_md5, - create => 1, - }) or die "How did write_value fail (no blist)?!\n"; - my $r = Scalar::Util::reftype( $value ) || ''; { last if $r eq ''; @@ -220,17 +212,23 @@ sub write_value { $class = 'DBM::Deep::Engine::Sector::Scalar'; } - if ( $blist->has_md5( $key_md5 ) ) { - $blist->get_data_for( $key_md5, { allow_head => 0 } )->free; - } + # This will be a Reference sector + my $sector = $self->_load_sector( $obj->_base_offset ) + or die "How did write_value fail (no sector for '$obj')?!\n"; + # Create this after loading the reference sector in case something bad happens. + # This way, we won't allocate value sector(s) needlessly. my $value_sector = $class->new({ engine => $self, data => $value, type => $type, }); - $blist->write_md5( $key_md5, $key, $value_sector->offset ); + $sector->write_data({ + key => $key, + key_md5 => $self->_apply_digest( $key ), + value => $value_sector, + }); # This code is to make sure we write all the values in the $value to the disk # and to make sure all changes to $value after the assignment are reflected @@ -463,6 +461,7 @@ sub _load_sector { offset => $offset, }); } + # XXX Don't we need key_md5 here? elsif ( $type eq $self->SIG_BLIST ) { return DBM::Deep::Engine::Sector::BucketList->new({ engine => $self, @@ -579,7 +578,6 @@ sub new { breadcrumbs => [], engine => $args->{engine}, base_offset => $args->{base_offset}, - trans_id => $args->{trans_id}, }, $class; Scalar::Util::weaken( $self->{engine} ); @@ -614,9 +612,23 @@ sub get_next_key { last; } + if ( $idx >= $self->{engine}->max_buckets ) { + $self->reset; + last; + } + my $sector = $self->{engine}->_load_sector( $offset ) or die "Iterator: How did this fail (no blist sector for '$offset')?!\n"; + #XXX Think this through! + my $loc = $sector->get_data_location_for({ + idx => $idx, + }); + unless ( $loc ) { + $crumbs->[-1][1]++; + next; + } + my $key_sector = $sector->get_key_for( $idx ); unless ( $key_sector ) { $self->reset; @@ -640,6 +652,7 @@ sub new { return $self; } sub _init {} +sub clone { die "Must be implemented in the child class" } sub engine { $_[0]{engine} } sub offset { $_[0]{offset} } @@ -667,6 +680,15 @@ our @ISA = qw( DBM::Deep::Engine::Sector ); # This is in bytes sub size { return 256 } +sub clone { + my $self = shift; + return ref($self)->new({ + engine => $self->engine, + data => $self->data, + type => $self->type, + }); +} + package DBM::Deep::Engine::Sector::Scalar; our @ISA = qw( DBM::Deep::Engine::Sector::Data ); @@ -694,10 +716,9 @@ sub _init { unless ( $self->offset ) { my $data_section = $self->size - 3 - 1 * $engine->byte_size; - my $data = delete $self->{data}; - $self->{offset} = $engine->_request_sector( $self->size ); + my $data = delete $self->{data}; my $dlen = length $data; my $continue = 1; my $curr_offset = $self->offset; @@ -844,12 +865,108 @@ sub _init { return; } +sub get_data_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}, + 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 $self->engine->_load_sector( $location ); +} + +sub write_data { + my $self = shift; + my ($args) = @_; + + my $blist = $self->get_bucket_list({ + key_md5 => $args->{key_md5}, + create => 1, + }) or die "How did write_value fail (no blist)?!\n"; + + # Handle any transactional bookkeeping. + if ( $self->engine->trans_id ) { + if ( ! $blist->{found} ) { + $blist->mark_deleted({ + trans_id => 0, + }); + } + } + else { + my @transactions = $self->engine->read_transaction_slots; + my @trans_ids = grep { $transactions[$_] } 0 .. $#transactions; + if ( $blist->{found} ) { + 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 ) { + next if $blist->get_data_location_for({ trans_id => $other_trans_id, allow_head => 0 }); + $blist->mark_deleted({ + trans_id => $other_trans_id, + }); + } + } + } + } + + # 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 die "How did delete_key fail (no blist)?!\n"; + + return $blist->delete_md5( $args ); +} + sub get_blist_loc { my $self = shift; - my $engine = $self->engine; - my $blist_loc = $engine->storage->read_at( $self->offset + 2, $engine->byte_size ); - return unpack( $StP{$engine->byte_size}, $blist_loc ); + my $e = $self->engine; + my $blist_loc = $e->storage->read_at( $self->offset + 2, $e->byte_size ); + return unpack( $StP{$e->byte_size}, $blist_loc ); } sub get_bucket_list { @@ -868,17 +985,21 @@ sub get_bucket_list { return unless $args->{create}; my $blist = DBM::Deep::Engine::Sector::BucketList->new({ - engine => $engine, + engine => $engine, + key_md5 => $args->{key_md5}, }); + $engine->storage->print_at( $self->offset + 2, pack( $StP{$engine->byte_size}, $blist->offset ), ); + return $blist; } return DBM::Deep::Engine::Sector::BucketList->new({ - engine => $engine, - offset => $blist_loc, + engine => $engine, + offset => $blist_loc, + key_md5 => $args->{key_md5}, }); } @@ -937,6 +1058,10 @@ sub _init { ); } + if ( $self->{key_md5} ) { + $self->find_md5; + } + return $self; } @@ -944,45 +1069,58 @@ sub base_size { 2 } # Sig + recycled counter sub size { my $self = shift; - my $e = $self->engine; - return $self->base_size + $e->max_buckets * $self->bucket_size; # Base + numbuckets * bucketsize + unless ( $self->{size} ) { + my $e = $self->engine; + $self->{size} = $self->base_size + $e->max_buckets * $self->bucket_size; # Base + numbuckets * bucketsize + } + return $self->{size}; } sub bucket_size { my $self = shift; - my $e = $self->engine; - # Key + transactions - my $locs_size = (1 + $e->num_txns ) * $e->byte_size; - return $e->hash_size + $locs_size; + unless ( $self->{bucket_size} ) { + my $e = $self->engine; + # Key + transactions + my $locs_size = (1 + $e->num_txns ) * $e->byte_size; + $self->{bucket_size} = $e->hash_size + $locs_size; + } + return $self->{bucket_size}; } sub has_md5 { my $self = shift; - my ($found, $idx) = $self->find_md5( @_ ); - return $found; + unless ( exists $self->{found} ) { + $self->find_md5; + } + return $self->{found}; } sub find_md5 { my $self = shift; - my ($md5, $opts) = @_; - $opts ||= {}; - foreach my $idx ( 0 .. $self->engine->max_buckets - 1 ) { - my $potential = $self->engine->storage->read_at( - $self->offset + $self->base_size + $idx * $self->bucket_size, $self->engine->hash_size, - ); + $self->{found} = undef; + $self->{idx} = -1; - return (undef, $idx) if $potential eq $self->engine->blank_md5; - if ( $md5 eq $potential ) { - my $location = $self->get_data_location_for( - $self->engine->trans_id, $idx, $opts, - ); + # If we don't have an MD5, then what are we supposed to do? + unless ( exists $self->{key_md5} ) { + DBM::Deep->throw( "Cannot find_md5 without a key_md5 set" ); + } - if ( $location > 1 ) { - return (1, $idx); - } + 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; + } - return (undef, $idx); + if ( $potential eq $self->{key_md5} ) { + $self->{found} = 1; + $self->{idx} = $idx; + return; } } @@ -991,50 +1129,67 @@ sub find_md5 { sub write_md5 { my $self = shift; - my ($md5, $key, $value_loc) = @_; + my ($args) = @_; + $args ||= {}; + + $args->{trans_id} = $self->engine->trans_id unless exists $args->{trans_id}; my $engine = $self->engine; - my ($found, $idx) = $self->find_md5( $md5, { allow_head => 0 } ); - my $spot = $self->offset + $self->base_size + $idx * $self->bucket_size; + my $spot = $self->offset + $self->base_size + $self->{idx} * $self->bucket_size; - unless ($found) { + unless ($self->{found}) { my $key_sector = DBM::Deep::Engine::Sector::Scalar->new({ engine => $self->engine, - data => $key, + data => $args->{key}, }); $engine->storage->print_at( $spot, - $md5, + $args->{key_md5}, pack( $StP{$self->engine->byte_size}, $key_sector->offset ), ); } $engine->storage->print_at( $spot + + $engine->hash_size + + $engine->byte_size + + $args->{trans_id} * $engine->byte_size, + pack( $StP{$engine->byte_size}, $args->{value}->offset ), + ); +} + +sub mark_deleted { + my $self = shift; + my ($args) = @_; + + my $spot = $self->offset + $self->base_size + $self->{idx} * $self->bucket_size; + $self->engine->storage->print_at( + $spot + $self->engine->hash_size + $self->engine->byte_size - + $self->engine->trans_id * $self->engine->byte_size, - pack( $StP{$engine->byte_size}, $value_loc ), # The pointer to the data in the HEAD + + $args->{trans_id} * $self->engine->byte_size, + pack( $StP{$self->engine->byte_size}, 1 ), # 1 is the marker for deleted ); } sub delete_md5 { my $self = shift; - my ($md5) = @_; + my ($args) = @_; my $engine = $self->engine; - my ($found, $idx) = $self->find_md5( $md5, { allow_head => 0 } ); - return undef unless $found; + return undef unless $self->{found}; # Save the location so that we can free the data - my $location = $self->get_data_location_for( $self->engine->trans_id, $idx, { allow_head => 0 } ); - my $key_sector = $self->get_key_for( $idx ); + my $location = $self->get_data_location_for({ + allow_head => 0, + }); + my $key_sector = $self->get_key_for; - my $spot = $self->offset + $self->base_size + $idx * $self->bucket_size; + 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->num_txns - $idx - 1 ), + $self->bucket_size * ( $engine->num_txns - $self->{idx} - 1 ), ), chr(0) x $self->bucket_size, ); @@ -1050,41 +1205,50 @@ sub delete_md5 { sub get_data_location_for { my $self = shift; - my ($trans_id, $idx, $opts) = @_; - $opts ||= {}; + 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 $location = $self->engine->storage->read_at( $self->offset + $self->base_size - + $idx * $self->bucket_size + + $args->{idx} * $self->bucket_size + $self->engine->hash_size + $self->engine->byte_size - + $trans_id * $self->engine->byte_size, + + $args->{trans_id} * $self->engine->byte_size, $self->engine->byte_size, ); my $loc = unpack( $StP{$self->engine->byte_size}, $location ); # If we're in a transaction and we never wrote to this location, try the # HEAD instead. - if ( $trans_id && !$loc && $opts->{allow_head} ) { - return $self->get_data_location_for( 0, $idx ); + if ( $args->{trans_id} && !$loc && $args->{allow_head} ) { + return $self->get_data_location_for({ + trans_id => 0, + allow_head => 1, + }); } - return $loc; + return $loc <= 1 ? 0 : $loc; } sub get_data_for { my $self = shift; - my ($md5, $opts) = @_; - $opts ||= {}; + my ($args) = @_; + $args ||= {}; - my ($found, $idx) = $self->find_md5( $md5, $opts ); - return unless $found; - my $location = $self->get_data_location_for( $self->engine->trans_id, $idx, $opts ); + 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; my $location = $self->engine->storage->read_at( $self->offset + $self->base_size + $idx * $self->bucket_size + $self->engine->hash_size, diff --git a/t/33_transactions.t b/t/33_transactions.t index 2b43f68..6d135de 100644 --- a/t/33_transactions.t +++ b/t/33_transactions.t @@ -1,5 +1,5 @@ use strict; -use Test::More tests => 65; +use Test::More tests => 69; use Test::Deep; use t::common qw( new_fh ); @@ -36,16 +36,29 @@ ok( $@, "Attempting to begin_work within a transaction throws an error" ); is( $db1->{x}, 'y', "DB1 transaction started, no actions - DB1's X is Y" ); is( $db2->{x}, 'y', "DB1 transaction started, no actions - DB2's X is Y" ); + $db2->{x} = 'a'; + is( $db1->{x}, 'y', "Within DB1 transaction, DB1's X is still Y" ); + is( $db2->{x}, 'a', "Within DB1 transaction, DB2's X is now A" ); + $db1->{x} = 'z'; is( $db1->{x}, 'z', "Within DB1 transaction, DB1's X is Z" ); - is( $db2->{x}, 'y', "Within DB1 transaction, DB2's X is still Y" ); -__END__ + is( $db2->{x}, 'a', "Within DB1 transaction, DB2's X is still A" ); + + $db1->{z} = 'a'; + is( $db1->{z}, 'a', "Within DB1 transaction, DB1's Z is A" ); + ok( !exists $db2->{z}, "Since z was added after the transaction began, DB2 doesn't see it." ); + $db2->{other_x} = 'foo'; is( $db2->{other_x}, 'foo', "DB2 set other_x within DB1's transaction, so DB2 can see it" ); ok( !exists $db1->{other_x}, "Since other_x was added after the transaction began, DB1 doesn't see it." ); - cmp_bag( [ keys %$db1 ], [qw( x )], "DB1 keys correct" ); + cmp_bag( [ keys %$db1 ], [qw( x z )], "DB1 keys correct" ); cmp_bag( [ keys %$db2 ], [qw( x other_x )], "DB2 keys correct" ); +SKIP:{ skip "unfinished yet", 51 } +__END__ + +# Reset to an expected value +$db2->{x} = 'y'; $db1->rollback; diff --git a/t/40_freespace.t b/t/40_freespace.t index 0ab05b0..718502c 100644 --- a/t/40_freespace.t +++ b/t/40_freespace.t @@ -12,10 +12,11 @@ my ($fh, $filename) = new_fh(); my $db = DBM::Deep->new( $filename ); $db->{foo} = '1234'; +$db->{foo} = '2345'; my $size = -s $filename; -$db->{foo} = '2345'; -cmp_ok( $size, '==', -s $filename, "Overwrite doesn't change size" ); +$db->{foo} = '3456'; +cmp_ok( $size, '==', -s $filename, "A second overwrite doesn't change size" ); $size = -s $filename; delete $db->{foo};