phplib/Job/Search.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace FOO;

/**
 * Class Search_Job
 * Represents a scheduled execution of a Search.
 * @package FOO
 */
class Search_Job extends Job {
    public static $TYPE = 'search';

    public function shouldRetry($date) {
        $search = SearchFinder::getById($this->obj['target_id']);
        return $search && $search->isTimeBased();
    }

    /**
     * Process a single Search.
     * @param bool $commit Whether to save Alerts.
     * @return array An array of Alerts, array of errors and whether failures are ignorable.
     */
    public function run($commit=true) {
        $search = SearchFinder::getById($this->obj['target_id']);
        if(is_null($search)) {
            throw new JobCancelException(sprintf("Search doesn't exist: %d", $this->obj['target_id']));
        }

        return $this->_run($commit, $search);
    }

    /**
     * Process a single Search.
     * @param bool $commit Whether to save Alerts.
     * @param Search $search The Search object.
     * @param bool $disable_search_commit Whether to disable Search updates.
     * @return array An array of Alerts, array of errors and whether failures are ignorable.
     */
    public function _run($commit, Search $search, $disable_search_commit=false) {
        $alerts = [];
        $errors = [];

        // Don't allow saving Alerts if the Search isn't in the DB.
        if($search->isNew()) {
            $commit = false;
        }

        // Whether to update the Search. We only want to do this when the current SearchJob is the newest one available.
        $job = JobFinder::getLastByQuery(['type' => Search_Job::$TYPE, 'target_id' => $search['id']]);
        $search_commit = $commit && (is_null($job) || $this->obj['target_date'] >= $job['target_date']) && !$disable_search_commit;
        if($search_commit) {
            $search['last_status'] = '';
        }

        // Attempt to run the Search. If it fails, we note the error and continue.
        $search_success = false;
        try {
            $alerts = $search->execute($this->obj['target_date']);
            $search_success = true;
        } catch(SearchException $e) {
            $errors[] = sprintf("SearchException: %s", $e->getMessage());
            Logger::except($e);
        } catch(\Exception $e) {
            $errors[] = sprintf("Catch all: %s", $e->getMessage());
            Logger::except($e);
        }

        $this->setCompletion(40);

        $filters = $search->getFilters();
        $targets = $search->getTargets();
        if(!Util::isTesting()) {
            array_unshift($targets, new DB_Target);
        }

        // Take each Alert result and pass it through the pipeline.
        list($final_alerts, $new_errors) = $this->processFilters($alerts, $filters, $this->obj['target_date']);
        $errors = array_merge($errors, $new_errors);
        if($commit) {
            $new_errors = $this->processTargets($final_alerts, $targets, $this->obj['target_date']);
            $errors = array_merge($errors, $new_errors);
        }

        $this->setCompletion(60);

        $prev_success = $search['last_success_date'] === $search['last_execution_date'];
        $curr_success = count($errors) === 0;

        // Send Search-related emails.
        if($search_commit) {
            $cfg = new DBConfig();
            $is_flapping = $search['flap_rate'] > Search::FLAP_THRES;

            if($cfg['error_email_enabled'] && !$is_flapping) {
                $to = $search->getEmails();

                if(!$curr_success && $search['last_error_email_date'] + $cfg['error_email_throttle'] * 60 <= $this->obj['target_date']) {
                    $search['last_error_email_date'] = $this->obj['target_date'];
                    Notification::sendSearchErrorEmail($to, $search, $errors, $this->getDebugData());
                }

                // Send recovery email if state changed from failure to success.
                if(!$prev_success && $curr_success) {
                    Notification::sendSearchRecoveryEmail($to, $search, $this->getDebugData());
                }
            }
        }

        $this->setCompletion(80);

        // Send Alerts email if configured for ondemand notifications and there are Alerts.
        if($commit) {
            if(count($final_alerts) > 0 && $search['notif_type'] == Search::NT_ONDEMAND) {
                Notification::sendAlertEmail(
                    $search->getEmails(),
                    $search,
                    $final_alerts,
                    $search['notif_format'] == Search::NF_CONTENTONLY,
                    $this->getDebugData()
                );
            }
        }

        $this->setCompletion(90);

        // Update the last execution of this search, if it's not new.
        if($search_commit) {
            $search['last_execution_date'] = $this->obj['target_date'];

            // Track last success/failure date.
            if($curr_success) {
                $search['last_status'] = $search['last_status'] ?: sprintf('%d Alerts generated', count($alerts));
                $search['last_success_date'] = $this->obj['target_date'];
            } else {
                $search['last_status'] = implode("\n", $errors);
                $search['last_failure_date'] = $this->obj['target_date'];
            }

            // Track how often the state of this Search changes.
            $search['flap_rate'] = $search['flap_rate'] * (1 - Search::FLAP_WEIGHT) + ($prev_success != $curr_success) * Search::FLAP_WEIGHT;

            $search->store();
        }

        // Record any errors.
        if(!$curr_success) {
            Logger::err('Search error', ['id' => $search['id'], 'job_id' => $this->obj['job_id'], 'ignorable' => $search_success, 'errors' => $errors], self::LOG_NAMESPACE);
        }

        return [$final_alerts, $errors, $search_success];
    }

