Skip to content

Commit 27432c8

Browse files
committed
loadtest: add synctest
Adds the synctest to our loadtest suite. There's 2 supported modes which correspond to whether we use the simple syncer or perform plain REST calls to the universe server.
1 parent 8537b32 commit 27432c8

File tree

2 files changed

+161
-0
lines changed

2 files changed

+161
-0
lines changed

itest/loadtest/load_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ var loadTestCases = []testCase{
5555
name: "multisig",
5656
fn: multisigTest,
5757
},
58+
{
59+
name: "sync",
60+
fn: syncTest,
61+
},
5862
}
5963

6064
// TestPerformance executes the configured performance tests.

itest/loadtest/sync_test.go

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package loadtest
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"strings"
10+
"sync"
11+
"testing"
12+
13+
tap "github.com/lightninglabs/taproot-assets"
14+
"github.com/lightninglabs/taproot-assets/taprpc/universerpc"
15+
"github.com/lightninglabs/taproot-assets/universe"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
// syncTest checks that the universe server can handle multiple requests to its
20+
// public universe endpoints. The number of clients is configurable.
21+
func syncTest(t *testing.T, ctx context.Context, cfg *Config) {
22+
alice := initAlice(t, ctx, cfg)
23+
24+
// Let's start by logging the aggregate universe stats.
25+
res, err := alice.UniverseStats(ctx, &universerpc.StatsRequest{})
26+
require.NoError(t, err)
27+
28+
t.Logf("Universe Aggregate Stats: %+v", res)
29+
30+
// We'll use this wait group to block until all clients are done.
31+
var wg sync.WaitGroup
32+
33+
// We dispatch a client sync for the configured number of clients.
34+
for i := range cfg.SyncNumClients {
35+
switch cfg.SyncType {
36+
case "simplesyncer":
37+
wg.Add(1)
38+
go simpleSyncer(t, ctx, cfg, i, &wg)
39+
40+
case "rest":
41+
wg.Add(1)
42+
go syncAssetRootsREST(t, ctx, cfg, i, &wg)
43+
}
44+
}
45+
46+
wg.Wait()
47+
}
48+
49+
// simpleSyncer creates a simple syncer instance and uses Alice as the remote
50+
// diff engine. It always triggers a full sync as the local diff engine returns
51+
// an empty leaf node.
52+
func simpleSyncer(t *testing.T, ctx context.Context, cfg *Config, id int,
53+
wg *sync.WaitGroup) {
54+
55+
defer wg.Done()
56+
57+
// Alice is always serving as the universe server.
58+
uniURL := fmt.Sprintf("%s:%v", cfg.Alice.Tapd.Host, cfg.Alice.Tapd.Port)
59+
60+
syncer := universe.NewSimpleSyncer(
61+
universe.SimpleSyncCfg{
62+
LocalDiffEngine: noopBaseUni{},
63+
NewRemoteDiffEngine: tap.NewRpcUniverseDiff,
64+
LocalRegistrar: noopBaseUni{},
65+
SyncBatchSize: 512,
66+
},
67+
)
68+
69+
t.Logf("SimpleSyncer-%02d: Starting full universe sync", id)
70+
71+
// We don't care about the diff, so we only check if an error occurred,
72+
// otherwise the sync completed.
73+
_, err := syncer.SyncUniverse(
74+
ctx, universe.NewServerAddrFromStr(uniURL),
75+
universe.SyncFull, universe.SyncConfigs{
76+
GlobalSyncConfigs: []*universe.FedGlobalSyncConfig{
77+
{
78+
// nolint:lll
79+
ProofType: universe.ProofTypeIssuance,
80+
AllowSyncInsert: true,
81+
},
82+
},
83+
},
84+
)
85+
require.NoError(t, err)
86+
87+
t.Logf("SimpleSyncer-%02d: Completed full universe sync", id)
88+
}
89+
90+
// syncAssetRootsREST performs a series of requests to the AssetRoots endpoint
91+
// of the universe server. It automatically progresses the requested page until
92+
// the whole data is read.
93+
func syncAssetRootsREST(t *testing.T, ctx context.Context, cfg *Config,
94+
id int, wg *sync.WaitGroup) {
95+
96+
defer wg.Done()
97+
var (
98+
limit = cfg.SyncPageSize
99+
offset = 0
100+
)
101+
102+
for {
103+
// This is the URL of the universe server, in our case that's
104+
// always Alice.
105+
baseURL := fmt.Sprintf(
106+
"https://%s:%v/v1/taproot-assets/universe/roots",
107+
cfg.Alice.Tapd.Host, cfg.Alice.Tapd.RestPort,
108+
)
109+
110+
// We inject the pagination GET params.
111+
fullURL := fmt.Sprintf(
112+
"%s?offset=%v&limit=%v", baseURL, offset, limit,
113+
)
114+
115+
t.Logf("Syncer%02d: Fetching AssetRoots offset=%v, limit=%v",
116+
id, offset, limit)
117+
118+
res := getAssetRoots(t, ctx, fullURL)
119+
120+
// In order to count the length of the response without doing
121+
// JSON parsing, we simply count the occurences of a top-level
122+
// field name that repeats for all entries in the array.
123+
len := strings.Count(res, "mssmt_root")
124+
125+
// Break if we reached the end. This is signalled by retrieving
126+
// less entities than what was defined as the max limit,
127+
// meaning that there's nothing left to consume.
128+
if len < limit {
129+
break
130+
}
131+
132+
offset += limit
133+
}
134+
}
135+
136+
// getAssetRoots performs a GET request to the AssetRoots REST endpoint of the
137+
// universe server. We don't care about handling the response, we just hit the
138+
// endpoint and return the text of the body.
139+
func getAssetRoots(t *testing.T, ctx context.Context, fullURL string) string {
140+
tr := &http.Transport{
141+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
142+
}
143+
144+
client := &http.Client{Transport: tr}
145+
146+
req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
147+
require.NoError(t, err)
148+
149+
resp, err := client.Do(req)
150+
require.NoError(t, err)
151+
defer resp.Body.Close()
152+
153+
body, err := io.ReadAll(resp.Body)
154+
require.NoError(t, err)
155+
156+
return string(body)
157+
}

0 commit comments

Comments
 (0)