com.ikanow.aleph2.graph.titan.services.TitanGraphService.java Source code

Java tutorial

Introduction

Here is the source code for com.ikanow.aleph2.graph.titan.services.TitanGraphService.java

Source

/*******************************************************************************
 * Copyright 2016, The IKANOW Open Source Project.
 *
 * 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.ikanow.aleph2.graph.titan.services;

import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.configuration.ConfigurationMap;
import org.apache.commons.configuration.MapConfiguration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;

import scala.Tuple2;

import com.google.inject.Inject;
import com.google.inject.Module;
import com.ikanow.aleph2.data_model.interfaces.data_import.IEnrichmentBatchModule;
import com.ikanow.aleph2.data_model.interfaces.data_services.IGraphService;
import com.ikanow.aleph2.data_model.interfaces.shared_services.ICrudService;
import com.ikanow.aleph2.data_model.interfaces.shared_services.IDataWriteService;
import com.ikanow.aleph2.data_model.interfaces.shared_services.ICrudService.IReadOnlyCrudService;
import com.ikanow.aleph2.data_model.interfaces.shared_services.IDataServiceProvider.IGenericDataService;
import com.ikanow.aleph2.data_model.interfaces.shared_services.IExtraDependencyLoader;
import com.ikanow.aleph2.data_model.objects.data_import.DataBucketBean;
import com.ikanow.aleph2.data_model.objects.data_import.DataSchemaBean;
import com.ikanow.aleph2.data_model.objects.data_import.DataSchemaBean.GraphSchemaBean;
import com.ikanow.aleph2.data_model.objects.data_import.GraphAnnotationBean;
import com.ikanow.aleph2.data_model.objects.shared.BasicMessageBean;
import com.ikanow.aleph2.data_model.utils.BeanTemplateUtils;
import com.ikanow.aleph2.data_model.utils.Lambdas;
import com.ikanow.aleph2.data_model.utils.ModuleUtils;
import com.ikanow.aleph2.data_model.utils.Optionals;
import com.ikanow.aleph2.data_model.utils.Patterns;
import com.ikanow.aleph2.data_model.utils.Tuples;
import com.ikanow.aleph2.data_model.utils.UuidUtils;
import com.ikanow.aleph2.graph.titan.data_model.TitanGraphConfigBean;
import com.ikanow.aleph2.graph.titan.module.TitanGraphModule;
import com.ikanow.aleph2.graph.titan.utils.ErrorUtils;
import com.thinkaurelius.titan.core.Cardinality;
import com.thinkaurelius.titan.core.PropertyKey;
import com.thinkaurelius.titan.core.TitanEdge;
import com.thinkaurelius.titan.core.TitanFactory;
import com.thinkaurelius.titan.core.TitanGraph;
import com.thinkaurelius.titan.core.TitanTransaction;
import com.thinkaurelius.titan.core.TitanVertex;
import com.thinkaurelius.titan.core.schema.Mapping;
import com.thinkaurelius.titan.core.schema.TitanManagement;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;

/** Titan implementation of the graph service
 * @author Alex
 *
 */
public class TitanGraphService implements IGraphService, IGenericDataService, IExtraDependencyLoader {
    protected static Logger _logger = LogManager.getLogger();

    public static String SEARCH_INDEX_NAME = "search";
    public static String GLOBAL_CREATED_GV = "aleph2_created_gv"; // (_G for graph as opposed to the secondary edge indices, "_ge" for graph-edge)
    public static String GLOBAL_MODIFIED_GV = "aleph2_modified_gv";
    public static String GLOBAL_PATH_INDEX_GV = "aleph2_path_query_gv";
    public static String GLOBAL_PATH_INDEX_GE = "aleph2_path_query_ge";
    public static String GLOBAL_DEFAULT_INDEX_GV = "aleph2_index_query_gv";
    protected static String UUID = System.getProperty("java.io.tmpdir") + "/titan_test_"
            + UuidUtils.get().getRandomUuid();
    protected final TitanGraph _titan;

    protected boolean _USE_ES_FOR_DEDUP_INDEXES = false;

    /** Guice injector
    =    */
    @Inject
    public TitanGraphService(final TitanGraphConfigBean config) {
        _titan = Lambdas.get(() -> {
            try {

                //TODO (ALEPH-15): or allow various overrides using the standard config bean syntax

                //TODO: (ALEPH-15): would be interesting to allow separate table/indexes for certain buckets ("contexts" like in dedup ... eg
                // generate a unique signature for a list of dedup contexts - would need to handle incremental changes, yikes - and then name the backend/ES store based on that) 

                return setup(config);
            } catch (Throwable t) {
                _logger.error(ErrorUtils.getLongForm("Unable to open Titan graph DB: {0}", t));
                return null;
            }
        });
    }

    /** Mock titan c'tor to allow it to use the protected _titan property
     * @param mock
     */
    protected TitanGraphService(final boolean mock) {
        _titan = TitanFactory.build().set("storage.backend", "inmemory")
                .set("index.search.backend", "elasticsearch").set("index.search.elasticsearch.local-mode", true)
                .set("index.search.directory", UUID).set("index.search.cluster-name", UUID)
                .set("index.search.ignore-cluster-name", false).set("index.search.elasticsearch.client-only", false)
                .set("query.force-index", true).open();
    }

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.shared_services.IUnderlyingService#getUnderlyingArtefacts()
     */
    @Override
    public Collection<Object> getUnderlyingArtefacts() {
        //TODO (ALEPH-15): also need ES if ES is enabled (+hbase if hbase is enabled, though going to make the hbase JARs embedded for now), etc (eg Cassandra support)
        return Arrays.asList(this);
    }

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.shared_services.IUnderlyingService#getUnderlyingPlatformDriver(java.lang.Class, java.util.Optional)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> Optional<T> getUnderlyingPlatformDriver(final Class<T> driver_class,
            final Optional<String> maybe_driver_options) {

        return Patterns.match(driver_class).<Optional<T>>andReturn()
                .when(clazz -> IEnrichmentBatchModule.class.isAssignableFrom(clazz) && maybe_driver_options
                        .map(driver_opts -> driver_opts
                                .equals("com.ikanow.aleph2.analytics.services.GraphBuilderEnrichmentService"))
                        .orElse(false), __ -> Optional.<T>of((T) new TitanGraphBuilderEnrichmentService()))
                .when(clazz -> TitanGraph.class.isAssignableFrom(clazz), __ -> Optional.<T>of((T) _titan))
                .otherwise(__ -> Optional.empty());
    }

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.data_services.IGraphService#validateSchema(com.ikanow.aleph2.data_model.objects.data_import.DataSchemaBean.GraphSchemaBean, com.ikanow.aleph2.data_model.objects.data_import.DataBucketBean)
     */
    @Override
    public Tuple2<String, List<BasicMessageBean>> validateSchema(final GraphSchemaBean schema,
            final DataBucketBean bucket) {

        final LinkedList<BasicMessageBean> errors = new LinkedList<>();

        if (Optionals.ofNullable(schema.custom_decomposition_configs()).isEmpty()) {
            errors.add(ErrorUtils.buildErrorMessage(this.getClass().getSimpleName(), "validateSchema",
                    ErrorUtils.DECOMPOSITION_ENRICHMENT_NEEDED, bucket.full_name()));
        }
        if (Optionals.ofNullable(schema.custom_merge_configs()).isEmpty()) {
            errors.add(ErrorUtils.buildErrorMessage(this.getClass().getSimpleName(), "validateSchema",
                    ErrorUtils.MERGE_ENRICHMENT_NEEDED, bucket.full_name()));
        }
        if (!Optionals.ofNullable(schema.deduplication_fields()).isEmpty()) {
            errors.add(ErrorUtils.buildErrorMessage(this.getClass().getSimpleName(), "validateSchema",
                    ErrorUtils.NOT_YET_IMPLEMENTED, "custom:deduplication_fields"));
        }
        if (!Optionals.ofNullable(schema.deduplication_contexts()).isEmpty()) {
            errors.add(ErrorUtils.buildErrorMessage(this.getClass().getSimpleName(), "validateSchema",
                    ErrorUtils.NOT_YET_IMPLEMENTED, "custom:deduplication_contexts"));
        }
        return Tuples._2T("", errors);
    }

    //////////////////////////////////////////////////////////

    // DATA SERVICE PROVIDER / GENERIC DATA SERVICE

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.shared_services.IDataServiceProvider#getDataService()
     */
    @Override
    public Optional<IGenericDataService> getDataService() {
        return Optional.of(this);
    }

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.shared_services.IDataServiceProvider#onPublishOrUpdate(com.ikanow.aleph2.data_model.objects.data_import.DataBucketBean, java.util.Optional, boolean, java.util.Set, java.util.Set)
     */
    @Override
    public CompletableFuture<Collection<BasicMessageBean>> onPublishOrUpdate(DataBucketBean bucket,
            Optional<DataBucketBean> old_bucket, boolean suspended, Set<String> data_services,
            Set<String> previous_data_services) {
        try {
            if (data_services.contains(DataSchemaBean.GraphSchemaBean.name)) {
                return createIndices(bucket, _titan, _USE_ES_FOR_DEDUP_INDEXES);
            } else {
                return CompletableFuture.completedFuture(Collections.emptyList());
            }
        } catch (Throwable t) {
            return CompletableFuture
                    .completedFuture(Arrays.asList(ErrorUtils.buildErrorMessage(this.getClass().getSimpleName(),
                            "onPublishOrUpdate", ErrorUtils.getLongForm("{0}", t))));
        }
    }

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.shared_services.IDataServiceProvider.IGenericDataService#getWritableDataService(java.lang.Class, com.ikanow.aleph2.data_model.objects.data_import.DataBucketBean, java.util.Optional, java.util.Optional)
     */
    @Override
    public <O> Optional<IDataWriteService<O>> getWritableDataService(Class<O> clazz, DataBucketBean bucket,
            Optional<String> options, Optional<String> secondary_buffer) {
        return Optional.empty();
    }

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.shared_services.IDataServiceProvider.IGenericDataService#getReadableCrudService(java.lang.Class, java.util.Collection, java.util.Optional)
     */
    @Override
    public <O> Optional<IReadOnlyCrudService<O>> getReadableCrudService(Class<O> clazz,
            Collection<DataBucketBean> buckets, Optional<String> options) {
        return Optional.empty();
    }

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.shared_services.IDataServiceProvider.IGenericDataService#getUpdatableCrudService(java.lang.Class, java.util.Collection, java.util.Optional)
     */
    @Override
    public <O> Optional<ICrudService<O>> getUpdatableCrudService(Class<O> clazz, Collection<DataBucketBean> buckets,
            Optional<String> options) {
        return Optional.empty();
    }

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.shared_services.IDataServiceProvider.IGenericDataService#getSecondaryBuffers(com.ikanow.aleph2.data_model.objects.data_import.DataBucketBean, java.util.Optional)
     */
    @Override
    public Set<String> getSecondaryBuffers(DataBucketBean bucket, Optional<String> intermediate_step) {
        return Collections.emptySet();
    }

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.shared_services.IDataServiceProvider.IGenericDataService#getPrimaryBufferName(com.ikanow.aleph2.data_model.objects.data_import.DataBucketBean, java.util.Optional)
     */
    @Override
    public Optional<String> getPrimaryBufferName(DataBucketBean bucket, Optional<String> intermediate_step) {
        return Optional.empty();
    }

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.shared_services.IDataServiceProvider.IGenericDataService#switchCrudServiceToPrimaryBuffer(com.ikanow.aleph2.data_model.objects.data_import.DataBucketBean, java.util.Optional, java.util.Optional, java.util.Optional)
     */
    @Override
    public CompletableFuture<BasicMessageBean> switchCrudServiceToPrimaryBuffer(DataBucketBean bucket,
            Optional<String> secondary_buffer, Optional<String> new_name_for_ex_primary,
            Optional<String> intermediate_step) {
        return CompletableFuture.completedFuture(ErrorUtils.buildErrorMessage(this.getClass().getSimpleName(),
                "switchCrudServiceToPrimaryBuffer", ErrorUtils.BUFFERS_NOT_SUPPORTED, bucket.full_name()));
    }

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.shared_services.IDataServiceProvider.IGenericDataService#handleAgeOutRequest(com.ikanow.aleph2.data_model.objects.data_import.DataBucketBean)
     */
    @Override
    public CompletableFuture<BasicMessageBean> handleAgeOutRequest(DataBucketBean bucket) {
        // TODO (ALEPH-15): implement various temporal handling features (don't return error though, just do nothing)
        return CompletableFuture.completedFuture(ErrorUtils.buildSuccessMessage(this.getClass().getSimpleName(),
                "handleAgeOutRequest", ErrorUtils.NOT_YET_IMPLEMENTED, "handleAgeOutRequest"));
    }

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.shared_services.IDataServiceProvider.IGenericDataService#handleBucketDeletionRequest(com.ikanow.aleph2.data_model.objects.data_import.DataBucketBean, java.util.Optional, boolean)
     */
    @Override
    public CompletableFuture<BasicMessageBean> handleBucketDeletionRequest(DataBucketBean bucket,
            Optional<String> secondary_buffer, boolean bucket_or_buffer_getting_deleted) {

        //(this first call just ensures indexes are present)
        return createIndices(bucket, _titan, _USE_ES_FOR_DEDUP_INDEXES).thenCompose(__ -> this
                .handleBucketDeletionRequest_internal(bucket, secondary_buffer, bucket_or_buffer_getting_deleted));
    }

    /** Deletes a bucket
     * @param bucket
     * @param secondary_buffer
     * @param bucket_or_buffer_getting_deleted
     * @return
     */
    private CompletableFuture<BasicMessageBean> handleBucketDeletionRequest_internal(DataBucketBean bucket,
            Optional<String> secondary_buffer, boolean bucket_or_buffer_getting_deleted) {

        //TODO (ALEPH-15): check if the indexes exist - just return if so

        if (secondary_buffer.isPresent()) {
            return CompletableFuture.completedFuture(ErrorUtils.buildErrorMessage(this.getClass().getSimpleName(),
                    "handleBucketDeletionRequest", ErrorUtils.BUFFERS_NOT_SUPPORTED, bucket.full_name()));
        }

        //TODO (ALEPH-15): At some point need to be able for services to (optionally) request batch enrichment jobs - eg would be much nicer to fire this off as a distributed job

        return CompletableFuture.runAsync(() -> {

            try {
                Thread.sleep(1000L);
            } catch (Exception e) {
            } // just check the indexes have refreshed...

            final TitanTransaction tx = _titan.buildTransaction().start();

            //DEBUG
            //final com.fasterxml.jackson.databind.ObjectMapper titan_mapper = _titan.io(org.apache.tinkerpop.gremlin.structure.io.IoCore.graphson()).mapper().create().createMapper();

            @SuppressWarnings("unchecked")
            final Stream<TitanVertex> vertices_to_check = Optionals.<TitanVertex>streamOf(
                    tx.query().has(GraphAnnotationBean.a2_p, bucket.full_name()).vertices(), false);
            vertices_to_check.forEach(v -> {
                {
                    final Iterator<VertexProperty<String>> props = v.<String>properties(GraphAnnotationBean.a2_p);
                    while (props.hasNext()) {
                        final VertexProperty<String> prop = props.next();
                        if (bucket.full_name().equals(prop.value())) {
                            prop.remove();
                        }
                    }
                }
                {
                    final Iterator<VertexProperty<String>> props = v.<String>properties(GraphAnnotationBean.a2_p);
                    if (!props.hasNext()) { // can delete this bucket
                        v.remove();
                    }
                }
            });
            @SuppressWarnings("unchecked")
            final Stream<TitanEdge> edges_to_check = Optionals.<TitanEdge>streamOf(
                    tx.query().has(GraphAnnotationBean.a2_p, bucket.full_name()).edges(), false);
            edges_to_check.forEach(e -> {
                e.remove(); // (can only have one edge so delete it)
            });

            tx.commit();
        }).thenApply(__ -> ErrorUtils.buildSuccessMessage(this.getClass().getSimpleName(),
                "handleBucketDeletionRequest", "Completed", "handleBucketDeletionRequest"))
                .exceptionally(t -> ErrorUtils.buildErrorMessage(this.getClass().getSimpleName(),
                        "handleBucketDeletionRequest", ErrorUtils.getLongForm("{0}", t),
                        "handleBucketDeletionRequest"));

    }

    //////////////////////////////////////////////////////

    // Worker utils

    /** Utility method for building the graph indices
     * @param bucket
     * @return
     */
    public static CompletableFuture<Collection<BasicMessageBean>> createIndices(DataBucketBean bucket,
            TitanGraph titan, boolean use_es_for_dedup_indices) {

        final TitanManagement mgmt = titan.openManagement();

        // First off, let's ensure that a2_p is indexed: (note these apply to both vertixes and edges)
        try {
            final PropertyKey bucket_index = mgmt.makePropertyKey(GraphAnnotationBean.a2_p).dataType(String.class)
                    .cardinality(Cardinality.SET).make();
            try {
                mgmt.buildIndex(GLOBAL_PATH_INDEX_GV, Vertex.class)
                        .addKey(bucket_index, Mapping.STRING.asParameter()).buildMixedIndex(SEARCH_INDEX_NAME);
            } catch (IllegalArgumentException e) {
                //DEBUG
                //_logger.error(ErrorUtils.getLongForm("{0}", e));
                //e.printStackTrace();
            } // (already indexed, this is fine/expected)            
            try {
                mgmt.buildIndex(GLOBAL_PATH_INDEX_GE, Edge.class).addKey(bucket_index, Mapping.STRING.asParameter())
                        .buildMixedIndex(SEARCH_INDEX_NAME);
            } catch (IllegalArgumentException e) {
                //DEBUG
                //_logger.error(ErrorUtils.getLongForm("{0}", e));
                //e.printStackTrace();
            } // (already indexed, this is fine/expected)
        } catch (IllegalArgumentException e) {
            //DEBUG
            //e.printStackTrace();
        } // (already indexed, this is fine/expected)

        // Created/modified
        try {
            mgmt.buildIndex(GLOBAL_CREATED_GV, Vertex.class)
                    .addKey(mgmt.makePropertyKey(GraphAnnotationBean.a2_tc).dataType(Long.class).make())
                    .buildMixedIndex(SEARCH_INDEX_NAME);
        } catch (IllegalArgumentException e) {
            //DEBUG
            //_logger.error(ErrorUtils.getLongForm("{0}", e));
            //e.printStackTrace();

        } // (already indexed, this is fine/expected)            
        try {
            mgmt.buildIndex(GLOBAL_MODIFIED_GV, Vertex.class)
                    .addKey(mgmt.makePropertyKey(GraphAnnotationBean.a2_tm).dataType(Long.class).make())
                    .buildMixedIndex(SEARCH_INDEX_NAME);
        } catch (IllegalArgumentException e) {
            //DEBUG
            //_logger.error(ErrorUtils.getLongForm("{0}", e));
            //e.printStackTrace();

        } // (already indexed, this is fine/expected)

        // Then check that the global default index is set
        Optional<List<String>> maybe_dedup_fields = Optionals
                .of(() -> bucket.data_schema().graph_schema().deduplication_fields());
        final Collection<BasicMessageBean> ret_val = maybe_dedup_fields.map(dedup_fields -> {
            //TODO (ALEPH-15): manage the index pointed to by the bucket's signature               
            return Arrays.asList(ErrorUtils.buildErrorMessage(TitanGraph.class.getSimpleName(), "onPublishOrUpdate",
                    ErrorUtils.NOT_YET_IMPLEMENTED, "custom:deduplication_fields"));
        }).orElseGet(() -> {
            try {
                //TODO (ALEPH-15): There's a slightly tricky decision here...
                // using ES makes dedup actions "very" no longer transactional because of the index refresh
                // (in theory using Cassandra or HBase makes things transactional, though I haven't tested that)
                // Conversely not using ES puts more of the load on the smaller Cassandra/HBase clusters                  
                // Need to make configurable, but default to using the transactional layer
                // Of course, if I move to eg unipop later on then I'll have to fix this
                // It's not really any different to deduplication _except_ it can happen across buckets (unlike dedup) so it's much harder to stop
                // A few possibilities:
                // 1) Have a job that runs on new-ish data (ofc that might not be easy to detect) that merges things (ofc very unclear how to do that)
                // 2) Centralize all insert actions
                // Ah here's some more interest - looks like Hbase and Cassandra's eventual consistency can cause duplicates:
                // http://s3.thinkaurelius.com/docs/titan/1.0.0/eventual-consistency.html ... tldr: basically need to have regular "clean up jobs" and live with transient issues
                // (or use a consistent data store - would also require a decent amount of code here because our dedup strategy is not perfect)

                final Function<String, PropertyKey> getOrCreateProperty = field -> Optional
                        .ofNullable(mgmt.getPropertyKey(field))
                        .orElseGet(() -> mgmt.makePropertyKey(field).dataType(String.class).make());

                final PropertyKey name_index = getOrCreateProperty.apply(GraphAnnotationBean.name);
                final PropertyKey type_index = getOrCreateProperty.apply(GraphAnnotationBean.type);

                if (use_es_for_dedup_indices) {
                    mgmt.buildIndex(GLOBAL_DEFAULT_INDEX_GV, Vertex.class)
                            .addKey(name_index, Mapping.TEXTSTRING.asParameter())
                            .addKey(type_index, Mapping.STRING.asParameter()).buildMixedIndex(SEARCH_INDEX_NAME);
                } else { // use the storage backend which should have better consistency properties
                    mgmt.buildIndex(GLOBAL_DEFAULT_INDEX_GV, Vertex.class).addKey(name_index).addKey(type_index)
                            //TODO (ALEPH-15: make this unique()? and then have multiple contexts, either via property or lots of graphs?
                            .buildCompositeIndex();

                    //(in this case, also index "name" as an ES field to make it easier to search over)
                    mgmt.buildIndex(GLOBAL_DEFAULT_INDEX_GV + "_TEXT", Vertex.class)
                            .addKey(name_index, Mapping.TEXT.asParameter()).buildMixedIndex(SEARCH_INDEX_NAME);
                }
            } catch (IllegalArgumentException e) {
                //DEBUG
                //_logger.error(ErrorUtils.getLongForm("{0}", e));
                //e.printStackTrace();

            } // (already indexed, this is fine/expected)
            return Collections.emptyList();
        });

        //TODO (ALEPH-15): allow other indexes (etc) via the schema technology override      

        // Complete management transaction
        mgmt.commit();

        //TODO (ALEPH-15): want to complete the future only once the indexing steps are done?
        return CompletableFuture.completedFuture(ret_val);
    }

    //////////////////////////////////////////////////////

    // Configuration utils

    /** This service needs to load some additional classes via Guice. Here's the module that defines the bindings
     * @return
     */
    public static List<Module> getExtraDependencyModules() {
        return Arrays.asList((Module) new TitanGraphModule());
    }

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.shared_services.IExtraDependencyLoader#youNeedToImplementTheStaticFunctionCalled_getExtraDependencyModules()
     */
    @Override
    public void youNeedToImplementTheStaticFunctionCalled_getExtraDependencyModules() {
        // (done see above)      
    }

    /* (non-Javadoc)
     * @see com.ikanow.aleph2.data_model.interfaces.shared_services.IUnderlyingService#createRemoteConfig(com.typesafe.config.Config)
     */
    @SuppressWarnings("unchecked")
    @Override
    public Config createRemoteConfig(Optional<DataBucketBean> maybe_bucket, Config local_config) {
        if (null == _titan)
            return local_config; // (titan is disabled, just pass through)

        final Config distributed_config = ConfigFactory.parseMap(
                (AbstractMap<String, ?>) (AbstractMap<?, ?>) new ConfigurationMap(_titan.configuration()));

        return local_config.withValue(TitanGraphConfigBean.PROPERTIES_ROOT + "."
                + BeanTemplateUtils.from(TitanGraphConfigBean.class).field(TitanGraphConfigBean::config_override),
                distributed_config.root());
    }

    /** Builds a Titan graph from the config bean
     * @param config
     * @return
     */
    protected TitanGraph setup(TitanGraphConfigBean config) {
        if (null != config.config_override()) {
            // First denest:
            final Config denested = ConfigFactory.parseMap(config.config_override());
            // Then build
            return TitanFactory.open(new MapConfiguration(denested.entrySet().stream()
                    .collect(Collectors.toMap(kv -> kv.getKey(), kv -> kv.getValue().unwrapped()))));
        } else {
            final String path = Optional.of(config.config_path_name())
                    .map(p -> (p.matches("^[a-zA-Z]:.*") || p.startsWith(".") || p.startsWith("/")) ? p
                            : ModuleUtils.getGlobalProperties().local_yarn_config_dir() + "/" + p)
                    .get();
            return TitanFactory.open(path);
        }
    }
}