Files
open-metadata--openmetadata/ingestion/tests/unit/test_sink_buffer_on_flush_failure.py
wehub-resource-sync bf2343b7e4
Integration Tests - MySQL + Elasticsearch / Detect Changes (push) Has been cancelled
Integration Tests - MySQL + Elasticsearch / integration-tests-mysql-elasticsearch (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / integration-tests-postgres-elasticsearch-redis (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / integration-tests-postgres-opensearch (push) Has been cancelled
Java Checkstyle / java-checkstyle (push) Has been cancelled
Maven Collate Tests / maven-collate-ci (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests-status (push) Has been cancelled
Publish Package to Maven Central Repository / publish-maven-packages (push) Has been cancelled
OpenMetadata Service Unit Tests / Detect Changes (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests (push) Has been cancelled
OpenMetadata Service Unit Tests / k8s_operator-unit-tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:35:45 +08:00

183 lines
7.6 KiB
Python

# Copyright 2025 Collate
# Licensed under the Collate Community License, Version 1.0 (the "License");
"""
Tests verifying that the bulk sink buffer is properly cleared when
bulk_create_or_update raises an exception, preventing:
1. Stale entities accumulating in the buffer
2. Unbounded buffer growth across flushes
3. Duplicate entities sent on subsequent flushes
4. Stale dedup tracking in buffered_entity_names
"""
from unittest.mock import MagicMock, Mock
import pytest
from metadata.generated.schema.api.data.createDashboardDataModel import (
CreateDashboardDataModelRequest,
)
from metadata.generated.schema.entity.data.dashboardDataModel import DataModelType
from metadata.generated.schema.entity.data.table import Column, DataType
from metadata.generated.schema.type.basic import EntityName, FullyQualifiedEntityName
from metadata.ingestion.sink.metadata_rest import (
MetadataRestSink,
MetadataRestSinkConfig,
)
def _make_data_model(name: str, display_name: str) -> CreateDashboardDataModelRequest:
return CreateDashboardDataModelRequest(
name=EntityName(name),
displayName=display_name,
service=FullyQualifiedEntityName("test_service"),
dataModelType=DataModelType.QuickSightDataModel,
columns=[Column(name="col1", dataType=DataType.STRING)],
)
@pytest.fixture
def sink():
mock_metadata = Mock()
config = MetadataRestSinkConfig(bulk_sink_batch_size=3)
return MetadataRestSink(config, mock_metadata)
class TestBufferClearedOnFlushException:
"""Verify that when bulk_create_or_update throws an exception the buffer
and dedup tracking dict are still cleared, preventing memory leaks."""
def test_buffer_cleared_after_flush_exception(self, sink):
"""Buffer must be empty after a failed flush so that stale entities
do not accumulate in memory."""
sink.metadata.bulk_create_or_update = Mock(side_effect=Exception("Connection refused"))
sink.write_create_request(_make_data_model("dm-1", "Model 1"))
sink.write_create_request(_make_data_model("dm-2", "Model 2"))
sink.write_create_request(_make_data_model("dm-3", "Model 3"))
assert len(sink.buffer) == 0
def test_buffer_does_not_grow_across_failed_flushes(self, sink):
"""With continuous failures the buffer must stay bounded at
batch_size, not grow linearly with total entities ingested."""
sink.metadata.bulk_create_or_update = Mock(side_effect=Exception("Connection refused"))
# Three consecutive batches, all failing
for i in range(9):
sink.write_create_request(_make_data_model(f"dm-{i}", f"Model {i}"))
# Buffer should contain at most the last partial batch (0, 1, or 2
# entities) — never more than batch_size
assert len(sink.buffer) < sink.config.bulk_sink_batch_size
def test_no_duplicate_entities_on_subsequent_flush(self, sink):
"""After a failed flush, the next flush must only contain new
entities — not stale ones from the failed batch."""
call_count = 0
entities_per_call = []
def track_bulk_calls(entities, use_async=False, **kwargs):
nonlocal call_count
call_count += 1
entities_per_call.append([e.displayName for e in entities])
if call_count == 1:
raise Exception("Transient failure") # noqa: TRY002
result = MagicMock()
result.status.value = "success"
result.numberOfRowsProcessed.root = len(entities)
result.numberOfRowsFailed.root = 0
result.successRequest = entities
result.failedRequest = []
return result
sink.metadata.bulk_create_or_update = Mock(side_effect=track_bulk_calls)
# First batch: flush fails
sink.write_create_request(_make_data_model("dm-1", "Model 1"))
sink.write_create_request(_make_data_model("dm-2", "Model 2"))
sink.write_create_request(_make_data_model("dm-3", "Model 3"))
assert call_count == 1
assert entities_per_call[0] == ["Model 1", "Model 2", "Model 3"]
# Second batch: flush succeeds — must only contain new entities
sink.write_create_request(_make_data_model("dm-4", "Model 4"))
sink.write_create_request(_make_data_model("dm-5", "Model 5"))
sink.write_create_request(_make_data_model("dm-6", "Model 6"))
assert call_count == 2
assert entities_per_call[1] == ["Model 4", "Model 5", "Model 6"]
def test_dedup_tracking_cleared_after_flush_exception(self, sink):
"""buffered_entity_names must be cleared alongside the buffer so that
a re-sent entity with the same name is not incorrectly rejected."""
sink.metadata.bulk_create_or_update = Mock(side_effect=Exception("Connection refused"))
dm = _make_data_model("same-name", "Original Model")
sink.write_create_request(dm)
# Flush hasn't triggered yet (1 < batch_size=3), so tracking is populated
assert (
"CreateDashboardDataModelRequest",
"same-name",
) in sink.buffered_entity_names
# Add two more to trigger the flush (which fails)
sink.write_create_request(_make_data_model("dm-2", "Model 2"))
sink.write_create_request(_make_data_model("dm-3", "Model 3"))
# After the failed flush, both buffer and tracking dict are cleared
assert len(sink.buffered_entity_names) == 0
# Re-sending the same entity name should now be accepted into the buffer
dm_retry = _make_data_model("same-name", "Retry Model")
sink.write_create_request(dm_retry)
assert len(sink.buffer) == 1
assert sink.buffer[0].displayName == "Retry Model"
def test_close_does_not_flush_stale_entities(self, sink):
"""After failed flushes that properly clear the buffer, close()
should only flush the remaining partial batch — not accumulated
stale entities from prior failures."""
call_count = 0
entities_per_call = []
def track_bulk_calls(entities, use_async=False, **kwargs):
nonlocal call_count
call_count += 1
entities_per_call.append(len(entities))
if call_count <= 2:
raise Exception("Transient failure") # noqa: TRY002
result = MagicMock()
result.status.value = "success"
result.numberOfRowsProcessed.root = len(entities)
result.numberOfRowsFailed.root = 0
result.successRequest = entities
result.failedRequest = []
return result
sink.metadata.bulk_create_or_update = Mock(side_effect=track_bulk_calls)
# First batch (3 entities): flush fails (call 1), buffer cleared
sink.write_create_request(_make_data_model("dm-1", "M1"))
sink.write_create_request(_make_data_model("dm-2", "M2"))
sink.write_create_request(_make_data_model("dm-3", "M3"))
assert call_count == 1
# Second batch (3 entities): flush fails (call 2), buffer cleared
sink.write_create_request(_make_data_model("dm-4", "M4"))
sink.write_create_request(_make_data_model("dm-5", "M5"))
sink.write_create_request(_make_data_model("dm-6", "M6"))
assert call_count == 2
# Partial batch (2 entities): no flush triggered yet
sink.write_create_request(_make_data_model("dm-7", "M7"))
sink.write_create_request(_make_data_model("dm-8", "M8"))
assert len(sink.buffer) == 2
# close() flushes only the 2 remaining entities (call 3) — succeeds
sink.close()
assert call_count == 3
assert entities_per_call[2] == 2