Wait for process to exit when stopping

If a process has some cleanup with purge-on-delete defined, the purge
has to wait until the process actually exited. Otherwise it may happen
that the process got the signal, files are purged, but the process is
still writing some files in order to exit cleanly. This would lead to
some artefacts left on the filesystem.
This commit is contained in:
Ingo Oppermann
2022-08-17 15:12:22 +03:00
parent e4463953b6
commit 50deaef4d3
5 changed files with 92 additions and 26 deletions

Binary file not shown.

View File

@@ -0,0 +1,18 @@
package main
import (
"os"
"os/signal"
"time"
)
func main() {
// Wait for interrupt signal to gracefully shutdown the app
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
time.Sleep(3 * time.Second)
os.Exit(255)
}

View File

@@ -33,11 +33,11 @@ type Process interface {
// Stop stops the process and will not let it restart // Stop stops the process and will not let it restart
// automatically. // automatically.
Stop() error Stop(wait bool) error
// Kill stops the process such that it will restart // Kill stops the process such that it will restart
// automatically if it is defined to do so. // automatically if it is defined to do so.
Kill() error Kill(wait bool) error
// IsRunning returns whether the process is currently // IsRunning returns whether the process is currently
// running or not. // running or not.
@@ -190,7 +190,7 @@ type process struct {
debuglogger log.Logger debuglogger log.Logger
callbacks struct { callbacks struct {
onStart func() onStart func()
onStop func() onExit func()
onStateChange func(from, to string) onStateChange func(from, to string)
} }
limits Limiter limits Limiter
@@ -239,7 +239,7 @@ func New(config Config) (Process, error) {
p.stale.timeout = config.StaleTimeout p.stale.timeout = config.StaleTimeout
p.callbacks.onStart = config.OnStart p.callbacks.onStart = config.OnStart
p.callbacks.onStop = config.OnExit p.callbacks.onExit = config.OnExit
p.callbacks.onStateChange = config.OnStateChange p.callbacks.onStateChange = config.OnStateChange
p.limits = NewLimiter(LimiterConfig{ p.limits = NewLimiter(LimiterConfig{
@@ -251,7 +251,7 @@ func New(config Config) (Process, error) {
"cpu": cpu, "cpu": cpu,
"memory": memory, "memory": memory,
}).Warn().Log("Stopping because limits are exceeded") }).Warn().Log("Stopping because limits are exceeded")
p.Kill() p.Kill(false)
}, },
}) })
@@ -523,7 +523,7 @@ func (p *process) start() error {
} }
// Stop will stop the process and set the order to "stop" // Stop will stop the process and set the order to "stop"
func (p *process) Stop() error { func (p *process) Stop(wait bool) error {
p.order.lock.Lock() p.order.lock.Lock()
defer p.order.lock.Unlock() defer p.order.lock.Unlock()
@@ -533,7 +533,7 @@ func (p *process) Stop() error {
p.order.order = "stop" p.order.order = "stop"
err := p.stop() err := p.stop(wait)
if err != nil { if err != nil {
p.debuglogger.WithFields(log.Fields{ p.debuglogger.WithFields(log.Fields{
"state": p.getStateString(), "state": p.getStateString(),
@@ -547,7 +547,7 @@ func (p *process) Stop() error {
// Kill will stop the process without changing the order such that it // Kill will stop the process without changing the order such that it
// will restart automatically if enabled. // will restart automatically if enabled.
func (p *process) Kill() error { func (p *process) Kill(wait bool) error {
// If the process is currently not running, we don't need // If the process is currently not running, we don't need
// to do anything. // to do anything.
if !p.isRunning() { if !p.isRunning() {
@@ -557,13 +557,13 @@ func (p *process) Kill() error {
p.order.lock.Lock() p.order.lock.Lock()
defer p.order.lock.Unlock() defer p.order.lock.Unlock()
err := p.stop() err := p.stop(wait)
return err return err
} }
// stop will stop a process considering the current order and state. // stop will stop a process considering the current order and state.
func (p *process) stop() error { func (p *process) stop(wait bool) error {
// If the process is currently not running, stop the restart timer // If the process is currently not running, stop the restart timer
if !p.isRunning() { if !p.isRunning() {
p.unreconnect() p.unreconnect()
@@ -583,6 +583,26 @@ func (p *process) stop() error {
"order": p.order.order, "order": p.order.order,
}).Debug().Log("Stopping") }).Debug().Log("Stopping")
wg := sync.WaitGroup{}
if wait {
wg.Add(1)
if p.callbacks.onExit == nil {
p.callbacks.onExit = func() {
wg.Done()
p.callbacks.onExit = nil
}
} else {
cb := p.callbacks.onExit
p.callbacks.onExit = func() {
cb()
wg.Done()
p.callbacks.onExit = cb
}
}
}
var err error var err error
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
// Windows doesn't know the SIGINT // Windows doesn't know the SIGINT
@@ -606,6 +626,10 @@ func (p *process) stop() error {
} }
} }
if err == nil && wait {
wg.Wait()
}
if err != nil { if err != nil {
p.parser.Parse(err.Error()) p.parser.Parse(err.Error())
p.debuglogger.WithFields(log.Fields{ p.debuglogger.WithFields(log.Fields{
@@ -683,7 +707,7 @@ func (p *process) staler(ctx context.Context) {
d := t.Sub(last) d := t.Sub(last)
if d.Seconds() > timeout.Seconds() { if d.Seconds() > timeout.Seconds() {
p.logger.Info().Log("Stale timeout after %s (%.2f).", timeout, d.Seconds()) p.logger.Info().Log("Stale timeout after %s (%.2f).", timeout, d.Seconds())
p.stop() p.stop(false)
return return
} }
} }
@@ -729,7 +753,7 @@ func (p *process) reader() {
// be scheduled for a restart. // be scheduled for a restart.
func (p *process) waiter() { func (p *process) waiter() {
if p.getState() == stateFinishing { if p.getState() == stateFinishing {
p.stop() p.stop(false)
} }
if err := p.cmd.Wait(); err != nil { if err := p.cmd.Wait(); err != nil {
@@ -806,8 +830,8 @@ func (p *process) waiter() {
p.parser.ResetStats() p.parser.ResetStats()
// Call the onStop callback // Call the onStop callback
if p.callbacks.onStop != nil { if p.callbacks.onExit != nil {
go p.callbacks.onStop() go p.callbacks.onExit()
} }
p.order.lock.Lock() p.order.lock.Lock()

View File

@@ -28,7 +28,7 @@ func TestProcess(t *testing.T) {
require.Equal(t, "running", p.Status().State) require.Equal(t, "running", p.Status().State)
p.Stop() p.Stop(false)
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
@@ -52,7 +52,7 @@ func TestReconnectProcess(t *testing.T) {
require.Equal(t, "finished", p.Status().State) require.Equal(t, "finished", p.Status().State)
p.Stop() p.Stop(false)
require.Equal(t, "finished", p.Status().State) require.Equal(t, "finished", p.Status().State)
} }
@@ -73,7 +73,7 @@ func TestStaleProcess(t *testing.T) {
require.Equal(t, "killed", p.Status().State) require.Equal(t, "killed", p.Status().State)
p.Stop() p.Stop(false)
require.Equal(t, "killed", p.Status().State) require.Equal(t, "killed", p.Status().State)
} }
@@ -94,7 +94,7 @@ func TestStaleReconnectProcess(t *testing.T) {
require.Equal(t, "killed", p.Status().State) require.Equal(t, "killed", p.Status().State)
p.Stop() p.Stop(false)
require.Equal(t, "killed", p.Status().State) require.Equal(t, "killed", p.Status().State)
} }
@@ -116,7 +116,7 @@ func TestNonExistingProcess(t *testing.T) {
require.Equal(t, "failed", p.Status().State) require.Equal(t, "failed", p.Status().State)
p.Stop() p.Stop(false)
require.Equal(t, "failed", p.Status().State) require.Equal(t, "failed", p.Status().State)
} }
@@ -138,7 +138,7 @@ func TestNonExistingReconnectProcess(t *testing.T) {
require.Equal(t, "failed", p.Status().State) require.Equal(t, "failed", p.Status().State)
p.Stop() p.Stop(false)
require.Equal(t, "failed", p.Status().State) require.Equal(t, "failed", p.Status().State)
} }
@@ -157,11 +157,35 @@ func TestProcessFailed(t *testing.T) {
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
p.Stop() p.Stop(false)
require.Equal(t, "failed", p.Status().State) require.Equal(t, "failed", p.Status().State)
} }
func TestFFmpegWaitStop(t *testing.T) {
binary, err := testhelper.BuildBinary("sigintwait", "../internal/testhelper")
require.NoError(t, err, "Failed to build helper program")
p, _ := New(Config{
Binary: binary,
Args: []string{},
Reconnect: false,
StaleTimeout: 0,
OnExit: func() {
time.Sleep(2 * time.Second)
},
})
err = p.Start()
require.NoError(t, err)
time.Sleep(4 * time.Second)
p.Stop(true)
require.Equal(t, "finished", p.Status().State)
}
func TestFFmpegKill(t *testing.T) { func TestFFmpegKill(t *testing.T) {
binary, err := testhelper.BuildBinary("sigint", "../internal/testhelper") binary, err := testhelper.BuildBinary("sigint", "../internal/testhelper")
require.NoError(t, err, "Failed to build helper program") require.NoError(t, err, "Failed to build helper program")
@@ -178,7 +202,7 @@ func TestFFmpegKill(t *testing.T) {
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
p.Stop() p.Stop(false)
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
@@ -201,7 +225,7 @@ func TestProcessForceKill(t *testing.T) {
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
p.Stop() p.Stop(false)
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)

View File

@@ -204,7 +204,7 @@ func (r *restream) Stop() {
// Start() they will get restarted. // Start() they will get restarted.
for id, t := range r.tasks { for id, t := range r.tasks {
if t.ffmpeg != nil { if t.ffmpeg != nil {
t.ffmpeg.Stop() t.ffmpeg.Stop(true)
} }
r.unsetCleanup(id) r.unsetCleanup(id)
@@ -996,7 +996,7 @@ func (r *restream) stopProcess(id string) error {
task.process.Order = "stop" task.process.Order = "stop"
task.ffmpeg.Stop() task.ffmpeg.Stop(true)
r.nProc-- r.nProc--
@@ -1024,7 +1024,7 @@ func (r *restream) restartProcess(id string) error {
return nil return nil
} }
task.ffmpeg.Kill() task.ffmpeg.Kill(true)
return nil return nil
} }