All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dorkbox.util.storage.DiskStorage Maven / Gradle / Ivy

There is a newer version: 1.48
Show newest version
/*
 * 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