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

core/aggsigdb: implement in-memory AggSigDB #249

Merged
merged 10 commits into from
Mar 21, 2022
Merged

Conversation

leolara
Copy link
Contributor

@leolara leolara commented Mar 18, 2022

category: feature
ticket: #220

@leolara leolara requested a review from corverroos March 18, 2022 09:15
@corverroos corverroos changed the title core/assisigdb: implementation of core.AggSigDB with a basic in-memory database core/aggsigdb: implement in-memory AggSigDB Mar 18, 2022
@codecov
Copy link

codecov bot commented Mar 18, 2022

Codecov Report

Merging #249 (083f303) into main (4579206) will increase coverage by 0.49%.
The diff coverage is 84.37%.

@@            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     
Impacted Files Coverage Δ
core/aggsigdb/memory.go 84.04% <84.04%> (ø)
core/types.go 4.25% <100.00%> (+4.25%) ⬆️
tbls/tblsconv/tblsconv.go 46.75% <0.00%> (-5.15%) ⬇️
app/app.go 62.45% <0.00%> (-0.71%) ⬇️
app/disk.go 57.14% <0.00%> (ø)
cmd/gen_simnet.go 50.70% <0.00%> (ø)
core/sigagg/sigagg.go 52.63% <0.00%> (ø)
core/validatorapi/validatorapi.go 41.26% <0.00%> (ø)
testutil/keystore/keystore.go 50.68% <0.00%> (ø)
core/aggsigdb/stub.go 0.00% <0.00%> (ø)
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4579206...083f303. Read the comment docs.

blockedQueries: []readQuery{},
}

go db.loop()
Copy link
Contributor

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 the lifecycle.Manager
  • Or even better, follow the reference in DutyDB that doesn't require any additional goroutines.

Comment on lines 40 to 54
db.commands <- writeCommand{
duty: duty,
pubKey: pubKey,
data: data,
}
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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.


// 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)
Copy link
Contributor

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

}

// memDB is a basic memory implementation of core.AggSigDB.
type memDB struct {
Copy link
Contributor

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

func (db *memDB) execQuery(query readQuery) {
data, ok := db.data[db.getKey(query.duty, query.pubKey)]
if ok {
query.response <- data
Copy link
Contributor

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...

Copy link
Contributor Author

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 :-(

// and removed from blockedQueries.
func (db *memDB) processBlockedQueries() {
queries := db.blockedQueries
db.blockedQueries = []readQuery{}
Copy link
Contributor

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.


// memDB is a basic memory implementation of core.AggSigDB.
type memDB struct {
data map[string]core.AggSignedData
Copy link
Contributor

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.

Comment on lines 129 to 131
duty core.Duty
pubKey core.PubKey
data core.AggSignedData
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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{}
Copy link
Contributor

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

// 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 {
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

Choose a reason for hiding this comment

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

oh yeah sorry

@leolara
Copy link
Contributor Author

leolara commented Mar 18, 2022

@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.

@leolara leolara force-pushed the leolara/asssigdb-memdb-basic branch from f1ddd5d to 3b08432 Compare March 18, 2022 14:36
@corverroos
Copy link
Contributor

corverroos commented Mar 18, 2022

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.

Copy link
Contributor

@corverroos corverroos left a comment

Choose a reason for hiding this comment

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

LGTM

response: response,
}

return <-response
Copy link
Contributor

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

}

// loop over commands and queries to serialise them.
func (db *memDB) loop() {
Copy link
Contributor

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 {
Copy link
Contributor

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

// and removed from blockedQueries.
func (db *memDB) processBlockedQueries() {
queries := db.blockedQueries
db.blockedQueries = []readQuery{}
Copy link
Contributor

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

@leolara leolara merged commit e8eb600 into main Mar 21, 2022
@leolara leolara deleted the leolara/asssigdb-memdb-basic branch March 21, 2022 10:31
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.

2 participants