Skip to content

feat: add async-config-poll sample (async-egress engine demo)#146

Merged
Aditya-eddy merged 2 commits into
keploy:mainfrom
Aditya-eddy:add-async-config-poll
Jul 16, 2026
Merged

feat: add async-config-poll sample (async-egress engine demo)#146
Aditya-eddy merged 2 commits into
keploy:mainfrom
Aditya-eddy:add-async-config-poll

Conversation

@Aditya-eddy

Copy link
Copy Markdown
Member

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:

Interaction When Keploy treats it as
GET /v1/buckets/app-common, app-features, app-config?watch=false once, at boot (blocking) ordinary sync mocks
GET /v1/buckets/app-config?watch=true&version=N forever, from a daemon thread async egress (lane config-watch)
MySQL SELECT ... per request ordinary sync mocks

The background watch poll runs on its own timer, so it does not line up one-to-one with the ingress testcases and the version param changes every poll. The async lane in keploy.yml tells Keploy to serve those polls independently of testcase ordering and to treat version as shape-noise, so replay stays green and the app's poller stays happy.

Endpoints

  • GET /health — small payload; runs SELECT 1 against MySQL.
  • GET /rules/{useCase} — ordered rules for (useCase, tenant) from MySQL. Requires X-Tenant-Id + X-Agent-Id. Example: GET /rules/ORDER_FLOW.

Contents

Self-contained: pom.xml, sources under com.example.asyncconfig, docker-compose.yml (MySQL 5.7), init.sql seed, a Go config-stub for the config service, and keploy.yml declaring the async lane. See the sample README.md.

Notes

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>
Copilot AI review requested due to automatic review settings July 16, 2026 06:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 /health and /rules/{useCase} endpoints backed by MySQL.
  • Adds a background config watcher (ConfigWatchService) that long-polls app-config and is intended to be replayed via Keploy async egress (lane config-watch).
  • Adds local-run assets: MySQL docker compose + seed SQL, a Go config-stub, and keploy.yml with 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"}]}]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in f5d608e — the Javadoc now shows a valid JSON example inside a <pre> block.

Comment on lines +58 to +60
List<UseCaseRules> result = ruleDao.rulesFor(useCase, tenantId);
String json = mapper.writeValueAsString(result);
return Response.ok(json).build();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Comment on lines +79 to +82
if (v > appConfigVersion) {
appConfigVersion = v;
log.info("config watch: app-config advanced to version {}", v);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Comment on lines +83 to +87
} 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());
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in f5d608e — now log.debug("config watch poll failed", e), so the stack trace is available when DEBUG is enabled.

Comment on lines +105 to +108
throw new IllegalStateException(
"ConfigWatchService: failed to fetch config bucket '" + name
+ "' from " + url + " — application cannot boot", e);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in f5d608e — the message now adds an actionable hint: "Ensure the config service is reachable and that app.config.baseUrl points at it."

Comment on lines +33 to +39
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"name": name,
"version": version,
"keys": map[string]string{
"feature.enabled": "true",
},
})

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Comment on lines +49 to +51

return Response.ok(mapper.writeValueAsString(body)).build();
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Aditya-eddy added a commit to keploy/keploy that referenced this pull request Jul 16, 2026
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>
@Aditya-eddy
Aditya-eddy merged commit d981643 into keploy:main Jul 16, 2026
2 checks passed
Aditya-eddy added a commit to keploy/keploy that referenced this pull request Jul 17, 2026
…s) (#4368)

* docs: design spec for async HTTP long-poll testcases

Slice 1 of the async-testcase effort: egress HTTP long-poll consumers.
Captures the recording-order-anchored interleave replay model, the
config-lane classifier, per-delivery artifact + side-effect assertion,
and the serialize-across-lanes concurrency decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* docs(async): general async-egress engine design + implementation plan

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): add AsyncLane model + config.Async section

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): add AsyncParser + AsyncAware capability interfaces

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): transport-agnostic engine (gated ordered serving + shape verdict)

Adds the real Engine implementation: per-lane ordered mock streams built
from async-tagged mocks (sorted by asyncSeq), gated serving via a
completed-testcase counter compared against each mock's anchorPos
(anchorPos=0 at startup is always armed), request-shape verdicts
delegated to the lane's AsyncParser, and keep-alive fallback when no
mock is armed. Removes the Task 2 temporary `type Engine struct{}`
placeholder from async.go now that the real type lives in engine.go.

Engine depends only on *models.Mock, models.AsyncLane, and the
AsyncParser interface (no transport imports), proven by a pluggability
test that drives the same engine with a second, independent fake
parser/lane with zero engine changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* fix(async): deterministic lane priority + idempotent Load + nil-parser warning

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): HTTP AsyncParser (lane glob, 204 keep-alive, shape via SchemaMatch)

Implements async.AsyncParser + async.AsyncAware on *HTTP:
- MatchesLane: host/path glob (path.Match) against lane.Match.
- EmptyResponse: minimal 204 No Content keep-alive.
- MatchRequestShape: reuses the existing SchemaMatch matcher against the
  single recorded mock. Volatile query params (lane.VolatileParams) are
  stripped from BOTH the live and recorded request before comparison —
  stripping only the recorded side (as a naive reading of the shape would
  suggest) leaves the query key-set counts unequal in SchemaMatch's
  MapsHaveSameKeys check and spuriously fails the match.

Adds asyncEngine *async.Engine field + SetAsyncEngine setter to HTTP struct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* test(async): cover recorded-side URLParams volatile strip in MatchRequestShape

Add TestMatchRequestShapeStripsRecordedURLParams and its negative companion
TestMatchRequestShapeRecordedURLParamsKeyDrift, which build a recorded mock
directly with a populated Spec.HTTPReq.URLParams (unlike httpMock(), which
always leaves URLParams nil). This exercises stripVolatile's recorded-side
`if req.URLParams != nil` branch for the first time, mirroring what
pkg.URLParams(req) populates at record time in production.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): record hook stamps async metadata on lane egress

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* fix(async): order-independent anchor tie-break + record-time nil-parser warning

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): plumb engine onto Proxy (construct + inject + per-test position advance)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): load complete async corpus at agent StoreMocks seam

Add AsyncMockLoader optional Proxy capability and wire the agent to hand
the async engine the COMPLETE async-mock corpus exactly once per store
path (Engine.Load is run-once). In StoreMocksStream, async mocks are
collected DURING the decode loop (before disk/resident routing) so
per-test async egress parked on disk under strict mock-window is still
loaded; in StoreMocks everything is resident so we collect from params.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): serve async deliveries + keep-alive in HTTP decode path

Extract the matched-mock response serializer into
(*HTTP).buildMockResponseBytes and reuse it in both the existing matched
path and a new async serving branch in decodeHTTP. When a live outbound
request routes to a configured async lane, the transport-agnostic engine
decides what to serve (recorded delivery once armed, or a keep-alive
payload) instead of normal mock matching. The branch mirrors the
telemetry short-circuit: write, read the next poll, continue, using the
errCh <- err / return + ctx.Err() early-return pattern. Adds
liveReqToMock to bridge the matcher's req into a *models.Mock for the
engine. Extraction is behavior-identical; full http suite still passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* docs(async): correct Task 6 plan (load at agent StoreMocks seam, split 6a/6b/6c)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): install AsyncRecorder record hook when lanes configured

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* fix(async): stamp in Before* record hooks so metadata persists

AfterMockInsert fires after mockDB.InsertMock persists the mock, so the async
metadata stamp never reached the recorded YAML. Move stamping to
BeforeMockInsert (mirrors the enterprise obfuscator) and track testcase windows
in AfterTestCaseInsert (TestCase.Name is assigned by InsertTestCase). Caught by
the end-to-end record run against a real long-poll sample app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* docs(async): record Before-hook fix + e2e findings (timing, verdict visibility)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): surface async verdict at replay wind-down

Add Engine.LogReport (fires once via sync.Once) and call it from
SetGracefulShutdown and StopProxyServer so the async verdict
(served / shape_flags / not_exercised) prints at end of replay, with one WARN
per shape drift. Makes the shape-flag verdict user-visible; verified e2e
(mutating a served mock's request method surfaces shape_flags:1 + WARN while
still serving, sync tests still pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* refactor(async): apply cleanup-review findings

- engine: run shape-match outside the mutex (Decide no longer holds the lock
  across the parser call); drop the parallel lanes map in favor of the ordered
  slice; cache seq/anchorPos as ints at Load instead of re-parsing per poll;
  own the first-window rule in Engine.AdvanceWindow (removes Proxy.asyncWindowSeen).
- http: build the volatile-param set once per shape check; move the shared
  buildMockResponseBytes serializer to decode.go (out of the async-only file);
  reuse flakyHeaderNoise() in decode.go's auto-noise setup; flatten the async
  serving branch's triplicated error handling into one closure.
- agent: skip the async-tag scan entirely when the proxy is not an AsyncMockLoader.

Behavior-preserving; unit suites + e2e verdict unchanged (served:1, not_exercised:11).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): query + regex lane matching (matchQuery, pathRegex)

