Пример #1
0
    def __init__(self, gcp_functions_config):
        self.log_level = os.getenv('CLOUDBUTTON_LOGLEVEL')
        self.name = 'gcp_functions'
        self.gcp_functions_config = gcp_functions_config
        self.package = 'cloudbutton_v' + __version__

        self.region = gcp_functions_config['region']
        self.service_account = gcp_functions_config['service_account']
        self.project = gcp_functions_config['project_name']
        self.credentials_path = gcp_functions_config['credentials_path']
        self.num_retries = gcp_functions_config['retries']
        self.retry_sleeps = gcp_functions_config['retry_sleeps']

        # Instantiate storage client (to upload function bin)
        self.internal_storage = InternalStorage(
            gcp_functions_config['storage'])

        # Setup pubsub client
        try:  # Get credenitals from JSON file
            service_account_info = json.load(open(self.credentials_path))
            credentials = jwt.Credentials.from_service_account_info(
                service_account_info, audience=AUDIENCE)
            credentials_pub = credentials.with_claims(audience=AUDIENCE)
        except Exception:  # Get credentials from gcp function environment
            credentials_pub = None
        self.publisher_client = pubsub_v1.PublisherClient(
            credentials=credentials_pub)

        log_msg = 'Cloudbutton v{} init for GCP Functions - Project: {} - Region: {}'.format(
            __version__, self.project, self.region)
        logger.info(log_msg)

        if not self.log_level:
            print(log_msg)
Пример #2
0
def delete_runtime(name, config=None):
    config = default_config(config)
    storage_config = extract_storage_config(config)
    internal_storage = InternalStorage(storage_config)
    compute_config = extract_compute_config(config)
    compute_handler = Compute(compute_config)

    runtimes = compute_handler.list_runtimes(name)
    for runtime in runtimes:
        compute_handler.delete_runtime(runtime[0], runtime[1])
        runtime_key = compute_handler.get_runtime_key(runtime[0], runtime[1])
        internal_storage.delete_runtime_meta(runtime_key)
Пример #3
0
def create_runtime(name, memory=None, config=None):
    config = default_config(config)
    storage_config = extract_storage_config(config)
    internal_storage = InternalStorage(storage_config)
    compute_config = extract_compute_config(config)
    compute_handler = Compute(compute_config)

    memory = config['cloudbutton']['runtime_memory'] if not memory else memory
    timeout = config['cloudbutton']['runtime_timeout']

    logger.info('Creating runtime: {}, memory: {}'.format(name, memory))

    runtime_key = compute_handler.get_runtime_key(name, memory)
    runtime_meta = compute_handler.create_runtime(name,
                                                  memory,
                                                  timeout=timeout)

    try:
        internal_storage.put_runtime_meta(runtime_key, runtime_meta)
    except Exception:
        raise ("Unable to upload 'preinstalled-modules' file into {}".format(
            internal_storage.backend))
Пример #4
0
def update_runtime(name, config=None):
    config = default_config(config)
    storage_config = extract_storage_config(config)
    internal_storage = InternalStorage(storage_config)
    compute_config = extract_compute_config(config)
    compute_handler = Compute(compute_config)

    timeout = config['cloudbutton']['runtime_timeout']
    logger.info('Updating runtime: {}'.format(name))

    runtimes = compute_handler.list_runtimes(name)

    for runtime in runtimes:
        runtime_key = compute_handler.get_runtime_key(runtime[0], runtime[1])
        runtime_meta = compute_handler.create_runtime(runtime[0], runtime[1],
                                                      timeout)

        try:
            internal_storage.put_runtime_meta(runtime_key, runtime_meta)
        except Exception:
            raise (
                "Unable to upload 'preinstalled-modules' file into {}".format(
                    internal_storage.backend))
Пример #5
0
    def __init__(self, config, num_invokers, log_level):
        self.config = config
        self.num_invokers = num_invokers
        self.log_level = log_level
        storage_config = extract_storage_config(self.config)
        self.internal_storage = InternalStorage(storage_config)
        compute_config = extract_compute_config(self.config)

        self.remote_invoker = self.config['cloudbutton'].get('remote_invoker', False)
        self.rabbitmq_monitor = self.config['cloudbutton'].get('rabbitmq_monitor', False)
        if self.rabbitmq_monitor:
            self.rabbit_amqp_url = self.config['rabbitmq'].get('amqp_url')

        self.num_workers = self.config['cloudbutton'].get('workers')
        logger.debug('Total workers: {}'.format(self.num_workers))

        self.compute_handlers = []
        cb = compute_config['backend']
        regions = compute_config[cb].get('region')
        if regions and type(regions) == list:
            for region in regions:
                new_compute_config = compute_config.copy()
                new_compute_config[cb]['region'] = region
                compute_handler = Compute(new_compute_config)
                self.compute_handlers.append(compute_handler)
        else:
            if cb == 'localhost':
                global CBH
                if cb in CBH and CBH[cb].compute_handler.num_workers != self.num_workers:
                    del CBH[cb]
                if cb in CBH:
                    logger.info('{} compute handler already started'.format(cb))
                    compute_handler = CBH[cb]
                    self.compute_handlers.append(compute_handler)
                else:
                    logger.info('Starting {} compute handler'.format(cb))
                    compute_handler = Compute(compute_config)
                    CBH[cb] = compute_handler
                    self.compute_handlers.append(compute_handler)
            else:
                compute_handler = Compute(compute_config)
                self.compute_handlers.append(compute_handler)

        self.token_bucket_q = Queue()
        self.pending_calls_q = Queue()

        self.job_monitor = JobMonitor(self.config, self.internal_storage, self.token_bucket_q)
Пример #6
0
def run_tests(test_to_run, config=None):
    global CONFIG, STORAGE_CONFIG, STORAGE

    CONFIG = json.load(args.config) if config else default_config()
    STORAGE_CONFIG = extract_storage_config(CONFIG)
    STORAGE = InternalStorage(STORAGE_CONFIG).storage_handler

    suite = unittest.TestSuite()
    if test_to_run == 'all':
        suite.addTest(unittest.makeSuite(TestPywren))
    else:
        try:
            suite.addTest(TestPywren(test_to_run))
        except ValueError:
            print("unknown test, use: --help")
            sys.exit()

    runner = unittest.TextTestRunner()
    runner.run(suite)
Пример #7
0
def clean_all(config=None):
    logger.info('Cleaning all Cloudbutton information')
    config = default_config(config)
    storage_config = extract_storage_config(config)
    internal_storage = InternalStorage(storage_config)
    compute_config = extract_compute_config(config)
    compute_handler = Compute(compute_config)

    # Clean localhost executor temp dirs
    shutil.rmtree(STORAGE_FOLDER, ignore_errors=True)
    shutil.rmtree(DOCKER_FOLDER, ignore_errors=True)

    # Clean object storage temp dirs
    compute_handler.delete_all_runtimes()
    sh = internal_storage.storage_handler
    clean_bucket(sh, storage_config['bucket'], RUNTIMES_PREFIX, sleep=1)
    clean_bucket(sh, storage_config['bucket'], JOBS_PREFIX, sleep=1)

    # Clean local cloudbutton cache
    shutil.rmtree(CACHE_DIR, ignore_errors=True)
