org.apache.wicket.pageStore.FileChannelPool Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.wicket.pageStore;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import org.apache.wicket.util.file.Files;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Thread safe pool of {@link FileChannel} objects.
*
* Opening and closing file is an expensive operation and under certain circumstances this can
* significantly harm the performance, because on every close the file system cache might be
* flushed.
*
* To minimize the negative impact opened files can be pooled, which is a responsibility of
* {@link FileChannelPool} class.
*
* {@link FileChannelPool} allows to specify maximum number of opened {@link FileChannel}s.
*
* Note that under certain circumstances (when there are no empty slots in pool) the initial
* capacity can be exceeded (more files are opened than the specified capacity is). If this happens,
* a warning is written to log, as this probably means that there is a problem with page store.
*
* @author Matej Knopp
*/
public class FileChannelPool
{
private static final Logger log = LoggerFactory.getLogger(FileChannelPool.class);
private final Map nameToChannel = new HashMap();
private final Map channelToName = new HashMap();
private final Map channelToUseCount = new HashMap();
private final LinkedList idleChannels = new LinkedList();
private final Set channelsToDeleteOnReturn = new HashSet();
private final int capacity;
/**
* Construct.
*
* @param capacity
* Maximum number of opened file channels.
*/
public FileChannelPool(int capacity)
{
this.capacity = capacity;
if (capacity < 1)
{
throw new IllegalArgumentException("Capacity must be at least one.");
}
log.debug("Starting file channel pool with capacity of '{}' channels", capacity);
}
/**
* Creates a new file channel with specified file name.
*
* @param fileName
* @param createIfDoesNotExist
* in case the file does not exist this parameter determines if the file should be
* created
* @return file channel or null
*/
private FileChannel newFileChannel(String fileName, boolean createIfDoesNotExist)
{
File file = new File(fileName);
if (file.exists() == false && createIfDoesNotExist == false)
{
return null;
}
try
{
return new RandomAccessFile(file, "rw").getChannel();
}
catch (FileNotFoundException e)
{
throw new RuntimeException(e);
}
}
/**
* Tries to reduce (close) enough channels to have at least one channel free (so that there are
* maximum capacity - 1 opened channel).
*/
private void reduceChannels()
{
// how much channels we need to close?
int channelsToReduce = nameToChannel.size() - capacity + 1;
// while there are still channels to close and we have still idle
// channels left
while (channelsToReduce > 0 && idleChannels.isEmpty() == false)
{
FileChannel channel = idleChannels.removeFirst();
String channelName = channelToName.get(channel);
// remove oldest idle channel
nameToChannel.remove(channelName);
channelToName.remove(channel);
// this shouldn't really happen
if (channelToUseCount.get(channel) != null)
{
log.warn("Channel " + channelName + " is both idle and in use at the same time!");
channelToUseCount.remove(channel);
}
try
{
channel.close();
}
catch (IOException e)
{
log.error("Error closing file channel", e);
}
--channelsToReduce;
}
if (channelsToReduce > 0)
{
log.warn("Unable to reduce enough channels, no idle channels left to remove.");
}
}
/**
* Returns a channel for given file. If the file doesn't exist, the createIfDoesNotExit
* attribute specifies if the file should be created.
*
* Do NOT call close on the returned channel. Instead call
* {@link #returnFileChannel(FileChannel)}
*
* @param fileName
* @param createIfDoesNotExist
* @return file channel
*/
public synchronized FileChannel getFileChannel(String fileName, boolean createIfDoesNotExist)
{
FileChannel channel = nameToChannel.get(fileName);
if (channel == null)
{
channel = newFileChannel(fileName, createIfDoesNotExist);
if (channel != null)
{
// we need to create new channel
// first, check how many channels we have already opened
if (nameToChannel.size() >= capacity)
{
reduceChannels();
}
nameToChannel.put(fileName, channel);
channelToName.put(channel, fileName);
}
}
if (channel != null)
{
// increase the usage count for this channel
Integer count = channelToUseCount.get(channel);
if (count == null || count == 0)
{
channelToUseCount.put(channel, 1);
idleChannels.remove(channel);
}
else
{
count = count + 1;
channelToUseCount.put(channel, count);
}
}
return channel;
}
/**
* Returns the channel to the pool. It is necessary to call this for every channel obtained by
* calling {@link #getFileChannel(String, boolean)}.
*
* @param channel
*/
public synchronized void returnFileChannel(FileChannel channel)
{
Integer count = channelToUseCount.get(channel);
if (count == null || count == 0)
{
throw new IllegalArgumentException("Trying to return unused channel");
}
count = count - 1;
// decrease the usage count
if (count == 0)
{
channelToUseCount.remove(channel);
if (channelsToDeleteOnReturn.contains(channel))
{
closeAndDelete(channel);
}
else
{
// this was the last usage, add chanel to idle channels
idleChannels.addLast(channel);
}
}
else
{
channelToUseCount.put(channel, count);
}
}
/**
*
* @param channel
*/
private void closeAndDelete(FileChannel channel)
{
channelsToDeleteOnReturn.remove(channel);
String name = channelToName.get(channel);
channelToName.remove(channel);
channelToUseCount.remove(channel);
idleChannels.remove(channel);
try
{
channel.close();
}
catch (IOException e)
{
log.error("Error closing file channel", e);
}
File file = new File(name);
Files.remove(file);
}
/**
* Closes the file channel with given name and removes it from pool. Also removes the file from
* file system. If the channel is in use, the pool first waits until the channel is returned to
* the pool and then closes it.
*
* @param name
*/
public synchronized void closeAndDeleteFileChannel(String name)
{
FileChannel channel = nameToChannel.get(name);
if (channel != null)
{
nameToChannel.remove(name);
Integer count = channelToUseCount.get(channel);
if (count != null && count > 0)
{
channelsToDeleteOnReturn.add(channel);
}
else
{
closeAndDelete(channel);
}
}
else
{
File file = new File(name);
Files.remove(file);
}
}
/**
* Destroys the {@link FileChannel} pool and closes all opened channels.
*/
public synchronized void destroy()
{
log.debug("Destroying FileChannel pool");
for (FileChannel channel : channelToName.keySet())
{
try
{
channel.close();
}
catch (IOException e)
{
log.error("Error closing file channel", e);
}
}
}
}