Skip to content

Commit

Permalink
feat: http runtime, error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
hjwalt committed Jun 29, 2023
1 parent 536ddb5 commit e4da0d9
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 76 deletions.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ make tidy
make htmlcov
```

Last coverage: 98.4%
Last coverage: 98.0%

## To Do

Expand Down Expand Up @@ -150,3 +150,20 @@ func MultiLevelInjector(ctx context.Context) (any, error) {
}, nil
}
```

### Runtime

Runtimes are independent context that needs to be maintained from the start until the end of the program execution.
Runtimes usually also needs to be instantiated and cleaned up.

Examples of runtimes:

1. HTTP server
2. Kafka consumer
3. Kafka producer
4. Database connection

Why is it important to standardise?

1. Golang is too barebone. This kind of runtime management needs to keep getting rewritten.
2. Resource management is not a difficult problem but the cost of mistakes is high. Your program can hang, you can have connections interrupted without clean disconnect, and many more. Reducing the potential scope of failure is in general a good idea.
10 changes: 8 additions & 2 deletions runtime/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package runtime

import "sync"

func NewController() Controller {
return &RuntimeController{}
func NewPrimaryController() (Controller, chan error) {
err := make(chan error, 1)
return &RuntimeController{err: err}, err
}

type RuntimeController struct {
wait sync.WaitGroup
err chan<- error
}

func (c *RuntimeController) Started() {
Expand All @@ -18,6 +20,10 @@ func (c *RuntimeController) Stopped() {
c.wait.Add(-1)
}

func (c *RuntimeController) Error(err error) {
c.err <- err
}

func (c *RuntimeController) Wait() {
c.wait.Wait()
}
43 changes: 22 additions & 21 deletions runtime/functional.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,26 @@ import (

// constructor
func NewFunctional[C any](configurations ...Configuration[*Functional[C]]) Runtime {
consumer := &Functional[C]{}
c := &Functional[C]{}
c = FunctionalDefault[C](c)
for _, configuration := range configurations {
consumer = configuration(consumer)
c = configuration(c)
}
return consumer
return c
}

// configuration
func FunctionalWithController[C any](controller Controller) Configuration[*Functional[C]] {
return func(c *Functional[C]) *Functional[C] {
c.controller = controller
return c
}
// default
func FunctionalDefault[C any](c *Functional[C]) *Functional[C] {
c.data = reflect.Construct[C]()

ctx, cancel := context.WithCancel(context.Background())
c.context = ctx
c.cancel = cancel

return c
}

// configuration
func FunctionalWithInitialise[C any](initialise func() (C, error)) Configuration[*Functional[C]] {
return func(c *Functional[C]) *Functional[C] {
c.initialise = initialise
Expand Down Expand Up @@ -71,24 +76,15 @@ func (r *Functional[C]) Start() error {
return ErrFunctionalRuntimeNoLoop
}

if r.initialise == nil {
r.data = reflect.Construct[C]()
} else {
if r.initialise != nil {
data, initerr := r.initialise()
if initerr != nil {
return errors.Join(ErrFunctionalRuntimeInitialise, initerr)
}
r.data = data
}

if r.context == nil {
ctx, cancel := context.WithCancel(context.Background())
r.context = ctx
r.cancel = cancel
}

go r.Run()

r.controller.Started()
return nil
}
Expand All @@ -97,17 +93,22 @@ func (r *Functional[C]) Stop() {
r.cancel()
}

func (r *Functional[C]) SetController(controller Controller) {
r.controller = controller
}

func (r *Functional[C]) Run() {
defer r.controller.Stopped()

for {
err := r.loop(r.data, r.context, r.cancel)
if err != nil {
logger.ErrorErr("functional runtime loop", err)
logger.ErrorErr("functional runtime loop error", err)
r.controller.Error(err)
break
}
if r.context.Err() != nil {
logger.ErrorErr("functional runtime context", err)
logger.WarnErr("functional runtime exitting via context", err)
break
}
}
Expand Down
33 changes: 19 additions & 14 deletions runtime/functional_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,20 @@ type TestData struct {
value int64
}

func NewController() runtime.Controller {
controller, _ := runtime.NewPrimaryController()
return controller
}

func TestFunctionalWillStopNormally(t *testing.T) {
assert := assert.New(t)

controller := runtime.NewController()
controller := NewController()
value := 0
initCalled := 0
exitCalled := 0

fnRuntime := runtime.NewFunctional[*TestData](
runtime.FunctionalWithController[*TestData](controller),
runtime.FunctionalWithInitialise[*TestData](func() (*TestData, error) {
initCalled += 1
return &TestData{}, nil
Expand All @@ -50,6 +54,7 @@ func TestFunctionalWillStopNormally(t *testing.T) {
return nil
}),
)
fnRuntime.SetController(controller)

startErr := fnRuntime.Start()
controller.Wait()
Expand All @@ -63,13 +68,12 @@ func TestFunctionalWillStopNormally(t *testing.T) {
func TestFunctionalWithContext(t *testing.T) {
assert := assert.New(t)

controller := runtime.NewController()
controller := NewController()
value := int64(0)
initCalled := 0
exitCalled := 0

fnRuntime := runtime.NewFunctional[*TestData](
runtime.FunctionalWithController[*TestData](controller),
runtime.FunctionalWithInitialise[*TestData](func() (*TestData, error) {
initCalled += 1
return &TestData{}, nil
Expand All @@ -86,6 +90,7 @@ func TestFunctionalWithContext(t *testing.T) {
}),
runtime.FunctionalWithContext[*TestData](context.WithValue(context.Background(), ContextKey("test"), int64(5))),
)
fnRuntime.SetController(controller)

startErr := fnRuntime.Start()
controller.Wait()
Expand All @@ -99,12 +104,11 @@ func TestFunctionalWithContext(t *testing.T) {
func TestFunctionalMissingInitialiseWillConstruct(t *testing.T) {
assert := assert.New(t)

controller := runtime.NewController()
controller := NewController()
value := 0
exitCalled := 0

fnRuntime := runtime.NewFunctional[*TestData](
runtime.FunctionalWithController[*TestData](controller),
runtime.FunctionalWithCleanup[*TestData](func(data *TestData) {
exitCalled += 1
}),
Expand All @@ -118,6 +122,7 @@ func TestFunctionalMissingInitialiseWillConstruct(t *testing.T) {
return nil
}),
)
fnRuntime.SetController(controller)

startErr := fnRuntime.Start()
controller.Wait()
Expand All @@ -130,13 +135,12 @@ func TestFunctionalMissingInitialiseWillConstruct(t *testing.T) {
func TestFunctionalWillStopOnError(t *testing.T) {
assert := assert.New(t)

controller := runtime.NewController()
controller := NewController()
value := 0
initCalled := 0
exitCalled := 0

fnRuntime := runtime.NewFunctional[*TestData](
runtime.FunctionalWithController[*TestData](controller),
runtime.FunctionalWithInitialise[*TestData](func() (*TestData, error) {
initCalled += 1
return &TestData{}, nil
Expand All @@ -154,6 +158,7 @@ func TestFunctionalWillStopOnError(t *testing.T) {
return nil
}),
)
fnRuntime.SetController(controller)

startErr := fnRuntime.Start()
controller.Wait()
Expand All @@ -167,13 +172,12 @@ func TestFunctionalWillStopOnError(t *testing.T) {
func TestFunctionalWillStopOnStop(t *testing.T) {
assert := assert.New(t)

controller := runtime.NewController()
controller := NewController()
value := 0
initCalled := 0
exitCalled := 0

fnRuntime := runtime.NewFunctional[*TestData](
runtime.FunctionalWithController[*TestData](controller),
runtime.FunctionalWithInitialise[*TestData](func() (*TestData, error) {
initCalled += 1
return &TestData{}, nil
Expand All @@ -187,6 +191,7 @@ func TestFunctionalWillStopOnStop(t *testing.T) {
return nil
}),
)
fnRuntime.SetController(controller)

startErr := fnRuntime.Start()
time.Sleep(time.Millisecond)
Expand All @@ -202,12 +207,11 @@ func TestFunctionalWillStopOnStop(t *testing.T) {
func TestFunctionalMissingLoop(t *testing.T) {
assert := assert.New(t)

controller := runtime.NewController()
controller := NewController()
initCalled := 0
exitCalled := 0

fnRuntime := runtime.NewFunctional[*TestData](
runtime.FunctionalWithController[*TestData](controller),
runtime.FunctionalWithInitialise[*TestData](func() (*TestData, error) {
initCalled += 1
return &TestData{}, nil
Expand All @@ -216,6 +220,7 @@ func TestFunctionalMissingLoop(t *testing.T) {
exitCalled += 1
}),
)
fnRuntime.SetController(controller)

startErr := fnRuntime.Start()
assert.ErrorIs(startErr, runtime.ErrFunctionalRuntimeNoLoop)
Expand All @@ -224,13 +229,12 @@ func TestFunctionalMissingLoop(t *testing.T) {
func TestFunctionalInitialiseError(t *testing.T) {
assert := assert.New(t)

controller := runtime.NewController()
controller := NewController()
value := 0
initCalled := 0
exitCalled := 0

fnRuntime := runtime.NewFunctional[*TestData](
runtime.FunctionalWithController[*TestData](controller),
runtime.FunctionalWithInitialise[*TestData](func() (*TestData, error) {
initCalled += 1
return &TestData{}, errors.New("error init")
Expand All @@ -248,6 +252,7 @@ func TestFunctionalInitialiseError(t *testing.T) {
return nil
}),
)
fnRuntime.SetController(controller)

startErr := fnRuntime.Start()
assert.ErrorIs(startErr, runtime.ErrFunctionalRuntimeInitialise)
Expand Down
Loading

0 comments on commit e4da0d9

Please sign in to comment.