From 8d954dc7e2dcb4953e7a3c9dc46bd2baca919797 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 17:08:32 -0700 Subject: [PATCH 01/18] Add design doc: finish sdk-tests Testcontainers migration + strip legacy CI Migrates the last 7 sdk-tests ITs off the dapr-run CLI harness onto BaseContainerIT/DaprContainer so the build job can drop the Dapr CLI, dapr init/uninstall, host Kafka, and host ToxiProxy setup. Covers the app-restart, sidecar-restart, multi-sidecar failover, ToxiProxy, and Kafka mechanics, plus CI and dead-code cleanup. Addresses #1522. Signed-off-by: Siri Varma Vegiraju --- ...k-tests-testcontainers-migration-design.md | 248 ++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md diff --git a/docs/superpowers/specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md b/docs/superpowers/specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md new file mode 100644 index 000000000..ca45eb519 --- /dev/null +++ b/docs/superpowers/specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md @@ -0,0 +1,248 @@ +# Finish sdk-tests Testcontainers Migration + Strip Legacy CI — Design + +**Date:** 2026-07-09 +**Status:** Draft (pending spec review) +**Author:** Siri Varma Vegiraju (with Claude) +**Scope:** [sdk-tests/](../../../sdk-tests/) module and [.github/workflows/build.yml](../../../.github/workflows/build.yml) +**Issue:** [dapr/java-sdk#1522](https://github.com/dapr/java-sdk/issues/1522) — Optimize CI/CD build times + +## Problem + +Issue #1522 reports CI timeouts in the integration-test job (the 30-min limit was hit; the +`build` job timeout has since been raised to 45 min). Job-level timing on a recent master run: + +| Job | Duration | +|---|---| +| Unit tests | 4.6 min | +| Durable Task build & tests | 5.5 min | +| Build sb:4.0.x (Testcontainers, `spring-boot-4-sdk-tests`) | 11.1 min | +| **Build sb:3.5.x (`sdk-tests`, mixed) | 26.3 min** ← bottleneck | +| publish | 9.3 min | + +The `sb:3.5.x` job runs the `sdk-tests` module. After PR #1754 (merged 2026-07-09), **13 of +its 20 integration tests already run on Testcontainers** (`BaseContainerIT`/`DaprContainer`), +but **7 still use the legacy `dapr run` CLI harness** (`BaseIT`/`DaprRun`/`AppRun`). Because +those 7 need the CLI, the `build` job must still install the Dapr CLI, run `dapr init`, +`dapr uninstall`, spin host Kafka via docker-compose, and install a host ToxiProxy binary — +and each legacy test shells out to `dapr run` per app. The Testcontainers-based `sb:4.0.x` +job doing comparable work finishes in less than half the time. + +The only way to remove the CLI/`dapr init`/Kafka/ToxiProxy setup from CI — and collapse the +26-min bottleneck — is to migrate the **last 7 tests** off the legacy harness so nothing in +`sdk-tests` needs the Dapr CLI. + +## The 7 remaining legacy tests + +| # | IT | Legacy mechanism | Group | +|---|---|---|---| +| 1 | [ActorStateIT](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java) | Restart **app** mid-test; verify actor state persisted in Redis | A | +| 2 | [ActorTimerRecoveryIT](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java) | Restart **app** quickly (no sleep); verify in-memory timer survives | A | +| 3 | [ActorReminderRecoveryIT](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java) | Restart **daprd sidecar**; verify persisted reminder resumes | B | +| 4 | [ActorReminderFailoverIT](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderFailoverIT.java) | Two apps + two sidecars; kill one app; verify reminder fails over | C | +| 5 | [WaitForSidecarIT](../../../sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java) | ToxiProxy latency + a not-running sidecar; verify `waitForSidecar` timeout semantics | D | +| 6 | [ActorSdkResiliencyIT](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorSdkResiliencyIT.java) | ToxiProxy latency/jitter between actor client and sidecar (its one `@Test` is `@Disabled`) | D | +| 7 | [BindingIT](../../../sdk-tests/src/test/java/io/dapr/it/binding/http/BindingIT.java) | Kafka input/output bindings + two HTTP output-binding error tests | E | + +## Goals + +- Migrate all 7 ITs to extend `BaseContainerIT` and run Dapr in a `DaprContainer`. +- Add the small set of reusable helpers these tests need to `BaseContainerIT` (app restart, + sidecar restart, shared-placement/scheduler multi-sidecar). +- Add a `KafkaContainer` for `BindingIT` and a `ToxiproxyContainer` pattern for the Group-D tests. +- Remove the Dapr CLI, `dapr init`/`dapr uninstall`, host Kafka (docker-compose), and host + ToxiProxy steps from the `build` job in [build.yml](../../../.github/workflows/build.yml). +- Delete the now-dead legacy harness (`BaseIT`, `DaprRun`, `DaprRunConfig`, `ToxiProxyRun`) and + the file-based component/config YAMLs and `deploy/local-test.yml` that only the 7 tests used. +- Land as a single PR (single cutover). + +## Non-Goals + +- Migrating the two `durabletask-client` ITs. They run in the separate `build-durabletask` + job, which uses its own sidecar container (not the Dapr CLI) and is untouched. +- **Re-enabling** `ActorSdkResiliencyIT`'s disabled test. It is migrated off the legacy harness + but stays `@Disabled` — re-enabling a known-flaky test is out of scope and contrary to #1754's + flakiness cleanup. +- Making `BindingIT`'s two HTTP output-binding tests hermetic. They keep pointing at + `https://api.github.com/unknown_path` (behavior preserved; a prior switch to httpbin was + reverted). Removing that external dependency is tracked as future work. +- Replacing `AppRun`'s `mvn exec:java` subprocess model. The app side stays a subprocess; only + the sidecar/backing-services are containerized. `AppRun`, `DaprPorts`, `Command`, `Stoppable` + are retained because `BaseContainerIT` depends on them. +- Parallelizing Surefire/Failsafe or sharding the CI matrix (separate, deferred approaches). + +## Decisions + +| # | Decision | Rationale | +|---|---|---| +| D1 | Migrate all 7 in one PR, then strip CI legacy setup in the same PR | The CI win is only realized when the last legacy test is gone; a partial migration keeps the CLI/init/Kafka/ToxiProxy setup and barely moves CI time. | +| D2 | Put reusable mechanics in `BaseContainerIT` (`restartApp`, `restartSidecar`, shared-placement multi-sidecar helper) | Keeps the 7 tests thin and consistent; mirrors how `BaseContainerIT` already centralizes lifecycle. | +| D3 | Keep `BindingIT` HTTP tests on the external GitHub URL | Preserves behavior, minimal blast radius; hermeticity is orthogonal to removing `dapr run`. | +| D4 | `ActorSdkResiliencyIT` migrated but stays `@Disabled` | Removes the last `ToxiProxyRun` consumer without re-introducing a flaky test into CI. | +| D5 | Reminders survive daprd-container restart via the **scheduler container** | Dapr 1.18 stores actor reminders in the scheduler service; restarting only the daprd container (placement + scheduler + Redis stay up) preserves them. | +| D6 | Failover uses **explicit shared** placement + scheduler containers via `withPlacementContainer`/`withSchedulerContainer` | `withReusablePlacement(true)` relies on Testcontainers reuse, which is disabled on CI hosts; explicit shared containers make the two-sidecar topology deterministic. Pattern already proven in `WorkflowsMultiAppCallActivityIT`. | +| D7 | Add `org.testcontainers:testcontainers-kafka` (BOM-managed, test scope) for `BindingIT` | Kafka Testcontainers is not currently a dependency; Testcontainers 2.0.5 module artifactIds are `testcontainers-`-prefixed and versionless (managed by `testcontainers-bom`). | +| D8 | Single spec + single staged implementation plan | Matches the single-cutover decision; plan tasks are ordered and independently reviewable. | + +## Architecture + +All new mechanics live in [`BaseContainerIT`](../../../sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java), +which already provides `daprBuilder`, `startAppAndAttach`, `newDaprClient`/`newActorClient`, +`redisStateStore`, `waitForActorsReady`, and LIFO `deferStop`/`deferClose` cleanup. Additions: + +### 1. `restartApp(AppRun app)` — Group A + +Stops and restarts the app subprocess on its **same** pre-allocated port. daprd stays up; since +`daprBuilder` configures **no** app-health-check, daprd does not deactivate actors during the +gap, so in-memory timers survive (matches the legacy `@DaprRunConfig(enableAppHealthCheck=false)` +on `MyActorService`). For `ActorTimerRecoveryIT` the restart must be "quick" (no sleep between +stop and start) — the helper calls `app.stop()` immediately followed by `app.start()`. + +```java +protected static void restartApp(AppRun app) throws Exception { + app.stop(); + app.start(); // rebinds the same host app port; daprd reconnects via host.testcontainers.internal:appPort +} +``` + +### 2. `restartSidecar(DaprContainer dapr)` — Group B + +Stops and restarts the daprd container, then re-verifies readiness. Placement + scheduler are +instance fields on `DaprContainer` that are **not** recreated on the second `start()` (the +`if (placementContainer == null)` / `if (schedulerContainer == null)` guards in +`DaprContainer.configure()`), so they persist across the restart — and so does the reminder, +which the scheduler owns. Pinned `setPortBindings` re-bind the same host ports, so the app's +`DAPR_HTTP_PORT`/`DAPR_GRPC_PORT` and any `DaprClient` remain valid. + +```java +protected static void restartSidecar(DaprContainer dapr) { + dapr.stop(); + dapr.start(); // reuses existing placement + scheduler; re-binds pinned ports + try (DaprClient c = newDaprClient(dapr)) { c.waitForSidecar(30_000).block(); } + waitForActorsReady(dapr); +} +``` + +`ActorReminderRecoveryIT` uses this in place of the legacy `DaprRun.stop()`/`start()`, keeping +its "pause 10s then 7s" waits so placement/health settle. + +### 3. Shared-placement multi-sidecar helper — Group C + +A helper (and/or a documented pattern) that builds two `DaprContainer` app-sidecars sharing one +`DaprPlacementContainer` and one `DaprSchedulerContainer` on `SharedTestInfra.network()`, each +attached to its own `AppRun` via the existing two-phase startup. Modeled directly on +[`WorkflowsMultiAppCallActivityIT`](../../../sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/multiapp/WorkflowsMultiAppCallActivityIT.java) +(lines 57-111): `new DaprPlacementContainer(...).withNetworkAliases("placement")`, ditto +scheduler, then each daprd `.withPlacementContainer(shared).withSchedulerContainer(shared)`. + +`ActorReminderFailoverIT` needs each sidecar's pinned HTTP port so it can match the actor host +returned by `MyActorBase.getIdentifier()` (which returns `DAPR_HTTP_PORT`) and kill the correct +`AppRun`. The helper therefore exposes both `(DaprContainer, AppRun)` pairs and their pinned ports. + +### 4. ToxiProxy wiring — Group D + +Copy the proven pattern from the already-migrated +[`SdkResiliencyIT`](../../../sdk-tests/src/test/java/io/dapr/it/resiliency/SdkResiliencyIT.java): +a `ToxiproxyContainer` (image `ContainerConstants.TOXI_PROXY_IMAGE_TAG` = +`ghcr.io/shopify/toxiproxy:2.5.0`) on the shared network, `DaprContainer` with +`.withNetworkAliases("dapr")`, a proxy `dapr:3500`/`dapr:50001`, and a `DaprClient`/`ActorClient` +pointed at `TOXIPROXY.getMappedPort(...)`. Toxics (latency/timeout) are added/removed per test +via the `ToxiproxyClient` API. `WaitForSidecarIT` needs no app; `ActorSdkResiliencyIT` combines +this with `startAppAndAttach` for its actor app. For `WaitForSidecarIT`'s "not running" case, +point the client at a stopped container's former port (or an unbound port) so the connection fails. + +### 5. `KafkaContainer` for BindingIT — Group E + +Add `org.testcontainers:testcontainers-kafka` (test scope, BOM-managed). Start a `KafkaContainer` +on `SharedTestInfra.network()` with alias `kafka`; build the `bindings.kafka` component in-code +with `brokers` = the internal listener (`kafka:9092`), `topics`/`publishTopic`/`consumerGroup` +using the `{appID}` placeholder as today, `authRequired=false`, `initialOffset=oldest`. The +`InputBindingService` app is started via `startAppAndAttach`. The two HTTP output-binding +components (`github-http-binding-404`, `github-http-binding-404-success`) are declared in-code as +`bindings.http` components (URL unchanged), replacing the file-based `http_binding.yaml`. + +### Coexistence during migration + +`AppRun`, `DaprPorts`, `Command`, `Stoppable` stay (consumed by `BaseContainerIT`). Once all 7 +tests are migrated, `BaseIT`, `DaprRun`, `DaprRunConfig`, and `ToxiProxyRun` have no remaining +references in `sdk-tests` and are deleted (verified by grep per file before deletion). + +## Per-IT migration matrix + +| # | IT | App service class / actor type | Components (in-code) | New mechanics | +|---|---|---|---|---| +| 1 | ActorStateIT | `StatefulActorService` / `StatefulActorTest` | `redisStateStore("statestore")` | `startAppAndAttach` + `restartApp` (verify Redis-persisted state after app restart) | +| 2 | ActorTimerRecoveryIT | `MyActorService` / `MyActorTest` | `redisStateStore` | `startAppAndAttach` + `restartApp` (quick, no-sleep) | +| 3 | ActorReminderRecoveryIT | `MyActorService` (+ separate client app) | `redisStateStore` | `startAppAndAttach` + `restartSidecar` | +| 4 | ActorReminderFailoverIT | 2× `MyActorService` + 1 client | `redisStateStore` (shared) | shared-placement multi-sidecar helper; kill one `AppRun` | +| 5 | WaitForSidecarIT | none | none (in-memory kvstore ok) | ToxiProxy pattern; one stopped/unavailable endpoint | +| 6 | ActorSdkResiliencyIT | `DemoActorService` / `DemoActorTest` | default | ToxiProxy pattern + `startAppAndAttach`; test stays `@Disabled` | +| 7 | BindingIT | `InputBindingService` | `bindings.kafka` (Kafka container), 2× `bindings.http` | `KafkaContainer` + `startAppAndAttach` | + +Each migrated IT drops `extends BaseIT`, all `startDaprApp`/`DaprRun`/`DaprPorts`/`DaprRunConfig` +imports, and file-based component lookups; it defines components in-code via the `Component` +model and uses the `BaseContainerIT` client/actor factories. + +## CI changes ([.github/workflows/build.yml](../../../.github/workflows/build.yml), `build` job) + +| Step | Disposition | +|---|---| +| Set up Dapr CLI (`wget … install.sh`) | **Remove** | +| Set up Go + checkout `dapr/cli` + `dapr/dapr` overrides + build daprd/placement | **Remove** (all `DAPR_REF`/`DAPR_CLI_REF` conditional blocks) | +| `dapr uninstall --all` + "Ensure Dapr runtime uninstalled" wait loop | **Remove** | +| `dapr init --runtime-version …` | **Remove** | +| `docker compose -f ./sdk-tests/deploy/local-test.yml up -d kafka` | **Remove** (Kafka now via Testcontainers) | +| Install host ToxiProxy (`wget toxiproxy-server`) | **Remove** (ToxiProxy now via Testcontainers) | +| `docker version` check | Keep (Testcontainers needs Docker) | +| Checkout / setup-java / `./mvnw clean install -DskipTests` | Keep | +| Integration-test run (sb 3.x / sb 4.x) + failsafe/surefire report uploads | Keep (test discovery surface unchanged) | +| `DAPR_CLI_VER` / `DAPR_RUNTIME_VER` / `DAPR_INSTALL_URL` / `TOXIPROXY_URL` / `GOVER…` env | **Remove** (no longer referenced) | + +Also delete `sdk-tests/deploy/local-test.yml` (Kafka/Zookeeper), unless another consumer is found. +`build-durabletask`, `test` (unit), and `publish` jobs are untouched. + +## Dead-code & resource cleanup + +After migration, delete (verifying zero remaining references first): +- `sdk-tests/.../io/dapr/it/BaseIT.java`, `DaprRun.java`, `DaprRunConfig.java`, `ToxiProxyRun.java` +- The file-based YAMLs only these tests loaded: Kafka binding, HTTP binding, and any + `components/`/`configurations/` files with no remaining `withComponent(Path)` or CLI consumer. +- `sdk-tests/deploy/local-test.yml` + +Retain: `AppRun`, `DaprPorts`, `Command`, `Stoppable`, `SharedTestInfra`, `BaseContainerIT`. + +## Risks & mitigations + +| Risk | Mitigation | +|---|---| +| **Timer/reminder tests are timing-sensitive and could flake in containers** (cf. the known `ActorTurnBasedConcurrencyIT` timer-count flakiness) | Preserve the legacy waits; prefer `Awaitility`/`callWithRetry` with generous timeouts over fixed sleeps for assertions; validate each locally across repeated runs before finalizing. | +| Reminder does **not** survive daprd restart (if scheduler assumption is wrong) | Verify on Dapr 1.18 that scheduler owns reminders and stays up across restart; if not, fall back to keeping the reminder in Redis-backed state and reloading. Prove `ActorReminderRecoveryIT` locally first. | +| Testcontainers **reuse disabled on CI** breaks shared placement for failover | Use explicit shared placement/scheduler containers (D6), not `withReusable*`. | +| Killing one app in failover doesn't migrate the actor within the window | Match the legacy 10s failover wait; assert via `getIdentifier()` host change with retry. | +| More containers per class (2 sidecars + placement + scheduler + Kafka/ToxiProxy) lengthen cold runs | Acceptable: removes host CLI/init/uninstall overhead and per-test `dapr run`; net expected win is large (target the ~11 min the sb:4.0.x job already hits). | +| Kafka container startup adds time to `BindingIT` | Single Kafka container per class; `KafkaContainer` boots in a few seconds; still far cheaper than host Kafka + CLI. | +| `host.testcontainers.internal` differences dev vs CI | Handled by `Testcontainers.exposeHostPorts` (already used by `startAppAndAttach`); CI is Linux. | + +## Testing strategy + +- Each migrated IT runs locally via `cd sdk-tests && ../mvnw verify -Dit.test=` and + passes; the timing-sensitive ones (Timer/Reminder recovery, Failover) are run repeatedly to + check for flakiness before finalizing. +- Full `sdk-tests` `verify` passes locally and on CI with the trimmed `build.yml`. +- The 13 already-migrated ITs continue to pass unchanged. +- No new unit tests for the `BaseContainerIT` helpers; they are exercised by the migrated ITs. +- CI acceptance: `build sb:3.5.x` completes well under the 45-min timeout and materially faster + than 26 min (target: comparable to the sb:4.0.x job). + +## Open questions + +None at spec-approval time. The implementation plan will pin: the exact `testcontainers-kafka` +coordinates/version from the BOM, the `KafkaContainer` image and internal-listener config, the +`ToxiproxyContainer` proxy wiring for the actor gRPC channel in `ActorSdkResiliencyIT`, and the +precise `restartSidecar` wait sequence. + +## Out of scope (future work) + +- Making `BindingIT` HTTP tests hermetic (local stub instead of GitHub). +- Re-enabling `ActorSdkResiliencyIT`. +- Migrating the two `durabletask-client` ITs. +- Surefire/Failsafe parallelization or CI matrix sharding. From 5fc7858c74ca5475a6f23778a7017277faa07dfa Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 17:12:09 -0700 Subject: [PATCH 02/18] Refine migration spec per review: kafka dep location, timer health-check invariant, reminder client, explicit YAML cleanup Signed-off-by: Siri Varma Vegiraju --- ...k-tests-testcontainers-migration-design.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md b/docs/superpowers/specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md index ca45eb519..e385762c3 100644 --- a/docs/superpowers/specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md +++ b/docs/superpowers/specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md @@ -80,7 +80,7 @@ The only way to remove the CLI/`dapr init`/Kafka/ToxiProxy setup from CI — and | D4 | `ActorSdkResiliencyIT` migrated but stays `@Disabled` | Removes the last `ToxiProxyRun` consumer without re-introducing a flaky test into CI. | | D5 | Reminders survive daprd-container restart via the **scheduler container** | Dapr 1.18 stores actor reminders in the scheduler service; restarting only the daprd container (placement + scheduler + Redis stay up) preserves them. | | D6 | Failover uses **explicit shared** placement + scheduler containers via `withPlacementContainer`/`withSchedulerContainer` | `withReusablePlacement(true)` relies on Testcontainers reuse, which is disabled on CI hosts; explicit shared containers make the two-sidecar topology deterministic. Pattern already proven in `WorkflowsMultiAppCallActivityIT`. | -| D7 | Add `org.testcontainers:testcontainers-kafka` (BOM-managed, test scope) for `BindingIT` | Kafka Testcontainers is not currently a dependency; Testcontainers 2.0.5 module artifactIds are `testcontainers-`-prefixed and versionless (managed by `testcontainers-bom`). | +| D7 | Add `org.testcontainers:testcontainers-kafka` (test scope, versionless) to `sdk-tests/pom.xml` for `BindingIT` | `testcontainers-kafka` is already version-managed in the **root** `pom.xml` `dependencyManagement` (via `testcontainers-bom`) but is not a declared dependency of `sdk-tests`. Add it to `sdk-tests/pom.xml` only, without a ``. Testcontainers 2.0.5 module artifactIds are `testcontainers-`-prefixed. | | D8 | Single spec + single staged implementation plan | Matches the single-cutover decision; plan tasks are ordered and independently reviewable. | ## Architecture @@ -97,6 +97,11 @@ gap, so in-memory timers survive (matches the legacy `@DaprRunConfig(enableAppHe on `MyActorService`). For `ActorTimerRecoveryIT` the restart must be "quick" (no sleep between stop and start) — the helper calls `app.stop()` immediately followed by `app.start()`. +> **Invariant:** this depends on `daprBuilder` (BaseContainerIT.java:75-88) never adding +> `withAppHealthCheckPath(...)`. If a future change adds an app-health-check path to the shared +> builder, daprd would deactivate the actor during the restart gap and `ActorTimerRecoveryIT` +> would lose its timer. Keep any health-check config test-local, not in `daprBuilder`. + ```java protected static void restartApp(AppRun app) throws Exception { app.stop(); @@ -182,6 +187,13 @@ Each migrated IT drops `extends BaseIT`, all `startDaprApp`/`DaprRun`/`DaprPorts imports, and file-based component lookups; it defines components in-code via the `Component` model and uses the `BaseContainerIT` client/actor factories. +> **ActorReminderRecoveryIT client:** the legacy test starts a second, long-lived client app +> (`ActorReminderRecoveryTestClient`) and builds the reminder proxy off that client run, not the +> service run. In the migrated test this is a plain `newActorClient(dapr)` against the same +> sidecar (no second app subprocess is required) — the "(+ separate client app)" in the matrix +> refers to this actor-client, which the plan should implement as an `ActorClient`, not a second +> `startAppAndAttach`. + ## CI changes ([.github/workflows/build.yml](../../../.github/workflows/build.yml), `build` job) | Step | Disposition | @@ -204,8 +216,9 @@ Also delete `sdk-tests/deploy/local-test.yml` (Kafka/Zookeeper), unless another After migration, delete (verifying zero remaining references first): - `sdk-tests/.../io/dapr/it/BaseIT.java`, `DaprRun.java`, `DaprRunConfig.java`, `ToxiProxyRun.java` -- The file-based YAMLs only these tests loaded: Kafka binding, HTTP binding, and any - `components/`/`configurations/` files with no remaining `withComponent(Path)` or CLI consumer. +- The file-based YAMLs only these tests loaded — explicitly `sdk-tests/components/kafka_bindings.yaml` + and `sdk-tests/components/http_binding.yaml`, plus any other `components/`/`configurations/` + file left with no remaining `withComponent(Path)` or CLI consumer (grep-verified per file). - `sdk-tests/deploy/local-test.yml` Retain: `AppRun`, `DaprPorts`, `Command`, `Stoppable`, `SharedTestInfra`, `BaseContainerIT`. From 3a8ac90d4890d552e4422ddc9e28a5b41514fd74 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 17:42:35 -0700 Subject: [PATCH 03/18] Add implementation plan: finish sdk-tests Testcontainers migration Signed-off-by: Siri Varma Vegiraju --- ...nish-sdk-tests-testcontainers-migration.md | 421 ++++++++++++++++++ 1 file changed, 421 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-09-finish-sdk-tests-testcontainers-migration.md diff --git a/docs/superpowers/plans/2026-07-09-finish-sdk-tests-testcontainers-migration.md b/docs/superpowers/plans/2026-07-09-finish-sdk-tests-testcontainers-migration.md new file mode 100644 index 000000000..426e117de --- /dev/null +++ b/docs/superpowers/plans/2026-07-09-finish-sdk-tests-testcontainers-migration.md @@ -0,0 +1,421 @@ +# Finish sdk-tests Testcontainers Migration — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate the last 7 `sdk-tests` integration tests off the legacy `dapr run` CLI harness onto `BaseContainerIT`/`DaprContainer`, then strip the Dapr CLI / `dapr init` / host-Kafka / host-ToxiProxy steps from the CI `build` job. + +**Architecture:** Each test drops `extends BaseIT` and its `DaprRun`/`startDaprApp` usage, defining Dapr components in-code and running the sidecar in a `DaprContainer`. New reusable mechanics (`restartApp`, `restartSidecar`, shared placement/scheduler, ToxiProxy, Kafka) are added to `BaseContainerIT`/`SharedTestInfra`, each introduced in the first task that consumes it so it is exercised immediately. Once all 7 are migrated, the dead legacy harness and CI setup are deleted. + +**Tech Stack:** Java 17, JUnit 5, Testcontainers 2.0.5 (`org.testcontainers:testcontainers-*`, BOM-managed), `io.dapr:testcontainers-dapr` (`DaprContainer`, `DaprPlacementContainer`, `DaprSchedulerContainer`), Dapr runtime 1.18.0, Maven Failsafe (`integration-tests` profile). + +**Spec:** [docs/superpowers/specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md](../specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md) + +--- + +## Conventions for every task + +- **Reference patterns to copy** (already-migrated, working): + - Actor + app: [ActivationDeactivationIT.java](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActivationDeactivationIT.java) + - ToxiProxy + `DaprContainer`: [SdkResiliencyIT.java](../../../sdk-tests/src/test/java/io/dapr/it/resiliency/SdkResiliencyIT.java) + - Shared placement/scheduler multi-sidecar: [WorkflowsMultiAppCallActivityIT.java](../../../sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/multiapp/WorkflowsMultiAppCallActivityIT.java) +- **Base class API to use:** [BaseContainerIT.java](../../../sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java) — `daprBuilder(appName)`, `startAppAndAttach(name, serviceClass, protocol, daprFactory)`, `newDaprClient(dapr)`, `newActorClient(dapr)`, `redisStateStore(name)`, `waitForActorsReady(dapr)`, `deferStop(...)`, `deferClose(...)`, `DAPR_IMAGE`, `STATE_STORE_NAME`. +- **When rewriting a test:** preserve every assertion, `Thread.sleep`/`callWithRetry`/`Awaitility` wait and its duration, and the actor type / service class exactly as in the legacy file. Only the *setup/teardown* and *restart mechanics* change from `DaprRun` to `DaprContainer`. +- **Per-test verification command** (requires a running Docker daemon; Testcontainers uses it): + ```bash + cd /Users/svegiraju/Git/java-sdk && \ + ./mvnw -B -pl sdk-tests -Pintegration-tests -Dit.test= \ + dependency:copy-dependencies verify + ``` + Expected: `BUILD SUCCESS`, the named IT runs (not skipped, except `ActorSdkResiliencyIT` whose one test is `@Disabled`). If the local Docker daemon is unavailable, state that the check must run in CI and do not claim local success. +- **Commits:** sign off every commit (`git commit -s`). Do NOT add a Co-Authored-By trailer. +- **Timing-sensitive tests** (Tasks 2, 3, 4): after the first pass, run the verification command **3 times** and confirm all 3 pass before committing; if any flakes, escalate (do not loosen assertions without re-planning). + +--- + +## Task 1: Migrate ActorStateIT + add `restartApp` helper (Group A) + +**Files:** +- Modify: `sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java` (add `restartApp`) +- Rewrite: `sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java` + +**Legacy behavior to preserve** (read [ActorStateIT.java](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java)): single `writeReadState()` test. Phase 1 — build an `ActorProxy` for actor type `StatefulActorTest` (service class `StatefulActorService`), write string/`MyData`/name/empty-name/bytes state, reading each back. Phase 2 — **restart the app**, rebuild the proxy with the **same** `actorId`, and assert every previously-written value is still readable (proves state persisted to Redis). Keep all `callWithRetry` timeouts and the 5s/10s/2s waits. + +- [ ] **Step 1: Add `restartApp` to `BaseContainerIT`** (place after `startAppAndAttach`, before the client factories): + +```java + /** + * Restarts the app subprocess on its same pre-allocated port. The daprd + * container stays up and reconnects to the app via + * {@code host.testcontainers.internal:appPort}. Because {@link #daprBuilder} + * configures NO app-health-check, daprd does not deactivate actors during the + * gap, so in-memory timers survive (matching the legacy + * {@code @DaprRunConfig(enableAppHealthCheck=false)}). There is intentionally + * no sleep between stop and start — {@code ActorTimerRecoveryIT} relies on a + * quick restart. + */ + protected static void restartApp(AppRun app) throws Exception { + app.stop(); + app.start(); + } +``` + +- [ ] **Step 2: Rewrite `ActorStateIT`** to `extends BaseContainerIT`. Use the `ActivationDeactivationIT` `@BeforeAll` shape but with `StatefulActorService.class`: + +```java +var pair = startAppAndAttach( + "actor-state-it", + io.dapr.it.actors.services.springboot.StatefulActorService.class, + AppRun.AppProtocol.HTTP, + appPort -> daprBuilder("actor-state-it") + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(redisStateStore(STATE_STORE_NAME))); +dapr = pair.dapr(); +app = pair.app(); +waitForActorsReady(dapr); +``` + Build the proxy via `new ActorProxyBuilder("StatefulActorTest", ActorProxy.class, newActorClient(dapr))`. Replace the legacy "stop `DaprRun`, `startDaprApp` again" restart with `restartApp(app)`, then **rebuild the proxy off a fresh `newActorClient(dapr)`** with the same `actorId`. Remove all `BaseIT`/`DaprRun`/`startDaprApp` imports and usage. + +- [ ] **Step 3: Verify** — run the per-test command with `-Dit.test=ActorStateIT`. Expected: PASS, `writeReadState` runs and green. + +- [ ] **Step 4: Commit** +```bash +git add sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java \ + sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java +git commit -s -m "test: migrate ActorStateIT to Testcontainers (+ restartApp helper)" +``` + +--- + +## Task 2: Migrate ActorTimerRecoveryIT (Group A) + +**Files:** +- Rewrite: `sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java` + +**Legacy behavior to preserve** (read [ActorTimerRecoveryIT.java](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java)): single `timerRecoveryTest()`. Actor type `MyActorTest`, service class `MyActorService`. Register a timer (`startTimer("myTimer")`, callback `clock`, ~2s delay / 3s period, message `"ping!"`); wait for ≥3 fires (30s `callWithRetry`); **restart the app with NO sleep between stop and start**; assert the timer keeps firing (≥3 more, 30s `callWithRetry`); assert none of the new log entries equal old entries (proves real restart); `stopTimer("myTimer")` at the end. + +- [ ] **Step 1: Rewrite `ActorTimerRecoveryIT`** to `extends BaseContainerIT`, mirroring Task 1's `startAppAndAttach` setup but with `MyActorService.class` and app name `"actor-timer-recovery-it"`, actor type `MyActorTest`. Replace `runs.left.stop(); runs.left.start();` with `restartApp(app);` (the helper already omits any inter-step sleep). Keep every wait/assertion. + +- [ ] **Step 2: Verify (×3)** — run `-Dit.test=ActorTimerRecoveryIT` three times; all PASS. The timer must survive the restart (relies on daprBuilder setting no health-check — see the invariant note in the spec). If the timer is lost, escalate. + +- [ ] **Step 3: Commit** +```bash +git add sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java +git commit -s -m "test: migrate ActorTimerRecoveryIT to Testcontainers" +``` + +--- + +## Task 3: Migrate ActorReminderRecoveryIT + add `restartSidecar` helper (Group B) + +**Files:** +- Modify: `sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java` (add `restartSidecar`) +- Rewrite: `sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java` + +**Legacy behavior to preserve** (read [ActorReminderRecoveryIT.java](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java)): `@ParameterizedTest` over 4 data variants (String `"36"`, String `"\"my_text\""`, `byte[]{0,1}`, object `{"name":"abc","age":30}`) with actor types `MyActorTest`/`MyActorBinaryTest`/`MyActorObjectImpl`. Register reminder (fires ~every 2s); wait for ≥3 fires; **stop the daprd sidecar**; sleep 10s; **start the sidecar**; sleep 7s; assert the reminder resumed and fired ≥3 more times. Per the spec, the reminder lives in the scheduler container which survives the daprd restart. Keep the separate actor-client (build via `newActorClient(dapr)`, NOT a second app — see spec note). + +- [ ] **Step 1: Add `restartSidecar` to `BaseContainerIT`** (after `restartApp`): + +```java + /** + * Restarts the daprd container in place and re-waits for readiness. Placement + * and scheduler are NOT recreated on the second start (their DaprContainer + * fields are non-null), so a persisted actor reminder survives. Pinned host + * ports re-bind, so the app's DAPR_HTTP_PORT/DAPR_GRPC_PORT and any DaprClient + * remain valid. + */ + protected static void restartSidecar(DaprContainer dapr) { + dapr.stop(); + dapr.start(); + try (DaprClient client = newDaprClient(dapr)) { + client.waitForSidecar(30_000).block(); + } + waitForActorsReady(dapr); + } +``` + +- [ ] **Step 2: Rewrite `ActorReminderRecoveryIT`** to `extends BaseContainerIT`. Use `startAppAndAttach` with `MyActorService.class` (app name `"actor-reminder-recovery-it"`, `redisStateStore`). Build the reminder proxy(ies) via `newActorClient(dapr)`. Replace the legacy `runs.right.stop()` / `runs.right.start()` sidecar restart with `restartSidecar(dapr)`, keeping the surrounding 10s and 7s sleeps. Preserve the parameterized data and all assertions. + +- [ ] **Step 3: Verify (×3)** — run `-Dit.test=ActorReminderRecoveryIT` three times; all PASS across all 4 parameterized variants. If reminders do not survive the restart, escalate (fallback in spec: reminder in Redis-backed state). + +- [ ] **Step 4: Commit** +```bash +git add sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java \ + sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java +git commit -s -m "test: migrate ActorReminderRecoveryIT to Testcontainers (+ restartSidecar helper)" +``` + +--- + +## Task 4: Migrate ActorReminderFailoverIT + add shared placement/scheduler helpers (Group C) + +**Files:** +- Modify: `sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java` (add shared placement/scheduler helpers) +- Rewrite: `sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderFailoverIT.java` + +**Legacy behavior to preserve** (source shown above): `reminderRecoveryTest()` — two `MyActorService` app-sidecars (`One`, `Two`) hosting actor type `MyActorTest`, plus a **client-only** sidecar (no app) whose `ActorClient` builds the proxy. Register reminder `myReminder`; wait 7s; assert ≥3 fires; read the actor host via `getIdentifier()` (returns the host sidecar's `DAPR_HTTP_PORT`); **stop the `AppRun` whose sidecar port matches**; sleep 10s; sleep 10s more; assert ≥4 additional fires; assert `getIdentifier()` now returns a **different** host. `@AfterEach` calls `stopReminder`. + +**Topology:** failover is keyed on **actor type**, not app-id — placement distributes `MyActorTest` across every sidecar that registered it and rebalances when one host's app dies. So all sidecars must share ONE placement + ONE scheduler + Redis. + +- [ ] **Step 1: Add shared control-plane helpers to `BaseContainerIT`** (import `io.dapr.testcontainers.DaprPlacementContainer`, `io.dapr.testcontainers.DaprSchedulerContainer`, `io.dapr.testcontainers.DaprContainerConstants`): + +```java + /** Shared placement for multi-sidecar ITs. Explicit (not reuse-based) so it is + * deterministic on CI where Testcontainers reuse is disabled. */ + protected static DaprPlacementContainer startSharedPlacement() { + DaprPlacementContainer placement = + new DaprPlacementContainer(DaprContainerConstants.DAPR_PLACEMENT_IMAGE_TAG) + .withNetwork(SharedTestInfra.network()) + .withNetworkAliases("placement") + .withReuse(false); + placement.start(); + deferStop(placement); + return placement; + } + + /** Shared scheduler for multi-sidecar ITs (owns actor reminders). */ + protected static DaprSchedulerContainer startSharedScheduler() { + DaprSchedulerContainer scheduler = + new DaprSchedulerContainer(DaprContainerConstants.DAPR_SCHEDULER_IMAGE_TAG) + .withNetwork(SharedTestInfra.network()) + .withNetworkAliases("scheduler") + .withReuse(false); + scheduler.start(); + deferStop(scheduler); + return scheduler; + } +``` + +- [ ] **Step 2: Rewrite `ActorReminderFailoverIT`** to `extends BaseContainerIT`. In `@BeforeEach`: + 1. `DaprPlacementContainer placement = startSharedPlacement(); DaprSchedulerContainer scheduler = startSharedScheduler();` + 2. Start two actor hosts with `startAppAndAttach("failover-one" / "failover-two", MyActorService.class, HTTP, factory)` where each `factory` is: + ```java + appPort -> daprBuilder(name) + .withPlacementContainer(placement) + .withSchedulerContainer(scheduler) + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(redisStateStore(STATE_STORE_NAME)) + ``` + (Passing an explicit placement/scheduler makes `DaprContainer.configure()` skip creating its own — the shared ones are used.) + 3. Start a **client-only** sidecar (no app): build `new DaprContainer(DAPR_IMAGE).withAppName("failover-client").withNetwork(SharedTestInfra.network()).withPlacementContainer(placement).withSchedulerContainer(scheduler).withComponent(redisStateStore(STATE_STORE_NAME)).withDaprLogLevel(DaprLogLevel.DEBUG)`, `.start()`, `deferStop(...)`. Build the proxy via `newActorClient(clientSidecar)`. + 4. `waitForActorsReady` on both actor-host sidecars. + 5. Keep the `Thread.sleep(3000)` and actor id / type (`MyActorTest`). + In the test body, replace `firstAppRun.getHttpPort()` / `secondAppRun.getHttpPort()` comparisons with the two host sidecars' `dapr.getHttpPort()`, and `firstAppRun.stop()` with the matching `AppRun.stop()` (the `AppRun` from the corresponding `startAppAndAttach` pair). Preserve every wait/assertion. + +- [ ] **Step 3: Verify (×3)** — run `-Dit.test=ActorReminderFailoverIT` three times; all PASS (actor migrates to the surviving host and reminder continues). This is the highest-risk migration; if placement does not fail over across sidecars, STOP and escalate to re-plan rather than adjusting assertions. + +- [ ] **Step 4: Commit** +```bash +git add sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java \ + sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderFailoverIT.java +git commit -s -m "test: migrate ActorReminderFailoverIT to Testcontainers (+ shared placement/scheduler)" +``` + +--- + +## Task 5: Migrate WaitForSidecarIT + add `newToxiproxy` helper (Group D) + +**Files:** +- Modify: `sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java` (add `newToxiproxy`) +- Rewrite: `sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java` + +**Legacy behavior to preserve** (read [WaitForSidecarIT.java](../../../sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java) and copy the ToxiProxy wiring from [SdkResiliencyIT.java](../../../sdk-tests/src/test/java/io/dapr/it/resiliency/SdkResiliencyIT.java)): four tests — `waitSucceeds()` (direct sidecar, `waitForSidecar(5000)` OK), `waitTimeout()` (5s latency toxic, timeout 4900ms → `RuntimeException`, duration ≥ timeout), `waitSlow()` (timeout 5100ms → OK, duration ≥ 5s), `waitNotRunningTimeout()` (unavailable sidecar, timeout 5000ms → `RuntimeException`). Latency constant = 5s. No app needed. + +- [ ] **Step 1: Add `newToxiproxy` to `BaseContainerIT`** (import `org.testcontainers.containers.ToxiproxyContainer` and `io.dapr.it.testcontainers.ContainerConstants`): + +```java + /** Starts a Toxiproxy container on the shared network and registers it for + * @AfterAll cleanup. Callers create proxies via a ToxiproxyClient against + * {@code getHost()}/{@code getControlPort()} and point clients at the mapped + * proxy listen port. Mirrors SdkResiliencyIT. */ + protected static ToxiproxyContainer newToxiproxy() { + ToxiproxyContainer toxiproxy = + new ToxiproxyContainer(ContainerConstants.TOXI_PROXY_IMAGE_TAG) + .withNetwork(SharedTestInfra.network()); + toxiproxy.start(); + deferStop(toxiproxy); + return toxiproxy; + } +``` + +- [ ] **Step 2: Rewrite `WaitForSidecarIT`** to `extends BaseContainerIT`. In `@BeforeAll`: start a `DaprContainer` via `daprBuilder("wait-for-sidecar-it").withNetworkAliases("dapr")` (in-memory kvstore is fine, no app), `deferStop`; `newToxiproxy()`; create a proxy `dapr:3500`/`dapr:50001` via `ToxiproxyClient` (see `SdkResiliencyIT` lines 122-123). Build the "latency" client pointed at `toxiproxy.getMappedPort()` and the direct client via `newDaprClient(dapr)`. For `waitNotRunningTimeout`, point a client at an unbound/stopped endpoint (e.g. a proxy with no upstream, or a stopped container's former mapped port). Add/remove the 5s latency toxic per test as in `SdkResiliencyIT`. Preserve all four tests, their timeouts, and duration assertions. + +- [ ] **Step 3: Verify** — run `-Dit.test=WaitForSidecarIT`. Expected: PASS, all 4 tests run. + +- [ ] **Step 4: Commit** +```bash +git add sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java \ + sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java +git commit -s -m "test: migrate WaitForSidecarIT to Testcontainers (+ newToxiproxy helper)" +``` + +--- + +## Task 6: Migrate ActorSdkResiliencyIT (Group D — stays @Disabled) + +**Files:** +- Rewrite: `sdk-tests/src/test/java/io/dapr/it/actors/ActorSdkResiliencyIT.java` + +**Legacy behavior to preserve** (read [ActorSdkResiliencyIT.java](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorSdkResiliencyIT.java)): one `@Test retryAndTimeout()` that is `@Disabled("Flaky when running on GitHub actions")`. Actor type `DemoActorTest`, service class `DemoActorService`. It routes an `ActorClient` (with `ResiliencyOptions`) through ToxiProxy latency/jitter and compares error counts across retry configs. **Keep the test `@Disabled`** — the goal is only to remove the `ToxiProxyRun` dependency, not to re-enable it. + +- [ ] **Step 1: Rewrite `ActorSdkResiliencyIT`** to `extends BaseContainerIT`. Combine the actor pattern (`startAppAndAttach` with `DemoActorService.class`, app name `"actor-sdk-resiliency-it"`) with the ToxiProxy wiring from Task 5 (`newToxiproxy()` + a proxy to `dapr:50001`). Build the resilient `ActorClient`s against the ToxiProxy mapped gRPC port using `new ActorClient(new Properties(overrides), resiliencyOptions)` where `overrides` map `GRPC_ENDPOINT`/`GRPC_PORT` to the proxy. Remove `BaseIT`/`DaprRun`/`ToxiProxyRun` imports and usage. Preserve the `@Disabled` annotation and the test body's assertions. + +- [ ] **Step 2: Verify** — run `-Dit.test=ActorSdkResiliencyIT`. Expected: `BUILD SUCCESS`; the class compiles and its one test is reported **skipped** (still `@Disabled`). This confirms the migration compiles and no longer references the legacy harness. + +- [ ] **Step 3: Commit** +```bash +git add sdk-tests/src/test/java/io/dapr/it/actors/ActorSdkResiliencyIT.java +git commit -s -m "test: migrate ActorSdkResiliencyIT off ToxiProxyRun (kept @Disabled)" +``` + +--- + +## Task 7: Migrate BindingIT + Kafka container (Group E) + +**Files:** +- Modify: `sdk-tests/pom.xml` (add `testcontainers-kafka`) +- Modify: `sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java` (add `kafka()`) +- Modify: `sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java` (add `kafkaBinding` + `httpBinding` component helpers) +- Rewrite: `sdk-tests/src/test/java/io/dapr/it/binding/http/BindingIT.java` + +**Legacy behavior to preserve** (read [BindingIT.java](../../../sdk-tests/src/test/java/io/dapr/it/binding/http/BindingIT.java)): three tests. `httpOutputBindingError()` — invoke `github-http-binding-404` → expect `DaprException` 404. `httpOutputBindingErrorIgnoredByComponent()` — invoke `github-http-binding-404-success` (`errorIfNot2XX=false`) → expect the GitHub 404 JSON payload. `inputOutputBinding()` — Kafka component `sample123`, service class `InputBindingService`: health-check the input binding, publish a `MyClass{message="hello"}` and a string `"cat"` via the output binding, then GET `/messages` and assert both arrive. Keep the GitHub URLs unchanged (spec D3). The two HTTP components are `bindings.http` (URL `https://api.github.com/unknown_path`; the `-success` one adds `errorIfNot2XX: "false"`); the Kafka component is `bindings.kafka` with `topics`/`publishTopic` = `topic-{appID}`, `consumerGroup` = `{appID}`, `authRequired=false`, `initialOffset=oldest`. + +- [ ] **Step 1: Add the Kafka dependency** to `sdk-tests/pom.xml` (in ``, no `` — managed by the root `testcontainers-bom`): +```xml + + org.testcontainers + testcontainers-kafka + test + +``` + +- [ ] **Step 2: Add `kafka()` to `SharedTestInfra`** (mirror the `redis()`/`mongo()` lazy-singleton style; use `org.testcontainers.kafka.KafkaContainer` with an in-network listener so the daprd container can reach it): +```java + private static final String KAFKA_NETWORK_ALIAS = "kafka"; + private static volatile org.testcontainers.kafka.KafkaContainer kafka; + + public static synchronized org.testcontainers.kafka.KafkaContainer kafka() { + if (kafka == null) { + kafka = new org.testcontainers.kafka.KafkaContainer("apache/kafka:3.8.0") + .withNetwork(network()) + .withNetworkAliases(KAFKA_NETWORK_ALIAS) + .withListener(KAFKA_NETWORK_ALIAS + ":19092") // advertised on the shared network + .withReuse(true); + kafka.start(); + } + return kafka; + } + + /** Broker address reachable from other containers on the shared network. */ + public static String kafkaInternalBroker() { + return KAFKA_NETWORK_ALIAS + ":19092"; + } +``` + > If the Testcontainers 2.0.5 `KafkaContainer` API for the in-network listener differs from `.withListener(String)`, consult the Testcontainers Kafka docs and adjust; the requirement is a broker advertised as `kafka:19092` on `SharedTestInfra.network()`. Do not guess silently — verify against the resolved API. + +- [ ] **Step 3: Add component helpers to `BaseContainerIT`**: +```java + protected static Component kafkaBinding(String name) { + SharedTestInfra.kafka(); + return new Component(name, "bindings.kafka", "v1", Map.of( + "brokers", SharedTestInfra.kafkaInternalBroker(), + "topics", "topic-{appID}", + "publishTopic", "topic-{appID}", + "consumerGroup", "{appID}", + "authRequired", "false", + "initialOffset", "oldest" + )); + } + + protected static Component httpBinding(String name, String url, boolean errorIfNot2xx) { + return new Component(name, "bindings.http", "v1", Map.of( + "url", url, + "errorIfNot2XX", Boolean.toString(errorIfNot2xx) + )); + } +``` + +- [ ] **Step 4: Rewrite `BindingIT`** to `extends BaseContainerIT`. For the two HTTP tests, start a `DaprContainer` (no app) via `daprBuilder(...)` with `.withComponent(httpBinding("github-http-binding-404", "https://api.github.com/unknown_path", true))` (and the `-success` variant with `false`); invoke via `newDaprClient(dapr)`. For `inputOutputBinding`, use `startAppAndAttach("bindingit-grpc", InputBindingService.class, HTTP, factory)` where the factory adds `.withComponent(kafkaBinding("sample123"))`; keep the health-check-then-publish-then-read flow and both assertions. Remove all `BaseIT`/`DaprRun`/`startDaprApp`/file-component usage. + +- [ ] **Step 5: Verify** — run `-Dit.test=BindingIT`. Expected: PASS, all 3 tests run (the two HTTP tests require outbound network to api.github.com, as before). + +- [ ] **Step 6: Commit** +```bash +git add sdk-tests/pom.xml \ + sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java \ + sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java \ + sdk-tests/src/test/java/io/dapr/it/binding/http/BindingIT.java +git commit -s -m "test: migrate BindingIT to Testcontainers Kafka + in-code bindings" +``` + +--- + +## Task 8: Strip legacy setup from the CI `build` job + +**Files:** +- Modify: `.github/workflows/build.yml` (the `build` job only) + +- [ ] **Step 1: Edit `build.yml`** — in the `build` job (`name: "Build jdk:… sb:…"`), remove these steps and env: "Set up Dapr CLI"; the Go setup + `dapr/cli` & `dapr/dapr` checkout/override/placement steps (all `DAPR_REF`/`DAPR_CLI_REF` conditionals); "Uninstall Dapr runtime" + "Ensure Dapr runtime uninstalled" wait loop; "Initialize Dapr runtime"; "Spin local environment" (`docker compose … local-test.yml up -d kafka`); "Install local ToxiProxy"; and the now-unused env keys `GOVER`/`GOOS`/`GOARCH`/`GOPROXY`/`DAPR_CLI_VER`/`DAPR_RUNTIME_VER`/`DAPR_INSTALL_URL`/`DAPR_CLI_REF`/`DAPR_REF`/`TOXIPROXY_URL`. **Keep:** checkout, `docker version`, setup-java, `./mvnw clean install -DskipTests`, the sb 3.x / sb 4.x integration-test run steps, and all failsafe/surefire report-upload steps. Do NOT touch the `test`, `build-durabletask`, or `publish` jobs. + +- [ ] **Step 2: Verify YAML** — `yamllint .github/workflows/build.yml` if available, else confirm the `build` job still has: checkout → setup-java → `mvnw clean install -DskipTests` → integration-test run → report uploads, and references no removed env var (grep for `DAPR_CLI_VER`, `TOXIPROXY_URL`, `local-test.yml`, `dapr init`, `dapr uninstall` → expect no matches in `build.yml`). + +- [ ] **Step 3: Commit** +```bash +git add .github/workflows/build.yml +git commit -s -m "ci: drop Dapr CLI/init/Kafka/ToxiProxy setup from build job" +``` + +--- + +## Task 9: Delete dead legacy harness + full-module verify + +**Files:** +- Delete: `sdk-tests/src/test/java/io/dapr/it/BaseIT.java`, `DaprRun.java`, `DaprRunConfig.java`, `ToxiProxyRun.java` +- Delete: `sdk-tests/components/kafka_bindings.yaml`, `sdk-tests/components/http_binding.yaml`, `sdk-tests/deploy/local-test.yml` +- **Keep:** `AppRun.java`, `DaprPorts.java`, `Command.java`, `Stoppable.java`, `SharedTestInfra.java`, `BaseContainerIT.java` + +- [ ] **Step 1: Confirm zero references** before deleting each file: +```bash +cd /Users/svegiraju/Git/java-sdk +for c in BaseIT DaprRun DaprRunConfig ToxiProxyRun; do + echo "== $c =="; grep -rn "\b$c\b" sdk-tests/src --include=*.java | grep -v "/$c.java:" || echo " (no refs)" +done +``` + Expected: `(no refs)` for all four. If any reference remains, that test was not fully migrated — fix it before deleting. Also grep for remaining consumers of the two YAMLs / `local-test.yml`: +```bash +grep -rn "kafka_bindings\|http_binding\|local-test.yml" sdk-tests .github || echo "(no refs)" +``` + +- [ ] **Step 2: Delete the files** (only after Step 1 is clean): +```bash +git rm sdk-tests/src/test/java/io/dapr/it/BaseIT.java \ + sdk-tests/src/test/java/io/dapr/it/DaprRun.java \ + sdk-tests/src/test/java/io/dapr/it/DaprRunConfig.java \ + sdk-tests/src/test/java/io/dapr/it/ToxiProxyRun.java \ + sdk-tests/components/kafka_bindings.yaml \ + sdk-tests/components/http_binding.yaml \ + sdk-tests/deploy/local-test.yml +``` + +- [ ] **Step 3: Full-module verify** — build the whole SDK then run every `sdk-tests` IT with the trimmed setup (requires Docker): +```bash +cd /Users/svegiraju/Git/java-sdk +./mvnw clean install -B -q -DskipTests && \ +./mvnw -B -pl sdk-tests -Pintegration-tests dependency:copy-dependencies verify +``` + Expected: `BUILD SUCCESS`; all 20 sdk-tests ITs run (the one `ActorSdkResiliencyIT` test skipped); no compilation error from the deletions. If Docker is unavailable locally, state that this must be validated in CI and push the branch. + +- [ ] **Step 4: Commit** +```bash +git add -A +git commit -s -m "test: remove dead dapr-run harness, binding YAMLs, and local-test.yml" +``` + +--- + +## Done criteria + +- All 7 ITs extend `BaseContainerIT`; no `sdk-tests` file references `BaseIT`/`DaprRun`/`DaprRunConfig`/`ToxiProxyRun`. +- `build.yml`'s `build` job installs no Dapr CLI and runs no `dapr init`/`dapr uninstall`/host Kafka/host ToxiProxy. +- Full `sdk-tests` `verify` is green in CI; the `build sb:3.5.x` job completes materially faster than 26 min (target ≈ the sb:4.0.x job) and well under the 45-min timeout — satisfying issue #1522's `<20 min` acceptance criterion. From db483be41bb78f77651eaa76016bdece203efeab Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 17:48:58 -0700 Subject: [PATCH 04/18] Refine plan per review: Toxiproxy import/docstring, ActorObject actor type name Signed-off-by: Siri Varma Vegiraju --- ...26-07-09-finish-sdk-tests-testcontainers-migration.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/plans/2026-07-09-finish-sdk-tests-testcontainers-migration.md b/docs/superpowers/plans/2026-07-09-finish-sdk-tests-testcontainers-migration.md index 426e117de..111779dab 100644 --- a/docs/superpowers/plans/2026-07-09-finish-sdk-tests-testcontainers-migration.md +++ b/docs/superpowers/plans/2026-07-09-finish-sdk-tests-testcontainers-migration.md @@ -112,7 +112,7 @@ git commit -s -m "test: migrate ActorTimerRecoveryIT to Testcontainers" - Modify: `sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java` (add `restartSidecar`) - Rewrite: `sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java` -**Legacy behavior to preserve** (read [ActorReminderRecoveryIT.java](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java)): `@ParameterizedTest` over 4 data variants (String `"36"`, String `"\"my_text\""`, `byte[]{0,1}`, object `{"name":"abc","age":30}`) with actor types `MyActorTest`/`MyActorBinaryTest`/`MyActorObjectImpl`. Register reminder (fires ~every 2s); wait for ≥3 fires; **stop the daprd sidecar**; sleep 10s; **start the sidecar**; sleep 7s; assert the reminder resumed and fired ≥3 more times. Per the spec, the reminder lives in the scheduler container which survives the daprd restart. Keep the separate actor-client (build via `newActorClient(dapr)`, NOT a second app — see spec note). +**Legacy behavior to preserve** (read [ActorReminderRecoveryIT.java](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java)): `@ParameterizedTest` over 4 data variants (String `"36"`, String `"\"my_text\""`, `byte[]{0,1}`, object `{"name":"abc","age":30}`) with actor types `MyActorTest`/`MyActorBinaryTest`/`MyActorObjectTest`. Register reminder (fires ~every 2s); wait for ≥3 fires; **stop the daprd sidecar**; sleep 10s; **start the sidecar**; sleep 7s; assert the reminder resumed and fired ≥3 more times. Per the spec, the reminder lives in the scheduler container which survives the daprd restart. Keep the separate actor-client (build via `newActorClient(dapr)`, NOT a second app — see spec note). - [ ] **Step 1: Add `restartSidecar` to `BaseContainerIT`** (after `restartApp`): @@ -222,13 +222,14 @@ git commit -s -m "test: migrate ActorReminderFailoverIT to Testcontainers (+ sha **Legacy behavior to preserve** (read [WaitForSidecarIT.java](../../../sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java) and copy the ToxiProxy wiring from [SdkResiliencyIT.java](../../../sdk-tests/src/test/java/io/dapr/it/resiliency/SdkResiliencyIT.java)): four tests — `waitSucceeds()` (direct sidecar, `waitForSidecar(5000)` OK), `waitTimeout()` (5s latency toxic, timeout 4900ms → `RuntimeException`, duration ≥ timeout), `waitSlow()` (timeout 5100ms → OK, duration ≥ 5s), `waitNotRunningTimeout()` (unavailable sidecar, timeout 5000ms → `RuntimeException`). Latency constant = 5s. No app needed. -- [ ] **Step 1: Add `newToxiproxy` to `BaseContainerIT`** (import `org.testcontainers.containers.ToxiproxyContainer` and `io.dapr.it.testcontainers.ContainerConstants`): +- [ ] **Step 1: Add `newToxiproxy` to `BaseContainerIT`** (import `org.testcontainers.toxiproxy.ToxiproxyContainer` — the same class `SdkResiliencyIT` uses, paired with the `eu.rekawek.toxiproxy.ToxiproxyClient` wiring — and `io.dapr.it.testcontainers.ContainerConstants`): ```java /** Starts a Toxiproxy container on the shared network and registers it for * @AfterAll cleanup. Callers create proxies via a ToxiproxyClient against - * {@code getHost()}/{@code getControlPort()} and point clients at the mapped - * proxy listen port. Mirrors SdkResiliencyIT. */ + * {@code getHost()}/{@code getControlPort()} (the control channel), then point + * Dapr clients at {@code getMappedPort()} of a proxy created on a + * fixed listen port (e.g. 8666). Mirrors SdkResiliencyIT. */ protected static ToxiproxyContainer newToxiproxy() { ToxiproxyContainer toxiproxy = new ToxiproxyContainer(ContainerConstants.TOXI_PROXY_IMAGE_TAG) From 3612e87707fe7851765052f6a522bf9e691a5ead Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 18:04:37 -0700 Subject: [PATCH 05/18] test: migrate ActorStateIT to Testcontainers (+ restartApp helper) Signed-off-by: Siri Varma Vegiraju --- .../java/io/dapr/it/actors/ActorStateIT.java | 57 ++++++++++--------- .../dapr/it/containers/BaseContainerIT.java | 15 +++++ 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java b/sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java index ffd5d3c3d..991fbca57 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java @@ -8,7 +8,7 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and -limitations under the License. + * limitations under the License. */ package io.dapr.it.actors; @@ -16,10 +16,12 @@ import io.dapr.actors.ActorId; import io.dapr.actors.client.ActorProxy; import io.dapr.actors.client.ActorProxyBuilder; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; +import io.dapr.it.AppRun; import io.dapr.it.actors.services.springboot.StatefulActor; import io.dapr.it.actors.services.springboot.StatefulActorService; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.DaprContainer; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,21 +31,30 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -public class ActorStateIT extends BaseIT { +public class ActorStateIT extends BaseContainerIT { private static Logger logger = LoggerFactory.getLogger(ActorStateIT.class); + private static DaprContainer dapr; + private static AppRun app; + + @BeforeAll + public static void start() throws Exception { + var pair = startAppAndAttach( + "actor-state-it", + StatefulActorService.class, + AppRun.AppProtocol.HTTP, + appPort -> daprBuilder("actor-state-it") + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(redisStateStore(STATE_STORE_NAME))); + dapr = pair.dapr(); + app = pair.app(); + waitForActorsReady(dapr); + } + @Test public void writeReadState() throws Exception { - logger.debug("Starting actor runtime ..."); - // The call below will fail if service cannot start successfully. - DaprRun run = startDaprApp( - this.getClass().getSimpleName(), - StatefulActorService.SUCCESS_MESSAGE, - StatefulActorService.class, - true, - 60000); - String message = "This is a message to be saved and retrieved."; String name = "Jon Doe"; byte[] bytes = new byte[] { 0x1 }; @@ -52,12 +63,12 @@ public void writeReadState() throws Exception { String actorType = "StatefulActorTest"; logger.debug("Building proxy ..."); ActorProxyBuilder proxyBuilder = - new ActorProxyBuilder(actorType, ActorProxy.class, deferClose(run.newActorClient())); + new ActorProxyBuilder(actorType, ActorProxy.class, newActorClient(dapr)); ActorProxy proxy = proxyBuilder.build(actorId); // waiting for actor to be activated Thread.sleep(5000); - + // Validate conditional read works. callWithRetry(() -> { logger.debug("Invoking readMessage where data is not present yet ... "); @@ -126,23 +137,15 @@ public void writeReadState() throws Exception { logger.debug("Waiting, so actor can be deactivated ..."); Thread.sleep(10000); - logger.debug("Stopping service ..."); - run.stop(); - - logger.debug("Starting service ..."); - DaprRun run2 = startDaprApp( - this.getClass().getSimpleName(), - StatefulActorService.SUCCESS_MESSAGE, - StatefulActorService.class, - true, - 60000); + logger.debug("Restarting app ..."); + restartApp(app); // Need new proxy builder because the proxy builder holds the channel. - proxyBuilder = new ActorProxyBuilder(actorType, ActorProxy.class, deferClose(run2.newActorClient())); + proxyBuilder = new ActorProxyBuilder(actorType, ActorProxy.class, newActorClient(dapr)); ActorProxy newProxy = proxyBuilder.build(actorId); // waiting for actor to be activated - Thread.sleep(2000); + Thread.sleep(2000); callWithRetry(() -> { logger.debug("Invoking readMessage where data is not cached ... "); diff --git a/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java index e6ec233c4..5d2ee2682 100644 --- a/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java @@ -187,6 +187,21 @@ protected static void waitForActorsReady(DaprContainer dapr) { DaprWait.forActors().waitUntilReady(dapr); } + /** + * Restarts the app subprocess on its same pre-allocated port. The daprd + * container stays up and reconnects to the app via + * {@code host.testcontainers.internal:appPort}. Because {@link #daprBuilder} + * configures NO app-health-check, daprd does not deactivate actors during the + * gap, so in-memory timers survive (matching the legacy + * {@code @DaprRunConfig(enableAppHealthCheck=false)}). There is intentionally + * no sleep between stop and start — {@code ActorTimerRecoveryIT} relies on a + * quick restart. + */ + protected static void restartApp(AppRun app) throws Exception { + app.stop(); + app.start(); + } + // ---------- DaprClient / ActorClient factories ---------- protected static DaprClient newDaprClient(DaprContainer dapr) { From dda75203e14747dd3d4f0c8fd38c381378a82856 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 18:12:34 -0700 Subject: [PATCH 06/18] test: migrate ActorTimerRecoveryIT to Testcontainers Signed-off-by: Siri Varma Vegiraju --- .../dapr/it/actors/ActorTimerRecoveryIT.java | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java b/sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java index 21910376f..273a72655 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java @@ -8,7 +8,7 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and -limitations under the License. + * limitations under the License. */ package io.dapr.it.actors; @@ -17,10 +17,10 @@ import io.dapr.actors.client.ActorProxy; import io.dapr.actors.client.ActorProxyBuilder; import io.dapr.it.AppRun; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; import io.dapr.it.actors.app.MyActorService; -import org.apache.commons.lang3.tuple.ImmutablePair; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.DaprContainer; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,30 +35,41 @@ import static io.dapr.it.actors.MyActorTestUtils.validateMethodCalls; import static org.junit.jupiter.api.Assertions.assertNotEquals; -public class ActorTimerRecoveryIT extends BaseIT { +public class ActorTimerRecoveryIT extends BaseContainerIT { private static final Logger logger = LoggerFactory.getLogger(ActorTimerRecoveryIT.class); private static final String METHOD_NAME = "clock"; + private static DaprContainer dapr; + private static AppRun app; + + @BeforeAll + public static void start() throws Exception { + var pair = startAppAndAttach( + "actor-timer-recovery-it", + MyActorService.class, + AppRun.AppProtocol.HTTP, + appPort -> daprBuilder("actor-timer-recovery-it") + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(redisStateStore(STATE_STORE_NAME))); + dapr = pair.dapr(); + app = pair.app(); + waitForActorsReady(dapr); + } + /** * Create an actor, register a timer, validates its content, restarts the Actor and confirms timer continues. * @throws Exception This test is not expected to throw. Thrown exceptions are bugs. */ @Test public void timerRecoveryTest() throws Exception { - ImmutablePair runs = startSplitDaprAndApp( - ActorTimerRecoveryIT.class.getSimpleName(), - "Started MyActorService", - MyActorService.class, - true, - 60000); - - String actorType="MyActorTest"; + String actorType = "MyActorTest"; logger.debug("Creating proxy builder"); ActorProxyBuilder proxyBuilder = - new ActorProxyBuilder(actorType, ActorProxy.class, deferClose(runs.right.newActorClient())); + new ActorProxyBuilder(actorType, ActorProxy.class, newActorClient(dapr)); logger.debug("Creating actorId"); ActorId actorId = new ActorId(UUID.randomUUID().toString()); logger.debug("Building proxy"); @@ -77,11 +88,10 @@ public void timerRecoveryTest() throws Exception { }, 30000); // Restarts app only. - runs.left.stop(); // Cannot sleep between app's stop and start since it can trigger unhealthy actor in runtime and lose timers. // Timers will survive only if the restart is "quick" and survives the runtime's actor health check. // Starting in 1.13, sidecar is more sensitive to an app restart and will not keep actors active for "too long". - runs.left.start(); + restartApp(app); final List newLogs = new ArrayList<>(); callWithRetry(() -> { From 9998fb4521eb986b5d85d83719d8a583ffcdb611 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 18:32:46 -0700 Subject: [PATCH 07/18] test: migrate ActorReminderRecoveryIT to Testcontainers (+ restartSidecar helper) Signed-off-by: Siri Varma Vegiraju --- .../it/actors/ActorReminderRecoveryIT.java | 54 +++++++++---------- .../dapr/it/containers/BaseContainerIT.java | 16 ++++++ 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java b/sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java index 4ea6a759f..fbfa8fce4 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java @@ -8,7 +8,7 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and -limitations under the License. + * limitations under the License. */ package io.dapr.it.actors; @@ -17,12 +17,12 @@ import io.dapr.actors.client.ActorProxy; import io.dapr.actors.client.ActorProxyBuilder; import io.dapr.it.AppRun; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; import io.dapr.it.actors.app.ActorReminderDataParam; import io.dapr.it.actors.app.MyActorService; -import org.apache.commons.lang3.tuple.ImmutablePair; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.DaprContainer; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -41,12 +41,15 @@ import static io.dapr.it.actors.MyActorTestUtils.validateMessageContent; import static io.dapr.it.actors.MyActorTestUtils.validateMethodCalls; -public class ActorReminderRecoveryIT extends BaseIT { +public class ActorReminderRecoveryIT extends BaseContainerIT { private static final Logger logger = LoggerFactory.getLogger(ActorReminderRecoveryIT.class); private static final String METHOD_NAME = "receiveReminder"; + private static DaprContainer dapr; + private static AppRun app; + /** * Parameters for this test. * Param #1: useGrpc. @@ -81,29 +84,27 @@ public static Stream data() { private ActorProxy proxy; - private ImmutablePair runs; - - private DaprRun clientRun; - - public void setup(String actorType) throws Exception { - runs = startSplitDaprAndApp( - ActorReminderRecoveryIT.class.getSimpleName(), - "Started MyActorService", + @BeforeAll + public static void start() throws Exception { + var pair = startAppAndAttach( + "actor-reminder-recovery-it", MyActorService.class, - true, - 60000); - - // Run that will stay up for integration tests. - // appId must not contain the appId from the other run, otherwise ITs will not run properly. - clientRun = startDaprApp("ActorReminderRecoveryTestClient", 5000); - - Thread.sleep(3000); + AppRun.AppProtocol.HTTP, + appPort -> daprBuilder("actor-reminder-recovery-it") + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(redisStateStore(STATE_STORE_NAME))); + dapr = pair.dapr(); + app = pair.app(); + waitForActorsReady(dapr); + } + public void setup(String actorType) { ActorId actorId = new ActorId(UUID.randomUUID().toString()); logger.debug("Creating proxy builder"); ActorProxyBuilder proxyBuilder = - new ActorProxyBuilder(actorType, ActorProxy.class, deferClose(clientRun.newActorClient())); + new ActorProxyBuilder(actorType, ActorProxy.class, newActorClient(dapr)); logger.debug("Creating actorId"); logger.debug("Building proxy"); proxy = proxyBuilder.build(actorId); @@ -144,16 +145,13 @@ public void reminderRecoveryTest( }, 30000); // Restarts runtime only. - logger.info("Stopping Dapr sidecar"); - runs.right.stop(); - // Pause a bit to let placements settle. logger.info("Pausing 10 seconds to let placements settle."); Thread.sleep(Duration.ofSeconds(10).toMillis()); - logger.info("Starting Dapr sidecar"); - runs.right.start(); - logger.info("Dapr sidecar started"); + logger.info("Restarting Dapr sidecar"); + restartSidecar(dapr); + logger.info("Dapr sidecar restarted"); logger.info("Pausing 7 seconds to allow sidecar to be healthy"); Thread.sleep(7000); diff --git a/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java index 5d2ee2682..53e932e5a 100644 --- a/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java @@ -202,6 +202,22 @@ protected static void restartApp(AppRun app) throws Exception { app.start(); } + /** + * Restarts the daprd container in place and re-waits for readiness. Placement + * and scheduler are NOT recreated on the second start (their DaprContainer + * fields are non-null), so a persisted actor reminder survives. Pinned host + * ports re-bind, so the app's DAPR_HTTP_PORT/DAPR_GRPC_PORT and any DaprClient + * remain valid. + */ + protected static void restartSidecar(DaprContainer dapr) throws Exception { + dapr.stop(); + dapr.start(); + try (DaprClient client = newDaprClient(dapr)) { + client.waitForSidecar(30_000).block(); + } + waitForActorsReady(dapr); + } + // ---------- DaprClient / ActorClient factories ---------- protected static DaprClient newDaprClient(DaprContainer dapr) { From ab305a30e29c6236dc8a38ee5a34f59bf5e673bf Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 18:54:34 -0700 Subject: [PATCH 08/18] test: migrate ActorReminderFailoverIT to Testcontainers (+ shared placement/scheduler) Signed-off-by: Siri Varma Vegiraju --- .../it/actors/ActorReminderFailoverIT.java | 104 +++++++++++++----- .../dapr/it/containers/BaseContainerIT.java | 30 +++++ 2 files changed, 104 insertions(+), 30 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderFailoverIT.java b/sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderFailoverIT.java index 8e7b64c93..44984d52c 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderFailoverIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderFailoverIT.java @@ -8,19 +8,20 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and -limitations under the License. + * limitations under the License. */ package io.dapr.it.actors; import io.dapr.actors.ActorId; -import io.dapr.actors.client.ActorClient; import io.dapr.actors.client.ActorProxy; import io.dapr.actors.client.ActorProxyBuilder; -import io.dapr.config.Properties; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; +import io.dapr.it.AppRun; import io.dapr.it.actors.app.MyActorService; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprPlacementContainer; +import io.dapr.testcontainers.DaprSchedulerContainer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,46 +36,82 @@ import static io.dapr.it.actors.MyActorTestUtils.validateMethodCalls; import static org.junit.jupiter.api.Assertions.assertNotEquals; -public class ActorReminderFailoverIT extends BaseIT { +/** + * Verifies that actor reminders fail over to a surviving sidecar when the sidecar + * currently hosting the actor is killed. Actor failover is keyed on actor type + * ("MyActorTest"): placement distributes actors of a type across every sidecar that + * registered that type, and rebalances when one host's app dies. This test starts two + * actor-hosting app+sidecar pairs plus a third, client-only sidecar (no app) whose + * ActorClient builds the proxy; all three share one placement, one scheduler, and Redis. + */ +public class ActorReminderFailoverIT extends BaseContainerIT { - private static Logger logger = LoggerFactory.getLogger(ActorReminderFailoverIT.class); + private static final Logger logger = LoggerFactory.getLogger(ActorReminderFailoverIT.class); private static final String METHOD_NAME = "receiveReminder"; - private ActorProxy proxy; + private static final String ACTOR_TYPE = "MyActorTest"; + + private DaprPlacementContainer placement; + + private DaprSchedulerContainer scheduler; - private DaprRun firstAppRun; + private DaprAndApp one; - private DaprRun secondAppRun; + private DaprAndApp two; - private DaprRun clientAppRun; + private DaprContainer client; + + private ActorProxy proxy; @BeforeEach public void init() throws Exception { - firstAppRun = startDaprApp( - ActorReminderFailoverIT.class.getSimpleName() + "One", - "Started MyActorService", + placement = startSharedPlacement(); + scheduler = startSharedScheduler(); + + one = startAppAndAttach( + "actor-reminder-failover-it-one", MyActorService.class, - true, - 60000); - secondAppRun = startDaprApp( - ActorReminderFailoverIT.class.getSimpleName() + "Two", - "Started MyActorService", + AppRun.AppProtocol.HTTP, + appPort -> daprBuilder("actor-reminder-failover-it-one") + .withPlacementContainer(placement) + .withSchedulerContainer(scheduler) + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(redisStateStore(STATE_STORE_NAME))); + + two = startAppAndAttach( + "actor-reminder-failover-it-two", MyActorService.class, - true, - 60000); - clientAppRun = startDaprApp( - ActorReminderFailoverIT.class.getSimpleName() + "Client", - 60000); + AppRun.AppProtocol.HTTP, + appPort -> daprBuilder("actor-reminder-failover-it-two") + .withPlacementContainer(placement) + .withSchedulerContainer(scheduler) + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(redisStateStore(STATE_STORE_NAME))); + + client = daprBuilder("actor-reminder-failover-it-client") + .withPlacementContainer(placement) + .withSchedulerContainer(scheduler) + .withComponent(redisStateStore(STATE_STORE_NAME)); + client.start(); + deferStop(client); + + // Prove both actor hosts have registered MyActorTest with the shared placement + // service before exercising failover. startAppAndAttach's waitForSidecar only proves + // daprd's gRPC channel is up, not that actor types are registered -- a fixed sleep + // alone is a flakiness risk under CI load. + waitForActorsReady(one.dapr()); + waitForActorsReady(two.dapr()); Thread.sleep(3000); ActorId actorId = new ActorId(UUID.randomUUID().toString()); - String actorType="MyActorTest"; logger.debug("Creating proxy builder"); ActorProxyBuilder proxyBuilder = - new ActorProxyBuilder(actorType, ActorProxy.class, deferClose(clientAppRun.newActorClient())); + new ActorProxyBuilder(ACTOR_TYPE, ActorProxy.class, newActorClient(client)); logger.debug("Creating actorId"); logger.debug("Building proxy"); proxy = proxyBuilder.build(actorId); @@ -104,11 +141,18 @@ public void reminderRecoveryTest() throws Exception { int originalActorHostIdentifier = Integer.parseInt( proxy.invokeMethod("getIdentifier", String.class).block()); - if (originalActorHostIdentifier == firstAppRun.getHttpPort()) { - firstAppRun.stop(); + // Stop BOTH the app and its sidecar for the host currently hosting the actor. Killing + // only the app subprocess leaves daprd connected to placement, so placement never + // reassigns the actor type away from that host (daprd just keeps retrying the dead + // app). The legacy harness's DaprRun.stop() ran `dapr stop --app-id`, which tore down + // the sidecar process along with its supervised app -- this mirrors that. + if (originalActorHostIdentifier == one.dapr().getHttpPort()) { + one.app().stop(); + one.dapr().stop(); } - if (originalActorHostIdentifier == secondAppRun.getHttpPort()) { - secondAppRun.stop(); + if (originalActorHostIdentifier == two.dapr().getHttpPort()) { + two.app().stop(); + two.dapr().stop(); } logger.debug("Pausing 10 seconds to allow failover to take place"); diff --git a/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java index 53e932e5a..f7bdc6d1f 100644 --- a/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java @@ -23,7 +23,10 @@ import io.dapr.it.Stoppable; import io.dapr.testcontainers.Component; import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprContainerConstants; import io.dapr.testcontainers.DaprLogLevel; +import io.dapr.testcontainers.DaprPlacementContainer; +import io.dapr.testcontainers.DaprSchedulerContainer; import io.dapr.testcontainers.wait.strategy.DaprWait; import org.junit.jupiter.api.AfterAll; import org.testcontainers.Testcontainers; @@ -87,6 +90,33 @@ protected static DaprContainer daprBuilder(String appName) { .withReusablePlacement(true); } + // ---------- Shared control plane (multi-sidecar ITs) ---------- + + /** Shared placement for multi-sidecar ITs. Explicit (not reuse-based) so it is + * deterministic on CI where Testcontainers reuse is disabled. */ + protected static DaprPlacementContainer startSharedPlacement() { + DaprPlacementContainer placement = + new DaprPlacementContainer(DaprContainerConstants.DAPR_PLACEMENT_IMAGE_TAG) + .withNetwork(SharedTestInfra.network()) + .withNetworkAliases("placement") + .withReuse(false); + placement.start(); + deferStop(placement); + return placement; + } + + /** Shared scheduler for multi-sidecar ITs (owns actor reminders). */ + protected static DaprSchedulerContainer startSharedScheduler() { + DaprSchedulerContainer scheduler = + new DaprSchedulerContainer(DaprContainerConstants.DAPR_SCHEDULER_IMAGE_TAG) + .withNetwork(SharedTestInfra.network()) + .withNetworkAliases("scheduler") + .withReuse(false); + scheduler.start(); + deferStop(scheduler); + return scheduler; + } + // ---------- App lifecycle ---------- /** Pair returned by {@link #startAppAndAttach}. */ From 2faba9efd205c7b5a4131b7ba5b81aa18839b370 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 19:22:18 -0700 Subject: [PATCH 09/18] test: migrate WaitForSidecarIT to Testcontainers (+ newToxiproxy helper) Signed-off-by: Siri Varma Vegiraju --- .../dapr/it/containers/BaseContainerIT.java | 16 +++ .../dapr/it/resiliency/WaitForSidecarIT.java | 97 +++++++++++++------ 2 files changed, 86 insertions(+), 27 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java index f7bdc6d1f..b23f5c1fd 100644 --- a/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java @@ -21,6 +21,7 @@ import io.dapr.it.AppRun; import io.dapr.it.DaprPorts; import io.dapr.it.Stoppable; +import io.dapr.it.testcontainers.ContainerConstants; import io.dapr.testcontainers.Component; import io.dapr.testcontainers.DaprContainer; import io.dapr.testcontainers.DaprContainerConstants; @@ -30,6 +31,7 @@ import io.dapr.testcontainers.wait.strategy.DaprWait; import org.junit.jupiter.api.AfterAll; import org.testcontainers.Testcontainers; +import org.testcontainers.toxiproxy.ToxiproxyContainer; import java.util.Deque; import java.util.HashMap; @@ -248,6 +250,20 @@ protected static void restartSidecar(DaprContainer dapr) throws Exception { waitForActorsReady(dapr); } + /** Starts a Toxiproxy container on the shared network and registers it for + * @AfterAll cleanup. Callers create proxies via a ToxiproxyClient against + * {@code getHost()}/{@code getControlPort()} (the control channel), then point + * Dapr clients at {@code getMappedPort()} of a proxy created on a + * fixed listen port (e.g. 8666). Mirrors SdkResiliencyIT. */ + protected static ToxiproxyContainer newToxiproxy() { + ToxiproxyContainer toxiproxy = + new ToxiproxyContainer(ContainerConstants.TOXI_PROXY_IMAGE_TAG) + .withNetwork(SharedTestInfra.network()); + toxiproxy.start(); + deferStop(toxiproxy); + return toxiproxy; + } + // ---------- DaprClient / ActorClient factories ---------- protected static DaprClient newDaprClient(DaprContainer dapr) { diff --git a/sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java b/sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java index c7dd5ccc5..00c6bfc64 100644 --- a/sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java @@ -8,65 +8,91 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and -limitations under the License. + * limitations under the License. */ package io.dapr.it.resiliency; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; -import io.dapr.it.ToxiProxyRun; +import eu.rekawek.toxiproxy.Proxy; +import eu.rekawek.toxiproxy.ToxiproxyClient; +import eu.rekawek.toxiproxy.model.ToxicDirection; +import eu.rekawek.toxiproxy.model.toxic.Latency; +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import io.dapr.config.Properties; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.DaprContainer; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.testcontainers.toxiproxy.ToxiproxyContainer; +import java.io.IOException; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; /** - * Test SDK resiliency. + * Test SDK resiliency around {@code waitForSidecar}. */ -public class WaitForSidecarIT extends BaseIT { +public class WaitForSidecarIT extends BaseContainerIT { // Use a number large enough to make sure it will respect the entire timeout. private static final Duration LATENCY = Duration.ofSeconds(5); - private static final Duration JITTER = Duration.ofSeconds(0); + private static DaprContainer dapr; - private static DaprRun daprRun; + private static ToxiproxyContainer toxiproxy; - private static ToxiProxyRun toxiProxyRun; + private static Proxy proxy; - private static DaprRun daprNotRunning; + private static DaprContainer daprNotRunning; + + private static int daprNotRunningHttpPort; @BeforeAll - public static void init() throws Exception { - daprRun = startDaprApp(WaitForSidecarIT.class.getSimpleName(), 5000); - daprNotRunning = startDaprApp(WaitForSidecarIT.class.getSimpleName() + "NotRunning", 5000); + public static void init() throws IOException { + dapr = daprBuilder("wait-for-sidecar-it") + .withNetworkAliases("dapr"); + dapr.start(); + deferStop(dapr); + + toxiproxy = newToxiproxy(); + ToxiproxyClient toxiproxyClient = new ToxiproxyClient(toxiproxy.getHost(), toxiproxy.getControlPort()); + proxy = toxiproxyClient.createProxy("dapr", "0.0.0.0:8666", "dapr:3500"); + + // A second sidecar that is started and then stopped, so a client pointed at its + // (now-dead) mapped port cannot possibly get a response. Deterministic stand-in + // for a "Dapr is not running" target. + daprNotRunning = daprBuilder("wait-for-sidecar-it-not-running") + .withNetworkAliases("dapr-not-running"); + daprNotRunning.start(); + daprNotRunningHttpPort = daprNotRunning.getHttpPort(); daprNotRunning.stop(); - - toxiProxyRun = new ToxiProxyRun(daprRun, LATENCY, JITTER); - toxiProxyRun.start(); } @Test public void waitSucceeds() throws Exception { - try(var client = daprRun.newDaprClient()) { + try (DaprClient client = newDaprClient(dapr)) { client.waitForSidecar(5000).block(); } } @Test - public void waitTimeout() { - int timeoutInMillis = (int)LATENCY.minusMillis(100).toMillis(); + public void waitTimeout() throws IOException { + int timeoutInMillis = (int) LATENCY.minusMillis(100).toMillis(); long started = System.currentTimeMillis(); - assertThrows(RuntimeException.class, () -> { - try(var client = toxiProxyRun.newDaprClientBuilder().build()) { - client.waitForSidecar(timeoutInMillis).block(); - } - }); + Latency latency = proxy.toxics().latency("latency", ToxicDirection.DOWNSTREAM, LATENCY.toMillis()); + try { + assertThrows(RuntimeException.class, () -> { + try (DaprClient client = viaProxyClientBuilder().build()) { + client.waitForSidecar(timeoutInMillis).block(); + } + }); + } finally { + latency.remove(); + } long duration = System.currentTimeMillis() - started; @@ -75,11 +101,16 @@ public void waitTimeout() { @Test public void waitSlow() throws Exception { - int timeoutInMillis = (int)LATENCY.plusMillis(100).toMillis(); + int timeoutInMillis = (int) LATENCY.plusMillis(100).toMillis(); long started = System.currentTimeMillis(); - try(var client = toxiProxyRun.newDaprClientBuilder().build()) { + Latency latency = proxy.toxics().latency("latency", ToxicDirection.DOWNSTREAM, LATENCY.toMillis()); + try { + try (DaprClient client = viaProxyClientBuilder().build()) { client.waitForSidecar(timeoutInMillis).block(); + } + } finally { + latency.remove(); } long duration = System.currentTimeMillis() - started; @@ -95,7 +126,7 @@ public void waitNotRunningTimeout() { long started = System.currentTimeMillis(); assertThrows(RuntimeException.class, () -> { - try(var client = daprNotRunning.newDaprClientBuilder().build()) { + try (DaprClient client = notRunningClientBuilder().build()) { client.waitForSidecar(timeoutMilliseconds).block(); } }); @@ -104,4 +135,16 @@ public void waitNotRunningTimeout() { assertThat(duration).isGreaterThanOrEqualTo(timeoutMilliseconds); } + + private static DaprClientBuilder viaProxyClientBuilder() { + return new DaprClientBuilder() + .withPropertyOverride(Properties.HTTP_ENDPOINT, "http://localhost:" + toxiproxy.getMappedPort(8666)) + .withPropertyOverride(Properties.GRPC_ENDPOINT, "http://localhost:" + toxiproxy.getMappedPort(8666)); + } + + private static DaprClientBuilder notRunningClientBuilder() { + return new DaprClientBuilder() + .withPropertyOverride(Properties.HTTP_ENDPOINT, "http://localhost:" + daprNotRunningHttpPort) + .withPropertyOverride(Properties.GRPC_ENDPOINT, "http://localhost:" + daprNotRunningHttpPort); + } } From b33c6a28dd15fd4d7bcb22ec025d2b989e70b7b2 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 19:37:24 -0700 Subject: [PATCH 10/18] test: migrate ActorSdkResiliencyIT off ToxiProxyRun (kept @Disabled) Signed-off-by: Siri Varma Vegiraju --- .../dapr/it/actors/ActorSdkResiliencyIT.java | 82 ++++++++++++------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/ActorSdkResiliencyIT.java b/sdk-tests/src/test/java/io/dapr/it/actors/ActorSdkResiliencyIT.java index c9b7f27b7..87b19e723 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/ActorSdkResiliencyIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/ActorSdkResiliencyIT.java @@ -8,27 +8,34 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and -limitations under the License. + * limitations under the License. */ package io.dapr.it.actors; +import eu.rekawek.toxiproxy.Proxy; +import eu.rekawek.toxiproxy.ToxiproxyClient; +import eu.rekawek.toxiproxy.model.ToxicDirection; import io.dapr.actors.ActorId; import io.dapr.actors.client.ActorClient; import io.dapr.actors.client.ActorProxyBuilder; import io.dapr.client.DaprClient; import io.dapr.client.resiliency.ResiliencyOptions; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; -import io.dapr.it.ToxiProxyRun; +import io.dapr.config.Properties; +import io.dapr.config.Property; +import io.dapr.it.AppRun; import io.dapr.it.actors.services.springboot.DemoActor; import io.dapr.it.actors.services.springboot.DemoActorService; -import org.junit.jupiter.api.AfterAll; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.DaprContainer; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.testcontainers.toxiproxy.ToxiproxyContainer; import java.time.Duration; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; @@ -38,7 +45,7 @@ /** * Test SDK resiliency. */ -public class ActorSdkResiliencyIT extends BaseIT { +public class ActorSdkResiliencyIT extends BaseContainerIT { private static final ActorId ACTOR_ID = new ActorId(UUID.randomUUID().toString()); @@ -52,13 +59,15 @@ public class ActorSdkResiliencyIT extends BaseIT { private static final int MAX_RETRIES = -1; // Infinity - private static DaprRun daprRun; + private static DaprContainer dapr; + + private static AppRun app; private static DaprClient daprClient; private static DemoActor demoActor; - private static ToxiProxyRun toxiProxyRun; + private static ToxiproxyContainer toxiproxy; private static DemoActor toxiDemoActor; @@ -68,25 +77,45 @@ public class ActorSdkResiliencyIT extends BaseIT { @BeforeAll public static void init() throws Exception { - daprRun = startDaprApp( - ActorSdkResiliencyIT.class.getSimpleName(), - DemoActorService.SUCCESS_MESSAGE, - DemoActorService.class, - true, - 60000); - - demoActor = buildDemoActorProxy(deferClose(daprRun.newActorClient())); - daprClient = daprRun.newDaprClientBuilder().build(); - - toxiProxyRun = new ToxiProxyRun(daprRun, LATENCY, JITTER); - toxiProxyRun.start(); + var pair = startAppAndAttach( + "actor-sdk-resiliency-it", + DemoActorService.class, + AppRun.AppProtocol.HTTP, + appPort -> daprBuilder("actor-sdk-resiliency-it") + .withNetworkAliases("dapr") + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(redisStateStore(STATE_STORE_NAME))); + dapr = pair.dapr(); + app = pair.app(); + waitForActorsReady(dapr); + + demoActor = buildDemoActorProxy(newActorClient(dapr)); + daprClient = deferClose(newDaprClient(dapr)); + + toxiproxy = newToxiproxy(); + ToxiproxyClient toxiproxyClient = new ToxiproxyClient(toxiproxy.getHost(), toxiproxy.getControlPort()); + Proxy grpcProxy = toxiproxyClient.createProxy("dapr_grpc", "0.0.0.0:8666", "dapr:50001"); + grpcProxy.toxics() + .latency("latency", ToxicDirection.DOWNSTREAM, LATENCY.toMillis()) + .setJitter(JITTER.toMillis()); toxiDemoActor = buildDemoActorProxy( - toxiProxyRun.newActorClient(new ResiliencyOptions().setTimeout(TIMEOUT))); + newActorClientViaProxy(new ResiliencyOptions().setTimeout(TIMEOUT))); resilientDemoActor = buildDemoActorProxy( - toxiProxyRun.newActorClient(new ResiliencyOptions().setTimeout(TIMEOUT).setMaxRetries(MAX_RETRIES))); + newActorClientViaProxy(new ResiliencyOptions().setTimeout(TIMEOUT).setMaxRetries(MAX_RETRIES))); oneRetryDemoActor = buildDemoActorProxy( - toxiProxyRun.newActorClient(new ResiliencyOptions().setTimeout(TIMEOUT).setMaxRetries(1))); + newActorClientViaProxy(new ResiliencyOptions().setTimeout(TIMEOUT).setMaxRetries(1))); + } + + private static ActorClient newActorClientViaProxy(ResiliencyOptions resiliencyOptions) { + Map, String> overrides = new HashMap<>(); + int mappedPort = toxiproxy.getMappedPort(8666); + overrides.put(Properties.GRPC_ENDPOINT, "127.0.0.1:" + mappedPort); + overrides.put(Properties.GRPC_PORT, String.valueOf(mappedPort)); + ActorClient client = new ActorClient(new Properties(overrides), resiliencyOptions); + deferClose(client); + return client; } private static DemoActor buildDemoActorProxy(ActorClient actorClient) { @@ -94,13 +123,6 @@ private static DemoActor buildDemoActorProxy(ActorClient actorClient) { return builder.build(ACTOR_ID); } - @AfterAll - public static void tearDown() throws Exception { - if (toxiProxyRun != null) { - toxiProxyRun.stop(); - } - } - @Test @Disabled("Flaky when running on GitHub actions") public void retryAndTimeout() { From 5b5d01adb845ce7125b4a8144218aa6da50bf6d5 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 20:03:17 -0700 Subject: [PATCH 11/18] test: migrate BindingIT to Testcontainers Kafka + in-code bindings Signed-off-by: Siri Varma Vegiraju --- sdk-tests/pom.xml | 5 ++ .../io/dapr/it/binding/http/BindingIT.java | 63 +++++++++++-------- .../dapr/it/containers/BaseContainerIT.java | 27 ++++++++ .../dapr/it/containers/SharedTestInfra.java | 18 ++++++ 4 files changed, 86 insertions(+), 27 deletions(-) diff --git a/sdk-tests/pom.xml b/sdk-tests/pom.xml index 9d68559ae..9a0dfcbd6 100644 --- a/sdk-tests/pom.xml +++ b/sdk-tests/pom.xml @@ -217,6 +217,11 @@ testcontainers-toxiproxy test + + org.testcontainers + testcontainers-kafka + test + diff --git a/sdk-tests/src/test/java/io/dapr/it/binding/http/BindingIT.java b/sdk-tests/src/test/java/io/dapr/it/binding/http/BindingIT.java index 2bdf7bb3c..438e8ac37 100644 --- a/sdk-tests/src/test/java/io/dapr/it/binding/http/BindingIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/binding/http/BindingIT.java @@ -15,14 +15,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.dapr.client.DaprClient; -import io.dapr.client.DaprClientBuilder; import io.dapr.client.domain.HttpExtension; import io.dapr.exceptions.DaprException; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; +import io.dapr.it.AppRun; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.DaprContainer; import org.junit.jupiter.api.Test; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -34,14 +33,18 @@ /** * Service for input and output binding example. */ -public class BindingIT extends BaseIT { +public class BindingIT extends BaseContainerIT { + + private static final String APP_NAME = "bindingit-grpc"; @Test public void httpOutputBindingError() throws Exception { - var run = startDaprApp( - this.getClass().getSimpleName() + "-httpoutputbinding-exception", - 60000); - try(DaprClient client = run.newDaprClientBuilder().build()) { + DaprContainer dapr = daprBuilder("bindingit-httpoutputbinding-exception") + .withComponent(httpBinding("github-http-binding-404", "https://api.github.com/unknown_path", true)); + dapr.start(); + deferStop(dapr); + + try (DaprClient client = newDaprClient(dapr)) { // Validate error message callWithRetry(() -> { System.out.println("Checking exception handling for output binding ..."); @@ -61,10 +64,13 @@ public void httpOutputBindingError() throws Exception { @Test public void httpOutputBindingErrorIgnoredByComponent() throws Exception { - var run = startDaprApp( - this.getClass().getSimpleName() + "-httpoutputbinding-ignore-error", - 60000); - try(DaprClient client = run.newDaprClientBuilder().build()) { + DaprContainer dapr = daprBuilder("bindingit-httpoutputbinding-ignore-error") + .withComponent( + httpBinding("github-http-binding-404-success", "https://api.github.com/unknown_path", false)); + dapr.start(); + deferStop(dapr); + + try (DaprClient client = newDaprClient(dapr)) { // Validate error message callWithRetry(() -> { System.out.println("Checking exception handling for output binding ..."); @@ -86,20 +92,23 @@ public void httpOutputBindingErrorIgnoredByComponent() throws Exception { @Test public void inputOutputBinding() throws Exception { - DaprRun daprRun = startDaprApp( - this.getClass().getSimpleName() + "-grpc", - InputBindingService.SUCCESS_MESSAGE, - InputBindingService.class, - true, - 60000); - - var bidingName = "sample123"; + var bindingName = "sample123"; - try(DaprClient client = daprRun.newDaprClientBuilder().build()) { + var pair = startAppAndAttach( + APP_NAME, + InputBindingService.class, + AppRun.AppProtocol.HTTP, + appPort -> daprBuilder(APP_NAME) + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(kafkaBinding(bindingName))); + DaprContainer dapr = pair.dapr(); + + try (DaprClient client = newDaprClient(dapr)) { callWithRetry(() -> { System.out.println("Checking if input binding is up before publishing events ..."); client.invokeBinding( - bidingName, "create", "ping").block(); + bindingName, "create", "ping").block(); try { Thread.sleep(1000); @@ -108,7 +117,7 @@ public void inputOutputBinding() throws Exception { throw new RuntimeException(e); } - client.invokeMethod(daprRun.getAppName(), "initialized", "", HttpExtension.GET).block(); + client.invokeMethod(APP_NAME, "initialized", "", HttpExtension.GET).block(); }, 120000); // This is an example of sending data in a user-defined object. The input binding will receive: @@ -118,21 +127,21 @@ public void inputOutputBinding() throws Exception { System.out.println("sending first message"); client.invokeBinding( - bidingName, "create", myClass, Map.of("MyMetadata", "MyValue"), Void.class).block(); + bindingName, "create", myClass, Map.of("MyMetadata", "MyValue"), Void.class).block(); // This is an example of sending a plain string. The input binding will receive // cat final String m = "cat"; System.out.println("sending " + m); client.invokeBinding( - bidingName, "create", m, Map.of("MyMetadata", "MyValue"), Void.class).block(); + bindingName, "create", m, Map.of("MyMetadata", "MyValue"), Void.class).block(); // Metadata is not used by Kafka component, so it is not possible to validate. callWithRetry(() -> { System.out.println("Checking results ..."); final List messages = client.invokeMethod( - daprRun.getAppName(), + APP_NAME, "messages", null, HttpExtension.GET, diff --git a/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java index b23f5c1fd..f24ce018f 100644 --- a/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java @@ -341,6 +341,33 @@ protected static Component mongoStateStore(String name) { )); } + /** + * Kafka-backed input/output binding. Lazily starts the shared Kafka container + * before returning the component. The {@code {appID}} placeholders in + * {@code topics}/{@code publishTopic}/{@code consumerGroup} resolve to the + * owning daprd's app-id, so topic and consumer group are self-consistent + * without needing a {@code scopes} entry. + */ + protected static Component kafkaBinding(String name) { + SharedTestInfra.kafka(); // ensure Kafka is up before DaprContainer needs it + return new Component(name, "bindings.kafka", "v1", Map.of( + "brokers", SharedTestInfra.kafkaInternalBroker(), + "topics", "topic-{appID}", + "publishTopic", "topic-{appID}", + "consumerGroup", "{appID}", + "authRequired", "false", + "initialOffset", "oldest" + )); + } + + /** HTTP output binding pointed at an arbitrary URL. */ + protected static Component httpBinding(String name, String url, boolean errorIfNot2xx) { + return new Component(name, "bindings.http", "v1", Map.of( + "url", url, + "errorIfNot2XX", Boolean.toString(errorIfNot2xx) + )); + } + // ---------- Cleanup ---------- protected static T deferClose(T object) { diff --git a/sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java b/sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java index e778d1f7d..5e61267aa 100644 --- a/sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java +++ b/sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java @@ -29,11 +29,13 @@ public final class SharedTestInfra { private static final String REDIS_NETWORK_ALIAS = "redis"; private static final String ZIPKIN_NETWORK_ALIAS = "zipkin"; private static final String MONGO_NETWORK_ALIAS = "mongo"; + private static final String KAFKA_NETWORK_ALIAS = "kafka"; private static volatile Network network; private static volatile GenericContainer redis; private static volatile GenericContainer zipkin; private static volatile GenericContainer mongo; + private static volatile org.testcontainers.kafka.KafkaContainer kafka; private SharedTestInfra() {} @@ -91,4 +93,20 @@ public static synchronized GenericContainer mongo() { public static String mongoInternalHost() { return MONGO_NETWORK_ALIAS + ":27017"; } + + public static synchronized org.testcontainers.kafka.KafkaContainer kafka() { + if (kafka == null) { + kafka = new org.testcontainers.kafka.KafkaContainer("apache/kafka:3.8.0") + .withNetwork(network()) + .withNetworkAliases(KAFKA_NETWORK_ALIAS) + .withListener(KAFKA_NETWORK_ALIAS + ":19092") + .withReuse(true); + kafka.start(); + } + return kafka; + } + + public static String kafkaInternalBroker() { + return KAFKA_NETWORK_ALIAS + ":19092"; + } } From 3ad067ae301fbcfeff68a3e3ce956b75160452a8 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 20:17:05 -0700 Subject: [PATCH 12/18] ci: drop Dapr CLI/init/Kafka/ToxiProxy setup from build job Signed-off-by: Siri Varma Vegiraju --- .github/workflows/build.yml | 73 ------------------------------------- 1 file changed, 73 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c70963b36..70410fe20 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -112,17 +112,7 @@ jobs: spring-boot-display-version: 4.0.x experimental: false env: - GOVER: "1.20" - GOOS: linux - GOARCH: amd64 - GOPROXY: https://proxy.golang.org JDK_VER: ${{ matrix.java }} - DAPR_CLI_VER: 1.18.0 - DAPR_RUNTIME_VER: 1.18.0 - DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.18.0/install/install.sh - DAPR_CLI_REF: - DAPR_REF: - TOXIPROXY_URL: https://github.com/Shopify/toxiproxy/releases/download/v2.5.0/toxiproxy-server-linux-amd64 steps: - uses: actions/checkout@v7 - name: Check Docker version @@ -132,69 +122,6 @@ jobs: with: distribution: 'temurin' java-version: ${{ env.JDK_VER }} - - name: Set up Dapr CLI - run: wget -q ${{ env.DAPR_INSTALL_URL }} -O - | /bin/bash -s ${{ env.DAPR_CLI_VER }} - - name: Set up Go ${{ env.GOVER }} - if: env.DAPR_REF != '' || env.DAPR_CLI_REF != '' - uses: actions/setup-go@v6 - with: - go-version: ${{ env.GOVER }} - - name: Checkout Dapr CLI repo to override dapr command. - uses: actions/checkout@v7 - if: env.DAPR_CLI_REF != '' - with: - repository: dapr/cli - ref: ${{ env.DAPR_CLI_REF }} - path: cli - - name: Checkout Dapr repo to override daprd. - uses: actions/checkout@v7 - if: env.DAPR_REF != '' - with: - repository: dapr/dapr - ref: ${{ env.DAPR_REF }} - path: dapr - - name: Build and override dapr cli with referenced commit. - if: env.DAPR_CLI_REF != '' - run: | - cd cli - make - sudo cp dist/linux_amd64/release/dapr /usr/local/bin/dapr - cd .. - - name: Uninstall Dapr runtime ${{ env.DAPR_RUNTIME_VER }} - run: dapr uninstall --all - - name: Ensure Dapr runtime uninstalled - run: | - while [ "$(docker ps -aq --filter 'name=^/dapr')" ]; do - echo "Waiting for Dapr containers to be deleted..." - sleep 5 - done - echo "All dapr containers are deleted." - - name: Initialize Dapr runtime ${{ env.DAPR_RUNTIME_VER }} - run: dapr init --runtime-version ${{ env.DAPR_RUNTIME_VER }} - - name: Build and override daprd with referenced commit. - if: env.DAPR_REF != '' - run: | - cd dapr - make - mkdir -p $HOME/.dapr/bin/ - cp dist/linux_amd64/release/daprd $HOME/.dapr/bin/daprd - cd .. - - name: Override placement service. - if: env.DAPR_REF != '' - run: | - docker stop dapr_placement - cd dapr - ./dist/linux_amd64/release/placement & - - name: Spin local environment - run: | - docker compose -f ./sdk-tests/deploy/local-test.yml up -d kafka - docker ps - - name: Install local ToxiProxy to simulate connectivity issues to Dapr sidecar - run: | - mkdir -p /home/runner/.local/bin - wget -q ${{ env.TOXIPROXY_URL }} -O /home/runner/.local/bin/toxiproxy-server - chmod +x /home/runner/.local/bin/toxiproxy-server - /home/runner/.local/bin/toxiproxy-server --version - name: Clean up and install sdk run: ./mvnw clean install -B -q -DskipTests - name: Integration tests using spring boot 3.x version ${{ matrix.spring-boot-version }} From beaca0e676892334a7dbfe5d59e497abc241954f Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 20:25:54 -0700 Subject: [PATCH 13/18] test: remove dead dapr-run harness, binding YAMLs, and local-test.yml Signed-off-by: Siri Varma Vegiraju --- sdk-tests/components/http_binding.yaml | 27 -- sdk-tests/components/kafka_bindings.yaml | 26 -- sdk-tests/deploy/local-test.yml | 23 - .../src/test/java/io/dapr/it/BaseIT.java | 187 -------- .../src/test/java/io/dapr/it/DaprRun.java | 436 ------------------ .../test/java/io/dapr/it/DaprRunConfig.java | 31 -- .../test/java/io/dapr/it/ToxiProxyRun.java | 110 ----- .../io/dapr/it/actors/app/MyActorService.java | 3 - .../springboot/StatefulActorService.java | 2 - .../grpc/MethodInvokeService.java | 4 - .../http/MethodInvokeService.java | 4 - .../java/io/dapr/it/tracing/grpc/Service.java | 4 - .../java/io/dapr/it/tracing/http/Service.java | 4 - 13 files changed, 861 deletions(-) delete mode 100644 sdk-tests/components/http_binding.yaml delete mode 100644 sdk-tests/components/kafka_bindings.yaml delete mode 100644 sdk-tests/deploy/local-test.yml delete mode 100644 sdk-tests/src/test/java/io/dapr/it/BaseIT.java delete mode 100644 sdk-tests/src/test/java/io/dapr/it/DaprRun.java delete mode 100644 sdk-tests/src/test/java/io/dapr/it/DaprRunConfig.java delete mode 100644 sdk-tests/src/test/java/io/dapr/it/ToxiProxyRun.java diff --git a/sdk-tests/components/http_binding.yaml b/sdk-tests/components/http_binding.yaml deleted file mode 100644 index 5a3ca0a1d..000000000 --- a/sdk-tests/components/http_binding.yaml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: github-http-binding-404 -spec: - type: bindings.http - version: v1 - metadata: - - name: url - value: https://api.github.com/unknown_path -scopes: - - bindingit-httpoutputbinding-exception ---- -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: github-http-binding-404-success -spec: - type: bindings.http - version: v1 - metadata: - - name: url - value: https://api.github.com/unknown_path - - name: errorIfNot2XX - value: "false" -scopes: - - bindingit-httpoutputbinding-ignore-error \ No newline at end of file diff --git a/sdk-tests/components/kafka_bindings.yaml b/sdk-tests/components/kafka_bindings.yaml deleted file mode 100644 index 873506376..000000000 --- a/sdk-tests/components/kafka_bindings.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: sample123 -spec: - type: bindings.kafka - version: v1 - metadata: - # Kafka broker connection setting - - name: brokers - value: localhost:9092 - # consumer configuration: topic and consumer group - - name: topics - value: "topic-{appID}" - - name: consumerGroup - value: "{appID}" - # publisher configuration: topic - - name: publishTopic - value: "topic-{appID}" - - name: authRequired - value: "false" - - name: initialOffset - value: oldest -scopes: - - bindingit-http-inputbindingservice - - bindingit-grpc-inputbindingservice \ No newline at end of file diff --git a/sdk-tests/deploy/local-test.yml b/sdk-tests/deploy/local-test.yml deleted file mode 100644 index 7160ac25b..000000000 --- a/sdk-tests/deploy/local-test.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: '3' -services: - zookeeper: - image: confluentinc/cp-zookeeper:7.4.4 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - ports: - - 2181:2181 - - kafka: - image: confluentinc/cp-kafka:7.4.4 - depends_on: - - zookeeper - ports: - - "9092:9092" - environment: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 diff --git a/sdk-tests/src/test/java/io/dapr/it/BaseIT.java b/sdk-tests/src/test/java/io/dapr/it/BaseIT.java deleted file mode 100644 index f50025e21..000000000 --- a/sdk-tests/src/test/java/io/dapr/it/BaseIT.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2021 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and -limitations under the License. -*/ - -package io.dapr.it; - -import io.dapr.actors.client.ActorClient; -import io.dapr.client.resiliency.ResiliencyOptions; -import io.dapr.config.Properties; -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.junit.jupiter.api.AfterAll; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Map; -import java.util.Queue; - -import static io.dapr.it.AppRun.AppProtocol.GRPC; -import static io.dapr.it.AppRun.AppProtocol.HTTP; - -public abstract class BaseIT { - - protected static final String STATE_STORE_NAME = "statestore"; - - protected static final String QUERY_STATE_STORE = "mongo-statestore"; - - private static final Map DAPR_RUN_BUILDERS = new HashMap<>(); - - private static final Queue TO_BE_STOPPED = new LinkedList<>(); - - private static final Queue TO_BE_CLOSED = new LinkedList<>(); - - protected static DaprRun startDaprApp( - String testName, - String successMessage, - Class serviceClass, - Boolean useAppPort, - int maxWaitMilliseconds) throws Exception { - return startDaprApp(testName, successMessage, serviceClass, useAppPort, maxWaitMilliseconds, HTTP); - } - - protected static DaprRun startDaprApp( - String testName, - String successMessage, - Class serviceClass, - AppRun.AppProtocol appProtocol, - int maxWaitMilliseconds) throws Exception { - return startDaprApp(testName, successMessage, serviceClass, true, maxWaitMilliseconds, appProtocol); - } - - protected static DaprRun startDaprApp( - String testName, - String successMessage, - Class serviceClass, - Boolean useAppPort, - int maxWaitMilliseconds, - AppRun.AppProtocol appProtocol) throws Exception { - return startDaprApp( - testName, - successMessage, - serviceClass, - useAppPort, - true, - maxWaitMilliseconds, - appProtocol); - } - - protected static DaprRun startDaprApp( - String testName, - int maxWaitMilliseconds) throws Exception { - return startDaprApp( - testName, - "You're up and running!", - null, - false, - true, - maxWaitMilliseconds, - HTTP); - } - - protected static DaprRun startDaprApp( - String testName, - String successMessage, - Class serviceClass, - Boolean useAppPort, - Boolean useDaprPorts, - int maxWaitMilliseconds, - AppRun.AppProtocol appProtocol) throws Exception { - DaprRun.Builder builder = new DaprRun.Builder( - testName, - () -> DaprPorts.build(useAppPort, useDaprPorts, useDaprPorts), - successMessage, - maxWaitMilliseconds, - appProtocol).withServiceClass(serviceClass); - DaprRun run = builder.build(); - TO_BE_STOPPED.add(run); - DAPR_RUN_BUILDERS.put(run.getAppName(), builder); - run.start(); - return run; - } - - protected static ImmutablePair startSplitDaprAndApp( - String testName, - String successMessage, - Class serviceClass, - Boolean useAppPort, - int maxWaitMilliseconds) throws Exception { - return startSplitDaprAndApp( - testName, - successMessage, - serviceClass, - useAppPort, - maxWaitMilliseconds, - HTTP); - } - - protected static ImmutablePair startSplitDaprAndApp( - String testName, - String successMessage, - Class serviceClass, - Boolean useAppPort, - int maxWaitMilliseconds, - AppRun.AppProtocol appProtocol) throws Exception { - DaprRun.Builder builder = new DaprRun.Builder( - testName, - () -> DaprPorts.build(useAppPort, true, true), - successMessage, - maxWaitMilliseconds, - appProtocol).withServiceClass(serviceClass); - ImmutablePair runs = builder.splitBuild(); - TO_BE_STOPPED.add(runs.left); - TO_BE_STOPPED.add(runs.right); - DAPR_RUN_BUILDERS.put(runs.right.getAppName(), builder); - runs.left.start(); - runs.right.start(); - return runs; - } - - protected static T deferClose(T object) { - TO_BE_CLOSED.add(object); - return object; - } - - @AfterAll - public static void cleanUp() throws Exception { - while (!TO_BE_CLOSED.isEmpty()) { - TO_BE_CLOSED.remove().close(); - } - - while (!TO_BE_STOPPED.isEmpty()) { - TO_BE_STOPPED.remove().stop(); - } - } - - protected static ActorClient newActorClient(Properties properties) { - return new ActorClient(properties, null); - } - - protected static ActorClient newActorClient(ResiliencyOptions resiliencyOptions) throws RuntimeException { - try { - Constructor constructor = ActorClient.class.getDeclaredConstructor(ResiliencyOptions.class); - constructor.setAccessible(true); - ActorClient client = constructor.newInstance(resiliencyOptions); - TO_BE_CLOSED.add(client); - return client; - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } catch (InvocationTargetException e) { - throw new RuntimeException(e); - } catch (InstantiationException e) { - throw new RuntimeException(e); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sdk-tests/src/test/java/io/dapr/it/DaprRun.java b/sdk-tests/src/test/java/io/dapr/it/DaprRun.java deleted file mode 100644 index e29c5f134..000000000 --- a/sdk-tests/src/test/java/io/dapr/it/DaprRun.java +++ /dev/null @@ -1,436 +0,0 @@ -/* - * Copyright 2021 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and -limitations under the License. -*/ - -package io.dapr.it; - -import com.google.protobuf.Empty; -import io.dapr.actors.client.ActorClient; -import io.dapr.client.DaprClient; -import io.dapr.client.DaprClientBuilder; -import io.dapr.client.DaprPreviewClient; -import io.dapr.client.resiliency.ResiliencyOptions; -import io.dapr.config.Properties; -import io.dapr.config.Property; -import io.dapr.v1.AppCallbackHealthCheckGrpc; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import org.apache.commons.lang3.tuple.ImmutablePair; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; - -import static io.dapr.it.Retry.callWithRetry; - - -public class DaprRun implements Stoppable { - - private static final String DEFAULT_DAPR_API_TOKEN = UUID.randomUUID().toString(); - private static final String DAPR_SUCCESS_MESSAGE = "You're up and running!"; - - private static final String DAPR_RUN = "dapr run --app-id %s --app-protocol %s " + - "--config ./configurations/configuration.yaml " + - "--resources-path ./components"; - - // the arg in -Dexec.args is the app's port - private static final String DAPR_COMMAND = - " -- mvn exec:java -D exec.mainClass=%s -D exec.classpathScope=test -D exec.args=\"%s\""; - - private final DaprPorts ports; - - private final String appName; - - private final AppRun.AppProtocol appProtocol; - - private final int maxWaitMilliseconds; - - private final AtomicBoolean started; - - private final Command startCommand; - - private final Command listCommand; - - private final Command stopCommand; - - private final boolean hasAppHealthCheck; - - private final Map, String> propertyOverrides; - - private DaprRun(String testName, - DaprPorts ports, - String successMessage, - Class serviceClass, - int maxWaitMilliseconds, - AppRun.AppProtocol appProtocol) { - this( - testName, - ports, - successMessage, - serviceClass, - maxWaitMilliseconds, - appProtocol, - resolveDaprApiToken(serviceClass)); - } - - private DaprRun(String testName, - DaprPorts ports, - String successMessage, - Class serviceClass, - int maxWaitMilliseconds, - AppRun.AppProtocol appProtocol, - String daprApiToken) { - // The app name needs to be deterministic since we depend on it to kill previous runs. - this.appName = serviceClass == null ? - testName.toLowerCase() : - String.format("%s-%s", testName, serviceClass.getSimpleName()).toLowerCase(); - this.appProtocol = appProtocol; - this.startCommand = - new Command( - successMessage, - buildDaprCommand(this.appName, serviceClass, ports, appProtocol), - daprApiToken == null ? null : Map.of("DAPR_API_TOKEN", daprApiToken)); - this.listCommand = new Command( - this.appName, - "dapr list"); - this.stopCommand = new Command( - "app stopped successfully", - "dapr stop --app-id " + this.appName); - this.ports = ports; - this.maxWaitMilliseconds = maxWaitMilliseconds; - this.started = new AtomicBoolean(false); - this.hasAppHealthCheck = isAppHealthCheckEnabled(serviceClass); - this.propertyOverrides = daprApiToken == null ? ports.getPropertyOverrides() : - Collections.unmodifiableMap(new HashMap<>(ports.getPropertyOverrides()) {{ - put(Properties.API_TOKEN, daprApiToken); - }}); - } - - public void start() throws InterruptedException, IOException { - long start = System.currentTimeMillis(); - // First, try to stop previous run (if left running). - this.stop(); - // Wait for the previous run to kill the prior process. - long timeLeft = this.maxWaitMilliseconds - (System.currentTimeMillis() - start); - System.out.println("Checking if previous run for Dapr application has stopped ..."); - checkRunState(timeLeft, false); - - System.out.println("Starting dapr application ..."); - this.startCommand.run(); - this.started.set(true); - - timeLeft = this.maxWaitMilliseconds - (System.currentTimeMillis() - start); - System.out.println("Checking if Dapr application has started ..."); - checkRunState(timeLeft, true); - - if (this.ports.getAppPort() != null) { - timeLeft = this.maxWaitMilliseconds - (System.currentTimeMillis() - start); - callWithRetry(() -> { - System.out.println("Checking if app is listening on port ..."); - assertListeningOnPort(this.ports.getAppPort()); - }, timeLeft); - } - - if (this.ports.getHttpPort() != null) { - timeLeft = this.maxWaitMilliseconds - (System.currentTimeMillis() - start); - callWithRetry(() -> { - System.out.println("Checking if Dapr is listening on HTTP port ..."); - assertListeningOnPort(this.ports.getHttpPort()); - }, timeLeft); - } - - if (this.ports.getGrpcPort() != null) { - timeLeft = this.maxWaitMilliseconds - (System.currentTimeMillis() - start); - callWithRetry(() -> { - System.out.println("Checking if Dapr is listening on GRPC port ..."); - assertListeningOnPort(this.ports.getGrpcPort()); - }, timeLeft); - } - System.out.println("Dapr application started."); - } - - @Override - public void stop() throws InterruptedException, IOException { - System.out.println("Stopping dapr application ..."); - try { - this.stopCommand.run(); - System.out.println("Dapr application stopped."); - } catch (RuntimeException e) { - if (e.getMessage() != null && e.getMessage().contains("Could not find success criteria")) { - System.out.println("App " + this.appName + " already stopped or not found (ignored)."); - } else { - System.out.println("Could not stop app " + this.appName + ": " + e.getMessage()); - } - } - } - - public Map, String> getPropertyOverrides() { - return this.propertyOverrides; - } - - public DaprClientBuilder newDaprClientBuilder() { - return new DaprClientBuilder().withPropertyOverrides(this.getPropertyOverrides()); - } - - public ActorClient newActorClient() { - return this.newActorClient(null, null); - } - - public ActorClient newActorClient(Map metadata) { - return this.newActorClient(metadata, null); - } - - public ActorClient newActorClient(ResiliencyOptions resiliencyOptions) { - return this.newActorClient(null, resiliencyOptions); - } - - public ActorClient newActorClient(Map metadata, ResiliencyOptions resiliencyOptions) { - return new ActorClient(new Properties(this.getPropertyOverrides()), metadata, resiliencyOptions); - } - - public void waitForAppHealth(int maxWaitMilliseconds) throws InterruptedException { - if (!this.hasAppHealthCheck) { - return; - } - - if (AppRun.AppProtocol.GRPC.equals(this.appProtocol)) { - ManagedChannel channel = ManagedChannelBuilder.forAddress("127.0.0.1", this.getAppPort()) - .usePlaintext() - .build(); - try { - AppCallbackHealthCheckGrpc.AppCallbackHealthCheckBlockingStub stub = - AppCallbackHealthCheckGrpc.newBlockingStub(channel); - long maxWait = System.currentTimeMillis() + maxWaitMilliseconds; - while (System.currentTimeMillis() <= maxWait) { - try { - stub.healthCheck(Empty.getDefaultInstance()); - Thread.sleep(2000); - return; - } catch (Exception e) { - Thread.sleep(1000); - } - } - - throw new RuntimeException("timeout: gRPC service is not healthy."); - } finally { - channel.shutdown(); - } - } else { - long maxWait = System.currentTimeMillis() + maxWaitMilliseconds; - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_1_1) - .connectTimeout(Duration.ofSeconds(5)) - .build(); - String url = "http://127.0.0.1:" + this.getAppPort() + "/health"; - HttpRequest request = HttpRequest.newBuilder() - .GET() - .uri(URI.create(url)) - .build(); - - while (System.currentTimeMillis() <= maxWait) { - try { - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() == 200) { - Thread.sleep(2000); - return; - } - } catch (IOException e) { - // not ready yet - } - Thread.sleep(1000); - } - - throw new RuntimeException("timeout: HTTP service is not healthy."); - } - } - - public Integer getGrpcPort() { - return ports.getGrpcPort(); - } - - public Integer getHttpPort() { - return ports.getHttpPort(); - } - - public Integer getAppPort() { - return ports.getAppPort(); - } - - public String getAppName() { - return appName; - } - - public DaprClient newDaprClient() { - return new DaprClientBuilder() - .withPropertyOverrides(this.getPropertyOverrides()) - .build(); - } - - public DaprPreviewClient newDaprPreviewClient() { - return new DaprClientBuilder() - .withPropertyOverrides(this.getPropertyOverrides()) - .buildPreviewClient(); - } - - public void checkRunState(long timeout, boolean shouldBeRunning) throws InterruptedException { - callWithRetry(() -> { - try { - this.listCommand.run(); - - if (!shouldBeRunning) { - throw new RuntimeException("Previous run for app has not stopped yet!"); - } - } catch (IllegalStateException e) { - // Bad case if the app is supposed to be running. - if (shouldBeRunning) { - throw e; - } - } catch (Exception e) { - throw new RuntimeException(e); - } - }, timeout); - } - - private static String buildDaprCommand( - String appName, Class serviceClass, DaprPorts ports, AppRun.AppProtocol appProtocol) { - StringBuilder stringBuilder = - new StringBuilder(String.format(DAPR_RUN, appName, appProtocol.toString().toLowerCase())) - .append(ports.getAppPort() != null ? " --app-port " + ports.getAppPort() : "") - .append(ports.getHttpPort() != null ? " --dapr-http-port " + ports.getHttpPort() : "") - .append(ports.getGrpcPort() != null ? " --dapr-grpc-port " + ports.getGrpcPort() : "") - .append(isAppHealthCheckEnabled(serviceClass) ? - " --enable-app-health-check --app-health-probe-interval=1" : "") - .append(serviceClass == null ? "" : - String.format(DAPR_COMMAND, serviceClass.getCanonicalName(), - ports.getAppPort() != null ? ports.getAppPort().toString() : "")); - return stringBuilder.toString(); - } - - private static boolean isAppHealthCheckEnabled(Class serviceClass) { - if (serviceClass != null) { - DaprRunConfig daprRunConfig = (DaprRunConfig) serviceClass.getAnnotation(DaprRunConfig.class); - if (daprRunConfig != null) { - return daprRunConfig.enableAppHealthCheck(); - } - } - - return false; - } - - private static String resolveDaprApiToken(Class serviceClass) { - if (serviceClass != null) { - DaprRunConfig daprRunConfig = (DaprRunConfig) serviceClass.getAnnotation(DaprRunConfig.class); - if (daprRunConfig != null) { - if (!daprRunConfig.enableDaprApiToken()) { - return null; - } - // We use the clas name itself as the token. Just needs to be deterministic. - return serviceClass.getCanonicalName(); - } - } - - // By default, we use a token. - return DEFAULT_DAPR_API_TOKEN; - } - - private static void assertListeningOnPort(int port) { - System.out.printf("Checking port %d ...\n", port); - - java.net.SocketAddress socketAddress = new java.net.InetSocketAddress(Properties.SIDECAR_IP.get(), port); - try (java.net.Socket socket = new java.net.Socket()) { - socket.connect(socketAddress, 1000); - } catch (Exception e) { - throw new RuntimeException(e); - } - - System.out.printf("Confirmed listening on port %d.\n", port); - } - - static class Builder { - - private final String testName; - - private final Supplier portsSupplier; - - private final String successMessage; - - private final int maxWaitMilliseconds; - - private Class serviceClass; - - private AppRun.AppProtocol appProtocol; - - private String daprApiToken; - - Builder( - String testName, - Supplier portsSupplier, - String successMessage, - int maxWaitMilliseconds, - AppRun.AppProtocol appProtocol) { - this.testName = testName; - this.portsSupplier = portsSupplier; - this.successMessage = successMessage; - this.maxWaitMilliseconds = maxWaitMilliseconds; - this.appProtocol = appProtocol; - this.daprApiToken = UUID.randomUUID().toString(); - } - - public Builder withServiceClass(Class serviceClass) { - this.serviceClass = serviceClass; - return this; - } - - DaprRun build() { - return new DaprRun( - this.testName, - this.portsSupplier.get(), - this.successMessage, - this.serviceClass, - this.maxWaitMilliseconds, - this.appProtocol); - } - - /** - * Builds app and dapr run separately. It can be useful to force the restart of one of them. - * @return Pair of AppRun and DaprRun. - */ - ImmutablePair splitBuild() { - DaprPorts ports = this.portsSupplier.get(); - AppRun appRun = new AppRun( - ports, - this.successMessage, - this.serviceClass, - this.maxWaitMilliseconds); - - DaprRun daprRun = new DaprRun( - this.testName, - ports, - DAPR_SUCCESS_MESSAGE, - null, - this.maxWaitMilliseconds, - this.appProtocol, - resolveDaprApiToken(serviceClass)); - - return new ImmutablePair<>(appRun, daprRun); - } - } -} diff --git a/sdk-tests/src/test/java/io/dapr/it/DaprRunConfig.java b/sdk-tests/src/test/java/io/dapr/it/DaprRunConfig.java deleted file mode 100644 index 72e9e3731..000000000 --- a/sdk-tests/src/test/java/io/dapr/it/DaprRunConfig.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2021 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and -limitations under the License. -*/ - -package io.dapr.it; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Customizes an app run for Dapr. - */ -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface DaprRunConfig { - - boolean enableAppHealthCheck() default false; - - boolean enableDaprApiToken() default true; -} diff --git a/sdk-tests/src/test/java/io/dapr/it/ToxiProxyRun.java b/sdk-tests/src/test/java/io/dapr/it/ToxiProxyRun.java deleted file mode 100644 index ad248ec4b..000000000 --- a/sdk-tests/src/test/java/io/dapr/it/ToxiProxyRun.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2023 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and -limitations under the License. -*/ - -package io.dapr.it; - -import eu.rekawek.toxiproxy.Proxy; -import eu.rekawek.toxiproxy.ToxiproxyClient; -import eu.rekawek.toxiproxy.model.ToxicDirection; -import io.dapr.actors.client.ActorClient; -import io.dapr.client.DaprClientBuilder; -import io.dapr.client.resiliency.ResiliencyOptions; -import io.dapr.config.Properties; -import io.dapr.config.Property; -import io.dapr.utils.NetworkUtils; - -import java.io.IOException; -import java.time.Duration; -import java.util.Map; - - -public class ToxiProxyRun implements Stoppable { - - private final DaprRun daprRun; - - private final Duration latency; - - private final Duration jitter; - - private final Command toxiProxyServer; - - private final DaprPorts toxiProxyPorts; - - private ToxiproxyClient toxiproxyClient; - - private Proxy grpcProxy; - - private Proxy httpProxy; - - public ToxiProxyRun(DaprRun run, Duration latency, Duration jitter) { - this.daprRun = run; - this.latency = latency; - this.jitter = jitter; - this.toxiProxyPorts = DaprPorts.build(true, true, true); - // artursouza: we use the "appPort" for the ToxiProxy server. - this.toxiProxyServer = new Command( - "Starting HTTP server on endpoint", - "toxiproxy-server --port " - + this.toxiProxyPorts.getAppPort()); - } - - public void start() throws IOException, InterruptedException { - this.toxiProxyServer.run(); - NetworkUtils.waitForSocket("127.0.0.1", this.toxiProxyPorts.getAppPort(), 10000); - this.toxiproxyClient = new ToxiproxyClient("127.0.0.1", this.toxiProxyPorts.getAppPort()); - - if (this.daprRun.getGrpcPort() != null) { - this.grpcProxy = toxiproxyClient.createProxy( - "daprd_grpc", - "127.0.0.1:" + this.toxiProxyPorts.getGrpcPort(), - "127.0.0.1:" + this.daprRun.getGrpcPort()); - this.grpcProxy.toxics() - .latency("latency", ToxicDirection.DOWNSTREAM, this.latency.toMillis()) - .setJitter(this.jitter.toMillis()); - } - - if (this.daprRun.getHttpPort() != null) { - this.httpProxy = toxiproxyClient.createProxy( - "daprd_http", - "127.0.0.1:" + this.toxiProxyPorts.getHttpPort(), - "127.0.0.1:" + this.daprRun.getHttpPort()); - this.httpProxy.toxics() - .latency("latency", ToxicDirection.DOWNSTREAM, this.latency.toMillis()) - .setJitter(this.jitter.toMillis()); - } - } - - public Map, String> getPropertyOverrides() { - return this.toxiProxyPorts.getPropertyOverrides(); - } - - public DaprClientBuilder newDaprClientBuilder() { - return this.daprRun.newDaprClientBuilder().withPropertyOverrides(this.getPropertyOverrides()); - } - - public ActorClient newActorClient() { - return this.newActorClient(null); - } - - public ActorClient newActorClient(ResiliencyOptions resiliencyOptions) { - return new ActorClient(new Properties(this.getPropertyOverrides()), resiliencyOptions); - } - - @Override - public void stop() throws InterruptedException, IOException { - this.toxiProxyServer.stop(); - this.toxiproxyClient = null; - this.grpcProxy = null; - this.httpProxy = null; - } -} diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/app/MyActorService.java b/sdk-tests/src/test/java/io/dapr/it/actors/app/MyActorService.java index 0bb2bac0e..94f995982 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/app/MyActorService.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/app/MyActorService.java @@ -14,10 +14,7 @@ package io.dapr.it.actors.app; import io.dapr.actors.runtime.ActorRuntime; -import io.dapr.it.DaprRunConfig; -// Enable dapr-api-token once runtime supports it in standalone mode. -@DaprRunConfig(enableDaprApiToken = false) public class MyActorService { public static final String SUCCESS_MESSAGE = "dapr initialized. Status: Running"; diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/services/springboot/StatefulActorService.java b/sdk-tests/src/test/java/io/dapr/it/actors/services/springboot/StatefulActorService.java index c96fa05d0..3a15a0c6b 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/services/springboot/StatefulActorService.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/services/springboot/StatefulActorService.java @@ -14,12 +14,10 @@ package io.dapr.it.actors.services.springboot; import io.dapr.actors.runtime.ActorRuntime; -import io.dapr.it.DaprRunConfig; import io.dapr.serializer.DefaultObjectSerializer; import java.time.Duration; -@DaprRunConfig(enableDaprApiToken = false) public class StatefulActorService { public static final String SUCCESS_MESSAGE = "dapr initialized. Status: Running"; diff --git a/sdk-tests/src/test/java/io/dapr/it/methodinvoke/grpc/MethodInvokeService.java b/sdk-tests/src/test/java/io/dapr/it/methodinvoke/grpc/MethodInvokeService.java index ac7b157af..90d3739c8 100644 --- a/sdk-tests/src/test/java/io/dapr/it/methodinvoke/grpc/MethodInvokeService.java +++ b/sdk-tests/src/test/java/io/dapr/it/methodinvoke/grpc/MethodInvokeService.java @@ -14,7 +14,6 @@ package io.dapr.it.methodinvoke.grpc; import io.dapr.grpc.GrpcHealthCheckService; -import io.dapr.it.DaprRunConfig; import io.dapr.it.MethodInvokeServiceGrpc; import io.grpc.Server; import io.grpc.ServerBuilder; @@ -28,9 +27,6 @@ import static io.dapr.it.MethodInvokeServiceProtos.SleepRequest; import static io.dapr.it.MethodInvokeServiceProtos.SleepResponse; -@DaprRunConfig( - enableAppHealthCheck = true -) public class MethodInvokeService { private static final long STARTUP_DELAY_SECONDS = 10; diff --git a/sdk-tests/src/test/java/io/dapr/it/methodinvoke/http/MethodInvokeService.java b/sdk-tests/src/test/java/io/dapr/it/methodinvoke/http/MethodInvokeService.java index 8b4858fc2..5d0e1cbbc 100644 --- a/sdk-tests/src/test/java/io/dapr/it/methodinvoke/http/MethodInvokeService.java +++ b/sdk-tests/src/test/java/io/dapr/it/methodinvoke/http/MethodInvokeService.java @@ -13,7 +13,6 @@ package io.dapr.it.methodinvoke.http; -import io.dapr.it.DaprRunConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -21,9 +20,6 @@ /** * Service for subscriber. */ -@DaprRunConfig( - enableAppHealthCheck = true -) @SpringBootApplication public class MethodInvokeService { diff --git a/sdk-tests/src/test/java/io/dapr/it/tracing/grpc/Service.java b/sdk-tests/src/test/java/io/dapr/it/tracing/grpc/Service.java index 1aa660d88..fc2e59e05 100644 --- a/sdk-tests/src/test/java/io/dapr/it/tracing/grpc/Service.java +++ b/sdk-tests/src/test/java/io/dapr/it/tracing/grpc/Service.java @@ -15,7 +15,6 @@ import com.google.protobuf.Any; import io.dapr.grpc.GrpcHealthCheckService; -import io.dapr.it.DaprRunConfig; import io.dapr.v1.AppCallbackGrpc; import io.dapr.v1.CommonProtos; import io.grpc.Server; @@ -27,9 +26,6 @@ import static io.dapr.it.MethodInvokeServiceProtos.SleepRequest; import static io.dapr.it.MethodInvokeServiceProtos.SleepResponse; -@DaprRunConfig( - enableAppHealthCheck = true -) public class Service { public static final String SUCCESS_MESSAGE = "application discovered on port "; diff --git a/sdk-tests/src/test/java/io/dapr/it/tracing/http/Service.java b/sdk-tests/src/test/java/io/dapr/it/tracing/http/Service.java index ba4317305..124483d26 100644 --- a/sdk-tests/src/test/java/io/dapr/it/tracing/http/Service.java +++ b/sdk-tests/src/test/java/io/dapr/it/tracing/http/Service.java @@ -13,7 +13,6 @@ package io.dapr.it.tracing.http; -import io.dapr.it.DaprRunConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -21,9 +20,6 @@ /** * Service for subscriber. */ -@DaprRunConfig( - enableAppHealthCheck = true -) @SpringBootApplication public class Service { From ed7a653b3d5d0536602e05eaac620c7d25b0d813 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 21:38:55 -0700 Subject: [PATCH 14/18] Fix actor IT flakiness: share one placement/scheduler per JVM Every migrated actor IT attaches daprd to the JVM-wide shared Network. The builder previously used withReusablePlacement(true); with Testcontainers reuse disabled (as on CI), DaprContainer.configure() auto-creates a placement + scheduler per daprd instance -- each claiming the "placement"/"scheduler" network alias and never stopped (no stop() override). Multiple containers answering one alias make Docker DNS round-robin daprd to an arbitrary/empty control plane, so multi-sidecar failover (ActorReminderFailoverIT) and post-restart reminder recovery (ActorReminderRecoveryIT) hit a placement that never saw the actor-host registrations -> "did not find address for actor" / reminder not resumed. Introduce JVM-singleton placement()/scheduler() in SharedTestInfra on the shared network and wire every daprd to them via withPlacementContainer/ withSchedulerContainer. Exactly one container answers each alias, so DNS resolves deterministically; this also eliminates the per-daprd control-plane container leak. Signed-off-by: Siri Varma Vegiraju --- .../dapr/it/containers/BaseContainerIT.java | 41 ++++++++--------- .../dapr/it/containers/SharedTestInfra.java | 46 +++++++++++++++++++ 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java index f24ce018f..de564c1e3 100644 --- a/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java @@ -24,7 +24,6 @@ import io.dapr.it.testcontainers.ContainerConstants; import io.dapr.testcontainers.Component; import io.dapr.testcontainers.DaprContainer; -import io.dapr.testcontainers.DaprContainerConstants; import io.dapr.testcontainers.DaprLogLevel; import io.dapr.testcontainers.DaprPlacementContainer; import io.dapr.testcontainers.DaprSchedulerContainer; @@ -87,36 +86,32 @@ protected static DaprContainer daprBuilder(String appName) { // errors. Without this, the container's stdout is consumed by Testcontainers // and we have no insight when actor registration or component init fails. .withLogConsumer(frame -> System.out.print("[daprd] " + frame.getUtf8String())) - // Reuses the placement sidecar container within this JVM (Testcontainers manages it); - // orthogonal to SharedTestInfra's Redis `withReuse(true)`. - .withReusablePlacement(true); + // Wire every daprd to the ONE shared placement + scheduler on the shared network. + // daprd resolves its control plane by DNS name ("placement"/"scheduler"), so there + // must be exactly one container answering each alias. DaprContainer would otherwise + // auto-create its own placement/scheduler per instance and never stop them (there is + // no stop() override), leaving several containers sharing the "placement"/"scheduler" + // aliases on the shared network -- Docker DNS then round-robins daprd to an arbitrary + // (often empty) one, breaking multi-sidecar failover and sidecar-restart reminder + // recovery. The JVM-singletons make each alias resolve to exactly one container and + // eliminate the per-container control-plane leak. + .withPlacementContainer(SharedTestInfra.placement()) + .withSchedulerContainer(SharedTestInfra.scheduler()); } // ---------- Shared control plane (multi-sidecar ITs) ---------- - /** Shared placement for multi-sidecar ITs. Explicit (not reuse-based) so it is - * deterministic on CI where Testcontainers reuse is disabled. */ + /** The JVM-wide shared placement. Multi-sidecar ITs (e.g. failover) pass this to every + * daprd via {@code withPlacementContainer} so all sidecars share one placement -- the + * same singleton {@link #daprBuilder} wires by default. Not stopped per class; it lives + * for the JVM (see {@link SharedTestInfra#placement()}). */ protected static DaprPlacementContainer startSharedPlacement() { - DaprPlacementContainer placement = - new DaprPlacementContainer(DaprContainerConstants.DAPR_PLACEMENT_IMAGE_TAG) - .withNetwork(SharedTestInfra.network()) - .withNetworkAliases("placement") - .withReuse(false); - placement.start(); - deferStop(placement); - return placement; + return SharedTestInfra.placement(); } - /** Shared scheduler for multi-sidecar ITs (owns actor reminders). */ + /** The JVM-wide shared scheduler (owns actor reminders). See {@link #startSharedPlacement()}. */ protected static DaprSchedulerContainer startSharedScheduler() { - DaprSchedulerContainer scheduler = - new DaprSchedulerContainer(DaprContainerConstants.DAPR_SCHEDULER_IMAGE_TAG) - .withNetwork(SharedTestInfra.network()) - .withNetworkAliases("scheduler") - .withReuse(false); - scheduler.start(); - deferStop(scheduler); - return scheduler; + return SharedTestInfra.scheduler(); } // ---------- App lifecycle ---------- diff --git a/sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java b/sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java index 5e61267aa..0fa55770b 100644 --- a/sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java +++ b/sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java @@ -13,6 +13,9 @@ package io.dapr.it.containers; +import io.dapr.testcontainers.DaprContainerConstants; +import io.dapr.testcontainers.DaprPlacementContainer; +import io.dapr.testcontainers.DaprSchedulerContainer; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.utility.DockerImageName; @@ -30,12 +33,16 @@ public final class SharedTestInfra { private static final String ZIPKIN_NETWORK_ALIAS = "zipkin"; private static final String MONGO_NETWORK_ALIAS = "mongo"; private static final String KAFKA_NETWORK_ALIAS = "kafka"; + private static final String PLACEMENT_NETWORK_ALIAS = "placement"; + private static final String SCHEDULER_NETWORK_ALIAS = "scheduler"; private static volatile Network network; private static volatile GenericContainer redis; private static volatile GenericContainer zipkin; private static volatile GenericContainer mongo; private static volatile org.testcontainers.kafka.KafkaContainer kafka; + private static volatile DaprPlacementContainer placement; + private static volatile DaprSchedulerContainer scheduler; private SharedTestInfra() {} @@ -109,4 +116,43 @@ public static synchronized org.testcontainers.kafka.KafkaContainer kafka() { public static String kafkaInternalBroker() { return KAFKA_NETWORK_ALIAS + ":19092"; } + + /** + * The single Dapr placement service shared by every daprd in the JVM. + * + *

daprd resolves its placement service by DNS name ({@code placement:50005}), so + * there must be exactly ONE container answering to the {@code "placement"} alias on the + * shared network. If several exist (which happens when each {@code DaprContainer} + * auto-creates its own, never-stopped placement), Docker DNS round-robins daprd to an + * arbitrary one -- often an empty placement that never received this test's actor-host + * registrations -- producing "did not find address for actor" in multi-sidecar failover + * and losing the actor host across a sidecar restart. Sharing this singleton (wired via + * {@code DaprContainer.withPlacementContainer}) guarantees a single DNS answer. + */ + public static synchronized DaprPlacementContainer placement() { + if (placement == null) { + placement = new DaprPlacementContainer(DaprContainerConstants.DAPR_PLACEMENT_IMAGE_TAG) + .withNetwork(network()) + .withNetworkAliases(PLACEMENT_NETWORK_ALIAS) + .withReuse(true); + placement.start(); + } + return placement; + } + + /** + * The single Dapr scheduler service shared by every daprd in the JVM. Owns actor + * reminders in Dapr 1.18+, so it must stay up across a daprd restart for reminder + * recovery to work. Same single-DNS-answer requirement as {@link #placement()}. + */ + public static synchronized DaprSchedulerContainer scheduler() { + if (scheduler == null) { + scheduler = new DaprSchedulerContainer(DaprContainerConstants.DAPR_SCHEDULER_IMAGE_TAG) + .withNetwork(network()) + .withNetworkAliases(SCHEDULER_NETWORK_ALIAS) + .withReuse(true); + scheduler.start(); + } + return scheduler; + } } From a92cb9bce7d0a8215e3d408b2a7db6c2e535bc87 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 21:48:57 -0700 Subject: [PATCH 15/18] docs: remove migration plan/spec scratch files These were working documents for the Testcontainers migration and are not part of the shipped SDK; dropping them from the PR. Signed-off-by: Siri Varma Vegiraju --- ...nish-sdk-tests-testcontainers-migration.md | 422 ------------------ ...k-tests-testcontainers-migration-design.md | 261 ----------- 2 files changed, 683 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-09-finish-sdk-tests-testcontainers-migration.md delete mode 100644 docs/superpowers/specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md diff --git a/docs/superpowers/plans/2026-07-09-finish-sdk-tests-testcontainers-migration.md b/docs/superpowers/plans/2026-07-09-finish-sdk-tests-testcontainers-migration.md deleted file mode 100644 index 111779dab..000000000 --- a/docs/superpowers/plans/2026-07-09-finish-sdk-tests-testcontainers-migration.md +++ /dev/null @@ -1,422 +0,0 @@ -# Finish sdk-tests Testcontainers Migration — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Migrate the last 7 `sdk-tests` integration tests off the legacy `dapr run` CLI harness onto `BaseContainerIT`/`DaprContainer`, then strip the Dapr CLI / `dapr init` / host-Kafka / host-ToxiProxy steps from the CI `build` job. - -**Architecture:** Each test drops `extends BaseIT` and its `DaprRun`/`startDaprApp` usage, defining Dapr components in-code and running the sidecar in a `DaprContainer`. New reusable mechanics (`restartApp`, `restartSidecar`, shared placement/scheduler, ToxiProxy, Kafka) are added to `BaseContainerIT`/`SharedTestInfra`, each introduced in the first task that consumes it so it is exercised immediately. Once all 7 are migrated, the dead legacy harness and CI setup are deleted. - -**Tech Stack:** Java 17, JUnit 5, Testcontainers 2.0.5 (`org.testcontainers:testcontainers-*`, BOM-managed), `io.dapr:testcontainers-dapr` (`DaprContainer`, `DaprPlacementContainer`, `DaprSchedulerContainer`), Dapr runtime 1.18.0, Maven Failsafe (`integration-tests` profile). - -**Spec:** [docs/superpowers/specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md](../specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md) - ---- - -## Conventions for every task - -- **Reference patterns to copy** (already-migrated, working): - - Actor + app: [ActivationDeactivationIT.java](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActivationDeactivationIT.java) - - ToxiProxy + `DaprContainer`: [SdkResiliencyIT.java](../../../sdk-tests/src/test/java/io/dapr/it/resiliency/SdkResiliencyIT.java) - - Shared placement/scheduler multi-sidecar: [WorkflowsMultiAppCallActivityIT.java](../../../sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/multiapp/WorkflowsMultiAppCallActivityIT.java) -- **Base class API to use:** [BaseContainerIT.java](../../../sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java) — `daprBuilder(appName)`, `startAppAndAttach(name, serviceClass, protocol, daprFactory)`, `newDaprClient(dapr)`, `newActorClient(dapr)`, `redisStateStore(name)`, `waitForActorsReady(dapr)`, `deferStop(...)`, `deferClose(...)`, `DAPR_IMAGE`, `STATE_STORE_NAME`. -- **When rewriting a test:** preserve every assertion, `Thread.sleep`/`callWithRetry`/`Awaitility` wait and its duration, and the actor type / service class exactly as in the legacy file. Only the *setup/teardown* and *restart mechanics* change from `DaprRun` to `DaprContainer`. -- **Per-test verification command** (requires a running Docker daemon; Testcontainers uses it): - ```bash - cd /Users/svegiraju/Git/java-sdk && \ - ./mvnw -B -pl sdk-tests -Pintegration-tests -Dit.test= \ - dependency:copy-dependencies verify - ``` - Expected: `BUILD SUCCESS`, the named IT runs (not skipped, except `ActorSdkResiliencyIT` whose one test is `@Disabled`). If the local Docker daemon is unavailable, state that the check must run in CI and do not claim local success. -- **Commits:** sign off every commit (`git commit -s`). Do NOT add a Co-Authored-By trailer. -- **Timing-sensitive tests** (Tasks 2, 3, 4): after the first pass, run the verification command **3 times** and confirm all 3 pass before committing; if any flakes, escalate (do not loosen assertions without re-planning). - ---- - -## Task 1: Migrate ActorStateIT + add `restartApp` helper (Group A) - -**Files:** -- Modify: `sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java` (add `restartApp`) -- Rewrite: `sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java` - -**Legacy behavior to preserve** (read [ActorStateIT.java](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java)): single `writeReadState()` test. Phase 1 — build an `ActorProxy` for actor type `StatefulActorTest` (service class `StatefulActorService`), write string/`MyData`/name/empty-name/bytes state, reading each back. Phase 2 — **restart the app**, rebuild the proxy with the **same** `actorId`, and assert every previously-written value is still readable (proves state persisted to Redis). Keep all `callWithRetry` timeouts and the 5s/10s/2s waits. - -- [ ] **Step 1: Add `restartApp` to `BaseContainerIT`** (place after `startAppAndAttach`, before the client factories): - -```java - /** - * Restarts the app subprocess on its same pre-allocated port. The daprd - * container stays up and reconnects to the app via - * {@code host.testcontainers.internal:appPort}. Because {@link #daprBuilder} - * configures NO app-health-check, daprd does not deactivate actors during the - * gap, so in-memory timers survive (matching the legacy - * {@code @DaprRunConfig(enableAppHealthCheck=false)}). There is intentionally - * no sleep between stop and start — {@code ActorTimerRecoveryIT} relies on a - * quick restart. - */ - protected static void restartApp(AppRun app) throws Exception { - app.stop(); - app.start(); - } -``` - -- [ ] **Step 2: Rewrite `ActorStateIT`** to `extends BaseContainerIT`. Use the `ActivationDeactivationIT` `@BeforeAll` shape but with `StatefulActorService.class`: - -```java -var pair = startAppAndAttach( - "actor-state-it", - io.dapr.it.actors.services.springboot.StatefulActorService.class, - AppRun.AppProtocol.HTTP, - appPort -> daprBuilder("actor-state-it") - .withAppPort(appPort) - .withAppChannelAddress("host.testcontainers.internal") - .withComponent(redisStateStore(STATE_STORE_NAME))); -dapr = pair.dapr(); -app = pair.app(); -waitForActorsReady(dapr); -``` - Build the proxy via `new ActorProxyBuilder("StatefulActorTest", ActorProxy.class, newActorClient(dapr))`. Replace the legacy "stop `DaprRun`, `startDaprApp` again" restart with `restartApp(app)`, then **rebuild the proxy off a fresh `newActorClient(dapr)`** with the same `actorId`. Remove all `BaseIT`/`DaprRun`/`startDaprApp` imports and usage. - -- [ ] **Step 3: Verify** — run the per-test command with `-Dit.test=ActorStateIT`. Expected: PASS, `writeReadState` runs and green. - -- [ ] **Step 4: Commit** -```bash -git add sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java \ - sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java -git commit -s -m "test: migrate ActorStateIT to Testcontainers (+ restartApp helper)" -``` - ---- - -## Task 2: Migrate ActorTimerRecoveryIT (Group A) - -**Files:** -- Rewrite: `sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java` - -**Legacy behavior to preserve** (read [ActorTimerRecoveryIT.java](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java)): single `timerRecoveryTest()`. Actor type `MyActorTest`, service class `MyActorService`. Register a timer (`startTimer("myTimer")`, callback `clock`, ~2s delay / 3s period, message `"ping!"`); wait for ≥3 fires (30s `callWithRetry`); **restart the app with NO sleep between stop and start**; assert the timer keeps firing (≥3 more, 30s `callWithRetry`); assert none of the new log entries equal old entries (proves real restart); `stopTimer("myTimer")` at the end. - -- [ ] **Step 1: Rewrite `ActorTimerRecoveryIT`** to `extends BaseContainerIT`, mirroring Task 1's `startAppAndAttach` setup but with `MyActorService.class` and app name `"actor-timer-recovery-it"`, actor type `MyActorTest`. Replace `runs.left.stop(); runs.left.start();` with `restartApp(app);` (the helper already omits any inter-step sleep). Keep every wait/assertion. - -- [ ] **Step 2: Verify (×3)** — run `-Dit.test=ActorTimerRecoveryIT` three times; all PASS. The timer must survive the restart (relies on daprBuilder setting no health-check — see the invariant note in the spec). If the timer is lost, escalate. - -- [ ] **Step 3: Commit** -```bash -git add sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java -git commit -s -m "test: migrate ActorTimerRecoveryIT to Testcontainers" -``` - ---- - -## Task 3: Migrate ActorReminderRecoveryIT + add `restartSidecar` helper (Group B) - -**Files:** -- Modify: `sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java` (add `restartSidecar`) -- Rewrite: `sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java` - -**Legacy behavior to preserve** (read [ActorReminderRecoveryIT.java](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java)): `@ParameterizedTest` over 4 data variants (String `"36"`, String `"\"my_text\""`, `byte[]{0,1}`, object `{"name":"abc","age":30}`) with actor types `MyActorTest`/`MyActorBinaryTest`/`MyActorObjectTest`. Register reminder (fires ~every 2s); wait for ≥3 fires; **stop the daprd sidecar**; sleep 10s; **start the sidecar**; sleep 7s; assert the reminder resumed and fired ≥3 more times. Per the spec, the reminder lives in the scheduler container which survives the daprd restart. Keep the separate actor-client (build via `newActorClient(dapr)`, NOT a second app — see spec note). - -- [ ] **Step 1: Add `restartSidecar` to `BaseContainerIT`** (after `restartApp`): - -```java - /** - * Restarts the daprd container in place and re-waits for readiness. Placement - * and scheduler are NOT recreated on the second start (their DaprContainer - * fields are non-null), so a persisted actor reminder survives. Pinned host - * ports re-bind, so the app's DAPR_HTTP_PORT/DAPR_GRPC_PORT and any DaprClient - * remain valid. - */ - protected static void restartSidecar(DaprContainer dapr) { - dapr.stop(); - dapr.start(); - try (DaprClient client = newDaprClient(dapr)) { - client.waitForSidecar(30_000).block(); - } - waitForActorsReady(dapr); - } -``` - -- [ ] **Step 2: Rewrite `ActorReminderRecoveryIT`** to `extends BaseContainerIT`. Use `startAppAndAttach` with `MyActorService.class` (app name `"actor-reminder-recovery-it"`, `redisStateStore`). Build the reminder proxy(ies) via `newActorClient(dapr)`. Replace the legacy `runs.right.stop()` / `runs.right.start()` sidecar restart with `restartSidecar(dapr)`, keeping the surrounding 10s and 7s sleeps. Preserve the parameterized data and all assertions. - -- [ ] **Step 3: Verify (×3)** — run `-Dit.test=ActorReminderRecoveryIT` three times; all PASS across all 4 parameterized variants. If reminders do not survive the restart, escalate (fallback in spec: reminder in Redis-backed state). - -- [ ] **Step 4: Commit** -```bash -git add sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java \ - sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java -git commit -s -m "test: migrate ActorReminderRecoveryIT to Testcontainers (+ restartSidecar helper)" -``` - ---- - -## Task 4: Migrate ActorReminderFailoverIT + add shared placement/scheduler helpers (Group C) - -**Files:** -- Modify: `sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java` (add shared placement/scheduler helpers) -- Rewrite: `sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderFailoverIT.java` - -**Legacy behavior to preserve** (source shown above): `reminderRecoveryTest()` — two `MyActorService` app-sidecars (`One`, `Two`) hosting actor type `MyActorTest`, plus a **client-only** sidecar (no app) whose `ActorClient` builds the proxy. Register reminder `myReminder`; wait 7s; assert ≥3 fires; read the actor host via `getIdentifier()` (returns the host sidecar's `DAPR_HTTP_PORT`); **stop the `AppRun` whose sidecar port matches**; sleep 10s; sleep 10s more; assert ≥4 additional fires; assert `getIdentifier()` now returns a **different** host. `@AfterEach` calls `stopReminder`. - -**Topology:** failover is keyed on **actor type**, not app-id — placement distributes `MyActorTest` across every sidecar that registered it and rebalances when one host's app dies. So all sidecars must share ONE placement + ONE scheduler + Redis. - -- [ ] **Step 1: Add shared control-plane helpers to `BaseContainerIT`** (import `io.dapr.testcontainers.DaprPlacementContainer`, `io.dapr.testcontainers.DaprSchedulerContainer`, `io.dapr.testcontainers.DaprContainerConstants`): - -```java - /** Shared placement for multi-sidecar ITs. Explicit (not reuse-based) so it is - * deterministic on CI where Testcontainers reuse is disabled. */ - protected static DaprPlacementContainer startSharedPlacement() { - DaprPlacementContainer placement = - new DaprPlacementContainer(DaprContainerConstants.DAPR_PLACEMENT_IMAGE_TAG) - .withNetwork(SharedTestInfra.network()) - .withNetworkAliases("placement") - .withReuse(false); - placement.start(); - deferStop(placement); - return placement; - } - - /** Shared scheduler for multi-sidecar ITs (owns actor reminders). */ - protected static DaprSchedulerContainer startSharedScheduler() { - DaprSchedulerContainer scheduler = - new DaprSchedulerContainer(DaprContainerConstants.DAPR_SCHEDULER_IMAGE_TAG) - .withNetwork(SharedTestInfra.network()) - .withNetworkAliases("scheduler") - .withReuse(false); - scheduler.start(); - deferStop(scheduler); - return scheduler; - } -``` - -- [ ] **Step 2: Rewrite `ActorReminderFailoverIT`** to `extends BaseContainerIT`. In `@BeforeEach`: - 1. `DaprPlacementContainer placement = startSharedPlacement(); DaprSchedulerContainer scheduler = startSharedScheduler();` - 2. Start two actor hosts with `startAppAndAttach("failover-one" / "failover-two", MyActorService.class, HTTP, factory)` where each `factory` is: - ```java - appPort -> daprBuilder(name) - .withPlacementContainer(placement) - .withSchedulerContainer(scheduler) - .withAppPort(appPort) - .withAppChannelAddress("host.testcontainers.internal") - .withComponent(redisStateStore(STATE_STORE_NAME)) - ``` - (Passing an explicit placement/scheduler makes `DaprContainer.configure()` skip creating its own — the shared ones are used.) - 3. Start a **client-only** sidecar (no app): build `new DaprContainer(DAPR_IMAGE).withAppName("failover-client").withNetwork(SharedTestInfra.network()).withPlacementContainer(placement).withSchedulerContainer(scheduler).withComponent(redisStateStore(STATE_STORE_NAME)).withDaprLogLevel(DaprLogLevel.DEBUG)`, `.start()`, `deferStop(...)`. Build the proxy via `newActorClient(clientSidecar)`. - 4. `waitForActorsReady` on both actor-host sidecars. - 5. Keep the `Thread.sleep(3000)` and actor id / type (`MyActorTest`). - In the test body, replace `firstAppRun.getHttpPort()` / `secondAppRun.getHttpPort()` comparisons with the two host sidecars' `dapr.getHttpPort()`, and `firstAppRun.stop()` with the matching `AppRun.stop()` (the `AppRun` from the corresponding `startAppAndAttach` pair). Preserve every wait/assertion. - -- [ ] **Step 3: Verify (×3)** — run `-Dit.test=ActorReminderFailoverIT` three times; all PASS (actor migrates to the surviving host and reminder continues). This is the highest-risk migration; if placement does not fail over across sidecars, STOP and escalate to re-plan rather than adjusting assertions. - -- [ ] **Step 4: Commit** -```bash -git add sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java \ - sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderFailoverIT.java -git commit -s -m "test: migrate ActorReminderFailoverIT to Testcontainers (+ shared placement/scheduler)" -``` - ---- - -## Task 5: Migrate WaitForSidecarIT + add `newToxiproxy` helper (Group D) - -**Files:** -- Modify: `sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java` (add `newToxiproxy`) -- Rewrite: `sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java` - -**Legacy behavior to preserve** (read [WaitForSidecarIT.java](../../../sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java) and copy the ToxiProxy wiring from [SdkResiliencyIT.java](../../../sdk-tests/src/test/java/io/dapr/it/resiliency/SdkResiliencyIT.java)): four tests — `waitSucceeds()` (direct sidecar, `waitForSidecar(5000)` OK), `waitTimeout()` (5s latency toxic, timeout 4900ms → `RuntimeException`, duration ≥ timeout), `waitSlow()` (timeout 5100ms → OK, duration ≥ 5s), `waitNotRunningTimeout()` (unavailable sidecar, timeout 5000ms → `RuntimeException`). Latency constant = 5s. No app needed. - -- [ ] **Step 1: Add `newToxiproxy` to `BaseContainerIT`** (import `org.testcontainers.toxiproxy.ToxiproxyContainer` — the same class `SdkResiliencyIT` uses, paired with the `eu.rekawek.toxiproxy.ToxiproxyClient` wiring — and `io.dapr.it.testcontainers.ContainerConstants`): - -```java - /** Starts a Toxiproxy container on the shared network and registers it for - * @AfterAll cleanup. Callers create proxies via a ToxiproxyClient against - * {@code getHost()}/{@code getControlPort()} (the control channel), then point - * Dapr clients at {@code getMappedPort()} of a proxy created on a - * fixed listen port (e.g. 8666). Mirrors SdkResiliencyIT. */ - protected static ToxiproxyContainer newToxiproxy() { - ToxiproxyContainer toxiproxy = - new ToxiproxyContainer(ContainerConstants.TOXI_PROXY_IMAGE_TAG) - .withNetwork(SharedTestInfra.network()); - toxiproxy.start(); - deferStop(toxiproxy); - return toxiproxy; - } -``` - -- [ ] **Step 2: Rewrite `WaitForSidecarIT`** to `extends BaseContainerIT`. In `@BeforeAll`: start a `DaprContainer` via `daprBuilder("wait-for-sidecar-it").withNetworkAliases("dapr")` (in-memory kvstore is fine, no app), `deferStop`; `newToxiproxy()`; create a proxy `dapr:3500`/`dapr:50001` via `ToxiproxyClient` (see `SdkResiliencyIT` lines 122-123). Build the "latency" client pointed at `toxiproxy.getMappedPort()` and the direct client via `newDaprClient(dapr)`. For `waitNotRunningTimeout`, point a client at an unbound/stopped endpoint (e.g. a proxy with no upstream, or a stopped container's former mapped port). Add/remove the 5s latency toxic per test as in `SdkResiliencyIT`. Preserve all four tests, their timeouts, and duration assertions. - -- [ ] **Step 3: Verify** — run `-Dit.test=WaitForSidecarIT`. Expected: PASS, all 4 tests run. - -- [ ] **Step 4: Commit** -```bash -git add sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java \ - sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java -git commit -s -m "test: migrate WaitForSidecarIT to Testcontainers (+ newToxiproxy helper)" -``` - ---- - -## Task 6: Migrate ActorSdkResiliencyIT (Group D — stays @Disabled) - -**Files:** -- Rewrite: `sdk-tests/src/test/java/io/dapr/it/actors/ActorSdkResiliencyIT.java` - -**Legacy behavior to preserve** (read [ActorSdkResiliencyIT.java](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorSdkResiliencyIT.java)): one `@Test retryAndTimeout()` that is `@Disabled("Flaky when running on GitHub actions")`. Actor type `DemoActorTest`, service class `DemoActorService`. It routes an `ActorClient` (with `ResiliencyOptions`) through ToxiProxy latency/jitter and compares error counts across retry configs. **Keep the test `@Disabled`** — the goal is only to remove the `ToxiProxyRun` dependency, not to re-enable it. - -- [ ] **Step 1: Rewrite `ActorSdkResiliencyIT`** to `extends BaseContainerIT`. Combine the actor pattern (`startAppAndAttach` with `DemoActorService.class`, app name `"actor-sdk-resiliency-it"`) with the ToxiProxy wiring from Task 5 (`newToxiproxy()` + a proxy to `dapr:50001`). Build the resilient `ActorClient`s against the ToxiProxy mapped gRPC port using `new ActorClient(new Properties(overrides), resiliencyOptions)` where `overrides` map `GRPC_ENDPOINT`/`GRPC_PORT` to the proxy. Remove `BaseIT`/`DaprRun`/`ToxiProxyRun` imports and usage. Preserve the `@Disabled` annotation and the test body's assertions. - -- [ ] **Step 2: Verify** — run `-Dit.test=ActorSdkResiliencyIT`. Expected: `BUILD SUCCESS`; the class compiles and its one test is reported **skipped** (still `@Disabled`). This confirms the migration compiles and no longer references the legacy harness. - -- [ ] **Step 3: Commit** -```bash -git add sdk-tests/src/test/java/io/dapr/it/actors/ActorSdkResiliencyIT.java -git commit -s -m "test: migrate ActorSdkResiliencyIT off ToxiProxyRun (kept @Disabled)" -``` - ---- - -## Task 7: Migrate BindingIT + Kafka container (Group E) - -**Files:** -- Modify: `sdk-tests/pom.xml` (add `testcontainers-kafka`) -- Modify: `sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java` (add `kafka()`) -- Modify: `sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java` (add `kafkaBinding` + `httpBinding` component helpers) -- Rewrite: `sdk-tests/src/test/java/io/dapr/it/binding/http/BindingIT.java` - -**Legacy behavior to preserve** (read [BindingIT.java](../../../sdk-tests/src/test/java/io/dapr/it/binding/http/BindingIT.java)): three tests. `httpOutputBindingError()` — invoke `github-http-binding-404` → expect `DaprException` 404. `httpOutputBindingErrorIgnoredByComponent()` — invoke `github-http-binding-404-success` (`errorIfNot2XX=false`) → expect the GitHub 404 JSON payload. `inputOutputBinding()` — Kafka component `sample123`, service class `InputBindingService`: health-check the input binding, publish a `MyClass{message="hello"}` and a string `"cat"` via the output binding, then GET `/messages` and assert both arrive. Keep the GitHub URLs unchanged (spec D3). The two HTTP components are `bindings.http` (URL `https://api.github.com/unknown_path`; the `-success` one adds `errorIfNot2XX: "false"`); the Kafka component is `bindings.kafka` with `topics`/`publishTopic` = `topic-{appID}`, `consumerGroup` = `{appID}`, `authRequired=false`, `initialOffset=oldest`. - -- [ ] **Step 1: Add the Kafka dependency** to `sdk-tests/pom.xml` (in ``, no `` — managed by the root `testcontainers-bom`): -```xml - - org.testcontainers - testcontainers-kafka - test - -``` - -- [ ] **Step 2: Add `kafka()` to `SharedTestInfra`** (mirror the `redis()`/`mongo()` lazy-singleton style; use `org.testcontainers.kafka.KafkaContainer` with an in-network listener so the daprd container can reach it): -```java - private static final String KAFKA_NETWORK_ALIAS = "kafka"; - private static volatile org.testcontainers.kafka.KafkaContainer kafka; - - public static synchronized org.testcontainers.kafka.KafkaContainer kafka() { - if (kafka == null) { - kafka = new org.testcontainers.kafka.KafkaContainer("apache/kafka:3.8.0") - .withNetwork(network()) - .withNetworkAliases(KAFKA_NETWORK_ALIAS) - .withListener(KAFKA_NETWORK_ALIAS + ":19092") // advertised on the shared network - .withReuse(true); - kafka.start(); - } - return kafka; - } - - /** Broker address reachable from other containers on the shared network. */ - public static String kafkaInternalBroker() { - return KAFKA_NETWORK_ALIAS + ":19092"; - } -``` - > If the Testcontainers 2.0.5 `KafkaContainer` API for the in-network listener differs from `.withListener(String)`, consult the Testcontainers Kafka docs and adjust; the requirement is a broker advertised as `kafka:19092` on `SharedTestInfra.network()`. Do not guess silently — verify against the resolved API. - -- [ ] **Step 3: Add component helpers to `BaseContainerIT`**: -```java - protected static Component kafkaBinding(String name) { - SharedTestInfra.kafka(); - return new Component(name, "bindings.kafka", "v1", Map.of( - "brokers", SharedTestInfra.kafkaInternalBroker(), - "topics", "topic-{appID}", - "publishTopic", "topic-{appID}", - "consumerGroup", "{appID}", - "authRequired", "false", - "initialOffset", "oldest" - )); - } - - protected static Component httpBinding(String name, String url, boolean errorIfNot2xx) { - return new Component(name, "bindings.http", "v1", Map.of( - "url", url, - "errorIfNot2XX", Boolean.toString(errorIfNot2xx) - )); - } -``` - -- [ ] **Step 4: Rewrite `BindingIT`** to `extends BaseContainerIT`. For the two HTTP tests, start a `DaprContainer` (no app) via `daprBuilder(...)` with `.withComponent(httpBinding("github-http-binding-404", "https://api.github.com/unknown_path", true))` (and the `-success` variant with `false`); invoke via `newDaprClient(dapr)`. For `inputOutputBinding`, use `startAppAndAttach("bindingit-grpc", InputBindingService.class, HTTP, factory)` where the factory adds `.withComponent(kafkaBinding("sample123"))`; keep the health-check-then-publish-then-read flow and both assertions. Remove all `BaseIT`/`DaprRun`/`startDaprApp`/file-component usage. - -- [ ] **Step 5: Verify** — run `-Dit.test=BindingIT`. Expected: PASS, all 3 tests run (the two HTTP tests require outbound network to api.github.com, as before). - -- [ ] **Step 6: Commit** -```bash -git add sdk-tests/pom.xml \ - sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java \ - sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java \ - sdk-tests/src/test/java/io/dapr/it/binding/http/BindingIT.java -git commit -s -m "test: migrate BindingIT to Testcontainers Kafka + in-code bindings" -``` - ---- - -## Task 8: Strip legacy setup from the CI `build` job - -**Files:** -- Modify: `.github/workflows/build.yml` (the `build` job only) - -- [ ] **Step 1: Edit `build.yml`** — in the `build` job (`name: "Build jdk:… sb:…"`), remove these steps and env: "Set up Dapr CLI"; the Go setup + `dapr/cli` & `dapr/dapr` checkout/override/placement steps (all `DAPR_REF`/`DAPR_CLI_REF` conditionals); "Uninstall Dapr runtime" + "Ensure Dapr runtime uninstalled" wait loop; "Initialize Dapr runtime"; "Spin local environment" (`docker compose … local-test.yml up -d kafka`); "Install local ToxiProxy"; and the now-unused env keys `GOVER`/`GOOS`/`GOARCH`/`GOPROXY`/`DAPR_CLI_VER`/`DAPR_RUNTIME_VER`/`DAPR_INSTALL_URL`/`DAPR_CLI_REF`/`DAPR_REF`/`TOXIPROXY_URL`. **Keep:** checkout, `docker version`, setup-java, `./mvnw clean install -DskipTests`, the sb 3.x / sb 4.x integration-test run steps, and all failsafe/surefire report-upload steps. Do NOT touch the `test`, `build-durabletask`, or `publish` jobs. - -- [ ] **Step 2: Verify YAML** — `yamllint .github/workflows/build.yml` if available, else confirm the `build` job still has: checkout → setup-java → `mvnw clean install -DskipTests` → integration-test run → report uploads, and references no removed env var (grep for `DAPR_CLI_VER`, `TOXIPROXY_URL`, `local-test.yml`, `dapr init`, `dapr uninstall` → expect no matches in `build.yml`). - -- [ ] **Step 3: Commit** -```bash -git add .github/workflows/build.yml -git commit -s -m "ci: drop Dapr CLI/init/Kafka/ToxiProxy setup from build job" -``` - ---- - -## Task 9: Delete dead legacy harness + full-module verify - -**Files:** -- Delete: `sdk-tests/src/test/java/io/dapr/it/BaseIT.java`, `DaprRun.java`, `DaprRunConfig.java`, `ToxiProxyRun.java` -- Delete: `sdk-tests/components/kafka_bindings.yaml`, `sdk-tests/components/http_binding.yaml`, `sdk-tests/deploy/local-test.yml` -- **Keep:** `AppRun.java`, `DaprPorts.java`, `Command.java`, `Stoppable.java`, `SharedTestInfra.java`, `BaseContainerIT.java` - -- [ ] **Step 1: Confirm zero references** before deleting each file: -```bash -cd /Users/svegiraju/Git/java-sdk -for c in BaseIT DaprRun DaprRunConfig ToxiProxyRun; do - echo "== $c =="; grep -rn "\b$c\b" sdk-tests/src --include=*.java | grep -v "/$c.java:" || echo " (no refs)" -done -``` - Expected: `(no refs)` for all four. If any reference remains, that test was not fully migrated — fix it before deleting. Also grep for remaining consumers of the two YAMLs / `local-test.yml`: -```bash -grep -rn "kafka_bindings\|http_binding\|local-test.yml" sdk-tests .github || echo "(no refs)" -``` - -- [ ] **Step 2: Delete the files** (only after Step 1 is clean): -```bash -git rm sdk-tests/src/test/java/io/dapr/it/BaseIT.java \ - sdk-tests/src/test/java/io/dapr/it/DaprRun.java \ - sdk-tests/src/test/java/io/dapr/it/DaprRunConfig.java \ - sdk-tests/src/test/java/io/dapr/it/ToxiProxyRun.java \ - sdk-tests/components/kafka_bindings.yaml \ - sdk-tests/components/http_binding.yaml \ - sdk-tests/deploy/local-test.yml -``` - -- [ ] **Step 3: Full-module verify** — build the whole SDK then run every `sdk-tests` IT with the trimmed setup (requires Docker): -```bash -cd /Users/svegiraju/Git/java-sdk -./mvnw clean install -B -q -DskipTests && \ -./mvnw -B -pl sdk-tests -Pintegration-tests dependency:copy-dependencies verify -``` - Expected: `BUILD SUCCESS`; all 20 sdk-tests ITs run (the one `ActorSdkResiliencyIT` test skipped); no compilation error from the deletions. If Docker is unavailable locally, state that this must be validated in CI and push the branch. - -- [ ] **Step 4: Commit** -```bash -git add -A -git commit -s -m "test: remove dead dapr-run harness, binding YAMLs, and local-test.yml" -``` - ---- - -## Done criteria - -- All 7 ITs extend `BaseContainerIT`; no `sdk-tests` file references `BaseIT`/`DaprRun`/`DaprRunConfig`/`ToxiProxyRun`. -- `build.yml`'s `build` job installs no Dapr CLI and runs no `dapr init`/`dapr uninstall`/host Kafka/host ToxiProxy. -- Full `sdk-tests` `verify` is green in CI; the `build sb:3.5.x` job completes materially faster than 26 min (target ≈ the sb:4.0.x job) and well under the 45-min timeout — satisfying issue #1522's `<20 min` acceptance criterion. diff --git a/docs/superpowers/specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md b/docs/superpowers/specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md deleted file mode 100644 index e385762c3..000000000 --- a/docs/superpowers/specs/2026-07-09-finish-sdk-tests-testcontainers-migration-design.md +++ /dev/null @@ -1,261 +0,0 @@ -# Finish sdk-tests Testcontainers Migration + Strip Legacy CI — Design - -**Date:** 2026-07-09 -**Status:** Draft (pending spec review) -**Author:** Siri Varma Vegiraju (with Claude) -**Scope:** [sdk-tests/](../../../sdk-tests/) module and [.github/workflows/build.yml](../../../.github/workflows/build.yml) -**Issue:** [dapr/java-sdk#1522](https://github.com/dapr/java-sdk/issues/1522) — Optimize CI/CD build times - -## Problem - -Issue #1522 reports CI timeouts in the integration-test job (the 30-min limit was hit; the -`build` job timeout has since been raised to 45 min). Job-level timing on a recent master run: - -| Job | Duration | -|---|---| -| Unit tests | 4.6 min | -| Durable Task build & tests | 5.5 min | -| Build sb:4.0.x (Testcontainers, `spring-boot-4-sdk-tests`) | 11.1 min | -| **Build sb:3.5.x (`sdk-tests`, mixed) | 26.3 min** ← bottleneck | -| publish | 9.3 min | - -The `sb:3.5.x` job runs the `sdk-tests` module. After PR #1754 (merged 2026-07-09), **13 of -its 20 integration tests already run on Testcontainers** (`BaseContainerIT`/`DaprContainer`), -but **7 still use the legacy `dapr run` CLI harness** (`BaseIT`/`DaprRun`/`AppRun`). Because -those 7 need the CLI, the `build` job must still install the Dapr CLI, run `dapr init`, -`dapr uninstall`, spin host Kafka via docker-compose, and install a host ToxiProxy binary — -and each legacy test shells out to `dapr run` per app. The Testcontainers-based `sb:4.0.x` -job doing comparable work finishes in less than half the time. - -The only way to remove the CLI/`dapr init`/Kafka/ToxiProxy setup from CI — and collapse the -26-min bottleneck — is to migrate the **last 7 tests** off the legacy harness so nothing in -`sdk-tests` needs the Dapr CLI. - -## The 7 remaining legacy tests - -| # | IT | Legacy mechanism | Group | -|---|---|---|---| -| 1 | [ActorStateIT](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java) | Restart **app** mid-test; verify actor state persisted in Redis | A | -| 2 | [ActorTimerRecoveryIT](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java) | Restart **app** quickly (no sleep); verify in-memory timer survives | A | -| 3 | [ActorReminderRecoveryIT](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java) | Restart **daprd sidecar**; verify persisted reminder resumes | B | -| 4 | [ActorReminderFailoverIT](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderFailoverIT.java) | Two apps + two sidecars; kill one app; verify reminder fails over | C | -| 5 | [WaitForSidecarIT](../../../sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java) | ToxiProxy latency + a not-running sidecar; verify `waitForSidecar` timeout semantics | D | -| 6 | [ActorSdkResiliencyIT](../../../sdk-tests/src/test/java/io/dapr/it/actors/ActorSdkResiliencyIT.java) | ToxiProxy latency/jitter between actor client and sidecar (its one `@Test` is `@Disabled`) | D | -| 7 | [BindingIT](../../../sdk-tests/src/test/java/io/dapr/it/binding/http/BindingIT.java) | Kafka input/output bindings + two HTTP output-binding error tests | E | - -## Goals - -- Migrate all 7 ITs to extend `BaseContainerIT` and run Dapr in a `DaprContainer`. -- Add the small set of reusable helpers these tests need to `BaseContainerIT` (app restart, - sidecar restart, shared-placement/scheduler multi-sidecar). -- Add a `KafkaContainer` for `BindingIT` and a `ToxiproxyContainer` pattern for the Group-D tests. -- Remove the Dapr CLI, `dapr init`/`dapr uninstall`, host Kafka (docker-compose), and host - ToxiProxy steps from the `build` job in [build.yml](../../../.github/workflows/build.yml). -- Delete the now-dead legacy harness (`BaseIT`, `DaprRun`, `DaprRunConfig`, `ToxiProxyRun`) and - the file-based component/config YAMLs and `deploy/local-test.yml` that only the 7 tests used. -- Land as a single PR (single cutover). - -## Non-Goals - -- Migrating the two `durabletask-client` ITs. They run in the separate `build-durabletask` - job, which uses its own sidecar container (not the Dapr CLI) and is untouched. -- **Re-enabling** `ActorSdkResiliencyIT`'s disabled test. It is migrated off the legacy harness - but stays `@Disabled` — re-enabling a known-flaky test is out of scope and contrary to #1754's - flakiness cleanup. -- Making `BindingIT`'s two HTTP output-binding tests hermetic. They keep pointing at - `https://api.github.com/unknown_path` (behavior preserved; a prior switch to httpbin was - reverted). Removing that external dependency is tracked as future work. -- Replacing `AppRun`'s `mvn exec:java` subprocess model. The app side stays a subprocess; only - the sidecar/backing-services are containerized. `AppRun`, `DaprPorts`, `Command`, `Stoppable` - are retained because `BaseContainerIT` depends on them. -- Parallelizing Surefire/Failsafe or sharding the CI matrix (separate, deferred approaches). - -## Decisions - -| # | Decision | Rationale | -|---|---|---| -| D1 | Migrate all 7 in one PR, then strip CI legacy setup in the same PR | The CI win is only realized when the last legacy test is gone; a partial migration keeps the CLI/init/Kafka/ToxiProxy setup and barely moves CI time. | -| D2 | Put reusable mechanics in `BaseContainerIT` (`restartApp`, `restartSidecar`, shared-placement multi-sidecar helper) | Keeps the 7 tests thin and consistent; mirrors how `BaseContainerIT` already centralizes lifecycle. | -| D3 | Keep `BindingIT` HTTP tests on the external GitHub URL | Preserves behavior, minimal blast radius; hermeticity is orthogonal to removing `dapr run`. | -| D4 | `ActorSdkResiliencyIT` migrated but stays `@Disabled` | Removes the last `ToxiProxyRun` consumer without re-introducing a flaky test into CI. | -| D5 | Reminders survive daprd-container restart via the **scheduler container** | Dapr 1.18 stores actor reminders in the scheduler service; restarting only the daprd container (placement + scheduler + Redis stay up) preserves them. | -| D6 | Failover uses **explicit shared** placement + scheduler containers via `withPlacementContainer`/`withSchedulerContainer` | `withReusablePlacement(true)` relies on Testcontainers reuse, which is disabled on CI hosts; explicit shared containers make the two-sidecar topology deterministic. Pattern already proven in `WorkflowsMultiAppCallActivityIT`. | -| D7 | Add `org.testcontainers:testcontainers-kafka` (test scope, versionless) to `sdk-tests/pom.xml` for `BindingIT` | `testcontainers-kafka` is already version-managed in the **root** `pom.xml` `dependencyManagement` (via `testcontainers-bom`) but is not a declared dependency of `sdk-tests`. Add it to `sdk-tests/pom.xml` only, without a ``. Testcontainers 2.0.5 module artifactIds are `testcontainers-`-prefixed. | -| D8 | Single spec + single staged implementation plan | Matches the single-cutover decision; plan tasks are ordered and independently reviewable. | - -## Architecture - -All new mechanics live in [`BaseContainerIT`](../../../sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java), -which already provides `daprBuilder`, `startAppAndAttach`, `newDaprClient`/`newActorClient`, -`redisStateStore`, `waitForActorsReady`, and LIFO `deferStop`/`deferClose` cleanup. Additions: - -### 1. `restartApp(AppRun app)` — Group A - -Stops and restarts the app subprocess on its **same** pre-allocated port. daprd stays up; since -`daprBuilder` configures **no** app-health-check, daprd does not deactivate actors during the -gap, so in-memory timers survive (matches the legacy `@DaprRunConfig(enableAppHealthCheck=false)` -on `MyActorService`). For `ActorTimerRecoveryIT` the restart must be "quick" (no sleep between -stop and start) — the helper calls `app.stop()` immediately followed by `app.start()`. - -> **Invariant:** this depends on `daprBuilder` (BaseContainerIT.java:75-88) never adding -> `withAppHealthCheckPath(...)`. If a future change adds an app-health-check path to the shared -> builder, daprd would deactivate the actor during the restart gap and `ActorTimerRecoveryIT` -> would lose its timer. Keep any health-check config test-local, not in `daprBuilder`. - -```java -protected static void restartApp(AppRun app) throws Exception { - app.stop(); - app.start(); // rebinds the same host app port; daprd reconnects via host.testcontainers.internal:appPort -} -``` - -### 2. `restartSidecar(DaprContainer dapr)` — Group B - -Stops and restarts the daprd container, then re-verifies readiness. Placement + scheduler are -instance fields on `DaprContainer` that are **not** recreated on the second `start()` (the -`if (placementContainer == null)` / `if (schedulerContainer == null)` guards in -`DaprContainer.configure()`), so they persist across the restart — and so does the reminder, -which the scheduler owns. Pinned `setPortBindings` re-bind the same host ports, so the app's -`DAPR_HTTP_PORT`/`DAPR_GRPC_PORT` and any `DaprClient` remain valid. - -```java -protected static void restartSidecar(DaprContainer dapr) { - dapr.stop(); - dapr.start(); // reuses existing placement + scheduler; re-binds pinned ports - try (DaprClient c = newDaprClient(dapr)) { c.waitForSidecar(30_000).block(); } - waitForActorsReady(dapr); -} -``` - -`ActorReminderRecoveryIT` uses this in place of the legacy `DaprRun.stop()`/`start()`, keeping -its "pause 10s then 7s" waits so placement/health settle. - -### 3. Shared-placement multi-sidecar helper — Group C - -A helper (and/or a documented pattern) that builds two `DaprContainer` app-sidecars sharing one -`DaprPlacementContainer` and one `DaprSchedulerContainer` on `SharedTestInfra.network()`, each -attached to its own `AppRun` via the existing two-phase startup. Modeled directly on -[`WorkflowsMultiAppCallActivityIT`](../../../sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/multiapp/WorkflowsMultiAppCallActivityIT.java) -(lines 57-111): `new DaprPlacementContainer(...).withNetworkAliases("placement")`, ditto -scheduler, then each daprd `.withPlacementContainer(shared).withSchedulerContainer(shared)`. - -`ActorReminderFailoverIT` needs each sidecar's pinned HTTP port so it can match the actor host -returned by `MyActorBase.getIdentifier()` (which returns `DAPR_HTTP_PORT`) and kill the correct -`AppRun`. The helper therefore exposes both `(DaprContainer, AppRun)` pairs and their pinned ports. - -### 4. ToxiProxy wiring — Group D - -Copy the proven pattern from the already-migrated -[`SdkResiliencyIT`](../../../sdk-tests/src/test/java/io/dapr/it/resiliency/SdkResiliencyIT.java): -a `ToxiproxyContainer` (image `ContainerConstants.TOXI_PROXY_IMAGE_TAG` = -`ghcr.io/shopify/toxiproxy:2.5.0`) on the shared network, `DaprContainer` with -`.withNetworkAliases("dapr")`, a proxy `dapr:3500`/`dapr:50001`, and a `DaprClient`/`ActorClient` -pointed at `TOXIPROXY.getMappedPort(...)`. Toxics (latency/timeout) are added/removed per test -via the `ToxiproxyClient` API. `WaitForSidecarIT` needs no app; `ActorSdkResiliencyIT` combines -this with `startAppAndAttach` for its actor app. For `WaitForSidecarIT`'s "not running" case, -point the client at a stopped container's former port (or an unbound port) so the connection fails. - -### 5. `KafkaContainer` for BindingIT — Group E - -Add `org.testcontainers:testcontainers-kafka` (test scope, BOM-managed). Start a `KafkaContainer` -on `SharedTestInfra.network()` with alias `kafka`; build the `bindings.kafka` component in-code -with `brokers` = the internal listener (`kafka:9092`), `topics`/`publishTopic`/`consumerGroup` -using the `{appID}` placeholder as today, `authRequired=false`, `initialOffset=oldest`. The -`InputBindingService` app is started via `startAppAndAttach`. The two HTTP output-binding -components (`github-http-binding-404`, `github-http-binding-404-success`) are declared in-code as -`bindings.http` components (URL unchanged), replacing the file-based `http_binding.yaml`. - -### Coexistence during migration - -`AppRun`, `DaprPorts`, `Command`, `Stoppable` stay (consumed by `BaseContainerIT`). Once all 7 -tests are migrated, `BaseIT`, `DaprRun`, `DaprRunConfig`, and `ToxiProxyRun` have no remaining -references in `sdk-tests` and are deleted (verified by grep per file before deletion). - -## Per-IT migration matrix - -| # | IT | App service class / actor type | Components (in-code) | New mechanics | -|---|---|---|---|---| -| 1 | ActorStateIT | `StatefulActorService` / `StatefulActorTest` | `redisStateStore("statestore")` | `startAppAndAttach` + `restartApp` (verify Redis-persisted state after app restart) | -| 2 | ActorTimerRecoveryIT | `MyActorService` / `MyActorTest` | `redisStateStore` | `startAppAndAttach` + `restartApp` (quick, no-sleep) | -| 3 | ActorReminderRecoveryIT | `MyActorService` (+ separate client app) | `redisStateStore` | `startAppAndAttach` + `restartSidecar` | -| 4 | ActorReminderFailoverIT | 2× `MyActorService` + 1 client | `redisStateStore` (shared) | shared-placement multi-sidecar helper; kill one `AppRun` | -| 5 | WaitForSidecarIT | none | none (in-memory kvstore ok) | ToxiProxy pattern; one stopped/unavailable endpoint | -| 6 | ActorSdkResiliencyIT | `DemoActorService` / `DemoActorTest` | default | ToxiProxy pattern + `startAppAndAttach`; test stays `@Disabled` | -| 7 | BindingIT | `InputBindingService` | `bindings.kafka` (Kafka container), 2× `bindings.http` | `KafkaContainer` + `startAppAndAttach` | - -Each migrated IT drops `extends BaseIT`, all `startDaprApp`/`DaprRun`/`DaprPorts`/`DaprRunConfig` -imports, and file-based component lookups; it defines components in-code via the `Component` -model and uses the `BaseContainerIT` client/actor factories. - -> **ActorReminderRecoveryIT client:** the legacy test starts a second, long-lived client app -> (`ActorReminderRecoveryTestClient`) and builds the reminder proxy off that client run, not the -> service run. In the migrated test this is a plain `newActorClient(dapr)` against the same -> sidecar (no second app subprocess is required) — the "(+ separate client app)" in the matrix -> refers to this actor-client, which the plan should implement as an `ActorClient`, not a second -> `startAppAndAttach`. - -## CI changes ([.github/workflows/build.yml](../../../.github/workflows/build.yml), `build` job) - -| Step | Disposition | -|---|---| -| Set up Dapr CLI (`wget … install.sh`) | **Remove** | -| Set up Go + checkout `dapr/cli` + `dapr/dapr` overrides + build daprd/placement | **Remove** (all `DAPR_REF`/`DAPR_CLI_REF` conditional blocks) | -| `dapr uninstall --all` + "Ensure Dapr runtime uninstalled" wait loop | **Remove** | -| `dapr init --runtime-version …` | **Remove** | -| `docker compose -f ./sdk-tests/deploy/local-test.yml up -d kafka` | **Remove** (Kafka now via Testcontainers) | -| Install host ToxiProxy (`wget toxiproxy-server`) | **Remove** (ToxiProxy now via Testcontainers) | -| `docker version` check | Keep (Testcontainers needs Docker) | -| Checkout / setup-java / `./mvnw clean install -DskipTests` | Keep | -| Integration-test run (sb 3.x / sb 4.x) + failsafe/surefire report uploads | Keep (test discovery surface unchanged) | -| `DAPR_CLI_VER` / `DAPR_RUNTIME_VER` / `DAPR_INSTALL_URL` / `TOXIPROXY_URL` / `GOVER…` env | **Remove** (no longer referenced) | - -Also delete `sdk-tests/deploy/local-test.yml` (Kafka/Zookeeper), unless another consumer is found. -`build-durabletask`, `test` (unit), and `publish` jobs are untouched. - -## Dead-code & resource cleanup - -After migration, delete (verifying zero remaining references first): -- `sdk-tests/.../io/dapr/it/BaseIT.java`, `DaprRun.java`, `DaprRunConfig.java`, `ToxiProxyRun.java` -- The file-based YAMLs only these tests loaded — explicitly `sdk-tests/components/kafka_bindings.yaml` - and `sdk-tests/components/http_binding.yaml`, plus any other `components/`/`configurations/` - file left with no remaining `withComponent(Path)` or CLI consumer (grep-verified per file). -- `sdk-tests/deploy/local-test.yml` - -Retain: `AppRun`, `DaprPorts`, `Command`, `Stoppable`, `SharedTestInfra`, `BaseContainerIT`. - -## Risks & mitigations - -| Risk | Mitigation | -|---|---| -| **Timer/reminder tests are timing-sensitive and could flake in containers** (cf. the known `ActorTurnBasedConcurrencyIT` timer-count flakiness) | Preserve the legacy waits; prefer `Awaitility`/`callWithRetry` with generous timeouts over fixed sleeps for assertions; validate each locally across repeated runs before finalizing. | -| Reminder does **not** survive daprd restart (if scheduler assumption is wrong) | Verify on Dapr 1.18 that scheduler owns reminders and stays up across restart; if not, fall back to keeping the reminder in Redis-backed state and reloading. Prove `ActorReminderRecoveryIT` locally first. | -| Testcontainers **reuse disabled on CI** breaks shared placement for failover | Use explicit shared placement/scheduler containers (D6), not `withReusable*`. | -| Killing one app in failover doesn't migrate the actor within the window | Match the legacy 10s failover wait; assert via `getIdentifier()` host change with retry. | -| More containers per class (2 sidecars + placement + scheduler + Kafka/ToxiProxy) lengthen cold runs | Acceptable: removes host CLI/init/uninstall overhead and per-test `dapr run`; net expected win is large (target the ~11 min the sb:4.0.x job already hits). | -| Kafka container startup adds time to `BindingIT` | Single Kafka container per class; `KafkaContainer` boots in a few seconds; still far cheaper than host Kafka + CLI. | -| `host.testcontainers.internal` differences dev vs CI | Handled by `Testcontainers.exposeHostPorts` (already used by `startAppAndAttach`); CI is Linux. | - -## Testing strategy - -- Each migrated IT runs locally via `cd sdk-tests && ../mvnw verify -Dit.test=` and - passes; the timing-sensitive ones (Timer/Reminder recovery, Failover) are run repeatedly to - check for flakiness before finalizing. -- Full `sdk-tests` `verify` passes locally and on CI with the trimmed `build.yml`. -- The 13 already-migrated ITs continue to pass unchanged. -- No new unit tests for the `BaseContainerIT` helpers; they are exercised by the migrated ITs. -- CI acceptance: `build sb:3.5.x` completes well under the 45-min timeout and materially faster - than 26 min (target: comparable to the sb:4.0.x job). - -## Open questions - -None at spec-approval time. The implementation plan will pin: the exact `testcontainers-kafka` -coordinates/version from the BOM, the `KafkaContainer` image and internal-listener config, the -`ToxiproxyContainer` proxy wiring for the actor gRPC channel in `ActorSdkResiliencyIT`, and the -precise `restartSidecar` wait sequence. - -## Out of scope (future work) - -- Making `BindingIT` HTTP tests hermetic (local stub instead of GitHub). -- Re-enabling `ActorSdkResiliencyIT`. -- Migrating the two `durabletask-client` ITs. -- Surefire/Failsafe parallelization or CI matrix sharding. From de6a3e5ee676fa3715ea477236f2ea2bee49643d Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 22:23:29 -0700 Subject: [PATCH 16/18] Fix TracingIT.http: skip Zipkin readiness probe in app bean The http tracing test app's Spring @Bean OpenTelemetryConfig.initOpenTelemetry() called the legacy io.dapr.it.tracing.OpenTelemetry.createOpenTelemetry(SERVICE_NAME) overload, which runs waitForZipkin() -> NetworkUtils.waitForSocket(127.0.0.1, 9411). After the Testcontainers migration Zipkin runs on a random mapped host port, not a fixed 9411, so that probe threw ConnectException, bean creation failed, the app subprocess never bound its port, and startAppAndAttach reported connection refused. Switch to the two-arg createOpenTelemetry(SERVICE_NAME, url) overload that skips the probe. The app exports no spans the test asserts on -- the validated calllocal/tracing-http-it/sleep span is emitted by the Dapr sidecar, which exports to Zipkin over the container network -- so the app only needs to boot. Signed-off-by: Siri Varma Vegiraju --- .../io/dapr/it/tracing/http/OpenTelemetryConfig.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/tracing/http/OpenTelemetryConfig.java b/sdk-tests/src/test/java/io/dapr/it/tracing/http/OpenTelemetryConfig.java index c66e79588..5c0447ab2 100644 --- a/sdk-tests/src/test/java/io/dapr/it/tracing/http/OpenTelemetryConfig.java +++ b/sdk-tests/src/test/java/io/dapr/it/tracing/http/OpenTelemetryConfig.java @@ -27,8 +27,16 @@ public class OpenTelemetryConfig { public static final String SERVICE_NAME = "integration testing service over http"; @Bean - public OpenTelemetry initOpenTelemetry() throws InterruptedException { - return io.dapr.it.tracing.OpenTelemetry.createOpenTelemetry(SERVICE_NAME); + public OpenTelemetry initOpenTelemetry() { + // Use the explicit-endpoint overload so this bean does NOT block on a Zipkin readiness + // probe during app startup. This app runs as a host subprocess and only needs a propagator + // for context extraction; it exports no spans that the test asserts on (the validated + // "calllocal//sleep" span is emitted by the Dapr sidecar, which exports to Zipkin over + // the container network). The legacy createOpenTelemetry(SERVICE_NAME) overload probed + // 127.0.0.1:9411, but Zipkin now runs as a Testcontainer on a random mapped port, so the + // probe failed and crashed startup -- startAppAndAttach then saw "connection refused". + return io.dapr.it.tracing.OpenTelemetry.createOpenTelemetry( + SERVICE_NAME, "http://localhost:9411/api/v2/spans"); } @Bean From 327566ca408d922e68b42550a9fc7ee2b9fcdafb Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 22:58:47 -0700 Subject: [PATCH 17/18] Optimize docs Signed-off-by: Siri Varma Vegiraju --- .github/workflows/validate-docs.yml | 8 +++++++- pom.xml | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-docs.yml b/.github/workflows/validate-docs.yml index fbc5f78be..34bb0bd45 100644 --- a/.github/workflows/validate-docs.yml +++ b/.github/workflows/validate-docs.yml @@ -28,7 +28,13 @@ jobs: with: distribution: 'temurin' java-version: ${{ env.JDK_VER }} + cache: 'maven' - name: Install jars run: ./mvnw install -q -B -DskipTests + # Validate that aggregate Javadoc generates cleanly (failOnError is set in the + # pom's reporting config). Run javadoc:aggregate directly rather than site-deploy: + # the aggregate report re-executes once per reactor module under the site + # lifecycle (~10x a full-source pass), which is what pushed this job to ~32 min. + # Invoked directly, the goal runs once over the whole reactor. - name: Validate Java docs generation - run: ./mvnw site-deploy + run: ./mvnw -q -B -DskipTests javadoc:aggregate diff --git a/pom.xml b/pom.xml index f1b5f598c..c9388bd31 100644 --- a/pom.xml +++ b/pom.xml @@ -689,6 +689,10 @@ org.apache.maven.plugins maven-javadoc-plugin ${maven-javadoc-plugin.version} + + false aggregate From c7fdb7625aed0e0ca16d3b37101405deb0b4fcd4 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 9 Jul 2026 23:09:06 -0700 Subject: [PATCH 18/18] Fix validate-docs: keep site-deploy; javadoc:aggregate drops reporting config The prior "Optimize docs" commit switched the job to javadoc:aggregate, but that goal does not apply the pom's configuration -- additionalDependencies (spring-data-commons/keyvalue) and excludePackageNames -- so aggregate Javadoc fails to resolve spring-data symbols (TypeInformation, PropertyPath) and the build errors out. Revert the step to site-deploy, which runs Javadoc as a site report so that config applies. The actual speedup is unchanged: false> on the aggregate report (in the same "Optimize docs" commit) stops the report re-executing once per reactor module -- the source of the ~32 min runtime -- so it now runs a single aggregate pass. Signed-off-by: Siri Varma Vegiraju --- .github/workflows/validate-docs.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/validate-docs.yml b/.github/workflows/validate-docs.yml index 34bb0bd45..da60f32b0 100644 --- a/.github/workflows/validate-docs.yml +++ b/.github/workflows/validate-docs.yml @@ -31,10 +31,5 @@ jobs: cache: 'maven' - name: Install jars run: ./mvnw install -q -B -DskipTests - # Validate that aggregate Javadoc generates cleanly (failOnError is set in the - # pom's reporting config). Run javadoc:aggregate directly rather than site-deploy: - # the aggregate report re-executes once per reactor module under the site - # lifecycle (~10x a full-source pass), which is what pushed this job to ~32 min. - # Invoked directly, the goal runs once over the whole reactor. - name: Validate Java docs generation - run: ./mvnw -q -B -DskipTests javadoc:aggregate + run: ./mvnw site-deploy