Skip to content

fix: make import not transactional #892

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: release/v2.2
Choose a base branch
from

Conversation

gfyrag
Copy link
Contributor

@gfyrag gfyrag commented Apr 29, 2025

No description provided.

@gfyrag gfyrag requested a review from a team as a code owner April 29, 2025 09:08
Copy link

coderabbitai bot commented Apr 29, 2025

Walkthrough

This update introduces a new LockLedger method to the ledger controller interface and its implementations, extending the locking mechanism to return a locked controller, a database interface, a release function, and an error. The method is implemented across various controller wrappers, adapters, and mock files. The ledger store's locking logic is refactored to use PostgreSQL advisory locks with explicit acquisition and release semantics, replacing the previous table lock approach. The system's state tracker centralizes ledger locking and transaction handling using a new withLock helper function. End-to-end tests are enhanced to verify advisory lock behavior and import error handling.

Changes

Files/Paths Change Summary
internal/controller/ledger/controller.go, controller_with_cache.go, controller_with_events.go, controller_with_too_many_client_handling.go, controller_with_traces.go, controller_default.go Added LockLedger method to the Controller interface and all wrappers, returning a locked controller, DB interface, release function, and error. Tracing and metric collection added in the traces wrapper.
internal/controller/ledger/store.go, internal/storage/ledger/store.go, internal/storage/ledger/legacy/adapters.go Refactored LockLedger to return four values (locked store, DB handle, release function, error), implementing advisory locks with explicit acquire/release. Adapter updated to wrap new return values.
internal/controller/ledger/controller_generated_test.go, store_generated_test.go Updated mocks to match new LockLedger signature, returning four values and updating expectation recorders accordingly.
internal/api/bulking/mocks_ledger_controller_test.go, internal/api/common/mocks_ledger_controller_test.go, internal/api/v1/mocks_ledger_controller_test.go, internal/api/v2/mocks_ledger_controller_test.go Added mock methods for LockLedger in various API mock files, supporting the new method signature for testing.
internal/controller/system/state_tracker.go Refactored locking and transaction logic: introduced withLock helper to centralize ledger lock acquisition, resource management, and error handling for state and import operations.
test/e2e/api_ledgers_import_test.go Added tests for import error handling and advisory lock behavior, verifying locks during concurrent operations and import resumption after failure.
test/e2e/app_lifecycle_test.go Updated comments and SQL queries to check advisory locks accurately during ledger initialization, adjusting expected lock counts and explanations.
internal/controller/system/controller.go Added tracer option to ledger controller instantiation for enhanced tracing capabilities.
internal/api/common/middleware_resolver.go Added OpenTelemetry attribute to tracing span with ledger name for improved observability.
internal/api/v2/controllers_logs_import.go Improved error wrapping and control flow in import goroutine with better context propagation and invariant enforcement.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller
    participant Store
    participant DB

    Client->>Controller: LockLedger(ctx)
    Controller->>Store: LockLedger(ctx)
    Store->>DB: Acquire advisory lock
    Store-->>Controller: (locked Store, DB handle, release func, error)
    Controller-->>Client: (locked Controller, DB handle, release func, error)
    Note over Client,Controller: Client uses locked controller and DB, then calls release func
Loading

Suggested reviewers

  • paul-nicolas
  • Dav-14

Poem

In the warren of code, a new lock appears,
With advisory keys for the ledgers and peers.
Controllers now guard with a handshake so neat,
Returning their handles, release at your feet.
Tests hop along, checking locks with delight—
Rabbits approve: “This locking feels right!”
🐇🔒

✨ 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.
    • Generate unit testing code for this file.
    • 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 generate unit testing code for this file.
    • @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 generate unit testing code.
    • @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.

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.

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.

@gfyrag gfyrag force-pushed the hotfix/v2.2/import-not-transactional branch from fb4367d to 3409f47 Compare April 29, 2025 09:10
@gfyrag
Copy link
Contributor Author

gfyrag commented Apr 29, 2025

@coderabbitai configuration

Copy link

coderabbitai bot commented Apr 29, 2025

CodeRabbit configuration
language: en-US
tone_instructions: ''
early_access: true
enable_free_tier: true
reviews:
  profile: chill
  request_changes_workflow: false
  high_level_summary: false
  high_level_summary_placeholder: '@coderabbitai summary'
  high_level_summary_in_walkthrough: false
  auto_title_placeholder: '@coderabbitai'
  auto_title_instructions: ''
  review_status: true
  commit_status: true
  fail_commit_status: false
  collapse_walkthrough: false
  changed_files_summary: true
  sequence_diagrams: true
  assess_linked_issues: true
  related_issues: true
  related_prs: true
  suggested_labels: true
  auto_apply_labels: false
  suggested_reviewers: true
  auto_assign_reviewers: false
  poem: true
  labeling_instructions: []
  path_filters:
    - '!dist/**'
    - '!**/*.app'
    - '!**/*.bin'
    - '!**/*.bz2'
    - '!**/*.class'
    - '!**/*.db'
    - '!**/*.csv'
    - '!**/*.tsv'
    - '!**/*.dat'
    - '!**/*.dll'
    - '!**/*.dylib'
    - '!**/*.egg'
    - '!**/*.glif'
    - '!**/*.gz'
    - '!**/*.xz'
    - '!**/*.zip'
    - '!**/*.7z'
    - '!**/*.rar'
    - '!**/*.zst'
    - '!**/*.ico'
    - '!**/*.jar'
    - '!**/*.tar'
    - '!**/*.war'
    - '!**/*.lo'
    - '!**/*.log'
    - '!**/*.mp3'
    - '!**/*.wav'
    - '!**/*.wma'
    - '!**/*.mp4'
    - '!**/*.avi'
    - '!**/*.mkv'
    - '!**/*.wmv'
    - '!**/*.m4a'
    - '!**/*.m4v'
    - '!**/*.3gp'
    - '!**/*.3g2'
    - '!**/*.rm'
    - '!**/*.mov'
    - '!**/*.flv'
    - '!**/*.iso'
    - '!**/*.swf'
    - '!**/*.flac'
    - '!**/*.nar'
    - '!**/*.o'
    - '!**/*.ogg'
    - '!**/*.otf'
    - '!**/*.p'
    - '!**/*.pdf'
    - '!**/*.doc'
    - '!**/*.docx'
    - '!**/*.xls'
    - '!**/*.xlsx'
    - '!**/*.map'
    - '!**/*.out'
    - '!**/*.ppt'
    - '!**/*.pptx'
    - '!**/*.pkl'
    - '!**/*.pickle'
    - '!**/*.pyc'
    - '!**/*.pyd'
    - '!**/*.pyo'
    - '!**/*.pub'
    - '!**/*.pem'
    - '!**/*.rkt'
    - '!**/*.so'
    - '!**/*.ss'
    - '!**/*.eot'
    - '!**/*.exe'
    - '!**/*.pb.go'
    - '!**/*.pb.gw.go'
    - '!**/*.lock'
    - '!**/*.ttf'
    - '!**/*.yaml'
    - '!**/*.yml'
    - '!**/*.xml'
    - '!**/*.cfg'
    - '!**/*.toml'
    - '!**/*.ini'
    - '!**/*.mod'
    - '!**/*.sum'
    - '!**/*.work'
    - '!**/*.json'
    - '!**/*.mmd'
    - '!**/*.svg'
    - '!**/*.jpeg'
    - '!**/*.jpg'
    - '!**/*.png'
    - '!**/*.gif'
    - '!**/*.bmp'
    - '!**/*.tiff'
    - '!**/*.webm'
    - '!**/*.woff'
    - '!**/*.woff2'
    - '!**/*.dot'
    - '!**/*.md5sum'
    - '!**/*.wasm'
    - '!**/*.snap'
    - '!**/*.parquet'
    - '!**/gen/**'
    - '!**/_gen/**'
    - '!**/generated/**'
    - '!**/@generated/**'
    - '!**/vendor/**'
    - '!**/*.min.js'
    - '!**/*.min.js.map'
    - '!**/*.min.js.css'
    - '!**/*.tfstate'
    - '!**/*.tfstate.backup'
  path_instructions: []
  abort_on_close: true
  disable_cache: false
  auto_review:
    enabled: true
    auto_incremental_review: true
    ignore_title_keywords: []
    labels: []
    drafts: false
    base_branches:
      - main
  finishing_touches:
    docstrings:
      enabled: true
  tools:
    ast-grep:
      rule_dirs: []
      util_dirs: []
      essential_rules: true
      packages: []
    shellcheck:
      enabled: true
    ruff:
      enabled: true
    markdownlint:
      enabled: true
    github-checks:
      enabled: true
      timeout_ms: 90000
    languagetool:
      enabled: true
      enabled_rules: []
      disabled_rules: []
      enabled_categories: []
      disabled_categories: []
      enabled_only: false
      level: default
    biome:
      enabled: true
    hadolint:
      enabled: true
    swiftlint:
      enabled: true
    phpstan:
      enabled: true
      level: default
    golangci-lint:
      enabled: true
    yamllint:
      enabled: true
    gitleaks:
      enabled: true
    checkov:
      enabled: true
    detekt:
      enabled: true
    eslint:
      enabled: true
    rubocop:
      enabled: true
    buf:
      enabled: true
    regal:
      enabled: true
    actionlint:
      enabled: true
    pmd:
      enabled: true
    cppcheck:
      enabled: true
    semgrep:
      enabled: true
    circleci:
      enabled: true
    sqlfluff:
      enabled: true
    prismaLint:
      enabled: true
    oxc:
      enabled: true
    shopifyThemeCheck:
      enabled: true
