Skip to content

fix: Fix race condition for EventNotification#12738

Merged
vbaranov merged 4 commits intomasterfrom
np-fix-db-events
Jul 9, 2025
Merged

fix: Fix race condition for EventNotification#12738
vbaranov merged 4 commits intomasterfrom
np-fix-db-events

Conversation

@nikitosing
Copy link
Copy Markdown
Member

@nikitosing nikitosing commented Jul 7, 2025

Closes #12724

Motivation

Race condition in case multiple API pods setup:
(Explorer.Chain.Events.Listener module)

defp fetch_and_delete_event_notification(id) do
  case Repo.get(EventNotification, id) do
    nil ->
      nil

    %{data: data} = notification ->
      try do
        Repo.delete(notification)
      rescue
        Ecto.StaleEntryError -> nil
      end

      data
  end
end

Changelog

  • fetch_and_delete_event_notification -> fetch_event_notification (Explorer.Chain.Events.Listener)
  • Added Indexer.Utils.NotificationsCleaner which is now responsible for clean up notifications
  • INDEXER_NOTIFICATIONS_CLEANUP_INTERVAL
  • INDEXER_NOTIFICATIONS_CLEANUP_ENABLED
  • INDEXER_NOTIFICATIONS_CLEANUP_MAX_AGE
  • https://github.com/blockscout/docs/pull/418

Checklist for your Pull Request (PR)

  • If I added new functionality, I added tests covering it.
  • If I fixed a bug, I added a regression test to prevent the bug from silently reappearing again.
  • I checked whether I should update the docs and did so by submitting a PR to docs repository.
  • If I added/changed/removed ENV var, I submitted a PR to docs repository to update the list of env vars and I updated the version to master in the Version column. If I removed variable, I added it to Deprecated ENV Variables page. After merging docs PR, changes will be reflected in these pages.
  • If I added new DB indices, I checked, that they are not redundant, with PGHero or other tools.
  • If I added/removed chain type, I modified the Github CI matrix and PR labels accordingly.

Summary by CodeRabbit

  • New Features

    • Added automatic cleanup of old event notifications from the database, configurable by interval and maximum age.
    • Introduced timestamp tracking for event notifications.
    • Added new configuration options for event notification cleanup, including environment variable support.
  • Bug Fixes

    • Improved event notification retrieval to prevent unintended deletion.
  • Chores

    • Added tests to verify event notification cleanup behavior.
    • Updated database schema and migrations to support timestamps on event notifications.
    • Enhanced test factories for event notifications.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jul 7, 2025

Walkthrough

A new GenServer module for event notification cleanup was introduced, which periodically deletes old event_notifications records based on a configurable age threshold. The notification fetch logic was updated to remove in-place deletion, and timestamp fields were added to the schema and database. Related configuration, migration, environment variables, and tests for the cleaner were also added.

Changes

Files/Paths Change Summary
apps/explorer/lib/explorer/chain/events/listener.ex Modified fetch logic: removed deletion from fetch_event_notification/1, renamed function, adjusted usage.
apps/explorer/lib/explorer/utility/event_notification.ex
apps/explorer/priv/repo/migrations/20250704124014_add_timestamp_to_event_notifications.exs
Added timestamps() to schema; new migration truncates table and adds timestamp columns.
apps/explorer/test/support/factory.ex Added factory for EventNotification with timestamp for testing.
apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex New GenServer module: periodic deletion of old notifications, configurable by age and interval.
apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs New tests for cleaner GenServer: verifies deletion logic and configuration.
apps/indexer/lib/indexer/supervisor.ex Registered the new cleaner GenServer as a supervised child.
config/runtime.exs Added configuration for cleaner interval, enabled flag, and max age via environment variables.
docker-compose/envs/common-blockscout.env Added commented environment variable placeholders for cleaner configuration.

Assessment against linked issues

