Skip to content

Conversation

mavriel
Copy link

@mavriel mavriel commented Sep 11, 2025

Pull Request

Issue

Closes: #9850

Approach

This fix addresses a data consistency issue in distributed database environments where beforeSave trigger validation queries may read from stale read replicas while update operations target the primary database.

Problem

During beforeSave trigger execution, Parse Server performs permission validation by querying the database before applying updates. However:

  • Update operations always target the primary database
  • Validation queries use default database settings, which may route to read replicas
  • In environments with replication lag, validation may fail with "Object not found" errors when the object exists in primary but hasn't propagated to replicas yet

Solution

Modified DatabaseController.update() method to ensure validation queries (validateOnly: true) explicitly use { readPreference: 'primary' }. This guarantees that:

  • Validation reads from the same database that updates will target
  • Eliminates race conditions caused by replication lag
  • Maintains consistency between validation and update operations

Key Changes

  • Added { readPreference: 'primary' } option for validation queries in src/Controllers/DatabaseController.js
  • Only affects internal validation behavior during beforeSave triggers
  • No changes to public APIs or user-facing functionality

Testing

Added comprehensive test coverage in spec/DatabaseController.spec.js:

  • Verifies validateOnly: true queries use primary readPreference
  • Confirms normal updates maintain existing behavior
  • Uses mock storage adapter to validate exact query parameters

Tasks

  • Add tests
  • Add changes to documentation (guides, repository pages, code comments)
  • Add security check
  • Add new Parse Error codes to Parse JS SDK

Summary by CodeRabbit

  • Bug Fixes

    • Validation-only updates now read from the primary database to prevent stale data and intermittent “not found” responses during validation. No change to API or update results.
  • Tests

    • Added tests to verify validate-only update behavior, ensuring primary-read validation and preventing unintended update calls.

…eadPreference

- Ensures update validation always reads from primary replica in DB
- Fixes potential data consistency issues in distributed database environments
- Adds comprehensive tests for validateOnly behavior with primary readPreference
Copy link

parse-github-assistant bot commented Sep 11, 2025

🚀 Thanks for opening this pull request!

Copy link

coderabbitai bot commented Sep 11, 2025

📝 Walkthrough

Walkthrough

Adds a primary-read option to the validation path of DatabaseController.update when validateOnly is true. Updates tests to cover both validateOnly=true and validateOnly=false paths, using a mock storage adapter to assert read vs update calls and their arguments.

Changes

Cohort / File(s) Summary
Controller: validateOnly read preference
src/Controllers/DatabaseController.js
In the validateOnly branch of update, changes adapter find call options from {} to { readPreference: 'primary' }. No other logic changes.
Tests: update with validateOnly
spec/DatabaseController.spec.js
Adds a new test suite verifying: when validateOnly is true, find is called with readPreference: 'primary' and findOneAndUpdate is not called; when false, find is not called and findOneAndUpdate is called. Introduces a mock storage adapter with spies.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant DBController as DatabaseController
  participant Adapter as StorageAdapter

  rect rgb(245,250,255)
  note over Client,DBController: validateOnly = true (validation path)
  Client->>DBController: update(request, validateOnly=true)
  DBController->>Adapter: find(query, options={ readPreference: 'primary' })
  alt object not found
    DBController-->>Client: error OBJECT_NOT_FOUND
  else object found
    DBController-->>Client: {}
  end
  end

  rect rgb(250,245,255)
  note over Client,DBController: validateOnly = false (update path)
  Client->>DBController: update(request, validateOnly=false)
  DBController->>Adapter: findOneAndUpdate(query, update, options)
  Adapter-->>DBController: result
  DBController-->>Client: result/ack
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed The pull request description closely follows the repository template and is mostly complete: it includes the issue link (Closes: #9850), a clear Approach that explains the problem, the implemented solution, key changes, and a Testing section describing the added tests. The Tasks checklist is present and correctly marks tests as added while leaving documentation and other tasks unchecked. Overall the description provides sufficient context for reviewers to understand intent and scope.
Title Check ✅ Passed The title clearly and concisely summarizes the primary change by stating the intent (prevent stale reads during update validation) and the technical fix (use primary readPreference), so it accurately reflects the changeset and is specific enough for a teammate scanning PR history.

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.

@parseplatformorg
Copy link
Contributor

parseplatformorg commented Sep 11, 2025

🎉 Snyk checks have passed. No issues have been found so far.

security/snyk check is complete. No issues have been found. (View Details)

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: 0

🧹 Nitpick comments (3)
src/Controllers/DatabaseController.js (1)

596-602: Force-primary read is correct; add limit and projection to cut load

Only existence is needed here. Pass limit: 1 and a minimal projection to reduce I/O without altering semantics.

-              if (validateOnly) {
-                return this.adapter.find(className, schema, query, { readPreference: 'primary' }).then(result => {
+              if (validateOnly) {
+                // Read from primary to avoid replica lag during beforeSave validation (see #9850).
+                return this.adapter.find(
+                  className,
+                  schema,
+                  query,
+                  { readPreference: 'primary', limit: 1, keys: ['objectId'] }
+                ).then(result => {
spec/DatabaseController.spec.js (2)

619-633: Prevent schema cache bleed between tests

Clear the schema cache in this suite to avoid flaky behavior from prior tests.

   describe('update with validateOnly', () => {
+    beforeEach(() => {
+      Config.get(Parse.applicationId).schemaCache.clear();
+    });

655-657: Make assertion resilient to future option additions

Use partial match so the test won’t break if more options (e.g., limit, keys) are passed.

-      expect(findCall.args[3]).toEqual({ readPreference: 'primary' }); // options parameter
+      expect(findCall.args[3]).toEqual(jasmine.objectContaining({ readPreference: 'primary' })); // options parameter
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 82fdb0d and 59e599b.

📒 Files selected for processing (2)
  • spec/DatabaseController.spec.js (1 hunks)
  • src/Controllers/DatabaseController.js (1 hunks)
🔇 Additional comments (1)
spec/DatabaseController.spec.js (1)

663-687: LGTM: correctly asserts update path when validateOnly is false

Good coverage ensuring validation read is bypassed and write path is invoked.

@mavriel mavriel changed the title fix: Prevent stale data reads during update validation with primary r… fix: Prevent stale data reads during update validation with primary readPreference Sep 11, 2025
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.

Ability to specify custom readPreference for beforeSave's internal validation
2 participants