394 lines
8.2 KiB
Go
394 lines
8.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/config"
|
|
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/healthz"
|
|
"git.tswf.io/infra/go-synapse-backupper/pkg/domain"
|
|
)
|
|
|
|
type fakeScheduler struct {
|
|
mu sync.Mutex
|
|
startCalled bool
|
|
stopCalled bool
|
|
stoppedCtx context.Context
|
|
stopCancel context.CancelFunc
|
|
}
|
|
|
|
func (f *fakeScheduler) Start() {
|
|
f.mu.Lock()
|
|
f.startCalled = true
|
|
f.mu.Unlock()
|
|
}
|
|
|
|
func (f *fakeScheduler) Stop() context.Context {
|
|
f.mu.Lock()
|
|
f.stopCalled = true
|
|
f.mu.Unlock()
|
|
return f.stoppedCtx
|
|
}
|
|
|
|
func (f *fakeScheduler) wasStarted() bool {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.startCalled
|
|
}
|
|
|
|
func (f *fakeScheduler) wasStopped() bool {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.stopCalled
|
|
}
|
|
|
|
type exitPanic int
|
|
|
|
func (e exitPanic) Error() string { return fmt.Sprintf("exit %d", int(e)) }
|
|
|
|
func TestRunCmd_Use(t *testing.T) {
|
|
if runCmd.Use != "run" {
|
|
t.Errorf("runCmd.Use = %q, want %q", runCmd.Use, "run")
|
|
}
|
|
}
|
|
|
|
func TestRunCmd_SIGTERM_stopsScheduler(t *testing.T) {
|
|
origNewScheduler := newScheduler
|
|
origOsExit := osExitFunc
|
|
origRunOnce := runOnceFunc
|
|
defer func() {
|
|
newScheduler = origNewScheduler
|
|
osExitFunc = origOsExit
|
|
runOnceFunc = origRunOnce
|
|
}()
|
|
|
|
fake := &fakeScheduler{}
|
|
fake.stoppedCtx, fake.stopCancel = context.WithCancel(context.Background())
|
|
|
|
newScheduler = func(expr string, job func()) (domain.Scheduler, error) {
|
|
return fake, nil
|
|
}
|
|
|
|
runOnceFunc = func(ctx context.Context, cfg *config.Config) error {
|
|
return nil
|
|
}
|
|
|
|
var exitCode int
|
|
exitPanicked := false
|
|
osExitFunc = func(code int) {
|
|
exitCode = code
|
|
exitPanicked = true
|
|
panic(exitPanic(code))
|
|
}
|
|
|
|
cfg := &config.Config{}
|
|
cfg.Healthz.Port = 0
|
|
cfg.Backup.Cron = "* * * * *"
|
|
cfg.ShutdownTimeout = 100 * time.Millisecond
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
if _, ok := r.(exitPanic); ok {
|
|
errCh <- nil
|
|
return
|
|
}
|
|
errCh <- fmt.Errorf("unexpected panic: %v", r)
|
|
return
|
|
}
|
|
}()
|
|
errCh <- runWithConfig(ctx, cfg)
|
|
}()
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
if !fake.wasStarted() {
|
|
t.Fatal("scheduler.Start was not called")
|
|
}
|
|
|
|
cancel()
|
|
go func() {
|
|
time.Sleep(50 * time.Millisecond)
|
|
fake.stopCancel()
|
|
}()
|
|
|
|
select {
|
|
case err := <-errCh:
|
|
if err != nil {
|
|
t.Fatalf("runWithConfig returned error: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timeout waiting for runWithConfig to exit")
|
|
}
|
|
|
|
if !fake.wasStopped() {
|
|
t.Error("scheduler.Stop was not called")
|
|
}
|
|
|
|
if exitPanicked {
|
|
t.Errorf("unexpected os.Exit call with code %d", exitCode)
|
|
}
|
|
}
|
|
|
|
func TestRunCmd_SIGTERM_forcesExitAfterTimeout(t *testing.T) {
|
|
origNewScheduler := newScheduler
|
|
origOsExit := osExitFunc
|
|
origNewHealthz := newHealthz
|
|
origRunOnce := runOnceFunc
|
|
defer func() {
|
|
newScheduler = origNewScheduler
|
|
osExitFunc = origOsExit
|
|
newHealthz = origNewHealthz
|
|
runOnceFunc = origRunOnce
|
|
}()
|
|
|
|
fake := &fakeScheduler{}
|
|
var cancelFunc context.CancelFunc
|
|
fake.stoppedCtx, cancelFunc = context.WithCancel(context.Background())
|
|
defer cancelFunc()
|
|
|
|
newScheduler = func(expr string, job func()) (domain.Scheduler, error) {
|
|
return fake, nil
|
|
}
|
|
|
|
newHealthz = func(port int) (*healthz.Server, error) {
|
|
return healthz.New(port)
|
|
}
|
|
|
|
runOnceFunc = func(ctx context.Context, cfg *config.Config) error {
|
|
return nil
|
|
}
|
|
|
|
var exitCode int
|
|
exitPanicked := false
|
|
osExitFunc = func(code int) {
|
|
exitCode = code
|
|
exitPanicked = true
|
|
panic(exitPanic(code))
|
|
}
|
|
|
|
cfg := &config.Config{}
|
|
cfg.Healthz.Port = 0
|
|
cfg.Backup.Cron = "* * * * * *"
|
|
cfg.ShutdownTimeout = 100 * time.Millisecond
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
if _, ok := r.(exitPanic); ok {
|
|
errCh <- nil
|
|
return
|
|
}
|
|
errCh <- fmt.Errorf("unexpected panic: %v", r)
|
|
return
|
|
}
|
|
}()
|
|
errCh <- runWithConfig(ctx, cfg)
|
|
}()
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
cancel()
|
|
|
|
select {
|
|
case err := <-errCh:
|
|
if err != nil {
|
|
t.Fatalf("runWithConfig returned error: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timeout waiting for runWithConfig to exit")
|
|
}
|
|
|
|
if !fake.wasStopped() {
|
|
t.Error("scheduler.Stop was not called")
|
|
}
|
|
|
|
if !exitPanicked {
|
|
t.Fatal("expected os.Exit to be called")
|
|
}
|
|
|
|
if exitCode != 0 {
|
|
t.Errorf("os.Exit code = %d, want 0", exitCode)
|
|
}
|
|
}
|
|
|
|
func TestRunCmd_Healthz503DuringShutdown(t *testing.T) {
|
|
origNewScheduler := newScheduler
|
|
origOsExit := osExitFunc
|
|
origNewHealthz := newHealthz
|
|
origRunOnce := runOnceFunc
|
|
defer func() {
|
|
newScheduler = origNewScheduler
|
|
osExitFunc = origOsExit
|
|
newHealthz = origNewHealthz
|
|
runOnceFunc = origRunOnce
|
|
}()
|
|
|
|
fake := &fakeScheduler{}
|
|
fake.stoppedCtx, fake.stopCancel = context.WithCancel(context.Background())
|
|
|
|
newScheduler = func(expr string, job func()) (domain.Scheduler, error) {
|
|
return fake, nil
|
|
}
|
|
|
|
srv, err := healthz.New(0)
|
|
if err != nil {
|
|
t.Fatalf("failed to create healthz server: %v", err)
|
|
}
|
|
|
|
newHealthz = func(port int) (*healthz.Server, error) {
|
|
return srv, nil
|
|
}
|
|
|
|
runOnceFunc = func(ctx context.Context, cfg *config.Config) error {
|
|
return nil
|
|
}
|
|
|
|
osExitFunc = func(code int) { panic(exitPanic(code)) }
|
|
|
|
cfg := &config.Config{}
|
|
cfg.Healthz.Port = 0
|
|
cfg.Backup.Cron = "* * * * * *"
|
|
cfg.ShutdownTimeout = 5 * time.Second
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
if _, ok := r.(exitPanic); ok {
|
|
errCh <- nil
|
|
return
|
|
}
|
|
errCh <- fmt.Errorf("unexpected panic: %v", r)
|
|
return
|
|
}
|
|
}()
|
|
errCh <- runWithConfig(ctx, cfg)
|
|
}()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
addr := srv.Addr()
|
|
resp, err := http.Get("http://" + addr + "/healthz")
|
|
if err != nil {
|
|
t.Fatalf("healthz request failed: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("healthz status before shutdown = %d, want %d", resp.StatusCode, http.StatusOK)
|
|
}
|
|
|
|
cancel()
|
|
|
|
var status503 bool
|
|
for i := 0; i < 20; i++ {
|
|
resp, err = http.Get("http://" + addr + "/healthz")
|
|
if err == nil {
|
|
resp.Body.Close()
|
|
if resp.StatusCode == http.StatusServiceUnavailable {
|
|
status503 = true
|
|
break
|
|
}
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
if !status503 {
|
|
t.Error("did not observe /healthz returning 503 during shutdown")
|
|
}
|
|
|
|
fake.stopCancel()
|
|
|
|
select {
|
|
case err := <-errCh:
|
|
if err != nil {
|
|
t.Fatalf("runWithConfig returned error: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timeout waiting for runWithConfig to exit")
|
|
}
|
|
}
|
|
|
|
func TestRunCmd_HealthzBindFailure(t *testing.T) {
|
|
origNewScheduler := newScheduler
|
|
origNewHealthz := newHealthz
|
|
origOsExit := osExitFunc
|
|
origRunOnce := runOnceFunc
|
|
defer func() {
|
|
newScheduler = origNewScheduler
|
|
newHealthz = origNewHealthz
|
|
osExitFunc = origOsExit
|
|
runOnceFunc = origRunOnce
|
|
}()
|
|
|
|
fake := &fakeScheduler{}
|
|
var cancelFunc context.CancelFunc
|
|
fake.stoppedCtx, cancelFunc = context.WithCancel(context.Background())
|
|
defer cancelFunc()
|
|
|
|
newScheduler = func(expr string, job func()) (domain.Scheduler, error) {
|
|
return fake, nil
|
|
}
|
|
|
|
newHealthz = func(port int) (*healthz.Server, error) {
|
|
return nil, errors.New("bind failed")
|
|
}
|
|
|
|
runOnceFunc = func(ctx context.Context, cfg *config.Config) error {
|
|
return nil
|
|
}
|
|
|
|
var exitCode int
|
|
exitPanicked := false
|
|
osExitFunc = func(code int) {
|
|
exitCode = code
|
|
exitPanicked = true
|
|
panic(exitPanic(code))
|
|
}
|
|
|
|
cfg := &config.Config{}
|
|
cfg.Backup.Cron = "* * * * * *"
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
if _, ok := r.(exitPanic); ok {
|
|
errCh <- nil
|
|
return
|
|
}
|
|
errCh <- fmt.Errorf("unexpected panic: %v", r)
|
|
return
|
|
}
|
|
}()
|
|
errCh <- runWithConfig(context.Background(), cfg)
|
|
}()
|
|
|
|
select {
|
|
case err := <-errCh:
|
|
if err != nil {
|
|
t.Fatalf("runWithConfig returned error: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timeout waiting for runWithConfig to exit")
|
|
}
|
|
|
|
if !exitPanicked {
|
|
t.Fatal("expected os.Exit to be called on healthz bind failure")
|
|
}
|
|
|
|
if exitCode != 1 {
|
|
t.Errorf("os.Exit code = %d, want 1", exitCode)
|
|
}
|
|
}
|