@@ -8,6 +8,7 @@ package kotlinx.rpc.krpc.ktor
8
8
9
9
import io.ktor.client.*
10
10
import io.ktor.client.engine.cio.*
11
+ import io.ktor.client.plugins.HttpRequestRetry
11
12
import io.ktor.client.request.*
12
13
import io.ktor.client.statement.*
13
14
import io.ktor.server.application.*
@@ -17,9 +18,7 @@ import io.ktor.server.response.*
17
18
import io.ktor.server.routing.*
18
19
import io.ktor.server.testing.*
19
20
import kotlinx.coroutines.*
20
- import kotlinx.coroutines.debug.DebugProbes
21
21
import kotlinx.rpc.annotations.Rpc
22
- import kotlinx.rpc.krpc.client.KrpcClient
23
22
import kotlinx.rpc.krpc.internal.logging.RpcInternalCommonLogger
24
23
import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLoggerContainer
25
24
import kotlinx.rpc.krpc.internal.logging.dumpLogger
@@ -32,12 +31,12 @@ import kotlinx.rpc.krpc.serialization.json.json
32
31
import kotlinx.rpc.test.runTestWithCoroutinesProbes
33
32
import kotlinx.rpc.withService
34
33
import org.junit.Assert.assertEquals
34
+ import org.junit.Assert.assertTrue
35
35
import java.net.ServerSocket
36
36
import java.util.concurrent.Executors
37
- import java.util.concurrent.TimeUnit
38
37
import kotlin.coroutines.cancellation.CancellationException
39
- import kotlin.test.Ignore
40
38
import kotlin.test.Test
39
+ import kotlin.test.fail
41
40
import kotlin.time.Duration.Companion.seconds
42
41
43
42
@Rpc
@@ -62,13 +61,14 @@ interface SlowService {
62
61
63
62
class SlowServiceImpl : SlowService {
64
63
val received = CompletableDeferred <Unit >()
64
+ val fence = CompletableDeferred <Unit >()
65
65
66
66
override suspend fun verySlow (): String {
67
67
received.complete(Unit )
68
68
69
- delay( Int . MAX_VALUE .toLong() )
69
+ fence.await( )
70
70
71
- error( " Must not be called " )
71
+ return " hello "
72
72
}
73
73
}
74
74
@@ -134,10 +134,7 @@ class KtorTransportTest {
134
134
135
135
@OptIn(DelicateCoroutinesApi ::class , ExperimentalCoroutinesApi ::class )
136
136
@Test
137
- @Ignore(" Wait for Ktor fix (https://github.com/ktorio/ktor/pull/4927) or apply workaround if rejected" )
138
- fun testEndpointsTerminateWhenWsDoes () = runTestWithCoroutinesProbes(timeout = 15 .seconds) {
139
- DebugProbes .install()
140
-
137
+ fun testClientTerminatesWhenServerWsDoes () = runTestWithCoroutinesProbes(timeout = 60 .seconds) {
141
138
val logger = setupLogger()
142
139
143
140
val port: Int = findFreePort()
@@ -147,7 +144,7 @@ class KtorTransportTest {
147
144
val serverReady = CompletableDeferred <Unit >()
148
145
val dropServer = CompletableDeferred <Unit >()
149
146
150
- val service = SlowServiceImpl ()
147
+ val impl = SlowServiceImpl ()
151
148
152
149
@Suppress(" detekt.GlobalCoroutineUsage" )
153
150
val serverJob = GlobalScope .launch(CoroutineName (" server" )) {
@@ -171,22 +168,27 @@ class KtorTransportTest {
171
168
}
172
169
}
173
170
174
- registerService<SlowService > { service }
171
+ registerService<SlowService > { impl }
175
172
}
176
173
}
177
- }.start (wait = false )
174
+ }.startSuspend (wait = false )
178
175
179
176
serverReady.complete(Unit )
180
177
181
178
dropServer.await()
182
179
183
- server.stop(shutdownGracePeriod = 100L , shutdownTimeout = 100L , timeUnit = TimeUnit . MILLISECONDS )
180
+ server.stopSuspend(gracePeriodMillis = 100L , timeoutMillis = 300L )
184
181
}
185
182
186
183
logger.info { " Server stopped" }
187
184
}
188
185
189
186
val ktorClient = HttpClient (CIO ) {
187
+ install(HttpRequestRetry ) {
188
+ retryOnServerErrors(maxRetries = 5 )
189
+ exponentialDelay()
190
+ }
191
+
190
192
installKrpc {
191
193
serialization {
192
194
json()
@@ -200,32 +202,151 @@ class KtorTransportTest {
200
202
201
203
val rpcClient = ktorClient.rpc(" ws://0.0.0.0:$port /rpc" )
202
204
203
- launch {
205
+ var cancellationExceptionCaught = false
206
+ val job = launch {
204
207
try {
205
208
rpcClient.withService<SlowService >().verySlow()
206
- error (" Must not be called" )
209
+ fail (" Must not be called" )
207
210
} catch (_: CancellationException ) {
208
- logger.info { " Cancellation exception caught for RPC request " }
211
+ cancellationExceptionCaught = true
209
212
ensureActive()
210
213
}
211
214
}
212
215
213
- service .received.await()
216
+ impl .received.await()
214
217
215
218
logger.info { " Received RPC request" }
216
219
217
220
dropServer.complete(Unit )
218
221
219
222
logger.info { " Waiting for RPC client to complete" }
220
223
221
- (rpcClient as KrpcClient ).awaitCompletion()
224
+ rpcClient.awaitCompletion()
225
+
226
+ job.join()
227
+
228
+ assertTrue(cancellationExceptionCaught)
222
229
223
230
logger.info { " RPC client completed" }
224
231
225
232
ktorClient.close()
233
+
234
+ serverJob.join()
226
235
newPool.close()
236
+ }
237
+
238
+ @OptIn(DelicateCoroutinesApi ::class , ExperimentalCoroutinesApi ::class )
239
+ @Test
240
+ fun testServerTerminatesWhenClientWsDoes () = runTestWithCoroutinesProbes(timeout = 60 .seconds) {
241
+ val logger = setupLogger()
242
+
243
+ val port: Int = findFreePort()
244
+
245
+ val newPool = Executors .newCachedThreadPool().asCoroutineDispatcher()
246
+
247
+ val serverReady = CompletableDeferred <Unit >()
248
+ val dropServer = CompletableDeferred <Unit >()
227
249
228
- serverJob.cancel()
250
+ val impl = SlowServiceImpl ()
251
+ val sessionFinished = CompletableDeferred <Unit >()
252
+
253
+ @Suppress(" detekt.GlobalCoroutineUsage" )
254
+ val serverJob = GlobalScope .launch(CoroutineName (" server" )) {
255
+ withContext(newPool) {
256
+ val server = embeddedServer(
257
+ factory = Netty ,
258
+ port = port,
259
+ parentCoroutineContext = newPool,
260
+ ) {
261
+ install(Krpc )
262
+
263
+ routing {
264
+ get {
265
+ call.respondText(" hello" )
266
+ }
267
+
268
+ rpc(" /rpc" ) {
269
+ coroutineContext.job.invokeOnCompletion {
270
+ sessionFinished.complete(Unit )
271
+ }
272
+
273
+ rpcConfig {
274
+ serialization {
275
+ json()
276
+ }
277
+ }
278
+
279
+ registerService<SlowService > { impl }
280
+ }
281
+ }
282
+ }.startSuspend(wait = false )
283
+
284
+ serverReady.complete(Unit )
285
+
286
+ dropServer.await()
287
+
288
+ server.stopSuspend(gracePeriodMillis = 100L , timeoutMillis = 300L )
289
+ }
290
+
291
+ logger.info { " Server stopped" }
292
+ }
293
+
294
+ val ktorClient = HttpClient (CIO ) {
295
+ install(HttpRequestRetry ) {
296
+ retryOnServerErrors(maxRetries = 5 )
297
+ exponentialDelay()
298
+ }
299
+
300
+ installKrpc {
301
+ serialization {
302
+ json()
303
+ }
304
+ }
305
+ }
306
+
307
+ serverReady.await()
308
+
309
+ assertEquals(" hello" , ktorClient.get(" http://0.0.0.0:$port " ).bodyAsText())
310
+
311
+ val rpcClient = ktorClient.rpc(" ws://0.0.0.0:$port /rpc" )
312
+
313
+ var cancellationExceptionCaught = false
314
+ val job = launch {
315
+ try {
316
+ rpcClient.withService<SlowService >().verySlow()
317
+ fail(" Must not be called" )
318
+ } catch (_: CancellationException ) {
319
+ cancellationExceptionCaught = true
320
+ ensureActive()
321
+ }
322
+ }
323
+
324
+ impl.received.await()
325
+
326
+ logger.info { " Received RPC request, Dropping client" }
327
+
328
+ ktorClient.cancel()
329
+
330
+ logger.info { " Waiting for RPC client to complete" }
331
+
332
+ rpcClient.awaitCompletion()
333
+
334
+ logger.info { " Waiting for request to complete" }
335
+
336
+ job.join()
337
+
338
+ assertTrue(cancellationExceptionCaught)
339
+
340
+ logger.info { " RPC client and request completed" }
341
+
342
+ sessionFinished.await()
343
+
344
+ logger.info { " Session finished" }
345
+
346
+ dropServer.complete(Unit )
347
+ serverJob.join()
348
+
349
+ newPool.close()
229
350
}
230
351
231
352
private fun findFreePort (): Int {
0 commit comments