Skip to content

Commit 8ce9679

Browse files
authored
Merge pull request #1394 from Kleidukos/store-uploaders-in-a-cache
Move uploader out of acid-state
2 parents 1bbcc36 + cf1d244 commit 8ce9679

File tree

3 files changed

+110
-376
lines changed

3 files changed

+110
-376
lines changed

hackage-server.cabal

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,6 @@ library
365365
Distribution.Server.Features.HaskellPlatform
366366
Distribution.Server.Features.HaskellPlatform.State
367367
Distribution.Server.Features.PackageInfoJSON
368-
Distribution.Server.Features.PackageInfoJSON.State
369368
Distribution.Server.Features.Search
370369
Distribution.Server.Features.Search.BM25F
371370
Distribution.Server.Features.Search.DocIdSet
Lines changed: 110 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{-# LANGUAGE RankNTypes #-}
2+
{-# LANGUAGE OverloadedRecordDot #-}
23
{-# LANGUAGE RecordWildCards #-}
34
{-# LANGUAGE ScopedTypeVariables #-}
5+
{-# LANGUAGE NamedFieldPuns #-}
46

57
module Distribution.Server.Features.PackageInfoJSON (
68
PackageInfoJSONFeature(..)
@@ -15,48 +17,102 @@ import Prelude ()
1517
import Distribution.Server.Prelude
1618

1719
import qualified Data.Aeson as Aeson
20+
import Data.Aeson ((.=))
21+
import qualified Data.Aeson.Key as Key
1822
import qualified Data.ByteString.Lazy.Char8 as BS (toStrict)
23+
import qualified Data.Map.Strict as Map
1924
import qualified Data.Text as T
2025
import qualified Data.Vector as Vector
2126

2227
import Distribution.License (licenseToSPDX)
2328
import Distribution.Package (PackageIdentifier(..),
24-
PackageName, packageName,
2529
packageVersion)
2630
import qualified Distribution.Parsec as Parsec
2731
import qualified Distribution.PackageDescription.Parsec as PkgDescr
32+
import Distribution.Text (display)
2833
import qualified Distribution.Types.GenericPackageDescription as PkgDescr
2934
import qualified Distribution.Types.PackageDescription as PkgDescr
30-
import Distribution.Version (nullVersion)
35+
import qualified Distribution.Pretty as Pretty
36+
import Distribution.SPDX.License (License)
37+
import Distribution.Version (nullVersion, Version)
3138

32-
import Distribution.Server.Framework ((</>))
33-
import qualified Distribution.Server.Framework as Framework
34-
import Distribution.Server.Features.Core (CoreFeature(..),
35-
CoreResource(..),
36-
isPackageChangeAny)
39+
import qualified Distribution.Server.Framework as Framework
40+
import Distribution.Server.Features.Core (CoreFeature(..),
41+
CoreResource(..))
3742
import qualified Distribution.Server.Features.PreferredVersions as Preferred
38-
import Distribution.Server.Packages.Types (CabalFileText(..), pkgMetadataRevisions)
39-
import Distribution.Server.Framework.BackupRestore (RestoreBackup(..))
40-
41-
import Distribution.Server.Features.PackageInfoJSON.State (PackageBasicDescription(..),
42-
PackageVersions(..),
43-
PackageInfoState(..),
44-
GetPackageInfo(..),
45-
ReplacePackageInfo(..),
46-
GetDescriptionFor(..),
47-
SetDescriptionFor(..),
48-
GetVersionsFor(..),
49-
SetVersionsFor(..),
50-
initialPackageInfoState
51-
)
43+
import Distribution.Server.Packages.Types (CabalFileText(..), pkgMetadataRevisions)
44+
5245
import Distribution.Utils.ShortText (fromShortText)
5346
import Data.Foldable (toList)
5447
import Data.Traversable (for)
5548
import qualified Data.List as List
5649
import Data.Time (UTCTime)
57-
import Distribution.Server.Users.Types (UserName, UserInfo(..))
50+
import Distribution.Server.Users.Types (UserName (..), UserInfo(..))
5851
import Distribution.Server.Features.Users (UserFeature(lookupUserInfo))
5952

53+
data PackageBasicDescription = PackageBasicDescription
54+
{ pbd_license :: !License
55+
, pbd_copyright :: !T.Text
56+
, pbd_synopsis :: !T.Text
57+
, pbd_description :: !T.Text
58+
, pbd_author :: !T.Text
59+
, pbd_homepage :: !T.Text
60+
, pbd_metadata_revision :: !Int
61+
, pbd_uploaded_at :: !UTCTime
62+
} deriving (Eq, Show)
63+
64+
65+
66+
-- | Data type used in the `/package/:packagename` JSON endpoint
67+
data PackageBasicDescriptionDTO = PackageBasicDescriptionDTO
68+
{ license :: !License
69+
, copyright :: !T.Text
70+
, synopsis :: !T.Text
71+
, description :: !T.Text
72+
, author :: !T.Text
73+
, homepage :: !T.Text
74+
, metadata_revision :: !Int
75+
, uploaded_at :: !UTCTime
76+
, uploader :: !UserName
77+
} deriving (Eq, Show)
78+
79+
instance Aeson.ToJSON PackageBasicDescriptionDTO where
80+
toJSON PackageBasicDescriptionDTO {..} =
81+
Aeson.object
82+
[ Key.fromString "license" .= Pretty.prettyShow license
83+
, Key.fromString "copyright" .= copyright
84+
, Key.fromString "synopsis" .= synopsis
85+
, Key.fromString "description" .= description
86+
, Key.fromString "author" .= author
87+
, Key.fromString "homepage" .= homepage
88+
, Key.fromString "metadata_revision" .= metadata_revision
89+
, Key.fromString "uploaded_at" .= uploaded_at
90+
, Key.fromString "uploader" .= uploader
91+
]
92+
93+
94+
-- | An index of versions for one Hackage package
95+
-- and their preferred/deprecated status
96+
newtype PackageVersions = PackageVersions {
97+
unPackageVersions :: [(Version, Preferred.VersionStatus)]
98+
} deriving (Eq, Show)
99+
100+
-- | This encoding of @PackageVersions@ is used in the
101+
-- `/package/$package` endpoint (when the URI doesn't specify)
102+
-- a version. Any change here is an API change.
103+
instance Aeson.ToJSON PackageVersions where
104+
toJSON (PackageVersions p) =
105+
Aeson.toJSON
106+
$ Map.mapKeys display
107+
$ fmap encodeStatus
108+
$ Map.fromList p
109+
where
110+
encodeStatus = \case
111+
Preferred.NormalVersion -> "normal"
112+
Preferred.DeprecatedVersion -> "deprecated"
113+
Preferred.UnpreferredVersion -> "unpreferred"
114+
115+
60116

61117
data PackageInfoJSONFeature = PackageInfoJSONFeature {
62118
packageInfoJSONFeatureInterface :: Framework.HackageFeature
@@ -76,14 +132,10 @@ data PackageInfoJSONResource = PackageInfoJSONResource {
76132
-- | Initializing our feature involves adding JSON variants to the
77133
-- endpoints that serve basic information about a package-version,
78134
-- and a packages version deprecation status.
79-
-- Additionally we set up caching for these endpoints,
80-
-- and attach a package change hook that invalidates the cache
81-
-- line for a package when it changes
82135
initPackageInfoJSONFeature
83136
:: Framework.ServerEnv
84137
-> IO (CoreFeature -> Preferred.VersionsFeature -> UserFeature -> IO PackageInfoJSONFeature)
85-
initPackageInfoJSONFeature env = do
86-
packageInfoState <- packageInfoStateComponent False (Framework.serverStateDir env)
138+
initPackageInfoJSONFeature _env = do
87139
return $ \core preferred userFeature -> do
88140

89141
let coreR = coreResource core
@@ -97,53 +149,38 @@ initPackageInfoJSONFeature env = do
97149
Framework.resourceDesc = [(Framework.GET, info)]
98150
, Framework.resourceGet =
99151
[("json", servePackageBasicDescription coreR userFeature
100-
preferred packageInfoState)]
152+
preferred)]
101153
}
102154
, (Framework.extendResource (coreCabalFileRev coreR)) {
103155
Framework.resourceDesc = [(Framework.GET, vInfo)]
104156
, Framework.resourceGet =
105157
[("json", servePackageBasicDescription coreR userFeature
106-
preferred packageInfoState)]
158+
preferred)]
107159
}
108160
]
109161

110-
-- When a package is modified in any way, delet all its
111-
-- PackageInfoState cache lines.
112-
-- They will be recalculated next time the endpoint
113-
-- is hit
114-
postInit = Framework.registerHookJust
115-
(packageChangeHook core)
116-
isPackageChangeAny $ \(pkgid, _) -> do
117-
118-
Framework.updateState packageInfoState $
119-
SetDescriptionFor (pkgid, Nothing) Nothing
120-
Framework.updateState packageInfoState $
121-
SetVersionsFor (packageName pkgid) Nothing
122-
123162
return $ PackageInfoJSONFeature {
124163
packageInfoJSONFeatureInterface =
125164
(Framework.emptyHackageFeature "package-info-json")
126165
{ Framework.featureDesc = "Provide JSON endpoints for basic package descriptions"
127166
, Framework.featureResources = jsonResources
128167
, Framework.featureCaches = []
129-
, Framework.featurePostInit = postInit
130-
, Framework.featureState =
131-
[Framework.abstractAcidStateComponent packageInfoState]
168+
, Framework.featurePostInit = pure ()
169+
, Framework.featureState = []
132170
}
133171
}
134172

135173

136174
-- | Pure function for extracting basic package info from a Cabal file
137175
getBasicDescription
138-
:: UserName
139-
-> UTCTime
176+
:: UTCTime
140177
-- ^ Time of upload
141178
-> CabalFileText
142179
-> Int
143180
-- ^ Metadata revision. This will be added to the resulting
144181
-- @PackageBasicDescription@
145182
-> Either String PackageBasicDescription
146-
getBasicDescription uploader uploadedAt (CabalFileText cf) metadataRev =
183+
getBasicDescription uploadedAt (CabalFileText cf) metadataRev =
147184
let parseResult = PkgDescr.parseGenericPackageDescription (BS.toStrict cf)
148185
in case PkgDescr.runParseResult parseResult of
149186
(_, Right pkg) -> let
@@ -157,14 +194,26 @@ getBasicDescription uploader uploadedAt (CabalFileText cf) metadataRev =
157194
pbd_homepage = T.pack . fromShortText $ PkgDescr.homepage pkgd
158195
pbd_metadata_revision = metadataRev
159196
pbd_uploaded_at = uploadedAt
160-
pbd_uploader = uploader
161197
in
162198
return $ PackageBasicDescription {..}
163199
(_, Left (_, perrs)) ->
164200
let errs = List.intersperse '\n' $ mconcat $ for (toList perrs) $ \err -> Parsec.showPError "" err
165201
in Left $ "Could not parse cabal file: "
166202
<> errs
167203

204+
basicDescriptionToDTO :: UserName -> PackageBasicDescription -> PackageBasicDescriptionDTO
205+
basicDescriptionToDTO uploader d =
206+
PackageBasicDescriptionDTO
207+
{ license = d.pbd_license
208+
, copyright = d.pbd_copyright
209+
, synopsis = d.pbd_synopsis
210+
, description = d.pbd_description
211+
, author = d.pbd_author
212+
, homepage = d.pbd_homepage
213+
, metadata_revision = d.pbd_metadata_revision
214+
, uploaded_at = d.pbd_uploaded_at
215+
, uploader
216+
}
168217

169218
-- | Get a JSON @PackageBasicDescription@ for a particular
170219
-- package/version/metadata-revision
@@ -174,48 +223,38 @@ servePackageBasicDescription
174223
:: CoreResource
175224
-> UserFeature
176225
-> Preferred.VersionsFeature
177-
-> Framework.StateComponent Framework.AcidState PackageInfoState
178226
-> Framework.DynamicPath
179227
-- ^ URI specifying a package and version `e.g. lens or lens-4.11`
180228
-> Framework.ServerPartE Framework.Response
181-
servePackageBasicDescription resource userFeature preferred packageInfoState dpath = do
229+
servePackageBasicDescription resource userFeature preferred dpath = do
182230

183231
let metadataRev :: Maybe Int = lookup "revision" dpath >>= Framework.fromReqURI
184232

185233
pkgid@(PackageIdentifier name version) <- packageInPath resource dpath
186234
guardValidPackageName resource name
187235

188236
if version /= nullVersion
189-
then lookupOrInsertDescr pkgid metadataRev
190-
else lookupOrInsertVersions name
237+
then fetchDescr pkgid metadataRev
238+
else Framework.toResponse . Aeson.toJSON <$> getVersionListing name
191239

192240
where
193241

194-
lookupOrInsertDescr
242+
fetchDescr
195243
:: PackageIdentifier
196244
-> Maybe Int
197245
-> Framework.ServerPartE Framework.Response
198-
lookupOrInsertDescr pkgid metadataRev = do
199-
cachedDescr <- Framework.queryState packageInfoState $
200-
GetDescriptionFor (pkgid, metadataRev)
201-
descr :: PackageBasicDescription <- case cachedDescr of
202-
Just d -> return d
203-
Nothing -> do
204-
d <- getPackageDescr pkgid metadataRev
205-
Framework.updateState packageInfoState $
206-
SetDescriptionFor (pkgid, metadataRev) (Just d)
207-
return d
208-
return $ Framework.toResponse $ Aeson.toJSON descr
209-
210-
getPackageDescr pkgid metadataRev = do
246+
fetchDescr pkgid metadataRev = do
211247
guardValidPackageId resource pkgid
212248
pkg <- lookupPackageId resource pkgid
213249

214250
let metadataRevs = fst <$> pkgMetadataRevisions pkg
215251
uploadInfos = snd <$> pkgMetadataRevisions pkg
216252
nMetadata = Vector.length metadataRevs
217253
metadataInd = fromMaybe (nMetadata - 1) metadataRev
254+
descr <- getPackageDescr metadataInd nMetadata metadataRevs uploadInfos
255+
return $ Framework.toResponse $ Aeson.toJSON descr
218256

257+
getPackageDescr metadataInd nMetadata metadataRevs uploadInfos = do
219258
when (metadataInd < 0 || metadataInd >= nMetadata)
220259
(Framework.errNotFound "Revision not found"
221260
[Framework.MText
@@ -227,25 +266,12 @@ servePackageBasicDescription resource userFeature preferred packageInfoState dpa
227266
uploadedAt = fst $ uploadInfos Vector.! metadataInd
228267
uploaderId = snd $ uploadInfos Vector.! metadataInd
229268
uploader <- userName <$> lookupUserInfo userFeature uploaderId
230-
let pkgDescr = getBasicDescription uploader uploadedAt cabalFile metadataInd
269+
let pkgDescr = getBasicDescription uploadedAt cabalFile metadataInd
231270
case pkgDescr of
232271
Left e -> Framework.errInternalError [Framework.MText e]
233-
Right d -> return d
234-
235-
lookupOrInsertVersions
236-
:: PackageName
237-
-> Framework.ServerPartE Framework.Response
238-
lookupOrInsertVersions pkgname = do
239-
cachedVersions <- Framework.queryState packageInfoState $
240-
GetVersionsFor pkgname
241-
vers :: PackageVersions <- case cachedVersions of
242-
Just vs -> return vs
243-
Nothing -> do
244-
vs <- getVersionListing pkgname
245-
Framework.updateState packageInfoState $
246-
SetVersionsFor pkgname (Just vs)
247-
return vs
248-
return $ Framework.toResponse $ Aeson.toJSON vers
272+
Right d -> do
273+
let packageInfoDTO = basicDescriptionToDTO uploader d
274+
return packageInfoDTO
249275

250276
getVersionListing name = do
251277
pkgs <- lookupPackageName resource name
@@ -254,34 +280,3 @@ servePackageBasicDescription resource userFeature preferred packageInfoState dpa
254280
. PackageVersions
255281
. Preferred.classifyVersions prefInfo
256282
$ fmap packageVersion pkgs
257-
258-
259-
-- | Our backup doesn't produce any entries, and backup restore
260-
-- returns an empty state. Our responses are cheap enough to
261-
-- compute that we would rather regenerate them by need than
262-
-- deal with the complexity persisting backups in
263-
-- yet-another-format
264-
packageInfoStateComponent
265-
:: Bool
266-
-> FilePath
267-
-> IO (Framework.StateComponent Framework.AcidState PackageInfoState)
268-
packageInfoStateComponent freshDB stateDir = do
269-
st <- Framework.openLocalStateFrom
270-
(stateDir </> "db" </> "PackageInfoJSON")
271-
(initialPackageInfoState freshDB)
272-
return Framework.StateComponent {
273-
stateDesc = "Preferred package versions"
274-
, stateHandle = st
275-
, getState = Framework.query st GetPackageInfo
276-
, putState = Framework.update st . ReplacePackageInfo
277-
, resetState = packageInfoStateComponent True
278-
, backupState = \_ -> return []
279-
, restoreState = nullRestore (initialPackageInfoState True)
280-
}
281-
where
282-
283-
nullRestore :: PackageInfoState -> RestoreBackup PackageInfoState
284-
nullRestore st = RestoreBackup {
285-
restoreEntry = \_ -> nullRestore <$> pure (initialPackageInfoState True)
286-
, restoreFinalize = return st
287-
}

0 commit comments

Comments
 (0)