/*
* Copyright (c) 2017 Strapdata (http://www.strapdata.com)
* Contains some code from Elasticsearch (http://www.elastic.co)
*
* 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 org.elassandra.index;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.nio.ByteBuffer;
import java.nio.file.DirectoryIteratorException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableSet;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.cql3.CQL3Type;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.cql3.statements.IndexTarget;
import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.DeletionTime;
import org.apache.cassandra.db.PartitionColumns;
import org.apache.cassandra.db.RangeTombstone;
import org.apache.cassandra.db.ReadCommand;
import org.apache.cassandra.db.SinglePartitionReadCommand;
import org.apache.cassandra.db.Slice;
import org.apache.cassandra.db.Slice.Bound;
import org.apache.cassandra.db.filter.RowFilter;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.CollectionType;
import org.apache.cassandra.db.marshal.CompositeType;
import org.apache.cassandra.db.marshal.ListType;
import org.apache.cassandra.db.marshal.MapType;
import org.apache.cassandra.db.marshal.SetType;
import org.apache.cassandra.db.partitions.PartitionIterator;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.CellPath;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.rows.RowIterator;
import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.db.rows.UnfilteredRowIterators;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.index.Index;
import org.apache.cassandra.index.IndexRegistry;
import org.apache.cassandra.index.transactions.IndexTransaction;
import org.apache.cassandra.index.transactions.IndexTransaction.Type;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.schema.IndexMetadata;
import org.apache.cassandra.service.ElassandraDaemon;
import org.apache.cassandra.utils.Pair;
import org.apache.cassandra.utils.concurrent.OpOrder;
import org.apache.cassandra.utils.concurrent.OpOrder.Group;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.document.BinaryDocValuesField;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.NumericRangeQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TermRangeQuery;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.CloseableThreadLocal;
import org.elassandra.cluster.InternalCassandraClusterService;
import org.elassandra.index.mapper.internal.TokenFieldMapper;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.admin.indices.flush.FlushRequest;
import org.elasticsearch.cluster.ClusterChangedEvent;
import org.elasticsearch.cluster.ClusterService;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.ClusterStateListener;
import org.elasticsearch.cluster.block.ClusterBlockException;
import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.metadata.IndexMetaData;
import org.elasticsearch.cluster.metadata.MappingMetaData;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.builders.ShapeBuilder;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.lucene.all.AllEntries;
import org.elasticsearch.common.lucene.uid.Versions;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.index.IndexService;
import org.elasticsearch.index.VersionType;
import org.elasticsearch.index.analysis.AnalysisService;
import org.elasticsearch.index.engine.Engine;
import org.elasticsearch.index.engine.Engine.DeleteByQuery;
import org.elasticsearch.index.engine.Engine.Operation;
import org.elasticsearch.index.mapper.ContentPath;
import org.elasticsearch.index.mapper.DocumentMapper;
import org.elasticsearch.index.mapper.DocumentMapperParser;
import org.elasticsearch.index.mapper.FieldMapper;
import org.elasticsearch.index.mapper.Mapper;
import org.elasticsearch.index.mapper.Mapper.BuilderContext;
import org.elasticsearch.index.mapper.MapperService;
import org.elasticsearch.index.mapper.Mapping;
import org.elasticsearch.index.mapper.MetadataFieldMapper;
import org.elasticsearch.index.mapper.ParseContext;
import org.elasticsearch.index.mapper.ParseContext.Document;
import org.elasticsearch.index.mapper.ParsedDocument;
import org.elasticsearch.index.mapper.SourceToParse;
import org.elasticsearch.index.mapper.Uid;
import org.elasticsearch.index.mapper.core.TypeParsers;
import org.elasticsearch.index.mapper.geo.BaseGeoPointFieldMapper;
import org.elasticsearch.index.mapper.geo.GeoPointFieldMapper;
import org.elasticsearch.index.mapper.geo.GeoPointFieldMapperLegacy;
import org.elasticsearch.index.mapper.geo.GeoShapeFieldMapper;
import org.elasticsearch.index.mapper.internal.IdFieldMapper;
import org.elasticsearch.index.mapper.internal.ParentFieldMapper;
import org.elasticsearch.index.mapper.internal.SourceFieldMapper;
import org.elasticsearch.index.mapper.internal.TypeFieldMapper;
import org.elasticsearch.index.mapper.internal.UidFieldMapper;
import org.elasticsearch.index.mapper.internal.UidFieldMapper.Defaults;
import org.elasticsearch.index.mapper.internal.VersionFieldMapper;
import org.elasticsearch.index.mapper.object.DynamicTemplate;
import org.elasticsearch.index.mapper.object.ObjectMapper;
import org.elasticsearch.index.mapper.object.RootObjectMapper;
import org.elasticsearch.index.shard.IndexShard;
import org.elasticsearch.index.shard.IndexShardState;
import org.elasticsearch.indices.IndicesService;
import org.elasticsearch.percolator.PercolatorService;
import com.carrotsearch.hppc.ObjectIntHashMap;
import com.carrotsearch.hppc.cursors.ObjectCursor;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* Custom secondary index for CQL3 only, should be created when mapping is applied and local shard started.
* Index rows as documents when Elasticsearch clusterState has no write blocks and local shard is started.
*
* @author vroyer
*
*/
public class ElasticSecondaryIndex implements Index, ClusterStateListener {
private final static SourceToParse EMPTY_SOURCE_TO_PARSE= SourceToParse.source((XContentParser)null);
private final static Field DEFAULT_INTERNAL_VERSION = new NumericDocValuesField(VersionFieldMapper.NAME, -1L);
private final static Field DEFAULT_EXTERNAL_VERSION = new NumericDocValuesField(VersionFieldMapper.NAME, 1L);
public static final Map<String, ElasticSecondaryIndex> elasticSecondayIndices = Maps.newConcurrentMap();
public static final Pattern TARGET_REGEX = Pattern.compile("^(keys|entries|values|full)\\((.+)\\)$");
public static boolean runsElassandra = false;
public static boolean userKeyspaceInitialized = false;
final String index_name; // keyspace_name.table_name
final ESLogger logger;
final ClusterService clusterService;
// updated when create/open/close/remove an ES index.
protected final ReadWriteLock mappingInfoLock = new ReentrantReadWriteLock();
protected volatile ImmutableMappingInfo mappingInfo;
protected final ColumnFamilyStore baseCfs;
protected final IndexMetadata indexMetadata;
protected final Set<ColumnDefinition> indexedColumns = Sets.newConcurrentHashSet();
protected final AtomicBoolean initialized = new AtomicBoolean(false);
ElasticSecondaryIndex(ColumnFamilyStore baseCfs, IndexMetadata indexDef) {
this.baseCfs = baseCfs;
this.indexMetadata = indexDef;
this.index_name = baseCfs.keyspace.getName()+"."+baseCfs.name;
this.logger = Loggers.getLogger(this.getClass().getName()+"."+baseCfs.keyspace.getName()+"."+baseCfs.name);
// clusterService must be started before creating 2i.
this.clusterService = ElassandraDaemon.injector().getInstance(ClusterService.class);
this.clusterService.addPost(this);
}
public static ElasticSecondaryIndex newElasticSecondaryIndex(ColumnFamilyStore baseCfs, IndexMetadata indexDef) {
ElasticSecondaryIndex esi = elasticSecondayIndices.computeIfAbsent(baseCfs.keyspace.getName()+"."+baseCfs.name, K -> new ElasticSecondaryIndex(baseCfs, indexDef));
esi.indexedColumns.add(parseTarget(baseCfs.metadata, indexDef).left);
return esi;
}
// Public because it's also used to convert index metadata into a thrift-compatible format
public static Pair<ColumnDefinition, IndexTarget.Type> parseTarget(CFMetaData cfm, IndexMetadata indexDef)
{
String target = indexDef.options.get("target");
assert target != null : String.format(Locale.ROOT,"No target definition found for index %s", indexDef.name);
// if the regex matches then the target is in the form "keys(foo)", "entries(bar)" etc
// if not, then it must be a simple column name and implictly its type is VALUES
Matcher matcher = TARGET_REGEX.matcher(target);
String columnName;
IndexTarget.Type targetType;
if (matcher.matches())
{
targetType = IndexTarget.Type.fromString(matcher.group(1));
columnName = matcher.group(2);
}
else
{
columnName = target;
targetType = IndexTarget.Type.VALUES;
}
// in the case of a quoted column name the name in the target string
// will be enclosed in quotes, which we need to unwrap. It may also
// include quote characters internally, escaped like so:
// abc"def -> abc""def.
// Because the target string is stored in a CQL compatible form, we
// need to un-escape any such quotes to get the actual column name
if (columnName.startsWith("\""))
{
columnName = StringUtils.substring(StringUtils.substring(columnName, 1), 0, -1);
columnName = columnName.replaceAll("\"\"", "\"");
}
// if it's not a CQL table, we can't assume that the column name is utf8, so
// in that case we have to do a linear scan of the cfm's columns to get the matching one
if (cfm.isCQLTable())
return Pair.create(cfm.getColumnDefinition(new ColumnIdentifier(columnName, true)), targetType);
else
for (ColumnDefinition column : cfm.allColumns())
if (column.name.toString().equals(columnName))
return Pair.create(column, targetType);
throw new RuntimeException(String.format(Locale.ROOT,"Unable to parse targets for index %s (%s)", indexDef.name, target));
}
// reusable per thread context
private CloseableThreadLocal<Context> perThreadContext = new CloseableThreadLocal<Context>() {
@Override
protected Context initialValue() {
return new Context();
}
};
abstract class FilterableDocument extends ParseContext.Document implements Predicate<IndexableField> {
boolean applyFilter = false;
public FilterableDocument(String path, Document parent) {
super(path, parent);
}
public FilterableDocument() {
super();
}
public void applyFilter(boolean apply) {
applyFilter = apply;
}
@Override
abstract public boolean apply(IndexableField input);
@Override
public Iterator<IndexableField> iterator() {
if (applyFilter) {
return Iterators.filter(super.iterator(), this);
} else {
return super.iterator();
}
}
}
class Context extends ParseContext {
private ImmutableMappingInfo.ImmutableIndexInfo indexInfo;
private final ContentPath path = new ContentPath(0);
private DocumentMapper docMapper;
private Document document;
private StringBuilder stringBuilder = new StringBuilder();
private String id;
private String parent;
private Field version, uid;
private final List<Document> documents = new ArrayList<Document>();
private AllEntries allEntries = new AllEntries();
private float docBoost = 1.0f;
private Mapper dynamicMappingsUpdate = null;
private boolean hasStaticField = false;
private boolean finalized = false;
private BytesReference source;
private Object externalValue = null;
public Context() {
}
public Context(ImmutableMappingInfo.ImmutableIndexInfo ii, Uid uid) {
this.indexInfo = ii;
this.docMapper = ii.indexService.mapperService().documentMapper(uid.type());
assert this.docMapper != null;
this.document = ii.indexStaticOnly() ? new StaticDocument("",null, uid) : new Document();
this.documents.add(this.document);
}
public void reset(ImmutableMappingInfo.ImmutableIndexInfo ii, Uid uid) {
this.indexInfo = ii;
this.docMapper = ii.indexService.mapperService().documentMapper(uid.type());
assert this.docMapper != null;
this.document = ii.indexStaticOnly() ? new StaticDocument("",null, uid) : new Document();
this.documents.clear();
this.documents.add(this.document);
this.id = null;
this.path.reset();
this.allEntries = (this.docMapper.allFieldMapper().enabled()) ? new AllEntries() : null;
this.docBoost = 1.0f;
this.dynamicMappingsUpdate = null;
this.parent = null;
this.externalValue = null;
}
// recusivelly add fields
public void addField(ImmutableMappingInfo.ImmutableIndexInfo indexInfo, Mapper mapper, Object value) throws IOException {
if (value == null && (!(mapper instanceof FieldMapper) || ((FieldMapper)mapper).fieldType().nullValue() == null))
return;
if (value instanceof Collection) {
// flatten list or set of fields
for(Object v : (Collection)value)
addField(indexInfo, mapper, v);
return;
}
if (logger.isTraceEnabled())
logger.trace("doc[{}] class={} name={} value={}", this.documents.indexOf(doc()), mapper.getClass().getSimpleName(), mapper.name(), value);
if (mapper instanceof GeoShapeFieldMapper) {
GeoShapeFieldMapper geoShapeMapper = (GeoShapeFieldMapper) mapper;
XContentParser parser = XContentType.JSON.xContent().createParser((String)value);
parser.nextToken();
ShapeBuilder shapeBuilder = ShapeBuilder.parse(parser, geoShapeMapper);
externalValue = shapeBuilder.build();
path().add(mapper.name());
geoShapeMapper.parse(this);
path().remove();
externalValue = null;
} else if (mapper instanceof GeoPointFieldMapper || mapper instanceof GeoPointFieldMapperLegacy) {
BaseGeoPointFieldMapper geoPointFieldMapper = (BaseGeoPointFieldMapper) mapper;
GeoPoint geoPoint;
if (value instanceof String) {
// geo_point stored as text
geoPoint = new GeoPoint((String)value);
} else {
// geo_point stored in UDT.
Map<String, Double> geo_point = (Map<String, Double>) value;
geoPoint = new GeoPoint(geo_point.get(BaseGeoPointFieldMapper.Names.LAT), geo_point.get(BaseGeoPointFieldMapper.Names.LON));
}
geoPointFieldMapper.parse(this, geoPoint, null);
} else if (mapper instanceof FieldMapper) {
FieldMapper fieldMapper = (FieldMapper)mapper;
if (value instanceof UUID)
value = value.toString(); // #74 uuid stored as string
fieldMapper.createField(this, value);
} else if (mapper instanceof ObjectMapper) {
final ObjectMapper objectMapper = (ObjectMapper)mapper;
final ObjectMapper.Nested nested = objectMapper.nested();
// see https://www.elastic.co/guide/en/elasticsearch/guide/current/nested-objects.html
// code from DocumentParser.parseObject()
if (nested.isNested()) {
beginNestedDocument(objectMapper.fullPath(),new Uid(docMapper.type(), id));
final ParseContext.Document nestedDoc = doc();
final ParseContext.Document parentDoc = nestedDoc.getParent();
// pre add the uid field if possible (id was already provided)
IndexableField uidField = parentDoc.getField(UidFieldMapper.NAME);
if (uidField != null) {
nestedDoc.add(new Field(UidFieldMapper.NAME, uidField.stringValue(), UidFieldMapper.Defaults.NESTED_FIELD_TYPE));
}
nestedDoc.add(new Field(TypeFieldMapper.NAME, objectMapper.nestedTypePathAsString(), TypeFieldMapper.Defaults.FIELD_TYPE));
}
ContentPath.Type origPathType = path().pathType();
path().pathType(objectMapper.pathType());
if (value instanceof Map<?,?>) {
for(Entry<String,Object> entry : ((Map<String,Object>)value).entrySet()) {
// see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html for locking a cache
if (mapper.cqlStruct().equals(Mapper.CqlStruct.MAP))
indexInfo.dynamicMappingUpdateLock.readLock().lock();
Mapper subMapper = objectMapper.getMapper(entry.getKey());
if (subMapper == null) {
// try from the mapperService that could be updated
DocumentMapper docMapper = indexInfo.indexService.mapperService().documentMapper(indexInfo.type);
ObjectMapper newObjectMapper = docMapper.objectMappers().get(mapper.name());
subMapper = newObjectMapper.getMapper(entry.getKey());
}
if (subMapper == null) {
// dynamic field in top level map => update mapping and add the field.
ColumnDefinition cd = baseCfs.metadata.getColumnDefinition(mapper.cqlName());
if (subMapper == null && cd != null && cd.type.isCollection() && cd.type instanceof MapType) {
logger.debug("Updating mapping for field={} type={} value={} ", entry.getKey(), cd.type.toString(), value);
CollectionType ctype = (CollectionType) cd.type;
if (ctype.kind == CollectionType.Kind.MAP && ((MapType)ctype).getKeysType().asCQL3Type().toString().equals("text")) {
// upgrade to write lock
indexInfo.dynamicMappingUpdateLock.readLock().unlock();
indexInfo.dynamicMappingUpdateLock.writeLock().lock();
try {
// Recheck objectMapper because another thread might have acquired write lock and changed state before we did.
if ((subMapper = objectMapper.getMapper(entry.getKey())) == null) {
final String valueType = InternalCassandraClusterService.cqlMapping.get(((MapType)ctype).getValuesType().asCQL3Type().toString());
final DynamicTemplate dynamicTemplate = docMapper.root().findTemplate(path, objectMapper.name()+"."+entry.getKey(), valueType);
// build a mapping update
Map<String,Object> objectMapping = (Map<String,Object>) ((Map<String,Object>)indexInfo.mapping.get("properties")).get(mapper.name());
XContentBuilder builder = XContentFactory.jsonBuilder()
.startObject()
.startObject(docMapper.type())
.startObject("properties")
.startObject(mapper.name());
boolean hasProperties = false;
for(String key : objectMapping.keySet()) {
if (key.equals("properties")) {
Map<String,Object> props = (Map<String,Object>)objectMapping.get(key);
builder.startObject("properties");
for(String key2 : props.keySet()) {
builder.field(key2, props.get(key2));
}
builder.field(entry.getKey(), (dynamicTemplate != null) ? dynamicTemplate.mappingForName(entry.getKey(), valueType) : new HashMap<String,String>() {{ put("type", valueType); }});
builder.endObject();
hasProperties = true;
} else {
builder.field(key, objectMapping.get(key));
}
}
if (!hasProperties) {
builder.startObject("properties");
builder.field(entry.getKey(), (dynamicTemplate != null) ? dynamicTemplate.mappingForName(entry.getKey(), valueType) : new HashMap<String,String>() {{ put("type", valueType); }});
builder.endObject();
}
builder.endObject().endObject().endObject().endObject();
String mappingUpdate = builder.string();
logger.info("updating mapping={}", mappingUpdate);
ElasticSecondaryIndex.this.clusterService.blockingMappingUpdate(indexInfo.indexService, docMapper.type(), mappingUpdate);
DocumentMapper docMapper = indexInfo.indexService.mapperService().documentMapper(indexInfo.type);
ObjectMapper newObjectMapper = docMapper.objectMappers().get(mapper.name());
subMapper = newObjectMapper.getMapper(entry.getKey());
}
} catch (Exception e) {
logger.error("error while updating mapping",e);
} finally {
// Downgrade by acquiring read lock before releasing write lock
indexInfo.dynamicMappingUpdateLock.readLock().lock();
indexInfo.dynamicMappingUpdateLock.writeLock().unlock();
}
}
} else {
logger.error("Unexpected subfield={} for field={} column type={}, ignoring value={}",entry.getKey(), mapper.name(), cd.type.asCQL3Type().toString(), entry.getValue());
}
}
try {
if (subMapper != null)
addField(indexInfo, subMapper, entry.getValue());
} finally {
if (mapper.cqlStruct().equals(Mapper.CqlStruct.MAP))
indexInfo.dynamicMappingUpdateLock.readLock().unlock();
}
}
} else {
if (docMapper.type().equals(PercolatorService.TYPE_NAME)) {
// store percolator query as source.
String sourceQuery = "{\"query\":"+value+"}";
if (logger.isDebugEnabled())
logger.debug("Store percolate query={}", sourceQuery);
BytesReference source = new BytesArray(sourceQuery);
source( source );
Field sourceQueryField = new StoredField(SourceFieldMapper.NAME, source.array(), source.arrayOffset(), source.length());
doc().add(sourceQueryField);
}
}
// restore the enable path flag
path().pathType(origPathType);
if (nested.isNested()) {
final ParseContext.Document nestedDoc = doc();
final ParseContext.Document parentDoc = nestedDoc.getParent();
if (nested.isIncludeInParent()) {
for (IndexableField field : nestedDoc.getFields()) {
if (field.name().equals(UidFieldMapper.NAME) || field.name().equals(TypeFieldMapper.NAME)) {
continue;
} else {
parentDoc.add(field);
}
}
}
if (nested.isIncludeInRoot()) {
final ParseContext.Document rootDoc = rootDoc();
// don't add it twice, if its included in parent, and we are handling the master doc...
if (!nested.isIncludeInParent() || parentDoc != rootDoc) {
for (IndexableField field : nestedDoc.getFields()) {
if (field.name().equals(UidFieldMapper.NAME) || field.name().equals(TypeFieldMapper.NAME)) {
continue;
} else {
rootDoc.add(field);
}
}
}
}
endNestedDocument();
}
}
}
public void beginNestedDocument(String fullPath, Uid uid) {
final Document doc = (baseCfs.metadata.hasStaticColumns()) ? new StaticDocument(fullPath, doc(), uid) : new Document(fullPath, doc());
addDoc(doc);
this.document = doc;
}
public void endNestedDocument() {
this.document = doc().getParent();
}
public boolean externalValueSet() {
return (externalValue != null);
}
public Object externalValue() {
if (externalValue == null)
throw new IllegalStateException("External value is not set");
return externalValue;
}
public void finalize() {
// reverse the order of docs for nested docs support, parent should be last
if (!finalized) {
if (this.documents.size() > 1) {
Collections.reverse(this.documents);
}
// apply doc boost
if (docBoost() != 1.0f) {
final Set<String> encounteredFields = Sets.newHashSet();
for (ParseContext.Document doc : this.documents) {
encounteredFields.clear();
for (IndexableField field : doc) {
if (field.fieldType().indexOptions() != IndexOptions.NONE && !field.fieldType().omitNorms()) {
if (!encounteredFields.contains(field.name())) {
((Field) field).setBoost(docBoost() * field.boost());
encounteredFields.add(field.name());
}
}
}
}
}
}
}
public boolean hasStaticField() {
return hasStaticField;
}
public void setStaticField(boolean hasStaticField) {
this.hasStaticField = hasStaticField;
}
/**
* Return a new context that will be used within a nested document.
*/
@Override
public boolean flyweight() {
return false;
}
@Override
public DocumentMapperParser docMapperParser() {
return null;
}
@Override
public String index() {
return indexInfo.name;
}
@Override
public Settings indexSettings() {
return indexInfo.indexService.indexSettings();
}
@Override
public String type() {
return this.docMapper.type();
}
@Override
public SourceToParse sourceToParse() {
return EMPTY_SOURCE_TO_PARSE;
}
@Override
public BytesReference source() {
return this.source;
}
@Override
public void source(BytesReference source) {
this.source = source;
}
@Override
public ContentPath path() {
return path;
}
@Override
public XContentParser parser() {
return null;
}
@Override
public Document rootDoc() {
return documents.get(0);
}
@Override
public List<Document> docs() {
return (List<Document>)this.documents;
}
@Override
public Document doc() {
return this.document;
}
@Override
public void addDoc(Document doc) {
this.documents.add(doc);
}
@Override
public RootObjectMapper root() {
return docMapper.root();
}
@Override
public DocumentMapper docMapper() {
return this.docMapper;
}
@Override
public AnalysisService analysisService() {
return indexInfo.indexService.analysisService();
}
@Override
public MapperService mapperService() {
return indexInfo.indexService.mapperService();
}
@Override
public String id() {
return id;
}
/**
* Really, just the id mapper should set this.
*/
@Override
public void id(String id) {
this.id = id;
}
public String parent() {
return parent;
}
public void parent(String parent) {
this.parent = parent;
}
@Override
public Field uid() {
return this.uid;
}
/**
* Really, just the uid mapper should set this.
*/
@Override
public void uid(Field uid) {
this.uid = uid;
}
@Override
public Field version() {
return this.version;
}
@Override
public void version(Field version) {
this.version = version;
}
@Override
public AllEntries allEntries() {
return this.allEntries;
}
@Override
public float docBoost() {
return this.docBoost;
}
@Override
public void docBoost(float docBoost) {
this.docBoost = docBoost;
}
@Override
public StringBuilder stringBuilder() {
stringBuilder.setLength(0);
return this.stringBuilder;
}
@Override
public void addDynamicMappingsUpdate(Mapper mapper) {
assert mapper instanceof RootObjectMapper : mapper;
if (dynamicMappingsUpdate == null) {
dynamicMappingsUpdate = mapper;
} else {
dynamicMappingsUpdate = dynamicMappingsUpdate.merge(mapper, false);
}
}
@Override
public Mapper dynamicMappingsUpdate() {
return dynamicMappingsUpdate;
}
class StaticDocument extends FilterableDocument {
Uid uid;
public StaticDocument(String path, Document parent, Uid uid) {
super(path, parent);
this.uid = uid;
}
public boolean apply(IndexableField input) {
// when applying filter for static columns, update _id and _uid....
if (input.name().equals(IdFieldMapper.NAME)) {
((Field)input).setStringValue(uid.id());
}
if (input.name().equals(UidFieldMapper.NAME)) {
if (input instanceof BinaryDocValuesField) {
((BinaryDocValuesField)input).setBytesValue(new BytesRef(uid.toString()));
} else if (input instanceof Field) {
((Field)input).setStringValue(uid.toString());
}
}
if (input.name().startsWith("_")) {
return true;
}
int x = input.name().indexOf('.');
String colName = (x > 0) ? input.name().substring(0,x) : input.name();
int idx = indexInfo.indexOf(colName);
return idx < baseCfs.metadata.partitionKeyColumns().size() || indexInfo.isStaticField(idx) ;
}
}
}
final class ImmutableMappingInfo {
class ImmutableIndexInfo {
final String name;
final String type;
final boolean refresh;
final boolean snapshot;
final boolean includeNodeId;
final IndexService indexService;
final Map<String,Object> mapping;
final boolean index_static_columns;
final boolean index_static_only;
final boolean index_on_compaction;
final boolean versionLessEngine;
Mapper[] mappers; // inititalized in the ImmutableMappingInfo constructor.
ReadWriteLock dynamicMappingUpdateLock;
volatile boolean updated = false;
public ImmutableIndexInfo(String name, IndexService indexService, MappingMetaData mappingMetaData, MetaData metadata, boolean versionLessEngine) throws IOException {
this.name = name;
this.versionLessEngine = versionLessEngine;
this.indexService = indexService;
this.mapping = mappingMetaData.sourceAsMap();
this.type = mappingMetaData.type();
Map<String,Object> mappingMap = (Map<String,Object>)mappingMetaData.getSourceAsMap();
Map<String,Object> metaMap = (mappingMap == null) ? null : (Map<String,Object>)mappingMap.get("_meta");
this.refresh = getMetaSettings(metadata.settings(), indexService.indexSettings(), metaMap, InternalCassandraClusterService.SYNCHRONOUS_REFRESH);
this.snapshot = getMetaSettings(metadata.settings(), indexService.indexSettings(), metaMap, InternalCassandraClusterService.SNAPSHOT_WITH_SSTABLE);
this.includeNodeId = getMetaSettings(metadata.settings(), indexService.indexSettings(), metaMap, InternalCassandraClusterService.INCLUDE_NODE_ID);
this.index_on_compaction = getMetaSettings(metadata.settings(), indexService.indexSettings(), metaMap, InternalCassandraClusterService.INDEX_ON_COMPACTION);
this.index_static_columns = getMetaSettings(metadata.settings(), indexService.indexSettings(), metaMap, InternalCassandraClusterService.INDEX_STATIC_COLUMNS);
this.index_static_only = getMetaSettings(metadata.settings(), indexService.indexSettings(), metaMap, InternalCassandraClusterService.INDEX_STATIC_ONLY);
}
// get _meta, index, cluster or system settings.
public boolean getMetaSettings(Settings metadataSettings, Settings indexSettings, Map<String,Object> metaMap, String propName) {
final boolean value;
if (metaMap != null && metaMap.get(propName) != null) {
value = XContentMapValues.nodeBooleanValue(metaMap.get(propName));
} else {
value = indexSettings.getAsBoolean(InternalCassandraClusterService.INDEX_PREFIX+propName, metadataSettings.getAsBoolean(InternalCassandraClusterService.CLUSTER_PREFIX+propName, Boolean.getBoolean(InternalCassandraClusterService.SYSTEM_PREFIX+propName)));
}
logger.debug("index.type=[{}.{}] {}=[{}]", name, this.type, propName, value);
return value;
}
public int indexOf(String f) {
return ImmutableMappingInfo.this.fieldsToIdx.getOrDefault(f, -1);
}
public boolean isStaticField(int idx) {
return (staticColumns == null) ? false : staticColumns.get(idx);
}
public IndexShard shard() {
final IndexShard indexShard = indexService.shard(0);
if (indexShard == null) {
logger.debug("No such shard {}.0", name);
return null;
}
if (indexShard.state() != IndexShardState.STARTED) {
logger.debug("Shard {}.0 not started", name);
return null;
}
return indexShard;
}
public void refresh() {
if (this.refresh) {
IndexShard shard = shard();
if (shard != null) {
try {
shard.refresh("synchronous_refresh");
} catch (Throwable e) {
logger.error("error", e);
}
}
}
}
public void deleteByQuery(RangeTombstone tombstone) {
IndexShard shard = shard();
if (shard != null) {
Slice slice = tombstone.deletedSlice();
Bound start = slice.start();
Bound end = slice.end();
DocumentMapper docMapper = indexService.mapperService().documentMapper(typeName);
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.add( new TermQuery(new Term(TypeFieldMapper.NAME, docMapper.typeMapper().fieldType().indexedValueForSearch(typeName))), Occur.FILTER);
// build the primary key part of the delete by query
int i = 0;
for(ColumnDefinition cd : baseCfs.metadata.primaryKeyColumns()) {
if (i >= start.size())
break;
if (indexedPkColumns[i]) {
FieldMapper mapper = docMapper.mappers().smartNameFieldMapper(cd.name.toString());
builder.add( buildQuery( cd, mapper, start.get(i), end.get(i), start.isInclusive(), end.isInclusive()), Occur.FILTER);
}
i++;
}
Query query = builder.build();
if (logger.isTraceEnabled()) {
logger.trace("delete rangeTombstone from ks.cf={}.{} query={} in elasticsearch index=[{}]", baseCfs.metadata.ksName, baseCfs.name, query, name);
}
if (!updated)
updated = true;
DeleteByQuery deleteByQuery = new DeleteByQuery(query, null, null, null, null, Operation.Origin.PRIMARY, System.currentTimeMillis(), typeName);
shard.engine().delete(deleteByQuery);
}
}
/**
* Build range query to remove a row slice.
* @param cd
* @param mapper
* @param lower
* @param upper
* @param includeLower
* @param includeUpper
* @return
*/
@SuppressForbidden(reason="unchecked")
private Query buildQuery(ColumnDefinition cd, FieldMapper mapper, ByteBuffer lower, ByteBuffer upper, boolean includeLower, boolean includeUpper) {
Object start = cd.type.compose(lower);
Object end = cd.type.compose(upper);
Query query = null;
if (mapper != null) {
CQL3Type cql3Type = cd.type.asCQL3Type();
if (cql3Type instanceof CQL3Type.Native) {
switch ((CQL3Type.Native) cql3Type) {
case ASCII:
case TEXT:
case VARCHAR:
if (start.equals(end)) {
query = new TermQuery(new Term(cd.name.toString(), mapper.fieldType().indexedValueForSearch(start)));
} else {
query = new TermRangeQuery(cd.name.toString(), mapper.fieldType().indexedValueForSearch(start), mapper.fieldType().indexedValueForSearch(end),includeLower, includeUpper);
}
break;
case INT:
case SMALLINT:
case TINYINT:
query = NumericRangeQuery.newIntRange(cd.name.toString(), (Integer) start, (Integer) end, includeLower, includeUpper);
break;
case INET:
case TIMESTAMP:
case BIGINT:
query = NumericRangeQuery.newLongRange(cd.name.toString(), (Long) start, (Long) start, includeLower, includeUpper);
break;
case DOUBLE:
query = NumericRangeQuery.newDoubleRange(cd.name.toString(), (Double) start, (Double) start, includeLower, includeUpper);
break;
case FLOAT:
query = NumericRangeQuery.newFloatRange(cd.name.toString(), (Float) start, (Float) start, includeLower, includeUpper);
break;
case DECIMAL:
case TIMEUUID:
case UUID:
case BLOB:
case BOOLEAN:
throw new UnsupportedOperationException("Unsupported data type in primary key");
}
}
} else {
throw new UnsupportedOperationException("Object type in primary key not supported");
}
return query;
}
public boolean indexStaticOnly() {
return this.index_static_only;
}
public String toString() {
return this.name;
}
}
class ImmutablePartitionFunction {
final String name;
final String pattern;
final String[] fields; // indexed fields used in the partition function
final int[] fieldsIdx; // column position in Rowcument.values
final Set<String> indices; // associated indices
final PartitionFunction partitionFunction;
ImmutablePartitionFunction(String[] args) {
this(args, new MessageFormatPartitionFunction());
}
ImmutablePartitionFunction(String[] args, PartitionFunction partitionFunc) {
this.name = args[0];
this.pattern = args[1];
this.fields = new String[args.length-2];
this.fieldsIdx = new int[args.length-2];
System.arraycopy(args, 2, this.fields, 0, args.length-2);
this.indices = new HashSet<String>();
this.partitionFunction = partitionFunc;
}
// values = indexed values in the same order as MappingInfo.fields
String indexName(Object[] values) {
Object[] args = new Object[fields.length];
for(int i=0; i < fieldsIdx.length; i++)
args[i] = (fieldsIdx[i] < values.length) ? values[fieldsIdx[i]] : null;
return partitionFunction.format(pattern, args);
}
public String toString() {
return this.name;
}
}
final Map<String, ImmutablePartitionFunction> partitionFunctions;
final ImmutableIndexInfo[] indices;
final ObjectIntHashMap<String> indexToIdx;
final ObjectIntHashMap<String> fieldsToIdx;
final BitSet fieldsToRead;
final BitSet staticColumns;
final boolean indexSomeStaticColumnsOnWideRow;
final boolean[] indexedPkColumns; // bit mask of indexed PK columns.
final long metadataVersion;
final String nodeId;
final String typeName = InternalCassandraClusterService.cfNameToType(ElasticSecondaryIndex.this.baseCfs.name);
final boolean indexOnCompaction; // true if at least one index has index_on_compaction=true;
ImmutableMappingInfo(final ClusterState state) {
this.metadataVersion = state.metaData().version();
this.nodeId = state.nodes().localNodeId();
if (state.blocks().hasGlobalBlock(ClusterBlockLevel.WRITE)) {
logger.debug("global write blocked");
this.indices = null;
this.indexToIdx = null;
this.fieldsToIdx = null;
this.fieldsToRead = null;
this.staticColumns = null;
this.indexSomeStaticColumnsOnWideRow = false;
this.indexedPkColumns = null;
this.partitionFunctions = null;
this.indexOnCompaction = false;
return;
}
Map<String, Boolean> fieldsMap = new HashMap<String, Boolean>();
Map<String, ImmutablePartitionFunction> partFuncs = null;
List<ImmutableIndexInfo> indexList = new ArrayList<ImmutableIndexInfo>();
for(Iterator<IndexMetaData> indexMetaDataIterator = state.metaData().iterator(); indexMetaDataIterator.hasNext(); ) {
IndexMetaData indexMetaData = indexMetaDataIterator.next();
String index = indexMetaData.getIndex();
MappingMetaData mappingMetaData;
if (indexMetaData.getState() != IndexMetaData.State.OPEN)
continue;
ClusterBlockException clusterBlockException = state.blocks().indexBlockedException(ClusterBlockLevel.WRITE, index);
if (clusterBlockException != null) {
if (logger.isInfoEnabled())
logger.info("ignore, index=[{}] blocked blocks={}", index, clusterBlockException.blocks());
continue;
}
if (ElasticSecondaryIndex.this.baseCfs.metadata.ksName.equals(indexMetaData.keyspace()) && (mappingMetaData = indexMetaData.mapping(typeName)) != null) {
try {
Map<String,Object> mappingMap = (Map<String,Object>)mappingMetaData.getSourceAsMap();
if (mappingMap.get("properties") != null) {
IndicesService indicesService = ElassandraDaemon.injector().getInstance(IndicesService.class);
IndexService indexService = indicesService.indexService(index);
if (indexService == null) {
logger.error("indexService not available for [{}], ignoring" , index);
continue;
}
ImmutableIndexInfo indexInfo = new ImmutableIndexInfo(index, indexService, mappingMetaData, state.metaData(), indexMetaData.isIndexUsingVersionLessEngine(indexMetaData.getSettings()));
indexList.add(indexInfo);
Map<String,Object> props = (Map<String,Object>)mappingMap.get("properties");
for(String fieldName : props.keySet() ) {
Map<String,Object> fieldMap = (Map<String,Object>)props.get(fieldName);
if (fieldMap.get("enabled") == null || XContentMapValues.nodeBooleanValue(fieldMap.get("enabled"))) {
boolean mandartory = (fieldMap.get(TypeParsers.CQL_MANDATORY) == null || XContentMapValues.nodeBooleanValue(fieldMap.get(TypeParsers.CQL_MANDATORY)));
if (fieldsMap.get(fieldName) != null) {
mandartory = mandartory || fieldsMap.get(fieldName);
}
fieldsMap.put(fieldName, mandartory);
}
}
if (mappingMetaData.hasParentField()) {
Map<String,Object> parentsProps = (Map<String,Object>)mappingMap.get(ParentFieldMapper.NAME);
String pkColumns = (String)parentsProps.get(ParentFieldMapper.CQL_PARENT_PK);
if (pkColumns == null) {
fieldsMap.put(ParentFieldMapper.NAME,true);
} else {
for(String colName : pkColumns.split(","))
fieldsMap.put(colName, true);
}
}
String[] pf = indexMetaData.partitionFunction();
if (pf != null) {
if (partFuncs == null)
partFuncs = new HashMap<String, ImmutablePartitionFunction>();
ImmutablePartitionFunction func = partFuncs.get(pf[0]);
if (func == null) {
func = new ImmutablePartitionFunction(pf, indexMetaData.partitionFunctionClass());
partFuncs.put(func.name, func);
}
if (!func.pattern.equals(pf[1])) {
logger.error("Partition function [{}] is defined with two different partterns [{}] and [{}]", pf[0], func.pattern, pf[1]);
}
func.indices.add(index);
}
}
} catch (IOException e) {
logger.error("Unexpected error index=[{}]", e, index);
}
}
}
if (indexList.size() == 0) {
if (logger.isTraceEnabled())
logger.warn("No active elasticsearch index for keyspace.table=[{}.{}] state={}",baseCfs.metadata.ksName, baseCfs.name, state);
this.indices = null;
this.indexToIdx = null;
this.fieldsToIdx = null;
this.fieldsToRead = null;
this.staticColumns = null;
this.indexSomeStaticColumnsOnWideRow = false;
this.indexedPkColumns = null;
this.partitionFunctions = null;
this.indexOnCompaction = false;
return;
}
// build indices array and indexToIdx map
this.indices = new ImmutableIndexInfo[indexList.size()];
this.indexToIdx = new ObjectIntHashMap<String>(indexList.size());
for(int i = 0; i < indexList.size(); i++) {
indices[i] = indexList.get(i);
indexToIdx.put(indexList.get(i).name, i);
}
// order fields with pk columns first
final String[] fields = new String[fieldsMap.size()];
final int pkLength = baseCfs.metadata.partitionKeyColumns().size()+baseCfs.metadata.clusteringColumns().size();
this.indexedPkColumns = new boolean[pkLength];
int j=0, l=0;
for(ColumnDefinition cd : Iterables.concat(baseCfs.metadata.partitionKeyColumns(), baseCfs.metadata.clusteringColumns())) {
indexedPkColumns[l] = fieldsMap.containsKey(cd.name.toString());
if (indexedPkColumns[l])
fields[j++] = cd.name.toString();
l++;
}
for(String f : fieldsMap.keySet()) {
boolean alreadyInFields = false;
for(int k=0; k < j; k++) {
if (f.equals(fields[k])) {
alreadyInFields = true;
break;
}
}
if (!alreadyInFields) {
fields[j++] = f;
}
}
// build a map for fields, as it is O(1) rather than O(n) for an array.
this.fieldsToIdx = new ObjectIntHashMap<String>(fields.length);
for(int i=0; i < fields.length; i++)
this.fieldsToIdx.put(fields[i], i);
this.fieldsToRead = new BitSet(fields.length);
this.staticColumns = (baseCfs.metadata.hasStaticColumns()) ? new BitSet(fields.length) : null;
for(int i=0; i < fields.length; i++) {
ColumnIdentifier colId = new ColumnIdentifier(fields[i],true);
ColumnDefinition colDef = baseCfs.metadata.getColumnDefinition(colId);
this.fieldsToRead.set(i, fieldsMap.get(fields[i]) && !colDef.isPrimaryKeyColumn());
if (staticColumns != null)
this.staticColumns.set(i,colDef.isStatic());
}
if (partFuncs != null && partFuncs.size() > 0) {
for(ImmutablePartitionFunction func : partFuncs.values()) {
int i = 0;
for(String field : func.fields)
func.fieldsIdx[i++] = this.fieldsToIdx.getOrDefault(field, -1);
}
this.partitionFunctions = partFuncs;
} else {
this.partitionFunctions = null;
}
// build InderInfo.mappers arrays.
for(ImmutableIndexInfo indexInfo : this.indices) {
indexInfo.mappers = new Mapper[fields.length];
for(int i=0; i < fields.length; i++) {
DocumentMapper docMapper = indexInfo.indexService.mapperService().documentMapper(typeName);
Mapper mapper = docMapper.mappers().smartNameFieldMapper(fields[i]);
if (mapper != null) {
indexInfo.mappers[i] = mapper;
} else {
ObjectMapper objectMapper = docMapper.objectMappers().get(fields[i]);
if (objectMapper.cqlStruct().equals(Mapper.CqlStruct.MAP))
indexInfo.dynamicMappingUpdateLock = new ReentrantReadWriteLock();
indexInfo.mappers[i] = objectMapper;
}
}
}
boolean _indexSomeStaticColumns = false;
boolean _indexOnCompaction = false;
for(ImmutableIndexInfo indexInfo : this.indices) {
if (indexInfo.index_static_columns)
_indexSomeStaticColumns = true;
if (indexInfo.index_on_compaction)
_indexOnCompaction = true;
if (_indexSomeStaticColumns && _indexOnCompaction)
break;
}
this.indexSomeStaticColumnsOnWideRow = _indexSomeStaticColumns;
this.indexOnCompaction = _indexOnCompaction;
}
public BitSet targetIndices(final Object[] values) {
if (this.partitionFunctions == null)
return null;
BitSet targets = new BitSet(this.indices.length);
for(ImmutablePartitionFunction func : this.partitionFunctions.values()) {
String indexName = func.indexName(values);
int indexIdx = this.indexToIdx.getOrDefault(indexName, -1);
if (indexIdx >= 0) {
targets.set(indexIdx);
} else {
if (logger.isDebugEnabled())
logger.debug("No target index=[{}] found for partition function name=[{}] pattern=[{}] indices={}",
indexName, func.name, func.pattern, Arrays.stream(mappingInfo.indices).map(i -> i.name));
}
}
if (logger.isTraceEnabled())
logger.trace("Partition index bitset={} indices={}", targets, this.indices);
return targets;
}
public BitSet targetIndicesForDelete(final Object[] values) {
if (this.partitionFunctions == null)
return null;
BitSet targets = new BitSet(this.indices.length);
for(ImmutablePartitionFunction func : this.partitionFunctions.values()) {
String indexName = func.indexName(values);
int indexIdx = this.indexToIdx.getOrDefault(indexName, -1);
if (indexIdx >= 0) {
targets.set(indexIdx);
} else {
if (logger.isWarnEnabled())
logger.warn("No target index=[{}] found, function name=[{}] pattern=[{}], return all indices={}",
indexName, func.name, func.pattern, Arrays.stream(mappingInfo.indices).map(i -> i.name));
for(String index : func.indices) {
int i = this.indexToIdx.getOrDefault(index, -1);
if (i >= 0)
targets.set(i);
}
}
}
return targets;
}
class WideRowcumentIndexer extends RowcumentIndexer {
NavigableSet<Clustering> clusterings = new java.util.TreeSet<Clustering>(baseCfs.metadata.comparator);
Map<Clustering, WideRowcument> rowcuments = new TreeMap<Clustering, WideRowcument>(baseCfs.metadata.comparator);
Row inStaticRow, outStaticRow;
public WideRowcumentIndexer(final DecoratedKey key,
final PartitionColumns columns,
final int nowInSec,
final OpOrder.Group opGroup,
final IndexTransaction.Type transactionType) {
super(key, columns, nowInSec, opGroup, transactionType);
}
public class WideRowcument extends Rowcument {
public WideRowcument(Row inRow, Row outRow) throws IOException {
super(inRow, outRow);
}
/**
* Check for missing fields
* @return true if the rowcument needs some fields.
*/
public boolean hasMissingFields() {
// add static fields before checking for missing fields
try {
if (inStaticRow != null)
readCellValues(inStaticRow, true);
if (outStaticRow != null)
readCellValues(outStaticRow, false);
} catch (IOException e) {
logger.error("Unexpected error", e);
}
return super.hasMissingFields();
}
}
@Override
public void collect(Row inRow, Row outRow) {
try {
if (logger.isTraceEnabled()) {
if (inRow != null)
logger.trace("indexer={} newRowData={} clustering={} static={} hasLiveData={}", WideRowcumentIndexer.this.hashCode(), inRow, inRow.clustering(), inRow.isStatic(), inRow.hasLiveData(nowInSec));
if (outRow != null)
logger.trace("indexer={} oldRowData={} clustering={} static={} hasLiveData={}", WideRowcumentIndexer.this.hashCode(), outRow, outRow.clustering(), outRow.isStatic(), outRow.hasLiveData(nowInSec));
}
if (inRow.isStatic()) {
inStaticRow = inRow;
outStaticRow = outRow;
} else {
clusterings.add(inRow.clustering());
rowcuments.put(inRow.clustering(), new WideRowcument(inRow, outRow));
}
} catch(Throwable t) {
logger.error("Unexpected error", t);
}
}
@Override
public void flush() {
if (logger.isTraceEnabled())
logger.trace("indexer={} inStaticRow={} outStaticRow={} clustering={}", this.hashCode(), inStaticRow, outStaticRow, this.clusterings);
switch(transactionType) {
case CLEANUP:
for(WideRowcument rowcument : rowcuments.values())
rowcument.delete();
break;
case COMPACTION:
case UPDATE:
if (!clusterings.isEmpty()) {
boolean hasMissingFields = false;
for(WideRowcument rowcument : rowcuments.values()) {
if (rowcument.hasMissingFields()) {
hasMissingFields = true;
break;
}
}
if (hasMissingFields) {
if (logger.isTraceEnabled())
logger.trace("indexer={} read partition for clusterings={}", this.hashCode(), clusterings);
SinglePartitionReadCommand command = SinglePartitionReadCommand.create(baseCfs.metadata, nowInSec, key, clusterings);
RowIterator rowIt = read(command);
this.inStaticRow = rowIt.staticRow();
for(; rowIt.hasNext(); ) {
Row row = rowIt.next();
try {
WideRowcument rowcument = new WideRowcument(row, null);
try {
if (indexSomeStaticColumnsOnWideRow && inStaticRow != null)
rowcument.readCellValues(inStaticRow, true);
} catch (IOException e) {
logger.error("Unexpected error", e);
}
if (rowcument.hasLiveData(nowInSec)) {
rowcument.index();
} else {
rowcument.delete();
}
} catch (IOException e) {
logger.error("Unexpected error", e);
}
}
} else {
for(WideRowcument rowcument : rowcuments.values()) {
if (rowcument.hasLiveData(nowInSec)) {
rowcument.index();
} else {
rowcument.delete();
}
}
}
}
}
// index static document.
if (this.inStaticRow != null) {
try {
WideRowcument rowcument = new WideRowcument(inStaticRow, outStaticRow);
if (rowcument.hasLiveData(nowInSec)) {
rowcument.index();
} else {
rowcument.delete();
}
} catch (IOException e) {
logger.error("Unexpected error", e);
}
}
}
/**
* Notification of a RangeTombstone.
* An update of a single partition may contain multiple RangeTombstones,
* and a notification will be passed for each of them.
* @param tombstone
*/
@Override
public void rangeTombstone(RangeTombstone tombstone) {
try {
BitSet targets = targetIndices(pkCols);
if (targets == null) {
for(ImmutableMappingInfo.ImmutableIndexInfo indexInfo : indices)
indexInfo.deleteByQuery(tombstone);
} else {
for(int i = targets.nextSetBit(0); i >= 0 && i < indices.length; i = targets.nextSetBit(i+1))
indices[i].deleteByQuery(tombstone);
}
} catch(Throwable t) {
logger.error("Unexpected error", t);
}
}
}
class SkinnyRowcumentIndexer extends RowcumentIndexer {
SkinnyRowcument rowcument;
public SkinnyRowcumentIndexer(final DecoratedKey key,
final PartitionColumns columns,
final int nowInSec,
final OpOrder.Group opGroup,
final IndexTransaction.Type transactionType) {
super(key, columns, nowInSec, opGroup, transactionType);
}
public class SkinnyRowcument extends Rowcument {
public SkinnyRowcument(Row inRow, Row outRow) throws IOException {
super(inRow, outRow);
}
}
@Override
public void collect(Row inRow, Row outRow) {
try {
this.rowcument = new SkinnyRowcument(inRow, outRow);
} catch (IOException e) {
logger.error("Unexpected error", e);
}
}
@Override
public void flush() {
if (rowcument != null) {
switch(transactionType) {
case CLEANUP:
this.rowcument.delete();
break;
case COMPACTION: // remove expired row or reindex a doc when a column has expired, happen only when index_on_compaction=true for at least one elasticsearch index.
case UPDATE:
if (rowcument.hasMissingFields()) {
SinglePartitionReadCommand command = SinglePartitionReadCommand.fullPartitionRead(baseCfs.metadata, nowInSec, key);
RowIterator rowIt = read(command);
if (rowIt.hasNext())
try {
this.rowcument = new SkinnyRowcument(rowIt.next(), null);
} catch (IOException e) {
logger.error("Unexpected error", e);
}
}
}
if (this.rowcument.hasLiveData(nowInSec)) {
this.rowcument.index();
} else {
this.rowcument.delete();
}
}
}
}
abstract class RowcumentIndexer implements Index.Indexer {
final DecoratedKey key;
final int nowInSec;
final IndexTransaction.Type transactionType;
final OpOrder.Group opGroup;
final Object[] pkCols = new Object[baseCfs.metadata.partitionKeyColumns().size()+baseCfs.metadata.clusteringColumns().size()];
final String partitionKey;
BitSet targets = null;
public RowcumentIndexer(final DecoratedKey key,
final PartitionColumns columns,
final int nowInSec,
final OpOrder.Group opGroup,
final IndexTransaction.Type transactionType) {
this.key = key;
this.nowInSec = nowInSec;
this.opGroup = opGroup;
this.transactionType = transactionType;
AbstractType<?> keyValidator = baseCfs.metadata.getKeyValidator();
int i = 0;
if (keyValidator instanceof CompositeType) {
CompositeType composite = (CompositeType) keyValidator;
for(ByteBuffer bb : composite.split(key.getKey())) {
AbstractType<?> type = composite.types.get(i);
pkCols[i++] = type.compose(bb);
}
} else {
pkCols[i++] = keyValidator.compose(key.getKey());
}
this.partitionKey = InternalCassandraClusterService.stringify(pkCols, i);
}
/**
* Notification of the start of a partition update.
* This event always occurs before any other during the update.
*/
@Override
public void begin() {
}
/**
* Notification that a new row was inserted into the Memtable holding the partition.
* This only implies that the inserted row was not already present in the Memtable,
* it *does not* guarantee that the row does not exist in an SSTable, potentially with
* additional column data.
*
* @param row the Row being inserted into the base table's Memtable.
*/
@Override
public void insertRow(Row row) {
collect(row, null);
}
/**
* Notification of a modification to a row in the base table's Memtable.
* This is allow an Index implementation to clean up entries for base data which is
* never flushed to disk (and so will not be purged during compaction).
* It's important to note that the old & new rows supplied here may not represent
* the totality of the data for the Row with this particular Clustering. There may be
* additional column data in SSTables which is not present in either the old or new row,
* so implementations should be aware of that.
* The supplied rows contain only column data which has actually been updated.
* oldRowData contains only the columns which have been removed from the Row's
* representation in the Memtable, while newRowData includes only new columns
* which were not previously present. Any column data which is unchanged by
* the update is not included.
*
* @param oldRowData data that was present in existing row and which has been removed from
* the base table's Memtable
* @param newRowData data that was not present in the existing row and is being inserted
* into the base table's Memtable
*/
@Override
public void updateRow(Row oldRowData, Row newRowData) {
collect(newRowData, oldRowData);
}
/**
* Notification that a row was removed from the partition.
* Note that this is only called as part of either a compaction or a cleanup.
* This context is indicated by the TransactionType supplied to the indexerFor method.
*
* As with updateRow, it cannot be guaranteed that all data belonging to the Clustering
* of the supplied Row has been removed (although in the case of a cleanup, that is the
* ultimate intention).
* There may be data for the same row in other SSTables, so in this case Indexer implementations
* should *not* assume that all traces of the row have been removed. In particular,
* it is not safe to assert that all values associated with the Row's Clustering
* have been deleted, so implementations which index primary key columns should not
* purge those entries from their indexes.
*
* @param row data being removed from the base table
*/
@Override
public void removeRow(Row row) {
collect(null, row);
}
/**
* Notification of the end of the partition update.
* This event always occurs after all others for the particular update.
*/
@Override
public void finish() {
flush();
if (this.targets == null) {
// refresh all associated indices.
for(ImmutableMappingInfo.ImmutableIndexInfo indexInfo : indices)
indexInfo.refresh();
} else {
// refresh matching partition indices.
for(int i = targets.nextSetBit(0); i >= 0 && i < indices.length; i = targets.nextSetBit(i+1))
indices[i].refresh();
}
}
public abstract void collect(Row inRow, Row outRow);
public abstract void flush();
public RowIterator read(SinglePartitionReadCommand command) {
UnfilteredRowIterator unfilteredRows = command.queryMemtableAndDisk(baseCfs, opGroup);
return UnfilteredRowIterators.filter(unfilteredRows, nowInSec);
}
class Rowcument {
final String id;
final Object[] values = new Object[fieldsToIdx.size()];
final BitSet fieldsNotNull = new BitSet(fieldsToIdx.size()); // regular or static columns only
final BitSet tombstoneColumns = new BitSet(fieldsToIdx.size()); // regular or static columns only
int docTtl = Integer.MAX_VALUE;
final boolean isStatic;
final boolean hasLiveData;
public Rowcument(Row inRow, Row outRow) throws IOException {
Row row = inRow != null ? inRow : outRow;
this.isStatic = row.isStatic();
this.hasLiveData = inRow != null && inRow.hasLiveData(nowInSec);
//if (inRow != null && inRow.isStatic())
// logger.error("indexer={} inRow static hasLive={} inRow.timestamp={}", RowcumentIndexer.this.hashCode(), hasLiveData, inRow.primaryKeyLivenessInfo().timestamp());
// copy the indexed columns of partition key in values
int x = 0;
for(int i=0 ; i < baseCfs.metadata.partitionKeyColumns().size(); i++) {
if (indexedPkColumns[i])
values[x++] = pkCols[i];
}
// copy the indexed columns of clustering key in values
if (!row.isStatic() && row.clustering().size() > 0) {
int i=0;
for(ColumnDefinition ccd : baseCfs.metadata.clusteringColumns()) {
Object value = InternalCassandraClusterService.deserialize(ccd.type, row.clustering().get(i));
pkCols[baseCfs.metadata.partitionKeyColumns().size()+i] = value;
if (indexedPkColumns[baseCfs.metadata.partitionKeyColumns().size()+i])
values[x++] = value;
i++;
}
id = InternalCassandraClusterService.stringify(pkCols, pkCols.length);
} else {
id = partitionKey;
}
if (inRow != null)
readCellValues(inRow, true);
if (outRow != null)
readCellValues(outRow, false);
}
public boolean hasLiveData(int nowInSec) {
return hasLiveData;
}
public boolean isStatic() {
return isStatic;
}
public void readCellValues(Row row, boolean indexOp) throws IOException {
for(Cell cell : row.cells())
readCellValue(cell, indexOp);
}
public void readCellValue(Cell cell, boolean indexOp) throws IOException {
final String cellNameString = cell.column().name.toString();
int idx = fieldsToIdx.getOrDefault(cellNameString, -1);
if (idx == - 1)
return; //ignore cell, not indexed.
if (cell.isLive(nowInSec) && indexOp) {
docTtl = Math.min(cell.localDeletionTime(), docTtl);
ColumnDefinition cd = cell.column();
if (cd.type.isCollection()) {
CollectionType ctype = (CollectionType) cd.type;
Object value = null;
switch (ctype.kind) {
case LIST:
value = InternalCassandraClusterService.deserialize(((ListType)cd.type).getElementsType(), cell.value() );
if (logger.isTraceEnabled())
logger.trace("list name={} kind={} type={} value={}", cellNameString, cd.kind, cd.type.asCQL3Type().toString(), value);
List l = (List) values[idx];
if (l == null) {
l = new ArrayList<>(1);
values[idx] = l;
}
l.add(value);
break;
case SET:
value = InternalCassandraClusterService.deserialize(((SetType)cd.type).getElementsType(), cell.value() );
if (logger.isTraceEnabled())
logger.trace("set name={} kind={} type={} value={}", cellNameString, cd.kind, cd.type.asCQL3Type().toString(), value);
Set s = (Set) values[idx];
if (s == null) {
s = new HashSet<>();
values[idx] = s;
}
s.add(value);
break;
case MAP:
value = InternalCassandraClusterService.deserialize(((MapType)cd.type).getValuesType(), cell.value() );
CellPath cellPath = cell.path();
Object key = InternalCassandraClusterService.deserialize(((MapType)cd.type).getKeysType(), cellPath.get(cellPath.size()-1));
if (logger.isTraceEnabled())
logger.trace("map name={} kind={} type={} key={} value={}",
cellNameString, cd.kind,
cd.type.asCQL3Type().toString(),
key,
value);
if (key instanceof String) {
Map m = (Map) values[idx];
if (m == null) {
m = new HashMap<>();
values[idx] = m;
}
m.put(key,value);
}
break;
}
fieldsNotNull.set(idx, value != null);
} else {
Object value = InternalCassandraClusterService.deserialize(cd.type, cell.value() );
if (logger.isTraceEnabled())
logger.trace("name={} kind={} type={} value={}", cellNameString, cd.kind, cd.type.asCQL3Type().toString(), value);
values[idx] = value;
fieldsNotNull.set(idx, value != null);
}
} else {
// tombstone => black list this column for later document.read().
if (values[idx]==null)
tombstoneColumns.set(idx);
}
}
/**
* Check for missing fields
* @return true if the rowcument needs some fields.
*/
public boolean hasMissingFields() {
// add missing or collection columns that should be read before indexing the document.
// read missing static or regular columns
final BitSet mustReadFields = (BitSet)fieldsToRead.clone();
boolean completeOnlyStatic = isStatic();
if (staticColumns != null) {
if (isStatic() || ImmutableMappingInfo.this.indexSomeStaticColumnsOnWideRow) {
// ignore regular columns, we are updating static ones.
int prev_cardinality = mustReadFields.cardinality();
mustReadFields.and(staticColumns);
// ensure that we don't request for static columns only.
if (mustReadFields.cardinality() < prev_cardinality)
completeOnlyStatic = true;
} else {
// ignore static columns, we got only regular columns.
mustReadFields.andNot(staticColumns);
}
}
mustReadFields.andNot(fieldsNotNull);
mustReadFields.andNot(tombstoneColumns);
return (mustReadFields.cardinality() > 0);
}
public Context buildContext(ImmutableIndexInfo indexInfo, boolean staticColumnsOnly) throws IOException {
Context context = ElasticSecondaryIndex.this.perThreadContext.get();
Uid uid = new Uid(typeName, (staticColumnsOnly) ? partitionKey : id);
context.reset(indexInfo, uid);
// preCreate for all metadata fields.
for (MetadataFieldMapper metadataMapper : context.docMapper.mapping().metadataMappers())
metadataMapper.preCreate(context);
context.docMapper.idFieldMapper().createField(context, uid.id());
context.docMapper.uidMapper().createField(context, uid);
context.docMapper.typeMapper().createField(context, typeName);
context.docMapper.tokenFieldMapper().createField(context, (Long) key.getToken().getTokenValue());
if (indexInfo.includeNodeId)
context.docMapper.nodeFieldMapper().createField(context, ImmutableMappingInfo.this.nodeId);
if (context.docMapper.routingFieldMapper().required())
context.docMapper.routingFieldMapper().createField(context, partitionKey);
if (context.docMapper.allFieldMapper().enabled())
context.docMapper.allFieldMapper().createField(context, null);
if (indexInfo.versionLessEngine) {
// versionLessEngine do not needs to index version
context.version(DEFAULT_EXTERNAL_VERSION);
} else {
context.version(DEFAULT_INTERNAL_VERSION);
context.doc().add(DEFAULT_INTERNAL_VERSION);
}
// add all mapped fields to the current context.
for(int i=0; i < values.length; i++) {
if (indexInfo.mappers[i] != null)
try {
context.addField(indexInfo, indexInfo.mappers[i], values[i]);
} catch (IOException e) {
logger.error("error", e);
}
}
// postCreate for all metadata fields.
Mapping mapping = context.docMapper.mapping();
for (MetadataFieldMapper metadataMapper : mapping.metadataMappers()) {
try {
metadataMapper.postCreate(context);
} catch (IOException e) {
logger.error("error", e);
}
}
// add _parent
ParentFieldMapper parentMapper = context.docMapper.parentFieldMapper();
if (parentMapper.active() && fieldsToIdx.getOrDefault(ParentFieldMapper.NAME, -1) == -1) {
String parent = null;
if (parentMapper.pkColumns() != null) {
String[] cols = parentMapper.pkColumns().split(",");
if (cols.length == 1) {
parent = (String) values[fieldsToIdx.get(cols[0])];
} else {
Object parentValues[] = new Object[cols.length];
for(int i = 0; i < cols.length; i++)
parentValues[i] = values[fieldsToIdx.get(cols[i])];
parent = InternalCassandraClusterService.stringify(parentValues, cols.length);
}
} else {
int parentIdx = fieldsToIdx.getOrDefault(ParentFieldMapper.NAME, -1);
if (parentIdx != -1 && values[parentIdx] instanceof String)
parent = (String) values[parentIdx];
}
if (parent != null) {
//parent = parentMapper.type() + Uid.DELIMITER + parent;
if (logger.isDebugEnabled())
logger.debug("add _parent={}", parent);
parentMapper.createField(context, parent);
context.parent(parent);
}
}
if (!parentMapper.active()) {
// need to call this for parent types
parentMapper.createField(context, null);
}
return context;
}
public void index() {
long startTime = System.nanoTime();
long ttl = (long)((this.docTtl < Integer.MAX_VALUE) ? this.docTtl : 0);
targets = ImmutableMappingInfo.this.targetIndices(values);
if (targets == null) {
// index for associated indices
for(ImmutableIndexInfo indexInfo : indices)
index(indexInfo, startTime, ttl);
} else {
// delete for matching target indices.
for(int i = targets.nextSetBit(0); i >= 0 && i < indices.length; i = targets.nextSetBit(i+1))
index(indices[i], startTime, ttl);
}
}
private void index(ImmutableIndexInfo indexInfo, long startTime, long ttl) {
if (indexInfo.index_on_compaction || transactionType == IndexTransaction.Type.UPDATE) {
try {
Context context = buildContext(indexInfo, isStatic());
Field uid = context.uid();
if (isStatic()) {
uid = new Field(UidFieldMapper.NAME, Uid.createUid(typeName, partitionKey), Defaults.FIELD_TYPE);
for(Document doc : context.docs()) {
if (doc instanceof Context.StaticDocument)
((Context.StaticDocument)doc).applyFilter(isStatic());
}
}
context.finalize();
final ParsedDocument parsedDoc = new ParsedDocument(
uid,
context.version(),
(isStatic()) ? partitionKey : context.id(),
context.type(),
InternalCassandraClusterService.stringify(pkCols, baseCfs.metadata.partitionKeyColumns().size()), // routing
System.currentTimeMillis(), // timstamp
ttl,
((Long)key.getToken().getTokenValue()).longValue(),
context.docs(),
context.source(), // source
(Mapping)null); // mappingUpdate
parsedDoc.parent(context.parent());
if (logger.isTraceEnabled())
logger.trace("index={} id={} type={} uid={} routing={} docs={}", context.indexInfo.name, parsedDoc.id(), parsedDoc.type(), parsedDoc.uid(), parsedDoc.routing(), parsedDoc.docs());
final IndexShard indexShard = context.indexInfo.shard();
if (indexShard != null) {
if (!indexInfo.updated)
indexInfo.updated = true;
final Engine.Index operation = new Engine.Index(context.docMapper.uidMapper().term(uid.stringValue()),
parsedDoc,
indexInfo.versionLessEngine ? 1 : Versions.MATCH_ANY,
indexInfo.versionLessEngine ? VersionType.EXTERNAL : VersionType.INTERNAL,
Engine.Operation.Origin.PRIMARY,
startTime,
false);
final boolean created = operation.execute(indexShard);
if (logger.isDebugEnabled()) {
logger.debug("document CF={}.{} index={} type={} id={} version={} created={} static={} ttl={} refresh={} ",
baseCfs.metadata.ksName, baseCfs.metadata.cfName,
context.indexInfo.name, typeName,
parsedDoc.id(), operation.version(), created, isStatic(), ttl, context.indexInfo.refresh);
}
}
} catch (IOException e) {
logger.error("error", e);
}
}
}
public void delete() {
targets = ImmutableMappingInfo.this.targetIndices(values);
if (targets == null) {
// delete for associated indices
for(ImmutableMappingInfo.ImmutableIndexInfo indexInfo : indices)
delete(indexInfo);
} else {
// delete for matching target indices.
for(int i = targets.nextSetBit(0); i >= 0 && i < indices.length; i = targets.nextSetBit(i+1))
delete(indices[i]);
}
}
private void delete(ImmutableIndexInfo indexInfo) {
final IndexShard indexShard = indexInfo.shard();
if (indexShard != null) {
if (logger.isDebugEnabled())
logger.debug("deleting document from index.type={}.{} id={}", indexInfo.name, typeName, id);
if (!indexInfo.updated)
indexInfo.updated = true;
Engine.Delete delete = indexShard.prepareDeleteOnPrimary(typeName, id,
indexInfo.versionLessEngine ? 1 : Versions.MATCH_ANY,
indexInfo.versionLessEngine ? VersionType.EXTERNAL : VersionType.INTERNAL);
indexShard.delete(delete);
}
}
}
/**
* Notification of a top level partition delete.
* @param deletionTime
*/
@Override
public void partitionDelete(DeletionTime deletionTime) {
Long token_long = (Long) key.getToken().getTokenValue();
String typeName = InternalCassandraClusterService.cfNameToType(ElasticSecondaryIndex.this.baseCfs.metadata.cfName);
NumericRangeQuery<Long> tokenRangeQuery = NumericRangeQuery.newLongRange(TokenFieldMapper.NAME, InternalCassandraClusterService.defaultPrecisionStep, token_long, token_long, true, true);
mappingInfoLock.readLock().lock();
try {
// Delete documents where _token = token_long + _type = typeName
for (ImmutableMappingInfo.ImmutableIndexInfo indexInfo : indices) {
if (logger.isTraceEnabled())
logger.trace("deleting documents where _token={} from index.type={}.{} id={}", token_long, indexInfo.name, typeName);
IndexShard indexShard = indexInfo.indexService.shard(0);
if (indexShard != null) {
if (!indexInfo.updated)
indexInfo.updated = true;
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.add( new TermQuery(new Term(TypeFieldMapper.NAME, indexInfo.indexService.mapperService().documentMapper(typeName).typeMapper().fieldType().indexedValueForSearch(typeName))), Occur.FILTER);
builder.add(tokenRangeQuery, Occur.FILTER);
DeleteByQuery deleteByQuery = new DeleteByQuery(builder.build(), null, null, null, null, Operation.Origin.PRIMARY, System.currentTimeMillis(), typeName);
indexShard.engine().delete(deleteByQuery);
}
}
} catch(Throwable t) {
logger.error("Unexpected error", t);
} finally {
mappingInfoLock.readLock().unlock();
}
}
/**
* Notification of a RangeTombstone.
* An update of a single partition may contain multiple RangeTombstones,
* and a notification will be passed for each of them.
* @param tombstone
*/
@Override
public void rangeTombstone(RangeTombstone tombstone) {
logger.warn("Ignoring range tombstone {}", tombstone);
}
}
}
public boolean isIndexing() {
if (!runsElassandra)
return false;
if (mappingInfo == null) {
if (logger.isWarnEnabled())
logger.warn("No Elasticsearch index ready");
return false;
}
if (mappingInfo.indices == null || mappingInfo.indices.length == 0) {
if (logger.isWarnEnabled())
logger.warn("No Elasticsearch index configured for {}.{}",this.baseCfs.metadata.ksName, this.baseCfs.metadata.cfName);
return false;
}
return true;
}
public void initMapping() {
mappingInfoLock.writeLock().lock();
try {
mappingInfo = new ImmutableMappingInfo(this.clusterService.state());
logger.debug("Secondary index=[{}] initialized, metadata.version={} mappingInfo.indices={}",
index_name, mappingInfo.metadataVersion, mappingInfo.indices==null ? null : Arrays.stream(mappingInfo.indices).map(i -> i.name));
} catch(Exception e) {
logger.error("Failed to update mapping index=[{}]",e ,index_name);
} finally {
mappingInfoLock.writeLock().unlock();
}
}
// TODO: notify 2i only for udated indices (not all)
@Override
public void clusterChanged(ClusterChangedEvent event) {
boolean updateMapping = false;
if (event.blocksChanged()) {
updateMapping = true;
} else {
for (ObjectCursor<IndexMetaData> cursor : event.state().metaData().indices().values()) {
IndexMetaData indexMetaData = cursor.value;
if (indexMetaData.keyspace().equals(this.baseCfs.metadata.ksName) &&
indexMetaData.mapping(InternalCassandraClusterService.cfNameToType(this.baseCfs.name)) != null &&
(event.indexRoutingTableChanged(indexMetaData.getIndex()) || event.indexMetaDataChanged(indexMetaData))) {
updateMapping = true;
break;
}
}
}
if (updateMapping) {
mappingInfoLock.writeLock().lock();
try {
mappingInfo = new ImmutableMappingInfo(event.state());
logger.debug("secondary index=[{}] metadata.version={} mappingInfo.indices={}",
this.index_name, event.state().metaData().version(), mappingInfo.indices == null ? "" : Arrays.stream(mappingInfo.indices).map(i -> i.name));
} catch(Exception e) {
logger.error("Failed to update mapping index=[{}]", e, index_name);
} finally {
mappingInfoLock.writeLock().unlock();
}
}
}
public Callable<?> getInitializationTask()
{
if (this.initialized.compareAndSet(false, true)) {
return () -> {
logger.debug("Initializing elastic secondary index [{}]", index_name);
initMapping();
// Avoid inter-bocking with Keyspace.open()->rebuild()->flush()->open().
if (userKeyspaceInitialized)
baseCfs.indexManager.buildIndexBlocking(this);
return null;
};
} else {
return null;
}
}
public Callable<?> getMetadataReloadTask(IndexMetadata indexMetadata) {
return null;
}
/**
* Cassandra index flush => Elasticsearch flush => lucene commit and disk sync.
*/
public Callable<?> getBlockingFlushTask()
{
return () -> {
if (isIndexing()) {
for(ImmutableMappingInfo.ImmutableIndexInfo indexInfo : mappingInfo.indices) {
try {
IndexShard indexShard = indexInfo.indexService.shard(0);
if (indexShard != null && indexInfo.updated) {
if (indexShard.state() == IndexShardState.STARTED) {
long start = System.currentTimeMillis();
indexInfo.updated = false; // reset updated state
indexShard.flush(new FlushRequest().force(false).waitIfOngoing(true));
if (logger.isInfoEnabled())
logger.info("Elasticsearch index=[{}] flushed, duration={}ms",indexInfo.name, System.currentTimeMillis() - start);
} else {
if (logger.isDebugEnabled())
logger.debug("Cannot flush index=[{}], state=[{}]",indexInfo.name, indexShard.state());
}
}
} catch (ElasticsearchException e) {
logger.error("Error while flushing index=[{}]",e,indexInfo.name);
}
}
}
return null;
};
}
static FileAttribute<?> snapshotDirPermissions = PosixFilePermissions.asFileAttribute(EnumSet.of(
PosixFilePermission.OWNER_EXECUTE,
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.GROUP_EXECUTE,
PosixFilePermission.GROUP_READ,
PosixFilePermission.OTHERS_EXECUTE,
PosixFilePermission.OTHERS_READ));
/**
* Cassandra table snapshot => hard links associated elasticsearch lucene files.
*/
@SuppressForbidden(reason="File used for snapshots")
public Callable<?> getSnapshotWithoutFlushTask(String snapshotName)
{
return () -> {
if (isIndexing()) {
for(ImmutableMappingInfo.ImmutableIndexInfo indexInfo : mappingInfo.indices) {
IndexShard indexShard = indexInfo.indexService.shard(0);
if (indexShard != null && indexInfo.snapshot) {
if (indexShard.state() == IndexShardState.STARTED) {
// snapshotPath = data/elasticsearch.data/<cluster_name>/nodes/0/snapshots
Path snapshotPath = indexShard.shardPath().resolveSnapshot();
if ((Files.notExists(snapshotPath)))
Files.createDirectory(snapshotPath, snapshotDirPermissions);
// snapshotIndex = data/elasticsearch.data/<cluster_name>/nodes/0/snapshots/<index_name>
Path snapshotIndex = snapshotPath.resolve(indexShard.shardId().getIndex());
if ((Files.notExists(snapshotIndex)))
Files.createDirectory(snapshotIndex, snapshotDirPermissions);
// snapshotDir = data/elasticsearch.data/<cluster_name>/nodes/0/snapshots/<index_name>/<snapshot_name>
Path snapshotDir = Files.createDirectory(snapshotIndex.resolve(snapshotName), snapshotDirPermissions);
Path indexPath = indexShard.shardPath().resolveIndex();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(indexPath, "{_*.*,segments*}")) {
for (Path luceneFile: stream) {
File targetLink = new File(snapshotDir.toFile(), luceneFile.getFileName().toString());
FileUtils.createHardLink(luceneFile.toFile(), targetLink);
}
if (logger.isDebugEnabled())
logger.debug("Elasticsearch index=[{}], snapshot=[{}], path=[{}]",indexInfo.name, snapshotName, snapshotDir.toString());
} catch (DirectoryIteratorException ex) {
logger.error("Failed to retreive lucene files in {}", ex, indexPath);
}
} else {
if (logger.isDebugEnabled())
logger.debug("Cannot snapshot index=[{}], state=[{}], snapshot=[{}]",indexInfo.name, indexShard.state(), snapshotName);
}
}
}
}
return null;
};
}
public Callable<?> getInvalidateTask() {
return () -> {
this.clusterService.remove(this);
elasticSecondayIndices.remove(index_name);
return null;
};
}
public Callable<?> getTruncateTask(long truncatedAt) {
return () -> {
if (isIndexing()) {
for(ImmutableMappingInfo.ImmutableIndexInfo indexInfo : mappingInfo.indices) {
try {
IndexShard indexShard = indexInfo.indexService.shard(0);
if (indexShard != null) {
DocumentMapper docMapper = indexInfo.indexService.mapperService().documentMapper(mappingInfo.typeName);
Query query = new TermQuery(new Term(TypeFieldMapper.NAME, docMapper.typeMapper().fieldType().indexedValueForSearch(mappingInfo.typeName)));
if (logger.isDebugEnabled()) {
logger.debug("truncating from ks.cf={}.{} query={} in elasticsearch index=[{}]", baseCfs.metadata.ksName, baseCfs.name, query, indexInfo.name);
}
if (!indexInfo.updated)
indexInfo.updated = true;
DeleteByQuery deleteByQuery = new DeleteByQuery(query, null, null, null, null, Operation.Origin.PRIMARY, System.currentTimeMillis(), mappingInfo.typeName);
indexShard.engine().delete(deleteByQuery);
}
} catch (ElasticsearchException e) {
logger.error("Error while truncating index=[{}]", e, indexInfo.name);
}
}
}
return null;
};
}
public boolean shouldBuildBlocking() {
return isIndexing();
}
public long getEstimatedResultRows() {
// TODO Auto-generated method stub
return 0;
}
public void validate(PartitionUpdate update) throws InvalidRequestException {
// TODO Auto-generated method stub
}
public boolean dependsOn(ColumnDefinition column) {
return this.indexedColumns.contains(column);
}
@Override
public IndexMetadata getIndexMetadata() {
return this.indexMetadata;
}
@Override
public void register(IndexRegistry registry) {
registry.registerIndex(this);
}
@Override
public Optional<ColumnFamilyStore> getBackingTable() {
return Optional.empty();
}
@Override
public boolean supportsExpression(ColumnDefinition column, Operator operator) {
return false;
}
@Override
public AbstractType<?> customExpressionValueType() {
return null;
}
@Override
public RowFilter getPostIndexQueryFilter(RowFilter filter) {
// TODO Auto-generated method stub
return null;
}
@Override
public BiFunction<PartitionIterator, ReadCommand, PartitionIterator> postProcessorFor(ReadCommand command) {
// TODO Auto-generated method stub
return null;
}
@Override
public Searcher searcherFor(ReadCommand command) {
// TODO Auto-generated method stub
return null;
}
public Indexer indexerFor(DecoratedKey key, PartitionColumns columns, int nowInSec, Group opGroup, Type transactionType) {
if (isIndexing()) {
if (transactionType == Type.COMPACTION && !this.mappingInfo.indexOnCompaction)
return null;
try {
if (baseCfs.getComparator().size() == 0)
return this.mappingInfo.new SkinnyRowcumentIndexer(key, columns, nowInSec, opGroup, transactionType);
else
return this.mappingInfo.new WideRowcumentIndexer(key, columns, nowInSec, opGroup, transactionType);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
return null;
}
}