/*
* 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.geode.cache.lucene.internal.filesystem;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collection;
import java.util.concurrent.ConcurrentMap;
/**
* A Filesystem like interface that stores file data in geode regions.
*
* This filesystem is safe for use with multiple threads if the threads are not modifying the same
* files. A single file is not safe to modify by multiple threads, even between different members of
* the distributed system.
*
* Changes to a file may not be visible to other members of the system until the FileOutputStream is
* closed.
*
*/
public class FileSystem {
// private final Cache cache;
private final ConcurrentMap<String, File> fileRegion;
private final ConcurrentMap<ChunkKey, byte[]> chunkRegion;
static final int CHUNK_SIZE = 1024 * 1024; // 1 MB
private final FileSystemStats stats;
/**
* Create filesystem that will store data in the two provided regions. The fileRegion contains
* metadata about the files, and the chunkRegion contains the actual data. If data from either
* region is missing or inconsistent, no guarantees are made about what this class will do, so
* it's best if these regions are colocated and in the same disk store to ensure the data remains
* together.
*
* @param fileRegion the region to store metadata about the files
* @param chunkRegion the region to store actual file data.
*/
public FileSystem(ConcurrentMap<String, File> fileRegion,
ConcurrentMap<ChunkKey, byte[]> chunkRegion, FileSystemStats stats) {
this.fileRegion = fileRegion;
this.chunkRegion = chunkRegion;
this.stats = stats;
}
public Collection<String> listFileNames() {
return fileRegion.keySet();
}
public File createFile(final String name) throws IOException {
// TODO lock region ?
final File file = new File(this, name);
if (null != fileRegion.putIfAbsent(name, file)) {
throw new IOException("File exists.");
}
stats.incFileCreates(1);
// TODO unlock region ?
return file;
}
public File createTemporaryFile(final String name) throws IOException {
final File file = new File(this, name);
stats.incTemporaryFileCreates(1);
return file;
}
public File getFile(final String name) throws FileNotFoundException {
final File file = fileRegion.get(name);
if (null == file) {
throw new FileNotFoundException(name);
}
file.setFileSystem(this);
return file;
}
public void deleteFile(final String name) throws FileNotFoundException {
// TODO locks?
// TODO - What is the state of the system if
// things crash in the middle of removing this file?
// Seems like a file will be left with some
// dangling chunks at the end of the file
File file = fileRegion.remove(name);
if (file == null) {
throw new FileNotFoundException(name);
}
// TODO consider removeAll with all ChunkKeys listed.
final ChunkKey key = new ChunkKey(file.id, 0);
while (true) {
// TODO consider mutable ChunkKey
if (null == chunkRegion.remove(key)) {
// no more chunks
break;
}
key.chunkId++;
}
stats.incFileDeletes(1);
}
public void renameFile(String source, String dest) throws IOException {
final File sourceFile = fileRegion.get(source);
if (null == sourceFile) {
throw new FileNotFoundException(source);
}
final File destFile = createFile(dest);
destFile.chunks = sourceFile.chunks;
destFile.created = sourceFile.created;
destFile.length = sourceFile.length;
destFile.modified = sourceFile.modified;
destFile.id = sourceFile.id;
updateFile(destFile);
// TODO - What is the state of the system if
// things crash in the middle of moving this file?
// Seems like we will have two files pointing
// at the same data
fileRegion.remove(source);
stats.incFileRenames(1);
}
byte[] getChunk(final File file, final int id) {
final ChunkKey key = new ChunkKey(file.id, id);
// The file's metadata indicates that this chunk shouldn't
// exist. Purge all of the chunks that are larger than the file metadata
if (id >= file.chunks) {
while (chunkRegion.containsKey(key)) {
chunkRegion.remove(key);
key.chunkId++;
}
return null;
}
final byte[] chunk = chunkRegion.get(key);
stats.incReadBytes(chunk.length);
return chunk;
}
public void putChunk(final File file, final int id, final byte[] chunk) {
final ChunkKey key = new ChunkKey(file.id, id);
chunkRegion.put(key, chunk);
stats.incWrittenBytes(chunk.length);
}
void updateFile(File file) {
fileRegion.put(file.getName(), file);
}
public ConcurrentMap<String, File> getFileRegion() {
return fileRegion;
}
public ConcurrentMap<ChunkKey, byte[]> getChunkRegion() {
return chunkRegion;
}
/**
* Export all of the files in the filesystem to the provided directory
*/
public void export(final java.io.File exportLocation) {
listFileNames().stream().forEach(fileName -> {
try {
getFile(fileName).export(exportLocation);
} catch (FileNotFoundException e) {
// ignore this, it was concurrently removed
}
});
}
}