feat(price-pusher): allow multiple injective gRPC endpoints with failover#3703
feat(price-pusher): allow multiple injective gRPC endpoints with failover#37030xghost42 wants to merge 3 commits into
Conversation
…over Closes pyth-network#1014. The injective price pusher accepted a single `--grpc-endpoint`. Cosmos nodes are occasionally flaky, so operators ended up running one pusher process per endpoint just to get redundancy. Maintainer (jayantk) said on the issue that cycling chain endpoints was the right shape. ### Change - `--grpc-endpoint` is now `type: "array"`, accepting either repeated flag occurrences or a comma-separated list (the handler splits + trims each entry). A single endpoint still works exactly as before. Empty input is rejected up front with a clear message. - New `src/injective/endpoint-pool.ts`: - `EndpointPool` — round-robin cursor over a non-empty endpoint list, shared across every gRPC API helper in the pusher so a bad endpoint affects the whole instance, not just one method. - `withEndpointFailover(pool, fn)` — runs `fn(currentEndpoint)`, rotates on failure, walks every endpoint at most once before re-throwing the last error. - `InjectivePriceListener` + `InjectivePricePusher` constructors take `string | readonly string[]` for backward compatibility, wrap each gRPC call site (`ChainGrpcWasmApi.fetchSmartContractState`, `ChainGrpcAuthApi.fetchAccount`, `TxGrpcApi.broadcast`, `TxGrpcApi.simulate`) in `withEndpointFailover`. - README: short note on the new fallback-set syntax under the Injective example. ### Verification - `pnpm --filter @pythnetwork/price-pusher exec test-unit` — 8 cases, all pass: empty rejection, default cursor, round-robin wrap, single-endpoint no-op, success no-rotate, single-failure rotate, exhaust-and-throw, cursor-position respected by `withEndpointFailover`. - `pnpm --filter @pythnetwork/price-pusher exec tsc --noEmit` clean. - Wired `test:unit` into the package scripts so the workspace test runner picks the new file up. ### Out of scope The original issue also mentioned the Pyth price service (Hermes) gRPC endpoint as a possibly useful failover target. That's a different code path (HermesClient takes a single URL today), so I kept this PR focused on the chain-endpoint case the reporter named primarily. Happy to follow up on the Hermes side in a separate PR if the maintainers want it.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 7 Skipped Deployments
|
|
Pushed Heads-up on coordination: #3689 (mnemonic env var) also bumps to Re: the additional 5 findings hidden in the Devin Review console — I can't see them from the GitHub PR view, so happy to address any that surface here as separate comments. |
The exports map drives ts-duality --noEsm CJS compilation. Without an entry for src/injective/endpoint-pool.ts, dist/injective/endpoint-pool.cjs is not generated and dist/injective/injective.cjs fails at runtime when it requires ./endpoint-pool.js. Mirror the pattern used by the other injective files.
|
Pushed "./injective/endpoint-pool": {
"default": "./dist/injective/endpoint-pool.cjs",
"types": "./dist/injective/endpoint-pool.d.ts"
}Confirmed the pattern matches every other source file in Lockfile / dependencies untouched. |
Summary
Closes #1014.
The injective price pusher only accepted a single
--grpc-endpoint. Cosmos nodes are occasionally flaky, so operators ended up spinning up one pusher process per endpoint just to get redundancy. Maintainer (@jayantk) confirmed on the original issue that cycling chain endpoints was the right shape.This PR makes
--grpc-endpointa fallback set: the pusher round-robins to the next endpoint whenever a gRPC call fails, walking every endpoint at most once before re-throwing.Change
--grpc-endpointis nowtype: "array". Accepts either repeated flag occurrences (--grpc-endpoint a --grpc-endpoint b) or comma-separated values (--grpc-endpoint a,b,c). A single endpoint still works exactly as before. Empty input rejected up front with a clear message.apps/price_pusher/src/injective/endpoint-pool.ts:EndpointPool— round-robin cursor over a non-empty endpoint list, shared across every gRPC API helper in a pusher instance so a bad endpoint affects the whole pusher, not just one method.withEndpointFailover(pool, fn)— runsfn(currentEndpoint), rotates on failure, walks every endpoint at most once before re-throwing the last error.InjectivePriceListener+InjectivePricePusherconstructors takestring | readonly string[]for backward compatibility and wrap each gRPC call site (ChainGrpcWasmApi.fetchSmartContractState,ChainGrpcAuthApi.fetchAccount,TxGrpcApi.broadcast,TxGrpcApi.simulate) inwithEndpointFailover.Verification
pnpm --filter @pythnetwork/price-pusher exec test-unit— 8 cases, all pass:withEndpointFailoverreturns first successful result without rotatingpnpm --filter @pythnetwork/price-pusher exec tsc --noEmitclean.test:unitintopackage.jsonscripts so the workspace test runner picks the new file up.Out of scope
The original issue also mentioned the Pyth price service (Hermes) gRPC endpoint as a possibly useful failover target. That's a different code path (
HermesClienttakes a single URL today), so I kept this PR focused on the chain-endpoint case @ewoolsey named primarily. Happy to follow up on the Hermes side in a separate PR if the maintainers want it.