✨ server: add debt repayment notification#722
✨ server: add debt repayment notification#722aguxez wants to merge 1 commit intofeature/webhook-queuefrom
Conversation
🦋 Changeset detectedLatest commit: 1d662ab The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR introduces a debt repayment notification system using BullMQ queues. It adds a maturity queue for periodically checking user debts across 24h and 1h windows, processing accounts in batches, selecting between market and previewer implementations, and triggering push notifications via Redis-based deduplication. Changes
Sequence DiagramsequenceDiagram
participant Scheduler as Scheduler
participant Queue as BullMQ Queue
participant Worker as Maturity Worker
participant DB as Database
participant Contract as Contract (Market/Previewer)
participant Redis as Redis
participant Sentry as Sentry
participant Push as Push Notifications
Scheduler->>Queue: scheduleMaturityChecks() creates<br/>CHECK_DEBTS jobs (24h, 1h)
Queue->>Worker: Job available
Worker->>DB: Read accounts in batch (250)
Worker->>Contract: Query debt status<br/>(implementation-dependent)
Contract->>Worker: Debt results per user
Worker->>Redis: Check notification<br/>idempotency key
alt Debt exists & not notified
Worker->>Redis: Write idempotency key
Worker->>Push: Send push notification
end
Worker->>Sentry: Log metrics & breadcrumbs<br/>(contract calls, errors, results)
alt Window is "1h"
Worker->>Queue: Schedule next maturity checks<br/>(increment maturity)
end
Worker->>Queue: Mark job complete
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment Tip CodeRabbit can use your project's `biome` configuration to improve the quality of JS/TS/CSS/JSON code reviews.Add a configuration file to your project to customize how CodeRabbit runs |
Summary of ChangesHello @aguxez, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a critical new feature to proactively notify users about their upcoming debt maturities. It establishes a robust server-side queueing system using BullMQ to periodically check user debt positions on the blockchain. Depending on configuration, it leverages either direct market contract interactions or a previewer contract to identify at-risk users. Timely push notifications are then dispatched via OneSignal, with Redis ensuring that users receive alerts only once per window, aiming to help users manage their debts and avoid potential liquidations. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
|
✅ All tests passed. |
6550d21 to
45be58c
Compare
45be58c to
06f8efd
Compare
06f8efd to
ef1ce32
Compare
ef1ce32 to
26e6a32
Compare
26e6a32 to
2105108
Compare
2105108 to
b229c65
Compare
b229c65 to
e267d55
Compare
e267d55 to
fed4d42
Compare
fed4d42 to
1ed3cb9
Compare
06d840d to
0c1eceb
Compare
0c1eceb to
82c77f5
Compare
82c77f5 to
96db3ec
Compare
96db3ec to
38b719e
Compare
38b719e to
616ebf5
Compare
616ebf5 to
bf7e914
Compare
bf7e914 to
a5d75cc
Compare
a5d75cc to
65ebdb9
Compare
4bce409 to
a5d5c22
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a9d65a5d97
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
server/utils/maturity.ts
Outdated
| { maturity: nextMaturity, window: "24h" }, | ||
| { | ||
| jobId: `check-debts-${nextMaturity}-24h`, | ||
| delay: Math.max(0, (nextMaturity - 24 * 3600 - now) * 1000), |
There was a problem hiding this comment.
Skip stale reminder windows when scheduling jobs
Using Math.max(0, ...) here enqueues reminder jobs immediately whenever the server starts after a reminder window has already passed (e.g., deploy/restart 2 hours before maturity still runs the "24h" job right away). Because server/index.ts calls scheduleMaturityChecks() on startup, this can send users inaccurate notifications ("due in 24 hours" / "due in 1 hour" when that is no longer true) and produce bursts of stale alerts after downtime; the scheduler should avoid creating jobs for windows that are already in the past.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a9ac13074c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
server/utils/maturity.ts
Outdated
| 86_400, | ||
| "NX", | ||
| ) | ||
| .then((r) => { | ||
| if (r === "OK") { |
There was a problem hiding this comment.
Set dedupe key only after push notification succeeds
Writing the notification:sent:* key before sendPushNotification means transient OneSignal failures permanently suppress that window’s alert: redis.set(..., "NX") succeeds, the push call rejects, and subsequent attempts (including retries or duplicate account rows) skip sending because the key already exists for 24h. In production this causes silent missed maturity notifications for affected users during temporary push-provider/API errors.
Useful? React with 👍 / 👎.
| async function checkDebts(chunk: { account: string }[], maturity: number): Promise<DebtCheckResult> { | ||
| const promises = chunk.map(({ account }) => | ||
| publicClient.readContract({ | ||
| address: previewerAddress, | ||
| abi: previewerAbi, | ||
| functionName: "exactly", | ||
| args: [account as `0x${string}`], | ||
| }), | ||
| ); | ||
|
|
||
| const results = await Promise.allSettled(promises); | ||
| const accounts: DebtCheckResult["accounts"] = []; | ||
|
|
||
| for (const [index, result] of results.entries()) { | ||
| const entry = chunk[index]; | ||
| if (!entry) continue; | ||
| const { account } = entry; | ||
| if (result.status === "rejected") { | ||
| captureException(result.reason, { extra: { account } }); | ||
| continue; | ||
| } | ||
| const hasDebt = result.value.some((market) => | ||
| market.fixedBorrowPositions.some((p) => p.maturity === BigInt(maturity) && p.position.principal > 0n), | ||
| ); | ||
| accounts.push({ account, hasDebt }); | ||
| } | ||
|
|
||
| return { accounts, contractCalls: chunk.length }; | ||
| } |
There was a problem hiding this comment.
🚩 No worker rate limiter unlike the account queue worker
The maturity worker is created without a limiter option (server/utils/maturity.ts:160), unlike the account queue worker which has limiter: { max: 10, duration: 1000 } at server/utils/createCredential.ts:121. Inside checkDebts, all accounts in a chunk (up to 50) fire concurrent readContract calls via Promise.allSettled (server/utils/maturity.ts:140). This could cause bursts of 50 simultaneous RPC calls per chunk. Whether this is a concern depends on the RPC provider's rate limits. The allSettled pattern handles individual failures gracefully, so this isn't a correctness issue, but it may warrant a concurrency limit if the provider rate-limits read calls.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1d662abd9c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (window === "1h") { | ||
| try { | ||
| await scheduleMaturityChecks(maturity); |
There was a problem hiding this comment.
Keep scheduling when no 1h reminder job is enqueued
Rescheduling is gated behind processing a window: "1h" job, so a startup that happens in the last hour before maturity (or after queue state is wiped during that period) can enqueue no jobs at all and never schedule future maturities. In that scenario scheduleMaturityChecks() skips stale windows, no 1h job runs, and this if (window === "1h") branch is never reached again, leaving notifications disabled until a later manual restart.
Useful? React with 👍 / 👎.
| const delay24h = (nextMaturity - 24 * 3600 - now) * 1000; | ||
| if (delay24h >= 0) { | ||
| await queue.add( | ||
| "check-debts", | ||
| { maturity: nextMaturity, window: "24h" }, | ||
| { | ||
| jobId: `check-debts-${nextMaturity}-24h`, | ||
| delay: delay24h, | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| const delay1h = (nextMaturity - 3600 - now) * 1000; | ||
| if (delay1h >= 0) { | ||
| await queue.add( | ||
| "check-debts", | ||
| { maturity: nextMaturity, window: "1h" }, | ||
| { | ||
| jobId: `check-debts-${nextMaturity}-1h`, | ||
| delay: delay1h, | ||
| }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
🔴 Rescheduling chain permanently breaks when server starts within 1 hour of the next maturity
When scheduleMaturityChecks() is called on startup without arguments (server/index.ts:346), nextMaturity is computed as the next maturity after now. If the server starts within 1 hour of that maturity, both delay24h and delay1h are negative, so both if guards at lines 194 and 206 fail and no jobs are scheduled. Since rescheduling for the next maturity only happens inside the 1h window processor's finally block (server/utils/maturity.ts:106), and no 1h job was ever enqueued, no future maturity checks will ever be scheduled. The notification system silently and permanently stops until the next server restart.
Scenario walkthrough
- Server starts when
nowis betweennextMaturity - 3600andnextMaturity. delay24h = (nextMaturity - 86400 - now) * 1000→ negative → skipped.delay1h = (nextMaturity - 3600 - now) * 1000→ negative → skipped.- No jobs enqueued → no
1hprocessor runs →scheduleMaturityChecks(maturity)in thefinallyblock never fires → chain is broken forever.
Prompt for agents
In server/utils/maturity.ts, the scheduleMaturityChecks function (lines 186-216) needs to handle the case where both delay24h and delay1h are negative for the computed nextMaturity. When this happens, the function should advance to the following maturity (nextMaturity + MATURITY_INTERVAL) and attempt to schedule jobs for that one instead, to ensure the rescheduling chain is never broken. One approach: after the two if-blocks, check if neither job was scheduled, and if so, recursively call scheduleMaturityChecks(nextMaturity) or loop to try the next maturity. Make sure to avoid infinite recursion by bounding how far ahead it looks (e.g., one extra interval should suffice).
Was this helpful? React with 👍 or 👎 to provide feedback.
|
|
||
| import { sendPushNotification } from "./onesignal"; | ||
| import publicClient from "./publicClient"; | ||
| import { queue as redis } from "./redis"; |
There was a problem hiding this comment.
🚩 BullMQ queue connection used for application-level Redis operations
The module imports { queue as redis } from ./redis (server/utils/redis.ts:8) — the connection configured with maxRetriesPerRequest: null for BullMQ. This same connection is then used for application-level get/set operations (notification dedup keys at lines 73, 81). While functionally correct, mixing BullMQ's dedicated connection with arbitrary commands is unusual. The default Redis export (without maxRetriesPerRequest: null) would be more appropriate for the dedup keys, keeping the BullMQ connection isolated for queue operations.
Was this helpful? React with 👍 or 👎 to provide feedback.
This is part 2 of 2 in a stack made with GitButler:
Summary by CodeRabbit
Release Notes