Skip to content

Commit eeb00d3

Browse files
committed
Initial commit. A lot of TODO to do.
1 parent f4fd895 commit eeb00d3

File tree

9 files changed

+700
-0
lines changed

9 files changed

+700
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@
1212

1313
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
1414
.glide/
15+
16+
# Visual Studio Code configurations
17+
.vscode/

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
11
# zerodt
22
Zero downtime and graceful shutdown in one line of code
3+
4+
- TODO: errors in a separate channel
5+
- TODO: prestart handler
6+
- TODO: build on Win
7+
- TODO: tests
8+
- TODO: travis
9+
- TODO: doc.go #godoc
10+
- TODO: changelog like here https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md

app.go

+219
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// +build linux darwin
2+
3+
package zerodt
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"net"
9+
"net/http"
10+
"os"
11+
"os/signal"
12+
"path/filepath"
13+
"sync"
14+
"syscall"
15+
)
16+
17+
var (
18+
// Get original working directory just on start to reduce
19+
// possibility of calling `os.Chdir` by somebody.
20+
originalWD, _ = os.Getwd()
21+
)
22+
23+
// App TODO
24+
type App struct {
25+
servers []*http.Server
26+
e *exchange
27+
}
28+
29+
// NewApp TODO
30+
func NewApp(servers ...*http.Server) *App {
31+
e, err := newExchange()
32+
if err != nil {
33+
panic(err)
34+
}
35+
logger.Printf("ZeroDT: started for pid=%d with inherited=%s", os.Getpid(), formatInherited(e))
36+
return &App{servers, e}
37+
}
38+
39+
// synchronous
40+
func (a *App) shutdown() {
41+
var wg sync.WaitGroup
42+
wg.Add(len(a.servers))
43+
44+
// Shutdown all servers in parallel
45+
for _, s := range a.servers {
46+
go func(s *http.Server) {
47+
defer wg.Done()
48+
s.Shutdown(context.Background())
49+
}(s)
50+
}
51+
52+
wg.Wait()
53+
}
54+
55+
func (a *App) interceptSignals(ctx context.Context, wg *sync.WaitGroup) {
56+
defer wg.Done()
57+
58+
signals := make(chan os.Signal, 1)
59+
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT, syscall.SIGUSR2)
60+
defer signal.Stop(signals)
61+
62+
for {
63+
select {
64+
// signal
65+
case s := <-signals:
66+
switch s {
67+
case syscall.SIGINT, syscall.SIGTERM:
68+
logger.Printf("ZeroDT: termination signal, shutdown servers...")
69+
a.shutdown()
70+
return
71+
72+
case syscall.SIGUSR2:
73+
logger.Printf("ZeroDT: activation signal, starting another process...")
74+
pid, err := a.startAnotherProcess()
75+
if err != nil {
76+
// TODO: send to error channel
77+
}
78+
logger.Printf("ZeroDT: child '%d' successfully started", pid)
79+
}
80+
// cancel, no need to shutdown servers
81+
case <-ctx.Done():
82+
return
83+
}
84+
}
85+
}
86+
87+
func (a *App) killParent() {
88+
if !a.e.didInherit() {
89+
return
90+
}
91+
// If it's systemd - keep it alive. This is possible when
92+
// 'socket activation' take place.
93+
if os.Getppid() == 1 {
94+
return
95+
}
96+
97+
logger.Printf("ZeroDT: send termination signal to the parent with pid=%d", os.Getppid())
98+
err := syscall.Kill(os.Getppid(), syscall.SIGTERM)
99+
if err != nil {
100+
// It does not allowed to run both binaries.
101+
panic(err)
102+
}
103+
}
104+
105+
// Serve TODO
106+
func (a *App) Serve() error {
107+
var srvWG sync.WaitGroup
108+
srvWG.Add(len(a.servers))
109+
110+
var sigWG sync.WaitGroup
111+
sigWG.Add(1)
112+
113+
sigCtx, cancelFunc := context.WithCancel(context.Background())
114+
go a.interceptSignals(sigCtx, &sigWG)
115+
116+
for _, s := range a.servers {
117+
go func(s *http.Server) {
118+
defer srvWG.Done()
119+
120+
l, err := createOrAcquireListener(a.e, "tcp", s.Addr)
121+
if err != nil {
122+
// TODO: error channel
123+
logger.Printf("ZeroDT: failed to listen on '%v' with %v", s.Addr, err)
124+
return
125+
}
126+
127+
err = s.Serve(tcpKeepAliveListener{l})
128+
// Serve always returns a non-nil error
129+
logger.Printf("ZeroDT: server '%v' is finished with %v", s.Addr, err)
130+
}(s)
131+
}
132+
133+
// Kill a parent in case the process was started with inherited sockets.
134+
a.killParent()
135+
136+
// Wait for all server's at first. They may fail or be stopped by
137+
// calling 'Shutdown'.
138+
srvWG.Wait()
139+
// Stop intercepting signals. No need to shutdown servers in this case.
140+
cancelFunc()
141+
// Wait for the last goroutine
142+
sigWG.Wait()
143+
144+
return nil
145+
}
146+
147+
func (a *App) startAnotherProcess() (int, error) {
148+
// Executable returns the path name for the executable that
149+
// started the current process.
150+
path, err := os.Executable()
151+
if err != nil {
152+
return -1, err
153+
}
154+
// EvalSymlinks returns the path name after the evaluation
155+
// of any symbolic links.
156+
path, err = filepath.EvalSymlinks(path)
157+
if err != nil {
158+
return -1, err
159+
}
160+
161+
// Set activation environment variables for a child process.
162+
env := os.Environ()
163+
env = append(env, fmt.Sprintf("%s=%d", envListenFds, 1))
164+
env = append(env, fmt.Sprintf("%s=%d", envListenPid, listenPidDefault))
165+
166+
// Start the original executable with the original working directory
167+
process, err := os.StartProcess(path, os.Args, &os.ProcAttr{
168+
Dir: originalWD,
169+
Env: env,
170+
Files: append([]*os.File{os.Stdin, os.Stdout, os.Stderr}, a.e.activeFiles()...),
171+
})
172+
if err != nil {
173+
return -1, err
174+
}
175+
176+
return process.Pid, nil
177+
}
178+
179+
// createOrAcquireListener is a helper function that acquires an inherited
180+
// listener or creates a new one and adds it an exchange
181+
func createOrAcquireListener(e *exchange, netStr, addrStr string) (*net.TCPListener, error) {
182+
addr, err := net.ResolveTCPAddr(netStr, addrStr)
183+
if err != nil {
184+
return nil, err
185+
}
186+
187+
// Try to acquire one of inherited listeners.
188+
l := e.acquireListener(addr)
189+
if l != nil {
190+
logger.Printf("ZeroDT: listener with TCPAddr:`%v` has successfully acquired", addr)
191+
return l, nil
192+
}
193+
194+
// Create a new TCP listener and add it to an exchange.
195+
l, err = net.ListenTCP(netStr, addr)
196+
if err != nil {
197+
return nil, err
198+
}
199+
err = e.addListener(l)
200+
if err != nil {
201+
l.Close()
202+
return nil, err
203+
}
204+
logger.Printf("ZeroDT: listener with TCPAddr:`%v` has successfully created", addr)
205+
206+
return l, nil
207+
}
208+
209+
func formatInherited(e *exchange) string {
210+
result := "["
211+
for i, pr := range e.inherited {
212+
if i != 0 {
213+
result += ","
214+
}
215+
result += fmt.Sprintf("%v", pr.l.Addr())
216+
}
217+
result += "]"
218+
return result
219+
}