Async lanes can now target egress more precisely on the same path:
- matchQuery: require query key=value (e.g. watch=true) so a long-poll variant
  of an endpoint is async while a same-path boot call (watch=false) is not.
- match.pathRegex: a regex alternative to the glob match.path, for URLs a glob
  can't express; reuses pkg/matcher.MatchesAnyRegex (cached, fail-closed).
queryOf follows the package's URLParams-first precedence; MatchesLane uses a
single matched flag instead of restating the configured-criteria list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* refactor(async): address PR review — log levels + capability registration

Apply Copilot review feedback on the async-egress engine:

- proxy.InitIntegrations: register an AsyncParser with the engine even when
  the integration does not implement AsyncAware. Registration was nested
  inside the AsyncAware check, so an AsyncParser-only integration would
  silently never register for routing (LaneFor/Decide).
- engine.Decide: no-parser branch WARN -> DEBUG with a config hint. The
  branch is defensive/near-unreachable (LaneFor only matches when a parser
  exists), so a WARN there is misleading noise.
- engine.LogReport: per-flag shape-drift WARN -> INFO with a remediation
  hint (re-record, or widen the lane's volatileParams / match). Matches the
  Info-level verdict header for one coherent end-of-run summary.
- record.BeforeMockInsert: per-mock unresolved-parser WARN -> DEBUG. The
  root cause is already reported once, loudly, at startup; keeping the
  per-mock repeat at DEBUG avoids flooding real recordings.
- record.ResolveAsyncParsers: unregistered-type and not-an-AsyncParser
  WARN -> ERROR with actionable hints. These are genuine config errors that
  silently defeat intent (mocks never stamped async).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): auto-generate lane names from routing identity

AsyncLane.Name is now optional. When omitted, EffectiveName derives a
deterministic name from the lane's ROUTING identity (type + match +
matchQuery) — stable across record and replay, so it works as the join key
stamped on mocks (MetaAsyncLane) and re-derived by the replay engine.
Caller-supplied names are still honored verbatim.

- models.AsyncLane: Name is now omitempty; add EffectiveName() and the bulk
  WithEffectiveNames(). The derived name excludes volatileParams/notExercised
  (replay-time tuning a user may change between runs, which must not shift the
  join key). A path-derived slug keeps it readable (e.g. http-poll-87288cbd).
- NewAsyncRecorder and NewEngine normalize their lanes through
  WithEffectiveNames, so the record-time stamp and the replay-time lookup use
  the same key. WithEffectiveNames also returns a fresh copy, subsuming the
  manual defensive copy NewEngine used to make.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(async): add async-config-poll e2e job to java_linux

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>

* docs(async): drop internal design/plan docs from the repo

Remove the superpowers-generated design specs and implementation plan; these
are internal working artifacts and do not belong in keploy/keploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* fix(async): keep async-egress mocks out of the per-test mock mapping

The test↔mock mapping is binned by the agent's SyncMockManager purely on
timestamp-window overlap; it has no async awareness (lanes are known only in
the record service, where the AsyncRecorder hook stamps MetaAsync). So a
background/long-poll egress whose timestamp happened to fall inside a
testcase's request window was being attributed to that testcase's mock set —
even though async mocks are served at replay by the async engine from the
complete corpus and must never be a per-test mock.

Track the tempIDs the hook stamps MetaAsync (asyncMockIDs) and exclude them
when building each test's mapping. The mock-insert goroutine records the async
marker before publishing the tempID to correlationMap, and the mapping
goroutine reads it only after finding the tempID there, so a resolved tempID
always has its async status decided (single writer, single reader, unique
tempIDs — no race).

Extracted the mapping resolution into Recorder.resolveMappingEntries and unit-
tested that async tempIDs overlapping a window are excluded while ordinary
(sync) egress is still mapped.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(async): drop unenforced notExercised knob, add lane diagnostics

The notExercised (skip|fail) lane field was documented and settable but its
value was never read by any code path, so `fail` silently behaved as `skip` —
a misleading knob on a new public config surface. Async polling is
keploy-managed and timing-dependent, so the not-exercised policy is keploy's
to decide, not a user knob: remove the field. The engine still tallies and
logs a not-exercised count (report-only, non-blocking).

Also address two diagnostics raised in review:
- NewEngine logs each configured lane's resolved match criteria at startup,
  so an over-broad glob (which would hijack ordinary sync egress) is visible.
