def enqueue_cubes(queue_arn, cubes):
    """Multiprocessing.Pool worker function for enqueuing a number of messages

    Called by populate_cubes()

    Args:
        queue_arn (str): The target SQS queue URL
        cubes (list[XYZ]): A list of XYZ cubes to enqueue
    """
    try:
        sqs = aws.get_session().resource('sqs')
        queue = sqs.Queue(queue_arn)
        count = 0

        msgs = ({
            'Id': str(id(cube)),
            'MessageBody': json.dumps(cube)
        } for cube in cubes)

        for batch in chunk(msgs, 10):  # 10 is the message batch limit for SQS
            count += 1
            if count % 500 == 0:
                log.debug("Enqueued {} cubes".format(count * 10))

            queue.send_messages(Entries=batch)

    except Exception as ex:
        log.exception("Error caught in process, raising to controller")
        raise ResolutionHierarchyError(str(ex))
예제 #2
0
def verify_count(args):
    """Verify that the number of messages in a queue is the given number

    Args:
        args: {
            'arn': ARN,
            'count': 0,
        }

    Returns:
        int: The total number of messages in the queue

    Raises:
        Error: If the count doesn't match the messages in the queue
    """

    session = aws.get_session()
    client = session.client('sqs')
    resp = client.get_queue_attributes(QueueUrl = args['arn'],
                                       AttributeNames = ['ApproximateNumberOfMessages'])
    messages = int(resp['Attributes']['ApproximateNumberOfMessages'])

    if messages != args['count']:
        raise Exception('Counts do not match')

    return args['count']
def invoke_lambdas(count, lambda_arn, lambda_args, dlq_arn):
    """Multiprocessing.Pool worker function for invoking a number of lambdas

    Called by launch_lambdas()

    The dlq_arn queue is only checked every 10 lambdas launched. This is so
    that the queue is not hit too hard when invoking a large number of lambdas
    via Multiprocessing.Pool.

    Args:
        count (int): The number of lambdas to launch
        lambda_arn (str): Name or ARN of the lambda function to invoke
        lambda_args (str): The lambda payload to pass when invoking
        dlq_arn (str): ARN of the SQS DLQ to monitor for error messages
    """
    try:
        lambda_ = aws.get_session().client('lambda')

        log.info("Launching {} lambdas".format(count))

        for i in range(1, count+1):
            if i % 500 == 0:
                log.debug("Launched {} lambdas".format(i))
            if i % 10 == 0:
                if check_queue(dlq_arn) > 0:
                    raise FailedLambdaError()

            lambda_.invoke(FunctionName = lambda_arn,
                           InvocationType = 'Event', # Async execution
                           Payload = lambda_args)
    except Exception as ex:
        log.exception("Error caught in process, raising to controller")
        raise ResolutionHierarchyError(str(ex))
def invoke_lambdas(count, lambda_arn, lambda_args, dlq_arn):
    """Multiprocessing.Pool worker function for invoking a number of lambdas

    Called by launch_lambdas()

    The dlq_arn queue is only checked every 10 lambdas launched. This is so
    that the queue is not hit too hard when invoking a large number of lambdas
    via Multiprocessing.Pool.

    Args:
        count (int): The number of lambdas to launch
        lambda_arn (str): Name or ARN of the lambda function to invoke
        lambda_args (str): The lambda payload to pass when invoking
        dlq_arn (str): ARN of the SQS DLQ to monitor for error messages
    """
    try:
        lambda_ = aws.get_session().client('lambda')

        log.info("Launching {} lambdas".format(count))

        for i in range(1, count + 1):
            if i % 500 == 0:
                log.debug("Launched {} lambdas".format(i))
            if i % 10 == 0:
                if check_queue(dlq_arn) > 0:
                    raise FailedLambdaError()

            lambda_.invoke(
                FunctionName=lambda_arn,
                InvocationType='Event',  # Async execution
                Payload=lambda_args)
    except Exception as ex:
        log.exception("Error caught in process, raising to controller")
        raise ResolutionHierarchyError(str(ex))
