Repository navigation
Failure policy - #329
Failure policy#329
Conversation
The 13 request relays share an identical block deciding whether an error returned for a plugin is fatal, in which case the plugin is closed and the request proceeds without it, or is returned to the runtime and fails the request. Move it into a single handleRequestError() helper so that this policy can be reasoned about, changed and unit tested in one place. No functional change. Signed-off-by: Antonio Ojea <aojea@google.com>
|
+1 @aojea will this require any changes to containerd outside of the go mod version bump? Lmk how I can help |
mikebrow
left a comment
There was a problem hiding this comment.
looking good to me...
@klihub @samuelkarp this reads close to what we've talked about before in having must run/complete plugins.
Thoughts?
@mikebrow @aojea This looks to me like a reasonable way to get rid of the current hardcoded blunt policy and let plugins instead declare their self-scoped preferences. I'm mostly wondering if we should make the policy preference more explicit/split wrt. how the plugin itself vs. the affected request are treated in connection of a protocol failure. IOW, if we should use, instead of an enum with multiple implied (and AFAICT largely orthogonal) semantic conventions, a more explicit expression with something like this Those names and the associated semantics are inverted to what we'd ideally have, and it mostly comes down to (implied semantics of) defaults and backward-compatibility. We could have these also as |
|
@klihub agree with you, I just defined the API based on the observed behavior, however, this proto should define the runtime/plugin behaviors, and the composition of those values will provide the FAIL_CLOSED or FAIL_OPEN user facing behavior. Plugin developers can now describe what they can influence, instead of having to choose what we invented for them. message FailurePolicy {
bool fail_request = 1;
bool keep_connected = 2;
}I went with fail_request + keep_connected rather than disconnect_plugin + fail_request with a DefaultPluginFailurePolicy because backwards compatibility.
Two rules keep the axes from contradicting each other:
Using a message rather than two scalars on ConfigureResponse also leaves room for extensions in the same place (e.g. a plugin-requested timeout, which is the actual root cause of the RDMA case, or per-event overrides) without touching ConfigureResponse again. Tests now cover the full matrix (3 failure kinds × nil + 4 policies) at the unit level, and all 4 combinations end-to-end through the stub, including validators. |
3ec95b6 to
6ab72d5
Compare
When a plugin fails to process a request, because it exceeds the request timeout or loses its connection, the adaptation closes the plugin and lets the request proceed without it. It does not notify the runtime. A plugin that returns an error, in contrast, fails the request. Some plugins must process every request they subscribe to, for example to set up networking or to inject devices. For them, this behavior produces pods and containers that were never set up, with no error. A plugin that exceeds the timeout once is also unregistered for good, although it is still connected (containerd#316, containerd#142, containerd#114, containerd#90, containerd#273). Add a FailurePolicy to ConfigureResponse. The plugin declares it together with its event subscription. The runtime takes two independent decisions when a plugin fails to process a request: one about the request and one about the plugin. The policy has one field for each decision, instead of named presets that bundle both: - fail_request: fail the request with an error that names the plugin and the request, like an error returned by the plugin. Default: the request proceeds with the remaining plugins. - keep_connected: keep a plugin that exceeds the request timeout registered. The timeout only means that the plugin did not handle this request in time. Default: the plugin is unregistered. This field only applies to timeouts. A plugin that loses its connection or causes a protocol error is always unregistered. The default of each field is the old behavior. A plugin without a policy, or built against an NRI version without this field, is treated as before. The proto definition describes the defaults as they are. The adaptation does not substitute anything. An error returned by the plugin is never subject to the policy. Neither is any error after the runtime cancelled its own context or its deadline expired. Until now, a runtime deadline that expired during a request was treated as a plugin timeout and unregistered the plugin. The stub gets a WithFailurePolicy() option. A runtime with an NRI version without failure policy support ignores the field. Fixes: containerd#316 Signed-off-by: Antonio Ojea <aojea@google.com>
There was a problem hiding this comment.
some nits on wording..
At the higher level I think we need a bit more for set of plugin cases.
Problem: When the runtime client included a timeout, the total timeout for all requests sent to all plugins will timeout.
Problem: Without having some sort of health checking, a plugin failing every request due to timeout is going to be problematic for the node. Probably need a plugin restart process and a health check process triggered by some number of timeouts.
Something else to consider: With the default behavior, when fail_request is false for a plugin in a chain of dependent plugins, continuing on to the subsequent plugins is problematic, possibly not if the plugin was some optional feature and/or not really a necessary step in the chain. A depends list is probably needed.. and if we do that I could see timeout being a sum of one or more types (sum of the chain tree, for all required chains/plugins)
Apply the wording suggestions from the review of containerd#329 to the FailurePolicy comments in api.proto, and regenerate api.pb.go. Suggested-by: Mike Brown <brownwm@us.ibm.com> Signed-off-by: Antonio Ojea <aojea@google.com>
The review of containerd#329 raised scenarios that the failure policy does not address. Add a test for each one. Each test states the current behavior so that follow-up work has a baseline: A. The runtime sets a deadline on an operation. NRI calls plugins one after the other, so a slow plugin consumes the budget of the plugins after it. The runtime gets its own error back, and the plugin that was in flight is not blamed. B. A plugin that never answers delays each request by the full timeout. Without a policy, NRI drops it, it reconnects, and the next request drops it again. With keep_connected, the same happens without the reconnect. There is no health check and no counter. C. A plugin that depends on an earlier plugin runs after NRI dropped that plugin. Nothing in the request tells it so. The container is created without the adjustments of the dropped plugin. D. The required plugins check of the default validator does not catch a dropped plugin. NRI adds a plugin to the list of consulted plugins before it relays the request. A plugin kept connected after a timeout can receive a request while an earlier handler still runs. Protect the pod and container maps of the mock plugin with a lock, so that the tests pass under the race detector. Signed-off-by: Antonio Ojea <aojea@google.com>
The review of containerd#329 raised scenarios that the failure policy does not address. Add a test for each one. Each test states the current behavior so that follow-up work has a baseline: A. The runtime sets a deadline on an operation. NRI calls plugins one after the other, so a slow plugin consumes the budget of the plugins after it. The runtime gets its own error back, and the plugin that was in flight is not blamed. B. A plugin that never answers delays each request by the full timeout. Without a policy, NRI drops it, it reconnects, and the next request drops it again. With keep_connected, the same happens without the reconnect. There is no health check and no counter. C. A plugin that depends on an earlier plugin runs after NRI dropped that plugin. Nothing in the request tells it so. The container is created without the adjustments of the dropped plugin. D. The required plugins check of the default validator does not catch a dropped plugin. NRI adds a plugin to the list of consulted plugins before it relays the request. A plugin kept connected after a timeout can receive a request while an earlier handler still runs. Protect the pod and container maps of the mock plugin with a lock, so that the tests pass under the race detector. Signed-off-by: Antonio Ojea <aojea@google.com>
|
@mikebrow applied all the wording suggestions in a separate commit to simplify the review (will squash before merge). I added one test per scenario in TestPluginFailureScenarios for each one of your points so we can assert what happens today:
This is far from perfect, but it moves us to a better state, and the two booleans looks like good primitives to start fixing the rest of things Thoughts |
thanks
good idea
budget management is what I was thinking of as well (for some follow-up), I was more noting that we should consider the use case.. which you have done via a test
I agree, we can't assume it's necessarily stuck, it may be just to slow for the budget/individual call. Thus mentioning a health check of some type to help make that determination. But yes it could be any reason and restarts are not always going to help, even restarting the runtime and node may not help. Answer may come via the recommended deployment patterns, as a systemd service w/health checking or as a pod via k8s health checking. Anyhow definitely needs a lot more thought and out of scope for this PR.
Agreed deserves it's own discussion.
nod
LGTM |
|
kindly ping @klihub for feedback |
|
We need this to be tagged, if its just a version bump I can do that really quick |
|
@aojea once the tag is ready I can update this branch @mikebrow it looks like I am able to create releases? Is that new? |
SGTM .. suggest also pinging the maintainer slack to see if there are objections.... |
When a plugin exceeds the request timeout or looses the connection, NRI closes the plugin and returns
nil, nilto the runtime: the plugin is unregistered and the runtime never learns that anything failed, so the pod or container is created without the plugin having processed it. However, a plugin that returns an error fails the request.This behavior is causing production problems and reported now by multiple end users, this is now more problematic because machines with large number of RDMA NICs get close to the default 2 seconds timeout and usually exceed it, so the Pod may get only a subset of NICs it requested, that is very problematic for these kind of workloads. See for a more detailed explanation kubernetes-sigs/dranet#370. That same PR has an attempt to solve it from the plugin itself but is very very very hacky.
Reported in #316 (a required plugin times out in
RunPodSandboxand pod creation continues; originally kubernetes-sigs/dranet#310) and discussed in #114 and #90 (plugins dropped for a single slow response, "the DeadlineExceeded error is also hidden"), #142 and #45 (requests for a per-plugin failure policy) and #273 (consistent failure handling, which names exactly this asymmetry between explicit errors failing closed and timeouts failing open).To avoid breaking changes we propose to add a
FailurePolicytoConfigureResponse, declared by the plugin together with its event subscription:FAIL_OPEN(default)FAIL_CLOSEDFAIL_OPENis the zero value and keeps the existing behavior and is backwards compatible with plugins that does not implement the new field.A
FAIL_CLOSEDplugin which exceeds the timeout but is still connected is still registered: the timeout only means that it failed to handle this particular request in time, not that it is gone, and dropping it would make all subsequent requests proceed without it, which is precisely what the plugin opted out of.Plugin developers UX
Fixes #316
Refs #114, #90, #142, #45, #273
/assign @klihub @mikebrow @samuelkarp @chrishenzie
/cc @gauravkghildiyal