* lib/am/lisp.am ($(am__ELCFILES)): Prevent races if the recover
authorAlexandre Duret-Lutz <adl@gnu.org>
Tue, 29 Mar 2005 18:46:55 +0000 (18:46 +0000)
committerAlexandre Duret-Lutz <adl@gnu.org>
Tue, 29 Mar 2005 18:46:55 +0000 (18:46 +0000)
rule is run with `make -j'.
* doc/automake.texi (Multiple Outputs): Adjust.
* tests/lisp6.test: Augment it.
* tests/lisp8.test: New file.
* tests/Makefile.am (TESTS): Add lisp8.test.
Suggested by Bruno Haible.

ChangeLog
doc/automake.texi
doc/stamp-vti
doc/version.texi
lib/am/lisp.am
tests/Makefile.am
tests/Makefile.in
tests/lisp6.test
tests/lisp8.test [new file with mode: 0755]

index 2031103..f98a5aa 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,13 @@
+2005-03-29  Alexandre Duret-Lutz  <adl@gnu.org>
+
+       * lib/am/lisp.am ($(am__ELCFILES)): Prevent races if the recover
+       rule is run with `make -j'.
+       * doc/automake.texi (Multiple Outputs): Adjust.
+       * tests/lisp6.test: Augment it.
+       * tests/lisp8.test: New file.
+       * tests/Makefile.am (TESTS): Add lisp8.test.
+       Suggested by Bruno Haible.
+
 2005-03-27  Alexandre Duret-Lutz  <adl@gnu.org>
 
        * doc/automake.texi: Use @:, @., @!, and @tie{} where appropriate.
index b9efd13..b9b8dae 100644 (file)
@@ -8895,38 +8895,47 @@ missing.  Here it is:
 data.c: data.foo
         foo data.foo
 data.h: data.c
+## Recover from the removal of $@@
         @@if test -f $@@; then :; else \
           rm -f data.c; \
           $(MAKE) $(AM_MAKEFLAGS) data.c; \
         fi
 @end example
 
-The above scales easily to more outputs and more inputs.  One of the
-output is picked up to serve as a witness of the run of the command,
-it depends upon all inputs, and all other outputs depend upon it.  For
-instance if @command{foo} should additionally read @file{data.bar} and
-also produce @file{data.w} and @file{data.x}, we would write:
+The above scheme can be extended to handle more outputs and more
+inputs.  One of the output is picked up to serve as a witness of the
+run of the command, it depends upon all inputs, and all other outputs
+depend upon it.  For instance if @command{foo} should additionally
+read @file{data.bar} and also produce @file{data.w} and @file{data.x},
+we would write:
 
 @example
 data.c: data.foo data.bar
         foo data.foo data.bar
 data.h data.w data.x: data.c
+## Recover from the removal of $@@
         @@if test -f $@@; then :; else \
           rm -f data.c; \
           $(MAKE) $(AM_MAKEFLAGS) data.c; \
         fi
 @end example
 
-There is still a minor problem with this setup.  @command{foo} outputs
-four files, but we do not know in which order these files are created.
-Suppose that @file{data.h} is created before @file{data.c}.  Then we
-have a weird situation.  The next time @command{make} is run,
-@file{data.h} will appear older than @file{data.c}, the second rule
-will be triggered, a shell will be started to execute the
-@samp{if@dots{}fi} command, but actually it will just execute the
-@code{then} branch, that is: nothing.  In other words, because the
-witness we selected is not the first file created by @command{foo},
-@command{make} will start a shell to do nothing each time it is run.
+However there are now two minor problems in this setup.  One is related
+to the timestamp ordering of @file{data.h}, @file{data.w},
+@file{data.x}, and @file{data.c}.  The other one is a race condition
+if a parallel @command{make} attempts to run multiple instance of the
+recover block at once.
+
+Let us deal with the first problem.  @command{foo} outputs four files,
+but we do not know in which order these files are created.  Suppose
+that @file{data.h} is created before @file{data.c}.  Then we have a
+weird situation.  The next time @command{make} is run, @file{data.h}
+will appear older than @file{data.c}, the second rule will be
+triggered, a shell will be started to execute the @samp{if@dots{}fi}
+command, but actually it will just execute the @code{then} branch,
+that is: nothing.  In other words, because the witness we selected is
+not the first file created by @command{foo}, @command{make} will start
+a shell to do nothing each time it is run.
 
 A simple riposte is to fix the timestamps when this happens.
 
@@ -8937,14 +8946,14 @@ data.h data.w data.x: data.c
         @@if test -f $@@; then \
           touch $@@; \
         else \
