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

storage/concurrency: don't push reservation transactions #45420

Merged
merged 2 commits into from
Feb 26, 2020

Conversation

nvanbenschoten
Copy link
Member

This commit updates the lockTableWaiter to no longer push reservation transactions. The waiter now only pushes the holders of conflicting locks that are held by other transactions.

This is because a transaction push will block until the pushee transaction has either committed or aborted. It is therefore not appropriate for cases where there is still a chance for the pusher to make progress without the pushee completing first. This is not true when the waiter is waiting on a held lock - the lock must be released before the waiter may proceed. However, it is true when the waiter is waiting on a reservation - the reservation holder may release the reservation without acquiring a lock, allowing the waiter to proceed without conflict.

This was the cause of deadlocks in some of the experiments I was running. The exact sequence I was running into was that:

  1. key K had a lock wait-queue
  2. txn A acquired a reservation for key K
  3. txn B queued behind txn A, becoming the distinguished waiter
  4. txn B began pushing txn A
  5. txn A hit a WriteTooOld error and did not acquire a lock
  6. txn A exited the lock wait-queue
  7. txn A handled the WriteTooOld error, returned to key K, and queued behind txn B
  8. txn B waited on txn A indefinitely in the txnWaitQueue
  9. deadlock!

@cockroach-teamcity
Copy link
Member

This change is Reviewable

Copy link
Collaborator

@sumeerbhola sumeerbhola left a comment

Choose a reason for hiding this comment

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

:lgtm:

Reviewed 2 of 2 files at r1, 1 of 1 files at r2.
Reviewable status: :shipit: complete! 1 of 0 LGTMs obtained (waiting on @nvanbenschoten)


pkg/storage/concurrency/lock_table_waiter.go, line 88 at r2 (raw file):

				// conflict.
				// NOTE: we could pull this logic into pushTxn, but then we'd be
				// creating useless timers.

The "therefore not appropriate" sentence followed by the "This is not true" sentence were a bit hard to read, so I tried to rephrase in a simpler form below, and added some further detail, in case you can use that to adjust the comment.

// If the lock is not held and instead has a reservation, we don't
// want to push the reservation transaction. A transaction push will
// block until the pushee transaction has either committed or
// aborted or pushed or rolled back savepoints, i.e., there is some state
// change that has happened to the transaction record that unblocks the
// pusher. It will not unblock merely because a request issued by the
// pushee transaction has completed and released a reservation. Note
// that:
// - reservations are not a guarantee that the lock will be acquired.
// - the two reasons to push do not apply to requests holding reservations:
// - competing requests compete at exactly one lock table, so there is no
// possibility of distributed deadlock due to reservations.
// - the lock table can prioritize requests based on transaction priorities (TODO(sbhola) ...)


pkg/storage/concurrency/lock_table_waiter.go, line 96 at r2 (raw file):

				// reservations?
				// TODO(sbhola): does this allow us to get rid of the concept of a
				// reservaton entirely? Is there a reason to keep it?

it is likely that we can eliminate reservations from the lock table.

This commit updates the lockTableWaiter to no longer push reservation
transactions. The waiter now only pushes the holders of conflicting
locks that are held by other transactions.

This is because a transaction push will block until the pushee
transaction has either committed or aborted. It is therefore not
appropriate for cases where there is still a chance for the pusher to
make progress without the pushee completing first. This is not true when
the waiter is waiting on a held lock - the lock must be released before
the waiter may proceed. However, it is true when the waiter is waiting
on a reservation - the reservation holder may release the reservation
without acquiring a lock, allowing the waiter to proceed without
conflict.

This was the cause of deadlocks in some of the experiments I was
running. The exact sequence I was running into was that:
1. key K had a lock wait-queue
2. txn A acquired a reservation for key K
3. txn B queued behind txn A, becoming the distinguished waiter
4. txn B began pushing txn A
5. txn A hit a WriteTooOld error and did not acquire a lock
6. txn A exited the lock wait-queue
7. txn A handled the WriteTooOld error, returned to key K, and queued
behind txn B
8. txn B waited on txn A indefinitely in the txnWaitQueue
9. deadlock!
…ests

The test was taking 55s. This drops it down to a more reasonable 4s.
@nvanbenschoten nvanbenschoten force-pushed the nvanbenschoten/lockTableWait branch from f2b0bb7 to ff898bd Compare February 26, 2020 21:22
Copy link
Member Author

@nvanbenschoten nvanbenschoten left a comment

Choose a reason for hiding this comment

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

bors r+

Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (and 1 stale) (waiting on @sumeerbhola)


pkg/storage/concurrency/lock_table_waiter.go, line 88 at r2 (raw file):

Previously, sumeerbhola wrote…

The "therefore not appropriate" sentence followed by the "This is not true" sentence were a bit hard to read, so I tried to rephrase in a simpler form below, and added some further detail, in case you can use that to adjust the comment.

// If the lock is not held and instead has a reservation, we don't
// want to push the reservation transaction. A transaction push will
// block until the pushee transaction has either committed or
// aborted or pushed or rolled back savepoints, i.e., there is some state
// change that has happened to the transaction record that unblocks the
// pusher. It will not unblock merely because a request issued by the
// pushee transaction has completed and released a reservation. Note
// that:
// - reservations are not a guarantee that the lock will be acquired.
// - the two reasons to push do not apply to requests holding reservations:
// - competing requests compete at exactly one lock table, so there is no
// possibility of distributed deadlock due to reservations.
// - the lock table can prioritize requests based on transaction priorities (TODO(sbhola) ...)

Thanks for the suggestion. Done.


pkg/storage/concurrency/lock_table_waiter.go, line 96 at r2 (raw file):

Previously, sumeerbhola wrote…

it is likely that we can eliminate reservations from the lock table.

Ack. That will be a nice simplification.

@craig
Copy link
Contributor

craig bot commented Feb 26, 2020

Build succeeded

@craig craig bot merged commit 75f4ff3 into cockroachdb:master Feb 26, 2020
@nvanbenschoten nvanbenschoten deleted the nvanbenschoten/lockTableWait branch February 26, 2020 22:31
nvanbenschoten added a commit to nvanbenschoten/cockroach that referenced this pull request Mar 2, 2020
This is a partial reversion of cockroachdb#45420.

It turns out that there are cases where a reservation holder is a link
in a dependency cycle. This can happen when a reservation holder txn
is holding on to one reservation while blocking on a lock on another
key. If any txns queued up behind the reservation did not push ~someone~,
they wouldn't detect the deadlock.

```
     range A      .     range B
                  .
       txnB       .       txnC              txnA
        |         .        |  ^________      |
        v         .        v            \    v
 [lock X: (txnA)] .  [lock Y: (txnB)]  [res Z: (txnC)]
```

It turns out that this segment of the dependency cycle is always local
to a single concurrency manager, so it could potentially forward the
push through the reservation links to shorten the cycle and prevent
non-lock holders from ever being the victim of a deadlock abort. This
is tricky though, so for now, we just push.

To address the issue that motivated cockroachdb#45420, we perform this form of
push asynchronously while continuing to listen to state transitions
in the lockTable. If the pusher is unblocked (see cockroachdb#45420 for an example
of when that can happen), it simply cancels the push and proceeds with
navigating the lockTable.
nvanbenschoten added a commit to nvanbenschoten/cockroach that referenced this pull request Mar 3, 2020
This is a partial reversion of cockroachdb#45420.

It turns out that there are cases where a reservation holder is a link
in a dependency cycle. This can happen when a reservation holder txn
is holding on to one reservation while blocking on a lock on another
key. If any txns queued up behind the reservation did not push ~someone~,
they wouldn't detect the deadlock.

```
     range A      .     range B
                  .
       txnB       .       txnC              txnA
        |         .        |  ^________      |
        v         .        v            \    v
 [lock X: (txnA)] .  [lock Y: (txnB)]  [res Z: (txnC)]
```

It turns out that this segment of the dependency cycle is always local
to a single concurrency manager, so it could potentially forward the
push through the reservation links to shorten the cycle and prevent
non-lock holders from ever being the victim of a deadlock abort. This
is tricky though, so for now, we just push.

To address the issue that motivated cockroachdb#45420, we perform this form of
push asynchronously while continuing to listen to state transitions
in the lockTable. If the pusher is unblocked (see cockroachdb#45420 for an example
of when that can happen), it simply cancels the push and proceeds with
navigating the lockTable.
craig bot pushed a commit that referenced this pull request Mar 3, 2020
45551: ui: standardize the display of node names in node lists r=dhartunian a=petermattis

Standardize the display of node names in node lists. The nodes overview
list was displaying nodes as `N<id> <ip-address>` while graphs display
`<ip-address> (n<id>)`. Standardize on the latter format. A similar
problem existed on the statement details page which was displaying
`N<id> <ip-address> (n<id>)`. The node id was being displayed twice!

Release note: None

45567: storage/concurrency: push reservation holders to detect deadlocks r=nvanbenschoten a=nvanbenschoten

This is a partial reversion of #45420.

It turns out that there are cases where a reservation holder is a link in a dependency cycle. This can happen when a reservation holder txn is holding on to one reservation while blocking on a lock on another key. If any txns queued up behind the reservation did not push _someone_, they wouldn't detect the deadlock.

```
     range A      .     range B
                  .
       txnB       .       txnC              txnA
        |         .        |  ^________      |
        v         .        v            \    v
 [lock X: (txnA)] .  [lock Y: (txnB)]  [res Z: (txnC)]
```

It turns out that this segment of the dependency cycle is always local to a single concurrency manager, so it could potentially forward the push through the reservation links to shorten the cycle and prevent non-lock holders from ever being the victim of a deadlock abort. This is tricky though, so for now, we just push.

To address the issue that motivated #45420, we perform this form of push asynchronously while continuing to listen to state transitions in the lockTable. If the pusher is unblocked (see #45420 for an example of when that can happen), it simply cancels the push and proceeds with navigating the lockTable.

This PR also adds a set of datadriven deadlock tests to the concurrency manager test suite: [`testdata/concurrency_manager/deadlocks`](https://github.com/cockroachdb/cockroach/pull/45567/files#diff-5934754d5a8f1086698cdbab628ee1b5). These tests construct deadlocks due to lock ordering and request ordering and demonstrate how the deadlocks are resolved.

45568: roachtest: enable txn heartbeat loops for 1PC txns in kv/contention/nodes=4 r=nvanbenschoten a=nvanbenschoten

I noticed when debugging issues in #45482 that unhandled deadlocks occasionally
resolved themselves because txns would eventually time out. This was because
we don't start the txn heartbeat loop for 1PC txns. In this kind of test, we
want any unhandled deadlocks to be as loud as possible, so just like we set
a very long txn expiration, we also enable the txn heartbeat loop for all
txns, even those that we expect will be 1PC.

This commit also drops the kv.lock_table.deadlock_detection_push_delay down
for the test, since it's already touching this code.

Co-authored-by: Peter Mattis <petermattis@gmail.com>
Co-authored-by: Nathan VanBenschoten <nvanbenschoten@gmail.com>
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.

3 participants