Added recursion test, hoisted staleness() to Sector.pm, and refactored to write_bucke...
[dbsrgits/DBM-Deep.git] / lib / DBM / Deep / Sector / File / Reference.pm
1 package DBM::Deep::Sector::File::Reference;
2
3 use 5.006_000;
4
5 use strict;
6 use warnings FATAL => 'all';
7
8 use base qw( DBM::Deep::Sector::File::Data );
9
10 my $STALE_SIZE = 2;
11
12 # Please refer to the pack() documentation for further information
13 my %StP = (
14     1 => 'C', # Unsigned char value (no order needed as it's just one byte)
15     2 => 'n', # Unsigned short in "network" (big-endian) order
16     4 => 'N', # Unsigned long in "network" (big-endian) order
17     8 => 'Q', # Usigned quad (no order specified, presumably machine-dependent)
18 );
19
20 sub _init {
21     my $self = shift;
22
23     my $e = $self->engine;
24
25     unless ( $self->offset ) {
26         my $classname = Scalar::Util::blessed( delete $self->{data} );
27         my $leftover = $self->size - $self->base_size - 3 * $e->byte_size;
28
29         my $class_offset = 0;
30         if ( defined $classname ) {
31             my $class_sector = DBM::Deep::Sector::File::Scalar->new({
32                 engine => $e,
33                 data   => $classname,
34             });
35             $class_offset = $class_sector->offset;
36         }
37
38         $self->{offset} = $e->_request_data_sector( $self->size );
39         $e->storage->print_at( $self->offset, $self->type ); # Sector type
40         # Skip staleness counter
41         $e->storage->print_at( $self->offset + $self->base_size,
42             pack( $StP{$e->byte_size}, 0 ),             # Index/BList loc
43             pack( $StP{$e->byte_size}, $class_offset ), # Classname loc
44             pack( $StP{$e->byte_size}, 1 ),             # Initial refcount
45             chr(0) x $leftover,                         # Zero-fill the rest
46         );
47     }
48     else {
49         $self->{type} = $e->storage->read_at( $self->offset, 1 );
50     }
51
52     $self->{staleness} = unpack(
53         $StP{$STALE_SIZE},
54         $e->storage->read_at( $self->offset + $e->SIG_SIZE, $STALE_SIZE ),
55     );
56
57     return;
58 }
59
60 sub get_data_location_for {
61     my $self = shift;
62     my ($args) = @_;
63
64     # Assume that the head is not allowed unless otherwise specified.
65     $args->{allow_head} = 0 unless exists $args->{allow_head};
66
67     # Assume we don't create a new blist location unless otherwise specified.
68     $args->{create} = 0 unless exists $args->{create};
69
70     my $blist = $self->get_bucket_list({
71         key_md5 => $args->{key_md5},
72         key => $args->{key},
73         create  => $args->{create},
74     });
75     return unless $blist && $blist->{found};
76
77     # At this point, $blist knows where the md5 is. What it -doesn't- know yet
78     # is whether or not this transaction has this key. That's part of the next
79     # function call.
80     my $location = $blist->get_data_location_for({
81         allow_head => $args->{allow_head},
82     }) or return;
83
84     return $location;
85 }
86
87 sub get_data_for {
88     my $self = shift;
89     my ($args) = @_;
90
91     my $location = $self->get_data_location_for( $args )
92         or return;
93
94     return $self->engine->load_sector( $location );
95 }
96
97 sub write_data {
98     my $self = shift;
99     my ($args) = @_;
100
101     my $blist = $self->get_bucket_list({
102         key_md5 => $args->{key_md5},
103         key => $args->{key},
104         create  => 1,
105     }) or DBM::Deep->_throw_error( "How did write_data fail (no blist)?!" );
106
107     # Handle any transactional bookkeeping.
108     if ( $self->engine->trans_id ) {
109         if ( ! $blist->has_md5 ) {
110             $blist->mark_deleted({
111                 trans_id => 0,
112             });
113         }
114     }
115     else {
116         my @trans_ids = $self->engine->get_running_txn_ids;
117         if ( $blist->has_md5 ) {
118             if ( @trans_ids ) {
119                 my $old_value = $blist->get_data_for;
120                 foreach my $other_trans_id ( @trans_ids ) {
121                     next if $blist->get_data_location_for({
122                         trans_id   => $other_trans_id,
123                         allow_head => 0,
124                     });
125                     $blist->write_md5({
126                         trans_id => $other_trans_id,
127                         key      => $args->{key},
128                         key_md5  => $args->{key_md5},
129                         value    => $old_value->clone,
130                     });
131                 }
132             }
133         }
134         else {
135             if ( @trans_ids ) {
136                 foreach my $other_trans_id ( @trans_ids ) {
137                     #XXX This doesn't seem to possible to ever happen . . .
138                     next if $blist->get_data_location_for({ trans_id => $other_trans_id, allow_head => 0 });
139                     $blist->mark_deleted({
140                         trans_id => $other_trans_id,
141                     });
142                 }
143             }
144         }
145     }
146
147     #XXX Is this safe to do transactionally?
148     # Free the place we're about to write to.
149     if ( $blist->get_data_location_for({ allow_head => 0 }) ) {
150         $blist->get_data_for({ allow_head => 0 })->free;
151     }
152
153     $blist->write_md5({
154         key      => $args->{key},
155         key_md5  => $args->{key_md5},
156         value    => $args->{value},
157     });
158 }
159
160 sub delete_key {
161     my $self = shift;
162     my ($args) = @_;
163
164     # This can return nothing if we are deleting an entry in a hashref that was
165     # auto-vivified as part of the delete process. For example:
166     #     my $x = {};
167     #     delete $x->{foo}{bar};
168     my $blist = $self->get_bucket_list({
169         key_md5 => $args->{key_md5},
170     }) or return;
171
172     # Save the location so that we can free the data
173     my $location = $blist->get_data_location_for({
174         allow_head => 0,
175     });
176     my $old_value = $location && $self->engine->load_sector( $location );
177
178     my @trans_ids = $self->engine->get_running_txn_ids;
179
180     # If we're the HEAD and there are running txns, then we need to clone this
181     # value to the other transactions to preserve Isolation.
182     if ( $self->engine->trans_id == 0 ) {
183         if ( @trans_ids ) {
184             foreach my $other_trans_id ( @trans_ids ) {
185                 next if $blist->get_data_location_for({ trans_id => $other_trans_id, allow_head => 0 });
186                 $blist->write_md5({
187                     trans_id => $other_trans_id,
188                     key      => $args->{key},
189                     key_md5  => $args->{key_md5},
190                     value    => $old_value->clone,
191                 });
192             }
193         }
194     }
195
196     my $data;
197     if ( @trans_ids ) {
198         $blist->mark_deleted( $args );
199
200         if ( $old_value ) {
201             #XXX Is this export => 1 actually doing anything?
202             $data = $old_value->data({ export => 1 });
203             $old_value->free;
204         }
205     }
206     else {
207         $data = $blist->delete_md5( $args );
208     }
209
210     return $data;
211 }
212
213 sub write_blist_loc {
214     my $self = shift;
215     my ($loc) = @_;
216
217     my $engine = $self->engine;
218     $engine->storage->print_at( $self->offset + $self->base_size,
219         pack( $StP{$engine->byte_size}, $loc ),
220     );
221
222 }
223
224 sub get_blist_loc {
225     my $self = shift;
226
227     my $e = $self->engine;
228     my $blist_loc = $e->storage->read_at( $self->offset + $self->base_size, $e->byte_size );
229     return unpack( $StP{$e->byte_size}, $blist_loc );
230 }
231
232 #sub clear {
233 #    my $self = shift;
234 #    my ($args) = @_;
235 #    $args ||= {};
236 #
237 #    my $engine = $self->engine;
238 #
239 #    # If there's nothing pointed to from this reference, there's nothing to do.
240 #    my $loc = $self->get_blist_loc
241 #        or return;
242 #
243 #    my $sector = $engine->load_sector( $loc )
244 #        or DBM::Deep->_throw_error( "Cannot read sector at $loc in clear()" );
245 #
246 #    $sector->clear;
247 #
248 #    $self->write_blist_loc( 0 );
249 #
250 #    return;
251 #}
252
253 sub get_bucket_list {
254     my $self = shift;
255     my ($args) = @_;
256     $args ||= {};
257
258     # XXX Add in check here for recycling?
259
260     my $engine = $self->engine;
261
262     my $blist_loc = $self->get_blist_loc;
263
264     # There's no index or blist yet
265     unless ( $blist_loc ) {
266         return unless $args->{create};
267
268         my $blist = DBM::Deep::Sector::File::BucketList->new({
269             engine  => $engine,
270             key_md5 => $args->{key_md5},
271         });
272
273         $self->write_blist_loc( $blist->offset );
274 #        $engine->storage->print_at( $self->offset + $self->base_size,
275 #            pack( $StP{$engine->byte_size}, $blist->offset ),
276 #        );
277
278         return $blist;
279     }
280
281     my $sector = $engine->load_sector( $blist_loc )
282         or DBM::Deep->_throw_error( "Cannot read sector at $blist_loc in get_bucket_list()" );
283     my $i = 0;
284     my $last_sector = undef;
285     while ( $sector->isa( 'DBM::Deep::Sector::File::Index' ) ) {
286         $blist_loc = $sector->get_entry( ord( substr( $args->{key_md5}, $i++, 1 ) ) );
287         $last_sector = $sector;
288         if ( $blist_loc ) {
289             $sector = $engine->load_sector( $blist_loc )
290                 or DBM::Deep->_throw_error( "Cannot read sector at $blist_loc in get_bucket_list()" );
291         }
292         else {
293             $sector = undef;
294             last;
295         }
296     }
297
298     # This means we went through the Index sector(s) and found an empty slot
299     unless ( $sector ) {
300         return unless $args->{create};
301
302         DBM::Deep->_throw_error( "No last_sector when attempting to build a new entry" )
303             unless $last_sector;
304
305         my $blist = DBM::Deep::Sector::File::BucketList->new({
306             engine  => $engine,
307             key_md5 => $args->{key_md5},
308         });
309
310         $last_sector->set_entry( ord( substr( $args->{key_md5}, $i - 1, 1 ) ) => $blist->offset );
311
312         return $blist;
313     }
314
315     $sector->find_md5( $args->{key_md5} );
316
317     # See whether or not we need to reindex the bucketlist
318     # Yes, the double-braces are there for a reason. if() doesn't create a
319     # redo-able block, so we have to create a bare block within the if() for
320     # redo-purposes.
321     # Patch and idea submitted by sprout@cpan.org. -RobK, 2008-01-09
322     if ( !$sector->has_md5 && $args->{create} && $sector->{idx} == -1 ) {{
323         my $redo;
324
325         my $new_index = DBM::Deep::Sector::File::Index->new({
326             engine => $engine,
327         });
328
329         my %blist_cache;
330         #XXX q.v. the comments for this function.
331         foreach my $entry ( $sector->chopped_up ) {
332             my ($spot, $md5) = @{$entry};
333             my $idx = ord( substr( $md5, $i, 1 ) );
334
335             # XXX This is inefficient
336             my $blist = $blist_cache{$idx}
337                 ||= DBM::Deep::Sector::File::BucketList->new({
338                     engine => $engine,
339                 });
340
341             $new_index->set_entry( $idx => $blist->offset );
342
343             my $new_spot = $blist->write_at_next_open( $md5 );
344             $engine->reindex_entry( $spot => $new_spot );
345         }
346
347         # Handle the new item separately.
348         {
349             my $idx = ord( substr( $args->{key_md5}, $i, 1 ) );
350
351             # If all the previous blist's items have been thrown into one
352             # blist and the new item belongs in there too, we need
353             # another index.
354             if ( keys %blist_cache == 1 and each %blist_cache == $idx ) {
355                 ++$i, ++$redo;
356             } else {
357                 my $blist = $blist_cache{$idx}
358                     ||= DBM::Deep::Sector::File::BucketList->new({
359                         engine => $engine,
360                     });
361     
362                 $new_index->set_entry( $idx => $blist->offset );
363     
364                 #XXX THIS IS HACKY!
365                 $blist->find_md5( $args->{key_md5} );
366                 $blist->write_md5({
367                     key     => $args->{key},
368                     key_md5 => $args->{key_md5},
369                     value   => DBM::Deep::Sector::File::Null->new({
370                         engine => $engine,
371                         data   => undef,
372                     }),
373                 });
374             }
375         }
376
377         if ( $last_sector ) {
378             $last_sector->set_entry(
379                 ord( substr( $args->{key_md5}, $i - 1, 1 ) ),
380                 $new_index->offset,
381             );
382         } else {
383             $engine->storage->print_at( $self->offset + $self->base_size,
384                 pack( $StP{$engine->byte_size}, $new_index->offset ),
385             );
386         }
387
388         $sector->wipe;
389         $sector->free;
390
391         if ( $redo ) {
392             (undef, $sector) = %blist_cache;
393             $last_sector = $new_index;
394             redo;
395         }
396
397         $sector = $blist_cache{ ord( substr( $args->{key_md5}, $i, 1 ) ) };
398         $sector->find_md5( $args->{key_md5} );
399     }}
400
401     return $sector;
402 }
403
404 sub get_class_offset {
405     my $self = shift;
406
407     my $e = $self->engine;
408     return unpack(
409         $StP{$e->byte_size},
410         $e->storage->read_at(
411             $self->offset + $self->base_size + 1 * $e->byte_size, $e->byte_size,
412         ),
413     );
414 }
415
416 sub get_classname {
417     my $self = shift;
418
419     my $class_offset = $self->get_class_offset;
420
421     return unless $class_offset;
422
423     return $self->engine->load_sector( $class_offset )->data;
424 }
425
426 # Look to hoist this method into a ::Reference trait
427 sub data {
428     my $self = shift;
429     my ($args) = @_;
430     $args ||= {};
431
432     my $obj;
433     unless ( $obj = $self->engine->cache->{ $self->offset } ) {
434         $obj = DBM::Deep->new({
435             type        => $self->type,
436             base_offset => $self->offset,
437             staleness   => $self->staleness,
438             storage     => $self->engine->storage,
439             engine      => $self->engine,
440         });
441
442         if ( $self->engine->storage->{autobless} ) {
443             my $classname = $self->get_classname;
444             if ( defined $classname ) {
445                 bless $obj, $classname;
446             }
447         }
448
449         $self->engine->cache->{$self->offset} = $obj;
450     }
451
452     # We're not exporting, so just return.
453     unless ( $args->{export} ) {
454         return $obj;
455     }
456
457     # We shouldn't export if this is still referred to.
458     if ( $self->get_refcount > 1 ) {
459         return $obj;
460     }
461
462     return $obj->export;
463 }
464
465 sub free {
466     my $self = shift;
467
468     # We're not ready to be removed yet.
469     return if $self->decrement_refcount > 0;
470
471     # Rebless the object into DBM::Deep::Null.
472     eval { %{ $self->engine->cache->{ $self->offset } } = (); };
473     eval { @{ $self->engine->cache->{ $self->offset } } = (); };
474     bless $self->engine->cache->{ $self->offset }, 'DBM::Deep::Null';
475     delete $self->engine->cache->{ $self->offset };
476
477     my $blist_loc = $self->get_blist_loc;
478     $self->engine->load_sector( $blist_loc )->free if $blist_loc;
479
480     my $class_loc = $self->get_class_offset;
481     $self->engine->load_sector( $class_loc )->free if $class_loc;
482
483     $self->SUPER::free();
484 }
485
486 sub increment_refcount {
487     my $self = shift;
488
489     my $refcount = $self->get_refcount;
490
491     $refcount++;
492
493     $self->write_refcount( $refcount );
494
495     return $refcount;
496 }
497
498 sub decrement_refcount {
499     my $self = shift;
500
501     my $refcount = $self->get_refcount;
502
503     $refcount--;
504
505     $self->write_refcount( $refcount );
506
507     return $refcount;
508 }
509
510 sub get_refcount {
511     my $self = shift;
512
513     my $e = $self->engine;
514     return unpack(
515         $StP{$e->byte_size},
516         $e->storage->read_at(
517             $self->offset + $self->base_size + 2 * $e->byte_size, $e->byte_size,
518         ),
519     );
520 }
521
522 sub write_refcount {
523     my $self = shift;
524     my ($num) = @_;
525
526     my $e = $self->engine;
527     $e->storage->print_at(
528         $self->offset + $self->base_size + 2 * $e->byte_size,
529         pack( $StP{$e->byte_size}, $num ),
530     );
531 }
532
533 1;
534 __END__