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

fix: Ensure connection pool metrics stay consistent #99

Merged
merged 2 commits into from
Oct 15, 2024

Conversation

SevInf
Copy link
Collaborator

@SevInf SevInf commented Oct 8, 2024

This started as a bugfix to several prisma issues regarding incorrect metrics:

prisma/prisma#25177
prisma/prisma#23525

And a couple of more discovered the testing, but not reported in the issues.
There were several causes for this:

  1. Following pattern appears quite a lot in mobc code:
gauge!("something").increment(1.0);
do_a_thing_that_could_fail()?;
gauge!("something").decrement(1.0);

So, in case do_a_thing_that_could_fail actually fails, gauge will get incremented, but never will get decremented.

  1. Couple of metrics were relying on Conn::close being manually called and that was not the case every once in a while.

To prevent both of those problems, I rewrote the internals of library to rely on RAII rather than manual counters and resources management.

Conn struct is now split into two:

  • ActiveConn - represents currently checked out connection that have been actively used by the client. Holds onto semaphore permit and can be converted into IdleConn. Doing so will free the permit.
  • IdleConn - represents idle connection, currently checked into the pool. Can be converted to ActiveConn by providing a valid permit.

ConnState represents the shared state of the connection that is retained between different activity states.

Both IdleConn and ActiveConn manage their corresponding gauges - increment them on creation and decrement them during drop. ConnState manages CONNECTIONS_OPEN gauge and CONNECTIONS_TOTAL and CLOSED_TOTAL counters in the same way.

This system ensures that metrics stay consistent: since metrics are automatically incremented and decremented on state conversions, we can always be sure that:

  • Connection is always either idle or active, there is no in between state.
  • Idle connections and active connections gauges will always add up to the currently open connections gauge
  • Total connections open counter, minus total connections closed gauge will always be equal to number of currently open connections.

Since resources are now managed by Drop::drop implementations, that removes the need for manual close method and simiplifies the code quite in a few places, also ensuring it is safer against future changes.

This started as a bugfix to several prisma issues regarding incorrect
metrics:

prisma/prisma#25177
prisma/prisma#23525

And a couple of more discovered the testing, but not reported in the
issues.
There were several causes for this:
1. Following pattern appears quite a lot in mobc code:

```rust
gauge!("something").increment(1.0);
do_a_thing_that_could_fail()?;
gauge!("something").decrement(1.0);
```

So, in case `do_a_thing_that_could_fail` actually fails, gauge will get
incremented, but never will get decremented.

2. Couple of metrics were relying on `Conn::close` being manually called
and that was not the case every once in a while.

To prevent both of those problems, I rewrote the internals of library to
rely on RAII rather than manual counters and resources management.

`Conn` struct is now split into two:
- `ActiveConn` - represents currently checked out connection that have
  been actively used by the client. Holds onto semaphore permit and can
  be converted into `IdleConn`. Doing so will free the permit.
- `IdleConn` - represents idle connection, currently checked into the
  pool. Can be converted to `ActiveConn` by providing a valid permit.

`ConnState` represents the shared state of the connection that is
retained between different activity states.

Both `IdleConn` and `ActiveConn` manage their corresponding gauges -
increment them on creation and decrement them during drop. `ConnState`
manages `CONNECTIONS_OPEN` gauge and `CONNECTIONS_TOTAL` and
`CLOSED_TOTAL` counters in the same way.

This system ensures that metrics stay consistent: since metrics are
automatically incremented and decremented on state conversions, we can
always be sure that:
- Connection is always either idle or active, there is no in between
state.
- Idle connections and active connections gauges will always add up to
the currently open connections gauge
- Total connections open counter, minus total connections closed gauge
will always be equal to number of currently open connections.

