mirror of
https://github.com/luxfi/indexer.git
synced 2026-09-24 00:25:37 +00:00
fix(cmd/explorer): mount API after DB+schema ready, surface init errors
The 30-second mount cap was firing before the EVM indexer wrote its first
block — for chains whose RPC was slow or whose subnet had only a handful of
blocks, the standalone API never mounted and requests fell through to the
SPA placeholder. Three fixes:
1. Pre-init the EVM schema in runChain *before* starting the indexing loop.
Run() also calls Init(), so this is idempotent — but doing it eagerly
means the API server can detect tables on its first probe instead of
waiting on a slow first GetLatestBlock RPC roundtrip.
2. Replace the 30-attempt cap with retry-until-ctx-done and log the actual
failure reason every five minutes. Bad RPC URLs and missing data dirs
are now visible in the log instead of silently disappearing.
3. Mount each chain at both /v1/indexer/* and /v1/explorer/* (legacy alias)
so existing Blockscout-fork frontends keep working without an ingress
middleware. Default chain also gets a /v1/indexer/{slug}/* alias for
hostname-based routing.
Also: PORT env (K8s convention) now feeds HTTP_ADDR if set, and
CHAIN_RPC_URL (LiquidExplorer operator) joins RPC_ENDPOINT as a single-chain
RPC source.
This commit is contained in:
+91
-12
@@ -127,7 +127,14 @@ func main() {
|
||||
*dataDir = envOr("DATA_DIR", filepath.Join(homeDir(), ".explorer", "data"))
|
||||
}
|
||||
if *httpAddr == "" {
|
||||
*httpAddr = envOr("HTTP_ADDR", ":8090")
|
||||
// HTTP_ADDR wins; otherwise honour the K8s-style PORT env (just a number).
|
||||
if addr := os.Getenv("HTTP_ADDR"); addr != "" {
|
||||
*httpAddr = addr
|
||||
} else if p := os.Getenv("PORT"); p != "" {
|
||||
*httpAddr = ":" + p
|
||||
} else {
|
||||
*httpAddr = ":8090"
|
||||
}
|
||||
}
|
||||
|
||||
// Load config: either YAML multi-chain or CLI single-chain
|
||||
@@ -138,7 +145,16 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
} else if rpc := envOr("RPC_ENDPOINT", *rpcEndpoint); rpc != "" {
|
||||
} else if rpc := func() string {
|
||||
// RPC source priority: --rpc flag, RPC_ENDPOINT, CHAIN_RPC_URL (LiquidExplorer operator).
|
||||
if *rpcEndpoint != "" {
|
||||
return *rpcEndpoint
|
||||
}
|
||||
if v := os.Getenv("RPC_ENDPOINT"); v != "" {
|
||||
return v
|
||||
}
|
||||
return os.Getenv("CHAIN_RPC_URL")
|
||||
}(); rpc != "" {
|
||||
// Single-chain mode
|
||||
if *chainName == "" {
|
||||
*chainName = envOr("CHAIN_NAME", "EVM Chain")
|
||||
@@ -243,14 +259,18 @@ func main() {
|
||||
|
||||
// Start HTTP server with explorer API
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
||||
healthHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
})
|
||||
}
|
||||
mux.HandleFunc("GET /health", healthHandler)
|
||||
mux.HandleFunc("GET /healthz", healthHandler) // K8s-style alias for operator probes
|
||||
|
||||
// Mount explorer API for each chain — wait for DB before starting server
|
||||
// Mount explorer API for each chain — wait for DB and schema before starting server.
|
||||
// Retries until ctx is done (no fixed timeout) and logs every failure reason
|
||||
// so misconfigurations (bad RPC, missing dir, schema init failure) are visible.
|
||||
log.Printf(" http: %s", cfg.HTTPAddr)
|
||||
var apiReady sync.WaitGroup
|
||||
for _, c := range enabled {
|
||||
@@ -258,11 +278,26 @@ func main() {
|
||||
apiReady.Add(1)
|
||||
go func(chain ChainConfig, path string) {
|
||||
defer apiReady.Done()
|
||||
for i := 0; i < 30; i++ {
|
||||
time.Sleep(time.Second)
|
||||
apiPrefix := "/v1/indexer"
|
||||
if !chain.Default {
|
||||
apiPrefix = fmt.Sprintf("/v1/indexer/%s", chain.Slug)
|
||||
apiPrefix := "/v1/indexer"
|
||||
if !chain.Default {
|
||||
apiPrefix = fmt.Sprintf("/v1/indexer/%s", chain.Slug)
|
||||
}
|
||||
var lastErr error
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
for attempt := 0; ; attempt++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
// Wait for DB file to exist.
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
lastErr = fmt.Errorf("db file not yet created: %w", err)
|
||||
if time.Now().After(deadline) {
|
||||
log.Printf("[%s] API mount waiting on DB file %s (attempt %d): %v", chain.Slug, path, attempt, lastErr)
|
||||
deadline = time.Now().Add(5 * time.Minute)
|
||||
}
|
||||
continue
|
||||
}
|
||||
apiSrv, err := explorer.NewStandaloneServer(explorer.Config{
|
||||
IndexerDBPath: path,
|
||||
@@ -272,17 +307,51 @@ func main() {
|
||||
APIPrefix: apiPrefix,
|
||||
})
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
if time.Now().After(deadline) {
|
||||
log.Printf("[%s] API mount failed (attempt %d): %v", chain.Slug, attempt, err)
|
||||
deadline = time.Now().Add(5 * time.Minute)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Mount the configured prefix.
|
||||
mux.Handle(apiPrefix+"/", apiSrv.Handler())
|
||||
// Backward-compat: also serve at /v1/explorer/* (legacy prefix
|
||||
// used by older clients and docs). For non-default chains the
|
||||
// alias is /v1/explorer/{slug}/*.
|
||||
legacyPrefix := strings.Replace(apiPrefix, "/v1/indexer", "/v1/explorer", 1)
|
||||
if legacyPrefix != apiPrefix {
|
||||
legacySrv, _ := explorer.NewStandaloneServer(explorer.Config{
|
||||
IndexerDBPath: path,
|
||||
ChainID: chain.ChainID,
|
||||
ChainName: chain.Name,
|
||||
CoinSymbol: chain.CoinSymbol,
|
||||
APIPrefix: legacyPrefix,
|
||||
})
|
||||
if legacySrv != nil {
|
||||
mux.Handle(legacyPrefix+"/", legacySrv.Handler())
|
||||
}
|
||||
}
|
||||
// Default chain also mounts under its slug so /v1/indexer/cchain/*
|
||||
// works alongside /v1/indexer/*. Hostname-based routing in the
|
||||
// ingress can then pin a frontend to a specific chain.
|
||||
if chain.Default {
|
||||
log.Printf("[%s] API mounted at /v1/indexer/* (default)", chain.Slug)
|
||||
slugSrv, _ := explorer.NewStandaloneServer(explorer.Config{
|
||||
IndexerDBPath: path,
|
||||
ChainID: chain.ChainID,
|
||||
ChainName: chain.Name,
|
||||
CoinSymbol: chain.CoinSymbol,
|
||||
APIPrefix: fmt.Sprintf("/v1/indexer/%s", chain.Slug),
|
||||
})
|
||||
if slugSrv != nil {
|
||||
mux.Handle(fmt.Sprintf("/v1/indexer/%s/", chain.Slug), slugSrv.Handler())
|
||||
}
|
||||
log.Printf("[%s] API mounted at /v1/indexer/* and /v1/indexer/%s/* (default)", chain.Slug, chain.Slug)
|
||||
} else {
|
||||
log.Printf("[%s] API mounted at %s/*", chain.Slug, apiPrefix)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Printf("[%s] API not mounted — DB not ready after 30s", chain.Slug)
|
||||
}(c, dbPath)
|
||||
|
||||
if c.Default {
|
||||
@@ -395,6 +464,16 @@ func runChain(ctx context.Context, baseDir string, chain ChainConfig) {
|
||||
log.Printf("[%s] failed to create indexer: %v", chain.Slug, err)
|
||||
return
|
||||
}
|
||||
// Initialize the EVM schema *before* the indexing loop runs. The API
|
||||
// server polls for the schema and won't mount handlers until tables
|
||||
// exist — without this pre-init, the API for non-default chains gets
|
||||
// stuck behind a slow first RPC call and the frontend falls through
|
||||
// to the SPA placeholder.
|
||||
if err := idx.Init(ctx); err != nil {
|
||||
log.Printf("[%s] failed to init evm schema: %v", chain.Slug, err)
|
||||
return
|
||||
}
|
||||
log.Printf("[%s] evm schema initialized", chain.Slug)
|
||||
if err := idx.Run(ctx); err != nil && ctx.Err() == nil {
|
||||
log.Printf("[%s] indexer error: %v", chain.Slug, err)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ require (
|
||||
github.com/lib/pq v1.12.1
|
||||
github.com/luxfi/age v1.4.0
|
||||
github.com/luxfi/database v1.17.44
|
||||
github.com/luxfi/graph v0.1.0
|
||||
github.com/luxfi/zap v0.2.1
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/onsi/ginkgo/v2 v2.28.1
|
||||
@@ -49,7 +50,6 @@ require (
|
||||
github.com/luxfi/compress v0.0.5 // indirect
|
||||
github.com/luxfi/concurrent v0.0.3 // indirect
|
||||
github.com/luxfi/crypto v1.17.45 // indirect
|
||||
github.com/luxfi/graph v0.1.0 // indirect
|
||||
github.com/luxfi/ids v1.2.9 // indirect
|
||||
github.com/luxfi/log v1.4.1 // indirect
|
||||
github.com/luxfi/math v1.2.4 // indirect
|
||||
|
||||
@@ -134,7 +134,7 @@ github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnm
|
||||
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
||||
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
|
||||
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
|
||||
github.com/hanzoai/replicate v0.6.0 h1:5Rl34569IHJ5hXOvrkjbadfKNt/2rGZd83vhcSIRSPE=
|
||||
github.com/hanzoai/replicate v0.6.0 h1:YxjIynlo3iKtpf6GeQngeB5RbexIKPPKsZIoq5EHe3E=
|
||||
github.com/hanzoai/replicate v0.6.0/go.mod h1:PBvHNQ7cXBJejTWDWxXWvtUwjT5O66pTC7npxLWu8H0=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
|
||||
Reference in New Issue
Block a user