IntelliJ Platform Plugin SDK Help

Coroutine Scopes

Kotlin's coroutines follow the principle of structured concurrency. It means that each coroutine is run in a specific CoroutineScope, which delimits the lifetime of the coroutine. This ensures that they are not lost and do not leak. An outer scope does not complete until all its child coroutines are completed. Cancellation of the outer scope also cancels its child coroutines. Structured concurrency ensures that any errors in the code are properly reported and never lost.

IntelliJ Platform Scopes

IntelliJ Platform provides special coroutine scopes that help ensure proper structured concurrency of coroutines run from the platform or plugin code. After cancellation, the platform awaits the completion of each scope. Using correct parent scopes guarantees that child coroutines will be properly canceled when no longer needed, preventing resource leaks.

The following diagram presents the scopes and their parent-child relationships:

IntelliJ Platform Coroutine Scopes

All scopes presented on the diagram are supervisor scopes — they ignore the failures of their children.

Each coroutine scope can have only one actual parent, pointed with solid arrow lines. Dashed arrow lines point to fictional parents, which follow the actual coroutine parent-child semantics:

  • a parent scope cancels children on its own cancellation

  • a parent scope awaits children before considering itself complete

  • a failed child cancels its parent (which effectively is not happening because presented scopes are supervisors)

The Application×Plugin and Project×Plugin are intersection scopes with two semantic parents (actual and fictional).

Main Scopes

  • Root - the root scope spans all the coroutines. This is the standard root scope launched with runBlocking coroutine builder.

  • Application - a scope associated with the Application container (component manager) lifetime. It is canceled on application shutdown. This triggers cancellation of the Application×Plugin scope and, subsequently, its children, including the Project×Plugin scope.

  • Project - a scope associated with a Project container (component manager) lifetime. It is canceled when a project is being closed. This triggers the cancellation of the Project×Plugin scope and, subsequently, its children.

  • Plugin - a scope associated with a plugin lifetime. It is canceled on unloading of the corresponding plugin. This triggers cancellation of the Application×Plugin scope and, subsequently, its children, including the Project×Plugin scope.

Intersection Scopes

  • Application×Plugin - a scope which is an intersection of the Application and Plugin scopes. It is canceled when the application is shutdown or the corresponding plugin is unloaded. This triggers the cancellation of its children and the Project×Plugin scope and, subsequently, its children.

  • Project×Plugin - a scope which is an intersection of the Project and Plugin scopes. It is canceled when a project is being closed or the corresponding plugin is unloaded.

Intersection scopes enable creating coroutines whose lifetime is limited by application/project and plugin lifetimes, e.g., application/project services provided by a plugin.

Service Scopes

The Application Service and Project Service scopes are bound to an application and project service lifetimes accordingly. They are children of the Intersection Scopes, which means that they are canceled when the application/project is closed or a plugin is unloaded.

The service scope is provided to services via constructor injection. The following constructor signatures are supported:

  • MyService(CoroutineScope) for application and project services

  • MyProjectService(Project, CoroutineScope) for project services

Each service instance receives its own scope instance. The injected scopes' contexts contain Dispatchers.Default and CoroutineName(serviceClass).

See Launching Coroutine From Service Scope for full samples.

Using a Correct Scope

Use Service Scopes

If a plugin requires running some code in a coroutine, the recommended approach is to create a separate service that will receive its own scope via constructor and launch the coroutine in this scope. This approach guarantees the usage of the correct scope, preventing leaks and canceling wrong scopes and killing all their (e.g., application's or project's) coroutines accidentally.

See the Launching Coroutines section for details.

Do Not Use Application/Project Scope

Application and Project scopes are exposed with Application.getCoroutineScope() and Project.getCoroutineScope(). Never use these methods, as they are deprecated and will be removed in the future.

Using these scopes could easily lead to project or plugin class leaks.

  1. Project leak:

    application.coroutineScope.launch { project.getService(PsiDirectoryFactory::class.java) }

    Closing the project cancels its scope. The application scope remains active, and the project is leaked.

    Application Scope Project Scope Project leak Lifetimes
  2. Plugin leak:

    project.coroutineScope.launch { project.getService(MyPluginService::class.java) }

    Unloading of the plugin cancels its scope. The project scope remains active, and the plugin classes are leaked.

    Project Scope Plugin Scope MyPluginService leak Lifetimes

Do Not Use Intersection Scopes

There is no API for retrieving Application×Plugin and Project×Plugin intersection scopes, but let's assume there is a method exposing the Project×Plugin scope:

/** * Returns the correct intersection scope for the project and plugin * by a given plugin class. */ fun Project.getCoroutineScope(pluginClass: Class<*>): CoroutineScope

Using this scope could lead to a plugin leak:

project.getCoroutineScope(PluginBService::class.java).launch { project.getService(PluginAService::class.java) }

Unloading of Plugin A cancels its scope. The scope of Plugin B remains active, and the Plugin A classes are leaked.

Application Scope Plugin A Scope Project Scope Project × Plugin A Scope Plugin B Scope Project × Plugin B Scope PluginAService leak Lifetimes
Last modified: 03 April 2024