Skip to content
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

Candles Cache Improvements #2590

Merged
merged 7 commits into from
Nov 26, 2024
Merged

Candles Cache Improvements #2590

merged 7 commits into from
Nov 26, 2024

Conversation

adamfraser
Copy link
Contributor

@adamfraser adamfraser commented Nov 20, 2024

Additions:

  • An orderbook mid price cache that holds a series of prices, any price older that 30 seconds is removed from the cache
  • Functionality to retrieve the median price from the series stored in the new orderbook mid prices cache
  • A roundtable job that fetches the current orderbook mid price for all markets and adds them to the new cache, this runs every 10 seconds
  • An in memory cache in ender that fetches the latest median price every 10 seconds
  • Candles orderbook mid price open / close values are set using the median prices stored in ender cache

Summary by CodeRabbit

  • New Features

    • Introduced caching for order book mid prices, enhancing data retrieval efficiency.
    • Added configuration options to control caching behavior and update intervals.
    • New Lua scripts for managing and retrieving mid prices in Redis.
  • Bug Fixes

    • Improved error handling for undefined prices in caching tasks.
  • Tests

    • Expanded test coverage for order book mid price caching and candle generation processes.
  • Documentation

    • Updated comments and documentation to reflect new functionalities and configurations.

Copy link
Contributor

coderabbitai bot commented Nov 20, 2024

Walkthrough

The changes in this pull request primarily involve enhancements to the handling and caching of order book mid prices across various modules. Key modifications include the introduction of new functions and scripts for managing prices, updates to existing test suites to accommodate these changes, and the addition of an in-memory cache for better performance. Additionally, configuration options related to caching behavior have been updated. The overall structure of the code remains intact, while functionality is expanded to support multiple market tickers efficiently.

Changes

File Change Summary
indexer/packages/postgres/src/index.ts Added export for loopHelpers from ./loops/loopHelper.
indexer/packages/redis/__tests__/caches/orderbook-mid-prices-cache.test.ts Renamed functions and updated tests to accommodate fetchAndCacheOrderbookMidPrices and getMedianPrices. Introduced mocks and expanded test cases for multiple tickers.
indexer/packages/redis/src/caches/orderbook-mid-prices-cache.ts Renamed setPrice to fetchAndCacheOrderbookMidPrices with updated parameters and internal logic. Renamed getMedianPrice to getMedianPrices with a new signature.
indexer/packages/redis/src/caches/scripts.ts Removed addMarketPriceScript and getMarketMedianScript. Added addOrderbookMidPricesScript and getOrderbookMidPricesScript.
indexer/packages/redis/src/scripts/add_market_price.lua Deleted Lua script managing price data in a Redis sorted set.
indexer/packages/redis/src/scripts/add_orderbook_mid_prices.lua Introduced new Lua script for managing market price data efficiently.
indexer/packages/redis/src/scripts/get_market_median_price.lua Deleted Lua script for calculating the median price from a sorted set.
indexer/packages/redis/src/scripts/get_orderbook_mid_prices.lua Introduced new Lua script to retrieve mid prices from a Redis cache for specified markets.
indexer/services/ender/__tests__/caches/orderbook-mid-price-memory-cache.test.ts Added unit tests for orderbook-mid-price-memory-cache, verifying retrieval and updates of mid prices.
indexer/services/ender/__tests__/lib/candles-generator.test.ts Updated tests to utilize new caching mechanism and ensure correct integration of order book mid prices into candles.
indexer/services/ender/src/caches/orderbook-mid-price-memory-cache.ts Introduced in-memory cache for median prices with functions for updating and retrieving prices.
indexer/services/ender/src/config.ts Added configuration variable ORDERBOOK_MID_PRICE_REFRESH_INTERVAL_MS for cache refresh interval.
indexer/services/ender/src/index.ts Added import for OrderbookMidPriceMemoryCache and invoked its start function during startup.
indexer/services/ender/src/lib/candles-generator.ts Refactored getOrderbookMidPriceMap to be synchronous, updating its usage in createOrUpdatePostgresCandles.
indexer/services/roundtable/__tests__/tasks/cache-orderbook-mid-prices.test.ts Introduced tests for caching order book mid prices, verifying both successful caching and handling of undefined prices.
indexer/services/roundtable/src/config.ts Added configuration LOOPS_ENABLED_CACHE_ORDERBOOK_MID_PRICES and updated LOOPS_INTERVAL_MS_CACHE_ORDERBOOK_MID_PRICES.
indexer/services/roundtable/src/index.ts Added import for cacheOrderbookMidPrices and integrated it into the start function based on configuration.
indexer/services/roundtable/src/tasks/cache-orderbook-mid-prices.ts Introduced runTask function to update OrderbookMidPricesCache with current mid prices for each market.

Possibly related PRs

Suggested labels

feature:indexer/affiliates

Suggested reviewers

  • dydxwill

🐰 In the meadow, where prices dance,
A cache was born, given a chance.
With loops and helpers, we fetch with glee,
Order books sing, as prices run free!
So hop along, let’s test and play,
In our code garden, we’ll brighten the day! 🌼


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@adamfraser adamfraser force-pushed the adam/candles-cache-main branch 2 times, most recently from dd7d0df to 40e67d0 Compare November 20, 2024 20:51
@adamfraser adamfraser force-pushed the adam/candles-cache-main branch from 40e67d0 to a774971 Compare November 20, 2024 22:11
@adamfraser adamfraser force-pushed the adam/candles-cache-main branch from a774971 to 354257b Compare November 20, 2024 23:31
Copy link
Contributor

@vincentwschau vincentwschau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic looks good, nits around missing types for most variables.

}

// Convert the prices to Big.js objects for precision
const bigPrices = prices.map((price) => Big(price));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type

