Merge pull request #6 from F1bonacc1/feature/support_daemon_launching

Feature/support daemon launching #3
This commit is contained in:
F1bonacc1 2022-07-05 18:16:57 +03:00 committed by GitHub
commit 13de4ab1f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 136 additions and 42 deletions

View File

@ -158,23 +158,43 @@ process2:
##### ✅ Termination Parameters
```yaml
process1:
command: "pg_ctl start"
nginx:
command: "docker run --rm --name nginx_test nginx"
shutdown:
command: "pg_ctl stop"
command: "docker stop nginx_test"
timeout_seconds: 10 # default 10
signal: 15 # default 15, but only if the 'command' is not defined or empty
```
`shutdown` is optional and can be omitted. The default behavior in this case: `SIGTERM` is issued to the running process.
In case only `shutdown.signal` is defined `[1..31] ` the running process will be terminated with its value.
In case the `shutdown.command` is defined:
1. The `shutdown.command` is executed with all the Environment Variables of the primary process
2. Wait for `shutdown.timeout_seconds` for its completion (if not defined wait for 10 seconds)
3. In case of timeout, the process will receive the `SIGKILL` signal
##### ✅ Background (detached) Processes
```yaml
nginx:
command: "docker run -d --rm --name nginx_test nginx" # note the '-d' for detached mode
is_daemon: true # this flag is required for background processes (default false)
shutdown:
command: "docker stop nginx_test"
timeout_seconds: 10 # default 10
signal: 15 # default 15, but only if command is not defined or empty
```
`shutdown` is optional and can be omitted. The default behaviour in this case: `SIGTERM` is issued to the running process.
1. For processes that start services / daemons in the background, please use the `is_daemon` flag set to `true`.
In case only `shutdown.signal` is defined `[1..31] ` the running process will be terminated with its value.
2. In case a process is daemon it will be considered running until stopped.
In case the the `shutdown.command` is defined:
3. Daemon processes can only be stopped with the `$PROCESSNAME.shutdown.command` as in the example above.
1. The `shutdown.command` is executed with all the Environment Variables of the main process
2. Wait `shutdown.timeout_seconds` for its completion (if not defined wait for 10 seconds)
3. In case of timeout the process will receive the `SIGKILL` signal
#### ✅ <u>Output Handling</u>

View File

@ -58,6 +58,16 @@ processes:
- 'ABC=2221'
- 'EXIT_CODE=4'
docker:
command: "docker run -d --rm --name nginx_test nginx"
# availability:
# restart: on-failure
is_daemon: true
shutdown:
command: "docker stop nginx_test"
signal: 15
timeout_seconds: 5
kcalc:
command: "kcalc"
disabled: true

View File

@ -26,6 +26,7 @@ type Processes map[string]ProcessConfig
type ProcessConfig struct {
Name string
Disabled bool `yaml:"disabled,omitempty"`
IsDaemon bool `yaml:"is_daemon,omitempty"`
Command string `yaml:"command"`
LogLocation string `yaml:"log_location,omitempty"`
Environment []string `yaml:"environment,omitempty"`
@ -65,6 +66,8 @@ const (
ProcessStateDisabled = "Disabled"
ProcessStatePending = "Pending"
ProcessStateRunning = "Running"
ProcessStateLaunching = "Launching"
ProcessStateLaunched = "Launched"
ProcessStateRestarting = "Restarting"
ProcessStateTerminating = "Terminating"
ProcessStateCompleted = "Completed"

27
src/app/daemon.go Normal file
View File

@ -0,0 +1,27 @@
package app
func (p *Process) waitForDaemonCompletion() {
if !p.isDaemonLaunched() {
return
}
loop:
for {
status := <-p.procStateChan
switch status {
case ProcessStateCompleted:
break loop
}
}
}
func (p *Process) notifyDaemonStopped() {
if p.isDaemonLaunched() {
p.procStateChan <- ProcessStateCompleted
}
}
func (p *Process) isDaemonLaunched() bool {
return p.procConf.IsDaemon && p.procState.ExitCode == 0
}

View File

@ -21,25 +21,28 @@ import (
)
const (
DEFAULT_SHUTDOWN_TIMEOUT_SEC = 10
UNDEFINED_SHUTDOWN_TIMEOUT_SEC = 0
DEFAULT_SHUTDOWN_TIMEOUT_SEC = 10
)
type Process struct {
globalEnv []string
procConf ProcessConfig
procState *ProcessState
sync.Mutex
stateMtx sync.Mutex
procCond sync.Cond
procColor func(a ...interface{}) string
noColor func(a ...interface{}) string
redColor func(a ...interface{}) string
logBuffer *pclog.ProcessLogBuffer
logger pclog.PcLogger
cmd *exec.Cmd
done bool
replica int
startTime time.Time
globalEnv []string
procConf ProcessConfig
procState *ProcessState
stateMtx sync.Mutex
procCond sync.Cond
procStateChan chan string
procColor func(a ...interface{}) string
noColor func(a ...interface{}) string
redColor func(a ...interface{}) string
logBuffer *pclog.ProcessLogBuffer
logger pclog.PcLogger
cmd *exec.Cmd
done bool
replica int
startTime time.Time
}
func NewProcess(
@ -50,20 +53,21 @@ func NewProcess(
procLog *pclog.ProcessLogBuffer,
replica int) *Process {
colNumeric := rand.Intn(int(color.FgHiWhite)-int(color.FgHiBlack)) + int(color.FgHiBlack)
//logger, _ := zap.NewProduction()
proc := &Process{
globalEnv: globalEnv,
procConf: procConf,
procColor: color.New(color.Attribute(colNumeric), color.Bold).SprintFunc(),
redColor: color.New(color.FgHiRed).SprintFunc(),
noColor: color.New(color.Reset).SprintFunc(),
logger: logger,
procState: procState,
done: false,
replica: replica,
logBuffer: procLog,
globalEnv: globalEnv,
procConf: procConf,
procColor: color.New(color.Attribute(colNumeric), color.Bold).SprintFunc(),
redColor: color.New(color.FgHiRed).SprintFunc(),
noColor: color.New(color.Reset).SprintFunc(),
logger: logger,
procState: procState,
done: false,
replica: replica,
logBuffer: procLog,
procStateChan: make(chan string, 1),
}
proc.procCond = *sync.NewCond(proc)
return proc
}
@ -83,7 +87,7 @@ func (p *Process) run() error {
go p.handleOutput(stderr, p.handleError)
return p.cmd.Start()
}
p.setStateAndRun(ProcessStateRunning, starter)
p.setStateAndRun(p.getStartingStateName(), starter)
p.startTime = time.Now()
p.procState.Pid = p.cmd.Process.Pid
@ -96,7 +100,12 @@ func (p *Process) run() error {
p.Lock()
p.procState.ExitCode = p.cmd.ProcessState.ExitCode()
p.Unlock()
log.Info().Msgf("%s exited with status %d", p.procConf.Name, p.procState.ExitCode)
log.Info().Msgf("%s exited with status %d", p.getNameWithReplica(), p.procState.ExitCode)
if p.isDaemonLaunched() {
p.setState(ProcessStateLaunched)
p.waitForDaemonCompletion()
}
if !p.isRestartable(p.procState.ExitCode) {
break
@ -144,6 +153,7 @@ func (p *Process) isRestartable(exitCode int) bool {
return p.procState.Restarts < p.procConf.RestartPolicy.MaxRestarts
}
// TODO consider if forking daemon should disable RestartPolicyAlways
if p.procConf.RestartPolicy.Restart == RestartPolicyAlways {
if p.procConf.RestartPolicy.MaxRestarts == 0 {
return true
@ -171,7 +181,7 @@ func (p *Process) wontRun() {
// perform gracefull process shutdown if defined in configuration
func (p *Process) shutDown() error {
if !p.isState(ProcessStateRunning) {
if !p.isRunning() {
log.Debug().Msgf("process %s is in state %s not shutting down", p.getName(), p.procState.Status)
// prevent pending process from running
p.setState(ProcessStateTerminating)
@ -186,12 +196,13 @@ func (p *Process) shutDown() error {
func (p *Process) doConfiguredStop(params ShutDownParams) error {
timeout := params.ShutDownTimeout
if timeout == 0 {
if timeout == UNDEFINED_SHUTDOWN_TIMEOUT_SEC {
timeout = DEFAULT_SHUTDOWN_TIMEOUT_SEC
}
log.Debug().Msgf("terminating %s with timeout %d ...", p.getName(), timeout)
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
defer p.notifyDaemonStopped()
cmd := exec.CommandContext(ctx, getRunnerShell(), getRunnerArg(), params.ShutDownCommand)
cmd.Env = p.getProcessEnvironment()
@ -204,6 +215,10 @@ func (p *Process) doConfiguredStop(params ShutDownParams) error {
return nil
}
func (p *Process) isRunning() bool {
return p.isOneOfStates(ProcessStateRunning, ProcessStateLaunched)
}
func (p *Process) prepareForShutDown() {
// prevent restart during global shutdown
p.procConf.RestartPolicy.Restart = RestartPolicyNo
@ -234,7 +249,7 @@ func (p *Process) getCommand() string {
}
func (p *Process) updateProcState() {
if p.isState(ProcessStateRunning) {
if p.isRunning() {
dur := time.Since(p.startTime)
p.procState.SystemTime = durationToString(dur)
}
@ -251,13 +266,13 @@ func (p *Process) handleOutput(pipe io.ReadCloser,
func (p *Process) handleInfo(message string) {
p.logger.Info(message, p.getName(), p.replica)
fmt.Printf("[%s]\t%s\n", p.procColor(p.getNameWithReplica()), message)
fmt.Printf("[%s\t] %s\n", p.procColor(p.getNameWithReplica()), message)
p.logBuffer.Write(message)
}
func (p *Process) handleError(message string) {
p.logger.Error(message, p.getName(), p.replica)
fmt.Printf("[%s]\t%s\n", p.procColor(p.getNameWithReplica()), p.redColor(message))
fmt.Printf("[%s\t] %s\n", p.procColor(p.getNameWithReplica()), p.redColor(message))
p.logBuffer.Write(message)
}
@ -267,6 +282,17 @@ func (p *Process) isState(state string) bool {
return p.procState.Status == state
}
func (p *Process) isOneOfStates(states ...string) bool {
p.stateMtx.Lock()
defer p.stateMtx.Unlock()
for _, state := range states {
if p.procState.Status == state {
return true
}
}
return false
}
func (p *Process) setState(state string) {
p.stateMtx.Lock()
defer p.stateMtx.Unlock()

View File

@ -0,0 +1,8 @@
package app
func (p *Process) getStartingStateName() string {
if p.procConf.IsDaemon {
return ProcessStateLaunching
}
return ProcessStateRunning
}