-
Notifications
You must be signed in to change notification settings - Fork 1
/
server.go
116 lines (98 loc) · 2.91 KB
/
server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package xtemplate
import (
"context"
"log/slog"
"net/http"
"sync"
"sync/atomic"
"time"
)
// Server is a configured, *reloadable*, xtemplate request handler ready to
// execute templates and serve static files in response to http requests. It
// manages an [Instance] and allows you to reload template files with the same
// config by calling `server.Reload()`. If successful, Reload atomically swaps
// the old Instance with the new Instance so subsequent requests are handled by
// the new instance, and any outstanding requests still being served by the old
// Instance can continue to completion. The old instance's Config.Ctx is also
// cancelled.
//
// The only way to create a valid *Server is to call [Config.Server].
type Server struct {
instance atomic.Pointer[Instance]
cancel func()
mutex sync.Mutex
config Config
}
// Build creates a new Server from an xtemplate.Config.
func (config Config) Server(cfgs ...Option) (*Server, error) {
if _, err := config.Defaults().Options(cfgs...); err != nil {
return nil, err
}
config.Logger = config.Logger.WithGroup("xtemplate")
server := &Server{
config: config,
}
err := server.Reload()
if err != nil {
return nil, err
}
return server, nil
}
// Instance returns the current [Instance]. After calling Reload, previous calls
// to Instance may be stale.
func (x *Server) Instance() *Instance {
return x.instance.Load()
}
// Serve opens a net listener on `listen_addr` and serves requests from it.
func (x *Server) Serve(listen_addr string) error {
x.config.Logger.Info("starting server")
return http.ListenAndServe(listen_addr, x.Handler())
}
// Handler returns a `http.Handler` that always routes new requests to the
// current Instance.
func (x *Server) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
x.Instance().ServeHTTP(w, r)
})
}
// Reload creates a new Instance from the config and swaps it with the
// current instance if successful, otherwise returns the error.
func (x *Server) Reload(cfgs ...Option) error {
start := time.Now()
x.mutex.Lock()
defer x.mutex.Unlock()
log := x.config.Logger.WithGroup("reload")
old := x.instance.Load()
if old != nil {
log = log.With(slog.Int64("old_id", old.id))
}
var newcancel func()
var new_ *Instance
{
var err error
config := x.config
config.Ctx, newcancel = context.WithCancel(x.config.Ctx)
new_, _, _, err = config.Instance(cfgs...)
if err != nil {
newcancel()
log.Info("failed to load", slog.Any("error", err), slog.Duration("rebuild_time", time.Since(start)))
return err
}
}
x.instance.CompareAndSwap(old, new_)
if x.cancel != nil {
x.cancel()
}
x.cancel = newcancel
log.Info("rebuild succeeded", slog.Int64("new_id", new_.id), slog.Duration("rebuild_time", time.Since(start)))
return nil
}
func (x *Server) Stop() {
x.mutex.Lock()
defer x.mutex.Unlock()
if x.cancel != nil {
x.cancel()
}
x.cancel = nil
x.instance.Store(nil)
}