Skip to content

fix: implement connection tracking in metrics - #4672

Merged
steve-chavez merged 1 commit into
PostgREST:mainfrom
mkleczek:refactor/connection-tracking
May 18, 2026
Merged

fix: implement connection tracking in metrics#4672
steve-chavez merged 1 commit into
PostgREST:mainfrom
mkleczek:refactor/connection-tracking

Conversation

@mkleczek

@mkleczek mkleczek commented Feb 25, 2026

Copy link
Copy Markdown
Collaborator

DISCLAIMER:
This commit was authored entirely by a human without the assistance of LLMs.

Right now metrics observation handler does not track database connections but updates a single Gauge based on HasqlPoolObs events.

This is problematic because Hasql pool reports various connection events in various states that make it impossible to predict the state change from the received event. The connection state machine is not simple and to precisely report the number of connections in various states, it is necessary to track their lifecycles.

Fixes #4622

@steve-chavez

Copy link
Copy Markdown
Member

Fixes #4622

Fix should have an entry on the CHANGELOG

Comment thread src/PostgREST/Metrics.hs Outdated
Comment thread src/PostgREST/Metrics.hs
@mkleczek
mkleczek force-pushed the refactor/connection-tracking branch 5 times, most recently from 0f475e3 to 592e5a3 Compare March 10, 2026 14:53
@mkleczek
mkleczek force-pushed the refactor/connection-tracking branch 2 times, most recently from 1f3b4bc to bce1f6e Compare March 13, 2026 06:49
@mkleczek
mkleczek force-pushed the refactor/connection-tracking branch 8 times, most recently from 7383b59 to 9b3c004 Compare April 2, 2026 14:45
@mkleczek
mkleczek force-pushed the refactor/connection-tracking branch from 9b3c004 to 6af7b30 Compare April 8, 2026 12:33
Comment thread test/observability/Observation/MetricsSpec.hs
@steve-chavez

Copy link
Copy Markdown
Member

@mkleczek Since this is a fix, let's change the commit, PR title and add a fix entry in the CHANGELOG.

@mkleczek
mkleczek force-pushed the refactor/connection-tracking branch 5 times, most recently from 27d9a1d to e438eed Compare April 14, 2026 07:02
@steve-chavez

Copy link
Copy Markdown
Member

@mkleczek Could you please address the feedback here? I've repeated the same twice now 😩

@mkleczek

mkleczek commented Apr 14, 2026

Copy link
Copy Markdown
Collaborator Author

@mkleczek Could you please address the feedback here? I've repeated the same twice now 😩

Done

Comment on lines +79 to +100
SpecState{specAppState = appState, specMetrics = metrics, specObsChan} <- getState
let waitFor = waitForObs specObsChan

liftIO $ checkState' metrics [
-- we expect in use connections to be the same once finished
inUseConnections (+ 0)
] $ do
signal <- newEmptyMVar
-- make sure waiting thread is signaled
bracket_ (pure ()) (putMVar signal ()) $
-- expecting one more connection in use
checkState' metrics [
inUseConnections (+ 1)
] $ do
-- start a thread hanging on a single connection until signaled
void $ forkIO $ void $ AppState.usePool appState $ liftIO (readMVar signal)
-- main thread waits for ConnectionObservation with InUseConnectionStatus
-- after which used connections count should be incremented
waitFor (1 * sec) "InUseConnectionStatus" $ \x -> [ o | o@(HasqlPoolObs (SQL.ConnectionObservation _ SQL.InUseConnectionStatus)) <- pure x]

-- hanging thread was signaled and should return the connection
waitFor (1 * sec) "ReadyForUseConnectionStatus" $ \x -> [ o | o@(HasqlPoolObs (SQL.ConnectionObservation _ (SQL.ReadyForUseConnectionStatus _))) <- pure x]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm looking at this test-case.. and it just feels like an imperative language like python is much better to represent such a step-by-step test.

I can't even make full sense of the test, but I believe pytest + toxiproxy + metrics endpoint should allow the same test, right?

@mkleczek mkleczek May 12, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I'm looking at this test-case.. and it just feels like an imperative language like python is much better to represent such a step-by-step test.

I personally prefer statically typed language and many people find Haskell The Best Imperative Language

Especially in terms of concurrency support (and it is important in this test) Python is very far behind Haskell.

I can't even make full sense of the test, but I believe pytest + toxiproxy + metrics endpoint should allow the same test, right?

Not really (well... you could but it is not very convenient).

Toxiproxy is not used in this test so does not matter.

The idea of this test is to start a worker thread that will borrow a connection from the pool and keep it used until signaled by main thread.
After spawning the worker thread, main thread first waits for InUseConnectionStatus so that it is sure connection has been indeed borrowed from the pool. Then it checks it is properly accounted for in the metrics. Then it signals the worker thread, which in turn releases the connection and exits. Main thread then makes sure ReadyForUseConnectionStatus was published and then verifies connection release was accounted for in the metrics.