+## Recover from the removal of $@@
           rm -f data.c; \
           $(MAKE) $(AM_MAKEFLAGS) data.c; \
         fi
 @end example
 
-Another solution, not incompatible with the previous one, is to use a
-different and dedicated file as witness, rather than using any of
-@command{foo}'s outputs.
+Another solution is to use a different and dedicated file as witness,
+rather than using any of @command{foo}'s outputs.
 
 @example
 data.stamp: data.foo data.bar
@@ -8953,9 +8962,8 @@ data.stamp: data.foo data.bar
         foo data.foo data.bar
         @@mv -f data.tmp $@@
 data.c data.h data.w data.x: data.stamp
-        @@if test -f $@@; then \
-          touch $@@; \
-        else \
+## Recover from the removal of $@@
+        @@if test -f $@@; then :; else \
           rm -f data.stamp; \
           $(MAKE) $(AM_MAKEFLAGS) data.stamp; \
         fi
@@ -8966,6 +8974,44 @@ timestamp older than output files output by @command{foo}.  It is then
 renamed to @file{data.stamp} after @command{foo} has run, because we
 do not want to update @file{data.stamp} if @command{foo} fails.
 
+This solution still suffers from the second problem: the race
+condition in the recover rule.  If, after a successful build, a user
+erases @file{data.c} and @file{data.h}, and run @samp{make -j}, then
+@command{make} may start both recover rules in parallel.  If the two
+instances of the rule execute @samp{$(MAKE) $(AM_MAKEFLAGS)
+data.stamp} concurrently the build is likely to fail (for instance the
+two rules will create @file{data.tmp}, but only one can rename it).
+
+Admittedly, such weird situation does not happen during ordinary
+builds.  It occurs only when the build tree is mutilated.  Here
+@file{data.c} and @file{data.h} have been explicitly removed without
+also removing @file{data.stamp} and the other output files.
+@code{make clean; make} will always recover from these situations even
+with parallel makes, so you may decide that the recover rule is just
+an help to non-parallel make users and leave things as-is.  Fixing
+this requires some locking mechanism to ensure only one instance of
+the recover rule rebuild @code{data.stamp}.  One could imagine
+something along the following lines.
+
+@example
+data.c data.h data.w data.x: data.stamp
+## Recover from the removal of $@@
+        @@if test -f $@@; then :; else \
+          trap 'rm -rf data.lock data.stamp 1 2 13 15; \
+## mkdir is a portable test-and-set
+          if mkdir data.lock 2>/dev/null; then \
+## This code is being executed by the first process.
+            rm -f data.stamp; \
+            $(MAKE) $(AM_MAKEFLAGS) data.stamp; \
+          else \
+## This code is being executed by the follower processes.
+## Wait until the first process is done.
+            while test -d data.lock; do sleep 1; done; \
+## Succeed if and only if the first process succeeded.
+            test -f data.stamp; exit $$?; \
+        fi
+@end example
+
 Using a dedicated witness like this is very handy when the list of
 output files is not known beforehand.  As an illustration, consider
 the following rules to compile many @file{*.el} files into
@@ -8984,11 +9030,21 @@ elc-stamp: $(ELFILES)
         @@mv -f elc-temp $@@
 
 $(ELCFILES): elc-stamp
-        @@if test -f $@@; then \
-          touch $@@; \
-        else \
-          rm -f elc-stamp; \
-          $(MAKE) $(AM_MAKEFLAGS) elc-stamp; \
+## Recover from the removal of $@@
+        @@if test -f $@@; then :; else \
+          trap 'rm -rf elc-lock elc-stamp' 1 2 13 15; \
+          if mkdir elc-lock 2>/dev/null; then \
+## This code is being executed by the first process.
+            rm -f elc-stamp; \
+            $(MAKE) $(AM_MAKEFLAGS) elc-stamp; \
+            rmdir elc-lock; \
+          else \
+## This code is being executed by the follower processes.
+## Wait until the first process is done.
+            while test -d elc-lock; do sleep 1; done; \
+## Succeed if and only if the first process succeeded.
+            test -f elc-stamp; exit $$?; \
+          fi; \
         fi
 @end example
 
index b69d015..1c752fe 100644 (file)
@@ -1,4 +1,4 @@
-@set UPDATED 27 March 2005
+@set UPDATED 29 March 2005
 @set UPDATED-MONTH March 2005
 @set EDITION 1.9a
 @set VERSION 1.9a
index b69d015..1c752fe 100644 (file)
@@ -1,4 +1,4 @@
-@set UPDATED 27 March 2005
+@set UPDATED 29 March 2005
 @set UPDATED-MONTH March 2005
 @set EDITION 1.9a
 @set VERSION 1.9a