- SetMocks (the non-windowed fallback path, which never advances the async
  window) warns once that only startup-anchored deliveries will arm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): AsyncLane.IsPoll/BaseType + poll metadata keys

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(async): HttpPoll kind + pollKind registry

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(async): HttpPoll encode/decode reuses the HTTP wire format

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(async): register Http->HttpPoll poll kind in the http integration

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(async): stamp poll metadata + re-kind on poll lanes; resolve parser by base type

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(async): hold poll deliveries until their resolve testcase (sync.Cond)

Adds asyncEntry.poll (from MetaAsyncPoll), a sync.Cond on the engine
mutex broadcast from AdvanceWindow/OnTestComplete, and a ctx-aware
Decide(ctx, lane, live) that parks a not-yet-armed POLL entry on the
cond until completed reaches its anchorPos or ctx is done (cond.Wait
releases the lock while parked, so AdvanceWindow is never gated by an
outstanding long-poll). Non-poll deliveries keep the pre-existing
immediate keep-alive behavior. Adds a held counter surfaced via
ReportSnapshot.Held and LogReport. Extracts the shape-match/verdict
logic into a decideServe helper and the "no data yet" path into a
nil-safe keepAlive helper, both reused by Decide.

Resolves parsers by lane.BaseType() in both LaneFor and Decide (not
raw lane.Type), so a "httpPoll" lane routes to the parser registered
under "http" — without this a poll lane's live request never matches
in LaneFor and the hold could never engage.

Also fixes the one other pre-existing caller of the old
Decide(lane, live) signature (pkg/agent/proxy/proxy_async_test.go,
predating this change) to pass context.Background(), so
`go test ./...` stays green repo-wide.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(async): re-peek cursor entry on every Decide wake, not just once

A parked poll's cond.Wait woke, then Decide re-read s.mocks[s.cursor]
and served/advanced it without re-checking that entry's own anchorPos.
Under concurrent same-lane Decide calls, another consumer could advance
the cursor while this one was parked, so the waking call would serve a
later, not-yet-armed delivery. Restructure the hold as a re-peek loop
under the lock: every iteration (initial pass and every wake) re-reads
the current cursor entry and re-validates completed >= entry.anchorPos
before serving, closing the gap the pre-Task-6 code never had.

Add TestDecideConcurrentSameLaneServesInOrderByAnchor: two poll
deliveries (anchorPos 1 and 2) on one lane, two concurrent Decide
calls, asserting each resolves only at its own anchor and never early.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: address final whole-branch review findings for httpPoll feature

- gofmt pkg/models/async.go (MetaPollDurationMs const-block alignment, CI lint blocker)
- engine.go Decide: read e.parsers[lane.BaseType()] under e.mu.Lock() instead of before it, closing a latent data race with RegisterParser (no behavior change)
- diskmocks.go EligibleForResponseSpill: include models.HttpPoll alongside models.HTTP so poll response bodies are disk-spill eligible, mirroring mockdb/util.go's four switches
- asynchook.go: fix stale ResolveAsyncParsers doc comment (keyed by BaseType(), not Type); note BaseType()'s case-sensitive keying vs IsPoll()'s case-insensitive suffix check
- asynchook_test.go: add TestPollLaneClampsNegativePollDurationToZero and TestAsyncPollMockExcludedFromPerTestMapping, locking the pollDurationMs clamp and the per-test mapping exclusion invariants (both verified via temporary code removal to ensure they actually fail on regression)

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(async): exempt poll-lane connections from the record hang watchdog

A long-poll egress connection makes no byte progress for far longer than
the supervisor's 60s hang budget while the server holds it open. The
watchdog could not tell that apart from a hung parser, so it aborted the
parser and fell through to passthrough before the delivery arrived — the
app still got its response but the poll's mock was never recorded (30s
holds captured, 300s holds silently dropped).

recordV2 now matches each request against the configured async lanes by
request shape (LaneFor) as soon as the request is parsed, and for a
poll-type lane calls the new Session.SuspendWatchdog / Supervisor.
SuspendWatchdog to permanently disarm hang detection on that connection.
Panic and mem-cap protection stay active; only no-progress hang detection
is disabled, and only for connections whose in-flight request routes to a
poll lane.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(async): address review of poll-lane watchdog exemption

- Latch the suspend once per connection so a keep-alive poll-poll-poll
  connection does not re-parse/re-match every request after the first.
- Document that SuspendWatchdog disarms hang detection for the whole
  connection, not just the in-flight request (bounded keep-alive trade-off).
- Add tests: a suspended supervisor still aborts on a mem-cap breach
  (suspend must not weaken mem-cap protection); malformed request bytes
  return false without panicking.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(async): /simplify pass on httpPoll — reuse + leaner Decide

