-
Notifications
You must be signed in to change notification settings - Fork 83
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
core/aggsigdb: implement in-memory AggSigDB #249
Conversation
Codecov Report
@@ Coverage Diff @@
## main #249 +/- ##
==========================================
+ Coverage 55.29% 55.78% +0.49%
==========================================
Files 50 53 +3
Lines 3541 3671 +130
==========================================
+ Hits 1958 2048 +90
- Misses 1314 1345 +31
- Partials 269 278 +9
Continue to review full report at Codecov.
|
core/aggsigdb/memory.go
Outdated
blockedQueries: []readQuery{}, | ||
} | ||
|
||
go db.loop() |
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.
I'm not of fan of using hidden goroutines that are never stopped. I would say we have two options:
- Add an explicit
Run(context.Context)
method that we can wire into thelifecycle.Manager
- Or even better, follow the reference in DutyDB that doesn't require any additional goroutines.
core/aggsigdb/memory.go
Outdated
db.commands <- writeCommand{ | ||
duty: duty, | ||
pubKey: pubKey, | ||
data: data, | ||
} |
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.
the problem with channels are that they give the illusion of safety. The store might actually fail since there is a duplicate key and mismatching value, but you cannot return that error to the caller, who should know about this, since the write didn't succeed.
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.
I would just need to add a response channel with error that could also return nil
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.
yeah that is also possible.
core/aggsigdb/memory.go
Outdated
|
||
// Await implements core.AggSigDB, see its godoc. | ||
func (db *memDB) Await(ctx context.Context, duty core.Duty, pubKey core.PubKey) (core.AggSignedData, error) { | ||
response := make(chan core.AggSignedData) |
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.
As in the reference, you need a channel size of 1
core/aggsigdb/memory.go
Outdated
} | ||
|
||
// memDB is a basic memory implementation of core.AggSigDB. | ||
type memDB struct { |
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.
I prefer that structs are define above their methods
core/aggsigdb/memory.go
Outdated
func (db *memDB) execQuery(query readQuery) { | ||
data, ok := db.data[db.getKey(query.duty, query.pubKey)] | ||
if ok { | ||
query.response <- data |
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.
this will block forever if the caller already timed out...
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.
I feel very bad I didn't catch this :-(
core/aggsigdb/memory.go
Outdated
// and removed from blockedQueries. | ||
func (db *memDB) processBlockedQueries() { | ||
queries := db.blockedQueries | ||
db.blockedQueries = []readQuery{} |
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.
I'm not a fan of having two workflows (best effort execQuery and then later blockedQueries), simplifying it to a single workflow reduces complexity.
Also you are modifying the blockedQueries array in different places which feel confusing.
core/aggsigdb/memory.go
Outdated
|
||
// memDB is a basic memory implementation of core.AggSigDB. | ||
type memDB struct { | ||
data map[string]core.AggSignedData |
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.
suggest using struct as key, that is more explicit.
core/aggsigdb/memory.go
Outdated
duty core.Duty | ||
pubKey core.PubKey | ||
data core.AggSignedData |
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.
nit: export struct fields if it is a pure value struct, since other things are accessing the fields. Unexported fields should in general only be used by the thing itself.
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.
I am not sure what you mean. Visibility is at package level and these do not need to be visible outside the package
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.
Indeed, visibility doesn't change. So technically one could either define exported field or unexported fields. But there is still implicit meaning attached to "Upper" fields vs "lower" fields. I always see "Upper" fields as, anyone can use this. While "lower" fields means, only this thing should use.
In the end it is subjective, but I would prefer if we pure struct values have "Upper" fields, since that conveys the meaning that this is a pure value and anyone can access the fields.
Signature: []byte("test signature"), | ||
} | ||
|
||
wg := sync.WaitGroup{} |
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.
nit: prefer simple variable declaration for zero values: var sg sync.WaitGroup
https://dave.cheney.net/practical-go/presentations/gophercon-singapore-2019.html#_use_a_consistent_declaration_style
core/aggsigdb/memory.go
Outdated
// if it is not present it will store the query in blockedQueries. | ||
func (db *memDB) execQuery(query readQuery) { | ||
data, ok := db.data[db.getKey(query.duty, query.pubKey)] | ||
if ok { |
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.
need to error if the data already exists AND is different. Idempotent inserts are supported, but we should error on mismatching data.
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.
This is the read but I understand what you mean, but for execCommand I think
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.
oh yeah sorry
@corverroos I think your comments are correct. But I feel you would prefer to do it with mutexes? I think with channels are more idiomatic, more functional style and feel more like immutable programming,. Mutexes feel more imperative. Of course we can, and perhaps we should in this PR, provide a way to stop the main loop. |
f1ddd5d
to
3b08432
Compare
Yeah, the choice is basically channels vs mutexes. In this case, we are mutating in-memory state, so we can't get away from some form of mutability unfortunately. I chose mutexes since I feel it is a bit simpler, and doesn't require an additional goroutine, but I do not think it matters either way in the end.|| You can stick with channels if you want. |
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.
LGTM
core/aggsigdb/memory.go
Outdated
response: response, | ||
} | ||
|
||
return <-response |
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.
should probably select on ctx.Done here, even though it should not really happen in practice
core/aggsigdb/memory.go
Outdated
} | ||
|
||
// loop over commands and queries to serialise them. | ||
func (db *memDB) loop() { |
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.
Change this to Run(ctx)
and then we can register this with lifecycle manager and it will stop on shutdown.
@@ -163,3 +164,7 @@ type AggSignedData struct { | |||
// Signature is the result of tbls aggregation and is inserted into the data. | |||
Signature []byte | |||
} | |||
|
|||
func (a AggSignedData) Equal(b AggSignedData) bool { |
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.
please add a simple test for this, our test coverage is creeping down
core/aggsigdb/memory.go
Outdated
// and removed from blockedQueries. | ||
func (db *memDB) processBlockedQueries() { | ||
queries := db.blockedQueries | ||
db.blockedQueries = []readQuery{} |
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.
nit: reset by setting to nil
, it is slightly more readable
category: feature
ticket: #220