-
Notifications
You must be signed in to change notification settings - Fork 49
Expand file tree
/
Copy pathconcurrent_source.py
More file actions
264 lines (248 loc) · 12.1 KB
/
Copy pathconcurrent_source.py
File metadata and controls
264 lines (248 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
import concurrent
import logging
import os
import time
from queue import Empty, Queue
from typing import Iterable, Iterator, List, Optional
from airbyte_cdk.models import AirbyteMessage, FailureType
from airbyte_cdk.sources.concurrent_source.concurrent_read_processor import ConcurrentReadProcessor
from airbyte_cdk.sources.concurrent_source.partition_generation_completed_sentinel import (
PartitionGenerationCompletedSentinel,
)
from airbyte_cdk.sources.concurrent_source.stream_thread_exception import StreamThreadException
from airbyte_cdk.sources.concurrent_source.thread_pool_manager import ThreadPoolManager
from airbyte_cdk.sources.message import InMemoryMessageRepository, MessageRepository
from airbyte_cdk.sources.streams.concurrent.abstract_stream import AbstractStream
from airbyte_cdk.sources.streams.concurrent.partition_enqueuer import PartitionEnqueuer
from airbyte_cdk.sources.streams.concurrent.partition_reader import PartitionLogger, PartitionReader
from airbyte_cdk.sources.streams.concurrent.partitions.partition import Partition
from airbyte_cdk.sources.streams.concurrent.partitions.types import (
PartitionCompleteSentinel,
QueueItem,
)
from airbyte_cdk.sources.types import Record
from airbyte_cdk.sources.utils.slice_logger import DebugSliceLogger, SliceLogger
from airbyte_cdk.utils import AirbyteTracedException
class ConcurrentSource:
"""
A Source that reads data from multiple AbstractStreams concurrently.
It does so by submitting partition generation, and partition read tasks to a thread pool.
The tasks asynchronously add their output to a shared queue.
The read is done when all partitions for all streams w ere generated and read.
"""
DEFAULT_TIMEOUT_SECONDS = 900
QUEUE_POLL_INTERVAL_SECONDS = 60
@staticmethod
def create(
num_workers: int,
initial_number_of_partitions_to_generate: int,
logger: logging.Logger,
slice_logger: SliceLogger,
message_repository: MessageRepository,
queue: Optional[Queue[QueueItem]] = None,
timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
no_progress_timeout_seconds: Optional[int] = None,
) -> "ConcurrentSource":
if initial_number_of_partitions_to_generate < 1:
raise ValueError(
f"initial_number_of_partitions_to_generate must be >= 1, got {initial_number_of_partitions_to_generate}"
)
is_single_threaded = initial_number_of_partitions_to_generate == 1 and num_workers == 1
too_many_generator = (
not is_single_threaded and initial_number_of_partitions_to_generate >= num_workers
)
assert not too_many_generator, (
"It is required to have more workers than threads generating partitions"
)
threadpool = ThreadPoolManager(
concurrent.futures.ThreadPoolExecutor(
max_workers=num_workers, thread_name_prefix="workerpool"
),
logger,
)
return ConcurrentSource(
threadpool=threadpool,
logger=logger,
slice_logger=slice_logger,
queue=queue,
message_repository=message_repository,
initial_number_partitions_to_generate=initial_number_of_partitions_to_generate,
timeout_seconds=timeout_seconds,
no_progress_timeout_seconds=no_progress_timeout_seconds,
)
def __init__(
self,
threadpool: ThreadPoolManager,
logger: logging.Logger,
slice_logger: SliceLogger = DebugSliceLogger(),
queue: Optional[Queue[QueueItem]] = None,
message_repository: MessageRepository = InMemoryMessageRepository(),
initial_number_partitions_to_generate: int = 1,
timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
no_progress_timeout_seconds: Optional[int] = None,
) -> None:
"""
:param threadpool: The threadpool to submit tasks to
:param logger: The logger to log to
:param slice_logger: The slice logger used to create messages on new slices
:param message_repository: The repository to emit messages to
:param initial_number_partitions_to_generate: The initial number of concurrent partition generation tasks. Limiting this number ensures will limit the latency of the first records emitted. While the latency is not critical, emitting the records early allows the platform and the destination to process them as early as possible.
:param timeout_seconds: The interval in seconds between no-progress warning logs.
:param no_progress_timeout_seconds: The optional number of seconds without queue progress before stopping the source.
"""
self._threadpool = threadpool
self._logger = logger
self._slice_logger = slice_logger
self._message_repository = message_repository
self._initial_number_partitions_to_generate = initial_number_partitions_to_generate
self._timeout_seconds = timeout_seconds
self._no_progress_timeout_seconds = self._get_no_progress_timeout(
no_progress_timeout_seconds
)
# We set a maxsize to for the main thread to process record items when the queue size grows. This assumes that there are less
# threads generating partitions that than are max number of workers. If it weren't the case, we could have threads only generating
# partitions which would fill the queue. This number is arbitrarily set to 10_000 but will probably need to be changed given more
# information and might even need to be configurable depending on the source
self._queue = queue or Queue(maxsize=10_000)
def read(
self,
streams: List[AbstractStream],
) -> Iterator[AirbyteMessage]:
self._logger.info("Starting syncing")
concurrent_stream_processor = ConcurrentReadProcessor(
streams,
PartitionEnqueuer(self._queue, self._threadpool),
self._threadpool,
self._logger,
self._slice_logger,
self._message_repository,
PartitionReader(
self._queue,
PartitionLogger(self._slice_logger, self._logger, self._message_repository),
),
max_concurrent_partition_generators=self._initial_number_partitions_to_generate,
)
# Enqueue initial partition generation tasks
yield from self._submit_initial_partition_generators(concurrent_stream_processor)
# Read from the queue until all partitions were generated and read
yield from self._consume_from_queue(
self._queue,
concurrent_stream_processor,
)
self._threadpool.check_for_errors_and_shutdown()
self._logger.info("Finished syncing")
def _get_no_progress_timeout(self, configured_timeout: Optional[int]) -> Optional[int]:
if configured_timeout is not None:
if configured_timeout <= 0:
self._logger.warning(
"Ignoring non-positive no_progress_timeout_seconds value: %s",
configured_timeout,
)
return None
return configured_timeout
configured_timeout_from_env = os.getenv("AIRBYTE_NO_PROGRESS_TIMEOUT_SECONDS")
if configured_timeout_from_env is None:
return None
try:
timeout = int(configured_timeout_from_env)
except ValueError:
self._logger.warning(
"Ignoring invalid AIRBYTE_NO_PROGRESS_TIMEOUT_SECONDS value: %s",
configured_timeout_from_env,
)
return None
if timeout <= 0:
self._logger.warning(
"Ignoring non-positive AIRBYTE_NO_PROGRESS_TIMEOUT_SECONDS value: %s",
configured_timeout_from_env,
)
return None
return timeout
def _submit_initial_partition_generators(
self, concurrent_stream_processor: ConcurrentReadProcessor
) -> Iterable[AirbyteMessage]:
for _ in range(self._initial_number_partitions_to_generate):
status_message = concurrent_stream_processor.start_next_partition_generator()
if status_message:
yield status_message
def _consume_from_queue(
self,
queue: Queue[QueueItem],
concurrent_stream_processor: ConcurrentReadProcessor,
) -> Iterable[AirbyteMessage]:
last_progress = time.monotonic()
last_warning = last_progress
poll_interval = min(
self.QUEUE_POLL_INTERVAL_SECONDS,
self._timeout_seconds,
self._no_progress_timeout_seconds or self._timeout_seconds,
)
while True:
try:
airbyte_message_or_record_or_exception = queue.get(timeout=poll_interval)
except Empty:
now = time.monotonic()
if now - last_progress >= self._timeout_seconds:
if now - last_warning >= self._timeout_seconds:
in_flight_description = (
concurrent_stream_processor.get_in_flight_streams_description()
)
self._logger.warning(
"No queue progress for %s seconds. %s",
self._timeout_seconds,
in_flight_description,
)
last_warning = now
if (
self._no_progress_timeout_seconds is not None
and now - last_progress >= self._no_progress_timeout_seconds
):
in_flight_description = (
concurrent_stream_processor.get_in_flight_streams_description()
)
self._threadpool.shutdown()
raise AirbyteTracedException(
message=f"Source made no progress for {self._no_progress_timeout_seconds} seconds and was stopped.",
internal_message=(
f"No queue progress for {self._no_progress_timeout_seconds} seconds. "
f"{in_flight_description}"
),
failure_type=FailureType.system_error,
)
continue
last_progress = time.monotonic()
if not airbyte_message_or_record_or_exception:
break
yield from self._handle_item(
airbyte_message_or_record_or_exception,
concurrent_stream_processor,
)
# In the event that a partition raises an exception, anything remaining in
# the queue will be missed because is_done() can raise an exception and exit
# out of this loop before remaining items are consumed
if queue.empty() and concurrent_stream_processor.is_done():
# all partitions were generated and processed. we're done here
break
def _handle_item(
self,
queue_item: QueueItem,
concurrent_stream_processor: ConcurrentReadProcessor,
) -> Iterable[AirbyteMessage]:
# handle queue item and call the appropriate handler depending on the type of the queue item
if isinstance(queue_item, StreamThreadException):
yield from concurrent_stream_processor.on_exception(queue_item)
elif isinstance(queue_item, PartitionGenerationCompletedSentinel):
yield from concurrent_stream_processor.on_partition_generation_completed(queue_item)
elif isinstance(queue_item, Partition):
concurrent_stream_processor.on_partition(queue_item)
elif isinstance(queue_item, PartitionCompleteSentinel):
yield from concurrent_stream_processor.on_partition_complete_sentinel(queue_item)
elif isinstance(queue_item, Record):
yield from concurrent_stream_processor.on_record(queue_item)
elif isinstance(queue_item, AirbyteMessage):
yield queue_item
else:
raise ValueError(f"Unknown queue item type: {type(queue_item)}")