Skip to content
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

Azure Functions support #16

Merged
merged 1 commit into from
Mar 25, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions azurefunctions/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
id 'java-library'
id 'maven-publish'
}

group 'com.microsoft.durabletask'
version = '0.1.0-SNAPSHOT'
archivesBaseName = 'durabletask-azurefunctions'

def protocVersion = '3.12.0'

dependencies {
api project(':sdk')
implementation group: 'com.microsoft.azure.functions', name: 'azure-functions-java-library', version: '1.4.2'
implementation "com.google.protobuf:protobuf-java:${protocVersion}"
}

publishing {
publications {
mavenPublication(MavenPublication) {
artifactId = archivesBaseName
from components.java
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for
* license information.
*/

package com.microsoft.durabletask.azurefunctions;
cgillum marked this conversation as resolved.
Show resolved Hide resolved
cgillum marked this conversation as resolved.
Show resolved Hide resolved

import com.microsoft.azure.functions.annotation.CustomBinding;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* <p>
* Azure Functions attribute for binding a function parameter to a Durable Task activity input.
* </p><p>
* The following is an example of an activity trigger function that accepts a String input and returns a String output.
* </p>
* <pre>
* {@literal @}FunctionName("SayHello")
* public String sayHello(
* {@literal @}DurableActivityTrigger(name = "name") String name,
* final ExecutionContext context) {
* context.getLogger().info("Saying hello to: " + name);
* return String.format("Hello %s!", name);
* }
* </pre>
*
* @since 2.0.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@CustomBinding(direction = "in", name = "", type = "activityTrigger")
public @interface DurableActivityTrigger {
Copy link
Member

@kaibocai kaibocai Mar 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This annotation may need to add a another method such as has_implicit_output (if we following python pattern, depending on the way we implement the java worker to support return value in detail. )
same apply to trigger orchestrationTrigger

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm cool with that design. 👍🏽

/**
* <p>The name of the activity function.</p>
* <p>If not specified, the function name is used as the name of the activity.</p>
* <p>This property supports binding parameters.</p>
*
* @return The name of the orchestrator function.
*/
String activity() default "";

/**
* The variable name used in function.json.
*
* @return The variable name used in function.json.
*/
String name();

/**
* <p>
* Defines how Functions runtime should treat the parameter value. Possible values are:
* </p>
* <ul>
* <li>"": get the value as a string, and try to deserialize to actual parameter type like POJO</li>
* <li>string: always get the value as a string</li>
* <li>binary: get the value as a binary data, and try to deserialize to actual parameter type byte[]</li>
* </ul>
*
* @return The dataType which will be used by the Functions runtime.
*/
String dataType() default "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.microsoft.durabletask.azurefunctions;

import com.microsoft.azure.functions.HttpRequestMessage;
import com.microsoft.azure.functions.HttpResponseMessage;

import com.microsoft.azure.functions.HttpStatus;
import com.microsoft.durabletask.DurableTaskClient;
import com.microsoft.durabletask.DurableTaskGrpcClient;

import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

/**
* The binding value type for the {@literal @}DurableClientInput parameter.
*/
public class DurableClientContext {
// These fields are populated via GSON deserialization by the Functions Java worker.
private String rpcBaseUrl;
private String taskHubName;
private String requiredQueryStringParameters;

/**
* Gets the name of the client binding's task hub.
*
* @return the name of the client binding's task hub.
*/
public String getTaskHubName() {
return this.taskHubName;
}

/**
* Gets the durable task client associated with the current function invocation.
*
* @return the Durable Task client object associated with the current function invocation.
*/
public DurableTaskClient getClient() {
if (this.rpcBaseUrl == null || this.rpcBaseUrl.length() == 0) {
throw new IllegalStateException("The client context wasn't populated with an RPC base URL!");
}

URL rpcURL;
try {
rpcURL = new URL(this.rpcBaseUrl);
} catch (MalformedURLException ex) {
throw new IllegalStateException("The client context RPC base URL was invalid!", ex);
}

return DurableTaskGrpcClient.newBuilder().forPort(rpcURL.getPort()).build();
}

public HttpResponseMessage createCheckStatusResponse(HttpRequestMessage<?> request, String instanceId) {
// TODO: To better support scenarios involving proxies or application gateways, this
// code should take the X-Forwarded-Host, X-Forwarded-Proto, and Forwarded HTTP
// request headers into consideration and generate the base URL accordingly.
// More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded.
// One potential workaround is to set ASPNETCORE_FORWARDEDHEADERS_ENABLED to true.
String baseUrl = request.getUri().getScheme() + "://" + request.getUri().getAuthority();
String encodedInstanceId;
try {
encodedInstanceId = URLEncoder.encode(instanceId, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException ex) {
throw new IllegalArgumentException("Failed to encode the instance ID: " + instanceId, ex);
}

String instanceStatusURL = baseUrl + "/runtime/webhooks/durabletask/instances/" + encodedInstanceId;

// Construct the response as an HTTP 201 with a JSON object payload
return request.createResponseBuilder(HttpStatus.CREATED)
.header("Location", instanceStatusURL + "?" + this.requiredQueryStringParameters)
.header("Content-Type", "application/json")
.body(new HttpCreateCheckStatusResponse(
instanceId,
instanceStatusURL,
this.requiredQueryStringParameters))
.build();
}

private static class HttpCreateCheckStatusResponse {
// These fields are serialized to JSON
public final String id;
public final String purgeHistoryDeleteUri;
public final String sendEventPostUri;
public final String statusQueryGetUri;
public final String terminatePostUri;

public HttpCreateCheckStatusResponse(
String instanceId,
String instanceStatusURL,
String requiredQueryStringParameters) {
this.id = instanceId;
this.purgeHistoryDeleteUri = instanceStatusURL + "?" + requiredQueryStringParameters;
this.sendEventPostUri = instanceStatusURL + "/raiseEvent/{eventName}?" + requiredQueryStringParameters;
this.statusQueryGetUri = instanceStatusURL + "?" + requiredQueryStringParameters;
this.terminatePostUri = instanceStatusURL + "/terminate?reason={text}&" + requiredQueryStringParameters;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for
* license information.
*/

package com.microsoft.durabletask.azurefunctions;

import com.microsoft.azure.functions.annotation.CustomBinding;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* <p>
* Azure Functions attribute for binding a function parameter to a {@literal @}DurableClientContext object.
* </p><p>
* The following is an example of an HTTP-trigger function that uses this input binding to start a new
* orchestration instance.
* </p>
* <pre>
* {@literal @}FunctionName("StartHelloCities")
* public HttpResponseMessage startHelloCities(
* {@literal @}HttpTrigger(name = "request", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
* {@literal @}DurableClientInput(name = "durableContext") DurableClientContext durableContext,
* final ExecutionContext context) {
*
* DurableTaskClient client = durableContext.getClient();
* String instanceId = client.scheduleNewOrchestrationInstance("HelloCities");
* context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId);
* return durableContext.createCheckStatusResponse(request, instanceId);
* }
* </pre>
*
* @since 2.0.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@CustomBinding(direction = "in", name = "", type = "durableClient")
public @interface DurableClientInput {
/**
* The variable name used in function.json.
*
* @return The variable name used in function.json.
*/
String name();

/**
* <p>
* Defines how Functions runtime should treat the parameter value. Possible values are:
* </p>
* <ul>
* <li>"": get the value as a string, and try to deserialize to actual parameter type like POJO</li>
* <li>string: always get the value as a string</li>
* <li>binary: get the value as a binary data, and try to deserialize to actual parameter type byte[]</li>
* </ul>
*
* @return The dataType which will be used by the Functions runtime.
*/
String dataType() default "";

/**
* <p>
* Optional. The name of the task hub in which the orchestration data lives.
* </p>
* <p>
* If not specified, the task hub name used by this binding will be the value specified in host.json.
* If a task hub name is not configured in host.json and if the function app is running in the
* Azure Functions hosted service, then task hub name is derived from the function app's name.
* Otherwise, a constant value is used for the task hub name.
* </p>
* <p>
* In general, you should <i>not</i> set a value for the task hub name here unless you intend to configure
* the client to interact with orchestrations in another app.
* </p>
*
* @return The task hub name to use for the client.
*/
String taskHub() default "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for
* license information.
*/

package com.microsoft.durabletask.azurefunctions;

import com.microsoft.azure.functions.annotation.CustomBinding;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* <p>
* Azure Functions attribute for binding a function parameter to a Durable Task orchestration request.
* </p><p>
* The following is an example of an orchestrator function that calls three activity functions in sequence.
* </p>
* <pre>
* {@literal @}FunctionName("HelloCities")
* public String helloCitiesOrchestrator(
* {@literal @}DurableOrchestrationTrigger(name = "orchestratorRequestProtoBytes") String orchestratorRequestProtoBytes) {
* return OrchestrationRunner.loadAndRun(orchestratorRequestProtoBytes, ctx -> {
* String result = "";
* result += ctx.callActivity("SayHello", "Tokyo", String.class).get() + ", ";
* result += ctx.callActivity("SayHello", "London", String.class).get() + ", ";
* result += ctx.callActivity("SayHello", "Seattle", String.class).get();
* return result;
* });
* }
* </pre>
*
* @since 2.0.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@CustomBinding(direction = "in", name = "", type = "orchestrationTrigger")
public @interface DurableOrchestrationTrigger {
/**
* <p>The name of the orchestrator function.</p>
* <p>If not specified, the function name is used as the name of the orchestration.</p>
* <p>This property supports binding parameters.</p>
* @return The name of the orchestrator function.
*/
String orchestration() default "";

/**
* The variable name used in function.json.
*
* @return The variable name used in function.json.
*/
String name();

/**
* <p>
* Defines how Functions runtime should treat the parameter value. Possible values are:
* </p>
* <ul>
* <li>"": get the value as a string, and try to deserialize to actual parameter type like POJO</li>
* <li>string: always get the value as a string</li>
* <li>binary: get the value as a binary data, and try to deserialize to actual parameter type byte[]</li>
* </ul>
*
* @return The dataType which will be used by the Functions runtime.
*/
String dataType() default "string";
}
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
subprojects {
repositories {
mavenLocal()
mavenCentral()
}
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
15 changes: 12 additions & 3 deletions sdk/build.gradle
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
plugins {
id 'java'
id 'java-library'
id 'com.google.protobuf' version '0.8.16'
// Generate IntelliJ IDEA's .idea & .iml project files
id 'idea'
id 'maven-publish'
}

group 'com.microsoft'
group 'com.microsoft.durabletask'
version = '0.1.0'
archivesBaseName = 'durabletask'
archivesBaseName = 'durabletask-sdk'

def grpcVersion = '1.38.0'
def protocVersion = '3.12.0'
Expand Down Expand Up @@ -75,3 +76,11 @@ task integrationTest(type: Test) {
testLogging.showStandardStreams = true
}

publishing {
publications {
mavenPublication(MavenPublication) {
artifactId = archivesBaseName
from components.java
}
}
}