indexer/services/roundtable/src/config.ts Outdated Show resolved Hide resolved
jest.useFakeTimers();
// Mock the getOrderBookMidPrice function for the ticker
const mockPrices = ['50000', '51000', '49000', '48000', '52000', '53000'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: type

@adamfraser adamfraser force-pushed the adam/candles-cache-main branch from 2254666 to 5212e1f Compare November 25, 2024 19:02
@adamfraser adamfraser force-pushed the adam/candles-cache-main branch from 5212e1f to 70421a1 Compare November 25, 2024 19:08
@adamfraser adamfraser force-pushed the adam/candles-cache-main branch from 70421a1 to 7a1bcc9 Compare November 25, 2024 19:08
@adamfraser adamfraser marked this pull request as ready for review November 25, 2024 20:37
@adamfraser adamfraser requested a review from a team as a code owner November 25, 2024 20:37
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

🧹 Outside diff range and nitpick comments (23)
indexer/packages/redis/src/scripts/get_orderbook_mid_prices.lua (2)

1-2: Enhance script documentation

The current documentation could be more comprehensive. Consider adding:

  • Expected format of cache keys
  • Description of the return value structure
  • Example usage
-- KEYS is an array of cache keys for a market
+-- Script to retrieve recent orderbook mid prices from Redis sorted sets
+-- 
+-- Input:
+--   KEYS: Array of cache keys for markets (e.g., "market:BTC-USD:mid_price")
+-- 
+-- Returns:
+--   Array of arrays, where each inner array contains up to 10 most recent prices
+--   for the corresponding market
+-- 
+-- Example:
+--   EVAL get_orderbook_mid_prices 2 market:BTC-USD:mid_price market:ETH-USD:mid_price

6-6: Consider adding performance optimization for bulk operations

The current implementation makes separate Redis calls for each key. For better performance with multiple keys, consider using MULTI/EXEC or pipelining if supported in your Redis setup.

-  local prices = redis.call("ZRANGE", key, 0, 9)
+  -- Pipeline multiple ZRANGE commands
+  redis.call("MULTI")
+  for _, key in ipairs(KEYS) do
+    redis.call("ZRANGE", key, 0, 9)
+  end
+  local all_prices = redis.call("EXEC")
indexer/packages/redis/src/scripts/add_orderbook_mid_prices.lua (3)

10-19: Consider making the time window configurable

The 30-second window is hardcoded, which might limit flexibility for different markets or testing scenarios. Consider making this configurable by passing it as an additional argument.

Suggested implementation:

-local thirtySeconds = 30
+-- Get the time window from the second-to-last argument
+local timeWindow = tonumber(ARGV[numArgs-1])
+if not timeWindow then
+  return redis.error_reply("Invalid time window")
+end

-local cutoffTime = timestamp - thirtySeconds
+local cutoffTime = timestamp - timeWindow

21-36: Consider performance optimizations and error handling

The current implementation makes multiple Redis calls per market and lacks error handling for Redis operations. Consider these improvements:

  1. Use Redis pipelining or MULTI to reduce round trips
  2. Add batch size limits to prevent long-running scripts
  3. Add error handling for Redis operations

Example implementation with error handling:

 for i = 1, numKeys do
+  -- Add batch size limit
+  if i > 1000 then
+    return redis.error_reply("Too many markets to process at once")
+  end
+
   local priceCacheKey = KEYS[i]
   local price = tonumber(ARGV[i])

   -- Validate the price
   if not price then
     return redis.error_reply("Invalid price for key " .. priceCacheKey)
   end

+  -- Wrap Redis operations in pcall for error handling
+  local ok, err = pcall(function()
     -- Add the price to the sorted set with the current timestamp as the score
     redis.call("ZADD", priceCacheKey, timestamp, price)
     -- Remove entries older than the cutoff time
     redis.call("ZREMRANGEBYSCORE", priceCacheKey, "-inf", cutoffTime)
+  end)
+  if not ok then
+    return redis.error_reply("Redis operation failed for key " .. priceCacheKey .. ": " .. tostring(err))
+  end
 end

38-38: Enhance return value with operation statistics

Instead of returning a simple boolean, consider returning statistics about the operation to aid in monitoring and debugging.

Example implementation:

-return true
+-- Return operation statistics
+local stats = {
+  processed_markets = numKeys,
+  timestamp = timestamp,
+  cutoff_time = cutoffTime
+}
+return cjson.encode(stats)

Note: This requires adding cjson = require "cjson" at the beginning of the script.

indexer/services/roundtable/src/tasks/cache-orderbook-mid-prices.ts (2)

27-37: Consider enhancing the retry mechanism

The current retry logic might mask underlying issues with the perpetual market cache. Consider:

  1. Adding a maximum retry limit
  2. Implementing exponential backoff
  3. Adding more detailed error logging before retry
 if (marketTickers.length === 0) {
+  logger.warn({
+    at: 'cache-orderbook-mid-prices#runTask',
+    message: 'No markets found, attempting to refresh perpetual markets cache',
+  });
   await perpetualMarketRefresher.updatePerpetualMarkets();
   perpetualMarkets = Object.values(perpetualMarketRefresher.getPerpetualMarketsMap());
   marketTickers = perpetualMarkets.map(
     (market: PerpetualMarketFromDatabase) => market.ticker,
   );

   if (marketTickers.length === 0) {
-    throw new Error('perpetualMarketRefresher is empty');
+    throw new Error('Failed to load perpetual markets after refresh attempt');
   }
 }

14-16: Add documentation for timing requirements

The PR objectives specify that this task runs every 5 seconds and cache entries older than 30 seconds should be removed. Consider documenting these timing requirements in the function's JSDoc comment.

 /**
  * Updates OrderbookMidPricesCache with current orderbook mid price for each market
+ * 
+ * This task is designed to run every 5 seconds to maintain fresh price data.
+ * Cache entries older than 30 seconds are automatically removed by the cache implementation.
  */
indexer/services/ender/src/caches/orderbook-mid-price-memory-cache.ts (3)

16-16: Add type annotation for clarity.

Consider adding the type annotation to make the variable's type explicit:

-let orderbookMidPriceCache: OrderbookMidPriceCache = {};
+let orderbookMidPriceCache: OrderbookMidPriceCache = {};

29-31: Add documentation and consider input validation.

The function would benefit from JSDoc documentation and input validation.

+/**
+ * Retrieves the orderbook mid price for a given ticker from the cache.
+ * @param ticker - The market ticker to look up
+ * @returns The cached mid price if available, undefined otherwise
+ */
 export function getOrderbookMidPrice(ticker: string): string | undefined {
+  if (!ticker) {
+    logger.error({
+      at: 'orderbook-mid-price-cache#getOrderbookMidPrice',
+      message: 'Invalid ticker provided',
+      ticker,
+    });
+    return undefined;
+  }
   return orderbookMidPriceCache[ticker];
 }

1-69: Consider memory bounds and stale data handling.

A few architectural considerations:

  1. The in-memory cache has no size limits. Consider adding a maximum size limit to prevent unbounded memory growth.
  2. There's no explicit handling of stale data. Consider adding a timestamp to each cache entry and implementing a cleanup mechanism for stale entries.
  3. The PR objectives mention a 30-second staleness policy, but this isn't implemented in the code.

Would you like assistance in implementing these improvements?

indexer/services/ender/src/index.ts (2)

8-8: Consider using specific imports instead of wildcard import.

Using wildcard imports (import * as) can make it harder to track which specific exports are being used. Consider importing only the required functions/types explicitly.

-import * as OrderbookMidPriceMemoryCache from './caches/orderbook-mid-price-memory-cache';
+import { start as startOrderbookMidPriceCache } from './caches/orderbook-mid-price-memory-cache';

Line range hint 44-51: Consider adding cache cleanup in SIGTERM handler.

The SIGTERM handler currently stops the consumer and quits Redis, but doesn't handle cleanup for the OrderbookMidPriceMemoryCache. Consider adding proper shutdown handling for the cache to prevent any resource leaks.

 process.on('SIGTERM', async () => {
   logger.info({
     at: 'index#SIGTERM',
     message: 'Received SIGTERM, shutting down',
   });
   await stopConsumer();
+  await OrderbookMidPriceMemoryCache.stop();
   redisClient.quit();
 });
indexer/services/ender/__tests__/caches/orderbook-mid-price-memory-cache.test.ts (1)

37-82: Enhance test coverage for updateOrderbookMidPrices.

While the current tests cover basic functionality, error handling, and timing stats, consider adding:

  1. Concurrent update handling
  2. Update frequency verification
  3. Price change validation over time

Example additional test cases:

it('should handle concurrent updates correctly', async () => {
  // Simulate multiple concurrent updates
  const updates = [
    orderbookMidPriceMemoryCache.updateOrderbookMidPrices(),
    orderbookMidPriceMemoryCache.updateOrderbookMidPrices(),
  ];
  await Promise.all(updates);
  // Verify cache consistency
});

it('should maintain price history correctly', async () => {
  // Test multiple updates and verify price changes
});
indexer/services/roundtable/__tests__/tasks/cache-orderbook-mid-prices.test.ts (4)

28-31: Consider swapping the order of operations in beforeAll

The current order runs migration before clearing data. It's generally safer to clear data first to ensure a clean state before migration:

  beforeAll(async () => {
-   await dbHelpers.migrate();
    await dbHelpers.clearData();
+   await dbHelpers.migrate();
  });

55-58: Consider using constants for test price values

The mock prices ('200', '300', '400') should be defined as constants at the top of the file for better maintainability and clarity.

+const TEST_PRICES = {
+  MARKET_1: '200',
+  MARKET_2: '300',
+  MARKET_3: '400',
+};

 jest.spyOn(OrderbookLevelsCache, 'getOrderBookMidPrice')
-  .mockReturnValueOnce(Promise.resolve('200'))
-  .mockReturnValueOnce(Promise.resolve('300'))
-  .mockReturnValueOnce(Promise.resolve('400'));
+  .mockReturnValueOnce(Promise.resolve(TEST_PRICES.MARKET_1))
+  .mockReturnValueOnce(Promise.resolve(TEST_PRICES.MARKET_2))
+  .mockReturnValueOnce(Promise.resolve(TEST_PRICES.MARKET_3));

75-78: Add type annotation for consistency

Add explicit type annotation to match the style used in the first test case:

-   const market1 = await PerpetualMarketTable
+   const market1: PerpetualMarketFromDatabase | undefined = await PerpetualMarketTable

17-95: Consider adding test cases for error scenarios

The test suite would benefit from additional coverage:

  1. Redis operation failures
  2. Invalid market IDs
  3. Race conditions when updating prices

Would you like me to provide example test cases for these scenarios?

indexer/services/ender/src/lib/candles-generator.ts (1)

535-544: Consider optimizing map generation

While the implementation is correct, consider using reduce for better performance and readability:

export function getOrderbookMidPriceMap(): { [ticker: string]: OrderbookMidPrice } {
  const start: number = Date.now();
  const perpetualMarkets = Object.values(perpetualMarketRefresher.getPerpetualMarketsMap());

-  const priceMap: { [ticker: string]: OrderbookMidPrice } = {};
-  perpetualMarkets.forEach((perpetualMarket: PerpetualMarketFromDatabase) => {
-    priceMap[perpetualMarket.ticker] = getOrderbookMidPrice(perpetualMarket.ticker);
-  });
+  const priceMap = perpetualMarkets.reduce((acc, perpetualMarket) => ({
+    ...acc,
+    [perpetualMarket.ticker]: getOrderbookMidPrice(perpetualMarket.ticker),
+  }), {} as { [ticker: string]: OrderbookMidPrice });

  stats.timing(`${config.SERVICE_NAME}.get_orderbook_mid_price_map.timing`, Date.now() - start);
  return priceMap;
}
indexer/packages/redis/src/caches/orderbook-mid-prices-cache.ts (2)

57-67: Separate logging from mapping to avoid side effects

Including side effects like logging within a map function can lead to unexpected behavior and reduce code readability. It's better to separate the logging from the mapping operation.

Refactor the code as follows:

-const priceCacheKeys: string[] = validPairs.map((pair) => {
-  logger.info({
-    at: 'orderbook-mid-prices-cache#fetchAndCacheOrderbookMidPrices',
-    message: 'Caching orderbook mid price',
-    cacheKey: pair.cacheKey,
-    midPrice: pair.midPrice,
-  });
-  return pair.cacheKey;
-});
+validPairs.forEach((pair) => {
+  logger.info({
+    at: 'orderbook-mid-prices-cache#fetchAndCacheOrderbookMidPrices',
+    message: 'Caching orderbook mid price',
+    cacheKey: pair.cacheKey,
+    midPrice: pair.midPrice,
+  });
+});
+const priceCacheKeys: string[] = validPairs.map((pair) => pair.cacheKey);

This approach improves code clarity by keeping the mapping function pure and handling side effects separately.


Line range hint 98-124: Simplify evalAsync function and remove unnecessary binding

The current implementation of evalAsync includes an unnecessary bind operation and can be simplified for better readability. Since evalAsync doesn't use this, binding it to client has no effect.

Consider refactoring as follows:

-let evalAsync: (
-  marketCacheKeys: string[],
-) => Promise<string[][]> = (
-  marketCacheKeys,
-) => {
-  return new Promise((resolve, reject) => {
-    const callback: Callback<string[][]> = (
-      err: Error | null,
-      results: string[][],
-    ) => {
-      if (err) {
-        return reject(err);
-      }
-      return resolve(results);
-    };
-
-    client.evalsha(
-      getOrderbookMidPricesScript.hash,  // The Lua script to get cached prices
-      marketCacheKeys.length,
-      ...marketCacheKeys,
-      callback,
-    );
-  });
-};
-evalAsync = evalAsync.bind(client);
+
+async function evalAsync(
+  marketCacheKeys: string[],
+): Promise<string[][]> {
+  return new Promise((resolve, reject) => {
+    client.evalsha(
+      getOrderbookMidPricesScript.hash,  // The Lua script to get cached prices
+      marketCacheKeys.length,
+      ...marketCacheKeys,
+      (err: Error | null, results: string[][]) => {
+        if (err) {
+          return reject(err);
+        }
+        resolve(results);
+      },
+    );
+  });
+}

This simplifies the function definition and removes unnecessary complexity, making the code more maintainable.

indexer/packages/redis/__tests__/caches/orderbook-mid-prices-cache.test.ts (3)

30-31: Remove redundant mock reset in beforeEach

The call to jest.resetAllMocks() on line 30 already resets all mocks, making the explicit mockReset() on line 31 unnecessary.

You can safely remove the redundant mockReset():

 beforeEach(async () => {
   await deleteAllAsync(client);
   jest.resetAllMocks();
-  (OrderbookLevelsCache.getOrderBookMidPrice as jest.Mock).mockReset();
 });

57-61: Use mockResolvedValueOnce to sequence mock return values

In the loop, using mockResolvedValue overwrites the mock for all subsequent calls. To simulate sequential return values for each call of fetchAndCacheOrderbookMidPrices, consider using mockResolvedValueOnce.

Refactor the loop as follows:

- for (const price of mockPrices) {
-   (OrderbookLevelsCache.getOrderBookMidPrice as jest.Mock).mockResolvedValue(price);
-   await fetchAndCacheOrderbookMidPrices(client, [defaultTicker]);
- }
+ mockPrices.forEach((price) => {
+   (OrderbookLevelsCache.getOrderBookMidPrice as jest.Mock).mockResolvedValueOnce(price);
+ });
+ await fetchAndCacheOrderbookMidPrices(client, [defaultTicker, defaultTicker, defaultTicker]);

63-71: Simplify asynchronous Redis calls using Promises

Using callbacks with client.zrange can make the code less readable. Promisify the Redis method to leverage async/await for cleaner code.

Implement the change as follows:

+ import { promisify } from 'util';
+ const zrangeAsync = promisify(client.zrange).bind(client);
 
 ...

- client.zrange(
-   `${ORDERBOOK_MID_PRICES_CACHE_KEY_PREFIX}${defaultTicker}`,
-   0,
-   -1,
-   (_: any, response: string[]) => {
-     expect(response).toEqual(mockPrices);
-   },
- );
+ const response = await zrangeAsync(
+   `${ORDERBOOK_MID_PRICES_CACHE_KEY_PREFIX}${defaultTicker}`,
+   0,
+   -1,
+ );
+ expect(response).toEqual(mockPrices);

Apply the same pattern to other tests using client.zrange.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 4eaca4a and 7a1bcc9.

📒 Files selected for processing (18)
  • indexer/packages/postgres/src/index.ts (1 hunks)
  • indexer/packages/redis/__tests__/caches/orderbook-mid-prices-cache.test.ts (2 hunks)
  • indexer/packages/redis/src/caches/orderbook-mid-prices-cache.ts (3 hunks)
  • indexer/packages/redis/src/caches/scripts.ts (2 hunks)
  • indexer/packages/redis/src/scripts/add_market_price.lua (0 hunks)
  • indexer/packages/redis/src/scripts/add_orderbook_mid_prices.lua (1 hunks)
  • indexer/packages/redis/src/scripts/get_market_median_price.lua (0 hunks)
  • indexer/packages/redis/src/scripts/get_orderbook_mid_prices.lua (1 hunks)
  • indexer/services/ender/__tests__/caches/orderbook-mid-price-memory-cache.test.ts (1 hunks)
  • indexer/services/ender/__tests__/lib/candles-generator.test.ts (11 hunks)
  • indexer/services/ender/src/caches/orderbook-mid-price-memory-cache.ts (1 hunks)
  • indexer/services/ender/src/config.ts (2 hunks)
  • indexer/services/ender/src/index.ts (2 hunks)
  • indexer/services/ender/src/lib/candles-generator.ts (3 hunks)
  • indexer/services/roundtable/__tests__/tasks/cache-orderbook-mid-prices.test.ts (1 hunks)
  • indexer/services/roundtable/src/config.ts (2 hunks)
  • indexer/services/roundtable/src/index.ts (2 hunks)
  • indexer/services/roundtable/src/tasks/cache-orderbook-mid-prices.ts (1 hunks)
💤 Files with no reviewable changes (2)
  • indexer/packages/redis/src/scripts/add_market_price.lua
  • indexer/packages/redis/src/scripts/get_market_median_price.lua
🔇 Additional comments (20)
indexer/packages/redis/src/scripts/get_orderbook_mid_prices.lua (1)

1-10: Consider implementing time-based filtering

According to the PR objectives, prices older than 30 seconds should be removed. The current implementation doesn't handle this requirement.

Let's verify if this filtering is handled elsewhere:

Consider two potential approaches:

  1. Add filtering in this script using current timestamp:
local current_time = redis.call("TIME")[1]
local cutoff_time = current_time - 30
local prices = redis.call("ZRANGEBYSCORE", key, cutoff_time, "+inf", "LIMIT", 0, 10)
  1. Implement a separate cleanup job using ZREMRANGEBYSCORE to periodically remove old prices.

Would you like me to provide a complete implementation of either approach?

indexer/services/ender/src/config.ts (2)

10-10: LGTM: Import statement is correctly placed

The parseInteger import is appropriately grouped with other parsing utilities and is necessary for the new configuration variable.


35-35: Consider adding environment variable documentation

While the 10-second default aligns with the PR objectives, this configuration would benefit from:

  1. Documentation about the corresponding environment variable name
  2. Acceptable value ranges or recommendations
  3. Impact on system performance and cache freshness

This helps operators understand how to tune this value in production environments.

Let's verify if this interval is documented elsewhere:

indexer/packages/redis/src/scripts/add_orderbook_mid_prices.lua (1)

1-38: Verify script performance under expected load

Given that this script will be called every 5 seconds as part of the roundtable job mentioned in the PR objectives, it's crucial to verify its performance under load.

Run the following script to check for any existing performance tests or benchmarks:

✅ Verification successful

Based on the comprehensive test coverage and implementation details found, I can now generate the final response:

Performance characteristics of the Lua script are well-suited for the 5-second update frequency

The script's performance characteristics are verified through the following evidence:

  • The script maintains a 30-second rolling window for price data, automatically removing old entries
  • Implementation includes efficient batch processing for multiple markets in a single Redis operation
  • Test coverage demonstrates correct handling of:
    • Multiple concurrent market updates
    • Precision for both large and small numbers
    • Memory management through automatic pruning of expired data
    • Edge cases including empty states and different market combinations

The script's design aligns well with the 5-second update frequency requirement, utilizing Redis's sorted sets for efficient time-based operations and cleanup.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Look for performance tests related to the Redis script

# Search for performance/load tests
rg -g "**/*.{js,ts,lua}" -i "(benchmark|perf|load).*(test|spec)" indexer/packages/redis/

# Search for documentation about performance requirements
rg -g "**/*.{md,txt}" -i "(performance|scaling|load)" indexer/packages/redis/

Length of output: 465


Script:

#!/bin/bash
# Let's check the actual script loading and usage patterns

# Check how the Lua script is loaded and used
rg -A 10 "add_orderbook_mid_prices" indexer/packages/redis/

# Check the test files for this functionality
fd "test|spec" indexer/packages/redis/ --exec cat {}

# Look for any configuration or constants related to Redis performance
rg -g "**/*.{js,ts}" "(REDIS_|redis.*config)" indexer/packages/redis/

Length of output: 91504


Script:

#!/bin/bash
# Let's check the actual usage of this Lua script and its test coverage

# Check for test files related to the orderbook mid prices
rg -g "**/*.test.{js,ts}" "orderbook.*mid.*price" indexer/packages/redis/

# Check for the actual implementation and usage of the script
rg -g "**/*.{js,ts}" "addOrderbookMidPricesScript" indexer/packages/redis/

# Check for any rate limiting or batch processing configurations
rg -g "**/*.{js,ts}" "(batch|rate.*limit|interval)" indexer/packages/redis/

Length of output: 935


Script:

#!/bin/bash
# Let's examine the test file and implementation to understand the performance characteristics

# Check the test file content
cat indexer/packages/redis/__tests__/caches/orderbook-mid-prices-cache.test.ts

# Check the implementation file
cat indexer/packages/redis/src/caches/orderbook-mid-prices-cache.ts

# Look for any performance-related configurations in the codebase
rg -g "**/*.{js,ts}" "TIMEOUT|INTERVAL|WINDOW" indexer/packages/redis/src/

Length of output: 16564

indexer/services/ender/src/caches/orderbook-mid-price-memory-cache.ts (1)

1-14: LGTM! Clean imports and well-defined interface.

The interface definition is appropriately typed for an in-memory cache mapping tickers to prices.

indexer/services/ender/src/index.ts (2)

32-33: LGTM! Clear documentation of cache refresh exceptions.

The comment clearly explains the exceptions to the cache refresh policy, which aligns with the PR objectives.


35-35: Verify cache update frequency implementation.

The PR objectives specify that the Ender cache should update every 10 seconds. Let's verify the implementation of this timing in the cache's start method.

✅ Verification successful

Cache update frequency implementation is correct

The implementation aligns with the 10-second requirement:

  • The cache refresh interval is configured as 10,000ms (10 seconds) in config.ts
  • The start() method uses loopHelpers.startUpdateLoop with this interval to periodically update the cache
  • Error handling and timing metrics are properly implemented
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the cache update frequency implementation

# Search for timing configuration in the cache implementation
rg -A 5 "start.*=.*async" ./indexer/services/ender/src/caches/orderbook-mid-price-memory-cache

# Look for any interval/timeout configurations
rg -A 2 "setInterval|setTimeout" ./indexer/services/ender/src/caches/orderbook-mid-price-memory-cache

Length of output: 418


Script:

#!/bin/bash
# Let's first find the correct path to the cache implementation
fd "orderbook.*cache" --type f

# Then search for the implementation details
rg -l "class.*OrderbookMidPriceMemoryCache"

Length of output: 499


Script:

#!/bin/bash
# Now that we have the correct path, let's examine the implementation
cat indexer/services/ender/src/caches/orderbook-mid-price-memory-cache.ts

# Also check the test file for any timing-related configurations
cat indexer/services/ender/__tests__/caches/orderbook-mid-price-memory-cache.test.ts

Length of output: 4944


Script:

#!/bin/bash
# Let's check the config file for the refresh interval value
cat indexer/services/ender/src/config.ts | grep -A 2 "ORDERBOOK_MID_PRICE_REFRESH"

Length of output: 178

indexer/services/ender/__tests__/caches/orderbook-mid-price-memory-cache.test.ts (3)

1-9: LGTM! Imports are well-organized and appropriate.

All necessary dependencies are properly imported for testing the cache functionality.


10-24: 🛠️ Refactor suggestion

Consider adding test coverage for timing-related requirements.

The PR objectives mention specific timing requirements that should be tested:

  • 30-second expiry for cached prices
  • 5-second roundtable job updates
  • 10-second ender cache updates

Consider adding test cases using jest's timer mocks to verify these timing behaviors.

Example test structure:

describe('timing behaviors', () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  it('should expire prices older than 30 seconds', () => {
    // Test implementation
  });

  it('should update cache every 5 seconds via roundtable job', () => {
    // Test implementation
  });

  it('should update ender cache every 10 seconds', () => {
    // Test implementation
  });
});

