Compare commits

...

4 Commits

Author SHA1 Message Date
Janis d2cd3254b3 feat(benchmarks): add MoveBenchmark for performance testing of game rules
Build & Test (NowChessSystems) TeamCity build finished
2026-05-06 09:19:20 +02:00
Janis 763a2b8c39 feat(benchmarks): add JMH benchmarks for performance testing and load scripts
Build & Test (NowChessSystems) TeamCity build failed
2026-05-06 08:41:49 +02:00
Janis 0eb752d493 fix(redis): enhance GameRedisSubscriberManager to use ReactiveRedisDataSource and improve subscription handling
Build & Test (NowChessSystems) TeamCity build finished
2026-05-06 08:41:30 +02:00
Janis e279c39246 fix(auth): add InternalClientHeadersFactory for custom client headers management
Build & Test (NowChessSystems) TeamCity build failed
2026-05-06 08:07:58 +02:00
16 changed files with 636 additions and 30 deletions
+3 -1
View File
@@ -3,6 +3,7 @@ plugins {
id("org.scoverage") version "8.1" apply false id("org.scoverage") version "8.1" apply false
id("com.diffplug.spotless") version "8.4.0" apply false id("com.diffplug.spotless") version "8.4.0" apply false
id("io.github.cosmicsilence.scalafix") version "0.2.6" apply false id("io.github.cosmicsilence.scalafix") version "0.2.6" apply false
id("me.champeau.jmh") version "0.7.2" apply false
} }
group = "de.nowchess" group = "de.nowchess"
@@ -99,7 +100,8 @@ val versions = mapOf(
"SCALA_PARSER_COMBINATORS" to "2.4.0", "SCALA_PARSER_COMBINATORS" to "2.4.0",
"FASTPARSE" to "3.0.2", "FASTPARSE" to "3.0.2",
"JACKSON" to "2.17.2", "JACKSON" to "2.17.2",
"JACKSON_SCALA" to "2.17.2" "JACKSON_SCALA" to "2.17.2",
"JMH" to "1.37"
) )
extra["VERSIONS"] = versions extra["VERSIONS"] = versions
+100
View File
@@ -0,0 +1,100 @@
# NowChess Load Testing with k6
Performance testing suite for NowChess services using k6.
## Installation
```bash
# Install k6 (macOS)
brew install k6
# Or download from https://k6.io/docs/getting-started/installation/
```
## Test Scenarios
### 1. Ramp-Up Test
Gradually increases load from 10 to 100 concurrent users.
```bash
k6 run ramp-up.js
```
Target: Identify system behavior under gradual load increase.
### 2. Stress Test
Incremental load increase up to 500 concurrent users to find breaking point.
```bash
k6 run stress-test.js
```
Target: Determine system capacity and failure point.
### 3. Spike Test
Sudden traffic surge (baseline 50 → 500 users instantly).
```bash
k6 run spike-test.js
```
Target: Test recovery and resilience to sudden spikes.
### 4. Constant Load Test
Maintains 50 VUs for 10 minutes.
```bash
k6 run constant-load.js
```
Target: Check stability under sustained load.
## Environment Variables
```bash
# Override service endpoints
export BASE_URL=http://localhost:8080
export ACCOUNT_HOST=http://localhost:8083
export STORE_HOST=http://localhost:8085
k6 run ramp-up.js
```
## Prerequisites
Ensure services are running:
- Core: `localhost:8080`
- Account: `localhost:8083`
- Store: `localhost:8085`
- Redis: `localhost:6379` (with increased pool size)
## Metrics Interpretation
- `http_req_duration`: Response time (p95, p99 percentiles matter most)
- `http_req_failed`: Failed requests (connection errors, errors, non-2xx responses)
- `vus`: Virtual Users (concurrent connections)
- `iterations`: Completed test cycles per VU
## Results
k6 generates HTML report output. Use with:
```bash
k6 run --out=csv=results.csv ramp-up.js
```
## Troubleshooting
If you see:
- **"max pool wait timeout"** → Redis pool still too small or maxWaitTime too short
- **"connection refused"** → Service not running
- **"high p99 latency"** → System approaching capacity
Increase Redis pool settings in `modules/*/src/main/resources/application.yml`:
```yaml
quarkus:
redis:
pool:
max-size: 128 # Increase if still hitting limits
max-waiting: 256
```
+25
View File
@@ -0,0 +1,25 @@
export const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
export const ACCOUNT_HOST = __ENV.ACCOUNT_HOST || 'http://localhost:8083';
export const STORE_HOST = __ENV.STORE_HOST || 'http://localhost:8085';
export const CORE_HOST = BASE_URL;
export const ENDPOINTS = {
// Account endpoints
accountCreateUser: `${ACCOUNT_HOST}/api/account`,
accountLogin: `${ACCOUNT_HOST}/api/account/login`,
accountProfile: `${ACCOUNT_HOST}/api/account/me`,
accountPublicProfile: (username) => `${ACCOUNT_HOST}/api/account/${username}`,
// Store endpoints
storeGame: `${STORE_HOST}/api/games`,
storeGameById: (gameId) => `${STORE_HOST}/api/games/${gameId}`,
// Core endpoints (game operations)
gameWebSocket: (gameId) => `ws://localhost:8080/api/games/${gameId}/ws`,
};
export const TEST_USERS = [
{ username: 'load-test-user-1', email: 'load1@example.com' },
{ username: 'load-test-user-2', email: 'load2@example.com' },
{ username: 'load-test-user-3', email: 'load3@example.com' },
];
+40
View File
@@ -0,0 +1,40 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
import { ENDPOINTS } from './config.js';
export const options = {
vus: 50,
duration: '10m',
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.1'],
},
};
export default function () {
// Simulate consistent user traffic
// 1. Get account profile
const profileRes = http.get(ENDPOINTS.accountProfile);
check(profileRes, {
'profile status is 200 or 401': (r) => r.status === 200 || r.status === 401,
});
sleep(0.5);
// 2. Get public profile
const publicRes = http.get(ENDPOINTS.accountPublicProfile('testuser'));
check(publicRes, {
'public profile status ok': (r) => r.status > 0,
});
sleep(0.5);
// 3. List store games
const gamesRes = http.get(ENDPOINTS.storeGame);
check(gamesRes, {
'store games status ok': (r) => r.status > 0,
});
sleep(1);
}
+35
View File
@@ -0,0 +1,35 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
import { ENDPOINTS, ACCOUNT_HOST } from './config.js';
export const options = {
stages: [
{ duration: '1m', target: 10 },
{ duration: '3m', target: 50 },
{ duration: '5m', target: 100 },
{ duration: '3m', target: 50 },
{ duration: '1m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.1'],
},
};
export default function () {
// Test account endpoints
const accountRes = http.get(ENDPOINTS.accountPublicProfile('testuser'));
check(accountRes, {
'account endpoint status is 200': (r) => r.status === 200,
});
sleep(1);
// Test store endpoints
const storeRes = http.get(ENDPOINTS.storeGame);
check(storeRes, {
'store endpoint status is 200': (r) => r.status === 200 || r.status === 401,
});
sleep(1);
}
+68
View File
@@ -0,0 +1,68 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default values
TEST_TYPE="${1:-ramp-up}"
BASE_URL="${BASE_URL:-http://localhost:8080}"
OUTPUT_FILE="${2:-results-$(date +%s).csv}"
echo -e "${BLUE}NowChess Load Test Runner${NC}"
echo "Test Type: $TEST_TYPE"
echo "Base URL: $BASE_URL"
echo ""
# Check if k6 is installed
if ! command -v k6 &> /dev/null; then
echo -e "${RED}Error: k6 not found. Install from https://k6.io/docs/getting-started/installation/${NC}"
exit 1
fi
case "$TEST_TYPE" in
ramp-up)
echo -e "${GREEN}Running Ramp-Up Test (10->100 VUs over 13 minutes)${NC}"
k6 run --out=csv=$OUTPUT_FILE ramp-up.js
;;
stress)
echo -e "${GREEN}Running Stress Test (up to 500 VUs)${NC}"
k6 run --out=csv=$OUTPUT_FILE stress-test.js
;;
spike)
echo -e "${GREEN}Running Spike Test (sudden 50->500 spike)${NC}"
k6 run --out=csv=$OUTPUT_FILE spike-test.js
;;
constant)
echo -e "${GREEN}Running Constant Load Test (50 VUs for 10m)${NC}"
k6 run --out=csv=$OUTPUT_FILE constant-load.js
;;
all)
echo -e "${GREEN}Running All Tests${NC}"
for test in ramp-up stress spike constant; do
echo ""
echo -e "${BLUE}Starting $test test...${NC}"
$0 $test
sleep 5
done
exit 0
;;
*)
echo -e "${RED}Usage: $0 {ramp-up|stress|spike|constant|all} [output-file]${NC}"
echo ""
echo "Examples:"
echo " $0 ramp-up"
echo " $0 stress results.csv"
echo " $0 all"
exit 1
;;
esac
echo -e "${GREEN}Test complete. Results saved to $OUTPUT_FILE${NC}"
+36
View File
@@ -0,0 +1,36 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
import { ENDPOINTS } from './config.js';
export const options = {
stages: [
{ duration: '1m', target: 50 },
{ duration: '30s', target: 500, ramp: 'fast' },
{ duration: '2m', target: 500 },
{ duration: '30s', target: 50, ramp: 'fast' },
{ duration: '1m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<2000', 'p(99)<5000'],
http_req_failed: ['rate<0.3'],
},
};
export default function () {
// Rapid account checks
const urls = [
ENDPOINTS.accountPublicProfile('testuser'),
ENDPOINTS.storeGame,
ENDPOINTS.accountProfile,
];
urls.forEach((url) => {
const res = http.get(url);
check(res, {
'response received': (r) => r.status > 0,
'response time under 3s': (r) => r.timings.duration < 3000,
});
});
sleep(0.1);
}
+38
View File
@@ -0,0 +1,38 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
import { ENDPOINTS } from './config.js';
export const options = {
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 200 },
{ duration: '5m', target: 300 },
{ duration: '5m', target: 400 },
{ duration: '5m', target: 500 },
{ duration: '5m', target: 400 },
{ duration: '5m', target: 200 },
{ duration: '2m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<1000', 'p(99)<2000'],
http_req_failed: ['rate<0.2'],
},
};
export default function () {
const userIndex = Math.floor(Math.random() * 10);
// Burst account endpoint
const accountRes = http.get(ENDPOINTS.accountPublicProfile(`user-${userIndex}`));
check(accountRes, {
'account endpoint responds': (r) => r.status > 0,
});
// Burst store endpoint
const storeRes = http.get(ENDPOINTS.storeGame);
check(storeRes, {
'store endpoint responds': (r) => r.status > 0,
});
sleep(Math.random() * 0.5);
}
@@ -1,9 +1,9 @@
package de.nowchess.account.client package de.nowchess.account.client
import de.nowchess.security.InternalSecretClientFilter import de.nowchess.security.{InternalClientHeadersFactory, InternalSecretClientFilter}
import jakarta.ws.rs.* import jakarta.ws.rs.*
import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.core.MediaType
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider import org.eclipse.microprofile.rest.client.annotation.{RegisterClientHeaders, RegisterProvider}
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
case class CorePlayerInfo(id: String, displayName: String) case class CorePlayerInfo(id: String, displayName: String)
@@ -19,6 +19,7 @@ case class CoreGameResponse(gameId: String)
@Path("/api/board/game") @Path("/api/board/game")
@RegisterRestClient(configKey = "core-service") @RegisterRestClient(configKey = "core-service")
@RegisterProvider(classOf[InternalSecretClientFilter]) @RegisterProvider(classOf[InternalSecretClientFilter])
@RegisterClientHeaders(classOf[InternalClientHeadersFactory])
trait CoreGameClient: trait CoreGameClient:
@POST @POST
+36
View File
@@ -0,0 +1,36 @@
# Gradle
.gradle/
build/
# Eclipse
.project
.classpath
.settings/
bin/
# IntelliJ
.idea
*.ipr
*.iml
*.iws
# NetBeans
nb-configuration.xml
# Visual Studio Code
.vscode
.factorypath
# OSX
.DS_Store
# Vim
*.swp
*.swo
# patch
*.orig
*.rej
# Local environment
.env
+77
View File
@@ -0,0 +1,77 @@
# JMH Benchmarks
Java Microbenchmark Harness (JMH) benchmarks for performance-critical components.
## Building
```bash
./gradlew modules:benchmarks:jmhJar
```
Produces `modules/benchmarks/build/libs/benchmarks-1.0-SNAPSHOT-jmh.jar`.
## Running Benchmarks
Since these benchmarks are written in Scala, JMH auto-discovery via jar packaging doesn't work. Instead, run benchmarks via classpath:
```bash
./gradlew modules:benchmarks:run --args='de.nowchess.benchmarks.MoveBenchmark'
```
Or compile and run with explicit classpath:
```bash
./gradlew modules:benchmarks:compileScala
java -cp "modules/benchmarks/build/classes/scala/main:modules/benchmarks/build/resources/main:$(./gradlew -q printClasspath)" \
org.openjdk.jmh.Main de.nowchess.benchmarks.MoveBenchmark
```
Adjust JMH options:
- `-f 1` — 1 fork (faster iteration, less reliable)
- `-i 3 -w 2` — 3 measurement iterations, 2 warmup iterations
- `-bm avgt` — average time mode
- `-tu us` — time unit (microseconds)
Example:
```bash
java -cp "..." org.openjdk.jmh.Main de.nowchess.benchmarks.MoveBenchmark -f 1 -i 3 -w 2 -bm avgt
```
## Writing Benchmarks
Create Scala class with `@Benchmark` methods:
```scala
@BenchmarkMode(Array(Mode.AverageTime, Mode.Throughput))
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Fork(2)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
class SomeBenchmark {
@Setup(Level.Trial)
def setup(): Unit = ???
@org.openjdk.jmh.annotations.Benchmark
def benchmarkSomething(bh: Blackhole): Unit = {
val result = expensiveOperation()
bh.consume(result) // Prevent dead-code elimination
}
}
```
## Benchmark Modes
- `AverageTime` — average execution time per operation
- `Throughput` — operations per unit time
- `SampleTime` — latency samples (min, max, percentiles)
- `SingleShotTime` — total time to execute N ops once
- `All` — run all modes
## Limitations
JMH's Gradle plugin doesn't fully support Scala annotation processing, so benchmarks must be run explicitly by class name rather than auto-discovered. This is a known limitation of JMH + Scala; consider Java-based benchmarks for full auto-discovery support.
## References
- [OpenJDK JMH](https://github.com/openjdk/jmh)
- [JMH Samples](https://hg.openjdk.org/code-tools/jmh/file/tip/jmh-samples)
+61
View File
@@ -0,0 +1,61 @@
plugins {
id("scala")
id("me.champeau.jmh")
}
group = "de.nowchess"
version = "1.0-SNAPSHOT"
@Suppress("UNCHECKED_CAST")
val versions = rootProject.extra["VERSIONS"] as Map<String, String>
repositories {
mavenCentral()
}
scala {
scalaVersion = versions["SCALA3"]!!
}
dependencies {
implementation("org.scala-lang:scala3-library_3") {
version {
strictly(versions["SCALA3"]!!)
}
}
compileOnly("org.scala-lang:scala3-compiler_3") {
version {
strictly(versions["SCALA3"]!!)
}
}
// Core modules for benchmarking
implementation(project(":modules:api"))
implementation(project(":modules:core"))
implementation(project(":modules:rule"))
implementation(project(":modules:bot-platform"))
// JMH (jmh configuration is for annotation processor + runtime)
implementation("org.openjdk.jmh:jmh-core:${versions["JMH"]!!}")
jmh("org.openjdk.jmh:jmh-core:${versions["JMH"]!!}")
jmh("org.openjdk.jmh:jmh-generator-annprocess:${versions["JMH"]!!}")
}
configurations.matching { !it.name.startsWith("jmh") }.configureEach {
resolutionStrategy.force("org.scala-lang:scala-library:${versions["SCALA_LIBRARY"]!!}")
}
jmh {
jmhVersion.set(versions["JMH"]!!)
includeTests.set(false)
duplicateClassesStrategy.set(DuplicatesStrategy.WARN)
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
options.compilerArgs.add("-parameters")
}
tasks.withType<org.gradle.api.tasks.scala.ScalaCompile> {
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
}
@@ -0,0 +1,52 @@
package de.nowchess.benchmarks
import org.openjdk.jmh.annotations.{BenchmarkMode, Fork, Measurement, OutputTimeUnit, Setup, State, Warmup}
import org.openjdk.jmh.annotations.Mode
import org.openjdk.jmh.annotations.Scope
import org.openjdk.jmh.annotations.Level
import org.openjdk.jmh.infra.Blackhole
import de.nowchess.api.board.{File, Rank, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.rules.sets.DefaultRules
import java.util.concurrent.TimeUnit
// scalafix:off DisableSyntax.var
@BenchmarkMode(Array(Mode.AverageTime, Mode.Throughput))
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Fork(value = 2, warmups = 1)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
class MoveBenchmark {
private var gameContext: GameContext = scala.compiletime.uninitialized
@Setup(Level.Trial)
def setup(): Unit =
gameContext = GameContext.initial
@org.openjdk.jmh.annotations.Benchmark
def benchmarkAllLegalMoves(bh: Blackhole): Unit = {
val moves = DefaultRules.allLegalMoves(gameContext)
bh.consume(moves)
}
@org.openjdk.jmh.annotations.Benchmark
def benchmarkIsCheck(bh: Blackhole): Unit = {
val inCheck = DefaultRules.isCheck(gameContext)
bh.consume(inCheck)
}
@org.openjdk.jmh.annotations.Benchmark
def benchmarkLegalMovesFirstSquare(bh: Blackhole): Unit = {
val firstSquare = Square(File.A, Rank.R2)
val moves = DefaultRules.legalMoves(gameContext)(firstSquare)
bh.consume(moves)
}
@org.openjdk.jmh.annotations.Benchmark
def benchmarkIsCheckmate(bh: Blackhole): Unit = {
val result = DefaultRules.isCheckmate(gameContext)
bh.consume(result)
}
}
@@ -10,8 +10,9 @@ import de.nowchess.chess.observer.Observer
import de.nowchess.chess.registry.GameRegistry import de.nowchess.chess.registry.GameRegistry
import de.nowchess.chess.resource.GameDtoMapper import de.nowchess.chess.resource.GameDtoMapper
import de.nowchess.chess.service.InstanceHeartbeatService import de.nowchess.chess.service.InstanceHeartbeatService
import io.quarkus.redis.datasource.ReactiveRedisDataSource
import io.quarkus.redis.datasource.RedisDataSource import io.quarkus.redis.datasource.RedisDataSource
import io.quarkus.redis.datasource.pubsub.PubSubCommands import io.quarkus.redis.datasource.pubsub.ReactivePubSubCommands
import jakarta.annotation.PreDestroy import jakarta.annotation.PreDestroy
import jakarta.enterprise.context.ApplicationScoped import jakarta.enterprise.context.ApplicationScoped
import jakarta.enterprise.inject.Instance import jakarta.enterprise.inject.Instance
@@ -29,6 +30,7 @@ class GameRedisSubscriberManager:
// scalafix:off DisableSyntax.var // scalafix:off DisableSyntax.var
@Inject var redis: RedisDataSource = uninitialized @Inject var redis: RedisDataSource = uninitialized
@Inject var reactiveRedis: ReactiveRedisDataSource = uninitialized
@Inject var registry: GameRegistry = uninitialized @Inject var registry: GameRegistry = uninitialized
@Inject var objectMapper: ObjectMapper = uninitialized @Inject var objectMapper: ObjectMapper = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized @Inject var redisConfig: RedisConfig = uninitialized
@@ -40,7 +42,7 @@ class GameRedisSubscriberManager:
if heartbeatServiceInstance.isUnsatisfied then None if heartbeatServiceInstance.isUnsatisfied then None
else Some(heartbeatServiceInstance.get()) else Some(heartbeatServiceInstance.get())
private val c2sListeners = new ConcurrentHashMap[String, PubSubCommands.RedisSubscriber]() private val c2sListeners = new ConcurrentHashMap[String, ReactivePubSubCommands.ReactiveRedisSubscriber]()
private val s2cObservers = new ConcurrentHashMap[String, Observer]() private val s2cObservers = new ConcurrentHashMap[String, Observer]()
private def c2sTopic(gameId: String): String = private def c2sTopic(gameId: String): String =
@@ -50,35 +52,37 @@ class GameRedisSubscriberManager:
s"${redisConfig.prefix}:game:$gameId:s2c" s"${redisConfig.prefix}:game:$gameId:s2c"
def subscribeGame(gameId: String): Unit = def subscribeGame(gameId: String): Unit =
try val writebackFn: String => Unit = json => redis.pubsub(classOf[String]).publish("game-writeback", json)
val handler: Consumer[String] = msg => handleC2sMessage(gameId, msg) val obs = new GameRedisPublisher(
val subscriber = redis.pubsub(classOf[String]).subscribe(c2sTopic(gameId), handler) gameId,
c2sListeners.put(gameId, subscriber) registry,
redis,
objectMapper,
s2cTopicName(gameId),
writebackFn,
ioClient,
unsubscribeGame,
)
s2cObservers.put(gameId, obs)
registry.get(gameId).foreach(_.engine.subscribe(obs))
heartbeatServiceOpt.foreach(_.addGameSubscription(gameId))
val writebackFn: String => Unit = json => redis.pubsub(classOf[String]).publish("game-writeback", json) val handler: Consumer[String] = msg => handleC2sMessage(gameId, msg)
val obs = new GameRedisPublisher( reactiveRedis
gameId, .pubsub(classOf[String])
registry, .subscribe(c2sTopic(gameId), handler)
redis, .subscribe()
objectMapper, .`with`(
s2cTopicName(gameId), subscriber => {
writebackFn, c2sListeners.put(gameId, subscriber)
ioClient, log.debugf("Subscribed to game %s", gameId)
unsubscribeGame, },
failure => log.warnf(failure, "Redis subscription failed for game %s", gameId),
) )
s2cObservers.put(gameId, obs)
registry.get(gameId).foreach(_.engine.subscribe(obs))
log.debugf("Subscribed to game %s", gameId)
heartbeatServiceOpt.foreach(_.addGameSubscription(gameId))
catch
case e: Exception =>
log.warnf(e, "Redis subscription failed for game %s", gameId)
()
def unsubscribeGame(gameId: String): Unit = def unsubscribeGame(gameId: String): Unit =
Option(c2sListeners.remove(gameId)).foreach { subscriber => Option(c2sListeners.remove(gameId)).foreach { subscriber =>
subscriber.unsubscribe(c2sTopic(gameId)) subscriber.unsubscribe(c2sTopic(gameId)).subscribe().`with`(_ => (), _ => ())
} }
Option(s2cObservers.remove(gameId)).foreach { obs => Option(s2cObservers.remove(gameId)).foreach { obs =>
registry.get(gameId).foreach(_.engine.unsubscribe(obs)) registry.get(gameId).foreach(_.engine.unsubscribe(obs))
@@ -154,5 +158,5 @@ class GameRedisSubscriberManager:
@PreDestroy @PreDestroy
def cleanup(): Unit = def cleanup(): Unit =
c2sListeners.forEach((gameId, subscriber) => subscriber.unsubscribe(c2sTopic(gameId))) c2sListeners.forEach((gameId, subscriber) => subscriber.unsubscribe(c2sTopic(gameId)).await().indefinitely())
s2cObservers.forEach((gameId, obs) => registry.get(gameId).foreach(_.engine.unsubscribe(obs))) s2cObservers.forEach((gameId, obs) => registry.get(gameId).foreach(_.engine.unsubscribe(obs)))
@@ -0,0 +1,30 @@
package de.nowchess.security
import jakarta.enterprise.context.ApplicationScoped
import jakarta.ws.rs.core.MultivaluedMap
import org.eclipse.microprofile.config.inject.ConfigProperty
import org.eclipse.microprofile.rest.client.ext.DefaultClientHeadersFactoryImpl
import scala.compiletime.uninitialized
@ApplicationScoped
class InternalClientHeadersFactory extends DefaultClientHeadersFactoryImpl {
@ConfigProperty(name = "nowchess.internal.secret", defaultValue = "")
// scalafix:off DisableSyntax.var
var secret: String = uninitialized
@ConfigProperty(name = "nowchess.internal.auth.enabled", defaultValue = "true")
var authEnabled: Boolean = uninitialized
// scalafix:on DisableSyntax.var
override def update(
incomingHeaders: MultivaluedMap[String, String],
clientOutgoingHeaders: MultivaluedMap[String, String],
): MultivaluedMap[String, String] = {
val default = super.update(incomingHeaders, clientOutgoingHeaders)
default.putSingle("X-Internal-Secret", secret)
default
}
}
+1
View File
@@ -26,4 +26,5 @@ include(
"modules:ws", "modules:ws",
"modules:store", "modules:store",
"modules:coordinator", "modules:coordinator",
"modules:benchmarks",
) )