-
Notifications
You must be signed in to change notification settings - Fork 316
Check for staleness in each Hyperliquid websocket channel #3289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
72a777d
1e24b69
f7d7f13
10c6f14
0529c6a
d5d12a3
5f7f754
a30b42b
e690d57
23419c9
8f664de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,8 @@ | ||
| import asyncio | ||
| import json | ||
| from loguru import logger | ||
| import time | ||
| import websockets | ||
| from tenacity import retry, retry_if_exception_type, wait_exponential | ||
| from tenacity import retry, retry_if_exception_type, wait_fixed | ||
|
|
||
| from pusher.config import Config, STALE_TIMEOUT_SECONDS | ||
| from pusher.exception import StaleConnectionError | ||
|
|
@@ -41,11 +40,12 @@ async def subscribe_all(self): | |
| await asyncio.gather(*(self.subscribe_single(router_url) for router_url in self.lazer_urls)) | ||
|
|
||
| @retry( | ||
| retry=retry_if_exception_type((StaleConnectionError, websockets.ConnectionClosed)), | ||
| wait=wait_exponential(multiplier=1, min=1, max=10), | ||
| retry=retry_if_exception_type(Exception), | ||
| wait=wait_fixed(1), | ||
| reraise=True, | ||
| ) | ||
| async def subscribe_single(self, router_url): | ||
| logger.info("Starting Lazer listener loop: {}", router_url) | ||
| return await self.subscribe_single_inner(router_url) | ||
|
|
||
| async def subscribe_single_inner(self, router_url): | ||
|
|
@@ -66,8 +66,10 @@ async def subscribe_single_inner(self, router_url): | |
| data = json.loads(message) | ||
| self.parse_lazer_message(data) | ||
| except asyncio.TimeoutError: | ||
| logger.warning("LazerListener: No messages in {} seconds, reconnecting...", STALE_TIMEOUT_SECONDS) | ||
| raise StaleConnectionError(f"No messages in {STALE_TIMEOUT_SECONDS} seconds, reconnecting") | ||
| except websockets.ConnectionClosed: | ||
| logger.warning("LazerListener: Connection closed, reconnecting...") | ||
| raise | ||
| except json.JSONDecodeError as e: | ||
| logger.error("Failed to decode JSON message: {}", e) | ||
|
|
@@ -85,14 +87,14 @@ def parse_lazer_message(self, data): | |
| if data.get("type", "") != "streamUpdated": | ||
| return | ||
| price_feeds = data["parsed"]["priceFeeds"] | ||
| logger.debug("price_feeds: {}", price_feeds) | ||
| now = time.time() | ||
| timestamp = int(data["parsed"]["timestampUs"]) / 1_000_000.0 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we name the variable something like |
||
| logger.debug("price_feeds: {} timestamp: {}", price_feeds, timestamp) | ||
| for feed_update in price_feeds: | ||
| feed_id = feed_update.get("priceFeedId", None) | ||
| price = feed_update.get("price", None) | ||
| if feed_id is None or price is None: | ||
| continue | ||
| else: | ||
| self.lazer_state.put(feed_id, PriceUpdate(price, now)) | ||
| self.lazer_state.put(feed_id, PriceUpdate(price, timestamp)) | ||
| except Exception as e: | ||
| logger.error("parse_lazer_message error: {}", e) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ | |
| from pusher.price_state import PriceState | ||
| from pusher.publisher import Publisher | ||
| from pusher.metrics import Metrics | ||
| from pusher.user_limit_listener import UserLimitListener | ||
|
|
||
|
|
||
| def load_config(): | ||
|
|
@@ -52,13 +53,15 @@ async def main(): | |
| lazer_listener = LazerListener(config, price_state.lazer_state) | ||
| hermes_listener = HermesListener(config, price_state.hermes_state) | ||
| seda_listener = SedaListener(config, price_state.seda_state) | ||
| user_limit_listener = UserLimitListener(config, metrics, publisher.user_limit_address) | ||
|
|
||
| await asyncio.gather( | ||
| publisher.run(), | ||
| hyperliquid_listener.subscribe_all(), | ||
| lazer_listener.subscribe_all(), | ||
| hermes_listener.subscribe_all(), | ||
| seda_listener.run(), | ||
| user_limit_listener.run(), | ||
|
Comment on lines
58
to
+64
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the expected behavior if any of these tasks exit/raise? I guess the app crashes and we let k8s restart it? |
||
| ) | ||
| logger.info("Exiting hip-3-pusher..") | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,11 +85,14 @@ def get_prices(self, symbol_configs: dict[str, list[PriceSourceConfig]], oracle_ | |
| pxs = {} | ||
| for symbol in symbol_configs: | ||
| for source_config in symbol_configs[symbol]: | ||
| # find first valid price in the waterfall | ||
| px = self.get_price(source_config, oracle_update) | ||
| if px is not None: | ||
| pxs[f"{self.market_name}:{symbol}"] = str(px) | ||
| break | ||
| try: | ||
| # find first valid price in the waterfall | ||
| px = self.get_price(source_config, oracle_update) | ||
| if px is not None: | ||
| pxs[f"{self.market_name}:{symbol}"] = str(px) | ||
| break | ||
| except Exception as e: | ||
| logger.exception("get_price exception for symbol: {} source_config: {} error: {}", symbol, source_config, e) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For printing out expections use |
||
| return pxs | ||
|
|
||
| def get_price(self, price_source_config: PriceSourceConfig, oracle_update: OracleUpdate): | ||
|
|
@@ -125,10 +128,10 @@ def get_price_from_pair_source(self, base_source: PriceSource, quote_source: Pri | |
| if base_price is None: | ||
| return None | ||
| quote_price = self.get_price_from_single_source(quote_source) | ||
| if quote_price is None: | ||
| if quote_price is None or float(quote_price) == 0: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What edge case are we covering here? |
||
| return None | ||
|
|
||
| return str(round(float(base_price) / float(quote_price), 2)) | ||
| return str(float(base_price) / float(quote_price)) | ||
|
|
||
| def get_price_from_oracle_mid_average(self, symbol: str, oracle_update: OracleUpdate): | ||
| oracle_price = oracle_update.oracle.get(symbol) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will it stop retrying after some max retries? Probably a good idea to reraise and crash the app when the retries are exhausted
(Same for the other decorators)