-
Notifications
You must be signed in to change notification settings - Fork 76
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add shared package
nginxprocess
This package is intended for use by any software that wants to identify NGINX processes. It's mostly a refactor of the existing `process.ProcessOperator` with a few differences and some features wanted by NGINXaaS. - minimizes `gopsutil` calls, many of which end up shelling out to `ps`. `nginxprocess.List` filters out non-NGINX processes early. `process.ProcessOperator` gathered details for all processes, and then non-NGINX processes were filtered out later by `instance.NginxProcessParser`. `nginxprocess.List` also fetches less process information, keeping only what the agent codebase actually used. A quick benchmark on my laptop (~750 processes, 12 of which are NGINX) showed a drastic speed improvement: | Benchmark | Iterations | Time (ns/op) | Memory (B/op) | Allocations (allocs/op) | | ----------- | ---------- | ------------ | ------------- | ----------------------- | | old | 1 | 26832273195 | 18282768 | 133775 | | new | 9 | 114558263 | 3439672 | 12836 | - uses a functional options pattern to optionally fetch process status, which is only needed in a few places and saves some more shelling out to `ps`. This could be expanded in the future to support other options while retaining backwards compatibility. - includes some error testing funcs to let callers handle different errors without needing to know how those errors are implemented. This helps trap `gopsutil` implementation details in `package nginxprocess` and makes future `gopsutil` upgrades easier. - respects context cancellation a little faster
- Loading branch information
Showing
4 changed files
with
398 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// Copyright (c) F5, Inc. | ||
// | ||
// This source code is licensed under the Apache License, Version 2.0 license found in the | ||
// LICENSE file in the root directory of this source tree. | ||
package nginxprocess | ||
|
||
import ( | ||
"errors" | ||
|
||
"github.com/shirou/gopsutil/v4/process" | ||
) | ||
|
||
// errNotAnNginxProcess is returned when querying a process that is not an NGINX process. | ||
var errNotAnNginxProcess = errors.New("not a NGINX process") | ||
|
||
// IsNotNginxErr returns true if this error is due to the process not being an NGINX process. | ||
func IsNotNginxErr(err error) bool { return errors.Is(err, errNotAnNginxProcess) } | ||
|
||
// IsNotRunningErr returns true if this error is due to the OS process no longer running. | ||
func IsNotRunningErr(err error) bool { return errors.Is(err, process.ErrorProcessNotRunning) } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
// Copyright (c) F5, Inc. | ||
// | ||
// This source code is licensed under the Apache License, Version 2.0 license found in the | ||
// LICENSE file in the root directory of this source tree. | ||
|
||
// Package nginxprocess contains utilities for working with OS-level NGINX processes. | ||
package nginxprocess | ||
|
||
import ( | ||
"context" | ||
"strings" | ||
"time" | ||
|
||
"github.com/shirou/gopsutil/v4/process" | ||
) | ||
|
||
// Process contains a snapshot of read-only data about an OS-level NGINX process. Create using [List] or [Find]. | ||
type Process struct { | ||
// Created is when this process was created, precision varies by platform and is at best to the millisecond. On | ||
// linux there can be significant skew compared to [time.Now], ± 1s. | ||
Created time.Time | ||
Name string | ||
Cmd string | ||
Exe string // path to the executable | ||
Status string // process status, only present if this process was created using [WithStatus] | ||
PID int32 | ||
PPID int32 // parent PID | ||
} | ||
|
||
// IsWorker returns true if the process is a NGINX worker process. | ||
func (p *Process) IsWorker() bool { return strings.HasPrefix(p.Cmd, "nginx: worker") } | ||
|
||
// IsMaster returns true if the process is a NGINX master process. | ||
func (p *Process) IsMaster() bool { return strings.HasPrefix(p.Cmd, "nginx: master") } | ||
|
||
// IsShuttingDown returns true if the process is shutting down. This can identify workers that are in the process of a | ||
// graceful shutdown. See [changing NGINX configuration] for more details. | ||
// | ||
// [changing NGINX configuration]: https://nginx.org/en/docs/control.html#reconfiguration | ||
func (p *Process) IsShuttingDown() bool { return strings.Contains(p.Cmd, "is shutting down") } | ||
|
||
// IsHealthy uses Status flags to judge process health. Only works on processes created using [WithStatus]. | ||
func (p *Process) IsHealthy() bool { | ||
return p.Status != "" && !strings.Contains(p.Status, process.Zombie) | ||
} | ||
|
||
type options struct { | ||
loadStatus bool | ||
} | ||
|
||
// Option customizes how processes are gathered from the OS. | ||
type Option interface{ apply(opts *options) } | ||
|
||
type optionFunc func(*options) | ||
|
||
func (f optionFunc) apply(o *options) { f(o) } | ||
|
||
// WithStatus runs an additional lookup to load the process status. | ||
func WithStatus(v bool) Option { //nolint:ireturn // functional options can be opaque | ||
return optionFunc(func(o *options) { o.loadStatus = v }) | ||
} | ||
|
||
func convert(ctx context.Context, p *process.Process, o options) (*Process, error) { | ||
if err := ctx.Err(); err != nil { // fail fast if we've canceled | ||
return nil, err | ||
} | ||
|
||
name, _ := p.NameWithContext(ctx) // slow: shells out to ps | ||
if name != "nginx" { | ||
return nil, errNotAnNginxProcess | ||
} | ||
|
||
cmdLine, _ := p.CmdlineWithContext(ctx) // slow: shells out to ps | ||
// ignore nginx processes in the middle of an upgrade | ||
if !strings.HasPrefix(cmdLine, "nginx:") || strings.Contains(cmdLine, "upgrade") { | ||
return nil, errNotAnNginxProcess | ||
} | ||
|
||
var status string | ||
if o.loadStatus { | ||
flags, _ := p.StatusWithContext(ctx) // slow: shells out to ps | ||
status = strings.Join(flags, " ") | ||
} | ||
|
||
// unconditionally run fast lookups | ||
var created time.Time | ||
if millisSinceEpoch, err := p.CreateTimeWithContext(ctx); err == nil { | ||
created = time.UnixMilli(millisSinceEpoch) | ||
} | ||
ppid, _ := p.PpidWithContext(ctx) | ||
exe, _ := p.ExeWithContext(ctx) | ||
|
||
return &Process{ | ||
PID: p.Pid, | ||
PPID: ppid, | ||
Name: name, | ||
Cmd: cmdLine, | ||
Created: created, | ||
Status: status, | ||
Exe: exe, | ||
}, ctx.Err() | ||
} | ||
|
||
// List returns a slice of all NGINX processes. Returns a zero-length slice if no NGINX processes are found. | ||
func List(ctx context.Context, opts ...Option) (ret []*Process, err error) { | ||
o := options{} | ||
for _, opt := range opts { | ||
opt.apply(&o) | ||
} | ||
processes, err := process.ProcessesWithContext(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
for _, p := range processes { | ||
pr, cerr := convert(ctx, p, o) | ||
if IsNotNginxErr(cerr) { | ||
continue | ||
} | ||
if cerr != nil { | ||
return nil, cerr | ||
} | ||
ret = append(ret, pr) | ||
} | ||
|
||
return ret, nil | ||
} | ||
|
||
// Find returns a single NGINX process by PID. Returns an error if the PID is no longer running or if it is not an NGINX | ||
// process. Use with [IsProcessNotRunningErr] and [IsNotNginxErr]. | ||
func Find(ctx context.Context, pid int32, opts ...Option) (*Process, error) { | ||
o := options{} | ||
for _, opt := range opts { | ||
opt.apply(&o) | ||
} | ||
p, err := process.NewProcessWithContext(ctx, pid) | ||
if err != nil { | ||
return nil, err | ||
} | ||
pr, err := convert(ctx, p, o) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return pr, nil | ||
} |
Oops, something went wrong.