-
Notifications
You must be signed in to change notification settings - Fork 868
Wait for MPS daemon readiness before advertising shared resources #1946
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| /** | ||
| # Copyright (c) NVIDIA CORPORATION. All rights reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| **/ | ||
|
|
||
| package mps | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestReadyFilePath(t *testing.T) { | ||
| require.Equal(t, "/mps/.ready", ContainerRoot.ReadyFilePath()) | ||
| require.Equal(t, "/custom/root/.ready", Root("/custom/root").ReadyFilePath()) | ||
| } | ||
|
|
||
| func TestDaemonReady(t *testing.T) { | ||
| root := t.TempDir() | ||
| d := &Daemon{root: Root(root)} | ||
|
|
||
| ready, err := d.Ready() | ||
| require.NoError(t, err) | ||
| require.False(t, ready, "not ready before the .ready file exists") | ||
|
|
||
| require.NoError(t, os.WriteFile(filepath.Join(root, ".ready"), nil, 0o644)) | ||
| ready, err = d.Ready() | ||
| require.NoError(t, err) | ||
| require.True(t, ready, "ready once the .ready file exists") | ||
|
|
||
| require.NoError(t, os.Remove(filepath.Join(root, ".ready"))) | ||
| ready, err = d.Ready() | ||
| require.NoError(t, err) | ||
| require.False(t, ready, "not ready after the .ready file is removed") | ||
| } | ||
|
|
||
| // TestDaemonReadyStatError verifies a stat error other than not-exist is | ||
| // surfaced rather than reported as "not ready". The root is a regular file, so | ||
| // stat-ing a path beneath it fails with ENOTDIR. | ||
| func TestDaemonReadyStatError(t *testing.T) { | ||
| f := filepath.Join(t.TempDir(), "not-a-dir") | ||
| require.NoError(t, os.WriteFile(f, nil, 0o644)) | ||
| d := &Daemon{root: Root(f)} | ||
|
|
||
| ready, err := d.Ready() | ||
| require.Error(t, err) | ||
| require.False(t, ready) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,9 +17,12 @@ | |
| package plugin | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "time" | ||
|
|
||
| "k8s.io/apimachinery/pkg/util/wait" | ||
| "k8s.io/klog/v2" | ||
| pluginapi "k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1" | ||
|
|
||
|
|
@@ -28,6 +31,13 @@ import ( | |
| "github.com/NVIDIA/k8s-device-plugin/internal/rm" | ||
| ) | ||
|
|
||
| const ( | ||
| mpsReadyCheckInterval = 5 * time.Second | ||
| // mpsReadyCheckTimeout bounds a single wait attempt; on timeout the plugin | ||
| // manager retries. | ||
| mpsReadyCheckTimeout = 5 * time.Minute | ||
| ) | ||
|
|
||
| type mpsOptions struct { | ||
| enabled bool | ||
| resourceName spec.ResourceName | ||
|
|
@@ -58,16 +68,34 @@ func (o *options) getMPSOptions(resourceManager rm.ResourceManager) (mpsOptions, | |
| return m, nil | ||
| } | ||
|
|
||
| func (m *mpsOptions) waitForDaemon() error { | ||
| func (m *mpsOptions) waitForDaemon(ctx context.Context) error { | ||
| if m == nil || !m.enabled { | ||
| return nil | ||
| } | ||
| // TODO: Check the .ready file here. | ||
| // TODO: Have some retry strategy here. | ||
|
|
||
| return wait.PollUntilContextTimeout(ctx, mpsReadyCheckInterval, mpsReadyCheckTimeout, true, func(context.Context) (bool, error) { | ||
| if err := m.checkDaemonReady(); err != nil { | ||
| klog.InfoS("Waiting for MPS daemon to be ready", "resource", m.resourceName, "reason", err) | ||
| return false, nil | ||
| } | ||
| klog.InfoS("MPS daemon is ready", "resource", m.resourceName) | ||
| return true, nil | ||
| }) | ||
| } | ||
|
|
||
| // checkDaemonReady requires the .ready file (written after full configuration) | ||
| // and a responsive pipe; AssertHealthy alone responds before config is applied. | ||
| func (m *mpsOptions) checkDaemonReady() error { | ||
| ready, err := m.daemon.Ready() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One case I think is still possible: say the current config is A and it changes to B. The device plugin can observe B before the MPS daemon does. In that window, This should converge once the MPS daemon processes the update, so I don't think it needs to block this PR, but ideally readiness should also confirm that MPS is running the expected config
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, you're right this gates startup but not a config change. Opened #2055 to track it; I'll follow up there once this PR merges. |
||
| if err != nil { | ||
| return fmt.Errorf("checking MPS daemon readiness: %w", err) | ||
| } | ||
| if !ready { | ||
| return fmt.Errorf("MPS daemon has not signalled readiness") | ||
| } | ||
| if err := m.daemon.AssertHealthy(); err != nil { | ||
| return fmt.Errorf("error checking MPS daemon health: %w", err) | ||
| return fmt.Errorf("MPS daemon is not healthy: %w", err) | ||
| } | ||
| klog.InfoS("MPS daemon is healthy", "resource", m.resourceName) | ||
| return nil | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /** | ||
| # Copyright (c) NVIDIA CORPORATION. All rights reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| **/ | ||
|
|
||
| package plugin | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/NVIDIA/k8s-device-plugin/cmd/mps-control-daemon/mps" | ||
| ) | ||
|
|
||
| // TestCheckDaemonReadyRequiresReadyFile is the regression for this PR's race: | ||
| // even when the control pipe would be healthy, readiness must be withheld until | ||
| // the .ready file exists. With no .ready file, checkDaemonReady returns | ||
| // not-ready before ever consulting the pipe (AssertHealthy). | ||
| func TestCheckDaemonReadyRequiresReadyFile(t *testing.T) { | ||
| root := t.TempDir() // no .ready file | ||
| m := &mpsOptions{ | ||
| enabled: true, | ||
| daemon: mps.NewDaemon(nil, mps.Root(root)), | ||
| } | ||
|
|
||
| err := m.checkDaemonReady() | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), "has not signalled readiness") | ||
| } |
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 we remove
.readybefore starting the mps daemons?/mpsis a hostPath, so the file can survive a container restart. A stale.readycan make the device plugin think mps is ready before the new daemon has finished applying the new config.