diff --git a/async-config-poll/config-stub/main.go b/async-config-poll/config-stub/main.go index 20a4e652..4157bb72 100644 --- a/async-config-poll/config-stub/main.go +++ b/async-config-poll/config-stub/main.go @@ -5,6 +5,11 @@ // - GET /v1/buckets/app-config?watch=true&version=N -> long-poll: returns the // NEXT version (N+1), simulating a config change on each watch poll. // +// By default a watch poll returns immediately (the periodic-poller scenario). +// Set POLL_HOLD_SECONDS>0 to model a real long-poll with a server timeout: the +// watch=true request is HELD open that long before delivering the next version +// (the httpPoll scenario), so Keploy records its open-duration as pollDurationMs. +// // It is hit only during `keploy record`. At replay time Keploy serves the // recorded responses instead, so this stub does not need to be running. package main @@ -13,11 +18,26 @@ import ( "encoding/json" "log" "net/http" + "os" "strconv" "strings" + "time" ) +// pollHold is the long-poll server timeout: a watch=true request is held open +// this long before delivering the next version. Default 0 (respond +// immediately); override with POLL_HOLD_SECONDS. +func pollHold() time.Duration { + if s := os.Getenv("POLL_HOLD_SECONDS"); s != "" { + if n, err := strconv.Atoi(s); err == nil { + return time.Duration(n) * time.Second + } + } + return 0 +} + func main() { + hold := pollHold() http.HandleFunc("/v1/buckets/", func(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(r.URL.Path, "/v1/buckets/") q := r.URL.Query() @@ -25,6 +45,15 @@ func main() { version := 1 if q.Get("watch") == "true" { cur, _ := strconv.Atoi(q.Get("version")) + if hold > 0 { + // Long-poll: hold the connection open until the server timeout, + // then deliver the next version. Abort if the client disconnects. + select { + case <-time.After(hold): + case <-r.Context().Done(): + return + } + } version = cur + 1 // deliver the next version -> a "change" per poll } diff --git a/async-config-poll/keploy-httppoll.yml b/async-config-poll/keploy-httppoll.yml new file mode 100644 index 00000000..7e9a98e1 --- /dev/null +++ b/async-config-poll/keploy-httppoll.yml @@ -0,0 +1,26 @@ +# Keploy config for the async-config-poll sample — httpPoll scenario. +# +# Identical to keploy.yml except the async lane's type is `httpPoll` instead of +# `http`. `httpPoll` marks the config-watch egress as a long-poll: at record +# Keploy stamps the mock kind `HttpPoll` and captures its open-duration +# (pollDurationMs); at replay the async engine HOLDS the poll until its resolve +# testcase and then serves it (verdict `held`), instead of serving it as soon as +# the request arrives. Paired with POLL_HOLD_SECONDS on the config-stub and +# WATCH_ONCE on the app so the recording holds a single server-timeout long-poll. +# +# Lane "config-watch": +# - match.pathRegex : only the /v1/buckets/app-config endpoint +# - matchQuery.watch : "true" -> only the background watch polls (the +# one-time boot "get current version" call uses +# watch=false and stays an ordinary blocking mock) +# - volatileParams : ["version"] -> the version query param changes every +# poll, so it is treated as shape-noise, not a mismatch +async: + lanes: + - name: config-watch + type: httpPoll + match: + pathRegex: "^/v1/buckets/app-config$" + matchQuery: + watch: "true" + volatileParams: ["version"] diff --git a/async-config-poll/src/main/java/com/example/asyncconfig/config/ConfigWatchService.java b/async-config-poll/src/main/java/com/example/asyncconfig/config/ConfigWatchService.java index 68efed1c..dd6364b2 100644 --- a/async-config-poll/src/main/java/com/example/asyncconfig/config/ConfigWatchService.java +++ b/async-config-poll/src/main/java/com/example/asyncconfig/config/ConfigWatchService.java @@ -63,6 +63,11 @@ public void init() { } private void startWatchPoller() { + // WATCH_ONCE opens exactly ONE watch poll then stops the daemon. The + // httpPoll scenario uses it so the whole recording holds a single long + // poll (a server-timeout long-poll) rather than a stream of them; the + // default (unset) keeps the periodic-poller behavior. + final boolean watchOnce = "true".equalsIgnoreCase(System.getenv("WATCH_ONCE")); Thread t = new Thread(() -> { while (watching) { try { @@ -88,6 +93,9 @@ private void startWatchPoller() { // the exception so a stack trace is available under DEBUG. log.debug("config watch poll failed", e); } + if (watchOnce) { + break; // single long-poll connection for the httpPoll scenario + } } }, "config-watch-poller"); t.setDaemon(true);