-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Conversation
@tv42 I definitely like the idea of pushing it into Bolt core. It's such a common use case that it doesn't make sense to push it into another repo. We can't have it replace I like your implementation better since it does the retries instead of rolling back everything. I don't have time right this moment to give it enough testing but I'll test and merge it in within the next couple of days. |
Yeah, |
Rebased and edited the commit to become |
I would highly recommend this pull request include documentation additions to README.md with example usage, caveats, etc. |
Needs more test coverage too. That work is just not useful if the whole approach is unwanted. |
Excellent addition. I hope this can be merged into Bolt core and the documentation expanded as davecgh suggested. |
Sorry for the delay but I wanted to make sure I had a couple hours to dedicate to the PR and really understand it better. Here's some feedback after playing with it for a while:
Also, I refactored some of the tests a bit: |
I can't just let the panic bubble up like Update or View do, because the func is typically executed in a different goroutine than where the matching Batch call is. The error has to be shipped to the right caller (hence the channel return from merge), and the type Second, the point of re-panicking instead of just returning an error is to keep the panic, well, exceptional. It just felt like the right thing; don't swallow the panic because we actually have a caller that can deal with it (as opposed to e.g. a net/http Handler). Maybe the recover bit should store the stack trace, and store it in the error sent to the caller. runtime.Stack makes it easy. (All goroutines or just current goroutine?) This seems to be the right thing to do regardless of whether it's re-panicked or returned as an error. Let me know what you think.
I'm very much open to making the code better, but I wonder if that's achievable with e.g. a nicer description of the state machine.
I can't vouch for the test changes right now, but I appreciate the motivation; less indentation is good, I was copying your older programming style ;) |
The test changes look good. Hint for reviewing: make diff ignore whitespace changes. |
digraph batch_control_flow {
start [shape=point];
testCur [shape=diamond, label="is there a batch?"];
start -> testCur;
tryJoin [shape=diamond, label="try to join\ncurrent batch"];
testCur -> tryJoin [label="yes"];
waitForErr [label="wait for error"];
tryJoin -> waitForErr [label="joined,\nif full\nnotify master"];
returnOrPanic [label="return or panic", shape=doublecircle];
waitForErr -> returnOrPanic;
tryJoin -> tryMaster [label="join refused\n(already started)"];
tryMaster [shape=diamond, label="try to\nbecome\nmaster"];
testCur -> tryMaster [label="no"];
tryMaster:se -> testCur:n [label="lost race"];
waitFullOrDelay [label="wait for full or max delay"];
tryMaster:sw -> waitFullOrDelay [label="we are master"];
doTransactions [label="call db.Update, handle errors"];
waitFullOrDelay -> doTransactions [label="full or timer /\nmark as started"];
doTransactions -> waitForErr;
} |
29c0764
to
6931794
Compare
Alright for your reviewing pleasure, https://github.com/tv42/bolt/tree/batch-3 / 29c0764 contains a rewrite using just a mutex for coordination. It got really simple once I realized I can wire a timer to a sync.Once. For actual merging, I squashed the branch as 6931794. It could still use more docs and tests, but is the current approach more understandable? Performance is close enough to the atomics version, I'm seeing a lot of jitter so any real difference is probably drowned by the noise and would need several repeats to get anything conclusive; I'd rather get the code out there. |
Err I just saw a bug in handling batch full, consider the details to be in flux. Need those tests ;) |
And I should still rip out the checks & errors on bad MaxBatchSize, MaxBatchDelay values and just handle whatever comes; if the user can set the fields on DB directly, there's no validation possible at that time anyway, and they degrade nicely in a way that just disables batching. |
👍 and 😄 |
I'm rebasing the "batch" branch again, new commit is b906f4e. commit 63c0780
commit e698383
commit d90b13e
commit 0fabc63
|
@benbjohnson what do you think of b906f4e / 5261724? @davecgh See the README addition in 5261724, does this match what you wanted to see? If not, can you talk more about what you would need as a user. |
err := db.Batch(func(tx *bolt.Tx) error { | ||
// Find last key in bucket, decode as bigendian uint64, increment | ||
// by one, encode back to []byte, and add new key. | ||
... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent indentation here.
@tv42 That addition to the README looks pretty good. I think it might benefit from a couple of extra things:
|
@davecgh Thank you. The problem with actually showing concurrency in the README is that it just gets too verbose. That would have to be a separate example. Batch is definitely aimed at the case where the code is e.g. serving incoming network requests with multiple goroutines; anything else cannot fit into the same "returns error" calling pattern, and then you might as well use Update or manual transactions. 663b133 adds "concurrent" in key spots and explicitly says "Batch is only useful when there are multiple goroutines calling it" -- how's that? |
@tv42 Looks good to me with those additions. Agreed about the example directly in the README. I was thinking more along the lines of referencing an example that is done in in That also has the benefit of being a testable example to ensure the example code actually remains current. |
@davecgh I still have trouble coming up with a non-silly example that's brief enough. Usually concurrency is driven by communication with the outside world, but that's not something you can encapsulate in ExampleDB_Batch easily. |
Can't wait. Example looks clear enough. |
@tv42 Sorry for the delay in re-reviewing this. I'm going to block out some time tonight to go through this PR again. Thanks again for all the work on it. It'll be nice to have batching in Bolt core. |
Rebased and squashed with all of the above nits handled (as far as I can tell) as commit 1578db9. If there's any more feedback, please let me know. |
|
||
if failIdx >= 0 { | ||
// take the failing transaction out of the batch. it's | ||
// safe to shorten b.calls here because b.batch no longer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
b.batch
-> db.batch
@tv42 I like the changes a lot. I think it flows really well now and it's easy to read. I like the use of
|
I just realized the benchmarks are racy, they share hashing across goroutines. Fixing.. |
DB.Batch makes it easy to make lots of small transactions with significantly better performance. Batch combines multiple concurrent Update calls into a single disk transaction, managing errors smartly.
Somewhere along the line I screwed up the commit message, fixed in adbb1a1 |
Ping. |
@tv42 Sorry for the delay on this. I keep wanting to add some |
@benbjohnson Apart from deserving more testing coverage -- which is just about always true -- I feel like I have nothing more to add, and the comments from others have stopped too. |
Batcher makes it easy to make lots of small transactions with
significantly better performance. Batcher.Update combines multiple
Update calls into a single disk transaction, managing errors smartly.
I'm aware of https://github.com/boltdb/coalescer. I wrote the original version of this before you pushed coalescer, it just took a while for me to circle back to cleaning it up. This code doesn't leave a goroutine around, and is a few percent faster ;) But the real difference to coalescer is this: with this code, callers don't need to write a for loop around ErrRollback. I pull failing updaters out of the transaction, and retry the others automatically.
I'd really like to see this as part of bolt, and I feel like having a good batcher mechanism would help significantly with performance complaints like #244 . If this looks promising, it could even sit alongside DB.Update as DB.Batch -- or even become DB.Update (though beware the "runs multiple times" behavior difference).
With this in place, I see no reason why any of my apps would ever again bother doing non-batched transactions.
Let me know what you think. If you say no, this'll live as tv42/semiauto (as opposed to bolt action..)