/*
* JBoss, Home of Professional Open Source.
* See the COPYRIGHT.txt file distributed with this work for information
* regarding copyright ownership. Some portions may be licensed
* to Red Hat, Inc. under one or more contributor license agreements.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*/
package org.teiid.runtime;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.teiid.adminapi.VDB.Status;
import org.teiid.adminapi.impl.ModelMetaData;
import org.teiid.adminapi.impl.VDBMetaData;
import org.teiid.client.util.ResultsFuture;
import org.teiid.client.util.ResultsFuture.CompletionListener;
import org.teiid.core.TeiidRuntimeException;
import org.teiid.core.util.StringUtil;
import org.teiid.deployers.CompositeVDB;
import org.teiid.deployers.ContainerLifeCycleListener;
import org.teiid.deployers.VDBLifeCycleListener;
import org.teiid.deployers.VDBRepository;
import org.teiid.dqp.internal.process.DQPCore;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.metadata.MetadataStore;
import org.teiid.metadata.Schema;
import org.teiid.metadata.Table;
import org.teiid.query.metadata.MaterializationMetadataRepository;
import org.teiid.query.metadata.TransformationMetadata;
import org.teiid.query.tempdata.TempTableDataManager;
import org.teiid.runtime.NodeTracker.NodeListener;
import org.teiid.vdb.runtime.VDBKey;
public abstract class MaterializationManager implements VDBLifeCycleListener, NodeListener {
private interface MaterializationAction {
void process(Table table);
}
private static final int WAITTIME = 60000;
public abstract Executor getExecutor();
public abstract ScheduledExecutorService getScheduledExecutorService();
public abstract DQPCore getDQP();
public abstract VDBRepository getVDBRepository();
private ContainerLifeCycleListener shutdownListener;
public MaterializationManager (ContainerLifeCycleListener shutdownListener) {
this.shutdownListener = shutdownListener;
}
@Override
public void added(String name, CompositeVDB cvdb) {
}
@Override
public void beforeRemove(String name, CompositeVDB cvdb) {
if (cvdb == null) {
return;
}
final VDBMetaData vdb = cvdb.getVDB();
// cancel any matview load pending tasks
Collection<Future<?>> tasks = cvdb.clearTasks();
if (tasks != null && !tasks.isEmpty()) {
for (Future<?> f:tasks) {
f.cancel(true);
}
}
// If VDB is being undeployed, run the shutdown triggers
if (!shutdownListener.isShutdownInProgress()) {
doMaterializationActions(vdb, new MaterializationAction() {
@Override
public void process(Table table) {
if (table.getMaterializedTable() == null) {
return;
}
String remove = table.getProperty(MaterializationMetadataRepository.ON_VDB_DROP_SCRIPT, false);
if (remove != null) {
try {
executeQuery(vdb, remove);
} catch (SQLException e) {
LogManager.logWarning(LogConstants.CTX_MATVIEWS, e, e.getMessage());
}
}
}
});
}
}
@Override
public void removed(String name, CompositeVDB cvdb) {
}
@Override
public void finishedDeployment(String name, final CompositeVDB cvdb) {
// execute start triggers
final VDBMetaData vdb = cvdb.getVDB();
if (vdb.getStatus() != Status.ACTIVE) {
return;
}
doMaterializationActions(vdb, new MaterializationAction() {
@Override
public void process(final Table table) {
if (table.getMaterializedTable() == null) {
String ttlStr = table.getProperty(MaterializationMetadataRepository.MATVIEW_TTL, false);
if (ttlStr != null) {
long ttl = Long.parseLong(ttlStr);
if (ttl > 0) {
//TODO: make the interval based upon the state as with the external, but
//for now just refresh on schedule
Future<?> f = getScheduledExecutorService().scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
boolean invalidate = TempTableDataManager.shouldInvalidate(vdb);
try {
executeAsynchQuery(vdb, "call SYSADMIN.refreshMatView('" + table.getFullName().replaceAll("'", "''") + "', " + invalidate + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
} catch (SQLException e) {
LogManager.logWarning(LogConstants.CTX_MATVIEWS, e, e.getMessage());
}
}
}, 0, ttl, TimeUnit.MILLISECONDS);
cvdb.addTask(f);
return;
}
}
//just a one time load
try {
//we use a count so that the load can cascade
executeAsynchQuery(vdb, "select count(*) from " + table.getSQLString()); //$NON-NLS-1$
} catch (SQLException e) {
LogManager.logWarning(LogConstants.CTX_MATVIEWS, e, e.getMessage());
}
return;
}
// reset any jobs started by this node that did not complete
String nodeName = System.getProperty("jboss.node.name"); //$NON-NLS-1$
resetPendingJob(vdb, table, nodeName);
String start = table.getProperty(MaterializationMetadataRepository.ON_VDB_START_SCRIPT, false);
if (start != null) {
try {
executeQuery(vdb, start);
} catch (SQLException e) {
LogManager.logWarning(LogConstants.CTX_MATVIEWS, e, e.getMessage());
}
}
long ttl = 0;
String ttlStr = table.getProperty(MaterializationMetadataRepository.MATVIEW_TTL, false);
if (ttlStr == null) {
ttlStr = String.valueOf(Long.MAX_VALUE);
}
if (ttlStr != null) {
ttl = Long.parseLong(ttlStr);
if (ttl > 0) {
scheduleSnapshotJob(cvdb, table, ttl, 0L, false);
}
}
String stalenessString = table.getProperty(MaterializationMetadataRepository.MATVIEW_MAX_STALENESS_PCT, false);
if (stalenessString != null) {
// run first time like, SNAPSHOT
if (ttl <= 0) {
scheduleSnapshotJob(cvdb, table, 0L, 0L, true);
}
// schedule a check every minute
scheduleSnapshotJob(cvdb, table, WAITTIME, WAITTIME, true);
}
}
});
}
public int resetPendingJob(final VDBMetaData vdb, final Table table, String nodeName){
try {
String statusTable = table.getProperty(MaterializationMetadataRepository.MATVIEW_STATUS_TABLE, false);
String updateStatusTable = "UPDATE "+statusTable+" SET LOADSTATE='needs_loading' "
+ "WHERE LOADSTATE = 'LOADING' AND NODENAME = '"+nodeName+"' "
+ "AND NAME = '"+table.getName()+"'";
List<Map<String, String>> results = executeQuery(vdb, updateStatusTable);
String count = results.get(0).get("update-count");
return Integer.parseInt(count);
} catch (SQLException e) {
LogManager.logWarning(LogConstants.CTX_MATVIEWS, e, e.getMessage());
}
return 0;
}
private void doMaterializationActions(VDBMetaData vdb, MaterializationAction action) {
TransformationMetadata metadata = vdb.getAttachment(TransformationMetadata.class);
if (metadata == null) {
return;
}
Set<String> imports = vdb.getImportedModels();
MetadataStore store = metadata.getMetadataStore();
// schedule materialization loads and do the start actions
for (Schema schema : store.getSchemaList()) {
if (imports.contains(schema.getName())) {
continue;
}
for (Table table:schema.getTables().values()) {
// find external matview table
if (!table.isVirtual() || !table.isMaterialized()
|| !Boolean.valueOf(table.getProperty(MaterializationMetadataRepository.ALLOW_MATVIEW_MANAGEMENT, false))) {
continue;
}
action.process(table);
}
}
}
public void scheduleSnapshotJob(CompositeVDB vdb, Table table, long ttl, long delay, boolean oneTimeJob) {
SnapshotJobScheduler task = new SnapshotJobScheduler(vdb, table, ttl, delay, oneTimeJob);
queueTask(vdb, task, delay);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void runJob(final CompositeVDB vdb, final Table table, final long ttl, final boolean onetimeJob) {
String command = "execute SYSADMIN.loadMatView('"+StringUtil.replaceAll(table.getParent().getName(), "'", "''")+"','"+StringUtil.replaceAll(table.getName(), "'", "''")+"')"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$
try {
final AtomicInteger procReturn = new AtomicInteger();
executeAsynchQuery(vdb.getVDB(), command, new DQPCore.ResultsListener() {
@Override
public void onResults(List<String> columns, List<? extends List<?>> results) throws Exception {
procReturn.set((Integer)results.get(0).get(0));
}}).addCompletionListener(new CompletionListener() {
@Override
public void onCompletion(ResultsFuture future) {
try {
future.get();
if (!onetimeJob) {
if (procReturn.get() >= 0) {
scheduleSnapshotJob(vdb, table, ttl, ttl, onetimeJob);
} else {
// when in error re-schedule in 1 min or less
scheduleSnapshotJob(vdb, table, ttl, Math.min(ttl/4, WAITTIME), onetimeJob);
}
}
} catch (InterruptedException e) {
} catch (ExecutionException e) {
LogManager.logWarning(LogConstants.CTX_MATVIEWS, e, e.getMessage());
scheduleSnapshotJob(vdb, table, ttl, Math.min(ttl/4, WAITTIME), onetimeJob); // re-schedule the same job in one minute
}
}
});
} catch (SQLException e) {
LogManager.logWarning(LogConstants.CTX_MATVIEWS, e, e.getMessage());
scheduleSnapshotJob(vdb, table, ttl, Math.min(ttl/4, WAITTIME), onetimeJob); // re-schedule the same job in one minute
}
}
private void queueTask(CompositeVDB vdb, SnapshotJobScheduler task, long delay) {
ScheduledFuture<?> sf = getScheduledExecutorService().schedule(task, (delay < 0)?0:delay, TimeUnit.MILLISECONDS);
task.attachScheduledFuture(sf);
}
interface MaterializationTask extends Runnable {
void attachScheduledFuture(ScheduledFuture<?> sf);
void removeScheduledFuture();
}
class SnapshotJobScheduler implements MaterializationTask {
protected Table table;
protected long ttl;
protected long delay;
protected CompositeVDB vdb;
protected ScheduledFuture<?> future;
protected boolean oneTimeJob;
public SnapshotJobScheduler(CompositeVDB vdb, Table table, long ttl, long delay, boolean oneTimeJob) {
this.vdb = vdb;
this.table = table;
this.ttl = ttl;
this.delay = delay;
this.oneTimeJob = oneTimeJob;
}
public void attachScheduledFuture(ScheduledFuture<?> sf) {
vdb.addTask(sf);
future = sf;
}
public void removeScheduledFuture() {
vdb.removeTask(future);
future = null;
}
@Override
public void run() {
removeScheduledFuture();
String query = "execute SYSADMIN.matViewStatus('"+StringUtil.replaceAll(table.getParent().getName(), "'", "''")+"', '"+StringUtil.replaceAll(table.getName(), "'", "''")+"')"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$
// in rare situations when VDB is force re-deployed, and is results in invalid
// deployment, due to nature of VDB deployment even previous VDB is removed.
if (vdb.getVDB().getStatus() != Status.ACTIVE) {
return;
}
// when source or target data source(s) are down, there is no reason
// to run the materialization jobs. schedule for later time
if (!vdb.getVDB().isValid()) {
scheduleSnapshotJob(vdb, table, ttl, Math.min(ttl/4, WAITTIME), oneTimeJob); // re-schedule the same job in one minute
return;
}
List<Map<String, String>> result = null;
try {
result = executeQuery(vdb.getVDB(), query);
} catch (SQLException e) {
LogManager.logWarning(LogConstants.CTX_MATVIEWS, e, e.getMessage());
scheduleSnapshotJob(vdb, table, ttl, Math.min(ttl/4, WAITTIME), oneTimeJob); // re-schedule the same job in one minute
return;
}
long updated = 0L;
this.delay = ttl;
String loadstate = null;
boolean valid = false;
if (result != null && !result.isEmpty()) {
Map<String, String> row = result.get(0);
if (row != null) {
loadstate = row.get("LoadState"); //$NON-NLS-1$
updated = Long.parseLong(row.get("Updated")); //$NON-NLS-1$
valid = Boolean.parseBoolean(row.get("Valid")); //$NON-NLS-1$
}
}
long elapsed = System.currentTimeMillis() - updated;
long next = ttl;
if (loadstate == null || loadstate.equalsIgnoreCase("needs_loading") || !valid) { //$NON-NLS-1$
// no entry found run immediately
runJob(vdb, table, ttl, oneTimeJob);
return;
}
else if (loadstate.equalsIgnoreCase("loading")) { //$NON-NLS-1$
// if the process is already loading do nothing
next = ttl - elapsed;
if (elapsed >= ttl) {
next = Math.min(ttl / 4, 60000);
}
}
else if (loadstate.equalsIgnoreCase("loaded")) { //$NON-NLS-1$
if (elapsed >= ttl) {
runJob(vdb, table, ttl, oneTimeJob);
return;
}
next = ttl - elapsed;
}
else if (loadstate.equalsIgnoreCase("failed_load")) { //$NON-NLS-1$
if (elapsed > ttl/4 || elapsed > 60000) { // exceeds 1/4 of cached time or 1 mins
runJob(vdb, table, ttl, oneTimeJob);
return;
}
next = Math.min(((ttl/4)-elapsed), (60000-elapsed));
}
scheduleSnapshotJob(vdb, table, ttl, next, oneTimeJob);
}
}
public ResultsFuture<?> executeAsynchQuery(VDBMetaData vdb, String command) throws SQLException {
try {
return DQPCore.executeQuery(command, vdb, "embedded-async", "internal", -1, getDQP(), new DQPCore.ResultsListener() { //$NON-NLS-1$ //$NON-NLS-2$
@Override
public void onResults(List<String> columns,
List<? extends List<?>> results) throws Exception {
}
});
} catch (Throwable e) {
throw new SQLException(e);
}
}
public ResultsFuture<?> executeAsynchQuery(VDBMetaData vdb, String command, DQPCore.ResultsListener listener) throws SQLException {
try {
return DQPCore.executeQuery(command, vdb, "embedded-async", "internal", -1, getDQP(), listener); //$NON-NLS-1$ //$NON-NLS-2$
} catch (Throwable e) {
throw new SQLException(e);
}
}
public List<Map<String, String>> executeQuery(VDBMetaData vdb, String command) throws SQLException {
final List<Map<String, String>> rows = new ArrayList<Map<String,String>>();
try {
Future<?> f = DQPCore.executeQuery(command, vdb, "embedded-async", "internal", -1, getDQP(), new DQPCore.ResultsListener() { //$NON-NLS-1$ //$NON-NLS-2$
@Override
public void onResults(List<String> columns, List<? extends List<?>> results) throws Exception {
for (List<?> row:results) {
TreeMap<String, String> rowResult = new TreeMap<String, String>();
for (int colNum = 0; colNum < columns.size(); colNum++) {
Object value = row.get(colNum);
if (value != null) {
if (value instanceof Timestamp) {
value = ((Timestamp)value).getTime();
}
}
rowResult.put(columns.get(colNum), value == null?null:value.toString());
}
rows.add(rowResult);
}
}
});
f.get();
} catch (InterruptedException e) {
//break
throw new TeiidRuntimeException(e);
} catch (ExecutionException e) {
if (e.getCause() != null) {
throw new SQLException(e.getCause());
}
throw new SQLException(e);
} catch (Throwable e) {
throw new SQLException(e);
}
return rows;
}
@Override
public void nodeJoined(String nodeName) {
// may be nothing to do for now, we can envision replicating cache pro actively.
}
@Override
public void nodeDropped(String nodeName) {
for (VDBMetaData vdb:getVDBRepository().getVDBs()) {
TransformationMetadata metadata = vdb.getAttachment(TransformationMetadata.class);
if (metadata == null) {
continue;
}
for (ModelMetaData model : vdb.getModelMetaDatas().values()) {
if (vdb.getImportedModels().contains(model.getName())) {
continue;
}
MetadataStore store = metadata.getMetadataStore();
Schema schema = store.getSchema(model.getName());
for (Table t:schema.getTables().values()) {
if (t.isVirtual() && t.isMaterialized() && t.getMaterializedTable() != null) {
String allow = t.getProperty(MaterializationMetadataRepository.ALLOW_MATVIEW_MANAGEMENT, false);
if (allow == null || !Boolean.valueOf(allow)) {
continue;
}
// reset the pending job if there is one.
int update = resetPendingJob(vdb, t, nodeName);
if (update > 0) {
String ttlStr = t.getProperty(MaterializationMetadataRepository.MATVIEW_TTL, false);
if (ttlStr == null) {
ttlStr = String.valueOf(Long.MAX_VALUE);
}
if (ttlStr != null) {
long ttl = Long.parseLong(ttlStr);
if (ttl > 0) {
// run the job
CompositeVDB cvdb = getVDBRepository().getCompositeVDB(new VDBKey(vdb.getName(), vdb.getVersion()));
scheduleSnapshotJob(cvdb, t, ttl, 0L, true);
}
}
}
}
}
}
}
}
}