Skip to content

Commit

Permalink
Item12477: Prepare to fix issues with .changes
Browse files Browse the repository at this point in the history
 - Sync up the recordChange routine between stores, based up on
   Crawford's rewrite for PlainFileStore
 - Extend the exclusive lock to cover the read/truncate code, so that
   there is not a window where the file might change before being
   rewritten.
 - Update the documentation for the recordChange function.
  • Loading branch information
gac410 committed Dec 3, 2014
1 parent af43c40 commit 0bc28d1
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 51 deletions.
48 changes: 38 additions & 10 deletions PlainFileStoreContrib/lib/Foswiki/Store/PlainFile.pm
Expand Up @@ -1135,27 +1135,38 @@ sub _writeMetaFile {
_saveFile( $mf, join( "\n", map { defined $_ ? $_ : '' } @_ ) );
}

# Record a change in the web history
=begin TML
---++ ObjectMethod recordChange(%args)
See Foswiki::Store for documentation
=cut

sub recordChange {
my $this = shift;
my %args = @_;
$args{more} ||= '';
ASSERT( $args{cuid} ) if DEBUG;
ASSERT( defined $args{more} ) if DEBUG;

# my ( $meta, $cUID, $rev, $more ) = @_;
# $more ||= '';
ASSERT( defined $args{_meta} ) if DEBUG;

my $file = _getData( $args{_meta}->web ) . '/.changes';
my @changes;
my $text = '';
my $t = time;
my $fh;

# If file exists, slurp in the contents, skipping records older than needed.
if ( -e $file ) {
my $cutoff = $t - $Foswiki::cfg{Store}{RememberChangesFor};
my $fh;
open( $fh, '<', $file )
or die "PlainFile: failed to read $file: $!";

# Open file in read/write mode, so we can hold lock across the truncate.
open( $fh, '+<', $file )
or die "PlainFile: failed to open $file: $!";
flock( $fh, LOCK_EX )
or die("PlainFile: failed to lock file $file: $!");
binmode($fh)
or die("PlainFile: failed to binmode $file: $!");
local $/ = "\n";
my $head = 1;
while ( my $line = <$fh> ) {
Expand All @@ -1167,14 +1178,31 @@ sub recordChange {
}
$text .= "$line\n";
}
close($fh);
seek( $fh, 0, 0 );
truncate( $fh, 0 );
}

# else create the file.
else {
_mkPathTo($file);
open( $fh, '>', $file )
or die("PlainFile: failed to create file $file: $!");
flock( $fh, LOCK_EX )
or die("PlainFile: failed to lock file $file: $!");
binmode($fh)
or die("PlainFile: failed to binmode $file: $!");
}

# Add the new change to the end of the file
$text .= $args{_meta}->topic || '.';
$text .= "\t$args{cuid}\t$t\t$args{revision}\t$args{more}\n";

_saveFile( $file, $text );
# Write out the contents. The lock is released on close
print $fh $text
or die("PlainFile: failed to print into $file: $!");
close($fh)
or die("PlainFile: failed to close file $file: $!");

}

