blob: 8f53c67b7ee138e0e12fb1bb34a7f52db2557779 [file] [log] [blame]
/*
* 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.Map;
import java.util.stream.Collectors;
import org.apache.logging.log4j.Logger;
import org.apache.geode.cache.lucene.internal.IndexRepositoryFactory;
import org.apache.geode.logging.internal.log4j.api.LogService;
/**
* 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 static final Logger logger = LogService.getLogger();
private final Map fileAndChunkRegion;
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 fileAndChunkRegion
* 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 fileAndChunkRegion the region to store metadata about the files
*/
public FileSystem(Map fileAndChunkRegion, FileSystemStats stats) {
this.fileAndChunkRegion = fileAndChunkRegion;
this.stats = stats;
}
public Collection<String> listFileNames() {
return (Collection<String>) fileAndChunkRegion.keySet().stream()
.filter(entry -> ((entry instanceof String) && !((String) entry)
.equalsIgnoreCase(IndexRepositoryFactory.APACHE_GEODE_INDEX_COMPLETE)))
.collect(Collectors.toList());
}
public File createFile(final String name) throws IOException {
// TODO lock region ?
final File file = new File(this, name);
if (null != fileAndChunkRegion.putIfAbsent(name, file)) {
throw new IOException("File exists.");
}
stats.incFileCreates(1);
// TODO unlock region ?
return file;
}
public File putIfAbsentFile(String name, File file) throws IOException {
// TODO lock region ?
if (null != fileAndChunkRegion.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 = (File) fileAndChunkRegion.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 = (File) fileAndChunkRegion.remove(name);
if (file == null) {
throw new FileNotFoundException(name);
}
if (file.possiblyRenamed == false) {
// TODO consider removeAll with all ChunkKeys listed.
final ChunkKey key = new ChunkKey(file.id, 0);
while (true) {
// TODO consider mutable ChunkKey
if (null == fileAndChunkRegion.remove(key)) {
// no more chunks
break;
}
key.chunkId++;
}
}
stats.incFileDeletes(1);
}
public void renameFile(String source, String dest) throws IOException {
final File sourceFile = (File) fileAndChunkRegion.get(source);
if (null == sourceFile) {
throw new FileNotFoundException(source);
}
final File destFile = new File(this, dest);
destFile.chunks = sourceFile.chunks;
destFile.created = sourceFile.created;
destFile.length = sourceFile.length;
destFile.modified = sourceFile.modified;
destFile.id = sourceFile.id;
sourceFile.possiblyRenamed = true;
// 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
updateFile(sourceFile);
putIfAbsentFile(dest, destFile);
fileAndChunkRegion.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 (fileAndChunkRegion.containsKey(key)) {
fileAndChunkRegion.remove(key);
key.chunkId++;
}
return null;
}
final byte[] chunk = (byte[]) fileAndChunkRegion.get(key);
if (chunk != null) {
stats.incReadBytes(chunk.length);
} else {
logger.debug("Chunk was null for file:" + file.getName() + " file id: " + key.getFileId()
+ " chunkKey:" + key.chunkId);
}
return chunk;
}
public void putChunk(final File file, final int id, final byte[] chunk) {
final ChunkKey key = new ChunkKey(file.id, id);
fileAndChunkRegion.put(key, chunk);
stats.incWrittenBytes(chunk.length);
}
void updateFile(File file) {
fileAndChunkRegion.put(file.getName(), file);
}
public Map getFileAndChunkRegion() {
return fileAndChunkRegion;
}
/**
* 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
}
});
}
}