/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.blur.manager.writer; import static org.apache.blur.metrics.MetricsConstants.BLUR; import static org.apache.blur.metrics.MetricsConstants.ORG_APACHE_BLUR; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.TimeUnit; import org.apache.blur.analysis.FieldManager; import org.apache.blur.lucene.search.IndexSearcherCloseable; import org.apache.blur.server.ShardContext; import org.apache.blur.server.TableContext; import org.apache.blur.thrift.BException; import org.apache.blur.thrift.MutationHelper; import org.apache.blur.thrift.generated.BlurException; import org.apache.blur.thrift.generated.Column; import org.apache.blur.thrift.generated.FetchRecordResult; import org.apache.blur.thrift.generated.Record; import org.apache.blur.thrift.generated.RecordMutation; import org.apache.blur.thrift.generated.RecordMutationType; import org.apache.blur.thrift.generated.Row; import org.apache.blur.thrift.generated.RowMutation; import org.apache.blur.thrift.generated.RowMutationType; import org.apache.blur.utils.BlurConstants; import org.apache.blur.utils.RowDocumentUtil; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.Field.Store; import org.apache.lucene.document.StringField; import org.apache.lucene.index.AtomicReader; import org.apache.lucene.index.AtomicReaderContext; import org.apache.lucene.index.DocsEnum; import org.apache.lucene.index.Fields; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.Term; import org.apache.lucene.index.Terms; import org.apache.lucene.index.TermsEnum; import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.util.BytesRef; import com.yammer.metrics.Metrics; import com.yammer.metrics.core.Meter; import com.yammer.metrics.core.MetricName; public class MutatableAction extends IndexAction { private static final Meter _writeRecordsMeter; private static final Meter _writeRowMeter; static { MetricName metricName1 = new MetricName(ORG_APACHE_BLUR, BLUR, "Write Records/s"); MetricName metricName2 = new MetricName(ORG_APACHE_BLUR, BLUR, "Write Row/s"); _writeRecordsMeter = Metrics.newMeter(metricName1, "Records/s", TimeUnit.SECONDS); _writeRowMeter = Metrics.newMeter(metricName2, "Row/s", TimeUnit.SECONDS); } static abstract class BaseRecordMutatorIterator implements Iterable<Record> { private final Iterable<Record> _iterable; private final Map<String, Record> _records; public BaseRecordMutatorIterator(Iterable<Record> iterable, List<Record> records) { _iterable = iterable; _records = new TreeMap<String, Record>(); for (Record r : records) { _records.put(r.getRecordId(), r); } } protected abstract Record handleRecordMutate(Record existingRecord, Record newRecord); @Override public Iterator<Record> iterator() { final Iterator<Record> iterator = _iterable.iterator(); return new Iterator<Record>() { private SortedSet<String> _needToBeApplied = new TreeSet<String>(_records.keySet()); private boolean _append = false; @Override public boolean hasNext() { boolean hasNext = iterator.hasNext(); if (hasNext) { return true; } if (areAllApplied()) { return false; // Already applied changes, finished. } _append = true; return true; // Still need to add new records. } private boolean areAllApplied() { return _needToBeApplied.size() == 0; } @Override public Record next() { if (_append) { String first = _needToBeApplied.first(); _needToBeApplied.remove(first); return _records.get(first); } Record record = iterator.next(); String recordId = record.getRecordId(); Record newRecord = _records.get(recordId); if (newRecord != null) { record = handleRecordMutate(record, newRecord); _needToBeApplied.remove(recordId); } return record; } @Override public void remove() { throw new RuntimeException("Not Supported."); } }; } } static class UpdateRow extends InternalAction { static abstract class UpdateRowAction { abstract IterableRow performAction(IterableRow row); } private final List<UpdateRowAction> _actions = new ArrayList<UpdateRowAction>(); private UpdateRowAction _deleteRecordsAction; private final Set<String> _deleteRecordsActionRecordsIdToDelete = new HashSet<String>(); private UpdateRowAction _appendColumnsAction; private List<Record> _appendColumnsActionRecords = new ArrayList<Record>(); private UpdateRowAction _replaceColumnsAction; private List<Record> _replaceColumnsActionRecords = new ArrayList<Record>(); private UpdateRowAction _replaceRecordAction; private List<Record> _replaceRecordActionRecords = new ArrayList<Record>(); private final String _rowId; private final TableContext _tableContext; private final FieldManager _fieldManager; UpdateRow(String rowId, TableContext tableContext) { _rowId = rowId; _tableContext = tableContext; _fieldManager = _tableContext.getFieldManager(); } void deleteRecord(final String recordId) { if (_deleteRecordsAction == null) { _deleteRecordsAction = new UpdateRowAction() { @Override IterableRow performAction(IterableRow row) { if (row == null) { return null; } else { return new IterableRow(row.getRowId(), new DeleteRecordIterator(row, _deleteRecordsActionRecordsIdToDelete)); } } }; _actions.add(_deleteRecordsAction); } _deleteRecordsActionRecordsIdToDelete.add(recordId); } static class DeleteRecordIterator implements Iterable<Record> { private final Set<String> _recordsIdToDelete; private final Iterable<Record> _iterable; public DeleteRecordIterator(Iterable<Record> iterable, Set<String> recordsIdToDelete) { _recordsIdToDelete = recordsIdToDelete; _iterable = iterable; } @Override public Iterator<Record> iterator() { final GenericPeekableIterator<Record> iterator = GenericPeekableIterator.wrap(_iterable.iterator()); return new Iterator<Record>() { @Override public boolean hasNext() { Record record = iterator.peek(); if (record == null) { return false; } if (_recordsIdToDelete.contains(record.getRecordId())) { iterator.next();// Eat the delete return hasNext();// Move to the next record } return iterator.hasNext(); } @Override public Record next() { return iterator.next(); } @Override public void remove() { throw new RuntimeException("Not Supported."); } }; } } void appendColumns(final Record record) { if (_appendColumnsAction == null) { _appendColumnsAction = new UpdateRowAction() { @Override IterableRow performAction(IterableRow row) { if (row == null) { return new IterableRow(_rowId, _appendColumnsActionRecords); } else { return new IterableRow(row.getRowId(), new AppendColumnsIterator(row, _appendColumnsActionRecords)); } } }; _actions.add(_appendColumnsAction); } _appendColumnsActionRecords.add(record); } static class AppendColumnsIterator extends BaseRecordMutatorIterator { public AppendColumnsIterator(Iterable<Record> iterable, List<Record> records) { super(iterable, records); } @Override protected Record handleRecordMutate(Record existingRecord, Record newRecord) { for (Column column : newRecord.getColumns()) { existingRecord.addToColumns(column); } return existingRecord; } } void replaceColumns(final Record record) { if (_replaceColumnsAction == null) { _replaceColumnsAction = new UpdateRowAction() { @Override IterableRow performAction(IterableRow row) { if (row == null) { return new IterableRow(_rowId, _replaceColumnsActionRecords); } else { return new IterableRow(row.getRowId(), new ReplaceColumnsIterator(row, _replaceColumnsActionRecords)); } } }; _actions.add(_replaceColumnsAction); } _replaceColumnsActionRecords.add(record); } static class ReplaceColumnsIterator extends BaseRecordMutatorIterator { public ReplaceColumnsIterator(Iterable<Record> iterable, List<Record> records) { super(iterable, records); } @Override protected Record handleRecordMutate(Record existingRecord, Record newRecord) { return replaceColumns(existingRecord, newRecord); } } protected static Record replaceColumns(Record existing, Record newRecord) { Map<String, List<Column>> existingColumns = getColumnMap(existing.getColumns()); Map<String, List<Column>> newColumns = getColumnMap(newRecord.getColumns()); existingColumns.putAll(newColumns); Record record = new Record(); record.setFamily(existing.getFamily()); record.setRecordId(existing.getRecordId()); record.setColumns(toList(existingColumns.values())); return record; } private static List<Column> toList(Collection<List<Column>> values) { ArrayList<Column> list = new ArrayList<Column>(); for (List<Column> v : values) { list.addAll(v); } return list; } private static Map<String, List<Column>> getColumnMap(List<Column> columns) { Map<String, List<Column>> columnMap = new TreeMap<String, List<Column>>(); for (Column column : columns) { String name = column.getName(); List<Column> list = columnMap.get(name); if (list == null) { list = new ArrayList<Column>(); columnMap.put(name, list); } list.add(column); } return columnMap; } void replaceRecord(final Record record) { if (_replaceRecordAction == null) { _replaceRecordAction = new UpdateRowAction() { @Override IterableRow performAction(IterableRow row) { if (row == null) { // New Row return new IterableRow(_rowId, _replaceRecordActionRecords); } else { // Existing Row return new IterableRow(row.getRowId(), new ReplaceRecordIterator(row, _replaceRecordActionRecords)); } } }; _actions.add(_replaceRecordAction); } _replaceRecordActionRecords.add(record); } static class ReplaceRecordIterator extends BaseRecordMutatorIterator { public ReplaceRecordIterator(Iterable<Record> iterable, List<Record> records) { super(iterable, records); } @Override protected Record handleRecordMutate(Record existingRecord, Record newRecord) { return newRecord; } } @Override void performAction(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException { IterableRow iterableRow = getIterableRow(_rowId, searcher); for (UpdateRowAction action : _actions) { iterableRow = action.performAction(iterableRow); } Term term = createRowId(_rowId); if (iterableRow != null) { RecordToDocumentIterable docsToUpdate = new RecordToDocumentIterable(iterableRow, _fieldManager); Iterator<Iterable<Field>> iterator = docsToUpdate.iterator(); final GenericPeekableIterator<Iterable<Field>> gpi = GenericPeekableIterator.wrap(iterator); if (gpi.peek() != null) { writer.updateDocuments(term, wrapPrimeDoc(new Iterable<Iterable<Field>>() { @Override public Iterator<Iterable<Field>> iterator() { return gpi; } })); } else { writer.deleteDocuments(term); } _writeRecordsMeter.mark(docsToUpdate.count()); } _writeRowMeter.mark(); } private static class AtomicReaderTermsEnum { AtomicReader _atomicReader; TermsEnum _termsEnum; AtomicReaderTermsEnum(AtomicReader atomicReader, TermsEnum termsEnum) { _atomicReader = atomicReader; _termsEnum = termsEnum; } } private IterableRow getIterableRow(String rowId, IndexSearcherCloseable searcher) throws IOException { IndexReader indexReader = searcher.getIndexReader(); BytesRef rowIdRef = new BytesRef(rowId); List<AtomicReaderTermsEnum> possibleRowIds = new ArrayList<AtomicReaderTermsEnum>(); for (AtomicReaderContext atomicReaderContext : indexReader.leaves()) { AtomicReader atomicReader = atomicReaderContext.reader(); Fields fields = atomicReader.fields(); if (fields == null) { continue; } Terms terms = fields.terms(BlurConstants.ROW_ID); if (terms == null) { continue; } TermsEnum termsEnum = terms.iterator(null); if (!termsEnum.seekExact(rowIdRef, true)) { continue; } // need atomic read as well... possibleRowIds.add(new AtomicReaderTermsEnum(atomicReader, termsEnum)); } if (possibleRowIds.isEmpty()) { return null; } return new IterableRow(rowId, getRecords(possibleRowIds)); } private Iterable<Record> getRecords(final List<AtomicReaderTermsEnum> possibleRowIds) { return new Iterable<Record>() { @Override public Iterator<Record> iterator() { final List<DocsEnum> docsEnums = new ArrayList<DocsEnum>(); for (AtomicReaderTermsEnum atomicReaderTermsEnum : possibleRowIds) { try { docsEnums.add(atomicReaderTermsEnum._termsEnum.docs(atomicReaderTermsEnum._atomicReader.getLiveDocs(), null)); } catch (IOException e) { throw new RuntimeException(e); } } return new Iterator<Record>() { private int _index = 0; private boolean _nextCalled; private int _docId; @Override public boolean hasNext() { try { if (_nextCalled) { if (_docId == DocIdSetIterator.NO_MORE_DOCS) { return false; } return true; } while (true) { if (_index >= docsEnums.size()) { _nextCalled = true; _docId = DocIdSetIterator.NO_MORE_DOCS; return false; } DocsEnum docsEnum = docsEnums.get(_index); int docId = docsEnum.nextDoc(); if (docId != DocIdSetIterator.NO_MORE_DOCS) { _nextCalled = true; _docId = docId; return true; } _index++; } } catch (IOException e) { throw new RuntimeException(e); } } @Override public Record next() { _nextCalled = false; AtomicReaderTermsEnum atomicReaderTermsEnum = possibleRowIds.get(_index); try { Document document = atomicReaderTermsEnum._atomicReader.document(_docId); FetchRecordResult fetchRecordResult = RowDocumentUtil.getRecord(document); return fetchRecordResult.getRecord(); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void remove() { throw new RuntimeException("Not Supported."); } }; } }; } Iterable<Iterable<Field>> wrapPrimeDoc(final Iterable<Iterable<Field>> iterable) { return new Iterable<Iterable<Field>>() { @Override public Iterator<Iterable<Field>> iterator() { final Iterator<Iterable<Field>> iterator = iterable.iterator(); return new Iterator<Iterable<Field>>() { private boolean _first = true; @Override public boolean hasNext() { return iterator.hasNext(); } @Override public Iterable<Field> next() { Iterable<Field> fields = iterator.next(); if (_first) { _first = false; return addPrimeDocField(fields); } else { return fields; } } private Iterable<Field> addPrimeDocField(Iterable<Field> fields) { return new IterablePlusOne<Field>(new StringField(BlurConstants.PRIME_DOC, BlurConstants.PRIME_DOC_VALUE, Store.NO), fields); } @Override public void remove() { throw new RuntimeException("Not Supported."); } }; } }; } } static abstract class InternalAction { abstract void performAction(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException; } private final List<InternalAction> _actions = new ArrayList<InternalAction>(); private final Map<String, UpdateRow> _rowUpdates = new HashMap<String, UpdateRow>(); private final FieldManager _fieldManager; private final TableContext _tableContext; public MutatableAction(ShardContext context) { _tableContext = context.getTableContext(); _fieldManager = _tableContext.getFieldManager(); } public void deleteRow(final String rowId) { _actions.add(new InternalAction() { @Override void performAction(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException { writer.deleteDocuments(createRowId(rowId)); _writeRowMeter.mark(); } }); } public void replaceRow(final Row row) { _actions.add(new InternalAction() { @Override void performAction(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException { List<List<Field>> docs = RowDocumentUtil.getDocs(row, _fieldManager); Term rowId = createRowId(row.getId()); writer.updateDocuments(rowId, docs); _writeRecordsMeter.mark(docs.size()); _writeRowMeter.mark(); } }); } public void deleteRecord(final String rowId, final String recordId) { UpdateRow updateRow = getUpdateRow(rowId); updateRow.deleteRecord(recordId); } public void replaceRecord(final String rowId, final Record record) { UpdateRow updateRow = getUpdateRow(rowId); updateRow.replaceRecord(record); } public void appendColumns(final String rowId, final Record record) { UpdateRow updateRow = getUpdateRow(rowId); updateRow.appendColumns(record); } public void replaceColumns(final String rowId, final Record record) { UpdateRow updateRow = getUpdateRow(rowId); updateRow.replaceColumns(record); } @Override public void performMutate(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException { try { for (InternalAction internalAction : _actions) { internalAction.performAction(searcher, writer); } } finally { _actions.clear(); } } public static Term createRowId(String id) { return new Term(BlurConstants.ROW_ID, id); } public static Term createRecordId(String id) { return new Term(BlurConstants.RECORD_ID, id); } private synchronized UpdateRow getUpdateRow(String rowId) { UpdateRow updateRow = _rowUpdates.get(rowId); if (updateRow == null) { updateRow = new UpdateRow(rowId, _tableContext); _rowUpdates.put(rowId, updateRow); _actions.add(updateRow); } return updateRow; } @Override public void doPreCommit(IndexSearcherCloseable indexSearcher, IndexWriter writer) { } @Override public void doPostCommit(IndexWriter writer) { } @Override public void doPreRollback(IndexWriter writer) { } @Override public void doPostRollback(IndexWriter writer) { } public void mutate(RowMutation mutation) { RowMutationType type = mutation.rowMutationType; switch (type) { case REPLACE_ROW: Row row = MutationHelper.getRowFromMutations(mutation.rowId, mutation.recordMutations); replaceRow(row); break; case UPDATE_ROW: doUpdateRowMutation(mutation, this); break; case DELETE_ROW: deleteRow(mutation.rowId); break; default: throw new RuntimeException("Not supported [" + type + "]"); } } private void doUpdateRowMutation(RowMutation mutation, MutatableAction mutatableAction) { String rowId = mutation.getRowId(); for (RecordMutation recordMutation : mutation.getRecordMutations()) { RecordMutationType type = recordMutation.recordMutationType; Record record = recordMutation.getRecord(); switch (type) { case DELETE_ENTIRE_RECORD: mutatableAction.deleteRecord(rowId, record.getRecordId()); break; case APPEND_COLUMN_VALUES: mutatableAction.appendColumns(rowId, record); break; case REPLACE_ENTIRE_RECORD: mutatableAction.replaceRecord(rowId, record); break; case REPLACE_COLUMNS: mutatableAction.replaceColumns(rowId, record); break; default: throw new RuntimeException("Unsupported record mutation type [" + type + "]"); } } } public void mutate(List<RowMutation> mutations) { for (int i = 0; i < mutations.size(); i++) { mutate(mutations.get(i)); } } public static List<RowMutation> reduceMutates(List<RowMutation> mutations) throws BlurException { Map<String, RowMutation> mutateMap = new TreeMap<String, RowMutation>(); for (RowMutation mutation : mutations) { if (mutation.getRowId() == null) { throw new BException("Mutation has null rowid [{0}]", mutation); } RowMutation rowMutation = mutateMap.get(mutation.getRowId()); if (rowMutation != null) { mutateMap.put(mutation.getRowId(), merge(rowMutation, mutation)); } else { mutateMap.put(mutation.getRowId(), mutation); } } return new ArrayList<RowMutation>(mutateMap.values()); } private static RowMutation merge(RowMutation mutation1, RowMutation mutation2) throws BlurException { RowMutationType rowMutationType1 = mutation1.getRowMutationType(); RowMutationType rowMutationType2 = mutation2.getRowMutationType(); if (!rowMutationType1.equals(rowMutationType2)) { throw new BException( "RowMutation conflict, cannot perform 2 different operations on the same row in the same batch. [{0}] [{1}]", mutation1, mutation2); } if (rowMutationType1.equals(RowMutationType.DELETE_ROW)) { // Since both are trying to delete the same row, just pick one and move // on. return mutation1; } else if (rowMutationType1.equals(RowMutationType.REPLACE_ROW)) { throw new BException( "RowMutation conflict, cannot perform 2 different REPLACE_ROW mutations on the same row in the same batch. [{0}] [{1}]", mutation1, mutation2); } else { // Now this is a row update, so try to merge the record mutations List<RecordMutation> recordMutations1 = mutation1.getRecordMutations(); List<RecordMutation> recordMutations2 = mutation2.getRecordMutations(); List<RecordMutation> mergedRecordMutations = merge(recordMutations1, recordMutations2); mutation1.setRecordMutations(mergedRecordMutations); return mutation1; } } private static List<RecordMutation> merge(List<RecordMutation> recordMutations1, List<RecordMutation> recordMutations2) throws BException { Map<String, RecordMutation> recordMutationMap = new TreeMap<String, RecordMutation>(); merge(recordMutations1, recordMutationMap); merge(recordMutations2, recordMutationMap); return new ArrayList<RecordMutation>(recordMutationMap.values()); } private static void merge(List<RecordMutation> recordMutations, Map<String, RecordMutation> recordMutationMap) throws BException { for (RecordMutation recordMutation : recordMutations) { Record record = recordMutation.getRecord(); String recordId = record.getRecordId(); RecordMutation existing = recordMutationMap.get(recordId); if (existing != null) { recordMutationMap.put(recordId, merge(recordMutation, existing)); } else { recordMutationMap.put(recordId, recordMutation); } } } private static RecordMutation merge(RecordMutation recordMutation1, RecordMutation recordMutation2) throws BException { RecordMutationType recordMutationType1 = recordMutation1.getRecordMutationType(); RecordMutationType recordMutationType2 = recordMutation2.getRecordMutationType(); if (!recordMutationType1.equals(recordMutationType2)) { throw new BException( "RecordMutation conflict, cannot perform 2 different operations on the same record in the same row in the same batch. [{0}] [{1}]", recordMutation1, recordMutation2); } if (recordMutationType1.equals(RecordMutationType.DELETE_ENTIRE_RECORD)) { // Since both are trying to delete the same record, just pick one and move // on. return recordMutation1; } else if (recordMutationType1.equals(RecordMutationType.REPLACE_ENTIRE_RECORD)) { throw new BException( "RecordMutation conflict, cannot perform 2 different replace record operations on the same record in the same row in the same batch. [{0}] [{1}]", recordMutation1, recordMutation2); } else if (recordMutationType1.equals(RecordMutationType.REPLACE_COLUMNS)) { throw new BException( "RecordMutation conflict, cannot perform 2 different replace columns operations on the same record in the same row in the same batch. [{0}] [{1}]", recordMutation1, recordMutation2); } else { Record record1 = recordMutation1.getRecord(); Record record2 = recordMutation2.getRecord(); String family1 = record1.getFamily(); String family2 = record2.getFamily(); if (isSameFamily(family1, family2)) { record1.getColumns().addAll(record2.getColumns()); return recordMutation1; } else { throw new BException("RecordMutation conflict, cannot merge records with different family. [{0}] [{1}]", recordMutation1, recordMutation2); } } } private static boolean isSameFamily(String family1, String family2) { if (family1 == null && family2 == null) { return true; } if (family1 != null && family1.equals(family2)) { return true; } return false; } }