feat: add async-config-poll sample (async-egress engine demo)#146
Conversation
A Spring Boot 1.5 / Java 8 rule engine (MySQL-backed) that also long-polls a
central config service in the background (GET /v1/buckets/app-config?watch=true).
That watch poll fires from a daemon thread on the app's own schedule — async
relative to the ingress testcases — so Keploy records and replays it through the
async-egress engine (lane "config-watch" in keploy.yml, matched on watch=true,
version treated as volatile).
Endpoints: GET /health (SELECT 1) and GET /rules/{useCase} (rules read from
MySQL). Ships docker-compose (MySQL 5.7), init.sql seed, a Go config-service
stub, and keploy.yml declaring the async lane.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
There was a problem hiding this comment.
Pull request overview
Adds a new self-contained sample app (async-config-poll/) demonstrating Keploy’s async-egress engine via a Spring Boot (1.5) / Java 8 rule-engine service that boot-fetches config and then long-polls config changes in a background daemon thread.
Changes:
- Introduces a Spring Boot + Jersey service with
/healthand/rules/{useCase}endpoints backed by MySQL. - Adds a background config watcher (
ConfigWatchService) that long-pollsapp-configand is intended to be replayed via Keploy async egress (laneconfig-watch). - Adds local-run assets: MySQL docker compose + seed SQL, a Go
config-stub, andkeploy.ymlwith async lane configuration.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| async-config-poll/src/main/resources/application.yml | App/server settings, datasource config, and tunables for the poll interval and artificial rule delay. |
| async-config-poll/src/main/java/com/example/asyncconfig/rules/UseCaseRules.java | DTO for grouped rules response payload. |
| async-config-poll/src/main/java/com/example/asyncconfig/rules/RulesResource.java | Jersey resource implementing GET /rules/{useCase}. |
| async-config-poll/src/main/java/com/example/asyncconfig/rules/RuleDao.java | JDBC data access for rules and their actions. |
| async-config-poll/src/main/java/com/example/asyncconfig/rules/RuleAction.java | DTO for rule actions in the response. |
| async-config-poll/src/main/java/com/example/asyncconfig/rules/Rule.java | DTO for individual rules in the response. |
| async-config-poll/src/main/java/com/example/asyncconfig/rules/HealthResource.java | Jersey resource implementing GET /health with a deterministic payload and a MySQL SELECT 1. |
| async-config-poll/src/main/java/com/example/asyncconfig/rest/JerseyConfig.java | Registers Jersey resources (/health, /rules). |
| async-config-poll/src/main/java/com/example/asyncconfig/config/ConfigWatchService.java | Boot-time config fetch + background long-poll watcher intended for async-egress replay. |
| async-config-poll/src/main/java/com/example/asyncconfig/Application.java | Spring Boot application entrypoint. |
| async-config-poll/README.md | Sample documentation and local record/replay instructions. |
| async-config-poll/pom.xml | Maven project definition for Spring Boot 1.5 + Jersey + JDBC + MySQL connector. |
| async-config-poll/keploy.yml | Declares Keploy async lane (config-watch) with volatile query param handling. |
| async-config-poll/init.sql | MySQL schema and seed data for the rule engine demo. |
| async-config-poll/docker-compose.yml | Local MySQL 5.7 service with seeded init script. |
| async-config-poll/config-stub/main.go | Go stub for the config service endpoints used at record time. |
| async-config-poll/config-stub/go.mod | Go module definition for the stub. |
| async-config-poll/.gitignore | Ignores build outputs, Keploy artifacts, and stub binary/logs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * Rule-engine endpoint: GET /rules/{useCase}. Requires the X-Tenant-Id and | ||
| * X-Agent-Id headers and returns the ordered rules for the (useCase, tenant), | ||
| * read from MySQL: | ||
| * [{"use_case","tenant","rules":[{"rule_id","constraints","actions":[...],"rule_type"}]}] |
There was a problem hiding this comment.
Fixed in f5d608e — the Javadoc now shows a valid JSON example inside a <pre> block.
| List<UseCaseRules> result = ruleDao.rulesFor(useCase, tenantId); | ||
| String json = mapper.writeValueAsString(result); | ||
| return Response.ok(json).build(); |
There was a problem hiding this comment.
The method is annotated @Produces("application/json;charset=utf-8"), so JAX-RS sets that as the response Content-Type even though the entity is a String — Response.ok(String) inherits the resource method's single @produces value. Verified against a recorded response: Content-Type: application/json;charset=utf-8. No change needed.
| if (v > appConfigVersion) { | ||
| appConfigVersion = v; | ||
| log.info("config watch: app-config advanced to version {}", v); | ||
| } |
There was a problem hiding this comment.
Fixed in f5d608e — demoted to log.debug. The poller runs forever and the stub advances the version on every poll, so info would flood normal runs.
| } catch (Exception e) { | ||
| // At replay the async engine keep-alives when nothing is | ||
| // armed; a failed poll is non-fatal to the running app. | ||
| log.debug("config watch poll failed: {}", e.getMessage()); | ||
| } |
There was a problem hiding this comment.
Fixed in f5d608e — now log.debug("config watch poll failed", e), so the stack trace is available when DEBUG is enabled.
| throw new IllegalStateException( | ||
| "ConfigWatchService: failed to fetch config bucket '" + name | ||
| + "' from " + url + " — application cannot boot", e); | ||
| } |
There was a problem hiding this comment.
Fixed in f5d608e — the message now adds an actionable hint: "Ensure the config service is reachable and that app.config.baseUrl points at it."
| _ = json.NewEncoder(w).Encode(map[string]interface{}{ | ||
| "name": name, | ||
| "version": version, | ||
| "keys": map[string]string{ | ||
| "feature.enabled": "true", | ||
| }, | ||
| }) |
There was a problem hiding this comment.
Fixed in f5d608e — ConfigWatchService now reads feature.enabled from the bucket's nested keys map (via a boolFromKeys helper), matching the config-stub's response shape. Previously the top-level read always returned null and silently fell back to the default.
|
|
||
| return Response.ok(mapper.writeValueAsString(body)).build(); | ||
| } |
There was a problem hiding this comment.
Same as the /rules thread: the method's @Produces("application/json;charset=UTF-8") sets the Content-Type for the String entity. Verified against the recorded /health response: Content-Type: application/json;charset=utf-8. No change needed.
Add a Java-on-Linux CI job that exercises the async-egress engine end to end against the async-config-poll sample (keploy/samples-java): record the /health and /rules HTTP tests plus the MySQL + config-service egress, then replay with the real deps down and assert both the ingress tests PASSED and the engine's `async egress verdict` (served >= 1, shape_flags == 0 — the volatile "version" watch param matches cleanly). - New reusable-workflow job `async_config_poll` in java_linux.yml, modelled on the existing tidb/mysql jobs (same guard, download-binary, checkout samples-java, upload artifacts). Deviations, each commented: Temurin 8 (the sample is Spring Boot 1.5), an added setup-go step for the config stub, and a single build/build matrix cell — the released binary can neither stamp nor serve async mocks, so the *_latest cells could never validate the feature (mirrors tidb-stmt-cache's omission comment). - New run script test_workflow_scripts/java/async_config_poll/java-linux.sh: brings up MySQL (docker compose) with a TCP readiness probe, builds+runs the Go config stub, records, then replays and checks the report + async verdict. Deliberately does not source update-java.sh (which pins Java 17). The sample app lives in keploy/samples-java#146; this job goes green once that merges. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
- ConfigWatchService: read feature.enabled from the bucket's nested "keys" map (matching the config-stub's response shape) instead of the top level, where it never resolved. - ConfigWatchService: demote the per-poll "version advanced" log to DEBUG (the poller runs forever and the version can advance every poll), and log the poll exception object so a stack trace is available under DEBUG. - ConfigWatchService: make the boot-failure message actionable (check the config service is reachable / app.config.baseUrl is correct). - RulesResource: make the response-shape Javadoc example valid JSON. Content-Type feedback on /health and /rules is already handled by each method's @produces("application/json...") (recorded responses carry application/json), so no change there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
What
Adds
async-config-poll/: a Spring Boot 1.5 / Java 8 rule-engine sample that demonstrates Keploy's async-egress engine.The app has two MySQL-backed HTTP endpoints and, in the background, long-polls a central config service for version changes:
GET /v1/buckets/app-common,app-features,app-config?watch=falseGET /v1/buckets/app-config?watch=true&version=Nconfig-watch)SELECT ...The background watch poll runs on its own timer, so it does not line up one-to-one with the ingress testcases and the
versionparam changes every poll. The async lane inkeploy.ymltells Keploy to serve those polls independently of testcase ordering and to treatversionas shape-noise, so replay stays green and the app's poller stays happy.Endpoints
GET /health— small payload; runsSELECT 1against MySQL.GET /rules/{useCase}— ordered rules for(useCase, tenant)from MySQL. RequiresX-Tenant-Id+X-Agent-Id. Example:GET /rules/ORDER_FLOW.Contents
Self-contained:
pom.xml, sources undercom.example.asyncconfig,docker-compose.yml(MySQL 5.7),init.sqlseed, a Goconfig-stubfor the config service, andkeploy.ymldeclaring the async lane. See the sampleREADME.md.Notes
async egress verdict {served: 2, shape_flags: 0, not_exercised: 6}.