File Coverage

blib/lib/Object/Transaction.pm
Criterion Covered Total %
statement 36 385 9.3
branch 0 190 0.0
condition 0 24 0.0
subroutine 12 51 23.5
pod n/a
total 48 650 7.3


line stmt bran cond sub pod time code
1              
2             # Copyright (C) 1999-2002, Internet Journals Corporation .
3             # Copyright (C) 2002 David Muir Sharnoff
4             # All rights reserved. License hearby granted for anyone to use this
5             # module at their own risk. Please feed useful changes back to
6             # David Muir Sharnoff .
7              
8             package Object::Transaction;
9              
10             my %cache;
11              
12             $VERSION = 1.01;
13             my $lock_debugging = 0;
14             my $debug = 0;
15             my $warnings = 0;
16             my $registered;
17              
18             require File::Flock;
19 1     1   1703 use Storable;
  1         3716  
  1         66  
20 1     1   836 use POSIX qw(O_CREAT O_RDWR);
  1         6247  
  1         7  
21             require File::Sync;
22 1     1   988 use Carp;
  1         6  
  1         43  
23 1     1   4 use Carp qw(verbose);
  1         1  
  1         133  
24 1     1   5 use vars qw($magic_cookie);
  1         1  
  1         67  
25             $magic_cookie = "O:Ta";
26              
27             require Exporter;
28             @ISA = qw(Exporter);
29             @EXPORT = qw(transaction transaction_pending commit abandon uncache);
30              
31 1     1   5 use strict;
  1         1  
  1         528  
32              
33             # things to override
34              
35 0     0     sub initialize { die "deferred" }
36 0     0     sub file { die "deferred" }
37 0     0     sub presave {}
38 0     0     sub postsave {}
39 0     0     sub postload {}
40 0     0     sub preload {}
41 0     0     sub preremove {}
42 0     0     sub postremove {}
43             sub id
44             {
45 0     0     my ($this) = @_;
46 0           return $this->{'ID'};
47             }
48 0     0     sub precommit {}
49              
50             # a few wrappers
51              
52             my %locks;
53              
54             sub _lock
55             {
56 0     0     my ($file) = @_;
57 0 0         if ($lock_debugging) {
58 0           my ($package, $filename, $line) = caller;
59 0           my ($package2, $filename2, $line2) = caller(1);
60 0           print STDERR "\n{{{{ $file $line, $line2";
61             }
62 0           $locks{$file} = 1;
63 0           File::Flock::lock($file);
64             }
65              
66             sub _unlock
67             {
68 0     0     my ($file) = @_;
69 0 0         if ($lock_debugging) {
70 0           my ($package, $filename, $line) = caller;
71 0           my ($package2, $filename2, $line2) = caller(1);
72 0           print STDERR "\n}}}} $file $line, $line2";
73             }
74 0           delete $locks{$file};
75 0           File::Flock::unlock($file);
76             }
77              
78             sub _lockrename
79             {
80 0     0     my ($from, $to) = @_;
81 0 0         if ($lock_debugging) {
82 0           my ($package, $filename, $line) = caller;
83 0           my ($package2, $filename2, $line2) = caller(1);
84 0           print STDERR "{$from->$to} $line, $line2";
85             }
86 0           $locks{$to} = $locks{$from};
87 0           delete $locks{$from};
88 0           File::Flock::lock_rename($from, $to);
89             }
90              
91             sub _unlock_all
92             {
93 0     0     for my $f (keys %locks) {
94 0           _unlock($f);
95             }
96             }
97              
98             sub _read_file
99             {
100 0     0     my ($file) = @_;
101              
102 1     1   4 no strict;
  1         1  
  1         117  
103              
104 0           my $r;
105             my (@r);
106              
107 0           local(*F);
108 0 0         open(F, "<$file") || die "open $file: $!";
109 0           @r = ;
110 0           close(F);
111              
112 0           return join("",@r);
113             }
114              
115             sub _write_file
116             {
117 0     0     my ($f, @data) = @_;
118              
119 1     1   4 no strict;
  1         1  
  1         275  
120              
121 0           undef $!;
122 0           my $d = join('', @data);
123              
124 0           local(*F,*O);
125 0 0         open(F, ">$f") || die "open >$f: $!";
126 0           $O = select(F);
127 0           $| = 1;
128 0           select($O);
129 0 0         (print F $d) || die "write $f: $!";
130 0 0         File::Sync::fsync_fd(fileno(F)) || die "fsync $f: $!";
131 0 0         close(F) || die "close $f: $!";
132 0 0 0       if ($d && ! -s $f) {
133             # Houston, we have a problem!
134             # Let's try this again!
135             # this code may no longer be necessary.
136 0 0         confess "cannot write $f: $!"
137             if caller(50); # prevent deep recursion
138 0 0         print STDERR "Write to $f failed ($!), trying again\n"
139             if $warnings;
140 0           _write_file($f, $d);
141             }
142 0           return 1;
143             }
144              
145             # now the meat
146              
147             sub new
148             {
149 0     0     my ($pkg, @args) = @_;
150 1     1   6 no strict 'refs';
  1         2  
  1         61  
151 0           my $obj = ${pkg}->initialize(@args);
152 0           bless $obj, $pkg;
153 0           $obj->cache;
154 0           return $obj;
155             }
156              
157 1     1   4 use vars qw($commit_inprogress);
  1         1  
  1         247  