def verify_count(args):
    """Verify that the number of messages in a queue is at least the given number

    Args:
        args: {
            'arn': ARN,
            'count': 0,
        }

    Returns:
        int: The total number of messages in the queue

    Raises:
        Error: If the messages in the queue is less then the count
    """

    session = aws.get_session()
    client = session.client('sqs')
    resp = client.get_queue_attributes(QueueUrl = args['arn'],
                                       AttributeNames = ['ApproximateNumberOfMessages'])
    messages = int(resp['Attributes']['ApproximateNumberOfMessages'])

    if messages > args['count']:
      log.debug("More SQS messages then expected: required tiles {}, actual messages {}".format(args['count'],
                                                                                                messages))
    elif messages < args['count']:
        raise Exception('Not enough messages in queue')

    return messages
def verify_count(args):
    """Verify that the number of messages in a queue is at least the given number

    Args:
        args: {
            'arn': ARN,
            'count': 0,
        }

    Returns:
        int: The total number of messages in the queue

    Raises:
        Error: If the messages in the queue is less then the count
    """

    session = aws.get_session()
    client = session.client('sqs')
    resp = client.get_queue_attributes(QueueUrl = args['arn'],
                                       AttributeNames = ['ApproximateNumberOfMessages'])
    messages = int(resp['Attributes']['ApproximateNumberOfMessages'])

    if messages > args['count']:
      log.debug("More SQS messages then expected: required tiles {}, actual messages {}".format(args['count'],
                                                                                                messages))
    elif messages < args['count']:
        raise Exception('Not enough messages in queue')

    return messages
def enqueue_cubes(queue_arn, cubes):
    """Multiprocessing.Pool worker function for enqueuing a number of messages

    Called by populate_cubes()

    Args:
        queue_arn (str): The target SQS queue URL
        cubes (list[XYZ]): A list of XYZ cubes to enqueue
    """
    try:
        sqs = aws.get_session().resource('sqs')
        queue = sqs.Queue(queue_arn)
        count = 0

        msgs = ({'Id': str(id(cube)),
                 'MessageBody': json.dumps(cube)}
                for cube in cubes)

        for batch in chunk(msgs, 10): # 10 is the message batch limit for SQS
            count += 1
            if count % 500 == 0:
                log.debug ("Enqueued {} cubes".format(count * 10))

            queue.send_messages(Entries=batch)

    except Exception as ex:
        log.exception("Error caught in process, raising to controller")
        raise ResolutionHierarchyError(str(ex))
def update_visibility_timeout(queue_url, receipt_handle):
    """
    Update the visibility timeout of the message for the current downsample job.

    This keeps the message from getting redelivered while the downsample is
    still running.

    Args:
        queue_url (str): URL of SQS queue.
        receipt_handle (str): Message's receipt handle.

    Returns:

    Raises:
    """
    try:
        session = aws.get_session()
        sqs = session.client('sqs')
        sqs.change_message_visibility(
            QueueUrl=queue_url,
            ReceiptHandle=receipt_handle,
            VisibilityTimeout=NEW_VISIBILITY_TIMEOUT.seconds)
    except Exception as ex:
        log.exception(
            f'Error trying to update visibilty timeout of downsample job: {ex}'
        )
        raise
예제 #9
0
def ingest_populate(args):
    """Populate the ingest upload SQS Queue with tile information

    Note: This activity will clear the upload queue of any existing
          messages

    Args:
        args: {
            'upload_sfn': ARN,

            'job_id': '',
            'upload_queue': ARN,
            'ingest_queue': ARN,

            'resolution': 0,
            'project_info': [col_id, exp_id, ch_id],

            't_start': 0,
            't_stop': 0,
            't_tile_size': 0,

            'x_start': 0,
            'x_stop': 0,
            'x_tile_size': 0,

            'y_start': 0,
            'y_stop': 0
            'y_tile_size': 0,

            'z_start': 0,
            'z_stop': 0
            'z_tile_size': 16,
        }

    Returns:
        {'arn': Upload queue ARN,
         'count': Number of messages put into the queue}
    """
    log.debug("Starting to populate upload queue")

    clear_queue(args['upload_queue'])

    results = fanout(aws.get_session(),
                     args['upload_sfn'],
                     split_args(args),
                     max_concurrent=MAX_NUM_PROCESSES,
                     rampup_delay=RAMPUP_DELAY,
                     rampup_backoff=RAMPUP_BACKOFF,
                     poll_delay=POLL_DELAY,
                     status_delay=STATUS_DELAY)

    total_sent = reduce(lambda x, y: x + y, results, 0)

    return {
        'arn': args['upload_queue'],
        'count': total_sent,
    }
