예제 #1
0
    def reduction_complete(self, message: Message):
        """
        Called when the destination queue was reduction_complete
        Updates the run as complete in the database.
        """
        self._logger.info("Run %s has completed reduction", message.run_number)
        reduction_run = self.find_run(message)

        if not reduction_run:
            raise MissingReductionRunRecord(rb_number=message.rb_number,
                                            run_number=message.run_number,
                                            run_version=message.run_version)

        if not reduction_run.status.value == 'p':  # verbose value = "Processing"
            raise InvalidStateException(
                "An invalid attempt to complete a reduction run that wasn't"
                " processing has been captured. "
                f" Experiment: {message.rb_number},"
                f" Run Number: {message.run_number},"
                f" Run Version {message.run_version}")

        reduction_run.status = self._utils.status.get_completed()
        reduction_run.finished = datetime.datetime.utcnow()
        reduction_run.message = message.message
        reduction_run.reduction_log = message.reduction_log
        reduction_run.admin_log = message.admin_log

        if message.reduction_data is not None:
            for location in message.reduction_data:
                model = db_access.start_database().data_model
                reduction_location = model \
                    .ReductionLocation(file_path=location,
                                       reduction_run=reduction_run)
                db_access.save_record(reduction_location)
        db_access.save_record(reduction_run)
예제 #2
0
 def set_cancelled(run):
     """ Set a run as canceled """
     run.message = "Run cancelled by user"
     run.status = StatusUtils().get_error()
     run.finished = datetime.datetime.utcnow()
     run.retry_when = None
     access.save_record(run)
예제 #3
0
    def reduction_skipped(self, message: Message):
        """
        Called when the destination was reduction skipped
        Updates the run to Skipped status in database
        Will NOT attempt re-run
        """
        if message.message is not None:
            self._logger.info("Run %s has been skipped - %s",
                              message.run_number, message.message)
        else:
            self._logger.info(
                "Run %s has been skipped - No error message was found",
                message.run_number)

        reduction_run = self.find_run(message)
        if not reduction_run:
            raise MissingReductionRunRecord(rb_number=message.rb_number,
                                            run_number=message.run_number,
                                            run_version=message.run_version)

        reduction_run.status = self._utils.status.get_skipped()
        reduction_run.finished = datetime.datetime.utcnow()
        reduction_run.message = message.message
        reduction_run.reduction_log = message.reduction_log
        reduction_run.admin_log = message.admin_log

        db_access.save_record(reduction_run)
예제 #4
0
    def _create_variables(self, instrument, script, variable_dict,
                          is_advanced):
        """ Create variables in the database. """
        variables = []
        for key, value in list(variable_dict.items()):
            str_value = str(value).replace('[', '').replace(']', '')
            if len(str_value) > 300:
                raise DataTooLong
            model = db.start_database().variable_model
            help_text = self._get_help_text('standard_vars', key,
                                            instrument.name, script)
            var_type = VariableUtils().get_type_string(value)
            # Please note: As instrument_variable inherits from Variable, the below creates BOTH an
            # an InstrumentVariable and Variable record in the database when saved. As such,
            # both sets of fields are required for initialisation.
            instrument_variable = model.InstrumentVariable(
                name=key,
                value=str_value,
                type=var_type,
                is_advanced=is_advanced,
                help_text=help_text,
                start_run=0,
                instrument_id=instrument.id,
                tracks_script=1)

            db.save_record(instrument_variable)
            variables.append(instrument_variable)
        return variables
예제 #5
0
    def reduction_started(self, message: Message):
        """
        Called when destination queue was reduction_started.
        Updates the run as started in the database.
        """
        self._logger.info("Run %s has started reduction", message.run_number)

        reduction_run = self.find_run(message=message)

        if not reduction_run:
            raise MissingReductionRunRecord(rb_number=message.rb_number,
                                            run_number=message.run_number,
                                            run_version=message.run_version)

        if reduction_run.status.value not in [
                'e', 'q'
        ]:  # verbose values = ["Error", "Queued"]
            raise InvalidStateException(
                "An invalid attempt to re-start a reduction run was captured."
                f" Experiment: {message.rb_number},"
                f" Run Number: {message.run_number},"
                f" Run Version {message.run_version}")

        reduction_run.status = self._utils.status.get_processing()
        reduction_run.started = datetime.datetime.utcnow()
        db_access.save_record(reduction_run)
