/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org)
*
* Licensed 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.jkiss.dbeaver.runtime.qm;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.exec.DBCExecutionContext;
import org.jkiss.dbeaver.model.exec.DBCResultSet;
import org.jkiss.dbeaver.model.exec.DBCSavepoint;
import org.jkiss.dbeaver.model.exec.DBCStatement;
import org.jkiss.dbeaver.model.qm.QMMCollector;
import org.jkiss.dbeaver.model.qm.QMMetaEvent;
import org.jkiss.dbeaver.model.qm.QMMetaListener;
import org.jkiss.dbeaver.model.qm.meta.*;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.runtime.AbstractJob;
import org.jkiss.utils.LongKeyMap;
import java.util.*;
/**
* Query manager execution handler implementation
*/
public class QMMCollectorImpl extends DefaultExecutionHandler implements QMMCollector {
private static final Log log = Log.getLog(QMMCollectorImpl.class);
private static final long EVENT_DISPATCH_PERIOD = 250;
private static final int MAX_HISTORY_EVENTS = 1000;
// Session map
private LongKeyMap<QMMSessionInfo> sessionMap = new LongKeyMap<>();
private List<Long> closedSessions = new ArrayList<>();
// External listeners
private List<QMMetaListener> listeners = new ArrayList<>();
// Temporary event pool
private List<QMMetaEvent> eventPool = new ArrayList<>();
// Sync object
private final Object historySync = new Object();
// History (may be purged when limit reached)
private List<QMMetaEvent> pastEvents = new ArrayList<>();
private boolean running = true;
public QMMCollectorImpl()
{
new EventDispatcher().schedule(EVENT_DISPATCH_PERIOD);
}
public synchronized void dispose()
{
if (!sessionMap.isEmpty()) {
List<QMMSessionInfo> openSessions = new ArrayList<>();
for (QMMSessionInfo session : sessionMap.values()) {
if (!session.isClosed()) {
openSessions.add(session);
}
}
if (!openSessions.isEmpty()) {
log.warn("Some sessions are still open: " + openSessions);
}
}
if (!listeners.isEmpty()) {
log.warn("Some QM meta collector listeners are still open: " + listeners);
listeners.clear();
}
running = false;
}
boolean isRunning()
{
return running;
}
@NotNull
@Override
public String getHandlerName()
{
return "Meta info collector";
}
public synchronized void addListener(QMMetaListener listener)
{
listeners.add(listener);
}
public synchronized void removeListener(QMMetaListener listener)
{
if (!listeners.remove(listener)) {
log.warn("Listener '" + listener + "' is not registered in QM meta collector");
}
}
private synchronized List<QMMetaListener> getListeners()
{
if (listeners.isEmpty()) {
return Collections.emptyList();
}
if (listeners.size() == 1) {
return Collections.singletonList(listeners.get(0));
}
return new ArrayList<>(listeners);
}
private synchronized void fireMetaEvent(final QMMObject object, final QMMetaEvent.Action action)
{
eventPool.add(new QMMetaEvent(object, action));
}
private synchronized List<QMMetaEvent> obtainEvents()
{
if (eventPool.isEmpty()) {
return Collections.emptyList();
}
List<QMMetaEvent> events = eventPool;
eventPool = new ArrayList<>();
return events;
}
public QMMSessionInfo getSessionInfo(DBCExecutionContext context)
{
QMMSessionInfo sessionInfo = sessionMap.get(context.getContextId());
if (sessionInfo == null) {
log.debug("Can't find sessionInfo meta information: " + context.getContextId() + " (" + context.getContextName() + ")");
}
return sessionInfo;
}
public List<QMMetaEvent> getPastEvents()
{
synchronized (historySync) {
return new ArrayList<>(pastEvents);
}
}
@Override
public synchronized void handleContextOpen(@NotNull DBCExecutionContext context, boolean transactional)
{
final long contextId = context.getContextId();
QMMSessionInfo session = sessionMap.get(contextId);
if (session == null) {
session = new QMMSessionInfo(
context,
transactional);
sessionMap.put(contextId, session);
} else {
// This session may already be in cache in case of reconnect/invalidate
// (when context closed and reopened without new context object creation)
session.reopen();
}
// Remove from closed sessions (in case of re-opened connection)
closedSessions.remove(contextId);
// Notify
fireMetaEvent(session, QMMetaEvent.Action.BEGIN);
}
@Override
public synchronized void handleContextClose(@NotNull DBCExecutionContext context)
{
QMMSessionInfo session = getSessionInfo(context);
if (session != null) {
session.close();
fireMetaEvent(session, QMMetaEvent.Action.END);
}
closedSessions.add(context.getContextId());
}
@Override
public synchronized void handleTransactionAutocommit(@NotNull DBCExecutionContext context, boolean autoCommit)
{
QMMSessionInfo sessionInfo = getSessionInfo(context);
if (sessionInfo != null) {
QMMTransactionInfo oldTxn = sessionInfo.changeTransactional(!autoCommit);
if (oldTxn != null) {
fireMetaEvent(oldTxn, QMMetaEvent.Action.END);
}
fireMetaEvent(sessionInfo, QMMetaEvent.Action.UPDATE);
}
}
@Override
public synchronized void handleTransactionCommit(@NotNull DBCExecutionContext context)
{
QMMSessionInfo sessionInfo = getSessionInfo(context);
if (sessionInfo != null) {
QMMTransactionInfo oldTxn = sessionInfo.commit();
if (oldTxn != null) {
fireMetaEvent(oldTxn, QMMetaEvent.Action.END);
}
}
}
@Override
public synchronized void handleTransactionRollback(@NotNull DBCExecutionContext context, DBCSavepoint savepoint)
{
QMMSessionInfo sessionInfo = getSessionInfo(context);
if (sessionInfo != null) {
QMMObject oldTxn = sessionInfo.rollback(savepoint);
if (oldTxn != null) {
fireMetaEvent(oldTxn, QMMetaEvent.Action.END);
}
}
}
@Override
public synchronized void handleStatementOpen(@NotNull DBCStatement statement)
{
QMMSessionInfo session = getSessionInfo(statement.getSession().getExecutionContext());
if (session != null) {
QMMStatementInfo stat = session.openStatement(statement);
fireMetaEvent(stat, QMMetaEvent.Action.BEGIN);
}
}
@Override
public synchronized void handleStatementClose(@NotNull DBCStatement statement, long rows)
{
QMMSessionInfo session = getSessionInfo(statement.getSession().getExecutionContext());
if (session != null) {
QMMStatementInfo stat = session.closeStatement(statement, rows);
if (stat == null) {
log.warn("Can't properly handle statement close");
} else {
fireMetaEvent(stat, QMMetaEvent.Action.END);
}
}
}
@Override
public synchronized void handleStatementExecuteBegin(@NotNull DBCStatement statement)
{
QMMSessionInfo session = getSessionInfo(statement.getSession().getExecutionContext());
if (session != null) {
QMMStatementExecuteInfo exec = session.beginExecution(statement);
if (exec != null) {
fireMetaEvent(exec, QMMetaEvent.Action.BEGIN);
}
}
}
@Override
public synchronized void handleStatementExecuteEnd(@NotNull DBCStatement statement, long rows, Throwable error)
{
QMMSessionInfo session = getSessionInfo(statement.getSession().getExecutionContext());
if (session != null) {
QMMStatementExecuteInfo exec = session.endExecution(statement, rows, error);
if (exec != null) {
fireMetaEvent(exec, QMMetaEvent.Action.END);
}
}
}
@Override
public synchronized void handleResultSetOpen(@NotNull DBCResultSet resultSet)
{
QMMSessionInfo session = getSessionInfo(resultSet.getSession().getExecutionContext());
if (session != null) {
QMMStatementExecuteInfo exec = session.beginFetch(resultSet);
if (exec != null) {
fireMetaEvent(exec, QMMetaEvent.Action.UPDATE);
}
}
}
@Override
public synchronized void handleResultSetClose(@NotNull DBCResultSet resultSet, long rowCount)
{
QMMSessionInfo session = getSessionInfo(resultSet.getSession().getExecutionContext());
if (session != null) {
QMMStatementExecuteInfo exec = session.endFetch(resultSet, rowCount);
if (exec != null) {
fireMetaEvent(exec, QMMetaEvent.Action.UPDATE);
}
}
}
private class EventDispatcher extends AbstractJob {
protected EventDispatcher()
{
super("QM meta events dispatcher");
setUser(false);
setSystem(true);
}
@Override
protected IStatus run(DBRProgressMonitor monitor)
{
final List<QMMetaEvent> events;
List<Long> sessionsToClose;
synchronized (QMMCollectorImpl.this) {
events = obtainEvents();
sessionsToClose = closedSessions;
closedSessions.clear();
}
final List<QMMetaListener> listeners = getListeners();
if (!listeners.isEmpty() && !events.isEmpty()) {
// Dispatch all events
for (QMMetaListener listener : listeners) {
try {
listener.metaInfoChanged(events);
} catch (Throwable e) {
log.error("Error notifying event listener", e);
}
}
}
synchronized (historySync) {
pastEvents.addAll(events);
int size = pastEvents.size();
if (size > MAX_HISTORY_EVENTS) {
pastEvents = new ArrayList<>(pastEvents.subList(
size - MAX_HISTORY_EVENTS,
size));
}
}
// Cleanup closed sessions
synchronized (QMMCollectorImpl.this) {
for (Long sessionId : sessionsToClose) {
final QMMSessionInfo session = sessionMap.get(sessionId);
if (session != null && !session.isClosed()) {
// It is possible (rarely) that session was reopened before event dispatcher run
// In that case just ignore it
sessionMap.remove(sessionId);
}
}
}
if (isRunning()) {
this.schedule(EVENT_DISPATCH_PERIOD);
}
return Status.OK_STATUS;
}
}
}