/*
* Copyright 2015 JBoss Inc
*
* 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 io.apiman.gateway.engine.jdbc;
import io.apiman.gateway.engine.async.IAsyncResult;
import io.apiman.gateway.engine.async.IAsyncResultHandler;
import io.apiman.gateway.engine.beans.Api;
import io.apiman.gateway.engine.beans.Client;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.ResultSetHandler;
/**
* Extends the {@link JdbcRegistry} to provide multi-node caching. This caching solution
* will work in a cluster, although it is a rather naive implementation. The approach
* taken is that whenever the data in the DB is modified, a "last modified" record is
* inserted/updated. The registry utilizes a thread to periodically poll the DB to
* check if this data has been changed. If the data *has* been changed, then the cache
* is invalidated.
*
* @author eric.wittmann@redhat.com
*/
public class PollCachingJdbcRegistry extends CachingJdbcRegistry {
private static final int DEFAULT_POLLING_INTERVAL = 10;
private static final int DEFAULT_STARTUP_DELAY = 30;
private int pollIntervalMillis;
private int startupDelayMillis;
private boolean polling = false;
private long dataVersion = -1;
/**
* Constructor.
*/
public PollCachingJdbcRegistry(Map<String, String> config) {
super(config);
String intervalVal = config.get("cache-polling-interval"); //$NON-NLS-1$
String startupVal = config.get("cache-polling-startup-delay"); //$NON-NLS-1$
if (intervalVal != null) {
pollIntervalMillis = new Integer(intervalVal) * 1000;
} else {
pollIntervalMillis = DEFAULT_POLLING_INTERVAL * 1000;
}
if (startupVal != null) {
startupDelayMillis = new Integer(startupVal) * 1000;
} else {
startupDelayMillis = DEFAULT_STARTUP_DELAY * 1000;
}
startCacheInvalidator();
}
/**
* @see io.apiman.gateway.engine.CachingJdbcRegistry.CachingESRegistry#publishApi(io.apiman.gateway.engine.beans.Api, io.apiman.gateway.engine.async.IAsyncResultHandler)
*/
@Override
public void publishApi(Api api, final IAsyncResultHandler<Void> handler) {
super.publishApi(api, new IAsyncResultHandler<Void>() {
@Override
public void handle(IAsyncResult<Void> result) {
if (result.isSuccess()) {
updateDataVersion();
}
handler.handle(result);
}
});
}
/**
* @see io.apiman.gateway.engine.CachingJdbcRegistry.CachingESRegistry#retireApi(io.apiman.gateway.engine.beans.Api, io.apiman.gateway.engine.async.IAsyncResultHandler)
*/
@Override
public void retireApi(Api api, final IAsyncResultHandler<Void> handler) {
super.retireApi(api, new IAsyncResultHandler<Void>() {
@Override
public void handle(IAsyncResult<Void> result) {
if (result.isSuccess()) {
updateDataVersion();
}
handler.handle(result);
}
});
}
/**
* @see io.apiman.gateway.engine.CachingJdbcRegistry.CachingESRegistry#registerClient(io.apiman.gateway.engine.beans.Client, io.apiman.gateway.engine.async.IAsyncResultHandler)
*/
@Override
public void registerClient(Client client, final IAsyncResultHandler<Void> handler) {
super.registerClient(client, new IAsyncResultHandler<Void>() {
/**
* @see io.apiman.gateway.engine.async.IAsyncHandler#handle(java.lang.Object)
*/
@Override
public void handle(IAsyncResult<Void> result) {
if (result.isSuccess()) {
updateDataVersion();
}
handler.handle(result);
}
});
}
/**
* @see io.apiman.gateway.engine.CachingJdbcRegistry.CachingESRegistry#unregisterClient(io.apiman.gateway.engine.beans.Client, io.apiman.gateway.engine.async.IAsyncResultHandler)
*/
@Override
public void unregisterClient(Client client, final IAsyncResultHandler<Void> handler) {
super.unregisterClient(client, new IAsyncResultHandler<Void>() {
@Override
public void handle(IAsyncResult<Void> result) {
if (result.isSuccess()) {
updateDataVersion();
}
handler.handle(result);
}
});
}
/**
* Stores a "dataversion" record in the ES store. There is only a single one of these. The
* return value of the add will include the version number of the entity. This version
* number is what we use to determine whether our cache is stale.
*/
protected void updateDataVersion() {
Connection conn = null;
try {
long newVersion = System.currentTimeMillis();
conn = ds.getConnection();
conn.setAutoCommit(false);
QueryRunner run = new QueryRunner();
run.update(conn, "DELETE FROM gw_dataversion"); //$NON-NLS-1$
run.update(conn, "INSERT INTO gw_dataversion (version) VALUES (?)", //$NON-NLS-1$
newVersion);
DbUtils.commitAndClose(conn);
dataVersion = newVersion;
} catch (SQLException e) {
dataVersion = -1;
}
}
/**
* Starts up a thread that polls the ES store for updates.
*/
protected void startCacheInvalidator() {
polling = true;
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// Wait on startup before starting to poll.
try { Thread.sleep(startupDelayMillis); } catch (InterruptedException e1) { e1.printStackTrace(); }
while (polling) {
try { Thread.sleep(pollIntervalMillis); } catch (Exception e) { e.printStackTrace(); }
checkCacheVersion();
}
}
}, "JdbcRegistryCacheInvalidator"); //$NON-NLS-1$
thread.setDaemon(true);
thread.start();
}
/**
* Stop polling.
*/
public void stop() {
polling = false;
}
/**
* Checks the ES store to see if the 'dataVersion' entry has been updated with a newer
* version #. If it has, then we need to invalidate our cache.
*/
protected void checkCacheVersion() {
// Be very aggressive in invalidating the cache.
boolean invalidate = true;
QueryRunner run = new QueryRunner(ds);
try {
long latestVersion = run.query("SELECT version FROM gw_dataversion", Handlers.LONG_HANDLER); //$NON-NLS-1$
if (latestVersion > -1 && dataVersion > -1 && latestVersion == dataVersion) {
invalidate = false;
} else {
dataVersion = latestVersion;
}
} catch (SQLException e) {
// TODO need to use the gateway logger to log this!
e.printStackTrace();
}
if (invalidate) {
invalidateCache();
}
}
private static final class Handlers {
public static final ResultSetHandler<Long> LONG_HANDLER = (ResultSet rs) -> {
if (!rs.next()) {
return -1L;
}
return rs.getLong(1);
};
}
}