/* * 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.jdbc.thin; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketTimeoutException; import java.sql.Array; import java.sql.BatchUpdateException; import java.sql.Blob; import java.sql.CallableStatement; import java.sql.Clob; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.NClob; import java.sql.PreparedStatement; import java.sql.SQLClientInfoException; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLPermission; import java.sql.SQLTimeoutException; import java.sql.SQLWarning; import java.sql.SQLXML; import java.sql.Savepoint; import java.sql.Statement; import java.sql.Struct; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Random; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteException; import org.apache.ignite.binary.BinaryObjectException; import org.apache.ignite.binary.BinaryType; import org.apache.ignite.cache.query.QueryCancelledException; import org.apache.ignite.client.ClientException; import org.apache.ignite.configuration.BinaryConfiguration; import org.apache.ignite.configuration.IgniteConfiguration; import org.apache.ignite.internal.MarshallerPlatformIds; import org.apache.ignite.internal.binary.BinaryCachingMetadataHandler; import org.apache.ignite.internal.binary.BinaryContext; import org.apache.ignite.internal.binary.BinaryMarshaller; import org.apache.ignite.internal.binary.BinaryMetadata; import org.apache.ignite.internal.binary.BinaryMetadataHandler; import org.apache.ignite.internal.binary.BinaryTypeImpl; import org.apache.ignite.internal.jdbc2.JdbcUtils; import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion; import org.apache.ignite.internal.processors.cache.GridCacheUtils; import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode; import org.apache.ignite.internal.processors.odbc.ClientListenerResponse; import org.apache.ignite.internal.processors.odbc.SqlStateCode; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcBinaryTypeGetRequest; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcBinaryTypeGetResult; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcBinaryTypeNameGetRequest; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcBinaryTypeNameGetResult; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcBinaryTypeNamePutRequest; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcBinaryTypePutRequest; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcBulkLoadBatchRequest; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcCachePartitionsRequest; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcCachePartitionsResult; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcOrderedBatchExecuteRequest; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcOrderedBatchExecuteResult; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcQuery; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcQueryCancelRequest; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcQueryExecuteRequest; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcQueryExecuteResult; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcRequest; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcResponse; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcResult; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcResultWithIo; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcStatementType; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcUpdateBinarySchemaResult; import org.apache.ignite.internal.sql.command.SqlCommand; import org.apache.ignite.internal.sql.command.SqlSetStreamingCommand; import org.apache.ignite.internal.sql.optimizer.affinity.PartitionClientContext; import org.apache.ignite.internal.sql.optimizer.affinity.PartitionResult; import org.apache.ignite.internal.util.HostAndPortRange; import org.apache.ignite.internal.util.future.GridFutureAdapter; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.internal.U; import org.apache.ignite.lang.IgnitePredicate; import org.apache.ignite.lang.IgniteProductVersion; import org.apache.ignite.logger.NullLogger; import org.apache.ignite.marshaller.MarshallerContext; import org.apache.ignite.marshaller.jdk.JdkMarshaller; import org.jetbrains.annotations.Nullable; import static java.sql.ResultSet.CLOSE_CURSORS_AT_COMMIT; import static java.sql.ResultSet.CONCUR_READ_ONLY; import static java.sql.ResultSet.HOLD_CURSORS_OVER_COMMIT; import static java.sql.ResultSet.TYPE_FORWARD_ONLY; import static org.apache.ignite.internal.processors.odbc.SqlStateCode.CLIENT_CONNECTION_FAILED; import static org.apache.ignite.internal.processors.odbc.SqlStateCode.CONNECTION_CLOSED; import static org.apache.ignite.internal.processors.odbc.SqlStateCode.CONNECTION_FAILURE; import static org.apache.ignite.internal.processors.odbc.SqlStateCode.INTERNAL_ERROR; import static org.apache.ignite.marshaller.MarshallerUtils.processSystemClasses; /** * JDBC connection implementation. * * See documentation of {@link org.apache.ignite.IgniteJdbcThinDriver} for details. */ public class JdbcThinConnection implements Connection { /** Logger. */ private static final Logger LOG = Logger.getLogger(JdbcThinConnection.class.getName()); /** Request timeout period. */ private static final int REQUEST_TIMEOUT_PERIOD = 1_000; /** Reconnection period. */ public static final int RECONNECTION_DELAY = 200; /** Reconnection maximum period. */ private static final int RECONNECTION_MAX_DELAY = 300_000; /** Network timeout permission */ private static final String SET_NETWORK_TIMEOUT_PERM = "setNetworkTimeout"; /** Zero timeout as query timeout means no timeout. */ static final int NO_TIMEOUT = 0; /** Index generator. */ private static final AtomicLong IDX_GEN = new AtomicLong(); /** Default retires count. */ public static final int DFLT_RETRIES_CNT = 4; /** No retries. */ public static final int NO_RETRIES = 0; /** Partition awareness enabled flag. */ private final boolean partitionAwareness; /** Statements modification mutex. */ private final Object stmtsMux = new Object(); /** Schema name. */ private String schema; /** Closed flag. */ private volatile boolean closed; /** Current transaction isolation. */ private int txIsolation; /** Auto-commit flag. */ private boolean autoCommit; /** Read-only flag. */ private boolean readOnly; /** Streaming flag. */ private volatile StreamState streamState; /** Current transaction holdability. */ private int holdability; /** Jdbc metadata. Cache the JDBC object on the first access */ private JdbcThinDatabaseMetadata metadata; /** Connection properties. */ private final ConnectionProperties connProps; /** The amount of potentially alive {@code JdbcThinTcpIo} instances - connections to server nodes. */ private final AtomicInteger connCnt = new AtomicInteger(); /** Tracked statements to close on disconnect. */ private final Set<JdbcThinStatement> stmts = Collections.newSetFromMap(new IdentityHashMap<>()); /** Affinity cache. */ private AffinityCache affinityCache; /** Ignite endpoint. */ private volatile JdbcThinTcpIo singleIo; /** Node Ids tp ignite endpoints. */ private final ConcurrentSkipListMap<UUID, JdbcThinTcpIo> ios = new ConcurrentSkipListMap<>(); /** Server index. */ private int srvIdx; /** Ignite server version. */ private Thread ownThread; /** Mutex. */ private final Object mux = new Object(); /** Ignite endpoint to use within transactional context. */ private volatile JdbcThinTcpIo txIo; /** Random generator. */ private static final Random RND = new Random(System.currentTimeMillis()); /** Network timeout. */ private int netTimeout; /** Query timeout. */ private int qryTimeout; /** Background periodical maintenance: query timeouts and reconnection handler. */ private final ScheduledExecutorService maintenanceExecutor = Executors.newScheduledThreadPool(2); /** Cancelable future for query timeout task. */ private ScheduledFuture<?> qryTimeoutScheduledFut; /** Cancelable future for connections handler task. */ private ScheduledFuture<?> connectionsHndScheduledFut; /** Connections handler timer. */ private final IgniteProductVersion baseEndpointVer; /** Binary context. */ private volatile BinaryContext ctx; /** Binary metadata handler. */ private volatile JdbcBinaryMetadataHandler metaHnd; /** Marshaller context. */ private final JdbcMarshallerContext marshCtx; /** * Creates new connection. * * @param connProps Connection properties. * @throws SQLException In case Ignite client failed to start. */ public JdbcThinConnection(ConnectionProperties connProps) throws SQLException { this.connProps = connProps; metaHnd = new JdbcBinaryMetadataHandler(); marshCtx = new JdbcMarshallerContext(); ctx = createBinaryCtx(metaHnd, marshCtx); holdability = HOLD_CURSORS_OVER_COMMIT; autoCommit = true; txIsolation = Connection.TRANSACTION_NONE; netTimeout = connProps.getConnectionTimeout(); qryTimeout = connProps.getQueryTimeout(); schema = JdbcUtils.normalizeSchema(connProps.getSchema()); partitionAwareness = connProps.isPartitionAwareness(); if (partitionAwareness) { baseEndpointVer = connectInBestEffortAffinityMode(null); connectionsHndScheduledFut = maintenanceExecutor.scheduleWithFixedDelay(new ConnectionHandlerTask(), 0, RECONNECTION_DELAY, TimeUnit.MILLISECONDS); } else { connectInCommonMode(); baseEndpointVer = null; } } /** Create new binary context. */ private BinaryContext createBinaryCtx(JdbcBinaryMetadataHandler metaHnd, JdbcMarshallerContext marshCtx) { BinaryMarshaller marsh = new BinaryMarshaller(); marsh.setContext(marshCtx); BinaryConfiguration binCfg = new BinaryConfiguration().setCompactFooter(true); BinaryContext ctx = new BinaryContext(metaHnd, new IgniteConfiguration(), new NullLogger()); ctx.configure(marsh, binCfg); ctx.registerUserTypesSchema(); return ctx; } /** * @throws SQLException On connection error. */ private void ensureConnected() throws SQLException { if (connCnt.get() > 0) return; assert !closed; assert ios.isEmpty(); if (partitionAwareness) connectInBestEffortAffinityMode(baseEndpointVer); else connectInCommonMode(); } /** * @return Whether this connection is streamed or not. */ boolean isStream() { return streamState != null; } /** * @param sql Statement. * @param cmd Parsed form of {@code sql}. * @param stmt Jdbc thin statement. * @throws SQLException if failed. */ void executeNative(String sql, SqlCommand cmd, JdbcThinStatement stmt) throws SQLException { if (cmd instanceof SqlSetStreamingCommand) { SqlSetStreamingCommand cmd0 = (SqlSetStreamingCommand)cmd; // If streaming is already on, we have to close it first. if (streamState != null) { streamState.close(); streamState = null; } boolean newVal = ((SqlSetStreamingCommand)cmd).isTurnOn(); ensureConnected(); JdbcThinTcpIo cliIo = cliIo(null); // Actual ON, if needed. if (newVal) { if (!cmd0.isOrdered() && !cliIo.isUnorderedStreamSupported()) { throw new SQLException("Streaming without order doesn't supported by server [remoteNodeVer=" + cliIo.igniteVersion() + ']', INTERNAL_ERROR); } streamState = new StreamState((SqlSetStreamingCommand)cmd, cliIo); sendRequest(new JdbcQueryExecuteRequest(JdbcStatementType.ANY_STATEMENT_TYPE, schema, 1, 1, autoCommit, sql, null), stmt, cliIo); streamState.start(); } } else throw IgniteQueryErrorCode.createJdbcSqlException("Unsupported native statement: " + sql, IgniteQueryErrorCode.UNSUPPORTED_OPERATION); } /** * Add another query for batched execution. * * @param sql Query. * @param args Arguments. * @throws SQLException On error. */ void addBatch(String sql, List<Object> args) throws SQLException { assert isStream(); streamState.addBatch(sql, args); } /** {@inheritDoc} */ @Override public Statement createStatement() throws SQLException { return createStatement(TYPE_FORWARD_ONLY, CONCUR_READ_ONLY, HOLD_CURSORS_OVER_COMMIT); } /** {@inheritDoc} */ @Override public Statement createStatement(int resSetType, int resSetConcurrency) throws SQLException { return createStatement(resSetType, resSetConcurrency, HOLD_CURSORS_OVER_COMMIT); } /** {@inheritDoc} */ @Override public Statement createStatement(int resSetType, int resSetConcurrency, int resSetHoldability) throws SQLException { ensureNotClosed(); checkCursorOptions(resSetType, resSetConcurrency); JdbcThinStatement stmt = new JdbcThinStatement(this, resSetHoldability, schema); stmt.setQueryTimeout(qryTimeout); synchronized (stmtsMux) { stmts.add(stmt); } return stmt; } /** {@inheritDoc} */ @Override public PreparedStatement prepareStatement(String sql) throws SQLException { return prepareStatement(sql, TYPE_FORWARD_ONLY, CONCUR_READ_ONLY, HOLD_CURSORS_OVER_COMMIT); } /** {@inheritDoc} */ @Override public PreparedStatement prepareStatement(String sql, int resSetType, int resSetConcurrency) throws SQLException { return prepareStatement(sql, resSetType, resSetConcurrency, HOLD_CURSORS_OVER_COMMIT); } /** {@inheritDoc} */ @Override public PreparedStatement prepareStatement(String sql, int resSetType, int resSetConcurrency, int resSetHoldability) throws SQLException { ensureNotClosed(); checkCursorOptions(resSetType, resSetConcurrency); if (sql == null) throw new SQLException("SQL string cannot be null."); JdbcThinPreparedStatement stmt = new JdbcThinPreparedStatement(this, sql, resSetHoldability, schema); synchronized (stmtsMux) { stmts.add(stmt); } return stmt; } /** * @param resSetType Cursor option. * @param resSetConcurrency Cursor option. * @throws SQLException If options unsupported. */ private void checkCursorOptions(int resSetType, int resSetConcurrency) throws SQLException { if (resSetType != TYPE_FORWARD_ONLY) throw new SQLFeatureNotSupportedException("Invalid result set type (only forward is supported)."); if (resSetConcurrency != CONCUR_READ_ONLY) throw new SQLFeatureNotSupportedException("Invalid concurrency (updates are not supported)."); } /** {@inheritDoc} */ @Override public CallableStatement prepareCall(String sql) throws SQLException { ensureNotClosed(); throw new SQLFeatureNotSupportedException("Callable functions are not supported."); } /** {@inheritDoc} */ @Override public CallableStatement prepareCall(String sql, int resSetType, int resSetConcurrency) throws SQLException { ensureNotClosed(); throw new SQLFeatureNotSupportedException("Callable functions are not supported."); } /** {@inheritDoc} */ @Override public String nativeSQL(String sql) throws SQLException { ensureNotClosed(); if (sql == null) throw new SQLException("SQL string cannot be null."); return sql; } /** {@inheritDoc} */ @Override public void setAutoCommit(boolean autoCommit) throws SQLException { ensureNotClosed(); // Do nothing if resulting value doesn't actually change. if (autoCommit != this.autoCommit) { doCommit(); this.autoCommit = autoCommit; } } /** {@inheritDoc} */ @Override public boolean getAutoCommit() throws SQLException { ensureNotClosed(); return autoCommit; } /** {@inheritDoc} */ @Override public void commit() throws SQLException { ensureNotClosed(); if (autoCommit) throw new SQLException("Transaction cannot be committed explicitly in auto-commit mode."); doCommit(); } /** {@inheritDoc} */ @Override public void rollback() throws SQLException { ensureNotClosed(); if (autoCommit) throw new SQLException("Transaction cannot be rolled back explicitly in auto-commit mode."); try (Statement s = createStatement()) { s.execute("ROLLBACK"); } } /** * Send to the server {@code COMMIT} command. * * @throws SQLException if failed. */ private void doCommit() throws SQLException { try (Statement s = createStatement()) { s.execute("COMMIT"); } } /** {@inheritDoc} */ @Override public void close() throws SQLException { if (isClosed()) return; closed = true; maintenanceExecutor.shutdown(); if (streamState != null) { streamState.close(); streamState = null; } synchronized (stmtsMux) { stmts.clear(); } SQLException err = null; if (partitionAwareness) { for (JdbcThinTcpIo clioIo : ios.values()) clioIo.close(); ios.clear(); } else { if (singleIo != null) singleIo.close(); } if (err != null) throw err; } /** {@inheritDoc} */ @Override public boolean isClosed() { return closed; } /** {@inheritDoc} */ @Override public DatabaseMetaData getMetaData() throws SQLException { ensureNotClosed(); if (metadata == null) metadata = new JdbcThinDatabaseMetadata(this); return metadata; } /** {@inheritDoc} */ @Override public void setReadOnly(boolean readOnly) throws SQLException { ensureNotClosed(); this.readOnly = readOnly; } /** {@inheritDoc} */ @Override public boolean isReadOnly() throws SQLException { ensureNotClosed(); return readOnly; } /** {@inheritDoc} */ @Override public void setCatalog(String catalog) throws SQLException { ensureNotClosed(); } /** {@inheritDoc} */ @Override public String getCatalog() throws SQLException { ensureNotClosed(); return null; } /** {@inheritDoc} */ @Override public void setTransactionIsolation(int level) throws SQLException { ensureNotClosed(); switch (level) { case Connection.TRANSACTION_READ_UNCOMMITTED: case Connection.TRANSACTION_READ_COMMITTED: case Connection.TRANSACTION_REPEATABLE_READ: case Connection.TRANSACTION_SERIALIZABLE: case Connection.TRANSACTION_NONE: break; default: throw new SQLException("Invalid transaction isolation level.", SqlStateCode.INVALID_TRANSACTION_LEVEL); } txIsolation = level; } /** {@inheritDoc} */ @Override public int getTransactionIsolation() throws SQLException { ensureNotClosed(); return txIsolation; } /** {@inheritDoc} */ @Override public SQLWarning getWarnings() throws SQLException { ensureNotClosed(); return null; } /** {@inheritDoc} */ @Override public void clearWarnings() throws SQLException { ensureNotClosed(); } /** {@inheritDoc} */ @Override public Map<String, Class<?>> getTypeMap() throws SQLException { ensureNotClosed(); throw new SQLFeatureNotSupportedException("Types mapping is not supported."); } /** {@inheritDoc} */ @Override public void setTypeMap(Map<String, Class<?>> map) throws SQLException { ensureNotClosed(); throw new SQLFeatureNotSupportedException("Types mapping is not supported."); } /** {@inheritDoc} */ @Override public void setHoldability(int holdability) throws SQLException { ensureNotClosed(); if (holdability != HOLD_CURSORS_OVER_COMMIT && holdability != CLOSE_CURSORS_AT_COMMIT) throw new SQLException("Invalid result set holdability value."); this.holdability = holdability; } /** {@inheritDoc} */ @Override public int getHoldability() throws SQLException { ensureNotClosed(); return holdability; } /** {@inheritDoc} */ @Override public Savepoint setSavepoint() throws SQLException { ensureNotClosed(); if (autoCommit) throw new SQLException("Savepoint cannot be set in auto-commit mode."); throw new SQLFeatureNotSupportedException("Savepoints are not supported."); } /** {@inheritDoc} */ @Override public Savepoint setSavepoint(String name) throws SQLException { ensureNotClosed(); if (name == null) throw new SQLException("Savepoint name cannot be null."); if (autoCommit) throw new SQLException("Savepoint cannot be set in auto-commit mode."); throw new SQLFeatureNotSupportedException("Savepoints are not supported."); } /** {@inheritDoc} */ @Override public void rollback(Savepoint savepoint) throws SQLException { ensureNotClosed(); if (savepoint == null) throw new SQLException("Invalid savepoint."); if (autoCommit) throw new SQLException("Auto-commit mode."); throw new SQLFeatureNotSupportedException("Savepoints are not supported."); } /** {@inheritDoc} */ @Override public void releaseSavepoint(Savepoint savepoint) throws SQLException { ensureNotClosed(); if (savepoint == null) throw new SQLException("Savepoint cannot be null."); throw new SQLFeatureNotSupportedException("Savepoints are not supported."); } /** {@inheritDoc} */ @Override public CallableStatement prepareCall(String sql, int resSetType, int resSetConcurrency, int resSetHoldability) throws SQLException { ensureNotClosed(); throw new SQLFeatureNotSupportedException("Callable functions are not supported."); } /** {@inheritDoc} */ @Override public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { ensureNotClosed(); throw new SQLFeatureNotSupportedException("Auto generated keys are not supported."); } /** {@inheritDoc} */ @Override public PreparedStatement prepareStatement(String sql, int[] colIndexes) throws SQLException { ensureNotClosed(); throw new SQLFeatureNotSupportedException("Auto generated keys are not supported."); } /** {@inheritDoc} */ @Override public PreparedStatement prepareStatement(String sql, String[] colNames) throws SQLException { ensureNotClosed(); throw new SQLFeatureNotSupportedException("Auto generated keys are not supported."); } /** {@inheritDoc} */ @Override public Clob createClob() throws SQLException { ensureNotClosed(); throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); } /** {@inheritDoc} */ @Override public Blob createBlob() throws SQLException { ensureNotClosed(); throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); } /** {@inheritDoc} */ @Override public NClob createNClob() throws SQLException { ensureNotClosed(); throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); } /** {@inheritDoc} */ @Override public SQLXML createSQLXML() throws SQLException { ensureNotClosed(); throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); } /** {@inheritDoc} */ @Override public boolean isValid(int timeout) throws SQLException { if (timeout < 0) throw new SQLException("Invalid timeout: " + timeout); return !closed; } /** {@inheritDoc} */ @Override public void setClientInfo(String name, String val) throws SQLClientInfoException { if (closed) throw new SQLClientInfoException("Connection is closed.", null); } /** {@inheritDoc} */ @Override public void setClientInfo(Properties props) throws SQLClientInfoException { if (closed) throw new SQLClientInfoException("Connection is closed.", null); } /** {@inheritDoc} */ @Override public String getClientInfo(String name) throws SQLException { ensureNotClosed(); return null; } /** {@inheritDoc} */ @Override public Properties getClientInfo() throws SQLException { ensureNotClosed(); return new Properties(); } /** {@inheritDoc} */ @Override public Array createArrayOf(String typeName, Object[] elements) throws SQLException { ensureNotClosed(); if (typeName == null) throw new SQLException("Type name cannot be null."); throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); } /** {@inheritDoc} */ @Override public Struct createStruct(String typeName, Object[] attrs) throws SQLException { ensureNotClosed(); if (typeName == null) throw new SQLException("Type name cannot be null."); throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); } /** {@inheritDoc} */ @Override public <T> T unwrap(Class<T> iface) throws SQLException { if (!isWrapperFor(iface)) throw new SQLException("Connection is not a wrapper for " + iface.getName()); return (T)this; } /** {@inheritDoc} */ @Override public boolean isWrapperFor(Class<?> iface) throws SQLException { return iface != null && iface.isAssignableFrom(JdbcThinConnection.class); } /** {@inheritDoc} */ @Override public void setSchema(String schema) throws SQLException { ensureNotClosed(); this.schema = JdbcUtils.normalizeSchema(schema); } /** {@inheritDoc} */ @Override public String getSchema() throws SQLException { ensureNotClosed(); return schema; } /** {@inheritDoc} */ @Override public void abort(Executor executor) throws SQLException { if (executor == null) throw new SQLException("Executor cannot be null."); close(); } /** {@inheritDoc} */ @Override public void setNetworkTimeout(Executor executor, int ms) throws SQLException { ensureNotClosed(); if (ms < 0) throw new SQLException("Network timeout cannot be negative."); SecurityManager secMgr = System.getSecurityManager(); if (secMgr != null) secMgr.checkPermission(new SQLPermission(SET_NETWORK_TIMEOUT_PERM)); netTimeout = ms; if (partitionAwareness) { for (JdbcThinTcpIo clioIo : ios.values()) clioIo.timeout(ms); } else singleIo.timeout(ms); } /** {@inheritDoc} */ @Override public int getNetworkTimeout() throws SQLException { ensureNotClosed(); return netTimeout; } /** * Ensures that connection is not closed. * * @throws SQLException If connection is closed. */ public void ensureNotClosed() throws SQLException { if (closed) throw new SQLException("Connection is closed.", CONNECTION_CLOSED); } /** * @return Ignite server version. */ IgniteProductVersion igniteVersion() { if (partitionAwareness) { return ios.values().stream().map(JdbcThinTcpIo::igniteVersion).min(IgniteProductVersion::compareTo). orElse(baseEndpointVer); } else return singleIo.igniteVersion(); } /** * @return Auto close server cursors flag. */ boolean autoCloseServerCursor() { return connProps.isAutoCloseServerCursor(); } /** * Send request for execution via corresponding singleIo from {@link #ios}. * * @param req Request. * @return Server response. * @throws SQLException On any error. */ JdbcResultWithIo sendRequest(JdbcRequest req) throws SQLException { return sendRequest(req, null, null); } /** * Send request for execution via corresponding singleIo from {@link #ios} or sticky singleIo. * * @param req Request. * @param stmt Jdbc thin statement. * @param stickyIo Sticky ignite endpoint. * @return Server response. * @throws SQLException On any error. */ JdbcResultWithIo sendRequest(JdbcRequest req, JdbcThinStatement stmt, @Nullable JdbcThinTcpIo stickyIo) throws SQLException { RequestTimeoutTask reqTimeoutTask = null; acquireMutex(); try { int retryAttemptsLeft = 1; Exception lastE = null; while (retryAttemptsLeft > 0) { JdbcThinTcpIo cliIo = null; ensureConnected(); try { cliIo = (stickyIo == null || !stickyIo.connected()) ? cliIo(calculateNodeIds(req)) : stickyIo; if (stmt != null && stmt.requestTimeout() != NO_TIMEOUT) { reqTimeoutTask = new RequestTimeoutTask( req instanceof JdbcBulkLoadBatchRequest ? stmt.currentRequestId() : req.requestId(), cliIo, stmt.requestTimeout()); qryTimeoutScheduledFut = maintenanceExecutor.scheduleAtFixedRate(reqTimeoutTask, 0, REQUEST_TIMEOUT_PERIOD, TimeUnit.MILLISECONDS); } JdbcQueryExecuteRequest qryReq = null; if (req instanceof JdbcQueryExecuteRequest) qryReq = (JdbcQueryExecuteRequest)req; JdbcResponse res = cliIo.sendRequest(req, stmt); txIo = res.activeTransaction() ? cliIo : null; if (res.status() == IgniteQueryErrorCode.QUERY_CANCELED && stmt != null && stmt.requestTimeout() != NO_TIMEOUT && reqTimeoutTask != null && reqTimeoutTask.expired.get()) { throw new SQLTimeoutException(QueryCancelledException.ERR_MSG, SqlStateCode.QUERY_CANCELLED, IgniteQueryErrorCode.QUERY_CANCELED); } else if (res.status() != ClientListenerResponse.STATUS_SUCCESS) throw new SQLException(res.error(), IgniteQueryErrorCode.codeToSqlState(res.status()), res.status()); updateAffinityCache(qryReq, res); return new JdbcResultWithIo(res.response(), cliIo); } catch (SQLException e) { if (LOG.isLoggable(Level.FINE)) LOG.log(Level.FINE, "Exception during sending an sql request.", e); throw e; } catch (Exception e) { if (LOG.isLoggable(Level.FINE)) LOG.log(Level.FINE, "Exception during sending an sql request.", e); // We reuse the same connection when deals with binary objects to synchronize the binary schema, // so if any error occurred during synchronization, we close the underlying IO when handling problem // for the first time and should skip it during next processing if (cliIo != null && cliIo.connected()) onDisconnect(cliIo); if (e instanceof SocketTimeoutException) throw new SQLException("Connection timed out.", CONNECTION_FAILURE, e); else { if (lastE == null) { retryAttemptsLeft = calculateRetryAttemptsCount(stickyIo, req); lastE = e; } else retryAttemptsLeft--; } } } throw new SQLException("Failed to communicate with Ignite cluster.", CONNECTION_FAILURE, lastE); } finally { if (stmt != null && stmt.requestTimeout() != NO_TIMEOUT && reqTimeoutTask != null) qryTimeoutScheduledFut.cancel(false); releaseMutex(); } } /** * Calculate node UUIDs. * * @param req Jdbc request for which we'll try to calculate node id. * @return node UUID or null if failed to calculate. * @throws IOException If Exception occurred during the network partition distribution retrieval. * @throws SQLException If Failed to calculate derived partitions. */ @Nullable private List<UUID> calculateNodeIds(JdbcRequest req) throws IOException, SQLException { if (!partitionAwareness || !(req instanceof JdbcQueryExecuteRequest)) return null; JdbcQueryExecuteRequest qry = (JdbcQueryExecuteRequest)req; if (affinityCache == null) { qry.partitionResponseRequest(true); return null; } JdbcThinPartitionResultDescriptor partResDesc = affinityCache.partitionResult( new QualifiedSQLQuery(qry.schemaName(), qry.sqlQuery())); // Value is empty. if (partResDesc == JdbcThinPartitionResultDescriptor.EMPTY_DESCRIPTOR) return null; // Key is missing. if (partResDesc == null) { qry.partitionResponseRequest(true); return null; } Collection<Integer> parts = calculatePartitions(partResDesc, qry.arguments()); if (parts == null || parts.isEmpty()) return null; UUID[] cacheDistr = retrieveCacheDistribution(partResDesc.cacheId(), partResDesc.partitionResult().partitionsCount()); if (parts.size() == 1) return Collections.singletonList(cacheDistr[parts.iterator().next()]); else { List<UUID> partitionAwarenessNodeIds = new ArrayList<>(); for (int part : parts) partitionAwarenessNodeIds.add(cacheDistr[part]); return partitionAwarenessNodeIds; } } /** * Retrieve cache distribution for specified cache Id. * * @param cacheId Cache Id. * @param partCnt Partitions count. * @return Partitions cache distribution. * @throws IOException If Exception occurred during the network partition distribution retrieval. */ private UUID[] retrieveCacheDistribution(int cacheId, int partCnt) throws IOException { UUID[] cacheDistr = affinityCache.cacheDistribution(cacheId); if (cacheDistr != null) return cacheDistr; JdbcResponse res; res = cliIo(null).sendRequest(new JdbcCachePartitionsRequest(Collections.singleton(cacheId)), null); assert res.status() == ClientListenerResponse.STATUS_SUCCESS; AffinityTopologyVersion resAffinityVer = res.affinityVersion(); if (affinityCache.version().compareTo(resAffinityVer) < 0) { affinityCache = new AffinityCache( resAffinityVer, connProps.getPartitionAwarenessPartitionDistributionsCacheSize(), connProps.getPartitionAwarenessSqlCacheSize()); } else if (affinityCache.version().compareTo(resAffinityVer) > 0) { // Jdbc thin affinity cache is binded to the newer affinity topology version, so we should ignore retrieved // partition distribution. Given situation might occur in case of concurrent race and is not // possible in single-threaded jdbc thin client, so it's a reserve for the future. return null; } List<JdbcThinPartitionAwarenessMappingGroup> mappings = ((JdbcCachePartitionsResult)res.response()).getMappings(); // Despite the fact that, at this moment, we request partition distribution only for one cache, // we might retrieve multiple caches but exactly with same distribution. assert mappings.size() == 1; JdbcThinPartitionAwarenessMappingGroup mappingGrp = mappings.get(0); cacheDistr = mappingGrp.revertMappings(partCnt); for (int mpCacheId : mappingGrp.cacheIds()) affinityCache.addCacheDistribution(mpCacheId, cacheDistr); return cacheDistr; } /** * Calculate partitions for the query. * * @param partResDesc Partition result descriptor. * @param args Arguments. * @return Calculated partitions or {@code null} if failed to calculate and there should be a broadcast. * @throws SQLException If Failed to calculate derived partitions. */ public static Collection<Integer> calculatePartitions(JdbcThinPartitionResultDescriptor partResDesc, Object[] args) throws SQLException { PartitionResult derivedParts = partResDesc.partitionResult(); if (derivedParts != null) { try { return derivedParts.tree().apply(partResDesc.partitionClientContext(), args); } catch (IgniteCheckedException e) { throw new SQLException("Failed to calculate derived partitions for query.", INTERNAL_ERROR); } } return null; } /** * Send request for execution via corresponding singleIo from {@link #ios} or sticky singleIo. * Response is waited at the separate thread (see {@link StreamState#asyncRespReaderThread}). * * @param req Request. * @throws SQLException On any error. */ void sendQueryCancelRequest(JdbcQueryCancelRequest req, JdbcThinTcpIo cliIo) throws SQLException { if (connCnt.get() == 0) throw new SQLException("Failed to communicate with Ignite cluster.", CONNECTION_FAILURE); assert cliIo != null; try { cliIo.sendCancelRequest(req); } catch (Exception e) { throw new SQLException("Failed to communicate with Ignite cluster.", CONNECTION_FAILURE, e); } } /** * Send request for execution via corresponding singleIo from {@link #ios} or sticky singleIo. * Response is waited at the separate thread (see {@link StreamState#asyncRespReaderThread}). * * @param req Request. * @param stickyIO Sticky ignite endpoint. * @throws SQLException On any error. */ private void sendRequestNotWaitResponse(JdbcRequest req, JdbcThinTcpIo stickyIO) throws SQLException { ensureConnected(); acquireMutex(); try { stickyIO.sendRequestNoWaitResponse(req); } catch (SQLException e) { throw e; } catch (Exception e) { onDisconnect(stickyIO); if (e instanceof SocketTimeoutException) throw new SQLException("Connection timed out.", CONNECTION_FAILURE, e); else throw new SQLException("Failed to communicate with Ignite cluster.", CONNECTION_FAILURE, e); } finally { releaseMutex(); } } /** * Acquire mutex. Allows subsequent acquire by the same thread. * <p> * How to use: * <pre> * acquireMutex(); * * try { * // do some work here * } * finally { * releaseMutex(); * } * * </pre> * * @throws SQLException If mutex already acquired by another thread. * @see JdbcThinConnection#releaseMutex() */ private void acquireMutex() throws SQLException { synchronized (mux) { Thread curr = Thread.currentThread(); if (ownThread != null && ownThread != curr) { throw new SQLException("Concurrent access to JDBC connection is not allowed" + " [ownThread=" + ownThread.getName() + ", curThread=" + curr.getName(), CONNECTION_FAILURE); } ownThread = curr; } } /** * Release mutex. Does nothing if nobody own the mutex. * <p> * How to use: * <pre> * acquireMutex(); * * try { * // do some work here * } * finally { * releaseMutex(); * } * * </pre> * * @throws IllegalStateException If mutex is owned by another thread. * @see JdbcThinConnection#acquireMutex() */ private void releaseMutex() { synchronized (mux) { Thread curr = Thread.currentThread(); if (ownThread != null && ownThread != curr) throw new IllegalStateException("Mutex is owned by another thread"); ownThread = null; } } /** * @return Connection URL. */ public String url() { return connProps.getUrl(); } /** * Called on IO disconnect: close the client IO and opened statements. */ private void onDisconnect(JdbcThinTcpIo cliIo) { assert connCnt.get() > 0; if (partitionAwareness) { cliIo.close(); ios.remove(cliIo.nodeId()); } else { if (singleIo != null) singleIo.close(); } connCnt.decrementAndGet(); if (streamState != null) { streamState.close0(); streamState = null; } synchronized (stmtsMux) { for (JdbcThinStatement s : stmts) s.closeOnDisconnect(); stmts.clear(); } // Clear local metadata cache on disconnect. metaHnd = new JdbcBinaryMetadataHandler(); ctx = createBinaryCtx(metaHnd, marshCtx); } /** * @param stmt Statement to close. */ void closeStatement(JdbcThinStatement stmt) { synchronized (stmtsMux) { stmts.remove(stmt); } } /** * Streamer state and */ private class StreamState { /** Maximum requests count that may be sent before any responses. */ private static final int MAX_REQUESTS_BEFORE_RESPONSE = 10; /** Batch size for streaming. */ private int streamBatchSize; /** Batch for streaming. */ private List<JdbcQuery> streamBatch; /** Last added query to recognize batches. */ private String lastStreamQry; /** Keep request order on execution. */ private long order; /** Async response reader thread. */ private Thread asyncRespReaderThread; /** Async response error. */ private volatile Exception err; /** The order of the last batch request at the stream. */ private long lastRespOrder = -1; /** Last response future. */ private final GridFutureAdapter<Void> lastRespFut = new GridFutureAdapter<>(); /** Response semaphore sem. */ private Semaphore respSem = new Semaphore(MAX_REQUESTS_BEFORE_RESPONSE); /** Streaming sticky ignite endpoint. */ private final JdbcThinTcpIo streamingStickyIo; /** * @param cmd Stream cmd. * @param stickyIo Sticky ignite endpoint. */ StreamState(SqlSetStreamingCommand cmd, JdbcThinTcpIo stickyIo) { streamBatchSize = cmd.batchSize(); asyncRespReaderThread = new Thread(this::readResponses); streamingStickyIo = stickyIo; } /** * Start reader. */ void start() { asyncRespReaderThread.start(); } /** * Add another query for batched execution. * * @param sql Query. * @param args Arguments. * @throws SQLException On error. */ void addBatch(String sql, List<Object> args) throws SQLException { checkError(); boolean newQry = (args == null || !F.eq(lastStreamQry, sql)); // Providing null as SQL here allows for recognizing subbatches on server and handling them more efficiently. JdbcQuery q = new JdbcQuery(newQry ? sql : null, args != null ? args.toArray() : null); if (streamBatch == null) streamBatch = new ArrayList<>(streamBatchSize); streamBatch.add(q); // Null args means "addBatch(String)" was called on non-prepared Statement, // we don't want to remember its query string. lastStreamQry = (args != null ? sql : null); if (streamBatch.size() == streamBatchSize) executeBatch(false); } /** * @param lastBatch Whether open data streamers must be flushed and closed after this batch. * @throws SQLException if failed. */ private void executeBatch(boolean lastBatch) throws SQLException { checkError(); if (lastBatch) lastRespOrder = order; try { respSem.acquire(); sendRequestNotWaitResponse( new JdbcOrderedBatchExecuteRequest(schema, streamBatch, autoCommit, lastBatch, order), streamingStickyIo); streamBatch = null; lastStreamQry = null; if (lastBatch) { try { lastRespFut.get(); } catch (IgniteCheckedException ignored) { // No-op. // No exceptions are expected here. } checkError(); } else order++; } catch (InterruptedException e) { throw new SQLException("Streaming operation was interrupted", INTERNAL_ERROR, e); } } /** * Throws at the user thread exception that was thrown at the {@link #asyncRespReaderThread} thread. * * @throws SQLException Saved exception. */ void checkError() throws SQLException { if (err != null) { Exception err0 = err; err = null; if (err0 instanceof SQLException) throw (SQLException)err0; else { onDisconnect(streamingStickyIo); if (err0 instanceof SocketTimeoutException) throw new SQLException("Connection timed out.", CONNECTION_FAILURE, err0); throw new SQLException("Failed to communicate with Ignite cluster on JDBC streaming.", CONNECTION_FAILURE, err0); } } } /** * @throws SQLException On error. */ void close() throws SQLException { close0(); checkError(); } /** */ void close0() { if (connCnt.get() > 0) { try { executeBatch(true); } catch (SQLException e) { err = e; LOG.log(Level.WARNING, "Exception during batch send on streamed connection close", e); } } if (asyncRespReaderThread != null) asyncRespReaderThread.interrupt(); } /** */ void readResponses() { try { while (true) { JdbcResponse resp = streamingStickyIo.readResponse(); if (resp.response() instanceof JdbcOrderedBatchExecuteResult) { JdbcOrderedBatchExecuteResult res = (JdbcOrderedBatchExecuteResult)resp.response(); respSem.release(); if (res.errorCode() != ClientListenerResponse.STATUS_SUCCESS) { err = new BatchUpdateException(res.errorMessage(), IgniteQueryErrorCode.codeToSqlState(res.errorCode()), res.errorCode(), res.updateCounts()); } // Receive the response for the last request. if (res.order() == lastRespOrder) { lastRespFut.onDone(); break; } } else if (resp.response() instanceof JdbcBinaryTypeGetResult) metaHnd.handleResult((JdbcBinaryTypeGetResult)resp.response()); else if (resp.response() instanceof JdbcBinaryTypeNameGetResult) marshCtx.handleResult((JdbcBinaryTypeNameGetResult)resp.response()); else if (resp.response() instanceof JdbcUpdateBinarySchemaResult) { JdbcUpdateBinarySchemaResult binarySchemaRes = (JdbcUpdateBinarySchemaResult)resp.response(); if (!marshCtx.handleResult(binarySchemaRes) && !metaHnd.handleResult(binarySchemaRes)) LOG.log(Level.WARNING, "Neither marshaller context nor metadata handler" + " wait for update binary schema result (req=" + binarySchemaRes + ")"); } else if (resp.status() != ClientListenerResponse.STATUS_SUCCESS) err = new SQLException(resp.error(), IgniteQueryErrorCode.codeToSqlState(resp.status())); else assert false : "Invalid response: " + resp; } } catch (Exception e) { err = e; } } } /** * @return True if query cancellation supported, false otherwise. */ boolean isQueryCancellationSupported() { return partitionAwareness || singleIo.isQueryCancellationSupported(); } /** * Whether custom objects are supported or not. * * @return True if custom objects are supported, false otherwise. */ boolean isCustomObjectSupported() { return singleIo.isCustomObjectSupported(); } /** * @param nodeIds Set of node's UUIDs. * @return Ignite endpoint to use for request/response transferring. */ private JdbcThinTcpIo cliIo(List<UUID> nodeIds) { if (!partitionAwareness) return singleIo; if (txIo != null) return txIo; if (nodeIds == null || nodeIds.isEmpty()) return randomIo(); JdbcThinTcpIo io = null; if (nodeIds.size() == 1) io = ios.get(nodeIds.get(0)); else { int initNodeId = RND.nextInt(nodeIds.size()); int iterCnt = 0; while (io == null) { io = ios.get(nodeIds.get(initNodeId)); initNodeId = initNodeId == nodeIds.size() ? 0 : initNodeId + 1; iterCnt++; if (iterCnt == nodeIds.size()) break; } } return io != null ? io : randomIo(); } /** * Returns random tcpIo, based on random UUID, generated in a custom way * with the help of {@code Random} instead of {@code SecureRandom}. It's * valid, cause cryptographically strong pseudo random number generator is * not required in this particular case. {@code Random} is much faster * than {@code SecureRandom}. * * @return random tcpIo */ private JdbcThinTcpIo randomIo() { byte[] randomBytes = new byte[16]; RND.nextBytes(randomBytes); randomBytes[6] &= 0x0f; /* clear version */ randomBytes[6] |= 0x40; /* set to version 4 */ randomBytes[8] &= 0x3f; /* clear variant */ randomBytes[8] |= 0x80; /* set to IETF variant */ long msb = 0; long lsb = 0; for (int i = 0; i < 8; i++) msb = (msb << 8) | (randomBytes[i] & 0xff); for (int i = 8; i < 16; i++) lsb = (lsb << 8) | (randomBytes[i] & 0xff); UUID randomUUID = new UUID(msb, lsb); Map.Entry<UUID, JdbcThinTcpIo> entry = ios.ceilingEntry(randomUUID); return entry != null ? entry.getValue() : ios.floorEntry(randomUUID).getValue(); } /** * @return Current server index. */ public int serverIndex() { return srvIdx; } /** * Get next server index. * * @param len Number of servers. * @return Index of the next server to connect to. */ private static int nextServerIndex(int len) { if (len == 1) return 0; else { long nextIdx = IDX_GEN.getAndIncrement(); return (int)(nextIdx % len); } } /** * Establishes a connection to ignite endpoint, trying all specified hosts and ports one by one. * Stops as soon as any connection is established. * * @throws SQLException If failed to connect to ignite cluster. */ private void connectInCommonMode() throws SQLException { HostAndPortRange[] srvs = connProps.getAddresses(); List<Exception> exceptions = null; for (int i = 0; i < srvs.length; i++) { srvIdx = nextServerIndex(srvs.length); HostAndPortRange srv = srvs[srvIdx]; try { InetAddress[] addrs = InetAddress.getAllByName(srv.host()); for (InetAddress addr : addrs) { for (int port = srv.portFrom(); port <= srv.portTo(); ++port) { try { JdbcThinTcpIo cliIo = new JdbcThinTcpIo(connProps, new InetSocketAddress(addr, port), ctx, 0); cliIo.timeout(netTimeout); singleIo = cliIo; connCnt.incrementAndGet(); return; } catch (Exception exception) { if (exceptions == null) exceptions = new ArrayList<>(); exceptions.add(exception); } } } } catch (Exception exception) { if (exceptions == null) exceptions = new ArrayList<>(); exceptions.add(exception); } } handleConnectExceptions(exceptions); } /** * Prepare and throw general {@code SQLException} with all specified exceptions as suppressed items. * * @param exceptions Exceptions list. * @throws SQLException Umbrella exception. */ private void handleConnectExceptions(List<Exception> exceptions) throws SQLException { if (connCnt.get() == 0 && exceptions != null) { close(); if (exceptions.size() == 1) { Exception ex = exceptions.get(0); if (ex instanceof SQLException) throw (SQLException)ex; else if (ex instanceof IOException) throw new SQLException("Failed to connect to Ignite cluster [url=" + connProps.getUrl() + ']', CLIENT_CONNECTION_FAILED, ex); } SQLException e = new SQLException("Failed to connect to server [url=" + connProps.getUrl() + ']', CLIENT_CONNECTION_FAILED); for (Exception ex : exceptions) e.addSuppressed(ex); throw e; } } /** * Establishes a connection to ignite endpoint, trying all specified hosts * and ports one by one. * * Stops as soon as all iosArr are established. * * @param baseEndpointVer Base endpoint version. * @return last connected endpoint version. * @throws SQLException If failed to connect to at least one ignite * endpoint, or if endpoints versions are less than base endpoint version. */ private IgniteProductVersion connectInBestEffortAffinityMode( IgniteProductVersion baseEndpointVer) throws SQLException { List<Exception> exceptions = null; for (int i = 0; i < connProps.getAddresses().length; i++) { HostAndPortRange srv = connProps.getAddresses()[i]; try { InetAddress[] addrs = InetAddress.getAllByName(srv.host()); for (InetAddress addr : addrs) { for (int port = srv.portFrom(); port <= srv.portTo(); ++port) { try { JdbcThinTcpIo cliIo = new JdbcThinTcpIo(connProps, new InetSocketAddress(addr, port), ctx, 0); if (!cliIo.isPartitionAwarenessSupported()) { cliIo.close(); throw new SQLException("Failed to connect to Ignite node [url=" + connProps.getUrl() + "]. address = [" + addr + ':' + port + "]." + "Node doesn't support partition awareness mode.", INTERNAL_ERROR); } IgniteProductVersion endpointVer = cliIo.igniteVersion(); if (baseEndpointVer != null && baseEndpointVer.compareTo(endpointVer) > 0) { cliIo.close(); throw new SQLException("Failed to connect to Ignite node [url=" + connProps.getUrl() + "], address = [" + addr + ':' + port + "]," + "the node version [" + endpointVer + "] " + "is smaller than the base one [" + baseEndpointVer + "].", INTERNAL_ERROR); } cliIo.timeout(netTimeout); JdbcThinTcpIo ioToSameNode = ios.putIfAbsent(cliIo.nodeId(), cliIo); // This can happen if the same node has several IPs or if connection manager background // timer task runs concurrently. if (ioToSameNode != null) cliIo.close(); else connCnt.incrementAndGet(); return cliIo.igniteVersion(); } catch (Exception exception) { if (exceptions == null) exceptions = new ArrayList<>(); exceptions.add(exception); } } } } catch (Exception exception) { if (exceptions == null) exceptions = new ArrayList<>(); exceptions.add(exception); } } handleConnectExceptions(exceptions); return null; } /** * Recreates affinity cache if affinity topology version was changed and adds partition result to sql cache. * * @param qryReq Query request. * @param res Jdbc Response. */ private void updateAffinityCache(JdbcQueryExecuteRequest qryReq, JdbcResponse res) { if (partitionAwareness) { AffinityTopologyVersion resAffVer = res.affinityVersion(); if (resAffVer != null && (affinityCache == null || affinityCache.version().compareTo(resAffVer) < 0)) { affinityCache = new AffinityCache( resAffVer, connProps.getPartitionAwarenessPartitionDistributionsCacheSize(), connProps.getPartitionAwarenessSqlCacheSize()); } // Partition result was requested. if (res.response() instanceof JdbcQueryExecuteResult && qryReq.partitionResponseRequest()) { PartitionResult partRes = ((JdbcQueryExecuteResult)res.response()).partitionResult(); if (partRes == null || affinityCache.version().equals(partRes.topologyVersion())) { int cacheId = (partRes != null && partRes.tree() != null) ? GridCacheUtils.cacheId(partRes.cacheName()) : -1; PartitionClientContext partClientCtx = partRes != null ? new PartitionClientContext(partRes.partitionsCount()) : null; QualifiedSQLQuery qry = new QualifiedSQLQuery(qryReq.schemaName(), qryReq.sqlQuery()); JdbcThinPartitionResultDescriptor partResDescr = new JdbcThinPartitionResultDescriptor(partRes, cacheId, partClientCtx); affinityCache.addSqlQuery(qry, partResDescr); } } } } /** * Calculates query retries count for given {@param req}. * * @param stickyIo sticky connection, if any. * @param req Jdbc request. * @return retries count. */ private int calculateRetryAttemptsCount(JdbcThinTcpIo stickyIo, JdbcRequest req) { if (!partitionAwareness) return NO_RETRIES; if (stickyIo != null) return NO_RETRIES; if (req.type() == JdbcRequest.META_TABLES || req.type() == JdbcRequest.META_COLUMNS || req.type() == JdbcRequest.META_INDEXES || req.type() == JdbcRequest.META_PARAMS || req.type() == JdbcRequest.META_PRIMARY_KEYS || req.type() == JdbcRequest.META_SCHEMAS || req.type() == JdbcRequest.CACHE_PARTITIONS) return DFLT_RETRIES_CNT; if (req.type() == JdbcRequest.QRY_EXEC) { JdbcQueryExecuteRequest qryExecReq = (JdbcQueryExecuteRequest)req; String trimmedQry = qryExecReq.sqlQuery().trim(); // Last symbol is ignored. for (int i = 0; i < trimmedQry.length() - 1; i++) { if (trimmedQry.charAt(i) == ';') return NO_RETRIES; } return trimmedQry.toUpperCase().startsWith("SELECT") ? DFLT_RETRIES_CNT : NO_RETRIES; } return NO_RETRIES; } /** * Request Timeout Task */ private class RequestTimeoutTask implements Runnable { /** Request id. */ private final long reqId; /** Sticky singleIo. */ private final JdbcThinTcpIo stickyIO; /** Remaining query timeout. */ private int remainingQryTimeout; /** Flag that shows whether TimerTask was expired or not. */ private AtomicBoolean expired; /** * @param reqId Request Id to cancel in case of timeout * @param initReqTimeout Initial request timeout */ RequestTimeoutTask(long reqId, JdbcThinTcpIo stickyIO, int initReqTimeout) { this.reqId = reqId; this.stickyIO = stickyIO; remainingQryTimeout = initReqTimeout; expired = new AtomicBoolean(false); } /** {@inheritDoc} */ @Override public void run() { try { if (remainingQryTimeout <= 0) { expired.set(true); sendQueryCancelRequest(new JdbcQueryCancelRequest(reqId), stickyIO); qryTimeoutScheduledFut.cancel(false); return; } remainingQryTimeout -= REQUEST_TIMEOUT_PERIOD; } catch (SQLException e) { LOG.log(Level.WARNING, "Request timeout processing failure: unable to cancel request [reqId=" + reqId + ']', e); qryTimeoutScheduledFut.cancel(false); } } } /** * Connection Handler Task */ private class ConnectionHandlerTask implements Runnable { /** Map with reconnection delays. */ private Map<InetSocketAddress, Integer> reconnectionDelays = new HashMap<>(); /** Map with reconnection delays remainder. */ private Map<InetSocketAddress, Integer> reconnectionDelaysRemainder = new HashMap<>(); /** {@inheritDoc} */ @Override public void run() { try { for (Map.Entry<InetSocketAddress, Integer> delayEntry : reconnectionDelaysRemainder.entrySet()) reconnectionDelaysRemainder.put(delayEntry.getKey(), delayEntry.getValue() - RECONNECTION_DELAY); Set<InetSocketAddress> aliveSockAddrs = ios.values().stream().map(JdbcThinTcpIo::socketAddress).collect(Collectors.toSet()); IgniteProductVersion prevIgniteEndpointVer = null; for (int i = 0; i < connProps.getAddresses().length; i++) { HostAndPortRange srv = connProps.getAddresses()[i]; try { InetAddress[] addrs = InetAddress.getAllByName(srv.host()); for (InetAddress addr : addrs) { for (int port = srv.portFrom(); port <= srv.portTo(); ++port) { InetSocketAddress sockAddr = null; try { sockAddr = new InetSocketAddress(addr, port); if (aliveSockAddrs.contains(sockAddr)) { reconnectionDelaysRemainder.remove(sockAddr); reconnectionDelays.remove(sockAddr); continue; } Integer delayRemainder = reconnectionDelaysRemainder.get(sockAddr); if (delayRemainder != null && delayRemainder != 0) continue; if (closed) { maintenanceExecutor.shutdown(); return; } JdbcThinTcpIo cliIo = new JdbcThinTcpIo(connProps, new InetSocketAddress(addr, port), ctx, 0); if (!cliIo.isPartitionAwarenessSupported()) { processDelay(sockAddr); LOG.log(Level.WARNING, "Failed to connect to Ignite node [url=" + connProps.getUrl() + "]. address = [" + addr + ':' + port + "]." + "Node doesn't support best effort affinity mode."); cliIo.close(); continue; } if (prevIgniteEndpointVer != null && !prevIgniteEndpointVer.equals(cliIo.igniteVersion())) { processDelay(sockAddr); LOG.log(Level.WARNING, "Failed to connect to Ignite node [url=" + connProps.getUrl() + "]. address = [" + addr + ':' + port + "]." + "Different versions of nodes are not supported in best " + "effort affinity mode."); cliIo.close(); continue; } cliIo.timeout(netTimeout); JdbcThinTcpIo ioToSameNode = ios.putIfAbsent(cliIo.nodeId(), cliIo); // This can happen if the same node has several IPs or if ensureConnected() runs // concurrently if (ioToSameNode != null) cliIo.close(); else connCnt.incrementAndGet(); prevIgniteEndpointVer = cliIo.igniteVersion(); if (closed) { maintenanceExecutor.shutdown(); cliIo.close(); ios.remove(cliIo.nodeId()); return; } } catch (Exception exception) { if (sockAddr != null) processDelay(sockAddr); LOG.log(Level.WARNING, "Failed to connect to Ignite node [url=" + connProps.getUrl() + "]. address = [" + addr + ':' + port + "]."); } } } } catch (Exception exception) { LOG.log(Level.WARNING, "Failed to connect to Ignite node [url=" + connProps.getUrl() + "]. server = [" + srv + "]."); } } } catch (Exception e) { LOG.log(Level.WARNING, "Connection handler processing failure. Reconnection processes was stopped.", e); connectionsHndScheduledFut.cancel(false); } } /** * Increase reconnection delay if needed and store it to corresponding maps. * * @param sockAddr Socket address. */ private void processDelay(InetSocketAddress sockAddr) { Integer delay = reconnectionDelays.get(sockAddr); delay = delay == null ? RECONNECTION_DELAY : delay * 2; if (delay > RECONNECTION_MAX_DELAY) delay = RECONNECTION_MAX_DELAY; reconnectionDelays.put(sockAddr, delay); reconnectionDelaysRemainder.put(sockAddr, delay); } } /** * JDBC implementation of {@link MarshallerContext}. */ private class JdbcMarshallerContext extends BlockingJdbcChannel implements MarshallerContext { /** Type ID -> class name map. */ private final Map<Integer, String> cache = new ConcurrentHashMap<>(); /** */ private final Set<String> sysTypes = new HashSet<>(); /** * Default constructor. */ public JdbcMarshallerContext() { try { processSystemClasses(U.gridClassLoader(), null, sysTypes::add); } catch (IOException e) { throw new IgniteException("Unable to initialize marshaller context", e); } } /** {@inheritDoc} */ @Override public boolean registerClassName( byte platformId, int typeId, String clsName, boolean failIfUnregistered ) throws IgniteCheckedException { assert platformId == MarshallerPlatformIds.JAVA_ID : String.format("Only Java platform is supported [expPlatformId=%d, actualPlatformId=%d].", MarshallerPlatformIds.JAVA_ID, platformId); boolean res = true; if (!cache.containsKey(typeId)) { try { JdbcUpdateBinarySchemaResult updateRes = doRequest( new JdbcBinaryTypeNamePutRequest(typeId, platformId, clsName)); res = updateRes.success(); } catch (ExecutionException | InterruptedException | ClientException | SQLException e) { throw new IgniteCheckedException(e); } if (res) cache.put(typeId, clsName); } return res; } /** {@inheritDoc} */ @Deprecated @Override public boolean registerClassName(byte platformId, int typeId, String clsName) throws IgniteCheckedException { return registerClassName(platformId, typeId, clsName, false); } /** {@inheritDoc} */ @Override public boolean registerClassNameLocally(byte platformId, int typeId, String clsName) { throw new UnsupportedOperationException("registerClassNameLocally not supported by " + this.getClass().getSimpleName()); } /** {@inheritDoc} */ @Override public Class getClass(int typeId, ClassLoader ldr) throws ClassNotFoundException, IgniteCheckedException { return U.forName(getClassName(MarshallerPlatformIds.JAVA_ID, typeId), ldr, null); } /** {@inheritDoc} */ @Override public String getClassName(byte platformId, int typeId) throws ClassNotFoundException, IgniteCheckedException { assert platformId == MarshallerPlatformIds.JAVA_ID : String.format("Only Java platform is supported [expPlatformId=%d, actualPlatformId=%d].", MarshallerPlatformIds.JAVA_ID, platformId); String clsName = cache.get(typeId); if (clsName == null) { try { JdbcBinaryTypeNameGetResult res = doRequest(new JdbcBinaryTypeNameGetRequest(typeId, platformId)); clsName = res.typeName(); } catch (ExecutionException | InterruptedException | ClientException | SQLException e) { throw new IgniteCheckedException(e); } } if (clsName == null) throw new ClassNotFoundException(String.format("Unknown type id [%s]", typeId)); return clsName; } /** * Handle update binary schema result. * * @param res Result. * @return {@code true} if marshaller was waiting for result with given request ID. */ public boolean handleResult(JdbcUpdateBinarySchemaResult res) { return handleResult(res.reqId(), res); } /** * Handle binary type name result. * * @param res Result. * @return {@code true} if marshaller was waiting for result with given request ID. */ public boolean handleResult(JdbcBinaryTypeNameGetResult res) { return handleResult(res.reqId(), res); } /** {@inheritDoc} */ @Override public boolean isSystemType(String typeName) { return sysTypes.contains(typeName); } /** {@inheritDoc} */ @Override public IgnitePredicate<String> classNameFilter() { return null; } /** {@inheritDoc} */ @Override public JdkMarshaller jdkMarshaller() { return new JdkMarshaller(); } } /** * JDBC implementation of {@link BinaryMetadataHandler}. */ private class JdbcBinaryMetadataHandler extends BlockingJdbcChannel implements BinaryMetadataHandler { /** In-memory metadata cache. */ private final BinaryMetadataHandler cache = BinaryCachingMetadataHandler.create(); /** {@inheritDoc} */ @Override public void addMeta(int typeId, BinaryType meta, boolean failIfUnregistered) throws BinaryObjectException { try { doRequest(new JdbcBinaryTypePutRequest(((BinaryTypeImpl)meta).metadata())); } catch (ExecutionException | InterruptedException | ClientException | SQLException e) { throw new BinaryObjectException(e); } cache.addMeta(typeId, meta, failIfUnregistered); // merge } /** {@inheritDoc} */ @Override public void addMetaLocally(int typeId, BinaryType meta, boolean failIfUnregistered) throws BinaryObjectException { throw new UnsupportedOperationException("Can't register metadata locally for thin client."); } /** {@inheritDoc} */ @Override public BinaryType metadata(int typeId) throws BinaryObjectException { BinaryType meta = cache.metadata(typeId); if (meta == null) meta = getBinaryType(typeId); return meta; } /** {@inheritDoc} */ @Override public BinaryMetadata metadata0(int typeId) throws BinaryObjectException { BinaryMetadata meta = cache.metadata0(typeId); if (meta == null) { BinaryTypeImpl binType = (BinaryTypeImpl)getBinaryType(typeId); if (binType != null) meta = binType.metadata(); } return meta; } /** * Request binary type from grid. * * @param typeId Type ID. * @return Binary type. */ private @Nullable BinaryType getBinaryType(int typeId) throws BinaryObjectException { BinaryType binType = null; try { JdbcBinaryTypeGetResult res = doRequest(new JdbcBinaryTypeGetRequest(typeId)); BinaryMetadata meta = res.meta(); if (meta != null) { binType = new BinaryTypeImpl(ctx, meta); cache.addMeta(typeId, binType, false); } } catch (ExecutionException | InterruptedException | ClientException | SQLException e) { throw new BinaryObjectException(e); } return binType; } /** * Handle update binary schema result. * * @param res Result. * @return {@code true} if handler was waiting for result with given * request ID. */ public boolean handleResult(JdbcUpdateBinarySchemaResult res) { return handleResult(res.reqId(), res); } /** * Handle binary type schema result. * * @param res Result. * @return {@code true} if handler was waiting for result with given * request ID. */ public boolean handleResult(JdbcBinaryTypeGetResult res) { return handleResult(res.reqId(), res); } /** {@inheritDoc} */ @Override public BinaryType metadata(int typeId, int schemaId) throws BinaryObjectException { BinaryType type = metadata(typeId); return type != null && ((BinaryTypeImpl)type).metadata().hasSchema(schemaId) ? type : null; } /** {@inheritDoc} */ @Override public Collection<BinaryType> metadata() throws BinaryObjectException { return cache.metadata(); } } /** * Jdbc channel to communicate in blocking style, regardless of whether * streaming mode is enabled or not. */ private abstract class BlockingJdbcChannel { /** Request ID -> Jdbc result map. */ private Map<Long, CompletableFuture<JdbcResult>> results = new ConcurrentHashMap<>(); /** * Do request in blocking style. It just call * {@link JdbcThinConnection#sendRequest(JdbcRequest)} for non-streaming * mode and creates future and waits it completion when streaming is * enabled. * * @param req Request. * @return Result for given request. */ <R extends JdbcResult> R doRequest(JdbcRequest req) throws SQLException, InterruptedException, ExecutionException { R res; if (isStream()) { CompletableFuture<JdbcResult> resFut = new CompletableFuture<>(); CompletableFuture<JdbcResult> oldFut = results.put(req.requestId(), resFut); assert oldFut == null : "Another request with the same id is waiting for result."; sendRequestNotWaitResponse(req, streamState.streamingStickyIo); res = (R)resFut.get(); } else res = sendRequest(req).response(); return res; } /** * Handles result for specified request ID. * * @param reqId Request id. * @param res Result. */ boolean handleResult(long reqId, JdbcResult res) { boolean handled = false; CompletableFuture<JdbcResult> fut = results.remove(reqId); if (fut != null) { fut.complete(res); handled = true; } return handled; } } }