Skip to content

Commit cc23fb4

Browse files
authored
Merge pull request #16 from tellor-io/fix/goroutine-tracking
Fix/goroutine tracking and Gas Estimation
2 parents 93cce93 + c945f6c commit cc23fb4

8 files changed

Lines changed: 631 additions & 52 deletions

File tree

cmd/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"os/signal"
88
"syscall"
9+
"time"
910

1011
"github.com/joho/godotenv"
1112
"github.com/rs/zerolog"
@@ -126,6 +127,7 @@ func init() {
126127
rootCmd.Flags().Uint32("auto-unbonding-frequency", 0, "Enable automatic unbonding every N days (0 = disabled, 1 - 21 days = valid")
127128
rootCmd.Flags().Uint32("auto-unbonding-amount", 0, "Amount of tokens in loya to unbond each unbonding transaction (0 = disabled)")
128129
rootCmd.Flags().String("auto-unbonding-max-stake-percentage", "0.0", "Maximum percentage of stake to unbond each unbonding transaction (0 = disabled, 1.0 = 100%). If unbonding amount exceeds this percentage, we will skip the unbonding transaction until it exceeds this percentage again.")
130+
rootCmd.Flags().Duration("refresh-gas-estimates-interval", 12*time.Hour, "Interval for resetting cached gas estimates and gas-adjustment levels (<=0 disables)")
129131

130132
// Marking required flags
131133
if err := rootCmd.MarkFlagRequired(flags.FlagHome); err != nil {

pricefeed/client/client.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ func (c *Client) start(ctx context.Context,
137137
exchangeIdToExchangeDetails map[types.ExchangeId]types.ExchangeQueryDetails,
138138
subTaskRunner SubTaskRunner,
139139
) (err error) {
140+
// Release daemonStartup on any exit before the normal "startup complete" point (avoids Stop() blocking forever).
141+
startupCompleted := false
142+
defer func() {
143+
if !startupCompleted {
144+
c.daemonStartup.Done()
145+
}
146+
}()
147+
140148
// 1. Establish connections to gRPC servers.
141149
queryConn, err := grpcClient.NewTcpConnection(ctx, grpcAddress)
142150
if err != nil {
@@ -246,6 +254,7 @@ func (c *Client) start(ctx context.Context,
246254
// Now that all persistent subtasks have been started and all tickers and stop channels are created,
247255
// signal that the startup process is complete. This needs to be called before entering the
248256
// price updater loop, which loops indefinitely until the daemon is stopped.
257+
startupCompleted = true
249258
c.daemonStartup.Done()
250259

251260
pricefeedClient := servertypes.NewPriceFeedServiceClient(daemonConn)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package client
2+
3+
import (
4+
"errors"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/require"
9+
daemonflags "github.com/tellor-io/layer-daemons/flags"
10+
"github.com/tellor-io/layer-daemons/mocks"
11+
"github.com/tellor-io/layer-daemons/pricefeed/client/types"
12+
"github.com/tellor-io/layer-daemons/testutil/constants"
13+
grpc_util "github.com/tellor-io/layer-daemons/testutil/grpc"
14+
15+
"cosmossdk.io/log"
16+
)
17+
18+
// TestStop_CompletesWhenStartFailsBeforeDaemonStartupDone guards the contract that
19+
// daemonStartup must be released on every early return from start(); otherwise Stop()
20+
// blocks forever on daemonStartup.Wait().
21+
func TestStop_CompletesWhenStartFailsBeforeDaemonStartupDone(t *testing.T) {
22+
t.Parallel()
23+
24+
tests := map[string]struct {
25+
mockGrpcClient *mocks.GrpcClient
26+
exchangeIdToQueryConfig map[types.ExchangeId]*types.ExchangeQueryConfig
27+
exchangeIdToExchangeDetails map[types.ExchangeId]types.ExchangeQueryDetails
28+
wantErrContains string
29+
}{
30+
"tcp_connection_fails": {
31+
mockGrpcClient: grpc_util.GenerateMockGrpcClientWithOptionalTcpConnectionErrors(
32+
errors.New(connectionFailsErrorMsg),
33+
nil,
34+
false,
35+
),
36+
wantErrContains: connectionFailsErrorMsg,
37+
},
38+
"grpc_connection_fails": {
39+
mockGrpcClient: grpc_util.GenerateMockGrpcClientWithOptionalGrpcConnectionErrors(
40+
errors.New(connectionFailsErrorMsg),
41+
nil,
42+
false,
43+
),
44+
wantErrContains: connectionFailsErrorMsg,
45+
},
46+
"empty_exchange_config": {
47+
mockGrpcClient: grpc_util.GenerateMockGrpcClientWithOptionalGrpcConnectionErrors(
48+
nil,
49+
nil,
50+
true,
51+
),
52+
exchangeIdToQueryConfig: map[types.ExchangeId]*types.ExchangeQueryConfig{},
53+
exchangeIdToExchangeDetails: map[types.ExchangeId]types.ExchangeQueryDetails{},
54+
wantErrContains: "exchangeIds must not be empty",
55+
},
56+
}
57+
58+
for name, tc := range tests {
59+
t.Run(name, func(t *testing.T) {
60+
t.Parallel()
61+
62+
cfg := tc.exchangeIdToQueryConfig
63+
details := tc.exchangeIdToExchangeDetails
64+
if cfg == nil {
65+
cfg = constants.TestExchangeQueryConfigs
66+
}
67+
if details == nil {
68+
details = constants.TestExchangeIdToExchangeQueryDetails
69+
}
70+
71+
faketaskRunner := FakeSubTaskRunner{}
72+
73+
client := newClient(log.NewNopLogger())
74+
err := client.start(
75+
grpc_util.Ctx,
76+
daemonflags.GetDefaultDaemonFlags(),
77+
grpc_util.TcpEndpoint,
78+
tc.mockGrpcClient,
79+
[]types.MarketParam{},
80+
cfg,
81+
details,
82+
&faketaskRunner,
83+
)
84+
require.Error(t, err)
85+
require.Contains(t, err.Error(), tc.wantErrContains)
86+
87+
stopDone := make(chan struct{})
88+
go func() {
89+
client.Stop()
90+
close(stopDone)
91+
}()
92+
93+
select {
94+
case <-stopDone:
95+
case <-time.After(2 * time.Second):
96+
t.Fatal("Stop() blocked — daemonStartup was likely not released on failed start()")
97+
}
98+
})
99+
}
100+
}

reporter/client/client.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ type Client struct {
7575
logger log.Logger
7676
txChan chan TxChannelInfo
7777
PriceGuard *PriceGuard
78+
// Gas estimate refresh interval; <=0 disables periodic refresh.
79+
refreshGasEstimatesInterval time.Duration
80+
gasEstimator *gasEstimateState
7881

7982
// Resources that need cleanup
8083
grpcConn *grpc.ClientConn
@@ -101,6 +104,16 @@ func NewClient(logger log.Logger, valGasMin string) *Client {
101104
logger: logger,
102105
minGasFee: valGasMin,
103106
txChan: txChan,
107+
gasEstimator: newGasEstimateState(map[string]gasBucketConfig{
108+
bridgeGasBucketKey: {
109+
levels: []float64{1.75, 2.0},
110+
baseIdx: 0,
111+
},
112+
defaultNonBridgeBucketConfigKey: {
113+
levels: []float64{1.25, 1.6, 2.0},
114+
baseIdx: 0,
115+
},
116+
}),
104117
}
105118
}
106119

@@ -190,6 +203,7 @@ func (c *Client) Start(
190203
autoUnbondingFrequency := viper.GetUint32("auto-unbonding-frequency")
191204
autoUnbondingAmount := viper.GetUint32("auto-unbonding-amount")
192205
autoUnbondingMaxStakePercentage := viper.GetString("auto-unbonding-max-stake-percentage")
206+
c.refreshGasEstimatesInterval = viper.GetDuration("refresh-gas-estimates-interval")
193207

194208
if autoUnbondingFrequency > 0 {
195209
if autoUnbondingAmount == 0 {
@@ -224,6 +238,11 @@ func (c *Client) Start(
224238
} else {
225239
c.logger.Info("Auto unbonding disabled")
226240
}
241+
if c.refreshGasEstimatesInterval > 0 {
242+
c.logger.Info("Periodic gas estimate refresh enabled", "interval", c.refreshGasEstimatesInterval.String())
243+
} else {
244+
c.logger.Info("Periodic gas estimate refresh disabled")
245+
}
227246

228247
c.cosmosCtx = c.cosmosCtx.WithChainID(chainId)
229248
c.cosmosCtx = c.cosmosCtx.WithHomeDir(homeDir)
@@ -332,9 +351,31 @@ func StartReporterDaemonTaskLoop(
332351
wg.Add(1)
333352
go client.AutoUnbondStakePeriodically(ctx, wg)
334353

354+
if client.refreshGasEstimatesInterval > 0 {
355+
wg.Add(1)
356+
go client.RefreshGasEstimatesPeriodically(ctx, wg)
357+
}
358+
335359
wg.Wait()
336360
}
337361

362+
func (c *Client) RefreshGasEstimatesPeriodically(ctx context.Context, wg *sync.WaitGroup) {
363+
defer wg.Done()
364+
ticker := time.NewTicker(c.refreshGasEstimatesInterval)
365+
defer ticker.Stop()
366+
367+
for {
368+
select {
369+
case <-ctx.Done():
370+
c.logger.Debug("RefreshGasEstimatesPeriodically: context canceled, exiting")
371+
return
372+
case <-ticker.C:
373+
c.logger.Info("Refreshing gas estimate buckets to base levels")
374+
c.resetAllGasLevelsToBase()
375+
}
376+
}
377+
}
378+
338379
func (c *Client) checkReporter(ctx context.Context) bool {
339380
c.logger.Info("Checking if reporter is created", "address", c.accAddr.String())
340381

0 commit comments

Comments
 (0)