Skip to content

Commit 900bb85

Browse files
sdk-java: add explicit client exceptions (#222)
1 parent 2acb12a commit 900bb85

70 files changed

Lines changed: 1242 additions & 856 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/examples-java-ci.yml‎

Lines changed: 22 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -40,63 +40,32 @@ jobs:
4040
working-directory: examples/java
4141
steps:
4242
- uses: actions/checkout@v7
43-
- name: Start Dex environment
44-
working-directory: sdk-java
45-
env:
46-
DEX_SERVER_IMAGE: superdurable/dex-server:latest
47-
run: |
48-
docker pull "$DEX_SERVER_IMAGE"
49-
docker compose -f script/docker-compose.yml up -d
50-
- name: Wait for Dex server
51-
working-directory: ${{ github.workspace }}
52-
run: |
53-
set -euo pipefail
54-
echo "Waiting for Elasticsearch..."
55-
for i in $(seq 1 60); do
56-
if docker exec temporal-elasticsearch-dex curl -fsS http://localhost:9200/_cluster/health >/dev/null 2>&1; then
57-
echo "Elasticsearch ready (attempt $i)"
58-
break
59-
fi
60-
if [ "$i" -eq 60 ]; then
61-
echo "Timed out waiting for Elasticsearch"
62-
docker logs temporal-elasticsearch-dex 2>&1 | tail -80 || true
63-
exit 1
64-
fi
65-
sleep 5
66-
done
67-
echo "Waiting for search attributes..."
68-
for i in $(seq 1 120); do
69-
if docker logs temporal-admin-tools-dex 2>&1 | grep -q "All search attributes are registered"; then
70-
echo "Search attributes ready (attempt $i)"
71-
break
72-
fi
73-
if [ "$i" -eq 120 ]; then
74-
echo "Timed out waiting for search attributes"
75-
docker logs temporal-admin-tools-dex 2>&1 | tail -80 || true
76-
docker logs temporal-dex 2>&1 | tail -80 || true
77-
exit 1
78-
fi
79-
sleep 2
80-
done
81-
echo "Waiting for Dex healthcheck..."
82-
for i in $(seq 1 90); do
83-
if curl -fsS http://127.0.0.1:8801/info/healthcheck >/dev/null 2>&1; then
84-
echo "Dex server healthy (attempt $i)"
85-
exit 0
86-
fi
87-
sleep 5
88-
done
89-
echo "Timed out waiting for Dex server"
90-
docker logs dex-server 2>&1 | tail -80 || true
91-
exit 1
43+
- uses: actions/setup-node@v6
44+
with:
45+
node-version: 22
46+
cache: npm
47+
cache-dependency-path: web/package-lock.json
48+
- uses: actions/setup-go@v7
49+
with:
50+
go-version-file: cli/go.mod
51+
cache-dependency-path: |
52+
cli/go.sum
53+
server/go.sum
54+
web/go.sum
55+
- name: Install Temporal CLI runtime for dexcli dev
56+
uses: temporalio/setup-temporal@v0
9257
- uses: actions/setup-java@v5
9358
with:
9459
distribution: zulu
9560
java-version: "17"
9661
- name: Setup Gradle
9762
uses: gradle/actions/setup-gradle@v6
98-
- name: Integration tests
99-
run: ./gradlew test --tests 'io.superdurable.dex.integ.*' --info
100-
- name: Dump docker logs
63+
- name: Integration tests with dexcli dev
64+
run: ./run-integration-tests.sh
65+
- name: Upload service logs
10166
if: always()
102-
run: bash "$GITHUB_WORKSPACE/.github/scripts/dump-docker-logs.sh"
67+
uses: actions/upload-artifact@v7
68+
with:
69+
name: java-examples-integration-services
70+
path: /tmp/test-java-examples-integration-services.log
71+
if-no-files-found: ignore

‎examples/go/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ temporal --address 127.0.0.1:7233 operator search-attribute create \
2626
temporal --address 127.0.0.1:7233 operator search-attribute create \
2727
--name CustomKeywordField --type Keyword
2828
temporal --address 127.0.0.1:7233 operator search-attribute create \
29-
--name CustomStringField --type Text
29+
--name CustomTextField --type Text
3030
```
3131

3232
The defaults connect to Dex at `localhost:8801`. These environment variables override the local addresses:

‎examples/java/README.md‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ temporal --address 127.0.0.1:7233 operator search-attribute create \
1717
temporal --address 127.0.0.1:7233 operator search-attribute create \
1818
--name CustomKeywordField --type Keyword
1919
temporal --address 127.0.0.1:7233 operator search-attribute create \
20-
--name CustomStringField --type Text
20+
--name CustomTextField --type Text
2121

2222
./gradlew bootRun
2323
```
@@ -26,6 +26,17 @@ Use JDK 17. Defaults connect to Dex at `localhost:8801`. Override with
2626
`DEX_FLOW_SERVICE_ADDRESS`, `DEX_WORKER_BIND_ADDRESS`, `DEX_WORKER_TARGET`,
2727
`DEX_BLOB_CACHE_DIR`.
2828

29+
Run the integration suite against an isolated `dexcli dev` environment:
30+
31+
```bash
32+
./run-integration-tests.sh
33+
```
34+
35+
Examples catch concrete types from `io.superdurable.dex.exceptions`.
36+
`FlowNotFoundException` is for read operations with no matching execution;
37+
`FlowNotActiveException` is for RPC or mutation operations after a Flow closes.
38+
`ErrorSubStatus` remains diagnostic metadata and is not used for control flow.
39+
2940
## Product examples
3041

3142
- [Money transfer](./src/main/java/io/superdurable/dex/workflow/money/transfer)
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/bin/bash
2+
3+
# Copyright (c) 2022-2026 Super Durable, Inc.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
set -euo pipefail
18+
19+
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
20+
repo_root=$(cd "$script_dir/../.." && pwd)
21+
dex_port="${DEX_JAVA_EXAMPLES_DEX_PORT:-19803}"
22+
web_port="${DEX_JAVA_EXAMPLES_WEB_PORT:-19903}"
23+
temporal_port="${DEX_JAVA_EXAMPLES_TEMPORAL_PORT:-19235}"
24+
temporal_ui_port="${DEX_JAVA_EXAMPLES_TEMPORAL_UI_PORT:-19335}"
25+
dex_address="127.0.0.1:${dex_port}"
26+
temporal_address="127.0.0.1:${temporal_port}"
27+
log_file="/tmp/test-java-examples-integration-services.log"
28+
test_dir=$(mktemp -d)
29+
binary_dir=$(mktemp -d)
30+
dexcli_pid=""
31+
: >"$log_file"
32+
33+
cleanup() {
34+
status=$?
35+
if [[ -n "$dexcli_pid" ]] && kill -0 "$dexcli_pid" 2>/dev/null; then
36+
kill -TERM "$dexcli_pid"
37+
wait "$dexcli_pid" || true
38+
fi
39+
if [[ "$status" -ne 0 ]]; then
40+
cat "$log_file" >&2
41+
fi
42+
rm -r "$test_dir" "$binary_dir"
43+
}
44+
trap cleanup EXIT
45+
46+
if [[ ! -f "$repo_root/web/assets/dist/index.html" ]]; then
47+
(
48+
cd "$repo_root/web"
49+
npm ci
50+
npm run build
51+
)
52+
fi
53+
54+
(
55+
cd "$repo_root/cli"
56+
GOWORK=off go build -trimpath -o "$binary_dir/dexcli" ./cmd/dexcli
57+
)
58+
59+
"$binary_dir/dexcli" dev \
60+
-bind-address 127.0.0.1 \
61+
-dex-port "$dex_port" \
62+
-web-port "$web_port" \
63+
-temporal-port "$temporal_port" \
64+
-temporal-ui-port "$temporal_ui_port" \
65+
-temporal-db-filename "$test_dir/temporal.db" \
66+
>>"$log_file" 2>&1 &
67+
dexcli_pid=$!
68+
69+
temporal_ready=false
70+
for _ in {1..240}; do
71+
if temporal --address "$temporal_address" operator search-attribute list >/dev/null 2>&1; then
72+
temporal_ready=true
73+
break
74+
fi
75+
if ! kill -0 "$dexcli_pid" 2>/dev/null; then
76+
echo "dexcli exited before Temporal became ready" >&2
77+
exit 1
78+
fi
79+
sleep 0.25
80+
done
81+
if ! $temporal_ready; then
82+
echo "Temporal did not become ready" >&2
83+
exit 1
84+
fi
85+
86+
temporal --address "$temporal_address" operator search-attribute create --name CustomKeywordField --type Keyword
87+
temporal --address "$temporal_address" operator search-attribute create --name CustomTextField --type Text
88+
89+
dex_ready=false
90+
for _ in {1..240}; do
91+
if grep -q "Dex development environment is ready" "$log_file"; then
92+
dex_ready=true
93+
break
94+
fi
95+
if ! kill -0 "$dexcli_pid" 2>/dev/null; then
96+
echo "dexcli exited before Dex became ready" >&2
97+
exit 1
98+
fi
99+
sleep 0.25
100+
done
101+
if ! $dex_ready; then
102+
echo "Dex did not become ready" >&2
103+
exit 1
104+
fi
105+
106+
cd "$script_dir"
107+
DEX_FLOW_SERVICE_ADDRESS="$dex_address" \
108+
./gradlew --include-build "$repo_root/sdk-java" \
109+
test --tests 'io.superdurable.dex.integ.*' --info --no-daemon

‎examples/java/src/main/java/io/superdurable/dex/config/CronScheduleStarter.java‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@
1717
package io.superdurable.dex.config;
1818

1919
import io.superdurable.dex.Client;
20-
import io.superdurable.dex.DexException;
21-
import io.superdurable.dex.ErrorSubStatus;
2220
import io.superdurable.dex.StartFlowOptions;
21+
import io.superdurable.dex.exceptions.FlowAlreadyStartedException;
2322
import io.superdurable.dex.patterns.workflow.cron.CronScheduleFlow;
2423
import org.springframework.boot.context.event.ApplicationReadyEvent;
2524
import org.springframework.context.event.EventListener;
@@ -52,10 +51,7 @@ public void startCronSchedule() {
5251
.timeout(Duration.ofHours(1))
5352
.cronSchedule(CRON_EXPRESSION)
5453
.build());
55-
} catch (final DexException e) {
56-
if (e.getSubStatus() != ErrorSubStatus.FLOW_ALREADY_STARTED) {
57-
throw e;
58-
}
54+
} catch (final FlowAlreadyStartedException ignored) {
5955
}
6056
}
6157
}

‎examples/java/src/main/java/io/superdurable/dex/controller/ShortlistCandidatesController.java‎

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
package io.superdurable.dex.controller;
1818

1919
import io.superdurable.dex.Client;
20-
import io.superdurable.dex.DexException;
21-
import io.superdurable.dex.ErrorSubStatus;
20+
import io.superdurable.dex.exceptions.FlowAlreadyStartedException;
21+
import io.superdurable.dex.exceptions.FlowNotActiveException;
2222
import io.superdurable.dex.workflow.shortlistcandidates.EmployerOptInFlow;
2323
import io.superdurable.dex.workflow.shortlistcandidates.EmployerOptInInput;
2424
import io.superdurable.dex.workflow.shortlistcandidates.ShortlistFlow;
@@ -62,12 +62,9 @@ public ResponseEntity<String> optIn(@RequestBody final Map<String, String> reque
6262
workflowId,
6363
input,
6464
ExampleFlows.startOptions());
65-
} catch (final DexException e) {
66-
if (e.getSubStatus() == ErrorSubStatus.FLOW_ALREADY_STARTED) {
67-
return ResponseEntity.ok(
68-
String.format("Employer %s has already opted in", employerId));
69-
}
70-
throw e;
65+
} catch (final FlowAlreadyStartedException alreadyStarted) {
66+
return ResponseEntity.ok(
67+
String.format("Employer %s has already opted in", employerId));
7168
}
7269

