/*
* INESC-ID, Instituto de Engenharia de Sistemas e Computadores Investigação e Desevolvimento em Lisboa
* Copyright 2013 INESC-ID and/or its affiliates and other
* contributors as indicated by the @author tags. All rights reserved.
* See the copyright.txt in the distribution for a full listing of
* individual contributors.
*
* This 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 3.0 of
* the License, or (at your option) any later version.
*
* This software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.infinispan.transaction.gmu.manager;
import org.infinispan.cacheviews.CacheViewsManager;
import org.infinispan.commands.CommandsFactory;
import org.infinispan.commands.remote.GarbageCollectorControlCommand;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.container.DataContainer;
import org.infinispan.container.gmu.L1GMUContainer;
import org.infinispan.container.versioning.EntryVersion;
import org.infinispan.container.versioning.VersionGenerator;
import org.infinispan.container.versioning.gmu.GMUVersion;
import org.infinispan.container.versioning.gmu.GMUVersionGenerator;
import org.infinispan.factories.annotations.Inject;
import org.infinispan.factories.annotations.Start;
import org.infinispan.factories.annotations.Stop;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachemanagerlistener.CacheManagerNotifier;
import org.infinispan.notifications.cachemanagerlistener.annotation.Merged;
import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged;
import org.infinispan.notifications.cachemanagerlistener.event.Event;
import org.infinispan.remoting.responses.Response;
import org.infinispan.remoting.responses.SuccessfulResponse;
import org.infinispan.remoting.rpc.ResponseMode;
import org.infinispan.remoting.rpc.RpcManager;
import org.infinispan.remoting.transport.Address;
import org.infinispan.transaction.LocalTransaction;
import org.infinispan.transaction.TransactionTable;
import org.infinispan.transaction.gmu.CommitLog;
import org.infinispan.util.concurrent.IsolationLevel;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static org.infinispan.commands.remote.GarbageCollectorControlCommand.Type.*;
import static org.infinispan.transaction.gmu.GMUHelper.toGMUVersionGenerator;
/**
* // TODO: Document this
*
* @author Pedro Ruivo
* @since 5.2
*/
@Listener
public class GarbageCollectorManager {
private static final Log log = LogFactory.getLog(GarbageCollectorManager.class);
private CommitLog commitLog;
private CommandsFactory commandsFactory;
private RpcManager rpcManager;
private Configuration configuration;
private GMUVersionGenerator versionGenerator;
private DataContainer dataContainer;
private TransactionTable transactionTable;
private L1GMUContainer l1GMUContainer;
private CacheViewsManager cacheViewsManager;
private boolean enabled;
private VersionGarbageCollectorThread versionGarbageCollectorThread;
private L1GarbageCollectorThread l1GarbageCollectorThread;
private ViewGarbageCollectorThread viewGarbageCollectorThread;
private CacheManagerNotifier cacheManagerNotifier;
@Inject
public void inject(CommitLog commitLog, CommandsFactory commandsFactory, RpcManager rpcManager,
Configuration configuration, VersionGenerator versionGenerator, DataContainer dataContainer,
TransactionTable transactionTable, L1GMUContainer l1GMUContainer, CacheViewsManager cacheViewsManager,
CacheManagerNotifier cacheManagerNotifier) {
this.enabled = configuration.garbageCollector().enabled() &&
configuration.locking().isolationLevel() == IsolationLevel.SERIALIZABLE;
if (!enabled) {
return;
}
this.commitLog = commitLog;
this.commandsFactory = commandsFactory;
this.rpcManager = rpcManager;
this.configuration = configuration;
this.versionGenerator = toGMUVersionGenerator(versionGenerator);
this.dataContainer = dataContainer;
this.transactionTable = transactionTable;
this.l1GMUContainer = l1GMUContainer;
this.cacheViewsManager = cacheViewsManager;
this.cacheManagerNotifier = cacheManagerNotifier;
}
@Start
public void start() {
if (!enabled) {
return;
}
versionGarbageCollectorThread = new VersionGarbageCollectorThread(configuration.garbageCollector().transactionThreshold(), configuration.garbageCollector().versionGCMaxIdle());
l1GarbageCollectorThread = new L1GarbageCollectorThread(configuration.garbageCollector().l1GCInterval());
viewGarbageCollectorThread = new ViewGarbageCollectorThread(configuration.garbageCollector().viewGCBackOff());
if (enabled) {
versionGarbageCollectorThread.start();
l1GarbageCollectorThread.start();
viewGarbageCollectorThread.start();
cacheManagerNotifier.addListener(this);
}
}
@Stop
public void stop() {
if (!enabled) {
return;
}
versionGarbageCollectorThread.interrupt();
l1GarbageCollectorThread.interrupt();
viewGarbageCollectorThread.interrupt();
cacheManagerNotifier.removeListener(this);
}
/**
* The main goal is to delete all version that cannot be visible by any transaction in the system. To do that, some
* remote interaction is needed.
* <p/>
* The algorithm is performed in 4 steps:
* <p/>
* 1 - for each node i, returns the i-th position for the oldest commit log entry used by it owns local transactions
* 2 - a version (VC) is created with all the previous values 3 - gets the newest commit log entry that is lower than
* the VC created in 2) 4 - delete all commit log entries older than the version in 3) and delete all values with
* version older than the version in 3)
*/
public final synchronized void triggerVersionGarbageCollection() {
if (!enabled) {
return;
}
versionGarbageCollectorThread.trigger();
}
/**
* The main goal is to delete all older view id => addresses that are not in use by any version
* <p/>
* Note: the view id is used in gmu distributed version, which are only used in transactions versions and commit log
* entries. checking in the commit log should be enough to determine the minimum view id
* <p/>
* Note: this algorithm is triggered only by the coordinator
* <p/>
* The algorithm is performed in 3 steps: 1 - collect the minimum view id in commit log from local node and remote
* node 2 - broadcast the minimum view id to all members 3 - when the message in 2) is received, delete all view ids
* less than minimum view id
*/
public final synchronized void triggerViewGarbageCollection() {
if (!enabled) {
return;
}
viewGarbageCollectorThread.triggerNow();
}
/**
* The main goal is to delete all versions that are not visible any more. Note that the L1 is used only be local
* transactions, so no remote interaction is needed.
* <p/>
* The algorithm is performed in 3 steps: 1 - pick the oldest version in commit log used by the local transactions 2
* - delete all values with creation version less than the version in 1)
*/
public final synchronized void triggerL1GarbageCollection() {
if (!enabled || configuration.clustering().cacheMode().isReplicated()) {
return;
}
l1GarbageCollectorThread.trigger();
}
public final GMUVersion handleGetMinimumVisibleVersion() {
if (!enabled) {
return null;
}
List<EntryVersion> localTransactionsCommitLogEntries = new LinkedList<EntryVersion>();
for (LocalTransaction localTransaction : transactionTable.getLocalTransactions()) {
localTransactionsCommitLogEntries.add(localTransaction.getTransactionVersion());
}
EntryVersion[] array = new EntryVersion[localTransactionsCommitLogEntries.size()];
array = localTransactionsCommitLogEntries.toArray(array);
GMUVersion minimumVisibleVersion = versionGenerator.mergeAndMin(array);
if (log.isTraceEnabled()) {
log.tracef("handleGetMinimumVisibleVersion() ==> %s", minimumVisibleVersion);
}
return minimumVisibleVersion;
}
public final int handleGetMinimumVisibleViewId() {
if (!enabled) {
return -1;
}
int minimumVisibleViewId = commitLog.calculateMinimumViewId();
if (log.isTraceEnabled()) {
log.tracef("handleGetMinimumVisibleViewId() ==> %s", minimumVisibleViewId);
}
return minimumVisibleViewId;
}
public final void handleDeleteOlderViewId(int minimumVisibleId) {
if (!enabled) {
return;
}
if (log.isTraceEnabled()) {
log.tracef("Deleting older view than %s", minimumVisibleId);
}
cacheViewsManager.gcViewHistory(commandsFactory.getCacheName(), minimumVisibleId);
versionGenerator.gcCacheView(minimumVisibleId);
}
public final void notifyCommittedTransaction() {
if (!enabled) {
return;
}
versionGarbageCollectorThread.incrementCommittedTx();
}
@ViewChanged
@Merged
public final void handle(Event event) {
if (!enabled) {
return;
}
viewGarbageCollectorThread.trigger();
}
private long getTimeout() {
return configuration.clustering().sync().replTimeout();
}
private <T> Map<Address, T> convert(Map<Address, Response> responseMap, Class<T> tClass) throws Exception {
Map<Address, T> retVal = new HashMap<Address, T>();
for (Map.Entry<Address, Response> entry : responseMap.entrySet()) {
Response response = entry.getValue();
if (!response.isSuccessful()) {
throw new Exception("Error executing command");
}
retVal.put(entry.getKey(), tClass.cast(((SuccessfulResponse) response).getResponseValue()));
}
return retVal;
}
private class VersionGarbageCollectorThread extends Thread {
private final int transactionThreshold;
private final int maxIdleTime;
private int txCommittedSinceLastReset;
private volatile boolean running;
private VersionGarbageCollectorThread(int transactionThreshold, int maxIdleTime) {
super("Version-GC-Thread");
this.transactionThreshold = transactionThreshold;
this.maxIdleTime = maxIdleTime;
}
@Override
public void run() {
running = true;
while (running) {
block();
if (running) {
gc();
}
}
}
@Override
public void interrupt() {
running = false;
super.interrupt();
}
public final synchronized void incrementCommittedTx() {
txCommittedSinceLastReset++;
if (txCommittedSinceLastReset > transactionThreshold) {
notify();
}
}
public final synchronized void trigger() {
notify();
}
/**
* The main goal is to delete all version that cannot be visible by any transaction in the system. To do that,
* some remote interaction is needed.
* <p/>
* The algorithm is performed in 4 steps:
* <p/>
* 1 - for each node i, returns the i-th position for the oldest commit log entry used by it owns local
* transactions 2 - a version (VC) is created with all the previous values 3 - gets the newest commit log entry
* that is lower than the VC created in 2) 4 - delete all commit log entries older than the version in 3) and
* delete all values with version older than the version in 3)
*/
private void gc() {
try {
if (log.isTraceEnabled()) {
log.tracef("Starting Garbage Collection for old versions");
}
//step 1
GarbageCollectorControlCommand cmd = commandsFactory.buildGarbageCollectorControlCommand(GET_VERSION, -1);
Map<Address, Response> responseMap = rpcManager.invokeRemotely(null, cmd,
ResponseMode.SYNCHRONOUS_IGNORE_LEAVERS,
getTimeout(), true, false);
Map<Address, EntryVersion> minVersionValues = convert(responseMap, EntryVersion.class);
commandsFactory.initializeReplicableCommand(cmd, false);
minVersionValues.put(rpcManager.getAddress(), (EntryVersion) cmd.perform(null));
EntryVersion[] array = new EntryVersion[minVersionValues.values().size()];
array = minVersionValues.values().toArray(array);
//step 2
GMUVersion globalMinimumVersion = versionGenerator.mergeAndMin(array);
if (log.isTraceEnabled()) {
log.tracef("Finished collection current transactions versions. Minimum version is %s",
globalMinimumVersion);
}
//step 3
GMUVersion minimumLocalVersion = commitLog.gcOlderVersions(globalMinimumVersion);
if (log.isTraceEnabled()) {
log.tracef("Minimum local visible version is %s", minimumLocalVersion);
}
//step 4
dataContainer.gc(minimumLocalVersion);
} catch (Throwable throwable) {
log.warnf("Exception caught while garbage collecting oldest versions: " + throwable.getLocalizedMessage());
}
}
private synchronized void block() {
txCommittedSinceLastReset = 0;
try {
wait(maxIdleTime * 1000);
} catch (InterruptedException e) {
//no-op
}
}
}
private class L1GarbageCollectorThread extends Thread {
private final int interval;
private volatile boolean running;
private L1GarbageCollectorThread(int interval) {
super("L1-GC-Thread");
this.interval = interval;
}
@Override
public void run() {
if (interval == 0 || configuration.clustering().cacheMode().isReplicated()) {
return;
}
running = true;
while (running) {
block();
if (running) {
gc();
}
}
}
@Override
public void interrupt() {
running = false;
super.interrupt();
}
public final synchronized void trigger() {
notify();
}
/**
* The main goal is to delete all versions that are not visible any more. Note that the L1 is used only be local
* transactions, so no remote interaction is needed.
* <p/>
* The algorithm is performed in 2 steps: 1 - pick the oldest version in commit log used by the local transactions
* 2 - delete all values with creation version less than the version in 1)
*/
private void gc() {
if (log.isTraceEnabled()) {
log.tracef("Starting Garbage Collection for old L1 versions");
}
//step 1
GMUVersion minLocalVersion = handleGetMinimumVisibleVersion();
if (log.isTraceEnabled()) {
log.tracef("Garbage Collector old versions in L1 Cache. Minimum local visible version is %s",
minLocalVersion);
}
//step 2
l1GMUContainer.gc(minLocalVersion);
}
private synchronized void block() {
try {
wait(interval * 1000);
} catch (InterruptedException e) {
//no-op
}
}
}
private class ViewGarbageCollectorThread extends Thread {
private final int backOff;
private boolean running;
private boolean triggered;
private boolean triggerNow;
private ViewGarbageCollectorThread(int backOff) {
super("View-GC-Thread");
this.backOff = backOff;
}
@Override
public void run() {
running = true;
while (running) {
block();
if (running) {
gc();
}
}
}
public final synchronized void triggerNow() {
triggerNow = true;
notify();
}
@Override
public void interrupt() {
running = false;
super.interrupt();
}
@ViewChanged
@Merged
public void handle(Event event) {
trigger();
}
public final synchronized void trigger() {
triggered = true;
notify();
}
/**
* The main goal is to delete all older view id => addresses that are not in use by any version
* <p/>
* Note: the view id is used in gmu distributed version, which are only used in transactions versions and commit
* log entries. checking in the commit log should be enough to determine the minimum view id
* <p/>
* Note: this algorithm is triggered only by the coordinator
* <p/>
* The algorithm is performed in 3 steps: 1 - collect the minimum view id in commit log from local node and remote
* node 2 - broadcast the minimum view id to all members 3 - when the message in 2) is received, delete all view
* ids less than minimum view id
*/
private void gc() {
try {
if (log.isTraceEnabled()) {
log.tracef("Starting Garbage Collector for old cache views");
}
GarbageCollectorControlCommand request = commandsFactory.buildGarbageCollectorControlCommand(GET_VIEW_ID, -1);
Map<Address, Response> responseMap = rpcManager.invokeRemotely(null, request,
ResponseMode.SYNCHRONOUS_IGNORE_LEAVERS,
getTimeout(), true, false);
Map<Address, Integer> viewIdMap = convert(responseMap, Integer.class);
commandsFactory.initializeReplicableCommand(request, false);
viewIdMap.put(rpcManager.getAddress(), (Integer) request.perform(null));
Iterator<Integer> iterator = viewIdMap.values().iterator();
int minimumViewId;
if (iterator.hasNext()) {
minimumViewId = iterator.next();
} else {
if (log.isTraceEnabled()) {
log.tracef("No Garbage Collect needed");
}
//no GC
return;
}
while (iterator.hasNext()) {
minimumViewId = Math.min(minimumViewId, iterator.next());
}
if (log.isTraceEnabled()) {
log.tracef("Garbage collect cache views older than %s", minimumViewId);
}
GarbageCollectorControlCommand control = commandsFactory.buildGarbageCollectorControlCommand(SET_VIEW_ID,
minimumViewId);
rpcManager.broadcastRpcCommand(control, false, false);
commandsFactory.initializeReplicableCommand(control, false);
control.perform(null);
} catch (Throwable throwable) {
log.warnf("Exception caught while garbage collecting oldest cache views: " + throwable.getLocalizedMessage());
}
}
private synchronized void block() {
try {
while (!triggered && !triggerNow) {
wait();
}
while (triggered && !triggerNow) {
triggered = false;
wait(backOff * 1000);
}
} catch (InterruptedException e) {
//no-op
}
triggered = false;
triggerNow = false;
}
}
}