From f4091fa55b0384a01fa9e75b0bccc07d32dfa9bf Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 29 Nov 2022 11:56:08 -0600 Subject: [PATCH 1/8] chore: ignore additional files Signed-off-by: Samantha Coyle --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ab516a0..5cadac2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ bin/ -./wait-for-it \ No newline at end of file +*wait-for-it +.idea/ \ No newline at end of file From c55af0744ed8246f7e2124b3c56503df287cd076 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 29 Nov 2022 11:57:23 -0600 Subject: [PATCH 2/8] refactor: create pkg for wait logic Signed-off-by: Samantha Coyle --- go.sum | 0 main.go | 13 ++++++------ pkg/wait/log.go | 8 +++++++ services.go => pkg/wait/wait.go | 37 +++++++++++++++++++++++---------- 4 files changed, 41 insertions(+), 17 deletions(-) create mode 100644 go.sum create mode 100644 pkg/wait/log.go rename services.go => pkg/wait/wait.go (55%) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/main.go b/main.go index 35a26a4..36aec6f 100644 --- a/main.go +++ b/main.go @@ -3,12 +3,13 @@ package main import ( "flag" "fmt" + "github.com/roerohan/wait-for-it/pkg/wait" "os" ) var ( timeout int - services Services + services wait.Services quiet bool strict bool ) @@ -21,12 +22,12 @@ func init() { } // Log is used to log with prefix wait-for-it: -func Log(message string) { +func log(message string) { if quiet { return } - fmt.Println("wait-for-it: " + message) + wait.Log("wait-for-it: " + message) } func main() { @@ -34,13 +35,13 @@ func main() { args := os.Args if len(services) != 0 { - Log(fmt.Sprintf("waiting %d seconds for %s", timeout, services.String())) + log(fmt.Sprintf("waiting %d seconds for %s", timeout, services.String())) ok := services.Wait(timeout) if !ok { - Log(fmt.Sprintf("timeout occured after waiting for %d seconds", timeout)) + log(fmt.Sprintf("timeout occured after waiting for %d seconds", timeout)) if strict { - Log("strict mode, refusing to execute subprocess") + log("strict mode, refusing to execute subprocess") return } } diff --git a/pkg/wait/log.go b/pkg/wait/log.go new file mode 100644 index 0000000..1f98ee4 --- /dev/null +++ b/pkg/wait/log.go @@ -0,0 +1,8 @@ +package wait + +import "fmt" + +// Log is used to log with prefix wait-for-it +func Log(message string) { + fmt.Println("wait-for-it: " + message) +} diff --git a/services.go b/pkg/wait/wait.go similarity index 55% rename from services.go rename to pkg/wait/wait.go index e6bf372..9a964c3 100644 --- a/services.go +++ b/pkg/wait/wait.go @@ -1,4 +1,4 @@ -package main +package wait import ( "fmt" @@ -10,13 +10,21 @@ import ( // Services is a string array storing // the services that are to be waited for -type Services []string +type Services []Service -// Set is used to append a string -// to the service, to implement +// Service is a string meant to denote a service with a wait condition upon start up +type Service string + +func (s *Service) String() string { + return string(*s) +} + +// Set is used to append a Service +// to the slice of Services, to implement // the interface flag.Value func (s *Services) Set(value string) error { - *s = append(*s, value) + service := interface{}(value).(Service) + *s = append(*s, service) return nil } @@ -25,7 +33,13 @@ func (s *Services) Set(value string) error { // to implement the interface // flag.Value func (s *Services) String() string { - return strings.Join(*s, ", ") + var sb strings.Builder + const formatter string = ", " + for _, service := range *s { + sb.WriteString(service.String()) + sb.WriteString(formatter) + } + return sb.String() } // Wait waits for all services @@ -39,7 +53,7 @@ func (s *Services) Wait(tSeconds int) bool { success := make(chan bool, 1) go func() { - for _, service := range services { + for _, service := range *s { go waitOne(service, &wg, now) } wg.Wait() @@ -52,16 +66,17 @@ func (s *Services) Wait(tSeconds int) bool { case <-time.After(t): return false } + } -func waitOne(service string, wg *sync.WaitGroup, start time.Time) { +func waitOne(service Service, wg *sync.WaitGroup, start time.Time) { defer wg.Done() for { - _, err := net.Dial("tcp", service) + _, err := net.Dial("tcp", service.String()) if err == nil { Log(fmt.Sprintf("%s is available after %s", service, time.Since(start))) - break } - time.Sleep(time.Second) + break } + time.Sleep(time.Second) } From 7d5bff25b58e6a8bd960eb3bcd9ae926a4fceacb Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Mon, 12 Dec 2022 14:15:28 -0600 Subject: [PATCH 3/8] refactor(service): use service struct with appropriate fields Signed-off-by: Samantha Coyle --- main.go | 11 +++-- pkg/wait/wait.go | 121 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 105 insertions(+), 27 deletions(-) diff --git a/main.go b/main.go index 36aec6f..f2c395a 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/roerohan/wait-for-it/pkg/wait" "os" + "time" ) var ( @@ -15,10 +16,10 @@ var ( ) func init() { - flag.IntVar(&timeout, "t", 15, "Timeout in seconds, zero for no timeout") + flag.IntVar(&timeout, "t", 15, "Service request timeout in seconds, zero for no timeout") flag.BoolVar(&quiet, "q", false, "Quiet, don't output any status messages") flag.BoolVar(&strict, "s", false, "Only execute subcommand if the test succeeds") - flag.Var(&services, "w", "Services to be waiting for, in the form `host:port`") + flag.Var(&services, "w", "Dependency services to be waiting for, in the form `host:port`") } // Log is used to log with prefix wait-for-it: @@ -36,10 +37,10 @@ func main() { if len(services) != 0 { log(fmt.Sprintf("waiting %d seconds for %s", timeout, services.String())) - ok := services.Wait(timeout) - - if !ok { + err := wait.ForDependencies(services, time.Duration(timeout), 30*time.Second) + if err != nil { log(fmt.Sprintf("timeout occured after waiting for %d seconds", timeout)) + log(fmt.Sprintf("wait.ForDependencies failed with err %v", err)) if strict { log("strict mode, refusing to execute subprocess") return diff --git a/pkg/wait/wait.go b/pkg/wait/wait.go index 9a964c3..45c87a7 100644 --- a/pkg/wait/wait.go +++ b/pkg/wait/wait.go @@ -1,37 +1,62 @@ package wait import ( + "errors" "fmt" "net" + "strconv" "strings" "sync" "time" ) -// Services is a string array storing -// the services that are to be waited for +// Services is a string array storing the services that are to be waited for type Services []Service -// Service is a string meant to denote a service with a wait condition upon start up -type Service string +// Service is a struct meant to denote a service hostname:port with a wait condition upon start up +type Service struct { + hostname string + port int +} + +var ( + // ErrServiceMaxTimeout is the error message to use in case a max service startup timeout is exceeded. + ErrServiceMaxTimeout = fmt.Errorf("max service startup timeout duration exceeded waiting for service dependencies") +) +// String prints out the human-readable Service hostname:port string. func (s *Service) String() string { - return string(*s) + return fmt.Sprintf("%s:%d", s.hostname, s.port) +} + +// NewService creates a wait.Service type. +func NewService(hostname string, port int) Service { + return Service{ + hostname: hostname, + port: port, + } } -// Set is used to append a Service -// to the slice of Services, to implement -// the interface flag.Value +// Set is used to append a Service to the slice of Services, +// to implement the interface flag.Value func (s *Services) Set(value string) error { - service := interface{}(value).(Service) + const separator = ":" + + // Note: serviceInfo[0] = hostname, serviceInfo[1] = port + serviceInfo := strings.Split(value, separator) + Log(fmt.Sprintf("serviceInfo %s", serviceInfo)) + port, err := strconv.Atoi(serviceInfo[1]) + if err != nil { + return err + } + + service := NewService(serviceInfo[0], port) *s = append(*s, service) return nil } -// String returns a string -// representation of the flag, -// to implement the interface -// flag.Value +// String returns a string representation of the flag, +// to implement the interface flag.Value func (s *Services) String() string { var sb strings.Builder const formatter string = ", " @@ -42,18 +67,65 @@ func (s *Services) String() string { return sb.String() } -// Wait waits for all services -func (s *Services) Wait(tSeconds int) bool { - t := time.Duration(tSeconds) * time.Second +//func ForDependencies(waitServices Services, serviceRequestTimeout time.Duration) error { +// //serviceTimeout := int(serviceRequestTimeout.Seconds()) +// success := make(chan bool, 1) +// +// if len(waitServices) != 0 { +// //lc.Infof("Service startup timeout invoked to wait %d seconds for dependent services %s", serviceTimeout, dependentServices) +// ok := wait(waitServices, serviceRequestTimeout) +// if ok { +// success <- true +// } else { +// Log("Waiting for service dependencies to become available...") +// } +// } +// +// // return err if service wait time exceeds ServiceMaxTimeout time +// select { +// case <-success: +// return nil +// //case <-time.After(ServiceMaxTimeout): +// // return ErrServiceMaxTimeout +// } +// return nil +//} + +// ForDependencies allows the service to wait for its dependencies to be up and ready for a configurable amount of time. +// If the service dependency request timeout is reached and the dependent services are not yet available, +// then the timeout wait interval will continue until the dependencies are up for a maximum wait time of maxTimeout. +func ForDependencies(waitServices Services, serviceRequestTimeout, maxTimeout time.Duration) error { + success := make(chan bool, 1) + + if len(waitServices) != 0 { + //lc.Infof("Service startup timeout invoked to wait %d seconds for dependent services %s", serviceTimeout, dependentServices) + ok := wait(waitServices, serviceRequestTimeout) + if ok { + success <- true + } else { + Log("Waiting for service dependencies to become available...") + } + } + + // return err if service wait time exceeds ServiceMaxTimeout time + select { + case <-success: + return nil + case <-time.After(maxTimeout): + return ErrServiceMaxTimeout + } +} + +func wait(waitServices Services, waitTimeOut time.Duration) bool { now := time.Now() var wg sync.WaitGroup - wg.Add(len(*s)) + wg.Add(len(waitServices)) success := make(chan bool, 1) go func() { - for _, service := range *s { + for _, service := range waitServices { go waitOne(service, &wg, now) } wg.Wait() @@ -63,7 +135,7 @@ func (s *Services) Wait(tSeconds int) bool { select { case <-success: return true - case <-time.After(t): + case <-time.After(waitTimeOut): return false } @@ -74,9 +146,14 @@ func waitOne(service Service, wg *sync.WaitGroup, start time.Time) { for { _, err := net.Dial("tcp", service.String()) if err == nil { - Log(fmt.Sprintf("%s is available after %s", service, time.Since(start))) + Log(fmt.Sprintf("%s is available after %s", service.String(), time.Since(start))) + break + } + opErr, ok := err.(*net.OpError) + if ok && errors.Is(err, opErr) { + Log(fmt.Sprintf("failed to dial service %s with error: %s", service.String(), opErr.Error())) + break } - break + time.Sleep(time.Second) } - time.Sleep(time.Second) } From 79692a3ea13d5c2b6d3d0ba4a6031cc189d5be67 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Mon, 12 Dec 2022 18:07:51 -0600 Subject: [PATCH 4/8] feat(cli): homogenize cli and pkg req/max timeout differentiation Signed-off-by: Samantha Coyle --- main.go | 20 ++++++++++--------- pkg/wait/wait.go | 51 ++++++++++++------------------------------------ 2 files changed, 24 insertions(+), 47 deletions(-) diff --git a/main.go b/main.go index f2c395a..45754b4 100644 --- a/main.go +++ b/main.go @@ -9,14 +9,16 @@ import ( ) var ( - timeout int - services wait.Services - quiet bool - strict bool + reqTimeout int + maxTimeout int + services wait.Services + quiet bool + strict bool ) func init() { - flag.IntVar(&timeout, "t", 15, "Service request timeout in seconds, zero for no timeout") + flag.IntVar(&reqTimeout, "t", 15, "Service request timeout in seconds, zero for no timeout") + flag.IntVar(&maxTimeout, "m", 30, "Max service timeout to retry request in seconds, zero for no max service timeout") flag.BoolVar(&quiet, "q", false, "Quiet, don't output any status messages") flag.BoolVar(&strict, "s", false, "Only execute subcommand if the test succeeds") flag.Var(&services, "w", "Dependency services to be waiting for, in the form `host:port`") @@ -28,7 +30,7 @@ func log(message string) { return } - wait.Log("wait-for-it: " + message) + wait.Log(message) } func main() { @@ -36,15 +38,15 @@ func main() { args := os.Args if len(services) != 0 { - log(fmt.Sprintf("waiting %d seconds for %s", timeout, services.String())) - err := wait.ForDependencies(services, time.Duration(timeout), 30*time.Second) + log(fmt.Sprintf("waiting %d seconds for %s for a max of %d seconds", reqTimeout, services.String(), maxTimeout)) + err := wait.ForDependencies(services, time.Duration(reqTimeout), time.Duration(maxTimeout)) if err != nil { - log(fmt.Sprintf("timeout occured after waiting for %d seconds", timeout)) log(fmt.Sprintf("wait.ForDependencies failed with err %v", err)) if strict { log("strict mode, refusing to execute subprocess") return } + return } } diff --git a/pkg/wait/wait.go b/pkg/wait/wait.go index 45c87a7..f7dde30 100644 --- a/pkg/wait/wait.go +++ b/pkg/wait/wait.go @@ -44,7 +44,6 @@ func (s *Services) Set(value string) error { // Note: serviceInfo[0] = hostname, serviceInfo[1] = port serviceInfo := strings.Split(value, separator) - Log(fmt.Sprintf("serviceInfo %s", serviceInfo)) port, err := strconv.Atoi(serviceInfo[1]) if err != nil { return err @@ -64,54 +63,30 @@ func (s *Services) String() string { sb.WriteString(service.String()) sb.WriteString(formatter) } - return sb.String() -} -//func ForDependencies(waitServices Services, serviceRequestTimeout time.Duration) error { -// //serviceTimeout := int(serviceRequestTimeout.Seconds()) -// success := make(chan bool, 1) -// -// if len(waitServices) != 0 { -// //lc.Infof("Service startup timeout invoked to wait %d seconds for dependent services %s", serviceTimeout, dependentServices) -// ok := wait(waitServices, serviceRequestTimeout) -// if ok { -// success <- true -// } else { -// Log("Waiting for service dependencies to become available...") -// } -// } -// -// // return err if service wait time exceeds ServiceMaxTimeout time -// select { -// case <-success: -// return nil -// //case <-time.After(ServiceMaxTimeout): -// // return ErrServiceMaxTimeout -// } -// return nil -//} + // trim the last comma that was added for last service + return strings.TrimSuffix(sb.String(), formatter) +} // ForDependencies allows the service to wait for its dependencies to be up and ready for a configurable amount of time. // If the service dependency request timeout is reached and the dependent services are not yet available, // then the timeout wait interval will continue until the dependencies are up for a maximum wait time of maxTimeout. func ForDependencies(waitServices Services, serviceRequestTimeout, maxTimeout time.Duration) error { - success := make(chan bool, 1) + if len(waitServices) == 0 { + return nil + } - if len(waitServices) != 0 { - //lc.Infof("Service startup timeout invoked to wait %d seconds for dependent services %s", serviceTimeout, dependentServices) - ok := wait(waitServices, serviceRequestTimeout) - if ok { - success <- true - } else { - Log("Waiting for service dependencies to become available...") - } + success := make(chan bool, 1) + ok := wait(waitServices, serviceRequestTimeout) + if ok { + success <- true } // return err if service wait time exceeds ServiceMaxTimeout time select { case <-success: return nil - case <-time.After(maxTimeout): + case <-time.After(maxTimeout * time.Second): return ErrServiceMaxTimeout } } @@ -135,7 +110,7 @@ func wait(waitServices Services, waitTimeOut time.Duration) bool { select { case <-success: return true - case <-time.After(waitTimeOut): + case <-time.After(waitTimeOut * time.Second): return false } @@ -151,7 +126,7 @@ func waitOne(service Service, wg *sync.WaitGroup, start time.Time) { } opErr, ok := err.(*net.OpError) if ok && errors.Is(err, opErr) { - Log(fmt.Sprintf("failed to dial service %s with error: %s", service.String(), opErr.Error())) + Log(fmt.Sprintf("failed to dial service %s with err: %s", service.String(), opErr.Error())) break } time.Sleep(time.Second) From d3c58fa627a245dde6b24e51ee4b9b0ac501496e Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Mon, 12 Dec 2022 18:25:52 -0600 Subject: [PATCH 5/8] refactor(utility): exit upon err Signed-off-by: Samantha Coyle --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 45754b4..713a7a4 100644 --- a/main.go +++ b/main.go @@ -44,9 +44,9 @@ func main() { log(fmt.Sprintf("wait.ForDependencies failed with err %v", err)) if strict { log("strict mode, refusing to execute subprocess") - return + os.Exit(1) } - return + os.Exit(1) } } From 1311a5af90fbf456872462af6b3bb4806b690e46 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Mon, 12 Dec 2022 18:45:55 -0600 Subject: [PATCH 6/8] fix: strict mode check placement Signed-off-by: Samantha Coyle --- main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 713a7a4..3ac3eb3 100644 --- a/main.go +++ b/main.go @@ -42,10 +42,10 @@ func main() { err := wait.ForDependencies(services, time.Duration(reqTimeout), time.Duration(maxTimeout)) if err != nil { log(fmt.Sprintf("wait.ForDependencies failed with err %v", err)) - if strict { - log("strict mode, refusing to execute subprocess") - os.Exit(1) - } + os.Exit(1) + } + if strict { + log("strict mode, refusing to execute subprocess") os.Exit(1) } } From f53d67e86abbe9676822b2ccfa54fe3a245c7f4b Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Mon, 12 Dec 2022 18:46:16 -0600 Subject: [PATCH 7/8] docs: update documentation with new flag Signed-off-by: Samantha Coyle --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f0e47d8..4f18e08 100644 --- a/README.md +++ b/README.md @@ -107,12 +107,14 @@ Use `wait-for-it -h` to display the following list. ``` Usage of wait-for-it: + -m int + Max service timeout to retry request in seconds, zero for no max service timeout (default 30) -q Quiet, don't output any status messages -s Only execute subcommand if the test succeeds -t int - Timeout in seconds, zero for no timeout (default 15) + Service request timeout in seconds, zero for no timeout (default 15) -w host:port - Services to be waiting for, in the form host:port + Dependency services to be waiting for, in the form host:port ``` You can run any executable after passing ` -- `, like in the examples below. @@ -129,8 +131,8 @@ wait-for-it -w google.com:80 -w localhost:27017 -t 30 -- echo "Waiting for 30 se ```sh $ wait-for-it -w abcd:80 -s -t 5 -- echo "Done\!" -wait-for-it: waiting 5 seconds for abcd:80 -wait-for-it: timeout occured after waiting for 5 seconds +wait-for-it: waiting 5 seconds for abcd:80 for a max of 10 seconds +wait-for-it: failed to dial service abcd:80 with err: dial tcp: lookup abcd on 172.24.128.1:53: no such host wait-for-it: strict mode, refusing to execute subprocess ``` From 460a2ddd6506b26ee6fa3729041700295e4f2254 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 13 Dec 2022 08:03:02 -0600 Subject: [PATCH 8/8] docs: add docs on consuming go pkg setup Signed-off-by: Samantha Coyle --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f18e08..8fcf550 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,10 @@ This package is adapted from [vishnubob/wait-for-it](https://github.com/vishnubo Since [vishnubob/wait-for-it](https://github.com/vishnubob/wait-for-it) is a bash script, it does not work directly with minimal containers like [scratch](https://hub.docker.com/_/scratch), which are commonly used to run binaries. -With the help of this package, you can generate a binary, which can run inside minimal Docker containers and wait for a TCP connection such as a `mysql` database. You can find an example here: [csivitu/bl0b](https://github.com/csivitu/bl0b/blob/master/docker-compose.yml). +With the help of this package, you can: +1. Generate a binary, which can run inside minimal Docker containers and wait for a TCP connection such as a `mysql` database. You can find an example here: [csivitu/bl0b](https://github.com/csivitu/bl0b/blob/master/docker-compose.yml). +2. Consume the `wait` package internally within your Go microservices. +This would allow your services running natively to have the same wait setup as that of when running within a Docker environment. ### Built With