def clear_queue(arn):
    """Delete any existing messages in the given SQS queue

    Args:
        arn (string): SQS ARN of the queue to empty
    """
    log.debug("Clearing queue {}".format(arn))
    session = aws.get_session()
    client = session.client('sqs')
    client.purge_queue(QueueUrl = arn)
    time.sleep(60)
예제 #11
0
def clear_queue(arn):
    """Delete any existing messages in the given SQS queue

    Args:
        arn (string): SQS ARN of the queue to empty
    """
    log.debug("Clearing queue {}".format(arn))
    session = aws.get_session()
    client = session.client('sqs')
    client.purge_queue(QueueUrl = arn)
    time.sleep(60)
예제 #12
0
def delete_queue(queue_arn):
    """Delete a SQS queue

    Args:
        queue_arn (str): The URL of the queue
    """
    session = aws.get_session()
    sqs = session.client('sqs')

    try:
        resp = sqs.delete_queue(QueueUrl = queue_arn)
    except:
        log.exception("Could not delete status queue '{}'".format(queue_arn))
예제 #13
0
def delete_queue(queue_arn):
    """Delete a SQS queue

    Args:
        queue_arn (str): The URL of the queue
    """
    session = aws.get_session()
    sqs = session.client('sqs')

    try:
        resp = sqs.delete_queue(QueueUrl=queue_arn)
    except:
        log.exception("Could not delete status queue '{}'".format(queue_arn))
예제 #14
0
def create_queue(queue_name):
    """Create a SQS queue

    Args:
        queue_name (str): Name of the queue

    Return:
        str: URL of the queue
    """
    session = aws.get_session()
    sqs = session.client('sqs')

    resp = sqs.create_queue(QueueName=queue_name)

    url = resp['QueueUrl']
    return url
예제 #15
0
def create_queue(queue_name):
    """Create a SQS queue

    Args:
        queue_name (str): Name of the queue

    Return:
        str: URL of the queue
    """
    session = aws.get_session()
    sqs = session.client('sqs')

    resp = sqs.create_queue(QueueName = queue_name)

    url = resp['QueueUrl']
    return url
예제 #16
0
def check_queue(queue_arn):
    """Get the count of messages in the given queue

    The count is a combination Approximate Number of Messages, Messages Delayed,
    and Messages Not Visible.

    If the queue_arn contains 'dlq' the first message will be read and the SNS
    DLQ message decoded to print the error that caused the DLQ message.

    Args:
        queue_arn (str): The URL of the queue

    Return:
        int: The count of messages or zero if the queue could not be queried
    """
    session = aws.get_session()
    sqs = session.client('sqs')

    try:
        resp = sqs.get_queue_attributes(
            QueueUrl=queue_arn,
            AttributeNames=[
                'ApproximateNumberOfMessages',
                'ApproximateNumberOfMessagesDelayed',
                'ApproximateNumberOfMessagesNotVisible'
            ])
    except:
        log.exception(
            "Could not get message count for queue '{}'".format(queue_arn))
        return 0
    else:
        # Include both the number of messages and the number of in-flight messages
        message_count = int(resp['Attributes']['ApproximateNumberOfMessages']) + \
                        int(resp['Attributes']['ApproximateNumberOfMessagesDelayed']) + \
                        int(resp['Attributes']['ApproximateNumberOfMessagesNotVisible'])
        if message_count > 0 and 'dlq' in queue_arn:
            try:
                resp = sqs.receive_message(QueueUrl=queue_arn)
                for msg in resp['Messages']:
                    body = json.loads(msg['Body'])
                    error = body['Records'][0]['Sns']['MessageAttributes'][
                        'ErrorMessage']['Value']
                    log.debug("DLQ Error: {}".format(error))
            except Exception as err:
                log.exception(
                    "Problem getting DLQ error message: {}".format(err))
        return message_count
