Files
go-admin/config/extend.go
zhangwenjian 5648bd1dcf feat: state the shutdown budget at start-up
The three budgets are spent one after the other, so what has to fit inside the
orchestrator's grace period is their sum - and nothing said what that sum was.
Working it out meant reading a configuration file, remembering which fields
were absent, and knowing what each one falls back to.

Start-up now prints it: the three values and the total, taken from the resolved
budget rather than from the file. A field left out still costs its default, so
adding up what was written down understates the total by exactly the fields
nobody wrote - which is the arithmetic somebody doing this by hand gets wrong.

Whether the total fits is a separate question, and the framework cannot answer
it alone: `docker stop` allows ten seconds and Kubernetes thirty, three times
apart. A fixed threshold would have warned about the manifest this repository
is about to ship. So extend.shutdown.grace is optional, nothing reads it during
a shutdown, and when it is absent the line says so and prints both reference
values instead of judging.

When it is set and the budget does not fit, the warning names the shortfall:
how many more seconds are needed. A minimum, not a target - this is somebody
else's deployment under constraints this process cannot see, and asking them to
leave headroom on top is not this line's business. Equal does not fit either;
the grace period is when SIGKILL is sent, so a budget that ends exactly then
leaves the last callback no time to return.
2026-09-06 21:49:17 +08:00

211 lines
7.2 KiB
Go

package config
import (
"fmt"
"strings"
)
var ExtConfig Extend
// Extend 扩展配置
//
// extend:
// demo:
// name: demo-name
//
// 使用方法: config.ExtConfig......即可!!
type Extend struct {
AMap AMap // 这里配置对应配置文件的结构即可
FileStore FileStore
RateLimit RateLimit
Shutdown Shutdown
}
// DefaultInboundQPS is the limit applied when nothing is configured. It is the
// value that used to be hard-coded in the middleware, so an existing deployment
// that adds nothing to settings.yml keeps the behaviour it already had.
const DefaultInboundQPS = 200
// RateLimit 全局入站限流。
//
// extend:
// ratelimit:
// inboundqps: 200 # 每秒入站请求上限;填 0 关闭限流
//
// The threshold used to live in common/middleware/sentinel.go as a constant,
// which made 200 QPS the ceiling of every deployment with nothing in the
// configuration to reveal it.
type RateLimit struct {
// InboundQPS caps inbound requests per second across the process.
//
// Absent means DefaultInboundQPS, zero disables the limiter, and a positive
// value is the threshold. The pointer is what separates "not configured"
// from "configured to zero" - the two need different answers and a plain
// float64 cannot tell them apart.
InboundQPS *float64
}
// Threshold reports the limit to apply. Zero means no limiting.
func (r RateLimit) Threshold() float64 {
if r.InboundQPS == nil {
return DefaultInboundQPS
}
return *r.InboundQPS
}
type AMap struct {
Key string
}
// FileStore 对象存储。上传接口的 source 参数决定走哪一家:2 是阿里云,3 是七牛。
// 没有填的那一家在被请求时返回明确错误,而不是上传到别处或者崩溃。
//
// common/file_store 里还实现了华为云 OBS,但上传接口没有对应的 source 取值,
// 所以这里也不为它提供配置。
type FileStore struct {
AliYun ObjectStore
QiNiu ObjectStore
}
type ObjectStore struct {
Endpoint string
AccessKeyID string
AccessKeySecret string
BucketName string
}
// Configured reports whether enough was filled in to attempt a connection.
func (o ObjectStore) Configured() bool {
return o.Endpoint != "" && o.AccessKeyID != "" && o.AccessKeySecret != "" && o.BucketName != ""
}
// Default budgets for a graceful shutdown, in seconds. Each applies to the
// matching field of extend.shutdown when that field is absent, and together
// they are what the process spent before the section existed - so a deployment
// that configures nothing keeps the shutdown it already had.
//
// The drain default is zero deliberately. The three budgets are spent one
// after the other, and once their sum reaches the orchestrator's stop grace
// period the process is killed part-way through its cleanup callbacks, which
// is worse than not draining at all. `docker stop` allows ten seconds by
// default and 5+3 already leaves little room, so a non-zero default here would
// slow down every existing shutdown to buy something only a load balancer that
// polls /ready can collect.
const (
DefaultDrainSeconds = 0
DefaultServerSeconds = 5
DefaultCleanupSeconds = 3
)
// Shutdown is how long a graceful shutdown may spend, stage by stage.
//
// extend:
// shutdown:
// drain: 0
// server: 5
// cleanup: 3
// grace: 30
//
// Every field is a pointer for the reason RateLimit.InboundQPS is: nil means
// "not configured" and takes the default, while a value that was written down
// is taken literally, zero included. Without that separation `server: 0` - do
// not wait for in-flight requests, which is a reasonable thing to ask under a
// very short grace period - could not be said at all, and `drain: 0` would
// have to mean something different from `server: 0` in the same section.
type Shutdown struct {
// Drain is how long to keep serving normally after a stop signal arrives.
// Throughout it /ready answers 503 and keep-alive is switched off, which
// is what gives whatever routes traffic here time to stop routing it
// before the listener closes. Zero is no window: the readiness flip and
// the listener closing are then microseconds apart and nothing observes
// the first.
//
// What the window is worth depends on who does the removing and on what
// basis; the package comment in common/health has the two cases, and they
// do not want the same value.
Drain *int
// Server is how long the server waits for in-flight requests once the
// listener is closed.
Server *int
// Cleanup is how long the BeforeExit callbacks get after that.
Cleanup *int
// Grace is the stop grace period the orchestrator gives this process -
// `docker stop --timeout`, or terminationGracePeriodSeconds. Nothing reads
// it during a shutdown; it exists so start-up can say whether the budget
// fits inside it. Absent means no comparison is made, because the
// reference values differ threefold between runtimes and a fixed threshold
// would warn about configurations that are correct.
Grace *int
}
// ShutdownBudget is what a shutdown will actually spend, in seconds, after the
// fallbacks have been applied.
type ShutdownBudget struct {
Drain int
Server int
Cleanup int
// Grace is zero when extend.shutdown.grace was not configured.
Grace int
}
// Budget resolves the configured section into the values that will be spent.
//
// A negative is refused rather than corrected. A wait cannot be negative, so
// there is no reading of one to honour, and quietly turning it into zero would
// be the failure this whole section exists to remove: written down, accepted,
// and not what happens. It is returned as an error rather than reported here
// so that the rule can be checked without ending the process.
func (s Shutdown) Budget() (ShutdownBudget, error) {
var negative []string
for _, f := range []struct {
name string
value *int
}{
{"drain", s.Drain},
{"server", s.Server},
{"cleanup", s.Cleanup},
{"grace", s.Grace},
} {
if f.value != nil && *f.value < 0 {
negative = append(negative, fmt.Sprintf("%s: %d", f.name, *f.value))
}
}
if len(negative) > 0 {
return ShutdownBudget{}, fmt.Errorf(
"extend.shutdown was given a negative number of seconds (%s); "+
"a wait cannot be negative, and 0 is how to say \"do not wait\"",
strings.Join(negative, ", "))
}
return ShutdownBudget{
Drain: budgetSeconds(s.Drain, DefaultDrainSeconds),
Server: budgetSeconds(s.Server, DefaultServerSeconds),
Cleanup: budgetSeconds(s.Cleanup, DefaultCleanupSeconds),
Grace: budgetSeconds(s.Grace, 0),
}, nil
}
func budgetSeconds(configured *int, fallback int) int {
if configured != nil {
return *configured
}
return fallback
}
// Total is the whole of the shutdown, since the three stages run one after the
// other.
func (b ShutdownBudget) Total() int { return b.Drain + b.Server + b.Cleanup }
// Overrun reports how many seconds have to be found for the budget to fit
// inside the configured grace period. It is zero when no grace period was
// configured and when the budget already fits.
//
// Fitting means strictly less: the grace period is when SIGKILL is sent, so a
// budget that ends exactly then leaves the last callback no time to return.
func (b ShutdownBudget) Overrun() int {
if b.Grace <= 0 || b.Total() < b.Grace {
return 0
}
return b.Total() - b.Grace + 1
}