From 18b346bf44e7f231738d926f06268c58d99f0ead Mon Sep 17 00:00:00 2001 From: Tom Harding Date: Wed, 1 Feb 2023 12:23:55 +0000 Subject: [PATCH] Extract `Hasura.Incremental` into `lib/hasura-incremental` PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7534 GitOrigin-RevId: 85863cee836d348e8e314f8af1b50bb5cd912b46 --- server/graphql-engine.cabal | 15 +- server/lib/arrows-extra/arrows-extra.cabal | 42 ++++++ .../src}/Control/Arrow/Extended.hs | 0 .../src}/Control/Arrow/Interpret.hs | 0 .../arrows-extra/src}/Control/Arrow/Trans.hs | 0 server/lib/hasura-incremental/README.md | 142 ++++++++++++++++++ .../hasura-incremental.cabal | 99 ++++++++++++ .../src}/Hasura/Incremental.hs | 0 .../src}/Hasura/Incremental/Internal/Cache.hs | 0 .../Hasura/Incremental/Internal/Dependency.hs | 0 .../src}/Hasura/Incremental/Internal/Rule.hs | 0 .../src}/Hasura/Incremental/Select.hs | 0 .../test}/Hasura/IncrementalSpec.hs | 0 server/lib/hasura-incremental/test/Spec.hs | 2 + server/lib/monad-unique/monad-unique.cabal | 35 +++++ .../monad-unique/src}/Control/Monad/Unique.hs | 6 +- 16 files changed, 330 insertions(+), 11 deletions(-) create mode 100644 server/lib/arrows-extra/arrows-extra.cabal rename server/{src-lib => lib/arrows-extra/src}/Control/Arrow/Extended.hs (100%) rename server/{src-lib => lib/arrows-extra/src}/Control/Arrow/Interpret.hs (100%) rename server/{src-lib => lib/arrows-extra/src}/Control/Arrow/Trans.hs (100%) create mode 100644 server/lib/hasura-incremental/README.md create mode 100644 server/lib/hasura-incremental/hasura-incremental.cabal rename server/{src-lib => lib/hasura-incremental/src}/Hasura/Incremental.hs (100%) rename server/{src-lib => lib/hasura-incremental/src}/Hasura/Incremental/Internal/Cache.hs (100%) rename server/{src-lib => lib/hasura-incremental/src}/Hasura/Incremental/Internal/Dependency.hs (100%) rename server/{src-lib => lib/hasura-incremental/src}/Hasura/Incremental/Internal/Rule.hs (100%) rename server/{src-lib => lib/hasura-incremental/src}/Hasura/Incremental/Select.hs (100%) rename server/{src-test => lib/hasura-incremental/test}/Hasura/IncrementalSpec.hs (100%) create mode 100644 server/lib/hasura-incremental/test/Spec.hs create mode 100644 server/lib/monad-unique/monad-unique.cabal rename server/{src-lib => lib/monad-unique/src}/Control/Monad/Unique.hs (76%) diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 2e6b8cdaec5..d2b52cc0a8c 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -264,6 +264,7 @@ common lib-depends , aeson , aeson-casing , aeson-ordered + , arrows-extra , attoparsec , attoparsec-iso8601 >= 1.0 , autodocodec @@ -284,6 +285,7 @@ common lib-depends , free , hashable , hasura-error-message + , hasura-incremental , hasura-prelude , hasura-schema-parsers , http-client-tls @@ -295,6 +297,7 @@ common lib-depends , lifted-base , monad-control , monad-loops + , monad-unique , monad-validate , mtl , nonempty-containers @@ -481,15 +484,11 @@ library import: common-all, lib-depends hs-source-dirs: src-lib exposed-modules: Autodocodec.Extended - , Control.Arrow.Extended - , Control.Arrow.Interpret - , Control.Arrow.Trans , Control.Concurrent.Extended , Control.Monad.Circular , Control.Monad.Memoize , Control.Monad.Stateless , Control.Monad.Trans.Managed - , Control.Monad.Unique , Data.Aeson.Extended , Data.Aeson.Kriti.Functions , Data.Environment @@ -688,7 +687,6 @@ library , Hasura.Cache.Bounded , Hasura.Logging , Hasura.HTTP - , Hasura.Incremental , Hasura.PingSources , Hasura.Server.API.Backend , Hasura.Server.API.Instances @@ -723,7 +721,6 @@ library , Hasura.EncJSON , Hasura.GraphQL.Execute.Query , Hasura.GraphQL.Logging - , Hasura.Incremental.Select , Hasura.RQL.DML.Select , Hasura.RQL.Types.Run , Hasura.Session @@ -736,14 +733,13 @@ library , Hasura.Server.Telemetry.Counters , Hasura.Server.Auth.JWT , Hasura.GC - , Hasura.Incremental.Internal.Cache - , Hasura.Incremental.Internal.Dependency - , Hasura.Incremental.Internal.Rule + , Hasura.NativeQuery.IR , Hasura.NativeQuery.Metadata , Hasura.NativeQuery.API , Hasura.NativeQuery.Schema , Hasura.NativeQuery.Types + , Hasura.Server.Auth.WebHook , Hasura.Server.Middleware , Hasura.Server.Cors @@ -1175,7 +1171,6 @@ test-suite graphql-engine-tests Hasura.GraphQL.Schema.Build.UpdateSpec Hasura.GraphQL.Schema.Introspection Hasura.GraphQL.Schema.RemoteTest - Hasura.IncrementalSpec Hasura.Metadata.DTO.MetadataDTOSpec Hasura.QuickCheck.Instances Hasura.RQL.DDL.Webhook.TransformSpec diff --git a/server/lib/arrows-extra/arrows-extra.cabal b/server/lib/arrows-extra/arrows-extra.cabal new file mode 100644 index 00000000000..2b1d1ddcccc --- /dev/null +++ b/server/lib/arrows-extra/arrows-extra.cabal @@ -0,0 +1,42 @@ +cabal-version: 2.2 +name: arrows-extra +version: 1.0.0 +build-type: Simple +copyright: Hasura Inc. +extra-source-files: README.md + +library + hs-source-dirs: src + default-language: GHC2021 + + ghc-options: + -- Taken from https://medium.com/mercury-bank/enable-all-the-warnings-a0517bc081c3 + -Weverything + -Wno-missing-exported-signatures + -Wno-missing-import-lists + -Wno-missed-specialisations + -Wno-all-missed-specialisations + -Wno-unsafe + -Wno-safe + -Wno-missing-local-signatures + -Wno-monomorphism-restriction + -Wno-missing-kind-signatures + -Wno-missing-safe-haskell-mode + -- We want these warnings, but the code doesn't satisfy them yet: + -Wno-missing-deriving-strategies + -Wno-unused-packages + + build-depends: + , base + , mtl + , transformers + + exposed-modules: + Control.Arrow.Extended + Control.Arrow.Interpret + Control.Arrow.Trans + + default-extensions: + BlockArguments + FunctionalDependencies + LambdaCase diff --git a/server/src-lib/Control/Arrow/Extended.hs b/server/lib/arrows-extra/src/Control/Arrow/Extended.hs similarity index 100% rename from server/src-lib/Control/Arrow/Extended.hs rename to server/lib/arrows-extra/src/Control/Arrow/Extended.hs diff --git a/server/src-lib/Control/Arrow/Interpret.hs b/server/lib/arrows-extra/src/Control/Arrow/Interpret.hs similarity index 100% rename from server/src-lib/Control/Arrow/Interpret.hs rename to server/lib/arrows-extra/src/Control/Arrow/Interpret.hs diff --git a/server/src-lib/Control/Arrow/Trans.hs b/server/lib/arrows-extra/src/Control/Arrow/Trans.hs similarity index 100% rename from server/src-lib/Control/Arrow/Trans.hs rename to server/lib/arrows-extra/src/Control/Arrow/Trans.hs diff --git a/server/lib/hasura-incremental/README.md b/server/lib/hasura-incremental/README.md new file mode 100644 index 00000000000..7960d2ac975 --- /dev/null +++ b/server/lib/hasura-incremental/README.md @@ -0,0 +1,142 @@ +# `hasura-incremental` + +A library for caching intermediate results in `Arrow` computations. Used by +`graphql-engine` to optimise schema cache updates by avoiding redundant +recomputation. + +## Schema cache? + +The [`SchemaCache`](https://hasura.github.io/graphql-engine/server/haddock/main/Hasura-RQL-Types-SchemaCache.html#t:SchemaCache) is +is a very complex object that, among other things, keeps track of the GraphQL +schema and its parser for each user role in a `graphql-engine` instance. Its +computation is based on both user-supplied metadata _and_ the schema of each +data source configured to work with Hasura. When one of these dependencies is +updated (perhaps the user makes a metadata change), we'd like to recompile the +schema cache as quickly (and efficiently with regards to memory) as possible. + +For our specific uses of `hasura-incremental`, see the implementation of +[`buildSchemaCacheRule`](https://hasura.github.io/graphql-engine/server/haddock/main/src/Hasura.RQL.DDL.Schema.Cache.html#buildSchemaCacheRule) +and its `Inc.cache` calls, as well as the note titled, [`Avoiding GraphQL schema +rebuilds when changing irrelevant +Metadata`](https://hasura.github.io/graphql-engine/server/notes/avoiding-graphql-schema-rebuilds-when-changing-irrelevant-metadata.html). + +## `Arrow` 101 and `ArrowCache` + +[The `Arrow` abstraction](http://www.cse.chalmers.se/~rjmh/Papers/arrows.pdf) +describes a computation from some input to some output. A very simple example +of an `Arrow` is regular Haskell functions `a -> b`: a computation that +transforms some `a` into some `b`. Another good example is functions of the +shape `a -> m b` for some `Monad m`: a computation that transforms some `a` +into some `b` with potential side-effects. The `Arrow` abstraction forms the +basis of a whole hierarchy of classes, such as +[`ArrowChoice`](https://hackage.haskell.org/package/base-4.17.0.0/docs/Control-Arrow.html#t:ArrowChoice) +and +[`ArrowLoop`](https://hackage.haskell.org/package/base-4.17.0.0/docs/Control-Arrow.html#t:ArrowLoop), +each of which build on this basic idea. We can think of these as analogous to +`Monad` being the basis of classes like +[`MonadPlus`](https://hackage.haskell.org/package/base-4.17.0.0/docs/GHC-Base.html#t:MonadPlus) +and +[`MonadIO`](https://hackage.haskell.org/package/base-4.17.0.0/docs/Control-Monad-IO-Class.html#t:MonadIO): +each class describes further specific capabilities of the generic `Monad` +interface. + +This library introduces the +[`ArrowCache`](https://hasura.github.io/graphql-engine/server/haddock/main/Hasura-Incremental-Internal-Cache.html#t:ArrowCache) class. Sometimes, an `Arrow` +represents a computation that is computationally expensive, but is also +consistent in its output for any given input. For example, a compiler takes a +collection of source files and produces a binary. This computation can be very +expensive if we have a lot of source files and/or a complicated compiler, but +the result should be deterministic. In this case, `ArrowCache` allows us to +decorate an `Arrow` with a caching mechanism, avoiding recompilation of +unchanged files. + +When we `cache` an arrow, we keep track of the last input/output pair. If the +next input matches the last input, we can skip the computation and return the +last output, avoiding the need for the expensive computation. If the next input +doesn't match the last input, we replace the stored pair with the new +input/output pair. We can cache any arrow or composition of arrows in a +pipeline, allowing us to express different granularities of caching[^1]. + +If thinking of programs as a composition of `Arrow`s is unfamiliar, we can +instead think of our program as a directed, acyclic graph of intermediate +results. For example, our compiler might first compile the individual modules, +and then bundle them together. Any one of these intermediate results can be +cached to avoid expensive recomputation when its dependencies remain unchanged. + +## How does it work? + +### `Rule` and `cache` + +[`Rule`](https://hasura.github.io/graphql-engine/server/haddock/main/Hasura-Incremental-Internal-Rule.html#t:Rule) +is the interesting implementation of `ArrowCache`. Its instance in +[`Hasura.Incremental.Internal.Cache`](https://hasura.github.io/graphql-engine/server/haddock/main/src/Hasura.Incremental.Internal.Cache.html) +explains the system above, specifically in the `cached` definition of its +`where` clause: if the input has not changed, then we return the last result. +Otherwise, we recompute a result for the new input. Note that this is not +memoisation: we don't store an input/output map. We only ever store the last +input/output pair. + +### Keyed Dependencies + +The `ArrowCache` also features two other functions: +[`newDependency`](https://hasura.github.io/graphql-engine/server/haddock/main/Hasura-Incremental-Internal-Cache.html#v:newDependency) +and +[`dependOn`](https://hasura.github.io/graphql-engine/server/haddock/main/Hasura-Incremental-Internal-Cache.html#v:dependOn). +Consider an arrow that takes `Metadata` as input, but only ever accesses a +small set of specific fields within it. In this case, we'd ideally only like to +consider those particular fields when we determine whether we should recompute! + +The `Dependency` type gives us a way to keep track of the parts of a structure +on which we'd like to depend, and `Rule` then uses this information to cache +specifically based on our +[`Accesses`](https://hasura.github.io/graphql-engine/server/haddock/main/Hasura-Incremental-Internal-Dependency.html#t:Accesses), +rather than just naïvely checking for equality between the last and next input. +The functions for working with `Dependency` live in +[`Hasura.Incremental.Internal.Dependency`](https://hasura.github.io/graphql-engine/server/haddock/main/Hasura-Incremental-Internal-Dependency.html). + +### Invalidation keys + +Sometimes, we want to cache arrows that are _not_ deterministic. In these +cases, we might want to re-compute some value despite the inputs being the +same. For example, data source introspection is an inherently non-deterministic +action: what if the database schema changes? In these cases, the provided +[`InvalidationKey`](https://hasura.github.io/graphql-engine/server/haddock/main/Hasura-Incremental.html#t:InvalidationKey) +helper type can be used. When we `invalidate` an `InvalidationKey`, the +resulting value does not equal the previous value, which should force a +recomputation of the arrow when it is given as part of the input. + +## Gotchas + +### Monadic parameter-passing in `Rule` + +The reason for using the `Arrow` abstraction is to talk about the computational +relationship between the input and the output. In our specific case, what we +want to talk about is whether or not we've seen this pairing before. However, +this relies on the `Arrow` input being the only input to the computation. + +When we `build` or `rebuild` a `Rule m a b` computation, we do so inside some +`Applicative m`. If this `m` happens to be, say, `(->) r`, then we have access +to an `r` parameter that isn't explicitly given as the `a` parameter, and it +will not be considered part of the input that determines whether or not we need +to recompute. This can lead to problems: if the result of the computation +meaningfully changes depending on this value, then repeated calls may yield +incorrect results. In general, we should be careful to make sure that our +chosen `m` does not affect our decision to cache. + +On the other hand, this is an occasionally helpful loophole. For example, in +`Hasura.RQL.Types.SchemaCache.Build`, we use a `MonadReader BuildReason m` +constraint to determine whether we need to update, say, the event trigger +catalogue. If the input (i.e. the schema) hasn't changed, then the +`BuildReason` doesn't matter: the fact that it is accessed via the `m` means +that caching works the way we'd like it to work. + +Another example is when `m` happens to be `IO`: in these cases, we have no +guarantees about values being introduced during the computation. As mentioned +in the `InvalidationKey` section, non-determinism may be desired, and in these +cases the onus is on the user to invalidate the cache when recomputations are +required. + +[^1]: It might be tempting to cache every single arrow. However, this quite +quickly leads to a large amount of memory being consumed, and often for no good +reason. Sometimes, a result is so simple to recompute that we're better off +recomputing rather than allocating more memory. diff --git a/server/lib/hasura-incremental/hasura-incremental.cabal b/server/lib/hasura-incremental/hasura-incremental.cabal new file mode 100644 index 00000000000..91287f7aca4 --- /dev/null +++ b/server/lib/hasura-incremental/hasura-incremental.cabal @@ -0,0 +1,99 @@ +cabal-version: 2.2 +name: hasura-incremental +version: 1.0.0 +build-type: Simple +copyright: Hasura Inc. +extra-source-files: README.md + +library + hs-source-dirs: src + default-language: GHC2021 + + ghc-options: + -- Taken from https://medium.com/mercury-bank/enable-all-the-warnings-a0517bc081c3 + -Weverything + -Wno-missing-exported-signatures + -Wno-missing-import-lists + -Wno-missed-specialisations + -Wno-all-missed-specialisations + -Wno-unsafe + -Wno-safe + -Wno-missing-local-signatures + -Wno-monomorphism-restriction + -Wno-missing-kind-signatures + -Wno-missing-safe-haskell-mode + -- We want these warnings, but the code doesn't satisfy them yet: + -Wno-missing-deriving-strategies + -Wno-unused-packages + + build-depends: + , base + , arrows-extra + , dependent-map + , dependent-sum + , hasura-prelude + , monad-unique + , profunctors + , reflection + , some + , unordered-containers + + exposed-modules: + Hasura.Incremental + Hasura.Incremental.Internal.Cache + Hasura.Incremental.Internal.Dependency + Hasura.Incremental.Internal.Rule + Hasura.Incremental.Select + + default-extensions: + BlockArguments + DefaultSignatures + DerivingVia + FunctionalDependencies + LambdaCase + MultiWayIf + NoImplicitPrelude + PackageImports + RoleAnnotations + TypeFamilies + +test-suite hasura-incremental-tests + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Spec.hs + default-language: GHC2021 + + ghc-options: + -- Taken from https://medium.com/mercury-bank/enable-all-the-warnings-a0517bc081c3 + -Weverything + -Wno-missing-exported-signatures + -Wno-missing-import-lists + -Wno-missed-specialisations + -Wno-all-missed-specialisations + -Wno-unsafe + -Wno-safe + -Wno-missing-local-signatures + -Wno-monomorphism-restriction + -Wno-missing-kind-signatures + -Wno-missing-safe-haskell-mode + -- We want these warnings, but the code doesn't satisfy them yet: + -Wno-missing-deriving-strategies + -Wno-unused-packages + + other-modules: + Hasura.IncrementalSpec + + build-depends: + , base + , arrows-extra + , hasura-incremental + , hasura-prelude + , hspec + , monad-unique + , unordered-containers + + build-tool-depends: + hspec-discover:hspec-discover + + default-extensions: + BlockArguments diff --git a/server/src-lib/Hasura/Incremental.hs b/server/lib/hasura-incremental/src/Hasura/Incremental.hs similarity index 100% rename from server/src-lib/Hasura/Incremental.hs rename to server/lib/hasura-incremental/src/Hasura/Incremental.hs diff --git a/server/src-lib/Hasura/Incremental/Internal/Cache.hs b/server/lib/hasura-incremental/src/Hasura/Incremental/Internal/Cache.hs similarity index 100% rename from server/src-lib/Hasura/Incremental/Internal/Cache.hs rename to server/lib/hasura-incremental/src/Hasura/Incremental/Internal/Cache.hs diff --git a/server/src-lib/Hasura/Incremental/Internal/Dependency.hs b/server/lib/hasura-incremental/src/Hasura/Incremental/Internal/Dependency.hs similarity index 100% rename from server/src-lib/Hasura/Incremental/Internal/Dependency.hs rename to server/lib/hasura-incremental/src/Hasura/Incremental/Internal/Dependency.hs diff --git a/server/src-lib/Hasura/Incremental/Internal/Rule.hs b/server/lib/hasura-incremental/src/Hasura/Incremental/Internal/Rule.hs similarity index 100% rename from server/src-lib/Hasura/Incremental/Internal/Rule.hs rename to server/lib/hasura-incremental/src/Hasura/Incremental/Internal/Rule.hs diff --git a/server/src-lib/Hasura/Incremental/Select.hs b/server/lib/hasura-incremental/src/Hasura/Incremental/Select.hs similarity index 100% rename from server/src-lib/Hasura/Incremental/Select.hs rename to server/lib/hasura-incremental/src/Hasura/Incremental/Select.hs diff --git a/server/src-test/Hasura/IncrementalSpec.hs b/server/lib/hasura-incremental/test/Hasura/IncrementalSpec.hs similarity index 100% rename from server/src-test/Hasura/IncrementalSpec.hs rename to server/lib/hasura-incremental/test/Hasura/IncrementalSpec.hs diff --git a/server/lib/hasura-incremental/test/Spec.hs b/server/lib/hasura-incremental/test/Spec.hs new file mode 100644 index 00000000000..7a1ea192c97 --- /dev/null +++ b/server/lib/hasura-incremental/test/Spec.hs @@ -0,0 +1,2 @@ +{-# OPTIONS_GHC -F -pgmF hspec-discover #-} +{-# OPTIONS_GHC -Wno-missing-export-lists #-} diff --git a/server/lib/monad-unique/monad-unique.cabal b/server/lib/monad-unique/monad-unique.cabal new file mode 100644 index 00000000000..932aa3b200b --- /dev/null +++ b/server/lib/monad-unique/monad-unique.cabal @@ -0,0 +1,35 @@ +cabal-version: 2.2 +name: monad-unique +version: 1.0.0 +build-type: Simple +copyright: Hasura Inc. +extra-source-files: README.md + +library + hs-source-dirs: src + default-language: GHC2021 + + ghc-options: + -- Taken from https://medium.com/mercury-bank/enable-all-the-warnings-a0517bc081c3 + -Weverything + -Wno-missing-exported-signatures + -Wno-missing-import-lists + -Wno-missed-specialisations + -Wno-all-missed-specialisations + -Wno-unsafe + -Wno-safe + -Wno-missing-local-signatures + -Wno-monomorphism-restriction + -Wno-missing-kind-signatures + -Wno-missing-safe-haskell-mode + -- We want these warnings, but the code doesn't satisfy them yet: + -Wno-missing-deriving-strategies + -Wno-unused-packages + + build-depends: + , base + , mtl + , transformers + + exposed-modules: + Control.Monad.Unique diff --git a/server/src-lib/Control/Monad/Unique.hs b/server/lib/monad-unique/src/Control/Monad/Unique.hs similarity index 76% rename from server/src-lib/Control/Monad/Unique.hs rename to server/lib/monad-unique/src/Control/Monad/Unique.hs index e1bc02f310f..199d69266d2 100644 --- a/server/src-lib/Control/Monad/Unique.hs +++ b/server/lib/monad-unique/src/Control/Monad/Unique.hs @@ -5,8 +5,12 @@ module Control.Monad.Unique ) where +import Control.Monad.Except (ExceptT) +import Control.Monad.Reader (ReaderT) +import Control.Monad.State.Strict (StateT) +import Control.Monad.Trans.Class (lift) +import Control.Monad.Writer.Strict (WriterT) import Data.Unique qualified as U -import Hasura.Prelude class (Monad m) => MonadUnique m where newUnique :: m U.Unique