    public function shouldRun($date) {
        $meta = new DBMeta;
        return (bool) Util::get($meta, sprintf("search_%s", static::$TYPE), true);
    }

    /**
     * Process and Finalize all Filters.
     * @param Alert[] $alerts The Alert object to process.
     * @param Filter[] $filters The list of Filters to use.
     * @param int $date The current date.
     * @return array An array of Alerts and errors.
     */
    private function processFilters(array $alerts, array $filters, $date) {
        $errors = [];

        foreach($filters as $filter) {
            $new_alerts = [];
            // Call process on each Filter.
            foreach($alerts as $alert) {
                try {
                    $new_alerts = array_merge($new_alerts, $filter->process($alert, $date));
                } catch(\Exception $e) {
                    $new_alerts[] = $alert;
                    Logger::err('Filter exception: process', ['exception' => $e->getMessage(), 'filter_id' => $filter['id']], self::LOG_NAMESPACE);
                    $errors[] = sprintf('Filter %s: %s', $filter['type'], $e->getMessage());
                }
            }

            // Call finalize on each Filter.
            try {
                $alerts = array_merge($new_alerts, $filter->finalize($date));
            } catch(\Exception $e) {
                $alerts = $new_alerts;
                Logger::err('Filter exception: finalize', ['exception' => $e->getMessage(), 'filter_id' => $filter['id']], self::LOG_NAMESPACE);
                $errors[] = sprintf('Filter %s: %s', $filter['type'], $e->getMessage());
            }
        }

        return [$alerts, $errors];
    }

    /**
     * Process and Finalize all Targets.
     * @param Alert[] $alerts The Alerts to process.
     * @param Target[] $targets The list of Targets to use.
     * @param int $date The current date.
     * @return string[] An array of errors.
     */
    private function processTargets(array $alerts, array $targets, $date) {
        $errors = [];
        foreach($targets as $target) {
            // Call process on each Target -> Alert.
            foreach($alerts as $alert) {
                try {
                    $target->process($alert, $date);
                } catch(\Exception $e) {
                    Logger::err('Target exception: process', ['exception' => $e->getMessage(), 'target_id' => $target['id']], self::LOG_NAMESPACE);
                    $errors[] = sprintf('Target %s: %s', $target['type'], $e->getMessage());
                }
            }

            // Call finalize on each Target.
            try {
                $target->finalize($date);
            } catch(\Exception $e) {
                Logger::err('Target exception: finalize', ['exception' => $e->getMessage(), 'target_id' => $target['id']], self::LOG_NAMESPACE);
                $errors[] = sprintf('Target %s: %s', $target['type'], $e->getMessage());
            }
        }
        return $errors;
    }
}