"""Implementation of upload to DynamoDB.""" import concurrent.futures # noqa import json import time import boto3 import smart_open from dim_refresh_etl.flows.dynamo_sync import config from dim_refresh_etl.flows.dynamo_sync.dynamo_upload import upload_config from dim_refresh_etl.flows.dynamo_sync.dynamo_upload import upload_utils def start_upload(model_type, s3_unload_link): """Run upload to DynamoDB. The main entry point for data uploading. Args: model_type (str): Data model type. s3_unload_link (str): Path on S3 in the following format: s3://bucket-name/target/path/ Raises: Exception: If any error appeared during upload process it will be reraised explicitly in this function. """ processes_number = upload_config.model_type_settings[ model_type]['processes_number'] s3_key_groups = _get_s3_keys_for_upload( s3_unload_link, keys_group_size=processes_number) with concurrent.futures.ProcessPoolExecutor( max_workers=processes_number) as mp_executor: futures = [ mp_executor.submit(_run_upload, s3_keys, model_type) for s3_keys in s3_key_groups] results = [ future.result() for future in concurrent.futures.as_completed(futures)] exception = upload_utils.find_exception(results) if exception: raise exception def _run_upload(s3_keys, model_type): try: threads_number = upload_config.model_type_settings[ model_type]['threads_number'] with concurrent.futures.ThreadPoolExecutor( max_workers=threads_number) as executor: futures = [ executor.submit( _try_put_in_dynamo, model_type, s3_path) for s3_path in s3_keys] results = [ future.result() for future in concurrent.futures.as_completed(futures)] exception = upload_utils.find_exception(results) if exception: raise exception except Exception as e: return e def _try_put_in_dynamo(model_type, s3_path): """Try to upload data to DynamoDB. The function runs the process of uploading of certain S3 key to DynamoDB. If any error occurs during the process it will be caught and the function will run the process one more time after some time pause. If after several attempts the S3 key still isn't fully uploaded than the function returns the last occurred error (Since the code is run in the separate thread we don't raise errors explicitly). """ attempts_left = 360 retries_interval = 10 exception = None while attempts_left: try: _put_in_dynamo(model_type, s3_path) break except Exception as e: exception = e attempts_left -= 1 print( 'An error occurred during upload of {} key ' 'to the DynamoDB table. Next try will be started in {} ' 'seconds. Attempts left {}. \n' 'Error: {}'.format( s3_path, retries_interval, attempts_left, exception)) time.sleep(retries_interval) if not attempts_left: return exception def _put_in_dynamo(model_type, s3_path): """Put all rows from the S3 key to the DynamoDB table.""" processed_rows = 0 rows = _get_rows(s3_path) table = _get_dynamodb_table() with table.batch_writer(overwrite_by_pkeys=[ 'first_id', 'second_id']) as batch: for row in rows: item = json.loads(row) batch.put_item(Item=item) processed_rows += 1 if processed_rows % 1000 == 0: print('{} rows of {} were processed'.format( processed_rows, s3_path)) upload_utils.delete_s3_key(s3_path) def _get_dynamodb_table(): """Get DynamoDB table for upload.""" dynamodb_resource = boto3.resource('dynamodb') return dynamodb_resource.Table(config.analytics_metadata_table) def _get_rows(s3_path): with smart_open.smart_open(s3_path, 'r') as f: for row in f: yield row def _get_s3_keys_for_upload(s3_unload_link, keys_group_size): bucket, path = upload_utils.bucket_and_path_from_s3_list(s3_unload_link) uploaded_s3_keys = upload_utils.keys_in_s3_location(bucket, path) return upload_utils.cut_list(uploaded_s3_keys, keys_group_size)