예제 #17
0
def check_downsample_queue(args):
    """
    Check queue for downsample jobs.

    Also marks downsample as in progress if message found.

    Args:
        (dict): { 'queue_url': <URL of SQS queue>, 'sfn_arn': <arn of the downsample step fcn> }

    Returns:
        (dict): {
            'start_downsample': True | False,
            'queue_url': <URL of SQS queue>,
            'sfn_arn': <arn of the downsample step fcn>,
            'status': 'IN_PROGRESS',
            'db_host': <host name of database>
            'channel_id': <id of channel for downsample>
            ... }
            if start_downsample, then args for downsample_channel() provided including
            job_receipt_handle so message's visibility timeout can be
            adjusted or be deleted from the queue.  The message's contents are
            provided in 'msg', if one is available.
    """
    session = aws.get_session()
    sqs = session.client('sqs')
    resp = sqs.receive_message(QueueUrl=args['queue_url'],
                               WaitTimeSeconds=2,
                               MaxNumberOfMessages=1)
    if 'Messages' not in resp or len(resp['Messages']) == 0:
        return {'start_downsample': False}

    msg = resp['Messages'][0]
    job = json.loads(msg['Body'])
    output = {
        'start_downsample': True,
        'job_receipt_handle': msg['ReceiptHandle'],
        'queue_url': args['queue_url'],
        'sfn_arn': args['sfn_arn'],
        'status': DownsampleStatus.IN_PROGRESS,
        'db_host': job['db_host'],
        'channel_id': job['channel_id'],
        'msg': job,
    }

    return output
예제 #18
0
def lambda_throttle_count(lambda_arn):
    """Read the Throttle count for the given Lambda function from Cloud Watch

    The metric is read for the last minute, which is in the process of being updated
    so the value read is not the final value that is recorded in Cloud Watch for the
    given time period.

    Args:
        lambda_arn (str): ARN or Name of the lambda to get the metric for

    Return:
        float: The Sample Count for the Lambda's Throttle count
        -1: If there was an error getting the metric
    """
    session = aws.get_session()
    cw = session.client('cloudwatch')

    lambda_name = lambda_arn.split(':')[-1]

    try:
        end = datetime.now()
        begin = end - timedelta(minutes=1)
        resp = cw.get_metric_statistics(
            Namespace='AWS/Lambda',
            MetricName='Throttles',
            # Limit the throttle count to only our target lambda function
            Dimensions=[{
                'Name': 'FunctionName',
                'Value': lambda_name
            }],
            StartTime=begin,
            EndTime=end,
            Period=60,
            Unit='Count',
            Statistics=['SampleCount'])

        if 'Datapoints' in resp and len(resp['Datapoints']) > 0:
            if 'SampleCount' in resp['Datapoints'][0]:
                return resp['Datapoints'][0]['SampleCount']
        return 0.0
    except Exception as err:
        log.exception("Problem getting Lambda Throttle Count: {}".format(err))
        return -1
예제 #19
0
def check_queue(queue_arn):
    """Get the count of messages in the given queue

    The count is a combination Approximate Number of Messages, Messages Delayed,
    and Messages Not Visible.

    If the queue_arn contains 'dlq' the first message will be read and the SNS
    DLQ message decoded to print the error that caused the DLQ message.

    Args:
        queue_arn (str): The URL of the queue

    Return:
        int: The count of messages or zero if the queue could not be queried
    """
    session = aws.get_session()
    sqs = session.client('sqs')

    try:
        resp = sqs.get_queue_attributes(QueueUrl = queue_arn,
                                        AttributeNames = ['ApproximateNumberOfMessages',
                                                          'ApproximateNumberOfMessagesDelayed',
                                                          'ApproximateNumberOfMessagesNotVisible'])
    except:
        log.exception("Could not get message count for queue '{}'".format(queue_arn))
        return 0
    else:
        # Include both the number of messages and the number of in-flight messages
        message_count = int(resp['Attributes']['ApproximateNumberOfMessages']) + \
                        int(resp['Attributes']['ApproximateNumberOfMessagesDelayed']) + \
                        int(resp['Attributes']['ApproximateNumberOfMessagesNotVisible'])
        if message_count > 0 and 'dlq' in queue_arn:
            try:
                resp = sqs.receive_message(QueueUrl = queue_arn)
                for msg in resp['Messages']:
                    body = json.loads(msg['Body'])
                    error = body['Records'][0]['Sns']['MessageAttributes']['ErrorMessage']['Value']
                    log.debug("DLQ Error: {}".format(error))
            except Exception as err:
                log.exception("Problem getting DLQ error message: {}".format(err))
        return message_count
