Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix pool registration rollback #1201

Merged
merged 6 commits into from
Dec 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/core/src/Cardano/DB/Sqlite.hs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

module Cardano.DB.Sqlite
( SqliteContext (..)
, DBLog (..)
, chunkSize
, dbChunked
, destroyDBLayer
Expand Down Expand Up @@ -187,6 +188,7 @@ data DBLog
| MsgQuery Text Severity
| MsgConnStr Text
| MsgClosing (Maybe FilePath)
| MsgDatabaseReset
deriving (Generic, Show, Eq, ToJSON)

instance DefinePrivacyAnnotation DBLog
Expand All @@ -197,6 +199,7 @@ instance DefineSeverity DBLog where
MsgQuery _ sev -> sev
MsgConnStr _ -> Debug
MsgClosing _ -> Debug
MsgDatabaseReset -> Notice

instance ToText DBLog where
toText msg = case msg of
Expand All @@ -205,6 +208,9 @@ instance ToText DBLog where
MsgQuery stmt _ -> stmt
MsgConnStr connStr -> "Using connection string: " <> connStr
MsgClosing fp -> "Closing database ("+|fromMaybe "in-memory" fp|+")"
MsgDatabaseReset ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

"Non backward compatible database found. Removing old database \
\and re-creating it from scratch."

{-------------------------------------------------------------------------------
Extra DB Helpers
Expand Down
10 changes: 8 additions & 2 deletions lib/core/src/Cardano/Pool/DB.hs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import Cardano.Wallet.Primitive.Types
)
import Control.Monad.Fail
( MonadFail )
import Control.Monad.IO.Class
( MonadIO )
import Control.Monad.Trans.Except
( ExceptT )
import Data.Map.Strict
Expand All @@ -50,7 +52,11 @@ import System.Random
-- >>> atomically $ putPoolProduction blockHeader pool
--
-- This gives you the power to also run /multiple/ operations atomically.
data DBLayer m = forall stm. MonadFail stm => DBLayer
--
-- FIXME: Allowing 'MonadIO' to enable logging also within db transactions.
-- Ideally, we should lower than constraint to only allow logging effects and
-- not any dragons in IO.
data DBLayer m = forall stm. (MonadFail stm, MonadIO stm) => DBLayer
{ putPoolProduction
:: BlockHeader
-> PoolId
Expand Down Expand Up @@ -86,7 +92,7 @@ data DBLayer m = forall stm. MonadFail stm => DBLayer
-- This is useful for the @NetworkLayer@ to know how far we have synced.

, putPoolRegistration
:: EpochNo
:: SlotId
-> PoolRegistrationCertificate
-> stm ()
-- ^ Add a mapping between stake pools and their corresponding
Expand Down
18 changes: 9 additions & 9 deletions lib/core/src/Cardano/Pool/DB/Model.hs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ data PoolDatabase = PoolDatabase
, owners :: !(Map PoolId [PoolOwner])
-- ^ Mapping between pool ids and owners

, metadata :: !(Map (EpochNo, PoolId) (Percentage, Quantity "lovelace" Word64))
, metadata :: !(Map (SlotId, PoolId) (Percentage, Quantity "lovelace" Word64))
-- ^ On-chain metadata associated with pools

, seed :: !SystemSeed
Expand Down Expand Up @@ -164,11 +164,11 @@ mReadStakeDistribution epoch db@PoolDatabase{distributions} =
, db
)

mPutPoolRegistration :: EpochNo -> PoolRegistrationCertificate -> ModelPoolOp ()
mPutPoolRegistration ep registration db@PoolDatabase{owners,metadata} =
mPutPoolRegistration :: SlotId -> PoolRegistrationCertificate -> ModelPoolOp ()
mPutPoolRegistration sl registration db@PoolDatabase{owners,metadata} =
( Right ()
, db { owners = Map.insert poolId poolOwners owners
, metadata = Map.insert (ep, poolId) (poolMargin, poolCost) metadata
, metadata = Map.insert (sl, poolId) (poolMargin, poolCost) metadata
}
)
where
Expand Down Expand Up @@ -222,15 +222,15 @@ mReadCursor k db@PoolDatabase{pools} =
mRollbackTo :: SlotId -> ModelPoolOp ()
mRollbackTo point PoolDatabase{pools, distributions, owners, metadata, seed} =
let
metadata' = Map.mapMaybeWithKey (\(ep, _) -> discardEpoch ep) metadata
metadata' = Map.mapMaybeWithKey (discardBy id . fst) metadata
owners' = Map.restrictKeys owners
$ Set.fromList
$ snd <$> Map.keys metadata'
in
( Right ()
, PoolDatabase
{ pools = updatePools $ updateSlots pools
, distributions = Map.mapMaybeWithKey discardEpoch distributions
, distributions = Map.mapMaybeWithKey (discardBy epochNumber) distributions
, owners = owners'
, metadata = metadata'
, seed
Expand All @@ -241,7 +241,7 @@ mRollbackTo point PoolDatabase{pools, distributions, owners, metadata, seed} =
updateSlots = Map.map (filter ((<= point) . slotId))
updatePools = Map.filter (not . L.null)

discardEpoch :: EpochNo -> a -> Maybe a
discardEpoch ep v
| ep <= epochNumber point = Just v
discardBy :: Ord point => (SlotId -> point) -> point -> a -> Maybe a
discardBy get point' v
| point' <= get point = Just v
| otherwise = Nothing
54 changes: 43 additions & 11 deletions lib/core/src/Cardano/Pool/DB/Sqlite.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ import Prelude
import Cardano.BM.Trace
( Trace )
import Cardano.DB.Sqlite
( SqliteContext (..)
( DBLog (..)
, SqliteContext (..)
, destroyDBLayer
, handleConstraint
, startSqliteBackend
Expand All @@ -40,7 +41,7 @@ import Cardano.Pool.DB
import Cardano.Wallet.DB.Sqlite.Types
( BlockId (..) )
import Cardano.Wallet.Logging
( transformTextTrace )
( logTrace, transformTextTrace )
import Cardano.Wallet.Primitive.Types
( BlockHeader (..)
, EpochNo (..)
Expand All @@ -49,13 +50,13 @@ import Cardano.Wallet.Primitive.Types
, SlotId (..)
)
import Control.Exception
( bracket )
( bracket, handle, throwIO )
import Control.Monad.IO.Class
( liftIO )
import Control.Monad.Trans.Except
( ExceptT (..) )
import Data.List
( foldl' )
( foldl', isSubsequenceOf )
import Data.Map.Strict
( Map )
import Data.Quantity
Expand All @@ -80,6 +81,10 @@ import Database.Persist.Sql
)
import Database.Persist.Sqlite
( SqlPersistT )
import Database.Persist.Types
( PersistException )
import System.Directory
( removeFile )
import System.FilePath
( (</>) )
import System.Random
Expand Down Expand Up @@ -140,8 +145,8 @@ newDBLayer
-> IO (SqliteContext, DBLayer IO)
newDBLayer logConfig trace fp = do
let trace' = transformTextTrace trace
ctx@SqliteContext{runQuery} <-
startSqliteBackend logConfig migrateAll trace' fp
let io = startSqliteBackend logConfig migrateAll trace' fp
ctx@SqliteContext{runQuery} <- handle (handlePersistError trace' fp io) io
return (ctx, DBLayer
{ putPoolProduction = \point pool -> ExceptT $
handleConstraint (ErrPointAlreadyExists point) $
Expand All @@ -168,16 +173,15 @@ newDBLayer logConfig trace fp = do
[ StakeDistributionEpoch ==. fromIntegral epoch ]
[]

, putPoolRegistration = \(EpochNo ep) PoolRegistrationCertificate
, putPoolRegistration = \point PoolRegistrationCertificate
{ poolId
, poolOwners
, poolMargin
, poolCost
} -> do
let ep_ = fromIntegral ep
let poolMargin_ = fromIntegral $ fromEnum poolMargin
let poolCost_ = getQuantity poolCost
insert_ $ PoolRegistration poolId ep_ poolMargin_ poolCost_
insert_ $ PoolRegistration poolId point poolMargin_ poolCost_
insertMany_ $ uncurry (PoolOwner poolId) <$> zip poolOwners [0..]

, readPoolRegistration = \poolId -> do
Expand All @@ -195,13 +199,13 @@ newDBLayer logConfig trace fp = do

, listRegisteredPools = do
fmap (poolRegistrationPoolId . entityVal) <$> selectList [ ]
[ Desc PoolRegistrationEpoch ]
[ Desc PoolRegistrationSlot ]

, rollbackTo = \point -> do
let (EpochNo epoch) = epochNumber point
deleteWhere [ PoolProductionSlot >. point ]
deleteWhere [ StakeDistributionEpoch >. fromIntegral epoch ]
deleteWhere [ PoolRegistrationEpoch >. fromIntegral epoch ]
deleteWhere [ PoolRegistrationSlot >. point ]

, readPoolProductionCursor = \k -> do
reverse . map (snd . fromPoolProduction . entityVal) <$> selectList
Expand All @@ -227,6 +231,34 @@ newDBLayer logConfig trace fp = do
, atomically = runQuery
})

-- | 'Temporary', catches migration error from previous versions and if any,
-- _removes_ the database file completely before retrying to start the database.
--
-- This comes in handy to fix database schema in a non-backward compatible way
-- without altering too much the user experience. Indeed, the pools' database
-- can swiftly be re-synced from the chain, so instead of patching our mistakes
-- with ugly work-around we can, at least for now, reset it semi-manually when
-- needed to keep things tidy here.
handlePersistError
:: Trace IO DBLog
-- ^ Logging object
-> Maybe FilePath
-- ^ Database file location, or Nothing for in-memory database
-> IO ctx
-- ^ Action to retry after "fixing" the database"
-> PersistException
-- ^ Error when trying to run a database migration
-> IO ctx
handlePersistError _trace Nothing _action e = throwIO e
handlePersistError trace (Just fp) action e = do
let mark = "Database migration: manual intervention required."
if mark `isSubsequenceOf` show e then do
logTrace trace MsgDatabaseReset
removeFile fp
action
else
throwIO e

{-------------------------------------------------------------------------------
Queries
-------------------------------------------------------------------------------}
Expand Down
2 changes: 1 addition & 1 deletion lib/core/src/Cardano/Pool/DB/Sqlite/TH.hs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ PoolOwner sql=pool_owner
-- Mapping of registration certificate to pool
PoolRegistration sql=pool_registration
poolRegistrationPoolId W.PoolId sql=pool_id
poolRegistrationEpoch Word64 sql=epoch
poolRegistrationSlot W.SlotId sql=slot
poolRegistrationMargin Word8 sql=margin
poolRegistrationCost Word64 sql=cost

Expand Down
18 changes: 8 additions & 10 deletions lib/core/src/Cardano/Pool/Metrics.hs
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,13 @@ monitorStakePools tr nl DBLayer{..} = do
initCursor :: IO [BlockHeader]
initCursor = do
let (block0, bp) = staticBlockchainParameters nl
let ep0 = block0 ^. #header . #slotId . #epochNumber
let sl0 = block0 ^. #header . #slotId
let k = fromIntegral . getQuantity . view #getEpochStability $ bp
atomically $ do
forM_ (poolRegistrations block0)
$ \r@PoolRegistrationCertificate{poolId} -> do
readPoolRegistration poolId >>= \case
Nothing -> putPoolRegistration ep0 r
Nothing -> putPoolRegistration sl0 r
Just{} -> pure ()
readPoolProductionCursor k

Expand All @@ -205,16 +205,14 @@ monitorStakePools tr nl DBLayer{..} = do
when (nodeTip /= currentTip) $ throwE ErrMonitorStakePoolsWrongTip

liftIO $ logInfo tr $ "Writing stake-distribution for epoch " <> pretty ep

let registrations = concatMap poolRegistrations blocks
liftIO $ forM_ registrations $ \registration ->
logInfo tr $ "Discovered stake pool registration: "
<> pretty registration

mapExceptT atomically $ do
lift $ putStakeDistribution ep (Map.toList dist)
lift $ mapM_ (putPoolRegistration ep) registrations
forM_ blocks $ \b ->
forM_ blocks $ \b -> do
forM_ (poolRegistrations b) $ \pool -> do
lift $ putPoolRegistration (b ^. #header . #slotId) pool
liftIO
$ logInfo tr
$ "Discovered stake pool registration: " <> pretty pool
withExceptT ErrMonitorStakePoolsPoolAlreadyExists $
putPoolProduction (header b) (producer b)
where
Expand Down
Binary file not shown.
12 changes: 6 additions & 6 deletions lib/core/test/unit/Cardano/Pool/DB/Properties.hs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ prop_poolRegistration DBLayer {..} entries =
setup = run $ atomically cleanDB
expected = L.sort entries
prop = do
run . atomically $ mapM_ (putPoolRegistration 0) entries
run . atomically $ mapM_ (putPoolRegistration (SlotId 0 0)) entries
pools <- run . atomically $ L.sort . catMaybes
<$> mapM (readPoolRegistration . poolId) entries
monitor $ counterexample $ unlines
Expand All @@ -383,7 +383,7 @@ prop_poolRegistration DBLayer {..} entries =
prop_rollbackRegistration
:: DBLayer IO
-> SlotId
-> [(EpochNo, PoolRegistrationCertificate)]
-> [(SlotId, PoolRegistrationCertificate)]
-> Property
prop_rollbackRegistration DBLayer{..} rollbackPoint entries =
monadicIO (setup >> prop)
Expand All @@ -394,9 +394,8 @@ prop_rollbackRegistration DBLayer{..} rollbackPoint entries =
case L.find (on (==) poolId pool . snd) entries of
Nothing ->
error "unknown pool?"
Just (ep, pool') ->
(ep <= epochNumber rollbackPoint) &&
(pool == pool')
Just (sl, pool') ->
(sl <= rollbackPoint) && (pool == pool')

ownerHasManyPools =
let owners = concatMap (poolOwners . snd) entries
Expand Down Expand Up @@ -427,7 +426,8 @@ prop_listRegisteredPools DBLayer {..} entries =
L.nub poolOwners /= poolOwners

prop = do
run . atomically $ mapM_ (uncurry putPoolRegistration) (zip [0..] entries)
let entries' = (zip [SlotId ep 0 | ep <- [0..]] entries)
run . atomically $ mapM_ (uncurry putPoolRegistration) entries'
pools <- run . atomically $ listRegisteredPools
monitor $ classify (any hasDuplicateOwners entries)
"same owner multiple time in the same certificate"
Expand Down
Loading