406 lines
8.4 KiB
Go
406 lines
8.4 KiB
Go
|
|
package hook
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"go.uber.org/zap"
|
||
|
|
)
|
||
|
|
|
||
|
|
type HookType string
|
||
|
|
|
||
|
|
const (
|
||
|
|
HookPostCreated HookType = "post.created"
|
||
|
|
HookPostUpdated HookType = "post.updated"
|
||
|
|
HookPostDeleted HookType = "post.deleted"
|
||
|
|
HookPostPreModerate HookType = "post.pre_moderate"
|
||
|
|
HookPostModerated HookType = "post.moderated"
|
||
|
|
HookCommentCreated HookType = "comment.created"
|
||
|
|
HookCommentDeleted HookType = "comment.deleted"
|
||
|
|
HookCommentPreModerate HookType = "comment.pre_moderate"
|
||
|
|
HookCommentModerated HookType = "comment.moderated"
|
||
|
|
HookUserCreated HookType = "user.created"
|
||
|
|
HookUserUpdated HookType = "user.updated"
|
||
|
|
HookUserDeleted HookType = "user.deleted"
|
||
|
|
HookUserLogin HookType = "user.login"
|
||
|
|
HookUserLogout HookType = "user.logout"
|
||
|
|
HookGroupCreated HookType = "group.created"
|
||
|
|
HookGroupUpdated HookType = "group.updated"
|
||
|
|
HookGroupDeleted HookType = "group.deleted"
|
||
|
|
HookGroupJoined HookType = "group.joined"
|
||
|
|
HookGroupLeft HookType = "group.left"
|
||
|
|
HookMessageSent HookType = "message.sent"
|
||
|
|
HookMessageRead HookType = "message.read"
|
||
|
|
HookNotificationSent HookType = "notification.sent"
|
||
|
|
HookVoteCreated HookType = "vote.created"
|
||
|
|
HookVoteUpdated HookType = "vote.updated"
|
||
|
|
HookFileUploaded HookType = "file.uploaded"
|
||
|
|
)
|
||
|
|
|
||
|
|
type Priority int
|
||
|
|
|
||
|
|
const (
|
||
|
|
PriorityLowest Priority = 0
|
||
|
|
PriorityLow Priority = 25
|
||
|
|
PriorityNormal Priority = 50
|
||
|
|
PriorityHigh Priority = 75
|
||
|
|
PriorityHighest Priority = 100
|
||
|
|
)
|
||
|
|
|
||
|
|
type HookContext struct {
|
||
|
|
Ctx context.Context
|
||
|
|
HookType HookType
|
||
|
|
UserID uint
|
||
|
|
Data any
|
||
|
|
Metadata map[string]any
|
||
|
|
}
|
||
|
|
|
||
|
|
type HookFunc func(ctx *HookContext) error
|
||
|
|
|
||
|
|
type Hook struct {
|
||
|
|
Name string
|
||
|
|
HookType HookType
|
||
|
|
Func HookFunc
|
||
|
|
Priority Priority
|
||
|
|
Async bool
|
||
|
|
Timeout time.Duration
|
||
|
|
Enabled bool
|
||
|
|
}
|
||
|
|
|
||
|
|
type HookStats struct {
|
||
|
|
TotalCalls int64
|
||
|
|
FailedCalls int64
|
||
|
|
TotalTime time.Duration
|
||
|
|
AverageTime time.Duration
|
||
|
|
LastCalledAt time.Time
|
||
|
|
}
|
||
|
|
|
||
|
|
type hookEntry struct {
|
||
|
|
hook *Hook
|
||
|
|
stats HookStats
|
||
|
|
mu sync.Mutex
|
||
|
|
}
|
||
|
|
|
||
|
|
type Manager struct {
|
||
|
|
hooks map[HookType][]*hookEntry
|
||
|
|
mu sync.RWMutex
|
||
|
|
asyncPool chan struct{}
|
||
|
|
enabled bool
|
||
|
|
metrics *Metrics
|
||
|
|
}
|
||
|
|
|
||
|
|
type ManagerOptions struct {
|
||
|
|
MaxConcurrentAsync int
|
||
|
|
Enabled bool
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewManager(opts *ManagerOptions) *Manager {
|
||
|
|
if opts == nil {
|
||
|
|
opts = &ManagerOptions{
|
||
|
|
MaxConcurrentAsync: 100,
|
||
|
|
Enabled: true,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if opts.MaxConcurrentAsync <= 0 {
|
||
|
|
opts.MaxConcurrentAsync = 100
|
||
|
|
}
|
||
|
|
|
||
|
|
return &Manager{
|
||
|
|
hooks: make(map[HookType][]*hookEntry),
|
||
|
|
asyncPool: make(chan struct{}, opts.MaxConcurrentAsync),
|
||
|
|
enabled: opts.Enabled,
|
||
|
|
metrics: NewMetrics(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) Register(hook *Hook) {
|
||
|
|
if hook == nil || hook.Func == nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if hook.Priority == 0 {
|
||
|
|
hook.Priority = PriorityNormal
|
||
|
|
}
|
||
|
|
if hook.Timeout == 0 {
|
||
|
|
hook.Timeout = 30 * time.Second
|
||
|
|
}
|
||
|
|
hook.Enabled = true
|
||
|
|
|
||
|
|
entry := &hookEntry{
|
||
|
|
hook: hook,
|
||
|
|
stats: HookStats{},
|
||
|
|
}
|
||
|
|
|
||
|
|
m.mu.Lock()
|
||
|
|
defer m.mu.Unlock()
|
||
|
|
|
||
|
|
hooks := m.hooks[hook.HookType]
|
||
|
|
hooks = append(hooks, entry)
|
||
|
|
|
||
|
|
for i := len(hooks) - 1; i > 0; i-- {
|
||
|
|
if hooks[i].hook.Priority > hooks[i-1].hook.Priority {
|
||
|
|
hooks[i], hooks[i-1] = hooks[i-1], hooks[i]
|
||
|
|
} else {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
m.hooks[hook.HookType] = hooks
|
||
|
|
zap.L().Debug("Hook registered",
|
||
|
|
zap.String("name", hook.Name),
|
||
|
|
zap.String("type", string(hook.HookType)),
|
||
|
|
zap.Int("priority", int(hook.Priority)),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) RegisterFunc(hookType HookType, name string, fn HookFunc, opts ...HookOption) {
|
||
|
|
hook := &Hook{
|
||
|
|
Name: name,
|
||
|
|
HookType: hookType,
|
||
|
|
Func: fn,
|
||
|
|
}
|
||
|
|
for _, opt := range opts {
|
||
|
|
opt(hook)
|
||
|
|
}
|
||
|
|
m.Register(hook)
|
||
|
|
}
|
||
|
|
|
||
|
|
type HookOption func(*Hook)
|
||
|
|
|
||
|
|
func WithPriority(p Priority) HookOption {
|
||
|
|
return func(h *Hook) {
|
||
|
|
h.Priority = p
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func WithAsync() HookOption {
|
||
|
|
return func(h *Hook) {
|
||
|
|
h.Async = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func WithTimeout(timeout time.Duration) HookOption {
|
||
|
|
return func(h *Hook) {
|
||
|
|
h.Timeout = timeout
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) Trigger(ctx context.Context, hookType HookType, userID uint, data any) error {
|
||
|
|
return m.TriggerWithMetadata(ctx, hookType, userID, data, nil)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) TriggerWithMetadata(ctx context.Context, hookType HookType, userID uint, data any, metadata map[string]any) error {
|
||
|
|
if !m.enabled {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
m.mu.RLock()
|
||
|
|
entries := m.hooks[hookType]
|
||
|
|
m.mu.RUnlock()
|
||
|
|
|
||
|
|
if len(entries) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
hookCtx := &HookContext{
|
||
|
|
Ctx: ctx,
|
||
|
|
HookType: hookType,
|
||
|
|
UserID: userID,
|
||
|
|
Data: data,
|
||
|
|
Metadata: metadata,
|
||
|
|
}
|
||
|
|
|
||
|
|
var asyncHooks []*hookEntry
|
||
|
|
|
||
|
|
for _, entry := range entries {
|
||
|
|
if !entry.hook.Enabled {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
if entry.hook.Async {
|
||
|
|
asyncHooks = append(asyncHooks, entry)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := m.executeHook(entry, hookCtx); err != nil {
|
||
|
|
zap.L().Error("Hook execution failed",
|
||
|
|
zap.String("name", entry.hook.Name),
|
||
|
|
zap.String("type", string(hookType)),
|
||
|
|
zap.Error(err),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, entry := range asyncHooks {
|
||
|
|
entry := entry
|
||
|
|
go func() {
|
||
|
|
select {
|
||
|
|
case m.asyncPool <- struct{}{}:
|
||
|
|
defer func() { <-m.asyncPool }()
|
||
|
|
if err := m.executeHook(entry, hookCtx); err != nil {
|
||
|
|
zap.L().Error("Async hook execution failed",
|
||
|
|
zap.String("name", entry.hook.Name),
|
||
|
|
zap.String("type", string(hookType)),
|
||
|
|
zap.Error(err),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
default:
|
||
|
|
zap.L().Warn("Async hook pool full, skipping hook",
|
||
|
|
zap.String("name", entry.hook.Name),
|
||
|
|
zap.String("type", string(hookType)),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) executeHook(entry *hookEntry, ctx *HookContext) error {
|
||
|
|
start := time.Now()
|
||
|
|
|
||
|
|
var err error
|
||
|
|
if entry.hook.Timeout > 0 {
|
||
|
|
done := make(chan error, 1)
|
||
|
|
go func() {
|
||
|
|
defer func() {
|
||
|
|
if r := recover(); r != nil {
|
||
|
|
zap.L().Error("Hook panic recovered",
|
||
|
|
zap.String("name", entry.hook.Name),
|
||
|
|
zap.Any("panic", r),
|
||
|
|
)
|
||
|
|
done <- nil
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
done <- entry.hook.Func(ctx)
|
||
|
|
}()
|
||
|
|
|
||
|
|
select {
|
||
|
|
case err = <-done:
|
||
|
|
case <-time.After(entry.hook.Timeout):
|
||
|
|
err = context.DeadlineExceeded
|
||
|
|
zap.L().Warn("Hook timeout",
|
||
|
|
zap.String("name", entry.hook.Name),
|
||
|
|
zap.Duration("timeout", entry.hook.Timeout),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
err = entry.hook.Func(ctx)
|
||
|
|
}
|
||
|
|
|
||
|
|
duration := time.Since(start)
|
||
|
|
|
||
|
|
entry.mu.Lock()
|
||
|
|
entry.stats.TotalCalls++
|
||
|
|
entry.stats.TotalTime += duration
|
||
|
|
entry.stats.AverageTime = time.Duration(int64(entry.stats.TotalTime) / entry.stats.TotalCalls)
|
||
|
|
entry.stats.LastCalledAt = time.Now()
|
||
|
|
if err != nil {
|
||
|
|
entry.stats.FailedCalls++
|
||
|
|
}
|
||
|
|
entry.mu.Unlock()
|
||
|
|
|
||
|
|
m.metrics.RecordHookExecution(entry.hook.Name, entry.hook.HookType, duration, err == nil)
|
||
|
|
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) Unregister(hookType HookType, name string) {
|
||
|
|
m.mu.Lock()
|
||
|
|
defer m.mu.Unlock()
|
||
|
|
|
||
|
|
entries := m.hooks[hookType]
|
||
|
|
for i, entry := range entries {
|
||
|
|
if entry.hook.Name == name {
|
||
|
|
m.hooks[hookType] = append(entries[:i], entries[i+1:]...)
|
||
|
|
zap.L().Debug("Hook unregistered",
|
||
|
|
zap.String("name", name),
|
||
|
|
zap.String("type", string(hookType)),
|
||
|
|
)
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) Enable(hookType HookType, name string) {
|
||
|
|
m.mu.RLock()
|
||
|
|
entries := m.hooks[hookType]
|
||
|
|
m.mu.RUnlock()
|
||
|
|
|
||
|
|
for _, entry := range entries {
|
||
|
|
if entry.hook.Name == name {
|
||
|
|
entry.hook.Enabled = true
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) Disable(hookType HookType, name string) {
|
||
|
|
m.mu.RLock()
|
||
|
|
entries := m.hooks[hookType]
|
||
|
|
m.mu.RUnlock()
|
||
|
|
|
||
|
|
for _, entry := range entries {
|
||
|
|
if entry.hook.Name == name {
|
||
|
|
entry.hook.Enabled = false
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) GetStats(hookType HookType, name string) *HookStats {
|
||
|
|
m.mu.RLock()
|
||
|
|
entries := m.hooks[hookType]
|
||
|
|
m.mu.RUnlock()
|
||
|
|
|
||
|
|
for _, entry := range entries {
|
||
|
|
if entry.hook.Name == name {
|
||
|
|
entry.mu.Lock()
|
||
|
|
stats := entry.stats
|
||
|
|
entry.mu.Unlock()
|
||
|
|
return &stats
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) GetAllStats() map[HookType]map[string]HookStats {
|
||
|
|
m.mu.RLock()
|
||
|
|
defer m.mu.RUnlock()
|
||
|
|
|
||
|
|
result := make(map[HookType]map[string]HookStats)
|
||
|
|
for hookType, entries := range m.hooks {
|
||
|
|
result[hookType] = make(map[string]HookStats)
|
||
|
|
for _, entry := range entries {
|
||
|
|
entry.mu.Lock()
|
||
|
|
result[hookType][entry.hook.Name] = entry.stats
|
||
|
|
entry.mu.Unlock()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) GetMetrics() *Metrics {
|
||
|
|
return m.metrics
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) Clear(hookType HookType) {
|
||
|
|
m.mu.Lock()
|
||
|
|
defer m.mu.Unlock()
|
||
|
|
delete(m.hooks, hookType)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) ClearAll() {
|
||
|
|
m.mu.Lock()
|
||
|
|
defer m.mu.Unlock()
|
||
|
|
m.hooks = make(map[HookType][]*hookEntry)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) SetEnabled(enabled bool) {
|
||
|
|
m.mu.Lock()
|
||
|
|
defer m.mu.Unlock()
|
||
|
|
m.enabled = enabled
|
||
|
|
}
|