Skip to content

Commit 080f2c1

Browse files
authored
replace ping liveness probe (#1799)
Signed-off-by: Rado M <radkomih@gmail.com>
1 parent 80fb5c7 commit 080f2c1

6 files changed

Lines changed: 421 additions & 8 deletions

File tree

sdk/account_info_query_unit_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,65 @@ func TestUnitAccountInfoQueryMock(t *testing.T) {
146146
require.NoError(t, err)
147147
require.Equal(t, HbarFromTinybar(2), cost)
148148
}
149+
150+
// A paid query on a client without an operator must fail cleanly instead of panicking while
151+
// building the payment.
152+
func TestUnitAccountInfoQueryExecuteWithoutOperator(t *testing.T) {
153+
t.Parallel()
154+
155+
client, server := NewMockClientAndServer([][]interface{}{{}})
156+
defer server.Close()
157+
158+
client.operator = nil
159+
160+
// An explicit payment skips the cost lookup and goes straight to payment signing.
161+
_, err := NewAccountInfoQuery().
162+
SetAccountID(AccountID{Account: 1800}).
163+
SetNodeAccountIDs([]AccountID{{Account: 3}}).
164+
SetQueryPayment(NewHbar(1)).
165+
Execute(client)
166+
require.ErrorIs(t, err, errNoClientProvided)
167+
}
168+
169+
// A COST_ANSWER query is unpaid, so it must go out without a payment even on a query object a
170+
// previous Execute already attached the client to - reaching for the absent operator there would
171+
// panic while building the payment.
172+
func TestUnitAccountInfoQueryGetCostAfterExecuteWithoutOperator(t *testing.T) {
173+
t.Parallel()
174+
175+
var captured []*services.Query
176+
client, server := NewMockClientAndServer([][]interface{}{{_CostAnswer(services.ResponseCodeEnum_OK, &captured)}})
177+
defer server.Close()
178+
179+
client.operator = nil
180+
181+
query := NewAccountInfoQuery().
182+
SetAccountID(AccountID{Account: 1800}).
183+
SetNodeAccountIDs([]AccountID{{Account: 3}})
184+
185+
// Fails for want of an operator, but leaves the client on the query.
186+
_, err := query.Execute(client)
187+
require.ErrorIs(t, err, errNoClientProvided)
188+
require.NotNil(t, query.client)
189+
190+
cost, err := query.GetCost(client)
191+
require.NoError(t, err)
192+
require.Equal(t, HbarFromTinybar(25), cost)
193+
194+
// The failed Execute must not have reached the network, so this is the cost lookup.
195+
require.Len(t, captured, 1)
196+
info, ok := captured[0].Query.(*services.Query_CryptoGetInfo)
197+
require.True(t, ok)
198+
require.Equal(t, services.ResponseType_COST_ANSWER, info.CryptoGetInfo.Header.ResponseType)
199+
require.Nil(t, info.CryptoGetInfo.Header.Payment, "a COST_ANSWER query must not attach a payment transaction")
200+
}
201+
202+
// GetCost must fail cleanly without a client.
203+
func TestUnitAccountInfoQueryGetCostNilClient(t *testing.T) {
204+
t.Parallel()
205+
206+
_, err := NewAccountInfoQuery().
207+
SetAccountID(AccountID{Account: 1800}).
208+
GetCost(nil)
209+
require.ErrorIs(t, err, errNoClientProvided)
210+
}

sdk/client.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -749,15 +749,22 @@ func (client *Client) GetOperatorPublicKey() PublicKey {
749749
return PublicKey{}
750750
}
751751

752-
// Ping sends an AccountBalanceQuery to the specified _Node returning nil if no
753-
// problems occur. Otherwise, an error representing the status of the _Node will
754-
// be returned.
752+
// treasuryAccountNum is the treasury system account, present on every network from genesis.
753+
const treasuryAccountNum = 2
754+
755+
// Ping checks the liveness of the given consensus node, returning nil if the node is reachable.
756+
//
757+
// It sends a COST_ANSWER getAccountInfo query for the treasury account, which a healthy node answers
758+
// with the query fee without executing it: nothing is charged and no operator is required.
759+
//
760+
// The probe follows the client's retry settings. A node already in backoff is never contacted, so
761+
// Ping fails without reaching the network; readmission happens when the backoff elapses, not on a
762+
// successful Ping.
755763
func (client *Client) Ping(nodeID AccountID) error {
756-
_, err := NewAccountBalanceQuery().
764+
_, err := NewAccountInfoQuery().
765+
SetAccountID(AccountID{Account: treasuryAccountNum}).
757766
SetNodeAccountIDs([]AccountID{nodeID}).
758-
SetAccountID(client.GetOperatorAccountID()).
759-
Execute(client)
760-
767+
GetCost(client)
761768
return err
762769
}
763770

sdk/client_e2e_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,52 @@ func TestClientIntegrationForMirrorNetworkWithShardAndRealm(t *testing.T) {
162162
require.Nil(t, client)
163163
assert.Contains(t, err.Error(), "failed to query address book: no healthy nodes")
164164
}
165+
166+
// Every node of a live network answers the probe, and PingAll probes every node.
167+
func TestIntegrationClientPingAllNetworkNodes(t *testing.T) { // nolint
168+
t.Parallel()
169+
env := NewIntegrationTestEnv(t)
170+
defer CloseIntegrationTestEnv(env, nil)
171+
172+
for address, nodeID := range env.Client.GetNetwork() {
173+
require.NoError(t, env.Client.Ping(nodeID), "node %s at %s should be reachable", nodeID, address)
174+
}
175+
176+
// PingAll swallows errors, so a rising use count is the proof it probed - valid only if healthy first.
177+
usesBefore := make(map[AccountID]int64)
178+
for address, nodeID := range env.Client.GetNetwork() {
179+
node, ok := env.Client.network._GetNodeForAccountID(nodeID)
180+
require.True(t, ok)
181+
require.True(t, node._IsHealthy(), "node %s at %s should be healthy before PingAll", nodeID, address)
182+
usesBefore[nodeID] = node._GetUseCount()
183+
}
184+
185+
env.Client.PingAll()
186+
for address, nodeID := range env.Client.GetNetwork() {
187+
node, ok := env.Client.network._GetNodeForAccountID(nodeID)
188+
require.True(t, ok)
189+
assert.Greater(t, node._GetUseCount(), usesBefore[nodeID],
190+
"PingAll must probe node %s at %s", nodeID, address)
191+
// Only transport failures back a node off, so this catches unreachability, not every failure.
192+
assert.True(t, node._IsHealthy(), "node %s at %s should stay healthy after PingAll", nodeID, address)
193+
}
194+
}
195+
196+
// The probe works against a live node with no operator configured: a COST_ANSWER query is free and
197+
// unsigned, so it attaches no payment.
198+
func TestIntegrationClientPingWithoutOperator(t *testing.T) { // nolint
199+
t.Parallel()
200+
env := NewIntegrationTestEnv(t)
201+
defer CloseIntegrationTestEnv(env, nil)
202+
203+
client, err := ClientForNetworkV2(env.Client.GetNetwork())
204+
require.NoError(t, err)
205+
defer func() {
206+
require.NoError(t, client.Close())
207+
}()
208+
require.Nil(t, client.operator)
209+
210+
for _, nodeID := range client.GetNetwork() {
211+
require.NoError(t, client.Ping(nodeID))
212+
}
213+
}

0 commit comments

Comments
 (0)