chat:
  auto_reply: true
  create_issues: true
  integrations:
    jira:
      usage: auto
    linear:
      usage: auto
knowledge_base:
  opt_out: false
  web_search:
    enabled: true
  learnings:
    scope: auto
  issues:
    scope: auto
  jira:
    usage: auto
    project_keys: []
  linear:
    usage: auto
    team_keys: []
  pull_requests:
    scope: auto
code_generation:
  docstrings:
    language: en-US
    path_instructions: []

@gfyrag gfyrag force-pushed the hotfix/v2.2/import-not-transactional branch from 93dadd5 to d99db44 Compare April 29, 2025 11:19
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: 1

🧹 Nitpick comments (3)
internal/controller/ledger/controller.go (1)

21-21: Expanded controller interface for non-transactional import

The LockLedger method addition to the Controller interface mirrors the changes in the Store interface, providing a consistent approach to locking throughout the codebase. The method now returns the locked controller, database interface, cleanup function, and error.

However, this method is missing documentation comments describing its behavior and usage.

Consider adding a documentation comment for the LockLedger method describing its purpose, return values, and usage pattern, similar to other methods in this interface:

+// LockLedger acquires an advisory lock on the ledger and returns:
+// - a controller with the lock held
+// - the database interface
+// - a release function that must be called to release the lock
+// - an error if the lock cannot be acquired
 LockLedger(ctx context.Context) (Controller, bun.IDB, func() error, error)
internal/storage/ledger/store.go (1)

149-184: Comprehensive refactoring of LockLedger from table-based to advisory locks

The implementation now uses PostgreSQL advisory locks instead of table locks, providing better granularity and control. The method distinguishes between regular database connections (using session-level locks) and transactions (using transaction-scoped locks), with appropriate clean-up mechanisms for each case.

This approach provides several benefits:

  1. Better concurrency - advisory locks are more granular than table locks
  2. Explicit lifecycle management with the returned release function
  3. Proper handling of both connection and transaction contexts

The hashing of the ledger ID ensures a unique lock key per ledger, allowing concurrent operations on different ledgers.

internal/controller/system/state_tracker.go (1)

133-147: Import process is now non-transactional with proper locking

The import process has been refactored to use withLock without wrapping the entire operation in a transaction, making it non-transactional as intended in the PR title. The implementation correctly re-checks the ledger state after acquiring the lock to ensure consistency.

This non-transactional approach for imports improves performance and reduces lock contention for large imports, which is especially important if they contain many transactions.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c0c32f6 and d99db44.