Пример #8
0
    def result(self, throw_except=True, internal_storage=None):
        """
        Return the value returned by the call.
        If the call raised an exception, this method will raise the same exception
        If the future is cancelled before completing then CancelledError will be raised.

        :param throw_except: Reraise exception if call raised. Default true.
        :param internal_storage: Storage handler to poll cloud storage. Default None.
        :return: Result of the call.
        :raises CancelledError: If the job is cancelled before completed.
        :raises TimeoutError: If job is not complete after `timeout` seconds.
        """
        if self._state == ResponseFuture.State.New:
            raise ValueError("task not yet invoked")

        if self._state == ResponseFuture.State.Success:
            return self._return_val

        if self._state == ResponseFuture.State.Futures:
            return self._new_futures

        if internal_storage is None:
            internal_storage = InternalStorage(
                storage_config=self._storage_config)

        self.status(throw_except=throw_except,
                    internal_storage=internal_storage)

        if self._state == ResponseFuture.State.Success:
            return self._return_val

        if self._state == ResponseFuture.State.Futures:
            return self._new_futures

        call_output = internal_storage.get_call_output(self.executor_id,
                                                       self.job_id,
                                                       self.call_id)
        self._output_query_count += 1

        while call_output is None and self._output_query_count < self.GET_RESULT_MAX_RETRIES:
            time.sleep(self.GET_RESULT_SLEEP_SECS)
            call_output = internal_storage.get_call_output(
                self.executor_id, self.job_id, self.call_id)
            self._output_query_count += 1

        if call_output is None:
            if throw_except:
                raise Exception('Unable to get the output from call {} - '
                                'Activation ID: {}'.format(
                                    self.call_id, self.activation_id))
            else:
                self._set_state(ResponseFuture.State.Error)
                return None

        self._call_output = pickle.loads(call_output)
        function_result = self._call_output['result']

        self.stats['output_done_tstamp'] = time.time()
        self.stats['output_query_count'] = self._output_query_count

        log_msg = (
            'ExecutorID {} | JobID {} - Got output from call {} - Activation '
            'ID: {}'.format(self.executor_id, self.job_id, self.call_id,
                            self.activation_id))
        logger.info(log_msg)

        if isinstance(function_result, ResponseFuture) or \
           (type(function_result) == list and len(function_result) > 0 and isinstance(function_result[0], ResponseFuture)):
            self._new_futures = [
                function_result
            ] if type(function_result) == ResponseFuture else function_result
            self._set_state(ResponseFuture.State.Futures)
            self.stats['status_done_tstamp'] = self.stats.pop(
                'output_done_tstamp')
            return self._new_futures

        else:
            self._return_val = function_result
            self._set_state(ResponseFuture.State.Success)
            return self._return_val
Пример #9
0
    def status(self, throw_except=True, internal_storage=None):
        """
        Return the status returned by the call.
        If the call raised an exception, this method will raise the same exception
        If the future is cancelled before completing then CancelledError will be raised.

        :param check_only: Return None immediately if job is not complete. Default False.
        :param throw_except: Reraise exception if call raised. Default true.
        :param storage_handler: Storage handler to poll cloud storage. Default None.
        :return: Result of the call.
        :raises CancelledError: If the job is cancelled before completed.
        :raises TimeoutError: If job is not complete after `timeout` seconds.
        """
        if self._state == ResponseFuture.State.New:
            raise ValueError("task not yet invoked")

        if self._state in [
                ResponseFuture.State.Ready, ResponseFuture.State.Success
        ]:
            return self._call_status

        if internal_storage is None:
            internal_storage = InternalStorage(self._storage_config)

        if self._call_status is None:
            check_storage_path(internal_storage.get_storage_config(),
                               self._storage_path)
            self._call_status = internal_storage.get_call_status(
                self.executor_id, self.job_id, self.call_id)
            self._status_query_count += 1

            while self._call_status is None:
                time.sleep(self.GET_RESULT_SLEEP_SECS)
                self._call_status = internal_storage.get_call_status(
                    self.executor_id, self.job_id, self.call_id)
                self._status_query_count += 1

        self.stats['status_done_tstamp'] = time.time()
        self.stats['status_query_count'] = self._status_query_count
        self.activation_id = self._call_status.pop('activation_id', None)

        if self._call_status['type'] == '__init__':
            self._set_state(ResponseFuture.State.Running)
            return self._call_status

        if self._call_status['exception']:
            self._set_state(ResponseFuture.State.Error)
            self._exception = pickle.loads(eval(self._call_status['exc_info']))

            msg1 = (
                'ExecutorID {} | JobID {} - There was an exception - Activation '
                'ID: {}'.format(self.executor_id, self.job_id,
                                self.activation_id))

            if not self._call_status.get('exc_pickle_fail', False):
                fn_exctype = self._exception[0]
                fn_exc = self._exception[1]
                if fn_exc.args and fn_exc.args[0] == "HANDLER":
                    self._handler_exception = True
                    try:
                        del fn_exc.errno
                    except Exception:
                        pass
                    fn_exc.args = (fn_exc.args[1], )
            else:
                fn_exctype = Exception
                fn_exc = Exception(self._exception['exc_value'])
                self._exception = (fn_exctype, fn_exc,
                                   self._exception['exc_traceback'])

            def exception_hook(exctype, exc, trcbck):
                if exctype == fn_exctype and str(exc) == str(fn_exc):
                    msg2 = '--> Exception: {} - {}'.format(
                        fn_exctype.__name__, fn_exc)
                    print(msg1) if not self.log_level else logger.info(msg1)
                    if self._handler_exception:
                        print(msg2 +
                              '\n') if not self.log_level else logger.info(
                                  msg2)
                    else:
                        traceback.print_exception(*self._exception)
                else:
                    sys.excepthook = sys.__excepthook__
                    traceback.print_exception(exctype, exc, trcbck)

            if throw_except:
                sys.excepthook = exception_hook
                reraise(*self._exception)
            else:
                logger.info(msg1)
                logger.debug('Exception: {} - {}'.format(
                    self._exception[0].__name__, self._exception[1]))
                return None

        for key in self._call_status:
            if any(ss in key for ss in ['time', 'tstamp', 'count', 'size']):
                self.stats[key] = self._call_status[key]

        self.stats['exec_time'] = round(
            self.stats['end_tstamp'] - self.stats['start_tstamp'], 8)
        total_time = format(round(self.stats['exec_time'], 2), '.2f')

        log_msg = (
            'ExecutorID {} | JobID {} - Got status from call {} - Activation '
            'ID: {} - Time: {} seconds'.format(self.executor_id, self.job_id,
                                               self.call_id,
                                               self.activation_id,
                                               str(total_time)))
        logger.info(log_msg)
        self._set_state(ResponseFuture.State.Ready)

        if not self._call_status['result']:
            self._produce_output = False

        if not self._produce_output:
            self._set_state(ResponseFuture.State.Success)

        if 'new_futures' in self._call_status:
            self.result(throw_except=throw_except,
                        internal_storage=internal_storage)

        return self._call_status
