Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Graceful shutdown with Structured Concurrency

Graceful shutdown with Structured Concurrency

In the modern world we often need to make strong guarantees about how our applications terminate; Structured Concurrency gives us a powerful tool to reason about concurrency, parallel processes and how they relate to each other.

This talk will cover some of the issues you might encounter when not respecting graceful shutdown, and how we can leverage Structured Concurrency to reason about these issues.

Simon Vergauwen

November 26, 2022
Tweet

More Decks by Simon Vergauwen

Other Decks in Programming

Transcript

  1. Closeable class MyServer : AutoCloseable { init { println("Initialising server")

    } fun routes(): Unit = println("Handling incoming requests") override fun close() = println("Closing server") }
  2. Closeable fun main(): Unit { MyServer().use { it.routes() } }

    Initialising server Handling incoming requests Closing server Process finished with exit code 0
  3. Closeable fun main(): Unit = runBlocking { MyServer().use { it.routes()

    awaitCancellation() } } Initialising server Handling incoming requests Process finished with exit code 143 (interrupted by signal 15: SIGTERM)
  4. Structured Concurrency Structured Concurrency Parent Job Child Job A Child

    Job B Child Job A-1 Child Job A-2 Successful completion Cancellation Failure
  5. SuspendApp fun main(): Unit = SuspendApp { MyServer().use { it.routes()

    awaitCancellation() } } Initialising server Handling incoming requests Closing Server Process finished with exit code 143 (interrupted by signal 15: SIGTERM)
  6. Resource sealed class Resource<A> public fun <A> Resource( acquire: suspend

    () -> A, release: suspend (A, ExitCase) -> Unit, ): Resource<A>
  7. Resource sealed class ExitCase { object Completed : ExitCase() data

    class Cancelled(val exception: CancellationException) : ExitCase() data class Failure(val failure: Throwable) : ExitCase() }
  8. Resource data class Resource<A>( private val acquire: suspend () ->

    A, private val finalizer: suspend (ExitCase, A) -> Unit ) { suspend fun <B> use(action: suspend (A) -> B): A = … }
  9. Resource class MyServer private constructor() { fun routes(): Unit =

    println("Handling incoming requests") suspend fun shutdown(exitCase: ExitCase) = withContext(Dispatchers.IO){ println("Shutting down server with $exitCase") } companion object { fun create(): Resource<MyServer> = Resource( { MyServer().also { println("Initialising server") } }, { server, exitCase -> server.shutdown(exitCase) } ) } }
  10. Resource fun main(): Unit = SuspendApp { MyServer.create().use { it.routes()

    awaitCancellation() } } Initialising server Handling incoming requests Shutting down server with Cancelled(exception=JobCancellationException:--.) Process finished with exit code 143 (interrupted by signal 15: SIGTERM)
  11. Composing Resource fun database(config: HikariConfig): Resource<HikariDataSource> = Resource.fromAutoCloseable { HikariDataSource(config)

    } private val metrics: Resource<PrometheusMeterRegistry> = Resource( acquire = { PrometheusMeterRegistry(PrometheusConfig.DEFAULT) }, release = { p, _: ExitCase -> p.close() } )
  12. Composing Resource fun main(): Unit = SuspendApp { resource {

    val config = loadConfig() val registry: PrometheusMeterRegistry = metrics.bind() val database: HikariDataSource = database(config.hikariConfig).bind() } }
  13. Composing Resource fun main(): Unit = SuspendApp { resource {

    val config = loadConfig() val registry: PrometheusMeterRegistry = metrics.bind() val database: HikariDataSource = database(config.hikariConfig).bind() val server = MyServer.create().bind() server.routes(registry, database) }.use { awaitCancellation() } }
  14. ResourceScope suspend fun ResourceScope.database(config: HikariConfig): HikariDataSource = autoCloseable { HikariDataSource(config)

    } suspend fun ResourceScope.metrics(): PrometheusMeterRegistry = install({ PrometheusMeterRegistry(PrometheusConfig.DEFAULT) }) { p, _: ExitCase -> p.close() }
  15. ResourceScope fun main(): Unit = SuspendApp { resource { val

    config = loadConfig() val registry = metrics() val database = database(config.hikariConfig) val server = MyServer.create() server.routes(registry, database) }.use { awaitCancellation() } }
  16. ResourceScope fun main(): Unit = SuspendApp { resourceScope { val

    config = loadConfig() val registry = metrics() val database = database(config.hikariConfig) val server = MyServer.create() server.routes(registry, database) awaitCancellation() } }