dorkbox.util.storage.DiskStorage Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of Utilities Show documentation
Show all versions of Utilities Show documentation
Utilities for use within Java projects
/*
* Copyright 2014 dorkbox, llc
*
* 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 dorkbox.util.storage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import org.slf4j.Logger;
import dorkbox.util.DelayTimer;
import dorkbox.util.serialization.SerializationManager;
/**
* Nothing spectacular about this storage -- it allows for persistent storage of objects to disk.
*
* Be wary of opening the database file in different JVM instances. Even with file-locks, you can corrupt the data.
*/
@SuppressWarnings({"Convert2Diamond", "Convert2Lambda"})
class DiskStorage implements Storage {
// null if we are a read-only storage
private final DelayTimer timer;
// must be volatile
private volatile HashMap actionMap = new HashMap();
private final Object singleWriterLock = new Object[0];
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
private static final AtomicReferenceFieldUpdater actionMapREF =
AtomicReferenceFieldUpdater.newUpdater(DiskStorage.class, HashMap.class, "actionMap");
private final StorageBase storage;
private final AtomicInteger references = new AtomicInteger(1);
private final AtomicBoolean isOpen = new AtomicBoolean(false);
private final long milliSeconds;
/**
* Creates or opens a new database file.
*/
DiskStorage(File storageFile,
SerializationManager serializationManager,
final boolean readOnly,
final long saveDelayInMilliseconds,
final Logger logger) throws IOException {
this.storage = new StorageBase(storageFile, serializationManager, logger);
this.milliSeconds = saveDelayInMilliseconds;
if (readOnly) {
this.timer = null;
}
else {
this.timer = new DelayTimer("Storage Writer", false, new Runnable() {
@Override
public
void run() {
Map actions;
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention.
synchronized (singleWriterLock) {
// do a fast swap on the actionMap.
actions = DiskStorage.this.actionMap;
DiskStorage.this.actionMap = new HashMap();
}
DiskStorage.this.storage.doActionThings(actions);
}
});
}
this.isOpen.set(true);
}
/**
* Returns the number of objects in the database.
*
* SLOW because this must save all data to disk first!
*/
@Override
public final
int size() {
if (!this.isOpen.get()) {
throw new RuntimeException("Unable to act on closed storage");
}
// flush actions
// timer action runs on THIS thread, not timer thread
if (timer != null) {
this.timer.delay(0L);
}
return this.storage.size();
}
/**
* Checks if there is a object corresponding to the given key.
*/
@Override
public final
boolean contains(StorageKey key) {
if (!this.isOpen.get()) {
throw new RuntimeException("Unable to act on closed storage");
}
// access a snapshot of the actionMap (single-writer-principle)
final HashMap actionMap = actionMapREF.get(this);
// check if our pending actions has it, or if our storage index has it
return actionMap.containsKey(key) || this.storage.contains(key);
}
/**
* Reads a object using the specific key, and casts it to the expected class
*/
@Override
public final
T get(StorageKey key) {
return get0(key);
}
/**
* Returns the saved data (or null) for the specified key. Also saves the data as default data.
*
* @param data If there is no object in the DB with the specified key, this value will be the default (and will be saved to the db)
*
* @return NULL if the saved data was the wrong type for the specified key.
*/
@Override
@SuppressWarnings("unchecked")
public
T get(StorageKey key, T data) {
Object source = get0(key);
if (source == null) {
// returned was null, so we should save the default value
put(key, data);
return data;
}
else {
final Class> expectedClass = data.getClass();
final Class> savedCLass = source.getClass();
if (!expectedClass.isAssignableFrom(savedCLass)) {
String message = "Saved value type '" + savedCLass + "' is different than expected value '" + expectedClass + "'";
if (storage.logger != null) {
storage.logger.error(message);
}
else {
System.err.print(message);
}
return null;
}
}
return (T) source;
}
/**
* Reads a object from pending or from storage
*/
private
T get0(StorageKey key) {
if (!this.isOpen.get()) {
throw new RuntimeException("Unable to act on closed storage");
}
// access a snapshot of the actionMap (single-writer-principle)
final HashMap actionMap = actionMapREF.get(this);
// if the object in is pending, we get it from there
Object object = actionMap.get(key);
if (object != null) {
@SuppressWarnings("unchecked")
T returnObject = (T) object;
return returnObject;
}
// not found, so we have to go find it on disk
return this.storage.get(key);
}
/**
* Saves the given data to storage with the associated key.
*
* Also will update existing data. If the new contents do not fit in the original space, then the update is handled by
* deleting the old data and adding the new.
*/
@Override
public final
void put(StorageKey key, Object object) {
if (!this.isOpen.get()) {
throw new RuntimeException("Unable to act on closed storage");
}
if (timer != null) {
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention.
synchronized (singleWriterLock) {
// push action to map
actionMap.put(key, object);
}
// timer action runs on TIMER thread, not this thread
this.timer.delay(this.milliSeconds);
} else {
throw new RuntimeException("Unable to put on a read-only storage");
}
}
/**
* Deletes an object from storage.
*
* @return true if the delete was successful. False if there were problems deleting the data.
*/
@Override
public final
boolean delete(StorageKey key) {
if (!this.isOpen.get()) {
throw new RuntimeException("Unable to act on closed storage");
}
// timer action runs on THIS thread, not timer thread
if (timer != null) {
// flush to storage, so we know if there were errors deleting from disk
this.timer.delay(0L);
return this.storage.delete(key);
}
else {
throw new RuntimeException("Unable to delete on a read-only storage");
}
}
/**
* Closes and removes this storage from the storage system. This is the same as calling {@link StorageSystem#close(Storage)}
*/
@Override
public
void close() {
StorageSystem.close(this);
}
/**
* Closes the database and file.
*/
void closeFully() {
// timer action runs on THIS thread, not timer thread
if (timer != null) {
this.timer.delay(0L);
}
// have to "close" it after we run the timer!
this.isOpen.set(false);
this.storage.close();
}
/**
* @return the file that backs this storage
*/
@Override
public final
File getFile() {
return this.storage.getFile();
}
/**
* Gets the backing file size.
*
* @return -1 if there was an error
*/
@Override
public final
long getFileSize() {
// timer action runs on THIS thread, not timer thread
if (timer != null) {
this.timer.delay(0L);
}
return this.storage.getFileSize();
}
/**
* @return true if there are objects queued to be written?
*/
@Override
public final
boolean hasWriteWaiting() {
if (!this.isOpen.get()) {
throw new RuntimeException("Unable to act on closed storage");
}
//noinspection SimplifiableIfStatement
if (timer != null) {
return this.timer.isWaiting();
}
else {
return false;
}
}
/**
* @return the delay in milliseconds this will wait after the last action to flush the data to the disk
*/
@Override
public final
long getSaveDelay() {
return this.milliSeconds;
}
/**
* @return the version of data stored in the database
*/
@Override
public final
int getVersion() {
return this.storage.getVersion();
}
/**
* Sets the version of data stored in the database
*/
@Override
public final
void setVersion(int version) {
this.storage.setVersion(version);
}
void increaseReference() {
this.references.incrementAndGet();
}
/**
* return true when this is the last reference
*/
boolean decrementReference() {
return this.references.decrementAndGet() <= 0;
}
@Override
protected
Object clone() throws CloneNotSupportedException {
return super.clone();
}
/**
* Save the storage to disk, immediately.
*
* This will save ALL of the pending save actions to the file
*/
@Override
public final
void save() {
if (!this.isOpen.get()) {
throw new RuntimeException("Unable to act on closed storage");
}
// timer action runs on THIS thread, not timer thread
if (timer != null) {
this.timer.delay(0L);
} else {
throw new RuntimeException("Unable to save on a read-only storage");
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy