Skip to content

Commit 5193120

Browse files
authored
Merge branch 'main' into patbeqo/otel-v2-workflow-prerequisites
2 parents cd21e5b + 5c9599d commit 5193120

169 files changed

Lines changed: 14044 additions & 1916 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ jobs:
114114
unit_test_cloud:
115115
name: Unit test with cloud
116116
runs-on: ubuntu-latest
117-
timeout-minutes: 30
117+
timeout-minutes: 60
118118
steps:
119119
- name: Checkout repo
120120
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -132,17 +132,70 @@ jobs:
132132
- name: Set up Gradle
133133
uses: gradle/actions/setup-gradle@ac396bf1a80af16236baf54bd7330ae21dc6ece5 # v6
134134

135+
- name: Check Cloud test eligibility
136+
id: cloud-test-eligibility
137+
# Secrets are unavailable to Dependabot and pull requests from forks.
138+
if: ${{ github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-java') }}
139+
env:
140+
TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }}
141+
run: |
142+
if [[ -n "$TEMPORAL_CLIENT_CLOUD_API_KEY" ]]; then
143+
echo "enabled=true" >> "$GITHUB_OUTPUT"
144+
else
145+
echo "::notice title=Cloud tests skipped::TEMPORAL_CLIENT_CLOUD_API_KEY is unavailable"
146+
fi
147+
148+
- name: Generate Cloud test certificates
149+
if: ${{ steps.cloud-test-eligibility.outputs.enabled == 'true' }}
150+
run: |
151+
cert_dir="$RUNNER_TEMP/cloud-test-certs"
152+
mkdir "$cert_dir"
153+
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
154+
-keyout "$cert_dir/ca.key" -out "$cert_dir/ca.pem" \
155+
-subj '/CN=Temporal Java SDK Cloud CI CA'
156+
openssl req -newkey rsa:2048 -nodes \
157+
-keyout "$cert_dir/client.key" -out "$cert_dir/client.csr" \
158+
-subj '/CN=Temporal Java SDK Cloud CI'
159+
openssl x509 -req -days 1 -in "$cert_dir/client.csr" \
160+
-CA "$cert_dir/ca.pem" -CAkey "$cert_dir/ca.key" -CAcreateserial \
161+
-out "$cert_dir/client.pem" -extfile <(printf 'extendedKeyUsage=clientAuth')
162+
{
163+
echo "TEMPORAL_CLOUD_CLIENT_CA_PATH=$cert_dir/ca.pem"
164+
echo "TEMPORAL_TLS_CLIENT_CERT_PATH=$cert_dir/client.pem"
165+
echo "TEMPORAL_TLS_CLIENT_KEY_PATH=$cert_dir/client.key"
166+
} >> "$GITHUB_ENV"
167+
168+
- name: Create Cloud namespace
169+
id: create-cloud-namespace
170+
if: ${{ steps.cloud-test-eligibility.outputs.enabled == 'true' }}
171+
env:
172+
TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }}
173+
TEMPORAL_CLIENT_CLOUD_API_VERSION: v0.19.1
174+
run: ./gradlew --no-daemon :temporal-sdk:createCloudTestNamespace
175+
135176
- name: Run cloud test
136-
# Only supported in non-fork runs, since secrets are not available in forks. We intentionally
137-
# are only doing this check on the step instead of the job so we require job passing in CI
138-
# even for those that can't run this step.
139-
if: ${{ github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-java' }}
177+
if: ${{ steps.cloud-test-eligibility.outputs.enabled == 'true' }}
178+
timeout-minutes: 15
140179
env:
141180
USER: unittest
142-
TEMPORAL_CLIENT_CLOUD_NAMESPACE: sdk-ci.a2dd6
181+
TEMPORAL_TEST_ENV_CONFIG_SERVER: "true"
182+
TEMPORAL_ADDRESS: ${{ steps.create-cloud-namespace.outputs.namespace }}.tmprl.cloud:7233
183+
TEMPORAL_NAMESPACE: ${{ steps.create-cloud-namespace.outputs.namespace }}
184+
TEMPORAL_CLIENT_CLOUD_NAMESPACE: ${{ steps.create-cloud-namespace.outputs.namespace }}
185+
TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }}
186+
TEMPORAL_CLIENT_CLOUD_API_VERSION: v0.19.1
187+
run: |
188+
./gradlew --no-daemon :temporal-sdk:test \
189+
--tests '*CloudOperationsClientTest' \
190+
--tests 'io.temporal.client.functional.SignalTest.signalCompletedWorkflow'
191+
192+
- name: Delete Cloud namespace
193+
if: ${{ always() && steps.create-cloud-namespace.outputs.namespace != '' }}
194+
env:
143195
TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }}
144-
TEMPORAL_CLIENT_CLOUD_API_VERSION: 2024-05-13-00
145-
run: ./gradlew --no-daemon :temporal-sdk:test --tests '*CloudOperationsClientTest'
196+
TEMPORAL_CLIENT_CLOUD_API_VERSION: v0.19.1
197+
TEMPORAL_CLOUD_TEST_NAMESPACE: ${{ steps.create-cloud-namespace.outputs.namespace }}
198+
run: ./gradlew --no-daemon :temporal-sdk:deleteCloudTestNamespace
146199

147200
- name: Publish Test Report
148201
uses: mikepenz/action-junit-report@bccf2e31636835cf0874589931c4116687171386 # v6

‎CONTRIBUTING.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,24 @@ Normal Gradle test filtering works, so a single dev-server-backed test can be ru
9494

9595
Java 11 must be available to Gradle for these commands.
9696

97+
To run an SDK test against an externally managed server using the standard Temporal client
98+
environment configuration, set `TEMPORAL_TEST_ENV_CONFIG_SERVER`. For example, the following runs
99+
one Cloud-safe workflow test:
100+
101+
```bash
102+
TEMPORAL_TEST_ENV_CONFIG_SERVER=true \
103+
TEMPORAL_ADDRESS=your-namespace.tmprl.cloud:7233 \
104+
TEMPORAL_NAMESPACE=your-namespace \
105+
TEMPORAL_API_KEY=your-api-key \
106+
./gradlew :temporal-sdk:test \
107+
--tests 'io.temporal.client.functional.SignalTest.signalCompletedWorkflow'
108+
```
109+
110+
The harness also supports the standard `TEMPORAL_CONFIG_FILE` and `TEMPORAL_PROFILE` variables.
111+
Values from `TEMPORAL_ADDRESS`, `TEMPORAL_NAMESPACE`, `TEMPORAL_API_KEY`, `TEMPORAL_TLS_*`, and
112+
`TEMPORAL_GRPC_META_*` override the selected profile. Envconfig mode connects to an existing server
113+
and namespace; it does not create or register either one.
114+
97115
## Things to Avoid
98116

99117
Avoid changes that make review harder without improving the contribution:

contrib/temporal-gcp-cloud-run/README.md renamed to contrib/temporal-gcp-cloud-run-opentelemetry/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Temporal Google Cloud Run module
1+
# Temporal Google Cloud Run OpenTelemetry module
22

33
This module provides an OpenTelemetry plugin with defaults for Temporal Java SDK workers running on Google Cloud Run. Cloud Run worker pools are the recommended deployment because Temporal workers are continuous, pull-based background workloads.
44

@@ -10,7 +10,7 @@ A Cloud Run service can also host a Temporal worker, but it must use instance-ba
1010

1111
## Usage
1212

13-
Add `temporal-gcp-cloud-run` next to your Temporal SDK dependency, then install the plugin on service stubs options before creating clients and workers:
13+
Add `temporal-gcp-cloud-run-opentelemetry` next to your Temporal SDK dependency, then install the plugin on service stubs options before creating clients and workers:
1414

