All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.yahoo.bullet.rest.service.QueryService Maven / Gradle / Ivy

/*
 *  Copyright 2019, Yahoo Inc.
 *  Licensed under the terms of the Apache License, Version 2.0.
 *  See the LICENSE file associated with the project for terms.
 */
package com.yahoo.bullet.rest.service;

import com.yahoo.bullet.common.metrics.MetricCollector;
import com.yahoo.bullet.common.metrics.MetricPublisher;
import com.yahoo.bullet.pubsub.Metadata;
import com.yahoo.bullet.pubsub.PubSubMessage;
import com.yahoo.bullet.pubsub.PubSubMessageSerDe;
import com.yahoo.bullet.pubsub.PubSubResponder;
import com.yahoo.bullet.pubsub.Publisher;
import com.yahoo.bullet.pubsub.Subscriber;
import com.yahoo.bullet.query.Query;
import com.yahoo.bullet.rest.common.MetricManager;
import com.yahoo.bullet.rest.common.PublisherRandomPool;
import com.yahoo.bullet.rest.common.Reader;
import com.yahoo.bullet.rest.common.Utils;
import com.yahoo.bullet.storage.StorageManager;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.PreDestroy;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

@Slf4j
@Component
public class QueryService extends PubSubResponder implements MetricManager {
    private final StorageManager storage;
    private final List responders;
    private final PublisherRandomPool publishers;
    private final List readers;
    private final PubSubMessageSerDe sendSerDe;
    @Getter
    private final boolean metricEnabled;
    @Getter
    private final MetricPublisher metricPublisher;
    @Getter
    private final MetricCollector metricCollector;

    private static final CompletableFuture NONE = CompletableFuture.completedFuture(null);

    static final String QUERY_SUBMIT_SUCCESS = "query.pubsub.submit.success";
    static final String QUERY_SUBMIT_FAIL = "query.pubsub.submit.fail";
    static final String QUERY_ADD_SUCCESS = "query.storage.add.success";
    static final String QUERY_ADD_FAIL = "query.storage.add.fail";
    static final String QUERY_REMOVE_SUCCESS = "query.storage.remove.success";
    static final String QUERY_REMOVE_FAIL = "query.storage.remove.fail";
    static final String QUERY_CLEANUP_SUCCESS = "query.storage.cleanup.success";
    static final String QUERY_CLEANUP_FAIL = "query.storage.cleanup.fail";
    static final String QUERY_RETRIEVE_SUCCESS = "query.storage.retrieve.success";
    static final String QUERY_RETRIEVE_FAIL = "query.storage.retrieve.fail";
    static final String QUERY_KILL_SUCCESS = "query.pubsub.kill.success";
    static final String QUERY_KILL_FAIL = "query.pubsub.kill.fail";
    static final String QUERY_RESPONSE_SUCCESS = "query.pubsub.response.success";
    static final String QUERY_RESPONSE_FAIL = "query.pubsub.response.fail";

    private static final List METRICS =
        Arrays.asList(QUERY_SUBMIT_SUCCESS, QUERY_SUBMIT_FAIL, QUERY_ADD_SUCCESS, QUERY_ADD_FAIL, QUERY_REMOVE_SUCCESS,
                      QUERY_REMOVE_FAIL, QUERY_CLEANUP_SUCCESS, QUERY_CLEANUP_FAIL, QUERY_RETRIEVE_SUCCESS,
                      QUERY_RETRIEVE_FAIL, QUERY_KILL_SUCCESS, QUERY_KILL_FAIL, QUERY_RESPONSE_SUCCESS, QUERY_RESPONSE_FAIL);

    /**
     * Constructor that takes various necessary components.
     *
     * @param storageManager The non-null {@link StorageManager} to use.
     * @param responders The non-empty {@link List} of {@link PubSubResponder} to use.
     * @param publishers The non-empty {@link List} of {@link Publisher} to use.
     * @param subscribers The non-empty {@link List} of {@link Subscriber} to use.
     * @param pubSubMessageSendSerDe The {@link PubSubMessageSerDe} to use for sending messages to the PubSub.
     * @param sleep The time to sleep between checking for messages from the pubsub.
     * @param metricPublisher The optional {@link MetricPublisher} to use to report metrics. Can be null.
     */
    public QueryService(StorageManager storageManager, List responders,
                        List publishers, List subscribers,
                        PubSubMessageSerDe pubSubMessageSendSerDe, int sleep, MetricPublisher metricPublisher) {
        super(null);
        Objects.requireNonNull(storageManager);
        Objects.requireNonNull(responders);
        Objects.requireNonNull(pubSubMessageSendSerDe);
        Utils.checkNotEmpty(publishers);
        Utils.checkNotEmpty(subscribers);
        this.metricEnabled = metricPublisher != null;
        this.metricPublisher = metricPublisher;
        this.metricCollector = new MetricCollector(METRICS);
        this.storage = storageManager;
        this.responders = responders;
        this.sendSerDe = pubSubMessageSendSerDe;
        this.publishers = new PublisherRandomPool(publishers);
        this.readers = subscribers.stream().map(x -> new Reader(x, this, sleep)).collect(Collectors.toList());
        this.readers.forEach(Reader::start);
    }

