All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.jackrabbit.oak.plugins.document.rdb.RDBDocumentStore Maven / Gradle / Ivy

There is a newer version: 1.62.0
Show newest version
/*
 * 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.jackrabbit.oak.plugins.document.rdb;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.partition;
import static org.apache.jackrabbit.oak.plugins.document.UpdateUtils.checkConditions;
import static org.apache.jackrabbit.oak.plugins.document.rdb.RDBJDBCTools.closeResultSet;
import static org.apache.jackrabbit.oak.plugins.document.rdb.RDBJDBCTools.closeStatement;
import static org.apache.jackrabbit.oak.plugins.document.rdb.RDBJDBCTools.createTableName;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.zip.Deflater;
import java.util.zip.GZIPOutputStream;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.sql.DataSource;

import org.apache.jackrabbit.oak.cache.CacheStats;
import org.apache.jackrabbit.oak.cache.CacheValue;
import org.apache.jackrabbit.oak.plugins.document.Collection;
import org.apache.jackrabbit.oak.plugins.document.Document;
import org.apache.jackrabbit.oak.plugins.document.DocumentMK;
import org.apache.jackrabbit.oak.plugins.document.DocumentStore;
import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
import org.apache.jackrabbit.oak.plugins.document.DocumentStoreStatsCollector;
import org.apache.jackrabbit.oak.plugins.document.NodeDocument;
import org.apache.jackrabbit.oak.plugins.document.UpdateOp;
import org.apache.jackrabbit.oak.plugins.document.UpdateOp.Condition;
import org.apache.jackrabbit.oak.plugins.document.UpdateOp.Key;
import org.apache.jackrabbit.oak.plugins.document.UpdateOp.Operation;
import org.apache.jackrabbit.oak.plugins.document.UpdateUtils;
import org.apache.jackrabbit.oak.plugins.document.cache.CacheChangesTracker;
import org.apache.jackrabbit.oak.plugins.document.cache.CacheInvalidationStats;
import org.apache.jackrabbit.oak.plugins.document.cache.NodeDocumentCache;
import org.apache.jackrabbit.oak.plugins.document.locks.NodeDocumentLocks;
import org.apache.jackrabbit.oak.plugins.document.locks.StripedNodeDocumentLocks;
import org.apache.jackrabbit.oak.plugins.document.mongo.MongoDocumentStore;
import org.apache.jackrabbit.oak.plugins.document.util.CloseableIterator;
import org.apache.jackrabbit.oak.util.OakVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

/**
 * Implementation of {@link DocumentStore} for relational databases.
 * 
 * 

Supported Databases

*

* The code is supposed to be sufficiently generic to run with a variety of * database implementations. However, the tables are created when required to * simplify testing, and that code specifically supports these * databases: *

    *
  • H2DB
  • *
  • Apache Derby
  • *
  • IBM DB2
  • *
  • PostgreSQL
  • *
  • MariaDB (MySQL) (experimental)
  • *
  • Microsoft SQL Server (experimental)
  • *
  • Oracle (experimental)
  • *
* *

Table Layout

*

* Data for each of the DocumentStore's {@link Collection}s is stored in its own * database table (with a name matching the collection). *

* The tables essentially implement key/value storage, where the key usually is * derived from an Oak path, and the value is a serialization of a * {@link Document} (or a part of one). Additional fields are used for queries, * debugging, and concurrency control: *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
ColumnTypeDescription
IDvarchar(512) not null primary keythe document's key (for databases that can not handle 512 character * primary keys, such as MySQL, varbinary is possible as wells)
MODIFIEDbigintlow-resolution timestamp *
HASBINARYsmallintflag indicating whether the document has binary properties *
DELETEDONCEsmallintflag indicating whether the document has been deleted once *
MODCOUNTbigintmodification counter, used for avoiding overlapping updates
DSIZEbigintthe approximate size of the document's JSON serialization (for debugging * purposes)
DATAvarchar(16384)the document's JSON serialization (only used for small document sizes, in * which case BDATA (below) is not set), or a sequence of JSON serialized update * operations to be applied against the last full serialization
BDATAblobthe document's JSON serialization (usually GZIPped, only used for "large" * documents)
*

* The names of database tables can be prefixed; the purpose is mainly for * testing, as tables can also be dropped automatically when the store is * disposed (this only happens for those tables that have been created on * demand). *

* Note that the database needs to be created/configured to support all Unicode * characters in text fields, and to collate by Unicode code point (in DB2: "collate using identity", * in Postgres: "C"). * THIS IS NOT THE DEFAULT! *

* For MySQL, the database parameter "max_allowed_packet" needs to be increased to support ~16M blobs. * *

Caching

*

* The cache borrows heavily from the {@link MongoDocumentStore} implementation. * *

Queries

*

* The implementation currently supports only three indexed properties: * "_bin", "deletedOnce", and "_modified". Attempts to use a different indexed property will * cause a {@link DocumentStoreException}. */ public class RDBDocumentStore implements DocumentStore { /** * Creates a {@linkplain RDBDocumentStore} instance using the provided * {@link DataSource}, {@link DocumentMK.Builder}, and {@link RDBOptions}. */ public RDBDocumentStore(DataSource ds, DocumentMK.Builder builder, RDBOptions options) { try { initialize(ds, builder, options); } catch (Exception ex) { throw new DocumentStoreException("initializing RDB document store", ex); } } /** * Creates a {@linkplain RDBDocumentStore} instance using the provided * {@link DataSource}, {@link DocumentMK.Builder}, and default * {@link RDBOptions}. */ public RDBDocumentStore(DataSource ds, DocumentMK.Builder builder) { this(ds, builder, new RDBOptions()); } @Override public T find(Collection collection, String id) { return find(collection, id, Integer.MAX_VALUE); } @Override public T find(final Collection collection, final String id, int maxCacheAge) { return readDocumentCached(collection, id, maxCacheAge); } @Nonnull @Override public List query(Collection collection, String fromKey, String toKey, int limit) { return query(collection, fromKey, toKey, null, 0, limit); } @Nonnull @Override public List query(Collection collection, String fromKey, String toKey, String indexedProperty, long startValue, int limit) { List conditions = Collections.emptyList(); if (indexedProperty != null) { conditions = Collections.singletonList(new QueryCondition(indexedProperty, ">=", startValue)); } return internalQuery(collection, fromKey, toKey, EMPTY_KEY_PATTERN, conditions, limit); } @Nonnull protected List query(Collection collection, String fromKey, String toKey, List excludeKeyPatterns, List conditions, int limit) { return internalQuery(collection, fromKey, toKey, excludeKeyPatterns, conditions, limit); } @Override public void remove(Collection collection, String id) { try { delete(collection, id); } finally { invalidateCache(collection, id, true); } } @Override public void remove(Collection collection, List ids) { try { delete(collection, ids); } finally { for (String id : ids) { invalidateCache(collection, id, true); } } } @Override public int remove(Collection collection, Map> toRemove) { try { return delete(collection, toRemove); } finally { for (String id : toRemove.keySet()) { invalidateCache(collection, id, true); } } } @Override public int remove(Collection collection, String indexedProperty, long startValue, long endValue) throws DocumentStoreException { try { List conditions = new ArrayList(); conditions.add(new QueryCondition(indexedProperty, ">", startValue)); conditions.add(new QueryCondition(indexedProperty, "<", endValue)); return deleteWithCondition(collection, conditions); } finally { if (collection == Collection.NODES) { // this method is currently being used only for Journal // collection while GC. But, to keep sanctity of the API, we // need to acknowledge that Nodes collection could've been used. // But, in this signature, there's no useful way to invalidate // cache. // So, we use the hammer for this task invalidateCache(); } } } @Override public boolean create(Collection collection, List updateOps) { return internalCreate(collection, updateOps); } @Override public void update(Collection collection, List keys, UpdateOp updateOp) { UpdateUtils.assertUnconditional(updateOp); internalUpdate(collection, keys, updateOp); } @Override public T createOrUpdate(Collection collection, UpdateOp update) { UpdateUtils.assertUnconditional(update); return internalCreateOrUpdate(collection, update, true, false); } @Override public List createOrUpdate(Collection collection, List updateOps) { if (!BATCHUPDATES) { List results = new ArrayList(updateOps.size()); for (UpdateOp update : updateOps) { results.add(createOrUpdate(collection, update)); } return results; } final Stopwatch watch = startWatch(); Map results = new LinkedHashMap(); Map operationsToCover = new LinkedHashMap(); Set duplicates = new HashSet(); for (UpdateOp updateOp : updateOps) { UpdateUtils.assertUnconditional(updateOp); if (operationsToCover.containsKey(updateOp.getId())) { duplicates.add(updateOp); results.put(updateOp, null); } else { UpdateOp clone = updateOp.copy(); addUpdateCounters(clone); operationsToCover.put(clone.getId(), clone); results.put(clone, null); } } Map oldDocs = new HashMap(); if (collection == Collection.NODES) { oldDocs.putAll(readDocumentCached(collection, operationsToCover.keySet())); } int i = 0; // iteration count // bulk update requires two DB requests, so if we have <= 2 operations // it's better to send them sequentially while (operationsToCover.size() > 2) { // We should try to insert documents only during the first // iteration. In the 2nd and 3rd iterations we only deal with // conflicting documents, so they already exist in the database // and there's no point in inserting them. boolean upsert = i == 0; if (i++ == 3) { // operations that conflicted in 3 consecutive bulk // updates should be applied sequentially break; } for (List partition : partition(newArrayList(operationsToCover.values()), CHUNKSIZE)) { Map successfulUpdates = bulkUpdate(collection, partition, oldDocs, upsert); results.putAll(successfulUpdates); operationsToCover.values().removeAll(successfulUpdates.keySet()); } } // if there are some changes left, we'll apply them one after another for (UpdateOp updateOp : updateOps) { UpdateOp conflictedOp = operationsToCover.remove(updateOp.getId()); if (conflictedOp != null) { results.put(conflictedOp, createOrUpdate(collection, updateOp)); } else if (duplicates.contains(updateOp)) { results.put(updateOp, createOrUpdate(collection, updateOp)); } } stats.doneCreateOrUpdate(watch.elapsed(TimeUnit.NANOSECONDS), collection, Lists.transform(updateOps, new Function() { @Override public String apply(UpdateOp input) { return input.getId(); } })); return new ArrayList(results.values()); } private Map readDocumentCached(Collection collection, Set keys) { Map documents = new HashMap(); if (collection == Collection.NODES) { for (String key : keys) { NodeDocument cached = nodesCache.getIfPresent(key); if (cached != null && cached != NodeDocument.NULL) { T doc = castAsT(unwrap(cached)); documents.put(doc.getId(), doc); } } } Set documentsToRead = Sets.difference(keys, documents.keySet()); Map readDocuments = readDocumentsUncached(collection, documentsToRead); documents.putAll(readDocuments); if (collection == Collection.NODES) { for (T doc : readDocuments.values()) { nodesCache.putIfAbsent((NodeDocument) doc); } } return documents; } private Map readDocumentsUncached(Collection collection, Set keys) { Map result = new HashMap(); Connection connection = null; RDBTableMetaData tmd = getTable(collection); try { connection = this.ch.getROConnection(); List rows = db.read(connection, tmd, keys); int size = rows.size(); for (int i = 0; i < size; i++) { RDBRow row = rows.set(i, null); T document = convertFromDBObject(collection, row); result.put(document.getId(), document); } connection.commit(); } catch (Exception ex) { throw new DocumentStoreException(ex); } finally { this.ch.closeConnection(connection); } return result; } private Map bulkUpdate(Collection collection, List updates, Map oldDocs, boolean upsert) { Set missingDocs = new HashSet(); for (UpdateOp op : updates) { if (!oldDocs.containsKey(op.getId())) { missingDocs.add(op.getId()); } } for (T doc : readDocumentsUncached(collection, missingDocs).values()) { oldDocs.put(doc.getId(), doc); if (collection == Collection.NODES) { nodesCache.putIfAbsent((NodeDocument) doc); } } List docsToUpdate = new ArrayList(updates.size()); Set keysToUpdate = new HashSet(); for (UpdateOp update : updates) { String id = update.getId(); T modifiedDoc = collection.newDocument(this); if (oldDocs.containsKey(id)) { oldDocs.get(id).deepCopy(modifiedDoc); } UpdateUtils.applyChanges(modifiedDoc, update); docsToUpdate.add(modifiedDoc); keysToUpdate.add(id); } Connection connection = null; RDBTableMetaData tmd = getTable(collection); try { connection = this.ch.getRWConnection(); Set successfulUpdates = db.update(connection, tmd, docsToUpdate, upsert); connection.commit(); Set failedUpdates = Sets.difference(keysToUpdate, successfulUpdates); oldDocs.keySet().removeAll(failedUpdates); if (collection == Collection.NODES) { for (T doc : docsToUpdate) { String id = doc.getId(); if (successfulUpdates.contains(id)) { if (oldDocs.containsKey(id)) { nodesCache.replaceCachedDocument((NodeDocument) oldDocs.get(id), (NodeDocument) doc); } else { nodesCache.putIfAbsent((NodeDocument) doc); } } } } Map result = new HashMap(); for (UpdateOp op : updates) { if (successfulUpdates.contains(op.getId())) { result.put(op, oldDocs.get(op.getId())); } } return result; } catch (SQLException ex) { this.ch.rollbackConnection(connection); throw handleException("update failed for: " + keysToUpdate, ex, collection, keysToUpdate); } finally { this.ch.closeConnection(connection); } } @Override public T findAndUpdate(Collection collection, UpdateOp update) { return internalCreateOrUpdate(collection, update, false, true); } @Override public CacheInvalidationStats invalidateCache() { for (CacheValue key : nodesCache.keys()) { invalidateCache(Collection.NODES, key.toString()); } return null; } @Override public CacheInvalidationStats invalidateCache(Iterable keys) { for (String key : keys) { invalidateCache(Collection.NODES, key); } return null; } @Override public void invalidateCache(Collection collection, String id) { invalidateCache(collection, id, false); } private void invalidateCache(Collection collection, String id, boolean remove) { if (collection == Collection.NODES) { invalidateNodesCache(id, remove); } } private void invalidateNodesCache(String id, boolean remove) { Lock lock = locks.acquire(id); try { if (remove) { nodesCache.invalidate(id); } else { nodesCache.markChanged(id); NodeDocument entry = nodesCache.getIfPresent(id); if (entry != null) { entry.markUpToDate(0); } } } finally { lock.unlock(); } } @Override public long determineServerTimeDifferenceMillis() { Connection connection = null; try { connection = this.ch.getROConnection(); long result = this.db.determineServerTimeDifferenceMillis(connection); connection.commit(); return result; } catch (SQLException ex) { LOG.error("Trying to determine time difference to server", ex); throw new DocumentStoreException(ex); } finally { this.ch.closeConnection(connection); } } // used for diagnostics private String droppedTables = ""; public String getDroppedTables() { return this.droppedTables; } // table names private static Map, String> TABLEMAP; private static List TABLENAMES; static { Map, String> tmp = new HashMap, String>(); tmp.put(Collection.CLUSTER_NODES, "CLUSTERNODES"); tmp.put(Collection.JOURNAL, "JOURNAL"); tmp.put(Collection.NODES, "NODES"); tmp.put(Collection.SETTINGS, "SETTINGS"); TABLEMAP = Collections.unmodifiableMap(tmp); List tl = new ArrayList(TABLEMAP.values()); Collections.sort(tl); TABLENAMES = Collections.unmodifiableList(tl); } public static List getTableNames() { return TABLENAMES; } /** * Holds the data about a table that can vary: name, whether the primary key * is binary, and the estimated size of the "data" column. */ static class RDBTableMetaData { private final String name; private boolean idIsBinary = false; private int dataLimitInOctets = 16384; public RDBTableMetaData(String name) { this.name = name; } public int getDataLimitInOctets() { return this.dataLimitInOctets; } public String getName() { return this.name; } public boolean isIdBinary() { return this.idIsBinary; } public void setIdIsBinary(boolean idIsBinary) { this.idIsBinary = idIsBinary; } public void setDataLimitInOctets(int dataLimitInOctets) { this.dataLimitInOctets = dataLimitInOctets; } } private final Map, RDBTableMetaData> tableMeta = new HashMap, RDBTableMetaData>(); @Override public void dispose() { if (!this.tablesToBeDropped.isEmpty()) { String dropped = ""; LOG.debug("attempting to drop: " + this.tablesToBeDropped); for (String tname : this.tablesToBeDropped) { Connection con = null; try { con = this.ch.getRWConnection(); Statement stmt = null; try { stmt = con.createStatement(); stmt.execute("drop table " + tname); stmt.close(); con.commit(); dropped += tname + " "; } catch (SQLException ex) { LOG.debug("attempting to drop: " + tname, ex); } finally { closeStatement(stmt); } } catch (SQLException ex) { LOG.debug("attempting to drop: " + tname, ex); } finally { this.ch.closeConnection(con); } } this.droppedTables = dropped.trim(); } try { this.ch.close(); } catch (IOException ex) { LOG.error("closing connection handler", ex); } try { this.nodesCache.close(); } catch (IOException ex) { LOG.warn("Error occurred while closing nodes cache", ex); } LOG.info("RDBDocumentStore (" + OakVersion.getVersion() + ") disposed" + getCnStats() + (this.droppedTables.isEmpty() ? "" : " (tables dropped: " + this.droppedTables + ")")); } @Override public T getIfCached(Collection collection, String id) { if (collection != Collection.NODES) { return null; } else { NodeDocument doc = nodesCache.getIfPresent(id); doc = (doc != null) ? unwrap(doc) : null; return castAsT(doc); } } private T getIfCached(Collection collection, String id, long modCount) { T doc = getIfCached(collection, id); if (doc != null && doc.getModCount() == modCount) { return doc; } else { return null; } } @Override public Iterable getCacheStats() { return nodesCache.getCacheStats(); } @Override public Map getMetadata() { return metadata; } // implementation private static final String MODIFIED = "_modified"; private static final String MODCOUNT = "_modCount"; /** * Optional counter for changes to "_collisions" map ({@link NodeDocument#COLLISIONS}). */ public static final String COLLISIONSMODCOUNT = "_collisionsModCount"; private static final String ID = "_id"; private static final Logger LOG = LoggerFactory.getLogger(RDBDocumentStore.class); private Exception callStack; private RDBConnectionHandler ch; // from options private Set tablesToBeDropped = new HashSet(); // ratio between Java characters and UTF-8 encoding // a) single characters will fit into 3 bytes // b) a surrogate pair (two Java characters) will fit into 4 bytes // thus... public static final int CHAR2OCTETRATIO = 3; // number of retries for updates private static final int RETRIES = 10; // see OAK-2044 protected static final boolean USECMODCOUNT = true; private static final Key MODIFIEDKEY = new Key(MODIFIED, null); // DB-specific information private RDBDocumentStoreDB dbInfo; // utility class for performing low-level operations private RDBDocumentStoreJDBC db; protected static final List EMPTY_KEY_PATTERN = Collections.emptyList(); private Map metadata; private DocumentStoreStatsCollector stats; // set of supported indexed properties private static final Set INDEXEDPROPERTIES = new HashSet(Arrays.asList(new String[] { MODIFIED, NodeDocument.HAS_BINARY_FLAG, NodeDocument.DELETED_ONCE })); // set of required table columns private static final Set REQUIREDCOLUMNS = Collections.unmodifiableSet(new HashSet(Arrays.asList( new String[] { "id", "dsize", "deletedonce", "bdata", "data", "cmodcount", "modcount", "hasbinary", "modified" }))); // set of properties not serialized to JSON // when adding new columns also update UNHANDLEDPROPS! private static final Set COLUMNPROPERTIES = new HashSet(Arrays.asList( new String[] { ID, NodeDocument.HAS_BINARY_FLAG, NodeDocument.DELETED_ONCE, COLLISIONSMODCOUNT, MODIFIED, MODCOUNT })); private final RDBDocumentSerializer ser = new RDBDocumentSerializer(this, COLUMNPROPERTIES); private void initialize(DataSource ds, DocumentMK.Builder builder, RDBOptions options) throws Exception { this.stats = builder.getDocumentStoreStatsCollector(); this.tableMeta.put(Collection.NODES, new RDBTableMetaData(createTableName(options.getTablePrefix(), TABLEMAP.get(Collection.NODES)))); this.tableMeta.put(Collection.CLUSTER_NODES, new RDBTableMetaData(createTableName(options.getTablePrefix(), TABLEMAP.get(Collection.CLUSTER_NODES)))); this.tableMeta.put(Collection.JOURNAL, new RDBTableMetaData(createTableName(options.getTablePrefix(), TABLEMAP.get(Collection.JOURNAL)))); this.tableMeta.put(Collection.SETTINGS, new RDBTableMetaData(createTableName(options.getTablePrefix(), TABLEMAP.get(Collection.SETTINGS)))); this.ch = new RDBConnectionHandler(ds); this.callStack = LOG.isDebugEnabled() ? new Exception("call stack of RDBDocumentStore creation") : null; this.locks = new StripedNodeDocumentLocks(); this.nodesCache = builder.buildNodeDocumentCache(this, locks); Connection con = this.ch.getRWConnection(); int isolation = con.getTransactionIsolation(); String isolationDiags = RDBJDBCTools.isolationLevelToString(isolation); if (isolation != Connection.TRANSACTION_READ_COMMITTED) { LOG.info("Detected transaction isolation level " + isolationDiags + " is " + (isolation < Connection.TRANSACTION_READ_COMMITTED ? "lower" : "higher") + " than expected " + RDBJDBCTools.isolationLevelToString(Connection.TRANSACTION_READ_COMMITTED) + " - check datasource configuration"); } DatabaseMetaData md = con.getMetaData(); String dbDesc = String.format("%s %s (%d.%d)", md.getDatabaseProductName(), md.getDatabaseProductVersion(), md.getDatabaseMajorVersion(), md.getDatabaseMinorVersion()).replaceAll("[\r\n\t]", " ").trim(); String driverDesc = String.format("%s %s (%d.%d)", md.getDriverName(), md.getDriverVersion(), md.getDriverMajorVersion(), md.getDriverMinorVersion()).replaceAll("[\r\n\t]", " ").trim(); String dbUrl = md.getURL(); this.dbInfo = RDBDocumentStoreDB.getValue(md.getDatabaseProductName()); this.db = new RDBDocumentStoreJDBC(this.dbInfo, this.ser, QUERYHITSLIMIT, QUERYTIMELIMIT); this.metadata = ImmutableMap.builder() .put("type", "rdb") .put("db", md.getDatabaseProductName()) .put("version", md.getDatabaseProductVersion()) .build(); String versionDiags = dbInfo.checkVersion(md); if (!versionDiags.isEmpty()) { LOG.error(versionDiags); } if (! "".equals(dbInfo.getInitializationStatement())) { Statement stmt = null; try { stmt = con.createStatement(); stmt.execute(dbInfo.getInitializationStatement()); stmt.close(); con.commit(); } finally { closeStatement(stmt); } } List tablesCreated = new ArrayList(); List tablesPresent = new ArrayList(); StringBuilder tableDiags = new StringBuilder(); try { createTableFor(con, Collection.CLUSTER_NODES, this.tableMeta.get(Collection.CLUSTER_NODES), tablesCreated, tablesPresent, tableDiags); createTableFor(con, Collection.NODES, this.tableMeta.get(Collection.NODES), tablesCreated, tablesPresent, tableDiags); createTableFor(con, Collection.SETTINGS, this.tableMeta.get(Collection.SETTINGS), tablesCreated, tablesPresent, tableDiags); createTableFor(con, Collection.JOURNAL, this.tableMeta.get(Collection.JOURNAL), tablesCreated, tablesPresent, tableDiags); } finally { con.commit(); con.close(); } if (options.isDropTablesOnClose()) { tablesToBeDropped.addAll(tablesCreated); } if (tableDiags.length() != 0) { tableDiags.insert(0, ", "); } String diag = dbInfo.getAdditionalDiagnostics(this.ch, this.tableMeta.get(Collection.NODES).getName()); LOG.info("RDBDocumentStore (" + OakVersion.getVersion() + ") instantiated for database " + dbDesc + ", using driver: " + driverDesc + ", connecting to: " + dbUrl + (diag.isEmpty() ? "" : (", properties: " + diag)) + ", transaction isolation level: " + isolationDiags + tableDiags); if (!tablesPresent.isEmpty()) { LOG.info("Tables present upon startup: " + tablesPresent); } if (!tablesCreated.isEmpty()) { LOG.info("Tables created upon startup: " + tablesCreated + (options.isDropTablesOnClose() ? " (will be dropped on exit)" : "")); } } private static boolean isBinaryType(int sqlType) { return sqlType == Types.VARBINARY || sqlType == Types.BINARY || sqlType == Types.LONGVARBINARY; } private void obtainFlagsFromResultSetMeta(ResultSetMetaData met, RDBTableMetaData tmd) throws SQLException { for (int i = 1; i <= met.getColumnCount(); i++) { String lcName = met.getColumnName(i).toLowerCase(Locale.ENGLISH); if ("id".equals(lcName)) { tmd.setIdIsBinary(isBinaryType(met.getColumnType(i))); } if ("data".equals(lcName)) { tmd.setDataLimitInOctets(met.getPrecision(i)); } } } private static String asQualifiedDbName(String one, String two) { if (one == null && two == null) { return null; } else { one = one == null ? "" : one.trim(); two = two == null ? "" : two.trim(); return one.isEmpty() ? two : one + "." + two; } } private static String indexTypeAsString(int type) { switch (type) { case DatabaseMetaData.tableIndexClustered: return "clustered"; case DatabaseMetaData.tableIndexHashed: return "hashed"; case DatabaseMetaData.tableIndexStatistic: return "statistic"; case DatabaseMetaData.tableIndexOther: return "other"; default: return "indexType=" + type; } } private String dumpIndexData(DatabaseMetaData met, ResultSetMetaData rmet, String tableName) { ResultSet rs = null; try { // if the result set metadata provides a table name, use that (the other one // might be inaccurate due to case insensitivity issues String rmetTableName = rmet.getTableName(1); if (rmetTableName != null && !rmetTableName.trim().isEmpty()) { tableName = rmetTableName.trim(); } String rmetSchemaName = rmet.getSchemaName(1); rmetSchemaName = rmetSchemaName == null ? "" : rmetSchemaName.trim(); Map> indices = new TreeMap>(); StringBuilder sb = new StringBuilder(); rs = met.getIndexInfo(null, null, tableName, false, true); getIndexInformation(rs, rmetSchemaName, indices); if (indices.isEmpty() && ! tableName.equals(tableName.toUpperCase(Locale.ENGLISH))) { // might have failed due to the DB's handling on ucase/lcase, retry ucase rs = met.getIndexInfo(null, null, tableName.toUpperCase(Locale.ENGLISH), false, true); getIndexInformation(rs, rmetSchemaName, indices); } for (Entry> index : indices.entrySet()) { boolean nonUnique = ((Boolean) index.getValue().get("nonunique")); Map fields = (Map) index.getValue().get("fields"); if (!fields.isEmpty()) { if (sb.length() != 0) { sb.append(", "); } sb.append(String.format("%sindex %s on %s (", nonUnique ? "" : "unique ", index.getKey(), index.getValue().get("tname"))); String delim = ""; for (String field : fields.values()) { sb.append(delim); delim = ", "; sb.append(field); } sb.append(")"); sb.append(" ").append(index.getValue().get("type")); } } if (sb.length() != 0) { sb.insert(0, "/* ").append(" */"); } return sb.toString(); } catch (SQLException ex) { // well it was best-effort return String.format("/* exception while retrieving index information: %s, code %d, state %s */", ex.getMessage(), ex.getErrorCode(), ex.getSQLState()); } finally { closeResultSet(rs); } } private void getIndexInformation(ResultSet rs, String rmetSchemaName, Map> indices) throws SQLException { while (rs.next()) { String name = asQualifiedDbName(rs.getString(5), rs.getString(6)); if (name != null) { Map info = indices.get(name); if (info == null) { info = new HashMap(); indices.put(name, info); info.put("fields", new TreeMap()); } info.put("nonunique", rs.getBoolean(4)); info.put("type", indexTypeAsString(rs.getInt(7))); String inSchema = rs.getString(2); inSchema = inSchema == null ? "" : inSchema.trim(); // skip indices on tables in other schemas in case we have that information if (rmetSchemaName.isEmpty() || inSchema.isEmpty() || rmetSchemaName.equals(inSchema)) { String tname = asQualifiedDbName(inSchema, rs.getString(3)); info.put("tname", tname); String cname = rs.getString(9); if (cname != null) { String order = "A".equals(rs.getString(10)) ? " ASC" : ("D".equals(rs.getString(10)) ? " DESC" : ""); ((Map) info.get("fields")).put(rs.getInt(8), cname + order); } } } } } private void createTableFor(Connection con, Collection col, RDBTableMetaData tmd, List tablesCreated, List tablesPresent, StringBuilder diagnostics) throws SQLException { String dbname = this.dbInfo.toString(); if (con.getMetaData().getURL() != null) { dbname += " (" + con.getMetaData().getURL() + ")"; } String tableName = tmd.getName(); Statement checkStatement = null; ResultSet checkResultSet = null; Statement creatStatement = null; try { // avoid PreparedStatement due to weird DB2 behavior (OAK-6237) checkStatement = con.createStatement(); checkResultSet = checkStatement.executeQuery("select * from " + tableName + " where ID = '0'"); // try to discover size of DATA column and binary-ness of ID ResultSetMetaData met = checkResultSet.getMetaData(); obtainFlagsFromResultSetMeta(met, tmd); // check that all required columns are present Set requiredColumns = new HashSet(REQUIREDCOLUMNS); Set unknownColumns = new HashSet(); for (int i = 1; i <= met.getColumnCount(); i++) { String cname = met.getColumnName(i).toLowerCase(Locale.ENGLISH); if (!requiredColumns.remove(cname)) { unknownColumns.add(cname); } } if (!requiredColumns.isEmpty()) { String message = String.format("Table %s: the following required columns are missing: %s", tableName, requiredColumns.toString()); LOG.error(message); throw new DocumentStoreException(message); } if (!unknownColumns.isEmpty()) { String message = String.format("Table %s: the following columns are unknown and will not be maintained: %s", tableName, unknownColumns.toString()); LOG.info(message); } if (col == Collection.NODES) { String tableInfo = RDBJDBCTools.dumpResultSetMeta(met); diagnostics.append(tableInfo); String indexInfo = dumpIndexData(con.getMetaData(), met, tableName); if (!indexInfo.isEmpty()) { diagnostics.append(" ").append(indexInfo); } } tablesPresent.add(tableName); } catch (SQLException ex) { // table does not appear to exist con.rollback(); PreparedStatement checkStatement2 = null; ResultSet checkResultSet2 = null; try { creatStatement = con.createStatement(); creatStatement.execute(this.dbInfo.getTableCreationStatement(tableName)); creatStatement.close(); for (String ic : this.dbInfo.getIndexCreationStatements(tableName)) { creatStatement = con.createStatement(); creatStatement.execute(ic); creatStatement.close(); } con.commit(); tablesCreated.add(tableName); checkStatement2 = con.prepareStatement("select * from " + tableName + " where ID = ?"); checkStatement2.setString(1, "0:/"); checkResultSet2 = checkStatement2.executeQuery(); // try to discover size of DATA column and binary-ness of ID ResultSetMetaData met = checkResultSet2.getMetaData(); obtainFlagsFromResultSetMeta(met, tmd); if (col == Collection.NODES) { String tableInfo = RDBJDBCTools.dumpResultSetMeta(met); diagnostics.append(tableInfo); String indexInfo = dumpIndexData(con.getMetaData(), met, tableName); if (!indexInfo.isEmpty()) { diagnostics.append(" ").append(indexInfo); } } } catch (SQLException ex2) { LOG.error("Failed to create table " + tableName + " in " + dbname, ex2); throw ex2; } finally { closeResultSet(checkResultSet2); closeStatement(checkStatement2); } } finally { closeResultSet(checkResultSet); closeStatement(checkStatement); closeStatement(creatStatement); } } @Override protected void finalize() throws Throwable { if (!this.ch.isClosed() && this.callStack != null) { LOG.debug("finalizing RDBDocumentStore that was not disposed", this.callStack); } super.finalize(); } private T readDocumentCached(final Collection collection, final String id, int maxCacheAge) { if (collection != Collection.NODES) { return readDocumentUncached(collection, id, null); } else { NodeDocument doc = null; if (maxCacheAge > 0) { // first try without lock doc = nodesCache.getIfPresent(id); if (doc != null) { long lastCheckTime = doc.getLastCheckTime(); if (lastCheckTime != 0) { if (maxCacheAge == Integer.MAX_VALUE || System.currentTimeMillis() - lastCheckTime < maxCacheAge) { stats.doneFindCached(Collection.NODES, id); return castAsT(unwrap(doc)); } } } } try { Lock lock = locks.acquire(id); try { // caller really wants the cache to be cleared if (maxCacheAge == 0) { invalidateNodesCache(id, true); doc = null; } final NodeDocument cachedDoc = doc; doc = nodesCache.get(id, new Callable() { @Override public NodeDocument call() throws Exception { NodeDocument doc = (NodeDocument) readDocumentUncached(collection, id, cachedDoc); if (doc != null) { doc.seal(); } return wrap(doc); } }); // inspect the doc whether it can be used long lastCheckTime = doc.getLastCheckTime(); if (lastCheckTime != 0 && (maxCacheAge == 0 || maxCacheAge == Integer.MAX_VALUE)) { // we either just cleared the cache or the caller does // not care; } else if (lastCheckTime != 0 && (System.currentTimeMillis() - lastCheckTime < maxCacheAge)) { // is new enough } else { // need to at least revalidate NodeDocument ndoc = (NodeDocument) readDocumentUncached(collection, id, cachedDoc); if (ndoc != null) { ndoc.seal(); } doc = wrap(ndoc); nodesCache.put(doc); } } finally { lock.unlock(); } return castAsT(unwrap(doc)); } catch (ExecutionException e) { throw new IllegalStateException("Failed to load document with " + id, e); } } } @CheckForNull private boolean internalCreate(Collection collection, List updates) { final Stopwatch watch = startWatch(); List ids = new ArrayList(updates.size()); boolean success = true; try { // try up to CHUNKSIZE ops in one transaction for (List chunks : Lists.partition(updates, CHUNKSIZE)) { List docs = new ArrayList(); for (UpdateOp update : chunks) { ids.add(update.getId()); maintainUpdateStats(collection, update.getId()); UpdateUtils.assertUnconditional(update); T doc = collection.newDocument(this); addUpdateCounters(update); UpdateUtils.applyChanges(doc, update); if (!update.getId().equals(doc.getId())) { throw new DocumentStoreException("ID mismatch - UpdateOp: " + update.getId() + ", ID property: " + doc.getId()); } docs.add(doc); } boolean done = insertDocuments(collection, docs); if (done) { if (collection == Collection.NODES) { for (T doc : docs) { nodesCache.putIfAbsent((NodeDocument) doc); } } } else { success = false; } } return success; } catch (DocumentStoreException ex) { return false; } finally { stats.doneCreate(watch.elapsed(TimeUnit.NANOSECONDS), collection, ids, success); } } @CheckForNull private T internalCreateOrUpdate(Collection collection, UpdateOp update, boolean allowCreate, boolean checkConditions) { T oldDoc = readDocumentCached(collection, update.getId(), Integer.MAX_VALUE); if (oldDoc == null) { if (!allowCreate) { return null; } else if (!update.isNew()) { throw new DocumentStoreException("Document does not exist: " + update.getId()); } T doc = collection.newDocument(this); if (checkConditions && !checkConditions(doc, update.getConditions())) { return null; } addUpdateCounters(update); UpdateUtils.applyChanges(doc, update); try { Stopwatch watch = startWatch(); if (!insertDocuments(collection, Collections.singletonList(doc))) { throw new DocumentStoreException("Can't insert the document: " + doc.getId()); } if (collection == Collection.NODES) { nodesCache.putIfAbsent((NodeDocument) doc); } stats.doneFindAndModify(watch.elapsed(TimeUnit.NANOSECONDS), collection, update.getId(), true, true, 0); return oldDoc; } catch (DocumentStoreException ex) { // may have failed due to a race condition; try update instead // this is an edge case, so it's ok to bypass the cache // (avoiding a race condition where the DB is already updated // but the cache is not) oldDoc = readDocumentUncached(collection, update.getId(), null); if (oldDoc == null) { // something else went wrong LOG.error("insert failed, but document " + update.getId() + " is not present, aborting", ex); throw (ex); } return internalUpdate(collection, update, oldDoc, checkConditions, RETRIES); } } else { T result = internalUpdate(collection, update, oldDoc, checkConditions, RETRIES); if (allowCreate && result == null) { // TODO OAK-2655 need to implement some kind of retry LOG.error("update of " + update.getId() + " failed, race condition?"); throw new DocumentStoreException("update of " + update.getId() + " failed, race condition?"); } return result; } } /** * @return previous version of document or null */ @CheckForNull private T internalUpdate(Collection collection, UpdateOp update, T oldDoc, boolean checkConditions, int maxRetries) { if (checkConditions && !UpdateUtils.checkConditions(oldDoc, update.getConditions())) { return null; } else { maintainUpdateStats(collection, update.getId()); addUpdateCounters(update); T doc = createNewDocument(collection, oldDoc, update); Lock l = locks.acquire(update.getId()); final Stopwatch watch = startWatch(); boolean success = false; int retries = maxRetries; try { while (!success && retries > 0) { long lastmodcount = modcountOf(oldDoc); success = updateDocument(collection, doc, update, lastmodcount); if (!success) { retries -= 1; oldDoc = readDocumentCached(collection, update.getId(), Integer.MAX_VALUE); if (oldDoc != null) { long newmodcount = modcountOf(oldDoc); if (lastmodcount == newmodcount) { // cached copy did not change so it probably was // updated by a different instance, get a fresh one oldDoc = readDocumentUncached(collection, update.getId(), null); } } if (oldDoc == null) { // document was there but is now gone LOG.debug("failed to apply update because document is gone in the meantime: " + update.getId(), new Exception("call stack")); return null; } if (checkConditions && !UpdateUtils.checkConditions(oldDoc, update.getConditions())) { return null; } else { addUpdateCounters(update); doc = createNewDocument(collection, oldDoc, update); } } else { if (collection == Collection.NODES) { nodesCache.replaceCachedDocument((NodeDocument) oldDoc, (NodeDocument) doc); } } } if (!success) { throw new DocumentStoreException("failed update of " + doc.getId() + " (race?) after " + maxRetries + " retries"); } return oldDoc; } finally { l.unlock(); int numOfAttempts = maxRetries - retries - 1; stats.doneFindAndModify(watch.elapsed(TimeUnit.NANOSECONDS), collection, update.getId(), false, success, numOfAttempts); } } } @Nonnull private T createNewDocument(Collection collection, T oldDoc, UpdateOp update) { T doc = collection.newDocument(this); oldDoc.deepCopy(doc); UpdateUtils.applyChanges(doc, update); doc.seal(); return doc; } private static void addUpdateCounters(UpdateOp update) { if (hasChangesToCollisions(update)) { update.increment(COLLISIONSMODCOUNT, 1); } update.increment(MODCOUNT, 1); } @CheckForNull private void internalUpdate(Collection collection, List ids, UpdateOp update) { if (isAppendableUpdate(update, true) && !requiresPreviousState(update)) { Operation modOperation = update.getChanges().get(MODIFIEDKEY); long modified = getModifiedFromOperation(modOperation); boolean modifiedIsConditional = modOperation == null || modOperation.type != UpdateOp.Operation.Type.SET; String appendData = ser.asString(update); for (List chunkedIds : Lists.partition(ids, CHUNKSIZE)) { if (collection == Collection.NODES) { for (String key : chunkedIds) { nodesCache.invalidate(key); } } Connection connection = null; RDBTableMetaData tmd = getTable(collection); boolean success = false; try { Stopwatch watch = startWatch(); connection = this.ch.getRWConnection(); success = db.batchedAppendingUpdate(connection, tmd, chunkedIds, modified, modifiedIsConditional, appendData); connection.commit(); //Internally 'db' would make multiple calls and number of those //remote calls would not be captured stats.doneUpdate(watch.elapsed(TimeUnit.NANOSECONDS), collection, chunkedIds.size()); } catch (SQLException ex) { success = false; this.ch.rollbackConnection(connection); } finally { this.ch.closeConnection(connection); } if (success) { if (collection == Collection.NODES) { for (String id : chunkedIds) { nodesCache.invalidate(id); } } } else { for (String id : chunkedIds) { UpdateOp up = update.copy(); up = up.shallowCopy(id); internalCreateOrUpdate(collection, up, false, true); } } } } else { Stopwatch watch = startWatch(); for (String id : ids) { UpdateOp up = update.copy(); up = up.shallowCopy(id); internalCreateOrUpdate(collection, up, false, true); } stats.doneUpdate(watch.elapsed(TimeUnit.NANOSECONDS), collection, ids.size()); } } private List internalQuery(Collection collection, String fromKey, String toKey, List excludeKeyPatterns, List conditions, int limit) { Connection connection = null; RDBTableMetaData tmd = getTable(collection); for (QueryCondition cond : conditions) { if (!INDEXEDPROPERTIES.contains(cond.getPropertyName())) { String message = "indexed property " + cond.getPropertyName() + " not supported, query was '" + cond.getOperator() + "'" + cond.getValue() + "'; supported properties are " + INDEXEDPROPERTIES; LOG.info(message); throw new DocumentStoreException(message); } } final Stopwatch watch = startWatch(); int resultSize = 0; CacheChangesTracker tracker = null; try { if (collection == Collection.NODES) { tracker = nodesCache.registerTracker(fromKey, toKey); } long now = System.currentTimeMillis(); connection = this.ch.getROConnection(); String from = collection == Collection.NODES && NodeDocument.MIN_ID_VALUE.equals(fromKey) ? null : fromKey; String to = collection == Collection.NODES && NodeDocument.MAX_ID_VALUE.equals(toKey) ? null : toKey; List dbresult = db.query(connection, tmd, from, to, excludeKeyPatterns, conditions, limit); connection.commit(); int size = dbresult.size(); List result = new ArrayList(size); for (int i = 0; i < size; i++) { // free RDBRow as early as possible RDBRow row = dbresult.set(i, null); T doc = getIfCached(collection, row.getId(), row.getModcount()); if (doc == null) { // parse DB contents into document if and only if it's not // already in the cache doc = convertFromDBObject(collection, row); } else { // we got a document from the cache, thus collection is NODES // and a tracker is present long lastmodified = modifiedOf(doc); if (lastmodified == row.getModified() && lastmodified >= 1) { Lock lock = locks.acquire(row.getId()); try { if (!tracker.mightBeenAffected(row.getId())) { // otherwise mark it as fresh ((NodeDocument) doc).markUpToDate(now); } } finally { lock.unlock(); } } else { // we need a fresh document instance doc = convertFromDBObject(collection, row); } } result.add(doc); } if (collection == Collection.NODES) { nodesCache.putNonConflictingDocs(tracker, castAsNodeDocumentList(result)); } resultSize = result.size(); return result; } catch (Exception ex) { LOG.error("SQL exception on query", ex); throw new DocumentStoreException(ex); } finally { if (tracker != null) { tracker.close(); } this.ch.closeConnection(connection); stats.doneQuery(watch.elapsed(TimeUnit.NANOSECONDS), collection, fromKey, toKey, !conditions.isEmpty(), resultSize, -1, false); } } private static interface MyCloseableIterable extends Closeable, Iterable { } protected Iterable queryAsIterable(final Collection collection, String fromKey, String toKey, final List excludeKeyPatterns, final List conditions, final int limit, final String sortBy) { final RDBTableMetaData tmd = getTable(collection); for (QueryCondition cond : conditions) { if (!INDEXEDPROPERTIES.contains(cond.getPropertyName())) { String message = "indexed property " + cond.getPropertyName() + " not supported, query was '" + cond.getOperator() + "'" + cond.getValue() + "'; supported properties are " + INDEXEDPROPERTIES; LOG.info(message); throw new DocumentStoreException(message); } } final String from = collection == Collection.NODES && NodeDocument.MIN_ID_VALUE.equals(fromKey) ? null : fromKey; final String to = collection == Collection.NODES && NodeDocument.MAX_ID_VALUE.equals(toKey) ? null : toKey; return new MyCloseableIterable() { Set> returned = Sets.newHashSet(); @Override public Iterator iterator() { try { Iterator res = db.queryAsIterator(ch, tmd, from, to, excludeKeyPatterns, conditions, limit, sortBy); returned.add(res); Iterator tmp = Iterators.transform(res, new Function() { @Override public T apply(RDBRow input) { return convertFromDBObject(collection, input); } }); return CloseableIterator.wrap(tmp, (Closeable) res); } catch (SQLException ex) { throw new RuntimeException(ex); } } @Override public void close() throws IOException { for (Iterator rdbi : returned) { if (rdbi instanceof Closeable) { ((Closeable) rdbi).close(); } } } }; } @Nonnull protected RDBTableMetaData getTable(Collection collection) { RDBTableMetaData tmd = this.tableMeta.get(collection); if (tmd != null) { return tmd; } else { throw new IllegalArgumentException("Unknown collection: " + collection.toString()); } } @CheckForNull private T readDocumentUncached(Collection collection, String id, NodeDocument cachedDoc) { Connection connection = null; RDBTableMetaData tmd = getTable(collection); final Stopwatch watch = startWatch(); boolean docFound = true; try { long lastmodcount = -1, lastmodified = -1; if (cachedDoc != null) { lastmodcount = modcountOf(cachedDoc); lastmodified = modifiedOf(cachedDoc); } connection = this.ch.getROConnection(); RDBRow row = db.read(connection, tmd, id, lastmodcount, lastmodified); connection.commit(); if (row == null) { docFound = false; return null; } else { if (lastmodcount == row.getModcount() && lastmodified == row.getModified() && lastmodified >= 1) { // we can re-use the cached document cachedDoc.markUpToDate(System.currentTimeMillis()); return castAsT(cachedDoc); } else { return convertFromDBObject(collection, row); } } } catch (Exception ex) { throw new DocumentStoreException(ex); } finally { this.ch.closeConnection(connection); stats.doneFindUncached(watch.elapsed(TimeUnit.NANOSECONDS), collection, id, docFound, false); } } private void delete(Collection collection, String id) { Connection connection = null; RDBTableMetaData tmd = getTable(collection); try { connection = this.ch.getRWConnection(); db.delete(connection, tmd, Collections.singletonList(id)); connection.commit(); } catch (Exception ex) { throw handleException("removing " + id, ex, collection, id); } finally { this.ch.closeConnection(connection); } } private int delete(Collection collection, List ids) { int numDeleted = 0; RDBTableMetaData tmd = getTable(collection); for (List sublist : Lists.partition(ids, 64)) { Connection connection = null; try { connection = this.ch.getRWConnection(); numDeleted += db.delete(connection, tmd, sublist); connection.commit(); } catch (Exception ex) { throw handleException("removing " + ids, ex, collection, ids); } finally { this.ch.closeConnection(connection); } } return numDeleted; } private int delete(Collection collection, Map> toRemove) { int numDeleted = 0; RDBTableMetaData tmd = getTable(collection); Map> subMap = Maps.newHashMap(); Iterator>> it = toRemove.entrySet().iterator(); while (it.hasNext()) { Entry> entry = it.next(); subMap.put(entry.getKey(), entry.getValue()); if (subMap.size() == 64 || !it.hasNext()) { Connection connection = null; try { connection = this.ch.getRWConnection(); numDeleted += db.delete(connection, tmd, subMap); connection.commit(); } catch (Exception ex) { Set ids = subMap.keySet(); throw handleException("deleting " + ids, ex, collection, ids); } finally { this.ch.closeConnection(connection); } subMap.clear(); } } return numDeleted; } private int deleteWithCondition(Collection collection, List conditions) { int numDeleted = 0; RDBTableMetaData tmd = getTable(collection); Connection connection = null; try { connection = this.ch.getRWConnection(); numDeleted = db.deleteWithCondition(connection, tmd, conditions); connection.commit(); } catch (Exception ex) { throw DocumentStoreException.convert(ex, "deleting " + collection + ": " + conditions); } finally { this.ch.closeConnection(connection); } return numDeleted; } private boolean updateDocument(@Nonnull Collection collection, @Nonnull T document, @Nonnull UpdateOp update, Long oldmodcount) { Connection connection = null; RDBTableMetaData tmd = getTable(collection); String data = null; try { connection = this.ch.getRWConnection(); Number hasBinary = (Number) document.get(NodeDocument.HAS_BINARY_FLAG); Boolean deletedOnce = (Boolean) document.get(NodeDocument.DELETED_ONCE); Long modcount = (Long) document.get(MODCOUNT); Long cmodcount = (Long) document.get(COLLISIONSMODCOUNT); boolean success = false; boolean shouldRetry = true; // every 16th update is a full rewrite if (isAppendableUpdate(update, false) && modcount % 16 != 0) { String appendData = ser.asString(update); if (appendData.length() < tmd.getDataLimitInOctets() / CHAR2OCTETRATIO) { try { Operation modOperation = update.getChanges().get(MODIFIEDKEY); long modified = getModifiedFromOperation(modOperation); boolean modifiedIsConditional = modOperation == null || modOperation.type != UpdateOp.Operation.Type.SET; success = db.appendingUpdate(connection, tmd, document.getId(), modified, modifiedIsConditional, hasBinary, deletedOnce, modcount, cmodcount, oldmodcount, appendData); // if we get here, a retry is not going to help (the SQL // operation succeeded but simply did not select a row // that could be updated, likely because of the check on // MODCOUNT shouldRetry = false; connection.commit(); } catch (SQLException ex) { continueIfStringOverflow(ex); this.ch.rollbackConnection(connection); success = false; } } } if (!success && shouldRetry) { data = ser.asString(document); Object m = document.get(MODIFIED); long modified = (m instanceof Long) ? ((Long)m).longValue() : 0; success = db.update(connection, tmd, document.getId(), modified, hasBinary, deletedOnce, modcount, cmodcount, oldmodcount, data); connection.commit(); } return success; } catch (SQLException ex) { this.ch.rollbackConnection(connection); String addDiags = ""; if (RDBJDBCTools.matchesSQLState(ex, "22", "72")) { byte[] bytes = asBytes(data); addDiags = String.format(" (DATA size in Java characters: %d, in octets: %d, computed character limit: %d)", data.length(), bytes.length, tmd.getDataLimitInOctets() / CHAR2OCTETRATIO); } String message = String.format("Update for %s failed%s", document.getId(), addDiags); LOG.debug(message, ex); throw handleException(message, ex, collection, document.getId()); } finally { this.ch.closeConnection(connection); } } private static void continueIfStringOverflow(SQLException ex) throws SQLException { String state = ex.getSQLState(); if ("22001".equals(state) /* everybody */|| ("72000".equals(state) && 1489 == ex.getErrorCode()) /* Oracle */) { // ok } else { throw (ex); } } // set of properties not serialized and not handled specifically by update code private static final Set UNHANDLEDPROPS = new HashSet( Arrays.asList(new Key[] { new Key(NodeDocument.HAS_BINARY_FLAG, null), new Key(NodeDocument.DELETED_ONCE, null) })); private static boolean isAppendableUpdate(UpdateOp update, boolean batched) { if (NOAPPEND) { return false; } if (batched) { // Detect update operations not supported when doing batch updates for (Key key : update.getChanges().keySet()) { if (UNHANDLEDPROPS.contains(key)) { return false; } } } return true; } /* * check whether this update operation requires knowledge about the previous * state */ private static boolean requiresPreviousState(UpdateOp update) { return !update.getConditions().isEmpty(); } private static long getModifiedFromOperation(Operation op) { return op == null ? 0L : Long.parseLong(op.value.toString()); } private boolean insertDocuments(Collection collection, List documents) { Connection connection = null; RDBTableMetaData tmd = getTable(collection); try { connection = this.ch.getRWConnection(); Set insertedKeys = db.insert(connection, tmd, documents); connection.commit(); return insertedKeys.size() == documents.size(); } catch (SQLException ex) { this.ch.rollbackConnection(connection); List ids = new ArrayList(); for (T doc : documents) { ids.add(doc.getId()); } String message = String.format("insert of %s failed", ids); LOG.debug(message, ex); // collect additional exceptions String messages = LOG.isDebugEnabled() ? RDBJDBCTools.getAdditionalMessages(ex) : ""; // see whether a DATA error was involved boolean dataRelated = false; SQLException walk = ex; while (walk != null && !dataRelated) { dataRelated = RDBJDBCTools.matchesSQLState(walk, "22", "72"); walk = walk.getNextException(); } if (dataRelated) { String id = null; int longest = 0, longestChars = 0; for (Document d : documents) { String data = ser.asString(d); byte bytes[] = asBytes(data); if (bytes.length > longest) { longest = bytes.length; longestChars = data.length(); id = d.getId(); } } String m = String .format(" (potential cause: long data for ID %s - longest octet DATA size in Java characters: %d, in octets: %d, computed character limit: %d)", id, longest, longestChars, tmd.getDataLimitInOctets() / CHAR2OCTETRATIO); messages += m; } if (!messages.isEmpty()) { LOG.debug("additional diagnostics: " + messages); } throw handleException(message, ex, collection, ids); } finally { this.ch.closeConnection(connection); } } // configuration // Whether to use GZIP compression private static final boolean NOGZIP = Boolean .getBoolean("org.apache.jackrabbit.oak.plugins.document.rdb.RDBDocumentStore.NOGZIP"); // Whether to use append operations (string concatenation) in the DATA column private static final boolean NOAPPEND = Boolean .getBoolean("org.apache.jackrabbit.oak.plugins.document.rdb.RDBDocumentStore.NOAPPEND"); // Number of documents to insert at once for batch create private static final int CHUNKSIZE = Integer.getInteger( "org.apache.jackrabbit.oak.plugins.document.rdb.RDBDocumentStore.CHUNKSIZE", 64); // Number of query hits above which a diagnostic warning is generated private static final int QUERYHITSLIMIT = Integer.getInteger( "org.apache.jackrabbit.oak.plugins.document.rdb.RDBDocumentStore.QUERYHITSLIMIT", 4096); // Number of elapsed ms in a query above which a diagnostic warning is generated private static final int QUERYTIMELIMIT = Integer.getInteger( "org.apache.jackrabbit.oak.plugins.document.rdb.RDBDocumentStore.QUERYTIMELIMIT", 10000); // Whether to use JDBC batch commands for the createOrUpdate (default: true). private static final boolean BATCHUPDATES = Boolean.parseBoolean(System .getProperty("org.apache.jackrabbit.oak.plugins.document.rdb.RDBDocumentStore.BATCHUPDATES", "true")); public static byte[] asBytes(String data) { byte[] bytes; try { bytes = data.getBytes("UTF-8"); } catch (UnsupportedEncodingException ex) { LOG.error("UTF-8 not supported??", ex); throw new DocumentStoreException(ex); } if (NOGZIP) { return bytes; } else { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length()); GZIPOutputStream gos = new GZIPOutputStream(bos) { { // TODO: make this configurable this.def.setLevel(Deflater.BEST_SPEED); } }; gos.write(bytes); gos.close(); return bos.toByteArray(); } catch (IOException ex) { LOG.error("Error while gzipping contents", ex); throw new DocumentStoreException(ex); } } } @Override public void setReadWriteMode(String readWriteMode) { // ignored } public void setStatsCollector(DocumentStoreStatsCollector stats) { this.stats = stats; } @SuppressWarnings("unchecked") private static T castAsT(NodeDocument doc) { return (T) doc; } @SuppressWarnings("unchecked") private static List castAsNodeDocumentList(List list) { return (List) list; } private NodeDocumentCache nodesCache; private NodeDocumentLocks locks; @CheckForNull private static NodeDocument unwrap(@Nonnull NodeDocument doc) { return doc == NodeDocument.NULL ? null : doc; } @Nonnull private static NodeDocument wrap(@CheckForNull NodeDocument doc) { return doc == null ? NodeDocument.NULL : doc; } @Nonnull private static String idOf(@Nonnull Document doc) { String id = doc.getId(); if (id == null) { throw new IllegalArgumentException("non-null ID expected"); } return id; } private static long modcountOf(@Nonnull Document doc) { Long n = doc.getModCount(); return n != null ? n : -1; } private static long modifiedOf(@Nonnull Document doc) { Object l = doc.get(NodeDocument.MODIFIED_IN_SECS); return (l instanceof Long) ? ((Long)l).longValue() : -1; } @Nonnull protected T convertFromDBObject(@Nonnull Collection collection, @Nonnull RDBRow row) { // this method is present here in order to facilitate unit testing for OAK-3566 return ser.fromRow(collection, row); } private static boolean hasChangesToCollisions(UpdateOp update) { if (!USECMODCOUNT) { return false; } else { for (Entry e : checkNotNull(update).getChanges().entrySet()) { Key k = e.getKey(); Operation op = e.getValue(); if (op.type == Operation.Type.SET_MAP_ENTRY) { if (NodeDocument.COLLISIONS.equals(k.getName())) { return true; } } } return false; } } // keeping track of CLUSTER_NODES updates private Map cnUpdates = new ConcurrentHashMap(); private void maintainUpdateStats(Collection collection, String key) { if (collection == Collection.CLUSTER_NODES) { synchronized (this) { Long old = cnUpdates.get(key); old = old == null ? Long.valueOf(1) : old + 1; cnUpdates.put(key, old); } } } private String getCnStats() { if (cnUpdates.isEmpty()) { return ""; } else { List> tmp = new ArrayList>(); tmp.addAll(cnUpdates.entrySet()); Collections.sort(tmp, new Comparator>() { @Override public int compare(Entry o1, Entry o2) { return o1.getKey().compareTo(o2.getKey()); }}); return " (Cluster Node updates: " + tmp.toString() + ")"; } } private Stopwatch startWatch() { return Stopwatch.createStarted(); } protected NodeDocumentCache getNodeDocumentCache() { return nodesCache; } private DocumentStoreException handleException(String message, Exception ex, Collection collection, java.util.Collection ids) { if (collection == Collection.NODES) { for (String id : ids) { invalidateCache(collection, id, false); } } return DocumentStoreException.convert(ex, message); } private DocumentStoreException handleException(String message, Exception ex, Collection collection, String id) { return handleException(message, ex, collection, Collections.singleton(id)); } // slightly extended query support protected static class QueryCondition { private final String propertyName, operator; private final long value; public QueryCondition(String propertyName, String operator, long value) { this.propertyName = propertyName; this.operator = operator; this.value = value; } public String getPropertyName() { return propertyName; } public String getOperator() { return operator; } public long getValue() { return value; } @Override public String toString() { return String.format("%s %s %d", propertyName, operator, value); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy