Plugins own their operation context
Each plugin creates and cancels its own context.WithCancel for terraform operations. The app signals cancellation via sdk.Cancellable on navigation away, but never holds a reference to the context itself. This prevents orphaned terraform subprocesses from cascading lock errors when users navigate freely during long operations.
Consequences
- Plugins that start async terraform operations must implement
sdk.Cancellable. The compliance suite (Rules 5-7) enforces this at test time. - Every async operation must cancel its predecessor. Before creating a new context, the plugin calls
Cancel()to kill any in-flight process from a prior activation or refresh. - The app respects
sdk.Busyas a cancel guard. If a plugin reportsBusy()(holding a terraform state lock during apply), the app skips cancellation on navigation to avoid leaving a stale lock. Cancel()is idempotent. Calling it with no in-flight operation is a no-op (nil check oncancelFn). No sync primitives needed – BubbleTea’s Update loop is single-threaded.
Considered Options
App-owned context via sdk.Context
The app creates a cancellable context at Init/Activate and passes it through to the plugin. Simpler API, but couples operation lifetime to navigation state. Breaks when a plugin runs sequential operations or when one operation should survive while another starts.
No explicit cancellation (rely on terraform lock timeout)
Zero code change. Fails in practice: orphaned plan processes re-lock state faster than timeouts expire, producing cascading lock errors observed in real sessions.
Actor model (per-plugin goroutine + channel)
Each plugin gets lifecycle-managed concurrency. Correct, but a full rewrite of the BubbleTea integration. Disproportionate to the problem – one cancelFn field per plugin achieves the same subprocess termination.