This is a "white box" test - we don't really test PostgREST as a whole here but only AppState.usePool function behavior.

There is no easy way to do the same in Python. We would need to somehow use the database advisory locks to be able to achieve "borrow connection from the pool and wait for a signal before releasing it". We also need to be able to wait for specific observations in the test. This was enabled for HSpec tests in #4671 and discussed with @steve-chavez and @taimoorzaeem

It is probably doable in Python (everything is doable in Turing complete language) but would require a lot of preparation and specific fixtures. It would also require accessing database directly from Python which we don't do in io tests.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Toxiproxy is not used in this test so does not matter.

But we do need it to prove the fix for #4622 as done in #4855 right?

I can't even make full sense of the test

I also don't find this readable, if the test in #4855 is enough maybe the test here is not really necessary?

(Not arguing for python or haskell, but seeing if is possible to save us some lines of code)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

If the test in #4855 is enough maybe the test here is not really necessary?

IMHO this test is necessary regardless. The test in #4855 tests if poolAvailable metric does not become negative during adverse network conditions.
This test is about inUse count during AppState.usePool execution.
It is something different and I would say this test is needed to simply make sure we properly calculate connection counts.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I personally prefer statically typed language and many people find Haskell The Best Imperative Language

From that link:

However, the false half is the syntax. I find Haskell pretty verbose and awkward to use in an imperative style.

Right on point for me.

Toxiproxy is not used in this test so does not matter.

One of the problems the io tests have is the big dependence on timing. Wait a second here, two there, whatever. Of course, we could write it with this approach in pytest as well - but I don't think that's a good idea.

So I thought of using toxiproxy to allow us to do this properly, i.e. by pausing the underlying connections, checking the state, then resuming / finishing the connection, checking the state again. But after reading the remainder of your comment about advisory locks, I see that we probably need these to even get to the right spot to pause the connection - and once we're there, we don't need toxiproxy for that specific case anymore.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

However, the false half is the syntax. I find Haskell pretty verbose and awkward to use in an imperative style.
Right on point for me.

It's the same for me but there's also another angle here. @mkleczek On #4672 (comment), you mention "support". I've had the experience of support + customers (i.e. users of PostgREST) challenging if a bug is fixed or not. Pointing them to a clear black box python test quickly settled the debate. It won't be the same in Haskell.

But after reading the remainder of your comment about advisory locks, I see that we probably need these to even get to the right spot to pause the connection - and once we're there, we don't need toxiproxy for that specific case anymore.

Can we use the advisory locks to achieve a negative connection metric?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not opposed to keep the current Haskell test. But perhaps we can add another one python io test that proves the Prometheus metrics are correct?

It would also require accessing database directly from Python which we don't do in io tests.

We do btw, a couple of choices:

  1. With psql

    with run(env=env):
    query = """
    select query
    from pg_stat_activity
    where application_name = 'listener-query-test'
    and query = 'LISTEN "pgrst"'
    limit 1;
    """
    output = subprocess.check_output(
    [
    "psql",
    "--set",
    "ON_ERROR_STOP=1",
    "--tuples-only",
    "--no-align",
    "-c",
    query,
    ],
    text=True,
    ).strip()

  2. Indirectly with an rpc

    response = postgrest.session.post("/rpc/uses_prepared_statements")

@mkleczek mkleczek May 15, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's the same for me but there's also another angle here. @mkleczek On #4672 (comment), you mention "support". I've had the experience of support + customers (i.e. users of PostgREST) challenging if a bug is fixed or not. Pointing them to a clear black box python test quickly settled the debate. It won't be the same in Haskell.

From my experience they don't really care about automated tests you have in your codebase, what language they are written in and whether they instantiate a program as a "black box" or test the module containing the bug - the whole source code and its organization is a black box to them (and they are not programmers most of the time).

They do care that you declare the bug is fixed though, which seems needs to wait.

But after reading the remainder of your comment about advisory locks, I see that we probably need these to even get to the right spot to pause the connection - and once we're there, we don't need toxiproxy for that specific case anymore.

Can we use the advisory locks to achieve a negative connection metric?

No, we can't. This is about connection errors - ie. failing to connect to the database server.


I'm sorry guys - I've raised #4855 to have a reproducer - I don't have any other idea on how to reproduce it, so if it is not mergeable then it looks like we have to live with #4622 until someone implements a different reproducer (possibly using different testing infrastructure and language). I am happy to wait and rebase this PR once such a reproducer is merged into main.

@steve-chavez @wolfgangwalther - you are maintainers of this project and you decide on the overall direction and whether PRs are aligned with your vision. I personally find it discouraging, though, that these grand vision discussions as in #4868 are really just blocking improvements and bug fixing.

I personally prefer continuous improvement and implementing sometimes imperfect solutions to current problems instead of blocking them just because they are imperfect - but that's my personal view.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I personally find it discouraging, though, that these grand vision discussions as in #4868 are really just blocking improvements and bug fixing.

