Pending Transaction - save in external datasource
Created redis cloud entry point. Pending transactions storages in Redis depending 'redis.enabled' property and Redis availability.
This commit is contained in:
commit
f87a0f5fea
|
@ -8,7 +8,7 @@ subprojects {
|
|||
apply plugin: 'java'
|
||||
|
||||
group = 'org.ethereum'
|
||||
version = '0.8.2-SNAPSHOT'
|
||||
version = '0.8.3-SNAPSHOT'
|
||||
|
||||
compileJava.options.encoding = 'UTF-8'
|
||||
compileJava.options.compilerArgs << '-XDignore.symbol.file'
|
||||
|
|
|
@ -50,6 +50,7 @@ public class SystemProperties {
|
|||
private final static String DEFAULT_VM_TRACE_DIR = "dmp";
|
||||
private final static int DEFAULT_PEER_LISTEN_PORT = 30303;
|
||||
private final static String DEFAULT_KEY_VALUE_DATA_SOURCE = "leveldb";
|
||||
private final static boolean DEFAULT_REDIS_ENABLED = true;
|
||||
|
||||
|
||||
/* Testing */
|
||||
|
@ -239,7 +240,11 @@ public class SystemProperties {
|
|||
}
|
||||
|
||||
public boolean vmTrace() {
|
||||
return Boolean.parseBoolean(prop.getProperty("vm.structured.trace", String.valueOf(DEFAULT_VM_TRACE)));
|
||||
return boolProperty("vm.structured.trace", DEFAULT_VM_TRACE);
|
||||
}
|
||||
|
||||
private boolean boolProperty(String key, Boolean defaultValue) {
|
||||
return Boolean.parseBoolean(prop.getProperty(key, String.valueOf(defaultValue)));
|
||||
}
|
||||
|
||||
public String vmTraceDir() {
|
||||
|
@ -250,10 +255,13 @@ public class SystemProperties {
|
|||
return Integer.parseInt(prop.getProperty("peer.listen.port", String.valueOf(DEFAULT_PEER_LISTEN_PORT)));
|
||||
}
|
||||
|
||||
|
||||
public String getKeyValueDataSource() {
|
||||
return prop.getProperty("keyvalue.datasource", DEFAULT_KEY_VALUE_DATA_SOURCE);
|
||||
}
|
||||
|
||||
public boolean isRedisEnabled() {
|
||||
return boolProperty("redis.enabled", DEFAULT_REDIS_ENABLED);
|
||||
}
|
||||
|
||||
public void setListenPort(Integer port) {
|
||||
prop.setProperty("peer.listen.port", port.toString());
|
||||
|
|
|
@ -16,9 +16,11 @@ import org.slf4j.LoggerFactory;
|
|||
import org.spongycastle.util.encoders.Hex;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
|
@ -72,7 +74,9 @@ public class BlockchainImpl implements Blockchain {
|
|||
// to avoid using minGasPrice=0 from Genesis for the wallet
|
||||
private static final long INITIAL_MIN_GAS_PRICE = 10 * SZABO.longValue();
|
||||
|
||||
private final Set<Transaction> pendingTransactions = Collections.synchronizedSet(new HashSet<Transaction>());
|
||||
@Resource
|
||||
@Qualifier("pendingTransactions")
|
||||
private Set<Transaction> pendingTransactions;
|
||||
|
||||
@Autowired
|
||||
private Repository repository;
|
||||
|
|
|
@ -9,19 +9,19 @@ import java.util.Set;
|
|||
*/
|
||||
public interface KeyValueDataSource {
|
||||
|
||||
public void init();
|
||||
void init();
|
||||
|
||||
public void setName(String name);
|
||||
void setName(String name);
|
||||
|
||||
public byte[] get(byte[] key);
|
||||
byte[] get(byte[] key);
|
||||
|
||||
public void put(byte[] key, byte[] value);
|
||||
byte[] put(byte[] key, byte[] value);
|
||||
|
||||
public void delete(byte[] key);
|
||||
void delete(byte[] key);
|
||||
|
||||
public Set<byte[]> keys();
|
||||
Set<byte[]> keys();
|
||||
|
||||
public void updateBatch(Map<byte[], byte[]> rows);
|
||||
void updateBatch(Map<byte[], byte[]> rows);
|
||||
|
||||
public void close();
|
||||
void close();
|
||||
}
|
||||
|
|
|
@ -88,8 +88,9 @@ public class LevelDbDataSource implements KeyValueDataSource {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void put(byte[] key, byte[] value) {
|
||||
public byte[] put(byte[] key, byte[] value) {
|
||||
db.put(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
package org.ethereum.datasource;
|
||||
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.Pipeline;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.ethereum.config.SystemProperties.CONFIG;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author: Roman Mandeleil
|
||||
* Created on: 18/01/2015 21:48
|
||||
*/
|
||||
|
||||
public class RedisDataSource implements KeyValueDataSource{
|
||||
|
||||
String name;
|
||||
int index;
|
||||
|
||||
Jedis jedis;
|
||||
|
||||
public RedisDataSource() {
|
||||
}
|
||||
|
||||
public RedisDataSource(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
|
||||
if (name == null) throw new NullPointerException("no name set to the db");
|
||||
|
||||
this.jedis = RedisPool.getResource(name);
|
||||
if (jedis == null) this.jedis = new Jedis("localhost");
|
||||
|
||||
if (CONFIG.databaseReset())
|
||||
this.jedis.flushAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
index = nameToIndex(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] get(byte[] key) {
|
||||
return jedis.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(byte[] key, byte[] value) {
|
||||
jedis.set(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(byte[] key) {
|
||||
jedis.del(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<byte[]> keys() {
|
||||
return jedis.keys("*".getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBatch(Map<byte[], byte[]> rows) {
|
||||
Pipeline pipeline = jedis.pipelined();
|
||||
|
||||
Iterator<Map.Entry<byte[], byte[]>> iterator = rows.entrySet().iterator();
|
||||
while(iterator.hasNext()){
|
||||
|
||||
Map.Entry<byte[], byte[]> row = iterator.next();
|
||||
byte[] key = row.getKey();
|
||||
byte[] val = row.getValue();
|
||||
pipeline.set(key, val);
|
||||
}
|
||||
pipeline.sync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
jedis.close();
|
||||
}
|
||||
|
||||
private static Integer nameToIndex(String name) {
|
||||
Integer index = DBNameScheme.get(name);
|
||||
if (index == null) {
|
||||
index = indexCounter.getAndIncrement();
|
||||
DBNameScheme.put(name, index);
|
||||
indexCounter.intValue();
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
private static Map<String, Integer> DBNameScheme = new HashMap<>();
|
||||
private static AtomicInteger indexCounter = new AtomicInteger(1);
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package org.ethereum.datasource;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisPoolConfig;
|
||||
import redis.clients.jedis.Protocol;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author: Roman Mandeleil
|
||||
* Created on: 20/01/2015 13:27
|
||||
*/
|
||||
|
||||
public class RedisPool {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger("db");
|
||||
private static JedisPool state;
|
||||
private static JedisPool details;
|
||||
|
||||
static {
|
||||
|
||||
try {
|
||||
|
||||
if (System.getenv("REDIS_STATE") != null) {
|
||||
URI redisUri = new URI(System.getenv("REDIS_STATE"));
|
||||
logger.info("Init redis pool: " + redisUri.toString());
|
||||
state = new JedisPool(new JedisPoolConfig(),
|
||||
redisUri.getHost(),
|
||||
redisUri.getPort(),
|
||||
Protocol.DEFAULT_TIMEOUT,
|
||||
redisUri.getUserInfo().split(":",2)[1]);
|
||||
}
|
||||
|
||||
if (System.getenv("REDIS_DETAILS") != null) {
|
||||
URI redisUri = new URI(System.getenv("REDIS_DETAILS"));
|
||||
logger.info("Init redis pool: " + redisUri.toString());
|
||||
details = new JedisPool(new JedisPoolConfig(),
|
||||
redisUri.getHost(),
|
||||
redisUri.getPort(),
|
||||
Protocol.DEFAULT_TIMEOUT,
|
||||
redisUri.getUserInfo().split(":",2)[1]);
|
||||
}
|
||||
|
||||
} catch (URISyntaxException e) {
|
||||
logger.info("Pool is not available");
|
||||
}
|
||||
}
|
||||
|
||||
public static Jedis getResource(String name){
|
||||
if (state == null) return null;
|
||||
|
||||
if (name.equals("state")) return state.getResource();
|
||||
if (name.equals("details")) return details.getResource();
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package org.ethereum.datasource.redis;
|
||||
|
||||
import org.ethereum.core.Transaction;
|
||||
import org.ethereum.datasource.KeyValueDataSource;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public interface RedisConnection {
|
||||
|
||||
public static final String REDISCLOUD_URL = "REDISCLOUD_URL";
|
||||
|
||||
boolean isAvailable();
|
||||
|
||||
<T> Set<T> createSetFor(Class<T> clazz, String name);
|
||||
|
||||
<K,V> Map<K, V> createMapFor(Class<K> keyClass, Class<V> valueClass, String name);
|
||||
|
||||
Set<Transaction> createTransactionSet(String name);
|
||||
|
||||
KeyValueDataSource createDataSource(String name);
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package org.ethereum.datasource.redis;
|
||||
|
||||
import org.ethereum.config.SystemProperties;
|
||||
import org.ethereum.core.Transaction;
|
||||
import org.ethereum.datasource.KeyValueDataSource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisPoolConfig;
|
||||
import redis.clients.jedis.Protocol;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.springframework.util.StringUtils.isEmpty;
|
||||
|
||||
@Component
|
||||
public class RedisConnectionImpl implements RedisConnection {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger("db");
|
||||
|
||||
private JedisPool jedisPool;
|
||||
|
||||
@PostConstruct
|
||||
public void tryConnect() {
|
||||
if (!SystemProperties.CONFIG.isRedisEnabled()) return;
|
||||
|
||||
String redisCloudUrl = System.getenv(REDISCLOUD_URL);
|
||||
if (isEmpty(redisCloudUrl)) {
|
||||
logger.info("Cannot connect to Redis. 'REDISCLOUD_URL' environment variable is not defined.");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Redis pool creating: " + redisCloudUrl);
|
||||
try {
|
||||
jedisPool = createJedisPool(new URI(redisCloudUrl));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Cannot connect to Redis cloud: ", e);
|
||||
} finally {
|
||||
logger.info(isAvailable() ? "Redis cloud connected successfully." : "Redis cloud connection failed.");
|
||||
}
|
||||
}
|
||||
|
||||
private static JedisPool createJedisPool(URI redisUri) {
|
||||
String userInfo = redisUri.getUserInfo();
|
||||
if (StringUtils.hasText(userInfo)) {
|
||||
return new JedisPool(new JedisPoolConfig(),
|
||||
redisUri.getHost(),
|
||||
redisUri.getPort(),
|
||||
Protocol.DEFAULT_TIMEOUT,
|
||||
userInfo.split(":", 2)[1]);
|
||||
}
|
||||
|
||||
return new JedisPool(new JedisPoolConfig(),
|
||||
redisUri.getHost(),
|
||||
redisUri.getPort(),
|
||||
Protocol.DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
if (jedisPool != null) {
|
||||
jedisPool.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
boolean available = jedisPool != null;
|
||||
if (available) {
|
||||
try {
|
||||
Jedis jedis = jedisPool.getResource();
|
||||
try {
|
||||
available = jedis.isConnected();
|
||||
} finally {
|
||||
jedisPool.returnResource(jedis);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.warn("Connection testing failed: ", t);
|
||||
available = false;
|
||||
}
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Set<T> createSetFor(Class<T> clazz, String name) {
|
||||
return new RedisSet<T>(name, jedisPool, Serializers.forClass(clazz));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <K,V> Map<K, V> createMapFor(Class<K> keyClass, Class<V> valueClass, String name) {
|
||||
return new RedisMap<K, V>(name, jedisPool, Serializers.forClass(keyClass), Serializers.forClass(valueClass));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Transaction> createTransactionSet(String name) {
|
||||
return createSetFor(Transaction.class, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyValueDataSource createDataSource(String name) {
|
||||
return new RedisDataSource(name, jedisPool);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package org.ethereum.datasource.redis;
|
||||
|
||||
import org.ethereum.datasource.KeyValueDataSource;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.ethereum.config.SystemProperties.CONFIG;
|
||||
import static org.ethereum.util.Functional.Consumer;
|
||||
|
||||
public class RedisDataSource extends RedisMap<byte[], byte[]> implements KeyValueDataSource {
|
||||
|
||||
RedisDataSource(String namespace, JedisPool pool) {
|
||||
super(namespace, pool, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] serializeKey(byte[] key) {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] deserializeKey(byte[] bytes) {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] serialize(byte[] value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] deserialize(byte[] bytes) {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
if (CONFIG.databaseReset()) {
|
||||
pooled(new Consumer<Jedis>() {
|
||||
@Override
|
||||
public void accept(Jedis jedis) {
|
||||
jedis.flushAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setName(String name) {
|
||||
super.setName(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] get(byte[] key) {
|
||||
return super.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(final byte[] key) {
|
||||
remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<byte[]> keys() {
|
||||
return super.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBatch(final Map<byte[], byte[]> rows) {
|
||||
putAll(rows);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
package org.ethereum.datasource.redis;
|
||||
|
||||
import org.apache.commons.collections4.Transformer;
|
||||
import org.apache.commons.collections4.keyvalue.AbstractMapEntry;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.apache.commons.collections4.CollectionUtils.collect;
|
||||
import static org.ethereum.util.Functional.Consumer;
|
||||
import static org.ethereum.util.Functional.Function;
|
||||
|
||||
public class RedisMap<K, V> extends RedisStorage<V> implements Map<K, V> {
|
||||
|
||||
private final RedisSerializer<K> keySerializer;
|
||||
|
||||
public RedisMap(String namespace, JedisPool pool, RedisSerializer<K> keySerializer, RedisSerializer<V> valueSerializer) {
|
||||
super(namespace, pool, valueSerializer);
|
||||
this.keySerializer = keySerializer;
|
||||
}
|
||||
|
||||
protected byte[] serializeKey(K key) {
|
||||
return keySerializer.serialize(key);
|
||||
}
|
||||
|
||||
protected K deserializeKey(byte[] input) {
|
||||
return keySerializer.deserialize(input);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return pooledWithResult(new Function<Jedis, Integer>() {
|
||||
@Override
|
||||
public Integer apply(Jedis jedis) {
|
||||
return jedis.hlen(getName()).intValue();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return size() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(final Object key) {
|
||||
return pooledWithResult(new Function<Jedis, Boolean>() {
|
||||
@Override
|
||||
public Boolean apply(Jedis jedis) {
|
||||
return jedis.hexists(getName(), serializeKey((K) key));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object value) {
|
||||
return values().contains(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V get(final Object key) {
|
||||
return pooledWithResult(new Function<Jedis, V>() {
|
||||
@Override
|
||||
public V apply(Jedis jedis) {
|
||||
byte[] value = jedis.hget(getName(), serializeKey((K) key));
|
||||
return deserialize(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public V put(final K key, final V value) {
|
||||
return pooledWithResult(new Function<Jedis, V>() {
|
||||
@Override
|
||||
public V apply(Jedis jedis) {
|
||||
byte[] serializedKey = serializeKey(key);
|
||||
byte[] oldValue = jedis.hget(getName(), serializedKey);
|
||||
jedis.hset(getName(), serializedKey, serialize(value));
|
||||
return deserialize(oldValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public V remove(final Object key) {
|
||||
return pooledWithResult(new Function<Jedis, V>() {
|
||||
@Override
|
||||
public V apply(Jedis jedis) {
|
||||
byte[] serializedKey = serializeKey((K) key);
|
||||
byte[] oldValue = jedis.hget(getName(), serializedKey);
|
||||
jedis.hdel(getName(), serializedKey);
|
||||
return deserialize(oldValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(final Map<? extends K, ? extends V> m) {
|
||||
pooled(new Consumer<Jedis>() {
|
||||
@Override
|
||||
public void accept(Jedis jedis) {
|
||||
Map<byte[], byte[]> map = new HashMap<byte[], byte[]>();
|
||||
for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
|
||||
map.put(serializeKey(entry.getKey()), serialize(entry.getValue()));
|
||||
}
|
||||
jedis.hmset(getName(), map);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
pooled(new Consumer<Jedis>() {
|
||||
@Override
|
||||
public void accept(Jedis jedis) {
|
||||
jedis.del(getName());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<K> keySet() {
|
||||
return pooledWithResult(new Function<Jedis, Set<K>>() {
|
||||
@Override
|
||||
public Set<K> apply(Jedis jedis) {
|
||||
Set<K> result = new HashSet<K>();
|
||||
collect(jedis.hkeys(getName()), new Transformer<byte[], K>() {
|
||||
@Override
|
||||
public K transform(byte[] input) {
|
||||
return deserializeKey(input);
|
||||
}
|
||||
}, result);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<V> values() {
|
||||
return pooledWithResult(new Function<Jedis, Collection<V>>() {
|
||||
@Override
|
||||
public Collection<V> apply(Jedis jedis) {
|
||||
return deserialize(jedis.hvals(getName()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<K, V>> entrySet() {
|
||||
return pooledWithResult(new Function<Jedis, Set<Entry<K, V>>>() {
|
||||
@Override
|
||||
public Set<Entry<K, V>> apply(Jedis jedis) {
|
||||
Set<Entry<K, V>> result = new HashSet<Entry<K, V>>();
|
||||
collect(jedis.hgetAll(getName()).entrySet(), new Transformer<Entry<byte[], byte[]>, Entry<K, V>>() {
|
||||
@Override
|
||||
public Entry<K, V> transform(Entry<byte[], byte[]> input) {
|
||||
K key = deserializeKey(input.getKey());
|
||||
V value = deserialize(input.getValue());
|
||||
return new RedisMapEntry<K, V>(key, value);
|
||||
}
|
||||
}, result);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class RedisMapEntry<K, V> extends AbstractMapEntry<K, V> {
|
||||
|
||||
RedisMapEntry(K key, V value) {
|
||||
super(key, value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.ethereum.datasource.redis;
|
||||
|
||||
public interface RedisSerializer<T> {
|
||||
|
||||
boolean canSerialize(Object o);
|
||||
|
||||
byte[] serialize(T t);
|
||||
|
||||
T deserialize(byte[] bytes);
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
package org.ethereum.datasource.redis;
|
||||
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.ethereum.util.Functional.Consumer;
|
||||
import static org.ethereum.util.Functional.Function;
|
||||
|
||||
public class RedisSet<T> extends RedisStorage<T> implements Set<T> {
|
||||
|
||||
RedisSet(String namespace, JedisPool pool, RedisSerializer<T> serializer) {
|
||||
super(namespace, pool, serializer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return pooledWithResult(new Function<Jedis, Integer>() {
|
||||
@Override
|
||||
public Integer apply(Jedis jedis) {
|
||||
return jedis.scard(getName()).intValue();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return size() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(final Object o) {
|
||||
return pooledWithResult(new Function<Jedis, Boolean>() {
|
||||
@Override
|
||||
public Boolean apply(Jedis jedis) {
|
||||
return jedis.sismember(getName(), serialize((T) o));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return asCollection().iterator();
|
||||
}
|
||||
|
||||
private Collection<T> asCollection() {
|
||||
Set<byte[]> members = pooledWithResult(new Function<Jedis, Set<byte[]>>() {
|
||||
@Override
|
||||
public Set<byte[]> apply(Jedis jedis) {
|
||||
return jedis.smembers(getName());
|
||||
}
|
||||
});
|
||||
return deserialize(members);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] toArray() {
|
||||
return asCollection().toArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T1> T1[] toArray(T1[] a) {
|
||||
return asCollection().toArray(a);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(T t) {
|
||||
return addAll(Arrays.asList(t));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object o) {
|
||||
return removeAll(Arrays.asList(o));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAll(Collection<?> c) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(final Collection<? extends T> c) {
|
||||
return pooledWithResult(new Function<Jedis, Boolean>() {
|
||||
@Override
|
||||
public Boolean apply(Jedis jedis) {
|
||||
return jedis.sadd(getName(), serialize(c)) == c.size();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retainAll(final Collection<?> c) {
|
||||
return pooledWithResult(new Function<Jedis, Boolean>() {
|
||||
@Override
|
||||
public Boolean apply(Jedis jedis) {
|
||||
byte[] tempName = temporaryName();
|
||||
try {
|
||||
jedis.sadd(tempName, serialize(c));
|
||||
return jedis.scard(getName()) != jedis.sinterstore(getName(), getName(), tempName);
|
||||
} finally {
|
||||
jedis.del(tempName);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeAll(final Collection<?> c) {
|
||||
return pooledWithResult(new Function<Jedis, Boolean>() {
|
||||
@Override
|
||||
public Boolean apply(Jedis jedis) {
|
||||
return jedis.srem(getName(), serialize(c)) == c.size();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
pooled(new Consumer<Jedis>() {
|
||||
@Override
|
||||
public void accept(Jedis jedis) {
|
||||
jedis.del(getName());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package org.ethereum.datasource.redis;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
import org.apache.commons.collections4.Transformer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.Pipeline;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.apache.commons.collections4.CollectionUtils.collect;
|
||||
import static org.ethereum.util.Functional.*;
|
||||
|
||||
public abstract class RedisStorage<T> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger("db");
|
||||
|
||||
private byte[] name;
|
||||
private final JedisPool pool;
|
||||
private final RedisSerializer<T> serializer;
|
||||
|
||||
RedisStorage(String name, JedisPool pool, RedisSerializer<T> serializer) {
|
||||
this.name = name.getBytes();
|
||||
this.pool = pool;
|
||||
this.serializer = serializer;
|
||||
}
|
||||
|
||||
protected byte[] getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
protected void setName(String name) {
|
||||
this.name = name.getBytes();
|
||||
}
|
||||
|
||||
protected byte[] formatName(String suffix) {
|
||||
return Bytes.concat(getName(), suffix.getBytes());
|
||||
}
|
||||
|
||||
protected byte[] temporaryName() {
|
||||
return formatName(":" + Thread.currentThread().getName() + ":" + System.currentTimeMillis());
|
||||
}
|
||||
|
||||
protected byte[] serialize(T o) {
|
||||
return serializer.serialize(o);
|
||||
}
|
||||
|
||||
protected byte[][] serialize(Collection<?> collection) {
|
||||
return collect(collection, new Transformer<Object, byte[]>() {
|
||||
@Override
|
||||
public byte[] transform(Object input) {
|
||||
return serialize((T) input);
|
||||
}
|
||||
}).toArray(new byte[][]{});
|
||||
}
|
||||
|
||||
protected T deserialize(byte[] bytes) {
|
||||
return serializer.deserialize(bytes);
|
||||
}
|
||||
|
||||
|
||||
protected Collection<T> deserialize(Collection<byte[]> bytesCollection) {
|
||||
return collect(bytesCollection, new Transformer<byte[], T>() {
|
||||
@Override
|
||||
public T transform(byte[] input) {
|
||||
return deserialize(input);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected <P> void doInPipeline(final Collection<P> collection, final BiConsumer<P, Pipeline> consumer) {
|
||||
pooled(new Consumer<Jedis>() {
|
||||
@Override
|
||||
public void accept(Jedis jedis) {
|
||||
Pipeline pipeline = jedis.pipelined();
|
||||
try {
|
||||
for (P el : collection) {
|
||||
consumer.accept(el, pipeline);
|
||||
}
|
||||
} finally {
|
||||
pipeline.sync();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void pooled(final Consumer<Jedis> consumer) {
|
||||
pooledWithResult(new Function<Jedis, Object>() {
|
||||
@Override
|
||||
public Object apply(Jedis jedis) {
|
||||
consumer.accept(jedis);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected <R> R pooledWithResult(Function<Jedis, R> function) {
|
||||
Jedis jedis = pool.getResource();
|
||||
Exception operationException = null;
|
||||
try {
|
||||
return function.apply(jedis);
|
||||
} catch (Exception e) {
|
||||
operationException = e;
|
||||
throw e;
|
||||
} finally {
|
||||
if (operationException == null) {
|
||||
pool.returnResource(jedis);
|
||||
} else {
|
||||
pool.returnBrokenResource(jedis);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
package org.ethereum.datasource.redis;
|
||||
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
import org.ethereum.core.AccountState;
|
||||
import org.ethereum.core.Block;
|
||||
import org.ethereum.core.Transaction;
|
||||
import org.ethereum.core.TransactionReceipt;
|
||||
import org.ethereum.db.ContractDetails;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class Serializers {
|
||||
|
||||
private static abstract class BaseRedisSerializer<T> implements RedisSerializer<T> {
|
||||
|
||||
public abstract boolean supports(Class<?> aClass);
|
||||
|
||||
@Override
|
||||
public boolean canSerialize(Object o) {
|
||||
return (o != null) && supports(o.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
private static class JacksonJsonRedisSerializer<T> implements RedisSerializer<T> {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final JavaType javaType;
|
||||
|
||||
JacksonJsonRedisSerializer(Class<T> clazz) {
|
||||
this.javaType = TypeFactory.defaultInstance().constructType(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T deserialize(byte[] bytes) {
|
||||
if (isEmpty(bytes)) return null;
|
||||
|
||||
try {
|
||||
return (T) this.objectMapper.readValue(bytes, 0, bytes.length, javaType);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException("Cannot read JSON: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(Object t) {
|
||||
if (t == null) return EMPTY_ARRAY;
|
||||
|
||||
try {
|
||||
return this.objectMapper.writeValueAsBytes(t);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException("Cannot write JSON: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSerialize(Object o) {
|
||||
return (o != null) && javaType.hasRawClass(o.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionReceiptSerializer extends BaseRedisSerializer<TransactionReceipt> {
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> aClass) {
|
||||
return TransactionReceipt.class.isAssignableFrom(aClass);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public byte[] serialize(TransactionReceipt transactionReceipt) {
|
||||
return (transactionReceipt == null) ? EMPTY_ARRAY : transactionReceipt.getEncoded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionReceipt deserialize(byte[] bytes) {
|
||||
return isEmpty(bytes) ? null : new TransactionReceipt(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionSerializer extends BaseRedisSerializer<Transaction> {
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> aClass) {
|
||||
return Transaction.class.isAssignableFrom(aClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(Transaction transaction) {
|
||||
return (transaction == null) ? EMPTY_ARRAY : transaction.getEncoded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transaction deserialize(byte[] bytes) {
|
||||
return isEmpty(bytes) ? null : new Transaction(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AccountStateSerializer extends BaseRedisSerializer<AccountState> {
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> aClass) {
|
||||
return AccountState.class.isAssignableFrom(aClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(AccountState accountState) {
|
||||
return (accountState == null) ? EMPTY_ARRAY : accountState.getEncoded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountState deserialize(byte[] bytes) {
|
||||
return isEmpty(bytes) ? null : new AccountState(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private static class BlockSerializer extends BaseRedisSerializer<Block> {
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> aClass) {
|
||||
return Block.class.isAssignableFrom(aClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(Block block) {
|
||||
return (block == null) ? EMPTY_ARRAY : block.getEncoded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block deserialize(byte[] bytes) {
|
||||
return isEmpty(bytes) ? null : new Block(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ContractDetailsSerializer extends BaseRedisSerializer<ContractDetails> {
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> aClass) {
|
||||
return ContractDetails.class.isAssignableFrom(aClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(ContractDetails contractDetails) {
|
||||
return (contractDetails == null) ? EMPTY_ARRAY : contractDetails.getEncoded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContractDetails deserialize(byte[] bytes) {
|
||||
return isEmpty(bytes) ? null : new ContractDetails(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final byte[] EMPTY_ARRAY = new byte[0];
|
||||
private static final Set<? extends BaseRedisSerializer> SERIALIZERS = new HashSet<BaseRedisSerializer>() {{
|
||||
add(new TransactionSerializer());
|
||||
add(new TransactionReceiptSerializer());
|
||||
add(new AccountStateSerializer());
|
||||
add(new BlockSerializer());
|
||||
add(new ContractDetailsSerializer());
|
||||
}};
|
||||
|
||||
public static <T> RedisSerializer<T> forClass(Class<T> clazz) {
|
||||
for (BaseRedisSerializer serializer : SERIALIZERS) {
|
||||
if (serializer.supports(clazz)) {
|
||||
return serializer;
|
||||
}
|
||||
}
|
||||
return new JacksonJsonRedisSerializer(clazz);
|
||||
}
|
||||
|
||||
private static boolean isEmpty(byte[] data) {
|
||||
return (data == null || data.length == 0);
|
||||
}
|
||||
}
|
|
@ -1,16 +1,11 @@
|
|||
package org.ethereum.db;
|
||||
|
||||
import org.ethereum.datasource.KeyValueDataSource;
|
||||
import org.ethereum.facade.EthereumFactory;
|
||||
import org.ethereum.util.ByteUtil;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.spongycastle.util.encoders.Hex;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.xml.crypto.dsig.keyinfo.KeyValue;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
@ -71,6 +66,7 @@ public class DatabaseImpl implements Database {
|
|||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
keyValueDataSource.close();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
package org.ethereum.facade;
|
||||
|
||||
import org.ethereum.config.SystemProperties;
|
||||
import org.ethereum.core.Transaction;
|
||||
import org.ethereum.datasource.KeyValueDataSource;
|
||||
import org.ethereum.datasource.LevelDbDataSource;
|
||||
import org.ethereum.datasource.redis.RedisConnection;
|
||||
import org.ethereum.db.RepositoryImpl;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import org.springframework.orm.hibernate4.LocalSessionFactoryBuilder;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.ethereum.config.SystemProperties.CONFIG;
|
||||
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
@ComponentScan(basePackages = "org.ethereum")
|
||||
public class CommonConfig {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger("general");
|
||||
|
||||
@Autowired
|
||||
private RedisConnection redisConnection;
|
||||
|
||||
@Bean
|
||||
Repository repository(){
|
||||
return new RepositoryImpl(keyValueDataSource(), keyValueDataSource());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
public KeyValueDataSource keyValueDataSource(){
|
||||
if (CONFIG.getKeyValueDataSource().equals("redis")) {
|
||||
if (redisConnection.isAvailable()) {
|
||||
// Name will be defined before initialization
|
||||
return redisConnection.createDataSource("");
|
||||
}
|
||||
}
|
||||
|
||||
return new LevelDbDataSource();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Set<Transaction> pendingTransactions() {
|
||||
return redisConnection.isAvailable()
|
||||
? redisConnection.createTransactionSet("pendingTransactions")
|
||||
: Collections.synchronizedSet(new HashSet<Transaction>());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SessionFactory sessionFactory() throws SQLException {
|
||||
LocalSessionFactoryBuilder builder =
|
||||
new LocalSessionFactoryBuilder(dataSource());
|
||||
builder.scanPackages("org.ethereum.db")
|
||||
.addProperties(getHibernateProperties());
|
||||
|
||||
return builder.buildSessionFactory();
|
||||
}
|
||||
|
||||
private Properties getHibernateProperties() {
|
||||
|
||||
Properties prop = new Properties();
|
||||
|
||||
if (SystemProperties.CONFIG.databaseReset())
|
||||
prop.put("hibernate.hbm2ddl.auto", "create");
|
||||
|
||||
prop.put("hibernate.format_sql", "true");
|
||||
|
||||
// todo: useful but annoying consider define by system.properties
|
||||
// prop.put("hibernate.show_sql", "true");
|
||||
prop.put("hibernate.dialect",
|
||||
"org.hibernate.dialect.HSQLDialect");
|
||||
return prop;
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public DataSourceTransactionManager transactionManager() {
|
||||
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
|
||||
dataSourceTransactionManager.setDataSource(dataSource());
|
||||
|
||||
return dataSourceTransactionManager;
|
||||
}
|
||||
|
||||
@Bean(name = "dataSource")
|
||||
public DriverManagerDataSource dataSource() {
|
||||
|
||||
logger.info("Connecting to the block store");
|
||||
|
||||
System.setProperty("hsqldb.reconfig_logging", "false");
|
||||
|
||||
String url =
|
||||
String.format("jdbc:hsqldb:file:./%s/blockchain/blockchain.db;" +
|
||||
"create=%s;hsqldb.default_table_type=cached",
|
||||
|
||||
SystemProperties.CONFIG.databaseDir(),
|
||||
SystemProperties.CONFIG.databaseReset());
|
||||
|
||||
DriverManagerDataSource ds = new DriverManagerDataSource();
|
||||
ds.setDriverClassName("org.hsqldb.jdbcDriver");
|
||||
ds.setUrl(url);
|
||||
ds.setUsername("sa");
|
||||
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,127 +1,29 @@
|
|||
package org.ethereum.facade;
|
||||
|
||||
import org.ethereum.config.SystemProperties;
|
||||
import org.ethereum.datasource.KeyValueDataSource;
|
||||
import org.ethereum.datasource.LevelDbDataSource;
|
||||
import org.ethereum.datasource.RedisDataSource;
|
||||
import org.ethereum.db.BlockStore;
|
||||
import org.ethereum.db.BlockStoreImpl;
|
||||
import org.ethereum.db.RepositoryImpl;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import org.springframework.orm.hibernate4.LocalSessionFactoryBuilder;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.Properties;
|
||||
|
||||
import static org.ethereum.config.SystemProperties.CONFIG;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author: Roman Mandeleil
|
||||
* Created on: 27/01/2015 01:05
|
||||
*/
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
@ComponentScan(basePackages = "org.ethereum")
|
||||
@Import(CommonConfig.class)
|
||||
public class DefaultConfig {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger("general");
|
||||
|
||||
@Autowired
|
||||
Ethereum eth;
|
||||
|
||||
@Bean
|
||||
Repository repository(){
|
||||
return new RepositoryImpl(keyValueDataSource(), keyValueDataSource());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
public KeyValueDataSource keyValueDataSource(){
|
||||
|
||||
if (CONFIG.getKeyValueDataSource().equals("redis")) {
|
||||
return new RedisDataSource();
|
||||
}
|
||||
|
||||
return new LevelDbDataSource();
|
||||
}
|
||||
@Autowired CommonConfig commonConfig;
|
||||
|
||||
@Bean
|
||||
@Transactional(propagation = Propagation.SUPPORTS)
|
||||
public BlockStore blockStore(SessionFactory sessionFactory){
|
||||
return new BlockStoreImpl(sessionFactory);
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public SessionFactory sessionFactory() throws SQLException {
|
||||
LocalSessionFactoryBuilder builder =
|
||||
new LocalSessionFactoryBuilder(dataSource());
|
||||
builder.scanPackages("org.ethereum.db")
|
||||
.addProperties(getHibernateProperties());
|
||||
|
||||
return builder.buildSessionFactory();
|
||||
}
|
||||
|
||||
private Properties getHibernateProperties() {
|
||||
|
||||
Properties prop = new Properties();
|
||||
|
||||
if (SystemProperties.CONFIG.databaseReset())
|
||||
prop.put("hibernate.hbm2ddl.auto", "create");
|
||||
|
||||
prop.put("hibernate.format_sql", "true");
|
||||
|
||||
// todo: useful but annoying consider define by system.properties
|
||||
// prop.put("hibernate.show_sql", "true");
|
||||
prop.put("hibernate.dialect",
|
||||
"org.hibernate.dialect.HSQLDialect");
|
||||
return prop;
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public DataSourceTransactionManager transactionManager() {
|
||||
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
|
||||
dataSourceTransactionManager.setDataSource(dataSource());
|
||||
|
||||
return dataSourceTransactionManager;
|
||||
}
|
||||
|
||||
@Bean(name = "dataSource")
|
||||
public DriverManagerDataSource dataSource() {
|
||||
|
||||
logger.info("Connecting to the block store");
|
||||
|
||||
System.setProperty("hsqldb.reconfig_logging", "false");
|
||||
|
||||
String url =
|
||||
String.format("jdbc:hsqldb:file:./%s/blockchain/blockchain.db;" +
|
||||
"create=%s;hsqldb.default_table_type=cached",
|
||||
|
||||
SystemProperties.CONFIG.databaseDir(),
|
||||
SystemProperties.CONFIG.databaseReset());
|
||||
|
||||
DriverManagerDataSource ds = new DriverManagerDataSource();
|
||||
ds.setDriverClassName("org.hsqldb.jdbcDriver");
|
||||
ds.setUrl(url);
|
||||
ds.setUsername("sa");
|
||||
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,25 +12,20 @@ import org.ethereum.net.server.PeerServer;
|
|||
import org.ethereum.net.submit.TransactionExecutor;
|
||||
import org.ethereum.net.submit.TransactionTask;
|
||||
import org.ethereum.util.ByteUtil;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.math.BigInteger;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import static org.ethereum.config.SystemProperties.CONFIG;
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,131 +1,32 @@
|
|||
package org.ethereum.facade;
|
||||
|
||||
import org.ethereum.config.SystemProperties;
|
||||
import org.ethereum.datasource.KeyValueDataSource;
|
||||
import org.ethereum.datasource.LevelDbDataSource;
|
||||
import org.ethereum.datasource.RedisDataSource;
|
||||
import org.ethereum.db.BlockStore;
|
||||
import org.ethereum.db.BlockStoreImpl;
|
||||
import org.ethereum.db.InMemoryBlockStore;
|
||||
import org.ethereum.db.RepositoryImpl;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import org.springframework.orm.hibernate4.LocalSessionFactoryBuilder;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.Properties;
|
||||
|
||||
import static org.ethereum.config.SystemProperties.CONFIG;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author: Roman Mandeleil
|
||||
* Created on: 27/01/2015 01:05
|
||||
*/
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
@ComponentScan(basePackages = "org.ethereum")
|
||||
@Import(CommonConfig.class)
|
||||
public class RemoteConfig {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger("general");
|
||||
@Autowired CommonConfig commonConfig;
|
||||
|
||||
@Autowired
|
||||
Ethereum eth;
|
||||
|
||||
// todo: init total difficulty
|
||||
// todo: init total difficulty
|
||||
// todo: init last 1000 blocks
|
||||
|
||||
@Bean
|
||||
Repository repository(){
|
||||
return new RepositoryImpl(keyValueDataSource(), keyValueDataSource());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
public KeyValueDataSource keyValueDataSource(){
|
||||
|
||||
if (CONFIG.getKeyValueDataSource().equals("redis")) {
|
||||
return new RedisDataSource();
|
||||
}
|
||||
|
||||
return new LevelDbDataSource();
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
@Transactional(propagation = Propagation.SUPPORTS)
|
||||
public BlockStore blockStore(SessionFactory sessionFactory){
|
||||
return new InMemoryBlockStore();
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public SessionFactory sessionFactory() throws SQLException {
|
||||
LocalSessionFactoryBuilder builder =
|
||||
new LocalSessionFactoryBuilder(dataSource());
|
||||
builder.scanPackages("org.ethereum.db")
|
||||
.addProperties(getHibernateProperties());
|
||||
|
||||
return builder.buildSessionFactory();
|
||||
}
|
||||
|
||||
private Properties getHibernateProperties() {
|
||||
|
||||
Properties prop = new Properties();
|
||||
|
||||
if (SystemProperties.CONFIG.databaseReset())
|
||||
prop.put("hibernate.hbm2ddl.auto", "create");
|
||||
|
||||
prop.put("hibernate.format_sql", "true");
|
||||
|
||||
// todo: useful but annoying consider define by system.properties
|
||||
// prop.put("hibernate.show_sql", "true");
|
||||
prop.put("hibernate.dialect",
|
||||
"org.hibernate.dialect.HSQLDialect");
|
||||
return prop;
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public DataSourceTransactionManager transactionManager() {
|
||||
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
|
||||
dataSourceTransactionManager.setDataSource(dataSource());
|
||||
|
||||
return dataSourceTransactionManager;
|
||||
}
|
||||
|
||||
@Bean(name = "dataSource")
|
||||
public DriverManagerDataSource dataSource() {
|
||||
|
||||
logger.info("Connecting to the block store");
|
||||
|
||||
System.setProperty("hsqldb.reconfig_logging", "false");
|
||||
|
||||
String url =
|
||||
String.format("jdbc:hsqldb:file:./%s/blockchain/blockchain.db;" +
|
||||
"create=%s;hsqldb.default_table_type=cached",
|
||||
|
||||
SystemProperties.CONFIG.databaseDir(),
|
||||
SystemProperties.CONFIG.databaseReset());
|
||||
|
||||
DriverManagerDataSource ds = new DriverManagerDataSource();
|
||||
ds.setDriverClassName("org.hsqldb.jdbcDriver");
|
||||
ds.setUrl(url);
|
||||
ds.setUsername("sa");
|
||||
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package org.ethereum.util;
|
||||
|
||||
|
||||
public interface Functional {
|
||||
|
||||
/**
|
||||
* Represents an operation that accepts a single input argument and returns no
|
||||
* result. Unlike most other functional interfaces, {@code Consumer} is expected
|
||||
* to operate via side-effects.
|
||||
*
|
||||
* @param <T> the type of the input to the operation
|
||||
*/
|
||||
public static interface Consumer<T> {
|
||||
|
||||
/**
|
||||
* Performs this operation on the given argument.
|
||||
*
|
||||
* @param t the input argument
|
||||
*/
|
||||
void accept(T t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an operation that accepts two input arguments and returns no
|
||||
* result. This is the two-arity specialization of {@link java.util.function.Consumer}.
|
||||
* Unlike most other functional interfaces, {@code BiConsumer} is expected
|
||||
* to operate via side-effects.
|
||||
*
|
||||
* @param <T> the type of the first argument to the operation
|
||||
* @param <U> the type of the second argument to the operation
|
||||
*
|
||||
* @see org.ethereum.util.Functional.Consumer
|
||||
*/
|
||||
public interface BiConsumer<T, U> {
|
||||
|
||||
/**
|
||||
* Performs this operation on the given arguments.
|
||||
*
|
||||
* @param t the first input argument
|
||||
* @param u the second input argument
|
||||
*/
|
||||
void accept(T t, U u);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a function that accepts one argument and produces a result.
|
||||
*
|
||||
* @param <T> the type of the input to the function
|
||||
* @param <R> the type of the result of the function
|
||||
*/
|
||||
public static interface Function<T, R> {
|
||||
|
||||
/**
|
||||
* Applies this function to the given argument.
|
||||
*
|
||||
* @param t the function argument
|
||||
* @return the function result
|
||||
*/
|
||||
R apply(T t);
|
||||
}
|
||||
|
||||
}
|
|
@ -153,4 +153,8 @@ root.hash.start = -1
|
|||
peer.capabilities = eth, shh
|
||||
|
||||
# Key value data source values: [leveldb/redis]
|
||||
keyvalue.datasource = leveldb
|
||||
keyvalue.datasource = leveldb
|
||||
|
||||
# Redis cloud enabled flag.
|
||||
# Allows using RedisConnection for creating cloud based data structures.
|
||||
redis.enabled=false
|
|
@ -0,0 +1,58 @@
|
|||
package test.ethereum.datasource;
|
||||
|
||||
import org.ethereum.datasource.redis.RedisConnection;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.support.AnnotationConfigContextLoader;
|
||||
import org.springframework.util.StringUtils;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisPoolConfig;
|
||||
import redis.clients.jedis.Protocol;
|
||||
import test.ethereum.TestContext;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration(loader = AnnotationConfigContextLoader.class)
|
||||
public abstract class AbstractRedisTest {
|
||||
|
||||
@Configuration
|
||||
@ComponentScan(basePackages = "org.ethereum")
|
||||
static class ContextConfiguration extends TestContext { }
|
||||
|
||||
@Autowired
|
||||
private RedisConnection redisConnection;
|
||||
|
||||
private Boolean connected;
|
||||
|
||||
protected RedisConnection getRedisConnection() {
|
||||
return redisConnection;
|
||||
}
|
||||
|
||||
protected Boolean isConnected() {
|
||||
if (connected == null) {
|
||||
String url = System.getenv(RedisConnection.REDISCLOUD_URL);
|
||||
try {
|
||||
Jedis jedis = new Jedis(new URI(url));
|
||||
connected = jedis.ping().equals("PONG");
|
||||
jedis.close();
|
||||
} catch (Exception e) {
|
||||
System.out.printf("Cannot connect to '%s' Redis cloud.\n", url);
|
||||
}
|
||||
|
||||
assertFalse(connected ^ redisConnection.isAvailable());
|
||||
}
|
||||
|
||||
return connected;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,68 +1,75 @@
|
|||
package test.ethereum.datasource;
|
||||
|
||||
import org.ethereum.datasource.RedisDataSource;
|
||||
|
||||
import org.ethereum.datasource.KeyValueDataSource;
|
||||
import org.ethereum.datasource.redis.RedisConnection;
|
||||
import org.ethereum.datasource.redis.RedisDataSource;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.spongycastle.util.encoders.Hex;
|
||||
import redis.clients.jedis.Jedis;
|
||||
|
||||
import redis.clients.jedis.exceptions.JedisConnectionException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author Roman Mandeleil
|
||||
*/
|
||||
public class RedisDataSourceTest {
|
||||
|
||||
public class RedisDataSourceTest extends AbstractRedisTest {
|
||||
|
||||
@Test
|
||||
public void testSet1() {
|
||||
if (!isConnected()) return;
|
||||
|
||||
KeyValueDataSource dataSource = createDataSource("test-state");
|
||||
try {
|
||||
RedisDataSource redis = new RedisDataSource();
|
||||
redis.setName("state");
|
||||
redis.init();
|
||||
|
||||
byte[] key = Hex.decode("a1a2a3");
|
||||
byte[] val = Hex.decode("b1b2b3");
|
||||
|
||||
redis.put(key, val);
|
||||
byte[] val2 = redis.get(key);
|
||||
dataSource.put(key, val);
|
||||
byte[] val2 = dataSource.get(key);
|
||||
|
||||
Assert.assertEquals(Hex.toHexString(val), Hex.toHexString(val2));
|
||||
} catch (JedisConnectionException e) {
|
||||
// no redis server consider test as pass
|
||||
} finally {
|
||||
clear(dataSource);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSet2() {
|
||||
if (!isConnected()) return;
|
||||
|
||||
KeyValueDataSource states = createDataSource("test-state");
|
||||
KeyValueDataSource details = createDataSource("test-details");
|
||||
|
||||
try {
|
||||
RedisDataSource redis1 = new RedisDataSource();
|
||||
redis1.setName("state");
|
||||
redis1.init();
|
||||
|
||||
RedisDataSource redis2 = new RedisDataSource();
|
||||
redis2.setName("details");
|
||||
redis2.init();
|
||||
|
||||
|
||||
byte[] key = Hex.decode("a1a2a3");
|
||||
byte[] val1 = Hex.decode("b1b2b3");
|
||||
byte[] val2 = Hex.decode("c1c2c3");
|
||||
|
||||
redis1.put(key, val1);
|
||||
redis2.put(key, val2);
|
||||
states.put(key, val1);
|
||||
details.put(key, val2);
|
||||
|
||||
byte[] res1 = redis1.get(key);
|
||||
byte[] res2 = redis2.get(key);
|
||||
byte[] res1 = states.get(key);
|
||||
byte[] res2 = details.get(key);
|
||||
|
||||
Assert.assertEquals(Hex.toHexString(val1), Hex.toHexString(res1));
|
||||
Assert.assertEquals(Hex.toHexString(val2), Hex.toHexString(res2));
|
||||
} catch (JedisConnectionException e) {
|
||||
// no redis server consider test as pass
|
||||
} finally {
|
||||
clear(states);
|
||||
clear(details);
|
||||
}
|
||||
}
|
||||
|
||||
private KeyValueDataSource createDataSource(String name) {
|
||||
KeyValueDataSource result = getRedisConnection().createDataSource(name);
|
||||
result.setName(name);
|
||||
result.init();
|
||||
return result;
|
||||
}
|
||||
|
||||
private void clear(KeyValueDataSource dataSource) {
|
||||
((RedisDataSource) dataSource).clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
package test.ethereum.datasource;
|
||||
|
||||
import org.ethereum.core.Transaction;
|
||||
import org.ethereum.crypto.ECKey;
|
||||
import org.ethereum.crypto.HashUtil;
|
||||
import org.junit.Test;
|
||||
import org.spongycastle.util.encoders.Hex;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class RedisStorageTest extends AbstractRedisTest {
|
||||
|
||||
@Test
|
||||
public void testRedisSet() {
|
||||
if (!isConnected()) return;
|
||||
|
||||
Pojo elephant = Pojo.create(5L, "elephant");
|
||||
Pojo lion = Pojo.create(5L, "lion");
|
||||
|
||||
Set<Pojo> ranch = getRedisConnection().createSetFor(Pojo.class, "ranch");
|
||||
Pojo chicken = Pojo.create(1L, "chicken");
|
||||
Pojo cow = Pojo.create(2L, "cow");
|
||||
Pojo puppy = Pojo.create(3L, "puppy");
|
||||
Pojo kitten = Pojo.create(4L, "kitten");
|
||||
|
||||
assertTrue(ranch.add(chicken));
|
||||
assertFalse(ranch.add(chicken));
|
||||
assertTrue(ranch.contains(chicken));
|
||||
assertEquals(1, ranch.size());
|
||||
|
||||
Pojo next = ranch.iterator().next();
|
||||
assertNotNull(next);
|
||||
assertEquals(chicken, next);
|
||||
|
||||
assertTrue(ranch.addAll(asList(cow, puppy, kitten)));
|
||||
assertEquals(4, ranch.size());
|
||||
assertFalse(ranch.isEmpty());
|
||||
assertFalse(ranch.remove(elephant));
|
||||
assertFalse(ranch.removeAll(asList(cow, lion, elephant)));
|
||||
assertEquals(3, ranch.size());
|
||||
|
||||
assertTrue(ranch.retainAll(asList(kitten, puppy)));
|
||||
assertEquals(2, ranch.size());
|
||||
|
||||
ranch.clear();
|
||||
assertEquals(0, ranch.size());
|
||||
assertTrue(ranch.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSeveralSetsWithOneName() {
|
||||
if (!isConnected()) return;
|
||||
|
||||
final String name = "testTransactions";
|
||||
Set<Transaction> transactions = getRedisConnection().createTransactionSet(name);
|
||||
transactions.add(createTransaction("09184e72a000", "4255", "1000000000000000000000", "cat"));
|
||||
transactions.add(createTransaction("09184e72a000", "4255", "1000000000000000000000", "dog"));
|
||||
transactions.add(createTransaction("09184e72a000", "4255", "1000000000000000000000", "rabbit"));
|
||||
|
||||
Set<Transaction> transactions1 = getRedisConnection().createTransactionSet(name);
|
||||
transactions1.add(createTransaction("09184e72a000", "4255", "1000000000000000000000", "duck"));
|
||||
transactions1.add(createTransaction("09184e72a000", "4255", "1000000000000000000000", "chicken"));
|
||||
transactions1.add(createTransaction("09184e72a000", "4255", "1000000000000000000000", "cow"));
|
||||
|
||||
assertEquals(6, transactions1.size());
|
||||
transactions.clear();
|
||||
assertTrue(transactions1.isEmpty());
|
||||
}
|
||||
|
||||
private static class Pojo {
|
||||
private Long id;
|
||||
private String name;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public static Pojo create(long id, String name) {
|
||||
Pojo result = new Pojo();
|
||||
result.setId(id);
|
||||
result.setName(name);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null || !getClass().isInstance(obj)) return false;
|
||||
if (this == obj) return true;
|
||||
|
||||
Pojo another = (Pojo) obj;
|
||||
return (another.getId() == getId()) && another.getName().equals(getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hashCode = 17;
|
||||
|
||||
hashCode += ((getId() == null) ? 0 : getId().hashCode()) * 31;
|
||||
hashCode += ((getName() == null) ? 0 : getName().hashCode()) * 31;
|
||||
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
public static Transaction createTransaction(String gasPrice, String gas, String val, String secret) {
|
||||
|
||||
ECKey ecKey = ECKey.fromPrivate(HashUtil.sha3(secret.getBytes()));
|
||||
|
||||
// Tn (nonce); Tp(pgas); Tg(gaslimi); Tt(value); Tv(value); Ti(sender); Tw; Tr; Ts
|
||||
return new Transaction(null, Hex.decode(gasPrice), Hex.decode(gas), ecKey.getAddress(),
|
||||
new BigInteger(val).toByteArray(),
|
||||
null);
|
||||
}
|
||||
}
|
|
@ -22,15 +22,13 @@ public class MockDB implements KeyValueDataSource {
|
|||
|
||||
@Override
|
||||
public byte[] get(byte[] arg0) throws DBException {
|
||||
|
||||
return storage.get(new ByteArrayWrapper(arg0));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void put(byte[] key, byte[] value) throws DBException {
|
||||
|
||||
storage.put(new ByteArrayWrapper(key), value);
|
||||
public byte[] put(byte[] key, byte[] value) throws DBException {
|
||||
return storage.put(new ByteArrayWrapper(key), value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue