├── benchmarks
├── run.sh
├── src
│ └── main
│ │ └── java
│ │ └── dev
│ │ └── pixelib
│ │ └── meteor
│ │ └── benchmarks
│ │ ├── MeteorBench.java
│ │ ├── SimpleIncrement.java
│ │ └── ScoresWithMaps.java
└── pom.xml
├── .github
├── assets
│ ├── flow.png
│ ├── map-benchmark.png
│ └── increment-benchmark.png
├── workflows
│ ├── publish.yml
│ └── maven.yml
└── badges
│ ├── jacoco.svg
│ └── branches.svg
├── meteor-common
├── README.md
├── src
│ └── main
│ │ └── java
│ │ └── dev
│ │ └── pixelib
│ │ └── meteor
│ │ └── base
│ │ ├── interfaces
│ │ └── SubscriptionHandler.java
│ │ ├── enums
│ │ └── Direction.java
│ │ ├── errors
│ │ ├── MethodInvocationException.java
│ │ └── InvocationTimedOutException.java
│ │ ├── RpcSerializer.java
│ │ ├── RpcOptions.java
│ │ ├── RpcTransport.java
│ │ └── defaults
│ │ ├── GsonSerializer.java
│ │ └── LoopbackTransport.java
├── .gitignore
└── pom.xml
├── meteor-jedis
├── src
│ ├── main
│ │ └── java
│ │ │ └── dev
│ │ │ └── pixelib
│ │ │ └── meteor
│ │ │ └── transport
│ │ │ └── redis
│ │ │ ├── StringMessageBroker.java
│ │ │ ├── RedisPacketListener.java
│ │ │ ├── RedisTransport.java
│ │ │ └── RedisSubscriptionThread.java
│ └── test
│ │ └── java
│ │ └── dev
│ │ └── pixelib
│ │ └── meteor
│ │ └── transport
│ │ └── redis
│ │ ├── RedisSubscriptionThreadTest.java
│ │ ├── RedisPacketListenerTest.java
│ │ └── RedisTransportTest.java
└── pom.xml
├── meteor-core
├── src
│ ├── test
│ │ └── java
│ │ │ └── dev
│ │ │ └── pixelib
│ │ │ └── meteor
│ │ │ └── core
│ │ │ ├── utils
│ │ │ ├── MathFunctions.java
│ │ │ └── ArgumentMapperTest.java
│ │ │ ├── MeteorLoopbackTest.java
│ │ │ ├── transport
│ │ │ └── packets
│ │ │ │ ├── InvocationDescriptorTest.java
│ │ │ │ └── InvocationResponseTest.java
│ │ │ ├── trackers
│ │ │ └── IncomingInvocationTrackerTest.java
│ │ │ ├── MeteorTest.java
│ │ │ ├── invocations
│ │ │ └── PendingInvocationTest.java
│ │ │ ├── executor
│ │ │ └── ImplementationWrapperTest.java
│ │ │ └── LogicTest.java
│ └── main
│ │ └── java
│ │ └── dev
│ │ └── pixelib
│ │ └── meteor
│ │ └── core
│ │ ├── proxy
│ │ ├── MeteorMock.java
│ │ ├── ProxyInvocHandler.java
│ │ └── PendingInvocation.java
│ │ ├── trackers
│ │ ├── IncomingInvocationTracker.java
│ │ └── OutgoingInvocationTracker.java
│ │ ├── transport
│ │ ├── packets
│ │ │ ├── InvocationResponse.java
│ │ │ └── InvocationDescriptor.java
│ │ └── TransportHandler.java
│ │ ├── executor
│ │ └── ImplementationWrapper.java
│ │ ├── Meteor.java
│ │ └── utils
│ │ └── ArgumentMapper.java
├── .gitignore
└── pom.xml
├── .gitignore
├── examples
├── pom.xml
└── src
│ └── main
│ └── java
│ └── dev
│ └── pixelib
│ └── meteor
│ └── sender
│ ├── ScoreboardExample.java
│ ├── SendingUpdateMethod.java
│ └── SendingUpdateMethodRedis.java
├── PERFORMANCE.md
├── CONTRIBUTING.md
├── README.md
├── pom.xml
└── LICENSE
/benchmarks/run.sh:
--------------------------------------------------------------------------------
1 | cd target/
2 | java -jar benchmarks.jar -rf json
--------------------------------------------------------------------------------
/.github/assets/flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixelib/Meteor/HEAD/.github/assets/flow.png
--------------------------------------------------------------------------------
/.github/assets/map-benchmark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixelib/Meteor/HEAD/.github/assets/map-benchmark.png
--------------------------------------------------------------------------------
/.github/assets/increment-benchmark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixelib/Meteor/HEAD/.github/assets/increment-benchmark.png
--------------------------------------------------------------------------------
/meteor-common/README.md:
--------------------------------------------------------------------------------
1 | # meteor-base
2 | ## Description
3 | This module does/should only contain bare minimum to get the meteor module working, and providing base interfaces or implementations for future extensions to be built upon (such as different transport protocols, etc.).
--------------------------------------------------------------------------------
/meteor-common/src/main/java/dev/pixelib/meteor/base/interfaces/SubscriptionHandler.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.base.interfaces;
2 |
3 | @FunctionalInterface
4 | public interface SubscriptionHandler {
5 |
6 | boolean onPacket(byte[] packet) throws Exception;
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/meteor-jedis/src/main/java/dev/pixelib/meteor/transport/redis/StringMessageBroker.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.transport.redis;
2 |
3 | @FunctionalInterface
4 | public interface StringMessageBroker {
5 |
6 | boolean onRedisMessage(String message) throws Exception;
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/meteor-core/src/test/java/dev/pixelib/meteor/core/utils/MathFunctions.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.utils;
2 |
3 | public interface MathFunctions {
4 |
5 | int multiply(int x, int times);
6 | int add(int... numbers);
7 | int substract(int from, int... numbers);
8 | }
9 |
--------------------------------------------------------------------------------
/meteor-core/src/main/java/dev/pixelib/meteor/core/proxy/MeteorMock.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.proxy;
2 |
3 | public interface MeteorMock {
4 |
5 | /*
6 | * This interface is treated as a marker interface to identify proxies from meteor.
7 | */
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/meteor-common/src/main/java/dev/pixelib/meteor/base/enums/Direction.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.base.enums;
2 |
3 | public enum Direction {
4 |
5 | METHOD_PROXY, // the side where the method was invoked
6 | IMPLEMENTATION // the side that holds the implementation of the method
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 | !**/src/main/**/target/
4 | !**/src/test/**/target/
5 |
6 | ### IntelliJ IDEA ###
7 | .idea/
8 | *.iws
9 | *.iml
10 | *.ipr
11 |
12 | ### VS Code ###
13 | .vscode/
14 |
15 | ### Mac OS ###
16 | .DS_Store
17 | **/.flattened-pom.xml
18 | **/dependency-reduced-pom.xml
--------------------------------------------------------------------------------
/meteor-core/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 | !**/src/main/**/target/
4 | !**/src/test/**/target/
5 |
6 | ### IntelliJ IDEA ###
7 | .idea/modules.xml
8 | .idea/jarRepositories.xml
9 | .idea/compiler.xml
10 | .idea/libraries/
11 | *.iws
12 | *.iml
13 | *.ipr
14 |
15 | ### Eclipse ###
16 | .apt_generated
17 | .classpath
18 | .factorypath
19 | .project
20 | .settings
21 | .springBeans
22 | .sts4-cache
23 |
24 | ### NetBeans ###
25 | /nbproject/private/
26 | /nbbuild/
27 | /dist/
28 | /nbdist/
29 | /.nb-gradle/
30 | build/
31 | !**/src/main/**/build/
32 | !**/src/test/**/build/
33 |
34 | ### VS Code ###
35 | .vscode/
36 |
37 | ### Mac OS ###
38 | .DS_Store
--------------------------------------------------------------------------------
/meteor-common/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 | !**/src/main/**/target/
4 | !**/src/test/**/target/
5 |
6 | ### IntelliJ IDEA ###
7 | .idea/modules.xml
8 | .idea/jarRepositories.xml
9 | .idea/compiler.xml
10 | .idea/libraries/
11 | *.iws
12 | *.iml
13 | *.ipr
14 |
15 | ### Eclipse ###
16 | .apt_generated
17 | .classpath
18 | .factorypath
19 | .project
20 | .settings
21 | .springBeans
22 | .sts4-cache
23 |
24 | ### NetBeans ###
25 | /nbproject/private/
26 | /nbbuild/
27 | /dist/
28 | /nbdist/
29 | /.nb-gradle/
30 | build/
31 | !**/src/main/**/build/
32 | !**/src/test/**/build/
33 |
34 | ### VS Code ###
35 | .vscode/
36 |
37 | ### Mac OS ###
38 | .DS_Store
--------------------------------------------------------------------------------
/meteor-common/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 |
8 |
9 | dev.pixelib.meteor
10 | meteor-parent
11 | ${revision}
12 | ../pom.xml
13 |
14 |
15 | meteor-common
16 |
17 |
18 |
19 |
20 |
21 |
22 | com.google.code.gson
23 | gson
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/benchmarks/src/main/java/dev/pixelib/meteor/benchmarks/MeteorBench.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.benchmarks;
2 |
3 | import org.openjdk.jmh.annotations.*;
4 | import org.openjdk.jmh.runner.Runner;
5 | import org.openjdk.jmh.runner.RunnerException;
6 | import org.openjdk.jmh.runner.options.Options;
7 | import org.openjdk.jmh.runner.options.OptionsBuilder;
8 |
9 | import java.util.concurrent.TimeUnit;
10 |
11 | @BenchmarkMode({Mode.All})
12 | @OutputTimeUnit(TimeUnit.SECONDS)
13 | @State(Scope.Thread)
14 | public class MeteorBench {
15 |
16 | public static void main(String[] args) throws RunnerException {
17 | Options options = new OptionsBuilder()
18 | .include(SimpleIncrement.class.getSimpleName())
19 | .include(ScoresWithMaps.class.getSimpleName())
20 | .forks(1)
21 | .build();
22 |
23 | new Runner(options).run();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/meteor-common/src/main/java/dev/pixelib/meteor/base/errors/MethodInvocationException.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.base.errors;
2 |
3 | public class MethodInvocationException extends RuntimeException {
4 |
5 | private final String methodName;
6 | private final String namespace;
7 | private final Throwable cause;
8 |
9 | public MethodInvocationException(String methodName, String namespace, Throwable cause) {
10 | super("Invocation of method " + methodName + " on target " + namespace + " failed.", cause);
11 | this.methodName = methodName;
12 | this.namespace = namespace;
13 | this.cause = cause;
14 | }
15 |
16 | public String getMethodName() {
17 | return methodName;
18 | }
19 |
20 | public String getNamespace() {
21 | return namespace;
22 | }
23 |
24 | @Override
25 | public Throwable getCause() {
26 | return cause;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/meteor-common/src/main/java/dev/pixelib/meteor/base/RpcSerializer.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.base;
2 |
3 | public interface RpcSerializer {
4 |
5 | /**
6 | * @param obj the object to serialize
7 | * @return the serialized object as a byte array, possibly untrimmed
8 | */
9 | byte[] serialize(Object obj);
10 |
11 | /**
12 | * @param bytes the bytes to deserialize (untrimmed)
13 | * @param clazz the class to deserialize to
14 | * @param the type of the class to deserialize to
15 | * @return the deserialized object
16 | */
17 | T deserialize(byte[] bytes, Class clazz);
18 |
19 | /**
20 | * @param str the string to deserialize (basic utf, not-localized)
21 | * @param clazz the class to deserialize to
22 | * @param the type of the class to deserialize to
23 | * @return the deserialized object
24 | */
25 | T deserialize(String str, Class clazz);
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish package to the Maven Central Repository
2 | on:
3 | release:
4 | types: [created,edited]
5 |
6 | jobs:
7 | publish:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v3
11 | - name: Set up Maven Central Repository
12 | uses: actions/setup-java@v3
13 | with:
14 | java-version: '17.0.8+7'
15 | distribution: 'temurin'
16 | server-id: ossrh
17 | server-username: MAVEN_USERNAME
18 | server-password: MAVEN_PASSWORD
19 | gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
20 | gpg-passphrase: MAVEN_GPG_PASSPHRASE
21 | - name: Publish package
22 | run: mvn -Drevision=${{ github.event.release.tag_name }} --batch-mode deploy -P release
23 | env:
24 | MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
25 | MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
26 | MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
--------------------------------------------------------------------------------
/.github/badges/jacoco.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/badges/branches.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/meteor-common/src/main/java/dev/pixelib/meteor/base/RpcOptions.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.base;
2 |
3 | public class RpcOptions {
4 |
5 | /**
6 | * The amount of time in seconds to wait for a response from the server.
7 | * An InvocationTimedOutException will be thrown if the timeout is exceeded.
8 | */
9 | private int timeoutSeconds = 30;
10 |
11 | /**
12 | * Generated pseudoclass for proxies will be registered with this class loader.
13 | */
14 | private ClassLoader classLoader = RpcOptions.class.getClassLoader();
15 |
16 | /**
17 | * The number of threads to use for executing methods on the server.
18 | */
19 | private int executorThreads = 1;
20 |
21 | public int getTimeoutSeconds() {
22 | return timeoutSeconds;
23 | }
24 |
25 | public void setTimeoutSeconds(int timeoutSeconds) {
26 | this.timeoutSeconds = timeoutSeconds;
27 | }
28 |
29 | public ClassLoader getClassLoader() {
30 | return classLoader;
31 | }
32 |
33 | public void setClassLoader(ClassLoader classLoader) {
34 | this.classLoader = classLoader;
35 | }
36 |
37 | public int getExecutorThreads() {
38 | return executorThreads;
39 | }
40 |
41 | public void setExecutorThreads(int executorThreads) {
42 | this.executorThreads = executorThreads;
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/meteor-common/src/main/java/dev/pixelib/meteor/base/RpcTransport.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.base;
2 |
3 | import dev.pixelib.meteor.base.enums.Direction;
4 | import dev.pixelib.meteor.base.interfaces.SubscriptionHandler;
5 |
6 | import java.io.Closeable;
7 |
8 | public interface RpcTransport extends Closeable {
9 |
10 | /**
11 | * @param direction the direction of the packet
12 | * @param bytes the bytes to send
13 | * bytes given should already been considered as a packet, and should not be further processed by the transport implementation
14 | */
15 | void send(Direction direction, byte[] bytes);
16 |
17 | /**
18 | * @param target the direction of the packet we want to listen to
19 | * @param onReceive a function that will be called when a packet is received.
20 | * the function should return a ReadStatus, which will be used to determine if the packet was handled or not.
21 | * if the packet was handled, the transport implementation should stop processing the packet.
22 | * if the packet was not handled, the transport implementation should continue processing the packet.
23 | * the transport implementation should call the onReceive function, regardless of the ReadStatus.
24 | */
25 | void subscribe(Direction target, SubscriptionHandler onReceive);
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/meteor-common/src/main/java/dev/pixelib/meteor/base/errors/InvocationTimedOutException.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.base.errors;
2 |
3 | public class InvocationTimedOutException extends RuntimeException {
4 |
5 | /**
6 | * Name of the method that timed out.
7 | */
8 | private final String methodName;
9 |
10 |
11 | /**
12 | * Name of the target that timed out.
13 | */
14 | private final String namespace;
15 |
16 | /**
17 | * Amount of time in seconds that the invocation was allowed to take.
18 | */
19 | private final int timeoutSeconds;
20 |
21 | public InvocationTimedOutException(String methodName, String namespace, int timeoutSeconds) {
22 | super("Invocation of method " + methodName + " on target " + namespace + " timed out after " + timeoutSeconds + " seconds.");
23 | this.methodName = methodName;
24 | this.namespace = namespace;
25 | this.timeoutSeconds = timeoutSeconds;
26 | }
27 |
28 | /**
29 | * @return Name of the method that timed out.
30 | */
31 | public String getMethodName() {
32 | return methodName;
33 | }
34 |
35 | /**
36 | * @return Name of the target that timed out.
37 | */
38 | public String getNamespace() {
39 | return namespace;
40 | }
41 |
42 | /**
43 | * @return Amount of time in seconds that the invocation was allowed to take.
44 | */
45 | public int getTimeoutSeconds() {
46 | return timeoutSeconds;
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/meteor-core/src/main/java/dev/pixelib/meteor/core/proxy/ProxyInvocHandler.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.proxy;
2 |
3 | import dev.pixelib.meteor.core.trackers.OutgoingInvocationTracker;
4 | import dev.pixelib.meteor.core.transport.packets.InvocationDescriptor;
5 |
6 | import java.lang.reflect.InvocationHandler;
7 | import java.lang.reflect.Method;
8 |
9 | public class ProxyInvocHandler implements InvocationHandler {
10 |
11 | private final OutgoingInvocationTracker localInvocationTracker;
12 | private final String namespace;
13 |
14 | public ProxyInvocHandler(OutgoingInvocationTracker outgoingInvocationTracker, String namespace) {
15 | this.localInvocationTracker = outgoingInvocationTracker;
16 | this.namespace = namespace;
17 | }
18 |
19 | @Override
20 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
21 | // is args null? if so, create an empty array
22 | if (args == null) {
23 | args = new Object[0];
24 | }
25 |
26 | // build invocation descriptor
27 | Class>[] argTypes = new Class>[args.length];
28 | for (int i = 0; i < args.length; i++)
29 | argTypes[i] = args[i].getClass();
30 |
31 | InvocationDescriptor invocationDescriptor = new InvocationDescriptor(namespace, method.getDeclaringClass(), method.getName(), args, argTypes, method.getReturnType());
32 |
33 | // wait for response or timeout
34 | return localInvocationTracker.invokeRemoteMethod(invocationDescriptor);
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/meteor-core/src/main/java/dev/pixelib/meteor/core/trackers/IncomingInvocationTracker.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.trackers;
2 |
3 | import dev.pixelib.meteor.core.executor.ImplementationWrapper;
4 |
5 | import java.util.Collection;
6 | import java.util.concurrent.ConcurrentHashMap;
7 |
8 | public class IncomingInvocationTracker {
9 |
10 | // Map invocation handlers by type; used for dispatching incoming invocations
11 | // Handlers without a namespace (which is nullable) are also stored here
12 | private final ConcurrentHashMap, Collection> implementations = new ConcurrentHashMap<>();
13 |
14 | public void registerImplementation(Object implementation, String namespace) {
15 | // get the interfaces implemented by the implementation
16 | Class>[] interfaces = implementation.getClass().getInterfaces();
17 |
18 | // there must be at least one interface
19 | if (interfaces.length == 0) {
20 | throw new IllegalArgumentException("Implementation must implement at least one interface/procedure");
21 | }
22 |
23 | // register this interface as all the implemented interfaces
24 | ImplementationWrapper implementationWrapper = new ImplementationWrapper(implementation, namespace);
25 |
26 | for (Class> anInterface : interfaces) {
27 | implementations.computeIfAbsent(anInterface, k -> ConcurrentHashMap.newKeySet()).add(implementationWrapper);
28 | }
29 | }
30 |
31 | public ConcurrentHashMap, Collection> getImplementations() {
32 | return implementations;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | dev.pixelib.meteor
8 | meteor-parent
9 | ${revision}
10 |
11 |
12 | examples
13 |
14 |
15 | 17
16 | 17
17 | UTF-8
18 |
19 |
20 |
21 |
22 | dev.pixelib.meteor
23 | meteor-core
24 | ${revision}
25 |
26 |
27 | dev.pixelib.meteor.transport
28 | meteor-jedis
29 | ${revision}
30 |
31 |
32 |
33 |
34 |
35 |
36 | org.apache.maven.plugins
37 | maven-deploy-plugin
38 |
39 | true
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/meteor-common/src/main/java/dev/pixelib/meteor/base/defaults/GsonSerializer.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.base.defaults;
2 |
3 | import com.google.gson.Gson;
4 | import dev.pixelib.meteor.base.RpcSerializer;
5 |
6 | public class GsonSerializer implements RpcSerializer {
7 |
8 | /**
9 | * the Gson instance to use for serialization/deserialization
10 | * this is a static field so that it is shared between all instances of this class and can be swapped out at runtime
11 | */
12 | public static Gson GSON = new Gson();
13 |
14 | /**
15 | * @param obj the object to serialize
16 | * @return the serialized object as a byte array
17 | *
18 | * this particular implementation uses Gson to serialize the object
19 | */
20 | @Override
21 | public byte[] serialize(Object obj) {
22 | return GSON.toJson(obj).getBytes();
23 | }
24 |
25 | /**
26 | * @param bytes the bytes to deserialize
27 | * @param clazz the class to deserialize to
28 | * @param the type of the class to deserialize to
29 | * @return the deserialized object
30 | *
31 | * this particular implementation uses Gson to deserialize the object
32 | */
33 | @Override
34 | public T deserialize(byte[] bytes, Class clazz) {
35 | return GSON.fromJson(new String(bytes), clazz);
36 | }
37 |
38 | /**
39 | * @param str the string to deserialize
40 | * @param clazz the class to deserialize to
41 | * @param the type of the class to deserialize to
42 | * @return the deserialized object
43 | */
44 | @Override
45 | public T deserialize(String str, Class clazz) {
46 | return GSON.fromJson(str, clazz);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/meteor-core/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 |
8 | dev.pixelib.meteor
9 | meteor-parent
10 | ${revision}
11 | ../pom.xml
12 |
13 |
14 | meteor-core
15 |
16 |
17 |
18 | io.netty
19 | netty-buffer
20 |
21 |
22 |
23 | dev.pixelib.meteor
24 | meteor-common
25 | ${revision}
26 | compile
27 |
28 |
29 |
30 | org.junit.jupiter
31 | junit-jupiter
32 | test
33 |
34 |
35 |
36 | org.mockito
37 | mockito-junit-jupiter
38 | test
39 |
40 |
41 |
42 |
43 |
44 |
45 | org.apache.maven.plugins
46 | maven-surefire-plugin
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Java CI with Maven
10 |
11 | on:
12 | push:
13 | branches: [ "main" ]
14 | pull_request:
15 | branches: [ "main" ]
16 |
17 | jobs:
18 | build:
19 |
20 | runs-on: ubuntu-latest
21 |
22 | steps:
23 | - uses: actions/checkout@v3
24 | with:
25 | ref: ${{ github.event.pull_request.head.ref }}
26 | - name: Set up JDK 20
27 | uses: actions/setup-java@v3
28 | with:
29 | java-version: '17.0.8+7'
30 | distribution: 'temurin'
31 | cache: maven
32 |
33 | # run tests with junit
34 | - name: Test with JUnit
35 | run: mvn -B test --file pom.xml
36 |
37 | - name: Generate JaCoCo Badge
38 | id: jacoco
39 | uses: cicirello/jacoco-badge-generator@v2
40 | with:
41 | generate-branches-badge: true
42 | jacoco-csv-file: >
43 | meteor-jedis/target/site/jacoco/jacoco.csv
44 | meteor-core/target/site/jacoco/jacoco.csv
45 |
46 | - name: Log coverage percentage
47 | run: |
48 | echo "coverage = ${{ steps.jacoco.outputs.coverage }}"
49 | echo "branch coverage = ${{ steps.jacoco.outputs.branches }}"
50 |
51 | - uses: EndBug/add-and-commit@v9 # You can change this to use a specific version.
52 | with:
53 | default_author: github_actions
54 | message: "Add JaCoCo badge"
--------------------------------------------------------------------------------
/benchmarks/src/main/java/dev/pixelib/meteor/benchmarks/SimpleIncrement.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.benchmarks;
2 |
3 | import dev.pixelib.meteor.base.RpcOptions;
4 | import dev.pixelib.meteor.base.defaults.LoopbackTransport;
5 | import dev.pixelib.meteor.core.Meteor;
6 | import org.openjdk.jmh.annotations.*;
7 |
8 | import java.io.IOException;
9 | import java.util.concurrent.TimeUnit;
10 |
11 | @BenchmarkMode({Mode.All})
12 | @OutputTimeUnit(TimeUnit.SECONDS)
13 | @State(Scope.Thread)
14 | public class SimpleIncrement {
15 |
16 | private Meteor meteor;
17 | private Scoreboard scoreboardStub;
18 |
19 | @Param({"1", "10", "50"}) // Example parameter values
20 | private int workerThreads;
21 |
22 | @Setup
23 | public void setup() {
24 | System.out.println("workerThreads: " + workerThreads);
25 | RpcOptions rpcOptions = new RpcOptions();
26 | rpcOptions.setExecutorThreads(workerThreads);
27 | meteor = new Meteor(new LoopbackTransport(), rpcOptions);
28 | meteor.registerImplementation(new ScoreboardImplementation(), "parkour-leaderboard");
29 | scoreboardStub = meteor.registerProcedure(Scoreboard.class, "parkour-leaderboard");
30 | }
31 |
32 | @TearDown
33 | public void tearDown() throws IOException {
34 | meteor.stop();
35 | }
36 |
37 | @Benchmark
38 | public void benchmarkMethod() {
39 | scoreboardStub.incrementScore();
40 | }
41 |
42 | public interface Scoreboard {
43 | int incrementScore();
44 | int getScore();
45 | }
46 |
47 | public static class ScoreboardImplementation implements Scoreboard {
48 | private Integer score = 0;
49 |
50 | @Override
51 | public int incrementScore() {
52 | return ++score;
53 | }
54 |
55 | @Override
56 | public int getScore() {
57 | return score;
58 | }
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/examples/src/main/java/dev/pixelib/meteor/sender/ScoreboardExample.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.sender;
2 |
3 | import dev.pixelib.meteor.base.defaults.LoopbackTransport;
4 | import dev.pixelib.meteor.core.Meteor;
5 |
6 | import java.util.HashMap;
7 | import java.util.Map;
8 |
9 | public class ScoreboardExample {
10 |
11 | public static void main(String[] args) throws Exception{
12 | Meteor meteor = new Meteor(new LoopbackTransport());
13 | meteor.registerImplementation(new ScoreboardImplementation(), "parkour-leaderboard");
14 |
15 | Scoreboard scoreboard = meteor.registerProcedure(Scoreboard.class, "parkour-leaderboard");
16 |
17 | scoreboard.setScoreForPlayer("player1", 10);
18 | scoreboard.setScoreForPlayer("player2", 20);
19 | scoreboard.setScoreForPlayer("player3", 30);
20 |
21 | Map scores = scoreboard.getAllScores();
22 | System.out.println("scores: " + scores);
23 |
24 | int player1Score = scoreboard.getScoreForPlayer("player1");
25 | System.out.println("player1 score: " + player1Score);
26 |
27 | meteor.stop();
28 | }
29 |
30 | public interface Scoreboard {
31 | int getScoreForPlayer(String player);
32 | void setScoreForPlayer(String player, int score);
33 | Map getAllScores();
34 | }
35 |
36 | public static class ScoreboardImplementation implements Scoreboard {
37 |
38 | private final Map scores = new HashMap<>();
39 |
40 | @Override
41 | public int getScoreForPlayer(String player) {
42 | return scores.getOrDefault(player, 0);
43 | }
44 |
45 | @Override
46 | public void setScoreForPlayer(String player, int score) {
47 | scores.put(player, score);
48 | }
49 |
50 | @Override
51 | public Map getAllScores() {
52 | return scores;
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/meteor-jedis/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | dev.pixelib.meteor
8 | meteor-parent
9 | ${revision}
10 | ../pom.xml
11 |
12 |
13 | dev.pixelib.meteor.transport
14 | meteor-jedis
15 |
16 |
17 | 17
18 | 17
19 | UTF-8
20 |
21 |
22 |
23 |
24 | dev.pixelib.meteor
25 | meteor-common
26 | ${revision}
27 |
28 |
29 |
30 | redis.clients
31 | jedis
32 |
33 |
34 |
35 | org.junit.jupiter
36 | junit-jupiter
37 | test
38 |
39 |
40 |
41 | com.github.fppt
42 | jedis-mock
43 | 1.0.10
44 | test
45 |
46 |
47 |
48 | org.mockito
49 | mockito-junit-jupiter
50 | test
51 |
52 |
53 |
--------------------------------------------------------------------------------
/benchmarks/src/main/java/dev/pixelib/meteor/benchmarks/ScoresWithMaps.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.benchmarks;
2 |
3 | import dev.pixelib.meteor.base.RpcOptions;
4 | import dev.pixelib.meteor.base.defaults.LoopbackTransport;
5 | import dev.pixelib.meteor.core.Meteor;
6 | import org.openjdk.jmh.annotations.*;
7 |
8 | import java.io.IOException;
9 | import java.util.HashMap;
10 | import java.util.concurrent.TimeUnit;
11 |
12 | @BenchmarkMode({Mode.All})
13 | @OutputTimeUnit(TimeUnit.SECONDS)
14 | @State(Scope.Thread)
15 | public class ScoresWithMaps {
16 |
17 | private Meteor meteor;
18 | private PersonalScores scoreboardStub;
19 |
20 | @Param({"1", "10", "50"}) // Example parameter values
21 | private int workerThreads;
22 |
23 | @Setup
24 | public void setup() {
25 | System.out.println("workerThreads: " + workerThreads);
26 | RpcOptions rpcOptions = new RpcOptions();
27 | rpcOptions.setExecutorThreads(workerThreads);
28 | meteor = new Meteor(new LoopbackTransport(), rpcOptions);
29 | meteor.registerImplementation(new ScoreboardImplementation(), "parkour-leaderboard");
30 | scoreboardStub = meteor.registerProcedure(PersonalScores.class, "parkour-leaderboard");
31 | }
32 |
33 | @TearDown
34 | public void tearDown() throws IOException {
35 | meteor.stop();
36 | }
37 |
38 | @Benchmark
39 | public void benchmarkMethod() {
40 | scoreboardStub.incrementFor("player1");
41 | }
42 |
43 | public interface PersonalScores {
44 | HashMap getScores();
45 | HashMap incrementFor(String name);
46 | }
47 |
48 | public static class ScoreboardImplementation implements PersonalScores {
49 | private HashMap scores = new HashMap<>();
50 |
51 | @Override
52 | public HashMap getScores() {
53 | return scores;
54 | }
55 |
56 | @Override
57 | public HashMap incrementFor(String name) {
58 | if (scores.containsKey(name)) {
59 | scores.put(name, scores.get(name) + 1);
60 | } else {
61 | scores.put(name, 1);
62 | }
63 | return scores;
64 | }
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/examples/src/main/java/dev/pixelib/meteor/sender/SendingUpdateMethod.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.sender;
2 |
3 | import dev.pixelib.meteor.base.defaults.LoopbackTransport;
4 | import dev.pixelib.meteor.core.Meteor;
5 |
6 | public class SendingUpdateMethod {
7 |
8 | public static void main(String[] args) throws Exception{
9 | Meteor meteor = new Meteor(new LoopbackTransport());
10 |
11 | MathAdd mathAdd = meteor.registerProcedure(MathAdd.class);
12 | MathSubstract mathSubstract = meteor.registerProcedure(MathSubstract.class);
13 | MathMultiply mathMultiply = meteor.registerProcedure(MathMultiply.class);
14 |
15 | // register an implementation, invocations will be dispatched to this object.
16 | // implementations will be registered under all interfaces they implement
17 | meteor.registerImplementation(new MathFunctionsImpl());
18 |
19 | int subResult = mathSubstract.substract(10, 1, 2, 3, 4, 5);
20 | System.out.println("10 - 1 - 2 - 3 - 4 - 5 = " + subResult);
21 |
22 | int addResult = mathAdd.add(1, 2, 3, 4, 5);
23 | System.out.println("1 + 2 + 3 + 4 + 5 = " + addResult);
24 |
25 | int multiResult = mathMultiply.multiply(5, 5);
26 | System.out.println("5 * 5 = " + multiResult);
27 |
28 | meteor.stop();
29 | }
30 |
31 | public interface MathAdd {
32 | int add(int... numbers);
33 | }
34 |
35 | public interface MathSubstract {
36 | int substract(int from, int... numbers);
37 | }
38 |
39 | public interface MathMultiply {
40 | int multiply(int x, int times);
41 | }
42 |
43 | public static class MathFunctionsImpl implements MathAdd, MathSubstract, MathMultiply {
44 |
45 | @Override
46 | public int multiply(int x, int times) {
47 | return x * times;
48 | }
49 |
50 | @Override
51 | public int add(int... numbers) {
52 | int result = 0;
53 | for (int number : numbers) {
54 | result += number;
55 | }
56 | return result;
57 | }
58 |
59 | @Override
60 | public int substract(int from, int... numbers) {
61 | int result = from;
62 | for (int number : numbers) {
63 | result -= number;
64 | }
65 | return result;
66 | }
67 |
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/PERFORMANCE.md:
--------------------------------------------------------------------------------
1 | # Performance Analysis
2 | ## 1. Introduction
3 | The goal of Meteor is to make your networked application easier, not a pain. Meteor is designed to be as easy as possible to use, but also to be performant.
4 | This document will go over the performance overhead incurred from Meteor remote invocations, how to optimize your application and other possible hurdles to consider.
5 |
6 | ## 2. Testing Environment
7 | The benchmarks presented here have been run on a `Ryzen 7 3700x 32gb ddr4 3200mhz`, and the results are the average of 5 runs (with 5 warmup runs per test). The benchmarks themselves are based on [OpenJDK's JMH](https://github.com/openjdk/jmh) and you can run them yourself through within the `benchmarks` folder.
8 |
9 | All tests have been run with a local loopback configuration. This means that all 'packets' have been locally evaluated and no network overhead has been incurred, however, serialization and deserialization overhead is still present.
10 |
11 | Invocation time is therefore not fully representative of a real-world scenario, but it is still a good indicator of the overhead incurred by Meteor.
12 | If you want a more realistic benchmark, you can run the tests with a remote configuration (like redis), or imagine adding your network RTT (round-trip time) to each invocation time.
13 |
14 | All benchmarks have been run with 1, 10 and 50 worker threads. This means that the meteor receiver was able to process 1, 10 and 50 invocations concurrently.
15 | Keep an eye on the results for the 1 thread case, as it is more representative of implementations where locking is used. The 10 and 50 thread cases are more representative of implementations where resources are not shared between threads.
16 |
17 | ## 3. Benchmarks
18 | ### 3.1 Map Based Datatypes
19 | This benchmark simulates a simple key-value store, where the initial invocation contains a String and an Integer, and the entire map is returned.
20 | The map is then updated with a new key-value pair, and simulates a more complicated datatype being returned.
21 | 
22 |
23 | ### 3.2 Small Invocation
24 | This benchmark simulates a remote integer being incremented and returned. It does not take any arguments, and therefore represents a scenario benchmark of Meteor logic without adapting any types or processing method overloads.
25 | 
--------------------------------------------------------------------------------
/meteor-jedis/src/main/java/dev/pixelib/meteor/transport/redis/RedisPacketListener.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.transport.redis;
2 |
3 | import dev.pixelib.meteor.base.interfaces.SubscriptionHandler;
4 | import redis.clients.jedis.JedisPubSub;
5 |
6 | import java.util.Base64;
7 | import java.util.Collection;
8 | import java.util.Map;
9 | import java.util.Set;
10 | import java.util.concurrent.ConcurrentHashMap;
11 | import java.util.concurrent.ExecutorService;
12 | import java.util.concurrent.Executors;
13 | import java.util.function.Consumer;
14 | import java.util.logging.Level;
15 | import java.util.logging.Logger;
16 |
17 | public class RedisPacketListener extends JedisPubSub {
18 |
19 | private final Logger logger;
20 |
21 | private final ExecutorService jedisThreadPool = Executors.newCachedThreadPool();
22 | private final Map> messageBrokers = new ConcurrentHashMap<>();
23 | private final Collection customSubscribedChannels = ConcurrentHashMap.newKeySet();
24 |
25 | public RedisPacketListener(StringMessageBroker messageBroker, String startChannel, Logger logger) {
26 | this.logger = logger;
27 | registerBroker(startChannel, messageBroker);
28 | customSubscribedChannels.add(startChannel);
29 | }
30 |
31 | @Override
32 | public void onMessage(String channel, String message) {
33 | messageBrokers.get(channel).forEach(subscriptionHandler -> {
34 | try {
35 | subscriptionHandler.onRedisMessage(message);
36 | } catch (Exception exception) {
37 | logger.log(Level.SEVERE, "Error while handling packet", exception);
38 | }
39 | });
40 | }
41 |
42 | public void subscribe(String channel, StringMessageBroker onReceive) {
43 | registerBroker(channel, onReceive);
44 |
45 | if (customSubscribedChannels.add(channel)) {
46 | super.subscribe(channel);
47 | }
48 | }
49 |
50 | public void stop() {
51 | unsubscribe();
52 | jedisThreadPool.shutdownNow();
53 | }
54 |
55 | public Collection getCustomSubscribedChannels() {
56 | return customSubscribedChannels;
57 | }
58 |
59 | private void registerBroker(String channel, StringMessageBroker onReceive) {
60 | messageBrokers.computeIfAbsent(channel, key -> ConcurrentHashMap.newKeySet()).add(onReceive);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/examples/src/main/java/dev/pixelib/meteor/sender/SendingUpdateMethodRedis.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.sender;
2 |
3 | import dev.pixelib.meteor.core.Meteor;
4 | import dev.pixelib.meteor.transport.redis.RedisTransport;
5 |
6 | public class SendingUpdateMethodRedis {
7 |
8 | public static void main(String[] args) throws Exception{
9 | Meteor meteor = new Meteor(new RedisTransport("192.168.178.46", 6379, "test"));
10 |
11 | MathAdd mathAdd = meteor.registerProcedure(MathAdd.class);
12 | MathSubstract mathSubstract = meteor.registerProcedure(MathSubstract.class);
13 | MathMultiply mathMultiply = meteor.registerProcedure(MathMultiply.class);
14 |
15 | // register an implementation, invocations will be dispatched to this object.
16 | // implementations will be registered under all interfaces they implement
17 | meteor.registerImplementation(new MathFunctionsImpl());
18 |
19 |
20 | int subResult = mathSubstract.substract(10, 1, 2, 3, 4, 5);
21 | System.out.println("10 - 1 - 2 - 3 - 4 - 5 = " + subResult);
22 |
23 | int addResult = mathAdd.add(1, 2, 3, 4, 5);
24 | System.out.println("1 + 2 + 3 + 4 + 5 = " + addResult);
25 |
26 | int multiResult = mathMultiply.multiply(5, 5);
27 | System.out.println("5 * 5 = " + multiResult);
28 |
29 | meteor.stop();
30 | }
31 |
32 | public interface MathAdd {
33 | int add(int... numbers);
34 | }
35 |
36 | public interface MathSubstract {
37 | int substract(int from, int... numbers);
38 | }
39 |
40 | public interface MathMultiply {
41 | int multiply(int x, int times);
42 | }
43 |
44 | public static class MathFunctionsImpl implements MathAdd, MathSubstract, MathMultiply {
45 |
46 | @Override
47 | public int multiply(int x, int times) {
48 | return x * times;
49 | }
50 |
51 | @Override
52 | public int add(int... numbers) {
53 | int result = 0;
54 | for (int number : numbers) {
55 | result += number;
56 | }
57 | return result;
58 | }
59 |
60 | @Override
61 | public int substract(int from, int... numbers) {
62 | int result = from;
63 | for (int number : numbers) {
64 | result -= number;
65 | }
66 | return result;
67 | }
68 |
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/meteor-core/src/test/java/dev/pixelib/meteor/core/MeteorLoopbackTest.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core;
2 |
3 | import dev.pixelib.meteor.base.defaults.LoopbackTransport;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import java.util.HashSet;
7 | import java.util.Set;
8 |
9 | import static org.junit.jupiter.api.Assertions.assertEquals;
10 | import static org.junit.jupiter.api.Assertions.assertTrue;
11 |
12 | public class MeteorLoopbackTest {
13 |
14 | @Test
15 | public void testLoopbackFunctionality() {
16 | Meteor meteor = new Meteor(new LoopbackTransport());
17 |
18 | // register a procedure
19 | ImplementationTracker proxy = meteor.registerProcedure(ImplementationTracker.class);
20 |
21 | // register the real implementation
22 | TrackerImpl impl = new TrackerImpl();
23 | meteor.registerImplementation(impl);
24 |
25 | // call the procedure
26 | proxy.addString("Hello");
27 | proxy.addString("World");
28 |
29 | Set strings = proxy.allStrings();
30 | boolean containsHello = proxy.containsString("Hello");
31 | boolean containsWorld = proxy.containsString("World");
32 | int invocationCount = proxy.getInvocationCount();
33 |
34 | assertEquals(2, strings.size());
35 | assertTrue(containsHello);
36 | assertTrue(containsWorld);
37 | assertEquals(5, invocationCount);
38 |
39 | }
40 |
41 | public interface ImplementationTracker {
42 | int getInvocationCount();
43 | Set allStrings();
44 | void addString(String string);
45 | boolean containsString(String string);
46 | }
47 |
48 | public class TrackerImpl implements ImplementationTracker {
49 | private int invocationCount = 0;
50 | private Set strings = new HashSet<>();
51 |
52 | @Override
53 | public int getInvocationCount() {
54 | return invocationCount;
55 | }
56 |
57 | @Override
58 | public Set allStrings() {
59 | invocationCount++;
60 | return strings;
61 | }
62 |
63 | @Override
64 | public void addString(String string) {
65 | invocationCount++;
66 | strings.add(string);
67 | }
68 |
69 | @Override
70 | public boolean containsString(String string) {
71 | invocationCount++;
72 | return strings.contains(string);
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/meteor-core/src/test/java/dev/pixelib/meteor/core/transport/packets/InvocationDescriptorTest.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.transport.packets;
2 |
3 | import dev.pixelib.meteor.base.RpcSerializer;
4 | import dev.pixelib.meteor.base.defaults.GsonSerializer;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import static org.junit.jupiter.api.Assertions.assertEquals;
8 |
9 | class InvocationDescriptorTest {
10 |
11 | private void compareInstances(InvocationDescriptor a, InvocationDescriptor b) {
12 | assertEquals(a.getNamespace(), b.getNamespace());
13 |
14 | assertEquals(a.getArgs().length, b.getArgs().length);
15 | for (int i = 0; i < a.getArgs().length; i++) {
16 | assertEquals(a.getArgs()[i], b.getArgs()[i]);
17 | }
18 |
19 | assertEquals(a.getArgTypes().length, b.getArgTypes().length);
20 | for (int i = 0; i < a.getArgTypes().length; i++) {
21 | assertEquals(a.getArgTypes()[i], b.getArgTypes()[i]);
22 | }
23 |
24 | assertEquals(a.getReturnType(), b.getReturnType());
25 | }
26 |
27 | @Test
28 | public void testSerializationWithNamespace() throws ClassNotFoundException {
29 | RpcSerializer defaultSerializer = new GsonSerializer();
30 |
31 | InvocationDescriptor original = new InvocationDescriptor(
32 | "namespace",
33 | InvocationDescriptorTest.class,
34 | "methodName",
35 | new Object[]{1, 2, 3},
36 | new Class>[]{int.class, int.class, int.class},
37 | int.class
38 | );
39 |
40 | byte[] serialized = original.toBuffer(defaultSerializer);
41 |
42 | InvocationDescriptor deserialized = InvocationDescriptor.fromBuffer(defaultSerializer, serialized);
43 | compareInstances(original, deserialized);
44 | }
45 |
46 | @Test
47 | public void testSerializationWithoutNamespace() throws ClassNotFoundException {
48 | RpcSerializer defaultSerializer = new GsonSerializer();
49 |
50 | InvocationDescriptor original = new InvocationDescriptor(
51 | null,
52 | InvocationDescriptorTest.class,
53 | "methodName",
54 | new Object[]{1, 2, 3},
55 | new Class>[]{int.class, int.class, int.class},
56 | int.class
57 | );
58 |
59 | byte[] serialized = original.toBuffer(defaultSerializer);
60 |
61 | InvocationDescriptor deserialized = InvocationDescriptor.fromBuffer(defaultSerializer, serialized);
62 | compareInstances(original, deserialized);
63 | }
64 |
65 | }
--------------------------------------------------------------------------------
/meteor-common/src/main/java/dev/pixelib/meteor/base/defaults/LoopbackTransport.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.base.defaults;
2 |
3 | import dev.pixelib.meteor.base.RpcTransport;
4 | import dev.pixelib.meteor.base.enums.Direction;
5 | import dev.pixelib.meteor.base.interfaces.SubscriptionHandler;
6 |
7 | import java.io.IOException;
8 | import java.util.ArrayList;
9 | import java.util.EnumMap;
10 | import java.util.List;
11 | import java.util.Map;
12 |
13 | public class LoopbackTransport implements RpcTransport {
14 |
15 | private final Map> onReceiveFunctions = new EnumMap<>(Direction.class);
16 |
17 | /**
18 | * @param bytes the bytes to send
19 | * bytes given should already been considered as a packet, and should1not be further processed by the transport implementation
20 | * this particular implementation will call all the onReceive functions, and stop if one of them returns HANDLED
21 | * no actual sending is done, as this is a loopback transport meant for testing
22 | */
23 | @Override
24 | public void send(Direction direction, byte[] bytes) {
25 | for (SubscriptionHandler onReceiveFunction : onReceiveFunctions.getOrDefault(direction, new ArrayList<>())) {
26 | try {
27 | boolean matched = onReceiveFunction.onPacket(bytes);
28 | if (matched) break;
29 | } catch (Exception e) {
30 | // TODO: Add Logger
31 | e.printStackTrace();
32 | }
33 | }
34 | }
35 |
36 | /**
37 | * @param target the direction of the packet we want to listen to
38 | * @param onReceive a function that will be called when a packet is received.
39 | * the function should return a ReadStatus, which will be used to determine if the packet was handled or not.
40 | * if the packet was handled, the transport implementation should stop processing the packet.
41 | * if the packet was not handled, the transport implementation should continue processing the packet.
42 | * the transport implementation should call the onReceive function, regardless of the ReadStatus.
43 | */
44 | @Override
45 | public void subscribe(Direction target, SubscriptionHandler onReceive) {
46 | onReceiveFunctions.computeIfAbsent(target, k -> new ArrayList<>()).add(onReceive);
47 | }
48 |
49 | /**
50 | * @throws IOException never thrown, as this is a loopback transport meant for testing so this method is unused
51 | */
52 | @Override
53 | public void close() throws IOException {
54 | // do nothing
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/meteor-core/src/test/java/dev/pixelib/meteor/core/trackers/IncomingInvocationTrackerTest.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.trackers;
2 |
3 | import dev.pixelib.meteor.core.executor.ImplementationWrapper;
4 | import dev.pixelib.meteor.core.utils.MathFunctions;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import static org.junit.jupiter.api.Assertions.*;
8 |
9 | class IncomingInvocationTrackerTest {
10 |
11 | @Test
12 | void testRegisterImplementation_thenSuccess() {
13 | IncomingInvocationTracker incomingInvocationTracker = new IncomingInvocationTracker();
14 | TestMathFunctions testMathFunctions = new TestMathFunctions();
15 |
16 | incomingInvocationTracker.registerImplementation(testMathFunctions, "test");
17 |
18 | assertEquals(incomingInvocationTracker.getImplementations().size(), 1);
19 | assertEquals(incomingInvocationTracker.getImplementations().get(MathFunctions.class).size(), 1);
20 |
21 | boolean matched = false;
22 | for (ImplementationWrapper implementationWrapper : incomingInvocationTracker.getImplementations().get(MathFunctions.class)) {
23 | if (implementationWrapper.getImplementation() == testMathFunctions) {
24 | // inverted, because the namespace is nullable
25 | if ("test".equals(implementationWrapper.getNamespace())) {
26 | if (matched) {
27 | fail("Implementation registered twice");
28 | return;
29 | }
30 | matched = true;
31 | }
32 | }
33 | }
34 |
35 | assertTrue(matched, "Implementation not registered");
36 | }
37 |
38 | @Test
39 | void testRegisterImplementationArgumentNoInterface_thenFail() {
40 | IncomingInvocationTracker incomingInvocationTracker = new IncomingInvocationTracker();
41 |
42 | assertThrowsExactly(IllegalArgumentException.class, () -> incomingInvocationTracker.registerImplementation(new Object(), "test"), "Implementation implemented an interface/procedure");
43 | }
44 |
45 | public static class TestMathFunctions implements MathFunctions {
46 |
47 | @Override
48 | public int multiply(int x, int times) {
49 | return x * times;
50 | }
51 |
52 | @Override
53 | public int add(int... numbers) {
54 | int result = 0;
55 | for (int number : numbers) {
56 | result += number;
57 | }
58 | return result;
59 | }
60 |
61 | @Override
62 | public int substract(int from, int... numbers) {
63 | int result = from;
64 | for (int number : numbers) {
65 | result -= number;
66 | }
67 | return result;
68 | }
69 |
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/meteor-core/src/test/java/dev/pixelib/meteor/core/transport/packets/InvocationResponseTest.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.transport.packets;
2 |
3 | import dev.pixelib.meteor.base.RpcSerializer;
4 | import dev.pixelib.meteor.base.defaults.GsonSerializer;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.util.UUID;
8 |
9 | import static org.junit.jupiter.api.Assertions.assertArrayEquals;
10 | import static org.junit.jupiter.api.Assertions.assertEquals;
11 |
12 | class InvocationResponseTest {
13 |
14 | private RpcSerializer serializer = new GsonSerializer();
15 |
16 | @Test
17 | void testInvocationResponsePrimitive() throws ClassNotFoundException {
18 | InvocationResponse invocationResponse = new InvocationResponse(UUID.randomUUID(), 1);
19 | byte[] bytes = invocationResponse.toBytes(serializer);
20 | InvocationResponse invocationResponse1 = InvocationResponse.fromBytes(serializer, bytes);
21 | assertEquals(invocationResponse.getInvocationId(), invocationResponse1.getInvocationId());
22 | assertEquals(invocationResponse.getResult(), invocationResponse1.getResult());
23 | }
24 |
25 | @Test
26 | void testInvocationResponseObject() throws ClassNotFoundException {
27 | InvocationResponse invocationResponse = new InvocationResponse(UUID.randomUUID(), "test");
28 | byte[] bytes = invocationResponse.toBytes(serializer);
29 | InvocationResponse invocationResponse1 = InvocationResponse.fromBytes(serializer, bytes);
30 | assertEquals(invocationResponse.getInvocationId(), invocationResponse1.getInvocationId());
31 | assertEquals(invocationResponse.getResult(), invocationResponse1.getResult());
32 | }
33 |
34 | @Test
35 | void testInvocationResponseNull() throws ClassNotFoundException {
36 | InvocationResponse invocationResponse = new InvocationResponse(UUID.randomUUID(), null);
37 | byte[] bytes = invocationResponse.toBytes(serializer);
38 | InvocationResponse invocationResponse1 = InvocationResponse.fromBytes(serializer, bytes);
39 | assertEquals(invocationResponse.getInvocationId(), invocationResponse1.getInvocationId());
40 | assertEquals(invocationResponse.getResult(), invocationResponse1.getResult());
41 | }
42 |
43 | @Test
44 | void testInvocationResponsePrimitiveArray() throws ClassNotFoundException {
45 | InvocationResponse invocationResponse = new InvocationResponse(UUID.randomUUID(), new int[]{1, 2, 3});
46 | byte[] bytes = invocationResponse.toBytes(serializer);
47 | InvocationResponse invocationResponse1 = InvocationResponse.fromBytes(serializer, bytes);
48 | assertEquals(invocationResponse.getInvocationId(), invocationResponse1.getInvocationId());
49 | assertArrayEquals((int[]) invocationResponse.getResult(), (int[]) invocationResponse1.getResult());
50 | }
51 | }
--------------------------------------------------------------------------------
/meteor-core/src/main/java/dev/pixelib/meteor/core/trackers/OutgoingInvocationTracker.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.trackers;
2 |
3 | import dev.pixelib.meteor.base.RpcOptions;
4 | import dev.pixelib.meteor.base.RpcSerializer;
5 | import dev.pixelib.meteor.base.RpcTransport;
6 | import dev.pixelib.meteor.base.enums.Direction;
7 | import dev.pixelib.meteor.core.proxy.PendingInvocation;
8 | import dev.pixelib.meteor.core.transport.packets.InvocationDescriptor;
9 | import dev.pixelib.meteor.core.transport.packets.InvocationResponse;
10 |
11 | import java.util.Timer;
12 | import java.util.UUID;
13 | import java.util.concurrent.CompletionException;
14 | import java.util.concurrent.ConcurrentHashMap;
15 |
16 | public class OutgoingInvocationTracker {
17 |
18 | private final Timer timer;
19 | private final RpcOptions options;
20 | private RpcTransport transport;
21 | private RpcSerializer serializer;
22 |
23 | // Map of pending invocations, keyed by invocation id
24 | private final ConcurrentHashMap> pendingInvocations = new ConcurrentHashMap<>();
25 |
26 | public OutgoingInvocationTracker(RpcTransport transport, RpcSerializer serializer, RpcOptions options, Timer timer) {
27 | this.transport = transport;
28 | this.options = options;
29 | this.timer = timer;
30 | this.serializer = serializer;
31 | }
32 |
33 | public T invokeRemoteMethod(InvocationDescriptor invocationDescriptor) throws Throwable {
34 | // create a pending invocation
35 | PendingInvocation pendingInvocation = new PendingInvocation<>(options.getTimeoutSeconds(), this.timer, invocationDescriptor, () -> {
36 | // remove the pending invocation from the map
37 | pendingInvocations.remove(invocationDescriptor.getUniqueInvocationId());
38 | });
39 |
40 | // add the pending invocation to the map
41 | pendingInvocations.put(invocationDescriptor.getUniqueInvocationId(), pendingInvocation);
42 |
43 | transport.send(Direction.IMPLEMENTATION, invocationDescriptor.toBuffer(serializer));
44 |
45 | // wait for response or timeout
46 | try {
47 | return pendingInvocation.waitForResponse();
48 | } catch (CompletionException e) {
49 | throw e.getCause();
50 | }
51 | }
52 |
53 | public boolean completeInvocation(InvocationResponse invocationResponse) {
54 | // do we have a pending invocation for this invocation id?
55 | PendingInvocation> pendingInvocation = pendingInvocations.get(invocationResponse.getInvocationId());
56 | if (pendingInvocation == null) {
57 | // we cannot handle this invocation, so it must be handled in another listener
58 | return false;
59 | }
60 |
61 | pendingInvocation.complete(invocationResponse.getResult());
62 |
63 | // remove the pending invocation from the map
64 | pendingInvocations.remove(invocationResponse.getInvocationId());
65 |
66 | // invocation was successfully completed
67 | return true;
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Meteor
2 |
3 | Thank you for considering contributing to Meteor! We welcome contributions from the community to help make this project better. Whether you're a developer, designer, writer, or just an enthusiastic user, there are many ways you can contribute.
4 |
5 | Before you start contributing, please take a moment to review this document for guidelines on how to contribute effectively to Meteor.
6 |
7 | ## How to Contribute
8 |
9 | 1. **Fork the Repository**: Click the "Fork" button at the top right of this repository to create a copy of it in your GitHub account.
10 |
11 | 2. **Clone Your Fork**: Clone the repository to your local machine using the following command, replacing `[your-username]` with your GitHub username:
12 |
13 | ```bash
14 | git clone https://github.com/pixelib/Meteor.git
15 | ```
16 | 3. **Create a Branch**: Create a new branch for your contributions, where `[branch-name]` is a descriptive name for your branch:
17 |
18 | ```bash
19 | git checkout -b [branch-name]
20 | ```
21 |
22 | 4. **Make Changes**: Make your desired changes to the codebase. Ensure that your code follows the project's coding guidelines.
23 |
24 | 5. **Test Your Changes**: If applicable, test your changes thoroughly to ensure they work as expected. Add or update any necessary tests.
25 |
26 | 6. **Commit Your Changes**: Commit your changes with a clear and descriptive commit message:
27 |
28 | ```bash
29 | git commit -m "Your descriptive commit message here"
30 | ```
31 |
32 | 7. **Push Your Changes**: Push your changes to your forked repository on GitHub:
33 |
34 | ```bash
35 | git push origin [branch-name]
36 | ```
37 |
38 | 8. **Create a Pull Request (PR)**: Go to the [original repository](https://github.com/pixelib/Meteor.git) and click the "New Pull Request" button. Follow the instructions to create a PR, describing your changes and their purpose.
39 |
40 | 9. **Review and Collaboration**: Your PR will be reviewed by the project maintainers and other contributors. Be prepared to address any feedback and make necessary changes.
41 |
42 | 10. **Merging**: Once your PR is approved, it will be merged into the main project. Congratulations, you've successfully contributed to Meteor!
43 |
44 | ## Reporting Issues
45 |
46 | If you encounter any issues or have suggestions for improvements, please feel free to [create an issue](https://github.com/pixelib/Meteor/issues). Make sure to provide as much detail as possible to help us understand and address the problem.
47 |
48 | ## Code Style Guidelines
49 |
50 | - Follow the established coding style and conventions used in the project. If there are specific style guides, they will be documented in the project's documentation or in a separate file.
51 |
52 | ## Licensing
53 |
54 | By contributing to Meteor, you agree that your contributions will be licensed under the project's [LICENSE](LICENSE).
55 |
56 | ## Thank You
57 |
58 | Your contributions are greatly appreciated! We couldn't do it without you. Thank you for being a part of the Meteor community.
59 |
60 | If you have any questions or need further assistance, feel free to reach out to us via the project's issue tracker.
61 |
62 | Happy contributing! 🚀
63 |
--------------------------------------------------------------------------------
/meteor-core/src/test/java/dev/pixelib/meteor/core/MeteorTest.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core;
2 |
3 | import dev.pixelib.meteor.base.defaults.LoopbackTransport;
4 | import dev.pixelib.meteor.core.utils.MathFunctions;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.lang.reflect.Proxy;
8 |
9 | import static org.junit.jupiter.api.Assertions.*;
10 |
11 | class MeteorTest {
12 |
13 | @Test
14 | void registerProcedureExpectSuccess() {
15 | Meteor meteor = new Meteor(new LoopbackTransport());
16 |
17 | MathFunctions mathFunctions = meteor.registerProcedure(MathFunctions.class);
18 |
19 | assertNotNull(mathFunctions);
20 | assertTrue(Meteor.isRpc(mathFunctions));
21 | assertTrue(Meteor.isMeteorProxy(mathFunctions));
22 | }
23 |
24 | @Test
25 | void registerProcedureNotWithInterfaceExpectToFail() {
26 | Meteor meteor = new Meteor(new LoopbackTransport());
27 |
28 | assertThrowsExactly(IllegalArgumentException.class, () -> {
29 | Meteor procedure = meteor.registerProcedure(Meteor.class);
30 | }, "Procedure was an interface");
31 | }
32 |
33 | @Test
34 | void registerProcedureWithNullArgumentExpectToFail() {
35 | Meteor meteor = new Meteor(new LoopbackTransport());
36 |
37 | assertThrowsExactly(NullPointerException.class, () -> {
38 | MathFunctions mathFunctions = meteor.registerProcedure(null);
39 | }, "Argument was not null");
40 | }
41 |
42 |
43 | @Test
44 | void testRegisterProcedureWithNameExpectSuccess() {
45 | Meteor meteor = new Meteor(new LoopbackTransport());
46 |
47 | MathFunctions mathFunctions = meteor.registerProcedure(MathFunctions.class, "Cooler-math-functions");
48 |
49 | assertNotNull(mathFunctions);
50 | assertTrue(Meteor.isRpc(mathFunctions));
51 | assertTrue(Meteor.isMeteorProxy(mathFunctions));
52 | }
53 |
54 | @Test
55 | void isRpcExpectSuccess() {
56 | Meteor meteor = new Meteor(new LoopbackTransport());
57 |
58 | MathFunctions mathFunctions = meteor.registerProcedure(MathFunctions.class, "Cooler-math-functions");
59 | boolean isRpc = Meteor.isRpc(mathFunctions);
60 |
61 | assertTrue(isRpc);
62 | }
63 |
64 | @Test
65 | void isRpcInvalidTypesExpectFail() {
66 | boolean objectIsRpc = Meteor.isRpc(new Object());
67 | assertFalse(objectIsRpc);
68 |
69 | boolean rpcSerializer = Meteor.isRpc("rpcSerializer");
70 | assertFalse(rpcSerializer);
71 |
72 | }
73 |
74 | @Test
75 | void isMeteorExpectSuccess() {
76 | Meteor meteor = new Meteor(new LoopbackTransport());
77 |
78 | MathFunctions mathFunctions = meteor.registerProcedure(MathFunctions.class, "Cooler-math-functions");
79 | boolean ismeteor = Meteor.isMeteorProxy(mathFunctions);
80 |
81 | assertTrue(ismeteor);
82 | }
83 |
84 | @Test
85 | void isMeteorExpectFail() {
86 | Object rpc = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{MathFunctions.class}, (proxy, method, args) -> null);
87 |
88 | boolean ismeteor = Meteor.isMeteorProxy(rpc);
89 |
90 | assertFalse(ismeteor);
91 | }
92 |
93 | }
--------------------------------------------------------------------------------
/meteor-core/src/test/java/dev/pixelib/meteor/core/invocations/PendingInvocationTest.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.invocations;
2 |
3 | import dev.pixelib.meteor.base.RpcOptions;
4 | import dev.pixelib.meteor.base.defaults.GsonSerializer;
5 | import dev.pixelib.meteor.base.defaults.LoopbackTransport;
6 | import dev.pixelib.meteor.base.errors.InvocationTimedOutException;
7 | import dev.pixelib.meteor.core.trackers.OutgoingInvocationTracker;
8 | import dev.pixelib.meteor.core.transport.packets.InvocationDescriptor;
9 | import dev.pixelib.meteor.core.transport.packets.InvocationResponse;
10 | import org.junit.jupiter.api.AfterAll;
11 | import org.junit.jupiter.api.BeforeAll;
12 | import org.junit.jupiter.api.Test;
13 | import org.junit.jupiter.api.Timeout;
14 |
15 | import java.util.Timer;
16 | import java.util.concurrent.LinkedBlockingQueue;
17 | import java.util.concurrent.ThreadPoolExecutor;
18 | import java.util.concurrent.TimeUnit;
19 |
20 | import static org.junit.jupiter.api.Assertions.assertEquals;
21 | import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
22 |
23 | public class PendingInvocationTest {
24 |
25 | // test thread pool
26 | private static ThreadPoolExecutor threadPoolExecutor;
27 |
28 | @BeforeAll
29 | public static void setUp() {
30 | // create thread pool
31 | threadPoolExecutor = new ThreadPoolExecutor(5, 5, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
32 | }
33 |
34 | @AfterAll
35 | public static void tearDown() {
36 | // shutdown thread pool
37 | threadPoolExecutor.shutdown();
38 | }
39 |
40 | @Test
41 | public void testPendingInvocation() throws Throwable {
42 | // base instance
43 | OutgoingInvocationTracker outgoingInvocationTracker = new OutgoingInvocationTracker(new LoopbackTransport(), new GsonSerializer(), new RpcOptions(), new Timer());
44 |
45 | InvocationDescriptor invocationDescriptor = new InvocationDescriptor("namespace", getClass(), "methodName", new Object[]{}, new Class[]{}, String.class);
46 |
47 | String testString = "test invocation";
48 |
49 | // complete invocation
50 | threadPoolExecutor.execute(() -> {
51 | try {
52 | Thread.sleep(1000);
53 | } catch (InterruptedException e) {
54 | e.printStackTrace();
55 | }
56 | outgoingInvocationTracker.completeInvocation(
57 | new InvocationResponse(invocationDescriptor.getUniqueInvocationId(), testString)
58 | );
59 | });
60 |
61 | String response = outgoingInvocationTracker.invokeRemoteMethod(invocationDescriptor);
62 | assertEquals(testString, response);
63 | }
64 |
65 | @Test
66 | @Timeout(2) // seconds
67 | public void testTimeout() {
68 | RpcOptions options = new RpcOptions();
69 | options.setTimeoutSeconds(1);
70 | OutgoingInvocationTracker outgoingInvocationTracker = new OutgoingInvocationTracker(new LoopbackTransport(), new GsonSerializer(), options, new Timer());
71 |
72 | InvocationDescriptor invocationDescriptor = new InvocationDescriptor("namespace", getClass(), "methodName", new Object[]{}, new Class[]{}, String.class);
73 |
74 | assertThrowsExactly(InvocationTimedOutException.class, () -> {
75 | outgoingInvocationTracker.invokeRemoteMethod(invocationDescriptor);
76 | });
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/meteor-core/src/main/java/dev/pixelib/meteor/core/transport/packets/InvocationResponse.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.transport.packets;
2 |
3 | import dev.pixelib.meteor.base.RpcSerializer;
4 | import dev.pixelib.meteor.core.utils.ArgumentMapper;
5 | import io.netty.buffer.ByteBuf;
6 | import io.netty.buffer.Unpooled;
7 |
8 | import java.nio.charset.Charset;
9 | import java.util.UUID;
10 |
11 | public class InvocationResponse {
12 |
13 | /**
14 | * Unique identifier for this invocation, used to match responses to requests.
15 | * References the ID in the InvocationDescriptor.
16 | */
17 | private final UUID invocationId;
18 |
19 | /**
20 | * Result of the invocation.
21 | */
22 | private final Object result;
23 |
24 | public InvocationResponse(UUID invocationId, Object result) {
25 | this.invocationId = invocationId;
26 | this.result = result;
27 | }
28 |
29 | public byte[] toBytes(RpcSerializer serializer) {
30 | ByteBuf buffer = Unpooled.buffer();
31 | buffer.writeLong(invocationId.getMostSignificantBits());
32 | buffer.writeLong(invocationId.getLeastSignificantBits());
33 |
34 | if (result == null) {
35 | buffer.writeBoolean(true);
36 | } else {
37 | buffer.writeBoolean(false);
38 | buffer.writeBoolean(result.getClass().isPrimitive());
39 |
40 | String resultType = result.getClass().getName();
41 | buffer.writeInt(resultType.length());
42 | buffer.writeCharSequence(resultType, Charset.defaultCharset());
43 |
44 | byte[] resultBytes = serializer.serialize(result);
45 | buffer.writeInt(resultBytes.length);
46 | buffer.writeBytes(resultBytes);
47 | }
48 |
49 | byte[] byteArray = new byte[buffer.readableBytes()];
50 | buffer.readBytes(byteArray);
51 | // release the buffer
52 | buffer.release();
53 | return byteArray;
54 | }
55 |
56 | public static InvocationResponse fromBytes(RpcSerializer serializer, byte[] bytes) throws ClassNotFoundException {
57 | ByteBuf buffer = Unpooled.wrappedBuffer(bytes);
58 | UUID invocationId = new UUID(buffer.readLong(), buffer.readLong());
59 |
60 | boolean isNull = buffer.readBoolean();
61 | if (isNull) {
62 | buffer.release();
63 | return new InvocationResponse(invocationId, null);
64 | } else {
65 | boolean isPrimitive = buffer.readBoolean();
66 |
67 | Class> resultClass;
68 | String responseType = buffer.readCharSequence(buffer.readInt(), Charset.defaultCharset()).toString();
69 | if (isPrimitive) {
70 | resultClass = ArgumentMapper.resolvePrimitive(responseType);
71 | } else {
72 | resultClass = Class.forName(responseType);
73 | }
74 |
75 | int resultLength = buffer.readInt();
76 | byte[] resultBytes = new byte[resultLength];
77 | buffer.readBytes(resultBytes);
78 | Object result = serializer.deserialize(resultBytes, resultClass);
79 | buffer.release();
80 | return new InvocationResponse(invocationId, result);
81 | }
82 |
83 | }
84 |
85 | public UUID getInvocationId() {
86 | return invocationId;
87 | }
88 |
89 | public Object getResult() {
90 | return result;
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/meteor-core/src/test/java/dev/pixelib/meteor/core/executor/ImplementationWrapperTest.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.executor;
2 |
3 | import dev.pixelib.meteor.core.transport.packets.InvocationDescriptor;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import java.util.Arrays;
7 |
8 | import static org.junit.jupiter.api.Assertions.*;
9 |
10 | class ImplementationWrapperTest {
11 |
12 | @Test
13 | void invokeOn_success() throws NoSuchMethodException {
14 |
15 | ImplementationWrapper wrapper = new ImplementationWrapper(new MathFunctionImplementation(), "namespace");
16 | Class>[] argTypes = new Class>[] { int.class, int.class };
17 |
18 | InvocationDescriptor descriptor = new InvocationDescriptor("math", MathFunctionImplementation.class, "add", new Object[]{1, 2},argTypes, int.class);
19 | assertEquals(3, wrapper.invokeOn(descriptor, int.class));
20 | }
21 |
22 | @Test
23 | void invokeOn_withBoxParams() throws NoSuchMethodException {
24 | ImplementationWrapper wrapper = new ImplementationWrapper(new MathFunctionImplementation(), "namespace");
25 | Class>[] argTypes = new Class>[] { Integer.class, Integer.class };
26 |
27 | InvocationDescriptor descriptor = new InvocationDescriptor("math", MathFunctionImplementation.class, "add", new Object[]{1, 2},argTypes, int.class);
28 | assertEquals(3, wrapper.invokeOn(descriptor, int.class));
29 | }
30 |
31 | @Test
32 | void invokeOn_unknownMethod() {
33 | ImplementationWrapper wrapper = new ImplementationWrapper(new MathFunctionImplementation(), "namespace");
34 | Class>[] argTypes = new Class>[] { Integer.class, Integer.class };
35 |
36 | InvocationDescriptor descriptor = new InvocationDescriptor("math", MathFunctionImplementation.class, "unknown", new Object[]{1, 2},argTypes, int.class);
37 | NoSuchMethodException noSuchMethodException = assertThrowsExactly(NoSuchMethodException.class, () -> {
38 | wrapper.invokeOn(descriptor, int.class);
39 | });
40 |
41 | assertEquals("No method found with name unknown and compatible arguments (on " + MathFunctionImplementation.class.getName() + ").", noSuchMethodException.getMessage());
42 | }
43 | @Test
44 | void getImplementation_success() {
45 | MathFunctionImplementation implementation = new MathFunctionImplementation();
46 |
47 | ImplementationWrapper implementationWrapper = new ImplementationWrapper(implementation, "math");
48 |
49 | assertSame(implementation, implementationWrapper.getImplementation());
50 | }
51 |
52 | @Test
53 | void getNamespace_success() {
54 | ImplementationWrapper implementationWrapper = new ImplementationWrapper(new MathFunctionImplementation(), "math");
55 | assertEquals("math", implementationWrapper.getNamespace());
56 | }
57 |
58 | static class MathFunctionImplementation implements MathFunction {
59 | @Override
60 | public int add(int a, int b) {
61 | return a + b;
62 | }
63 |
64 | @Override
65 | public int add(int... a) {
66 | return Arrays.stream(a).sum();
67 | }
68 |
69 | @Override
70 | public int add(int a, Integer... b) {
71 | return a + Arrays.stream(b).mapToInt(Integer::intValue).sum();
72 | }
73 |
74 | @Override
75 | public int add(int a, Double... b) {
76 | return a + Arrays.stream(b).mapToInt(Double::intValue).sum();
77 | }
78 |
79 | @Override
80 | public int sub(int a, int b) {
81 | return a - b;
82 | }
83 | }
84 |
85 | public interface MathFunction {
86 | int add(int a, int b);
87 | int add(int... a);
88 | int add(int a, Integer... b);
89 |
90 |
91 | int add(int a, Double... b);
92 |
93 | int sub(int a, int b);
94 | }
95 | }
--------------------------------------------------------------------------------
/meteor-jedis/src/main/java/dev/pixelib/meteor/transport/redis/RedisTransport.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.transport.redis;
2 |
3 | import dev.pixelib.meteor.base.RpcTransport;
4 | import dev.pixelib.meteor.base.enums.Direction;
5 | import dev.pixelib.meteor.base.interfaces.SubscriptionHandler;
6 | import redis.clients.jedis.Jedis;
7 | import redis.clients.jedis.JedisPool;
8 |
9 | import java.io.IOException;
10 | import java.util.Base64;
11 | import java.util.Locale;
12 | import java.util.UUID;
13 | import java.util.function.Consumer;
14 | import java.util.logging.Logger;
15 |
16 | public class RedisTransport implements RpcTransport {
17 |
18 | private final Logger logger = Logger.getLogger(RedisTransport.class.getSimpleName());
19 |
20 | private final JedisPool jedisPool;
21 | private final String topic;
22 | private RedisSubscriptionThread redisSubscriptionThread;
23 | private final UUID transportId = UUID.randomUUID();
24 | private boolean ignoreSelf = true;
25 |
26 | public RedisTransport(JedisPool jedisPool, String topic) {
27 | this.jedisPool = jedisPool;
28 | this.topic = topic;
29 | }
30 |
31 | public RedisTransport(String url, String topic) {
32 | this.jedisPool = new JedisPool(url);
33 | this.topic = topic;
34 | }
35 |
36 | public RedisTransport(String host, int port, String topic) {
37 | this.jedisPool = new JedisPool(host, port);
38 | this.topic = topic;
39 | }
40 |
41 | public RedisTransport withIgnoreSelf(boolean ignoreSelf) {
42 | this.ignoreSelf = ignoreSelf;
43 | return this;
44 | }
45 |
46 | @Override
47 | public void send(Direction direction, byte[] bytes) {
48 | if (jedisPool.isClosed()) {
49 | throw new IllegalStateException("Jedis pool is closed");
50 | }
51 |
52 | try (Jedis connection = jedisPool.getResource()) {
53 | connection.publish(
54 | getTopicName(direction),
55 | transportId + Base64.getEncoder().encodeToString(bytes)
56 | );
57 | }
58 | }
59 |
60 | @Override
61 | public void subscribe(Direction direction, SubscriptionHandler onReceive) {
62 | if (jedisPool.isClosed()) {
63 | throw new IllegalStateException("Jedis pool is closed");
64 | }
65 |
66 | StringMessageBroker wrappedHandler = (message) -> {
67 | byte[] bytes = message.getBytes();
68 | // only split after UUID, which always has a length of 36
69 | byte[] uuid = new byte[36];
70 | System.arraycopy(bytes, 0, uuid, 0, 36);
71 |
72 | if (ignoreSelf && transportId.toString().equals(new String(uuid))) {
73 | return false;
74 | }
75 |
76 | byte[] data = new byte[bytes.length - 36];
77 | System.arraycopy(bytes, 36, data, 0, data.length);
78 |
79 | try {
80 | return onReceive.onPacket(Base64.getDecoder().decode(data));
81 | } catch (Exception e) {
82 | throw new RuntimeException(e);
83 | }
84 | };
85 |
86 | if (redisSubscriptionThread == null) {
87 | redisSubscriptionThread = new RedisSubscriptionThread(wrappedHandler, logger, getTopicName(direction), jedisPool);
88 | redisSubscriptionThread.start().join();
89 | } else {
90 | redisSubscriptionThread.subscribe(getTopicName(direction), wrappedHandler);
91 | }
92 | }
93 |
94 | public String getTopicName(Direction direction) {
95 | return topic + "_" + direction.name().toLowerCase(Locale.ROOT);
96 | }
97 |
98 | @Override
99 | public void close() throws IOException {
100 | if (redisSubscriptionThread != null) {
101 | redisSubscriptionThread.stop();
102 | }
103 |
104 | jedisPool.close();
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/meteor-jedis/src/main/java/dev/pixelib/meteor/transport/redis/RedisSubscriptionThread.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.transport.redis;
2 |
3 | import dev.pixelib.meteor.base.interfaces.SubscriptionHandler;
4 | import redis.clients.jedis.Jedis;
5 | import redis.clients.jedis.JedisPool;
6 | import redis.clients.jedis.exceptions.JedisConnectionException;
7 |
8 | import java.util.concurrent.CompletableFuture;
9 | import java.util.concurrent.CompletionException;
10 | import java.util.concurrent.ExecutorService;
11 | import java.util.concurrent.Executors;
12 | import java.util.function.Consumer;
13 | import java.util.logging.Level;
14 | import java.util.logging.Logger;
15 |
16 | public class RedisSubscriptionThread {
17 |
18 | private final StringMessageBroker messageBroker;
19 | private final Logger logger;
20 | private final String defaultChannel;
21 | private final JedisPool jedisPool;
22 | private boolean isStopping = false;
23 |
24 | private RedisPacketListener jedisPacketListener;
25 |
26 | private final ExecutorService listenerThread = Executors.newSingleThreadExecutor(r -> {
27 | Thread thread = new Thread(r, "meteor-redis-listener-thread");
28 | thread.setDaemon(true);
29 | return thread;
30 | });
31 |
32 | public RedisSubscriptionThread(StringMessageBroker messageBroker, Logger logger, String channel, JedisPool jedisPool) {
33 | this.messageBroker = messageBroker;
34 | this.logger = logger;
35 | this.defaultChannel = channel;
36 | this.jedisPool = jedisPool;
37 | }
38 |
39 | public CompletableFuture start() {
40 | jedisPacketListener = new RedisPacketListener(messageBroker, defaultChannel, logger);
41 |
42 | Runnable runnable = () -> {
43 | while (!Thread.currentThread().isInterrupted()) {
44 | try (Jedis connection = jedisPool.getResource()) {
45 | connection.ping();
46 | logger.log(Level.FINE, "Redis connected!");
47 |
48 | //Start blocking
49 | connection.subscribe(jedisPacketListener, jedisPacketListener.getCustomSubscribedChannels().toArray(new String[]{}));
50 | break;
51 | } catch (JedisConnectionException e) {
52 | if (isStopping) {
53 | logger.log(Level.FINE, "Redis connection closed, interrupted by stop");
54 | return;
55 | }
56 | logger.log(Level.SEVERE, "Redis has lost connection", e);
57 | }
58 |
59 | try {
60 | Thread.sleep(1000);
61 | } catch (InterruptedException exception) {
62 | Thread.currentThread().interrupt();
63 | }
64 | }
65 | };
66 |
67 | listenerThread.execute(runnable);
68 |
69 | return isSubscribed();
70 |
71 | }
72 |
73 | public void stop() {
74 | if (isStopping) return;
75 | isStopping = true;
76 | jedisPacketListener.stop();
77 | listenerThread.shutdownNow();
78 | }
79 |
80 | public void subscribe(String channel, StringMessageBroker onReceive) {
81 | jedisPacketListener.subscribe(channel, onReceive);
82 |
83 | }
84 |
85 | private CompletableFuture isSubscribed() {
86 | return CompletableFuture.supplyAsync(() -> {
87 | final int maxAttempts = 5;
88 | for (int i = 0; i < maxAttempts; i++) {
89 | if (jedisPacketListener.isSubscribed()) {
90 | return true;
91 | }
92 |
93 | try {
94 | Thread.sleep(1000);
95 | } catch (InterruptedException e) {
96 | Thread.currentThread().interrupt();
97 | throw new CompletionException("Thread was interrupted while waiting for subscription", e);
98 | }
99 | }
100 |
101 | // If it fails to subscribe within 5 attempts (5 seconds), throw an exception
102 | throw new IllegalStateException("Failed to subscribe within the given timeframe");
103 | });
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/benchmarks/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | dev.pixelib.meteor
8 | meteor-parent
9 | ${revision}
10 |
11 |
12 | benchmarks
13 |
14 |
15 | 17
16 | 17
17 | UTF-8
18 |
19 | 1.37
20 |
21 |
22 |
23 |
24 | dev.pixelib.meteor
25 | meteor-core
26 | ${revision}
27 |
28 |
29 | dev.pixelib.meteor
30 | meteor-common
31 | ${revision}
32 |
33 |
34 | dev.pixelib.meteor.transport
35 | meteor-jedis
36 | ${revision}
37 |
38 |
39 |
40 | org.openjdk.jmh
41 | jmh-core
42 | ${jmh.version}
43 |
44 |
45 |
46 | org.openjdk.jmh
47 | jmh-generator-annprocess
48 | ${jmh.version}
49 | test
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | org.apache.maven.plugins
59 | maven-compiler-plugin
60 |
61 | 17
62 | 17
63 |
64 |
65 | org.openjdk.jmh
66 | jmh-generator-annprocess
67 | ${jmh.version}
68 |
69 |
70 |
71 |
72 |
73 |
74 | org.apache.maven.plugins
75 | maven-shade-plugin
76 |
77 |
78 | package
79 |
80 | shade
81 |
82 |
83 | benchmarks
84 |
85 |
87 | org.openjdk.jmh.Main
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | org.apache.maven.plugins
97 | maven-deploy-plugin
98 |
99 | true
100 |
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/meteor-core/src/main/java/dev/pixelib/meteor/core/proxy/PendingInvocation.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.proxy;
2 |
3 | import dev.pixelib.meteor.base.errors.InvocationTimedOutException;
4 | import dev.pixelib.meteor.core.transport.packets.InvocationDescriptor;
5 | import dev.pixelib.meteor.core.utils.ArgumentMapper;
6 |
7 | import java.util.Timer;
8 | import java.util.TimerTask;
9 | import java.util.concurrent.CompletableFuture;
10 | import java.util.concurrent.TimeUnit;
11 | import java.util.concurrent.atomic.AtomicBoolean;
12 |
13 | public class PendingInvocation extends TimerTask {
14 |
15 | /**
16 | * A pending invocation represents a blocking invocation request which is awaiting a response, acknowledgement or timeout.
17 | * It is used to block the calling thread until the invocation is complete.
18 | */
19 |
20 | private final int timeoutSeconds;
21 | private final CompletableFuture completable;
22 | private final InvocationDescriptor invocationDescriptor;
23 |
24 | // A callback to be called when the invocation times out. Used to mitigate the risk of memory leaks.
25 | private final Runnable timeoutCallback;
26 |
27 | private final AtomicBoolean isComplete = new AtomicBoolean(false);
28 | private final AtomicBoolean isTimedOut = new AtomicBoolean(false);
29 |
30 | public PendingInvocation(int timeoutSeconds, Timer timer, InvocationDescriptor invocationDescriptor, Runnable timeoutCallback) {
31 | this.invocationDescriptor = invocationDescriptor;
32 | this.timeoutCallback = timeoutCallback;
33 | this.timeoutSeconds = timeoutSeconds;
34 | completable = new CompletableFuture<>();
35 |
36 | // schedule timeout, timeoutSeconds is in seconds, Timer.schedule() takes milliseconds
37 | timer.schedule(this, TimeUnit.SECONDS.toMillis(timeoutSeconds));
38 | }
39 |
40 | /**
41 | * Complete and clean a pending invocation. This method should only be called once.
42 | * This should be directly invoked from the transport when a response is received and deserialized.
43 | * @param response The response to complete the invocation with.
44 | */
45 | public void complete(Object response) throws IllegalStateException {
46 | if (isTimedOut.get()) {
47 | throw new IllegalStateException("Cannot complete invocation after timeout.");
48 | }
49 |
50 | if (isComplete.get()) {
51 | throw new IllegalStateException("Cannot complete invocation twice.");
52 | }
53 |
54 | boolean isVoidOrNullable = invocationDescriptor.getReturnType().equals(Void.TYPE) || !invocationDescriptor.getReturnType().isPrimitive();
55 |
56 | // check instance of response
57 | if (!isVoidOrNullable && !invocationDescriptor.getReturnType().isInstance(response)) {
58 | // is the normal return type primitive? then check if its still assignable as a boxed
59 | if (invocationDescriptor.getReturnType().isPrimitive()) {
60 | if (!ArgumentMapper.ensureBoxedClass(invocationDescriptor.getReturnType()).isAssignableFrom(response.getClass())) {
61 | throw new IllegalStateException("Response is not an instance of the expected return type. " +
62 | "Expected: " + invocationDescriptor.getReturnType().getName() + ", " +
63 | "Actual: " + response.getClass().getName());
64 | }
65 | }
66 | }
67 |
68 | isComplete.set(true);
69 | this.completable.complete((T) response);
70 | }
71 |
72 | public T waitForResponse() throws InvocationTimedOutException {
73 | // wait for response or timeout
74 | return this.completable.join();
75 | }
76 |
77 | /**
78 | * Inherited from TimerTask.
79 | * Called when the timeout expires.
80 | */
81 | @Override
82 | public void run() {
83 | if (isComplete.get()) {
84 | // the invocation completed before the timeout
85 | return;
86 | }
87 |
88 | isTimedOut.set(true);
89 |
90 | this.completable.completeExceptionally(
91 | new InvocationTimedOutException(invocationDescriptor.getMethodName(), invocationDescriptor.getNamespace(), timeoutSeconds)
92 | );
93 |
94 | // call the timeout callback
95 | if (timeoutCallback != null) {
96 | timeoutCallback.run();
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/meteor-jedis/src/test/java/dev/pixelib/meteor/transport/redis/RedisSubscriptionThreadTest.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.transport.redis;
2 |
3 | import com.github.fppt.jedismock.RedisServer;
4 | import com.github.fppt.jedismock.operations.server.MockExecutor;
5 | import com.github.fppt.jedismock.server.ServiceOptions;
6 | import org.junit.jupiter.api.Disabled;
7 | import org.junit.jupiter.api.Test;
8 | import redis.clients.jedis.JedisPool;
9 |
10 | import java.util.Collection;
11 | import java.util.HashSet;
12 | import java.util.concurrent.CompletionException;
13 | import java.util.logging.Logger;
14 |
15 | import static org.junit.jupiter.api.Assertions.*;
16 |
17 | class RedisSubscriptionThreadTest {
18 |
19 | @Test
20 | @Disabled
21 | void start_success() throws Exception {
22 | RedisServer server = RedisServer.newRedisServer().start();
23 |
24 | JedisPool jedisPool = new JedisPool(server.getHost(), server.getBindPort());
25 | RedisSubscriptionThread subThread = new RedisSubscriptionThread(packet -> true, Logger.getAnonymousLogger(), "channel", jedisPool);
26 |
27 | boolean result = subThread.start().join();
28 |
29 | assertTrue(result);
30 | assertFalse(jedisPool.isClosed());
31 |
32 |
33 | subThread.stop();
34 | jedisPool.close();
35 | server.stop();
36 | assertTrue(jedisPool.isClosed());
37 | }
38 |
39 | @Test
40 | void start_NoConnection() throws Exception {
41 | JedisPool jedisPool = new JedisPool("127.0.0.5", 2314);
42 | RedisSubscriptionThread subThread = new RedisSubscriptionThread(packet -> true, Logger.getAnonymousLogger(), "channel", jedisPool);
43 |
44 | CompletionException exception = assertThrowsExactly(CompletionException.class, () -> {
45 | subThread.start().join();
46 | });
47 |
48 | assertEquals(IllegalStateException.class, exception.getCause().getClass());
49 | assertEquals("Failed to subscribe within the given timeframe", exception.getCause().getMessage());
50 | }
51 |
52 | @Test
53 | @Disabled
54 | void start_reconnect() throws Exception {
55 | RedisServer server = RedisServer.newRedisServer().start();
56 |
57 | JedisPool jedisPool = new JedisPool(server.getHost(), server.getBindPort());
58 | RedisSubscriptionThread subThread = new RedisSubscriptionThread(packet -> true, Logger.getAnonymousLogger(), "channel", jedisPool);
59 |
60 | boolean result = subThread.start().join();
61 |
62 | assertTrue(result);
63 | assertFalse(jedisPool.isClosed());
64 |
65 | server.stop();
66 |
67 | Thread.sleep(100);
68 | server.start();
69 |
70 | subThread.stop();
71 | jedisPool.close();
72 | assertTrue(jedisPool.isClosed());
73 | }
74 |
75 | @Test
76 | @Disabled
77 | void subscribe_success() throws Exception {
78 | Collection subscribedChannels = new HashSet<>();
79 |
80 | RedisServer server = RedisServer.newRedisServer()
81 | .setOptions(ServiceOptions.withInterceptor((state, command, params) -> {
82 | if ("subscribe".equals(command)) {
83 | subscribedChannels.add(params.get(0).toString());
84 | }
85 | return MockExecutor.proceed(state, command, params);
86 | }))
87 | .start();
88 |
89 | JedisPool jedisPool = new JedisPool(server.getHost(), server.getBindPort());
90 | RedisSubscriptionThread subThread = new RedisSubscriptionThread(packet -> true, Logger.getAnonymousLogger(), "channel", jedisPool);
91 |
92 | subThread.start().join();
93 |
94 | subThread.stop();
95 | jedisPool.close();
96 | server.stop();
97 | assertTrue(jedisPool.isClosed());
98 | assertTrue(subscribedChannels.contains("channel"));
99 | }
100 |
101 | @Test
102 | @Disabled
103 | void stop_success() throws Exception {
104 | RedisServer server = RedisServer.newRedisServer().start();
105 |
106 | JedisPool jedisPool = new JedisPool(server.getHost(), server.getBindPort());
107 | RedisSubscriptionThread subThread = new RedisSubscriptionThread(packet -> true, Logger.getAnonymousLogger(), "channel", jedisPool);
108 |
109 | subThread.start().join();
110 |
111 | subThread.stop();
112 | jedisPool.close();
113 | server.stop();
114 | assertTrue(jedisPool.isClosed());
115 | }
116 | }
--------------------------------------------------------------------------------
/meteor-core/src/test/java/dev/pixelib/meteor/core/LogicTest.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core;
2 |
3 | import dev.pixelib.meteor.base.RpcSerializer;
4 | import dev.pixelib.meteor.base.RpcTransport;
5 | import dev.pixelib.meteor.base.defaults.GsonSerializer;
6 | import dev.pixelib.meteor.base.defaults.LoopbackTransport;
7 | import dev.pixelib.meteor.core.executor.ImplementationWrapper;
8 | import dev.pixelib.meteor.core.transport.packets.InvocationDescriptor;
9 | import org.junit.jupiter.api.Test;
10 |
11 | import static org.junit.jupiter.api.Assertions.assertEquals;
12 |
13 | public class LogicTest {
14 |
15 | @Test
16 | public void testSerializedReflectionWithPrimitiveArray() throws ClassNotFoundException, NoSuchMethodException {
17 | // Confirmation that core logic of barebones reflection invocations over a serialized array still works
18 | RpcTransport transport = new LoopbackTransport();
19 | RpcSerializer serializer = new GsonSerializer();
20 |
21 | // this should return 30
22 | InvocationDescriptor invocationDescriptor = new InvocationDescriptor(
23 | null,
24 | LogicTest.class,
25 | "worstCaseScenarioTest",
26 | new Object[]{2, 5, 5, 5},
27 | new Class>[]{int.class, int[].class},
28 | int.class
29 | );
30 |
31 | byte[] serializedInvocationDescriptor = invocationDescriptor.toBuffer(serializer);
32 |
33 | InvocationDescriptor deserializedInvocationDescriptor = InvocationDescriptor.fromBuffer(serializer, serializedInvocationDescriptor);
34 |
35 | ImplementationWrapper implementationWrapper = new ImplementationWrapper(this, null);
36 |
37 | int result = (int) implementationWrapper.invokeOn(deserializedInvocationDescriptor, deserializedInvocationDescriptor.getReturnType());
38 |
39 | assertEquals(30, result);
40 | }
41 |
42 | @Test
43 | public void testSerializedReflectionWithPrimitiveArrayAndNull() throws ClassNotFoundException, NoSuchMethodException {
44 | // Confirmation that core logic of barebones reflection invocations over a serialized array still works
45 | RpcTransport transport = new LoopbackTransport();
46 | RpcSerializer serializer = new GsonSerializer();
47 |
48 | // this should return 30
49 | InvocationDescriptor invocationDescriptor = new InvocationDescriptor(
50 | null,
51 | LogicTest.class,
52 | "worstCaseScenarioTest",
53 | new Object[]{2, null, 5, 5},
54 | new Class>[]{int.class, int[].class},
55 | int.class
56 | );
57 |
58 | byte[] serializedInvocationDescriptor = invocationDescriptor.toBuffer(serializer);
59 |
60 | InvocationDescriptor deserializedInvocationDescriptor = InvocationDescriptor.fromBuffer(serializer, serializedInvocationDescriptor);
61 |
62 | ImplementationWrapper implementationWrapper = new ImplementationWrapper(this, null);
63 |
64 | int result = (int) implementationWrapper.invokeOn(deserializedInvocationDescriptor, deserializedInvocationDescriptor.getReturnType());
65 |
66 | assertEquals(20, result);
67 | }
68 |
69 | @Test
70 | public void testSerializedReflectionWithPrimitiveArrayAndNullAndNull() throws ClassNotFoundException, NoSuchMethodException {
71 | // Confirmation that core logic of barebones reflection invocations over a serialized array still works
72 | RpcTransport transport = new LoopbackTransport();
73 | RpcSerializer serializer = new GsonSerializer();
74 |
75 | // this should return 30
76 | InvocationDescriptor invocationDescriptor = new InvocationDescriptor(
77 | null,
78 | LogicTest.class,
79 | "concatStrings",
80 | new Object[]{"hello", null, "world"},
81 | new Class>[]{String[].class},
82 | String.class
83 | );
84 |
85 | byte[] serializedInvocationDescriptor = invocationDescriptor.toBuffer(serializer);
86 |
87 | InvocationDescriptor deserializedInvocationDescriptor = InvocationDescriptor.fromBuffer(serializer, serializedInvocationDescriptor);
88 |
89 | ImplementationWrapper implementationWrapper = new ImplementationWrapper(this, null);
90 |
91 | String result = (String) implementationWrapper.invokeOn(deserializedInvocationDescriptor, deserializedInvocationDescriptor.getReturnType());
92 |
93 | assertEquals("hellonullworld", result);
94 | }
95 |
96 |
97 | public String concatStrings(String[] strings) {
98 | StringBuilder sb = new StringBuilder();
99 | for (String s : strings) {
100 | sb.append(s);
101 | }
102 | return sb.toString();
103 | }
104 |
105 | public int worstCaseScenarioTest(int a, int... addBeforeMultiplying) {
106 | int result = 0;
107 | for (int i : addBeforeMultiplying) {
108 | result += i;
109 | }
110 | return a * result;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/meteor-core/src/main/java/dev/pixelib/meteor/core/transport/TransportHandler.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.transport;
2 |
3 | import dev.pixelib.meteor.base.RpcSerializer;
4 | import dev.pixelib.meteor.base.RpcTransport;
5 | import dev.pixelib.meteor.base.enums.Direction;
6 | import dev.pixelib.meteor.core.executor.ImplementationWrapper;
7 | import dev.pixelib.meteor.core.trackers.IncomingInvocationTracker;
8 | import dev.pixelib.meteor.core.trackers.OutgoingInvocationTracker;
9 | import dev.pixelib.meteor.core.transport.packets.InvocationDescriptor;
10 | import dev.pixelib.meteor.core.transport.packets.InvocationResponse;
11 |
12 | import java.io.Closeable;
13 | import java.io.IOException;
14 | import java.util.Collection;
15 | import java.util.concurrent.ExecutorService;
16 | import java.util.concurrent.Executors;
17 | import java.util.logging.Level;
18 | import java.util.logging.Logger;
19 |
20 | public class TransportHandler implements Closeable {
21 |
22 | private final Logger logger = Logger.getLogger(TransportHandler.class.getSimpleName());
23 | private final RpcSerializer serializer;
24 | private final RpcTransport transport;
25 | private final IncomingInvocationTracker incomingInvocationTracker;
26 | private final OutgoingInvocationTracker outgoingInvocationTracker;
27 |
28 | private final ExecutorService executorPool;
29 |
30 | private boolean isClosed = false;
31 |
32 | public TransportHandler(
33 | RpcSerializer serializer,
34 | RpcTransport transport,
35 | IncomingInvocationTracker incomingInvocationTracker,
36 | OutgoingInvocationTracker outgoingInvocationTracker,
37 | int threadPoolSize
38 | ) {
39 | this.serializer = serializer;
40 | this.transport = transport;
41 | this.incomingInvocationTracker = incomingInvocationTracker;
42 | this.outgoingInvocationTracker = outgoingInvocationTracker;
43 |
44 | this.executorPool = Executors.newFixedThreadPool(threadPoolSize, r -> new Thread(r, "meteor-executor-thread"));
45 |
46 | transport.subscribe(Direction.METHOD_PROXY, this::handleInvocationResponse);
47 | transport.subscribe(Direction.IMPLEMENTATION, TransportHandler.this::handleInvocationRequest);
48 | }
49 |
50 | private boolean handleInvocationResponse(byte[] bytes) throws ClassNotFoundException {
51 | InvocationResponse invocationResponse = InvocationResponse.fromBytes(serializer, bytes);
52 | return outgoingInvocationTracker.completeInvocation(invocationResponse);
53 | }
54 |
55 | private boolean handleInvocationRequest(byte[] bytes) throws ClassNotFoundException {
56 | if (isClosed) {
57 | return false;
58 | }
59 |
60 | // deserialize the packet
61 | InvocationDescriptor invocationDescriptor = InvocationDescriptor.fromBuffer(serializer, bytes);
62 |
63 | // get the invocation handler for this packet
64 | Collection implementations = incomingInvocationTracker.getImplementations().get(invocationDescriptor.getDeclaringClass());
65 |
66 | // if there is no invocation handler, return
67 | if (implementations == null || implementations.isEmpty()) {
68 | return false;
69 | }
70 |
71 | ImplementationWrapper matchedImplementation = implementations.stream()
72 | .filter(
73 | implementation ->
74 | // Either have matching namespaces
75 | (invocationDescriptor.getNamespace() != null && invocationDescriptor.getNamespace().equals(implementation.getNamespace()))
76 | ||
77 | // Or they both don't have namespaces
78 | (implementation.getNamespace() == null && invocationDescriptor.getNamespace() == null)
79 | )
80 | .findFirst()
81 | .orElse(null);
82 |
83 | // if there is an invocation handler, call it
84 |
85 | // We do have handlers for this type, just not by the same name
86 | if (matchedImplementation == null) {
87 | return false;
88 | }
89 |
90 | this.executorPool.submit(() -> {
91 | try {
92 | // move to separate threading
93 | Object response = matchedImplementation.invokeOn(invocationDescriptor, invocationDescriptor.getReturnType());
94 | InvocationResponse invocationResponse = new InvocationResponse(invocationDescriptor.getUniqueInvocationId(), response);
95 | transport.send(Direction.METHOD_PROXY, invocationResponse.toBytes(serializer));
96 | } catch (Throwable e) {
97 | logger.log(Level.SEVERE, "An error occurred while invoking a method", e);
98 | }
99 | });
100 |
101 | return true;
102 | }
103 |
104 | @Override
105 | public void close() throws IOException {
106 | if (isClosed) {
107 | return;
108 | }
109 | isClosed = true;
110 | executorPool.shutdown();
111 | transport.close();
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/meteor-core/src/main/java/dev/pixelib/meteor/core/executor/ImplementationWrapper.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.executor;
2 |
3 | import dev.pixelib.meteor.base.errors.MethodInvocationException;
4 | import dev.pixelib.meteor.core.transport.packets.InvocationDescriptor;
5 | import dev.pixelib.meteor.core.utils.ArgumentMapper;
6 |
7 | import java.lang.reflect.Method;
8 |
9 | public class ImplementationWrapper {
10 |
11 | private final Object implementation;
12 | private final String namespace;
13 |
14 | public ImplementationWrapper(Object implementation, String namespace) {
15 | this.implementation = implementation;
16 | this.namespace = namespace;
17 | }
18 |
19 | public R invokeOn(InvocationDescriptor invocationDescriptor, Class returnType /* not unused, see comment below */) throws NoSuchMethodException {
20 | // Get the method that should be invoked
21 | String methodName = invocationDescriptor.getMethodName();
22 | Class>[] argTypes = invocationDescriptor.getArgTypes();
23 |
24 | Method method;
25 | try {
26 | method = implementation.getClass().getDeclaredMethod(methodName, argTypes);
27 | } catch (NoSuchMethodException e) {
28 | // if the method is not found, try to find a method that is compatible despite signature
29 | method = findCompatibleDespiteSignature(implementation.getClass().getDeclaredMethods(), methodName, argTypes);
30 |
31 | if (method == null) {
32 | throw new NoSuchMethodException("No method found with name " + methodName + " and compatible arguments (on " + implementation.getClass().getName() + ").");
33 | }
34 | }
35 |
36 | // make accessible if private
37 | method.setAccessible(true);
38 |
39 | // sanitize args
40 | Object[] args = ArgumentMapper.overflowArguments(method, invocationDescriptor.getArgs());
41 |
42 | // invoke method
43 | try {
44 | // Normally, we'd use "returnType.cast" here, but it's technically unsafe.
45 | // the cast function does an isInstance check, to make sure that the value is at least a descendant of R,
46 | // However... there are exceptions to this rule, for example, if R is an int and the value is an Integer
47 | // then they aren't strictly the same type (due to the jvm boxing/unboxing rules), but the cast will still
48 | // succeed. So we use the unchecked cast here, because we know that the value is assignable to R.
49 | return (R) method.invoke(implementation, args);
50 | } catch (Exception e) {
51 | throw new MethodInvocationException(invocationDescriptor.getMethodName(), namespace, e);
52 | }
53 | }
54 |
55 | /**
56 | * We don't know yet which method is the best match, because there are multiple ways to arrange the arguments.
57 | * so this method serves to find the best match for when a normal match cannot be done.
58 | * this can get pretty computationally expensive, so this should only be done when an initial lookup fails.
59 | * @param methods The methods to search through
60 | * @param methodName The name of the method to find
61 | * @param argTypes The argument types of the method to find
62 | * @return The method if found, null otherwise
63 | */
64 | private Method findCompatibleDespiteSignature(Method[] methods, String methodName, Class>[] argTypes) {
65 | for (Method method : methods) {
66 | if (!method.getName().equals(methodName) || argTypes.length < method.getParameterCount()) {
67 | continue;
68 | }
69 |
70 | // check if the methods match, or if the last method is an optional array then if the other types match
71 | if (methodMatchesArguments(method, argTypes)) {
72 | return method;
73 | }
74 | }
75 | return null;
76 | }
77 |
78 | private boolean methodMatchesArguments(Method method, Class>[] argTypes) {
79 | for (int i = 0; i < method.getParameterCount(); i++) {
80 | if (i == method.getParameterCount() - 1 && method.getParameterTypes()[i].isArray()) {
81 | // if the last parameter is an array, check if the other parameters match
82 | Class> arrayType = ArgumentMapper.ensureBoxedClass(method.getParameterTypes()[i].getComponentType());
83 |
84 | Class> argType = argTypes[argTypes.length - 1];
85 | Class> typeOfLastValue = ArgumentMapper.ensureBoxedClass(argType.isArray() ? argType.getComponentType() : argType);
86 |
87 | // are all the other parameters assignable to the array type?
88 | if (!arrayType.isAssignableFrom(typeOfLastValue)) {
89 | return false;
90 | }
91 | } else if (!ArgumentMapper.ensureBoxedClass(method.getParameterTypes()[i]).isAssignableFrom(argTypes[i])) {
92 | return false;
93 | }
94 | }
95 | return true;
96 | }
97 |
98 | public Object getImplementation() {
99 | return implementation;
100 | }
101 |
102 | public String getNamespace() {
103 | return namespace;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/meteor-jedis/src/test/java/dev/pixelib/meteor/transport/redis/RedisPacketListenerTest.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.transport.redis;
2 |
3 | import com.github.fppt.jedismock.RedisServer;
4 | import dev.pixelib.meteor.base.interfaces.SubscriptionHandler;
5 | import org.junit.jupiter.api.Disabled;
6 | import org.junit.jupiter.api.Test;
7 | import org.junit.jupiter.api.Timeout;
8 | import org.junit.jupiter.api.extension.ExtendWith;
9 | import org.mockito.Mock;
10 | import org.mockito.junit.jupiter.MockitoExtension;
11 | import redis.clients.jedis.JedisPool;
12 |
13 | import java.util.Base64;
14 | import java.util.Collection;
15 | import java.util.logging.Logger;
16 |
17 | import static org.junit.jupiter.api.Assertions.*;
18 | import static org.mockito.Mockito.*;
19 |
20 | @ExtendWith(MockitoExtension.class)
21 | class RedisPacketListenerTest {
22 |
23 | @Mock
24 | StringMessageBroker subscriptionHandler;
25 |
26 | @Test
27 | void onMessage_withValidChannel() throws Exception {
28 | String topic = "test";
29 | String expected = "message";
30 |
31 | RedisPacketListener redisPacketListener = new RedisPacketListener(subscriptionHandler, topic, Logger.getAnonymousLogger());
32 |
33 | redisPacketListener.onMessage(topic, expected);
34 | verify(subscriptionHandler, times(1)).onRedisMessage(expected);
35 | verify(subscriptionHandler).onRedisMessage(argThat(argument -> {
36 | assertEquals(expected,argument);
37 | return true;
38 | }));
39 | }
40 |
41 | @Test
42 | void onMessage_throwException() throws Exception {
43 | String topic = "test";
44 | String expected = "message";
45 |
46 | StringMessageBroker handler = new StringMessageBroker() {
47 | @Override
48 | public boolean onRedisMessage(String message) throws Exception {
49 | throw new NullPointerException();
50 | }
51 | };
52 |
53 | StringMessageBroker handlerSub = spy(handler);
54 | RedisPacketListener redisPacketListener = new RedisPacketListener(handlerSub, topic, Logger.getAnonymousLogger());
55 |
56 | redisPacketListener.onMessage(topic, expected);
57 | verify(handlerSub, times(1)).onRedisMessage(expected);
58 | verify(handlerSub).onRedisMessage(argThat(argument -> {
59 | assertEquals(expected,argument);
60 | return true;
61 | }));
62 | }
63 |
64 | @Test
65 | void onMessage_withUnKnownChannel() throws Exception {
66 | String topic = "test";
67 | String message = "message";
68 |
69 | RedisPacketListener redisPacketListener = new RedisPacketListener(subscriptionHandler, topic, Logger.getAnonymousLogger());
70 |
71 | assertThrows(NullPointerException.class, () -> redisPacketListener.onMessage("fake-test", message));
72 | }
73 |
74 | @Test
75 | @Timeout(10)
76 | @Disabled
77 | void subscribe_success() throws Exception{
78 | String topic = "test";
79 | String newTopic = "newTopic";
80 |
81 | RedisServer server = RedisServer.newRedisServer().start();
82 |
83 | JedisPool jedisPool = new JedisPool(server.getHost(), server.getBindPort());
84 |
85 |
86 | RedisPacketListener redisPacketListener = new RedisPacketListener(subscriptionHandler, topic, Logger.getAnonymousLogger());
87 |
88 | Thread runner = new Thread(() -> {
89 | jedisPool.getResource().subscribe(redisPacketListener, topic);
90 | });
91 |
92 | runner.start();
93 |
94 | while(!redisPacketListener.isSubscribed()) {
95 | Thread.sleep(20);
96 | }
97 |
98 | redisPacketListener.subscribe(newTopic, subscriptionHandler);
99 |
100 | assertTrue(redisPacketListener.getCustomSubscribedChannels().contains(newTopic));
101 |
102 | redisPacketListener.stop();
103 |
104 | while(redisPacketListener.isSubscribed()) {
105 | Thread.sleep(20);
106 | }
107 |
108 | jedisPool.close();
109 | server.stop();
110 |
111 |
112 | assertEquals(0, redisPacketListener.getSubscribedChannels());
113 | }
114 |
115 | @Test
116 | @Timeout(10)
117 | @Disabled
118 | void stop_success() throws Exception{
119 | String topic = "test";
120 |
121 | RedisServer server = RedisServer.newRedisServer().start();
122 |
123 | JedisPool jedisPool = new JedisPool(server.getHost(), server.getBindPort());
124 |
125 |
126 | RedisPacketListener redisPacketListener = new RedisPacketListener(subscriptionHandler, topic, Logger.getAnonymousLogger());
127 |
128 | Thread runner = new Thread(() -> {
129 | jedisPool.getResource().subscribe(redisPacketListener, topic);
130 | });
131 |
132 | runner.start();
133 |
134 | while(!redisPacketListener.isSubscribed()) {
135 | Thread.sleep(20);
136 | }
137 |
138 | redisPacketListener.stop();
139 | jedisPool.close();
140 | server.stop();
141 |
142 | assertEquals(0, redisPacketListener.getSubscribedChannels());
143 | }
144 |
145 | @Test
146 | void getCustomSubscribedChannels_success() {
147 | String topic = "test";
148 |
149 | RedisPacketListener redisPacketListener = new RedisPacketListener(subscriptionHandler, topic, Logger.getAnonymousLogger());
150 |
151 | Collection result = redisPacketListener.getCustomSubscribedChannels();
152 |
153 | assertNotNull(result);
154 | assertEquals(1, result.size());
155 | assertTrue(result.contains(topic));
156 | }
157 | }
--------------------------------------------------------------------------------
/meteor-core/src/main/java/dev/pixelib/meteor/core/Meteor.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core;
2 |
3 | import dev.pixelib.meteor.base.RpcOptions;
4 | import dev.pixelib.meteor.base.RpcSerializer;
5 | import dev.pixelib.meteor.base.RpcTransport;
6 | import dev.pixelib.meteor.base.defaults.GsonSerializer;
7 | import dev.pixelib.meteor.core.proxy.MeteorMock;
8 | import dev.pixelib.meteor.core.proxy.ProxyInvocHandler;
9 | import dev.pixelib.meteor.core.trackers.IncomingInvocationTracker;
10 | import dev.pixelib.meteor.core.trackers.OutgoingInvocationTracker;
11 | import dev.pixelib.meteor.core.transport.TransportHandler;
12 |
13 | import java.io.IOException;
14 | import java.lang.reflect.Proxy;
15 | import java.util.Timer;
16 |
17 | public class Meteor {
18 |
19 | private final RpcOptions options;
20 |
21 | // Timer for scheduling timeouts and retries
22 | private final Timer timer = new Timer();
23 |
24 | private final OutgoingInvocationTracker outgoingInvocationTracker;
25 | private final IncomingInvocationTracker incomingInvocationTracker;
26 | private final TransportHandler transportHandler;
27 |
28 | /**
29 | * @param options A preconfigured RpcOptions object.
30 | * @param serializer The serializer to use for serializing and deserializing objects.
31 | * @param transport The transport to use for sending and receiving data.
32 | */
33 | public Meteor(RpcTransport transport, RpcOptions options, RpcSerializer serializer) {
34 | this.options = options;
35 |
36 | outgoingInvocationTracker = new OutgoingInvocationTracker(transport, serializer, options, timer);
37 | incomingInvocationTracker = new IncomingInvocationTracker();
38 | transportHandler = new TransportHandler(serializer, transport, incomingInvocationTracker, outgoingInvocationTracker, options.getExecutorThreads());
39 | }
40 |
41 | /**
42 | * @param serializer The serializer to use for serializing and deserializing objects.
43 | * @param transport The transport to use for sending and receiving data.
44 | */
45 | public Meteor(RpcTransport transport, RpcSerializer serializer) {
46 | this(transport, new RpcOptions(), serializer);
47 | }
48 |
49 | /**
50 | * @param options A preconfigured RpcOptions object.
51 | * @param transport The transport to use for sending and receiving data.
52 | */
53 | public Meteor(RpcTransport transport, RpcOptions options) {
54 | this(transport, options, new GsonSerializer());
55 | }
56 |
57 | /**
58 | * Use default GsonSerializer and options.
59 | *
60 | * @param transport The transport to use for sending and receiving data.
61 | */
62 | public Meteor(RpcTransport transport) {
63 | this(transport, new RpcOptions(), new GsonSerializer());
64 | }
65 |
66 | /**
67 | * @return Get a mutable reference to the options.
68 | */
69 | public RpcOptions getOptions() {
70 | return options;
71 | }
72 |
73 | /**
74 | * Register a procedure without a namespace.
75 | *
76 | * @param procedure The interface to register as a procedure.
77 | * @param The type of the interface.
78 | * @return A proxy object that implements the given interface.
79 | */
80 | public T registerProcedure(Class procedure) {
81 | return registerProcedure(procedure, null);
82 | }
83 |
84 | /**
85 | * Register a procedure with a namespace. Invocations will only be mapped on implementations with the same namespace.
86 | *
87 | * @param procedure The interface to register as a procedure.
88 | * @param name The name of the procedure.
89 | * @param The type of the interface.
90 | * @return A proxy object that implements the given interface.
91 | */
92 | public T registerProcedure(Class procedure, String name) {
93 | if (!procedure.isInterface()) {
94 | throw new IllegalArgumentException("Procedure must be an interface");
95 | }
96 |
97 | return procedure.cast(Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{procedure, MeteorMock.class}, new ProxyInvocHandler(outgoingInvocationTracker, name)));
98 | }
99 |
100 | /**
101 | * @param target The object to check.
102 | * @return Whether the given object is a proxy object.
103 | */
104 | public static boolean isRpc(Object target) {
105 | return Proxy.isProxyClass(target.getClass());
106 | }
107 |
108 | /**
109 | * @param target The object to check.
110 | * @return Whether the given object is a proxy object created by meteor.
111 | */
112 | public static boolean isMeteorProxy(Object target) {
113 | return target instanceof MeteorMock;
114 | }
115 |
116 | /**
117 | * Received remote procedure calls will be dispatched to implementations registered with this method.
118 | * The implementation will be registered under all interfaces implemented by the object, and under the given namespace.
119 | *
120 | * @param implementation The object to register as an implementation.
121 | * @param namespace The namespace to register the implementation under.
122 | */
123 | public void registerImplementation(Object implementation, String namespace) {
124 | incomingInvocationTracker.registerImplementation(implementation, namespace);
125 | }
126 |
127 | /**
128 | * Received remote procedure calls will be dispatched to implementations registered with this method.
129 | * The implementation will be registered under all interfaces implemented by the object, and must be called without a namespace.
130 | *
131 | * @param implementation The object to register as an implementation.
132 | */
133 | public void registerImplementation(Object implementation) {
134 | registerImplementation(implementation, null);
135 | }
136 |
137 | /**
138 | * Gracefully shutdown the meteor instance.
139 | */
140 | public void stop() throws IOException {
141 | transportHandler.close();
142 | timer.cancel();
143 | }
144 |
145 | }
146 |
--------------------------------------------------------------------------------
/meteor-core/src/main/java/dev/pixelib/meteor/core/utils/ArgumentMapper.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.utils;
2 |
3 | import java.lang.reflect.Array;
4 | import java.lang.reflect.Method;
5 | import java.util.Map;
6 | import java.util.Objects;
7 |
8 | public class ArgumentMapper {
9 |
10 | /**
11 | * Map of primitive classes to their boxed counterparts.
12 | * This is because they aren't strictly the same, so one cannot construct an array of primitives despite
13 | * the method signature declaring it as such.
14 | */
15 | private static final Map, Class>> PRIMITIVE_TO_BOXED = Map.of(
16 | boolean.class, Boolean.class,
17 | byte.class, Byte.class,
18 | char.class, Character.class,
19 | double.class, Double.class,
20 | float.class, Float.class,
21 | int.class, Integer.class,
22 | long.class, Long.class,
23 | short.class, Short.class,
24 | void.class, Void.class
25 | );
26 |
27 | public static Class> ensureBoxedClass(Class> primitiveClass) {
28 | return PRIMITIVE_TO_BOXED.getOrDefault(primitiveClass, primitiveClass);
29 | }
30 |
31 | public static Class> resolvePrimitive(final String className) {
32 | Objects.requireNonNull(className, "className cannot be null");
33 | switch (className) {
34 | case "boolean":
35 | return boolean.class;
36 | case "byte":
37 | return byte.class;
38 | case "short":
39 | return short.class;
40 | case "int":
41 | return int.class;
42 | case "long":
43 | return long.class;
44 | case "float":
45 | return float.class;
46 | case "double":
47 | return double.class;
48 | case "char":
49 | return char.class;
50 | case "void":
51 | return void.class;
52 | default:
53 | String fqn = className.contains(".") ? className : "java.lang.".concat(className);
54 | try {
55 | return Class.forName(fqn);
56 | } catch (ClassNotFoundException ex) {
57 | throw new IllegalArgumentException("Class not found: " + fqn);
58 | }
59 | }
60 | }
61 |
62 | /**
63 | * Method arguments cannot be mapped one-to-one to an invocation, because the actual signatures
64 | * may differ from the declared signatures (this can happen with optional arrays, for example).
65 | * Luckily, arrays can only be declared as the last argument and there can only be one array,
66 | * so we can use this to our advantage to simply overflown everything after l-1 into the array, and using that
67 | * as the final argument.
68 | */
69 | public static Object[] overflowArguments(Method method, Object[] allArguments) {
70 | Object[] output = new Object[method.getParameterCount()];
71 |
72 | // is it an empty array? if so, return an empty array
73 | if (allArguments.length == 0) {
74 | return output;
75 | }
76 |
77 | for (int i = 0; i < method.getParameterCount() - 1; i++) {
78 | output[i] = allArguments[i];
79 | }
80 |
81 | Class> lastParameterType = method.getParameterTypes()[method.getParameterCount() - 1];
82 | if (lastParameterType.isArray()) {
83 | Class> componentType = lastParameterType.getComponentType();
84 |
85 | int length = allArguments.length - method.getParameterCount() + 1;
86 | Object array = Array.newInstance(componentType, length);
87 |
88 | for (int i = 0; i < length; i++) {
89 | Object argument = allArguments[method.getParameterCount() - 1 + i];
90 |
91 | // is argument also an array? of so, then check if the component types match
92 | if (argument != null && argument.getClass().isArray()) {
93 | Class> argumentComponentType = argument.getClass().getComponentType();
94 | if (componentType.isAssignableFrom(argumentComponentType)) {
95 | // loop over argument and push it onto the array
96 | for (int j = 0; j < Array.getLength(argument); j++) {
97 | // is the array big enough? if not, resize it
98 | if (i + j >= length) {
99 | Object newArray = Array.newInstance(componentType, i + j + 1);
100 | System.arraycopy(array, 0, newArray, 0, Array.getLength(array));
101 | array = newArray;
102 | }
103 | Array.set(array, i + j, Array.get(argument, j));
104 | }
105 | } else {
106 | throw new RuntimeException("Argument type mismatch. " + componentType + " expected, got " + argumentComponentType + " instead for argument " + i + ".");
107 | }
108 | } else {
109 |
110 | if (componentType.isPrimitive()) {
111 | Class> wrapperType = PRIMITIVE_TO_BOXED.get(componentType);
112 | if (wrapperType.isInstance(argument)) {
113 | Array.set(array, i, argument);
114 | } else {
115 | // ignore nulls, they are fine
116 | if (argument != null)
117 | throw new RuntimeException("Argument type mismatch. " + wrapperType + " expected, got " + argument.getClass() + " instead for argument " + i + ".");
118 | }
119 | } else {
120 | Array.set(array, i, argument);
121 | }
122 | }
123 | }
124 |
125 | output[output.length - 1] = array;
126 | } else {
127 | output[output.length - 1] = allArguments[allArguments.length - 1];
128 | }
129 |
130 | if (output.length != method.getParameterCount()) {
131 | throw new RuntimeException("Overflowing arguments failed! Expected " + method.getParameterCount() + " arguments, got " + output.length + " arguments.");
132 | }
133 |
134 | return output;
135 | }
136 |
137 |
138 | }
139 |
--------------------------------------------------------------------------------
/meteor-core/src/test/java/dev/pixelib/meteor/core/utils/ArgumentMapperTest.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.utils;
2 |
3 | import dev.pixelib.meteor.core.Meteor;
4 | import org.junit.jupiter.api.Assertions;
5 | import org.junit.jupiter.api.Test;
6 | import org.junit.jupiter.params.ParameterizedTest;
7 | import org.junit.jupiter.params.provider.Arguments;
8 | import org.junit.jupiter.params.provider.MethodSource;
9 | import org.junit.jupiter.params.provider.NullAndEmptySource;
10 | import org.junit.jupiter.params.provider.ValueSource;
11 |
12 | import java.lang.reflect.Method;
13 | import java.util.stream.Stream;
14 |
15 | import static org.junit.jupiter.api.Assertions.*;
16 |
17 | class ArgumentMapperTest {
18 |
19 | @ParameterizedTest
20 | @MethodSource
21 | void ensureBoxedClass_success(Class> primitive, Class> boxed) {
22 | Class> result = ArgumentMapper.ensureBoxedClass(primitive);
23 |
24 | assertEquals(boxed, result);
25 | }
26 |
27 | static Stream ensureBoxedClass_success() {
28 | return Stream.of(
29 | Arguments.of(byte.class, Byte.class),
30 | Arguments.of(boolean.class, Boolean.class),
31 | Arguments.of(void.class, Void.class),
32 | Arguments.of(double.class, Double.class),
33 | Arguments.of(char.class, Character.class)
34 | );
35 | }
36 |
37 | @Test
38 | void ensureBoxedClass_withNull() {
39 | assertThrowsExactly(NullPointerException.class, () -> {
40 | ArgumentMapper.ensureBoxedClass(null);
41 | });
42 | }
43 |
44 | @Test
45 | void ensureBoxedClass_withNonPrimitiveClass() {
46 | Class> result = ArgumentMapper.ensureBoxedClass(Meteor.class);
47 |
48 | assertEquals(Meteor.class, result);
49 | }
50 |
51 |
52 | @ParameterizedTest
53 | @ValueSource(strings = {"boolean", "byte", "short", "int", "long", "float", "double", "char", "void"})
54 | void testResolvePrimitive_success(String className) {
55 | Class> result = ArgumentMapper.resolvePrimitive(className);
56 | assertNotNull(result);
57 | assertTrue(result.isPrimitive());
58 | }
59 |
60 | @Test
61 | void testResolvePrimitive_withFullQualifiedName() {
62 | Class> result = ArgumentMapper.resolvePrimitive("java.lang.String");
63 | assertNotNull(result);
64 | assertEquals(String.class, result);
65 | }
66 |
67 | @Test
68 | void testResolvePrimitive_withSimpleClassName() {
69 | Class> result = ArgumentMapper.resolvePrimitive("String");
70 | assertNotNull(result);
71 | assertEquals(String.class, result);
72 | }
73 |
74 | @Test
75 | void testResolvePrimitive_classNotFound() {
76 | Exception exception = assertThrows(IllegalArgumentException.class, () -> {
77 | ArgumentMapper.resolvePrimitive("UnknownClass");
78 | });
79 | assertTrue(exception.getMessage().contains("Class not found: java.lang.UnknownClass"));
80 | }
81 |
82 | @Test
83 | void testResolvePrimitive_nullValue() {
84 | Exception exception = assertThrows(NullPointerException.class, () -> {
85 | ArgumentMapper.resolvePrimitive(null);
86 | });
87 | assertEquals("className cannot be null", exception.getMessage());
88 | }
89 |
90 |
91 | static class Example {
92 | private void singleParamMethod(Integer integer) { }
93 | private void multipleParamsMethod(Integer integer, String str, Double dd) { }
94 | private void oneArrayMethod(int[] a) { }
95 | private void optionalArrayMethod(int a, int... b) {}
96 | private void sampleMethod(String a, Integer... b) {}
97 | }
98 |
99 | private static Stream provideArgumentsForTest() throws NoSuchMethodException {
100 | Method singleParamMethod = Example.class.getDeclaredMethod("singleParamMethod", Integer.class);
101 | Method multiParamsMethod = Example.class.getDeclaredMethod("multipleParamsMethod", Integer.class, String.class, Double.class);
102 | Method oneArrayMethod = Example.class.getDeclaredMethod("oneArrayMethod", int[].class);
103 | Method optionalArrayMethod = Example.class.getDeclaredMethod("optionalArrayMethod", int.class, int[].class);
104 |
105 | return Stream.of(
106 | Arguments.of(singleParamMethod, new Object[]{1}, new Object[]{1}),
107 | Arguments.of(multiParamsMethod, new Object[]{1, "test", 2.0}, new Object[]{1, "test", 2.0}),
108 | Arguments.of(singleParamMethod, new Object[]{null}, new Object[]{null}),
109 | Arguments.of(multiParamsMethod, new Object[]{1, null, 2.0}, new Object[]{1, null, 2.0}),
110 | Arguments.of(oneArrayMethod, new Object[]{1, 2, 3}, new Object[]{new int[]{1, 2, 3}}),
111 | Arguments.of(optionalArrayMethod, new Object[]{1, 2, 3, 4}, new Object[]{1, new int[]{2, 3, 4}})
112 |
113 | );
114 | }
115 |
116 | @ParameterizedTest
117 | @MethodSource("provideArgumentsForTest")
118 | void testOverflowArguments(Method method, Object[] allArguments, Object[] expected) {
119 | Object[] output = ArgumentMapper.overflowArguments(method, allArguments);
120 |
121 | assertArrayEquals(expected, output);
122 | }
123 |
124 | @ParameterizedTest
125 | @NullAndEmptySource
126 | void testOverflowArguments_WithEmptyOrNullExceptions(Object[] allArguments) {
127 | assertThrows(NullPointerException.class, () -> ArgumentMapper.overflowArguments(null, allArguments));
128 | }
129 |
130 | @Test
131 | void testOverflowArguments() throws NoSuchMethodException {
132 | Method method = Example.class.getDeclaredMethod("sampleMethod", String.class, Integer[].class);
133 | final Object[] allArguments = new Object[] { "test", new Integer[] {1, 2, 3}, new Integer[] {4, 5, 6}};
134 |
135 | //Valid path
136 | Assertions.assertDoesNotThrow(() -> ArgumentMapper.overflowArguments(method, allArguments));
137 |
138 | //Invalid path - Type mismatch
139 | Object[] allArguments2 = new Object[] { "test", new Boolean[] {true, false}, new Integer[] {4, 5, 6}};
140 | Assertions.assertThrows(RuntimeException.class,
141 | () -> ArgumentMapper.overflowArguments(method, allArguments2),
142 | "Argument type mismatch. java.lang.Integer expected, got java.lang.Boolean instead for argument 0.");
143 | }
144 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Meteor   
6 | > A general-purpose Java RPC library that plugs in like magic
7 |
8 | Meteor is designed to fill the (physical) gap between application instances and remote services where applications need to interface with a service that may be available locally or be provided by a remote JVM instance.
9 | It allows you to write your application against your interface as if it's local code and either supply the implementation locally (like you would with any other interface) or tunnel it through a transport layer (like Redis), without needing to design API specifications or write any networking logic.
10 |
11 |
12 |
13 |
14 |
15 | > *Note that this diagram omits certain parts, like scheduling/threading and abstracted serialization layers to make it more readable.*
16 |
17 | # Installation
18 | Meteor comes by default with a local loopback transport.
19 | Meteor is available on Maven Central, and can be installed by adding the following dependency to your `pom.xml` file:
20 | ```xml
21 |
22 | dev.pixelib.meteor
23 | meteor-core
24 | 1.0.4
25 |
26 | ```
27 |
28 | ## Transports
29 | For the time being, only Jedis transport layer is supported, but more transports are planned for the future.
30 | You can install the Redis transport by adding the following dependency to your `pom.xml` file:
31 | ```xml
32 |
33 | dev.pixelib.meteor.transport
34 | meteor-jedis
35 | 1.0.4
36 |
37 | ```
38 |
39 | # Usage
40 | Let's say that you have an interface like this;
41 | ```java
42 | public interface Scoreboard {
43 | int getScoreForPlayer(String player);
44 | void setScoreForPlayer(String player, int score);
45 | Map getAllScores();
46 | }
47 | ```
48 |
49 | with the following normal implementation
50 | ```java
51 | public class ScoreboardImplementation implements Scoreboard {
52 |
53 | private final Map scores = new HashMap<>();
54 |
55 | @Override
56 | public int getScoreForPlayer(String player) {
57 | return scores.getOrDefault(player, 0);
58 | }
59 |
60 | @Override
61 | public void setScoreForPlayer(String player, int score) {
62 | scores.put(player, score);
63 | }
64 |
65 | @Override
66 | public Map getAllScores() {
67 | return scores;
68 | }
69 | }
70 | ```
71 |
72 | Then you can use your own implementation instance within your application, or share it with external applications by registering it as an implementation, for this example, we'll give it a namespace so we can have multiple scoreboards for each game mode.
73 |
74 | ```java
75 | Meteor meteor = new Meteor(new RedisTransport("localhost", 6379, "scoreboard-sync"));
76 | meteor.registerImplementation(new ScoreboardImplementation(), "parkour-leaderboard");
77 | ```
78 |
79 | and we can obtain an instance of this scoreboard from any other process by requesting a procedure class
80 | ```java
81 | Meteor meteor = new Meteor(new RedisTransport("localhost", 6379, "scoreboard-sync"));
82 | Scoreboard parkourScoreboard = meteor.registerProcedure(Scoreboard.class, "parkour-leaderboard");
83 | ```
84 |
85 | and that's it! `parkourScoreboard` is a dynamically generated implementation of the Scoreboard class, mapping to our `ScoreboardImplementation` running in the other process. All methods are blocking and function *exactly* like they would if you're calling them on a local instance. This allows you to run a single instance if you want, or easily scale your code across multiple processes without having to worry about any of the details.
86 |
87 | **[View example code with a local loopback](https://github.com/pixelib/Meteor/blob/main/examples/src/main/java/dev/pixelib/meteor/sender/ScoreboardExample.java)**
88 |
89 | # Parameters
90 | - *RpcTransport* transport - The transport to use for this Meteor instance (see below, also open to own implementations)
91 | - *RpcSerializer* serializer - The serializer to use for this Meteor instance (Defaults to a Gson based generic, open to own implementations)
92 | - *RpcOptions* options
93 | - *int* threadPoolSize - The size of the thread pool to use for invocations (defaults to 1)
94 | - *int* invocationTimeout - The timeout for invocations (defaults to 30 seconds)
95 | - *ClassLoader* classLoader - The classloader to use for dynamically generated classes (defaults to the current thread's context classloader)
96 |
97 | # Transport Options
98 | Current official transport options:
99 | - `meteor-redis` Which is a Jedis-based Redis transport
100 | - `loopback` Which is a local loopback transport (for testing)
101 |
102 | # Performance
103 | Meteor is designed to be as fast as possible, and the overhead incurred by Meteor is minimal.
104 | A full performance analysis can be found [here](PERFORMANCE.md)
105 |
106 | # Design considerations
107 | ### To queue or not to queue
108 | The library itself is un opinionated about transport and thus execution.
109 | It's up to the specific transport implementation to decide whether a fan-out or queueing strategy is appropriate. The default Redis implementation uses a normal broadcast, meaning that all invocations will be executed on all nodes which provide an implementation (but only the first return value is returned to the invoker function). Other implementations may use a more sophisticated queueing mechanism, allowing implementation instances to only process a subset of the invocations.
110 |
111 | ### Error handling
112 | Invocations leading to an exception on the implementation are considered as not being successful and cause an `InvocationTimedOutException` to be thrown by the invoker after the configured timeout (which defaults to 30 seconds). The invocation will still be successful if another listening implementation succeeds in invoking the method within the timeframe.
113 |
114 | ### Concurrency
115 | Each Meteor instance uses its own internal thread pool for invocations against implementations. Invocations are queued up (in order of invocation time) if an implementation is busy. The thread pool size can be configured through the `RpcOptions`, and defaults to `1`.
116 |
117 | ### To serialize or not to serialize
118 | The library itself is un opinionated about serialization.
119 | GSON gets used by default, but you can use any other serialization library you want, as long as it can serialize and deserialize generic types with another fallback method for unknown types.
120 |
--------------------------------------------------------------------------------
/meteor-core/src/main/java/dev/pixelib/meteor/core/transport/packets/InvocationDescriptor.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.core.transport.packets;
2 |
3 | import dev.pixelib.meteor.base.RpcSerializer;
4 | import dev.pixelib.meteor.core.utils.ArgumentMapper;
5 | import io.netty.buffer.ByteBuf;
6 | import io.netty.buffer.Unpooled;
7 |
8 | import java.nio.charset.Charset;
9 | import java.util.UUID;
10 |
11 | public class InvocationDescriptor {
12 |
13 | /**
14 | * Unique identifier for this invocation, used to match responses to requests.
15 | */
16 | private UUID id;
17 |
18 | /**
19 | * Name of the targeted handler.
20 | * Can be used to address specific instances of an implementation class.
21 | */
22 | private String namespace;
23 |
24 | /**
25 | * Class that declares the method that should be invoked.
26 | */
27 | private Class> declaringClass;
28 |
29 | /**
30 | * Method name that should be invoked (always map against the argTypes due to overloading).
31 | */
32 | private String methodName;
33 |
34 | /**
35 | * Arguments that should be passed to the method, which may contain null values.
36 | */
37 | private Object[] args;
38 |
39 | /**
40 | * Types of the arguments that should be passed to the method.
41 | */
42 | private Class>[] argTypes;
43 |
44 | /**
45 | * Return type of the method.
46 | */
47 | private Class> returnType;
48 |
49 | public InvocationDescriptor(String namespace, Class> declaringClass, String methodName, Object[] args, Class>[] argTypes, Class> returnType) {
50 | this(UUID.randomUUID(), namespace, declaringClass, methodName, args, argTypes, returnType);
51 | }
52 |
53 | public InvocationDescriptor(UUID id, String namespace, Class> declaringClass, String methodName, Object[] args, Class>[] argTypes, Class> returnType) {
54 | this.id = id;
55 | this.namespace = namespace;
56 | this.declaringClass = declaringClass;
57 | this.methodName = methodName;
58 | this.args = args;
59 | this.argTypes = argTypes;
60 | this.returnType = returnType;
61 | }
62 |
63 | public byte[] toBuffer(RpcSerializer serializer) {
64 | ByteBuf buffer = Unpooled.buffer();
65 | buffer.writeLong(id.getMostSignificantBits());
66 | buffer.writeLong(id.getLeastSignificantBits());
67 |
68 | buffer.writeBoolean(namespace != null);
69 | if (namespace != null) {
70 | buffer.writeInt(namespace.length());
71 | buffer.writeCharSequence(namespace, Charset.defaultCharset());
72 | }
73 |
74 | buffer.writeInt(declaringClass.getName().length());
75 | buffer.writeCharSequence(declaringClass.getName(), Charset.defaultCharset());
76 |
77 | buffer.writeInt(methodName.length());
78 | buffer.writeCharSequence(methodName, Charset.defaultCharset());
79 |
80 | buffer.writeInt(args.length);
81 | for (Object arg : args) {
82 | buffer.writeBoolean(arg != null);
83 | if (arg != null) {
84 | Class> argClass = arg.getClass();
85 | String argClassName = argClass.getName();
86 | buffer.writeInt(argClassName.length());
87 | buffer.writeCharSequence(argClassName, Charset.defaultCharset());
88 |
89 | byte[] serialized = serializer.serialize(arg);
90 | buffer.writeInt(serialized.length);
91 | buffer.writeBytes(serialized);
92 | }
93 | }
94 |
95 | buffer.writeInt(argTypes.length);
96 | for (Class> argType : argTypes) {
97 | buffer.writeBoolean(argType.isPrimitive());
98 | buffer.writeInt(argType.getName().length());
99 | buffer.writeCharSequence(argType.getName(), Charset.defaultCharset());
100 | }
101 |
102 | buffer.writeBoolean(returnType.isPrimitive());
103 | buffer.writeInt(returnType.getName().length());
104 | buffer.writeCharSequence(returnType.getName(), Charset.defaultCharset());
105 | byte[] byteArray = new byte[buffer.readableBytes()];
106 | buffer.readBytes(byteArray);
107 | // release the buffer
108 | buffer.release();
109 | return byteArray;
110 | }
111 |
112 | public static InvocationDescriptor fromBuffer(RpcSerializer customDataSerializer, byte[] raw) throws ClassNotFoundException {
113 | ByteBuf buffer = Unpooled.wrappedBuffer(raw);
114 | UUID id = new UUID(buffer.readLong(), buffer.readLong());
115 |
116 | String namespace = null;
117 | if (buffer.readBoolean()) {
118 | namespace = buffer.readCharSequence(buffer.readInt(), Charset.defaultCharset()).toString();
119 | }
120 |
121 | String declaringClassName = buffer.readCharSequence(buffer.readInt(), Charset.defaultCharset()).toString();
122 | Class> declaringClass = Class.forName(declaringClassName);
123 |
124 | String methodName = buffer.readCharSequence(buffer.readInt(), Charset.defaultCharset()).toString();
125 |
126 | Object[] args = new Object[buffer.readInt()];
127 | for (int i = 0; i < args.length; i++) {
128 | if (buffer.readBoolean()) {
129 | String argClassName = buffer.readCharSequence(buffer.readInt(), Charset.defaultCharset()).toString();
130 | byte[] serialized = new byte[buffer.readInt()];
131 | buffer.readBytes(serialized);
132 | args[i] = customDataSerializer.deserialize(serialized, Class.forName(argClassName));
133 | }
134 | }
135 |
136 | Class>[] argTypes = new Class>[buffer.readInt()];
137 | for (int i = 0; i < argTypes.length; i++) {
138 | boolean isPrimitive = buffer.readBoolean();
139 | String argTypeClassName = buffer.readCharSequence(buffer.readInt(), Charset.defaultCharset()).toString();
140 | if (isPrimitive) {
141 | argTypes[i] = ArgumentMapper.resolvePrimitive(argTypeClassName);
142 | } else {
143 | argTypes[i] = Class.forName(argTypeClassName);
144 | }
145 | }
146 |
147 | boolean isReturnPrimitive = buffer.readBoolean();
148 | String returnTypeName = buffer.readCharSequence(buffer.readInt(), Charset.defaultCharset()).toString();
149 | Class> returnType;
150 | if (isReturnPrimitive) {
151 | returnType = ArgumentMapper.resolvePrimitive(returnTypeName);
152 | } else {
153 | returnType = Class.forName(returnTypeName);
154 | }
155 |
156 | // release the buffer
157 | buffer.release();
158 |
159 | return new InvocationDescriptor(id, namespace, declaringClass, methodName, args, argTypes, returnType);
160 | }
161 |
162 | public String getNamespace() {
163 | return namespace;
164 | }
165 |
166 | public String getMethodName() {
167 | return methodName;
168 | }
169 |
170 | public Object[] getArgs() {
171 | return args;
172 | }
173 |
174 | public Class>[] getArgTypes() {
175 | return argTypes;
176 | }
177 |
178 | public Class> getReturnType() {
179 | return returnType;
180 | }
181 |
182 | public UUID getUniqueInvocationId() {
183 | return id;
184 | }
185 |
186 | public Class> getDeclaringClass() {
187 | return declaringClass;
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | dev.pixelib.meteor
8 | meteor-parent
9 | ${revision}
10 | pom
11 |
12 | Meteor
13 | Meteor is a lightweight, fast and easy to use Java RPC library.
14 | https://github.com/pixelib/Meteor
15 |
16 |
17 |
18 | duckelekuuk
19 | Duco
20 | Duckelekuuk
21 | https://github.com/Duckelekuuk
22 | contact@duckelekuuk.com
23 |
24 |
25 | mindgamesnl
26 | Mats
27 | Mindgamesnl
28 | https://github.com/Duckelekuuk
29 | mats@toetmats.nl
30 |
31 |
32 |
33 |
34 | scm:git:git://github.com/pixelib/Meteor.git
35 | scm:git:ssh://github.com/pixelib/Meteor.git
36 | https://github.com/pixelib/Meteor
37 |
38 |
39 |
40 | meteor-common
41 | meteor-core
42 | examples
43 | meteor-jedis
44 | benchmarks
45 |
46 |
47 |
48 | 1.0.2-localbuild
49 | 17
50 | 17
51 | UTF-8
52 |
53 |
54 |
55 |
56 | GNU General Public License v3.0
57 | https://www.gnu.org/licenses/gpl-3.0.html
58 | repo
59 |
60 |
61 |
62 |
63 |
64 | ossrh
65 | https://s01.oss.sonatype.org/content/repositories/snapshots/
66 |
67 |
68 | ossrh
69 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | org.junit.jupiter
78 | junit-jupiter
79 | 5.10.0
80 |
81 |
82 |
83 | org.mockito
84 | mockito-junit-jupiter
85 | 4.8.1
86 | test
87 |
88 |
89 |
90 | redis.clients
91 | jedis
92 | 5.0.2
93 |
94 |
95 | io.netty
96 | netty-buffer
97 | 4.1.97.Final
98 |
99 |
100 | com.google.code.gson
101 | gson
102 | 2.10.1
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | org.apache.maven.plugins
113 | maven-surefire-plugin
114 | 3.1.2
115 |
116 |
117 | org.apache.maven.plugins
118 | maven-compiler-plugin
119 | 3.11.0
120 |
121 |
122 | org.apache.maven.plugins
123 | maven-resources-plugin
124 | 3.3.1
125 |
126 |
127 | org.apache.maven.plugins
128 | maven-shade-plugin
129 | 3.5.0
130 |
131 |
132 |
133 | org.jacoco
134 | jacoco-maven-plugin
135 | 0.8.9
136 |
137 |
138 |
139 | org.apache.maven.plugins
140 | maven-javadoc-plugin
141 | 3.5.0
142 |
143 |
144 |
145 | org.apache.maven.plugins
146 | maven-source-plugin
147 | 3.3.0
148 |
149 |
150 |
151 | org.apache.maven.plugins
152 | maven-deploy-plugin
153 | 3.1.1
154 |
155 |
156 |
157 | org.codehaus.mojo
158 | flatten-maven-plugin
159 | 1.5.0
160 |
161 |
162 |
163 |
164 |
165 |
166 | org.jacoco
167 | jacoco-maven-plugin
168 |
169 |
170 |
171 | prepare-agent
172 |
173 |
174 |
175 | generate-code-coverage-report
176 | test
177 |
178 | report
179 |
180 |
181 |
182 |
183 |
184 |
185 | maven-javadoc-plugin
186 |
187 | all,-missing
188 |
189 |
190 |
191 | attach-javadocs
192 |
193 | jar
194 |
195 |
196 |
197 |
198 |
199 |
200 | maven-source-plugin
201 |
202 | true
203 |
204 |
205 |
206 | attach-sources
207 |
208 | jar
209 |
210 |
211 |
212 |
213 |
214 |
215 | org.codehaus.mojo
216 | flatten-maven-plugin
217 |
218 | true
219 | resolveCiFriendliesOnly
220 |
221 |
222 |
223 | flatten
224 | process-resources
225 |
226 | flatten
227 |
228 |
229 |
230 | flatten.clean
231 | clean
232 |
233 | clean
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 | release
245 |
246 |
247 |
248 |
249 | maven-gpg-plugin
250 | 3.1.0
251 |
252 |
253 | --pinentry-mode
254 | loopback
255 |
256 |
257 |
258 |
259 | sign-artifacts
260 | verify
261 |
262 | sign
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
--------------------------------------------------------------------------------
/meteor-jedis/src/test/java/dev/pixelib/meteor/transport/redis/RedisTransportTest.java:
--------------------------------------------------------------------------------
1 | package dev.pixelib.meteor.transport.redis;
2 |
3 | import com.github.fppt.jedismock.RedisServer;
4 | import com.github.fppt.jedismock.operations.server.MockExecutor;
5 | import com.github.fppt.jedismock.server.ServiceOptions;
6 | import dev.pixelib.meteor.base.enums.Direction;
7 | import org.junit.jupiter.api.Disabled;
8 | import org.junit.jupiter.api.Test;
9 | import org.opentest4j.AssertionFailedError;
10 | import redis.clients.jedis.JedisPool;
11 |
12 | import java.io.IOException;
13 | import java.util.*;
14 |
15 | import static org.junit.jupiter.api.Assertions.*;
16 |
17 | class RedisTransportTest {
18 |
19 | @Test
20 | @Disabled
21 | void send_validImplementation() throws IOException {
22 | String topic = "test";
23 | String channel = "test_implementation";
24 | String message = "cool_message";
25 |
26 | List assertionErrors = new ArrayList<>();
27 |
28 | RedisServer server = RedisServer.newRedisServer()
29 | .setOptions(ServiceOptions.withInterceptor((state, command, params) -> {
30 | try {
31 | if ("publish".equals(command)) {
32 | // loop over all params and print them
33 | assertEquals(channel, params.get(0).toString(), "Channel name is not correct");
34 | assertEquals(message, new String(Base64.getDecoder().decode(params.get(1).data())), "Message is not correct");
35 | }
36 | } catch (AssertionFailedError e) {
37 | assertionErrors.add(e);
38 | }
39 | return MockExecutor.proceed(state, command, params);
40 | }))
41 | .start();
42 |
43 | RedisTransport transport = new RedisTransport(server.getHost(), server.getBindPort(), topic);
44 | transport.send(Direction.IMPLEMENTATION, message.getBytes());
45 |
46 | transport.close();
47 | server.stop();
48 |
49 | if (!assertionErrors.isEmpty()) {
50 | throw assertionErrors.get(0);
51 | }
52 | }
53 |
54 | @Test
55 | @Disabled
56 | void send_validMethodProxy() throws IOException {
57 | String topic = "test";
58 | String channel = "test_method_proxy";
59 | String message = "cool_message";
60 |
61 | List assertionErrors = new ArrayList<>();
62 |
63 | RedisServer server = RedisServer.newRedisServer()
64 | .setOptions(ServiceOptions.withInterceptor((state, command, params) -> {
65 | try {
66 | if ("publish".equals(command)) {
67 | // loop over all params and print them
68 | assertEquals(channel, params.get(0).toString(), "Channel name is not correct");
69 | assertEquals(message, new String(Base64.getDecoder().decode(params.get(1).data())), "Message is not correct");
70 | }
71 | } catch (AssertionFailedError e) {
72 | assertionErrors.add(e);
73 | }
74 | return MockExecutor.proceed(state, command, params);
75 | }))
76 | .start();
77 |
78 | RedisTransport transport = new RedisTransport(server.getHost(), server.getBindPort(), topic);
79 | transport.send(Direction.METHOD_PROXY, message.getBytes());
80 |
81 | transport.close();
82 | server.stop();
83 |
84 | if (!assertionErrors.isEmpty()) {
85 | throw assertionErrors.get(0);
86 | }
87 | }
88 |
89 | @Test
90 | @Disabled
91 | void subscribe_implementation() throws IOException, InterruptedException {
92 | String topic = "test";
93 | String channel = "test_implementation";
94 |
95 | List assertionErrors = new ArrayList<>();
96 |
97 | RedisServer server = RedisServer.newRedisServer()
98 | .setOptions(ServiceOptions.withInterceptor((state, command, params) -> {
99 | try {
100 | if ("subscribe".equals(command)) {
101 | assertEquals(channel, params.get(0).toString(), "Channel name is not correct");
102 | }
103 | } catch (AssertionFailedError e) {
104 | assertionErrors.add(e);
105 | }
106 | return MockExecutor.proceed(state, command, params);
107 | }))
108 | .start();
109 |
110 | RedisTransport transport = new RedisTransport(server.getHost(), server.getBindPort(), topic);
111 | transport.subscribe(Direction.IMPLEMENTATION, packet -> true);
112 |
113 | transport.close();
114 | server.stop();
115 |
116 | if (!assertionErrors.isEmpty()) {
117 | throw assertionErrors.get(0);
118 | }
119 | }
120 |
121 | @Test
122 | @Disabled
123 | void subscribe_methodProxy() throws IOException {
124 | String topic = "test";
125 | String channel = "test_method_proxy";
126 |
127 | List assertionErrors = new ArrayList<>();
128 |
129 | RedisServer server = RedisServer.newRedisServer()
130 | .setOptions(ServiceOptions.withInterceptor((state, command, params) -> {
131 | try {
132 | if ("subscribe".equals(command)) {
133 | assertEquals(channel, params.get(0).toString(), "Channel name is not correct");
134 | }
135 | } catch (AssertionFailedError e) {
136 | assertionErrors.add(e);
137 | }
138 | return MockExecutor.proceed(state, command, params);
139 | }))
140 | .start();
141 |
142 | RedisTransport transport = new RedisTransport(server.getHost(), server.getBindPort(), topic);
143 | transport.subscribe(Direction.METHOD_PROXY, packet -> true);
144 |
145 | transport.close();
146 | server.stop();
147 |
148 | if (!assertionErrors.isEmpty()) {
149 | throw assertionErrors.get(0);
150 | }
151 | }
152 |
153 | @Test
154 | @Disabled
155 | void subscribe_secondSubscription() throws IOException {
156 | String topic = "test";
157 | String channelProxy = "test_method_proxy";
158 | String channelImpl = "test_implementation";
159 |
160 | Collection subscribedChannels = new HashSet<>();
161 |
162 | RedisServer server = RedisServer.newRedisServer()
163 | .setOptions(ServiceOptions.withInterceptor((state, command, params) -> {
164 | if ("subscribe".equals(command)) {
165 | subscribedChannels.add(params.get(0).toString());
166 | }
167 | return MockExecutor.proceed(state, command, params);
168 | }))
169 | .start();
170 |
171 | RedisTransport transport = new RedisTransport(server.getHost(), server.getBindPort(), topic);
172 | transport.subscribe(Direction.METHOD_PROXY, packet -> true);
173 | transport.subscribe(Direction.IMPLEMENTATION, packet -> true);
174 |
175 | transport.close();
176 | server.stop();
177 |
178 | assertTrue(subscribedChannels.contains(channelProxy));
179 | assertTrue(subscribedChannels.contains(channelImpl));
180 | }
181 |
182 | @Test
183 | void getTopicName_withImplementationDirection() throws IOException {
184 | String topic = "test";
185 | String expected = "test_implementation";
186 |
187 | RedisServer server = RedisServer.newRedisServer().start();
188 | RedisTransport transport = new RedisTransport(server.getHost(), server.getBindPort(), topic);
189 |
190 |
191 | String resultTopicName = transport.getTopicName(Direction.IMPLEMENTATION);
192 | assertEquals(expected, resultTopicName, "Topic name is not correct");
193 |
194 |
195 | transport.close();
196 | server.stop();
197 | }
198 |
199 | @Test
200 | @Disabled
201 | void getTopicName_withMethodProxy() throws IOException {
202 | String topic = "test";
203 | String expected = "test_method_proxy";
204 |
205 | RedisServer server = RedisServer.newRedisServer().start();
206 | RedisTransport transport = new RedisTransport(server.getHost(), server.getBindPort(), topic);
207 |
208 |
209 | String resultTopicName = transport.getTopicName(Direction.METHOD_PROXY);
210 | assertEquals(expected, resultTopicName, "Topic name is not correct");
211 |
212 |
213 | transport.close();
214 | server.stop();
215 | }
216 |
217 |
218 |
219 | @Test
220 | @Disabled
221 | void getTopic_nameWithNull() throws IOException {
222 | String topic = "test";
223 |
224 | RedisServer server = RedisServer.newRedisServer().start();
225 | RedisTransport transport = new RedisTransport(server.getHost(), server.getBindPort(), topic);
226 |
227 |
228 | assertThrowsExactly(NullPointerException.class, () -> {
229 | transport.getTopicName(null);
230 | }, "Method returned a topic name for a null direction");
231 |
232 |
233 | transport.close();
234 | server.stop();
235 | }
236 |
237 | @Test
238 | @Disabled
239 | void close_success() throws IOException {
240 | String topic = "test";
241 |
242 | RedisServer server = RedisServer.newRedisServer().start();
243 |
244 | JedisPool jedisPool = new JedisPool(server.getHost(), server.getBindPort());
245 | RedisTransport transport = new RedisTransport(jedisPool, topic);
246 |
247 | transport.close();
248 | server.stop();
249 |
250 | assertThrowsExactly(IllegalStateException.class, () -> {
251 | transport.send(Direction.IMPLEMENTATION, "test".getBytes());
252 |
253 | }, "Method did not throw an exception when trying to send a message after closing");
254 | assertThrowsExactly(IllegalStateException.class, () -> {
255 | transport.subscribe(Direction.IMPLEMENTATION, (data) -> true);
256 | }, "Method did not throw an exception when trying to send a message after closing");
257 |
258 | assertTrue(jedisPool.isClosed());
259 | }
260 |
261 |
262 | @Test
263 | @Disabled
264 | void close_whenAlreadyClosed() throws IOException {
265 | String topic = "test";
266 |
267 | RedisServer server = RedisServer.newRedisServer().start();
268 |
269 | JedisPool jedisPool = new JedisPool(server.getHost(), server.getBindPort());
270 | RedisTransport transport = new RedisTransport(jedisPool, topic);
271 |
272 | transport.close();
273 | server.stop();
274 |
275 | assertTrue(jedisPool.isClosed());
276 | }
277 |
278 | @Test
279 | @Disabled
280 | void construct_withJedisPool() throws IOException {
281 | String topic = "test";
282 |
283 | RedisServer server = RedisServer.newRedisServer().start();
284 |
285 | JedisPool jedisPool = new JedisPool(server.getHost(), server.getBindPort());
286 | RedisTransport transport = new RedisTransport(jedisPool, topic);
287 |
288 | assertFalse(jedisPool.isClosed());
289 |
290 | transport.close();
291 | server.stop();
292 |
293 | assertTrue(jedisPool.isClosed());
294 | }
295 | @Test
296 | @Disabled
297 | void construct_withUrl() throws IOException {
298 | String topic = "test";
299 |
300 | RedisServer server = RedisServer.newRedisServer().start();
301 |
302 | RedisTransport transport = new RedisTransport("redis://" + server.getHost() + ":" + server.getBindPort(), topic);
303 | assertNotNull(transport);
304 |
305 | transport.close();
306 | server.stop();
307 |
308 | }
309 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------