/*
* 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.jdbc2;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.IgniteJdbcDriver;
import org.apache.ignite.IgniteSystemProperties;
import org.apache.ignite.cache.query.QueryCursor;
import org.apache.ignite.cache.query.SqlFieldsQuery;
import org.apache.ignite.internal.IgniteKernal;
import org.apache.ignite.internal.processors.cache.QueryCursorImpl;
import org.apache.ignite.internal.processors.query.GridQueryFieldMetadata;
import org.apache.ignite.internal.util.typedef.CAX;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgniteCallable;
import org.apache.ignite.resources.IgniteInstanceResource;
/**
* Task for SQL queries execution through {@link IgniteJdbcDriver}.
* <p>
* Not closed cursors will be removed after {@link #RMV_DELAY} milliseconds.
* This parameter can be configured via {@link IgniteSystemProperties#IGNITE_JDBC_DRIVER_CURSOR_REMOVE_DELAY}
* system property.
*/
class JdbcQueryTask implements IgniteCallable<JdbcQueryTask.QueryResult> {
/** Serial version uid. */
private static final long serialVersionUID = 0L;
/** How long to store open cursor. */
private static final long RMV_DELAY = IgniteSystemProperties.getLong(
IgniteSystemProperties.IGNITE_JDBC_DRIVER_CURSOR_REMOVE_DELAY, 600000);
/** Scheduler. */
private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1);
/** Open cursors. */
private static final ConcurrentMap<UUID, Cursor> CURSORS = new ConcurrentHashMap<>();
/** Ignite. */
@IgniteInstanceResource
private Ignite ignite;
/** Uuid. */
private final UUID uuid;
/** Cache name. */
private final String cacheName;
/** Sql. */
private final String sql;
/** Operation type flag - query or not. */
private Boolean isQry;
/** Args. */
private final Object[] args;
/** Fetch size. */
private final int fetchSize;
/** Local execution flag. */
private final boolean loc;
/** Local query flag. */
private final boolean locQry;
/** Collocated query flag. */
private final boolean collocatedQry;
/** Distributed joins flag. */
private final boolean distributedJoins;
/**
* @param ignite Ignite.
* @param cacheName Cache name.
* @param sql Sql query.
* @param isQry Operation type flag - query or not - to enforce query type check.
* @param loc Local execution flag.
* @param args Args.
* @param fetchSize Fetch size.
* @param uuid UUID.
* @param locQry Local query flag.
* @param collocatedQry Collocated query flag.
* @param distributedJoins Distributed joins flag.
*/
public JdbcQueryTask(Ignite ignite, String cacheName, String sql,
Boolean isQry, boolean loc, Object[] args, int fetchSize, UUID uuid,
boolean locQry, boolean collocatedQry, boolean distributedJoins) {
this.ignite = ignite;
this.args = args;
this.uuid = uuid;
this.cacheName = cacheName;
this.sql = sql;
this.isQry = isQry;
this.fetchSize = fetchSize;
this.loc = loc;
this.locQry = locQry;
this.collocatedQry = collocatedQry;
this.distributedJoins = distributedJoins;
}
/** {@inheritDoc} */
@Override public JdbcQueryTask.QueryResult call() throws Exception {
Cursor cursor = CURSORS.get(uuid);
List<String> tbls = null;
List<String> cols = null;
List<String> types = null;
boolean first;
if (first = (cursor == null)) {
IgniteCache<?, ?> cache = ignite.cache(cacheName);
// Don't create caches on server nodes in order to avoid of data rebalancing.
boolean start = ignite.configuration().isClientMode();
if (cache == null && cacheName == null)
cache = ((IgniteKernal)ignite).context().cache().getOrStartPublicCache(start, !loc && locQry);
if (cache == null) {
if (cacheName == null)
throw new SQLException("Failed to execute query. No suitable caches found.");
else
throw new SQLException("Cache not found [cacheName=" + cacheName + ']');
}
SqlFieldsQuery qry = (isQry != null ? new JdbcSqlFieldsQuery(sql, isQry) : new SqlFieldsQuery(sql))
.setArgs(args);
qry.setPageSize(fetchSize);
qry.setLocal(locQry);
qry.setCollocated(collocatedQry);
qry.setDistributedJoins(distributedJoins);
QueryCursorImpl<List<?>> qryCursor = (QueryCursorImpl<List<?>>)cache.withKeepBinary().query(qry);
if (isQry == null)
isQry = qryCursor.isQuery();
Collection<GridQueryFieldMetadata> meta = qryCursor.fieldsMeta();
tbls = new ArrayList<>(meta.size());
cols = new ArrayList<>(meta.size());
types = new ArrayList<>(meta.size());
for (GridQueryFieldMetadata desc : meta) {
tbls.add(desc.typeName());
cols.add(desc.fieldName().toUpperCase());
types.add(desc.fieldTypeName());
}
CURSORS.put(uuid, cursor = new Cursor(qryCursor, qryCursor.iterator()));
}
List<List<?>> rows = new ArrayList<>();
for (List<?> row : cursor) {
List<Object> row0 = new ArrayList<>(row.size());
for (Object val : row)
row0.add(val == null || JdbcUtils.isSqlType(val.getClass()) ? val : val.toString());
rows.add(row0);
if (rows.size() == fetchSize) // If fetchSize is 0 then unlimited
break;
}
boolean finished = !cursor.hasNext();
if (finished)
remove(uuid, cursor);
else if (first) {
if (!loc)
scheduleRemoval(uuid, RMV_DELAY);
}
else if (!loc && !CURSORS.replace(uuid, cursor, new Cursor(cursor.cursor, cursor.iter)))
assert !CURSORS.containsKey(uuid) : "Concurrent cursor modification.";
assert isQry != null : "Query flag must be set prior to returning result";
return new QueryResult(uuid, finished, isQry, rows, cols, tbls, types);
}
/**
* Schedules removal of stored cursor in case of remote query execution.
*
* @param uuid Cursor UUID.
* @param delay Delay in milliseconds.
*/
private void scheduleRemoval(final UUID uuid, long delay) {
assert !loc;
SCHEDULER.schedule(new CAX() {
@Override public void applyx() {
while (true) {
Cursor c = CURSORS.get(uuid);
if (c == null)
break;
// If the cursor was accessed since last scheduling then reschedule.
long untouchedTime = U.currentTimeMillis() - c.lastAccessTime;
if (untouchedTime < RMV_DELAY) {
scheduleRemoval(uuid, RMV_DELAY - untouchedTime);
break;
}
else if (remove(uuid, c))
break;
}
}
}, delay, TimeUnit.MILLISECONDS);
}
/**
* @param uuid Cursor UUID.
* @param c Cursor.
* @return {@code true} If succeeded.
*/
private static boolean remove(UUID uuid, Cursor c) {
boolean rmv = CURSORS.remove(uuid, c);
if (rmv)
c.cursor.close();
return rmv;
}
/**
* Closes and removes cursor.
*
* @param uuid Cursor UUID.
*/
static void remove(UUID uuid) {
Cursor c = CURSORS.remove(uuid);
if (c != null)
c.cursor.close();
}
/**
* Result of query execution.
*/
static class QueryResult implements Serializable {
/** Serial version uid. */
private static final long serialVersionUID = 0L;
/** Uuid. */
private final UUID uuid;
/** Finished. */
private final boolean finished;
/** Result type - query or update. */
private final boolean isQry;
/** Rows. */
private final List<List<?>> rows;
/** Tables. */
private final List<String> tbls;
/** Columns. */
private final List<String> cols;
/** Types. */
private final List<String> types;
/**
* @param uuid UUID..
* @param finished Finished.
* @param isQry
* @param rows Rows.
* @param cols Columns.
* @param tbls Tables.
* @param types Types.
*/
public QueryResult(UUID uuid, boolean finished, boolean isQry, List<List<?>> rows, List<String> cols,
List<String> tbls, List<String> types) {
this.isQry = isQry;
this.cols = cols;
this.uuid = uuid;
this.finished = finished;
this.rows = rows;
this.tbls = tbls;
this.types = types;
}
/**
* @return Query result rows.
*/
public List<List<?>> getRows() {
return rows;
}
/**
* @return Tables metadata.
*/
public List<String> getTbls() {
return tbls;
}
/**
* @return Columns metadata.
*/
public List<String> getCols() {
return cols;
}
/**
* @return Types metadata.
*/
public List<String> getTypes() {
return types;
}
/**
* @return Query UUID.
*/
public UUID getUuid() {
return uuid;
}
/**
* @return {@code True} if it is finished query.
*/
public boolean isFinished() {
return finished;
}
/**
* @return {@code true} if it is result of a query operation, not update; {@code false} otherwise.
*/
public boolean isQuery() {
return isQry;
}
}
/**
* Cursor.
*/
private static final class Cursor implements Iterable<List<?>> {
/** Cursor. */
final QueryCursor<List<?>> cursor;
/** Iterator. */
final Iterator<List<?>> iter;
/** Last access time. */
final long lastAccessTime;
/**
* @param cursor Cursor.
* @param iter Iterator.
*/
private Cursor(QueryCursor<List<?>> cursor, Iterator<List<?>> iter) {
this.cursor = cursor;
this.iter = iter;
this.lastAccessTime = U.currentTimeMillis();
}
/** {@inheritDoc} */
@Override public Iterator<List<?>> iterator() {
return iter;
}
/**
* @return {@code True} if cursor has next element.
*/
public boolean hasNext() {
return iter.hasNext();
}
}
}