-
Notifications
You must be signed in to change notification settings - Fork 372
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
Disassemble the Component interface #1844
Comments
Sounds good to me in general. More fine grained interfaces will make component implementations much tolerable and removes the guess work if something needs to be implemented or not. 👍 The only part that I'm not really sure is the using of error channel as return value on |
Good question. The current implementation for Lines 100 to 121 in 63f4bd1
So it stops the components sequentially in reverse order, logging any errors. The "done" channel returned from We could completely remove the error (i.e. return I'd rather not do this. I think that "error passing" is a solid, future-proof strategy for functions that aren't infallible. "You don't know what to do with this failure? Let your caller handle it!". Whenever we actually need some special handling in the future, we'd need to refactor the Component interface again, touching all of its implementors. It might already be useful in unit tests, to check if the component stops correctly or not. I'd change the manager's implementation to not log anything, but return some composite error, as we do for errors during reconciliation. Then we can still log it when stopping the manager (and I'd consider to terminate the k0s command with a non-zero exit code in case it failed to shutdown cleanly). |
Well, I would rather think about this from the lifecycle manager perspective. It has to do a few things:
Renaming And we can't get rid of the
Reconciling is a completely separate process and it's not coupled with the Component interface at all. So, we can move the |
The manager needs to have control over the stopping behavior, true. The proposal changes the way it's done from two distinct I see the following advantages of the single start method approach over the double start/stop method approach:
|
Yes, it will have the same behavior as we have now, the only difference is that we'll need to close the "done" channel instead of explicitly calling the
over some kind of this:
Context has nothing to do with stopping a component. And shouldn't, since it's just a context. Even context cancellation is just an agreement, not a contract. Components don't have to respect the context at all (and most of them don't), but they have to respect the contract, ie interface. The difference between the cancellation of the context and calling the Stop method is that
I don't think it's a problem. It's quite ok if some components don't need some function or another. More importantly, we have a simple and clear contract, which is easy to use and implement. The same I can say about the |
That blog post covers widely perceived weaknesses about Go's The Start/Stop and Start-only approaches are convertible into each other, while I think that the Start-only approach caters to more of k0s's components as the other one. There's quite some state that can be removed from the component structs, invalid states and concurrency issues are less likely to occur. Whichever approach we choose, there should be some auxiliary structs/functions added to the codebase which help in implementing the interface, i.e. to convert between the synchronous style and the asynchronous style, depending on what the interface prescribes and the component needs. Come to think of it, there could even be a wrapper struct, so components could choose to implement the sync or async version, and
There are quite some components in the k0s codebase which actually use the context in the proposed way already, having a no-op
Explicitness is okay, but nevertheless it didn't help the components to have correct implementations for Concerning the complexity of the manager: I'd happily move complexity out of the components into the manager, since we have only one manager, but dozens of components. So we just need to get it right once, not dozens of times. To conclude: I'm okay with both approaches, having a preference for the Start-only one. Whichever the consensus is, we should adapt the component implementations to better adhere to the Here's a summary:
Components that are using Some more elaborate components not fitting directly into one category: All the other components don't need to be stopped, i.e. they have a no-op |
I think it is outdated now? |
Is your feature request related to a problem? Please describe.
The
component.Component
interface is the backbone of k0s's internal structure:k0s/pkg/component/component.go
Lines 25 to 30 in 71a2c70
It serves as a lifecycle abstraction for all the different things in k0s. However, its contract is not very well defined, which makes it hard to judge what to do when implementing it.
There has been some effort in documenting the lifecycle in Add docs to Component interfaces #1657, but it's still not clear enough. Are components expected to be concurrency safe, or is it the responsibility of its users to ensure that access to the lifecycle methods is synchronized?
Init
,Stop
orHealthy
implementations.Some even don't need a
Run
method, since they only act onReconcile
. Especially theHealthy
method is only used in theManager
when starting components.Init
/Run
/Stop
behavior.Currently, both the
Init
and theRun
method take a context parameter. It is not entirely clear how those contexts should be used, given that there's also theStop
method. Are components expected to stop by themselves if any of those contexts is cancelled (a.k.a the "Merge" problem)? If yes, why does theStop
method exist?Describe the solution you would like
The key purpose of the
Component
interface is to drive the lifecycle of multiple components via theManager
. It is responsible to call the "main" lifecycle methodsInit
,Run
,Stop
andHealthy
. It does it in a sequential manner w.r.t. each individual component. For those methods, it seems reasonable to document that they don't need to be concurrency safe. Then there's theReconcile
method. It's special in a way that it's not really a lifecycle method, but a special method to be called "out of band" on each registered component. Some more complex components have even more public methods, all of them may be called at different times, potentially concurrently. Let's clearly document that: as long as a component only exposes the fundamental lifecycle methods, it doesn't need to care about concurrency. But as soon as it's doing more than that, it should.Instead of combining all of the methods into one interface, let's split them up into individual interfaces. There's already the specialized
ReconcilerComponent
interface for those components that require/support reconciliation. TheManager
simply uses type assertions to figure out if a component implements it or not. How about having anInitializable
andStoppable
interface for those components which support those lifecycle phases, and let theManager
use type assertions to figure it out. This would remove a lot of empty function implementations from the codebase, and would also obliterate the question of whether a component should actually fail ifStop
was called beforeRun
when it cannot be stopped anyway (same forRun
beforeInit
, whenInit
is a no-op). This approach is also extensible, when there are more lifecycle phases to be added in the future, there could be just a new interface for those. Not all existing components would need to implement it.Concerning the
Healthy
method: Given that only theManager
uses it when starting components, I would argue that we can remove it completely. Instead, for those components that have a reasonableHealthy
implementation, it should be embedded into theRun
method.Run
can then block until the component becomes healthy. The timeout loop implemented in theManager
'swaitForHealthy
method could be transformed into a context that gets cancelled ifRun
doesn't return in time.Let's define the context passed to
Init
as a short-lived one that may get cancelled afterInit
returns. TheRun
method would better be calledStart
, since it's actually starting something, not running it (i.e. it's not blocking until stopped). The stop method is currently being used to block theManager
until a component is actually stopped. It combines both the cancellation request and the blocking until cancelled into one method. How about using the context passed toStart
as the cancellation signal, and returning an error channel that may be used to block until the component has actually been stopped? This would require us to make theManager
implementation a bit more elaborate, but It would make implementing Components easier. I consider this a plus, since there's only oneManager
, but many components.Describe alternatives you've considered
No response
Additional context
No response
The text was updated successfully, but these errors were encountered: