HTTP server refactor (#164)

* Rework current server
* Update call is blocking
* Run first update without blocking
This commit is contained in:
Quentin McGaw
2021-02-08 21:22:05 -05:00
committed by GitHub
parent ce1a447e0a
commit a86ddd42d1
10 changed files with 168 additions and 59 deletions

View File

@@ -150,9 +150,12 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
runner := update.NewRunner(db, updater, ipGetter, p.cooldown, logger, timeNow)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
forceUpdate := make(chan struct{})
go runner.Run(ctx, p.period, forceUpdate)
forceUpdate <- struct{}{}
go runner.Run(ctx, p.period)
// note: errors are logged within the goroutine,
// no need to collect the resulting errors.
go runner.ForceUpdate(ctx)
const healthServerAddr = "127.0.0.1:9999"
isHealthy := health.MakeIsHealthy(db, net.LookupIP, logger)
@@ -164,7 +167,7 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
address := fmt.Sprintf("0.0.0.0:%d", p.listeningPort)
uiDir := p.dir + "/ui"
server := server.New(address, p.rootURL, uiDir, db, logger.WithPrefix("http server: "), forceUpdate)
server := server.New(ctx, address, p.rootURL, uiDir, db, logger.WithPrefix("http server: "), runner)
wg.Add(1)
go server.Run(ctx, wg)
notify(1, fmt.Sprintf("Launched with %d records to watch", len(records)))

1
go.mod
View File

@@ -3,6 +3,7 @@ module github.com/qdm12/ddns-updater
go 1.15
require (
github.com/go-chi/chi v1.5.1
github.com/kyokomi/emoji v2.2.4+incompatible
github.com/ovh/go-ovh v1.1.0
github.com/qdm12/golibs v0.0.0-20210110211000-0a3a4541ae09

2
go.sum
View File

@@ -14,6 +14,8 @@ github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb h1:D4uzjWwKYQ5XnAvUbuvHW93esHg7F8N/OYeBBcJoTr0=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-chi/chi v1.5.1 h1:kfTK3Cxd/dkMu/rKs5ZceWYp+t5CtiE7vmaTv3LjC6w=
github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k=
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
github.com/go-openapi/analysis v0.17.0 h1:8JV+dzJJiK46XqGLqqLav8ZfEiJECp8jlOFhpiCdZ+0=
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=

35
internal/server/error.go Normal file
View File

@@ -0,0 +1,35 @@
package server
import (
"encoding/json"
"net/http"
)
type errJSONWrapper struct {
Error string `json:"error"`
}
func httpError(w http.ResponseWriter, status int, errString string) {
w.WriteHeader(status)
if errString == "" {
errString = http.StatusText(status)
}
body := errJSONWrapper{Error: errString}
_ = json.NewEncoder(w).Encode(body)
}
type errorsJSONWrapper struct {
Errors []string `json:"errors"`
}
func httpErrors(w http.ResponseWriter, status int, errors []error) {
w.WriteHeader(status)
errs := make([]string, len(errors))
for i := range errors {
errs[i] = errors[i].Error()
}
body := errorsJSONWrapper{Errors: errs}
_ = json.NewEncoder(w).Encode(body)
}

View File

@@ -0,0 +1,24 @@
package server
import (
"net/http"
"strings"
"github.com/go-chi/chi"
)
func fileServer(router chi.Router, path string, root http.FileSystem) {
if path != "/" && path[len(path)-1] != '/' {
router.Get(path,
http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
path += "/"
}
path += "*"
router.Get(path, func(w http.ResponseWriter, r *http.Request) {
rctx := chi.RouteContext(r.Context())
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
fs := http.StripPrefix(pathPrefix, http.FileServer(root))
fs.ServeHTTP(w, r)
})
}

View File

@@ -1,63 +1,50 @@
package server
import (
"fmt"
"context"
"net/http"
"strings"
"text/template"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/qdm12/ddns-updater/internal/data"
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/ddns-updater/internal/update"
)
func newHandler(rootURL, uiDir string, db data.Database,
logger logging.Logger, forceUpdate chan<- struct{}) http.Handler {
return &handler{
rootURL: rootURL,
uiDir: uiDir,
db: db,
logger: logger, // TODO log middleware
// TODO build information
timeNow: time.Now,
forceUpdate: forceUpdate,
}
}
type handler struct {
// Configuration
rootURL string
uiDir string
// Channels
forceUpdate chan<- struct{}
// Objects and mock functions
db data.Database
logger logging.Logger
type handlers struct {
ctx context.Context
// Objects
db data.Database
runner update.Runner
indexTemplate *template.Template
// Mockable functions
timeNow func() time.Time
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.logger.Info("HTTP %s %s", r.Method, r.RequestURI)
func newHandler(ctx context.Context, rootURL, uiDir string,
db data.Database, runner update.Runner) http.Handler {
indexTemplate := template.Must(template.ParseFiles(uiDir + "/index.html"))
r.RequestURI = strings.TrimPrefix(r.RequestURI, h.rootURL)
switch {
case r.Method == http.MethodGet && r.RequestURI == h.rootURL+"/":
t := template.Must(template.ParseFiles(h.uiDir + "/index.html"))
var htmlData models.HTMLData
for _, record := range h.db.SelectAll() {
row := record.HTML(h.timeNow())
htmlData.Rows = append(htmlData.Rows, row)
}
if err := t.ExecuteTemplate(w, "index.html", htmlData); err != nil {
h.logger.Warn(err)
fmt.Fprint(w, "An error occurred creating this webpage")
}
case r.Method == http.MethodGet && r.RequestURI == h.rootURL+"/update":
h.logger.Info("Update started manually")
h.forceUpdate <- struct{}{}
http.Redirect(w, r, h.rootURL, 301)
default:
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
handlers := &handlers{
ctx: ctx,
db: db,
indexTemplate: indexTemplate,
// TODO build information
timeNow: time.Now,
runner: runner,
}
router := chi.NewRouter()
router.Use(middleware.Logger, middleware.CleanPath)
router.Get(rootURL+"/", handlers.index)
router.Get(rootURL+"/update", handlers.update)
// UI file server for other paths
fileServer(router, rootURL+"/", http.Dir(uiDir))
return router
}

18
internal/server/index.go Normal file
View File

@@ -0,0 +1,18 @@
package server
import (
"net/http"
"github.com/qdm12/ddns-updater/internal/models"
)
func (h *handlers) index(w http.ResponseWriter, r *http.Request) {
var htmlData models.HTMLData
for _, record := range h.db.SelectAll() {
row := record.HTML(h.timeNow())
htmlData.Rows = append(htmlData.Rows, row)
}
if err := h.indexTemplate.ExecuteTemplate(w, "index.html", htmlData); err != nil {
httpError(w, http.StatusInternalServerError, "failed generating webpage: "+err.Error())
}
}

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/qdm12/ddns-updater/internal/data"
"github.com/qdm12/ddns-updater/internal/update"
"github.com/qdm12/golibs/logging"
)
@@ -20,9 +21,9 @@ type server struct {
handler http.Handler
}
func New(address, rootURL, uiDir string, db data.Database, logger logging.Logger,
forceUpdate chan<- struct{}) Server {
handler := newHandler(rootURL, uiDir, db, logger, forceUpdate)
func New(ctx context.Context, address, rootURL, uiDir string, db data.Database, logger logging.Logger,
runner update.Runner) Server {
handler := newHandler(ctx, rootURL, uiDir, db, runner)
return &server{
address: address,
logger: logger,

18
internal/server/update.go Normal file
View File

@@ -0,0 +1,18 @@
package server
import (
"net/http"
)
func (h *handlers) update(w http.ResponseWriter, r *http.Request) {
start := h.timeNow()
errors := h.runner.ForceUpdate(h.ctx)
duration := h.timeNow().Sub(start)
if len(errors) > 0 {
httpErrors(w, http.StatusInternalServerError, errors)
return
}
w.WriteHeader(http.StatusAccepted)
message := "All records updated successfully in " + duration.String()
_, _ = w.Write([]byte(message))
}

View File

@@ -13,12 +13,15 @@ import (
)
type Runner interface {
Run(ctx context.Context, period time.Duration, force <-chan struct{})
Run(ctx context.Context, period time.Duration)
ForceUpdate(ctx context.Context) []error
}
type runner struct {
db data.Database
updater Updater
force chan struct{}
forceResult chan []error
cooldown time.Duration
netLookupIP func(hostname string) ([]net.IP, error)
ipGetter IPGetter
@@ -31,6 +34,8 @@ func NewRunner(db data.Database, updater Updater, ipGetter IPGetter,
return &runner{
db: db,
updater: updater,
force: make(chan struct{}),
forceResult: make(chan []error),
cooldown: cooldown,
netLookupIP: net.LookupIP,
ipGetter: ipGetter,
@@ -201,7 +206,7 @@ func setInitialUpToDateStatus(db data.Database, id int, updateIP net.IP, now tim
return db.Update(id, record)
}
func (r *runner) updateNecessary(ctx context.Context) {
func (r *runner) updateNecessary(ctx context.Context) (errors []error) {
records := r.db.SelectAll()
doIP, doIPv4, doIPv6 := doIPVersion(records)
ip, ipv4, ipv6, errors := r.getNewIPs(ctx, doIP, doIPv4, doIPv6)
@@ -219,6 +224,7 @@ func (r *runner) updateNecessary(ctx context.Context) {
}
updateIP := getIPMatchingVersion(ip, ipv4, ipv6, record.Settings.IPVersion())
if err := setInitialUpToDateStatus(r.db, id, updateIP, now); err != nil {
errors = append(errors, err)
r.logger.Error(err)
}
}
@@ -227,22 +233,36 @@ func (r *runner) updateNecessary(ctx context.Context) {
updateIP := getIPMatchingVersion(ip, ipv4, ipv6, record.Settings.IPVersion())
r.logger.Info("Updating record %s to use %s", record.Settings, updateIP)
if err := r.updater.Update(ctx, id, updateIP, r.timeNow()); err != nil {
errors = append(errors, err)
r.logger.Error(err)
}
}
return errors
}
func (r *runner) Run(ctx context.Context, period time.Duration, force <-chan struct{}) {
func (r *runner) Run(ctx context.Context, period time.Duration) {
ticker := time.NewTicker(period)
for {
select {
case <-ticker.C:
r.updateNecessary(ctx)
case <-force:
r.updateNecessary(ctx)
case <-r.force:
r.forceResult <- r.updateNecessary(ctx)
case <-ctx.Done():
ticker.Stop()
return
}
}
}
func (r *runner) ForceUpdate(ctx context.Context) (errs []error) {
r.force <- struct{}{}
select {
case errs = <-r.forceResult:
case <-ctx.Done():
errs = []error{ctx.Err()}
}
return errs
}