// 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...), } }