Skip to main content

laminar_connectors/lakehouse/delta/
lifecycle.rs

1//! Runtime sink lifecycle and coordinated-committer protocol implementations.
2
3use super::{
4    async_trait, debug, info, warn, Arc, ConnectorConfig, ConnectorError, ConnectorState,
5    ConnectorTaskTracker, DeliveryGuarantee, DeltaLakeSink, DeltaLakeSinkConfig, DeltaWriteMode,
6    Duration, Instant, RecordBatch, SchemaRef, SinkConnector, SinkConsistency, SinkContract,
7    SinkInputMode, SinkTopology, StorageCredentialResolver, StorageProvider, WriteResult,
8};
9#[cfg(feature = "delta-lake")]
10use super::{
11    publish_coordinated_delta_batch, retry_delta_metadata_until, UnresolvedDeltaPublication,
12    MAX_COORDINATED_COMMIT_PAYLOAD_BYTES,
13};
14
15#[async_trait]
16impl SinkConnector for DeltaLakeSink {
17    fn terminal_task_tracker(&self) -> Option<ConnectorTaskTracker> {
18        Some(self.task_tracker.clone())
19    }
20
21    fn contract(&self, config: &ConnectorConfig) -> Result<SinkContract, ConnectorError> {
22        let cfg = if config.properties().is_empty() {
23            self.config.clone()
24        } else {
25            DeltaLakeSinkConfig::from_config(config)?
26        };
27        #[cfg(feature = "delta-lake")]
28        if cfg.delivery_guarantee == DeliveryGuarantee::ExactlyOnce {
29            super::super::delta_io::validate_coordinated_storage_preflight(
30                &cfg.table_path,
31                &cfg.storage_options,
32            )?;
33        }
34        if cfg.delivery_guarantee == DeliveryGuarantee::ExactlyOnce
35            && cfg.write_mode != DeltaWriteMode::Append
36        {
37            return Err(ConnectorError::ConfigurationError(
38                "Delta exactly-once requires coordinated append mode".into(),
39            ));
40        }
41        let consistency = if cfg.delivery_guarantee == DeliveryGuarantee::ExactlyOnce
42            && cfg.write_mode == DeltaWriteMode::Append
43        {
44            // Append mode emits durable data-file descriptors for one
45            // designated catalog commit. Certification still requires an
46            // exact external namespace/cursor, enforced by runtime admission.
47            SinkConsistency::CheckpointCommittable
48        } else {
49            SinkConsistency::DurableAtLeastOnce
50        };
51        let unity_target = cfg
52            .table_path
53            .split_once("://")
54            .is_some_and(|(scheme, _)| scheme.eq_ignore_ascii_case("uc"));
55        let shared_target = StorageProvider::is_shared_uri(&cfg.table_path) || unity_target;
56        let topology = if cfg.write_mode == DeltaWriteMode::Append && shared_target {
57            SinkTopology::MultiWriter
58        } else {
59            // Node-local files plus overwrite/MERGE lack fenced distributed ownership.
60            SinkTopology::Singleton
61        };
62        let input_mode = match cfg.write_mode {
63            DeltaWriteMode::Append | DeltaWriteMode::Upsert => SinkInputMode::FullChangelog,
64            DeltaWriteMode::Overwrite => SinkInputMode::AppendOnly,
65        };
66        let contract = SinkContract::new(consistency, topology, input_mode);
67        let cluster_exact_target = StorageProvider::is_direct_s3_uri(&cfg.table_path);
68        Ok(
69            if consistency == SinkConsistency::CheckpointCommittable && cluster_exact_target {
70                contract.with_cluster_exact_delivery_certification()
71            } else {
72                contract
73            },
74        )
75    }
76
77    fn keyed_mutation_key(
78        &self,
79        config: &ConnectorConfig,
80    ) -> Result<Option<Vec<String>>, ConnectorError> {
81        let cfg = if config.properties().is_empty() {
82            self.config.clone()
83        } else {
84            DeltaLakeSinkConfig::from_config(config)?
85        };
86        // Upsert mode collapses each epoch to one terminal `_op` per merge key and MERGEs it:
87        // `U` updates or inserts, `D` deletes the matched row.
88        Ok((cfg.write_mode == DeltaWriteMode::Upsert).then_some(cfg.merge_key_columns))
89    }
90
91    async fn resolve_schema(
92        &mut self,
93        config: &ConnectorConfig,
94        input: SchemaRef,
95    ) -> Result<crate::schema::resolution::SchemaBinding, ConnectorError> {
96        #[cfg(feature = "delta-lake")]
97        {
98            super::super::schema_resolution::delta_sink(config, input).await
99        }
100        #[cfg(not(feature = "delta-lake"))]
101        {
102            let _ = (config, input);
103            Err(ConnectorError::FeatureUnsupported(
104                "Delta schema resolution requires the delta-lake feature".into(),
105            ))
106        }
107    }
108
109    async fn prepare_schema(
110        &mut self,
111        config: &ConnectorConfig,
112        binding: &mut crate::schema::resolution::SchemaBinding,
113    ) -> Result<(), ConnectorError> {
114        #[cfg(feature = "delta-lake")]
115        {
116            super::super::schema_resolution::prepare_delta(config, binding).await
117        }
118        #[cfg(not(feature = "delta-lake"))]
119        {
120            let _ = (config, binding);
121            Err(ConnectorError::FeatureUnsupported(
122                "Delta preparation requires the delta-lake feature".into(),
123            ))
124        }
125    }
126
127    async fn open(&mut self, config: &ConnectorConfig) -> Result<(), ConnectorError> {
128        self.state = ConnectorState::Initializing;
129
130        // Re-parse config if properties provided.
131        if !config.properties().is_empty() {
132            self.config = DeltaLakeSinkConfig::from_config(config)?;
133        }
134        if config.get("_arrow_schema").is_some() {
135            let schema = config.arrow_schema().ok_or_else(|| {
136                ConnectorError::ConfigurationError(
137                    "invalid Delta sink '_arrow_schema' encoding".into(),
138                )
139            })?;
140            self.query_schema = Some(Arc::clone(&schema));
141            self.schema = Some(Self::target_schema(&schema, self.config.write_mode));
142        }
143        if self.query_schema.is_none() {
144            return Err(ConnectorError::ConfigurationError("Delta sink needs its bound input schema before open; supply _arrow_schema or with_schema".into()));
145        }
146        #[cfg(feature = "delta-lake")]
147        if self.config.delivery_guarantee == DeliveryGuarantee::ExactlyOnce {
148            super::super::delta_io::validate_coordinated_storage_preflight(
149                &self.config.table_path,
150                &self.config.storage_options,
151            )?;
152        }
153        if self.config.delivery_guarantee == DeliveryGuarantee::ExactlyOnce
154            && !self.is_coordinated()
155        {
156            self.state = ConnectorState::Failed;
157            return Err(ConnectorError::ConfigurationError(
158                "Delta exactly-once requires coordinated append mode".into(),
159            ));
160        }
161
162        self.bind_writer_contract(config).await?;
163
164        if self.config.catalog_type == super::super::delta_config::DeltaCatalogType::None {
165            let explicit_storage = if config.properties().is_empty() {
166                self.config.storage_options.clone()
167            } else {
168                config.properties_with_prefix("storage.")
169            };
170            let diagnostics =
171                StorageCredentialResolver::resolve(&self.config.table_path, &explicit_storage);
172            info!(
173                operation_class = "connector-open",
174                storage_provider = %diagnostics.provider,
175                storage_endpoint_class = %diagnostics.endpoint_class(),
176                storage_auth_source = %diagnostics.auth_source,
177                mode = %self.config.write_mode,
178                guarantee = %self.config.delivery_guarantee,
179                "opening Delta Lake sink connector"
180            );
181        } else {
182            info!(
183                operation_class = "connector-open",
184                storage_provider = "catalog-resolved",
185                storage_endpoint_class = "catalog-resolved",
186                storage_auth_source = "catalog-resolved",
187                mode = %self.config.write_mode,
188                guarantee = %self.config.delivery_guarantee,
189                "opening Delta Lake sink connector"
190            );
191        }
192
193        // Initialize only after the input contract and explicit creation policy are known.
194        #[cfg(feature = "delta-lake")]
195        {
196            let deadline = self.operation_deadline();
197            self.init_delta_table(deadline).await?;
198            if let Some(table) = &self.table {
199                super::super::schema_resolution::verify_identity(
200                    self.schema_binding.as_ref(),
201                    &super::super::schema_resolution::delta_native(table)?,
202                )?;
203            }
204        }
205
206        #[cfg(not(feature = "delta-lake"))]
207        {
208            self.state = ConnectorState::Failed;
209            return Err(ConnectorError::ConfigurationError(
210                "Delta Lake sink requires the 'delta-lake' feature to be enabled. \
211                 Build with: cargo build --features delta-lake"
212                    .into(),
213            ));
214        }
215
216        #[cfg(feature = "delta-lake")]
217        {
218            self.state = ConnectorState::Running;
219            info!("Delta Lake sink connector opened successfully");
220            Ok(())
221        }
222    }
223
224    async fn write_batch(&mut self, batch: &RecordBatch) -> Result<WriteResult, ConnectorError> {
225        if self.state != ConnectorState::Running {
226            return Err(ConnectorError::InvalidState {
227                expected: "Running".into(),
228                actual: self.state.to_string(),
229            });
230        }
231
232        #[cfg(feature = "delta-lake")]
233        {
234            self.ensure_coordinated_reconciled()?;
235            self.ensure_write_generation_usable()?;
236        }
237        #[cfg(feature = "delta-lake")]
238        let deadline = self.operation_deadline();
239
240        if batch.num_rows() == 0 {
241            return Ok(WriteResult::new(0, 0));
242        }
243
244        if self
245            .query_schema
246            .as_ref()
247            .is_none_or(|schema| schema.as_ref() != batch.schema().as_ref())
248        {
249            return Err(ConnectorError::SchemaMismatch(
250                "Delta batch changed the frozen query-output schema".into(),
251            ));
252        }
253        let projected;
254        let batch = if let Some(indices) = &self.writer_projection {
255            projected = batch
256                .project(indices)
257                .map_err(|error| ConnectorError::SchemaMismatch(error.to_string()))?;
258            &projected
259        } else {
260            batch
261        };
262        let num_rows = batch.num_rows();
263        let estimated_bytes = Self::estimate_batch_size(batch);
264
265        // At-least-once retries retain failed staged data, so cap the combined
266        // in-memory backlog. Coordinated append instead drains full buffers to
267        // invisible Parquet files below and bounds only their retained Adds.
268        if !self.is_coordinated() {
269            let pending_rows = self.buffered_rows + self.staged_rows + num_rows;
270            let pending_bytes = self.buffered_bytes + self.staged_bytes + estimated_bytes;
271            let row_cap = self
272                .config
273                .max_buffer_records
274                .saturating_mul(4)
275                .max(num_rows);
276            let byte_cap = (self.config.target_file_size as u64)
277                .saturating_mul(4)
278                .max(estimated_bytes);
279            if pending_rows > row_cap || pending_bytes > byte_cap {
280                return Err(ConnectorError::WriteError(format!(
281                    "delta sink buffer full ({pending_rows} rows, \
282                     {pending_bytes} bytes pending; cap {row_cap} rows, \
283                     {byte_cap} bytes) — backpressure until next flush/commit"
284                )));
285            }
286        }
287
288        // Buffer the batch.
289        if self.buffer_start_time.is_none() {
290            self.buffer_start_time = Some(Instant::now());
291        }
292        self.buffer.push(batch.clone());
293        self.buffered_rows += num_rows;
294        self.buffered_bytes += estimated_bytes;
295
296        #[cfg(feature = "delta-lake")]
297        if self.is_coordinated() && self.should_flush() {
298            self.stage_coordinated_buffer(deadline).await?;
299        } else if self.config.delivery_guarantee != DeliveryGuarantee::ExactlyOnce
300            && self.should_flush()
301        {
302            if !self.staged_batches.is_empty() {
303                self.flush_staged_to_delta(deadline).await?;
304            }
305            self.staged_batches = std::mem::take(&mut self.buffer);
306            self.staged_rows = self.buffered_rows;
307            self.staged_bytes = self.buffered_bytes;
308            self.buffered_rows = 0;
309            self.buffered_bytes = 0;
310            self.buffer_start_time = None;
311            self.flush_staged_to_delta(deadline).await?;
312        }
313
314        Ok(WriteResult::new(0, 0))
315    }
316
317    fn schema(&self) -> SchemaRef {
318        self.schema
319            .clone()
320            .unwrap_or_else(|| Arc::new(arrow_schema::Schema::empty()))
321    }
322
323    async fn checkpoint_artifact_intent(
324        &mut self,
325        _epoch: u64,
326    ) -> Result<Option<Vec<u8>>, ConnectorError> {
327        // Delta retains unpublished files for retention-safe vacuum because a cancelled catalog
328        // commit may still publish them. Abort cleanup therefore has no exact deletion payload.
329        Ok(None)
330    }
331
332    async fn begin_epoch(&mut self, epoch: u64) -> Result<(), ConnectorError> {
333        #[cfg(feature = "delta-lake")]
334        {
335            self.ensure_coordinated_reconciled()?;
336            self.ensure_write_generation_usable()?;
337        }
338        #[cfg(feature = "delta-lake")]
339        if self.is_coordinated()
340            && (!self.buffer.is_empty()
341                || !self.staged_batches.is_empty()
342                || !self.coordinated_adds.is_empty())
343        {
344            return Err(ConnectorError::InvalidState {
345                expected: "the previous coordinated epoch to be prepared or rolled back".into(),
346                actual: "unresolved Delta staging data remains".into(),
347            });
348        }
349
350        self.current_epoch = epoch;
351        self.buffer.clear();
352        self.buffered_rows = 0;
353        self.buffered_bytes = 0;
354        self.buffer_start_time = None;
355
356        debug!(epoch, "Delta Lake: began epoch");
357        Ok(())
358    }
359
360    async fn pre_commit(&mut self, epoch: u64) -> Result<Option<Vec<u8>>, ConnectorError> {
361        // Coordinated append may already have materialized several invisible
362        // Parquet chunks during writes. Flush only the remainder, then return
363        // one bounded descriptor for the entire checkpoint cut.
364        #[cfg(feature = "delta-lake")]
365        if self.is_coordinated() {
366            self.ensure_coordinated_reconciled()?;
367            self.ensure_write_generation_usable()?;
368            let deadline = self.operation_deadline();
369            self.renew_credentials_if_expiring(deadline).await?;
370            self.stage_coordinated_buffer(deadline).await?;
371            if self.coordinated_adds.is_empty() {
372                if self.coordinated_binding.is_some() {
373                    return Err(ConnectorError::Internal(
374                        "empty Delta coordinated cut retained a table binding".into(),
375                    ));
376                }
377                return Ok(None);
378            }
379
380            let binding = self.coordinated_binding.as_ref().ok_or_else(|| {
381                ConnectorError::Internal(
382                    "non-empty Delta coordinated cut has no table binding".into(),
383                )
384            })?;
385            let descriptor =
386                super::super::delta_io::encode_commit_descriptor(binding, &self.coordinated_adds)?;
387            if descriptor.len() != self.coordinated_descriptor_bytes
388                || descriptor.len() > MAX_COORDINATED_COMMIT_PAYLOAD_BYTES
389            {
390                return Err(ConnectorError::Internal(format!(
391                    "Delta coordinated descriptor accounting mismatch: tracked {}, encoded {}",
392                    self.coordinated_descriptor_bytes,
393                    descriptor.len()
394                )));
395            }
396            self.coordinated_adds.clear();
397            self.coordinated_binding = None;
398            self.coordinated_descriptor_bytes = 0;
399            return Ok(Some(descriptor));
400        }
401
402        // Non-coordinated callers retain the existing in-memory phase-one
403        // behavior; the checkpoint runtime only invokes this path for sinks
404        // whose contract admits it.
405        if !self.buffer.is_empty() {
406            self.staged_batches = std::mem::take(&mut self.buffer);
407            self.staged_rows = self.buffered_rows;
408            self.staged_bytes = self.buffered_bytes;
409            self.buffered_rows = 0;
410            self.buffered_bytes = 0;
411            self.buffer_start_time = None;
412        }
413
414        debug!(epoch, "Delta Lake: pre-committed (batches staged)");
415        Ok(None)
416    }
417
418    async fn rollback_epoch(&mut self, epoch: u64) -> Result<(), ConnectorError> {
419        // Discard buffered/staged data. A coordinated attempt may already have
420        // materialized an unreferenced immutable Parquet file; it must be reclaimed only by a
421        // retention-safe vacuum after ambiguous catalog commits are impossible, never here.
422        self.buffer.clear();
423        self.buffered_rows = 0;
424        self.buffered_bytes = 0;
425        self.buffer_start_time = None;
426        self.staged_batches.clear();
427        self.staged_rows = 0;
428        self.staged_bytes = 0;
429        #[cfg(feature = "delta-lake")]
430        {
431            self.coordinated_adds.clear();
432            self.coordinated_binding = None;
433            self.coordinated_descriptor_bytes = 0;
434        }
435
436        self.metrics.record_rollback();
437        warn!(epoch, "Delta Lake: rolled back epoch");
438        Ok(())
439    }
440
441    fn suggested_write_timeout(&self) -> Duration {
442        self.config.write_timeout
443    }
444
445    async fn flush(&mut self) -> Result<(), ConnectorError> {
446        #[cfg(feature = "delta-lake")]
447        {
448            self.ensure_coordinated_reconciled()?;
449            self.ensure_write_generation_usable()?;
450        }
451        #[cfg(feature = "delta-lake")]
452        let deadline = self.operation_deadline();
453
454        // For at-least-once delivery, flush() is the commit trigger. Write
455        // directly to Delta without entering the coordinated protocol.
456        #[cfg(feature = "delta-lake")]
457        if self.config.delivery_guarantee != DeliveryGuarantee::ExactlyOnce {
458            // Retry any orphaned staged data from a prior failed flush
459            // before moving new data in, to prevent silent data loss.
460            // flush_staged_to_delta handles table == None via reopen_table().
461            if !self.staged_batches.is_empty() {
462                self.flush_staged_to_delta(deadline).await?;
463            }
464
465            // Stage new buffered data and flush to Delta.
466            if !self.buffer.is_empty() {
467                self.staged_batches = std::mem::take(&mut self.buffer);
468                self.staged_rows = self.buffered_rows;
469                self.staged_bytes = self.buffered_bytes;
470                self.buffered_rows = 0;
471                self.buffered_bytes = 0;
472                self.buffer_start_time = None;
473
474                self.flush_staged_to_delta(deadline).await?;
475            }
476            return Ok(());
477        }
478
479        #[cfg(feature = "delta-lake")]
480        return self.stage_coordinated_buffer(deadline).await;
481
482        #[cfg(not(feature = "delta-lake"))]
483        Ok(())
484    }
485
486    async fn close(&mut self) -> Result<(), ConnectorError> {
487        info!("closing Delta Lake sink connector");
488
489        // Only at-least-once may land buffered data during close. Exactly-once output without a
490        // matching durable checkpoint must be discarded and replayed after recovery.
491        #[cfg(feature = "delta-lake")]
492        if self.config.delivery_guarantee == DeliveryGuarantee::ExactlyOnce
493            || self.unresolved_delta_write
494        {
495            self.buffer.clear();
496            self.staged_batches.clear();
497            self.buffered_rows = 0;
498            self.buffered_bytes = 0;
499            self.staged_rows = 0;
500            self.staged_bytes = 0;
501            self.coordinated_adds.clear();
502            self.coordinated_binding = None;
503            self.coordinated_descriptor_bytes = 0;
504        } else {
505            self.flush().await?;
506        }
507
508        // Drop the table handle when closing.
509        #[cfg(feature = "delta-lake")]
510        {
511            self.table = None;
512        }
513
514        self.state = ConnectorState::Closed;
515
516        info!(
517            delta_version = self.delta_version,
518            "Delta Lake sink connector closed"
519        );
520
521        Ok(())
522    }
523
524    #[cfg(feature = "delta-lake")]
525    fn as_coordinated_committer(&self) -> Option<&dyn crate::connector::CoordinatedCommitter> {
526        self.is_coordinated()
527            .then_some(self as &dyn crate::connector::CoordinatedCommitter)
528    }
529
530    fn coordinated_abort_cleaner(
531        &self,
532    ) -> Option<Arc<dyn crate::connector::CoordinatedAbortCleaner>> {
533        None
534    }
535}
536
537#[cfg(feature = "delta-lake")]
538#[async_trait]
539impl crate::connector::CoordinatedCommitter for DeltaLakeSink {
540    async fn commit_aggregated(
541        &self,
542        batch: crate::connector::CoordinatedCommitBatch,
543        context: crate::connector::CoordinatedCommitContext,
544    ) -> Result<(), ConnectorError> {
545        let deadline = context.deadline();
546        let publication_budget = context.remaining();
547        if publication_budget.is_zero() {
548            return Err(ConnectorError::TransactionError(
549                "Delta coordinated publication deadline elapsed before I/O".into(),
550            ));
551        }
552
553        batch.validate_shape().map_err(|error| {
554            ConnectorError::TransactionError(format!(
555                "Delta coordinated batch validation failed: {error}"
556            ))
557        })?;
558        let external_key = batch.namespace.external_key();
559        let exact_batch_fingerprint = batch.exact_fingerprint();
560        let target_cursor = crate::connector::CoordinatedCommitCursor {
561            checkpoint_id: batch.target.checkpoint_id,
562            fencing_token: batch.fencing_token,
563        };
564        let pending = UnresolvedDeltaPublication {
565            external_key,
566            target: target_cursor,
567            exact_batch_fingerprint,
568        };
569        {
570            let mut unresolved = self.coordinated_unresolved_publication.lock();
571            if unresolved
572                .as_ref()
573                .is_some_and(|unresolved| unresolved != &pending)
574            {
575                return Err(ConnectorError::TransactionError(
576                    "Delta has a different unresolved coordinated publication; only that exact cut may be retried"
577                        .into(),
578                ));
579            }
580            *unresolved = Some(pending.clone());
581        }
582
583        let operation = publish_coordinated_delta_batch(
584            self.resolved_table_path.clone(),
585            self.resolved_storage_options.clone(),
586            Arc::clone(&self.coordinated_unresolved_publication),
587            pending.clone(),
588            batch,
589            deadline,
590            publication_budget,
591        );
592        // Tokio task-local state is intentionally propagated only for the
593        // deterministic delayed-catalog test hook.
594        #[cfg(test)]
595        let operation = {
596            let delay = super::super::delta_io::DELAY_COORDINATED_CATALOG_COMMIT
597                .try_with(Clone::clone)
598                .ok();
599            async move {
600                if let Some(delay) = delay {
601                    super::super::delta_io::DELAY_COORDINATED_CATALOG_COMMIT
602                        .scope(delay, operation)
603                        .await
604                } else {
605                    operation.await
606                }
607            }
608        };
609        let task = match self.spawn_tracked_delta_task(operation) {
610            Ok(task) => task,
611            Err(error) => {
612                let mut unresolved = self.coordinated_unresolved_publication.lock();
613                if unresolved.as_ref() == Some(&pending) {
614                    *unresolved = None;
615                }
616                return Err(error);
617            }
618        };
619
620        match tokio::time::timeout_at(deadline, task).await {
621            Ok(Ok(result)) => result,
622            Ok(Err(error)) => Err(ConnectorError::outcome_unknown(
623                format!("Delta coordinated publication task terminated unexpectedly: {error}"),
624                true,
625            )),
626            Err(_) => Err(ConnectorError::outcome_unknown(
627                format!(
628                    "Delta coordinated publication exceeded its {publication_budget:?} remaining \
629                     budget; reconcile the exact cursor before replay"
630                ),
631                true,
632            )),
633        }
634    }
635
636    async fn committed_cursor(
637        &self,
638        namespace: &crate::connector::CoordinatedCommitNamespace,
639    ) -> Result<Option<crate::connector::CoordinatedCommitCursor>, ConnectorError> {
640        namespace.validate()?;
641        let external_key = namespace.external_key();
642        let deadline = self.operation_deadline();
643        // RECOVERY: both operations are metadata reads. Retrying typed transient failures cannot
644        // duplicate publication; the sink-task command still clamps this future to its caller's
645        // earlier checkpoint deadline.
646        let observed = retry_delta_metadata_until(deadline, "coordinated cursor read", || async {
647            let table = super::super::delta_io::open_or_create_table(
648                &self.resolved_table_path,
649                self.resolved_storage_options.clone(),
650                None,
651            )
652            .await?;
653            // A replacement table carries no cursor; reading it as "never published" would
654            // replay committed output into a different table incarnation.
655            super::super::schema_resolution::verify_identity(
656                self.schema_binding.as_ref(),
657                &super::super::schema_resolution::delta_native(&table)?,
658            )?;
659            super::super::delta_io::get_coordinated_cursor(&table, &external_key).await
660        })
661        .await?;
662        let mut unresolved = self.coordinated_unresolved_publication.lock();
663        if unresolved.as_ref().is_some_and(|pending| {
664            pending.external_key == external_key && pending.reconciled_by(observed)
665        }) {
666            *unresolved = None;
667        }
668        Ok(observed)
669    }
670}