Objective Addressed Explanation
Remove in-place deletion from fetch logic to avoid race conditions (#12724)
Add background job to periodically purge old event notifications (#12724)
Make event notification cleanup configurable (interval, enabled, max_age) (#12724)
Add timestamp fields to event_notifications for age-based cleanup (#12724)

Suggested reviewers

  • vbaranov
✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@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 (1)
apps/indexer/lib/indexer/utils/notifications_cleaner.ex (1)

1-5: Add missing @doc for the module.

The module documentation should follow the established format for better clarity.

defmodule Indexer.Utils.NotificationsCleaner do
  @moduledoc """
-  Module is responsible for cleaning up notifications from the database.
+  GenServer responsible for periodically cleaning up old event notifications from the database to prevent accumulation and potential race conditions.
+
+  This module performs two types of cleanup:
+  - Legacy cleanup: Removes notifications with nil inserted_at timestamps
+  - Age-based cleanup: Removes notifications older than the configured max_age
+
+  The cleanup process runs at configurable intervals and is controlled by the
+  enabled flag in the application configuration.
  """
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5b3531 and 98818de.

📒 Files selected for processing (8)
  • apps/explorer/lib/explorer/chain/events/listener.ex (2 hunks)
  • apps/explorer/lib/explorer/utility/event_notification.ex (1 hunks)
  • apps/explorer/priv/repo/migrations/20250704124014_add_timestamp_to_event_notifications.exs (1 hunks)
  • apps/explorer/test/support/factory.ex (2 hunks)
  • apps/indexer/lib/indexer/supervisor.ex (2 hunks)
  • apps/indexer/lib/indexer/utils/notifications_cleaner.ex (1 hunks)
  • apps/indexer/test/indexer/utils/notifications_cleaner_test.exs (1 hunks)
  • config/runtime.exs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ex`: This is a module written in Elixir. For changed functions here are ge...

**/*.ex: This is a module written in Elixir. For changed functions here are general guidelines for reviewing:

  1. Context Awareness:
  • Before raising a comment about missing specifications (@SPEC) or documentation (@doc), verify whether they exist in the corresponding code.
  • Check if a single @SPEC or @doc covers multiple clauses of a function (e.g., functions with different argument patterns). Do not raise a comment if one shared @SPEC or @doc exists.
  1. Behavior-Implementing Functions:
  • If a function is preceded by @impl, confirm that it implements a behavior. Do not raise comments for missing @SPEC or @doc if:
    • @SPEC is omitted (this is acceptable).
    • Documentation is omitted or provided in #-style comments.
  1. Predefined Functions:
  • For GenServer functions like child_spec/1 and start_link/1 do not raise comments for missing @SPEC or @doc.
  1. Documentation Format:
  • When reviewing @doc comments, ensure that it follows the following format:
@doc """
  This function does X, Y, and Z and this very first sentence of the comment must not be wrapped if the line exceeds 80 chars.

  The next lines in the documentation comment of any function must be wrapped
  after 80 chars (soft limit). It will allow to read documentation comments
  well in the code editors.

  ## Parameters
  - `param1`: Description of param1
  - `param2`: Description of param2

  ## Returns
  - what the function returns in one case
  - what the function returns if there is more than one possible outcome
"""
  1. Exceptions for Private Functions:
  • For private functions:
    • Skip comments about missing @SPEC if the function has no arguments.
    • Ensure no duplicate @SPEC is required for multiple clauses.

Example Scenarios to Reduce False Positives

  1. Single Specification and Documentation for Multiple Clauses (i.e., different implementations based on pattern matching of arguments):
@doc """
  Provides an example function.

  ## Parameters
  - `integer`: An integer input.

  ## Returns
  - A string result.
"""
@spec example_function(integer()) :: String.t()
def example_function(1), do: "One"
def example_function(2), do: "Two"
  • Do not raise a comment here, as both @SPEC and @doc cover all clauses.
  1. Behavior-Implementing Function:
# Handles an example call.
@impl GenServer
def handle_call(:example, _from, state) do
  {:reply, :ok, state}
end
  • Do not raise a comment for missing @SPEC.
  • Ensure the documentation is valid as a # comment.
  1. Function with No Arguments:
defp no_arg_function, do: :ok
  • Do not raise a comment for a missing @SPEC.

⚙️ Source: CodeRabbit Configuration File

List of files the instruction was applied to:

  • apps/indexer/lib/indexer/supervisor.ex
  • apps/explorer/lib/explorer/utility/event_notification.ex
  • apps/explorer/test/support/factory.ex
  • apps/explorer/lib/explorer/chain/events/listener.ex
  • apps/indexer/lib/indexer/utils/notifications_cleaner.ex
🧠 Learnings (3)
config/runtime.exs (4)
Learnt from: Qwerty5Uiop
PR: blockscout/blockscout#11685
File: apps/explorer/lib/explorer/chain/token/instance.ex:929-950
Timestamp: 2025-01-20T08:06:21.116Z
Learning: The token instance retry configuration in `config/runtime.exs` has robust defaults and validation through ConfigHelper:
- max_refetch_interval: "168h" (1 week), parsed through parse_time_env_var
- exp_timeout_base: 2, parsed through parse_integer_env_var
- exp_timeout_coeff: 100, parsed through parse_integer_env_var
These values are used in exponential backoff calculations and are guaranteed to be valid through the ConfigHelper parsing.
Learnt from: Qwerty5Uiop
PR: blockscout/blockscout#11685
File: apps/explorer/lib/explorer/chain/token/instance.ex:929-950
Timestamp: 2025-01-20T08:06:21.116Z
Learning: The token instance retry configuration in `config/runtime.exs` has robust defaults:
- max_refetch_interval: "168h" (1 week)
- exp_timeout_base: 2
- exp_timeout_coeff: 100
These values are parsed through ConfigHelper functions and have proven reliable in production.
Learnt from: Qwerty5Uiop
PR: blockscout/blockscout#11685
File: apps/explorer/lib/explorer/chain/token/instance.ex:929-950
Timestamp: 2025-01-20T08:06:21.116Z
Learning: The `max_refetch_interval` configuration value for `Indexer.Fetcher.TokenInstance.Retry` has a default value in config and cannot be nil.
Learnt from: Qwerty5Uiop
PR: blockscout/blockscout#12559
File: apps/explorer/test/explorer/migrator/reindex_blocks_with_missing_transactions_test.exs:14-20
Timestamp: 2025-06-16T14:39:16.305Z
Learning: In the Blockscout codebase, test files for migrators like ReindexBlocksWithMissingTransactions should not provide default values when calling Application.get_env/2 for configuration, because the configuration is guaranteed to be set in runtime config. It's preferred for tests to fail fast if configuration is missing rather than silently using defaults, as this indicates a real configuration problem.
apps/explorer/priv/repo/migrations/20250704124014_add_timestamp_to_event_notifications.exs (1)
Learnt from: akolotov
PR: blockscout/blockscout#11485
File: apps/explorer/lib/explorer/migrator/filling_migration.ex:266-267
Timestamp: 2024-12-25T21:09:01.545Z
Learning: In `apps/explorer/lib/explorer/migrator/filling_migration.ex`, it is the user's preference to avoid making changes to the implementation of `run_task/1` (e.g., adding error handling logic) to prevent affecting existing functionality in use.
apps/indexer/test/indexer/utils/notifications_cleaner_test.exs (1)
Learnt from: Qwerty5Uiop
PR: blockscout/blockscout#12559
File: apps/explorer/test/explorer/migrator/reindex_blocks_with_missing_transactions_test.exs:14-20
Timestamp: 2025-06-16T14:39:16.305Z
Learning: In the Blockscout codebase, test files for migrators like ReindexBlocksWithMissingTransactions should not provide default values when calling Application.get_env/2 for configuration, because the configuration is guaranteed to be set in runtime config. It's preferred for tests to fail fast if configuration is missing rather than silently using defaults, as this indicates a real configuration problem.
🧬 Code Graph Analysis (3)
apps/indexer/lib/indexer/supervisor.ex (1)
apps/explorer/lib/explorer/application.ex (1)
  • configure (360-366)
config/runtime.exs (2)
apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs (1)
  • parse_time_env_var (277-285)
config/config_helper.exs (1)
  • parse_time_env_var (102-116)
apps/indexer/test/indexer/utils/notifications_cleaner_test.exs (1)
apps/indexer/lib/indexer/utils/notifications_cleaner.ex (1)
  • start_link (15-17)
⏰ 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). (5)
  • GitHub Check: EthereumJSONRPC Tests (default)
  • GitHub Check: Blockscout Web Tests (default)
  • GitHub Check: Indexer Tests (default)
  • GitHub Check: Dialyzer static analysis (default)
  • GitHub Check: Explorer Tests (default)
🔇 Additional comments (16)
apps/explorer/lib/explorer/utility/event_notification.ex (1)

11-11: LGTM: Timestamps added to support cleanup functionality.

The addition of timestamps() is correctly implemented and aligns with the migration. This enables the NotificationsCleaner to identify old notifications for periodic cleanup.

apps/indexer/lib/indexer/supervisor.ex (2)

67-67: LGTM: Alias added for NotificationsCleaner.

The alias is correctly added to support the new cleanup functionality.


261-262: LGTM: Appropriate section comment added.

The comment clearly identifies the notifications cleaner section in the supervision tree.

apps/explorer/test/support/factory.ex (2)

65-65: LGTM: EventNotification alias added appropriately.

The alias is correctly added to support the new factory function.


1403-1408: LGTM: Well-implemented factory function.

The factory follows standard ExMachina patterns and correctly provides test data with timestamp, supporting testing of the cleanup functionality.

apps/explorer/priv/repo/migrations/20250704124014_add_timestamp_to_event_notifications.exs (1)

1-9: LGTM: Well-structured migration for adding timestamps.

The migration correctly adds nullable timestamp columns to support the new cleanup functionality. The null: true option is appropriate for existing records, and the implementation follows standard Ecto patterns.

config/runtime.exs (1)

1414-1417: LGTM! Configuration follows established patterns.

The configuration block properly uses ConfigHelper functions and sets reasonable defaults. The environment variable naming follows the established INDEXER_ prefix pattern.

apps/explorer/lib/explorer/chain/events/listener.ex (2)

43-43: LGTM! Function call updated to match renamed function.

The call correctly references the renamed fetch_event_notification/1 function.


71-79: LGTM! Function properly renamed and simplified.

The function correctly:

  • Removes the deletion logic to prevent race conditions
  • Maintains the same return behavior (returns data or nil)
  • Has a clearer name that reflects its actual behavior

This change aligns with the PR objective to decouple fetching from deletion, allowing the new NotificationsCleaner to handle cleanup asynchronously.

apps/indexer/lib/indexer/utils/notifications_cleaner.ex (4)

15-23: LGTM! Proper GenServer implementation.

The start_link/1 and init/1 functions follow GenServer best practices. The immediate scheduling of both cleanup messages on startup is appropriate for ensuring immediate cleanup of any existing legacy data.


25-34: LGTM! Well-structured message handling.

The message handlers properly:

  • Handle legacy cleanup as a one-time operation
  • Schedule periodic cleanup with Process.send_after
  • Return proper GenServer responses

36-53: LGTM! Cleanup functions are well-implemented.

Both cleanup functions:

  • Use proper Ecto query patterns with ago/2 and is_nil/1
  • Log the deletion counts for observability
  • Handle the database operations correctly

The separation of legacy cleanup (nil timestamps) from age-based cleanup is a good design choice.


54-65: LGTM! Clean configuration access pattern.

The private helper functions properly abstract configuration access and follow established patterns in the codebase.

apps/indexer/test/indexer/utils/notifications_cleaner_test.exs (3)

10-21: LGTM! Proper test configuration management.

The setup correctly:

  • Stores the original configuration before each test
  • Restores it in the on_exit callback to prevent test pollution
  • Follows the pattern of not providing defaults, which aligns with the codebase learning that tests should fail fast if configuration is missing

22-28: LGTM! Appropriate GenServer startup test.

The test properly verifies that the GenServer starts and the process is alive, then cleans up by terminating the process.


30-97: LGTM! Comprehensive cleanup behavior testing.

The test suite excellently covers:

  • Deletion of old notifications while preserving new ones
  • Multiple old notifications deletion
  • No deletions when notifications are within the age threshold
  • Proper use of factory functions with timestamps
  • Appropriate assertions for expected behavior

The use of Process.sleep(500) is necessary to allow the asynchronous GenServer cleanup to complete.

@nikitosing nikitosing requested a review from a team July 7, 2025 14:35
Comment thread apps/indexer/lib/indexer/utils/notifications_cleaner.ex Outdated
@nikitosing nikitosing requested a review from rimrakhimov July 8, 2025 16:19
Copy link
Copy Markdown

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

🧹 Nitpick comments (1)
apps/indexer/lib/indexer/utils/notifications_cleaner.ex (1)

2-4: Enhance module documentation for better clarity.

The module documentation is brief. Consider expanding it to describe the cleanup process, configuration options, and how it integrates with the supervision tree.

-  @moduledoc """
-  Module is responsible for cleaning up notifications from the database.
-  """
+  @moduledoc """
+  GenServer that periodically cleans up old event notifications from the database.
+  
+  The cleaner runs immediately upon initialization and then schedules periodic
+  cleanups based on the configured interval. It deletes notifications older than
+  the configured maximum age using the `inserted_at` timestamp.
+  
+  ## Configuration
+  
+  The module expects the following configuration in the application environment:
+  - `:max_age` - Maximum age of notifications in milliseconds
+  - `:interval` - Cleanup interval in milliseconds
+  """
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69b1192 and 5e3b68e.

📒 Files selected for processing (2)
  • apps/explorer/priv/repo/migrations/20250704124014_add_timestamp_to_event_notifications.exs (1 hunks)
  • apps/indexer/lib/indexer/utils/notifications_cleaner.ex (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ex`: This is a module written in Elixir. For changed functions here are ge...

**/*.ex: This is a module written in Elixir. For changed functions here are general guidelines for reviewing:

  1. Context Awareness:
  • Before raising a comment about missing specifications (@SPEC) or documentation (@doc), verify whether they exist in the corresponding code.
  • Check if a single @SPEC or @doc covers multiple clauses of a function (e.g., functions with different argument patterns). Do not raise a comment if one shared @SPEC or @doc exists.
  1. Behavior-Implementing Functions:
  • If a function is preceded by @impl, confirm that it implements a behavior. Do not raise comments for missing @SPEC or @doc if:
    • @SPEC is omitted (this is acceptable).
    • Documentation is omitted or provided in #-style comments.
  1. Predefined Functions:
  • For GenServer functions like child_spec/1 and start_link/1 do not raise comments for missing @SPEC or @doc.
  1. Documentation Format:
  • When reviewing @doc comments, ensure that it follows the following format:
@doc """
  This function does X, Y, and Z and this very first sentence of the comment must not be wrapped if the line exceeds 80 chars.

  The next lines in the documentation comment of any function must be wrapped
  after 80 chars (soft limit). It will allow to read documentation comments
  well in the code editors.

  ## Parameters
  - `param1`: Description of param1
  - `param2`: Description of param2

  ## Returns
  - what the function returns in one case
  - what the function returns if there is more than one possible outcome
"""
  1. Exceptions for Private Functions:
  • For private functions:
    • Skip comments about missing @SPEC if the function has no arguments.
    • Ensure no duplicate @SPEC is required for multiple clauses.

Example Scenarios to Reduce False Positives

  1. Single Specification and Documentation for Multiple Clauses (i.e., different implementations based on pattern matching of arguments):
@doc """
  Provides an example function.

  ## Parameters
  - `integer`: An integer input.

  ## Returns
  - A string result.
"""
@spec example_function(integer()) :: String.t()
def example_function(1), do: "One"
def example_function(2), do: "Two"
  • Do not raise a comment here, as both @SPEC and @doc cover all clauses.
  1. Behavior-Implementing Function:
# Handles an example call.
@impl GenServer
def handle_call(:example, _from, state) do
  {:reply, :ok, state}
end
  • Do not raise a comment for missing @SPEC.
  • Ensure the documentation is valid as a # comment.
  1. Function with No Arguments:
defp no_arg_function, do: :ok
  • Do not raise a comment for a missing @SPEC.

⚙️ Source: CodeRabbit Configuration File

List of files the instruction was applied to:

  • apps/indexer/lib/indexer/utils/notifications_cleaner.ex
🧠 Learnings (1)
apps/explorer/priv/repo/migrations/20250704124014_add_timestamp_to_event_notifications.exs (1)
Learnt from: akolotov
PR: blockscout/blockscout#11485
File: apps/explorer/lib/explorer/migrator/filling_migration.ex:266-267
Timestamp: 2024-12-25T21:09:01.545Z
Learning: In `apps/explorer/lib/explorer/migrator/filling_migration.ex`, it is the user's preference to avoid making changes to the implementation of `run_task/1` (e.g., adding error handling logic) to prevent affecting existing functionality in use.
⏰ 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: Analyze (javascript)
🔇 Additional comments (2)
apps/explorer/priv/repo/migrations/20250704124014_add_timestamp_to_event_notifications.exs (2)

5-5: Consider the implications of truncating existing data.

The truncation removes all existing event notifications. While this aligns with the race condition fix and the need for timestamped records, ensure that losing existing notifications is acceptable in your use case.

Consider documenting this data loss in the migration or PR description to make the impact clear to other developers.


7-9: LGTM! Clean migration structure.

The migration correctly adds timestamp fields using the timestamps() macro, which will create both inserted_at and updated_at columns with appropriate defaults and constraints.

Comment on lines +30 to +37
defp clean_up_notifications do
{count, _} =
EventNotification
|> where([en], en.inserted_at < ago(^max_age(), "millisecond"))
|> Repo.delete_all()

Logger.info("Deleted #{count} event notifications")
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling and configuration validation.

The cleanup function doesn't validate configuration values or handle database errors gracefully.

 defp clean_up_notifications do
+  max_age_val = max_age()
+  
+  if is_nil(max_age_val) or max_age_val <= 0 do
+    Logger.warning("Invalid max_age configuration: #{inspect(max_age_val)}")
+    return
+  end
+  
   {count, _} =
     EventNotification
-    |> where([en], en.inserted_at < ago(^max_age(), "millisecond"))
+    |> where([en], en.inserted_at < ago(^max_age_val, "millisecond"))
     |> Repo.delete_all()

   Logger.info("Deleted #{count} event notifications")
 end

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/indexer/lib/indexer/utils/notifications_cleaner.ex around lines 30 to
37, the clean_up_notifications function lacks validation for the max_age
configuration and does not handle potential database errors from
Repo.delete_all. Add checks to ensure max_age returns a valid value before using
it, and wrap the Repo.delete_all call in a try-rescue block or use with to catch
and log any errors gracefully without crashing the process.

Comment on lines +24 to +28
def handle_info(:clean_up_notifications, state) do
clean_up_notifications()
Process.send_after(self(), :clean_up_notifications, interval())
{:noreply, state}
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for the cleanup process.

The handle_info/2 callback doesn't handle potential database errors during cleanup, which could crash the GenServer.

 def handle_info(:clean_up_notifications, state) do
-  clean_up_notifications()
-  Process.send_after(self(), :clean_up_notifications, interval())
+  try do
+    clean_up_notifications()
+  rescue
+    error ->
+      Logger.error("Failed to clean up notifications: #{inspect(error)}")
+  end
+  
+  Process.send_after(self(), :clean_up_notifications, interval())
   {:noreply, state}
 end
📝 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
def handle_info(:clean_up_notifications, state) do
clean_up_notifications()
Process.send_after(self(), :clean_up_notifications, interval())
{:noreply, state}
end
def handle_info(:clean_up_notifications, state) do
try do
clean_up_notifications()
rescue
error ->
Logger.error("Failed to clean up notifications: #{inspect(error)}")
end
Process.send_after(self(), :clean_up_notifications, interval())
{:noreply, state}
end
🤖 Prompt for AI Agents
In apps/indexer/lib/indexer/utils/notifications_cleaner.ex around lines 24 to
28, the handle_info/2 function calls clean_up_notifications without handling
potential errors, risking a GenServer crash. Wrap the call to
clean_up_notifications in a try-rescue block or use pattern matching to catch
and handle any errors or exceptions from the cleanup process, ensuring the
GenServer remains stable and logs or manages errors appropriately.

Comment on lines +19 to +22
def init(args) do
Process.send(self(), :clean_up_notifications, [])
{:ok, args}
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider potential timing issues with immediate cleanup.

The immediate cleanup in init/1 might run before the database migration is complete or before the system is fully initialized, potentially causing errors.

Consider adding a small delay or checking system readiness:

 def init(args) do
-  Process.send(self(), :clean_up_notifications, [])
+  Process.send_after(self(), :clean_up_notifications, 1000)
   {:ok, args}
 end
📝 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
def init(args) do
Process.send(self(), :clean_up_notifications, [])
{:ok, args}
end
def init(args) do
Process.send_after(self(), :clean_up_notifications, 1000)
{:ok, args}
end
🤖 Prompt for AI Agents
In apps/indexer/lib/indexer/utils/notifications_cleaner.ex around lines 19 to
22, the immediate call to clean up notifications in the init/1 function may
execute before the system or database is fully ready, causing errors. Modify the
code to introduce a small delay before sending the :clean_up_notifications
message, such as using Process.send_after/3 with a short timeout, or implement a
readiness check to ensure the system is fully initialized before triggering
cleanup.

Comment on lines +47 to +49
defp config do
Application.get_env(:indexer, __MODULE__)
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add configuration validation.

The configuration helper doesn't validate that the required configuration exists, which could cause runtime errors.

 defp config do
-  Application.get_env(:indexer, __MODULE__)
+  case Application.get_env(:indexer, __MODULE__) do
+    nil ->
+      Logger.error("Missing configuration for #{__MODULE__}")
+      %{max_age: 86_400_000, interval: 3_600_000}  # Default values
+    config -> config
+  end
 end

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/indexer/lib/indexer/utils/notifications_cleaner.ex around lines 47 to
49, the config function retrieves configuration without validating its presence.
Modify the function to check if the required configuration exists and raise a
clear error or handle the missing config case appropriately to prevent runtime
errors.

Comment thread apps/indexer/lib/indexer/utils/notifications_cleaner.ex Outdated
Comment thread apps/indexer/lib/indexer/utils/notifications_cleaner.ex
Comment thread apps/indexer/lib/indexer/utils/notifications_cleaner.ex Outdated
Comment thread apps/indexer/lib/indexer/utils/notifications_cleaner.ex Outdated
Comment thread apps/indexer/lib/indexer/utils/notifications_cleaner.ex Outdated
Comment thread apps/indexer/test/indexer/utils/notifications_cleaner_test.exs Outdated
Comment thread apps/indexer/test/indexer/utils/notifications_cleaner_test.exs Outdated
Comment thread apps/indexer/test/indexer/utils/notifications_cleaner_test.exs Outdated
Comment thread apps/indexer/test/indexer/utils/notifications_cleaner_test.exs Outdated
Comment thread apps/indexer/test/indexer/utils/notifications_cleaner_test.exs Outdated
Copy link
Copy Markdown

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

🧹 Nitpick comments (2)
apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex (1)

24-28: Add @impl annotation for GenServer callback.

The handle_info/2 function should include the @impl annotation for clarity and maintainability.

+@impl GenServer
def handle_info(:clean_up_event_notifications, state) do
apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs (1)

30-97: Add test coverage for error scenarios.

The tests only cover happy path scenarios. Consider adding tests for error conditions such as database failures or invalid configurations.

+test "handles database errors gracefully" do
+  # Mock Repo.delete_all to raise an error
+  # Test that the GenServer continues running after an error
+end
+
+test "validates configuration on startup" do
+  # Test behavior with missing or invalid configuration
+end
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5e3b68e and 83f7255.

📒 Files selected for processing (5)
  • apps/indexer/lib/indexer/supervisor.ex (2 hunks)
  • apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex (1 hunks)
  • apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs (1 hunks)
  • config/runtime.exs (1 hunks)
  • docker-compose/envs/common-blockscout.env (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/indexer/lib/indexer/supervisor.ex
  • config/runtime.exs
  • docker-compose/envs/common-blockscout.env
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ex`: This is a module written in Elixir. For changed functions here are ge...

**/*.ex: This is a module written in Elixir. For changed functions here are general guidelines for reviewing:

  1. Context Awareness:
  • Before raising a comment about missing specifications (@SPEC) or documentation (@doc), verify whether they exist in the corresponding code.
  • Check if a single @SPEC or @doc covers multiple clauses of a function (e.g., functions with different argument patterns). Do not raise a comment if one shared @SPEC or @doc exists.
  1. Behavior-Implementing Functions:
  • If a function is preceded by @impl, confirm that it implements a behavior. Do not raise comments for missing @SPEC or @doc if:
    • @SPEC is omitted (this is acceptable).
    • Documentation is omitted or provided in #-style comments.
  1. Predefined Functions:
  • For GenServer functions like child_spec/1 and start_link/1 do not raise comments for missing @SPEC or @doc.
  1. Documentation Format:
  • When reviewing @doc comments, ensure that it follows the following format:
@doc """
  This function does X, Y, and Z and this very first sentence of the comment must not be wrapped if the line exceeds 80 chars.

  The next lines in the documentation comment of any function must be wrapped
  after 80 chars (soft limit). It will allow to read documentation comments
  well in the code editors.

  ## Parameters
  - `param1`: Description of param1
  - `param2`: Description of param2

  ## Returns
  - what the function returns in one case
  - what the function returns if there is more than one possible outcome
"""
  1. Exceptions for Private Functions:
  • For private functions:
    • Skip comments about missing @SPEC if the function has no arguments.
    • Ensure no duplicate @SPEC is required for multiple clauses.

Example Scenarios to Reduce False Positives

  1. Single Specification and Documentation for Multiple Clauses (i.e., different implementations based on pattern matching of arguments):
@doc """
  Provides an example function.

  ## Parameters
  - `integer`: An integer input.

  ## Returns
  - A string result.
"""
@spec example_function(integer()) :: String.t()
def example_function(1), do: "One"
def example_function(2), do: "Two"
  • Do not raise a comment here, as both @SPEC and @doc cover all clauses.
  1. Behavior-Implementing Function:
# Handles an example call.
@impl GenServer
def handle_call(:example, _from, state) do
  {:reply, :ok, state}
end
  • Do not raise a comment for missing @SPEC.
  • Ensure the documentation is valid as a # comment.
  1. Function with No Arguments:
defp no_arg_function, do: :ok
  • Do not raise a comment for a missing @SPEC.

⚙️ Source: CodeRabbit Configuration File

List of files the instruction was applied to:

  • apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex
🧠 Learnings (1)
apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs (1)
Learnt from: Qwerty5Uiop
PR: blockscout/blockscout#12559
File: apps/explorer/test/explorer/migrator/reindex_blocks_with_missing_transactions_test.exs:14-20
Timestamp: 2025-06-16T14:39:16.305Z
Learning: In the Blockscout codebase, test files for migrators like ReindexBlocksWithMissingTransactions should not provide default values when calling Application.get_env/2 for configuration, because the configuration is guaranteed to be set in runtime config. It's preferred for tests to fail fast if configuration is missing rather than silently using defaults, as this indicates a real configuration problem.
⏰ 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). (5)
  • GitHub Check: EthereumJSONRPC Tests (default)
  • GitHub Check: Explorer Tests (default)
  • GitHub Check: Blockscout Web Tests (default)
  • GitHub Check: Indexer Tests (default)
  • GitHub Check: Dialyzer static analysis (default)
🔇 Additional comments (5)
apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex (3)

1-14: LGTM - Well-structured module setup.

The module declaration, imports, and dependencies are properly organized. The moduledoc follows the expected format.


15-17: LGTM - Standard GenServer start_link function.

The implementation follows GenServer conventions correctly.


19-22: LGTM - Proper init callback implementation.

The immediate scheduling of cleanup on startup is appropriate for this use case.

apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs (2)

1-20: LGTM - Proper test setup with configuration isolation.

The setup properly preserves and restores configuration between tests, which is essential for isolated test runs.


22-28: LGTM - Good GenServer startup test.

The test properly verifies GenServer startup and includes cleanup of the process.

Comment on lines +39 to +49
defp max_age do
config()[:max_age]
end

defp interval do
config()[:interval]
end

defp config do
Application.get_env(:indexer, __MODULE__)
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add validation for configuration values.

The configuration helper functions should validate that required values are present and valid.

defp max_age do
-  config()[:max_age]
+  case config()[:max_age] do
+    nil -> raise "EventNotificationsCleaner max_age configuration is missing"
+    value when is_integer(value) and value > 0 -> value
+    value -> raise "EventNotificationsCleaner max_age must be a positive integer, got: #{inspect(value)}"
+  end
end

defp interval do
-  config()[:interval]
+  case config()[:interval] do
+    nil -> raise "EventNotificationsCleaner interval configuration is missing"
+    value when is_integer(value) and value > 0 -> value
+    value -> raise "EventNotificationsCleaner interval must be a positive integer, got: #{inspect(value)}"
+  end
end

defp config do
-  Application.get_env(:indexer, __MODULE__)
+  case Application.get_env(:indexer, __MODULE__) do
+    nil -> raise "EventNotificationsCleaner configuration is missing"
+    config -> config
+  end
end
📝 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
defp max_age do
config()[:max_age]
end
defp interval do
config()[:interval]
end
defp config do
Application.get_env(:indexer, __MODULE__)
end
defp max_age do
case config()[:max_age] do
nil ->
raise "EventNotificationsCleaner max_age configuration is missing"
value when is_integer(value) and value > 0 ->
value
value ->
raise "EventNotificationsCleaner max_age must be a positive integer, got: #{inspect(value)}"
end
end
defp interval do
case config()[:interval] do
nil ->
raise "EventNotificationsCleaner interval configuration is missing"
value when is_integer(value) and value > 0 ->
value
value ->
raise "EventNotificationsCleaner interval must be a positive integer, got: #{inspect(value)}"
end
end
defp config do
case Application.get_env(:indexer, __MODULE__) do
nil ->
raise "EventNotificationsCleaner configuration is missing"
config ->
config
end
end
🤖 Prompt for AI Agents
In apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex around lines 39
to 49, the config helper functions do not validate the presence or validity of
required configuration values. Add validation logic in the config function or in
max_age and interval functions to check that the retrieved values are not nil
and meet expected criteria, raising an appropriate error or fallback if
validation fails.

Comment on lines +30 to +37
defp clean_up_event_notifications do
{count, _} =
EventNotification
|> where([en], en.inserted_at < ago(^max_age(), "millisecond"))
|> Repo.delete_all()

Logger.info("Deleted #{count} event notifications")
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for database operations.

The cleanup function should handle potential database errors gracefully to prevent GenServer crashes.

defp clean_up_event_notifications do
-  {count, _} =
-    EventNotification
-    |> where([en], en.inserted_at < ago(^max_age(), "millisecond"))
-    |> Repo.delete_all()
+  try do
+    {count, _} =
+      EventNotification
+      |> where([en], en.inserted_at < ago(^max_age(), "millisecond"))
+      |> Repo.delete_all()
+
+    Logger.info("Deleted #{count} event notifications")
+  rescue
+    error ->
+      Logger.error("Failed to clean up event notifications: #{inspect(error)}")
+  end
-  Logger.info("Deleted #{count} event notifications")
end
🤖 Prompt for AI Agents
In apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex around lines 30
to 37, the clean_up_event_notifications function lacks error handling for the
database delete operation, which can cause GenServer crashes if an error occurs.
Modify the function to wrap the Repo.delete_all call in a try-rescue block or
use pattern matching on the result to handle errors gracefully, logging any
failures without crashing the process.

Comment on lines +55 to +78
test "deletes multiple old notifications" do
old_time = DateTime.utc_now() |> DateTime.add(-2000, :millisecond)

# Insert multiple old notifications
insert_list(3, :event_notification, inserted_at: old_time)

# Insert one new notification
insert(:event_notification, data: "new_data")

# Verify all notifications exist
assert Repo.aggregate(EventNotification, :count) == 4

# Set configuration
config = Application.get_env(:indexer, EventNotificationsCleaner)
Application.put_env(:indexer, EventNotificationsCleaner, Keyword.put(config, :max_age, 1000))

assert {:ok, _pid} = EventNotificationsCleaner.start_link([])
Process.sleep(500)

# Verify only the new notification remains
assert Repo.aggregate(EventNotification, :count) == 1
remaining = Repo.one(from(n in EventNotification, select: n.data))
assert remaining == "new_data"
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Same timing issue applies here.

This test has the same Process.sleep(500) timing dependency that should be addressed.

Apply the same synchronization improvement as suggested for the previous test.

🤖 Prompt for AI Agents
In apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs around
lines 55 to 78, the test relies on Process.sleep(500) to wait for the
EventNotificationsCleaner process to complete, which is a timing-dependent and
unreliable approach. Replace the Process.sleep call with a synchronization
mechanism such as monitoring the cleaner process or using a message/assertion
that confirms the cleanup has finished before proceeding with assertions. This
ensures the test only continues once the cleanup is complete, making it more
stable and deterministic.

Comment on lines +80 to +96
test "does not delete notifications when none are old enough" do
# Insert only new notifications
insert_list(3, :event_notification)

# Verify notifications exist
assert Repo.aggregate(EventNotification, :count) == 3

# Set configuration
config = Application.get_env(:indexer, EventNotificationsCleaner)
Application.put_env(:indexer, EventNotificationsCleaner, Keyword.put(config, :max_age, 1000))

assert {:ok, _pid} = EventNotificationsCleaner.start_link([])
Process.sleep(500)

# Verify all notifications still exist
assert Repo.aggregate(EventNotification, :count) == 3
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Same timing issue applies here.

This test also uses Process.sleep(500) which should be replaced with more reliable synchronization.

Apply the same synchronization improvement as suggested for the previous tests.

🤖 Prompt for AI Agents
In apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs around
lines 80 to 96, replace the unreliable Process.sleep(500) call with a more
reliable synchronization method, such as waiting for a specific message or event
that confirms the EventNotificationsCleaner process has completed its work. This
ensures the test only proceeds after the cleanup attempt finishes, avoiding
flaky timing issues.

Comment on lines +31 to +53
test "deletes notifications older than max_age" do
# Create notifications with different timestamps
old_time = DateTime.utc_now() |> DateTime.add(-2000, :millisecond)
new_time = DateTime.utc_now() |> DateTime.add(-500, :millisecond)

insert(:event_notification, data: "old_data", inserted_at: old_time)
insert(:event_notification, data: "new_data", inserted_at: new_time)

# Verify both notifications exist
assert Repo.aggregate(EventNotification, :count) == 2

# Set configuration for max_age of 1000ms
config = Application.get_env(:indexer, EventNotificationsCleaner)
Application.put_env(:indexer, EventNotificationsCleaner, Keyword.put(config, :max_age, 1000))

assert {:ok, _pid} = EventNotificationsCleaner.start_link([])
Process.sleep(500)

# Verify only the old notification was deleted
assert Repo.aggregate(EventNotification, :count) == 1
remaining = Repo.one(from(n in EventNotification, select: n.data))
assert remaining == "new_data"
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace Process.sleep with more reliable synchronization.

Using Process.sleep(500) makes tests fragile and can lead to flaky test results. Consider using a more deterministic approach.

assert {:ok, _pid} = EventNotificationsCleaner.start_link([])
-Process.sleep(500)
+
+# Wait for cleanup to complete by polling the database
+:timer.sleep(100)
+Process.send(pid, :clean_up_event_notifications, [])
+:timer.sleep(100)

Alternatively, consider exposing the cleanup function for direct testing:

+# In the cleaner module, add a public function for testing
+def clean_up_event_notifications_sync do
+  clean_up_event_notifications()
+end

+# In the test
+EventNotificationsCleaner.clean_up_event_notifications_sync()

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs around
lines 31 to 53, replace the use of Process.sleep(500) with a more reliable
synchronization method to avoid flaky tests. Instead of sleeping, either wait
for a message or use a callback that confirms the cleanup process has completed.
Alternatively, refactor the EventNotificationsCleaner module to expose the
cleanup function directly and call it in the test to deterministically verify
the deletion of old notifications.

@nikitosing nikitosing requested a review from vbaranov July 9, 2025 14:10
@vbaranov vbaranov merged commit 56677c3 into master Jul 9, 2025
18 of 19 checks passed
@vbaranov vbaranov deleted the np-fix-db-events branch July 9, 2025 14:20
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.

Add event_notifications sanitizer

3 participants