Skip to content

Commit 15f8e9c

Browse files
authored
Merge pull request #109 from smartcontractkit/runlog_soak
Runlog soak test
2 parents d22e314 + a0074e0 commit 15f8e9c

File tree

10 files changed

+459
-4
lines changed

10 files changed

+459
-4
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ test_ocr: ## run ocr tests
6868
test_ocr_soak: ## run OCR soak test
6969
NETWORK="ethereum_geth_performance" ginkgo -r --focus="@soak-ocr"
7070

71+
.PHONY: test_runlog_soak
72+
test_runlog_soak: ## run Runlog soak test
73+
NETWORK="ethereum_geth_performance" ginkgo -r --focus="@soak-runlog"
74+
7175
.PHONY: test_keeper_soak
7276
test_keeper_soak: ## run Keeper soak/performance test
7377
NETWORK="ethereum_geth_performance" ginkgo -r --focus="@performance-keeper"

config.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,3 @@ contracts:
240240
ocr:
241241
path: ocr/artifacts/contract/src
242242
commit: f27c14a905c5735abbb6e0c9699e9d0e3e9b7217
243-

contracts/contract_models.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ type Oracle interface {
131131

132132
type APIConsumer interface {
133133
Address() string
134+
RoundID(ctx context.Context) (*big.Int, error)
134135
Fund(fromWallet client.BlockchainWallet, ethAmount, linkAmount *big.Float) error
135136
Data(ctx context.Context) (*big.Int, error)
136137
CreateRequestTo(

contracts/ethereum/APIConsumer.go

Lines changed: 33 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contracts/ethereum/v0.6/abi/APIConsumer.abi

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,19 @@
210210
"stateMutability": "view",
211211
"type": "function"
212212
},
213+
{
214+
"inputs": [],
215+
"name": "roundID",
216+
"outputs": [
217+
{
218+
"internalType": "uint256",
219+
"name": "",
220+
"type": "uint256"
221+
}
222+
],
223+
"stateMutability": "view",
224+
"type": "function"
225+
},
213226
{
214227
"inputs": [],
215228
"name": "selector",

contracts/ethereum/v0.6/bin/APIConsumer.bin

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

contracts/ethereum/v0.6/src/APIConsumer.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import "./Ownable.sol";
1818
* local test networks
1919
*/
2020
contract APIConsumer is ChainlinkClient, Ownable {
21+
uint256 public roundID = 0;
2122
uint256 public data;
2223
bytes4 public selector;
2324

@@ -85,6 +86,7 @@ contract APIConsumer is ChainlinkClient, Ownable {
8586
public
8687
{
8788
data = _data;
89+
roundID += 1;
8890
}
8991

9092
/**

contracts/ethereum_contracts.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ func (e *EthereumAPIConsumer) Address() string {
6060
return e.address.Hex()
6161
}
6262

63+
func (e *EthereumAPIConsumer) RoundID(ctx context.Context) (*big.Int, error) {
64+
opts := &bind.CallOpts{
65+
From: common.HexToAddress(e.callerWallet.Address()),
66+
Pending: true,
67+
Context: ctx,
68+
}
69+
return e.consumer.RoundID(opts)
70+
}
71+
6372
func (e *EthereumAPIConsumer) Fund(fromWallet client.BlockchainWallet, ethAmount, linkAmount *big.Float) error {
6473
return e.client.Fund(fromWallet, e.address.Hex(), ethAmount, linkAmount)
6574
}
@@ -731,6 +740,63 @@ func (o *EthereumOffchainAggregator) GetLatestRound(ctxt context.Context) (*Roun
731740
}, err
732741
}
733742

743+
// RunlogRoundConfirmer is a header subscription that awaits for a certain Runlog round to be completed
744+
type RunlogRoundConfirmer struct {
745+
consumer APIConsumer
746+
roundID *big.Int
747+
doneChan chan struct{}
748+
context context.Context
749+
cancel context.CancelFunc
750+
}
751+
752+
// NewRunlogRoundConfirmer provides a new instance of a RunlogRoundConfirmer
753+
func NewRunlogRoundConfirmer(
754+
contract APIConsumer,
755+
roundID *big.Int,
756+
timeout time.Duration,
757+
) *RunlogRoundConfirmer {
758+
ctx, ctxCancel := context.WithTimeout(context.Background(), timeout)
759+
return &RunlogRoundConfirmer{
760+
consumer: contract,
761+
roundID: roundID,
762+
doneChan: make(chan struct{}),
763+
context: ctx,
764+
cancel: ctxCancel,
765+
}
766+
}
767+
768+
// ReceiveBlock will query the latest Runlog round and check to see whether the round has confirmed
769+
func (o *RunlogRoundConfirmer) ReceiveBlock(_ client.NodeBlock) error {
770+
currentRoundID, err := o.consumer.RoundID(context.Background())
771+
if err != nil {
772+
return err
773+
}
774+
ocrLog := log.Info().
775+
Str("Contract Address", o.consumer.Address()).
776+
Int64("Current Round", currentRoundID.Int64()).
777+
Int64("Waiting for Round", o.roundID.Int64())
778+
if currentRoundID.Cmp(o.roundID) >= 0 {
779+
ocrLog.Msg("Runlog round completed")
780+
o.doneChan <- struct{}{}
781+
} else {
782+
ocrLog.Msg("Waiting for Runlog round")
783+
}
784+
return nil
785+
}
786+
787+
// Wait is a blocking function that will wait until the round has confirmed, and timeout if the deadline has passed
788+
func (o *RunlogRoundConfirmer) Wait() error {
789+
for {
790+
select {
791+
case <-o.doneChan:
792+
o.cancel()
793+
return nil
794+
case <-o.context.Done():
795+
return fmt.Errorf("timeout waiting for OCR round to confirm: %d", o.roundID)
796+
}
797+
}
798+
}
799+
734800
// OffchainAggregatorRoundConfirmer is a header subscription that awaits for a certain OCR round to be completed
735801
type OffchainAggregatorRoundConfirmer struct {
736802
ocrInstance OffchainAggregator

suite/performance/runlog.go

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package performance
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/ethereum/go-ethereum/common"
7+
"github.com/onsi/ginkgo"
8+
"github.com/rs/zerolog/log"
9+
uuid "github.com/satori/go.uuid"
10+
"github.com/smartcontractkit/integrations-framework/actions"
11+
"github.com/smartcontractkit/integrations-framework/client"
12+
"github.com/smartcontractkit/integrations-framework/contracts"
13+
"github.com/smartcontractkit/integrations-framework/environment"
14+
"golang.org/x/sync/errgroup"
15+
"math/big"
16+
"strings"
17+
"time"
18+
)
19+
20+
// RunlogJobMap is a custom map type that holds the record of jobs by the contract instance and the chainlink node
21+
type RunlogJobMap map[ConsumerOraclePair]map[client.Chainlink]string
22+
23+
// ConsumerOraclePair consumer and oracle pair
24+
type ConsumerOraclePair struct {
25+
consumer contracts.APIConsumer
26+
oracle contracts.Oracle
27+
jobUUID string
28+
}
29+
30+
// RunlogTestOptions contains the parameters for the Runlog soak test to be executed
31+
type RunlogTestOptions struct {
32+
TestOptions
33+
RoundTimeout time.Duration
34+
AdapterValue int
35+
TestDuration time.Duration
36+
}
37+
38+
// RunlogTest is the implementation of Test that will configure and execute soak test
39+
// of Runlog contracts & jobs
40+
type RunlogTest struct {
41+
TestOptions RunlogTestOptions
42+
Environment environment.Environment
43+
Blockchain client.BlockchainClient
44+
Wallets client.BlockchainWallets
45+
Deployer contracts.ContractDeployer
46+
Link contracts.LinkToken
47+
48+
chainlinkClients []client.Chainlink
49+
nodeAddresses []common.Address
50+
contractInstances []*ConsumerOraclePair
51+
adapter environment.ExternalAdapter
52+
53+
jobMap RunlogJobMap
54+
}
55+
56+
// NewRunlogTest creates new Runlog performance/soak test
57+
func NewRunlogTest(
58+
testOptions RunlogTestOptions,
59+
env environment.Environment,
60+
link contracts.LinkToken,
61+
blockchain client.BlockchainClient,
62+
wallets client.BlockchainWallets,
63+
deployer contracts.ContractDeployer,
64+
adapter environment.ExternalAdapter,
65+
) Test {
66+
return &RunlogTest{
67+
TestOptions: testOptions,
68+
Environment: env,
69+
Link: link,
70+
Blockchain: blockchain,
71+
Wallets: wallets,
72+
Deployer: deployer,
73+
adapter: adapter,
74+
jobMap: RunlogJobMap{},
75+
}
76+
}
77+
78+
// RecordValues records Runlog metrics
79+
func (f *RunlogTest) RecordValues(b ginkgo.Benchmarker) error {
80+
// TODO: collect metrics
81+
return nil
82+
}
83+
84+
// Setup setups Runlog performance/soak test
85+
func (f *RunlogTest) Setup() error {
86+
chainlinkClients, err := environment.GetChainlinkClients(f.Environment)
87+
if err != nil {
88+
return err
89+
}
90+
nodeAddresses, err := actions.ChainlinkNodeAddresses(chainlinkClients)
91+
if err != nil {
92+
return err
93+
}
94+
adapter, err := environment.GetExternalAdapter(f.Environment)
95+
if err != nil {
96+
return err
97+
}
98+
f.chainlinkClients = chainlinkClients
99+
f.nodeAddresses = nodeAddresses
100+
f.adapter = adapter
101+
return f.deployContracts()
102+
}
103+
104+
func (f *RunlogTest) deployContract(c chan<- *ConsumerOraclePair) error {
105+
oracle, err := f.Deployer.DeployOracle(f.Wallets.Default(), f.Link.Address())
106+
if err != nil {
107+
return err
108+
}
109+
if err = oracle.SetFulfillmentPermission(f.Wallets.Default(), f.nodeAddresses[0].Hex(), true); err != nil {
110+
return err
111+
}
112+
consumer, err := f.Deployer.DeployAPIConsumer(f.Wallets.Default(), f.Link.Address())
113+
if err != nil {
114+
return err
115+
}
116+
err = consumer.Fund(f.Wallets.Default(), nil, big.NewFloat(20000))
117+
if err != nil {
118+
return err
119+
}
120+
c <- &ConsumerOraclePair{consumer: consumer, oracle: oracle}
121+
return nil
122+
}
123+
124+
func (f *RunlogTest) deployContracts() error {
125+
contractChan := make(chan *ConsumerOraclePair, f.TestOptions.NumberOfContracts)
126+
g := errgroup.Group{}
127+
128+
for i := 0; i < f.TestOptions.NumberOfContracts; i++ {
129+
g.Go(func() error {
130+
return f.deployContract(contractChan)
131+
})
132+
}
133+
if err := g.Wait(); err != nil {
134+
return err
135+
}
136+
close(contractChan)
137+
for contract := range contractChan {
138+
f.contractInstances = append(f.contractInstances, contract)
139+
}
140+
log.Warn().Int("Pairs", len(f.contractInstances)).Msg("Pairs")
141+
return f.Blockchain.WaitForEvents()
142+
}
143+
144+
func (f *RunlogTest) requestData() error {
145+
g := errgroup.Group{}
146+
for _, p := range f.contractInstances {
147+
p := p
148+
g.Go(func() error {
149+
jobUUIDReplaces := strings.Replace(p.jobUUID, "-", "", 4)
150+
var jobID [32]byte
151+
copy(jobID[:], jobUUIDReplaces)
152+
if err := p.consumer.CreateRequestTo(
153+
f.Wallets.Default(),
154+
p.oracle.Address(),
155+
jobID,
156+
big.NewInt(1e18),
157+
fmt.Sprintf("%s/five", f.adapter.ClusterURL()),
158+
"data,result",
159+
big.NewInt(100),
160+
); err != nil {
161+
return err
162+
}
163+
return nil
164+
})
165+
}
166+
return g.Wait()
167+
}
168+
169+
// Run runs Runlog performance/soak test
170+
func (f *RunlogTest) Run() error {
171+
if err := f.createChainlinkJobs(); err != nil {
172+
return err
173+
}
174+
ctx, cancel := context.WithTimeout(context.Background(), f.TestOptions.TestDuration)
175+
defer cancel()
176+
i := 1
177+
for {
178+
select {
179+
case <-ctx.Done():
180+
log.Warn().Msg("Test finished")
181+
return nil
182+
default:
183+
log.Warn().Int("RoundID", i).Msg("New round")
184+
if err := f.requestData(); err != nil {
185+
return err
186+
}
187+
if err := f.waitRoundEnd(i); err != nil {
188+
return err
189+
}
190+
i++
191+
}
192+
}
193+
}
194+
195+
func (f *RunlogTest) waitRoundEnd(roundID int) error {
196+
for _, p := range f.contractInstances {
197+
rc := contracts.NewRunlogRoundConfirmer(p.consumer, big.NewInt(int64(roundID)), f.TestOptions.RoundTimeout)
198+
f.Blockchain.AddHeaderEventSubscription(p.consumer.Address(), rc)
199+
}
200+
return f.Blockchain.WaitForEvents()
201+
}
202+
203+
func (f *RunlogTest) createChainlinkJobs() error {
204+
jobsChan := make(chan RunlogJobMap, len(f.contractInstances))
205+
g := errgroup.Group{}
206+
207+
bta := client.BridgeTypeAttributes{
208+
Name: "five",
209+
URL: fmt.Sprintf("%s/five", f.adapter.ClusterURL()),
210+
}
211+
if err := f.chainlinkClients[0].CreateBridge(&bta); err != nil {
212+
return err
213+
}
214+
os := &client.DirectRequestTxPipelineSpec{
215+
BridgeTypeAttributes: bta,
216+
DataPath: "data,result",
217+
}
218+
ost, err := os.String()
219+
if err != nil {
220+
return err
221+
}
222+
223+
for _, p := range f.contractInstances {
224+
p := p
225+
g.Go(func() error {
226+
jobUUID := uuid.NewV4()
227+
p.jobUUID = jobUUID.String()
228+
_, err := f.chainlinkClients[0].CreateJob(&client.DirectRequestJobSpec{
229+
Name: "direct_request",
230+
ContractAddress: p.oracle.Address(),
231+
ExternalJobID: jobUUID.String(),
232+
ObservationSource: ost,
233+
})
234+
if err != nil {
235+
return err
236+
}
237+
return nil
238+
})
239+
}
240+
if err := g.Wait(); err != nil {
241+
return err
242+
}
243+
close(jobsChan)
244+
245+
for jobMap := range jobsChan {
246+
for contractAddr, m := range jobMap {
247+
if _, ok := f.jobMap[contractAddr]; !ok {
248+
f.jobMap[contractAddr] = map[client.Chainlink]string{}
249+
}
250+
for k, v := range m {
251+
f.jobMap[contractAddr][k] = v
252+
}
253+
}
254+
}
255+
return nil
256+
}

0 commit comments

Comments
 (0)