8000 fix: configure sequences after import by gfyrag · Pull Request #935 · formancehq/ledger · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

fix: configure sequences after import #935

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

8000 Merged
merged 1 commit into from
May 22, 2025

Conversation

gfyrag
Copy link
Contributor
@gfyrag gfyrag commented May 20, 2025

No description provided.

@gfyrag gfyrag requested a review from a team as a code owner May 20, 2025 12:37
Copy link
coderabbitai bot commented May 20, 2025

Walkthrough

The changes update the ledger state management logic in the controller, ensuring that state transitions and database sequence resets occur atomically and conditionally within a transaction. End-to-end tests are extended to verify correct sequence restoration after ledger import, and test assertions and comments are refined for accuracy regarding advisory locks and concurrency.

Changes

Files/Paths Change Summary
internal/controller/system/state_tracker.go Refactored handleState to use a read-write mutex, conditionally update ledger state, reset sequences atomically, and improve error handling.
test/e2e/api_ledgers_import_test.go Extended ledger import test to verify sequence restoration; updated concurrency test comment and SQL query filter to reflect actual blocking query.
test/e2e/app_lifecycle_test.go Adjusted assertion for advisory lock count during in-flight transactions to expect exact count without extra lock.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ControllerFacade
    participant Database

    Client->>ControllerFacade: handleState(fn)
    ControllerFacade->>Database: Begin Transaction
    ControllerFacade->>Database: Update ledger state to 'InUse' if 'Initializing'
    alt State was 'Initializing'
        ControllerFacade->>Database: Reset transaction_id and log_id sequences
    end
    ControllerFacade->>ControllerFacade: fn(ctrl)
    ControllerFacade->>Database: Commit Transaction
    ControllerFacade-->>Client: Return result or error
Loading

Possibly related PRs

Suggested reviewers

  • paul-nicolas
  • Dav-14

Poem

A ledger’s state, now locked with care,
Sequences reset with atomic flair.
Tests ensure IDs march on in line,
Advisory locks now counted just fine.
With every hop, the code grows neat—
A rabbit’s work is quite the feat!
🐇✨

Note

⚡️ AI Code Reviews for VS Code, Cursor, Windsurf

CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
Learn more here.


Note

⚡️ Faster reviews with caching

CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.
Enjoy the performance boost—your workflow just got faster.


📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between b930d05 and 0c56c9c.

📒 Files selected for processing (3)
  • internal/controller/system/state_tracker.go (2 hunks)
  • test/e2e/api_ledgers_import_test.go (2 hunks)
  • test/e2e/app_lifecycle_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/e2e/app_lifecycle_test.go
🧰 Additional context used
🧠 Learnings (1)
internal/controller/system/state_tracker.go (1)
Learnt from: gfyrag
PR: formancehq/ledger#935
File: internal/controller/system/state_tracker.go:50-55
Timestamp: 2025-05-20T13:07:54.477Z
Learning: In the ledger codebase's `handleState` method, when updating ledger state from `StateInitializing` to `StateInUse`, it's intentional to proceed silently when `rowsAffected == 0`. This indicates another parallel transaction has already updated the ledger state and configured the sequences, so no error needs to be returned and no sequence updating is required.
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Tests
🔇 Additional comments (6)
internal/controller/system/state_tracker.go (5)

12-12: Added mutex for thread-safe ledger state management.

Adding the sync package to support the new RWMutex implementation is a good step toward thread-safe state management.


17-17: Added mutex for concurrent ledger state access protection.

The addition of the RWMutex field improves thread-safety by protecting concurrent access to the ledger state.


22-26: Protected ledger state reads with a read lock.

Using a read lock to safely access the ledger state is a good practice. The local copy ensures consistent state throughout the function execution.


40-54: Improved ledger state update to handle concurrent modifications.

The state update now properly checks if the ledger was in the initializing state before proceeding, which prevents race conditions. The returned rowsAffected value is correctly handled to detect if the update succeeded.

Based on the retrieved learning, proceeding silently when rowsAffected is 0 is intentional because it indicates another parallel transaction has already updated the ledger state and configured the sequences.


94-102: Improved transaction handling and in-memory state update.

The code now correctly:

  1. Updates the in-memory ledger state only after the transaction is successfully committed
  2. Uses a write lock to protect the state update

This avoids race conditions and ensures the in-memory state always reflects the database state.

test/e2e/api_ledgers_import_test.go (1)

430-437: Clarified test comments and improved query filter.

The updated comment and query filter better reflect what's actually happening - the sequence setting operation is blocked by the table lock, not log insertion. This makes the test more accurate and easier to understand.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate Unit Tests
  • Create PR with Unit Tests
  • Commit Unit Tests in branch hotfix/v2.2/configure-sequences-after-import
  • Post Copyable Unit Tests in Comment

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 auto-generate unit tests to generate unit tests for 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/configure-sequences-after-import branch from df3eee0 to b930d05 Compare May 20, 2025 12:38
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: 5

🧹 Nitpick comments (1)
test/e2e/app_lifecycle_test.go (1)

430-437: Predicate tightened to the actual blocking query

Changing the filter to LIKE '%select setval%' targets the sequence-update statement that is indeed blocked by the table lock – good catch.
Consider making the pattern case-insensitive (ILIKE) to avoid future breakages due to capitalisation.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7b80907 and b930d05.

📒 Files selected for processing (3)
  • internal/controller/system/state_tracker.go (1 hunks)
  • test/e2e/api_ledgers_import_test.go (2 hunks)
  • test/e2e/app_lifecycle_test.go (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
test/e2e/api_ledgers_import_test.go (4)
pkg/testserver/api.go (1)
  • CreateTransaction (37-45)
pkg/client/models/operations/v2createtransaction.go (1)
  • V2CreateTransactionRequest (9-23)
pkg/client/models/components/v2posttransaction.go (1)
  • V2PostTransaction (29-35)
pkg/client/models/components/v2posting.go (1)
  • V2Posting (10-15)
internal/controller/system/state_tracker.go (1)
internal/ledger.go (2)
  • StateInUse (15-15)
  • StateInitializing (14-14)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Tests
🔇 Additional comments (1)
test/e2e/app_lifecycle_test.go (1)

119-121: Expectation update looks correct

Adjusting the expected advisory-lock count from countTransactions+1 to countTransactions reflects the removal of the extra lock for sequence hashing. Test intention stays intact.

Comment on lines +46 to +55
rowsAffected, err := ret.RowsAffected()
if err != nil {
return err
}

if rowsAffected > 0 {
Copy link
@coderabbitai coderabbitai bot May 20, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Return a dedicated error when the ledger is already in use

The current branch silently proceeds when rowsAffected == 0, which is ambiguous: the ledger could already be in-use or the row is missing. Propagate an explicit error (e.g. ErrLedgerAlreadyInUse) so callers can distinguish both cases.

🤖 Prompt for AI Agents
In internal/controller/system/state_tracker.go around lines 46 to 51, the code
currently proceeds silently when rowsAffected is 0, which is ambiguous. Modify
the code to return a dedicated error, such as ErrLedgerAlreadyInUse, when
rowsAffected equals 0, so that callers can clearly distinguish between the
ledger being already in use and the row being missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't worry, it's the purpose.
If the ledger was already updated in a parallel transaction, we don't need to update sequences.

Copy link

Choose a reason for hiding this comment

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

@gfyrag I understand now - thanks for the clarification! The behavior is intentional as part of your concurrency design. If a parallel transaction already updated the ledger state from StateInitializing to StateInUse, the sequences would have already been configured by that transaction, so there's no need to update them again or raise an error.


✏️ Learnings added
Learnt from: gfyrag
PR: formancehq/ledger#935
File: internal/controller/system/state_tracker.go:50-55
Timestamp: 2025-05-20T13:07:54.477Z
Learning: In the ledger codebase's `handleState` method, when updating ledger state from `StateInitializing` to `StateInUse`, it's intentional to proceed silently when `rowsAffected == 0`. This indicates another parallel transaction has already updated the ledger state and configured the sequences, so no error needs to be returned and no sequence updating is required.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines 52 to 82
_, err := tx.NewRaw(
fmt.Sprintf(`
select setval(
'"%s"."transaction_id_%d"',
(
select max(id) from "%s".transactions where ledger = '%s'
)::bigint
)
`, c.ledger.Bucket, c.ledger.ID, c.ledger.Bucket, c.ledger.Name),
).Exec(ctx)
if err != nil {
return fmt.Errorf("failed to update transactions sequence value: %w", err)
}

_, err = tx.NewRaw(
fmt.Sprintf(`
select setval(
'"%s"."log_id_%d"',
(
select max(id) from "%s".logs where ledger = '%s'
)::bigint
)
`, c.ledger.Bucket, c.ledger.ID, c.ledger.Bucket, c.ledger.Name),
).Exec(ctx)
if err != nil {
return fmt.Errorf("failed to update logs sequence value: %w", err)
}
Copy link
@coderabbitai coderabbitai bot May 20, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Robustness & SQL-injection hardening of sequence reset queries

  1. setval(.., (select max(id)…)) fails with NULL value when the table is empty. Wrap the sub-query with COALESCE(max(id),0).
  2. Building SQL via fmt.Sprintf interpolates Bucket, Name etc. directly into the query. Although these values come from our own schema, using placeholders avoids surprises and quoting headaches.
  3. Both transaction_id_* and log_id_* queries share the same structure – consider extracting a helper to DRY.

Example safer statement:

- _, err := tx.NewRaw(fmt.Sprintf(`
-     SELECT setval('"%s"."transaction_id_%d"',
-                   COALESCE((SELECT max(id) FROM "%s".transactions WHERE ledger = $1),0)::bigint)
- `, c.ledger.Bucket, c.ledger.ID, c.ledger.Bucket), c.ledger.Name).Exec(ctx)
+ _, err := tx.NewRaw(`
+     SELECT setval(format('"%I"."transaction_id_%s"', ?, ?)::regclass,
+                   COALESCE((SELECT max(id) FROM format('"%I".transactions', ?)::regclass
+                             WHERE ledger = $1),0)::bigint)
+ `, c.ledger.Bucket, c.ledger.ID, c.ledger.Bucket, c.ledger.Bucket, c.ledger.Name).Exec(ctx)

(Exact syntax may vary, but illustrates parameterisation & COALESCE.)

Failing to apply COALESCE will break brand-new ledgers and the SQL-injection risk is small but real.

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

🤖 Prompt for AI Agents
In internal/controller/system/state_tracker.go around lines 52 to 78, the SQL
queries resetting sequences use fmt.Sprintf with direct string interpolation,
risking SQL injection and failing when tables are empty due to NULL max(id). Fix
by replacing max(id) with COALESCE(max(id), 0) to handle empty tables, use
parameterized queries or placeholders instead of fmt.Sprintf to safely inject
variables like Bucket and Name, and refactor the repeated sequence reset logic
into a helper function to avoid duplication.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

SQL injections can't occurs as the values of the parameters are know and secured.
Also, the coalesce call is not needed, even for brand new ledgers.

Copy link

Choose a reason for hiding this comment

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

@gfyrag Thank you for the clarification. I understand that in this context the values are controlled internally, mitigating the SQL injection risk. I also appreciate your insight about brand new ledgers not requiring the COALESCE handling - that's valuable context about how ledger initialization works in this system.


✏️ Learnings added
Learnt from: gfyrag
PR: formancehq/ledger#935
File: internal/controller/system/state_tracker.go:0-0
Timestamp: 2025-05-20T13:48:07.434Z
Learning: In the Formance ledger codebase, sequence reset queries with `select setval` don't require COALESCE around max(id) even for brand new ledgers, as the system handles this case properly.

Learnt from: gfyrag
PR: formancehq/ledger#935
File: internal/controller/system/state_tracker.go:0-0
Timestamp: 2025-05-20T13:48:07.434Z
Learning: The parameters used in sequence reset queries in the Formance ledger system (like Bucket, ID, and Name) are internally controlled values that are known to be secure, reducing SQL injection concerns when using string interpolation.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +338 to +352
By("Checking sequence restoration", func() {
tx, err := CreateTransaction(ctx, testServer.GetValue(), operations.V2CreateTransactionRequest{
Ledger: createLedgerRequest.Ledger,
V2PostTransaction: components.V2PostTransaction{
Postings: []components.V2Posting{{
Source: "world",
Destination: "dst",
Asset: "USD",
Amount: big.NewInt(100),
}},
},
})
Expect(err).To(BeNil())
Expect(tx.ID.Uint64()).To(Equal(transactionsFromOriginalLedger.Data[0].ID.Uint64() + 1))
})
Copy link
@coderabbitai coderabbitai bot May 20, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Potential flakiness: assumes transaction list is ordered

The assertion uses transactionsFromOriginalLedger.Data[0].ID, implicitly relying on the list being ordered descending by ID.
If the API ever changes its default ordering, the test will start failing even though the sequence logic is fine.

Safer alternative:

- Expect(tx.ID.Uint64()).To(Equal(transactionsFromOriginalLedger.Data[0].ID.Uint64() + 1))
+ maxID := uint64(0)
+ for _, t := range transactionsFromOriginalLedger.Data {
+     if id := t.ID.Uint64(); id > maxID {
+         maxID = id
+     }
+ }
+ Expect(tx.ID.Uint64()).To(Equal(maxID + 1))
🤖 Prompt for AI Agents
In test/e2e/api_ledgers_import_test.go around lines 338 to 352, the test assumes
the transaction list is ordered by ID descending when accessing
transactionsFromOriginalLedger.Data[0].ID, which can cause flakiness if the API
changes ordering. To fix this, explicitly sort or find the transaction with the
highest ID before asserting the next transaction ID, ensuring the test does not
rely on implicit ordering.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The default behavior of the api will never changes.

Copy link

Choose a reason for hiding this comment

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

@gfyrag I understand. If the API's default ordering is guaranteed to remain stable, then the current implementation is indeed reliable. Thanks for the clarification.


✏️ Learnings added
Learnt from: gfyrag
PR: formancehq/ledger#935
File: test/e2e/api_ledgers_import_test.go:338-352
Timestamp: 2025-05-20T13:08:10.000Z
Learning: The transactions list API for the ledger service has a stable ordering by ID that will never change, making it safe to rely on transactionsFromOriginalLedger.Data[0].ID in tests.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@gfyrag gfyrag force-pushed the hotfix/v2.2/configure-sequences-after-import branch from b930d05 to 0c56c9c Compare May 20, 2025 13:06
if rowsAffected > 0 {
_, err := tx.NewRaw(
fmt.Sprintf(`
select setval(
Copy link

Choose a reason for hiding this comment

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

bucket and name are open for sql injection -- as we're not only manipulating values but also field names, it's not super obvious, but we should make sure ledger name and bucket are sanitized before this (I assume they are already, but I need to ask ;) )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, checked here

@gfyrag gfyrag merged commit 03723c8 into release/v2.2 May 22, 2025
8 checks passed
@gfyrag gfyrag deleted the hotfix/v2.2/configure-sequences-after-import branch May 22, 2025 10:43
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.

2 participants
0