158             $commit_inprogress = 0;
159             my $firstload;
160              
161             sub load
162             {
163 0     0     my ($package, $baseid) = @_;
164              
165 0 0         print STDERR "LOAD $package $baseid\n" if $debug;
166              
167 0 0         if (exists $cache{$package}{$baseid}) {
168 0 0         print STDERR "Returing cached $package $baseid\n" if $debug;
169 0           return $cache{$package}{$baseid};
170             }
171              
172 0           my $newid;
173 0           eval {
174 0           $newid = $package->preload($baseid);
175             };
176 0 0         confess $@ if $@;
177              
178 0 0 0       if ($newid && exists $cache{$package}{$newid}) {
179 0 0         print STDERR "Returing cached $package $baseid\n" if $debug;
180 0           return $cache{$package}{$newid};
181             }
182              
183 0 0         $firstload = time unless $firstload;
184              
185 0   0       my $id = $newid || $baseid;
186              
187 0 0         return undef unless $id;
188              
189 0           my $file = $package->file($id);
190              
191             # all method invocations can have side-effects.
192 0 0         if ($cache{$package}{$id}) {
193 0 0         print STDERR "Returing recently-cached $package $id\n" if $debug;
194 0           return $cache{$package}{$id};
195             }
196              
197             #
198             # No read-lock is required because files are only modified
199             # through rename rather than rewrite.
200             #
201             # This does create the possibility of a program failure if you
202             # try to read a file that is deleted at just the right time.
203             #
204 0 0         return undef unless -e $file;
205 0           my $frozen = _read_file($file);
206             {
207 1     1   5 no re 'taint';
  1         2  
  1         739  
  0            
208 0 0         substr($frozen, 0, length($magic_cookie)) eq $magic_cookie
209             or die "corrupt file: $file";
210 0           substr($frozen, 0, length($magic_cookie)) = '';
211             }
212 0           my $obj = Storable::thaw $frozen;
213 0 0         print STDERR "Pulling fresh copy for $package $id from $file\n" if $debug;
214 0 0         die "unable to thaw $file!" unless $obj;
215 0           $obj->{'OLD'} = Storable::thaw $frozen;
216 0           $obj->{'OLD'}{'__frozen'} = \$frozen;
217              
218 0           $obj->postload($id);
219              
220 0           $cache{$package}{$id} = $obj;
221 0 0         modperl_register() unless $registered;
222              
223 0 0         if ($obj->{'__transfollowers'}) {
    0          
224 0 0         print STDERR "Transleader with followers\n" if $debug;
225 0           for my $class (sort keys %{$obj->{'__transfollowers'}}) {
  0            
226 0           for my $id (sort keys %{$obj->{'__transfollowers'}{$class}}) {
  0            
227             # will rollback as side-effect
228 0           my $follower = _loadany($class, $id);
229             }
230             }
231 0           $obj = Storable::thaw ${$obj->{'__rollback'}};
  0            
232 0           $cache{$package}{$id} = $obj;
233 0           _lock $file;
234 0           $obj->postload($id);
235 0           _unlock $file;
236 0           $obj->_realsave();
237             } elsif ($obj->{'__transleader'}) {
238 0 0         print STDERR "Transfollower\n" if $debug;
239 0           my $leader = _loadany($obj->{'__transleader'}{'CLASS'},
240             $obj->{'__transleader'}{'ID'});
241 0 0 0       if (exists $leader->{'__transfollower'}
      0        
242             && exists $leader->{'__transfollower'}{$package}
243             && exists $leader->{'__transfollower'}{$package}{$id})
244             {
245             # rollback time!
246 0           $obj = Storable::thaw ${$obj->{'__rollback'}};
  0            
247 0           $cache{$package}{$id} = $obj;
248 0           _lock $file;
249 0           $obj->postload($id);
250 0           _unlock $file;
251             } else {
252 0           delete $obj->{'__transleader'};
253 0           delete $obj->{'__rollback'};
254             }
255              
256 0           eval {
257 0           $obj->_realsave();
258             };
259 0 0         if ($@ =~ /^DATACHANGE: file/) {
260 0           return load($package, $baseid);
261             }
262 0 0         die $@ if $@;
263             }
264              
265 0 0         if ($obj->{'__removenow'}) {
266 0           $obj->_realremove();
267 0           return undef;
268             }
269              
270 0           return $obj;
271             }
272              
273             sub objectref
274             {
275 0     0     my ($this) = @_;
276 0           my $id = $this->id();
277 0 0         die "id function returned empty on $this" unless $id;
278 0           return bless [ ref $this, $id ], 'Object::Transaction::Reference';
279             }
280              
281             {
282             package Object::Transaction::Reference;
283              
284             sub loadref
285             {
286 0     0     my ($ref) = @_;
287 0           my ($pkg, $id) = @$ref;
288 0           return Object::Transaction::_loadany($pkg, $id);
289             }
290             }
291              
292             sub _loadany
293             {
294 0     0     my ($pkg, $id) = @_;
295 1     1   5 no strict qw(refs);
  1         2  
  1         2688  
296 0 0         unless (defined @{"${pkg}::ISA"}) {
  0            
297 0           require "$pkg.pm";
298             }
299 0           return ${pkg}->load($id);
300             }
301              
302             my %tosave;
303              
304             sub abandon
305             {
306 0     0     %tosave = ();
307             }
308              
309             sub cache
310             {
311 0     0     my ($this) = @_;
312 0           my $pkg = ref $this;
313 0           my $id = $this->id();
314 0 0         confess unless defined $id;
315 0 0 0       confess "id clash with $pkg $id\n"
      0        
316             if $cache{$pkg}
317             && defined $cache{$pkg}{$id}
318             && $cache{$pkg}{$id} ne $this;
319 0           $cache{$pkg}{$id} = $this;
320 0 0         modperl_register() unless $registered;
321             }
322              
323             sub uncache
324             {
325 0     0     my ($this) = @_;
326 0 0         if (ref $this) {
327 0           delete $cache{ref $this}{$this->id()};
328 0           $this->{'__uncached'} = 1;
329             } else {
330 0           %cache = ();
331 0           undef $firstload;
332             }
333             }
334              
335             sub removelater
336             {
337 0     0     my ($this) = @_;
338 0           $this->{'__removenow'} = 1;
339 0           $this->savelater();
340             }
341              
342             sub remove
343             {
344 0     0     my ($this) = @_;
345 0 0         $this->removelater()
346             if $this;
347              
348 0           commit();
349             }
350              
351             sub savelater
352             {
353 0     0     my ($this, $trivial, $code) = @_;
354 0 0         confess "attempt to call savelater() from within a presave() or postsave()"
355             if $commit_inprogress == 2;
356 0           my $id = $this->id();
357 0 0         confess "id not defined" unless defined $id;
358 0           $tosave{ref $this}{$id} = $this;
359 0           $this->{'__readonly'} = 0;
360 0 0         if ($code) {
361 0 0         $this->{'__doatsave'} = []
362             unless $this->{'__doatsave'};
363             }
364 0 0         if ($trivial) {
365 0           $this->{'__trivial'} = 1;
366             } else {
367 0           delete $this->{'__trivial'};
368             }
369 0 0         $this->cache() unless $this->{'OLD'};
370              
371 0           check_hash($this);
372             }
373              
374             sub readlock
375             {
376 0     0     my ($this) = @_;
377 0           my $id = $this->id();
378 0 0         confess unless defined $id;
379 0           $tosave{ref $this}{$id} = $this;
380 0 0         $this->{'__readonly'} = 1
381             unless exists $this->{'__readonly'};
382             }
383              
384             sub save
385             {
386 0     0     my ($this) = @_;
387 0 0         $this->savelater()
388             if $this;
389              
390 0           commit();
391             }
392              
393             sub transaction_pending
394             {
395 0 0   0     return 1 if %tosave;
396 0           return 0;
397             }
398              
399             sub transaction
400             {
401 0     0     eval {
402 0           require ObjTransLclCnfg;
403             };
404 0 0         shift if ref $_[0] ne 'CODE';
405 0           my ($funcref, @args) = @_;
406 0           my (%c) = (%cache);
407 0           my $r;
408             my @r;
409 0           my $want = wantarray();
410 0           my $die = 0;
411 0           my $count = 0;
412 0           for(;;) {
413 0 0         die if $die; # protect against 'next' et al inside eval
414 0           $die = 1;
415 0           eval {
416 0 0         if ($want) {
417 0           @r = &$funcref(@args);
418             } else {
419 0           $r = &$funcref(@args);
420             }
421             };
422 0 0         if ($@ =~ /^DATACHANGE: file/) {
423 0           %cache = %c;
424 0 0         print STDERR "Restarting transaction: $@" if $warnings;
425 0           $die = 0;
426 0 0 0       die "Aborting Transaction -- Too many locking failures ($count): $@"
427             if $ObjTransLclCnfg::maxtries
428             && $count++ > $ObjTransLclCnfg::maxtries;
429 0           redo;
430             }
431 0           require Carp;
432 0 0         Carp::croak $@ if $@;
433 0           last;
434             }
435 0 0         return @r if $want;
436 0           return $r;
437             }
438              
439             #
440             # One of the changed objects becomes the transaction leader. The state
441             # of the leader determines the state of the entire transaction.
442             #
443             # The leader gets modified twice: first to note the other participants
444             # in the transaction and then later to commit the transaction.
445             #
446             # The other participants also get written twice, but the second writing
447             # happens the next time the object gets loaded, rather than at the time
448             # of the transaction.
449             #
450              
451             my $unlock;
452             my $datachangefailures;
453              
454             sub commit
455             {
456 0 0   0     confess "attemp to call commit() from within a precommit(), presave() or postsave()"
457             if $commit_inprogress;
458 0           local($commit_inprogress) = 1;
459              
460 0 0         return 0 unless %tosave;
461              
462 0           my @commitlist;
463             my %precommitdone;
464              
465 0           my $done = 0;
466 0           while (! $done) {
467 0           $done = 1;
468 0           for my $type (keys %tosave) {
469 0           for my $obj (values %{$tosave{$type}}) {
  0            
470 0 0         next if $precommitdone{$obj}++;
471 0 0         if ($obj->precommit($obj->old)) {
472 0           $done = 0;
473             }
474             }
475             }
476             }
477              
478 0           my @savelist;
479 0           for my $cls (sort keys %tosave) {
480 0           for my $id (sort keys %{$tosave{$cls}}) {
  0            
481 0           push(@savelist, $tosave{$cls}{$id});
482             }
483             }
484              
485 0           $commit_inprogress = 2;
486              
487 0 0         if (@savelist == 1) {
488 0 0         if ($savelist[0]->{'__removenow'}) {
489 0           $savelist[0]->_realremove();
490             } else {
491 0           $savelist[0]->_realsave();
492             }
493 0           %tosave = ();
494 0           $datachangefailures = 0;
495 0           return 1;
496             }
497              
498 0           my $leader = shift(@savelist);
499 0 0         $leader->{'__rollback'} = exists $leader->{'OLD'}
500             ? $leader->{'OLD'}{'__frozen'}
501             : Storable::nfreeze { '__removenow' => 1 };
502              
503 0           for my $s (@savelist) {
504 0 0         die "attemp to save an 'uncached' object"
505             if exists $s->{'__uncached'};
506 0 0         $leader->{'__toremove'}{ref($s)}{$s->id()} = 1
507             if $s->{'__deletenow'};
508 0 0         next if $s->{'__trivial'};
509 0           $leader->{'__transfollowers'}{ref($s)}{$s->id()} = 1;
510 0           $s->{'__transleader'} = {
511             'CLASS' => ref($leader),
512             'ID' => $leader->id(),
513             };
514 0 0         $s->{'__rollback'} = exists $s->{'OLD'}
515             ? $s->{'OLD'}{'__frozen'}
516             : Storable::nfreeze { '__removenow' => 1 };
517             }
518              
519 0           delete $leader->{'__readonly'};
520 0 0         if (! -e $leader->file()) {
521 0           $leader->_realsave();
522             }
523 0           _lock $leader->file();
524 0           $leader->_realsave(1);
525              
526 0           for my $s (@savelist) {
527 0           $s->_realsave();
528             }
529              
530 0           delete $leader->{'__transfollowers'};
531 0           delete $leader->{'__rollback'};
532 0           $leader->_realsave(1);
533              
534 0 0         if ($leader->{'__toremove'}) {
535 0           $leader->_removeall();
536 0           $leader->_realsave(1);
537             }
538 0           _unlock $leader->file();
539              
540 0 0         if (exists $leader->{'__removenow'}) {
541 0           $leader->_realremove();
542             }
543              
544 0           %tosave = ();
545 0           $datachangefailures = 0;
546 0           return 1;
547             }
548              
549             my $srand;
550             sub _realsave
551             {
552 0     0     my ($this, $keeplock) = @_;
553              
554 0           my $id = $this->id();
555 0           my $file = $this->file($id);
556              
557 0           my $old = $this->old();
558              
559 0           my (@passby) = $this->presave($old);
560              
561 0 0         if (defined $old) {
562 0 0         _lock $file unless $keeplock;
563 0           my $frozen = _read_file($file);
564 0 0         substr($frozen, 0, length($magic_cookie)) eq $magic_cookie
565             or die "corrupt file: $file";
566 0           substr($frozen, 0, length($magic_cookie)) = '';
567 0 0         if ($frozen ne ${$old->{'__frozen'}}) {
  0            
568 0           _unlock_all();
569 0           abandon();
570 0           uncache();
571 0 0         srand(time ^ ($$ < 5))
572             unless $srand;
573 0           $srand = 1;
574 0           require Time::HiRes;
575 0           my $st = rand(0.5)*(1.3**$datachangefailures);
576 0 0         $st = ($st % 200 + 100) if $st > 300;
577 0 0         printf STDERR "DATACHANGE sleep %d for %.2f seconds\n", $$, $st
578             if $warnings;
579 0           Time::HiRes::sleep($st);
580 0 0         printf STDERR "DATACHANGE sleep %d done\n", $$
581             if $warnings;
582 0           $datachangefailures++;
583 0           $firstload = undef;
584 0 0         if ($this->{__poison}) {
585 0           die "Cached object from previous transaction reused";
586             }
587 0           $this->{__poison} = 'DATACHANGE';
588 0 0         warn "DATACHANGE: file $file changed out from under $$\n"
589             if $warnings;
590 0           die "DATACHANGE: file $file changed out from under $$, please retry";
591             }
592 0 0         if ($this->{'__readonly'}) {
593 0 0         _unlock $file unless $keeplock;
594 0           return;
595             }
596             } else {
597 0 0         _lock $file unless $keeplock;
598             }
599              
600 0           delete $this->{'OLD'};
601 0           delete $this->{'__readonly'};
602              
603 0           my $newfrozen = Storable::nfreeze($this);
604 0           _write_file("$file.tmp", $magic_cookie, $newfrozen);
605              
606 0           _lock "$file.tmp";
607              
608 0 0         confess("write failed on $file.tmp") unless -s "$file.tmp";
609              
610 0 0         rename("$file.tmp", $file)
611             or die "rename $file.tmp -> $file: $!";
612              
613 0 0         die unless -e $file;
614              
615 0           _lockrename("$file.tmp", $file);
616              
617 0           $this->postsave($old, @passby);
618              
619 0 0         if ($file ne $this->file($id)) {
620             # can change sometimes
621 0           my $new = $this->file($id);
622 0           File::Flock::lock_rename($file, $new);
623 0           $file = $new;
624             }
625 0 0         _unlock $file
626             unless $keeplock;
627              
628 0           $this->{'OLD'} = Storable::thaw($newfrozen);
629 0           $this->{'OLD'}{'__frozen'} = \$newfrozen;
630             }
631              
632             sub _removeall
633             {
634 0     0     my ($this) = @_;
635 0           for my $class (sort keys %{$this->{'__toremove'}}) {
  0            
636 0           for my $id (sort keys %{$this->{'__toremove'}{$class}}) {
  0            
637             # will remove as side-effect
638 0           my $follower = $class->load($id);
639             }
640             }
641             }
642              
643             sub _realremove
644             {
645 0     0     my ($this) = @_;
646 0           _lock $this->file();
647 0           $this->preremove();
648 0           _unlock $this->file();
649 0           unlink($this->file());
650 0           $this->postremove();
651 0           delete $cache{ref $this}{$this->id()}
652             }
653              
654             sub old
655             {
656 0     0     my ($this) = @_;
657 0 0         return $this->{'OLD'} if exists $this->{'OLD'};
658 0           return undef;
659             }
660              
661             sub check_hash
662             {
663             # Look for references used as hash keys.
664             # XXXX Turn this off in production.
665 0     0     my ($hash_ref) = @_;
666              
667 0           for my $key (keys %{$hash_ref}) {
  0            
668 0 0         if($key =~ /HASH\(0x[0-9a-f]+\)/) {
669 0           confess "Hash used as a key; class: " . ref($hash_ref) .
670             "; value: $hash_ref->{$key}\n";
671             } else {
672 0           my $val = $hash_ref->{$key};
673 0 0         if(ref($val) eq 'HASH') {
674 0           check_hash($val);
675             }
676             }
677             }
678             }
679              
680             sub modperl_register
681             {
682 0     0     $registered = 1;
683 0 0         return unless $ENV{MOD_PERL};
684 0           Apache->push_handlers("PerlCleanupHandler", \&modperl_cleanup);
685             }
686              
687             sub modperl_cleanup
688             {
689 0     0     $registered = 0;
690 0           undef %locks;
691 0           undef %tosave;
692 0           $datachangefailures = 0;
693 0           $commit_inprogress = 0;
694              
695             #
696             # This next one is debateable. If we don't clear
697             # out the cache then the process will grow and grow.
698             # If we don't clear out the cache we will have many
699             # more aborted transactions. An in-between setting
700             # is probably necessary.
701             #
702 0           undef %cache;
703             }
704              
705             1;