-
Notifications
You must be signed in to change notification settings - Fork 0
feat(core): define module registry and lifecycle hooks #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: topic/kaialang-eng-1063-open-core-module-system-v1
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
package core | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/wundergraph/cosmo/router/internal/utils" | ||
) | ||
|
||
// Application Lifecycle Hooks | ||
type ApplicationLifecycleHook interface { | ||
ApplicationStartHook | ||
ApplicationStopHook | ||
} | ||
|
||
type ApplicationStartHook interface { | ||
OnApplicationStart(ctx context.Context) | ||
} | ||
|
||
type ApplicationStopHook interface { | ||
OnApplicationStop(ctx context.Context) | ||
} | ||
|
||
// GraphQL Server Lifecycle Hooks | ||
type GraphQLServerLifecycleHook interface { | ||
GraphQLServerStartHook | ||
GraphQLServerStopHook | ||
} | ||
|
||
type GraphQLServerStartHook interface { | ||
OnGraphQLServerStart(ctx context.Context) | ||
} | ||
|
||
type GraphQLServerStopHook interface { | ||
OnGraphQLServerStop(ctx context.Context) | ||
} | ||
|
||
// Router Lifecycle Hooks | ||
type RouterRequestHook interface { | ||
OnRouterRequest(ctx context.Context) | ||
} | ||
|
||
type RouterResponseHook interface { | ||
OnRouterResponse(ctx context.Context) | ||
} | ||
|
||
type RouterLifecycleHook interface { | ||
RouterRequestHook | ||
RouterResponseHook | ||
} | ||
|
||
// Subgraph Lifecycle Hooks | ||
type SubgraphRequestHook interface { | ||
OnSubgraphRequest(ctx context.Context) | ||
} | ||
|
||
type SubgraphResponseHook interface { | ||
OnSubgraphResponse(ctx context.Context) | ||
} | ||
|
||
type SubgraphLifecycleHook interface { | ||
SubgraphRequestHook | ||
SubgraphResponseHook | ||
} | ||
|
||
// Operation Lifecycle Hooks | ||
type OperationLifecycleHook interface { | ||
OperationParseLifecycleHook | ||
OperationNormalizeLifecycleHook | ||
OperationValidateLifecycleHook | ||
OperationPlanLifecycleHook | ||
OperationExecuteLifecycleHook | ||
} | ||
|
||
type OperationParseLifecycleHook interface { | ||
OperationPreParseHook | ||
OperationPostParseHook | ||
} | ||
|
||
type OperationPreParseHook interface { | ||
OnOperationPreParse(ctx context.Context) | ||
} | ||
|
||
type OperationPostParseHook interface { | ||
OnOperationPostParse(ctx context.Context) | ||
} | ||
|
||
type OperationNormalizeLifecycleHook interface { | ||
OperationPreNormalizeHook | ||
OperationPostNormalizeHook | ||
} | ||
|
||
type OperationPreNormalizeHook interface { | ||
OnOperationPreNormalize(ctx context.Context) | ||
} | ||
|
||
type OperationPostNormalizeHook interface { | ||
OnOperationPostNormalize(ctx context.Context) | ||
} | ||
|
||
type OperationValidateLifecycleHook interface { | ||
OperationPreValidateHook | ||
OperationPostValidateHook | ||
} | ||
|
||
type OperationPreValidateHook interface { | ||
OnOperationPreValidate(ctx context.Context) | ||
} | ||
|
||
type OperationPostValidateHook interface { | ||
OnOperationPostValidate(ctx context.Context) | ||
} | ||
|
||
type OperationPlanLifecycleHook interface { | ||
OperationPrePlanHook | ||
OperationPostPlanHook | ||
} | ||
|
||
type OperationPrePlanHook interface { | ||
OnOperationPrePlan(ctx context.Context) | ||
} | ||
|
||
type OperationPostPlanHook interface { | ||
OnOperationPostPlan(ctx context.Context) | ||
} | ||
|
||
type OperationExecuteLifecycleHook interface { | ||
OperationPreExecuteHook | ||
OperationPostExecuteHook | ||
} | ||
|
||
type OperationPreExecuteHook interface { | ||
OnOperationPreExecute(ctx context.Context) | ||
} | ||
|
||
type OperationPostExecuteHook interface { | ||
OnOperationPostExecute(ctx context.Context) | ||
} | ||
|
||
// hookRegistry holds the list of hooks for each type. | ||
type hookRegistry struct { | ||
applicationStartHooks *utils.OrderedSet[ApplicationStartHook] | ||
applicationStopHooks *utils.OrderedSet[ApplicationStopHook] | ||
|
||
graphQLServerStartHooks *utils.OrderedSet[GraphQLServerStartHook] | ||
graphQLServerStopHooks *utils.OrderedSet[GraphQLServerStopHook] | ||
|
||
routerRequestHooks *utils.OrderedSet[RouterRequestHook] | ||
routerResponseHooks *utils.OrderedSet[RouterResponseHook] | ||
|
||
subgraphRequestHooks *utils.OrderedSet[SubgraphRequestHook] | ||
subgraphResponseHooks *utils.OrderedSet[SubgraphResponseHook] | ||
|
||
operationPreParseHooks *utils.OrderedSet[OperationPreParseHook] | ||
operationPostParseHooks *utils.OrderedSet[OperationPostParseHook] | ||
|
||
operationPreNormalizeHooks *utils.OrderedSet[OperationPreNormalizeHook] | ||
operationPostNormalizeHooks *utils.OrderedSet[OperationPostNormalizeHook] | ||
|
||
operationPreValidateHooks *utils.OrderedSet[OperationPreValidateHook] | ||
operationPostValidateHooks *utils.OrderedSet[OperationPostValidateHook] | ||
|
||
operationPrePlanHooks *utils.OrderedSet[OperationPrePlanHook] | ||
operationPostPlanHooks *utils.OrderedSet[OperationPostPlanHook] | ||
|
||
operationPreExecuteHooks *utils.OrderedSet[OperationPreExecuteHook] | ||
operationPostExecuteHooks *utils.OrderedSet[OperationPostExecuteHook] | ||
} | ||
|
||
// newHookRegistry initializes with empty sets. | ||
func newHookRegistry() *hookRegistry { | ||
return &hookRegistry{ | ||
applicationStartHooks: utils.NewOrderedSet[ApplicationStartHook](), | ||
applicationStopHooks: utils.NewOrderedSet[ApplicationStopHook](), | ||
|
||
graphQLServerStartHooks: utils.NewOrderedSet[GraphQLServerStartHook](), | ||
graphQLServerStopHooks: utils.NewOrderedSet[GraphQLServerStopHook](), | ||
|
||
routerRequestHooks: utils.NewOrderedSet[RouterRequestHook](), | ||
routerResponseHooks: utils.NewOrderedSet[RouterResponseHook](), | ||
|
||
subgraphRequestHooks: utils.NewOrderedSet[SubgraphRequestHook](), | ||
subgraphResponseHooks: utils.NewOrderedSet[SubgraphResponseHook](), | ||
|
||
operationPreParseHooks: utils.NewOrderedSet[OperationPreParseHook](), | ||
operationPostParseHooks: utils.NewOrderedSet[OperationPostParseHook](), | ||
|
||
operationPreNormalizeHooks: utils.NewOrderedSet[OperationPreNormalizeHook](), | ||
operationPostNormalizeHooks: utils.NewOrderedSet[OperationPostNormalizeHook](), | ||
|
||
operationPreValidateHooks: utils.NewOrderedSet[OperationPreValidateHook](), | ||
operationPostValidateHooks: utils.NewOrderedSet[OperationPostValidateHook](), | ||
|
||
operationPrePlanHooks: utils.NewOrderedSet[OperationPrePlanHook](), | ||
operationPostPlanHooks: utils.NewOrderedSet[OperationPostPlanHook](), | ||
|
||
operationPreExecuteHooks: utils.NewOrderedSet[OperationPreExecuteHook](), | ||
operationPostExecuteHooks: utils.NewOrderedSet[OperationPostExecuteHook](), | ||
} | ||
} | ||
|
||
// registerHook is a helper to add any hook type if implemented. | ||
func registerHook[H comparable](inst any, set *utils.OrderedSet[H]) { | ||
if h, ok := inst.(H); ok { | ||
set.Add(h) | ||
} | ||
} | ||
|
||
// AddApplicationLifecycle wires up start/stop hooks. | ||
func (hr *hookRegistry) AddApplicationLifecycle(inst any) { | ||
registerHook(inst, hr.applicationStartHooks) | ||
registerHook(inst, hr.applicationStopHooks) | ||
} | ||
|
||
// AddGraphQLServerLifecycle wires up GraphQL server start/stop hooks. | ||
func (hr *hookRegistry) AddGraphQLServerLifecycle(inst any) { | ||
registerHook(inst, hr.graphQLServerStartHooks) | ||
registerHook(inst, hr.graphQLServerStopHooks) | ||
} | ||
|
||
// AddRouterLifecycle wires up router request/response hooks. | ||
func (hr *hookRegistry) AddRouterLifecycle(inst any) { | ||
registerHook(inst, hr.routerRequestHooks) | ||
registerHook(inst, hr.routerResponseHooks) | ||
} | ||
|
||
// AddSubgraphLifecycle wires up subgraph request/response hooks. | ||
func (hr *hookRegistry) AddSubgraphLifecycle(inst any) { | ||
registerHook(inst, hr.subgraphRequestHooks) | ||
registerHook(inst, hr.subgraphResponseHooks) | ||
} | ||
|
||
// AddOperationLifecycle wires up all operation lifecycle hooks. | ||
func (hr *hookRegistry) AddOperationLifecycle(inst any) { | ||
registerHook(inst, hr.operationPreParseHooks) | ||
registerHook(inst, hr.operationPostParseHooks) | ||
registerHook(inst, hr.operationPreNormalizeHooks) | ||
registerHook(inst, hr.operationPostNormalizeHooks) | ||
registerHook(inst, hr.operationPreValidateHooks) | ||
registerHook(inst, hr.operationPostValidateHooks) | ||
registerHook(inst, hr.operationPrePlanHooks) | ||
registerHook(inst, hr.operationPostPlanHooks) | ||
registerHook(inst, hr.operationPreExecuteHooks) | ||
registerHook(inst, hr.operationPostExecuteHooks) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
package core | ||
|
||
import ( | ||
"fmt" | ||
"math" | ||
"sort" | ||
"sync" | ||
"context" | ||
"go.uber.org/zap" | ||
) | ||
|
||
type moduleRegistry struct { | ||
mu sync.RWMutex | ||
modules map[string]MyModuleInfo | ||
} | ||
// NewModuleRegistry returns an empty, thread-safe module registry. | ||
// Call this in tests (and anywhere you need isolation) instead of using the global. | ||
func newModuleRegistry() *moduleRegistry { | ||
return &moduleRegistry{ | ||
modules: make(map[string]MyModuleInfo), | ||
} | ||
} | ||
|
||
// TODO: @kaialang discuss if we should push for dependency injection. | ||
// defaultModuleRegistry is the package-level registry used by RegisterMyModule. | ||
// For unit tests you should use newModuleRegistry() to get a fresh instance and avoid shared state. | ||
var defaultModuleRegistry = newModuleRegistry() | ||
|
||
|
||
type MyModuleInfo struct { | ||
// ID is the unique identifier for a module, it must be unique across all modules. | ||
ID string | ||
// Priority decideds the order of execution of the module. | ||
// The smaller the number, the higher the priority, the earlier the module is executed. | ||
// For example, a priority of 0 is the highest priority. | ||
// Modules with the same priority are executed in the order they are registered. | ||
// If Priority is nil, the module is considered to have the lowest priority. | ||
Priority *int | ||
// New creates a new instance of the module. | ||
New func() MyModule | ||
} | ||
|
||
type MyModule interface { | ||
MyModule() MyModuleInfo | ||
} | ||
|
||
// RegisterMyModule registers a new MyModule instance. | ||
// The registration order matters. Modules with the same priority | ||
// are executed in the order they are registered. | ||
// It panics if the module is already registered. | ||
func RegisterMyModule(instance MyModule) { | ||
defaultModuleRegistry.registerMyModule(instance) | ||
} | ||
|
||
func (r *moduleRegistry) registerMyModule(instance MyModule) { | ||
m := instance.MyModule() | ||
|
||
if m.ID == "" { | ||
panic("MyModule.ID is required") | ||
} | ||
if val := m.New(); val == nil { | ||
panic("MyModuleInfo.New must return a non-nil module instance") | ||
} | ||
|
||
r.mu.Lock() | ||
defer r.mu.Unlock() | ||
|
||
if _, ok := r.modules[m.ID]; ok { | ||
panic(fmt.Sprintf("MyModule already registered: %s", m.ID)) | ||
} | ||
r.modules[m.ID] = m | ||
} | ||
|
||
// sortMyModules sorts the modules by priority, 0 is the highest priority, is the first to be executed. | ||
// If two modules have the same priority, they are sorted by registration order. | ||
// If a module has no priority, it is considered to have the lowest priority. | ||
func sortMyModules(modules []MyModuleInfo) []MyModuleInfo { | ||
sort.Slice(modules, func(i, j int) bool { | ||
var priorityI, priorityJ int = math.MaxInt, math.MaxInt | ||
if modules[i].Priority != nil { | ||
priorityI = *modules[i].Priority | ||
} | ||
if modules[j].Priority != nil { | ||
priorityJ = *modules[j].Priority | ||
} | ||
|
||
return priorityI < priorityJ | ||
}) | ||
return modules | ||
} | ||
|
||
// getMyModules returns all registered modules sorted by priority | ||
func (r *moduleRegistry) getMyModules() []MyModuleInfo { | ||
r.mu.RLock() | ||
defer r.mu.RUnlock() | ||
|
||
modules := make([]MyModuleInfo, 0, len(r.modules)) | ||
for _, m := range r.modules { | ||
modules = append(modules, m) | ||
} | ||
return sortMyModules(modules) | ||
} | ||
|
||
// coreModules manages module initialization and hook registration. | ||
type coreModules struct { | ||
hookRegistry *hookRegistry | ||
logger *zap.Logger | ||
} | ||
|
||
// newCoreModules initializes with an empty registry. | ||
func newCoreModules(logger *zap.Logger) *coreModules { | ||
return &coreModules{ | ||
hookRegistry: newHookRegistry(), | ||
logger: logger, | ||
} | ||
} | ||
|
||
// initMyModules instantiates each module, registers any implemented hooks, and saves the hook registry. | ||
func (c *coreModules) initMyModules(ctx context.Context, modules []MyModuleInfo) error { | ||
hookRegistry := newHookRegistry() | ||
|
||
for _, info := range modules { | ||
moduleInstance := info.New() | ||
|
||
hookRegistry.AddApplicationLifecycle(moduleInstance) | ||
hookRegistry.AddGraphQLServerLifecycle(moduleInstance) | ||
hookRegistry.AddRouterLifecycle(moduleInstance) | ||
hookRegistry.AddSubgraphLifecycle(moduleInstance) | ||
hookRegistry.AddOperationLifecycle(moduleInstance) | ||
} | ||
|
||
c.hookRegistry = hookRegistry | ||
|
||
return nil | ||
} | ||
Comment on lines
+119
to
+135
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems that all of my modules will be registered to all lifecycles? doesn't this mean that these modules will run on all hooks and there is no flexibility if I want to register my module on specific hook? Maybe MyModuleInfo should have a field that represents a list of hooks that you want this module to be registered and run on. Let me know if I am not understanding things correctly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My guess is that this will be called from each hook to get the list of modules to run?
I wonder if we can have a data structure that keeps lifecycle/hook to modules mapping (or update
moduleRegistry
to have that mapping), and sort the module order on register time so that we don't have to sort on hook runtime.