On Mon, Jan 28, 2013 at 11:55 +0100, Iustin Pop <[email protected]> wrote:
> On Thu, Jan 24, 2013 at 02:06:46AM +0000, Dato Simó wrote:
> > Parse auto-repair tags to set each instance in one of ArHealthy, ArFailed,
> > or ArPendingRepair. The implementation tries to be well behaved when old
> > tags have been left behind, which future patches will still try _not_ to do.

> Hi,

> > +-- | Parse a tag into an 'AutoRepairData' record.
> > +--
> > +-- @Nothing@ is returned if the tag is not an auto-repair tag, or if it's
> > +-- malformed.
> > +parseInitTag :: String -> Maybe AutoRepairData
> > +parseInitTag tag =
> > +  let parsePending = do
> > +        subtag <- chompPrefix C.autoRepairTagPending tag
> > +        let [rtype, uuid, ts, jobs] = sepSplit ':' subtag

> This, and

> > +        makeArData rtype uuid ts jobs
> > +
> > +      parseResult = do
> > +        subtag <- chompPrefix C.autoRepairTagResult tag
> > +        let [rtype, uuid, ts, result, jobs] = sepSplit ':' subtag

> this seem very unsafe to me. While testing similar code in the Maybe
> monad on a test example with bad data, I get:

> runhaskell l.hs 
> l.hs: l.hs:4:17-31: Irrefutable pattern failed for pattern [a, b, c]

> Does this actually behave correctly with invalid tags?

No, you're right. This is way too brittle for an "erroneous input" condition.
How about a monadic wrapper for sepSplit? (If you think I better put it in
Utils.hs, just let me know.)

--- src/Ganeti/HTools/Program/Harep.hs
+++ src/Ganeti/HTools/Program/Harep.hs
@@ -85,12 +85,12 @@ parseInitTag :: String -> Maybe AutoRepairData
 parseInitTag tag =
   let parsePending = do
         subtag <- chompPrefix C.autoRepairTagPending tag
-        let [rtype, uuid, ts, jobs] = sepSplit ':' subtag
+        [rtype, uuid, ts, jobs] <- splitN 4 subtag
         makeArData rtype uuid ts jobs
 
       parseResult = do
         subtag <- chompPrefix C.autoRepairTagResult tag
-        let [rtype, uuid, ts, result, jobs] = sepSplit ':' subtag
+        [rtype, uuid, ts, result, jobs] <- splitN 5 subtag
         arData <- makeArData rtype uuid ts jobs
         result' <- autoRepairResultFromRaw result
         return arData { arResult = Just result' }
@@ -106,6 +106,12 @@ parseInitTag tag =
                               , arResult = Nothing
                               , arTag = tag
                               }
+
+      splitN n str = do
+        let parts = sepSplit ':' str
+        guard $ length parts == n
+        return parts
+
   in
    parsePending `mplus` parseResult

