mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
* Enable linter: stylecheck This enabled the stylecheck linter, but excludes some staticchecks for now. The following are ignored because they will take a bit of time to address, but the intention is to address them and remove them from the exclusion list. ST1000: at least one file in a package should have a package comment ST1003: golang naming standards ST1008: error should be returned as the last argument ST1016: methods on the same type should have the same receiver name ST1020: comment on exported function * Address ST1015 For some reason this failed in CI but not locally. I can't figure out why that check isn't happening locally. This just moves the switch statements around so that the `default` is the first (or last) item. * Adjust error string in test to match case-adjusted error * Remove TestCloseTimeout
514 lines
15 KiB
Go
514 lines
15 KiB
Go
// Copyright 2022 Molecula Corp. (DBA FeatureBase).
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
package testhook
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"runtime/debug"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// KV represents a key/value mapping. You don't need to use the type
|
|
// for anything, it's just to make typing easier.
|
|
type KV map[string]interface{}
|
|
|
|
// Registry represents a set of known objects of a given common type.
|
|
// A typical implementation might maintain a map of objects it's seen which
|
|
// haven't been deleted. A minimal implementation just does nothing.
|
|
//
|
|
// A Registry may implement reference-count semantics, or may treat
|
|
// reopening of a still-open object as an error.
|
|
//
|
|
// All objects provided to a registry should have the same type.
|
|
type Registry interface {
|
|
Created(interface{}, KV) error // Object has been created, and must be destroyed later.
|
|
Opened(interface{}, KV) error // Object has been opened, and must be closed later.
|
|
Seen(interface{}, KV) error // Object has been interacted with, and should be between an open and a close.
|
|
Closed(interface{}, KV) error // Object has been closed, and must have been opened previously.
|
|
Destroyed(interface{}, KV) error // Object has been destroyed
|
|
Live() (map[interface{}]*RegistryEntry, error) // Currently live objects
|
|
}
|
|
|
|
// RegistryHook is a generic interface, but any real implementation should
|
|
// implement at least one of RegistryHookPreOpen, RegistryHookPostOpen,
|
|
// etcetera. Pre-hooks are called before any error checks by the registry,
|
|
// post-hooks are called with after those error checks, and are passed the
|
|
// (possibly nil) error that would be returned at that point. If the post-hook
|
|
// overrides the error, the registry continues with operations as though it
|
|
// hadn't occurred, which may be a very bad idea.
|
|
type RegistryHook interface{}
|
|
|
|
type RegistryHookPreCreate interface {
|
|
Created(interface{}, KV, *RegistryEntry) error
|
|
}
|
|
|
|
type RegistryHookPostCreate interface {
|
|
WasCreated(interface{}, KV, *RegistryEntry, error) error
|
|
}
|
|
|
|
type RegistryHookPreOpen interface {
|
|
Opened(interface{}, KV, *RegistryEntry) error
|
|
}
|
|
|
|
type RegistryHookPostOpen interface {
|
|
WasOpened(interface{}, KV, *RegistryEntry, error) error
|
|
}
|
|
|
|
type RegistryHookPreSee interface {
|
|
Seen(interface{}, KV, *RegistryEntry) error
|
|
}
|
|
|
|
type RegistryHookPostSee interface {
|
|
WasSeen(interface{}, KV, *RegistryEntry, error) error
|
|
}
|
|
|
|
type RegistryHookPreClose interface {
|
|
Closed(interface{}, KV, *RegistryEntry) error
|
|
}
|
|
|
|
type RegistryHookPostClose interface {
|
|
WasClosed(interface{}, KV, *RegistryEntry, error) error
|
|
}
|
|
|
|
type RegistryHookPreDestroy interface {
|
|
Destroyed(interface{}, KV, *RegistryEntry) error
|
|
}
|
|
|
|
type RegistryHookPostDestroy interface {
|
|
WasDestroyed(interface{}, KV, *RegistryEntry, error) error
|
|
}
|
|
|
|
// RegistryHookLive is an interface implemented by a registry hook.
|
|
//
|
|
// If a registry's hooks implement RegistryHookLive, Live() should
|
|
// return only those entries for which a non-nil error was returned, with the
|
|
// error inserted in the RegistryEntry.
|
|
type RegistryHookLive interface {
|
|
Live(interface{}, *RegistryEntry) error
|
|
}
|
|
|
|
// RegistryHooks represents a set of registry hook values to use for
|
|
// registries, corresponding to different types.
|
|
type RegistryHooks map[reflect.Type]RegistryHook
|
|
|
|
// RegistryEntry represents the data we might have about an entry. Every
|
|
// entry in it could be zero-valued in some implementations
|
|
type RegistryEntry struct {
|
|
Error error
|
|
Stack []byte
|
|
Stamp time.Time
|
|
Data KV
|
|
OpenCount int
|
|
}
|
|
|
|
// NopRegistry doesn't do anything; it exists to fit the interface but not
|
|
// consume resources.
|
|
type NopRegistry struct{}
|
|
|
|
var _ Registry = &NopRegistry{}
|
|
|
|
func (*NopRegistry) Created(interface{}, KV) error { return nil }
|
|
func (*NopRegistry) Opened(interface{}, KV) error { return nil }
|
|
func (*NopRegistry) Seen(interface{}, KV) error { return nil }
|
|
func (*NopRegistry) Closed(interface{}, KV) error { return nil }
|
|
func (*NopRegistry) Destroyed(interface{}, KV) error { return nil }
|
|
func (*NopRegistry) Live() (map[interface{}]*RegistryEntry, error) { return nil, nil }
|
|
|
|
func NewNopRegistry() *NopRegistry {
|
|
return &NopRegistry{}
|
|
}
|
|
|
|
// SimpleRegistry asserts that objects are created, then opened, then
|
|
// possibly seen, then closed, then destroyed, and that they are not
|
|
// opened more than once at a time, or destroyed while open. As a
|
|
// convenience feature for users whose use cases might rely on this,
|
|
// it will actually accept a new item being opened without being
|
|
// previously created; to prevent this, use a PreOpen hook that checks
|
|
// for a nil *RegistryEntry. The default Live check will report only
|
|
// objects with an open count other than 0, but if you provide a Live
|
|
// hook, you can return errors for objects still existing.
|
|
type SimpleRegistry struct {
|
|
hooks RegistryHook
|
|
entries map[interface{}]*RegistryEntry
|
|
mu sync.Mutex
|
|
}
|
|
|
|
var _ Registry = &SimpleRegistry{}
|
|
|
|
func NewSimpleRegistry(hooks RegistryHook) *SimpleRegistry {
|
|
return &SimpleRegistry{entries: map[interface{}]*RegistryEntry{}, hooks: hooks}
|
|
}
|
|
|
|
func (s *SimpleRegistry) Created(o interface{}, kv KV) (err error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
existing := s.entries[o]
|
|
if hook, ok := s.hooks.(RegistryHookPreCreate); ok {
|
|
if err := hook.Created(o, kv, existing); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if existing != nil {
|
|
err = fmt.Errorf("object %T:%v previously registered at %v", o, o, existing.Stamp)
|
|
} else {
|
|
existing = &RegistryEntry{
|
|
Stack: debug.Stack(),
|
|
Data: kv,
|
|
Stamp: time.Now(),
|
|
}
|
|
}
|
|
if hook, ok := s.hooks.(RegistryHookPostCreate); ok {
|
|
err = hook.WasCreated(o, kv, existing, err)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// if you overrode the error, or there wasn't one and you didn't
|
|
// create one, we now stash the entry.
|
|
s.entries[o] = existing
|
|
return nil
|
|
}
|
|
|
|
func (s *SimpleRegistry) Opened(o interface{}, kv KV) (err error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
existing := s.entries[o]
|
|
if hook, ok := s.hooks.(RegistryHookPreOpen); ok {
|
|
if err := hook.Opened(o, kv, existing); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if existing == nil {
|
|
existing = &RegistryEntry{
|
|
Stack: debug.Stack(),
|
|
Data: kv,
|
|
Stamp: time.Now(),
|
|
}
|
|
s.entries[o] = existing
|
|
}
|
|
existing.OpenCount++
|
|
if existing.OpenCount > 1 {
|
|
err = fmt.Errorf("object %T:%v opened %d times", o, o, existing.OpenCount)
|
|
}
|
|
if hook, ok := s.hooks.(RegistryHookPostOpen); ok {
|
|
err = hook.WasOpened(o, kv, existing, err)
|
|
}
|
|
if err != nil {
|
|
if existing != nil {
|
|
existing.OpenCount--
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *SimpleRegistry) Seen(o interface{}, kv KV) (err error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
existing := s.entries[o]
|
|
if hook, ok := s.hooks.(RegistryHookPreSee); ok {
|
|
if err = hook.Seen(o, kv, existing); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if existing == nil {
|
|
err = fmt.Errorf("object %T:%v seen but not previously registered", o, o)
|
|
}
|
|
if hook, ok := s.hooks.(RegistryHookPostSee); ok {
|
|
err = hook.WasSeen(o, kv, existing, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *SimpleRegistry) Closed(o interface{}, kv KV) (err error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
existing := s.entries[o]
|
|
if hook, ok := s.hooks.(RegistryHookPreClose); ok {
|
|
if err = hook.Closed(o, kv, existing); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if existing == nil {
|
|
err = fmt.Errorf("object %T:%v closed but not previously registered", o, o)
|
|
} else {
|
|
existing.OpenCount--
|
|
if existing.OpenCount < 0 {
|
|
err = fmt.Errorf("object %T:%v closed more often than it was open: %d", o, o, existing.OpenCount)
|
|
}
|
|
}
|
|
if hook, ok := s.hooks.(RegistryHookPostClose); ok {
|
|
err = hook.WasClosed(o, kv, existing, err)
|
|
}
|
|
if err != nil {
|
|
// if a close "failed", we don't want to count it as being closed.
|
|
if existing != nil {
|
|
existing.OpenCount++
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *SimpleRegistry) Destroyed(o interface{}, kv KV) (err error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
existing := s.entries[o]
|
|
if hook, ok := s.hooks.(RegistryHookPreDestroy); ok {
|
|
if err = hook.Destroyed(o, kv, existing); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if existing == nil {
|
|
err = fmt.Errorf("object %T:%v destroyed but not previously registered", o, o)
|
|
} else if existing.OpenCount != 0 {
|
|
err = fmt.Errorf("object %T:%v destroyed while open count is %d", o, o, existing.OpenCount)
|
|
}
|
|
if hook, ok := s.hooks.(RegistryHookPostDestroy); ok {
|
|
err = hook.WasDestroyed(o, kv, existing, err)
|
|
}
|
|
if err != nil {
|
|
fmt.Printf("destroyed error: %v\n", err)
|
|
return err
|
|
}
|
|
// remove the entry from the list.
|
|
delete(s.entries, o)
|
|
return nil
|
|
}
|
|
|
|
func (s *SimpleRegistry) Live() (results map[interface{}]*RegistryEntry, err error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
results = make(map[interface{}]*RegistryEntry)
|
|
if hook, ok := s.hooks.(RegistryHookLive); ok {
|
|
for k, v := range s.entries {
|
|
v.Error = hook.Live(k, v)
|
|
if v.Error != nil {
|
|
results[k] = v
|
|
}
|
|
}
|
|
} else {
|
|
for k, v := range s.entries {
|
|
if v.OpenCount != 0 {
|
|
v.Error = fmt.Errorf("open count %d", v.OpenCount)
|
|
results[k] = v
|
|
}
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
type PreHook func(interface{}, KV, *RegistryEntry) error
|
|
type PostHook func(interface{}, KV, *RegistryEntry, error) error
|
|
type LiveHook func(interface{}, *RegistryEntry) error
|
|
|
|
// PluggableRegistry is a registry which takes dynamically-generated functions,
|
|
// and calls them if they're not nil.
|
|
type PluggableRegistry struct {
|
|
implCreated PreHook
|
|
implWasCreated PostHook
|
|
implOpened PreHook
|
|
implWasOpened PostHook
|
|
implSeen PreHook
|
|
implWasSeen PostHook
|
|
implClosed PreHook
|
|
implWasClosed PostHook
|
|
implDestroyed PreHook
|
|
implWasDestroyed PostHook
|
|
implLive LiveHook
|
|
}
|
|
|
|
func (p *PluggableRegistry) Created(i interface{}, kv KV, ent *RegistryEntry) error {
|
|
if p.implCreated != nil {
|
|
return p.implCreated(i, kv, ent)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *PluggableRegistry) WasCreated(i interface{}, kv KV, ent *RegistryEntry, err error) error {
|
|
if p.implWasCreated != nil {
|
|
return p.implWasCreated(i, kv, ent, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *PluggableRegistry) Opened(i interface{}, kv KV, ent *RegistryEntry) error {
|
|
if p.implOpened != nil {
|
|
return p.implOpened(i, kv, ent)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *PluggableRegistry) WasOpened(i interface{}, kv KV, ent *RegistryEntry, err error) error {
|
|
if p.implWasOpened != nil {
|
|
return p.implWasOpened(i, kv, ent, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *PluggableRegistry) Seen(i interface{}, kv KV, ent *RegistryEntry) error {
|
|
if p.implSeen != nil {
|
|
return p.implSeen(i, kv, ent)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *PluggableRegistry) WasSeen(i interface{}, kv KV, ent *RegistryEntry, err error) error {
|
|
if p.implWasSeen != nil {
|
|
return p.implWasSeen(i, kv, ent, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *PluggableRegistry) Closed(i interface{}, kv KV, ent *RegistryEntry) error {
|
|
if p.implClosed != nil {
|
|
return p.implClosed(i, kv, ent)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *PluggableRegistry) WasClosed(i interface{}, kv KV, ent *RegistryEntry, err error) error {
|
|
if p.implWasClosed != nil {
|
|
return p.implWasClosed(i, kv, ent, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *PluggableRegistry) Destroyed(i interface{}, kv KV, ent *RegistryEntry) error {
|
|
if p.implDestroyed != nil {
|
|
return p.implDestroyed(i, kv, ent)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *PluggableRegistry) WasDestroyed(i interface{}, kv KV, ent *RegistryEntry, err error) error {
|
|
if p.implWasDestroyed != nil {
|
|
return p.implWasDestroyed(i, kv, ent, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *PluggableRegistry) Live(i interface{}, ent *RegistryEntry) error {
|
|
if p.implLive != nil {
|
|
return p.implLive(i, ent)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func composePreHooks(fns ...PreHook) PreHook {
|
|
if len(fns) == 0 {
|
|
return nil
|
|
}
|
|
if len(fns) == 1 {
|
|
return fns[0]
|
|
}
|
|
return func(i interface{}, kv KV, ent *RegistryEntry) error {
|
|
for _, fn := range fns {
|
|
err := fn(i, kv, ent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func composePostHooks(fns ...PostHook) PostHook {
|
|
if len(fns) == 0 {
|
|
return nil
|
|
}
|
|
if len(fns) == 1 {
|
|
return fns[0]
|
|
}
|
|
return func(i interface{}, kv KV, ent *RegistryEntry, err error) error {
|
|
// We run the functions in reverse order, so the last
|
|
// hook added has the option of overriding a lower hook's
|
|
// opinion.
|
|
for i := range fns {
|
|
fn := fns[len(fns)-1-i]
|
|
err = fn(i, kv, ent, err)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
func composeLiveHooks(fns ...LiveHook) LiveHook {
|
|
if len(fns) == 0 {
|
|
return nil
|
|
}
|
|
if len(fns) == 1 {
|
|
return fns[0]
|
|
}
|
|
return func(i interface{}, ent *RegistryEntry) error {
|
|
for _, fn := range fns {
|
|
err := fn(i, ent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Compose takes a list of RegistryHook objects, and combines their hook
|
|
// functions into a unified registry. For Pre hooks and Live hooks, the
|
|
// functions are called in the order they were provided to this function,
|
|
// and the first one to return an error causes the remainder to be
|
|
// skipped; for Post hooks, they are called in the opposite order, and
|
|
// the whole list continues to be called, with each function getting passed
|
|
// the error (or non-error) value returned by the previous one.
|
|
func Compose(hooks ...RegistryHook) *PluggableRegistry {
|
|
var created, opened, seen, closed, destroyed []PreHook
|
|
var wasCreated, wasOpened, wasSeen, wasClosed, wasDestroyed []PostHook
|
|
var live []LiveHook
|
|
for _, h := range hooks {
|
|
if hook, ok := h.(RegistryHookPreCreate); ok {
|
|
created = append(created, hook.Created)
|
|
}
|
|
if hook, ok := h.(RegistryHookPostCreate); ok {
|
|
wasCreated = append(wasCreated, hook.WasCreated)
|
|
}
|
|
if hook, ok := h.(RegistryHookPreOpen); ok {
|
|
opened = append(opened, hook.Opened)
|
|
}
|
|
if hook, ok := h.(RegistryHookPostOpen); ok {
|
|
wasOpened = append(wasOpened, hook.WasOpened)
|
|
}
|
|
if hook, ok := h.(RegistryHookPreSee); ok {
|
|
seen = append(seen, hook.Seen)
|
|
}
|
|
if hook, ok := h.(RegistryHookPostSee); ok {
|
|
wasSeen = append(wasSeen, hook.WasSeen)
|
|
}
|
|
if hook, ok := h.(RegistryHookPreClose); ok {
|
|
closed = append(closed, hook.Closed)
|
|
}
|
|
if hook, ok := h.(RegistryHookPostClose); ok {
|
|
wasClosed = append(wasClosed, hook.WasClosed)
|
|
}
|
|
if hook, ok := h.(RegistryHookPreDestroy); ok {
|
|
destroyed = append(destroyed, hook.Destroyed)
|
|
}
|
|
if hook, ok := h.(RegistryHookPostDestroy); ok {
|
|
wasDestroyed = append(wasDestroyed, hook.WasDestroyed)
|
|
}
|
|
if hook, ok := h.(RegistryHookLive); ok {
|
|
live = append(live, hook.Live)
|
|
}
|
|
}
|
|
return &PluggableRegistry{
|
|
implCreated: composePreHooks(created...),
|
|
implWasCreated: composePostHooks(wasCreated...),
|
|
implOpened: composePreHooks(opened...),
|
|
implWasOpened: composePostHooks(wasOpened...),
|
|
implSeen: composePreHooks(seen...),
|
|
implWasSeen: composePostHooks(wasSeen...),
|
|
implClosed: composePreHooks(closed...),
|
|
implWasClosed: composePostHooks(wasClosed...),
|
|
implDestroyed: composePreHooks(destroyed...),
|
|
implWasDestroyed: composePostHooks(wasDestroyed...),
|
|
implLive: composeLiveHooks(live...),
|
|
}
|
|
}
|