diff --git a/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisConnection.java b/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisConnection.java index 0932abdc..e67f49cd 100644 --- a/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisConnection.java +++ b/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisConnection.java @@ -3,14 +3,19 @@ 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(); Set createSetFor(Class clazz, String name); + Map createMapFor(Class keyClass, Class valueClass, String name); + Set createTransactionSet(String name); KeyValueDataSource createDataSource(String name); diff --git a/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisConnectionImpl.java b/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisConnectionImpl.java index ced78c0f..9bca3854 100644 --- a/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisConnectionImpl.java +++ b/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisConnectionImpl.java @@ -15,6 +15,7 @@ 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; @@ -30,7 +31,7 @@ public class RedisConnectionImpl implements RedisConnection { public void tryConnect() { if (!SystemProperties.CONFIG.isRedisEnabled()) return; - String redisCloudUrl = System.getenv("REDISCLOUD_URL"); + String redisCloudUrl = System.getenv(REDISCLOUD_URL); if (isEmpty(redisCloudUrl)) { logger.info("Cannot connect to Redis. 'REDISCLOUD_URL' environment variable is not defined."); return; @@ -73,14 +74,16 @@ public class RedisConnectionImpl implements RedisConnection { public boolean isAvailable() { boolean available = jedisPool != null; if (available) { - Jedis jedis = jedisPool.getResource(); try { - available = jedis.isConnected(); + Jedis jedis = jedisPool.getResource(); + try { + available = jedis.isConnected(); + } finally { + jedisPool.returnResource(jedis); + } } catch (Throwable t) { logger.warn("Connection testing failed: ", t); available = false; - } finally { - jedisPool.returnResource(jedis); } } return available; @@ -90,6 +93,11 @@ public class RedisConnectionImpl implements RedisConnection { public Set createSetFor(Class clazz, String name) { return new RedisSet(name, jedisPool, Serializers.forClass(clazz)); } + + @Override + public Map createMapFor(Class keyClass, Class valueClass, String name) { + return new RedisMap(name, jedisPool, Serializers.forClass(keyClass), Serializers.forClass(valueClass)); + } @Override public Set createTransactionSet(String name) { diff --git a/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisSet.java b/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisSet.java index 807dcb2c..63882579 100644 --- a/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisSet.java +++ b/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisSet.java @@ -87,7 +87,7 @@ public class RedisSet extends RedisStorage implements Set { return pooledWithResult(new Function() { @Override public Boolean apply(Jedis jedis) { - return jedis.sadd(getName(), serialize(c)) == 1; + return jedis.sadd(getName(), serialize(c)) == c.size(); } }); } @@ -97,7 +97,13 @@ public class RedisSet extends RedisStorage implements Set { return pooledWithResult(new Function() { @Override public Boolean apply(Jedis jedis) { - return jedis.sinterstore(getName(), serialize(c)) == c.size(); + byte[] tempName = temporaryName(); + try { + jedis.sadd(tempName, serialize(c)); + return jedis.scard(getName()) != jedis.sinterstore(getName(), getName(), tempName); + } finally { + jedis.del(tempName); + } } }); } @@ -107,7 +113,7 @@ public class RedisSet extends RedisStorage implements Set { return pooledWithResult(new Function() { @Override public Boolean apply(Jedis jedis) { - return jedis.srem(getName(), serialize(c)) == 1; + return jedis.srem(getName(), serialize(c)) == c.size(); } }); } diff --git a/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisStorage.java b/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisStorage.java index 74307052..5303c36f 100644 --- a/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisStorage.java +++ b/ethereumj-core/src/main/java/org/ethereum/datasource/redis/RedisStorage.java @@ -1,5 +1,6 @@ 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; @@ -34,6 +35,14 @@ public abstract class RedisStorage { 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); } @@ -89,10 +98,18 @@ public abstract class RedisStorage { protected R pooledWithResult(Function function) { Jedis jedis = pool.getResource(); + Exception operationException = null; try { return function.apply(jedis); + } catch (Exception e) { + operationException = e; + throw e; } finally { - pool.returnResource(jedis); + if (operationException == null) { + pool.returnResource(jedis); + } else { + pool.returnBrokenResource(jedis); + } } } } diff --git a/ethereumj-core/src/test/java/test/ethereum/datasource/AbstractRedisTest.java b/ethereumj-core/src/test/java/test/ethereum/datasource/AbstractRedisTest.java index feeedeee..0193a5c8 100644 --- a/ethereumj-core/src/test/java/test/ethereum/datasource/AbstractRedisTest.java +++ b/ethereumj-core/src/test/java/test/ethereum/datasource/AbstractRedisTest.java @@ -1,6 +1,7 @@ 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; @@ -8,17 +9,50 @@ 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 class AbstractRedisTest { +public abstract class AbstractRedisTest { @Configuration @ComponentScan(basePackages = "org.ethereum") static class ContextConfiguration extends TestContext { } @Autowired - protected RedisConnection redisConnection; + 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; + } } diff --git a/ethereumj-core/src/test/java/test/ethereum/datasource/RedisConnectionTest.java b/ethereumj-core/src/test/java/test/ethereum/datasource/RedisConnectionTest.java deleted file mode 100644 index 81f7cd9e..00000000 --- a/ethereumj-core/src/test/java/test/ethereum/datasource/RedisConnectionTest.java +++ /dev/null @@ -1,91 +0,0 @@ -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 org.junit.Assert.*; - -public class RedisConnectionTest extends AbstractRedisTest { - - @Test - public void test() { - if (!redisConnection.isAvailable()) return; - - Set pojos = redisConnection.createSetFor(Pojo.class, "pojos"); - - Pojo pojo = Pojo.create(1L, "test"); - pojos.add(pojo); - assertTrue(pojos.contains(pojo)); - assertEquals(1, pojos.size()); - Pojo next = pojos.iterator().next(); - assertNotNull(next); - assertEquals(pojo.getId(), next.getId()); - assertEquals(pojo.getName(), next.getName()); - assertTrue(pojos.remove(pojo)); - assertTrue(pojos.isEmpty()); - } - - @Test - public void transactionStorageTest() { - if (!redisConnection.isAvailable()) return; - - String namespace = "txnNamespace"; - Set transactions = redisConnection.createTransactionSet(namespace); - transactions.add(createTransaction("09184e72a000", "4255", "1000000000000000000000", "cat")); - transactions.add(createTransaction("09184e72a000", "4255", "1000000000000000000000", "dog")); - transactions.add(createTransaction("09184e72a000", "4255", "1000000000000000000000", "rabbit")); - - Set transactions1 = redisConnection.createTransactionSet(namespace); - 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; - } - } - - 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); - } -} \ No newline at end of file diff --git a/ethereumj-core/src/test/java/test/ethereum/datasource/RedisDataSourceTest.java b/ethereumj-core/src/test/java/test/ethereum/datasource/RedisDataSourceTest.java index 3ad071ee..9925fb56 100644 --- a/ethereumj-core/src/test/java/test/ethereum/datasource/RedisDataSourceTest.java +++ b/ethereumj-core/src/test/java/test/ethereum/datasource/RedisDataSourceTest.java @@ -1,10 +1,16 @@ package test.ethereum.datasource; 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 java.net.URI; +import java.net.URISyntaxException; +import java.util.Set; /** * @author Roman Mandeleil @@ -13,7 +19,7 @@ public class RedisDataSourceTest extends AbstractRedisTest { @Test public void testSet1() { - if (!redisConnection.isAvailable()) return; + if (!isConnected()) return; KeyValueDataSource dataSource = createDataSource("test-state"); try { @@ -29,9 +35,23 @@ public class RedisDataSourceTest extends AbstractRedisTest { } } + @Test + public void test() { + try { + Jedis jedis = new Jedis(new URI(System.getenv(RedisConnection.REDISCLOUD_URL))); + Long count = jedis.sinterstore("f", "f", "s"); + System.out.println(count); + Set r = jedis.smembers("f"); + System.out.println(r); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + + } + @Test public void testSet2() { - if (!redisConnection.isAvailable()) return; + if (!isConnected()) return; KeyValueDataSource states = createDataSource("test-state"); KeyValueDataSource details = createDataSource("test-details"); @@ -56,7 +76,7 @@ public class RedisDataSourceTest extends AbstractRedisTest { } private KeyValueDataSource createDataSource(String name) { - KeyValueDataSource result = redisConnection.createDataSource(name); + KeyValueDataSource result = getRedisConnection().createDataSource(name); result.setName(name); result.init(); return result; diff --git a/ethereumj-core/src/test/java/test/ethereum/datasource/RedisStorageTest.java b/ethereumj-core/src/test/java/test/ethereum/datasource/RedisStorageTest.java new file mode 100644 index 00000000..97c04abc --- /dev/null +++ b/ethereumj-core/src/test/java/test/ethereum/datasource/RedisStorageTest.java @@ -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 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 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 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); + } +} \ No newline at end of file