/* * 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.ignite.internal.processors.query.h2.opt; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteInterruptedException; import org.apache.ignite.internal.processors.cache.CacheObject; import org.apache.ignite.internal.processors.cache.GridCacheContext; import org.apache.ignite.internal.processors.cache.KeyCacheObject; import org.apache.ignite.internal.processors.cache.version.GridCacheVersion; import org.apache.ignite.internal.processors.query.h2.database.H2RowFactory; import org.apache.ignite.internal.processors.query.h2.database.H2TreeIndex; import org.apache.ignite.internal.util.offheap.unsafe.GridUnsafeMemory; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.lang.IgniteBiTuple; import org.h2.command.ddl.CreateTableData; import org.h2.engine.Session; import org.h2.index.Index; import org.h2.index.IndexType; import org.h2.index.SpatialIndex; import org.h2.message.DbException; import org.h2.result.Row; import org.h2.result.SearchRow; import org.h2.result.SortOrder; import org.h2.table.Column; import org.h2.table.IndexColumn; import org.h2.table.TableBase; import org.h2.table.TableType; import org.h2.value.Value; import org.jetbrains.annotations.Nullable; import org.jsr166.ConcurrentHashMap8; import org.jsr166.LongAdder8; import static org.apache.ignite.cache.CacheMode.PARTITIONED; import static org.apache.ignite.internal.processors.query.h2.opt.GridH2AbstractKeyValueRow.KEY_COL; import static org.apache.ignite.internal.processors.query.h2.opt.GridH2AbstractKeyValueRow.VAL_COL; import static org.apache.ignite.internal.processors.query.h2.opt.GridH2QueryType.MAP; /** * H2 Table implementation. */ public class GridH2Table extends TableBase { /** */ private final String spaceName; /** */ private final GridH2RowDescriptor desc; /** */ private volatile ArrayList<Index> idxs; /** */ private final Map<String, GridH2IndexBase> tmpIdxs = new HashMap<>(); /** */ private final ReadWriteLock lock; /** */ private boolean destroyed; /** */ private final ConcurrentMap<Session, Boolean> sessions = new ConcurrentHashMap8<>(); /** */ private final AtomicReference<Object[]> actualSnapshot = new AtomicReference<>(); /** */ private IndexColumn affKeyCol; /** */ private final LongAdder8 size = new LongAdder8(); /** */ private final boolean snapshotEnabled; /** */ private final H2RowFactory rowFactory; /** */ private volatile boolean rebuildFromHashInProgress; /** * Creates table. * * @param createTblData Table description. * @param desc Row descriptor. * @param rowFactory Row factory. * @param idxsFactory Indexes factory. * @param spaceName Space name. */ public GridH2Table(CreateTableData createTblData, @Nullable GridH2RowDescriptor desc, H2RowFactory rowFactory, GridH2SystemIndexFactory idxsFactory, @Nullable String spaceName) { super(createTblData); assert idxsFactory != null; this.desc = desc; this.spaceName = spaceName; if (desc != null && desc.context() != null && !desc.context().customAffinityMapper()) { boolean affinityColExists = true; String affKey = desc.type().affinityKey(); int affKeyColId = -1; if (affKey != null) { String colName = desc.context().config().isSqlEscapeAll() ? affKey : affKey.toUpperCase(); if (doesColumnExist(colName)) affKeyColId = getColumn(colName).getColumnId(); else affinityColExists = false; } else affKeyColId = KEY_COL; if (affinityColExists) { affKeyCol = indexColumn(affKeyColId, SortOrder.ASCENDING); assert affKeyCol != null; } } this.rowFactory = rowFactory; // Indexes must be created in the end when everything is ready. idxs = idxsFactory.createSystemIndexes(this); assert idxs != null; List<Index> clones = new ArrayList<>(idxs.size()); for (Index index: idxs) { Index clone = createDuplicateIndexIfNeeded(index); if (clone != null) clones.add(clone); } idxs.addAll(clones); // Add scan index at 0 which is required by H2. if (idxs.size() >= 2 && index(0).getIndexType().isHash()) idxs.add(0, new GridH2PrimaryScanIndex(this, index(1), index(0))); else idxs.add(0, new GridH2PrimaryScanIndex(this, index(0), null)); snapshotEnabled = desc == null || desc.snapshotableIndex(); lock = new ReentrantReadWriteLock(); } /** * @return {@code true} If this is a partitioned table. */ public boolean isPartitioned() { return desc != null && desc.configuration().getCacheMode() == PARTITIONED; } /** * @return Affinity key column or {@code null} if not available. */ @Nullable public IndexColumn getAffinityKeyColumn() { return affKeyCol; } /** {@inheritDoc} */ @Override public long getDiskSpaceUsed() { return 0; } /** * @return Row descriptor. */ public GridH2RowDescriptor rowDescriptor() { return desc; } /** * @return Space name. */ @Nullable public String spaceName() { return spaceName; } /** {@inheritDoc} */ @Override public boolean lock(Session ses, boolean exclusive, boolean force) { Boolean putRes = sessions.putIfAbsent(ses, exclusive); // In accordance with base method semantics, we'll return true if we were already exclusively locked if (putRes != null) return putRes; ses.addLock(this); lock(exclusive); if (destroyed) { unlock(exclusive); throw new IllegalStateException("Table " + identifier() + " already destroyed."); } if (snapshotInLock()) snapshotIndexes(null); return false; } /** * @return {@code True} If we must snapshot and release index snapshots in {@link #lock(Session, boolean, boolean)} * and {@link #unlock(Session)} methods. */ private boolean snapshotInLock() { if (!snapshotEnabled) return false; GridH2QueryContext qctx = GridH2QueryContext.get(); // On MAP queries with distributed joins we lock tables before the queries. return qctx == null || qctx.type() != MAP || !qctx.hasIndexSnapshots(); } /** * @param qctx Query context. */ public void snapshotIndexes(GridH2QueryContext qctx) { if (!snapshotEnabled) return; Object[] snapshots; // Try to reuse existing snapshots outside of the lock. for (long waitTime = 200;; waitTime *= 2) { // Increase wait time to avoid starvation. snapshots = actualSnapshot.get(); if (snapshots != null) { // Reuse existing snapshot without locking. snapshots = doSnapshotIndexes(snapshots, qctx); if (snapshots != null) return; // Reused successfully. } if (tryLock(true, waitTime)) break; } try { ensureNotDestroyed(); // Try again inside of the lock. snapshots = actualSnapshot.get(); if (snapshots != null) // Try reusing. snapshots = doSnapshotIndexes(snapshots, qctx); if (snapshots == null) { // Reuse failed, produce new snapshots. snapshots = doSnapshotIndexes(null, qctx); assert snapshots != null; actualSnapshot.set(snapshots); } } finally { unlock(true); } } /** * @return Table identifier. */ public String identifier() { return getSchema().getName() + '.' + getName(); } /** * Acquire table lock. * * @param exclusive Exclusive flag. */ private void lock(boolean exclusive) { Lock l = exclusive ? lock.writeLock() : lock.readLock(); try { l.lockInterruptibly(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IgniteInterruptedException("Thread got interrupted while trying to acquire table lock.", e); } } /** * @param exclusive Exclusive lock. * @param waitMillis Milliseconds to wait for the lock. * @return Whether lock was acquired. */ private boolean tryLock(boolean exclusive, long waitMillis) { Lock l = exclusive ? lock.writeLock() : lock.readLock(); try { if (!l.tryLock(waitMillis, TimeUnit.MILLISECONDS)) return false; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IgniteInterruptedException("Thread got interrupted while trying to acquire table lock.", e); } return true; } /** * Release table lock. * * @param exclusive Exclusive flag. */ private void unlock(boolean exclusive) { Lock l = exclusive ? lock.writeLock() : lock.readLock(); l.unlock(); } /** * Check if table is not destroyed. */ private void ensureNotDestroyed() { if (destroyed) throw new IllegalStateException("Table " + identifier() + " already destroyed."); } /** * Must be called inside of write lock because when using multiple indexes we have to ensure that all of them have * the same contents at snapshot taking time. * * @param qctx Query context. * @return New indexes data snapshot. */ @SuppressWarnings("unchecked") private Object[] doSnapshotIndexes(Object[] snapshots, GridH2QueryContext qctx) { assert snapshotEnabled; if (snapshots == null) // Nothing to reuse, create new snapshots. snapshots = new Object[idxs.size() - 2]; // Take snapshots on all except first which is scan and second which is hash. for (int i = 2, len = idxs.size(); i < len; i++) { Object s = snapshots[i - 2]; boolean reuseExisting = s != null; if (!(idxs.get(i) instanceof GridH2IndexBase)) continue; s = index(i).takeSnapshot(s, qctx); if (reuseExisting && s == null) { // Existing snapshot was invalidated before we were able to reserve it. // Release already taken snapshots. if (qctx != null) qctx.clearSnapshots(); for (int j = 2; j < i; j++) if ((idxs.get(j) instanceof GridH2IndexBase)) index(j).releaseSnapshot(); // Drop invalidated snapshot. actualSnapshot.compareAndSet(snapshots, null); return null; } snapshots[i - 2] = s; } return snapshots; } /** {@inheritDoc} */ @Override public void close(Session ses) { // No-op. } /** * Destroy the table. */ public void destroy() { lock(true); try { ensureNotDestroyed(); assert sessions.isEmpty() : sessions; destroyed = true; for (int i = 1, len = idxs.size(); i < len; i++) if (idxs.get(i) instanceof GridH2IndexBase) index(i).destroy(); } finally { unlock(true); } } /** {@inheritDoc} */ @Override public void unlock(Session ses) { Boolean exclusive = sessions.remove(ses); if (exclusive == null) return; if (snapshotInLock()) releaseSnapshots(); unlock(exclusive); } /** * Releases snapshots. */ public void releaseSnapshots() { if (!snapshotEnabled) return; releaseSnapshots0(idxs); } /** * @param idxs Indexes. */ private void releaseSnapshots0(ArrayList<Index> idxs) { // Release snapshots on all except first which is scan and second which is hash. for (int i = 2, len = idxs.size(); i < len; i++) ((GridH2IndexBase)idxs.get(i)).releaseSnapshot(); } /** * Updates table for given key. If value is null then row with given key will be removed from table, * otherwise value and expiration time will be updated or new row will be added. * * @param key Key. * @param val Value. * @param expirationTime Expiration time. * @param rmv If {@code true} then remove, else update row. * @return {@code true} If operation succeeded. * @throws IgniteCheckedException If failed. */ public boolean update(KeyCacheObject key, int partId, CacheObject val, GridCacheVersion ver, long expirationTime, boolean rmv, long link) throws IgniteCheckedException { assert desc != null; GridH2Row row = desc.createRow(key, partId, val, ver, expirationTime); row.link = link; if (!rmv) ((GridH2AbstractKeyValueRow)row).valuesCache(new Value[getColumns().length]); try { return doUpdate(row, rmv); } finally { if (!rmv) ((GridH2AbstractKeyValueRow)row).valuesCache(null); } } /** * @param key Key to read. * @return Read value. * @throws IgniteCheckedException If failed. */ public IgniteBiTuple<CacheObject, GridCacheVersion> read( GridCacheContext cctx, KeyCacheObject key, int partId ) throws IgniteCheckedException { assert desc != null; GridH2Row row = desc.createRow(key, partId, null, null, 0); GridH2IndexBase primaryIdx = pk(); GridH2Row res = primaryIdx.findOne(row); return res != null ? F.t(res.val, res.ver) : null; } /** * Gets index by index. * * @param idx Index in list. * @return Index. */ private GridH2IndexBase index(int idx) { return (GridH2IndexBase)idxs.get(idx); } /** * Gets primary key. * * @return Primary key. */ private GridH2IndexBase pk() { return (GridH2IndexBase)idxs.get(2); } /** * For testing only. * * @param row Row. * @param del If given row should be deleted from table. * @return {@code True} if operation succeeded. * @throws IgniteCheckedException If failed. */ @SuppressWarnings("LockAcquiredButNotSafelyReleased") boolean doUpdate(final GridH2Row row, boolean del) throws IgniteCheckedException { // Here we assume that each key can't be updated concurrently and case when different indexes // getting updated from different threads with different rows with the same key is impossible. GridUnsafeMemory mem = desc == null ? null : desc.memory(); lock(false); if (mem != null) desc.guard().begin(); try { ensureNotDestroyed(); GridH2IndexBase pk = pk(); if (!del) { assert rowFactory == null || row.link != 0 : row; GridH2Row old = pk.put(row); // Put to PK. if (old == null) size.increment(); int len = idxs.size(); int i = 2; // Put row if absent to all indexes sequentially. // Start from 3 because 0 - Scan (don't need to update), 1 - PK hash (already updated), 2 - PK (already updated). while (++i < len) { if (!(idxs.get(i) instanceof GridH2IndexBase)) continue; GridH2IndexBase idx = index(i); addToIndex(idx, pk, row, old, false); } for (GridH2IndexBase idx : tmpIdxs.values()) addToIndex(idx, pk, row, old, true); } else { // index(1) is PK, get full row from there (search row here contains only key but no other columns). GridH2Row old = pk.remove(row); if (old != null) { // Remove row from all indexes. // Start from 3 because 0 - Scan (don't need to update), 1 - PK hash (already updated), 2 - PK (already updated). for (int i = 3, len = idxs.size(); i < len; i++) { if (!(idxs.get(i) instanceof GridH2IndexBase)) continue; Row res = index(i).remove(old); assert eq(pk, res, old) : "\n" + old + "\n" + res + "\n" + i + " -> " + index(i).getName(); } for (GridH2IndexBase idx : tmpIdxs.values()) idx.remove(old); size.decrement(); } else return false; } // The snapshot is not actual after update. actualSnapshot.set(null); return true; } finally { unlock(false); if (mem != null) desc.guard().end(); } } /** * Add row to index. * * @param idx Index to add row to. * @param pk Primary key index. * @param row Row to add to index. * @param old Previous row state, if any. * @param tmp {@code True} if this is proposed index which may be not consistent yet. */ private void addToIndex(GridH2IndexBase idx, Index pk, GridH2Row row, GridH2Row old, boolean tmp) { assert !idx.getIndexType().isUnique() : "Unique indexes are not supported: " + idx; GridH2Row old2 = idx.put(row); if (old2 != null) { // Row was replaced in index. if (!tmp) { if (!eq(pk, old2, old)) throw new IllegalStateException("Row conflict should never happen, unique indexes are " + "not supported [idx=" + idx + ", old=" + old + ", old2=" + old2 + ']'); } } else if (old != null) // Row was not replaced, need to remove manually. idx.removex(old); } /** * Check row equality. * * @param pk Primary key index. * @param r1 First row. * @param r2 Second row. * @return {@code true} if rows are the same. */ private static boolean eq(Index pk, SearchRow r1, SearchRow r2) { return r1 == r2 || (r1 != null && r2 != null && pk.compareRows(r1, r2) == 0); } /** * For testing only. * * @return Indexes. */ ArrayList<GridH2IndexBase> indexes() { ArrayList<GridH2IndexBase> res = new ArrayList<>(idxs.size() - 2); for (int i = 2, len = idxs.size(); i < len; i++) if (idxs.get(i) instanceof GridH2IndexBase) res.add(index(i)); return res; } /** * */ public void markRebuildFromHashInProgress(boolean value) { rebuildFromHashInProgress = value; } /** * */ public boolean rebuildFromHashInProgress() { return rebuildFromHashInProgress; } /** {@inheritDoc} */ @Override public Index addIndex(Session ses, String idxName, int idxId, IndexColumn[] cols, IndexType idxType, boolean create, String idxComment) { return commitUserIndex(ses, idxName); } /** * Add index that is in an intermediate state and is still being built, thus is not used in queries until it is * promoted. * * @param idx Index to add. * @throws IgniteCheckedException If failed. */ public void proposeUserIndex(Index idx) throws IgniteCheckedException { assert idx instanceof GridH2IndexBase; lock(true); try { ensureNotDestroyed(); for (Index oldIdx : idxs) { if (F.eq(oldIdx.getName(), idx.getName())) throw new IgniteCheckedException("Index already exists: " + idx.getName()); } Index oldTmpIdx = tmpIdxs.put(idx.getName(), (GridH2IndexBase)idx); assert oldTmpIdx == null; } finally { unlock(true); } } /** * Promote temporary index to make it usable in queries. * * @param ses H2 session. * @param idxName Index name. * @return Temporary index with given name. */ private Index commitUserIndex(Session ses, String idxName) { lock(true); try { ensureNotDestroyed(); Index idx = tmpIdxs.remove(idxName); assert idx != null; Index cloneIdx = createDuplicateIndexIfNeeded(idx); ArrayList<Index> newIdxs = new ArrayList<>( idxs.size() + ((cloneIdx == null) ? 1 : 2)); newIdxs.addAll(idxs); newIdxs.add(idx); if (cloneIdx != null) newIdxs.add(cloneIdx); idxs = newIdxs; database.addSchemaObject(ses, idx); if (cloneIdx != null) database.addSchemaObject(ses, cloneIdx); setModified(); return idx; } finally { unlock(true); } } /** * Remove user index without promoting it. * * @param idxName Index name. */ public void rollbackUserIndex(String idxName) { lock(true); try { ensureNotDestroyed(); GridH2IndexBase rmvIdx = tmpIdxs.remove(idxName); assert rmvIdx != null; } finally { unlock(true); } } /** {@inheritDoc} */ @Override public void removeIndex(Index h2Idx) { throw DbException.getUnsupportedException("must use removeIndex(session, idx)"); } /** * Remove the given index from the list. * * @param h2Idx the index to remove */ public void removeIndex(Session session, Index h2Idx) { lock(true); try { ArrayList<Index> idxs = new ArrayList<>(this.idxs); Index targetIdx = (h2Idx instanceof GridH2ProxyIndex)? ((GridH2ProxyIndex)h2Idx).underlyingIndex(): h2Idx; for (int i = 2; i < idxs.size(); ) { Index idx = idxs.get(i); if (idx == targetIdx || (idx instanceof GridH2ProxyIndex && ((GridH2ProxyIndex)idx).underlyingIndex() == targetIdx)) { idxs.remove(i); if (idx instanceof GridH2ProxyIndex && idx.getSchema().findIndex(session, idx.getName()) != null) database.removeSchemaObject(session, idx); continue; } i++; } this.idxs = idxs; } finally { unlock(true); } } /** {@inheritDoc} */ @Override public void removeRow(Session ses, Row row) { throw DbException.getUnsupportedException("removeRow"); } /** {@inheritDoc} */ @Override public void truncate(Session ses) { throw DbException.getUnsupportedException("truncate"); } /** {@inheritDoc} */ @Override public void addRow(Session ses, Row row) { throw DbException.getUnsupportedException("addRow"); } /** {@inheritDoc} */ @Override public void checkSupportAlter() { throw DbException.getUnsupportedException("alter"); } /** {@inheritDoc} */ @Override public TableType getTableType() { return TableType.EXTERNAL_TABLE_ENGINE; } /** {@inheritDoc} */ @Override public Index getScanIndex(Session ses) { return getIndexes().get(0); // Scan must be always first index. } /** {@inheritDoc} */ @Override public Index getUniqueIndex() { if (rebuildFromHashInProgress) return index(1); else return index(2); } /** {@inheritDoc} */ @Override public ArrayList<Index> getIndexes() { if (!rebuildFromHashInProgress) return idxs; ArrayList<Index> idxs = new ArrayList<>(2); idxs.add(this.idxs.get(0)); idxs.add(this.idxs.get(1)); return idxs; } /** * @return All indexes, even marked for rebuild. */ public ArrayList<Index> getAllIndexes() { return idxs; } /** {@inheritDoc} */ @Override public boolean isLockedExclusively() { return false; } /** {@inheritDoc} */ @Override public boolean isLockedExclusivelyBy(Session ses) { return false; } /** {@inheritDoc} */ @Override public long getMaxDataModificationId() { return 0; } /** {@inheritDoc} */ @Override public boolean isDeterministic() { return true; } /** {@inheritDoc} */ @Override public boolean canGetRowCount() { return true; } /** {@inheritDoc} */ @Override public boolean canDrop() { return true; } /** {@inheritDoc} */ @Override public long getRowCount(@Nullable Session ses) { return getUniqueIndex().getRowCount(ses); } /** {@inheritDoc} */ @Override public long getRowCountApproximation() { return size.longValue(); } /** {@inheritDoc} */ @Override public void checkRename() { throw DbException.getUnsupportedException("rename"); } /** * Creates index column for table. * * @param col Column index. * @param sorting Sorting order {@link SortOrder} * @return Created index column. */ public IndexColumn indexColumn(int col, int sorting) { IndexColumn res = new IndexColumn(); res.column = getColumn(col); res.columnName = res.column.getName(); res.sortType = sorting; return res; } /** * @return Data store. */ public H2RowFactory rowFactory() { return rowFactory; } /** * Creates proxy index for given target index. * Proxy index refers to alternative key and val columns. * * @param target Index to clone. * @return Proxy index. */ public Index createDuplicateIndexIfNeeded(Index target) { if (!(target instanceof H2TreeIndex) && !(target instanceof SpatialIndex)) return null; IndexColumn[] cols = target.getIndexColumns(); List<IndexColumn> proxyCols = new ArrayList<>(cols.length); boolean modified = false; for (int i = 0; i < cols.length; i++) { IndexColumn col = cols[i]; IndexColumn proxyCol = new IndexColumn(); proxyCol.columnName = col.columnName; proxyCol.column = col.column; proxyCol.sortType = col.sortType; int altColId = desc.getAlternativeColumnId(proxyCol.column.getColumnId()); if (altColId != proxyCol.column.getColumnId()) { proxyCol.column = getColumn(altColId); proxyCol.columnName = proxyCol.column.getName(); modified = true; } proxyCols.add(proxyCol); } if (modified) { String proxyName = target.getName() + "_proxy"; if (target.getIndexType().isSpatial()) return new GridH2ProxySpatialIndex(this, proxyName, proxyCols, target); return new GridH2ProxyIndex(this, proxyName, proxyCols, target); } return null; } }