diff options
author | gjoranv <gv@verizonmedia.com> | 2022-07-13 11:38:09 +0200 |
---|---|---|
committer | gjoranv <gv@verizonmedia.com> | 2022-07-13 11:39:09 +0200 |
commit | 3ad0117c5ec96bb3d32495a222409df074cfd7d9 (patch) | |
tree | 3f2d0024cbf719b8cd38365016e1dd49754b5a48 /vespajlib | |
parent | 0781a0b3ce6b7433f442d3a1bc3097272971f0ba (diff) |
Move yolean code into vespajlib.
Diffstat (limited to 'vespajlib')
31 files changed, 2237 insertions, 0 deletions
diff --git a/vespajlib/abi-spec.json b/vespajlib/abi-spec.json index 2c861dd2f1f..97eaa1f76f6 100644 --- a/vespajlib/abi-spec.json +++ b/vespajlib/abi-spec.json @@ -3637,5 +3637,335 @@ "public abstract com.yahoo.vespa.objects.Serializer put(com.yahoo.vespa.objects.FieldBase, java.lang.String)" ], "fields": [] + }, + "com.yahoo.yolean.Exceptions$RunnableThrowingIOException": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract void run()" + ], + "fields": [] + }, + "com.yahoo.yolean.Exceptions$RunnableThrowingInterruptedException": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract void run()" + ], + "fields": [] + }, + "com.yahoo.yolean.Exceptions$SupplierThrowingIOException": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract java.lang.Object get()" + ], + "fields": [] + }, + "com.yahoo.yolean.Exceptions": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public" + ], + "methods": [ + "public void <init>()", + "public static java.lang.String toMessageString(java.lang.Throwable)", + "public static java.util.Optional findCause(java.lang.Throwable, java.lang.Class)", + "public static void uncheck(com.yahoo.yolean.Exceptions$RunnableThrowingIOException)", + "public static void uncheckInterrupted(com.yahoo.yolean.Exceptions$RunnableThrowingInterruptedException)", + "public static void uncheckInterruptedAndRestoreFlag(com.yahoo.yolean.Exceptions$RunnableThrowingInterruptedException)", + "public static varargs void uncheck(com.yahoo.yolean.Exceptions$RunnableThrowingIOException, java.lang.String, java.lang.String[])", + "public static void uncheckAndIgnore(com.yahoo.yolean.Exceptions$RunnableThrowingIOException, java.lang.Class)", + "public static java.lang.Object uncheck(com.yahoo.yolean.Exceptions$SupplierThrowingIOException)", + "public static varargs java.lang.Object uncheck(com.yahoo.yolean.Exceptions$SupplierThrowingIOException, java.lang.String, java.lang.String[])", + "public static java.lang.Object uncheckAndIgnore(com.yahoo.yolean.Exceptions$SupplierThrowingIOException, java.lang.Class)", + "public static java.lang.RuntimeException throwUnchecked(java.lang.Throwable)" + ], + "fields": [] + }, + "com.yahoo.yolean.UncheckedInterruptedException": { + "superClass": "java.lang.RuntimeException", + "interfaces": [], + "attributes": [ + "public" + ], + "methods": [ + "public void <init>(java.lang.String, java.lang.InterruptedException, boolean)", + "public void <init>(java.lang.InterruptedException, boolean)", + "public void <init>(java.lang.String, boolean)", + "public void <init>(java.lang.String, java.lang.InterruptedException)", + "public void <init>(java.lang.InterruptedException)", + "public java.lang.InterruptedException getCause()", + "public bridge synthetic java.lang.Throwable getCause()" + ], + "fields": [] + }, + "com.yahoo.yolean.chain.After": { + "superClass": "java.lang.Object", + "interfaces": [ + "java.lang.annotation.Annotation" + ], + "attributes": [ + "public", + "interface", + "abstract", + "annotation" + ], + "methods": [ + "public abstract java.lang.String[] value()" + ], + "fields": [] + }, + "com.yahoo.yolean.chain.Before": { + "superClass": "java.lang.Object", + "interfaces": [ + "java.lang.annotation.Annotation" + ], + "attributes": [ + "public", + "interface", + "abstract", + "annotation" + ], + "methods": [ + "public abstract java.lang.String[] value()" + ], + "fields": [] + }, + "com.yahoo.yolean.chain.Provides": { + "superClass": "java.lang.Object", + "interfaces": [ + "java.lang.annotation.Annotation" + ], + "attributes": [ + "public", + "interface", + "abstract", + "annotation" + ], + "methods": [ + "public abstract java.lang.String[] value()" + ], + "fields": [] + }, + "com.yahoo.yolean.concurrent.ConcurrentResourcePool": { + "superClass": "java.lang.Object", + "interfaces": [ + "java.lang.Iterable" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void <init>(java.util.function.Supplier)", + "public void preallocate(int)", + "public final java.lang.Object alloc()", + "public final void free(java.lang.Object)", + "public java.util.Iterator iterator()" + ], + "fields": [] + }, + "com.yahoo.yolean.concurrent.CopyOnWriteHashMap": { + "superClass": "java.lang.Object", + "interfaces": [ + "java.util.Map" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void <init>()", + "public int size()", + "public boolean isEmpty()", + "public boolean containsKey(java.lang.Object)", + "public boolean containsValue(java.lang.Object)", + "public java.lang.Object get(java.lang.Object)", + "public java.lang.Object put(java.lang.Object, java.lang.Object)", + "public java.lang.Object remove(java.lang.Object)", + "public void putAll(java.util.Map)", + "public void clear()", + "public java.util.Set keySet()", + "public java.util.Collection values()", + "public java.util.Set entrySet()" + ], + "fields": [] + }, + "com.yahoo.yolean.concurrent.Memoized$Closer": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract void close(java.lang.Object)" + ], + "fields": [] + }, + "com.yahoo.yolean.concurrent.Memoized": { + "superClass": "java.lang.Object", + "interfaces": [ + "java.util.function.Supplier", + "java.lang.AutoCloseable" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void <init>(java.util.function.Supplier, com.yahoo.yolean.concurrent.Memoized$Closer)", + "public static com.yahoo.yolean.concurrent.Memoized of(java.util.function.Supplier)", + "public static com.yahoo.yolean.concurrent.Memoized combine(com.yahoo.yolean.concurrent.Memoized, java.util.function.Function, com.yahoo.yolean.concurrent.Memoized$Closer)", + "public java.lang.Object get()", + "public void close()" + ], + "fields": [] + }, + "com.yahoo.yolean.concurrent.ResourcePool": { + "superClass": "java.lang.Object", + "interfaces": [ + "java.lang.Iterable" + ], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void <init>(java.util.function.Supplier)", + "public java.lang.Object alloc()", + "public void free(java.lang.Object)", + "public java.util.Iterator iterator()" + ], + "fields": [] + }, + "com.yahoo.yolean.concurrent.Sleeper": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public void sleep(java.time.Duration)", + "public void sleepChecked(java.time.Duration)", + "public void sleep(long)", + "public abstract void sleepChecked(long)" + ], + "fields": [ + "public static final com.yahoo.yolean.concurrent.Sleeper DEFAULT", + "public static final com.yahoo.yolean.concurrent.Sleeper NOOP" + ] + }, + "com.yahoo.yolean.concurrent.ThreadRobustList": { + "superClass": "java.lang.Object", + "interfaces": [ + "java.lang.Iterable" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void <init>()", + "public void <init>(int)", + "public boolean isEmpty()", + "public void add(java.lang.Object)", + "public java.util.Iterator iterator()" + ], + "fields": [] + }, + "com.yahoo.yolean.function.ThrowingConsumer": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract void accept(java.lang.Object)", + "public com.yahoo.yolean.function.ThrowingConsumer andThen(com.yahoo.yolean.function.ThrowingConsumer)" + ], + "fields": [] + }, + "com.yahoo.yolean.function.ThrowingFunction": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract java.lang.Object apply(java.lang.Object)", + "public com.yahoo.yolean.function.ThrowingFunction andThen(com.yahoo.yolean.function.ThrowingFunction)", + "public com.yahoo.yolean.function.ThrowingFunction compose(com.yahoo.yolean.function.ThrowingFunction)" + ], + "fields": [] + }, + "com.yahoo.yolean.function.ThrowingSupplier": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract java.lang.Object get()" + ], + "fields": [] + }, + "com.yahoo.yolean.trace.TraceNode": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public" + ], + "methods": [ + "public void <init>(java.lang.Object, long)", + "public com.yahoo.yolean.trace.TraceNode add(com.yahoo.yolean.trace.TraceNode)", + "public java.lang.Iterable descendants(java.lang.Class)", + "public java.lang.Object payload()", + "public long timestamp()", + "public com.yahoo.yolean.trace.TraceNode parent()", + "public java.lang.Iterable children()", + "public boolean isRoot()", + "public com.yahoo.yolean.trace.TraceNode root()", + "public com.yahoo.yolean.trace.TraceVisitor accept(com.yahoo.yolean.trace.TraceVisitor)", + "public java.lang.String toString()" + ], + "fields": [] + }, + "com.yahoo.yolean.trace.TraceVisitor": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "abstract" + ], + "methods": [ + "public void <init>()", + "public abstract void visit(com.yahoo.yolean.trace.TraceNode)", + "public void entering(com.yahoo.yolean.trace.TraceNode)", + "public void leaving(com.yahoo.yolean.trace.TraceNode)" + ], + "fields": [] } }
\ No newline at end of file diff --git a/vespajlib/src/main/java/com/yahoo/yolean/Exceptions.java b/vespajlib/src/main/java/com/yahoo/yolean/Exceptions.java new file mode 100644 index 00000000000..89b4e76368b --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/Exceptions.java @@ -0,0 +1,202 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Optional; + +/** + * Helper methods for handling exceptions + * + * @author bratseth + */ +public class Exceptions { + + /** + * <p>Returns a user friendly error message string which includes information from all nested exceptions.</p> + * + * <p>The form of this string is + * <code>e.getMessage(): e.getCause().getMessage(): e.getCause().getCause().getMessage()...</code> + * In addition, some heuristics are used to clean up common cases where exception nesting causes bad messages. + */ + public static String toMessageString(Throwable t) { + StringBuilder b = new StringBuilder(); + String lastMessage = null; + String message; + for (; t != null; t = t.getCause()) { + message = getMessage(t); + if (message == null) continue; + if (message.equals(lastMessage)) continue; + if (b.length() > 0) { + b.append(": "); + } + b.append(message); + lastMessage = message; + } + return b.toString(); + } + + /** Returns a useful message from *this* exception, or null if there is nothing useful to return */ + private static String getMessage(Throwable t) { + String message = t.getMessage(); + if (t.getCause() == null) { + if (message == null) return t.getClass().getSimpleName(); + } else { + if (message == null) return null; + if (message.equals(t.getCause().getClass().getName() + ": " + t.getCause().getMessage())) return null; + } + return message; + } + + /** + * Returns the first cause or the given throwable that is an instance of {@code clazz} + */ + public static <T extends Throwable> Optional<T> findCause(Throwable t, Class<T> clazz) { + for (; t != null; t = t.getCause()) { + if (clazz.isInstance(t)) + return Optional.of(clazz.cast(t)); + } + return Optional.empty(); + } + + /** + * Wraps any IOException thrown from a runnable in an UncheckedIOException. + */ + public static void uncheck(RunnableThrowingIOException runnable) { + try { + runnable.run(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static void uncheckInterrupted(RunnableThrowingInterruptedException runnable) { + try { + runnable.run(); + } catch (InterruptedException e) { + throw new UncheckedInterruptedException(e, false); + } + } + + public static void uncheckInterruptedAndRestoreFlag(RunnableThrowingInterruptedException runnable) { + try { + runnable.run(); + } catch (InterruptedException e) { + throw new UncheckedInterruptedException(e, true); + } + } + + /** + * Wraps any IOException thrown from a runnable in an UncheckedIOException w/message. + */ + public static void uncheck(RunnableThrowingIOException runnable, String format, String... args) { + try { + runnable.run(); + } catch (IOException e) { + String message = String.format(format, (Object[]) args); + throw new UncheckedIOException(message, e); + } + } + + /** Similar to uncheck(), except an exceptionToIgnore exception is silently ignored. */ + public static <T extends IOException> void uncheckAndIgnore(RunnableThrowingIOException runnable, Class<T> exceptionToIgnore) { + try { + runnable.run(); + } catch (UncheckedIOException e) { + IOException cause = e.getCause(); + if (cause == null) throw e; + try { + cause.getClass().asSubclass(exceptionToIgnore); + } catch (ClassCastException f) { + throw e; + } + // Do nothing - OK + } catch (IOException e) { + try { + e.getClass().asSubclass(exceptionToIgnore); + } catch (ClassCastException f) { + throw new UncheckedIOException(e); + } + // Do nothing - OK + } + } + + @FunctionalInterface + public interface RunnableThrowingIOException { + void run() throws IOException; + } + + @FunctionalInterface public interface RunnableThrowingInterruptedException { void run() throws InterruptedException; } + + /** + * Wraps any IOException thrown from a supplier in an UncheckedIOException. + */ + public static <T> T uncheck(SupplierThrowingIOException<T> supplier) { + try { + return supplier.get(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Wraps any IOException thrown from a supplier in an UncheckedIOException w/message. + */ + public static <T> T uncheck(SupplierThrowingIOException<T> supplier, String format, String... args) { + try { + return supplier.get(); + } catch (IOException e) { + String message = String.format(format, (Object[]) args); + throw new UncheckedIOException(message, e); + } + } + + /** Similar to uncheck(), except null is returned if exceptionToIgnore is thrown. */ + public static <R, T extends IOException> R uncheckAndIgnore(SupplierThrowingIOException<R> supplier, Class<T> exceptionToIgnore) { + try { + return supplier.get(); + } catch (UncheckedIOException e) { + IOException cause = e.getCause(); + if (cause == null) throw e; + try { + cause.getClass().asSubclass(exceptionToIgnore); + } catch (ClassCastException f) { + throw e; + } + return null; + } catch (IOException e) { + try { + e.getClass().asSubclass(exceptionToIgnore); + } catch (ClassCastException f) { + throw new UncheckedIOException(e); + } + return null; + } + } + + @FunctionalInterface + public interface SupplierThrowingIOException<T> { + T get() throws IOException; + } + + /** + * Allows treating checked exceptions as unchecked. + * Usage: + * throw throwUnchecked(e); + * The reason for the return type is to allow writing throw at the call site + * instead of just calling throwUnchecked. Just calling throwUnchecked + * means that the java compiler won't know that the statement will throw an exception, + * and will therefore complain on things such e.g. missing return value. + */ + public static RuntimeException throwUnchecked(Throwable e) { + throwUncheckedImpl(e); + return new RuntimeException(); // Non-null return value to stop tooling from complaining about potential NPE + } + + @SuppressWarnings("unchecked") + private static <T extends Throwable> void throwUncheckedImpl(Throwable t) throws T { + throw (T)t; + } + + +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/UncheckedInterruptedException.java b/vespajlib/src/main/java/com/yahoo/yolean/UncheckedInterruptedException.java new file mode 100644 index 00000000000..d3317b5fb26 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/UncheckedInterruptedException.java @@ -0,0 +1,27 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean; + +/** + * Wraps an {@link InterruptedException} with an unchecked exception. + * + * @author bjorncs + */ +public class UncheckedInterruptedException extends RuntimeException { + + public UncheckedInterruptedException(String message, InterruptedException cause, boolean restoreInterruptFlag) { + super(message, cause); + if (restoreInterruptFlag) Thread.currentThread().interrupt(); + } + + public UncheckedInterruptedException(InterruptedException cause, boolean restoreInterruptFlags) { + this(cause.toString(), cause, restoreInterruptFlags); + } + + public UncheckedInterruptedException(String message, boolean restoreInterruptFlag) { this(message, null, false); } + + public UncheckedInterruptedException(String message, InterruptedException cause) { this(message, cause, false); } + + public UncheckedInterruptedException(InterruptedException cause) { this(cause.toString(), cause, false); } + + @Override public InterruptedException getCause() { return (InterruptedException) super.getCause(); } +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/chain/After.java b/vespajlib/src/main/java/com/yahoo/yolean/chain/After.java new file mode 100644 index 00000000000..a02408bb616 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/chain/After.java @@ -0,0 +1,23 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The component that is annotated with this must be placed later than the components or phases providing names + * contained in the given list. + * + * @author Tony Vaagenes + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface After { + + String[] value() default { }; + +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/chain/Before.java b/vespajlib/src/main/java/com/yahoo/yolean/chain/Before.java new file mode 100644 index 00000000000..7bbba8ded5f --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/chain/Before.java @@ -0,0 +1,23 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The component that is annotated with this must be placed earlier than the components or phases providing names + * contained in the given list. + * + * @author Tony Vaagenes + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface Before { + + String[] value() default { }; + +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/chain/Provides.java b/vespajlib/src/main/java/com/yahoo/yolean/chain/Provides.java new file mode 100644 index 00000000000..b8bf40686cb --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/chain/Provides.java @@ -0,0 +1,23 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * <p>Mark this component as providing some named functionality. Other components can then mark themselves as "before" + * and "after" the string provided here, to impose constraints on ordering.</p> + * + * @author Tony Vaagenes + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface Provides { + + String[] value() default { }; + +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/chain/package-info.java b/vespajlib/src/main/java/com/yahoo/yolean/chain/package-info.java new file mode 100644 index 00000000000..e767e192ad7 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/chain/package-info.java @@ -0,0 +1,6 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi package com.yahoo.yolean.chain; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/vespajlib/src/main/java/com/yahoo/yolean/concurrent/ConcurrentResourcePool.java b/vespajlib/src/main/java/com/yahoo/yolean/concurrent/ConcurrentResourcePool.java new file mode 100644 index 00000000000..0e91a44bf5d --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/concurrent/ConcurrentResourcePool.java @@ -0,0 +1,53 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.concurrent; + +import java.util.Iterator; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Supplier; + +/** + * A pool of a resource. This create new instances of the resource on request until enough are created + * to deliver a unique one to all threads needing one concurrently and then reuse those instances + * in subsequent requests. + * + * @author baldersheim + */ +public class ConcurrentResourcePool<T> implements Iterable<T> { + + private final Queue<T> pool = new ConcurrentLinkedQueue<>(); + private final Supplier<T> factory; + + + public ConcurrentResourcePool(Supplier<T> factory) { + this.factory = factory; + } + + public void preallocate(int instances) { + for (int i = 0; i < instances; i++) { + pool.offer(factory.get()); + } + } + + /** + * Allocates an instance of the resource to the requestor. + * The resource will be allocated exclusively to the requestor until it calls free(instance). + * + * @return a reused or newly created instance of the resource + */ + public final T alloc() { + T e = pool.poll(); + return e != null ? e : factory.get(); + } + + /** Frees an instance previously acquired bty alloc */ + public final void free(T e) { + pool.offer(e); + } + + @Override + public Iterator<T> iterator() { + return pool.iterator(); + } + +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMap.java b/vespajlib/src/main/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMap.java new file mode 100644 index 00000000000..536d9ab15c1 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMap.java @@ -0,0 +1,93 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.concurrent; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * <p>This is a thread hash map for small collections that are stable once built. Until it is stable there will be a + * race among all threads missing something in the map. They will then clone the map add the missing stuff and then put + * it back as active again. Here are no locks, but the cost is that inserts will happen a lot more than necessary. The + * map reference is volatile, but on most multi-cpu machines that has no cost unless modified.</p> + * + * @author baldersheim + */ +public class CopyOnWriteHashMap<K, V> implements Map<K, V> { + + private volatile HashMap<K, V> map = new HashMap<>(); + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + @Override + public V get(Object key) { + return map.get(key); + } + + @Override + public V put(K key, V value) { + HashMap<K, V> next = new HashMap<>(map); + V old = next.put(key, value); + map = next; + return old; + } + + @Override + @SuppressWarnings("SuspiciousMethodCalls") + public V remove(Object key) { + HashMap<K, V> prev = map; + if (!prev.containsKey(key)) { + return null; + } + HashMap<K, V> next = new HashMap<>(prev); + V old = next.remove(key); + map = next; + return old; + } + + @Override + public void putAll(Map<? extends K, ? extends V> m) { + HashMap<K, V> next = new HashMap<>(map); + next.putAll(m); + map = next; + } + + @Override + public void clear() { + map = new HashMap<>(); + } + + @Override + public Set<K> keySet() { + return map.keySet(); + } + + @Override + public Collection<V> values() { + return map.values(); + } + + @Override + public Set<Entry<K, V>> entrySet() { + return map.entrySet(); + } +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/concurrent/Memoized.java b/vespajlib/src/main/java/com/yahoo/yolean/concurrent/Memoized.java new file mode 100644 index 00000000000..8e2b7b7a7eb --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/concurrent/Memoized.java @@ -0,0 +1,103 @@ +package com.yahoo.yolean.concurrent; + +import com.yahoo.api.annotations.Beta; + +import java.util.function.Function; +import java.util.function.Supplier; + +import static java.util.Objects.requireNonNull; + +/** + * Wraps a lazily initialised resource which needs to be shut down. + * The wrapped supplier may not return {@code null}, and should be retryable on failure. + * If it throws, it will be retried if {@link #get} is retried. A supplier that fails to + * clean up partial state on failure may cause a resource leak. + * + * @author jonmv + */ +@Beta +public class Memoized<T, E extends Exception> implements Supplier<T>, AutoCloseable { + + /** + * Provides a tighter bound on the thrown exception type. + */ + @FunctionalInterface + public interface Closer<T, E extends Exception> { + + void close(T t) throws E; + + } + + + private final Object monitor = new Object(); + private final Closer<T, E> closer; + private volatile T wrapped; + private Supplier<T> factory; + + public Memoized(Supplier<T> factory, Closer<T, E> closer) { + this.factory = requireNonNull(factory); + this.closer = requireNonNull(closer); + } + + public static <T extends AutoCloseable> Memoized<T, ?> of(Supplier<T> factory) { + return new Memoized<>(factory, AutoCloseable::close); + } + + public static <T, U, E extends Exception> Memoized<U, E> combine(Memoized<T, ? extends E> inner, Function<T, U> outer, Closer<U, ? extends E> closer) { + return new Memoized<>(() -> outer.apply(inner.get()), compose(closer, inner::close)); + } + + @Override + public T get() { + // Double-checked locking: try the variable, and if not initialized, try to initialize it. + if (wrapped == null) synchronized (monitor) { + // Ensure the factory is called only once, by clearing it once successfully called. + if (factory != null) wrapped = requireNonNull(factory.get()); + factory = null; + + // If we found the factory, we won the initialization race, and return normally; otherwise + // if wrapped is non-null, we lost the race, wrapped was set by the winner, and we return; otherwise + // we tried to initialise because wrapped was cleared by closing this, and we fail. + if (wrapped == null) throw new IllegalStateException("already closed"); + } + return wrapped; + } + + @Override + public void close() throws E { + // Alter state only when synchronized with calls to get(). + synchronized (monitor) { + // Ensure we only try to close the generated resource once, by clearing it after picking it up here. + T maybe = wrapped; + wrapped = null; + // Clear the factory, to signal this has been closed. + factory = null; + if (maybe != null) closer.close(maybe); + } + } + + private interface Thrower<E extends Exception> { void call() throws E; } + + private static <T, E extends Exception> Closer<T, E> compose(Closer<T, ? extends E> outer, Thrower<? extends E> inner) { + return parent -> { + Exception thrown = null; + try { + outer.close(parent); + } + catch (Exception e) { + thrown = e; + } + try { + inner.call(); + } + catch (Exception e) { + if (thrown != null) thrown.addSuppressed(e); + else thrown = e; + } + @SuppressWarnings("unchecked") + E e = (E) thrown; + if (e != null) throw e; + }; + } + +}
\ No newline at end of file diff --git a/vespajlib/src/main/java/com/yahoo/yolean/concurrent/ResourcePool.java b/vespajlib/src/main/java/com/yahoo/yolean/concurrent/ResourcePool.java new file mode 100644 index 00000000000..ffc761ad625 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/concurrent/ResourcePool.java @@ -0,0 +1,38 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.concurrent; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; +import java.util.function.Supplier; + +/** + * <p>This implements a simple stack based resource pool. If you are out of resources new are allocated from the + * factory.</p> + * + * @author baldersheim + * @since 5.2 + */ +public final class ResourcePool<T> implements Iterable<T> { + + private final Deque<T> pool = new ArrayDeque<>(); + private final Supplier<T> factory; + + + public ResourcePool(Supplier<T> factory) { + this.factory = factory; + } + + public T alloc() { + return pool.isEmpty() ? factory.get() : pool.pop(); + } + + public void free(T e) { + pool.push(e); + } + + @Override + public Iterator<T> iterator() { + return pool.iterator(); + } +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/concurrent/Sleeper.java b/vespajlib/src/main/java/com/yahoo/yolean/concurrent/Sleeper.java new file mode 100644 index 00000000000..530be935bc1 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/concurrent/Sleeper.java @@ -0,0 +1,29 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.concurrent; + +import com.yahoo.yolean.UncheckedInterruptedException; + +import java.time.Duration; + +import static com.yahoo.yolean.Exceptions.uncheckInterrupted; + +/** + * An abstraction used for mocking {@link Thread#sleep(long)} in unit tests. + * + * @author bjorncs + */ +public interface Sleeper { + default void sleep(Duration duration) throws UncheckedInterruptedException { + uncheckInterrupted(() -> sleepChecked(duration.toMillis())); + } + + default void sleepChecked(Duration duration) throws InterruptedException { sleepChecked(duration.toMillis()); } + + default void sleep(long millis) throws UncheckedInterruptedException { uncheckInterrupted(() -> sleepChecked(millis)); } + + void sleepChecked(long millis) throws InterruptedException; + + Sleeper DEFAULT = Thread::sleep; + Sleeper NOOP = millis -> {}; + +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/concurrent/ThreadRobustList.java b/vespajlib/src/main/java/com/yahoo/yolean/concurrent/ThreadRobustList.java new file mode 100644 index 00000000000..f6d8b68416c --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/concurrent/ThreadRobustList.java @@ -0,0 +1,122 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.concurrent; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * <p>This class implements a thread-safe, lock-free list of Objects that supports multiple readers and a single writer. + * Because there are no locks or other memory barriers involved, there exists no <i>happens-before</i> relationship + * among calls to either methods of the <code>ThreadRobustList</code>. This means that there are no guarantees as to when + * (or even if) an item {@link #add(Object)}ed becomes visible through {@link #iterator()}. If visibility is required, + * either use explicit synchronization between reader and writer thread, or move to a different concurrent collection + * (e.g. <code>CopyOnWriteArrayList</code>).</p> + * <p>Because it is lock-free, the <code>ThreadRobustList</code> has minimal overhead to both reading and writing. The + * iterator offered by this class always observes the list in a consistent state, and it never throws a + * <code>ConcurrentModificationException</code>.</p> + * <p>The <code>ThreadRobustList</code> does not permit adding <code>null</code> items.</p> + * <p>The usage of <code>ThreadRobustList</code> has no memory consistency effects. </p> + * + * @author Steinar Knutsen + * @author bratseth + * @since 5.1.15 + */ +public class ThreadRobustList<T> implements Iterable<T> { + + private Object[] items; + private int next = 0; + + /** + * <p>Constructs a new instance of this class with an initial capacity of <code>10</code>.</p> + */ + public ThreadRobustList() { + this(10); + } + + /** + * <p>Constructs a new instance of this class with a given initial capacity.</p> + * + * @param initialCapacity the initial capacity of this list + */ + public ThreadRobustList(int initialCapacity) { + items = new Object[initialCapacity]; + } + + /** + * <p>Returns whether or not this list is empty.</p> + * + * @return <code>true</code> if this list has zero items + */ + public boolean isEmpty() { + return next == 0; + } + + /** + * <p>Adds an item to this list. As opposed to <code>CopyOnWriteArrayList</code>, items added to this list may become + * visible to iterators created <em>before</em> a call to this method.</p> + * + * @param item the item to add + * @throws NullPointerException if <code>item</code> is <code>null</code> + */ + public void add(T item) { + if (item == null) { + throw new NullPointerException(); + } + Object[] workItems = items; + if (next >= items.length) { + workItems = Arrays.copyOf(workItems, 20 + items.length * 2); + workItems[next++] = item; + items = workItems; + } else { + workItems[next++] = item; + } + } + + /** + * <p>Returns an iterator over the items in this list. As opposed to <code>CopyOnWriteArrayList</code>, this iterator + * may see items added to the <code>ThreadRobustList</code> even if they occur <em>after</em> a call to this method.</p> + * <p>The returned iterator does not support <code>remove()</code>.</p> + * + * @return an iterator over this list + */ + @Override + public Iterator<T> iterator() { + return new ThreadRobustIterator<>(items); + } + + private static class ThreadRobustIterator<T> implements Iterator<T> { + + final Object[] items; + int nextIndex = 0; + + ThreadRobustIterator(Object[] items) { + this.items = items; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return (T)items[nextIndex++]; + } + + @Override + public boolean hasNext() { + if (nextIndex >= items.length) { + return false; + } + if (items[nextIndex] == null) { + return false; + } + return true; + } + } +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/concurrent/package-info.java b/vespajlib/src/main/java/com/yahoo/yolean/concurrent/package-info.java new file mode 100644 index 00000000000..1e89d85714e --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/concurrent/package-info.java @@ -0,0 +1,7 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.yolean.concurrent; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/vespajlib/src/main/java/com/yahoo/yolean/function/ThrowingConsumer.java b/vespajlib/src/main/java/com/yahoo/yolean/function/ThrowingConsumer.java new file mode 100644 index 00000000000..0860c7c34b4 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/function/ThrowingConsumer.java @@ -0,0 +1,20 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.function; + +import java.util.Objects; + +/** + * Functional interface that mirrors the Consumer interface, but allows for an + * exception to be thrown. + * + * @author oyving + */ +@FunctionalInterface +public interface ThrowingConsumer<T, E extends Throwable> { + void accept(T input) throws E; + + default ThrowingConsumer<T, E> andThen(ThrowingConsumer<? super T, ? extends E> after) { + Objects.requireNonNull(after); + return (T t) -> { accept(t); after.accept(t); }; + } +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/function/ThrowingFunction.java b/vespajlib/src/main/java/com/yahoo/yolean/function/ThrowingFunction.java new file mode 100644 index 00000000000..6e459509b1d --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/function/ThrowingFunction.java @@ -0,0 +1,25 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.function; + +import java.util.Objects; + +/** + * Functional interface that mirrors the Function interface, but allows for an + * exception to be thrown. + * + * @author oyving + */ +@FunctionalInterface +public interface ThrowingFunction<T, R, E extends Throwable> { + R apply(T input) throws E; + + default <V> ThrowingFunction<T, V, E> andThen(ThrowingFunction<? super R, ? extends V, ? extends E> after) { + Objects.requireNonNull(after); + return (T t) -> after.apply(apply(t)); + } + + default <V> ThrowingFunction<V, R, E> compose(ThrowingFunction<? super V, ? extends T, ? extends E> before) { + Objects.requireNonNull(before); + return (V v) -> apply(before.apply(v)); + } +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/function/ThrowingSupplier.java b/vespajlib/src/main/java/com/yahoo/yolean/function/ThrowingSupplier.java new file mode 100644 index 00000000000..348c1c739ee --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/function/ThrowingSupplier.java @@ -0,0 +1,13 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.function; + +/** + * Functional interface that mirrors the Supplier interface, but allows for an + * exception to be thrown. + * + * @author oyving + */ +@FunctionalInterface +public interface ThrowingSupplier<R, E extends Throwable> { + R get() throws E; +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/function/package-info.java b/vespajlib/src/main/java/com/yahoo/yolean/function/package-info.java new file mode 100644 index 00000000000..e55b39c478f --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/function/package-info.java @@ -0,0 +1,10 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * @author oyving + */ +@ExportPackage +@PublicApi +package com.yahoo.yolean.function; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/vespajlib/src/main/java/com/yahoo/yolean/package-info.java b/vespajlib/src/main/java/com/yahoo/yolean/package-info.java new file mode 100644 index 00000000000..c9f2b088688 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/package-info.java @@ -0,0 +1,7 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.yolean; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/vespajlib/src/main/java/com/yahoo/yolean/system/CatchSignals.java b/vespajlib/src/main/java/com/yahoo/yolean/system/CatchSignals.java new file mode 100644 index 00000000000..572d8fba122 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/system/CatchSignals.java @@ -0,0 +1,75 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.system; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.concurrent.atomic.AtomicBoolean; + +// import sun.misc.Signal; +// import sun.misc.SignalHandler; + + +public class CatchSignals { + /** + * Sets up a signal handler for SIGTERM and SIGINT, where a given AtomicBoolean + * gets a true value when the signal is caught. + * + * Callers basically have two options for acting on the signal: + * + * They may choose to synchronize and wait() on this variable, + * and they will be notified when it changes state to true. To avoid + * problems with spurious wakeups, use a while loop and wait() + * again if the state is still false. As soon as the caller has been + * woken up and the state is true, the application should exit as + * soon as possible. + * + * They may also choose to poll the state of this variable. As soon + * as its state becomes true, the signal has been received, and the + * application should exit as soon as possible. + * + * @param signalCaught set to false initially, will be set to true when SIGTERM or SIGINT is caught. + */ + @SuppressWarnings("rawtypes") + public static void setup(final AtomicBoolean signalCaught) { + signalCaught.set(false); + try { + Class shc = Class.forName("sun.misc.SignalHandler"); + Class ssc = Class.forName("sun.misc.Signal"); + + InvocationHandler ihandler = new InvocationHandler() { + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + synchronized (signalCaught) { + signalCaught.set(true); + signalCaught.notifyAll(); + } + return null; + } + }; + Object shandler = Proxy.newProxyInstance(CatchSignals.class.getClassLoader(), + new Class[] { shc }, + ihandler); + Constructor[] c = ssc.getDeclaredConstructors(); + assert c.length == 1; + Object sigterm = c[0].newInstance("TERM"); + Object sigint = c[0].newInstance("INT"); + Method m = findMethod(ssc, "handle"); + assert m != null; // "NoSuchMethodException" + m.invoke(null, sigterm, shandler); + m.invoke(null, sigint, shandler); + } catch (ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException e) { + System.err.println("FAILED setting up signal catching: "+e); + } + } + + private static Method findMethod(Class<?> c, String name) { + for (Method m : c.getDeclaredMethods()) { + if (m.getName().equals(name)) { + return m; + } + } + return null; + } +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/system/package-info.java b/vespajlib/src/main/java/com/yahoo/yolean/system/package-info.java new file mode 100644 index 00000000000..cf3a4f33e1a --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/system/package-info.java @@ -0,0 +1,5 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.yolean.system; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/vespajlib/src/main/java/com/yahoo/yolean/trace/TraceNode.java b/vespajlib/src/main/java/com/yahoo/yolean/trace/TraceNode.java new file mode 100644 index 00000000000..fd19c1b1388 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/trace/TraceNode.java @@ -0,0 +1,247 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.trace; + +import com.yahoo.yolean.concurrent.ThreadRobustList; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * <p>This class represents a single node in a tree of <code>TraceNodes</code>. The trace forms a tree where there is a + * branch for each parallel execution, and a node within such a branch for each traced event. As each <code>TraceNode</code> + * may contain a payload of any type, the trace tree can be used to exchange any thread-safe state between producers and + * consumers in different threads, whether or not the shape of the trace tree is relevant to the particular + * information.</p> + * <p>This class uses a {@link ThreadRobustList} for its children. That list allows multiple threads to inspect the + * hierarchy of a <code>TraceNode</code> tree while there are other threads concurrently modifying it, without incurring the + * cost of memory synchronization. The only caveat being that for each <code>TraceNode</code> there can never be more than + * exactly one writer thread. If multiple threads need to mutate a single <code>TraceNode</code>, then the writer threads + * need to synchronize their access on the <code>TraceNode</code>.</p> + * + * @author Steinar Knutsen + * @author bratseth + * @since 5.1.15 + */ +public class TraceNode { + + private final Object payload; + private final long timestamp; + private ThreadRobustList<TraceNode> children; + private TraceNode parent; + + /** + * <p>Creates a new instance of this class.</p> + * + * @param payload the payload to assign to this, may be <code>null</code> + * @param timestamp the timestamp to assign to this + */ + public TraceNode(Object payload, long timestamp) { + this.payload = payload; + this.timestamp = timestamp; + } + + /** + * <p>Adds another <code>TraceNode</code> as a child to this.</p> + * + * @param child the TraceNode to add + * @return this, to allow chaining + * @throws IllegalArgumentException if <code>child</code> is not a root TraceNode + * @see #isRoot() + */ + public TraceNode add(TraceNode child) { + if (child.parent != null) { + throw new IllegalArgumentException("Can not add " + child + " to " + this + "; it is not a root."); + } + child.parent = this; + if (children == null) { + children = new ThreadRobustList<>(); + } + children.add(child); + return this; + } + + /** + * <p>Returns a read-only iterable of all {@link #payload() payloads} that are instances of <code>payloadType</code>, + * in all its decendants. The payload of <em>this</em> <code>TraceNode</code> is ignored.</p> + * <p>The payloads are retrieved in depth-first, prefix order.</p> + * + * @param payloadType the type of payloads to retrieve + * @return the payloads, never <code>null</code> + */ + public <PAYLOADTYPE> Iterable<PAYLOADTYPE> descendants(final Class<PAYLOADTYPE> payloadType) { + if (children == null) { + return Collections.emptyList(); + } + return new Iterable<PAYLOADTYPE>() { + + @Override + public Iterator<PAYLOADTYPE> iterator() { + return new PayloadIterator<>(TraceNode.this, payloadType); + } + }; + } + + /** + * <p>Returns the payload of this <code>TraceNode</code>, or null if none.</p> + * + * @return the payload + */ + public Object payload() { + return payload; + } + + /** + * <p>Returns the timestamp of this <code>TraceNode</code>.</p> + * + * @return the timestamp + */ + public long timestamp() { + return timestamp; + } + + /** + * <p>Returns the parent <code>TraceNode</code> of this.</p> + * + * @return the parent + */ + public TraceNode parent() { + return parent; + } + + /** + * <p>Returns the child <code>TraceNodes</code> of this.</p> + * + * @return the children + */ + public Iterable<TraceNode> children() { + if (children == null) { + return Collections.emptyList(); + } + return children; + } + + /** + * <p>Returns whether or not this <code>TraceNode</code> is a root node (i.e. it has no parent).</p> + * + * @return <code>true</code> if {@link #parent()} returns <code>null</code> + */ + public boolean isRoot() { + return parent == null; + } + + /** + * <p>Returns the root <code>TraceNode</code> of the tree that this <code>TraceNode</code> belongs to.</p> + * + * @return the root + */ + public TraceNode root() { + TraceNode node = this; + while (node.parent != null) { + node = node.parent; + } + return node; + } + + /** + * <p>Visits this <code>TraceNode</code> and all of its descendants in depth-first, prefix order.</p> + * + * @param visitor The visitor to accept. + * @return The <code>visitor</code> parameter. + */ + public <T extends TraceVisitor> T accept(T visitor) { + visitor.visit(this); + if (children == null || children.isEmpty()) { + return visitor; + } + visitor.entering(this); + for (TraceNode child : children) { + child.accept(visitor); + } + visitor.leaving(this); + return visitor; + } + + @Override + public String toString() { + final StringBuilder out = new StringBuilder("[ "); + accept(new TraceVisitor() { + + @Override + public void visit(TraceNode node) { + if (node.payload != null) { + out.append(node.payload).append(" "); + } + } + + @Override + public void entering(TraceNode node) { + out.append("[ "); + } + + @Override + public void leaving(TraceNode node) { + out.append("] "); + } + }); + return out.append("]").toString(); + } + + private static class PayloadIterator<PAYLOADTYPE> implements Iterator<PAYLOADTYPE> { + + final List<TraceNode> unexploredNodes = new LinkedList<>(); + final Class<PAYLOADTYPE> payloadType; + PAYLOADTYPE next; + + PayloadIterator(TraceNode root, Class<PAYLOADTYPE> payloadType) { + payloadType.getClass(); // throws NullPointerException + this.payloadType = payloadType; + unexploredNodes.add(root); + next = advance(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public PAYLOADTYPE next() { + if (next == null) { + throw new NoSuchElementException(); + } + PAYLOADTYPE current = next; + next = advance(); + return current; + } + + PAYLOADTYPE advance() { + // Current node is depleted, find next + while (unexploredNodes.size() > 0) { + // Take the next node + TraceNode node = unexploredNodes.remove(0); + + // Add its children to the list of nodes we1'll look at + if (node.children != null) { + int i = 0; // used to fabricate depth-first traversal order + for (TraceNode child : node.children) { + unexploredNodes.add(i++, child); + } + } + + Object payload = node.payload(); + if (payloadType.isInstance(payload)) { + return payloadType.cast(payload); + } + } + return null; + } + } +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/trace/TraceVisitor.java b/vespajlib/src/main/java/com/yahoo/yolean/trace/TraceVisitor.java new file mode 100644 index 00000000000..1b3507777b7 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/trace/TraceVisitor.java @@ -0,0 +1,45 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.trace; + +/** + * <p>This class is an abstract visitor of {@link TraceNode}. See {@link TraceNode#accept(TraceVisitor)}.</p> + * + * @author bratseth + * @since 5.1.15 + */ +public abstract class TraceVisitor { + + /** + * <p>Visits a {@link TraceNode}. Called by {@link TraceNode#accept(TraceVisitor)}, before visiting its + * children.</p> + * + * @param node the <code>TraceNode</code> being visited + * @see TraceNode#accept(TraceVisitor) + */ + public abstract void visit(TraceNode node); + + /** + * <p>Enters a {@link TraceNode}. This method is called after {@link #visit(TraceNode)}, but before visiting its + * children. Note that this method is NOT called if a <code>TraceNode</code> has zero children.</p> + * <p>The default implementation of this method does nothing.</p> + * + * @param node the <code>TraceNode</code> being entered + * @see #leaving(TraceNode) + */ + @SuppressWarnings("UnusedParameters") + public void entering(TraceNode node) { + // empty + } + + /** + * <p>Leaves a {@link TraceNode}. This method is called after {@link #entering(TraceNode)}, and after visiting its + * children. Note that this method is NOT called if a <code>TraceNode</code> has zero children.</p> + * <p>The default implementation of this method does nothing.</p> + * + * @param node the <code>TraceNode</code> being left + */ + @SuppressWarnings("UnusedParameters") + public void leaving(TraceNode node) { + // empty + } +} diff --git a/vespajlib/src/main/java/com/yahoo/yolean/trace/package-info.java b/vespajlib/src/main/java/com/yahoo/yolean/trace/package-info.java new file mode 100644 index 00000000000..dabc8217025 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/yolean/trace/package-info.java @@ -0,0 +1,7 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.yolean.trace; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/vespajlib/src/test/java/com/yahoo/yolean/ExceptionsTestCase.java b/vespajlib/src/test/java/com/yahoo/yolean/ExceptionsTestCase.java new file mode 100644 index 00000000000..53cf3efe363 --- /dev/null +++ b/vespajlib/src/test/java/com/yahoo/yolean/ExceptionsTestCase.java @@ -0,0 +1,81 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean; + +import org.junit.Test; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.NoSuchFileException; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; + +/** + * @author bratseth + */ +public class ExceptionsTestCase { + + @Test + public void testFindCause() { + IllegalArgumentException e1 = new IllegalArgumentException(); + IllegalStateException e2 = new IllegalStateException(e1); + RuntimeException e3 = new RuntimeException(e2); + + assertEquals(Optional.of(e3), Exceptions.findCause(e3, RuntimeException.class)); + assertEquals(Optional.of(e1), Exceptions.findCause(e3, IllegalArgumentException.class)); + assertEquals(Optional.empty(), Exceptions.findCause(e3, NumberFormatException.class)); + + assertEquals(Optional.of(e2), Exceptions.findCause(e2, RuntimeException.class)); + } + + @Test + public void testToMessageStrings() { + assertEquals("Blah",Exceptions.toMessageString(new Exception("Blah"))); + assertEquals("Blah", Exceptions.toMessageString(new Exception(new Exception("Blah")))); + assertEquals("Exception",Exceptions.toMessageString(new Exception())); + assertEquals("Foo: Blah",Exceptions.toMessageString(new Exception("Foo",new Exception(new IllegalArgumentException("Blah"))))); + assertEquals("Foo",Exceptions.toMessageString(new Exception("Foo",new Exception("Foo")))); + assertEquals("Foo: Exception",Exceptions.toMessageString(new Exception("Foo",new Exception()))); + assertEquals("Foo",Exceptions.toMessageString(new Exception(new Exception("Foo")))); + } + + @Test + public void testUnchecks() { + try { + Exceptions.uncheck(this::throwNoSuchFileException); + } catch (UncheckedIOException e) { + assertEquals("filename", e.getCause().getMessage()); + } + + try { + Exceptions.uncheck(this::throwNoSuchFileException, "additional %s", "info"); + } catch (UncheckedIOException e) { + assertEquals("additional info", e.getMessage()); + } + + try { + int i = Exceptions.uncheck(this::throwNoSuchFileExceptionSupplier); + } catch (UncheckedIOException e) { + assertEquals("filename", e.getCause().getMessage()); + } + + try { + int i = Exceptions.uncheck(this::throwNoSuchFileExceptionSupplier, "additional %s", "info"); + } catch (UncheckedIOException e) { + assertEquals("additional info", e.getMessage()); + } + + Exceptions.uncheckAndIgnore(this::throwNoSuchFileException, NoSuchFileException.class); + assertNull(Exceptions.uncheckAndIgnore(this::throwNoSuchFileExceptionSupplier, NoSuchFileException.class)); + } + + private void throwNoSuchFileException() throws IOException { + throw new NoSuchFileException("filename"); + } + + private int throwNoSuchFileExceptionSupplier() throws IOException { + throw new NoSuchFileException("filename"); + } +} diff --git a/vespajlib/src/test/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMapTest.java b/vespajlib/src/test/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMapTest.java new file mode 100644 index 00000000000..3f2526172a9 --- /dev/null +++ b/vespajlib/src/test/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMapTest.java @@ -0,0 +1,106 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.concurrent; + +import org.junit.Test; + +import java.util.Iterator; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * @author baldersheim + * @since 5.2 + */ +public class CopyOnWriteHashMapTest { + + @Test + public void requireThatAccessorsWork() { + Map<String, String> map = new CopyOnWriteHashMap<>(); + assertEquals(0, map.size()); + assertEquals(true, map.isEmpty()); + assertEquals(false, map.containsKey("fooKey")); + assertEquals(false, map.containsValue("fooVal")); + assertNull(map.get("fooKey")); + assertNull(map.remove("fooKey")); + assertEquals(0, map.keySet().size()); + assertEquals(0, map.entrySet().size()); + assertEquals(0, map.values().size()); + + map.put("fooKey", "fooVal"); + assertEquals(1, map.size()); + assertEquals(false, map.isEmpty()); + assertEquals(true, map.containsKey("fooKey")); + assertEquals(true, map.containsValue("fooVal")); + assertEquals("fooVal", map.get("fooKey")); + assertEquals(1, map.keySet().size()); + assertEquals(1, map.entrySet().size()); + assertEquals(1, map.values().size()); + + map.put("barKey", "barVal"); + assertEquals(2, map.size()); + assertEquals(false, map.isEmpty()); + assertEquals(true, map.containsKey("fooKey")); + assertEquals(true, map.containsKey("barKey")); + assertEquals(true, map.containsValue("fooVal")); + assertEquals(true, map.containsValue("barVal")); + assertEquals("fooVal", map.get("fooKey")); + assertEquals("barVal", map.get("barKey")); + assertEquals(2, map.keySet().size()); + assertEquals(2, map.entrySet().size()); + assertEquals(2, map.values().size()); + + assertEquals("fooVal", map.remove("fooKey")); + assertEquals(1, map.size()); + assertEquals(false, map.isEmpty()); + assertEquals(false, map.containsKey("fooKey")); + assertEquals(true, map.containsKey("barKey")); + assertEquals(false, map.containsValue("fooVal")); + assertEquals(true, map.containsValue("barVal")); + assertNull(map.get("fooKey")); + assertEquals("barVal", map.get("barKey")); + assertEquals(1, map.keySet().size()); + assertEquals(1, map.entrySet().size()); + assertEquals(1, map.values().size()); + } + + @Test + public void requireThatEntrySetDoesNotReflectConcurrentModifications() { + Map<String, String> map = new CopyOnWriteHashMap<>(); + map.put("fooKey", "fooVal"); + + Iterator<Map.Entry<String, String>> it = map.entrySet().iterator(); + assertEquals("fooVal", map.remove("fooKey")); + + assertTrue(it.hasNext()); + Map.Entry<String, String> entry = it.next(); + assertEquals("fooKey", entry.getKey()); + assertEquals("fooVal", entry.getValue()); + } + + @Test + public void requireThatKeySetDoesNotReflectConcurrentModifications() { + Map<String, String> map = new CopyOnWriteHashMap<>(); + map.put("fooKey", "fooVal"); + + Iterator<String> it = map.keySet().iterator(); + assertEquals("fooVal", map.remove("fooKey")); + + assertTrue(it.hasNext()); + assertEquals("fooKey", it.next()); + } + + @Test + public void requireThatValuesDoNotReflectConcurrentModifications() { + Map<String, String> map = new CopyOnWriteHashMap<>(); + map.put("fooKey", "fooVal"); + + Iterator<String> it = map.values().iterator(); + assertEquals("fooVal", map.remove("fooKey")); + + assertTrue(it.hasNext()); + assertEquals("fooVal", it.next()); + } +} diff --git a/vespajlib/src/test/java/com/yahoo/yolean/concurrent/MemoizedTest.java b/vespajlib/src/test/java/com/yahoo/yolean/concurrent/MemoizedTest.java new file mode 100644 index 00000000000..7f2f49c75f2 --- /dev/null +++ b/vespajlib/src/test/java/com/yahoo/yolean/concurrent/MemoizedTest.java @@ -0,0 +1,101 @@ +package com.yahoo.yolean.concurrent; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.Phaser; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +/** + * @author jonmv + */ +public class MemoizedTest { + + final Phaser phaser = new Phaser(); + final int threads = 128; + + @Test + public void test() throws ExecutionException, InterruptedException { + var lazy = new Memoized<>(new OnceSupplier(), OnceCloseable::close); + phaser.register(); // test thread + phaser.register(); // whoever calls the factory + + Phaser latch = new Phaser(threads + 1); + ExecutorService executor = Executors.newFixedThreadPool(threads); + List<Future<?>> futures = new ArrayList<>(); + for (int i = 0; i < 128; i++) { + futures.add(executor.submit(() -> { + latch.arriveAndAwaitAdvance(); + lazy.get().rendezvous(); + while (true) lazy.get(); + })); + } + + // All threads waiting for latch, will race to factory + latch.arriveAndAwaitAdvance(); + + // One thread waiting in factory, the others are blocked, will go to rendezvous + phaser.arriveAndAwaitAdvance(); + + // All threads waiting in rendezvous, will repeatedly get until failure + phaser.arriveAndAwaitAdvance(); + + // Unsynchronized close should be detected by all threads + lazy.close(); + + // Close should carry through only once + lazy.close(); + + assertEquals("already closed", + assertThrows(IllegalStateException.class, lazy::get).getMessage()); + + for (Future<?> future : futures) + assertEquals("java.lang.IllegalStateException: already closed", + assertThrows(ExecutionException.class, future::get).getMessage()); + + executor.shutdown(); + } + + @Test + public void closeBeforeFirstGet() throws Exception { + OnceSupplier supplier = new OnceSupplier(); + Memoized<OnceCloseable, ?> lazy = Memoized.of(supplier); + lazy.close(); + assertEquals("already closed", + assertThrows(IllegalStateException.class, lazy::get).getMessage()); + lazy.close(); + assertFalse(supplier.initialized.get()); + } + + class OnceSupplier implements Supplier<OnceCloseable> { + final AtomicBoolean initialized = new AtomicBoolean(); + @Override public OnceCloseable get() { + phaser.arriveAndAwaitAdvance(); + if ( ! initialized.compareAndSet(false, true)) fail("initialized more than once"); + phaser.bulkRegister(threads - 1); // register all the threads who didn't get the factory + return new OnceCloseable(); + } + } + + class OnceCloseable implements AutoCloseable { + final AtomicBoolean closed = new AtomicBoolean(); + @Override public void close() { + if ( ! closed.compareAndSet(false, true)) fail("closed more than once"); + } + void rendezvous() { + phaser.arriveAndAwaitAdvance(); + } + } + +} diff --git a/vespajlib/src/test/java/com/yahoo/yolean/concurrent/ThreadRobustListTestCase.java b/vespajlib/src/test/java/com/yahoo/yolean/concurrent/ThreadRobustListTestCase.java new file mode 100644 index 00000000000..c2edaf1fb00 --- /dev/null +++ b/vespajlib/src/test/java/com/yahoo/yolean/concurrent/ThreadRobustListTestCase.java @@ -0,0 +1,146 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.concurrent; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + */ +public class ThreadRobustListTestCase { + + private final static int NUM_THREADS = 64; + private final static int NUM_ITEMS_TO_WRITE = 1000000; + private final static int NUM_TIMES_TO_READ = 10; + + @Test + public void requireThatListIsThreadRobust() throws Exception { + final CountDownLatch latch = new CountDownLatch(NUM_THREADS); + final ThreadRobustList<Integer> sharedList = new ThreadRobustList<>(); + + List<Callable<Boolean>> tasks = new ArrayList<>(NUM_THREADS); + tasks.add(new WriterTask(latch, sharedList)); + for (int i = 1; i < NUM_THREADS; ++i) { + tasks.add(new ReaderTask(latch, sharedList)); + } + for (Future<Boolean> result : Executors.newFixedThreadPool(NUM_THREADS).invokeAll(tasks)) { + assertTrue(result.get(60, TimeUnit.SECONDS)); + } + } + + @Test + public void requireThatAccessorsWork() { + ThreadRobustList<Object> lst = new ThreadRobustList<>(); + assertTrue(lst.isEmpty()); + assertFalse(lst.iterator().hasNext()); + + Object foo = new Object(); + lst.add(foo); + assertFalse(lst.isEmpty()); + Iterator<Object> it = lst.iterator(); + assertNotNull(it); + assertTrue(it.hasNext()); + assertSame(foo, it.next()); + assertFalse(it.hasNext()); + + Object bar = new Object(); + lst.add(bar); + assertFalse(lst.isEmpty()); + assertNotNull(it = lst.iterator()); + assertTrue(it.hasNext()); + assertSame(foo, it.next()); + assertTrue(it.hasNext()); + assertSame(bar, it.next()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatIteratorNextThrowsNoSuchElementExceptionWhenDone() { + ThreadRobustList<Object> lst = new ThreadRobustList<>(); + Iterator<Object> it = lst.iterator(); + assertFalse(it.hasNext()); + try { + it.next(); + fail(); + } catch (NoSuchElementException e) { + + } + } + + @Test + public void requireThatIteratorRemoveIsNotSupported() { + ThreadRobustList<Object> lst = new ThreadRobustList<>(); + Object obj = new Object(); + lst.add(obj); + Iterator<Object> it = lst.iterator(); + assertTrue(it.hasNext()); + assertSame(obj, it.next()); + try { + it.remove(); + fail(); + } catch (UnsupportedOperationException e) { + + } + } + + private static class WriterTask implements Callable<Boolean> { + + final CountDownLatch latch; + final ThreadRobustList<Integer> sharedList; + + WriterTask(CountDownLatch latch, ThreadRobustList<Integer> sharedList) { + this.latch = latch; + this.sharedList = sharedList; + } + + @Override + public Boolean call() throws Exception { + latch.countDown(); + assertTrue(latch.await(60, TimeUnit.SECONDS)); + for (int i = 0; i < NUM_ITEMS_TO_WRITE; ++i) { + sharedList.add(i); + } + return true; + } + } + + private static class ReaderTask implements Callable<Boolean> { + + final CountDownLatch latch; + final ThreadRobustList<Integer> sharedList; + + ReaderTask(CountDownLatch latch, ThreadRobustList<Integer> sharedList) { + this.latch = latch; + this.sharedList = sharedList; + } + + @Override + public Boolean call() throws Exception { + latch.countDown(); + assertTrue(latch.await(60, TimeUnit.SECONDS)); + for (int i = 0; i < NUM_TIMES_TO_READ; ++i) { + Iterator<Integer> it = sharedList.iterator(); + for (int j = 0; it.hasNext(); ++j) { + assertEquals(j, it.next().intValue()); + } + } + return true; + } + } +} diff --git a/vespajlib/src/test/java/com/yahoo/yolean/system/CatchSignalsTestCase.java b/vespajlib/src/test/java/com/yahoo/yolean/system/CatchSignalsTestCase.java new file mode 100644 index 00000000000..66a27235088 --- /dev/null +++ b/vespajlib/src/test/java/com/yahoo/yolean/system/CatchSignalsTestCase.java @@ -0,0 +1,18 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.system; + +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author arnej27959 + */ +public class CatchSignalsTestCase { + + @Test + public void testThatSetupCompiles() { + CatchSignals.setup(new AtomicBoolean(false)); + } + +} diff --git a/vespajlib/src/test/java/com/yahoo/yolean/trace/TraceNodeTestCase.java b/vespajlib/src/test/java/com/yahoo/yolean/trace/TraceNodeTestCase.java new file mode 100644 index 00000000000..3019b646867 --- /dev/null +++ b/vespajlib/src/test/java/com/yahoo/yolean/trace/TraceNodeTestCase.java @@ -0,0 +1,231 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.trace; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author bratseth + */ +public class TraceNodeTestCase { + + @Test + public void requireThatAccessorsWork() { + TraceNode node = new TraceNode(null, 6); + assertNull(node.payload()); + assertEquals(6, node.timestamp()); + assertFalse(node.children().iterator().hasNext()); + assertFalse(node.descendants(Object.class).iterator().hasNext()); + assertTrue(node.isRoot()); + assertNull(node.parent()); + assertSame(node, node.root()); + } + + @Test + public void requireThatToStringIsReadable() { + TraceNode trace = new TraceNode(null, 0) + .add(new TraceNode("a", 1)) + .add(new TraceNode("b", 2) + .add(new TraceNode("c", 3))); + assertEquals("[ [ a b [ c ] ] ]", trace.toString()); + } + + @Test + public void requireThatPayloadMayBeNull() { + TraceNode node = new TraceNode(null, 6); + assertNull(node.payload()); + } + + @Test + public void requireThatRootNodesCanBeAdded() { + TraceNode parent = new TraceNode(null, 1); + + TraceNode foo = new TraceNode(null, 2); + parent.add(foo); + assertSame(parent, foo.parent()); + + TraceNode bar = new TraceNode(null, 3); + parent.add(bar); + assertSame(parent, bar.parent()); + + Iterator<TraceNode> children = parent.children().iterator(); + assertTrue(children.hasNext()); + assertSame(foo, children.next()); + assertTrue(children.hasNext()); + assertSame(bar, children.next()); + assertFalse(children.hasNext()); + + Iterator<Object> payloads = parent.descendants(Object.class).iterator(); + assertFalse(payloads.hasNext()); + } + + @Test + public void requireThatNonRootNodeCanNotBeAdded() { + TraceNode foo = new TraceNode(null, 0); + TraceNode bar = new TraceNode(null, 0); + TraceNode baz = new TraceNode(null, 0); + bar.add(baz); + try { + foo.add(baz); + fail(); + } catch (IllegalArgumentException e) { + + } + assertSame(bar, baz.parent()); + assertTrue(bar.children().iterator().hasNext()); + assertFalse(foo.children().iterator().hasNext()); + } + + @Test + public void requireThatChildrenIsNeverNull() { + assertNotNull(new TraceNode(null, 69).children()); + } + + @Test + public void requireThatDescendantsIsNeverNull() { + assertNotNull(new TraceNode(null, 69).descendants(Object.class)); + } + + @Test + public void requireThatDescendantsOrderIsDepthFirstPrefix() { + TraceNode trace = new TraceNode(null, 0) + .add(new TraceNode("a", 0) + .add(new TraceNode("b", 0)) + .add(new TraceNode("c", 0) + .add(new TraceNode("d", 0)) + .add(new TraceNode("e", 0)))) + .add(new TraceNode("f", 0) + .add(new TraceNode("g", 0))); + + Iterator<String> it = trace.descendants(String.class).iterator(); + assertTrue(it.hasNext()); + assertEquals("a", it.next()); + assertTrue(it.hasNext()); + assertEquals("b", it.next()); + assertTrue(it.hasNext()); + assertEquals("c", it.next()); + assertTrue(it.hasNext()); + assertEquals("d", it.next()); + assertTrue(it.hasNext()); + assertEquals("e", it.next()); + assertTrue(it.hasNext()); + assertEquals("f", it.next()); + assertTrue(it.hasNext()); + assertEquals("g", it.next()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatDescendantsFilterPayloads() { + TraceNode trace = new TraceNode(null, 0) + .add(new TraceNode("a", 0) + .add(new TraceNode(69, 0)) + .add(new TraceNode("b", 0) + .add(new TraceNode("c", 0)) + .add(new TraceNode(new Object(), 0)))) + .add(new TraceNode("d", 0) + .add(new TraceNode("e", 0))); + + Iterator<String> it = trace.descendants(String.class).iterator(); + assertTrue(it.hasNext()); + assertEquals("a", it.next()); + assertTrue(it.hasNext()); + assertEquals("b", it.next()); + assertTrue(it.hasNext()); + assertEquals("c", it.next()); + assertTrue(it.hasNext()); + assertEquals("d", it.next()); + assertTrue(it.hasNext()); + assertEquals("e", it.next()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatVisitorOrderIsDepthFirstPrefix() { + TraceNode trace = new TraceNode(null, 0) + .add(new TraceNode("a", 0) + .add(new TraceNode("b", 0)) + .add(new TraceNode("c", 0) + .add(new TraceNode("d", 0)) + .add(new TraceNode(3, 0)))) + .add(new TraceNode("f", 0) + .add(new TraceNode("g", 0))); + + final List<Object> payloads = new ArrayList<>(); + trace.accept(new TraceVisitor() { + + @Override + public void visit(TraceNode node) { + payloads.add(node.payload()); + } + }); + assertEquals(Arrays.<Object>asList(null, "a", "b", "c", "d", 3, "f", "g"), + payloads); + } + + @Test + public void requireThatVisitorDoesNotEnterOrLeaveNodesThatHaveNoChildren() { + TraceNode trace = new TraceNode(null, 0); + trace.accept(new TraceVisitor() { + + @Override + public void visit(TraceNode node) { + + } + + @Override + public void entering(TraceNode node) { + fail(); + } + + @Override + public void leaving(TraceNode node) { + fail(); + } + }); + } + + @Test + public void requireThatVisitorEntersAndLeavesNodesThatHaveChildren() { + TraceNode trace = new TraceNode("", 0) + .add(new TraceNode("a", 0) + .add(new TraceNode("b", 0)) + .add(new TraceNode("c", 0) + .add(new TraceNode("d", 0)) + .add(new TraceNode("e", 0)))) + .add(new TraceNode("f", 0) + .add(new TraceNode("g", 0))); + + final StringBuilder out = new StringBuilder(); + trace.accept(new TraceVisitor() { + + @Override + public void visit(TraceNode node) { + out.append(node.payload()); + } + + @Override + public void entering(TraceNode node) { + out.append("["); + } + + @Override + public void leaving(TraceNode node) { + out.append("]"); + } + }); + assertEquals("[a[bc[de]]f[g]]", out.toString()); + } +} diff --git a/vespajlib/src/test/java/com/yahoo/yolean/trace/TraceVisitorTestCase.java b/vespajlib/src/test/java/com/yahoo/yolean/trace/TraceVisitorTestCase.java new file mode 100644 index 00000000000..4eaa5b0241e --- /dev/null +++ b/vespajlib/src/test/java/com/yahoo/yolean/trace/TraceVisitorTestCase.java @@ -0,0 +1,21 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.trace; + +import org.junit.Test; + +/** + * @author Simon Thoresen Hult + */ +public class TraceVisitorTestCase { + + @Test + public void requireThatTraceVisitorCompilesWithOnlyVisitImplemented() { + new TraceNode(null, 0).accept(new TraceVisitor() { + + @Override + public void visit(TraceNode node) { + + } + }); + } +} |