From d4c9e156ece9d1254cd8451a82e48d19e7b21a12 Mon Sep 17 00:00:00 2001 From: Samuel Evans-Powell Date: Mon, 17 May 2021 09:09:26 +0800 Subject: [PATCH 1/9] Fix treatment of JSON Values vs JSON strings --- token-metadata-creator/app/Config.hs | 35 +++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/token-metadata-creator/app/Config.hs b/token-metadata-creator/app/Config.hs index 7a7a3fb..fb4148e 100644 --- a/token-metadata-creator/app/Config.hs +++ b/token-metadata-creator/app/Config.hs @@ -84,7 +84,7 @@ data Arguments argumentParser :: Maybe Subject -> OA.Parser Arguments argumentParser defaultSubject = - OA.hsubparser + OA.hsubparser ( OA.command "entry" (OA.info (ArgumentsEntryUpdate <$> entryUpdateArgumentParser defaultSubject) mempty) <> OA.command "validate" (OA.info (ArgumentsValidate <$> pFileA <*> pFileB <*> pLogSeverity) mempty) ) @@ -154,7 +154,7 @@ entryUpdateArgumentParser defaultSubject = EntryUpdateArguments <*> pure Nothing -- logo <*> optional (emptyAttested <$> wellKnownOption (OA.long "url" <> OA.short 'h' <> OA.metavar "URL")) <*> optional (emptyAttested <$> wellKnownOption (OA.long "ticker" <> OA.short 't' <> OA.metavar "TICKER")) - <*> optional (emptyAttested <$> OA.option (OA.eitherReader ((Aeson.parseEither parseWellKnown =<<) . Aeson.eitherDecodeStrict . BC8.pack)) (OA.long "decimals" <> OA.metavar "DECIMALS")) + <*> optional (emptyAttested <$> wellKnownOption (OA.long "decimals" <> OA.metavar "DECIMALS")) pLogSeverity :: OA.Parser Colog.Severity pLogSeverity = pDebug <|> pInfo <|> pWarning <|> pError <|> pure I @@ -196,8 +196,31 @@ wellKnownOption => OA.Mod OA.OptionFields p -> OA.Parser p wellKnownOption = - OA.option wellKnownReader + OA.option (OA.eitherReader (\arg -> + let + r1 = asJSONValue arg + in + if isPotentiallyString r1 + then asJSONString arg + else r1 + )) where - wellKnownReader :: OA.ReadM p - wellKnownReader = OA.eitherReader $ - Aeson.parseEither parseWellKnown . Aeson.toJSON + -- Presume we got a value (i.e. 3 or []). + asJSONValue :: String -> Either String p + asJSONValue = (Aeson.parseEither parseWellKnown =<<) . Aeson.eitherDecodeStrict' . BC8.pack + -- Presume we got a string (i.e. "3" or "potato"). + asJSONString :: String -> Either String p + asJSONString = Aeson.parseEither parseWellKnown . Aeson.toJSON + + -- A program called with the arguments "--arg hello" will pass the + -- value "hello" to the JSON parser. "hello" is not a valid JSON + -- value, you might expect it to be treated as a string, but the + -- correct way to pass a string to a JSON parser is "\"hello\"". + -- To handle the cases of both JSON strings and any other JSON + -- value, we first try to parse it as a raw value, then if that + -- fails with an error like "not a valid json value", then we + -- treat the input as a string and try to parse that. + isPotentiallyString :: forall x. Either String x -> Bool + isPotentiallyString (Left err) | "not a valid json value" `isSuffixOf` err = True + isPotentiallyString (Left _) = False + isPotentiallyString (Right _) = False From 8e9cd576ede82cf5c10114f9cc25fb2e44e84d49 Mon Sep 17 00:00:00 2001 From: Samuel Evans-Powell Date: Mon, 17 May 2021 09:39:05 +0800 Subject: [PATCH 2/9] Fix treatment of "number-like" JSON values e.g. "0x" or "0.x" --- token-metadata-creator/app/Config.hs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/token-metadata-creator/app/Config.hs b/token-metadata-creator/app/Config.hs index fb4148e..9ce8d3d 100644 --- a/token-metadata-creator/app/Config.hs +++ b/token-metadata-creator/app/Config.hs @@ -219,8 +219,11 @@ wellKnownOption = -- To handle the cases of both JSON strings and any other JSON -- value, we first try to parse it as a raw value, then if that -- fails with an error like "not a valid json value", then we - -- treat the input as a string and try to parse that. + -- treat the input as a string and try to parse that. This at the + -- very least gives us a better error message. isPotentiallyString :: forall x. Either String x -> Bool isPotentiallyString (Left err) | "not a valid json value" `isSuffixOf` err = True + isPotentiallyString (Left err) | "endOfInput" `isSuffixOf` err = True + isPotentiallyString (Left err) | "takeWhile1" `isSuffixOf` err = True isPotentiallyString (Left _) = False isPotentiallyString (Right _) = False From a0d773d64af9797a0f583d52098572180103ee58 Mon Sep 17 00:00:00 2001 From: Samuel Evans-Powell Date: Mon, 17 May 2021 09:46:21 +0800 Subject: [PATCH 3/9] Make "treat as string" a little more obvious --- token-metadata-creator/app/Config.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token-metadata-creator/app/Config.hs b/token-metadata-creator/app/Config.hs index 9ce8d3d..462887b 100644 --- a/token-metadata-creator/app/Config.hs +++ b/token-metadata-creator/app/Config.hs @@ -210,7 +210,7 @@ wellKnownOption = asJSONValue = (Aeson.parseEither parseWellKnown =<<) . Aeson.eitherDecodeStrict' . BC8.pack -- Presume we got a string (i.e. "3" or "potato"). asJSONString :: String -> Either String p - asJSONString = Aeson.parseEither parseWellKnown . Aeson.toJSON + asJSONString = Aeson.parseEither parseWellKnown . Aeson.String . T.pack -- A program called with the arguments "--arg hello" will pass the -- value "hello" to the JSON parser. "hello" is not a valid JSON From 80ab948af3cce7cb0a294f7605ec46436eae2ab4 Mon Sep 17 00:00:00 2001 From: Samuel Evans-Powell Date: Mon, 17 May 2021 15:32:46 +0800 Subject: [PATCH 4/9] Fix issues with desync between metadata-server and GitHub repository - Adds a script that queries the GH repository and sets the state of metadata database to match. - Add NixOS service. - Add integration tests that check that the write part of the script writes correctly, and doesn't touch the database at all if an exception occurs. --- cabal-nix.project | 1 + cabal.project | 4 + default.nix | 1 + .../src/Test/Cardano/Metadata/Generators.hs | 4 +- metadata-sync/metadata-sync.cabal | 140 +++++++++++++++ metadata-sync/src/Cardano/Metadata/Sync.hs | 50 ++++++ .../src/Cardano/Metadata/Sync/Config.hs | 80 +++++++++ metadata-sync/src/Main.hs | 34 ++++ metadata-sync/test/Main.hs | 165 ++++++++++++++++++ nix/haskell.nix | 5 + nix/nixos/metadata-sync.nix | 131 ++++++++++++++ nix/nixos/module-list.nix | 1 + nix/nixos/tests/default.nix | 1 + nix/nixos/tests/metadata-sync.nix | 93 ++++++++++ release.nix | 1 + 15 files changed, 709 insertions(+), 2 deletions(-) create mode 100644 metadata-sync/metadata-sync.cabal create mode 100644 metadata-sync/src/Cardano/Metadata/Sync.hs create mode 100644 metadata-sync/src/Cardano/Metadata/Sync/Config.hs create mode 100644 metadata-sync/src/Main.hs create mode 100644 metadata-sync/test/Main.hs create mode 100644 nix/nixos/metadata-sync.nix create mode 100644 nix/nixos/tests/metadata-sync.nix diff --git a/cabal-nix.project b/cabal-nix.project index 7b5d558..8a0391d 100644 --- a/cabal-nix.project +++ b/cabal-nix.project @@ -5,5 +5,6 @@ packages: ./metadata-webhook ./metadata-validator-github ./token-metadata-creator + ./metadata-sync tests: True benchmarks: True \ No newline at end of file diff --git a/cabal.project b/cabal.project index e166c1c..166ae1f 100644 --- a/cabal.project +++ b/cabal.project @@ -7,6 +7,7 @@ packages: ./metadata-store-postgres ./metadata-validator-github ./token-metadata-creator + ./metadata-sync package metadata-lib tests: True @@ -26,6 +27,9 @@ package metadata-validator-github package token-metadata-creator tests: True +package metadata-sync + tests: True + -- --------------------------------------------------------- -- Disable all tests belonging to dependencies diff --git a/default.nix b/default.nix index c3b5b87..4d00697 100644 --- a/default.nix +++ b/default.nix @@ -92,6 +92,7 @@ let inherit (project.hsPkgs.metadata-server.identifier) version; inherit (project.hsPkgs.metadata-server.components.exes) metadata-server; inherit (project.hsPkgs.metadata-webhook.components.exes) metadata-webhook; + inherit (project.hsPkgs.metadata-sync.components.exes) metadata-sync; inherit (project.hsPkgs.metadata-validator-github.components.exes) metadata-validator-github; inherit (project.hsPkgs.token-metadata-creator.components.exes) token-metadata-creator; inherit (project) metadata-validator-github-tarball token-metadata-creator-tarball; diff --git a/metadata-lib/src/Test/Cardano/Metadata/Generators.hs b/metadata-lib/src/Test/Cardano/Metadata/Generators.hs index fdf0e17..621f558 100644 --- a/metadata-lib/src/Test/Cardano/Metadata/Generators.hs +++ b/metadata-lib/src/Test/Cardano/Metadata/Generators.hs @@ -246,12 +246,12 @@ validationMetadata' = do validationMetadataSignedWith skey subj propertyName :: MonadGen m => m PropertyName -propertyName = PropertyName <$> Gen.text (Range.linear 1 64) Gen.unicodeAll +propertyName = PropertyName <$> Gen.text (Range.linear 1 64) Gen.unicode propertyValue :: MonadGen m => m Aeson.Value propertyValue = Gen.recursive Gen.choice - [ Aeson.String <$> Gen.text (Range.linear 1 64) Gen.unicodeAll + [ Aeson.String <$> Gen.text (Range.linear 1 64) Gen.unicode , Aeson.Number <$> fromIntegral <$> Gen.word8 Range.constantBounded , Aeson.Bool <$> Gen.bool , pure $ Aeson.Null diff --git a/metadata-sync/metadata-sync.cabal b/metadata-sync/metadata-sync.cabal new file mode 100644 index 0000000..196297a --- /dev/null +++ b/metadata-sync/metadata-sync.cabal @@ -0,0 +1,140 @@ +cabal-version: >=1.10 +name: metadata-sync +version: 0.1.0.0 +author: Samuel Evans-Powell +maintainer: mail@sevanspowell.net +build-type: Simple +extra-source-files: CHANGELOG + +library + hs-source-dirs: src + + exposed-modules: Cardano.Metadata.Sync + Cardano.Metadata.Sync.Config + + build-depends: aeson + , base + , bytestring + , casing + , containers + , directory + , filepath + , postgresql-simple + , lens + , lens-aeson + , metadata-lib + , monad-logger + , mtl + , optparse-applicative + , persistent + , persistent-postgresql + , persistent-template + , resource-pool + , safe-exceptions + , scientific + , servant + , servant-server + , temporary + , text + , time + , turtle + , unordered-containers + , wai + , warp + + ghc-options: -Wall + -Wincomplete-record-updates + -Wincomplete-uni-patterns + -Wincomplete-patterns + -Wredundant-constraints + -Wpartial-fields + -Wcompat + +test-suite integration-tests + hs-source-dirs: test + main-is: Main.hs + type: exitcode-stdio-1.0 + + build-depends: base >=4.12 && <5 + , HUnit + , QuickCheck + , aeson + , aeson-pretty + , base + , bytestring + , casing + , containers + , directory + , hedgehog + , hspec + , http-client + , lens + , lens-aeson + , metadata-lib + , metadata-sync + , monad-logger + , postgresql-simple + , mtl + , raw-strings-qq + , resource-pool + , safe-exceptions + , scientific + , servant + , servant-client + , servant-server + , smallcheck + , tagged + , tasty + , tasty-hedgehog + , tasty-hspec + , tasty-hunit + , tasty-quickcheck + , text + , unordered-containers + , wai + , warp + + ghc-options: -Wall + -Wincomplete-record-updates + -Wincomplete-uni-patterns + -Wincomplete-patterns + -Wredundant-constraints + -Wpartial-fields + -Wcompat + +executable metadata-sync + hs-source-dirs: src + main-is: Main.hs + build-depends: base + , aeson + , bytestring + , containers + , directory + , filepath + , lens + , metadata-lib + , metadata-sync + , monad-logger + , mtl + , optparse-applicative + , persistent + , postgresql-simple + , resource-pool + , safe-exceptions + , scientific + , servant + , temporary + , text + , time + , turtle + , warp + + ghc-options: -Wall + -Wcompat + -fwarn-redundant-constraints + -fwarn-incomplete-patterns + -fwarn-unused-imports + -Wincomplete-record-updates + -Wincomplete-uni-patterns + -Wno-unsafe + -threaded \ No newline at end of file diff --git a/metadata-sync/src/Cardano/Metadata/Sync.hs b/metadata-sync/src/Cardano/Metadata/Sync.hs new file mode 100644 index 0000000..6b7729d --- /dev/null +++ b/metadata-sync/src/Cardano/Metadata/Sync.hs @@ -0,0 +1,50 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Cardano.Metadata.Sync where + +import qualified Data.Aeson as Aeson +import Data.Text (Text) +import qualified Data.Text as T +import qualified Turtle.Prelude as Turtle +import System.Directory (listDirectory) +import System.FilePath.Posix (takeBaseName) +import Data.String (fromString) +import System.IO.Temp (withSystemTempDirectory) +import Database.PostgreSQL.Simple (withTransaction, execute, executeMany, Connection) +import Database.PostgreSQL.Simple.Types (Identifier(..), Only(..), In(..)) +import Data.Traversable (forM) +import Data.Functor (void) + +import Cardano.Metadata.Sync.Config (withConnectionFromPool) +import Cardano.Metadata.Types.Common (Subject(..)) + +-- | View the current state of the registry (source-of-truth). +view :: Text -> Text -> IO [(Subject, Aeson.Value)] +view gitURL gitSubFolder = do + withSystemTempDirectory "metadata-sync" $ \dir -> do + + gitURL `cloneTo` dir + + let dataDir = dir <> "/" <> T.unpack gitSubFolder + ks <- listDirectory dataDir + flip foldMap ks $ \k -> do + mV <- Aeson.decodeFileStrict' (dataDir <> "/" <> k) + case mV of + Nothing -> pure [] + Just v -> pure [(Subject $ T.pack $ takeBaseName k, v)] + + where + emptyDirectory dir = Turtle.procs "rm" ["-r", T.pack dir <> "/*"] mempty + cloneTo gitUrl dir = Turtle.procs "git" ["clone", gitUrl, T.pack dir] mempty + +-- | Write out a new state to our local copy of the registry. +write :: Connection -> Text -> [(Subject, Aeson.Value)] -> IO () +write conn tableName kvs = + withTransaction conn $ do + let table = Identifier tableName + + void $ execute conn "TRUNCATE ?" (Only table) + + let dat = fmap (\(Subject k, v) -> (k, v)) kvs + + void $ executeMany conn ("INSERT INTO " <> fromString (T.unpack tableName) <> " VALUES (?,?)") dat diff --git a/metadata-sync/src/Cardano/Metadata/Sync/Config.hs b/metadata-sync/src/Cardano/Metadata/Sync/Config.hs new file mode 100644 index 0000000..a2d9635 --- /dev/null +++ b/metadata-sync/src/Cardano/Metadata/Sync/Config.hs @@ -0,0 +1,80 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Cardano.Metadata.Sync.Config where + +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import Options.Applicative (Parser, ParserInfo) +import qualified Options.Applicative as Opt +import Options.Applicative (strOption, long, metavar, help, showDefault, value, option, auto, info, fullDesc, progDesc, header) +import Data.Pool (Pool, createPool, destroyAllResources) +import Database.PostgreSQL.Simple (Connection) +import Data.Time.Clock (NominalDiffTime) +import qualified Database.PostgreSQL.Simple as Sql +import qualified Data.Pool as Pool +import qualified Data.ByteString.Char8 as BC +import Control.Exception (bracket) + +data Opts = Opts + { optDbName :: Text + , optDbUser :: Text + , optDbHost :: FilePath + , optDbMetadataTableName :: Text + , optDbConnections :: Int + , optGitURL :: Text + , optGitSubFolder :: Text + } + deriving (Eq, Show) + +parseOpts :: Parser Opts +parseOpts = Opts + <$> strOption (long "db" <> metavar "DB_NAME" <> help "Name of the database to store and read metadata from") + <*> strOption (long "db-user" <> metavar "DB_USER" <> help "User to connect to metadata database with") + <*> strOption (long "db-host" <> metavar "DB_HOST" <> showDefault <> value "/run/postgresql" <> help "Host for the metadata database connection") + <*> strOption (long "db-table" <> metavar "DB_TABLE" <> showDefault <> value "metadata" <> help "Table in the database to store metadata") + <*> option auto (long "db-conns" <> metavar "INT" <> showDefault <> value 1 <> help "Number of connections to open to the database") + <*> strOption (long "git-url" <> metavar "GIT_URL" <> help "URL of the metadata registry git repository") + <*> strOption (long "git-metadata-folder" <> metavar "GIT_METADATA_FOLDER" <> help "Sub-folder of the git repository containing the metadata") + +opts :: ParserInfo Opts +opts = + info + parseOpts + ( fullDesc + <> progDesc "Sync up a metadata database with a GitHub repository" + <> header "metadata-sync - a tool to keep the metadata storage layer and the GitHub repository in sync" + ) + +pgConnectionString :: Opts -> BC.ByteString +pgConnectionString (Opts { optDbName = dbName, optDbUser = dbUser, optDbHost = dbHost }) = + TE.encodeUtf8 $ "host=" <> T.pack dbHost <> " dbname=" <> dbName <> " user=" <> dbUser + +mkConnectionPool + :: BC.ByteString + -- ^ Libpq connection string + -> Int + -- ^ Maximum number of postgresql connections to allow + -> IO (Pool Connection) +mkConnectionPool connectionStr numConns = + createPool + (Sql.connectPostgreSQL connectionStr) + Sql.close + 1 -- Number of sub-pools + (10 :: NominalDiffTime) -- Amount of time for which an unused connection is kept open + numConns + +withConnectionPool + :: BC.ByteString + -- ^ Libpq connection string + -> Int + -- ^ Maximum number of postgresql connections to allow + -> (Pool Connection -> IO r) + -> IO r +withConnectionPool connectionInfo numConns f = bracket + (mkConnectionPool connectionInfo numConns) + destroyAllResources + f + +withConnectionFromPool :: Pool Connection -> (Connection -> IO b) -> IO b +withConnectionFromPool pool action = Pool.withResource pool $ action diff --git a/metadata-sync/src/Main.hs b/metadata-sync/src/Main.hs new file mode 100644 index 0000000..2c5f7ab --- /dev/null +++ b/metadata-sync/src/Main.hs @@ -0,0 +1,34 @@ +module Main where + +import Control.Monad.IO.Class + ( liftIO ) +import qualified Data.ByteString.Char8 as BC +import qualified Data.Text as T +import qualified Network.Wai.Handler.Warp as Warp +import qualified Options.Applicative as Opt +import Control.Exception (bracket) +import Database.PostgreSQL.Simple (Connection, connectPostgreSQL, close) +import Data.Pool (Pool, createPool, destroyAllResources) +import Data.Time.Clock (NominalDiffTime) + +import qualified Cardano.Metadata.Sync as Sync +import Cardano.Metadata.Sync.Config + ( Opts (..), pgConnectionString, parseOpts, withConnectionPool, withConnectionFromPool, opts) + +main :: IO () +main = do + options@(Opts { optDbConnections = numDbConns + , optDbMetadataTableName = tableName + , optGitURL = gitURL + , optGitSubFolder = gitSubFolder + }) <- Opt.execParser opts + + let pgConnString = pgConnectionString options + putStrLn $ "Connecting to database using connection string: " <> BC.unpack pgConnString + withConnectionPool pgConnString numDbConns $ \pool -> do + withConnectionFromPool pool $ \conn -> do + putStrLn $ "Reading registry state from '" <> T.unpack gitURL <> "'." + state <- Sync.view gitURL gitSubFolder + + putStrLn $ "Syncing to table '" <> T.unpack tableName <> "'." + Sync.write conn tableName state diff --git a/metadata-sync/test/Main.hs b/metadata-sync/test/Main.hs new file mode 100644 index 0000000..673f3a5 --- /dev/null +++ b/metadata-sync/test/Main.hs @@ -0,0 +1,165 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} + +import Control.Monad.IO.Class + ( liftIO ) +import Control.Monad.Logger + ( runNoLoggingT ) +import Data.Proxy + ( Proxy (Proxy) ) +import Data.Tagged + ( Tagged, unTagged ) +import Data.Text + ( Text ) +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import qualified Data.Aeson as Aeson +import Data.Word + ( Word8 ) +import Data.Pool (Pool, destroyAllResources) +import Control.Monad (join, void) +import Test.Tasty + ( TestTree + , askOption + , defaultIngredients + , defaultMainWithIngredients + , includingOptions + , testGroup + , withResource + ) +import Test.Tasty.Options +import Hedgehog + ( evalIO, forAll, property, (===), MonadGen ) +import qualified Hedgehog as H + ( Property ) +import Control.Exception (catch, SomeException) +import qualified Hedgehog.Gen as Gen +import qualified Data.HashMap.Strict as HM +import qualified Hedgehog.Range as Range +import Test.Tasty +import Test.Tasty.Hedgehog + +import Cardano.Metadata.Store.Types +import Cardano.Metadata.Types.Common (Subject(..), unPropertyName) +import Cardano.Metadata.Sync.Config (mkConnectionPool, withConnectionFromPool) +import Cardano.Metadata.Sync (write) +import Test.Cardano.Metadata.Generators + ( ComplexType ) +import qualified Test.Cardano.Metadata.Generators as Gen +import Test.Cardano.Metadata.Store +import Database.PostgreSQL.Simple (query, withTransaction, execute, executeMany, Connection) +import Database.PostgreSQL.Simple.Types (Only(..), Identifier(..)) + +newtype DbHost = DbHost { _dbHost :: Text } +newtype DbName = DbName { _dbName :: Text } +newtype DbUser = DbUser { _dbUser :: Text } +newtype DbTableName = DbTableName { _dbTableName :: Text } + +instance IsOption DbHost where + defaultValue = DbHost "/run/postgresql" + parseValue str = Just $ DbHost (T.pack str) + optionName = pure "db-host" + optionHelp = pure "Postgres server hostname or, for UNIX domain sockets, the socket filename." + +instance IsOption DbName where + defaultValue = error $ "'" <> unTagged (optionName :: Tagged DbName String) <> "' is required." + parseValue str = Just $ DbName (T.pack str) + optionName = pure "db-name" + optionHelp = pure "Database name within the postgres server." + +instance IsOption DbUser where + defaultValue = error $ "'" <> unTagged (optionName :: Tagged DbUser String) <> "' is required." + parseValue str = Just $ DbUser (T.pack str) + optionName = pure "db-user" + optionHelp = pure "User with which to authenticate to the postgres server" + +instance IsOption DbTableName where + defaultValue = DbTableName "metadata_integration_test" + parseValue str = Just $ DbTableName (T.pack str) + optionName = pure "db-table" + optionHelp = pure "Postgres database table to submit test data to." + +main :: IO () +main = + defaultMainWithIngredients + ((includingOptions + [ Option (Proxy @DbHost) + , Option (Proxy @DbName) + , Option (Proxy @DbUser) + , Option (Proxy @DbTableName) + ] + ) + : defaultIngredients + ) + testsWithPosgresConn + +testsWithPosgresConn :: TestTree +testsWithPosgresConn = + askOption $ \(DbHost dbHost) -> + askOption $ \(DbName dbName) -> + askOption $ \(DbUser dbUser) -> + askOption $ \(DbTableName dbTableName) -> + withResource + ( (,dbTableName) <$> mkConnectionPool (TE.encodeUtf8 $ "host=" <> dbHost <> " dbname=" <> dbName <> " user=" <> dbUser) 1 ) + ( \(pool, _) -> destroyAllResources pool ) + tests + +tests + :: IO ( Pool Connection, Text ) + -> TestTree +tests resources = + testGroup "integration tests" + [ + testGroup "Metadata sync script" + [ testProperty "write/last-wins" (prop_write_last_wins resources) + , testProperty "write/no-partial-write" (prop_write_no_partial resources) + ] + ] + +prop_write_last_wins :: IO (Pool Connection, Text) -> H.Property +prop_write_last_wins getPool = property $ do + (pool, tableName) <- evalIO getPool + kvs1 <- forAll genKvs + kvs2 <- forAll genKvs + + join $ evalIO $ withConnectionFromPool pool $ \conn -> do + reset conn tableName + result <- write conn tableName kvs1 >> write conn tableName kvs2 >> toList conn tableName + + reset conn tableName + expected <- write conn tableName kvs2 >> toList conn tableName + + pure $ + result === expected + +prop_write_no_partial :: IO (Pool Connection, Text) -> H.Property +prop_write_no_partial getPool = property $ do + (pool, tableName) <- evalIO getPool + kvs <- forAll genKvs + + join $ evalIO $ withConnectionFromPool pool $ \conn -> do + reset conn tableName + result <- (write conn tableName (kvs <> [undefined]) `catch` (\(_ :: SomeException) -> pure ())) >> toList conn tableName + + pure $ + result === [] + +reset :: Connection -> Text -> IO () +reset conn table = do + void $ execute conn "CREATE TABLE IF NOT EXISTS ? (\"key\" VARCHAR PRIMARY KEY UNIQUE, \"value\" JSONB NOT NULL)" (Only $ Identifier table) + void $ execute conn "TRUNCATE ?" (Only $ Identifier table) + +toList :: Connection -> Text -> IO [(Subject, Aeson.Value)] +toList conn table = do + results <- query conn "SELECT key, value FROM ?" (Only $ Identifier table) + + pure $ fmap (\(k, v) -> (Subject k, v)) results + +genKvs :: MonadGen m => m [(Subject, Aeson.Value)] +genKvs = + let + kv = (,) <$> Gen.subject <*> (fmap (Aeson.Object . HM.fromList) $ Gen.list (Range.linear 0 20) ((,) <$> (unPropertyName <$> Gen.propertyName) <*> Gen.propertyValue)) + in + Gen.list (Range.linear 0 15) kv diff --git a/nix/haskell.nix b/nix/haskell.nix index 1346f7e..1c7b673 100644 --- a/nix/haskell.nix +++ b/nix/haskell.nix @@ -38,6 +38,7 @@ let packages.metadata-lib = filterSubDir "metadata-lib"; packages.metadata-server = filterSubDir "metadata-server"; packages.metadata-webhook = filterSubDir "metadata-webhook"; + packages.metadata-sync = filterSubDir "metadata-sync"; packages.metadata-store-postgres = filterSubDir "metadata-store-postgres"; packages.metadata-validator-github = filterSubDir "metadata-validator-github"; } @@ -46,12 +47,14 @@ let packages.metadata-lib.flags.release = true; packages.metadata-server.flags.release = true; packages.metadata-webhook.flags.release = true; + packages.metadata-sync.flags.release = true; packages.metadata-store-postgres.flags.release = true; packages.metadata-validator-github.flags.release = true; } { packages.metadata-server.components.exes.metadata-server.postInstall = optparseCompletionPostInstall; packages.metadata-webhook.components.exes.metadata-webhook.postInstall = optparseCompletionPostInstall; + packages.metadata-sync.components.exes.metadata-sync.postInstall = optparseCompletionPostInstall; packages.metadata-validator-github.components.exes.metadata-validator-github.postInstall = optparseCompletionPostInstall; } # Enable profiling on executables if the profiling argument is set. @@ -59,6 +62,7 @@ let enableLibraryProfiling = true; packages.metadata-server.components.exes.metadata-server.enableExecutableProfiling = true; packages.metadata-webhook.components.exes.metadata-webhook.enableExecutableProfiling = true; + packages.metadata-sync.components.exes.metadata-sync.enableExecutableProfiling = true; packages.metadata-validator-github.components.exes.metadata-validator-github.enableExecutableProfiling = true; }) @@ -106,6 +110,7 @@ let } { packages.metadata-store-postgres.components.tests.integration-tests.doCheck = false; + packages.metadata-sync.components.tests.integration-tests.doCheck = false; } ]; }); diff --git a/nix/nixos/metadata-sync.nix b/nix/nixos/metadata-sync.nix new file mode 100644 index 0000000..7a5b6b7 --- /dev/null +++ b/nix/nixos/metadata-sync.nix @@ -0,0 +1,131 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.metadata-sync; +in { + + options = { + services.metadata-sync = { + enable = lib.mkEnableOption "enable the metadata sync script"; + script = lib.mkOption { + internal = true; + type = lib.types.package; + }; + offchainMetadataToolsPkgs = lib.mkOption { + type = lib.types.attrs; + default = (import ../../. {}).project; + defaultText = "offchain-metadata-tools pkgs"; + description = '' + The offchain-metadata-tools package set. + ''; + internal = true; + }; + package = lib.mkOption { + type = lib.types.package; + default = cfg.offchainMetadataToolsPkgs.metadata-sync.components.exes.metadata-sync; + }; + user = lib.mkOption { + type = lib.types.str; + # Linux OS user can have a hyphen (`-`) along with systemd service names as standard + default = "metadata-sync"; + description = "the user to run as"; + }; + git = { + repositoryUrl = lib.mkOption { + type = lib.types.str; + description = "URL of the git repository of the metadata registry."; + }; + + metadataFolder = lib.mkOption { + type = lib.types.str; + default = "/"; + description = "Folder within the git repository that contains the metadata entries."; + }; + }; + postgres = { + socketdir = lib.mkOption { + type = lib.types.str; + default = "/run/postgresql"; + description = "the path to the postgresql socket"; + }; + port = lib.mkOption { + type = lib.types.int; + default = 5432; + description = "the postgresql port"; + }; + database = lib.mkOption { + type = lib.types.str; + # Postgresql cannot have a hyphen (`-`) + default = "metadata_server"; + description = "the postgresql database to use"; + }; + table = lib.mkOption { + type = lib.types.str; + default = "metadata"; + description = "the postgresql database table to use"; + }; + user = lib.mkOption { + type = lib.types.str; + # Postgresql cannot have a hyphen (`-`) + default = "metadata_user"; + description = "the postgresql user to use"; + }; + numConnections = lib.mkOption { + type = lib.types.int; + default = 1; + description = "the number of connections to open to the postgresql database"; + }; + }; + }; + }; + config = lib.mkIf cfg.enable { + services.metadata-sync.script = let + exec = "metadata-sync"; + cmd = builtins.filter (x: x != "") [ + "${cfg.package}/bin/${exec}" + "--db ${config.services.metadata-sync.postgres.database}" + "--db-user ${config.services.metadata-sync.postgres.user}" + "--db-host ${config.services.metadata-sync.postgres.socketdir}" + "--db-table ${config.services.metadata-sync.postgres.table}" + "--db-conns ${toString config.services.metadata-sync.postgres.numConnections}" + "--git-url ${config.services.metadata-sync.git.repositoryUrl}" + "--git-metadata-folder ${config.services.metadata-sync.git.metadataFolder}" + ]; + in pkgs.writeShellScript "metadata-sync" '' + set -euo pipefail + echo "Starting ${exec}: ${lib.concatStringsSep "\"\n echo \"" cmd}" + echo "..or, once again, in a single line:" + echo "${toString cmd}" + exec ${toString cmd} + ''; + environment.systemPackages = [ cfg.package config.services.postgresql.package ]; + systemd.services.metadata-sync = { + path = [ cfg.package pkgs.netcat pkgs.postgresql pkgs.git ]; + preStart = '' + for x in {1..60}; do + nc -z localhost ${toString config.services.metadata-sync.postgres.port} && break + echo loop $x: waiting for postgresql 2 sec... + sleep 2 + done + sleep 1 + ''; + serviceConfig = { + ExecStart = config.services.metadata-sync.script; + DynamicUser = true; + RuntimeDirectory = "metadata-sync"; + StateDirectory = "metadata-sync"; + }; + + wantedBy = [ "multi-user.target" ]; + after = [ "postgres.service" ]; + requires = [ "postgresql.service" ]; + }; + + systemd.timers.run-metadata-sync = { + timerConfig = { + Unit = "metadata-sync.service"; + OnCalendar = "hourly"; + }; + wantedBy = [ "timers.target" ]; + }; + }; +} diff --git a/nix/nixos/module-list.nix b/nix/nixos/module-list.nix index a505899..89a1c0f 100644 --- a/nix/nixos/module-list.nix +++ b/nix/nixos/module-list.nix @@ -1,4 +1,5 @@ [ ./metadata-server.nix ./metadata-webhook.nix + ./metadata-sync.nix ] diff --git a/nix/nixos/tests/default.nix b/nix/nixos/tests/default.nix index 7356e31..c097ff2 100644 --- a/nix/nixos/tests/default.nix +++ b/nix/nixos/tests/default.nix @@ -17,6 +17,7 @@ with pkgs.commonLib; callTest = fn: args: forAllSystems (system: let test = importTest fn args system; in hydraJob test // { inherit test; }); in rec { metadataStorePostgres = callTest ./metadata-store-postgres.nix { inherit haskellPackages; }; + metadataSync = callTest ./metadata-sync.nix { inherit haskellPackages; }; # Test will require local faucet setup # asset = callTest ./docs/asset.nix { inherit (pkgs.commonLib) sources; }; noNixSetup = callTest ./docs/no-nix-setup.nix { inherit haskellPackages; }; diff --git a/nix/nixos/tests/metadata-sync.nix b/nix/nixos/tests/metadata-sync.nix new file mode 100644 index 0000000..afa8d5d --- /dev/null +++ b/nix/nixos/tests/metadata-sync.nix @@ -0,0 +1,93 @@ + +{ pkgs, haskellPackages, ... }: +with pkgs; +let + # Single source of truth for all tutorial constants + database = "postgres"; + table = "metadata"; + user = "metadata-server"; + postgresUser = "metadata_server"; + postgresPort = 5432; +in +{ + name = "metadata-sync-integration-test"; + + nodes = { + server = { config, ... }: { + nixpkgs.pkgs = pkgs; + imports = [ + ../. + ]; + + # Open the default port for `postgrest` in the firewall + networking.firewall.allowedTCPPorts = []; + + services.postgresql = { + enable = true; + port = postgresPort; + package = pkgs.postgresql; + ensureDatabases = [ "${database}" ]; + ensureUsers = [ + { + name = "${postgresUser}"; + ensurePermissions = { + "DATABASE ${database}" = "ALL PRIVILEGES"; + }; + } + ]; + identMap = '' + metadata-server-users root ${postgresUser} + metadata-server-users ${user} ${postgresUser} + metadata-server-users postgres postgres + ''; + authentication = '' + local all all ident map=metadata-server-users + ''; + }; + + users = { + mutableUsers = false; + + users = { + # For ease of debugging the VM as the `root` user + root.password = ""; + + # Create a system user that matches the database user so that we + # can use peer authentication. + "${user}".isSystemUser = true; + }; + }; + + services.metadata-server = { + enable = true; + + user = user; + + postgres = { + port = postgresPort; + table = table; + user = postgresUser; + database = database; + }; + }; + }; + }; + + testScript = + '' + import json + import sys + + start_all() + + server.wait_for_open_port(${toString postgresPort}) + + server.succeed( + "${haskellPackages.metadata-sync.components.tests.integration-tests}/bin/integration-tests \ + --db-user ${postgresUser} \ + --db-host /run/postgresql \ + --db-name ${database} \ + --db-table ${table}" + ) + ''; +} diff --git a/release.nix b/release.nix index 8908dac..498edbf 100644 --- a/release.nix +++ b/release.nix @@ -103,6 +103,7 @@ let [ jobs.native.metadata-server.x86_64-linux jobs.native.metadata-webhook.x86_64-linux + jobs.native.metadata-sync.x86_64-linux jobs.native.metadata-validator-github.x86_64-linux jobs.native.token-metadata-creator.x86_64-linux From 68a6f13ae737b165d51d69f2e4e624188e76cc93 Mon Sep 17 00:00:00 2001 From: Samuel Evans-Powell Date: Tue, 18 May 2021 14:46:08 +0800 Subject: [PATCH 5/9] Styling --- metadata-sync/src/Cardano/Metadata/Sync.hs | 35 ++++++++++------ .../src/Cardano/Metadata/Sync/Config.hs | 39 ++++++++++++----- metadata-sync/src/Main.hs | 20 ++++++--- metadata-sync/test/Main.hs | 42 +++++++++++-------- 4 files changed, 92 insertions(+), 44 deletions(-) diff --git a/metadata-sync/src/Cardano/Metadata/Sync.hs b/metadata-sync/src/Cardano/Metadata/Sync.hs index 6b7729d..0798ada 100644 --- a/metadata-sync/src/Cardano/Metadata/Sync.hs +++ b/metadata-sync/src/Cardano/Metadata/Sync.hs @@ -3,20 +3,31 @@ module Cardano.Metadata.Sync where import qualified Data.Aeson as Aeson -import Data.Text (Text) +import Data.Functor + ( void ) +import Data.String + ( fromString ) +import Data.Text + ( Text ) import qualified Data.Text as T +import Data.Traversable + ( forM ) +import Database.PostgreSQL.Simple + ( Connection, execute, executeMany, withTransaction ) +import Database.PostgreSQL.Simple.Types + ( Identifier (..), In (..), Only (..) ) +import System.Directory + ( listDirectory ) +import System.FilePath.Posix + ( takeBaseName ) +import System.IO.Temp + ( withSystemTempDirectory ) import qualified Turtle.Prelude as Turtle -import System.Directory (listDirectory) -import System.FilePath.Posix (takeBaseName) -import Data.String (fromString) -import System.IO.Temp (withSystemTempDirectory) -import Database.PostgreSQL.Simple (withTransaction, execute, executeMany, Connection) -import Database.PostgreSQL.Simple.Types (Identifier(..), Only(..), In(..)) -import Data.Traversable (forM) -import Data.Functor (void) - -import Cardano.Metadata.Sync.Config (withConnectionFromPool) -import Cardano.Metadata.Types.Common (Subject(..)) + +import Cardano.Metadata.Sync.Config + ( withConnectionFromPool ) +import Cardano.Metadata.Types.Common + ( Subject (..) ) -- | View the current state of the registry (source-of-truth). view :: Text -> Text -> IO [(Subject, Aeson.Value)] diff --git a/metadata-sync/src/Cardano/Metadata/Sync/Config.hs b/metadata-sync/src/Cardano/Metadata/Sync/Config.hs index a2d9635..68f4ddf 100644 --- a/metadata-sync/src/Cardano/Metadata/Sync/Config.hs +++ b/metadata-sync/src/Cardano/Metadata/Sync/Config.hs @@ -2,19 +2,38 @@ module Cardano.Metadata.Sync.Config where -import Data.Text (Text) +import Control.Exception + ( bracket ) +import qualified Data.ByteString.Char8 as BC +import Data.Pool + ( Pool, createPool, destroyAllResources ) +import qualified Data.Pool as Pool +import Data.Text + ( Text ) import qualified Data.Text as T import qualified Data.Text.Encoding as TE -import Options.Applicative (Parser, ParserInfo) -import qualified Options.Applicative as Opt -import Options.Applicative (strOption, long, metavar, help, showDefault, value, option, auto, info, fullDesc, progDesc, header) -import Data.Pool (Pool, createPool, destroyAllResources) -import Database.PostgreSQL.Simple (Connection) -import Data.Time.Clock (NominalDiffTime) +import Data.Time.Clock + ( NominalDiffTime ) +import Database.PostgreSQL.Simple + ( Connection ) import qualified Database.PostgreSQL.Simple as Sql -import qualified Data.Pool as Pool -import qualified Data.ByteString.Char8 as BC -import Control.Exception (bracket) +import Options.Applicative + ( Parser, ParserInfo ) +import Options.Applicative + ( auto + , fullDesc + , header + , help + , info + , long + , metavar + , option + , progDesc + , showDefault + , strOption + , value + ) +import qualified Options.Applicative as Opt data Opts = Opts { optDbName :: Text diff --git a/metadata-sync/src/Main.hs b/metadata-sync/src/Main.hs index 2c5f7ab..a614e3a 100644 --- a/metadata-sync/src/Main.hs +++ b/metadata-sync/src/Main.hs @@ -1,19 +1,29 @@ module Main where +import Control.Exception + ( bracket ) import Control.Monad.IO.Class ( liftIO ) import qualified Data.ByteString.Char8 as BC +import Data.Pool + ( Pool, createPool, destroyAllResources ) import qualified Data.Text as T +import Data.Time.Clock + ( NominalDiffTime ) +import Database.PostgreSQL.Simple + ( Connection, close, connectPostgreSQL ) import qualified Network.Wai.Handler.Warp as Warp import qualified Options.Applicative as Opt -import Control.Exception (bracket) -import Database.PostgreSQL.Simple (Connection, connectPostgreSQL, close) -import Data.Pool (Pool, createPool, destroyAllResources) -import Data.Time.Clock (NominalDiffTime) import qualified Cardano.Metadata.Sync as Sync import Cardano.Metadata.Sync.Config - ( Opts (..), pgConnectionString, parseOpts, withConnectionPool, withConnectionFromPool, opts) + ( Opts (..) + , opts + , parseOpts + , pgConnectionString + , withConnectionFromPool + , withConnectionPool + ) main :: IO () main = do diff --git a/metadata-sync/test/Main.hs b/metadata-sync/test/Main.hs index 673f3a5..c847e9e 100644 --- a/metadata-sync/test/Main.hs +++ b/metadata-sync/test/Main.hs @@ -3,10 +3,18 @@ {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} +import Control.Exception + ( SomeException, catch ) +import Control.Monad + ( join, void ) import Control.Monad.IO.Class ( liftIO ) import Control.Monad.Logger ( runNoLoggingT ) +import qualified Data.Aeson as Aeson +import qualified Data.HashMap.Strict as HM +import Data.Pool + ( Pool, destroyAllResources ) import Data.Proxy ( Proxy (Proxy) ) import Data.Tagged @@ -15,11 +23,14 @@ import Data.Text ( Text ) import qualified Data.Text as T import qualified Data.Text.Encoding as TE -import qualified Data.Aeson as Aeson import Data.Word ( Word8 ) -import Data.Pool (Pool, destroyAllResources) -import Control.Monad (join, void) +import Hedgehog + ( MonadGen, evalIO, forAll, property, (===) ) +import qualified Hedgehog as H + ( Property ) +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range import Test.Tasty ( TestTree , askOption @@ -29,28 +40,25 @@ import Test.Tasty , testGroup , withResource ) -import Test.Tasty.Options -import Hedgehog - ( evalIO, forAll, property, (===), MonadGen ) -import qualified Hedgehog as H - ( Property ) -import Control.Exception (catch, SomeException) -import qualified Hedgehog.Gen as Gen -import qualified Data.HashMap.Strict as HM -import qualified Hedgehog.Range as Range import Test.Tasty import Test.Tasty.Hedgehog +import Test.Tasty.Options import Cardano.Metadata.Store.Types -import Cardano.Metadata.Types.Common (Subject(..), unPropertyName) -import Cardano.Metadata.Sync.Config (mkConnectionPool, withConnectionFromPool) -import Cardano.Metadata.Sync (write) +import Cardano.Metadata.Sync + ( write ) +import Cardano.Metadata.Sync.Config + ( mkConnectionPool, withConnectionFromPool ) +import Cardano.Metadata.Types.Common + ( Subject (..), unPropertyName ) +import Database.PostgreSQL.Simple + ( Connection, execute, executeMany, query, withTransaction ) +import Database.PostgreSQL.Simple.Types + ( Identifier (..), Only (..) ) import Test.Cardano.Metadata.Generators ( ComplexType ) import qualified Test.Cardano.Metadata.Generators as Gen import Test.Cardano.Metadata.Store -import Database.PostgreSQL.Simple (query, withTransaction, execute, executeMany, Connection) -import Database.PostgreSQL.Simple.Types (Only(..), Identifier(..)) newtype DbHost = DbHost { _dbHost :: Text } newtype DbName = DbName { _dbName :: Text } From 3aa8557b75fc8d34775c07ccbd95eb6cf27ad082 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 13 May 2021 13:20:34 +0200 Subject: [PATCH 6/9] Test for decimals range --- token-metadata-creator/test/index.spec.js | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/token-metadata-creator/test/index.spec.js b/token-metadata-creator/test/index.spec.js index 9cb8f9f..25b0188 100644 --- a/token-metadata-creator/test/index.spec.js +++ b/token-metadata-creator/test/index.spec.js @@ -138,6 +138,31 @@ describe("token-metadata-creator", () => { assert.isNull(getDraft(alice).name); }); + it("Decimals range = [0,255]", () => { + try { + cli(alice, "--decimals ", -1); + assert.fail("should have thrown."); + } catch (e) {} + + try { + cli(alice, "--decimals ", 256); + assert.fail("should have thrown."); + } catch (e) {} + + cli(alice, "--decimals ", 25) + assert.equal(getDraft(alice).decimals.value, 25); + assert.equal(getDraft(alice).decimals.sequenceNumber, 0); + + try { + cli(alice, "--decimals ", "potato"); + assert.fail("should have thrown."); + } catch (e) {} + + cli(alice, "--decimals ", 0) + assert.equal(getDraft(alice).decimals.value, 0); + assert.equal(getDraft(alice).decimals.sequenceNumber, 1); + }); + it("Edit property on successive calls", () => { let name = "SuperCoin" cli(alice, "--name", "foo"); From 947c9081cd3ddae65cea87b0ccd49cedb7b0d59e Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 13 May 2021 13:33:28 +0200 Subject: [PATCH 7/9] Update acceptance test --- test/Acceptance.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/Acceptance.md b/test/Acceptance.md index 84930a1..ba79fe8 100644 --- a/test/Acceptance.md +++ b/test/Acceptance.md @@ -19,8 +19,7 @@ $ cd metadata-registry-testnet/registry ``` ``` -$ token-metadata-creator entry --init bb59e0d0065c3368e0b7add747f -795026a93489b550bf0ddfbd6dc636f6666636861696e546f6f6c7354657374696e67 +$ token-metadata-creator entry --init bb59e0d0065c3368e0b7add747f795026a93489b550bf0ddfbd6dc636f6666636861696e546f6f6c7354657374696e67 $ token-metadata-creator entry bb59e0d0065c3368e0b7add747f795026a93489b550bf0ddfbd6dc636f6666636861696e546f6f6c7354657374696e67 \ --name "OffchainToolsCoin" \ @@ -30,7 +29,8 @@ $ token-metadata-creator entry bb59e0d0065c3368e0b7add747f795026a93489b550bf0ddf $ token-metadata-creator entry bb59e0d0065c3368e0b7add747f795026a93489b550bf0ddfbd6dc636f6666636861696e546f6f6c7354657374696e67 \ --ticker "OTC" \ --url "https://github.com/input-output-hk/offchain-metadata-tools/" \ - --logo "/home/piotr/wb/offchain-metadata-tools/token-metadata-creator/test/testData/icon.png" + --logo "/home/piotr/wb/offchain-metadata-tools/token-metadata-creator/test/testData/icon.png" \ + --decimals 5 \ $ token-metadata-creator entry bb59e0d0065c3368e0b7add747f795026a93489b550bf0ddfbd6dc636f6666636861696e546f6f6c7354657374696e67 -a /home/piotr/t/node/tokens/minter/offchain-tools-test/policy.skey @@ -76,7 +76,8 @@ curl -X GET http://localhost:8090/v2/wallets/1b0aa24994b4181e79116c131510f2abf6c "name": "OffchainToolsCoin", "ticker": "OTC", "logo": "...", - "description": "Test OffchainTools" + "description": "Test OffchainTools", + "decimals": 5 } }, ``` @@ -140,7 +141,8 @@ curl -X GET http://localhost:8090/v2/wallets/1b0aa24994b4181e79116c131510f2abf6c "name": "OffchainToolsCoin update", "ticker": "OTC", "logo": "...", - "description": "Test OffchainTools" + "description": "Test OffchainTools", + "decimals": 5 } }, From 62b23532d966e883097db56068dc3843e06393e0 Mon Sep 17 00:00:00 2001 From: Samuel Evans-Powell Date: Wed, 21 Apr 2021 12:45:32 +0800 Subject: [PATCH 8/9] Memory usage test - Add a memory usage NixOS test that provides an overview of metadata-server's memory usage under high load. It doesn't actually "test" anything, only provides statistics. --- metadata-server/metadata-server.cabal | 1 + .../metadata-store-postgres.cabal | 1 + nix/haskell.nix | 1 + nix/nixos/metadata-server.nix | 12 +- nix/nixos/tests/default.nix | 1 + nix/nixos/tests/mem-usage.nix | 142 +++++ nix/nixos/tests/mem-usage/large-query.json | 500 ++++++++++++++++++ nix/nixos/tests/mem-usage/vegeta-attack.nix | 7 + nix/nixos/tests/metadata-store-postgres.nix | 8 + 9 files changed, 671 insertions(+), 2 deletions(-) create mode 100644 nix/nixos/tests/mem-usage.nix create mode 100644 nix/nixos/tests/mem-usage/large-query.json create mode 100644 nix/nixos/tests/mem-usage/vegeta-attack.nix diff --git a/metadata-server/metadata-server.cabal b/metadata-server/metadata-server.cabal index 75488cb..31dcba3 100644 --- a/metadata-server/metadata-server.cabal +++ b/metadata-server/metadata-server.cabal @@ -40,6 +40,7 @@ executable metadata-server -Wincomplete-uni-patterns -Wno-unsafe -threaded + -rtsopts other-modules: Paths_metadata_server Config \ No newline at end of file diff --git a/metadata-store-postgres/metadata-store-postgres.cabal b/metadata-store-postgres/metadata-store-postgres.cabal index 209bbde..ddb6000 100644 --- a/metadata-store-postgres/metadata-store-postgres.cabal +++ b/metadata-store-postgres/metadata-store-postgres.cabal @@ -46,6 +46,7 @@ library -Wredundant-constraints -Wpartial-fields -Wcompat + -rtsopts test-suite integration-tests hs-source-dirs: test diff --git a/nix/haskell.nix b/nix/haskell.nix index 1c7b673..ce93507 100644 --- a/nix/haskell.nix +++ b/nix/haskell.nix @@ -60,6 +60,7 @@ let # Enable profiling on executables if the profiling argument is set. (lib.optionalAttrs profiling { enableLibraryProfiling = true; + packages.metadata-store-postgres.components.library.enableLibraryProfiling = true; packages.metadata-server.components.exes.metadata-server.enableExecutableProfiling = true; packages.metadata-webhook.components.exes.metadata-webhook.enableExecutableProfiling = true; packages.metadata-sync.components.exes.metadata-sync.enableExecutableProfiling = true; diff --git a/nix/nixos/metadata-server.nix b/nix/nixos/metadata-server.nix index db3eb96..d57bc27 100644 --- a/nix/nixos/metadata-server.nix +++ b/nix/nixos/metadata-server.nix @@ -36,6 +36,14 @@ in { default = 8080; description = "the port the metadata server runs on"; }; + extraFlags = lib.mkOption { + type = with lib.types; listOf str; + default = []; + description = '' + Extra flags to pass to the metadata-server executable. + ''; + example = ["+RTS" "-M500M" "-RTS"]; + }; postgres = { socketdir = lib.mkOption { type = lib.types.str; @@ -75,7 +83,7 @@ in { config = lib.mkIf cfg.enable { services.metadata-server.script = let exec = "metadata-server"; - cmd = builtins.filter (x: x != "") [ + cmd = builtins.filter (x: x != "") ([ "${cfg.package}/bin/${exec}" "--db ${config.services.metadata-server.postgres.database}" "--db-user ${config.services.metadata-server.postgres.user}" @@ -83,7 +91,7 @@ in { "--db-table ${config.services.metadata-server.postgres.table}" "--db-conns ${toString config.services.metadata-server.postgres.numConnections}" "--port ${toString config.services.metadata-server.port}" - ]; + ] ++ cfg.extraFlags); in pkgs.writeShellScript "metadata-server" '' set -euo pipefail echo "Starting ${exec}: ${lib.concatStringsSep "\"\n echo \"" cmd}" diff --git a/nix/nixos/tests/default.nix b/nix/nixos/tests/default.nix index c097ff2..7dfdff7 100644 --- a/nix/nixos/tests/default.nix +++ b/nix/nixos/tests/default.nix @@ -18,6 +18,7 @@ with pkgs.commonLib; in rec { metadataStorePostgres = callTest ./metadata-store-postgres.nix { inherit haskellPackages; }; metadataSync = callTest ./metadata-sync.nix { inherit haskellPackages; }; + memUsage = callTest ./mem-usage.nix {}; # Test will require local faucet setup # asset = callTest ./docs/asset.nix { inherit (pkgs.commonLib) sources; }; noNixSetup = callTest ./docs/no-nix-setup.nix { inherit haskellPackages; }; diff --git a/nix/nixos/tests/mem-usage.nix b/nix/nixos/tests/mem-usage.nix new file mode 100644 index 0000000..f323d47 --- /dev/null +++ b/nix/nixos/tests/mem-usage.nix @@ -0,0 +1,142 @@ +{ pkgs, ... }: +with pkgs; +let + # Single source of truth for all tutorial constants + database = "postgres"; + schema = "api"; + table = "metadata"; + user = "metadata-server"; + postgresUser = "metadata_server"; + password = "mysecretpassword"; + webRole = "web_anon"; + postgresPort = 5432; + metadataServerPort = 8080; + metadataWorkingDir = "/run/metadata-server"; + + totalMem = 1024; + allowedMem = 100; + + vegetaCommands = pkgs.callPackage ./mem-usage/vegeta-attack.nix { port = metadataServerPort; }; + vegetaGo = pkgs.writeShellScriptBin "vegeta-go" '' + cat ${vegetaCommands} | vegeta attack -duration 10s -connections 1 -rate 100/s -output vegeta.log + ''; +in +{ + name = "metadata-server-mem-usage-test"; + + nodes = { + server = { config, ... }: { + nixpkgs.pkgs = pkgs; + imports = [ + ../. + ]; + + virtualisation.memorySize = totalMem; + + environment.systemPackages = [ pkgs.vegeta pkgs.htop vegetaGo pkgs.haskellPackages.hp2pretty pkgs.haskellPackages.profiteur ]; + + services.postgresql = { + enable = true; + port = postgresPort; + package = pkgs.postgresql; + ensureDatabases = [ "${database}" ]; + ensureUsers = [ + { + name = "${postgresUser}"; + ensurePermissions = { + "DATABASE ${database}" = "ALL PRIVILEGES"; + }; + } + ]; + identMap = '' + metadata-server-users root ${postgresUser} + metadata-server-users ${user} ${postgresUser} + metadata-server-users postgres postgres + ''; + authentication = '' + local all all ident map=metadata-server-users + ''; + + settings = + { + log_connections = true; + # log_statement = "all"; + logging_collector = true; + log_disconnections = true; + log_destination = lib.mkForce "syslog"; + }; + }; + + users = { + mutableUsers = false; + + users = { + # For ease of debugging the VM as the `root` user + root.password = ""; + + # Create a system user that matches the database user so that we + # can use peer authentication. + "${user}".isSystemUser = true; + }; + }; + + services.metadata-server = { + enable = true; + + user = user; + port = metadataServerPort; + + postgres = { + port = postgresPort; + table = table; + user = postgresUser; + database = database; + }; + + extraFlags = ["+RTS" "-h" "-p" "-RTS"]; + + metadataServerPkgs = (import ../../../. {}).profiledProject; + }; + systemd.services.metadata-server.serviceConfig.WorkingDirectory = metadataWorkingDir; + systemd.services.metadata-server.serviceConfig.RuntimeDirectoryPreserve = true; + systemd.services.metadata-server.serviceConfig.KillSignal = "SIGINT"; + systemd.services.metadata-server.serviceConfig.TimeoutStopSec = "10"; + + systemd.tmpfiles.rules = [ + "L /root/vegeta.atk - - - - ${vegetaCommands}" + ]; + }; + }; + + testScript = + '' + # fmt: off + import os + import pathlib + + start_all() + + server.wait_for_open_port(${toString postgresPort}) + server.wait_for_open_port(${toString metadataServerPort}) + server.succeed("cat ${vegetaCommands} | vegeta attack -duration 10s -connections 1 -rate 10/s") + # Stop metadata-server.service so GHC writes out .prof file contents + server.succeed("systemctl stop metadata-server.service") + server.succeed("sleep 10") + server.succeed("cd ${metadataWorkingDir} && hp2pretty metadata-server.hp") + server.succeed("cd ${metadataWorkingDir} && profiteur metadata-server.prof") + server.copy_from_vm("${metadataWorkingDir}/metadata-server.svg") + server.copy_from_vm("${metadataWorkingDir}/metadata-server.prof") + server.copy_from_vm("${metadataWorkingDir}/metadata-server.prof.html") + + def write_hydra_build_products(data): + out_dir = pathlib.Path(os.environ.get("out", os.getcwd())) + hydra_build_products = out_dir / "nix-support" / "hydra-build-products" + hydra_build_products.parent.mkdir(exist_ok=True, parents=True) + with hydra_build_products.open(mode="a") as f: + f.write(data.format(out_dir) + "\n") + + write_hydra_build_products("file svg {}/metadata-server.svg") + write_hydra_build_products("file html {}/metadata-server.prof.html") + write_hydra_build_products("file text {}/metadata-server.prof") + ''; +} diff --git a/nix/nixos/tests/mem-usage/large-query.json b/nix/nixos/tests/mem-usage/large-query.json new file mode 100644 index 0000000..a7ab02d --- /dev/null +++ b/nix/nixos/tests/mem-usage/large-query.json @@ -0,0 +1,500 @@ +{ + "subjects": [ + "5e3ff95a3447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303231", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031354643303031", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031354643303030", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031345253", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643313030", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303939", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303938", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303937", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303936", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303935", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303934", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303933", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303932", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303931", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303930", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303839", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303838", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303837", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303836", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303835", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303834", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303833", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303832", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303831", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303830", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303739", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303738", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303737", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303736", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303735", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303734", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303733", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303732", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303731", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303730", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303639", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303638", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303637", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303636", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303635", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303634", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303633", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303632", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303631", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303630", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303539", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303538", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303537", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303536", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303535", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303534", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303533", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303532", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303531", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303530", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303439", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303438", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303437", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303436", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303435", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303434", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303433", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303432", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303431", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303430", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303339", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303338", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303337", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303336", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303335", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303334", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303333", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303332", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303331", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303330", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303239", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303238", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303237", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303236", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303235", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303234", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303233", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303232", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303231", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303230", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303139", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303138", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303137", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303136", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303135", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303134", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303133", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303132", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303131", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303130", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303039", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303038", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303037", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303036", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303035", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303034", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303033", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303032", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303031", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031344643303030", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031335253", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643313030", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303939", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303938", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303937", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303936", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303935", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303934", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303933", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303932", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303931", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303930", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303839", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303838", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303837", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303836", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303835", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303834", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303833", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303832", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303831", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303830", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303739", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303738", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303737", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303736", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303735", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303734", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303733", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303732", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303731", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303730", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303639", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303638", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303637", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303636", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303635", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303634", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303633", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303632", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303631", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303630", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303539", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303538", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303537", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303536", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303535", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303534", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303533", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303532", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303531", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303530", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303439", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303438", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303437", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303436", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303435", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303434", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303433", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303432", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303431", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303430", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303339", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303338", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303337", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303336", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303335", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303334", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303333", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303332", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303331", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303330", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303239", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303238", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303237", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303236", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303235", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303234", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303233", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303232", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303231", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303230", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303139", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303138", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303137", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303136", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303135", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303134", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303133", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303132", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303131", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303130", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303039", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303038", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303037", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303036", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303035", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303034", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303033", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303032", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303031", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031334643303030", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031325253", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643313030", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303939", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303938", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303937", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303936", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303935", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303934", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303933", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303932", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303931", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303930", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303839", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303838", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303837", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303836", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303835", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303834", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303833", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303832", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303831", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303830", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303739", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303738", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303737", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303736", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303735", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303734", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303733", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303732", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303731", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303730", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303639", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303638", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303637", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303636", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303635", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303634", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303633", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303632", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303631", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303630", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303539", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303538", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303537", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303536", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303535", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303534", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303533", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303532", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303531", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303530", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303439", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303438", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303437", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303436", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303435", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303434", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303433", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303432", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303431", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303430", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303339", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303338", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303337", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303336", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303335", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303334", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303333", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303332", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303331", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303330", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303239", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303238", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303237", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303236", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303235", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303234", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303233", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303232", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303231", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303230", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303139", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303138", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303137", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303136", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303135", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303134", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303133", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303132", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303131", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303130", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303039", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303038", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303037", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303036", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303035", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303034", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303033", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303032", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303031", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031324643303030", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031315253", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643313030", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303939", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303938", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303937", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303936", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303935", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303934", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303933", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303932", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303931", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303930", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303839", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303838", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303837", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303836", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303835", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303834", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303833", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303832", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303831", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303830", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303739", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303738", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303737", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303736", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303735", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303734", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303733", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303732", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303731", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303730", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303639", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303638", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303637", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303636", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303635", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303634", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303633", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303632", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303631", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303630", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303539", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303538", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303537", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303536", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303535", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303534", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303533", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303532", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303531", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303530", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303439", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303438", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303437", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303436", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303435", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303434", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303433", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303432", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303431", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303430", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303339", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303338", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303337", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303336", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303335", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303334", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303333", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303332", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303331", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303330", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303239", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303238", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303237", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303236", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303235", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303234", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303233", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303232", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303231", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303230", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303139", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303138", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303137", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303136", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303135", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303134", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303133", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303132", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303131", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303130", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303039", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303038", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303037", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303036", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303035", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303034", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303033", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303032", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303031", + "5534595a12198346446ca7be0a56548e7091c3f2126f360a5fd8fe2d43617264616e6f4b69647a303031314643303030", + "5526e823c4d22ee5954bcfab7f6519af49656613588f9b5c1ebb153a524547474145", + "54de866237258c023d20bb14e0bc83531623d40995bb0852b2ae54cb686868636f69", + "54b024d57e039e1a22c58bb1ba9fdaadc15810e904021da37b2d7d79454253636f696e", + "547946fff572aa719ed094264df688ca452f1b382a78606228e8fc154754", + "546cd9cdde943dccdc1b7b779f1a1b9c7ba44f897b244634cc94aa8a43617264616e6f4275647a437573746f6d31", + "546a6165648fd686a13aa81427f1f5c3d0b587d8c4859e0181d16fd4426972647a", + "544571c086d0e5c5022aca9717dd0f438e21190abb48f37b3ae129f047524f57", + "53ce07f29e08950e27c82d3bf78c293600ee0d8932a7c8fc1c14b625496d6167654e6674546573745632", + "5338b0fc0034e44bd3f98719966cb855a2f1a2b0a628f81172be4f1e436f6c6f7234", + "5338b0fc0034e44bd3f98719966cb855a2f1a2b0a628f81172be4f1e436f6c6f7233", + "5338b0fc0034e44bd3f98719966cb855a2f1a2b0a628f81172be4f1e436f6c6f7232", + "5338b0fc0034e44bd3f98719966cb855a2f1a2b0a628f81172be4f1e436f6c6f7231", + "5337c606a288bc10993a7e65243613b1ae842729928bb69414ef85e335474c697a617264", + "5315f253d24b597a7054d9ca93710b60b3d45febb92d510216dac4224c574e4457534b303334", + "52ba0fe66618e8dbb24290a1a8004cfe54ad587b6a55a4860fd14bad544843", + "52b8878252271481a726b871af9bbba9649a03af9693fa33ccca16c25041524953436f696e", + "524f01b88a66031c1693ce089a05141824b0ac8cadc9c177849e43f14144414a6f696e7473", + "5234593099ec0eccf4bbf6298eed47dc50e5f69e207df9d89c4d4dcb4d455441", + "51f0beaac7a92ab31da474aa2eb5bcb27b7cbef775b4e112ed21def05a455553", + "51e7e01f2a6debba1a54a8bb2235aa59b9323351284a60c1c8f8441043617264616e6f4a696d626f37", + "51a8b917114265855de3bf1c3c077a8d14123eb2a725a32a37e966bf4d4453", + "514e10d3fff6164c4ef4a1ff9f8fdaf21f3bcbcc060164f58664828b446f6765", + "5146470454d0eaee6bc96ad3eb01f0c7abf9e007a29b70e6074d5dc14c574e4457534b303335", + "51315043b5288badcd3c15055ea37a951f9a1a6451b6ef08abd9f2915350495241", + "50e8466119a80770237b836aa985756837fc47c534a9e3374ee9219a6b6964", + "507e3c04f838dc4fd99e6996236326fe6d159f62793408982a2acc9c474f44", + "5047b1e24673127b46a7051360db3135c273cfab87a70e7b4155832f446563656e7472616c697a65644e6574776f726b5574696c697479546f6b656e", + "4fe8998be763a8ba5acbfffc75deb2e35371bfe99968f48a5bb5cd1549747341506972617465734c696665666f724d65", + "4fd0d998dc0700ca6ef89fafff05fbf523a3a25c1fc8314bf7e4d1c247726f777468", + "4f926153bb6a9d2b3e95afff14ec15979134f1b9b6f8e3b24cb014b06d6f6f6e7374616b6539", + "4f17fa7624930826b2fe0d27e0ab8dcae6f838674389014119ce4cc96170706c656a61636b", + "4ee39d1d7e375d18b8eec9ece1fe88f4f33dfbe3ed218a7088b893664974734f6e", + "4e9c58a772bee7bef7bd16575bf1f61cca82f26f5d87265080b550397469636b6574", + "4e9a6b1098a85fb303504f36ec9e41a05bbc2cf306428438f9fd1b71466c79696e67576f6d626174", + "4e6bb3ce4965847611061812707c73ec0a77a1c8123cdfb66dc3f4b449637954657374", + "4e2bb5f8b70b86ec485b7f7bbc9acc1dd438841d96974d3be7a9851d546573744e46543133", + "4ddec7d5207a7a3cb0ea89644844ab736eb88f902cef8a832f1867d5434954525553", + "4d96d472f8287d851cd609c9afb86c4f6b95893b60b86caeba466f9b4b595a", + "4d78bc38579c64e11ebf6e5aec7ae6d40ea68d29289c20773e5dfe9b6b697761", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303731", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303730", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303639", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303638", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303637", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303636", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303635", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303634", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303633", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303632", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303631", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303630", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303539", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303538", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303537", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303536", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303535", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303534", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303533", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303532", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303531", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303530", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303439", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303438", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303437", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303436", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303435", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303434", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303433", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303432", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303431", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303430", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303339", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303338", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303337", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303336", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303335", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303334", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303333", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303332", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303331", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303330", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303239", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303238", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303237", + "4d4ff7563447720d50a46ac83f8236430db79dc9da75a7cf91852e6a46696e6765724d6f6e73746572734e46545665676173303236" + ] +} diff --git a/nix/nixos/tests/mem-usage/vegeta-attack.nix b/nix/nixos/tests/mem-usage/vegeta-attack.nix new file mode 100644 index 0000000..e134980 --- /dev/null +++ b/nix/nixos/tests/mem-usage/vegeta-attack.nix @@ -0,0 +1,7 @@ +{ writeText, port }: + +writeText "vegeta.atk" '' + POST http://localhost:${builtins.toString port}/metadata/query + Content-Type: application/json + @${./large-query.json} +'' diff --git a/nix/nixos/tests/metadata-store-postgres.nix b/nix/nixos/tests/metadata-store-postgres.nix index 70f43f2..11b36af 100644 --- a/nix/nixos/tests/metadata-store-postgres.nix +++ b/nix/nixos/tests/metadata-store-postgres.nix @@ -45,6 +45,14 @@ in authentication = '' local all all ident map=metadata-server-users ''; + settings = + { + log_connections = true; + log_statement = "all"; + logging_collector = true; + log_disconnections = true; + log_destination = lib.mkForce "syslog"; + }; }; users = { From bf50f66a7083801ab60798472fcfd1dd2fc24815 Mon Sep 17 00:00:00 2001 From: Samuel Evans-Powell Date: Wed, 12 May 2021 13:01:31 +0800 Subject: [PATCH 9/9] Use file-based store instead of Postgres DB - Store metadata in the file system instead of a Postgres DB. PR #24 demonstrates that a large user of memory is the Postgres store, specifically building the query for large batch-style requrests. This could be investigated further, but a quick fix is simply to use a file-based store. There is no need to build a query, and files can be looked up quickly using their filename. --- cabal-nix.project | 1 + cabal.project | 1 + .../src/Test/Cardano/Metadata/Generators.hs | 2 +- metadata-server/metadata-server.cabal | 2 +- metadata-server/src/Config.hs | 2 +- metadata-server/src/Main.hs | 22 +-- metadata-store-file/metadata-store-file.cabal | 88 ++++++++++ .../src/Cardano/Metadata/Store/File.hs | 166 ++++++++++++++++++ .../src/Cardano/Metadata/Store/File/Config.hs | 15 ++ metadata-store-file/test/Main.hs | 74 ++++++++ nix/haskell.nix | 5 +- nix/nixos/metadata-server.nix | 54 +----- nix/nixos/tests/default.nix | 1 + nix/nixos/tests/mem-usage.nix | 8 +- nix/nixos/tests/metadata-store-file.nix | 58 ++++++ 15 files changed, 425 insertions(+), 74 deletions(-) create mode 100644 metadata-store-file/metadata-store-file.cabal create mode 100644 metadata-store-file/src/Cardano/Metadata/Store/File.hs create mode 100644 metadata-store-file/src/Cardano/Metadata/Store/File/Config.hs create mode 100644 metadata-store-file/test/Main.hs create mode 100644 nix/nixos/tests/metadata-store-file.nix diff --git a/cabal-nix.project b/cabal-nix.project index 8a0391d..793b823 100644 --- a/cabal-nix.project +++ b/cabal-nix.project @@ -2,6 +2,7 @@ packages: ./metadata-lib ./metadata-server ./metadata-store-postgres + ./metadata-store-file ./metadata-webhook ./metadata-validator-github ./token-metadata-creator diff --git a/cabal.project b/cabal.project index 166ae1f..6155cf8 100644 --- a/cabal.project +++ b/cabal.project @@ -5,6 +5,7 @@ packages: ./metadata-server ./metadata-webhook ./metadata-store-postgres + ./metadata-store-file ./metadata-validator-github ./token-metadata-creator ./metadata-sync diff --git a/metadata-lib/src/Test/Cardano/Metadata/Generators.hs b/metadata-lib/src/Test/Cardano/Metadata/Generators.hs index 621f558..b50d339 100644 --- a/metadata-lib/src/Test/Cardano/Metadata/Generators.hs +++ b/metadata-lib/src/Test/Cardano/Metadata/Generators.hs @@ -75,7 +75,7 @@ complexType = <*> Gen.map (Range.linear 0 20) ((,) <$> key <*> val) complexKey :: MonadGen m => m ComplexKey -complexKey = unSubject <$> subject +complexKey = Gen.text (Range.linear 1 255) Gen.alphaNum complexKeyVals :: MonadGen m => m [(ComplexKey, ComplexType)] complexKeyVals = Gen.list (Range.linear 0 20) ((,) <$> complexKey <*> complexType) diff --git a/metadata-server/metadata-server.cabal b/metadata-server/metadata-server.cabal index 31dcba3..9a040c2 100644 --- a/metadata-server/metadata-server.cabal +++ b/metadata-server/metadata-server.cabal @@ -19,7 +19,7 @@ executable metadata-server , lens , lens-aeson , metadata-lib - , metadata-store-postgres + , metadata-store-file , monad-logger , mtl , persistent-postgresql diff --git a/metadata-server/src/Config.hs b/metadata-server/src/Config.hs index 18adb75..32d7a26 100644 --- a/metadata-server/src/Config.hs +++ b/metadata-server/src/Config.hs @@ -2,7 +2,7 @@ module Config where import Options.Applicative -import Cardano.Metadata.Store.Postgres.Config +import Cardano.Metadata.Store.File.Config ( Opts, parseOpts ) opts :: ParserInfo Opts diff --git a/metadata-server/src/Main.hs b/metadata-server/src/Main.hs index 0aceed3..3697f64 100644 --- a/metadata-server/src/Main.hs +++ b/metadata-server/src/Main.hs @@ -21,25 +21,19 @@ import qualified Options.Applicative as Opt import Cardano.Metadata.Server ( webApp ) -import qualified Cardano.Metadata.Store.Postgres as Store -import Cardano.Metadata.Store.Postgres.Config - ( Opts (..), pgConnectionString ) +import qualified Cardano.Metadata.Store.File as Store +import Cardano.Metadata.Store.File.Config + ( Opts (..) ) import Config ( opts ) main :: IO () main = do - options@(Opts { optDbConnections = numDbConns - , optDbMetadataTableName = tableName + options@(Opts { optMetadataLocation = folder , optServerPort = port }) <- Opt.execParser opts - let pgConnString = pgConnectionString options - putStrLn $ "Connecting to database using connection string: " <> BC.unpack pgConnString - runStdoutLoggingT $ - Postgresql.withPostgresqlPool pgConnString numDbConns $ \pool -> liftIO $ do - putStrLn $ "Initializing table '" <> tableName <> "'." - intf <- Store.postgresStore pool (T.pack tableName) - - putStrLn $ "Metadata server is starting on port " <> show port <> "." - liftIO $ Warp.run port (webApp intf) + putStrLn $ "Using file store at: " <> folder + intf <- Store.fileStore folder + putStrLn $ "Metadata server is starting on port " <> show port <> "." + liftIO $ Warp.run port (webApp intf) diff --git a/metadata-store-file/metadata-store-file.cabal b/metadata-store-file/metadata-store-file.cabal new file mode 100644 index 0000000..47b7b9b --- /dev/null +++ b/metadata-store-file/metadata-store-file.cabal @@ -0,0 +1,88 @@ +cabal-version: >=1.10 +name: metadata-store-file +version: 0.1.0.0 +author: Samuel Evans-Powell +maintainer: mail@sevanspowell.net +build-type: Simple +extra-source-files: CHANGELOG + +library + hs-source-dirs: src + + exposed-modules: Cardano.Metadata.Store.File + Cardano.Metadata.Store.File.Config + + build-depends: aeson + , base + , bytestring + , containers + , directory + , metadata-lib + , mtl + , filepath + , optparse-applicative + , safe-exceptions + , scientific + , text + , unordered-containers + , warp + + ghc-options: -Wall + -Wincomplete-record-updates + -Wincomplete-uni-patterns + -Wincomplete-patterns + -Wredundant-constraints + -Wpartial-fields + -Wcompat + -rtsopts + +test-suite integration-tests + hs-source-dirs: test + main-is: Main.hs + type: exitcode-stdio-1.0 + + build-depends: base >=4.12 && <5 + , HUnit + , QuickCheck + , aeson + , aeson-pretty + , base + , bytestring + , casing + , containers + , directory + , hedgehog + , hspec + , http-client + , lens + , lens-aeson + , metadata-lib + , metadata-store-file + , monad-logger + , mtl + , raw-strings-qq + , resource-pool + , safe-exceptions + , scientific + , servant + , servant-client + , servant-server + , smallcheck + , tagged + , tasty + , tasty-hedgehog + , tasty-hspec + , tasty-hunit + , tasty-quickcheck + , text + , unordered-containers + , wai + , warp + + ghc-options: -Wall + -Wincomplete-record-updates + -Wincomplete-uni-patterns + -Wincomplete-patterns + -Wredundant-constraints + -Wpartial-fields + -Wcompat \ No newline at end of file diff --git a/metadata-store-file/src/Cardano/Metadata/Store/File.hs b/metadata-store-file/src/Cardano/Metadata/Store/File.hs new file mode 100644 index 0000000..cd37987 --- /dev/null +++ b/metadata-store-file/src/Cardano/Metadata/Store/File.hs @@ -0,0 +1,166 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE IncoherentInstances #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeFamilies #-} + +module Cardano.Metadata.Store.File + ( read + , write + , update + , delete + , empty + , toList + , init + , fileStore + ) where + +import Cardano.Metadata.Store.Types +import Control.Exception.Safe +import Control.Monad.Reader +import Data.Aeson + ( FromJSON, FromJSONKey, ToJSON, ToJSONKey ) +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Encoding.Internal as Aeson +import qualified Data.Aeson.Types as Aeson +import Data.Coerce + ( coerce ) +import Data.Maybe + ( catMaybes, fromMaybe ) +import Data.Text + ( Text ) +import qualified Data.Text as T +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.Encoding as TLE +import Prelude hiding + ( init, read ) +import System.Directory +import System.FilePath.Posix + ( takeFileName ) + +data PostgresKeyValueException = UniqueKeyConstraintViolated + | FailedToDecodeJSONValue String Text + deriving (Eq, Show, Exception) + +data KeyValue k v = KeyValue { _kvFolder :: FilePath } + +init + :: FilePath + -- ^ Folder containing metadata entries + -> IO (KeyValue k v) + -- ^ Resulting key-value store +init = pure . KeyValue + +fileStore + :: ( ToJSONKey k + , ToJSON v + , FromJSONKey k + , FromJSON v + ) + => FilePath + -- ^ Folder containing metadata entries + -> IO (StoreInterface k v) +fileStore folder = do + let kvs = KeyValue folder + pure $ StoreInterface (\k -> read k kvs) + (\ks -> readBatch ks kvs) + (\k v -> write k v kvs) + (\k -> delete k kvs) + (\f k -> update f k kvs) + (toList kvs) + (empty kvs) + +-- | Ensure file path is within folder. +safeFilePath :: ToJSONKey k => KeyValue k v -> k -> FilePath +safeFilePath (KeyValue folder) k = + let + -- Disallow user to enter a sub-directory or a parent directory by + -- limiting the requested path to a file name. I.e. "../x.txt" and + -- "inner/x.txt" are normalised to "x.txt" to restrict the user + -- from looking outside the specified folder. + raw :: FilePath + raw = takeFileName . T.unpack $ toJSONKeyText k + in + folder <> "/" <> raw + +withFileIfExists :: ToJSONKey k => KeyValue k v -> k -> (FilePath -> IO r) -> IO (Maybe r) +withFileIfExists kvs k f = do + let safe = safeFilePath kvs k + exists <- doesFileExist safe + if exists + then do + r <- f safe + pure $ Just r + else pure Nothing + +read :: (ToJSONKey k, FromJSON v) => k -> KeyValue k v -> IO (Maybe v) +read k kvs = do + withFileIfExists kvs k $ \safe -> + Aeson.eitherDecodeFileStrict' safe + >>= (\v -> handleJSONDecodeError (T.pack safe) v) + +readBatch :: (ToJSONKey k, FromJSON v) => [k] -> KeyValue k v -> IO [v] +readBatch [] _kvs = pure [] +readBatch ks kvs = fmap catMaybes $ forM ks (\k -> read k kvs) + +write :: (ToJSONKey k, ToJSON v) => k -> v -> KeyValue k v -> IO () +write k v kvs = + let + safe = safeFilePath kvs k + in + Aeson.encodeFile safe v + +delete :: ToJSONKey k => k -> KeyValue k v -> IO () +delete k kvs = + fromMaybe () <$> withFileIfExists kvs k removeFile + +update :: (ToJSONKey k, ToJSON v, FromJSON v) => (v -> Maybe v) -> k -> KeyValue k v -> IO () +update fv k kvs = do + mv <- read k kvs + case mv of + Nothing -> pure () + Just v -> case fv v of + Nothing -> delete k kvs + Just newValue -> write k newValue kvs + +toList :: (ToJSONKey k, FromJSONKey k, FromJSON v) => KeyValue k v -> IO [(k, v)] +toList kvs@(KeyValue folder) = do + ks <- fmap (fmap T.pack) $ listDirectory folder + forM ks $ \kText -> do + k <- handleJSONDecodeError kText $ decodeJSONKey kText + mV <- read k kvs + pure $ maybe (error $ "Unable to find file with name '" <> (T.unpack $ toJSONKeyText k) <> "'") (k,) mV + +empty :: (FromJSONKey k, ToJSONKey k) => KeyValue k v -> IO () +empty kvs@(KeyValue folder) = do + ks <- fmap (fmap T.pack) $ listDirectory folder + void . forM ks $ \kText -> do + k <- handleJSONDecodeError undefined $ decodeJSONKey kText + delete k kvs + +handleJSONDecodeError :: Text -> Either String a -> IO a +handleJSONDecodeError t = either (\err -> throw $ FailedToDecodeJSONValue err t) pure + +toJSONKeyText :: ToJSONKey k => k -> Text +toJSONKeyText k = + case Aeson.toJSONKey of + Aeson.ToJSONKeyText f _ -> f k + Aeson.ToJSONKeyValue _ f -> TL.toStrict $ TLE.decodeUtf8 $ Aeson.encodingToLazyByteString $ f k + +decodeJSONKey :: FromJSONKey k => Text -> Either String k +decodeJSONKey t = case Aeson.fromJSONKey of + Aeson.FromJSONKeyCoerce -> pure $ coerce t + Aeson.FromJSONKeyText f -> pure $ f t + Aeson.FromJSONKeyTextParser p -> Aeson.parseEither p t + Aeson.FromJSONKeyValue pv -> do + (v :: Aeson.Value) <- Aeson.eitherDecode (TLE.encodeUtf8 . TL.fromStrict $ t) + Aeson.parseEither pv v diff --git a/metadata-store-file/src/Cardano/Metadata/Store/File/Config.hs b/metadata-store-file/src/Cardano/Metadata/Store/File/Config.hs new file mode 100644 index 0000000..d7dd1cd --- /dev/null +++ b/metadata-store-file/src/Cardano/Metadata/Store/File/Config.hs @@ -0,0 +1,15 @@ +module Cardano.Metadata.Store.File.Config where + +import qualified Network.Wai.Handler.Warp as Warp +import Options.Applicative + +data Opts = Opts + { optMetadataLocation :: FilePath + , optServerPort :: Warp.Port + } + deriving (Eq, Show) + +parseOpts :: Parser Opts +parseOpts = Opts + <$> strOption (long "folder" <> metavar "FOLDER" <> help "Folder containing the metadata entries") + <*> option auto (short 'p' <> long "port" <> metavar "PORT" <> showDefault <> value 8080 <> help "Port to run the metadata web server on") diff --git a/metadata-store-file/test/Main.hs b/metadata-store-file/test/Main.hs new file mode 100644 index 0000000..577feca --- /dev/null +++ b/metadata-store-file/test/Main.hs @@ -0,0 +1,74 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} + +import Control.Monad.IO.Class + ( liftIO ) +import Data.Proxy + ( Proxy (Proxy) ) +import Data.Tagged + ( Tagged, unTagged ) +import Data.Text + ( Text ) +import Data.Word + ( Word8 ) +import Test.Tasty + ( TestTree + , askOption + , defaultIngredients + , defaultMainWithIngredients + , includingOptions + , testGroup + , withResource + ) +import Test.Tasty.Options + +import Cardano.Metadata.Store.File +import Cardano.Metadata.Store.Types +import Test.Cardano.Metadata.Generators + ( ComplexType ) +import Test.Cardano.Metadata.Store + +newtype Folder = Folder { _folder :: FilePath } + +instance IsOption Folder where + defaultValue = error $ "'" <> unTagged (optionName :: Tagged Folder String) <> "' is required." + parseValue str = Just $ Folder str + optionName = pure "folder" + optionHelp = pure "Folder to store and retrieve metadata entries from." + +main :: IO () +main = + defaultMainWithIngredients + ((includingOptions + [ Option (Proxy @Folder) ] + ) + : defaultIngredients + ) + testsWithStore + +testsWithStore :: TestTree +testsWithStore = + askOption $ \(Folder folder) -> + withResource + (do + fileIntf1 <- liftIO $ fileStore folder + fileIntf2 <- liftIO $ fileStore folder + + pure (fileIntf1, fileIntf2) + ) + (const $ pure ()) + tests + +tests + :: IO ( StoreInterface Word8 Word8 + , StoreInterface Text ComplexType + ) + -> TestTree +tests resources = + testGroup "integration tests" + [ + testGroup "File data store implementation" + [ testKeyValueImplementation $ (\(intf, _) -> intf) <$> resources + , testKeyValueComplexTypeImplementation $ (\(_, intf) -> intf) <$> resources + ] + ] diff --git a/nix/haskell.nix b/nix/haskell.nix index ce93507..6c09d44 100644 --- a/nix/haskell.nix +++ b/nix/haskell.nix @@ -40,6 +40,7 @@ let packages.metadata-webhook = filterSubDir "metadata-webhook"; packages.metadata-sync = filterSubDir "metadata-sync"; packages.metadata-store-postgres = filterSubDir "metadata-store-postgres"; + packages.metadata-store-file = filterSubDir "metadata-store-file"; packages.metadata-validator-github = filterSubDir "metadata-validator-github"; } # Enable release flag (optimization and -Werror) on all local packages @@ -49,6 +50,7 @@ let packages.metadata-webhook.flags.release = true; packages.metadata-sync.flags.release = true; packages.metadata-store-postgres.flags.release = true; + packages.metadata-store-file.flags.release = true; packages.metadata-validator-github.flags.release = true; } { @@ -61,6 +63,7 @@ let (lib.optionalAttrs profiling { enableLibraryProfiling = true; packages.metadata-store-postgres.components.library.enableLibraryProfiling = true; + packages.metadata-store-file.components.library.enableLibraryProfiling = true; packages.metadata-server.components.exes.metadata-server.enableExecutableProfiling = true; packages.metadata-webhook.components.exes.metadata-webhook.enableExecutableProfiling = true; packages.metadata-sync.components.exes.metadata-sync.enableExecutableProfiling = true; @@ -110,8 +113,8 @@ let ]; } { - packages.metadata-store-postgres.components.tests.integration-tests.doCheck = false; packages.metadata-sync.components.tests.integration-tests.doCheck = false; + packages.metadata-store-file.components.tests.integration-tests.doCheck = false; } ]; }); diff --git a/nix/nixos/metadata-server.nix b/nix/nixos/metadata-server.nix index d57bc27..d1c4dab 100644 --- a/nix/nixos/metadata-server.nix +++ b/nix/nixos/metadata-server.nix @@ -44,38 +44,10 @@ in { ''; example = ["+RTS" "-M500M" "-RTS"]; }; - postgres = { - socketdir = lib.mkOption { + fileStore = { + folder = lib.mkOption { type = lib.types.str; - default = "/run/postgresql"; - description = "the path to the postgresql socket"; - }; - port = lib.mkOption { - type = lib.types.int; - default = 5432; - description = "the postgresql port"; - }; - database = lib.mkOption { - type = lib.types.str; - # Postgresql cannot have a hyphen (`-`) - default = "metadata_server"; - description = "the postgresql database to use"; - }; - table = lib.mkOption { - type = lib.types.str; - default = "metadata"; - description = "the postgresql database table to use"; - }; - user = lib.mkOption { - type = lib.types.str; - # Postgresql cannot have a hyphen (`-`) - default = "metadata_user"; - description = "the postgresql user to use"; - }; - numConnections = lib.mkOption { - type = lib.types.int; - default = 1; - description = "the number of connections to open to the postgresql database"; + description = "The path to the folder where the metadata entries are stored."; }; }; }; @@ -85,12 +57,7 @@ in { exec = "metadata-server"; cmd = builtins.filter (x: x != "") ([ "${cfg.package}/bin/${exec}" - "--db ${config.services.metadata-server.postgres.database}" - "--db-user ${config.services.metadata-server.postgres.user}" - "--db-host ${config.services.metadata-server.postgres.socketdir}" - "--db-table ${config.services.metadata-server.postgres.table}" - "--db-conns ${toString config.services.metadata-server.postgres.numConnections}" - "--port ${toString config.services.metadata-server.port}" + "--folder ${config.services.metadata-server.fileStore.folder}" ] ++ cfg.extraFlags); in pkgs.writeShellScript "metadata-server" '' set -euo pipefail @@ -99,17 +66,8 @@ in { echo "${toString cmd}" exec ${toString cmd} ''; - environment.systemPackages = [ cfg.package config.services.postgresql.package ]; systemd.services.metadata-server = { - path = [ cfg.package pkgs.netcat pkgs.postgresql ]; - preStart = '' - for x in {1..60}; do - nc -z localhost ${toString config.services.metadata-server.postgres.port} && break - echo loop $x: waiting for postgresql 2 sec... - sleep 2 - done - sleep 1 - ''; + path = [ cfg.package ]; serviceConfig = { ExecStart = config.services.metadata-server.script; DynamicUser = true; @@ -118,8 +76,6 @@ in { }; wantedBy = [ "multi-user.target" ]; - after = [ "postgres.service" ]; - requires = [ "postgresql.service" ]; }; }; } diff --git a/nix/nixos/tests/default.nix b/nix/nixos/tests/default.nix index 7dfdff7..63843cf 100644 --- a/nix/nixos/tests/default.nix +++ b/nix/nixos/tests/default.nix @@ -18,6 +18,7 @@ with pkgs.commonLib; in rec { metadataStorePostgres = callTest ./metadata-store-postgres.nix { inherit haskellPackages; }; metadataSync = callTest ./metadata-sync.nix { inherit haskellPackages; }; + metadataStoreFile = callTest ./metadata-store-file.nix { inherit haskellPackages; }; memUsage = callTest ./mem-usage.nix {}; # Test will require local faucet setup # asset = callTest ./docs/asset.nix { inherit (pkgs.commonLib) sources; }; diff --git a/nix/nixos/tests/mem-usage.nix b/nix/nixos/tests/mem-usage.nix index f323d47..2cc6d17 100644 --- a/nix/nixos/tests/mem-usage.nix +++ b/nix/nixos/tests/mem-usage.nix @@ -86,12 +86,7 @@ in user = user; port = metadataServerPort; - postgres = { - port = postgresPort; - table = table; - user = postgresUser; - database = database; - }; + fileStore.folder = "/"; extraFlags = ["+RTS" "-h" "-p" "-RTS"]; @@ -116,7 +111,6 @@ in start_all() - server.wait_for_open_port(${toString postgresPort}) server.wait_for_open_port(${toString metadataServerPort}) server.succeed("cat ${vegetaCommands} | vegeta attack -duration 10s -connections 1 -rate 10/s") # Stop metadata-server.service so GHC writes out .prof file contents diff --git a/nix/nixos/tests/metadata-store-file.nix b/nix/nixos/tests/metadata-store-file.nix new file mode 100644 index 0000000..4e7d748 --- /dev/null +++ b/nix/nixos/tests/metadata-store-file.nix @@ -0,0 +1,58 @@ +{ pkgs, haskellPackages, ... }: +with pkgs; +let + # Single source of truth for all constants + table = "metadata"; + user = "metadata-server"; + folder = "/tmp/metadata-store-file-integration-test"; +in +{ + name = "metadata-store-file-integration-test"; + + nodes = { + server = { config, ... }: { + nixpkgs.pkgs = pkgs; + imports = [ + ../. + ]; + + # Open the default port for `postgrest` in the firewall + networking.firewall.allowedTCPPorts = []; + + users = { + mutableUsers = false; + + users = { + # For ease of debugging the VM as the `root` user + root.password = ""; + + # Create a system user that matches the database user so that we + # can use peer authentication. + "${user}".isSystemUser = true; + }; + }; + + services.metadata-server = { + enable = true; + + user = user; + + fileStore.folder = folder; + }; + }; + }; + + testScript = + '' + import json + import sys + + start_all() + + server.succeed("mkdir -p ${folder}") + server.succeed( + "${haskellPackages.metadata-store-file.components.tests.integration-tests}/bin/integration-tests \ + --folder ${folder}" + ) + ''; +}