}.
20 | *
21 | * All actors bound to the same {@code ActorThread} will be executed in the same {@link Thread}, so it is OK for
22 | * them to share some mutable state when it is known that all the actors are bound to the same thread. A common
23 | * pattern is to pass an actor its own {@code ActorThread}, so that it can create short-lived actors for callbacks,
24 | * or a reference to itself, when communicating with other actors.
25 | *
26 | * After nobody is holding the actor's {@link ActorRef}, that actor will be garbage collected. It is OK to create
27 | * lots of short-lived actors.
28 | */
29 | ActorRef bindActor(Class type, T rawActor);
30 |
31 | /**
32 | * Stops all actors which are bound to this {@code ActorThread} after all previously sent messages to them
33 | * have been processed. It is not possible to stop just one actor from an {@code ActorThread}, though due to garbage
34 | * collection that is usually not needed.
35 | *
36 | * An alternative way to stop actors is to call {@link Thread#interrupt()} on the thread where the actor is running,
37 | * which will stop that {@code ActorThread} immediately.
38 | */
39 | void stop();
40 | }
41 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/Callback.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors;
6 |
7 | import javax.annotation.Nullable;
8 |
9 | public interface Callback {
10 |
11 | void onResult(@Nullable T result);
12 | }
13 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/MessageProcessor.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors;
6 |
7 | interface MessageProcessor {
8 |
9 | void processNextMessage() throws InterruptedException;
10 |
11 | boolean processNextMessageIfAny();
12 | }
13 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/MultiThreadedActors.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors;
6 |
7 | import fi.jumi.actors.eventizers.EventizerProvider;
8 | import fi.jumi.actors.listeners.*;
9 |
10 | import javax.annotation.concurrent.ThreadSafe;
11 | import java.util.concurrent.Executor;
12 |
13 |
14 | /**
15 | * Multi-threaded actors container for production use. Each {@link ActorThread} will be backed by a thread from the
16 | * {@link Executor} which is given to the constructor of this class.
17 | */
18 | @ThreadSafe
19 | public class MultiThreadedActors extends Actors {
20 |
21 | private final Executor executor;
22 |
23 | public MultiThreadedActors(Executor executor, EventizerProvider eventizerProvider, FailureHandler failureHandler, MessageListener messageListener) {
24 | super(eventizerProvider, failureHandler, messageListener);
25 | this.executor = executor;
26 | }
27 |
28 | @Override
29 | void startActorThread(MessageProcessor actorThread) {
30 | executor.execute(new BlockingActorProcessor(actorThread));
31 | }
32 |
33 |
34 | @ThreadSafe
35 | private static class BlockingActorProcessor implements Runnable {
36 | private final MessageProcessor actorThread;
37 |
38 | public BlockingActorProcessor(MessageProcessor actorThread) {
39 | this.actorThread = actorThread;
40 | }
41 |
42 | @Override
43 | public void run() {
44 | try {
45 | while (!Thread.interrupted()) {
46 | actorThread.processNextMessage();
47 | }
48 | } catch (InterruptedException e) {
49 | // actor was told to exit
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/Promise.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors;
6 |
7 | import com.google.common.util.concurrent.*;
8 |
9 | import javax.annotation.Nullable;
10 | import javax.annotation.concurrent.ThreadSafe;
11 | import java.util.concurrent.Future;
12 |
13 | @ThreadSafe
14 | public final class Promise extends AbstractFuture {
15 |
16 | public static Deferred defer() {
17 | return new Deferred<>();
18 | }
19 |
20 | public static Promise of(@Nullable V value) {
21 | Deferred deferred = defer();
22 | deferred.resolve(value);
23 | return deferred.promise();
24 | }
25 |
26 | private Promise() {
27 | }
28 |
29 | public void then(Callback callback) {
30 | Futures.addCallback(this, new FutureCallback() {
31 | @Override
32 | public void onSuccess(@Nullable V result) {
33 | callback.onResult(result);
34 | }
35 |
36 | @Override
37 | public void onFailure(Throwable error) {
38 | // TODO
39 | }
40 | });
41 | }
42 |
43 | @ThreadSafe
44 | public static final class Deferred {
45 |
46 | private final Promise promise = new Promise<>();
47 |
48 | private Deferred() {
49 | }
50 |
51 | public Promise promise() {
52 | return promise;
53 | }
54 |
55 | public boolean resolve(@Nullable V result) {
56 | return promise.set(result);
57 | }
58 |
59 | public boolean reject(Throwable error) {
60 | // TODO
61 | //return promise.setException(error);
62 | throw new UnsupportedOperationException("TODO");
63 | }
64 |
65 | public void delegate(Future source) {
66 | Futures.addCallback(JdkFutureAdapters.listenInPoolThread(source), new FutureCallback() {
67 | @Override
68 | public void onSuccess(@Nullable V result) {
69 | resolve(result);
70 | }
71 |
72 | @Override
73 | public void onFailure(Throwable error) {
74 | reject(error);
75 | }
76 | });
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/SingleThreadedActors.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors;
6 |
7 | import fi.jumi.actors.eventizers.EventizerProvider;
8 | import fi.jumi.actors.listeners.*;
9 |
10 | import javax.annotation.concurrent.*;
11 | import java.util.List;
12 | import java.util.concurrent.*;
13 |
14 | /**
15 | * Single-threaded actors container for testing. The {@link ActorThread}s are not backed by real threads -
16 | * instead they will process messages when the {@link #processEventsUntilIdle()} method is called.
17 | */
18 | @NotThreadSafe
19 | public class SingleThreadedActors extends Actors {
20 |
21 | private final List actorThreads = new CopyOnWriteArrayList<>();
22 | private final MessageListener messageListener;
23 |
24 | public SingleThreadedActors(EventizerProvider eventizerProvider, FailureHandler failureHandler, MessageListener messageListener) {
25 | super(eventizerProvider, failureHandler, messageListener);
26 | this.messageListener = messageListener;
27 | }
28 |
29 | @Override
30 | void startActorThread(MessageProcessor actorThread) {
31 | actorThreads.add(actorThread);
32 | }
33 |
34 | /**
35 | * Processes in the current thread all messages which were sent to actors. The order of processing messages is
36 | * deterministic. Will block until all messages have been processed and nobody is sending more messages.
37 | *
38 | * When using {@link CrashEarlyFailureHandler}, will rethrow uncaught exceptions from actors to the caller of this
39 | * method.
40 | */
41 | public void processEventsUntilIdle() {
42 | boolean idle;
43 | do {
44 | idle = true;
45 | for (MessageProcessor actorThread : actorThreads) {
46 | if (actorThread.processNextMessageIfAny()) {
47 | idle = false;
48 | }
49 | if (Thread.interrupted()) {
50 | actorThreads.remove(actorThread);
51 | }
52 | }
53 | } while (!idle);
54 | }
55 |
56 | /**
57 | * Returns an asynchronous {@link Executor} which works the same way as all the actors in this container. Useful in
58 | * tests to have asynchrony without the non-determinism of real threads.
59 | *
60 | * @see #processEventsUntilIdle()
61 | */
62 | public Executor getExecutor() {
63 | return messageListener.getListenedExecutor(new AsynchronousExecutor());
64 | }
65 |
66 |
67 | @ThreadSafe
68 | private class AsynchronousExecutor implements Executor {
69 | @Override
70 | public void execute(final Runnable command) {
71 | // To unify the concepts of an executor and actors,
72 | // we implement the executor as one-time actor threads.
73 | ActorThread actorThread = startActorThread();
74 | ActorRef actor = actorThread.bindActor(Runnable.class, command);
75 | actor.tell().run();
76 | actorThread.stop();
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/eventizers/ComposedEventizerProvider.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers;
6 |
7 | import javax.annotation.concurrent.Immutable;
8 | import java.util.*;
9 |
10 | /**
11 | * To be used with generated or hand-written {@link Eventizer}s.
12 | */
13 | @Immutable
14 | public class ComposedEventizerProvider implements EventizerProvider {
15 |
16 | private final Map, Eventizer>> eventizers;
17 |
18 | public ComposedEventizerProvider(Eventizer>... eventizers) {
19 | HashMap, Eventizer>> map = new HashMap<>();
20 | for (Eventizer> eventizer : eventizers) {
21 | map.put(eventizer.getType(), eventizer);
22 | }
23 | this.eventizers = Collections.unmodifiableMap(map);
24 | }
25 |
26 | @SuppressWarnings({"unchecked"})
27 | @Override
28 | public Eventizer getEventizerForType(Class type) {
29 | Eventizer> eventizer = eventizers.get(type);
30 | if (eventizer == null) {
31 | throw new IllegalArgumentException("unsupported type: " + type);
32 | }
33 | return (Eventizer) eventizer;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/eventizers/Event.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers;
6 |
7 | public interface Event {
8 |
9 | void fireOn(T target);
10 | }
11 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/eventizers/EventToString.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers;
6 |
7 | import javax.annotation.concurrent.NotThreadSafe;
8 | import java.nio.charset.*;
9 |
10 | @NotThreadSafe
11 | public class EventToString {
12 |
13 | private final CharsetEncoder charsetEncoder;
14 | private final StringBuilder result = new StringBuilder();
15 |
16 | public EventToString(Charset charset) {
17 | charsetEncoder = charset.newEncoder();
18 | }
19 |
20 | public static String format(String className, String methodName, Object... args) {
21 | return new EventToString(Charset.defaultCharset())
22 | .formatMethodCall(className, methodName, args)
23 | .build();
24 | }
25 |
26 | public String build() {
27 | return result.toString();
28 | }
29 |
30 | public EventToString formatMethodCall(String className, String methodName, Object... args) {
31 | result.append(className);
32 | result.append('.');
33 | result.append(methodName);
34 | result.append('(');
35 | for (int i = 0; i < args.length; i++) {
36 | if (i > 0) {
37 | result.append(", ");
38 | }
39 | formatArg(args[i]);
40 | }
41 | result.append(')');
42 | return this;
43 | }
44 |
45 | private void formatArg(Object arg) {
46 | if (arg instanceof String) {
47 | result.append('"');
48 | escapeSpecialChars((String) arg);
49 | result.append('"');
50 | } else {
51 | result.append(arg);
52 | }
53 | }
54 |
55 | // package-private for testing
56 | EventToString escapeSpecialChars(String arg) {
57 | for (int i = 0; i < arg.length(); i++) {
58 | escapeSpecialChar(arg.charAt(i));
59 | }
60 | return this;
61 | }
62 |
63 | private void escapeSpecialChar(char ch) {
64 | switch (ch) {
65 | case '\b':
66 | result.append("\\b");
67 | return;
68 | case '\t':
69 | result.append("\\t");
70 | return;
71 | case '\n':
72 | result.append("\\n");
73 | return;
74 | case '\f':
75 | result.append("\\f");
76 | return;
77 | case '\r':
78 | result.append("\\r");
79 | return;
80 | case '\"':
81 | result.append("\\\"");
82 | return;
83 | case '\\':
84 | result.append("\\\\");
85 | return;
86 | }
87 |
88 | if (Character.isISOControl(ch) || isUnmappable(ch)) {
89 | String mask = "\\u0000";
90 | String hex = Integer.toHexString(ch);
91 | result.append(mask, 0, mask.length() - hex.length());
92 | result.append(hex);
93 | } else {
94 | result.append(ch);
95 | }
96 | }
97 |
98 | private boolean isUnmappable(char ch) {
99 | return !charsetEncoder.canEncode(ch);
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/eventizers/Eventizer.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers;
6 |
7 | import fi.jumi.actors.queue.MessageSender;
8 |
9 | /**
10 | * Converts method calls to event objects, and those event objects back to method calls.
11 | */
12 | public interface Eventizer {
13 |
14 | Class getType();
15 |
16 | T newFrontend(MessageSender> target);
17 |
18 | MessageSender> newBackend(T target);
19 | }
20 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/eventizers/EventizerProvider.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers;
6 |
7 | import fi.jumi.actors.Actors;
8 | import fi.jumi.actors.eventizers.dynamic.DynamicEventizerProvider;
9 |
10 | /**
11 | * Determines the types of actors that the {@link Actors} container can create.
12 | *
13 | * @see DynamicEventizerProvider
14 | * @see ComposedEventizerProvider
15 | */
16 | public interface EventizerProvider {
17 |
18 | Eventizer getEventizerForType(Class type);
19 | }
20 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/eventizers/Eventizers.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2018, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers;
6 |
7 | import fi.jumi.actors.Promise;
8 |
9 | import javax.annotation.concurrent.Immutable;
10 | import java.lang.reflect.Method;
11 | import java.util.*;
12 | import java.util.concurrent.Future;
13 |
14 | @Immutable
15 | public class Eventizers {
16 |
17 | private static List> ALLOWED_RETURN_TYPES = Arrays.asList(
18 | Void.TYPE,
19 | Future.class,
20 | Promise.class
21 | );
22 |
23 |
24 | private Eventizers() {
25 | // utility class, not to be instantiated
26 | }
27 |
28 | public static void validateActorInterface(Class> type) {
29 | checkIsInterface(type);
30 | for (Method method : type.getMethods()) {
31 | checkReturnTypeIsAllowed(type, method);
32 | checkDoesNotThrowExceptions(type, method);
33 | }
34 | }
35 |
36 | private static void checkIsInterface(Class> type) {
37 | if (!type.isInterface()) {
38 | throw new IllegalArgumentException("actor interfaces must be interfaces, but got " + type);
39 | }
40 | }
41 |
42 | private static void checkReturnTypeIsAllowed(Class> type, Method method) {
43 | Class> returnType = method.getReturnType();
44 | if (!ALLOWED_RETURN_TYPES.contains(returnType)) {
45 | throw new IllegalArgumentException("actor interface methods must return void or " + Future.class.getName() + ", " +
46 | "but method " + method.getName() + " of " + type + " had return type " + returnType.getName());
47 | }
48 | }
49 |
50 | private static void checkDoesNotThrowExceptions(Class> type, Method method) {
51 | Class>[] exceptionTypes = method.getExceptionTypes();
52 | if (exceptionTypes.length > 0) {
53 | throw new IllegalArgumentException("actor interface methods may not throw exceptions, " +
54 | "but method " + method.getName() + " of " + type + " throws " + format(exceptionTypes));
55 | }
56 | }
57 |
58 | private static String format(Class>[] types) {
59 | List names = new ArrayList<>();
60 | for (Class> type : types) {
61 | names.add(type.getName());
62 | }
63 | String s = names.toString();
64 | return s.substring(1, s.length() - 1);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/eventizers/dynamic/DynamicEvent.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers.dynamic;
6 |
7 | import com.google.common.base.Throwables;
8 | import fi.jumi.actors.Promise;
9 | import fi.jumi.actors.eventizers.*;
10 |
11 | import javax.annotation.Nullable;
12 | import javax.annotation.concurrent.ThreadSafe;
13 | import java.io.*;
14 | import java.lang.reflect.Method;
15 | import java.util.concurrent.Future;
16 |
17 | @ThreadSafe
18 | public class DynamicEvent implements Event, Serializable {
19 |
20 | private transient Method method;
21 | private final Object[] args;
22 | private final transient Promise.Deferred deferred;
23 |
24 | public DynamicEvent(Method method, Object[] args) {
25 | this(method, args, null);
26 | }
27 |
28 | public DynamicEvent(Method method, Object[] args, @Nullable Promise.Deferred deferred) {
29 | this.method = method;
30 | this.args = args;
31 | this.deferred = deferred;
32 | }
33 |
34 | @SuppressWarnings("unchecked")
35 | @Override
36 | public void fireOn(T target) {
37 | try {
38 | Object result = method.invoke(target, args);
39 | if (deferred != null) {
40 | deferred.delegate((Future) result);
41 | }
42 | } catch (Exception e) {
43 | throw Throwables.propagate(e);
44 | }
45 | }
46 |
47 | private void writeObject(ObjectOutputStream out) throws IOException {
48 | out.defaultWriteObject();
49 | out.writeObject(method.getName());
50 | out.writeObject(method.getDeclaringClass());
51 | out.writeObject(method.getParameterTypes());
52 | }
53 |
54 | private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
55 | in.defaultReadObject();
56 | String name = (String) in.readObject();
57 | Class> declaringClass = (Class>) in.readObject();
58 | Class>[] parameterTypes = (Class>[]) in.readObject();
59 | try {
60 | method = declaringClass.getMethod(name, parameterTypes);
61 | } catch (NoSuchMethodException e) {
62 | throw new RuntimeException(e);
63 | }
64 | }
65 |
66 | @Override
67 | public String toString() {
68 | return EventToString.format(method.getDeclaringClass().getSimpleName(), method.getName(), nonNull(args));
69 | }
70 |
71 | private static Object[] nonNull(@Nullable Object[] args) {
72 | if (args == null) {
73 | return new Object[0];
74 | }
75 | return args;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/eventizers/dynamic/DynamicEventizer.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers.dynamic;
6 |
7 | import fi.jumi.actors.eventizers.*;
8 | import fi.jumi.actors.queue.MessageSender;
9 |
10 | import javax.annotation.concurrent.Immutable;
11 | import java.lang.reflect.Proxy;
12 |
13 | /**
14 | * Supports any actor interface using reflection.
15 | */
16 | @Immutable
17 | public class DynamicEventizer implements Eventizer {
18 |
19 | private final Class type;
20 |
21 | public DynamicEventizer(Class type) {
22 | Eventizers.validateActorInterface(type);
23 | this.type = type;
24 | }
25 |
26 | @Override
27 | public Class getType() {
28 | return type;
29 | }
30 |
31 | @Override
32 | public T newFrontend(MessageSender> target) {
33 | return type.cast(Proxy.newProxyInstance(
34 | type.getClassLoader(),
35 | new Class>[]{type},
36 | new DynamicListenerToEvent<>(target))
37 | );
38 | }
39 |
40 | @Override
41 | public MessageSender> newBackend(T target) {
42 | return new EventToDynamicListener<>(target);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/eventizers/dynamic/DynamicEventizerProvider.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers.dynamic;
6 |
7 | import fi.jumi.actors.eventizers.*;
8 |
9 | import javax.annotation.concurrent.Immutable;
10 | import java.util.concurrent.ConcurrentHashMap;
11 |
12 | /**
13 | * Supports all actor interfaces using reflection.
14 | */
15 | @Immutable
16 | public class DynamicEventizerProvider implements EventizerProvider {
17 |
18 | private final ConcurrentHashMap, Eventizer>> cache = new ConcurrentHashMap<>();
19 |
20 | @SuppressWarnings("unchecked")
21 | @Override
22 | public Eventizer getEventizerForType(Class type) {
23 | Eventizer eventizer = (Eventizer) cache.get(type);
24 | if (eventizer == null) {
25 | eventizer = new DynamicEventizer<>(type);
26 | cache.put(type, eventizer);
27 | }
28 | return eventizer;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/eventizers/dynamic/DynamicListenerToEvent.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2018, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers.dynamic;
6 |
7 | import fi.jumi.actors.Promise;
8 | import fi.jumi.actors.eventizers.Event;
9 | import fi.jumi.actors.queue.MessageSender;
10 |
11 | import javax.annotation.concurrent.ThreadSafe;
12 | import java.lang.reflect.*;
13 | import java.util.concurrent.Future;
14 |
15 | @ThreadSafe
16 | public class DynamicListenerToEvent implements InvocationHandler {
17 |
18 | private final MessageSender> target;
19 |
20 | public DynamicListenerToEvent(MessageSender> target) {
21 | this.target = target;
22 | }
23 |
24 | @Override
25 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
26 | if (method.getDeclaringClass().equals(Object.class)) {
27 | return method.invoke(this, args);
28 | }
29 | // The declared return type must be assignable from Promise, because this handler will always
30 | // return Promise to the caller, even if the callee returns some other Future implementation.
31 | Class> returnType = method.getReturnType();
32 | if (returnType == Future.class || returnType == Promise.class) {
33 | Promise.Deferred deferred = Promise.defer();
34 | target.send(new DynamicEvent<>(method, args, deferred));
35 | return deferred.promise();
36 | } else {
37 | target.send(new DynamicEvent<>(method, args));
38 | return null;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/eventizers/dynamic/EventToDynamicListener.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers.dynamic;
6 |
7 | import fi.jumi.actors.eventizers.Event;
8 | import fi.jumi.actors.queue.MessageSender;
9 |
10 | import javax.annotation.concurrent.ThreadSafe;
11 |
12 | @ThreadSafe
13 | public class EventToDynamicListener implements MessageSender> {
14 |
15 | private final T target;
16 |
17 | public EventToDynamicListener(T target) {
18 | this.target = target;
19 | }
20 |
21 | @Override
22 | public void send(Event message) {
23 | message.fireOn(target);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/eventizers/dynamic/package-info.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | @ParametersAreNonnullByDefault
6 | package fi.jumi.actors.eventizers.dynamic;
7 |
8 | import javax.annotation.ParametersAreNonnullByDefault;
9 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/eventizers/package-info.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | @ParametersAreNonnullByDefault
6 | package fi.jumi.actors.eventizers;
7 |
8 | import javax.annotation.ParametersAreNonnullByDefault;
9 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/listeners/CrashEarlyFailureHandler.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.listeners;
6 |
7 | import fi.jumi.actors.SingleThreadedActors;
8 |
9 | import javax.annotation.concurrent.Immutable;
10 |
11 | /**
12 | * Used with {@link SingleThreadedActors} to fail the test when an actor throws an exception.
13 | */
14 | @Immutable
15 | public class CrashEarlyFailureHandler implements FailureHandler {
16 |
17 | @Override
18 | public void uncaughtException(Object actor, Object message, Throwable exception) {
19 | throw new RuntimeException("uncaught exception from " + actor + " when processing message " + message, exception);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/listeners/FailureHandler.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.listeners;
6 |
7 | /**
8 | * Gets notified about uncaught exceptions thrown by actors.
9 | *
10 | * @see PrintStreamFailureLogger
11 | * @see CrashEarlyFailureHandler
12 | */
13 | public interface FailureHandler {
14 |
15 | /**
16 | * Should log the exception and possibly do some error recovery.
17 | *
18 | * May stop the actor thread by interrupting the current thread. Otherwise the actor thread (and all actors in it)
19 | * will keep on processing messages.
20 | *
21 | * Should not throw any exceptions - that would result in implementation specific behaviour.
22 | */
23 | void uncaughtException(Object actor, Object message, Throwable exception);
24 | }
25 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/listeners/MessageListener.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.listeners;
6 |
7 | import java.util.concurrent.Executor;
8 |
9 | /**
10 | * Gets notified about all messages that actors send and receive. Can also listen for all commands submitted to an
11 | * {@link Executor} by wrapping it in {@link #getListenedExecutor}.
12 | *
13 | * @see NullMessageListener
14 | * @see PrintStreamMessageLogger
15 | */
16 | public interface MessageListener {
17 |
18 | void onMessageSent(Object message);
19 |
20 | void onProcessingStarted(Object actor, Object message);
21 |
22 | void onProcessingFinished();
23 |
24 | Executor getListenedExecutor(Executor realExecutor);
25 | }
26 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/listeners/NullMessageListener.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.listeners;
6 |
7 | import javax.annotation.concurrent.Immutable;
8 | import java.util.concurrent.Executor;
9 |
10 | /**
11 | * Does nothing. Meant for production use.
12 | */
13 | @Immutable
14 | public class NullMessageListener implements MessageListener {
15 |
16 | @Override
17 | public void onMessageSent(Object message) {
18 | }
19 |
20 | @Override
21 | public void onProcessingStarted(Object actor, Object message) {
22 | }
23 |
24 | @Override
25 | public void onProcessingFinished() {
26 | }
27 |
28 | @Override
29 | public Executor getListenedExecutor(Executor realExecutor) {
30 | return realExecutor;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/listeners/PrintStreamFailureLogger.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.listeners;
6 |
7 | import javax.annotation.concurrent.Immutable;
8 | import java.io.PrintStream;
9 |
10 | /**
11 | * Prints all uncaught exceptions. Meant for production use (without a logging framework).
12 | */
13 | @Immutable
14 | public class PrintStreamFailureLogger implements FailureHandler {
15 |
16 | private final PrintStream out;
17 |
18 | public PrintStreamFailureLogger(PrintStream out) {
19 | this.out = out;
20 | }
21 |
22 | @Override
23 | public void uncaughtException(Object actor, Object message, Throwable exception) {
24 | synchronized (out) {
25 | out.println("uncaught exception from " + actor + " when processing " + message);
26 | exception.printStackTrace(out);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/listeners/PrintStreamMessageLogger.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.listeners;
6 |
7 | import javax.annotation.concurrent.ThreadSafe;
8 | import java.io.PrintStream;
9 | import java.util.Locale;
10 | import java.util.concurrent.Executor;
11 |
12 | /**
13 | * Prints all messages that actors send and receive. Meant for debugging.
14 | */
15 | @ThreadSafe
16 | public class PrintStreamMessageLogger implements MessageListener {
17 |
18 | private static final String OUTGOING_MESSAGE = "->";
19 | private static final String INCOMING_MESSAGE = "<-";
20 |
21 | private final PrintStream out;
22 | private final ThreadLocal currentActor = new ThreadLocal<>();
23 | private final long startTime;
24 |
25 | public PrintStreamMessageLogger(PrintStream out) {
26 | this.out = out;
27 | this.startTime = nanoTime();
28 | }
29 |
30 | @Override
31 | public void onMessageSent(Object message) {
32 | logMessage(OUTGOING_MESSAGE, message);
33 | }
34 |
35 | @Override
36 | public void onProcessingStarted(Object actor, Object message) {
37 | currentActor.set(actor);
38 | logMessage(INCOMING_MESSAGE, message);
39 | }
40 |
41 | private void logMessage(String messageDirection, Object message) {
42 | String threadName = Thread.currentThread().getName();
43 | int messageId = System.identityHashCode(message);
44 | out.println(String.format(Locale.ENGLISH, "[%11.6f] [%s] %s %s 0x%08x %s",
45 | secondsSinceStart(), threadName, currentActorFormatted(), messageDirection, messageId, message));
46 | }
47 |
48 | private double secondsSinceStart() {
49 | long nanosSinceStart = nanoTime() - startTime;
50 | return nanosSinceStart / 1000000000.0;
51 | }
52 |
53 | protected long nanoTime() { // protected to allow overriding in tests
54 | return System.nanoTime();
55 | }
56 |
57 | private String currentActorFormatted() {
58 | Object currentActor = this.currentActor.get();
59 | if (currentActor == null) {
60 | return "";
61 | }
62 | return currentActor.toString();
63 | }
64 |
65 | @Override
66 | public void onProcessingFinished() {
67 | currentActor.remove();
68 | }
69 |
70 | @Override
71 | public Executor getListenedExecutor(Executor realExecutor) {
72 | return new LoggedExecutor(realExecutor);
73 | }
74 |
75 |
76 | @ThreadSafe
77 | private class LoggedExecutor implements Executor {
78 | private final Executor realExecutor;
79 |
80 | public LoggedExecutor(Executor realExecutor) {
81 | this.realExecutor = realExecutor;
82 | }
83 |
84 | @Override
85 | public void execute(Runnable realCommand) {
86 | onMessageSent(realCommand);
87 | realExecutor.execute(new LoggedRunnable(realExecutor, realCommand));
88 | }
89 | }
90 |
91 | @ThreadSafe
92 | private class LoggedRunnable implements Runnable {
93 | private final Executor realExecutor;
94 | private final Runnable realCommand;
95 |
96 | public LoggedRunnable(Executor realExecutor, Runnable realCommand) {
97 | this.realExecutor = realExecutor;
98 | this.realCommand = realCommand;
99 | }
100 |
101 | @Override
102 | public void run() {
103 | onProcessingStarted(realExecutor, realCommand);
104 | try {
105 | realCommand.run();
106 | } finally {
107 | onProcessingFinished();
108 | }
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/listeners/package-info.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | @ParametersAreNonnullByDefault
6 | package fi.jumi.actors.listeners;
7 |
8 | import javax.annotation.ParametersAreNonnullByDefault;
9 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/package-info.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | @ParametersAreNonnullByDefault
6 | package fi.jumi.actors;
7 |
8 | import javax.annotation.ParametersAreNonnullByDefault;
9 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/queue/MessageQueue.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.queue;
6 |
7 | import javax.annotation.Nullable;
8 | import javax.annotation.concurrent.ThreadSafe;
9 | import java.util.concurrent.*;
10 |
11 | /**
12 | * Asynchronous unbounded queue for message passing.
13 | */
14 | @ThreadSafe
15 | public class MessageQueue implements MessageSender, MessageReceiver {
16 |
17 | private final BlockingQueue queue = new LinkedBlockingQueue<>();
18 |
19 | @Override
20 | public void send(T message) {
21 | queue.add(message);
22 | }
23 |
24 | @Override
25 | public T take() throws InterruptedException {
26 | return queue.take();
27 | }
28 |
29 | @Nullable
30 | @Override
31 | public T poll() {
32 | return queue.poll();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/queue/MessageReceiver.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.queue;
6 |
7 | import javax.annotation.Nullable;
8 |
9 | public interface MessageReceiver {
10 |
11 | T take() throws InterruptedException;
12 |
13 | @Nullable
14 | T poll();
15 | }
16 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/queue/MessageSender.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.queue;
6 |
7 | public interface MessageSender {
8 |
9 | void send(T message);
10 | }
11 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/queue/package-info.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | @ParametersAreNonnullByDefault
6 | package fi.jumi.actors.queue;
7 |
8 | import javax.annotation.ParametersAreNonnullByDefault;
9 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/workers/WorkerCounter.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.workers;
6 |
7 | import fi.jumi.actors.ActorRef;
8 |
9 | import javax.annotation.concurrent.*;
10 | import java.util.concurrent.Executor;
11 | import java.util.concurrent.atomic.AtomicInteger;
12 |
13 | /**
14 | * Fires a callback after a transitive bunch of worker threads are finished.
15 | */
16 | @ThreadSafe
17 | public class WorkerCounter implements Executor {
18 |
19 | private final Executor realExecutor;
20 | private final AtomicInteger activeWorkers = new AtomicInteger(0);
21 |
22 | @GuardedBy("this")
23 | private ActorRef onFinished;
24 |
25 | public WorkerCounter(Executor realExecutor) {
26 | this.realExecutor = realExecutor;
27 | }
28 |
29 | @Override
30 | public void execute(Runnable command) {
31 | realExecutor.execute(new Worker(command));
32 | }
33 |
34 | /**
35 | * Calls {@link WorkerListener#onAllWorkersFinished()} on the specified callback after all commands previously
36 | * submitted to {@link #execute(Runnable)}, and recursively all commands which they submitted to {@link
37 | * #execute(Runnable)}, have finished executing.
38 | */
39 | public synchronized void afterPreviousWorkersFinished(ActorRef onFinished) {
40 | if (this.onFinished != null) {
41 | throw new IllegalStateException("a callback already exists; wait for the workers to finish before setting a new callback");
42 | }
43 | this.onFinished = onFinished;
44 | if (activeWorkers.get() == 0) {
45 | fireAllWorkersFinished();
46 | }
47 | }
48 |
49 | private synchronized void fireAllWorkersFinished() {
50 | if (onFinished != null) {
51 | onFinished.tell().onAllWorkersFinished();
52 | onFinished = null;
53 | }
54 | }
55 |
56 |
57 | // Used only from the Worker class, to make sure that they are always called
58 |
59 | private void fireWorkerCreated() {
60 | activeWorkers.incrementAndGet();
61 | }
62 |
63 | private void fireWorkerFinished() {
64 | int workers = activeWorkers.decrementAndGet();
65 | if (workers == 0) {
66 | fireAllWorkersFinished();
67 | }
68 | }
69 |
70 | @ThreadSafe
71 | private class Worker implements Runnable {
72 | private final Runnable command;
73 |
74 | public Worker(Runnable command) {
75 | fireWorkerCreated();
76 | this.command = command;
77 | }
78 |
79 | @Override
80 | public void run() {
81 | try {
82 | command.run();
83 | } finally {
84 | fireWorkerFinished();
85 | }
86 | }
87 |
88 | @Override
89 | public String toString() {
90 | return command.toString();
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/workers/WorkerListener.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.workers;
6 |
7 | public interface WorkerListener {
8 |
9 | void onAllWorkersFinished();
10 | }
11 |
--------------------------------------------------------------------------------
/jumi-actors/src/main/java/fi/jumi/actors/workers/package-info.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | @ParametersAreNonnullByDefault
6 | package fi.jumi.actors.workers;
7 |
8 | import javax.annotation.ParametersAreNonnullByDefault;
9 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/ActorInterfaceContractsTest.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2018, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors;
6 |
7 | import fi.jumi.actors.eventizers.Eventizers;
8 | import org.junit.*;
9 | import org.junit.rules.ExpectedException;
10 |
11 | import java.util.concurrent.*;
12 |
13 | public class ActorInterfaceContractsTest {
14 |
15 | public static final Class INVALID_ACTOR_INTERFACE = HasNonVoidMethods.class;
16 |
17 | @Rule
18 | public final ExpectedException thrown = ExpectedException.none();
19 |
20 | @Test
21 | public void must_be_interfaces() {
22 | thrown.expect(IllegalArgumentException.class);
23 | thrown.expectMessage("actor interfaces must be interfaces, " +
24 | "but got class fi.jumi.actors.ActorInterfaceContractsTest$NotAnInterface");
25 |
26 | Eventizers.validateActorInterface(NotAnInterface.class);
27 | }
28 |
29 | @Test
30 | public void methods_may_return_void_or_future() {
31 | Eventizers.validateActorInterface(AllAllowedReturnTypes.class);
32 | }
33 |
34 | @Test
35 | public void methods_must_not_return_other_types() {
36 | thrown.expect(IllegalArgumentException.class);
37 | thrown.expectMessage("actor interface methods must return void or java.util.concurrent.Future, " +
38 | "but method onSomething of interface fi.jumi.actors.ActorInterfaceContractsTest$HasNonVoidMethods " +
39 | "had return type java.lang.String");
40 |
41 | Eventizers.validateActorInterface(HasNonVoidMethods.class);
42 | }
43 |
44 | @Test
45 | public void methods_must_not_throw_exceptions() {
46 | thrown.expect(IllegalArgumentException.class);
47 | thrown.expectMessage("actor interface methods may not throw exceptions, " +
48 | "but method onSomething of interface fi.jumi.actors.ActorInterfaceContractsTest$HasExceptionThrowingMethods " +
49 | "throws java.lang.Exception");
50 |
51 | Eventizers.validateActorInterface(HasExceptionThrowingMethods.class);
52 | }
53 |
54 | @Test
55 | public void methods_returning_future_must_use_the_interface_instead_of_the_implementation_class() {
56 | thrown.expect(IllegalArgumentException.class);
57 | thrown.expectMessage("actor interface methods must return void or java.util.concurrent.Future, " +
58 | "but method returnsFutureTask of interface fi.jumi.actors.ActorInterfaceContractsTest$DeclaresWrongFutureImplementation " +
59 | "had return type java.util.concurrent.FutureTask");
60 |
61 | Eventizers.validateActorInterface(DeclaresWrongFutureImplementation.class);
62 | }
63 |
64 |
65 | // guinea pigs
66 |
67 | public static abstract class NotAnInterface {
68 | }
69 |
70 | public interface AllAllowedReturnTypes {
71 |
72 | void returnsVoid();
73 |
74 | Future> returnsFuture();
75 |
76 | Promise> returnsPromise();
77 | }
78 |
79 | public interface HasNonVoidMethods {
80 | String onSomething();
81 | }
82 |
83 | public interface HasExceptionThrowingMethods {
84 | void onSomething() throws Exception;
85 | }
86 |
87 | public interface DeclaresWrongFutureImplementation {
88 | FutureTask> returnsFutureTask();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/DummyException.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors;
6 |
7 | public class DummyException extends RuntimeException {
8 |
9 | public DummyException() {
10 | }
11 |
12 | public DummyException(String message) {
13 | super(message);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/EventSpy.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors;
6 |
7 | import java.util.*;
8 | import java.util.concurrent.ConcurrentLinkedQueue;
9 |
10 | import static org.hamcrest.MatcherAssert.assertThat;
11 | import static org.hamcrest.Matchers.is;
12 |
13 | public class EventSpy {
14 |
15 | private final Queue events = new ConcurrentLinkedQueue<>();
16 |
17 | public void log(String event) {
18 | events.add(event);
19 | }
20 |
21 | public void await(int expectedCount, long timeout) {
22 | long limit = System.currentTimeMillis() + timeout;
23 | while (events.size() < expectedCount) {
24 | if (System.currentTimeMillis() > limit) {
25 | throw new AssertionError("timed out; expected " + expectedCount + " or more events but got " + events);
26 | }
27 | Thread.yield();
28 | }
29 | }
30 |
31 | public void assertContains(String... expected) {
32 | List actual = new ArrayList<>(events);
33 | assertThat("events", actual, is(Arrays.asList(expected)));
34 | }
35 |
36 | public void expectNoMoreEvents() {
37 | int before = events.size();
38 | try {
39 | Thread.sleep(1);
40 | } catch (InterruptedException e) {
41 | Thread.currentThread().interrupt();
42 | }
43 | int after = events.size();
44 | assertThat("expected no more events, but still got some more: " + events, after, is(before));
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/Matchers.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors;
6 |
7 | import org.hamcrest.*;
8 |
9 | public class Matchers {
10 |
11 | public static Matcher containsLineWithWords(final String... expectedWords) {
12 | return new TypeSafeMatcher() {
13 | @Override
14 | protected boolean matchesSafely(String item) {
15 | for (String line : item.split("\n")) {
16 | if (lineContainsExpectedWords(line)) {
17 | return true;
18 | }
19 | }
20 | return false;
21 | }
22 |
23 | private boolean lineContainsExpectedWords(String line) {
24 | int pos = 0;
25 | for (String expectedWord : expectedWords) {
26 | pos = line.indexOf(expectedWord, pos);
27 | if (pos < 0) {
28 | return false;
29 | }
30 | pos++;
31 | }
32 | return true;
33 | }
34 |
35 | @Override
36 | public void describeTo(Description description) {
37 | description.appendText("contains line with words ").appendValueList("", ", ", "", expectedWords);
38 | }
39 | };
40 | }
41 |
42 | public static Matcher hasCause(final Matcher> causeMatcher) {
43 | return new TypeSafeMatcher() {
44 | @Override
45 | protected boolean matchesSafely(Throwable item) {
46 | return causeMatcher.matches(item.getCause());
47 | }
48 |
49 | @Override
50 | public void describeTo(Description description) {
51 | description.appendText("has cause ")
52 | .appendDescriptionOf(causeMatcher);
53 | }
54 | };
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/MultiThreadedActorsTest.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors;
6 |
7 | import fi.jumi.actors.eventizers.EventizerProvider;
8 | import fi.jumi.actors.listeners.*;
9 | import org.junit.*;
10 |
11 | import java.util.concurrent.*;
12 |
13 | import static org.hamcrest.MatcherAssert.assertThat;
14 | import static org.hamcrest.Matchers.*;
15 |
16 | public class MultiThreadedActorsTest extends ActorsContract {
17 |
18 | private final ExecutorService executor = Executors.newCachedThreadPool();
19 |
20 | @Override
21 | protected MultiThreadedActors newActors(EventizerProvider eventizerProvider, FailureHandler failureHandler, MessageListener messageListener) {
22 | return new MultiThreadedActors(executor, eventizerProvider, failureHandler, messageListener);
23 | }
24 |
25 | @Override
26 | protected void processEvents() {
27 | // noop; background threads run automatically, rely on the timeouts in the contract tests for waiting
28 | }
29 |
30 | @After
31 | public void stopExecutor() throws InterruptedException {
32 | executor.shutdownNow();
33 | }
34 |
35 |
36 | @Test
37 | public void actor_threads_are_backed_by_real_threads() throws InterruptedException {
38 | SpyDummyListener rawActor = new SpyDummyListener();
39 |
40 | ActorThread actorThread = actors.startActorThread();
41 | ActorRef actorRef = actorThread.bindActor(DummyListener.class, rawActor);
42 | actorRef.tell().onSomething("event");
43 | awaitEvents(1);
44 |
45 | assertThat(rawActor.thread, is(notNullValue()));
46 | assertThat(rawActor.thread, is(not(Thread.currentThread())));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/UncaughtExceptionCollector.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors;
6 |
7 | import fi.jumi.actors.listeners.FailureHandler;
8 |
9 | import javax.annotation.concurrent.ThreadSafe;
10 | import java.util.List;
11 | import java.util.concurrent.CopyOnWriteArrayList;
12 |
13 | @ThreadSafe
14 | public class UncaughtExceptionCollector implements Thread.UncaughtExceptionHandler, FailureHandler {
15 |
16 | private final List uncaughtExceptions = new CopyOnWriteArrayList<>();
17 |
18 | @Override
19 | public void uncaughtException(Thread t, Throwable e) {
20 | uncaughtExceptions.add(e);
21 | }
22 |
23 | @Override
24 | public void uncaughtException(Object actor, Object message, Throwable exception) {
25 | uncaughtExceptions.add(exception);
26 | }
27 |
28 | public RuntimeException throwDummyException() {
29 | throw new DummyException();
30 | }
31 |
32 | public void failIfNotEmpty() {
33 | for (Throwable uncaughtException : uncaughtExceptions) {
34 | if (!isDummyException(uncaughtException)) {
35 | throw (AssertionError)
36 | new AssertionError("there were exceptions in a background thread")
37 | .initCause(uncaughtException);
38 | }
39 | }
40 | }
41 |
42 | private static boolean isDummyException(Throwable t) {
43 | return t instanceof DummyException;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/benchmarks/BusyWaitBarrier.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.benchmarks;
6 |
7 | public class BusyWaitBarrier {
8 |
9 | private volatile boolean triggered = false;
10 |
11 | public void trigger() {
12 | triggered = true;
13 | }
14 |
15 | public void await() {
16 | while (!triggered) {
17 | // busy wait
18 | }
19 | triggered = false;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/benchmarks/Ring.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.benchmarks;
6 |
7 | import fi.jumi.actors.ActorRef;
8 |
9 | public interface Ring {
10 |
11 | void build(int ringSize, ActorRef first);
12 |
13 | void forward(int roundTrips);
14 | }
15 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/benchmarks/RingBenchmark.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2018, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.benchmarks;
6 |
7 | import com.google.caliper.*;
8 | import com.google.caliper.api.VmOptions;
9 | import com.google.caliper.runner.CaliperMain;
10 | import fi.jumi.actors.*;
11 | import fi.jumi.actors.eventizers.dynamic.DynamicEventizerProvider;
12 | import fi.jumi.actors.listeners.*;
13 |
14 | import java.util.concurrent.*;
15 |
16 | /**
17 | * Create a ring of actors and forward a message around the ring multiple times.
18 | *
19 | * Based on http://blog.grayproductions.net/articles/erlang_message_passing/
20 | */
21 | // XXX: workaround for https://stackoverflow.com/questions/29199509/caliper-error-cicompilercount-of-1-is-invalid-must-be-at-least-2
22 | @VmOptions("-XX:-TieredCompilation")
23 | public class RingBenchmark {
24 |
25 | @Param int ringSize;
26 | @Param int roundTrips;
27 |
28 | private final CyclicBarrier barrier = new CyclicBarrier(2);
29 | private final ExecutorService executor = Executors.newCachedThreadPool();
30 |
31 | private ActorRef ring;
32 |
33 | @BeforeExperiment
34 | public void before() {
35 | MultiThreadedActors actors = new MultiThreadedActors(
36 | executor,
37 | new DynamicEventizerProvider(),
38 | new CrashEarlyFailureHandler(),
39 | new NullMessageListener()
40 | );
41 | ActorThread actorThread = actors.startActorThread();
42 |
43 | ring = actorThread.bindActor(Ring.class, new RingStart(actorThread) {
44 | @Override
45 | public void forward(int roundTrips) {
46 | super.forward(roundTrips);
47 | if (roundTrips == 0) {
48 | sync(barrier);
49 | }
50 | }
51 | });
52 | ring.tell().build(ringSize, ring);
53 | }
54 |
55 | @AfterExperiment
56 | public void after() {
57 | executor.shutdownNow();
58 | }
59 |
60 | @Benchmark
61 | public void timeRingRoundTrips(int reps) {
62 | for (int i = 0; i < reps; i++) {
63 | ring.tell().forward(roundTrips);
64 | sync(barrier);
65 | }
66 | }
67 |
68 | private static void sync(CyclicBarrier barrier) {
69 | try {
70 | barrier.await();
71 | } catch (Exception e) {
72 | e.printStackTrace();
73 | }
74 | }
75 |
76 | public static void main(String[] args) {
77 | CaliperMain.main(RingBenchmark.class, new String[]{"-DringSize=1,10,100,1000,10000", "-DroundTrips=1000"});
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/benchmarks/RingStart.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.benchmarks;
6 |
7 | import fi.jumi.actors.*;
8 |
9 | public class RingStart extends RingNode {
10 |
11 | public RingStart(ActorThread actorThread) {
12 | super(actorThread);
13 | }
14 |
15 | @Override
16 | public void forward(int roundTrips) {
17 | if (roundTrips > 0) {
18 | roundTrips--;
19 | super.forward(roundTrips);
20 | }
21 | }
22 | }
23 |
24 | class RingNode implements Ring {
25 |
26 | private final ActorThread actorThread;
27 | private ActorRef next;
28 |
29 | public RingNode(ActorThread actorThread) {
30 | this.actorThread = actorThread;
31 | }
32 |
33 | @Override
34 | public void build(int ringSize, ActorRef first) {
35 | ringSize--;
36 | if (ringSize > 0) {
37 | next = actorThread.bindActor(Ring.class, new RingNode(actorThread));
38 | next.tell().build(ringSize, first);
39 | } else {
40 | next = first;
41 | }
42 | }
43 |
44 | @Override
45 | public void forward(int roundTrips) {
46 | next.tell().forward(roundTrips);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/benchmarks/RingTest.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.benchmarks;
6 |
7 | import fi.jumi.actors.*;
8 | import fi.jumi.actors.eventizers.dynamic.DynamicEventizerProvider;
9 | import fi.jumi.actors.listeners.*;
10 | import org.junit.Test;
11 |
12 | import static org.mockito.Mockito.*;
13 |
14 | public class RingTest {
15 |
16 | private final MessageListener messageListener = mock(MessageListener.class);
17 | private final SingleThreadedActors actors = new SingleThreadedActors(
18 | new DynamicEventizerProvider(),
19 | new CrashEarlyFailureHandler(),
20 | messageListener
21 | );
22 | private final ActorThread actorThread = actors.startActorThread();
23 |
24 |
25 | @Test
26 | public void one_node_one_round_trip() {
27 | int ringSize = 1;
28 | int roundTrips = 1;
29 | checkRingContract(ringSize, roundTrips);
30 | }
31 |
32 | @Test
33 | public void many_nodes_one_round_trip() {
34 | int ringSize = 10;
35 | int roundTrips = 1;
36 | checkRingContract(ringSize, roundTrips);
37 | }
38 |
39 | @Test
40 | public void one_node_many_round_trips() {
41 | int ringSize = 1;
42 | int roundTrips = 10;
43 | checkRingContract(ringSize, roundTrips);
44 | }
45 |
46 | @Test
47 | public void many_nodes_many_round_trips() {
48 | int ringSize = 3;
49 | int roundTrips = 5;
50 | checkRingContract(ringSize, roundTrips);
51 | }
52 |
53 | private void checkRingContract(int ringSize, int roundTrips) {
54 | ActorRef ring = createRing(ringSize);
55 |
56 | doRoundTrips(ring, roundTrips);
57 |
58 | verifyNumberOfMessagesSent(ringSize, roundTrips);
59 | }
60 |
61 | private ActorRef createRing(int ringSize) {
62 | ActorRef first = actorThread.bindActor(Ring.class, new RingStart(actorThread));
63 | first.tell().build(ringSize, first);
64 | actors.processEventsUntilIdle();
65 | return first;
66 | }
67 |
68 | private void doRoundTrips(ActorRef ring, int roundTrips) {
69 | reset(messageListener);
70 | ring.tell().forward(roundTrips);
71 | actors.processEventsUntilIdle();
72 | }
73 |
74 | private void verifyNumberOfMessagesSent(int ringSize, int roundTrips) {
75 | int initialMessage = 1;
76 | verify(messageListener, times(ringSize * roundTrips + initialMessage)).onProcessingStarted(any(), any());
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/benchmarks/WarmStartupBenchmark.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2018, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.benchmarks;
6 |
7 | import com.google.caliper.Benchmark;
8 | import com.google.caliper.api.VmOptions;
9 | import com.google.caliper.runner.CaliperMain;
10 | import fi.jumi.actors.*;
11 | import fi.jumi.actors.eventizers.dynamic.DynamicEventizerProvider;
12 | import fi.jumi.actors.listeners.*;
13 |
14 | import java.util.concurrent.*;
15 |
16 | /**
17 | * Create the actors container, send and receive one message.
18 | */
19 | // XXX: workaround for https://stackoverflow.com/questions/29199509/caliper-error-cicompilercount-of-1-is-invalid-must-be-at-least-2
20 | @VmOptions("-XX:-TieredCompilation")
21 | public class WarmStartupBenchmark {
22 |
23 | private final BusyWaitBarrier barrier = new BusyWaitBarrier();
24 | private final ExecutorService executor = Executors.newCachedThreadPool();
25 |
26 | @Benchmark
27 | public void timeMultiThreadedActors(int reps) throws Exception {
28 | for (int i = 0; i < reps; i++) {
29 | MultiThreadedActors actors = new MultiThreadedActors(
30 | executor,
31 | new DynamicEventizerProvider(),
32 | new CrashEarlyFailureHandler(),
33 | new NullMessageListener()
34 | );
35 | ActorThread actorThread = actors.startActorThread();
36 |
37 | ActorRef runnable = actorThread.bindActor(Runnable.class, new Runnable() {
38 | @Override
39 | public void run() {
40 | barrier.trigger();
41 | }
42 | });
43 | runnable.tell().run();
44 | barrier.await();
45 |
46 | actorThread.stop();
47 | }
48 | }
49 |
50 | @Benchmark
51 | public void timeSingleThreadedActors(int reps) {
52 | for (int i = 0; i < reps; i++) {
53 | SingleThreadedActors actors = new SingleThreadedActors(
54 | new DynamicEventizerProvider(),
55 | new CrashEarlyFailureHandler(),
56 | new NullMessageListener()
57 | );
58 | ActorThread actorThread = actors.startActorThread();
59 |
60 | ActorRef runnable = actorThread.bindActor(Runnable.class, new Runnable() {
61 | @Override
62 | public void run() {
63 | }
64 | });
65 | runnable.tell().run();
66 |
67 | actors.processEventsUntilIdle();
68 | }
69 | }
70 |
71 | public static void main(String[] args) {
72 | CaliperMain.main(WarmStartupBenchmark.class, new String[0]);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/eventizers/ComposedEventizerProviderTest.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers;
6 |
7 | import fi.jumi.actors.queue.MessageSender;
8 | import org.junit.*;
9 | import org.junit.rules.ExpectedException;
10 |
11 | import static org.hamcrest.MatcherAssert.assertThat;
12 | import static org.hamcrest.Matchers.is;
13 |
14 | public class ComposedEventizerProviderTest {
15 |
16 | @Rule
17 | public ExpectedException thrown = ExpectedException.none();
18 |
19 | @Test
20 | public void returns_an_eventizer_which_corresponds_the_specified_type() {
21 | Class type1 = Integer.class;
22 | Eventizer eventizer1 = new DummyEventizer<>(type1);
23 | Class type2 = Double.class;
24 | Eventizer eventizer2 = new DummyEventizer<>(type2);
25 |
26 | ComposedEventizerProvider provider = new ComposedEventizerProvider(eventizer1, eventizer2);
27 |
28 | assertThat(provider.getEventizerForType(type1), is(eventizer1));
29 | assertThat(provider.getEventizerForType(type2), is(eventizer2));
30 | }
31 |
32 | @Test
33 | public void cannot_return_eventizers_for_unsupported_types() {
34 | ComposedEventizerProvider provider = new ComposedEventizerProvider();
35 |
36 | thrown.expect(IllegalArgumentException.class);
37 | thrown.expectMessage("unsupported type");
38 | thrown.expectMessage(NoEventizerForThisListener.class.getName());
39 |
40 | provider.getEventizerForType(NoEventizerForThisListener.class);
41 | }
42 |
43 |
44 | private interface NoEventizerForThisListener {
45 | }
46 |
47 | private static class DummyEventizer implements Eventizer {
48 | private final Class type;
49 |
50 | private DummyEventizer(Class type) {
51 | this.type = type;
52 | }
53 |
54 | @Override
55 | public Class getType() {
56 | return type;
57 | }
58 |
59 | @Override
60 | public T newFrontend(MessageSender> target) {
61 | return null;
62 | }
63 |
64 | @Override
65 | public MessageSender> newBackend(Object target) {
66 | return null;
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/eventizers/EventToStringTest.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers;
6 |
7 | import org.junit.Test;
8 |
9 | import java.nio.charset.Charset;
10 |
11 | import static org.hamcrest.MatcherAssert.assertThat;
12 | import static org.hamcrest.Matchers.is;
13 |
14 | public class EventToStringTest {
15 |
16 | @Test
17 | public void shows_the_method_and_all_its_arguments() {
18 | assertThat(EventToString.format("TheClass", "theMethod"), is("TheClass.theMethod()"));
19 | assertThat(EventToString.format("TheClass", "theMethod", 123), is("TheClass.theMethod(123)"));
20 | assertThat(EventToString.format("TheClass", "theMethod", 123, true), is("TheClass.theMethod(123, true)"));
21 | }
22 |
23 | @Test
24 | public void does_not_crash_to_null_arguments() {
25 | assertThat(EventToString.format("TheClass", "theMethod", (Object) null), is("TheClass.theMethod(null)"));
26 | }
27 |
28 | @Test
29 | public void quotes_string_arguments() {
30 | assertThat(EventToString.format("TheClass", "theMethod", "foo"), is("TheClass.theMethod(\"foo\")"));
31 | }
32 |
33 | @Test
34 | public void shows_strings_using_string_literal_escape_sequences() {
35 | assertThat(escapeSpecialChars("\b"), is("\\b"));
36 | assertThat(escapeSpecialChars("\t"), is("\\t"));
37 | assertThat(escapeSpecialChars("\n"), is("\\n"));
38 | assertThat(escapeSpecialChars("\f"), is("\\f"));
39 | assertThat(escapeSpecialChars("\r"), is("\\r"));
40 | assertThat(escapeSpecialChars("\""), is("\\\""));
41 | assertThat(escapeSpecialChars("\\"), is("\\\\"));
42 | }
43 |
44 | @Test
45 | public void escapes_ISO_control_characters() {
46 | assertThat(escapeSpecialChars("\0"), is("\\u0000"));
47 | assertThat(escapeSpecialChars("\1"), is("\\u0001"));
48 | assertThat(escapeSpecialChars("\u001f"), is("\\u001f"));
49 | }
50 |
51 | @Test
52 | public void escapes_unmappable_characters_for_the_current_charset() {
53 | assertThat(escapeSpecialChars("åäö", Charset.forName("ISO-8859-1")), is("åäö"));
54 | assertThat(escapeSpecialChars("åäö", Charset.forName("US-ASCII")), is("\\u00e5\\u00e4\\u00f6"));
55 | }
56 |
57 |
58 | // helpers
59 |
60 | private static String escapeSpecialChars(String arg) {
61 | return escapeSpecialChars(arg, Charset.forName("UTF-8"));
62 | }
63 |
64 | private static String escapeSpecialChars(String arg, Charset charset) {
65 | return new EventToString(charset).escapeSpecialChars(arg).build();
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/eventizers/dynamic/DynamicEventizerBenchmark.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2018, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers.dynamic;
6 |
7 | import com.google.caliper.Benchmark;
8 | import com.google.caliper.api.VmOptions;
9 | import com.google.caliper.runner.CaliperMain;
10 | import fi.jumi.actors.eventizers.Eventizer;
11 |
12 | // XXX: workaround for https://stackoverflow.com/questions/29199509/caliper-error-cicompilercount-of-1-is-invalid-must-be-at-least-2
13 | @VmOptions("-XX:-TieredCompilation")
14 | public class DynamicEventizerBenchmark {
15 |
16 | private final DynamicEventizerProvider provider = new DynamicEventizerProvider();
17 |
18 | @Benchmark
19 | public int timeEventizerLookup(int reps) {
20 | int junk = 0;
21 | for (int i = 0; i < reps; i++) {
22 | Eventizer> eventizer = provider.getEventizerForType(ListenerWithLotsOfMethods.class);
23 | junk += eventizer.hashCode();
24 | }
25 | return junk;
26 | }
27 |
28 |
29 | public static void main(String[] args) {
30 | CaliperMain.main(DynamicEventizerBenchmark.class, new String[0]);
31 | }
32 |
33 | private interface ListenerWithLotsOfMethods {
34 |
35 | void event1();
36 |
37 | void event2();
38 |
39 | void event3();
40 |
41 | void event4();
42 |
43 | void event5();
44 |
45 | void event6();
46 |
47 | void event7();
48 |
49 | void event8();
50 |
51 | void event9();
52 |
53 | void event10();
54 |
55 | void event11();
56 |
57 | void event12();
58 |
59 | void event13();
60 |
61 | void event14();
62 |
63 | void event15();
64 |
65 | void event16();
66 |
67 | void event17();
68 |
69 | void event18();
70 |
71 | void event19();
72 |
73 | void event20();
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/eventizers/dynamic/DynamicEventizerProviderTest.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.eventizers.dynamic;
6 |
7 | import fi.jumi.actors.eventizers.Eventizer;
8 | import org.junit.Test;
9 |
10 | import static org.fest.assertions.Assertions.assertThat;
11 |
12 | public class DynamicEventizerProviderTest {
13 |
14 | @Test
15 | public void returns_an_eventizer_for_any_listener_interface() {
16 | DynamicEventizerProvider provider = new DynamicEventizerProvider();
17 |
18 | Eventizer eventizer = provider.getEventizerForType(DummyListener.class);
19 |
20 | assertThat(eventizer.getType()).isEqualTo(DummyListener.class);
21 | }
22 |
23 | @Test
24 | public void caches_the_eventizer_instances() {
25 | DynamicEventizerProvider provider = new DynamicEventizerProvider();
26 |
27 | Eventizer eventizer1 = provider.getEventizerForType(DummyListener.class);
28 | Eventizer eventizer2 = provider.getEventizerForType(DummyListener.class);
29 |
30 | assertThat(eventizer1).isSameAs(eventizer2);
31 | }
32 |
33 |
34 | private interface DummyListener {
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/examples/HelloWorld.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.examples;
6 |
7 | import fi.jumi.actors.*;
8 | import fi.jumi.actors.eventizers.dynamic.DynamicEventizerProvider;
9 | import fi.jumi.actors.listeners.*;
10 |
11 | import java.util.concurrent.*;
12 |
13 | public class HelloWorld {
14 |
15 | public static void main(String[] args) {
16 |
17 | // Configure the actors implementation and its dependencies:
18 | // - Executor for running the actors (when using MultiThreadedActors)
19 | // - Eventizers for converting method calls to event objects and back again
20 | // - Handler for uncaught exceptions from actors
21 | // - Logging of all messages for debug purposes (here disabled)
22 | ExecutorService actorsThreadPool = Executors.newCachedThreadPool();
23 | Actors actors = new MultiThreadedActors(
24 | actorsThreadPool,
25 | new DynamicEventizerProvider(),
26 | new CrashEarlyFailureHandler(),
27 | new NullMessageListener()
28 | );
29 |
30 | // Start up a thread where messages to actors will be executed
31 | ActorThread actorThread = actors.startActorThread();
32 |
33 | // Create an actor to be executed in this actor thread. Some guidelines:
34 | // - Never pass around a direct reference to an actor, but always use ActorRef
35 | // - To avoid confusion, also avoid passing around the proxy returned by ActorRef.tell(),
36 | // though that may sometimes be warranted when interacting with actor-unaware code
37 | // or if you wish to avoid the dependency to ActorRef
38 | ActorRef helloGreeter = actorThread.bindActor(Greeter.class, new Greeter() {
39 | @Override
40 | public void sayGreeting(String name) {
41 | System.out.println("Hello " + name + " from " + Thread.currentThread().getName());
42 | }
43 | });
44 |
45 | // The pattern for sending messages to actors
46 | helloGreeter.tell().sayGreeting("World");
47 |
48 | // "Wazzup" should be printed before "Hello World" and the thread name will be different
49 | System.out.println("Wazzup from " + Thread.currentThread().getName());
50 |
51 | // Tell all actors in this actor thread to stop themselves after processing
52 | // all previously sent messages. An actor can also itself call Thread.currentThread().interrupt()
53 | actorThread.stop();
54 |
55 | // Finally do an orderly shutdown of the executor.
56 | // Calling shutdownNow() on the executor would interrupt and stop all
57 | // actor threads immediately without waiting for messages to be processed.
58 | actorsThreadPool.shutdown();
59 | }
60 |
61 | public interface Greeter {
62 |
63 | // Methods on actor interfaces must return void and not have throws declarations.
64 | // Any parameters may be used, but immutable ones are strongly encouraged.
65 | // Actors should always be passed around as ActorRefs.
66 | void sayGreeting(String name);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/examples/PiTest.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.examples;
6 |
7 | import fi.jumi.actors.*;
8 | import fi.jumi.actors.eventizers.dynamic.DynamicEventizerProvider;
9 | import fi.jumi.actors.listeners.*;
10 | import org.junit.Test;
11 |
12 | import java.util.*;
13 | import java.util.concurrent.Executor;
14 |
15 | import static fi.jumi.actors.examples.Pi.*;
16 | import static org.hamcrest.MatcherAssert.assertThat;
17 | import static org.hamcrest.Matchers.*;
18 |
19 | public class PiTest {
20 |
21 | /**
22 | * An example of a unit test. We don't need to create the actors container, we just
23 | * need to wrap our test double into an ActorRef (never do this in production code!)
24 | */
25 | @Test
26 | public void each_worker_calculates_one_part_of_the_pi_approximation() {
27 | final List results = new ArrayList<>();
28 | ActorRef listener = ActorRef.wrap((ResultListener) new ResultListener() {
29 | @Override
30 | public void onResult(double result) {
31 | results.add(result);
32 | }
33 | });
34 |
35 | int nrOfElements = 10;
36 | new Worker(0, nrOfElements, listener).run();
37 | new Worker(1, nrOfElements, listener).run();
38 | new Worker(2, nrOfElements, listener).run();
39 |
40 | assertThat(results.size(), is(3));
41 | assertThat(results.get(0), is(closeTo(3.041, 0.001)));
42 | assertThat(results.get(1), is(closeTo(0.049, 0.001)));
43 | assertThat(results.get(2), is(closeTo(0.016, 0.001)));
44 | }
45 |
46 | /**
47 | * An example of an integration test. We use a single-threaded actors container
48 | * so we can test easily, deterministically, how the actors play together.
49 | */
50 | @Test
51 | public void master_combines_the_results_from_workers() {
52 | class SpyListener implements ResultListener {
53 | double result;
54 |
55 | @Override
56 | public void onResult(double result) {
57 | this.result = result;
58 | }
59 | }
60 | SpyListener spy = new SpyListener();
61 | SingleThreadedActors actors = new SingleThreadedActors(
62 | new DynamicEventizerProvider(),
63 | // Will rethrow any exceptions to this test thread
64 | new CrashEarlyFailureHandler(),
65 | new NullMessageListener()
66 | );
67 | Executor workersThreadPool = actors.getExecutor(); // Also single-threaded, runs in this same test thread
68 | ActorThread thread = actors.startActorThread();
69 |
70 | ActorRef master = thread.bindActor(Calculator.class, new Master(thread, workersThreadPool, 10, 10));
71 | ActorRef listener = thread.bindActor(ResultListener.class, spy);
72 | master.tell().approximatePi(listener);
73 |
74 | actors.processEventsUntilIdle(); // Any exceptions from actors would be thrown here
75 | assertThat(spy.result, is(closeTo(3.14, 0.01)));
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/listeners/ContainsLineWithWordsMatcherTest.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.listeners;
6 |
7 | import org.junit.Test;
8 |
9 | import static fi.jumi.actors.Matchers.containsLineWithWords;
10 | import static org.hamcrest.MatcherAssert.assertThat;
11 |
12 | public class ContainsLineWithWordsMatcherTest {
13 |
14 | @Test
15 | public void passes_when_input_contains_only_expected_words() {
16 | assertThat("foo", containsLineWithWords("foo"));
17 | }
18 |
19 | @Test(expected = AssertionError.class)
20 | public void fails_when_input_does_not_contain_any_expected_words() {
21 | assertThat("foo", containsLineWithWords("bar"));
22 | }
23 |
24 | @Test
25 | public void passes_when_at_least_one_line_has_the_expected_words() {
26 | assertThat("before\nfoo\nafter", containsLineWithWords("foo"));
27 | }
28 |
29 | @Test
30 | public void passes_when_the_same_line_has_additional_words_around_the_expected_words() {
31 | assertThat("before foo after", containsLineWithWords("foo"));
32 | }
33 |
34 | @Test
35 | public void passes_when_the_same_line_has_additional_words_between_expected_words() {
36 | assertThat("foo middle bar", containsLineWithWords("foo", "bar"));
37 | }
38 |
39 | @Test(expected = AssertionError.class)
40 | public void fails_when_the_expected_words_are_in_wrong_order() {
41 | assertThat("bar middle foo", containsLineWithWords("foo", "bar"));
42 | }
43 |
44 | @Test(expected = AssertionError.class)
45 | public void fails_when_expected_word_is_not_repeated_enough_many_times() {
46 | assertThat("xx", containsLineWithWords("x", "x", "x"));
47 | }
48 |
49 | @Test(expected = AssertionError.class)
50 | public void fails_when_line_has_only_some_of_the_expected_words() {
51 | assertThat("foo", containsLineWithWords("foo", "bar"));
52 | }
53 |
54 | @Test(expected = AssertionError.class)
55 | public void fails_when_expected_words_are_on_different_lines() {
56 | assertThat("foo\nbar", containsLineWithWords("foo", "bar"));
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/listeners/PrintStreamFailureLoggerTest.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.listeners;
6 |
7 | import fi.jumi.actors.DummyException;
8 | import org.junit.Test;
9 |
10 | import java.io.*;
11 |
12 | import static org.hamcrest.MatcherAssert.assertThat;
13 | import static org.hamcrest.Matchers.containsString;
14 |
15 | public class PrintStreamFailureLoggerTest {
16 |
17 | private final ByteArrayOutputStream output = new ByteArrayOutputStream();
18 | private final PrintStreamFailureLogger failureHandler = new PrintStreamFailureLogger(new PrintStream(output));
19 |
20 | @Test
21 | public void logs_uncaught_exceptions() {
22 | failureHandler.uncaughtException("the actor", "the message", new DummyException());
23 |
24 | String output = this.output.toString();
25 | assertThat(output, containsString("uncaught exception"));
26 | assertThat(output, containsString("the actor"));
27 | assertThat(output, containsString("the message"));
28 | assertThat(output, containsString(DummyException.class.getName()));
29 | assertThat("should contain the stack trace", output, containsString("at " + getClass().getName()));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/jumi-actors/src/test/java/fi/jumi/actors/queue/MessageQueueTest.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.actors.queue;
6 |
7 | import org.junit.*;
8 |
9 | import static org.hamcrest.MatcherAssert.assertThat;
10 | import static org.hamcrest.Matchers.is;
11 |
12 | public class MessageQueueTest {
13 |
14 | private final MessageQueue messageQueue = new MessageQueue<>();
15 |
16 | @After
17 | public void clearThreadInterruptedStatus() {
18 | Thread.interrupted();
19 | }
20 |
21 | @Test
22 | public void send_does_not_change_the_interrupt_status_of_the_current_thread() {
23 | Thread.currentThread().interrupt();
24 |
25 | messageQueue.send("any message");
26 |
27 | assertThat("interrupt status after send", Thread.currentThread().isInterrupted(), is(true));
28 | }
29 |
30 | @Test
31 | public void send_enqueues_even_when_interrupted() {
32 | Thread.currentThread().interrupt();
33 |
34 | messageQueue.send("the message");
35 |
36 | assertThat(messageQueue.poll(), is("the message"));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/parent/build.properties:
--------------------------------------------------------------------------------
1 | revision=${env.RELEASE_REVISION}
2 |
--------------------------------------------------------------------------------
/parent/parent.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | fi.jumi.actors
8 | parent
9 | 1.1-SNAPSHOT
10 | parent/pom.xml
11 |
12 |
13 | project
14 | pom
15 |
16 | Jumi Actors
17 |
18 |
19 | jumi-actors
20 | jumi-actors-generator
21 | thread-safety-agent
22 | end-to-end-tests
23 | parent
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | maven-deploy-plugin
33 |
34 | true
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/project.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/thread-safety-agent/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | fi.jumi.actors
8 | parent
9 | 1.1-SNAPSHOT
10 | ../parent/pom.xml
11 |
12 |
13 | thread-safety-agent
14 | jar
15 |
16 |
17 | fi.jumi.threadsafetyagent.INTERNAL
18 |
19 |
20 |
21 |
22 |
23 | org.ow2.asm
24 | asm-debug-all
25 |
26 |
27 |
28 | commons-io
29 | commons-io
30 | test
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | maven-shade-plugin
42 |
43 |
44 |
45 | shade
46 |
47 |
48 |
49 |
50 |
51 | fi.jumi.threadsafetyagent.PreMain
52 |
53 |
54 |
55 |
56 |
57 |
58 | org.objectweb.asm
59 | ${shadedPrefix}.org.objectweb.asm
60 |
61 |
62 |
63 |
64 |
65 | asm:asm
66 |
67 | META-INF/**
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/thread-safety-agent/src/main/java/fi/jumi/threadsafetyagent/EnabledWhenAnnotatedWith.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.threadsafetyagent;
6 |
7 | import fi.jumi.threadsafetyagent.util.DoNotTransformException;
8 | import org.objectweb.asm.*;
9 |
10 | import java.util.*;
11 |
12 | public class EnabledWhenAnnotatedWith extends ClassVisitor {
13 |
14 | private final List myAnnotationDescs = new ArrayList<>();
15 | private final String enablerAnnotationDesc;
16 |
17 | public EnabledWhenAnnotatedWith(String enablerAnnotation, ClassVisitor next) {
18 | super(Opcodes.ASM5, next);
19 | this.enablerAnnotationDesc = "L" + enablerAnnotation + ";";
20 | }
21 |
22 | @Override
23 | public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
24 | myAnnotationDescs.add(desc);
25 | return super.visitAnnotation(desc, visible);
26 | }
27 |
28 | @Override
29 | public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
30 | checkIsTransformationEnabled();
31 | return super.visitField(access, name, desc, signature, value);
32 | }
33 |
34 | @Override
35 | public void visitInnerClass(String name, String outerName, String innerName, int access) {
36 | checkIsTransformationEnabled();
37 | super.visitInnerClass(name, outerName, innerName, access);
38 | }
39 |
40 | @Override
41 | public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
42 | checkIsTransformationEnabled();
43 | return super.visitMethod(access, name, desc, signature, exceptions);
44 | }
45 |
46 | @Override
47 | public void visitOuterClass(String owner, String name, String desc) {
48 | checkIsTransformationEnabled();
49 | super.visitOuterClass(owner, name, desc);
50 | }
51 |
52 | @Override
53 | public void visitEnd() {
54 | checkIsTransformationEnabled();
55 | super.visitEnd();
56 | }
57 |
58 | private void checkIsTransformationEnabled() {
59 | if (!myAnnotationDescs.contains(enablerAnnotationDesc)) {
60 | throw new DoNotTransformException();
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/thread-safety-agent/src/main/java/fi/jumi/threadsafetyagent/PreMain.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.threadsafetyagent;
6 |
7 | import java.lang.instrument.Instrumentation;
8 |
9 | public class PreMain {
10 |
11 | // For details on Java agents, see http://java.sun.com/javase/6/docs/api/java/lang/instrument/package-summary.html
12 |
13 | public static void premain(String agentArgs, Instrumentation inst) {
14 | inst.addTransformer(new ThreadSafetyCheckerTransformer());
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/thread-safety-agent/src/main/java/fi/jumi/threadsafetyagent/ThreadSafetyChecker.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.threadsafetyagent;
6 |
7 | import javax.annotation.concurrent.ThreadSafe;
8 | import java.util.*;
9 |
10 | @ThreadSafe
11 | public class ThreadSafetyChecker {
12 |
13 | /**
14 | * Not volatile, because it is not required for thread safety;
15 | * it would only would impose a read barrier and might reduce performance.
16 | */
17 | private Thread lastThread = null;
18 |
19 | private final Set calledFromThreads = new HashSet<>(1, 1);
20 | private CallLocation callLocations = null;
21 |
22 |
23 | public void checkCurrentThread() {
24 | Thread currentThread = Thread.currentThread();
25 |
26 | // TODO: measure the usefulness of this optimization later, in a scenario which is not dominated by class loading overhead
27 | // Performance optimization: avoid checking a thread twice
28 | // when called repeatedly from the same thread, which is the most common case.
29 | if (currentThread == lastThread) {
30 | return;
31 | }
32 | lastThread = currentThread;
33 |
34 | fullCheck(currentThread);
35 | }
36 |
37 | private synchronized void fullCheck(Thread currentThread) {
38 | if (calledFromThreads.contains(currentThread)) {
39 | return;
40 | }
41 | calledFromThreads.add(currentThread);
42 | callLocations = new CallLocation(callLocations);
43 | if (calledFromThreads.size() > 1) {
44 | AssertionError e = new AssertionError("non-thread-safe instance called from multiple threads: " + threadNames(calledFromThreads)) {
45 | @Override
46 | public Throwable fillInStackTrace() {
47 | return this;
48 | }
49 | };
50 | e.initCause(callLocations);
51 | throw e;
52 | }
53 | }
54 |
55 | private static String threadNames(Set threads) {
56 | List threadNames = new ArrayList<>();
57 | for (Thread thread : threads) {
58 | threadNames.add(thread.getName());
59 | }
60 | Collections.sort(threadNames);
61 | String s = "";
62 | for (String threadName : threadNames) {
63 | if (s.length() > 0) {
64 | s += ", ";
65 | }
66 | s += threadName;
67 | }
68 | return s;
69 | }
70 |
71 | private static class CallLocation extends RuntimeException {
72 | public CallLocation(CallLocation previousLocations) {
73 | super("called from thread: " + Thread.currentThread().getName(), previousLocations);
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/thread-safety-agent/src/main/java/fi/jumi/threadsafetyagent/ThreadSafetyCheckerTransformer.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.threadsafetyagent;
6 |
7 | import fi.jumi.threadsafetyagent.util.AbstractTransformationChain;
8 | import org.objectweb.asm.ClassVisitor;
9 |
10 | public class ThreadSafetyCheckerTransformer extends AbstractTransformationChain {
11 |
12 | @Override
13 | protected ClassVisitor getAdapters(ClassVisitor cv) {
14 | // the adapter declared last is processed first
15 | cv = new AddThreadSafetyChecks(cv);
16 | cv = new EnabledWhenAnnotatedWith("javax/annotation/concurrent/NotThreadSafe", cv);
17 | return cv;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/thread-safety-agent/src/main/java/fi/jumi/threadsafetyagent/util/AbstractTransformationChain.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2014, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.threadsafetyagent.util;
6 |
7 | import org.objectweb.asm.*;
8 |
9 | import java.lang.instrument.*;
10 | import java.security.ProtectionDomain;
11 |
12 | public abstract class AbstractTransformationChain implements ClassFileTransformer {
13 |
14 | @Override
15 | public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined,
16 | ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
17 | // TODO: at least the ClassLoader could be passed to the adapters, so they could examine super classes, package annotations etc.
18 | ClassReader cr = new ClassReader(classfileBuffer);
19 | ClassWriter cw;
20 | if (enableAdditiveTransformationOptimization()) {
21 | cw = new ClassWriter(cr, 0);
22 | } else {
23 | cw = new ClassWriter(0);
24 | }
25 | try {
26 | ClassVisitor cv = getAdapters(cw);
27 | cr.accept(cv, 0);
28 | return cw.toByteArray();
29 | } catch (DoNotTransformException e) {
30 | return null;
31 | }
32 | }
33 |
34 | protected boolean enableAdditiveTransformationOptimization() {
35 | return true;
36 | }
37 |
38 | protected abstract ClassVisitor getAdapters(ClassVisitor cv);
39 | }
40 |
--------------------------------------------------------------------------------
/thread-safety-agent/src/main/java/fi/jumi/threadsafetyagent/util/DoNotTransformException.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.threadsafetyagent.util;
6 |
7 | public class DoNotTransformException extends RuntimeException {
8 |
9 | @Override
10 | public Throwable fillInStackTrace() {
11 | // performance optimization: do not generate stack trace
12 | return this;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/thread-safety-agent/src/test/java/fi/jumi/threadsafetyagent/ThreadUtil.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2015, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.threadsafetyagent;
6 |
7 | import java.util.concurrent.*;
8 |
9 | public class ThreadUtil {
10 |
11 | public static void runInNewThread(String threadName, Runnable target) throws Throwable {
12 | FutureTask future = new FutureTask<>(target, null);
13 | new Thread(future, threadName).start();
14 | try {
15 | future.get();
16 | } catch (ExecutionException e) {
17 | throw e.getCause();
18 | }
19 | }
20 |
21 | public static Throwable getExceptionFromNewThread(String threadName, Runnable target) {
22 | try {
23 | runInNewThread(threadName, target);
24 | } catch (Throwable throwable) {
25 | return throwable;
26 | }
27 | throw new AssertionError("Expected to throw an exception but did not throw anything");
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/thread-safety-agent/src/test/java/fi/jumi/threadsafetyagent/util/ClassFileTransformerTest.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2014, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.threadsafetyagent.util;
6 |
7 | import org.junit.Test;
8 | import org.objectweb.asm.*;
9 |
10 | import java.lang.instrument.ClassFileTransformer;
11 |
12 | import static org.hamcrest.MatcherAssert.assertThat;
13 | import static org.hamcrest.Matchers.is;
14 |
15 | public class ClassFileTransformerTest {
16 |
17 | @Test
18 | public void instruments_classes_with_the_provided_transformer() throws Exception {
19 | AbstractTransformationChain transformer = new AbstractTransformationChain() {
20 | @Override
21 | protected ClassVisitor getAdapters(ClassVisitor cv) {
22 | return new AddEqualsMethodWhichReturnsTrue(cv);
23 | }
24 | };
25 |
26 | Object obj = createWithTransformer(ClassToInstrument.class, transformer);
27 |
28 | assertThat(obj.equals(new Object()), is(true));
29 | }
30 |
31 | @Test
32 | public void null_transformer_does_not_instrument_classes() throws Exception {
33 | NullClassFileTransformer transformer = new NullClassFileTransformer();
34 |
35 | Object obj = createWithTransformer(ClassToInstrument.class, transformer);
36 |
37 | assertThat(obj.equals(new Object()), is(false));
38 | }
39 |
40 | private static Object createWithTransformer(Class> clazz, ClassFileTransformer transformer) throws Exception {
41 | String className = clazz.getName();
42 | ClassLoader loader = new TransformationTestClassLoader(className, transformer, null);
43 | return loader.loadClass(className).newInstance();
44 | }
45 |
46 |
47 | private static class AddEqualsMethodWhichReturnsTrue extends ClassVisitor {
48 |
49 | public AddEqualsMethodWhichReturnsTrue(ClassVisitor next) {
50 | super(Opcodes.ASM5, next);
51 | }
52 |
53 | @Override
54 | public void visitEnd() {
55 | MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC, "equals", "(Ljava/lang/Object;)Z", null, null);
56 | mv.visitCode();
57 | mv.visitInsn(Opcodes.ICONST_1);
58 | mv.visitInsn(Opcodes.IRETURN);
59 | mv.visitMaxs(1, 2);
60 | mv.visitEnd();
61 | super.visitEnd();
62 | }
63 | }
64 |
65 | public static class ClassToInstrument {
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/thread-safety-agent/src/test/java/fi/jumi/threadsafetyagent/util/ClassNameMatcher.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.threadsafetyagent.util;
6 |
7 | import java.util.regex.Pattern;
8 |
9 | /**
10 | * Matches class names with a pattern syntax similar to Ant .
11 | *
12 | * foo.Bar - Single class foo.bar
13 | * foo.* - All classes in package foo
14 | * foo.** - All classes in package foo and its subpackages
15 | *
16 | */
17 | public class ClassNameMatcher {
18 |
19 | private static final String PACKAGE_REGEX = "[^\\.]*";
20 | private static final String SUBPACKAGE_REGEX = ".*";
21 |
22 | private final Pattern pattern;
23 |
24 | public ClassNameMatcher(String pattern) {
25 | this.pattern = Pattern.compile(toRegex(pattern));
26 | }
27 |
28 | private static String toRegex(String pattern) {
29 | String regex = "";
30 | for (int i = 0; i < pattern.length(); i++) {
31 | if (subpackagePatternAt(i, pattern)) {
32 | regex += SUBPACKAGE_REGEX;
33 | } else if (packagePatternAt(i, pattern)) {
34 | regex += PACKAGE_REGEX;
35 | } else {
36 | regex += quoteCharAt(i, pattern);
37 | }
38 | }
39 | return regex;
40 | }
41 |
42 | private static boolean subpackagePatternAt(int i, String pattern) {
43 | return packagePatternAt(i, pattern)
44 | && packagePatternAt(i + 1, pattern); // PIT: false warning about "Replaced integer addition with subtraction"
45 | }
46 |
47 | private static boolean packagePatternAt(int i, String pattern) {
48 | return i < pattern.length()
49 | && pattern.charAt(i) == '*';
50 | }
51 |
52 | private static String quoteCharAt(int i, String pattern) {
53 | return Pattern.quote("" + pattern.charAt(i));
54 | }
55 |
56 | public boolean matches(String className) {
57 | return pattern.matcher(className).matches();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/thread-safety-agent/src/test/java/fi/jumi/threadsafetyagent/util/ClassNameMatcherTest.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola
2 | // This software is released under the Apache License 2.0.
3 | // The license text is at http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | package fi.jumi.threadsafetyagent.util;
6 |
7 | import org.junit.Test;
8 |
9 | import static org.junit.Assert.*;
10 |
11 | public class ClassNameMatcherTest {
12 |
13 | @Test
14 | public void matching_a_single_class() {
15 | ClassNameMatcher matcher = new ClassNameMatcher("x.Foo");
16 |
17 | assertTrue("that class", matcher.matches("x.Foo"));
18 | assertFalse("other class", matcher.matches("x.Bar"));
19 | }
20 |
21 | @Test
22 | public void matching_all_classes_in_a_package() {
23 | ClassNameMatcher matcher = new ClassNameMatcher("x.*");
24 |
25 | assertTrue("that package", matcher.matches("x.Foo"));
26 | assertFalse("subpackage", matcher.matches("x.y.Foo"));
27 | assertFalse("other package", matcher.matches("y.Foo"));
28 |
29 | assertFalse("Corner case: package with the same prefix", matcher.matches("xx.Foo"));
30 | }
31 |
32 | @Test
33 | public void matching_all_classes_in_subpackages() {
34 | ClassNameMatcher matcher = new ClassNameMatcher("x.**");
35 |
36 | assertTrue("that package", matcher.matches("x.Foo"));
37 | assertTrue("subpackage", matcher.matches("x.y.Foo"));
38 | assertFalse("other package", matcher.matches("y.Foo"));
39 |
40 | assertFalse("Corner case: package with the same prefix", matcher.matches("xx.Foo"));
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/thread-safety-agent/src/test/java/fi/jumi/threadsafetyagent/util/NullClassFileTransformer.java:
--------------------------------------------------------------------------------
1 | // Copyright © 2011-2012, Esko Luontola