本文へ移動
cccskills
無料GitHub で公開日本語紹介

platform-coroutines-structured-concurrency

IntelliJ向けKotlinコルーチンのスコープ所有者やキャンセルの伝わり方を確認し、リークやキャンセル漏れを防ぐ修正方針を示します。

原文原文の説明を見る

Write or review IntelliJ coroutine scope ownership and cancellation.

JetBrains/intellij-communityスター 20,664(GitHub 20,664 + cccskills 0)

実行スコア 未検証

インストール方法を見る

こんなときに便利

  • コルーチンのスコープをレビューしたいとき
  • キャンセル漏れやジョブのリークを調べたいとき
  • Flow収集の停止処理を確認したいとき

日本語での紹介

できること

IntelliJ製品のKotlinコードで、コルーチンのスコープがどのオブジェクトに所有され、いつキャンセルされるかを整理します。親子間のキャンセルや失敗の伝わり方、長時間処理やFlow収集がキャンセルに協調するかを確認し、問題があれば影響度と小さな構造修正を提案します。GlobalScopeや切り離されたスコープ、キャンセル例外の握りつぶし、launchの失敗を外側のtryで捕まえようとするコードなども確認対象です。

こんなときに便利

コルーチンを新しく追加するときや、既存コードのリーク、停止しないループ、キャンセル後の不整合を調べるときに役立ちます。特に、サービスやUIのライフサイクルに処理を結び付けたい開発者に向いています。

使い方の例

  • 「このコルーチンのスコープ所有者とキャンセル時点を確認してください」
  • 「この例外処理でキャンセルが握りつぶされないかレビューしてください」
  • 「このFlow収集処理のキャンセル漏れを調べてください」

注意点

対象はIntelliJベース製品のKotlinコルーチンコードです。UIやEDT(イベントディスパッチスレッド)の所有関係、コルーチン内部の詳細な解説は、関連する別スキルの対象として案内されています。

この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。

含まれるファイル(1)

  • SKILL.md11.3 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

<!-- Generated by community/.ai/render-guides.mjs; edit community/.agents/skills/platform-coroutines-structured-concurrency/SKILL.md -->

Coroutines: structured concurrency

Keep cancellation, failures, and lifetimes predictable in Kotlin coroutine code across any IntelliJ-based product module. Three rules govern every decision:

  1. Hierarchy — which scope owns this coroutine, and when is that scope cancelled.
  2. Propagation — what a cancellation or failure does to parents, siblings, and children.
  3. Don't be creative — prefer standard primitives (coroutineScope, supervisorScope, a lifecycle-bound scope) over hand-rolled scope wiring, the most common source of leaks.

Reach for this skill when changing suspend functions, launch/async, CoroutineScope construction, cancellation handling, or Flow collection — and when diagnosing leaked coroutines, uncancellable loops, swallowed cancellation, GlobalScope, launch-in-init, detached scopes, invokeOnCompletion, ProcessCanceledException, blocking/progress bridges, ModalityState context capture, or promise/callback cancellation bridges.

Related skills: platform-deep-dives (coroutine internals, dispatchers, read/write actions); kotlin-ui-swing-component-architecture (Swing UI and EDT ownership).

Core review loop

  1. Name the owning CoroutineScope and the exact moment it is cancelled (Disposable disposal, service teardown, explicit cancel()). If you cannot name that moment, that is the finding.
  2. Check long-running, CPU-bound, blocking, or looped coroutine bodies (and Flow collectors) for a cooperative cancellation checkpoint — do not flag short bodies that already suspend.
  3. Check every catch on the path for a swallowed CancellationException / ProcessCanceledException.
  4. Check any invokeOnCompletion, GlobalScope, manual CoroutineScope(...), or launch in init against the rules below, then classify (severity table) and propose the smallest structural fix.