Just to be clear: I specifically said this discussion is not blocking for me. What is blocking for Steve is essentially the backlog of #1766.

Of course, this won't help you in any way and I fully understand that this sucks. :/

@mkleczek
mkleczek force-pushed the refactor/connection-tracking branch 4 times, most recently from 90134d4 to c196a70 Compare May 13, 2026 13:21
@mkleczek

Copy link
Copy Markdown
Collaborator Author

@steve-chavez @wolfgangwalther - what do you think is required to push this forward?

#4622 is being now reported by our support and it is becoming urgent to fix it.

@mkleczek
mkleczek force-pushed the refactor/connection-tracking branch 3 times, most recently from c389f35 to 202f84e Compare May 14, 2026 08:30
@wolfgangwalther

Copy link
Copy Markdown
Member

what do you think is required to push this forward?

This does not introduce any new test infrastructure that I'd be opposed to, so I won't block on the long term vision of how our tests should be structured. It uses the existing infrastructure. I might not like the way the test is written, but I don't see a need to block on that either.

To be clear, the question in #4672 (comment) was asked to get a feeling of how things could be done differently, if we had better test infrastructure elsewhere - and not to block this PR's progress.

Imho the only previously blocking comment is #4672 (comment). Now, since I wrote that comment, I started a major discussion on how we should test in general, which is blocking the other, test-infra related, issues/PRs. I don't think we should hold this PR hostage to that either.

TLDR: No blockers for me.

@steve-chavez

Copy link
Copy Markdown
Member

what do you think is required to push this forward?

It's gonna be really confusing when we look back in history and we say we fix #4622 and there isn't a precise test proving it (the current test does not).

Let's not set a precedent here that can later hurt us with tech debt, so we should first clear the above thread.

@wolfgangwalther

Copy link
Copy Markdown
Member

It's gonna be really confusing when we look back in history and we say we fix #4622 and there isn't a precise test proving it (the current test does not).

Let's not set a precedent here that can later hurt us with tech debt, so we should first clear the above thread.

The thread you linked is about the currently existing test, so that doesn't quite match the first sentence about a missing test. If you're concerned about a missing test, then we need to clear #4672 (comment).

I'd still say the situation now is different compared to when #1766 happened - we are actively working on improving the test situation and we have an open PR to track the addition of the herein-missing test. Thus, I'd say the risk of this getting forgotten is much smaller than earlier.

I'd say we should go ahead with this.

@mkleczek
mkleczek force-pushed the refactor/connection-tracking branch from 202f84e to 8e12a9a Compare May 15, 2026 07:59
@steve-chavez

Copy link
Copy Markdown
Member

@wolfgangwalther Let's not merge because I have a much simpler test almost ready for PR, let's merge this after that.

@wolfgangwalther

Copy link
Copy Markdown
Member

No worries, I don't intend to merge. I just wanted to make my implicit approval explicit. I am well aware that you still have a thread open (this is now actually blocking the merge as well) - and I'm not just going to override you and resolve that thread. That's for you to decide :)

Right now metrics observation handler does not track database connections but updates a single Gauge based on HasqlPoolObs events. This is problematic because Hasql pool reports various connection events in multiple phases. The connection state machine is not simple and to precisely report the number of connections in various states, it is necessary to track their lifecycles.

This change adds a ConnTrack data structure and logic to track database connections lifecycles. At the moment it supports "connected" and "inUse" connection counts precisely. The "pgrst_db_pool_available" metric is implemented on top of ConnTrack instead of a simple Gauge.
@mkleczek
mkleczek force-pushed the refactor/connection-tracking branch from bf18e9a to 7785329 Compare May 18, 2026 12:16
@mkleczek
mkleczek requested a review from steve-chavez May 18, 2026 12:25
@steve-chavez
steve-chavez merged commit a297391 into PostgREST:main May 18, 2026
33 checks passed
Comment thread CHANGELOG.md
### Fixed

- Fix unnecessary connection pool flushes during schema cache reloading by @mkleczek in #4645
- Fix race condition in pool_available metric causing negative values during network instability by @mkleczek in #4622

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@mkleczek This was added in an old version Fixed section https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md#fixed-2 😕

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ehh... rebasing changelog is inherently tricky. My bad.

Raised #4942

@steve-chavez

Copy link
Copy Markdown
Member

I think we should backport this, it will require:

@mkleczek

Copy link
Copy Markdown
Collaborator Author

I think we should backport this, it will require:

Hmm... most probably the test in observability test suite is a problem for backporting.

Maybe we should split it into 2 PRs then (separate the test in MetricsSpec)?

@steve-chavez

Copy link
Copy Markdown
Member

Maybe we should split it into 2 PRs then (separate the test in MetricsSpec)?

Yup, sounds good.

@taimoorzaeem

Copy link
Copy Markdown
Member

I think we just follow the order in the git log. It involves 5 commits in order:

Let me try doing this in 1 PR, so it's easier to revert, just in case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Race condition in pool_available metric causes negative values during network instability

4 participants