|
3 | 3 | import org.junit.jupiter.api.*; |
4 | 4 | import org.springframework.http.ResponseEntity; |
5 | 5 | import org.springframework.test.context.ActiveProfiles; |
| 6 | +import redis.clients.jedis.Jedis; |
| 7 | +import redis.clients.jedis.Pipeline; |
6 | 8 |
|
7 | 9 | import java.util.*; |
8 | 10 | import java.util.stream.LongStream; |
@@ -78,6 +80,18 @@ void benchmarkListReservations() { |
78 | 80 | timings[0], timings[timings.length - 1], mean(timings))); |
79 | 81 | } |
80 | 82 |
|
| 83 | + @Test |
| 84 | + @DisplayName("GET /v1/reservations sorted at 1k total rows") |
| 85 | + void benchmarkSortedReservationsAt1k() { |
| 86 | + benchmarkSortedReservations(1_000, "LIST sorted @1k"); |
| 87 | + } |
| 88 | + |
| 89 | + @Test |
| 90 | + @DisplayName("GET /v1/reservations sorted at 10k total rows") |
| 91 | + void benchmarkSortedReservationsAt10k() { |
| 92 | + benchmarkSortedReservations(10_000, "LIST sorted @10k"); |
| 93 | + } |
| 94 | + |
81 | 95 | @Test |
82 | 96 | @DisplayName("GET /v1/balances") |
83 | 97 | void benchmarkGetBalances() { |
@@ -152,4 +166,50 @@ private Map<String, Object> decideBody(String tenant, long amount) { |
152 | 166 | body.put("estimate", Map.of("unit", "TOKENS", "amount", amount)); |
153 | 167 | return body; |
154 | 168 | } |
| 169 | + |
| 170 | + /** |
| 171 | + * Seed half the population for the authenticated tenant and half for an unrelated |
| 172 | + * tenant. The current sorted path scans the global reservation keyspace before |
| 173 | + * filtering by tenant, so this fixture exposes both population size and tenant |
| 174 | + * selectivity. Direct pipelined hashes keep setup time out of the measurement. |
| 175 | + */ |
| 176 | + private void benchmarkSortedReservations(int totalRows, String benchmarkName) { |
| 177 | + seedReservationPopulation(totalRows); |
| 178 | + |
| 179 | + long[] timings = runBenchmark(benchmarkName, () -> { |
| 180 | + ResponseEntity<Map> resp = get( |
| 181 | + "/v1/reservations?limit=20&sort_by=created_at_ms&sort_dir=desc", |
| 182 | + API_KEY_SECRET_A); |
| 183 | + assertThat(resp.getStatusCode().value()).isEqualTo(200); |
| 184 | + assertThat((List<?>) resp.getBody().get("reservations")).hasSize(20); |
| 185 | + }); |
| 186 | + record(new BenchmarkResult(benchmarkName, p(timings, 50), p(timings, 95), p(timings, 99), |
| 187 | + timings[0], timings[timings.length - 1], mean(timings))); |
| 188 | + } |
| 189 | + |
| 190 | + private void seedReservationPopulation(int totalRows) { |
| 191 | + final long createdAtBase = 1_700_000_000_000L; |
| 192 | + try (Jedis jedis = jedisPool.getResource(); Pipeline pipeline = jedis.pipelined()) { |
| 193 | + for (int i = 0; i < totalRows; i++) { |
| 194 | + String tenant = i % 2 == 0 ? TENANT_A : TENANT_B; |
| 195 | + String reservationId = String.format("bench-%05d", i); |
| 196 | + pipeline.hset("reservation:res_" + reservationId, Map.ofEntries( |
| 197 | + Map.entry("reservation_id", reservationId), |
| 198 | + Map.entry("tenant", tenant), |
| 199 | + Map.entry("state", "ACTIVE"), |
| 200 | + Map.entry("subject_json", "{\"tenant\":\"" + tenant + "\"}"), |
| 201 | + Map.entry("action_json", "{\"kind\":\"llm.completion\",\"name\":\"benchmark\"}"), |
| 202 | + Map.entry("estimate_amount", "100"), |
| 203 | + Map.entry("estimate_unit", "TOKENS"), |
| 204 | + Map.entry("scope_path", "tenant:" + tenant), |
| 205 | + Map.entry("affected_scopes", "[\"tenant:" + tenant + "\"]"), |
| 206 | + Map.entry("created_at", String.valueOf(createdAtBase + i)), |
| 207 | + Map.entry("expires_at", String.valueOf(createdAtBase + 86_400_000L + i)))); |
| 208 | + if ((i + 1) % 500 == 0) { |
| 209 | + pipeline.sync(); |
| 210 | + } |
| 211 | + } |
| 212 | + pipeline.sync(); |
| 213 | + } |
| 214 | + } |
155 | 215 | } |
0 commit comments