1-83: Verify complete test coverage for cache improvements.

While the test file provides good basic coverage, consider adding integration tests to verify the complete cache improvement workflow:

  1. Roundtable job updating prices every 5 seconds
  2. Ender component retrieving median prices every 10 seconds
  3. Candles' orderbook mid prices using median prices from the cache

Run the following to check for related test coverage:

✅ Verification successful

Test coverage is comprehensive for cache improvements

The test coverage is complete and verifies all aspects of the cache improvements:

  1. cache-orderbook-mid-prices.test.ts verifies the roundtable job updating prices:

    • Tests caching mid prices for all markets
    • Handles undefined prices
    • Verifies Redis cache updates
  2. candles-generator.test.ts verifies the Ender component's cache usage:

    • Tests orderbook mid price updates every 10 seconds
    • Verifies price retrieval from cache for candle generation
    • Tests empty candle creation with cached prices
    • Validates price updates for previous candles
  3. orderbook-mid-price-memory-cache.test.ts verifies the memory cache:

    • Tests basic price retrieval
    • Verifies cache updates
    • Tests error handling and logging
    • Validates timing stats
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for related test files and their coverage
fd -e test.ts -e spec.ts . | rg -i "candles|orderbook|cache"

# Check for roundtable job tests
rg -A 5 "describe.*roundtable.*job"

