Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/libraries/go/lib/pkg/nvkit/servers/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ go_library(
srcs = [
"grpc.go",
"server.go",
"shutdown.go",
],
importpath = "github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/nvkit/servers",
visibility = ["//visibility:public"],
Expand All @@ -28,6 +29,7 @@ go_library(
"//src/libraries/go/lib/pkg/nvkit/clients",
"//src/libraries/go/lib/pkg/nvkit/errors",
"//src/libraries/go/lib/pkg/nvkit/servers/utils",
"//src/libraries/go/lib/pkg/nvkit/shutdown",
"//src/libraries/go/lib/pkg/nvkit/tracing",
"@com_github_go_kit_kit//log",
"@com_github_go_kit_kit//transport/grpc",
Expand Down Expand Up @@ -60,12 +62,14 @@ go_test(
"grpc_shutdown_test.go",
"grpc_test.go",
"server_test.go",
"shutdown_test.go",
],
data = ["//src/libraries/go/lib/pkg/nvkit/test/certs"],
embed = [":servers"],
deps = [
"//src/libraries/go/lib/pkg/nvkit/auth",
"//src/libraries/go/lib/pkg/nvkit/errors",
"//src/libraries/go/lib/pkg/nvkit/shutdown",
"//src/libraries/go/lib/pkg/nvkit/tracing",
"@com_github_go_kit_kit//log",
"@com_github_grpc_ecosystem_grpc_gateway_v2//runtime",
Expand Down
13 changes: 4 additions & 9 deletions src/libraries/go/lib/pkg/nvkit/servers/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (
"os/signal"
"strings"
"sync"
"syscall"
"time"

// By default, it sets `GOMEMLIMIT` to 90% of cgroup's memory limit.
Expand Down Expand Up @@ -58,6 +57,7 @@ import (
"github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/nvkit/clients"
"github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/nvkit/errors"
"github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/nvkit/servers/utils"
"github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/nvkit/shutdown"
"github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/nvkit/tracing"
)

Expand Down Expand Up @@ -536,17 +536,12 @@ func (g *grpcServer) Run() error {
})
}
{
// This function just sits and waits for ctrl-C.
// This function just sits and waits for a shutdown signal.
cancelInterrupt := make(chan struct{})
svrGroup.Add(func() error {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
select {
case sig := <-c:
return fmt.Errorf("received signal %s", sig)
case <-cancelInterrupt:
return nil
}
signal.Notify(c, shutdown.Signals()...)
return awaitShutdownSignal(c, cancelInterrupt)
}, func(error) {
close(cancelInterrupt)
})
Expand Down
43 changes: 43 additions & 0 deletions src/libraries/go/lib/pkg/nvkit/servers/shutdown.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0

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 servers

import (
"os"

"github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/nvkit/shutdown"
)

// awaitShutdownSignal blocks until a shutdown signal arrives or the run group
// tears this actor down, and is the run group's signal actor.
//
// A signal is reported as a *shutdown.SignalError. The run group has no way to
// end an actor other than returning an error, so a graceful stop and a crash
// reach the caller through the same channel; the typed error is what lets them
// be told apart with errors.Is instead of by matching the message.
//
// cancelInterrupt closing means another actor already failed and the group is
// shutting this one down, which is not this actor's error to report.
func awaitShutdownSignal(signals <-chan os.Signal, cancelInterrupt <-chan struct{}) error {
select {
case sig := <-signals:
return shutdown.NewSignalError(sig)
case <-cancelInterrupt:
return nil
}
}
88 changes: 88 additions & 0 deletions src/libraries/go/lib/pkg/nvkit/servers/shutdown_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0

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 servers

import (
"errors"
"os"
"syscall"
"testing"
"time"

"github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/nvkit/shutdown"
)

func TestAwaitShutdownSignal_ReportsSignalAsShutdown(t *testing.T) {
for _, sig := range []os.Signal{syscall.SIGTERM, syscall.SIGINT} {
t.Run(sig.String(), func(t *testing.T) {
signals := make(chan os.Signal, 1)
signals <- sig

err := awaitShutdownSignal(signals, make(chan struct{}))

if err == nil {
t.Fatal("awaitShutdownSignal() = nil, want a shutdown error")
}
if !errors.Is(err, shutdown.ErrSignal) {
t.Errorf("errors.Is(%v, shutdown.ErrSignal) = false, want true", err)
}
if !shutdown.IsSignalError(err) {
t.Errorf("shutdown.IsSignalError(%v) = false, want true", err)
}

var signalErr *shutdown.SignalError
if errors.As(err, &signalErr) && signalErr.Signal != sig {
t.Errorf("Signal = %v, want %v", signalErr.Signal, sig)
}
})
}
}

func TestAwaitShutdownSignal_ReturnsNilWhenInterruptedByRunGroup(t *testing.T) {
// Another actor failed first and the run group is tearing this one down.
// That is not this actor's error to report.
cancelInterrupt := make(chan struct{})
close(cancelInterrupt)

if err := awaitShutdownSignal(make(chan os.Signal), cancelInterrupt); err != nil {
t.Errorf("awaitShutdownSignal() = %v, want nil", err)
}
}

func TestAwaitShutdownSignal_BlocksUntilSomethingHappens(t *testing.T) {
signals := make(chan os.Signal, 1)
done := make(chan error, 1)

go func() { done <- awaitShutdownSignal(signals, make(chan struct{})) }()

select {
case err := <-done:
t.Fatalf("awaitShutdownSignal() returned %v before any signal arrived", err)
case <-time.After(50 * time.Millisecond):
}

signals <- syscall.SIGTERM
select {
case err := <-done:
if !errors.Is(err, shutdown.ErrSignal) {
t.Errorf("errors.Is(%v, shutdown.ErrSignal) = false, want true", err)
}
case <-time.After(time.Second):
t.Fatal("awaitShutdownSignal() did not return after a signal arrived")
}
}
29 changes: 29 additions & 0 deletions src/libraries/go/lib/pkg/nvkit/shutdown/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.

load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "shutdown",
srcs = ["shutdown.go"],
importpath = "github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/nvkit/shutdown",
visibility = ["//visibility:public"],
)

go_test(
name = "shutdown_test",
srcs = ["shutdown_test.go"],
embed = [":shutdown"],
)
125 changes: 125 additions & 0 deletions src/libraries/go/lib/pkg/nvkit/shutdown/shutdown.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0

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 shutdown tells a graceful stop apart from a crash.
//
// The servers run group models a shutdown signal as a terminal error, so an
// expected stop and a genuine failure both reach callers as ordinary errors.
// Every consumer therefore had to decide which was which, and each did it by
// matching the message text against the SIGINT wording. SIGTERM stringifies as
// "terminated" rather than "interrupt", so those checks classified every
// SIGTERM -- and SIGTERM is how Kubernetes asks a container to stop -- as a
// crash, turning routine pod terminations into panics.
//
// This package is the single place that decision lives. Producers report a
// signal with NewSignalError so consumers can use errors.Is; consumers call
// IsSignalError, which also recognizes the historical wording so it keeps
// working against builds that predate the typed error.
package shutdown

import (
"errors"
"os"
"strings"
"syscall"
)

// ErrSignal is the sentinel every shutdown-signal error matches under
// errors.Is, so callers can classify a stop without depending on the concrete
// error type or on its message.
var ErrSignal = errors.New("shutdown signal")

// signalErrorPrefix is the historical wording of the run group's terminal
// error. It is part of the contract: consumers pinned to a lib revision that
// predates this package, and consumers whose servers package comes from the
// separate nvcf-go module, only ever see the text.
const signalErrorPrefix = "received signal "

// handledSignals are the signals a server installs a handler for. SIGTERM is
// what Kubernetes sends to stop a container, so it is the one production
// actually sees; SIGINT only shows up in local runs.
var handledSignals = []os.Signal{syscall.SIGINT, syscall.SIGTERM}

// Signals returns the signals a server should stop on, for passing to
// signal.Notify. Sharing the list with IsSignalError keeps the set a server
// listens for and the set callers forgive from drifting apart.
func Signals() []os.Signal {
return append([]os.Signal(nil), handledSignals...)
}

// SignalError reports that a server stopped because it received a shutdown
// signal rather than because something failed.
type SignalError struct {
Signal os.Signal
}

// NewSignalError returns the error a server should report when sig stops it.
func NewSignalError(sig os.Signal) *SignalError {
return &SignalError{Signal: sig}
}

// Error preserves the historical wording so consumers still matching on text
// keep working.
func (e *SignalError) Error() string {
if e.Signal == nil {
return strings.TrimSpace(signalErrorPrefix)
}
return signalErrorPrefix + e.Signal.String()
}

// Is reports SignalError as ErrSignal so errors.Is classifies any shutdown
// signal without the caller naming a particular one.
func (e *SignalError) Is(target error) bool {
return target == ErrSignal
}

// IsSignalError reports whether err is a server stopping on a shutdown signal
// rather than failing.
//
// A typed SignalError matches directly. Anything else falls back to the
// historical wording, derived from the signal names rather than hard-coded, so
// that neither SIGINT nor SIGTERM can be mistaken for a crash. The fallback is
// still text matching and cannot be exact: an unrelated error that happens to
// embed "received signal terminated" is read as a shutdown. Producers inside
// this repo should return NewSignalError so their consumers never rely on it.
func IsSignalError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, ErrSignal) {
return true
}

msg := strings.ToLower(err.Error())
for _, sig := range handledSignals {
if strings.Contains(msg, signalErrorPrefix+strings.ToLower(sig.String())) {
return true
}
}
return false
}

// IsFatal reports whether a terminal error from a server or root command is a
// genuine failure, rather than nil or the expected response to a shutdown
// signal. It is what a caller deciding whether to panic should ask.
//
// The nil case is the reason this exists rather than being spelled out at each
// call site: IsSignalError(nil) is false, so the obvious hand-written form
// treats a clean exit as fatal unless the caller remembers the nil check.
func IsFatal(err error) bool {
return err != nil && !IsSignalError(err)
}
Loading
Loading