Пример #10
0
def function_handler(event):
    start_tstamp = time.time()

    log_level = event['log_level']
    cloud_logging_config(log_level)
    logger.debug("Action handler started")

    extra_env = event.get('extra_env', {})
    os.environ.update(extra_env)

    os.environ.update({'CLOUDBUTTON_FUNCTION': 'True',
                       'PYTHONUNBUFFERED': 'True'})

    config = event['config']
    call_id = event['call_id']
    job_id = event['job_id']
    executor_id = event['executor_id']
    exec_id = "{}/{}/{}".format(executor_id, job_id, call_id)
    logger.info("Execution-ID: {}".format(exec_id))

    runtime_name = event['runtime_name']
    runtime_memory = event['runtime_memory']
    execution_timeout = event['execution_timeout']
    logger.debug("Runtime name: {}".format(runtime_name))
    logger.debug("Runtime memory: {}MB".format(runtime_memory))
    logger.debug("Function timeout: {}s".format(execution_timeout))

    func_key = event['func_key']
    data_key = event['data_key']
    data_byte_range = event['data_byte_range']

    storage_config = extract_storage_config(config)
    internal_storage = InternalStorage(storage_config)

    call_status = CallStatus(config, internal_storage)
    call_status.response['host_submit_tstamp'] = event['host_submit_tstamp']
    call_status.response['start_tstamp'] = start_tstamp
    context_dict = {
        'cloudbutton_version': os.environ.get("CLOUDBUTTON_VERSION"),
        'call_id': call_id,
        'job_id': job_id,
        'executor_id': executor_id,
        'activation_id': os.environ.get('__PW_ACTIVATION_ID')
    }
    call_status.response.update(context_dict)

    show_memory_peak = strtobool(os.environ.get('SHOW_MEMORY_PEAK', 'False'))

    try:
        if version.__version__ != event['cloudbutton_version']:
            msg = ("Cloudbutton version mismatch. Host version: {} - Runtime version: {}"
                   .format(event['cloudbutton_version'], version.__version__))
            raise RuntimeError('HANDLER', msg)

        # send init status event
        call_status.send('__init__')

        # call_status.response['free_disk_bytes'] = free_disk_space("/tmp")
        custom_env = {'CLOUDBUTTON_CONFIG': json.dumps(config),
                      'CLOUDBUTTON_EXECUTION_ID': exec_id,
                      'PYTHONPATH': "{}:{}".format(os.getcwd(), LIBS_PATH)}
        os.environ.update(custom_env)

        jobrunner_stats_dir = os.path.join(STORAGE_FOLDER,
                                           storage_config['bucket'],
                                           JOBS_PREFIX, executor_id,
                                           job_id, call_id)
        os.makedirs(jobrunner_stats_dir, exist_ok=True)
        jobrunner_stats_filename = os.path.join(jobrunner_stats_dir, 'jobrunner.stats.txt')

        jobrunner_config = {'cloudbutton_config': config,
                            'call_id':  call_id,
                            'job_id':  job_id,
                            'executor_id':  executor_id,
                            'func_key': func_key,
                            'data_key': data_key,
                            'log_level': log_level,
                            'data_byte_range': data_byte_range,
                            'output_key': create_output_key(JOBS_PREFIX, executor_id, job_id, call_id),
                            'stats_filename': jobrunner_stats_filename}

        if show_memory_peak:
            mm_handler_conn, mm_conn = Pipe()
            memory_monitor = Thread(target=memory_monitor_worker, args=(mm_conn, ))
            memory_monitor.start()

        handler_conn, jobrunner_conn = Pipe()
        jobrunner = JobRunner(jobrunner_config, jobrunner_conn, internal_storage)
        logger.debug('Starting JobRunner process')
        local_execution = strtobool(os.environ.get('__PW_LOCAL_EXECUTION', 'False'))
        jrp = Thread(target=jobrunner.run) if local_execution else Process(target=jobrunner.run)
        jrp.start()

        jrp.join(execution_timeout)
        logger.debug('JobRunner process finished')

        if jrp.is_alive():
            # If process is still alive after jr.join(job_max_runtime), kill it
            try:
                jrp.terminate()
            except Exception:
                # thread does not have terminate method
                pass
            msg = ('Function exceeded maximum time of {} seconds and was '
                   'killed'.format(execution_timeout))
            raise TimeoutError('HANDLER', msg)

        if show_memory_peak:
            mm_handler_conn.send('STOP')
            memory_monitor.join()
            peak_memory_usage = int(mm_handler_conn.recv())
            logger.info("Peak memory usage: {}".format(sizeof_fmt(peak_memory_usage)))
            call_status.response['peak_memory_usage'] = peak_memory_usage

        if not handler_conn.poll():
            logger.error('No completion message received from JobRunner process')
            logger.debug('Assuming memory overflow...')
            # Only 1 message is returned by jobrunner when it finishes.
            # If no message, this means that the jobrunner process was killed.
            # 99% of times the jobrunner is killed due an OOM, so we assume here an OOM.
            msg = 'Function exceeded maximum memory and was killed'
            raise MemoryError('HANDLER', msg)

        if os.path.exists(jobrunner_stats_filename):
            with open(jobrunner_stats_filename, 'r') as fid:
                for l in fid.readlines():
                    key, value = l.strip().split(" ", 1)
                    try:
                        call_status.response[key] = float(value)
                    except Exception:
                        call_status.response[key] = value
                    if key in ['exception', 'exc_pickle_fail', 'result', 'new_futures']:
                        call_status.response[key] = eval(value)

    except Exception:
        # internal runtime exceptions
        print('----------------------- EXCEPTION !-----------------------', flush=True)
        traceback.print_exc(file=sys.stdout)
        print('----------------------------------------------------------', flush=True)
        call_status.response['exception'] = True

        pickled_exc = pickle.dumps(sys.exc_info())
        pickle.loads(pickled_exc)  # this is just to make sure they can be unpickled
        call_status.response['exc_info'] = str(pickled_exc)

    finally:
        call_status.response['end_tstamp'] = time.time()
        call_status.send('__end__')

        for key in extra_env:
            os.environ.pop(key)

        logger.info("Finished")
Пример #11
0
    def __init__(self,
                 config=None,
                 runtime=None,
                 runtime_memory=None,
                 compute_backend=None,
                 compute_backend_region=None,
                 storage_backend=None,
                 storage_backend_region=None,
                 workers=None,
                 rabbitmq_monitor=None,
                 remote_invoker=None,
                 log_level=None):
        """
        Initialize a FunctionExecutor class.

        :param config: Settings passed in here will override those in config file. Default None.
        :param runtime: Runtime name to use. Default None.
        :param runtime_memory: memory to use in the runtime. Default None.
        :param compute_backend: Name of the compute backend to use. Default None.
        :param compute_backend_region: Name of the compute backend region to use. Default None.
        :param storage_backend: Name of the storage backend to use. Default None.
        :param storage_backend_region: Name of the storage backend region to use. Default None.
        :param workers: Max number of concurrent workers.
        :param rabbitmq_monitor: use rabbitmq as the monitoring system. Default None.
        :param log_level: log level to use during the execution. Default None.

        :return `FunctionExecutor` object.
        """
        self.is_cloudbutton_function = is_cloudbutton_function()

        # Log level Configuration
        self.log_level = log_level
        if not self.log_level:
            if (logger.getEffectiveLevel() != logging.WARNING):
                self.log_level = logging.getLevelName(
                    logger.getEffectiveLevel())
        if self.log_level:
            os.environ["CLOUDBUTTON_LOGLEVEL"] = self.log_level
            if not self.is_cloudbutton_function:
                default_logging_config(self.log_level)

        # Overwrite pywren config parameters
        pw_config_ow = {}
        if runtime is not None:
            pw_config_ow['runtime'] = runtime
        if runtime_memory is not None:
            pw_config_ow['runtime_memory'] = int(runtime_memory)
        if compute_backend is not None:
            pw_config_ow['compute_backend'] = compute_backend
        if compute_backend_region is not None:
            pw_config_ow['compute_backend_region'] = compute_backend_region
        if storage_backend is not None:
            pw_config_ow['storage_backend'] = storage_backend
        if storage_backend_region is not None:
            pw_config_ow['storage_backend_region'] = storage_backend_region
        if workers is not None:
            pw_config_ow['workers'] = workers
        if rabbitmq_monitor is not None:
            pw_config_ow['rabbitmq_monitor'] = rabbitmq_monitor
        if remote_invoker is not None:
            pw_config_ow['remote_invoker'] = remote_invoker

        self.config = default_config(copy.deepcopy(config), pw_config_ow)

        self.executor_id = create_executor_id()
        logger.debug('FunctionExecutor created with ID: {}'.format(
            self.executor_id))

        self.data_cleaner = self.config['cloudbutton'].get(
            'data_cleaner', True)
        self.rabbitmq_monitor = self.config['cloudbutton'].get(
            'rabbitmq_monitor', False)

        if self.rabbitmq_monitor:
            if 'rabbitmq' in self.config and 'amqp_url' in self.config[
                    'rabbitmq']:
                self.rabbit_amqp_url = self.config['rabbitmq'].get('amqp_url')
            else:
                raise Exception(
                    "You cannot use rabbitmq_mnonitor since 'amqp_url'"
                    " is not present in configuration")

        storage_config = extract_storage_config(self.config)
        self.internal_storage = InternalStorage(storage_config)
        self.invoker = FunctionInvoker(self.config, self.executor_id,
                                       self.internal_storage)

        self.futures = []
        self.total_jobs = 0
        self.cleaned_jobs = set()
        self.last_call = None
Пример #12
0
class FunctionExecutor:
    def __init__(self,
                 config=None,
                 runtime=None,
                 runtime_memory=None,
                 compute_backend=None,
                 compute_backend_region=None,
                 storage_backend=None,
                 storage_backend_region=None,
                 workers=None,
                 rabbitmq_monitor=None,
                 remote_invoker=None,
                 log_level=None):
        """
        Initialize a FunctionExecutor class.

        :param config: Settings passed in here will override those in config file. Default None.
        :param runtime: Runtime name to use. Default None.
        :param runtime_memory: memory to use in the runtime. Default None.
        :param compute_backend: Name of the compute backend to use. Default None.
        :param compute_backend_region: Name of the compute backend region to use. Default None.
        :param storage_backend: Name of the storage backend to use. Default None.
        :param storage_backend_region: Name of the storage backend region to use. Default None.
        :param workers: Max number of concurrent workers.
        :param rabbitmq_monitor: use rabbitmq as the monitoring system. Default None.
        :param log_level: log level to use during the execution. Default None.

        :return `FunctionExecutor` object.
        """
        self.is_cloudbutton_function = is_cloudbutton_function()

        # Log level Configuration
        self.log_level = log_level
        if not self.log_level:
            if (logger.getEffectiveLevel() != logging.WARNING):
                self.log_level = logging.getLevelName(
                    logger.getEffectiveLevel())
        if self.log_level:
            os.environ["CLOUDBUTTON_LOGLEVEL"] = self.log_level
            if not self.is_cloudbutton_function:
                default_logging_config(self.log_level)

        # Overwrite pywren config parameters
        pw_config_ow = {}
        if runtime is not None:
            pw_config_ow['runtime'] = runtime
        if runtime_memory is not None:
            pw_config_ow['runtime_memory'] = int(runtime_memory)
        if compute_backend is not None:
            pw_config_ow['compute_backend'] = compute_backend
        if compute_backend_region is not None:
            pw_config_ow['compute_backend_region'] = compute_backend_region
        if storage_backend is not None:
            pw_config_ow['storage_backend'] = storage_backend
        if storage_backend_region is not None:
            pw_config_ow['storage_backend_region'] = storage_backend_region
        if workers is not None:
            pw_config_ow['workers'] = workers
        if rabbitmq_monitor is not None:
            pw_config_ow['rabbitmq_monitor'] = rabbitmq_monitor
        if remote_invoker is not None:
            pw_config_ow['remote_invoker'] = remote_invoker

        self.config = default_config(copy.deepcopy(config), pw_config_ow)

        self.executor_id = create_executor_id()
        logger.debug('FunctionExecutor created with ID: {}'.format(
            self.executor_id))

        self.data_cleaner = self.config['cloudbutton'].get(
            'data_cleaner', True)
        self.rabbitmq_monitor = self.config['cloudbutton'].get(
            'rabbitmq_monitor', False)

        if self.rabbitmq_monitor:
            if 'rabbitmq' in self.config and 'amqp_url' in self.config[
                    'rabbitmq']:
                self.rabbit_amqp_url = self.config['rabbitmq'].get('amqp_url')
            else:
                raise Exception(
                    "You cannot use rabbitmq_mnonitor since 'amqp_url'"
                    " is not present in configuration")

        storage_config = extract_storage_config(self.config)
        self.internal_storage = InternalStorage(storage_config)
        self.invoker = FunctionInvoker(self.config, self.executor_id,
                                       self.internal_storage)

        self.futures = []
        self.total_jobs = 0
        self.cleaned_jobs = set()
        self.last_call = None

    def __enter__(self):
        return self

    def _create_job_id(self, call_type):
        job_id = str(self.total_jobs).zfill(3)
        self.total_jobs += 1
        return '{}{}'.format(call_type, job_id)

    def call_async(self,
                   func,
                   data,
                   extra_env=None,
                   runtime_memory=None,
                   timeout=None,
                   include_modules=[],
                   exclude_modules=[]):
        """
        For running one function execution asynchronously

        :param func: the function to map over the data
        :param data: input data
        :param extra_data: Additional data to pass to action. Default None.
        :param extra_env: Additional environment variables for action environment. Default None.
        :param runtime_memory: Memory to use to run the function. Default None (loaded from config).
        :param timeout: Time that the functions have to complete their execution before raising a timeout.
        :param include_modules: Explicitly pickle these dependencies.
        :param exclude_modules: Explicitly keep these modules from pickled dependencies.

        :return: future object.
        """
        job_id = self._create_job_id('A')
        self.last_call = 'call_async'

        runtime_meta = self.invoker.select_runtime(job_id, runtime_memory)

        job = create_map_job(self.config,
                             self.internal_storage,
                             self.executor_id,
                             job_id,
                             map_function=func,
                             iterdata=[data],
                             runtime_meta=runtime_meta,
                             runtime_memory=runtime_memory,
                             extra_env=extra_env,
                             include_modules=include_modules,
                             exclude_modules=exclude_modules,
                             execution_timeout=timeout)

        futures = self.invoker.run(job)
        self.futures.extend(futures)

        return futures[0]

    def map(self,
            map_function,
            map_iterdata,
            extra_args=None,
            extra_env=None,
            runtime_memory=None,
            chunk_size=None,
            chunk_n=None,
            timeout=None,
            invoke_pool_threads=500,
            include_modules=[],
            exclude_modules=[]):
        """
        :param map_function: the function to map over the data
        :param map_iterdata: An iterable of input data
        :param extra_args: Additional arguments to pass to the function activation. Default None.
        :param extra_env: Additional environment variables for action environment. Default None.
        :param runtime_memory: Memory to use to run the function. Default None (loaded from config).
        :param chunk_size: the size of the data chunks to split each object. 'None' for processing
                           the whole file in one function activation.
        :param chunk_n: Number of chunks to split each object. 'None' for processing the whole
                        file in one function activation.
        :param remote_invocation: Enable or disable remote_invocation mechanism. Default 'False'
        :param timeout: Time that the functions have to complete their execution before raising a timeout.
        :param invoke_pool_threads: Number of threads to use to invoke.
        :param include_modules: Explicitly pickle these dependencies.
        :param exclude_modules: Explicitly keep these modules from pickled dependencies.

        :return: A list with size `len(iterdata)` of futures.
        """
        job_id = self._create_job_id('M')
        self.last_call = 'map'

        runtime_meta = self.invoker.select_runtime(job_id, runtime_memory)

        job = create_map_job(self.config,
                             self.internal_storage,
                             self.executor_id,
                             job_id,
                             map_function=map_function,
                             iterdata=map_iterdata,
                             runtime_meta=runtime_meta,
                             runtime_memory=runtime_memory,
                             extra_args=extra_args,
                             extra_env=extra_env,
                             obj_chunk_size=chunk_size,
                             obj_chunk_number=chunk_n,
                             invoke_pool_threads=invoke_pool_threads,
                             include_modules=include_modules,
                             exclude_modules=exclude_modules,
                             execution_timeout=timeout)

        futures = self.invoker.run(job)
        self.futures.extend(futures)

        return futures

    def map_reduce(self,
                   map_function,
                   map_iterdata,
                   reduce_function,
                   extra_args=None,
                   extra_env=None,
                   map_runtime_memory=None,
                   reduce_runtime_memory=None,
                   chunk_size=None,
                   chunk_n=None,
                   timeout=None,
                   invoke_pool_threads=500,
                   reducer_one_per_object=False,
                   reducer_wait_local=False,
                   include_modules=[],
                   exclude_modules=[]):
        """
        Map the map_function over the data and apply the reduce_function across all futures.
        This method is executed all within CF.

        :param map_function: the function to map over the data
        :param map_iterdata:  the function to reduce over the futures
        :param reduce_function:  the function to reduce over the futures
        :param extra_env: Additional environment variables for action environment. Default None.
        :param extra_args: Additional arguments to pass to function activation. Default None.
        :param map_runtime_memory: Memory to use to run the map function. Default None (loaded from config).
        :param reduce_runtime_memory: Memory to use to run the reduce function. Default None (loaded from config).
        :param chunk_size: the size of the data chunks to split each object. 'None' for processing
                           the whole file in one function activation.
        :param chunk_n: Number of chunks to split each object. 'None' for processing the whole
                        file in one function activation.
        :param remote_invocation: Enable or disable remote_invocation mechanism. Default 'False'
        :param timeout: Time that the functions have to complete their execution before raising a timeout.
        :param reducer_one_per_object: Set one reducer per object after running the partitioner
        :param reducer_wait_local: Wait for results locally
        :param invoke_pool_threads: Number of threads to use to invoke.
        :param include_modules: Explicitly pickle these dependencies.
        :param exclude_modules: Explicitly keep these modules from pickled dependencies.

        :return: A list with size `len(map_iterdata)` of futures.
        """
        map_job_id = self._create_job_id('M')
        self.last_call = 'map_reduce'

        runtime_meta = self.invoker.select_runtime(map_job_id,
                                                   map_runtime_memory)

        map_job = create_map_job(self.config,
                                 self.internal_storage,
                                 self.executor_id,
                                 map_job_id,
                                 map_function=map_function,
                                 iterdata=map_iterdata,
                                 runtime_meta=runtime_meta,
                                 runtime_memory=map_runtime_memory,
                                 extra_args=extra_args,
                                 extra_env=extra_env,
                                 obj_chunk_size=chunk_size,
                                 obj_chunk_number=chunk_n,
                                 invoke_pool_threads=invoke_pool_threads,
                                 include_modules=include_modules,
                                 exclude_modules=exclude_modules,
                                 execution_timeout=timeout)

        map_futures = self.invoker.run(map_job)
        self.futures.extend(map_futures)

        if reducer_wait_local:
            self.wait(fs=map_futures)

        reduce_job_id = map_job_id.replace('M', 'R')

        runtime_meta = self.invoker.select_runtime(reduce_job_id,
                                                   reduce_runtime_memory)

        reduce_job = create_reduce_job(
            self.config,
            self.internal_storage,
            self.executor_id,
            reduce_job_id,
            reduce_function,
            map_job,
            map_futures,
            runtime_meta=runtime_meta,
            reducer_one_per_object=reducer_one_per_object,
            runtime_memory=reduce_runtime_memory,
            extra_env=extra_env,
            include_modules=include_modules,
            exclude_modules=exclude_modules)

        reduce_futures = self.invoker.run(reduce_job)

        self.futures.extend(reduce_futures)

        for f in map_futures:
            f._produce_output = False

        return map_futures + reduce_futures

    def wait(self,
             fs=None,
             throw_except=True,
             return_when=ALL_COMPLETED,
             download_results=False,
             timeout=None,
             THREADPOOL_SIZE=128,
             WAIT_DUR_SEC=1):
        """
        Wait for the Future instances (possibly created by different Executor instances)
        given by fs to complete. Returns a named 2-tuple of sets. The first set, named done,
        contains the futures that completed (finished or cancelled futures) before the wait
        completed. The second set, named not_done, contains the futures that did not complete
        (pending or running futures). timeout can be used to control the maximum number of
        seconds to wait before returning.

        :param fs: Futures list. Default None
        :param throw_except: Re-raise exception if call raised. Default True.
        :param return_when: One of `ALL_COMPLETED`, `ANY_COMPLETED`, `ALWAYS`
        :param download_results: Download results. Default false (Only get statuses)
        :param timeout: Timeout of waiting for results.
        :param THREADPOOL_SIZE: Number of threads to use. Default 64
        :param WAIT_DUR_SEC: Time interval between each check.

        :return: `(fs_done, fs_notdone)`
            where `fs_done` is a list of futures that have completed
            and `fs_notdone` is a list of futures that have not completed.
        :rtype: 2-tuple of list
        """
        futures = fs or self.futures
        if type(futures) != list:
            futures = [futures]

        if not futures:
            raise Exception(
                'You must run the call_async(), map() or map_reduce(), or provide'
                ' a list of futures before calling the wait()/get_result() method'
            )

        if download_results:
            msg = 'ExecutorID {} - Getting results...'.format(self.executor_id)
            fs_done = [f for f in futures if f.done]
            fs_not_done = [f for f in futures if not f.done]

        else:
            msg = 'ExecutorID {} - Waiting for functions to complete...'.format(
                self.executor_id)
            fs_done = [f for f in futures if f.ready or f.done]
            fs_not_done = [f for f in futures if not f.ready and not f.done]

        if not fs_not_done:
            return fs_done, fs_not_done

        print(msg) if not self.log_level else logger.info(msg)

        if is_unix_system() and timeout is not None:
            logger.debug(
                'Setting waiting timeout to {} seconds'.format(timeout))
            error_msg = 'Timeout of {} seconds exceeded waiting for function activations to finish'.format(
                timeout)
            signal.signal(signal.SIGALRM, partial(timeout_handler, error_msg))
            signal.alarm(timeout)

        pbar = None
        error = False
        if not self.is_cloudbutton_function and not self.log_level:
            from tqdm.auto import tqdm

            if is_notebook():
                pbar = tqdm(bar_format='{n}/|/ {n_fmt}/{total_fmt}',
                            total=len(fs_not_done))  # ncols=800
            else:
                print()
                pbar = tqdm(bar_format='  {l_bar}{bar}| {n_fmt}/{total_fmt}  ',
                            total=len(fs_not_done),
                            disable=False)

        try:
            if self.rabbitmq_monitor:
                logger.info('Using RabbitMQ to monitor function activations')
                wait_rabbitmq(futures,
                              self.internal_storage,
                              rabbit_amqp_url=self.rabbit_amqp_url,
                              download_results=download_results,
                              throw_except=throw_except,
                              pbar=pbar,
                              return_when=return_when,
                              THREADPOOL_SIZE=THREADPOOL_SIZE)
            else:
                wait_storage(futures,
                             self.internal_storage,
                             download_results=download_results,
                             throw_except=throw_except,
                             return_when=return_when,
                             pbar=pbar,
                             THREADPOOL_SIZE=THREADPOOL_SIZE,
                             WAIT_DUR_SEC=WAIT_DUR_SEC)

        except KeyboardInterrupt:
            if download_results:
                not_dones_call_ids = [(f.job_id, f.call_id) for f in futures
                                      if not f.done]
            else:
                not_dones_call_ids = [(f.job_id, f.call_id) for f in futures
                                      if not f.ready and not f.done]
            msg = ('ExecutorID {} - Cancelled - Total Activations not done: {}'
                   .format(self.executor_id, len(not_dones_call_ids)))
            if pbar:
                pbar.close()
                print()
            print(msg) if not self.log_level else logger.info(msg)
            error = True

        except Exception as e:
            error = True
            raise e

        finally:
            self.invoker.stop()
            if is_unix_system():
                signal.alarm(0)
            if pbar and not pbar.disable:
                pbar.close()
                if not is_notebook():
                    print()
            if self.data_cleaner and not self.is_cloudbutton_function:
                self.clean(cloudobjects=False, force=False, log=False)
            if not fs and error and is_notebook():
                del self.futures[len(self.futures) - len(futures):]

        if download_results:
            fs_done = [f for f in futures if f.done]
            fs_notdone = [f for f in futures if not f.done]
        else:
            fs_done = [f for f in futures if f.ready or f.done]
            fs_notdone = [f for f in futures if not f.ready and not f.done]

        return fs_done, fs_notdone

    def get_result(self,
                   fs=None,
                   throw_except=True,
                   timeout=None,
                   THREADPOOL_SIZE=128,
                   WAIT_DUR_SEC=1):
        """
        For getting the results from all function activations

        :param fs: Futures list. Default None
        :param throw_except: Reraise exception if call raised. Default True.
        :param verbose: Shows some information prints. Default False
        :param timeout: Timeout for waiting for results.
        :param THREADPOOL_SIZE: Number of threads to use. Default 128
        :param WAIT_DUR_SEC: Time interval between each check.

        :return: The result of the future/s
        """
        fs_done, unused_fs_notdone = self.wait(fs=fs,
                                               throw_except=throw_except,
                                               timeout=timeout,
                                               download_results=True,
                                               THREADPOOL_SIZE=THREADPOOL_SIZE,
                                               WAIT_DUR_SEC=WAIT_DUR_SEC)
        result = []
        fs_done = [f for f in fs_done if not f.futures and f._produce_output]
        for f in fs_done:
            if fs:
                # Process futures provided by the user
                result.append(
                    f.result(throw_except=throw_except,
                             internal_storage=self.internal_storage))
            elif not fs and not f._read:
                # Process internally stored futures
                result.append(
                    f.result(throw_except=throw_except,
                             internal_storage=self.internal_storage))
                f._read = True

        logger.debug("ExecutorID {} Finished getting results".format(
            self.executor_id))

        if len(result) == 1 and self.last_call != 'map':
            return result[0]

        return result

    def plot(self, fs=None, dst=None):
        """
        Creates timeline and histogram of the current execution in dst_dir.

        :param dst_dir: destination folder to save .png plots.
        :param dst_file_name: prefix name of the file.
        :param fs: list of futures.
        """
        ftrs = self.futures if not fs else fs

        if type(ftrs) != list:
            ftrs = [ftrs]

        ftrs_to_plot = [f for f in ftrs if (f.ready or f.done) and not f.error]

        if not ftrs_to_plot:
            logger.debug('ExecutorID {} - No futures ready to plot'.format(
                self.executor_id))
            return

        logging.getLogger('matplotlib').setLevel(logging.WARNING)
        from .plots import create_timeline, create_histogram

        msg = 'ExecutorID {} - Creating execution plots'.format(
            self.executor_id)
        print(msg) if not self.log_level else logger.info(msg)

        create_timeline(ftrs_to_plot, dst)
        create_histogram(ftrs_to_plot, dst)

    def clean(self, fs=None, cs=None, cloudobjects=True, force=True, log=True):
        """
        Deletes all the files from COS. These files include the function,
        the data serialization and the function invocation results.
        """
        if cs:
            storage_config = self.internal_storage.get_storage_config()
            delete_cloudobject(list(cs), storage_config)
            if not fs:
                return

        futures = self.futures if not fs else fs
        if type(futures) != list:
            futures = [futures]

        if not futures:
            logger.debug('ExecutorID {} - No jobs to clean'.format(
                self.executor_id))
            return

        if fs or force:
            present_jobs = {(f.executor_id, f.job_id)
                            for f in futures if f.executor_id.count('/') == 1}
            jobs_to_clean = present_jobs
        else:
            present_jobs = {(f.executor_id, f.job_id)
                            for f in futures
                            if f.done and f.executor_id.count('/') == 1}
            jobs_to_clean = present_jobs - self.cleaned_jobs

        if jobs_to_clean:
            msg = "ExecutorID {} - Cleaning temporary data".format(
                self.executor_id)
            print(msg) if not self.log_level and log else logger.info(msg)
            storage_config = self.internal_storage.get_storage_config()
            clean_job(jobs_to_clean,
                      storage_config,
                      clean_cloudobjects=cloudobjects)
            self.cleaned_jobs.update(jobs_to_clean)

    def __exit__(self, exc_type, exc_value, traceback):
        self.invoker.stop()
        if self.data_cleaner:
            self.clean(log=False)
