diff --git a/Jenkinsfile-continuous b/Jenkinsfile-continuous index 2bbc34b2..acfa3fe8 100644 --- a/Jenkinsfile-continuous +++ b/Jenkinsfile-continuous @@ -19,7 +19,7 @@ pipeline { stage('Build') { steps { - sh 'mvn clean verify -P code-coverage' + sh 'mvn clean verify' } } } diff --git a/README.md b/README.md index d8e9bb35..ed844bdc 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ The main cleanup we made with Jedis is, if the jedis client came from a pool, it ## Now onto Lua and Coolness: -Jedis provides a basic way of loading a Lua script into Redis and eval the script by its sha1 hash. rDBI provides this functionality via fluent queries, based off of [jDBI's fluent queries](http://jdbi.org/fluent_queries/). The application developer does not have to think about preloading the scripts on startup of the app or creating enums and storing sha1 in hashmaps. rDBI will cache the lua scripts internally and load them on demand while keeping it all thread-safe. +Jedis provides a basic way of loading a Lua script into Redis and eval the script by its sha1 hash. rDBI provides this functionality via fluent queries, based off of [jDBI's fluent queries](http://jdbi.org/fluent_queries/). +The application developer does not have to think about preloading the scripts on startup of the app or creating enums and storing sha1 in hashmaps. rDBI will cache the lua scripts internally and load them on demand while keeping it all thread-safe. private static interface TestDAO { @RedisQuery( diff --git a/rdbi-core/pom.xml b/rdbi-core/pom.xml index 2b0754d4..386464b3 100644 --- a/rdbi-core/pom.xml +++ b/rdbi-core/pom.xml @@ -13,42 +13,10 @@ rdbi-core rDBI-core - - - - - org.apache.maven.plugins - maven-shade-plugin - - - shade-proxying-implementation - package - - shade - - - - - cglib:cglib-nodep - - - - - net.sf.cglib - com.lithium.dbi.rdbi.shaded.net.sf.cglib - - - - - - - - - - cglib - cglib-nodep + net.bytebuddy + byte-buddy redis.clients diff --git a/rdbi-core/src/main/java/com/lithium/dbi/rdbi/Handle.java b/rdbi-core/src/main/java/com/lithium/dbi/rdbi/Handle.java index 0a012d84..ce7638f0 100644 --- a/rdbi-core/src/main/java/com/lithium/dbi/rdbi/Handle.java +++ b/rdbi-core/src/main/java/com/lithium/dbi/rdbi/Handle.java @@ -11,7 +11,7 @@ public class Handle implements Closeable { private final Jedis jedis; private final Tracer tracer; - private JedisWrapperDoNotUse jedisWrapper; + private Jedis jedisWrapper; private final ProxyFactory proxyFactory; diff --git a/rdbi-core/src/main/java/com/lithium/dbi/rdbi/JedisWrapperDoNotUse.java b/rdbi-core/src/main/java/com/lithium/dbi/rdbi/JedisWrapperDoNotUse.java index 686b315d..81e4d063 100644 --- a/rdbi-core/src/main/java/com/lithium/dbi/rdbi/JedisWrapperDoNotUse.java +++ b/rdbi-core/src/main/java/com/lithium/dbi/rdbi/JedisWrapperDoNotUse.java @@ -4,7 +4,7 @@ abstract class JedisWrapperDoNotUse extends Jedis { - JedisWrapperDoNotUse() { + public JedisWrapperDoNotUse() { super(); } diff --git a/rdbi-core/src/main/java/com/lithium/dbi/rdbi/JedisWrapperMethodInterceptor.java b/rdbi-core/src/main/java/com/lithium/dbi/rdbi/JedisWrapperMethodInterceptor.java index 0fad37a4..c7c0231c 100644 --- a/rdbi-core/src/main/java/com/lithium/dbi/rdbi/JedisWrapperMethodInterceptor.java +++ b/rdbi-core/src/main/java/com/lithium/dbi/rdbi/JedisWrapperMethodInterceptor.java @@ -5,55 +5,85 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Scope; -import net.sf.cglib.proxy.Enhancer; -import net.sf.cglib.proxy.Factory; -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.AllArguments; +import net.bytebuddy.implementation.bind.annotation.Origin; +import net.bytebuddy.implementation.bind.annotation.RuntimeType; +import net.bytebuddy.matcher.ElementMatchers; import redis.clients.jedis.Jedis; import redis.clients.jedis.exceptions.JedisException; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -class JedisWrapperMethodInterceptor implements MethodInterceptor { +public class JedisWrapperMethodInterceptor { private final Jedis jedis; private final Tracer tracer; private final Attributes commonAttributes; + private static final Class clazz = newLoadedClass(); - static Factory newFactory() { - Enhancer e = new Enhancer(); - e.setClassLoader(Jedis.class.getClassLoader()); - e.setSuperclass(JedisWrapperDoNotUse.class); - e.setCallback(new MethodNoOpInterceptor()); - return (Factory) e.create(); + static Jedis newInstance(final Jedis realJedis, final Tracer tracer) { + try { + Object proxy = clazz.getDeclaredConstructor() + .newInstance(); + final Field field = proxy.getClass().getDeclaredField("handler"); + field.set(proxy, new JedisWrapperMethodInterceptor(realJedis, tracer)); + return (Jedis) proxy; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException | + NoSuchFieldException e) { + throw new RuntimeException(e); + } } - static JedisWrapperDoNotUse newInstance(final Factory factory, final Jedis realJedis, final Tracer tracer) { - return (JedisWrapperDoNotUse) factory.newInstance(new JedisWrapperMethodInterceptor(realJedis, tracer)); + private static Class newLoadedClass() { + return new ByteBuddy() + .subclass(JedisWrapperDoNotUse.class) + .defineField("handler", JedisWrapperMethodInterceptor.class, Visibility.PUBLIC) + .method(ElementMatchers.isMethod()) + .intercept(MethodDelegation.toField("handler")) + .make() + .load(Jedis.class.getClassLoader(), ClassLoadingStrategy.UsingLookup.withFallback(MethodHandles::lookup)) + .getLoaded(); } - private JedisWrapperMethodInterceptor(Jedis jedis, Tracer tracer) { + public JedisWrapperMethodInterceptor(Jedis jedis, Tracer tracer) { this.jedis = jedis; this.tracer = tracer; commonAttributes = Attributes.of( AttributeKey.stringKey("db.type"), "redis", AttributeKey.stringKey("component"), "rdbi" - ); + ); } - @Override - public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + @RuntimeType + public Object intercept( + @AllArguments Object[] args, + @Origin Method method) { Span s = tracer.spanBuilder(method.getName()) - .setAllAttributes(commonAttributes) - .startSpan(); + .setAllAttributes(commonAttributes) + .startSpan(); if (args.length > 0 && args[0] instanceof String) { s.setAttribute("redis.key", (String) args[0]); } - try (Scope scope = s.makeCurrent()) { - return methodProxy.invoke(jedis, args); + try (Scope ignored = s.makeCurrent()) { + return method.invoke(jedis, args); } catch (JedisException e) { s.recordException(e); throw e; + } catch (InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw new RuntimeException(e.getCause()); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); } finally { s.end(); } diff --git a/rdbi-core/src/main/java/com/lithium/dbi/rdbi/MethodContextInterceptor.java b/rdbi-core/src/main/java/com/lithium/dbi/rdbi/MethodContextInterceptor.java index 9ac3853e..f5eef620 100644 --- a/rdbi-core/src/main/java/com/lithium/dbi/rdbi/MethodContextInterceptor.java +++ b/rdbi-core/src/main/java/com/lithium/dbi/rdbi/MethodContextInterceptor.java @@ -1,7 +1,8 @@ package com.lithium.dbi.rdbi; -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; +import net.bytebuddy.implementation.bind.annotation.AllArguments; +import net.bytebuddy.implementation.bind.annotation.Origin; +import net.bytebuddy.implementation.bind.annotation.RuntimeType; import redis.clients.jedis.Jedis; import redis.clients.jedis.exceptions.JedisDataException; @@ -10,43 +11,68 @@ import java.util.List; import java.util.Map; -class MethodContextInterceptor implements MethodInterceptor { +public class MethodContextInterceptor { private final Jedis jedis; private final Map contexts; + public MethodContextInterceptor(Jedis jedis, Map contexts) { this.jedis = jedis; this.contexts = contexts; } - @Override - @SuppressWarnings("unchecked") - public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { + @RuntimeType + public Object intercept(@AllArguments Object[] args, + @Origin Method method) { MethodContext context = contexts.get(method); - Object ret = context.hasDynamicLists() ? callEvalDynamicList(context, objects) - : callEval(context, objects); - + Object ret = context.hasDynamicLists() ? callEvalDynamicList(context, args) + : callEval(context, args); if (ret == null) { return null; } - if (contexts.get(method).getMapper() != null) { - return contexts.get(method).getMapper().map(ret); + + if (context.getMapper() != null) { + // here we need to adjust for the expected input to the mapper + Class parameterType; + try { + parameterType = context.getMapper().getClass().getMethod("map", Integer.class).getParameterTypes()[0]; + } catch (NoSuchMethodException e) { + parameterType = null; + } + return context.getMapper().map(adjust(parameterType, ret)); + } else { + return adjust(method.getReturnType(), ret); + } + } + + private Object adjust(Class c, Object ret) { + // problem here is we are getting a LONG but expected an int + // did this work on older jdks? doesn't seem to be a cglib vs bytebuddy thing + // java.lang.ClassCastException: class java.lang.Long cannot be cast to class java.lang.Integer (java.lang.Long and java.lang.Integer are in module java.base of loader 'bootstrap') + // i am not sure why this wasn't required before, and there's probably a utility out there + // to do it better the primary thing to handle is explicit cast to int where jedis returns a number + // as a long + if (c == null) { + return ret; + } else if ((c.equals(Integer.TYPE) || c.isAssignableFrom(Integer.class)) && ret instanceof Long) { + return ((Long) ret).intValue(); } else { return ret; } } + @SuppressWarnings("unchecked") private Object callEval(MethodContext context, Object[] objects) { - List keys = objects.length > 0 ? (List) objects[0] : null; - List argv = objects.length > 1 ? (List) objects[1] : null; + List keys = objects.length > 0 ? (List) objects[0] : new ArrayList<>(); + List argv = objects.length > 1 ? (List) objects[1] : new ArrayList<>(); - return evalShaHandleReloadScript(context, keys, argv); + return evalShaHandleReloadScript(context, keys, argv); } private Object callEvalDynamicList(MethodContext context, Object[] objects) { @@ -62,7 +88,7 @@ private Object callEvalDynamicList(MethodContext context, Object[] objects) { } } - return evalShaHandleReloadScript(context, keys, argv); + return evalShaHandleReloadScript(context, keys, argv); } private Object evalShaHandleReloadScript(MethodContext context, List keys, List argv) { diff --git a/rdbi-core/src/main/java/com/lithium/dbi/rdbi/MethodNoOpInterceptor.java b/rdbi-core/src/main/java/com/lithium/dbi/rdbi/MethodNoOpInterceptor.java deleted file mode 100644 index 220846f9..00000000 --- a/rdbi-core/src/main/java/com/lithium/dbi/rdbi/MethodNoOpInterceptor.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.lithium.dbi.rdbi; - -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; - -import java.lang.reflect.Method; - -public class MethodNoOpInterceptor implements MethodInterceptor { - @Override - public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { - return null; - } -} diff --git a/rdbi-core/src/main/java/com/lithium/dbi/rdbi/ProxyFactory.java b/rdbi-core/src/main/java/com/lithium/dbi/rdbi/ProxyFactory.java index 669092ca..763c8a59 100644 --- a/rdbi-core/src/main/java/com/lithium/dbi/rdbi/ProxyFactory.java +++ b/rdbi-core/src/main/java/com/lithium/dbi/rdbi/ProxyFactory.java @@ -1,76 +1,84 @@ package com.lithium.dbi.rdbi; import io.opentelemetry.api.trace.Tracer; -import net.sf.cglib.proxy.Callback; -import net.sf.cglib.proxy.CallbackFilter; -import net.sf.cglib.proxy.Enhancer; -import net.sf.cglib.proxy.Factory; -import net.sf.cglib.proxy.MethodInterceptor; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.matcher.ElementMatchers; import redis.clients.jedis.Jedis; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; class ProxyFactory { - private static final MethodInterceptor NO_OP = new MethodNoOpInterceptor(); - private static final CallbackFilter FINALIZE_FILTER = new FinalizeFilter(); + final Map, Class> cache = new ConcurrentHashMap<>(); + final Map, Map> methodContextCache = new ConcurrentHashMap<>(); - final ConcurrentMap, Factory> factoryCache; - - final ConcurrentMap, Map> methodContextCache; - - private final Factory jedisInterceptorFactory; + Jedis attachJedis(final Jedis jedis, Tracer tracer) { + return JedisWrapperMethodInterceptor.newInstance(jedis, tracer); + } - ProxyFactory() { - factoryCache = new ConcurrentHashMap<>(); - methodContextCache = new ConcurrentHashMap<>(); - jedisInterceptorFactory = JedisWrapperMethodInterceptor.newFactory(); + @SuppressWarnings("unchecked") + T createInstance(final Jedis jedis, final Class t) { + try { + MethodContextInterceptor interceptor = new MethodContextInterceptor(jedis, getMethodMethodContextMap(t, jedis)); + Object instance = get(t).getDeclaredConstructor().newInstance(); + final Field field = instance.getClass().getDeclaredField("handler"); + field.set(instance, interceptor); + return (T) instance; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException | + NoSuchFieldException e) { + throw new RuntimeException(e); + } } - JedisWrapperDoNotUse attachJedis(final Jedis jedis, Tracer tracer) { - return JedisWrapperMethodInterceptor.newInstance(jedisInterceptorFactory, jedis, tracer); + boolean isCached(Class t) { + return cache.get(t) != null; } - @SuppressWarnings("unchecked") - T createInstance(final Jedis jedis, final Class t) { - Factory factory; - if (factoryCache.containsKey(t)) { - return (T) factoryCache.get(t).newInstance(new Callback[]{NO_OP,new MethodContextInterceptor(jedis, methodContextCache.get(t))}); + private Class get(Class t) throws IllegalAccessException, InstantiationException { + Class cached = cache.get(t); + if (cached != null) { + return cached; } else { + Class newClass = buildClass(t); + cache.putIfAbsent(t, newClass); + return cache.get(t); + } + } - try { - buildMethodContext(t, jedis); - } catch (InstantiationException | IllegalAccessException e) { - throw new RuntimeException(e); - } + private Class buildClass(Class t) throws IllegalAccessException, InstantiationException { - Enhancer e = new Enhancer(); - e.setSuperclass(t); - e.setCallbacks(new Callback[]{NO_OP,NO_OP}); //this will be overriden anyway, we set 2 so that it valid for FINALIZE_FILTER - e.setCallbackFilter(FINALIZE_FILTER); - factory = (Factory) e.create(); - factoryCache.putIfAbsent(t, factory); - return (T) factory.newInstance(new Callback[]{NO_OP, new MethodContextInterceptor(jedis, methodContextCache.get(t))}); - } - } + return new ByteBuddy() + .subclass(t, ConstructorStrategy.Default.DEFAULT_CONSTRUCTOR) + .defineField("handler", MethodContextInterceptor.class, Visibility.PUBLIC) + .method(ElementMatchers.any()) + .intercept(MethodDelegation.toField("handler")) + .make() + .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) + .getLoaded(); - private void buildMethodContext(Class t, Jedis jedis) throws IllegalAccessException, InstantiationException { + } - if (methodContextCache.containsKey(t)) { - return; + private Map getMethodMethodContextMap(Class t, Jedis jedis) throws InstantiationException, IllegalAccessException { + Map cached = methodContextCache.get(t); + if (cached != null) { + return cached; } Map contexts = new HashMap<>(); - for (Method method : t.getDeclaredMethods()) { - Query query = method.getAnnotation(Query.class); String queryStr = query.value(); @@ -85,19 +93,23 @@ private void buildMethodContext(Class t, Jedis jedis) throws IllegalAcces } Mapper methodMapper = method.getAnnotation(Mapper.class); - ResultMapper mapper = null; + ResultMapper mapper = null; if (methodMapper != null) { - mapper = methodMapper.value().newInstance(); + try { + mapper = methodMapper.value().getDeclaredConstructor().newInstance(); + } catch (InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } } - contexts.put(method, new MethodContext(sha1, mapper, luaContext)); } - - methodContextCache.putIfAbsent(t, contexts); + methodContextCache.putIfAbsent(t, Collections.unmodifiableMap(contexts)); + return methodContextCache.get(t); } /** * If the method does not have @Bind or @BindKey it is assumed to be a call without script bindings + * * @param method the function to check on * @return true if the method is considered not to have any bindings needed */ @@ -105,16 +117,4 @@ private boolean isRawMethod(Method method) { return (method.getParameterTypes().length == 0) || (method.getParameterTypes()[0] == List.class); } - - private static class FinalizeFilter implements CallbackFilter { - @Override - public int accept(Method method) { - if (method.getName().equals("finalize") && - method.getParameterTypes().length == 0 && - method.getReturnType() == Void.TYPE) { - return 0; //the NO_OP method interceptor - } - return 1; //the everything else method interceptor - } - } } diff --git a/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIGeneralTest.java b/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIGeneralTest.java index 9309f53d..4371345d 100644 --- a/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIGeneralTest.java +++ b/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIGeneralTest.java @@ -3,6 +3,8 @@ import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; +import static org.testng.Assert.assertEquals; + /** * User: phutwo * Date: 9/8/13 @@ -13,12 +15,15 @@ public class RDBIGeneralTest { public static void main(String[] args) { RDBI rdbi = new RDBI(new JedisPool("localhost", 6379)); - Handle handle = rdbi.open(); + try (Handle handle = rdbi.open()) { + Jedis jedis = handle.jedis(); + jedis.set("hey", "now"); + } - try { + try (Handle handle = rdbi.open()) { Jedis jedis = handle.jedis(); - } finally { - handle.close(); + String hey = jedis.get("hey"); + assertEquals(hey, "now"); } } } diff --git a/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIOpenRawTest.java b/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIOpenRawTest.java index bb0a05ab..38671a9b 100644 --- a/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIOpenRawTest.java +++ b/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIOpenRawTest.java @@ -9,7 +9,7 @@ public class RDBIOpenRawTest { @Test public void testNormalOpen() { - RDBI rdbi = new RDBI(RDBITest.getJedisPool()); + RDBI rdbi = new RDBI(RDBITest.getMockJedisPool()); Handle jedis = rdbi.open(); diff --git a/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIOpenTest.java b/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIOpenTest.java index d6f543ed..a10d4df4 100644 --- a/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIOpenTest.java +++ b/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIOpenTest.java @@ -6,9 +6,9 @@ public class RDBIOpenTest { - static interface TestDAO { + interface TestDAO { @Query("doesn't matter") - public int doesntMatter(); + int doesntMatter(); } @Test(expectedExceptions = JedisException.class) @@ -17,7 +17,7 @@ public void testOpenThrow() { rdbi.open().attach(TestDAO.class); } - @Test(expectedExceptions = Exception.class) + @Test(expectedExceptions = RuntimeException.class) public void testOpenThrowRuntimeException() { RDBI rdbi = new RDBI(RDBITest.getBadJedisPoolWithRuntimeException()); rdbi.open().attach(TestDAO.class); diff --git a/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBITest.java b/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBITest.java index 0032d47b..39f1e601 100644 --- a/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBITest.java +++ b/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBITest.java @@ -15,12 +15,13 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; public class RDBITest { - interface TestDAO { + public interface TestDAO { @Query( "redis.call('SET', KEYS[1], ARGV[1]);" + "return 0;" @@ -28,7 +29,7 @@ interface TestDAO { int testExec(List keys, List args); } - interface TestCopyDAO { + public interface TestCopyDAO { @Query( "redis.call('SET', KEYS[1], ARGV[1]);" + "return 0;" @@ -36,19 +37,19 @@ interface TestCopyDAO { int testExec2(List keys, List args); } - interface NoInputDAO { + public interface NoInputDAO { @Query("return 0;") int noInputMethod(); } - interface DynamicDAO { + public interface DynamicDAO { @Query( "redis.call('SET', $a$, $b$); return 0;" ) int testExec(@BindKey("a") String a, @BindArg("b") String b); } - static class BasicObjectUnderTest { + public static class BasicObjectUnderTest { private final String input; @@ -60,13 +61,13 @@ public String getInput() { return input; } } - static class BasicResultMapper implements ResultMapper { + public static class BasicResultMapper implements ResultMapper { @Override public BasicObjectUnderTest map(Integer result) { return new BasicObjectUnderTest(result); } } - static interface TestDAOWithResultSetMapper { + public interface TestDAOWithResultSetMapper { @Query( "redis.call('SET', KEYS[1], ARGV[1]);" + @@ -86,8 +87,7 @@ public void testExceptionThrownInRDBIAttach() { fail("Should have thrown exception for loadScript error"); } catch (RuntimeException e) { //expected - assertFalse(rdbi.proxyFactory.factoryCache.containsKey(TestCopyDAO.class)); - assertFalse(rdbi.proxyFactory.methodContextCache.containsKey(TestCopyDAO.class)); + assertFalse(rdbi.proxyFactory.isCached(TestCopyDAO.class)); } finally { handle.close(); } @@ -102,7 +102,7 @@ public void testExceptionThrownInNormalGet() { handle.jedis().get("hello"); fail("Should have thrown exception on get"); } catch (Exception e) { - //expected + //expected // hmm i don't think this is right } finally { handle.close(); } @@ -112,7 +112,7 @@ public void testExceptionThrownInNormalGet() { @SuppressWarnings("unchecked") public void testBasicAttachRun() { - RDBI rdbi = new RDBI(getJedisPool()); + RDBI rdbi = new RDBI(getMockJedisPool()); Handle handle1 = rdbi.open(); try { @@ -128,9 +128,7 @@ public void testBasicAttachRun() { handle2.close(); } - assertTrue(rdbi.proxyFactory.factoryCache.containsKey(TestDAO.class)); - assertTrue(rdbi.proxyFactory.methodContextCache.containsKey(TestDAO.class)); - + assertTrue(rdbi.proxyFactory.isCached(TestDAO.class)); Handle handle3 = rdbi.open(); try { @@ -142,7 +140,7 @@ public void testBasicAttachRun() { @Test public void testAttachWithResultSetMapper() { - RDBI rdbi = new RDBI(getJedisPool()); + RDBI rdbi = new RDBI(getMockJedisPool()); Handle handle = rdbi.open(); try { @@ -156,7 +154,7 @@ public void testAttachWithResultSetMapper() { @Test public void testMethodWithNoInput() { - RDBI rdbi = new RDBI(getJedisPool()); + RDBI rdbi = new RDBI(getMockJedisPool()); Handle handle = rdbi.open(); try { @@ -169,33 +167,30 @@ public void testMethodWithNoInput() { @Test public void testDynamicDAO() { - RDBI rdbi = new RDBI(getJedisPool()); - Handle handle = rdbi.open(); + RDBI rdbi = new RDBI(getMockJedisPool()); - try { + try (Handle handle = rdbi.open()) { handle.attach(DynamicDAO.class).testExec("a", "b"); - } finally { - handle.close(); } } @Test public void testCacheHitDAO() { - RDBI rdbi = new RDBI(getJedisPool()); + RDBI rdbi = new RDBI(getMockJedisPool()); Handle handle = rdbi.open(); try { for (int i = 0; i < 2; i++) { handle.attach(DynamicDAO.class).testExec("a", "b"); } - assertTrue(rdbi.proxyFactory.factoryCache.containsKey(DynamicDAO.class)); + assertTrue(rdbi.proxyFactory.isCached(DynamicDAO.class)); } finally { handle.close(); } } @SuppressWarnings("unchecked") - static JedisPool getJedisPool() { + static JedisPool getMockJedisPool() { Jedis jedis = mock(Jedis.class); when(jedis.scriptLoad(anyString())).thenReturn("my-sha1-hash"); when(jedis.evalsha(anyString(), anyList(), anyList())).thenReturn(0); diff --git a/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIWithHandleTest.java b/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIWithHandleTest.java index 086172c3..f194b4b7 100644 --- a/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIWithHandleTest.java +++ b/rdbi-core/src/test/java/com/lithium/dbi/rdbi/RDBIWithHandleTest.java @@ -8,11 +8,13 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNull; import static org.testng.Assert.fail; -public class RDBIWithHandleTest { +public class +RDBIWithHandleTest { - static interface TestDAO { + public interface TestDAO { @Query( "redis.call('SET', KEYS[1], ARGV[1]);" + "return 0;" @@ -22,14 +24,11 @@ static interface TestDAO { @Test public void testBasicWithHandle() { - RDBI rdbi = new RDBI(RDBITest.getJedisPool()); + RDBI rdbi = new RDBI(RDBITest.getMockJedisPool()); - rdbi.withHandle(new Callback() { - @Override - public Object run(Handle handle) { - assertEquals(handle.attach(TestDAO.class).testExec(Collections.singletonList("hello"), Collections.singletonList("world")), 0); - return null; - } + rdbi.withHandle(handle -> { + assertEquals(handle.attach(TestDAO.class).testExec(Collections.singletonList("hello"), Collections.singletonList("world")), 0); + return null; }); } @@ -38,16 +37,12 @@ void testBasicWithHandleFailure() { RDBI rdbi = new RDBI(RDBITest.getBadJedisPool()); try { - rdbi.withHandle(new Callback() { - @Override - public Object run(Handle handle) { - handle.attach(TestDAO.class); - return null; - } + rdbi.withHandle(handle -> { + handle.attach(TestDAO.class); + return null; }); } catch (RuntimeException e) { - assertFalse(rdbi.proxyFactory.methodContextCache.containsKey(TestDAO.class)); - assertFalse(rdbi.proxyFactory.factoryCache.containsKey(TestDAO.class)); + assertFalse(rdbi.proxyFactory.isCached(TestDAO.class)); } } @@ -56,13 +51,10 @@ void testBasicWithRuntimeException() { RDBI rdbi = new RDBI(RDBITest.getBadJedisPool()); - rdbi.withHandle(new Callback() { - @Override - public Object run(Handle handle) { - handle.jedis().get("hello"); - fail("Should have thrown exception on get"); - return null; - } + rdbi.withHandle(handle -> { + handle.jedis().get("hello"); + fail("Should have thrown exception on get"); + return null; }); } } diff --git a/rdbi-parent/pom.xml b/rdbi-parent/pom.xml index 22cf2b05..98e897ec 100644 --- a/rdbi-parent/pom.xml +++ b/rdbi-parent/pom.xml @@ -56,11 +56,6 @@ guava ${guava.version} - - cglib - cglib-nodep - 3.2.4 - redis.clients jedis @@ -112,11 +107,16 @@ opentelemetry-api 1.4.1 + + net.bytebuddy + byte-buddy + LATEST + org.testng testng - 7.4.0 + 7.5 test @@ -164,7 +164,7 @@ org.apache.maven.plugins maven-failsafe-plugin - 2.20 + 2.22.2 diff --git a/rdbi-recipes/pom.xml b/rdbi-recipes/pom.xml index 80df0301..f9364af3 100644 --- a/rdbi-recipes/pom.xml +++ b/rdbi-recipes/pom.xml @@ -90,6 +90,11 @@ assertj-core test + + org.awaitility + awaitility + test + diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/ratelimiter/TokenBucketRateLimiterTest.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/ratelimiter/TokenBucketRateLimiterTest.java index 2b7f7ce7..fceae9cb 100644 --- a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/ratelimiter/TokenBucketRateLimiterTest.java +++ b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/ratelimiter/TokenBucketRateLimiterTest.java @@ -3,7 +3,7 @@ import com.lithium.dbi.rdbi.Callback; import com.lithium.dbi.rdbi.Handle; import com.lithium.dbi.rdbi.RDBI; -import com.lithium.dbi.rdbi.TestClock; +import com.lithium.dbi.rdbi.testutil.TestClock; import org.testng.annotations.Test; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/cache/RedisCacheInvalidateTest.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/cache/RedisCacheInvalidateTest.java index 4c96e98f..9ec06c1a 100644 --- a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/cache/RedisCacheInvalidateTest.java +++ b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/cache/RedisCacheInvalidateTest.java @@ -28,7 +28,7 @@ public class RedisCacheInvalidateTest { private static AtomicInteger sharedMutableState; @BeforeClass - public static void setUp() throws Exception { + public void setUp() { jedisPool = new JedisPool("localhost", 6379); final RDBI rdbi = new RDBI(jedisPool); @@ -84,7 +84,7 @@ public String encode(Integer value) { } @AfterClass - public static void tearDown() throws Exception { + public void tearDown() { jedisPool.close(); } diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/cache/RedisHashCacheTest.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/cache/RedisHashCacheTest.java index 1badede9..91dfef12 100644 --- a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/cache/RedisHashCacheTest.java +++ b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/cache/RedisHashCacheTest.java @@ -379,19 +379,19 @@ public void testRemove() throws ExecutionException { cache.invalidateAll(); // cache contains expected keys - assertEquals(originalValueForKey1, cache.get(key1)); - assertEquals(originalValueForKey2, cache.get(key2)); + assertEquals(cache.get(key1), originalValueForKey1); + assertEquals(cache.get(key2), originalValueForKey2); // manipulate the data source for key1 final TestContainer newValueForKey1 = new TestContainer(key1, UUID.randomUUID(), "test-remove-new-value-for-key-1"); dataSource.put(key1, newValueForKey1); - assertEquals(originalValueForKey1, cache.get(key1)); + assertEquals(cache.get(key1), originalValueForKey1); // invalidate the key ... should reload on next request cache.invalidate(key1); assertTrue(cache.getMissing().contains(key1)); - assertEquals(newValueForKey1, cache.get(key1)); - assertEquals(originalValueForKey2, cache.get(key2)); + assertEquals(cache.get(key1), newValueForKey1); + assertEquals(cache.get(key2), originalValueForKey2); assertTrue(cache.getMissing().isEmpty()); // manipulate the data source for key1 diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/channel/ChannelLuaReceiverTest.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/channel/ChannelLuaReceiverTest.java index 705c0f8f..3806a617 100644 --- a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/channel/ChannelLuaReceiverTest.java +++ b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/channel/ChannelLuaReceiverTest.java @@ -14,9 +14,12 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.IntStream; +import static com.lithium.dbi.rdbi.testutil.Utils.assertTiming; import static java.util.stream.Collectors.toList; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertTrue; @@ -162,7 +165,7 @@ public void testEmptyChannelPublishAndReceive() throws Exception { } @Test - public void testMultiThreadedMultiChannelPublishAndReceive() throws InterruptedException { + public void testMultiThreadedMultiChannelPublishAndReceive() { final Set channelSet = ImmutableSet.of("channel1", "channel2", "channel3", "channel4", "channel5"); final int messageAmount = 50; @@ -171,55 +174,24 @@ public void testMultiThreadedMultiChannelPublishAndReceive() throws InterruptedE final ChannelPublisher channelPublisher = new ChannelPublisher(rdbi); channelPublisher.resetChannels(channelSet); - final AtomicBoolean thread1Finished = new AtomicBoolean(false); - final AtomicBoolean thread2Finished = new AtomicBoolean(false); - Map uuidMap = new HashMap<>(); - Thread thread1 = new Thread(new Runnable() { - @Override - public void run() { - for (int i = 0; i < messageAmount; i++) { - String stringVal = "value" + UUID.randomUUID(); - uuidMap.put(stringVal, 0); - final List value = ImmutableList.of(stringVal); - channelPublisher.publish(channelSet, value); - - if (Thread.interrupted()) { - return; - } - } - thread1Finished.set(true); - } - }); - - Thread thread2 = new Thread(new Runnable() { - @Override - public void run() { - for (int i = 0; i < messageAmount; i++) { - String stringVal = "value" + UUID.randomUUID(); - uuidMap.put(stringVal, 0); - final List value = ImmutableList.of(stringVal); - channelPublisher.publish(channelSet, value); - - if (Thread.interrupted()) { - return; - } - } - thread2Finished.set(true); - } - }); - - thread1.start(); - thread2.start(); - - long timeToFinish = 1500; - thread1.join(timeToFinish); - thread2.join(timeToFinish); - - if (!thread1Finished.get() && !thread2Finished.get()) { - fail("Did not finish in time"); - } + assertTiming(1500L, TimeUnit.MILLISECONDS, + () -> IntStream.range(0, messageAmount) + .forEach(i -> { + String stringVal = "value" + UUID.randomUUID(); + uuidMap.put(stringVal, 0); + final List value = ImmutableList.of(stringVal); + channelPublisher.publish(channelSet, value); + }), + () -> IntStream.range(0, messageAmount) + .forEach(i -> { + String stringVal = "value" + UUID.randomUUID(); + uuidMap.put(stringVal, 0); + final List value = ImmutableList.of(stringVal); + channelPublisher.publish(channelSet, value); + }) + ); final List channels = ImmutableList.of("channel1", "channel2", "channel3", "channel4", "channel5"); diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/channel/ChannelPublisherTest.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/channel/ChannelPublisherTest.java index 86404ea8..6aa1bc55 100644 --- a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/channel/ChannelPublisherTest.java +++ b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/channel/ChannelPublisherTest.java @@ -7,11 +7,12 @@ import org.testng.annotations.Test; import redis.clients.jedis.JedisPool; -import java.time.Instant; import java.util.List; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; +import static com.lithium.dbi.rdbi.testutil.Utils.assertTiming; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertNotNull; import static org.testng.AssertJUnit.assertNull; @@ -28,37 +29,12 @@ public void testPublishChannelLuaPerformanceTest() throws InterruptedException { final ChannelPublisher channelPublisher = new ChannelPublisher(new RDBI(new JedisPool("localhost", 6379))); channelPublisher.resetChannels(channel); - final List value =ImmutableList.of("value1"); - final AtomicBoolean thread1Finished = new AtomicBoolean(false); - final AtomicBoolean thread2Finished = new AtomicBoolean(false); - - Thread thread1 = new Thread(() -> { - for ( int i = 0; i < 1000; i++) { - channelPublisher.publish(channel, value ); - - if (Thread.interrupted()) { - return; - } - } - thread1Finished.set(true); - }); - Thread thread2 = new Thread(() -> { - for ( int i = 0; i < 1000; i++) { - channelPublisher.publish(channel, value ); - } - thread2Finished.set(true); - }); - - thread1.start(); - thread2.start(); - - long timeToFinish = 1500; - thread1.join(timeToFinish); - thread2.join(timeToFinish); - - if (!thread1Finished.get() && !thread2Finished.get()) { - fail("Did not finish in time"); - } + final List value = ImmutableList.of("value1"); + + assertTiming(1500, TimeUnit.MILLISECONDS, + () -> IntStream.range(0, 1000).forEach(i -> channelPublisher.publish(channel, value)), + () -> IntStream.range(0, 1000).forEach(i -> channelPublisher.publish(channel, value)) + ); } @Test @@ -149,7 +125,7 @@ public void getDepthTest() { } @Test - public void testPublishChannelPerformanceTest() throws InterruptedException { + public void testPublishChannelPerformanceTest() { final Set channel = ImmutableSet.of("channel1", "channel2", "channel3", "channel4", "channel5"); @@ -157,81 +133,19 @@ public void testPublishChannelPerformanceTest() throws InterruptedException { final ChannelPublisher channelPublisher = new ChannelPublisher(rdbi); channelPublisher.resetChannels(channel); - final List value =ImmutableList.of("value1"); - final AtomicBoolean thread1Finished = new AtomicBoolean(false); - final AtomicBoolean thread2Finished = new AtomicBoolean(false); - - Thread thread1 = new Thread(new Runnable() { - @Override - public void run() { - for ( int i = 0; i < 1000; i++) { - channelPublisher.publish(channel, value ); - - if (Thread.interrupted()) { - return; - } - } - thread1Finished.set(true); - } - }); - Thread thread2 = new Thread(new Runnable() { - @Override - public void run() { - for ( int i = 0; i < 1000; i++) { - channelPublisher.publish(channel, value ); - } - thread2Finished.set(true); - } - }); - - thread1.start(); - thread2.start(); - - long timeToFinish = 1500; - thread1.join(timeToFinish); - thread2.join(timeToFinish); - - if (!thread1Finished.get() || !thread2Finished.get()) { - fail("Did not finish in time"); - } + final List value = ImmutableList.of("value1"); + + assertTiming(1500, TimeUnit.MILLISECONDS, + () -> IntStream.range(0, 1000).forEach(i -> channelPublisher.publish(channel, value)), + () -> IntStream.range(0, 1000).forEach(i -> channelPublisher.publish(channel, value)) + ); final ChannelReceiver receiver = new ChannelLuaReceiver(rdbi); - final AtomicBoolean thread3Finished = new AtomicBoolean(false); - final AtomicBoolean thread4Finished = new AtomicBoolean(false); - - - Thread thread3 = new Thread(new Runnable() { - @Override - public void run() { - for (int i = 0; i < 100; i++) { - receiver.get(channel.iterator().next(), 900L); - } - thread3Finished.set(true); - } - }); - - Thread thread4 = new Thread(new Runnable() { - @Override - public void run() { - for (int i = 0; i < 100; i++) { - receiver.get(channel.iterator().next(), 900L); - } - thread4Finished.set(true); - } - }); - - Instant before = Instant.now(); - thread3.start(); - thread4.start(); - - thread3.join(timeToFinish); - thread4.join(timeToFinish); - Instant after = Instant.now(); - - if (!thread3Finished.get() || !thread3Finished.get()) { - fail("Did not finish in time"); - } - System.out.println("final time " + (after.toEpochMilli() - before.toEpochMilli())); + assertTiming(1500, TimeUnit.MILLISECONDS, + () -> IntStream.range(0, 1000).forEach(i -> channelPublisher.publish(channel, value)), + () -> IntStream.range(0, 1000).forEach(i -> channelPublisher.publish(channel, value)) + ); + } } diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/locking/MultiReadSingleWriteLockTest.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/locking/MultiReadSingleWriteLockTest.java index 12c7a007..93a3f429 100644 --- a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/locking/MultiReadSingleWriteLockTest.java +++ b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/locking/MultiReadSingleWriteLockTest.java @@ -12,10 +12,10 @@ import java.time.Duration; import java.time.Instant; import java.util.List; -import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; +import static org.awaitility.Awaitility.await; import static org.testng.Assert.assertNull; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertTrue; @@ -81,16 +81,10 @@ public void testAcquireWriteLock_writeLockExpiration() throws Exception { final String newLockOwnerId = UUID.randomUUID().toString(); lock.acquireWriteLock(newLockOwnerId); assertEquals(newLockOwnerId, handle.jedis().get(writeLockKey)); - - // wait for new owner to expire and check that no one owns lock - final Instant beyondExpiration = Instant.now().plus(Duration.ofMillis(500)); - while (true) { - Thread.sleep(100); - if (Instant.now().isAfter(beyondExpiration)) { - break; - } - } - assertNull(handle.jedis().get(writeLockKey)); + await() + .atLeast(Duration.ofMillis(500)) + .atMost(Duration.ofSeconds(1)) + .untilAsserted(() -> assertNull(handle.jedis().get(writeLockKey))); } } @@ -161,14 +155,12 @@ public void testAcquireReadLock_readLockExpiration() throws Exception { assertTrue(owners.contains(lockOwnerId)); // wait for expiration and verify the lock has expired - final Instant beyondExpiration = Instant.now().plus(Duration.ofMillis(500)); - while (true) { - Thread.sleep(100); - if (Instant.now().isAfter(beyondExpiration)) { - break; - } - } - assertTrue(handle.jedis().zrangeByScore(readLockKey, Long.toString(Instant.now().toEpochMilli()), "+inf", 0, 1).isEmpty()); + await() + .atLeast(Duration.ofMillis(500)) + .atMost(Duration.ofSeconds(1)) + .untilAsserted(() -> { + assertTrue(handle.jedis().zrangeByScore(readLockKey, Long.toString(Instant.now().toEpochMilli()), "+inf", 0, 1).isEmpty()); + }); } } @@ -211,7 +203,7 @@ public void testAcquireReadLock_blockedByWrite() throws Exception { assertTrue(Instant.now().isAfter(expiration)); } - @Test (timeOut = 5000L) + @Test(timeOut = 5000L) public void testReacquireReadLock() throws Exception { final MultiReadSingleWriteLock lock = new MultiReadSingleWriteLock(rdbi, writeLockKey, diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/presence/PresenceRepositoryTest.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/presence/PresenceRepositoryTest.java index 7efd13b7..c3eb41e7 100644 --- a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/presence/PresenceRepositoryTest.java +++ b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/presence/PresenceRepositoryTest.java @@ -10,7 +10,11 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; +import static com.lithium.dbi.rdbi.testutil.Utils.assertTiming; +import static org.awaitility.Awaitility.await; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; @@ -19,7 +23,7 @@ public class PresenceRepositoryTest { @Test - public void addTest () throws InterruptedException { + public void addTest() throws InterruptedException { final PresenceRepository presenceRepository = new PresenceRepository(new RDBI(new JedisPool("localhost", 6379)), "myprefix"); @@ -44,47 +48,36 @@ public void basicPerformanceTest() throws InterruptedException { final PresenceRepository presenceRepository = new PresenceRepository(new RDBI(new JedisPool("localhost", 6379)), "myprefix"); presenceRepository.nukeForTest("mytube"); - Instant before = Instant.now(); - for ( int i = 0; i < 10000; i++ ) { - presenceRepository.addHeartbeat("mytube", "id" + i, 10 * 1000L); - } - Instant after = Instant.now(); - System.out.println("Time for 10,000 heartbeats " + Long.toString(after.toEpochMilli() - before.toEpochMilli())); - - assertTrue(after.toEpochMilli() - before.toEpochMilli() < 2000L); - - Instant before2 = Instant.now(); - for ( int i = 0; i < 10000; i++ ) { - assertFalse(presenceRepository.expired("mytube", "id" + i)); - } - Instant after2 = Instant.now(); - System.out.println("Time for 10,000 expired " + Long.toString(after2.toEpochMilli() - before2.toEpochMilli())); - - assertTrue(after2.toEpochMilli() - before2.toEpochMilli() < 2000L); - - Thread.sleep(10 * 1000L); - - Instant before3 = Instant.now(); - for ( int i = 0; i < 5000; i++ ) { - assertTrue(presenceRepository.remove("mytube", "id" + i)); - } - Instant after3 = Instant.now(); - System.out.println("Time for 5000 removes " + Long.toString(after3.toEpochMilli() - before3.toEpochMilli())); + assertTiming(2000, TimeUnit.MILLISECONDS, + () -> IntStream + .range(0, 10_000) + .forEach(i -> presenceRepository.addHeartbeat("mytube", "id" + i, 10 * 1000L)) + ); + + assertTiming(2000, TimeUnit.MILLISECONDS, + () -> IntStream + .range(0, 10_000) + .forEach(i -> assertFalse(presenceRepository.expired("mytube", "id" + i))) + ); + Thread.sleep(2_000L); + + assertTiming(2000, TimeUnit.MILLISECONDS, + () -> IntStream + .range(0, 5_000) + .forEach(i -> assertTrue(presenceRepository.remove("mytube", "id" + i))) + ); + + assertTiming(500L, TimeUnit.MILLISECONDS, + () -> presenceRepository.cull("mytube") + ); - assertTrue(after3.toEpochMilli() - before3.toEpochMilli() < 1000L); - - Instant before4 = Instant.now(); - presenceRepository.cull("mytube"); - Instant after4 = Instant.now(); - System.out.println("Time for 5000 cull " + Long.toString(after4.toEpochMilli() - before4.toEpochMilli())); - - assertTrue(after4.toEpochMilli() - before4.toEpochMilli() < 500L); } @Test - public void getPresentTest() throws InterruptedException { + public void getPresentTest() { final String mytube = "getPresentTest"; - final PresenceRepository presenceRepository = new PresenceRepository(new RDBI(new JedisPool("localhost", 6379)), "myprefix"); + RDBI rdbi = new RDBI(new JedisPool("localhost", 6379)); + final PresenceRepository presenceRepository = new PresenceRepository(rdbi, "myprefix"); presenceRepository.nukeForTest(mytube); // assert set is empty at start @@ -93,6 +86,7 @@ public void getPresentTest() throws InterruptedException { // put something in and verify we can get it back out final String uuid = UUID.randomUUID().toString(); presenceRepository.addHeartbeat(mytube, uuid, Duration.ofSeconds(1).toMillis()); + final long insertionTimeApprox = Instant.now().toEpochMilli(); final List presentSet = presenceRepository.getPresent(mytube, Optional.empty()); assertEquals(uuid, presentSet.iterator().next(), "Expected to have one heartbeat with uuid: " + uuid); @@ -102,15 +96,17 @@ public void getPresentTest() throws InterruptedException { assertEquals(stillpresentSet.iterator().next(), uuid, "Expected to still have one heartbeat with uuid: " + uuid); // wait a second and verify previous heartbeat is expired - final Instant beforeSleep = Instant.now(); - while (true) { - Thread.sleep(Duration.ofSeconds(1).toMillis()); - if (Duration.between(beforeSleep, Instant.now()).compareTo(Duration.ofSeconds(1)) > 0) { - break; - } - } - assertTrue(presenceRepository.getPresent(mytube, Optional.empty()).isEmpty()); - + await() + .atLeast(Duration.ofSeconds(1)) + .atMost(Duration.ofMillis(1250L)) + .untilAsserted( + () -> { + final long expirationCheckApprox = Instant.now().toEpochMilli(); + List tubeContents = presenceRepository.getPresent(mytube, Optional.empty()); + assertTrue(tubeContents.isEmpty(), String.format("tube contents should be empty, but are %s. inserted around %d, checked at %d." + + " should have expire at 1s", tubeContents, insertionTimeApprox, expirationCheckApprox)); + + }); // test with limit will not return full set for (int i = 0; i < 100; ++i) { presenceRepository.addHeartbeat(mytube, UUID.randomUUID().toString(), Duration.ofMinutes(1).toMillis()); @@ -140,14 +136,10 @@ public void getExpiredTest() throws InterruptedException { assertEquals(stillpresentSet.iterator().next(), uuid, "Expected to still have one heartbeat with uuid: " + uuid); // wait a second and verify previous heartbeat is expired - final Instant beforeSleep = Instant.now(); - while (true) { - Thread.sleep(Duration.ofSeconds(1).toMillis()); - if (Duration.between(beforeSleep, Instant.now()).compareTo(Duration.ofSeconds(1)) > 0) { - break; - } - } - assertFalse(presenceRepository.getExpired(mytube, Optional.empty()).isEmpty()); + await() + .atLeast(Duration.ofSeconds(1)) + .atMost(Duration.ofMillis(1250L)) + .untilAsserted(() -> assertFalse(presenceRepository.getExpired(mytube, Optional.empty()).isEmpty())); // test with limit will not return full set for (int i = 0; i < 100; ++i) { diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/ExclusiveJobSchedulerTest.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/ExclusiveJobSchedulerTest.java index 8a775ccc..5443fe2a 100644 --- a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/ExclusiveJobSchedulerTest.java +++ b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/ExclusiveJobSchedulerTest.java @@ -4,7 +4,7 @@ import com.lithium.dbi.rdbi.Callback; import com.lithium.dbi.rdbi.Handle; import com.lithium.dbi.rdbi.RDBI; -import com.lithium.dbi.rdbi.testutil.TubeUtils; +import com.lithium.dbi.rdbi.testutil.Utils; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -30,7 +30,7 @@ public class ExclusiveJobSchedulerTest { @BeforeMethod public void setup(){ - tubeName = TubeUtils.uniqueTubeName(); + tubeName = Utils.uniqueTubeName(); scheduledJobSystem = new ExclusiveJobScheduler(rdbi, "myprefix:"); } diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/MultiChannelSchedulerTest.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/MultiChannelSchedulerTest.java index 337f4606..df7715b4 100644 --- a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/MultiChannelSchedulerTest.java +++ b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/MultiChannelSchedulerTest.java @@ -2,7 +2,7 @@ import com.lithium.dbi.rdbi.Handle; import com.lithium.dbi.rdbi.RDBI; -import com.lithium.dbi.rdbi.TestClock; +import com.lithium.dbi.rdbi.testutil.TestClock; import org.testng.annotations.AfterMethod; import org.testng.annotations.Test; import redis.clients.jedis.JedisPool; diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/PriorityBasedJobSchedulerTest.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/PriorityBasedJobSchedulerTest.java index 1ff1be03..b2d500fc 100644 --- a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/PriorityBasedJobSchedulerTest.java +++ b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/PriorityBasedJobSchedulerTest.java @@ -2,7 +2,7 @@ import com.lithium.dbi.rdbi.Callback; import com.lithium.dbi.rdbi.RDBI; -import com.lithium.dbi.rdbi.testutil.TubeUtils; +import com.lithium.dbi.rdbi.testutil.Utils; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -25,7 +25,7 @@ public class PriorityBasedJobSchedulerTest { @BeforeMethod public void setup(){ - tubeName = TubeUtils.uniqueTubeName(); + tubeName = Utils.uniqueTubeName(); scheduledJobSystem = new PriorityBasedJobScheduler(rdbi, "myprefix:"); } diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/StateDedupedJobSchedulerTest.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/StateDedupedJobSchedulerTest.java index d7814ed9..f4d4d5c6 100644 --- a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/StateDedupedJobSchedulerTest.java +++ b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/StateDedupedJobSchedulerTest.java @@ -1,8 +1,8 @@ package com.lithium.dbi.rdbi.recipes.scheduler; import com.lithium.dbi.rdbi.RDBI; -import com.lithium.dbi.rdbi.TestClock; -import com.lithium.dbi.rdbi.testutil.TubeUtils; +import com.lithium.dbi.rdbi.testutil.TestClock; +import com.lithium.dbi.rdbi.testutil.Utils; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -27,7 +27,7 @@ public class StateDedupedJobSchedulerTest { @BeforeMethod public void setup() { - tubeName = TubeUtils.uniqueTubeName(); + tubeName = Utils.uniqueTubeName(); scheduledJobSystem = new StateDedupedJobScheduler(rdbi, "myprefix:"); } diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/TimeBasedJobSchedulerTest.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/TimeBasedJobSchedulerTest.java index 11597dd0..10c7b034 100644 --- a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/TimeBasedJobSchedulerTest.java +++ b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/recipes/scheduler/TimeBasedJobSchedulerTest.java @@ -3,7 +3,7 @@ import com.google.common.collect.ImmutableSet; import com.lithium.dbi.rdbi.Callback; import com.lithium.dbi.rdbi.RDBI; -import com.lithium.dbi.rdbi.testutil.TubeUtils; +import com.lithium.dbi.rdbi.testutil.Utils; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -33,7 +33,7 @@ public class TimeBasedJobSchedulerTest { @BeforeMethod public void setup(){ - tubeName = TubeUtils.uniqueTubeName(); + tubeName = Utils.uniqueTubeName(); scheduledJobSystem = new TimeBasedJobScheduler(rdbi, "myprefix:"); } diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/TestClock.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/testutil/TestClock.java similarity index 90% rename from rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/TestClock.java rename to rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/testutil/TestClock.java index 736a30f2..72e2fbd5 100644 --- a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/TestClock.java +++ b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/testutil/TestClock.java @@ -1,4 +1,4 @@ -package com.lithium.dbi.rdbi; +package com.lithium.dbi.rdbi.testutil; import java.util.function.LongSupplier; diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/testutil/TubeUtils.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/testutil/TubeUtils.java deleted file mode 100644 index 7702605d..00000000 --- a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/testutil/TubeUtils.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.lithium.dbi.rdbi.testutil; - -import java.util.UUID; - -public class TubeUtils { - - public static String uniqueTubeName() { - return "test_tube_" + UUID.randomUUID().toString(); - } -} diff --git a/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/testutil/Utils.java b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/testutil/Utils.java new file mode 100644 index 00000000..b0b13671 --- /dev/null +++ b/rdbi-recipes/src/test/java/com/lithium/dbi/rdbi/testutil/Utils.java @@ -0,0 +1,45 @@ +package com.lithium.dbi.rdbi.testutil; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import static org.testng.AssertJUnit.fail; + +public class Utils { + + public static String uniqueTubeName() { + return "test_tube_" + UUID.randomUUID().toString(); + } + + public static void assertTiming(long timeLimit, TimeUnit unit, Runnable... runnables) { + long timeOut = timeLimit * 10; + long start = System.currentTimeMillis(); + + + List> futures = Arrays.stream(runnables) + .map(CompletableFuture::runAsync) + .collect(Collectors.toList()); + + CompletableFuture all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])); + + try { + all.get(timeOut, unit); + } catch (TimeoutException e) { + fail(String.format("did not complete within time limit %d %s, or timeout %d %s", timeLimit, unit, timeOut, unit)); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + + long elapsed = System.currentTimeMillis() - start; + if (elapsed > timeLimit) { + fail(String.format("Did not finish in time. Expected to finish in %d %s, but finished in %d", timeLimit, unit, elapsed)); + } + } + +}