Implement AddSequencedLeaves in MySQL storage#1036
Conversation
storage/mysql/log_storage.go
Outdated
| } | ||
|
|
||
| func (m *mySQLLogStorage) beginInternal(ctx context.Context, tree *trillian.Tree) (storage.LogTreeTX, error) { | ||
| func (m *mySQLLogStorage) beginInternal(ctx context.Context, tree *trillian.Tree) (*logTreeTX, error) { |
There was a problem hiding this comment.
Not sure why this changed.
There was a problem hiding this comment.
AddSequencedLeaves below needs to access logTreeTX internals, in particular tx. This is because I could not use LogTreeTX.UpdateSequencedLeaves as it does not update LeafData.
I think what I could do is shifting this implementation a bit deeper to a dedicated storage.LogTreeTX.AddSequencedLeaves method. WDYT?
storage/mysql/log_storage.go
Outdated
| return nil, err | ||
| } | ||
|
|
||
| ok := status.New(codes.OK, "OK").Proto() |
There was a problem hiding this comment.
Not sure these really help that much?
There was a problem hiding this comment.
Indeed, the 2 vars below are better looking if inlined. Not sure about ok, seems like a good optimization for the quick-path case when most/all returned statuses are OK: instead of allocating new proto for the status each time we reuse this one everywhere.
| if err != nil && err != storage.ErrTreeNeedsInit { | ||
| return nil, err | ||
| } | ||
| return tx.(storage.ReadOnlyLogTreeTX), err |
There was a problem hiding this comment.
Does removing this cast change any behaviour such as losing the readonly?
There was a problem hiding this comment.
This line did not compile as it was because tx had become a *logTreeTX. The returned value of this function is still the readonly interface, so I suppose nothing changes on the caller's side?
There was a problem hiding this comment.
Now tx is again storage.LogTreeTX, but I think casting is unnecessary.
Codecov Report
@@ Coverage Diff @@
## master #1036 +/- ##
==========================================
- Coverage 62.6% 62.32% -0.28%
==========================================
Files 103 103
Lines 8455 8513 +58
==========================================
+ Hits 5293 5306 +13
- Misses 2632 2667 +35
- Partials 530 540 +10
Continue to review full report at Codecov.
|
+ refactor + dummy implementations in other storages + go generate
storage/mysql/log_storage.go
Outdated
| res := make([]*trillian.QueuedLogLeaf, len(leaves)) | ||
| ok := status.New(codes.OK, "OK").Proto() | ||
|
|
||
| // Note: Leaves are sorted by LeafIndex, so no reordering is necessary. |
There was a problem hiding this comment.
selfnit: ... no deterministic reordering is necessary.
|
@AlCutter @daviddrysdale Martin is OOO, could you please take a look? |
storage/mysql/log_storage.go
Outdated
| @@ -259,15 +261,26 @@ func (m *mySQLLogStorage) ReadWriteTransaction(ctx context.Context, tree *trilli | |||
| } | |||
|
|
|||
| func (m *mySQLLogStorage) AddSequencedLeaves(ctx context.Context, tree *trillian.Tree, leaves []*trillian.LogLeaf) ([]*trillian.QueuedLogLeaf, error) { | |||
There was a problem hiding this comment.
Is this func (on LogStorage impl) needed, or is this a LogTreeTX thing?
There was a problem hiding this comment.
It is both, similarly to QueueLeaves. Currently both are transactional, but we discussed earlier that we wanted to allow the LogStorage ones treat entries independently. WDYT?
storage/mysql/log_storage.go
Outdated
| // TODO(pavelkalinnikov): Measure latencies. | ||
| _, err := t.tx.ExecContext(ctx, insertLeafDataSQL, | ||
| t.treeID, leaf.LeafIdentityHash, leaf.LeafValue, leaf.ExtraData, 0) | ||
| // Note: QueueTimestamp == 0 because the entry bypasses the queue. |
There was a problem hiding this comment.
Will that mess with the integration latency metrics? (The signer sets integration_timestamp and computes the difference for each leaf, which it then add to a histogram - might be useful for seeing how the mirroring etc. is going on fetching leaves vs. integrating them?)
There was a problem hiding this comment.
Good catch. Are you suggesting to overload QueueTimestamp so that for PREORDERED_LOG it really means AddTimestamp?
We could add another field like SequencingTimestamp (which would make sense for regular LOG mode as well), or just rename this one to a more generic AddTimestamp. WDYT?
There was a problem hiding this comment.
Also, I think we should split the metric in two: one for the LOG's merge delay, another for PREORDERED_LOG's integration delay.
There was a problem hiding this comment.
Sorry, I think splitting the metrics is a good idea.
I guess I was suggesting overloading the QueueTimestamp, it's still kinda true in that it's queued to be properly integrated into the tree right?
There was a problem hiding this comment.
Done timestamp saving, TODOed the new metric bit.
Yeah, kinda, but not queued in the sense we are used to, rather added/stored. Leaving it as is for now though.
storage/mysql/log_storage.go
Outdated
| return nil, err | ||
| } | ||
|
|
||
| // Note: If LeafIdentityHash collides, we still store the indexed entry. |
There was a problem hiding this comment.
@AlCutter Since we decided to postpone fixing the duplicates issue, I think it's safer to not silently store the erroneous data here, but rather return an error. See the updated code.
storage/mysql/log_storage.go
Outdated
| res := make([]*trillian.QueuedLogLeaf, len(leaves)) | ||
| ok := status.New(codes.OK, "OK").Proto() | ||
|
|
||
| // Note: Leaves are sorted by LeafIndex, so no reordering is necessary. |
storage/mysql/log_storage.go
Outdated
| // TODO(pavelkalinnikov): Measure latencies. | ||
| _, err := t.tx.ExecContext(ctx, insertLeafDataSQL, | ||
| t.treeID, leaf.LeafIdentityHash, leaf.LeafValue, leaf.ExtraData, 0) | ||
| // Note: QueueTimestamp == 0 because the entry bypasses the queue. |
There was a problem hiding this comment.
Good catch. Are you suggesting to overload QueueTimestamp so that for PREORDERED_LOG it really means AddTimestamp?
We could add another field like SequencingTimestamp (which would make sense for regular LOG mode as well), or just rename this one to a more generic AddTimestamp. WDYT?
| t.treeID, leaf.LeafIdentityHash, leaf.MerkleLeafHash, leaf.LeafIndex, 0) | ||
| // TODO(pavelkalinnikov): Update IntegrateTimestamp on integrating the leaf. | ||
|
|
||
| if isDuplicateErr(err) { |
There was a problem hiding this comment.
Started manually testing, found a bug. Suppose we are trying to erroneously insert a new unique identity to an occupied leaf index. The INSERT above stores the identity, but the second INSERT fails because the index is occupied. Still, the tx gets committed, and the side-effect remains. Now this identity can't be inserted anymore.
I think I should add a test for this, and couple of simpler scenarios.
There was a problem hiding this comment.
Wow, this actually complicates things more than I thought it would. Essentially, we need to do 2 INSERTs, each can fail independently of the other (it can be conflicting identity and/or conflicting leaf index). Thus, if one fails, we should cancel the effect of the other to leave the db in the old state. We can do it by either making separate transactions, which is slow, or by manually deleting inserted keys which doesn't look nice.
@AlCutter @Martin2112 Any thoughts?
There was a problem hiding this comment.
The third option is reading before doing the inserts.
There was a problem hiding this comment.
Let's look at this again next week. Might be able to use savepoints for this at least in MySQL.
There was a problem hiding this comment.
Yes, this works. Will update the PR shortly (after #1061 is done as this one now depends on it).
|
@Martin2112 PTAL. |
|
Maybe alcutter@ knows but how will we implement this on CloudSpanner? I don't think it has an equivalent of savepoints. |
|
I think we can at least do a ReadWrite transaction that first tries to read the identity/entry and does the two inserts only if there is no conflict. Anyway, this would be in a separate PR. |
|
Yes I'm not suggesting we write the code now. Just checking we have a plan. |
storage/mysql/log_storage.go
Outdated
|
|
||
| // Leaves in this transaction are inserted in two tables. For each leaf, if | ||
| // one of the two inserts fails, we remove the side effect by rolling back to | ||
| // a savepoint installed before the first insert. |
There was a problem hiding this comment.
I thought the savepoint would be updated each pass through the loop so only the ones that fail are rolled back? Is that not how it's meant to work?
There was a problem hiding this comment.
OK I now realize we're still in an all or nothing situation. Ignore that comment.
There was a problem hiding this comment.
Oh, actually we are not in all-or-nothing. Your initial interpretation was right: only the failed-to-insert entries are rolled back. This is why I update the savepoint on each loop iteration below.
There was a problem hiding this comment.
OK. It's hard to read GitHub diffs. I'll have another look.
There was a problem hiding this comment.
The reason why I create this savepoint upfront is making sure the "RELEASE SAVEPOINT ..." after the loop has something to delete and doesn't return an error.
| glog.Errorf("Error adding savepoint: %s", err) | ||
| return nil, err | ||
| } | ||
| // TODO(pavelkalinnikov): Consider performance implication of executing this |
There was a problem hiding this comment.
I think the overhead of creating a savepoint is low but we should test this theory.
There was a problem hiding this comment.
Yes, will get to it later.
|
Yep. It's probably better to clean it up like this rather than wait for
commit or rollback.
…On 21 March 2018 at 12:11, Pavel Kalinnikov ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In storage/mysql/log_storage.go
<#1036 (comment)>:
> @@ -501,6 +513,78 @@ func (t *logTreeTX) QueueLeaves(ctx context.Context, leaves []*trillian.LogLeaf,
return existingLeaves, nil
}
+func (t *logTreeTX) AddSequencedLeaves(ctx context.Context, leaves []*trillian.LogLeaf) ([]*trillian.QueuedLogLeaf, error) {
+ res := make([]*trillian.QueuedLogLeaf, len(leaves))
+ ok := status.New(codes.OK, "OK").Proto()
+
+ // Leaves in this transaction are inserted in two tables. For each leaf, if
+ // one of the two inserts fails, we remove the side effect by rolling back to
+ // a savepoint installed before the first insert.
The reason why I create this savepoint upfront is making sure the "RELEASE
SAVEPOINT ..." after the loop has something to delete and doesn't return an
error.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1036 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AMv2T9P8PC0qMIVv09fdk5IjDdBXbysJks5tgkNZgaJpZM4SYluF>
.
|
|
I wonder why the 4th builder in Travis consistently complains about SQL syntax, while others don't. |
|
That's a different queue implementation for Lets Encrypt with a build flag
to enable it. It's probably a real error.
…On 21 March 2018 at 12:15, Pavel Kalinnikov ***@***.***> wrote:
I wonder why the 4th builder in Travis consistently complains about SQL
syntax, while others don't.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1036 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AMv2T2UVstc1uuyijrIBvXhx0yD0rI70ks5tgkR4gaJpZM4SYluF>
.
|
|
I've looked at it again and I think it's doing the right thing. Let's investigate the batched_queue error travis error now. |
|
I think the reason is that |
|
@Martin2112 I fixed the errors, PTAL. For the performance (with or without savepoints), I will probably add a Benchmark test in a follow-up PR, and play with it locally to see the difference. |
* master: storage/testdb: drop now-unused entrypoints (google#1067) Drop use of SQLite (google#1064) Implement AddSequencedLeaves in MySQL storage (google#1036)
This is a stub which does not yet fully implement the API contract, in particular it doesn't return duplicates if any.