예제 #20
0
def lambda_throttle_count(lambda_arn):
    """Read the Throttle count for the given Lambda function from Cloud Watch

    The metric is read for the last minute, which is in the process of being updated
    so the value read is not the final value that is recorded in Cloud Watch for the
    given time period.

    Args:
        lambda_arn (str): ARN or Name of the lambda to get the metric for

    Return:
        float: The Sample Count for the Lambda's Throttle count
        -1: If there was an error getting the metric
    """
    session = aws.get_session()
    cw = session.client('cloudwatch')

    lambda_name = lambda_arn.split(':')[-1]

    try:
        end = datetime.now()
        begin = end - timedelta(minutes=1)
        resp = cw.get_metric_statistics(Namespace = 'AWS/Lambda',
                                        MetricName = 'Throttles',
                                        # Limit the throttle count to only our target lambda function
                                        Dimensions = [{'Name': 'FunctionName', 'Value': lambda_name}],
                                        StartTime = begin,
                                        EndTime = end,
                                        Period = 60,
                                        Unit = 'Count',
                                        Statistics = ['SampleCount'])

        if 'Datapoints' in resp and len(resp['Datapoints']) > 0:
            if 'SampleCount' in resp['Datapoints'][0]:
                return resp['Datapoints'][0]['SampleCount']
        return 0.0
    except Exception as err:
        log.exception("Problem getting Lambda Throttle Count: {}".format(err))
        return -1
예제 #21
0
def delete_downsample_job(args):
    """
    Delete the message for the finished downsample job from SQS.

    Returns a dict that allows the last state to start a new instance of the
    downsample step function.

    Args:
        args (dict): {
            'sfn_arn': ARN of this step function,,
            'queue_url': <URL of SQS queue>,
            'job_receipt_handle': <msg's receipt handle,
            'lookup_key': <full lookup key of channel>,
            ...
        }

    Returns:
        (dict): {
            'queue_url': <URL of SQS queue>,
            'sfn_arn': <arn of the downsample step fcn>,
            'lookup_key': <full lookup key of channel>,
        }
    """
    try:
        session = aws.get_session()
        sqs = session.client('sqs')
        sqs.delete_message(QueueUrl=args['queue_url'],
                           ReceiptHandle=args['job_receipt_handle'])
        log.info(
            f"Deleting SQS message for downsample of {args['lookup_key']}")
        return {
            'sfn_arn': args['sfn_arn'],
            'queue_url': args['queue_url'],
            'lookup_key': args['lookup_key'],
        }
    except Exception as ex:
        log.exception(f'Error trying to downsample job from SQS: {ex}')
        raise
