Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add deterministic app configuration #139

Merged
merged 1 commit into from
Jul 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions app/fabric.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"os"
"runtime"
"strings"
"sync"
"time"

Expand All @@ -28,6 +29,7 @@ type Fabric struct {
Factories Factories

singletons Singletons
initOrder []string
services map[string]Service
contexts map[string]*serviceContext
updated map[string]time.Time
Expand Down Expand Up @@ -72,7 +74,12 @@ func (f *Fabric) Start(ctx context.Context) {
return f
}
// and every dependency would just recursively resolve
f.singletons = f.Factories.Init()
singletons, initOrder, err := f.Factories.Init()
if err != nil {
panic(err)
}
f.singletons = singletons
f.initOrder = initOrder
syncTrigger := f.configuration["app"].DurOr("sync", 1*time.Minute)
f.syncTrigger = time.NewTicker(syncTrigger)
f.State = f.configuration["app"].StrOr("state", "$HOME/.$APP/data")
Expand All @@ -93,6 +100,9 @@ func (f *Fabric) Start(ctx context.Context) {
// treat all ListenAndServe exposing singletons as another service
f.services["monitor"] = monitor

// monitor has to be initialized last, as we assume all services started
f.initOrder = append(f.initOrder, "monitor")

f.initServices()
f.configureServices()
f.loadState()
Expand All @@ -115,14 +125,14 @@ func (f *Fabric) initLogging() {
"warn": zerolog.WarnLevel,
}
logLevel := f.configuration["log"].StrOr("level", "info")
level, ok := levels[logLevel]
level, ok := levels[strings.ToLower(logLevel)]
if !ok {
level = zerolog.InfoLevel
}
zerolog.SetGlobalLevel(level)

logFormat := f.configuration["log"].StrOr("format", "pretty")
switch logFormat {
switch strings.ToLower(logFormat) {
case "pretty":
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
case "json":
Expand Down Expand Up @@ -175,8 +185,12 @@ func (m *monitorServers) listenAndServe(service string, server aServer) {
}

func (f *Fabric) startAll(ctx context.Context) {
for service := range f.services {
for _, service := range f.initOrder {
log.Debug().Str("service", service).Msg("starting")
_, ok := f.services[service]
if !ok {
continue
}
f.contexts[service] = &serviceContext{
ctx: ctx,
sync: f.syncService,
Expand All @@ -202,8 +216,8 @@ func (f *Fabric) loadState() {
}

func (f *Fabric) configureServices() {
for service, s := range f.singletons {
c, ok := s.(configurable)
for _, service := range f.initOrder {
c, ok := f.singletons[service].(configurable)
if !ok {
continue
}
Expand Down
78 changes: 64 additions & 14 deletions app/factories.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app
import (
"fmt"
"reflect"
"strings"
)

type Factories map[string]interface{}
Expand All @@ -28,21 +29,22 @@ func (i instances) Singletons() Singletons {
return singletons
}

func (c Factories) Init() Singletons {
func (c Factories) Init() (Singletons, []string, error) {
deps, err := c.dependencies()
if err != nil {
panic(err)
return nil, nil, err
}
order := deps.ordered()
inst := instances{}
// resolve reflections on all dependencies
for k := range deps {
dep, err := deps.resolve(k, inst)
if err != nil {
panic(err)
return nil, nil, err
}
inst[k] = dep
}
return inst.Singletons()
return inst.Singletons(), order, nil
}

func (c Factories) dependencies() (dependencies, error) {
Expand All @@ -67,6 +69,54 @@ func (c Factories) dependencies() (dependencies, error) {
return deps, nil
}

func (d *dependency) matches(in reflect.Type) bool {
isInIface := in.Kind() == reflect.Interface
inImplsType := isInIface && d.Out.Implements(in)
inEqualsType := d.Out == in
return inImplsType || inEqualsType
}

// sort dependencies topologically according to Kahn's algorithm (1962)
// see https://doi.org/10.1145%2F368996.369025
func (deps dependencies) ordered() (order []string) {
edges := map[string][]string{}
indegree := map[string]int{}
// materialize interface dependencies into neighbour adjacency graph
for k := range deps {
for _, in := range deps[k].In {
for otherKey, otherType := range deps {
if !otherType.matches(in) {
continue
}
edges[k] = append(edges[k], otherKey)
edges[otherKey] = append(edges[otherKey], k)
indegree[k]++
}
}
}
q := []string{}
// First we add items with no upstream dependencies
for k := range deps {
if indegree[k] == 0 {
q = append(q, k)
}
}
for len(q) > 0 {
k := q[0]
q = q[1:]
order = append(order, k)
for _, j := range edges[k] {
// Reduce the indegree of adjacent nodes and add them to
// the queue if their indegree becomes 0
indegree[j]--
if indegree[j] == 0 {
q = append(q, j)
}
}
}
return order
}

func (deps dependencies) resolve(k string, inst instances) (reflect.Value, error) {
ex, ok := inst[k]
if ok {
Expand All @@ -78,23 +128,23 @@ func (deps dependencies) resolve(k string, inst instances) (reflect.Value, error
}
args := []reflect.Value{}
for _, in := range t.In {
found := false
isInIface := in.Kind() == reflect.Interface
for other_key, other_type := range deps {
inImplsType := isInIface && other_type.Out.Implements(in)
inEqualsType := other_type.Out == in
if !inImplsType && !inEqualsType {
found := []string{}
for otherKey, otherType := range deps {
if !otherType.matches(in) {
continue
}
dep, err := deps.resolve(other_key, inst)
dep, err := deps.resolve(otherKey, inst)
if err != nil {
return reflect.Value{}, fmt.Errorf(
"cannot resolve %s because of %s: %s", k, other_key, err)
"cannot resolve %s because of %s: %s", k, otherKey, err)
}
found = append(found, otherKey)
args = append(args, dep)
found = true
}
if !found {
if len(found) > 1 {
return reflect.Value{}, fmt.Errorf("multiple matches for %s: %s", in, strings.Join(found, ", "))
}
if len(found) == 0 {
return reflect.Value{}, fmt.Errorf("cannot find %s for %s", in, k)
}
}
Expand Down