- isPollLaneRequest reuses liveReqToMock (the same request->mock shaping the
  replay LaneFor path uses) instead of hand-building a mock; drops the manual
  URLParams loop that truncated multi-valued queries, and gates on a new
  Engine.HasPollLanes() so deployments with no poll lanes skip the re-parse.
- Decide registers the ctx-done wake bridge lazily via context.AfterFunc only
  before it actually parks on cond.Wait, instead of spawning a goroutine +
  channel on every call (immediate-return paths no longer pay for it).
- Drop decideServe's unused context parameter.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(async): cover the httpPoll scenario in async_config_poll e2e

Adds a scenario: [periodic, httppoll] axis to the async_config_poll job so
the async-egress e2e validates the new httpPoll long-poll lane type, not
just the periodic poller:

- periodic (unchanged): fast poller, stub answers immediately, lane type
  http -> asserts the engine SERVED the polls (served >= 1, no drift).
- httppoll: one WATCH_ONCE poll held open by the stub (POLL_HOLD_SECONDS),
  lane type httpPoll (keploy-httppoll.yml) -> asserts the poll is recorded
  as kind:HttpPoll with a pollDurationMs, and that at replay the engine
  HELD it until its resolve testcase (held >= 1). The >60s hang-watchdog
  exemption is unit-tested (supervisor); this e2e uses a short hold for speed.

TEMP: the samples-java checkout is pinned to the fork branch
(Aditya-eddy/samples-java async-config-poll-httppoll) because the sample
changes are in keploy/samples-java#147, not yet on main. REVERT the checkout
to keploy/samples-java (main) once #147 merges.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(async): unpin samples-java checkout now that #147 is merged

keploy/samples-java#147 (the async-config-poll httpPoll sample hooks) is
merged to main, so the async_config_poll job no longer needs the temporary
fork-branch pin. Restore the checkout to keploy/samples-java (main).

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(async): move async bookkeeping to a typed top-level block

Async-egress data (lane, order, anchor, poll/duration) was stamped into the
owning parser's flat Spec.Metadata, and poll deliveries were re-kinded to a
cosmetic HttpPoll kind. Both mixed the async engine's concern into the parser's
model and taxed every Kind switch.

Introduce models.AsyncMeta and carry it in its own block — Spec.Async in
memory, a kind-agnostic top-level `async:` block on the recorded doc. Its
presence marks a mock as async (replacing the MetaAsync flag); Poll marks a
held long-poll (replacing the HttpPoll kind + pollKind registry). Fields are
typed (seq/anchorPos/pollDurationMs ints, poll bool). A poll mock keeps its
base kind (Http). Following keploy's PostgresV3 precedent (sub-type in Spec,
not Kind).

Removed: HttpPoll kind, MetaAsync* string consts, RegisterPollKind/PollKindFor.
AsyncLane.IsPoll()/BaseType() stay (they describe the config lane type).
Round-trip test (TestAsyncBlockRoundTrips) proves the block survives
encode->decode; the async keys no longer leak into Spec.Metadata.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(async): /simplify pass on the async-block refactor

- Carry the Async envelope field across the two codec.go format converters
  (DocToJSON, jsonDocToYamlDoc) and clone it in Mock.DeepCopy — the async
  field was added to the doc structs but these paths were missed (mirrors how
  Noise is handled).
- Add Mock.IsAsync() and route the 5 duplicated `Spec.Async != nil` checks
  (engine Load, agent collect x2, record mapping) through it (matches the
  HasSpilledResponse predicate precedent).
- ci: the httppoll async_config_poll assertion checked `kind: HttpPoll`, which
  no longer exists — poll mocks stay kind Http, so assert the async block's
  `poll: true` + pollDurationMs instead.
- Rename the local `async` var (shadowed the async package import), fix
  comments referencing removed MetaAsync*/HttpPoll symbols, and add a
  DeepCopy async-isolation test.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* review(async): address PR Pilot (opus/high) findings

- engine.go LaneFor: read e.parsers under e.mu, mirroring Decide's deliberate
  lock (commit 9dff3ad) so every e.parsers access is guarded — LaneFor is on
  both the replay decode and record isPollLaneRequest paths.
- Lock the JSON storage-format round-trip: TestAsyncBlockRoundTrips now also
  serializes via MarshalDoc(FormatJSON)/UnmarshalDoc and asserts Spec.Async
  survives DocToJSON + jsonDocToYamlDoc (two converters that were originally
  missed), not just the yaml.Node path.
- WarnNonWindowed: note that a testcase-anchored POLL delivery blocks its
  connection until replay teardown on the non-windowed path.
- Fix stale collectAsyncMocks doc comment (Spec.Metadata → Mock.IsAsync).

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants