Skip to content

Improve performance for pools with 30k+ users#38

Merged
mrv777 merged 13 commits intomrv777:mainfrom
pdath:performance
Mar 19, 2026
Merged

Improve performance for pools with 30k+ users#38
mrv777 merged 13 commits intomrv777:mainfrom
pdath:performance

Conversation

@pdath
Copy link
Contributor

@pdath pdath commented Feb 15, 2026

This branch significantly improves ckstats performance when a pool has a large number of users. CPU load on the PostgreSQL server has been significantly reduced. Key changes include rewriting many queries and introducing node-cache to cache expensive operations.

Also, many modules have been updated, which has reduced the number of "pnpm audit" issues reported in issue #31. Versions have been selected that retain compatibility with Ubuntu 22.04.

A race condition in getDb() has also been resolved.

pnpm is not correctly updating dependencies when you apply this branch. Consequently, you'll need to run "pnpm store prune" before "pnpm build" to ensure it builds correctly.

Summary by CodeRabbit

  • Documentation

    • Expanded installation steps with a store-prune step when upgrading and detailed cron job examples.
  • New Features

    • Added API capabilities to list top user difficulties and hashrates.
  • Improvements

    • Caching and database connection tweaks for faster responses and more reliable backend access.
  • Chores

    • Dependency updates and minor maintenance.

@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2dfa3d24-8740-49f2-92e1-2f78200f5e5d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Removed react-hooks ESLint rules, expanded README install and cronjob instructions, added NodeCache-based caching and new top-user DB queries in the API, increased DB pool sizes and introduced an exported async getDb() accessor, and bumped several dependencies (added node-cache).

Changes

Cohort / File(s) Summary
Build Configuration
\.eslintrc.json, package.json
Removed plugin:react-hooks/recommended and react-hooks plugin entry; updated multiple dependencies and devDependencies and added node-cache.
Documentation
README.md
Reordered installation steps, added pnpm store pruning step, renumbered steps, and expanded cronjob examples and instructions.
API Caching & Top Users
lib/api.ts
Added node-cache usage; cached pool stats (60s) and top-user queries (300s); refactored user stats fetch to use latest-per-user; added getTopUserDifficulties(limit) and getTopUserHashrates(limit).
Database Access
lib/db.ts
Increased connection pool (min 2→10, max 10→20); removed default AppDataSource export; added exported async getDb() initializer/accessor with documented retry/initialization semantics.

Sequence Diagram

sequenceDiagram
    participant Client
    participant API as API Layer
    participant Cache as NodeCache
    participant DB as Database

    Client->>API: getLatestPoolStats()
    API->>Cache: get("pool:latest")
    alt Cache Hit
        Cache-->>API: cached stats
    else Cache Miss
        API->>DB: query latest pool stats
        DB-->>API: stats
        API->>Cache: set("pool:latest", stats, 60)
    end
    API-->>Client: stats

    Client->>API: getTopUserHashrates(limit)
    API->>Cache: get("top:hashrates:" + limit)
    alt Cache Hit
        Cache-->>API: cached rankings
    else Cache Miss
        API->>DB: lateral join latest UserStats per user, order by hashrate1hr, limit
        DB-->>API: top users
        API->>Cache: set("top:hashrates:" + limit, results, 300)
    end
    API-->>Client: rankings
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through docs and code today,
Caching crumbs along the way,
Latest stats snug in a tiny nest,
DB pools deepened for their rest,
New top-rabbit rankings bound to play.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the pull request—improving performance for large pools (30k+ users) through caching, query optimization, and database pool adjustments.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use Trivy to scan for security misconfigurations and secrets in Infrastructure as Code files.

Add a .trivyignore file to your project to customize which findings Trivy reports.

Copy link

@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: 1

🤖 Fix all issues with AI agents
In `@lib/api.ts`:
- Around line 182-190: The map that builds `result` in lib/api.ts calls
`.toString()` on fields like `bestEver`, `hashrate1hr`, `hashrate1d`,
`hashrate7d`, and `bestShare` which will throw if any are null; update the
mapping in the function that returns `result` to defensively handle
null/undefined (e.g., use nullish coalescing or String(value ?? 0) or
value?.toString() ?? "0") so you never call `.toString()` on null, and apply the
same defensive pattern to the analogous mapping in getTopUserHashrates to
prevent runtime errors from NULL DB columns.
🧹 Nitpick comments (5)
lib/db.ts (2)

26-27: Consider the aggregate connection count across Next.js workers.

With min: 10, each Node.js process will eagerly open 10 connections. If Next.js spawns multiple workers (common in production), the total minimum connections could exceed PostgreSQL's max_connections default (typically 100). Ensure your PostgreSQL max_connections accommodates min * number_of_workers plus headroom for scripts (seed, update-users, cleanup) that also connect.


56-73: Race-condition comment is accurate for Node.js; minor nit on identity .then().

The concurrency guard is correct — Node.js is single-threaded so the if (!connectionPromise) check plus immediate assignment is atomic. The .then((connection) => { return connection; }) on line 61-62 is a no-op identity transform that can be simplified.

♻️ Suggested simplification
   if (!connectionPromise) {
-    connectionPromise = AppDataSource.initialize()
-      .then((connection) => {
-        return connection;
-      })
-      .catch((error) => {
+    connectionPromise = AppDataSource.initialize().catch((error) => {
         console.error('Database connection error:', error);
         connectionPromise = null;
         throw error;
       });
   }
package.json (1)

26-26: next changed from pinned to caret range.

The version specifier changed from "14.2.18" (exact) to "^14.2.18" (caret). This now allows automatic minor/patch upgrades within 14.x. If the previous pinning was intentional (e.g., to avoid regressions), consider keeping it exact or using a tilde ~14.2.18 for patch-only updates.

lib/api.ts (2)

154-158: Inconsistent cache-hit check — use !== undefined for safety.

getLatestPoolStats (line 31) and getHistoricalPoolStats (line 54) correctly use cached !== undefined to distinguish a cache miss from a cached falsy value. Here and in getTopUserHashrates (line 214), you use if (cached), which would skip the cache if the cached value were an empty array []… except empty arrays are truthy in JS, so it works today. Still, aligning with the !== undefined pattern used elsewhere avoids a subtle trap if the return type ever changes.

♻️ Suggested fix
   const cached = cache.get(cacheKey);
-  if (cached) {
+  if (cached !== undefined) {
     return cached;
   }

Apply the same change in getTopUserHashrates (line 214).


113-137: getWorkerWithStats loads all stats eagerly — consider the same "latest only" optimization.

Other functions were refactored to fetch only the latest stat row. This function still loads all worker stats via relations: { stats: true } and sorts them in-memory. For workers with long histories, this could be a significant amount of data. If callers only need recent stats, consider applying a similar query-builder approach with a limit.

Comment on lines +182 to 190
const result = rows.map((r: any) => ({
address: r.userAddress,
workerCount: r.workerCount,
difficulty: r.bestEver.toString(),
hashrate1hr: r.hashrate1hr.toString(),
hashrate1d: r.hashrate1d.toString(),
hashrate7d: r.hashrate7d.toString(),
bestShare: r.bestShare.toString(),
}));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Null safety: .toString() will throw if any field is null.

The raw query returns untyped rows. If any column (bestEver, hashrate1hr, etc.) contains a NULL value, calling .toString() on it will throw a runtime error. While the JOIN LATERAL ensures a stats row exists, individual columns could still be NULL depending on schema constraints.

🛡️ Defensive approach
   const result = rows.map((r: any) => ({
     address: r.userAddress,
     workerCount: r.workerCount,
-    difficulty: r.bestEver.toString(),
-    hashrate1hr: r.hashrate1hr.toString(),
-    hashrate1d: r.hashrate1d.toString(),
-    hashrate7d: r.hashrate7d.toString(),
-    bestShare: r.bestShare.toString(),
+    difficulty: r.bestEver?.toString() ?? '0',
+    hashrate1hr: r.hashrate1hr?.toString() ?? '0',
+    hashrate1d: r.hashrate1d?.toString() ?? '0',
+    hashrate7d: r.hashrate7d?.toString() ?? '0',
+    bestShare: r.bestShare?.toString() ?? '0',
   }));

Apply the same pattern to getTopUserHashrates (lines 240-248).

📝 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 result = rows.map((r: any) => ({
address: r.userAddress,
workerCount: r.workerCount,
difficulty: r.bestEver.toString(),
hashrate1hr: r.hashrate1hr.toString(),
hashrate1d: r.hashrate1d.toString(),
hashrate7d: r.hashrate7d.toString(),
bestShare: r.bestShare.toString(),
}));
const result = rows.map((r: any) => ({
address: r.userAddress,
workerCount: r.workerCount,
difficulty: r.bestEver?.toString() ?? '0',
hashrate1hr: r.hashrate1hr?.toString() ?? '0',
hashrate1d: r.hashrate1d?.toString() ?? '0',
hashrate7d: r.hashrate7d?.toString() ?? '0',
bestShare: r.bestShare?.toString() ?? '0',
}));
🤖 Prompt for AI Agents
In `@lib/api.ts` around lines 182 - 190, The map that builds `result` in
lib/api.ts calls `.toString()` on fields like `bestEver`, `hashrate1hr`,
`hashrate1d`, `hashrate7d`, and `bestShare` which will throw if any are null;
update the mapping in the function that returns `result` to defensively handle
null/undefined (e.g., use nullish coalescing or String(value ?? 0) or
value?.toString() ?? "0") so you never call `.toString()` on null, and apply the
same defensive pattern to the analogous mapping in getTopUserHashrates to
prevent runtime errors from NULL DB columns.

@mrv777
Copy link
Owner

mrv777 commented Mar 19, 2026

@pdath i think pnpm-lock just needs to be updated and then can merge

@pdath
Copy link
Contributor Author

pdath commented Mar 19, 2026

@pdath i think pnpm-lock just needs to be updated and then can merge

This should be done now.

@mrv777 mrv777 merged commit d9cc84f into mrv777:main Mar 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants