diff --git a/Makefile b/Makefile index 5bd7f5d..bc68268 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,13 @@ +build-modules: + CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -buildmode=plugin -o $(DEST)/module_set/module_set.so ./volumes/modules/module_set/module_set.go && \ +CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -buildmode=plugin -o $(DEST)/module_get/module_get.so ./volumes/modules/module_get/module_get.go + build-server: - CC=$(CC) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(DEST)/server ./cmd/main.go + CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(DEST)/server ./cmd/main.go build: - env CC=x86_64-linux-musl-gcc GOOS=linux GOARCH=amd64 DEST=bin/linux/x86_64 make build-server + env CGO_ENABLED=1 CC=x86_64-linux-musl-gcc GOOS=linux GOARCH=amd64 DEST=volumes/modules make build-modules && \ + env CGO_ENABLED=1 CC=x86_64-linux-musl-gcc GOOS=linux GOARCH=amd64 DEST=bin/linux/x86_64 make build-server run: make build && docker-compose up --build diff --git a/docker-compose.yaml b/docker-compose.yaml index 6ca6c93..ad1053d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -40,15 +40,15 @@ services: # List of client certificate authorities - CLIENT_CA_1=/etc/ssl/certs/echovault/client/rootCA.crt # List of shared object plugins to load on startup - - MODULE_1=/lib/echovault/modules/module_1.so - - MODULE_2=/lib/echovault/modules/module_2.so + - MODULE_1=/lib/echovault/modules/module_set/module_set.so + - MODULE_2=/lib/echovault/modules/module_get/module_get.so ports: - "7480:7480" - "7946:7946" - "7999:8000" volumes: - ./volumes/config:/etc/echovault/config - - ./volumes/plugins:/lib/echovault/plugins + - ./volumes/modules:/lib/echovault/modules - ./volumes/nodes/standalone_node:/var/lib/echovault networks: - testnet @@ -87,8 +87,8 @@ services: # List of client certificate authorities - CLIENT_CA_1=/etc/ssl/certs/echovault/client/rootCA.crt # List of shared object plugins to load on startup - - MODULE_1=/lib/echovault/modules/module_1.so - - MODULE_2=/lib/echovault/modules/module_2.so + - MODULE_1=/lib/echovault/modules/module_set/module_set.so + - MODULE_2=/lib/echovault/modules/module_get/module_get.so ports: - "7481:7480" - "7945:7946" @@ -134,8 +134,8 @@ services: # List of client certificate authorities - CLIENT_CA_1=/etc/ssl/certs/echovault/client/rootCA.crt # List of shared object plugins to load on startup - - MODULE_1=/lib/echovault/modules/module_1.so - - MODULE_2=/lib/echovault/modules/module_2.so + - MODULE_1=/lib/echovault/modules/module_set/module_set.so + - MODULE_2=/lib/echovault/modules/module_get/module_get.so ports: - "7482:7480" - "7947:7946" @@ -181,8 +181,8 @@ services: # List of client certificate authorities - CLIENT_CA_1=/etc/ssl/certs/echovault/client/rootCA.crt # List of shared object plugins to load on startup - - MODULE_1=/lib/echovault/modules/module_1.so - - MODULE_2=/lib/echovault/modules/module_2.so + - MODULE_1=/lib/echovault/modules/module_set/module_set.so + - MODULE_2=/lib/echovault/modules/module_get/module_get.so ports: - "7483:7480" - "7948:7946" @@ -228,8 +228,8 @@ services: # List of client certificate authorities - CLIENT_CA_1=/etc/ssl/certs/echovault/client/rootCA.crt # List of shared object plugins to load on startup - - MODULE_1=/lib/echovault/modules/module_1.so - - MODULE_2=/lib/echovault/modules/module_2.so + - MODULE_1=/lib/echovault/modules/module_set/module_set.so + - MODULE_2=/lib/echovault/modules/module_get/module_get.so ports: - "7484:7480" - "7949:7946" @@ -275,8 +275,8 @@ services: # List of client certificate authorities - CLIENT_CA_1=/etc/ssl/certs/echovault/client/rootCA.crt # List of shared object plugins to load on startup - - MODULE_1=/lib/echovault/modules/module_1.so - - MODULE_2=/lib/echovault/modules/module_2.so + - MODULE_1=/lib/echovault/modules/module_set/module_set.so + - MODULE_2=/lib/echovault/modules/module_get/module_get.so ports: - "7485:7480" - "7950:7946" diff --git a/echovault/api_admin.go b/echovault/api_admin.go index e18e57f..60b7c76 100644 --- a/echovault/api_admin.go +++ b/echovault/api_admin.go @@ -231,6 +231,8 @@ func (server *EchoVault) RewriteAOF() (string, error) { // // "command already exists" - If a command with the same command name as the passed command already exists. func (server *EchoVault) AddCommand(command CommandOptions) error { + server.commandsRWMut.Lock() + defer server.commandsRWMut.Unlock() // Check if command already exists for _, c := range server.commands { if strings.EqualFold(c.Command, command.Command) { @@ -398,6 +400,9 @@ func (server *EchoVault) ExecuteCommand(command ...string) ([]byte, error) { // // `command` - ...string. func (server *EchoVault) RemoveCommand(command ...string) { + server.commandsRWMut.Lock() + defer server.commandsRWMut.Unlock() + switch len(command) { case 1: // Remove command diff --git a/echovault/echovault.go b/echovault/echovault.go index 3c64a71..0f73c82 100644 --- a/echovault/echovault.go +++ b/echovault/echovault.go @@ -82,8 +82,9 @@ type EchoVault struct { } // Holds the list of all commands supported by the echovault. - commands []internal.Command - getCommands func() []internal.Command + commandsRWMut sync.RWMutex + commands []internal.Command + getCommands func() []internal.Command raft *raft.Raft // The raft replication layer for the echovault. memberList *memberlist.MemberList // The memberlist layer for the echovault. @@ -133,6 +134,7 @@ func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) { store: make(map[string]internal.KeyData), keyLocks: make(map[string]*sync.RWMutex), keyCreationLock: &sync.Mutex{}, + commandsRWMut: sync.RWMutex{}, commands: func() []internal.Command { var commands []internal.Command commands = append(commands, acl.Commands()...) @@ -161,8 +163,10 @@ func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) { // Load .so modules from config for _, path := range echovault.config.Modules { if err := echovault.LoadModule(path); err != nil { - log.Println(err) + log.Printf("%s %v\n", path, err) + continue } + log.Printf("loaded plugin %s\n", path) } // Function for server commands retrieval diff --git a/echovault/modules.go b/echovault/modules.go index a13aa41..daf5654 100644 --- a/echovault/modules.go +++ b/echovault/modules.go @@ -25,6 +25,8 @@ import ( ) func (server *EchoVault) getCommand(cmd string) (internal.Command, error) { + server.commandsRWMut.RLock() + defer server.commandsRWMut.RUnlock() for _, command := range server.commands { if strings.EqualFold(command.Command, cmd) { return command, nil @@ -52,6 +54,9 @@ func (server *EchoVault) getHandlerFuncParams(ctx context.Context, cmd []string, TakeSnapshot: server.takeSnapshot, GetLatestSnapshotTime: server.getLatestSnapshotTime, RewriteAOF: server.rewriteAOF, + LoadModule: server.LoadModule, + UnloadModule: server.UnloadModule, + ListModules: server.ListModules, GetClock: server.getClock, GetPubSub: server.getPubSub, GetACL: server.getACL, diff --git a/echovault/plugin.go b/echovault/plugin.go index 846b01f..5df4c70 100644 --- a/echovault/plugin.go +++ b/echovault/plugin.go @@ -3,23 +3,25 @@ package echovault import ( "context" "errors" + "fmt" "github.com/echovault/echovault/internal" "plugin" "slices" "strings" ) +// TODO: Add godoc comment func (server *EchoVault) LoadModule(path string, args ...string) error { p, err := plugin.Open(path) if err != nil { - return err + return fmt.Errorf("plugin open: %v", err) } commandSymbol, err := p.Lookup("Command") if err != nil { return err } - command, ok := commandSymbol.(string) + command, ok := commandSymbol.(*string) if !ok { return errors.New("command symbol is not a string") } @@ -28,7 +30,7 @@ func (server *EchoVault) LoadModule(path string, args ...string) error { if err != nil { return err } - categories, ok := categoriesSymbol.([]string) + categories, ok := categoriesSymbol.(*[]string) if !ok { return errors.New("categories symbol not a string slice") } @@ -37,7 +39,7 @@ func (server *EchoVault) LoadModule(path string, args ...string) error { if err != nil { return err } - description, ok := descriptionSymbol.(string) + description, ok := descriptionSymbol.(*string) if !ok { return errors.New("description symbol is no a string") } @@ -46,14 +48,14 @@ func (server *EchoVault) LoadModule(path string, args ...string) error { if err != nil { return err } - sync, ok := syncSymbol.(bool) + sync, ok := syncSymbol.(*bool) if !ok { return errors.New("sync symbol is not a bool") } keyExtractionFuncSymbol, err := p.Lookup("KeyExtractionFunc") if err != nil { - return err + return fmt.Errorf("key extraction func symbol: %v", err) } keyExtractionFunc, ok := keyExtractionFuncSymbol.(func(cmd []string, args ...string) ([]string, []string, error)) if !ok { @@ -62,7 +64,7 @@ func (server *EchoVault) LoadModule(path string, args ...string) error { handlerFuncSymbol, err := p.Lookup("HandlerFunc") if err != nil { - return err + return fmt.Errorf("handler func symbol: %v", err) } handlerFunc, ok := handlerFuncSymbol.(func( ctx context.Context, @@ -81,19 +83,22 @@ func (server *EchoVault) LoadModule(path string, args ...string) error { return errors.New("handler function has unexpected signature") } + server.commandsRWMut.Lock() + defer server.commandsRWMut.Unlock() + server.commands = append(server.commands, internal.Command{ - Command: command, + Command: *command, Module: path, Categories: func() []string { // Convert all the categories to lower case for uniformity - cats := make([]string, len(categories)) - for i, cat := range categories { + cats := make([]string, len(*categories)) + for i, cat := range *categories { cats[i] = strings.ToLower(cat) } return cats }(), - Description: description, - Sync: sync, + Description: *description, + Sync: *sync, SubCommands: make([]internal.SubCommand, 0), KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) { readKeys, writeKeys, err := keyExtractionFunc(cmd, args...) @@ -125,8 +130,26 @@ func (server *EchoVault) LoadModule(path string, args ...string) error { return nil } +// TODO: Add godoc comment func (server *EchoVault) UnloadModule(module string) { + server.commandsRWMut.Lock() + defer server.commandsRWMut.Unlock() server.commands = slices.DeleteFunc(server.commands, func(command internal.Command) bool { return strings.EqualFold(command.Module, module) }) } + +// TODO: Add godoc comment +func (server *EchoVault) ListModules() []string { + server.commandsRWMut.RLock() + defer server.commandsRWMut.RUnlock() + var modules []string + for _, command := range server.commands { + if !slices.ContainsFunc(modules, func(module string) bool { + return strings.EqualFold(module, command.Module) + }) { + modules = append(modules, strings.ToLower(command.Module)) + } + } + return modules +} diff --git a/internal/modules/admin/commands.go b/internal/modules/admin/commands.go index 18591ba..e5b5f56 100644 --- a/internal/modules/admin/commands.go +++ b/internal/modules/admin/commands.go @@ -199,9 +199,7 @@ func Commands() []internal.Command { Sync: false, KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) { return internal.KeyExtractionFuncResult{ - Channels: make([]string, 0), - ReadKeys: make([]string, 0), - WriteKeys: make([]string, 0), + Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, HandlerFunc: handleGetAllCommands, @@ -214,9 +212,7 @@ func Commands() []internal.Command { Sync: false, KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) { return internal.KeyExtractionFuncResult{ - Channels: make([]string, 0), - ReadKeys: make([]string, 0), - WriteKeys: make([]string, 0), + Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, SubCommands: []internal.SubCommand{ @@ -228,9 +224,7 @@ func Commands() []internal.Command { Sync: false, KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) { return internal.KeyExtractionFuncResult{ - Channels: make([]string, 0), - ReadKeys: make([]string, 0), - WriteKeys: make([]string, 0), + Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, HandlerFunc: handleCommandDocs, @@ -243,9 +237,7 @@ func Commands() []internal.Command { Sync: false, KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) { return internal.KeyExtractionFuncResult{ - Channels: make([]string, 0), - ReadKeys: make([]string, 0), - WriteKeys: make([]string, 0), + Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, HandlerFunc: handleCommandCount, @@ -259,9 +251,7 @@ Allows for filtering by ACL category or glob pattern.`, Sync: false, KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) { return internal.KeyExtractionFuncResult{ - Channels: make([]string, 0), - ReadKeys: make([]string, 0), - WriteKeys: make([]string, 0), + Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, HandlerFunc: handleCommandList, @@ -276,9 +266,7 @@ Allows for filtering by ACL category or glob pattern.`, Sync: true, KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) { return internal.KeyExtractionFuncResult{ - Channels: make([]string, 0), - ReadKeys: make([]string, 0), - WriteKeys: make([]string, 0), + Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, HandlerFunc: func(params internal.HandlerFuncParams) ([]byte, error) { @@ -296,9 +284,7 @@ Allows for filtering by ACL category or glob pattern.`, Sync: false, KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) { return internal.KeyExtractionFuncResult{ - Channels: make([]string, 0), - ReadKeys: make([]string, 0), - WriteKeys: make([]string, 0), + Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, HandlerFunc: func(params internal.HandlerFuncParams) ([]byte, error) { @@ -317,9 +303,7 @@ Allows for filtering by ACL category or glob pattern.`, Sync: false, KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) { return internal.KeyExtractionFuncResult{ - Channels: make([]string, 0), - ReadKeys: make([]string, 0), - WriteKeys: make([]string, 0), + Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, HandlerFunc: func(params internal.HandlerFuncParams) ([]byte, error) { @@ -329,5 +313,84 @@ Allows for filtering by ACL category or glob pattern.`, return []byte(constants.OkResponse), nil }, }, + { + Command: "module", + Module: constants.AdminModule, + Categories: []string{}, + Description: "Module commands", + KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) { + return internal.KeyExtractionFuncResult{ + Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), + }, nil + }, + SubCommands: []internal.SubCommand{ + { + Command: "load", + Module: constants.AdminModule, + Categories: []string{constants.AdminCategory, constants.SlowCategory, constants.DangerousCategory}, + Description: `(MODULE LOAD path [arg [arg ...]]) Load a module from a dynamic library at runtime. +The path should be the full path to the module, including the .so filename. Any args will be be passed unmodified to the +module's key extraction and handler functions.`, + Sync: true, + KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) { + return internal.KeyExtractionFuncResult{ + Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), + }, nil + }, + HandlerFunc: func(params internal.HandlerFuncParams) ([]byte, error) { + if len(params.Command) < 3 { + return nil, errors.New(constants.WrongArgsResponse) + } + var args []string + if len(params.Command) > 3 { + args = params.Command[3:] + } + if err := params.LoadModule(params.Command[2], args...); err != nil { + return nil, err + } + return []byte(constants.OkResponse), nil + }, + }, + { + Command: "unload", + Module: constants.AdminModule, + Categories: []string{constants.AdminCategory, constants.SlowCategory, constants.DangerousCategory}, + Description: `(MODULE UNLOAD name) Unloads a module based on the its name as displayed by the MODULE LIST command.`, + Sync: true, + KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) { + return internal.KeyExtractionFuncResult{ + Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), + }, nil + }, + HandlerFunc: func(params internal.HandlerFuncParams) ([]byte, error) { + if len(params.Command) != 3 { + return nil, errors.New(constants.WrongArgsResponse) + } + params.UnloadModule(params.Command[2]) + return []byte(constants.OkResponse), nil + }, + }, + { + Command: "list", + Module: constants.AdminModule, + Categories: []string{constants.AdminModule, constants.SlowCategory, constants.DangerousCategory}, + Description: `(MODULE LIST) List all the modules that are currently loaded in the server.`, + Sync: false, + KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) { + return internal.KeyExtractionFuncResult{ + Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), + }, nil + }, + HandlerFunc: func(params internal.HandlerFuncParams) ([]byte, error) { + modules := params.ListModules() + res := fmt.Sprintf("*%d\r\n", len(modules)) + for _, module := range modules { + res += fmt.Sprintf("$%d\r\n%s\r\n", len(module), module) + } + return []byte(res), nil + }, + }, + }, + }, } } diff --git a/internal/types.go b/internal/types.go index 56958cc..d17e5c7 100644 --- a/internal/types.go +++ b/internal/types.go @@ -78,6 +78,9 @@ type HandlerFuncParams struct { TakeSnapshot func() error RewriteAOF func() error GetLatestSnapshotTime func() int64 + LoadModule func(path string, args ...string) error + UnloadModule func(module string) + ListModules func() []string } type HandlerFunc func(params HandlerFuncParams) ([]byte, error) diff --git a/volumes/modules/module_1.go b/volumes/modules/module_1.go deleted file mode 100644 index 11174d7..0000000 --- a/volumes/modules/module_1.go +++ /dev/null @@ -1 +0,0 @@ -package modules diff --git a/volumes/modules/module_2.go b/volumes/modules/module_2.go deleted file mode 100644 index 11174d7..0000000 --- a/volumes/modules/module_2.go +++ /dev/null @@ -1 +0,0 @@ -package modules diff --git a/volumes/modules/module_get/module_get.go b/volumes/modules/module_get/module_get.go new file mode 100644 index 0000000..c7415cb --- /dev/null +++ b/volumes/modules/module_get/module_get.go @@ -0,0 +1,59 @@ +package main + +import ( + "context" + "fmt" +) + +var Command string = "Module.Get" + +var Categories []string = []string{"read", "fast"} + +var Description string = `(Module.Get key) This module fetches the integer value from the key and returns the value ^ 2. +0 is returned if the key does not exist. An error is returned if the value is not an integer.` + +var Sync bool = false + +func KeyExtractionFunc(cmd []string, args ...string) ([]string, []string, error) { + if len(cmd) != 2 { + return nil, nil, fmt.Errorf("wrong no of args for %s command", Command) + } + return cmd[1:], []string{}, nil +} + +func HandlerFunc( + ctx context.Context, + command []string, + keyExists func(ctx context.Context, key string) bool, + keyLock func(ctx context.Context, key string) (bool, error), + keyUnlock func(ctx context.Context, key string), + keyRLock func(ctx context.Context, key string) (bool, error), + keyRUnlock func(ctx context.Context, key string), + createKeyAndLock func(ctx context.Context, key string) (bool, error), + getValue func(ctx context.Context, key string) interface{}, + setValue func(ctx context.Context, key string, value interface{}) error, + args ...string) ([]byte, error) { + + readKeys, _, err := KeyExtractionFunc(command, args...) + if err != nil { + return nil, err + } + key := readKeys[0] + + if !keyExists(ctx, key) { + return []byte(":0\r\n"), nil + } + + _, err = keyRLock(ctx, key) + if err != nil { + return nil, err + } + defer keyRUnlock(ctx, key) + + val, ok := getValue(ctx, key).(int64) + if !ok { + return nil, fmt.Errorf("value at key %s is not an integer", key) + } + + return []byte(fmt.Sprintf(":%d\r\n", val*val)), nil +} diff --git a/volumes/modules/module_get/module_get.so b/volumes/modules/module_get/module_get.so new file mode 100644 index 0000000..ff1accb Binary files /dev/null and b/volumes/modules/module_get/module_get.so differ diff --git a/volumes/modules/module_set/module_set.go b/volumes/modules/module_set/module_set.go new file mode 100644 index 0000000..257622a --- /dev/null +++ b/volumes/modules/module_set/module_set.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "fmt" + "strconv" +) + +var Command string = "Module.Set" + +var Categories []string = []string{"write", "fast"} + +var Description string = `(Module.Set key value) This module stores the given value at the specified key. +The value must be an integer` + +var Sync bool = true + +func KeyExtractionFunc(cmd []string, args ...string) ([]string, []string, error) { + if len(cmd) != 3 { + return nil, nil, fmt.Errorf("wrong no of args for %s command", Command) + } + return []string{}, cmd[1:2], nil +} + +func HandlerFunc( + ctx context.Context, + command []string, + keyExists func(ctx context.Context, key string) bool, + keyLock func(ctx context.Context, key string) (bool, error), + keyUnlock func(ctx context.Context, key string), + keyRLock func(ctx context.Context, key string) (bool, error), + keyRUnlock func(ctx context.Context, key string), + createKeyAndLock func(ctx context.Context, key string) (bool, error), + getValue func(ctx context.Context, key string) interface{}, + setValue func(ctx context.Context, key string, value interface{}) error, + args ...string) ([]byte, error) { + + _, writeKeys, err := KeyExtractionFunc(command, args...) + if err != nil { + return nil, err + } + key := writeKeys[0] + + if !keyExists(ctx, key) { + _, err := createKeyAndLock(ctx, key) + if err != nil { + return nil, err + } + } else { + _, err := keyLock(ctx, key) + if err != nil { + return nil, err + } + } + defer keyUnlock(ctx, key) + + value, err := strconv.ParseInt(command[2], 10, 64) + if err != nil { + return nil, err + } + + err = setValue(ctx, key, value) + if err != nil { + return nil, err + } + + return []byte("+OK\r\n"), nil +} diff --git a/volumes/modules/module_set/module_set.so b/volumes/modules/module_set/module_set.so new file mode 100644 index 0000000..996b991 Binary files /dev/null and b/volumes/modules/module_set/module_set.so differ