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

RUST-954 Pin cursor connections in load balancer mode #446

Merged

Conversation

abr-egn
Copy link
Contributor

@abr-egn abr-egn commented Aug 30, 2021

RUST-954

This implements the cursor portion of connection pinning; transactions and checkout type tracking will come in future PRs.

self.execute_operation_pinned(op, session, None).await
}

pub(crate) async fn execute_operation_pinned<T: Operation>(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not thrilled with _pinned (use passed-in connection) vs _pinnable (return checked-out connection), but this at least avoids the churn of updating all unrelated execute_operation calls.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think a slight improvement would be to move the _pinned portion to the Operation trait by adding a new fn pinned_connection(&mut self) -> Option<&mut Connection>, similar to how we currently handle selection criteria on the operation. Then we can actually eliminate the _pinned method altogether and use just execute_operation most of the time. This will work nicely with transactions that have connections pinned to them as well, since we'll probably want the logic for that in the executor somewhere rather than outside of it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That cleans things up but runs into lifetime issues - with that signature it mutably borrows the Operation for the span that the returned connection is used, which conflicts with the rest of the execution. Operation can be parameterized to give the return a &'conn mut Connection lifetime to disconnect the borrows, but that has substantial ripple effects on the rest of the driver code. Sadly, default lifetimes never made it in, because Operation<'conn = 'static> would be ideal here.


#[derive(Clone, Debug)]
pub(crate) struct PinnedConnection {
connection: Option<Arc<Mutex<Connection>>>,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's hypothetically possible that this doesn't need to be an Arc<Mutex<_>>, but could instead use a similar trick to the Session associated type in GetMoreProviderResult. However, couldn't get that to line up well after a day or so of work - particular challenges were the manual polling and state management in the cursor lifecycle, the drop impls for the cursors, and not having lifetimes leak out into the public cursor api - so I went with this solution.

There is a runtime cost incurred by the reference counting and locking, but it's unlikely to be a significant issue in practice because the cursors buffer values returned, so there's not going to be a hot loop involving those operations.

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 agree this is probably fine, and it's simple which is really nice considering the complexity of the existing session stuff. The runtime cost from the locking at least will be insignificant I think since cursors themselves can't be used concurrently.

}
}

pub(super) fn kill_cursor(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I opted to make this a free function rather than a method on PinnedConnection because it doesn't feel like it belongs to the connection as such.

@abr-egn abr-egn marked this pull request as ready for review August 30, 2021 20:21
@abr-egn abr-egn force-pushed the RUST-954/load-balancer-connection-pinning branch from 19075c9 to 5354a2f Compare August 31, 2021 18:00
self.execute_operation_pinned(op, session, None).await
}

pub(crate) async fn execute_operation_pinned<T: Operation>(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think a slight improvement would be to move the _pinned portion to the Operation trait by adding a new fn pinned_connection(&mut self) -> Option<&mut Connection>, similar to how we currently handle selection criteria on the operation. Then we can actually eliminate the _pinned method altogether and use just execute_operation most of the time. This will work nicely with transactions that have connections pinned to them as well, since we'll probably want the logic for that in the executor somewhere rather than outside of it.

src/client/executor.rs Outdated Show resolved Hide resolved
src/client/executor.rs Outdated Show resolved Hide resolved

#[derive(Clone, Debug)]
pub(crate) struct PinnedConnection {
connection: Option<Arc<Mutex<Connection>>>,
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 agree this is probably fine, and it's simple which is really nice considering the complexity of the existing session stuff. The runtime cost from the locking at least will be insignificant I think since cursors themselves can't be used concurrently.

src/cursor/common.rs Outdated Show resolved Hide resolved
@abr-egn
Copy link
Contributor Author

abr-egn commented Sep 2, 2021

I've updated this to use the discussed pin-by-id setup; the key point this enables is that a connection can be pinned while mutably borrowed rather than just owned, which will be crucial for transactions. As a side benefit, it also enables passing the pin via the Operation as Patrick suggested.

This comes at the cost of losing the potential performance improvements of directly owned connections - pinned will now have to incur the same channel round-trip as checked out - and some increased potential contention on the connection pool worker from handling pin takes / unpins. I think the latter is unlikely to be substantial (or even really measurable above normal noise), but if it is it could probably be mitigated by moving the pinned connections to a different structure with its own worker task and channel.

Copy link
Contributor

@patrickfreed patrickfreed left a comment

Choose a reason for hiding this comment

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

Looks great! I mostly have comments about naming/bikeshedding and documentation.

src/client/executor.rs Outdated Show resolved Hide resolved
src/cmap/manager.rs Outdated Show resolved Hide resolved
src/cursor/common.rs Outdated Show resolved Hide resolved
src/cursor/common.rs Outdated Show resolved Hide resolved
src/cursor/common.rs Show resolved Hide resolved
src/cursor/common.rs Show resolved Hide resolved
src/cursor/common.rs Outdated Show resolved Hide resolved
src/cmap/conn/mod.rs Outdated Show resolved Hide resolved
src/cmap/conn/mod.rs Outdated Show resolved Hide resolved
src/cmap/conn/mod.rs Outdated Show resolved Hide resolved
})
.await
}

/// Execute the given operation, returning the implicit session created for it if one was.
/// Execute the given operation, returning cursor created by the operation.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Execute the given operation, returning cursor created by the operation.
/// Execute the given operation, returning the cursor created by the operation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Applied.

Ok(())
}

pub(super) async fn take_pinned(&self, id: u32) -> Result<Connection> {
Copy link
Contributor

Choose a reason for hiding this comment

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

(For my own understanding) what is the difference between taking a pinned connection and unpinning?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pinning a connection removes it from the normal connection pool and makes it available to retrieve by id (wrapped by PinnedConnectionHandle). Taking a pinned connection is that retrieval; it enforces ownership mechanics at runtime by only allowing a connection to be taken by id if that connection had previously been pinned and dropped. Unpinning returns the connection to the normal connection pool, either immediately (if nothing's using it) or when it gets dropped.

Hopefully the docstrings added will help make this clearer :)

src/cursor/common.rs Show resolved Hide resolved
src/cmap/conn/mod.rs Outdated Show resolved Hide resolved
src/cmap/worker.rs Outdated Show resolved Hide resolved
src/cursor/common.rs Outdated Show resolved Hide resolved
src/cursor/common.rs Show resolved Hide resolved
Copy link
Contributor

@patrickfreed patrickfreed left a comment

Choose a reason for hiding this comment

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

LGTM, great job with this! it was a really tricky one

@abr-egn abr-egn force-pushed the RUST-954/load-balancer-connection-pinning branch from 53eab21 to 429a396 Compare September 14, 2021 13:27
@abr-egn abr-egn merged commit a0fa2b0 into mongodb:master Sep 14, 2021
awitten1 pushed a commit to awitten1/mongo-rust-driver that referenced this pull request Sep 21, 2021
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.

None yet

3 participants