Kotlin websockets stuck in TIME_WAIT - kotlin

While trying to create a large number of websockets I get a java.net.BindException. I think this is because the websockets are not being properly closed.
How do I properly close a websocket in kotlin?
Example code in a single file:
import io.ktor.application.*
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.features.websocket.*
import io.ktor.http.*
import io.ktor.http.cio.websocket.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.websocket.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Semaphore
const val port = 8080
const val WEBSOCKET_ROUTE = "/ws"
const val TOTAL_REQUESTS = 100000
const val CONCURRENT_REQUESTS = 100
fun main(args: Array<String>) = runBlocking {
val embeddedServer = embeddedServer(Netty, port = port, module = modules)
embeddedServer.start()
runClients()
embeddedServer.stop(0, 0)
}
private suspend fun runClients() = coroutineScope {
val client = HttpClient(CIO) {
install(io.ktor.client.features.websocket.WebSockets)
}
val semaphore = Semaphore(CONCURRENT_REQUESTS)
val clientJobs = List(TOTAL_REQUESTS) {
semaphore.acquire()
launch {
makeClientWebSocketRequest(client, it)
semaphore.release()
}
}
clientJobs.joinAll()
}
private suspend fun makeClientWebSocketRequest(client: HttpClient, id: Int) {
client.webSocket(method = HttpMethod.Get, "localhost", port, WEBSOCKET_ROUTE) {
try {
send(Frame.Text("ehlo $id"))
val data = (incoming.receive() as Frame.Text).readText()
println("client: $data")
} finally {
close()
}
}
}
val modules = fun Application.() {
install(io.ktor.websocket.WebSockets)
routing {
webSocket(WEBSOCKET_ROUTE) {
handleWebSocketServer(this)
}
}
}
private suspend fun handleWebSocketServer(websocket: DefaultWebSocketServerSession) {
try {
val data = (websocket.incoming.receive() as Frame.Text).readText()
println("server: $data")
websocket.outgoing.send(Frame.Text(data.reversed()))
} finally {
websocket.close()
}
}
Exception:
Exception in thread "main" java.net.BindException: Address already in use: no further information
netstat output:
PS C:\Users\partkyle> netstat
Active Connections
Proto Local Address Foreign Address State
TCP 127.0.0.1:8080 kubernetes:55278 TIME_WAIT
TCP 127.0.0.1:49152 kubernetes:8080 TIME_WAIT
TCP 127.0.0.1:49153 kubernetes:8080 TIME_WAIT
TCP 127.0.0.1:49154 kubernetes:8080 TIME_WAIT
TCP 127.0.0.1:49155 kubernetes:8080 TIME_WAIT
TCP 127.0.0.1:49156 kubernetes:8080 TIME_WAIT
TCP 127.0.0.1:49157 kubernetes:8080 TIME_WAIT
......

Related

Ktor: Mock Principal

I have following route:
get("/user") {
val principal: UserIdPrincipal = call.principal()
?: return#get call.respond(HttpStatusCode.Unauthorized)
val user = userService.findUserForId(principal.name.toLong())
?: return#get call.respond(HttpStatusCode.Unauthorized)
val userResponse = UserResponse(username = user.username)
call.respond(userResponse)
}
I don't want to test if authentication works for every single of my routes, so I would like to mock call.principal(). Since it is an inline extension function, it cannot be easily mocked. Any ideas how to solve this problem?
If you can change the configuration for the Authentication plugin in a test then you can register a provider, intercept its pipeline in the AuthenticationPipeline.RequestAuthentication phase, to get an access to authentication context, and finally assign new principal. Here is an example:
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
fun main() {
val server = embeddedServer(Netty, port = 8080) {
install(Authentication) {
provider {
pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->
context.principal(UserIdPrincipal("principal"))
}
}
}
routing {
authenticate {
get("/user") {
val principal: UserIdPrincipal = call.principal()!!
call.respond(principal.name)
}
}
}
}
server.start(wait = true)
}

KTOR - Unzip file in POST routing

I want to unzip a file zip sent within the body of a http query (content type: application/x-gzip) in Ktor (bloc rounting).
I've tried this:
val zip_received=call.receiveStream()
val incomingContent = GZIPInputStream(zip_received).toByteReadChannel()
But I got this error:
java.lang.IllegalStateException: Acquiring blocking primitives on this dispatcher is not allowed. Consider using async channel or doing withContext(Dispatchers.IO) { call.receive().use { ... } } instead.
I'm not able to write such function.
Can I have some helps ?
Thanks
You can use the following code to read GZip uncompressed request body as string:
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.application.*
import io.ktor.request.*
import io.ktor.routing.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.util.zip.GZIPInputStream
fun main(args: Array<String>) {
embeddedServer(Netty, port = 9090) {
routing {
post("/") {
withContext(Dispatchers.IO) {
call.receive<InputStream>().use { stream ->
val gzipStream = GZIPInputStream(stream)
val uncompressedBody = String(gzipStream.readAllBytes())
println(uncompressedBody)
}
}
}
}
}.start()
}

