Skip to content

etcd: STM transaction queue to effectively reduce retries for conflicting transactions - #4457

Merged
bhandras merged 6 commits into
lightningnetwork:masterfrom
bhandras:etcd_tx_queue
Sep 17, 2020
Merged

bhandras merged 6 commits into
lightningnetwork:masterfrom
bhandras:etcd_tx_queue

Conversation

@bhandras

@bhandras bhandras commented Jul 10, 2020

Copy link
Copy Markdown
Collaborator

rebased on #4411
now on master since #4411 is merged

This PR adds (and integrates) commitQueue which' purpose is to detect conflicts for concurrently applied transactions and effectively reduce retries, by applying queuing up conflicting transactions for sequential execution, while leaving all non-conflicting ones to run freely (potentially in parallel).

@bhandras
bhandras requested review from Roasbeef and cfromknecht July 13, 2020 08:14
@Roasbeef Roasbeef added database Related to the database/storage of LND etcd optimization labels Jul 20, 2020
@bhandras
bhandras force-pushed the etcd_tx_queue branch 3 times, most recently from 6e5b085 to e8fb359 Compare August 10, 2020 14:52
@bhandras bhandras changed the title wip tx queue STM transaction queue to effectively reduce retries for conflicting transactions Aug 10, 2020
@bhandras
bhandras marked this pull request as ready for review August 10, 2020 14:59
@bhandras bhandras changed the title STM transaction queue to effectively reduce retries for conflicting transactions etcd: STM transaction queue to effectively reduce retries for conflicting transactions Aug 10, 2020
@bhandras bhandras added this to the 0.12.0 milestone Aug 10, 2020

@Roasbeef Roasbeef left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The implementation is much simpler than I thought it would be! Just completed an initial pass, and nothing glaring jumped out. Will do another pass once I run it on an actual replicated db lnd instance. It would also be interesting to create a small patch that lets us run certain itests w/ and w/o this change so we can gauge the rough impact of the change on perf.

Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated
Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why don't we need to block if there's a pending transaction in the queue that reads this key, we want to write it, but don't also read the key ourselves?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's because if our read set contains the key then we already increased c.readerMap[key] above, so to make sure reader lock count is non zero we have to "uncount ourselves".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this would be much easier to reason about by just by doing two passes through the sets:

for key := range rset {
    blocked |= c.writerMap[key] > 0
}
for key := range wset {
    blocked |= c.writerMap[key] > 0 || c.readerMap[key] > 0
}
for key := range rset {
    c.readerMap[key] += 1
}
for key := range wset {
    c.writerMap[key] += 1
}

Performance wise I doubt we'll see any difference.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, it's a bit hard to read... Unfortunately we can't use the the simplified version above because if the same transaction also reads the key (where no other readers are present) then will unnecessary block. This is the reason for the rsetContainsKey variable. Added a few comments to clarify. I'm open to any suggestions you may find that simplifies though.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

** and also simplified a bit to make it more? readable

Comment thread channeldb/kvdb/etcd/db.go Outdated

@cfromknecht cfromknecht left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

very cool how small the diff is!

Comment thread channeldb/kvdb/etcd/db.go Outdated
Comment thread channeldb/kvdb/etcd/embed.go Outdated
Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated
Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated
Comment thread channeldb/kvdb/etcd/stm.go Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is it intentional that this shadows the err in the outer scope? o/w i don't see where that error is read?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, so it's a bit tricky to read this at first, but it's really simple actually.

  1. What we do is we first run the apply closure to gather the read/write sets so we can add the tx to the contention queue.
  2. The execute closure is executed there (either immediately or in the queue goroutine).

The err simply holds the error trough the above described execution graph.
3) we wait for the done signal and them clean the keys from the queue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thanks for the explanation, makes sense now! i also see that it's the return value at the end of the function, so that's where it is "read"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since we return immediately if the error is non-nil here, wouldn't if err := apply(s); err != nil be equivalent?