예제 #22
0
def start(request, resource):
    """Main code to start a downsample

    Args:
        request: DRF Request object
        resource (BossResourceDjango): The channel to downsample

    Returns:
        (HttpResponse)
    """

    channel = resource.get_channel()
    chan_status = channel.downsample_status.upper()
    if chan_status == Channel.DownsampleStatus.IN_PROGRESS:
        return BossHTTPError(
            "Channel is currently being downsampled. Invalid Request.",
            ErrorCodes.INVALID_STATE)
    elif chan_status == Channel.DownsampleStatus.QUEUED:
        return BossHTTPError(
            "Channel is already waiting to be downsampled. Invalid Request.",
            ErrorCodes.INVALID_STATE)
    elif chan_status == Channel.DownsampleStatus.DOWNSAMPLED and not request.user.is_staff:
        return BossHTTPError(
            "Channel is already downsampled. Invalid Request.",
            ErrorCodes.INVALID_STATE)

    if request.user.is_staff:
        # DP HACK: allow admin users to override the coordinate frame
        frame = request.data
    else:
        frame = {}

    boss_config = BossConfig()
    collection = resource.get_collection()
    experiment = resource.get_experiment()
    coord_frame = resource.get_coord_frame()
    lookup_key = resource.get_lookup_key()
    col_id, exp_id, ch_id = lookup_key.split("&")

    def get_frame(idx):
        return int(frame.get(idx, getattr(coord_frame, idx)))

    downsample_sfn = boss_config['sfn']['downsample_sfn']
    db_host = boss_config['aws']['db']

    args = {
        'lookup_key':
        lookup_key,
        'collection_id':
        int(col_id),
        'experiment_id':
        int(exp_id),
        'channel_id':
        int(ch_id),
        'annotation_channel':
        not channel.is_image(),
        'data_type':
        resource.get_data_type(),
        's3_bucket':
        boss_config["aws"]["cuboid_bucket"],
        's3_index':
        boss_config["aws"]["s3-index-table"],
        'x_start':
        get_frame('x_start'),
        'y_start':
        get_frame('y_start'),
        'z_start':
        get_frame('z_start'),
        'x_stop':
        get_frame('x_stop'),
        'y_stop':
        get_frame('y_stop'),
        'z_stop':
        get_frame('z_stop'),
        'resolution':
        int(channel.base_resolution),
        'resolution_max':
        int(experiment.num_hierarchy_levels),
        'res_lt_max':
        int(channel.base_resolution) + 1 < int(
            experiment.num_hierarchy_levels),
        'type':
        experiment.hierarchy_method,
        'iso_resolution':
        int(resource.get_isotropic_level()),

        # This step function executes: boss-tools/activities/resolution_hierarchy.py
        'downsample_volume_lambda':
        boss_config['lambda']['downsample_volume'],

        # Need to pass step function's ARN to itself, so it can start another
        # instance of itself after finishing a downsample.
        'sfn_arn':
        downsample_sfn,
        'db_host':
        db_host,
        'aws_region':
        get_region(),
    }

    # Check that only administrators are triggering extra large downsamples
    if ((not request.user.is_staff) and
       ((args['x_stop'] - args['x_start']) *\
        (args['y_stop'] - args['y_start']) *\
        (args['z_stop'] - args['z_start']) > settings.DOWNSAMPLE_MAX_SIZE)):
        return BossHTTPError(
            "Large downsamples require admin permissions to trigger. Invalid Request.",
            ErrorCodes.INVALID_STATE)

    session = get_session()

    downsample_sqs = boss_config['aws']['downsample-queue']

    try:
        enqueue_job(session, args, downsample_sqs)
    except BossError as be:
        return BossHTTPError(be.message, be.error_code)

    compute_usage_metrics(session, args, boss_config['system']['fqdn'],
                          request.user.username or "public", collection.name,
                          experiment.name, channel.name)

    region = get_region()
    account_id = get_account_id()
    downsample_sfn_arn = f'arn:aws:states:{region}:{account_id}:stateMachine:{downsample_sfn}'
    if not check_for_running_sfn(session, downsample_sfn_arn):
        bossutils.aws.sfn_run(session, downsample_sfn_arn, {
            'queue_url': downsample_sqs,
            'sfn_arn': downsample_sfn_arn,
        })

    return HttpResponse(status=201)
