How to Simulate Fixed QPS Mock APIs with Java Semaphore and Moco
This article explains how to overcome the difficulty of isolating external service performance during load testing by creating a fixed‑QPS mock endpoint using Java's Semaphore and a custom Moco ResponseHandler, includes code samples, accuracy observations, and practical guidelines.
Background and Problem
When performing interface testing, calls to other services or third‑party APIs make it hard to isolate performance variations during load testing. Maintaining a separate environment is resource‑intensive, and third‑party performance data is often unavailable.
Initial Attempt with Moco
We first tried to use the Moco API to create a fixed‑QPS mock endpoint, but after an hour of reading the documentation and trying demos, the approach proved infeasible.
Custom Solution Using Java Semaphore
We implemented a custom rate‑limiting mock by leveraging java.util.concurrent.Semaphore to control concurrency and a custom ResponseHandler that sleeps before releasing the permit. The handler receives a token, pauses for a configurable gap (in microseconds), then forwards the response.
Observed Accuracy
Tests showed error margins within 10 % and, with well‑designed test plans, typically under 5 %. Three empirical rules emerged:
More request threads reduce error.
Higher request counts reduce error.
Fully warmed‑up systems reduce error.
Demo Code
HttpServer server = getServer(8088);
server.get(urlOnly("/aaa")).response(qps(textRes("faun"), 10));
server.response("haha");
MocoServer drive = run(server);
waitForKey("fan");
drive.stop();Utility Methods
static ResponseHandler qps(ResponseHandler handler) {
return QPSHandler.newSeq(handler, 1000);
}
static ResponseHandler qps(ResponseHandler handler, int gap) {
return QPSHandler.newSeq(handler, gap);
}Implementation of QPSHandler
package com.fun.moco.support;
import com.github.dreamhead.moco.ResponseHandler;
import com.github.dreamhead.moco.handler.AbstractResponseHandler;
import com.github.dreamhead.moco.internal.SessionContext;
import com.github.dreamhead.moco.util.Idles;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import static com.google.common.base.Preconditions.checkArgument;
class QPSHandler extends AbstractResponseHandler {
private static final Semaphore semaphore = new Semaphore(1, true);
private final int gap;
private final ResponseHandler handler;
private QPSHandler(ResponseHandler handler, int gap) {
this.gap = gap;
this.handler = handler;
}
public static ResponseHandler newSeq(final ResponseHandler handler, int gap) {
checkArgument(handler != null, "responsehandler cannot be null!");
return new QPSHandler(handler, gap);
}
@Override
void writeToResponse(SessionContext context) {
semaphore.acquire();
Idles.idle(gap * 1000, TimeUnit.MICROSECONDS);
handler.writeToResponse(context);
semaphore.release();
}
}How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