I agree that the shadowing is tricky to read. Could make sense to add more errors with descriptive names (i.e. executeErr) to make it easier.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, maybe it's simpler to read if we distinguish errors by scope. PTAL

Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated
Comment thread channeldb/kvdb/etcd/commit_queue_test.go Outdated
@bhandras

Copy link
Copy Markdown
Collaborator Author

The implementation is much simpler than I thought it would be! Just completed an initial pass, and nothing glaring jumped out. Will do another pass once I run it on an actual replicated db lnd instance. It would also be interesting to create a small patch that lets us run certain itests w/ and w/o this change so we can gauge the rough impact of the change on perf.

Yes, originally the queue was optional but decided to make it non-optional as really it should be on all the time.
We can still do the comparison, as it just requires removing these commits from the itest PR (#4402)

@bhandras
bhandras force-pushed the etcd_tx_queue branch 3 times, most recently from 0cb3658 to cf25382 Compare September 4, 2020 14:20

@cfromknecht cfromknecht left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 🌮

@bhandras
bhandras requested a review from halseth September 16, 2020 06:37

@halseth halseth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fun change :)

Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

style suggestion: blocked ||= c.writerMap[key] > 0

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment thread channeldb/kvdb/etcd/stm.go Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since we return immediately if the error is non-nil here, wouldn't if err := apply(s); err != nil be equivalent?

I agree that the shadowing is tricky to read. Could make sense to add more errors with descriptive names (i.e. executeErr) to make it easier.

Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

use the more common waitgroup pattern instead?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this would be much easier to reason about by just by doing two passes through the sets:

for key := range rset {
    blocked |= c.writerMap[key] > 0
}
for key := range wset {
    blocked |= c.writerMap[key] > 0 || c.readerMap[key] > 0
}
for key := range rset {
    c.readerMap[key] += 1
}
for key := range wset {
    c.writerMap[key] += 1
}

Performance wise I doubt we'll see any difference.

Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated

@bhandras bhandras left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the review @halseth! Main change is the (hopefully) more readable rset/wset scans. PTAL

Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, it's a bit hard to read... Unfortunately we can't use the the simplified version above because if the same transaction also reads the key (where no other readers are present) then will unnecessary block. This is the reason for the rsetContainsKey variable. Added a few comments to clarify. I'm open to any suggestions you may find that simplifies though.

Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated
Comment thread channeldb/kvdb/etcd/stm.go Outdated

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, maybe it's simpler to read if we distinguish errors by scope. PTAL

Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

still not sure if this is correct. Say this tx reads and writes this key, increases c.readerMap[key] to 2.

That will leave (c.readerMap[key] > 0 && !keyRead) == false while it should be blocked.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That was a really nice catch!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

After some back and forth, decided to go with the simplified version above just with three loops.

@bhandras
bhandras requested a review from halseth September 16, 2020 12:41
@bhandras
bhandras force-pushed the etcd_tx_queue branch 2 times, most recently from aec925f to 8ad8dcd Compare September 16, 2020 13:26

@Roasbeef Roasbeef left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I found the latest iteration much easier to reason about this time around, kudos to the prior reviewers in this series!

LGTM 🚁

Should wait to merge this till we get 3/3 since the last iteration had a nice find.

Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated
Comment thread .github/workflows/main.yml Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍

@halseth halseth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM now, great work! 😀

Comment thread channeldb/kvdb/etcd/stm.go Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍

Comment thread channeldb/kvdb/etcd/commit_queue.go Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: optimization here and below, can immediately break loop if already blocked.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

This commit adds commitQueue which is a lightweight contention manager
for STM transactions. The queue attempts to queue up transactions that
conflict for sequential execution, while leaving all "unblocked"
transactons to run freely in parallel.
This commit integrates an externally passed commitQueue instance with
the STM to reduce retries for conflicting transactions.
@bhandras

Copy link
Copy Markdown
Collaborator Author

Thanks everyone for the reviews!

@bhandras
bhandras merged commit 111db80 into lightningnetwork:master Sep 17, 2020
@bhandras
bhandras deleted the etcd_tx_queue branch September 12, 2023 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

database Related to the database/storage of LND etcd optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants