Type protocol persistence around model phases
This commit is contained in:
parent
37a7a55bfe
commit
a31b883516
6 changed files with 607 additions and 258 deletions
|
|
@ -57,9 +57,20 @@ runPut env user root rel op size sha dispatchPut = do
|
||||||
available <-
|
available <-
|
||||||
if existing then pure True else objectStoreAvailable (envS3 env)
|
if existing then pure True else objectStoreAvailable (envS3 env)
|
||||||
if available
|
if available
|
||||||
then Just <$> admitMutation conn uid Put op pool key rel size (Just sha)
|
then Just <$> admitMutation
|
||||||
(cfgMaxObjectBytes (envConfig env))
|
conn
|
||||||
(cfgIdempotencyWindowSec (envConfig env))
|
AdmissionRequest
|
||||||
|
{ admissionIdentity = AdmissionIdentity uid Put op
|
||||||
|
, admissionPoolId = pool
|
||||||
|
, admissionRelativePath = rel
|
||||||
|
, admissionFingerprint =
|
||||||
|
MutationFingerprint Put key size (Just sha)
|
||||||
|
}
|
||||||
|
AdmissionLimits
|
||||||
|
{ admissionMaxObjectBytes = cfgMaxObjectBytes (envConfig env)
|
||||||
|
, admissionIdempotencyWindowSec =
|
||||||
|
cfgIdempotencyWindowSec (envConfig env)
|
||||||
|
}
|
||||||
else pure Nothing
|
else pure Nothing
|
||||||
case admitted of
|
case admitted of
|
||||||
Nothing ->
|
Nothing ->
|
||||||
|
|
@ -103,9 +114,20 @@ runDelete env user root rel op = do
|
||||||
available <-
|
available <-
|
||||||
if existing then pure True else objectStoreAvailable (envS3 env)
|
if existing then pure True else objectStoreAvailable (envS3 env)
|
||||||
if available
|
if available
|
||||||
then Just <$> admitMutation conn uid Delete op pool key rel 0 Nothing
|
then Just <$> admitMutation
|
||||||
(cfgMaxObjectBytes (envConfig env))
|
conn
|
||||||
(cfgIdempotencyWindowSec (envConfig env))
|
AdmissionRequest
|
||||||
|
{ admissionIdentity = AdmissionIdentity uid Delete op
|
||||||
|
, admissionPoolId = pool
|
||||||
|
, admissionRelativePath = rel
|
||||||
|
, admissionFingerprint =
|
||||||
|
MutationFingerprint Delete key 0 Nothing
|
||||||
|
}
|
||||||
|
AdmissionLimits
|
||||||
|
{ admissionMaxObjectBytes = cfgMaxObjectBytes (envConfig env)
|
||||||
|
, admissionIdempotencyWindowSec =
|
||||||
|
cfgIdempotencyWindowSec (envConfig env)
|
||||||
|
}
|
||||||
else pure Nothing
|
else pure Nothing
|
||||||
case admitted of
|
case admitted of
|
||||||
Nothing -> pure (Left "object store unavailable")
|
Nothing -> pure (Left "object store unavailable")
|
||||||
|
|
@ -143,8 +165,10 @@ dispatchAndFinalize env user root record timeoutFailure dispatch = do
|
||||||
else markExternalPending
|
else markExternalPending
|
||||||
conn
|
conn
|
||||||
record
|
record
|
||||||
(requestTimeoutSec * 2)
|
DispatchTiming
|
||||||
(cfgRecoveryGraceSec cfg)
|
{ dispatchSettlementBoundSec = requestTimeoutSec * 2
|
||||||
|
, dispatchWorkerGraceSec = cfgRecoveryGraceSec cfg
|
||||||
|
}
|
||||||
if not prepared
|
if not prepared
|
||||||
then pure (Right MutationPending)
|
then pure (Right MutationPending)
|
||||||
else do
|
else do
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,32 @@
|
||||||
-- | Short Postgres transactions for mutation authority; store I/O stays outside them.
|
-- | Short Postgres transactions for mutation authority; store I/O stays outside them.
|
||||||
module Nuthatch.Protocol
|
module Nuthatch.Protocol
|
||||||
( AdmitOutcome (..)
|
( AdmitOutcome (..)
|
||||||
|
, AdmissionIdentity (..)
|
||||||
|
, MutationFingerprint (..)
|
||||||
|
, AdmissionRequest (..)
|
||||||
|
, AdmissionLimits (..)
|
||||||
|
, DispatchTiming (..)
|
||||||
|
, PriorObject (..)
|
||||||
|
, DesiredObject (..)
|
||||||
, ExistingDisposition (..)
|
, ExistingDisposition (..)
|
||||||
, MutationRecord (..)
|
, MutationRecord (..)
|
||||||
|
, MutationPhase (..)
|
||||||
|
, TerminalResult (..)
|
||||||
|
, mutationState
|
||||||
|
, externalState
|
||||||
|
, encodeMutationPhase
|
||||||
|
, decodeMutationPhase
|
||||||
|
, mrState
|
||||||
|
, mrExternal
|
||||||
|
, mrUserId
|
||||||
|
, mrKind
|
||||||
|
, mrOperationId
|
||||||
|
, mrObjectKey
|
||||||
|
, mrDeclaredSize
|
||||||
|
, mrContentSha256
|
||||||
|
, mrPriorSize
|
||||||
|
, mrPriorSha256
|
||||||
|
, desiredObject
|
||||||
, ObjectObservation (..)
|
, ObjectObservation (..)
|
||||||
, admitMutation
|
, admitMutation
|
||||||
, markApplying
|
, markApplying
|
||||||
|
|
@ -27,27 +51,74 @@ module Nuthatch.Protocol
|
||||||
, finalizeFromObservation
|
, finalizeFromObservation
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.Maybe (listToMaybe, mapMaybe)
|
import Control.Exception (throwIO)
|
||||||
|
import Data.Maybe (listToMaybe)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Database.PostgreSQL.Simple (Connection, Only (..), execute, query)
|
import Database.PostgreSQL.Simple (Connection, Only (..), execute, query)
|
||||||
|
import qualified Database.PostgreSQL.Simple.FromField as PG
|
||||||
|
import qualified Database.PostgreSQL.Simple.FromRow as PGRow
|
||||||
|
import qualified Database.PostgreSQL.Simple.ToField as PGToField
|
||||||
|
import qualified Database.PostgreSQL.Simple.ToRow as PGToRow
|
||||||
import Nuthatch.Types
|
import Nuthatch.Types
|
||||||
|
|
||||||
data MutationRecord = MutationRecord
|
data MutationRecord = MutationRecord
|
||||||
{ mrUserId :: UserId
|
{ mrAdmissionIdentity :: AdmissionIdentity
|
||||||
, mrKind :: MutationKind
|
, mrPoolId :: PoolId
|
||||||
, mrOperationId :: OperationId
|
, mrRelativePath :: RelativePath
|
||||||
, mrPoolId :: PoolId
|
, mrFingerprint :: MutationFingerprint
|
||||||
, mrObjectKey :: ObjectKey
|
, mrPriorObject :: PriorObject
|
||||||
, mrRelativePath :: RelativePath
|
, mrGrowth :: Integer
|
||||||
, mrDeclaredSize :: Integer
|
, mrPhase :: MutationPhase
|
||||||
, mrContentSha256 :: Maybe ContentSha256
|
|
||||||
, mrPriorSize :: Integer
|
|
||||||
, mrPriorSha256 :: Maybe ContentSha256
|
|
||||||
, mrGrowth :: Integer
|
|
||||||
, mrState :: MutationState
|
|
||||||
, mrExternal :: ExternalState
|
|
||||||
}
|
}
|
||||||
deriving stock (Show)
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
data MutationPhase
|
||||||
|
= PhasePrepared
|
||||||
|
| PhaseApplying ExternalState
|
||||||
|
| PhaseTerminal TerminalResult
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
data TerminalResult
|
||||||
|
= TerminalSucceeded
|
||||||
|
| TerminalFailed
|
||||||
|
| TerminalRejected
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
mutationState :: MutationPhase -> MutationState
|
||||||
|
mutationState = \case
|
||||||
|
PhasePrepared -> Prepared
|
||||||
|
PhaseApplying _ -> Applying
|
||||||
|
PhaseTerminal TerminalSucceeded -> Succeeded
|
||||||
|
PhaseTerminal TerminalFailed -> Failed
|
||||||
|
PhaseTerminal TerminalRejected -> Rejected
|
||||||
|
|
||||||
|
externalState :: MutationPhase -> ExternalState
|
||||||
|
externalState = \case
|
||||||
|
PhaseApplying external -> external
|
||||||
|
_ -> ExtIdle
|
||||||
|
|
||||||
|
encodeMutationPhase :: MutationPhase -> (MutationState, ExternalState)
|
||||||
|
encodeMutationPhase phase = (mutationState phase, externalState phase)
|
||||||
|
|
||||||
|
decodeMutationPhase :: MutationState -> ExternalState -> Maybe MutationPhase
|
||||||
|
decodeMutationPhase state external =
|
||||||
|
case (state, external) of
|
||||||
|
(Prepared, ExtIdle) -> Just PhasePrepared
|
||||||
|
(Applying, value) -> Just (PhaseApplying value)
|
||||||
|
(Succeeded, ExtIdle) ->
|
||||||
|
Just (PhaseTerminal TerminalSucceeded)
|
||||||
|
(Failed, ExtIdle) ->
|
||||||
|
Just (PhaseTerminal TerminalFailed)
|
||||||
|
(Rejected, ExtIdle) ->
|
||||||
|
Just (PhaseTerminal TerminalRejected)
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
mrState :: MutationRecord -> MutationState
|
||||||
|
mrState = mutationState . mrPhase
|
||||||
|
|
||||||
|
mrExternal :: MutationRecord -> ExternalState
|
||||||
|
mrExternal = externalState . mrPhase
|
||||||
|
|
||||||
-- | Unavailable must not drive terminal finalize.
|
-- | Unavailable must not drive terminal finalize.
|
||||||
data ObjectObservation
|
data ObjectObservation
|
||||||
|
|
@ -63,38 +134,214 @@ data AdmitOutcome
|
||||||
| AdmitConflict
|
| AdmitConflict
|
||||||
deriving stock (Show)
|
deriving stock (Show)
|
||||||
|
|
||||||
|
data AdmissionIdentity = AdmissionIdentity
|
||||||
|
{ admissionUserId :: UserId
|
||||||
|
, admissionKind :: MutationKind
|
||||||
|
, admissionOperationId :: OperationId
|
||||||
|
}
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
data MutationFingerprint = MutationFingerprint
|
||||||
|
{ fingerprintKind :: MutationKind
|
||||||
|
, fingerprintObjectKey :: ObjectKey
|
||||||
|
, fingerprintDeclaredSize :: Integer
|
||||||
|
, fingerprintContentSha256 :: Maybe ContentSha256
|
||||||
|
}
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
data AdmissionRequest = AdmissionRequest
|
||||||
|
{ admissionIdentity :: AdmissionIdentity
|
||||||
|
, admissionPoolId :: PoolId
|
||||||
|
, admissionRelativePath :: RelativePath
|
||||||
|
, admissionFingerprint :: MutationFingerprint
|
||||||
|
}
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
data AdmissionLimits = AdmissionLimits
|
||||||
|
{ admissionMaxObjectBytes :: Integer
|
||||||
|
, admissionIdempotencyWindowSec :: Integer
|
||||||
|
}
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
data DispatchTiming = DispatchTiming
|
||||||
|
{ dispatchSettlementBoundSec :: Integer
|
||||||
|
, dispatchWorkerGraceSec :: Integer
|
||||||
|
}
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
data PriorObject = PriorObject
|
||||||
|
{ priorObjectSize :: Integer
|
||||||
|
, priorObjectSha256 :: Maybe ContentSha256
|
||||||
|
}
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
data DesiredObject
|
||||||
|
= DesiredPresent Integer (Maybe ContentSha256)
|
||||||
|
| DesiredAbsent
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
mrUserId :: MutationRecord -> UserId
|
||||||
|
mrUserId = admissionUserId . mrAdmissionIdentity
|
||||||
|
|
||||||
|
mrKind :: MutationRecord -> MutationKind
|
||||||
|
mrKind = admissionKind . mrAdmissionIdentity
|
||||||
|
|
||||||
|
mrOperationId :: MutationRecord -> OperationId
|
||||||
|
mrOperationId = admissionOperationId . mrAdmissionIdentity
|
||||||
|
|
||||||
|
mrObjectKey :: MutationRecord -> ObjectKey
|
||||||
|
mrObjectKey = fingerprintObjectKey . mrFingerprint
|
||||||
|
|
||||||
|
mrDeclaredSize :: MutationRecord -> Integer
|
||||||
|
mrDeclaredSize = fingerprintDeclaredSize . mrFingerprint
|
||||||
|
|
||||||
|
mrContentSha256 :: MutationRecord -> Maybe ContentSha256
|
||||||
|
mrContentSha256 = fingerprintContentSha256 . mrFingerprint
|
||||||
|
|
||||||
|
mrPriorSize :: MutationRecord -> Integer
|
||||||
|
mrPriorSize = priorObjectSize . mrPriorObject
|
||||||
|
|
||||||
|
mrPriorSha256 :: MutationRecord -> Maybe ContentSha256
|
||||||
|
mrPriorSha256 = priorObjectSha256 . mrPriorObject
|
||||||
|
|
||||||
|
desiredObject :: MutationRecord -> DesiredObject
|
||||||
|
desiredObject record =
|
||||||
|
case mrKind record of
|
||||||
|
Put -> DesiredPresent
|
||||||
|
(mrDeclaredSize record)
|
||||||
|
(mrContentSha256 record)
|
||||||
|
Delete -> DesiredAbsent
|
||||||
|
|
||||||
data ExistingDisposition
|
data ExistingDisposition
|
||||||
= NewMutationEpoch
|
= NewMutationEpoch
|
||||||
| ExistingReplay MutationResult MutationRecord
|
| ExistingReplay MutationResult MutationRecord
|
||||||
| ExistingConflict
|
| ExistingConflict
|
||||||
deriving stock (Show)
|
deriving stock (Show)
|
||||||
|
|
||||||
type ResultRow =
|
data MutationRow = MutationRow
|
||||||
( Text, Text, Text, Text, Text, Text, Integer, Maybe Text
|
{ rowUserId :: Text
|
||||||
, Integer, Maybe Text, Integer, Text, Text
|
, rowKind :: DbMutationKind
|
||||||
)
|
, rowOperationId :: Text
|
||||||
|
, rowPoolId :: Text
|
||||||
|
, rowObjectKey :: Text
|
||||||
|
, rowRelativePath :: Text
|
||||||
|
, rowDeclaredSize :: Integer
|
||||||
|
, rowContentSha256 :: Maybe Text
|
||||||
|
, rowPriorSize :: Integer
|
||||||
|
, rowPriorSha256 :: Maybe Text
|
||||||
|
, rowGrowth :: Integer
|
||||||
|
, rowState :: DbMutationState
|
||||||
|
, rowExternal :: DbExternalState
|
||||||
|
}
|
||||||
|
|
||||||
rowToRecord :: ResultRow -> Maybe MutationRecord
|
data SettlementRow =
|
||||||
rowToRecord (user, kindT, op, pool, key, rel, size, csha, prior, psha, growth, stateT, extT) = do
|
SettlementRow DbMutationState DbExternalState Integer Integer
|
||||||
kind <- parseKind kindT
|
|
||||||
state <- parseState stateT
|
newtype DbMutationKind = DbMutationKind MutationKind
|
||||||
ext <- parseExternal extT
|
|
||||||
|
newtype DbMutationState = DbMutationState MutationState
|
||||||
|
|
||||||
|
newtype DbExternalState = DbExternalState ExternalState
|
||||||
|
|
||||||
|
newtype MutationInsertRow = MutationInsertRow MutationRecord
|
||||||
|
|
||||||
|
instance PG.FromField DbMutationKind where
|
||||||
|
fromField field value = do
|
||||||
|
label <- PG.fromField field value
|
||||||
|
maybe
|
||||||
|
(PG.returnError PG.ConversionFailed field "invalid mutation kind")
|
||||||
|
(pure . DbMutationKind)
|
||||||
|
(parseKind label)
|
||||||
|
|
||||||
|
instance PG.FromField DbMutationState where
|
||||||
|
fromField field value = do
|
||||||
|
label <- PG.fromField field value
|
||||||
|
maybe
|
||||||
|
(PG.returnError PG.ConversionFailed field "invalid mutation state")
|
||||||
|
(pure . DbMutationState)
|
||||||
|
(parseState label)
|
||||||
|
|
||||||
|
instance PG.FromField DbExternalState where
|
||||||
|
fromField field value = do
|
||||||
|
label <- PG.fromField field value
|
||||||
|
maybe
|
||||||
|
(PG.returnError PG.ConversionFailed field "invalid external state")
|
||||||
|
(pure . DbExternalState)
|
||||||
|
(parseExternal label)
|
||||||
|
|
||||||
|
instance PGRow.FromRow MutationRow where
|
||||||
|
fromRow =
|
||||||
|
MutationRow
|
||||||
|
<$> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
|
||||||
|
instance PGRow.FromRow SettlementRow where
|
||||||
|
fromRow =
|
||||||
|
SettlementRow
|
||||||
|
<$> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
<*> PGRow.field
|
||||||
|
|
||||||
|
instance PGToRow.ToRow MutationInsertRow where
|
||||||
|
toRow (MutationInsertRow record) =
|
||||||
|
let (state, external) = encodeMutationPhase (mrPhase record)
|
||||||
|
in
|
||||||
|
[ PGToField.toField (unUserId (mrUserId record))
|
||||||
|
, PGToField.toField (kindLabel (mrKind record))
|
||||||
|
, PGToField.toField (unOperationId (mrOperationId record))
|
||||||
|
, PGToField.toField (unPoolId (mrPoolId record))
|
||||||
|
, PGToField.toField (unObjectKey (mrObjectKey record))
|
||||||
|
, PGToField.toField (unRelativePath (mrRelativePath record))
|
||||||
|
, PGToField.toField (mrDeclaredSize record)
|
||||||
|
, PGToField.toField
|
||||||
|
(unContentSha256 <$> mrContentSha256 record)
|
||||||
|
, PGToField.toField (mrPriorSize record)
|
||||||
|
, PGToField.toField
|
||||||
|
(unContentSha256 <$> mrPriorSha256 record)
|
||||||
|
, PGToField.toField (mrGrowth record)
|
||||||
|
, PGToField.toField (stateLabel state)
|
||||||
|
, PGToField.toField (externalLabel external)
|
||||||
|
]
|
||||||
|
|
||||||
|
rowToRecord :: MutationRow -> Either Text MutationRecord
|
||||||
|
rowToRecord MutationRow{..} = do
|
||||||
|
let DbMutationKind kind = rowKind
|
||||||
|
DbMutationState state = rowState
|
||||||
|
DbExternalState ext = rowExternal
|
||||||
|
phase <- maybe (Left "invalid mutation phase") Right (decodeMutationPhase state ext)
|
||||||
pure MutationRecord
|
pure MutationRecord
|
||||||
{ mrUserId = UserId user
|
{ mrAdmissionIdentity =
|
||||||
, mrKind = kind
|
AdmissionIdentity (UserId rowUserId) kind (OperationId rowOperationId)
|
||||||
, mrOperationId = OperationId op
|
, mrPoolId = PoolId rowPoolId
|
||||||
, mrPoolId = PoolId pool
|
, mrRelativePath = RelativePath rowRelativePath
|
||||||
, mrObjectKey = ObjectKey key
|
, mrFingerprint =
|
||||||
, mrRelativePath = RelativePath rel
|
MutationFingerprint
|
||||||
, mrDeclaredSize = size
|
kind
|
||||||
, mrContentSha256 = ContentSha256 <$> csha
|
(ObjectKey rowObjectKey)
|
||||||
, mrPriorSize = prior
|
rowDeclaredSize
|
||||||
, mrPriorSha256 = ContentSha256 <$> psha
|
(ContentSha256 <$> rowContentSha256)
|
||||||
, mrGrowth = growth
|
, mrPriorObject =
|
||||||
, mrState = state
|
PriorObject rowPriorSize (ContentSha256 <$> rowPriorSha256)
|
||||||
, mrExternal = ext
|
, mrGrowth = rowGrowth
|
||||||
|
, mrPhase = phase
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decodeRows :: [MutationRow] -> IO [MutationRecord]
|
||||||
|
decodeRows rows =
|
||||||
|
either (throwIO . userError . T.unpack) pure (traverse rowToRecord rows)
|
||||||
|
|
||||||
lookupMutation :: Connection -> UserId -> MutationKind -> OperationId -> IO (Maybe MutationRecord)
|
lookupMutation :: Connection -> UserId -> MutationKind -> OperationId -> IO (Maybe MutationRecord)
|
||||||
lookupMutation conn uid kind op = do
|
lookupMutation conn uid kind op = do
|
||||||
rows <- query conn
|
rows <- query conn
|
||||||
|
|
@ -103,7 +350,7 @@ lookupMutation conn uid kind op = do
|
||||||
\state, external_state FROM mutations \
|
\state, external_state FROM mutations \
|
||||||
\WHERE user_id = ? AND kind = ? AND operation_id = ?"
|
\WHERE user_id = ? AND kind = ? AND operation_id = ?"
|
||||||
(unUserId uid, kindLabel kind, unOperationId op)
|
(unUserId uid, kindLabel kind, unOperationId op)
|
||||||
pure (listToMaybe rows >>= rowToRecord)
|
listToMaybe <$> decodeRows rows
|
||||||
|
|
||||||
mutationEpochExists
|
mutationEpochExists
|
||||||
:: Connection
|
:: Connection
|
||||||
|
|
@ -137,25 +384,19 @@ lookupObjectMeta conn (ObjectKey key) = do
|
||||||
[(sz, sha)] -> Just (sz, ContentSha256 sha)
|
[(sz, sha)] -> Just (sz, ContentSha256 sha)
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
sameFingerprint :: MutationRecord -> MutationKind -> ObjectKey -> Integer -> Maybe ContentSha256 -> Bool
|
sameFingerprint :: MutationRecord -> MutationFingerprint -> Bool
|
||||||
sameFingerprint rec kind key size msha =
|
sameFingerprint rec fingerprint =
|
||||||
mrKind rec == kind
|
mrFingerprint rec == fingerprint
|
||||||
&& mrObjectKey rec == key
|
|
||||||
&& mrDeclaredSize rec == size
|
|
||||||
&& mrContentSha256 rec == msha
|
|
||||||
|
|
||||||
existingDisposition
|
existingDisposition
|
||||||
:: Maybe MutationRecord
|
:: Maybe MutationRecord
|
||||||
-> MutationKind
|
-> MutationFingerprint
|
||||||
-> ObjectKey
|
|
||||||
-> Integer
|
|
||||||
-> Maybe ContentSha256
|
|
||||||
-> ExistingDisposition
|
-> ExistingDisposition
|
||||||
existingDisposition existing kind key size msha =
|
existingDisposition existing fingerprint =
|
||||||
case existing of
|
case existing of
|
||||||
Nothing -> NewMutationEpoch
|
Nothing -> NewMutationEpoch
|
||||||
Just rec
|
Just rec
|
||||||
| sameFingerprint rec kind key size msha ->
|
| sameFingerprint rec fingerprint ->
|
||||||
ExistingReplay (stateToResult (mrState rec)) rec
|
ExistingReplay (stateToResult (mrState rec)) rec
|
||||||
| otherwise -> ExistingConflict
|
| otherwise -> ExistingConflict
|
||||||
|
|
||||||
|
|
@ -180,18 +421,38 @@ recoveryEligible state elapsedSeconds graceSeconds =
|
||||||
|
|
||||||
admitMutation
|
admitMutation
|
||||||
:: Connection
|
:: Connection
|
||||||
-> UserId
|
-> AdmissionRequest
|
||||||
-> MutationKind
|
-> AdmissionLimits
|
||||||
-> OperationId
|
|
||||||
-> PoolId
|
|
||||||
-> ObjectKey
|
|
||||||
-> RelativePath
|
|
||||||
-> Integer
|
|
||||||
-> Maybe ContentSha256
|
|
||||||
-> Integer
|
|
||||||
-> Integer
|
|
||||||
-> IO AdmitOutcome
|
-> IO AdmitOutcome
|
||||||
admitMutation conn uid kind op pool key rel size msha maxObject idempotencyWindow = do
|
admitMutation conn AdmissionRequest{..} limits =
|
||||||
|
if fingerprintKind admissionFingerprint
|
||||||
|
/= admissionKind admissionIdentity
|
||||||
|
then pure AdmitConflict
|
||||||
|
else admitConsistentMutation
|
||||||
|
conn
|
||||||
|
admissionIdentity
|
||||||
|
admissionPoolId
|
||||||
|
admissionRelativePath
|
||||||
|
admissionFingerprint
|
||||||
|
limits
|
||||||
|
|
||||||
|
admitConsistentMutation
|
||||||
|
:: Connection
|
||||||
|
-> AdmissionIdentity
|
||||||
|
-> PoolId
|
||||||
|
-> RelativePath
|
||||||
|
-> MutationFingerprint
|
||||||
|
-> AdmissionLimits
|
||||||
|
-> IO AdmitOutcome
|
||||||
|
admitConsistentMutation
|
||||||
|
conn
|
||||||
|
(AdmissionIdentity uid kind op)
|
||||||
|
pool
|
||||||
|
rel
|
||||||
|
fingerprint@(MutationFingerprint _ key size msha)
|
||||||
|
AdmissionLimits{..} = do
|
||||||
|
let maxObject = admissionMaxObjectBytes
|
||||||
|
idempotencyWindow = admissionIdempotencyWindowSec
|
||||||
let identity =
|
let identity =
|
||||||
unUserId uid <> "\NUL" <> kindLabel kind <> "\NUL" <> unOperationId op
|
unUserId uid <> "\NUL" <> kindLabel kind <> "\NUL" <> unOperationId op
|
||||||
_ <- query conn
|
_ <- query conn
|
||||||
|
|
@ -205,7 +466,7 @@ admitMutation conn uid kind op pool key rel size msha maxObject idempotencyWindo
|
||||||
\AND updated_at < now() - (? * interval '1 second')"
|
\AND updated_at < now() - (? * interval '1 second')"
|
||||||
(unUserId uid, kindLabel kind, unOperationId op, idempotencyWindow)
|
(unUserId uid, kindLabel kind, unOperationId op, idempotencyWindow)
|
||||||
existing <- lookupMutation conn uid kind op
|
existing <- lookupMutation conn uid kind op
|
||||||
case existingDisposition existing kind key size msha of
|
case existingDisposition existing fingerprint of
|
||||||
ExistingReplay result rec -> pure (AdmitReplay result rec)
|
ExistingReplay result rec -> pure (AdmitReplay result rec)
|
||||||
ExistingConflict -> pure AdmitConflict
|
ExistingConflict -> pure AdmitConflict
|
||||||
NewMutationEpoch -> do
|
NewMutationEpoch -> do
|
||||||
|
|
@ -216,21 +477,15 @@ admitMutation conn uid kind op pool key rel size msha maxObject idempotencyWindo
|
||||||
Put -> size
|
Put -> size
|
||||||
Delete -> 0
|
Delete -> 0
|
||||||
growth = growthBytes priorSize desiredSize
|
growth = growthBytes priorSize desiredSize
|
||||||
baseRec state =
|
baseRec phase =
|
||||||
MutationRecord
|
MutationRecord
|
||||||
{ mrUserId = uid
|
{ mrAdmissionIdentity = AdmissionIdentity uid kind op
|
||||||
, mrKind = kind
|
|
||||||
, mrOperationId = op
|
|
||||||
, mrPoolId = pool
|
, mrPoolId = pool
|
||||||
, mrObjectKey = key
|
|
||||||
, mrRelativePath = rel
|
, mrRelativePath = rel
|
||||||
, mrDeclaredSize = size
|
, mrFingerprint = fingerprint
|
||||||
, mrContentSha256 = msha
|
, mrPriorObject = PriorObject priorSize priorSha
|
||||||
, mrPriorSize = priorSize
|
|
||||||
, mrPriorSha256 = priorSha
|
|
||||||
, mrGrowth = 0
|
, mrGrowth = 0
|
||||||
, mrState = state
|
, mrPhase = phase
|
||||||
, mrExternal = ExtIdle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
poolRows <- query conn
|
poolRows <- query conn
|
||||||
|
|
@ -261,24 +516,22 @@ admitMutation conn uid kind op pool key rel size msha maxObject idempotencyWindo
|
||||||
rec <- insertTerminalRejected conn uid kind op pool key rel size msha priorSize priorSha
|
rec <- insertTerminalRejected conn uid kind op pool key rel size msha priorSize priorSha
|
||||||
pure (AdmitRejected "quota exceeded" rec)
|
pure (AdmitRejected "quota exceeded" rec)
|
||||||
else do
|
else do
|
||||||
|
let preparedRecord = MutationRecord
|
||||||
|
{ mrAdmissionIdentity = AdmissionIdentity uid kind op
|
||||||
|
, mrPoolId = pool
|
||||||
|
, mrRelativePath = rel
|
||||||
|
, mrFingerprint = fingerprint
|
||||||
|
, mrPriorObject = PriorObject priorSize priorSha
|
||||||
|
, mrGrowth = growth
|
||||||
|
, mrPhase = PhasePrepared
|
||||||
|
}
|
||||||
n <- execute conn
|
n <- execute conn
|
||||||
"INSERT INTO mutations \
|
"INSERT INTO mutations \
|
||||||
\(user_id, kind, operation_id, pool_id, object_key, relative_path, \
|
\(user_id, kind, operation_id, pool_id, object_key, relative_path, \
|
||||||
\ declared_size, content_sha256, prior_size, prior_sha256, growth_bytes, \
|
\ declared_size, content_sha256, prior_size, prior_sha256, growth_bytes, \
|
||||||
\ state, external_state) \
|
\ state, external_state) \
|
||||||
\VALUES (?,?,?,?,?,?,?,?,?,?,?,'prepared','idle')"
|
\VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||||
( unUserId uid
|
(MutationInsertRow preparedRecord)
|
||||||
, kindLabel kind
|
|
||||||
, unOperationId op
|
|
||||||
, unPoolId pool
|
|
||||||
, unObjectKey key
|
|
||||||
, unRelativePath rel
|
|
||||||
, size
|
|
||||||
, unContentSha256 <$> msha
|
|
||||||
, priorSize
|
|
||||||
, unContentSha256 <$> priorSha
|
|
||||||
, growth
|
|
||||||
)
|
|
||||||
if n /= 1
|
if n /= 1
|
||||||
then pure AdmitConflict
|
then pure AdmitConflict
|
||||||
else do
|
else do
|
||||||
|
|
@ -294,26 +547,12 @@ admitMutation conn uid kind op pool key rel size msha maxObject idempotencyWindo
|
||||||
\WHERE user_id = ? AND kind = ? AND operation_id = ?"
|
\WHERE user_id = ? AND kind = ? AND operation_id = ?"
|
||||||
(unUserId uid, kindLabel kind, unOperationId op)
|
(unUserId uid, kindLabel kind, unOperationId op)
|
||||||
pure $ AdmitRejected "key locked by another mutation"
|
pure $ AdmitRejected "key locked by another mutation"
|
||||||
(baseRec Rejected)
|
(baseRec (PhaseTerminal TerminalRejected))
|
||||||
else do
|
else do
|
||||||
_ <- execute conn
|
_ <- execute conn
|
||||||
"UPDATE pools SET reserved_bytes = reserved_bytes + ? WHERE pool_id = ?"
|
"UPDATE pools SET reserved_bytes = reserved_bytes + ? WHERE pool_id = ?"
|
||||||
(growth, unPoolId pool)
|
(growth, unPoolId pool)
|
||||||
pure $ AdmitPrepared MutationRecord
|
pure (AdmitPrepared preparedRecord)
|
||||||
{ mrUserId = uid
|
|
||||||
, mrKind = kind
|
|
||||||
, mrOperationId = op
|
|
||||||
, mrPoolId = pool
|
|
||||||
, mrObjectKey = key
|
|
||||||
, mrRelativePath = rel
|
|
||||||
, mrDeclaredSize = size
|
|
||||||
, mrContentSha256 = msha
|
|
||||||
, mrPriorSize = priorSize
|
|
||||||
, mrPriorSha256 = priorSha
|
|
||||||
, mrGrowth = growth
|
|
||||||
, mrState = Prepared
|
|
||||||
, mrExternal = ExtIdle
|
|
||||||
}
|
|
||||||
|
|
||||||
insertTerminalRejected
|
insertTerminalRejected
|
||||||
:: Connection
|
:: Connection
|
||||||
|
|
@ -329,39 +568,25 @@ insertTerminalRejected
|
||||||
-> Maybe ContentSha256
|
-> Maybe ContentSha256
|
||||||
-> IO MutationRecord
|
-> IO MutationRecord
|
||||||
insertTerminalRejected conn uid kind op pool key rel size msha priorSize priorSha = do
|
insertTerminalRejected conn uid kind op pool key rel size msha priorSize priorSha = do
|
||||||
|
let record = MutationRecord
|
||||||
|
{ mrAdmissionIdentity = AdmissionIdentity uid kind op
|
||||||
|
, mrPoolId = pool
|
||||||
|
, mrRelativePath = rel
|
||||||
|
, mrFingerprint =
|
||||||
|
MutationFingerprint kind key size msha
|
||||||
|
, mrPriorObject = PriorObject priorSize priorSha
|
||||||
|
, mrGrowth = 0
|
||||||
|
, mrPhase = PhaseTerminal TerminalRejected
|
||||||
|
}
|
||||||
_ <- execute conn
|
_ <- execute conn
|
||||||
"INSERT INTO mutations \
|
"INSERT INTO mutations \
|
||||||
\(user_id, kind, operation_id, pool_id, object_key, relative_path, \
|
\(user_id, kind, operation_id, pool_id, object_key, relative_path, \
|
||||||
\ declared_size, content_sha256, prior_size, prior_sha256, growth_bytes, \
|
\ declared_size, content_sha256, prior_size, prior_sha256, growth_bytes, \
|
||||||
\ state, external_state) \
|
\ state, external_state) \
|
||||||
\VALUES (?,?,?,?,?,?,?,?,?,?,0,'rejected','idle') \
|
\VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) \
|
||||||
\ON CONFLICT DO NOTHING"
|
\ON CONFLICT DO NOTHING"
|
||||||
( unUserId uid
|
(MutationInsertRow record)
|
||||||
, kindLabel kind
|
pure record
|
||||||
, unOperationId op
|
|
||||||
, unPoolId pool
|
|
||||||
, unObjectKey key
|
|
||||||
, unRelativePath rel
|
|
||||||
, size
|
|
||||||
, unContentSha256 <$> msha
|
|
||||||
, priorSize
|
|
||||||
, unContentSha256 <$> priorSha
|
|
||||||
)
|
|
||||||
pure MutationRecord
|
|
||||||
{ mrUserId = uid
|
|
||||||
, mrKind = kind
|
|
||||||
, mrOperationId = op
|
|
||||||
, mrPoolId = pool
|
|
||||||
, mrObjectKey = key
|
|
||||||
, mrRelativePath = rel
|
|
||||||
, mrDeclaredSize = size
|
|
||||||
, mrContentSha256 = msha
|
|
||||||
, mrPriorSize = priorSize
|
|
||||||
, mrPriorSha256 = priorSha
|
|
||||||
, mrGrowth = 0
|
|
||||||
, mrState = Rejected
|
|
||||||
, mrExternal = ExtIdle
|
|
||||||
}
|
|
||||||
|
|
||||||
stateToResult :: MutationState -> MutationResult
|
stateToResult :: MutationState -> MutationResult
|
||||||
stateToResult Prepared = MutationPending
|
stateToResult Prepared = MutationPending
|
||||||
|
|
@ -381,10 +606,9 @@ markApplying conn rec = do
|
||||||
markExternalPending
|
markExternalPending
|
||||||
:: Connection
|
:: Connection
|
||||||
-> MutationRecord
|
-> MutationRecord
|
||||||
-> Integer
|
-> DispatchTiming
|
||||||
-> Integer
|
|
||||||
-> IO Bool
|
-> IO Bool
|
||||||
markExternalPending conn rec settlementBoundSec workerGraceSec = do
|
markExternalPending conn rec DispatchTiming{..} = do
|
||||||
n <- execute conn
|
n <- execute conn
|
||||||
"UPDATE mutations SET external_state = 'pending', \
|
"UPDATE mutations SET external_state = 'pending', \
|
||||||
\dispatch_started_at = now(), \
|
\dispatch_started_at = now(), \
|
||||||
|
|
@ -393,8 +617,8 @@ markExternalPending conn rec settlementBoundSec workerGraceSec = do
|
||||||
\updated_at = now() \
|
\updated_at = now() \
|
||||||
\WHERE user_id = ? AND kind = ? AND operation_id = ? \
|
\WHERE user_id = ? AND kind = ? AND operation_id = ? \
|
||||||
\AND state = 'applying' AND external_state = 'idle'"
|
\AND state = 'applying' AND external_state = 'idle'"
|
||||||
( settlementBoundSec
|
( dispatchSettlementBoundSec
|
||||||
, workerGraceSec
|
, dispatchWorkerGraceSec
|
||||||
, unUserId (mrUserId rec)
|
, unUserId (mrUserId rec)
|
||||||
, kindLabel (mrKind rec)
|
, kindLabel (mrKind rec)
|
||||||
, unOperationId (mrOperationId rec)
|
, unOperationId (mrOperationId rec)
|
||||||
|
|
@ -427,10 +651,15 @@ finalizeSuccess conn rec finalSize finalSha = do
|
||||||
"SELECT state, external_state, growth_bytes, prior_size FROM mutations \
|
"SELECT state, external_state, growth_bytes, prior_size FROM mutations \
|
||||||
\WHERE user_id = ? AND kind = ? AND operation_id = ? FOR UPDATE"
|
\WHERE user_id = ? AND kind = ? AND operation_id = ? FOR UPDATE"
|
||||||
(unUserId (mrUserId rec), kindLabel (mrKind rec), unOperationId (mrOperationId rec))
|
(unUserId (mrUserId rec), kindLabel (mrKind rec), unOperationId (mrOperationId rec))
|
||||||
:: IO [(Text, Text, Integer, Integer)]
|
:: IO [SettlementRow]
|
||||||
case rows of
|
case rows of
|
||||||
[(st, ext, growth, prior)]
|
[SettlementRow
|
||||||
| st == "applying" && ext == "settled" -> do
|
(DbMutationState state)
|
||||||
|
(DbExternalState external)
|
||||||
|
growth
|
||||||
|
prior]
|
||||||
|
| decodeMutationPhase state external
|
||||||
|
== Just (PhaseApplying ExtSettled) -> do
|
||||||
let pool = mrPoolId rec
|
let pool = mrPoolId rec
|
||||||
_ <- execute conn
|
_ <- execute conn
|
||||||
"UPDATE pools SET used_bytes = used_bytes - ? + ?, \
|
"UPDATE pools SET used_bytes = used_bytes - ? + ?, \
|
||||||
|
|
@ -470,13 +699,18 @@ finalizeSuccess conn rec finalSize finalSha = do
|
||||||
finalizeFailure :: Connection -> MutationRecord -> IO Bool
|
finalizeFailure :: Connection -> MutationRecord -> IO Bool
|
||||||
finalizeFailure conn rec = do
|
finalizeFailure conn rec = do
|
||||||
rows <- query conn
|
rows <- query conn
|
||||||
"SELECT state, external_state, growth_bytes FROM mutations \
|
"SELECT state, external_state, growth_bytes, prior_size FROM mutations \
|
||||||
\WHERE user_id = ? AND kind = ? AND operation_id = ? FOR UPDATE"
|
\WHERE user_id = ? AND kind = ? AND operation_id = ? FOR UPDATE"
|
||||||
(unUserId (mrUserId rec), kindLabel (mrKind rec), unOperationId (mrOperationId rec))
|
(unUserId (mrUserId rec), kindLabel (mrKind rec), unOperationId (mrOperationId rec))
|
||||||
:: IO [(Text, Text, Integer)]
|
:: IO [SettlementRow]
|
||||||
case rows of
|
case rows of
|
||||||
[(st, ext, growth)]
|
[SettlementRow
|
||||||
| st == "applying" && ext == "settled" -> do
|
(DbMutationState state)
|
||||||
|
(DbExternalState external)
|
||||||
|
growth
|
||||||
|
_]
|
||||||
|
| decodeMutationPhase state external
|
||||||
|
== Just (PhaseApplying ExtSettled) -> do
|
||||||
_ <- execute conn
|
_ <- execute conn
|
||||||
"UPDATE pools SET reserved_bytes = reserved_bytes - ? WHERE pool_id = ?"
|
"UPDATE pools SET reserved_bytes = reserved_bytes - ? WHERE pool_id = ?"
|
||||||
(growth, unPoolId (mrPoolId rec))
|
(growth, unPoolId (mrPoolId rec))
|
||||||
|
|
@ -515,38 +749,35 @@ claimActiveBatch conn limit graceSec = do
|
||||||
\FOR UPDATE SKIP LOCKED \
|
\FOR UPDATE SKIP LOCKED \
|
||||||
\LIMIT ?"
|
\LIMIT ?"
|
||||||
(graceSec, graceSec, limit)
|
(graceSec, graceSec, limit)
|
||||||
pure (mapMaybe rowToRecord rows)
|
decodeRows rows
|
||||||
|
|
||||||
desiredMatches :: MutationRecord -> ObjectObservation -> Bool
|
desiredMatches :: MutationRecord -> ObjectObservation -> Bool
|
||||||
desiredMatches rec = \case
|
desiredMatches rec observation =
|
||||||
ObjectUnavailable _ -> False
|
case (desiredObject rec, observation) of
|
||||||
ObjectAbsent ->
|
(_, ObjectUnavailable _) -> False
|
||||||
case mrKind rec of
|
(DesiredAbsent, ObjectAbsent) -> True
|
||||||
Delete -> True
|
(DesiredPresent size expectedSha, ObjectPresent observedSize observedSha) ->
|
||||||
Put -> False
|
size == observedSize
|
||||||
ObjectPresent sz msha ->
|
&& case (expectedSha, observedSha) of
|
||||||
case mrKind rec of
|
(Just want, Just got) -> want == got
|
||||||
Delete -> False
|
(Just _, Nothing) -> False
|
||||||
Put ->
|
(Nothing, _) -> True
|
||||||
sz == mrDeclaredSize rec
|
_ -> False
|
||||||
&& case (mrContentSha256 rec, msha) of
|
|
||||||
(Just want, Just got) -> want == got
|
|
||||||
(Just _, Nothing) -> False
|
|
||||||
(Nothing, _) -> True
|
|
||||||
|
|
||||||
priorMatches :: MutationRecord -> ObjectObservation -> Bool
|
priorMatches :: MutationRecord -> ObjectObservation -> Bool
|
||||||
priorMatches rec obs =
|
priorMatches rec obs =
|
||||||
let priorEmpty = mrPriorSize rec == 0 && mrPriorSha256 rec == Nothing
|
let PriorObject priorSize priorSha = mrPriorObject rec
|
||||||
|
priorEmpty = priorSize == 0 && priorSha == Nothing
|
||||||
in case obs of
|
in case obs of
|
||||||
ObjectUnavailable _ -> False
|
ObjectUnavailable _ -> False
|
||||||
ObjectAbsent -> priorEmpty
|
ObjectAbsent -> priorEmpty
|
||||||
ObjectPresent sz msha ->
|
ObjectPresent sz msha ->
|
||||||
sz == mrPriorSize rec
|
sz == priorSize
|
||||||
&& ( case (mrPriorSha256 rec, msha) of
|
&& ( case (priorSha, msha) of
|
||||||
(Just want, Just got) -> want == got
|
(Just want, Just got) -> want == got
|
||||||
(Nothing, Nothing) -> True
|
(Nothing, Nothing) -> True
|
||||||
(Just _, Nothing) -> False
|
(Just _, Nothing) -> False
|
||||||
(Nothing, Just _) -> mrPriorSize rec == sz
|
(Nothing, Just _) -> priorSize == sz
|
||||||
)
|
)
|
||||||
|
|
||||||
finalizeFromObservation
|
finalizeFromObservation
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ main = do
|
||||||
migrate database "sql" config
|
migrate database "sql" config
|
||||||
verifySchema database
|
verifySchema database
|
||||||
verifyAdmission database config
|
verifyAdmission database config
|
||||||
|
verifyRowDecoder database
|
||||||
store <- newS3 config
|
store <- newS3 config
|
||||||
verifyStorage config store
|
verifyStorage config store
|
||||||
putStrLn "Storage foundation integration gate passed"
|
putStrLn "Storage foundation integration gate passed"
|
||||||
|
|
@ -160,7 +161,8 @@ verifyAdmission database config = do
|
||||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))
|
||||||
settled <- withTransaction database $ \connection -> do
|
settled <- withTransaction database $ \connection -> do
|
||||||
applying <- markApplying connection observationRecord
|
applying <- markApplying connection observationRecord
|
||||||
pending <- markExternalPending connection observationRecord 600 90
|
pending <- markExternalPending
|
||||||
|
connection observationRecord (DispatchTiming 600 90)
|
||||||
externalSettled <- markExternalSettled connection observationRecord
|
externalSettled <- markExternalSettled connection observationRecord
|
||||||
pure (applying && pending && externalSettled)
|
pure (applying && pending && externalSettled)
|
||||||
assert "observation fixture reached applying/settled" settled
|
assert "observation fixture reached applying/settled" settled
|
||||||
|
|
@ -193,7 +195,7 @@ verifyIdempotencyExpiry database = do
|
||||||
let rec = preparedRecord [first]
|
let rec = preparedRecord [first]
|
||||||
finalized <- withTransaction database $ \connection -> do
|
finalized <- withTransaction database $ \connection -> do
|
||||||
applying <- markApplying connection rec
|
applying <- markApplying connection rec
|
||||||
pending <- markExternalPending connection rec 600 90
|
pending <- markExternalPending connection rec (DispatchTiming 600 90)
|
||||||
settled <- markExternalSettled connection rec
|
settled <- markExternalSettled connection rec
|
||||||
succeeded <- finalizeSuccess connection rec 10 (Just fixedIdentity)
|
succeeded <- finalizeSuccess connection rec 10 (Just fixedIdentity)
|
||||||
pure (applying && pending && settled && succeeded)
|
pure (applying && pending && settled && succeeded)
|
||||||
|
|
@ -230,6 +232,40 @@ verifyIdempotencyExpiry database = do
|
||||||
assertEqual "aged active key remains locked" 1 (countRejected [blocked])
|
assertEqual "aged active key remains locked" 1 (countRejected [blocked])
|
||||||
assertAuthority database 1 10
|
assertAuthority database 1 10
|
||||||
|
|
||||||
|
verifyRowDecoder :: Db -> IO ()
|
||||||
|
verifyRowDecoder database = do
|
||||||
|
resetAuthority database 100
|
||||||
|
outcome <- admit database "invalid-phase-operation" "invalid-phase-key" 10
|
||||||
|
let record = preparedRecord [outcome]
|
||||||
|
roundTripped <- withTransaction database $ \connection ->
|
||||||
|
lookupMutation
|
||||||
|
connection
|
||||||
|
(mrUserId record)
|
||||||
|
(mrKind record)
|
||||||
|
(mrOperationId record)
|
||||||
|
assertEqual "typed mutation row round-trip" (Just record) roundTripped
|
||||||
|
_ <- Db.execute database
|
||||||
|
"UPDATE mutations SET external_state = 'pending', \
|
||||||
|
\dispatch_started_at = now(), request_deadline_at = now(), \
|
||||||
|
\worker_lease_until = now() \
|
||||||
|
\WHERE user_id = ? AND kind = ? AND operation_id = ?"
|
||||||
|
( unUserId (mrUserId record)
|
||||||
|
, kindLabel (mrKind record)
|
||||||
|
, unOperationId (mrOperationId record)
|
||||||
|
)
|
||||||
|
invalidDecode <-
|
||||||
|
( try $ withTransaction database $ \connection ->
|
||||||
|
lookupMutation
|
||||||
|
connection
|
||||||
|
(mrUserId record)
|
||||||
|
(mrKind record)
|
||||||
|
(mrOperationId record)
|
||||||
|
) :: IO (Either SomeException (Maybe MutationRecord))
|
||||||
|
case invalidDecode of
|
||||||
|
Left _ -> pure ()
|
||||||
|
Right value ->
|
||||||
|
fail ("invalid persisted mutation phase decoded as " <> show value)
|
||||||
|
|
||||||
verifyStorage :: Config -> S3 -> IO ()
|
verifyStorage :: Config -> S3 -> IO ()
|
||||||
verifyStorage config store = do
|
verifyStorage config store = do
|
||||||
let fallbackKey = ObjectKey "shared/fallback.bin"
|
let fallbackKey = ObjectKey "shared/fallback.bin"
|
||||||
|
|
@ -322,16 +358,21 @@ admitWithWindow database operation key size window =
|
||||||
withTransaction database $ \connection ->
|
withTransaction database $ \connection ->
|
||||||
admitMutation
|
admitMutation
|
||||||
connection
|
connection
|
||||||
(UserId "integration-user")
|
AdmissionRequest
|
||||||
Put
|
{ admissionIdentity =
|
||||||
(OperationId operation)
|
AdmissionIdentity (UserId "integration-user") Put
|
||||||
sharedPool
|
(OperationId operation)
|
||||||
(ObjectKey ("shared/" <> key))
|
, admissionPoolId = sharedPool
|
||||||
(RelativePath key)
|
, admissionRelativePath = RelativePath key
|
||||||
size
|
, admissionFingerprint =
|
||||||
(Just fixedIdentity)
|
MutationFingerprint
|
||||||
1000
|
Put
|
||||||
window
|
(ObjectKey ("shared/" <> key)) size (Just fixedIdentity)
|
||||||
|
}
|
||||||
|
AdmissionLimits
|
||||||
|
{ admissionMaxObjectBytes = 1000
|
||||||
|
, admissionIdempotencyWindowSec = window
|
||||||
|
}
|
||||||
|
|
||||||
resetAuthority :: Db -> Integer -> IO ()
|
resetAuthority :: Db -> Integer -> IO ()
|
||||||
resetAuthority database limitBytes = do
|
resetAuthority database limitBytes = do
|
||||||
|
|
|
||||||
|
|
@ -457,7 +457,7 @@ dispatch :: Db -> MutationRecord -> IO ()
|
||||||
dispatch database record = do
|
dispatch database record = do
|
||||||
ok <- withTransaction database $ \connection -> do
|
ok <- withTransaction database $ \connection -> do
|
||||||
applying <- markApplying connection record
|
applying <- markApplying connection record
|
||||||
pending <- markExternalPending connection record 10 5
|
pending <- markExternalPending connection record (DispatchTiming 10 5)
|
||||||
pure (applying && pending)
|
pure (applying && pending)
|
||||||
assert "PrepareEffect/DispatchEffect" ok
|
assert "PrepareEffect/DispatchEffect" ok
|
||||||
coveredAction "PrepareEffect"
|
coveredAction "PrepareEffect"
|
||||||
|
|
@ -498,48 +498,59 @@ admitPut cfg database operation path size identity =
|
||||||
withTransaction database $ \connection ->
|
withTransaction database $ \connection ->
|
||||||
admitMutation
|
admitMutation
|
||||||
connection
|
connection
|
||||||
protocolUser
|
AdmissionRequest
|
||||||
Put
|
{ admissionIdentity =
|
||||||
(OperationId operation)
|
AdmissionIdentity protocolUser Put (OperationId operation)
|
||||||
sharedPool
|
, admissionPoolId = sharedPool
|
||||||
(ObjectKey ("shared/" <> path))
|
, admissionRelativePath = RelativePath path
|
||||||
(RelativePath path)
|
, admissionFingerprint =
|
||||||
size
|
MutationFingerprint Put (ObjectKey ("shared/" <> path)) size
|
||||||
(Just identity)
|
(Just identity)
|
||||||
(cfgMaxObjectBytes cfg)
|
}
|
||||||
(cfgIdempotencyWindowSec cfg)
|
AdmissionLimits
|
||||||
|
{ admissionMaxObjectBytes = cfgMaxObjectBytes cfg
|
||||||
|
, admissionIdempotencyWindowSec = cfgIdempotencyWindowSec cfg
|
||||||
|
}
|
||||||
|
|
||||||
admitDelete :: Config -> Db -> Text -> Text -> IO AdmitOutcome
|
admitDelete :: Config -> Db -> Text -> Text -> IO AdmitOutcome
|
||||||
admitDelete cfg database operation path =
|
admitDelete cfg database operation path =
|
||||||
withTransaction database $ \connection ->
|
withTransaction database $ \connection ->
|
||||||
admitMutation
|
admitMutation
|
||||||
connection
|
connection
|
||||||
protocolUser
|
AdmissionRequest
|
||||||
Delete
|
{ admissionIdentity =
|
||||||
(OperationId operation)
|
AdmissionIdentity protocolUser Delete (OperationId operation)
|
||||||
sharedPool
|
, admissionPoolId = sharedPool
|
||||||
(ObjectKey ("shared/" <> path))
|
, admissionRelativePath = RelativePath path
|
||||||
(RelativePath path)
|
, admissionFingerprint =
|
||||||
0
|
MutationFingerprint Delete (ObjectKey ("shared/" <> path)) 0 Nothing
|
||||||
Nothing
|
}
|
||||||
(cfgMaxObjectBytes cfg)
|
AdmissionLimits
|
||||||
(cfgIdempotencyWindowSec cfg)
|
{ admissionMaxObjectBytes = cfgMaxObjectBytes cfg
|
||||||
|
, admissionIdempotencyWindowSec = cfgIdempotencyWindowSec cfg
|
||||||
|
}
|
||||||
|
|
||||||
admitPersonal :: Config -> Db -> UserId -> Text -> IO AdmitOutcome
|
admitPersonal :: Config -> Db -> UserId -> Text -> IO AdmitOutcome
|
||||||
admitPersonal cfg database user operation =
|
admitPersonal cfg database user operation =
|
||||||
withTransaction database $ \connection ->
|
withTransaction database $ \connection ->
|
||||||
admitMutation
|
admitMutation
|
||||||
connection
|
connection
|
||||||
user
|
AdmissionRequest
|
||||||
Put
|
{ admissionIdentity =
|
||||||
(OperationId operation)
|
AdmissionIdentity user Put (OperationId operation)
|
||||||
(personalPool user)
|
, admissionPoolId = personalPool user
|
||||||
(ObjectKey ("users/" <> unUserId user <> "/file.bin"))
|
, admissionRelativePath = RelativePath "file.bin"
|
||||||
(RelativePath "file.bin")
|
, admissionFingerprint =
|
||||||
9
|
MutationFingerprint
|
||||||
(Just identityA)
|
Put
|
||||||
(cfgMaxObjectBytes cfg)
|
(ObjectKey ("users/" <> unUserId user <> "/file.bin"))
|
||||||
(cfgIdempotencyWindowSec cfg)
|
9
|
||||||
|
(Just identityA)
|
||||||
|
}
|
||||||
|
AdmissionLimits
|
||||||
|
{ admissionMaxObjectBytes = cfgMaxObjectBytes cfg
|
||||||
|
, admissionIdempotencyWindowSec = cfgIdempotencyWindowSec cfg
|
||||||
|
}
|
||||||
|
|
||||||
seedObject :: Db -> S3 -> Text -> BL.ByteString -> IO ()
|
seedObject :: Db -> S3 -> Text -> BL.ByteString -> IO ()
|
||||||
seedObject database store path body = do
|
seedObject database store path body = do
|
||||||
|
|
|
||||||
|
|
@ -112,16 +112,15 @@ preparePut database redis name size identity = do
|
||||||
outcome <- withTransaction database $ \connection ->
|
outcome <- withTransaction database $ \connection ->
|
||||||
admitMutation
|
admitMutation
|
||||||
connection
|
connection
|
||||||
recoveryUser
|
AdmissionRequest
|
||||||
Put
|
{ admissionIdentity =
|
||||||
(phaseOperation name)
|
AdmissionIdentity recoveryUser Put (phaseOperation name)
|
||||||
sharedPool
|
, admissionPoolId = sharedPool
|
||||||
(phaseKey name)
|
, admissionRelativePath = RelativePath (name <> ".bin")
|
||||||
(RelativePath (name <> ".bin"))
|
, admissionFingerprint =
|
||||||
size
|
MutationFingerprint Put (phaseKey name) size (Just identity)
|
||||||
(Just identity)
|
}
|
||||||
(1024 * 1024 * 1024)
|
(AdmissionLimits (1024 * 1024 * 1024) 3600)
|
||||||
3600
|
|
||||||
prepared outcome
|
prepared outcome
|
||||||
|
|
||||||
prepareDelete :: Config -> Db -> Redis -> S3 -> IO MutationRecord
|
prepareDelete :: Config -> Db -> Redis -> S3 -> IO MutationRecord
|
||||||
|
|
@ -146,16 +145,18 @@ prepareDelete cfg database redis store = do
|
||||||
outcome <- withTransaction database $ \connection ->
|
outcome <- withTransaction database $ \connection ->
|
||||||
admitMutation
|
admitMutation
|
||||||
connection
|
connection
|
||||||
recoveryUser
|
AdmissionRequest
|
||||||
Delete
|
{ admissionIdentity =
|
||||||
(phaseOperation name)
|
AdmissionIdentity recoveryUser Delete (phaseOperation name)
|
||||||
sharedPool
|
, admissionPoolId = sharedPool
|
||||||
key
|
, admissionRelativePath = RelativePath (name <> ".bin")
|
||||||
(RelativePath (name <> ".bin"))
|
, admissionFingerprint =
|
||||||
0
|
MutationFingerprint Delete key 0 Nothing
|
||||||
Nothing
|
}
|
||||||
(cfgMaxObjectBytes cfg)
|
AdmissionLimits
|
||||||
(cfgIdempotencyWindowSec cfg)
|
{ admissionMaxObjectBytes = cfgMaxObjectBytes cfg
|
||||||
|
, admissionIdempotencyWindowSec = cfgIdempotencyWindowSec cfg
|
||||||
|
}
|
||||||
prepared outcome
|
prepared outcome
|
||||||
|
|
||||||
beginDispatch :: Config -> Db -> MutationRecord -> IO ()
|
beginDispatch :: Config -> Db -> MutationRecord -> IO ()
|
||||||
|
|
@ -165,8 +166,10 @@ beginDispatch cfg database rec = do
|
||||||
pending <- markExternalPending
|
pending <- markExternalPending
|
||||||
connection
|
connection
|
||||||
rec
|
rec
|
||||||
(cfgObjectRequestTimeoutSec cfg * 2)
|
DispatchTiming
|
||||||
(cfgRecoveryGraceSec cfg)
|
{ dispatchSettlementBoundSec = cfgObjectRequestTimeoutSec cfg * 2
|
||||||
|
, dispatchWorkerGraceSec = cfgRecoveryGraceSec cfg
|
||||||
|
}
|
||||||
pure (applying && pending)
|
pure (applying && pending)
|
||||||
unless started (fail "could not begin dispatch")
|
unless started (fail "could not begin dispatch")
|
||||||
|
|
||||||
|
|
|
||||||
91
test/Spec.hs
91
test/Spec.hs
|
|
@ -101,12 +101,21 @@ import Nuthatch.Paths
|
||||||
, listCacheAncestorPrefixes
|
, listCacheAncestorPrefixes
|
||||||
)
|
)
|
||||||
import Nuthatch.Protocol
|
import Nuthatch.Protocol
|
||||||
( ExistingDisposition (..)
|
( AdmissionIdentity (..)
|
||||||
|
, ExistingDisposition (..)
|
||||||
|
, MutationFingerprint (..)
|
||||||
|
, MutationPhase (..)
|
||||||
, MutationRecord (..)
|
, MutationRecord (..)
|
||||||
, ObjectObservation (..)
|
, ObjectObservation (..)
|
||||||
|
, PriorObject (..)
|
||||||
|
, TerminalResult (..)
|
||||||
|
, decodeMutationPhase
|
||||||
, desiredMatches
|
, desiredMatches
|
||||||
|
, encodeMutationPhase
|
||||||
, existingDisposition
|
, existingDisposition
|
||||||
, growthBytes
|
, growthBytes
|
||||||
|
, mrContentSha256
|
||||||
|
, mrObjectKey
|
||||||
, priorMatches
|
, priorMatches
|
||||||
, quotaAllows
|
, quotaAllows
|
||||||
, recoveryEligible
|
, recoveryEligible
|
||||||
|
|
@ -307,20 +316,50 @@ identityTests = testGroup "identities"
|
||||||
|
|
||||||
protocolTests :: TestTree
|
protocolTests :: TestTree
|
||||||
protocolTests = testGroup "mutation protocol decisions"
|
protocolTests = testGroup "mutation protocol decisions"
|
||||||
[ testProperty "request fingerprints distinguish every bound field" propFingerprintDistinction
|
[ testCase "protocol phases round-trip through persisted state labels" $
|
||||||
|
mapM_
|
||||||
|
(\phase -> do
|
||||||
|
let (state, external) = encodeMutationPhase phase
|
||||||
|
decodeMutationPhase state external @?= Just phase)
|
||||||
|
[ PhasePrepared
|
||||||
|
, PhaseApplying ExtIdle
|
||||||
|
, PhaseApplying ExtPending
|
||||||
|
, PhaseApplying ExtSettled
|
||||||
|
, PhaseTerminal TerminalSucceeded
|
||||||
|
, PhaseTerminal TerminalFailed
|
||||||
|
, PhaseTerminal TerminalRejected
|
||||||
|
]
|
||||||
|
, testCase "invalid persisted protocol phase combinations are rejected" $
|
||||||
|
mapM_
|
||||||
|
(\(state, external) ->
|
||||||
|
decodeMutationPhase state external @?= Nothing)
|
||||||
|
[ (Prepared, ExtPending)
|
||||||
|
, (Prepared, ExtSettled)
|
||||||
|
, (Succeeded, ExtPending)
|
||||||
|
, (Succeeded, ExtSettled)
|
||||||
|
, (Failed, ExtPending)
|
||||||
|
, (Failed, ExtSettled)
|
||||||
|
, (Rejected, ExtPending)
|
||||||
|
, (Rejected, ExtSettled)
|
||||||
|
]
|
||||||
|
, testProperty "request fingerprints distinguish every bound field" propFingerprintDistinction
|
||||||
, testCase "removed terminal identity starts a new mutation epoch" $ do
|
, testCase "removed terminal identity starts a new mutation epoch" $ do
|
||||||
let rec = sampleMutation
|
let rec = sampleMutation
|
||||||
Put
|
Put
|
||||||
(ObjectKey "users/u/file")
|
(ObjectKey "users/u/file")
|
||||||
4
|
4
|
||||||
(Just (ContentSha256 "content"))
|
(Just (ContentSha256 "content"))
|
||||||
case existingDisposition Nothing Put (mrObjectKey rec) 4 (mrContentSha256 rec) of
|
let fingerprint =
|
||||||
|
MutationFingerprint Put (mrObjectKey rec) 4 (mrContentSha256 rec)
|
||||||
|
case existingDisposition Nothing fingerprint of
|
||||||
NewMutationEpoch -> pure ()
|
NewMutationEpoch -> pure ()
|
||||||
other -> assertFailure ("expected new epoch, got " <> show other)
|
other -> assertFailure ("expected new epoch, got " <> show other)
|
||||||
case existingDisposition (Just rec) Put (mrObjectKey rec) 4 (mrContentSha256 rec) of
|
case existingDisposition (Just rec) fingerprint of
|
||||||
ExistingReplay MutationPending _ -> pure ()
|
ExistingReplay MutationPending _ -> pure ()
|
||||||
other -> assertFailure ("expected replay, got " <> show other)
|
other -> assertFailure ("expected replay, got " <> show other)
|
||||||
case existingDisposition (Just rec) Put (mrObjectKey rec) 5 (mrContentSha256 rec) of
|
case existingDisposition
|
||||||
|
(Just rec)
|
||||||
|
(fingerprint { fingerprintDeclaredSize = 5 }) of
|
||||||
ExistingConflict -> pure ()
|
ExistingConflict -> pure ()
|
||||||
other -> assertFailure ("expected conflict, got " <> show other)
|
other -> assertFailure ("expected conflict, got " <> show other)
|
||||||
, testProperty "quota growth and admission preserve the limit" propQuotaInvariant
|
, testProperty "quota growth and admission preserve the limit" propQuotaInvariant
|
||||||
|
|
@ -334,8 +373,8 @@ protocolTests = testGroup "mutation protocol decisions"
|
||||||
, testCase "ambiguous same-size content matches no terminal state" $ do
|
, testCase "ambiguous same-size content matches no terminal state" $ do
|
||||||
let rec = (sampleMutation Put (ObjectKey "shared/file") 10
|
let rec = (sampleMutation Put (ObjectKey "shared/file") 10
|
||||||
(Just (ContentSha256 "desired")))
|
(Just (ContentSha256 "desired")))
|
||||||
{ mrPriorSize = 10
|
{ mrPriorObject =
|
||||||
, mrPriorSha256 = Just (ContentSha256 "prior")
|
PriorObject 10 (Just (ContentSha256 "prior"))
|
||||||
}
|
}
|
||||||
observation = ObjectPresent 10 (Just (ContentSha256 "other"))
|
observation = ObjectPresent 10 (Just (ContentSha256 "other"))
|
||||||
desiredMatches rec observation @?= False
|
desiredMatches rec observation @?= False
|
||||||
|
|
@ -1105,12 +1144,19 @@ propFingerprintDistinction = property $ do
|
||||||
otherPathKey = buildObjectKey Personal uid (RelativePath ("other/" <> raw))
|
otherPathKey = buildObjectKey Personal uid (RelativePath ("other/" <> raw))
|
||||||
sha = Just (ContentSha256 "content-a")
|
sha = Just (ContentSha256 "content-a")
|
||||||
rec = sampleMutation Put key size sha
|
rec = sampleMutation Put key size sha
|
||||||
assert (sameFingerprint rec Put key size sha)
|
let fingerprint = MutationFingerprint Put key size sha
|
||||||
assert (not (sameFingerprint rec Delete key size sha))
|
assert (sameFingerprint rec fingerprint)
|
||||||
assert (not (sameFingerprint rec Put otherRootKey size sha))
|
assert (not (sameFingerprint rec
|
||||||
assert (not (sameFingerprint rec Put otherPathKey size sha))
|
(fingerprint { fingerprintKind = Delete })))
|
||||||
assert (not (sameFingerprint rec Put key (size + 1) sha))
|
assert (not (sameFingerprint rec
|
||||||
assert (not (sameFingerprint rec Put key size (Just (ContentSha256 "content-b"))))
|
(fingerprint { fingerprintObjectKey = otherRootKey })))
|
||||||
|
assert (not (sameFingerprint rec
|
||||||
|
(fingerprint { fingerprintObjectKey = otherPathKey })))
|
||||||
|
assert (not (sameFingerprint rec
|
||||||
|
(fingerprint { fingerprintDeclaredSize = size + 1 })))
|
||||||
|
assert (not (sameFingerprint rec
|
||||||
|
(fingerprint
|
||||||
|
{ fingerprintContentSha256 = Just (ContentSha256 "content-b") })))
|
||||||
|
|
||||||
propQuotaInvariant :: Property
|
propQuotaInvariant :: Property
|
||||||
propQuotaInvariant = property $ do
|
propQuotaInvariant = property $ do
|
||||||
|
|
@ -1140,9 +1186,7 @@ propObservations = property $ do
|
||||||
let desiredSha = Just (ContentSha256 "desired")
|
let desiredSha = Just (ContentSha256 "desired")
|
||||||
priorSha = if priorSize == 0 then Nothing else Just (ContentSha256 "prior")
|
priorSha = if priorSize == 0 then Nothing else Just (ContentSha256 "prior")
|
||||||
rec = (sampleMutation kind (ObjectKey "users/u/file") desiredSize desiredSha)
|
rec = (sampleMutation kind (ObjectKey "users/u/file") desiredSize desiredSha)
|
||||||
{ mrPriorSize = priorSize
|
{ mrPriorObject = PriorObject priorSize priorSha }
|
||||||
, mrPriorSha256 = priorSha
|
|
||||||
}
|
|
||||||
desiredMatches rec observation === modelDesired kind desiredSize desiredSha observation
|
desiredMatches rec observation === modelDesired kind desiredSize desiredSha observation
|
||||||
priorMatches rec observation === modelPrior priorSize priorSha observation
|
priorMatches rec observation === modelPrior priorSize priorSha observation
|
||||||
|
|
||||||
|
|
@ -1257,17 +1301,12 @@ sampleMutation
|
||||||
-> Maybe ContentSha256
|
-> Maybe ContentSha256
|
||||||
-> MutationRecord
|
-> MutationRecord
|
||||||
sampleMutation kind key size sha = MutationRecord
|
sampleMutation kind key size sha = MutationRecord
|
||||||
{ mrUserId = UserId "u"
|
{ mrAdmissionIdentity =
|
||||||
, mrKind = kind
|
AdmissionIdentity (UserId "u") kind (OperationId "op")
|
||||||
, mrOperationId = OperationId "op"
|
|
||||||
, mrPoolId = PoolId "personal:u"
|
, mrPoolId = PoolId "personal:u"
|
||||||
, mrObjectKey = key
|
|
||||||
, mrRelativePath = RelativePath "file"
|
, mrRelativePath = RelativePath "file"
|
||||||
, mrDeclaredSize = size
|
, mrFingerprint = MutationFingerprint kind key size sha
|
||||||
, mrContentSha256 = sha
|
, mrPriorObject = PriorObject 0 Nothing
|
||||||
, mrPriorSize = 0
|
|
||||||
, mrPriorSha256 = Nothing
|
|
||||||
, mrGrowth = size
|
, mrGrowth = size
|
||||||
, mrState = Applying
|
, mrPhase = PhaseApplying ExtSettled
|
||||||
, mrExternal = ExtSettled
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue