Skip to content

Commit

Permalink
feat(Context): provides context integration
Browse files Browse the repository at this point in the history
I arrived on a solution to provide context support that I was happy to
include in the core implementation. Peripheral, and entirely optional extra
functionality, as usual. The new Context type simplifies context utilisation,
via composition of basic building blocks.
  • Loading branch information
joeycumines committed Jul 19, 2020
1 parent 9b78656 commit 1a76f90
Show file tree
Hide file tree
Showing 4 changed files with 430 additions and 2 deletions.
117 changes: 115 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ finite state machines, which tends to cripple the modularity of any interruptabl
it's own exit condition(s). Golang's `context` doesn't really help in that case, either, being a communication
mechanism, rather than a control mechanism. As an alternative, implementations may use pre-conditions (preceding
child(ren) in a sequence), guarding an asynchronous tick. Context support may be desirable, and may be implemented as
`Tick` implementations(s). Context support is peripheral in that it's only relevant to a subset of implementations,
and was deliberately omitted (from `Tick`).
`Tick` implementations(s). This package provides a `Context` implementation to address several common use cases, such
as operations with timeouts. Context support is peripheral in that it's only relevant to a subset of implementations,
and was deliberately omitted from the core `Tick` type.

### Modularity

Expand Down Expand Up @@ -430,4 +431,116 @@ func ExampleBackground_asyncJobQueue() {
//[client] job "3. 150ms" FINISHED
//running jobs: 0
}


// ExampleContext demonstrates how the Context implementation may be used to integrate with the context package
func ExampleContext() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

var (
btCtx = new(Context).WithTimeout(ctx, time.Millisecond*100)
debug = func(args ...interface{}) Tick {
return func([]Node) (Status, error) {
fmt.Println(args...)
return Success, nil
}
}
counter int
counterEqual = func(v int) Tick {
return func([]Node) (Status, error) {
if counter == v {
return Success, nil
}
return Failure, nil
}
}
counterInc Tick = func([]Node) (Status, error) {
counter++
//fmt.Printf("counter = %d\n", counter)
return Success, nil
}
ticker = NewTicker(ctx, time.Millisecond, New(
Sequence,
New(
Selector,
New(Not(btCtx.Err)),
New(
Sequence,
New(debug(`(re)initialising btCtx...`)),
New(btCtx.Init),
New(Not(btCtx.Err)),
),
),
New(
Selector,
New(
Sequence,
New(counterEqual(0)),
New(debug(`blocking on context-enabled tick...`)),
New(
btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) {
fmt.Printf("NOTE children (%d) passed through\n", len(children))
<-ctx.Done()
return Success, nil
}),
New(Sequence),
New(Sequence),
),
New(counterInc),
),
New(
Sequence,
New(counterEqual(1)),
New(debug(`blocking on done...`)),
New(btCtx.Done),
New(counterInc),
),
New(
Sequence,
New(counterEqual(2)),
New(debug(`canceling local then rechecking the above...`)),
New(btCtx.Cancel),
New(btCtx.Err),
New(btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) {
<-ctx.Done()
return Success, nil
})),
New(btCtx.Done),
New(counterInc),
),
New(
Sequence,
New(counterEqual(3)),
New(debug(`canceling parent then rechecking the above...`)),
New(func([]Node) (Status, error) {
cancel()
return Success, nil
}),
New(btCtx.Err),
New(btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) {
<-ctx.Done()
return Success, nil
})),
New(btCtx.Done),
New(debug(`exiting...`)),
),
),
))
)

<-ticker.Done()

//output:
//(re)initialising btCtx...
//blocking on context-enabled tick...
//NOTE children (2) passed through
//(re)initialising btCtx...
//blocking on done...
//(re)initialising btCtx...
//canceling local then rechecking the above...
//(re)initialising btCtx...
//canceling parent then rechecking the above...
//exiting...
}
```
101 changes: 101 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
Copyright 2020 Joseph Cumines
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package behaviortree

import (
"context"
"time"
)

// Context provides support for tick(s) utilising context as a means of cancelation, with cancelation triggered by
// either BT-driven logic or the normal means (parent cancelation, deadline / timeout).
//
// Note that it must be initialised by means of it's Init method (implements a tick) prior to use (Context.Tick tick).
// Init may be ticked any number of times (each time triggering cancelation of any prior context).
type Context struct {
parent func() (context.Context, context.CancelFunc)
ctx context.Context
cancel context.CancelFunc
}

// WithCancel configures the receiver to initialise context like context.WithCancel(parent), returning the receiver
func (c *Context) WithCancel(parent context.Context) *Context {
c.parent = func() (context.Context, context.CancelFunc) { return context.WithCancel(parent) }
return c
}

// WithDeadline configures the receiver to initialise context like context.WithDeadline(parent, deadline), returning
// the receiver
func (c *Context) WithDeadline(parent context.Context, deadline time.Time) *Context {
c.parent = func() (context.Context, context.CancelFunc) { return context.WithDeadline(parent, deadline) }
return c
}

// WithTimeout configures the receiver to initialise context like context.WithTimeout(parent, timeout), returning
// the receiver
func (c *Context) WithTimeout(parent context.Context, timeout time.Duration) *Context {
c.parent = func() (context.Context, context.CancelFunc) { return context.WithTimeout(parent, timeout) }
return c
}

// Init implements a tick that will cancel existing context, (re)initialise the context, then succeed, note that it
// must not be called concurrently with any other method, and it must be ticked prior to any Context.Tick tick
func (c *Context) Init([]Node) (Status, error) {
if c.cancel != nil {
c.cancel()
}
if c.parent != nil {
c.ctx, c.cancel = c.parent()
} else {
c.ctx, c.cancel = context.WithCancel(context.Background())
}
return Success, nil
}

// Tick returns a tick that will call fn with the receiver's context, returning nil if fn is nil (for consistency
// with other implementations in this package), note that a Init node must have already been ticked on all possible
// execution paths, or a panic may occur, due to fn being passed a nil context.Context
func (c *Context) Tick(fn func(ctx context.Context, children []Node) (Status, error)) Tick {
if fn != nil {
return func(children []Node) (Status, error) { return fn(c.ctx, children) }
}
return nil
}

// Cancel implements a tick that will cancel the receiver's context (noop if it has none) then succeed
func (c *Context) Cancel([]Node) (Status, error) {
if c.cancel != nil {
c.cancel()
}
return Success, nil
}

// Err implements a tick that will succeed if the receiver does not have a context or it has been canceled
func (c *Context) Err([]Node) (Status, error) {
if c.ctx == nil || c.ctx.Err() != nil {
return Success, nil
}
return Failure, nil
}

// Done implements a tick that will block on the receiver's context being canceled (noop if it has none) then succeed
func (c *Context) Done([]Node) (Status, error) {
if c.ctx != nil {
<-c.ctx.Done()
}
return Success, nil
}
103 changes: 103 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
Copyright 2020 Joseph Cumines
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package behaviortree

import (
"context"
"testing"
"time"
)

func TestContext_Tick_nilFn(t *testing.T) {
if v := new(Context).Tick(nil); v != nil {
t.Error(`expected nil`)
}
}

func TestContext_Cancel_noContext(t *testing.T) {
if status, err := new(Context).Cancel(nil); err != nil || status != Success {
t.Error(status, err)
}
}

func TestContext_Done_noContext(t *testing.T) {
if status, err := new(Context).Done(nil); err != nil || status != Success {
t.Error(status, err)
}
}

func TestContext_Init_default(t *testing.T) {
c := new(Context)
if status, err := c.Init(nil); err != nil || status != Success {
t.Fatal(status, err)
}
ctx := c.ctx
if err := ctx.Err(); err != nil {
t.Fatal(err)
}
if status, err := c.Init(nil); err != nil || status != Success {
t.Fatal(status, err)
}
if err := ctx.Err(); err == nil {
t.Error(c)
}
if c.ctx == nil || c.cancel == nil || c.ctx.Err() != nil {
t.Fatal(c)
}
c.cancel()
if c.ctx.Err() == nil {
t.Fatal(c)
}
}

func TestContext_WithCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c := new(Context)
if v := c.WithCancel(ctx); v != c {
t.Error(v)
}
if c.ctx != nil || c.cancel != nil || c.parent == nil {
t.Fatal(c)
}
if status, err := c.Init(nil); err != nil || status != Success {
t.Fatal(status, err)
}
if c.ctx == nil || c.cancel == nil || c.ctx.Err() != nil {
t.Fatal(c)
}
cancel()
if err := c.ctx.Err(); err != context.Canceled {
t.Fatal(err)
}
}

func TestContext_WithDeadline(t *testing.T) {
c := new(Context)
if v := c.WithDeadline(context.Background(), time.Now().Add(-time.Second)); v != c {
t.Error(v)
}
if c.ctx != nil || c.cancel != nil || c.parent == nil {
t.Fatal(c)
}
if status, err := c.Init(nil); err != nil || status != Success {
t.Fatal(status, err)
}
if err := c.ctx.Err(); err != context.DeadlineExceeded {
t.Fatal(err)
}
}
Loading

0 comments on commit 1a76f90

Please sign in to comment.