7370
return ResponseEntity.ok(String.format("Started workflowId: %s", workflowId));
@@ -81,12 +78,9 @@ public ResponseEntity<String> optOut(@RequestBody final Map<String, String> requ
8178
client.newRpcStub(EmployerOptInFlow.class, workflowId);
8279
try {
8380
client.invokeRPC(stub::optOut);
84-
} catch (final DexException e) {
85-
if (e.getSubStatus() == ErrorSubStatus.FLOW_NOT_EXISTS) {
86-
return ResponseEntity.ok(
87-
String.format("Employer %s is not in the opt-in status", employerId));
88-
}
89-
throw e;
81+
} catch (final FlowNotActiveException inactive) {
82+
return ResponseEntity.ok(
83+
String.format("Employer %s is not in the opt-in status", employerId));
9084
}
9185
return ResponseEntity.ok(String.format("Employer %s has opted out", employerId));
9286
}
@@ -118,12 +112,9 @@ public ResponseEntity<String> shortlist(@RequestBody final Map<String, String> r
118112
workflowId,
119113
input,
120114
ExampleFlows.startOptions());
121-
} catch (final DexException e) {
122-
if (e.getSubStatus() == ErrorSubStatus.FLOW_ALREADY_STARTED) {
123-
return ResponseEntity.ok(
124-
String.format("Already running workflowId: %s", workflowId));
125-
}
126-
throw e;
115+
} catch (final FlowAlreadyStartedException alreadyStarted) {
116+
return ResponseEntity.ok(
117+
String.format("Already running workflowId: %s", workflowId));
127118
}
128119

