File Coverage

blib/lib/Directory/Transactional.pm
Criterion Covered Total %
statement 2 4 50.0
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 4 6 66.6


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             # ABSTRACT: ACID transactions on a directory tree
3              
4             package Directory::Transactional;
5             BEGIN {
6 1     1   31669 $Directory::Transactional::VERSION = '0.09';
7             }
8 1     1   591 use Moose;
  0            
  0            
9              
10             use Time::HiRes qw(alarm);
11              
12             use Set::Object;
13              
14             use Carp;
15             use Fcntl qw(LOCK_EX LOCK_SH LOCK_NB);
16              
17             use File::Spec;
18             use File::Find qw(find);
19             use File::Path qw(make_path remove_tree);
20             use File::Copy;
21             use IO::Dir;
22              
23             use Directory::Transactional::TXN::Root;
24             use Directory::Transactional::TXN::Nested;
25             #use Directory::Transactional::Stream; # we require it later, it wants real Moose
26              
27             use Try::Tiny;
28              
29             use namespace::clean -except => 'meta';
30              
31             has root => (
32             is => "ro",
33             required => 1,
34             );
35              
36             has _fatal => (
37             isa => "Bool",
38             is => "rw",
39             );
40              
41             has [qw(_root _work _backups _txns _locks _dirty _dirty_lock)] => (
42             isa => "Str",
43             is => "ro",
44             lazy_build => 1,
45             );
46              
47             sub _build__root { my $self = shift; blessed($self->root) ? $self->root->stringify : $self->root }
48             sub _build__work { File::Spec->catdir(shift->_root, ".txn_work_dir") } # top level for all temp files
49             sub _build__txns { File::Spec->catdir(shift->_work, "txns") } # one subdir per transaction, used for temporary files when transactions are active
50             sub _build__backups { File::Spec->catdir(shift->_work, "backups") } # one subdir per transaction, used during commit to root
51             sub _build__locks { File::Spec->catdir(shift->_work, "locks") } # shared between all workers, directory for lockfiles
52             sub _build__dirty { File::Spec->catfile(shift->_work, "dirty") }
53             sub _build__dirty_lock { shift->_dirty . ".lock" }
54              
55             has nfs => (
56             isa => "Bool",
57             is => "ro",
58             default => 0,
59             );
60              
61             has global_lock => (
62             isa => "Bool",
63             is => "ro",
64             lazy => 1,
65             default => sub { shift->nfs },
66             );
67              
68             has auto_commit => (
69             isa => "Bool",
70             is => "ro",
71             default => 1,
72             );
73              
74             has crash_detection => (
75             isa => "Bool",
76             is => "ro",
77             default => 1,
78             );
79              
80             has timeout => (
81             isa => "Num",
82             is => "ro",
83             predicate => "has_timeout",
84             );
85              
86             sub _get_lock {
87             my ( $self, @args ) = @_;
88              
89             return $self->nfs ? $self->_get_nfslock(@args) : $self->_get_flock(@args);
90             }
91              
92             # slow, portable locking
93             # relies on atomic link()
94             # on OSX the stress test gets race conditions
95             sub _get_nfslock {
96             my ( $self, $file, $mode ) = @_;
97              
98             # create the parent directory for the lock if necessary
99             # (the lock dir is cleaned on destruction)
100             my ( $vol, $dir ) = File::Spec->splitpath($file);
101             my $parent = File::Spec->catpath($vol, $dir, '');
102             make_path($parent) unless -d $parent;
103              
104             require File::NFSLock;
105             if ( my $lock = File::NFSLock->new({
106             file => $file,
107             lock_type => $mode,
108             ( $self->has_timeout ? ( blocking_timeout => $self->timeout ) : () ),
109             }) ) {
110             return $lock;
111             } elsif ( not($mode & LOCK_NB) ) {
112             no warnings 'once';
113             die $File::NFSLock::errstr;
114             }
115              
116             return;
117             }
118              
119             # much faster locking, doesn't work on NFS though
120             sub _get_flock {
121             my ( $self, $file, $mode ) = @_;
122              
123             # create the parent directory for the lock if necessary
124             # (the lock dir is cleaned on destruction)
125             my ( $vol, $dir ) = File::Spec->splitpath($file);
126             my $parent = File::Spec->catpath($vol, $dir, '');
127             make_path($parent) unless -d $parent;
128              
129             # open the lockfile, creating if necessary
130             open my $fh, "+>", $file or die $!;
131              
132             my $ret;
133              
134             if ( not($mode & LOCK_NB) and $self->has_timeout ) {
135             local $SIG{ALRM} = sub { croak "Lock timed out" };
136             alarm($self->timeout);
137             $ret = flock($fh, $mode);
138             alarm(0);
139             } else {
140             $ret = flock($fh, $mode);
141             }
142              
143             if ( $ret ) {
144             my $class = ($mode & LOCK_EX) ? "Directory::Transactional::Lock::Exclusive" : "Directory::Transactional::Lock::Shared";
145             return bless $fh, $class;
146             } elsif ( $!{EWOULDBLOCK} or $!{EAGAIN} ) {
147             # LOCK_NB failed
148             return;
149             } else {
150             # die on any error except failing to obtain a nonblocking lock
151             die $!;
152             }
153             }
154              
155             # support methods for fine grained locking
156             {
157             package Directory::Transactional::Lock;
158             BEGIN {
159             $Directory::Transactional::Lock::VERSION = '0.09';
160             }
161              
162             sub unlock { close $_[0] }
163             sub is_exclusive { 0 }
164             sub is_shared { 0 }
165             sub upgrade { }
166             sub upgrade_nb { $_[0] }
167             sub downgrade { }
168              
169             package Directory::Transactional::Lock::Exclusive;
170             BEGIN {
171             $Directory::Transactional::Lock::Exclusive::VERSION = '0.09';
172             }
173             use Fcntl qw(LOCK_SH);
174              
175             BEGIN { our @ISA = qw(Directory::Transactional::Lock) }
176              
177             sub is_exclusive { 1 }
178              
179             sub downgrade {
180             my $self = shift;
181             flock($self, LOCK_SH) or die $!;
182             bless $self, "Directory::Transactional::Lock::Shared";
183             }
184              
185             package Directory::Transactional::Lock::Shared;
186             BEGIN {
187             $Directory::Transactional::Lock::Shared::VERSION = '0.09';
188             }
189             use Fcntl qw(LOCK_EX LOCK_NB);
190              
191             BEGIN { our @ISA = qw(Directory::Transactional::Lock) }
192              
193             sub is_shared { 1 }
194             sub upgrade {
195             my $self = shift;
196             flock($self, LOCK_EX) or die $!;
197             bless($self, "Directory::Transactional::Lock::Exclusive");
198             }
199              
200             sub upgrade_nb {
201             my $self = shift;
202              
203             unless ( flock($self, LOCK_EX|LOCK_NB) ) {
204             if ( $!{EWOULDBLOCK} ) {
205             return;
206             } else {
207             die $!;
208             }
209             }
210              
211             bless($self, "Directory::Transactional::Lock::Exclusive");
212             }
213             }
214              
215             # this is the current active TXN (head of transaction stack)
216             has _txn => (
217             isa => "Directory::Transactional::TXN",
218             is => "rw",
219             clearer => "_clear_txn",
220             );
221              
222             has _shared_lock_file => (
223             isa => "Str",
224             is => "ro",
225             lazy_build => 1,
226             );
227              
228             sub _build__shared_lock_file { shift->_work . ".lock" }
229              
230             has _shared_lock => (
231             is => "ro",
232             lazy_build => 1,
233             );
234              
235             # the shared lock is always taken at startup
236             # a nonblocking attempt to lock it exclusively is made first, and if granted we
237             # have exclusive access to the work directory so recovery is run if necessary
238             sub _build__shared_lock {
239             my $self = shift;
240              
241             my $file = $self->_shared_lock_file;
242              
243             if ( my $ex_lock = $self->_get_lock( $file, LOCK_EX|LOCK_NB ) ) {
244             $self->recover;
245              
246             undef $ex_lock;
247             }
248              
249             $self->_get_lock($file, LOCK_SH);
250             }
251              
252             sub BUILD {
253             my $self = shift;
254              
255             croak "If 'nfs' is set then so must be 'global_lock'"
256             if $self->nfs and !$self->global_lock;
257              
258             # obtains the shared lock, running recovery if needed
259             $self->_shared_lock;
260              
261             make_path($self->_work);
262             }
263              
264             sub DEMOLISH {
265             my $self = shift;
266              
267             return if $self->_fatal; # encountered a fatal error, we need to run recovery
268              
269             # rollback any open txns
270             while ( $self->_txn ) {
271             $self->txn_rollback;
272             }
273              
274             # lose the shared lock
275             $self->_clear_shared_lock;
276              
277             # cleanup workdirs
278             # only remove if no other workers are active, so that there is no race
279             # condition in their directory creation code
280             if ( my $ex_lock = $self->_get_lock( $self->_shared_lock_file, LOCK_EX|LOCK_NB ) ) {
281             # we don't really care if there's an error
282             try { local $SIG{__WARN__} = sub { }; remove_tree($self->_locks) };
283             rmdir $self->_work;
284             rmdir $self->_txns;
285             rmdir $self->_backups;
286              
287             unlink $self->_dirty;
288             unlink $self->_dirty_lock;
289              
290             rmdir $self->_work;
291              
292             CORE::unlink $self->_shared_lock_file;
293             }
294             }
295              
296             sub check_dirty {
297             my $self = shift;
298              
299             return unless $self->crash_detection;
300              
301             # get the short lived dirty flag manipulation lock
302             # nobody else can check or modify the dirty flag while we have it
303             my $ex_lock = $self->_get_lock( $self->_dirty_lock, LOCK_EX );
304              
305             my $dirty = $self->_dirty;
306              
307             # if the dirty flag is set, run a check
308             if ( -e $dirty ) {
309             my $b = $self->_backups;
310              
311             # go through the comitting transactions
312             foreach my $name ( IO::Dir->new($b)->read ) {
313             next unless $name =~ /^[\w\-]+$/; # txn dir
314              
315             my $dir = File::Spec->catdir($b, $name);
316              
317             if ( my $ex_lock = $self->_get_lock( $dir . ".lock", LOCK_EX|LOCK_NB ) ) {
318             # there is a potential race condition between the readdir
319             # and getting the lock. make sure it still exists
320             if ( -d $dir ) {
321             $self->online_recover;
322             return $ex_lock;
323             }
324             }
325             }
326              
327             # the check passed, now we can clear the dirty flag if there are no
328             # other running commits
329             if ( my $flag_ex_lock = $self->_get_lock( $dirty, LOCK_EX|LOCK_NB ) ) {
330             unlink $dirty;
331             }
332             }
333              
334             # return the lock.
335             # for as long as it is held the workdir cannot be marked dirty except by
336             # this process
337             return $ex_lock;
338             }
339              
340             sub set_dirty {
341             my $self = shift;
342              
343             return unless $self->crash_detection;
344              
345             # first check that the dir is not dirty, and take an exclusive lock for
346             # dirty flag manipulation
347             my $ex_lock = $self->check_dirty;
348              
349             # next mark the dir as dirty, and take a shared lock so the flag won't be
350             # cleared by check_dirty
351              
352             my $dirty_lock = $self->_get_lock( $self->_dirty, LOCK_SH );
353              
354             # create the file if necessary (nfs uses an auxillary lock file)
355             open my $fh, ">", $self->_dirty or die $! if $self->nfs;
356              
357             return $dirty_lock;
358             }
359              
360             sub recover {
361             my $self = shift;
362              
363             # first rollback partially comitted transactions if there are any
364             if ( -d ( my $b = $self->_backups ) ) {
365             foreach my $name ( IO::Dir->new($b)->read ) {
366             next if $name eq '.' || $name eq '..';
367              
368             my $txn_backup = File::Spec->catdir($b, $name); # each of these is one transaction
369              
370             if ( -d $txn_backup ) {
371             my $files = $self->_get_file_list($txn_backup);
372              
373             # move all the backups back into the root directory
374             $self->merge_overlay( from => $txn_backup, to => $self->_root, files => $files );
375              
376             remove_tree($txn_backup);
377             }
378             }
379              
380             remove_tree($b, { keep_root => 1 });
381             }
382              
383             # delete all temp files (fully comitted but not cleaned up transactions,
384             # and uncomitted transactions)
385             if ( -d $self->_txns ) {
386             remove_tree( $self->_txns, { keep_root => 1 } );
387             }
388              
389             unlink $self->_dirty;
390             unlink $self->_dirty_lock;
391             }
392              
393             sub online_recover {
394             my $self = shift;
395              
396             unless ( $self->nfs ) { # can't upgrade an nfs lock
397             my $lock = $self->_shared_lock;
398              
399             if ( $lock->upgrade_nb ) {
400             $self->recover;
401             $lock->downgrade;
402             return 1;
403             }
404             }
405              
406             $self->_fatal(1);
407             croak "Detected crashed transaction, terminate all processes and run recovery by reinstantiating directory";
408             }
409              
410             sub _get_file_list {
411             my ( $self, $from ) = @_;
412              
413             my $files = Set::Object->new;
414              
415             find( { no_chdir => 1, wanted => sub { $files->insert( File::Spec->abs2rel($_, $from) ) if -f $_ } }, $from );
416              
417             return $files;
418             }
419              
420             sub merge_overlay {
421             my ( $self, %args ) = @_;
422              
423             my ( $from, $to, $backup, $files ) = @args{qw(from to backup files)};
424              
425             my @rem;
426              
427             # if requested, back up first by moving all the files from the target
428             # directory to the backup directory
429             if ( $backup ) {
430             foreach my $file ( $files->members ) {
431             my $src = File::Spec->catfile($to, $file);
432              
433             next unless -e $src; # there is no source file to back
434              
435             my $targ = File::Spec->catfile($backup, $file);
436              
437             # create the parent directory in the backup dir as necessary
438             my ( undef, $dir ) = File::Spec->splitpath($targ);
439             if ( $dir ) {
440             make_path($dir) unless -d $dir;
441             }
442              
443             CORE::rename $src, $targ or die $!;
444             }
445             }
446              
447             # then apply all the changes to the target dir from the source dir
448             foreach my $file ( $files->members ) {
449             my $src = File::Spec->catfile($from,$file);
450              
451             if ( -f $src ) {
452             my $targ = File::Spec->catfile($to,$file);
453              
454             # make sure the parent directory in the target path exists first
455             my ( undef, $dir ) = File::Spec->splitpath($targ);
456             if ( $dir ) {
457             make_path($dir) unless -d $dir;
458             }
459              
460             if ( -f $src ) {
461             CORE::rename $src => $targ or die $!;
462             } elsif ( -f $targ ) {
463             CORE::unlink $targ or die $!;
464             }
465             }
466             }
467             }
468              
469             sub txn_do {
470             my ( $self, @args ) = @_;
471              
472             unshift @args, "body" if @args % 2;
473              
474             my %args = @args;
475              
476             my ( $coderef, $commit, $rollback, $code_args ) = @args{qw(body commit rollback args)};
477              
478             ref $coderef eq 'CODE' or croak '$coderef must be a CODE reference';
479              
480             $code_args ||= [];
481              
482             $self->txn_begin;
483              
484             my @result;
485              
486             my $wantarray = wantarray; # gotta capture, eval { } has its own
487              
488             my ( $success, $err ) = do {
489             local $@;
490              
491             my $success = eval {
492             if ( $wantarray ) {
493             @result = $coderef->(@$code_args);
494             } elsif( defined $wantarray ) {
495             $result[0] = $coderef->(@$code_args);
496             } else {
497             $coderef->(@$code_args);
498             }
499              
500             $commit && $commit->();
501             $self->txn_commit;
502              
503             1;
504             };
505              
506             ( $success, $@ );
507             };
508              
509             if ( $success ) {
510             return wantarray ? @result : $result[0];
511             } else {
512             my $rollback_exception = do {
513             local $@;
514             eval { $self->txn_rollback; $rollback && $rollback->() };
515             $@;
516             };
517              
518             if ($rollback_exception) {
519             croak "Transaction aborted: $err, rollback failed: $rollback_exception";
520             }
521              
522             die $err;
523             }
524             }
525              
526             sub txn_begin {
527             my ( $self, @args ) = @_;
528              
529             my $txn;
530              
531             if ( my $p = $self->_txn ) {
532             # this is a child transaction
533              
534             croak "Can't txn_begin if an auto transaction is still alive" if $p->auto_handle;
535              
536             $txn = Directory::Transactional::TXN::Nested->new(
537             parent => $p,
538             manager => $self,
539             );
540             } else {
541             # this is a top level transaction
542             $txn = Directory::Transactional::TXN::Root->new(
543             @args,
544             manager => $self,
545             ( $self->global_lock ? (
546             # when global_lock is set, take an exclusive lock on the root dir
547             # non global lockers take a shared lock on it
548             global_lock => $self->_get_flock( File::Spec->catfile( $self->_locks, ".lock" ), LOCK_EX)
549             ) : () ),
550             );
551             }
552              
553             $self->_txn($txn);
554              
555             return;
556             }
557              
558             sub _pop_txn {
559             my $self = shift;
560              
561             my $txn = $self->_txn or croak "No active transaction";
562              
563             if ( $txn->isa("Directory::Transactional::TXN::Nested") ) {
564             $self->_txn( $txn->parent );
565             } else {
566             $self->_clear_txn;
567             }
568              
569             return $txn;
570             }
571              
572             sub txn_commit {
573             my $self = shift;
574              
575             my $txn = $self->_txn;
576              
577             my $changed = $txn->changed;
578              
579             if ( $changed->size ) {
580             if ( $txn->isa("Directory::Transactional::TXN::Root") ) {
581             # commit the work, backing up in the backup dir
582              
583             # first take a lock on the backup dir
584             # this is used to detect crashed transactions
585             # if the dir exists but isn't locked then the transaction crashed
586             my $txn_lockfile = $txn->backup . ".lock";
587             my $txn_lock = $self->_get_lock( $txn_lockfile, LOCK_EX );
588              
589             {
590             # during a commit the work dir is considered dirty
591             # this flag is set until check_dirty clears it
592             my $dirty_lock = $self->set_dirty;
593              
594             $txn->create_backup_dir;
595              
596             # move all the files from the txn dir into the root dir, using the backup dir
597             $self->merge_overlay( from => $txn->work, to => $self->_root, backup => $txn->backup, files => $changed );
598              
599             # we're finished, remove backup dir denoting successful commit
600             CORE::rename $txn->backup, $txn->work . ".cleanup" or die $!;
601             }
602              
603             unlink $txn_lockfile;
604             } else {
605             # it's a nested transaction, which means we don't need to be
606             # careful about comitting to the parent, just share all the locks,
607             # deletion metadata etc by merging it
608             $txn->propagate;
609              
610             $self->merge_overlay( from => $txn->work, to => $txn->parent->work, files => $changed );
611             }
612              
613             # clean up work dir and (renamed) backup dir
614             remove_tree( $txn->work );
615             remove_tree( $txn->work . ".cleanup" );
616             }
617              
618             $self->_pop_txn;
619              
620             return;
621             }
622              
623             sub txn_rollback {
624             my $self = shift;
625              
626             my $txn = $self->_pop_txn;
627              
628             if ( $txn->isa("Directory::Transactional::TXN::Root") ) {
629             # an error happenned during txn_commit trigerring a rollback
630             if ( -d ( my $txn_backup = $txn->backup ) ) {
631             my $files = $self->_get_file_list($txn_backup);
632              
633             # move all the backups back into the root directory
634             $self->merge_overlay( from => $txn_backup, to => $self->_root, files => $files );
635             }
636             } else {
637             # any inherited locks that have been upgraded in this txn need to be
638             # downgraded back to shared locks
639             foreach my $lock ( @{ $txn->downgrade } ) {
640             $lock->downgrade;
641             }
642             }
643              
644             # now all we need to do is trash the tempfiles and we're done
645             if ( $txn->has_work ) {
646             remove_tree( $txn->work );
647             }
648              
649             return;
650             }
651              
652             sub _auto_txn {
653             my $self = shift;
654              
655             return if $self->_txn;
656              
657             croak "Auto commit is disabled" unless $self->auto_commit;
658              
659             require Scope::Guard;
660              
661             $self->txn_begin;
662              
663             return Scope::Guard->new(sub { $self->txn_commit });
664             }
665              
666             sub _resource_auto_txn {
667             my $self = shift;
668              
669             if ( my $txn = $self->_txn ) {
670             # return the same handle so that more resources can be registered
671             return $txn->auto_handle;
672             } else {
673             croak "Auto commit is disabled" unless $self->auto_commit;
674              
675             require Directory::Transactional::AutoCommit;
676              
677             my $h = Directory::Transactional::AutoCommit->new( manager => $self );
678              
679             $self->txn_begin( auto_handle => $h );
680              
681             return $h;
682             }
683             }
684              
685             sub _lock_path_read {
686             my ( $self, $path ) = @_;
687              
688             my $txn = $self->_txn;
689              
690             if ( my $lock = $txn->find_lock($path) ) {
691             return $lock;
692             } else {
693             my $lock = $self->_get_flock( File::Spec->catfile( $self->_locks, $path . ".lock" ), LOCK_SH);
694             $txn->set_lock( $path, $lock );
695             }
696             }
697              
698             sub _lock_path_write {
699             my ( $self, $path ) = @_;
700              
701             my $txn = $self->_txn;
702              
703             if ( my $lock = $txn->get_lock($path) ) {
704             # simplest scenario, we already have a lock in this transaction
705             $lock->upgrade; # upgrade it if necessary
706             } elsif ( my $inherited_lock = $txn->find_lock($path) ) {
707             # a parent transaction has a lock
708             unless ( $inherited_lock->is_exclusive ) {
709             # upgrade it, and mark for downgrade on rollback
710             $inherited_lock->upgrade;
711             push @{ $txn->downgrade }, $inherited_lock;
712             }
713             $txn->set_lock( $path, $inherited_lock );
714             } else {
715             # otherwise create a new lock
716             my $lock = $self->_get_flock( File::Spec->catfile( $self->_locks, $path . ".lock" ), LOCK_EX);
717             $txn->set_lock( $path, $lock );
718             }
719             }
720              
721             sub _lock_parent {
722             my ( $self, $path ) = @_;
723              
724             my ( undef, $dir ) = File::Spec->splitpath($path);
725              
726             my @dirs = File::Spec->splitdir($dir);
727              
728             {
729             no warnings 'uninitialized';
730             pop @dirs unless length $dirs[-1]; # trailing slash
731             }
732             pop @dirs if $dir eq $path;
733              
734             my $parent = "";
735              
736             do {
737             $self->_lock_path_read($parent);
738             } while (
739             @dirs
740             and
741             $parent = length($parent)
742             ? File::Spec->catdir($parent, shift @dirs)
743             : shift @dirs
744             );
745              
746             return;
747             }
748              
749             # lock a path for reading
750             sub lock_path_read {
751             my ( $self, $path ) = @_;
752              
753             unless ( $self->_txn ) {
754             croak("Can't lock file for reading without an active transaction");
755             }
756              
757             return if $self->global_lock;
758              
759             $self->_lock_parent($path);
760              
761             $self->_lock_path_read($path);
762              
763             return;
764             }
765              
766             sub lock_path_write {
767             my ( $self, $path ) = @_;
768              
769             unless ( $self->_txn ) {
770             croak("Can't lock file for writing without an active transaction");
771             }
772              
773             return if $self->global_lock;
774              
775             $self->_lock_parent($path);
776              
777             $self->_lock_path_write($path);
778              
779             return;
780             }
781              
782             sub _txn_stack {
783             my $self = shift;
784              
785             if ( my $txn = $self->_txn ) {
786             my @ret = $txn;
787             push @ret, $txn = $txn->parent while $txn->can("parent");
788             return @ret;
789             }
790              
791             return;
792             }
793              
794             sub _txn_for_path {
795             my ( $self, $path ) = @_;
796              
797             if ( my $txn = $self->_txn ) {
798             do {
799             if ( $txn->is_changed_in_head($path) ) {
800             return $txn;
801             };
802             } while ( $txn->can("parent") and $txn = $txn->parent );
803             }
804              
805             return;
806             }
807              
808             sub _locate_dirs_in_overlays {
809             my ( $self, $path ) = @_;
810              
811             my @dirs = ( (map { $_->work } $self->_txn_stack), $self->root );
812              
813             if ( defined $path ) {
814             return grep { -d $_ } map { File::Spec->catdir($_, $path) } @dirs;
815             } else {
816             return @dirs;
817             }
818             }
819              
820             sub _locate_file_in_overlays {
821             my ( $self, $path ) = @_;
822              
823             if ( my $txn = $self->_txn_for_path($path) ) {
824             File::Spec->catfile($txn->work, $path);
825             } else {
826             #unless ( $self->_txn->find_lock($path) ) { # can't optimize this way if an explicit lock was taken
827             # we only take a read lock on the root dir if the state isn't dirty
828             my $ex_lock = $self->check_dirty;
829             $self->lock_path_read($path);
830             #}
831             File::Spec->catfile($self->_root, $path);
832             }
833             }
834              
835             sub old_stat {
836             my ( $self, $path ) = @_;
837              
838             my $t = $self->_auto_txn;
839              
840             CORE::stat($self->_locate_file_in_overlays($path));
841             }
842              
843             sub stat {
844             my ( $self, $path ) = @_;
845              
846             my $t = $self->_auto_txn;
847              
848             require File::stat;
849             File::stat::stat($self->_locate_file_in_overlays($path));
850             }
851              
852             sub is_deleted {
853             my ( $self, $path ) = @_;
854              
855             not $self->exists($path);
856             }
857              
858             sub exists {
859             my ( $self, $path ) = @_;
860              
861             my $t = $self->_auto_txn;
862              
863             return -e $self->_locate_file_in_overlays($path);
864             }
865              
866             sub is_dir {
867             my ( $self, $path ) = @_;
868              
869             my $t = $self->_auto_txn;
870              
871             # FIXME this is an ugly kludge, we really need to keep better track of
872             # why/when directories are created, make note of them in 'is_changed', etc.
873              
874             my @dirs = ( (map { $_->work } $self->_txn_stack), $self->root );
875              
876             foreach my $dir ( @dirs ) {
877             return 1 if -d File::Spec->catdir($dir, $path);
878             }
879              
880             return;
881             }
882              
883             sub is_file {
884             my ( $self, $path ) = @_;
885              
886             my $t = $self->_auto_txn;
887              
888             return -f $self->_locate_file_in_overlays($path);
889             }
890              
891             sub unlink {
892             my ( $self, $path ) = @_;
893              
894             my $t = $self->_auto_txn;
895              
896             # lock parent for writing
897             my ( undef, $dir ) = File::Spec->splitpath($path);
898             $self->lock_path_write($dir);
899              
900             my $txn_file = $self->_work_path($path);
901              
902             if ( -e $txn_file ) {
903             CORE::unlink $txn_file or die $!;
904             } else {
905             return 1;
906             }
907             }
908              
909             sub rename {
910             my ( $self, $from, $to ) = @_;
911              
912             my $t = $self->_auto_txn;
913              
914             foreach my $path ( $from, $to ) {
915             # lock parents for writing
916             my ( undef, $dir ) = File::Spec->splitpath($path);
917             $self->lock_path_write($dir);
918             }
919              
920             $self->vivify_path($from),
921              
922             CORE::rename (
923             $self->_work_path($from),
924             $self->_work_path($to),
925             ) or die $!;
926             }
927              
928             sub openr {
929             my ( $self, $file ) = @_;
930              
931             my $t = $self->_resource_auto_txn;
932              
933             my $src = $self->_locate_file_in_overlays($file);
934              
935             open my $fh, "<", $src or die "openr($file): $!";
936              
937             $t->register($fh) if $t;
938              
939             return $fh;
940             }
941              
942             sub openw {
943             my ( $self, $path ) = @_;
944              
945             my $t = $self->_resource_auto_txn;
946              
947             my $txn = $self->_txn;
948              
949             my $file = File::Spec->catfile( $txn->work, $path );
950              
951             unless ( $txn->is_changed_in_head($path) ) {
952             my ( undef, $dir ) = File::Spec->splitpath($path);
953              
954             $self->lock_path_write($path);
955              
956             make_path( File::Spec->catdir($txn->work, $dir) ) if length($dir); # FIXME only if it exists in the original?
957             }
958              
959             $txn->mark_changed($path);
960              
961             open my $fh, ">", $file or die "openw($path): $!";
962              
963             $t->register($fh) if $t;
964              
965             return $fh;
966             }
967              
968             sub opena {
969             my ( $self, $file ) = @_;
970              
971             my $t = $self->_resource_auto_txn;
972              
973             $self->vivify_path($file);
974              
975             open my $fh, ">>", $self->_work_path($file) or die "opena($file): $!";
976              
977             $t->register($fh) if $t;
978              
979             return $fh;
980             }
981              
982             sub open {
983             my ( $self, $mode, $file ) = @_;
984              
985             my $t = $self->_resource_auto_txn;
986              
987             $self->vivify_path($file);
988              
989             open my $fh, $mode, $self->_work_path($file) or die "open($mode, $file): $!";
990              
991             $t->register($fh) if $t;
992              
993             return $fh;
994             }
995              
996             sub _readdir_from_overlay {
997             my ( $self, $path ) = @_;
998              
999             my $t = $self->_auto_txn;
1000              
1001             my $ex_lock = $self->check_dirty;
1002              
1003             my @dirs = $self->_locate_dirs_in_overlays($path);
1004              
1005             my $files = Set::Object->new;
1006              
1007             # compute union of all directories
1008             foreach my $dir ( @dirs ) {
1009             $files->insert( IO::Dir->new($dir)->read );
1010             }
1011              
1012             unless ( defined $path ) {
1013             $files->remove(".txn_work_dir");
1014             $files->remove(".txn_work_dir.lock");
1015             $files->remove(".txn_work_dir.lock.NFSLock") if $self->nfs;
1016             }
1017              
1018             return $files;
1019             }
1020              
1021             sub readdir {
1022             my ( $self, $path ) = @_;
1023              
1024             undef $path if $path eq "/" or !length($path);
1025              
1026             my $t = $self->_auto_txn;
1027              
1028             my $files = $self->_readdir_from_overlay($path);
1029              
1030             my @txns = $self->_txn_stack;
1031              
1032             # remove deleted files
1033             file: foreach my $file ( $files->members ) {
1034             next if $file eq '.' or $file eq '..';
1035              
1036             my $file_path = $path ? File::Spec->catfile($path, $file) : $file;
1037              
1038             foreach my $txn ( @txns ) {
1039             if ( $txn->is_changed_in_head($file_path) ) {
1040             if ( not( -e File::Spec->catfile( $txn->work, $file_path ) ) ) {
1041             $files->remove($file);
1042             }
1043             next file;
1044             }
1045             }
1046             }
1047              
1048             return $files->members;
1049             }
1050              
1051             sub list {
1052             my ( $self, $path ) = @_;
1053              
1054             undef $path if $path eq "/" or !length($path);
1055              
1056             my $t = $self->_auto_txn;
1057              
1058             my $files = $self->_readdir_from_overlay($path);
1059              
1060             $files->remove('.', '..');
1061              
1062             my @txns = $self->_txn_stack;
1063              
1064             my @ret;
1065              
1066             # remove deleted files
1067             file: foreach my $file ( $files->members ) {
1068             my $file_path = $path ? File::Spec->catfile($path, $file) : $file;
1069              
1070             foreach my $txn ( @txns ) {
1071             if ( $txn->is_changed_in_head($file_path) ) {
1072             if ( -e File::Spec->catfile( $txn->work, $file_path ) ) {
1073             push @ret, $file_path;
1074             }
1075             next file;
1076             }
1077             }
1078              
1079             push @ret, $file_path;
1080             }
1081              
1082             return sort @ret;
1083             }
1084              
1085             sub _work_path {
1086             my ( $self, $path ) = @_;
1087              
1088             $self->lock_path_write($path);
1089              
1090             my $txn = $self->_txn;
1091              
1092             $txn->mark_changed($path);
1093              
1094             my $file = File::Spec->catfile( $txn->work, $path );
1095              
1096             my ( undef, $dir ) = File::Spec->splitpath($path);
1097             make_path( File::Spec->catdir($txn->work, $dir ) ) if length($dir); # FIXME only if it exists in the original?
1098              
1099             return $file;
1100             }
1101              
1102             sub vivify_path {
1103             my ( $self, $path ) = @_;
1104              
1105             my $txn = $self->_txn;
1106              
1107             my $txn_path = File::Spec->catfile( $txn->work, $path );
1108              
1109             unless ( $txn->is_changed_in_head($path) ) {
1110             $self->lock_path_write($path);
1111              
1112             my $src = $self->_locate_file_in_overlays($path);
1113              
1114             if ( my $stat = File::stat::stat($src) ) {
1115             if ( $stat->nlink > 1 ) {
1116             croak "the file $src has a link count of more than one.";
1117             }
1118              
1119             if ( -l $src ) {
1120             croak "The file $src is a symbolic link.";
1121             }
1122              
1123             $self->_work_path($path); # FIXME vivifies parent dir
1124             copy( $src, $txn_path ) or die "copy($src, $txn_path): $!";
1125             }
1126             }
1127              
1128             return $txn_path;
1129             }
1130              
1131              
1132              
1133             sub file_stream {
1134             my ( $self, @args ) = @_;
1135              
1136             my $t = $self->_resource_auto_txn;
1137              
1138             require Directory::Transactional::Stream;
1139              
1140             my $stream = Directory::Transactional::Stream->new(
1141             manager => $self,
1142             @args,
1143             );
1144              
1145             $t->register($stream) if $t;
1146              
1147             return $stream;
1148             }
1149              
1150             __PACKAGE__->meta->make_immutable;
1151              
1152             __PACKAGE__
1153              
1154             __END__
1155              
1156             =pod
1157              
1158             =head1 NAME
1159              
1160              
1161             =head1 VERSION
1162              
1163             version 0.09
1164             Directory::Transactional - ACID transactions on a set of files with
1165             journalling/recovery using C<flock> or L<File::NFSLock>
1166              
1167             =head1 SYNOPSIS
1168              
1169             use Directory::Transactional;
1170              
1171             my $d = Directory::Transactional->new( root => $path );
1172              
1173             $d->txn_do(sub {
1174             my $fh = $d->openw("path/to/file");
1175              
1176             $fh->print("I AR MODIFY");
1177              
1178             close $fh;
1179             });
1180              
1181             =head1 DESCRIPTION
1182              
1183             This module provides lock based transactions over a set of files with full
1184             supported for nested transactions.
1185              
1186             =head1 THE RULES
1187              
1188             There are a few limitations to what this module can do.
1189              
1190             Following this guideline will prevent unpleasant encounters:
1191              
1192             =over 4
1193              
1194             =item Always use relative paths
1195              
1196             No attempt is made to sanify paths reaching outside of the root.
1197              
1198             All paths are assumed to be relative and within the root.
1199              
1200             =item No funny stuff
1201              
1202             Stick with plain files, with a link count of 1, or you will not get what you
1203             expect.
1204              
1205             For instance a rename will first copy the source file to the txn work dir, and
1206             then when comitting rename that file to the target dir and unlink the original.
1207              
1208             While seemingly more work, this is the only way to ensure that modifications to
1209             the file both before and after the rename are consistent.
1210              
1211             Modifications to directories are likewise not supported, but support may be
1212             added in the future.
1213              
1214             =item Always work in a transaction
1215              
1216             If you don't need transaction, use a global lock file and don't use this
1217             module.
1218              
1219             If you do, then make sure even your read access goes through this object with
1220             an active transaction, or you may risk reading uncomitted data, or conflicting
1221             with the transaction commit code.
1222              
1223             =item Use C<global_lock> or make sure you lock right
1224              
1225             If you stick to modifying the files through the API then you shouldn't have
1226             issues with locking, but try not to reuse paths and always reask for them to
1227             ensure that the right "real" path is returned even if the transaction stack has
1228             changed, or anything else.
1229              
1230             =item No forking
1231              
1232             If you fork in the middle of the transaction both the parent and the child have
1233             write locks, and both the parent and the child will try to commit or rollback
1234             when resources are being cleaned up.
1235              
1236             Either create the L<Directory::Transactional> instance within the child
1237             process, or use L<POSIX/_exit> and do not open or close any transactions in the
1238             child.
1239              
1240             =item No mixing of C<nfs> and C<flock>
1241              
1242             C<nfs> mode is not compatible with C<flock> mode. If you enable C<nfs> enable
1243             it in B<all> processes working on the same directory.
1244              
1245             Conversely, under C<flock> mode C<global_lock> B<is> compatible with fine
1246             grained locking.
1247              
1248             =back
1249              
1250             =head1 ACID GUARANTEES
1251              
1252             ACID stands for atomicity, consistency, isolation and durability.
1253              
1254             Transactions are atomic (using locks), consistent (a recovery mode is able to
1255             restore the state of the directory if a process crashed while comitting a
1256             transaction), isolated (each transaction works in its own temporary directory),
1257             and durable (once C<txn_commit> returns a software crash will not cause the
1258             transaction to rollback).
1259              
1260             =head1 TRANSACTIONAL PROTOCOL
1261              
1262             This section describes the way the ACID guarantees are met:
1263              
1264             When the object is being constructed a nonblocking attempt to get an exclusive
1265             lock on the global shared lock file using L<File::NFSLock> or C<flock> is made.
1266              
1267             If this lock is successful this means that this object is the only active
1268             instance, and no other instance can access the directory for now.
1269              
1270             The work directory's state is inspected, any partially comitted transactions
1271             are rolled back, and all work files are cleaned up, producing a consistent
1272             state.
1273              
1274             At this point the exclusive lock is dropped, and a shared lock on the same file
1275             is taken, which will be retained for the lifetime of the object.
1276              
1277             Each transaction (root or nested) gets its own work directory, which is an
1278             overlay of its parent.
1279              
1280             All write operations are performed in the work directory, while read operations
1281             walk up the tree.
1282              
1283             Aborting a transaction consists of simply removing its work directory.
1284              
1285             Comitting a nested transaction involves overwriting its parent's work directory
1286             with all the changes in the child transaction's work directory.
1287              
1288             Comitting a root transaction to the root directory involves moving aside every
1289             file from the root to a backup directory, then applying the changes in the work
1290             directory to the root, renaming the backup directory to a work directory, and
1291             then cleaning up the work directory and the renamed backup directory.
1292              
1293             If at any point in the root transaction commit work is interrupted, the backup
1294             directory acts like a journal entry. Recovery will rollback this transaction by
1295             restoring all the renamed backup files. Moving the backup directory into the
1296             work directory signifies that the transaction has comitted successfully, and
1297             recovery will clean these files up normally.
1298              
1299             If C<crash_detection> is enabled (the default) when reading any file from the
1300             root directory (shared global state) the system will first check for crashed
1301             commits.
1302              
1303             Crashed commits are detected by means of lock files. If the backup directory is
1304             locked that means its comitting process is still alive, but if a directory
1305             exists without a lock then that process has crashed. A global dirty flag is
1306             maintained to avoid needing to check all the backup directories each time.
1307              
1308             If the commit is still running then it can be assumed that the process
1309             comitting it still has all of its exclusive locks so reading from the root
1310             directory is safe.
1311              
1312             =head1 DEADLOCKS
1313              
1314             This module does not implement deadlock detection. Unfortunately maintaing a
1315             lock table is a delicate and difficult task, so I doubt I will ever implement
1316             it.
1317              
1318             The good news is that certain operating systems (like HPUX) may implement
1319             deadlock detection in the kernel, and return C<EDEADLK> instead of just
1320             blocking forever.
1321              
1322             If you are not so lucky, specify a C<timeout> or make sure you always take
1323             locks in the same order.
1324              
1325             The C<global_lock> flag can also be used to prevent deadlocks entirely, at the
1326             cost of concurrency. This provides fully serializable level transaction
1327             isolation with no possibility of serialization failures due to deadlocks.
1328              
1329             There is no pessimistic locking mode (read-modify-write optimized) since all
1330             paths leading to a file are locked for reading. This mode, if implemented,
1331             would be semantically identical to C<global_lock> but far less efficient.
1332              
1333             In the future C<fcntl> based locking may be implemented in addition to
1334             C<flock>. C<EDEADLK> seems to be more widely supported when using C<fcntl>.
1335              
1336             =head1 LIMITATIONS
1337              
1338             =head2 Auto-Commit
1339              
1340             If you perform any operation outside of a transaction and C<auto_commit> is
1341             enabled a transaction will be created for you.
1342              
1343             For operations like C<rename> or C<readdir> which do not return resource the
1344             transaction is comitted immediately.
1345              
1346             Operations like C<open> or C<file_stream> on the other create a transaction
1347             that will be alive as long as the return value is alive.
1348              
1349             This means that you should not leak filehandles when relying on autocommit.
1350              
1351             Opening a new transaction when an automatic one is already opened is an error.
1352              
1353             Note that this resource tracking comes with an overhead, especially on Perl
1354             5.8, so even if you are only performing read operations it is reccomended that
1355             you operate within the scope of a real transaction.
1356              
1357             =head2 Open Filehandles
1358              
1359             One filehandle is required per every lock when using fine grained locking.
1360              
1361             For large transactions it is reccomended you set C<global_lock>, which is like
1362             taking an exclusive lock on the root directory.
1363              
1364             C<global_lock> also performs better, but causes long wait times if multiple
1365             processes are accessing the same database but not the same data. For web
1366             applications C<global_lock> should probably be off for better concurrency.
1367              
1368             =head1 ATTRIBUTES
1369              
1370             =over 4
1371              
1372             =item root
1373              
1374             This is the managed directory in which transactional semantics will be maintained.
1375              
1376             This can be either a string path or a L<Path::Class::Dir>.
1377              
1378             =item _work
1379              
1380             This attribute is named with a leading underscore to prevent thoughtless
1381             modification (if you have two workers accessing the same directory
1382             simultaneously but the work dir is different they will conflict and not even
1383             know it).
1384              
1385             The default work directory is placed under root, and is named C<.txn_work_dir>.
1386              
1387             The work dir's parent must be writable, because a lock file needs to be created
1388             next to it (the workdir name with C<.lock> appended).
1389              
1390             =item nfs
1391              
1392             If true (defaults to false), L<File::NFSLock> will be used for all locks
1393             instead of C<flock>.
1394              
1395             Note that on my machine the stress test reliably B<FAILS> with
1396             L<File::NFSLock>, due to a race condition (exclusive write lock granted to two
1397             writers simultaneously), even on a local filesystem. If you specify the C<nfs>
1398             flag make sure your C<link> system call is truly atomic.
1399              
1400             =item global_lock
1401              
1402             If true instead of using fine grained locking, a global write lock is obtained
1403             on the first call to C<txn_begin> and will be kept for as long as there is a
1404             running transaction.
1405              
1406             This is useful for avoiding deadlocks (there is no deadlock detection code in
1407             the fine grained locking).
1408              
1409             This flag is automatically set if C<nfs> is set.
1410              
1411             =item timeout
1412              
1413             If set will be used to specify a time limit for blocking calls to lock.
1414              
1415             If you are experiencing deadlocks it is reccomended to set this or
1416             C<global_lock>.
1417              
1418             =item auto_commit
1419              
1420             If true (the default) any operation not performed within a transaction will
1421             cause a transaction to be automatically created and comitted.
1422              
1423             Transactions automatically created for operations which return things like
1424             filehandles will stay alive for as long as the returned resource does.
1425              
1426             =item crash_detection
1427              
1428             IF true (the default), all read operations accessing global state (the root
1429             directory) will first ensure that the global directory is not dirty.
1430              
1431             If the perl process crashes while comitting the transaction but other
1432             concurrent processes are still alive, the directory is left in an inconsistent
1433             state, but all the locks are dropped. When C<crash_detection> is enabled ACID
1434             semantics are still guaranteed, at the cost of locking and stating a file for
1435             each read operation on the global directory.
1436              
1437             If you disable this then you are only protected from system crashes (recovery
1438             will be run on the next instantiation of L<Directory::Transactional>) or soft
1439             crashes where the crashing process has a chance to run all its destructors
1440             properly.
1441              
1442             =back
1443              
1444             =head1 METHODS
1445              
1446             =head2 Transaction Management
1447              
1448             =over 4
1449              
1450             =item txn_do $code, %callbacks
1451              
1452             Executes C<$code> within a transaction in an C<eval> block.
1453              
1454             If any error is thrown the transaction will be rolled back. Otherwise the
1455             transaction is comitted.
1456              
1457             C<%callbacks> can contain entries for C<commit> and C<rollback>, which are
1458             called when the appropriate action is taken.
1459              
1460             =item txn_begin
1461              
1462             Begin a new transaction. Can be called even if there is already a running
1463             transaction (nested transactions are supported).
1464              
1465             =item txn_commit
1466              
1467             Commit the current transaction. If it is a nested transaction, it will commit
1468             to the parent transaction's work directory.
1469              
1470             =item txn_rollback
1471              
1472             Discard the current transaction, throwing away all changes since the last call
1473             to C<txn_begin>.
1474              
1475             =back
1476              
1477             =head2 Lock Management
1478              
1479             =over 4
1480              
1481             =item lock_path_read $path, $no_parent
1482              
1483             =item lock_path_write $path, $no_parent
1484              
1485             Lock the resource at C<$path> for writing or reading.
1486              
1487             By default the ancestors of C<$path> will be locked for reading to (from
1488             outermost to innermost).
1489              
1490             The only way to unlock a resource is by comitting the root transaction, or
1491             aborting the transaction in which the resource was locked.
1492              
1493             C<$path> does not have to be a real file in the C<root> directory, it is
1494             possible to use symbolic names in order to avoid deadlocks.
1495              
1496             Note that these methods are no-ops if C<global_lock> is set.
1497              
1498             =back
1499              
1500             =head2 File Access
1501              
1502             =over 4
1503              
1504             =item openr $path
1505              
1506             =item openw $path
1507              
1508             =item opena $path
1509              
1510             =item open $mode, $path
1511              
1512             Open a file for reading, writing (clobbers) or appending, or with a custom mode
1513             for three arg open.
1514              
1515             Using C<openw> or C<openr> is reccomended if that's all you need, because it
1516             will not copy the file into the transaction work dir first.
1517              
1518             =item stat $path
1519              
1520             Runs L<File::stat/stat> on the physical path.
1521              
1522             =item old_stat $path
1523              
1524             Runs C<CORE::stat> on the physical path.
1525              
1526             =item exists $path
1527              
1528             =item is_deleted $path
1529              
1530             Whether a file exists or has been deleted in the current transaction.
1531              
1532             =item is_file $path
1533              
1534             Runs the C<-f> file test on the right physical path.
1535              
1536             =item is_dir $path
1537              
1538             Runs the C<-d> file test on the right physical path.
1539              
1540             =item unlink $path
1541              
1542             Deletes the file in the current transaction
1543              
1544             =item rename $from, $to
1545              
1546             Renames the file in the current transaction.
1547              
1548             Note that while this is a real C<rename> call in the txn work dir that is done
1549             on a copy, when comitting to the top level directory the original will be
1550             unlinked and the new file from the txn work dir will be renamed to the original.
1551              
1552             Hard links will B<NOT> be retained.
1553              
1554             =item readdir $path
1555              
1556             Merges the overlays of all the transactions and returns unsorted basenames.
1557              
1558             A path of C<""> can be used to list the root directory.
1559              
1560             =item list $path
1561              
1562             A DWIM version of C<readdir> that returns paths relative to C<root>, filters
1563             out C<.> and C<..> and sorts the output.
1564              
1565             A path of C<""> can be used to list the root directory.
1566              
1567             =item file_stream %args
1568              
1569             Creates a L<Directory::Transactional::Stream> for a recursive file listing.
1570              
1571             The C<dir> option can be used to specify a directory, defaulting to C<root>.
1572              
1573             =back
1574              
1575             =head2 Internal Methods
1576              
1577             These are documented so that they may provide insight into the inner workings
1578             of the module, but should not be considered part of the API.
1579              
1580             =over 4
1581              
1582             =item merge_overlay
1583              
1584             Merges one directory over another.
1585              
1586             =item recover
1587              
1588             Runs the directory state recovery code.
1589              
1590             See L</"TRANSACTIONAL PROTOCOL">
1591              
1592             =item online_recover
1593              
1594             Called to recover when the directory is already instantiated, by C<check_dirty>
1595             if a dirty state was found.
1596              
1597             =item check_dirty
1598              
1599             Check for transactions that crashed in mid commit
1600              
1601             =item set_dirty
1602              
1603             Called just before starting a commit.
1604              
1605             =item vivify_path $path
1606              
1607             Copies C<$path> as necessary from a parent transaction or the root directory in
1608             order to facilitate local work.
1609              
1610             Does not support hard or symbolic links (yet).
1611              
1612             =back
1613              
1614             =cut