Skip to content

Commit cfa2514

Browse files
committed
feat(server): add GracefulServer with tests to the project
Fixes: #5
1 parent d5420d2 commit cfa2514

File tree

2 files changed

+205
-0
lines changed

2 files changed

+205
-0
lines changed

server.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright 2023-2025 Flavio Garcia
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package httpok
16+
17+
import (
18+
"context"
19+
"log"
20+
"net/http"
21+
"os"
22+
"os/signal"
23+
"syscall"
24+
)
25+
26+
// RunLogger defines the interface for logging used within the GracefulServer's
27+
// Run method.
28+
// It provides methods for formatted printing and fatal errors which halt the
29+
// program.
30+
type RunLogger interface {
31+
Printf(format string, v ...interface{})
32+
Fatalf(format string, v ...interface{})
33+
}
34+
35+
// newSignalChan creates a channel that listens for specified signals or
36+
// default signals if none are provided.
37+
// It returns a channel that receives these signals. This function is used
38+
// internally by [GracefulServer.Run]
39+
func newSignalChan(sig ...os.Signal) chan os.Signal {
40+
if len(sig) == 0 {
41+
sig = []os.Signal{
42+
syscall.SIGINT,
43+
syscall.SIGHUP,
44+
syscall.SIGQUIT,
45+
syscall.SIGTERM,
46+
syscall.SIGKILL,
47+
}
48+
}
49+
c := make(chan os.Signal, 1)
50+
signal.Notify(c, sig...)
51+
return c
52+
}
53+
54+
// GracefulServer combines an HTTP server with a context for graceful shutdown
55+
// handling.
56+
type GracefulServer struct {
57+
Name string
58+
*http.Server
59+
context.Context
60+
RunLogger
61+
}
62+
63+
// Run starts the HTTP server in a goroutine and listens for termination
64+
// signals to gracefully shut down.
65+
// It takes optional signals to listen for; if none are provided, it uses
66+
// default signals.
67+
func (s *GracefulServer) Run(sig ...os.Signal) {
68+
logger := s.RunLogger
69+
if logger == nil {
70+
logger = &basicLogger{}
71+
}
72+
73+
go func() {
74+
if err := s.ListenAndServe(); err != http.ErrServerClosed {
75+
logger.Fatalf("server %s HTTP ListenAndServe error: %v", s.Name, err)
76+
}
77+
}()
78+
79+
ctx := context.Background()
80+
ctx, cancel := context.WithCancel(ctx)
81+
82+
log.Printf("server %s running at %s", s.Name, s.Addr)
83+
84+
c := newSignalChan(sig...)
85+
86+
defer func() {
87+
signal.Stop(c)
88+
logger.Printf("%s shutdown gracefully", s.Name)
89+
cancel()
90+
}()
91+
92+
select {
93+
case sig := <-c:
94+
logger.Printf("shutting down %s due to signal %s", s.Name, sig)
95+
cancel()
96+
case <-ctx.Done():
97+
logger.Printf("shutting down %s", s.Name)
98+
}
99+
}
100+
101+
// basicLogger implements the RunLogger interface using Go's standard log
102+
// package.
103+
type basicLogger struct{}
104+
105+
// Printf logs a formatted message using the standard log package's Printf
106+
// method.
107+
func (l *basicLogger) Printf(format string, v ...interface{}) {
108+
log.Printf(format, v...)
109+
}
110+
111+
// Fatalf logs a formatted message and then terminates the program using the
112+
// standard log package's Fatalf method.
113+
func (l *basicLogger) Fatalf(format string, v ...interface{}) {
114+
log.Fatalf(format, v...)
115+
}

server_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2023-2025 Flavio Garcia
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package httpok
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"log"
21+
"net"
22+
"net/http"
23+
"os"
24+
"os/signal"
25+
"syscall"
26+
"testing"
27+
"time"
28+
29+
"github.com/stretchr/testify/assert"
30+
)
31+
32+
// Helper function to get a free port
33+
func getFreePort() (int, error) {
34+
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
35+
if err != nil {
36+
return 0, err
37+
}
38+
39+
l, err := net.ListenTCP("tcp", addr)
40+
if err != nil {
41+
return 0, err
42+
}
43+
defer l.Close()
44+
return l.Addr().(*net.TCPAddr).Port, nil
45+
}
46+
47+
func TestGracefulServer(t *testing.T) {
48+
// Generate a random free port
49+
port, err := getFreePort()
50+
if err != nil {
51+
t.Fatalf("Failed to get free port: %v", err)
52+
}
53+
addr := fmt.Sprintf(":%d", port)
54+
55+
mux := http.NewServeMux()
56+
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
57+
log.Println("Request received")
58+
w.WriteHeader(http.StatusOK)
59+
})
60+
61+
srv := &http.Server{
62+
Addr: addr,
63+
Handler: mux,
64+
}
65+
66+
// Create GracefulServer with our custom server
67+
gs := &GracefulServer{
68+
Server: srv,
69+
Context: context.Background(),
70+
}
71+
72+
go func() {
73+
gs.Run()
74+
}()
75+
76+
time.Sleep(1 * time.Second)
77+
78+
resp, err := http.Get("http://localhost" + addr)
79+
if err != nil {
80+
t.Fatalf("failed to make request: %v", err)
81+
}
82+
assert.Equal(t, http.StatusOK, resp.StatusCode)
83+
resp.Body.Close()
84+
85+
// Send SIGTERM down the pipe
86+
c := make(chan os.Signal, 1)
87+
signal.Notify(c, syscall.SIGTERM)
88+
defer signal.Stop(c)
89+
c <- syscall.SIGTERM
90+
}

0 commit comments

Comments
 (0)