routing_plugin: release with a live context

`ReleaseRoutingPlugin` called `Done` with the caller context. In the
loop-out payment flow this cleanup is deferred, so the context may already be
canceled by the time teardown runs.

That can prevent mission-control restoration in `Done`, leaving
plugin-induced routing state behind after a swap exits.

Fix this by detaching cancellation in `ReleaseRoutingPlugin` and passing
`context.WithoutCancel(ctx)` to `Done`, so teardown still executes during
caller cancellation.

Also add a regression test that cancels the caller context before release and
asserts the plugin `Done` call still receives a live context.
This commit is contained in:
Boris Nagaev 2026-03-01 22:17:35 -05:00 committed by Slyghtning
parent 3e526faf82
commit f191775773
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
2 changed files with 60 additions and 3 deletions

View file

@ -692,3 +692,51 @@ func TestRoutingPluginAcquireRelease(t *testing.T) {
require.NotNil(t, plugin2)
require.NoError(t, err)
}
// mockRoutingPlugin is a minimal RoutingPlugin used to capture the context
// passed to Done.
type mockRoutingPlugin struct {
doneCtxErr error
}
// Init is a no-op initializer for the mock plugin.
func (m *mockRoutingPlugin) Init(_ context.Context, _ route.Vertex,
_ [][]zpay32.HopHint, _ btcutil.Amount) error {
return nil
}
// Done records ctx.Err() so tests can assert whether teardown ran with a live
// context.
func (m *mockRoutingPlugin) Done(ctx context.Context) error {
m.doneCtxErr = ctx.Err()
return nil
}
// BeforePayment is a no-op hook for the mock plugin.
func (m *mockRoutingPlugin) BeforePayment(_ context.Context, _, _ int) error {
return nil
}
// TestReleaseRoutingPluginUsesLiveContext checks that ReleaseRoutingPlugin does
// not propagate caller cancellation to plugin teardown. The test cancels the
// caller context before release and verifies mock Done still sees nil ctx.Err.
func TestReleaseRoutingPluginUsesLiveContext(t *testing.T) {
ReleaseRoutingPlugin(context.Background())
t.Cleanup(func() {
ReleaseRoutingPlugin(context.Background())
})
mockPlugin := &mockRoutingPlugin{}
routingPluginMx.Lock()
routingPluginInstance = mockPlugin
routingPluginMx.Unlock()
ctx, cancel := context.WithCancel(context.Background())
cancel()
ReleaseRoutingPlugin(ctx)
require.NoError(t, mockPlugin.doneCtxErr)
}