Since resources are now managed by `Drop::drop` implementations, that
removes the need for manual `close` method and simiplifies the code
quite in a few places, also ensuring  it is safer against future
changes.
@@ -706,14 +706,6 @@ fn test_max_idle_lifetime() {
drop(v);
delay_for(Duration::from_millis(800)).await;

let mut v = vec![];
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This assertion was incorrect - connection lifeteme is 1 sec, by that time 1800ms passed but connections were not freed for some reason

SevInf added a commit to prisma/prisma-engines that referenced this pull request Oct 8, 2024
mobc update includes fixes from importcjj/mobc#99
Rest of the PR is update to new api of the `metrics` crate.

Close prisma/team-orm#1317
max_lifetime_closed: AtomicU64,
max_idle_closed: AtomicU64,
max_idle_closed: Arc<AtomicU64>,
Copy link

Choose a reason for hiding this comment

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

Why are atomics wrapped in an Arc?
Arc<T> is Send + Safe as long as T: Send + Safe, and AtomicU64 is already Send + Safe.
What do we need the Arc for?

Copy link

Choose a reason for hiding this comment

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

I think I was wrong.

From https://doc.rust-lang.org/std/sync/atomic/:

Atomic variables are safe to share between threads (they implement Sync) but they do not themselves provide the mechanism for sharing and follow the threading model of Rust. The most common way to share an atomic variable is to put it into an Arc (an atomically-reference-counted shared pointer).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep, exactly. Those two are needed to be shared becuase drop for ConnState needs to increment one and decrement another

Copy link
Collaborator

Choose a reason for hiding this comment

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

@jkomyno atomics are safely shareable (and you can even safely concurrently mutate them through a shared reference) but they are not magically shared just by cloning them (that would violate the idea of ownership), you need some kind of a reference or a pointer to be able to access the same location in memory from multiple places. If you don't want an Arc here, the only other option is for ConnState to borrow from the Pool and that is not necessarily practical.

Copy link
Collaborator

@aqrln aqrln left a comment

Choose a reason for hiding this comment

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

beautiful

src/conn.rs Outdated Show resolved Hide resolved
Comment on lines +445 to +446
internals.wait_duration += wait_guard.elapsed();
drop(wait_guard);
Copy link
Collaborator

@aqrln aqrln Oct 10, 2024

Choose a reason for hiding this comment

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

A bit of an edge case and I'm not sure how critical it is here, but if the thread is preempted by the kernel between these two statements, the counters in internal and the histogram may diverge a bit. Could be especially sensitive to running on a cloud VM with something like 100m CPU quota.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

True. I think instead of a manual drop we can add a method to HistogramGuard that takes ownership of self and returns elapsed time.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yep internals.wait_duration += wait_guard.into_elapsed() sounds great


impl Drop for ConnState {
fn drop(&mut self) {
self.total_connections_open.fetch_sub(1, Ordering::Relaxed);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is Relaxed sound here and no happens-before relationship is necessary, or should the loads of PoolState::num_open form an acquire-release pair with this store?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've just moved existing code around, but I agree with you here, acquire-release makes more sense in this case

Co-authored-by: Alexey Orlenko <[email protected]>
SevInf added a commit to prisma/prisma-engines that referenced this pull request Oct 10, 2024
mobc update includes fixes from importcjj/mobc#99
Rest of the PR is update to new api of the `metrics` crate.

Close prisma/team-orm#1317
Copy link
Collaborator

@garrensmith garrensmith left a comment

Choose a reason for hiding this comment

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

Hey @SevInf

Thanks for the PR. There is a lot of changes here and I haven't looked at this code for awhile so I'm not entirely sure on a few things

So, in case do_a_thing_that_could_fail actually fails, gauge will get incremented, but never will get decremented.

Can you give an example of this? Why what adding the GaugeGuard your solution to this?

Couple of metrics were relying on Conn::close being manually called and that was not the case every once in a while.

What causes the Conn::close not to be called?

I don't understand why two connection states, I can't see what the difference is between them?

The error in metrics you are trying to fix makes sense. Are you 100% certain this fixes them?
Have you measure what the performance impact this has on the library?
I know you say it makes the code simpler but I don't see how, it seems to add a lot more to it.

Sorry for lots of questions, I'm trying to get some context here?

};

let shared = Arc::new(SharedPool {
config: share_config,
manager,
internals,
semaphore: Semaphore::new(max_open),
semaphore: Arc::new(Semaphore::new(max_open)),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do you need the Arc around the Semaphore?
What is the performance implications of this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Arc is needed to get owned permit, that method exists only on Arc<Self>
No noticeable negative performance implications, if anything, mobc own testsuite consistently runs 10-15s faster on my machine after my changes.

Self {
inner,
state,
_permit: permit,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do you keep the permit instead of forgetting it? I can't see you using it anywhere?

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 I forget it, i will then need to add it back manually when connection drops. If I keep on struct, it will automatically gets returned when the struct drops, we don't need to think about doing manual management and we can't get it wrong in case struct drops earlier then we thought.

@SevInf
Copy link
Collaborator Author

SevInf commented Oct 14, 2024

Can you give an example of this? Why what adding the GaugeGuard your solution to this?

For example:

  1. increment
  2. decrement
  3. ? between the two that will return skip decrement in case of an error

What causes the Conn::close not to be called?

Generally, Conn::close is called only when connection is closed "normally" after timeouts or failed healthcheck and it's might not get closed in case of "abnormal" error.

Why what adding the GaugeGuard your solution to this?

GaugeGuard decrements counter on drop and drop is automatically called in case of early return like in example above. This way it is guaranteed that every increment will have one and only decrement call and it is guaranteed that corresponding decrement will be called.

I don't understand why two connection states, I can't see what the difference is between them?

Just a safety mechanism to ensure connections are used correctly when checked in and out of pool. For example, now typesystem guarantees that:

  • it is impossible to check connection out of the pool without a semaphore permit
  • it is impossible to check connection in and out and mess up the gauges of currently open/idle connections

The error in metrics you are trying to fix makes sense. Are you 100% certain this fixes them?

Not 100% certain, but I went from being able to reproduce them consistently to not being able to reproduce them at all. (autocannon with 10k connections on a test service).

Have you measure what the performance impact this has on the library?

mobc's own test suite consistently runs faster for me after my changes

I know you say it makes the code simpler but I don't see how, it seems to add a lot more to it.

Sure, it's more code, but resource management is no longer relies on us calling right cleanup method at the right time, Rust does it for us in a safer and less brittle way.

@garrensmith garrensmith merged commit 04eb7b2 into importcjj:main Oct 15, 2024
1 check passed
@garrensmith
Copy link
Collaborator

Awesome thanks for the work and explanations.

@garrensmith
Copy link
Collaborator

@SevInf you should have commit access and be able to push a release.

@SevInf
Copy link
Collaborator Author

SevInf commented Oct 15, 2024

Thank you @garrensmith!
Could you maybe also add @aqrln? This is my last week at Prisma, I don't think I'll be that actively working on mobc in future

@SevInf
Copy link
Collaborator Author

SevInf commented Oct 15, 2024

Also, @garrensmith, I do have access on Github but not on crates.io so can't publish a release right now

SevInf added a commit to prisma/prisma-engines that referenced this pull request Oct 16, 2024
mobc update includes fixes from importcjj/mobc#99
Rest of the PR is update to new api of the `metrics` crate.

Close prisma/team-orm#1317
SevInf added a commit to prisma/prisma-engines that referenced this pull request Oct 16, 2024
mobc update includes fixes from importcjj/mobc#99
Rest of the PR is update to new api of the `metrics` crate.

Close prisma/team-orm#1317
aqrln pushed a commit to prisma/prisma-engines that referenced this pull request Oct 21, 2024
mobc update includes fixes from importcjj/mobc#99
Rest of the PR is update to new api of the `metrics` crate.

Close prisma/team-orm#1317
aqrln pushed a commit to prisma/prisma-engines that referenced this pull request Oct 23, 2024
mobc update includes fixes from importcjj/mobc#99
Rest of the PR is update to new api of the `metrics` crate.

Close prisma/team-orm#1317
tmm1 added a commit to anysphere/prisma-engines that referenced this pull request Oct 26, 2024
@tmm1
Copy link
Contributor

tmm1 commented Oct 27, 2024

FYI when building prisma-engines 5.14.0 w/ mobc 0.8.5 i'm not getting any values for most metrics

@aqrln
Copy link
Collaborator

aqrln commented Oct 27, 2024

@tmm1 yeah that's a known issue in Prisma (one problem was masking another), will be fixed in 5.22.0

@aqrln
Copy link
Collaborator

aqrln commented Oct 29, 2024

@tmm1 actually on a second thought it wouldn't have led to not getting any values at all. In your case it's probably just because the version of the metrics crate in prisma-engines 5.14.0 is much older than the one used in mobc, so now you have two versions of metrics in the tree, and the one metrics are written to is not the same that the one metrics are read from.

@tmm1
Copy link
Contributor

tmm1 commented Oct 29, 2024

Good catch! Can confirm things work as expected with 5.14.0 + prisma/prisma-engines#5015

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.

5 participants