feat(plugins): 插件侧计数器与周期健康日志,Limbo/刷新失败按故障周期升级日志

This commit is contained in:
Lemon-miaow committed 2026-09-26 00:33:04 +08:00
1 parent fcf5c305ea
commit b65c2c00b3
13 files changed
+634 -17

No files matched your search

@@ -20,7 +20,8 @@ import java.util.concurrent.atomic.AtomicInteger;
* felis-api that counts the requests each path receives: a GET is tried once more
* after a connection that dropped mid-reply or a 502/503/504, never a third time; a POST is
* never repeated (the first attempt may have landed); a GET that ran past the
* request timeout is cut off at that timeout and not repeated.
* request timeout is cut off at that timeout and not repeated. It then checks the
* client's {@link ApiStats} against the requests the stub saw.
*
* <p>Run: {@code javac -d <out> shared/src/main/java/best/lolicon/felis/link/*.java
* shared/test/best/lolicon/felis/link/FelisApiClientRetryTest.java && java -cp <out>
@@ -49,6 +50,8 @@ public final class FelisApiClientRetryTest {
getIsNotRetriedOnAClientError(api);
postIsNeverRetried(api);
timedOutGetIsCutOffAndNotRetried(api);
statsCountEveryAttempt(api);
statsArithmetic();
} finally {
stub.stop(0);
pool.shutdownNow();
@@ -149,6 +152,61 @@ public final class FelisApiClientRetryTest {
assertEq("slow requests", 1, count("GET /api/v1/internal/servers/slow/status"));
}
// The calls above, attempt by attempt: flaky 503+200, cut dropped+200, down 502+502,
// gone 404, busy 503 (POST), slow timed out. Nine attempts, three of them retries;
// no answer twice (the cut reply, the timeout), one 4xx, four 5xx.
private static void statsCountEveryAttempt(FelisApiClient api) throws LinkException {
ApiStats.Snapshot t = api.stats().total();
int sent = hits.values().stream().mapToInt(AtomicInteger::get).sum();
assertEq("stats calls = requests the stub saw", (long) sent, t.calls);
assertEq("stats calls", 9L, t.calls);
assertEq("stats no answer", 2L, t.transport);
assertEq("stats 4xx", 1L, t.clientErrors);
assertEq("stats 5xx", 4L, t.serverErrors);
assertEq("stats retried", 3L, t.retries);
// The slowest attempt is the one cut off by the 400 ms request timeout.
if (t.maxMillis < 390 || t.maxMillis >= 1200) {
throw new AssertionError("stats max: " + t.maxMillis + " ms, want the ~400 ms timeout");
}
checks++;
ApiStats.Snapshot w1 = api.stats().window();
assertEq("first window = totals so far", 9L, w1.calls);
assertEq("first window 5xx", 4L, w1.serverErrors);
assertEq("first window max", t.maxMillis, w1.maxMillis);
ApiStats.Snapshot w2 = api.stats().window();
assertEq("empty window calls", 0L, w2.calls);
assertEq("empty window max", 0L, w2.maxMillis);
assertEq("empty window avg", 0L, w2.avgMillis());
assertEq("total max outlives the windows", t.maxMillis, api.stats().total().maxMillis);
api.serverStatus("flaky"); // answers 200 from now on
ApiStats.Snapshot w3 = api.stats().window();
assertEq("next window calls", 1L, w3.calls);
assertEq("next window 5xx", 0L, w3.serverErrors);
assertEq("next window retried", 0L, w3.retries);
assertEq("totals keep growing", 10L, api.stats().total().calls);
}
// The status classes at their edges and the latency figures, on fixed inputs.
private static void statsArithmetic() {
ApiStats s = new ApiStats();
s.record(200, 10);
s.record(399, 40);
s.record(400, 30);
s.record(499, 5);
s.record(500, 20);
s.record(0, 15);
ApiStats.Snapshot t = s.total();
assertEq("edge calls", 6L, t.calls);
assertEq("edge 4xx (400, 499)", 2L, t.clientErrors);
assertEq("edge 5xx (500)", 1L, t.serverErrors);
assertEq("edge no answer", 1L, t.transport);
assertEq("avg of 10+40+30+5+20+15 over 6", 20L, t.avgMillis());
assertEq("max is the slowest, not the last", 40L, t.maxMillis);
assertEq("summary", "calls=6 (no answer=1, 4xx=2, 5xx=1, retried=0), avg=20 ms, max=40 ms",
t.summary());
}
// ---- harness ----
interface Call {