[GHC] #7930: Nested STM Invariants are lost

GHC cvs-ghc at haskell.org
Sat May 25 16:28:33 CEST 2013


#7930: Nested STM Invariants are lost
----------------------------------------+-----------------------------------
Reporter:  fryguybob                    |          Owner:  fryguybob       
    Type:  bug                          |         Status:  new             
Priority:  normal                       |      Component:  Runtime System  
 Version:  7.6.3                        |       Keywords:  STM             
      Os:  Unknown/Multiple             |   Architecture:  Unknown/Multiple
 Failure:  Incorrect result at runtime  |      Blockedby:                  
Blocking:                               |        Related:                  
----------------------------------------+-----------------------------------

Comment(by fryguybob):

 Luite Stegeman and I have been investigating this further and while the
 fix for this issue
 should be straight forward (merging the {{{invariants_to_check}}} field
 when committing a
 nested transaction) we have found some additional issues with invariants.
 In the original
 paper [1] the text makes it mostly clear how invariants and {{{retry}}}
 interact:

   [D7] If an invariant evaluates to retry then the user transaction is
   aborted and re-executed (potentially after blocking until it is worth
   re-executing it).

 And

   [D8] If an invariant executes a check statement, then the new
   invariant is checked at that point, but is not retained by the system.

 The {{{check}}} mentioned in the paper is the {{{alwaysSucceeds}}} in
 {{{GHC.Conc.Sync}}} which is currently defined as:

 {{{
 -- | Low-level primitive on which always and alwaysSucceeds are built.
 -- checkInv differs form these in that (i) the invariant is not
 -- checked when checkInv is called, only at the end of this and
 -- subsequent transactions, (ii) the invariant failure is indicated
 -- by raising an exception.
 checkInv :: STM a -> STM ()
 checkInv (STM m) = STM (\s -> (check# m) s)

 -- | alwaysSucceeds adds a new invariant that must be true when passed
 -- to alwaysSucceeds, at the end of the current transaction, and at
 -- the end of every subsequent transaction.  If it fails at any
 -- of those points then the transaction violating it is aborted
 -- and the exception raised by the invariant is propagated.
 alwaysSucceeds :: STM a -> STM ()
 alwaysSucceeds i = do ( i >> retry ) `orElse` ( return () )
                       checkInv i

 -- | always is a variant of alwaysSucceeds in which the invariant is
 -- expressed as an STM Bool action that must return True.  Returning
 -- False or raising an exception are both treated as invariant failures.
 always :: STM Bool -> STM ()
 always i = alwaysSucceeds ( do v <- i
                                if (v) then return () else ( error
 "Transactional invariant violation" ) )
 }}}

 This definition attempts to reuse the {{{retry}}} mechanism to handle the
 rollback of the initial run of the invariant.  Lets consider when {{{i}}}
 evaluates to {{{retry}}}.  According to [D7] this {{{i}}} should cause a
 {{{retry}}} at the level of {{{alwaysSucceeds}}}.  But as we see with the
 code above it will be caught by the {{{orElse}}} as if the invariant
 succeeded.
 All is not lost as the invariant is added to the {{{invariants_to_check}}}
 queue by {{{check#}}} and eventually the {{{retry}}} happens at top-level.

 Not so fast!  There are several problems with this delay.

  1. The way [D7] is worded implies that any {{{retry}}} in an invariant is
     really a top-level {{{retry}}}.  I don't know if this is accidental or
     not, but if so, delay makes some sense.  If not the wrong thing
 happens
     when you have {{{alwaysSucceeds i `orElse` ...}}}.
  2. The delay does not make sense because there are other effects that can
     intervene before the transaction is committed!  Indeed, it could be
 that
     it will not hit the retry later on.  Or an exception could happen due
 to
     the very invariant property being checked (See example below).
  3. How does [D8] interact with [D7]?  If I have an {{{alwaysSucceeds}}}
     nested inside another and it does a {{{retry}}} this should be a
     top-level retry by [D7].  But it should not be retained, so it will
     never be seen by [D8].  Its parent will, however, be run which will
     have the effect of running the child again.  Its {{{retry}}} will
     be delayed to commit, but there is no commit.  We are checking an
     invariant!  Such a nested {{{retry}}} will never be seen (See other
     example below).

 An example that exhibits (2) is the following.  The exception
 should never happen as it is guarded by the {{{retry}}} in the
 first invariant check.

 {{{
 import Control.Concurrent
 import Control.Concurrent.STM
 import Control.Exception.Base

 test c = do
     y <- atomically $ do
         always $ do
             x <- readTVar c
             if x
               then return True
               else retry
         z <- readTVar c
         if z
           then return z
           else throwSTM (AssertionFailed "Fail") -- Should never happen

     print y

 main = do
     c <- newTVarIO False
     forkIO (test c)

     threadDelay 100000

     atomically (writeTVar c True)

     threadDelay 100000

     putStrLn "Done."
 }}}

 Running we get the output:

 {{{
 *Main> :main
 <interactive>: Fail
 Done.
 }}}

 While we would hope to get the output:

 {{{
 *Main> :main
 True
 Done.
 }}}

 We can exhibit problem (3) with the following program.  The idea
 here is to show an invariant nested within an invariant has different
 semantics then a single level.

 {{{
 import Control.Concurrent
 import Control.Concurrent.STM

 test c = do
     y <- atomically $ do
         always $ do
             always $ do
                 x <- readTVar c
                 if x
                   then return True
                   else retry -- Always retry when c is false.
             return True
         readTVar c  -- Should always be True.
     print y

 main = do
     c <- newTVarIO False
     forkIO (test c)

     threadDelay 100000

     atomically (writeTVar c True)

     threadDelay 100000

     putStrLn "Done."
 }}}

 Here we get:

 {{{
 *Main> :main
 False                    (immediately)
 Done.
 }}}

 When we would expect:

 {{{
 *Main> :main
 True                     (after slight delay)
 Done.
 }}}

 I only bring all this up in this ticket as I believe the oversight of the
 issue in this ticket
 may have been due to attempts to get [D8] right.  I will see if I can
 address this ticket directly
 without resolving these other issue with a fix that will work with an
 eventual resolution of
 these additional issues.  The new issues are harder to fix as it will
 require a change to
 `alwaysSucceeds` as we need to distinguish between a {{{retry}}} to
 rollback the
 effects and a {{{retry}}} with semantic meaning for the invariant.
 Additionally it will require
 some clarification to the semantics.  I'm no
 expert in reading semantics but it appears to me that there should be a
 CHECK3 that specifies
 what happens when the term transitions to a {{{retry}}} in the context of
 a {{{check}}}.  My reading
 of the paper is that the intent was for {{{check retry ==> retry}}}.  This
 I think clarifies
 all three points above with {{{retry}}} in check catchable it its initial
 run, but not at commit
 time, and no delay in seeing a {{{retry}}}.

 [1]: Harris, Tim, and Simon Peyton Jones. "Transactional memory with data
 invariants." First ACM SIGPLAN Workshop on Languages, Compilers, and
 Hardware
 Support for Transactional Computing (TRANSACT’06), Ottowa. 2006.

-- 
Ticket URL: <http://hackage.haskell.org/trac/ghc/ticket/7930#comment:1>
GHC <http://www.haskell.org/ghc/>
The Glasgow Haskell Compiler



More information about the ghc-tickets mailing list