/* * Copyright [2017] Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.o19s.es.ltr.action; import com.o19s.es.ltr.action.AddFeaturesToSetAction.AddFeaturesToSetRequest; import com.o19s.es.ltr.action.AddFeaturesToSetAction.AddFeaturesToSetResponse; import com.o19s.es.ltr.action.FeatureStoreAction.FeatureStoreRequest; import com.o19s.es.ltr.feature.FeatureValidation; import com.o19s.es.ltr.feature.store.StorableElement; import com.o19s.es.ltr.feature.store.StoredFeature; import com.o19s.es.ltr.feature.store.StoredFeatureSet; import com.o19s.es.ltr.feature.store.index.IndexFeatureStore; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.get.TransportGetAction; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.TransportSearchAction; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.CountDown; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import static org.elasticsearch.action.ActionListener.wrap; public class TransportAddFeatureToSetAction extends HandledTransportAction<AddFeaturesToSetRequest, AddFeaturesToSetResponse> { private final ClusterService clusterService; private final TransportSearchAction searchAction; private final TransportGetAction getAction; private final TransportFeatureStoreAction featureStoreAction; @Inject public TransportAddFeatureToSetAction(Settings settings, ThreadPool threadPool, TransportService transportService, ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, ClusterService clusterService, TransportSearchAction searchAction, TransportGetAction getAction, TransportFeatureStoreAction featureStoreAction) { super(AddFeaturesToSetAction.NAME, transportService, actionFilters, AddFeaturesToSetRequest::new); this.clusterService = clusterService; this.searchAction = searchAction; this.getAction = getAction; this.featureStoreAction = featureStoreAction; } @Override protected void doExecute(Task task, AddFeaturesToSetRequest request, ActionListener<AddFeaturesToSetResponse> listener) { if (!clusterService.state().routingTable().hasIndex(request.getStore())) { throw new IllegalArgumentException("Store [" + request.getStore() + "] does not exist, please create it first."); } new AsyncAction(task, request, listener, clusterService, searchAction, getAction, featureStoreAction).start(); } /** * Async action that does the following: * - send an async GetRequest to fetch the existing StoreFeatureSet if it exists * - send an async Searchrequest to fetch the features requested * - synchronize on CountDown, the last action to return will trigger the next step * - merge the StoredFeature and the new list of features * - send an async FeatureStoreAction to save the modified (or new) StoredFeatureSet */ private static class AsyncAction { private final Task task; private final String store; private final ActionListener<AddFeaturesToSetResponse> listener; private final String featureNamesQuery; private final List<StoredFeature> features; private final boolean merge; private final String featureSetName; private final String routing; private final AtomicReference<Exception> searchException = new AtomicReference<>(); private final AtomicReference<Exception> getException = new AtomicReference<>(); private final AtomicReference<StoredFeatureSet> setRef = new AtomicReference<>(); private final AtomicReference<List<StoredFeature>> featuresRef = new AtomicReference<>(); private final CountDown countdown; private final AtomicLong version = new AtomicLong(-1L); private final ClusterService clusterService; private final TransportSearchAction searchAction; private final TransportGetAction getAction; private final TransportFeatureStoreAction featureStoreAction; private final FeatureValidation validation; AsyncAction(Task task, AddFeaturesToSetRequest request, ActionListener<AddFeaturesToSetResponse> listener, ClusterService clusterService, TransportSearchAction searchAction, TransportGetAction getAction, TransportFeatureStoreAction featureStoreAction) { this.task = task; this.listener = listener; this.featureSetName = request.getFeatureSet(); this.featureNamesQuery = request.getFeatureNameQuery(); this.features = request.getFeatures(); if (featureNamesQuery != null) { assert features == null || features.isEmpty(); // 2 async actions if we fetch features from store, one otherwize. this.countdown = new CountDown(2); } else { assert features != null && !features.isEmpty(); // 1 async actions if we already have features. this.countdown = new CountDown(1); } this.merge = request.isMerge(); this.store = request.getStore(); this.routing = request.getRouting(); this.clusterService = clusterService; this.searchAction = searchAction; this.getAction = getAction; this.featureStoreAction = featureStoreAction; this.validation = request.getValidation(); } private void start() { if (featureNamesQuery != null) { fetchFeaturesFromStore(); } else { featuresRef.set(features); } GetRequest getRequest = new GetRequest(store) .type(IndexFeatureStore.ES_TYPE) .id(StorableElement.generateId(StoredFeatureSet.TYPE, featureSetName)) .routing(routing); getRequest.setParentTask(clusterService.localNode().getId(), task.getId()); getAction.execute(getRequest, wrap(this::onGetResponse, this::onGetFailure)); } private void fetchFeaturesFromStore() { SearchRequest srequest = new SearchRequest(store); srequest.setParentTask(clusterService.localNode().getId(), task.getId()); QueryBuilder nameQuery; if (featureNamesQuery.endsWith("*")) { String parsed = featureNamesQuery.replaceAll("[*]+$", ""); if (parsed.isEmpty()) { nameQuery = QueryBuilders.matchAllQuery(); } else { nameQuery = QueryBuilders.matchQuery("name.prefix", parsed); } } else { nameQuery = QueryBuilders.matchQuery("name", featureNamesQuery); } BoolQueryBuilder bq = QueryBuilders.boolQuery(); bq.must(nameQuery); bq.must(QueryBuilders.matchQuery("type", StoredFeature.TYPE)); srequest.types(IndexFeatureStore.ES_TYPE); srequest.source().query(bq); srequest.source().fetchSource(true); srequest.source().size(StoredFeatureSet.MAX_FEATURES); searchAction.execute(srequest, wrap(this::onSearchResponse, this::onSearchFailure)); } private void onGetFailure(Exception e) { getException.set(e); maybeFinish(); } private void onSearchFailure(Exception e) { searchException.set(e); maybeFinish(); } private void onGetResponse(GetResponse getResponse) { try { StoredFeatureSet featureSet; if (getResponse.isExists()) { version.set(getResponse.getVersion()); featureSet = IndexFeatureStore.parse(StoredFeatureSet.class, StoredFeatureSet.TYPE, getResponse.getSourceAsBytesRef()); } else { version.set(-1L); featureSet = new StoredFeatureSet(featureSetName, Collections.emptyList()); } setRef.set(featureSet); } catch (Exception e) { getException.set(e); } finally { maybeFinish(); } } private void onSearchResponse(SearchResponse sr) { try { if (sr.getHits().getTotalHits().value > StoredFeatureSet.MAX_FEATURES) { throw new IllegalArgumentException("The feature query [" + featureNamesQuery + "] returns too many features"); } if (sr.getHits().getTotalHits().value == 0) { throw new IllegalArgumentException("The feature query [" + featureNamesQuery + "] returned no features"); } final List<StoredFeature> features = new ArrayList<>(sr.getHits().getHits().length); for (SearchHit hit : sr.getHits().getHits()) { features.add(IndexFeatureStore.parse(StoredFeature.class, StoredFeature.TYPE, hit.getSourceRef())); } featuresRef.set(features); } catch (Exception e) { searchException.set(e); } finally { maybeFinish(); } } private void maybeFinish() { if (!countdown.countDown()) { return; } try { checkErrors(); finishRequest(); } catch (Exception e) { listener.onFailure(e); } } private void finishRequest() throws Exception { assert setRef.get() != null && featuresRef.get() != null; StoredFeatureSet set = setRef.get(); if (merge) { set = set.merge(featuresRef.get()); } else { set = set.append(featuresRef.get()); } updateSet(set); } private void checkErrors() throws Exception { if (searchException.get() == null && getException.get() == null) { return; } Exception sExc = searchException.get(); Exception gExc = getException.get(); final Exception exc; if (sExc != null && gExc != null) { sExc.addSuppressed(gExc); exc = sExc; } else if (sExc != null) { exc = sExc; } else { assert gExc != null; exc = gExc; } throw exc; } private void updateSet(StoredFeatureSet set) { long version = this.version.get(); final FeatureStoreRequest frequest; if (version > 0) { frequest = new FeatureStoreRequest(store, set, version); } else { frequest = new FeatureStoreRequest(store, set, FeatureStoreRequest.Action.CREATE); } frequest.setRouting(routing); frequest.setParentTask(clusterService.localNode().getId(), task.getId()); frequest.setValidation(validation); featureStoreAction.execute(frequest, wrap( (r) -> listener.onResponse(new AddFeaturesToSetResponse(r.getResponse())), listener::onFailure)); } } private static class AsyncFetchSet implements ActionListener<GetResponse> { private ActionListener<AddFeaturesToSetResponse> listener; @Override public void onResponse(GetResponse getFields) { } @Override public void onFailure(Exception e) { listener.onFailure(e); } } }