adk_graph/deferred.rs
1//! Deferred node (fan-in barrier) support for graph workflows.
2//!
3//! Provides fan-in barrier semantics for nodes that wait on multiple upstream
4//! parallel paths before executing. This enables scatter-gather patterns where
5//! work is distributed across parallel branches and then collected at a single
6//! synchronization point.
7//!
8//! # Overview
9//!
10//! A deferred node is declared with a [`DeferredNodeConfig`] that specifies:
11//! - [`MergeStrategy`]: How upstream outputs are combined (collect, merge maps, first, or custom).
12//! - `fan_in_timeout`: Optional maximum wait duration for all upstream paths.
13//!
14//! The [`FanInTracker`] tracks which upstream paths have completed and merges
15//! their outputs according to the configured strategy.
16//!
17//! # Example
18//!
19//! ```rust
20//! use std::time::Duration;
21//! use adk_graph::deferred::{DeferredNodeConfig, FanInTracker, MergeStrategy};
22//! use serde_json::json;
23//!
24//! // Configure a deferred node that collects all upstream outputs
25//! let config = DeferredNodeConfig {
26//! merge_strategy: MergeStrategy::Collect,
27//! fan_in_timeout: Some(Duration::from_secs(30)),
28//! ..Default::default()
29//! };
30//!
31//! // Track upstream completions
32//! let mut tracker = FanInTracker::new(vec!["branch_a", "branch_b", "branch_c"]);
33//!
34//! tracker.record("branch_a", json!({"result": 1}));
35//! tracker.record("branch_b", json!({"result": 2}));
36//! assert!(!tracker.is_ready());
37//!
38//! tracker.record("branch_c", json!({"result": 3}));
39//! assert!(tracker.is_ready());
40//!
41//! // Merge outputs using the configured strategy
42//! let merged = tracker.merge(&config.merge_strategy);
43//! assert_eq!(merged, json!([{"result": 1}, {"result": 2}, {"result": 3}]));
44//! ```
45
46use std::collections::{BTreeMap, HashMap, HashSet};
47use std::fmt;
48use std::sync::Arc;
49use std::time::{Duration, Instant};
50
51use serde::{Deserialize, Serialize};
52use serde_json::Value;
53
54use crate::error::{GraphError, Result};
55
56/// How to combine outputs from multiple upstream parallel paths.
57///
58/// The merge strategy determines how the collected outputs from all upstream
59/// branches are combined into a single value for the deferred node's input.
60///
61/// # Example
62///
63/// ```rust
64/// use adk_graph::deferred::MergeStrategy;
65///
66/// // Default strategy collects all outputs into a Vec
67/// let strategy = MergeStrategy::default();
68/// assert!(matches!(strategy, MergeStrategy::Collect));
69///
70/// // MergeMap combines all output maps with last-write-wins
71/// let strategy = MergeStrategy::MergeMap;
72/// ```
73#[derive(Clone, Default)]
74pub enum MergeStrategy {
75 /// Collect all outputs into a `Vec<Value>`.
76 ///
77 /// Outputs are ordered by the insertion order of source nodes
78 /// (the order in which they were recorded).
79 #[default]
80 Collect,
81
82 /// Merge all output maps into a single map (last-write-wins on key conflict).
83 ///
84 /// Each upstream output is expected to be a JSON object. Non-object outputs
85 /// are skipped. When multiple outputs contain the same key, the value from
86 /// the later-recorded source wins.
87 MergeMap,
88
89 /// Use only the first completed output.
90 ///
91 /// Returns the output from whichever upstream path completed first
92 /// (i.e., was recorded first).
93 First,
94
95 /// Custom merge function.
96 ///
97 /// Accepts a closure that takes all collected outputs and produces a
98 /// single merged value.
99 ///
100 /// # Example
101 ///
102 /// ```rust
103 /// use std::sync::Arc;
104 /// use adk_graph::deferred::MergeStrategy;
105 /// use serde_json::{json, Value};
106 ///
107 /// let strategy = MergeStrategy::Custom(Arc::new(|outputs: Vec<Value>| {
108 /// json!({ "count": outputs.len() })
109 /// }));
110 /// ```
111 Custom(Arc<dyn Fn(Vec<Value>) -> Value + Send + Sync>),
112}
113
114impl fmt::Debug for MergeStrategy {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 match self {
117 Self::Collect => write!(f, "Collect"),
118 Self::MergeMap => write!(f, "MergeMap"),
119 Self::First => write!(f, "First"),
120 Self::Custom(_) => write!(f, "Custom(<fn>)"),
121 }
122 }
123}
124
125/// Configuration for a deferred (fan-in) node.
126///
127/// A deferred node waits for all upstream parallel paths to complete before
128/// executing. The configuration controls how outputs are merged and how long
129/// the node waits.
130///
131/// # Example
132///
133/// ```rust
134/// use std::time::Duration;
135/// use adk_graph::deferred::{DeferredNodeConfig, MergeStrategy};
136///
137/// let config = DeferredNodeConfig {
138/// merge_strategy: MergeStrategy::MergeMap,
139/// fan_in_timeout: Some(Duration::from_secs(60)),
140/// ..Default::default()
141/// };
142/// ```
143#[derive(Debug, Clone, Default)]
144pub struct DeferredNodeConfig {
145 /// Strategy for combining upstream outputs.
146 pub merge_strategy: MergeStrategy,
147
148 /// Maximum time to wait for all upstream paths to complete.
149 ///
150 /// - `None`: Wait indefinitely for all upstream paths.
151 /// - `Some(duration)`: If the timeout expires and some paths have completed,
152 /// proceed with partial results. If zero paths have completed, return
153 /// `GraphError::FanInTimedOut`.
154 pub fan_in_timeout: Option<Duration>,
155 /// How many predecessors must have arrived for `fan_in_timeout` to release
156 /// the node with partial results.
157 ///
158 /// `None` means one, which is the behaviour this field replaced. Set it to
159 /// the full predecessor count to require all of them even after a timeout, or
160 /// to any value between for an n-of-m join: release once that many branches
161 /// have answered and abandon the rest.
162 pub min_predecessors: Option<usize>,
163}
164
165/// Tracks which upstream paths have completed for a deferred node.
166///
167/// The tracker maintains a set of expected source nodes and records their
168/// outputs as they arrive. Once all expected sources have reported, the
169/// tracker is ready and outputs can be merged.
170///
171/// # Example
172///
173/// ```rust
174/// use adk_graph::deferred::{FanInTracker, MergeStrategy};
175/// use serde_json::json;
176///
177/// let mut tracker = FanInTracker::new(vec!["node_a", "node_b"]);
178///
179/// assert!(!tracker.is_ready());
180/// assert_eq!(tracker.received_count(), 0);
181/// assert_eq!(tracker.expected_count(), 2);
182///
183/// tracker.record("node_a", json!("output_a"));
184/// assert!(!tracker.is_ready());
185///
186/// tracker.record("node_b", json!("output_b"));
187/// assert!(tracker.is_ready());
188///
189/// let merged = tracker.merge(&MergeStrategy::Collect);
190/// assert_eq!(merged, json!(["output_a", "output_b"]));
191/// ```
192pub struct FanInTracker {
193 /// The set of source node names we expect to receive output from.
194 expected: HashSet<String>,
195 /// Outputs received so far, keyed by source node name.
196 received: HashMap<String, Value>,
197 /// Insertion order of received outputs (for deterministic merge ordering).
198 insertion_order: Vec<String>,
199}
200
201impl FanInTracker {
202 /// Create a new tracker expecting outputs from the given source nodes.
203 ///
204 /// # Arguments
205 ///
206 /// * `expected_sources` - Names of upstream nodes that must complete
207 /// before this deferred node can execute.
208 ///
209 /// # Example
210 ///
211 /// ```rust
212 /// use adk_graph::deferred::FanInTracker;
213 ///
214 /// let tracker = FanInTracker::new(vec!["branch_1", "branch_2", "branch_3"]);
215 /// assert_eq!(tracker.expected_count(), 3);
216 /// assert!(!tracker.is_ready());
217 /// ```
218 pub fn new(expected_sources: Vec<&str>) -> Self {
219 Self {
220 expected: expected_sources.iter().map(|s| (*s).to_string()).collect(),
221 received: HashMap::new(),
222 insertion_order: Vec::new(),
223 }
224 }
225
226 /// Returns `true` when all expected sources have reported their output.
227 ///
228 /// # Example
229 ///
230 /// ```rust
231 /// use adk_graph::deferred::FanInTracker;
232 /// use serde_json::json;
233 ///
234 /// let mut tracker = FanInTracker::new(vec!["a"]);
235 /// assert!(!tracker.is_ready());
236 ///
237 /// tracker.record("a", json!(42));
238 /// assert!(tracker.is_ready());
239 /// ```
240 pub fn is_ready(&self) -> bool {
241 self.expected.iter().all(|s| self.received.contains_key(s))
242 }
243
244 /// Record the output from a source node.
245 ///
246 /// If the source has already been recorded, the previous value is
247 /// overwritten (last-write-wins). Recording a source that is not in
248 /// the expected set is a no-op for readiness but the value is still stored.
249 ///
250 /// # Arguments
251 ///
252 /// * `source_node` - The name of the upstream node that produced the output.
253 /// * `output` - The output value from the source node.
254 ///
255 /// # Example
256 ///
257 /// ```rust
258 /// use adk_graph::deferred::FanInTracker;
259 /// use serde_json::json;
260 ///
261 /// let mut tracker = FanInTracker::new(vec!["worker_1", "worker_2"]);
262 /// tracker.record("worker_1", json!({"status": "done"}));
263 /// assert_eq!(tracker.received_count(), 1);
264 /// ```
265 pub fn record(&mut self, source_node: &str, output: Value) {
266 let key = source_node.to_string();
267 if !self.received.contains_key(&key) {
268 self.insertion_order.push(key.clone());
269 }
270 self.received.insert(key, output);
271 }
272
273 /// Merge all received outputs according to the given strategy.
274 ///
275 /// The merge operation combines all recorded outputs into a single
276 /// [`Value`] based on the [`MergeStrategy`]:
277 ///
278 /// - [`MergeStrategy::Collect`]: Returns a JSON array of all outputs in
279 /// insertion order.
280 /// - [`MergeStrategy::MergeMap`]: Merges all JSON object outputs into a
281 /// single object (last-write-wins). Non-object outputs are skipped.
282 /// - [`MergeStrategy::First`]: Returns the first recorded output.
283 /// - [`MergeStrategy::Custom`]: Invokes the custom function with all outputs.
284 ///
285 /// # Arguments
286 ///
287 /// * `strategy` - The merge strategy to apply.
288 ///
289 /// # Example
290 ///
291 /// ```rust
292 /// use adk_graph::deferred::{FanInTracker, MergeStrategy};
293 /// use serde_json::json;
294 ///
295 /// let mut tracker = FanInTracker::new(vec!["a", "b"]);
296 /// tracker.record("a", json!({"x": 1}));
297 /// tracker.record("b", json!({"y": 2}));
298 ///
299 /// // Collect strategy
300 /// let result = tracker.merge(&MergeStrategy::Collect);
301 /// assert_eq!(result, json!([{"x": 1}, {"y": 2}]));
302 ///
303 /// // MergeMap strategy
304 /// let result = tracker.merge(&MergeStrategy::MergeMap);
305 /// assert_eq!(result, json!({"x": 1, "y": 2}));
306 /// ```
307 pub fn merge(&self, strategy: &MergeStrategy) -> Value {
308 match strategy {
309 MergeStrategy::Collect => {
310 let outputs: Vec<Value> = self
311 .insertion_order
312 .iter()
313 .filter_map(|key| self.received.get(key).cloned())
314 .collect();
315 Value::Array(outputs)
316 }
317 MergeStrategy::MergeMap => {
318 let mut merged = serde_json::Map::new();
319 for key in &self.insertion_order {
320 if let Some(Value::Object(map)) = self.received.get(key) {
321 for (k, v) in map {
322 merged.insert(k.clone(), v.clone());
323 }
324 }
325 }
326 Value::Object(merged)
327 }
328 MergeStrategy::First => self
329 .insertion_order
330 .first()
331 .and_then(|key| self.received.get(key).cloned())
332 .unwrap_or(Value::Null),
333 MergeStrategy::Custom(f) => {
334 let outputs: Vec<Value> = self
335 .insertion_order
336 .iter()
337 .filter_map(|key| self.received.get(key).cloned())
338 .collect();
339 f(outputs)
340 }
341 }
342 }
343
344 /// Returns the number of outputs received so far.
345 pub fn received_count(&self) -> usize {
346 self.received.len()
347 }
348
349 /// Returns the number of expected source nodes.
350 pub fn expected_count(&self) -> usize {
351 self.expected.len()
352 }
353
354 /// Returns the names of sources that have not yet reported.
355 pub fn pending_sources(&self) -> Vec<&str> {
356 self.expected
357 .iter()
358 .filter(|s| !self.received.contains_key(*s))
359 .map(|s| s.as_str())
360 .collect()
361 }
362
363 /// Returns the names of sources that have reported.
364 pub fn completed_sources(&self) -> Vec<&str> {
365 self.insertion_order.iter().map(|s| s.as_str()).collect()
366 }
367}
368
369/// Checkpoint metadata key holding the arrivals of fan-in nodes still waiting.
370pub(crate) const FAN_IN_METADATA_KEY: &str = "adk.graph.fanIn";
371
372/// Layout version of [`FAN_IN_METADATA_KEY`].
373///
374/// A checkpoint without the key predates it and restores with no arrivals.
375const FAN_IN_SNAPSHOT_VERSION: u32 = 1;
376
377/// The fan-in arrivals a checkpoint carries, so a resumed run still releases a
378/// join whose earlier predecessors finished before the pause or crash.
379#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
380#[serde(rename_all = "camelCase")]
381pub(crate) struct FanInSnapshot {
382 version: u32,
383 nodes: BTreeMap<String, PendingFanIn>,
384}
385
386/// One deferred node's recorded arrivals.
387#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
388#[serde(rename_all = "camelCase")]
389struct PendingFanIn {
390 /// Arrivals in recording order, which is the merge order.
391 arrivals: Vec<FanInArrival>,
392 /// When the node started waiting, so `fan_in_timeout` spans the pause.
393 waiting_since: chrono::DateTime<chrono::Utc>,
394}
395
396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
397#[serde(rename_all = "camelCase")]
398struct FanInArrival {
399 source: String,
400 output: Value,
401}
402
403impl FanInSnapshot {
404 /// Captures every waiting tracker, or `None` when no join is waiting.
405 pub(crate) fn capture(
406 trackers: &HashMap<String, FanInTracker>,
407 started: &HashMap<String, Instant>,
408 ) -> Option<Self> {
409 if trackers.is_empty() {
410 return None;
411 }
412 let now = chrono::Utc::now();
413 let nodes = trackers
414 .iter()
415 .map(|(node, tracker)| {
416 let waited = started
417 .get(node)
418 .and_then(|start| chrono::Duration::from_std(start.elapsed()).ok())
419 .unwrap_or_default();
420 let arrivals = tracker
421 .insertion_order
422 .iter()
423 .filter_map(|source| {
424 tracker.received.get(source).map(|output| FanInArrival {
425 source: source.clone(),
426 output: output.clone(),
427 })
428 })
429 .collect();
430 (node.clone(), PendingFanIn { arrivals, waiting_since: now - waited })
431 })
432 .collect();
433 Some(Self { version: FAN_IN_SNAPSHOT_VERSION, nodes })
434 }
435
436 /// Reads the snapshot from checkpoint metadata; `None` when the key is absent.
437 ///
438 /// # Errors
439 ///
440 /// Returns [`GraphError::CheckpointError`] when the value is malformed or was
441 /// written in a newer layout than this release reads.
442 pub(crate) fn from_metadata(metadata: &HashMap<String, Value>) -> Result<Option<Self>> {
443 let Some(value) = metadata.get(FAN_IN_METADATA_KEY) else { return Ok(None) };
444 let snapshot: Self = serde_json::from_value(value.clone()).map_err(|e| {
445 GraphError::CheckpointError(format!(
446 "checkpoint metadata '{FAN_IN_METADATA_KEY}' is malformed: {e}"
447 ))
448 })?;
449 if snapshot.version != FAN_IN_SNAPSHOT_VERSION {
450 return Err(GraphError::CheckpointError(format!(
451 "checkpoint metadata '{FAN_IN_METADATA_KEY}' has layout version {}, but this \
452 adk-graph release reads version {FAN_IN_SNAPSHOT_VERSION}; resume the thread \
453 with the release that wrote it",
454 snapshot.version
455 )));
456 }
457 Ok(Some(snapshot))
458 }
459
460 /// Rebuilds the trackers and their wait start times.
461 ///
462 /// `upstream` names each node's predecessors from the graph being resumed, so
463 /// the expected set is never read from the checkpoint.
464 pub(crate) fn restore(
465 self,
466 upstream: impl Fn(&str) -> Vec<String>,
467 ) -> (HashMap<String, FanInTracker>, HashMap<String, Instant>) {
468 let now = chrono::Utc::now();
469 let mut trackers = HashMap::new();
470 let mut started = HashMap::new();
471 for (node, pending) in self.nodes {
472 let sources = upstream(&node);
473 let mut tracker = FanInTracker::new(sources.iter().map(String::as_str).collect());
474 for arrival in pending.arrivals {
475 tracker.record(&arrival.source, arrival.output);
476 }
477 let waited = (now - pending.waiting_since).to_std().unwrap_or_default();
478 started.insert(
479 node.clone(),
480 Instant::now().checked_sub(waited).unwrap_or_else(Instant::now),
481 );
482 trackers.insert(node, tracker);
483 }
484 (trackers, started)
485 }
486
487 /// Serializes the snapshot for checkpoint metadata.
488 pub(crate) fn to_value(&self) -> Value {
489 serde_json::to_value(self).unwrap_or(Value::Null)
490 }
491}
492
493impl fmt::Debug for FanInTracker {
494 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
495 f.debug_struct("FanInTracker")
496 .field("expected", &self.expected)
497 .field("received_keys", &self.insertion_order)
498 .field("is_ready", &self.is_ready())
499 .finish()
500 }
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506 use serde_json::json;
507
508 #[test]
509 fn test_tracker_new_empty_not_ready() {
510 let tracker = FanInTracker::new(vec!["a", "b", "c"]);
511 assert!(!tracker.is_ready());
512 assert_eq!(tracker.expected_count(), 3);
513 assert_eq!(tracker.received_count(), 0);
514 }
515
516 #[test]
517 fn test_tracker_ready_when_all_received() {
518 let mut tracker = FanInTracker::new(vec!["a", "b"]);
519 tracker.record("a", json!(1));
520 assert!(!tracker.is_ready());
521 tracker.record("b", json!(2));
522 assert!(tracker.is_ready());
523 }
524
525 #[test]
526 fn test_merge_collect() {
527 let mut tracker = FanInTracker::new(vec!["x", "y", "z"]);
528 tracker.record("x", json!("first"));
529 tracker.record("y", json!("second"));
530 tracker.record("z", json!("third"));
531
532 let result = tracker.merge(&MergeStrategy::Collect);
533 assert_eq!(result, json!(["first", "second", "third"]));
534 }
535
536 #[test]
537 fn test_merge_map_combines_objects() {
538 let mut tracker = FanInTracker::new(vec!["a", "b"]);
539 tracker.record("a", json!({"key1": "val1", "shared": "from_a"}));
540 tracker.record("b", json!({"key2": "val2", "shared": "from_b"}));
541
542 let result = tracker.merge(&MergeStrategy::MergeMap);
543 assert_eq!(result, json!({"key1": "val1", "key2": "val2", "shared": "from_b"}));
544 }
545
546 #[test]
547 fn test_merge_map_skips_non_objects() {
548 let mut tracker = FanInTracker::new(vec!["a", "b"]);
549 tracker.record("a", json!(42)); // Not an object, skipped
550 tracker.record("b", json!({"key": "value"}));
551
552 let result = tracker.merge(&MergeStrategy::MergeMap);
553 assert_eq!(result, json!({"key": "value"}));
554 }
555
556 #[test]
557 fn test_merge_first() {
558 let mut tracker = FanInTracker::new(vec!["a", "b", "c"]);
559 tracker.record("b", json!("first_to_arrive"));
560 tracker.record("a", json!("second_to_arrive"));
561 tracker.record("c", json!("third_to_arrive"));
562
563 let result = tracker.merge(&MergeStrategy::First);
564 assert_eq!(result, json!("first_to_arrive"));
565 }
566
567 #[test]
568 fn test_merge_first_empty() {
569 let tracker = FanInTracker::new(vec!["a"]);
570 let result = tracker.merge(&MergeStrategy::First);
571 assert_eq!(result, Value::Null);
572 }
573
574 #[test]
575 fn test_merge_custom() {
576 let mut tracker = FanInTracker::new(vec!["a", "b"]);
577 tracker.record("a", json!(10));
578 tracker.record("b", json!(20));
579
580 let strategy = MergeStrategy::Custom(Arc::new(|outputs| {
581 let sum: i64 = outputs.iter().filter_map(|v| v.as_i64()).sum();
582 json!(sum)
583 }));
584
585 let result = tracker.merge(&strategy);
586 assert_eq!(result, json!(30));
587 }
588
589 #[test]
590 fn test_record_overwrites_previous() {
591 let mut tracker = FanInTracker::new(vec!["a"]);
592 tracker.record("a", json!("first"));
593 tracker.record("a", json!("second"));
594
595 assert!(tracker.is_ready());
596 assert_eq!(tracker.received_count(), 1);
597
598 let result = tracker.merge(&MergeStrategy::First);
599 assert_eq!(result, json!("second"));
600 }
601
602 #[test]
603 fn test_pending_and_completed_sources() {
604 let mut tracker = FanInTracker::new(vec!["a", "b", "c"]);
605 tracker.record("b", json!(1));
606
607 let mut pending = tracker.pending_sources();
608 pending.sort();
609 assert_eq!(pending, vec!["a", "c"]);
610 assert_eq!(tracker.completed_sources(), vec!["b"]);
611 }
612
613 #[test]
614 fn test_default_config() {
615 let config = DeferredNodeConfig::default();
616 assert!(matches!(config.merge_strategy, MergeStrategy::Collect));
617 assert!(config.fan_in_timeout.is_none());
618 }
619
620 #[test]
621 fn fan_in_snapshot_round_trips_arrivals_in_order() {
622 let mut tracker = FanInTracker::new(vec!["a", "b", "c"]);
623 tracker.record("b", json!("from b"));
624 tracker.record("a", json!({"x": 1}));
625 let trackers = HashMap::from([("join".to_string(), tracker)]);
626 let started = HashMap::from([("join".to_string(), Instant::now())]);
627
628 let snapshot = FanInSnapshot::capture(&trackers, &started).expect("a join is waiting");
629 let metadata = HashMap::from([(FAN_IN_METADATA_KEY.to_string(), snapshot.to_value())]);
630 let restored = FanInSnapshot::from_metadata(&metadata).unwrap().expect("key present");
631 let (trackers, started) =
632 restored.restore(|_| vec!["a".to_string(), "b".to_string(), "c".to_string()]);
633
634 let tracker = &trackers["join"];
635 assert_eq!(tracker.completed_sources(), vec!["b", "a"]);
636 assert_eq!(tracker.merge(&MergeStrategy::Collect), json!(["from b", {"x": 1}]));
637 assert!(!tracker.is_ready());
638 assert!(started.contains_key("join"));
639 }
640
641 #[test]
642 fn a_checkpoint_without_fan_in_metadata_restores_nothing() {
643 assert_eq!(FanInSnapshot::from_metadata(&HashMap::new()).unwrap(), None);
644 assert_eq!(FanInSnapshot::capture(&HashMap::new(), &HashMap::new()), None);
645 }
646
647 #[test]
648 fn a_newer_fan_in_layout_is_rejected() {
649 let metadata =
650 HashMap::from([(FAN_IN_METADATA_KEY.to_string(), json!({"version": 99, "nodes": {}}))]);
651 assert!(matches!(
652 FanInSnapshot::from_metadata(&metadata),
653 Err(GraphError::CheckpointError(_))
654 ));
655 }
656
657 #[test]
658 fn test_merge_strategy_debug() {
659 assert_eq!(format!("{:?}", MergeStrategy::Collect), "Collect");
660 assert_eq!(format!("{:?}", MergeStrategy::MergeMap), "MergeMap");
661 assert_eq!(format!("{:?}", MergeStrategy::First), "First");
662 let custom = MergeStrategy::Custom(Arc::new(Value::Array));
663 assert_eq!(format!("{:?}", custom), "Custom(<fn>)");
664 }
665}