    /**
     * Submit a query to Bullet and store it in the storage. Unless the publishing succeeds, the query is not stored.
     *
     * @param id The query ID of the query.
     * @param query The query to send.
     * @param queryString The string representation of the query.
     * @return A {@link CompletableFuture} that resolves to the sent {@link PubSubMessage} or null if it could not be sent.
     */
    public CompletableFuture submit(String id, Query query, String queryString) {
        log.debug("Submitting query {}", id);
        PubSubMessage message = sendSerDe.toMessage(id, query, queryString);
        // Publish then store. Publishing might change the message. Store the sent result
        return publish(message).thenComposeAsync(sent -> store(id, sent))
                               .thenApply(sent -> onSubmit(id, sent))
                               .exceptionally(e -> onSubmitFail(e, id));
    }

    /**
     * Submits a {@link Metadata.Signal#KILL} signal to Bullet for the given query ID and removes the query.
     *
     * @param id The query ID to submit the kill signal for.
     * @return A {@link CompletableFuture} that resolves when the kill was finished.
     */
    public CompletableFuture kill(String id) {
        log.debug("Removing metadata for query {} and killing it", id);
        CompletableFuture removed = storage.remove(id);
        return removed.thenAccept(this::onStoredMessageRemove)
                      .exceptionally(e -> onStoredMessageRemoveFail(e, id))
                      .thenAccept(u -> killQuery(id));
    }

    /**
     * Respond to a {@link PubSubMessage}.
     *
     * @param id The id of the query.
     * @param response The {@link PubSubMessage} response.
     */
    public void respond(String id, PubSubMessage response) {
        log.debug("Received response {} for {}", id, response);
        if (Utils.isDone(response)) {
            CompletableFuture removed = storage.remove(id);
            removed.thenAccept(this::onStoredMessageRemoveForResponseSuccess)
                   .exceptionally(e -> onStoredMessageRemoveForResponseFail(e, id, response));
        }
        for (PubSubResponder responder : responders) {
            try {
                responder.respond(id, response);
                this.incrementMetric(QUERY_RESPONSE_SUCCESS);
            } catch (Exception e) {
                log.error("Error while responding for {} using {}", id, responder);
                this.incrementMetric(QUERY_RESPONSE_FAIL);
            }
        }
    }

    /**
     * Sends a {@link Metadata.Signal} to Bullet without storing it.
     *
     * @param id The non-null ID of the message to send this signal in.
     * @param signal The non-null {@link Metadata.Signal} to send.
     * @return A {@link CompletableFuture} that resolves to the sent {@link PubSubMessage} or null if it could not be sent.
     */
    public CompletableFuture send(String id, Metadata.Signal signal) {
        Objects.requireNonNull(signal);
        return publish(sendSerDe.toMessage(new PubSubMessage(id, signal)));
    }

    /**
     * Sends a {@link PubSubMessage} to Bullet without storing it. This can be used to send signals or anything else.
     *
     * @param message The non-null {@link PubSubMessage} to send.
     * @return A {@link CompletableFuture} that resolves to the sent {@link PubSubMessage} or null if it could not be sent.
     */
    public CompletableFuture send(PubSubMessage message) {
        Objects.requireNonNull(message);
        return publish(sendSerDe.toMessage(message));
    }

    /**
     * Retrieves the stored {@link PubSubMessage} of a submitted query.
     *
     * @param id The non-null ID of the query.
     * @return A {@link CompletableFuture} that resolves to the stored {@link PubSubMessage} or null if it could not be found.
     */
    public CompletableFuture get(String id) {
        return storage.get(id)
                      .thenApply(this::onStoredMessageRetrieve)
                      .exceptionally(e -> onStoredMessageRetrieveFail(e, id));
    }