예제 #6
0
        def update_variable(old_var):
            """ Update the existing variables. """
            old_var.keep = True
            # Find the new variable from the script.
            matching_vars = [
                variable for variable in defaults
                if old_var.name == variable.name
            ]

            # Check whether we should and can update the old one.
            if matching_vars and old_var.tracks_script:
                new_var = matching_vars[0]
                map(
                    lambda name: setattr(old_var, name, getattr(new_var, name)
                                         ),
                    ["value", "type", "is_advanced", "help_text"]
                )  # Copy the new one's important attributes onto the old variable.
                if save:
                    db.save_record(old_var)
            elif not matching_vars:
                # Or remove the variable if it doesn't exist any more.
                if save:
                    db.save_record(old_var)
                old_var.keep = False
            return old_var
예제 #7
0
 def test_save_record(self):
     """
     Test: .save() is called on the provided object
     When: Calling save_record()
     """
     mock_record = Mock()
     access.save_record(mock_record)
     mock_record.save.assert_called_once()
예제 #8
0
 def save_run_variables(self, instrument_vars, reduction_run):
     """ Save reduction run variables in the database. """
     logger.info('Saving run variables for %s',
                 str(reduction_run.run_number))
     run_variables = map(
         lambda ins_var: self.derive_run_variable(ins_var, reduction_run),
         instrument_vars)
     for run_variable in run_variables:
         access.save_record(run_variable)
     return run_variables
예제 #9
0
def log_error_and_notify(message):
    """
    Helper method to log an error and save a notification
    """
    logger.error(message)
    model = db.start_database().data_model
    notification = model.Notification(is_active=True,
                                      is_staff_only=True,
                                      severity='e',
                                      message=message)
    db.save_record(notification)
예제 #10
0
 def _get_and_activate_db_inst(self, instrument_name):
     """
     Gets the DB instrument record from the database, if one is not
     found it instead creates and saves the record to the DB, then
     returns it.
     """
     # Check if the instrument is active or not in the MySQL database
     instrument = db_access.get_instrument(str(instrument_name),
                                           create=True)
     # Activate the instrument if it is currently set to inactive
     if not instrument.is_active:
         self._logger.info("Activating %s", instrument_name)
         instrument.is_active = 1
         db_access.save_record(instrument)
     return instrument
예제 #11
0
 def copy_metadata(new_var):
     """ Copy the source variable's metadata to the new one. """
     source_var = variables[0]
     model = db.start_database().variable_model
     if isinstance(source_var, model.InstrumentVariable):
         map(
             lambda name: setattr(new_var, name,
                                  getattr(source_var, name)),
             ["instrument", "experiment_reference", "start_run"])
     elif isinstance(source_var, model.RunVariable):
         # Create a run variable.
         VariableUtils().derive_run_variable(new_var,
                                             source_var.reduction_run)
     else:
         return
     db.save_record(new_var)
예제 #12
0
    def cancel_run(reduction_run):
        """
        Try to cancel the run given, or the run that was scheduled as the next retry of the run.
        When we cancel, we send a message to the backend queue processor, telling it to ignore this
        run if it arrives. This is most likely through a delayed message through ActiveMQ's. We
        also set statuses and error messages. If we can't do any of the above, we set the variable
        (retry_run.cancel) that tells the frontend to not schedule another retry if the next run
        fails.
        """
        def set_cancelled(run):
            """ Set a run as canceled """
            run.message = "Run cancelled by user"
            run.status = StatusUtils().get_error()
            run.finished = datetime.datetime.utcnow()
            run.retry_when = None
            access.save_record(run)

        # This is the queued run, send the message to queueProcessor to cancel it
        if reduction_run.status == StatusUtils().get_queued():
            MessagingUtils().send_cancel(reduction_run)
            set_cancelled(reduction_run)

        # Otherwise this run has already failed, and we're looking at a scheduled rerun of it
        # We don't actually have a rerun, so just ensure the retry time is set to "Never" (None)
        elif not reduction_run.retry_run:
            reduction_run.retry_when = None

        # This run is being queued to retry, so send the message to queueProcessor to cancel it,
        # and set it as cancelled
        elif reduction_run.retry_run.status == StatusUtils().get_queued():
            MessagingUtils().send_cancel(reduction_run.retry_run)
            set_cancelled(reduction_run.retry_run)

        # We have a run that's retrying, so just make sure it doesn't retry next time
        elif reduction_run.retry_run.status == StatusUtils().get_processing():
            reduction_run.cancel = True
            reduction_run.retry_run.cancel = True

        # The retry run already completed, so do nothing
        else:
            pass

        # save the run states we modified
        access.save_record(reduction_run)
        if reduction_run.retry_run:
            access.save_record(reduction_run.retry_run)