app_windows.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package zerodt
2+
3+
import (
4+
"net/http"
5+
)
6+
7+
// TODO: use os.Interrupt
8+
9+
// App TODO
10+
type App struct {
11+
servers []*http.Server
12+
}
13+
14+
// NewApp TODO
15+
func NewApp(servers ...*http.Server) *App {
16+
logger.Printf("ZeroDT: started for pid=%d without inherited")
17+
return &App{servers}
18+
}
19+
20+
// Serve TODO
21+
func (a *App) Serve() error {
22+
panic("Implement")
23+
}

examples/test/main.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"os"
7+
"ssgreg/zerodt"
8+
9+
"github.com/Sirupsen/logrus"
10+
"github.com/gorilla/mux"
11+
)
12+
13+
func handler(w http.ResponseWriter, r *http.Request) {
14+
fmt.Fprintf(w, "%v", os.Getpid())
15+
// time.Sleep(time.Second * 20)
16+
fmt.Println("ready!")
17+
}
18+
19+
func main() {
20+
zerodt.SetLogger(logrus.StandardLogger())
21+
22+
r := mux.NewRouter()
23+
r.Methods("GET").HandlerFunc(handler)
24+
25+
a := zerodt.NewApp(&http.Server{Addr: "127.0.0.1:8081", Handler: r})
26+
a.Serve()
27+
28+
zerodt.Greg()
29+
fmt.Printf("That's all Folks!")
30+
}

0 commit comments

Comments
 (0)