From 45e5561460f316533e84c014dbaa658681e46e91 Mon Sep 17 00:00:00 2001 From: "Ray J. Greenwell" Date: Fri, 20 Feb 2026 10:23:53 -0800 Subject: [PATCH] InvocationProxy: View a *Provider instance as a *Service. This is used on multi-noded servers so that I can have a service interface for talking to the node in charge of a particular resource, and use the same code path even if that node is the current node. Perhaps this be more smoothly integrated in the future but for now it's a static utility you need to manually set up. --- .../presents/server/InvocationProxy.java | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 core/src/main/java/com/threerings/presents/server/InvocationProxy.java diff --git a/core/src/main/java/com/threerings/presents/server/InvocationProxy.java b/core/src/main/java/com/threerings/presents/server/InvocationProxy.java new file mode 100644 index 000000000..00782afc2 --- /dev/null +++ b/core/src/main/java/com/threerings/presents/server/InvocationProxy.java @@ -0,0 +1,167 @@ +package com.threerings.presents.server; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import com.samskivert.util.MethodFinder; + +import com.threerings.io.ObjectOutputStream; + +import com.threerings.presents.client.InvocationService; +import com.threerings.presents.data.InvocationCodes; +import com.threerings.presents.server.InvocationException; +import com.threerings.presents.server.InvocationProvider; + +import static com.threerings.presents.Log.log; + +/** + * Utility methods for getting an InvocationService proxy for a InvocationProvider. + * This lets you view the methods on your FooProvider on the server through a + * FooService interface. This is used by multi-noded servers that have peer services + * to each other when they want to have one code path for making requests whether it's + * handeled locally or remotely. + */ +public class InvocationProxy +{ + /** If true, create proxies in development mode: validate all arguments are streamable. */ + public static boolean validate = + Boolean.getBoolean("com.threerings.presents.server.InvocationProxy.validate"); + + /** + * Get an InvocationService method for calling services directly on the provided provider + * implementation. + * Note: No checking is done to make sure that the specified provider is in any way related + * to the supplied service provider. Don't screw up! + */ + public static > S getProxy ( + Class serviceClass, final InvocationProvider provider) + { + return getProxy(serviceClass, + new ServiceHandler(serviceClass) { + protected Object invokeMethod (String name, Object[] args) + throws Throwable + { + // make a new arg array including the ClientObject (null) + Object[] pArgs = new Object[args.length + 1]; + System.arraycopy(args, 0, pArgs, 1, args.length); + Method pMethod = methodFinder.findMethod(name, pArgs); + return pMethod.invoke(provider, pArgs); + } + final MethodFinder methodFinder = new MethodFinder(provider.getClass()); + }); + } + + /** + * Get an InvocationService that fails on every request with the specified message. + */ + public static > S getFailureProxy ( + Class serviceClass, final String failureMessage) + { + return getProxy(serviceClass, + new ServiceHandler(serviceClass) { + protected Object invokeMethod (String name, Object[] args) + throws Throwable + { + throw new InvocationException(failureMessage); + } + }); + } + + /** + * Convenience method to construct and cast a proxy. + */ + protected static S getProxy (Class serviceClass, InvocationHandler handler) + { + if (validate) { + final InvocationHandler base = handler; + handler = (proxy, method, args) -> { + for (Object o : args) { + if (!(o instanceof InvocationService.InvocationListener)) { + ensureStreamable(o); + } + } + return base.invoke(proxy, method, args); + }; + } + + return serviceClass.cast(Proxy.newProxyInstance(serviceClass.getClassLoader(), + new Class[] { serviceClass }, handler)); + } + + /** + * Ensure that the args are all streamable. + */ + protected static void ensureStreamable (Object anything) + { + try { + new ObjectOutputStream(new ByteArrayOutputStream()).writeObject(anything); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * A helper class that takes care of finding the InvocationListeners and properly dealing + * with exceptions. + */ + protected static abstract class ServiceHandler + implements InvocationHandler + { + public ServiceHandler (Class> clazz) + { + _serviceClass = clazz; + } + + // from InvocationHandler + public final Object invoke (Object proxy, Method method, Object[] args) + { + // first locate the "primary listener" + InvocationService.InvocationListener listener = null; + for (Object o : args) { + if (o instanceof InvocationService.InvocationListener) { + listener = (InvocationService.InvocationListener)o; + break; + } + } + + try { + return invokeMethod(method.getName(), args); + + } catch (Throwable t) { + if (listener == null) { + log.warning("No listener for uncaught exception in InvocationProxy", + "serviceClass", _serviceClass, "method", method.getName(), t); + // nothing to report back to! + return null; + } + + // see if we can find an InvocationException in the chain + for (Throwable cause = t; cause != null; cause = cause.getCause()) { + if (cause instanceof InvocationException) { + listener.requestFailed(cause.getMessage()); + return null; + } + } + + // unknown error + log.warning("Throwable in InvocationProxy", + "serviceClass", _serviceClass, "method", method.getName(), t); + listener.requestFailed(InvocationCodes.INTERNAL_ERROR); + return null; + } + } + + /** + * Implement to invoke the service method. + */ + protected abstract Object invokeMethod (String name, Object[] args) throws Throwable; + + /** The service class. */ + protected final Class> _serviceClass; + } +}