⛔ Files ignored due to path filters (1)
  • .coderabbit.yaml is excluded by !**/*.yaml
📒 Files selected for processing (18)
  • internal/api/bulking/mocks_ledger_controller_test.go (1 hunks)
  • internal/api/common/mocks_ledger_controller_test.go (1 hunks)
  • internal/api/v1/mocks_ledger_controller_test.go (1 hunks)
  • internal/api/v2/mocks_ledger_controller_test.go (1 hunks)
  • internal/controller/ledger/controller.go (1 hunks)
  • internal/controller/ledger/controller_default.go (2 hunks)
  • internal/controller/ledger/controller_generated_test.go (1 hunks)
  • internal/controller/ledger/controller_with_cache.go (1 hunks)
  • internal/controller/ledger/controller_with_events.go (1 hunks)
  • internal/controller/ledger/controller_with_too_many_client_handling.go (1 hunks)
  • internal/controller/ledger/controller_with_traces.go (3 hunks)
  • internal/controller/ledger/store.go (1 hunks)
  • internal/controller/ledger/store_generated_test.go (1 hunks)
  • internal/controller/system/state_tracker.go (4 hunks)
  • internal/storage/ledger/legacy/adapters.go (1 hunks)
  • internal/storage/ledger/store.go (1 hunks)
  • test/e2e/api_ledgers_import_test.go (3 hunks)
  • test/e2e/app_lifecycle_test.go (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
internal/controller/ledger/controller_with_events.go (2)
internal/controller/ledger/controller.go (1)
  • Controller (17-78)
internal/controller/system/controller.go (1)
  • Controller (22-33)
internal/api/bulking/mocks_ledger_controller_test.go (4)
internal/api/common/mocks_ledger_controller_test.go (2)
  • LedgerController (22-25)
  • LedgerControllerMockRecorder (28-30)
internal/api/v1/mocks_ledger_controller_test.go (2)
  • LedgerController (22-25)
  • LedgerControllerMockRecorder (28-30)
internal/api/v2/mocks_ledger_controller_test.go (2)
  • LedgerController (22-25)
  • LedgerControllerMockRecorder (28-30)
internal/controller/ledger/controller.go (1)
  • Controller (17-78)
internal/api/v1/mocks_ledger_controller_test.go (1)
internal/controller/ledger/controller.go (1)
  • Controller (17-78)
internal/storage/ledger/legacy/adapters.go (3)
internal/storage/ledger/store.go (1)
  • Store (23-45)
internal/controller/ledger/store.go (1)
  • Store (30-65)
internal/storage/ledger/legacy/store.go (1)
  • Store (9-14)
internal/controller/ledger/controller_generated_test.go (1)
internal/controller/ledger/controller.go (1)
  • Controller (17-78)
🔇 Additional comments (24)
internal/controller/ledger/store.go (1)

52-52: Implementation change aligned with making import non-transactional

The LockLedger method signature has been updated to return four values (Store, bun.IDB, func() error, error) instead of just an error. This change enables more fine-grained control over ledger locking by providing:

  1. A locked store
  2. A database interface (for direct DB operations)
  3. A cleanup function to release the lock
  4. An error value

This is consistent with the PR title "make import not transactional" as it separates lock acquisition from transaction boundaries, allowing for more flexible transaction management.

internal/controller/ledger/store_generated_test.go (1)

220-229: Mock updated to match new interface signature

The mock implementation of LockLedger has been correctly updated to match the new signature, properly extracting and returning all four values from the mock call results.

internal/controller/ledger/controller_with_cache.go (1)

52-62: Proper implementation of new LockLedger method in cache wrapper

The implementation of LockLedger in ControllerWithCache correctly:

  1. Delegates to the underlying controller's LockLedger method
  2. Returns early with errors if the underlying call fails
  3. Wraps the returned controller in a new ControllerWithCache with the same registry and ledger
  4. Passes through the database interface and release function

This maintains the cache functionality while supporting the non-transactional lock pattern.

internal/api/v1/mocks_ledger_controller_test.go (1)

328-343: Implementation looks good!

The mock implementation of the new LockLedger method correctly follows the interface pattern. This method will now return a Controller, a database interface, a release function, and an error, supporting the non-transactional locking mechanism for imports.

internal/controller/ledger/controller_with_too_many_client_handling.go (1)

135-145: Implementation follows existing patterns!

The LockLedger implementation correctly follows the same pattern as the existing BeginTX method, wrapping the returned controller while preserving the delay calculator and tracer. Error handling is properly implemented by returning early if the underlying controller's LockLedger call fails.

internal/api/common/mocks_ledger_controller_test.go (1)

328-343: Implementation looks good!

The mock implementation of the new LockLedger method correctly follows the interface pattern. This method will now return a Controller, a database interface, a release function, and an error, supporting the non-transactional locking mechanism for imports.

internal/controller/ledger/controller_with_events.go (1)

169-180: Implementation follows existing patterns!

The LockLedger implementation correctly follows the same pattern as the existing BeginTX method, wrapping the returned controller while preserving the ledger, listener, and parent reference. Error handling is properly implemented by returning early if the underlying controller's LockLedger call fails.

One observation: unlike the BeginTX method, this implementation doesn't set hasTx to true. This appears intentional since the lock operation doesn't create a transaction, which is consistent with the PR's purpose to make imports non-transactional.

internal/controller/ledger/controller_with_traces.go (3)

42-42: Field added for tracing ledger locking operations.

The new lockLedgerHistogram field adds metrics tracking for the lock ledger operation, consistent with the existing pattern for other operations in this controller.


140-143: Proper initialization for new lock ledger metrics.

The initialization follows the established pattern used for other operation metrics in this controller.


463-485: Well-implemented LockLedger method with proper tracing.

The new method wraps the underlying controller's LockLedger method with tracing and metrics instrumentation, consistent with the implementation pattern used for other operations in this controller. The method properly propagates the controller, connection, release function, and error.

internal/controller/ledger/controller_default.go (2)

77-90: LockLedger method implementation follows established controller patterns.

The method properly creates a controller copy, delegates to the store's LockLedger, and returns the relevant components (controller, database connection, release function, and error). This implementation aligns with the PR objective to make imports non-transactional by using a dedicated locking mechanism.


194-194: Improved error handling with specialized error type.

Using NewErrImport instead of the generic fmt.Errorf provides better error categorization and handling capabilities for import-specific errors.

internal/api/v2/mocks_ledger_controller_test.go (1)

328-343: Mock implementation for the new LockLedger method.

The mock correctly implements the expected behavior for the new LockLedger method, returning the appropriate four values: a controller, database connection, release function, and error. This addition maintains test compatibility with the interface changes.

test/e2e/app_lifecycle_test.go (3)

77-77: Updated comment to reflect new advisory lock behavior.

The comment now accurately explains that transactions will block earlier at the advisory lock acquisition stage rather than at the log insertion stage when the ledger is initializing.


111-113: Test updated to verify advisory locks instead of active insert queries.

The query now correctly checks for advisory locks in pg_locks instead of active insert operations in pg_stat_activity, reflecting the system's new locking mechanism implementation.


118-120: Updated assertion for correct advisory lock count.

The test now correctly expects countTransactions+1 advisory locks, accounting for an additional lock used for logs sync hashing. This aligns with the new implementation and ensures proper verification of the locking behavior.

internal/storage/ledger/legacy/adapters.go (1)

299-309: Implementation of LockLedger aligns with interface changes

The new LockLedger implementation properly propagates the locking mechanism through the adapter pattern. It correctly:

  1. Delegates to the underlying newStore's locking mechanism
  2. Creates a new adapter instance that wraps both the locked store and the legacy store with the updated database connection
  3. Returns the necessary components (store, database connection, release function, error) according to the interface contract

This change supports the PR's goal of making imports non-transactional by using explicit locking with proper cleanup.

internal/api/bulking/mocks_ledger_controller_test.go (1)

328-343: Mock implementation for LockLedger correctly matches the interface

The added mock method properly implements the LockLedger method signature from the Controller interface, returning the expected four values: a controller, a database interface, a release function, and an error. The mock recorder method is also correctly implemented to set up expectations for test cases.

internal/controller/ledger/controller_generated_test.go (1)

327-342: Mock Controller implementation for LockLedger properly matches the interface

The generated mock implementation for LockLedger correctly follows the method signature from the Controller interface, returning the expected four return values (Controller, bun.IDB, func() error, error). The implementation properly handles type assertions and helper methods for test expectations.

test/e2e/api_ledgers_import_test.go (3)

134-170: Comprehensive test for import error handling and recovery

This new test case verifies important behavior for the non-transactional import process:

  1. It confirms that when importing data with errors (invalid transaction ID), the import correctly fails with the expected error code
  2. It verifies that despite the failure, valid logs before the error are successfully inserted
  3. It tests that the import process can be resumed from the point of failure with corrected logs

This test is essential for validating the PR's goal of making imports non-transactional, ensuring partial progress is maintained and can be resumed after addressing errors.


402-412: Improved test verification for advisory locks

This enhancement verifies that PostgreSQL advisory locks are being used as expected during concurrent ledger operations. The test now explicitly checks that:

  1. A transaction creation acquires exactly one advisory lock
  2. The test properly waits for the lock to be acquired before proceeding

This addition helps validate that the new locking mechanism is working correctly at the database level.


433-445: Thorough verification of advisory lock contention

This test enhancement verifies the expected behavior when import attempts to run concurrently with transaction creation:

  1. It checks that two advisory locks exist: one granted and one waiting
  2. It confirms that import blocks on acquiring the advisory lock already held by transaction creation
  3. The test properly validates the non-transactional locking behavior

This verification is crucial for ensuring the new locking mechanism handles contention correctly and prevents concurrent modifications as expected.

internal/controller/system/state_tracker.go (2)

32-34: Improved ledger locking in handleState

The implementation now uses the centralized withLock helper function for ledger locking, which properly manages the lock lifecycle.


159-175: Well-implemented withLock helper function

The new withLock helper function centralizes the ledger locking logic with proper error handling and resource cleanup. It correctly logs errors from lock release but doesn't propagate them to avoid masking the original operation errors.

The implementation includes:

  1. Proper acquisition of the lock via LockLedger
  2. Deferred release of the lock
  3. Error logging and telemetry for release failures
  4. Clean function signature that provides both the locked controller and connection to the callback

@gfyrag gfyrag force-pushed the hotfix/v2.2/import-not-transactional branch from b03d097 to 171a055 Compare April 29, 2025 13:20
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: 1

🧹 Nitpick comments (2)
internal/storage/ledger/store.go (1)

149-185: Advisory locks replace table locks with more explicit control

The implementation now uses PostgreSQL advisory locks instead of table locks, with different locking strategies for different database types:

  1. For *bun.DB: Gets a dedicated connection and uses pg_advisory_lock, requiring explicit unlock and connection closure
  2. For bun.Tx: Uses pg_advisory_xact_lock which is automatically released when the transaction ends
  3. For other DB types: Panics with an error message

This approach provides more fine-grained control and aligns with the PR's goal to "make import not transactional".

Two suggestions:

  1. Consider adding comments explaining why advisory locks are preferred over table locks
  2. The panic for unknown DB types could be replaced with a proper error return for better caller experience
internal/controller/system/state_tracker.go (1)

158-175: Add withLock helper for centralized lock management

This new helper function:

  1. Acquires a lock using the controller's LockLedger method
  2. Sets up proper release using defer to ensure locks are always released
  3. Logs and records errors during lock release
  4. Provides a consistent pattern for managing locks

This is a good architectural improvement that centralizes locking logic and ensures proper resource management.

One suggestion: Consider adding more context to the error messages, such as including the ledger ID in error logs to make debugging easier.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b03d097 and 171a055.

⛔ Files ignored due to path filters (1)
  • .coderabbit.yaml is excluded by !**/*.yaml
