Madan Jampani

WIP: Revamped transaction API. Introduces a transaction context for running bloc…

…ks of code that can be committed
atomically.

Change-Id: I6ba21050a2644a42f3c073fa04ff776ef2c5ff4c
......@@ -17,7 +17,6 @@
package org.onosproject.store.service;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.Map.Entry;
......@@ -37,13 +36,6 @@ import java.util.Map.Entry;
* concurrency by allowing conditional updates that take into consideration
* the version or value that was previously read.
* </p><p>
* The map also supports atomic batch updates (transactions). One can provide a list
* of updates to be applied atomically if and only if all the operations are guaranteed
* to succeed i.e. all their preconditions are met. For example, the precondition
* for a putIfAbsent API call is absence of a mapping for the key. Similarly, the
* precondition for a conditional replace operation is the presence of an expected
* version or value
* </p><p>
* This map does not allow null values. All methods can throw a ConsistentMapException
* (which extends RuntimeException) to indicate failures.
*
......@@ -202,15 +194,4 @@ public interface ConsistentMap<K, V> {
* @return true if the value was replaced
*/
boolean replace(K key, long oldVersion, V newValue);
/**
* Atomically apply the specified list of updates to the map.
* If any of the updates cannot be applied due to a precondition
* violation, none of the updates will be applied and the state of
* the map remains unaltered.
*
* @param updates list of updates to apply atomically.
* @return true if the map was updated.
*/
boolean batchUpdate(List<UpdateOperation<K, V>> updates);
}
......
......@@ -39,5 +39,9 @@ public interface StorageService {
*/
<K, V> ConsistentMap<K , V> createConsistentMap(String name, Serializer serializer);
// TODO: add API for creating Eventually Consistent Map.
/**
* Creates a new transaction context.
* @return transaction context
*/
TransactionContext createTransactionContext();
}
\ No newline at end of file
......
/*
* Copyright 2015 Open Networking Laboratory
*
* 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 org.onosproject.store.service;
/**
* Provides a context for transactional operations.
* <p>
* A transaction context provides a boundary within which transactions
* are run. It also is a place where all modifications made within a transaction
* are cached until the point when the transaction commits or aborts. It thus ensures
* isolation of work happening with in the transaction boundary.
* <p>
* A transaction context is a vehicle for grouping operations into a unit with the
* properties of atomicity, isolation, and durability. Transactions also provide the
* ability to maintain an application's invariants or integrity constraints,
* supporting the property of consistency. Together these properties are known as ACID.
*/
public interface TransactionContext {
/**
* Returns if this transaction context is open.
* @return true if open, false otherwise.
*/
boolean isOpen();
/**
* Starts a new transaction.
*/
void begin();
/**
* Commits a transaction that was previously started thereby making its changes permanent
* and externally visible.
* @throws TransactionException if transaction fails to commit.
*/
void commit();
/**
* Rolls back the current transaction, discarding all its changes.
*/
void rollback();
/**
* Creates a new transactional map.
* @param mapName name of the transactional map.
* @param serializer serializer to use for encoding/decoding keys and vaulues.
* @return new Transactional Map.
*/
<K, V> TransactionalMap<K, V> createTransactionalMap(String mapName, Serializer serializer);
}
\ No newline at end of file
/*
* Copyright 2015 Open Networking Laboratory
*
* 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 org.onosproject.store.service;
/**
* Top level exception for Transaction failures.
*/
@SuppressWarnings("serial")
public class TransactionException extends RuntimeException {
public TransactionException() {
}
public TransactionException(Throwable t) {
super(t);
}
/**
* Transaction timeout.
*/
public static class Timeout extends TransactionException {
}
/**
* Transaction interrupted.
*/
public static class Interrupted extends TransactionException {
}
/**
* Transaction failure due to optimistic concurrency failure.
*/
public static class OptimisticConcurrencyFailure extends TransactionException {
}
}
\ No newline at end of file
/*
* Copyright 2015 Open Networking Laboratory
*
* 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 org.onosproject.store.service;
import java.util.Collection;
import java.util.Set;
import java.util.Map.Entry;
/**
* Transactional Map data structure.
* <p>
* A TransactionalMap is created by invoking {@link TransactionContext#createTransactionalMap createTransactionalMap}
* method. All operations performed on this map with in a transaction boundary are invisible externally
* until the point when the transaction commits. A commit usually succeeds in the absence of conflicts.
*
* @param <K> type of key.
* @param <V> type of value.
*/
public interface TransactionalMap<K, V> {
/**
* Returns the number of entries in the map.
*
* @return map size.
*/
int size();
/**
* Returns true if the map is empty.
*
* @return true if map has no entries, false otherwise.
*/
boolean isEmpty();
/**
* Returns true if this map contains a mapping for the specified key.
*
* @param key key
* @return true if map contains key, false otherwise.
*/
boolean containsKey(K key);
/**
* Returns true if this map contains the specified value.
*
* @param value value
* @return true if map contains value, false otherwise.
*/
boolean containsValue(V value);
/**
* Returns the value to which the specified key is mapped, or null if this
* map contains no mapping for the key.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped, or null if
* this map contains no mapping for the key
*/
V get(K key);
/**
* Associates the specified value with the specified key in this map (optional operation).
* If the map previously contained a mapping for the key, the old value is replaced by the
* specified value.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with key, or null if there was
* no mapping for key.
*/
V put(K key, V value);
/**
* Removes the mapping for a key from this map if it is present (optional operation).
*
* @param key key whose value is to be removed from the map
* @return the value to which this map previously associated the key,
* or null if the map contained no mapping for the key.
*/
V remove(K key);
/**
* Removes all of the mappings from this map (optional operation).
* The map will be empty after this call returns.
*/
void clear();
/**
* Returns a Set view of the keys contained in this map.
* This method differs from the behavior of java.util.Map.keySet() in that
* what is returned is a unmodifiable snapshot view of the keys in the ConsistentMap.
* Attempts to modify the returned set, whether direct or via its iterator,
* result in an UnsupportedOperationException.
*
* @return a set of the keys contained in this map
*/
Set<K> keySet();
/**
* Returns the collection of values contained in this map.
* This method differs from the behavior of java.util.Map.values() in that
* what is returned is a unmodifiable snapshot view of the values in the ConsistentMap.
* Attempts to modify the returned collection, whether direct or via its iterator,
* result in an UnsupportedOperationException.
*
* @return a collection of the values contained in this map
*/
Collection<V> values();
/**
* Returns the set of entries contained in this map.
* This method differs from the behavior of java.util.Map.entrySet() in that
* what is returned is a unmodifiable snapshot view of the entries in the ConsistentMap.
* Attempts to modify the returned set, whether direct or via its iterator,
* result in an UnsupportedOperationException.
*
* @return set of entries contained in this map.
*/
Set<Entry<K, V>> entrySet();
/**
* If the specified key is not already associated with a value
* associates it with the given value and returns null, else returns the current value.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with the specified key or null
* if key does not already mapped to a value.
*/
V putIfAbsent(K key, V value);
/**
* Removes the entry for the specified key only if it is currently
* mapped to the specified value.
*
* @param key key with which the specified value is associated
* @param value value expected to be associated with the specified key
* @return true if the value was removed
*/
boolean remove(K key, V value);
/**
* Replaces the entry for the specified key only if currently mapped
* to the specified value.
*
* @param key key with which the specified value is associated
* @param oldValue value expected to be associated with the specified key
* @param newValue value to be associated with the specified key
* @return true if the value was replaced
*/
boolean replace(K key, V oldValue, V newValue);
}
\ No newline at end of file
......@@ -20,7 +20,6 @@ import static com.google.common.base.Preconditions.*;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
......@@ -35,7 +34,6 @@ import org.onlab.util.HexString;
import org.onosproject.store.service.ConsistentMap;
import org.onosproject.store.service.ConsistentMapException;
import org.onosproject.store.service.Serializer;
import org.onosproject.store.service.UpdateOperation;
import org.onosproject.store.service.Versioned;
import com.google.common.cache.CacheBuilder;
......@@ -73,7 +71,7 @@ public class ConsistentMapImpl<K, V> implements ConsistentMap<K, V> {
return serializer.decode(HexString.fromHexString(key));
}
ConsistentMapImpl(String name,
public ConsistentMapImpl(String name,
DatabaseProxy<String, byte[]> proxy,
Serializer serializer) {
this.name = checkNotNull(name, "map name cannot be null");
......@@ -196,15 +194,6 @@ public class ConsistentMapImpl<K, V> implements ConsistentMap<K, V> {
return complete(proxy.replace(name, keyCache.getUnchecked(key), oldVersion, serializer.encode(newValue)));
}
@Override
public boolean batchUpdate(List<UpdateOperation<K, V>> updates) {
checkNotNull(updates, "updates cannot be null");
return complete(proxy.atomicBatchUpdate(updates
.stream()
.map(this::toRawUpdateOperation)
.collect(Collectors.toList())));
}
private static <T> T complete(CompletableFuture<T> future) {
try {
return future.get(OPERATION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
......@@ -225,31 +214,4 @@ public class ConsistentMapImpl<K, V> implements ConsistentMap<K, V> {
serializer.decode(e.getValue().value()),
e.getValue().version()));
}
private UpdateOperation<String, byte[]> toRawUpdateOperation(UpdateOperation<K, V> update) {
checkArgument(name.equals(update.tableName()), "Unexpected table name");
UpdateOperation.Builder<String, byte[]> rawUpdate = UpdateOperation.<String, byte[]>newBuilder();
rawUpdate = rawUpdate.withKey(keyCache.getUnchecked(update.key()))
.withCurrentVersion(update.currentVersion())
.withType(update.type());
rawUpdate = rawUpdate.withTableName(update.tableName());
if (update.value() != null) {
rawUpdate = rawUpdate.withValue(serializer.encode(update.value()));
} else {
checkState(update.type() == UpdateOperation.Type.REMOVE
|| update.type() == UpdateOperation.Type.REMOVE_IF_VERSION_MATCH,
ERROR_NULL_VALUE);
}
if (update.currentValue() != null) {
rawUpdate = rawUpdate.withCurrentValue(serializer.encode(update.currentValue()));
}
return rawUpdate.build();
}
}
\ No newline at end of file
......
......@@ -41,6 +41,7 @@ import org.onosproject.cluster.DefaultControllerNode;
import org.onosproject.store.service.ConsistentMap;
import org.onosproject.store.service.Serializer;
import org.onosproject.store.service.StorageService;
import org.onosproject.store.service.TransactionContext;
import org.slf4j.Logger;
import com.google.common.collect.Sets;
......@@ -154,4 +155,9 @@ public class DatabaseManager implements StorageService {
public <K, V> ConsistentMap<K , V> createConsistentMap(String name, Serializer serializer) {
return new ConsistentMapImpl<K, V>(name, partitionedDatabase, serializer);
}
@Override
public TransactionContext createTransactionContext() {
return new DefaultTransactionContext(partitionedDatabase);
}
}
\ No newline at end of file
......
/*
* Copyright 2015 Open Networking Laboratory
*
* 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 org.onosproject.store.consistent.impl;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static com.google.common.base.Preconditions.*;
import org.onosproject.store.service.ConsistentMap;
import org.onosproject.store.service.Serializer;
import org.onosproject.store.service.TransactionContext;
import org.onosproject.store.service.TransactionException;
import org.onosproject.store.service.TransactionalMap;
import org.onosproject.store.service.UpdateOperation;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
/**
* Default TransactionContext implementation.
*/
public class DefaultTransactionContext implements TransactionContext {
private final Map<String, DefaultTransactionalMap> txMaps = Maps.newHashMap();
private boolean isOpen = false;
DatabaseProxy<String, byte[]> databaseProxy;
private static final String TX_NOT_OPEN_ERROR = "Transaction is not open";
private static final int TRANSACTION_TIMEOUT_MILLIS = 2000;
DefaultTransactionContext(DatabaseProxy<String, byte[]> proxy) {
this.databaseProxy = proxy;
}
@Override
public void begin() {
isOpen = true;
}
@Override
public <K, V> TransactionalMap<K, V> createTransactionalMap(String mapName,
Serializer serializer) {
checkNotNull(mapName, "map name is null");
checkNotNull(serializer, "serializer is null");
checkState(isOpen, TX_NOT_OPEN_ERROR);
if (!txMaps.containsKey(mapName)) {
ConsistentMap<K, V> backingMap = new ConsistentMapImpl<>(mapName, databaseProxy, serializer);
DefaultTransactionalMap<K, V> txMap = new DefaultTransactionalMap<>(mapName, backingMap, this, serializer);
txMaps.put(mapName, txMap);
}
return txMaps.get(mapName);
}
@Override
public void commit() {
checkState(isOpen, TX_NOT_OPEN_ERROR);
List<UpdateOperation<String, byte[]>> allUpdates =
Lists.newLinkedList();
try {
txMaps.values()
.stream()
.forEach(m -> {
allUpdates.addAll(m.prepareDatabaseUpdates());
});
if (!complete(databaseProxy.atomicBatchUpdate(allUpdates))) {
throw new TransactionException.OptimisticConcurrencyFailure();
}
} finally {
isOpen = false;
}
}
@Override
public void rollback() {
checkState(isOpen, TX_NOT_OPEN_ERROR);
txMaps.values()
.stream()
.forEach(m -> m.rollback());
}
@Override
public boolean isOpen() {
return false;
}
private static <T> T complete(CompletableFuture<T> future) {
try {
return future.get(TRANSACTION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new TransactionException.Interrupted();
} catch (TimeoutException e) {
throw new TransactionException.Timeout();
} catch (ExecutionException e) {
throw new TransactionException(e.getCause());
}
}
}
/*
* Copyright 2015 Open Networking Laboratory
*
* 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 org.onosproject.store.consistent.impl;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.Set;
import org.onlab.util.HexString;
import org.onosproject.store.service.ConsistentMap;
import org.onosproject.store.service.Serializer;
import org.onosproject.store.service.TransactionContext;
import org.onosproject.store.service.TransactionalMap;
import org.onosproject.store.service.UpdateOperation;
import org.onosproject.store.service.Versioned;
import static com.google.common.base.Preconditions.*;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* Default Transactional Map implementation that provides a repeatable reads
* transaction isolation level.
*
* @param <K> key type
* @param <V> value type.
*/
public class DefaultTransactionalMap<K, V> implements TransactionalMap<K, V> {
private final TransactionContext txContext;
private static final String TX_CLOSED_ERROR = "Transaction is closed";
private final ConsistentMap<K, V> backingMap;
private final String name;
private final Serializer serializer;
private final Map<K, Versioned<V>> readCache = Maps.newConcurrentMap();
private final Map<K, V> writeCache = Maps.newConcurrentMap();
private final Set<K> deleteSet = Sets.newConcurrentHashSet();
public DefaultTransactionalMap(
String name,
ConsistentMap<K, V> backingMap,
TransactionContext txContext,
Serializer serializer) {
this.name = name;
this.backingMap = backingMap;
this.txContext = txContext;
this.serializer = serializer;
}
@Override
public V get(K key) {
checkState(txContext.isOpen(), TX_CLOSED_ERROR);
if (deleteSet.contains(key)) {
return null;
} else if (writeCache.containsKey(key)) {
return writeCache.get(key);
} else {
if (!readCache.containsKey(key)) {
readCache.put(key, backingMap.get(key));
}
Versioned<V> v = readCache.get(key);
return v != null ? v.value() : null;
}
}
@Override
public V put(K key, V value) {
checkState(txContext.isOpen(), TX_CLOSED_ERROR);
Versioned<V> original = readCache.get(key);
V recentUpdate = writeCache.put(key, value);
deleteSet.remove(key);
return recentUpdate == null ? (original != null ? original.value() : null) : recentUpdate;
}
@Override
public V remove(K key) {
checkState(txContext.isOpen(), TX_CLOSED_ERROR);
Versioned<V> original = readCache.get(key);
V recentUpdate = writeCache.remove(key);
deleteSet.add(key);
return recentUpdate == null ? (original != null ? original.value() : null) : recentUpdate;
}
@Override
public boolean remove(K key, V value) {
V currentValue = get(key);
if (value.equals(currentValue)) {
remove(key);
return true;
}
return false;
}
@Override
public boolean replace(K key, V oldValue, V newValue) {
V currentValue = get(key);
if (oldValue.equals(currentValue)) {
put(key, newValue);
return true;
}
return false;
}
@Override
public int size() {
// TODO
throw new UnsupportedOperationException();
}
@Override
public boolean isEmpty() {
return size() == 0;
}
@Override
public boolean containsKey(K key) {
return get(key) != null;
}
@Override
public boolean containsValue(V value) {
// TODO
throw new UnsupportedOperationException();
}
@Override
public void clear() {
// TODO
throw new UnsupportedOperationException();
}
@Override
public Set<K> keySet() {
// TODO
throw new UnsupportedOperationException();
}
@Override
public Collection<V> values() {
// TODO
throw new UnsupportedOperationException();
}
@Override
public Set<Entry<K, V>> entrySet() {
// TODO
throw new UnsupportedOperationException();
}
@Override
public V putIfAbsent(K key, V value) {
V currentValue = get(key);
if (currentValue == null) {
put(key, value);
return null;
}
return currentValue;
}
protected List<UpdateOperation<String, byte[]>> prepareDatabaseUpdates() {
List<UpdateOperation<K, V>> updates = Lists.newLinkedList();
deleteSet.forEach(key -> {
Versioned<V> original = readCache.get(key);
if (original != null) {
updates.add(UpdateOperation.<K, V>newBuilder()
.withTableName(name)
.withType(UpdateOperation.Type.REMOVE_IF_VERSION_MATCH)
.withKey(key)
.withCurrentVersion(original.version())
.build());
}
});
writeCache.forEach((key, value) -> {
Versioned<V> original = readCache.get(key);
if (original == null) {
updates.add(UpdateOperation.<K, V>newBuilder()
.withTableName(name)
.withType(UpdateOperation.Type.PUT_IF_ABSENT)
.withKey(key)
.withValue(value)
.build());
} else {
updates.add(UpdateOperation.<K, V>newBuilder()
.withTableName(name)
.withType(UpdateOperation.Type.PUT_IF_VERSION_MATCH)
.withKey(key)
.withCurrentVersion(original.version())
.withValue(value)
.build());
}
});
return updates.stream().map(this::toRawUpdateOperation).collect(Collectors.toList());
}
private UpdateOperation<String, byte[]> toRawUpdateOperation(UpdateOperation<K, V> update) {
UpdateOperation.Builder<String, byte[]> rawUpdate = UpdateOperation.<String, byte[]>newBuilder();
rawUpdate = rawUpdate.withKey(HexString.toHexString(serializer.encode(update.key())))
.withCurrentVersion(update.currentVersion())
.withType(update.type());
rawUpdate = rawUpdate.withTableName(update.tableName());
if (update.value() != null) {
rawUpdate = rawUpdate.withValue(serializer.encode(update.value()));
}
if (update.currentValue() != null) {
rawUpdate = rawUpdate.withCurrentValue(serializer.encode(update.currentValue()));
}
return rawUpdate.build();
}
/**
* Discards all changes made to this transactional map.
*/
protected void rollback() {
readCache.clear();
writeCache.clear();
deleteSet.clear();
}
}
\ No newline at end of file