Skip to content

Commit cd8393d

Browse files
authored
Merge pull request #2 from SLNE-Development/copilot/implement-requests-and-responses
Add request-response pattern with timeout support
2 parents 1b89c67 + 8656ba1 commit cd8393d

23 files changed

+2933
-0
lines changed

README.md

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,175 @@ class MyPluginListener {
169169
}
170170
```
171171

172+
## Request-Response Pattern
173+
174+
In addition to the event bus, surf-redis supports request-response patterns where a server can send a request and wait for a response with timeout support.
175+
176+
### Quick Start
177+
178+
```kotlin
179+
import kotlinx.coroutines.runBlocking
180+
import kotlinx.coroutines.launch
181+
import de.slne.redis.request.*
182+
183+
// 1. Create your request and response (must be @Serializable)
184+
@Serializable
185+
data class GetPlayerRequest(val minLevel: Int) : RedisRequest()
186+
187+
@Serializable
188+
data class PlayerListResponse(val players: List<String>) : RedisResponse()
189+
190+
// 2. Create a request handler
191+
class PlayerRequestHandler {
192+
@RequestHandler
193+
fun handlePlayerRequest(context: RequestContext<GetPlayerRequest>) {
194+
// Launch coroutine to respond asynchronously
195+
context.coroutineScope.launch {
196+
val players = fetchPlayersAsync(context.request.minLevel)
197+
context.respond(PlayerListResponse(players))
198+
}
199+
}
200+
}
201+
202+
// 3. Set up and use
203+
runBlocking {
204+
val bus = RequestResponseBus("redis://localhost:6379")
205+
206+
// Register handler (ServerA)
207+
bus.registerRequestHandler(PlayerRequestHandler())
208+
209+
// Send request and wait for response (ServerB or same server)
210+
val response = bus.sendRequest<PlayerListResponse>(
211+
GetPlayerRequest(minLevel = 5),
212+
timeoutMs = 3000 // Default timeout is 3 seconds
213+
)
214+
println("Players: ${response.players}")
215+
}
216+
```
217+
218+
### Features
219+
220+
- 🔄 **Request-Response Pattern**: Send requests and receive typed responses
221+
- ⏱️ **Timeout Support**: Configurable timeout (default 3 seconds)
222+
- 🔀 **Bidirectional**: Any server can both send requests and respond to requests
223+
- 🚀 **Flexible Async**: Handlers control when to launch coroutines (not forced to be suspend)
224+
- 🎯 **Type-safe**: Request and response types are validated at compile time
225+
226+
### Usage
227+
228+
#### 1. Create Request and Response Classes
229+
230+
```kotlin
231+
import de.slne.redis.request.RedisRequest
232+
import de.slne.redis.request.RedisResponse
233+
import kotlinx.serialization.Serializable
234+
235+
@Serializable
236+
data class GetPlayerRequest(val minLevel: Int) : RedisRequest()
237+
238+
@Serializable
239+
data class PlayerListResponse(val players: List<String>) : RedisResponse()
240+
```
241+
242+
#### 2. Create Request Handlers
243+
244+
Handlers receive a `RequestContext` that provides:
245+
- `request`: The incoming request
246+
- `coroutineScope`: Scope for launching coroutines if needed
247+
- `respond(response)`: Method to send the response
248+
249+
```kotlin
250+
import de.slne.redis.request.RequestHandler
251+
import de.slne.redis.request.RequestContext
252+
import kotlinx.coroutines.launch
253+
254+
class MyRequestHandler {
255+
@RequestHandler
256+
fun handlePlayerRequest(context: RequestContext<GetPlayerRequest>) {
257+
// Launch coroutine for async operations
258+
context.coroutineScope.launch {
259+
val players = fetchPlayersFromDatabaseAsync(context.request.minLevel)
260+
context.respond(PlayerListResponse(players))
261+
}
262+
}
263+
}
264+
```
265+
266+
#### 3. Register Handlers and Send Requests
267+
268+
```kotlin
269+
import de.slne.redis.request.RequestResponseBus
270+
import kotlinx.coroutines.runBlocking
271+
272+
fun main() = runBlocking {
273+
val bus = RequestResponseBus("redis://localhost:6379")
274+
275+
// Register handler (this server will respond to requests)
276+
bus.registerRequestHandler(MyRequestHandler())
277+
278+
// Send request and wait for response (with timeout)
279+
try {
280+
val response = bus.sendRequest<PlayerListResponse>(
281+
GetPlayerRequest(minLevel = 10),
282+
timeoutMs = 3000
283+
)
284+
println("Received ${response.players.size} players")
285+
} catch (e: RequestTimeoutException) {
286+
println("Request timed out: ${e.message}")
287+
}
288+
289+
bus.close()
290+
}
291+
```
292+
293+
### Request-Response vs Events
294+
295+
**Use Request-Response when:**
296+
- You need a reply/acknowledgment
297+
- You need data back from another server
298+
- You need to know if the operation succeeded
299+
- You want timeout handling
300+
301+
**Use Events when:**
302+
- You want to broadcast information
303+
- You don't need a reply
304+
- Multiple servers should react to the same event
305+
- Fire-and-forget pattern is acceptable
306+
307+
### Example Scenario
308+
309+
**ServerA** (Lobby Server) and **ServerB** (Game Server):
310+
311+
```kotlin
312+
// ServerA can handle requests
313+
class LobbyHandler {
314+
@RequestHandler
315+
fun getServerStatus(context: RequestContext<ServerStatusRequest>) {
316+
// Respond using coroutine scope
317+
context.coroutineScope.launch {
318+
context.respond(ServerStatusResponse("Lobby-1", online = true, playerCount = 42))
319+
}
320+
}
321+
}
322+
323+
// ServerB can also send requests to ServerA
324+
val response = bus.sendRequest<ServerStatusResponse>(
325+
ServerStatusRequest("Lobby-1"),
326+
timeoutMs = 3000
327+
)
328+
```
329+
330+
Both servers can simultaneously:
331+
- Send requests to other servers
332+
- Respond to requests from other servers
333+
172334
## Example
173335

174336
See the `de.slne.redis.example` package for complete examples:
175337
- `ExampleEvents.kt` - Example event definitions
176338
- `ExampleUsage.kt` - Example usage demonstrating async publishing and subscribing
339+
- `ExampleRequests.kt` - Example request/response definitions
340+
- `ExampleRequestResponseUsage.kt` - Example usage demonstrating request-response pattern
177341

178342
## API Reference
179343

@@ -202,6 +366,46 @@ Main class for managing events with async support.
202366
- `fun unregisterListener(listener: Any)` - Unregister a listener and all its handlers
203367
- `fun close()` - Close connections and clean up resources
204368

369+
### RedisRequest
370+
371+
Base class for all requests. Extend this to create custom requests. Must be annotated with `@Serializable`.
372+
373+
### RedisResponse
374+
375+
Base class for all responses. Extend this to create custom responses. Must be annotated with `@Serializable`.
376+
377+
### @RequestHandler
378+
379+
Annotation for marking request handler methods. Methods must:
380+
- Have exactly one parameter of type `RequestContext<TRequest>`
381+
- Return void (Unit)
382+
- Handler controls when/how to respond using `context.respond()`
383+
384+
### RequestContext<TRequest>
385+
386+
Context object provided to request handlers containing:
387+
- `request: TRequest` - The incoming request
388+
- `coroutineScope: CoroutineScope` - Scope for launching coroutines if needed
389+
- `suspend fun respond(response: RedisResponse)` - Send the response
390+
391+
### RequestResponseBus
392+
393+
Main class for managing request-response patterns with async support and timeout handling.
394+
395+
**Constructor:**
396+
- `RequestResponseBus(redisUri: String, coroutineScope: CoroutineScope = ...)` - Create a new request-response bus connected to Redis
397+
398+
**Methods:**
399+
- `suspend fun <T : RedisResponse> sendRequest(request: RedisRequest, timeoutMs: Long = 3000): T` - Send a request and wait for response asynchronously
400+
- `fun <T : RedisResponse> sendRequestBlocking(request: RedisRequest, timeoutMs: Long = 3000): T` - Send a request and wait for response synchronously (blocking)
401+
- `fun registerRequestHandler(handler: Any)` - Register an object with @RequestHandler methods
402+
- `fun unregisterRequestHandler(handler: Any)` - Unregister a handler and all its request handlers
403+
- `fun close()` - Close connections and clean up resources
404+
405+
### RequestTimeoutException
406+
407+
Exception thrown when a request times out without receiving a response.
408+
205409
## License
206410

207411
This project is open source and available under the MIT License.

build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ dependencies {
2626
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
2727
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3")
2828

29+
// Fastutil for high-performance collections
30+
implementation("it.unimi.dsi:fastutil:8.5.12")
31+
2932
// Test dependencies
3033
testImplementation(kotlin("test"))
3134
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package de.slne.redis.example.request
2+
3+
import de.slne.redis.request.RequestContext
4+
import de.slne.redis.request.RequestHandler
5+
import de.slne.redis.request.RequestResponseBus
6+
import kotlinx.coroutines.delay
7+
import kotlinx.coroutines.launch
8+
import kotlinx.coroutines.runBlocking
9+
10+
/**
11+
* Example request handler demonstrating how to handle requests and send responses.
12+
*/
13+
class ExampleRequestHandler {
14+
15+
@RequestHandler
16+
fun handlePlayerRequest(context: RequestContext<GetPlayerRequest>) {
17+
// Launch coroutine to respond asynchronously (if needed)
18+
context.coroutineScope.launch {
19+
// Simulate some async work (e.g., database query)
20+
delay(100)
21+
22+
// Filter players based on minimum level
23+
val allPlayers = listOf("Steve", "Alex", "Notch", "Herobrine", "Jeb")
24+
val filteredPlayers = if (context.request.minLevel > 5) {
25+
allPlayers.take(2) // Only high-level players
26+
} else {
27+
allPlayers
28+
}
29+
30+
context.respond(PlayerListResponse(filteredPlayers))
31+
}
32+
}
33+
34+
@RequestHandler
35+
fun handleServerStatus(context: RequestContext<ServerStatusRequest>) {
36+
// Respond synchronously by launching in runBlocking or use coroutineScope.launch for async
37+
context.coroutineScope.launch {
38+
// Simulate checking server status
39+
delay(50)
40+
41+
context.respond(ServerStatusResponse(
42+
serverName = context.request.serverName,
43+
online = true,
44+
playerCount = 42
45+
))
46+
}
47+
}
48+
}
49+
50+
/**
51+
* Example usage of the RequestResponseBus demonstrating request/response pattern
52+
*/
53+
fun main() = runBlocking {
54+
// Create request-response bus with Redis connection URI
55+
val requestResponseBus = RequestResponseBus("redis://localhost:6379")
56+
57+
// Register a request handler (this is the server that responds to requests)
58+
val handler = ExampleRequestHandler()
59+
requestResponseBus.registerRequestHandler(handler)
60+
61+
println("Server ready - listening for requests...")
62+
63+
// Simulate a client sending requests after a short delay
64+
delay(500)
65+
66+
try {
67+
// Send a request and wait for response
68+
println("Sending GetPlayerRequest with minLevel=5...")
69+
val response1 = requestResponseBus.sendRequest<PlayerListResponse>(
70+
GetPlayerRequest(minLevel = 5),
71+
timeoutMs = 3000
72+
)
73+
println("Received response: ${response1.players}")
74+
75+
// Send another request
76+
println("\nSending ServerStatusRequest...")
77+
val response2 = requestResponseBus.sendRequest<ServerStatusResponse>(
78+
ServerStatusRequest("Lobby-1"),
79+
timeoutMs = 3000
80+
)
81+
println("Server status: ${response2.serverName} - Online: ${response2.online}, Players: ${response2.playerCount}")
82+
83+
} catch (e: Exception) {
84+
println("Error: ${e.message}")
85+
e.printStackTrace()
86+
}
87+
88+
// Keep the application running to handle more requests
89+
println("\nPress Ctrl+C to exit")
90+
91+
Runtime.getRuntime().addShutdownHook(Thread {
92+
println("Shutting down...")
93+
requestResponseBus.close()
94+
})
95+
96+
// Wait indefinitely
97+
Thread.currentThread().join()
98+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package de.slne.redis.example.request
2+
3+
import de.slne.redis.request.RedisRequest
4+
import de.slne.redis.request.RedisResponse
5+
import kotlinx.serialization.Serializable
6+
7+
/**
8+
* Example request: Get players with a minimum level
9+
*/
10+
@Serializable
11+
data class GetPlayerRequest(val minLevel: Int) : RedisRequest()
12+
13+
/**
14+
* Example response: List of player names
15+
*/
16+
@Serializable
17+
data class PlayerListResponse(val players: List<String>) : RedisResponse()
18+
19+
/**
20+
* Example request: Get server status
21+
*/
22+
@Serializable
23+
data class ServerStatusRequest(val serverName: String) : RedisRequest()
24+
25+
/**
26+
* Example response: Server status information
27+
*/
28+
@Serializable
29+
data class ServerStatusResponse(
30+
val serverName: String,
31+
val online: Boolean,
32+
val playerCount: Int
33+
) : RedisResponse()

0 commit comments

Comments
 (0)