-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
476 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
name: Go | ||
|
||
on: | ||
push: | ||
branches: [ "main", "develop" ] | ||
pull_request: | ||
branches: [ "main", "develop" ] | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
strategy: | ||
matrix: | ||
go-version: [ '1.18.x', '1.19.x', '1.20.x' ] | ||
|
||
steps: | ||
- uses: actions/checkout@v3 | ||
- name: Setup Go ${{ matrix.go-version }} | ||
uses: actions/setup-go@v3 | ||
with: | ||
go-version: ${{ matrix.go-version }} | ||
|
||
- name: Display Go version | ||
run: go version | ||
|
||
- name: Install tparse | ||
run: go install github.com/mfridman/tparse@latest | ||
|
||
- name: Run Test | ||
run: go test -race -count=1 -timeout 30s -coverprofile=coverage.txt -covermode=atomic -json ./... | tparse -all -format markdown |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,3 +19,6 @@ | |
|
||
# Go workspace file | ||
go.work | ||
|
||
## Editor | ||
.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,54 @@ | ||
# httpgraceful | ||
# httpgraceful | ||
|
||
A wrapper for `http.Server` that enables graceful shutdown handling. | ||
|
||
## Installation | ||
|
||
```shell | ||
go get github.com/pkg-id/httpgraceful | ||
``` | ||
|
||
## Example | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
"github.com/pkg-id/httpgraceful" | ||
) | ||
|
||
func main() { | ||
mux := http.NewServeMux() | ||
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { | ||
_, _ = io.WriteString(w, "Hello, World") | ||
}) | ||
|
||
server := &http.Server{ | ||
Addr: fmt.Sprintf("0.0.0.0:%d", 8080), | ||
Handler: mux, | ||
} | ||
|
||
gs := httpgraceful.WrapServer(server) | ||
if err := gs.ListenAndServe(); err != nil { | ||
panic(err) | ||
} | ||
} | ||
``` | ||
|
||
When you wrap your server with `httpgraceful.WrapServer`, the `ListenAndServe` method will keep the server running until it receives either the `os.Interrupt` signal or the `syscall.SIGTERM` signal. | ||
At that point, the graceful shutdown process will be initiated, waiting for all active connections to be closed. If there are still active connections after the wait timeout is exceeded, the server will be force closed. | ||
The default wait timeout is 5 seconds. | ||
|
||
However, you can customize the timeout and the signal as shown in the example below: | ||
|
||
```go | ||
gs := httpgraceful.WrapServer( | ||
server, | ||
httpgraceful.WithWaitTimeout(100*time.Millisecond), | ||
httpgraceful.WithSignal(syscall.SIGTERM) | ||
) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
"github.com/pkg-id/httpgraceful" | ||
) | ||
|
||
func main() { | ||
mux := http.NewServeMux() | ||
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { | ||
_, _ = io.WriteString(w, "Hello, World") | ||
}) | ||
|
||
server := &http.Server{ | ||
Addr: fmt.Sprintf("0.0.0.0:%d", 8080), | ||
Handler: mux, | ||
} | ||
|
||
gs := httpgraceful.WrapServer(server) | ||
if err := gs.ListenAndServe(); err != nil { | ||
panic(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/pkg-id/httpgraceful | ||
|
||
go 1.20 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package httpgraceful | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"os" | ||
"os/signal" | ||
"syscall" | ||
"time" | ||
) | ||
|
||
// Server is a contract for server that can be started and shutdown gracefully. | ||
type Server interface { | ||
// ListenAndServe starts listening and serving the server. | ||
// This method should block until shutdown signal received or failed to start. | ||
ListenAndServe() error | ||
|
||
// Shutdown gracefully shuts down the server, it will wait for all active connections to be closed. | ||
Shutdown(ctx context.Context) error | ||
|
||
// Close force closes the server. | ||
// Close is called when Shutdown timeout exceeded. | ||
Close() error | ||
} | ||
|
||
// gracefulServer is a wrapper of http.Server that can be shutdown gracefully. | ||
type gracefulServer struct { | ||
Server | ||
signalListener chan os.Signal | ||
waitTimeout time.Duration | ||
shutdownDone chan struct{} | ||
} | ||
|
||
// Option is a function to configure gracefulServer. | ||
type Option func(*gracefulServer) | ||
|
||
func (f Option) apply(gs *gracefulServer) { f(gs) } | ||
|
||
// WithSignals sets the signals that will be listened to initiate shutdown. | ||
func WithSignals(signals ...os.Signal) Option { | ||
return func(s *gracefulServer) { | ||
signalListener := make(chan os.Signal, 1) | ||
signal.Notify(signalListener, signals...) | ||
s.signalListener = signalListener | ||
} | ||
} | ||
|
||
// WithWaitTimeout sets the timeout for waiting active connections to be closed. | ||
func WithWaitTimeout(timeout time.Duration) Option { | ||
return func(s *gracefulServer) { | ||
s.waitTimeout = timeout | ||
} | ||
} | ||
|
||
// WrapServer wraps a Server with graceful shutdown capability. | ||
// It will listen to SIGINT and SIGTERM signals to initiate shutdown and | ||
// wait for all active connections to be closed. If still active connections | ||
// after wait timeout exceeded, it will force close the server. The default | ||
// wait timeout is 5 seconds. | ||
func WrapServer(server Server, opts ...Option) Server { | ||
gs := gracefulServer{ | ||
Server: server, | ||
shutdownDone: make(chan struct{}), | ||
} | ||
|
||
for _, opt := range opts { | ||
opt.apply(&gs) | ||
} | ||
|
||
if gs.signalListener == nil { | ||
WithSignals(syscall.SIGTERM, syscall.SIGINT).apply(&gs) | ||
} | ||
|
||
if gs.waitTimeout <= 0 { | ||
WithWaitTimeout(5 * time.Second).apply(&gs) | ||
} | ||
|
||
return &gs | ||
} | ||
|
||
// ListenAndServe starts listening and serving the server gracefully. | ||
func (s *gracefulServer) ListenAndServe() error { | ||
serverErr := make(chan error, 1) | ||
shutdownCompleted := make(chan struct{}) | ||
|
||
// start the original server. | ||
go func() { | ||
err := s.Server.ListenAndServe() | ||
// if shutdown succeeded, http.ErrServerClosed will be returned. | ||
if errors.Is(err, http.ErrServerClosed) { | ||
shutdownCompleted <- struct{}{} | ||
} else { | ||
// only send error if it's not http.ErrServerClosed. | ||
serverErr <- err | ||
} | ||
}() | ||
|
||
// block until signalListener received or mux failed to start. | ||
select { | ||
case sig := <-s.signalListener: | ||
ctx, cancel := context.WithTimeout(context.Background(), s.waitTimeout) | ||
defer cancel() | ||
|
||
err := s.Server.Shutdown(ctx) | ||
// only force shutdown if deadline exceeded. | ||
if errors.Is(err, context.DeadlineExceeded) { | ||
closeErr := s.Server.Close() | ||
if closeErr != nil { | ||
return fmt.Errorf("deadline exceeded, force shutdown failed: %w", closeErr) | ||
} | ||
// force shutdown succeeded. | ||
return nil | ||
} | ||
|
||
// unexpected error. | ||
if err != nil { | ||
return fmt.Errorf("shutdown failed, signal: %s: %w", sig, err) | ||
} | ||
|
||
// make sure shutdown completed. | ||
<-shutdownCompleted | ||
return nil | ||
case err := <-serverErr: | ||
return fmt.Errorf("server failed to start: %w", err) | ||
} | ||
} | ||
|
||
// SendSignal sends a signal to the server, if signal is one of registered signals, | ||
// shutdown will be triggered. | ||
// this useful for testing. | ||
func (s *gracefulServer) SendSignal(sig os.Signal) { | ||
if s.signalListener != nil { | ||
s.signalListener <- sig | ||
} | ||
} |
Oops, something went wrong.