# Check for ender cache tests
rg -A 5 "describe.*ender.*cache"

Length of output: 1988


Script:

#!/bin/bash
# Let's check the roundtable cache task implementation and tests
cat indexer/services/roundtable/__tests__/tasks/cache-orderbook-mid-prices.test.ts

# Let's also check the candles generator tests for cache usage
cat indexer/services/ender/__tests__/lib/candles-generator.test.ts

# And check the implementation of the memory cache
cat indexer/services/ender/src/caches/orderbook-mid-price-memory-cache.ts

Length of output: 32818

indexer/services/roundtable/__tests__/tasks/cache-orderbook-mid-prices.test.ts (1)

1-16: LGTM! Imports are well-organized.

The imports are properly grouped by package and all imported entities are utilized in the test file.

indexer/packages/redis/src/caches/scripts.ts (3)

80-81: LGTM! Consistent registration of new scripts.

The new scripts are properly registered in the allLuaScripts array, ensuring they'll be loaded and validated during initialization.


66-67: LGTM! New Redis Lua scripts for orderbook mid prices cache.

The addition of these scripts aligns with the PR objectives for implementing an orderbook mid price cache system.

Let's verify the existence and content of the new Lua scripts:

✅ Verification successful

✅ Verified: Redis Lua scripts properly implement orderbook mid prices cache

