Test and wait on the availability of remote resources before proceeding with your application logic.
waitfor is a Go library that provides a robust way to test and wait for remote resource availability with built-in retry logic, exponential backoff, and extensible resource support. It's particularly useful for ensuring dependencies are ready before starting applications or running critical operations.
- Features
- Installation
- Supported Resources
- Resource URLs
- Quick Start
- API Reference
- Advanced Usage
- Error Handling
- Best Practices
- Troubleshooting
- Contributing
- License
- Parallel Testing: Test multiple resources concurrently for faster startup times
- Exponential Backoff: Smart retry logic that prevents overwhelming resources
- Extensible Architecture: Support for custom resource types through a plugin system
- Context Support: Full context support for cancellation and timeouts
- Zero Dependencies: Minimal external dependencies for easy integration
- Production Ready: Battle-tested retry logic with configurable parameters
Install waitfor using Go modules:
go get github.com/go-waitfor/waitforFor specific resource types, install the corresponding packages:
# Database resources
go get github.com/go-waitfor/waitfor-postgres
go get github.com/go-waitfor/waitfor-mysql
# File system and process resources
go get github.com/go-waitfor/waitfor-fs
go get github.com/go-waitfor/waitfor-proc
# HTTP resources
go get github.com/go-waitfor/waitfor-http
# NoSQL databases
go get github.com/go-waitfor/waitfor-mongodbThe following resource types are available through separate packages:
| Resource Type | Package | URL Schemes | Description |
|---|---|---|---|
| File System | waitfor-fs |
file:// |
Test file/directory existence |
| OS Process | waitfor-proc |
proc:// |
Test process availability |
| HTTP(S) Endpoint | waitfor-http |
http://, https:// |
Test HTTP endpoint availability |
| PostgreSQL | waitfor-postgres |
postgres:// |
Test PostgreSQL database connectivity |
| MySQL/MariaDB | waitfor-mysql |
mysql://, mariadb:// |
Test MySQL/MariaDB connectivity |
| MongoDB | waitfor-mongodb |
mongodb:// |
Test MongoDB connectivity |
Resource locations are specified using standard URL format with scheme-specific parameters:
Format: scheme://[user[:password]@]host[:port][/path][?query]
Examples:
file://./myfile- Local file pathfile:///absolute/path/to/file- Absolute file pathhttp://localhost:8080/health- HTTP health check endpointhttps://api.example.com/status- HTTPS endpoint with pathpostgres://user:password@localhost:5432/mydb- PostgreSQL databasemysql://user:password@localhost:3306/mydb- MySQL databasemongodb://localhost:27017/mydb- MongoDB databaseproc://nginx- Process by name
Use waitfor to test if resources are available before proceeding:
package main
import (
"context"
"fmt"
"github.com/go-waitfor/waitfor"
"github.com/go-waitfor/waitfor-postgres"
"os"
)
func main() {
runner := waitfor.New(postgres.Use())
err := runner.Test(
context.Background(),
[]string{"postgres://locahost:5432/mydb?user=user&password=test"},
waitfor.WithAttempts(5),
)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}waitfor can ensure dependencies are ready before executing external commands, making it perfect for application startup scripts and deployment scenarios:
package main
import (
"context"
"fmt"
"github.com/go-waitfor/waitfor"
"github.com/go-waitfor/waitfor-postgres"
"os"
)
func main() {
runner := waitfor.New(postgres.Use())
program := waitfor.Program{
Executable: "myapp",
Args: []string{"--database", "postgres://locahost:5432/mydb?user=user&password=test"},
Resources: []string{"postgres://locahost:5432/mydb?user=user&password=test"},
}
out, err := runner.Run(
context.Background(),
program,
waitfor.WithAttempts(5),
)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println(string(out))
}waitfor supports custom resource types through its extensible registry system. You can register your own resource checkers:
package main
import (
"context"
"database/sql"
"fmt"
_ "github.com/lib/pq"
"github.com/go-waitfor/waitfor"
"net/url"
"os"
"strings"
)
const PostgresScheme = "postgres"
type PostgresResource struct {
url *url.URL
}
func (p *PostgresResource) Test(ctx context.Context) error {
db, err := sql.Open(p.url.Scheme, strings.TrimPrefix(p.url.String(), PostgresScheme+"://"))
if err != nil {
return err
}
defer db.Close()
return db.PingContext(ctx)
}
func main() {
runner := waitfor.New(waitfor.ResourceConfig{
Scheme: []string{PostgresScheme},
Factory: func(u *url.URL) (waitfor.Resource, error) {
return &PostgresResource{u}, nil
},
})
err := runner.Test(
context.Background(),
[]string{"postgres://locahost:5432/mydb?user=user&password=test"},
waitfor.WithAttempts(5),
)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}The main entry point for testing resources and running programs.
type Runner struct {
// registry contains all registered resource factories
}Defines an external command with its dependencies.
type Program struct {
Executable string // Command to execute
Args []string // Command arguments
Resources []string // Dependencies to test before execution
}Interface that all resource types must implement.
type Resource interface {
Test(ctx context.Context) error
}Configuration for registering resource types.
type ResourceConfig struct {
Scheme []string // URL schemes this resource handles
Factory ResourceFactory // Factory function to create resource instances
}Function signature for creating resource instances.
type ResourceFactory func(u *url.URL) (Resource, error)Creates a new Runner with the specified resource configurations.
func New(configurators ...ResourceConfig) *RunnerParameters:
configurators: Variable number of ResourceConfig instances to register
Returns: A new Runner instance
Example:
runner := waitfor.New(postgres.Use(), http.Use())Tests the availability of specified resources.
func (r *Runner) Test(ctx context.Context, resources []string, setters ...Option) errorParameters:
ctx: Context for cancellation and timeout controlresources: Slice of resource URLs to testsetters: Configuration options (WithAttempts, WithInterval, etc.)
Returns: Error if any resource is unavailable after all retry attempts
Tests resources and executes a program if all resources are available.
func (r *Runner) Run(ctx context.Context, program Program, setters ...Option) ([]byte, error)Parameters:
ctx: Context for cancellation and timeout controlprogram: Program configuration with executable, args, and resource dependenciessetters: Configuration options
Returns: Combined stdout/stderr output and error
Returns the resource registry for advanced usage.
func (r *Runner) Resources() *RegistryReturns: The internal Registry instance
Helper function to convert module functions to ResourceConfig.
func Use(mod Module) ResourceConfigParameters:
mod: Module function that returns schemes and factory
Returns: ResourceConfig ready for use with New()
All test and run operations accept configuration options to customize behavior:
Sets the maximum number of retry attempts.
func WithAttempts(attempts uint64) OptionDefault: 5 attempts
Example:
err := runner.Test(ctx, resources, waitfor.WithAttempts(10))Sets the initial retry interval in seconds.
func WithInterval(interval uint64) OptionDefault: 5 seconds
Example:
err := runner.Test(ctx, resources, waitfor.WithInterval(2))Sets the maximum retry interval for exponential backoff in seconds.
func WithMaxInterval(interval uint64) OptionDefault: 60 seconds
Example:
err := runner.Test(ctx, resources, waitfor.WithMaxInterval(120))Options can be combined for fine-tuned control:
err := runner.Test(
ctx,
resources,
waitfor.WithAttempts(15),
waitfor.WithInterval(1),
waitfor.WithMaxInterval(30),
)Test different types of resources simultaneously:
package main
import (
"context"
"fmt"
"github.com/go-waitfor/waitfor"
"github.com/go-waitfor/waitfor-postgres"
"github.com/go-waitfor/waitfor-http"
"github.com/go-waitfor/waitfor-fs"
)
func main() {
runner := waitfor.New(
postgres.Use(),
http.Use(),
fs.Use(),
)
resources := []string{
"postgres://user:pass@localhost:5432/mydb",
"http://localhost:8080/health",
"file://./config.json",
}
err := runner.Test(context.Background(), resources)
if err != nil {
fmt.Printf("Dependencies not ready: %v\n", err)
return
}
fmt.Println("All dependencies are ready!")
}Use context for timeout control and cancellation:
package main
import (
"context"
"time"
"github.com/go-waitfor/waitfor"
"github.com/go-waitfor/waitfor-postgres"
)
func main() {
runner := waitfor.New(postgres.Use())
// Set a 30-second timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err := runner.Test(
ctx,
[]string{"postgres://localhost:5432/mydb"},
waitfor.WithAttempts(10),
)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("Timeout waiting for resources")
} else {
fmt.Printf("Resource test failed: %v\n", err)
}
return
}
fmt.Println("Resources are ready!")
}Register resources at runtime:
package main
import (
"context"
"net/url"
"github.com/go-waitfor/waitfor"
)
func main() {
runner := waitfor.New()
// Register a custom resource type
err := runner.Resources().Register("custom", func(u *url.URL) (waitfor.Resource, error) {
return &MyCustomResource{url: u}, nil
})
if err != nil {
panic(err)
}
// Now you can use the custom resource
err = runner.Test(context.Background(), []string{"custom://example.com"})
// ... handle error
}waitfor provides specific error types for different failure scenarios:
ErrWait: Returned when resources are not available after all retry attemptsErrInvalidArgument: Returned for invalid input parametersErrResourceNotFound: Returned when a resource type is not registeredErrResourceAlreadyRegistered: Returned when trying to register a resource type that already exists
err := runner.Test(ctx, resources)
if err != nil {
// Check if it's a waitfor-specific error
if strings.Contains(err.Error(), waitfor.ErrWait.Error()) {
fmt.Println("Resources are not available after retries")
// Maybe wait longer or use different configuration
} else {
fmt.Printf("Configuration or setup error: %v\n", err)
}
return
}ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
err := runner.Test(ctx, resources)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("Overall timeout exceeded")
} else {
fmt.Println("Resource-specific failure:", err)
}
}Set timeouts based on your application's requirements:
// For quick startup (development)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
// For production startup
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)Adjust retry parameters based on resource characteristics:
// For fast resources (local files, processes)
waitfor.WithAttempts(3),
waitfor.WithInterval(1),
waitfor.WithMaxInterval(5)
// For slow resources (remote databases, external APIs)
waitfor.WithAttempts(15),
waitfor.WithInterval(5),
waitfor.WithMaxInterval(60)Test related resources together for better error reporting:
// Test database cluster
databaseResources := []string{
"postgres://localhost:5432/primary",
"postgres://localhost:5433/replica1",
"postgres://localhost:5434/replica2",
}
// Test web services
webResources := []string{
"http://localhost:8080/health",
"http://localhost:8081/ready",
}
// Test separately for clearer error messages
if err := runner.Test(ctx, databaseResources); err != nil {
log.Fatal("Database cluster not ready:", err)
}
if err := runner.Test(ctx, webResources); err != nil {
log.Fatal("Web services not ready:", err)
}Integrate with structured logging for better observability:
logger := log.With().Str("component", "waitfor").Logger()
logger.Info().Msg("Starting dependency checks")
err := runner.Test(ctx, resources)
if err != nil {
logger.Error().Err(err).Msg("Dependencies not ready")
return
}
logger.Info().Msg("All dependencies ready")This error occurs when you try to use a resource type that hasn't been registered.
Solution: Import and register the appropriate resource package:
import "github.com/go-waitfor/waitfor-postgres"
runner := waitfor.New(postgres.Use())This indicates that resources were not available after all retry attempts.
Solutions:
- Increase retry attempts:
waitfor.WithAttempts(20) - Increase retry intervals:
waitfor.WithMaxInterval(120) - Check if the resource URL is correct
- Verify the resource is actually running and accessible
These are typically network-related issues.
Solutions:
- Verify the resource is running:
telnet hostname port - Check firewall rules and network connectivity
- Verify DNS resolution for hostnames
- Use IP addresses instead of hostnames if DNS is an issue
Enable verbose error reporting for troubleshooting:
err := runner.Test(ctx, resources)
if err != nil {
fmt.Printf("Detailed error: %+v\n", err)
// Test each resource individually to isolate issues
for _, resource := range resources {
if testErr := runner.Test(ctx, []string{resource}); testErr != nil {
fmt.Printf("Failed resource: %s - %v\n", resource, testErr)
}
}
}By default, waitfor tests all resources in parallel for faster execution. For resource-constrained environments, consider testing sequentially:
// Test resources one by one
for _, resource := range resources {
err := runner.Test(ctx, []string{resource})
if err != nil {
return fmt.Errorf("resource %s failed: %w", resource, err)
}
}When testing many resources, be aware of goroutine overhead. For very large numbers of resources (100+), consider batching:
const batchSize = 10
for i := 0; i < len(resources); i += batchSize {
end := i + batchSize
if end > len(resources) {
end = len(resources)
}
batch := resources[i:end]
err := runner.Test(ctx, batch)
if err != nil {
return err
}
}We welcome contributions! Here's how you can help:
- Create a new repository following the pattern
waitfor-{resourcetype} - Implement the
Resourceinterface:type MyResource struct { url *url.URL } func (r *MyResource) Test(ctx context.Context) error { // Implement your resource test logic return nil }
- Provide a
Use()function:func Use() waitfor.ResourceConfig { return waitfor.ResourceConfig{ Scheme: []string{"myscheme"}, Factory: func(u *url.URL) (waitfor.Resource, error) { return &MyResource{url: u}, nil }, } }
When reporting issues, please include:
- Go version
waitforversion- Resource types and URLs being tested
- Complete error messages
- Minimal reproduction case
git clone https://github.com/go-waitfor/waitfor.git
cd waitfor
go mod tidy
go test ./...This project is licensed under the MIT License. See the LICENSE file for details.