-
Notifications
You must be signed in to change notification settings - Fork 63
Expand file tree
/
Copy pathdistributed.rs
More file actions
228 lines (207 loc) · 9.12 KB
/
Copy pathdistributed.rs
File metadata and controls
228 lines (207 loc) · 9.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
use crate::common::{require_one_child, serialize_uuid};
use crate::coordinator::metrics_store::MetricsStore;
use crate::coordinator::prepare_static_plan::prepare_static_plan;
use crate::coordinator::query_coordinator::QueryCoordinator;
use crate::distributed_planner::NetworkBoundaryExt;
use crate::worker::generated::worker::TaskKey;
use datafusion::common::internal_datafusion_err;
use datafusion::common::tree_node::{TreeNode, TreeNodeRecursion};
use datafusion::common::{Result, exec_err};
use datafusion::execution::{SendableRecordBatchStream, TaskContext};
use datafusion::physical_expr_common::metrics::MetricsSet;
use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
use datafusion::physical_plan::stream::RecordBatchReceiverStreamBuilder;
use datafusion::physical_plan::{DisplayAs, DisplayFormatType, ExecutionPlan, PlanProperties};
use futures::StreamExt;
use std::fmt::Formatter;
use std::sync::{Arc, Mutex};
/// [ExecutionPlan] that executes the inner plan in distributed mode.
/// Before executing it, two modifications are lazily performed on the plan:
/// 1. Assigns worker URLs to all the stages. Unless explicitly set in
/// [crate::TaskEstimator::route_tasks], a random set of URLs are sampled from the
/// channel resolver and assigned to each task in each stage.
/// 2. Encodes all the plans in protobuf format so that network boundary nodes can send them
/// over the wire.
#[derive(Debug)]
pub struct DistributedExec {
/// Initial [ExecutionPlan] present before execution.
/// - If the plan was distributed statically, this will be the final distributed plan with all
/// the appropriate network boundaries in it.
/// - If the plan is going to be distributed dynamically during execution, this is the initial
/// non-distributed plan.
base_plan: Arc<dyn ExecutionPlan>,
/// Resulting [ExecutionPlan] after execution ready for visualization purposes.
/// - If the plan was distributed statically, this is equal to the base plan.
/// - If the plan is going to be distributed dynamically during execution, this is the resulting
/// plan re-calculated based on runtime statistics.
plan_for_viz: Arc<Mutex<Option<Arc<dyn ExecutionPlan>>>>,
/// The head stage meant to be executed locally on [DistributedExec::execute].
head_stage: Arc<Mutex<Option<Arc<dyn ExecutionPlan>>>>,
/// DataFusion metrics.
metrics: ExecutionPlanMetricsSet,
/// Storage where metrics collected from workers at runtime will place their results as they
/// finish their respective remote tasks.
pub(crate) metrics_store: Option<Arc<MetricsStore>>,
}
pub(super) struct PreparedPlan {
/// The head stage meant to be executed locally by the coordinator.
pub(super) head_stage: Arc<dyn ExecutionPlan>,
/// A final representation of the plan for visualization purposes.
pub(super) plan_for_viz: Arc<dyn ExecutionPlan>,
}
impl DistributedExec {
pub fn new(base_plan: Arc<dyn ExecutionPlan>) -> Self {
Self {
base_plan,
plan_for_viz: Arc::new(Mutex::new(None)),
head_stage: Arc::new(Mutex::new(None)),
metrics: ExecutionPlanMetricsSet::new(),
metrics_store: None,
}
}
/// Enables task metrics collection from remote workers.
pub fn with_metrics_collection(mut self, enabled: bool) -> Self {
self.metrics_store = match enabled {
true => Some(Arc::new(MetricsStore::new())),
false => None,
};
self
}
/// Waits until all worker tasks have reported their metrics back via the coordinator channel.
///
/// Metrics are delivered asynchronously after query execution completes, so callers that need
/// complete metrics (e.g. for observability or display) should await this before inspecting
/// [`Self::task_metrics`] or calling [`rewrite_distributed_plan_with_metrics`].
///
/// [`rewrite_distributed_plan_with_metrics`]: crate::rewrite_distributed_plan_with_metrics
pub async fn wait_for_metrics(&self) {
let mut expected_keys: Vec<TaskKey> = Vec::new();
let Some(task_metrics) = &self.metrics_store else {
return;
};
let Some(plan) = self.plan_for_viz.lock().unwrap().as_ref().cloned() else {
return;
};
let _ = plan.apply(|plan| {
if let Some(boundary) = plan.as_network_boundary() {
let stage = boundary.input_stage();
for i in 0..stage.task_count() {
expected_keys.push(TaskKey {
query_id: serialize_uuid(&stage.query_id()),
stage_id: stage.num() as u64,
task_number: i as u64,
});
}
}
Ok(TreeNodeRecursion::Continue)
});
if expected_keys.is_empty() {
return;
}
let mut rx = task_metrics.rx.clone();
let _ = rx
.wait_for(|map| expected_keys.iter().all(|key| map.contains_key(key)))
.await;
}
/// Returns the plan which is lazily prepared on `execute()` and actually gets executed.
/// It is updated on every call to `execute()`. Returns an error if `.execute()` has not been
/// called.
pub(crate) fn plan_for_viz(&self) -> Result<Arc<dyn ExecutionPlan>> {
self.plan_for_viz
.lock()
.map_err(|e| internal_datafusion_err!("Failed to lock prepared plan: {}", e))?
.clone()
.ok_or_else(|| {
internal_datafusion_err!("No prepared plan found. Was execute() called?")
})
}
/// Returns the head stage that was actually executed. Unlike [`Self::plan_for_viz`] (which is
/// reconstructed for visualization, with `Stage::Local` boundaries and rebuilt ancestor
/// `Arc`s), this returns the original `Arc` instances whose metrics were populated during
/// execution.
pub(crate) fn head_stage(&self) -> Result<Arc<dyn ExecutionPlan>> {
self.head_stage
.lock()
.map_err(|e| internal_datafusion_err!("Failed to lock head stage: {}", e))?
.clone()
.ok_or_else(|| internal_datafusion_err!("No head stage found. Was execute() called?"))
}
}
impl DisplayAs for DistributedExec {
fn fmt_as(&self, _: DisplayFormatType, f: &mut Formatter) -> std::fmt::Result {
write!(f, "DistributedExec")
}
}
impl ExecutionPlan for DistributedExec {
fn name(&self) -> &str {
"DistributedExec"
}
fn properties(&self) -> &Arc<PlanProperties> {
self.base_plan.properties()
}
fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
vec![&self.base_plan]
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn ExecutionPlan>>,
) -> Result<Arc<dyn ExecutionPlan>> {
Ok(Arc::new(DistributedExec {
base_plan: require_one_child(&children)?,
plan_for_viz: Arc::new(Mutex::new(None)),
head_stage: Arc::new(Mutex::new(None)),
metrics: self.metrics.clone(),
metrics_store: self.metrics_store.clone(),
}))
}
fn execute(
&self,
partition: usize,
context: Arc<TaskContext>,
) -> Result<SendableRecordBatchStream> {
if partition > 0 {
// The DistributedExec node calls try_assign_urls() lazily upon calling .execute(). This means
// that .execute() must only be called once, as we cannot afford to perform several
// random URL assignation while calling multiple partitions, as they will differ,
// producing an invalid plan
return exec_err!(
"DistributedExec must only have 1 partition, but it was called with partition index {partition}"
);
}
let base_plan = Arc::clone(&self.base_plan);
let plan_for_viz = Arc::clone(&self.plan_for_viz);
let head_stage = Arc::clone(&self.head_stage);
let query_coordinator = QueryCoordinator::new(
Arc::clone(&context),
&self.metrics,
self.metrics_store.clone(),
);
let mut builder = RecordBatchReceiverStreamBuilder::new(self.schema(), 1);
let tx = builder.tx();
builder.spawn(async move {
let _guard = query_coordinator.end_query_guard();
let result = prepare_static_plan(&query_coordinator, &base_plan)?;
plan_for_viz
.lock()
.expect("poisoned lock")
.replace(result.plan_for_viz);
head_stage
.lock()
.expect("poisoned lock")
.replace(Arc::clone(&result.head_stage));
let mut stream = result.head_stage.execute(partition, context)?;
while let Some(msg) = stream.next().await {
if tx.send(msg).await.is_err() {
break; // channel closed
}
}
drop(tx);
query_coordinator.drain_pending_tasks().await?;
Ok(())
});
Ok(builder.build())
}
fn metrics(&self) -> Option<MetricsSet> {
Some(self.metrics.clone_inner())
}
}