Scope ownership and lifecycle

  • Never use GlobalScope for feature work — it is never cancelled, so its coroutines outlive the component that started them.

  • Prefer an injected or platform lifecycle scope (project/service scope, or the scope handed to you). A standalone CoroutineScope(context) with no Job in the context gets a fresh root Job() not linked to any parent — nothing cancels or awaits it, so it leaks:

    // Wrong: detached root scope; cleanup depends on a manual cancel() that is easy to miss.
    val scope = CoroutineScope(someContextElement.asContextElement())
    
    // Right: child of an owning scope; cancelled structurally with its parent.
    val scope = owner.coroutineScope.childScope("MyFeature", someContextElement.asContextElement())
    
  • If a manual scope is unavoidable, either make it a child of an owning scope, or register the owning Disposable (cancel the scope in dispose()) before launching any work, and verify that registration actually runs. Disposal-only cancellation with no parent link is fragile — a missed dispose() leaks everything the scope launched.

  • launch in init {} — depends on what cancels the scope. If the object owns a scope tied to its own Disposer registration, don't launch in init: the coroutine can run (touching a half-built this) before registration completes and leak on failure — expose start() and call it after Disposer.register. A @Service with an injected CoroutineScope is the exception: that scope is cancelled with the service's container (app/project/plugin) by the platform — not via your dispose(), and no Disposable is needed solely to cancel the injected scope — so a service self-starting workers from init is acceptable. Just keep the constructor cheap and don't read not-yet-initialized state.

    // Owned scope, cancelled via your own Disposer registration:
    init { scope.launch { observe() } }          // Wrong — may run before Disposer.register completes
    fun start() { scope.launch { observe() } }   // Right — after Disposer.register(owner, this)
    // @Service(private val scope: CoroutineScope) — self-start from init is fine; scope is platform-managed.
    
  • Prefer the shortest lifecycle that consumes the work; use an app/project service scope only when the result is genuinely owned for that whole lifetime.

  • Do not store per-submission UI context in a long-lived scope. Context elements such as current ModalityState must be captured at the submit site, not at service/object construction. Avoid CoroutineScope(ModalityState.defaultModalityState().asContextElement()); prefer scope.launch(currentModality.asContextElement()) { ... } when modality belongs to that operation. Modality dispatch semantics belong to kotlin-ui-swing-component-architecture / platform-deep-dives.

Job hierarchy and failure propagation

  • Cancelling a parent cancels all children recursively; a child failing with anything other than CancellationException cancels its parent and siblings. SupervisorJob/supervisorScope opts child failure out of cancelling the parent; parent cancellation still cancels supervised children.
  • A parent that finished its block is completing, not done, until every child completes.
  • coroutineScope { } is all-or-nothing (one failure fails the scope and rethrows); supervisorScope { } is only for genuinely independent children.

Cooperative cancellation

  • A loop/CPU-bound body with no suspension point is not cancellable and hangs on cancel(). Add a suspension point (delay, yield), ensureActive(), or checkCanceled() in progress-aware code.
  • Callback APIs: bridge with suspendCancellableCoroutine (not suspendCoroutine) and release the resource in invokeOnCancellation.
  • Blocking APIs: use a blocking-appropriate context (Dispatchers.IO) or, for progress-aware blocking code, coroutineToIndicator { indicator -> ... }. Pair cancellation with an explicit interrupt/close strategy; coroutine cancellation alone does not abort an in-progress blocking call. Do not wrap blocking code in blockingContext: deprecated since 2024.2 because context is installed implicitly (ReplaceWith("action()")).

Exception and failure propagation

  • runCatching { launch { ... } } does not catch the child's failure. The child fails its parent scope out-of-band while launch returns normally, so try/runCatching sees nothing. To isolate a child, use supervisorScope and handle the failure inside the child.

  • The same trap applies to async: try/runCatching around async { } does not contain a later failure — the exception surfaces at await(), and an unsupervised failure may already have cancelled the parent before you await.

  • Never swallow cancellation. runCatching and bare catch (e: Throwable) catch CancellationException too; swallowing it breaks structured concurrency and can hang cancellation. ProcessCanceledException is a CancellationException subtype, so catching cancellation covers it too. Rethrow cancellation first:

    try {
      body()
    } catch (c: CancellationException) {   // also covers ProcessCanceledException (a subtype)
      throw c                              // never swallow cancellation
    } catch (x: Throwable) {
      handle(x)
    }
    
  • When bridging to a non-coroutine promise/callback, settle it before rethrowing cancellation. If a coroutine owns an AsyncPromise or a callback result, cancellation can skip the normal result path and leave external awaiters pending. In catch (c: CancellationException), complete the external primitive (setError(c) / cancel) before throw c. Plain Deferred does not need this — cancellation completes it.

  • Do not report a caught CancellationException as a user-visible error, even if you rethrow it.

Don't cancel yourself

Do not call cancel() on the coroutine you are currently running in — it does not stop execution immediately (only at the next suspension point) and poisons the surrounding scope. Return early or throw CancellationException. A shared suspend fun must never cancel its caller's job.

invokeOnCompletion

Prefer to avoid it. Three failure modes make it a trap:

  1. Runs concurrently, unordered, on an unspecified thread — not guaranteed on the EDT or after your surrounding code. A check-then-act on a shared field races; use AtomicReference.compareAndSet.
  2. Retained until the job completes — registering on a long-lived job (or in a loop) accumulates handlers and leaks. Only register on short-lived jobs that actually finish.
  3. Must not throw — exceptions from handlers are reported through coroutine exception handling/logging, not to the caller waiting for completion. Keep the body to trivial, non-failing bookkeeping.

Preferred alternative — do completion work as the last step inside the coroutine:

scope.launch {
  try { doWork() }
  finally { withContext(NonCancellable + Dispatchers.EDT) { onFinished() } }
}

Use NonCancellable only for small, bounded cleanup that must run even after cancellation — never wrap substantial work in it, and keep UI cleanup lifecycle/disposal-guarded (do not touch a disposed component). Cancelling an already-completed Job is a no-op, so "clear my job handle on completion" bookkeeping is often unnecessary — verify it changes behavior before adding it.

Severity defaults

Adjust for actual impact.

PatternDefaultSmallest fix
GlobalScope for feature workCriticalchild of a lifecycle scope
Detached CoroutineScope never cancelledCriticalchildScope of an owning scope
Swallowed CancellationException/ProcessCanceledExceptionCriticalrethrow cancellation before catch (Throwable)
Non-cooperative infinite/long loopCriticaladd ensureActive()/yield/suspension point
runCatching/try around launch/async to contain failureCriticalsupervisorScope + handle inside child
launch in init with a scope tied to own Disposer registrationMajorstart() after registration
Manual scope where childScope fits; scope not tied to lifecycleMajorinject/childScope; register Disposable before launch
invokeOnCompletion mutating shared state unsynchronized or that can throwMajorcompareAndSet, or clean up in coroutine finally
Self-cancellation (cancel() on current job)Majorreturn / throw CancellationException
Promise/callback bridge left pending on cancellationMajorcomplete/cancel external primitive before rethrow
Unnecessary invokeOnCompletion bookkeepingMinorremove it
blockingContext wrapper (deprecated 2024.2; context now implicit)Minordelete the wrapper
Cancellation reported as a user-visible errorMinorrethrow silently
supervisorScope/SupervisorJob where a plain scope sufficesMinoruse coroutineScope

Further reading

For deep internals see platform-deep-dives (coroutine notebooks: cancellation model, context propagation) and the ultimate coroutine docs docs/IntelliJ-Platform/4_man/Kotlin-Coroutines/, especially 9_Gotchas-and-practices/ and 8_UI-EDT-Dispatchers.md.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

actions

無料概要あり

IntelliJプラグインのAnActionと登録設定を実装・変更し、表示文言やアイコンの定義、非同期処理の起動方法を整えます。

  • IDEに新しいアクションを追加したいとき
  • アクションの登録やアイコンを変更したいとき
  • 表示名と説明文の定義を整理したいとき
JetBrains/intellij-community2.1万2026年10月26日 更新

bazel-test-migration

無料概要あり

IntelliJのJPSテストモジュールをBazelへ移行し、Bazelだけで起きる失敗を依存関係や実行環境から切り分ける手順を案内します。

  • JPSテストをBazelへ移行したいとき
  • Bazelだけで起きるテスト失敗を調べたいとき
  • テストの依存関係やデータ設定を確認したいとき
JetBrains/intellij-community2.1万2026年10月26日 更新

code-style

無料概要あり

IntelliJのKotlin・Javaコードを新規作成やレビューする際に、言語の選び方、書式、型の扱い、例外処理などのプロジェクト規約に沿って整えるスキルです。

  • IntelliJ規約に沿ってKotlinを書きたいとき
  • Javaコードをレビューしたいとき
  • 例外処理やsealed型の分岐を確認したいとき
JetBrains/intellij-community2.1万2026年10月26日 更新

commits

無料概要あり

IntelliJ向けの変更を適切な単位に分け、チケットの有無に応じた形式で、理由や判断も伝わるコミットメッセージを作成・修正するスキルです。

  • チケット付き変更のメッセージを作りたいとき
  • テストやドキュメントのコミットを整理したいとき
  • 既存のコミットメッセージを修正したいとき
JetBrains/intellij-community2.1万2026年10月26日 更新

compare-python-typecheckers

無料概要あり

Pythonコードの型推論や型エラーを複数の型チェッカーで比較し、各ツールの判定と診断内容をまとめたレポートを作成します。

  • 型推論の違いを比較したいとき
  • 型エラーになるか確かめたいとき
  • 複数チェッカーの診断を見比べたいとき
JetBrains/intellij-community2.1万2026年10月26日 更新

compose-ui-architecture

無料概要あり

Compose Desktopの画面で状態や副作用をどこに持たせるか整理し、表示と判断の境界、再利用部品やリストの設計を整えます。

  • 画面とプレゼンターの責務を分けたいとき
  • UI状態の置き場所を整理したいとき
  • Effectやコルーチンの使い方を見直したいとき
JetBrains/intellij-community2.1万2026年10月26日 更新

JetBrains のスキルをすべて見る

このスキルの問題を報告する