# Read an entire file
Expand Down
97 changes: 60 additions & 37 deletions RCSStoreContrib/lib/Foswiki/Store/VC/Handler.pm
Expand Up @@ -1439,59 +1439,82 @@ sub hidePath {

=begin TML
---++ ObjectMethod recordChange($cUID, $rev, $more)
Record that the file changed, and who changed it
---++ ObjectMethod recordChange(%args)
cuid => $cUID,
revision => $rev,
verb => $verb,
newmeta => $topicObject,
newattachment => $name
See Foswiki::Store for documentation
=cut

sub recordChange {
my $this = shift;
my %args = ( 'more', '', @_ );
my %args = @_;
$args{more} ||= '';
ASSERT( $args{cuid} ) if DEBUG;
ASSERT( defined $args{_meta} ) if DEBUG;

#we do'nt record autoattach events in the .changes file, but other stores may be interested
return if ( $args{verb} eq 'autoattach' );

my $file = $Foswiki::cfg{DataDir} . '/' . $this->{web} . '/.changes';
my $file = _getData( $args{_meta}->web ) . '/.changes';
my @changes;
my $text = '';
my $t = time;
my $fh;

my @changes =
map {
my @row = split( /\t/, $_, 5 );
\@row
}
split( /[\r\n]+/, readFile( $this, $file ) );
# If file exists, slurp in the contents, skipping records older than needed.
if ( -e $file ) {
my $cutoff = $t - $Foswiki::cfg{Store}{RememberChangesFor};

# Open file in read/write mode, so we can hold lock across the truncate.
open( $fh, '+<', $file )
or die "RCSStore: failed to open $file: $!";
flock( $fh, LOCK_EX )
or die("RCSStore: failed to lock file $file: $!");
binmode($fh)
or die("RCSStore: failed to binmode $file: $!");
local $/ = "\n";
my $head = 1;
while ( my $line = <$fh> ) {
chomp($line);
if ($head) {
my @row = split( /\t/, $line, 4 );
next if ( $row[2] < $cutoff );
$head = 0;
}
$text .= "$line\n";
}
seek( $fh, 0, 0 );
truncate( $fh, 0 );
}

# Forget old stuff
my $cutoff = time() - $Foswiki::cfg{Store}{RememberChangesFor};
while ( scalar(@changes) && $changes[0]->[2] < $cutoff ) {
shift(@changes);
# else create the file.
else {
$this->mkPathTo($file);
open( $fh, '>', $file )
or die("RCSStore: failed to create file $file: $!");
flock( $fh, LOCK_EX )
or die("RCSStore: failed to lock file $file: $!");
binmode($fh)
or die("RCSStore: failed to binmode $file: $!");
}

# Add the new change to the end of the file
push(
@changes,
[
$this->{topic} || '.', $args{cuid},
time(), $args{revision},
$args{more}
]
);
$text .= $args{_meta}->topic || '.';
$text .= "\t$args{cuid}\t$t\t$args{revision}\t$args{more}\n";

# Doing this using a Schwartzian transform sometimes causes a mysterious
# undefined value, so had to unwrap it to a for loop.
for ( my $i = 0 ; $i <= $#changes ; $i++ ) {
$changes[$i] = join( "\t", @{ $changes[$i] } );
}
# Write out the contents. The lock is released on close
print $fh $text
or die("RCSStore: failed to print into $file: $!");
close($fh)
or die("RCSStore: failed to close file $file: $!");

my $text = join( "\n", @changes );
}

saveFile( $this, $file, $text );
# Get the absolute file path to a file in data. $what can be a Meta or
# a string path (e.g. a web name)
sub _getData {
my ($what) = @_;
my $path = $Foswiki::cfg{DataDir} . '/';
return $path . $what unless ref($what);
return $path . $what->web unless $what->topic;
return $path . $what->web . '/' . $what->topic;
}

=begin TML
Expand Down
22 changes: 22 additions & 0 deletions RCSStoreContrib/lib/Foswiki/Store/VC/Store.pm
Expand Up @@ -159,6 +159,7 @@ sub readTopic {

#this record should be ignored by the .changes file
$this->recordChange(
_meta => $topicObject,
_handler => $handler,
cuid => $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID,
revision => 0,
Expand Down Expand Up @@ -194,6 +195,7 @@ sub moveAttachment {
$handler->moveAttachment( $this, $newTopicObject->web,
$newTopicObject->topic, $newAttachment );
$this->recordChange(
_meta => $oldTopicObject,
_handler => $handler,
cuid => $cUID,
revision => 0,
Expand Down Expand Up @@ -221,6 +223,7 @@ sub copyAttachment {
$handler->copyAttachment( $this, $newTopicObject->web,
$newTopicObject->topic, $newAttachment );
$this->recordChange(
_meta => $newTopicObject,
_handler => $handler,
cuid => $cUID,
revision => 0,
Expand Down Expand Up @@ -253,6 +256,7 @@ sub moveTopic {

# Record that it was moved away
$this->recordChange(
_meta => $oldTopicObject,
_handler => $handler,

cuid => $cUID,
Expand All @@ -267,6 +271,7 @@ sub moveTopic {
$handler =
$this->getHandler( $newTopicObject->web, $newTopicObject->topic, '' );
$this->recordChange(
_meta => $newTopicObject,
_handler => $handler,

cuid => $cUID,
Expand All @@ -289,6 +294,7 @@ sub moveWeb {
# a useless .changes. See Item9278
$handler = $this->getHandler( $newWebObject->web );
$this->recordChange(
_meta => $oldWebObject,
_handler => $handler,

cuid => $cUID,
Expand Down Expand Up @@ -432,6 +438,7 @@ sub saveAttachment {

my $rev = $handler->getLatestRevisionID();
$this->recordChange(
_meta => $topicObject,
_handler => $handler,

cuid => $cUID,
Expand Down Expand Up @@ -476,6 +483,7 @@ sub saveTopic {

my $extra = $options->{minor} ? 'minor' : '';
$this->recordChange(
_meta => $topicObject,
_handler => $handler,

cuid => $cUID,
Expand Down Expand Up @@ -508,6 +516,7 @@ sub repRev {

my $rev = $handler->getLatestRevisionID();
$this->recordChange(
_meta => $topicObject,
_handler => $handler,
cuid => $cUID,
revision => $rev,
Expand Down Expand Up @@ -541,6 +550,7 @@ sub delRev {
$handler->restoreLatestRevision($cUID);

$this->recordChange(
_meta => $topicObject,
_handler => $handler,

cuid => $cUID,
Expand Down Expand Up @@ -623,6 +633,17 @@ sub eachChange {
return $handler->eachChange($time);
}

=begin TML
---++ ObjectMethod recordChange(%args)
Skip recording removal of a web. The directory containing
the web .changes file has been removed.
See Foswiki::Store for further documentation.
=cut

sub recordChange {
my ( $this, %args ) = @_;
ASSERT( $args{_handler} );
Expand Down Expand Up @@ -697,6 +718,7 @@ sub remove {
$more .= ': ' . $attachment;
}
$this->recordChange(
_meta => $topicObject,
_handler => $handler,

cuid => $cUID,
Expand Down
13 changes: 9 additions & 4 deletions core/lib/Foswiki/Store.pm
Expand Up @@ -726,18 +726,23 @@ This is a private method to be called only from the store internals,
but it can be used by $Foswiki::Cfg{Store}{ImplementationClasses} to
chain in to eveavesdrop on Store events
* meta - TopicObject of the item being changed - (new or old)
* cuid - who is making the change
* revision - the revision of the topic or attachment that the change appears in
* verb - the action - one of
* =update= - a web, topic or attachment has been modified
* =insert= - a web, topic or attachment is being inserted
* =remove= - a topic or attachment is being removed
* =autoattach= - special case of =insert= for autoattachments
* newmeta - Foswiki::Meta object for the new object (not remove)
* newattachment - attachment name (not remove)
* oldmeta - Foswiki::Meta object for the origin of a move (move, remove only)
* oldattachment - origin of move (move, remove only)
* more - descriptive text containing store-specific flags
* minor - Minor change not reported
* delRev - Remove the head revision
* repRev - Replace the head revision
* Provided but never used:
* newmeta - Foswiki::Meta object for the new object (not remove)
* newattachment - attachment name (not remove)
* oldmeta - Foswiki::Meta object for the origin of a move (move, remove only)
* oldattachment - origin of move (move, remove only)
=cut
Expand Down

0 comments on commit 0bc28d1

Please sign in to comment.