coordinator: display consumer dynamic filters after execution - #623
coordinator: display consumer dynamic filters after execution#623jayshrivastava wants to merge 27 commits into
Conversation
2a2bffc to
f549dc2
Compare
|
Thanks for working on this: this will be very useful! One quick thought: the dynamic filter will sometimes be much, much larger than what you would actually want to display in an EXPLAIN plan (a large Also, I have a draft of a related change on our codebase, and it seemed like the easiest mechanism for transferring this kind of information back is via metrics... but the most natural/obvious thing that seemed to be missing in that case was essentially a "string" metric type (we would use it to display a chosen strategy/enum from a scan). Do you think that that might be worth pursuing upstream? |
Serializing them as a string is reasonable. Rather than a metric, I think we can implement a |
|
🤔 I'm not sure if I'm understanding the suggestion. Updating a filter with |
4ccec74 to
bac65ba
Compare
## Summary Closes #530 - This PR updates the upstream datafusion SHA to the HEAD of https://github.com/apache/datafusion/commits/branch-55/ (edit: this branch is continuously being updated. I will make sure this PR is at the head before merging) - Rust upgade to 1.94 ## Changes 1. In `src/protobuf/distributed_codec.rs` we now use the `proto_converter` argument during serde - We still don't use the `DeduplicatingProtoConverter`, so dynamic filters don't necessarily work. I think this is outside the scope of this PR will be addressed in #623, which will be rebased after the upgrade. 3. `ExecutionPlan::apply_expressions` is added for every custom `ExecutionPlan` in this repo - Wrapper types (`MetricsWrapperExec`, `WorkUnitFileScanConfig`, `DistributedLeafExec`) delegate to the inner type - Other plans take`TreeNodeRecursion::Continue` because they have no expressions (ex. `SamplerExec`) - Note that `apply_expressions` does not need to yield sort or partitioning expressions in the plan properties 3. We migrate from `partition_statistics` to `statistics_from_inputs` for every `ExecutionPlan`. - `src/distributed_planner/statistics/plan_statistics.rs` can just use `statistics_from_inputs` directly instead of doing the `StatisticsWrapper` workaround. 5. Range partitioning is now supported. - CPU costing now includes range-key comparison cost and has a new unit test. See src/distributed_planner/ statistics/complexity_cpu.rs:238. - I think there's open questions about range partitioning. I've opened an issue here to make sure it behaves as expected after the upgrade: #628 (comment) 6. Peak-memory metrics use the existing gauge wire representation. DataFusion added MetricValue::PeakMemoryUsage. It is serialized as the existing named-gauge protobuf variant to avoid a wire-format change. See src/protocol/grpc/ metrics_proto.rs:124. The value and name survive, and aggregation is still additive, but decoding produces a generic Gauge, not PeakMemoryUsage. The practical difference is mainly display formatting: it may render as a count rather than human-readable bytes. This is the clearest remaining compromise/risk in the upgrade. 7. File-scan rebalancing changed its discriminator. DataFusion removed partitioned_by_file_group; output_partitioning.is_some() is now the source of truth. See src/events/defaults/file_scan_config.rs:43. This decides whether files are round-robin rebalanced or split through FileGroupPartitioner, so it is behavior-sensitive even though it is a one-line migration. 8. Two previously ignored correctness tests were enabled. - See `tests/multi_task_collect_join_repros.rs` - These were upstream DataFusion correctness fixes, not fixes made locally in this upgrade. 9. drop(reporter) was made explicit on the sampler’s empty-input path. The reporter sends its result on Drop; explicitly dropping it both satisfies the new compiler/lint behavior and guarantees the zero-row EOS report is sent before returning. See src/ execution_plans/sampler.rs:259. 10. Plan changes - `dynamic_rg_pruning=eligible` is now displayed on eligible scans: 1,354 occurrences in TPC-DS, 188 in TPC-H, and 12 in ClickBench - `DataSourceExec` now displays its output partitioning. See `tests/join.rs` (eventually, someone should delete this test #628) - Project after sort. This looks like some upstream optimizer rule change ex. `tests/distributed_unions.rs` and `tests/distributed_aggregation.rs`. ``` - │ SortExec: expr=[MinTemp@0 ASC NULLS LAST, RainToday@1 ASC NULLS LAST], preserve_partitioning=[true] - │ ProjectionExec: expr=[MaxTemp@0 as MinTemp, RainToday@1 as RainToday] + │ ProjectionExec: expr=[MaxTemp@0 as MinTemp, RainToday@1 as RainToday] + │ SortExec: expr=[MaxTemp@0 ASC NULLS LAST, RainToday@1 ASC NULLS LAST], preserve_partitioning=[true] ``` - LocalLimitExec became more common: TPC-DS went from 0 to 20 occurrences and ClickBench from 1 to 21, reflecting additional local limit pushdown. - Subquery/semi-join plans became more distributed: - TPC-DS CollectLeft hash joins: 615 → 610 - TPC-DS partitioned hash joins: 98 → 103 - TPC-DS left-semi occurrences: 11 → 25 - TPC-DS network shuffles: 368 → 378 - TPC-H - just a few - These are meaningful topology changes: some subqueries now use partitioned left-semi joins and therefore introduce hash shuffles instead of collecting/broadcasting one side. - Scalar rendering improved, especially decimal literals: internal forms such as Some(0),7,2 now display as CAST(0.00 AS Decimal128(7, 2)). - Minor changes (Ex. tpcds 21) - `__common_expr_4` became `__common_expr_3`; that is only an internal alias renumbering. - The projection that renamed `d_date` to `__common_expr_2` disappeared. - `d_date` is retained directly in the join output and referenced directly by partial/final aggregates. - Column positions changed - File-group allocation changed substantially - Some explicit RoundRobinBatch repartitions disappeared and scans gained different numbers of file groups - Distribute byte ranges across partitions: apache/datafusion#22439 - Lowers `repartition_file_min_size` from 10 MiB to 1 MiB. The PR explicitly calls out TPC-DS SF1 dimension tables. Files may be duplicated across multiple partitions where but each partition reads a different byte range (this is hidden by <int>....<int>, but we know from the correctness tests that nothing broke). A lot of tpcds queries now split across `target_partitions` instead of staying under-partitioned. In the `tpcds` plan tests, we use `target_partitions=3`. Example: ``` - │ t0: DataSourceExec: file_groups={2 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ] - │ t1: DataSourceExec: file_groups={2 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ] - │ t2: DataSourceExec: file_groups={2 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ] - │ t3: DataSourceExec: file_groups={2 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ] + │ t0: DataSourceExec: file_groups={3 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ] + │ t1: DataSourceExec: file_groups={3 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ] + │ t2: DataSourceExec: file_groups={3 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ] + │ t3: DataSourceExec: file_groups={3 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ] ``` --------- Co-authored-by: Gabriel <45515538+gabotechs@users.noreply.github.com> Co-authored-by: Gabriel <gabriel.musatmestre@datadoghq.com>
bac65ba to
75e43d6
Compare
|
@gabotechs This is ready for another round. I've left some of the conversations open. I've refactored things quite a bit, mainly moving source code to The main thing that's still up for discussion is dynamic filter serialization. We have to do this to basically deep copy them to break in-memory relations. I've left a lot of comments in |
gabotechs
left a comment
There was a problem hiding this comment.
Still an incomplete review, but flushing today's comments, I'll resume review tomorrow. I'm mainly trying to invest some time in coming up with suggestions for improving the more sketchy parts, but for most of the things I'm not finding better alternatives to what you already have here.
Given the cards we are dealt with, this is looking like pretty solid work!
| // The store abstraction relies on one entry being present for each task to mark | ||
| // completition. Since not all tasks report dynamic filters, add placeholders here. | ||
| // | ||
| // Also, note that completed_dynamic_filter_store will be None if collection is | ||
| // disabled. | ||
| if let Some(store) = &completed_dynamic_filter_store | ||
| && store.get(&task_key).is_none() | ||
| { | ||
| store.insert(task_key, TaskCompletedDynamicFilters::default()); | ||
| } | ||
| }); |
There was a problem hiding this comment.
It feels a bit strange that we need to do this special case here. I'll keep reading.
| Ok(Transformed::no(plan)) | ||
| })?; | ||
| Ok((transformed.data, work_unit_feed_declarations)) | ||
| let plan = maybe_roundtrip_plan_to_sever_in_memory_dynamic_filter_relationships( |
There was a problem hiding this comment.
😂 I really like this long ugly name.
| // Dynamic-filter reports may carry decoded physical expressions. Retain the worker's | ||
| // task data so the gRPC boundary can encode them with its configured codecs, even after | ||
| // the completed task has been removed from the worker cache. | ||
| let task_data_entry = self | ||
| .task_data_entries | ||
| .get_with(task_key, async { Default::default() }) | ||
| .await; | ||
|
|
There was a problem hiding this comment.
Even if this is allowed, it feels like the grpc layer is access an internal detail of the Worker by accessing task_data_entries.
If we were to move the grpc module to a specific crate, this will start giving problems. Just something to keep in mind, I don't think we need to solve this now, what you have here is good enough.
There was a problem hiding this comment.
Sure, let's clean this up whenever it becomes a problem.
| /// worry about any shared state. | ||
| /// | ||
| /// [`update()`]: DynamicFilterPhysicalExpr::update() | ||
| pub(crate) fn sever_dynamic_filter_relationships_in_plan_for_display( |
There was a problem hiding this comment.
You did a pretty good job design these functions so that they are isolated from the main code paths. I still see all these as "hacks" we need to do for workarounding the fact that shared stated is being baked into the nodes and expressions.
I think this is a broader topic worth rising upstream (if not already), and all the workarounds we need to do in this project are a strong signal that shared state, in general, should be scoped to a TaskContext, not to individual ExecutionPlans or PhysicalExprs.
Ballista and other distribution layers are going to have the same issues, so I don't think this is a problem only this project will suffer.
There was a problem hiding this comment.
👍🏽 Will look out for ways to improve this
gabotechs
left a comment
There was a problem hiding this comment.
This is looking very good! I think this is my last round of comments before approval.
| pub(crate) async fn wait_for_dynamic_filters( | ||
| &self, | ||
| ) -> Result<Option<HashMap<TaskKey, TaskCompletedDynamicFilters>>> { | ||
| let Some(store) = &self.completed_dynamic_filter_store else { | ||
| return Ok(None); | ||
| }; | ||
| let plan = self.plan_for_viz()?; | ||
| Ok(Some(store.wait_for(&task_keys_for_plan(&plan)).await)) | ||
| } | ||
|
|
There was a problem hiding this comment.
Nit: For consistency, it's probably worth it to make the signature and behavior of this function match the one above (wait_for_metrics). Unless you think there's a reason to diverge.
I think we need to clean up all this API for collecting metrics, dyn filters, etc... so this might go away at that point anyways.
There was a problem hiding this comment.
| vec![ | ||
| self.prepared_execution | ||
| .get() | ||
| .map(|prepared| &prepared.plan_for_viz) | ||
| .unwrap_or(&self.base_plan), | ||
| ] | ||
| } |
There was a problem hiding this comment.
🤔 this is a behavior change that can blow up on other people's faces...
It will not blow up at Datadog, because this fits how we use DistributedExec, but I wonder if other people are actually relying on retrieving the original children of the DistributedExec after execution.
If you see opportunities for avoiding this change, I'd try to do it.
There was a problem hiding this comment.
Reverted. I think that's fair.
There's a weird lifecycle that the metrics rewrite does that I'm just going to preserve for now.
children() is always the base plan, but rewrite_distributed_plan_with_dynamic_filters and rewrite_distributed_plan_with_metrics replaces both the children and plan_for_viz.
- It replaces the
children()so users can iterate over theDistributedExec/Arc<dyn ExecutionPlanand extract metrics plan_for_vizis used fordisplay_plan_asciiin this repo
I opened for 2 separate APIs
with_new_childrenreplaces the childrenwith_plan_for_vizreplacesplan_for_viz
| let child = require_one_child(&children)?; | ||
| if self.prepared_execution.get().is_some() { | ||
| return self.with_plan_for_viz(child); | ||
| } | ||
| Ok(Arc::new(DistributedExec { |
There was a problem hiding this comment.
All the conditional logic about different behaviors for when there's a prepared_execution and when there's not is getting a bit messy...
I think there are some opportunities for improving this, but probably worth exploring in another PR.
There was a problem hiding this comment.
Removed the logic. I opted to reset the prepared_plan though. I felt that it doesn't make sense to change the children and leave the old prepared_plan / plan_for_viz.
|
|
||
| #[tokio::test] | ||
| async fn completed_filter_collection_can_be_disabled() -> Result<()> { | ||
| TestQuery::new( |
There was a problem hiding this comment.
This test is not really asserting that dynamic filters are not getting collected no?
There was a problem hiding this comment.
It happens in execute(). We assert that the plan isn't rewritten because there's no dynamic filters to collect. I'll add a comment.
| /// worry about any shared state. | ||
| /// | ||
| /// [`update()`]: DynamicFilterPhysicalExpr::update() | ||
| pub(crate) fn sever_dynamic_filter_relationships_in_plan_for_display( |
There was a problem hiding this comment.
Here's one idea that is comes to mind:
What if we add yet another preparatory step for the plan in distributed_planner/, something like insert_broadcast or normalize_collect_joins, that severs all dynamic filter connections for good?
This would imply that dynamic filters will never be able to work through normal upstream mechanisms, and they should always be updated passing through the coordinator, even in the local case, but I do imagine this can simplify the overall approach, specially for future PRs.
There was a problem hiding this comment.
I would like to leave this for now.
The tricky part is that we want to sever some relationships but not all of them. For example, if there's local dynamic filters, a producer should be able to atomically update them in memory.
In future PRs, I detect local vs remote dynamic filters during static and dynamic planning, so we can think about severing some relationships then.
131517e to
2f10b0b
Compare
| /// Execution state produced by distributed planning (static or dynamic) retained | ||
| /// for post-execution work such as plan rewrites to display metrics and dynamic filters. | ||
| #[derive(Debug, Clone)] | ||
| struct PreparedExecution { |
There was a problem hiding this comment.
Now that TaskCtx is gone, we can now use PreparedPlan directly
| let task_metrics = self.metrics_store.as_ref()?; | ||
| let plan = &self.prepared_plan.get()?.plan_for_viz; | ||
| Some(task_metrics.wait_for(&task_keys_for_plan(plan)).await) | ||
| } |
There was a problem hiding this comment.
I decided this is the cleanest API: "wait until all task datas are present and then return them". It lets us remove the whole get() API on the store and just work with HashMap<...> directly
There was a problem hiding this comment.
👍 yeap, this does look clean indeed, thanks!
| } | ||
|
|
||
| /// Gathers metrics that belong to a task as a whole rather than to an execution-plan node. | ||
| fn stage_metrics( |
There was a problem hiding this comment.
Basically a copy paste of gather_stage_header_metrics removed below
|
@gabotechs Addressed the comments in the last few commits. I left the nontrivial comments open. I can check CI and rebase tomorrow 🫡 |
| ┌───── Stage 1 ── tasks=4, partitions=8 | ||
| │ SortExec: TopK(fetch=5), expr=[id@0 ASC NULLS LAST], preserve_partitioning=[true] | ||
| │ DistributedLeafExec: | ||
| │ t0: DataSourceExec: file_groups={2 groups: [[/target/multi_task_collect_join_repros/build_side/part-0.parquet:<int>..<int>], [/target/multi_task_collect_join_repros/build_side/part-2.parquet:<int>..<int>]]}, projection=[id], file_type=parquet, predicate=DynamicFilter [ empty ], sort_order_for_reorder=[id@0 ASC NULLS LAST], dynamic_rg_pruning=eligible |
There was a problem hiding this comment.
We lose these fields when doing the proto roundtrip before displaying. So, this means we actually lose these fields during execution since we serialize this plan before executing.
In a way, roundtripping the plan before displaying gives us a better picture of what's actually happening during execution.
I've addressed this here: apache/datafusion#24930, so these optimizations will come back in df56.
|
benchmarks run tpch/sf100 |
|
Requested by this comment. Benchmark resultsCompared: PR base === Comparing tpch/sf100 results 'datafusion-benchmark-base' [prev] with 'datafusion-benchmark-head' [new] === TASKS: prev=849.0, new=849.0, diff=no change (sum of per-query averages) TOTAL: prev=82049 ms, new=81259 ms, diff=1.01 faster ✔ Show full query output q1: prev=3251 ms, new=2890 ms, diff=1.12 faster ✔, tasks: prev=18.0, new=18.0, diff=no change
q2: prev=3485 ms, new=3668 ms, diff=1.05 slower ✖, tasks: prev=40.0, new=40.0, diff=no change
q3: prev=3397 ms, new=2968 ms, diff=1.14 faster ✔, tasks: prev=48.0, new=48.0, diff=no change
q4: prev=1441 ms, new=1437 ms, diff=1.00 faster ✔, tasks: prev=38.0, new=38.0, diff=no change
q5: prev=4820 ms, new=4810 ms, diff=1.00 faster ✔, tasks: prev=52.0, new=52.0, diff=no change
q6: prev=1510 ms, new=1758 ms, diff=1.16 slower ✖, tasks: prev=12.0, new=12.0, diff=no change
q7: prev=10115 ms, new=10316 ms, diff=1.02 slower ✖, tasks: prev=53.0, new=53.0, diff=no change
q8: prev=5518 ms, new=5374 ms, diff=1.03 faster ✔, tasks: prev=73.0, new=73.0, diff=no change
q9: prev=9982 ms, new=10005 ms, diff=1.00 slower ✖, tasks: prev=77.0, new=77.0, diff=no change
q10: prev=7865 ms, new=7664 ms, diff=1.03 faster ✔, tasks: prev=40.0, new=40.0, diff=no change
q11: prev=3471 ms, new=3581 ms, diff=1.03 slower ✖, tasks: prev=24.0, new=24.0, diff=no change
q12: prev=2239 ms, new=2062 ms, diff=1.09 faster ✔, tasks: prev=44.0, new=44.0, diff=no change
q13: prev=1991 ms, new=1959 ms, diff=1.02 faster ✔, tasks: prev=32.0, new=32.0, diff=no change
q14: prev=1852 ms, new=1829 ms, diff=1.01 faster ✔, tasks: prev=24.0, new=24.0, diff=no change
q15: prev=4271 ms, new=4101 ms, diff=1.04 faster ✔, tasks: prev=30.0, new=30.0, diff=no change
q16: prev= 826 ms, new= 972 ms, diff=1.18 slower ✖, tasks: prev=47.0, new=47.0, diff=no change
q17: prev=5002 ms, new=5125 ms, diff=1.02 slower ✖, tasks: prev=40.0, new=40.0, diff=no change
q18: prev=5771 ms, new=5682 ms, diff=1.02 faster ✔, tasks: prev=68.0, new=68.0, diff=no change
q19: prev=2179 ms, new=2209 ms, diff=1.01 slower ✖, tasks: prev=24.0, new=24.0, diff=no change
q20: prev=3063 ms, new=2849 ms, diff=1.08 faster ✔, tasks: prev=65.0, new=65.0, diff=no change
q21: Previously failed, and now also failed ❌
q22: Previously failed, and now also failed ❌
Verification and run detailsJob
Workload: Capacity: 12 Other timings: Queue 1s · Dataset validation 0s · Total 20m 21s |
Stack
This stack of PRs implements distributed dynamic filtering #528
Closes: #529
Problem
Post df-55 upgrade, dynamic filters should work in the worker-local case. There's no way to observe them working other than looking at metrics.
Ideally we want the final filters visible when displaying plans.
Solution
This PR adds a new protocol which is basically identical to the metrics protocol. Even the
MetricsStoreis now justStoreand is generic overTaskMetricsandTaskCompletedDynamicFilters(contains completed dynamic filters for a task).Similar to the metrics protocol, workers now collect completed dynamic filters and send them back to the coordinator.
Then, at display time, we call
apply_reports_to_distributed_leaveswhich traverses theplan_for_vizand updates the dynamic filters for all the variants:Notes
Duplicate RPC Messages
We will eventually have more dynamic filter RPCs which manage the worker -> coordinator -> merge -> worker flow mentioned in #553.
In theory, the coordinator will know at
mergetime what the completed filters are, making theTaskCompletedDynamicFiltersand final worker -> coordinator message in this PR irrelevant.However, I think having these mechanisms be separate is good because a) it helps us validate that the dynamic filter coordinator -> worker flow work using external "oracle", and b) there's no guarantee that the coordinator -> worker propagation happens before the query is done (ex. the
DataSourceExecmay not block execution waiting for dynamic filters), so it's good to have a separate way to know if the finalDataSourceExecapplied a filter or not.AND trueand empty filtersIn this filter
AND trueoccurs because of apache/datafusion#24277. The firstDynamicFilteris active but we lose theHashTableLookupExprwhen serializing it to send back to the coordinator.The 2nd filter is
DynamicFilter [ empty ]because this is a dynamic filter produced by a remote producer, which does not get propagated to this node yet. This will be fixed later.Displaying Dynamic Filters
Protocol is as similar to the metrics protocol as possible. Due to double wrapping (
MetricsWrapperExecwrapsDistributedLeafExec, it's tricky to do the dynamic filter rewrite after doing the metrics rewrite. Sorewrite_distributed_plan_with_dynamic_filtershas to be called first.Testing
tests/dynamic_filtering.rs