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

[Merged by Bors] - hare active set: create bootstrap data updater #4169

Closed

Conversation

countvonzero
Copy link
Contributor

@countvonzero countvonzero commented Mar 20, 2023

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

bootstrap/updater.go Outdated Show resolved Hide resolved
@codecov-commenter
Copy link

codecov-commenter commented Mar 20, 2023

Codecov Report

Merging #4169 (9eea57b) into develop (947f91b) will decrease coverage by 0.1%.
The diff coverage is 69.1%.

@@            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     
Impacted Files Coverage Δ
bootstrap/types.go 0.0% <0.0%> (ø)
bootstrap/updater.go 71.5% <71.5%> (ø)
config/config.go 100.0% <100.0%> (+2.7%) ⬆️

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

bootstrap/mocks.go Outdated Show resolved Hide resolved
bootstrap/updater.go Outdated Show resolved Hide resolved
@piersy
Copy link
Contributor

piersy commented Mar 22, 2023

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/updater.go Outdated Show resolved Hide resolved
bootstrap/updater.go Outdated Show resolved Hide resolved
//go:generate mockgen -package=bootstrap -write_package_comment=false -destination=./mocks.go -source=./interface.go

type Receiver interface {
OnBoostrapUpdate(*VerifiedUpdate)
Copy link
Contributor

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/)

Copy link
Contributor

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)

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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

bootstrap/schema.json Outdated Show resolved Hide resolved
bootstrap/updater.go Outdated Show resolved Hide resolved
return 0
}

func (u *Updater) DoIt(ctx context.Context) error {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Comment on lines +179 to +180
u.mu.Lock()
defer u.mu.Unlock()
Copy link
Contributor

@piersy piersy Mar 27, 2023

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.

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

Copy link
Contributor

@piersy piersy Mar 27, 2023

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok. we disagreed.

Copy link
Contributor

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

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?

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 a design decision. there is no need for now. but i'll keep that in mind in the upcoming iterations.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

@countvonzero
Copy link
Contributor Author

bors merge

bors bot pushed a commit that referenced this pull request Mar 27, 2023
## 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
@bors
Copy link

bors bot commented Mar 27, 2023

Pull request successfully merged into develop.

Build succeeded:

@bors bors bot changed the title hare active set: create bootstrap data updater [Merged by Bors] - hare active set: create bootstrap data updater Mar 27, 2023
@bors bors bot closed this Mar 27, 2023
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.

4 participants