deltacat 1.1.5__py3-none-any.whl → 1.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deltacat/__init__.py +1 -1
- deltacat/aws/constants.py +21 -2
- deltacat/aws/s3u.py +107 -33
- deltacat/compute/compactor/model/round_completion_info.py +4 -0
- deltacat/compute/compactor_v2/compaction_session.py +51 -25
- deltacat/compute/compactor_v2/constants.py +12 -0
- deltacat/compute/compactor_v2/model/compaction_session.py +21 -0
- deltacat/compute/compactor_v2/steps/hash_bucket.py +6 -0
- deltacat/compute/compactor_v2/steps/merge.py +6 -0
- deltacat/compute/compactor_v2/utils/task_options.py +8 -2
- deltacat/storage/interface.py +10 -3
- deltacat/tests/aws/test_s3u.py +193 -0
- deltacat/tests/catalog/test_default_catalog_impl.py +2 -0
- deltacat/tests/compute/compact_partition_test_cases.py +61 -0
- deltacat/tests/compute/compactor_v2/test_compaction_session.py +2 -0
- deltacat/tests/compute/test_compact_partition_incremental.py +89 -32
- deltacat/tests/compute/test_compact_partition_rebase_then_incremental.py +21 -26
- deltacat/tests/compute/test_util_create_table_deltas_repo.py +45 -2
- deltacat/tests/local_deltacat_storage/__init__.py +38 -19
- deltacat/tests/utils/ray_utils/__init__.py +0 -0
- deltacat/tests/utils/ray_utils/test_concurrency.py +50 -0
- deltacat/tests/utils/test_resources.py +28 -0
- deltacat/utils/resources.py +45 -0
- {deltacat-1.1.5.dist-info → deltacat-1.1.7.dist-info}/METADATA +1 -1
- {deltacat-1.1.5.dist-info → deltacat-1.1.7.dist-info}/RECORD +28 -25
- {deltacat-1.1.5.dist-info → deltacat-1.1.7.dist-info}/LICENSE +0 -0
- {deltacat-1.1.5.dist-info → deltacat-1.1.7.dist-info}/WHEEL +0 -0
- {deltacat-1.1.5.dist-info → deltacat-1.1.7.dist-info}/top_level.txt +0 -0
deltacat/__init__.py
CHANGED
deltacat/aws/constants.py
CHANGED
@@ -1,8 +1,27 @@
|
|
1
|
-
|
1
|
+
import botocore
|
2
|
+
from typing import Set
|
2
3
|
|
3
4
|
from deltacat.utils.common import env_integer, env_string
|
4
5
|
|
5
6
|
DAFT_MAX_S3_CONNECTIONS_PER_FILE = env_integer("DAFT_MAX_S3_CONNECTIONS_PER_FILE", 8)
|
6
7
|
BOTO_MAX_RETRIES = env_integer("BOTO_MAX_RETRIES", 5)
|
7
|
-
|
8
|
+
BOTO_TIMEOUT_ERROR_CODES: Set[str] = {"ReadTimeoutError", "ConnectTimeoutError"}
|
9
|
+
BOTO_THROTTLING_ERROR_CODES: Set[str] = {"Throttling", "SlowDown"}
|
10
|
+
RETRYABLE_TRANSIENT_ERRORS = (
|
11
|
+
OSError,
|
12
|
+
botocore.exceptions.ConnectionError,
|
13
|
+
botocore.exceptions.HTTPClientError,
|
14
|
+
botocore.exceptions.NoCredentialsError,
|
15
|
+
botocore.exceptions.ConnectTimeoutError,
|
16
|
+
botocore.exceptions.ReadTimeoutError,
|
17
|
+
)
|
8
18
|
AWS_REGION = env_string("AWS_REGION", "us-east-1")
|
19
|
+
UPLOAD_DOWNLOAD_RETRY_STOP_AFTER_DELAY = env_integer(
|
20
|
+
"UPLOAD_DOWNLOAD_RETRY_STOP_AFTER_DELAY", 10 * 60
|
21
|
+
)
|
22
|
+
UPLOAD_SLICED_TABLE_RETRY_STOP_AFTER_DELAY = env_integer(
|
23
|
+
"UPLOAD_SLICED_TABLE_RETRY_STOP_AFTER_DELAY", 30 * 60
|
24
|
+
)
|
25
|
+
DOWNLOAD_MANIFEST_ENTRY_RETRY_STOP_AFTER_DELAY = env_integer(
|
26
|
+
"DOWNLOAD_MANIFEST_ENTRY_RETRY_STOP_AFTER_DELAY", 30 * 60
|
27
|
+
)
|
deltacat/aws/s3u.py
CHANGED
@@ -4,7 +4,15 @@ from functools import partial
|
|
4
4
|
from typing import Any, Callable, Dict, Generator, List, Optional, Union
|
5
5
|
from uuid import uuid4
|
6
6
|
from botocore.config import Config
|
7
|
-
from deltacat.aws.constants import
|
7
|
+
from deltacat.aws.constants import (
|
8
|
+
BOTO_MAX_RETRIES,
|
9
|
+
UPLOAD_DOWNLOAD_RETRY_STOP_AFTER_DELAY,
|
10
|
+
BOTO_THROTTLING_ERROR_CODES,
|
11
|
+
RETRYABLE_TRANSIENT_ERRORS,
|
12
|
+
BOTO_TIMEOUT_ERROR_CODES,
|
13
|
+
UPLOAD_SLICED_TABLE_RETRY_STOP_AFTER_DELAY,
|
14
|
+
DOWNLOAD_MANIFEST_ENTRY_RETRY_STOP_AFTER_DELAY,
|
15
|
+
)
|
8
16
|
|
9
17
|
import pyarrow as pa
|
10
18
|
import ray
|
@@ -25,9 +33,6 @@ from tenacity import (
|
|
25
33
|
from deltacat.utils.ray_utils.concurrency import invoke_parallel
|
26
34
|
import deltacat.aws.clients as aws_utils
|
27
35
|
from deltacat import logs
|
28
|
-
from deltacat.aws.constants import (
|
29
|
-
TIMEOUT_ERROR_CODES,
|
30
|
-
)
|
31
36
|
from deltacat.exceptions import NonRetryableError, RetryableError
|
32
37
|
from deltacat.storage import (
|
33
38
|
DistributedDataset,
|
@@ -253,10 +258,21 @@ def read_file(
|
|
253
258
|
)
|
254
259
|
return table
|
255
260
|
except ClientError as e:
|
256
|
-
if
|
261
|
+
if (
|
262
|
+
e.response["Error"]["Code"]
|
263
|
+
in BOTO_TIMEOUT_ERROR_CODES | BOTO_THROTTLING_ERROR_CODES
|
264
|
+
):
|
257
265
|
# Timeout error not caught by botocore
|
258
|
-
raise RetryableError(
|
259
|
-
|
266
|
+
raise RetryableError(
|
267
|
+
f"Retry table download from: {s3_url} after receiving {type(e).__name__}"
|
268
|
+
) from e
|
269
|
+
raise NonRetryableError(
|
270
|
+
f"Failed table download from: {s3_url} after receiving {type(e).__name__}"
|
271
|
+
) from e
|
272
|
+
except RETRYABLE_TRANSIENT_ERRORS as e:
|
273
|
+
raise RetryableError(
|
274
|
+
f"Retry upload for: {s3_url} after receiving {type(e).__name__}"
|
275
|
+
) from e
|
260
276
|
except BaseException as e:
|
261
277
|
logger.warn(
|
262
278
|
f"Read has failed for {s3_url} and content_type={content_type} "
|
@@ -281,7 +297,7 @@ def upload_sliced_table(
|
|
281
297
|
# @retry decorator can't be pickled by Ray, so wrap upload in Retrying
|
282
298
|
retrying = Retrying(
|
283
299
|
wait=wait_random_exponential(multiplier=1, max=60),
|
284
|
-
stop=stop_after_delay(
|
300
|
+
stop=stop_after_delay(UPLOAD_SLICED_TABLE_RETRY_STOP_AFTER_DELAY),
|
285
301
|
retry=retry_if_exception_type(RetryableError),
|
286
302
|
)
|
287
303
|
|
@@ -315,7 +331,6 @@ def upload_sliced_table(
|
|
315
331
|
**s3_client_kwargs,
|
316
332
|
)
|
317
333
|
manifest_entries.extend(slice_entries)
|
318
|
-
|
319
334
|
return manifest_entries
|
320
335
|
|
321
336
|
|
@@ -363,8 +378,23 @@ def upload_table(
|
|
363
378
|
except ClientError as e:
|
364
379
|
if e.response["Error"]["Code"] == "NoSuchKey":
|
365
380
|
# s3fs may swallow S3 errors - we were probably throttled
|
366
|
-
raise RetryableError(
|
367
|
-
|
381
|
+
raise RetryableError(
|
382
|
+
f"Retry table download from: {s3_url} after receiving {type(e).__name__}"
|
383
|
+
) from e
|
384
|
+
if (
|
385
|
+
e.response["Error"]["Code"]
|
386
|
+
in BOTO_TIMEOUT_ERROR_CODES | BOTO_THROTTLING_ERROR_CODES
|
387
|
+
):
|
388
|
+
raise RetryableError(
|
389
|
+
f"Retry table download from: {s3_url} after receiving {type(e).__name__}"
|
390
|
+
) from e
|
391
|
+
raise NonRetryableError(
|
392
|
+
f"Failed table upload to: {s3_url} after receiving {type(e).__name__}"
|
393
|
+
) from e
|
394
|
+
except RETRYABLE_TRANSIENT_ERRORS as e:
|
395
|
+
raise RetryableError(
|
396
|
+
f"Retry upload for: {s3_url} after receiving {type(e).__name__}"
|
397
|
+
) from e
|
368
398
|
except BaseException as e:
|
369
399
|
logger.warn(
|
370
400
|
f"Upload has failed for {s3_url} and content_type={content_type}. Error: {e}",
|
@@ -412,7 +442,7 @@ def download_manifest_entry(
|
|
412
442
|
# @retry decorator can't be pickled by Ray, so wrap download in Retrying
|
413
443
|
retrying = Retrying(
|
414
444
|
wait=wait_random_exponential(multiplier=1, max=60),
|
415
|
-
stop=stop_after_delay(
|
445
|
+
stop=stop_after_delay(DOWNLOAD_MANIFEST_ENTRY_RETRY_STOP_AFTER_DELAY),
|
416
446
|
retry=retry_if_not_exception_type(NonRetryableError),
|
417
447
|
)
|
418
448
|
table = retrying(
|
@@ -504,41 +534,85 @@ def download_manifest_entries_distributed(
|
|
504
534
|
|
505
535
|
def upload(s3_url: str, body, **s3_client_kwargs) -> Dict[str, Any]:
|
506
536
|
|
507
|
-
# TODO (pdames): add tenacity retrying
|
508
537
|
parsed_s3_url = parse_s3_url(s3_url)
|
509
538
|
s3 = s3_client_cache(None, **s3_client_kwargs)
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
539
|
+
retrying = Retrying(
|
540
|
+
wait=wait_random_exponential(multiplier=1, max=15),
|
541
|
+
stop=stop_after_delay(UPLOAD_DOWNLOAD_RETRY_STOP_AFTER_DELAY),
|
542
|
+
retry=retry_if_exception_type(RetryableError),
|
543
|
+
)
|
544
|
+
return retrying(
|
545
|
+
_put_object,
|
546
|
+
s3,
|
547
|
+
body,
|
548
|
+
parsed_s3_url.bucket,
|
549
|
+
parsed_s3_url.key,
|
514
550
|
)
|
515
551
|
|
516
552
|
|
553
|
+
def _put_object(
|
554
|
+
s3_client, body: Any, bucket: str, key: str, **s3_put_object_kwargs
|
555
|
+
) -> Dict[str, Any]:
|
556
|
+
try:
|
557
|
+
return s3_client.put_object(
|
558
|
+
Body=body, Bucket=bucket, Key=key, **s3_put_object_kwargs
|
559
|
+
)
|
560
|
+
except ClientError as e:
|
561
|
+
if e.response["Error"]["Code"] in BOTO_THROTTLING_ERROR_CODES:
|
562
|
+
raise RetryableError(
|
563
|
+
f"Retry upload for: {bucket}/{key} after receiving {e.response['Error']['Code']}"
|
564
|
+
) from e
|
565
|
+
raise NonRetryableError(f"Failed table upload to: {bucket}/{key}") from e
|
566
|
+
except RETRYABLE_TRANSIENT_ERRORS as e:
|
567
|
+
raise RetryableError(
|
568
|
+
f"Retry upload for: {bucket}/{key} after receiving {type(e).__name__}"
|
569
|
+
) from e
|
570
|
+
except BaseException as e:
|
571
|
+
logger.error(
|
572
|
+
f"Upload has failed for {bucket}/{key}. Error: {type(e).__name__}",
|
573
|
+
exc_info=True,
|
574
|
+
)
|
575
|
+
raise NonRetryableError(f"Failed table upload to: {bucket}/{key}") from e
|
576
|
+
|
577
|
+
|
517
578
|
def download(
|
518
579
|
s3_url: str, fail_if_not_found: bool = True, **s3_client_kwargs
|
519
580
|
) -> Optional[Dict[str, Any]]:
|
520
581
|
|
521
|
-
# TODO (pdames): add tenacity retrying
|
522
582
|
parsed_s3_url = parse_s3_url(s3_url)
|
523
583
|
s3 = s3_client_cache(None, **s3_client_kwargs)
|
584
|
+
retrying = Retrying(
|
585
|
+
wait=wait_random_exponential(multiplier=1, max=15),
|
586
|
+
stop=stop_after_delay(UPLOAD_DOWNLOAD_RETRY_STOP_AFTER_DELAY),
|
587
|
+
retry=retry_if_exception_type(RetryableError),
|
588
|
+
)
|
589
|
+
return retrying(
|
590
|
+
_get_object,
|
591
|
+
s3,
|
592
|
+
parsed_s3_url.bucket,
|
593
|
+
parsed_s3_url.key,
|
594
|
+
fail_if_not_found=fail_if_not_found,
|
595
|
+
)
|
596
|
+
|
597
|
+
|
598
|
+
def _get_object(s3_client, bucket: str, key: str, fail_if_not_found: bool = True):
|
524
599
|
try:
|
525
|
-
return
|
526
|
-
Bucket=
|
527
|
-
Key=
|
600
|
+
return s3_client.get_object(
|
601
|
+
Bucket=bucket,
|
602
|
+
Key=key,
|
528
603
|
)
|
529
604
|
except ClientError as e:
|
530
|
-
if
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
logger.info(f"file not found: {s3_url}")
|
605
|
+
if e.response["Error"]["Code"] == "NoSuchKey":
|
606
|
+
if fail_if_not_found:
|
607
|
+
raise NonRetryableError(
|
608
|
+
f"Failed get object from: {bucket}/{key}"
|
609
|
+
) from e
|
610
|
+
logger.info(f"file not found: {bucket}/{key}")
|
611
|
+
except RETRYABLE_TRANSIENT_ERRORS as e:
|
612
|
+
raise RetryableError(
|
613
|
+
f"Retry get object: {bucket}/{key} after receiving {type(e).__name__}"
|
614
|
+
) from e
|
615
|
+
|
542
616
|
return None
|
543
617
|
|
544
618
|
|
@@ -128,3 +128,7 @@ class RoundCompletionInfo(dict):
|
|
128
128
|
@property
|
129
129
|
def input_average_record_size_bytes(self) -> Optional[float]:
|
130
130
|
return self.get("inputAverageRecordSizeBytes")
|
131
|
+
|
132
|
+
@staticmethod
|
133
|
+
def get_audit_bucket_name_and_key(compaction_audit_url: str) -> Tuple[str, str]:
|
134
|
+
return compaction_audit_url.replace("s3://", "").split("/", 1)
|
@@ -24,6 +24,9 @@ from deltacat.compute.compactor import (
|
|
24
24
|
)
|
25
25
|
from deltacat.compute.compactor_v2.model.merge_result import MergeResult
|
26
26
|
from deltacat.compute.compactor_v2.model.hash_bucket_result import HashBucketResult
|
27
|
+
from deltacat.compute.compactor_v2.model.compaction_session import (
|
28
|
+
ExecutionCompactionResult,
|
29
|
+
)
|
27
30
|
from deltacat.compute.compactor.model.materialize_result import MaterializeResult
|
28
31
|
from deltacat.compute.compactor_v2.utils.merge import (
|
29
32
|
generate_local_merge_input,
|
@@ -41,8 +44,11 @@ from deltacat.compute.compactor_v2.deletes.utils import prepare_deletes
|
|
41
44
|
from deltacat.storage import (
|
42
45
|
Delta,
|
43
46
|
DeltaLocator,
|
47
|
+
DeltaType,
|
44
48
|
Manifest,
|
45
49
|
Partition,
|
50
|
+
Stream,
|
51
|
+
StreamLocator,
|
46
52
|
)
|
47
53
|
from deltacat.compute.compactor.model.compact_partition_params import (
|
48
54
|
CompactPartitionParams,
|
@@ -57,7 +63,7 @@ from deltacat.compute.compactor_v2.utils import io
|
|
57
63
|
from deltacat.compute.compactor.utils import round_completion_file as rcf
|
58
64
|
from deltacat.utils.metrics import metrics
|
59
65
|
|
60
|
-
from typing import List, Optional
|
66
|
+
from typing import List, Optional
|
61
67
|
from collections import defaultdict
|
62
68
|
from deltacat.compute.compactor.model.compaction_session_audit_info import (
|
63
69
|
CompactionSessionAuditInfo,
|
@@ -81,35 +87,52 @@ logger = logs.configure_deltacat_logger(logging.getLogger(__name__))
|
|
81
87
|
|
82
88
|
@metrics
|
83
89
|
def compact_partition(params: CompactPartitionParams, **kwargs) -> Optional[str]:
|
84
|
-
|
85
90
|
assert (
|
86
91
|
params.hash_bucket_count is not None and params.hash_bucket_count >= 1
|
87
92
|
), "hash_bucket_count is a required arg for compactor v2"
|
88
93
|
|
89
94
|
with memray.Tracker(
|
90
|
-
|
95
|
+
"compaction_partition.bin"
|
91
96
|
) if params.enable_profiler else nullcontext():
|
92
|
-
|
97
|
+
execute_compaction_result: ExecutionCompactionResult = _execute_compaction(
|
93
98
|
params,
|
94
99
|
**kwargs,
|
95
100
|
)
|
96
|
-
|
101
|
+
compaction_session_type: str = (
|
102
|
+
"INPLACE"
|
103
|
+
if execute_compaction_result.is_inplace_compacted
|
104
|
+
else "NON-INPLACE"
|
105
|
+
)
|
97
106
|
logger.info(
|
98
107
|
f"Partition-{params.source_partition_locator} -> "
|
99
|
-
f"Compaction session data processing completed"
|
108
|
+
f"{compaction_session_type} Compaction session data processing completed"
|
100
109
|
)
|
101
|
-
round_completion_file_s3_url = None
|
102
|
-
if
|
103
|
-
|
104
|
-
|
105
|
-
|
110
|
+
round_completion_file_s3_url: Optional[str] = None
|
111
|
+
if execute_compaction_result.new_compacted_partition:
|
112
|
+
previous_partition: Optional[Partition] = None
|
113
|
+
if execute_compaction_result.is_inplace_compacted:
|
114
|
+
previous_partition: Optional[
|
115
|
+
Partition
|
116
|
+
] = params.deltacat_storage.get_partition(
|
117
|
+
params.source_partition_locator.stream_locator,
|
118
|
+
params.source_partition_locator.partition_values,
|
119
|
+
**params.deltacat_storage_kwargs,
|
120
|
+
)
|
121
|
+
# NOTE: Retrieving the previous partition again as the partition_id may have changed by the time commit_partition is called.
|
122
|
+
logger.info(
|
123
|
+
f"Committing compacted partition to: {execute_compaction_result.new_compacted_partition.locator} "
|
124
|
+
f"using previous partition: {previous_partition.locator if previous_partition else None}"
|
106
125
|
)
|
107
|
-
|
108
|
-
|
126
|
+
commited_partition: Partition = params.deltacat_storage.commit_partition(
|
127
|
+
execute_compaction_result.new_compacted_partition,
|
128
|
+
previous_partition,
|
129
|
+
**params.deltacat_storage_kwargs,
|
130
|
+
)
|
131
|
+
logger.info(f"Committed compacted partition: {commited_partition}")
|
109
132
|
round_completion_file_s3_url = rcf.write_round_completion_file(
|
110
133
|
params.compaction_artifact_s3_bucket,
|
111
|
-
|
112
|
-
|
134
|
+
execute_compaction_result.new_round_completion_file_partition_locator,
|
135
|
+
execute_compaction_result.new_round_completion_info,
|
113
136
|
**params.s3_client_kwargs,
|
114
137
|
)
|
115
138
|
else:
|
@@ -123,7 +146,7 @@ def compact_partition(params: CompactPartitionParams, **kwargs) -> Optional[str]
|
|
123
146
|
|
124
147
|
def _execute_compaction(
|
125
148
|
params: CompactPartitionParams, **kwargs
|
126
|
-
) ->
|
149
|
+
) -> ExecutionCompactionResult:
|
127
150
|
|
128
151
|
rcf_source_partition_locator = (
|
129
152
|
params.rebase_source_partition_locator or params.source_partition_locator
|
@@ -142,7 +165,7 @@ def _execute_compaction(
|
|
142
165
|
|
143
166
|
compaction_start = time.monotonic()
|
144
167
|
|
145
|
-
task_max_parallelism = params.task_max_parallelism
|
168
|
+
task_max_parallelism: int = params.task_max_parallelism
|
146
169
|
|
147
170
|
if params.pg_config:
|
148
171
|
logger.info(
|
@@ -205,7 +228,7 @@ def _execute_compaction(
|
|
205
228
|
)
|
206
229
|
if not input_deltas:
|
207
230
|
logger.info("No input deltas found to compact.")
|
208
|
-
return None, None, None
|
231
|
+
return ExecutionCompactionResult(None, None, None, False)
|
209
232
|
|
210
233
|
delete_strategy: Optional[DeleteStrategy] = None
|
211
234
|
delete_file_envelopes: Optional[List[DeleteFileEnvelope]] = None
|
@@ -217,7 +240,7 @@ def _execute_compaction(
|
|
217
240
|
for delete_file_envelope in delete_file_envelopes:
|
218
241
|
delete_file_size_bytes += delete_file_envelope.table_size_bytes
|
219
242
|
logger.info(
|
220
|
-
f" Input deltas contain DELETE-type deltas. Total delete file size={delete_file_size_bytes}."
|
243
|
+
f" Input deltas contain {DeltaType.DELETE}-type deltas. Total delete file size={delete_file_size_bytes}."
|
221
244
|
f" Total length of delete file envelopes={len(delete_file_envelopes)}"
|
222
245
|
)
|
223
246
|
uniform_deltas: List[DeltaAnnotated] = io.create_uniform_input_deltas(
|
@@ -247,14 +270,16 @@ def _execute_compaction(
|
|
247
270
|
)
|
248
271
|
|
249
272
|
# create a new stream for this round
|
250
|
-
compacted_stream_locator
|
251
|
-
|
273
|
+
compacted_stream_locator: Optional[
|
274
|
+
StreamLocator
|
275
|
+
] = params.destination_partition_locator.stream_locator
|
276
|
+
compacted_stream: Stream = params.deltacat_storage.get_stream(
|
252
277
|
compacted_stream_locator.namespace,
|
253
278
|
compacted_stream_locator.table_name,
|
254
279
|
compacted_stream_locator.table_version,
|
255
280
|
**params.deltacat_storage_kwargs,
|
256
281
|
)
|
257
|
-
compacted_partition = params.deltacat_storage.stage_partition(
|
282
|
+
compacted_partition: Partition = params.deltacat_storage.stage_partition(
|
258
283
|
compacted_stream,
|
259
284
|
params.destination_partition_locator.partition_values,
|
260
285
|
**params.deltacat_storage_kwargs,
|
@@ -532,7 +557,7 @@ def _execute_compaction(
|
|
532
557
|
|
533
558
|
# Note: An appropriate last stream position must be set
|
534
559
|
# to avoid correctness issue.
|
535
|
-
merged_delta = Delta.merge_deltas(
|
560
|
+
merged_delta: Delta = Delta.merge_deltas(
|
536
561
|
deltas,
|
537
562
|
stream_position=params.last_stream_position_to_compact,
|
538
563
|
)
|
@@ -545,7 +570,7 @@ def _execute_compaction(
|
|
545
570
|
)
|
546
571
|
logger.info(record_info_msg)
|
547
572
|
|
548
|
-
compacted_delta = params.deltacat_storage.commit_delta(
|
573
|
+
compacted_delta: Delta = params.deltacat_storage.commit_delta(
|
549
574
|
merged_delta,
|
550
575
|
properties=kwargs.get("properties", {}),
|
551
576
|
**params.deltacat_storage_kwargs,
|
@@ -653,8 +678,9 @@ def _execute_compaction(
|
|
653
678
|
f"and rcf source partition_id of {rcf_source_partition_locator.partition_id}."
|
654
679
|
)
|
655
680
|
rcf_source_partition_locator = compacted_partition.locator
|
656
|
-
return (
|
681
|
+
return ExecutionCompactionResult(
|
657
682
|
compacted_partition,
|
658
683
|
new_round_completion_info,
|
659
684
|
rcf_source_partition_locator,
|
685
|
+
is_inplace_compacted,
|
660
686
|
)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
from deltacat.utils.common import env_integer
|
2
|
+
|
1
3
|
TOTAL_BYTES_IN_SHA1_HASH = 20
|
2
4
|
|
3
5
|
PK_DELIMITER = "L6kl7u5f"
|
@@ -41,6 +43,16 @@ DROP_DUPLICATES = True
|
|
41
43
|
# size in metadata to pyarrow table size.
|
42
44
|
PARQUET_TO_PYARROW_INFLATION = 4
|
43
45
|
|
46
|
+
# A merge task will fail after this timeout
|
47
|
+
# The default is currently double the observed maximum.
|
48
|
+
# This timeout depends on total data processed per task.
|
49
|
+
MERGE_TASK_TIMEOUT_IN_SECONDS = env_integer("MERGE_TASK_TIMEOUT_IN_SECONDS", 25 * 60)
|
50
|
+
|
51
|
+
# A hash bucket task will fail after this timeout
|
52
|
+
HASH_BUCKET_TASK_TIMEOUT_IN_SECONDS = env_integer(
|
53
|
+
"HASH_BUCKET_TASK_TIMEOUT_IN_SECONDS", 25 * 60
|
54
|
+
)
|
55
|
+
|
44
56
|
# Metric Names
|
45
57
|
# Time taken for a hash bucket task
|
46
58
|
HASH_BUCKET_TIME_IN_SECONDS = "hash_bucket_time"
|
@@ -0,0 +1,21 @@
|
|
1
|
+
from dataclasses import dataclass, fields
|
2
|
+
|
3
|
+
from deltacat.storage import (
|
4
|
+
Partition,
|
5
|
+
PartitionLocator,
|
6
|
+
)
|
7
|
+
from deltacat.compute.compactor import (
|
8
|
+
RoundCompletionInfo,
|
9
|
+
)
|
10
|
+
from typing import Optional
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass(frozen=True)
|
14
|
+
class ExecutionCompactionResult:
|
15
|
+
new_compacted_partition: Optional[Partition]
|
16
|
+
new_round_completion_info: Optional[RoundCompletionInfo]
|
17
|
+
new_round_completion_file_partition_locator: Optional[PartitionLocator]
|
18
|
+
is_inplace_compacted: bool
|
19
|
+
|
20
|
+
def __iter__(self):
|
21
|
+
return (getattr(self, field.name) for field in fields(self))
|
@@ -29,12 +29,14 @@ from deltacat.utils.metrics import emit_timer_metrics, failure_metric, success_m
|
|
29
29
|
from deltacat.utils.resources import (
|
30
30
|
get_current_process_peak_memory_usage_in_bytes,
|
31
31
|
ProcessUtilizationOverTimeRange,
|
32
|
+
timeout,
|
32
33
|
)
|
33
34
|
from deltacat.constants import BYTES_PER_GIBIBYTE
|
34
35
|
from deltacat.compute.compactor_v2.constants import (
|
35
36
|
HASH_BUCKET_TIME_IN_SECONDS,
|
36
37
|
HASH_BUCKET_FAILURE_COUNT,
|
37
38
|
HASH_BUCKET_SUCCESS_COUNT,
|
39
|
+
HASH_BUCKET_TASK_TIMEOUT_IN_SECONDS,
|
38
40
|
)
|
39
41
|
|
40
42
|
if importlib.util.find_spec("memray"):
|
@@ -96,8 +98,12 @@ def _group_file_records_by_pk_hash_bucket(
|
|
96
98
|
return hb_to_delta_file_envelopes, total_record_count, total_size_bytes
|
97
99
|
|
98
100
|
|
101
|
+
# TODO: use timeout parameter in ray.remote
|
102
|
+
# https://github.com/ray-project/ray/issues/18916
|
103
|
+
# Note: order of decorators is important
|
99
104
|
@success_metric(name=HASH_BUCKET_SUCCESS_COUNT)
|
100
105
|
@failure_metric(name=HASH_BUCKET_FAILURE_COUNT)
|
106
|
+
@timeout(HASH_BUCKET_TASK_TIMEOUT_IN_SECONDS)
|
101
107
|
def _timed_hash_bucket(input: HashBucketInput):
|
102
108
|
task_id = get_current_ray_task_id()
|
103
109
|
worker_id = get_current_ray_worker_id()
|
@@ -28,6 +28,7 @@ from deltacat.utils.metrics import emit_timer_metrics, failure_metric, success_m
|
|
28
28
|
from deltacat.utils.resources import (
|
29
29
|
get_current_process_peak_memory_usage_in_bytes,
|
30
30
|
ProcessUtilizationOverTimeRange,
|
31
|
+
timeout,
|
31
32
|
)
|
32
33
|
from deltacat.compute.compactor_v2.utils.primary_key_index import (
|
33
34
|
generate_pk_hash_column,
|
@@ -46,6 +47,7 @@ from deltacat.compute.compactor_v2.constants import (
|
|
46
47
|
MERGE_TIME_IN_SECONDS,
|
47
48
|
MERGE_SUCCESS_COUNT,
|
48
49
|
MERGE_FAILURE_COUNT,
|
50
|
+
MERGE_TASK_TIMEOUT_IN_SECONDS,
|
49
51
|
)
|
50
52
|
|
51
53
|
|
@@ -484,8 +486,12 @@ def _copy_manifests_from_hash_bucketing(
|
|
484
486
|
return materialized_results
|
485
487
|
|
486
488
|
|
489
|
+
# TODO: use timeout parameter in ray.remote
|
490
|
+
# https://github.com/ray-project/ray/issues/18916
|
491
|
+
# Note: order of decorators is important
|
487
492
|
@success_metric(name=MERGE_SUCCESS_COUNT)
|
488
493
|
@failure_metric(name=MERGE_FAILURE_COUNT)
|
494
|
+
@timeout(MERGE_TASK_TIMEOUT_IN_SECONDS)
|
489
495
|
def _timed_merge(input: MergeInput) -> MergeResult:
|
490
496
|
task_id = get_current_ray_task_id()
|
491
497
|
worker_id = get_current_ray_worker_id()
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import botocore
|
2
2
|
import logging
|
3
|
+
import tenacity
|
3
4
|
from typing import Dict, Optional, List, Tuple, Any
|
4
5
|
from deltacat import logs
|
5
6
|
from deltacat.compute.compactor_v2.model.merge_file_group import (
|
@@ -20,7 +21,6 @@ from deltacat.compute.compactor_v2.utils.primary_key_index import (
|
|
20
21
|
from deltacat.compute.compactor_v2.constants import (
|
21
22
|
PARQUET_TO_PYARROW_INFLATION,
|
22
23
|
)
|
23
|
-
|
24
24
|
from daft.exceptions import DaftTransientError
|
25
25
|
|
26
26
|
|
@@ -65,7 +65,12 @@ def get_task_options(
|
|
65
65
|
cpu: float, memory: float, ray_custom_resources: Optional[Dict] = None
|
66
66
|
) -> Dict:
|
67
67
|
|
68
|
-
|
68
|
+
# NOTE: With DEFAULT scheduling strategy in Ray 2.20.0, autoscaler does
|
69
|
+
# not spin up enough nodes fast and hence we see only approximately
|
70
|
+
# 20 tasks get scheduled out of 100 tasks in queue. Hence, we use SPREAD
|
71
|
+
# which is also ideal for merge and hash bucket tasks.
|
72
|
+
# https://docs.ray.io/en/latest/ray-core/scheduling/index.html
|
73
|
+
task_opts = {"num_cpus": cpu, "memory": memory, "scheduling_strategy": "SPREAD"}
|
69
74
|
|
70
75
|
if ray_custom_resources:
|
71
76
|
task_opts["resources"] = ray_custom_resources
|
@@ -80,6 +85,7 @@ def get_task_options(
|
|
80
85
|
ConnectionError,
|
81
86
|
TimeoutError,
|
82
87
|
DaftTransientError,
|
88
|
+
tenacity.RetryError,
|
83
89
|
]
|
84
90
|
|
85
91
|
return task_opts
|
deltacat/storage/interface.py
CHANGED
@@ -414,11 +414,18 @@ def stage_partition(
|
|
414
414
|
raise NotImplementedError("stage_partition not implemented")
|
415
415
|
|
416
416
|
|
417
|
-
def commit_partition(
|
417
|
+
def commit_partition(
|
418
|
+
partition: Partition,
|
419
|
+
previous_partition: Optional[Partition] = None,
|
420
|
+
*args,
|
421
|
+
**kwargs
|
422
|
+
) -> Partition:
|
418
423
|
"""
|
419
424
|
Commits the given partition to its associated table version stream,
|
420
|
-
replacing any previous partition registered for the same stream and
|
421
|
-
partition values.
|
425
|
+
replacing any previous partition (i.e., "partition being replaced") registered for the same stream and
|
426
|
+
partition values.
|
427
|
+
If the previous_partition is passed as an argument, the specified previous_partition will be the partition being replaced, otherwise it will be retrieved.
|
428
|
+
Returns the registered partition. If the partition's
|
422
429
|
previous delta stream position is specified, then the commit will
|
423
430
|
be rejected if it does not match the actual previous stream position of
|
424
431
|
the partition being replaced. If the partition's previous partition ID is
|