예제 #23
0
def downsample_channel(args):
    """
    Slice the given channel into chunks of 2x2x2 or 2x2x1 cubes that are then
    sent to the downsample_volume lambda for downsampling into a 1x1x1 cube at
    resolution + 1.

    Makes use of the bossutils.multidimensional library for simplified vector
    math.

    Args:
        args {
            downsample_volume_sfn (ARN)

            collection_id (int)
            experiment_id (int)
            channel_id (int)
            annotation_channel (bool)
            data_type (str) 'uint8' | 'uint16' | 'uint64'

            s3_bucket (URL)
            s3_index (URL)
            id_index (URL)

            x_start (int)
            y_start (int)
            z_start (int)

            x_stop (int)
            y_stop (int)
            z_stop (int)

            resolution (int) The resolution to downsample. Creates resolution + 1
            resolution_max (int) The maximum resolution to generate
            res_lt_max (bool) = args['resolution'] < (args['resolution_max'] - 1)

            annotation_index_max (int) The maximum resolution to index annotation channel cubes at
                                       When annotation_index_max = N, indices will exist for res 0 - (N - 1)

            type (str) 'isotropic' | 'anisotropic'
            iso_resolution (int) if resolution >= iso_resolution && type == 'anisotropic' downsample both
        }
    """

    #log.debug("Downsampling resolution " + str(args['resolution']))

    resolution = args['resolution']

    dim = XYZ(*CUBOIDSIZE[resolution])
    #log.debug("Cube dimensions: {}".format(dim))

    def frame(key):
        return XYZ(args[key.format('x')], args[key.format('y')], args[key.format('z')])

    # Figure out variables for isotropic, anisotropic, or isotropic and anisotropic
    # downsampling. If both are happening, fanout one and then the other in series.
    configs = []
    if args['type'] == 'isotropic':
        configs.append({
            'name': 'isotropic',
            'step': XYZ(2,2,2),
            'iso_flag': False,
            'frame_start_key': '{}_start',
            'frame_stop_key': '{}_stop',
        })
    else:
        configs.append({
            'name': 'anisotropic',
            'step': XYZ(2,2,1),
            'iso_flag': False,
            'frame_start_key': '{}_start',
            'frame_stop_key': '{}_stop',
        })

        if resolution >= args['iso_resolution']: # DP TODO: Figure out how to launch aniso iso version with mutating arguments
            configs.append({
                'name': 'isotropic',
                'step': XYZ(2,2,2),
                'iso_flag': True,
                'frame_start_key': 'iso_{}_start',
                'frame_stop_key': 'iso_{}_stop',
            })

    for config in configs:
        frame_start = frame(config['frame_start_key'])
        frame_stop = frame(config['frame_stop_key'])
        step = config['step']
        use_iso_flag = config['iso_flag'] # If the resulting cube should be marked with the ISO flag
        index_annotations = args['resolution'] < (args['annotation_index_max'] - 1)

        # Round to the furthest full cube from the center of the data
        cubes_start = frame_start // dim
        cubes_stop = ceildiv(frame_stop, dim)

        log.debug('Downsampling {} resolution {}'.format(config['name'], resolution))
        log.debug("Frame corner: {}".format(frame_start))
        log.debug("Frame extent: {}".format(frame_stop))
        log.debug("Cubes corner: {}".format(cubes_start))
        log.debug("Cubes extent: {}".format(cubes_stop))
        log.debug("Downsample step: {}".format(step))
        log.debug("Indexing Annotations: {}".format(index_annotations))

        # Call the downsample_volume lambda to process the data
        fanout(aws.get_session(),
               args['downsample_volume_sfn'],
               make_args(args, cubes_start, cubes_stop, step, dim, use_iso_flag, index_annotations),
               max_concurrent = MAX_NUM_PROCESSES,
               rampup_delay = RAMPUP_DELAY,
               rampup_backoff = RAMPUP_BACKOFF,
               poll_delay = POLL_DELAY,
               status_delay = STATUS_DELAY)

        # Resize the coordinate frame extents as the data shrinks
        # DP NOTE: doesn't currently work correctly with non-zero frame starts
        def resize(var, size):
            start = config['frame_start_key'].format(var)
            stop = config['frame_stop_key'].format(var)
            args[start] //= size
            args[stop] = ceildiv(args[stop], size)
        resize('x', step.x)
        resize('y', step.y)
        resize('z', step.z)

    # if next iteration will split into aniso and iso downsampling, copy the coordinate frame
    if args['type'] != 'isotropic' and (resolution + 1) == args['iso_resolution']:
        def copy(var):
            args['iso_{}_start'.format(var)] = args['{}_start'.format(var)]
            args['iso_{}_stop'.format(var)] = args['{}_stop'.format(var)]
        copy('x')
        copy('y')
        copy('z')

    # Advance the loop and recalculate the conditional
    # Using max - 1 because resolution_max should not be a valid resolution
    # and res < res_max will end with res = res_max - 1, which generates res_max resolution
    args['resolution'] = resolution + 1
    args['res_lt_max'] = args['resolution'] < (args['resolution_max'] - 1)
    return args
