/*******************************************************************************
* Copyright (c) 2013 Imperial College London.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Raul Castro Fernandez - initial design and implementation
******************************************************************************/
package uk.ac.imperial.lsds.seep.api.largestateimpls;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.ac.imperial.lsds.seep.state.EmptyStateException;
import uk.ac.imperial.lsds.seep.state.LargeState;
import uk.ac.imperial.lsds.seep.state.MalformedStateChunk;
import uk.ac.imperial.lsds.seep.state.NullChunkWhileMerging;
import uk.ac.imperial.lsds.seep.state.Streamable;
import uk.ac.imperial.lsds.seep.state.Versionable;
import uk.ac.imperial.lsds.seep.state.annotations.GlobalStateAccess;
import uk.ac.imperial.lsds.seep.state.annotations.OperatorState;
import uk.ac.imperial.lsds.seep.state.annotations.PartitionStateAccess;
import uk.ac.imperial.lsds.seep.state.annotations.PartitioningKey;
import uk.ac.imperial.lsds.seep.state.annotations.ReadAccess;
import uk.ac.imperial.lsds.seep.state.annotations.WriteAccess;
/**
* SeepMap is an implementation of a standard java HashMap. It supports multi-versioning to enable lock-free operations and it
* implements Streamable so that the system can handle it even when it grow large.
* @author raulcf
*
* @param <K>
* @param <V>
*/
@OperatorState(partitionable=true)
public class SeepMap<K, V> extends HashMap<Object, Object> implements Versionable, Streamable, LargeState{
final Logger LOG = LoggerFactory.getLogger(SeepMap.class);
private static final long serialVersionUID = 1L;
// Keep updates and deletes in different structures.
private HashMap<Object, Object> dirtyUpdates = new HashMap<Object, Object>();
private HashMap<Object, Object> dirtyRemoves = new HashMap<Object, Object>();
private boolean clearInVersion = false;
// Flag to indicate the structure is currently on snapshot mode
private AtomicBoolean snapshotMode = new AtomicBoolean();
// Mutex lock to do while reconciliating
private Semaphore mutex = new Semaphore(1);
// For internal use only
private Iterator<Object> iterator = null;
public SeepMap(){
super();
}
public SeepMap(int initialSize) {
super(initialSize);
}
@GlobalStateAccess
@WriteAccess
public void clear(){
this.lock();
if(snapshotMode.get()){
// Just reset all dirty structures and flag it so that snapshot is cleared out when reconciling
dirtyUpdates.clear();
dirtyRemoves.clear();
clearInVersion = true;
}
// Fallback to snapshot
super.clear();
this.release();
}
@PartitionStateAccess(partitioningKeyPositionInArguments=0)
@ReadAccess
public boolean containsKey(@PartitioningKey Object key){
this.lock();
if(snapshotMode.get()){
// If dirtyUpdates has the key, the it is true
if(dirtyUpdates.containsKey(key)){
this.release();
return true;
}
// If it does not have the key, and this is in removes, then it is false
else if(clearInVersion || dirtyRemoves.containsKey(key)){
this.release();
return false;
}
}
// Fallback to snapshot
boolean containsKey = super.containsKey(key);
this.release();
return containsKey;
}
@GlobalStateAccess
@ReadAccess
public boolean containsValue(Object value){
this.lock();
if(snapshotMode.get()){
// Check if dirtyUpdates has the value
if(dirtyUpdates.containsValue(value)){
this.release();
return true;
}
// if the value has been removed, then the answer is no
else if(clearInVersion || dirtyRemoves.containsValue(value)){
this.release();
return false;
}
}
// Otherwise, we fall back to the snapshot, in a read-only operation
boolean containsValue = super.containsValue(value);
this.release();
return containsValue;
}
@GlobalStateAccess
@ReadAccess
public boolean isEmpty(){
this.lock();
boolean isEmpty;
// Empty only if there are no updates in dirty state AND the snapshot was asked to be cleared
if(snapshotMode.get()){
isEmpty = (dirtyUpdates.isEmpty() && clearInVersion) ? true : false;
this.release();
return isEmpty;
}
isEmpty = super.isEmpty();
this.release();
return isEmpty;
}
@PartitionStateAccess(partitioningKeyPositionInArguments=0)
@WriteAccess
public Object remove(@PartitioningKey Object key){
this.lock();
Object oldValue;
if(snapshotMode.get()){
// Remove entry from dirtyUpdates
oldValue = dirtyUpdates.remove(key);
// Register the deletion in dirtyRemoves. Attach key and value
dirtyRemoves.put(key, oldValue);
this.release();
return oldValue;
}
oldValue = super.remove(key);
this.release();
return oldValue;
}
@GlobalStateAccess
@ReadAccess
public Set<Map.Entry<Object,Object>> entrySet(){
this.lock();
if(snapshotMode.get()){
LOG.warn("NOT IMPLEMENTED");
System.exit(0);
}
Set<Map.Entry<Object,Object>> toReturn = super.entrySet();
this.release();
return toReturn;
}
@GlobalStateAccess
@ReadAccess
public Set<Object> keySet(){
this.lock();
if(snapshotMode.get()){
LOG.warn("NOT IMPLEMENTED");
System.exit(0);
}
Set<Object> toReturn = super.keySet();
this.release();
return toReturn;
}
@GlobalStateAccess
@ReadAccess
public Collection<Object> values(){
this.lock();
if(snapshotMode.get()){
LOG.warn("NOT IMPLEMENTED");
System.exit(0);
}
Collection<Object> toReturn = super.values();
this.release();
return toReturn;
}
@GlobalStateAccess
@ReadAccess
public int size(){
return super.size();
}
@PartitionStateAccess(partitioningKeyPositionInArguments=0)
@ReadAccess
public Object get(@PartitioningKey Object key){
this.lock();
if(snapshotMode.get()){
// Return from recent updates
if(dirtyUpdates.containsKey(key)){
Object toReturn = dirtyUpdates.get(key);
this.release();
return toReturn;
}
// It's been specifically removed
else if(clearInVersion || dirtyRemoves.containsKey(key)){
this.release();
return null;
}
}
Object toReturn = super.get(key);
this.release();
return toReturn;
}
@PartitionStateAccess(partitioningKeyPositionInArguments=0)
@WriteAccess
public Object put(@PartitioningKey Object key, Object value){
this.lock();
if(snapshotMode.get()){
// Update value
Object toReturn = dirtyUpdates.put(key, value);
// Remove from dirtyRemoves to avoid inconsistencies when reconciling
dirtyRemoves.remove(key);
this.release();
return toReturn;
}
// or fall back to the snapshot
Object toReturn = super.put(key, value);
this.release();
return toReturn;
}
/** Implement Streamable interface **/
@Override
public int getSize() {
LOG.warn("CALLING getSize of SeepMap, this method is not implemented...");
return this.size();
}
/** Return the total number of chunks necessary to stream this snapshot given a @param chunkSize
* @throws EmptyStateException **/
@Override
public int getTotalNumberOfChunks(int chunkSize) throws EmptyStateException {
if(chunkSize == 0){
throw new IllegalArgumentException("chunkSize must be > 0");
}
if(this.size() == 0){
throw new EmptyStateException("State size is 0, this method should not be called in that state");
}
double chunks = (double)this.size()/chunkSize;
System.out.println("$$$$ CHUNKS: "+chunks);
int totalChunks = (int) Math.ceil((double)this.size()/chunkSize);
System.out.println("MAP SIZE: "+this.size()+" so total chunks: "+totalChunks);
return totalChunks;
}
@Override
public Iterator<?> getIterator() {
iterator = super.keySet().iterator();
return iterator;
}
@Override
public ArrayList<Object> streamSplitState(int chunkSize) {
ArrayList<Object> chunk = new ArrayList<Object>();
int sizeCounter = 0;
while(iterator.hasNext()){
String key = (String) iterator.next();
chunk.add(key);
chunk.add(this.getFromBackup(key));
sizeCounter++; // new unit added
if(sizeCounter >= chunkSize){
return chunk;
}
}
return null;
}
@Override
public void reset(){
this.clear();
dirtyUpdates.clear();
dirtyRemoves.clear();
clearInVersion = false;
}
@Override
public synchronized void appendChunk(ArrayList<Object> chunk) throws NullChunkWhileMerging, MalformedStateChunk {
if(chunk == null){
throw new NullChunkWhileMerging("Received a null chunk");
}
int chunkSize = chunk.size();
if(chunkSize % 2 != 0 || chunkSize == 0){
throw new MalformedStateChunk("Does not contain an even number of object or size is 0. Size->"+chunkSize);
}
System.out.println("Appending: "+chunk.size());
for(int i = 0; i < chunk.size(); i++){
Object key = chunk.get(i);
i++;
Object value = chunk.get(i);
synchronized(this){
this.put(key, value);
}
}
}
@Override
public Object getFromBackup(Object key){
return super.get(key);
}
/**
* Methods implementing the Versionable interface
*/
/** Flag this structure as Snapshot, so that new updates and reads happen in a new version **/
@Override
public void setSnapshotMode(boolean newValue){
this.snapshotMode.set(newValue);
}
/** Reconcile changes kept in version with the original snapshot. **/
@Override
public synchronized void reconcile(){
System.out.println("OR: "+super.size()+" DIRTY: "+dirtyUpdates.size());
this.lock();
// Either we need to delete everything and only add what is stored in dirtyUpdates
if(clearInVersion){
super.clear();
}
// Or we just need to remove certain entries
else{
for(Map.Entry<Object, Object> entry : dirtyRemoves.entrySet()){
super.remove(entry.getKey());
}
}
// In any case, we then add whatever is stored in dirtyUpdates
for(Map.Entry<Object, Object> entry : dirtyUpdates.entrySet()){
super.put(entry.getKey(), entry.getValue());
}
//We reset the structures used during the versioning
clearInVersion = false;
dirtyRemoves.clear();
dirtyUpdates.clear();
// We get out of snapshotMode
snapshotMode.set(false);
this.release();
}
/** Request mutual exclusion access to the structure **/
@Override
public void lock(){
try {
this.mutex.acquire();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
/** Release mutual exclusion access to the structure **/
@Override
public void release(){
this.mutex.release();
}
@Override
public Object getVersionableAndStreamableState() {
return this;
}
}