"""Lambda function to reprocess Vector orders. This function looks up errors for specified Vector order IDs in the dedicated RDS Aurora DB and triggers orders stream reprocessing by updating DynamoDB records. It is expected to be run from the (non-scheduled) Jenkins job or the AWS console manually. """ import boto3 from boto3.dynamodb import conditions from botocore import config as botocore_config from botocore import exceptions as botocore_exceptions import sqlalchemy import common_config from constants import common_const from constants import common_fields from constants import fields from constants import sql from connectors import sentry from connectors import vector_obtain_orders import config import dynamodb_config import exceptions def get_error_records(order_ids): """Get records from the errors table. Args: order_ids (iterable): An iterable of order IDs. Returns: iterable: Error records in the following format: [ { 'vector_order_id': 1234, 'last_updated_at': datetime.datetime(2017, 9, 23, 0, 0), 'error_timestamp': datetime.datetime(2017, 9, 22, 0, 0), }, ... ] """ common_config.logger.debug('Reading error records.') with vector_obtain_orders.session_scope() as session: # execute query and map to OrderDetails results = session.execute( sqlalchemy.text(sql.SELECT_VECTOR_ORDER_ERRORS), { 'order_ids': tuple(order_ids), } ) return (dict(result) for result in results) def trigger_orders_reprocessing(error_records): """Update Vector orders in DynamoDB to trigger their reprocessing. Args: error_records (iterable): Error records iterable in the format returned by the get_error_records(). Returns: set: Reprocessed order IDs. """ common_config.logger.info('Reprocessing orders.') reprocessed_order_ids = set() # Process records by iterating only once, since it can be a generator. for record in error_records: common_config.logger.debug( 'Reprocessing order for error record: %s', record) # DynamoDB should be configured with StreamViewType = NEW_IMAGE. error_timestamp = record[fields.VOO_ERROR_TIMESTAMP].strftime( common_const.JSON_DATETIME_FORMAT) vector_order_id = record[fields.VOO_ORDER_ID] error_record_id = record[fields.VOO_PK] try: update_order_error_timestamp( vector_order_id=vector_order_id, timestamp=error_timestamp) resolve_error(error_record_id) reprocessed_order_ids.add(vector_order_id) except Exception as e: sentry.sentry_client.captureMessage( 'Error while reprocessing error record. ' 'Error: {}, record: {}'.format(e, record), extra=record, stack=True) return reprocessed_order_ids def update_order_error_timestamp(*, vector_order_id, timestamp): """Update Vector order in DynamoDB to trigger it's reprocessing. Args: vector_order_id (str): Vector order ID in DynamoDB table. timestamp (str): datetime value for the error timestamp field. """ dynamodb = get_dynamodb() table = dynamodb.Table(dynamodb_config.ORDERS_DDB_TABLE) common_config.logger.debug('Updating order %s', vector_order_id) try: table.update_item( Key={common_fields.VO_ORDER_ID: vector_order_id}, ConditionExpression=conditions.Key( common_fields.VO_ORDER_ID).eq(vector_order_id), ReturnValues='UPDATED_NEW', UpdateExpression='SET {timestamp_field} = :timestamp'.format( timestamp_field=common_fields.VO_ERROR_TIMESTAMP), ExpressionAttributeValues={':timestamp': timestamp}) except botocore_exceptions.ClientError as e: err_code = e.response.get('Error', {}).get('Code') # If this is an error we do not expect - raise it. if err_code != 'ConditionalCheckFailedException': raise # If order does not exist - just log this fact and proceed. # This should not happen in production, as we never delete created # orders. common_config.logger.warning( 'Record \'%s\' is not found in DynamoDB', vector_order_id) def resolve_error(error_record_id): """Resolve error record in the errors table. Args: error_record_id (int): An error record ID. """ common_config.logger.debug('Resolving error record %d.', error_record_id) with vector_obtain_orders.session_scope() as session: result = session.execute( sqlalchemy.text(sql.UPDATE_VECTOR_ORDER_SYNC_ERRORS_SET_RESOLVED), {'error_record_id': error_record_id}) # Note: this is not the actual number of affected rows. common_config.logger.debug( 'Number of rows matching the UPDATE condition: %d', result.rowcount) @exceptions.sentry_capture_exception def handler(event, context): """Reprocess Vector orders. - Read errors for specified order IDs. - Trigger reprocessing if 'reprocess' flag is True. - Resolve error records. """ order_ids = (str(order_id) for order_id in event.get('order_ids', [])) do_reprocess = event.get('do_reprocess', False) error_records = get_error_records(order_ids) if do_reprocess: # Trigger reprocessing & resolve reprocessed records. reprocessed_order_ids = trigger_orders_reprocessing(error_records) common_config.logger.info( 'Reprocessed order_ids: %s', ','.join(reprocessed_order_ids)) common_config.logger.info( 'Not reprocessed order_ids: %s', ','.join(set(order_ids) - reprocessed_order_ids)) else: # Just resolve the all the selected errors. for err in error_records: resolve_error(err[fields.VOO_PK]) # TODO: This is also present in another lambda. Move to common/connectors. def get_dynamodb(): """Get Boto DynamoDB resource. Returns: dynamodb.ServiceResource: DynamodDB instance. """ config_instance = botocore_config.Config( retries={'max_attempts': config.DDB_WRITE_MAX_ATTEMPTS}) return boto3.resource('dynamodb', config=config_instance)