Decouple gRPC from the worker protocol and add an in-process transport - #573
Decouple gRPC from the worker protocol and add an in-process transport#573stuhood wants to merge 4 commits into
Conversation
The prost message types carry no tonic dependency, so a transport that is not gRPC can speak the same wire shape without pulling in the gRPC stack. Only the tonic client and server stay gated; the generator emits those gates so a regeneration cannot drop them. tonic-prost feeds only the generated client and server, so it moves behind the feature too. Co-authored-by: Stu Hood <stuhood@gmail.com>
The benchmarks crate's dev-dependency on the lib re-unified grpc into every test build, so a genuine no-gRPC test run was impossible; the dataset suites move into the benchmarks crate and the gRPC-coupled test utilities gate behind grpc. A unit-test-no-grpc job then runs the whole lib suite with the feature off. Co-authored-by: Stu Hood <stuhood@gmail.com>
InProcessChannelResolver routes the three protocol methods straight to a co-located Worker, with no gRPC, no IPC, and no serialization round-trip: the reference implementation of the protocol for a co-located worker, and the first transport that exercises the abstraction with grpc off. Its end-to-end test (a distributed GROUP BY across tasks) runs under the no-gRPC CI job. Co-authored-by: Stu Hood <stuhood@gmail.com>
9be56b0 to
3888470
Compare
gabotechs
left a comment
There was a problem hiding this comment.
This is starting to look good!
I see there are different pieces of work that can be factored out into independent PRs that we can ship incrementally. I identify the following pieces of work in the following order:
- split
worker.protointoworker_service.protoandmessages.proto, putting some thought into where to place the proto generator script so that we don't copy-paste too much. - Ship the
InProcessWorkerChannel, and wire it up to channel_resolver.rs (or somewhere else that makes sense) so that we can start executing queries withoutgrpc. Probably, the interaction ofInProcessWorkerChannelandLocalWorkerContextneeds to happen here. - Tweak existing tests unit tests that rely on
grpcso that they just use in-process comms (mainly the tests about rewritting plans with metrics). That will allow to decouple them for gRPC by using the in-process implementation. - Tweak the remaining test utils, moving some to grpc/ and flagging them appropriately:
- If
grpcfeature is enabled, then gRPC worker channels are used (still with in-process fallbacks for self urls) - If
grpcis disabled, exclusively in-process worker channels are used. This should be transparent to the tests.
- If
| if event::poll(Duration::from_millis(16))? | ||
| && let Event::Key(key) = event::read()? | ||
| { | ||
| input::handle_key_event(app, key); | ||
| } |
There was a problem hiding this comment.
🤔 this change is unrelated right? if it is, I'd avoid introducing unrelated changes, mainly for better traceability.
| if old_task.status == TaskStatus::Running as i32 | ||
| && let Some(sk) = &old_task.task_key | ||
| { |
| let proto_file = proto_dir.join("worker.proto"); | ||
| let out_dir = repo_root.join("src/protocol/grpc/generated"); | ||
| let protocol_dir = repo_root.join("src/protocol"); | ||
| let messages_proto = protocol_dir.join("messages.proto"); |
There was a problem hiding this comment.
This generator file is under the grpc scope, but it's in charge of generating messages.proto which is not part of grpc.
Probably copy-pasting this generator script one folder above is not worth it. What about moving it to the root of the project under a codegen/ folder or something like that? we can centralize proto generation there.
|
|
||
| #[cfg(feature = "grpc")] | ||
| #[allow(clippy::all)] | ||
| pub mod worker_grpc; |
There was a problem hiding this comment.
If this is specific to grpc, ideally it should be generated under the grpc/ folder
| #[cfg(feature = "grpc")] | ||
| mod channel { | ||
| use super::InMemoryWorkerResolver; |
There was a problem hiding this comment.
All the test utils that are specific to grpc should probably live under the grpc/ folder, that way they are gated automatically.
| // `worker_handles` builds `tonic` servers and Flight channels for the benchmark fixtures, which | ||
| // only compile with the gRPC transport. | ||
| #[cfg(all(feature = "grpc", any(test, feature = "integration")))] | ||
| pub(crate) mod test_utils; |
There was a problem hiding this comment.
This is the pattern we should aim to get rid of in the project: if it's grpc related, it should ideally live under grpc/, this means that we probably need to refactor some tests to rely on the in-memory representation.
| } | ||
|
|
||
| #[cfg(test)] | ||
| #[cfg(all(test, feature = "grpc"))] |
There was a problem hiding this comment.
Seems like this is needed because this code is used in the task_metrics_rewriter.rs tests, and those tests are still coupled to grpc.
It'd be nice to decouple that test from grpc first so that we can avoid flagging this here.
| @@ -1,4 +1,4 @@ | |||
| #[cfg(all(feature = "integration", feature = "clickbench", test))] | |||
| #[cfg(all(feature = "clickbench", test))] | |||
There was a problem hiding this comment.
These tests should really not be under benchmarks. I think I might be missing the reason for these to be here. Do they really need to be here?
|
|
||
| #[cfg(test)] | ||
| #[cfg(all(test, feature = "grpc"))] | ||
| pub(crate) fn inner(&self) -> &Arc<dyn ExecutionPlan> { |
There was a problem hiding this comment.
This is only used in task_metrics_rewriter.rs tests. If we manage to decouple those tests from grpc, we can leave this untouched.
| /// state for every task, keyed by [`crate::TaskKey`], the same way the gRPC worker does when several | ||
| /// tasks of a query land on it. | ||
| #[derive(Clone)] | ||
| pub struct InProcessChannelResolver { |
There was a problem hiding this comment.
For a follow-up PR: this should somehow interact with channel_resolver.rs and LocalWorkerContext so that, once a request is made to a URL that matches LocalWorkerContext.self_url, the InProcessChannelResolver kicks in automatically.
What
Decouples the
datafusion-distributedcore logic from thegrpctransport and introduces an in-process transport for single-process distributed task execution.Why
Previously, the worker protocol and the
grpcfeature were coupled, which meant any use of the distributed execution abstraction required thetonicgRPC stack. Separating the transport layer from the protocol messages allows the framework to be used with alternative transports. The in-process transport provides a mechanism to run and test the abstraction when thegrpcfeature is disabled.How
worker.protointomessages.proto(data structures) andworker_service.proto(gRPC service definition in theworker_grpcpackage). This splits the generated code intoworker.rs(always compiled) andworker_grpc.rs(compiled only when thegrpcfeature is enabled).InProcessChannelResolver. It implementsWorkerChannelby routing the three protocol methods directly to a co-locatedWorkerinstance in the same process, bypassing IPC, networking, and serialization.benchmarksdev-dependency to preventgrpcfrom being enabled in every test build.test_utilsbehind thegrpcfeature.integrationfeature flag still assumesgrpcis enabled becausetest_utils::InMemoryChannelResolveris used across the integration suite.)Tests
unit-test-no-grpcCI job that verifies the core library and the in-process transport compile and pass unit tests without thegrpcfeature.InProcessChannelResolver(a distributedGROUP BYacross tasks) that runs in the no-gRPC CI job.