Skip to main content

tenferro_runtime/runtime/
extension.rs

1use std::collections::{hash_map::DefaultHasher, BTreeMap};
2use std::fmt;
3use std::hash::Hasher;
4use std::sync::Arc;
5
6use super::identity::validate_identifier;
7use super::{
8    CacheOwnerId, EngineId, ExtensionEngine, ExtensionPlanningConfig, RegistrationIdentity,
9    RuntimeCacheOwner,
10};
11use super::{
12    ExecutionContextIdentity, ExtensionModuleError, IdentityError, IdentityKind, RegistrationKey,
13    RuntimeConfigError,
14};
15
16pub(super) type ExtensionFamilyId = &'static str;
17
18/// Validated extension module identifier.
19///
20/// # Examples
21///
22/// ```
23/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
24/// use tenferro_runtime::ExtensionModuleId;
25///
26/// assert_eq!(ExtensionModuleId::new("tenferro.module.test")?.as_str(), "tenferro.module.test");
27/// # Ok(())
28/// # }
29/// ```
30#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
31pub struct ExtensionModuleId(Arc<str>);
32
33impl ExtensionModuleId {
34    /// Validate a lowercase ASCII namespaced extension module identifier.
35    ///
36    /// # Errors
37    ///
38    /// Returns [`IdentityError`] when `value` does not match the runtime
39    /// identifier grammar.
40    pub fn new(value: impl Into<Arc<str>>) -> Result<Self, IdentityError> {
41        validate_identifier(value.into(), IdentityKind::ExtensionModule).map(Self)
42    }
43
44    /// Borrow the validated identifier text.
45    pub fn as_str(&self) -> &str {
46        &self.0
47    }
48}
49
50/// Transactional extension module.
51pub trait ExtensionModule: fmt::Debug + Send + Sync + 'static {
52    /// Return this module's validated ID.
53    fn module_id(&self) -> &ExtensionModuleId;
54
55    /// Register extension engines, planning configs, and cache owners.
56    ///
57    /// # Errors
58    ///
59    /// Returns [`ExtensionModuleError`] when this module's transaction is
60    /// internally inconsistent.
61    fn configure(
62        &self,
63        registrar: &mut ExtensionModuleRegistrar<'_>,
64    ) -> Result<(), ExtensionModuleError>;
65}
66
67pub(super) struct CandidateModuleRecord {
68    pub(super) module: Arc<dyn ExtensionModule>,
69    pub(super) engines: BTreeMap<(ExtensionFamilyId, EngineId), CandidateExtensionEngine>,
70    pub(super) configs: BTreeMap<EngineId, Arc<dyn ExtensionPlanningConfig>>,
71    pub(super) owners: BTreeMap<CacheOwnerId, Arc<dyn RuntimeCacheOwner>>,
72}
73
74impl fmt::Debug for CandidateModuleRecord {
75    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76        formatter
77            .debug_struct("CandidateModuleRecord")
78            .field("module_id", self.module.module_id())
79            .field("engine_count", &self.engines.len())
80            .field("config_count", &self.configs.len())
81            .field("owner_count", &self.owners.len())
82            .finish_non_exhaustive()
83    }
84}
85
86impl Clone for CandidateModuleRecord {
87    fn clone(&self) -> Self {
88        Self {
89            module: Arc::clone(&self.module),
90            engines: self.engines.clone(),
91            configs: self.configs.clone(),
92            owners: self.owners.clone(),
93        }
94    }
95}
96
97impl CandidateModuleRecord {
98    pub(super) fn module_identical(&self, module: &Arc<dyn ExtensionModule>) -> bool {
99        Arc::ptr_eq(&self.module, module)
100    }
101}
102
103pub(super) struct CandidateExtensionEngine {
104    pub(super) engine: Arc<dyn ExtensionEngine>,
105    pub(super) identity: CandidateRegistrationIdentity,
106}
107
108#[derive(Clone, Debug)]
109pub(super) enum CandidateRegistrationIdentity {
110    New,
111    Preserved(RegistrationIdentity),
112}
113
114impl Clone for CandidateExtensionEngine {
115    fn clone(&self) -> Self {
116        Self {
117            engine: Arc::clone(&self.engine),
118            identity: self.identity.clone(),
119        }
120    }
121}
122
123impl fmt::Debug for CandidateExtensionEngine {
124    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125        formatter
126            .debug_struct("CandidateExtensionEngine")
127            .field("family_id", &self.engine.family_id())
128            .field("engine_id", self.engine.engine_id())
129            .field("context_identity", &self.engine.context_identity())
130            .field("identity", &self.identity)
131            .finish_non_exhaustive()
132    }
133}
134
135pub(super) struct BoundCandidateExtensionEngine {
136    pub(super) engine: Arc<dyn ExtensionEngine>,
137    pub(super) identity: RegistrationIdentity,
138}
139
140pub(super) struct BoundCandidateModuleRecord {
141    pub(super) module: Arc<dyn ExtensionModule>,
142    pub(super) engines: BTreeMap<(ExtensionFamilyId, EngineId), BoundCandidateExtensionEngine>,
143    pub(super) configs: BTreeMap<EngineId, Arc<dyn ExtensionPlanningConfig>>,
144    pub(super) owners: BTreeMap<CacheOwnerId, Arc<dyn RuntimeCacheOwner>>,
145}
146
147#[derive(Clone)]
148pub(super) struct FrozenExtensionEngineSlot {
149    module_id: ExtensionModuleId,
150    family_id: ExtensionFamilyId,
151    engine_id: EngineId,
152    context_identity: ExecutionContextIdentity,
153    registration_identity: RegistrationIdentity,
154    engine: Arc<dyn ExtensionEngine>,
155    config: Arc<dyn ExtensionPlanningConfig>,
156}
157
158impl fmt::Debug for FrozenExtensionEngineSlot {
159    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
160        formatter
161            .debug_struct("FrozenExtensionEngineSlot")
162            .field("module_id", &self.module_id)
163            .field("family_id", &self.family_id)
164            .field("engine_id", &self.engine_id)
165            .field("context_identity", &self.context_identity)
166            .field("registration_identity", &self.registration_identity)
167            .field("config_retained_bytes", &self.config.retained_bytes())
168            .finish_non_exhaustive()
169    }
170}
171
172#[derive(Clone)]
173pub(super) struct FrozenExtensionSlots {
174    modules: BTreeMap<ExtensionModuleId, Arc<dyn ExtensionModule>>,
175    engines: Arc<[FrozenExtensionEngineSlot]>,
176    by_family_engine: BTreeMap<(ExtensionFamilyId, EngineId), usize>,
177    owners: BTreeMap<(ExtensionModuleId, CacheOwnerId), Arc<dyn RuntimeCacheOwner>>,
178}
179
180pub(super) struct ExtensionEngineSnapshotView<'a> {
181    slot: &'a FrozenExtensionEngineSlot,
182}
183
184impl<'a> ExtensionEngineSnapshotView<'a> {
185    pub(super) fn module_id(&self) -> &'a ExtensionModuleId {
186        &self.slot.module_id
187    }
188
189    pub(super) fn family_id(&self) -> ExtensionFamilyId {
190        self.slot.family_id
191    }
192
193    pub(super) fn engine_id(&self) -> &'a EngineId {
194        &self.slot.engine_id
195    }
196
197    pub(super) fn context_identity(&self) -> ExecutionContextIdentity {
198        self.slot.context_identity
199    }
200
201    pub(super) fn registration_identity(&self) -> RegistrationIdentity {
202        self.slot.registration_identity
203    }
204
205    pub(super) fn engine(&self) -> &'a Arc<dyn ExtensionEngine> {
206        &self.slot.engine
207    }
208
209    pub(super) fn config(&self) -> &'a Arc<dyn ExtensionPlanningConfig> {
210        &self.slot.config
211    }
212}
213
214#[cfg(test)]
215pub(super) type ExtensionSlotFullForTest<'a> = (
216    &'a ExtensionModuleId,
217    ExtensionFamilyId,
218    &'a EngineId,
219    RegistrationIdentity,
220    &'a Arc<dyn ExtensionEngine>,
221    &'a Arc<dyn ExtensionPlanningConfig>,
222);
223
224impl FrozenExtensionSlots {
225    pub(super) fn module_count(&self) -> usize {
226        self.modules.len()
227    }
228
229    pub(super) fn engine_count(&self) -> usize {
230        self.engines.len()
231    }
232
233    pub(super) fn has_family(&self, family_id: ExtensionFamilyId) -> bool {
234        self.engines.iter().any(|slot| slot.family_id == family_id)
235    }
236
237    pub(super) fn to_candidate_modules(
238        &self,
239    ) -> BTreeMap<ExtensionModuleId, CandidateModuleRecord> {
240        let mut modules = BTreeMap::new();
241        for (module_id, module) in &self.modules {
242            modules.insert(
243                module_id.clone(),
244                CandidateModuleRecord {
245                    module: Arc::clone(module),
246                    engines: BTreeMap::new(),
247                    configs: BTreeMap::new(),
248                    owners: BTreeMap::new(),
249                },
250            );
251        }
252        for slot in self.engines.iter() {
253            if let Some(record) = modules.get_mut(&slot.module_id) {
254                record.engines.insert(
255                    (slot.family_id, slot.engine_id.clone()),
256                    CandidateExtensionEngine {
257                        engine: Arc::clone(&slot.engine),
258                        identity: CandidateRegistrationIdentity::Preserved(
259                            slot.registration_identity,
260                        ),
261                    },
262                );
263                record
264                    .configs
265                    .insert(slot.engine_id.clone(), Arc::clone(&slot.config));
266            }
267        }
268        for ((module_id, owner_id), owner) in &self.owners {
269            if let Some(record) = modules.get_mut(module_id) {
270                record.owners.insert(owner_id.clone(), Arc::clone(owner));
271            }
272        }
273        modules
274    }
275
276    pub(super) fn cache_owner_records(
277        &self,
278    ) -> impl Iterator<Item = (CacheOwnerId, Arc<dyn RuntimeCacheOwner>)> + '_ {
279        self.owners.iter().map(|((module_id, owner_id), owner)| {
280            (
281                extension_cache_owner_id(module_id, owner_id),
282                Arc::clone(owner),
283            )
284        })
285    }
286
287    #[cfg(test)]
288    pub(super) fn slots_for_test(
289        &self,
290    ) -> impl Iterator<
291        Item = (
292            &ExtensionModuleId,
293            ExtensionFamilyId,
294            &EngineId,
295            RegistrationIdentity,
296        ),
297    > {
298        self.engines.iter().map(|slot| {
299            (
300                &slot.module_id,
301                slot.family_id,
302                &slot.engine_id,
303                slot.registration_identity,
304            )
305        })
306    }
307
308    #[cfg(test)]
309    pub(super) fn slot_identity_for_test(
310        &self,
311        family_id: ExtensionFamilyId,
312        engine_id: &EngineId,
313    ) -> Option<RegistrationIdentity> {
314        self.extension_engine_slot(family_id, engine_id)
315            .map(|slot| slot.registration_identity)
316    }
317
318    #[cfg(test)]
319    pub(super) fn slot_full_for_test(
320        &self,
321        family_id: ExtensionFamilyId,
322        engine_id: &EngineId,
323    ) -> Option<ExtensionSlotFullForTest<'_>> {
324        self.extension_engine_slot(family_id, engine_id)
325            .map(|slot| {
326                (
327                    &slot.module_id,
328                    slot.family_id,
329                    &slot.engine_id,
330                    slot.registration_identity,
331                    &slot.engine,
332                    &slot.config,
333                )
334            })
335    }
336
337    pub(super) fn slot_for_preparation(
338        &self,
339        family_id: ExtensionFamilyId,
340        engine_id: &EngineId,
341    ) -> Option<ExtensionEngineSnapshotView<'_>> {
342        self.extension_engine_slot(family_id, engine_id)
343            .map(|slot| ExtensionEngineSnapshotView { slot })
344    }
345
346    pub(super) fn has_engine(&self, family_id: ExtensionFamilyId, engine_id: &EngineId) -> bool {
347        self.by_family_engine
348            .contains_key(&(family_id, engine_id.clone()))
349    }
350
351    /// Return whether the exact validated module instance is installed.
352    ///
353    /// This mirrors the `module_identical` no-op predicate of
354    /// `replace_extension_module_candidate`: the same module ID and the same
355    /// allocation, which is the eager direct-bridge steady-state no-op.
356    pub(super) fn has_module_identical(&self, module: &Arc<dyn ExtensionModule>) -> bool {
357        self.modules
358            .get(module.module_id())
359            .is_some_and(|installed| Arc::ptr_eq(installed, module))
360    }
361
362    /// Return whether the installed module with `module_id` owns the exact
363    /// `(family_id, engine_id)` registration.
364    ///
365    /// This mirrors the no-op predicate of the owner-scoped
366    /// `ensure_extension_module_for_engine` edit without building a candidate.
367    pub(super) fn has_module_engine(
368        &self,
369        module_id: &ExtensionModuleId,
370        family_id: ExtensionFamilyId,
371        engine_id: &EngineId,
372    ) -> bool {
373        self.extension_engine_slot(family_id, engine_id)
374            .is_some_and(|slot| &slot.module_id == module_id)
375    }
376
377    fn extension_engine_slot(
378        &self,
379        family_id: ExtensionFamilyId,
380        engine_id: &EngineId,
381    ) -> Option<&FrozenExtensionEngineSlot> {
382        self.by_family_engine
383            .get(&(family_id, engine_id.clone()))
384            .map(|&index| &self.engines[index])
385    }
386}
387
388impl fmt::Debug for FrozenExtensionSlots {
389    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
390        formatter
391            .debug_struct("FrozenExtensionSlots")
392            .field("module_count", &self.modules.len())
393            .field("engine_count", &self.engines.len())
394            .field("index_count", &self.by_family_engine.len())
395            .field("owner_count", &self.owners.len())
396            .finish_non_exhaustive()
397    }
398}
399
400pub(super) fn freeze_extension_slots(
401    modules: BTreeMap<ExtensionModuleId, BoundCandidateModuleRecord>,
402) -> Result<FrozenExtensionSlots, RuntimeConfigError> {
403    let mut frozen_modules = BTreeMap::new();
404    let mut engines = Vec::new();
405    let mut by_family_engine = BTreeMap::new();
406    let mut owners = BTreeMap::new();
407
408    for (module_id, module) in modules {
409        frozen_modules.insert(module_id.clone(), Arc::clone(&module.module));
410        for (owner_id, owner) in module.owners {
411            owners.insert((module_id.clone(), owner_id), owner);
412        }
413        for ((family_id, engine_id), record) in module.engines {
414            if by_family_engine
415                .insert((family_id, engine_id.clone()), engines.len())
416                .is_some()
417            {
418                return Err(RuntimeConfigError::ConflictingRegistration {
419                    key: RegistrationKey::ExtensionEngine {
420                        family: family_id,
421                        engine: engine_id,
422                    },
423                });
424            }
425            let config = module.configs.get(&engine_id).cloned().ok_or_else(|| {
426                RuntimeConfigError::ExtensionModule {
427                    source: ExtensionModuleError::MissingPlanningConfig {
428                        module_id: module_id.clone(),
429                        engine_id: engine_id.clone(),
430                    },
431                }
432            })?;
433            engines.push(FrozenExtensionEngineSlot {
434                module_id: module_id.clone(),
435                family_id,
436                engine_id: engine_id.clone(),
437                context_identity: record.engine.context_identity(),
438                registration_identity: record.identity,
439                engine: record.engine,
440                config,
441            });
442        }
443    }
444
445    Ok(FrozenExtensionSlots {
446        modules: frozen_modules,
447        engines: engines.into(),
448        by_family_engine,
449        owners,
450    })
451}
452
453struct ExtensionRegistrationTransaction {
454    module_id: ExtensionModuleId,
455    engines: BTreeMap<(ExtensionFamilyId, EngineId), Arc<dyn ExtensionEngine>>,
456    configs: BTreeMap<EngineId, Arc<dyn ExtensionPlanningConfig>>,
457    owners: BTreeMap<CacheOwnerId, Arc<dyn RuntimeCacheOwner>>,
458}
459
460impl ExtensionRegistrationTransaction {
461    fn new(module_id: ExtensionModuleId) -> Self {
462        Self {
463            module_id,
464            engines: BTreeMap::new(),
465            configs: BTreeMap::new(),
466            owners: BTreeMap::new(),
467        }
468    }
469
470    fn into_candidate(
471        self,
472        module: Arc<dyn ExtensionModule>,
473    ) -> Result<CandidateModuleRecord, ExtensionModuleError> {
474        self.validate()?;
475        let engines = self
476            .engines
477            .into_iter()
478            .map(|(key, engine)| {
479                (
480                    key,
481                    CandidateExtensionEngine {
482                        engine,
483                        identity: CandidateRegistrationIdentity::New,
484                    },
485                )
486            })
487            .collect();
488        Ok(CandidateModuleRecord {
489            module,
490            engines,
491            configs: self.configs,
492            owners: self.owners,
493        })
494    }
495
496    fn validate(&self) -> Result<(), ExtensionModuleError> {
497        for &(family_id, ref engine_id) in self.engines.keys() {
498            match self.configs.get(engine_id) {
499                Some(config) if config.family_id() == family_id => {}
500                Some(config) => {
501                    return Err(ExtensionModuleError::PlanningConfigFamilyMismatch {
502                        module_id: self.module_id.clone(),
503                        engine_id: engine_id.clone(),
504                        expected: family_id,
505                        actual: config.family_id(),
506                    });
507                }
508                None => {
509                    return Err(ExtensionModuleError::MissingPlanningConfig {
510                        module_id: self.module_id.clone(),
511                        engine_id: engine_id.clone(),
512                    });
513                }
514            }
515        }
516        for engine_id in self.configs.keys() {
517            if engine_match_count(&self.engines, engine_id) != 1 {
518                return Err(ExtensionModuleError::PlanningConfigWithoutEngine {
519                    module_id: self.module_id.clone(),
520                    engine_id: engine_id.clone(),
521                });
522            }
523        }
524        Ok(())
525    }
526}
527
528pub(super) fn bind_candidate_module(
529    module: CandidateModuleRecord,
530    allocate_identity: &mut impl FnMut() -> Result<RegistrationIdentity, RuntimeConfigError>,
531) -> Result<BoundCandidateModuleRecord, RuntimeConfigError> {
532    let engines = module
533        .engines
534        .into_iter()
535        .map(|(key, record)| {
536            let identity = match record.identity {
537                CandidateRegistrationIdentity::New => allocate_identity()?,
538                CandidateRegistrationIdentity::Preserved(identity) => identity,
539            };
540            Ok((
541                key,
542                BoundCandidateExtensionEngine {
543                    engine: record.engine,
544                    identity,
545                },
546            ))
547        })
548        .collect::<Result<BTreeMap<_, _>, RuntimeConfigError>>()?;
549    Ok(BoundCandidateModuleRecord {
550        module: module.module,
551        engines,
552        configs: module.configs,
553        owners: module.owners,
554    })
555}
556
557/// Borrowed registrar for one extension module transaction.
558pub struct ExtensionModuleRegistrar<'a> {
559    transaction: &'a mut ExtensionRegistrationTransaction,
560}
561
562impl ExtensionModuleRegistrar<'_> {
563    /// Register one extension preparation engine.
564    ///
565    /// # Errors
566    ///
567    /// Returns [`ExtensionModuleError::ConflictingEngine`] when a distinct
568    /// engine already occupies the same `(family, engine)` transaction key.
569    pub fn register_engine(
570        &mut self,
571        engine: Arc<dyn ExtensionEngine>,
572    ) -> Result<(), ExtensionModuleError> {
573        let key = (engine.family_id(), engine.engine_id().clone());
574        match self.transaction.engines.get(&key) {
575            Some(existing) if Arc::ptr_eq(existing, &engine) => Ok(()),
576            Some(_) => Err(ExtensionModuleError::ConflictingEngine {
577                module_id: self.transaction.module_id.clone(),
578                family_id: key.0,
579                engine_id: key.1,
580            }),
581            None => {
582                self.transaction.engines.insert(key, engine);
583                Ok(())
584            }
585        }
586    }
587
588    /// Register the planning config for one extension engine.
589    ///
590    /// # Errors
591    ///
592    /// Returns a typed [`ExtensionModuleError`] when the target engine is absent,
593    /// the config family is mismatched, or an unequal config is already present.
594    pub fn register_planning_config(
595        &mut self,
596        engine_id: EngineId,
597        config: Arc<dyn ExtensionPlanningConfig>,
598    ) -> Result<(), ExtensionModuleError> {
599        let Some((family_id, _)) = unique_engine_for_config(&self.transaction.engines, &engine_id)
600        else {
601            return Err(ExtensionModuleError::PlanningConfigWithoutEngine {
602                module_id: self.transaction.module_id.clone(),
603                engine_id,
604            });
605        };
606        if config.family_id() != family_id {
607            return Err(ExtensionModuleError::PlanningConfigFamilyMismatch {
608                module_id: self.transaction.module_id.clone(),
609                engine_id,
610                expected: family_id,
611                actual: config.family_id(),
612            });
613        }
614
615        match self.transaction.configs.get(&engine_id) {
616            Some(existing) if config_payloads_equal(existing.as_ref(), config.as_ref()) => Ok(()),
617            Some(_) => Err(ExtensionModuleError::ConflictingPlanningConfig {
618                module_id: self.transaction.module_id.clone(),
619                engine_id,
620            }),
621            None => {
622                self.transaction.configs.insert(engine_id, config);
623                Ok(())
624            }
625        }
626    }
627
628    /// Register a cache owner owned by this extension module.
629    ///
630    /// # Errors
631    ///
632    /// Returns [`ExtensionModuleError::ConflictingCacheOwner`] when a distinct
633    /// owner already occupies the same local owner ID.
634    pub fn register_cache_owner(
635        &mut self,
636        id: CacheOwnerId,
637        owner: Arc<dyn RuntimeCacheOwner>,
638    ) -> Result<(), ExtensionModuleError> {
639        match self.transaction.owners.get(&id) {
640            Some(existing) if Arc::ptr_eq(existing, &owner) => Ok(()),
641            Some(_) => Err(ExtensionModuleError::ConflictingCacheOwner {
642                module_id: self.transaction.module_id.clone(),
643                owner: id,
644            }),
645            None => {
646                self.transaction.owners.insert(id, owner);
647                Ok(())
648            }
649        }
650    }
651}
652
653impl fmt::Debug for ExtensionModuleRegistrar<'_> {
654    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
655        formatter
656            .debug_struct("ExtensionModuleRegistrar")
657            .field("module_id", &self.transaction.module_id)
658            .field("engine_count", &self.transaction.engines.len())
659            .field("config_count", &self.transaction.configs.len())
660            .field("owner_count", &self.transaction.owners.len())
661            .finish_non_exhaustive()
662    }
663}
664
665pub(super) fn configure_module(
666    module: Arc<dyn ExtensionModule>,
667) -> Result<CandidateModuleRecord, ExtensionModuleError> {
668    let module_id = module.module_id().clone();
669    let mut transaction = ExtensionRegistrationTransaction::new(module_id);
670    {
671        let mut registrar = ExtensionModuleRegistrar {
672            transaction: &mut transaction,
673        };
674        module.configure(&mut registrar)?;
675    }
676    transaction.into_candidate(module)
677}
678
679pub(super) fn extension_cache_owner_id(
680    module_id: &ExtensionModuleId,
681    local: &CacheOwnerId,
682) -> CacheOwnerId {
683    let module = module_id.as_str();
684    let local = local.as_str();
685    CacheOwnerId::from_canonical_owner_id(Arc::<str>::from(format!(
686        "extension[{}]:{module}[{}]:{local}",
687        module.len(),
688        local.len(),
689    )))
690}
691
692fn unique_engine_for_config(
693    engines: &BTreeMap<(ExtensionFamilyId, EngineId), Arc<dyn ExtensionEngine>>,
694    engine_id: &EngineId,
695) -> Option<(ExtensionFamilyId, EngineId)> {
696    let mut matches = engines
697        .keys()
698        .filter(|(_, candidate_engine)| candidate_engine == engine_id);
699    let first = matches.next()?;
700    matches.next().is_none().then(|| (first.0, first.1.clone()))
701}
702
703fn engine_match_count(
704    engines: &BTreeMap<(ExtensionFamilyId, EngineId), Arc<dyn ExtensionEngine>>,
705    engine_id: &EngineId,
706) -> usize {
707    engines
708        .keys()
709        .filter(|(_, candidate_engine)| candidate_engine == engine_id)
710        .count()
711}
712
713fn config_payloads_equal(
714    left: &dyn ExtensionPlanningConfig,
715    right: &dyn ExtensionPlanningConfig,
716) -> bool {
717    payload_hash(left) == payload_hash(right) && left.payload_eq(right)
718}
719
720fn payload_hash(config: &dyn ExtensionPlanningConfig) -> u64 {
721    let mut hasher = DefaultHasher::new();
722    config.payload_hash(&mut hasher);
723    hasher.finish()
724}