-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
InitManager: TargetImpl::ready() behavior change #7498
Conversation
Signed-off-by: Yuchen Dai <silentdai@gmail.com>
Signed-off-by: Yuchen Dai <silentdai@gmail.com>
@mergeconflict would you be up for doing first pass, and tagging Harvey/Matt when you've LGTMd? |
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.
Looks like a good start. My only issue with this revision is that I don't want to change the contract between init manager and target, I want to keep the changes just in the target.
source/common/init/target_impl.cc
Outdated
@@ -27,19 +27,27 @@ TargetImpl::TargetImpl(absl::string_view name, InitializeFn fn) | |||
fn_(std::make_shared<InternalInitalizeFn>([this, fn](WatcherHandlePtr watcher_handle) { |
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 the key spot I think you need to modify. I think it needs to look something like:
if (is_ready_) {
// If we were already ready before being called by the init manager, just signal immediately...
watcher_handle->ready();
} else {
// ... Otherwise, hang onto the init manager's watcher handle and initialize.
watcher_handle_ = std::move(watcher_handle);
fn();
}
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.
Thanks! That is close to my plan B. Well, this is even better as I somehow considered is_ready_
should be shared pointer. Will use your clean code :)
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.
@mergeconflict It's arguable if the a ready target would invoke fn().
I prefer to keep the existing behavior that fn() would be invoked to . WDYT?
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 know, that seems a little sketchy to me, maybe... fn()
is the target's initialization function, and if the target is already ready, wouldn't that mean fn()
(or its equivalent) would have already been called before?
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.
For those user who call ready() only in fn(), both are fine.
For the users who may call ready() before fn(), the users will see fn() invoked prior to this PR. Since we don't have a strong opinion whether fn() should be called or not, let's not break the behavior that fn() is called in this situation.
For my use case, my fn in Target constructor is empty as the ready() depends on other signal. So I vote for NOT to assume ready() always comes from fn().
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.
Whoa... this doesn't make sense to me. If the initialization function for your target is empty, and your target is always ready immediately, it seems like you have no need for Init::Manager
at all.
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.
Sorry for the unclear.
The initialization function is empty
True
your target is always ready immediately
Not always. I don't want to introduce the complexity here so let me simplify the case I was seeing.
The listener could provide the server scope Init::Manager or per listener Init::Manager. The former one could be initialized really late but my target wants to be started as early as possible but it doesn't guarantee be ready before Init::Manager::initialize().
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.
Hmm, that's complicated... So, in this case, even though the init manager isn't telling the target when to initialize, the init manager still needs to know that the target has initialized, did I get that right?
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.
Exactly. Actually I think the existing CDS/LDS should follow this pattern as well: cluster should be a target of listener's init manager and cluster should be updated independently. However, CDS/LDS doesn't use this at the risk that throw the ordering to ADS or user.
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.
Beside the existing comment I will move the newly added test cases to target_test as it is implementation details of Target
source/common/init/target_impl.cc
Outdated
@@ -27,19 +27,27 @@ TargetImpl::TargetImpl(absl::string_view name, InitializeFn fn) | |||
fn_(std::make_shared<InternalInitalizeFn>([this, fn](WatcherHandlePtr watcher_handle) { |
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.
Thanks! That is close to my plan B. Well, this is even better as I somehow considered is_ready_
should be shared pointer. Will use your clean code :)
Signed-off-by: Yuchen Dai <silentdai@gmail.com>
@@ -49,18 +49,21 @@ TEST(InitManagerImplTest, AddReadyTarget) { | |||
|
|||
ExpectableWatcherImpl w; | |||
|
|||
t1.expectInitialize(); |
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.
@mergeconflict add this not only because it is easier to coordinate but also less astonishment.
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.
Reason:
AddTargetAndMarkReadyBeforeInitialization
does call expectInitialize
Prior to this PR this function is also called anyway.
source/common/init/target_impl.cc
Outdated
@@ -27,6 +27,10 @@ TargetImpl::TargetImpl(absl::string_view name, InitializeFn fn) | |||
fn_(std::make_shared<InternalInitalizeFn>([this, fn](WatcherHandlePtr watcher_handle) { | |||
watcher_handle_ = std::move(watcher_handle); | |||
fn(); | |||
// It is possible that fn() mutates the is_ready_. |
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 this logic is correct. The only way that the initialization fn()
can mutate is_ready_
is by calling ready()
. You don't want to call it twice.
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.
The fact is that TargetImpl::ready()
is idempotent. I see several places utilize this assumption.
Is the lambda abusing the fact? If so I can fix
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.
True, it's idempotent, but that's for the user's benefit. To me, it feels really weird to say:
if (is_ready_) {
ready();
}
It also feels wrong to call fn()
, and unnecessary to hang onto the watcher handle, if is_ready_
is true.
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.
fixed in the latest commit
Signed-off-by: Yuchen Dai <silentdai@gmail.com>
source/common/init/target_impl.cc
Outdated
watcher_handle_ = std::move(watcher_handle); | ||
fn(); | ||
if (is_ready_) { | ||
fn(); |
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 still don't really understand why the call to fn()
is needed in the case where the target is already initialized.
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.
IMHO it's not a must-have. I leave it in this way because that is what user will see.
Suppose we have the code and happens to be executed in below order:
Context A Context B Context C
ManagerImpl m;
TargetImpl t("foo", fn);
m.add(t);
...
X: t.ready();
Y: m.initialize(w);
Prior to this PR:
- Either t.ready() happens before m.initialize(w), or the reverse, we will observe that fn will be called.
- w will be triggered only if t.ready() happens after m.initialize(w)
This goal of this PR is to change item 2.
Our conflict is whether 1 should be maintained. I don't have a strong preference but I feel there is no gain to change 1.
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.
That's a really good explanation.
My assumption has always been that fn()
(or something called by it indirectly) calls t.ready()
. Or, in other words, if t.ready()
has been called, there's no need for fn()
to be called again. You've said that in your case, fn
is actually empty, and ready
is called by some other code, and I still sort of think that's an abuse of the init manager, but I guess we can ignore that for now :)
Here's my thinking: we can't assume that fn
is idempotent, and I think we should assume that if is_ready_
is true, that means initialization is done. Like, I think of it as really being is_initialized_
. So, to your point, it would be a change in behavior to not call fn
, but I think it's most consistent with the design intent of the init manager (that is, I think it's really fixing a latent bug).
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.
but I guess we can ignore that for now :)
I haven't been fully tracking this conversation, but I don't think we should ignore this for now. Can we step back and make sure we are actually solving the right problem? However we think it's best to do that is fine with me.
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.
we can't assume that fn is idempotent
I used to consider fn()
- a signal which is harmless to call more than once.
- may have side affect other than transit state from not-ready to ready.
It leads to my code that InitManager should call it at least once.
I start to understand your concern:
if is_ready
is true, it doesn't make sense that the init system calls fn().
Will update following your opinion
source/common/init/target_impl.h
Outdated
* any time. Calling it before initialization begins or after initialization has already ended | ||
* will have no effect. | ||
* any time. | ||
* Notes: Calling it before initialization begins is allowed. |
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 probably clarify what happens when you call ready()
before the init manager has called the target.
Signed-off-by: Yuchen Dai <silentdai@gmail.com>
Signed-off-by: Yuchen Dai <silentdai@gmail.com>
source/common/init/target_impl.h
Outdated
@@ -47,11 +47,32 @@ class TargetHandleImpl : public TargetHandle, Logger::Loggable<Logger::Id::init> | |||
const std::weak_ptr<InternalInitalizeFn> fn_; | |||
}; | |||
|
|||
class AbstractTarget : public Target, protected Logger::Loggable<Logger::Id::init> { |
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 think the Envoy naming convention for this would be TargetImplBase
.
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 see. Will fix
*/ | ||
bool ready() override; | ||
}; | ||
/** |
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.
Need blank line after TargetImpl
body, and need to update comment for EagerTargetImpl
.
source/common/init/target_impl.h
Outdated
* doesn't take a WatcherHandlePtr (like TargetFn does). Managing the watcher handle is done | ||
* internally to simplify usage. | ||
*/ | ||
EagerTargetImpl(absl::string_view name, InitializeFn fn); |
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 think the idea we talked about is that fn
isn't even needed here, since the target will initialize itself as soon as it can (eagerly), rather than being invoked by the manager.
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.
Sorry I didn't understand. You are right, fn doesn't needed in the unique case. Will remove.
source/common/init/target_impl.cc
Outdated
fn(); | ||
})) {} | ||
|
||
TargetImpl::~TargetImpl() { ENVOY_LOG(debug, "{} destroyed", name_); } |
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 think this and ~EagerTargetImpl
can be moved to the base class dtor.
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.
great catch! will fix
source/common/init/target_impl.cc
Outdated
|
||
EagerTargetImpl::~EagerTargetImpl() { ENVOY_LOG(debug, "{} destroyed", name_); } | ||
|
||
bool EagerTargetImpl::ready() { |
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.
Common code between here and TargetImpl::ready()
can be refactored.
source/common/init/target_impl.h
Outdated
@@ -47,11 +47,32 @@ class TargetHandleImpl : public TargetHandle, Logger::Loggable<Logger::Id::init> | |||
const std::weak_ptr<InternalInitalizeFn> fn_; | |||
}; | |||
|
|||
class AbstractTarget : public Target, protected Logger::Loggable<Logger::Id::init> { | |||
public: | |||
AbstractTarget(absl::string_view name, std::shared_ptr<InternalInitalizeFn> fn); |
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.
See comment in EagerTargetImpl
's ctor: I don't think the base class needs an initialization function, I think only TargetImpl
needs it.
test/mocks/init/mocks.h
Outdated
* calling `ready` immediately. | ||
*/ | ||
::testing::internal::TypedExpectation<void()>& expectInitializeWillCallReady(); | ||
}; |
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.
Need blank line after class body.
test/mocks/init/mocks.h
Outdated
@@ -50,6 +50,23 @@ class ExpectableTargetImpl : public TargetImpl { | |||
::testing::internal::TypedExpectation<void()>& expectInitializeWillCallReady(); | |||
}; | |||
|
|||
class ExpectableEagerTargetImpl : public EagerTargetImpl { |
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.
Can the code duplication here be refactored out?
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.
ExpectableEagerTargetImpl
doesn't need fn
and this mock is not helpful. will remove
|
||
using TargetTypes = ::testing::Types<ExpectableTargetImpl, ExpectableEagerTargetImpl>; | ||
|
||
TYPED_TEST_SUITE(InitManagerImplTest, TargetTypes); |
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.
Neat, I hadn't seen this before. I'm not sure if you can totally get away with it, though, if you follow my suggestion of removing the initialization function from the eager target. I think the expectation is that the two target types should not behave the same in all cases.
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.
Right. Once the fn is removed from EagerTargetImpl
the existing test cases don't fit. Will refactor
@silentdai can we step back for a sec? I'm a little concerned that we are not solving the right problem here. Can you summarize what problem you are trying to solve? My understanding is that you want a resource to be |
Signed-off-by: Yuchen Dai <silentdai@gmail.com>
@mattklein123 Thank you for the comment!
Yes!
Yes. I can. I did it in #7440 EagerTarget beats my code in two ways
|
My personal opinion is that init manager is already complicated enough as it is, and unless we really need to make changes to support a new use case, we probably should not. IMO it's not that complicated to just call ready() in the context of initialize(). I'm pretty sure we already did that in other places, e.g., for static clusters. However I will defer to @mergeconflict as he has been thinking about this area a lot more closely. |
Yeah, I honestly agree with @mattklein123. If it's possible to support your use case without changes/additions to the init code, I'd prefer that, since the init manager is already tricky enough without having to reason about multiple varieties of target with different polymorphic behaviors. |
OK. I will adopt the idea that check ready flag in initialize function. Sorry my initial intention is to extract the logic from xDS to By polymorphic behaviors I also find a RAII Target which invoke ready() is also useful but I guess you don't like either. I appreciate your comments and feedback! |
Signed-off-by: Yuchen Dai silentdai@gmail.com
Description:
TargetImpl::ready() called before Init::Manager switch the target state to
ready
.This behavior change allows asynchronous TargetImpl::ready() invocation.
This PR remains the behavior that
destroyed TargetImpl is considered as ready
.Risk Level: Mid
Testing: Existing tests and added unit test for InitManager
Docs Changes: Inline comment
Release Notes:
This is a developer behavior change but not a user behavior change. Not sure if release notes is needed.
This behavior is discussed in #6904