refactor(avs): rewrite AVS synch (WIP)

This commit is contained in:
Steffen Jost 2024-04-24 18:01:44 +02:00
parent a52c8a6ad7
commit 2e4e1a94c9
6 changed files with 105 additions and 37 deletions

View File

@ -29,6 +29,6 @@ UserAvs
AvsSync AvsSync
user UserId -- Note: we need to lookup UserAvs Entity anyway, so no benefit from storing AvsPersonId here user UserId -- Note: we need to lookup UserAvs Entity anyway, so no benefit from storing AvsPersonId here
creationTime UTCTime creationTime UTCTime
pause Day Maybe pause Day Maybe -- Don't synch if last synch after this day, otherwise synch
UniqueAvsSyncUser user UniqueAvsSyncUser user
deriving Generic deriving Generic

View File

@ -52,6 +52,7 @@ module Database.Esqueleto.Utils
, day, day', dayMaybe, interval, diffDays, diffTimes , day, day', dayMaybe, interval, diffDays, diffTimes
, exprLift , exprLift
, explicitUnsafeCoerceSqlExprValue , explicitUnsafeCoerceSqlExprValue
, truncateTable
, module Database.Esqueleto.Utils.TH , module Database.Esqueleto.Utils.TH
) where ) where
@ -68,6 +69,8 @@ import qualified Database.Esqueleto.PostgreSQL as E
import qualified Database.Esqueleto.Internal.Internal as E import qualified Database.Esqueleto.Internal.Internal as E
import Database.Esqueleto.Utils.TH import Database.Esqueleto.Utils.TH
import qualified Database.Persist.Postgresql as P
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Lazy as Lazy (Text) import qualified Data.Text.Lazy as Lazy (Text)
import qualified Data.ByteString.Lazy as Lazy (ByteString) import qualified Data.ByteString.Lazy as Lazy (ByteString)
@ -768,3 +771,7 @@ instance (PersistField a1, PersistField a2, PersistField b, Finite a1, Finite a2
] ]
(E.else_ $ E.else_ $ E.veryUnsafeCoerceSqlExprValue (E.nothing :: E.SqlExpr (E.Value (Maybe ())))) (E.else_ $ E.else_ $ E.veryUnsafeCoerceSqlExprValue (E.nothing :: E.SqlExpr (E.Value (Maybe ()))))
truncateTable :: (MonadIO m, BackendCompatible SqlBackend backend, PersistEntity record)
=> record -> ReaderT backend m ()
truncateTable tbl = E.rawExecute ("TRUNCATE TABLE " <> P.tableName tbl <> " RESTART IDENTITY") []

View File

