Skip to content

Commit 52aec00

Browse files
saishibunbDoc
andauthored
fix(stats): seed account counter with long deadline + retry; never scan on request path (#94)
The one-time account/DID seed called CountAccounts, whose immudb Count over the 'address:' prefix is hard-capped at 30s (Accounts_helper.go) and now exceeds it on the live accounts DB -> DeadlineExceeded. Result: the counter never seeded, and getStats' fallback re-ran the same 30s Count per request and errored the whole /api/stats response. - GetCountofRecords takes a countTimeout (<=0 => 30s default); GetMainDBCount/GetAccountsDBCount pass 30s (unchanged). New CountBuilder.GetAccountsDBCountWithTimeout + DB_OPs.CountAccountsWithTimeout expose a caller-chosen deadline. - The one-time seed now uses a 5-min per-attempt deadline and retries with exponential backoff (30s..10m) until it succeeds or the node shuts down; it runs off the request path so a long count never delays startup. - getStats reads ONLY the sqlite counter (O(1)); it no longer falls back to the immudb Count on the request path. Until the background seed populates the counter it reports 0 (transient) instead of erroring the endpoint. Verified: gofmt clean; DB_OPs + explorer build; go vet ./ (package main) exit 0. Co-authored-by: Doc <doc@local>
1 parent b737079 commit 52aec00

4 files changed

Lines changed: 69 additions & 33 deletions

File tree

DB_OPs/Accounts_helper.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,11 @@ func CloseAccountsImmuClient(PooledConnection *config.PooledConnection) error {
8989

9090
// Adapter function to get the count of records using the Native ImmuDB API
9191
// Get the count of accounts uisng immudb native apis
92-
func GetCountofRecords(PooledConnection *config.PooledConnection, ConnType int, prefix string) (int, error) {
92+
// GetCountofRecords counts keys with the given prefix via immudb's Count API.
93+
// countTimeout bounds the Count RPC; pass <= 0 for the default 30s. Large
94+
// prefixes (e.g. all accounts) can exceed 30s, so off-request-path callers (the
95+
// one-time stats seed) pass a longer budget.
96+
func GetCountofRecords(PooledConnection *config.PooledConnection, ConnType int, prefix string, countTimeout time.Duration) (int, error) {
9397
var err error
9498
var shouldReturnConnection = false
9599

@@ -154,7 +158,10 @@ func GetCountofRecords(PooledConnection *config.PooledConnection, ConnType int,
154158
}
155159
}
156160

157-
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
161+
if countTimeout <= 0 {
162+
countTimeout = 30 * time.Second
163+
}
164+
ctx, cancel := context.WithTimeout(context.Background(), countTimeout)
158165
defer cancel()
159166

160167
// Use immudb Count API — much faster and simpler
@@ -201,9 +208,16 @@ func (cb CountBuilder) Build() (*CountBuilder, error) {
201208
}
202209

203210
func (cb CountBuilder) GetMainDBCount(prefix string) (int, error) {
204-
return GetCountofRecords(nil, MainImmuConn, prefix)
211+
return GetCountofRecords(nil, MainImmuConn, prefix, 30*time.Second)
205212
}
206213

207214
func (cb CountBuilder) GetAccountsDBCount(prefix string) (int, error) {
208-
return GetCountofRecords(nil, AccountsImmuConn, prefix)
215+
return GetCountofRecords(nil, AccountsImmuConn, prefix, 30*time.Second)
216+
}
217+
218+
// GetAccountsDBCountWithTimeout is GetAccountsDBCount with a caller-chosen
219+
// deadline for the immudb Count — used by the one-time stats seed, which runs
220+
// off the request path and can allow minutes on a large accounts DB.
221+
func (cb CountBuilder) GetAccountsDBCountWithTimeout(prefix string, countTimeout time.Duration) (int, error) {
222+
return GetCountofRecords(nil, AccountsImmuConn, prefix, countTimeout)
209223
}

DB_OPs/account_immuclient.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,14 @@ func CountAccounts(PooledConnection *config.PooledConnection) (int, error) {
13651365
return count, nil
13661366
}
13671367

1368+
// CountAccountsWithTimeout is CountAccounts with a caller-chosen deadline for the
1369+
// underlying immudb Count. The one-time explorer-stats seed uses this: it runs
1370+
// off the request path and can allow minutes on a large accounts DB instead of
1371+
// failing at the default 30s.
1372+
func CountAccountsWithTimeout(countTimeout time.Duration) (int, error) {
1373+
return CountBuilder{}.GetAccountsDBCountWithTimeout(Prefix, countTimeout)
1374+
}
1375+
13681376
// GetTransactionsByDID retrieves all transactions associated with a given DID
13691377
// This implementation iterates through all blocks to find matching transactions,
13701378
// which is more efficient than fetching each transaction individually.

explorer/BlockOps.go

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -595,23 +595,18 @@ func (s *ImmuDBServer) getStats(c *gin.Context) {
595595
return nil
596596
}, local.AddToWaitGroup(GRO.ExplorerBlockOpsWaitGroup))
597597

598-
// Get total DIDs/accounts in a goroutine. Prefer the maintained sqlite
599-
// counter (O(1)); fall back to the immudb prefix count only when the counter
600-
// has not been seeded yet (first boot before the one-time seed completes).
598+
// Get total DIDs/accounts from the maintained sqlite counter only (O(1)).
599+
// Deliberately does NOT fall back to the immudb prefix Count on the request
600+
// path: that Count is O(n) and can exceed its deadline on a large accounts DB,
601+
// which would fail the whole stats response. Until the one-time background
602+
// seed populates the counter, report 0 (transient) rather than erroring.
601603
BlockOpsLocalGRO.Go(GRO.ExplorerBlockOpsThread, func(ctx context.Context) error {
602-
if n, seeded, err := txindex.GetAccountCount(ctx); err == nil && seeded {
603-
mu.Lock()
604-
stats.TotalDIDs = n
605-
mu.Unlock()
606-
return nil
607-
}
608-
totalDIDs, err := DB_OPs.CountAccounts(&s.accountsdb)
604+
n, _, err := txindex.GetAccountCount(ctx)
609605
if err != nil {
610-
handleErr(fmt.Errorf("failed to count DIDs: %w", err))
611-
return fmt.Errorf("failed to count DIDs: %w", err)
606+
n = 0 // counter unavailable/unseeded — never block the endpoint on a scan
612607
}
613608
mu.Lock()
614-
stats.TotalDIDs = int64(totalDIDs)
609+
stats.TotalDIDs = n
615610
mu.Unlock()
616611
return nil
617612
}, local.AddToWaitGroup(GRO.ExplorerBlockOpsWaitGroup))

main.go

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,28 +1006,47 @@ func main() {
10061006
}()
10071007
})
10081008
// One-time seed: indexing the existing DIDs/accounts is a one-shot activity.
1009-
// Only run the expensive immudb CountAccounts if the counter has never been
1010-
// seeded; once present it is maintained by the increments above. Runs in a
1011-
// goroutine so a first-boot seed never delays startup.
1009+
// Only run the expensive immudb Count if the counter has never been seeded;
1010+
// once present it is maintained by the increments above. Runs in a goroutine
1011+
// so a first-boot seed never delays startup, and retries with backoff: the
1012+
// immudb Count over the accounts prefix can exceed the default 30s on a large
1013+
// DB or under load, so the seed uses a long per-attempt deadline and keeps
1014+
// retrying until it succeeds (or the node shuts down).
10121015
go func() {
1013-
_, seeded, err := txindex.GetAccountCount(context.Background())
1014-
if err != nil {
1016+
if _, seeded, err := txindex.GetAccountCount(context.Background()); err != nil {
10151017
log.Debug().Err(err).Msg("[stats] account counter unavailable; skipping one-time seed")
10161018
return
1017-
}
1018-
if seeded {
1019+
} else if seeded {
10191020
return // already indexed once — keep incrementing
10201021
}
1021-
n, err := DB_OPs.CountAccounts(nil)
1022-
if err != nil {
1023-
log.Warn().Err(err).Msg("[stats] one-time account/DID count seed failed; will retry next boot")
1024-
return
1025-
}
1026-
if err := txindex.SetAccountCount(context.Background(), int64(n)); err != nil {
1027-
log.Warn().Err(err).Msg("[stats] failed to persist seeded account/DID count")
1028-
return
1022+
backoff := 30 * time.Second
1023+
for attempt := 1; ; attempt++ {
1024+
// Re-check: another path (e.g. a manual reseed) may have seeded it.
1025+
if _, seeded, _ := txindex.GetAccountCount(context.Background()); seeded {
1026+
return
1027+
}
1028+
// Long per-attempt deadline — this runs off the request path.
1029+
n, err := DB_OPs.CountAccountsWithTimeout(5 * time.Minute)
1030+
if err == nil {
1031+
if serr := txindex.SetAccountCount(context.Background(), int64(n)); serr != nil {
1032+
log.Warn().Err(serr).Msg("[stats] failed to persist seeded account/DID count")
1033+
} else {
1034+
log.Info().Int("count", n).Int("attempt", attempt).Msg("[stats] account/DID counter seeded (one-time)")
1035+
return
1036+
}
1037+
} else {
1038+
log.Warn().Err(err).Int("attempt", attempt).Dur("retry_in", backoff).
1039+
Msg("[stats] one-time account/DID count seed failed; retrying")
1040+
}
1041+
select {
1042+
case <-ctx.Done():
1043+
return
1044+
case <-time.After(backoff):
1045+
}
1046+
if backoff < 10*time.Minute {
1047+
backoff *= 2
1048+
}
10291049
}
1030-
log.Info().Int("count", n).Msg("[stats] account/DID counter seeded (one-time)")
10311050
}()
10321051

10331052
// ── Account Sync Worker (Redis Stream) ───────────────────────────────────

0 commit comments

Comments
 (0)