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