Ktor websocket client connected with more than one server

So I am creating a distributed Key-Value datastore and have a broker to manage/query.
Here is my client:
val client = HttpClient(CIO) {
install(WebSockets)
}
runBlocking {
client.ws(
method = HttpMethod.Get,
host = ip,
port = port,
path = "/thepath"
) {
...
}
}
client.close()
So far I can connect only one server to the client (Obviously by the code above).
What I tried is to create an array of all the available servers and randomly pick one and work with the broker (client). But this works only for the connected server and the others should wait until the connection is closed.
val clients: Array<HttpClient?> = arrayOfNulls(replicationFactor)
for (i in 0 until replicationFactor) {
clients[i] = HttpClient(CIO) {
install(WebSockets)
}
}
runBlocking {
clients[0]?.ws(
method = HttpMethod.Get,
host = "some ip",
port = the_port,
path = "/thepath"
) {
....
}
...
...
}
Any ideas of how to tackle this problem? Maybe I can keep the connection with each server on a separate thread.
You can create any number of HTTP clients and connect them to a server concurrently. Here is an example:
suspend fun main() {
val clients = (0 until 3).map {
HttpClient(CIO) {
install(WebSockets)
}
}
val connections = coroutineScope {
clients.mapIndexed { index, client ->
async {
client.ws("wss://echo.websocket.org") {
outgoing.send(Frame.Text("Hello server"))
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
println("[$index] Server replied ${frame.readText()}")
}
}
}
}
}.toTypedArray()
}
awaitAll(*connections)
}

How to debug org.apache.sshd.common.scp.ScpException: Received nack: Can not write to error?

I am writing a tool for testing the SSH connection with basic functionality: connect, upload a file, download a file.
My SftpTestServer implementation:
import org.apache.sshd.client.SshClient
import org.apache.sshd.client.scp.ScpClientCreator
import org.apache.sshd.client.session.ClientSession
import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory
import org.apache.sshd.server.SshServer
import org.apache.sshd.server.auth.password.PasswordAuthenticator
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.apache.sshd.server.scp.ScpCommandFactory
import org.apache.sshd.server.session.ServerSession
import org.apache.sshd.server.subsystem.SubsystemFactory
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory
import java.io.File
import java.nio.file.Paths
import java.util.*
class SftpTestServer(
private val host: String,
private val port: Int,
private val rootDir: File,
private val userName: String,
private val password: String
) : TestServer {
private val server: SshServer
private val client: SshClient
private fun session(): ClientSession {
val session = client.connect(userName, server.host, server.port).verify().session
session.addPasswordIdentity(password)
session.auth().verify().isSuccess
return session
}
override fun start() {
server.start()
client.start()
}
override fun stop() {
client.stop()
server.stop()
}
private fun setupServer(host: String, port: Int, rootDir: File, userName: String, pwd: String): SshServer {
val sshd = SshServer.setUpDefaultServer()
sshd.keyPairProvider = SimpleGeneratorHostKeyProvider(File(rootDir, "sshd_key.ser").toPath())
sshd.fileSystemFactory = VirtualFileSystemFactory(Paths.get(rootDir.absolutePath))
sshd.port = port
sshd.host = host
sshd.passwordAuthenticator = PasswordAuthenticator { username: String, password: String, _: ServerSession? -> username == userName && password == pwd }
sshd.commandFactory = ScpCommandFactory()
val factoryList: MutableList<SubsystemFactory> = ArrayList()
factoryList.add(SftpSubsystemFactory())
sshd.subsystemFactories = factoryList
return sshd
}
fun upload(file: File) {
val creator = ScpClientCreator.instance()
val scpClient = creator.createScpClient(session())
scpClient.upload(Paths.get(file.absolutePath), rootDir.absolutePath)
}
fun listFiles(): List<String> {
return listOf()
}
init {
server = setupServer(host, port, rootDir, userName, password)
client = setupClient()
}
private fun setupClient(): SshClient {
return SshClient.setUpDefaultClient()
}
}
My SftpTestServerTest implementation:
import org.apache.commons.io.FileUtils
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.*
import java.io.File
#Tag("servers")
class SftpTestServerTest {
companion object {
private const val HOST: String = "localhost"
private const val PORT: Int = 23456
private val TEST_FILE_RESOURCE: File = File("target/test-folder")
private const val USERNAME: String = "username"
private const val PASSWORD: String = "password"
}
private val server = SftpTestServer(HOST, PORT, TEST_FILE_RESOURCE, USERNAME, PASSWORD)
#BeforeEach
fun setUp() {
server.start()
FileUtils.deleteDirectory(TEST_FILE_RESOURCE)
FileUtils.forceMkdir(TEST_FILE_RESOURCE)
}
#AfterEach
fun tearDown() {
server.stop()
}
#Test
fun `should upload file from file path`() {
val fileName = "src/test/resources/ssh_test_file.xml"
val file = File(fileName)
server.upload(file)
assertThat(server.listFiles()).contains(fileName)
}
}
Surprisingly, there is no much info on the web I could find about errors I get from org.apache.sshd.
org.apache.sshd.common.scp.ScpException: Received nack: Can not write to
How can I debug this error?
NOTE: I am using Mac. The issue shouldn't be OS-specific, though.
The issue was that I connected to the wrong path inside the SSH server with the SSH client.
Therefore, my recommendations to debug this issue:
Check your permissions reading/writing to file system
Connect to the SSH server, check folder structure
Based on the previous steps, fix issues in SSH Client connection

