mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Required notification fanout should not block the manager lock, but subscribers still need ordered delivery once brief backpressure clears. Sending must-deliver notifications directly can couple manager progress to subscriber receive timing, while queueing optional reservation notifications would contradict their best-effort delivery semantics. Add bounded per-subscriber queues for must-deliver notifications, let those queues own channel shutdown instead of relying on recover for closed-channel sends, keep reservation fanout best-effort, and cover queued delivery, queue cleanup, and capacity drops in manager tests.
880 lines
23 KiB
Go
880 lines
23 KiB
Go
package notifications
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
|
|
"github.com/lightninglabs/aperture/l402"
|
|
"github.com/lightninglabs/loop/swapserverrpc"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/metadata"
|
|
)
|
|
|
|
var (
|
|
testReservationId = []byte{0x01, 0x02}
|
|
testReservationId2 = []byte{0x03, 0x04}
|
|
)
|
|
|
|
// mockNotificationsClient implements the NotificationsClient interface for testing.
|
|
type mockNotificationsClient struct {
|
|
sync.Mutex
|
|
|
|
mockStream swapserverrpc.SwapServer_SubscribeNotificationsClient
|
|
subscribeErr error
|
|
attemptTimes []time.Time
|
|
timesCalled int
|
|
}
|
|
|
|
func (m *mockNotificationsClient) SubscribeNotifications(ctx context.Context,
|
|
in *swapserverrpc.SubscribeNotificationsRequest,
|
|
opts ...grpc.CallOption) (
|
|
swapserverrpc.SwapServer_SubscribeNotificationsClient, error) {
|
|
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
m.timesCalled++
|
|
m.attemptTimes = append(m.attemptTimes, time.Now())
|
|
if m.subscribeErr != nil {
|
|
return nil, m.subscribeErr
|
|
}
|
|
return m.mockStream, nil
|
|
}
|
|
|
|
// mockSubscribeNotificationsClient simulates the server stream.
|
|
type mockSubscribeNotificationsClient struct {
|
|
grpc.ClientStream
|
|
|
|
recvChan chan *swapserverrpc.SubscribeNotificationsResponse
|
|
recvErrChan chan error
|
|
}
|
|
|
|
func (m *mockSubscribeNotificationsClient) Recv() (
|
|
*swapserverrpc.SubscribeNotificationsResponse, error) {
|
|
|
|
select {
|
|
case err := <-m.recvErrChan:
|
|
return nil, err
|
|
case notif, ok := <-m.recvChan:
|
|
if !ok {
|
|
return nil, io.EOF
|
|
}
|
|
return notif, nil
|
|
}
|
|
}
|
|
|
|
func (m *mockSubscribeNotificationsClient) Header() (metadata.MD, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *mockSubscribeNotificationsClient) Trailer() metadata.MD {
|
|
return nil
|
|
}
|
|
|
|
func (m *mockSubscribeNotificationsClient) CloseSend() error {
|
|
return nil
|
|
}
|
|
|
|
func (m *mockSubscribeNotificationsClient) Context() context.Context {
|
|
return context.TODO()
|
|
}
|
|
|
|
func (m *mockSubscribeNotificationsClient) SendMsg(any) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *mockSubscribeNotificationsClient) RecvMsg(any) error {
|
|
return nil
|
|
}
|
|
|
|
// TestManager_ReservationNotification tests that the Manager correctly
|
|
// forwards reservation notifications to subscribers.
|
|
func TestManager_ReservationNotification(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a mock notification client
|
|
recvChan := make(chan *swapserverrpc.SubscribeNotificationsResponse, 1)
|
|
errChan := make(chan error, 1)
|
|
mockStream := &mockSubscribeNotificationsClient{
|
|
recvChan: recvChan,
|
|
recvErrChan: errChan,
|
|
}
|
|
mockClient := &mockNotificationsClient{
|
|
mockStream: mockStream,
|
|
}
|
|
|
|
// Create a Manager with the mock client
|
|
mgr := NewManager(&Config{
|
|
Client: mockClient,
|
|
CurrentToken: func() (*l402.Token, error) {
|
|
// Simulate successful fetching of L402
|
|
return &l402.Token{
|
|
Preimage: lntypes.Preimage{1, 2, 3},
|
|
}, nil
|
|
},
|
|
})
|
|
|
|
// Subscribe to reservation notifications.
|
|
subCtx, subCancel := context.WithCancel(context.Background())
|
|
subChan := mgr.SubscribeReservations(subCtx)
|
|
|
|
// Run the manager.
|
|
ctx := t.Context()
|
|
|
|
go func() {
|
|
err := mgr.Run(ctx)
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
// Wait a bit to ensure manager is running and has subscribed to the
|
|
// server notifications stream.
|
|
require.Eventually(t, func() bool {
|
|
mockClient.Lock()
|
|
defer mockClient.Unlock()
|
|
|
|
return mockClient.timesCalled > 0
|
|
}, time.Second*5, 10*time.Millisecond)
|
|
|
|
require.Eventually(t, func() bool {
|
|
mockClient.Lock()
|
|
defer mockClient.Unlock()
|
|
return mockClient.timesCalled == 1
|
|
}, time.Second*5, 10*time.Millisecond)
|
|
|
|
// Send a test notification
|
|
testNotif := getTestNotification(testReservationId)
|
|
|
|
// Send the notification to the recvChan
|
|
recvChan <- testNotif
|
|
|
|
// Collect the notification in the callback
|
|
receivedNotification := <-subChan
|
|
|
|
// Now, check that the notification received in the callback matches the one sent
|
|
require.NotNil(t, receivedNotification)
|
|
require.Equal(t, testReservationId, receivedNotification.ReservationId)
|
|
|
|
// Cancel the subscription
|
|
subCancel()
|
|
|
|
// Send another test notification`
|
|
testNotif2 := getTestNotification(testReservationId2)
|
|
recvChan <- testNotif2
|
|
|
|
// Check that the subChan is eventually closed.
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case _, ok := <-subChan:
|
|
return !ok
|
|
default:
|
|
return false
|
|
}
|
|
}, time.Second*5, 10*time.Millisecond)
|
|
}
|
|
|
|
func getTestNotification(resId []byte) *swapserverrpc.SubscribeNotificationsResponse {
|
|
return &swapserverrpc.SubscribeNotificationsResponse{
|
|
Notification: &swapserverrpc.SubscribeNotificationsResponse_ReservationNotification{
|
|
ReservationNotification: &swapserverrpc.ServerReservationNotification{
|
|
ReservationId: resId,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// unfinishedSwapNotification builds an unfinished swap notification.
|
|
func unfinishedSwapNotification(
|
|
swapHash lntypes.Hash) *swapserverrpc.SubscribeNotificationsResponse {
|
|
|
|
return &swapserverrpc.SubscribeNotificationsResponse{
|
|
Notification: &swapserverrpc.
|
|
SubscribeNotificationsResponse_UnfinishedSwap{
|
|
UnfinishedSwap: &swapserverrpc.
|
|
ServerUnfinishedSwapNotification{
|
|
SwapHash: swapHash[:],
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// staticLoopInSweepNotification builds a static loop-in sweep notification.
|
|
func staticLoopInSweepNotification(
|
|
swapHash lntypes.Hash) *swapserverrpc.SubscribeNotificationsResponse {
|
|
|
|
return &swapserverrpc.SubscribeNotificationsResponse{
|
|
Notification: &swapserverrpc.
|
|
SubscribeNotificationsResponse_StaticLoopInSweep{
|
|
StaticLoopInSweep: &swapserverrpc.
|
|
ServerStaticLoopInSweepNotification{
|
|
SwapHash: swapHash[:],
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// TestManager_SlowReservationSubscriberDoesNotBlock tests that a reservation
|
|
// subscriber with a full notification channel does not block delivery to other
|
|
// subscribers. Reservation notifications are best-effort, so slow subscribers
|
|
// drop new notifications instead of queueing them.
|
|
func TestManager_SlowReservationSubscriberDoesNotBlock(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mgr := NewManager(&Config{})
|
|
|
|
slowCtx, slowCancel := context.WithCancel(t.Context())
|
|
defer slowCancel()
|
|
slowChan := mgr.SubscribeReservations(slowCtx)
|
|
|
|
fastCtx, fastCancel := context.WithCancel(t.Context())
|
|
defer fastCancel()
|
|
fastChan := mgr.SubscribeReservations(fastCtx)
|
|
|
|
firstNotif := getTestNotification(testReservationId)
|
|
mgr.handleNotification(firstNotif)
|
|
|
|
received := <-fastChan
|
|
require.Equal(t, testReservationId, received.ReservationId)
|
|
|
|
secondNotif := getTestNotification(testReservationId2)
|
|
done := make(chan struct{})
|
|
go func() {
|
|
mgr.handleNotification(secondNotif)
|
|
close(done)
|
|
}()
|
|
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case <-done:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, time.Second, 10*time.Millisecond)
|
|
|
|
select {
|
|
case received = <-fastChan:
|
|
require.Equal(t, testReservationId2, received.ReservationId)
|
|
|
|
case <-time.After(time.Second):
|
|
t.Fatal("fast subscriber did not receive notification")
|
|
}
|
|
|
|
require.Len(t, slowChan, 1)
|
|
|
|
select {
|
|
case received = <-slowChan:
|
|
require.Equal(t, testReservationId, received.ReservationId)
|
|
|
|
case <-time.After(time.Second):
|
|
t.Fatal("slow subscriber did not receive first notification")
|
|
}
|
|
|
|
select {
|
|
case received = <-slowChan:
|
|
t.Fatalf("slow subscriber received dropped notification %x",
|
|
received.ReservationId)
|
|
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
}
|
|
|
|
// TestManager_UnfinishedSwapNotificationWaitsForSubscriber verifies that
|
|
// unfinished swap recovery notifications are not dropped when the local
|
|
// subscriber is briefly behind.
|
|
func TestManager_UnfinishedSwapNotificationWaitsForSubscriber(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
assertQueuedSwapHashNotifications(
|
|
t,
|
|
func(mgr *Manager, ctx context.Context) <-chan *swapserverrpc.
|
|
ServerUnfinishedSwapNotification {
|
|
|
|
return mgr.SubscribeUnfinishedSwaps(ctx)
|
|
},
|
|
unfinishedSwapNotification,
|
|
func(ntfn *swapserverrpc.ServerUnfinishedSwapNotification) []byte {
|
|
return ntfn.SwapHash
|
|
},
|
|
lntypes.Hash{0x02, 0x03}, lntypes.Hash{0x04, 0x05},
|
|
"did not receive first unfinished swap notification",
|
|
"second unfinished swap notification was dropped",
|
|
)
|
|
}
|
|
|
|
// TestManager_StaticLoopInSweepNotificationQueuesForSlowSubscriber verifies
|
|
// that a full static-loop-in sweep subscriber channel does not block the global
|
|
// notification receive loop.
|
|
func TestManager_StaticLoopInSweepNotificationQueuesForSlowSubscriber(
|
|
t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
assertQueuedSwapHashNotifications(
|
|
t,
|
|
func(mgr *Manager, ctx context.Context) <-chan *swapserverrpc.
|
|
ServerStaticLoopInSweepNotification {
|
|
|
|
return mgr.SubscribeStaticLoopInSweepRequests(ctx)
|
|
},
|
|
staticLoopInSweepNotification,
|
|
func(ntfn *swapserverrpc.ServerStaticLoopInSweepNotification) []byte {
|
|
return ntfn.SwapHash
|
|
},
|
|
lntypes.Hash{0x12, 0x13}, lntypes.Hash{0x14, 0x15},
|
|
"did not receive first sweep notification",
|
|
"second sweep notification was not queued",
|
|
)
|
|
}
|
|
|
|
// TestManager_QueuedNotificationChannelClosesOnCancel verifies that queued
|
|
// subscribers own their channel shutdown even when delivery is blocked.
|
|
func TestManager_QueuedNotificationChannelClosesOnCancel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mgr := NewManager(&Config{})
|
|
|
|
subCtx, subCancel := context.WithCancel(t.Context())
|
|
subChan := mgr.SubscribeUnfinishedSwaps(subCtx)
|
|
|
|
swapHashA := lntypes.Hash{0x21, 0x22}
|
|
mgr.handleNotification(unfinishedSwapNotification(swapHashA))
|
|
|
|
require.Eventually(t, func() bool {
|
|
return len(subChan) == 1
|
|
}, time.Second, 10*time.Millisecond)
|
|
|
|
swapHashB := lntypes.Hash{0x23, 0x24}
|
|
done := make(chan struct{})
|
|
go func() {
|
|
mgr.handleNotification(unfinishedSwapNotification(swapHashB))
|
|
close(done)
|
|
}()
|
|
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case <-done:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, time.Second, 10*time.Millisecond)
|
|
|
|
subCancel()
|
|
|
|
select {
|
|
case received, ok := <-subChan:
|
|
require.True(t, ok)
|
|
require.Equal(t, swapHashA[:], received.SwapHash)
|
|
|
|
case <-time.After(time.Second):
|
|
t.Fatal("first unfinished swap notification was not delivered")
|
|
}
|
|
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case _, ok := <-subChan:
|
|
return !ok
|
|
default:
|
|
return false
|
|
}
|
|
}, time.Second, 10*time.Millisecond)
|
|
}
|
|
|
|
// TestNotificationQueueDropsAtCapacity checks the queue's explicit drop policy
|
|
// once a subscriber reaches its configured backlog limit.
|
|
func TestNotificationQueueDropsAtCapacity(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
|
|
recvChan := make(chan int, 1)
|
|
enqueue := newNotificationQueue(ctx, recvChan, 0)
|
|
|
|
enqueue(1)
|
|
|
|
select {
|
|
case ntfn := <-recvChan:
|
|
t.Fatalf("received dropped notification %d", ntfn)
|
|
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
}
|
|
|
|
// assertQueuedSwapHashNotifications checks queued delivery for swap hashes.
|
|
func assertQueuedSwapHashNotifications[T any](t *testing.T,
|
|
subscribe func(*Manager, context.Context) <-chan T,
|
|
notification func(lntypes.Hash) *swapserverrpc.
|
|
SubscribeNotificationsResponse,
|
|
swapHash func(T) []byte, swapHashA, swapHashB lntypes.Hash,
|
|
firstFailureMsg, secondFailureMsg string) {
|
|
|
|
t.Helper()
|
|
|
|
mgr := NewManager(&Config{})
|
|
|
|
subCtx, subCancel := context.WithCancel(t.Context())
|
|
defer subCancel()
|
|
|
|
subChan := subscribe(mgr, subCtx)
|
|
|
|
mgr.handleNotification(notification(swapHashA))
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
mgr.handleNotification(notification(swapHashB))
|
|
close(done)
|
|
}()
|
|
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case <-done:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, time.Second, 10*time.Millisecond)
|
|
|
|
select {
|
|
case received := <-subChan:
|
|
require.Equal(t, swapHashA[:], swapHash(received))
|
|
|
|
case <-time.After(time.Second):
|
|
t.Fatal(firstFailureMsg)
|
|
}
|
|
|
|
select {
|
|
case received := <-subChan:
|
|
require.Equal(t, swapHashB[:], swapHash(received))
|
|
|
|
case <-time.After(time.Second):
|
|
t.Fatal(secondFailureMsg)
|
|
}
|
|
}
|
|
|
|
// TestManager_Backoff verifies that repeated failures in
|
|
// subscribeNotifications cause the Manager to space out subscription attempts
|
|
// via a predictable incremental backoff.
|
|
func TestManager_Backoff(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// We'll tolerate a bit of jitter in the timing checks.
|
|
const tolerance = 300 * time.Millisecond
|
|
|
|
recvChan := make(chan *swapserverrpc.SubscribeNotificationsResponse)
|
|
recvErrChan := make(chan error)
|
|
|
|
mockStream := &mockSubscribeNotificationsClient{
|
|
recvChan: recvChan,
|
|
recvErrChan: recvErrChan,
|
|
}
|
|
|
|
// Create a new mock client that will fail to subscribe.
|
|
mockClient := &mockNotificationsClient{
|
|
mockStream: mockStream,
|
|
subscribeErr: errors.New("failing on purpose"),
|
|
}
|
|
|
|
// Manager with a successful CurrentToken so that it always tries
|
|
// to subscribe.
|
|
mgr := NewManager(&Config{
|
|
Client: mockClient,
|
|
CurrentToken: func() (*l402.Token, error) {
|
|
// Simulate successful fetching of L402
|
|
return &l402.Token{
|
|
Preimage: lntypes.Preimage{1, 2, 3},
|
|
}, nil
|
|
},
|
|
})
|
|
|
|
// Run the manager in a background goroutine.
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Go(func() {
|
|
// We ignore the returned error because the Manager returns
|
|
// nil on context cancel.
|
|
_ = mgr.Run(ctx)
|
|
})
|
|
|
|
// Wait long enough to see at least 3 subscription attempts using
|
|
// the Manager's default pattern.
|
|
// We'll wait ~5 seconds total so we capture at least 3 attempts:
|
|
// - Attempt #1: immediate
|
|
// - Attempt #2: ~1 second
|
|
// - Attempt #3: ~3 seconds after that etc.
|
|
time.Sleep(5 * time.Second)
|
|
|
|
// Cancel the context to stop the manager.
|
|
cancel()
|
|
wg.Wait()
|
|
|
|
// Check how many attempts we made.
|
|
require.GreaterOrEqual(t, len(mockClient.attemptTimes), 3,
|
|
"expected at least 3 attempts within 5 seconds",
|
|
)
|
|
|
|
expectedDelay := time.Second
|
|
for i := 1; i < len(mockClient.attemptTimes); i++ {
|
|
// The expected delay for the i-th gap (comparing attempt i to
|
|
// attempt i-1) is i seconds (because the manager increments
|
|
// the backoff by 1 second each time).
|
|
actualDelay := mockClient.attemptTimes[i].Sub(
|
|
mockClient.attemptTimes[i-1],
|
|
)
|
|
|
|
require.InDeltaf(
|
|
t, expectedDelay, actualDelay, float64(tolerance),
|
|
"Attempt %d -> Attempt %d delay should be ~%v, got %v",
|
|
i, i+1, expectedDelay, actualDelay,
|
|
)
|
|
|
|
expectedDelay += time.Second
|
|
}
|
|
}
|
|
|
|
// TestManager_MinAliveConnTime verifies that the Manager enforces the minimum
|
|
// alive connection time before considering a subscription successful.
|
|
func TestManager_MinAliveConnTime(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Tolerance to allow for scheduling jitter.
|
|
const tolerance = 300 * time.Millisecond
|
|
|
|
// Set a small MinAliveConnTime so the test doesn't run too long.
|
|
// Once a subscription stays alive longer than 2s, the manager resets
|
|
// its backoff to 10s on the next loop iteration.
|
|
const minAlive = 1 * time.Second
|
|
|
|
// We'll provide a channel for incoming notifications
|
|
// and another for forcing errors to close the subscription.
|
|
recvChan := make(chan *swapserverrpc.SubscribeNotificationsResponse)
|
|
recvErrChan := make(chan error)
|
|
|
|
mockStream := &mockSubscribeNotificationsClient{
|
|
recvChan: recvChan,
|
|
recvErrChan: recvErrChan,
|
|
}
|
|
|
|
// No immediate error from SubscribeNotifications, so it "succeeds".
|
|
// We trigger subscription closure by sending an error to recvErrChan.
|
|
mockClient := &mockNotificationsClient{
|
|
mockStream: mockStream,
|
|
// subscribeErr stays nil => success on each call.
|
|
}
|
|
|
|
// Create a Manager that uses our mock client and enforces
|
|
// MinAliveConnTime=2s.
|
|
mgr := NewManager(&Config{
|
|
Client: mockClient,
|
|
MinAliveConnTime: minAlive,
|
|
CurrentToken: func() (*l402.Token, error) {
|
|
// Simulate successful fetching of L402
|
|
return &l402.Token{
|
|
Preimage: lntypes.Preimage{1, 2, 3},
|
|
}, nil
|
|
},
|
|
})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
var wg sync.WaitGroup
|
|
wg.Go(func() {
|
|
_ = mgr.Run(ctx)
|
|
})
|
|
|
|
// Let the subscription stay alive for 2s, which is >1s (minAlive).
|
|
// Then force an error to end the subscription. The manager sees
|
|
// it stayed connected ~2s and resets its backoff to 10s.
|
|
go func() {
|
|
time.Sleep(2 * time.Second)
|
|
recvErrChan <- errors.New("mock subscription closed")
|
|
}()
|
|
|
|
// Wait enough time (~13s) to see:
|
|
// - First subscription (2s)
|
|
// - Manager resets to 10s
|
|
// - Second subscription attempt starts ~10s later.
|
|
time.Sleep(13 * time.Second)
|
|
|
|
// Signal EOF so the subscription stops.
|
|
close(recvChan)
|
|
|
|
// Stop the manager and wait for cleanup.
|
|
cancel()
|
|
wg.Wait()
|
|
|
|
// Expect at least 2 attempts in attemptTimes:
|
|
// 1) The one that stayed alive for 2s,
|
|
// 2) The next attempt ~10s after that.
|
|
require.GreaterOrEqual(
|
|
t, len(mockClient.attemptTimes), 2,
|
|
"expected at least 2 attempts with a successful subscription",
|
|
)
|
|
|
|
require.InDeltaf(
|
|
t, 12*time.Second,
|
|
mockClient.attemptTimes[1].Sub(mockClient.attemptTimes[0]),
|
|
float64(tolerance),
|
|
"Second attempt should occur ~2s after the first",
|
|
)
|
|
}
|
|
|
|
// TestManager_Backoff_Pending_Token verifies that the Manager backs off when
|
|
// the token is pending.
|
|
func TestManager_Backoff_Pending_Token(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// We'll tolerate a bit of jitter in the timing checks.
|
|
const tolerance = 300 * time.Millisecond
|
|
|
|
recvChan := make(chan *swapserverrpc.SubscribeNotificationsResponse)
|
|
recvErrChan := make(chan error)
|
|
|
|
mockStream := &mockSubscribeNotificationsClient{
|
|
recvChan: recvChan,
|
|
recvErrChan: recvErrChan,
|
|
}
|
|
|
|
// Create a new mock client that will fail to subscribe.
|
|
mockClient := &mockNotificationsClient{
|
|
mockStream: mockStream,
|
|
// subscribeErr stays nil => would succeed on each call.
|
|
}
|
|
|
|
var tokenCalls []time.Time
|
|
// Manager with a successful CurrentToken so that it always tries
|
|
// to subscribe.
|
|
mgr := NewManager(&Config{
|
|
Client: mockClient,
|
|
CurrentToken: func() (*l402.Token, error) {
|
|
tokenCalls = append(tokenCalls, time.Now())
|
|
if len(tokenCalls) < 3 {
|
|
// Simulate a pending token.
|
|
return &l402.Token{}, nil
|
|
}
|
|
|
|
// Simulate successful fetching of L402
|
|
return &l402.Token{
|
|
Preimage: lntypes.Preimage{1, 2, 3},
|
|
}, nil
|
|
},
|
|
})
|
|
|
|
// Run the manager in a background goroutine.
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Go(func() {
|
|
// We ignore the returned error because the Manager returns
|
|
// nil on context cancel.
|
|
_ = mgr.Run(ctx)
|
|
})
|
|
|
|
// Wait long enough to see at least 3 token calls, so we can see that
|
|
// we'll indeed backoff when the token is pending.
|
|
time.Sleep(5 * time.Second)
|
|
|
|
// Signal EOF so the subscription stops.
|
|
close(recvChan)
|
|
|
|
// Cancel the context to stop the manager.
|
|
cancel()
|
|
wg.Wait()
|
|
|
|
// Expect exactly 3 token calls.
|
|
require.Len(t, tokenCalls, 3)
|
|
|
|
require.InDeltaf(
|
|
t, 3*time.Second, tokenCalls[2].Sub(tokenCalls[0]),
|
|
float64(tolerance),
|
|
"Expected to backoff for at ~3 seconds due to pending token",
|
|
)
|
|
}
|
|
|
|
// TestManager_HtlcConfirmedNotification tests that the Manager correctly
|
|
// forwards htlc confirmed notifications to subscribers via the end-to-end
|
|
// subscription path.
|
|
func TestManager_HtlcConfirmedNotification(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a mock notification client.
|
|
recvChan := make(
|
|
chan *swapserverrpc.SubscribeNotificationsResponse, 1,
|
|
)
|
|
errChan := make(chan error, 1)
|
|
mockStream := &mockSubscribeNotificationsClient{
|
|
recvChan: recvChan,
|
|
recvErrChan: errChan,
|
|
}
|
|
mockClient := &mockNotificationsClient{
|
|
mockStream: mockStream,
|
|
}
|
|
|
|
mgr := NewManager(&Config{
|
|
Client: mockClient,
|
|
CurrentToken: func() (*l402.Token, error) {
|
|
return &l402.Token{
|
|
Preimage: lntypes.Preimage{1, 2, 3},
|
|
}, nil
|
|
},
|
|
})
|
|
|
|
// Subscribe to htlc confirmed notifications.
|
|
ctx := t.Context()
|
|
subCtx, subCancel := context.WithCancel(ctx)
|
|
defer subCancel()
|
|
subChan := mgr.SubscribeHtlcConfirmed(subCtx)
|
|
|
|
// Run the manager.
|
|
go func() {
|
|
_ = mgr.Run(ctx)
|
|
}()
|
|
|
|
// Wait for the manager to subscribe to the server stream.
|
|
require.Eventually(t, func() bool {
|
|
mockClient.Lock()
|
|
defer mockClient.Unlock()
|
|
|
|
return mockClient.timesCalled > 0
|
|
}, time.Second*5, 10*time.Millisecond)
|
|
|
|
// Send an htlc confirmed notification via the mock stream.
|
|
testSwapHash := []byte("test_hash_32_bytes_long_padding!")
|
|
testNtfn := &swapserverrpc.SubscribeNotificationsResponse{
|
|
Notification: &swapserverrpc.SubscribeNotificationsResponse_HtlcConfirmed{ // nolint: lll
|
|
HtlcConfirmed: &swapserverrpc.ServerHtlcConfirmedNotification{ // nolint: lll
|
|
SwapHash: testSwapHash,
|
|
HtlcOutpoint: "abc123:0",
|
|
HtlcAddress: "tb1qexamplehtlcaddress",
|
|
SatPerVbyte: 25,
|
|
},
|
|
},
|
|
}
|
|
recvChan <- testNtfn
|
|
|
|
// Verify the subscriber receives it.
|
|
select {
|
|
case received := <-subChan:
|
|
require.NotNil(t, received)
|
|
require.Equal(t, testSwapHash, received.SwapHash)
|
|
require.Equal(t, "abc123:0", received.HtlcOutpoint)
|
|
require.Equal(t, "tb1qexamplehtlcaddress", received.HtlcAddress)
|
|
require.Equal(t, uint32(25), received.SatPerVbyte)
|
|
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("did not receive htlc confirmed notification")
|
|
}
|
|
|
|
// Cancel the subscription and verify the channel closes.
|
|
subCancel()
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case _, ok := <-subChan:
|
|
return !ok
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}, time.Second*5, 10*time.Millisecond)
|
|
}
|
|
|
|
// TestManager_HtlcConfirmedNonBlocking tests that a slow htlc confirmed
|
|
// subscriber does not block the notification pipeline.
|
|
func TestManager_HtlcConfirmedNonBlocking(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
synctest.Test(t, func(t *testing.T) {
|
|
recvChan := make(
|
|
chan *swapserverrpc.SubscribeNotificationsResponse, 1,
|
|
)
|
|
errChan := make(chan error, 1)
|
|
mockStream := &mockSubscribeNotificationsClient{
|
|
recvChan: recvChan,
|
|
recvErrChan: errChan,
|
|
}
|
|
mockClient := &mockNotificationsClient{
|
|
mockStream: mockStream,
|
|
}
|
|
|
|
mgr := NewManager(&Config{
|
|
Client: mockClient,
|
|
CurrentToken: func() (*l402.Token, error) {
|
|
return &l402.Token{
|
|
Preimage: lntypes.Preimage{1, 2, 3},
|
|
}, nil
|
|
},
|
|
})
|
|
|
|
// Subscribe but never read from the channel.
|
|
subCtx, subCancel := context.WithCancel(t.Context())
|
|
defer subCancel()
|
|
_ = mgr.SubscribeHtlcConfirmed(subCtx)
|
|
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
go func() {
|
|
_ = mgr.Run(ctx)
|
|
}()
|
|
|
|
// Wait for the manager to connect and block on stream Recv().
|
|
synctest.Wait()
|
|
mockClient.Lock()
|
|
require.Greater(t, mockClient.timesCalled, 0)
|
|
mockClient.Unlock()
|
|
|
|
// Also send a reservation notification to prove the pipeline
|
|
// is not blocked — it should still be dispatched once the
|
|
// timed htlc fanout drop has elapsed.
|
|
resChan := mgr.SubscribeReservations(subCtx)
|
|
|
|
// Send two notifications rapidly. The subscriber channel has
|
|
// buffer 1, so the first should be delivered and the second
|
|
// should be dropped after waiting for the timeout.
|
|
for i := range 2 {
|
|
ntfn := &swapserverrpc.SubscribeNotificationsResponse{
|
|
Notification: &swapserverrpc.SubscribeNotificationsResponse_HtlcConfirmed{ // nolint: lll
|
|
HtlcConfirmed: &swapserverrpc.ServerHtlcConfirmedNotification{ // nolint: lll
|
|
SwapHash: fmt.Appendf(nil, "hash_%d_padding_to_32bytes!!", i), // nolint: lll
|
|
HtlcOutpoint: "abc:0",
|
|
HtlcAddress: "tb1qexamplehtlcaddress",
|
|
SatPerVbyte: 25,
|
|
},
|
|
},
|
|
}
|
|
recvChan <- ntfn
|
|
}
|
|
|
|
resNtfn := &swapserverrpc.SubscribeNotificationsResponse{
|
|
Notification: &swapserverrpc.SubscribeNotificationsResponse_ReservationNotification{ // nolint: lll
|
|
ReservationNotification: &swapserverrpc.ServerReservationNotification{ // nolint: lll
|
|
ReservationId: []byte("res1"),
|
|
},
|
|
},
|
|
}
|
|
recvChan <- resNtfn
|
|
|
|
select {
|
|
case res := <-resChan:
|
|
require.Equal(t, []byte("res1"), res.ReservationId)
|
|
|
|
case <-time.After(5 * htlcConfirmedSubscriberSendTimeout):
|
|
t.Fatal("reservation notification blocked by slow " +
|
|
"htlc confirmed subscriber")
|
|
}
|
|
|
|
// Stop the manager and ensure all goroutines in the test exit.
|
|
cancel()
|
|
close(recvChan)
|
|
synctest.Wait()
|
|
})
|
|
}
|