-
Notifications
You must be signed in to change notification settings - Fork 215
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
[Merged by Bors] - hare active set: create bootstrap data updater #4169
[Merged by Bors] - hare active set: create bootstrap data updater #4169
Conversation
Codecov Report
@@ Coverage Diff @@
## develop #4169 +/- ##
=========================================
- Coverage 76.9% 76.9% -0.1%
=========================================
Files 236 238 +2
Lines 24660 24940 +280
=========================================
+ Hits 18985 19179 +194
- Misses 4482 4542 +60
- Partials 1193 1219 +26
... and 5 files with indirect coverage changes Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
Hey @countvonzero it would be useful for me if there was some package doc explaining what the purpose of the bootstrap package is, how its expected to be used and how it functions at a high level. |
bootstrap/interface.go
Outdated
//go:generate mockgen -package=bootstrap -write_package_comment=false -destination=./mocks.go -source=./interface.go | ||
|
||
type Receiver interface { | ||
OnBoostrapUpdate(*VerifiedUpdate) |
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.
my preference is to avoid dependency injection. this code can be pushed outside by providing a way to subscribe for updates, and then add a goroutine that listens for updates and integrates with go-spacemesh
bootstrap := ...
bootstrap.Listen()
for update := bootstrap.Updates() {
oracle.UpdateActiveSet(update.Epoch, update.ActiveSet)
beacon.UpdateBeacon(update.Epoch, update.Beacon)
}
this is sometimes called dependency rejection (https://blog.ploeh.dk/2017/02/02/dependency-rejection/)
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.
@dshulyak I have my own thoughts on this but could you elaborate on what you see as the concrete benefits of the approach you proposed? ( I read the article but I'm not sure it maps very well to our codebase and the current situation)
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.
@dshulyak
i had two considerations for the current design decisions
-
keeping the code as simple as possible. currently the code doesn't require locking (single-threaded). if allowing to subscribe outside, then it requires locking. and i decide to keep it simple by passing fixed subscribers in the init flow
-
in your model, which i did consider doing,
bootstrap := ...
bootstrap.Listen()
for update := bootstrap.Updates() {
oracle.UpdateActiveSet(update.Epoch, update.ActiveSet)
beacon.UpdateBeacon(update.Epoch, update.Beacon)
}
the activeset data is potentially big, and since this data isn't immediately used to oracle or beacon because both components need to wait for X period of time to fall back to the value provided in the update, i don't want to keep multiple copies around in memory. in my prototype, beacon and oracle will each keep a reference to the latest update (the same copy as the updater) and use it as it needs.
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.
if allowing to subscribe outside, then it requires locking. and i decide to keep it simple by passing fixed subscribers in the init flow
by doing dependency injection this way you are multiplying complexity. nested code path is always more complex than flat code path. what i shared is flat.
i know that this is a very simple app, but just consider what should be done to understand how active set is updated. in my example i can tell immediately how it is updated. in your - i will have to find how this bootstrap is initialized and then how does it call this thing.
also you will need to have oracle.UpdateActiveSet (or alternative method) to handle changes in the consensus in a good way
the activeset data is potentially big, and since this data isn't immediately used to oracle or beacon because both components need to wait for X period of time to fall back to the value provided in the update, i don't want to keep multiple copies around in memory
i think there is some confusion about how many pointers needs to be around for long time. what i shared doesn't force runtime to keep multiple copies of data for long time, or copy any data... also memory concern is in general doesn't seem very relevant if you consider how this data compares to other 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.
nested code path is always more complex than flat code path. what i shared is flat.
this i agree with.
i think there is some confusion about how many pointers needs to be around for long time.
yes. i realized this later as well. oracle will keep the ptr to the backing array of the atx ids, which is the same ptr updater will hold onto until the next update.
ok. i'll change to the approach you described.
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.
ok. i'll change to the approach you described.
btw, i wasn't trying to force a change, if there is no time and/or what you have is enough for it to work, it is certainly ok for me
return 0 | ||
} | ||
|
||
func (u *Updater) DoIt(ctx context.Context) error { |
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 this be exported (also Load
)? From what I understand about the updater it seems like the public API should be:
Start
Subscribe
Close
I see it's used in the test but the test could be moved to this 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.
it's a deliberate act on my part. in my view it is more important to put tests in XXX_test pkg here.
there are so many places in go-spacemesh where tests access private members directly and cause race issues.
Load
and DoIt
makes testing easier. i'll keep this in mind in my next iteration (there are prolly 2 more PRs) and see how i can reduce the exported methods.
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 prefer keeping the public API clean.
I don't think putting the tests in a different package is helping if we then export all the private things needed for the test. In this case it looks like the exported methods could cause race issues if called concurrently, so they're exhibiting the very problem that this approach was trying to avoid.
In my opinion the way to deal with race issues in tests is to structure the tests correctly, and that requires an understanding of the underlying code, and that becomes harder if the public API is not clear about what should and should not be called.
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.
yes your preference is clear to me.
if i were developing low level libraries, i'd agree with you. and even there, you will get disagreement across the team.
my consideration is that we are trying to hit genesis testnet next week. so there is time crunch. i don't feel spending more time perfecting this PR is worthwhile.
u.mu.Lock() | ||
defer u.mu.Unlock() |
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 don't think we need to lock here. u.latest
seems like its only accessed by the single internal go routine. It looks like the lock is only required to lock overs subscribers, if so I think calling it subscribersMu
would help with readability.
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 don't think one should conditionally lock mutable data.
the only data i didn't protect are those that are assigned in New
and never get changed in the object's life cycle.
this module is not performance critical. for those that are changed outside of New
, i think it's safer/future-proof to protect always.
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.
Well, we disagree on this point :)
I find locking of objects that do not need to be synchronised makes the code much harder to understand, since it implies to me that those objects are going to be accessed by multiple go-routines.
I'm also not convinced that it makes the code any safer since adding locked sections also raises the risk of deadlocks.
Also fine grained locking can lead to bugs such as the race conditions in the broker that would have been much easier to track down if the parts had not been locked, since they would have triggered a data race. Having the fine grained locking technically made them safe from the point of view of the go race detector, but actually that hid the real race conditions.
I think if we actually followed the approach of locking over all mutable fields the code-base would become exceedingly complex.
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.
ok. we disagreed.
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.
Lol :D
u.mu.Lock() | ||
defer u.mu.Unlock() | ||
ch := make(chan *VerifiedUpdate, 10) | ||
u.subscribers = append(u.subscribers, ch) |
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.
Do we need to push the latest update to the newly subscribed channel?
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 a design decision. there is no need for now. but i'll keep that in mind in the upcoming iterations.
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.
Is there some doc explaining the design decision?
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.
nope. if in my follow up PRs it looks problematic to you, then i can change then.
bors merge |
## Motivation part of #4089 ## Changes bootstrap data updater that - load the latest bootstrap data from disk at instantiation time - after app started, periodically query an URL for json update - validate data against json schema - validate data with app logic that cannot be enforced by schema - persist the latest update on disk and prune old ones - notify subscribers of a new update note: - test with a memory filesystem from github.com/spf13/afero
Pull request successfully merged into develop. Build succeeded:
|
Motivation
part of #4089
Changes
bootstrap data updater that
note: