Skip to content

fix(worker): resolve #41-#43, #49, #52 - #55

Merged
jonquixote merged 1 commit into
mainfrom
fix/issues-worker
Jul 18, 2026
Merged

fix(worker): resolve #41-#43, #49, #52#55
jonquixote merged 1 commit into
mainfrom
fix/issues-worker

Conversation

@jonquixote

@jonquixote jonquixote commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Fixes #41, #42, #43, #49, #52 — alert cap, memory headroom, index alignment, tick orchestration test, off-market worker filters. Worker suite 56/56 + typecheck clean.

Summary by CodeRabbit

  • New Features
    • Added a configurable cap on watchlists processed during each alert cycle, with a default limit and warning when reached.
  • Bug Fixes
    • Alert notifications and digest results now include only active listings.
    • Rent calculations skip sold, stale, and misclassified rental listings.
    • Increased alert service memory capacity to support bounded watchlist processing.
  • Tests
    • Added coverage for watchlist limits, lifecycle execution order, and aggregated lifecycle results.

…dates, lifecycle tick test, off-market filters

Fixes #41, #42, #43, #49, #52

- alerts.ts: keyset-batched watchlist fetch (ALERT_WATCHLIST_BATCH=2000) — bounded memory
- oper-worker-alerts.service: MemoryMax 192M -> 384M
- CANDIDATES_SQL predicates aligned to the idx_listings_last_seen partial index (subset match), comment names the index
- lifecycle.test.ts: mock-pool orchestration test (step order, params, stats aggregation, null rowCount)
- digest/rent-estimator/watchlist-alerts: lifecycle filters (active-only notifications; NOT IN for recompute)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ytTRmZoRpCSnhKA3xJURj
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
onepercentrealestate Ready Ready Preview, Comment Jul 18, 2026 10:28pm

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The worker now bounds alert-tick watchlist loading, filters inactive listings from digest and alert queries, excludes off-market rent-estimator rows, increases alert service memory, and adds lifecycle orchestration tests.

Changes

Worker processing controls

Layer / File(s) Summary
Bounded alert-tick watchlist loading
apps/worker/src/alerts.ts, apps/worker/src/env.ts, apps/worker/src/alerts.test.ts, ops/systemd/oper-worker-alerts.service
Alert ticks use configurable, ordered watchlist limits, warn when capped, and run with an increased memory limit.
Active listing filtering
apps/worker/src/digest.ts, apps/worker/src/watchlist-alerts.ts, apps/worker/src/rent-estimator.ts
Digest and watchlist alert queries require active listings, while rent estimation excludes sold, stale, and rental-misfiled rows.
Lifecycle tick orchestration coverage
apps/worker/src/lifecycle.test.ts
Tests verify lifecycle SQL ordering, parameter binding, row-count aggregation, and null-count handling.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

  • jonquixote/OnePercentRealEstate issue 42 — Directly addresses bounding watchlists and increasing the alert worker memory limit.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Several files add unrelated digest, lifecycle, rent-estimator, and systemd changes beyond the watchlist-capping issue. Split the non-#41 changes into separate PRs or link the corresponding issues so the scope matches the stated objective.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and references the worker fixes via issue numbers, which are directly related to the change set.
Linked Issues check ✅ Passed The alert tick now caps watchlist fetching with a bounded LIMIT and warning, matching the requirement to avoid loading the whole table.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issues-worker

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/worker/src/alerts.test.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

apps/worker/src/alerts.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

apps/worker/src/digest.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 4 others

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a batch limit on watchlists fetched per alert tick to bound memory and CPU usage, increases the systemd service memory limit to 384M, and updates various queries to filter by active listing status to align with partial indexes. Feedback highlights a critical SQL three-valued logic bug in rent-estimator.ts where using NOT IN on the nullable listing_status column will cause NULL rows to be permanently stuck in a pending state. Additionally, a false positive warning issue was identified in alerts.ts when the watchlist count exactly matches the batch size, which can be resolved by querying for one extra row to detect if more records remain.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +580 to +582
-- Lifecycle (#52): don't spend ML recompute on off-market rows.
-- NOT IN (not = 'active') keeps pending_verify scoreable.
AND listing_status NOT IN ('sold','stale','rental_misfiled')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

SQL Three-Valued Logic Bug & Pending Row Leak

Using NOT IN on a nullable column like listing_status introduces a classic SQL three-valued logic issue. If listing_status is NULL (which is common for legacy rows or before the lifecycle migration fully populates all rows), the expression listing_status NOT IN ('sold','stale','rental_misfiled') evaluates to UNKNOWN rather than TRUE. As a result, any rentable listing with a NULL status will be silently excluded from this query.

Because these rows are also not handled by the settled or noGeo queries, they will be permanently stuck in rent_calc_status = 'pending' and leak/accumulate in the database forever, potentially degrading performance over time.

Recommendations:

  1. Fix the Correctness Bug: Update the predicate to safely handle NULL values by using IS DISTINCT FROM or an explicit OR listing_status IS NULL check.
  2. Settle Off-Market Rows: In a future PR, consider adding a query at the start of drainBatch to transition off-market pending rows directly to done with a rent_model_version like 'off_market_skip' so they do not remain pending indefinitely.
Suggested change
-- Lifecycle (#52): don't spend ML recompute on off-market rows.
-- NOT IN (not = 'active') keeps pending_verify scoreable.
AND listing_status NOT IN ('sold','stale','rental_misfiled')
-- Lifecycle (#52): don't spend ML recompute on off-market rows.
-- NOT IN (not = 'active') keeps pending_verify scoreable.
AND (listing_status IS NULL OR listing_status NOT IN ('sold','stale','rental_misfiled'))

Comment thread apps/worker/src/alerts.ts
Comment on lines 351 to +361
const wlRes = await client.query(
`SELECT user_id, name, query_json FROM watchlists`,
`SELECT user_id, name, query_json FROM watchlists ORDER BY id LIMIT $1`,
[watchlistBatch],
);
watchlists = wlRes.rows;
if (watchlists.length >= watchlistBatch) {
log.warn(
{ cap: watchlistBatch, fetched: watchlists.length },
'watchlists fetch hit ALERT_WATCHLIST_BATCH cap — some watchlists were not evaluated this tick',
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

False Positive Warning on Exact Batch Size Match

Currently, the query uses LIMIT $1 with watchlistBatch, and the warning is triggered if watchlists.length >= watchlistBatch. If the total number of watchlists in the database is exactly equal to watchlistBatch, this warning will fire even though no watchlists were actually skipped. This can lead to false positive warnings in production logs and cause alert fatigue.

Recommendation:

To accurately detect if there are more watchlists remaining, query for watchlistBatch + 1 rows. If the returned length is greater than watchlistBatch, log the warning and slice the array back to watchlistBatch for processing.

Note: If you apply this suggestion, please also update the corresponding test in apps/worker/src/alerts.test.ts (line 320) to expect [2] instead of [1] for the query parameters.

      const wlRes = await client.query(
        "SELECT user_id, name, query_json FROM watchlists ORDER BY id LIMIT $1",
        [watchlistBatch + 1],
      );
      const hasMore = wlRes.rows.length > watchlistBatch;
      watchlists = hasMore ? wlRes.rows.slice(0, watchlistBatch) : wlRes.rows;
      if (hasMore) {
        log.warn(
          { cap: watchlistBatch },
          "watchlists fetch hit ALERT_WATCHLIST_BATCH cap — some watchlists were not evaluated this tick",
        );
      }

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/worker/src/digest.ts (1)

463-503: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid survivorship bias in historical statistics.

Filtering historical cohorts by the current listing_status = 'active' introduces survivorship bias. Listings created in previous time windows (e.g., 7–14 days ago) that were highly desirable and sold quickly will have transitioned to sold and will be excluded from the median_prev, median_now, and new_count calculations. This leaves only homes that failed to sell quickly, artificially skewing your medians and counts.

Use listing_status IN ('active', 'sold', 'pending_verify', 'stale') (excluding just rental_misfiled) to include all legitimately created listings that existed in that historical cohort.

📈 Proposed fix for accurate historical cohorts
       SELECT
         percentile_cont(0.5) WITHIN GROUP (ORDER BY price) FILTER (
-          WHERE listing_type = 'for_sale' AND listing_status = 'active' AND created_at > now() - interval '7 days'
+          WHERE listing_type = 'for_sale' AND listing_status IN ('active', 'sold', 'pending_verify', 'stale') AND created_at > now() - interval '7 days'
         ) AS median_now,
         percentile_cont(0.5) WITHIN GROUP (ORDER BY price) FILTER (
-          WHERE listing_type = 'for_sale' AND listing_status = 'active'
+          WHERE listing_type = 'for_sale' AND listing_status IN ('active', 'sold', 'pending_verify', 'stale')
             AND created_at > now() - interval '14 days'
             AND created_at <= now() - interval '7 days'
         ) AS median_prev
       FROM listings
       WHERE zip_code = $1
     `,
     [zip]
   );
 
   const countsRes = await client.query(
     `
       SELECT
         (SELECT count(*)::int FROM listings
-           WHERE zip_code = $1 AND listing_type = 'for_sale' AND listing_status = 'active'
+           WHERE zip_code = $1 AND listing_type = 'for_sale' AND listing_status IN ('active', 'sold', 'pending_verify', 'stale')
              AND created_at > now() - interval '7 days') AS new_count,
         (SELECT count(*)::int FROM sold_listings
            WHERE zip_code = $1 AND sold_date > now() - interval '7 days') AS sold_count
     `,
     [zip]
   );
 
   // Rent $/sqft trend from listings (estimated_rent / sqft). h3_market_stats
   // is keyed by H3 hex with no H3 index available in Postgres, so it can't be
   // joined to a ZIP here — see report note. We fall back to the listings
   // surface, which is ZIP-keyed and real.
   const rentRes = await client.query(
     `
       SELECT
         percentile_cont(0.5) WITHIN GROUP (ORDER BY estimated_rent / NULLIF(sqft, 0)) FILTER (
-          WHERE listing_type = 'for_sale' AND listing_status = 'active' AND created_at > now() - interval '7 days'
+          WHERE listing_type = 'for_sale' AND listing_status IN ('active', 'sold', 'pending_verify', 'stale') AND created_at > now() - interval '7 days'
             AND sqft > 0 AND estimated_rent > 0
         ) AS rent_now,
         percentile_cont(0.5) WITHIN GROUP (ORDER BY estimated_rent / NULLIF(sqft, 0)) FILTER (
-          WHERE listing_type = 'for_sale' AND listing_status = 'active'
+          WHERE listing_type = 'for_sale' AND listing_status IN ('active', 'sold', 'pending_verify', 'stale')
             AND created_at > now() - interval '14 days'
             AND created_at <= now() - interval '7 days'
             AND sqft > 0 AND estimated_rent > 0
         ) AS rent_prev
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/worker/src/digest.ts` around lines 463 - 503, Update the historical
cohort filters in the median queries and new_count calculation to use
listing_status IN ('active', 'sold', 'pending_verify', 'stale') instead of
requiring listing_status = 'active'. Apply this to median_prev, median_now, and
the listings subquery for new_count, while continuing to exclude rental_misfiled
listings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/worker/src/digest.ts`:
- Around line 463-503: Update the historical cohort filters in the median
queries and new_count calculation to use listing_status IN ('active', 'sold',
'pending_verify', 'stale') instead of requiring listing_status = 'active'. Apply
this to median_prev, median_now, and the listings subquery for new_count, while
continuing to exclude rental_misfiled listings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57c59d00-2901-420a-8f1f-8191f516e5b4

📥 Commits

Reviewing files that changed from the base of the PR and between 04aa89b and 5dfd9f3.

📒 Files selected for processing (8)
  • apps/worker/src/alerts.test.ts
  • apps/worker/src/alerts.ts
  • apps/worker/src/digest.ts
  • apps/worker/src/env.ts
  • apps/worker/src/lifecycle.test.ts
  • apps/worker/src/rent-estimator.ts
  • apps/worker/src/watchlist-alerts.ts
  • ops/systemd/oper-worker-alerts.service

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.

Cap/paginate unbounded SELECT FROM watchlists in alert tick

1 participant