예제 #13
0
    def reduction_error(self, message: Message):
        """
        Called when the destination was reduction_error.
        Updates the run as complete in the database.
        """
        if message.message:
            self._logger.info("Run %s has encountered an error - %s",
                              message.run_number, message.message)
        else:
            self._logger.info(
                "Run %s has encountered an error - No error message was found",
                message.run_number)

        reduction_run = self.find_run(message)

        if not reduction_run:
            raise MissingReductionRunRecord(rb_number=message.rb_number,
                                            run_number=message.run_number,
                                            run_version=message.run_version)

        reduction_run.status = self._utils.status.get_error()
        reduction_run.finished = datetime.datetime.utcnow()
        reduction_run.message = message.message
        reduction_run.reduction_log = message.reduction_log
        reduction_run.admin_log = message.admin_log
        db_access.save_record(reduction_run)

        if message.retry_in is not None:
            experiment = db_access.get_experiment(message.rb_number)
            max_version = db_access.find_highest_run_version(
                run_number=message.run_number, experiment=experiment)

            # If we have already tried more than 5 times, we want to give up
            # and we don't want
            # to retry the run
            if max_version <= 4:
                self.retry_run(message.started_by, reduction_run,
                               message.retry_in)
            else:
                # Need to delete the retry_in entry from the dictionary so
                # that the front end
                # doesn't report a false retry instance.
                message.retry_in = None
예제 #14
0
    def create_retry_run(user_id,
                         reduction_run,
                         script=None,
                         variables=None,
                         delay=0):
        """
        Create a run ready for re-running based on the run provided. If variables (RunVariable) are
        provided, copy them and associate them with the new one, otherwise use the previous run's.
        If a script (as a string) is supplied then use it, otherwise use the previous run's.
        """
        model = access.start_database().data_model
        # find the previous run version, so we don't create a duplicate
        last_version = access.find_highest_run_version(
            reduction_run.experiment, reduction_run.run_number)

        # get the script to use:
        script_text = script if script is not None else reduction_run.script

        # create the run object and save it
        new_job = model.ReductionRun(run_number=reduction_run.run_number,
                                     run_version=last_version + 1,
                                     run_name="",
                                     experiment=reduction_run.experiment,
                                     instrument=reduction_run.instrument,
                                     script=script_text,
                                     status=StatusUtils().get_queued(),
                                     created=datetime.datetime.utcnow(),
                                     last_updated=datetime.datetime.utcnow(),
                                     message="",
                                     started_by=user_id,
                                     cancel=0,
                                     hidden_in_failviewer=0,
                                     admin_log="",
                                     reduction_log="")

        try:
            access.save_record(new_job)

            reduction_run.retry_run = new_job
            reduction_run.retry_when = \
                datetime.datetime.utcnow() + datetime.timedelta(seconds=delay if delay else 0)
            access.save_record(reduction_run)

            data_locations = model.DataLocation.objects \
                .filter(reduction_run_id=reduction_run.id)

            # copy the previous data locations
            for data_location in data_locations:
                new_data_location = model.DataLocation(
                    file_path=data_location.file_path, reduction_run=new_job)
                access.save_record(new_data_location)

            if variables is not None:
                # associate the variables with the new run
                for var in variables:
                    var.reduction_run = new_job
                    access.save_record(var)
            else:
                # provide variables if they aren't already
                InstrumentVariablesUtils().create_variables_for_run(new_job)

            return new_job

        except:
            new_job.delete()
            raise
예제 #15
0
    def data_ready(self, message: Message):
        """
        Called when destination queue was data_ready.
        Updates the reduction run in the database.
        """
        self._logger.info("Data ready for processing run %s on %s",
                          message.run_number, message.instrument)
        if not validate_rb_number(message.rb_number):
            # rb_number is invalid so send message to skip queue and early return
            message.message = f"Found non-integer RB number: {message.rb_number}"
            self._logger.warning("%s. Skipping %s%s.", message.message,
                                 message.instrument, message.run_number)
            message.rb_number = 0

        run_no = str(message.run_number)
        instrument = self._get_and_activate_db_inst(message.instrument)

        status = self._utils.status.get_skipped() if instrument.is_paused \
            else self._utils.status.get_queued()

        # This must be done before looking up the run version to make sure
        # the record exists
        experiment = db_access.get_experiment(message.rb_number, create=True)
        run_version = db_access.find_highest_run_version(run_number=run_no,
                                                         experiment=experiment)
        run_version += 1
        message.run_version = run_version

        # Get the script text for the current instrument. If the script text
        # is null then send to
        # error queue
        script_text = self._utils. \
            instrument_variable.get_current_script_text(instrument.name)[0]
        if script_text is None:
            self.reduction_error(message)
            raise InvalidStateException(
                "Script text for current instrument is null")

        # Make the new reduction run with the information collected so far
        # and add it into the database
        reduction_run = db_records.create_reduction_run_record(
            experiment=experiment,
            instrument=instrument,
            message=message,
            run_version=run_version,
            script_text=script_text,
            status=status)
        db_access.save_record(reduction_run)

        # Create a new data location entry which has a foreign key linking
        # it to the current
        # reduction run. The file path itself will point to a datafile
        # (e.g. "\isis\inst$\NDXWISH\Instrument\data\cycle_17_1\WISH00038774
        # .nxs")
        data_location = self._data_model.DataLocation(
            file_path=message.data, reduction_run_id=reduction_run.id)
        db_access.save_record(data_location)

        # We now need to create all of the variables for the run such that
        # the script can run
        # through in the desired way
        self._logger.info('Creating variables for run')
        variables = self._utils.instrument_variable.create_variables_for_run(
            reduction_run)
        if not variables:
            self._logger.warning(
                "No instrument variables found on %s for run %s",
                instrument.name, message.run_number)

        self._logger.info('Getting script and arguments')
        reduction_script, arguments = self._utils.reduction_run. \
            get_script_and_arguments(reduction_run)
        message.reduction_script = reduction_script
        message.reduction_arguments = arguments

        # Make sure the RB number is valid
        try:
            message.validate("/queue/DataReady")
        except RuntimeError as validation_err:
            self._logger.error("Validation error from handler: %s",
                               str(validation_err))
            self._client.send_message('/queue/ReductionSkipped', message)
            return

        if instrument.is_paused:
            self._logger.info("Run %s has been skipped", message.run_number)
        else:
            self._client.send_message('/queue/ReductionPending', message)
            self._logger.info("Run %s ready for reduction", message.run_number)
예제 #16
0
    def set_variables_for_runs(self,
                               instrument_name,
                               variables,
                               start_run=0,
                               end_run=None):
        """
        Given a list of variables, we set them to be the variables used for subsequent runs in the
        given run range. If end_run is not supplied, these variables will be ongoing indefinitely.
        If start_run is not supplied, these variables will be set for all run numbers going
        backwards.
        """
        instrument = db.get_instrument(instrument_name)

        # Ensure that the variables we set will be the only ones used for the range given.
        model = db.start_database().variable_model
        applicable_variables = model.InstrumentVariable.objects \
            .filter(instrument_id=instrument.id) \
            .filter(start_run=start_run)

        final_variables = []
        if end_run:
            applicable_variables = applicable_variables.filter(
                start_run__lte=end_run)
            # pylint: disable=no-member
            after_variables = model.InstrumentVariable.objects \
                .filter(instrument_id=instrument.id) \
                .filter(start_run=end_run + 1) \
                .order_by('start_run')

            # pylint: disable=no-member
            previous_variables = model.InstrumentVariable.objects \
                .filter(instrument_id=instrument.id) \
                .filter(start_run__lt=start_run)

            if applicable_variables and not after_variables:
                # The last set of applicable variables extends outside our range.

                # Find the last set.
                final_start = applicable_variables.order_by(
                    '-start_run').first().start_run
                final_variables = list(
                    applicable_variables.filter(start_run=final_start))
                applicable_variables = applicable_variables.exclude(
                    start_run=final_start)  # Don't delete the final set.

            elif not applicable_variables and not after_variables and previous_variables:
                # There is a previous set that applies but doesn't start or end in the range.

                # Find the last set.
                final_start = previous_variables.order_by(
                    '-start_run').first().start_run
                # Set them to apply after our variables.
                # pylint: disable=expression-not-assigned,no-member
                final_variables = list(
                    previous_variables.filter(start_run=final_start))
                [
                    VariableUtils().copy_variable(var).save()
                    for var in final_variables
                ]  # Also copy them to apply before our variables.

            elif not applicable_variables and not after_variables and not previous_variables:
                # There are instrument defaults which apply after our range.
                final_variables = self.get_default_variables(instrument_name)

        # Delete all currently saved variables that apply to the range.
        map(lambda var: var.delete(), applicable_variables)

        # Modify the range of the final set to after the specified range, if there is one.
        for var in final_variables:
            var.start_run = end_run + 1
            db.save_record(var)

        # Then save the new ones.
        for var in variables:
            var.start_run = start_run
            db.save_record(var)