@ -12,7 +12,10 @@
module Handler.Utils.Avs module Handler.Utils.Avs
( guessAvsUser ( guessAvsUser
, upsertAvsUserById, upsertAvsUserByCard , upsertAvsUserByCard
, upsertAvsUserById
, updateAvsUserByIds
, linktoAvsUserByUIDs
-- , getLicence, getLicenceDB, getLicenceByAvsId -- not supported by interface -- , getLicence, getLicenceDB, getLicenceByAvsId -- not supported by interface
, AvsLicenceDifferences(..) , AvsLicenceDifferences(..)
, setLicence, setLicenceAvs, setLicencesAvs , setLicence, setLicenceAvs, setLicencesAvs
@ -312,20 +315,49 @@ updateRecord ent new (CheckAvsUpdate up l) =
lensRec = fieldLensVal up lensRec = fieldLensVal up
in ent & lensRec .~ newval in ent & lensRec .~ newval
-- | Update given AvsPersonId by querying AVS for each; update only, no insertion! Uses batch mechanism, but single query may throw -- | shall not throw, updates exisitng and attempts to link users with yet unknown AVSIDs
updateAvsUserByIds :: Set AvsPersonId -> DB (Set (AvsPersonId, UserId))
updateAvsUserByIds apids0 = do
apids <- Set.fromList <$> E.filterExists UserAvsPersonId apids0
linktoAvsUserByUIDs :: Set UserId -> Handler ()
linktoAvsUserByUIDs = error "TODO: Not yet implemented."
-- | Like `updateAvsUserByIds`, but exceptions are not caught here to allow rollbacks
updateAvsUserById :: AvsPersonId -> DB (Maybe UserId)
updateAvsUserById apid = do
AvsResponseContact adcs <- avsQuery $ AvsQueryContact $ Set.singleton $ AvsObjPersonId apid
let res = Set.filter ((== apid) . avsContactPersonID) adcs
snd <<$>> traverseJoin updateAvsUserByADC (Set.lookupMax res)
-- | Update given AvsPersonIds by querying AVS for each; update only, no insertion! Uses batch mechanism abd should not throw. Each user dealt within own runDB, i.e. own DB transaction
updateAvsUserByIds :: Set AvsPersonId -> Handler (Set (AvsPersonId, UserId))
updateAvsUserByIds apids = do
-- apids <- Set.fromList <$> E.filterExists UserAvsPersonId apids0 --not needed anymore, we expect the set to be linked
AvsResponseContact adcs <- avsQuery $ AvsQueryContact $ Set.mapMonotonic AvsObjPersonId apids -- automatically batched AvsResponseContact adcs <- avsQuery $ AvsQueryContact $ Set.mapMonotonic AvsObjPersonId apids -- automatically batched
let requestedAnswers = Set.filter (view (_avsContactPersonID . to (`Set.member` apids))) adcs -- should not occur, neither should one apid occur multiple times within the response (if so, all responses processed here in random order) let requestedAnswers = Set.filter (view (_avsContactPersonID . to (`Set.member` apids))) adcs -- should not occur, neither should one apid occur multiple times within the response (if so, all responses processed here in random order)
res <- foldMapM procResp requestedAnswers (oks,bad) <- foldlM procResp mempty requestedAnswers
let missing = Set.toList $ Set.difference apids $ Set.map fst res let missing = Set.toList $ Set.difference (Set.difference apids $ Set.map fst oks) bad
unless (null missing) $ do unless (null missing) $ do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
updateWhere [UserAvsPersonId <-. missing] [UserAvsLastSynch =. now, UserAvsLastSynchError =. Just "Contact unknown for AvsPersonId"] -- all others were already marked as updated runDB $ updateWhere [UserAvsPersonId <-. missing] [UserAvsLastSynch =. now, UserAvsLastSynchError =. Just "Avs contact info unknown for AvsPersonId"] -- all others were already marked as updated
return res return oks
where where
procResp (AvsDataContact apid newAvsPersonInfo newAvsFirmInfo) = fmap maybeMonoid . runMaybeT $ do procResp :: (Set (AvsPersonId, UserId), Set AvsPersonId) -> AvsDataContact -> Handler (Set (AvsPersonId, UserId), Set AvsPersonId)
procResp (accOk, accBad) adc = do
let errHandler e = runDB $ do
let apid = avsContactPersonID adc
now <- liftIO getCurrentTime
updateBy (UniqueUserAvsId apid) [UserAvsLastSynch =. now, UserAvsLastSynchError =. Just (tshow e)]
return (accOk, Set.insert apid accBad)
updateAvsUserByADC' :: DB (Set (AvsPersonId, UserId), Set AvsPersonId)
updateAvsUserByADC' = do
res <- updateAvsUserByADC adc
return (maybeInsert res accOk, accBad)
catchAll (runDB updateAvsUserByADC') errHandler
updateAvsUserByADC :: AvsDataContact -> DB (Maybe (AvsPersonId, UserId))
updateAvsUserByADC (AvsDataContact apid newAvsPersonInfo newAvsFirmInfo) = runMaybeT $ do
(Entity uaId usravs) <- MaybeT $ getBy $ UniqueUserAvsId apid (Entity uaId usravs) <- MaybeT $ getBy $ UniqueUserAvsId apid
let usrId = userAvsUser usravs let usrId = userAvsUser usravs
usr <- MaybeT $ get usrId usr <- MaybeT $ get usrId
@ -420,7 +452,7 @@ updateAvsUserByIds apids0 = do
repsertSuperiorSupervisor (Just newCompanyId) newAvsFirmInfo usrId -- ensure firmInfo superior is at least normal supervisor, must be executed after updating company default supervisors repsertSuperiorSupervisor (Just newCompanyId) newAvsFirmInfo usrId -- ensure firmInfo superior is at least normal supervisor, must be executed after updating company default supervisors
update usrId $ usr_up2 `mcons` usr_up1 -- update user eventually update usrId $ usr_up2 `mcons` usr_up1 -- update user eventually
update uaId avs_ups -- update stored avsinfo for future updates update uaId avs_ups -- update stored avsinfo for future updates
return $ Set.singleton (apid, usrId) return (apid, usrId)
-- createAvsUserById :: Set AvsPersonId -> Handler (Set (AvsPersonId, UserId)) ??? -- createAvsUserById :: Set AvsPersonId -> Handler (Set (AvsPersonId, UserId)) ???
-- | Create new user from AVS-Id. Will throw an AvsException if this is not possible, e.g. due to Uniqueness Constraints -- | Create new user from AVS-Id. Will throw an AvsException if this is not possible, e.g. due to Uniqueness Constraints
@ -461,12 +493,8 @@ createAvsUserById api = do
| otherwise -> throwM $ AvsUserUnknownByAvs api | otherwise -> throwM $ AvsUserUnknownByAvs api
(Just uid, Nothing) -> runDB $ do -- link with matching exisitng user (Just uid, Nothing) -> runDB $ do -- link with matching exisitng user
insert_ $ usrAvs uid Nothing -- company info should cause the user to be associated with the company during the update insert_ $ usrAvs uid Nothing -- company info should cause the user to be associated with the company during the update
updRes <- updateAvsUserByIds $ Set.singleton api -- no loop, since updateAvsUserByIds does not call createAvsUserById updRes <- updateAvsUserById api -- no loop, since updateAvsUserById does not call createAvsUserById
case Set.toList updRes of maybe (throwM $ AvsUserUnknownByAvs api) return updRes
[(api',uid')] | api == api' -> return uid' -- && uid == uid' -> return uid
| otherwise -> throwM $ AvsIdMismatch api api'
[] -> throwM $ AvsUserUnknownByAvs api
_ -> throwM $ AvsUserAmbiguous api
(Nothing, Nothing) -> do (Nothing, Nothing) -> do
Entity{entityKey=cid, entityVal=cmp} <- runDB $ upsertAvsCompany firmInfo Nothing -- individual runDB, since no need to rollback Entity{entityKey=cid, entityVal=cmp} <- runDB $ upsertAvsCompany firmInfo Nothing -- individual runDB, since no need to rollback
let pinPass = avsFullCardNo2pin <$> usrCardNo let pinPass = avsFullCardNo2pin <$> usrCardNo
@ -617,15 +645,10 @@ upsertAvsUserByCard persNo = do
-- Throws errors if the avsInterface in unavailable or new user would violate uniqueness constraints -- Throws errors if the avsInterface in unavailable or new user would violate uniqueness constraints
upsertAvsUserById :: AvsPersonId -> Handler UserId upsertAvsUserById :: AvsPersonId -> Handler UserId
upsertAvsUserById api = do upsertAvsUserById api = do
upd <- runDB (updateAvsUserByIds $ Set.singleton api) upd <- runDB (updateAvsUserById api)
case Set.toList upd of case upd of
[] -> createAvsUserById api Nothing -> createAvsUserById api
[(api',uid)] (Just uid) -> return uid
| api == api' -> return uid
| otherwise -> throwM $ AvsIdMismatch api api'
-- error $ "Handler.Utils.Avs.updateAvsUserByIds returned unasked user with AvsPersonId " <> show api' <> " for queried AvsPersonId " <> show api <> "."
(_:_:_) -> throwM $ AvsUserAmbiguous api
-- Licences -- Licences
setLicence :: (PersistUniqueRead backend, MonadThrow m, setLicence :: (PersistUniqueRead backend, MonadThrow m,

View File

@ -6,21 +6,27 @@ module Jobs.Handler.SynchroniseAvs
( dispatchJobSynchroniseAvs ( dispatchJobSynchroniseAvs
, dispatchJobSynchroniseAvsId , dispatchJobSynchroniseAvsId
, dispatchJobSynchroniseAvsUser , dispatchJobSynchroniseAvsUser
, dispatchJobSynchroniseAvsNext , dispatchJobSynchroniseAvsNext -- internal only
, dispatchJobSynchroniseAvsQueue , dispatchJobSynchroniseAvsQueue -- internal only
, dispatchJobSynchroniseAvsQueue' -- internal only TODO replace unprimed
) where ) where
import Import import Import
import qualified Data.Set as Set
import qualified Data.Conduit.List as C
import Database.Esqueleto.Experimental ((:&)(..))
import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
-- import qualified Database.Esqueleto.Legacy as E hiding (upsert) -- import qualified Database.Esqueleto.Legacy as E hiding (upsert)
-- import qualified Database.Esqueleto.PostgreSQL as E -- import qualified Database.Esqueleto.PostgreSQL as E
-- import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
import qualified Data.Conduit.List as C
import Jobs.Queue import Jobs.Queue
import Handler.Utils.Avs import Handler.Utils.Avs
-- pause is a date in the past; don't synch again if the last synch was after pause
dispatchJobSynchroniseAvs :: Natural -> Natural -> Natural -> Maybe Day -> JobHandler UniWorX dispatchJobSynchroniseAvs :: Natural -> Natural -> Natural -> Maybe Day -> JobHandler UniWorX
dispatchJobSynchroniseAvs numIterations epoch iteration pause dispatchJobSynchroniseAvs numIterations epoch iteration pause
= JobHandlerException . runDB $ do = JobHandlerException . runDB $ do
@ -105,3 +111,30 @@ dispatchJobSynchroniseAvsQueue = JobHandlerException $ do
-- needed, since JobSynchroniseAvsQueue cannot requeue itself due to JobNoQueueSame (and having no parameters) -- needed, since JobSynchroniseAvsQueue cannot requeue itself due to JobNoQueueSame (and having no parameters)
dispatchJobSynchroniseAvsNext :: JobHandler UniWorX dispatchJobSynchroniseAvsNext :: JobHandler UniWorX
dispatchJobSynchroniseAvsNext = JobHandlerException $ void $ queueJob JobSynchroniseAvsQueue dispatchJobSynchroniseAvsNext = JobHandlerException $ void $ queueJob JobSynchroniseAvsQueue
dispatchJobSynchroniseAvsQueue' :: JobHandler UniWorX
dispatchJobSynchroniseAvsQueue' = JobHandlerException $ do
(unlinked,linked) <- runDB $ do
jobs <- E.select (do
(avsSync :& usrAvs) <- E.from $ E.table @AvsSync
`E.leftJoin` E.table @UserAvs
`E.on` (\(avsSync :& usrAvs) -> avsSync E.^. AvsSyncUser E.=?. usrAvs E.?. UserAvsUser)
let pause = avsSync E.^. AvsSyncPause
lastSync = usrAvs E.?. UserAvsLastSynch
E.where_ $ E.isNothing pause
E.||. E.isNothing lastSync
E.||. pause E.>. E.dayMaybe lastSync
return (avsSync E.^. AvsSyncId, avsSync E.^. AvsSyncUser, usrAvs E.?. UserAvsPersonId)
)
let (syncIds, unlinked, linked) = foldl' discernJob mempty jobs
E.deleteWhere [AvsSyncId <-. syncIds]
return (unlinked, linked)
void $ updateAvsUserByIds linked
void $ linktoAvsUserByUIDs unlinked
-- we do not reschedule failed synchs here in order to avoid a loop
where
discernJob (accSync, accUid, accApi) (E.Value k, _, E.Value (Just api)) = (k:accSync, accUid, Set.insert api accApi)
discernJob (accSync, accUid, accApi) (E.Value k, E.Value uid, E.Value Nothing ) = (k:accSync, Set.insert uid accUid, accApi)

View File

@ -30,14 +30,15 @@ import GHC.Stack (HasCallStack, CallStack, callStack)
-- import Control.Monad.Trans.Reader (withReaderT) -- import Control.Monad.Trans.Reader (withReaderT)
-- | Obtain the record projection from the EntityField value -- | Obtain a record projection from an EntityField
getFieldEnt :: PersistEntity record => EntityField record typ -> Entity record -> typ getFieldEnt :: PersistEntity record => EntityField record typ -> Entity record -> typ
getFieldEnt = view . fieldLens getFieldEnt = view . fieldLens
getField :: PersistEntity record => EntityField record typ -> record -> typ getField :: PersistEntity record => EntityField record typ -> record -> typ
getField = view . fieldLensVal getField = view . fieldLensVal
fieldLensVal :: PersistEntity record => EntityField record field -> Lens' record field -- | Obtain a lens from an EntityField
fieldLensVal :: PersistEntity record => EntityField record typ -> Lens' record typ
fieldLensVal f = entityLens . fieldLens f fieldLensVal f = entityLens . fieldLens f
where where
entityLens :: Lens' record (Entity record) entityLens :: Lens' record (Entity record)
@ -45,7 +46,7 @@ fieldLensVal f = entityLens . fieldLens f
getVal :: record -> Entity record getVal :: record -> Entity record
getVal = Entity (error "fieldLensVal unexpectectly required an entity key") -- this is safe, since the lens is only used locally getVal = Entity (error "fieldLensVal unexpectectly required an entity key") -- this is safe, since the lens is only used locally
setVal :: record -> Entity record -> record setVal :: record -> Entity record -> record
setVal _ = entityVal setVal _ = entityVal
emptyOrIn :: PersistField typ emptyOrIn :: PersistField typ

View File

@ -13,6 +13,7 @@ module Utils.Set
, setFromFunc , setFromFunc
, mapIntersectNotOne , mapIntersectNotOne
, set2NonEmpty , set2NonEmpty
, maybeInsert
) where ) where
import qualified Data.List.NonEmpty as NonEmpty import qualified Data.List.NonEmpty as NonEmpty
@ -81,8 +82,11 @@ setPartitionEithers = (,) <$> setMapMaybeMonotonic (preview _Left) <*> setMapMay
setFromFunc :: (Finite k, Ord k) => (k -> Bool) -> Set k setFromFunc :: (Finite k, Ord k) => (k -> Bool) -> Set k
setFromFunc = Set.fromList . flip filter universeF setFromFunc = Set.fromList . flip filter universeF
-- | convert a Set to NonEmpty, inserting a default value if necessary -- | convert a Set to NonEmpty, inserting a default value if necessary
set2NonEmpty :: a -> Set a -> NonEmpty.NonEmpty a set2NonEmpty :: a -> Set a -> NonEmpty.NonEmpty a
set2NonEmpty _ (Set.toList -> h:t) = h NonEmpty.:| t set2NonEmpty _ (Set.toList -> h:t) = h NonEmpty.:| t
set2NonEmpty d _ = d NonEmpty.:| [] set2NonEmpty d _ = d NonEmpty.:| []
maybeInsert :: Ord a => Maybe a -> Set a -> Set a
maybeInsert Nothing = id
maybeInsert (Just k) = Set.insert k