Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use transaction settings #25

Merged
merged 20 commits into from
Jul 28, 2022
Merged

Use transaction settings #25

merged 20 commits into from
Jul 28, 2022

Conversation

kawu
Copy link
Collaborator

@kawu kawu commented Jul 22, 2022

An attempt to resolve #22 using a newtype wrapper over the internal DB effect stack. The wrapper itself has a MonadDB instance which avoids the re-implementation of withTransaction.

@kawu kawu force-pushed the use-transaction-settings branch from 8e7b74a to 088beb3 Compare July 22, 2022 15:34
@kawu kawu marked this pull request as ready for review July 25, 2022 10:25
@kawu kawu requested review from arybczak and kubek2k July 25, 2022 10:26
@kawu
Copy link
Collaborator Author

kawu commented Jul 25, 2022

@arybczak I used a newtype wrapper to avoid re-implementing withTransaction. What do you think about this approach?

@kubek2k I reorganized the tests, would be great if you could check I haven't broken anything 🙏.

Copy link
Contributor

@arybczak arybczak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, using a newtype, providing an instance for it and then simply delegating in the "canonical" instance is quite clever, I didn't think of this 👍 Too bad wrapping and unwrapping gets quite messy.

I think it's fine, since it also makes #27 easy to solve.

src/Effectful/HPQTypes.hs Outdated Show resolved Hide resolved
src/Effectful/HPQTypes.hs Outdated Show resolved Hide resolved
src/Effectful/HPQTypes.hs Outdated Show resolved Hide resolved
liftBase b = DBEff $ liftBase b

-- Convenience instance to avoid writing @DBEff get@ etc.
-- REVIEW: This comes with the mtl dependency, so maybe it isn't worth it? The
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't think it's worth it, I'd suggest removing it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, removed. I added some helper functions (get, put, ...) instead.

test/Main.hs Outdated Show resolved Hide resolved
test/Main.hs Outdated
void . runQuery $ mkSQL "COMMIT"
noOfResults <- runQuery $ mkSQL "SELECT * FROM some_table"
liftIO $ assertEqual "Results should be visible" 1 noOfResults
void . runQuery $ mkSQL "DROP TABLE some_table"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could also have an original test to prove that transaction isolation works between connections.

Copy link
Contributor

@arybczak arybczak Jul 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests need sorting out anyway (e.g. if the new connection test fails, the table is not removed and subsequent test runs fail, void . runQuery is runQuery_), but let's do that in a separate PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, let's create a ticket once this is merged and improve the tests in a separate PR.

@arybczak
Copy link
Contributor

arybczak commented Jul 26, 2022

I added a commit that makes the interpreter take ConnectionSourceM just as runDBT does (needed in order for the runner to be able to take a connection source that e.g. uses a logger).

This needed a new utility function: haskell-effectful/effectful#73

I'll release a new effectful version after you'll have a look at what I did.

put st {PQ.dbRecordLastQuery = False}
action

withNewConnection action = DBEff $ do
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Things get a bit complicated so it took me some time to understand what's going on :). But it looks good to me.

In the process I realized we could (I think) also get away without raiseWith or even bracket and instead use raise and run separate internal state (as this is done in DBT I believe). Something like:

  withNewConnection action = do
    dbState0 <- get
    (result, dbState') <- DBEff . raise $ do
      PQ.withConnection (PQ.dbConnectionSource dbState0) $ \newConn -> do
        let transactionSettings = PQ.dbTransactionSettings dbState0
            dbState = mkDBState (PQ.dbConnectionSource dbState0) newConn transactionSettings
        runState dbState . unDBEff $ do
          handleAutoTransaction transactionSettings PQ.withTransaction' action
    result <$ put dbState'

I think it should do the same thing? Maybe avoiding to run an internal state is better, though, I'm not sure 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh this version actually doesn't work, it makes the withNewConnection test fail. It looks like action still uses the external state, even though it's run within the context of a new, internal state. Is that normal? I will have to investigate that...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's normal (although somewhat subtle).

If you look at runEffectDB (we should rename it to runDB btw 🙂 ), the code that actually gets executed in tests when you call withNewConnection is the following:

WithNewConnection (action :: Eff localEs b) ->
      unDBEff . PQ.withNewConnection . DBEff $
        localSeqUnlift env $
          \unlift -> unlift action

so the action is run using the provided LocalEnv (which really is just an opaque wrapper for Env that's in use at the point where withNewConnection was called in tests). This LocalEnv somewhere inside has a handler for DB and the handler holds a reference to Env of the handler that has the original State created by runWithState.

Now, when you call raise in the handler, a new branch of Env that can be modified independently of the original Env of the handler is created (it has to be independent since if it wasn't, you could call raise and then overwrite the last effect on the stack with whatever you want). The LocalEnv doesn't see that independent branch anymore, hence it won't see the State that is created after the raise in that independent branch.

That's a minus of using the newtype and intermediate instance of DBEff - this is not immediately obvious at all until you mentally inline the definition of withNewConnection from the DBEff instance into the interpreter.

Does that make sense?

In fact, in the beginning I did what you did and was confused for a moment why it doesn't work, until I realized the above 🙂

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you look at runEffectDB (we should rename it to runDB btw slightly_smiling_face ), the code that actually gets executed in tests when you call withNewConnection is the following:

Ah yes, that's what I was missing, I guess that makes sense 🙂.

Still, this entire combination of raise and runState with unlifted action is somewhat confusing to me. Since the dbState provided to the internal runState is irrelevant (because action uses the state provided in LocalEnv), as in the following snippet:

  ...
  raise $ do
    ...
    runState dbState $ localSeqUnlift env $ \unlift -> unlift action

one could expect the following to have the same behavior:

  ...
  raise $ do
    ...
    runState () $ localSeqUnlift env $ \unlift -> unlift action

but the second version doesn't compile - the internal state might be irrelevant, but it must have the same type as the original state 🤯.

But I guess I will get used to that 🙂.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still, this entire combination of raise and runState with unlifted action is somewhat confusing to me. Since the dbState provided to the internal runState is irrelevant (because action uses the state provided in LocalEnv), as in the following snippet:

  ...
  raise $ do
    ...
    runState dbState $ localSeqUnlift env $ \unlift -> unlift action

one could expect the following to have the same behavior:

  ...
  raise $ do
    ...
    runState () $ localSeqUnlift env $ \unlift -> unlift action

but the second version doesn't compile - the internal state might be irrelevant, but it must have the same type as the original state exploding_head.

It doesn't, but for unrelated reasons. It doesn't compile because SuffixOf constraint won't let it. This was supposed to prevent doing a few nasty things, but it turns out it's too restrictive, which causes confusion (as you found out) and also it doesn't really behave as advertised because type families are weird. I fixed it in haskell-effectful/effectful#74 (and documented the "nasty things" in the haddock). After this is merged, your second version will work as well as this one:

  ...
  raise $ do
    ...
    localSeqUnlift env $ \unlift -> unlift action

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, this is much more predictable indeed, thanks!

@kawu
Copy link
Collaborator Author

kawu commented Jul 28, 2022

I've made some additional cosmetic changes (rename runEffectDB as runDB, use runQuery_ in tests). Are we ready to merge?

@arybczak
Copy link
Contributor

Sure, feel free to merge 👍 I'll make another PR that gets rid of the git revision of effectful-core when I release 1.2.0.0 (in a few days).

@kawu kawu merged commit 20cbcff into main Jul 28, 2022
@kawu kawu deleted the use-transaction-settings branch July 28, 2022 07:41
@kawu kawu mentioned this pull request Aug 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Transaction settings are not used
3 participants