Skip to content

Commit

Permalink
WIP all: impl dynamic reconfiguration
Browse files Browse the repository at this point in the history
  • Loading branch information
ainar-g committed Oct 7, 2022
1 parent 330ac30 commit 7287a86
Show file tree
Hide file tree
Showing 19 changed files with 402 additions and 67 deletions.
6 changes: 3 additions & 3 deletions internal/aghos/filewalker_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import (
// errFSOpen.
type errFS struct{}

// errFSOpen is returned from errGlobFS.Open.
// errFSOpen is returned from errFS.Open.
const errFSOpen errors.Error = "test open error"

// Open implements the fs.FS interface for *errGlobFS. fsys is always nil and
// err is always errFSOpen.
// Open implements the fs.FS interface for *errFS. fsys is always nil and err
// is always errFSOpen.
func (efs *errFS) Open(name string) (fsys fs.File, err error) {
return nil, errFSOpen
}
Expand Down
10 changes: 10 additions & 0 deletions internal/aghos/os.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,21 @@ func RootDirFS() (fsys fs.FS) {
return os.DirFS("")
}

// NotifyReconfigureSignal notifies c on receiving reconfigure signals.
func NotifyReconfigureSignal(c chan<- os.Signal) {
notifyReconfigureSignal(c)
}

// NotifyShutdownSignal notifies c on receiving shutdown signals.
func NotifyShutdownSignal(c chan<- os.Signal) {
notifyShutdownSignal(c)
}

// IsReconfigureSignal returns true if sig is a reconfigure signal.
func IsReconfigureSignal(sig os.Signal) (ok bool) {
return isReconfigureSignal(sig)
}

// IsShutdownSignal returns true if sig is a shutdown signal.
func IsShutdownSignal(sig os.Signal) (ok bool) {
return isShutdownSignal(sig)
Expand Down
8 changes: 8 additions & 0 deletions internal/aghos/os_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@ import (
"golang.org/x/sys/unix"
)

func notifyReconfigureSignal(c chan<- os.Signal) {
signal.Notify(c, unix.SIGHUP)
}

func notifyShutdownSignal(c chan<- os.Signal) {
signal.Notify(c, unix.SIGINT, unix.SIGQUIT, unix.SIGTERM)
}

func isReconfigureSignal(sig os.Signal) (ok bool) {
return sig == unix.SIGHUP
}

func isShutdownSignal(sig os.Signal) (ok bool) {
switch sig {
case
Expand Down
8 changes: 8 additions & 0 deletions internal/aghos/os_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,20 @@ func isOpenWrt() (ok bool) {
return false
}

func notifyReconfigureSignal(c chan<- os.Signal) {
signal.Notify(c, windows.SIGHUP)
}

func notifyShutdownSignal(c chan<- os.Signal) {
// syscall.SIGTERM is processed automatically. See go doc os/signal,
// section Windows.
signal.Notify(c, os.Interrupt)
}

func isReconfigureSignal(sig os.Signal) (ok bool) {
return sig == windows.SIGHUP
}

func isShutdownSignal(sig os.Signal) (ok bool) {
switch sig {
case
Expand Down
17 changes: 10 additions & 7 deletions internal/aghtest/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net"

"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/miekg/dns"
)
Expand Down Expand Up @@ -88,7 +89,7 @@ func (l *Listener) Close() (err error) {
return l.OnClose()
}

// Module AdGuardHome
// Module adguard-home

// Package aghos

Expand Down Expand Up @@ -117,29 +118,31 @@ func (w *FSWatcher) Close() (err error) {
return w.OnClose()
}

// Package websvc
// Package agh

// ServiceWithConfig is a mock [websvc.ServiceWithConfig] implementation for
// tests.
// type check
var _ agh.ServiceWithConfig[struct{}] = (*ServiceWithConfig[struct{}])(nil)

// ServiceWithConfig is a mock [agh.ServiceWithConfig] implementation for tests.
type ServiceWithConfig[ConfigType any] struct {
OnStart func() (err error)
OnShutdown func(ctx context.Context) (err error)
OnConfig func() (c ConfigType)
}

// Start implements the [websvc.ServiceWithConfig] interface for
// Start implements the [agh.ServiceWithConfig] interface for
// *ServiceWithConfig.
func (s *ServiceWithConfig[_]) Start() (err error) {
return s.OnStart()
}

// Shutdown implements the [websvc.ServiceWithConfig] interface for
// Shutdown implements the [agh.ServiceWithConfig] interface for
// *ServiceWithConfig.
func (s *ServiceWithConfig[_]) Shutdown(ctx context.Context) (err error) {
return s.OnShutdown(ctx)
}

// Config implements the [websvc.ServiceWithConfig] interface for
// Config implements the [agh.ServiceWithConfig] interface for
// *ServiceWithConfig.
func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {
return s.OnConfig()
Expand Down
8 changes: 1 addition & 7 deletions internal/aghtest/interface_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
package aghtest_test

import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
)

// type check
var _ websvc.ServiceWithConfig[struct{}] = (*aghtest.ServiceWithConfig[struct{}])(nil)
// Put interface checks that cause import cycles here.
40 changes: 35 additions & 5 deletions internal/next/agh/agh.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// Package agh contains common entities and interfaces of AdGuard Home.
//
// TODO(a.garipov): Move to the upper-level internal/.
package agh

import "context"
Expand All @@ -23,11 +21,43 @@ type Service interface {
// type check
var _ Service = EmptyService{}

// EmptyService is a Service that does nothing.
// EmptyService is a [Service] that does nothing.
//
// TODO(a.garipov): Remove if unnecessary.
type EmptyService struct{}

// Start implements the Service interface for EmptyService.
// Start implements the [Service] interface for EmptyService.
func (EmptyService) Start() (err error) { return nil }

// Shutdown implements the Service interface for EmptyService.
// Shutdown implements the [Service] interface for EmptyService.
func (EmptyService) Shutdown(_ context.Context) (err error) { return nil }

// ServiceWithConfig is an extension of the [Service] interface for services
// that can return their configuration.
//
// TODO(a.garipov): Consider removing this generic interface if we figure out
// how to make it testable in a better way.
type ServiceWithConfig[ConfigType any] interface {
Service

Config() (c ConfigType)
}

// type check
var _ ServiceWithConfig[struct{}] = (*EmptyServiceWithConfig[struct{}])(nil)

// EmptyServiceWithConfig is a ServiceWithConfig that does nothing. Its Config
// method returns Conf.
//
// TODO(a.garipov): Remove if unnecessary.
type EmptyServiceWithConfig[T any] struct {
EmptyService

Conf T
}

// Config implements the [ServiceWithConfig] interface for
// *EmptyServiceWithConfig.
func (s *EmptyServiceWithConfig[ConfigType]) Config() (conf ConfigType) {
return s.Conf
}
33 changes: 19 additions & 14 deletions internal/next/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import (
"context"
"io/fs"
"math/rand"
"net/netip"
"os"
"time"

"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
"github.com/AdguardTeam/AdGuardHome/internal/next/svc"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/log"
)

Expand All @@ -24,29 +25,33 @@ func Main(clientBuildFS fs.FS) {

// TODO(a.garipov): Set up logging.

log.Info("starting adguard home, version %s, pid %d", version.Version(), os.Getpid())

// Web Service

// TODO(a.garipov): Use in the Web service.
_ = clientBuildFS

// TODO(a.garipov): Make configurable.
web := websvc.New(&websvc.Config{
// TODO(a.garipov): Use an actual implementation.
ConfigManager: nil,
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:3001")},
Start: start,
Timeout: 60 * time.Second,
ForceHTTPS: false,
})

err := web.Start()
// TODO(a.garipov): Set up configuration file name.
const confFile = "AdGuardHome.1.yaml"

confMgr, err := svc.New(confFile, start)
fatalOnError(err)

web := confMgr.Web()
err = web.Start()
fatalOnError(err)

dns := confMgr.DNS()
err = dns.Start()
fatalOnError(err)

sigHdlr := newSignalHandler(
web,
dns,
)

go sigHdlr.handle()
go sigHdlr.handle(confFile, start)

select {}
}
Expand Down
48 changes: 43 additions & 5 deletions internal/next/cmd/signal.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package cmd

import (
"os"
"time"

"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/svc"
"github.com/AdguardTeam/golibs/log"
)

Expand All @@ -18,26 +20,57 @@ type signalHandler struct {
}

// handle processes OS signals.
func (h *signalHandler) handle() {
func (h *signalHandler) handle(confFile string, start time.Time) {
defer log.OnPanic("signalHandler.handle")

for sig := range h.signal {
log.Info("sighdlr: received signal %q", sig)

if aghos.IsShutdownSignal(sig) {
h.shutdown()
if aghos.IsReconfigureSignal(sig) {
h.reconfigure(confFile, start)
} else if aghos.IsShutdownSignal(sig) {
h.shutdown(true)
}
}
}

// reconfigure rereads the configuration file and updates and restarts services.
func (h *signalHandler) reconfigure(confFile string, start time.Time) {
log.Info("sighdlr: reconfiguring adguard home")

h.shutdown(false)

// TODO(a.garipov): This is a very rough way to do it. Some services can be
// reconfigured without the full shutdown, and the error handling is
// currently not the best.

confMgr, err := svc.New(confFile, start)
fatalOnError(err)

web := confMgr.Web()
err = web.Start()
fatalOnError(err)

dns := confMgr.DNS()
err = dns.Start()
fatalOnError(err)

h.services = []agh.Service{
dns,
web,
}

log.Info("sighdlr: successfully reconfigured adguard home")
}

// Exit status constants.
const (
statusSuccess = 0
statusError = 1
)

// shutdown gracefully shuts down all services.
func (h *signalHandler) shutdown() {
func (h *signalHandler) shutdown(shouldExit bool) {
ctx, cancel := ctxWithDefaultTimeout()
defer cancel()

Expand All @@ -54,7 +87,11 @@ func (h *signalHandler) shutdown() {

log.Info("sighdlr: shutting down adguard home")

os.Exit(status)
if shouldExit {
log.Info("sighdlr: exiting with status %d", status)

os.Exit(status)
}
}

// newSignalHandler returns a new signalHandler that shuts down svcs.
Expand All @@ -65,6 +102,7 @@ func newSignalHandler(svcs ...agh.Service) (h *signalHandler) {
}

aghos.NotifyShutdownSignal(h.signal)
aghos.NotifyReconfigureSignal(h.signal)

return h
}
40 changes: 40 additions & 0 deletions internal/next/svc/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package svc

import (
"net/netip"

"github.com/AdguardTeam/golibs/timeutil"
)

// Configuration Structures

// config is the top-level on-disk configuration structure.
type config struct {
DNS *dnsConfig `yaml:"dns"`
HTTP *httpConfig `yaml:"http"`
// TODO(a.garipov): Use.
SchemaVersion int `yaml:"schema_version"`
// TODO(a.garipov): Use.
DebugPprof bool `yaml:"debug_pprof"`
Verbose bool `yaml:"verbose"`
}

// dnsConfig is the on-disk DNS configuration.
//
// TODO(a.garipov): Validate.
type dnsConfig struct {
Addresses []netip.AddrPort `yaml:"addresses"`
BootstrapDNS []string `yaml:"bootstrap_dns"`
UpstreamDNS []string `yaml:"upstream_dns"`
UpstreamTimeout timeutil.Duration `yaml:"upstream_timeout"`
}

// httpConfig is the on-disk web API configuration.
//
// TODO(a.garipov): Validate.
type httpConfig struct {
Addresses []netip.AddrPort `yaml:"addresses"`
SecureAddresses []netip.AddrPort `yaml:"secure_addresses"`
Timeout timeutil.Duration `yaml:"timeout"`
ForceHTTPS bool `yaml:"force_https"`
}
Loading

0 comments on commit 7287a86

Please sign in to comment.