index 15b8bfe..dda1f59 100644 (file)
@@ -49,12 +49,30 @@ elc-stamp: $(LISP)
 $(am__ELCFILES): elc-stamp
 ## Recover from the removal of $@.
 ##
-## Make sure not to call `make elc-stamp' if emacs is not available,
-## because as all *.elc files appear as missing, a parallel make would
-## attempt to build elc-stamp several times.
+## Do not call `make elc-stamp' if emacs is not available, because it would
+## be useless.
        @if test "$(EMACS)" != no && test ! -f $@; then \
-         rm -f elc-stamp; \
-         $(MAKE) $(AM_MAKEFLAGS) elc-stamp; \
+## If `make -j' is used and more than one file has been erased, several
+## processes can execute this block.  We have to make sure that only
+## the first one will run `$(MAKE) $(AM_MAKEFLAGS) elc-stamp', and the
+## other ones will wait.
+##
+## There is a race here if only one child of make receive a signal.
+## In that case the build may fail.  We remove elc-stamp when we receive
+## a signal so we are sure the build will succeed the next time.
+         trap 'rm -rf elc-lock elc-stamp' 1 2 13 15; \
+         if mkdir elc-lock 2>/dev/null; then \
+## This code is being executed by the first process.
+           rm -f elc-stamp; \
+           $(MAKE) $(AM_MAKEFLAGS) elc-stamp; \
+           rmdir elc-lock; \
+         else \
+## This code is being executed by the follower processes.
+## Wait until the first process is done.
+           while test -d elc-lock; do sleep 1; done; \
+## Succeed if and only if the first process succeeded.
+           test -f elc-stamp; exit $$?; \
+         fi; \
        else : ; fi
 
 
index af4ac59..e7b1b25 100644 (file)
@@ -321,6 +321,7 @@ lisp4.test \
 lisp5.test \
 lisp6.test \
 lisp7.test \
+lisp8.test \
 listval.test \
 location.test \
 longline.test \
index 8e30dce..052987a 100644 (file)
@@ -441,6 +441,7 @@ lisp4.test \
 lisp5.test \
 lisp6.test \
 lisp7.test \
+lisp8.test \
 listval.test \
 location.test \
 longline.test \
index 0b415a6..724c052 100755 (executable)
@@ -1,5 +1,5 @@
 #! /bin/sh
-# Copyright (C) 2004  Free Software Foundation, Inc.
+# Copyright (C) 2004, 2005  Free Software Foundation, Inc.
 #
 # This file is part of GNU Automake.
 #
@@ -83,6 +83,14 @@ test -f am-two.elc
 test -f am-three.elc
 test -f elc-stamp
 
+# Let's mutilate the source tree, the check the recover rule.
+rm -f am-*.elc
+$MAKE
+test -f am-one.elc
+test -f am-two.elc
+test -f am-three.elc
+test -f elc-stamp
+
 $MAKE install
 test -f lisp/am-one.el
 test -f lisp/am-one.elc
diff --git a/tests/lisp8.test b/tests/lisp8.test
new file mode 100755 (executable)
index 0000000..650cdda
--- /dev/null
@@ -0,0 +1,65 @@
+#! /bin/sh
+# Copyright (C) 2005  Free Software Foundation, Inc.
+#
+# This file is part of GNU Automake.
+#
+# GNU Automake is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# GNU Automake is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Automake; see the file COPYING.  If not, write to
+# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+# Check the recover rule of lisp_LISP with parallel make.
+
+required='GNUmake emacs'
+. ./defs || exit 1
+
+set -e
+
+cat > Makefile.am << 'EOF'
+dist_lisp_LISP = am-one.el am-two.el am-three.el
+EOF
+
+cat >> configure.in << 'EOF'
+AM_PATH_LISPDIR
+AC_OUTPUT
+EOF
+
+echo "(require 'am-two)" > am-one.el
+echo "(require 'am-three) (provide 'am-two)" > am-two.el
+echo "(provide 'am-three)" > am-three.el
+
+$ACLOCAL
+$AUTOCONF
+$AUTOMAKE --add-missing
+./configure
+
+$MAKE -j >stdout
+
+cat stdout
+test 1 -eq `grep 'Warnings can be ignored' stdout | wc -l`
+
+test -f am-one.elc
+test -f am-two.elc
+test -f am-three.elc
+test -f elc-stamp
+
+rm -f am-*.elc
+
+$MAKE -j >stdout
+
+cat stdout
+test 1 -eq `grep 'Warnings can be ignored' stdout | wc -l`
+test -f am-one.elc
+test -f am-two.elc
+test -f am-three.elc
+test -f elc-stamp