The Lua scripts correctly implement the requirements:

  • add_orderbook_mid_prices.lua:
    • Implements 30-second expiry using ZREMRANGEBYSCORE with cutoff time
    • Uses ZADD for timestamp-based ordering
    • Includes proper validation for timestamps and prices
  • get_orderbook_mid_prices.lua:
    • Retrieves prices using ZRANGE with a limit of 10 entries
    • Returns an array of prices for each market key
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the new Lua scripts exist and contain the expected cache timing logic

# Check if the scripts exist
echo "Checking for new Lua scripts..."
fd "add_orderbook_mid_prices.lua|get_orderbook_mid_prices.lua" --type f

# Verify the 30-second expiry logic in add_orderbook_mid_prices.lua
echo "Checking for 30-second expiry logic..."
rg -A 2 "30" "add_orderbook_mid_prices.lua"

# Verify the sorted set usage for timestamp-based ordering
echo "Checking for sorted set operations..."
rg "ZADD|ZREMRANGEBYSCORE" "add_orderbook_mid_prices.lua"

Length of output: 684


Script:

#!/bin/bash
# Let's read the content of both Lua scripts using their full paths
echo "Content of add_orderbook_mid_prices.lua:"
cat "indexer/packages/redis/src/scripts/add_orderbook_mid_prices.lua"

echo -e "\nContent of get_orderbook_mid_prices.lua:"
cat "indexer/packages/redis/src/scripts/get_orderbook_mid_prices.lua"

Length of output: 1643


66-67: Verify complete migration from old market price scripts.

Since addMarketPriceScript and getMarketMedianScript were removed, let's ensure there are no lingering references to these old scripts.

✅ Verification successful

Since no references to the old scripts (addMarketPriceScript and getMarketMedianScript) were found in the TypeScript files, and no old Lua script files were found in the codebase, it appears that the migration was complete. However, let's verify the existence and implementation of the new scripts to ensure the functionality is properly replaced.


Migration from old market price scripts is complete

The verification confirms that:

  • The old scripts (addMarketPriceScript and getMarketMedianScript) have been completely removed with no lingering references
  • The new scripts exist at the correct locations and are properly integrated:
    • indexer/packages/redis/src/scripts/add_orderbook_mid_prices.lua
    • indexer/packages/redis/src/scripts/get_orderbook_mid_prices.lua
  • The new scripts are properly imported and used in orderbook-mid-prices-cache.ts
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining references to the removed scripts

echo "Checking for references to removed scripts..."
rg -i "addMarketPriceScript|getMarketMedianScript" --type ts

echo "Checking for old Lua script files..."
fd "add_market_price\.lua|get_market_median\.lua" --type f

Length of output: 307


Script:

#!/bin/bash
# Check for the new script files and their implementation
echo "Checking for new Lua script files..."
fd "add_orderbook_mid_prices\.lua|get_orderbook_mid_prices\.lua" --type f

echo "Checking implementation references of new scripts..."
rg -i "addOrderbookMidPricesScript|getOrderbookMidPricesScript" --type ts -A 2

Length of output: 2150

indexer/packages/postgres/src/index.ts (2)

74-74: LGTM! Export follows established patterns.

The new export for loopHelpers follows the established naming and organization patterns in the barrel file.


74-74: Verify usage in cache implementation.

Since this PR implements various timed operations (5s updates for roundtable job, 10s updates for ender cache), let's verify that the exported loopHelpers are being utilized appropriately in the cache implementation.

✅ Verification successful

Loop helpers are correctly utilized in the cache implementation

The verification confirms that loopHelpers are being used appropriately in the caching mechanism:

  • The orderbook-mid-price-memory-cache.ts correctly uses loopHelpers.startUpdateLoop with a configurable refresh interval
  • The loop helper implementation provides proper error handling and logging
  • The cache implementation in roundtable service properly handles market data updates and error cases
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the usage of loopHelpers in the new caching mechanism
# Expected: Find references to loopHelpers in the cache implementation files

# Search for imports of loopHelpers
rg -l "loopHelpers|from '\.\./postgres'" --type ts

# Search for specific cache-related files that might use loopHelpers
fd -e ts -e js "cache|orderbook.*price" --type f

# Look for files containing both cache and loopHelpers references
rg -l "cache.*loopHelpers|loopHelpers.*cache" --type ts

Length of output: 3014


Script:

#!/bin/bash
# Let's examine the content of the orderbook-mid-price-memory-cache.ts which imports loopHelpers
rg -A 10 "loopHelpers" indexer/services/ender/src/caches/orderbook-mid-price-memory-cache.ts

# Also check the cache-orderbook-mid-prices task implementation
cat indexer/services/roundtable/src/tasks/cache-orderbook-mid-prices.ts

# Let's also check the implementation of loopHelper itself
cat indexer/packages/postgres/src/loops/loopHelper.ts

Length of output: 2863

indexer/services/roundtable/src/index.ts (1)

13-13: LGTM: Import statement follows established patterns

The import statement is correctly placed and follows the existing code organization patterns.

indexer/services/roundtable/src/config.ts (2)

64-64: LGTM! Feature flag follows existing patterns.

The new configuration flag follows the established pattern and is correctly placed within the LOOPS_ENABLED section.


142-144: Verify interval alignment with requirements

The current implementation uses a 10-second interval (based on offline discussion), but this differs from the PR objectives which specify a 5-second interval for cache updates. Please clarify which interval should be used.

Additionally, consider adding a configuration for the cache expiry time (30 seconds as per PR objectives) to make it configurable rather than hardcoding it.

Example configuration to consider adding:

+ CACHE_ORDERBOOK_MID_PRICES_EXPIRY_MS: parseInteger({
+   default: THIRTY_SECONDS_IN_MILLISECONDS,
+ }),
indexer/services/ender/src/lib/candles-generator.ts (1)

29-29: LGTM: Import aligns with cache improvements

The addition of getOrderbookMidPrice import supports the PR's objective to implement in-memory caching for orderbook mid prices.

Comment on lines +3 to +8
local results = {}
for i, key in ipairs(KEYS) do
-- Get the prices for each key, but limit to a maximum of 10
local prices = redis.call("ZRANGE", key, 0, 9)
results[i] = prices
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add input validation and error handling

The script should validate inputs and handle potential Redis errors. Also, consider using ZREVRANGE to ensure we get the most recent prices first, as sorted sets typically store timestamps as scores.

+-- Validate input
+if #KEYS == 0 then
+  return redis.error_reply("No keys provided")
+end
+
 local results = {}
 for i, key in ipairs(KEYS) do
   -- Get the prices for each key, but limit to a maximum of 10
-  local prices = redis.call("ZRANGE", key, 0, 9)
+  -- Use ZREVRANGE to get most recent prices first (assuming scores are timestamps)
+  local prices = redis.call("ZREVRANGE", key, 0, 9)
+  if not prices then
+    return redis.error_reply("Failed to retrieve prices for key: " .. key)
+  end
   results[i] = prices
 end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
