0%
September 23, 2024

Run Blocking Servlet Requests to Achieve Non-Blocking Performance

kotlin

springboot

The DeferredResult Trick

class SomeController() {
    @PostMapping("/create-customer-portal-session")
    fun createCustomerPortalSession(
        @RequestBody someDto: SomeDto,
    ): DeferredResult<Success<SomeResult>> {
        val deferredSessionURL = someApplicationService.createCustomerPortalSession(
            someDto
        )
        return deferredSessionURL
    }
}
  • When we return DeferredResult, Spring understands that the result is not immediately available. It doesn't block the servlet thread but instead releases it back to the thread pool.

  • Spring sets up an asynchronous context to handle the DeferredResult. It doesn't actively wait for the result but is prepared to process it when it becomes available.

  • When setResult() is called on the DeferredResult (within a coroutine), Spring is notified that the result is ready.

  • The thread that calls setResult() notifies Spring's asynchronous request handling mechanism.

  • Spring then uses one of its own servlet container threads to process the result and send it back to the client.

What Happens in Application Service?

We initiate DeferredResult(timeout in Long) as a placeholder, we later setResult in a coroutine scope asynchronously:

1class SomeApplicationService() {
2    fun createCustomerPortalSession(
3        someDto: SomeDto,
4    ): DeferredResult<Success<SomeResult>> {
5        val deferredSessionURL = DeferredResult<Success<SomeResult>>(10000L)
6        val scope = CoroutineScope(Dispatchers.IO)
7        scope.launch {
8            try {
9                val (customerId) = someDto
10                val params = SessionCreateParams.builder()
11                    .setCustomer(customerId)
12                    .setReturnUrl(managePlanURL)
13                    .build()
14                val session = Session.create(params)
15                deferredSessionURL.setResult(Success(SomeResult(session.url)))
16            } catch (err: Exception) {
17                deferredSessionURL.setErrorResult(err)
18            } finally {
19                scope.cancel()
20            }
21        }
22        return deferredSessionURL
23    }
24}

Customer DSL to Simplify the Logic Via Trailing Closure

We further simplify the previous code block via the following util:

@Component
class DeferUtil {
    fun <T> defer(block: suspend () -> T): DeferredResult<T> {
        val deferredResult = DeferredResult<T>(20000L)
        val scope = CoroutineScope(Dispatchers.IO)
        scope.launch {
            try {
                val result = block()
                deferredResult.setResult(result)
            } catch (err: Exception) {
                deferredResult.setErrorResult(err)
            } finally {
                scope.cancel()
            }
        }
        return deferredResult
    }
}

Now our SomeApplicationService becomes

class SomeApplicationService(private val deferUtil: DeferUtil) {
    fun createCustomerPortalSession(someDto: SomeDto): DeferredResult<Success<SomeResult>> {
        val deferred = deferUtil.defer {
            val (customerId) = someDto
            val params = SessionCreateParams.builder()
                    .setCustomer(customerId)
                    .setReturnUrl(managePlanURL)
                    .build()
            val session = Session.create(params)
            Success(result=SomeResult(session.url))
        }
        return deferred
    }
}