> > +        arData <- makeArData rtype uuid ts jobs
> > +        result' <- autoRepairResultFromRaw result
> > +        return arData { arResult = Just result' }
> > +
> > +      makeArData rtype uuid ts jobs = do
> > +        rtype' <- autoRepairTypeFromRaw rtype
> > +        ts' <- tryRead "auto-repair time" ts
> > +        jobs' <- mapM makeJobIdS $ sepSplit '+' jobs
> > +        return AutoRepairData { arType = rtype'
> > +                              , arUuid = uuid
> > +                              , arTime = TOD ts' 0
> > +                              , arJobs = jobs'
> > +                              , arResult = Nothing
> > +                              , arTag = tag
> > +                              }
> > +  in
> > +   mplus parsePending parseResult

> Can you use parsePending `mplus` parseResult please?

Done (see previous diff).

> > +-- | Return the 'AutoRepairData' element of an 'AutoRepairStatus' type.
> > +getArData :: AutoRepairStatus -> Maybe AutoRepairData
> > +getArData status =
> > +  case status of
> > +    ArHealthy (Just d) -> Just d
> > +    ArFailedRepair d   -> Just d
> > +    ArPendingRepair d  -> Just d
> > +    ArNeedsRepair d    -> Just d
> > +    _                  -> Nothing

> (fyi, you could also align the 'd's there)

Done:

getArData :: AutoRepairStatus -> Maybe AutoRepairData
getArData status =
  case status of
    ArHealthy (Just d) -> Just d
    ArFailedRepair  d  -> Just d
    ArPendingRepair d  -> Just d
    ArNeedsRepair   d  -> Just d
    _                  -> Nothing

> > +-- | Return a new list of tags to remove that includes @arTag@ if present.
> > +delCurTag :: InstanceData -> [String]
> > +delCurTag instData =
> > +  let arData = getArData $ arState instData
> > +      rmTags = tagsToRemove instData
> > +  in
> > +   case arData of
> > +     Just d  -> arTag d : rmTags
> > +     Nothing -> rmTags
> > +
> > +-- | Set the initial auto-repair state of an instance from its auto-repair 
> > tags.
> > +--
> > +-- The rules when there are multiple tags is:
> > +--
> > +--   * the earliest failure result always wins
> > +--
> > +--   * two or more pending repairs results in a fatal error
> > +--
> > +--   * a pending result from id X and a success result from id Y result in 
> > error
> > +--     if Y is newer than X
> > +--
> > +--   * if there are no pending repairs, the newest success result wins,
> > +--     otherwise the pending result is used.
> > +setInitialState :: Instance.Instance -> InstanceData
> > +setInitialState inst =
> > +  let arData = mapMaybe parseInitTag $ Instance.allTags inst
> > +      -- Group all the AutoRepairData records by id (i.e. by repair task), 
> > and
> > +      -- present them from oldest to newest.
> > +      arData' = sortBy (comparing arUuid) arData
> > +      arGroups = groupBy ((==) `on` arUuid) arData'
> > +      arGroups' = sortBy (comparing $ minimum . map arTime) arGroups
> > +  in
> > +   foldl arStatusCmp (InstanceData inst (ArHealthy Nothing) []) arGroups'
> > +-- | Update the initial status of an instance with new repair task tags.
> > +--
> > +-- This function gets called once per repair group in an instance's tag, 
> > and it
> > +-- determines whether to set the status of the instance according to this 
> > new
> > +-- group, or to keep the existing state. See the documentation for
> > +-- 'setInitialState' for the rules to be followed when determining this.
> > +arStatusCmp :: InstanceData -> [AutoRepairData] -> InstanceData
> > +arStatusCmp instData arData =
> > +  let curSt = arState instData
> > +      arData' = sortBy (comparing keyfn) arData
> > +      keyfn d = (arResult d, arTime d)
> > +      newData = last arData'
> > +      newSt = case arResult newData of
> > +                Just ArSuccess -> ArHealthy $ Just newData
> > +                Just ArEnoperm -> ArHealthy $ Just newData
> > +                Just ArFailure -> ArFailedRepair newData
> > +                Nothing        -> ArPendingRepair newData
> > +  in
> > +   case curSt of
> > +     ArFailedRepair _ -> instData  -- Always keep the earliest failure.
> > +     ArHealthy _      -> instData { arState = newSt
> > +                                  , tagsToRemove = delCurTag instData
> > +                                  }
> > +     ArPendingRepair d -> error (
> > +       "An unfinished repair was found in instance " ++
> > +       Instance.name (arInstance instData) ++ ": found tag " ++
> > +       show (arTag newData) ++ ", but older pending tag " ++
> > +       show (arTag d) ++ "exists.")

> Please, never ever use error. Put this into a proper error monad if you
> need to signal failure, but don't use exceptions to signal errors.

Ack, sorry about that. I've converted the code to use Result, see below. I'm a
bit on sure on how I handled back the result in main, so if there's another way
to better do it, please let me know.

Interdiff:

--- src/Ganeti/HTools/Program/Harep.hs
+++ src/Ganeti/HTools/Program/Harep.hs
@@ -160,7 +160,7 @@ delCurTag instData =
 --
 --   * if there are no pending repairs, the newest success result wins,
 --     otherwise the pending result is used.
-setInitialState :: Instance.Instance -> InstanceData
+setInitialState :: Instance.Instance -> Result InstanceData
 setInitialState inst =
   let arData = mapMaybe parseInitTag $ Instance.allTags inst
       -- Group all the AutoRepairData records by id (i.e. by repair task), and
@@ -169,7 +169,7 @@ setInitialState inst =
       arGroups = groupBy ((==) `on` arUuid) arData'
       arGroups' = sortBy (comparing $ minimum . map arTime) arGroups
   in
-   foldl arStatusCmp (InstanceData inst (ArHealthy Nothing) []) arGroups'
+   foldM arStatusCmp (InstanceData inst (ArHealthy Nothing) []) arGroups'
 
 -- | Update the initial status of an instance with new repair task tags.
 --
@@ -177,7 +177,7 @@ setInitialState inst =
 -- determines whether to set the status of the instance according to this new
 -- group, or to keep the existing state. See the documentation for
 -- 'setInitialState' for the rules to be followed when determining this.
-arStatusCmp :: InstanceData -> [AutoRepairData] -> InstanceData
+arStatusCmp :: InstanceData -> [AutoRepairData] -> Result InstanceData
 arStatusCmp instData arData =
   let curSt = arState instData
       arData' = sortBy (comparing keyfn) arData
@@ -190,18 +190,18 @@ arStatusCmp instData arData =
                 Nothing        -> ArPendingRepair newData
   in
    case curSt of
-     ArFailedRepair _ -> instData  -- Always keep the earliest failure.
-     ArHealthy _      -> instData { arState = newSt
-                                  , tagsToRemove = delCurTag instData
-                                  }
-     ArPendingRepair d -> error (
+     ArFailedRepair _ -> Ok instData  -- Always keep the earliest failure.
+     ArHealthy _      -> Ok instData { arState = newSt
+                                     , tagsToRemove = delCurTag instData
+                                     }
+     ArPendingRepair d -> Bad (
        "An unfinished repair was found in instance " ++
        Instance.name (arInstance instData) ++ ": found tag " ++
        show (arTag newData) ++ ", but older pending tag " ++
        show (arTag d) ++ "exists.")
 
      ArNeedsRepair _ -> instData  -- Never happens, but silence GHC.
 
 -- | query jobs of a pending repair, returning the new instance data.
 processPending :: L.Client -> InstanceData -> IO InstanceData
@@ -450,9 +450,13 @@ main opts args = do
 
   (ClusterData _ nl il _ _) <- loadExternalData opts'
 
-  let iniData = map setInitialState $ Container.elems il
+  let iniDataRes = mapM setInitialState $ Container.elems il
+      iniDataChk = case iniDataRes of
+                     Ok d    -> return d
+                     Bad msg -> exitErr msg
 
   -- First step: check all pending repairs, see if they are completed.
+  iniData  <- iniDataChk
   iniData' <- bracket (L.getClient master) L.closeClient $
               forM iniData . processPending
 

> > +     ArNeedsRepair _ -> instData  -- Never happens, but silence GHC.

> If this never happens, should this be an error value?

Sure, that works too. Given that this would be a programming error, as opposed
to an "errononeous input" condition, I initially thought about ‘undefined’:

@@ -194,7 +194,8 @@ arStatusCmp instData arData =
        show (arTag newData) ++ ", but older pending tag " ++
        show (arTag d) ++ "exists.")

-     ArNeedsRepair _ -> instData  -- Never happens, but silence GHC.
+     ArNeedsRepair _ -> undefined -- Never happens, because ArNeedsRepair is
+                                  -- not an initial state; still, silences GHC.

However, now that arStatusCmp is using Result, I may as well do:

-     ArNeedsRepair _ -> instData  -- Never happens, but silence GHC.
+     ArNeedsRepair _ -> Bad (
+       "programming error: ArNeedsRepair found as an initial state")

Thanks for the review!

-- 
Dato Simó | [email protected]
Corp Fleet Management / Ganeti SRE (Dublin)

-- 
You received this message because you are subscribed to the Google Groups 
"ganeti-devel" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
For more options, visit https://groups.google.com/groups/opt_out.


Reply via email to