129120
return ResponseEntity.ok(String.format("Started workflowId: %s", workflowId));
@@ -138,13 +129,10 @@ public ResponseEntity<String> revokeShortlist(
138129

139130
try {
140131
client.publish(workflowId, shortlistFlow.revokeShortlist, (Void) null);
141-
} catch (final DexException e) {
142-
if (e.getSubStatus() == ErrorSubStatus.FLOW_NOT_EXISTS) {
143-
return ResponseEntity.ok(String.format(
144-
"No running workflow to revoke for %s",
145-
employerId + "-" + candidateId));
146-
}
147-
throw e;
132+
} catch (final FlowNotActiveException inactive) {
133+
return ResponseEntity.ok(String.format(
134+
"No running workflow to revoke for %s",
135+
employerId + "-" + candidateId));
148136
}
149137

150138
return ResponseEntity.ok(String.format(
@@ -160,11 +148,8 @@ public ResponseEntity<Long> getEmailSentTimestamp(
160148
final ShortlistFlow stub = client.newRpcStub(ShortlistFlow.class, workflowId);
161149
try {
162150
return ResponseEntity.ok(client.invokeRPC(stub::getEmailSentTimestamp));
163-
} catch (final DexException e) {
164-
if (e.getSubStatus() == ErrorSubStatus.FLOW_NOT_EXISTS) {
165-
return ResponseEntity.notFound().build();
166-
}
167-
throw e;
151+
} catch (final FlowNotActiveException inactive) {
152+
return ResponseEntity.notFound().build();
168153
}
169154
}
170155
}

‎examples/java/src/main/java/io/superdurable/dex/controller/SignupWorkflowController.java‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
package io.superdurable.dex.controller;
1818

1919
import io.superdurable.dex.Client;
20-
import io.superdurable.dex.DexException;
21-
import io.superdurable.dex.ErrorSubStatus;
20+
import io.superdurable.dex.exceptions.FlowAlreadyStartedException;
2221
import io.superdurable.dex.workflow.signup.SignupForm;
2322
import io.superdurable.dex.workflow.signup.UserSignupFlow;
2423
import org.springframework.http.ResponseEntity;
@@ -47,11 +46,8 @@ public ResponseEntity<String> submit(
4746
final SignupForm form = new SignupForm(username, email, "Test", "Test");
4847
try {
4948
client.startFlow(flow, username, form, ExampleFlows.startOptions());
50-
} catch (final DexException exception) {
51-
if (exception.getSubStatus() == ErrorSubStatus.FLOW_ALREADY_STARTED) {
52-
return ResponseEntity.ok("username already started registry");
53-
}
54-
throw exception;
49+
} catch (final FlowAlreadyStartedException alreadyStarted) {
50+
return ResponseEntity.ok("username already started registry");
5551
}
5652
return ResponseEntity.ok("success");
5753
}

0 commit comments

Comments
 (0)