Skip to content
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

fix(tee): fix race condition in batch locking #3342

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

pbeza
Copy link
Collaborator

@pbeza pbeza commented Nov 28, 2024

What ❔

After scaling zksync-tee-prover to two instances/replicas on Azure for azure-stage2, azure-testnet2, and azure-mainnet2, we started experiencing duplicated proving for some batches.
logs
While this is not an erroneous situation, it is wasteful from a resource perspective. This was due to a race condition in batch locking. This PR fixes the issue by adding atomic batch locking.

Why ❔

To fix the bug that only activates after running zksync-tee-prover on multiple instances.

Checklist

  • PR title corresponds to the body of PR (we generate changelog entries from PRs).
  • Tests for the changes have been added / updated.
  • Documentation comments have been added / updated.
  • Code has been formatted via zkstack dev fmt and zkstack dev lint.

After [scaling][1] [zksync-tee-prover][2] to two instances/replicas on
Azure for azure-stage2, azure-testnet2, and azure-mainnet2, we started
experiencing [duplicated proving for some batches][3]. While this is not
an erroneous situation, it is wasteful from a resource perspective. This
was due to a race condition in batch locking. This PR fixes the issue by
adding atomic batch locking.

[1]: https://github.com/matter-labs/gitops-kubernetes/pull/7033/files
[2]: https://github.com/matter-labs/zksync-era/blob/aaca32b6ab411d5cdc1234c20af8b5c1092195d7/core/bin/zksync_tee_prover/src/main.rs
[3]: https://grafana.matterlabs.dev/goto/M1I_Bq7HR?orgId=1
@pbeza pbeza force-pushed the tee/fix/atomic-batch-locking branch from 46dcfde to 7d96c1c Compare November 29, 2024 12:10
Copy link
Contributor

@slowli slowli left a comment

Choose a reason for hiding this comment

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

Dumb question: How is the locking made atomic in this PR? AFAIU, the first SELECT statement, if queried concurrently, can still return the same L1 batch number unless some kind of row-level locking is implemented (cf. SELECT FOR UPDATE SKIP LOCKED in this contract verifier query). I'm not even sure the UPDATE query will fail for the transaction committed last in case of a race (maybe it would with serialization isolation level, but I'd argue that erroring is not the best cause of action here; row-level locks seem to work better).

core/lib/dal/src/models/storage_tee_proof.rs Outdated Show resolved Hide resolved
let batch_number = match batch_number {
Some(batch) => batch.l1_batch_number,
None => {
transaction.commit().await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

There are seemingly no changes in the transaction at this point. Is the commit here just to not clean up DB resources faster?

Copy link
Collaborator Author

@pbeza pbeza Nov 29, 2024

Choose a reason for hiding this comment

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

To be honest, I just presumed every .start_transaction() needed to either end with .commit() or .rollback(). I'm not sure how you determine that it's OK to skip it altogether, but I trust your judgment here. I removed this line here. ✅

Copy link
Contributor

Choose a reason for hiding this comment

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

A transaction that is dropped w/o being committed is rolled back implicitly. You can add an explicit call to be more precise (but IMO it should be rollback so that it's clear that no modifying actions have been performed).

@pbeza
Copy link
Collaborator Author

pbeza commented Nov 29, 2024

Dumb question: How is the locking made atomic in this PR? (...)

Not a dumb question at all! The dumb one here was me! ;P I totally misunderstood what SQL transactions can actually handle in this context. Had to brush up on the finer details of SQL locking. Thanks for steering me in the right direction! These two links were super helpful:

Comment on lines +93 to +94
FOR UPDATE OF p
SKIP LOCKED
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I had to explicitly use OF p here. If you leave it out or switch it to OF tee, you will run into this error:

error: error returned from database: FOR UPDATE cannot be applied to the nullable side of an outer join

Copy link
Contributor

Choose a reason for hiding this comment

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

The error is understandable. Just to clarify, since it's the tee_proof_generation_details table that is updated below: Do I understand correctly that this new query can actually have tee_proof_generation_details side of the join absent (I guess if the query is invoked for a certain batch for the first time?). If so, I'd suggest to add a comment explaining why rows are locked on a table that's not modified in the transaction.

@pbeza
Copy link
Collaborator Author

pbeza commented Nov 29, 2024

@slowli, I’ve addressed your code review comments. Take a look when you get a chance.

It’s kinda hard to test properly without deploying it to stage and letting it run for a while. Specifically, let me know if locking rows in the proof_generation_details table is okay (instead of just locking tee_proof_generation_details rows).

@pbeza pbeza requested a review from slowli November 29, 2024 19:00
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