Skip to content

Commit

Permalink
fix: golang engine start should await healthcheck.
Browse files Browse the repository at this point in the history
  • Loading branch information
outofcoffee committed Jan 30, 2025
1 parent a384e8f commit 54e9348
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 44 deletions.
4 changes: 1 addition & 3 deletions engine/docker/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,7 @@ func (d *DockerMockEngine) startWithOptions(wg *sync.WaitGroup, options engine.S
up := engine.WaitUntilUp(options.Port, d.shutDownC)

// watch in case container stops
go func() {
notifyOnStopBlocking(d, wg, containerId, cli, ctx)
}()
go notifyOnStopBlocking(d, wg, containerId, cli, ctx)

return up
}
Expand Down
10 changes: 7 additions & 3 deletions engine/enginetests/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ import (
)

type EngineTestFields struct {
ConfigDir string
Options engine.StartOptions
ConfigDir string
Options engine.StartOptions
SkipCheckPort bool
}

type EngineTestScenario struct {
Expand Down Expand Up @@ -118,7 +119,10 @@ func List(t *testing.T, tests []EngineTestScenario, builder func(scenario Engine
require.Equal(t, 1, len(mocks), "expected 1 running mock")
require.NotNilf(t, mocks[0].ID, "mock id should be set")
require.NotNilf(t, mocks[0].Name, "mock name should be set")
require.Equal(t, mocks[0].Port, tt.Fields.Options.Port, "mock port should be correct")

if !tt.Fields.SkipCheckPort {
require.Equal(t, tt.Fields.Options.Port, mocks[0].Port, "mock port should be correct")
}
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion engine/enginetests/testdata/imposter-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ resources:
method: get
response:
statusCode: 200
staticData: "Hello world"
content: "Hello world"
84 changes: 58 additions & 26 deletions engine/golang/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package golang

import (
"fmt"
"os"
"os/exec"
"sync"

"gatehill.io/imposter/debounce"
"gatehill.io/imposter/engine"
"gatehill.io/imposter/engine/procutil"
"gatehill.io/imposter/logging"
"github.com/sirupsen/logrus"
"os"
"os/exec"
"strconv"
"sync"
)

var logger = logging.GetLogger()
Expand All @@ -19,6 +21,8 @@ type GolangMockEngine struct {
options engine.StartOptions
provider *Provider
cmd *exec.Cmd
debouncer debounce.Debouncer
shutDownC chan bool
}

// NewGolangMockEngine creates a new instance of the golang mock engine
Expand All @@ -27,6 +31,8 @@ func NewGolangMockEngine(configDir string, options engine.StartOptions, provider
configDir: configDir,
options: options,
provider: provider,
debouncer: debounce.Build(),
shutDownC: make(chan bool),
}
}

Expand Down Expand Up @@ -57,46 +63,71 @@ func (g *GolangMockEngine) startWithOptions(wg *sync.WaitGroup, options engine.S
logger.Errorf("failed to start golang mock engine: %v", err)
return false
}
g.debouncer.Register(wg, strconv.Itoa(command.Process.Pid))
logger.Trace("starting golang mock engine")
g.cmd = command

wg.Add(1)
// watch in case process stops
up := engine.WaitUntilUp(options.Port, g.shutDownC)

go g.notifyOnStopBlocking(wg)
return true
return up
}

func (g *GolangMockEngine) Stop(wg *sync.WaitGroup) {
if g.cmd != nil && g.cmd.Process != nil {
logger.Debugf("stopping golang mock engine process: %d", g.cmd.Process.Pid)
if err := g.cmd.Process.Signal(os.Interrupt); err != nil {
logger.Warnf("error sending interrupt signal to golang mock engine: %v", err)
g.StopImmediately(wg)
}
if g.cmd == nil {
logger.Tracef("no process to remove")
wg.Done()
return
}
if logger.IsLevelEnabled(logrus.TraceLevel) {
logger.Tracef("stopping mock engine with PID: %v", g.cmd.Process.Pid)
} else {
logger.Info("stopping mock engine")
}

err := g.cmd.Process.Kill()
if err != nil {
logger.Fatalf("error stopping engine with PID: %d: %v", g.cmd.Process.Pid, err)
}
g.notifyOnStopBlocking(wg)
}

func (g *GolangMockEngine) StopImmediately(wg *sync.WaitGroup) {
if g.cmd != nil && g.cmd.Process != nil {
logger.Debugf("force stopping golang mock engine process: %d", g.cmd.Process.Pid)
if err := g.cmd.Process.Kill(); err != nil {
logger.Warnf("error killing golang mock engine process: %v", err)
}
}
go func() { g.shutDownC <- true }()
g.Stop(wg)
}

func (g *GolangMockEngine) Restart(wg *sync.WaitGroup) {
wg.Add(1)
g.Stop(wg)
g.Start(wg)

// don't pull again
restartOptions := g.options
restartOptions.PullPolicy = engine.PullSkip

g.startWithOptions(wg, restartOptions)
wg.Done()
}

func (g *GolangMockEngine) notifyOnStopBlocking(wg *sync.WaitGroup) {
defer wg.Done()
if g.cmd == nil {
return
if g.cmd == nil || g.cmd.Process == nil {
logger.Trace("no subprocess - notifying immediately")
g.debouncer.Notify(wg, debounce.AtMostOnceEvent{})
}
pid := strconv.Itoa(g.cmd.Process.Pid)
if g.cmd.ProcessState != nil && g.cmd.ProcessState.Exited() {
logger.Tracef("process with PID: %v already exited - notifying immediately", pid)
g.debouncer.Notify(wg, debounce.AtMostOnceEvent{Id: pid})
}
if err := g.cmd.Wait(); err != nil {
if _, ok := err.(*exec.ExitError); !ok {
logger.Errorf("error waiting for golang mock engine process: %v", err)
}
_, err := g.cmd.Process.Wait()
if err != nil {
g.debouncer.Notify(wg, debounce.AtMostOnceEvent{
Id: pid,
Err: fmt.Errorf("failed to wait for process with PID: %v: %v", pid, err),
})
} else {
g.debouncer.Notify(wg, debounce.AtMostOnceEvent{Id: pid})
}
}

Expand All @@ -113,5 +144,6 @@ func (g *GolangMockEngine) StopAllManaged() int {
}

func (g *GolangMockEngine) GetVersionString() (string, error) {
// TODO get from binary
return g.options.Version, nil
}
111 changes: 111 additions & 0 deletions engine/golang/engine_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
Copyright © 2021 Pete Cornish <outofcoffee@gmail.com>
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 golang

import (
"gatehill.io/imposter/engine"
"gatehill.io/imposter/engine/enginetests"
"github.com/sirupsen/logrus"
"os"
"path/filepath"
"testing"
)

var engineBuilder = func(tt enginetests.EngineTestScenario) engine.MockEngine {
return engine.BuildEngine("golang", tt.Fields.ConfigDir, tt.Fields.Options)
}

func init() {
logger.SetLevel(logrus.TraceLevel)
EnableEngine()
}

func TestEngine_StartStop(t *testing.T) {
workingDir, err := os.Getwd()
if err != nil {
panic(err)
}
testConfigPath := filepath.Join(workingDir, "../enginetests/testdata")

tests := []enginetests.EngineTestScenario{
{
Name: "start golang engine",
Fields: enginetests.EngineTestFields{
ConfigDir: testConfigPath,
Options: engine.StartOptions{
Port: enginetests.GetFreePort(),
Version: "0.16.0",
PullPolicy: engine.PullIfNotPresent,
LogLevel: "DEBUG",
},
},
},
}
enginetests.StartStop(t, tests, engineBuilder)
}

func TestEngine_Restart(t *testing.T) {
logger.SetLevel(logrus.TraceLevel)
workingDir, err := os.Getwd()
if err != nil {
panic(err)
}
testConfigPath := filepath.Join(workingDir, "../enginetests/testdata")

tests := []enginetests.EngineTestScenario{
{
Name: "restart golang engine",
Fields: enginetests.EngineTestFields{
ConfigDir: testConfigPath,
Options: engine.StartOptions{
Port: enginetests.GetFreePort(),
Version: "0.16.0",
PullPolicy: engine.PullIfNotPresent,
LogLevel: "DEBUG",
},
},
},
}
enginetests.Restart(t, tests, engineBuilder)
}

func TestEngine_List(t *testing.T) {
logger.SetLevel(logrus.TraceLevel)
workingDir, err := os.Getwd()
if err != nil {
panic(err)
}
testConfigPath := filepath.Join(workingDir, "../enginetests/testdata")

tests := []enginetests.EngineTestScenario{
{
Name: "list golang engine",
Fields: enginetests.EngineTestFields{
ConfigDir: testConfigPath,
Options: engine.StartOptions{
Port: enginetests.GetFreePort(),
Version: "0.16.0",
PullPolicy: engine.PullIfNotPresent,
LogLevel: "DEBUG",
},
// skip port check as port can't be determined from environment variables
SkipCheckPort: true,
},
},
}
enginetests.List(t, tests, engineBuilder)
}
8 changes: 4 additions & 4 deletions engine/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@ func WaitForUrl(desc string, url string, abortC chan bool) (success bool) {
})
}

func WaitForOp(desc string, timeout time.Duration, abortC chan bool, operation func() bool) (success bool) {
func WaitForOp(desc string, timeoutDuration time.Duration, abortC chan bool, operation func() bool) (success bool) {
logger.Tracef("waiting for %s", desc)

successC := make(chan bool)
max := time.NewTimer(timeout)
defer max.Stop()
timeout := time.NewTimer(timeoutDuration)
defer timeout.Stop()

go func() {
for {
Expand All @@ -109,7 +109,7 @@ func WaitForOp(desc string, timeout time.Duration, abortC chan bool, operation f

finished := false
select {
case <-max.C:
case <-timeout.C:
finished = true
logger.Fatalf("timed out waiting for %s", desc)
return false
Expand Down
6 changes: 2 additions & 4 deletions engine/jvm/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,8 @@ func (j *JvmMockEngine) startWithOptions(wg *sync.WaitGroup, options engine.Star

up := engine.WaitUntilUp(options.Port, j.shutDownC)

// watch in case container stops
go func() {
j.notifyOnStopBlocking(wg)
}()
// watch in case process stops
go j.notifyOnStopBlocking(wg)

return up
}
Expand Down
6 changes: 3 additions & 3 deletions remote/cloudmocks/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ func (m CloudMocksRemote) waitForStatus(s string, shutDownC chan bool) bool {
logger.Infof("waiting for mock status to be: %s...", s)

finishedC := make(chan bool)
max := time.NewTimer(120 * time.Second)
defer max.Stop()
timeout := time.NewTimer(120 * time.Second)
defer timeout.Stop()

go func() {
for {
Expand All @@ -157,7 +157,7 @@ func (m CloudMocksRemote) waitForStatus(s string, shutDownC chan bool) bool {

finished := false
select {
case <-max.C:
case <-timeout.C:
finished = true
logger.Fatalf("timed out waiting for mock status to be: %s", s)
return false
Expand Down

0 comments on commit 54e9348

Please sign in to comment.