def ingest_populate(args):
    """Populate the ingest upload SQS Queue with tile information

    Note: This activity will clear the upload queue of any existing
          messages

    Args:
        args: {
            'upload_sfn': ARN,

            'job_id': '',
            'upload_queue': ARN,
            'ingest_queue': ARN,
            'ingest_type': int (0 == TILE, 1 == VOLUMETRIC),
            'resolution': 0,
            'project_info': [col_id, exp_id, ch_id],

            't_start': 0,
            't_stop': 0,
            't_tile_size': 0,

            'x_start': 0,
            'x_stop': 0,
            'x_tile_size': 0,

            'y_start': 0,
            'y_stop': 0
            'y_tile_size': 0,

            'z_start': 0,
            'z_stop': 0
            'z_tile_size': 1,
            'z_chunk_size': 16 for Tile or Probably 64 for Volumetric
        }

    Returns:
        {'arn': Upload queue ARN,
         'count': Number of messages put into the queue}
    """
    log.debug("Starting to populate upload queue")

    args['MAX_NUM_ITEMS_PER_LAMBDA'] = MAX_NUM_ITEMS_PER_LAMBDA

    if (args["ingest_type"] != 0) and (args["ingest_type"] != 1):
        raise ValueError("{}".format("Unknown ingest_type: {}".format(args["ingest_type"])))

    clear_queue(args['upload_queue'])

    results = fanout(aws.get_session(),
                     args['upload_sfn'],
                     split_args(args),
                     max_concurrent=MAX_NUM_PROCESSES,
                     rampup_delay=RAMPUP_DELAY,
                     rampup_backoff=RAMPUP_BACKOFF,
                     poll_delay=POLL_DELAY,
                     status_delay=STATUS_DELAY)

    # At least one None values in the return of fanout. This avoids an exception in those cases.
    if results is None:
        messages_uploaded = 0
    else:
        messages_uploaded = sum(filter(None, results))

    if args["ingest_type"] == 0:
        tile_count = get_tile_count(args)
        if tile_count != messages_uploaded:
            log.warning("Messages uploaded do not match tile count.  tile count: {} messages uploaded: {}"
                      .format(tile_count, messages_uploaded))
        else:
            log.debug("tile count and messages uploaded match: {}".format(tile_count))

        return {
            'arn': args['upload_queue'],
            'count': tile_count,
        }
    elif args["ingest_type"] == 1:
        vol_count = get_volumetric_count(args)
        if vol_count != messages_uploaded:
            log.warning("Messages uploaded do not match volumetric count.  volumetric count: {} messages uploaded: {}"
                        .format(vol_count, messages_uploaded))
        else:
            log.debug("volumetric count and messages uploaded match: {}".format(vol_count))

        return {
            'arn': args['upload_queue'],
            'count': vol_count,
        }
def ingest_populate(args):
    """Populate the ingest upload SQS Queue with tile information

    Note: This activity will clear the upload queue of any existing
          messages

    Args:
        args: {
            'upload_sfn': ARN,

            'job_id': '',
            'upload_queue': ARN,
            'ingest_queue': ARN,
            'ingest_type': int (0 == TILE, 1 == VOLUMETRIC),
            'resolution': 0,
            'project_info': [col_id, exp_id, ch_id],

            't_start': 0,
            't_stop': 0,
            't_tile_size': 0,

            'x_start': 0,
            'x_stop': 0,
            'x_tile_size': 0,

            'y_start': 0,
            'y_stop': 0
            'y_tile_size': 0,

            'z_start': 0,
            'z_stop': 0
            'z_tile_size': 1,
            'z_chunk_size': 16 for Tile or Probably 64 for Volumetric
        }

    Returns:
        {'arn': Upload queue ARN,
         'count': Number of messages put into the queue}
    """
    log.debug("Starting to populate upload queue")

    args['MAX_NUM_ITEMS_PER_LAMBDA'] = MAX_NUM_ITEMS_PER_LAMBDA

    if (args["ingest_type"] != 0) and (args["ingest_type"] != 1):
        raise ValueError("{}".format("Unknown ingest_type: {}".format(args["ingest_type"])))

    clear_queue(args['upload_queue'])

    results = fanout(aws.get_session(),
                     args['upload_sfn'],
                     split_args(args),
                     max_concurrent=MAX_NUM_PROCESSES,
                     rampup_delay=RAMPUP_DELAY,
                     rampup_backoff=RAMPUP_BACKOFF,
                     poll_delay=POLL_DELAY,
                     status_delay=STATUS_DELAY)

    # At least one None values in the return of fanout. This avoids an exception in those cases.
    if results is None:
        messages_uploaded = 0
    else:
        messages_uploaded = sum(filter(None, results))

    if args["ingest_type"] == 0:
        tile_count = get_tile_count(args)
        if tile_count != messages_uploaded:
            log.warning("Messages uploaded do not match tile count.  tile count: {} messages uploaded: {}"
                      .format(tile_count, messages_uploaded))
        else:
            log.debug("tile count and messages uploaded match: {}".format(tile_count))

        return {
            'arn': args['upload_queue'],
            'count': tile_count,
        }
    elif args["ingest_type"] == 1:
        vol_count = get_volumetric_count(args)
        if vol_count != messages_uploaded:
            log.warning("Messages uploaded do not match volumetric count.  volumetric count: {} messages uploaded: {}"
                        .format(vol_count, messages_uploaded))
        else:
            log.debug("volumetric count and messages uploaded match: {}".format(vol_count))

        return {
            'arn': args['upload_queue'],
            'count': vol_count,
        }