+
+ diff --git a/clientlibs/java/CLAUDE.md b/clientlibs/java/CLAUDE.md index feea73a24..883fbc7f4 100644 --- a/clientlibs/java/CLAUDE.md +++ b/clientlibs/java/CLAUDE.md @@ -73,7 +73,18 @@ TestNG is the test framework but there is no `src/test/java` directory — `Quic To run a manual integration test: ```bash mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTest" # localhost:3030 +mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTest" -Dexec.args="local:3031" # another local port mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTest" -Dexec.args="qa" # or staging +mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTest" -Dexec.args="https://..." # any base URL +``` + +`QuickTestBurst` takes the same values via `--env` (`--env local:3031`), or a base URL via `--url`. + +`QuickTestBurstUi` serves a local form for `QuickTestBurst` at http://localhost:8090: it starts each run as a separate `QuickTestBurst` process, streams the output to the page, and shows the equivalent command line. Its page is `src/main/resources/org/upgradeplatform/client/quicktest-burst-ui.html`. From the repo root, `yarn quickburst:ui` (`scripts/quickburst-ui.sh`) compiles and starts it: + +```bash +yarn quickburst:ui # http://localhost:8090 +yarn quickburst:ui --port 8091 # another port ``` Only localhost is in the code. `qa` / `staging` (here and in `QuickTestBurst --env`) read the base URL from `UPGRADE_QA_URL` / `UPGRADE_STAGING_URL`, which you export yourself (e.g. in your shell profile) — hosted environments' URLs stay out of this public repo. See `QuickTestHosts`. diff --git a/clientlibs/java/src/main/java/org/upgradeplatform/client/QuickTest.java b/clientlibs/java/src/main/java/org/upgradeplatform/client/QuickTest.java index 582cbbf3e..572b5cb3a 100644 --- a/clientlibs/java/src/main/java/org/upgradeplatform/client/QuickTest.java +++ b/clientlibs/java/src/main/java/org/upgradeplatform/client/QuickTest.java @@ -20,12 +20,24 @@ * *
* mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTest" + * mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTest" -Dexec.args="local:3031" * mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTest" -Dexec.args="qa" * mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTest" -Dexec.args="staging" + * mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTest" -Dexec.args="https://some-host.example.com" ** - * The optional argument is local (default, http://localhost:3030), qa or staging; qa and staging read - * UPGRADE_QA_URL / UPGRADE_STAGING_URL from the environment (see QuickTestHosts). + * The optional argument is local (default, http://localhost:3030), local:<port> for localhost on another port, + * qa, staging, or a base URL starting with http:// or https://. + *
+ * qa and staging are not built in. Their URLs come from environment variables you set on your own machine, + * which you need to add to your shell profile (e.g. {@code ~/.zshrc}) once: + * + *
+ * export UPGRADE_QA_URL=https://<qa backend host> + * export UPGRADE_STAGING_URL=https://<staging backend host> + *+ * + * See QuickTestHosts for details. Passing a base URL directly needs no variables. */ public class QuickTest { private static final String userId = "quicktest_user_" + System.currentTimeMillis(); @@ -56,9 +68,13 @@ public class QuickTest { logRequest.add(logEntry); } - /** Optional first argument: local (default), qa or staging; see QuickTestHosts for the env vars qa/staging need. */ + /** + * Optional first argument: local (default), local:<port>, qa, staging, or a base URL; see QuickTestHosts for + * the env vars qa/staging need. + */ public static void main(String[] args) throws InterruptedException, ExecutionException { - String hostUrl = QuickTestHosts.forEnv(args.length > 0 ? args[0] : "local"); + String host = args.length > 0 ? args[0] : "local"; + String hostUrl = host.startsWith("http://") || host.startsWith("https://") ? host : QuickTestHosts.forEnv(host); System.out.println("host: " + hostUrl); System.out.println("client_session_id: " + sessionId); ExperimentClient client = new ExperimentClient(userId, context, "BearerToken", sessionId, hostUrl, diff --git a/clientlibs/java/src/main/java/org/upgradeplatform/client/QuickTestBurst.java b/clientlibs/java/src/main/java/org/upgradeplatform/client/QuickTestBurst.java index 0de526122..ccaf20007 100644 --- a/clientlibs/java/src/main/java/org/upgradeplatform/client/QuickTestBurst.java +++ b/clientlibs/java/src/main/java/org/upgradeplatform/client/QuickTestBurst.java @@ -2,13 +2,22 @@ import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Random; +import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jdt.annotation.NonNull; @@ -37,6 +46,12 @@ * aliases) is not counted in any timing. Bursts never overlap. The sizes and waits come from {@code --seed} * (printed on every run), so a run can be repeated exactly, e.g. to compare two backend builds. * + * With {@code --ratePerSecond}, bursts overlap instead: a new user (setup, then its burst) starts every 1/rate + * seconds on average, whether or not earlier users have finished, the way users arrive in production. {@code --loops} + * is then the number of users started (e.g. 360 at 6/s is about a minute). Gaps between starts are random + * (exponential, i.e. Poisson arrivals) and come from {@code --seed}, or exactly 1/rate with {@code --evenArrivals}. + * The summary adds the arrival rate actually achieved and the peak users and pairs in flight at once. + * * Pairs are streamed: a new pair starts only when one of the {@code --concurrency} slots frees up, and nothing is * kept per pair once it finishes except its latency samples, so memory stays flat however many loops run. * @@ -46,20 +61,57 @@ * -Dexec.args="185 --concurrency 50 --env qa --context assign-prog" * mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTestBurst" \ * -Dexec.args="--loops 50 --minBurst 10 --maxBurst 180 --minBurstWait 100 --maxBurstWait 1000 --seed 42" + * mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTestBurst" \ + * -Dexec.args="--loops 360 --minBurst 1 --maxBurst 50 --ratePerSecond 6 --seed 42 --env qa" + * mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTestBurst" \ + * -Dexec.args="--loops 360 --burstTiers 90:1,8:2-10,1:11-30,1:31-200 --ratePerSecond 6 --seed 42 --env qa" + * mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTestBurst" \ + * -Dexec.args="185 --url https://some-host.example.com" + * + * + * {@code --env qa} and {@code --env staging} are not built in. Their URLs come from environment variables you + * set on your own machine, which you need to add to your shell profile (e.g. {@code ~/.zshrc}) once: + * + *
+ * export UPGRADE_QA_URL=https://<qa backend host> + * export UPGRADE_STAGING_URL=https://<staging backend host> ** - * Options (give either {@code
+ * yarn quickburst:ui (from the repo root; compiles first, see scripts/quickburst-ui.sh) + * yarn quickburst:ui --port 8091 + * mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTestBurstUi" + * mvn exec:java -Dexec.mainClass="org.upgradeplatform.client.QuickTestBurstUi" -Dexec.args="--port 8091" + *+ * + * then open http://localhost:8090 (or the port given). Ctrl-C stops it, along with any run in progress. + * + * Each run is its own QuickTestBurst process, started with this one's classpath and environment, so {@code qa} and + * {@code staging} need UPGRADE_QA_URL / UPGRADE_STAGING_URL set (see QuickTestHosts) before this starts; the page + * shows which are missing. One run at a time. The page shows the command line for the options it sends, so any run + * can be repeated from a terminal. It listens on localhost only. + */ +public class QuickTestBurstUi { + private static final int DEFAULT_PORT = 8090; + // Every POST must carry this. A page on another site can't add it without a CORS preflight, which this never + // answers, so another site can't start runs. + private static final String REQUEST_HEADER = "X-QuickTestBurst"; + // Older lines are dropped past this, so a long --verbose run doesn't grow without bound. The summary is printed + // last, so it's always kept. + private static final int MAX_LINES = 20_000; + private static final String JSON = "application/json; charset=utf-8"; + + private static int port = DEFAULT_PORT; + private static String page; + private static int runCount = 0; + private static Run current = null; + + /** One QuickTestBurst process and the output it has printed so far. */ + private static final class Run { + final int id; + final List
+ * export UPGRADE_QA_URL=https://<qa backend host> + * export UPGRADE_STAGING_URL=https://<staging backend host> + *+ * + * then open a new terminal (or {@code source ~/.zshrc}). An IDE reads your profile only when it starts, so restart it + * before running from there. To skip the variables entirely, pass the base URL itself instead of an env name. + *
+ * {@code local} is localhost on port 3030; {@code local: Runs QuickTestBurst with these options. One run at a time.QuickTestBurst
+
+
+