Skip to content

Commit 4b3b4fc

Browse files
Aditya-eddyclaude
andauthored
async-config-poll: add httpPoll (long-poll) scenario (#147)
Extends the sample so it can exercise Keploy's httpPoll async-egress lane type (keploy#4368), not just the periodic poller: - config-stub: POLL_HOLD_SECONDS (default 0) holds a watch=true request open that long before answering — a server-timeout long-poll. Default 0 keeps the current immediate behavior for the periodic scenario. - ConfigWatchService: WATCH_ONCE (default off) opens exactly one watch poll then stops the daemon, so the recording holds a single long poll rather than a stream. Default off keeps the current infinite poller. - keploy-httppoll.yml: same lane as keploy.yml but type: httpPoll, so the poll is recorded as kind:HttpPoll with an open-duration (pollDurationMs) and held until its resolve testcase at replay. All additions are env-gated and backward compatible; the default run is unchanged. Signed-off-by: Aditya Sharma <aditya282003@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d981643 commit 4b3b4fc

3 files changed

Lines changed: 63 additions & 0 deletions

File tree

async-config-poll/config-stub/main.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
// - GET /v1/buckets/app-config?watch=true&version=N -> long-poll: returns the
66
// NEXT version (N+1), simulating a config change on each watch poll.
77
//
8+
// By default a watch poll returns immediately (the periodic-poller scenario).
9+
// Set POLL_HOLD_SECONDS>0 to model a real long-poll with a server timeout: the
10+
// watch=true request is HELD open that long before delivering the next version
11+
// (the httpPoll scenario), so Keploy records its open-duration as pollDurationMs.
12+
//
813
// It is hit only during `keploy record`. At replay time Keploy serves the
914
// recorded responses instead, so this stub does not need to be running.
1015
package main
@@ -13,18 +18,42 @@ import (
1318
"encoding/json"
1419
"log"
1520
"net/http"
21+
"os"
1622
"strconv"
1723
"strings"
24+
"time"
1825
)
1926

27+
// pollHold is the long-poll server timeout: a watch=true request is held open
28+
// this long before delivering the next version. Default 0 (respond
29+
// immediately); override with POLL_HOLD_SECONDS.
30+
func pollHold() time.Duration {
31+
if s := os.Getenv("POLL_HOLD_SECONDS"); s != "" {
32+
if n, err := strconv.Atoi(s); err == nil {
33+
return time.Duration(n) * time.Second
34+
}
35+
}
36+
return 0
37+
}
38+
2039
func main() {
40+
hold := pollHold()
2141
http.HandleFunc("/v1/buckets/", func(w http.ResponseWriter, r *http.Request) {
2242
name := strings.TrimPrefix(r.URL.Path, "/v1/buckets/")
2343
q := r.URL.Query()
2444

2545
version := 1
2646
if q.Get("watch") == "true" {
2747
cur, _ := strconv.Atoi(q.Get("version"))
48+
if hold > 0 {
49+
// Long-poll: hold the connection open until the server timeout,
50+
// then deliver the next version. Abort if the client disconnects.
51+
select {
52+
case <-time.After(hold):
53+
case <-r.Context().Done():
54+
return
55+
}
56+
}
2857
version = cur + 1 // deliver the next version -> a "change" per poll
2958
}
3059

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Keploy config for the async-config-poll sample — httpPoll scenario.
2+
#
3+
# Identical to keploy.yml except the async lane's type is `httpPoll` instead of
4+
# `http`. `httpPoll` marks the config-watch egress as a long-poll: at record
5+
# Keploy stamps the mock kind `HttpPoll` and captures its open-duration
6+
# (pollDurationMs); at replay the async engine HOLDS the poll until its resolve
7+
# testcase and then serves it (verdict `held`), instead of serving it as soon as
8+
# the request arrives. Paired with POLL_HOLD_SECONDS on the config-stub and
9+
# WATCH_ONCE on the app so the recording holds a single server-timeout long-poll.
10+
#
11+
# Lane "config-watch":
12+
# - match.pathRegex : only the /v1/buckets/app-config endpoint
13+
# - matchQuery.watch : "true" -> only the background watch polls (the
14+
# one-time boot "get current version" call uses
15+
# watch=false and stays an ordinary blocking mock)
16+
# - volatileParams : ["version"] -> the version query param changes every
17+
# poll, so it is treated as shape-noise, not a mismatch
18+
async:
19+
lanes:
20+
- name: config-watch
21+
type: httpPoll
22+
match:
23+
pathRegex: "^/v1/buckets/app-config$"
24+
matchQuery:
25+
watch: "true"
26+
volatileParams: ["version"]

async-config-poll/src/main/java/com/example/asyncconfig/config/ConfigWatchService.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ public void init() {
6363
}
6464

6565
private void startWatchPoller() {
66+
// WATCH_ONCE opens exactly ONE watch poll then stops the daemon. The
67+
// httpPoll scenario uses it so the whole recording holds a single long
68+
// poll (a server-timeout long-poll) rather than a stream of them; the
69+
// default (unset) keeps the periodic-poller behavior.
70+
final boolean watchOnce = "true".equalsIgnoreCase(System.getenv("WATCH_ONCE"));
6671
Thread t = new Thread(() -> {
6772
while (watching) {
6873
try {
@@ -88,6 +93,9 @@ private void startWatchPoller() {
8893
// the exception so a stack trace is available under DEBUG.
8994
log.debug("config watch poll failed", e);
9095
}
96+
if (watchOnce) {
97+
break; // single long-poll connection for the httpPoll scenario
98+
}
9199
}
92100
}, "config-watch-poller");
93101
t.setDaemon(true);

0 commit comments

Comments
 (0)