Skip to content

fix: add concurrency validation for Ecto projections - #33

Merged
yordis merged 1 commit into
mainfrom
yordis/fix-concurrency
Nov 20, 2025
Merged

yordis merged 1 commit into
mainfrom
yordis/fix-concurrency

Conversation

@yordis

@yordis yordis commented Nov 20, 2025 •

Copy link
Copy Markdown
Member

Note

Adds compile-time validation blocking :concurrency > 1 for Ecto projections (with clearer errors and batch compatibility), updates guides, and adds tests.

  • Library (lib/commanded/projections/ecto.ex):
    • Add compile-time :concurrency validation (validate_concurrency_compatibility/1), rejecting values > 1 and invalid types; allow concurrency: 1.
    • Refine mutual exclusivity: permit batch_size with concurrency: 1; error when :concurrency > 1 with/without :batch_size.
    • Extend moduledoc with explicit concurrency limitation and guidance to use :batch_size.
    • Wire validations into __using__/1; improve error messages.
  • Docs:
    • New explanation guide: guides/explanations/ecto-projections.md (architecture, idempotency, batching, no-concurrency rationale).
    • Update how-to and explanations to link to new guide and clarify examples/naming.
  • Tests (test/projections/ecto_projection_test.exs):
    • Add tests covering allowed concurrency: 1, absence of option, rejection of > 1, mutual exclusivity with batch_size, and invalid values (atom, string, list, 0, negative).
    • Assert informative error messages (mention out-of-order processing, silent data loss, and :batch_size alternative).

Written by Cursor Bugbot for commit ca4f988. This will update automatically on new commits. Configure here.

@coderabbitai

coderabbitai Bot commented Nov 20, 2025 •

Copy link
Copy Markdown

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

The PR adds comprehensive documentation for Ecto projections covering architecture, idempotency mechanisms, and configuration patterns. It restructures existing how-to guides with updated examples reflecting new module naming conventions and domain events. Additionally, concurrency validation logic is introduced to enforce a maximum concurrency of 1, with guidance to use batch_size instead.

Changes

Cohort / File(s) Summary
Documentation: New Explanations
guides/explanations/ecto-projections.md
New comprehensive guide documenting Ecto projections architecture, idempotency via watermark mechanism, transaction semantics with Ecto.Multi, callback behavior, batch processing workflow, concurrency limitations, and multi-tenant schema prefixing.
Documentation: How-To Guide Restructuring
guides/howtos/building-read-models-with-ecto.md
Restructured with reorganized sections, renamed modules (ExampleProjection → MyApp.Accounts.Projections.Account), updated domain event examples, batch processing emphasis, and expanded multi-tenant configuration details. Updated public API examples for projector options, callbacks, error handling, and runtime configuration.
Documentation: Reference Updates
guides/explanations/read-model-projections.md, guides/howtos/ecto-projections-getting-started.md
Updated external references and links to point to new documentation paths in howtos directory.
Implementation: Concurrency Validation
lib/commanded/projections/ecto.ex
Added new public function validate_concurrency_compatibility/1 to enforce concurrency ≤ 1 at compile time. Updated documentation with "Concurrency Limitation" section explaining rationale and recommending batch_size usage.
Tests: Validation Coverage
test/projections/ecto_projection_test.exs
Added comprehensive test suites covering concurrency edge cases (values > 1, invalid types, defaults), mutual exclusivity with batch_size, and error message validation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

  • guides/howtos/building-read-models-with-ecto.md: Requires careful review of module naming changes, event type updates, and API pattern consistency across multiple examples.
  • lib/commanded/projections/ecto.ex: New validation logic and error pathways require verification of edge case handling and compile-time error messaging.
  • test/projections/ecto_projection_test.exs: Extensive test additions need validation that all combinations and error messages are correct and complete.

Possibly related PRs

Poem

🐰 A rabbit hops through projections so keen,
With watermarks flowing, idempotent and clean!
Batch-sized morsels, no concurrency dance—
Just sequential steps in a coordinated prance.
✨ Ecto's projections, now documented just right!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding concurrency validation for Ecto projections, which is the core focus across library code, documentation, and tests.
Description check ✅ Passed The description comprehensively covers the changeset: it details library validation changes, documentation additions, and test coverage, all directly related to the concurrency validation implementation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch yordis/fix-concurrency

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.

@yordis
yordis force-pushed the yordis/fix-concurrency branch from 28ad85a to b0ab1e7 Compare November 20, 2025 00:11
Comment thread lib/commanded/projections/ecto.ex
@yordis
yordis force-pushed the yordis/fix-concurrency branch 3 times, most recently from 84a91ca to 40caf96 Compare November 20, 2025 00:27
Comment thread lib/commanded/projections/ecto.ex
@yordis
yordis force-pushed the yordis/fix-concurrency branch from 40caf96 to a5d8d74 Compare November 20, 2025 00:30
Comment thread lib/commanded/projections/ecto.ex Outdated
@yordis
yordis force-pushed the yordis/fix-concurrency branch 3 times, most recently from 5754362 to c9a8230 Compare November 20, 2025 03:07
Comment thread lib/commanded/projections/ecto.ex
@yordis
yordis force-pushed the yordis/fix-concurrency branch from c9a8230 to ac8df89 Compare November 20, 2025 03:15
Comment thread lib/commanded/projections/ecto.ex
@yordis
yordis force-pushed the yordis/fix-concurrency branch from ac8df89 to 78890f4 Compare November 20, 2025 04:07
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis
yordis force-pushed the yordis/fix-concurrency branch from 78890f4 to ca4f988 Compare November 20, 2025 04:10
@yordis
yordis marked this pull request as ready for review November 20, 2025 04:11

@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.

Actionable comments posted: 1

♻️ Duplicate comments (2)
lib/commanded/projections/ecto.ex (2)

89-117: Critical: Mutual exclusivity check also misses :subscription_opts[:concurrency].

Same issue as validate_concurrency_compatibility/1 - this function only checks top-level :concurrency. This allows:

use Commanded.Projections.Ecto,
  batch_size: 10,
  subscription_opts: [concurrency: 10]  # Should error but passes!

Apply this diff:

 @doc false
 def validate_mutual_exclusivity(opts) do
   batch_size = Keyword.get(opts, :batch_size)
-  concurrency = Keyword.get(opts, :concurrency)
+  top_level = Keyword.get(opts, :concurrency)
+  nested = opts |> Keyword.get(:subscription_opts, []) |> Keyword.get(:concurrency)
+  concurrency = top_level || nested

131-159: Critical: Validation misses :concurrency in :subscription_opts.

The function only checks top-level :concurrency but doesn't validate opts[:subscription_opts][:concurrency]. Users following Commanded's event handler patterns can set concurrency within subscription_opts, completely bypassing this validation.

This allows dangerous configurations like:

use Commanded.Projections.Ecto,
  batch_size: 10,
  subscription_opts: [concurrency: 4]  # Silently bypasses validation!

Apply this diff to check both locations:

 @doc false
 def validate_concurrency_compatibility(opts) do
-  concurrency = Keyword.get(opts, :concurrency)
+  top_level = Keyword.get(opts, :concurrency)
+  nested = opts |> Keyword.get(:subscription_opts, []) |> Keyword.get(:concurrency)
+  concurrency = top_level || nested

   case concurrency do
🧹 Nitpick comments (1)
guides/howtos/building-read-models-with-ecto.md (1)

206-239: Consider using headings instead of bold text for options.

The bold text for "Option 1:", "Option 2:", etc. could be converted to level-4 headings (####) for better document structure. This would also resolve the markdownlint warnings.

Example:

-**Option 1: Static schema prefix**
+#### Option 1: Static schema prefix

However, the current formatting is acceptable and may better suit the guide's structure.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af1bb2c and ca4f988.

📒 Files selected for processing (6)
  • guides/explanations/ecto-projections.md (1 hunks)
  • guides/explanations/read-model-projections.md (1 hunks)
  • guides/howtos/building-read-models-with-ecto.md (1 hunks)
  • guides/howtos/ecto-projections-getting-started.md (1 hunks)
  • lib/commanded/projections/ecto.ex (4 hunks)
  • test/projections/ecto_projection_test.exs (1 hunks)
🧰 Additional context used
🪛 markdownlint-cli2 (0.18.1)
guides/howtos/building-read-models-with-ecto.md

206-206: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


216-216: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


229-229: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


267-267: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


290-290: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Quality Assurance (1.18.x, 27)
🔇 Additional comments (6)
guides/howtos/ecto-projections-getting-started.md (1)

75-75: LGTM!

The updated documentation references provide clear navigation to both practical examples and architectural details.

guides/explanations/ecto-projections.md (2)

92-123: Excellent explanation of the concurrency limitation.

The timeline diagram clearly illustrates the race condition and data loss scenario. The guidance to use :batch_size instead provides a practical alternative.


61-91: LGTM!

The batch processing section clearly explains the workflow, benefits, and trade-offs. The step-by-step breakdown is particularly helpful for understanding the mechanism.

lib/commanded/projections/ecto.ex (1)

64-79: LGTM!

The concurrency limitation documentation is clear and provides actionable guidance. The warning block effectively communicates the data loss risk.

guides/howtos/building-read-models-with-ecto.md (1)

1-365: LGTM!

The guide updates provide concrete, practical examples with realistic domain events and module names. The restructured content with batch processing, error handling, and multi-tenancy examples significantly improves the guide's utility.

guides/explanations/read-model-projections.md (1)

7-7: LGTM!

The updated references provide comprehensive navigation to all three relevant documentation resources.

Comment thread test/projections/ecto_projection_test.exs
@yordis
yordis merged commit 50ec412 into main Nov 20, 2025
7 of 9 checks passed
@yordis
yordis deleted the yordis/fix-concurrency branch November 20, 2025 04:27
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.

1 participant