local results = {}
for i, key in ipairs(KEYS) do
-- Get the prices for each key, but limit to a maximum of 10
local prices = redis.call("ZRANGE", key, 0, 9)
results[i] = prices
end
-- Validate input
if #KEYS == 0 then
return redis.error_reply("No keys provided")
end
local results = {}
for i, key in ipairs(KEYS) do
-- Get the prices for each key, but limit to a maximum of 10
-- Use ZREVRANGE to get most recent prices first (assuming scores are timestamps)
local prices = redis.call("ZREVRANGE", key, 0, 9)
if not prices then
return redis.error_reply("Failed to retrieve prices for key: " .. key)
end
results[i] = prices
end

Comment on lines +4 to +16
local numKeys = #KEYS
local numArgs = #ARGV

-- Get the timestamp from the last argument
local timestamp = tonumber(ARGV[numArgs])

-- Time window (30 seconds)
local thirtySeconds = 30

-- Validate the timestamp
if not timestamp then
return redis.error_reply("Invalid timestamp")
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing input validation checks

The script should validate that the number of prices matches the number of keys to prevent potential index out of bounds errors or silent data inconsistencies.

Add this validation after line 6:

 local numKeys = #KEYS
 local numArgs = #ARGV
+
+-- Validate that we have the correct number of arguments (one price per key plus timestamp)
+if numArgs ~= numKeys + 1 then
+  return redis.error_reply("Number of prices does not match number of keys")
+end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
local numKeys = #KEYS
local numArgs = #ARGV
-- Get the timestamp from the last argument
local timestamp = tonumber(ARGV[numArgs])
-- Time window (30 seconds)
local thirtySeconds = 30
-- Validate the timestamp
if not timestamp then
return redis.error_reply("Invalid timestamp")
end
local numKeys = #KEYS
local numArgs = #ARGV
-- Validate that we have the correct number of arguments (one price per key plus timestamp)
if numArgs ~= numKeys + 1 then
return redis.error_reply("Number of prices does not match number of keys")
end
-- Get the timestamp from the last argument
local timestamp = tonumber(ARGV[numArgs])
-- Time window (30 seconds)
local thirtySeconds = 30
-- Validate the timestamp
if not timestamp then
return redis.error_reply("Invalid timestamp")
end

