1 package DBM::Deep::Engine3;
7 our $VERSION = q(0.99_03);
12 # * Every method in here assumes that the storage has been appropriately
13 # safeguarded. This can be anything from flock() to some sort of manual
14 # mutex. But, it's the caller's responsability to make sure that this has
17 # Setup file and tag signatures. These should never change.
18 sub SIG_FILE () { 'DPDB' }
19 sub SIG_HEADER () { 'h' }
20 sub SIG_INTERNAL () { 'i' }
21 sub SIG_HASH () { 'H' }
22 sub SIG_ARRAY () { 'A' }
23 sub SIG_NULL () { 'N' }
24 sub SIG_DATA () { 'D' }
25 sub SIG_INDEX () { 'I' }
26 sub SIG_BLIST () { 'B' }
27 sub SIG_FREE () { 'F' }
28 sub SIG_KEYS () { 'K' }
31 ################################################################################
33 # Please refer to the pack() documentation for further information
35 1 => 'C', # Unsigned char value
36 2 => 'n', # Unsigned short in "network" (big-endian) order
37 4 => 'N', # Unsigned long in "network" (big-endian) order
38 8 => 'Q', # Usigned quad (no order specified, presumably machine-dependent)
49 hash_size => 16, # In bytes
51 num_txns => 16, # HEAD plus 15 running txns
52 trans_id => 0, # Default to the HEAD
57 if ( defined $args->{pack_size} ) {
58 if ( lc $args->{pack_size} eq 'small' ) {
59 $args->{byte_size} = 2;
61 elsif ( lc $args->{pack_size} eq 'medium' ) {
62 $args->{byte_size} = 4;
64 elsif ( lc $args->{pack_size} eq 'large' ) {
65 $args->{byte_size} = 8;
68 die "Unknown pack_size value: '$args->{pack_size}'\n";
72 # Grab the parameters we want to use
73 foreach my $param ( keys %$self ) {
74 next unless exists $args->{$param};
75 $self->{$param} = $args->{$param};
78 $self->{byte_pack} = $StP{ $self->byte_size };
81 # Number of buckets per blist before another level of indexing is
82 # done. Increase this value for slightly greater speed, but larger database
83 # files. DO NOT decrease this value below 16, due to risk of recursive
86 if ( $self->{max_buckets} < 16 ) {
87 warn "Floor of max_buckets is 16. Setting it to 16 from '$self->{max_buckets}'\n";
88 $self->{max_buckets} = 16;
91 if ( !$self->{digest} ) {
93 $self->{digest} = \&Digest::MD5::md5;
99 ################################################################################
103 my ($obj, $key) = @_;
105 # This will be a Reference sector
106 my $sector = $self->_load_sector( $obj->_base_offset )
107 or die "How did read_value fail (no sector for '$obj')?!\n";
109 my $key_md5 = $self->_apply_digest( $key );
111 # XXX What should happen if this fails?
112 my $blist = $sector->get_bucket_list({
115 }) or die "How did read_value fail (no blist)?!\n";
117 my $value_sector = $blist->get_data_for( $key_md5, { allow_head => 1 } );
118 if ( !$value_sector ) {
120 $value_sector = DBM::Deep::Engine::Sector::Null->new({
125 $blist->write_md5( $key_md5, $key, $value_sector->offset );
128 return $value_sector->data;
135 # This will be a Reference sector
136 my $sector = $self->_load_sector( $obj->_base_offset )
137 or die "How did read_value fail (no sector for '$obj')?!\n";
139 return $sector->get_classname;
144 my ($obj, $key) = @_;
146 # This will be a Reference sector
147 my $sector = $self->_load_sector( $obj->_base_offset )
148 or die "How did key_exists fail (no sector for '$obj')?!\n";
150 my $key_md5 = $self->_apply_digest( $key );
152 # XXX What should happen if this fails?
153 my $blist = $sector->get_bucket_list({
155 }) or die "How did key_exists fail (no blist)?!\n";
157 # exists() returns 1 or '' for true/false.
158 return $blist->has_md5( $key_md5, { allow_head => 1 } ) ? 1 : '';
163 my ($obj, $key) = @_;
165 my $sector = $self->_load_sector( $obj->_base_offset )
166 or die "How did delete_key fail (no sector for '$obj')?!\n";
168 my $key_md5 = $self->_apply_digest( $key );
170 # XXX What should happen if this fails?
171 my $blist = $sector->get_bucket_list({
173 }) or die "How did delete_key fail (no blist)?!\n";
175 return $blist->delete_md5( $key_md5 );
180 my ($obj, $key, $value) = @_;
182 # This will be a Reference sector
183 my $sector = $self->_load_sector( $obj->_base_offset )
184 or die "How did write_value fail (no sector for '$obj')?!\n";
186 my $key_md5 = $self->_apply_digest( $key );
188 # XXX What should happen if this fails?
189 my $blist = $sector->get_bucket_list({
192 }) or die "How did write_value fail (no blist)?!\n";
194 my $r = Scalar::Util::reftype( $value ) || '';
197 last if $r eq 'HASH';
198 last if $r eq 'ARRAY';
200 DBM::Deep->_throw_error(
201 "Storage of references of type '$r' is not supported."
206 if ( !defined $value ) {
207 $class = 'DBM::Deep::Engine::Sector::Null';
209 elsif ( $r eq 'ARRAY' || $r eq 'HASH' ) {
210 if ( $r eq 'ARRAY' && tied(@$value) ) {
211 DBM::Deep->_throw_error( "Cannot store something that is tied." );
213 if ( $r eq 'HASH' && tied(%$value) ) {
214 DBM::Deep->_throw_error( "Cannot store something that is tied." );
216 $class = 'DBM::Deep::Engine::Sector::Reference';
217 $type = substr( $r, 0, 1 );
220 $class = 'DBM::Deep::Engine::Sector::Scalar';
223 if ( $blist->has_md5( $key_md5 ) ) {
224 $blist->get_data_for( $key_md5, { allow_head => 0 } )->free;
227 my $value_sector = $class->new({
233 $blist->write_md5( $key_md5, $key, $value_sector->offset );
235 # This code is to make sure we write all the values in the $value to the disk
236 # and to make sure all changes to $value after the assignment are reflected
237 # on disk. This may be counter-intuitive at first, but it is correct dwimmery.
238 # NOTE - simply tying $value won't perform a STORE on each value. Hence, the
239 # copy to a temp value.
240 if ( $r eq 'ARRAY' ) {
242 tie @$value, 'DBM::Deep', {
243 base_offset => $value_sector->offset,
244 storage => $self->storage,
248 bless $value, 'DBM::Deep::Array' unless Scalar::Util::blessed( $value );
250 elsif ( $r eq 'HASH' ) {
252 tie %$value, 'DBM::Deep', {
253 base_offset => $value_sector->offset,
254 storage => $self->storage,
259 bless $value, 'DBM::Deep::Hash' unless Scalar::Util::blessed( $value );
267 my ($obj, $prev_key) = @_;
269 # XXX Need to add logic about resetting the iterator if any key in the reference has changed
270 unless ( $prev_key ) {
271 $obj->{iterator} = DBM::Deep::Engine::Iterator->new({
272 base_offset => $obj->_base_offset,
277 return $obj->{iterator}->get_next_key;
280 ################################################################################
286 # We're opening the file.
287 unless ( $obj->_base_offset ) {
288 my $bytes_read = $self->_read_file_header;
290 # Creating a new file
291 unless ( $bytes_read ) {
292 $self->_write_file_header;
294 # 1) Create Array/Hash entry
295 my $initial_reference = DBM::Deep::Engine::Sector::Reference->new({
299 $obj->{base_offset} = $initial_reference->offset;
301 $self->storage->flush;
303 # Reading from an existing file
305 $obj->{base_offset} = $bytes_read;
306 my $initial_reference = DBM::Deep::Engine::Sector::Reference->new({
308 offset => $obj->_base_offset,
310 unless ( $initial_reference ) {
311 DBM::Deep->_throw_error("Corrupted file, no master index record");
314 unless ($obj->_type eq $initial_reference->type) {
315 DBM::Deep->_throw_error("File type mismatch");
327 if ( $self->trans_id ) {
328 DBM::Deep->throw_error( "Cannot begin_work within a transaction" );
331 my @slots = $self->read_transaction_slots;
332 for my $i ( 1 .. @slots ) {
335 $self->set_trans_id( $i );
338 $self->write_transaction_slots( @slots );
340 if ( !$self->trans_id ) {
341 DBM::Deep->throw_error( "Cannot begin_work - no available transactions" );
351 if ( !$self->trans_id ) {
352 DBM::Deep->throw_error( "Cannot rollback without a transaction" );
360 if ( !$self->trans_id ) {
361 DBM::Deep->throw_error( "Cannot commit without a transaction" );
365 sub read_transaction_slots {
367 return split '', unpack( "b32", $self->storage->read_at( $self->trans_loc, 4 ) );
370 sub write_transaction_slots {
372 $self->storage->print_at( $self->trans_loc,
373 pack( "b32", join('', @_) ),
377 ################################################################################
380 my $header_fixed = length( SIG_FILE ) + 1 + 4 + 4;
382 sub _write_file_header {
385 my $header_var = 1 + 1 + 4 + 2 * $self->byte_size;
387 my $loc = $self->storage->request_space( $header_fixed + $header_var );
389 $self->storage->print_at( $loc,
392 pack('N', 1), # header version - at this point, we're at 9 bytes
393 pack('N', $header_var), # header size
394 # --- Above is $header_fixed. Below is $header_var
395 pack('C', $self->byte_size),
396 pack('C', $self->max_buckets),
397 pack('N', 0 ), # Running transactions
398 pack($StP{$self->byte_size}, 0), # Start of free chain (blist size)
399 pack($StP{$self->byte_size}, 0), # Start of free chain (data size)
402 $self->set_trans_loc( $header_fixed + 2 );
403 $self->set_chains_loc( $header_fixed + 6 );
408 sub _read_file_header {
411 my $buffer = $self->storage->read_at( 0, $header_fixed );
412 return unless length($buffer);
414 my ($file_signature, $sig_header, $header_version, $size) = unpack(
418 unless ( $file_signature eq SIG_FILE ) {
419 $self->storage->close;
420 DBM::Deep->_throw_error( "Signature not found -- file is not a Deep DB" );
423 unless ( $sig_header eq SIG_HEADER ) {
424 $self->storage->close;
425 DBM::Deep->_throw_error( "Old file version found." );
428 my $buffer2 = $self->storage->read_at( undef, $size );
429 my @values = unpack( 'C C', $buffer2 );
431 $self->set_trans_loc( $header_fixed + 2 );
432 $self->set_chains_loc( $header_fixed + 6 );
434 if ( @values < 2 || grep { !defined } @values ) {
435 $self->storage->close;
436 DBM::Deep->_throw_error("Corrupted file - bad header");
439 #XXX Add warnings if values weren't set right
440 @{$self}{qw(byte_size max_buckets)} = @values;
442 my $header_var = 1 + 1 + 4 + 2 * $self->byte_size;
443 unless ( $size eq $header_var ) {
444 $self->storage->close;
445 DBM::Deep->_throw_error( "Unexpected size found ($size <-> $header_var)." );
448 return length($buffer) + length($buffer2);
456 my $type = $self->storage->read_at( $offset, 1 );
457 return if $type eq chr(0);
459 if ( $type eq $self->SIG_ARRAY || $type eq $self->SIG_HASH ) {
460 return DBM::Deep::Engine::Sector::Reference->new({
466 elsif ( $type eq $self->SIG_BLIST ) {
467 return DBM::Deep::Engine::Sector::BucketList->new({
473 elsif ( $type eq $self->SIG_NULL ) {
474 return DBM::Deep::Engine::Sector::Null->new({
480 elsif ( $type eq $self->SIG_DATA ) {
481 return DBM::Deep::Engine::Sector::Scalar->new({
487 # This was deleted from under us, so just return and let the caller figure it out.
488 elsif ( $type eq $self->SIG_FREE ) {
492 die "'$offset': Don't know what to do with type '$type'\n";
497 return $self->{digest}->(@_);
500 sub _add_free_sector {
502 my ($offset, $size) = @_;
506 if ( $size == 256 ) {
507 $chains_offset = $self->byte_size;
514 my $old_head = $self->storage->read_at( $self->chains_loc + $chains_offset, $self->byte_size );
516 $self->storage->print_at( $self->chains_loc + $chains_offset,
517 pack( $StP{$self->byte_size}, $offset ),
520 # Record the old head in the new sector after the signature
521 $self->storage->print_at( $offset + 1, $old_head );
524 sub _request_sector {
530 if ( $size == 256 ) {
531 $chains_offset = $self->byte_size;
538 my $old_head = $self->storage->read_at( $self->chains_loc + $chains_offset, $self->byte_size );
539 my $loc = unpack( $StP{$self->byte_size}, $old_head );
541 # We don't have any free sectors of the right size, so allocate a new one.
543 return $self->storage->request_space( $size );
546 my $new_head = $self->storage->read_at( $loc + 1, $self->byte_size );
547 $self->storage->print_at( $self->chains_loc + $chains_offset, $new_head );
552 ################################################################################
554 sub storage { $_[0]{storage} }
555 sub byte_size { $_[0]{byte_size} }
556 sub hash_size { $_[0]{hash_size} }
557 sub num_txns { $_[0]{num_txns} }
558 sub max_buckets { $_[0]{max_buckets} }
559 sub blank_md5 { chr(0) x $_[0]->hash_size }
561 sub trans_id { $_[0]{trans_id} }
562 sub set_trans_id { $_[0]{trans_id} = $_[1] }
564 sub trans_loc { $_[0]{trans_loc} }
565 sub set_trans_loc { $_[0]{trans_loc} = $_[1] }
567 sub chains_loc { $_[0]{chains_loc} }
568 sub set_chains_loc { $_[0]{chains_loc} = $_[1] }
570 ################################################################################
572 package DBM::Deep::Engine::Iterator;
580 engine => $args->{engine},
581 base_offset => $args->{base_offset},
582 trans_id => $args->{trans_id},
585 Scalar::Util::weaken( $self->{engine} );
592 $self->{breadcrumbs} = [];
598 my $crumbs = $self->{breadcrumbs};
600 unless ( @$crumbs ) {
601 # This will be a Reference sector
602 my $sector = $self->{engine}->_load_sector( $self->{base_offset} )
603 # or die "Iterator: How did this fail (no ref sector for '$self->{base_offset}')?!\n";
604 # If no sector is found, thist must have been deleted from under us.
606 push @$crumbs, [ $sector->get_blist_loc, 0 ];
611 my ($offset, $idx) = @{ $crumbs->[-1] };
617 my $sector = $self->{engine}->_load_sector( $offset )
618 or die "Iterator: How did this fail (no blist sector for '$offset')?!\n";
620 my $key_sector = $sector->get_key_for( $idx );
621 unless ( $key_sector ) {
627 $key = $key_sector->data;
634 package DBM::Deep::Engine::Sector;
637 my $self = bless $_[1], $_[0];
638 Scalar::Util::weaken( $self->{engine} );
644 sub engine { $_[0]{engine} }
645 sub offset { $_[0]{offset} }
646 sub type { $_[0]{type} }
651 $self->engine->storage->print_at( $self->offset,
652 $self->engine->SIG_FREE,
653 chr(0) x ($self->size - 1),
656 $self->engine->_add_free_sector(
657 $self->offset, $self->size,
663 package DBM::Deep::Engine::Sector::Data;
665 our @ISA = qw( DBM::Deep::Engine::Sector );
668 sub size { return 256 }
670 package DBM::Deep::Engine::Sector::Scalar;
672 our @ISA = qw( DBM::Deep::Engine::Sector::Data );
677 my $chain_loc = $self->chain_loc;
679 $self->SUPER::free();
682 $self->engine->_load_sector( $chain_loc )->free;
688 sub type { $_[0]{engine}->SIG_DATA }
692 my $engine = $self->engine;
694 unless ( $self->offset ) {
695 my $data_section = $self->size - 3 - 1 * $engine->byte_size;
697 my $data = delete $self->{data};
699 $self->{offset} = $engine->_request_sector( $self->size );
701 my $dlen = length $data;
703 my $curr_offset = $self->offset;
704 while ( $continue ) {
708 my ($leftover, $this_len, $chunk);
709 if ( $dlen > $data_section ) {
711 $this_len = $data_section;
712 $chunk = substr( $data, 0, $this_len );
714 $dlen -= $data_section;
715 $next_offset = $engine->_request_sector( $self->size );
716 $data = substr( $data, $this_len );
719 $leftover = $data_section - $dlen;
726 $engine->storage->print_at( $curr_offset,
727 $self->type, # Sector type
728 pack( $StP{1}, 0 ), # Recycled counter
729 pack( $StP{$engine->byte_size}, $next_offset ), # Chain loc
730 pack( $StP{1}, $this_len ), # Data length
731 $chunk, # Data to be stored in this sector
732 chr(0) x $leftover, # Zero-fill the rest
735 $curr_offset = $next_offset;
745 my $buffer = $self->engine->storage->read_at(
746 $self->offset + 2 + $self->engine->byte_size, 1
749 return unpack( $StP{1}, $buffer );
754 my $chain_loc = $self->engine->storage->read_at(
755 $self->offset + 2, $self->engine->byte_size,
757 return unpack( $StP{$self->engine->byte_size}, $chain_loc );
765 my $chain_loc = $self->chain_loc;
767 $data .= $self->engine->storage->read_at(
768 $self->offset + 2 + $self->engine->byte_size + 1, $self->data_length,
771 last unless $chain_loc;
773 $self = $self->engine->_load_sector( $chain_loc );
779 package DBM::Deep::Engine::Sector::Null;
781 our @ISA = qw( DBM::Deep::Engine::Sector::Data );
783 sub type { $_[0]{engine}->SIG_NULL }
784 sub data_length { 0 }
790 my $engine = $self->engine;
792 unless ( $self->offset ) {
793 my $leftover = $self->size - 3 - 1 * $engine->byte_size;
795 $self->{offset} = $engine->_request_sector( $self->size );
796 $engine->storage->print_at( $self->offset,
797 $self->type, # Sector type
798 pack( $StP{1}, 0 ), # Recycled counter
799 pack( $StP{$engine->byte_size}, 0 ), # Chain loc
800 pack( $StP{1}, $self->data_length ), # Data length
801 chr(0) x $leftover, # Zero-fill the rest
808 package DBM::Deep::Engine::Sector::Reference;
810 our @ISA = qw( DBM::Deep::Engine::Sector::Data );
815 my $engine = $self->engine;
817 unless ( $self->offset ) {
818 my $classname = Scalar::Util::blessed( delete $self->{data} );
819 my $leftover = $self->size - 4 - 2 * $engine->byte_size;
821 my $class_offset = 0;
822 if ( defined $classname ) {
823 my $class_sector = DBM::Deep::Engine::Sector::Scalar->new({
824 engine => $self->engine,
827 $class_offset = $class_sector->offset;
830 $self->{offset} = $engine->_request_sector( $self->size );
831 $engine->storage->print_at( $self->offset,
832 $self->type, # Sector type
833 pack( $StP{1}, 0 ), # Recycled counter
834 pack( $StP{$engine->byte_size}, 0 ), # Index/BList loc
835 pack( $StP{$engine->byte_size}, $class_offset ), # Classname loc
836 chr(0) x $leftover, # Zero-fill the rest
842 $self->{type} = $engine->storage->read_at( $self->offset, 1 );
850 my $engine = $self->engine;
851 my $blist_loc = $engine->storage->read_at( $self->offset + 2, $engine->byte_size );
852 return unpack( $StP{$engine->byte_size}, $blist_loc );
855 sub get_bucket_list {
860 # XXX Add in check here for recycling?
862 my $engine = $self->engine;
864 my $blist_loc = $self->get_blist_loc;
866 # There's no index or blist yet
867 unless ( $blist_loc ) {
868 return unless $args->{create};
870 my $blist = DBM::Deep::Engine::Sector::BucketList->new({
873 $engine->storage->print_at( $self->offset + 2,
874 pack( $StP{$engine->byte_size}, $blist->offset ),
879 return DBM::Deep::Engine::Sector::BucketList->new({
881 offset => $blist_loc,
888 my $class_offset = $self->engine->storage->read_at(
889 $self->offset + 2 + 1 * $self->engine->byte_size, $self->engine->byte_size,
891 $class_offset = unpack ( $StP{$self->engine->byte_size}, $class_offset );
893 return unless $class_offset;
895 return $self->engine->_load_sector( $class_offset )->data;
901 my $new_obj = DBM::Deep->new({
903 base_offset => $self->offset,
904 storage => $self->engine->storage,
905 engine => $self->engine,
908 if ( $self->engine->storage->{autobless} ) {
909 my $classname = $self->get_classname;
910 if ( defined $classname ) {
911 bless $new_obj, $classname;
918 package DBM::Deep::Engine::Sector::BucketList;
920 our @ISA = qw( DBM::Deep::Engine::Sector );
922 sub idx_for_txn { return $_[1] + 1 }
927 my $engine = $self->engine;
929 unless ( $self->offset ) {
930 my $leftover = $self->size - $self->base_size;
932 $self->{offset} = $engine->_request_sector( $self->size );
933 $engine->storage->print_at( $self->offset,
934 $engine->SIG_BLIST, # Sector type
935 pack( $StP{1}, 0 ), # Recycled counter
936 chr(0) x $leftover, # Zero-fill the data
943 sub base_size { 2 } # Sig + recycled counter
947 my $e = $self->engine;
948 return $self->base_size + $e->max_buckets * $self->bucket_size; # Base + numbuckets * bucketsize
953 my $e = $self->engine;
955 my $locs_size = (1 + $e->num_txns ) * $e->byte_size;
956 return $e->hash_size + $locs_size;
961 my ($found, $idx) = $self->find_md5( @_ );
967 my ($md5, $opts) = @_;
970 foreach my $idx ( 0 .. $self->engine->max_buckets - 1 ) {
971 my $potential = $self->engine->storage->read_at(
972 $self->offset + $self->base_size + $idx * $self->bucket_size, $self->engine->hash_size,
975 return (undef, $idx) if $potential eq $self->engine->blank_md5;
976 if ( $md5 eq $potential ) {
977 my $location = $self->get_data_location_for(
978 $self->engine->trans_id, $idx, $opts,
981 if ( $location > 1 ) {
985 return (undef, $idx);
994 my ($md5, $key, $value_loc) = @_;
996 my $engine = $self->engine;
997 my ($found, $idx) = $self->find_md5( $md5, { allow_head => 0 } );
998 my $spot = $self->offset + $self->base_size + $idx * $self->bucket_size;
1001 my $key_sector = DBM::Deep::Engine::Sector::Scalar->new({
1002 engine => $self->engine,
1006 $engine->storage->print_at( $spot,
1008 pack( $StP{$self->engine->byte_size}, $key_sector->offset ),
1012 $engine->storage->print_at(
1014 + $self->engine->hash_size
1015 + $self->engine->byte_size
1016 + $self->engine->trans_id * $self->engine->byte_size,
1017 pack( $StP{$engine->byte_size}, $value_loc ), # The pointer to the data in the HEAD
1025 my $engine = $self->engine;
1026 my ($found, $idx) = $self->find_md5( $md5, { allow_head => 0 } );
1027 return undef unless $found;
1029 # Save the location so that we can free the data
1030 my $location = $self->get_data_location_for( $self->engine->trans_id, $idx, { allow_head => 0 } );
1031 my $key_sector = $self->get_key_for( $idx );
1033 my $spot = $self->offset + $self->base_size + $idx * $self->bucket_size;
1034 $engine->storage->print_at( $spot,
1035 $engine->storage->read_at(
1036 $spot + $self->bucket_size,
1037 $self->bucket_size * ( $engine->num_txns - $idx - 1 ),
1039 chr(0) x $self->bucket_size,
1044 my $data_sector = $self->engine->_load_sector( $location );
1045 my $data = $data_sector->data;
1051 sub get_data_location_for {
1053 my ($trans_id, $idx, $opts) = @_;
1056 my $location = $self->engine->storage->read_at(
1057 $self->offset + $self->base_size
1058 + $idx * $self->bucket_size
1059 + $self->engine->hash_size
1060 + $self->engine->byte_size
1061 + $trans_id * $self->engine->byte_size,
1062 $self->engine->byte_size,
1064 my $loc = unpack( $StP{$self->engine->byte_size}, $location );
1066 # If we're in a transaction and we never wrote to this location, try the
1068 if ( $trans_id && !$loc && $opts->{allow_head} ) {
1069 return $self->get_data_location_for( 0, $idx );
1076 my ($md5, $opts) = @_;
1079 my ($found, $idx) = $self->find_md5( $md5, $opts );
1080 return unless $found;
1081 my $location = $self->get_data_location_for( $self->engine->trans_id, $idx, $opts );
1082 return $self->engine->_load_sector( $location );
1089 my $location = $self->engine->storage->read_at(
1090 $self->offset + $self->base_size + $idx * $self->bucket_size + $self->engine->hash_size,
1091 $self->engine->byte_size,
1093 $location = unpack( $StP{$self->engine->byte_size}, $location );
1094 return unless $location;
1095 return $self->engine->_load_sector( $location );