    /**
     * Fires and forgets the metrics using the publisher.
     */
    @Scheduled(fixedDelayString = "${bullet.metric.publish.interval.ms}")
    public void publishMetrics() {
        if (metricEnabled) {
            metricPublisher.fire(metricCollector.extractMetrics());
        }
    }

    /**
     * Stop all service threads and clear pending requests.
     */
    @PreDestroy
    @Override
    public void close() {
        readers.forEach(Reader::close);
        responders.forEach(PubSubResponder::close);
        storage.close();
        publishers.close();
    }

    private CompletableFuture store(String id, PubSubMessage message) {
        if (message == null)  {
            log.error("Could not publish query first. Not storing it {}", message);
            return NONE;
        }
        // TODO: consider sending a kill if an exception happens here. It's technically a leak to the backend
        return storage.put(id, message).thenComposeAsync(result -> onStore(result, id, message));
    }

    private CompletableFuture publish(PubSubMessage message) {
        Publisher publisher = publishers.get();
        try {
            PubSubMessage sent = publisher.send(message);
            return CompletableFuture.completedFuture(sent);
        } catch (Exception e) {
            log.error("Unable to publish message", e);
            return NONE;
        }
    }

    private PubSubMessage killQuery(String id) {
        Publisher publisher = publishers.get();
        PubSubMessage message = new PubSubMessage(id, Metadata.Signal.KILL);
        try {
            log.debug("Sending kill signal for {}", id);
            PubSubMessage sent = publisher.send(sendSerDe.toMessage(message));
            this.incrementMetric(QUERY_KILL_SUCCESS);
            return sent;
        } catch (Exception e) {
            log.error("Could not send message {}", message);
            log.error("Error: ", e);
            this.incrementMetric(QUERY_KILL_FAIL);
            return null;
        }
    }

    private CompletableFuture onStore(Boolean status, String id, PubSubMessage message) {
        if (!status) {
            log.error("Error while trying to store query after submitting. Sending a kill for it...");
            log.error("Sending a kill signal for {}", id);
            this.incrementMetric(QUERY_ADD_FAIL);
            return send(id, Metadata.Signal.KILL).thenApply(d -> null);
        }
        this.incrementMetric(QUERY_ADD_SUCCESS);
        return CompletableFuture.completedFuture(message);
    }

    private PubSubMessage onSubmit(String id, PubSubMessage message) {
        if (message != null) {
            log.debug("Successfully submitted message for {}", id);
            this.incrementMetric(QUERY_SUBMIT_SUCCESS);
            return message;
        } else {
            log.error("Could not submit message for {}", id);
            this.incrementMetric(QUERY_SUBMIT_FAIL);
            return null;
        }
    }

    private PubSubMessage onSubmitFail(Throwable error, String id) {
        log.error("Failed to submit query {} due to failures in storing or publishing the query", id);
        log.error("Received exception", error);
        this.incrementMetric(QUERY_SUBMIT_FAIL);
        return null;
    }

    private PubSubMessage onStoredMessageRetrieve(PubSubMessage message) {
        log.debug("Retrieved message {} from storage", message);
        if (message == null) {
            return null;
        }
        this.incrementMetric(QUERY_RETRIEVE_SUCCESS);
        return sendSerDe.fromMessage(message);
    }

    private PubSubMessage onStoredMessageRetrieveFail(Throwable e, String id) {
        log.error("Exception while trying to retrieve stored message", e);
        log.error("Could not retrieve {} from storage", id);
        this.incrementMetric(QUERY_RETRIEVE_FAIL);
        return null;
    }

    private void onStoredMessageRemove(PubSubMessage message) {
        log.debug("Removed message {} from storage", message);
        if (message != null) {
            this.incrementMetric(QUERY_REMOVE_SUCCESS);
        }
    }

    private Void onStoredMessageRemoveFail(Throwable e, String id) {
        log.error("Exception while trying to remove stored message", e);
        log.error("Could not remove {} from storage", id);
        this.incrementMetric(QUERY_REMOVE_FAIL);
        return null;
    }

    private Void onStoredMessageRemoveForResponseSuccess(PubSubMessage message) {
        log.debug("Removed message {} from storage for response", message);
        if (message != null) {
            this.incrementMetric(QUERY_CLEANUP_SUCCESS);
        }
        return null;
    }

    private Void onStoredMessageRemoveForResponseFail(Throwable e, String id, PubSubMessage response) {
        log.error("Exception while trying to remove stored message", e);
        log.error("Could not remove {} from storage upon receiving {}", id, response);
        this.incrementMetric(QUERY_CLEANUP_FAIL);
        return null;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy