/*
* Copyright (C) 2015 hops.io.
*
* 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.apache.hadoop.hdfs.server.namenode;
import io.hops.common.IDsGeneratorFactory;
import io.hops.exception.StorageException;
import io.hops.exception.TransactionContextException;
import io.hops.exception.TransientStorageException;
import io.hops.metadata.HdfsStorageFactory;
import io.hops.metadata.hdfs.dal.QuotaUpdateDataAccess;
import io.hops.metadata.hdfs.entity.INodeIdentifier;
import io.hops.metadata.hdfs.entity.QuotaUpdate;
import io.hops.transaction.EntityManager;
import io.hops.transaction.handler.HDFSOperationType;
import io.hops.transaction.handler.HopsTransactionalRequestHandler;
import io.hops.transaction.handler.LightWeightRequestHandler;
import io.hops.transaction.lock.LockFactory;
import io.hops.transaction.lock.SubtreeLockHelper;
import io.hops.transaction.lock.TransactionLockTypes;
import io.hops.transaction.lock.TransactionLocks;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hdfs.DFSConfigKeys;
import org.apache.hadoop.util.Daemon;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import static org.apache.hadoop.util.ExitUtil.terminate;
/**
* Daemon that is asynchronously updating the quota counts of directories.
* Each operation that affects the quota adds a log entry to our database.
* This daemon periodically reads a batch of these updates, combines them if
* possible and applies them.
*/
public class QuotaUpdateManager {
static final Log LOG = LogFactory.getLog(QuotaUpdateManager.class);
private final FSNamesystem namesystem;
private final int updateInterval;
private final int updateLimit;
private final Daemon updateThread = new Daemon(new QuotaUpdateMonitor());
private final ConcurrentLinkedQueue<Iterator<Integer>> prioritizedUpdates =
new ConcurrentLinkedQueue<Iterator<Integer>>();
public QuotaUpdateManager(FSNamesystem namesystem, Configuration conf) {
this.namesystem = namesystem;
updateInterval =
conf.getInt(DFSConfigKeys.DFS_NAMENODE_QUOTA_UPDATE_INTERVAL_KEY,
DFSConfigKeys.DFS_NAMENODE_QUOTA_UPDATE_INTERVAL_DEFAULT);
updateLimit = conf.getInt(DFSConfigKeys.DFS_NAMENODE_QUOTA_UPDATE_LIMIT_KEY,
DFSConfigKeys.DFS_NAMENODE_QUOTA_UPDATE_LIMIT_DEFAULT);
}
public void activate() {
LOG.debug("QuotaUpdateMonitor is running");
updateThread.start();
}
public void close() {
if (updateThread != null) {
updateThread.interrupt();
try {
updateThread.join(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private int nextId() {
return IDsGeneratorFactory.getInstance().getUniqueQuotaUpdateID();
}
public void addUpdate(final int inodeId, final long namespaceDelta,
final long diskspaceDelta)
throws StorageException, TransactionContextException {
QuotaUpdate update =
new QuotaUpdate(nextId(), inodeId, namespaceDelta, diskspaceDelta);
EntityManager.add(update);
}
private class QuotaUpdateMonitor implements Runnable {
@Override
public void run() {
long startTime;
while (namesystem.isRunning()) {
startTime = System.currentTimeMillis();
try {
if (namesystem.isLeader()) {
if (!prioritizedUpdates.isEmpty()) {
Iterator<Integer> iterator = prioritizedUpdates.poll();
while (iterator.hasNext()) {
processUpdates(iterator.next());
}
synchronized (iterator) {
iterator.notify();
}
}
processNextUpdateBatch();
}
long sleepDuration =
updateInterval - (System.currentTimeMillis() - startTime);
if (sleepDuration > 0) {
Thread.sleep(updateInterval);
}
} catch (InterruptedException ie) {
LOG.warn("QuotaUpdateMonitor thread received InterruptedException.",
ie);
break;
} catch (StorageException e) {
LOG.warn("QuotaUpdateMonitor thread received StorageException.", e);
if( e instanceof TransientStorageException) {
continue; // do not quit thread on storage exception
}
terminate(1, e);
} catch (Throwable t) {
LOG.fatal("QuotaUpdateMonitor thread received Runtime exception. ",
t);
terminate(1, t);
}
}
}
}
private final Comparator<QuotaUpdate> quotaUpdateComparator =
new Comparator<QuotaUpdate>() {
@Override
public int compare(QuotaUpdate quotaUpdate, QuotaUpdate quotaUpdate2) {
if (quotaUpdate.getInodeId() < quotaUpdate2.getInodeId()) {
return -1;
}
if (quotaUpdate.getInodeId() > quotaUpdate2.getInodeId()) {
return 1;
}
return 0;
}
};
private void processUpdates(final Integer id) throws IOException {
LightWeightRequestHandler findHandler =
new LightWeightRequestHandler(HDFSOperationType.GET_UPDATES_FOR_ID) {
@Override
public Object performTask() throws IOException {
QuotaUpdateDataAccess<QuotaUpdate> dataAccess =
(QuotaUpdateDataAccess) HdfsStorageFactory
.getDataAccess(QuotaUpdateDataAccess.class);
return dataAccess.findByInodeId(id);
}
};
List<QuotaUpdate> quotaUpdates = (List<QuotaUpdate>) findHandler.handle();
LOG.debug("processUpdates for inode id="+id+" quotaUpdates ids are "+ Arrays.toString(quotaUpdates.toArray()));
applyBatchedUpdate(quotaUpdates);
}
private void processNextUpdateBatch() throws IOException {
LightWeightRequestHandler findHandler =
new LightWeightRequestHandler(HDFSOperationType.GET_NEXT_QUOTA_BATCH) {
@Override
public Object performTask() throws IOException {
QuotaUpdateDataAccess<QuotaUpdate> dataAccess =
(QuotaUpdateDataAccess) HdfsStorageFactory
.getDataAccess(QuotaUpdateDataAccess.class);
return dataAccess.findLimited(updateLimit);
}
};
List<QuotaUpdate> quotaUpdates = (List<QuotaUpdate>) findHandler.handle();
Collections.sort(quotaUpdates, quotaUpdateComparator);
ArrayList<QuotaUpdate> batch = new ArrayList<QuotaUpdate>();
for (QuotaUpdate update : quotaUpdates) {
if (batch.size() == 0 ||
batch.get(0).getInodeId() == update.getInodeId()) {
batch.add(update);
} else {
applyBatchedUpdate(batch);
batch = new ArrayList<QuotaUpdate>();
batch.add(update);
}
}
if (batch.size() != 0) {
applyBatchedUpdate(batch);
}
}
private void applyBatchedUpdate(final List<QuotaUpdate> updates)
throws IOException {
if (updates.size() == 0) {
return;
}
new HopsTransactionalRequestHandler(HDFSOperationType.APPLY_QUOTA_UPDATE) {
INodeIdentifier iNodeIdentifier;
@Override
public void setUp() throws IOException {
super.setUp();
iNodeIdentifier = new INodeIdentifier(updates.get(0).getInodeId());
}
@Override
public void acquireLock(TransactionLocks locks) throws IOException {
LockFactory lf = LockFactory.getInstance();
locks.add(
lf.getIndividualINodeLock(TransactionLockTypes.INodeLockType.WRITE,
iNodeIdentifier));
}
@Override
public Object performTask() throws IOException {
INodeDirectory dir = (INodeDirectory) EntityManager
.find(INode.Finder.ByINodeIdFTIS, updates.get(0).getInodeId());
if (dir != null && SubtreeLockHelper
.isSubtreeLocked(dir.isSubtreeLocked(), dir.getSubtreeLockOwner(),
namesystem.getNameNode().getActiveNameNodes()
.getActiveNodes()) && dir.getSubtreeLockOwner() != namesystem.getNamenodeId()) {
LOG.warn("XXXXXXXXXXXXXXX ignoring updates as the subtree lock is set");
/*
* We cannot process updates to keep move operations consistent. Otherwise the calculated size of the subtree
* could differ from the view of the parent if outstanding quota updates are applied after being considered
* by the QuotaCountingFileTree but before successfully moving the subtree.
*/
return null;
}
long namespaceDelta = 0;
long diskspaceDelta = 0;
for (QuotaUpdate update : updates) {
namespaceDelta += update.getNamespaceDelta();
diskspaceDelta += update.getDiskspaceDelta();
LOG.debug("handling " + update);
EntityManager.remove(update);
}
if (dir == null) {
LOG.debug("dropping update for " + updates.get(0) + " ns " +
namespaceDelta + " ds " + diskspaceDelta +
" because of deletion");
return null;
}
if (namespaceDelta == 0 && diskspaceDelta == 0) {
return null;
}
if (dir != null && dir.isQuotaSet()) {
INodeDirectoryWithQuota quotaDir = (INodeDirectoryWithQuota) dir;
INodeAttributes attributes = quotaDir.getINodeAttributes();
attributes.setNsCount(attributes.getNsCount() + namespaceDelta);
attributes.setDiskspace(attributes.getDiskspace() + diskspaceDelta);
LOG.debug("applying aggregated update for directory " + dir.getId() +
" with namespace delta " + namespaceDelta +
" and diskspace delta " + diskspaceDelta);
}
if (dir != null && dir.getId() != INodeDirectory.ROOT_ID) {
QuotaUpdate parentUpdate =
new QuotaUpdate(nextId(), dir.getParentId(), namespaceDelta,
diskspaceDelta);
EntityManager.add(parentUpdate);
LOG.debug("adding parent update " + parentUpdate);
}
return null;
}
}.handle(this);
}
/**
* Ids which updates need to be applied. Note that children must occur before
* their parents
* in order to guarantee that updates are applied completely.
*
* @param iterator
* Ids to be updates sorted from the leaves to the root of the subtree
*/
void addPrioritizedUpdates(Iterator<Integer> iterator) {
prioritizedUpdates.add(iterator);
}
}