risingwave_meta/barrier/checkpoint/independent_job/iceberg_v3_job/
render.rs1use std::collections::{HashMap, HashSet};
16
17use risingwave_common::util::stream_graph_visitor::visit_stream_node_cont;
18use risingwave_meta_model::WorkerId;
19use risingwave_pb::id::{ActorId, FragmentId, PartialGraphId};
20use risingwave_pb::stream_plan::stream_node::NodeBody;
21use risingwave_pb::stream_plan::{PbStreamNode, UpdateMutation};
22
23use super::super::RenderedIndependentJobActors;
24use crate::MetaResult;
25use crate::barrier::edge_builder::FragmentEdgeBuilder;
26use crate::barrier::rpc::ControlStreamManager;
27use crate::controller::fragment::InflightFragmentInfo;
28use crate::model::{
29 DownstreamFragmentRelation, FragmentDownstreamRelation, StreamActor, StreamJobActorsToCreate,
30};
31
32#[derive(Debug)]
33pub(super) struct CompactionTransition {
34 pub actors_to_create: StreamJobActorsToCreate,
35 pub mutation: UpdateMutation,
36}
37
38#[derive(Debug)]
40pub(super) struct CompactionTransitions {
41 pub node_actors: HashMap<WorkerId, HashSet<ActorId>>,
43 pub begin_compaction: CompactionTransition,
44 pub end_compaction: CompactionTransition,
45}
46
47#[derive(Debug)]
50pub(super) struct RenderedResolver {
51 pub fragment_info: InflightFragmentInfo,
52 pub stream_actors: Vec<StreamActor>,
54 pub downstream_relation: DownstreamFragmentRelation,
55}
56
57#[derive(Debug)]
60pub(super) struct IcebergV3FragmentPartition {
61 normal: HashSet<FragmentId>,
62 resolver: HashSet<FragmentId>,
63 long_running: HashSet<FragmentId>,
64}
65
66pub(super) fn build_compaction_transitions(
67 active_fragment_infos: &HashMap<FragmentId, InflightFragmentInfo>,
68 active_stream_actors: &HashMap<FragmentId, Vec<StreamActor>>,
69 active_downstreams: &FragmentDownstreamRelation,
70 resolver: &RenderedResolver,
71 partition: &IcebergV3FragmentPartition,
72 partial_graph_id: PartialGraphId,
73 control_stream_manager: &ControlStreamManager,
74) -> MetaResult<CompactionTransitions> {
75 let fragment_infos: HashMap<_, _> = active_fragment_infos
76 .iter()
77 .map(|(&fragment_id, info)| (fragment_id, info))
78 .chain(std::iter::once((
79 resolver.fragment_info.fragment_id,
80 &resolver.fragment_info,
81 )))
82 .collect();
83 let stream_actors: HashMap<_, _> = active_stream_actors
84 .iter()
85 .map(|(&fragment_id, actors)| (fragment_id, actors.as_slice()))
86 .chain(std::iter::once((
87 resolver.fragment_info.fragment_id,
88 resolver.stream_actors.as_slice(),
89 )))
90 .collect();
91 let mut downstreams = active_downstreams.clone();
92 assert!(
93 downstreams
94 .insert(
95 resolver.fragment_info.fragment_id,
96 vec![resolver.downstream_relation.clone()],
97 )
98 .is_none(),
99 "resolver relation must be absent from the active graph"
100 );
101 let begin_compaction = build_transition(
102 &partition.normal,
103 &partition.resolver,
104 &partition.long_running,
105 &fragment_infos,
106 &stream_actors,
107 &downstreams,
108 partial_graph_id,
109 control_stream_manager,
110 )?;
111 let end_compaction = build_transition(
112 &partition.resolver,
113 &partition.normal,
114 &partition.long_running,
115 &fragment_infos,
116 &stream_actors,
117 &downstreams,
118 partial_graph_id,
119 control_stream_manager,
120 )?;
121 let node_actors = InflightFragmentInfo::actor_ids_to_collect(fragment_infos.values().copied());
122
123 Ok(CompactionTransitions {
124 node_actors,
125 begin_compaction,
126 end_compaction,
127 })
128}
129
130fn ensure_writer_relation(
131 downstreams: &FragmentDownstreamRelation,
132 edge_fragment_id: FragmentId,
133 writer_fragment_id: FragmentId,
134) -> MetaResult<()> {
135 if downstreams
136 .get(&edge_fragment_id)
137 .into_iter()
138 .flatten()
139 .any(|relation| relation.downstream_fragment_id == writer_fragment_id)
140 {
141 Ok(())
142 } else {
143 Err(anyhow::anyhow!(
144 "fragment {} has no edge to writer fragment {}",
145 edge_fragment_id,
146 writer_fragment_id
147 )
148 .into())
149 }
150}
151
152fn build_transition(
153 stopping_fragment_ids: &HashSet<FragmentId>,
154 starting_fragment_ids: &HashSet<FragmentId>,
155 long_running_fragment_ids: &HashSet<FragmentId>,
156 fragment_infos: &HashMap<FragmentId, &InflightFragmentInfo>,
157 stream_actors: &HashMap<FragmentId, &[StreamActor]>,
158 downstreams: &FragmentDownstreamRelation,
159 partial_graph_id: PartialGraphId,
160 control_stream_manager: &ControlStreamManager,
161) -> MetaResult<CompactionTransition> {
162 let (mut edges, _) = FragmentEdgeBuilder::new()
163 .add_existing_fragments(
164 stopping_fragment_ids
165 .iter()
166 .map(|fragment_id| fragment_infos[fragment_id])
167 .chain(
168 long_running_fragment_ids
169 .iter()
170 .map(|fragment_id| fragment_infos[fragment_id]),
171 ),
172 partial_graph_id,
173 control_stream_manager,
174 )
175 .stop_existing_fragments(stopping_fragment_ids.iter().copied())
176 .add_new_fragments(
177 starting_fragment_ids
178 .iter()
179 .map(|fragment_id| fragment_infos[fragment_id]),
180 partial_graph_id,
181 control_stream_manager,
182 )
183 .finish_fragments()
184 .add_relations(downstreams)?
185 .build();
186 let actors_to_create =
187 edges.collect_actors_to_create(starting_fragment_ids.iter().map(|fragment_id| {
188 let fragment = fragment_infos[fragment_id];
189 (
190 fragment.fragment_id,
191 &fragment.nodes,
192 fragment.actors.iter().map(|(actor_id, actor)| {
193 let stream_actor = stream_actors[&fragment.fragment_id]
194 .iter()
195 .find(|stream_actor| stream_actor.actor_id == *actor_id)
196 .expect("rendered actor should exist");
197 (stream_actor, actor.worker_id)
198 }),
199 vec![],
200 )
201 }));
202 let mut mutation = UpdateMutation {
203 dropped_actors: stopping_fragment_ids
204 .iter()
205 .flat_map(|fragment_id| fragment_infos[fragment_id].actors.keys().copied())
206 .collect(),
207 ..Default::default()
208 };
209 edges.apply_to_update_mutation(&mut mutation);
210 Ok(CompactionTransition {
211 actors_to_create,
212 mutation,
213 })
214}
215
216pub(super) fn partition_resolver(
220 mut rendered: RenderedIndependentJobActors,
221 downstreams: &mut FragmentDownstreamRelation,
222) -> MetaResult<(
223 RenderedIndependentJobActors,
224 RenderedResolver,
225 IcebergV3FragmentPartition,
226)> {
227 let writer_fragment = rendered
230 .fragment_infos
231 .values()
232 .find(|fragment| find_writer_node(&fragment.nodes).is_some())
233 .ok_or_else(|| anyhow::anyhow!("Iceberg V3 graph has no pk-index writer"))?;
234 let writer_fragment_id = writer_fragment.fragment_id;
235 let writer_node = find_writer_node(&writer_fragment.nodes)
236 .expect("writer fragment was selected by its writer node");
237 let [normal_input, resolver_input] = writer_node.input.as_slice() else {
238 return Err(anyhow::anyhow!("Iceberg writer must have exactly two inputs").into());
239 };
240 let Some(NodeBody::Merge(normal_merge)) = normal_input.node_body.as_ref() else {
241 return Err(anyhow::anyhow!("Iceberg writer normal input must be a Merge node").into());
242 };
243 let Some(NodeBody::Merge(resolver_merge)) = resolver_input.node_body.as_ref() else {
244 return Err(anyhow::anyhow!("Iceberg writer resolver input must be a Merge node").into());
245 };
246 let normal_fragment_id = normal_merge.upstream_fragment_id;
247 let resolver_fragment_id = resolver_merge.upstream_fragment_id;
248
249 let mut long_running = HashSet::from([writer_fragment_id]);
252 let mut pending = vec![writer_fragment_id];
253 while let Some(fragment_id) = pending.pop() {
254 for downstream in downstreams.get(&fragment_id).into_iter().flatten() {
255 if long_running.insert(downstream.downstream_fragment_id) {
256 pending.push(downstream.downstream_fragment_id);
257 }
258 }
259 }
260 let normal: HashSet<_> = rendered
263 .fragment_infos
264 .keys()
265 .filter(|fragment_id| {
266 !long_running.contains(*fragment_id) && **fragment_id != resolver_fragment_id
267 })
268 .copied()
269 .collect();
270 if !normal.contains(&normal_fragment_id) {
271 return Err(anyhow::anyhow!(
272 "normal input fragment {} was classified as long-running",
273 normal_fragment_id
274 )
275 .into());
276 }
277 ensure_writer_relation(downstreams, normal_fragment_id, writer_fragment_id)?;
280 ensure_writer_relation(downstreams, resolver_fragment_id, writer_fragment_id)?;
281 let partition = IcebergV3FragmentPartition {
282 normal,
283 resolver: HashSet::from([resolver_fragment_id]),
284 long_running,
285 };
286
287 let resolver_fragment = rendered
290 .fragment_infos
291 .remove(&resolver_fragment_id)
292 .ok_or_else(|| {
293 anyhow::anyhow!(
294 "Iceberg writer resolver fragment {} was not rendered",
295 resolver_fragment_id
296 )
297 })?;
298 if !contains_resolver(&resolver_fragment.nodes) {
299 return Err(anyhow::anyhow!(
300 "Iceberg writer resolver input fragment {} has no resolver node",
301 resolver_fragment_id
302 )
303 .into());
304 }
305 let resolver_actors = rendered
306 .stream_actors
307 .remove(&resolver_fragment_id)
308 .ok_or_else(|| {
309 anyhow::anyhow!(
310 "Iceberg writer resolver fragment {} has no rendered actors",
311 resolver_fragment_id
312 )
313 })?;
314
315 let downstream_relation = {
316 let relations = downstreams.get_mut(&resolver_fragment_id).ok_or_else(|| {
317 anyhow::anyhow!(
318 "resolver fragment {} has no downstream relation",
319 resolver_fragment_id
320 )
321 })?;
322 let [relation] = relations.as_slice() else {
323 return Err(anyhow::anyhow!(
324 "expected resolver fragment {} to have exactly one downstream relation, found {}",
325 resolver_fragment_id,
326 relations.len(),
327 )
328 .into());
329 };
330 if relation.downstream_fragment_id != writer_fragment_id {
331 return Err(anyhow::anyhow!(
332 "resolver fragment {} points to fragment {} instead of writer fragment {}",
333 resolver_fragment_id,
334 relation.downstream_fragment_id,
335 writer_fragment_id,
336 )
337 .into());
338 }
339 relations.remove(0)
340 };
341 downstreams.remove(&resolver_fragment_id);
342 if downstreams
343 .values()
344 .flatten()
345 .any(|relation| relation.downstream_fragment_id == resolver_fragment_id)
346 {
347 return Err(anyhow::anyhow!(
348 "resolver fragment {} must not have an upstream fragment",
349 resolver_fragment_id
350 )
351 .into());
352 }
353
354 Ok((
355 rendered,
356 RenderedResolver {
357 fragment_info: resolver_fragment,
358 stream_actors: resolver_actors,
359 downstream_relation,
360 },
361 partition,
362 ))
363}
364
365fn find_writer_node(node: &PbStreamNode) -> Option<&PbStreamNode> {
366 let mut found = None;
367 visit_stream_node_cont(node, |node| {
368 if found.is_none() && matches!(node.node_body, Some(NodeBody::IcebergWithPkIndexWriter(_)))
369 {
370 found = Some(node);
371 }
372 found.is_none()
373 });
374 found
375}
376
377fn contains_resolver(node: &PbStreamNode) -> bool {
378 let mut found = false;
379 visit_stream_node_cont(node, |node| {
380 if matches!(node.node_body, Some(NodeBody::CompactionResolver(_))) {
381 found = true;
382 }
383 !found
384 });
385 found
386}