From 475e843b9e3989251276a77f6e3d916ecce3e2a8 Mon Sep 17 00:00:00 2001 From: yashnevatia Date: Wed, 29 Jul 2026 17:03:20 +0100 Subject: [PATCH 1/2] Retry DonsForCapability during cap init --- .../transmission_schedule.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/chain_capabilities/common/transmission_schedule/transmission_schedule.go b/chain_capabilities/common/transmission_schedule/transmission_schedule.go index fb7d53726..eb619516b 100644 --- a/chain_capabilities/common/transmission_schedule/transmission_schedule.go +++ b/chain_capabilities/common/transmission_schedule/transmission_schedule.go @@ -15,6 +15,8 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types/core" + + capcommon "github.com/smartcontractkit/capabilities/chain_capabilities/common" ) // TransmissionScheduler handles capability-layer transmission scheduling by waiting for the appropriate delay @@ -118,7 +120,15 @@ func InitMyDON(ctx context.Context, registry core.CapabilitiesRegistry, capabili return capabilities.DON{}, fmt.Errorf("capabilities registry is nil") } - donsWithNodes, err := registry.DONsForCapability(ctx, capabilityID) + // The host node populates its capabilities metadata registry asynchronously + // (CapabilitiesLauncher.OnNewRegistry after the first on-chain registry sync). + // A plugin's Initialise can win the race and call in before that happens, in + // which case the registry returns "metadataRegistry information not available". + // Retry with backoff until the registry is ready or the host-provided ctx + // (bounded by the standard-capabilities startTimeout) expires. + donsWithNodes, err := capcommon.WithPollingRetry(ctx, lggr, func(ctx context.Context) ([]capabilities.DONWithNodes, error) { + return registry.DONsForCapability(ctx, capabilityID) + }) if err != nil { lggr.Errorw("failed getting DONs for capability", "capabilityID", capabilityID, "error", err) return capabilities.DON{}, fmt.Errorf("failed getting dons for capability: %w", err) @@ -139,6 +149,9 @@ func InitMyDON(ctx context.Context, registry core.CapabilitiesRegistry, capabili } // Legacy path: filter by local PeerID to find which DON(s) this node belongs to. + // No retry needed here: DONsForCapability above already gates on the metadata + // registry being populated, so by this point LocalNode will not hit the + // "metadataRegistry information not available" race. localNode, err := registry.LocalNode(ctx) if err != nil { lggr.Errorw("failed to get local node", "error", err) From af15b275b25944d535f3373c573799e17236712d Mon Sep 17 00:00:00 2001 From: yashnevatia Date: Wed, 29 Jul 2026 17:15:01 +0100 Subject: [PATCH 2/2] Add test --- .../transmission_schedule_test.go | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/chain_capabilities/common/transmission_schedule/transmission_schedule_test.go b/chain_capabilities/common/transmission_schedule/transmission_schedule_test.go index a03ad7288..ae9ff910e 100644 --- a/chain_capabilities/common/transmission_schedule/transmission_schedule_test.go +++ b/chain_capabilities/common/transmission_schedule/transmission_schedule_test.go @@ -2,6 +2,7 @@ package transmissionschedule_test import ( "context" + "errors" "testing" "time" @@ -183,6 +184,19 @@ func TestInitMyDON(t *testing.T) { require.EqualValues(t, 20, got.ID) }) + t.Run("authoritative: returns correct DON by ID after failing once", func(t *testing.T) { + reg := mocks.NewCapabilitiesRegistry(t) + reg.EXPECT().DONsForCapability(mock.Anything, capID).Return(nil, errors.New("metadataRegistry information not available")).Once() + reg.EXPECT().DONsForCapability(mock.Anything, capID).Return(buildDONs(map[uint32][]p2ptypes.PeerID{ + 10: {myPeerID, otherPeerID}, + 20: {myPeerID, otherPeerID}, + }), nil) + + got, err := ts.InitMyDON(context.Background(), reg, capID, 20, logger.Test(t), false) + require.NoError(t, err) + require.EqualValues(t, 20, got.ID) + }) + t.Run("authoritative: selects correct DON even when local node is not listed as a member of all DONs", func(t *testing.T) { // Plex scenario: node serves DON 20 but the registry lists it only in DON 10's // membership. With the authoritative ID we bypass peer-membership filtering @@ -234,6 +248,19 @@ func TestInitMyDON(t *testing.T) { require.EqualValues(t, 10, got.ID) }) + t.Run("legacy: single matched DON returns that DON after failing once", func(t *testing.T) { + reg := mocks.NewCapabilitiesRegistry(t) + withLocalNode(reg) + reg.EXPECT().DONsForCapability(mock.Anything, capID).Return(nil, errors.New("metadataRegistry information not available")).Once() + reg.EXPECT().DONsForCapability(mock.Anything, capID).Return(buildDONs(map[uint32][]p2ptypes.PeerID{ + 10: {myPeerID, otherPeerID}, + }), nil) + + got, err := ts.InitMyDON(context.Background(), reg, capID, 0, logger.Test(t), false) + require.NoError(t, err) + require.EqualValues(t, 10, got.ID) + }) + t.Run("legacy: multiple matched DONs returns first and warns", func(t *testing.T) { reg := mocks.NewCapabilitiesRegistry(t) withLocalNode(reg)