Skip to content

Commit

Permalink
Intial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
supertick committed Dec 9, 2023
1 parent fef8292 commit 975a806
Show file tree
Hide file tree
Showing 15 changed files with 618 additions and 89 deletions.
5 changes: 5 additions & 0 deletions src/main/java/org/myrobotlab/codec/ForeignProcessUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ public class ForeignProcessUtils {
* @return Whether name is a valid FQCN
*/
public static boolean isValidJavaClassName(String name) {
// TODO: this is temporary, until proxy java classes
// are proxied in the same way as other services
if (name.equals("Unknown")) {
return false;
}
return JAVA_FQCN_PATTERN.matcher(name).matches();
}

Expand Down
124 changes: 67 additions & 57 deletions src/main/java/org/myrobotlab/framework/ProxyFactory.java
Original file line number Diff line number Diff line change
@@ -1,77 +1,87 @@
package org.myrobotlab.framework;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodDelegation;
import org.myrobotlab.framework.interfaces.ServiceInterface;
import static net.bytebuddy.matcher.ElementMatchers.any;

import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.stream.Collectors;

import static net.bytebuddy.matcher.ElementMatchers.any;
import org.myrobotlab.framework.interfaces.ServiceInterface;
import org.myrobotlab.logging.LoggerFactory;
import org.slf4j.Logger;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodDelegation;

/**
* ProxyFactory takes a service description via {@link Registration}
* and uses <a href="https://bytebuddy.net/#/">ByteBuddy</a> to generate
* a new class and instance for it, delegating all method calls to a new instance
* of {@link ProxyInterceptor}. The registration must contain at least the
* fully-qualified name of {@link ServiceInterface} in its {@link Registration#interfaces}
* list. If this name is not present, an {@link IllegalArgumentException} is thrown.
* ProxyFactory takes a service description via {@link Registration} and uses
* <a href="https://bytebuddy.net/#/">ByteBuddy</a> to generate a new class and
* instance for it, delegating all method calls to a new instance of
* {@link ProxyInterceptor}. The registration must contain at least the
* fully-qualified name of {@link ServiceInterface} in its
* {@link Registration#interfaces} list. If this name is not present, an
* {@link IllegalArgumentException} is thrown.
*
* @author AutonomicPerfectionist
*/
public class ProxyFactory {

/**
* Creates a proxy class and instantiates it using the given registration.
* If the registration's {@link Registration#interfaces} list does not contain
* the fully-qualified name of {@link ServiceInterface}, an {@link IllegalArgumentException}
* is thrown. The generated proxy uses the name, id, and interfaces present in
* the registration to create the new service. The proxy
* delegates to a new instance of {@link ProxyInterceptor}.
*
* @param registration The information required to generate a new proxy
* @return A newly-instantiated proxy
* @throws IllegalArgumentException if registration's interfaces list
* does not contain ServiceInterface
*/
public static ServiceInterface createProxyService(Registration registration) {
// TODO add caching support
if (!registration.interfaces.contains(ServiceInterface.class.getName())) {
throw new IllegalArgumentException(
"Registration must list at least ServiceInterface in the interfaces list."
);
}
ByteBuddy buddy = new ByteBuddy();
DynamicType.Builder<?> builder = buddy.subclass(Object.class);
List<Class<?>> interfaceClasses = registration.interfaces.stream().map(i -> {
try {
return Class.forName(i);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Unable to load interface " + i + " for registration " + registration, e);
}
}).collect(Collectors.toList());
transient public final static Logger log = LoggerFactory.getLogger(ProxyFactory.class);

/**
* Creates a proxy class and instantiates it using the given registration. If
* the registration's {@link Registration#interfaces} list does not contain
* the fully-qualified name of {@link ServiceInterface}, an
* {@link IllegalArgumentException} is thrown. The generated proxy uses the
* name, id, and interfaces present in the registration to create the new
* service. The proxy delegates to a new instance of {@link ProxyInterceptor}.
*
* @param registration
* The information required to generate a new proxy
* @return A newly-instantiated proxy
* @throws IllegalArgumentException
* if registration's interfaces list does not
* contain
* ServiceInterface
*/
public static ServiceInterface createProxyService(Registration registration) {

builder = builder.implement(interfaceClasses)
.method(any())
.intercept(MethodDelegation
.withDefaultConfiguration()
.to(new ProxyInterceptor(registration.name, registration.id)));
if (registration.interfaces == null) {
log.info("remote did not provide any interfaces, creating minimal getId and getName from registration data");
} else {
// TODO add caching support
if (!registration.interfaces.contains(ServiceInterface.class.getName())) {
throw new IllegalArgumentException("Registration must list at least ServiceInterface in the interfaces list.");
}
}
ByteBuddy buddy = new ByteBuddy();
DynamicType.Builder<?> builder = buddy.subclass(Object.class);
List<Class<?>> interfaceClasses = registration.interfaces.stream().map(i -> {
try {
return Class.forName(i);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Unable to load interface " + i + " for registration " + registration, e);
}
}).collect(Collectors.toList());

builder = builder.implement(interfaceClasses).method(any()).intercept(MethodDelegation.withDefaultConfiguration()
.to(new ProxyInterceptor(registration.name, registration.id, registration.typeKey)));

Class<?> proxyClass = builder.make()
.load(ProxyFactory.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
try {
// We never defined any constructors so the default no-args is available
return (ServiceInterface) proxyClass.getConstructors()[0].newInstance();
Class<?> proxyClass = builder.make().load(ProxyFactory.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
try {
// We never defined any constructors so the default no-args is available
ServiceInterface si = (ServiceInterface) proxyClass.getConstructors()[0].newInstance();
MethodCache cache = MethodCache.getInstance();
cache.cacheMethodEntries(si.getClass());
return si;

} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
// Really shouldn't happen since we have full control over the
// newly-generated class
throw new RuntimeException(e);
}
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
// Really shouldn't happen since we have full control over the
// newly-generated class
throw new RuntimeException(e);
}
}
}
106 changes: 80 additions & 26 deletions src/main/java/org/myrobotlab/framework/ProxyInterceptor.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package org.myrobotlab.framework;

import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import java.lang.reflect.Method;
import java.util.Arrays;

import org.myrobotlab.codec.CodecUtils;
import org.myrobotlab.logging.LoggerFactory;
import org.myrobotlab.service.Runtime;
import org.slf4j.Logger;

import java.lang.reflect.Method;
import java.util.Arrays;
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;

/**
* This class is used internally to intercept
Expand All @@ -24,30 +26,82 @@
*/
public class ProxyInterceptor {

protected static Logger log = LoggerFactory.getLogger(ProxyInterceptor.class);
protected static Logger log = LoggerFactory.getLogger(ProxyInterceptor.class);

public static volatile int timeout = 3000;

/**
* name of remote service
*/
private final String name;

/**
* id of remote instance
*/
private final String id;

private final String typeKey;

public ProxyInterceptor(String name, String id, String typeKey) {
this.name = name;
this.id = id;
this.typeKey = typeKey;
// this.state ?
}

/**
* Name, Id, FullName, TypeKey and list of interfaces
* are all available during registration, these methods
* should not later go out to the client to resolve.
* @return
*/
public String getId() {
return id;
}

public static volatile int timeout = 3000;
private final String name;
/**
* Name is availble at registration, don't need to ask the
* remote service again
* @return
*/
public String getName() {
return name;
}

private final String id;
/**
* Convenience method
* @return
*/
public String getFullName() {
return String.format("%s@%s", name, id);
}

public ProxyInterceptor(String name, String id) {
this.name = name;
this.id = id;
}
/**
* Given on registration, don't need client to be queried for it
* @return
*/
public String getTypeKey() {
return typeKey;
}

/**
* A guess at what might be best
*/
public String toString() {
return CodecUtils.toJson(this);
}

@RuntimeType
public Object intercept(@Origin Method method, @AllArguments Object... args) throws InterruptedException, TimeoutException {
log.debug(
"Executing proxy method {}@{}.{}({})",
name,
id,
method,
((args == null) ? "" : Arrays.toString(args))
);
// Timeout should be more sophisticated for long blocking methods
return Runtime.getInstance().sendBlocking(name + "@" + id, timeout, method.getName(),
(args != null) ? args : new Object[0]);
}
@RuntimeType
public Object intercept(@Origin Method method, @AllArguments Object... args)
throws InterruptedException, TimeoutException {
log.debug(
"Executing proxy method {}@{}.{}({})",
name,
id,
method,
((args == null) ? "" : Arrays.toString(args)));
// Timeout should be more sophisticated for long blocking methods
return Runtime.getInstance().sendBlocking(name + "@" + id, timeout, method.getName(),
(args != null) ? args : new Object[0]);
}
}
13 changes: 11 additions & 2 deletions src/main/java/org/myrobotlab/framework/Registration.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Objects;

import org.myrobotlab.codec.CodecUtils;
import org.myrobotlab.codec.ForeignProcessUtils;
import org.myrobotlab.framework.interfaces.ServiceInterface;
import org.myrobotlab.logging.LoggerFactory;
import org.slf4j.Logger;
Expand Down Expand Up @@ -70,9 +71,17 @@ public Registration(ServiceInterface service) {
this.id = service.getId();
this.name = service.getName();
this.typeKey = service.getTypeKey();
// when this registration is re-broadcasted to remotes it will use this
// When this registration is re-broadcasted to remotes it will use this
// serialization to init state
this.state = CodecUtils.toJson(service);

// FIXME: This switch would not be necessary
// if Java remote services were handled the same way, would not need to do this
if (ForeignProcessUtils.isValidJavaClassName(service.getTypeKey())) {
this.state = CodecUtils.toJson(service);
} else {
this.state = service.toString();
}

// if this is a local registration - need reference to service
this.service = service;
}
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/org/myrobotlab/generics/SlidingWindowList.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.myrobotlab.generics;

import java.util.ArrayList;

public class SlidingWindowList<E> extends ArrayList<E> {
private static final long serialVersionUID = 1L;
private final int maxSize;

public SlidingWindowList(int maxSize) {
this.maxSize = maxSize;
}

@Override
public boolean add(E element) {
boolean added = super.add(element);
if (size() > maxSize) {
removeRange(0, size() - maxSize); // Remove oldest elements if size exceeds maxSize
}
return added;
}

}
Loading

0 comments on commit 975a806

Please sign in to comment.