1515
```java
1616
CloudRunOpenTelemetryPlugin plugin = CloudRunOpenTelemetryPlugin.newBuilder().build();
File renamed without changes.

contrib/temporal-gcp-cloud-run/src/main/java/io/temporal/gcp/cloudrun/CloudRunOpenTelemetryPlugin.java renamed to contrib/temporal-gcp-cloud-run-opentelemetry/src/main/java/io/temporal/gcp/cloudrun/opentelemetry/CloudRunOpenTelemetryPlugin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package io.temporal.gcp.cloudrun;
1+
package io.temporal.gcp.cloudrun.opentelemetry;
22

33
import io.opentelemetry.api.OpenTelemetry;
44
import io.temporal.client.WorkflowClientOptions;

contrib/temporal-gcp-cloud-run/src/test/java/io/temporal/gcp/cloudrun/CloudRunOpenTelemetryPluginTest.java renamed to contrib/temporal-gcp-cloud-run-opentelemetry/src/test/java/io/temporal/gcp/cloudrun/opentelemetry/CloudRunOpenTelemetryPluginTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package io.temporal.gcp.cloudrun;
1+
package io.temporal.gcp.cloudrun.opentelemetry;
22

33
import static org.junit.Assert.*;
44

‎contrib/temporal-opentracing/src/test/java/io/temporal/opentracing/StandaloneActivityClientTracingTest.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.opentracing.util.ThreadLocalScopeManager;
88
import io.temporal.api.workflowservice.v1.CountActivityExecutionsResponse;
99
import io.temporal.client.ActivityExecutionCount;
10+
import io.temporal.client.DescribeActivityOptions;
1011
import io.temporal.client.StartActivityOptions;
1112
import io.temporal.common.interceptors.ActivityClientCallsInterceptor;
1213
import io.temporal.common.interceptors.ActivityClientCallsInterceptorBase;
@@ -108,7 +109,8 @@ public void testManagementCallsDoNotCreateSpans() throws TimeoutException {
108109
new ActivityClientCallsInterceptor.GetActivityResultInput<>(
109110
"act-result-async", null, String.class));
110111
interceptor.describeActivity(
111-
new ActivityClientCallsInterceptor.DescribeActivityInput("act-desc", null));
112+
new ActivityClientCallsInterceptor.DescribeActivityInput(
113+
"act-desc", null, DescribeActivityOptions.getDefaultInstance()));
112114
interceptor.cancelActivity(
113115
new ActivityClientCallsInterceptor.CancelActivityInput("act-cancel", null, "reason"));
114116
interceptor.terminateActivity(
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
description = '''Temporal Java SDK External Storage S3 Driver - AWS SDK v2 Client'''
2+
3+
ext {
4+
awsSdkVersion = '2.31.0'
5+
}
6+
7+
dependencies {
8+
api project(':temporal-payload-storage-s3driver')
9+
api platform("software.amazon.awssdk:bom:$awsSdkVersion")
10+
api "software.amazon.awssdk:s3"
11+
12+
// For the @Experimental annotation only.
13+
compileOnly project(':temporal-sdk')
14+
15+
testImplementation project(':temporal-payload-storage-s3driver')
16+
testImplementation "junit:junit:${junitVersion}"
17+
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
18+
testRuntimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: "${logbackVersion}"
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package io.temporal.payload.storage.s3driver.awssdkv2;
2+
3+
import io.temporal.common.Experimental;
4+
import io.temporal.payload.storage.s3driver.S3StorageDriverClient;
5+
import java.util.Collections;
6+
import java.util.Map;
7+
import java.util.Objects;
8+
import java.util.concurrent.CompletableFuture;
9+
import java.util.concurrent.CompletionException;
10+
import javax.annotation.Nonnull;
11+
import software.amazon.awssdk.core.ResponseBytes;
12+
import software.amazon.awssdk.core.async.AsyncRequestBody;
13+
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
14+
import software.amazon.awssdk.regions.Region;
15+
import software.amazon.awssdk.services.s3.S3AsyncClient;
16+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
17+
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
18+
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
19+
import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
20+
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
21+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
22+
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
23+
import software.amazon.awssdk.services.s3.model.S3Exception;
24+
25+
/**
26+
* {@link S3StorageDriverClient} backed by the AWS SDK for Java v2 {@link S3AsyncClient}. The
27+
* wrapped client must be configured with credentials and a region by the caller.
28+
*/
29+
@Experimental
30+
public final class S3AsyncClientAdapter implements S3StorageDriverClient {
31+
private final S3AsyncClient client;
32+
33+
public S3AsyncClientAdapter(@Nonnull S3AsyncClient client) {
34+
this.client = Objects.requireNonNull(client, "client");
35+
}
36+
37+
@Nonnull
38+
@Override
39+
public CompletableFuture<Void> putObject(
40+
@Nonnull String bucket, @Nonnull String key, @Nonnull byte[] data) {
41+
CompletableFuture<PutObjectResponse> request =
42+
client.putObject(
43+
PutObjectRequest.builder().bucket(bucket).key(key).build(),
44+
AsyncRequestBody.fromBytesUnsafe(data)); // avoids a defensive copy
45+
return abortRequestOnCancel(request, request.thenApply(response -> (Void) null));
46+
}
47+
48+
@Nonnull
49+
@Override
50+
public CompletableFuture<Boolean> objectExists(@Nonnull String bucket, @Nonnull String key) {
51+
CompletableFuture<HeadObjectResponse> request =
52+
client.headObject(HeadObjectRequest.builder().bucket(bucket).key(key).build());
53+
return abortRequestOnCancel(
54+
request,
55+
request.handle(
56+
(response, ex) -> {
57+
if (ex == null) {
58+
return true;
59+
}
60+
Throwable cause =
61+
(ex instanceof CompletionException && ex.getCause() != null) ? ex.getCause() : ex;
62+
if (cause instanceof NoSuchKeyException) {
63+
return false;
64+
}
65+
if (cause instanceof S3Exception && ((S3Exception) cause).statusCode() == 404) {
66+
return false;
67+
}
68+
if (cause instanceof RuntimeException) {
69+
throw (RuntimeException) cause;
70+
}
71+
throw new RuntimeException(cause);
72+
}));
73+
}
74+
75+
@Nonnull
76+
@Override
77+
public CompletableFuture<byte[]> getObject(@Nonnull String bucket, @Nonnull String key) {
78+
CompletableFuture<ResponseBytes<GetObjectResponse>> request =
79+
client.getObject(
80+
GetObjectRequest.builder().bucket(bucket).key(key).build(),
81+
AsyncResponseTransformer.toBytes());
82+
return abortRequestOnCancel(request, request.thenApply(ResponseBytes::asByteArrayUnsafe));
83+
}
84+
85+
/**
86+
* Returns {@code result}, wired so that cancelling it cancels the underlying {@code request}. The
87+
* AWS SDK aborts an async request when the future it returns is cancelled. Cancellation does not
88+
* otherwise propagate across the {@code thenApply}/{@code handle} boundary.
89+
*/
90+
private static <T> CompletableFuture<T> abortRequestOnCancel(
91+
CompletableFuture<?> request, CompletableFuture<T> result) {
92+
result.whenComplete(
93+
(value, ex) -> {
94+
if (result.isCancelled()) {
95+
request.cancel(true);
96+
}
97+
});
98+
return result;
99+
}
100+
101+
@Nonnull
102+
@Override
103+
public Map<String, String> describe() {
104+
Region region = client.serviceClientConfiguration().region();
105+
if (region == null) {
106+
return Collections.emptyMap();
107+
}
108+
return Collections.singletonMap("client_region", region.id());
109+
}
110+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.temporal.payload.storage.s3driver.awssdkv2;
2+
3+
import static org.junit.Assert.assertFalse;
4+
import static org.junit.Assert.assertTrue;
5+
import static org.mockito.ArgumentMatchers.any;
6+
import static org.mockito.Mockito.mock;
7+
import static org.mockito.Mockito.when;
8+
9+
import java.util.concurrent.CompletableFuture;
10+
import org.junit.Test;
11+
import software.amazon.awssdk.core.async.AsyncRequestBody;
12+
import software.amazon.awssdk.services.s3.S3AsyncClient;
13+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
14+
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
15+
16+
public class S3AsyncClientAdapterTest {
17+
18+
/**
19+
* Cancelling the future the adapter returns must abort the underlying AWS request. The adapter
20+
* wraps the AWS future with {@code thenApply}, which does not propagate cancellation upstream, so
21+
* this verifies the explicit forwarding does its job.
22+
*/
23+
@Test
24+
public void cancellingReturnedFutureAbortsTheUnderlyingRequest() {
25+
S3AsyncClient s3 = mock(S3AsyncClient.class);
26+
CompletableFuture<PutObjectResponse> awsRequest = new CompletableFuture<>();
27+
when(s3.putObject(any(PutObjectRequest.class), any(AsyncRequestBody.class)))
28+
.thenReturn(awsRequest);
29+
30+
CompletableFuture<Void> result =
31+
new S3AsyncClientAdapter(s3).putObject("bucket", "key", new byte[] {1, 2, 3});
32+
33+
assertFalse(awsRequest.isCancelled());
34+
result.cancel(true);
35+
assertTrue(
36+
"cancelling the adapter's future should abort the AWS request", awsRequest.isCancelled());
37+
}
38+
}

0 commit comments

Comments
 (0)