don't pass undef to bigfloat
[rersyncrecent.git] / lib / File / Rsync / Mirror / Recentfile.pm
blob4a29924e72d0eb2599faa5a8444b5f5649bc776c
1 package File::Rsync::Mirror::Recentfile;
3 # use warnings;
4 use strict;
6 =encoding utf-8
8 =head1 NAME
10 File::Rsync::Mirror::Recentfile - mirroring via rsync made efficient
12 =cut
14 my $HAVE = {};
15 for my $package (
16 "Data::Serializer",
17 "File::Rsync"
18 ) {
19 $HAVE->{$package} = eval qq{ require $package; };
21 use Config;
22 use File::Basename qw(basename dirname fileparse);
23 use File::Copy qw(cp);
24 use File::Path qw(mkpath);
25 use File::Rsync::Mirror::Recentfile::FakeBigFloat qw(:all);
26 use File::Temp;
27 use List::Util qw(first max min);
28 use Scalar::Util qw(reftype);
29 use Storable;
30 use Time::HiRes qw();
31 use YAML::Syck;
33 use version; our $VERSION = qv('0.0.1');
35 use constant MAX_INT => ~0>>1; # anything better?
36 use constant DEFAULT_PROTOCOL => 1;
38 # cf. interval_secs
39 my %seconds;
41 # maybe subclass if this mapping is bad?
42 my %serializers;
44 =head1 SYNOPSIS
46 B<!!!! PRE-ALPHA ALERT !!!!>
48 Nothing in here is believed to be stable, nothing yet intended for
49 public consumption. The plan is to provide scripts that act as
50 frontends for all the backend functionality. Option and method names
51 will very likely change.
53 For the rationale see the section BACKGROUND.
55 This is published only for developers of the (yet to be named)
56 script(s).
58 Writer (of a single file):
60 use File::Rsync::Mirror::Recentfile;
61 my $fr = File::Rsync::Mirror::Recentfile->new
63 interval => q(6h),
64 filenameroot => "RECENT",
65 comment => "These 'RECENT' files are part of a test of a new CPAN mirroring concept. Please ignore them for now.",
66 localroot => "/home/ftp/pub/PAUSE/authors/",
67 aggregator => [qw(1d 1W 1M 1Q 1Y Z)],
69 $rf->update("/home/ftp/pub/PAUSE/authors/id/A/AN/ANDK/CPAN-1.92_63.tar.gz","new");
71 Reader/mirrorer:
73 my $rf = File::Rsync::Mirror::Recentfile->new
75 filenameroot => "RECENT",
76 ignore_link_stat_errors => 1,
77 interval => q(6h),
78 localroot => "/home/ftp/pub/PAUSE/authors",
79 remote_dir => "",
80 remote_host => "pause.perl.org",
81 remote_module => "authors",
82 rsync_options => {
83 compress => 1,
84 'rsync-path' => '/usr/bin/rsync',
85 links => 1,
86 times => 1,
87 'omit-dir-times' => 1,
88 checksum => 1,
90 verbose => 1,
92 $rf->mirror;
94 Aggregator (usually the writer):
96 my $rf = File::Rsync::Mirror::Recentfile->new_from_file ( $file );
97 $rf->aggregate;
99 =head1 EXPORT
101 No exports.
103 =head1 CONSTRUCTORS / DESTRUCTOR
105 =head2 my $obj = CLASS->new(%hash)
107 Constructor. On every argument pair the key is a method name and the
108 value is an argument to that method name.
110 If a recentfile for this resource already exists, metadata that are
111 not defined by the constructor will be fetched from there as soon as
112 it is being read by recent_events().
114 =cut
116 sub new {
117 my($class, @args) = @_;
118 my $self = bless {}, $class;
119 while (@args) {
120 my($method,$arg) = splice @args, 0, 2;
121 $self->$method($arg);
123 unless (defined $self->protocol) {
124 $self->protocol(DEFAULT_PROTOCOL);
126 unless (defined $self->filenameroot) {
127 $self->filenameroot("RECENT");
129 unless (defined $self->serializer_suffix) {
130 $self->serializer_suffix(".yaml");
132 return $self;
135 =head2 my $obj = CLASS->new_from_file($file)
137 Constructor. $file is a I<recentfile>.
139 =cut
141 sub new_from_file {
142 my($class, $file) = @_;
143 my $self = bless {}, $class;
144 $self->_rfile($file);
145 #?# $self->lock;
146 my $serialized = do { open my $fh, $file or die "Could not open '$file': $!";
147 local $/;
148 <$fh>;
150 # XXX: we can skip this step when the metadata are sufficient, but
151 # we cannot parse the file without some magic stuff about
152 # serialized formats
153 while (-l $file) {
154 my($name,$path) = fileparse $file;
155 my $symlink = readlink $file;
156 if ($symlink =~ m|/|) {
157 die "FIXME: filenames containing '/' not supported, got $symlink";
159 $file = File::Spec->catfile ( $path, $symlink );
161 my($name,$path,$suffix) = fileparse $file, keys %serializers;
162 $self->serializer_suffix($suffix);
163 $self->localroot($path);
164 die "Could not determine file format from suffix" unless $suffix;
165 my $deserialized;
166 if ($suffix eq ".yaml") {
167 require YAML::Syck;
168 $deserialized = YAML::Syck::LoadFile($file);
169 } elsif ($HAVE->{"Data::Serializer"}) {
170 my $serializer = Data::Serializer->new
171 ( serializer => $serializers{$suffix} );
172 $deserialized = $serializer->raw_deserialize($serialized);
173 } else {
174 die "Data::Serializer not installed, cannot proceed with suffix '$suffix'";
176 while (my($k,$v) = each %{$deserialized->{meta}}) {
177 next if $k ne lc $k; # "Producers"
178 $self->$k($v);
180 unless (defined $self->protocol) {
181 $self->protocol(DEFAULT_PROTOCOL);
183 return $self;
186 =head2 DESTROY
188 A simple unlock.
190 =cut
191 sub DESTROY { shift->unlock }
193 =head1 ACCESSORS
195 =cut
197 my @accessors;
199 BEGIN {
200 @accessors = (
201 "_current_tempfile",
202 "_current_tempfile_fh",
203 "_delayed_operations",
204 "_done",
205 "_interval",
206 "_is_locked",
207 "_localroot",
208 "_merged",
209 "_pathdb",
210 "_remember_last_uptodate_call",
211 "_remote_dir",
212 "_remoteroot",
213 "_rfile",
214 "_rsync",
215 "_seeded",
216 "_uptodateness_ever_reached",
217 "_use_tempfile",
220 my @pod_lines =
221 split /\n/, <<'=cut'; push @accessors, grep {s/^=item\s+//} @pod_lines; }
223 =over 4
225 =item aggregator
227 A list of interval specs that tell the aggregator which I<recentfile>s
228 are to be produced.
230 =item canonize
232 The name of a method to canonize the path before rsyncing. Only
233 supported value is C<naive_path_normalize>. Defaults to that.
235 =item comment
237 A comment about this tree and setup.
239 =item dirtymark
241 A timestamp. The dirtymark is updated whenever an out of band change
242 on the origin server is performed that violates the protocol. Say,
243 they add or remove files in the middle somewhere. Slaves must react
244 with a devaluation of their C<done> structure which then leads to a
245 full re-sync of all files.
247 =item filenameroot
249 The (prefix of the) filename we use for this I<recentfile>. Defaults to
250 C<RECENT>. The string must not contain a directory separator.
252 =item have_mirrored
254 Timestamp remembering when we mirrored this recentfile the last time.
255 Only relevant for slaves.
257 =item ignore_link_stat_errors
259 If set to true, rsync errors are ignored that complain about link stat
260 errors. These seem to happen only when there are files missing at the
261 origin. In race conditions this can always happen, so it is
262 recommended to set this value to true.
264 =item is_slave
266 If set to true, this object will fetch a new recentfile from remote
267 when the timespan between the last mirror (see have_mirrored) and now
268 is too large (currently hardcoded arbitrary 420 seconds).
270 =item locktimeout
272 After how many seconds shall we die if we cannot lock a I<recentfile>?
273 Defaults to 600 seconds.
275 =item loopinterval
277 When mirror_loop is called, this accessor can specify how much time
278 every loop shall at least take. If the work of a loop is done before
279 that time has gone, sleeps for the rest of the time. Defaults to
280 arbitrary 42 seconds.
282 =item max_files_per_connection
284 Maximum number of files that are transferred on a single rsync call.
285 Setting it higher means higher performance at the price of holding
286 connections longer and potentially disturbing other users in the pool.
287 Defaults to the arbitrary value 42.
289 =item max_rsync_errors
291 When rsync operations encounter that many errors without any resetting
292 success in between, then we die. Defaults to unlimited. A value of
293 -1 means we run forever ignoring all rsync errors.
295 =item minmax
297 Hashref remembering when we read the recent_events from this file the
298 last time and what the timespan was.
300 =item protocol
302 When the RECENT file format changes, we increment the protocol. We try
303 to support older protocols in later releases.
305 =item remote_host
307 The host we are mirroring from. Leave empty for the local filesystem.
309 =item remote_module
311 Rsync servers have so called modules to separate directory trees from
312 each other. Put here the name of the module under which we are
313 mirroring. Leave empty for local filesystem.
315 =item rsync_options
317 Things like compress, links, times or checksums. Passed in to the
318 File::Rsync object used to run the mirror.
320 =item serializer_suffix
322 Mostly untested accessor. The only well tested format for
323 I<recentfile>s at the moment is YAML. It is used with YAML::Syck via
324 Data::Serializer. But in principle other formats are supported as
325 well. See section SERIALIZERS below.
327 =item sleep_per_connection
329 Sleep that many seconds (floating point OK) after every chunk of rsyncing
330 has finished. Defaults to arbitrary 0.42.
332 =item ttl
334 Time to live. Number of seconds after which this recentfile must be
335 fetched again from the origin server. Only relevant for slaves.
336 Defaults to arbitrary 24.2 seconds.
338 =item verbose
340 Boolean to turn on a bit verbosity.
342 =back
344 =cut
346 use accessors @accessors;
348 =head1 METHODS
350 =head2 (void) $obj->aggregate( %options )
352 Takes all intervals that are collected in the accessor called
353 aggregator. Sorts them by actual length of the interval.
354 Removes those that are shorter than our own interval. Then merges this
355 object into the next larger object. The merging continues upwards
356 as long as the next I<recentfile> is old enough to warrant a merge.
358 If a merge is warranted is decided according to the interval of the
359 previous interval so that larger files are not so often updated as
360 smaller ones. If $options{force} is true, all files get updated.
362 Here is an example to illustrate the behaviour. Given aggregators
364 1h 1d 1W 1M 1Q 1Y Z
366 then
368 1h updates 1d on every call to aggregate()
369 1d updates 1W earliest after 1h
370 1W updates 1M earliest after 1d
371 1M updates 1Q earliest after 1W
372 1Q updates 1Y earliest after 1M
373 1Y updates Z earliest after 1Q
375 Note that all but the smallest recentfile get updated at an arbitrary
376 rate and as such are quite useless on their own.
378 =cut
380 sub aggregate {
381 my($self, %option) = @_;
382 my @aggs = sort { $a->{secs} <=> $b->{secs} }
383 grep { $_->{secs} >= $self->interval_secs }
384 map { { interval => $_, secs => $self->interval_secs($_)} }
385 $self->interval, @{$self->aggregator || []};
386 $self->update;
387 $aggs[0]{object} = $self;
388 AGGREGATOR: for my $i (0..$#aggs-1) {
389 my $this = $aggs[$i]{object};
390 my $next = $this->_sparse_clone;
391 $next->interval($aggs[$i+1]{interval});
392 my $want_merge = 0;
393 if ($option{force} || $i == 0) {
394 $want_merge = 1;
395 } else {
396 my $next_rfile = $next->rfile;
397 if (-e $next_rfile) {
398 my $prev = $aggs[$i-1]{object};
399 local $^T = time;
400 my $next_age = 86400 * -M $next_rfile;
401 if ($next_age > $prev->interval_secs) {
402 $want_merge = 1;
404 } else {
405 $want_merge = 1;
408 if ($want_merge) {
409 $next->merge($this);
410 $aggs[$i+1]{object} = $next;
411 } else {
412 last AGGREGATOR;
417 # collect file size and mtime for all files of this aggregate
418 sub _debug_aggregate {
419 my($self) = @_;
420 my @aggs = sort { $a->{secs} <=> $b->{secs} }
421 map { { interval => $_, secs => $self->interval_secs($_)} }
422 $self->interval, @{$self->aggregator || []};
423 my $report = [];
424 for my $i (0..$#aggs) {
425 my $this = Storable::dclone $self;
426 $this->interval($aggs[$i]{interval});
427 my $rfile = $this->rfile;
428 my @stat = stat $rfile;
429 push @$report, {rfile => $rfile, size => $stat[7], mtime => $stat[9]};
431 $report;
434 # (void) $self->_assert_symlink()
435 sub _assert_symlink {
436 my($self) = @_;
437 my $recentrecentfile = File::Spec->catfile
439 $self->localroot,
440 sprintf
442 "%s.recent",
443 $self->filenameroot
446 if ($Config{d_symlink} eq "define") {
447 my $howto_create_symlink; # 0=no need; 1=straight symlink; 2=rename symlink
448 if (-l $recentrecentfile) {
449 my $found_symlink = readlink $recentrecentfile;
450 if ($found_symlink eq $self->rfilename) {
451 return;
452 } else {
453 $howto_create_symlink = 2;
455 } else {
456 $howto_create_symlink = 1;
458 if (1 == $howto_create_symlink) {
459 symlink $self->rfilename, $recentrecentfile or die "Could not create symlink '$recentrecentfile': $!"
460 } else {
461 unlink "$recentrecentfile.$$"; # may fail
462 symlink $self->rfilename, "$recentrecentfile.$$" or die "Could not create symlink '$recentrecentfile.$$': $!";
463 rename "$recentrecentfile.$$", $recentrecentfile or die "Could not rename '$recentrecentfile.$$' to $recentrecentfile: $!";
465 } else {
466 warn "Warning: symlinks not supported on this system, doing a copy instead\n";
467 unlink "$recentrecentfile.$$"; # may fail
468 cp $self->rfilename, "$recentrecentfile.$$" or die "Could not copy to '$recentrecentfile.$$': $!";
469 rename "$recentrecentfile.$$", $recentrecentfile or die "Could not rename '$recentrecentfile.$$' to $recentrecentfile: $!";
473 =head2 $hashref = $obj->delayed_operations
475 A hash of hashes containing unlink and rmdir operations which had to
476 wait until the recentfile got unhidden in order to not confuse
477 downstream mirrors (in case we have some).
479 =cut
481 sub delayed_operations {
482 my($self) = @_;
483 my $x = $self->_delayed_operations;
484 unless (defined $x) {
485 $x = {
486 unlink => {},
487 rmdir => {},
489 $self->_delayed_operations ($x);
491 return $x;
494 =head2 $done = $obj->done
496 $done is a reference to a File::Rsync::Mirror::Recentfile::Done object
497 that keeps track of rsync activities. Only needed and used when we are
498 a mirroring slave.
500 =cut
502 sub done {
503 my($self) = @_;
504 my $done = $self->_done;
505 if (!$done) {
506 require File::Rsync::Mirror::Recentfile::Done;
507 $done = File::Rsync::Mirror::Recentfile::Done->new();
508 $done->_rfinterval ($self->interval);
509 $self->_done ( $done );
511 return $done;
514 =head2 $tempfilename = $obj->get_remote_recentfile_as_tempfile ()
516 Stores the remote I<recentfile> locally as a tempfile. The caller is
517 responsible to remove the file after use.
519 Note: if you're intending to act as an rsync server for other slaves,
520 then you must prefer this method to fetch that file with
521 get_remotefile(). Otherwise downstream mirrors would expect you to
522 already have mirrored all the files that are in the I<recentfile>
523 before you have them mirrored.
525 =cut
527 sub get_remote_recentfile_as_tempfile {
528 my($self) = @_;
529 mkpath $self->localroot;
530 my $fh;
531 my $trfilename;
532 if ( $self->_use_tempfile() ) {
533 return $self->_current_tempfile if ! $self->ttl_reached;
534 $fh = $self->_current_tempfile_fh;
535 $trfilename = $self->rfilename;
536 } else {
537 $trfilename = $self->rfilename;
540 my $dst;
541 if ($fh) {
542 $dst = $self->_current_tempfile;
543 } else {
544 $fh = $self->_get_remote_rat_provide_tempfile_object ($trfilename);
545 $dst = $fh->filename;
546 $self->_current_tempfile ($dst);
547 my $rfile = eval { $self->rfile; }; # may fail (RECENT.recent has no rfile)
548 if (defined $rfile && -e $rfile) {
549 # saving on bandwidth. Might need to be configurable
550 # $self->bandwidth_is_cheap?
551 cp $rfile, $dst or die "Could not copy '$rfile' to '$dst': $!"
554 my $src = join ("/",
555 $self->remoteroot,
556 $trfilename,
558 if ($self->verbose) {
559 my $doing = -e $dst ? "Sync" : "Get";
560 my $display_dst = join "/", "...", basename(dirname($dst)), basename($dst);
561 printf STDERR
563 "%-4s %d (1/1/%s) temp %s ... ",
564 $doing,
565 time,
566 $self->interval,
567 $display_dst,
570 my $gaveup = 0;
571 my $retried = 0;
572 while (!$self->rsync->exec(
573 src => $src,
574 dst => $dst,
575 )) {
576 $self->register_rsync_error ($self->rsync->err);
577 if (++$retried >= 3) {
578 warn "XXX giving up";
579 $gaveup = 1;
580 last;
583 if ($gaveup) {
584 printf STDERR "Warning: gave up mirroring %s, will try again later", $self->interval;
585 } else {
586 $self->_refresh_internals ($dst);
587 $self->have_mirrored (Time::HiRes::time);
588 $self->un_register_rsync_error ();
590 if ($self->verbose) {
591 print STDERR "DONE\n";
593 my $mode = 0644;
594 chmod $mode, $dst or die "Could not chmod $mode '$dst': $!";
595 return $dst;
598 sub _get_remote_rat_provide_tempfile_object {
599 my($self, $trfilename) = @_;
600 my $fh = File::Temp->new
601 (TEMPLATE => sprintf(".FRMRecent-%s-XXXX",
602 $trfilename,
604 DIR => $self->localroot,
605 SUFFIX => $self->serializer_suffix,
606 UNLINK => $self->_use_tempfile,
608 if ($self->_use_tempfile) {
609 $self->_current_tempfile_fh ($fh); # delay self destruction
611 return $fh;
614 =head2 $localpath = $obj->get_remotefile ( $relative_path )
616 Rsyncs one single remote file to local filesystem.
618 Note: no locking is done on this file. Any number of processes may
619 mirror this object.
621 Note II: do not use for recentfiles. If you are a cascading
622 slave/server combination, it would confuse other slaves. They would
623 expect the contents of these recentfiles to be available. Use
624 get_remote_recentfile_as_tempfile() instead.
626 =cut
628 sub get_remotefile {
629 my($self, $path) = @_;
630 my $dst = File::Spec->catfile($self->localroot, $path);
631 mkpath dirname $dst;
632 if ($self->verbose) {
633 my $doing = -e $dst ? "Sync" : "Get";
634 printf STDERR
636 "%-4s %d (1/1/%s) %s ... ",
637 $doing,
638 time,
639 $self->interval,
640 $path,
643 while (!$self->rsync->exec(
644 src => join("/",
645 $self->remoteroot,
646 $path),
647 dst => $dst,
648 )) {
649 $self->register_rsync_error ($self->rsync->err);
651 $self->un_register_rsync_error ();
652 if ($self->verbose) {
653 print STDERR "DONE\n";
655 return $dst;
658 =head2 $obj->interval ( $interval_spec )
660 Get/set accessor. $interval_spec is a string and described below in
661 the section INTERVAL SPEC.
663 =cut
665 sub interval {
666 my ($self, $interval) = @_;
667 if (@_ >= 2) {
668 $self->_interval($interval);
669 $self->_rfile(undef);
671 $interval = $self->_interval;
672 unless (defined $interval) {
673 # do not ask the $self too much, it recurses!
674 require Carp;
675 Carp::confess("Alert: interval undefined for '".$self."'. Cannot continue.");
677 return $interval;
680 =head2 $secs = $obj->interval_secs ( $interval_spec )
682 $interval_spec is described below in the section INTERVAL SPEC. If
683 empty defaults to the inherent interval for this object.
685 =cut
687 sub interval_secs {
688 my ($self, $interval) = @_;
689 $interval ||= $self->interval;
690 unless (defined $interval) {
691 die "interval_secs() called without argument on an object without a declared one";
693 my ($n,$t) = $interval =~ /^(\d*)([smhdWMQYZ]$)/ or
694 die "Could not determine seconds from interval[$interval]";
695 if ($interval eq "Z") {
696 return MAX_INT;
697 } elsif (exists $seconds{$t} and $n =~ /^\d+$/) {
698 return $seconds{$t}*$n;
699 } else {
700 die "Invalid interval specification: n[$n]t[$t]";
704 =head2 $obj->localroot ( $localroot )
706 Get/set accessor. The local root of the tree.
708 =cut
710 sub localroot {
711 my ($self, $localroot) = @_;
712 if (@_ >= 2) {
713 $self->_localroot($localroot);
714 $self->_rfile(undef);
716 $localroot = $self->_localroot;
719 =head2 $ret = $obj->local_path($path_found_in_recentfile)
721 Combines the path to our local mirror and the path of an object found
722 in this I<recentfile>. In other words: the target of a mirror operation.
724 Implementation note: We split on slashes and then use
725 File::Spec::catfile to adjust to the local operating system.
727 =cut
729 sub local_path {
730 my($self,$path) = @_;
731 unless (defined $path) {
732 # seems like a degenerated case
733 return $self->localroot;
735 my @p = split m|/|, $path;
736 File::Spec->catfile($self->localroot,@p);
739 =head2 (void) $obj->lock
741 Locking is implemented with an C<mkdir> on a locking directory
742 (C<.lock> appended to $rfile).
744 =cut
746 sub lock {
747 my ($self) = @_;
748 # not using flock because it locks on filehandles instead of
749 # old school ressources.
750 my $locked = $self->_is_locked and return;
751 my $rfile = $self->rfile;
752 # XXX need a way to allow breaking the lock
753 my $start = time;
754 my $locktimeout = $self->locktimeout || 600;
755 while (not mkdir "$rfile.lock") {
756 Time::HiRes::sleep 0.01;
757 if (time - $start > $locktimeout) {
758 die "Could not acquire lockdirectory '$rfile.lock': $!";
761 $self->_is_locked (1);
764 =head2 (void) $obj->merge ($other)
766 Bulk update of this object with another one. It's used to merge a
767 smaller and younger $other object into the current one. If this file
768 is a C<Z> file, then we do not merge in objects of type C<delete>. But
769 if we encounter an object of type delete we delete the corresponding
770 C<new> object if we have it.
772 If there is nothing to be merged, nothing is done.
774 =cut
776 sub merge {
777 my($self, $other) = @_;
778 $self->_merge_sanitycheck ( $other );
779 $other->lock;
780 my $other_recent = $other->recent_events || [];
781 # $DB::single++ if $other->interval_secs eq "2" and grep {$_->{epoch} eq "999.999"} @$other_recent;
782 $self->lock;
783 my $my_recent = $self->recent_events || [];
785 # calculate the target time span
786 my $myepoch = $my_recent->[0] ? $my_recent->[0]{epoch} : undef;
787 my $epoch = $other_recent->[0] ? $other_recent->[0]{epoch} : $myepoch;
788 my $oldest_allowed = 0;
789 my $something_done;
790 unless ($my_recent->[0]) {
791 # obstetrics
792 $something_done = 1;
794 if ($epoch) {
795 if (_bigfloatgt($other->dirtymark, $self->dirtymark||0)) {
796 $oldest_allowed = 0;
797 $something_done = 1;
798 } elsif (my $merged = $self->merged) {
799 my $secs = $self->interval_secs();
800 $oldest_allowed = min($epoch - $secs, $merged->{epoch}||0);
802 while (@$my_recent && _bigfloatlt($my_recent->[-1]{epoch}, $oldest_allowed)) {
803 pop @$my_recent;
804 $something_done = 1;
808 my %have_path;
809 my $other_recent_filtered = [];
810 for my $oev (@$other_recent) {
811 my $oevepoch = $oev->{epoch} || 0;
812 next if _bigfloatlt($oevepoch, $oldest_allowed);
813 my $path = $oev->{path};
814 next if $have_path{$path}++;
815 if ( $self->interval eq "Z"
816 and $oev->{type} eq "delete") {
817 # do nothing
818 } else {
819 if (!$myepoch || _bigfloatgt($oevepoch, $myepoch)) {
820 $something_done = 1;
822 push @$other_recent_filtered, { epoch => $oev->{epoch}, path => $path, type => $oev->{type} };
825 if ($something_done) {
826 $self->_merge_something_done ($other_recent_filtered, $my_recent, $other_recent, $other, \%have_path, $epoch);
828 $self->unlock;
829 $other->unlock;
832 sub _merge_something_done {
833 my($self, $other_recent_filtered, $my_recent, $other_recent, $other, $have_path, $epoch) = @_;
834 my $recent = [];
835 my $epoch_conflict = 0;
836 my $last_epoch;
837 ZIP: while (@$other_recent_filtered || @$my_recent) {
838 my $event;
839 if (!@$my_recent ||
840 @$other_recent_filtered && _bigfloatge($other_recent_filtered->[0]{epoch},$my_recent->[0]{epoch})) {
841 $event = shift @$other_recent_filtered;
842 } else {
843 $event = shift @$my_recent;
844 next ZIP if $have_path->{$event->{path}}++;
846 $epoch_conflict=1 if defined $last_epoch && $event->{epoch} eq $last_epoch;
847 $last_epoch = $event->{epoch};
848 push @$recent, $event;
850 if ($epoch_conflict) {
851 my %have_epoch;
852 for (my $i = $#$recent;$i>=0;$i--) {
853 my $epoch = $recent->[$i]{epoch};
854 if ($have_epoch{$epoch}++) {
855 while ($have_epoch{$epoch}) {
856 $epoch = _increase_a_bit($epoch);
858 $recent->[$i]{epoch} = $epoch;
859 $have_epoch{$epoch}++;
863 if (!$self->dirtymark || _bigfloatgt($other->dirtymark, $self->dirtymark)) {
864 $self->dirtymark ( $other->dirtymark );
866 $self->write_recent($recent);
867 $other->merged({
868 time => Time::HiRes::time, # not used anywhere
869 epoch => $recent->[0]{epoch},
870 into_interval => $self->interval, # not used anywhere
872 $other->write_recent($other_recent);
875 sub _merge_sanitycheck {
876 my($self, $other) = @_;
877 if ($self->interval_secs <= $other->interval_secs) {
878 die sprintf
880 "Alert: illegal merge operation of a bigger interval[%d] into a smaller[%d]",
881 $self->interval_secs,
882 $other->interval_secs,
887 =head2 merged
889 Hashref denoting when this recentfile has been merged into some other
890 at which epoch.
892 =cut
894 sub merged {
895 my($self, $set) = @_;
896 if (defined $set) {
897 $self->_merged ($set);
899 my $merged = $self->_merged;
900 my $into;
901 if ($merged and $into = $merged->{into_interval} and defined $self->_interval) {
902 # sanity checks
903 if ($into eq $self->interval) {
904 require Carp;
905 Carp::cluck(sprintf
907 "Warning: into_interval[%s] same as own interval[%s]. Danger ahead.",
908 $into,
909 $self->interval,
911 } elsif ($self->interval_secs($into) < $self->interval_secs) {
912 require Carp;
913 Carp::cluck(sprintf
915 "Warning: into_interval_secs[%s] smaller than own interval_secs[%s] on interval[%s]. Danger ahead.",
916 $self->interval_secs($into),
917 $self->interval_secs,
918 $self->interval,
922 $merged;
925 =head2 $hashref = $obj->meta_data
927 Returns the hashref of metadata that the server has to add to the
928 I<recentfile>.
930 =cut
932 sub meta_data {
933 my($self) = @_;
934 my $ret = $self->{meta};
935 for my $m (
936 "aggregator",
937 "canonize",
938 "comment",
939 "dirtymark",
940 "filenameroot",
941 "merged",
942 "interval",
943 "protocol",
944 "serializer_suffix",
946 my $v = $self->$m;
947 if (defined $v) {
948 $ret->{$m} = $v;
951 # XXX need to reset the Producer if I am a writer, keep it when I
952 # am a reader
953 $ret->{Producers} ||= {
954 __PACKAGE__, "$VERSION", # stringified it looks better
955 '$0', $0,
956 'time', Time::HiRes::time,
958 $ret->{dirtymark} ||= Time::HiRes::time;
959 return $ret;
962 =head2 $success = $obj->mirror ( %options )
964 Mirrors the files in this I<recentfile> as reported by
965 C<recent_events>. Options named C<after>, C<before>, C<max>, and
966 C<skip-deletes> are passed through to the L<recent_events> call. The
967 boolean option C<piecemeal>, if true, causes C<mirror> to only rsync
968 C<max_files_per_connection> and keep track of the rsynced files so
969 that future calls will rsync different files until all files are
970 brought to sync.
972 =cut
974 sub mirror {
975 my($self, %options) = @_;
976 my $trecentfile = $self->get_remote_recentfile_as_tempfile();
977 $self->_use_tempfile (1);
978 my %passthrough = map { ($_ => $options{$_}) } qw(before after max skip-deletes);
979 my ($recent_events) = $self->recent_events(%passthrough);
980 my(@error, @xcollector);
981 my $first_item = 0;
982 my $last_item = $#$recent_events;
983 my $done = $self->done;
984 my $pathdb = $self->_pathdb;
985 ITEM: for my $i ($first_item..$last_item) {
986 my $status = +{};
987 $self->_mirror_item
990 $recent_events,
991 $last_item,
992 $done,
993 $pathdb,
994 \@xcollector,
995 \%options,
996 $status,
997 \@error,
999 last if $i == $last_item;
1000 return if $status->{mustreturn};
1002 if (@xcollector) {
1003 my $success = eval { $self->_mirror_empty_xcollector (\@xcollector,$pathdb,$recent_events);};
1004 if (!$success || $@) {
1005 warn "Warning: Unknown error while mirroring: $@";
1006 push @error, $@;
1007 sleep 1;
1010 if ($self->verbose) {
1011 print STDERR "DONE\n";
1013 # once we've gone to the end we consider ourselve free of obligations
1014 $self->unseed;
1015 $self->_mirror_unhide_tempfile ($trecentfile);
1016 $self->_mirror_perform_delayed_ops;
1017 return !@error;
1020 sub _mirror_item {
1021 my($self,
1023 $recent_events,
1024 $last_item,
1025 $done,
1026 $pathdb,
1027 $xcollector,
1028 $options,
1029 $status,
1030 $error,
1031 ) = @_;
1032 my $recent_event = $recent_events->[$i];
1033 return if $done->covered ( $recent_event->{epoch} );
1034 if ($pathdb) {
1035 my $rec = $pathdb->{$recent_event->{path}};
1036 if ($rec && $rec->{recentepoch}) {
1037 if (_bigfloatgt
1038 ( $rec->{recentepoch}, $recent_event->{epoch} )){
1039 $done->register ($recent_events, [$i]);
1040 return;
1044 my $dst = $self->local_path($recent_event->{path});
1045 if ($recent_event->{type} eq "new"){
1046 $self->_mirror_item_new
1048 $dst,
1050 $last_item,
1051 $recent_events,
1052 $recent_event,
1053 $xcollector,
1054 $pathdb,
1055 $status,
1056 $error,
1057 $options,
1059 } elsif ($recent_event->{type} eq "delete") {
1060 my $activity;
1061 if ($options->{'skip-deletes'}) {
1062 $activity = "skipped";
1063 } else {
1064 if (! -e $dst) {
1065 $activity = "not_found";
1066 } elsif (-l $dst or not -d _) {
1067 $self->delayed_operations->{unlink}{$dst}++;
1068 $activity = "deleted";
1069 } else {
1070 $self->delayed_operations->{rmdir}{$dst}++;
1071 $activity = "deleted";
1074 $done->register ($recent_events, [$i]);
1075 if ($pathdb) {
1076 $self->_mirror_register_path($pathdb,[$recent_event],$activity);
1078 } else {
1079 warn "Warning: invalid upload type '$recent_event->{type}'";
1083 sub _mirror_item_new {
1084 my($self,
1085 $dst,
1087 $last_item,
1088 $recent_events,
1089 $recent_event,
1090 $xcollector,
1091 $pathdb,
1092 $status,
1093 $error,
1094 $options,
1095 ) = @_;
1096 if ($self->verbose) {
1097 my $doing = -e $dst ? "Sync" : "Get";
1098 printf STDERR
1100 "%-4s %d (%d/%d/%s) %s ... ",
1101 $doing,
1102 time,
1103 1+$i,
1104 1+$last_item,
1105 $self->interval,
1106 $recent_event->{path},
1109 my $max_files_per_connection = $self->max_files_per_connection || 42;
1110 my $success;
1111 if ($self->verbose) {
1112 print STDERR "\n";
1114 push @$xcollector, { rev => $recent_event, i => $i };
1115 if (@$xcollector >= $max_files_per_connection) {
1116 $success = eval {$self->_mirror_empty_xcollector ($xcollector,$pathdb,$recent_events);};
1117 my $sleep = $self->sleep_per_connection;
1118 $sleep = 0.42 unless defined $sleep;
1119 Time::HiRes::sleep $sleep;
1120 if ($options->{piecemeal}) {
1121 $status->{mustreturn} = 1;
1122 return;
1124 } else {
1125 return;
1127 if (!$success || $@) {
1128 warn "Warning: Error while mirroring: $@";
1129 push @$error, $@;
1130 sleep 1;
1132 if ($self->verbose) {
1133 print STDERR "DONE\n";
1137 sub _mirror_empty_xcollector {
1138 my($self,$xcoll,$pathdb,$recent_events) = @_;
1139 my $success = $self->mirror_path([map {$_->{rev}{path}} @$xcoll]);
1140 if ($pathdb) {
1141 $self->_mirror_register_path($pathdb,[map {$_->{rev}} @$xcoll],"rsync");
1143 $self->done->register($recent_events, [map {$_->{i}} @$xcoll]);
1144 @$xcoll = ();
1145 return $success;
1148 sub _mirror_register_path {
1149 my($self,$pathdb,$coll,$activity) = @_;
1150 my $time = time;
1151 for my $item (@$coll) {
1152 $pathdb->{$item->{path}} =
1154 recentepoch => $item->{epoch},
1155 ($activity."_on") => $time,
1160 sub _mirror_unhide_tempfile {
1161 my($self, $trecentfile) = @_;
1162 my $rfile = $self->rfile;
1163 if (rename $trecentfile, $rfile) {
1164 # warn "DEBUG: renamed '$trecentfile' to '$rfile'";
1165 } else {
1166 require Carp;
1167 Carp::confess("Could not rename '$trecentfile' to '$rfile': $!");
1169 $self->_use_tempfile (0);
1170 if (my $ctfh = $self->_current_tempfile_fh) {
1171 $ctfh->unlink_on_destroy (0);
1172 $self->_current_tempfile_fh (undef);
1176 sub _mirror_perform_delayed_ops {
1177 my($self) = @_;
1178 my $delayed = $self->delayed_operations;
1179 for my $dst (keys %{$delayed->{unlink}}) {
1180 unless (unlink $dst) {
1181 require Carp;
1182 Carp::cluck ( "Warning: Error while unlinking '$dst': $!" );
1184 delete $delayed->{unlink}{$dst};
1186 for my $dst (keys %{$delayed->{rmdir}}) {
1187 unless (rmdir $dst) {
1188 require Carp;
1189 Carp::cluck ( "Warning: Error on rmdir '$dst': $!" );
1191 delete $delayed->{rmdir}{$dst};
1195 =head2 (void) $obj->mirror_loop
1197 Run mirror in an endless loop. See the accessor C<loopinterval>. XXX
1198 What happens/should happen if we miss the interval during a single loop?
1200 =cut
1202 sub mirror_loop {
1203 my($self) = @_;
1204 my $iteration_start = time;
1206 my $Signal = 0;
1207 $SIG{INT} = sub { $Signal++ };
1208 my $loopinterval = $self->loopinterval || 42;
1209 my $after = -999999999;
1210 LOOP: while () {
1211 $self->mirror($after);
1212 last LOOP if $Signal;
1213 my $re = $self->recent_events;
1214 $after = $re->[0]{epoch};
1215 if ($self->verbose) {
1216 local $| = 1;
1217 print "($after)";
1219 if (time - $iteration_start < $loopinterval) {
1220 sleep $iteration_start + $loopinterval - time;
1222 if ($self->verbose) {
1223 local $| = 1;
1224 print "~";
1229 =head2 $success = $obj->mirror_path ( $arrref | $path )
1231 If the argument is a scalar it is treated as a path. The remote path
1232 is mirrored into the local copy. $path is the path found in the
1233 I<recentfile>, i.e. it is relative to the root directory of the
1234 mirror.
1236 If the argument is an array reference then all elements are treated as
1237 a path below the current tree and all are rsynced with a single
1238 command (and a single connection).
1240 =cut
1242 sub mirror_path {
1243 my($self,$path) = @_;
1244 # XXX simplify the two branches such that $path is treated as
1245 # [$path] maybe even demand the argument as an arrayref to
1246 # simplify docs and code. (rsync-over-recentfile-2.pl uses the
1247 # interface)
1248 if (ref $path and ref $path eq "ARRAY") {
1249 my $dst = $self->localroot;
1250 mkpath dirname $dst;
1251 my($fh) = File::Temp->new(TEMPLATE => sprintf(".%s-XXXX",
1252 lc $self->filenameroot,
1254 TMPDIR => 1,
1255 UNLINK => 0,
1257 for my $p (@$path) {
1258 print $fh $p, "\n";
1260 $fh->flush;
1261 $fh->unlink_on_destroy(1);
1262 my $gaveup = 0;
1263 my $retried = 0;
1264 while (!$self->rsync->exec
1266 src => join("/",
1267 $self->remoteroot,
1269 dst => $dst,
1270 'files-from' => $fh->filename,
1271 )) {
1272 my(@err) = $self->rsync->err;
1273 if ($self->ignore_link_stat_errors && "@err" =~ m{^ rsync: \s link_stat }x ) {
1274 if ($self->verbose) {
1275 warn "Info: ignoring link_stat error '@err'";
1277 return 1;
1279 $self->register_rsync_error (@err);
1280 if (++$retried >= 3) {
1281 warn "XXX giving up.";
1282 $gaveup = 1;
1283 last;
1286 unless ($gaveup) {
1287 $self->un_register_rsync_error ();
1289 } else {
1290 my $dst = $self->local_path($path);
1291 mkpath dirname $dst;
1292 while (!$self->rsync->exec
1294 src => join("/",
1295 $self->remoteroot,
1296 $path
1298 dst => $dst,
1299 )) {
1300 my(@err) = $self->rsync->err;
1301 if ($self->ignore_link_stat_errors && "@err" =~ m{^ rsync: \s link_stat }x ) {
1302 if ($self->verbose) {
1303 warn "Info: ignoring link_stat error '@err'";
1305 return 1;
1307 $self->register_rsync_error (@err);
1309 $self->un_register_rsync_error ();
1311 return 1;
1314 sub _my_current_rfile {
1315 my($self) = @_;
1316 my $rfile;
1317 if ($self->_use_tempfile) {
1318 $rfile = $self->_current_tempfile;
1319 } else {
1320 $rfile = $self->rfile;
1322 return $rfile;
1325 =head2 $path = $obj->naive_path_normalize ($path)
1327 Takes an absolute unix style path as argument and canonicalizes it to
1328 a shorter path if possible, removing things like double slashes or
1329 C</./> and removes references to C<../> directories to get a shorter
1330 unambiguos path. This is used to make the code easier that determines
1331 if a file passed to C<upgrade()> is indeed below our C<localroot>.
1333 =cut
1335 sub naive_path_normalize {
1336 my($self,$path) = @_;
1337 $path =~ s|/+|/|g;
1338 1 while $path =~ s|/[^/]+/\.\./|/|;
1339 $path =~ s|/$||;
1340 $path;
1343 =head2 $ret = $obj->read_recent_1 ( $data )
1345 Delegate of C<recent_events()> on protocol 1
1347 =cut
1349 sub read_recent_1 {
1350 my($self, $data) = @_;
1351 return $data->{recent};
1354 =head2 $array_ref = $obj->recent_events ( %options )
1356 Note: the code relies on the resource being written atomically. We
1357 cannot lock because we may have no write access. If the caller has
1358 write access (eg. aggregate() or update()), it has to care for any
1359 necessary locking and it MUST write atomically.
1361 If $options{after} is specified, only file events after this timestamp
1362 are returned.
1364 If $options{before} is specified, only file events before this
1365 timestamp are returned.
1367 IF $options{'skip-deletes'} is specified, no files-to-be-deleted will
1368 be returned.
1370 If $options{max} is specified only a maximum of this many events is
1371 returned.
1373 If $options{contains} is specified the value must be a hash reference
1374 containing a query. The query may contain the keys C<epoch>, C<path>,
1375 and C<type>. Each represents a condition that must be met. If there is
1376 more than one such key, the conditions are ANDed.
1378 If $options{info} is specified, it must be a hashref. This hashref
1379 will be filled with metadata about the unfiltered recent_events of
1380 this object, in key C<first> there is the first item, in key C<last>
1381 is the last.
1383 =cut
1385 sub recent_events {
1386 my ($self, %options) = @_;
1387 my $info = $options{info};
1388 if ($self->is_slave) {
1389 $self->get_remote_recentfile_as_tempfile;
1391 my $rfile_or_tempfile = $self->_my_current_rfile or return [];
1392 -e $rfile_or_tempfile or return [];
1393 my $suffix = $self->serializer_suffix;
1394 my ($data) = eval {
1395 $self->_try_deserialize
1397 $suffix,
1398 $rfile_or_tempfile,
1401 my $err = $@;
1402 if ($err or !$data) {
1403 return [];
1405 my $re;
1406 if (reftype $data eq 'ARRAY') { # protocol 0
1407 $re = $data;
1408 } else {
1409 $re = $self->_recent_events_protocol_x
1411 $data,
1412 $rfile_or_tempfile,
1415 return $re unless grep {defined $options{$_}} qw(after before max);
1416 $self->_recent_events_handle_options ($re, \%options);
1419 sub _recent_events_handle_options {
1420 my($self, $re, $options) = @_;
1421 my $last_item = $#$re;
1422 my $info = $options->{info};
1423 if ($info) {
1424 $info->{first} = $re->[0];
1425 $info->{last} = $re->[-1];
1427 if (defined $options->{after}) {
1428 if ($re->[0]{epoch} > $options->{after}) {
1429 if (
1430 my $f = first
1431 {$re->[$_]{epoch} <= $options->{after}}
1432 0..$#$re
1434 $last_item = $f-1;
1436 } else {
1437 $last_item = -1;
1440 my $first_item = 0;
1441 if (defined $options->{before}) {
1442 if ($re->[0]{epoch} > $options->{before}) {
1443 if (
1444 my $f = first
1445 {$re->[$_]{epoch} < $options->{before}}
1446 0..$last_item
1448 $first_item = $f;
1450 } else {
1451 $first_item = 0;
1454 if (0 != $first_item || -1 != $last_item) {
1455 @$re = splice @$re, $first_item, 1+$last_item-$first_item;
1457 if ($options->{'skip-deletes'}) {
1458 @$re = grep { $_->{type} ne "delete" } @$re;
1460 if (my $contopt = $options->{contains}) {
1461 my $seen_allowed = 0;
1462 for my $allow (qw(epoch path type)) {
1463 if (exists $contopt->{$allow}) {
1464 $seen_allowed++;
1465 my $v = $contopt->{$allow};
1466 @$re = grep { $_->{$allow} eq $v } @$re;
1469 if (keys %$contopt > $seen_allowed) {
1470 require Carp;
1471 Carp::confess
1472 (sprintf "unknown query: %s", join ", ", %$contopt);
1475 if ($options->{max} && @$re > $options->{max}) {
1476 @$re = splice @$re, 0, $options->{max};
1478 $re;
1481 sub _recent_events_protocol_x {
1482 my($self,
1483 $data,
1484 $rfile_or_tempfile,
1485 ) = @_;
1486 my $meth = sprintf "read_recent_%d", $data->{meta}{protocol};
1487 # we may be reading meta for the first time
1488 while (my($k,$v) = each %{$data->{meta}}) {
1489 next if $k ne lc $k; # "Producers"
1490 next if defined $self->$k;
1491 $self->$k($v);
1493 my $re = $self->$meth ($data);
1494 my @stat = stat $rfile_or_tempfile or die "Cannot stat '$rfile_or_tempfile': $!";
1495 my $minmax = { mtime => $stat[9] };
1496 if (@$re) {
1497 $minmax->{min} = $re->[-1]{epoch};
1498 $minmax->{max} = $re->[0]{epoch};
1500 $self->minmax ( $minmax );
1501 return $re;
1504 sub _try_deserialize {
1505 my($self,
1506 $suffix,
1507 $rfile_or_tempfile,
1508 ) = @_;
1509 if ($suffix eq ".yaml") {
1510 require YAML::Syck;
1511 YAML::Syck::LoadFile($rfile_or_tempfile);
1512 } elsif ($HAVE->{"Data::Serializer"}) {
1513 my $serializer = Data::Serializer->new
1514 ( serializer => $serializers{$suffix} );
1515 my $serialized = do
1517 open my $fh, $rfile_or_tempfile or die "Could not open: $!";
1518 local $/;
1519 <$fh>;
1521 $serializer->raw_deserialize($serialized);
1522 } else {
1523 die "Data::Serializer not installed, cannot proceed with suffix '$suffix'";
1527 sub _refresh_internals {
1528 my($self, $dst) = @_;
1529 my $class = ref $self;
1530 my $rfpeek = $class->new_from_file ($dst);
1531 for my $acc (qw(
1532 _merged
1533 minmax
1534 )) {
1535 $self->$acc ( $rfpeek->$acc );
1537 my $old_dirtymark = $self->dirtymark;
1538 my $new_dirtymark = $rfpeek->dirtymark;
1539 if ($old_dirtymark && $new_dirtymark && _bigfloatgt($new_dirtymark,$old_dirtymark)) {
1540 $self->done->reset;
1541 $self->dirtymark ( $new_dirtymark );
1542 $self->seed;
1546 =head2 $ret = $obj->rfilename
1548 Just the basename of our I<recentfile>, composed from C<filenameroot>,
1549 a dash, C<interval>, and C<serializer_suffix>. E.g. C<RECENT-6h.yaml>
1551 =cut
1553 sub rfilename {
1554 my($self) = @_;
1555 my $file = sprintf("%s-%s%s",
1556 $self->filenameroot,
1557 $self->interval,
1558 $self->serializer_suffix,
1560 return $file;
1563 =head2 $str = $self->remote_dir
1565 The directory we are mirroring from.
1567 =cut
1569 sub remote_dir {
1570 my($self, $set) = @_;
1571 if (defined $set) {
1572 $self->_remote_dir ($set);
1574 my $x = $self->_remote_dir;
1575 $self->is_slave (1);
1576 return $x;
1579 =head2 $str = $obj->remoteroot
1581 =head2 (void) $obj->remoteroot ( $set )
1583 Get/Set the composed prefix needed when rsyncing from a remote module.
1584 If remote_host, remote_module, and remote_dir are set, it is composed
1585 from these.
1587 =cut
1589 sub remoteroot {
1590 my($self, $set) = @_;
1591 if (defined $set) {
1592 $self->_remoteroot($set);
1594 my $remoteroot = $self->_remoteroot;
1595 unless (defined $remoteroot) {
1596 $remoteroot = sprintf
1598 "%s%s%s",
1599 defined $self->remote_host ? ($self->remote_host."::") : "",
1600 defined $self->remote_module ? ($self->remote_module."/") : "",
1601 defined $self->remote_dir ? $self->remote_dir : "",
1603 $self->_remoteroot($remoteroot);
1605 return $remoteroot;
1608 =head2 (void) $obj->resolve_recentfilename ( $recentfilename )
1610 Inverse method to L<rfilename>. $recentfilename is a plain filename of
1611 the pattern
1613 $filenameroot-$interval$serializer_suffix
1615 e.g.
1617 RECENT-1M.yaml
1619 This filename is split into its parts and the parts are fed to the
1620 object itself.
1622 =cut
1624 sub resolve_recentfilename {
1625 my($self, $rfname) = @_;
1626 my($splitter) = qr(^(.+)-([^-\.]+)(\.[^\.]+));
1627 if (my($f,$i,$s) = $rfname =~ $splitter) {
1628 $self->filenameroot ($f);
1629 $self->interval ($i);
1630 $self->serializer_suffix ($s);
1631 } else {
1632 die "Alert: cannot split '$rfname', doesn't match '$splitter'";
1634 return;
1637 =head2 my $rfile = $obj->rfile
1639 Returns the full path of the I<recentfile>
1641 =cut
1643 sub rfile {
1644 my($self) = @_;
1645 my $rfile = $self->_rfile;
1646 return $rfile if defined $rfile;
1647 $rfile = File::Spec->catfile
1648 ($self->localroot,
1649 $self->rfilename,
1651 $self->_rfile ($rfile);
1652 return $rfile;
1655 =head2 $rsync_obj = $obj->rsync
1657 The File::Rsync object that this object uses for communicating with an
1658 upstream server.
1660 =cut
1662 sub rsync {
1663 my($self) = @_;
1664 my $rsync = $self->_rsync;
1665 unless (defined $rsync) {
1666 my $rsync_options = $self->rsync_options || {};
1667 if ($HAVE->{"File::Rsync"}) {
1668 $rsync = File::Rsync->new($rsync_options);
1669 $self->_rsync($rsync);
1670 } else {
1671 die "File::Rsync required for rsync operations. Cannot continue";
1674 return $rsync;
1677 =head2 (void) $obj->register_rsync_error(@err)
1679 =head2 (void) $obj->un_register_rsync_error()
1681 Register_rsync_error is called whenever the File::Rsync object fails
1682 on an exec (say, connection doesn't succeed). It issues a warning and
1683 sleeps for an increasing amount of time. Un_register_rsync_error
1684 resets the error count. See also accessor C<max_rsync_errors>.
1686 =cut
1689 my $no_success_count = 0;
1690 my $no_success_time = 0;
1691 sub register_rsync_error {
1692 my($self, @err) = @_;
1693 chomp @err;
1694 $no_success_time = time;
1695 $no_success_count++;
1696 my $max_rsync_errors = $self->max_rsync_errors;
1697 $max_rsync_errors = MAX_INT unless defined $max_rsync_errors;
1698 if ($max_rsync_errors>=0 && $no_success_count >= $max_rsync_errors) {
1699 require Carp;
1700 Carp::confess
1702 sprintf
1704 "Alert: Error while rsyncing (%s): '%s', error count: %d, exiting now,",
1705 $self->interval,
1706 join(" ",@err),
1707 $no_success_count,
1710 my $sleep = 12 * $no_success_count;
1711 $sleep = 300 if $sleep > 300;
1712 require Carp;
1713 Carp::cluck
1714 (sprintf
1716 "Warning: %s, Error while rsyncing (%s): '%s', sleeping %d",
1717 scalar(localtime($no_success_time)),
1718 $self->interval,
1719 join(" ",@err),
1720 $sleep,
1722 sleep $sleep
1724 sub un_register_rsync_error {
1725 my($self) = @_;
1726 $no_success_time = 0;
1727 $no_success_count = 0;
1731 =head2 $clone = $obj->_sparse_clone
1733 Clones just as much from itself that it does not hurt. Experimental
1734 method.
1736 Note: what fits better: sparse or shallow? Other suggestions?
1738 =cut
1740 sub _sparse_clone {
1741 my($self) = @_;
1742 my $new = bless {}, ref $self;
1743 for my $m (qw(
1744 _interval
1745 _localroot
1746 _remoteroot
1747 _rfile
1748 _use_tempfile
1749 aggregator
1750 filenameroot
1751 is_slave
1752 max_files_per_connection
1753 protocol
1754 rsync_options
1755 serializer_suffix
1756 sleep_per_connection
1757 verbose
1758 )) {
1759 my $o = $self->$m;
1760 $o = Storable::dclone $o if ref $o;
1761 $new->$m($o);
1763 $new;
1766 =head2 $boolean = OBJ->ttl_reached ()
1768 =cut
1770 sub ttl_reached {
1771 my($self) = @_;
1772 my $have_mirrored = $self->have_mirrored || 0;
1773 my $now = Time::HiRes::time;
1774 my $ttl = $self->ttl;
1775 $ttl = 24.2 unless defined $ttl;
1776 if ($now > $have_mirrored + $ttl) {
1777 return 1;
1779 return 0;
1782 =head2 (void) $obj->unlock()
1784 Unlocking is implemented with an C<rmdir> on a locking directory
1785 (C<.lock> appended to $rfile).
1787 =cut
1789 sub unlock {
1790 my($self) = @_;
1791 return unless $self->_is_locked;
1792 my $rfile = $self->rfile;
1793 rmdir "$rfile.lock";
1794 $self->_is_locked (0);
1797 =head2 unseed
1799 Sets this recentfile in the state of not 'seeded'.
1801 =cut
1802 sub unseed {
1803 my($self) = @_;
1804 $self->seeded(0);
1807 =head2 $ret = $obj->update ($path, $type)
1809 =head2 $ret = $obj->update ($path, "new", $dirty_epoch)
1811 =head2 $ret = $obj->update ()
1813 Enter one file into the local I<recentfile>. $path is the (usually
1814 absolute) path. If the path is outside I<our> tree, then it is
1815 ignored.
1817 $type is one of C<new> or C<delete>.
1819 Events of type C<new> may set $dirty_epoch. $dirty_epoch is normally
1820 not used and the epoch is calculated by the update() routine itself
1821 based on current time. But if there is the demand to insert a
1822 not-so-current file into the dataset, then the caller sets
1823 $dirty_epoch. This causes the epoch of the registered event to become
1824 $dirty_epoch or -- if the exact value given is already taken -- a tiny
1825 bit more. As compensation the dirtymark of the whole dataset is set to
1826 the current epoch.
1828 The new file event is unshifted (or, if dirty_epoch is set, inserted
1829 at the place it belongs to, according to the rule to have a sequence
1830 of strictly decreasing timestamps) to the array of recent_events and
1831 the array is shortened to the length of the timespan allowed. This is
1832 usually the timespan specified by the interval of this recentfile but
1833 as long as this recentfile has not been merged to another one, the
1834 timespan may grow without bounds.
1836 The third form runs an update without inserting a new file. This may
1837 be disired to truncate a recentfile.
1839 =cut
1840 sub _epoch_monotonically_increasing {
1841 my($self,$epoch,$recent) = @_;
1842 return $epoch unless @$recent; # the first one goes unoffended
1843 if (_bigfloatgt("".$epoch,$recent->[0]{epoch})) {
1844 return $epoch;
1845 } else {
1846 return _increase_a_bit($recent->[0]{epoch});
1849 sub update {
1850 my($self,$path,$type,$dirty_epoch) = @_;
1851 if (defined $path or defined $type or defined $dirty_epoch) {
1852 die "update called without path argument" unless defined $path;
1853 die "update called without type argument" unless defined $type;
1854 die "update called with illegal type argument: $type" unless $type =~ /(new|delete)/;
1855 die "update called with \$type=$type and \$dirty_epoch=$dirty_epoch; ".
1856 "dirty_epoch only allowed with type=new" if $dirty_epoch and $type ne "new";
1857 my $canonmeth = $self->canonize;
1858 unless ($canonmeth) {
1859 $canonmeth = "naive_path_normalize";
1861 $path = $self->$canonmeth($path);
1863 my $lrd = $self->localroot;
1864 $self->lock;
1865 # you must calculate the time after having locked, of course
1866 my $now = Time::HiRes::time;
1867 my $interval = $self->interval;
1868 my $secs = $self->interval_secs();
1869 my $recent = $self->recent_events;
1871 my $epoch;
1872 if ($dirty_epoch) {
1873 $epoch = $dirty_epoch;
1874 } else {
1875 $epoch = $self->_epoch_monotonically_increasing($now,$recent);
1878 $recent ||= [];
1879 my $oldest_allowed = 0;
1880 my $merged = $self->merged;
1881 if ($merged->{epoch}) {
1882 my $virtualnow = max($now,$epoch);
1883 # for the lower bound could we need big math?
1884 $oldest_allowed = min($virtualnow - $secs, $merged->{epoch}, $epoch);
1885 } else {
1886 # as long as we are not merged at all, no limits!
1888 my $something_done = 0;
1889 TRUNCATE: while (@$recent) {
1890 # $DB::single++ unless defined $oldest_allowed;
1891 if (_bigfloatlt($recent->[-1]{epoch}, $oldest_allowed)) {
1892 pop @$recent;
1893 $something_done = 1;
1894 } else {
1895 last TRUNCATE;
1898 if (defined $path && $path =~ s|^\Q$lrd\E||) {
1899 $path =~ s|^/||;
1900 my $splicepos;
1901 # remove the older duplicates of this $path, irrespective of $type:
1902 if ($dirty_epoch) {
1903 my $ctx = $self->_update_with_dirty_epoch($path,$recent,$epoch);
1904 $recent = $ctx->{recent};
1905 $splicepos = $ctx->{splicepos};
1906 $epoch = $ctx->{epoch};
1907 my $dirtymark = $self->dirtymark;
1908 my $new_dm = $now;
1909 if (_bigfloatgt($epoch, $now)) {
1910 $new_dm = $epoch;
1912 $self->dirtymark($new_dm);
1913 my $merged = $self->merged;
1914 if (not defined $merged->{epoch} or _bigfloatlt($epoch,$merged->{epoch})) {
1915 $self->merged(+{});
1917 } else {
1918 $recent = [ grep { $_->{path} ne $path } @$recent ];
1919 $splicepos = 0;
1921 if (defined $splicepos) {
1922 splice @$recent, $splicepos, 0, { epoch => $epoch, path => $path, type => $type };
1924 $something_done = 1;
1927 $self->write_recent($recent) if $something_done;
1928 $self->_assert_symlink;
1929 $self->unlock;
1932 sub _update_with_dirty_epoch {
1933 my($self,$path,$recent,$epoch) = @_;
1934 my $splicepos;
1935 my $new_recent = [];
1936 if (grep { $_->{path} ne $path } @$recent) {
1937 my $cancel = 0;
1938 KNOWN_EVENT: for my $i (0..$#$recent) {
1939 if ($recent->[$i]{path} eq $path) {
1940 if ($recent->[$i]{epoch} eq $epoch) {
1941 # nothing to do
1942 $cancel = 1;
1943 last KNOWN_EVENT;
1945 } else {
1946 push @$new_recent, $recent->[$i];
1949 @$recent = @$new_recent unless $cancel;
1951 if (!exists $recent->[0] or _bigfloatgt($epoch,$recent->[0]{epoch})) {
1952 $splicepos = 0;
1953 } elsif (_bigfloatlt($epoch,$recent->[0]{epoch})) {
1954 $splicepos = @$recent;
1955 } else {
1956 RECENT: for my $i (0..$#$recent) {
1957 my $ev = $recent->[$i];
1958 if ($epoch eq $recent->[$i]{epoch}) {
1959 $epoch = _increase_a_bit($epoch, $i ? $recent->[$i-1]{epoch} : undef);
1961 if (_bigfloatgt($epoch,$recent->[$i]{epoch})) {
1962 $splicepos = $i;
1963 last RECENT;
1967 return {
1968 recent => $recent,
1969 splicepos => $splicepos,
1970 epoch => $epoch,
1974 =head2 seed
1976 Sets this recentfile in the state of 'seeded' which means it has to
1977 re-evaluate its uptodateness.
1979 =cut
1980 sub seed {
1981 my($self) = @_;
1982 $self->seeded(1);
1985 =head2 seeded
1987 Tells if the recentfile is in the state 'seeded'.
1989 =cut
1990 sub seeded {
1991 my($self, $set) = @_;
1992 if (defined $set) {
1993 $self->_seeded ($set);
1995 my $x = $self->_seeded;
1996 unless (defined $x) {
1997 $x = 0;
1998 $self->_seeded ($x);
2000 return $x;
2003 =head2 uptodate
2005 True if this object has mirrored the complete interval covered by the
2006 current recentfile.
2008 *** WIP ***
2010 =cut
2011 sub uptodate {
2012 my($self) = @_;
2013 my $uptodate;
2014 my $why;
2015 if ($self->_uptodateness_ever_reached and not $self->seeded) {
2016 $why = "saturated";
2017 $uptodate = 1;
2019 unless (defined $uptodate) {
2020 if ($self->ttl_reached){
2021 $why = "ttl_reached returned true, so we are not uptodate";
2022 $uptodate = 0 ;
2025 unless (defined $uptodate) {
2026 # look if recentfile has unchanged timestamp
2027 my $minmax = $self->minmax;
2028 if (exists $minmax->{mtime}) {
2029 my $rfile = $self->_my_current_rfile;
2030 my @stat = stat $rfile;
2031 my $mtime = $stat[9];
2032 if ($mtime > $minmax->{mtime}) {
2033 $why = "mtime[$mtime] of rfile[$rfile] > minmax/mtime[$minmax->{mtime}], so we are not uptodate";
2034 $uptodate = 0;
2035 } else {
2036 my $covered = $self->done->covered(@$minmax{qw(max min)});
2037 $why = "minmax covered[$covered], so we return that";
2038 $uptodate = $covered;
2042 unless (defined $uptodate) {
2043 $why = "fallthrough, so not uptodate";
2044 $uptodate = 0;
2046 if ($uptodate) {
2047 $self->_uptodateness_ever_reached(1);
2048 $self->unseed;
2050 my $remember =
2052 uptodate => $uptodate,
2053 why => $why,
2055 $self->_remember_last_uptodate_call($remember);
2056 return $uptodate;
2059 =head2 $obj->write_recent ($recent_files_arrayref)
2061 Writes a I<recentfile> based on the current reflection of the current
2062 state of the tree limited by the current interval.
2064 =cut
2066 sub write_recent {
2067 my ($self,$recent) = @_;
2068 die "write_recent called without argument" unless defined $recent;
2069 my $meth = sprintf "write_%d", $self->protocol;
2070 $self->$meth($recent);
2073 =head2 $obj->write_0 ($recent_files_arrayref)
2075 Delegate of C<write_recent()> on protocol 0
2077 =cut
2079 sub write_0 {
2080 my ($self,$recent) = @_;
2081 my $rfile = $self->rfile;
2082 YAML::Syck::DumpFile("$rfile.new",$recent);
2083 rename "$rfile.new", $rfile or die "Could not rename to '$rfile': $!";
2086 =head2 $obj->write_1 ($recent_files_arrayref)
2088 Delegate of C<write_recent()> on protocol 1
2090 =cut
2092 sub write_1 {
2093 my ($self,$recent) = @_;
2094 my $rfile = $self->rfile;
2095 my $suffix = $self->serializer_suffix;
2096 my $data = {
2097 meta => $self->meta_data,
2098 recent => $recent,
2100 my $serialized;
2101 if ($suffix eq ".yaml") {
2102 $serialized = YAML::Syck::Dump($data);
2103 } elsif ($HAVE->{"Data::Serializer"}) {
2104 my $serializer = Data::Serializer->new
2105 ( serializer => $serializers{$suffix} );
2106 $serialized = $serializer->raw_serialize($data);
2107 } else {
2108 die "Data::Serializer not installed, cannot proceed with suffix '$suffix'";
2110 open my $fh, ">", "$rfile.new" or die "Could not open >'$rfile.new': $!";
2111 print $fh $serialized;
2112 close $fh or die "Could not close '$rfile.new': $!";
2113 rename "$rfile.new", $rfile or die "Could not rename to '$rfile': $!";
2116 BEGIN {
2117 my @pod_lines =
2118 split /\n/, <<'=cut'; %serializers = map { eval } grep {s/^=item\s+C<<(.+)>>$/$1/} @pod_lines; }
2120 =head1 THE ARCHITECTURE OF A COLLECTION OF RECENTFILES
2122 The idea is that we want to have a short file that records really
2123 recent changes. So that a fresh mirror can be kept fresh as long as
2124 the connectivity is given. Then we want longer files that record the
2125 history before. So when the mirror falls behind the update period
2126 reflected in the shortest file, it can complement the list of recent
2127 file events with the next one. And if this is not long enough we want
2128 another one, again a bit longer. And we want one that completes the
2129 history back to the oldest file. The index files do contain the
2130 complete list of current files. The larger an index file is the less
2131 often it is updated. For practical reasons adjacent files will often
2132 overlap a bit but this is neither necessary nor enforced. That's the
2133 basic idea. The following example represents a tree that has a few
2134 updates every day:
2136 RECENT.recent -> RECENT-1h.yaml
2137 RECENT-6h.yaml
2138 RECENT-1d.yaml
2139 RECENT-1M.yaml
2140 RECENT-1W.yaml
2141 RECENT-1Q.yaml
2142 RECENT-1Y.yaml
2143 RECENT-Z.yaml
2145 The first file is the principal file, in so far it is the one that is
2146 written first after a filesystem change. Usually a symlink links to it
2147 with a filename that has the same filenameroot and the suffix
2148 C<.recent>. On systems that do not support symlinks there is a plain
2149 copy maintained instead.
2151 The last file, the Z file, contains the complementary files that are
2152 in none of the other files. It does never contain C<deletes>. Besides
2153 this it serves the role of a recovery mechanism or spill over pond.
2154 When things go wrong, it's a valuable controlling instance to hold the
2155 differences between the collection of limited interval files and the
2156 actual filesystem.
2158 =head2 A SINGLE RECENTFILE
2160 A I<recentfile> consists of a hash that has two keys: C<meta> and
2161 C<recent>. The C<meta> part has metadata and the C<recent> part has a
2162 list of fileobjects.
2164 =head2 THE META PART
2166 Here we find things that are pretty much self explaining: all
2167 lowercase attributes are accessors and as such explained somewhere
2168 above in this manpage. The uppercase attribute C<Producers> contains
2169 version information about involved software components. Nothing to
2170 worry about as I believe.
2172 =head2 THE RECENT PART
2174 This is the interesting part. Every entry refers to some filesystem
2175 change (with path, epoch, type). The epoch value is the point in time
2176 when some change was I<registered>. Do not be tempted to believe that
2177 the entry has a direct relation to something like modification time or
2178 change time on the filesystem level. The timestamp (I<epoch> element)
2179 is a floating point number and does practically never correspond
2180 exactly to the data recorded in the filesystem but rather to the time
2181 when some process succeeded to report to the I<recentfile> mechanism
2182 that something has changed. This is why many parts of the code refer
2183 to I<events>, because we merely try to record the I<event> of the
2184 discovery of a change, not the time of the change itself.
2186 All these entries can be devided into two types (denoted by the
2187 C<type> attribute): C<new>s and C<delete>s. Changes and creations are
2188 C<new>s. Deletes are C<delete>s.
2190 Another distinction is for objects with an epoch timestamp and others
2191 without. All files that were already existing on the filesystem before
2192 the I<recentfile> mechanism was installed, get recorded with a
2193 timestamp of zero.
2195 Besides an C<epoch> and a C<type> attribute we find a third one:
2196 C<path>. This path is relative to the directory we find the
2197 I<recentfile> in.
2199 The order of the entries in the I<recentfile> is by decreasing epoch
2200 attribute. These are either 0 or a unique floating point number. They
2201 are zero for events that were happening either before the time that
2202 the I<recentfile> mechanism was set up or were left undiscovered for a
2203 while and never handed over to update(). They are floating point
2204 numbers for all events being regularly handed to update(). And when
2205 the server has ntp running correctly, then the timestamps are
2206 actually decreasing and unique.
2208 =head1 CORRUPTION AND RECOVERY
2210 If the origin host breaks the promise to deliver consistent and
2211 complete I<recentfiles> then the way back to sanity shall be achieved
2212 through either the C<zloop> (still TBD) or traditional rsyncing
2213 between the hosts. For example, if the origin server forgets to deploy
2214 ntp and the clock on it jumps backwards some day, then this would
2215 probably go unnoticed for a while and many software components that
2216 rely on the time never running backwards will make wrong decisions.
2217 After some time this accident would probably still be found in one of
2218 the I<recentfiles> but would become meaningless as soon as a mirror
2219 has run through the sanitizing procedures. Same goes for origin hosts
2220 that forget to include or deliberately omit some files.
2222 =head1 SERIALIZERS
2224 The following suffixes are supported and trigger the use of these
2225 serializers:
2227 =over 4
2229 =item C<< ".yaml" => "YAML::Syck" >>
2231 =item C<< ".json" => "JSON" >>
2233 =item C<< ".sto" => "Storable" >>
2235 =item C<< ".dd" => "Data::Dumper" >>
2237 =back
2239 =cut
2241 BEGIN {
2242 my @pod_lines =
2243 split /\n/, <<'=cut'; %seconds = map { eval } grep {s/^=item\s+C<<(.+)>>$/$1/} @pod_lines; }
2245 =head1 INTERVAL SPEC
2247 An interval spec is a primitive way to express time spans. Normally it
2248 is composed from an integer and a letter.
2250 As a special case, a string that consists only of the single letter
2251 C<Z>, stands for unlimited time.
2253 The following letters express the specified number of seconds:
2255 =over 4
2257 =item C<< s => 1 >>
2259 =item C<< m => 60 >>
2261 =item C<< h => 60*60 >>
2263 =item C<< d => 60*60*24 >>
2265 =item C<< W => 60*60*24*7 >>
2267 =item C<< M => 60*60*24*30 >>
2269 =item C<< Q => 60*60*24*90 >>
2271 =item C<< Y => 60*60*24*365.25 >>
2273 =back
2275 =cut
2277 =head1 BACKGROUND
2279 This is about speeding up rsync operation on large trees to many
2280 places. Uses a small metadata cocktail and pull technology.
2282 =head2 NON-COMPETITORS
2284 File::Mirror JWU/File-Mirror/File-Mirror-0.10.tar.gz only local trees
2285 Mirror::YAML ADAMK/Mirror-YAML-0.03.tar.gz some sort of inner circle
2286 Net::DownloadMirror KNORR/Net-DownloadMirror-0.04.tar.gz FTP sites and stuff
2287 Net::MirrorDir KNORR/Net-MirrorDir-0.05.tar.gz dito
2288 Net::UploadMirror KNORR/Net-UploadMirror-0.06.tar.gz dito
2289 Pushmi::Mirror CLKAO/Pushmi-v1.0.0.tar.gz something SVK
2291 rsnapshot www.rsnapshot.org focus on backup
2292 csync www.csync.org more like unison
2293 multi-rsync sourceforge 167893 lan push to many
2295 =head2 COMPETITORS
2297 The problem to solve which clusters and ftp mirrors and otherwise
2298 replicated datasets like CPAN share: how to transfer only a minimum
2299 amount of data to determine the diff between two hosts.
2301 Normally it takes a long time to determine the diff itself before it
2302 can be transferred. Known solutions at the time of this writing are
2303 csync2, and rsync 3 batch mode.
2305 For many years the best solution was csync2 which solves the
2306 problem by maintining a sqlite database on both ends and talking a
2307 highly sophisticated protocol to quickly determine which files to send
2308 and which to delete at any given point in time. Csync2 is often
2309 inconvenient because the act of syncing demands quite an intimate
2310 relationship between the sender and the receiver and suffers when the
2311 number of syncing sites is large or connections are unreliable.
2313 Rsync 3 batch mode works around these problems by providing rsync-able
2314 batch files which allow receiving nodes to replay the history of the
2315 other nodes. This reduces the need to have an incestuous relation but
2316 it has the disadvantage that these batch files replicate the contents
2317 of the involved files. This seems inappropriate when the nodes already
2318 have a means of communicating over rsync.
2320 rersyncrecent solves this problem with a couple of (usually 2-10)
2321 index files which cover different overlapping time intervals. The
2322 master writes these files and the clients can construct the full tree
2323 from the information contained in them. The most recent index file
2324 usually covers the last seconds or minutes or hours of the tree and
2325 depending on the needs, slaves can rsync every few seconds and then
2326 bring their trees in full sync.
2328 The rersyncrecent mode was developed for CPAN but I hope it is a
2329 convenient and economic general purpose solution. I'm looking forward
2330 to see a CPAN backbone that is only a few seconds behind PAUSE. And
2331 then ... the first FUSE based CPAN filesystem anyone?
2333 =head1 SEE ALSO
2335 Barbie is providing a database of release dates. See
2336 http://use.perl.org/~barbie/journal/37907
2338 =head1 AUTHOR
2340 Andreas König
2342 =head1 BUGS
2344 Please report any bugs or feature requests through the web interface
2346 L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=File-Rsync-Mirror-Recentfile>.
2347 I will be notified, and then you'll automatically be notified of
2348 progress on your bug as I make changes.
2350 =head1 SUPPORT
2352 You can find documentation for this module with the perldoc command.
2354 perldoc File::Rsync::Mirror::Recentfile
2356 You can also look for information at:
2358 =over 4
2360 =item * RT: CPAN's request tracker
2362 L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=File-Rsync-Mirror-Recentfile>
2364 =item * AnnoCPAN: Annotated CPAN documentation
2366 L<http://annocpan.org/dist/File-Rsync-Mirror-Recentfile>
2368 =item * CPAN Ratings
2370 L<http://cpanratings.perl.org/d/File-Rsync-Mirror-Recentfile>
2372 =item * Search CPAN
2374 L<http://search.cpan.org/dist/File-Rsync-Mirror-Recentfile>
2376 =back
2379 =head1 ACKNOWLEDGEMENTS
2381 Thanks to RJBS for module-starter.
2383 =head1 COPYRIGHT & LICENSE
2385 Copyright 2008 Andreas König.
2387 This program is free software; you can redistribute it and/or modify it
2388 under the same terms as Perl itself.
2391 =cut
2393 1; # End of File::Rsync::Mirror::Recentfile
2395 # Local Variables:
2396 # mode: cperl
2397 # cperl-indent-level: 4
2398 # End: