|
3 | 3 | from copy import deepcopy |
4 | 4 | from datetime import datetime, timedelta |
5 | 5 | from typing import Any, List, Mapping, MutableMapping, Optional, Union |
| 6 | +from unittest.mock import MagicMock, patch |
6 | 7 | from urllib.parse import unquote |
7 | 8 |
|
8 | 9 | import pytest |
|
18 | 19 | from airbyte_cdk.sources.declarative.concurrent_declarative_source import ( |
19 | 20 | ConcurrentDeclarativeSource, |
20 | 21 | ) |
| 22 | +from airbyte_cdk.sources.declarative.incremental import ConcurrentPerPartitionCursor |
21 | 23 | from airbyte_cdk.test.catalog_builder import CatalogBuilder, ConfiguredAirbyteStreamBuilder |
22 | 24 | from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read |
23 | 25 |
|
@@ -1181,14 +1183,18 @@ def test_incremental_parent_state( |
1181 | 1183 | initial_state, |
1182 | 1184 | expected_state, |
1183 | 1185 | ): |
1184 | | - run_incremental_parent_state_test( |
1185 | | - manifest, |
1186 | | - mock_requests, |
1187 | | - expected_records, |
1188 | | - num_intermediate_states, |
1189 | | - initial_state, |
1190 | | - [expected_state], |
1191 | | - ) |
| 1186 | + # Patch `_throttle_state_message` so it always returns a float (indicating "no throttle") |
| 1187 | + with patch.object( |
| 1188 | + ConcurrentPerPartitionCursor, "_throttle_state_message", return_value=9999999.0 |
| 1189 | + ): |
| 1190 | + run_incremental_parent_state_test( |
| 1191 | + manifest, |
| 1192 | + mock_requests, |
| 1193 | + expected_records, |
| 1194 | + num_intermediate_states, |
| 1195 | + initial_state, |
| 1196 | + [expected_state], |
| 1197 | + ) |
1192 | 1198 |
|
1193 | 1199 |
|
1194 | 1200 | STATE_MIGRATION_EXPECTED_STATE = { |
@@ -2967,3 +2973,47 @@ def test_incremental_substream_request_options_provider( |
2967 | 2973 | expected_records, |
2968 | 2974 | expected_state, |
2969 | 2975 | ) |
| 2976 | + |
| 2977 | + |
| 2978 | +def test_state_throttling(mocker): |
| 2979 | + """ |
| 2980 | + Verifies that _emit_state_message does not emit a new state if less than 60s |
| 2981 | + have passed since last emission, and does emit once 60s or more have passed. |
| 2982 | + """ |
| 2983 | + cursor = ConcurrentPerPartitionCursor( |
| 2984 | + cursor_factory=MagicMock(), |
| 2985 | + partition_router=MagicMock(), |
| 2986 | + stream_name="test_stream", |
| 2987 | + stream_namespace=None, |
| 2988 | + stream_state={}, |
| 2989 | + message_repository=MagicMock(), |
| 2990 | + connector_state_manager=MagicMock(), |
| 2991 | + connector_state_converter=MagicMock(), |
| 2992 | + cursor_field=MagicMock(), |
| 2993 | + ) |
| 2994 | + |
| 2995 | + mock_connector_manager = cursor._connector_state_manager |
| 2996 | + mock_repo = cursor._message_repository |
| 2997 | + |
| 2998 | + # Set the last emission time to "0" so we can control offset from that |
| 2999 | + cursor._last_emission_time = 0 |
| 3000 | + |
| 3001 | + mock_time = mocker.patch("time.time") |
| 3002 | + |
| 3003 | + # First attempt: only 10 seconds passed => NO emission |
| 3004 | + mock_time.return_value = 10 |
| 3005 | + cursor._emit_state_message() |
| 3006 | + mock_connector_manager.update_state_for_stream.assert_not_called() |
| 3007 | + mock_repo.emit_message.assert_not_called() |
| 3008 | + |
| 3009 | + # Second attempt: 30 seconds passed => still NO emission |
| 3010 | + mock_time.return_value = 30 |
| 3011 | + cursor._emit_state_message() |
| 3012 | + mock_connector_manager.update_state_for_stream.assert_not_called() |
| 3013 | + mock_repo.emit_message.assert_not_called() |
| 3014 | + |
| 3015 | + # Advance time: 70 seconds => exceed 60s => MUST emit |
| 3016 | + mock_time.return_value = 70 |
| 3017 | + cursor._emit_state_message() |
| 3018 | + mock_connector_manager.update_state_for_stream.assert_called_once() |
| 3019 | + mock_repo.emit_message.assert_called_once() |
0 commit comments