-
Notifications
You must be signed in to change notification settings - Fork 53
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
Internal API changes #1
base: master
Are you sure you want to change the base?
Conversation
rm duplicate mark check in tests
…me type assertions needed
I tried to update this with a change that I think had gotten us stuck earlier. Specifically, it sometimes makes sense for us to separate the logic of choosing the next host to request, and maintaining the state around that choice. This is because extensions of the HostPool (such as epsilon greedy or anything else a user might come up with) often want to be able to invoke (or override) only one of those functions, without knowing the details of how they're implemented. This led to me exposing those two as public methods on the HostPool interface. The intention was to communicate that they should be used when developing extensions to HostPool, but as a user the Unfortunately, in yet another head-on collision with the golang type system, it appears that the only way for this solution to proceed will be to refactor the public API from a @rymo4, it was your idea to try to pull this out of the grave, you got any ideas? @mreiferson, I know you're tired of writing javascript, you got any input? Does anyone think it's worthwhile to spend any more time on this? |
I'm not sure I understand what necessitated the breaking API change (although I admit the end result is a reasonable outcome)... I think the real question is what do we want this external API to support? If we're looking to provide end-users the ability to introduce custom selection mechanisms what about an external API like this: type HostPool interface {
Get() Response
}
type Response interface {
Host() string
Mark(error)
}
type Selector interface {
SelectNextHost() Response
MarkHost(string, error)
}
func NewHostPool(hosts []string, selector Selector) HostPool It would be used something like: hp := hostpool.New([]string{"a", "b", "c"}, &hostpool.EpsilonGreedySelector{})
r := hp.Get()
host := r.Host()
err := doWork(host)
r.Mark(err) It seems like the common subset of functionality is a list of hosts and where behavior differs is in the selection.. so I feel like the interfaces should reflect that, right? The nature of the proposed Thoughts? |
@mreiferson Nice, I think that the |
I do like the approach of separating the interfaces that the user interacts with from those that developers of extensions work with, and think I may take a stab at putting that together. One thing worth keeping in mind is that, when extending HostPools (or Selectors here), it's often helpful to embed some kind of Selector. For example, doing this means that the Epsilon Greedy hostpool doesn't need to keep track of dead hosts, because it can leverage the base hostpool to do that. It was this practice that forced me to change the external API a bit, basically because those embedded Hostpools don't behave like superclasses in other languages (duh). I think that, for the same reason the package-level function got me out of that jam, separate Selector and HostPool interfaces won't suffer from my earlier problems. Now, the way I'm envisioning implementing this, the struct backing HostPool will be very very thin, not much more than what the package level functions would have provided. But I do think that having the HostPool interface exposed to the user is more natural, as well as being backwards compatible. |
I think the base functionality is so simple that I'm not convinced it's necessary to re-use just for the sake of DRY... I think it contributed to some of the original complexity. Also, I believe the difficulty with the proposed new API will be One way or another, to settle on an API we're happy with, I don't think we're going to be able to avoid any breaking API changes, so I wouldn't worry too much about it. I'm excited to see how it comes out... |
I think you've been incepted 😜 |
} | ||
h.epsilonCounts[h.epsilonIndex]++ | ||
h.epsilonValues[h.epsilonIndex] += int64(duration.Seconds() * 1000) | ||
} | ||
|
||
func (s *epsilonGreedySelector) MakeHostResponse(host string) HostPoolResponse { |
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 sure there's any value in maintaining a separate MakeHostResponse
, having the function is fine, but making it part of the interface is unnecessary and can be hidden as an implementation detail
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 agree on this point. I really want it to be possible to create a HostPool with multiple Selectors composed together. To do that, I don't think we can rely on hidden implementation details of the Selectors (among other things, extensions shouldn't need to be in the hostpool package). I haven't quite gotten to that point in this pull req, due to some stuff with the implementation of hostEntry
that I've been deliberately leaving to a subsequent pull req to keep things from getting too complicated.
MakeHostResponse
does the work necessary to update state based on the chosen host, I do think it's a necessary inclusion in the interface. Note here that we call out to the embedded Selector's MakeHostResponse
, without privileged access to its implementation. That's the pattern I'd like to encourage for other Selectors going forward
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 could have been more specific. I wasn't suggesting that you rely on or even have access to internals...
It seems like SelectNextHost
is the only public interface that a Selector
needs to implement, it just needs to be changed to return a HostPoolResponse
interface... that method's internal implementation doesn't seem relevant.
But, I'm not quite sure where you're going with selector composition, so please elaborate if I'm just missing something.
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.
So, that definitely sounds more straightforward, and is closer to what I originally wanted to do. The problem I was running into was that we typically want to update some state when we select which host to try. This may be on the HostPool/Selector side (state around retrying a host previously marked dead) or on the HostPoolResponse side (storing the time that the request was initiated for timing purposes). MakeHostResponse
was a poorly named way to shoehorn all those state updates together.
All of this is further muddied by the fact that I'm still just working with one hostEntry
implementation. I'll try to continue thinking about this on a higher level in terms of what state needs to kept and when it needs to be accessed, maybe I'll come up with something simpler.
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 on the same page w/ different selectors needing to maintain separate state, I just don't think that requires two public interface methods, right?
I agree with your assessment that hostEntry
has too much responsibility and should be specific to each selector, ideally.
Another cleanup item... we can easily remove the embedded ✂️ 🚧 |
@@ -42,86 +46,84 @@ type epsilonGreedyHostPool struct { | |||
// To compute the weighting scores, we perform a weighted average of recent response times, over the course of | |||
// `decayDuration`. decayDuration may be set to 0 to use the default value of 5 minutes | |||
// We then use the supplied EpsilonValueCalculator to calculate a score from that weighted average response time. | |||
func NewEpsilonGreedy(hosts []string, decayDuration time.Duration, calc EpsilonValueCalculator) HostPool { | |||
func NewEpsilonGreedy(decayDuration time.Duration, calc EpsilonValueCalculator) Selector { |
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.
s/NewEpsilonGreedy/NewEpsilonGreedySelector
Allow hosts for an existing hostpool to be changed
Just wanted to put some work I've done on the internal api changes up for folks to look at. The main goal of all this stuff is to increase the separation between the epsilon-greedy stuff and the standard hostpool so they can be composed as desired. I'll try to summarize the main changes:
epsilonGreedyHostPoolResponse
keeps a pointer to aepsilonGreedyHostPool
struct, not the interface. Internally it embeds aHostPoolResponse
- the interface, not the basic struct impl.epsilonGreedyHostPool
embeds theHostPool
interface, not the basic struct implHostPoolResponse
impls no longer have async.Once
. I don't think there should be any expectation that they be threadsafeHostPool
interface now has a privateselectHost
method to produce aHostPoolResponse
from a host string. This also takes care of any state changes involved in selecting that host, such as dealing with retry time and starting a time for epsilonGreedy operationgetRoundRobin
andgetEpsilonGreedy
just select a hostname. TheselectHost
method takes care of other state changes involved in delivering theHostResponse
to the user.I have some more changes in mind that I would eventually like to make in the service of the separation I mentioned, but I think that this is a good place to start. The tests pass, though I did have to change them slightly: I have to make a couple of type assertions, and I had to remove one line that tested the
sync.Once
functionality of theHostPoolResponse
s.cc @jehiah @mreiferson