예제 #1
0
class WorkflowTest(TransactionalTestCase):
    """
    Test that workflow conversion methods are valid.
    """
    def setUp(self):
        #        self.clean_database()
        self.test_user = TestFactory.create_user()
        self.test_project = TestFactory.create_project(self.test_user)
        self.old_config_file = cfg.CURRENT_DIR
        cfg.CURRENT_DIR = os.path.dirname(tvb_test.__file__)
        self.workflow_service = WorkflowService()
        self.burst_service = BurstService()
        self.operation_service = OperationService()
        self.flow_service = FlowService()

    def tearDown(self):
        """
        Remove project folders and clean up database.
        """
        FilesHelper().remove_project_structure(self.test_project.name)
        self.delete_project_folders()
        cfg.CURRENT_DIR = self.old_config_file

    def __create_complex_workflow(self, workflow_step_list):
        """
        Creates a burst with a complex workflow with a given list of workflow steps.
        @param workflow_step_list: a lsit of workflow steps that will be used in the
            creation of a new workflow for a new burst
        """
        burst_config = TestFactory.store_burst(self.test_project.id)

        stored_dt = datatypes_factory.DatatypesFactory()._store_datatype(
            Datatype1())

        first_step_algorithm = self.flow_service.get_algorithm_by_module_and_class(
            "tvb_test.adapters.testadapter1", "TestAdapterDatatypeInput")[0]
        metadata = {DataTypeMetaData.KEY_BURST: burst_config.id}
        kwargs = {"test_dt_input": stored_dt.gid, 'test_non_dt_input': '0'}
        operations, group = self.operation_service.prepare_operations(
            self.test_user.id, self.test_project.id, first_step_algorithm,
            first_step_algorithm.algo_group.group_category, metadata, **kwargs)

        workflows = self.workflow_service.create_and_store_workflow(
            project_id=self.test_project.id,
            burst_id=burst_config.id,
            simulator_index=0,
            simulator_id=first_step_algorithm.id,
            operations=operations)
        self.operation_service.prepare_operations_for_workflowsteps(
            workflow_step_list, workflows, self.test_user.id, burst_config.id,
            self.test_project.id, group, operations)
        #fire the first op
        if len(operations) > 0:
            self.operation_service.launch_operation(operations[0].id, False)
        return burst_config.id

    def test_workflow_generation(self):
        """
        A simple test just for the fact that a workflow is created an ran, 
        no dynamic parameters are passed. In this case we create a two steps
        workflow: step1 - tvb_test.adapters.testadapter2.TestAdapter2
                  step2 - tvb_test.adapters.testadapter1.TestAdapter1
        The first adapter doesn't return anything and the second returns one
        tvb.datatypes.datatype1.Datatype1 instance. We check that the steps
        are actually ran by checking that two operations are created and that
        one dataType is stored.
        """
        workflow_step_list = [
            TestFactory.create_workflow_step("tvb_test.adapters.testadapter2",
                                             "TestAdapter2",
                                             static_kwargs={"test2": 2},
                                             step_index=1),
            TestFactory.create_workflow_step("tvb_test.adapters.testadapter1",
                                             "TestAdapter1",
                                             static_kwargs={
                                                 "test1_val1": 1,
                                                 "test1_val2": 1
                                             },
                                             step_index=2)
        ]
        self.__create_complex_workflow(workflow_step_list)
        stored_datatypes = dao.get_datatypes_info_for_project(
            self.test_project.id)
        self.assertTrue(
            len(stored_datatypes) == 2,
            "DataType from second step was not stored.")
        self.assertTrue(stored_datatypes[0][0] == 'Datatype1',
                        "Wrong type was stored.")
        self.assertTrue(stored_datatypes[1][0] == 'Datatype1',
                        "Wrong type was stored.")

        finished, started, error, _ = dao.get_operation_numbers(
            self.test_project.id)
        self.assertEqual(
            finished, 3,
            "Didnt start operations for both adapters in workflow.")
        self.assertEqual(started, 0,
                         "Some operations from workflow didnt finish.")
        self.assertEqual(error, 0,
                         "Some operations finished with error status.")

    def test_workflow_dynamic_params(self):
        """
        A simple test just for the fact that dynamic parameters are passed properly
        between two workflow steps: 
                  step1 - tvb_test.adapters.testadapter1.TestAdapter1
                  step2 - tvb_test.adapters.testadapter3.TestAdapter3
        The first adapter returns a tvb.datatypes.datatype1.Datatype1 instance. 
        The second adapter has this passed as a dynamic workflow parameter.
        We check that the steps are actually ran by checking that two operations 
        are created and that two dataTypes are stored.
        """
        workflow_step_list = [
            TestFactory.create_workflow_step("tvb_test.adapters.testadapter1",
                                             "TestAdapter1",
                                             static_kwargs={
                                                 "test1_val1": 1,
                                                 "test1_val2": 1
                                             },
                                             step_index=1),
            TestFactory.create_workflow_step("tvb_test.adapters.testadapter3",
                                             "TestAdapter3",
                                             dynamic_kwargs={
                                                 "test": {
                                                     wf_cfg.DATATYPE_INDEX_KEY:
                                                     0,
                                                     wf_cfg.STEP_INDEX_KEY: 1
                                                 }
                                             },
                                             step_index=2)
        ]

        self.__create_complex_workflow(workflow_step_list)
        stored_datatypes = dao.get_datatypes_info_for_project(
            self.test_project.id)
        self.assertTrue(
            len(stored_datatypes) == 3,
            "DataType from all step were not stored.")
        for result_row in stored_datatypes:
            self.assertTrue(result_row[0] in ['Datatype1', 'Datatype2'],
                            "Wrong type was stored.")

        finished, started, error, _ = dao.get_operation_numbers(
            self.test_project.id)
        self.assertEqual(
            finished, 3,
            "Didn't start operations for both adapters in workflow.")
        self.assertEqual(started, 0,
                         "Some operations from workflow didn't finish.")
        self.assertEqual(error, 0,
                         "Some operations finished with error status.")

    def test_configuration2workflow(self):
        """
        Test that building a WorflowStep from a WorkflowStepConfiguration. Make sure all the data is 
        correctly passed. Also check that any base_wf_step is incremented to dynamic parameters step index.
        """
        workflow_step = TestFactory.create_workflow_step(
            "tvb_test.adapters.testadapter1",
            "TestAdapter1",
            static_kwargs={"static_param": "test"},
            dynamic_kwargs={
                "dynamic_param": {
                    wf_cfg.STEP_INDEX_KEY: 0,
                    wf_cfg.DATATYPE_INDEX_KEY: 0
                }
            },
            step_index=1,
            base_step=5)
        self.assertEqual(workflow_step.step_index, 1,
                         "Wrong step index in created workflow step.")
        self.assertEqual(workflow_step.static_param, {'static_param': 'test'},
                         'Different static parameters on step.')
        self.assertEqual(
            workflow_step.dynamic_param, {
                'dynamic_param': {
                    wf_cfg.STEP_INDEX_KEY: 5,
                    wf_cfg.DATATYPE_INDEX_KEY: 0
                }
            },
            "Dynamic parameters not saved properly, or base workflow index not added to step index."
        )

    def test_create_workflow(self):
        """
        Test that a workflow with all the associated workflow steps is actually created.
        """
        workflow_step_list = [
            TestFactory.create_workflow_step("tvb_test.adapters.testadapter2",
                                             "TestAdapter2",
                                             static_kwargs={"test2": 2},
                                             step_index=1),
            TestFactory.create_workflow_step("tvb_test.adapters.testadapter1",
                                             "TestAdapter1",
                                             static_kwargs={
                                                 "test1_val1": 1,
                                                 "test1_val2": 1
                                             },
                                             step_index=2)
        ]
        burst_id = self.__create_complex_workflow(workflow_step_list)
        workflow_entities = dao.get_workflows_for_burst(burst_id)
        self.assertTrue(
            len(workflow_entities) == 1,
            "For some reason workflow was not stored in database.")
        workflow_steps = dao.get_workflow_steps(workflow_entities[0].id)
        self.assertEqual(len(workflow_steps),
                         len(workflow_step_list) + 1,
                         "Wrong number of workflow steps created.")
예제 #2
0
class BurstService():
    """
    Service layer for Burst related entities.
    """
    def __init__(self):
        self.operation_service = OperationService()
        self.workflow_service = WorkflowService()
        self.logger = get_logger(self.__class__.__module__)

    def build_portlet_interface(self, portlet_configuration, project_id):
        """
        From a portlet_id and a project_id, first build the portlet
        entity then get it's configurable interface. 
        
        :param portlet_configuration: a portlet configuration entity. It holds at the
            least the portlet_id, and in case any default parameters were saved
            they can be rebuilt from the analyzers // visualizer parameters
        :param project_id: the id of the current project   
            
        :returns: the portlet interface will be of the following form::
            [{'interface': adapter_interface, 
            'prefix': prefix_for_parameter_names, 
            'subalg': {algorithm_field_name: default_algorithm_value},
            'algo_group': algorithm_group,
            'alg_ui_name': displayname},
            ......]
            A list of dictionaries for each adapter that makes up the portlet.
            
        """
        portlet_entity = dao.get_portlet_by_id(
            portlet_configuration.portlet_id)
        if portlet_entity is None:
            raise InvalidPortletConfiguration(
                "No portlet entity located in database with id=%s. "
                "Portlet configuration %s is not valid." %
                (portlet_configuration.portlet_id, portlet_configuration))
        portlet_configurer = PortletConfigurer(portlet_entity)
        portlet_interface = portlet_configurer.get_configurable_interface()
        self.logger.debug("Created interface for portlet " +
                          str([portlet_entity]))

        for adapter_conf in portlet_interface:
            interface = adapter_conf.interface
            interface = FlowService().prepare_parameters(
                interface, project_id, adapter_conf.group.fk_category)
            interface = ABCAdapter.prepare_param_names(interface,
                                                       adapter_conf.prefix)
            adapter_conf.interface = interface

        portlet_configurer.update_default_values(portlet_interface,
                                                 portlet_configuration)
        portlet_configurer.prefix_adapters_parameters(portlet_interface)

        return portlet_interface

    @staticmethod
    def update_portlet_configuration(portlet_configuration,
                                     submited_parameters):
        """
        :param portlet_configuration: the portlet configuration that needs to be updated
        :param submited_parameters: a list of parameters as submitted from the UI. This 
            is a dictionary in the form : 
            {'dynamic' : {name:value pairs}, 'static' : {name:value pairs}}
            
        All names are prefixed with adapter specific generated prefix.
        """
        portlet_entity = dao.get_portlet_by_id(
            portlet_configuration.portlet_id)
        portlet_configurer = PortletConfigurer(portlet_entity)
        return portlet_configurer.update_portlet_configuration(
            portlet_configuration, submited_parameters)

    @staticmethod
    def new_burst_configuration(project_id):
        """
        Return a new burst configuration entity with all the default values.
        """
        burst_configuration = model.BurstConfiguration(project_id)
        burst_configuration.selected_tab = 0
        BurstService.set_default_portlets(burst_configuration)
        return burst_configuration

    @staticmethod
    def set_default_portlets(burst_configuration):
        """
        Sets the default portlets for the specified burst configuration.
        The default portlets are specified in the __init__.py script from tvb root.
        """
        for tab_idx, value in DEFAULT_PORTLETS.items():
            for sel_idx, portlet_identifier in value.items():
                portlet = BurstService.get_portlet_by_identifier(
                    portlet_identifier)
                if portlet is not None:
                    portlet_configuration = BurstService.new_portlet_configuration(
                        portlet.id, tab_idx, sel_idx,
                        portlet.algorithm_identifier)
                    burst_configuration.set_portlet(tab_idx, sel_idx,
                                                    portlet_configuration)

    @staticmethod
    def _store_burst_config(burst_config):
        """
        Store a burst configuration entity.
        """
        burst_config.prepare_before_save()
        saved_entity = dao.store_entity(burst_config)
        return saved_entity.id

    @staticmethod
    def get_available_bursts(project_id):
        """
        Return all the burst for the current project.
        """
        bursts = dao.get_bursts_for_project(
            project_id, page_end=MAX_BURSTS_DISPLAYED) or []
        for burst in bursts:
            burst.prepare_after_load()
        return bursts

    @staticmethod
    def rename_burst(burst_id, new_name):
        """
        Rename the burst given by burst_id, setting it's new name to
        burst_name.
        """
        burst = dao.get_burst_by_id(burst_id)
        burst.name = new_name
        dao.store_entity(burst)

    def load_burst(self, burst_id):
        """
        :param burst_id: the id of the burst that should be loaded
        
        Having this input the method should:
        
            - load the entity from the DB
            - get all the workflow steps for the saved burst id
            - go trough the visualization workflow steps to create the tab 
                configuration of the burst using the tab_index and index_in_tab 
                fields saved on each workflow_step
                
        """
        burst = dao.get_burst_by_id(burst_id)
        burst.prepare_after_load()
        burst.reset_tabs()
        burst_workflows = dao.get_workflows_for_burst(burst.id)

        group_gid = None
        if len(burst_workflows) == 1:
            # A simple burst with no range parameters
            burst = self.__populate_tabs_from_workflow(burst,
                                                       burst_workflows[0])
        elif len(burst_workflows) > 1:
            # A burst workflow with a range of values, created multiple workflows and need
            # to launch parameter space exploration with the resulted group
            self.__populate_tabs_from_workflow(burst, burst_workflows[0])
            executed_steps = dao.get_workflow_steps(burst_workflows[0].id)

            operation = dao.get_operation_by_id(executed_steps[0].fk_operation)
            if operation.operation_group:
                workflow_group = dao.get_datatypegroup_by_op_group_id(
                    operation.operation_group.id)
                group_gid = workflow_group.gid
        return burst, group_gid

    @staticmethod
    def __populate_tabs_from_workflow(burst_entity, workflow):
        """
        Given a burst and a workflow populate the tabs of the burst with the PortletConfigurations
        generated from the steps of the workflow.
        """
        visualizers = dao.get_visualization_steps(workflow.id)
        for entry in visualizers:
            ## For each visualize step, also load all of the analyze steps.
            portlet_cfg = PortletConfiguration(entry.fk_portlet)
            portlet_cfg.set_visualizer(entry)
            analyzers = dao.get_workflow_steps_for_position(
                entry.fk_workflow, entry.tab_index, entry.index_in_tab)
            portlet_cfg.set_analyzers(analyzers)
            burst_entity.tabs[entry.tab_index].portlets[
                entry.index_in_tab] = portlet_cfg
        return burst_entity

    def load_tab_configuration(self, burst_entity, op_id):
        """
        Given a burst entity and an operation id, find the workflow to which the op_id
        belongs and the load the burst_entity's tab configuration with those workflow steps.
        """
        originating_workflow = dao.get_workflow_for_operation_id(op_id)
        burst_entity = self.__populate_tabs_from_workflow(
            burst_entity, originating_workflow)
        return burst_entity

    @staticmethod
    def new_portlet_configuration(portlet_id,
                                  tab_nr=-1,
                                  index_in_tab=-1,
                                  portlet_name='Default'):
        """
        Return a new portlet configuration entitiy with default parameters.
        
        :param portlet_id: the id of the portlet for which a configuration will
            be stored
        :param tab_nr: the index of the currently selected tab
        :param index_in_tab: the index from the currently selected tab
        
        """
        portlet_entity = dao.get_portlet_by_id(portlet_id)
        if portlet_entity is None:
            raise InvalidPortletConfiguration(
                "No portlet entity located in database with id=%s." %
                portlet_id)
        portlet_configurer = PortletConfigurer(portlet_entity)
        configuration = portlet_configurer.create_new_portlet_configuration(
            portlet_name)
        for wf_step in configuration.analyzers:
            wf_step.tab_index = tab_nr
            wf_step.index_in_tab = index_in_tab
        configuration.visualizer.tab_index = tab_nr
        configuration.visualizer.index_in_tab = index_in_tab
        return configuration

    @staticmethod
    def get_available_portlets():
        """
        :returns: a list of all the available portlet entites
        """
        return dao.get_available_portlets()

    @staticmethod
    def get_portlet_by_id(portlet_id):
        """
        :returns: the portlet entity with the id =@portlet_id
        """
        return dao.get_portlet_by_id(portlet_id)

    @staticmethod
    def get_portlet_by_identifier(portlet_identifier):
        """
        :returns: the portlet entity with the algorithm identifier =@portlet_identifier
        """
        return dao.get_portlet_by_identifier(portlet_identifier)

    def launch_burst(self,
                     burst_configuration,
                     simulator_index,
                     simulator_id,
                     user_id,
                     launch_mode='new'):
        """
        Given a burst configuration and all the necessary data do the actual launch.
        
        :param burst_configuration: BurstConfiguration   
        :param simulator_index: the position within the workflows step list that the simulator will take. This is needed
            so that the rest of the portlet workflow steps know what steps do their dynamic parameters come from.
        :param simulator_id: the id of the simulator adapter as stored in the DB. It's needed to load the simulator algo
            group and category that are then passed to the launcher's prepare_operation method.
        :param user_id: the id of the user that launched this burst
        :param launch_mode: new/branch/continue
        """
        ## 1. Prepare BurstConfiguration entity
        if launch_mode == 'new':
            ## Fully new entity for new simulation
            burst_config = burst_configuration.clone()
            if burst_config.name is None:
                new_id = dao.get_max_burst_id() + 1
                burst_config.name = 'simulation_' + str(new_id)
        else:
            ## Branch or Continue simulation
            burst_config = burst_configuration
            simulation_state = dao.get_generic_entity(
                SIMULATION_DATATYPE_MODULE + "." + SIMULATION_DATATYPE_CLASS,
                burst_config.id, "fk_parent_burst")
            if simulation_state is None or len(simulation_state) < 1:
                exc = BurstServiceException(
                    "Simulation state not found for burst_id %s" %
                    burst_config.id)
                self.logger.error(exc)
                raise exc

            simulation_state = simulation_state[0]
            burst_config.update_simulation_parameter("simulation_state",
                                                     simulation_state.gid)
            burst_config = burst_configuration.clone()

            count = dao.count_bursts_with_name(burst_config.name,
                                               burst_config.fk_project)
            burst_config.name = burst_config.name + "_" + launch_mode + str(
                count)

        ## 2. Create Operations and do the actual launch
        if launch_mode in ['new', 'branch']:
            ## New Burst entry in the history
            burst_id = self._store_burst_config(burst_config)
            thread = threading.Thread(target=self._async_launch_and_prepare,
                                      kwargs={
                                          'burst_config': burst_config,
                                          'simulator_index': simulator_index,
                                          'simulator_id': simulator_id,
                                          'user_id': user_id
                                      })
            thread.start()
            return burst_id, burst_config.name
        else:
            ## Continue simulation
            ## TODO
            return burst_config.id, burst_config.name

    @transactional
    def _prepare_operations(self, burst_config, simulator_index, simulator_id,
                            user_id):
        """
        Prepare all required operations for burst launch.
        """
        project_id = burst_config.fk_project
        burst_id = burst_config.id
        workflow_step_list = []
        starting_index = simulator_index + 1

        sim_algo = FlowService().get_algorithm_by_identifier(simulator_id)
        metadata = {DataTypeMetaData.KEY_BURST: burst_id}
        launch_data = burst_config.get_all_simulator_values()[0]
        operations, group = self.operation_service.prepare_operations(
            user_id, project_id, sim_algo, sim_algo.algo_group.group_category,
            metadata, **launch_data)
        group_launched = group is not None
        if group_launched:
            starting_index += 1

        for tab in burst_config.tabs:
            for portlet_cfg in tab.portlets:
                ### For each portlet configuration stored, update the step index ###
                ### and also change the dynamic parameters step indexes to point ###
                ### to the simulator outputs.                                     ##
                if portlet_cfg is not None:
                    analyzers = portlet_cfg.analyzers
                    visualizer = portlet_cfg.visualizer
                    for entry in analyzers:
                        entry.step_index = starting_index
                        self.workflow_service.set_dynamic_step_references(
                            entry, simulator_index)
                        workflow_step_list.append(entry)
                        starting_index += 1
                    ### Change the dynamic parameters to point to the last adapter from this portlet execution.
                    visualizer.step_visible = False
                    if len(workflow_step_list) > 0 and isinstance(
                            workflow_step_list[-1], model.WorkflowStep):
                        self.workflow_service.set_dynamic_step_references(
                            visualizer, workflow_step_list[-1].step_index)
                    else:
                        self.workflow_service.set_dynamic_step_references(
                            visualizer, simulator_index)
                    workflow_step_list.append(visualizer)

        if group_launched:
            ###  For a group of operations, make sure the metric for PSE view
            ### is also computed, immediately after the simulation.
            metric_algo, metric_group = FlowService(
            ).get_algorithm_by_module_and_class(MEASURE_METRICS_MODULE,
                                                MEASURE_METRICS_CLASS)
            _, metric_interface = FlowService().prepare_adapter(
                project_id, metric_group)
            dynamics = {}
            for entry in metric_interface:
                # We have a select that should be the dataType and a select multiple with the
                # required metric algorithms to be evaluated. Only dynamic parameter should be
                # the select type.
                if entry[ABCAdapter.KEY_TYPE] == 'select':
                    dynamics[entry[ABCAdapter.KEY_NAME]] = {
                        WorkflowStepConfiguration.DATATYPE_INDEX_KEY: 0,
                        WorkflowStepConfiguration.STEP_INDEX_KEY:
                        simulator_index
                    }
            metric_step = model.WorkflowStep(algorithm_id=metric_algo.id,
                                             step_index=simulator_index + 1,
                                             static_param={},
                                             dynamic_param=dynamics)
            metric_step.step_visible = False
            workflow_step_list.insert(0, metric_step)

        workflows = self.workflow_service.create_and_store_workflow(
            project_id, burst_id, simulator_index, simulator_id, operations)
        self.operation_service.prepare_operations_for_workflowsteps(
            workflow_step_list, workflows, user_id, burst_id, project_id,
            group, operations)
        operation_ids = [operation.id for operation in operations]
        return operation_ids

    def _async_launch_and_prepare(self, burst_config, simulator_index,
                                  simulator_id, user_id):
        """
        Prepare operations asynchronously.
        """
        try:
            operation_ids = self._prepare_operations(burst_config,
                                                     simulator_index,
                                                     simulator_id, user_id)
            self.logger.debug("Starting a total of %s workflows" %
                              (len(operation_ids, )))
            wf_errs = 0
            for operation_id in operation_ids:
                try:
                    OperationService().launch_operation(operation_id, True)
                except Exception, excep:
                    self.logger.error(excep)
                    wf_errs += 1
                    self.workflow_service.mark_burst_finished(
                        burst_config, error=True, error_message=str(excep))

            self.logger.debug("Finished launching workflows. " +
                              str(len(operation_ids) - wf_errs) +
                              " were launched successfully, " + str(wf_errs) +
                              " had error on pre-launch steps")
        except Exception, excep:
            self.logger.error(excep)
            self.workflow_service.mark_burst_finished(burst_config,
                                                      error=True,
                                                      error_message=str(excep))