How can I make sure all Kotlin coroutines created by a ktor websocket client are cleared up?

I'm trying to wrap my head around Kotlin coroutines and Ktors websocket support. My understanding is that runBlocking will create a scope and that it will block as long as there are coroutines living inside that scope (or child scopes), but I when the call to runBlocking in the test below returns there are still two coroutines alive..
Why am I leaking coroutines here?
package dummy
import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.WebSockets
import io.ktor.client.features.websocket.wss
import io.ktor.http.HttpMethod
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readBytes
import io.ktor.http.cio.websocket.readText
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.*
import kotlinx.coroutines.debug.DebugProbes
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
#ExperimentalCoroutinesApi
#KtorExperimentalAPI
class WebsocketTest {
#Test
fun tidy() {
DebugProbes.install()
runBlocking {
val socketJob = Job()
launch(CoroutineName("Websocket") + socketJob) {
println("Connecting to websocket")
connectWebsocket(socketJob)
println("Websocket dead?")
}
launch(CoroutineName("Ninja socket killer")) {
delay(3500)
println("Killing websocket client")
socketJob.cancel(message = "Time to die..")
}
}
println("\n\n-------")
DebugProbes.dumpCoroutines(System.err)
Assertions.assertEquals(0, DebugProbes.dumpCoroutinesInfo().size, "It would be nice if all coroutines had been cleared up by now..")
}
}
#KtorExperimentalAPI
private suspend fun connectWebsocket(socketJob: CompletableJob) {
val client = HttpClient {
install(WebSockets)
}
socketJob.invokeOnCompletion {
println("Shutting down ktor http client")
client.close()
}
client.wss(
method = HttpMethod.Get,
host = "echo.websocket.org",
port = 443,
path = "/"
) {
send(Frame.Text("Hello World"))
for (frame in incoming) {
when (frame) {
is Frame.Text -> println(frame.readText())
is Frame.Binary -> println(frame.readBytes())
}
delay(1000)
send(Frame.Text("Hello World"))
}
}
}
build.gradle.kts
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
plugins {
kotlin("jvm") version "1.3.41" apply true
}
repositories {
mavenCentral()
}
val ktorVersion = "1.2.3"
val junitVersion = "5.5.1"
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation("io.ktor:ktor-client-websockets:$ktorVersion")
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.3.0-RC2")
testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
}
tasks.withType<Test> {
useJUnitPlatform()
testLogging {
showExceptions = true
showStackTraces = true
exceptionFormat = TestExceptionFormat.FULL
events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
}
}
Seems like I have figured it out (obviously just after ripping my hair out long enough to make this post in the first place). When I wrote the post I leaked two coroutines and one of them "solved itself" (I'm not very happy about that, but what ever I do I can't reproduce it).
The second coroutine leaked because Nonce.kt from Ktor explicitly launches a coroutine in GlobalScope.
https://github.com/ktorio/ktor/blob/master/ktor-utils/jvm/src/io/ktor/util/Nonce.kt#L30
private val nonceGeneratorJob =
GlobalScope.launch(
context = Dispatchers.IO + NonCancellable + NonceGeneratorCoroutineName,
start = CoroutineStart.LAZY
) { ....