Пример #13
0
class GCPFunctionsBackend:
    def __init__(self, gcp_functions_config):
        self.log_level = os.getenv('CLOUDBUTTON_LOGLEVEL')
        self.name = 'gcp_functions'
        self.gcp_functions_config = gcp_functions_config
        self.package = 'cloudbutton_v' + __version__

        self.region = gcp_functions_config['region']
        self.service_account = gcp_functions_config['service_account']
        self.project = gcp_functions_config['project_name']
        self.credentials_path = gcp_functions_config['credentials_path']
        self.num_retries = gcp_functions_config['retries']
        self.retry_sleeps = gcp_functions_config['retry_sleeps']

        # Instantiate storage client (to upload function bin)
        self.internal_storage = InternalStorage(
            gcp_functions_config['storage'])

        # Setup pubsub client
        try:  # Get credenitals from JSON file
            service_account_info = json.load(open(self.credentials_path))
            credentials = jwt.Credentials.from_service_account_info(
                service_account_info, audience=AUDIENCE)
            credentials_pub = credentials.with_claims(audience=AUDIENCE)
        except Exception:  # Get credentials from gcp function environment
            credentials_pub = None
        self.publisher_client = pubsub_v1.PublisherClient(
            credentials=credentials_pub)

        log_msg = 'Cloudbutton v{} init for GCP Functions - Project: {} - Region: {}'.format(
            __version__, self.project, self.region)
        logger.info(log_msg)

        if not self.log_level:
            print(log_msg)

    def _format_action_name(self, runtime_name, runtime_memory):
        runtime_name = (self.package + '_' + runtime_name).replace('.', '-')
        return '{}_{}MB'.format(runtime_name, runtime_memory)

    def _format_topic_name(self, runtime_name, runtime_memory):
        return self._format_action_name(runtime_name,
                                        runtime_memory) + '_topic'

    def _unformat_action_name(self, action_name):
        split = action_name.split('_')
        runtime_name = split[1].replace('-', '.')
        runtime_memory = int(split[2].replace('MB', ''))
        return runtime_name, runtime_memory

    def _full_function_location(self, function_name):
        return 'projects/{}/locations/{}/functions/{}'.format(
            self.project, self.region, function_name)

    def _full_topic_location(self, topic_name):
        return 'projects/{}/topics/{}'.format(self.project, topic_name)

    def _full_default_location(self):
        return 'projects/{}/locations/{}'.format(self.project, self.region)

    def _encode_payload(self, payload):
        return base64.b64encode(bytes(json.dumps(payload),
                                      'utf-8')).decode('utf-8')

    def _get_auth_session(self):
        credentials = service_account.Credentials.from_service_account_file(
            self.credentials_path, scopes=SCOPES)
        http = httplib2.Http()
        return AuthorizedHttp(credentials, http=http)

    def _get_funct_conn(self):
        http = self._get_auth_session()
        return build('cloudfunctions',
                     FUNCTIONS_API_VERSION,
                     http=http,
                     cache_discovery=False)

    def _get_default_runtime_image_name(self):
        return 'python' + version_str(sys.version_info)

    def _create_handler_zip(self):
        logger.debug("Creating function \
            handler zip in {}".format(ZIP_LOCATION))

        def add_folder_to_zip(zip_file, full_dir_path, sub_dir=''):
            for file in os.listdir(full_dir_path):
                full_path = os.path.join(full_dir_path, file)
                if os.path.isfile(full_path):
                    zip_file.write(full_path,
                                   os.path.join('cloudbutton', sub_dir, file),
                                   zipfile.ZIP_DEFLATED)
                elif os.path.isdir(
                        full_path) and '__pycache__' not in full_path:
                    add_folder_to_zip(zip_file, full_path,
                                      os.path.join(sub_dir, file))

        try:
            with zipfile.ZipFile(ZIP_LOCATION, 'w') as cloudbutton_zip:
                current_location = os.path.dirname(os.path.abspath(__file__))
                module_location = os.path.dirname(
                    os.path.abspath(cloudbutton.__file__))
                main_file = os.path.join(current_location, 'entry_point.py')
                cloudbutton_zip.write(main_file, 'main.py',
                                      zipfile.ZIP_DEFLATED)
                req_file = os.path.join(current_location, 'requirements.txt')
                cloudbutton_zip.write(req_file, 'requirements.txt',
                                      zipfile.ZIP_DEFLATED)
                add_folder_to_zip(cloudbutton_zip, module_location)
        except Exception as e:
            raise Exception('Unable to create the {} package: {}'.format(
                ZIP_LOCATION, e))

    def _create_function(self,
                         runtime_name,
                         memory,
                         code,
                         timeout=60,
                         trigger='HTTP'):
        logger.debug(
            "Creating function {} - Memory: {} Timeout: {} Trigger: {}".format(
                runtime_name, memory, timeout, trigger))
        default_location = self._full_default_location()
        function_location = self._full_function_location(
            self._format_action_name(runtime_name, memory))
        bin_name = self._format_action_name(runtime_name, memory) + '_bin.zip'
        self.internal_storage.put_data(bin_name, code)

        cloud_function = {
            'name':
            function_location,
            'description':
            self.package,
            'entryPoint':
            'main',
            'runtime':
            runtime_name.lower().replace('.', ''),
            'timeout':
            str(timeout) + 's',
            'availableMemoryMb':
            memory,
            'serviceAccountEmail':
            self.service_account,
            'maxInstances':
            0,
            'sourceArchiveUrl':
            'gs://{}/{}'.format(self.internal_storage.bucket, bin_name)
        }

        if trigger == 'HTTP':
            cloud_function['httpsTrigger'] = {}
        elif trigger == 'Pub/Sub':
            topic_location = self._full_topic_location(
                self._format_topic_name(runtime_name, memory))
            cloud_function['eventTrigger'] = {
                'eventType': 'providers/cloud.pubsub/eventTypes/topic.publish',
                'resource': topic_location,
                'failurePolicy': {}
            }

        response = self._get_funct_conn().projects().locations().functions(
        ).create(  # pylint: disable=no-member
            location=default_location,
            body=cloud_function).execute(num_retries=self.num_retries)

        # Wait until function is completely deployed
        while True:
            response = self._get_funct_conn().projects().locations().functions(
            ).get(  # pylint: disable=no-member
                name=function_location).execute(num_retries=self.num_retries)
            if response['status'] == 'ACTIVE':
                break
            else:
                time.sleep(random.choice(self.retry_sleeps))

    def build_runtime(self):
        pass

    def update_runtime(self, runtime_name, code, memory=3008, timeout=900):
        pass

    def create_runtime(self, runtime_name, memory, timeout=60):
        logger.debug("Creating runtime {} - \
            Memory: {} Timeout: {}".format(runtime_name, memory, timeout))

        # Get runtime preinstalls
        runtime_meta = self._generate_runtime_meta(runtime_name)

        # Create topic
        topic_name = self._format_topic_name(runtime_name, memory)
        topic_location = self._full_topic_location(topic_name)
        try:
            # Try getting topic config # pylint: disable=no-member
            self.publisher_client.get_topic(topic_location)
            # If no exception is raised, then the topic exists
            logger.info("Topic {} already exists - Restarting queue...".format(
                topic_location))
            self.publisher_client.delete_topic(topic_location)
        except google.api_core.exceptions.GoogleAPICallError:
            pass
        logger.debug("Creating topic {}...".format(topic_location))
        self.publisher_client.create_topic(topic_location)

        # Create function
        self._create_handler_zip()
        with open(ZIP_LOCATION, "rb") as action_zip:
            action_bin = action_zip.read()

        self._create_function(runtime_name,
                              memory,
                              action_bin,
                              timeout=timeout,
                              trigger='Pub/Sub')

        return runtime_meta

    def delete_runtime(self, runtime_name, runtime_memory):
        function_location = self._full_function_location(
            self._format_action_name(runtime_name, runtime_memory))

        self._get_funct_conn().projects().locations().functions().delete(  # pylint: disable=no-member
            name=function_location, ).execute(num_retries=self.num_retries)

        # Wait until function is completely deleted
        while True:
            try:
                response = self._get_funct_conn().projects().locations(
                ).functions().get(  # pylint: disable=no-member
                    name=function_location).execute(
                        num_retries=self.num_retries)
            except HttpError:
                break
            if response['status'] == 'DELETE_IN_PROGRESS':
                time.sleep(random.choice(self.retry_sleeps))

    def delete_all_runtimes(self):
        runtimes = self.list_runtimes()
        for runtime in runtimes:
            if 'cloudbutton_v' in runtime:
                runtime_name, runtime_memory = self._unformat_action_name(
                    runtime)
                self.delete_runtime(runtime_name, runtime_memory)

    def list_runtimes(self, docker_image_name='all'):
        default_location = self._full_default_location()
        response = self._get_funct_conn().projects().locations().functions(
        ).list(  # pylint: disable=no-member
            location=default_location,
            body={}).execute(num_retries=self.num_retries)

        result = response['Functions'] if 'Functions' in response else []
        return result

    def invoke(self, runtime_name, runtime_memory, payload={}):
        exec_id = payload['executor_id']
        call_id = payload['call_id']
        topic_location = self._full_topic_location(
            self._format_topic_name(runtime_name, runtime_memory))

        start = time.time()
        try:
            # Publish message
            fut = self.publisher_client.publish(
                topic_location, bytes(json.dumps(payload).encode('utf-8')))
            invokation_id = fut.result()
        except Exception as e:
            logger.debug(
                'ExecutorID {} - Function {} invocation failed: {}'.format(
                    exec_id, call_id, str(e)))
            return None

        roundtrip = time.time() - start
        resp_time = format(round(roundtrip, 3), '.3f')

        logger.debug(
            'ExecutorID {} - Function {} invocation done! ({}s) - Activation ID: {}'
            .format(exec_id, call_id, resp_time, invokation_id))

        return (invokation_id)

    def invoke_with_result(self, runtime_name, runtime_memory, payload={}):
        action_name = self._format_action_name(runtime_name, runtime_memory)
        function_location = self._full_function_location(action_name)

        response = self._get_funct_conn().projects().locations().functions(
        ).call(  # pylint: disable=no-member
            name=function_location,
            body={
                'data': json.dumps({'data': self._encode_payload(payload)})
            }).execute(num_retries=self.num_retries)

        return json.loads(response['result'])

    def get_runtime_key(self, runtime_name, runtime_memory):
        action_name = self._format_action_name(runtime_name, runtime_memory)
        runtime_key = os.path.join(self.name, self.region, action_name)

        return runtime_key

    def _generate_runtime_meta(self, runtime_name):
        action_code = """
            import sys
            import pkgutil
            import json

            def main(request):
                runtime_meta = dict()
                mods = list(pkgutil.iter_modules())
                runtime_meta['preinstalls'] = [entry for entry in sorted([[mod, is_pkg] for _, mod, is_pkg in mods])]
                python_version = sys.version_info
                runtime_meta['python_ver'] = str(python_version[0])+"."+str(python_version[1])
                return json.dumps(runtime_meta)
        """
        action_location = os.path.join(tempfile.gettempdir(),
                                       'extract_preinstalls_gcp.py')
        with open(action_location, 'w') as f:
            f.write(textwrap.dedent(action_code))

        modules_zip_action = os.path.join(tempfile.gettempdir(),
                                          'extract_preinstalls_gcp.zip')
        with zipfile.ZipFile(modules_zip_action, 'w') as extract_modules_zip:
            extract_modules_zip.write(action_location, 'main.py')
            extract_modules_zip.close()
        with open(modules_zip_action, 'rb') as modules_zip:
            action_code = modules_zip.read()

        self._create_function(runtime_name, 128, action_code, trigger='HTTP')

        logger.debug(
            "Extracting Python modules list from: {}".format(runtime_name))
        try:
            runtime_meta = self.invoke_with_result(runtime_name, 128)
        except Exception:
            raise ("Unable to invoke 'modules' action")
        try:
            self.delete_runtime(runtime_name, 128)
        except Exception:
            raise ("Unable to delete 'modules' action")

        if not runtime_meta or 'preinstalls' not in runtime_meta:
            raise Exception(runtime_meta)

        return runtime_meta