-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement supporting types for hooks. (#125)
This PR should be reviewed before other hooks PRs. This PR adds supporting types for hooks, but does not use them from the client. --------- Co-authored-by: Casey Waldren <[email protected]>
- Loading branch information
1 parent
41dec51
commit 7f06147
Showing
12 changed files
with
1,095 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package hooks | ||
|
||
import ( | ||
"github.com/launchdarkly/go-server-sdk/v7/ldhooks" | ||
) | ||
|
||
type iterator struct { | ||
reverse bool | ||
cursor int | ||
collection []ldhooks.Hook | ||
} | ||
|
||
// newIterator creates a new hook iterator which can iterate hooks forward or reverse. | ||
// | ||
// The collection being iterated should not be modified during iteration. | ||
// | ||
// Example: | ||
// it := newIterator(false, hooks) | ||
// | ||
// for it.hasNext() { | ||
// hook := it.getNext() | ||
// } | ||
func newIterator(reverse bool, hooks []ldhooks.Hook) *iterator { | ||
cursor := -1 | ||
if reverse { | ||
cursor = len(hooks) | ||
} | ||
return &iterator{ | ||
reverse: reverse, | ||
cursor: cursor, | ||
collection: hooks, | ||
} | ||
} | ||
|
||
func (it *iterator) hasNext() bool { | ||
nextCursor := it.getNextIndex() | ||
return it.inBounds(nextCursor) | ||
} | ||
|
||
func (it *iterator) inBounds(nextCursor int) bool { | ||
inBounds := nextCursor < len(it.collection) && nextCursor >= 0 | ||
return inBounds | ||
} | ||
|
||
func (it *iterator) getNextIndex() int { | ||
var nextCursor int | ||
if it.reverse { | ||
nextCursor = it.cursor - 1 | ||
} else { | ||
nextCursor = it.cursor + 1 | ||
} | ||
return nextCursor | ||
} | ||
|
||
func (it *iterator) getNext() (int, ldhooks.Hook) { | ||
i := it.getNextIndex() | ||
if it.inBounds(i) { | ||
it.cursor = i | ||
return it.cursor, it.collection[it.cursor] | ||
} | ||
return it.cursor, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package hooks | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest" | ||
"github.com/launchdarkly/go-server-sdk/v7/ldhooks" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestIterator(t *testing.T) { | ||
testCases := []bool{false, true} | ||
for _, reverse := range testCases { | ||
t.Run(fmt.Sprintf("reverse: %v", reverse), func(t *testing.T) { | ||
t.Run("empty collection", func(t *testing.T) { | ||
|
||
var hooks []ldhooks.Hook | ||
it := newIterator(reverse, hooks) | ||
|
||
assert.False(t, it.hasNext()) | ||
|
||
_, value := it.getNext() | ||
assert.Zero(t, value) | ||
|
||
}) | ||
|
||
t.Run("collection with items", func(t *testing.T) { | ||
hooks := []ldhooks.Hook{ | ||
sharedtest.NewTestHook("a"), | ||
sharedtest.NewTestHook("b"), | ||
sharedtest.NewTestHook("c"), | ||
} | ||
|
||
it := newIterator(reverse, hooks) | ||
|
||
var cursor int | ||
count := 0 | ||
if reverse { | ||
cursor = 2 | ||
} else { | ||
cursor += 0 | ||
} | ||
for it.hasNext() { | ||
index, value := it.getNext() | ||
assert.Equal(t, cursor, index) | ||
assert.Equal(t, hooks[cursor].Metadata().Name(), value.Metadata().Name()) | ||
|
||
count += 1 | ||
|
||
if reverse { | ||
cursor -= 1 | ||
} else { | ||
cursor += 1 | ||
} | ||
|
||
} | ||
assert.Equal(t, 3, count) | ||
assert.False(t, it.hasNext()) | ||
}) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
// Package hooks is an internal package containing implementations to run hooks. | ||
package hooks |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package hooks | ||
|
||
import ( | ||
"context" | ||
"sync" | ||
|
||
"github.com/launchdarkly/go-sdk-common/v3/ldcontext" | ||
"github.com/launchdarkly/go-sdk-common/v3/ldlog" | ||
"github.com/launchdarkly/go-sdk-common/v3/ldreason" | ||
"github.com/launchdarkly/go-sdk-common/v3/ldvalue" | ||
"github.com/launchdarkly/go-server-sdk/v7/ldhooks" | ||
) | ||
|
||
// Runner manages the registration and execution of hooks. | ||
type Runner struct { | ||
hooks []ldhooks.Hook | ||
loggers ldlog.Loggers | ||
mutex *sync.RWMutex | ||
} | ||
|
||
// EvaluationExecution represents the state of a running series of evaluation stages. | ||
type EvaluationExecution struct { | ||
hooks []ldhooks.Hook | ||
data []ldhooks.EvaluationSeriesData | ||
context ldhooks.EvaluationSeriesContext | ||
} | ||
|
||
func (e EvaluationExecution) withData(data []ldhooks.EvaluationSeriesData) EvaluationExecution { | ||
return EvaluationExecution{ | ||
hooks: e.hooks, | ||
context: e.context, | ||
data: data, | ||
} | ||
} | ||
|
||
// NewRunner creates a new hook runner. | ||
func NewRunner(loggers ldlog.Loggers, hooks []ldhooks.Hook) *Runner { | ||
return &Runner{ | ||
loggers: loggers, | ||
hooks: hooks, | ||
mutex: &sync.RWMutex{}, | ||
} | ||
} | ||
|
||
// AddHooks adds hooks to the runner. | ||
func (h *Runner) AddHooks(hooks ...ldhooks.Hook) { | ||
h.mutex.Lock() | ||
defer h.mutex.Unlock() | ||
|
||
h.hooks = append(h.hooks, hooks...) | ||
} | ||
|
||
// getHooks returns a copy of the hooks. This copy is suitable for use when executing a series. This keeps the set | ||
// of hooks stable for the duration of the series. This prevents things like calling the AfterEvaluation method for | ||
// a hook that didn't have the BeforeEvaluation method called. | ||
func (h *Runner) getHooks() []ldhooks.Hook { | ||
h.mutex.RLock() | ||
defer h.mutex.RUnlock() | ||
copiedHooks := make([]ldhooks.Hook, len(h.hooks)) | ||
copy(copiedHooks, h.hooks) | ||
return copiedHooks | ||
} | ||
|
||
// PrepareEvaluationSeries creates an EvaluationExecution suitable for executing evaluation stages and gets a copy | ||
// of hooks to use during series execution. | ||
// | ||
// For an invocation of a series the same set of hooks should be used. For instance a hook added mid-evaluation should | ||
// not be executed during the "AfterEvaluation" stage of that evaluation. | ||
func (h *Runner) PrepareEvaluationSeries( | ||
flagKey string, | ||
evalContext ldcontext.Context, | ||
defaultVal ldvalue.Value, | ||
method string, | ||
) EvaluationExecution { | ||
hooksForEval := h.getHooks() | ||
|
||
returnData := make([]ldhooks.EvaluationSeriesData, len(hooksForEval)) | ||
for i := range hooksForEval { | ||
returnData[i] = ldhooks.EmptyEvaluationSeriesData() | ||
} | ||
return EvaluationExecution{ | ||
hooks: hooksForEval, | ||
data: returnData, | ||
context: ldhooks.NewEvaluationSeriesContext(flagKey, evalContext, defaultVal, method), | ||
} | ||
} | ||
|
||
// BeforeEvaluation executes the BeforeEvaluation stage of registered hooks. | ||
func (h *Runner) BeforeEvaluation(ctx context.Context, execution EvaluationExecution) EvaluationExecution { | ||
return h.executeStage( | ||
execution, | ||
false, | ||
"BeforeEvaluation", | ||
func(hook ldhooks.Hook, data ldhooks.EvaluationSeriesData) (ldhooks.EvaluationSeriesData, error) { | ||
return hook.BeforeEvaluation(ctx, execution.context, data) | ||
}) | ||
} | ||
|
||
// AfterEvaluation executes the AfterEvaluation stage of registered hooks. | ||
func (h *Runner) AfterEvaluation( | ||
ctx context.Context, | ||
execution EvaluationExecution, | ||
detail ldreason.EvaluationDetail, | ||
) EvaluationExecution { | ||
return h.executeStage( | ||
execution, | ||
true, | ||
"AfterEvaluation", | ||
func(hook ldhooks.Hook, data ldhooks.EvaluationSeriesData) (ldhooks.EvaluationSeriesData, error) { | ||
return hook.AfterEvaluation(ctx, execution.context, data, detail) | ||
}) | ||
} | ||
|
||
func (h *Runner) executeStage( | ||
execution EvaluationExecution, | ||
reverse bool, | ||
stageName string, | ||
fn func( | ||
hook ldhooks.Hook, | ||
data ldhooks.EvaluationSeriesData, | ||
) (ldhooks.EvaluationSeriesData, error)) EvaluationExecution { | ||
returnData := make([]ldhooks.EvaluationSeriesData, len(execution.hooks)) | ||
iterator := newIterator(reverse, execution.hooks) | ||
for iterator.hasNext() { | ||
i, hook := iterator.getNext() | ||
|
||
outData, err := fn(hook, execution.data[i]) | ||
if err != nil { | ||
returnData[i] = execution.data[i] | ||
h.loggers.Errorf( | ||
"During evaluation of flag \"%s\", an error was encountered in \"%s\" of the \"%s\" hook: %s", | ||
execution.context.FlagKey(), | ||
stageName, | ||
hook.Metadata().Name(), | ||
err.Error()) | ||
continue | ||
} | ||
returnData[i] = outData | ||
} | ||
return execution.withData(returnData) | ||
} |
Oops, something went wrong.