Skip to content

refactor java worker - add middleware support #652

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<azure.functions.java.core.library.version>1.0.0</azure.functions.java.core.library.version>
<azure.functions.java.core.library.version>1.1.0</azure.functions.java.core.library.version>
<azure.functions.java.library.version>2.0.1</azure.functions.java.library.version>
</properties>
<licenses>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,24 @@ public void addTriggerMetadataSource(Map<String, TypedData> metadata) {
}
}

public void addExecutionContextSource(String invocationId, String funcname, ExecutionTraceContext traceContext, ExecutionRetryContext retryContext) {
otherSources.put(ExecutionContext.class,
new ExecutionContextDataSource(
invocationId,
funcname,
traceContext,
retryContext
)
);
public void addExecutionContextSource(ExecutionContextDataSource executionContextDataSource) {
otherSources.put(ExecutionContext.class,executionContextDataSource);
}

public Optional<BindingData> getDataByName(String name, Type target) {
return this.inputSources.get(name).computeByName(name, target);
DataSource<?> parameterDataSource = this.inputSources.get(name);
if (parameterDataSource == null) {
throw new RuntimeException("Cannot find matched parameter name of customer function, please check if customer function is defined correctly");
}
return parameterDataSource.computeByName(name, target);
}

public Optional<BindingData> getTriggerMetatDataByName(String name, Type target) {
return this.metadataSources.get(name).computeByName(name, target);
DataSource<?> metadataDataSource = this.metadataSources.get(name);
if (metadataDataSource == null) {
throw new RuntimeException("Cannot find matched @BindingName of customer function, please check if customer function is defined correctly");
}
return metadataDataSource.computeByName(name, target);
}

public Optional<BindingData> getDataByType(Type target) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,74 @@
package com.microsoft.azure.functions.worker.binding;

import com.microsoft.azure.functions.*;
import com.microsoft.azure.functions.internal.spi.middleware.MiddlewareContext;
import com.microsoft.azure.functions.rpc.messages.ParameterBinding;
import com.microsoft.azure.functions.rpc.messages.TypedData;
import com.microsoft.azure.functions.worker.WorkerLogManager;
import com.microsoft.azure.functions.worker.broker.MethodBindInfo;
import com.microsoft.azure.functions.worker.broker.ParamBindInfo;

import java.lang.annotation.Annotation;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Logger;

import com.microsoft.azure.functions.ExecutionContext;
import com.microsoft.azure.functions.worker.WorkerLogManager;
import com.microsoft.azure.functions.TraceContext;
import com.microsoft.azure.functions.RetryContext;
public final class ExecutionContextDataSource extends DataSource<ExecutionContext> implements MiddlewareContext {
private final String invocationId;
private final TraceContext traceContext;
private final RetryContext retryContext;
private final Logger logger;
private final String funcname;
private final BindingDataStore dataStore;
private final MethodBindInfo methodBindInfo;
private final Class<?> containingClass;

/*
Key is the name defined on customer function parameters. For ex:
@HttpTrigger(
name = "req",
methods = {HttpMethod.GET, HttpMethod.POST},
authLevel = AuthorizationLevel.ANONYMOUS)
HttpRequestMessage<Optional<String>> request,
Here name will be the "req".

Value is java.lang.reflect.Parameter type
*/
private final Map<String, Parameter> parameterDefinitions;

// currently the values are only Strings (resolved from grpc String values)
// but planning to support other types in the future
private final Map<String, Object> parameterValues;

// these are parameters provided by the middleware, which will override the host provided parameter values
// currently the values are only Strings, but planning to support other types in the future
private final Map<String, Object> middlewareParameterValues = new HashMap<>();
private Object returnValue;

//TODO: refactor class to have subclass dedicate to middleware to make logics clean
private static final DataOperations<ExecutionContext, Object> EXECONTEXT_DATA_OPERATIONS = new DataOperations<>();
static {
EXECONTEXT_DATA_OPERATIONS.addGenericOperation(ExecutionContext.class, DataOperations::generalAssignment);
}

final class ExecutionContextDataSource extends DataSource<ExecutionContext> implements ExecutionContext {
ExecutionContextDataSource(String invocationId, String funcname, TraceContext traceContext, RetryContext retryContext) {
public ExecutionContextDataSource(String invocationId, TraceContext traceContext, RetryContext retryContext,
String funcname, BindingDataStore dataStore, MethodBindInfo methodBindInfo,
Class<?> containingClass, List<ParameterBinding> parameterBindings){
super(null, null, EXECONTEXT_DATA_OPERATIONS);
this.invocationId = invocationId;
this.traceContext = traceContext;
this.retryContext = retryContext;
this.logger = WorkerLogManager.getInvocationLogger(invocationId);
this.funcname = funcname;
this.dataStore = dataStore;
this.methodBindInfo = methodBindInfo;
this.containingClass = containingClass;
this.parameterDefinitions = getParameterDefinitions(methodBindInfo);
this.parameterValues = resolveParameterValuesForMiddleware(parameterBindings);
this.setValue(this);
}

Expand All @@ -32,15 +86,90 @@ final class ExecutionContextDataSource extends DataSource<ExecutionContext> impl

@Override
public String getFunctionName() { return this.funcname; }

private final String invocationId;
private final TraceContext traceContext;
private final RetryContext retryContext;
private final Logger logger;
private final String funcname;

private static final DataOperations<ExecutionContext, Object> EXECONTEXT_DATA_OPERATIONS = new DataOperations<>();
static {
EXECONTEXT_DATA_OPERATIONS.addGenericOperation(ExecutionContext.class, DataOperations::generalAssignment);
}
public BindingDataStore getDataStore() {
return dataStore;
}

public MethodBindInfo getMethodBindInfo() {
return methodBindInfo;
}

public Class<?> getContainingClass() {
return containingClass;
}

private static Map<String, Parameter> getParameterDefinitions(MethodBindInfo methodBindInfo){
Map<String, Parameter> map = new HashMap<>();
for (ParamBindInfo paramBindInfo : methodBindInfo.getParams()) {
map.put(paramBindInfo.getName(), paramBindInfo.getParameter());
}
return map;
}


//TODO: leverage stream to do the check
@Override
public String getParameterName(String annotationSimpleClassName){
for (Map.Entry<String, Parameter> entry : this.parameterDefinitions.entrySet()){
if (hasAnnotation(entry.getValue(), annotationSimpleClassName)){
return entry.getKey();
}
}
return null;
}

private static boolean hasAnnotation(Parameter parameter, String annotationSimpleClassName){
Annotation[] annotations = parameter.getAnnotations();
for (Annotation annotation : annotations) {
if(annotation.annotationType().getSimpleName().equals(annotationSimpleClassName)){
return true;
}
}
return false;
}

// TODO: Refactor the code in V5 to make resolve arguments logics before middleware invocation.
// for now only supporting String parameter values mapped to String values
private static Map<String, Object> resolveParameterValuesForMiddleware(List<ParameterBinding> parameterBindings){
Map<String, Object> map = new HashMap<>();
for (ParameterBinding parameterBinding : parameterBindings) {
TypedData typedData = parameterBinding.getData();
if (typedData.getDataCase() == TypedData.DataCase.STRING){
map.put(parameterBinding.getName(), typedData.getString());
}
}
return map;
}

@Override
public Object getParameterValue(String name) {
return this.parameterValues.get(name);
}

@Override
public void updateParameterValue(String name, Object value) {
this.middlewareParameterValues.put(name, value);
}

@Override
public Object getReturnValue() {
return this.returnValue;
}

@Override
public void updateReturnValue(Object returnValue) {
this.returnValue = returnValue;
// set the return value that will be sent back to host
this.dataStore.setDataTargetValue(BindingDataStore.RETURN_NAME, this.returnValue);
}

public Optional<BindingData> getBindingData(String paramName, Type paramType) {
Object inputValue = this.middlewareParameterValues.get(paramName);
if (inputValue != null) {
return Optional.of(new BindingData(inputValue));
}else{
return dataStore.getDataByName(paramName, paramType);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ static String getBindingNameAnnotation(Parameter param) {
return new String("");
}

static boolean checkHasImplicitOutput(Parameter parameter) {
static boolean checkImplicitOutput(Parameter parameter) {
Annotation[] annotations = parameter.getAnnotations();
for (Annotation annotation : annotations) {
for (Annotation item : annotation.annotationType().getAnnotations()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,70 +1,28 @@
package com.microsoft.azure.functions.worker.broker;

import java.lang.reflect.*;
import java.util.*;

import com.microsoft.azure.functions.worker.binding.*;
import com.microsoft.azure.functions.worker.description.*;
import com.microsoft.azure.functions.worker.reflect.*;
import com.microsoft.azure.functions.rpc.messages.*;

/**
* Used to executor of arbitrary Java method in any JAR using reflection.
* Thread-Safety: Multiple thread.
*/
public class EnhancedJavaMethodExecutorImpl implements JavaMethodExecutor {
public EnhancedJavaMethodExecutorImpl(FunctionMethodDescriptor descriptor, Map<String, BindingInfo> bindingInfos, ClassLoaderProvider classLoaderProvider)
throws ClassNotFoundException, NoSuchMethodException
{
descriptor.validateMethodInfo();

this.classLoader = classLoaderProvider.createClassLoader();
this.containingClass = getContainingClass(descriptor.getFullClassName());
this.overloadResolver = new ParameterResolver();

for (Method method : this.containingClass.getMethods()) {
if (method.getDeclaringClass().getName().equals(descriptor.getFullClassName()) && method.getName().equals(descriptor.getMethodName())) {
this.overloadResolver.addCandidate(method);
}
}

if (!this.overloadResolver.hasCandidates()) {
throw new NoSuchMethodException("There are no methods named \"" + descriptor.getName() + "\" in class \"" + descriptor.getFullClassName() + "\"");
}

if (this.overloadResolver.hasMultipleCandidates()) {
throw new UnsupportedOperationException("Found more than one function with method name \"" + descriptor.getName() + "\" in class \"" + descriptor.getFullClassName() + "\"");
}

this.bindingDefinitions = new HashMap<>();
private final ClassLoader classLoader;

for (Map.Entry<String, BindingInfo> entry : bindingInfos.entrySet()) {
this.bindingDefinitions.put(entry.getKey(), new BindingDefinition(entry.getKey(), entry.getValue()));
}
public EnhancedJavaMethodExecutorImpl(ClassLoader classLoader) {
this.classLoader = classLoader;
}

public Map<String, BindingDefinition> getBindingDefinitions() { return this.bindingDefinitions; }

public ParameterResolver getOverloadResolver() { return this.overloadResolver; }

public void execute(BindingDataStore dataStore) throws Exception {
public void execute(ExecutionContextDataSource executionContextDataSource) throws Exception {
try {
Thread.currentThread().setContextClassLoader(this.classLoader);
Object retValue = this.overloadResolver.resolve(dataStore)
Object retValue = ParameterResolver.resolveArguments(executionContextDataSource)
.orElseThrow(() -> new NoSuchMethodException("Cannot locate the method signature with the given input"))
.invoke(() -> this.containingClass.newInstance());
dataStore.setDataTargetValue(BindingDataStore.RETURN_NAME, retValue);
.invoke(() -> executionContextDataSource.getContainingClass().newInstance());
executionContextDataSource.updateReturnValue(retValue);
} finally {
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
}
}

private Class<?> getContainingClass(String className) throws ClassNotFoundException {
return Class.forName(className, false, this.classLoader);
}

private final Class<?> containingClass;
private final ClassLoader classLoader;
private final ParameterResolver overloadResolver;
private final Map<String, BindingDefinition> bindingDefinitions;
}

This file was deleted.

Loading