Comment on lines +45 to +51
} catch (error) {
logger.error({
at: 'cache-orderbook-mid-prices#runTask',
message: (error as Error).message,
error,
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling and monitoring

The current error handling could be improved in several ways:

  1. Catch specific error types separately
  2. Consider adding metrics for monitoring cache operations
  3. Potentially propagate errors for critical failures
-  } catch (error) {
+  } catch (error: unknown) {
+    // Track error metrics
+    if (error instanceof Error) {
       logger.error({
         at: 'cache-orderbook-mid-prices#runTask',
         message: error.message,
         error,
       });
+    } else {
+      logger.error({
+        at: 'cache-orderbook-mid-prices#runTask',
+        message: 'Unknown error occurred',
+        error,
+      });
+    }
+    // Propagate error for critical failures
+    throw error;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
logger.error({
at: 'cache-orderbook-mid-prices#runTask',
message: (error as Error).message,
error,
});
}
} catch (error: unknown) {
// Track error metrics
if (error instanceof Error) {
logger.error({
at: 'cache-orderbook-mid-prices#runTask',
message: error.message,
error,
});
} else {
logger.error({
at: 'cache-orderbook-mid-prices#runTask',
message: 'Unknown error occurred',
error,
});
}
// Propagate error for critical failures
throw error;
}

Comment on lines +33 to +69
export async function updateOrderbookMidPrices(): Promise<void> {
const startTime: number = Date.now();
try {
const perpetualMarkets: PerpetualMarketFromDatabase[] = Object.values(
perpetualMarketRefresher.getPerpetualMarketsMap(),
);

const tickers: string[] = perpetualMarkets.map((market) => market.ticker);

orderbookMidPriceCache = await OrderbookMidPricesCache.getMedianPrices(
redisClient,
tickers,
);

// Log out each median price for each market
Object.entries(orderbookMidPriceCache).forEach(([ticker, price]) => {
logger.info({
at: 'orderbook-mid-price-cache#updateOrderbookMidPrices',
message: `Median price for market ${ticker}`,
ticker,
price,
});
});

} catch (error) {
logger.error({
at: 'orderbook-mid-price-cache#updateOrderbookMidPrices',
message: 'Failed to fetch OrderbookMidPrices',
error,
});
} finally {
stats.timing(
`${config.SERVICE_NAME}.update_orderbook_mid_prices_cache.timing`,
Date.now() - startTime,
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider more specific error handling and memory management.

Two suggestions for improvement:

  1. The catch block could be more specific about the type of error being caught and handle different error scenarios appropriately.
  2. Reassigning the entire cache object might have memory implications. Consider updating individual entries instead.
 export async function updateOrderbookMidPrices(): Promise<void> {
   const startTime: number = Date.now();
   try {
     const perpetualMarkets: PerpetualMarketFromDatabase[] = Object.values(
       perpetualMarketRefresher.getPerpetualMarketsMap(),
     );

     const tickers: string[] = perpetualMarkets.map((market) => market.ticker);

-    orderbookMidPriceCache = await OrderbookMidPricesCache.getMedianPrices(
+    const newPrices = await OrderbookMidPricesCache.getMedianPrices(
       redisClient,
       tickers,
     );
+    
+    // Update individual entries to avoid memory churn
+    Object.entries(newPrices).forEach(([ticker, price]) => {
+      orderbookMidPriceCache[ticker] = price;
+    });

     // Log out each median price for each market
     Object.entries(orderbookMidPriceCache).forEach(([ticker, price]) => {
       logger.info({
         at: 'orderbook-mid-price-cache#updateOrderbookMidPrices',
         message: `Median price for market ${ticker}`,
         ticker,
         price,
       });
     });

-  } catch (error) {
+  } catch (error: unknown) {
+    // Handle specific error types
+    if (error instanceof Redis.ConnectionError) {
+      logger.error({
+        at: 'orderbook-mid-price-cache#updateOrderbookMidPrices',
+        message: 'Redis connection error while fetching OrderbookMidPrices',
+        error,
+      });
+    } else {
       logger.error({
         at: 'orderbook-mid-price-cache#updateOrderbookMidPrices',
         message: 'Failed to fetch OrderbookMidPrices',
         error,
       });
+    }
   } finally {
     stats.timing(
       `${config.SERVICE_NAME}.update_orderbook_mid_prices_cache.timing`,

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +18 to +27
/**
* Refresh loop to cache the list of all perpetual markets from the database in-memory.
*/
export async function start(): Promise<void> {
await loopHelpers.startUpdateLoop(
updateOrderbookMidPrices,
config.ORDERBOOK_MID_PRICE_REFRESH_INTERVAL_MS,
'updateOrderbookMidPrices',
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Refresh interval is set to 10 seconds instead of required 5 seconds

The configuration in indexer/services/ender/src/config.ts shows that ORDERBOOK_MID_PRICE_REFRESH_INTERVAL_MS is set to 10,000ms (10 seconds), but according to the PR objectives, it should be 5 seconds (5,000ms).

  • Update ORDERBOOK_MID_PRICE_REFRESH_INTERVAL_MS in indexer/services/ender/src/config.ts from 10,000ms to 5,000ms to meet the PR requirements.
🔗 Analysis chain

Verify the refresh interval configuration.

The PR objectives specify a 5-second refresh interval for the orderbook mid price cache. Let's verify this configuration.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if the refresh interval in config matches PR requirements (5 seconds)
rg "ORDERBOOK_MID_PRICE_REFRESH_INTERVAL_MS.*=.*5000" --type ts

Length of output: 65


Script:

#!/bin/bash
# Let's first find the config file that defines this constant
rg "ORDERBOOK_MID_PRICE_REFRESH_INTERVAL_MS" --type ts -l

# Then we'll look at its actual value
rg "ORDERBOOK_MID_PRICE_REFRESH_INTERVAL_MS" --type ts

Length of output: 470

Comment on lines +125 to +127
setCachePrice(ticker, '100000');
setCachePrice(ticker, '105000');
setCachePrice(ticker, '110000');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Await calls to the asynchronous 'setCachePrice' function

Since setCachePrice is an asynchronous function, its invocations should be awaited to ensure that the Redis operations complete before proceeding.

Apply this diff to fix the issue:

-setCachePrice(ticker, '100000');
-setCachePrice(ticker, '105000');
-setCachePrice(ticker, '110000');
+await setCachePrice(ticker, '100000');
+await setCachePrice(ticker, '105000');
+await setCachePrice(ticker, '110000');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setCachePrice(ticker, '100000');
setCachePrice(ticker, '105000');
setCachePrice(ticker, '110000');
await setCachePrice(ticker, '100000');
await setCachePrice(ticker, '105000');
await setCachePrice(ticker, '110000');

Comment on lines +169 to +171
setCachePrice(ticker, '80000');
setCachePrice(ticker, '81000');
setCachePrice(ticker, '80500');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Await calls to the asynchronous 'setCachePrice' function

Again, ensure that you await the calls to setCachePrice to handle the asynchronous nature of the function.

Apply this diff:

-setCachePrice(ticker, '80000');
-setCachePrice(ticker, '81000');
-setCachePrice(ticker, '80500');
+await setCachePrice(ticker, '80000');
+await setCachePrice(ticker, '81000');
+await setCachePrice(ticker, '80500');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setCachePrice(ticker, '80000');
setCachePrice(ticker, '81000');
setCachePrice(ticker, '80500');
await setCachePrice(ticker, '80000');
await setCachePrice(ticker, '81000');
await setCachePrice(ticker, '80500');

Comment on lines +66 to +69
const setCachePrice = (marketTicker: string, price: string) => {
const now = Date.now();
redisClient.zadd(`${ORDERBOOK_MID_PRICES_CACHE_KEY_PREFIX}${marketTicker}`, now, price);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing 'await' in asynchronous Redis operation

The redisClient.zadd function is asynchronous and returns a Promise. Not handling the Promise may lead to unhandled rejections or unexpected behavior. The function setCachePrice should be declared as async, and the zadd operation should be awaited.

Apply this diff to fix the issue:

-const setCachePrice = (marketTicker: string, price: string) => {
+const setCachePrice = async (marketTicker: string, price: string) => {
   const now = Date.now();
-  redisClient.zadd(`${ORDERBOOK_MID_PRICES_CACHE_KEY_PREFIX}${marketTicker}`, now, price);
+  await redisClient.zadd(`${ORDERBOOK_MID_PRICES_CACHE_KEY_PREFIX}${marketTicker}`, now, price);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const setCachePrice = (marketTicker: string, price: string) => {
const now = Date.now();
redisClient.zadd(`${ORDERBOOK_MID_PRICES_CACHE_KEY_PREFIX}${marketTicker}`, now, price);
};
const setCachePrice = async (marketTicker: string, price: string) => {
const now = Date.now();
await redisClient.zadd(`${ORDERBOOK_MID_PRICES_CACHE_KEY_PREFIX}${marketTicker}`, now, price);
};

@@ -435,7 +440,8 @@ describe('candleHelper', () => {
containsKafkaMessages: boolean = true,
orderbookMidPrice: number,
) => {
await OrderbookMidPricesCache.setPrice(redisClient, 'BTC-USD', orderbookMidPrice.toFixed());
setCachePrice('BTC-USD', orderbookMidPrice.toFixed());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Await the asynchronous 'setCachePrice' function

Ensure to await the setCachePrice call to handle the asynchronous operation properly.

Apply this diff:

-setCachePrice('BTC-USD', orderbookMidPrice.toFixed());
+await setCachePrice('BTC-USD', orderbookMidPrice.toFixed());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setCachePrice('BTC-USD', orderbookMidPrice.toFixed());
await setCachePrice('BTC-USD', orderbookMidPrice.toFixed());

} from '@dydxprotocol-indexer/redis';
import { ORDERBOOK_MID_PRICES_CACHE_KEY_PREFIX } from '@dydxprotocol-indexer/redis/build/src/caches/orderbook-mid-prices-cache';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid importing from the 'build' directory

Importing from the 'build' directory directly can lead to issues with portability and maintainability. It is better to import from the source module or an established entry point to ensure consistent behavior across different environments.

Apply this diff to update the import statement:

-import { ORDERBOOK_MID_PRICES_CACHE_KEY_PREFIX } from '@dydxprotocol-indexer/redis/build/src/caches/orderbook-mid-prices-cache';
+import { ORDERBOOK_MID_PRICES_CACHE_KEY_PREFIX } from '@dydxprotocol-indexer/redis';

Committable suggestion skipped: line range outside the PR's diff.

@adamfraser adamfraser merged commit 7454ced into main Nov 26, 2024
16 checks passed
@adamfraser adamfraser deleted the adam/candles-cache-main branch November 26, 2024 13:12
@adamfraser
Copy link
Contributor Author

@mergify backport release/indexer/v7.x

Copy link
Contributor

mergify bot commented Nov 26, 2024

backport release/indexer/v7.x

✅ Backports have been created

mergify bot pushed a commit that referenced this pull request Nov 26, 2024
(cherry picked from commit 7454ced)
adamfraser added a commit that referenced this pull request Nov 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

Successfully merging this pull request may close these issues.

3 participants