📒 Files selected for processing (18)
  • internal/api/bulking/mocks_ledger_controller_test.go (1 hunks)
  • internal/api/common/mocks_ledger_controller_test.go (1 hunks)
  • internal/api/v1/mocks_ledger_controller_test.go (1 hunks)
  • internal/api/v2/mocks_ledger_controller_test.go (1 hunks)
  • internal/controller/ledger/controller.go (1 hunks)
  • internal/controller/ledger/controller_default.go (2 hunks)
  • internal/controller/ledger/controller_generated_test.go (1 hunks)
  • internal/controller/ledger/controller_with_cache.go (1 hunks)
  • internal/controller/ledger/controller_with_events.go (1 hunks)
  • internal/controller/ledger/controller_with_too_many_client_handling.go (1 hunks)
  • internal/controller/ledger/controller_with_traces.go (3 hunks)
  • internal/controller/ledger/store.go (1 hunks)
  • internal/controller/ledger/store_generated_test.go (1 hunks)
  • internal/controller/system/state_tracker.go (4 hunks)
  • internal/storage/ledger/legacy/adapters.go (1 hunks)
  • internal/storage/ledger/store.go (1 hunks)
  • test/e2e/api_ledgers_import_test.go (3 hunks)
  • test/e2e/app_lifecycle_test.go (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • test/e2e/app_lifecycle_test.go
🚧 Files skipped from review as they are similar to previous changes (14)
  • internal/controller/ledger/controller.go
  • internal/controller/ledger/controller_with_cache.go
  • internal/api/v1/mocks_ledger_controller_test.go
  • internal/controller/ledger/store_generated_test.go
  • internal/controller/ledger/controller_with_too_many_client_handling.go
  • internal/controller/ledger/controller_generated_test.go
  • internal/storage/ledger/legacy/adapters.go
  • internal/controller/ledger/controller_default.go
  • internal/api/v2/mocks_ledger_controller_test.go
  • internal/api/common/mocks_ledger_controller_test.go
  • internal/api/bulking/mocks_ledger_controller_test.go
  • internal/controller/ledger/controller_with_traces.go
  • test/e2e/api_ledgers_import_test.go
  • internal/controller/ledger/controller_with_events.go
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Tests
  • GitHub Check: Dirty
🔇 Additional comments (4)
internal/controller/system/state_tracker.go (4)

8-9: Add new imports for OTLP error recording and Bun database

The imports have been updated to include OTLP for error recording and Bun for database operations. This aligns with the changes to use advisory locks and structured error handling.

Also applies to: 11-12


32-36: Centralize ledger locking through withLock helper

The handleState method now uses the new withLock helper function instead of directly managing transactions. This centralizes the locking logic and ensures consistent lock acquisition and release across different operations.


40-41: Maintain TODO comment

Keep the TODO comment to remove the code in a later version. This suggests the ledger state persistence might be handled differently in the future.


133-147: Simplify Import method with withLock helper

The Import method has been significantly simplified by using the withLock helper. The previous implementation likely used explicit transaction management with SERIALIZABLE isolation, which has been replaced with advisory locks.

This change aligns perfectly with the PR's goal to "make import not transactional" while still providing isolation through advisory locks.

@@ -49,7 +49,7 @@ type Store interface {
DeleteAccountMetadata(ctx context.Context, address, key string) error
InsertLog(ctx context.Context, log *ledger.Log) error

LockLedger(ctx context.Context) error
LockLedger(ctx context.Context) (Store, bun.IDB, func() error, error)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Significant interface change to LockLedger

The LockLedger method signature has been extended from returning just an error to returning a tuple of (Store, bun.IDB, func() error, error). This change suggests a shift to a more explicit resource management pattern where callers receive a locked controller, database connection, and release function.

This approach provides better control over lock acquisition and release but requires careful handling by callers to ensure the release function is always called.

#!/bin/bash
# Find all implementations of the LockLedger method to ensure they match the new signature
rg -A 3 'func.*LockLedger\(ctx context.Context\)' --type go
🤖 Prompt for AI Agents (early access)
In internal/controller/ledger/store.go at line 52, the LockLedger method signature has changed to return (Store, bun.IDB, func() error, error) instead of just an error. Update all implementations of LockLedger to match this new signature by returning the locked Store instance, the database connection, a release function to unlock or clean up resources, and an error. Ensure callers of LockLedger are updated to handle the new return values properly, especially calling the release function to avoid resource leaks.

@gfyrag gfyrag force-pushed the hotfix/v2.2/import-not-transactional branch from 171a055 to b72771b Compare April 29, 2025 15:35
@gfyrag gfyrag force-pushed the hotfix/v2.2/import-not-transactional branch from b72771b to aa98ff9 Compare April 29, 2025 15:51
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

♻️ Duplicate comments (1)
internal/controller/system/state_tracker.go (1)

165-171: Good error handling for lock release.

The error handling for the lock release is well-implemented, with proper logging and OTLP error recording. As discussed in the previous reviews, the lock will be automatically released if the underlying PostgreSQL connection is broken.

🧹 Nitpick comments (2)
internal/controller/system/state_tracker.go (2)

40-40: Consider adding more context to the TODO comment.

The TODO comment lacks specifics about what needs to be removed in a later version and why.

-	// todo: remove that in a later version
+	// TODO: Remove the explicit state update in a later version as it will be handled automatically by the locking mechanism

134-135: Add more context to the TODO comment.

Similar to the previous TODO, this comment would benefit from additional context.

-		// todo: remove that in a later version
+		// TODO: Remove the explicit ledger state check in a later version as it will be handled at a different layer
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b72771b and aa98ff9.

📒 Files selected for processing (3)
  • internal/controller/ledger/controller_default.go (2 hunks)
  • internal/controller/system/controller.go (1 hunks)
  • internal/controller/system/state_tracker.go (4 hunks)
✅ Files skipped from review due to trivial changes (1)
  • internal/controller/system/controller.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/controller/ledger/controller_default.go
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: GoReleaser
  • GitHub Check: Tests
🔇 Additional comments (4)
internal/controller/system/state_tracker.go (4)

8-11: Appropriate imports for the new locking mechanism.

The added imports of otlp and bun support the new locking mechanism, with otlp for error recording and bun for database connections.


32-34: Good refactoring to use the new centralized locking mechanism.

Refactoring the handleState method to use the new withLock helper function centralizes the locking logic and improves code maintainability.


133-146: Improved import process to handle locking without transactions.

The rewritten Import method now uses the centralized locking mechanism without wrapping the entire import in a transaction, which aligns with the PR objective to make imports non-transactional. This change improves system robustness by allowing partial imports to succeed.


158-175: Well-implemented centralized locking mechanism.

The new withLock helper function properly centralizes the ledger locking logic with good error handling:

  1. It acquires a lock via LockLedger
  2. Executes the provided function with the locked controller and DB connection
  3. Ensures lock release in a deferred function
  4. Properly logs and records any errors during release using OTLP

This approach improves code maintainability and ensures consistent lock management throughout the codebase.

@gfyrag gfyrag force-pushed the hotfix/v2.2/import-not-transactional branch from 739f94d to f61be4a Compare April 30, 2025 08:10
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: 2

🧹 Nitpick comments (1)
internal/api/v2/controllers_logs_import.go (1)

44-46: HTTP status might be too harsh for malformed JSON

A decoding error that is not io.EOF currently returns a 500.
If the body contains invalid JSON, the client supplied bad data, so 400 (Bad Request) may be more appropriate:

- common.InternalServerError(w, r, fmt.Errorf("reading input stream: %w", err))
+ api.BadRequest(w, "IMPORT_DECODE", fmt.Errorf("invalid JSON input: %w", err))

This distinguishes server failures from client mistakes.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa98ff9 and f61be4a.

📒 Files selected for processing (2)
  • internal/api/common/middleware_resolver.go (2 hunks)
  • internal/api/v2/controllers_logs_import.go (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • internal/api/common/middleware_resolver.go
🧰 Additional context used
🧬 Code Graph Analysis (1)
internal/api/v2/controllers_logs_import.go (2)
pkg/testserver/api.go (1)
  • Import (225-228)
internal/api/common/errors.go (1)
  • InternalServerError (40-44)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: GoReleaser
  • GitHub Check: Tests
🔇 Additional comments (1)
internal/api/v2/controllers_logs_import.go (1)

22-27: 👍 Good defensive error wrapping

Wrapping the result of Import with a contextual message before pushing it onto errChan makes downstream error handling clearer. No further action required.

Comment on lines +55 to 64
if err != nil {
handleError(err)
return
}
if stream != nil {
panic("got nil error while not at the end of the stream")
}

api.NoContent(w)
return
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Invariant panic is useful but could be replaced with structured handling

The panic guards against receiving a nil error before the stream is drained,
which is great for catching programmer mistakes in tests, but it will crash the
server in production. Prefer returning a 500 and logging the state instead:

- if stream != nil {
-     panic("got nil error while not at the end of the stream")
- }
+ if stream != nil {
+     err := errors.New("import routine finished prematurely while stream open")
+     common.InternalServerError(w, r, err)
+     return
+ }

This preserves the invariant without taking the whole service down.

🤖 Prompt for AI Agents (early access)
In internal/api/v2/controllers_logs_import.go around lines 55 to 64, replace the panic that occurs when stream is not nil but error is nil with structured error handling. Instead of panicking, log the unexpected state and respond with a 500 Internal Server Error to the client. This change maintains the invariant check while preventing the server from crashing in production.

Comment on lines 41 to 47
close(stream)
break
stream = nil
} else {
common.InternalServerError(w, r, err)
common.InternalServerError(w, r, fmt.Errorf("reading input stream: %w", err))
return
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

💥 Potential panic & busy-loop after EOF

  1. close(stream) is executed every time dec.Decode returns io.EOF.
    On the second (and every subsequent) EOF, stream is already nil, so
    close(stream) will panic with “close of nil channel”.
  2. After io.EOF, the loop continues and immediately calls dec.Decode again, which
    will keep returning io.EOF, creating a tight busy-loop while you wait for
    errChan.

A minimal fix is to guard the close and skip further decoding once EOF is seen:

if errors.Is(err, io.EOF) {
-    close(stream)
-    stream = nil
+    if stream != nil {           // avoid double-close
+        close(stream)
+        stream = nil
+    }
+    continue                     // wait on select for errChan / ctx
}

However, this still leaves the outer for to spin on continue.
Consider introducing a doneDecoding flag (or breaking the loop and having a
separate select) to eliminate the busy-loop entirely.

📝 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
close(stream)
break
stream = nil
} else {
common.InternalServerError(w, r, err)
common.InternalServerError(w, r, fmt.Errorf("reading input stream: %w", err))
return
}
}
if errors.Is(err, io.EOF) {
if stream != nil { // avoid double-close
close(stream)
stream = nil
}
continue // skip to next select/loop iteration
} else {
common.InternalServerError(w, r, fmt.Errorf("reading input stream: %w", err))
return
}
🤖 Prompt for AI Agents (early access)
In internal/api/v2/controllers_logs_import.go around lines 41 to 47, the code closes the stream channel every time dec.Decode returns io.EOF, which causes a panic on subsequent EOFs because the channel is already closed or nil. Additionally, the loop continues decoding after EOF, causing a busy-loop. Fix this by adding a guard to only close the stream once and prevent further decoding after EOF by setting a doneDecoding flag or breaking the loop to avoid the busy-loop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants